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 <m...@0x0.st> + +- 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: andyap...@gmail.com 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 = 'andyap...@gmail.com' 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: andyap...@gmail.com 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()