| 1 | import numpy |
| 2 | import svg.path |
| 3 | import lxml.etree as ET |
| 4 | |
| 5 | ''' |
| 6 | SVG path linearization library (eg. for SVG2gcode use) |
| 7 | Made by Tomas 'Harvie' Mudrunka 2018 |
| 8 | |
| 9 | heavily based on SVG compression library |
| 10 | released under GPLv3? |
| 11 | by Gen Del Raye (July 22, 2014): https://pypi.org/project/SVGCompress/ |
| 12 | ''' |
| 13 | |
| 14 | class SVGcode: |
| 15 | |
| 16 | def __init__(self, svg_file): |
| 17 | ''' |
| 18 | Read svg from path 'svg_file' (e.g. 'test/test_vector.svg') |
| 19 | ''' |
| 20 | #self.filename, self.extension = svg_file.split('.') # Get filename and extension |
| 21 | #assert self.extension == 'svg', 'File must be an svg' |
| 22 | try: |
| 23 | self.figure_data = ET.parse(svg_file) # Parse svg data as xml |
| 24 | except ET.XMLSyntaxError, e: |
| 25 | ''' |
| 26 | Large svgs may trigger lxml's 'excessive depth in document' exception |
| 27 | ''' |
| 28 | warnings.warn('lxml error: %s - Trying huge xml parser' %(e.message)) |
| 29 | huge_parser = ET.XMLParser(huge_tree = True) |
| 30 | self.figure_data = ET.parse(svg_file, parser = huge_parser) |
| 31 | self.root = self.figure_data.getroot() # Root object in svg |
| 32 | |
| 33 | def find_paths(self): |
| 34 | ''' |
| 35 | Find and parse nodes in the xml that correspond to paths in the svg |
| 36 | ''' |
| 37 | tag_prefix = '{*}' |
| 38 | self.path_nodes = self.root.findall('.//%spath' %(tag_prefix)) |
| 39 | self.paths = list() |
| 40 | self.paths = [svg.path.parse_path(p.attrib.get('d', 'M 0,0 z')) for p in self.path_nodes] |
| 41 | |
| 42 | def linearize_paths(self, curve_fidelity = 10): |
| 43 | ''' |
| 44 | Turn svg paths into discrete lines |
| 45 | Inputs: |
| 46 | curve_fidelity(int) - number of lines with which to approximate curves |
| 47 | in svg. Higher values necessitates longer computation time. |
| 48 | ''' |
| 49 | self.linear_coords = [self.linearize(p, curve_fidelity) for p in self.paths] |
| 50 | |
| 51 | |
| 52 | def linearize_line(self, segment, n_interpolate = None): |
| 53 | ''' |
| 54 | Turn svg line into set of coordinates by returning |
| 55 | start and end coordinates of the line segment. |
| 56 | n_interpolate is only used for consistency of use |
| 57 | with linearize_curve() |
| 58 | ''' |
| 59 | return numpy.array([segment.start, segment.end]) |
| 60 | |
| 61 | def linearize_curve(self, segment, n_interpolate = 10): |
| 62 | ''' |
| 63 | Estimate svg curve (e.g. Bezier, Arc, etc.) using |
| 64 | a set of n discrete lines. n_interpolate sets the |
| 65 | number of discrete lines per curve. |
| 66 | ''' |
| 67 | interpolation_pts = numpy.linspace(0, 1, n_interpolate, endpoint = False)[1:] |
| 68 | interpolated = numpy.zeros(n_interpolate + 1, dtype = complex) |
| 69 | interpolated[0] = segment.start |
| 70 | interpolated[-1] = segment.end |
| 71 | for i, pt in enumerate(interpolation_pts): |
| 72 | interpolated[i + 1] = segment.point(pt) |
| 73 | return interpolated |
| 74 | |
| 75 | def complex2coord(self, complexnum): |
| 76 | return (complexnum.real, complexnum.imag) |
| 77 | |
| 78 | def linearize(self, path, n_interpolate = 10): |
| 79 | segmenttype2func = {'CubicBezier': self.linearize_curve, |
| 80 | 'Line': self.linearize_line, |
| 81 | 'QuadraticBezier': self.linearize_curve, |
| 82 | 'Arc': self.linearize_curve} |
| 83 | ''' |
| 84 | More sophisticated linearization option |
| 85 | compared to endpts2line(). |
| 86 | Turn svg path into discrete coordinates |
| 87 | with number of coordinates per curve set |
| 88 | by n_interpolate. i.e. if n_interpolate |
| 89 | is 100, each curve is approximated by |
| 90 | 100 discrete lines. |
| 91 | ''' |
| 92 | segments = path._segments |
| 93 | complex_coords = list() |
| 94 | for segment in segments: |
| 95 | # Output coordinates for each segment, minus last point (because |
| 96 | # point is same as first point of next segment) |
| 97 | segment_type = type(segment).__name__ |
| 98 | segment_linearize = segmenttype2func[segment_type] |
| 99 | linearized = segment_linearize(segment, n_interpolate) |
| 100 | complex_coords.extend(linearized[:-1]) |
| 101 | # Append last point of final segment to close the polygon |
| 102 | complex_coords.append(linearized[-1]) |
| 103 | return [self.complex2coord(complexnum) for complexnum in complex_coords] # Output list of (x, y) tuples |