Debugging xml namespace behavior -- needs improvement

pull/58/head
Michael X. Grey 2018-05-10 16:45:41 -07:00
parent be675f1b1c
commit a29b392234
8 changed files with 78 additions and 48 deletions

View File

@ -7,7 +7,6 @@ Author: Andy Port
Author-email: AndyAPort@gmail.com Author-email: AndyAPort@gmail.com
License: MIT License: MIT
Download-URL: http://github.com/mathandy/svgpathtools/tarball/1.3.2 Download-URL: http://github.com/mathandy/svgpathtools/tarball/1.3.2
Description-Content-Type: UNKNOWN
Description: Description:
svgpathtools svgpathtools
============ ============
@ -595,9 +594,8 @@ Description:
of the 'parallel' offset curve.""" of the 'parallel' offset curve."""
nls = [] nls = []
for seg in path: for seg in path:
ct = 1
for k in range(steps): for k in range(steps):
t = k / steps t = k / float(steps)
offset_vector = offset_distance * seg.normal(t) offset_vector = offset_distance * seg.normal(t)
nl = Line(seg.point(t), seg.point(t) + offset_vector) nl = Line(seg.point(t), seg.point(t) + offset_vector)
nls.append(nl) nls.append(nl)

View File

@ -15,6 +15,7 @@ test.svg
vectorframes.svg vectorframes.svg
svgpathtools/__init__.py svgpathtools/__init__.py
svgpathtools/bezier.py svgpathtools/bezier.py
svgpathtools/document.py
svgpathtools/misctools.py svgpathtools/misctools.py
svgpathtools/parser.py svgpathtools/parser.py
svgpathtools/path.py svgpathtools/path.py
@ -29,11 +30,13 @@ svgpathtools.egg-info/requires.txt
svgpathtools.egg-info/top_level.txt svgpathtools.egg-info/top_level.txt
test/circle.svg test/circle.svg
test/ellipse.svg test/ellipse.svg
test/groups.svg
test/polygons.svg test/polygons.svg
test/rects.svg test/rects.svg
test/test.svg test/test.svg
test/test_bezier.py test/test_bezier.py
test/test_generation.py test/test_generation.py
test/test_groups.py
test/test_parsing.py test/test_parsing.py
test/test_path.py test/test_path.py
test/test_polytools.py test/test_polytools.py

View File

