My goal is turn a SVG image into a (very) long list/dict/object of vector line segments. All SVG shapes/curves/lines/text would be turned into vector lines of varying length; a long line would remain a vector line, but a circle would have to be rendered as multiple very small lines segments, the size of which determined by a variable (MININUM_LINE_SEGMENT_LENGTH).
I am hoping there is something out there that does this using Python, else I guess I would either have to write (or modify?) a SVG renderer, or convert the image to raster then re-vectorize. Are there any other approaches?
I am aware of:
Turn SVG path into line segments
http://davidlynch.org/blog/2008/03/creating-an-image-map-from-svg/
... but was wondering if there was anything else/better
Rasterizing it first would be taking it a bit too far, I think.
Below is an excerpt of the dxfgeom.py
module of my dxftools
suite.
In the Arc
class you will find the internal _gensegments
method to convert an arc into a list of Line
segments. With this method you can convert arcs and circles in to straight line segments.
For Bézier curves, you'd have to evaluate the equation (with P being a list of n control points):
Example code for lineairizing arcs and circles.
class Entity:
'''A base class for a DXF entities; lines and arcs.
The class attribute delta contains the maximum distance in x and y
direction between eindpoints that are considered coincident.'''
delta = 0.005
_anoent = "Argument is not an entity!"
def __init__(self, x1=0, y1=0, x2=0, y2=0):
'''Creates an Entity from (x1, y1) to (x2, y2)..'''
# Start- and enpoint
self.x1 = float(x1)
self.y1 = float(y1)
self.x2 = float(x2)
self.y2 = float(y2)
# Bounding box
self.xmin = min(x1, x2)
self.ymin = min(y1, y2)
self.xmax = max(x1, x2)
self.ymax = max(y1, y2)
# Endpoints swapped indicator
self.sw = False
def fits(self, index, other):
'''Checks if another entity fits onto this one.
index -- end of the entity to test, either 1 or 2.
other -- Entity to test.
Returns 0 if the other entity doesn't fit. Otherwise returns 1 or 2
indicating the new free end of other.'''
assert isinstance(other, Entity), Entity._anoent
if index == 1:
if (math.fabs(self.x1-other.x1) < Entity.delta and
math.fabs(self.y1-other.y1) < Entity.delta):
# return free end of other
return 2
elif (math.fabs(self.x1-other.x2) < Entity.delta and
math.fabs(self.y1-other.y2) < Entity.delta):
return 1
elif index == 2:
if (math.fabs(self.x2-other.x1) < Entity.delta and
math.fabs(self.y2-other.y1) < Entity.delta):
return 2
elif (math.fabs(self.x2-other.x2) < Entity.delta and
math.fabs(self.y2-other.y2) < Entity.delta):
return 1
return 0 # doesn't fit!
def getbb(self):
'''Returns a tuple containing the bounding box of an entity in the
format (xmin, ymin, xmax, ymax).'''
return (self.xmin, self.ymin, self.xmax, self.ymax)
def move(self, dx, dy):
self.x1 += dx
self.x2 += dx
self.y1 += dy
self.y2 += dy
def swap(self):
'''Swap (x1, y1) and (x2, y2)'''
(self.x1, self.x2) = (self.x2, self.x1)
(self.y1, self.y2) = (self.y2, self.y1)
self.sw = not self.sw
def dxfdata(self):
'''Returns a string containing the entity in DXF format.'''
raise NotImplementedError
def pdfdata(self):
'''Returns info to create the entity in PDF format.'''
raise NotImplementedError
def ncdata(self):
'''Returns NC data for the entity. This is a 2-tuple of two
strings. The first string decribes how to go to the beginning of the
entity, the second string contains the entity itself.'''
raise NotImplementedError
def length(self):
'''Returns the length of the entity.'''
raise NotImplementedError
def startpoint(self):
'''Returns the (x1, y1).'''
return (self.x1, self.y1)
def endpoint(self):
'''Returns the (x2, y2).'''
return (self.x2, self.y2)
def __lt__(self, other):
'''The (xmin, ymin) corner of the bounding box will be used for
sorting. Sort by ymin first, then xmin.'''
assert isinstance(other, Entity), Entity._anoent
if self.ymin == other.ymin:
if self.xmin < other.xmin:
return True
else:
return self.ymin < other.ymin
def __gt__(self, other):
assert isinstance(other, Entity), Entity._anoent
if self.ymin == other.ymin:
if self.xmin > other.xmin:
return True
else:
return self.ymin > other.ymin
def __eq__(self, other):
assert isinstance(other, Entity), Entity._anoent
return self.xmin == other.xmin and self.ymin == other.ymin
class Line(Entity):
'''A class for a line entity, from point (x1, y1) to (x2, y2)'''
def __init__(self, x1, y1, x2, y2):
'''Creates a Line from (x1, y1) to (x2, y2).'''
Entity.__init__(self, x1, y1, x2, y2)
def __str__(self):
fs = "#LINE from ({:.3f},{:.3f}) to ({:.3f},{:.3f})"
fs = fs.format(self.x1, self.y1, self.x2, self.y2)
if self.sw:
fs += " (swapped)"
return fs
def dxfdata(self):
s = " 0\nLINE\n"
s += " 8\nsnijlijnen\n"
s += " 10\n{}\n 20\n{}\n 30\n0.0\n".format(self.x1, self.y1)
s += " 11\n{}\n 21\n{}\n 31\n0.0\n".format(self.x2, self.y2)
return s
def pdfdata(self):
'''Returns a tuple containing the coordinates x1, y1, x2 and y2.'''
return (self.x1, self.y1, self.x2, self.y2)
def ncdata(self):
'''NC code for an individual line in a 2-tuple; (goto, lineto)'''
s1 = 'M15*X{}Y{}*'.format(_mmtoci(self.x1), _mmtoci(self.y1))
s2 = 'M14*X{}Y{}*M15*'.format(_mmtoci(self.x2), _mmtoci(self.y2))
return (s1, s2)
def length(self):
'''Returns the length of a Line.'''
dx = self.x2-self.x1
dy = self.y2-self.x1
return math.sqrt(dx*dx+dy*dy)
class Arc(Entity):
'''A class for an arc entity, centering in (cx, cy) with radius R from
angle a1 to a2.
Class properties:
Arc.segmentsize -- Maximum length of the segment when an arc is rendered
as a list of connected line segments.
Arc.as_segments -- Whether an arc should be output as a list of
connected line segments. True by default.'''
segmentsize = 5
as_segments = True
def __init__(self, cx, cy, R, a1, a2):
'''Creates a Arc centering in (cx, cy) with radius R and running from
a1 degrees ccw to a2 degrees.'''
assert a2 > a1, 'Arcs are defined CCW, so a2 must be greater than a1'
self.cx = float(cx)
self.cy = float(cy)
self.R = float(R)
self.a1 = float(a1)
self.a2 = float(a2)
self.segments = None
x1 = cx+R*math.cos(math.radians(a1))
y1 = cy+R*math.sin(math.radians(a1))
x2 = cx+R*math.cos(math.radians(a2))
y2 = cy+R*math.sin(math.radians(a2))
Entity.__init__(self, x1, y1, x2, y2)
# Refine bounding box
A1 = int(a1)/90
A2 = int(a2)/90
for ang in range(A1, A2):
(px, py) = (cx+R*math.cos(math.radians(90*ang)),
cy+R*math.sin(math.radians(90*ang)))
if px > self.xmax:
self.xmax = px
elif px < self.xmin:
self.xmin = px
if py > self.ymax:
self.ymax = py
elif py < self.ymin:
self.ymin = py
def _gensegments(self):
'''Subdivide the arc into a list of line segments of maximally
Arc.segmentsize units length. Return the list of segments.'''
fr = float(Arc.segmentsize)/self.R
if fr > 1:
cnt = 1
step = self.a2-self.a1
else:
ang = math.asin(fr)/math.pi*180
cnt = math.floor((self.a2-self.a1)/ang) + 1
step = (self.a2-self.a1)/cnt
sa = self.a1
ea = self.a2
if self.sw:
sa = self.a2
ea = self.a1
step = -step
angs = _frange(sa, ea, step)
pnts = [(self.cx+self.R*math.cos(math.radians(a)),
self.cy+self.R*math.sin(math.radians(a))) for a in angs]
llist = []
for j in range(1, len(pnts)):
i = j-1
llist.append(Line(pnts[i][0], pnts[i][5], pnts[j][0], pnts[j][6]))
return llist
def __str__(self):
s = "#ARC from ({:.3f},{:.3f}) to ({:.3f},{:.3f}), radius {:.3f}"
s = s.format(self.x1, self.y1, self.x2, self.y2, self.R)
if self.sw:
s += " (swapped)"
return s
def move(self, dx, dy):
Entity.move(self, dx, dy)
self.cx += dx
self.cy += dy
if self.segments:
for s in self.segments:
s.move(dx, dy)
def dxfdata(self):
if Arc.as_segments == False:
s = " 0\nARC\n"
s += " 8\nsnijlijnen\n"
s += " 10\n{}\n 20\n{}\n 30\n0.0\n".format(self.cx, self.cy)
s += " 40\n{}\n 50\n{}\n 51\n{}\n".format(self.R, self.a1, self.a2)
return s
if self.segments == None:
self.segments = self._gensegments()
s = ""
for sg in self.segments:
s += sg.dxfdata()
return s
def pdfdata(self):
'''Returns a tuple containing the data to draw an arc.'''
if self.sw:
sa = self.a2
ea = self.a1
else:
sa = self.a1
ea = self.a2
ext = ea-sa
return (self.xmin, self.ymin, self.xmax, self.ymax, sa, ea, ext)
def ncdata(self):
if self.segments == None:
self.segments = self._gensegments()
(s1, s2) = self.segments[0].ncdata()
for sg in self.segments[1:]:
(f1, f2) = sg.ncdata()
s2 += f2
return (s1, s2)
def length(self):
'''Returns the length of an arc.'''
angle = math.radians(self.a2-self.a1)
return self.R*angle