diff --git a/setup.cfg b/setup.cfg
index 7a38d72..82b6f28 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -2,4 +2,7 @@
universal = 1
[metadata]
-license_file = LICENSE.txt
\ No newline at end of file
+license_file = LICENSE.txt
+
+[pydocstyle]
+convention = pep257
\ No newline at end of file
diff --git a/setup.py b/setup.py
index b90fe80..07b678f 100644
--- a/setup.py
+++ b/setup.py
@@ -3,7 +3,7 @@
import os
-VERSION = '1.7.5'
+VERSION = '1.8.0'
AUTHOR_NAME = 'Andy Port'
AUTHOR_EMAIL = 'AndyAPort@gmail.com'
GITHUB = 'https://github.com/mathandy/svgpathtools'
diff --git a/svgpathtools/__init__.py b/svgpathtools/__init__.py
index 7e5da65..abe9f3d 100644
--- a/svgpathtools/__init__.py
+++ b/svgpathtools/__init__.py
@@ -7,7 +7,7 @@
is_bezier_path, concatpaths, poly2bez, bpoints2bezier,
closest_point_in_path, farthest_point_in_path,
path_encloses_pt, bbox2path, polygon, polyline)
-from .parser import parse_path
+from .parser import parse_path, parse_transform, SVGSyntaxWarning
from .paths2svg import disvg, wsvg, paths2Drawing
from .polytools import polyroots, polyroots01, rational_limit, real, imag
from .misctools import hex2rgb, rgb2hex
diff --git a/svgpathtools/document.py b/svgpathtools/document.py
index d9f477a..6ac8e93 100644
--- a/svgpathtools/document.py
+++ b/svgpathtools/document.py
@@ -84,7 +84,8 @@
def flattened_paths(group, group_filter=lambda x: True,
path_filter=lambda x: True, path_conversions=CONVERSIONS,
- group_search_xpath=SVG_GROUP_TAG):
+ group_search_xpath=SVG_GROUP_TAG,
+ strict_transform_parsing=False):
"""Returns the paths inside a group (recursively), expressing the
paths in the base coordinates.
@@ -102,6 +103,10 @@ def flattened_paths(group, group_filter=lambda x: True,
dictionary will be ignored (including the `path` tag). To
only convert explicit path elements, pass in
`path_conversions=CONVERT_ONLY_PATHS`.
+ strict_transform_parsing (bool): If true, a ValueError is
+ raised when a transform attribute contains invalid syntax;
+ by default invalid transform substrings are skipped with
+ an SVGSyntaxWarning.
"""
if not isinstance(group, Element):
raise TypeError('Must provide an xml.etree.Element object. '
@@ -123,7 +128,8 @@ def flattened_paths(group, group_filter=lambda x: True,
def new_stack_element(element, last_tf):
return StackElement(element, last_tf.dot(
- parse_transform(element.get('transform'))))
+ parse_transform(element.get('transform'),
+ strict=strict_transform_parsing)))
def get_relevant_children(parent, last_tf):
children = []
@@ -145,7 +151,8 @@ def get_relevant_children(parent, last_tf):
for path_elem in filter(path_filter, top.group.iterfind(
'svg:'+key, SVG_NAMESPACE)):
path_tf = top.transform.dot(
- parse_transform(path_elem.get('transform')))
+ parse_transform(path_elem.get('transform'),
+ strict=strict_transform_parsing))
path = transform(parse_path(converter(path_elem)), path_tf)
path.element = path_elem
path.transform = path_tf
@@ -160,12 +167,17 @@ def flattened_paths_from_group(group_to_flatten, root, recursive=True,
group_filter=lambda x: True,
path_filter=lambda x: True,
path_conversions=CONVERSIONS,
- group_search_xpath=SVG_GROUP_TAG):
+ group_search_xpath=SVG_GROUP_TAG,
+ strict_transform_parsing=False):
"""Flatten all the paths in a specific group.
The paths will be flattened into the 'root' frame. Note that root
needs to be an ancestor of the group that is being flattened.
- Otherwise, no paths will be returned."""
+ Otherwise, no paths will be returned.
+
+ If `strict_transform_parsing` is true, a ValueError is raised when
+ a transform attribute contains invalid syntax; by default invalid
+ transform substrings are skipped with an SVGSyntaxWarning."""
if not any(group_to_flatten is descendant for descendant in root.iter()):
warnings.warn('The requested group_to_flatten is not a '
@@ -226,24 +238,33 @@ def desired_path_filter(x):
return (id(x) not in ignore_paths) and path_filter(x)
return flattened_paths(root, desired_group_filter, desired_path_filter,
- path_conversions, group_search_xpath)
+ path_conversions, group_search_xpath,
+ strict_transform_parsing=strict_transform_parsing)
class Document:
- def __init__(self, filepath=None):
- """A container for a DOM-style SVG document.
+ def __init__(self, filepath=None, strict_transform_parsing=False):
+ """
+ A container for a DOM-style SVG document.
- The `Document` class provides a simple interface to modify and analyze
- the path elements in a DOM-style document. The DOM-style document is
+ The `Document` class provides a simple interface to modify and analyze
+ the path elements in a DOM-style document. The DOM-style document is
parsed into an ElementTree object (stored in the `tree` attribute).
This class provides functions for extracting SVG data into Path objects.
The output Path objects will be transformed based on their parent groups.
-
+
Args:
filepath (str or file-like): The filepath of the
DOM-style object or a file-like object containing it.
+ strict_transform_parsing (bool): If true, a ValueError is
+ raised when a transform attribute containing invalid
+ syntax is parsed (transforms are parsed lazily, by
+ `paths()` and `paths_from_group()`); by default invalid
+ transform substrings are skipped with an
+ SVGSyntaxWarning.
"""
+ self.strict_transform_parsing = strict_transform_parsing
# strings are interpreted as file location everything else is treated as
# file-like object and passed to the xml parser directly
@@ -259,12 +280,13 @@ def __init__(self, filepath=None):
self.root = self.tree.getroot()
@classmethod
- def from_svg_string(cls, svg_string):
+ def from_svg_string(cls, svg_string, strict_transform_parsing=False):
"""Constructor for creating a Document object from a string."""
# wrap string into StringIO object
svg_file_obj = StringIO(svg_string)
# create document from file object
- return Document(svg_file_obj)
+ return Document(svg_file_obj,
+ strict_transform_parsing=strict_transform_parsing)
def paths(self, group_filter=lambda x: True,
path_filter=lambda x: True, path_conversions=CONVERSIONS):
@@ -273,8 +295,9 @@ def paths(self, group_filter=lambda x: True,
Note that any transform attributes are applied before returning
the paths.
"""
- return flattened_paths(self.tree.getroot(), group_filter,
- path_filter, path_conversions)
+ return flattened_paths(
+ self.tree.getroot(), group_filter, path_filter, path_conversions,
+ strict_transform_parsing=self.strict_transform_parsing)
def paths_from_group(self, group, recursive=True, group_filter=lambda x: True,
path_filter=lambda x: True, path_conversions=CONVERSIONS):
@@ -292,8 +315,10 @@ def paths_from_group(self, group, recursive=True, group_filter=lambda x: True,
warnings.warn("Could not find the requested group!")
return []
- return flattened_paths_from_group(group, self.tree.getroot(), recursive,
- group_filter, path_filter, path_conversions)
+ return flattened_paths_from_group(
+ group, self.tree.getroot(), recursive, group_filter, path_filter,
+ path_conversions,
+ strict_transform_parsing=self.strict_transform_parsing)
def add_path(self, path, attribs=None, group=None):
"""Add a new path to the SVG."""
diff --git a/svgpathtools/parser.py b/svgpathtools/parser.py
index 955c671..12f6b63 100644
--- a/svgpathtools/parser.py
+++ b/svgpathtools/parser.py
@@ -1,66 +1,102 @@
-"""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)."""
+"""
+Parse SVG path element d-strings into svgpathtools Path objects.
+
+This submodule contains the parse_path() function. 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
+from typing import Optional, Sequence, TYPE_CHECKING
+import math
+import re
import numpy as np
import warnings
# Internal dependencies
from .path import Path
+if TYPE_CHECKING:
+ # Imported for type annotations only; no XML is parsed here.
+ from xml.etree.ElementTree import Element
+
+# The SVG number grammar. Stricter than float(), which also accepts
+# e.g. '1_0', 'nan', and non-ASCII digits. (The one deliberate
+# looseness: '1.' is accepted, though the grammar technically wants a
+# digit after the point.)
+_NUMBER_RE = re.compile(r'[+-]?([0-9]+(\.[0-9]*)?|\.[0-9]+)([eE][+-]?[0-9]+)?\Z')
+
-def parse_path(pathdef, current_pos=0j, tree_element=None):
+class SVGSyntaxWarning(UserWarning):
+ """Category for warnings about invalid SVG syntax handled leniently."""
+
+
+def parse_path(pathdef: str, current_pos: complex = 0j,
+ tree_element: Optional['Element'] = None) -> Path:
+ """Convert an SVG path element d-string into a Path object."""
return Path(pathdef, current_pos=current_pos, tree_element=tree_element)
-def _check_num_parsed_values(values, allowed):
+def _check_num_parsed_values(values: Sequence[float], allowed: Sequence[int],
+ transform_substr: str) -> None:
if not any(num == len(values) for num in allowed):
if len(allowed) > 1:
- warnings.warn('Expected one of the following number of values {0}, but found {1} values instead: {2}'
- .format(allowed, len(values), values))
+ raise ValueError('Expected one of the following number of values {0}, '
+ 'but found {1} values in {2!r}: {3}'
+ .format(allowed, len(values), transform_substr, values))
elif allowed[0] != 1:
- warnings.warn('Expected {0} values, found {1}: {2}'.format(allowed[0], len(values), values))
+ raise ValueError('Expected {0} values in {1!r}, found {2}: {3}'
+ .format(allowed[0], transform_substr, len(values), values))
else:
- warnings.warn('Expected 1 value, found {0}: {1}'.format(len(values), values))
- return False
- return True
+ raise ValueError('Expected 1 value in {0!r}, found {1}: {2}'
+ .format(transform_substr, len(values), values))
+
+def _parse_transform_substr(transform_substr: str) -> np.ndarray:
+ """
+ Convert a single SVG transform substring into a 3x3 matrix.
-def _parse_transform_substr(transform_substr):
+ A well-formed transform substring has the form `type(v1 v2 ...)`.
+ Raises a ValueError on invalid transform syntax.
+ """
+ if transform_substr.count('(') != 1:
+ raise ValueError('Invalid SVG transform substring: {0!r}'.format(transform_substr))
type_str, value_str = transform_substr.split('(')
- value_str = value_str.replace(',', ' ')
- values = list(map(float, filter(None, value_str.split(' '))))
+ # Any leading commas/whitespace are the separator from the preceding
+ # transform in the list, e.g. 'translate(1), rotate(30)'.
+ type_str = type_str.strip(', \t\n\r')
+ tokens = value_str.replace(',', ' ').split()
+ if not all(_NUMBER_RE.match(t) for t in tokens):
+ raise ValueError('Invalid SVG transform substring: {0!r}'.format(transform_substr))
+ values = [float(t) for t in tokens]
+ # A grammar-valid value can still overflow float, e.g. '1e999'.
+ if not all(math.isfinite(v) for v in values):
+ raise ValueError('Non-finite value in SVG transform substring: {0!r}'.format(transform_substr))
transform = np.identity(3)
- if 'matrix' in type_str:
- if not _check_num_parsed_values(values, [6]):
- return transform
+ if type_str == 'matrix':
+ _check_num_parsed_values(values, [6], transform_substr)
transform[0:2, 0:3] = np.array([values[0:6:2], values[1:6:2]])
- elif 'translate' in transform_substr:
- if not _check_num_parsed_values(values, [1, 2]):
- return transform
+ elif type_str == 'translate':
+ _check_num_parsed_values(values, [1, 2], transform_substr)
transform[0, 2] = values[0]
if len(values) > 1:
transform[1, 2] = values[1]
- elif 'scale' in transform_substr:
- if not _check_num_parsed_values(values, [1, 2]):
- return transform
+ elif type_str == 'scale':
+ _check_num_parsed_values(values, [1, 2], transform_substr)
x_scale = values[0]
y_scale = values[1] if (len(values) > 1) else x_scale
transform[0, 0] = x_scale
transform[1, 1] = y_scale
- elif 'rotate' in transform_substr:
- if not _check_num_parsed_values(values, [1, 3]):
- return transform
+ elif type_str == 'rotate':
+ _check_num_parsed_values(values, [1, 3], transform_substr)
angle = values[0] * np.pi / 180.0
if len(values) == 3:
@@ -76,35 +112,63 @@ def _parse_transform_substr(transform_substr):
transform = tf_offset.dot(tf_rotate).dot(tf_offset_neg)
- elif 'skewX' in transform_substr:
- if not _check_num_parsed_values(values, [1]):
- return transform
+ elif type_str == 'skewX':
+ _check_num_parsed_values(values, [1], transform_substr)
transform[0, 1] = np.tan(values[0] * np.pi / 180.0)
- elif 'skewY' in transform_substr:
- if not _check_num_parsed_values(values, [1]):
- return transform
+ elif type_str == 'skewY':
+ _check_num_parsed_values(values, [1], transform_substr)
transform[1, 0] = np.tan(values[0] * np.pi / 180.0)
else:
- # Return an identity matrix if the type of transform is unknown, and warn the user
- warnings.warn('Unknown SVG transform type: {0}'.format(type_str))
+ raise ValueError('Unknown SVG transform type: {0}'.format(type_str))
return transform
-def parse_transform(transform_str):
- """Converts a valid SVG transformation string into a 3x3 matrix.
- If the string is empty or null, this returns a 3x3 identity matrix"""
- if not transform_str:
+def parse_transform(transform_str: Optional[str], strict: bool = False) -> np.ndarray:
+ """
+ Convert a valid SVG transformation string into a 3x3 matrix.
+
+ If the string is empty, null, or 'none', this returns a 3x3
+ identity matrix. By default each invalid transform substring is
+ skipped (i.e. contributes an identity matrix) with a warning. If
+ `strict` is true, a ValueError is raised on invalid transform
+ syntax instead.
+
+ The warnings use the SVGSyntaxWarning category, so they can be
+ silenced or escalated selectively, e.g.
+ `warnings.simplefilter('error', SVGSyntaxWarning)`.
+ """
+ if transform_str is None or transform_str == '':
return np.identity(3)
elif not isinstance(transform_str, str):
raise TypeError('Must provide a string to parse')
+ # 'none' is valid (SVG 2 / CSS transform syntax) and means no transform.
+ if transform_str.strip() == 'none':
+ return np.identity(3)
+
+ transform_substrs = transform_str.split(')')
+ # Anything after the last ')' (e.g. a stray 'matrix' with no
+ # parentheses) is invalid syntax, not a transform to apply -- but a
+ # trailing list-separator comma is harmless.
+ trailing = transform_substrs.pop()
+ if trailing.strip(', \t\n\r'):
+ if strict:
+ raise ValueError('Invalid SVG transform substring: {0!r}'.format(trailing))
+ warnings.warn('Skipping invalid SVG transform substring: {0!r}'.format(trailing),
+ SVGSyntaxWarning)
+
total_transform = np.identity(3)
- transform_substrs = transform_str.split(')')[:-1] # Skip the last element, because it should be empty
for substr in transform_substrs:
- total_transform = total_transform.dot(_parse_transform_substr(substr))
+ try:
+ total_transform = total_transform.dot(_parse_transform_substr(substr))
+ except ValueError as e:
+ if strict:
+ raise
+ warnings.warn('Skipping invalid SVG transform substring {0!r}: {1}'.format(substr, e),
+ SVGSyntaxWarning)
return total_transform
diff --git a/svgpathtools/svg_io_sax.py b/svgpathtools/svg_io_sax.py
index 7faced1..4992a09 100644
--- a/svgpathtools/svg_io_sax.py
+++ b/svgpathtools/svg_io_sax.py
@@ -44,14 +44,20 @@
class SaxDocument:
- def __init__(self, filename):
- """A container for a SAX SVG light tree objects document.
+ def __init__(self, filename, strict_transform_parsing=False):
+ """
+ A container for a SAX SVG light tree objects document.
This class provides functions for extracting SVG data into Path objects.
Args:
filename (str): The filename of the SVG file
+ strict_transform_parsing (bool): If true, a ValueError is
+ raised when a transform attribute contains invalid
+ syntax; by default invalid transform substrings are
+ skipped with an SVGSyntaxWarning.
"""
+ self.strict_transform_parsing = strict_transform_parsing
self.root_values = {}
self.tree = []
# remember location of original svg file
@@ -69,57 +75,63 @@ def sax_parse(self, filename):
stack = []
values = {}
matrix = None
- for event, elem in iterparse(filename, events=('start', 'end')):
- if event == 'start':
- stack.append((values, matrix))
- if matrix is not None:
- matrix = matrix.copy() # copy of matrix
- current_values = values
- values = {}
- values.update(current_values) # copy of dictionary
- attrs = elem.attrib
- values.update(attrs)
- name = elem.tag[28:]
- if "style" in attrs:
- for equate in attrs["style"].split(";"):
- equal_item = equate.split(":")
- values[equal_item[0]] = equal_item[1]
- if "transform" in attrs:
- transform_matrix = parse_transform(attrs["transform"])
- if matrix is None:
- matrix = np.identity(3)
- matrix = transform_matrix.dot(matrix)
- if "svg" == name:
+ # Open the file ourselves (rather than letting iterparse do it)
+ # so the handle is closed even if parsing raises; otherwise the
+ # file stays locked on Windows until garbage collection.
+ with open(filename, 'rb') as svg_file:
+ for event, elem in iterparse(svg_file, events=('start', 'end')):
+ if event == 'start':
+ stack.append((values, matrix))
+ if matrix is not None:
+ matrix = matrix.copy() # copy of matrix
current_values = values
values = {}
- values.update(current_values)
- self.root_values = current_values
- continue
- elif "g" == name:
- continue
- elif 'path' == name:
- values['d'] = path2pathd(values)
- elif 'circle' == name:
- values["d"] = ellipse2pathd(values)
- elif 'ellipse' == name:
- values["d"] = ellipse2pathd(values)
- elif 'line' == name:
- values["d"] = line2pathd(values)
- elif 'polyline' == name:
- values["d"] = polyline2pathd(values)
- elif 'polygon' == name:
- values["d"] = polygon2pathd(values)
- elif 'rect' == name:
- values["d"] = rect2pathd(values)
+ values.update(current_values) # copy of dictionary
+ attrs = elem.attrib
+ values.update(attrs)
+ name = elem.tag[28:]
+ if "style" in attrs:
+ for equate in attrs["style"].split(";"):
+ equal_item = equate.split(":")
+ values[equal_item[0]] = equal_item[1]
+ if "transform" in attrs:
+ transform_matrix = parse_transform(
+ attrs["transform"],
+ strict=self.strict_transform_parsing)
+ if matrix is None:
+ matrix = np.identity(3)
+ matrix = transform_matrix.dot(matrix)
+ if "svg" == name:
+ current_values = values
+ values = {}
+ values.update(current_values)
+ self.root_values = current_values
+ continue
+ elif "g" == name:
+ continue
+ elif 'path' == name:
+ values['d'] = path2pathd(values)
+ elif 'circle' == name:
+ values["d"] = ellipse2pathd(values)
+ elif 'ellipse' == name:
+ values["d"] = ellipse2pathd(values)
+ elif 'line' == name:
+ values["d"] = line2pathd(values)
+ elif 'polyline' == name:
+ values["d"] = polyline2pathd(values)
+ elif 'polygon' == name:
+ values["d"] = polygon2pathd(values)
+ elif 'rect' == name:
+ values["d"] = rect2pathd(values)
+ else:
+ continue
+ values["matrix"] = matrix
+ values["name"] = name
+ self.tree.append(values)
else:
- continue
- values["matrix"] = matrix
- values["name"] = name
- self.tree.append(values)
- else:
- v = stack.pop()
- values = v[0]
- matrix = v[1]
+ v = stack.pop()
+ values = v[0]
+ matrix = v[1]
def flatten_all_paths(self):
flat = []
diff --git a/test/test_parsing.py b/test/test_parsing.py
index 6ef5c9d..90392e1 100644
--- a/test/test_parsing.py
+++ b/test/test_parsing.py
@@ -1,6 +1,9 @@
# Note: This file was taken mostly as is from the svg.path module (v 2.0)
from __future__ import division, absolute_import, print_function
+import os
+import tempfile
import unittest
+import warnings
from svgpathtools import Path, Line, QuadraticBezier, CubicBezier, Arc, parse_path
import svgpathtools
@@ -295,6 +298,152 @@ def test_transform(self):
scale(10 0.5)""")
))
+ def test_transform_whitespace(self):
+ # Values may be separated by any whitespace, not just spaces.
+ expected_tf_matrix = np.identity(3)
+ expected_tf_matrix[0:2, 0:3] = np.array([[1.0, 3.0, 5.0],
+ [2.0, 4.0, 6.0]])
+ tf_matrix = svgpathtools.parser.parse_transform(
+ 'matrix(1, 2,\n3 4\t5 6)')
+ self.assertTrue(np.array_equal(expected_tf_matrix, tf_matrix))
+
+ def test_transform_malformed(self):
+ bad_transforms = ('matrix(1 x 3 4 5 6)', # non-numeric value
+ 'translate(a)', # non-numeric value
+ 'scale()', # wrong number of values
+ 'rotate(1 2 z)', # non-numeric value
+ 'bogus(5)', # unknown transform type
+ 'notmatrix(1 0 0 1 0 0)', # unknown transform type
+ 'translate(nan)', # non-finite value
+ 'scale(1 inf)', # non-finite value
+ 'translate(1_0)', # not an SVG number
+ 'scale(1e999)', # overflows to inf
+ 'foo(1', # missing closing paren
+ 'matrix', # no parens at all
+ 'matrix(1(2)', # extra opening paren
+ 'rotate(30))') # stray closing paren
+
+ # By default, each invalid substring warns and contributes an
+ # identity matrix. ('rotate(30))' is excluded because its valid
+ # 'rotate(30)' part still applies.)
+ identity = np.identity(3)
+ for bad in bad_transforms[:-1]:
+ with warnings.catch_warnings(record=True) as caught:
+ warnings.simplefilter('always')
+ tf = svgpathtools.parser.parse_transform(bad)
+ self.assertTrue(caught, msg=bad)
+ self.assertTrue(np.array_equal(identity, tf), msg=bad)
+
+ # Valid substrings still apply alongside skipped invalid ones.
+ expected_tf_translate = np.identity(3)
+ expected_tf_translate[0, 2] = 10
+ expected_tf_translate[1, 2] = 20
+ with warnings.catch_warnings(record=True) as caught:
+ warnings.simplefilter('always')
+ tf = svgpathtools.parser.parse_transform(
+ 'translate(10 20) matrix(1 x 3 4 5 6)')
+ self.assertTrue(caught)
+ self.assertTrue(np.array_equal(expected_tf_translate, tf))
+
+ # With strict=True, invalid transform syntax raises a ValueError.
+ for bad in bad_transforms:
+ with self.assertRaises(ValueError, msg=bad):
+ svgpathtools.parser.parse_transform(bad, strict=True)
+
+ # Error messages identify the offending substring.
+ with self.assertRaisesRegex(ValueError, r'rotate\(1 2'):
+ svgpathtools.parser.parse_transform('rotate(1 2)', strict=True)
+
+ # None, '' and 'none' yield identity silently, even in strict
+ # mode; other non-strings raise TypeError.
+ for empty in (None, '', 'none', ' none '):
+ with warnings.catch_warnings():
+ warnings.simplefilter('error')
+ tf = svgpathtools.parser.parse_transform(empty, strict=True)
+ self.assertTrue(np.array_equal(identity, tf), msg=repr(empty))
+ for bad_type in (0, False, [], 0.5):
+ with self.assertRaises(TypeError, msg=repr(bad_type)):
+ svgpathtools.parser.parse_transform(bad_type)
+
+ def test_transform_separators(self):
+ # Transforms in a list may be separated by whitespace, commas, or
+ # (leniently) nothing at all; all should parse identically, in
+ # strict mode too.
+ expected = svgpathtools.parser.parse_transform(
+ 'translate(10 20) rotate(30)', strict=True)
+ for tf_str in ('translate(10 20),rotate(30)',
+ 'translate(10 20) , rotate(30)',
+ 'translate(10 20)\nrotate(30)',
+ 'translate(10 20)rotate(30)',
+ # A trailing list-separator comma is harmless.
+ 'translate(10 20),rotate(30),',
+ 'translate(10 20),rotate(30) , '):
+ with warnings.catch_warnings():
+ warnings.simplefilter('error')
+ tf = svgpathtools.parser.parse_transform(tf_str, strict=True)
+ self.assertTrue(np.array_equal(expected, tf), msg=tf_str)
+
+ def test_transform_warning_category(self):
+ # Lenient-mode warnings use SVGSyntaxWarning, a UserWarning
+ # subclass, so they can be filtered or escalated selectively
+ # while existing UserWarning filters keep working.
+ with warnings.catch_warnings(record=True) as caught:
+ warnings.simplefilter('always')
+ svgpathtools.parse_transform('translate(a)')
+ self.assertTrue(caught)
+ for w in caught:
+ self.assertTrue(issubclass(w.category,
+ svgpathtools.SVGSyntaxWarning))
+ self.assertTrue(issubclass(w.category, UserWarning))
+
+ def test_document_strict_transform_parsing(self):
+ svg = ('')
+
+ # Default: lenient, warns with SVGSyntaxWarning.
+ with warnings.catch_warnings(record=True) as caught:
+ warnings.simplefilter('always')
+ paths = svgpathtools.Document.from_svg_string(svg).paths()
+ self.assertEqual(len(paths), 1)
+ self.assertTrue(any(issubclass(w.category,
+ svgpathtools.SVGSyntaxWarning)
+ for w in caught))
+
+ # The warning category can be escalated to an error.
+ with warnings.catch_warnings():
+ warnings.simplefilter('error', svgpathtools.SVGSyntaxWarning)
+ with self.assertRaises(svgpathtools.SVGSyntaxWarning):
+ svgpathtools.Document.from_svg_string(svg).paths()
+
+ # Opt-in strict parsing raises ValueError.
+ doc = svgpathtools.Document.from_svg_string(
+ svg, strict_transform_parsing=True)
+ with self.assertRaises(ValueError):
+ doc.paths()
+
+ def test_sax_document_strict_transform_parsing(self):
+ svg = ('')
+ fd, fname = tempfile.mkstemp(suffix='.svg')
+ try:
+ with os.fdopen(fd, 'w') as f:
+ f.write(svg)
+
+ # Default: lenient, warns with SVGSyntaxWarning.
+ with warnings.catch_warnings(record=True) as caught:
+ warnings.simplefilter('always')
+ svgpathtools.SaxDocument(fname)
+ self.assertTrue(any(issubclass(w.category,
+ svgpathtools.SVGSyntaxWarning)
+ for w in caught))
+
+ # Opt-in strict parsing raises ValueError.
+ with self.assertRaises(ValueError):
+ svgpathtools.SaxDocument(fname,
+ strict_transform_parsing=True)
+ finally:
+ os.remove(fname)
+
def test_pathd_init(self):
path0 = Path('')
path1 = parse_path("M 100 100 L 300 100 L 200 300 z")