From d20ef060aaf310d5259200b1b744c48a14138138 Mon Sep 17 00:00:00 2001 From: Andy Date: Thu, 13 Jul 2017 20:41:55 -0700 Subject: [PATCH] Some progress (and added CONTRIBUTING.md) --- CONTRIBUTING.md | 50 ++++++++ build/lib/svgpathtools/svg2paths.py | 12 +- svgpathtools/document.py | 174 ++++++++++++++++++++++++++++ svgpathtools/parser.py | 11 +- svgpathtools/path.py | 3 + svgpathtools/svg2paths.py | 151 +++++++++++------------- 6 files changed, 309 insertions(+), 92 deletions(-) create mode 100644 CONTRIBUTING.md create mode 100644 svgpathtools/document.py diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..31896e6 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,50 @@ +# Contributing to svgpathtools + +The following is a few and guidelines regarding the current philosophy, style, +flaws, and the future directions of svgpathtools. These guidelines are meant +to make it easy to contribute. + +## Basic Considerations + +### New features should come with unittests and docstrings. +If you want to add a cool/useful feature to svgpathtools, that's great! Just +make sure your pull-request includes both thorough unittests and well-written +docstrings. See relevant sections below on "Testing Style" and +"Docstring Style" below. + + +### Modifications to old code may require additional unittests. +Certain submodules of svgpathtools are poorly covered by the current set of +unittests. That said, most functionality in svgpathtools has been tested quite +a bit through use. +The point being, if you're working on functionality not currently covered by +unittests (and your changes replace more than a few lines), then please include +unittests designed to verify that any affected functionary still works. + + +## Style +### Coding Style +* Follow the PEP8 guidelines unless you have good reason to violate them (e.g. +you want your code's variable names to match some official documentation, or +PEP8 guidelines contradict those present in this document). +* Include docstrings and in-line comments where appropriate. See +"Docstring Style" section below for more info. +* Use explicit, uncontracted names (e.g. "parse_transform" instead of +"parse_trafo"). The ideal names should be something a user can guess +* Use a capital 'T' denote a Path object's parameter, use a lower case 't' to +denote a Path segment's parameter. See the methods `Path.t2T` and `Path.T2t` +if you're unsure what I mean. In the ambiguous case, use either 't' or another +appropriate option (e.g. "tau"). + + +### Testing Style +See the svgpathtools/test folder for examples. + + +### Docstring Style +All docstrings in svgpathtools should (roughly) adhere to the Google Python +Style Guide. Currently, this is not the case... but for the sake of +consistency, Google Style is the officially preferred docstring style of +svgpathtools. +[Some nice examples of Google Python Style docstrings]( +https://sphinxcontrib-napoleon.readthedocs.io/en/latest/example_google.html) \ No newline at end of file diff --git a/build/lib/svgpathtools/svg2paths.py b/build/lib/svgpathtools/svg2paths.py index f1ecbea..e8d8aa2 100644 --- a/build/lib/svgpathtools/svg2paths.py +++ b/build/lib/svgpathtools/svg2paths.py @@ -69,29 +69,29 @@ def svg2paths(svg_file_location, return dict(list(zip(keys, values))) # Use minidom to extract path strings from input SVG - paths = [dom2dict(el) for el in doc.getElementsByTagName('path')] + paths = [dom2dict(el) for el in doc.get_elements_by_tag('path')] d_strings = [el['d'] for el in paths] attribute_dictionary_list = paths # if pathless_svg: - # for el in doc.getElementsByTagName('path'): + # for el in doc.get_elements_by_tag('path'): # el.parentNode.removeChild(el) # Use minidom to extract polyline strings from input SVG, convert to # path strings, add to list if convert_polylines_to_paths: - plins = [dom2dict(el) for el in doc.getElementsByTagName('polyline')] + plins = [dom2dict(el) for el in doc.get_elements_by_tag('polyline')] d_strings += [polyline2pathd(pl['points']) for pl in plins] attribute_dictionary_list += plins # Use minidom to extract polygon strings from input SVG, convert to # path strings, add to list if convert_polygons_to_paths: - pgons = [dom2dict(el) for el in doc.getElementsByTagName('polygon')] + pgons = [dom2dict(el) for el in doc.get_elements_by_tag('polygon')] d_strings += [polyline2pathd(pg['points']) + 'z' for pg in pgons] attribute_dictionary_list += pgons if convert_lines_to_paths: - lines = [dom2dict(el) for el in doc.getElementsByTagName('line')] + lines = [dom2dict(el) for el in doc.get_elements_by_tag('line')] d_strings += [('M' + l['x1'] + ' ' + l['y1'] + 'L' + l['x2'] + ' ' + l['y2']) for l in lines] attribute_dictionary_list += lines @@ -101,7 +101,7 @@ def svg2paths(svg_file_location, # doc.writexml(f) if return_svg_attributes: - svg_attributes = dom2dict(doc.getElementsByTagName('svg')[0]) + svg_attributes = dom2dict(doc.get_elements_by_tag('svg')[0]) doc.unlink() path_list = [parse_path(d) for d in d_strings] return path_list, attribute_dictionary_list, svg_attributes diff --git a/svgpathtools/document.py b/svgpathtools/document.py new file mode 100644 index 0000000..d665265 --- /dev/null +++ b/svgpathtools/document.py @@ -0,0 +1,174 @@ +"""An (experimental) replacement for the svg2paths and paths2svg functionality. + +This module contains the `Document` class, a container for a DOM-style document +(e.g. svg, html, xml, etc.) designed to replace and improve upon the IO functionality of svgpathtools (i.e. +the svg2paths and disvg/wsvg functions). + +An Historic Note: + The functionality in this module is meant to replace and improve upon the + IO functionality previously provided by the the `svg2paths` and + `disvg`/`wsvg` functions. + +Example: + Typical usage looks something the following. + + >> from svgpathtools import * + >> doc = Document('my_file.html') + >> for p in doc.paths: + >> foo(p) # do some stuff using svgpathtools functionality + >> foo2(doc.tree) # do some stuff using ElementTree's functionality + >> doc.display() # open modified document in OS's default application + >> doc.save('my_new_file.html') + +Attributes: + CONVERSIONS (dict): A dictionary whose keys are tag-names (of path-like + objects to be converted to paths during parsing) and whose values are + functions that take in a dictionary (of attributes) and return a string + (the path d-string). See the `Document` class docstring for more info. + +Todo: (please see contributor guidelines in CONTRIBUTING.md) + * Finish "NotImplemented" methods. + * Find some clever (and easy to implement) way to create a thorough set of + unittests. + * Finish Documentation for each method (approximately following the Google + Python Style Guide, see [1]_ for some nice examples). + For nice style examples, see [1]_. + +Some thoughts on this module's direction: + * The `Document` class should ONLY grab path elements that are inside an + SVG. + * To handle transforms... there should be a "get_transform" function and + also a "flatten_transforms" tool that removes any present transform + attributes from all SVG-Path elements in the document (applying the + transformations before to the svgpathtools Path objects). + Note: This ability to "flatten" will ignore CSS files (and any relevant + files that are not parsed into the tree automatically by ElementTree)... + that is unless you have any bright ideas on this. I really know very + little about DOM-style documents. +""" + +# External dependencies +from __future__ import division, absolute_import, print_function +import os +import xml.etree.cElementTree as etree + +# Internal dependencies +from .parser import parse_path +from .svg2paths import (ellipse2pathd, line2pathd, polyline2pathd, + polygon2pathd, rect2pathd) +from .misctools import open_in_browser + + +CONVERSIONS = {'circle': ellipse2pathd, + 'ellipse': ellipse2pathd, + 'line': line2pathd, + 'polyline': polyline2pathd, + 'polygon': polygon2pathd, + 'rect': rect2pathd} + + +class Document: + def __init__(self, filename=None, conversions=CONVERSIONS): + """A container for a DOM-style document. + + The `Document` class is meant to be used to parse, create, save, and + modify DOM-style documents. Given the `filename` of a DOM-style + document, it parses the document into an ElementTree object, extracts + all SVG-Path and Path-like (see `conversions` below) objects into + a list of svgpathtools Path objects.""" + + # remember location of original svg file + if filename is not None and os.path.dirname(filename) == '': + self.original_filename = os.path.join(os.getcwd(), filename) + else: + self.original_filename = filename + + # parse svg to ElementTree object + self.tree = etree.parse(filename) + self.root = self.tree.getroot() + + # get URI namespace (only necessary in OS X?) + root_tag = self.tree.getroot().tag + if root_tag[0] == "{": + self._prefix = root_tag[:root_tag.find('}') + 1] + else: + self._prefix = '' + # etree.register_namespace('', prefix) + + self.paths = self._get_paths(conversions) + + def get_elements_by_tag(self, tag): + """Returns a generator of all elements with the give tag. + + Note: for more advanced XML-related functionality, use the `tree` + attribute (an ElementTree object). + """ + return self.tree.iter(tag=self._prefix + tag) + + def _get_paths(self, conversions): + paths = [] + + # Get d-strings for SVG-Path elements + paths += [el.attrib for el in self.get_elements_by_tag('path')] + d_strings = [el['d'] for el in paths] + attribute_dictionary_list = paths + + # Get d-strings for path-like elements (using `conversions` dictionary) + if conversions: + for tag, fcn in conversions.items(): + attributes = [el.attrib for el in self.get_elements_by_tag(tag)] + d_strings += [fcn(d) for d in attributes] + + path_list = [parse_path(d) for d in d_strings] + return path_list + + def get_svg_attributes(self): + """To help with backwards compatibility.""" + return self.get_elements_by_tag('svg')[0].attrib + + def get_path_attributes(self): + """To help with backwards compatibility.""" + return [p.tree_element.attrib for p in self.paths] + + def add(self, path, attribs={}, parent=None): + """Add a new path to the SVG.""" + if parent is None: + parent = self.tree.getroot() + # just get root + # then add new path + # then record element_tree object in path + raise NotImplementedError + + def add_group(self, group_attribs={}, parent=None): + """Add an empty group element to the SVG.""" + if parent is None: + parent = self.tree.getroot() + raise NotImplementedError + + def update_tree(self): + """Rewrite the d-string's for each path in the `tree` attribute.""" + raise NotImplementedError + + def save(self, filename, update=True): + """Write to svg to a file.""" + if update: + self.update_tree() + + with open(filename, 'w') as output_svg: + output_svg.write(etree.tostring(self.tree.getroot())) + + def display(self, filename=None, update=True): + """Display the document. + + Opens the document """ + if update: + self.update_tree() + + if filename is None: + raise NotImplementedError + + # write to a (by default temporary) file + with open(filename, 'w') as output_svg: + output_svg.write(etree.tostring(self.tree.getroot())) + + open_in_browser(filename) diff --git a/svgpathtools/parser.py b/svgpathtools/parser.py index 66147b8..9e7c22b 100644 --- a/svgpathtools/parser.py +++ b/svgpathtools/parser.py @@ -1,7 +1,6 @@ """This submodule contains the path_parse() function used to convert SVG path element d-strings into svgpathtools Path objects. -Note: This file was taken (nearly) as is from the svg.path module -(v 2.0).""" +Note: This file was taken (nearly) as is from the svg.path module (v 2.0).""" # External dependencies from __future__ import division, absolute_import, print_function @@ -26,7 +25,7 @@ def _tokenize_path(pathdef): yield token -def parse_path(pathdef, current_pos=0j): +def parse_path(pathdef, current_pos=0j, tree_element=None): # In the SVG specs, initial movetos are absolute, even if # specified as 'm'. This is the default behavior here as well. # But if you pass in a current_pos variable, the initial moveto @@ -35,7 +34,11 @@ def parse_path(pathdef, current_pos=0j): # Reverse for easy use of .pop() elements.reverse() - segments = Path() + if tree_element is None: + segments = Path() + else: + segments = Path(tree_element=tree_element) + start_pos = None command = None diff --git a/svgpathtools/path.py b/svgpathtools/path.py index 41989de..d5234f1 100644 --- a/svgpathtools/path.py +++ b/svgpathtools/path.py @@ -1715,6 +1715,9 @@ class Path(MutableSequence): self._start = None self._end = None + if 'tree_element' in kw: + self._tree_element = kw['tree_element'] + def __getitem__(self, index): return self._segments[index] diff --git a/svgpathtools/svg2paths.py b/svgpathtools/svg2paths.py index 8f62932..c145fa9 100644 --- a/svgpathtools/svg2paths.py +++ b/svgpathtools/svg2paths.py @@ -3,15 +3,15 @@ The main tool being the svg2paths() function.""" # External dependencies from __future__ import division, absolute_import, print_function -from xml.dom.minidom import parse -from os import path as os_path, getcwd +import os +import xml.etree.cElementTree as etree # Internal dependencies from .parser import parse_path def ellipse2pathd(ellipse): - """converts the parameters from an ellipse or a circle to a string for a + """converts the parameters from an ellipse or a circle to a string for a Path object d-attribute""" cx = ellipse.get('cx', None) @@ -40,6 +40,10 @@ def ellipse2pathd(ellipse): def polyline2pathd(polyline_d): """converts the string from a polyline points-attribute to a string for a Path object d-attribute""" + try: + points = polyline_d['points'] + except: + pass points = polyline_d.replace(', ', ',') points = points.replace(' ,', ',') points = points.split() @@ -59,6 +63,10 @@ def polygon2pathd(polyline_d): Path object d-attribute. Note: For a polygon made from n points, the resulting path will be composed of n lines (even if some of these lines have length zero).""" + try: + points = polyline_d['points'] + except: + pass points = polyline_d.replace(', ', ',') points = points.replace(' ,', ',') points = points.split() @@ -80,8 +88,8 @@ def polygon2pathd(polyline_d): def rect2pathd(rect): """Converts an SVG-rect element to a Path d-string. - - The rectangle will start at the (x,y) coordinate specified by the rectangle + + The rectangle will start at the (x,y) coordinate specified by the rectangle object and proceed counter-clockwise.""" x0, y0 = float(rect.get('x', 0)), float(rect.get('y', 0)) w, h = float(rect["width"]), float(rect["height"]) @@ -93,16 +101,19 @@ def rect2pathd(rect): "".format(x0, y0, x1, y1, x2, y2, x3, y3)) return d +def line2pathd(l): + return 'M' + l['x1'] + ' ' + l['y1'] + 'L' + l['x2'] + ' ' + l['y2'] -def svg2paths(svg_file_location, - return_svg_attributes=False, - convert_circles_to_paths=True, - convert_ellipses_to_paths=True, - convert_lines_to_paths=True, - convert_polylines_to_paths=True, - convert_polygons_to_paths=True, - convert_rectangles_to_paths=True): - """Converts an SVG into a list of Path objects and attribute dictionaries. +CONVERSIONS = {'circle': ellipse2pathd, + 'ellipse': ellipse2pathd, + 'line': line2pathd, + 'polyline': polyline2pathd, + 'polygon': polygon2pathd, + 'rect': rect2pathd} + +def svg2paths(svg_file_location, return_svg_attributes=False, + conversions=CONVERSIONS, return_tree=False): + """Converts an SVG into a list of Path objects and attribute dictionaries. Converts an SVG file into a list of Path objects and a list of dictionaries containing their attributes. This currently supports @@ -111,13 +122,13 @@ def svg2paths(svg_file_location, Args: svg_file_location (string): the location of the svg file return_svg_attributes (bool): Set to True and a dictionary of - svg-attributes will be extracted and returned. See also the + svg-attributes will be extracted and returned. See also the `svg2paths2()` function. convert_circles_to_paths: Set to False to exclude SVG-Circle - elements (converted to Paths). By default circles are included as + elements (converted to Paths). By default circles are included as paths of two `Arc` objects. convert_ellipses_to_paths (bool): Set to False to exclude SVG-Ellipse - elements (converted to Paths). By default ellipses are included as + elements (converted to Paths). By default ellipses are included as paths of two `Arc` objects. convert_lines_to_paths (bool): Set to False to exclude SVG-Line elements (converted to Paths) @@ -128,89 +139,65 @@ def svg2paths(svg_file_location, convert_rectangles_to_paths (bool): Set to False to exclude SVG-Rect elements (converted to Paths). - Returns: + Returns: list: The list of Path objects. list: The list of corresponding path attribute dictionaries. dict (optional): A dictionary of svg-attributes (see `svg2paths2()`). """ - if os_path.dirname(svg_file_location) == '': - svg_file_location = os_path.join(getcwd(), svg_file_location) + if os.path.dirname(svg_file_location) == '': + svg_file_location = os.path.join(getcwd(), svg_file_location) - doc = parse(svg_file_location) + tree = etree.parse(svg_file_location) - def dom2dict(element): - """Converts DOM elements to dictionaries of attributes.""" - keys = list(element.attributes.keys()) - values = [val.value for val in list(element.attributes.values())] - return dict(list(zip(keys, values))) + # get URI namespace + root_tag = tree.getroot().tag + if root_tag[0] == "{": + prefix = root_tag[:root_tag.find('}') + 1] + else: + prefix = '' + # etree.register_namespace('', prefix) - # Use minidom to extract path strings from input SVG - paths = [dom2dict(el) for el in doc.getElementsByTagName('path')] + def getElementsByTagName(tag): + return tree.iter(tag=prefix+tag) + + # Get d-strings for Path elements + paths = [el.attrib for el in getElementsByTagName('path')] d_strings = [el['d'] for el in paths] attribute_dictionary_list = paths - # Use minidom to extract polyline strings from input SVG, convert to - # path strings, add to list - if convert_polylines_to_paths: - plins = [dom2dict(el) for el in doc.getElementsByTagName('polyline')] - d_strings += [polyline2pathd(pl['points']) for pl in plins] - attribute_dictionary_list += plins + # Get d-strings for Path-like elements (using `conversions` dictionary) + for tag, fcn in conversions.items(): + attributes = [el.attrib for el in getElementsByTagName(tag)] + d_strings += [fcn(d) for d in attributes] - # Use minidom to extract polygon strings from input SVG, convert to - # path strings, add to list - if convert_polygons_to_paths: - pgons = [dom2dict(el) for el in doc.getElementsByTagName('polygon')] - d_strings += [polygon2pathd(pg['points']) for pg in pgons] - attribute_dictionary_list += pgons + path_list = [parse_path(d) for d in d_strings] + if return_tree: # svg2paths3 default behavior + return path_list, tree - if convert_lines_to_paths: - lines = [dom2dict(el) for el in doc.getElementsByTagName('line')] - d_strings += [('M' + l['x1'] + ' ' + l['y1'] + - 'L' + l['x2'] + ' ' + l['y2']) for l in lines] - attribute_dictionary_list += lines - - if convert_ellipses_to_paths: - ellipses = [dom2dict(el) for el in doc.getElementsByTagName('ellipse')] - d_strings += [ellipse2pathd(e) for e in ellipses] - attribute_dictionary_list += ellipses - - if convert_circles_to_paths: - circles = [dom2dict(el) for el in doc.getElementsByTagName('circle')] - d_strings += [ellipse2pathd(c) for c in circles] - attribute_dictionary_list += circles - - if convert_rectangles_to_paths: - rectangles = [dom2dict(el) for el in doc.getElementsByTagName('rect')] - d_strings += [rect2pathd(r) for r in rectangles] - attribute_dictionary_list += rectangles - - if return_svg_attributes: - svg_attributes = dom2dict(doc.getElementsByTagName('svg')[0]) - doc.unlink() - path_list = [parse_path(d) for d in d_strings] + elif return_svg_attributes: # svg2paths2 default behavior + svg_attributes = getElementsByTagName('svg')[0].attrib return path_list, attribute_dictionary_list, svg_attributes - else: - doc.unlink() - path_list = [parse_path(d) for d in d_strings] + + else: # svg2paths default behavior return path_list, attribute_dictionary_list -def svg2paths2(svg_file_location, - return_svg_attributes=True, - convert_circles_to_paths=True, - convert_ellipses_to_paths=True, - convert_lines_to_paths=True, - convert_polylines_to_paths=True, - convert_polygons_to_paths=True, - convert_rectangles_to_paths=True): +def svg2paths2(svg_file_location, return_svg_attributes=True, + conversions=CONVERSIONS, return_tree=False): """Convenience function; identical to svg2paths() except that return_svg_attributes=True by default. See svg2paths() docstring for more info.""" return svg2paths(svg_file_location=svg_file_location, return_svg_attributes=return_svg_attributes, - convert_circles_to_paths=convert_circles_to_paths, - convert_ellipses_to_paths=convert_ellipses_to_paths, - convert_lines_to_paths=convert_lines_to_paths, - convert_polylines_to_paths=convert_polylines_to_paths, - convert_polygons_to_paths=convert_polygons_to_paths, - convert_rectangles_to_paths=convert_rectangles_to_paths) + conversions=conversions, return_tree=return_tree) + + +def svg2paths3(svg_file_location, return_svg_attributes=True, + conversions=CONVERSIONS, return_tree=True): + """Convenience function; identical to svg2paths() except that + return_tree=True. See svg2paths() docstring for more info.""" + return svg2paths(svg_file_location=svg_file_location, + return_svg_attributes=return_svg_attributes, + conversions=conversions, return_tree=return_tree) + +