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

Reply via email to