Some progress (and added CONTRIBUTING.md)

ElementTree
Andy 2017-07-13 20:41:55 -07:00
parent e8367d463a
commit d20ef060aa
6 changed files with 309 additions and 92 deletions

50
CONTRIBUTING.md Normal file
View File

@ -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)

View File

@ -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

174
svgpathtools/document.py Normal file
View File

@ -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)

View File

@ -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

View File

@ -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]

View File

@ -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)