def fixVHbehaviour(self, elem): raw = Path(elem.get("d")).to_arrays() subpaths, prev = [], 0 for i in range(len(raw)): # Breaks compound paths into simple paths if raw[i][0] == 'M' and i != 0: subpaths.append(raw[prev:i]) prev = i subpaths.append(raw[prev:]) seg = [] for simpath in subpaths: if simpath[-1][0] == 'Z': simpath[-1][0] = 'L' if simpath[-2][0] == 'L': simpath[-1][1] = simpath[0][1] else: simpath.pop() for i in range(len(simpath)): if simpath[i][0] == 'V': # vertical and horizontal lines only have one point in args, but 2 are required #inkex.utils.debug(simpath[i][0]) simpath[i][0]='L' #overwrite V with regular L command add=simpath[i-1][1][0] #read the X value from previous segment simpath[i][1].append(simpath[i][1][0]) #add the second (missing) argument by taking argument from previous segment simpath[i][1][0]=add #replace with recent X after Y was appended if simpath[i][0] == 'H': # vertical and horizontal lines only have one point in args, but 2 are required #inkex.utils.debug(simpath[i][0]) simpath[i][0]='L' #overwrite H with regular L command simpath[i][1].append(simpath[i-1][1][1]) #add the second (missing) argument by taking argument from previous segment #inkex.utils.debug(simpath[i]) seg.append(simpath[i]) elem.set("d", Path(seg)) return seg
def effect(self): path_strings = [] net_strings = ["M"] my_path = etree.Element(inkex.addNS('path', 'svg')) s = { 'stroke-width': self.options.s_width, 'stroke': '#000000', 'fill': 'none' } my_path.set('style', str(inkex.Style(s))) for id, node in self.svg.selected.items(): if node.tag == inkex.addNS('path', 'svg'): d = node.get('d') p = CubicSuperPath(Path(d)) for subpath in p: for i, csp in enumerate(subpath): path_strings.append("%f,%f" % (csp[1][0], csp[1][1])) node.set('d', str(Path(p))) while len(path_strings) > 0: net_strings.append(path_strings.pop(0)) if len(path_strings) > 0: net_strings.append(path_strings.pop()) my_path.set('d', " ".join(net_strings)) self.svg.get_current_layer().append(my_path)
def effect(self): if len(self.svg.selected) == 0: exit("Please select at least one path.") for obj in self.svg.selected: # The objects are the paths, which may be compound if obj.tag == inkex.addNS('path','svg'): curr = self.svg.selected[obj] raw = Path(curr.get("d")).to_arrays() subpaths, prev = [], 0 for i in range(len(raw)): # Breaks compound paths into simple paths if raw[i][0] == 'M' and i != 0: subpaths.append(raw[prev:i]) prev = i subpaths.append(raw[prev:]) seg = [] for simpath in subpaths: if simpath[-1][0] == 'Z': simpath[-1][0] = 'L' if simpath[-2][0] == 'L': simpath[-1][1] = simpath[0][1] else: simpath.pop() for i in range(len(simpath)): if simpath[i][0] == 'V': # vertical and horizontal lines only have one point in args, but 2 are required #inkex.utils.debug(simpath[i][0]) simpath[i][0]='L' #overwrite V with regular L command add=simpath[i-1][1][0] #read the X value from previous segment simpath[i][1].append(simpath[i][1][0]) #add the second (missing) argument by taking argument from previous segment simpath[i][1][0]=add #replace with recent X after Y was appended if simpath[i][0] == 'H': # vertical and horizontal lines only have one point in args, but 2 are required #inkex.utils.debug(simpath[i][0]) simpath[i][0]='L' #overwrite H with regular L command simpath[i][1].append(simpath[i-1][1][1]) #add the second (missing) argument by taking argument from previous segment #inkex.utils.debug(simpath[i]) seg.append(simpath[i]) curr.set("d", Path(seg)) else: inkex.utils.debug("Object " + obj.get('id') + " is not a path. Please convert it to a path first.")
def effect(self): for id, node in self.svg.selected.items(): if node.tag == inkex.addNS('path', 'svg'): d = node.get('d') d = str(Path((Path(d).to_arrays()))) d = re.sub(r'(?i)(m[^mz]+)', r'\1 Z ', d) d = re.sub(r'(?i)\s*z\s*z\s*', r' Z ', d) node.set('d', d)
def test_closing(self): """Closing paths create two arrays""" path = Path("M 0,0 C 1.505,0 2.727,-0.823 2.727,-1.841 V -4.348 C 2.727,-5.363"\ " 1.505,-6.189 0,-6.189 H -8.3 V 0 Z m -10.713,1.991 h -0.211 V -8.178"\ " H 0 c 2.954,0 5.345,1.716 5.345,3.83 v 2.507 C 5.345,0.271 2.954,1.991" " 0,1.991 Z") csp = path.to_superpath() self.assertEqual(len(csp), 2)
def test_closing_without_z(self): """Closing paths without z create two arrays""" path = Path("m 51.553104,253.58572 c -11.644086,-0.14509 -4.683516,-19.48876"\ " 2.096523,-8.48973 1.722993,2.92995 0.781608,6.73867 -2.096523,8.48973"\ " m -3.100522,-13.02176 c -18.971587,17.33811 15.454875,20.05577"\ " 6.51412,3.75474 -1.362416,-2.30812 -3.856221,-3.74395 -6.51412,-3.75474") csp = path.to_superpath() self.assertEqual(len(csp), 2)
def getTranslatedPath(d, posX, posY): if(CommonDefs.inkVer == 1.0): path = Path(d) path.translate(posX, posY, inplace = True) return path.to_superpath().__str__() else: path = simplepath.parsePath(d) simplepath.translatePath(path, posX, posY) return simplepath.formatPath(path)
def test_extending(self): """Paths can be extended using addition""" ret = Path('M 20 20') + Path('L 40 40 9 10') self.assertEqual(type(ret), Path) self._assertPath(ret, 'M 20 20 L 40 40 L 9 10') ret = Path('M 20 20') + 'C 40 40 9 10 10 10' self.assertEqual(type(ret), Path) self._assertPath(ret, 'M 20 20 C 40 40 9 10 10 10')
def test_bounding_box_lines(self): """ Test the bounding box calculations A diagonal line from 20,20 to 90,90 then to +10,+10 "\" """ self.assertEqual((20, 100), (20, 100), Path('M 20,20 L 90,90 l 10,10 Z').bounding_box()) self.assertEqual((10, 90), (10, 90), Path('M 20,20 L 90,90 L 10,10 Z').bounding_box())
def recursiveFuseTransform(self, node, transf=[[1.0, 0.0, 0.0], [0.0, 1.0, 0.0]]): transf = Transform(transf) * Transform(node.get("transform", None)) if 'transform' in node.attrib: del node.attrib['transform'] node = ApplyTransform.objectToPath(node) if 'd' in node.attrib: d = node.get('d') p = CubicSuperPath(d) p = Path(p).to_absolute().transform(transf, True) node.set('d', Path(CubicSuperPath(p).to_path())) self.scaleStrokeWidth(node, transf) elif node.tag in [ inkex.addNS('polygon', 'svg'), inkex.addNS('polyline', 'svg') ]: points = node.get('points') points = points.strip().split(' ') for k, p in enumerate(points): if ',' in p: p = p.split(',') p = [float(p[0]), float(p[1])] applyTransformToPoint(transf, p) p = [str(p[0]), str(p[1])] p = ','.join(p) points[k] = p points = ' '.join(points) node.set('points', points) self.scaleStrokeWidth(node, transf) elif node.tag in [ inkex.addNS('rect', 'svg'), inkex.addNS('text', 'svg'), inkex.addNS('image', 'svg'), inkex.addNS('use', 'svg'), inkex.addNS('circle', 'svg') ]: # node.set('transform', str(Transform(transf))) inkex.utils.errormsg("Shape %s not yet supported" % node.tag) else: # e.g. <g style="..."> self.scaleStrokeWidth(node, transf) for child in node.getchildren(): self.recursiveFuseTransform(child, transf)
def test_inline_transformations(self): path = Path() self.assertTrue(path is not path.translate(10, 20)) self.assertTrue(path is not path.transform(Transform(scale=10))) self.assertTrue(path is not path.rotate(10)) self.assertTrue(path is not path.scale(10, 20)) self.assertTrue(path is path.translate(10, 20, inplace=True)) self.assertTrue( path is path.transform(Transform(scale=10), inplace=True)) self.assertTrue(path is path.rotate(10, inplace=True)) self.assertTrue(path is path.scale(10, 20, inplace=True))
def effect(self): unit_factor = 1.0 / self.svg.uutounit(1.0, self.options.unit) for id, node in self.svg.selected.items(): #get recent XY coordinates (top left corner of the bounding box) bbox = node.bounding_box() new_horiz_scale = self.options.expected_size * unit_factor / bbox.width new_vert_scale = self.options.expected_size * unit_factor / bbox.height if self.options.scale_type == "Horizontal": translation_matrix = [[new_horiz_scale, 0.0, 0.0], [0.0, 1.0, 0.0]] elif self.options.scale_type == "Vertical": translation_matrix = [[1.0, 0.0, 0.0], [0.0, new_vert_scale, 0.0]] else: #Uniform translation_matrix = [[new_horiz_scale, 0.0, 0.0], [0.0, new_vert_scale, 0.0]] node.transform = Transform(translation_matrix) * node.transform # now that the node moved we need to get the nodes XY coordinates again to fix them bbox_new = node.bounding_box() #inkex.utils.debug(cx) #inkex.utils.debug(cy) #inkex.utils.debug(cx_new) #inkex.utils.debug(cy_new) # We remove the transformation attribute from SVG XML tree in case it's regular path. In case the node is an object like arc,circle, ellipse or star we have the attribute "sodipodi:type". Then we do not play with it's path because the size transformation will not apply (this code block is ugly) if node.get('sodipodi:type') is None and 'd' in node.attrib: #inkex.utils.debug("it's a path!") d = node.get('d') p = CubicSuperPath(d) transf = Transform(node.get("transform", None)) if 'transform' in node.attrib: del node.attrib['transform'] p = Path(p).to_absolute().transform(transf, True) node.set('d', Path(CubicSuperPath(p).to_path())) #else: #inkex.utils.debug("it's an object!") #we perform second translation to reset the center of the path translation_matrix = [[ 1.0, 0.0, bbox.left - bbox_new.left + (bbox.width - bbox_new.width) / 2 ], [ 0.0, 1.0, bbox.top - bbox_new.top + (bbox.height - bbox_new.height) / 2 ]] node.transform = Transform(translation_matrix) * node.transform
def fill_row(self, node): #max_line_width = self.svg.unittouu('450mm') #x_start = self.svg.unittouu('3mm') #y_start = self.svg.unittouu('1600mm') - self.svg.unittouu('3mm') #gap_x = gap_y = self.svg.unittouu('10mm') svg = self.document.getroot() x_start = 0 y_start = self.svg.unittouu(svg.attrib['height']) max_line_width = self.svg.unittouu(svg.get('width')) total_width = 0 total_height = self.total_height group = etree.SubElement(self.svg.get_current_layer(), addNS('g', 'svg')) bbox = node.bounding_box() x = bbox.left y = bbox.top node_width = self.options.gap_x + bbox.width while total_width + node_width < max_line_width: node_copy = deepcopy(node) group.append(node_copy) x_dest = x_start + total_width y_dest = y_start - (total_height + bbox.height) # translation logic if node_copy.tag == addNS('path', 'svg'): x_delta = x_dest - x y_delta = y_dest - y path = Path(node_copy.attrib['d']) path.translate(x_delta, y_delta, True) node_copy.attrib['d'] = str(Path(path)) elif node_copy.tag == addNS('g', 'svg'): x_delta = x_dest - x y_delta = y_dest - y translation_matrix = [[1.0, 0.0, x_delta], [0.0, 1.0, y_delta]] Transform(translation_matrix) * node_copy.transform else: node_copy.attrib['x'] = str(x_dest) node_copy.attrib['y'] = str(y_dest) total_width += node_width self.total_height += group.bounding_box().height + self.options.gap_y
def test_is_line(self): """Test is super path segments can detect lines""" path = Path("m 49,88 70,-1 c 18,17 1,59 1.7,59 "\ "0,0 -48.7,18 -70.5,-1 18,-15 25,-32.4 -1.5,-57.2 z") csp = path.to_superpath() self.assertTrue(csp.is_line(csp[0][0], csp[0][1]), "Should be a line") self.assertFalse(csp.is_line(csp[0][3], csp[0][4]), "Both controls not detected") self.assertFalse(csp.is_line(csp[0][1], csp[0][2]), "Start control not detected") self.assertFalse(csp.is_line(csp[0][2], csp[0][3]), "End control not detected") # Also tests if zone close is applied correctly. self.assertEqual(str(csp.to_path()), "M 49 88 L 119 87 C 137 104 120 146 120.7 146 "\ "C 120.7 146 72 164 50.2 145 C 68.2 130 75.2 112.6 48.7 87.8 Z")
def test_relative(self): """Paths can be converted to relative""" ret = Path("M 100 100 L 110 120 140 140 300 300") self._assertPath(ret.to_relative(), "m 100 100 l 10 20 l 30 20 l 160 160") ret = Path("M 150,150 A 76,55 0 1 1 433,278") self._assertPath(ret.to_relative(), "m 150 150 a 76 55 0 1 1 283 128") ret = Path("M 1 2 H 3 V 3 Z M 5 2 H 7 V 3 Z M 5 4 H 7 V 5 Z") self._assertPath(ret.to_relative(), "m 1 2 h 2 v 1 z m 4 0 h 2 v 1 z m 0 2 h 2 v 1 z")
def morphPath(path, axes): bounds = [y for x in list(Path(path).bounding_box()) for y in list(x)] assert len(bounds) == 4 new_path = [] current = [0.0, 0.0] start = [0.0, 0.0] for cmd, params in path: segmentType = cmd points = params if segmentType == "M": start[0] = points[0] start[1] = points[1] segmentType = convertSegmentToCubic(current, segmentType, points, start) percentages = [0.0] * len(points) morphed = [0.0] * len(points) numPts = getNumPts(segmentType) normalizePoints(bounds, points, percentages, numPts) mapPointsToMorph(axes, percentages, morphed, numPts) addSegment(new_path, segmentType, morphed) if len(points) >= 2: current[0] = points[len(points) - 2] current[1] = points[len(points) - 1] return new_path
def path_round_it(self, node): if node.tag == inkex.addNS('path', 'svg'): d = node.get('d') p = CubicSuperPath(d) for subpath in p: for csp in subpath: delta = self.roundit(csp[1]) if self.options.along: csp[0][0] += delta[0] csp[0][1] += delta[1] csp[1][0] += delta[0] csp[1][1] += delta[1] if self.options.along: csp[2][0] += delta[0] csp[2][1] += delta[1] if self.options.ctrl: delta = self.roundit(csp[0]) csp[0][0] += delta[0] csp[0][1] += delta[1] delta = self.roundit(csp[2]) csp[2][0] += delta[0] csp[2][1] += delta[1] node.set('d', str(Path(p))) elif node.tag == inkex.addNS('g', 'svg'): for e in node: self.path_round_it(e)
def test_scale(self): """Paths can be scaled using the times operator""" ret = Path('M 10,10 L 30,30 C 20 20 10 10 10 10 l 10 10').scale(2.5, 3) self._assertPath(ret, 'M 25 30 L 75 90 C 50 60 25 30 25 30 l 25 30') ret = Path( "M 29.867708,101.68274 A 14.867708,14.867708 0 0 1 15,116.55045 14.867708," "14.867708 0 0 1 0.13229179,101.68274 14.867708,14.867708 0 0 1 15,86.815031 " "14.867708,14.867708 0 0 1 29.867708,101.68274 Z") ret = ret.scale(1.2, 0.8) self._assertPath( ret, 'M 35.8412 81.3462 ' 'A 17.8412 11.8942 0 0 1 18 93.2404 ' 'A 17.8412 11.8942 0 0 1 0.15875 81.3462 ' 'A 17.8412 11.8942 0 0 1 18 69.452 ' 'A 17.8412 11.8942 0 0 1 35.8412 81.3462 Z')
def breakContours(self, node): #this does the same as "CTRL + SHIFT + K" if node.tag == inkex.addNS('path', 'svg'): parent = node.getparent() idx = parent.index(node) idSuffix = 0 raw = Path(node.get("d")).to_arrays() subPaths, prev = [], 0 for i in range( len(raw)): # Breaks compound paths into simple paths if raw[i][0] == 'M' and i != 0: subPaths.append(raw[prev:i]) prev = i subPaths.append(raw[prev:]) for subpath in subPaths: replacedNode = copy.copy(node) oldId = replacedNode.get('id') replacedNode.set('d', CubicSuperPath(subpath)) replacedNode.set('id', oldId + str(idSuffix).zfill(5)) parent.insert(idx, replacedNode) idSuffix += 1 self.replacedNodes.append(replacedNode) node.delete() for child in node: self.breakContours(child)
def effect(self): if len(self.options.ids) < 2: raise Exception( "Two paths must be selected. The 1st is the letter, the 2nd is the envelope and must have 4 sides." ) exit() letterElement = self.svg.selected[self.options.ids[0]] envelopeElement = self.svg.selected[self.options.ids[1]] if letterElement.tag != inkex.addNS( 'path', 'svg') or envelopeElement.tag != inkex.addNS( 'path', 'svg'): raise Exception("Both letter and envelope must be SVG paths.") exit() axes = extractMorphAxes(Path(envelopeElement.get('d')).to_arrays()) if axes is None: raise Exception("No axes found on envelope.") axisCount = len(axes) if axisCount < 4: raise Exception("The envelope path has less than 4 segments.") for i in range(0, 4): if axes[i] is None: raise Exception("axes[%i] is None" % i) # morph the enveloped element according to the axes morph_element(letterElement, envelopeElement, axes)
def morph_path(path, axes): bounds = [ y for x in list(Path(paths.Path(path).to_superpath()).bounding_box()) for y in list(x) ] new_path = [] current = [0.0, 0.0] start = [0.0, 0.0] for cmd, params in path: segment_type = cmd points = params if segment_type == "M": start[0] = points[0] start[1] = points[1] segment_type = convert_segment_to_cubic(current, segment_type, points, start) percentages = [0.0] * len(points) morphed = [0.0] * len(points) num_points = get_num_points(segment_type) normalize_points(bounds, points, percentages, num_points) map_points_to_morph(axes, percentages, morphed, num_points) add_segment(new_path, segment_type, morphed) if len(points) >= 2: current[0] = points[len(points) - 2] current[1] = points[len(points) - 1] return new_path
def getCubicSuperPath(d = None): if(CommonDefs.inkVer == 1.0): if(d == None): return CubicSuperPath([]) return CubicSuperPath(Path(d).to_superpath()) else: if(d == None): return [] return CubicSuperPath(simplepath.parsePath(d))
def test_passthrough(self): """Create a path and test the re-rendering of the commands""" for path in ( 'M 50,50 L 10,10 m 10 10 l 2.1,2', 'm 150 150 c 10 10 6 6 20 10 L 10 10', ): self._assertPath(Path(path), path.replace(',', ' '))
def scaleCubicSuper(self, cspath, scaleFactor, scaleFrom): bbox = Path(cspath).bounding_box() if (scaleFrom == 'topLeft'): oldOrigin = [bbox.left, bbox.bottom] elif (scaleFrom == 'topRight'): oldOrigin = [bbox.right, bbox.bottom] elif (scaleFrom == 'bottomLeft'): oldOrigin = [bbox.left, bbox.top] elif (scaleFrom == 'bottomRight'): oldOrigin = [bbox.right, bbox.top] else: #if(scaleFrom == 'center'): oldOrigin = [ bbox.left + (bbox.right - bbox.left) / 2., bbox.bottom + (bbox.top - bbox.bottom) / 2. ] newOrigin = [oldOrigin[0] * scaleFactor, oldOrigin[1] * scaleFactor] for subpath in cspath: for bezierPt in subpath: for i in range(0, len(bezierPt)): bezierPt[i] = [ bezierPt[i][0] * scaleFactor, bezierPt[i][1] * scaleFactor ] bezierPt[i][0] += (oldOrigin[0] - newOrigin[0]) bezierPt[i][1] += (oldOrigin[1] - newOrigin[1])
def _prep(val): if val: if attr == 'd': from inkex.paths import Path return [attr] + Path(val).to_arrays() return (attr, val) return val
def test_control_points(self): """Test how x,y points are extracted""" for path, ret in ( ('M 100 100', ((100, 100), )), ('L 100 100', ((100, 100), )), ('H 133', ((133, 0), )), ('V 144', ((0, 144), )), ('Q 40 20 12 99 T 100 100', ( (40, 20), (12, 99), (-16, 178), (100, 100), )), ('C 12 12 15 15 20 20', ((12, 12), (15, 15), (20, 20))), ('S 50 90 30 10', ( (0, 0), (50, 90), (30, 10), )), ('Q 40 20 12 99', ( (40, 20), (12, 99), )), ('A 1,2,3,0,0,10,20', ((10, 20), )), ('Z', ((0, 0), )), ): points = list(Path(path).control_points) self.assertEqual(len(points), len(ret), msg=path) self.assertTrue(all(p.is_close(r) for p, r in zip(points, ret)), msg=path)
def convertPath(self, node): if node.tag == inkex.addNS('path', 'svg'): polypath = [] i = 0 for x, y in node.path.end_points: if i == 0: polypath.append(['M', [x, y]]) else: polypath.append(['L', [x, y]]) if i == 1 and polypath[len(polypath) - 2][1] == polypath[len(polypath) - 1][1]: polypath.pop( len(polypath) - 1 ) #special handling for the seconds point after M command elif polypath[len(polypath) - 2] == polypath[len(polypath) - 1]: #get the previous point polypath.pop(len(polypath) - 1) i += 1 node.set('d', str(Path(polypath))) children = node.getchildren() if children is not None: for child in children: self.convertPath(child)
def effect(self): elements = self.document.xpath('//svg:path', namespaces=inkex.NSS) for el in elements: oldpathstring = el.attrib['d'] nodes = Path(oldpathstring).to_arrays() currentSection = [] sections = [currentSection] for node in nodes: command = node.pop(0) currentSection.append( command + ' ' + ' '.join(list(map(lambda c: ','.join(map(str, c)), node)))) if command.lower() == 'z': currentSection = [] sections.append(currentSection) sections = list( map(lambda n: ' '.join(n), filter(lambda n: len(n) > 0, sections))) if (sections[-1][-2].lower() != 'z'): nonClosedSection = ' ' + sections.pop() else: nonClosedSection = '' sections = filter(lambda s: s[0].lower() != 'z', sections) sections = sorted(sections, key=getArea) newpathstring = "z ".join(sections) + nonClosedSection el.set('d', newpathstring)
def test_to_arrays(self): """Return the full path as a bunch of arrays""" ret = Path("M 100 100 L 110 120 H 20 C 120 0 6 10 10 2 Z").to_arrays() self.assertEqual(len(ret), 5) self.assertEqual(ret[0][0], 'M') self.assertEqual(ret[1][0], 'L') self.assertEqual(ret[2][0], 'L') self.assertEqual(ret[3][0], 'C')
def test_transform(self): """Transform by a whole matrix""" ret = Path("M 100 100 L 110 120 L 140 140 L 300 300") ret = ret.transform(Transform(translate=(10, 10))) self.assertEqual(str(ret), 'M 110 110 L 120 130 L 150 150 L 310 310') ret = ret.transform(Transform(translate=(-10, -10))) self.assertEqual(str(ret), 'M 100 100 L 110 120 L 140 140 L 300 300') ret = Path('M 5 5 H 10 V 15') ret = ret.transform(Transform(rotate=-10)) self.assertEqual( 'M 5.79228 4.0558 ' 'L 10.7163 3.18756 ' 'L 12.4528 13.0356', str(ret)) ret = Path("M 10 10 A 50,50 0 0 1 85.355333,85.355341 L 100 0") ret = ret.transform(Transform(scale=10)) self.assertEqual(str(ret), 'M 100 100 A 500 500 0 0 1 853.553 853.553 L 1000 0') self.assertRaises(ValueError, Horz([10]).transform, Transform())