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 2025-06-10 09:08:16
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-svgpathtools (Old)
and /work/SRC/openSUSE:Factory/.python-svgpathtools.new.19631 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-svgpathtools"
Tue Jun 10 09:08:16 2025 rev:9 rq:1284231 version:1.7.1
Changes:
--------
--- /work/SRC/openSUSE:Factory/python-svgpathtools/python-svgpathtools.changes
2025-05-09 18:54:02.727785868 +0200
+++
/work/SRC/openSUSE:Factory/.python-svgpathtools.new.19631/python-svgpathtools.changes
2025-06-10 09:10:31.284422460 +0200
@@ -1,0 +2,14 @@
+Mon Jun 9 13:55:49 UTC 2025 - Mia Herkt <[email protected]>
+
+- Switch to pip-based build
+- Update to 1.7.1
+Fixes:
+ * Rotation angle calculation when transforming arcs
+ * Converting rounded rect to a d-string path
+ * Floating point error in bezier bbox calculation
+ * Skip end path in polyline if no dedicated end path is provided
+Changes:
+ * When converting ellipses to d-string paths, use arcs by default
+ * Add various new test cases
+
+-------------------------------------------------------------------
Old:
----
svgpathtools-1.7.0.tar.gz
New:
----
svgpathtools-1.7.1.tar.gz
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Other differences:
------------------
++++++ python-svgpathtools.spec ++++++
--- /var/tmp/diff_new_pack.iV6W2s/_old 2025-06-10 09:10:31.940449588 +0200
+++ /var/tmp/diff_new_pack.iV6W2s/_new 2025-06-10 09:10:31.940449588 +0200
@@ -18,13 +18,14 @@
%{?sle15_python_module_pythons}
Name: python-svgpathtools
-Version: 1.7.0
+Version: 1.7.1
Release: 0
Summary: Tools for manipulating and analyzing SVG Path objects and
Bézier curves
License: MIT
URL: https://github.com/mathandy/svgpathtools
Source:
https://files.pythonhosted.org/packages/source/s/svgpathtools/svgpathtools-%{version}.tar.gz
BuildRequires: %{python_module numpy}
+BuildRequires: %{python_module pip}
BuildRequires: %{python_module pytest}
BuildRequires: %{python_module scipy}
BuildRequires: %{python_module setuptools}
@@ -45,10 +46,10 @@
%setup -q -n svgpathtools-%{version}
%build
-%python_build
+%pyproject_wheel
%install
-%python_install
+%pyproject_install
%python_expand %fdupes %{buildroot}%{$python_sitelib}
%check
++++++ svgpathtools-1.7.0.tar.gz -> svgpathtools-1.7.1.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/svgpathtools-1.7.0/PKG-INFO
new/svgpathtools-1.7.1/PKG-INFO
--- old/svgpathtools-1.7.0/PKG-INFO 2025-05-08 02:17:50.233075600 +0200
+++ new/svgpathtools-1.7.1/PKG-INFO 2025-05-28 01:31:28.195323500 +0200
@@ -1,9 +1,9 @@
Metadata-Version: 2.1
Name: svgpathtools
-Version: 1.7.0
+Version: 1.7.1
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.0/svgpathtools-1.7.0-py3-none-any.whl
+Download-URL:
https://github.com/mathandy/svgpathtools/releases/download/1.7.1/svgpathtools-1.7.1-py3-none-any.whl
Author: Andy Port
Author-email: [email protected]
License: MIT
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/svgpathtools-1.7.0/setup.py
new/svgpathtools-1.7.1/setup.py
--- old/svgpathtools-1.7.0/setup.py 2025-05-08 02:17:39.000000000 +0200
+++ new/svgpathtools-1.7.1/setup.py 2025-05-28 01:31:17.000000000 +0200
@@ -3,7 +3,7 @@
import os
-VERSION = '1.7.0'
+VERSION = '1.7.1'
AUTHOR_NAME = 'Andy Port'
AUTHOR_EMAIL = '[email protected]'
GITHUB = 'https://github.com/mathandy/svgpathtools'
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/svgpathtools-1.7.0/svgpathtools/bezier.py
new/svgpathtools-1.7.1/svgpathtools/bezier.py
--- old/svgpathtools-1.7.0/svgpathtools/bezier.py 2025-05-08
02:17:39.000000000 +0200
+++ new/svgpathtools-1.7.1/svgpathtools/bezier.py 2025-05-28
01:31:17.000000000 +0200
@@ -10,6 +10,7 @@
# Internal dependencies
from .polytools import real, imag, polyroots, polyroots01
+from .constants import FLOAT_EPSILON
# Evaluation ##################################################################
@@ -171,7 +172,7 @@
if len(p) == 4: # cubic case
a = [p.real for p in p]
denom = a[0] - 3*a[1] + 3*a[2] - a[3]
- if denom != 0:
+ if abs(denom) > FLOAT_EPSILON: # check that denom != 0 accounting for
floating point error
delta = a[1]**2 - (a[0] + a[1])*a[2] + a[2]**2 + (a[0] - a[1])*a[3]
if delta >= 0: # otherwise no local extrema
sqdelta = sqrt(delta)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/svgpathtools-1.7.0/svgpathtools/constants.py
new/svgpathtools-1.7.1/svgpathtools/constants.py
--- old/svgpathtools-1.7.0/svgpathtools/constants.py 1970-01-01
01:00:00.000000000 +0100
+++ new/svgpathtools-1.7.1/svgpathtools/constants.py 2025-05-28
01:31:17.000000000 +0200
@@ -0,0 +1,3 @@
+"""This submodule contains constants used throughout the project."""
+
+FLOAT_EPSILON = 1e-12
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/svgpathtools-1.7.0/svgpathtools/path.py
new/svgpathtools-1.7.1/svgpathtools/path.py
--- old/svgpathtools-1.7.0/svgpathtools/path.py 2025-05-08 02:17:39.000000000
+0200
+++ new/svgpathtools-1.7.1/svgpathtools/path.py 2025-05-28 01:31:17.000000000
+0200
@@ -335,7 +335,7 @@
new_radius = complex(rx, ry)
xeigvec = eigvecs[:, 0]
- rot = np.degrees(np.arccos(xeigvec[0]))
+ rot = np.degrees(np.arctan2(xeigvec[1], xeigvec[0]))
if new_radius.real == 0 or new_radius.imag == 0 :
return Line(new_start, new_end)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/svgpathtools-1.7.0/svgpathtools/svg_to_paths.py
new/svgpathtools-1.7.1/svgpathtools/svg_to_paths.py
--- old/svgpathtools-1.7.0/svgpathtools/svg_to_paths.py 2025-05-08
02:17:39.000000000 +0200
+++ new/svgpathtools-1.7.1/svgpathtools/svg_to_paths.py 2025-05-28
01:31:17.000000000 +0200
@@ -27,7 +27,7 @@
return path.get('d', '')
-def ellipse2pathd(ellipse):
+def ellipse2pathd(ellipse, use_cubics=False):
"""converts the parameters from an ellipse or a circle to a string for a
Path object d-attribute"""
@@ -46,10 +46,32 @@
cx = float(cx)
cy = float(cy)
- d = ''
- d += 'M' + str(cx - rx) + ',' + str(cy)
- d += 'a' + str(rx) + ',' + str(ry) + ' 0 1,0 ' + str(2 * rx) + ',0'
- d += 'a' + str(rx) + ',' + str(ry) + ' 0 1,0 ' + str(-2 * rx) + ',0'
+ if use_cubics:
+ # Modified by NXP 2024, 2025
+ PATH_KAPPA = 0.552284
+ rxKappa = rx * PATH_KAPPA;
+ ryKappa = ry * PATH_KAPPA;
+
+ #According to the SVG specification
(https://lists.w3.org/Archives/Public/www-archive/2005May/att-0005/SVGT12_Main.pdf),
+ #Section 9.4, "The 'ellipse' element": "The arc of an 'ellipse'
element begins at the "3 o'clock" point on
+ #the radius and progresses towards the "9 o'clock". Therefore, the
ellipse begins at the rightmost point
+ #and progresses clockwise.
+ d = ''
+ # Move to the rightmost point
+ d += 'M' + str(cx + rx) + ' ' + str(cy)
+ # Draw bottom-right quadrant
+ d += 'C' + str(cx + rx) + ' ' + str(cy + ryKappa) + ' ' + str(cx +
rxKappa) + ' ' + str(cy + ry) + ' ' + str(cx) + ' ' + str(cy + ry)
+ # Draw bottom-left quadrant
+ d += 'C' + str(cx - rxKappa) + ' ' + str(cy + ry) + ' ' + str(cx - rx)
+ ' ' + str(cy + ryKappa) + ' ' + str(cx - rx) + ' ' + str(cy)
+ # Draw top-left quadrant
+ d += 'C' + str(cx - rx) + ' ' + str(cy - ryKappa) + ' ' + str(cx -
rxKappa) + ' ' + str(cy - ry) + ' ' + str(cx) + ' ' + str(cy - ry)
+ # Draw top-right quadrant
+ d += 'C' + str(cx + rxKappa) + ' ' + str(cy - ry) + ' ' + str(cx + rx)
+ ' ' + str(cy - ryKappa) + ' ' + str(cx + rx) + ' ' + str(cy)
+ else:
+ d = ''
+ d += 'M' + str(cx - rx) + ',' + str(cy)
+ d += 'a' + str(rx) + ',' + str(ry) + ' 0 1,0 ' + str(2 * rx) + ',0'
+ d += 'a' + str(rx) + ',' + str(ry) + ' 0 1,0 ' + str(-2 * rx) + ',0'
return d + 'z'
@@ -62,6 +84,9 @@
else:
points = COORD_PAIR_TMPLT.findall(polyline.get('points', ''))
+ if len(points) == 0:
+ return ''
+
closed = (float(points[0][0]) == float(points[-1][0]) and
float(points[0][1]) == float(points[-1][1]))
@@ -77,13 +102,13 @@
return d
-def polygon2pathd(polyline):
+def polygon2pathd(polyline, is_polygon=True):
"""converts the string from a polygon points-attribute to a string
for a Path object d-attribute.
Note: For a polygon made from n points, the resulting path will be
composed of n lines (even if some of these lines have length zero).
"""
- return polyline2pathd(polyline, True)
+ return polyline2pathd(polyline, is_polygon)
def rect2pathd(rect):
@@ -93,7 +118,8 @@
rectangle object and proceed counter-clockwise."""
x, y = float(rect.get('x', 0)), float(rect.get('y', 0))
w, h = float(rect.get('width', 0)), float(rect.get('height', 0))
- if 'rx' in rect or 'ry' in rect:
+
+ if 'rx' in rect.keys() or 'ry' in rect.keys():
# if only one, rx or ry, is present, use that value for both
# https://developer.mozilla.org/en-US/docs/Web/SVG/Element/rect
@@ -204,7 +230,7 @@
# path strings, add to list
if convert_polygons_to_paths:
pgons = [dom2dict(el) for el in doc.getElementsByTagName('polygon')]
- d_strings += [polygon2pathd(pg) for pg in pgons]
+ d_strings += [polygon2pathd(pg, True) for pg in pgons]
attribute_dictionary_list += pgons
if convert_lines_to_paths:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/svgpathtools-1.7.0/svgpathtools.egg-info/PKG-INFO
new/svgpathtools-1.7.1/svgpathtools.egg-info/PKG-INFO
--- old/svgpathtools-1.7.0/svgpathtools.egg-info/PKG-INFO 2025-05-08
02:17:50.000000000 +0200
+++ new/svgpathtools-1.7.1/svgpathtools.egg-info/PKG-INFO 2025-05-28
01:31:28.000000000 +0200
@@ -1,9 +1,9 @@
Metadata-Version: 2.1
Name: svgpathtools
-Version: 1.7.0
+Version: 1.7.1
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.0/svgpathtools-1.7.0-py3-none-any.whl
+Download-URL:
https://github.com/mathandy/svgpathtools/releases/download/1.7.1/svgpathtools-1.7.1-py3-none-any.whl
Author: Andy Port
Author-email: [email protected]
License: MIT
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/svgpathtools-1.7.0/svgpathtools.egg-info/SOURCES.txt
new/svgpathtools-1.7.1/svgpathtools.egg-info/SOURCES.txt
--- old/svgpathtools-1.7.0/svgpathtools.egg-info/SOURCES.txt 2025-05-08
02:17:50.000000000 +0200
+++ new/svgpathtools-1.7.1/svgpathtools.egg-info/SOURCES.txt 2025-05-28
01:31:28.000000000 +0200
@@ -15,6 +15,7 @@
vectorframes.svg
svgpathtools/__init__.py
svgpathtools/bezier.py
+svgpathtools/constants.py
svgpathtools/document.py
svgpathtools/misctools.py
svgpathtools/parser.py
@@ -35,6 +36,8 @@
test/groups.svg
test/negative-scale.svg
test/polygons.svg
+test/polygons_no_points.svg
+test/polyline.svg
test/rects.svg
test/test.svg
test/test_bezier.py
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/svgpathtools-1.7.0/test/polygons_no_points.svg
new/svgpathtools-1.7.1/test/polygons_no_points.svg
--- old/svgpathtools-1.7.0/test/polygons_no_points.svg 1970-01-01
01:00:00.000000000 +0100
+++ new/svgpathtools-1.7.1/test/polygons_no_points.svg 2025-05-28
01:31:17.000000000 +0200
@@ -0,0 +1,5 @@
+<?xml version="1.0" ?>
+<svg baseProfile="full" height="600px" version="1.1" viewBox="-10.05 -10.05
120.1 120.1" width="600px" xmlns="http://www.w3.org/2000/svg"
xmlns:ev="http://www.w3.org/2001/xml-events"
xmlns:xlink="http://www.w3.org/1999/xlink">
+ <polygon xml:id="polygon-01" fill="red" stroke="black" />
+ <polygon xml:id="polygon-02" fill="red" stroke="black" points="" />
+</svg>
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/svgpathtools-1.7.0/test/polyline.svg
new/svgpathtools-1.7.1/test/polyline.svg
--- old/svgpathtools-1.7.0/test/polyline.svg 1970-01-01 01:00:00.000000000
+0100
+++ new/svgpathtools-1.7.1/test/polyline.svg 2025-05-28 01:31:17.000000000
+0200
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg version="1.2" baseProfile="tiny" xml:id="svg-root" width="100%"
height="100%"
+ viewBox="0 0 480 360" xmlns="http://www.w3.org/2000/svg"
+ xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:xe="http://www.w3.org/2001/xml-events">
+ <polyline xml:id="polyline-04" fill="none" stroke="red" stroke-width="8"
points="59,185,98,203,108,245,82,279,39,280,11,247,19,205" />
+ <polyline xml:id="polyline-02" fill="none" stroke="blue" stroke-width="8"
points="220,50,267,84,249,140,190,140,172,84,220,50" />
+</svg>
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/svgpathtools-1.7.0/test/test_bezier.py
new/svgpathtools-1.7.1/test/test_bezier.py
--- old/svgpathtools-1.7.0/test/test_bezier.py 2025-05-08 02:17:39.000000000
+0200
+++ new/svgpathtools-1.7.1/test/test_bezier.py 2025-05-28 01:31:17.000000000
+0200
@@ -1,8 +1,8 @@
from __future__ import division, absolute_import, print_function
import numpy as np
import unittest
-from svgpathtools.bezier import bezier_point, bezier2polynomial,
polynomial2bezier
-from svgpathtools.path import bpoints2bezier
+from svgpathtools.bezier import bezier_point, bezier2polynomial,
polynomial2bezier, bezier_bounding_box, bezier_real_minmax
+from svgpathtools.path import bpoints2bezier, CubicBezier
seed = 2718
@@ -58,5 +58,53 @@
self.assertAlmostEqual(b.point(t), p(t), msg=msg)
+class TestBezierBoundingBox(unittest.TestCase):
+ def test_bezier_bounding_box(self):
+ # This bezier curve has denominator == 0 but due to floating point
arithmetic error it is not exactly 0
+ zero_denominator_bezier_curve = CubicBezier(612.547 + 109.3261j,
579.967 - 19.4422j, 428.0344 - 19.4422j, 395.4374 + 109.3261j)
+ zero_denom_xmin, zero_denom_xmax, zero_denom_ymin, zero_denom_ymax =
bezier_bounding_box(zero_denominator_bezier_curve)
+ self.assertAlmostEqual(zero_denom_xmin, 395.437400, 5)
+ self.assertAlmostEqual(zero_denom_xmax, 612.547, 5)
+ self.assertAlmostEqual(zero_denom_ymin, 12.7498749, 5)
+ self.assertAlmostEqual(zero_denom_ymax, 109.3261, 5)
+
+ # This bezier curve has global extrema at the start and end points
+ start_end_bbox_bezier_curve = CubicBezier(886.8238 + 354.8439j,
884.4765 + 340.5983j, 877.6258 + 330.0518j, 868.2909 + 323.2453j)
+ start_end_xmin, start_end_xmax, start_end_ymin, start_end_ymax =
bezier_bounding_box(start_end_bbox_bezier_curve)
+ self.assertAlmostEqual(start_end_xmin, 868.2909, 5)
+ self.assertAlmostEqual(start_end_xmax, 886.8238, 5)
+ self.assertAlmostEqual(start_end_ymin, 323.2453, 5)
+ self.assertAlmostEqual(start_end_ymax, 354.8439, 5)
+
+ # This bezier curve is to cover some random case where at least one of
the global extrema is not the start or end point
+ general_bezier_curve = CubicBezier(295.2282 + 402.0233j, 310.3734 +
355.5329j, 343.547 + 340.5983j, 390.122 + 355.7018j)
+ general_xmin, general_xmax, general_ymin, general_ymax =
bezier_bounding_box(general_bezier_curve)
+ self.assertAlmostEqual(general_xmin, 295.2282, 5)
+ self.assertAlmostEqual(general_xmax, 390.121999999, 5)
+ self.assertAlmostEqual(general_ymin, 350.030030142, 5)
+ self.assertAlmostEqual(general_ymax, 402.0233, 5)
+
+
+class TestBezierRealMinMax(unittest.TestCase):
+ def test_bezier_real_minmax(self):
+ # This bezier curve has denominator == 0 but due to floating point
arithmetic error it is not exactly 0
+ zero_denominator_bezier_curve = [109.3261, -19.4422, -19.4422,
109.3261]
+ zero_denominator_minmax =
bezier_real_minmax(zero_denominator_bezier_curve)
+ self.assertAlmostEqual(zero_denominator_minmax[0], 12.7498749, 5)
+ self.assertAlmostEqual(zero_denominator_minmax[1], 109.3261, 5)
+
+ # This bezier curve has global extrema at the start and end points
+ start_end_bbox_bezier_curve = [354.8439, 340.5983, 330.0518, 323.2453]
+ start_end_bbox_minmax = bezier_real_minmax(start_end_bbox_bezier_curve)
+ self.assertAlmostEqual(start_end_bbox_minmax[0], 323.2453, 5)
+ self.assertAlmostEqual(start_end_bbox_minmax[1], 354.8439, 5)
+
+ # This bezier curve is to cover some random case where at least one of
the global extrema is not the start or end point
+ general_bezier_curve = [402.0233, 355.5329, 340.5983, 355.7018]
+ general_minmax = bezier_real_minmax(general_bezier_curve)
+ self.assertAlmostEqual(general_minmax[0], 350.030030142, 5)
+ self.assertAlmostEqual(general_minmax[1], 402.0233, 5)
+
+
if __name__ == '__main__':
unittest.main()
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/svgpathtools-1.7.0/test/test_generation.py
new/svgpathtools-1.7.1/test/test_generation.py
--- old/svgpathtools-1.7.0/test/test_generation.py 2025-05-08
02:17:39.000000000 +0200
+++ new/svgpathtools-1.7.1/test/test_generation.py 2025-05-28
01:31:17.000000000 +0200
@@ -82,6 +82,12 @@
psf = 'M 0.0,0.0 L 340.0,-10.0 L 100.0,100.0 L 200.0,0.0'
self.assertTrue(parse_path(path).d() in (ps, psf))
+ def test_floating_point_stability(self):
+ # Check that reading and then outputting a d-string
+ # does not introduce floating point error noise.
+ path = "M 70.63,10.42 C 0.11,0.33 -0.89,2.09 -1.54,2.45 C -4.95,2.73
-17.52,7.24 -39.46,11.04"
+ self.assertEqual(parse_path(path).d(), path)
+
if __name__ == '__main__':
unittest.main()
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/svgpathtools-1.7.0/test/test_groups.py
new/svgpathtools-1.7.1/test/test_groups.py
--- old/svgpathtools-1.7.0/test/test_groups.py 2025-05-08 02:17:39.000000000
+0200
+++ new/svgpathtools-1.7.1/test/test_groups.py 2025-05-28 01:31:17.000000000
+0200
@@ -46,6 +46,65 @@
self.check_values(tf.dot(v_s), actual.start)
self.check_values(tf.dot(v_e), actual.end)
+ def test_nonrounded_rect(self):
+ # Check that (nonrounded) rect is parsed properly
+
+ x, y = 10, 10
+ w, h = 100, 100
+
+ doc = Document.from_svg_string(
+ "\n".join(
+ [
+ '<svg width="200" height="200"
xmlns="http://www.w3.org/2000/svg"',
+ ' style="fill:green;stroke:black;stroke-width:1.5">',
+ f' <rect x="{x}" y="{y}" width="{w}" height="{h}"/>',
+ "</svg>",
+ ]
+ )
+ )
+
+ line_count, arc_count = 0, 0
+
+ for p in doc.paths():
+ for s in p:
+ if isinstance(s, Line):
+ line_count += 1
+ if isinstance(s, Arc):
+ arc_count += 1
+
+ self.assertEqual(line_count, 4)
+ self.assertEqual(arc_count, 0)
+
+ def test_rounded_rect(self):
+ # Check that rounded rect is parsed properly
+
+ x, y = 10, 10
+ rx, ry = 15, 12
+ w, h = 100, 100
+
+ doc = Document.from_svg_string(
+ "\n".join(
+ [
+ '<svg width="200" height="200"
xmlns="http://www.w3.org/2000/svg"',
+ ' style="fill:green;stroke:black;stroke-width:1.5">',
+ f' <rect x="{x}" y="{y}" rx="{rx}" ry="{ry}" width="{w}"
height="{h}"/>',
+ "</svg>",
+ ]
+ )
+ )
+
+ line_count, arc_count = 0, 0
+
+ for p in doc.paths():
+ for s in p:
+ if isinstance(s, Line):
+ line_count += 1
+ if isinstance(s, Arc):
+ arc_count += 1
+
+ self.assertEqual(line_count, 4)
+ self.assertEqual(arc_count, 4)
+
def test_group_transform(self):
# The input svg has a group transform of "scale(1,-1)", which
# can mess with Arc sweeps.
@@ -62,6 +121,42 @@
self.assertEqual(path[8], Line(start=(90+0j), end=(10+0j)))
self.assertEqual(path[9], Arc(start=(10+0j), radius=(10+10j),
rotation=0.0, large_arc=False, sweep=True, end=-10j))
+ def test_ellipse_transform(self):
+ # Check that ellipse to path conversion respects rotation transforms
+
+ cx, cy = 40, 80
+ rx, ry = 15, 20
+
+ def dist_to_ellipse(angle, pt):
+ rot = np.exp(-1j * np.radians(angle))
+ transformed_pt = rot * complex(pt.real - cx, pt.imag - cy)
+ return transformed_pt.real**2 / rx**2 + transformed_pt.imag**2 /
ry**2 - 1
+
+ for angle in np.linspace(-179, 180, num=123):
+ svgstring = "\n".join(
+ [
+ '<svg width="200" height="200"
xmlns="http://www.w3.org/2000/svg"',
+ ' style="fill:green;stroke:black;stroke-width:1.5">',
+ f' <ellipse cx="{cx}" cy="{cy}" rx="{rx}" ry="{ry}"
transform="rotate({angle} {cx} {cy})"/>',
+ "</svg>",
+ ]
+ )
+
+ doc = Document.from_svg_string(svgstring)
+
+ for p in doc.paths():
+ subtended_angle = 0.0
+ for s in p:
+ if isinstance(s, Arc):
+ # check that several points lie on the original ellipse
+ for t in [0.0, 1 / 3.0, 0.5, 2 / 3.0, 1.0]:
+ dist = dist_to_ellipse(angle, s.point(t))
+ self.assertAlmostEqual(dist, 0)
+
+ # and that the subtended angles sum to 2*pi
+ subtended_angle = subtended_angle + s.delta
+ self.assertAlmostEqual(np.abs(subtended_angle), 360)
+
def test_group_flatten(self):
# Test the Document.paths() function against the
# groups.svg test file.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/svgpathtools-1.7.0/test/test_svg2paths.py
new/svgpathtools-1.7.1/test/test_svg2paths.py
--- old/svgpathtools-1.7.0/test/test_svg2paths.py 2025-05-08
02:17:39.000000000 +0200
+++ new/svgpathtools-1.7.1/test/test_svg2paths.py 2025-05-28
01:31:17.000000000 +0200
@@ -81,10 +81,47 @@
shutil.rmtree(tmpdir)
def test_rect2pathd(self):
- non_rounded = {"x":"10", "y":"10", "width":"100","height":"100"}
- self.assertEqual(rect2pathd(non_rounded), 'M10.0 10.0 L 110.0 10.0 L
110.0 110.0 L 10.0 110.0 z')
- rounded = {"x":"10", "y":"10", "width":"100","height":"100",
"rx":"15", "ry": "12"}
- self.assertEqual(rect2pathd(rounded), "M 25.0 10.0 L 95.0 10.0 A 15.0
12.0 0 0 1 110.0 22.0 L 110.0 98.0 A 15.0 12.0 0 0 1 95.0 110.0 L 25.0 110.0 A
15.0 12.0 0 0 1 10.0 98.0 L 10.0 22.0 A 15.0 12.0 0 0 1 25.0 10.0 z")
+ non_rounded_dict = {"x": "10", "y": "10", "width": "100", "height":
"100"}
+ self.assertEqual(
+ rect2pathd(non_rounded_dict),
+ "M10.0 10.0 L 110.0 10.0 L 110.0 110.0 L 10.0 110.0 z",
+ )
+
+ non_rounded_svg = """<?xml version="1.0" encoding="UTF-8"?>
+ <svg xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg" width="200mm" height="200mm"
version="1.1">
+ <rect id="non_rounded" x="10" y="10" width="100" height="100" />
+ </svg>"""
+
+ paths, _ = svg2paths(StringIO(non_rounded_svg))
+ self.assertEqual(len(paths), 1)
+ self.assertTrue(paths[0].isclosed())
+ self.assertEqual(
+ paths[0].d(use_closed_attrib=True),
+ "M 10.0,10.0 L 110.0,10.0 L 110.0,110.0 L 10.0,110.0 Z",
+ )
+ self.assertEqual(
+ paths[0].d(use_closed_attrib=False),
+ "M 10.0,10.0 L 110.0,10.0 L 110.0,110.0 L 10.0,110.0 L 10.0,10.0",
+ )
+
+ rounded_dict = {"x": "10", "y": "10", "width": "100","height": "100",
"rx": "15", "ry": "12"}
+ self.assertEqual(
+ rect2pathd(rounded_dict),
+ "M 25.0 10.0 L 95.0 10.0 A 15.0 12.0 0 0 1 110.0 22.0 L 110.0 98.0
A 15.0 12.0 0 0 1 95.0 110.0 L 25.0 110.0 A 15.0 12.0 0 0 1 10.0 98.0 L 10.0
22.0 A 15.0 12.0 0 0 1 25.0 10.0 z",
+ )
+
+ rounded_svg = """<?xml version="1.0" encoding="UTF-8"?>
+ <svg xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg" width="200mm" height="200mm"
version="1.1">
+ <rect id="rounded" x="10" y="10" width="100" height ="100"
rx="15" ry="12" />
+ </svg>"""
+
+ paths, _ = svg2paths(StringIO(rounded_svg))
+ self.assertEqual(len(paths), 1)
+ self.assertTrue(paths[0].isclosed())
+ self.assertEqual(
+ paths[0].d(),
+ "M 25.0,10.0 L 95.0,10.0 A 15.0,12.0 0.0 0,1 110.0,22.0 L
110.0,98.0 A 15.0,12.0 0.0 0,1 95.0,110.0 L 25.0,110.0 A 15.0,12.0 0.0 0,1
10.0,98.0 L 10.0,22.0 A 15.0,12.0 0.0 0,1 25.0,10.0",
+ )
def test_from_file_path_string(self):
"""Test reading svg from file provided as path"""
@@ -131,6 +168,44 @@
self.assertEqual(len(paths), 2)
+ def test_svg2paths_polygon_no_points(self):
+
+ paths, _ = svg2paths(join(dirname(__file__), 'polygons_no_points.svg'))
+
+ path = paths[0]
+ path_correct = Path()
+ self.assertTrue(len(path)==0)
+ self.assertTrue(path==path_correct)
+
+ path = paths[1]
+ self.assertTrue(len(path)==0)
+ self.assertTrue(path==path_correct)
+
+ def test_svg2paths_polyline_tests(self):
+
+ paths, _ = svg2paths(join(dirname(__file__), 'polyline.svg'))
+
+ path = paths[0]
+ path_correct = Path(Line(59+185j, 98+203j),
+ Line(98+203j, 108+245j),
+ Line(108+245j, 82+279j),
+ Line(82+279j, 39+280j),
+ Line(39+280j, 11+247j),
+ Line(11+247j, 19+205j))
+ self.assertFalse(path.isclosed())
+ self.assertTrue(len(path)==6)
+ self.assertTrue(path==path_correct)
+
+ path = paths[1]
+ path_correct = Path(Line(220+50j, 267+84j),
+ Line(267+84j, 249+140j),
+ Line(249+140j, 190+140j),
+ Line(190+140j, 172+84j),
+ Line(172+84j, 220+50j))
+ self.assertTrue(path.isclosed())
+ self.assertTrue(len(path)==5)
+ self.assertTrue(path==path_correct)
+
if __name__ == '__main__':
unittest.main()