Script 'mail_helper' called by obssrc
Hello community,
here is the log from the commit of package python-svgpathtools for
openSUSE:Factory checked in at 2026-09-08 16:53:33
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-svgpathtools (Old)
and /work/SRC/openSUSE:Factory/.python-svgpathtools.new.1265 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-svgpathtools"
Tue Sep 8 16:53:33 2026 rev:11 rq:1375980 version:1.8.0
Changes:
--------
--- /work/SRC/openSUSE:Factory/python-svgpathtools/python-svgpathtools.changes
2025-12-05 16:54:34.035193860 +0100
+++
/work/SRC/openSUSE:Factory/.python-svgpathtools.new.1265/python-svgpathtools.changes
2026-09-08 16:54:21.346565623 +0200
@@ -1,0 +2,24 @@
+Sun Sep 6 00:10:02 UTC 2026 - Mia Herkt <[email protected]>
+
+- Note: Upstream development now makes extensive use of LLMs.
+- Switch to declarative BuildSystem
+- Update to 1.8.0:
+ * Overhaul malformed-transform handling in `parse_transform`
+ + Stricter validation of floating point numbers and transform
+ types (nan/inf, 1_0 and overflow are now rejected);
+ falsy non-strings raise `TypeError`
+ + `transform="none"`, tabs/newlines and trailing separator
+ commas are now parsed correctly
+ + Emits `SVGSyntaxWarning` (top-level exported `UserWarning`)
+ + Added `strict=True` kwarg to `parse_transform`, which will
+ cause it to raise `ValueError` instead.
+ The `strict_transform_parsing=True` kwarg enables this for
+ `Document`, `Document.from_svg_string`, `SaxDocument`,
+ `flattened_paths` and `flattened_paths_from_group`.
+- Changes in 1.7.3/1.7.4:
+ * Parse SVG arc paths with packed large-arc/sweep flags
+ #gh/mathandy/svgpathtools#129
+ * Raise `ValueError` for truncated path commands instead of
+ `IndexError`
+
+-------------------------------------------------------------------
Old:
----
svgpathtools-1.7.2.tar.gz
New:
----
svgpathtools-1.8.0.tar.gz
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Other differences:
------------------
++++++ python-svgpathtools.spec ++++++
--- /var/tmp/diff_new_pack.qguhSL/_old 2026-09-08 16:54:22.993634723 +0200
+++ /var/tmp/diff_new_pack.qguhSL/_new 2026-09-08 16:54:22.995634807 +0200
@@ -1,7 +1,7 @@
#
# spec file for package python-svgpathtools
#
-# Copyright (c) 2025 SUSE LLC and contributors
+# Copyright (c) 2026 SUSE LLC and contributors
#
# All modifications and additions to the file contributed by third parties
# remain the property of their copyright owners, unless otherwise agreed
@@ -16,11 +16,11 @@
#
-%{?sle15_python_module_pythons}
Name: python-svgpathtools
-Version: 1.7.2
+Version: 1.8.0
Release: 0
Summary: Tools for manipulating and analyzing SVG Path objects and
Bézier curves
+# Legal-Review-Notice: Development makes extensive use of LLMs (e.g. Claude
Fable) since version 1.8.0
License: MIT
URL: https://github.com/mathandy/svgpathtools
Source:
https://files.pythonhosted.org/packages/source/s/svgpathtools/svgpathtools-%{version}.tar.gz
@@ -36,24 +36,15 @@
Requires: python-scipy
Requires: python-svgwrite
BuildArch: noarch
+BuildSystem: pyproject
%python_subpackages
%description
Svgpathtools is a collection of tools for manipulating and
analyzing SVG Path objects and Bézier curves.
-%prep
-%setup -q -n svgpathtools-%{version}
-
-%build
-%pyproject_wheel
-
-%install
-%pyproject_install
-%python_expand %fdupes %{buildroot}%{$python_sitelib}
-
-%check
-%pytest
+%install -a
+%fdupes %{buildroot}
%files %{python_files}
%license LICENSE.txt LICENSE2.txt
++++++ svgpathtools-1.7.2.tar.gz -> svgpathtools-1.8.0.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/svgpathtools-1.7.2/PKG-INFO
new/svgpathtools-1.8.0/PKG-INFO
--- old/svgpathtools-1.7.2/PKG-INFO 2025-11-30 20:14:48.031777600 +0100
+++ new/svgpathtools-1.8.0/PKG-INFO 2026-09-05 21:33:39.410393200 +0200
@@ -1,9 +1,9 @@
Metadata-Version: 2.1
Name: svgpathtools
-Version: 1.7.2
+Version: 1.8.0
Summary: A collection of tools for manipulating and analyzing SVG Path objects
and Bezier curves.
Home-page: https://github.com/mathandy/svgpathtools
-Download-URL:
https://github.com/mathandy/svgpathtools/releases/download/1.7.2/svgpathtools-1.7.2-py3-none-any.whl
+Download-URL:
https://github.com/mathandy/svgpathtools/releases/download/1.8.0/svgpathtools-1.8.0-py3-none-any.whl
Author: Andy Port
Author-email: [email protected]
License: MIT
@@ -20,6 +20,7 @@
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
+Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Multimedia :: Graphics :: Editors :: Vector-Based
Classifier: Topic :: Scientific/Engineering
Classifier: Topic :: Scientific/Engineering :: Image Recognition
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/svgpathtools-1.7.2/setup.cfg
new/svgpathtools-1.8.0/setup.cfg
--- old/svgpathtools-1.7.2/setup.cfg 2025-11-30 20:14:48.032777500 +0100
+++ new/svgpathtools-1.8.0/setup.cfg 2026-09-05 21:33:39.410783300 +0200
@@ -4,6 +4,9 @@
[metadata]
license_file = LICENSE.txt
+[pydocstyle]
+convention = pep257
+
[egg_info]
tag_build =
tag_date = 0
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/svgpathtools-1.7.2/setup.py
new/svgpathtools-1.8.0/setup.py
--- old/svgpathtools-1.7.2/setup.py 2025-11-30 20:14:37.000000000 +0100
+++ new/svgpathtools-1.8.0/setup.py 2026-09-05 21:33:29.000000000 +0200
@@ -3,7 +3,7 @@
import os
-VERSION = '1.7.2'
+VERSION = '1.8.0'
AUTHOR_NAME = 'Andy Port'
AUTHOR_EMAIL = '[email protected]'
GITHUB = 'https://github.com/mathandy/svgpathtools'
@@ -46,6 +46,7 @@
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
+ "Programming Language :: Python :: 3.14",
"Topic :: Multimedia :: Graphics :: Editors :: Vector-Based",
"Topic :: Scientific/Engineering",
"Topic :: Scientific/Engineering :: Image Recognition",
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/svgpathtools-1.7.2/svgpathtools/__init__.py
new/svgpathtools-1.8.0/svgpathtools/__init__.py
--- old/svgpathtools-1.7.2/svgpathtools/__init__.py 2025-11-30
20:14:37.000000000 +0100
+++ new/svgpathtools-1.8.0/svgpathtools/__init__.py 2026-09-05
21:33:29.000000000 +0200
@@ -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 -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/svgpathtools-1.7.2/svgpathtools/document.py
new/svgpathtools-1.8.0/svgpathtools/document.py
--- old/svgpathtools-1.7.2/svgpathtools/document.py 2025-11-30
20:14:37.000000000 +0100
+++ new/svgpathtools-1.8.0/svgpathtools/document.py 2026-09-05
21:33:29.000000000 +0200
@@ -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 @@
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 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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/svgpathtools-1.7.2/svgpathtools/parser.py
new/svgpathtools-1.8.0/svgpathtools/parser.py
--- old/svgpathtools-1.7.2/svgpathtools/parser.py 2025-11-30
20:14:37.000000000 +0100
+++ new/svgpathtools-1.8.0/svgpathtools/parser.py 2026-09-05
21:33:29.000000000 +0200
@@ -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')
+
+
+class SVGSyntaxWarning(UserWarning):
+ """Category for warnings about invalid SVG syntax handled leniently."""
-def parse_path(pathdef, current_pos=0j, tree_element=None):
+
+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):
+def _parse_transform_substr(transform_substr: str) -> np.ndarray:
+ """
+ Convert a single SVG transform substring into a 3x3 matrix.
+
+ 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 @@
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 -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/svgpathtools-1.7.2/svgpathtools/path.py
new/svgpathtools-1.8.0/svgpathtools/path.py
--- old/svgpathtools-1.7.2/svgpathtools/path.py 2025-11-30 20:14:37.000000000
+0100
+++ new/svgpathtools-1.8.0/svgpathtools/path.py 2026-09-05 21:33:29.000000000
+0200
@@ -44,8 +44,41 @@
COMMANDS = set('MmZzLlHhVvCcSsQqTtAa')
UPPERCASE = set('MZLHVCSQTA')
+# Number of numeric parameters each command consumes per repetition.
+COMMAND_NUM_ARGS = {'M': 2, 'L': 2, 'H': 1, 'V': 1, 'C': 6,
+ 'S': 4, 'Q': 4, 'T': 2, 'A': 7, 'Z': 0}
+
COMMAND_RE = re.compile(r"([MmZzLlHhVvCcSsQqTtAa])")
FLOAT_RE = re.compile(r"[-+]?[0-9]*\.?[0-9]+(?:[eE][-+]?[0-9]+)?")
+# In an elliptical arc argument the large-arc-flag and sweep-flag are each a
+# single "0"/"1" character that, unlike the other fields, may be written with
+# no separator before the next number (e.g. "0110 0" == "0 1 10 0"); see the
+# SVG path grammar: https://www.w3.org/TR/SVG/paths.html#PathDataBNF
+ARC_FLAG_RE = re.compile(r"[01]")
+WSP_COMMA_RE = re.compile(r"[\s,]*")
+
+
+def _tokenize_arc_args(arg_chunk):
+ """Yield the tokens of one or more seven-field elliptical-arc groups."""
+ pos = 0
+ field = 0
+ n = len(arg_chunk)
+ while True:
+ sep = WSP_COMMA_RE.match(arg_chunk, pos)
+ if sep:
+ pos = sep.end()
+ if pos >= n:
+ return
+ match = None
+ if field % 7 in (3, 4):
+ match = ARC_FLAG_RE.match(arg_chunk, pos)
+ if match is None:
+ match = FLOAT_RE.match(arg_chunk, pos)
+ if match is None:
+ return
+ yield match.group()
+ pos = match.end()
+ field += 1
# Default Parameters ##########################################################
@@ -3191,11 +3224,20 @@
return zip(a, b)
def _tokenize_path(self, pathdef):
+ command = None
for x in COMMAND_RE.split(pathdef):
if x in COMMANDS:
+ command = x
yield x
- for token in FLOAT_RE.findall(x):
- yield token
+ continue
+ if command in ('A', 'a'):
+ # Arc arguments need flag-aware tokenizing; the other commands
+ # only carry plain numbers.
+ for token in _tokenize_arc_args(x):
+ yield token
+ else:
+ for token in FLOAT_RE.findall(x):
+ yield token
def _parse_path(self, pathdef, current_pos=0j, tree_element=None):
# In the SVG specs, initial movetos are absolute, even if
@@ -3227,6 +3269,15 @@
pathdef, len(pathdef.split()) - len(elements)))
last_command = command # Used by S and T
+ # A command must be followed by its full set of coordinates; a
+ # truncated path (e.g. "M 0 0 L") would otherwise pop past the end
+ # of the token list and raise a bare IndexError.
+ if len(elements) < COMMAND_NUM_ARGS[command]:
+ raise ValueError("Invalid path string: command '%s' expects %d
"
+ "values but only %d remain in %r" % (
+ command, COMMAND_NUM_ARGS[command],
+ len(elements), pathdef))
+
if command == 'M':
# Moveto command.
x = elements.pop()
@@ -3297,7 +3348,7 @@
# Smooth curve. First control point is the "reflection" of
# the second control point in the previous path.
- if last_command not in 'CS':
+ if last_command not in ('C', 'S'):
# If there is no previous command or if the previous
command
# was not an C, c, S or s, assume the first control point
is
# coincident with the current point.
@@ -3333,7 +3384,7 @@
# Smooth curve. Control point is the "reflection" of
# the second control point in the previous path.
- if last_command not in 'QT':
+ if last_command not in ('Q', 'T'):
# If there is no previous command or if the previous
command
# was not an Q, q, T or t, assume the first control point
is
# coincident with the current point.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/svgpathtools-1.7.2/svgpathtools/svg_io_sax.py
new/svgpathtools-1.8.0/svgpathtools/svg_io_sax.py
--- old/svgpathtools-1.7.2/svgpathtools/svg_io_sax.py 2025-11-30
20:14:37.000000000 +0100
+++ new/svgpathtools-1.8.0/svgpathtools/svg_io_sax.py 2026-09-05
21:33:29.000000000 +0200
@@ -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 @@
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 -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/svgpathtools-1.7.2/svgpathtools.egg-info/PKG-INFO
new/svgpathtools-1.8.0/svgpathtools.egg-info/PKG-INFO
--- old/svgpathtools-1.7.2/svgpathtools.egg-info/PKG-INFO 2025-11-30
20:14:48.000000000 +0100
+++ new/svgpathtools-1.8.0/svgpathtools.egg-info/PKG-INFO 2026-09-05
21:33:39.000000000 +0200
@@ -1,9 +1,9 @@
Metadata-Version: 2.1
Name: svgpathtools
-Version: 1.7.2
+Version: 1.8.0
Summary: A collection of tools for manipulating and analyzing SVG Path objects
and Bezier curves.
Home-page: https://github.com/mathandy/svgpathtools
-Download-URL:
https://github.com/mathandy/svgpathtools/releases/download/1.7.2/svgpathtools-1.7.2-py3-none-any.whl
+Download-URL:
https://github.com/mathandy/svgpathtools/releases/download/1.8.0/svgpathtools-1.8.0-py3-none-any.whl
Author: Andy Port
Author-email: [email protected]
License: MIT
@@ -20,6 +20,7 @@
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
+Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Multimedia :: Graphics :: Editors :: Vector-Based
Classifier: Topic :: Scientific/Engineering
Classifier: Topic :: Scientific/Engineering :: Image Recognition
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/svgpathtools-1.7.2/test/test_parsing.py
new/svgpathtools-1.8.0/test/test_parsing.py
--- old/svgpathtools-1.7.2/test/test_parsing.py 2025-11-30 20:14:37.000000000
+0100
+++ new/svgpathtools-1.8.0/test/test_parsing.py 2026-09-05 21:33:29.000000000
+0200
@@ -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
@@ -170,6 +173,36 @@
path2 = parse_path('M 100 200 c 10 -5 20 -10 30 -20')
self.assertEqual(path1, path2)
+ def test_arc_flag_packing(self):
+ """Arc flags may run straight into the next number (#129)."""
+ # Each packed form must parse identically to the spaced-out form.
+ equivalent = [
+ ('M 0 0 A 5 5 0 0110 0', 'M 0 0 A 5 5 0 0 1 10 0'),
+ ('M 0 0 A 5 5 0 1110 0', 'M 0 0 A 5 5 0 1 1 10 0'),
+ ('M 0 0 A 5 5 0 0010 0', 'M 0 0 A 5 5 0 0 0 10 0'),
+ ('M 0 0 a 5 5 0 0010 0', 'M 0 0 a 5 5 0 0 0 10 0'),
+ ('M 0 0 A 5 5 0 01-10 0', 'M 0 0 A 5 5 0 0 1 -10 0'),
+ ('M 0 0 A 5 5 0 0 1 10 0 5 5 0 1120 0',
+ 'M 0 0 A 5 5 0 0 1 10 0 5 5 0 1 1 20 0'),
+ ]
+ for packed, spaced in equivalent:
+ self.assertEqual(parse_path(packed), parse_path(spaced))
+
+ # The flags carry through to the resulting Arc.
+ arc = parse_path('M 0 0 A 5 5 0 0110 0')[0]
+ self.assertFalse(arc.large_arc)
+ self.assertTrue(arc.sweep)
+ self.assertEqual(arc.end, 10 + 0j)
+
+ # Real-world path from issue #129 (simple-icons "emlakjet") with packed
+ # "00" flags; this used to raise IndexError.
+ icon = 'M12 0a3.543 3.543 0 00-1.267 2.471A3.543 3.543 0 0012 0z'
+ path = parse_path(icon)
+ self.assertEqual(len(path), 2)
+ self.assertTrue(all(isinstance(seg, Arc) for seg in path))
+ self.assertFalse(path[0].large_arc)
+ self.assertFalse(path[0].sweep)
+
def test_numbers(self):
"""Exponents and other number format cases"""
# It can be e or E, the plus is optional, and a minimum of
@@ -182,6 +215,22 @@
self.assertRaises(ValueError, parse_path,
'M 100 100 L 200 200 Z 100 200')
+ def test_truncated_commands(self):
+ # A command missing some of its coordinates used to pop past the end of
+ # the token list and raise a bare IndexError instead of a ValueError.
+ for d in ('M', 'M 10', 'M 0 0 L', 'M 0 0 C 1 1 2 2',
+ 'H', 'V', 'Q 1 1', 'A 1 1 0 0 1'):
+ self.assertRaises(ValueError, parse_path, d)
+
+ def test_leading_smooth_command(self):
+ # A path starting with a smooth command (S/T) has no previous control
+ # point to reflect; the first control point coincides with the current
+ # point. This used to raise a TypeError from ``None in 'CS'``.
+ self.assertEqual(parse_path('S 1 1 2 2'),
+ Path(CubicBezier(0j, 0j, 1 + 1j, 2 + 2j)))
+ self.assertEqual(parse_path('T 1 1'),
+ Path(QuadraticBezier(0j, 0j, 1 + 1j)))
+
def test_transform(self):
@@ -249,6 +298,152 @@
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 = ('<svg xmlns="http://www.w3.org/2000/svg">'
+ '<path d="M 0,0 L 1,1" transform="translate(a)"/></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 = ('<svg xmlns="http://www.w3.org/2000/svg">'
+ '<path d="M 0,0 L 1,1" transform="translate(a)"/></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")