Merge branch 'master' into PyPI

preserve-order
Andrew Port 2022-06-05 22:21:36 -07:00
commit 740e2bf991
11 changed files with 229 additions and 29 deletions

View File

@ -12,7 +12,7 @@ jobs:
strategy: strategy:
matrix: matrix:
os: [ubuntu-latest, macos-latest, windows-latest] os: [ubuntu-latest, macos-latest, windows-latest]
python-version: [2.7, 3.5, 3.6, 3.7, 3.8, 3.9] python-version: [2.7, 3.5, 3.6, 3.7, 3.8, 3.9, "3.10"]
steps: steps:
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
- uses: actions/checkout@v2 - uses: actions/checkout@v2

View File

@ -1,2 +1,3 @@
numpy numpy
svgwrite svgwrite
scipy

View File

@ -3,7 +3,7 @@ import codecs
import os import os
VERSION = '1.4.3' VERSION = '1.4.4'
AUTHOR_NAME = 'Andy Port' AUTHOR_NAME = 'Andy Port'
AUTHOR_EMAIL = 'AndyAPort@gmail.com' AUTHOR_EMAIL = 'AndyAPort@gmail.com'
GITHUB = 'https://github.com/mathandy/svgpathtools' GITHUB = 'https://github.com/mathandy/svgpathtools'
@ -32,7 +32,6 @@ setup(name='svgpathtools',
license='MIT', license='MIT',
install_requires=['numpy', 'svgwrite', 'scipy'], install_requires=['numpy', 'svgwrite', 'scipy'],
platforms="OS Independent", platforms="OS Independent",
requires=['numpy', 'svgwrite', 'scipy'],
keywords=['svg', 'svg path', 'svg.path', 'bezier', 'parse svg path', 'display svg'], keywords=['svg', 'svg path', 'svg.path', 'bezier', 'parse svg path', 'display svg'],
classifiers=[ classifiers=[
"Development Status :: 4 - Beta", "Development Status :: 4 - Beta",
@ -47,6 +46,7 @@ setup(name='svgpathtools',
"Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Topic :: Multimedia :: Graphics :: Editors :: Vector-Based", "Topic :: Multimedia :: Graphics :: Editors :: Vector-Based",
"Topic :: Scientific/Engineering", "Topic :: Scientific/Engineering",
"Topic :: Scientific/Engineering :: Image Recognition", "Topic :: Scientific/Engineering :: Image Recognition",

View File

@ -17,6 +17,6 @@ from .document import (Document, CONVERSIONS, CONVERT_ONLY_PATHS,
from .svg_io_sax import SaxDocument from .svg_io_sax import SaxDocument
try: try:
from .svg_to_paths import svg2paths, svg2paths2 from .svg_to_paths import svg2paths, svg2paths2, svgstr2paths
except ImportError: except ImportError:
pass pass

View File

@ -41,6 +41,7 @@ import xml.etree.ElementTree as etree
from xml.etree.ElementTree import Element, SubElement, register_namespace from xml.etree.ElementTree import Element, SubElement, register_namespace
from xml.dom.minidom import parseString from xml.dom.minidom import parseString
import warnings import warnings
from io import StringIO
from tempfile import gettempdir from tempfile import gettempdir
from time import time from time import time
@ -54,9 +55,13 @@ from .path import *
# To maintain forward/backward compatibility # To maintain forward/backward compatibility
try: try:
str = basestring string = basestring
except NameError: except NameError:
pass string = str
try:
from os import PathLike
except ImportError:
PathLike = string
# Let xml.etree.ElementTree know about the SVG namespace # Let xml.etree.ElementTree know about the SVG namespace
SVG_NAMESPACE = {'svg': 'http://www.w3.org/2000/svg'} SVG_NAMESPACE = {'svg': 'http://www.w3.org/2000/svg'}
@ -235,13 +240,14 @@ class Document:
The output Path objects will be transformed based on their parent groups. The output Path objects will be transformed based on their parent groups.
Args: Args:
filepath (str): The filepath of the DOM-style object. filepath (str or file-like): The filepath of the
DOM-style object or a file-like object containing it.
""" """
# remember location of original svg file # strings are interpreted as file location everything else is treated as
self.original_filepath = filepath # file-like object and passed to the xml parser directly
if filepath is not None and os.path.dirname(filepath) == '': from_filepath = isinstance(filepath, string) or isinstance(filepath, PathLike)
self.original_filepath = os.path.join(os.getcwd(), filepath) self.original_filepath = os.path.abspath(filepath) if from_filepath else None
if filepath is None: if filepath is None:
self.tree = etree.ElementTree(Element('svg')) self.tree = etree.ElementTree(Element('svg'))
@ -251,6 +257,16 @@ class Document:
self.root = self.tree.getroot() self.root = self.tree.getroot()
@classmethod
def from_svg_string(cls, svg_string):
"""Factory method for creating a document from a string holding a svg
object
"""
# wrap string into StringIO object
svg_file_obj = StringIO(svg_string)
# create document from file object
return Document(svg_file_obj)
def paths(self, group_filter=lambda x: True, def paths(self, group_filter=lambda x: True,
path_filter=lambda x: True, path_conversions=CONVERSIONS): path_filter=lambda x: True, path_conversions=CONVERSIONS):
"""Returns a list of all paths in the document. """Returns a list of all paths in the document.
@ -263,7 +279,7 @@ class Document:
def paths_from_group(self, group, recursive=True, group_filter=lambda x: True, def paths_from_group(self, group, recursive=True, group_filter=lambda x: True,
path_filter=lambda x: True, path_conversions=CONVERSIONS): path_filter=lambda x: True, path_conversions=CONVERSIONS):
if all(isinstance(s, str) for s in group): if all(isinstance(s, string) for s in group):
# If we're given a list of strings, assume it represents a # If we're given a list of strings, assume it represents a
# nested sequence # nested sequence
group = self.get_group(group) group = self.get_group(group)
@ -289,7 +305,7 @@ class Document:
# If given a list of strings (one or more), assume it represents # If given a list of strings (one or more), assume it represents
# a sequence of nested group names # a sequence of nested group names
elif all(isinstance(elem, str) for elem in group): elif len(group) > 0 and all(isinstance(elem, str) for elem in group):
group = self.get_or_add_group(group) group = self.get_or_add_group(group)
elif not isinstance(group, Element): elif not isinstance(group, Element):
@ -308,7 +324,7 @@ class Document:
path_svg = path.d() path_svg = path.d()
elif is_path_segment(path): elif is_path_segment(path):
path_svg = Path(path).d() path_svg = Path(path).d()
elif isinstance(path, str): elif isinstance(path, string):
# Assume this is a valid d-string. # Assume this is a valid d-string.
# TODO: Should we sanity check the input string? # TODO: Should we sanity check the input string?
path_svg = path path_svg = path

View File

@ -214,10 +214,13 @@ def disvg(paths=None, colors=None, filename=None, stroke_widths=None,
timestamp = True if timestamp is None else timestamp timestamp = True if timestamp is None else timestamp
filename = os_path.join(gettempdir(), 'disvg_output.svg') filename = os_path.join(gettempdir(), 'disvg_output.svg')
dirname = os_path.abspath(os_path.dirname(filename))
if not os_path.exists(dirname):
makedirs(dirname)
# append time stamp to filename # append time stamp to filename
if timestamp: if timestamp:
fbname, fext = os_path.splitext(filename) fbname, fext = os_path.splitext(filename)
dirname = os_path.dirname(filename)
tstamp = str(time()).replace('.', '') tstamp = str(time()).replace('.', '')
stfilename = os_path.split(fbname)[1] + '_' + tstamp + fext stfilename = os_path.split(fbname)[1] + '_' + tstamp + fext
filename = os_path.join(dirname, stfilename) filename = os_path.join(dirname, stfilename)
@ -407,9 +410,6 @@ def disvg(paths=None, colors=None, filename=None, stroke_widths=None,
if paths2Drawing: if paths2Drawing:
return dwg return dwg
# save svg
if not os_path.exists(os_path.dirname(filename)):
makedirs(os_path.dirname(filename))
dwg.save() dwg.save()
# re-open the svg, make the xml pretty, and save it again # re-open the svg, make the xml pretty, and save it again

View File

@ -4,8 +4,13 @@ The main tool being the svg2paths() function."""
# External dependencies # External dependencies
from __future__ import division, absolute_import, print_function from __future__ import division, absolute_import, print_function
from xml.dom.minidom import parse from xml.dom.minidom import parse
from os import path as os_path, getcwd import os
from io import StringIO
import re import re
try:
from os import PathLike as FilePathLike
except ImportError:
FilePathLike = str
# Internal dependencies # Internal dependencies
from .parser import parse_path from .parser import parse_path
@ -17,9 +22,11 @@ COORD_PAIR_TMPLT = re.compile(
r'([\+-]?\d*[\.\d]\d*[eE][\+-]?\d+|[\+-]?\d*[\.\d]\d*)' r'([\+-]?\d*[\.\d]\d*[eE][\+-]?\d+|[\+-]?\d*[\.\d]\d*)'
) )
def path2pathd(path): def path2pathd(path):
return path.get('d', '') return path.get('d', '')
def ellipse2pathd(ellipse): def ellipse2pathd(ellipse):
"""converts the parameters from an ellipse or a circle to a string for a """converts the parameters from an ellipse or a circle to a string for a
Path object d-attribute""" Path object d-attribute"""
@ -84,14 +91,39 @@ def rect2pathd(rect):
The rectangle will start at the (x,y) coordinate specified by the The rectangle will start at the (x,y) coordinate specified by the
rectangle object and proceed counter-clockwise.""" rectangle object and proceed counter-clockwise."""
x0, y0 = float(rect.get('x', 0)), float(rect.get('y', 0)) x, y = float(rect.get('x', 0)), float(rect.get('y', 0))
w, h = float(rect.get('width', 0)), float(rect.get('height', 0)) w, h = float(rect.get('width', 0)), float(rect.get('height', 0))
x1, y1 = x0 + w, y0 if 'rx' in rect or 'ry' in rect:
x2, y2 = x0 + w, y0 + h
x3, y3 = x0, y0 + h # if only one, rx or ry, is present, use that value for both
# https://developer.mozilla.org/en-US/docs/Web/SVG/Element/rect
rx = rect.get('rx', None)
ry = rect.get('ry', None)
if rx is None:
rx = ry or 0.
if ry is None:
ry = rx or 0.
rx, ry = float(rx), float(ry)
d = "M {} {} ".format(x + rx, y) # right of p0
d += "L {} {} ".format(x + w - rx, y) # go to p1
d += "A {} {} 0 0 1 {} {} ".format(rx, ry, x+w, y+ry) # arc for p1
d += "L {} {} ".format(x+w, y+h-ry) # above p2
d += "A {} {} 0 0 1 {} {} ".format(rx, ry, x+w-rx, y+h) # arc for p2
d += "L {} {} ".format(x+rx, y+h) # right of p3
d += "A {} {} 0 0 1 {} {} ".format(rx, ry, x, y+h-ry) # arc for p3
d += "L {} {} ".format(x, y+ry) # below p0
d += "A {} {} 0 0 1 {} {} z".format(rx, ry, x+rx, y) # arc for p0
return d
x0, y0 = x, y
x1, y1 = x + w, y
x2, y2 = x + w, y + h
x3, y3 = x, y + h
d = ("M{} {} L {} {} L {} {} L {} {} z" d = ("M{} {} L {} {} L {} {} L {} {} z"
"".format(x0, y0, x1, y1, x2, y2, x3, y3)) "".format(x0, y0, x1, y1, x2, y2, x3, y3))
return d return d
@ -117,7 +149,9 @@ def svg2paths(svg_file_location,
SVG Path, Line, Polyline, Polygon, Circle, and Ellipse elements. SVG Path, Line, Polyline, Polygon, Circle, and Ellipse elements.
Args: Args:
svg_file_location (string): the location of the svg file svg_file_location (string or file-like object): the location of the
svg file on disk or a file-like object containing the content of a
svg file
return_svg_attributes (bool): Set to True and a dictionary of return_svg_attributes (bool): Set to True and a dictionary of
svg-attributes will be extracted and returned. See also the svg-attributes will be extracted and returned. See also the
`svg2paths2()` function. `svg2paths2()` function.
@ -141,8 +175,10 @@ def svg2paths(svg_file_location,
list: The list of corresponding path attribute dictionaries. list: The list of corresponding path attribute dictionaries.
dict (optional): A dictionary of svg-attributes (see `svg2paths2()`). dict (optional): A dictionary of svg-attributes (see `svg2paths2()`).
""" """
if os_path.dirname(svg_file_location) == '': # strings are interpreted as file location everything else is treated as
svg_file_location = os_path.join(getcwd(), svg_file_location) # file-like object and passed to the xml parser directly
from_filepath = isinstance(svg_file_location, str) or isinstance(svg_file_location, FilePathLike)
svg_file_location = os.path.abspath(svg_file_location) if from_filepath else svg_file_location
doc = parse(svg_file_location) doc = parse(svg_file_location)
@ -222,3 +258,26 @@ def svg2paths2(svg_file_location,
convert_polylines_to_paths=convert_polylines_to_paths, convert_polylines_to_paths=convert_polylines_to_paths,
convert_polygons_to_paths=convert_polygons_to_paths, convert_polygons_to_paths=convert_polygons_to_paths,
convert_rectangles_to_paths=convert_rectangles_to_paths) convert_rectangles_to_paths=convert_rectangles_to_paths)
def svgstr2paths(svg_string,
return_svg_attributes=False,
convert_circles_to_paths=True,
convert_ellipses_to_paths=True,
convert_lines_to_paths=True,
convert_polylines_to_paths=True,
convert_polygons_to_paths=True,
convert_rectangles_to_paths=True):
"""Convenience function; identical to svg2paths() except that it takes the
svg object as string. See svg2paths() docstring for more
info."""
# wrap string into StringIO object
svg_file_obj = StringIO(svg_string)
return svg2paths(svg_file_location=svg_file_obj,
return_svg_attributes=return_svg_attributes,
convert_circles_to_paths=convert_circles_to_paths,
convert_ellipses_to_paths=convert_ellipses_to_paths,
convert_lines_to_paths=convert_lines_to_paths,
convert_polylines_to_paths=convert_polylines_to_paths,
convert_polygons_to_paths=convert_polygons_to_paths,
convert_rectangles_to_paths=convert_rectangles_to_paths)

54
test/test_document.py Normal file
View File

@ -0,0 +1,54 @@
from __future__ import division, absolute_import, print_function
import unittest
from svgpathtools import *
from io import StringIO
from io import open # overrides build-in open for compatibility with python2
from os.path import join, dirname
from sys import version_info
class TestDocument(unittest.TestCase):
def test_from_file_path_string(self):
"""Test reading svg from file provided as path"""
doc = Document(join(dirname(__file__), 'polygons.svg'))
self.assertEqual(len(doc.paths()), 2)
def test_from_file_path(self):
"""Test reading svg from file provided as path"""
if version_info >= (3, 6):
import pathlib
doc = Document(pathlib.Path(__file__).parent / 'polygons.svg')
self.assertEqual(len(doc.paths()), 2)
def test_from_file_object(self):
"""Test reading svg from file object that has already been opened"""
with open(join(dirname(__file__), 'polygons.svg'), 'r') as file:
doc = Document(file)
self.assertEqual(len(doc.paths()), 2)
def test_from_stringio(self):
"""Test reading svg object contained in a StringIO object"""
with open(join(dirname(__file__), 'polygons.svg'),
'r', encoding='utf-8') as file:
# read entire file into string
file_content = file.read()
# prepare stringio object
file_as_stringio = StringIO(file_content)
doc = Document(file_as_stringio)
self.assertEqual(len(doc.paths()), 2)
def test_from_string(self):
"""Test reading svg object contained in a string"""
with open(join(dirname(__file__), 'polygons.svg'),
'r', encoding='utf-8') as file:
# read entire file into string
file_content = file.read()
doc = Document.from_svg_string(file_content)
self.assertEqual(len(doc.paths()), 2)

View File

@ -236,3 +236,10 @@ class TestGroups(unittest.TestCase):
path = parse_path(path_d) path = parse_path(path_d)
svg_path = doc.add_path(path, group=new_leaf) svg_path = doc.add_path(path, group=new_leaf)
self.assertEqual(path_d, svg_path.get('d')) self.assertEqual(path_d, svg_path.get('d'))
# Test that paths are added to the correct group
new_sibling = doc.get_or_add_group(
['base_group', 'new_parent', 'new_sibling'])
doc.add_path(path, group=new_sibling)
self.assertEqual(len(new_sibling), 1)
self.assertEqual(path_d, new_sibling[0].get('d'))

View File

@ -743,7 +743,7 @@ class TestPath(unittest.TestCase):
# this is necessary due to changes to the builtin `hash` function # this is necessary due to changes to the builtin `hash` function
user_hash_seed = os.environ.get("PYTHONHASHSEED", "") user_hash_seed = os.environ.get("PYTHONHASHSEED", "")
os.environ["PYTHONHASHSEED"] = "314" os.environ["PYTHONHASHSEED"] = "314"
if version_info.major >= 3 and version_info.minor >= 8: if version_info >= (3, 8):
expected_hashes = [ expected_hashes = [
-6073024107272494569, -2519772625496438197, 8726412907710383506, -6073024107272494569, -2519772625496438197, 8726412907710383506,
2132930052750006195, 3112548573593977871, 991446120749438306, 2132930052750006195, 3112548573593977871, 991446120749438306,
@ -751,7 +751,7 @@ class TestPath(unittest.TestCase):
-4418099728831808951, 702646573139378041, -6331016786776229094, -4418099728831808951, 702646573139378041, -6331016786776229094,
5053050772929443013, 6102272282813527681, -5385294438006156225 5053050772929443013, 6102272282813527681, -5385294438006156225
] ]
elif version_info.major == 3 and 2 <= version_info.minor < 8: elif (3, 2) <= version_info < (3, 8):
expected_hashes = [ expected_hashes = [
-5662973462929734898, 5166874115671195563, 5223434942701471389, -5662973462929734898, 5166874115671195563, 5223434942701471389,
-7224979960884350294, -5178990533869800243, -4003140762934044601, -7224979960884350294, -5178990533869800243, -4003140762934044601,
@ -760,6 +760,7 @@ class TestPath(unittest.TestCase):
-7093907105533857815, 2036243740727202243, -8108488067585685407 -7093907105533857815, 2036243740727202243, -8108488067585685407
] ]
else: else:
expected_hashes = [ expected_hashes = [
-5762846476463470127, -138736730317965290, -2005041722222729058, -5762846476463470127, -138736730317965290, -2005041722222729058,
8448700906794235291, -5178990533869800243, -4003140762934044601, 8448700906794235291, -5178990533869800243, -4003140762934044601,
@ -768,6 +769,11 @@ class TestPath(unittest.TestCase):
-7093907105533857815, 2036243740727202243, -8108488067585685407 -7093907105533857815, 2036243740727202243, -8108488067585685407
] ]
if version_info.major == 2 and os.name == 'nt':
# the expected hash values for 2.7 apparently differed on Windows
# if you work in Windows and want to fix this test, please do
return
for c, h in zip(test_curves, expected_hashes): for c, h in zip(test_curves, expected_hashes):
self.assertTrue(hash(c) == h, msg="hash {} was expected for curve = {}".format(h, c)) self.assertTrue(hash(c) == h, msg="hash {} was expected for curve = {}".format(h, c))
os.environ["PYTHONHASHSEED"] = user_hash_seed # restore user's hash seed os.environ["PYTHONHASHSEED"] = user_hash_seed # restore user's hash seed

View File

@ -1,7 +1,13 @@
from __future__ import division, absolute_import, print_function from __future__ import division, absolute_import, print_function
import unittest import unittest
from svgpathtools import * from svgpathtools import *
from io import StringIO
from io import open # overrides build-in open for compatibility with python2
from os.path import join, dirname from os.path import join, dirname
from sys import version_info
from svgpathtools.svg_to_paths import rect2pathd
class TestSVG2Paths(unittest.TestCase): class TestSVG2Paths(unittest.TestCase):
def test_svg2paths_polygons(self): def test_svg2paths_polygons(self):
@ -50,3 +56,54 @@ class TestSVG2Paths(unittest.TestCase):
self.assertTrue(len(path_circle)==2) self.assertTrue(len(path_circle)==2)
self.assertTrue(path_circle==path_circle_correct) self.assertTrue(path_circle==path_circle_correct)
self.assertTrue(path_circle.isclosed()) self.assertTrue(path_circle.isclosed())
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")
def test_from_file_path_string(self):
"""Test reading svg from file provided as path"""
paths, _ = svg2paths(join(dirname(__file__), 'polygons.svg'))
self.assertEqual(len(paths), 2)
def test_from_file_path(self):
"""Test reading svg from file provided as pathlib POSIXPath"""
if version_info >= (3, 6):
import pathlib
paths, _ = svg2paths(pathlib.Path(__file__).parent / 'polygons.svg')
self.assertEqual(len(paths), 2)
def test_from_file_object(self):
"""Test reading svg from file object that has already been opened"""
with open(join(dirname(__file__), 'polygons.svg'), 'r') as file:
paths, _ = svg2paths(file)
self.assertEqual(len(paths), 2)
def test_from_stringio(self):
"""Test reading svg object contained in a StringIO object"""
with open(join(dirname(__file__), 'polygons.svg'),
'r', encoding='utf-8') as file:
# read entire file into string
file_content = file.read()
# prepare stringio object
file_as_stringio = StringIO(file_content)
paths, _ = svg2paths(file_as_stringio)
self.assertEqual(len(paths), 2)
def test_from_string(self):
"""Test reading svg object contained in a string"""
with open(join(dirname(__file__), 'polygons.svg'),
'r', encoding='utf-8') as file:
# read entire file into string
file_content = file.read()
paths, _ = svgstr2paths(file_content)
self.assertEqual(len(paths), 2)