From 5f557e141e460038a7101d3d586cc1303a9cf8df Mon Sep 17 00:00:00 2001 From: Kristaps Karlsons Date: Sat, 31 Jan 2015 22:14:28 +0200 Subject: [PATCH 1/2] Python2 -> Python3 conversion without backwards compatibility. --- cleanSVG.py | 87 +++++++++++++++++++++++++++-------------------------- 1 file changed, 44 insertions(+), 43 deletions(-) diff --git a/cleanSVG.py b/cleanSVG.py index c8abaa4..ce3e9aa 100644 --- a/cleanSVG.py +++ b/cleanSVG.py @@ -6,13 +6,13 @@ import sys # Regex -re_transform = re.compile('([a-zA-Z]+)\((-?\d+\.?\d*),?\s*(-?\d+\.?\d*)?\)') -re_translate = re.compile('\((-?\d+\.?\d*)\s*,?\s*(-?\d+\.?\d*)\)') -re_coord_split = re.compile('\s+|,') -re_path_coords = re.compile('[a-zA-Z]') -re_path_split = re.compile('([ACHLMQSTVZachlmqstvz])') -re_trailing_zeros = re.compile('\.(\d*?)(0+)$') -re_length = re.compile('^(\d+\.?\d*)\s*(em|ex|px|in|cm|mm|pt|pc|%|\w*)') +re_transform = re.compile(r'([a-zA-Z]+)\((-?\d+\.?\d*),?\s*(-?\d+\.?\d*)?\)') +re_translate = re.compile(r'\((-?\d+\.?\d*)\s*,?\s*(-?\d+\.?\d*)\)') +re_coord_split = re.compile(r'\s+|,') +re_path_coords = re.compile(r'[a-zA-Z]') +re_path_split = re.compile(r'([ACHLMQSTVZachlmqstvz])') +re_trailing_zeros = re.compile(r'\.(\d*?)(0+)$') +re_length = re.compile(r'^(\d+\.?\d*)\s*(em|ex|px|in|cm|mm|pt|pc|%|\w*)') # Path commands path_commands = { @@ -138,7 +138,7 @@ def parseFile(self, filename): try: self.tree = etree.parse(filename) except IOError: - print "Unable to open file", filename + print("Unable to open file", filename) sys.exit(1) self.root = self.tree.getroot() @@ -146,29 +146,29 @@ def parseFile(self, filename): def analyse(self): """ Search for namespaces. Will do more later """ - print "Namespaces:" - for ns, link in self.root.nsmap.iteritems(): - print " %s: %s" % (ns, link) + print("Namespaces:") + for ns, link in self.root.nsmap.items(): + print(" %s: %s" % (ns, link)) def removeGroups(self): """ Remove groups with no attributes """ # Doesn't work for nested groups for element in self.tree.iter(): - if not isinstance(element.tag, basestring): + if not isinstance(element.tag, str): continue element_type = element.tag.split('}')[1] - if element_type == 'g' and not element.keys(): + if element_type == 'g' and not list(element.keys()): parent = element.getparent() if parent is not None: parent_postion = parent.index(element) - print - print parent + print() + print(parent) # Move children outside of group for i, child in enumerate(element, parent_postion): - print i - print "move %s to %s" % (child, i) + print(i) + print("move %s to %s" % (child, i)) parent.insert(i, child) #del parent[i] @@ -189,11 +189,12 @@ def toString(self, pretty_print=False): self._addStyleElement() if self.removeWhitespace: - svg_string = etree.tostring(self.root) - svg_string = re.sub(r'\n\s*' , "", svg_string) + svg_binary = etree.tostring(self.root) + svg_binary = re.sub(b'\n\s*', b"", svg_binary) else: - svg_string = etree.tostring(self.root, pretty_print=pretty_print) - + svg_binary = etree.tostring(self.root, pretty_print=pretty_print) + svg_string = svg_binary.decode('utf-8') + return svg_string def _addStyleElement(self): @@ -204,7 +205,7 @@ def _addStyleElement(self): self.root.insert(0, style_element) style_text = '\n' - for styles, style_class in sorted(self.styles.iteritems(), key=lambda (k,v): v): + for styles, style_class in sorted(iter(self.styles.items()), key=lambda k_v: k_v[1]): style_text += "\t.%s{\n" % style_class for (style_id, style_value) in styles: style_text += '\t\t%s:\t%s;\n' % (style_id, style_value) @@ -218,7 +219,7 @@ def setDecimalPlaces(self, decimal_places): self.num_format = "%%.%df" % decimal_places for element in self.tree.iter(): - if not isinstance(element.tag, basestring): + if not isinstance(element.tag, str): continue tag = element.tag.split('}')[1] @@ -230,11 +231,11 @@ def setDecimalPlaces(self, decimal_places): point_list = " ".join((formatted_values[i] + "," + formatted_values[i+1] for i in range(0, len(formatted_values), 2))) element.set("points", point_list) except IndexError: - print "Could not parse points list" + print("Could not parse points list") pass elif tag == "path": - coords = map(self._formatNumber, re_coord_split.split(element.get("d"))) + coords = list(map(self._formatNumber, re_coord_split.split(element.get("d")))) coord_list = " ".join(coords) element.set("d", coord_list) #for coord in coords: @@ -242,7 +243,7 @@ def setDecimalPlaces(self, decimal_places): # print coord else: - for attribute in element.attrib.keys(): + for attribute in list(element.attrib.keys()): if attribute in value_attributes: element.set(attribute, self._formatNumber(element.get(attribute))) @@ -251,11 +252,11 @@ def removeAttribute(self, attribute, exception_list=None): if exception_list is None: exception_list = [] - if self._verbose: print '\nRemoving attribute: %s' % attribute + if self._verbose: print('\nRemoving attribute: %s' % attribute) for element in self.tree.iter(): - if attribute in element.attrib.keys() and element.attrib[attribute] not in exception_list: - if self._verbose: print ' - Removed attribute: %s="%s"' % (attribute, element.attrib[attribute]) + if attribute in list(element.attrib.keys()) and element.attrib[attribute] not in exception_list: + if self._verbose: print(' - Removed attribute: %s="%s"' % (attribute, element.attrib[attribute])) del element.attrib[attribute] def removeNonDefIDAttributes(self): @@ -264,13 +265,13 @@ def removeNonDefIDAttributes(self): def_IDs = [] for element in self.tree.iter(): - if not isinstance(element.tag, basestring): + if not isinstance(element.tag, str): continue tag = element.tag.split('}')[1] if tag == 'defs': for child in element.getchildren(): - for key, value in child.attrib.iteritems(): + for key, value in child.attrib.items(): if key.endswith('href'): def_IDs.append(value) @@ -282,9 +283,9 @@ def removeNamespace(self, namespace): nslink = self.root.nsmap.get(namespace) if self._verbose: - print "\nRemoving namespace, %s" % namespace + print("\nRemoving namespace, %s" % namespace) if nslink: - print " - Link: %s" % nslink + print(" - Link: %s" % nslink) if nslink: nslink = "{%s}" % nslink @@ -294,13 +295,13 @@ def removeNamespace(self, namespace): if element.tag[:length] == nslink: self.root.remove(element) if self._verbose: - print " - removed element: %s" % element.tag[length:] + print(" - removed element: %s" % element.tag[length:]) - for attribute in element.attrib.keys(): + for attribute in list(element.attrib.keys()): if attribute[:length] == nslink: del element.attrib[attribute] if self._verbose: - print " - removed attribute from tag: %s" % element.tag + print(" - removed attribute from tag: %s" % element.tag) del self.root.nsmap[namespace] @@ -310,7 +311,7 @@ def extractStyles(self): for element in self.tree.iter(): style_list = [] - if "style" in element.keys(): + if "style" in list(element.keys()): styles = element.attrib["style"].split(';') style_list.extend([tuple(style.split(':')) for style in styles]) del element.attrib["style"] @@ -349,7 +350,7 @@ def applyTransforms(self): """ Apply transforms to element coordinates. """ for element in self.tree.iter(): - if 'transform' in element.keys(): + if 'transform' in list(element.keys()): all_transforms = element.get('transform') transform_list = re_transform.findall(all_transforms) transform_list.reverse() @@ -373,7 +374,7 @@ def applyTransforms(self): def _applyGroupTransforms(self, group_element, transformations): # Ensure all child elements are paths - children = [child for child in group_element if isinstance(child.tag, basestring)] + children = [child for child in group_element if isinstance(child.tag, str)] if any((child.tag.split('}')[1] != 'path' for child in children)): return @@ -421,14 +422,14 @@ def _translateElement(self, element, delta): element.set(coord_name, self._formatNumber(new_coord)) return True - elif "points" in element.keys(): + elif "points" in list(element.keys()): values = [float(v) + delta[i % 2] for i, v in enumerate(re_coord_split.split(element.get("points")))] - str_values = map(self._formatNumber, values) + str_values = list(map(self._formatNumber, values)) point_list = " ".join((str_values[i] + "," + str_values[i+1] for i in range(0, len(str_values), 2))) element.set("points", point_list) return True - elif "d" in element.keys(): + elif "d" in list(element.keys()): self._translatePath(element, delta) return True @@ -445,7 +446,7 @@ def _scaleElement(self, element, delta): element.set(coord_name, self._formatNumber(new_coord)) return True - elif "d" in element.keys(): + elif "d" in list(element.keys()): self._scalePath(element, delta) return True From ae69b114a4f402015d5cb8356fc82bd8cc6919e0 Mon Sep 17 00:00:00 2001 From: Kristaps Karlsons Date: Sat, 31 Jan 2015 22:48:30 +0200 Subject: [PATCH 2/2] General cleanup. Adjusted code to conform to pep8 standard. Removed trailing whitespace. --- cleanSVG.py | 458 +++++++++++++++++++++++++++------------------------- example.py | 10 +- 2 files changed, 239 insertions(+), 229 deletions(-) diff --git a/cleanSVG.py b/cleanSVG.py index ce3e9aa..34484ba 100644 --- a/cleanSVG.py +++ b/cleanSVG.py @@ -8,8 +8,8 @@ # Regex re_transform = re.compile(r'([a-zA-Z]+)\((-?\d+\.?\d*),?\s*(-?\d+\.?\d*)?\)') re_translate = re.compile(r'\((-?\d+\.?\d*)\s*,?\s*(-?\d+\.?\d*)\)') -re_coord_split = re.compile(r'\s+|,') -re_path_coords = re.compile(r'[a-zA-Z]') +re_coordinates_split = re.compile(r'\s+|,') +re_path_coordinates = re.compile(r'[a-zA-Z]') re_path_split = re.compile(r'([ACHLMQSTVZachlmqstvz])') re_trailing_zeros = re.compile(r'\.(\d*?)(0+)$') re_length = re.compile(r'^(\d+\.?\d*)\s*(em|ex|px|in|cm|mm|pt|pc|%|\w*)') @@ -19,10 +19,10 @@ "M": (0, 1), "L": (0, 1), "T": (0, 1), - "H": (0), - "V": (1), - "A": (-1, -1, -1, -1, -1, 0, 1), - "C": (0, 1, 0, 1, 0, 1) + "H": (0,), + "V": (1,), + "A": (-1, -1, -1, -1, -1, 0, 1), + "C": (0, 1, 0, 1, 0, 1), } # How relative commands are scaled @@ -30,16 +30,16 @@ "m": (0, 1), "l": (0, 1), "t": (0, 1), - "h": (0), - "v": (1), + "h": (0,), + "v": (1,), "a": (0, 1, -1, -1, -1, 0, 1), - "c": (0, 1, 0, 1, 0, 1) + "c": (0, 1, 0, 1, 0, 1), } scale_commands.update(path_commands) # Attribute names value_attributes = ["x", "y", "x1", "y1", "x2", "y2", "cx", "cy", "r", "rx", "ry", "width", "height"] -default_styles = set([ +default_styles = { ("opacity", "1"), ("fill-opacity", "1"), ("stroke", "none"), @@ -54,87 +54,92 @@ ("font-style", "normal"), ("font-weight", "normal"), ("font-stretch", "normal"), - ("font-variant", "normal") -]) - -position_attributes = {"rect": (["x", "y"]), - "tspan": (["x", "y"]), - "circle": (["cx", "cy"]), - "ellipse": (["cx", "cy"]), - "line": (["x1", "y1", "x2", "y2"])} - -scaling_attributes = {"rect": (["x", "y", "width", "height"]),} - -STYLES = set([ -"alignment-baseline", -"baseline-shift", -"clip-path", -"clip-rule", -"color-interpolation", -"color-interpolation-filters", -"color-profile", -"color-rendering", -"direction", -"dominant-baseline", -"fill", -"fill-opacity", -"fill-rule", -"font", -"font-family", -"font-size", -"font-size-adjust", -"font-stretch", -"font-style", -"font-variant", -"font-weight", -"glyph-orientation-horizontal", -"glyph-orientation-vertical", -"image-rendering", -"kerning", -"letter-spacing", -"marker", -"marker-end", -"marker-mid", -"marker-start", -"mask", -"opacity", -"pointer-events", -"shape-rendering", -"stop-color", -"stop-opacity", -"stroke", -"stroke-dasharray", -"stroke-dashoffset", -"stroke-linecap", -"stroke-linejoin", -"stroke-miterlimit", -"stroke-opacity", -"stroke-width", -"text-anchor", -"text-decoration", -"text-rendering", -"unicode-bidi", -"word-spacing", -"writing-mode", -]) + ("font-variant", "normal"), +} + +position_attributes = { + "rect": (["x", "y"]), + "tspan": (["x", "y"]), + "circle": (["cx", "cy"]), + "ellipse": (["cx", "cy"]), + "line": (["x1", "y1", "x2", "y2"]) +} + +scaling_attributes = { + "rect": (["x", "y", "width", "height"]), +} + +STYLES = { + "alignment-baseline", + "baseline-shift", + "clip-path", + "clip-rule", + "color-interpolation", + "color-interpolation-filters", + "color-profile", + "color-rendering", + "direction", + "dominant-baseline", + "fill", + "fill-opacity", + "fill-rule", + "font", + "font-family", + "font-size", + "font-size-adjust", + "font-stretch", + "font-style", + "font-variant", + "font-weight", + "glyph-orientation-horizontal", + "glyph-orientation-vertical", + "image-rendering", + "kerning", + "letter-spacing", + "marker", + "marker-end", + "marker-mid", + "marker-start", + "mask", + "opacity", + "pointer-events", + "shape-rendering", + "stop-color", + "stop-opacity", + "stroke", + "stroke-dasharray", + "stroke-dashoffset", + "stroke-linecap", + "stroke-linejoin", + "stroke-miterlimit", + "stroke-opacity", + "stroke-width", + "text-anchor", + "text-decoration", + "text-rendering", + "unicode-bidi", + "word-spacing", + "writing-mode", +} + class CleanSVG: def __init__(self, svgfile=None, verbose=False): self._verbose = verbose self.tree = None self.root = None - + # Need to update this if style elements found self.styles = {} self.style_counter = 0 - + self.num_format = "%s" self.removeWhitespace = True - + if svgfile: - self.parseFile(svgfile) - - def parseFile(self, filename): + self.parse_file(svgfile) + + def parse_file(self, filename): try: self.tree = etree.parse(filename) except IOError: @@ -142,22 +147,22 @@ def parseFile(self, filename): sys.exit(1) self.root = self.tree.getroot() - + def analyse(self): """ Search for namespaces. Will do more later """ - + print("Namespaces:") for ns, link in self.root.nsmap.items(): print(" %s: %s" % (ns, link)) - - def removeGroups(self): + + def remove_groups(self): """ Remove groups with no attributes """ # Doesn't work for nested groups - + for element in self.tree.iter(): if not isinstance(element.tag, str): continue - + element_type = element.tag.split('}')[1] if element_type == 'g' and not list(element.keys()): parent = element.getparent() @@ -170,23 +175,23 @@ def removeGroups(self): print(i) print("move %s to %s" % (child, i)) parent.insert(i, child) - - #del parent[i] - + + # del parent[i] + def write(self, filename): """ Write current SVG to a file. """ - + if not filename.endswith('.svg'): filename += '.svg' - + with open(filename, 'w') as f: - f.write(self.toString(True)) - - def toString(self, pretty_print=False): + f.write(self.to_string(True)) + + def to_string(self, pretty_print=False): """ Return a string of the current SVG """ - + if self.styles: - self._addStyleElement() + self._add_style_element() if self.removeWhitespace: svg_binary = etree.tostring(self.root) @@ -196,90 +201,95 @@ def toString(self, pretty_print=False): svg_string = svg_binary.decode('utf-8') return svg_string - - def _addStyleElement(self): - """ Insert a CSS style element containing information + + def _add_style_element(self): + """ Insert a CSS style element containing information from self.styles to the top of the file. """ - + style_element = etree.SubElement(self.root, "style") self.root.insert(0, style_element) style_text = '\n' - + for styles, style_class in sorted(iter(self.styles.items()), key=lambda k_v: k_v[1]): style_text += "\t.%s{\n" % style_class for (style_id, style_value) in styles: style_text += '\t\t%s:\t%s;\n' % (style_id, style_value) style_text += "\t}\n" - + style_element.text = style_text - - def setDecimalPlaces(self, decimal_places): + + def set_decimal_places(self, decimal_places): """ Round attribute numbers to a given number of decimal places. """ - + self.num_format = "%%.%df" % decimal_places - + for element in self.tree.iter(): if not isinstance(element.tag, str): continue - + tag = element.tag.split('}')[1] - + if tag == "polyline" or tag == "polygon": - values = re_coord_split.split(element.get("points")) - formatted_values = [self._formatNumber(x) for x in values if x] + values = re_coordinates_split.split(element.get("points")) + formatted_values = [self._format_number(x) for x in values if x] try: - point_list = " ".join((formatted_values[i] + "," + formatted_values[i+1] for i in range(0, len(formatted_values), 2))) + point_list = " ".join( + (formatted_values[i] + "," + formatted_values[i+1] for i in range(0, len(formatted_values), 2)) + ) element.set("points", point_list) except IndexError: print("Could not parse points list") pass - + elif tag == "path": - coords = list(map(self._formatNumber, re_coord_split.split(element.get("d")))) - coord_list = " ".join(coords) - element.set("d", coord_list) - #for coord in coords: - # if re_path_coords.match(coord): - # print coord - + coordinates = list(map(self._format_number, re_coordinates_split.split(element.get("d")))) + coordinates_list = " ".join(coordinates) + element.set("d", coordinates_list) + # for coordinate in coordinates: + # if re_path_coordinates.match(coord): + # print coordinate + else: for attribute in list(element.attrib.keys()): if attribute in value_attributes: - element.set(attribute, self._formatNumber(element.get(attribute))) + element.set(attribute, self._format_number(element.get(attribute))) - def removeAttribute(self, attribute, exception_list=None): + def remove_attribute(self, attribute, exception_list=None): """ Remove all instances of an attribute ignoring any with a value in the exception list. """ - if exception_list is None: exception_list = [] + if exception_list is None: + exception_list = [] - if self._verbose: print('\nRemoving attribute: %s' % attribute) + if self._verbose: + print('\nRemoving attribute: %s' % attribute) for element in self.tree.iter(): if attribute in list(element.attrib.keys()) and element.attrib[attribute] not in exception_list: - if self._verbose: print(' - Removed attribute: %s="%s"' % (attribute, element.attrib[attribute])) + if self._verbose: + print(' - Removed attribute: %s="%s"' % (attribute, element.attrib[attribute])) del element.attrib[attribute] - - def removeNonDefIDAttributes(self): + + def remove_non_def_id_attributes(self): """ Go through def elements and find IDs referred to, then remove all IDs except those. """ - def_IDs = [] + def_ids = [] for element in self.tree.iter(): if not isinstance(element.tag, str): continue - + tag = element.tag.split('}')[1] if tag == 'defs': for child in element.getchildren(): for key, value in child.attrib.items(): if key.endswith('href'): - def_IDs.append(value) + def_ids.append(value) - self.removeAttribute('id', exception_list=def_IDs) + self.remove_attribute('id', exception_list=def_ids) - def removeNamespace(self, namespace): + def remove_namespace(self, namespace): """ Remove all attributes of a given namespace. """ - + nslink = self.root.nsmap.get(namespace) if self._verbose: @@ -296,18 +306,18 @@ def removeNamespace(self, namespace): self.root.remove(element) if self._verbose: print(" - removed element: %s" % element.tag[length:]) - + for attribute in list(element.attrib.keys()): if attribute[:length] == nslink: del element.attrib[attribute] if self._verbose: print(" - removed attribute from tag: %s" % element.tag) - + del self.root.nsmap[namespace] - - def extractStyles(self): + + def extract_styles(self): """ Remove style attributes and values of the style attribute and put in