Convert a SVG image to multiple vector line segmen

2019-02-21 01:44发布

问题:

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

回答1:

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