@ -52,9 +52,8 @@ A Big Problem:
from __future__ import division, absolute_import, print_function from __future__ import division, absolute_import, print_function
import os import os
import collections import collections
import xml.etree.cElementTree as etree import xml.etree.ElementTree as etree
import xml.etree.ElementTree.Element as Element from xml.etree.ElementTree import Element, SubElement, register_namespace, _namespace_map
import xml.etree.ElementTree.SubElement as SubElement
import warnings import warnings
# Internal dependencies # Internal dependencies
@ -65,6 +64,11 @@ from .svg2paths import (path2pathd, ellipse2pathd, line2pathd, polyline2pathd,
from .misctools import open_in_browser from .misctools import open_in_browser
from .path import * from .path import *
# Let xml.etree.ElementTree know about the SVG namespace
print(' ------------------ about to register the svg namespace')
register_namespace('svg', 'http://www.w3.org/2000/svg')
print('namespace map: {0}'.format(_namespace_map))
# THESE MUST BE WRAPPED TO OUTPUT ElementTree.element objects # THESE MUST BE WRAPPED TO OUTPUT ElementTree.element objects
CONVERSIONS = {'path': path2pathd, CONVERSIONS = {'path': path2pathd,
'circle': ellipse2pathd, 'circle': ellipse2pathd,
@ -74,12 +78,15 @@ CONVERSIONS = {'path': path2pathd,
'polygon': polygon2pathd, 'polygon': polygon2pathd,
'rect': rect2pathd} 'rect': rect2pathd}
ONLY_PATHS = {'path': path2pathd}
def flatten_all_paths( def flatten_all_paths(
group, group,
group_filter=lambda x: True, group_filter=lambda x: True,
path_filter=lambda x: True, path_filter=lambda x: True,
path_conversions=CONVERSIONS): path_conversions=CONVERSIONS,
search_xpath='{http://www.w3.org/2000/svg}g'):
"""Returns the paths inside a group (recursively), expressing the paths in the base coordinates. """Returns the paths inside a group (recursively), expressing the paths in the base coordinates.
Note that if the group being passed in is nested inside some parent group(s), we cannot take the parent group(s) Note that if the group being passed in is nested inside some parent group(s), we cannot take the parent group(s)
@ -92,7 +99,8 @@ def flatten_all_paths(
that are not included in this dictionary will be ignored (including the `path` tag). that are not included in this dictionary will be ignored (including the `path` tag).
""" """
if not isinstance(group, Element): if not isinstance(group, Element):
raise TypeError('Must provide an xml.etree.Element object') raise TypeError('Must provide an xml.etree.Element object. Instead you provided {0} : compared to {1}'
.format(type(group), type(Element('some tag'))))
# Stop right away if the group_selector rejects this group # Stop right away if the group_selector rejects this group
if not group_filter(group): if not group_filter(group):
@ -108,7 +116,7 @@ def flatten_all_paths(
def get_relevant_children(parent, last_tf): def get_relevant_children(parent, last_tf):
children = [] children = []
for elem in filter(group_filter, parent.iterfind('g')): for elem in filter(group_filter, parent.iterfind(search_xpath)):
children.append(new_stack_element(elem, last_tf)) children.append(new_stack_element(elem, last_tf))
return children return children
@ -120,10 +128,13 @@ def flatten_all_paths(
while stack: while stack:
top = stack.pop() top = stack.pop()
print('popping group {0}'.format(top.group.attrib))
print('has children: {0}'.format(list(elem.tag for elem in top.group.iter())))
# For each element type that we know how to convert into path data, parse the element after confirming that # For each element type that we know how to convert into path data, parse the element after confirming that
# the path_filter accepts it. # the path_filter accepts it.
for key, converter in path_conversions: for key, converter in path_conversions.iteritems():
for path_elem in filter(path_filter, top.group.iterfind(key)): for path_elem in filter(path_filter, top.group.iterfind('{http://www.w3.org/2000/svg}'+key)):
path_tf = top.transform * parse_transform(path_elem.get('transform')) path_tf = top.transform * parse_transform(path_elem.get('transform'))
path = transform(parse_path(converter(path_elem)), path_tf) path = transform(parse_path(converter(path_elem)), path_tf)
paths.append(FlattenedPath(path, path_elem.attrib, path_tf)) paths.append(FlattenedPath(path, path_elem.attrib, path_tf))
@ -139,7 +150,8 @@ def flatten_group(
recursive=True, recursive=True,
group_filter=lambda x: True, group_filter=lambda x: True,
path_filter=lambda x: True, path_filter=lambda x: True,
path_conversions=CONVERSIONS): path_conversions=CONVERSIONS,
search_xpath='g'):
"""Flatten all the paths in a specific group. """Flatten all the paths in a specific group.
The paths will be flattened into the 'root' frame. Note that root needs to be The paths will be flattened into the 'root' frame. Note that root needs to be
@ -150,7 +162,7 @@ def flatten_group(
# We will shortcut here, because it is impossible for any paths to be returned anyhow. # We will shortcut here, because it is impossible for any paths to be returned anyhow.
return [] return []
# We create a set of the unique IDs of each group object that we want to flatten. # We create a set of the unique IDs of each element that we wish to flatten, if those elements are groups.
# Any groups outside of this set will be skipped while we flatten the paths. # Any groups outside of this set will be skipped while we flatten the paths.
desired_groups = set() desired_groups = set()
if recursive: if recursive:
@ -162,7 +174,7 @@ def flatten_group(
def desired_group_filter(x): def desired_group_filter(x):
return (id(x) in desired_groups) and group_filter(x) return (id(x) in desired_groups) and group_filter(x)
return flatten_all_paths(root, desired_group_filter, path_filter, path_conversions) return flatten_all_paths(root, desired_group_filter, path_filter, path_conversions, search_xpath)
class Document: class Document:
@ -190,14 +202,6 @@ class Document:
self.tree = etree.parse(filename) self.tree = etree.parse(filename)
self.root = self.tree.getroot() 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)
def flatten_all_paths(self, def flatten_all_paths(self,
group_filter=lambda x: True, group_filter=lambda x: True,
path_filter=lambda x: True, path_filter=lambda x: True,
@ -215,7 +219,7 @@ class Document:
group = self.get_or_add_group(group) group = self.get_or_add_group(group)
elif not isinstance(group, Element): elif not isinstance(group, Element):
raise TypeError('Must provide a list of strings that represent a nested group name, ' raise TypeError('Must provide a list of strings that represent a nested group name, '
'or provide an xml.etree.Element object') 'or provide an xml.etree.Element object. Instead you provided {0}'.format(group))
return flatten_group(group, self.tree.getroot(), recursive, group_filter, path_filter, path_conversions) return flatten_group(group, self.tree.getroot(), recursive, group_filter, path_filter, path_conversions)
@ -247,7 +251,8 @@ class Document:
group = self.get_or_add_group(group) group = self.get_or_add_group(group)
elif not isinstance(group, Element): elif not isinstance(group, Element):
raise TypeError('Must provide a list of strings or an xml.etree.Element object') raise TypeError('Must provide a list of strings or an xml.etree.Element object. '
'Instead you provided {0}'.format(group))
else: else:
# Make sure that the group belongs to this Document object # Make sure that the group belongs to this Document object
@ -262,7 +267,8 @@ class Document:
# Assume this is a valid d-string. TODO: Should we sanity check the input string? # Assume this is a valid d-string. TODO: Should we sanity check the input string?
path_svg = path path_svg = path
else: else:
raise TypeError('Must provide a Path, a path segment type, or a valid SVG path d-string') raise TypeError('Must provide a Path, a path segment type, or a valid SVG path d-string. '
'Instead you provided {0}'.format(path))
if attribs is None: if attribs is None:
attribs = {} attribs = {}
@ -313,7 +319,7 @@ class Document:
if parent is None: if parent is None:
parent = self.tree.getroot() parent = self.tree.getroot()
elif not self.contains_group(parent): elif not self.contains_group(parent):
warnings.warn('The requested group does not belong to this Document') warnings.warn('The requested group {0} does not belong to this Document'.format(parent))
if group_attribs is None: if group_attribs is None:
group_attribs = {} group_attribs = {}

View File

@ -206,30 +206,38 @@ def _check_num_parsed_values(values, allowed):
if len(allowed) > 1: if len(allowed) > 1:
warnings.warn('Expected one of the following number of values {0}, found {1}: {2}' warnings.warn('Expected one of the following number of values {0}, found {1}: {2}'
.format(allowed, len(values), values)) .format(allowed, len(values), values))
elif allowed != 1: elif allowed[0] != 1:
warnings.warn('Expected {0} values, found {1}: {2}'.format(allowed, len(values), values)) warnings.warn('Expected {0} values, found {1}: {2}'.format(allowed[0], len(values), values))
else: else:
warnings.warn('Expected 1 value, found {0}: {1}'.format(len(values), values)) warnings.warn('Expected 1 value, found {0}: {1}'.format(len(values), values))
return False
return True
def _parse_transform_substr(transform_substr): def _parse_transform_substr(transform_substr):
type_str, value_str = transform_substr.split('(') type_str, value_str = transform_substr.split('(')
values = list(map(float, value_str.split(','))) value_str = value_str.replace(',', ' ')
values = list(map(float, filter(None, value_str.split(' '))))
transform = np.identity(3) transform = np.identity(3)
if 'matrix' in type_str: if 'matrix' in type_str:
_check_num_parsed_values(values, 6) if not _check_num_parsed_values(values, [6]):
return transform
transform[0:2, 0:3] = np.matrix([values[0:6:2], values[1:6:2]]) transform[0:2, 0:3] = np.matrix([values[0:6:2], values[1:6:2]])
elif 'translate' in transform_substr: elif 'translate' in transform_substr:
_check_num_parsed_values(values, [1, 2]) if not _check_num_parsed_values(values, [1, 2]):
return transform
transform[0, 2] = values[0] transform[0, 2] = values[0]
if len(values) > 1: if len(values) > 1:
transform[1, 2] = values[1] transform[1, 2] = values[1]
elif 'scale' in transform_substr: elif 'scale' in transform_substr:
_check_num_parsed_values(values, [1, 2]) if not _check_num_parsed_values(values, [1, 2]):
return transform
x_scale = values[0] x_scale = values[0]
y_scale = values[1] if (len(values) > 1) else x_scale y_scale = values[1] if (len(values) > 1) else x_scale
@ -237,26 +245,31 @@ def _parse_transform_substr(transform_substr):
transform[1, 1] = y_scale transform[1, 1] = y_scale
elif 'rotate' in transform_substr: elif 'rotate' in transform_substr:
_check_num_parsed_values(values, [1, 3]) if not _check_num_parsed_values(values, [1, 3]):
return transform
angle = values[0] * np.pi / 180.0 angle = values[0] * np.pi / 180.0
if len(values) == 3: if len(values) == 3:
offset = values[1:3] offset = values[1:3]
else: else:
offset = (0, 0) offset = (0, 0)
T_offset = np.identity(3) tf_offset = np.identity(3)
T_offset[0:2, 2] = np.matrix([[offset[0]], [offset[1]]]) tf_offset[0:2, 2:3] = np.matrix([[offset[0]], [offset[1]]])
T_rotate = np.identity(3) tf_rotate = np.identity(3)
T_rotate[0:2, 0:2] = np.matrix([np.cos(angle), -np.sin(angle)], [np.sin(angle), np.cos(angle)]) tf_rotate[0:2, 0:2] = np.matrix([[np.cos(angle), -np.sin(angle)], [np.sin(angle), np.cos(angle)]])
transform = T_offset * T_rotate * (-T_offset) transform = tf_offset * tf_rotate * (-tf_offset)
elif 'skewX' in transform_substr: elif 'skewX' in transform_substr:
_check_num_parsed_values(values, 1) if not _check_num_parsed_values(values, [1]):
return transform
transform[0, 1] = np.tan(values[0] * np.pi / 180.0) transform[0, 1] = np.tan(values[0] * np.pi / 180.0)
elif 'skewY' in transform_substr: elif 'skewY' in transform_substr:
_check_num_parsed_values(values, 1) if not _check_num_parsed_values(values, [1]):
return transform
transform[1, 0] = np.tan(values[0] * np.pi / 180.0) transform[1, 0] = np.tan(values[0] * np.pi / 180.0)
else: else:
# Return an identity matrix if the type of transform is unknown, and warn the user # Return an identity matrix if the type of transform is unknown, and warn the user
@ -274,6 +287,8 @@ def parse_transform(transform_str):
raise TypeError('Must provide a string to parse') raise TypeError('Must provide a string to parse')
total_transform = np.identity(3) total_transform = np.identity(3)
transform_substrs = transform_str.split(')') transform_substrs = transform_str.split(')')[:-1] # Skip the last element, because it should be empty
for substr in transform_substrs: for substr in transform_substrs:
total_transform *= _parse_transform_substr(substr) total_transform *= _parse_transform_substr(substr)
return total_transform

View File

@ -5,13 +5,11 @@
viewBox="0 0 365 365" viewBox="0 0 365 365"
height="100%" height="100%"
width="100%" width="100%"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg">
xmlns:ev="http://www.w3.org/2001/xml-events"
xmlns:xlink="http://www.w3.org/1999/xlink">
<defs/> <defs/>
<g <g
id="matrix group" id="matrix group"
transform="matrix(1.5 0 0 0.5 -40.0 20.0)"> transform="matrix(1.5 0.0 0.0 0.5 -40.0 20.0)">
<path <path
d="M 183,183 l 0,-50" d="M 183,183 l 0,-50"

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@ -1,8 +1,14 @@
from __future__ import division, absolute_import, print_function from __future__ import division, absolute_import, print_function
import unittest import unittest
from svgpathtools import * from svgpathtools import *
from os.path import join, dirname
class TestGroups(unittest.TestCase): class TestGroups(unittest.TestCase):
def test_group_transform(self): def test_group_flatten(self):
pass doc = Document(join(dirname(__file__), 'groups.svg'))
result = doc.flatten_all_paths()
print('\nnumber of paths: '+str(len(result)))
self.assertGreater(len(result), 0)

View File

@ -137,3 +137,7 @@ class TestParser(unittest.TestCase):
def test_errors(self): def test_errors(self):
self.assertRaises(ValueError, parse_path, 'M 100 100 L 200 200 Z 100 200') self.assertRaises(ValueError, parse_path, 'M 100 100 L 200 200 Z 100 200')
def test_transform(self):
# TODO: Write these tests
pass