Compare commits

...

264 Commits

Author SHA1 Message Date
Andrew Port fcb648b9bb add back pypi action new tag condition 2023-05-20 14:38:22 -04:00
Andrew Port ec546a71d4 Merge branch 'PyPI' 2023-05-20 14:37:02 -04:00
Andrew Port bc930005c2 test fixed pypi action 2023-05-20 14:34:32 -04:00
Andrew Port ae9b79e77a revert temporary fix for pushing 1.6.1 to pypi 2023-05-20 14:26:44 -04:00
Andrew Port 289ee6ecb4 temporary fix for pushing 1.6.1 to pypi 2023-05-20 14:25:19 -04:00
Andrew Port 592fe3a525 fix pypi actions 2023-05-20 14:18:42 -04:00
Andrew Port 81870e1f85 update version 2023-05-20 14:13:15 -04:00
Andrew Port 6015a97090
Merge pull request #207 from mathandy/fix-issue-205
return error if Path.point() cannot be computed
2023-05-20 14:11:07 -04:00
Andrew Port 788b2b43a2
Merge pull request #203 from kasbah/fix-escape-sequences
Fix invalid escape sequences in strings
2023-05-20 14:10:19 -04:00
Andrew Port b282094b53 return error if Path.point() cannot be computed 2023-05-20 13:20:05 -04:00
Andrew Port 229773ff9d
Merge pull request #201 from SebKuzminsky/fix-arc-sweep-with-negative-scale
Fix arc sweep with negative scale
2023-05-20 12:50:17 -04:00
Kaspar Emanuel a16a060c27
Fix invalid escape sequences in strings 2023-05-06 19:33:31 +01:00
Sebastian Kuzminsky e94483510e path.transform: Arc sweep is reversed by negative scale
When transforming an Arc, negative scale reverses the sweep.
2023-05-05 00:38:55 -06:00
Sebastian Kuzminsky 6abda09d1c add a test loading arcs with negative scale
This test currently fails, fix in the following commit.

The test loads an svg with a group with a transform with "scale(1,-1)".
This situation can mess up arc sweeps, resulting in corrupted paths.
2023-05-05 00:38:49 -06:00
Andrew Port 5c73056420
Merge pull request #199 from mathandy/issue-198
Issue 198
2023-04-01 15:39:39 -04:00
Andrew Port 4f5d8f3bf2 fix issue-198; circles parsing to non-closed paths 2023-04-01 15:30:04 -04:00
Andrew Port c4d98afc68 add test for issue 198 2023-04-01 15:26:22 -04:00
Andrew Port 9c69e45d6e add python==3.11 to setup.py 2023-02-13 18:15:36 -05:00
Andrew Port dc2f6e90cc remove broken build shield 2023-02-13 18:13:32 -05:00
Andrew Port 96676b7697 increment version number 2023-02-13 17:46:17 -05:00
Andrew Port 2a1cb735e9
Merge pull request #191 from tatarize/fastfail-intersection
Fastfail Intersection
2023-02-03 18:06:56 -08:00
Andrew Port 3eb21161cf add report of intersection count 2023-02-03 21:00:57 -05:00
Andrew Port 31b6f3dd90 Merge branch 'master' into fastfail-intersection 2023-02-03 20:49:49 -05:00
Andrew Port d9515ea399 remove all wildcard imports 2023-02-03 19:46:23 -05:00
Andrew Port 944ccf5e89 create new yaml for legacy system tests 2023-02-03 18:45:50 -05:00
Tatarize b6e5a623ea Add random intersections test 2022-12-04 00:59:40 -08:00
Tatarize 4c6abc5820 Add quick fails to paths 2022-12-04 00:59:15 -08:00
Andrew Port b8dfb6770a correct descriptive action name 2022-07-10 22:38:14 -03:00
Andrew Port 0e17702d04 update version to 1.5.1 2022-07-10 22:19:50 -03:00
Andrew Port 8df19f1c12 fixed issue 171 2022-07-09 19:49:12 -03:00
Andrew Port 9ac7f62515 cleanup long comment 2022-07-09 19:38:28 -03:00
Andrew Port d8a6e5e509 delete unused line 2022-07-09 19:30:40 -03:00
Andrew Port 73c887a8a3 rename as name already used by outside function 2022-07-09 19:28:42 -03:00
Andrew Port f7e074339d update version to 1.5.0 (now that string/file-like objects are handled) 2022-06-05 22:27:22 -07:00
Andrew Port d9f5a2a781 Merge branch 'master' into PyPI 2022-06-05 22:22:18 -07:00
Andrew Port 740e2bf991 Merge branch 'master' into PyPI 2022-06-05 22:21:36 -07:00
Andrew Port 8cbe6f0f81 minor docstring change 2022-06-05 22:19:26 -07:00
Andrew Port b82530aaac minor fix to docstring formatting 2022-06-05 21:49:21 -07:00
Andrew Port e8792f4d2d add support for os.PathLike arguments 2022-06-05 21:47:37 -07:00
Andrew Port d3a66f0bbd skip pathlib support for python < 3.6 2022-06-05 21:17:28 -07:00
Andrew Port 356d86df78 restore support for PosixPath inputs for Document 2022-06-05 21:00:42 -07:00
Andrew Port a989c9831d
Merge pull request #176 from FlyingSamson/read-svg-from-file-like-obj
Support reading of SVGs from strings and file-like objects
2022-06-05 20:12:19 -07:00
FlyingSamson 07f46d41f8 Rename svg_string2paths to svgstr2paths 2022-05-25 19:24:50 +02:00
FlyingSamson 2fc016d48f Make factory method a classmethod 2022-05-25 17:58:14 +02:00
FlyingSamson aacd5fa96d Remove second version of function returning the svg_attributes by default 2022-05-25 17:57:22 +02:00
FlyingSamson db5200f460 Switch back to previous function parameter names 2022-05-25 17:39:10 +02:00
FlyingSamson a473ee3f4c Remove unnecessary seek commands 2022-05-24 18:15:52 +02:00
FlyingSamson 02a223c220 Fix fileio for test compatibility with python2.7 2022-05-22 17:02:51 +02:00
FlyingSamson 68e0d1f30d Fix tests for old python versions not supporting type hints 2022-05-22 15:51:03 +02:00
FlyingSamson a743e0293c Add tests for creating from file location, file, StringIO, and string 2022-05-22 15:46:56 +02:00
FlyingSamson 1771fbfb06 Add factory method for creating from string holding svg object 2022-05-22 15:30:48 +02:00
FlyingSamson 33f4639bbf Add tests for functions taking svg objects as string 2022-05-22 14:51:16 +02:00
FlyingSamson 50b335f3da Add convenience functions for converting svgs contained in a string to paths 2022-05-22 14:37:27 +02:00
FlyingSamson ccdd10212c Add unit tests for reading from different sources in svg2paths 2022-05-22 13:32:09 +02:00
FlyingSamson ce43c75cd8 Allow file-like object as input to Documents ctor and svg2paths function 2022-05-22 13:09:13 +02:00
chanicpanic 19df25b99b
Fix Document.add_path for empty groups (#170) 2022-02-27 18:48:50 -08:00
Andrew Port c84c897bf2 aesthetic cleanup 2022-02-03 18:11:55 -08:00
Andrew Port ac138b8e5d aesthetic cleanup 2022-02-03 18:10:00 -08:00
Andrew Port 2dc06df20f rounded rect now parsed properly if only rx or only ry is included 2022-02-03 18:09:09 -08:00
Andrew Port 72fa3dcf17 Merge branch 'master' of github.com:mathandy/svgpathtools 2022-01-25 17:45:24 -08:00
Catherine Holloway 6c655ad220
add support for rounded rectangles (#161) 2022-01-25 17:23:04 -08:00
Andrew Port 5037fac574 fix issue with filenames with no directory causing error 2021-11-26 18:34:58 -08:00
Andrew Port abd99f0846 fix issue with filenames with no directory causing error 2021-11-26 18:31:38 -08:00
Andrew Port d1421d6286 fix issue with filenames with no directory causing error 2021-11-26 18:23:07 -08:00
Andrew Port 8ad7458b31 fix github action syntax for publishing to PyPI 2021-11-09 20:45:13 -08:00
Andrew Port 1b8caeec71 fix github action syntax 2021-11-09 20:35:58 -08:00
Andrew Port 002e691686 Publish now on any v* tag pushed to PyPI branch 2021-11-09 20:28:00 -08:00
Andrew Port 3576591e08 Publish now on any v* tag pushed to PyPI branch 2021-11-09 20:26:36 -08:00
Andrew Port ca094feea9 Add 3.10 CI check and disable test_hash unittest for Windows Python 2 environments 2021-11-09 19:48:41 -08:00
Andrew Port 3a6711a5e7 add scipy to requirements file 2021-11-09 18:47:17 -08:00
Andrew Port bbf75d0b5a Remove deprecated 'requires' setuptools parameter 2021-11-09 18:45:31 -08:00
Andrew Port 5b9ee30544 now scipy is installed by default 2021-11-09 18:30:30 -08:00
Andrew Port b35488efb0 fix path hash unit test 2021-11-01 18:05:39 -07:00
Andrew Port d97519ffa3 change codeql action to run on all branches 2021-10-18 22:40:22 -07:00
Andrew Port 5a3bb8fca8 update official python version support 2021-10-18 22:20:24 -07:00
Andrew Port d998587a32
Update README.md 2021-10-18 19:00:49 -07:00
Andrew Port b8579b2c12
Make donate badge test lowercase 2021-10-18 18:50:12 -07:00
Andrew Port 05a2d271b7
remove old donate button 2021-10-18 18:42:37 -07:00
Andrew Port a69898f83b
add some badges 2021-10-18 18:41:40 -07:00
Andrew Port 09ce497a4f add example how to use svgpathtools in JS with Pyodide 2021-10-04 21:24:04 -07:00
Julian Rüth 39d3ba713f
Fix implementation of points() (#155)
fix incorrect implementation of `points`  method
2021-09-26 18:22:09 -07:00
Andrew Port e4c7b53f62 fix text name 2021-09-26 18:17:58 -07:00
Andrew Port 8f4b1fee00 update setup.py download_url to point to wheel 2021-09-23 04:37:37 -07:00
Andrew Port a78ecf4290 update version 2021-09-23 04:13:12 -07:00
Andrew Port 73e0ae2b21 temporarily disable problemsome hash test 2021-09-23 04:02:51 -07:00
Andrew Port da5286f79e prevent pushing existing dists to testpypi 2021-09-23 04:00:36 -07:00
Andrew Port 8b8ac6c9fe fix issue with test failing due to hash builtin changing in python 3.2 then again in 3.8 2021-09-23 03:52:16 -07:00
Andrew Port 44d08b6737 fix issue with test failing due to hash builtin changing in python 3.8 2021-09-23 03:15:26 -07:00
Andrew Port 60984969a7 update testpypi secret 2021-09-23 03:08:33 -07:00
Andrew Port 3b33445c25 add download_url to setup.py 2021-09-23 03:03:30 -07:00
Andrew Port c4b77697f2 remove old distribution files 2021-09-23 02:58:06 -07:00
Andrew Port 56bbba0bd1 add workflow to publish to PyPI on tag 2021-09-23 02:51:01 -07:00
Andrew Port 4f685e732a remove junk comment from workflow tutorial 2021-09-23 02:30:16 -07:00
Andrew Port b8f4e71f5b remove accidentally committed debugging code 2021-09-23 00:18:03 -07:00
Andrew Port e993ff95c5 fix issue 156 caused by arc.intersect returning values of t outside [0, 1] 2021-09-23 00:17:19 -07:00
Andrew Port 1b503a7b2f fix test that failed in python2 due to changes to hash builtin 2021-09-23 00:15:37 -07:00
Andrew Port e0f212a334 make paths and path semgents hashable 2021-09-22 21:19:13 -07:00
Andrew Port be33f182fb
Create SECURITY.md 2021-09-20 20:52:01 -07:00
Andrew Port 81ff41f881 rename unit test yaml/action 2021-09-18 00:37:31 -07:00
Andrew Port 5be6b258e1 run CI only on push to master 2021-09-18 00:28:39 -07:00
Andrew Port add170c926 run CI on any push or pull-request 2021-09-18 00:27:35 -07:00
Andrew Port 7d22c76e10 remove CI workflow_dispatch trigger 2021-09-18 00:23:16 -07:00
Andrew Port f19c473c69 fix ci test command 2021-09-18 00:21:51 -07:00
Andrew Port bd55d303de rename codacy action 2021-09-18 00:21:24 -07:00
Andrew Port 43c0d2d807 replace travis-ci with github-ci 2021-09-18 00:09:44 -07:00
Andrew Port ecd39743ab disable improperly configured codacy coverage reporting 2021-09-17 23:56:05 -07:00
Andrew Port cfc97e0ce2 rename codacy yaml and job 2021-09-17 23:44:07 -07:00
Andrew Port 426ce7d56c add reference to codacy api token 2021-09-17 23:40:23 -07:00
Andrew Port 9c2e403036 style change to prevent false codacy critical alert 2021-09-17 23:38:01 -07:00
Andrew Port 767413c896 remove project api key reference 2021-09-17 23:26:51 -07:00
Andrew Port ed207f2241 create codacity action 2021-09-17 23:16:01 -07:00
Andrew Port c5f49de5fe do not ignore .github directory 2021-09-17 23:15:32 -07:00
Andrew Port dac4600e6f
Create codeql-analysis.yml 2021-09-17 22:49:15 -07:00
Andrew Port 3d22a7cf52 remove redundant is_bezier_segment and is_path_segment defs 2021-09-17 22:35:27 -07:00
Andrew Port a67de75137 remove tags files 2021-09-17 22:32:34 -07:00
Andrew Port 5ea0fb226d add donate button 2021-08-13 00:47:11 -07:00
Andrew Port 8dc12d4efc add delta tolerance to all assertAlmostEquals that don't have one 2021-06-27 22:30:56 -07:00
Vrroom baba1d18b2
arc transform fixed (#98)
* arc transform fixed

* Update svgpathtools/path.py

Co-authored-by: Matthijs Kooijman <matthijs@stdin.nl>

* Fixed rotation bug in transformation of arcs

* Made compatible with python2.7

Changed the shorthand @ for np matrix multiplication to matmul

Co-authored-by: vrroom <vrroomc@github.com>
Co-authored-by: Matthijs Kooijman <matthijs@stdin.nl>
2021-06-22 20:28:24 -07:00
Wes Bouaziz 1e5bfb4252
Correcting a few functions not handling Path having empty _segments (#143)
* Correcting poping from Path objects

Poping from a 1-lengthed Path would yield an error due to not considering that the _segments attribute would be empty after deleting the last element.

* Correcting point function

Unspecified behavior if _segments is empty

* Correcting setter and getter of start and end
2021-05-10 21:12:06 -07:00
Nathan Hurst a0fc28849c
add support for units and handle mindim=None correctly. (#149)
* add support for units and handle mindim=None correctly.

* Move baseunit positional argument to the end to avoid disruption.
2021-05-10 21:09:58 -07:00
tatarize 3a1fe8695d
Bug Corrections. Closes #113, #95, #94, and #71 (#136)
* Closes #113

Previous fix stopped working because numpy is more liberal dividing by zero and returning nan values.

* Closes #71

* Closes #95

* Closes #94
2021-01-16 20:08:58 -08:00
Andrew Port 091394b5e3 fixed problem with path joints becoming discontious after transformation 2020-12-08 22:36:16 -08:00
Andrew Port 561c89ad47 fix fail test (test assumed untrue number of intersections) 2020-12-08 18:45:35 -08:00
Andrew Port ee58270f66 fix incorrectly written assertTrue tests 2020-12-08 18:38:51 -08:00
Andrew Port 30f517e735 fix Arc.center being nan cause of switch to np.sqrt raising different error than math.sqrt 2020-12-08 18:21:24 -08:00
Andrew Port 44e88d54e5 derivative, normal, and unit_tangent now generalize to vectors for path segments (not paths) 2020-12-01 22:49:38 -08:00
Andrew Port f9febbd85e minor, 2/360 -> 180 2020-12-01 22:38:41 -08:00
Andrew Port 1c9363d426 arc.point now supports numpy arrays and math and cmath functions now imported from numpy 2020-12-01 22:09:20 -08:00
Andrew Port 69e2e27efb update travis secure token for pypi 2020-12-01 20:35:08 -08:00
Andrew Port ceffdc4a5b replaced f-strings to keep compatibility with python<3.6 2020-12-01 20:32:26 -08:00
Andrew Port 2fb96b0906 remove no longer used os.getcwd import 2020-12-01 20:03:30 -08:00
Andrew Port 07771be9bf remove unnecessary code block that appends CWD to filenames not involving directories 2020-12-01 20:02:35 -08:00
Andrew Port 5aeb6e3bf7 aesthetic cleanup 2020-12-01 20:00:59 -08:00
Andrew Port 0f4c9c598a set tempdir as default for writing svgs and changed timestamp default behavior 2020-12-01 19:44:54 -08:00
tatarize b3d9544624
Approximate Arcs With Beziers (#130)
* Approximate Arcs With Beziers

* Quadratic in documentation.

* Test Coverage, approximate arcs.
2020-12-01 18:37:15 -08:00
Andrew Port 45dc873f82 fix zero-radius arc crashes and crashes related to use of degenerate Path.closed attribute in parse_path() 2020-11-15 16:23:27 -08:00
Andrew Port cae729bd48 prepared for pypi 2020-11-14 23:44:19 -08:00
Andrew Port 110acc9e00 version -> 1.4.1 2020-11-14 23:42:36 -08:00
Andrew Port 7a183c4e3c aesthetic changes 2020-11-14 23:37:09 -08:00
Andrew Port 9fa559a070 change setup to use md readme and remove rst readme 2020-11-14 23:31:53 -08:00
Andrew Port 1b4ed34ac9 fix messed up setup 2020-11-14 23:30:19 -08:00
Andrew Port 772d6698bf add markdown readme 2020-11-14 23:29:16 -08:00
Andrew Port bdbd976e0a commit branch 2020-11-14 23:22:09 -08:00
Andy Port d354b8ffe4 auto-deploy with travis-ci on new tag 2020-08-04 18:52:45 -07:00
Andy Port 8f92e43f58 prep for PyPI update 2020-08-04 18:22:20 -07:00
Andy Port 4342501591 prep for PyPI update 2020-08-04 18:22:15 -07:00
tatarize 0c9dd318aa
Fix Issue #99 + Test Coverage. (#115) 2020-07-16 17:51:07 -07:00
Andy Port 1a4807e929 fix error related feeding str to polyline2pathd 2020-07-06 17:33:24 -07:00
Andy Port 12c8d07bad fix error related feeding str to polyline2pathd 2020-07-06 17:30:30 -07:00
Andy Port 22f3dafe87 remove unused numpy import 2020-07-06 17:21:29 -07:00
Andy Port 945ae49967 fixed test_group_flatten for document.py changes 2020-07-06 11:13:47 -07:00
tatarize 90dfeb7b13
Move Path Parsing into Path. (#114) 2020-07-06 11:01:15 -07:00
Andy Port c89c68f421 speed up randialrange for Line objects 2020-07-01 22:31:57 -07:00
Andy Port ab44fcd564 save and write fixed, __repr__() and pretty() methods added 2020-06-23 22:42:23 -07:00
Andy Port 4b7f17c7bd minor line width fix 2020-06-23 22:18:26 -07:00
Andy Port 70534a6b6c added vectorized points() method for bezier segments 2020-06-23 22:16:09 -07:00
Andy Port 1f7503aabd some renames and add __repr__ and pretty() 2020-06-23 21:54:58 -07:00
Andy Port 445899b2eb renamed Document.flatten_all_paths to flattened_paths 2020-06-23 20:54:27 -07:00
Andy Port d673176347 Document now stores element and transform in path and Document.display() is fixed 2020-06-23 20:50:43 -07:00
NataliaTs b503b9b3a5
fix polyline and polygon conversion (#85)
* fix polyline and polygon conversion

* update version

* revert version
2020-06-19 20:32:01 -07:00
Andy Port b714ff872d add Path.attributes and Path.meta attributes 2020-06-19 20:21:28 -07:00
Sebastian Kuzminsky 685f9a6eaf
fix Arc/Arc intersections (#110)
* add some failing Arc.intersect(Arc) tests

* implementing Arc.intersect(Arc)

This commit adds special handling in Arc.intersect() when the other
segment is an Arc, and when both segments are circular and non-rotated.

This particular case is common, and quick and easy to solve algebraically.

This commit fixes the failing tests added in the previous commit.
2020-06-19 19:40:38 -07:00
Matthew Carruth 5ae88df6d5
actually return svgwrite.Drawing if requested (#102)
no use having the option if the wsvg and paths2Drawing entry points do not return `dwg` back from disvg
2020-06-19 19:39:38 -07:00
David Romero 90f8f76185
Method for checking if one path is inside another path (#105)
* Added method is_contained_by to the Path class

* Adding a requirements.txt file

Signed-off-by: David Romero <dromero.fisica@gmail.com>

* Correcting tests of is_contained_by
2020-06-19 19:36:22 -07:00
Grey f99f9d6bb3
Support flattening paths of only nested groups (#88)
* Fixed document.flatten_group(~) for nested groups and added a test

* Add space for PEP8 conformance

* Add documentation for document.get_group()

* Use group_search_xpath to be consistent and customizable

* Fix lexical mistake in comments

* Fix grammar mistake in comments
2020-06-19 18:59:47 -07:00
Antoine Beyeler b117f85811
Fixed crash in line2pathd() (#101)
* Fixed crash in line2pathd().

* Further fixed to support implicit attributes
2020-06-19 18:58:25 -07:00
ugultopu 538b8777e1
Remove explicitly installing requirements (#76)
Since a PIP package has requirements data in it, when installing that package, if requirements have not already been installed, PIP handles installing them automatically. There is no need to install them explicitly.
2020-06-19 18:55:32 -07:00
Antoine Beyeler b767536e38
Fixed MutableSequence import (importing from collections is deprecated) (#100) 2020-06-19 18:52:09 -07:00
Sebastian Kuzminsky 2eb8fb62ed
test: show arguments that failed, to aid debugging (#83) 2020-06-19 18:47:04 -07:00
Sebastian Kuzminsky 929202aa62
Cache arc length (#82)
* add Arc.__hash__()

This function returns a hash of the Arc parameters, providing a kind of
Arc object fingerprint.

* Arc: cache segment length

Computing the length of an Arc segment takes a relatively long time.

This commit teaches the Arc class to remember its segment length,
to avoid recomputing it.  It invalidates the cache and recomputes the
segment length if any of the Arc parameters have changed.
2020-06-19 18:45:56 -07:00
taoari c7b6c030a6
relative path support (#81)
* relative path support

* add test for path.d(rel=True)

* add path test to test_path.py
2020-06-19 18:43:29 -07:00
Sebastian Kuzminsky 8457dc01ee
fix Arc.sweep when transform scale flips it around (#112)
Fixes #111.
2020-06-19 18:29:36 -07:00
skef fd7348a1df Fix tuple assignment (#86) 2019-01-07 21:24:35 -08:00
Andy ae42197d10 fixed path area (issue #74) 2018-12-03 22:27:59 -08:00
Sumeet P b4e211fd79 cx and cy to be 0 for circle/ellipse by default (#78)
When cx and cy attributes are not defined either in ellipse or circle tag, consider both to be 0.
2018-11-13 15:43:09 -08:00
Andy Port 58d48029ac fixed bug in new Arc-friendly Path.area() method 2018-11-04 22:22:20 -08:00
Sebastian Kuzminsky b37e74f5f3 Update Path.area to work with Arc segments (#65)
* add some tests of Path.area()

These tests currently fail because area() doesn't deal with Arc segments.
Fix in the following commit.

* make Path.area() approximate arcs

Fixes #37.

* Path.area(): fixup tabs/spaces for python3

* added asin to imports

* added asin to imports

* minor improvements to style, performance, and docstring
2018-11-04 21:40:56 -08:00
tatarize e91a35c3da Basic Svg_Io_Sax (#66)
* Basic Svg_Io_Sax

* Update to binary write

* Fixed SaxSvg Load, tweaked test.

* Couple tweaks.

* Couple tweaks to fit the dom parser api better

* Switch to iterparse for speed

* Used None matrix for identity in default case.

* Test Update for None Matrix
2018-11-04 21:07:40 -08:00
Sebastian Kuzminsky 2feb3c92b5 Arc line intersect, 3rd try (#64)
* add Line.point_to_t() and tests

These tests don't print anything while they run, and they use use the
assert() helpers from the unittest module.

* add Arc.point_to_t() and tests

These tests don't print anything while they run, and they use use the
assert() helpers from the unittest module.

* add a bunch of failing arc/line intersection tests

This commit contains a bunch of failing arc/line intersections that I
and other people have run into.

These tests don't print anything while they run, and they use use the
assert() helpers from the unittest module where possible.

All these tests are fixed in the following commit.

* better implementation of Arc.intersect(Line)

Fixes mathandy/svgpathtools#35.

This commit fixes all the arc/line intersection test cases added in the
previous commit.

This implementation provides special handling in Arc.intersect() when
`self` is a non-rotated Arc and `other_seg` is a Line.  In this case
it uses the straight-forward closed-form solution to identify the
intersection points.

Rotated Arcs and Arcs intersecting with non-Line objects still use
the pre-existing intersection code, that part is totally untouched by
this commit.
2018-11-04 20:55:17 -08:00
Andy Port d810653b63
minor improvements 2018-11-02 16:42:11 -07:00
Andy Port 9e218b2b3b
minor wording change 2018-11-02 16:19:39 -07:00
Andy Port 74a881a181
Added paths2Drawing 2018-10-18 21:28:34 -07:00
Andy Port a86be9d306
added paths2Drawing functionality 2018-10-18 21:27:04 -07:00
Andy Port ee8eda5aae now `dimensions` parameter supports units will specify `viewbox` if not given 2018-10-13 22:22:52 -07:00
Andy Port aa03a4aecb now parameter supports units will specify if not given 2018-10-13 21:40:35 -07:00
Andy Port 95179a6bfa fixed issue with parameter being ignored 2018-10-13 20:36:01 -07:00
Andy Port 7fa103e533 Fixed issue with viewbox being written as tuple 2018-10-13 19:50:33 -07:00
Andy f77f94db5b added SciPy to Travis CI environment 2018-08-22 20:21:09 -07:00
Andy Port 165372562e
Revert "Arc line intersect, take 2 (#60)" (#63)
This reverts commit 2da39e4c02.
2018-08-22 00:27:55 -07:00
Sebastian Kuzminsky 2da39e4c02 Arc line intersect, take 2 (#60)
* add Line.point_to_t() and tests

* add Arc.point_to_t() and tests

* add a bunch of failing arc/line intersection tests

This commit contains a bunch of failing arc/line intersections that I
and other people have run into.

All these tests are fixed in the following commit.

* better implementation of Arc.intersect(Line)

Fixes mathandy/svgpathtools#35.

This commit fixes all the arc/line intersection test cases added in the
previous commit.

This implementation provides special handling in Arc.intersect() when
`self` is a non-rotated Arc and `other_seg` is a Line.  In this case
it uses the straight-forward closed-form solution to identify the
intersection points.

Rotated Arcs and Arcs intersecting with non-Line objects still use
the pre-existing intersection code, that part is totally untouched by
this commit.
2018-08-22 00:19:05 -07:00
Andy Port 4bc146fd62 save() now requires filename 2018-08-21 21:27:29 -07:00
Andy Port 0955278f73 aesthetic cleanup 2018-08-21 21:10:27 -07:00
Andy Port 7ebc56a831 aesthetic cleanup 2018-08-21 20:54:02 -07:00
Andy Port b1dfc9e8f7 removed future dependency 2018-08-21 20:09:29 -07:00
Andy Port ee5ab1813b updated for python3 compatibility 2018-08-21 20:07:53 -07:00
Andy Port 40a515ee63 removed all np.matrix uses (deprecation) 2018-08-21 20:05:59 -07:00
Andy Port 3d1a225503 removed future dependencies 2018-08-21 19:39:25 -07:00
Michael X. Grey 360d6b224c Flattening SVG groups and handling transforms (#55)
* Some progress (and added CONTRIBUTING.md)

* fixed documentation line-width to be PEP 8 compliant

* fixed documentation line-width to be PEP 8 compliant

* style changes

* made some design changes

* Make the Document class available when importing the library

* Add a method to parse transform strings

* Iterate on the implementation of the Document class

* Tweaks to transform parsing implementation

* Implementing a depth-first flattening of groups

* Finish implementation of flatten_paths

* Beginning to write tests for groups

* Refactoring flatten_paths() into flatten_all_paths()

* Clean up implementation of document classes

* Debugging xml namespace behavior -- needs improvement

* Improve the way the svg namespace is handled

* Print out some paths to see that they're sane

* Fix multiplication of numpy matrices -- need to use .dot() instead of operator*

* Create a unit test for parsing SVG groups

* Return a reference to an element instead of a copied dictionary of attributes

* Add a test for <path> elements that contain a 'transform' attribute

* minor docstring improvements

* got rid of svg2path changes (reverted to master)

* updated to match master

* Remove accidental paranthesis

* Remove unnecessary import

* Use a default width and height of 0, as dictated by SVG specs, in case width or height is missing

* Expose the CONVERSIONS and CONVERT_ONLY_PATHS constants

* Fix the use of some numpy operations

* Remove untested functions

* Fix add_group() and write tests for adding groups and paths

* Update documentation of document module

* Add tests for parsing transforms

* Update the module name for svg_to_paths

* Improve Python3 compatibility

* Try to improve compatibility

* More tweaks for compatibility
2018-08-21 18:00:29 -07:00
Andy ccc9ee6ae1 added clipping to work around floating point error in Arc._parameterize() 2018-07-24 15:07:02 -07:00
Andy b47345da91 added clipping to work around floating point error in Arc._parameterize() 2018-07-24 14:57:54 -07:00
Andy be946b8d92 removed 2018-07-24 14:35:39 -07:00
Andy 2b1670460e removed build & eggs-info from git repo 2018-07-24 14:34:28 -07:00
Andy 7fc7e45113 fixed false error raised when scipy not installed 2018-07-09 21:40:00 -04:00
Andy 8d5023939e updated for new PyPI release 2018-07-06 23:04:23 -04:00
Andy fb916596e2 fixed scale 2018-07-06 23:00:25 -04:00
Andy fd521748fa fixed scaled (before cleanup) 2018-07-06 22:59:28 -04:00
Andy 2cb56c50c5 added .travis.yml 2018-06-10 14:57:24 -07:00
Andy 31a8d2ac11 rgb2hex now allows lists as input 2018-06-10 14:41:27 -07:00
Andy 850055fa57 fixed bez2poly function 2018-06-10 14:38:08 -07:00
Andy a2ea4a0e80 fixed path curvature 2018-06-10 14:33:17 -07:00
Andy fd95b5609f partially fixed scale fcn and improved tests 2018-06-08 22:13:18 -07:00
mdejean f932036fb5 After Z, next subpath starts at same position (#53)
* After Z, next subpath starts at same position

Fixes #52

* removed old comment to mirror new behavior
2018-06-02 00:49:49 -07:00
Andy Port 18b1337877 renamed svg2paths module to allow imports 2018-06-02 00:32:16 -07:00
Andy Port 55661d18a4
Merge pull request #54 from playi/scale
add scale() for curves, and scaled() for paths
2018-05-30 21:20:22 -07:00
Andy Port 304c0bbe1d
improved `scale` related tests 2018-05-30 19:30:24 -07:00
Andy Port 72d7467896
implemented (almost) full SVG scale transform functionality 2018-05-30 19:07:58 -07:00
Orion Elenzil ee656c7de0 refactor `scale()` and `scaled()` to `scale_uniform()` and `scaled_uniform()` 2018-05-30 10:54:56 -07:00
Andy Port 6fcc71d63b minor docstring improvements 2018-05-28 19:46:38 -07:00
Andy Port 6da601f8a7
Merge pull request #56 from DmitryMilk/polyline-adjust-to-specification
Polyline/polygon parsing adjusted to specification
2018-05-28 18:50:12 -07:00
Dmitry_Milk 321e097a4e polygon/polyline parsing tests added 2018-05-27 12:34:12 +03:00
Dmitry_Milk 2dcfa88cf2 Adjust polyline/polygon parsing to W3C specification
https://www.w3.org/TR/SVG11/shapes.html#PointsBNF
2018-05-27 11:47:44 +03:00
Andy Port 0827206953
style changes 2018-05-22 19:59:03 -07:00
Andy Port eafe3682b9
altered so lines aren't (much) over 79 characters
Note: this is mostly unrelated to changes requested by @playi
2018-05-22 19:34:56 -07:00
Andy Port 6394415108
minor aesthetic change 2018-05-22 19:22:09 -07:00
Orion Elenzil 1ba9d45b35 unit test for new scale() and scaled() path transformation. tests all current segment types, composite paths, etc 2018-05-22 15:48:45 -07:00
Orion Elenzil d21a66aff0 add scale() for curves, and scaled() for paths 2018-05-22 12:45:04 -07:00
Andy Port 71b4403375
fix typo `joins_smoothl_with` 2018-04-13 14:34:28 -07:00
Andy Port 29a49197a7
Merge pull request #42 from SebKuzminsky/line-line-intersect
Fix Line.intersect(Line) (bug #41)
2018-02-27 22:38:33 -08:00
Andy Port a50c522f86
use np.close to check to check for vanishing denom
Just to offer users some amount of control over the tolerance.
2018-02-27 22:38:01 -08:00
Sebastian Kuzminsky 89d9acf06e Line.intersect(Line): fix a "miss some intersections" bug
Negative numbers are allowed in the denominator, what we really want to
avoid is near-zero denominators.
2018-01-20 20:46:54 -07:00
Sebastian Kuzminsky cc4573ffc7 add a failing Line.intersect(Line) test
I introduced this bug recently, sorry!  The bug is fixed in the following
commit.
2018-01-20 20:45:47 -07:00
Sebastian Kuzminsky de600f9b91 rename test to make room for more 2018-01-20 20:36:06 -07:00
Sebastian Kuzminsky fc34d2c4cf deal with float rounding error in Line.intersect(Line)
This commit fixes #41.

In the test case added in the previous commit, two non-intersecting lines
are very nearly collinear, but float rounding errors lead to incorrect
intersections reported.

This commit makes Line.intersect() treat denominators below 1e-9 as 0,
to make it more accepting of float rounding.
2018-01-06 23:15:43 -07:00
Sebastian Kuzminsky 847b270bc2 add a failing Line.intersect(Line) test
These two lines are parallel but do not intersect.  Line.intersect()
finds an incorrect intersection.
2018-01-06 23:05:55 -07:00
Andy bf95944c49 moved test for checking if `Arc()` throws `acos` domain error due to numerical error 2017-12-26 00:57:48 -05:00
Andy Port 50d7db0352
Merge pull request #31 from andersgb/acos-roundoff-fix
Arc parsing round-off fix
2017-12-25 23:55:26 -05:00
Andy Port c32c7a627b
Merge pull request #34 from SebKuzminsky/fixup-offset-example
README: fix a type error in Offseting Paths example
2017-12-23 02:51:50 -08:00
Sebastian Kuzminsky 2a456b168d README: remove dead code from Offsetting Paths example 2017-12-18 08:22:40 -07:00
Sebastian Kuzminsky 8c93eb0f2f README: fix a type error in Offseting Paths example
Before this commit the `t = k / steps` math is done with integers,
which always results in `t = 0`.

This commit forces the math to be done with floating-point numbers,
which results in the progression of `t` values we want.
2017-12-18 00:07:26 -07:00
Andy b54bf778b4 changed version number in preparation for PyPI update 2017-11-27 15:27:25 -08:00
Andy Port 1f074d5adc
changed setup to auto-install svgwrite + numpy 2017-11-27 15:17:29 -08:00
Anders Granskogen Bjørnstad ec63d0c312 Arc: Handle round-off issue with acos()
math.acos raises outside of valid range [-1, 1]. Handle round-off errors
gracefully by using `round()` here too.
2017-11-10 13:44:24 +01:00
Anders Granskogen Bjørnstad 316dc8bfe9 test_parsing.py: Add test_roundoff example
This test currently fails due to math.acos(1.0000000000001) raising a
ValueError.
2017-11-10 13:43:39 +01:00
Andy Port 5d65d575b5 Merge pull request #25 from mathandy/svgwrite-debug-false-by-default
now disvg and wsvg pass `debug=False` into svgwrite.Drawing() by default
2017-07-22 15:04:56 -07:00
Andy 586bccd0f2 now svgwrite debug is false by default 2017-07-18 20:01:37 -07:00
Andy Port f9229ca9bf minor change 2017-07-18 18:36:36 -07:00
Andy 5b1d0dc9cd added CONTRIBUTION.md -- guidelines for contributors 2017-07-18 18:33:00 -07:00
Andy e8367d463a added svg (planning to make unittest in future)
TODO: create unittest for rect element support
2017-04-26 02:21:43 -07:00
Andy 38eeac858c fixed support for rectangles when no x,y attributes are specified
defaults to 0,0
2017-04-26 02:21:01 -07:00
Andy d670547149 added polygon() and polyline() to __init__ 2017-04-26 02:20:11 -07:00
Andy a094f92b3a added polyline and polygon functions (points --> Path of Line objects) 2017-04-26 01:54:44 -07:00
Andy a3a529899c added rect element support to svg2paths 2017-04-26 01:53:34 -07:00
Andy 1d1bec1877 add ellipse/circle option to svg2paths2 and change return_svg_attributes to be second argument. 2017-04-23 01:14:59 -07:00
Andy aa02116e87 prettify svg2paths docstring (Google style) 2017-04-23 01:12:12 -07:00
Andy 900d5ba93b fixed typo and long line in docstrings 2017-04-23 00:45:41 -07:00
Andy Port 9a1b1ff1e5 Merge pull request #21 from jpcofr/ellipse_d_squashed
Added functionality to extract circle and ellipse elements from SVG files.  Circles and ellipses are treated as paths of two `Arc` objects.
2017-04-23 00:33:06 -07:00
Juan Pablo Contreras Franco cf91c95724 Merge 2017-04-23 07:53:10 +02:00
Juan Pablo Contreras Franco 216965e6ab Corrected wrongly deleted docstring line 2017-04-23 07:50:43 +02:00
Juan Pablo Contreras Franco 7b4bd38b21 Small syntax error in the docstring. 2017-04-23 07:38:24 +02:00
Andy Port 977914528c Merge pull request #18 from derVedro/patch-5
fixed ignored height and width arguments (specified using svg_attributes argument in disvg/wsvg)
2017-04-20 20:34:59 -07:00
Juan Pablo Contreras 2a24cf640d Added functionality to process both circles and ellipses.
Added tests related to the functionality.
2017-04-07 09:44:04 +02:00
Andy Port d8dfbd01fc changed default (when svg_attributes doesn't have a height/width) to the szx and szy constructed from path bounding boxes
@DerVedro was there a reason you thought the default should be 100% (beyond that being the current behavior -- which I see as a bug)?
2017-04-06 16:05:28 -07:00
derVedro 88185419f5 fliped width and height
the x and y were swaped by my mistake
2017-03-31 00:26:02 +02:00
derVedro f72987d69b fixed ignored height and width arguments
issue #17 fix
2017-03-31 00:17:59 +02:00
75 changed files with 5635 additions and 6138 deletions

14
.github/workflows/codacy.yml vendored Normal file
View File

@ -0,0 +1,14 @@
name: Codacy
on: ["push"]
jobs:
codacy-analysis-cli:
name: Codacy Analysis CLI
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@main
- name: Run Codacy Analysis CLI
uses: codacy/codacy-analysis-cli-action@master

65
.github/workflows/codeql-analysis.yml vendored Normal file
View File

@ -0,0 +1,65 @@
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
#
# ******** NOTE ********
# We have attempted to detect the languages in your repository. Please check
# the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages.
#
name: "CodeQL"
on:
push:
pull_request:
schedule:
- cron: '30 2 * * 3'
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
security-events: write
strategy:
fail-fast: false
matrix:
language: [ 'python' ]
steps:
- name: Checkout repository
uses: actions/checkout@v2
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# queries: ./path/to/local/query, your-org/your-repo/queries@main
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v1
# Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
# and modify them (or add more) to build your code if your project
# uses a compiled language
#- run: |
# make bootstrap
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1

34
.github/workflows/github-ci-legacy.yml vendored Normal file
View File

@ -0,0 +1,34 @@
name: Github CI Unit Testing for Legacy Environments
on:
push:
pull_request:
workflow_dispatch:
jobs:
build:
runs-on: ${{ matrix.os }}
continue-on-error: true
strategy:
matrix:
os: [ubuntu-18.04, macos-10.15, windows-2019]
python-version: [2.7, 3.5, 3.6]
steps:
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
- uses: actions/checkout@v2
# configure python
- uses: actions/setup-python@v2
with:
python-version: ${{ matrix.python-version }}
# install deps
- name: Install dependencies for ${{ matrix.os }} Python ${{ matrix.python-version }}
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
pip install scipy
# find and run all unit tests
- name: Run unit tests
run: python -m unittest discover test

34
.github/workflows/github-ci.yml vendored Normal file
View File

@ -0,0 +1,34 @@
name: Github CI Unit Testing
on:
push:
pull_request:
workflow_dispatch:
jobs:
build:
runs-on: ${{ matrix.os }}
continue-on-error: true
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
python-version: [3.7, 3.8, 3.9, "3.10", "3.11"]
steps:
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
- uses: actions/checkout@v2
# configure python
- uses: actions/setup-python@v2
with:
python-version: ${{ matrix.python-version }}
# install deps
- name: Install dependencies for ${{ matrix.os }} Python ${{ matrix.python-version }}
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
pip install scipy
# find and run all unit tests
- name: Run unit tests
run: python -m unittest discover test

View File

@ -0,0 +1,37 @@
name: Publish to TestPyPI
on:
push:
branches:
- master
jobs:
build-n-publish:
name: Build and publish to TestPyPI
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@master
- name: Set up Python 3
uses: actions/setup-python@v1
with:
python-version: 3
- name: Install pypa/build
run: >-
python -m
pip install
build
--user
- name: Build a binary wheel and a source tarball
run: >-
python -m
build
--sdist
--wheel
--outdir dist/
.
- name: Publish to Test PyPI
uses: pypa/gh-action-pypi-publish@release/v1
with:
skip_existing: true
password: ${{ secrets.TESTPYPI_API_TOKEN }}
repository_url: https://test.pypi.org/legacy/

42
.github/workflows/publish-on-pypi.yml vendored Normal file
View File

@ -0,0 +1,42 @@
name: Publish to PyPI if new version
on:
push:
tags:
- 'v*'
jobs:
build-n-publish:
name: Build and publish to TestPyPI and PyPI
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@master
- name: Set up Python 3
uses: actions/setup-python@v1
with:
python-version: 3
- name: Install pypa/build
run: >-
python -m
pip install
build
--user
- name: Build a binary wheel and a source tarball
run: >-
python -m
build
--sdist
--wheel
--outdir dist/
.
- name: Publish to Test PyPI
uses: pypa/gh-action-pypi-publish@release/v1
with:
skip_existing: true
password: ${{ secrets.TESTPYPI_API_TOKEN }}
repository_url: https://test.pypi.org/legacy/
- name: Publish to PyPI
if: startsWith(github.ref, 'refs/tags')
uses: pypa/gh-action-pypi-publish@release/v1
with:
password: ${{ secrets.PYPI_API_TOKEN }}

6
.gitignore vendored
View File

@ -1,4 +1,8 @@
*.pyc
.*
/svgpathtools/nonunittests
!/.gitignore
build
svgpathtools.egg-info
!.travis.yml
!/.gitignore
!/.github

65
CONTRIBUTING.md Normal file
View File

@ -0,0 +1,65 @@
# Contributing to svgpathtools
The following is a few and guidelines regarding the current philosophy, style,
flaws, and the future directions of svgpathtools. These guidelines are meant
to make it easy to contribute.
## Being a Hero
We need better automated testing coverage. Please, submit unittests! See the
Testing Style section below for info.
Here's a list of things that need (more) unittests:
* TBA (feel free to help)
## Submitting Bugs
If you find a bug, please submit an issue along with an **easily reproducible
example**. Feel free to make a pull-request too (see relevant section below).
## Submitting Pull-Requests
#### New features come with unittests and docstrings.
If you want to add a cool/useful feature to svgpathtools, that's great! Just
make sure your pull-request includes both thorough unittests and well-written
docstrings. See relevant sections below on "Testing Style" and
"Docstring Style" below.
#### Modifications to old code may require additional unittests.
Certain submodules of svgpathtools are poorly covered by the current set of
unittests. That said, most functionality in svgpathtools has been tested quite
a bit through use.
The point being, if you're working on functionality not currently covered by
unittests (and your changes replace more than a few lines), then please include
unittests designed to verify that any affected functionary still works.
## Style
### Coding Style
* Follow the PEP8 guidelines unless you have good reason to violate them (e.g.
you want your code's variable names to match some official documentation, or
PEP8 guidelines contradict those present in this document).
* Include docstrings and in-line comments where appropriate. See
"Docstring Style" section below for more info.
* Use explicit, uncontracted names (e.g. `parse_transform` instead of
`parse_trafo`). Maybe the most important feature for a name is how easy it is
for a user to guess (after having seen other names used in `svgpathtools`).
* Use a capital 'T' denote a Path object's parameter, use a lower case 't' to
denote a Path segment's parameter. See the methods `Path.t2T` and `Path.T2t`
if you're unsure what I mean. In the ambiguous case, use either 't' or another
appropriate option (e.g. "tau").
### Testing Style
You want to submit unittests?! Yes! Please see the svgpathtools/test folder
for examples.
### Docstring Style
All docstrings in svgpathtools should (roughly) adhere to the Google Python
Style Guide. Currently, this is not the case... but for the sake of
consistency, Google Style is the officially preferred docstring style of
svgpathtools.
[Some nice examples of Google Python Style docstrings](
https://sphinxcontrib-napoleon.readthedocs.io/en/latest/example_google.html)

View File

@ -2,11 +2,12 @@
"cells": [
{
"cell_type": "markdown",
"metadata": {
"deletable": true,
"editable": true
},
"metadata": {},
"source": [
"[![Donate](https://img.shields.io/badge/donate-paypal-brightgreen)](https://www.paypal.com/donate?business=4SKJ27AM4EYYA&amp;no_recurring=0&amp;item_name=Support+the+creator+of+svgpathtools?++He%27s+a+student+and+would+appreciate+it.&amp;currency_code=USD)\n",
"![Python](https://img.shields.io/pypi/pyversions/svgpathtools.svg)\n",
"[![PyPI](https://img.shields.io/pypi/v/svgpathtools)](https://pypi.org/project/svgpathtools/)\n",
"[![PyPI - Downloads](https://img.shields.io/pypi/dm/svgpathtools?color=yellow)](https://pypistats.org/packages/svgpathtools)\n",
"# svgpathtools\n",
"\n",
"svgpathtools is a collection of tools for manipulating and analyzing SVG Path objects and Bézier curves.\n",
@ -39,25 +40,15 @@
"## Prerequisites\n",
"- **numpy**\n",
"- **svgwrite**\n",
"- **scipy** (optional but recommended for performance)\n",
"\n",
"## Setup\n",
"\n",
"If not already installed, you can **install the prerequisites** using pip.\n",
"\n",
"```bash\n",
"$ pip install numpy\n",
"```\n",
"\n",
"```bash\n",
"$ pip install svgwrite\n",
"```\n",
"\n",
"Then **install svgpathtools**:\n",
"```bash\n",
"$ pip install svgpathtools\n",
"``` \n",
" \n",
"### Alternative Setup \n",
"### Alternative Setup\n",
"You can download the source from Github and install by using the command (from inside the folder containing setup.py):\n",
"\n",
"```bash\n",
@ -91,9 +82,7 @@
"cell_type": "code",
"execution_count": 1,
"metadata": {
"collapsed": true,
"deletable": true,
"editable": true
"collapsed": true
},
"outputs": [],
"source": [
@ -103,11 +92,7 @@
{
"cell_type": "code",
"execution_count": 2,
"metadata": {
"collapsed": false,
"deletable": true,
"editable": true
},
"metadata": {},
"outputs": [
{
"name": "stdout",
@ -146,10 +131,7 @@
},
{
"cell_type": "markdown",
"metadata": {
"deletable": true,
"editable": true
},
"metadata": {},
"source": [
"The ``Path`` class is a mutable sequence, so it behaves much like a list.\n",
"So segments can **append**ed, **insert**ed, set by index, **del**eted, **enumerate**d, **slice**d out, etc."
@ -158,11 +140,7 @@
{
"cell_type": "code",
"execution_count": 3,
"metadata": {
"collapsed": false,
"deletable": true,
"editable": true
},
"metadata": {},
"outputs": [
{
"name": "stdout",
@ -226,10 +204,7 @@
},
{
"cell_type": "markdown",
"metadata": {
"deletable": true,
"editable": true
},
"metadata": {},
"source": [
"### Reading SVGSs\n",
"\n",
@ -240,11 +215,7 @@
{
"cell_type": "code",
"execution_count": 4,
"metadata": {
"collapsed": false,
"deletable": true,
"editable": true
},
"metadata": {},
"outputs": [
{
"name": "stdout",
@ -277,10 +248,7 @@
},
{
"cell_type": "markdown",
"metadata": {
"deletable": true,
"editable": true
},
"metadata": {},
"source": [
"### Writing SVGSs (and some geometric functions and methods)\n",
"\n",
@ -291,11 +259,7 @@
{
"cell_type": "code",
"execution_count": 5,
"metadata": {
"collapsed": false,
"deletable": true,
"editable": true
},
"metadata": {},
"outputs": [],
"source": [
"# Let's make a new SVG that's identical to the first\n",
@ -304,20 +268,14 @@
},
{
"cell_type": "markdown",
"metadata": {
"deletable": true,
"editable": true
},
"metadata": {},
"source": [
"![output1.svg](output1.svg)"
]
},
{
"cell_type": "markdown",
"metadata": {
"deletable": true,
"editable": true
},
"metadata": {},
"source": [
"There will be many more examples of writing and displaying path data below.\n",
"\n",
@ -334,11 +292,7 @@
{
"cell_type": "code",
"execution_count": 6,
"metadata": {
"collapsed": false,
"deletable": true,
"editable": true
},
"metadata": {},
"outputs": [
{
"name": "stdout",
@ -374,10 +328,7 @@
},
{
"cell_type": "markdown",
"metadata": {
"deletable": true,
"editable": true
},
"metadata": {},
"source": [
"### Bezier curves as NumPy polynomial objects\n",
"Another great way to work with the parameterizations for `Line`, `QuadraticBezier`, and `CubicBezier` objects is to convert them to ``numpy.poly1d`` objects. This is done easily using the ``Line.poly()``, ``QuadraticBezier.poly()`` and ``CubicBezier.poly()`` methods. \n",
@ -402,11 +353,7 @@
{
"cell_type": "code",
"execution_count": 7,
"metadata": {
"collapsed": false,
"deletable": true,
"editable": true
},
"metadata": {},
"outputs": [
{
"name": "stdout",
@ -442,10 +389,7 @@
},
{
"cell_type": "markdown",
"metadata": {
"deletable": true,
"editable": true
},
"metadata": {},
"source": [
"The ability to convert between Bezier objects to NumPy polynomial objects is very useful. For starters, we can take turn a list of Bézier segments into a NumPy array \n",
"\n",
@ -461,11 +405,7 @@
{
"cell_type": "code",
"execution_count": 8,
"metadata": {
"collapsed": false,
"deletable": true,
"editable": true
},
"metadata": {},
"outputs": [
{
"name": "stdout",
@ -510,10 +450,7 @@
},
{
"cell_type": "markdown",
"metadata": {
"deletable": true,
"editable": true
},
"metadata": {},
"source": [
"### Translations (shifts), reversing orientation, and normal vectors"
]
@ -521,11 +458,7 @@
{
"cell_type": "code",
"execution_count": 9,
"metadata": {
"collapsed": false,
"deletable": true,
"editable": true
},
"metadata": {},
"outputs": [],
"source": [
"# Speaking of tangents, let's add a normal vector to the picture\n",
@ -551,20 +484,14 @@
},
{
"cell_type": "markdown",
"metadata": {
"deletable": true,
"editable": true
},
"metadata": {},
"source": [
"![vectorframes.svg](vectorframes.svg)"
]
},
{
"cell_type": "markdown",
"metadata": {
"deletable": true,
"editable": true
},
"metadata": {},
"source": [
"### Rotations and Translations"
]
@ -572,11 +499,7 @@
{
"cell_type": "code",
"execution_count": 10,
"metadata": {
"collapsed": false,
"deletable": true,
"editable": true
},
"metadata": {},
"outputs": [],
"source": [
"# Let's take a Line and an Arc and make some pictures\n",
@ -599,20 +522,14 @@
},
{
"cell_type": "markdown",
"metadata": {
"deletable": true,
"editable": true
},
"metadata": {},
"source": [
"![decorated_ellipse.svg](decorated_ellipse.svg)"
]
},
{
"cell_type": "markdown",
"metadata": {
"deletable": true,
"editable": true
},
"metadata": {},
"source": [
"### arc length and inverse arc length\n",
"\n",
@ -622,11 +539,7 @@
{
"cell_type": "code",
"execution_count": 11,
"metadata": {
"collapsed": false,
"deletable": true,
"editable": true
},
"metadata": {},
"outputs": [],
"source": [
"# First we'll load the path data from the file test.svg\n",
@ -664,20 +577,14 @@
},
{
"cell_type": "markdown",
"metadata": {
"deletable": true,
"editable": true
},
"metadata": {},
"source": [
"![output2.svg](output2.svg)"
]
},
{
"cell_type": "markdown",
"metadata": {
"deletable": true,
"editable": true
},
"metadata": {},
"source": [
"### Intersections between Bezier curves"
]
@ -685,11 +592,7 @@
{
"cell_type": "code",
"execution_count": 12,
"metadata": {
"collapsed": false,
"deletable": true,
"editable": true
},
"metadata": {},
"outputs": [],
"source": [
"# Let's find all intersections between redpath and the other \n",
@ -706,20 +609,14 @@
},
{
"cell_type": "markdown",
"metadata": {
"deletable": true,
"editable": true
},
"metadata": {},
"source": [
"![output_intersections.svg](output_intersections.svg)"
]
},
{
"cell_type": "markdown",
"metadata": {
"deletable": true,
"editable": true
},
"metadata": {},
"source": [
"### An Advanced Application: Offsetting Paths\n",
"Here we'll find the [offset curve](https://en.wikipedia.org/wiki/Parallel_curve) for a few paths."
@ -728,11 +625,7 @@
{
"cell_type": "code",
"execution_count": 13,
"metadata": {
"collapsed": false,
"deletable": true,
"editable": true
},
"metadata": {},
"outputs": [],
"source": [
"from svgpathtools import parse_path, Line, Path, wsvg\n",
@ -766,26 +659,20 @@
" for distances in offset_distances:\n",
" offset_paths.append(offset_curve(path, distances))\n",
"\n",
"# Note: This will take a few moments\n",
"# Let's take a look\n",
"wsvg(paths + offset_paths, 'g'*len(paths) + 'r'*len(offset_paths), filename='offset_curves.svg')"
]
},
{
"cell_type": "markdown",
"metadata": {
"deletable": true,
"editable": true
},
"metadata": {},
"source": [
"![offset_curves.svg](offset_curves.svg)"
]
},
{
"cell_type": "markdown",
"metadata": {
"deletable": true,
"editable": true
},
"metadata": {},
"source": [
"## Compatibility Notes for users of svg.path (v2.0)\n",
"\n",
@ -806,9 +693,7 @@
"cell_type": "code",
"execution_count": null,
"metadata": {
"collapsed": true,
"deletable": true,
"editable": true
"collapsed": true
},
"outputs": [],
"source": []
@ -823,16 +708,16 @@
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 2
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython2",
"version": "2.7.13"
"pygments_lexer": "ipython3",
"version": "3.7.6"
}
},
"nbformat": 4,
"nbformat_minor": 0
"nbformat_minor": 1
}

515
README.md Normal file
View File

@ -0,0 +1,515 @@
[![Donate](https://img.shields.io/badge/donate-paypal-brightgreen)](https://www.paypal.com/donate?business=4SKJ27AM4EYYA&amp;no_recurring=0&amp;item_name=Support+the+creator+of+svgpathtools?++He%27s+a+student+and+would+appreciate+it.&amp;currency_code=USD)
![Python](https://img.shields.io/pypi/pyversions/svgpathtools.svg)
[![PyPI](https://img.shields.io/pypi/v/svgpathtools)](https://pypi.org/project/svgpathtools/)
[![PyPI - Downloads](https://img.shields.io/pypi/dm/svgpathtools?color=yellow)](https://pypistats.org/packages/svgpathtools)
# svgpathtools
svgpathtools is a collection of tools for manipulating and analyzing SVG Path objects and Bézier curves.
## Features
svgpathtools contains functions designed to **easily read, write and display SVG files** as well as *a large selection of geometrically\-oriented tools* to **transform and analyze path elements**.
Additionally, the submodule *bezier.py* contains tools for for working with general **nth order Bezier curves stored as n-tuples**.
Some included tools:
- **read**, **write**, and **display** SVG files containing Path (and other) SVG elements
- convert Bézier path segments to **numpy.poly1d** (polynomial) objects
- convert polynomials (in standard form) to their Bézier form
- compute **tangent vectors** and (right-hand rule) **normal vectors**
- compute **curvature**
- break discontinuous paths into their **continuous subpaths**.
- efficiently compute **intersections** between paths and/or segments
- find a **bounding box** for a path or segment
- **reverse** segment/path orientation
- **crop** and **split** paths and segments
- **smooth** paths (i.e. smooth away kinks to make paths differentiable)
- **transition maps** from path domain to segment domain and back (T2t and t2T)
- compute **area** enclosed by a closed path
- compute **arc length**
- compute **inverse arc length**
- convert RGB color tuples to hexadecimal color strings and back
## Prerequisites
- **numpy**
- **svgwrite**
- **scipy** (optional, but recommended for performance)
## Setup
```bash
$ pip install svgpathtools
```
### Alternative Setup
You can download the source from Github and install by using the command (from inside the folder containing setup.py):
```bash
$ python setup.py install
```
## Credit where credit's due
Much of the core of this module was taken from [the svg.path (v2.0) module](https://github.com/regebro/svg.path). Interested svg.path users should see the compatibility notes at bottom of this readme.
## Basic Usage
### Classes
The svgpathtools module is primarily structured around four path segment classes: ``Line``, ``QuadraticBezier``, ``CubicBezier``, and ``Arc``. There is also a fifth class, ``Path``, whose objects are sequences of (connected or disconnected<sup id="a1">[1](#f1)</sup>) path segment objects.
* ``Line(start, end)``
* ``Arc(start, radius, rotation, large_arc, sweep, end)`` Note: See docstring for a detailed explanation of these parameters
* ``QuadraticBezier(start, control, end)``
* ``CubicBezier(start, control1, control2, end)``
* ``Path(*segments)``
See the relevant docstrings in *path.py* or the [official SVG specifications](<http://www.w3.org/TR/SVG/paths.html>) for more information on what each parameter means.
<u id="f1">1</u> Warning: Some of the functionality in this library has not been tested on discontinuous Path objects. A simple workaround is provided, however, by the ``Path.continuous_subpaths()`` method. [](#a1)
```python
from __future__ import division, print_function
```
```python
# Coordinates are given as points in the complex plane
from svgpathtools import Path, Line, QuadraticBezier, CubicBezier, Arc
seg1 = CubicBezier(300+100j, 100+100j, 200+200j, 200+300j) # A cubic beginning at (300, 100) and ending at (200, 300)
seg2 = Line(200+300j, 250+350j) # A line beginning at (200, 300) and ending at (250, 350)
path = Path(seg1, seg2) # A path traversing the cubic and then the line
# We could alternatively created this Path object using a d-string
from svgpathtools import parse_path
path_alt = parse_path('M 300 100 C 100 100 200 200 200 300 L 250 350')
# Let's check that these two methods are equivalent
print(path)
print(path_alt)
print(path == path_alt)
# On a related note, the Path.d() method returns a Path object's d-string
print(path.d())
print(parse_path(path.d()) == path)
```
Path(CubicBezier(start=(300+100j), control1=(100+100j), control2=(200+200j), end=(200+300j)),
Line(start=(200+300j), end=(250+350j)))
Path(CubicBezier(start=(300+100j), control1=(100+100j), control2=(200+200j), end=(200+300j)),
Line(start=(200+300j), end=(250+350j)))
True
M 300.0,100.0 C 100.0,100.0 200.0,200.0 200.0,300.0 L 250.0,350.0
True
The ``Path`` class is a mutable sequence, so it behaves much like a list.
So segments can **append**ed, **insert**ed, set by index, **del**eted, **enumerate**d, **slice**d out, etc.
```python
# Let's append another to the end of it
path.append(CubicBezier(250+350j, 275+350j, 250+225j, 200+100j))
print(path)
# Let's replace the first segment with a Line object
path[0] = Line(200+100j, 200+300j)
print(path)
# You may have noticed that this path is connected and now is also closed (i.e. path.start == path.end)
print("path is continuous? ", path.iscontinuous())
print("path is closed? ", path.isclosed())
# The curve the path follows is not, however, smooth (differentiable)
from svgpathtools import kinks, smoothed_path
print("path contains non-differentiable points? ", len(kinks(path)) > 0)
# If we want, we can smooth these out (Experimental and only for line/cubic paths)
# Note: smoothing will always works (except on 180 degree turns), but you may want
# to play with the maxjointsize and tightness parameters to get pleasing results
# Note also: smoothing will increase the number of segments in a path
spath = smoothed_path(path)
print("spath contains non-differentiable points? ", len(kinks(spath)) > 0)
print(spath)
# Let's take a quick look at the path and its smoothed relative
# The following commands will open two browser windows to display path and spaths
from svgpathtools import disvg
from time import sleep
disvg(path)
sleep(1) # needed when not giving the SVGs unique names (or not using timestamp)
disvg(spath)
print("Notice that path contains {} segments and spath contains {} segments."
"".format(len(path), len(spath)))
```
Path(CubicBezier(start=(300+100j), control1=(100+100j), control2=(200+200j), end=(200+300j)),
Line(start=(200+300j), end=(250+350j)),
CubicBezier(start=(250+350j), control1=(275+350j), control2=(250+225j), end=(200+100j)))
Path(Line(start=(200+100j), end=(200+300j)),
Line(start=(200+300j), end=(250+350j)),
CubicBezier(start=(250+350j), control1=(275+350j), control2=(250+225j), end=(200+100j)))
path is continuous? True
path is closed? True
path contains non-differentiable points? True
spath contains non-differentiable points? False
Path(Line(start=(200+101.5j), end=(200+298.5j)),
CubicBezier(start=(200+298.5j), control1=(200+298.505j), control2=(201.057124638+301.057124638j), end=(201.060660172+301.060660172j)),
Line(start=(201.060660172+301.060660172j), end=(248.939339828+348.939339828j)),
CubicBezier(start=(248.939339828+348.939339828j), control1=(249.649982143+349.649982143j), control2=(248.995+350j), end=(250+350j)),
CubicBezier(start=(250+350j), control1=(275+350j), control2=(250+225j), end=(200+100j)),
CubicBezier(start=(200+100j), control1=(199.62675237+99.0668809257j), control2=(200+100.495j), end=(200+101.5j)))
Notice that path contains 3 segments and spath contains 6 segments.
### Reading SVGSs
The **svg2paths()** function converts an svgfile to a list of Path objects and a separate list of dictionaries containing the attributes of each said path.
Note: Line, Polyline, Polygon, and Path SVG elements can all be converted to Path objects using this function.
```python
# Read SVG into a list of path objects and list of dictionaries of attributes
from svgpathtools import svg2paths, wsvg
paths, attributes = svg2paths('test.svg')
# Update: You can now also extract the svg-attributes by setting
# return_svg_attributes=True, or with the convenience function svg2paths2
from svgpathtools import svg2paths2
paths, attributes, svg_attributes = svg2paths2('test.svg')
# Let's print out the first path object and the color it was in the SVG
# We'll see it is composed of two CubicBezier objects and, in the SVG file it
# came from, it was red
redpath = paths[0]
redpath_attribs = attributes[0]
print(redpath)
print(redpath_attribs['stroke'])
```
Path(CubicBezier(start=(10.5+80j), control1=(40+10j), control2=(65+10j), end=(95+80j)),
CubicBezier(start=(95+80j), control1=(125+150j), control2=(150+150j), end=(180+80j)))
red
### Writing SVGSs (and some geometric functions and methods)
The **wsvg()** function creates an SVG file from a list of path. This function can do many things (see docstring in *paths2svg.py* for more information) and is meant to be quick and easy to use.
Note: Use the convenience function **disvg()** (or set 'openinbrowser=True') to automatically attempt to open the created svg file in your default SVG viewer.
```python
# Let's make a new SVG that's identical to the first
wsvg(paths, attributes=attributes, svg_attributes=svg_attributes, filename='output1.svg')
```
![output1.svg](output1.svg)
There will be many more examples of writing and displaying path data below.
### The .point() method and transitioning between path and path segment parameterizations
SVG Path elements and their segments have official parameterizations.
These parameterizations can be accessed using the ``Path.point()``, ``Line.point()``, ``QuadraticBezier.point()``, ``CubicBezier.point()``, and ``Arc.point()`` methods.
All these parameterizations are defined over the domain 0 <= t <= 1.
**Note:** In this document and in inline documentation and doctrings, I use a capital ``T`` when referring to the parameterization of a Path object and a lower case ``t`` when referring speaking about path segment objects (i.e. Line, QaudraticBezier, CubicBezier, and Arc objects).
Given a ``T`` value, the ``Path.T2t()`` method can be used to find the corresponding segment index, ``k``, and segment parameter, ``t``, such that ``path.point(T)=path[k].point(t)``.
There is also a ``Path.t2T()`` method to solve the inverse problem.
```python
# Example:
# Let's check that the first segment of redpath starts
# at the same point as redpath
firstseg = redpath[0]
print(redpath.point(0) == firstseg.point(0) == redpath.start == firstseg.start)
# Let's check that the last segment of redpath ends on the same point as redpath
lastseg = redpath[-1]
print(redpath.point(1) == lastseg.point(1) == redpath.end == lastseg.end)
# This next boolean should return False as redpath is composed multiple segments
print(redpath.point(0.5) == firstseg.point(0.5))
# If we want to figure out which segment of redpoint the
# point redpath.point(0.5) lands on, we can use the path.T2t() method
k, t = redpath.T2t(0.5)
print(redpath[k].point(t) == redpath.point(0.5))
```
True
True
False
True
### Bezier curves as NumPy polynomial objects
Another great way to work with the parameterizations for `Line`, `QuadraticBezier`, and `CubicBezier` objects is to convert them to ``numpy.poly1d`` objects. This is done easily using the ``Line.poly()``, ``QuadraticBezier.poly()`` and ``CubicBezier.poly()`` methods.
There's also a ``polynomial2bezier()`` function in the pathtools.py submodule to convert polynomials back to Bezier curves.
**Note:** cubic Bezier curves are parameterized as $$\mathcal{B}(t) = P_0(1-t)^3 + 3P_1(1-t)^2t + 3P_2(1-t)t^2 + P_3t^3$$
where $P_0$, $P_1$, $P_2$, and $P_3$ are the control points ``start``, ``control1``, ``control2``, and ``end``, respectively, that svgpathtools uses to define a CubicBezier object. The ``CubicBezier.poly()`` method expands this polynomial to its standard form
$$\mathcal{B}(t) = c_0t^3 + c_1t^2 +c_2t+c3$$
where
$$\begin{bmatrix}c_0\\c_1\\c_2\\c_3\end{bmatrix} =
\begin{bmatrix}
-1 & 3 & -3 & 1\\
3 & -6 & -3 & 0\\
-3 & 3 & 0 & 0\\
1 & 0 & 0 & 0\\
\end{bmatrix}
\begin{bmatrix}P_0\\P_1\\P_2\\P_3\end{bmatrix}$$
`QuadraticBezier.poly()` and `Line.poly()` are [defined similarly](https://en.wikipedia.org/wiki/B%C3%A9zier_curve#General_definition).
```python
# Example:
b = CubicBezier(300+100j, 100+100j, 200+200j, 200+300j)
p = b.poly()
# p(t) == b.point(t)
print(p(0.235) == b.point(0.235))
# What is p(t)? It's just the cubic b written in standard form.
bpretty = "{}*(1-t)^3 + 3*{}*(1-t)^2*t + 3*{}*(1-t)*t^2 + {}*t^3".format(*b.bpoints())
print("The CubicBezier, b.point(x) = \n\n" +
bpretty + "\n\n" +
"can be rewritten in standard form as \n\n" +
str(p).replace('x','t'))
```
True
The CubicBezier, b.point(x) =
(300+100j)*(1-t)^3 + 3*(100+100j)*(1-t)^2*t + 3*(200+200j)*(1-t)*t^2 + (200+300j)*t^3
can be rewritten in standard form as
3 2
(-400 + -100j) t + (900 + 300j) t - 600 t + (300 + 100j)
The ability to convert between Bezier objects to NumPy polynomial objects is very useful. For starters, we can take turn a list of Bézier segments into a NumPy array
### Numpy Array operations on Bézier path segments
[Example available here](https://github.com/mathandy/svgpathtools/blob/master/examples/compute-many-points-quickly-using-numpy-arrays.py)
To further illustrate the power of being able to convert our Bezier curve objects to numpy.poly1d objects and back, lets compute the unit tangent vector of the above CubicBezier object, b, at t=0.5 in four different ways.
### Tangent vectors (and more on NumPy polynomials)
```python
t = 0.5
### Method 1: the easy way
u1 = b.unit_tangent(t)
### Method 2: another easy way
# Note: This way will fail if it encounters a removable singularity.
u2 = b.derivative(t)/abs(b.derivative(t))
### Method 2: a third easy way
# Note: This way will also fail if it encounters a removable singularity.
dp = p.deriv()
u3 = dp(t)/abs(dp(t))
### Method 4: the removable-singularity-proof numpy.poly1d way
# Note: This is roughly how Method 1 works
from svgpathtools import real, imag, rational_limit
dx, dy = real(dp), imag(dp) # dp == dx + 1j*dy
p_mag2 = dx**2 + dy**2 # p_mag2(t) = |p(t)|**2
# Note: abs(dp) isn't a polynomial, but abs(dp)**2 is, and,
# the limit_{t->t0}[f(t) / abs(f(t))] ==
# sqrt(limit_{t->t0}[f(t)**2 / abs(f(t))**2])
from cmath import sqrt
u4 = sqrt(rational_limit(dp**2, p_mag2, t))
print("unit tangent check:", u1 == u2 == u3 == u4)
# Let's do a visual check
mag = b.length()/4 # so it's not hard to see the tangent line
tangent_line = Line(b.point(t), b.point(t) + mag*u1)
disvg([b, tangent_line], 'bg', nodes=[b.point(t)])
```
unit tangent check: True
### Translations (shifts), reversing orientation, and normal vectors
```python
# Speaking of tangents, let's add a normal vector to the picture
n = b.normal(t)
normal_line = Line(b.point(t), b.point(t) + mag*n)
disvg([b, tangent_line, normal_line], 'bgp', nodes=[b.point(t)])
# and let's reverse the orientation of b!
# the tangent and normal lines should be sent to their opposites
br = b.reversed()
# Let's also shift b_r over a bit to the right so we can view it next to b
# The simplest way to do this is br = br.translated(3*mag), but let's use
# the .bpoints() instead, which returns a Bezier's control points
br.start, br.control1, br.control2, br.end = [3*mag + bpt for bpt in br.bpoints()] #
tangent_line_r = Line(br.point(t), br.point(t) + mag*br.unit_tangent(t))
normal_line_r = Line(br.point(t), br.point(t) + mag*br.normal(t))
wsvg([b, tangent_line, normal_line, br, tangent_line_r, normal_line_r],
'bgpkgp', nodes=[b.point(t), br.point(t)], filename='vectorframes.svg',
text=["b's tangent", "br's tangent"], text_path=[tangent_line, tangent_line_r])
```
![vectorframes.svg](vectorframes.svg)
### Rotations and Translations
```python
# Let's take a Line and an Arc and make some pictures
top_half = Arc(start=-1, radius=1+2j, rotation=0, large_arc=1, sweep=1, end=1)
midline = Line(-1.5, 1.5)
# First let's make our ellipse whole
bottom_half = top_half.rotated(180)
decorated_ellipse = Path(top_half, bottom_half)
# Now let's add the decorations
for k in range(12):
decorated_ellipse.append(midline.rotated(30*k))
# Let's move it over so we can see the original Line and Arc object next
# to the final product
decorated_ellipse = decorated_ellipse.translated(4+0j)
wsvg([top_half, midline, decorated_ellipse], filename='decorated_ellipse.svg')
```
![decorated_ellipse.svg](decorated_ellipse.svg)
### arc length and inverse arc length
Here we'll create an SVG that shows off the parametric and geometric midpoints of the paths from ``test.svg``. We'll need to compute use the ``Path.length()``, ``Line.length()``, ``QuadraticBezier.length()``, ``CubicBezier.length()``, and ``Arc.length()`` methods, as well as the related inverse arc length methods ``.ilength()`` function to do this.
```python
# First we'll load the path data from the file test.svg
paths, attributes = svg2paths('test.svg')
# Let's mark the parametric midpoint of each segment
# I say "parametric" midpoint because Bezier curves aren't
# parameterized by arclength
# If they're also the geometric midpoint, let's mark them
# purple and otherwise we'll mark the geometric midpoint green
min_depth = 5
error = 1e-4
dots = []
ncols = []
nradii = []
for path in paths:
for seg in path:
parametric_mid = seg.point(0.5)
seg_length = seg.length()
if seg.length(0.5)/seg.length() == 1/2:
dots += [parametric_mid]
ncols += ['purple']
nradii += [5]
else:
t_mid = seg.ilength(seg_length/2)
geo_mid = seg.point(t_mid)
dots += [parametric_mid, geo_mid]
ncols += ['red', 'green']
nradii += [5] * 2
# In 'output2.svg' the paths will retain their original attributes
wsvg(paths, nodes=dots, node_colors=ncols, node_radii=nradii,
attributes=attributes, filename='output2.svg')
```
![output2.svg](output2.svg)
### Intersections between Bezier curves
```python
# Let's find all intersections between redpath and the other
redpath = paths[0]
redpath_attribs = attributes[0]
intersections = []
for path in paths[1:]:
for (T1, seg1, t1), (T2, seg2, t2) in redpath.intersect(path):
intersections.append(redpath.point(T1))
disvg(paths, filename='output_intersections.svg', attributes=attributes,
nodes = intersections, node_radii = [5]*len(intersections))
```
![output_intersections.svg](output_intersections.svg)
### An Advanced Application: Offsetting Paths
Here we'll find the [offset curve](https://en.wikipedia.org/wiki/Parallel_curve) for a few paths.
```python
from svgpathtools import parse_path, Line, Path, wsvg
def offset_curve(path, offset_distance, steps=1000):
"""Takes in a Path object, `path`, and a distance,
`offset_distance`, and outputs an piecewise-linear approximation
of the 'parallel' offset curve."""
nls = []
for seg in path:
ct = 1
for k in range(steps):
t = k / steps
offset_vector = offset_distance * seg.normal(t)
nl = Line(seg.point(t), seg.point(t) + offset_vector)
nls.append(nl)
connect_the_dots = [Line(nls[k].end, nls[k+1].end) for k in range(len(nls)-1)]
if path.isclosed():
connect_the_dots.append(Line(nls[-1].end, nls[0].end))
offset_path = Path(*connect_the_dots)
return offset_path
# Examples:
path1 = parse_path("m 288,600 c -52,-28 -42,-61 0,-97 ")
path2 = parse_path("M 151,395 C 407,485 726.17662,160 634,339").translated(300)
path3 = parse_path("m 117,695 c 237,-7 -103,-146 457,0").translated(500+400j)
paths = [path1, path2, path3]
offset_distances = [10*k for k in range(1,51)]
offset_paths = []
for path in paths:
for distances in offset_distances:
offset_paths.append(offset_curve(path, distances))
# Let's take a look
wsvg(paths + offset_paths, 'g'*len(paths) + 'r'*len(offset_paths), filename='offset_curves.svg')
```
![offset_curves.svg](offset_curves.svg)
## Compatibility Notes for users of svg.path (v2.0)
- renamed Arc.arc attribute as Arc.large_arc
- Path.d() : For behavior similar<sup id="a2">[2](#f2)</sup> to svg.path (v2.0), set both useSandT and use_closed_attrib to be True.
<u id="f2">2</u> The behavior would be identical, but the string formatting used in this method has been changed to use default format (instead of the General format, {:G}), for inceased precision. [](#a2)
Licence
-------
This module is under a MIT License.
```python
```

View File

@ -1,636 +0,0 @@
svgpathtools
============
svgpathtools is a collection of tools for manipulating and analyzing SVG
Path objects and Bézier curves.
Features
--------
svgpathtools contains functions designed to **easily read, write and
display SVG files** as well as *a large selection of
geometrically-oriented tools* to **transform and analyze path
elements**.
Additionally, the submodule *bezier.py* contains tools for for working
with general **nth order Bezier curves stored as n-tuples**.
Some included tools:
- **read**, **write**, and **display** SVG files containing Path (and
other) SVG elements
- convert Bézier path segments to **numpy.poly1d** (polynomial) objects
- convert polynomials (in standard form) to their Bézier form
- compute **tangent vectors** and (right-hand rule) **normal vectors**
- compute **curvature**
- break discontinuous paths into their **continuous subpaths**.
- efficiently compute **intersections** between paths and/or segments
- find a **bounding box** for a path or segment
- **reverse** segment/path orientation
- **crop** and **split** paths and segments
- **smooth** paths (i.e. smooth away kinks to make paths
differentiable)
- **transition maps** from path domain to segment domain and back (T2t
and t2T)
- compute **area** enclosed by a closed path
- compute **arc length**
- compute **inverse arc length**
- convert RGB color tuples to hexadecimal color strings and back
Prerequisites
-------------
- **numpy**
- **svgwrite**
Setup
-----
If not already installed, you can **install the prerequisites** using
pip.
.. code:: bash
$ pip install numpy
.. code:: bash
$ pip install svgwrite
Then **install svgpathtools**:
.. code:: bash
$ pip install svgpathtools
Alternative Setup
~~~~~~~~~~~~~~~~~
You can download the source from Github and install by using the command
(from inside the folder containing setup.py):
.. code:: bash
$ python setup.py install
Credit where credit's due
-------------------------
Much of the core of this module was taken from `the svg.path (v2.0)
module <https://github.com/regebro/svg.path>`__. Interested svg.path
users should see the compatibility notes at bottom of this readme.
Basic Usage
-----------
Classes
~~~~~~~
The svgpathtools module is primarily structured around four path segment
classes: ``Line``, ``QuadraticBezier``, ``CubicBezier``, and ``Arc``.
There is also a fifth class, ``Path``, whose objects are sequences of
(connected or disconnected\ `1 <#f1>`__\ ) path segment objects.
- ``Line(start, end)``
- ``Arc(start, radius, rotation, large_arc, sweep, end)`` Note: See
docstring for a detailed explanation of these parameters
- ``QuadraticBezier(start, control, end)``
- ``CubicBezier(start, control1, control2, end)``
- ``Path(*segments)``
See the relevant docstrings in *path.py* or the `official SVG
specifications <http://www.w3.org/TR/SVG/paths.html>`__ for more
information on what each parameter means.
1 Warning: Some of the functionality in this library has not been tested
on discontinuous Path objects. A simple workaround is provided, however,
by the ``Path.continuous_subpaths()`` method. `↩ <#a1>`__
.. code:: ipython2
from __future__ import division, print_function
.. code:: ipython2
# Coordinates are given as points in the complex plane
from svgpathtools import Path, Line, QuadraticBezier, CubicBezier, Arc
seg1 = CubicBezier(300+100j, 100+100j, 200+200j, 200+300j) # A cubic beginning at (300, 100) and ending at (200, 300)
seg2 = Line(200+300j, 250+350j) # A line beginning at (200, 300) and ending at (250, 350)
path = Path(seg1, seg2) # A path traversing the cubic and then the line
# We could alternatively created this Path object using a d-string
from svgpathtools import parse_path
path_alt = parse_path('M 300 100 C 100 100 200 200 200 300 L 250 350')
# Let's check that these two methods are equivalent
print(path)
print(path_alt)
print(path == path_alt)
# On a related note, the Path.d() method returns a Path object's d-string
print(path.d())
print(parse_path(path.d()) == path)
.. parsed-literal::
Path(CubicBezier(start=(300+100j), control1=(100+100j), control2=(200+200j), end=(200+300j)),
Line(start=(200+300j), end=(250+350j)))
Path(CubicBezier(start=(300+100j), control1=(100+100j), control2=(200+200j), end=(200+300j)),
Line(start=(200+300j), end=(250+350j)))
True
M 300.0,100.0 C 100.0,100.0 200.0,200.0 200.0,300.0 L 250.0,350.0
True
The ``Path`` class is a mutable sequence, so it behaves much like a
list. So segments can **append**\ ed, **insert**\ ed, set by index,
**del**\ eted, **enumerate**\ d, **slice**\ d out, etc.
.. code:: ipython2
# Let's append another to the end of it
path.append(CubicBezier(250+350j, 275+350j, 250+225j, 200+100j))
print(path)
# Let's replace the first segment with a Line object
path[0] = Line(200+100j, 200+300j)
print(path)
# You may have noticed that this path is connected and now is also closed (i.e. path.start == path.end)
print("path is continuous? ", path.iscontinuous())
print("path is closed? ", path.isclosed())
# The curve the path follows is not, however, smooth (differentiable)
from svgpathtools import kinks, smoothed_path
print("path contains non-differentiable points? ", len(kinks(path)) > 0)
# If we want, we can smooth these out (Experimental and only for line/cubic paths)
# Note: smoothing will always works (except on 180 degree turns), but you may want
# to play with the maxjointsize and tightness parameters to get pleasing results
# Note also: smoothing will increase the number of segments in a path
spath = smoothed_path(path)
print("spath contains non-differentiable points? ", len(kinks(spath)) > 0)
print(spath)
# Let's take a quick look at the path and its smoothed relative
# The following commands will open two browser windows to display path and spaths
from svgpathtools import disvg
from time import sleep
disvg(path)
sleep(1) # needed when not giving the SVGs unique names (or not using timestamp)
disvg(spath)
print("Notice that path contains {} segments and spath contains {} segments."
"".format(len(path), len(spath)))
.. parsed-literal::
Path(CubicBezier(start=(300+100j), control1=(100+100j), control2=(200+200j), end=(200+300j)),
Line(start=(200+300j), end=(250+350j)),
CubicBezier(start=(250+350j), control1=(275+350j), control2=(250+225j), end=(200+100j)))
Path(Line(start=(200+100j), end=(200+300j)),
Line(start=(200+300j), end=(250+350j)),
CubicBezier(start=(250+350j), control1=(275+350j), control2=(250+225j), end=(200+100j)))
path is continuous? True
path is closed? True
path contains non-differentiable points? True
spath contains non-differentiable points? False
Path(Line(start=(200+101.5j), end=(200+298.5j)),
CubicBezier(start=(200+298.5j), control1=(200+298.505j), control2=(201.057124638+301.057124638j), end=(201.060660172+301.060660172j)),
Line(start=(201.060660172+301.060660172j), end=(248.939339828+348.939339828j)),
CubicBezier(start=(248.939339828+348.939339828j), control1=(249.649982143+349.649982143j), control2=(248.995+350j), end=(250+350j)),
CubicBezier(start=(250+350j), control1=(275+350j), control2=(250+225j), end=(200+100j)),
CubicBezier(start=(200+100j), control1=(199.62675237+99.0668809257j), control2=(200+100.495j), end=(200+101.5j)))
Notice that path contains 3 segments and spath contains 6 segments.
Reading SVGSs
~~~~~~~~~~~~~
| The **svg2paths()** function converts an svgfile to a list of Path
objects and a separate list of dictionaries containing the attributes
of each said path.
| Note: Line, Polyline, Polygon, and Path SVG elements can all be
converted to Path objects using this function.
.. code:: ipython2
# Read SVG into a list of path objects and list of dictionaries of attributes
from svgpathtools import svg2paths, wsvg
paths, attributes = svg2paths('test.svg')
# Update: You can now also extract the svg-attributes by setting
# return_svg_attributes=True, or with the convenience function svg2paths2
from svgpathtools import svg2paths2
paths, attributes, svg_attributes = svg2paths2('test.svg')
# Let's print out the first path object and the color it was in the SVG
# We'll see it is composed of two CubicBezier objects and, in the SVG file it
# came from, it was red
redpath = paths[0]
redpath_attribs = attributes[0]
print(redpath)
print(redpath_attribs['stroke'])
.. parsed-literal::
Path(CubicBezier(start=(10.5+80j), control1=(40+10j), control2=(65+10j), end=(95+80j)),
CubicBezier(start=(95+80j), control1=(125+150j), control2=(150+150j), end=(180+80j)))
red
Writing SVGSs (and some geometric functions and methods)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The **wsvg()** function creates an SVG file from a list of path. This
function can do many things (see docstring in *paths2svg.py* for more
information) and is meant to be quick and easy to use. Note: Use the
convenience function **disvg()** (or set 'openinbrowser=True') to
automatically attempt to open the created svg file in your default SVG
viewer.
.. code:: ipython2
# Let's make a new SVG that's identical to the first
wsvg(paths, attributes=attributes, svg_attributes=svg_attributes, filename='output1.svg')
.. figure:: https://cdn.rawgit.com/mathandy/svgpathtools/master/output1.svg
:alt: output1.svg
output1.svg
There will be many more examples of writing and displaying path data
below.
The .point() method and transitioning between path and path segment parameterizations
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
SVG Path elements and their segments have official parameterizations.
These parameterizations can be accessed using the ``Path.point()``,
``Line.point()``, ``QuadraticBezier.point()``, ``CubicBezier.point()``,
and ``Arc.point()`` methods. All these parameterizations are defined
over the domain 0 <= t <= 1.
| **Note:** In this document and in inline documentation and doctrings,
I use a capital ``T`` when referring to the parameterization of a Path
object and a lower case ``t`` when referring speaking about path
segment objects (i.e. Line, QaudraticBezier, CubicBezier, and Arc
objects).
| Given a ``T`` value, the ``Path.T2t()`` method can be used to find the
corresponding segment index, ``k``, and segment parameter, ``t``, such
that ``path.point(T)=path[k].point(t)``.
| There is also a ``Path.t2T()`` method to solve the inverse problem.
.. code:: ipython2
# Example:
# Let's check that the first segment of redpath starts
# at the same point as redpath
firstseg = redpath[0]
print(redpath.point(0) == firstseg.point(0) == redpath.start == firstseg.start)
# Let's check that the last segment of redpath ends on the same point as redpath
lastseg = redpath[-1]
print(redpath.point(1) == lastseg.point(1) == redpath.end == lastseg.end)
# This next boolean should return False as redpath is composed multiple segments
print(redpath.point(0.5) == firstseg.point(0.5))
# If we want to figure out which segment of redpoint the
# point redpath.point(0.5) lands on, we can use the path.T2t() method
k, t = redpath.T2t(0.5)
print(redpath[k].point(t) == redpath.point(0.5))
.. parsed-literal::
True
True
False
True
Bezier curves as NumPy polynomial objects
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
| Another great way to work with the parameterizations for ``Line``,
``QuadraticBezier``, and ``CubicBezier`` objects is to convert them to
``numpy.poly1d`` objects. This is done easily using the
``Line.poly()``, ``QuadraticBezier.poly()`` and ``CubicBezier.poly()``
methods.
| There's also a ``polynomial2bezier()`` function in the pathtools.py
submodule to convert polynomials back to Bezier curves.
**Note:** cubic Bezier curves are parameterized as
.. math:: \mathcal{B}(t) = P_0(1-t)^3 + 3P_1(1-t)^2t + 3P_2(1-t)t^2 + P_3t^3
where :math:`P_0`, :math:`P_1`, :math:`P_2`, and :math:`P_3` are the
control points ``start``, ``control1``, ``control2``, and ``end``,
respectively, that svgpathtools uses to define a CubicBezier object. The
``CubicBezier.poly()`` method expands this polynomial to its standard
form
.. math:: \mathcal{B}(t) = c_0t^3 + c_1t^2 +c_2t+c3
where
.. math::
\begin{bmatrix}c_0\\c_1\\c_2\\c_3\end{bmatrix} =
\begin{bmatrix}
-1 & 3 & -3 & 1\\
3 & -6 & -3 & 0\\
-3 & 3 & 0 & 0\\
1 & 0 & 0 & 0\\
\end{bmatrix}
\begin{bmatrix}P_0\\P_1\\P_2\\P_3\end{bmatrix}
``QuadraticBezier.poly()`` and ``Line.poly()`` are `defined
similarly <https://en.wikipedia.org/wiki/B%C3%A9zier_curve#General_definition>`__.
.. code:: ipython2
# Example:
b = CubicBezier(300+100j, 100+100j, 200+200j, 200+300j)
p = b.poly()
# p(t) == b.point(t)
print(p(0.235) == b.point(0.235))
# What is p(t)? It's just the cubic b written in standard form.
bpretty = "{}*(1-t)^3 + 3*{}*(1-t)^2*t + 3*{}*(1-t)*t^2 + {}*t^3".format(*b.bpoints())
print("The CubicBezier, b.point(x) = \n\n" +
bpretty + "\n\n" +
"can be rewritten in standard form as \n\n" +
str(p).replace('x','t'))
.. parsed-literal::
True
The CubicBezier, b.point(x) =
(300+100j)*(1-t)^3 + 3*(100+100j)*(1-t)^2*t + 3*(200+200j)*(1-t)*t^2 + (200+300j)*t^3
can be rewritten in standard form as
3 2
(-400 + -100j) t + (900 + 300j) t - 600 t + (300 + 100j)
The ability to convert between Bezier objects to NumPy polynomial
objects is very useful. For starters, we can take turn a list of Bézier
segments into a NumPy array
Numpy Array operations on Bézier path segments
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
`Example available
here <https://github.com/mathandy/svgpathtools/blob/master/examples/compute-many-points-quickly-using-numpy-arrays.py>`__
To further illustrate the power of being able to convert our Bezier
curve objects to numpy.poly1d objects and back, lets compute the unit
tangent vector of the above CubicBezier object, b, at t=0.5 in four
different ways.
Tangent vectors (and more on NumPy polynomials)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. code:: ipython2
t = 0.5
### Method 1: the easy way
u1 = b.unit_tangent(t)
### Method 2: another easy way
# Note: This way will fail if it encounters a removable singularity.
u2 = b.derivative(t)/abs(b.derivative(t))
### Method 2: a third easy way
# Note: This way will also fail if it encounters a removable singularity.
dp = p.deriv()
u3 = dp(t)/abs(dp(t))
### Method 4: the removable-singularity-proof numpy.poly1d way
# Note: This is roughly how Method 1 works
from svgpathtools import real, imag, rational_limit
dx, dy = real(dp), imag(dp) # dp == dx + 1j*dy
p_mag2 = dx**2 + dy**2 # p_mag2(t) = |p(t)|**2
# Note: abs(dp) isn't a polynomial, but abs(dp)**2 is, and,
# the limit_{t->t0}[f(t) / abs(f(t))] ==
# sqrt(limit_{t->t0}[f(t)**2 / abs(f(t))**2])
from cmath import sqrt
u4 = sqrt(rational_limit(dp**2, p_mag2, t))
print("unit tangent check:", u1 == u2 == u3 == u4)
# Let's do a visual check
mag = b.length()/4 # so it's not hard to see the tangent line
tangent_line = Line(b.point(t), b.point(t) + mag*u1)
disvg([b, tangent_line], 'bg', nodes=[b.point(t)])
.. parsed-literal::
unit tangent check: True
Translations (shifts), reversing orientation, and normal vectors
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. code:: ipython2
# Speaking of tangents, let's add a normal vector to the picture
n = b.normal(t)
normal_line = Line(b.point(t), b.point(t) + mag*n)
disvg([b, tangent_line, normal_line], 'bgp', nodes=[b.point(t)])
# and let's reverse the orientation of b!
# the tangent and normal lines should be sent to their opposites
br = b.reversed()
# Let's also shift b_r over a bit to the right so we can view it next to b
# The simplest way to do this is br = br.translated(3*mag), but let's use
# the .bpoints() instead, which returns a Bezier's control points
br.start, br.control1, br.control2, br.end = [3*mag + bpt for bpt in br.bpoints()] #
tangent_line_r = Line(br.point(t), br.point(t) + mag*br.unit_tangent(t))
normal_line_r = Line(br.point(t), br.point(t) + mag*br.normal(t))
wsvg([b, tangent_line, normal_line, br, tangent_line_r, normal_line_r],
'bgpkgp', nodes=[b.point(t), br.point(t)], filename='vectorframes.svg',
text=["b's tangent", "br's tangent"], text_path=[tangent_line, tangent_line_r])
.. figure:: https://cdn.rawgit.com/mathandy/svgpathtools/master/vectorframes.svg
:alt: vectorframes.svg
vectorframes.svg
Rotations and Translations
~~~~~~~~~~~~~~~~~~~~~~~~~~
.. code:: ipython2
# Let's take a Line and an Arc and make some pictures
top_half = Arc(start=-1, radius=1+2j, rotation=0, large_arc=1, sweep=1, end=1)
midline = Line(-1.5, 1.5)
# First let's make our ellipse whole
bottom_half = top_half.rotated(180)
decorated_ellipse = Path(top_half, bottom_half)
# Now let's add the decorations
for k in range(12):
decorated_ellipse.append(midline.rotated(30*k))
# Let's move it over so we can see the original Line and Arc object next
# to the final product
decorated_ellipse = decorated_ellipse.translated(4+0j)
wsvg([top_half, midline, decorated_ellipse], filename='decorated_ellipse.svg')
.. figure:: https://cdn.rawgit.com/mathandy/svgpathtools/master/decorated_ellipse.svg
:alt: decorated\_ellipse.svg
decorated\_ellipse.svg
arc length and inverse arc length
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Here we'll create an SVG that shows off the parametric and geometric
midpoints of the paths from ``test.svg``. We'll need to compute use the
``Path.length()``, ``Line.length()``, ``QuadraticBezier.length()``,
``CubicBezier.length()``, and ``Arc.length()`` methods, as well as the
related inverse arc length methods ``.ilength()`` function to do this.
.. code:: ipython2
# First we'll load the path data from the file test.svg
paths, attributes = svg2paths('test.svg')
# Let's mark the parametric midpoint of each segment
# I say "parametric" midpoint because Bezier curves aren't
# parameterized by arclength
# If they're also the geometric midpoint, let's mark them
# purple and otherwise we'll mark the geometric midpoint green
min_depth = 5
error = 1e-4
dots = []
ncols = []
nradii = []
for path in paths:
for seg in path:
parametric_mid = seg.point(0.5)
seg_length = seg.length()
if seg.length(0.5)/seg.length() == 1/2:
dots += [parametric_mid]
ncols += ['purple']
nradii += [5]
else:
t_mid = seg.ilength(seg_length/2)
geo_mid = seg.point(t_mid)
dots += [parametric_mid, geo_mid]
ncols += ['red', 'green']
nradii += [5] * 2
# In 'output2.svg' the paths will retain their original attributes
wsvg(paths, nodes=dots, node_colors=ncols, node_radii=nradii,
attributes=attributes, filename='output2.svg')
.. figure:: https://cdn.rawgit.com/mathandy/svgpathtools/master/output2.svg
:alt: output2.svg
output2.svg
Intersections between Bezier curves
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. code:: ipython2
# Let's find all intersections between redpath and the other
redpath = paths[0]
redpath_attribs = attributes[0]
intersections = []
for path in paths[1:]:
for (T1, seg1, t1), (T2, seg2, t2) in redpath.intersect(path):
intersections.append(redpath.point(T1))
disvg(paths, filename='output_intersections.svg', attributes=attributes,
nodes = intersections, node_radii = [5]*len(intersections))
.. figure:: https://cdn.rawgit.com/mathandy/svgpathtools/master/output_intersections.svg
:alt: output\_intersections.svg
output\_intersections.svg
An Advanced Application: Offsetting Paths
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Here we'll find the `offset
curve <https://en.wikipedia.org/wiki/Parallel_curve>`__ for a few paths.
.. code:: ipython2
from svgpathtools import parse_path, Line, Path, wsvg
def offset_curve(path, offset_distance, steps=1000):
"""Takes in a Path object, `path`, and a distance,
`offset_distance`, and outputs an piecewise-linear approximation
of the 'parallel' offset curve."""
nls = []
for seg in path:
ct = 1
for k in range(steps):
t = k / steps
offset_vector = offset_distance * seg.normal(t)
nl = Line(seg.point(t), seg.point(t) + offset_vector)
nls.append(nl)
connect_the_dots = [Line(nls[k].end, nls[k+1].end) for k in range(len(nls)-1)]
if path.isclosed():
connect_the_dots.append(Line(nls[-1].end, nls[0].end))
offset_path = Path(*connect_the_dots)
return offset_path
# Examples:
path1 = parse_path("m 288,600 c -52,-28 -42,-61 0,-97 ")
path2 = parse_path("M 151,395 C 407,485 726.17662,160 634,339").translated(300)
path3 = parse_path("m 117,695 c 237,-7 -103,-146 457,0").translated(500+400j)
paths = [path1, path2, path3]
offset_distances = [10*k for k in range(1,51)]
offset_paths = []
for path in paths:
for distances in offset_distances:
offset_paths.append(offset_curve(path, distances))
# Note: This will take a few moments
wsvg(paths + offset_paths, 'g'*len(paths) + 'r'*len(offset_paths), filename='offset_curves.svg')
.. figure:: https://cdn.rawgit.com/mathandy/svgpathtools/master/offset_curves.svg
:alt: offset\_curves.svg
offset\_curves.svg
Compatibility Notes for users of svg.path (v2.0)
------------------------------------------------
- renamed Arc.arc attribute as Arc.large\_arc
- Path.d() : For behavior similar\ `2 <#f2>`__\ to svg.path (v2.0),
set both useSandT and use\_closed\_attrib to be True.
2 The behavior would be identical, but the string formatting used in
this method has been changed to use default format (instead of the
General format, {:G}), for inceased precision. `↩ <#a2>`__
Licence
-------
This module is under a MIT License.

5
SECURITY.md Normal file
View File

@ -0,0 +1,5 @@
# Security Policy
## Reporting a Vulnerability
To report any security vulnerability, email andyaport@gmail.com

View File

@ -1,19 +0,0 @@
from .bezier import (bezier_point, bezier2polynomial,
polynomial2bezier, split_bezier,
bezier_bounding_box, bezier_intersections,
bezier_by_line_intersections)
from .path import (Path, Line, QuadraticBezier, CubicBezier, Arc,
bezier_segment, is_bezier_segment, is_path_segment,
is_bezier_path, concatpaths, poly2bez, bpoints2bezier,
closest_point_in_path, farthest_point_in_path,
path_encloses_pt, bbox2path)
from .parser import parse_path
from .paths2svg import disvg, wsvg
from .polytools import polyroots, polyroots01, rational_limit, real, imag
from .misctools import hex2rgb, rgb2hex
from .smoothing import smoothed_path, smoothed_joint, is_differentiable, kinks
try:
from .svg2paths import svg2paths, svg2paths2
except ImportError:
pass

View File

@ -1,375 +0,0 @@
"""This submodule contains tools that deal with generic, degree n, Bezier
curves.
Note: Bezier curves here are always represented by the tuple of their control
points given by their standard representation."""
# External dependencies:
from __future__ import division, absolute_import, print_function
from math import factorial as fac, ceil, log, sqrt
from numpy import poly1d
# Internal dependencies
from .polytools import real, imag, polyroots, polyroots01
# Evaluation ##################################################################
def n_choose_k(n, k):
return fac(n)//fac(k)//fac(n-k)
def bernstein(n, t):
"""returns a list of the Bernstein basis polynomials b_{i, n} evaluated at
t, for i =0...n"""
t1 = 1-t
return [n_choose_k(n, k) * t1**(n-k) * t**k for k in range(n+1)]
def bezier_point(p, t):
"""Evaluates the Bezier curve given by it's control points, p, at t.
Note: Uses Horner's rule for cubic and lower order Bezier curves.
Warning: Be concerned about numerical stability when using this function
with high order curves."""
# begin arc support block ########################
try:
p.large_arc
return p.point(t)
except:
pass
# end arc support block ##########################
deg = len(p) - 1
if deg == 3:
return p[0] + t*(
3*(p[1] - p[0]) + t*(
3*(p[0] + p[2]) - 6*p[1] + t*(
-p[0] + 3*(p[1] - p[2]) + p[3])))
elif deg == 2:
return p[0] + t*(
2*(p[1] - p[0]) + t*(
p[0] - 2*p[1] + p[2]))
elif deg == 1:
return p[0] + t*(p[1] - p[0])
elif deg == 0:
return p[0]
else:
bern = bernstein(deg, t)
return sum(bern[k]*p[k] for k in range(deg+1))
# Conversion ##################################################################
def bezier2polynomial(p, numpy_ordering=True, return_poly1d=False):
"""Converts a tuple of Bezier control points to a tuple of coefficients
of the expanded polynomial.
return_poly1d : returns a numpy.poly1d object. This makes computations
of derivatives/anti-derivatives and many other operations quite quick.
numpy_ordering : By default (to accommodate numpy) the coefficients will
be output in reverse standard order."""
if len(p) == 4:
coeffs = (-p[0] + 3*(p[1] - p[2]) + p[3],
3*(p[0] - 2*p[1] + p[2]),
3*(p[1]-p[0]),
p[0])
elif len(p) == 3:
coeffs = (p[0] - 2*p[1] + p[2],
2*(p[1] - p[0]),
p[0])
elif len(p) == 2:
coeffs = (p[1]-p[0],
p[0])
elif len(p) == 1:
coeffs = p
else:
# https://en.wikipedia.org/wiki/Bezier_curve#Polynomial_form
n = len(p) - 1
coeffs = [fac(n)//fac(n-j) * sum(
(-1)**(i+j) * p[i] / (fac(i) * fac(j-i)) for i in range(j+1))
for j in range(n+1)]
coeffs.reverse()
if not numpy_ordering:
coeffs = coeffs[::-1] # can't use .reverse() as might be tuple
if return_poly1d:
return poly1d(coeffs)
return coeffs
def polynomial2bezier(poly):
"""Converts a cubic or lower order Polynomial object (or a sequence of
coefficients) to a CubicBezier, QuadraticBezier, or Line object as
appropriate."""
if isinstance(poly, poly1d):
c = poly.coeffs
else:
c = poly
order = len(c)-1
if order == 3:
bpoints = (c[3], c[2]/3 + c[3], (c[1] + 2*c[2])/3 + c[3],
c[0] + c[1] + c[2] + c[3])
elif order == 2:
bpoints = (c[2], c[1]/2 + c[2], c[0] + c[1] + c[2])
elif order == 1:
bpoints = (c[1], c[0] + c[1])
else:
raise AssertionError("This function is only implemented for linear, "
"quadratic, and cubic polynomials.")
return bpoints
# Curve Splitting #############################################################
def split_bezier(bpoints, t):
"""Uses deCasteljau's recursion to split the Bezier curve at t into two
Bezier curves of the same order."""
def split_bezier_recursion(bpoints_left_, bpoints_right_, bpoints_, t_):
if len(bpoints_) == 1:
bpoints_left_.append(bpoints_[0])
bpoints_right_.append(bpoints_[0])
else:
new_points = [None]*(len(bpoints_) - 1)
bpoints_left_.append(bpoints_[0])
bpoints_right_.append(bpoints_[-1])
for i in range(len(bpoints_) - 1):
new_points[i] = (1 - t_)*bpoints_[i] + t_*bpoints_[i + 1]
bpoints_left_, bpoints_right_ = split_bezier_recursion(
bpoints_left_, bpoints_right_, new_points, t_)
return bpoints_left_, bpoints_right_
bpoints_left = []
bpoints_right = []
bpoints_left, bpoints_right = \
split_bezier_recursion(bpoints_left, bpoints_right, bpoints, t)
bpoints_right.reverse()
return bpoints_left, bpoints_right
def halve_bezier(p):
# begin arc support block ########################
try:
p.large_arc
return p.split(0.5)
except:
pass
# end arc support block ##########################
if len(p) == 4:
return ([p[0], (p[0] + p[1])/2, (p[0] + 2*p[1] + p[2])/4,
(p[0] + 3*p[1] + 3*p[2] + p[3])/8],
[(p[0] + 3*p[1] + 3*p[2] + p[3])/8,
(p[1] + 2*p[2] + p[3])/4, (p[2] + p[3])/2, p[3]])
else:
return split_bezier(p, 0.5)
# Bounding Boxes ##############################################################
def bezier_real_minmax(p):
"""returns the minimum and maximum for any real cubic bezier"""
local_extremizers = [0, 1]
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:
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)
tau = a[0] - 2*a[1] + a[2]
r1 = (tau + sqdelta)/denom
r2 = (tau - sqdelta)/denom
if 0 < r1 < 1:
local_extremizers.append(r1)
if 0 < r2 < 1:
local_extremizers.append(r2)
local_extrema = [bezier_point(a, t) for t in local_extremizers]
return min(local_extrema), max(local_extrema)
# find reverse standard coefficients of the derivative
dcoeffs = bezier2polynomial(a, return_poly1d=True).deriv().coeffs
# find real roots, r, such that 0 <= r <= 1
local_extremizers += polyroots01(dcoeffs)
local_extrema = [bezier_point(a, t) for t in local_extremizers]
return min(local_extrema), max(local_extrema)
def bezier_bounding_box(bez):
"""returns the bounding box for the segment in the form
(xmin, xmax, ymin, ymax).
Warning: For the non-cubic case this is not particularly efficient."""
# begin arc support block ########################
try:
bla = bez.large_arc
return bez.bbox() # added to support Arc objects
except:
pass
# end arc support block ##########################
if len(bez) == 4:
xmin, xmax = bezier_real_minmax([p.real for p in bez])
ymin, ymax = bezier_real_minmax([p.imag for p in bez])
return xmin, xmax, ymin, ymax
poly = bezier2polynomial(bez, return_poly1d=True)
x = real(poly)
y = imag(poly)
dx = x.deriv()
dy = y.deriv()
x_extremizers = [0, 1] + polyroots(dx, realroots=True,
condition=lambda r: 0 < r < 1)
y_extremizers = [0, 1] + polyroots(dy, realroots=True,
condition=lambda r: 0 < r < 1)
x_extrema = [x(t) for t in x_extremizers]
y_extrema = [y(t) for t in y_extremizers]
return min(x_extrema), max(x_extrema), min(y_extrema), max(y_extrema)
def box_area(xmin, xmax, ymin, ymax):
"""
INPUT: 2-tuple of cubics (given by control points)
OUTPUT: boolean
"""
return (xmax - xmin)*(ymax - ymin)
def interval_intersection_width(a, b, c, d):
"""returns the width of the intersection of intervals [a,b] and [c,d]
(thinking of these as intervals on the real number line)"""
return max(0, min(b, d) - max(a, c))
def boxes_intersect(box1, box2):
"""Determines if two rectangles, each input as a tuple
(xmin, xmax, ymin, ymax), intersect."""
xmin1, xmax1, ymin1, ymax1 = box1
xmin2, xmax2, ymin2, ymax2 = box2
if interval_intersection_width(xmin1, xmax1, xmin2, xmax2) and \
interval_intersection_width(ymin1, ymax1, ymin2, ymax2):
return True
else:
return False
# Intersections ###############################################################
class ApproxSolutionSet(list):
"""A class that behaves like a set but treats two elements , x and y, as
equivalent if abs(x-y) < self.tol"""
def __init__(self, tol):
self.tol = tol
def __contains__(self, x):
for y in self:
if abs(x - y) < self.tol:
return True
return False
def appadd(self, pt):
if pt not in self:
self.append(pt)
class BPair(object):
def __init__(self, bez1, bez2, t1, t2):
self.bez1 = bez1
self.bez2 = bez2
self.t1 = t1 # t value to get the mid point of this curve from cub1
self.t2 = t2 # t value to get the mid point of this curve from cub2
def bezier_intersections(bez1, bez2, longer_length, tol=1e-8, tol_deC=1e-8):
"""INPUT:
bez1, bez2 = [P0,P1,P2,...PN], [Q0,Q1,Q2,...,PN] defining the two
Bezier curves to check for intersections between.
longer_length - the length (or an upper bound) on the longer of the two
Bezier curves. Determines the maximum iterations needed together with tol.
tol - is the smallest distance that two solutions can differ by and still
be considered distinct solutions.
OUTPUT: a list of tuples (t,s) in [0,1]x[0,1] such that
abs(bezier_point(bez1[0],t) - bezier_point(bez2[1],s)) < tol_deC
Note: This will return exactly one such tuple for each intersection
(assuming tol_deC is small enough)."""
maxits = int(ceil(1-log(tol_deC/longer_length)/log(2)))
pair_list = [BPair(bez1, bez2, 0.5, 0.5)]
intersection_list = []
k = 0
approx_point_set = ApproxSolutionSet(tol)
while pair_list and k < maxits:
new_pairs = []
delta = 0.5**(k + 2)
for pair in pair_list:
bbox1 = bezier_bounding_box(pair.bez1)
bbox2 = bezier_bounding_box(pair.bez2)
if boxes_intersect(bbox1, bbox2):
if box_area(*bbox1) < tol_deC and box_area(*bbox2) < tol_deC:
point = bezier_point(bez1, pair.t1)
if point not in approx_point_set:
approx_point_set.append(point)
# this is the point in the middle of the pair
intersection_list.append((pair.t1, pair.t2))
# this prevents the output of redundant intersection points
for otherPair in pair_list:
if pair.bez1 == otherPair.bez1 or \
pair.bez2 == otherPair.bez2 or \
pair.bez1 == otherPair.bez2 or \
pair.bez2 == otherPair.bez1:
pair_list.remove(otherPair)
else:
(c11, c12) = halve_bezier(pair.bez1)
(t11, t12) = (pair.t1 - delta, pair.t1 + delta)
(c21, c22) = halve_bezier(pair.bez2)
(t21, t22) = (pair.t2 - delta, pair.t2 + delta)
new_pairs += [BPair(c11, c21, t11, t21),
BPair(c11, c22, t11, t22),
BPair(c12, c21, t12, t21),
BPair(c12, c22, t12, t22)]
pair_list = new_pairs
k += 1
if k >= maxits:
raise Exception("bezier_intersections has reached maximum "
"iterations without terminating... "
"either there's a problem/bug or you can fix by "
"raising the max iterations or lowering tol_deC")
return intersection_list
def bezier_by_line_intersections(bezier, line):
"""Returns tuples (t1,t2) such that bezier.point(t1) ~= line.point(t2)."""
# The method here is to translate (shift) then rotate the complex plane so
# that line starts at the origin and proceeds along the positive real axis.
# After this transformation, the intersection points are the real roots of
# the imaginary component of the bezier for which the real component is
# between 0 and abs(line[1]-line[0])].
assert len(line[:]) == 2
assert line[0] != line[1]
if not any(p != bezier[0] for p in bezier):
raise ValueError("bezier is nodal, use "
"bezier_by_line_intersection(bezier[0], line) "
"instead for a bool to be returned.")
# First let's shift the complex plane so that line starts at the origin
shifted_bezier = [z - line[0] for z in bezier]
shifted_line_end = line[1] - line[0]
line_length = abs(shifted_line_end)
# Now let's rotate the complex plane so that line falls on the x-axis
rotation_matrix = line_length/shifted_line_end
transformed_bezier = [rotation_matrix*z for z in shifted_bezier]
# Now all intersections should be roots of the imaginary component of
# the transformed bezier
transformed_bezier_imag = [p.imag for p in transformed_bezier]
coeffs_y = bezier2polynomial(transformed_bezier_imag)
roots_y = list(polyroots01(coeffs_y)) # returns real roots 0 <= r <= 1
transformed_bezier_real = [p.real for p in transformed_bezier]
intersection_list = []
for bez_t in set(roots_y):
xval = bezier_point(transformed_bezier_real, bez_t)
if 0 <= xval <= line_length:
line_t = xval/line_length
intersection_list.append((bez_t, line_t))
return intersection_list

View File

@ -1,21 +0,0 @@
def directional_field(curve, tvals=np.linspace(0, 1, N), asize=1e-2,
colored=False):
size = asize * curve.length()
arrows = []
tvals = np.linspace(0, 1, N)
for t in tvals:
pt = curve.point(t)
ut = curve.unit_tangent(t)
un = curve.normal(t)
l1 = Line(pt, pt + size*(un - ut)/2).reversed()
l2 = Line(pt, pt + size*(-un - ut)/2)
if colored:
arrows.append(Path(l1, l2))
else:
arrows += [l1, l2]
if colored:
colors = [(int(255*t), 0, 0) for t in tvals]
return arrows, tvals, colors
else:
return Path(arrows)

View File

@ -1,64 +0,0 @@
"""This submodule contains miscellaneous tools that are used internally, but
aren't specific to SVGs or related mathematical objects."""
# External dependencies:
from __future__ import division, absolute_import, print_function
import os
import sys
import webbrowser
# stackoverflow.com/questions/214359/converting-hex-color-to-rgb-and-vice-versa
def hex2rgb(value):
"""Converts a hexadeximal color string to an RGB 3-tuple
EXAMPLE
-------
>>> hex2rgb('#0000FF')
(0, 0, 255)
"""
value = value.lstrip('#')
lv = len(value)
return tuple(int(value[i:i+lv//3], 16) for i in range(0, lv, lv//3))
# stackoverflow.com/questions/214359/converting-hex-color-to-rgb-and-vice-versa
def rgb2hex(rgb):
"""Converts an RGB 3-tuple to a hexadeximal color string.
EXAMPLE
-------
>>> rgb2hex((0,0,255))
'#0000FF'
"""
return ('#%02x%02x%02x' % rgb).upper()
def isclose(a, b, rtol=1e-5, atol=1e-8):
"""This is essentially np.isclose, but slightly faster."""
return abs(a - b) < (atol + rtol * abs(b))
def open_in_browser(file_location):
"""Attempt to open file located at file_location in the default web
browser."""
# If just the name of the file was given, check if it's in the Current
# Working Directory.
if not os.path.isfile(file_location):
file_location = os.path.join(os.getcwd(), file_location)
if not os.path.isfile(file_location):
raise IOError("\n\nFile not found.")
# For some reason OSX requires this adjustment (tested on 10.10.4)
if sys.platform == "darwin":
file_location = "file:///"+file_location
new = 2 # open in a new tab, if possible
webbrowser.get().open(file_location, new=new)
BugException = Exception("This code should never be reached. You've found a "
"bug. Please submit an issue to \n"
"https://github.com/mathandy/svgpathtools/issues"
"\nwith an easily reproducible example.")

View File

@ -1,196 +0,0 @@
"""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)."""
# External dependencies
from __future__ import division, absolute_import, print_function
import re
# Internal dependencies
from .path import Path, Line, QuadraticBezier, CubicBezier, Arc
COMMANDS = set('MmZzLlHhVvCcSsQqTtAa')
UPPERCASE = set('MZLHVCSQTA')
COMMAND_RE = re.compile("([MmZzLlHhVvCcSsQqTtAa])")
FLOAT_RE = re.compile("[-+]?[0-9]*\.?[0-9]+(?:[eE][-+]?[0-9]+)?")
def _tokenize_path(pathdef):
for x in COMMAND_RE.split(pathdef):
if x in COMMANDS:
yield x
for token in FLOAT_RE.findall(x):
yield token
def parse_path(pathdef, current_pos=0j):
# In the SVG specs, initial movetos are absolute, even if
# specified as 'm'. This is the default behavior here as well.
# But if you pass in a current_pos variable, the initial moveto
# will be relative to that current_pos. This is useful.
elements = list(_tokenize_path(pathdef))
# Reverse for easy use of .pop()
elements.reverse()
segments = Path()
start_pos = None
command = None
while elements:
if elements[-1] in COMMANDS:
# New command.
last_command = command # Used by S and T
command = elements.pop()
absolute = command in UPPERCASE
command = command.upper()
else:
# If this element starts with numbers, it is an implicit command
# and we don't change the command. Check that it's allowed:
if command is None:
raise ValueError("Unallowed implicit command in %s, position %s" % (
pathdef, len(pathdef.split()) - len(elements)))
if command == 'M':
# Moveto command.
x = elements.pop()
y = elements.pop()
pos = float(x) + float(y) * 1j
if absolute:
current_pos = pos
else:
current_pos += pos
# when M is called, reset start_pos
# This behavior of Z is defined in svg spec:
# http://www.w3.org/TR/SVG/paths.html#PathDataClosePathCommand
start_pos = current_pos
# Implicit moveto commands are treated as lineto commands.
# So we set command to lineto here, in case there are
# further implicit commands after this moveto.
command = 'L'
elif command == 'Z':
# Close path
if not (current_pos == start_pos):
segments.append(Line(current_pos, start_pos))
segments.closed = True
current_pos = start_pos
start_pos = None
command = None # You can't have implicit commands after closing.
elif command == 'L':
x = elements.pop()
y = elements.pop()
pos = float(x) + float(y) * 1j
if not absolute:
pos += current_pos
segments.append(Line(current_pos, pos))
current_pos = pos
elif command == 'H':
x = elements.pop()
pos = float(x) + current_pos.imag * 1j
if not absolute:
pos += current_pos.real
segments.append(Line(current_pos, pos))
current_pos = pos
elif command == 'V':
y = elements.pop()
pos = current_pos.real + float(y) * 1j
if not absolute:
pos += current_pos.imag * 1j
segments.append(Line(current_pos, pos))
current_pos = pos
elif command == 'C':
control1 = float(elements.pop()) + float(elements.pop()) * 1j
control2 = float(elements.pop()) + float(elements.pop()) * 1j
end = float(elements.pop()) + float(elements.pop()) * 1j
if not absolute:
control1 += current_pos
control2 += current_pos
end += current_pos
segments.append(CubicBezier(current_pos, control1, control2, end))
current_pos = end
elif command == 'S':
# Smooth curve. First control point is the "reflection" of
# the second control point in the previous path.
if last_command not in 'CS':
# 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.
control1 = current_pos
else:
# The first control point is assumed to be the reflection of
# the second control point on the previous command relative
# to the current point.
control1 = current_pos + current_pos - segments[-1].control2
control2 = float(elements.pop()) + float(elements.pop()) * 1j
end = float(elements.pop()) + float(elements.pop()) * 1j
if not absolute:
control2 += current_pos
end += current_pos
segments.append(CubicBezier(current_pos, control1, control2, end))
current_pos = end
elif command == 'Q':
control = float(elements.pop()) + float(elements.pop()) * 1j
end = float(elements.pop()) + float(elements.pop()) * 1j
if not absolute:
control += current_pos
end += current_pos
segments.append(QuadraticBezier(current_pos, control, end))
current_pos = end
elif command == 'T':
# Smooth curve. Control point is the "reflection" of
# the second control point in the previous path.
if last_command not in 'QT':
# 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.
control = current_pos
else:
# The control point is assumed to be the reflection of
# the control point on the previous command relative
# to the current point.
control = current_pos + current_pos - segments[-1].control
end = float(elements.pop()) + float(elements.pop()) * 1j
if not absolute:
end += current_pos
segments.append(QuadraticBezier(current_pos, control, end))
current_pos = end
elif command == 'A':
radius = float(elements.pop()) + float(elements.pop()) * 1j
rotation = float(elements.pop())
arc = float(elements.pop())
sweep = float(elements.pop())
end = float(elements.pop()) + float(elements.pop()) * 1j
if not absolute:
end += current_pos
segments.append(Arc(current_pos, radius, rotation, arc, sweep, end))
current_pos = end
return segments

File diff suppressed because it is too large Load Diff

View File

@ -1,387 +0,0 @@
"""This submodule contains tools for creating svg files from paths and path
segments."""
# External dependencies:
from __future__ import division, absolute_import, print_function
from math import ceil
from os import getcwd, path as os_path, makedirs
from xml.dom.minidom import parse as md_xml_parse
from svgwrite import Drawing, text as txt
from time import time
from warnings import warn
# Internal dependencies
from .path import Path, Line, is_path_segment
from .misctools import open_in_browser
# Used to convert a string colors (identified by single chars) to a list.
color_dict = {'a': 'aqua',
'b': 'blue',
'c': 'cyan',
'd': 'darkblue',
'e': '',
'f': '',
'g': 'green',
'h': '',
'i': '',
'j': '',
'k': 'black',
'l': 'lime',
'm': 'magenta',
'n': 'brown',
'o': 'orange',
'p': 'pink',
'q': 'turquoise',
'r': 'red',
's': 'salmon',
't': 'tan',
'u': 'purple',
'v': 'violet',
'w': 'white',
'x': '',
'y': 'yellow',
'z': 'azure'}
def str2colorlist(s, default_color=None):
color_list = [color_dict[ch] for ch in s]
if default_color:
for idx, c in enumerate(color_list):
if not c:
color_list[idx] = default_color
return color_list
def is3tuple(c):
return isinstance(c, tuple) and len(c) == 3
def big_bounding_box(paths_n_stuff):
"""Finds a BB containing a collection of paths, Bezier path segments, and
points (given as complex numbers)."""
bbs = []
for thing in paths_n_stuff:
if is_path_segment(thing) or isinstance(thing, Path):
bbs.append(thing.bbox())
elif isinstance(thing, complex):
bbs.append((thing.real, thing.real, thing.imag, thing.imag))
else:
try:
complexthing = complex(thing)
bbs.append((complexthing.real, complexthing.real,
complexthing.imag, complexthing.imag))
except ValueError:
raise TypeError(
"paths_n_stuff can only contains Path, CubicBezier, "
"QuadraticBezier, Line, and complex objects.")
xmins, xmaxs, ymins, ymaxs = list(zip(*bbs))
xmin = min(xmins)
xmax = max(xmaxs)
ymin = min(ymins)
ymax = max(ymaxs)
return xmin, xmax, ymin, ymax
def disvg(paths=None, colors=None,
filename=os_path.join(getcwd(), 'disvg_output.svg'),
stroke_widths=None, nodes=None, node_colors=None, node_radii=None,
openinbrowser=True, timestamp=False,
margin_size=0.1, mindim=600, dimensions=None,
viewbox=None, text=None, text_path=None, font_size=None,
attributes=None, svg_attributes=None):
"""Takes in a list of paths and creates an SVG file containing said paths.
REQUIRED INPUTS:
:param paths - a list of paths
OPTIONAL INPUT:
:param colors - specifies the path stroke color. By default all paths
will be black (#000000). This paramater can be input in a few ways
1) a list of strings that will be input into the path elements stroke
attribute (so anything that is understood by the svg viewer).
2) a string of single character colors -- e.g. setting colors='rrr' is
equivalent to setting colors=['red', 'red', 'red'] (see the
'color_dict' dictionary above for a list of possibilities).
3) a list of rgb 3-tuples -- e.g. colors = [(255, 0, 0), ...].
:param filename - the desired location/filename of the SVG file
created (by default the SVG will be stored in the current working
directory and named 'disvg_output.svg').
:param stroke_widths - a list of stroke_widths to use for paths
(default is 0.5% of the SVG's width or length)
:param nodes - a list of points to draw as filled-in circles
:param node_colors - a list of colors to use for the nodes (by default
nodes will be red)
:param node_radii - a list of radii to use for the nodes (by default
nodes will be radius will be 1 percent of the svg's width/length)
:param text - string or list of strings to be displayed
:param text_path - if text is a list, then this should be a list of
path (or path segments of the same length. Note: the path must be
long enough to display the text or the text will be cropped by the svg
viewer.
:param font_size - a single float of list of floats.
:param openinbrowser - Set to True to automatically open the created
SVG in the user's default web browser.
:param timestamp - if True, then the a timestamp will be appended to
the output SVG's filename. This will fix issues with rapidly opening
multiple SVGs in your browser.
:param margin_size - The min margin (empty area framing the collection
of paths) size used for creating the canvas and background of the SVG.
:param mindim - The minimum dimension (height or width) of the output
SVG (default is 600).
:param dimensions - The display dimensions of the output SVG. Using
this will override the mindim parameter.
:param viewbox - This specifies what rectangular patch of R^2 will be
viewable through the outputSVG. It should be input in the form
(min_x, min_y, width, height). This is different from the display
dimension of the svg, which can be set through mindim or dimensions.
:param attributes - a list of dictionaries of attributes for the input
paths. Note: This will override any other conflicting settings.
:param svg_attributes - a dictionary of attributes for output svg.
Note 1: This will override any other conflicting settings.
Note 2: Setting `svg_attributes={'debug': False}` may result in a
significant increase in speed.
NOTES:
-The unit of length here is assumed to be pixels in all variables.
-If this function is used multiple times in quick succession to
display multiple SVGs (all using the default filename), the
svgviewer/browser will likely fail to load some of the SVGs in time.
To fix this, use the timestamp attribute, or give the files unique
names, or use a pause command (e.g. time.sleep(1)) between uses.
"""
_default_relative_node_radius = 5e-3
_default_relative_stroke_width = 1e-3
_default_path_color = '#000000' # black
_default_node_color = '#ff0000' # red
_default_font_size = 12
# append directory to filename (if not included)
if os_path.dirname(filename) == '':
filename = os_path.join(getcwd(), filename)
# append time stamp to filename
if timestamp:
fbname, fext = os_path.splitext(filename)
dirname = os_path.dirname(filename)
tstamp = str(time()).replace('.', '')
stfilename = os_path.split(fbname)[1] + '_' + tstamp + fext
filename = os_path.join(dirname, stfilename)
# check paths and colors are set
if isinstance(paths, Path) or is_path_segment(paths):
paths = [paths]
if paths:
if not colors:
colors = [_default_path_color] * len(paths)
else:
assert len(colors) == len(paths)
if isinstance(colors, str):
colors = str2colorlist(colors,
default_color=_default_path_color)
elif isinstance(colors, list):
for idx, c in enumerate(colors):
if is3tuple(c):
colors[idx] = "rgb" + str(c)
# check nodes and nodes_colors are set (node_radii are set later)
if nodes:
if not node_colors:
node_colors = [_default_node_color] * len(nodes)
else:
assert len(node_colors) == len(nodes)
if isinstance(node_colors, str):
node_colors = str2colorlist(node_colors,
default_color=_default_node_color)
elif isinstance(node_colors, list):
for idx, c in enumerate(node_colors):
if is3tuple(c):
node_colors[idx] = "rgb" + str(c)
# set up the viewBox and display dimensions of the output SVG
# along the way, set stroke_widths and node_radii if not provided
assert paths or nodes
stuff2bound = []
if viewbox:
szx, szy = viewbox[2:4]
else:
if paths:
stuff2bound += paths
if nodes:
stuff2bound += nodes
if text_path:
stuff2bound += text_path
xmin, xmax, ymin, ymax = big_bounding_box(stuff2bound)
dx = xmax - xmin
dy = ymax - ymin
if dx == 0:
dx = 1
if dy == 0:
dy = 1
# determine stroke_widths to use (if not provided) and max_stroke_width
if paths:
if not stroke_widths:
sw = max(dx, dy) * _default_relative_stroke_width
stroke_widths = [sw]*len(paths)
max_stroke_width = sw
else:
assert len(paths) == len(stroke_widths)
max_stroke_width = max(stroke_widths)
else:
max_stroke_width = 0
# determine node_radii to use (if not provided) and max_node_diameter
if nodes:
if not node_radii:
r = max(dx, dy) * _default_relative_node_radius
node_radii = [r]*len(nodes)
max_node_diameter = 2*r
else:
assert len(nodes) == len(node_radii)
max_node_diameter = 2*max(node_radii)
else:
max_node_diameter = 0
extra_space_for_style = max(max_stroke_width, max_node_diameter)
xmin -= margin_size*dx + extra_space_for_style/2
ymin -= margin_size*dy + extra_space_for_style/2
dx += 2*margin_size*dx + extra_space_for_style
dy += 2*margin_size*dy + extra_space_for_style
viewbox = "%s %s %s %s" % (xmin, ymin, dx, dy)
if dimensions:
szx, szy = dimensions
else:
if dx > dy:
szx = str(mindim) + 'px'
szy = str(int(ceil(mindim * dy / dx))) + 'px'
else:
szx = str(int(ceil(mindim * dx / dy))) + 'px'
szy = str(mindim) + 'px'
# Create an SVG file
if svg_attributes:
dwg = Drawing(filename=filename, **svg_attributes)
else:
dwg = Drawing(filename=filename, size=(szx, szy), viewBox=viewbox)
# add paths
if paths:
for i, p in enumerate(paths):
if isinstance(p, Path):
ps = p.d()
elif is_path_segment(p):
ps = Path(p).d()
else: # assume this path, p, was input as a Path d-string
ps = p
if attributes:
good_attribs = {'d': ps}
for key in attributes[i]:
val = attributes[i][key]
if key != 'd':
try:
dwg.path(ps, **{key: val})
good_attribs.update({key: val})
except Exception as e:
warn(str(e))
dwg.add(dwg.path(**good_attribs))
else:
dwg.add(dwg.path(ps, stroke=colors[i],
stroke_width=str(stroke_widths[i]),
fill='none'))
# add nodes (filled in circles)
if nodes:
for i_pt, pt in enumerate([(z.real, z.imag) for z in nodes]):
dwg.add(dwg.circle(pt, node_radii[i_pt], fill=node_colors[i_pt]))
# add texts
if text:
assert isinstance(text, str) or (isinstance(text, list) and
isinstance(text_path, list) and
len(text_path) == len(text))
if isinstance(text, str):
text = [text]
if not font_size:
font_size = [_default_font_size]
if not text_path:
pos = complex(xmin + margin_size*dx, ymin + margin_size*dy)
text_path = [Line(pos, pos + 1).d()]
else:
if font_size:
if isinstance(font_size, list):
assert len(font_size) == len(text)
else:
font_size = [font_size] * len(text)
else:
font_size = [_default_font_size] * len(text)
for idx, s in enumerate(text):
p = text_path[idx]
if isinstance(p, Path):
ps = p.d()
elif is_path_segment(p):
ps = Path(p).d()
else: # assume this path, p, was input as a Path d-string
ps = p
# paragraph = dwg.add(dwg.g(font_size=font_size[idx]))
# paragraph.add(dwg.textPath(ps, s))
pathid = 'tp' + str(idx)
dwg.defs.add(dwg.path(d=ps, id=pathid))
txter = dwg.add(dwg.text('', font_size=font_size[idx]))
txter.add(txt.TextPath('#'+pathid, s))
# save svg
if not os_path.exists(os_path.dirname(filename)):
makedirs(os_path.dirname(filename))
dwg.save()
# re-open the svg, make the xml pretty, and save it again
xmlstring = md_xml_parse(filename).toprettyxml()
with open(filename, 'w') as f:
f.write(xmlstring)
# try to open in web browser
if openinbrowser:
try:
open_in_browser(filename)
except:
print("Failed to open output SVG in browser. SVG saved to:")
print(filename)
def wsvg(paths=None, colors=None,
filename=os_path.join(getcwd(), 'disvg_output.svg'),
stroke_widths=None, nodes=None, node_colors=None, node_radii=None,
openinbrowser=False, timestamp=False,
margin_size=0.1, mindim=600, dimensions=None,
viewbox=None, text=None, text_path=None, font_size=None,
attributes=None, svg_attributes=None):
"""Convenience function; identical to disvg() except that
openinbrowser=False by default. See disvg() docstring for more info."""
disvg(paths, colors=colors, filename=filename,
stroke_widths=stroke_widths, nodes=nodes,
node_colors=node_colors, node_radii=node_radii,
openinbrowser=openinbrowser, timestamp=timestamp,
margin_size=margin_size, mindim=mindim, dimensions=dimensions,
viewbox=viewbox, text=text, text_path=text_path, font_size=font_size,
attributes=attributes, svg_attributes=svg_attributes)

View File

@ -1,14 +0,0 @@
"""This submodule contains additional tools for working with paths composed of
Line and CubicBezier objects. QuadraticBezier and Arc objects are only
partially supported."""
# External dependencies:
from __future__ import division, absolute_import, print_function
# Internal dependencies
from .path import Path, Line, QuadraticBezier, CubicBezier, Arc
# Misc#########################################################################

View File

@ -1,80 +0,0 @@
"""This submodule contains tools for working with numpy.poly1d objects."""
# External Dependencies
from __future__ import division, absolute_import
from itertools import combinations
import numpy as np
# Internal Dependencies
from .misctools import isclose
def polyroots(p, realroots=False, condition=lambda r: True):
"""
Returns the roots of a polynomial with coefficients given in p.
p[0] * x**n + p[1] * x**(n-1) + ... + p[n-1]*x + p[n]
INPUT:
p - Rank-1 array-like object of polynomial coefficients.
realroots - a boolean. If true, only real roots will be returned and the
condition function can be written assuming all roots are real.
condition - a boolean-valued function. Only roots satisfying this will be
returned. If realroots==True, these conditions should assume the roots
are real.
OUTPUT:
A list containing the roots of the polynomial.
NOTE: This uses np.isclose and np.roots"""
roots = np.roots(p)
if realroots:
roots = [r.real for r in roots if isclose(r.imag, 0)]
roots = [r for r in roots if condition(r)]
duplicates = []
for idx, (r1, r2) in enumerate(combinations(roots, 2)):
if isclose(r1, r2):
duplicates.append(idx)
return [r for idx, r in enumerate(roots) if idx not in duplicates]
def polyroots01(p):
"""Returns the real roots between 0 and 1 of the polynomial with
coefficients given in p,
p[0] * x**n + p[1] * x**(n-1) + ... + p[n-1]*x + p[n]
p can also be a np.poly1d object. See polyroots for more information."""
return polyroots(p, realroots=True, condition=lambda tval: 0 <= tval <= 1)
def rational_limit(f, g, t0):
"""Computes the limit of the rational function (f/g)(t)
as t approaches t0."""
assert isinstance(f, np.poly1d) and isinstance(g, np.poly1d)
assert g != np.poly1d([0])
if g(t0) != 0:
return f(t0)/g(t0)
elif f(t0) == 0:
return rational_limit(f.deriv(), g.deriv(), t0)
else:
raise ValueError("Limit does not exist.")
def real(z):
try:
return np.poly1d(z.coeffs.real)
except AttributeError:
return z.real
def imag(z):
try:
return np.poly1d(z.coeffs.imag)
except AttributeError:
return z.imag
def poly_real_part(poly):
"""Deprecated."""
return np.poly1d(poly.coeffs.real)
def poly_imag_part(poly):
"""Deprecated."""
return np.poly1d(poly.coeffs.imag)

View File

@ -1,201 +0,0 @@
"""This submodule contains functions related to smoothing paths of Bezier
curves."""
# External Dependencies
from __future__ import division, absolute_import, print_function
# Internal Dependencies
from .path import Path, CubicBezier, Line
from .misctools import isclose
from .paths2svg import disvg
def is_differentiable(path, tol=1e-8):
for idx in range(len(path)):
u = path[(idx-1) % len(path)].unit_tangent(1)
v = path[idx].unit_tangent(0)
u_dot_v = u.real*v.real + u.imag*v.imag
if abs(u_dot_v - 1) > tol:
return False
return True
def kinks(path, tol=1e-8):
"""returns indices of segments that start on a non-differentiable joint."""
kink_list = []
for idx in range(len(path)):
if idx == 0 and not path.isclosed():
continue
try:
u = path[(idx - 1) % len(path)].unit_tangent(1)
v = path[idx].unit_tangent(0)
u_dot_v = u.real*v.real + u.imag*v.imag
flag = False
except ValueError:
flag = True
if flag or abs(u_dot_v - 1) > tol:
kink_list.append(idx)
return kink_list
def _report_unfixable_kinks(_path, _kink_list):
mes = ("\n%s kinks have been detected at that cannot be smoothed.\n"
"To ignore these kinks and fix all others, run this function "
"again with the second argument 'ignore_unfixable_kinks=True' "
"The locations of the unfixable kinks are at the beginnings of "
"segments: %s" % (len(_kink_list), _kink_list))
disvg(_path, nodes=[_path[idx].start for idx in _kink_list])
raise Exception(mes)
def smoothed_joint(seg0, seg1, maxjointsize=3, tightness=1.99):
""" See Andy's notes on
Smoothing Bezier Paths for an explanation of the method.
Input: two segments seg0, seg1 such that seg0.end==seg1.start, and
jointsize, a positive number
Output: seg0_trimmed, elbow, seg1_trimmed, where elbow is a cubic bezier
object that smoothly connects seg0_trimmed and seg1_trimmed.
"""
assert seg0.end == seg1.start
assert 0 < maxjointsize
assert 0 < tightness < 2
# sgn = lambda x:x/abs(x)
q = seg0.end
try: v = seg0.unit_tangent(1)
except: v = seg0.unit_tangent(1 - 1e-4)
try: w = seg1.unit_tangent(0)
except: w = seg1.unit_tangent(1e-4)
max_a = maxjointsize / 2
a = min(max_a, min(seg1.length(), seg0.length()) / 20)
if isinstance(seg0, Line) and isinstance(seg1, Line):
'''
Note: Letting
c(t) = elbow.point(t), v= the unit tangent of seg0 at 1, w = the
unit tangent vector of seg1 at 0,
Q = seg0.point(1) = seg1.point(0), and a,b>0 some constants.
The elbow will be the unique CubicBezier, c, such that
c(0)= Q-av, c(1)=Q+aw, c'(0) = bv, and c'(1) = bw
where a and b are derived above/below from tightness and
maxjointsize.
'''
# det = v.imag*w.real-v.real*w.imag
# Note:
# If det is negative, the curvature of elbow is negative for all
# real t if and only if b/a > 6
# If det is positive, the curvature of elbow is negative for all
# real t if and only if b/a < 2
# if det < 0:
# b = (6+tightness)*a
# elif det > 0:
# b = (2-tightness)*a
# else:
# raise Exception("seg0 and seg1 are parallel lines.")
b = (2 - tightness)*a
elbow = CubicBezier(q - a*v, q - (a - b/3)*v, q + (a - b/3)*w, q + a*w)
seg0_trimmed = Line(seg0.start, elbow.start)
seg1_trimmed = Line(elbow.end, seg1.end)
return seg0_trimmed, [elbow], seg1_trimmed
elif isinstance(seg0, Line):
'''
Note: Letting
c(t) = elbow.point(t), v= the unit tangent of seg0 at 1,
w = the unit tangent vector of seg1 at 0,
Q = seg0.point(1) = seg1.point(0), and a,b>0 some constants.
The elbow will be the unique CubicBezier, c, such that
c(0)= Q-av, c(1)=Q, c'(0) = bv, and c'(1) = bw
where a and b are derived above/below from tightness and
maxjointsize.
'''
# det = v.imag*w.real-v.real*w.imag
# Note: If g has the same sign as det, then the curvature of elbow is
# negative for all real t if and only if b/a < 4
b = (4 - tightness)*a
# g = sgn(det)*b
elbow = CubicBezier(q - a*v, q + (b/3 - a)*v, q - b/3*w, q)
seg0_trimmed = Line(seg0.start, elbow.start)
return seg0_trimmed, [elbow], seg1
elif isinstance(seg1, Line):
args = (seg1.reversed(), seg0.reversed(), maxjointsize, tightness)
rseg1_trimmed, relbow, rseg0 = smoothed_joint(*args)
elbow = relbow[0].reversed()
return seg0, [elbow], rseg1_trimmed.reversed()
else:
# find a point on each seg that is about a/2 away from joint. Make
# line between them.
t0 = seg0.ilength(seg0.length() - a/2)
t1 = seg1.ilength(a/2)
seg0_trimmed = seg0.cropped(0, t0)
seg1_trimmed = seg1.cropped(t1, 1)
seg0_line = Line(seg0_trimmed.end, q)
seg1_line = Line(q, seg1_trimmed.start)
args = (seg0_trimmed, seg0_line, maxjointsize, tightness)
dummy, elbow0, seg0_line_trimmed = smoothed_joint(*args)
args = (seg1_line, seg1_trimmed, maxjointsize, tightness)
seg1_line_trimmed, elbow1, dummy = smoothed_joint(*args)
args = (seg0_line_trimmed, seg1_line_trimmed, maxjointsize, tightness)
seg0_line_trimmed, elbowq, seg1_line_trimmed = smoothed_joint(*args)
elbow = elbow0 + [seg0_line_trimmed] + elbowq + [seg1_line_trimmed] + elbow1
return seg0_trimmed, elbow, seg1_trimmed
def smoothed_path(path, maxjointsize=3, tightness=1.99, ignore_unfixable_kinks=False):
"""returns a path with no non-differentiable joints."""
if len(path) == 1:
return path
assert path.iscontinuous()
sharp_kinks = []
new_path = [path[0]]
for idx in range(len(path)):
if idx == len(path)-1:
if not path.isclosed():
continue
else:
seg1 = new_path[0]
else:
seg1 = path[idx + 1]
seg0 = new_path[-1]
try:
unit_tangent0 = seg0.unit_tangent(1)
unit_tangent1 = seg1.unit_tangent(0)
flag = False
except ValueError:
flag = True # unit tangent not well-defined
if not flag and isclose(unit_tangent0, unit_tangent1): # joint is already smooth
if idx != len(path)-1:
new_path.append(seg1)
continue
else:
kink_idx = (idx + 1) % len(path) # kink at start of this seg
if not flag and isclose(-unit_tangent0, unit_tangent1):
# joint is sharp 180 deg (must be fixed manually)
new_path.append(seg1)
sharp_kinks.append(kink_idx)
else: # joint is not smooth, let's smooth it.
args = (seg0, seg1, maxjointsize, tightness)
new_seg0, elbow_segs, new_seg1 = smoothed_joint(*args)
new_path[-1] = new_seg0
new_path += elbow_segs
if idx == len(path) - 1:
new_path[0] = new_seg1
else:
new_path.append(new_seg1)
# If unfixable kinks were found, let the user know
if sharp_kinks and not ignore_unfixable_kinks:
_report_unfixable_kinks(path, sharp_kinks)
return Path(*new_path)

View File

@ -1,126 +0,0 @@
"""This submodule contains tools for creating path objects from SVG files.
The main tool being the svg2paths() function."""
# External dependencies
from __future__ import division, absolute_import, print_function
from xml.dom.minidom import parse
from os import path as os_path, getcwd
from shutil import copyfile
# Internal dependencies
from .parser import parse_path
def polyline2pathd(polyline_d):
"""converts the string from a polyline d-attribute to a string for a Path
object d-attribute"""
points = polyline_d.replace(', ', ',')
points = points.replace(' ,', ',')
points = points.split()
if points[0] == points[-1]:
closed = True
else:
closed = False
d = 'M' + points.pop(0).replace(',', ' ')
for p in points:
d += 'L' + p.replace(',', ' ')
if closed:
d += 'z'
return d
def svg2paths(svg_file_location,
convert_lines_to_paths=True,
convert_polylines_to_paths=True,
convert_polygons_to_paths=True,
return_svg_attributes=False):
"""
Converts an SVG file into a list of Path objects and a list of
dictionaries containing their attributes. This currently supports
SVG Path, Line, Polyline, and Polygon elements.
:param svg_file_location: the location of the svg file
:param convert_lines_to_paths: Set to False to disclude SVG-Line objects
(converted to Paths)
:param convert_polylines_to_paths: Set to False to disclude SVG-Polyline
objects (converted to Paths)
:param convert_polygons_to_paths: Set to False to disclude SVG-Polygon
objects (converted to Paths)
:param return_svg_attributes: Set to True and a dictionary of
svg-attributes will be extracted and returned
:return: list of Path objects, list of path attribute dictionaries, and
(optionally) a dictionary of svg-attributes
"""
if os_path.dirname(svg_file_location) == '':
svg_file_location = os_path.join(getcwd(), svg_file_location)
# if pathless_svg:
# copyfile(svg_file_location, pathless_svg)
# doc = parse(pathless_svg)
# else:
doc = parse(svg_file_location)
def dom2dict(element):
"""Converts DOM elements to dictionaries of attributes."""
keys = list(element.attributes.keys())
values = [val.value for val in list(element.attributes.values())]
return dict(list(zip(keys, values)))
# Use minidom to extract path strings from input SVG
paths = [dom2dict(el) for el in doc.getElementsByTagName('path')]
d_strings = [el['d'] for el in paths]
attribute_dictionary_list = paths
# if pathless_svg:
# for el in doc.getElementsByTagName('path'):
# el.parentNode.removeChild(el)
# Use minidom to extract polyline strings from input SVG, convert to
# path strings, add to list
if convert_polylines_to_paths:
plins = [dom2dict(el) for el in doc.getElementsByTagName('polyline')]
d_strings += [polyline2pathd(pl['points']) for pl in plins]
attribute_dictionary_list += plins
# Use minidom to extract polygon strings from input SVG, convert to
# path strings, add to list
if convert_polygons_to_paths:
pgons = [dom2dict(el) for el in doc.getElementsByTagName('polygon')]
d_strings += [polyline2pathd(pg['points']) + 'z' for pg in pgons]
attribute_dictionary_list += pgons
if convert_lines_to_paths:
lines = [dom2dict(el) for el in doc.getElementsByTagName('line')]
d_strings += [('M' + l['x1'] + ' ' + l['y1'] +
'L' + l['x2'] + ' ' + l['y2']) for l in lines]
attribute_dictionary_list += lines
# if pathless_svg:
# with open(pathless_svg, "wb") as f:
# doc.writexml(f)
if return_svg_attributes:
svg_attributes = dom2dict(doc.getElementsByTagName('svg')[0])
doc.unlink()
path_list = [parse_path(d) for d in d_strings]
return path_list, attribute_dictionary_list, svg_attributes
else:
doc.unlink()
path_list = [parse_path(d) for d in d_strings]
return path_list, attribute_dictionary_list
def svg2paths2(svg_file_location,
convert_lines_to_paths=True,
convert_polylines_to_paths=True,
convert_polygons_to_paths=True,
return_svg_attributes=True):
"""Convenience function; identical to svg2paths() except that
return_svg_attributes=True by default. See svg2paths() docstring for more
info."""
return svg2paths(svg_file_location=svg_file_location,
convert_lines_to_paths=convert_lines_to_paths,
convert_polylines_to_paths=convert_polylines_to_paths,
convert_polygons_to_paths=convert_polygons_to_paths,
return_svg_attributes=return_svg_attributes)

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -1,8 +0,0 @@
<?xml version="1.0" ?>
<svg baseProfile="full" height="600px" version="1.1" viewBox="161.5 79.0 152.0 242.0" width="377px" xmlns="http://www.w3.org/2000/svg" xmlns:ev="http://www.w3.org/2001/xml-events" xmlns:xlink="http://www.w3.org/1999/xlink">
<defs/>
<path d="M 300.0,100.0 C 100.0,100.0 200.0,200.0 200.0,300.0" fill="none" stroke="blue" stroke-width="0.2"/>
<path d="M 175.0,162.5 L 175.0,236.805280985" fill="none" stroke="green" stroke-width="0.2"/>
<path d="M 175.0,162.5 L 249.305280985,162.5" fill="none" stroke="pink" stroke-width="0.2"/>
<circle cx="175.0" cy="162.5" fill="#ff0000" r="1.0"/>
</svg>

Before

Width:  |  Height:  |  Size: 618 B

16
donate-button.svg Normal file
View File

@ -0,0 +1,16 @@
<?xml version="1.0" ?>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:ev="http://www.w3.org/2001/xml-events" xmlns:xlink="http://www.w3.org/1999/xlink" baseProfile="full" height="50px" version="1.1" viewBox="-15.075 -5.075 180.15 60.15" width="150px">
<defs>
<path d="M 10.0,24.0 L 140.0,24.0" id="tp0"/>
<path d="M 10.0,40.0 L 140.0,40.0" id="tp1"/>
</defs>
<a href="https://www.paypal.com/donate?business=4SKJ27AM4EYYA&amp;no_recurring=0&amp;item_name=Support+the+creator+of+svgpathtools?++He%27s+a+student+and+would+appreciate+it.&amp;currency_code=USD">
<path d="M 0.0,25.0 C 0.0,0.0 0.0,0.0 75.0,0.0 C 150.0,0.0 150.0,0.0 150.0,25.0 C 150.0,50.0 150.0,50.0 75.0,50.0 C 0.0,50.0 0.0,50.0 0.0,25.0" fill="#34eb86" stroke="#000000" stroke-width="0.15"/>
<text font-size="15" font-weight="bold">
<textPath startOffset="50%" text-anchor="middle" xlink:href="#tp0">Donate to the creator</textPath>
</text>
<text font-size="16">
<textPath startOffset="50%" text-anchor="middle" xlink:href="#tp1">(He's a student.)</textPath>
</text>
</a>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -6,9 +6,9 @@ easily be generalized to paths containing `Line` and `QuadraticBezier` objects
also.
Note: The relevant matrix transformation for quadratics can be found in the
svgpathtools.bezier module."""
from __future__ import print_function
import numpy as np
from svgpathtools import *
from svgpathtools import bezier_point, Path, bpoints2bezier, polynomial2bezier
class HigherOrderBezier:
@ -38,10 +38,10 @@ def points_in_each_seg_slow(path, tvals):
def points_in_each_seg(path, tvals):
"""Compute seg.point(t) for each seg in path and each t in tvals."""
A = np.matrix([[-1, 3, -3, 1], # transforms cubic bez to standard poly
[ 3, -6, 3, 0],
[-3, 3, 0, 0],
[ 1, 0, 0, 0]])
A = np.array([[-1, 3, -3, 1], # transforms cubic bez to standard poly
[ 3, -6, 3, 0],
[-3, 3, 0, 0],
[ 1, 0, 0, 0]])
B = [seg.bpoints() for seg in path]
return np.dot(B, np.dot(A, np.power(tvals, [[3],[2],[1],[0]])))
@ -53,4 +53,4 @@ if __name__ == '__main__':
pts = points_in_each_seg(testpath, tvals)
pts_check = points_in_each_seg_slow(testpath, tvals)
print np.max(pts - pts_check)
print(np.max(pts - pts_check))

View File

@ -7,7 +7,8 @@ Path.continuous_subpaths() method to split a paths into a list of its
continuous subpaths.
"""
from svgpathtools import *
from svgpathtools import Path, Line
def path1_is_contained_in_path2(path1, path2):
assert path2.isclosed() # This question isn't well-defined otherwise
@ -16,11 +17,11 @@ def path1_is_contained_in_path2(path1, path2):
# find a point that's definitely outside path2
xmin, xmax, ymin, ymax = path2.bbox()
B = (xmin + 1) + 1j*(ymax + 1)
b = (xmin + 1) + 1j*(ymax + 1)
A = path1.start # pick an arbitrary point in path1
AB_line = Path(Line(A, B))
number_of_intersections = len(AB_line.intersect(path2))
a = path1.start # pick an arbitrary point in path1
ab_line = Path(Line(a, b))
number_of_intersections = len(ab_line.intersect(path2))
if number_of_intersections % 2: # if number of intersections is odd
return True
else:

View File

@ -1,13 +1,16 @@
from svgpathtools import *
from svgpathtools import disvg, Line, CubicBezier
from scipy.optimize import fminbound
# create some example paths
path1 = CubicBezier(1,2+3j,3-5j,4+1j)
path2 = path1.rotated(60).translated(3)
# find minimizer
from scipy.optimize import fminbound
def dist(t):
return path1.radialrange(path2.point(t))[0][0]
# find minimizer
T2 = fminbound(dist, 0, 1)
# Let's do a visual check

View File

@ -0,0 +1,62 @@
<!DOCTYPE html>
<html lang="en">
<head>
<script src="https://cdn.jsdelivr.net/pyodide/v0.18.1/full/pyodide.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
<meta charset="utf-8" />
<title>svgpathtools in JS!</title>
</head>
<body>
<button id="go_button" onclick="tick()" hidden>Click Me!</button>
<br />
<br />
<div>Output:</div>
<label for="output"></label>
<textarea id="output" style="width: 100%;" rows="6" disabled></textarea>
<svg height="100" width="100">
<circle cx="50" cy="50" r="40" stroke-width="2" stroke="black" fill="blue"/>
<path id="ticker" d="M 50 50 L 50 15" stroke-width="2" stroke="black"/>
<circle cx="50" cy="50" r="3" stroke-width="2" stroke="black" fill="green"/>
Sorry, your browser does not support inline SVG.
</svg>
<script>
// init Pyodide environment and install svgpathtools
async function main() {
let pyodide = await loadPyodide({
indexURL: "https://cdn.jsdelivr.net/pyodide/v0.18.1/full/",
});
await pyodide.loadPackage("micropip");
pyodide.runPythonAsync(`
import micropip
await micropip.install('svgpathtools')
`);
output.value += "svgpathtools is ready!\n";
return pyodide;
}
async function tick() {
let clock_hand = document.getElementById("ticker");
let pyodide = await pyodideReadyPromise;
try {
let result = pyodide.runPython(`
from svgpathtools import parse_path
parse_path('${clock_hand.getAttribute('d')}').rotated(45, origin=50+50j).d()
`);
clock_hand.setAttribute('d', result);
} catch (err) {
output.value += err;
}
}
let pyodideReadyPromise = main();
$(document).ready(function(){
const output = document.getElementById("output");
output.value = "Initializing...\n";
document.getElementById("go_button").removeAttribute('hidden');
});
</script>
</body>
</html>

View File

@ -0,0 +1,22 @@
<svg version="1.1"
baseProfile="full"
width="1000" height="1000"
xmlns="http://www.w3.org/2000/svg">
<path d="M50,50 A 40 40 0 1 0 100 100Z" stroke="blue" stroke-width="4" fill="yellow"/>
<circle cx="50" cy="50" r="5" stroke="green" stroke-width="1" fill="none"/>
<circle cx="100" cy="100" r="5" stroke="red" stroke-width="1" fill="none"/>
<path d="M150,150 A 0 40 0 1 0 200 200Z" stroke="beige" stroke-width="4" fill="yellow"/>
<circle cx="150" cy="150" r="5" stroke="green" stroke-width="1" fill="none"/>
<circle cx="200" cy="200" r="5" stroke="red" stroke-width="1" fill="none"/>
<path d="M250,250 A 40 0 0 1 0 300 300Z" stroke="purple" stroke-width="4" fill="yellow"/>
<circle cx="250" cy="250" r="5" stroke="green" stroke-width="1" fill="none"/>
<circle cx="300" cy="300" r="5" stroke="red" stroke-width="1" fill="none"/>
<path d="M350,350 A 0 0 0 1 0 400 400Z" stroke="orange" stroke-width="4" fill="yellow"/>
<circle cx="350" cy="350" r="5" stroke="green" stroke-width="1" fill="none"/>
<circle cx="400" cy="400" r="5" stroke="red" stroke-width="1" fill="none"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

3
requirements.txt Normal file
View File

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

View File

@ -3,18 +3,17 @@ import codecs
import os
VERSION = '1.3.2beta'
VERSION = '1.6.1'
AUTHOR_NAME = 'Andy Port'
AUTHOR_EMAIL = 'AndyAPort@gmail.com'
GITHUB = 'https://github.com/mathandy/svgpathtools'
_here = os.path.abspath(os.path.dirname(__file__))
def read(*parts):
"""
Build an absolute path from *parts* and and return the contents of the
resulting file. Assume UTF-8 encoding.
"""
HERE = os.path.abspath(os.path.dirname(__file__))
with codecs.open(os.path.join(HERE, *parts), "rb", "utf-8") as f:
def read(relative_path):
"""Reads file at relative path, returning contents as string."""
with codecs.open(os.path.join(_here, relative_path), "rb", "utf-8") as f:
return f.read()
@ -23,26 +22,32 @@ setup(name='svgpathtools',
version=VERSION,
description=('A collection of tools for manipulating and analyzing SVG '
'Path objects and Bezier curves.'),
long_description=read("README.rst"),
# long_description=open('README.rst').read(),
long_description=read("README.md"),
long_description_content_type='text/markdown',
author=AUTHOR_NAME,
author_email=AUTHOR_EMAIL,
url='https://github.com/mathandy/svgpathtools',
download_url = 'http://github.com/mathandy/svgpathtools/tarball/'+VERSION,
url=GITHUB,
download_url='{}/releases/download/{}/svgpathtools-{}-py2.py3-none-any.whl'
''.format(GITHUB, VERSION, VERSION),
license='MIT',
# install_requires=['numpy', 'svgwrite'],
install_requires=['numpy', 'svgwrite', 'scipy'],
platforms="OS Independent",
# test_suite='tests',
requires=['numpy', 'svgwrite'],
keywords=['svg', 'svg path', 'svg.path', 'bezier', 'parse svg path', 'display svg'],
classifiers = [
classifiers=[
"Development Status :: 4 - Beta",
"Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
"Programming Language :: Python :: 2",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 2.7",
"Programming Language :: Python :: 3.5",
"Programming Language :: Python :: 3.6",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Topic :: Multimedia :: Graphics :: Editors :: Vector-Based",
"Topic :: Scientific/Engineering",
"Topic :: Scientific/Engineering :: Image Recognition",

View File

@ -1,657 +0,0 @@
Metadata-Version: 1.1
Name: svgpathtools
Version: 1.3.1
Summary: A collection of tools for manipulating and analyzing SVG Path objects and Bezier curves.
Home-page: https://github.com/mathandy/svgpathtools
Author: Andy Port
Author-email: AndyAPort@gmail.com
License: MIT
Download-URL: http://github.com/mathandy/svgpathtools/tarball/1.3.1
Description: svgpathtools
============
svgpathtools is a collection of tools for manipulating and analyzing SVG
Path objects and Bézier curves.
Features
--------
svgpathtools contains functions designed to **easily read, write and
display SVG files** as well as *a large selection of
geometrically-oriented tools* to **transform and analyze path
elements**.
Additionally, the submodule *bezier.py* contains tools for for working
with general **nth order Bezier curves stored as n-tuples**.
Some included tools:
- **read**, **write**, and **display** SVG files containing Path (and
other) SVG elements
- convert Bézier path segments to **numpy.poly1d** (polynomial) objects
- convert polynomials (in standard form) to their Bézier form
- compute **tangent vectors** and (right-hand rule) **normal vectors**
- compute **curvature**
- break discontinuous paths into their **continuous subpaths**.
- efficiently compute **intersections** between paths and/or segments
- find a **bounding box** for a path or segment
- **reverse** segment/path orientation
- **crop** and **split** paths and segments
- **smooth** paths (i.e. smooth away kinks to make paths
differentiable)
- **transition maps** from path domain to segment domain and back (T2t
and t2T)
- compute **area** enclosed by a closed path
- compute **arc length**
- compute **inverse arc length**
- convert RGB color tuples to hexadecimal color strings and back
Note on Python 3
----------------
While I am hopeful that this package entirely works with Python 3, it was born from a larger project coded in Python 2 and has not been thoroughly tested in
Python 3. Please let me know if you find any incompatibilities.
Prerequisites
-------------
- **numpy**
- **svgwrite**
Setup
-----
If not already installed, you can **install the prerequisites** using
pip.
.. code:: bash
$ pip install numpy
.. code:: bash
$ pip install svgwrite
Then **install svgpathtools**:
.. code:: bash
$ pip install svgpathtools
Alternative Setup
~~~~~~~~~~~~~~~~~
You can download the source from Github and install by using the command
(from inside the folder containing setup.py):
.. code:: bash
$ python setup.py install
Credit where credit's due
-------------------------
Much of the core of this module was taken from `the svg.path (v2.0)
module <https://github.com/regebro/svg.path>`__. Interested svg.path
users should see the compatibility notes at bottom of this readme.
Also, a big thanks to the author(s) of `A Primer on Bézier Curves <http://pomax.github.io/bezierinfo/>`_, an outstanding resource for learning about Bézier curves and Bézier curve-related algorithms.
Basic Usage
-----------
Classes
~~~~~~~
The svgpathtools module is primarily structured around four path segment
classes: ``Line``, ``QuadraticBezier``, ``CubicBezier``, and ``Arc``.
There is also a fifth class, ``Path``, whose objects are sequences of
(connected or disconnected\ `1 <#f1>`__\ ) path segment objects.
- ``Line(start, end)``
- ``Arc(start, radius, rotation, large_arc, sweep, end)`` Note: See
docstring for a detailed explanation of these parameters
- ``QuadraticBezier(start, control, end)``
- ``CubicBezier(start, control1, control2, end)``
- ``Path(*segments)``
See the relevant docstrings in *path.py* or the `official SVG
specifications <http://www.w3.org/TR/SVG/paths.html>`__ for more
information on what each parameter means.
1 Warning: Some of the functionality in this library has not been tested
on discontinuous Path objects. A simple workaround is provided, however,
by the ``Path.continuous_subpaths()`` method. `↩ <#a1>`__
.. code:: python
from __future__ import division, print_function
.. code:: python
# Coordinates are given as points in the complex plane
from svgpathtools import Path, Line, QuadraticBezier, CubicBezier, Arc
seg1 = CubicBezier(300+100j, 100+100j, 200+200j, 200+300j) # A cubic beginning at (300, 100) and ending at (200, 300)
seg2 = Line(200+300j, 250+350j) # A line beginning at (200, 300) and ending at (250, 350)
path = Path(seg1, seg2) # A path traversing the cubic and then the line
# We could alternatively created this Path object using a d-string
from svgpathtools import parse_path
path_alt = parse_path('M 300 100 C 100 100 200 200 200 300 L 250 350')
# Let's check that these two methods are equivalent
print(path)
print(path_alt)
print(path == path_alt)
# On a related note, the Path.d() method returns a Path object's d-string
print(path.d())
print(parse_path(path.d()) == path)
.. parsed-literal::
Path(CubicBezier(start=(300+100j), control1=(100+100j), control2=(200+200j), end=(200+300j)),
Line(start=(200+300j), end=(250+350j)))
Path(CubicBezier(start=(300+100j), control1=(100+100j), control2=(200+200j), end=(200+300j)),
Line(start=(200+300j), end=(250+350j)))
True
M 300.0,100.0 C 100.0,100.0 200.0,200.0 200.0,300.0 L 250.0,350.0
True
The ``Path`` class is a mutable sequence, so it behaves much like a
list. So segments can **append**\ ed, **insert**\ ed, set by index,
**del**\ eted, **enumerate**\ d, **slice**\ d out, etc.
.. code:: python
# Let's append another to the end of it
path.append(CubicBezier(250+350j, 275+350j, 250+225j, 200+100j))
print(path)
# Let's replace the first segment with a Line object
path[0] = Line(200+100j, 200+300j)
print(path)
# You may have noticed that this path is connected and now is also closed (i.e. path.start == path.end)
print("path is continuous? ", path.iscontinuous())
print("path is closed? ", path.isclosed())
# The curve the path follows is not, however, smooth (differentiable)
from svgpathtools import kinks, smoothed_path
print("path contains non-differentiable points? ", len(kinks(path)) > 0)
# If we want, we can smooth these out (Experimental and only for line/cubic paths)
# Note: smoothing will always works (except on 180 degree turns), but you may want
# to play with the maxjointsize and tightness parameters to get pleasing results
# Note also: smoothing will increase the number of segments in a path
spath = smoothed_path(path)
print("spath contains non-differentiable points? ", len(kinks(spath)) > 0)
print(spath)
# Let's take a quick look at the path and its smoothed relative
# The following commands will open two browser windows to display path and spaths
from svgpathtools import disvg
from time import sleep
disvg(path)
sleep(1) # needed when not giving the SVGs unique names (or not using timestamp)
disvg(spath)
print("Notice that path contains {} segments and spath contains {} segments."
"".format(len(path), len(spath)))
.. parsed-literal::
Path(CubicBezier(start=(300+100j), control1=(100+100j), control2=(200+200j), end=(200+300j)),
Line(start=(200+300j), end=(250+350j)),
CubicBezier(start=(250+350j), control1=(275+350j), control2=(250+225j), end=(200+100j)))
Path(Line(start=(200+100j), end=(200+300j)),
Line(start=(200+300j), end=(250+350j)),
CubicBezier(start=(250+350j), control1=(275+350j), control2=(250+225j), end=(200+100j)))
path is continuous? True
path is closed? True
path contains non-differentiable points? True
spath contains non-differentiable points? False
Path(Line(start=(200+101.5j), end=(200+298.5j)),
CubicBezier(start=(200+298.5j), control1=(200+298.505j), control2=(201.057124638+301.057124638j), end=(201.060660172+301.060660172j)),
Line(start=(201.060660172+301.060660172j), end=(248.939339828+348.939339828j)),
CubicBezier(start=(248.939339828+348.939339828j), control1=(249.649982143+349.649982143j), control2=(248.995+350j), end=(250+350j)),
CubicBezier(start=(250+350j), control1=(275+350j), control2=(250+225j), end=(200+100j)),
CubicBezier(start=(200+100j), control1=(199.62675237+99.0668809257j), control2=(200+100.495j), end=(200+101.5j)))
Notice that path contains 3 segments and spath contains 6 segments.
Reading SVGSs
~~~~~~~~~~~~~
| The **svg2paths()** function converts an svgfile to a list of Path
objects and a separate list of dictionaries containing the attributes
of each said path.
| Note: Line, Polyline, Polygon, and Path SVG elements can all be
converted to Path objects using this function.
.. code:: python
# Read SVG into a list of path objects and list of dictionaries of attributes
from svgpathtools import svg2paths, wsvg
paths, attributes = svg2paths('test.svg')
# Update: You can now also extract the svg-attributes by setting
# return_svg_attributes=True, or with the convenience function svg2paths2
from svgpathtools import svg2paths2
paths, attributes, svg_attributes = svg2paths2('test.svg')
# Let's print out the first path object and the color it was in the SVG
# We'll see it is composed of two CubicBezier objects and, in the SVG file it
# came from, it was red
redpath = paths[0]
redpath_attribs = attributes[0]
print(redpath)
print(redpath_attribs['stroke'])
.. parsed-literal::
Path(CubicBezier(start=(10.5+80j), control1=(40+10j), control2=(65+10j), end=(95+80j)),
CubicBezier(start=(95+80j), control1=(125+150j), control2=(150+150j), end=(180+80j)))
red
Writing SVGSs (and some geometric functions and methods)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The **wsvg()** function creates an SVG file from a list of path. This
function can do many things (see docstring in *paths2svg.py* for more
information) and is meant to be quick and easy to use. Note: Use the
convenience function **disvg()** (or set 'openinbrowser=True') to
automatically attempt to open the created svg file in your default SVG
viewer.
.. code:: python
# Let's make a new SVG that's identical to the first
wsvg(paths, attributes=attributes, svg_attributes=svg_attributes, filename='output1.svg')
.. figure:: https://cdn.rawgit.com/mathandy/svgpathtools/master/output1.svg
:alt: output1.svg
output1.svg
There will be many more examples of writing and displaying path data
below.
The .point() method and transitioning between path and path segment parameterizations
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
SVG Path elements and their segments have official parameterizations.
These parameterizations can be accessed using the ``Path.point()``,
``Line.point()``, ``QuadraticBezier.point()``, ``CubicBezier.point()``,
and ``Arc.point()`` methods. All these parameterizations are defined
over the domain 0 <= t <= 1.
| **Note:** In this document and in inline documentation and doctrings,
I use a capital ``T`` when referring to the parameterization of a Path
object and a lower case ``t`` when referring speaking about path
segment objects (i.e. Line, QaudraticBezier, CubicBezier, and Arc
objects).
| Given a ``T`` value, the ``Path.T2t()`` method can be used to find the
corresponding segment index, ``k``, and segment parameter, ``t``, such
that ``path.point(T)=path[k].point(t)``.
| There is also a ``Path.t2T()`` method to solve the inverse problem.
.. code:: python
# Example:
# Let's check that the first segment of redpath starts
# at the same point as redpath
firstseg = redpath[0]
print(redpath.point(0) == firstseg.point(0) == redpath.start == firstseg.start)
# Let's check that the last segment of redpath ends on the same point as redpath
lastseg = redpath[-1]
print(redpath.point(1) == lastseg.point(1) == redpath.end == lastseg.end)
# This next boolean should return False as redpath is composed multiple segments
print(redpath.point(0.5) == firstseg.point(0.5))
# If we want to figure out which segment of redpoint the
# point redpath.point(0.5) lands on, we can use the path.T2t() method
k, t = redpath.T2t(0.5)
print(redpath[k].point(t) == redpath.point(0.5))
.. parsed-literal::
True
True
False
True
Tangent vectors and Bezier curves as numpy polynomial objects
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
| Another great way to work with the parameterizations for Line,
QuadraticBezier, and CubicBezier objects is to convert them to
``numpy.poly1d`` objects. This is done easily using the
``Line.poly()``, ``QuadraticBezier.poly()`` and ``CubicBezier.poly()``
methods.
| There's also a ``polynomial2bezier()`` function in the pathtools.py
submodule to convert polynomials back to Bezier curves.
**Note:** cubic Bezier curves are parameterized as
.. math:: \mathcal{B}(t) = P_0(1-t)^3 + 3P_1(1-t)^2t + 3P_2(1-t)t^2 + P_3t^3
where :math:`P_0`, :math:`P_1`, :math:`P_2`, and :math:`P_3` are the
control points ``start``, ``control1``, ``control2``, and ``end``,
respectively, that svgpathtools uses to define a CubicBezier object. The
``CubicBezier.poly()`` method expands this polynomial to its standard
form
.. math:: \mathcal{B}(t) = c_0t^3 + c_1t^2 +c_2t+c3
where
.. math::
\begin{bmatrix}c_0\\c_1\\c_2\\c_3\end{bmatrix} =
\begin{bmatrix}
-1 & 3 & -3 & 1\\
3 & -6 & -3 & 0\\
-3 & 3 & 0 & 0\\
1 & 0 & 0 & 0\\
\end{bmatrix}
\begin{bmatrix}P_0\\P_1\\P_2\\P_3\end{bmatrix}
QuadraticBezier.poly() and Line.poly() are defined similarly.
.. code:: python
# Example:
b = CubicBezier(300+100j, 100+100j, 200+200j, 200+300j)
p = b.poly()
# p(t) == b.point(t)
print(p(0.235) == b.point(0.235))
# What is p(t)? It's just the cubic b written in standard form.
bpretty = "{}*(1-t)^3 + 3*{}*(1-t)^2*t + 3*{}*(1-t)*t^2 + {}*t^3".format(*b.bpoints())
print("The CubicBezier, b.point(x) = \n\n" +
bpretty + "\n\n" +
"can be rewritten in standard form as \n\n" +
str(p).replace('x','t'))
.. parsed-literal::
True
The CubicBezier, b.point(x) =
(300+100j)*(1-t)^3 + 3*(100+100j)*(1-t)^2*t + 3*(200+200j)*(1-t)*t^2 + (200+300j)*t^3
can be rewritten in standard form as
3 2
(-400 + -100j) t + (900 + 300j) t - 600 t + (300 + 100j)
To illustrate the awesomeness of being able to convert our Bezier curve
objects to numpy.poly1d objects and back, lets compute the unit tangent
vector of the above CubicBezier object, b, at t=0.5 in four different
ways.
Tangent vectors (and more on polynomials)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. code:: python
t = 0.5
### Method 1: the easy way
u1 = b.unit_tangent(t)
### Method 2: another easy way
# Note: This way will fail if it encounters a removable singularity.
u2 = b.derivative(t)/abs(b.derivative(t))
### Method 2: a third easy way
# Note: This way will also fail if it encounters a removable singularity.
dp = p.deriv()
u3 = dp(t)/abs(dp(t))
### Method 4: the removable-singularity-proof numpy.poly1d way
# Note: This is roughly how Method 1 works
from svgpathtools import real, imag, rational_limit
dx, dy = real(dp), imag(dp) # dp == dx + 1j*dy
p_mag2 = dx**2 + dy**2 # p_mag2(t) = |p(t)|**2
# Note: abs(dp) isn't a polynomial, but abs(dp)**2 is, and,
# the limit_{t->t0}[f(t) / abs(f(t))] ==
# sqrt(limit_{t->t0}[f(t)**2 / abs(f(t))**2])
from cmath import sqrt
u4 = sqrt(rational_limit(dp**2, p_mag2, t))
print("unit tangent check:", u1 == u2 == u3 == u4)
# Let's do a visual check
mag = b.length()/4 # so it's not hard to see the tangent line
tangent_line = Line(b.point(t), b.point(t) + mag*u1)
disvg([b, tangent_line], 'bg', nodes=[b.point(t)])
.. parsed-literal::
unit tangent check: True
Translations (shifts), reversing orientation, and normal vectors
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. code:: python
# Speaking of tangents, let's add a normal vector to the picture
n = b.normal(t)
normal_line = Line(b.point(t), b.point(t) + mag*n)
disvg([b, tangent_line, normal_line], 'bgp', nodes=[b.point(t)])
# and let's reverse the orientation of b!
# the tangent and normal lines should be sent to their opposites
br = b.reversed()
# Let's also shift b_r over a bit to the right so we can view it next to b
# The simplest way to do this is br = br.translated(3*mag), but let's use
# the .bpoints() instead, which returns a Bezier's control points
br.start, br.control1, br.control2, br.end = [3*mag + bpt for bpt in br.bpoints()] #
tangent_line_r = Line(br.point(t), br.point(t) + mag*br.unit_tangent(t))
normal_line_r = Line(br.point(t), br.point(t) + mag*br.normal(t))
wsvg([b, tangent_line, normal_line, br, tangent_line_r, normal_line_r],
'bgpkgp', nodes=[b.point(t), br.point(t)], filename='vectorframes.svg',
text=["b's tangent", "br's tangent"], text_path=[tangent_line, tangent_line_r])
.. figure:: https://cdn.rawgit.com/mathandy/svgpathtools/master/vectorframes.svg
:alt: vectorframes.svg
vectorframes.svg
Rotations and Translations
~~~~~~~~~~~~~~~~~~~~~~~~~~
.. code:: python
# Let's take a Line and an Arc and make some pictures
top_half = Arc(start=-1, radius=1+2j, rotation=0, large_arc=1, sweep=1, end=1)
midline = Line(-1.5, 1.5)
# First let's make our ellipse whole
bottom_half = top_half.rotated(180)
decorated_ellipse = Path(top_half, bottom_half)
# Now let's add the decorations
for k in range(12):
decorated_ellipse.append(midline.rotated(30*k))
# Let's move it over so we can see the original Line and Arc object next
# to the final product
decorated_ellipse = decorated_ellipse.translated(4+0j)
wsvg([top_half, midline, decorated_ellipse], filename='decorated_ellipse.svg')
.. figure:: https://cdn.rawgit.com/mathandy/svgpathtools/master/decorated_ellipse.svg
:alt: decorated\_ellipse.svg
decorated\_ellipse.svg
arc length and inverse arc length
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Here we'll create an SVG that shows off the parametric and geometric
midpoints of the paths from ``test.svg``. We'll need to compute use the
``Path.length()``, ``Line.length()``, ``QuadraticBezier.length()``,
``CubicBezier.length()``, and ``Arc.length()`` methods, as well as the
related inverse arc length methods ``.ilength()`` function to do this.
.. code:: python
# First we'll load the path data from the file test.svg
paths, attributes = svg2paths('test.svg')
# Let's mark the parametric midpoint of each segment
# I say "parametric" midpoint because Bezier curves aren't
# parameterized by arclength
# If they're also the geometric midpoint, let's mark them
# purple and otherwise we'll mark the geometric midpoint green
min_depth = 5
error = 1e-4
dots = []
ncols = []
nradii = []
for path in paths:
for seg in path:
parametric_mid = seg.point(0.5)
seg_length = seg.length()
if seg.length(0.5)/seg.length() == 1/2:
dots += [parametric_mid]
ncols += ['purple']
nradii += [5]
else:
t_mid = seg.ilength(seg_length/2)
geo_mid = seg.point(t_mid)
dots += [parametric_mid, geo_mid]
ncols += ['red', 'green']
nradii += [5] * 2
# In 'output2.svg' the paths will retain their original attributes
wsvg(paths, nodes=dots, node_colors=ncols, node_radii=nradii,
attributes=attributes, filename='output2.svg')
.. figure:: https://cdn.rawgit.com/mathandy/svgpathtools/master/output2.svg
:alt: output2.svg
output2.svg
Intersections between Bezier curves
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. code:: python
# Let's find all intersections between redpath and the other
redpath = paths[0]
redpath_attribs = attributes[0]
intersections = []
for path in paths[1:]:
for (T1, seg1, t1), (T2, seg2, t2) in redpath.intersect(path):
intersections.append(redpath.point(T1))
disvg(paths, filename='output_intersections.svg', attributes=attributes,
nodes = intersections, node_radii = [5]*len(intersections))
.. figure:: https://cdn.rawgit.com/mathandy/svgpathtools/master/output_intersections.svg
:alt: output\_intersections.svg
output\_intersections.svg
An Advanced Application: Offsetting Paths
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Here we'll find the `offset
curve <https://en.wikipedia.org/wiki/Parallel_curve>`__ for a few paths.
.. code:: python
from svgpathtools import parse_path, Line, Path, wsvg
def offset_curve(path, offset_distance, steps=1000):
"""Takes in a Path object, `path`, and a distance,
`offset_distance`, and outputs an piecewise-linear approximation
of the 'parallel' offset curve."""
nls = []
for seg in path:
ct = 1
for k in range(steps):
t = k / steps
offset_vector = offset_distance * seg.normal(t)
nl = Line(seg.point(t), seg.point(t) + offset_vector)
nls.append(nl)
connect_the_dots = [Line(nls[k].end, nls[k+1].end) for k in range(len(nls)-1)]
if path.isclosed():
connect_the_dots.append(Line(nls[-1].end, nls[0].end))
offset_path = Path(*connect_the_dots)
return offset_path
# Examples:
path1 = parse_path("m 288,600 c -52,-28 -42,-61 0,-97 ")
path2 = parse_path("M 151,395 C 407,485 726.17662,160 634,339").translated(300)
path3 = parse_path("m 117,695 c 237,-7 -103,-146 457,0").translated(500+400j)
paths = [path1, path2, path3]
offset_distances = [10*k for k in range(1,51)]
offset_paths = []
for path in paths:
for distances in offset_distances:
offset_paths.append(offset_curve(path, distances))
# Note: This will take a few moments
wsvg(paths + offset_paths, 'g'*len(paths) + 'r'*len(offset_paths), filename='offset_curves.svg')
.. figure:: https://cdn.rawgit.com/mathandy/svgpathtools/master/offset_curves.svg
:alt: offset\_curves.svg
offset\_curves.svg
Compatibility Notes for users of svg.path (v2.0)
------------------------------------------------
- renamed Arc.arc attribute as Arc.large\_arc
- Path.d() : For behavior similar\ `2 <#f2>`__\ to svg.path (v2.0),
set both useSandT and use\_closed\_attrib to be True.
2 The behavior would be identical, but the string formatting used in
this method has been changed to use default format (instead of the
General format, {:G}), for inceased precision. `↩ <#a2>`__
Licence
-------
This module is under a MIT License.
Keywords: svg,svg path,svg.path,bezier,parse svg path,display svg
Platform: OS Independent
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 2
Classifier: Programming Language :: Python :: 3
Classifier: Topic :: Multimedia :: Graphics :: Editors :: Vector-Based
Classifier: Topic :: Scientific/Engineering
Classifier: Topic :: Scientific/Engineering :: Image Recognition
Classifier: Topic :: Scientific/Engineering :: Information Analysis
Classifier: Topic :: Scientific/Engineering :: Mathematics
Classifier: Topic :: Scientific/Engineering :: Visualization
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires: numpy
Requires: svgwrite

View File

@ -1,37 +0,0 @@
LICENSE.txt
LICENSE2.txt
MANIFEST.in
README.rst
decorated_ellipse.svg
disvg_output.svg
offset_curves.svg
output1.svg
output2.svg
output_intersections.svg
path.svg
setup.cfg
setup.py
test.svg
vectorframes.svg
svgpathtools/__init__.py
svgpathtools/bezier.py
svgpathtools/directional_field.py
svgpathtools/misctools.py
svgpathtools/parser.py
svgpathtools/path.py
svgpathtools/paths2svg.py
svgpathtools/pathtools.py
svgpathtools/polytools.py
svgpathtools/smoothing.py
svgpathtools/svg2paths.py
svgpathtools.egg-info/PKG-INFO
svgpathtools.egg-info/SOURCES.txt
svgpathtools.egg-info/dependency_links.txt
svgpathtools.egg-info/top_level.txt
test/test.svg
test/test_bezier.py
test/test_generation.py
test/test_parsing.py
test/test_path.py
test/test_pathtools.py
test/test_polytools.py

View File

@ -1 +0,0 @@

View File

@ -1 +0,0 @@
svgpathtools

View File

@ -6,14 +6,17 @@ from .path import (Path, Line, QuadraticBezier, CubicBezier, Arc,
bezier_segment, is_bezier_segment, is_path_segment,
is_bezier_path, concatpaths, poly2bez, bpoints2bezier,
closest_point_in_path, farthest_point_in_path,
path_encloses_pt, bbox2path)
path_encloses_pt, bbox2path, polygon, polyline)
from .parser import parse_path
from .paths2svg import disvg, wsvg
from .paths2svg import disvg, wsvg, paths2Drawing
from .polytools import polyroots, polyroots01, rational_limit, real, imag
from .misctools import hex2rgb, rgb2hex
from .smoothing import smoothed_path, smoothed_joint, is_differentiable, kinks
from .document import (Document, CONVERSIONS, CONVERT_ONLY_PATHS,
SVG_GROUP_TAG, SVG_NAMESPACE)
from .svg_io_sax import SaxDocument
try:
from .svg2paths import svg2paths, svg2paths2
from .svg_to_paths import svg2paths, svg2paths2, svgstr2paths
except ImportError:
pass
pass

462
svgpathtools/document.py Normal file
View File

@ -0,0 +1,462 @@
"""(Experimental) replacement for import/export functionality.
This module contains the `Document` class, a container for a DOM-style
document (e.g. svg, html, xml, etc.) designed to replace and improve
upon the IO functionality of svgpathtools (i.e. the svg2paths and
disvg/wsvg functions).
An Historic Note:
The functionality in this module is meant to replace and improve
upon the IO functionality previously provided by the the
`svg2paths` and `disvg`/`wsvg` functions.
Example:
Typical usage looks something like the following.
>> from svgpathtools import Document
>> doc = Document('my_file.html')
>> for path in doc.paths():
>> # Do something with the transformed Path object.
>> foo(path)
>> # Inspect the raw SVG element, e.g. change its attributes
>> foo(path.element)
>> transform = result.transform
>> # Use the transform that was applied to the path.
>> foo(path.transform)
>> foo(doc.tree) # do stuff using ElementTree's functionality
>> doc.display() # display doc in OS's default application
>> doc.save('my_new_file.html')
A Big Problem:
Derivatives and other functions may be messed up by
transforms unless transforms are flattened (and not included in
css)
"""
# External dependencies
from __future__ import division, absolute_import, print_function
import os
import collections
import xml.etree.ElementTree as etree
from xml.etree.ElementTree import Element, SubElement, register_namespace
from xml.dom.minidom import parseString
import warnings
from io import StringIO
from tempfile import gettempdir
from time import time
import numpy as np
# Internal dependencies
from .parser import parse_path
from .parser import parse_transform
from .svg_to_paths import (path2pathd, ellipse2pathd, line2pathd,
polyline2pathd, polygon2pathd, rect2pathd)
from .misctools import open_in_browser
from .path import transform, Path, is_path_segment
# To maintain forward/backward compatibility
try:
string = basestring
except NameError:
string = str
try:
from os import PathLike
except ImportError:
PathLike = string
# Let xml.etree.ElementTree know about the SVG namespace
SVG_NAMESPACE = {'svg': 'http://www.w3.org/2000/svg'}
register_namespace('svg', 'http://www.w3.org/2000/svg')
# THESE MUST BE WRAPPED TO OUTPUT ElementTree.element objects
CONVERSIONS = {'path': path2pathd,
'circle': ellipse2pathd,
'ellipse': ellipse2pathd,
'line': line2pathd,
'polyline': polyline2pathd,
'polygon': polygon2pathd,
'rect': rect2pathd}
CONVERT_ONLY_PATHS = {'path': path2pathd}
SVG_GROUP_TAG = 'svg:g'
def flattened_paths(group, group_filter=lambda x: True,
path_filter=lambda x: True, path_conversions=CONVERSIONS,
group_search_xpath=SVG_GROUP_TAG):
"""Returns the paths inside a group (recursively), expressing the
paths in the base coordinates.
Note that if the group being passed in is nested inside some parent
group(s), we cannot take the parent group(s) into account, because
xml.etree.Element has no pointer to its parent. You should use
Document.flattened_paths_from_group(group) to flatten a specific nested group into
the root coordinates.
Args:
group is an Element
path_conversions (dict):
A dictionary to convert from an SVG element to a path data
string. Any element tags that are not included in this
dictionary will be ignored (including the `path` tag). To
only convert explicit path elements, pass in
`path_conversions=CONVERT_ONLY_PATHS`.
"""
if not isinstance(group, Element):
raise TypeError('Must provide an xml.etree.Element object. '
'Instead you provided {0}'.format(type(group)))
# Stop right away if the group_selector rejects this group
if not group_filter(group):
warnings.warn('The input group [{}] (id attribute: {}) was rejected by the group filter'
.format(group, group.get('id')))
return []
# To handle the transforms efficiently, we'll traverse the tree of
# groups depth-first using a stack of tuples.
# The first entry in the tuple is a group element and the second
# entry is its transform. As we pop each entry in the stack, we
# will add all its child group elements to the stack.
StackElement = collections.namedtuple('StackElement',
['group', 'transform'])
def new_stack_element(element, last_tf):
return StackElement(element, last_tf.dot(
parse_transform(element.get('transform'))))
def get_relevant_children(parent, last_tf):
children = []
for elem in filter(group_filter,
parent.iterfind(group_search_xpath, SVG_NAMESPACE)):
children.append(new_stack_element(elem, last_tf))
return children
stack = [new_stack_element(group, np.identity(3))]
paths = []
while stack:
top = stack.pop()
# For each element type that we know how to convert into path
# data, parse the element after confirming that the path_filter
# accepts it.
for key, converter in path_conversions.items():
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')))
path = transform(parse_path(converter(path_elem)), path_tf)
path.element = path_elem
path.transform = path_tf
paths.append(path)
stack.extend(get_relevant_children(top.group, top.transform))
return paths
def flattened_paths_from_group(group_to_flatten, root, recursive=True,
group_filter=lambda x: True,
path_filter=lambda x: True,
path_conversions=CONVERSIONS,
group_search_xpath=SVG_GROUP_TAG):
"""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."""
if not any(group_to_flatten is descendant for descendant in root.iter()):
warnings.warn('The requested group_to_flatten is not a '
'descendant of root')
# We will shortcut here, because it is impossible for any paths
# to be returned anyhow.
return []
# We create a set of the unique IDs of each element that we wish to
# flatten, if those elements are groups. Any groups outside of this
# set will be skipped while we flatten the paths.
desired_groups = set()
if recursive:
for group in group_to_flatten.iter():
desired_groups.add(id(group))
else:
desired_groups.add(id(group_to_flatten))
ignore_paths = set()
# Use breadth-first search to find the path to the group that we care about
if root is not group_to_flatten:
search = [[root]]
route = None
while search:
top = search.pop(0)
frontier = top[-1]
for child in frontier.iterfind(group_search_xpath, SVG_NAMESPACE):
if child is group_to_flatten:
route = top
break
future_top = list(top)
future_top.append(child)
search.append(future_top)
if route is not None:
for group in route:
# Add each group from the root to the parent of the desired group
# to the list of groups that we should traverse. This makes sure
# that paths will not stop before reaching the desired
# group.
desired_groups.add(id(group))
for key in path_conversions.keys():
for path_elem in group.iterfind('svg:'+key, SVG_NAMESPACE):
# Add each path in the parent groups to the list of paths
# that should be ignored. The user has not requested to
# flatten the paths of the parent groups, so we should not
# include any of these in the result.
ignore_paths.add(id(path_elem))
break
if route is None:
raise ValueError('The group_to_flatten is not a descendant of the root!')
def desired_group_filter(x):
return (id(x) in desired_groups) and group_filter(x)
def desired_path_filter(x):
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)
class Document:
def __init__(self, filepath=None):
"""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
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.
"""
# strings are interpreted as file location everything else is treated as
# file-like object and passed to the xml parser directly
from_filepath = isinstance(filepath, string) or isinstance(filepath, PathLike)
self.original_filepath = os.path.abspath(filepath) if from_filepath else None
if filepath is None:
self.tree = etree.ElementTree(Element('svg'))
else:
# parse svg to ElementTree object
self.tree = etree.parse(filepath)
self.root = self.tree.getroot()
@classmethod
def from_svg_string(cls, svg_string):
"""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)
def paths(self, group_filter=lambda x: True,
path_filter=lambda x: True, path_conversions=CONVERSIONS):
"""Returns a list of all paths in the document.
Note that any transform attributes are applied before returning
the paths.
"""
return flattened_paths(self.tree.getroot(), group_filter,
path_filter, path_conversions)
def paths_from_group(self, group, recursive=True, group_filter=lambda x: True,
path_filter=lambda x: True, path_conversions=CONVERSIONS):
if all(isinstance(s, string) for s in group):
# If we're given a list of strings, assume it represents a
# nested sequence
group = self.get_group(group)
elif not isinstance(group, Element):
raise TypeError(
'Must provide a list of strings that represent a nested '
'group name, or provide an xml.etree.Element object. '
'Instead you provided {0}'.format(group))
if group is None:
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)
def add_path(self, path, attribs=None, group=None):
"""Add a new path to the SVG."""
# If not given a parent, assume that the path does not have a group
if group is None:
group = self.tree.getroot()
# If given a list of strings (one or more), assume it represents
# a sequence of nested group names
elif len(group) > 0 and all(isinstance(elem, str) for elem in group):
group = self.get_or_add_group(group)
elif not isinstance(group, Element):
raise TypeError(
'Must provide a list of strings or an xml.etree.Element '
'object. Instead you provided {0}'.format(group))
else:
# Make sure that the group belongs to this Document object
if not self.contains_group(group):
warnings.warn('The requested group does not belong to '
'this Document')
# TODO: It might be better to use duck-typing here with a try-except
if isinstance(path, Path):
path_svg = path.d()
elif is_path_segment(path):
path_svg = Path(path).d()
elif isinstance(path, string):
# Assume this is a valid d-string.
# TODO: Should we sanity check the input string?
path_svg = path
else:
raise TypeError(
'Must provide a Path, a path segment type, or a valid '
'SVG path d-string. Instead you provided {0}'.format(path))
if attribs is None:
attribs = {}
else:
attribs = attribs.copy()
attribs['d'] = path_svg
return SubElement(group, 'path', attribs)
def contains_group(self, group):
return any(group is owned for owned in self.tree.iter())
def get_group(self, nested_names, name_attr='id'):
"""Get a group from the tree, or None if the requested group
does not exist. Use get_or_add_group(~) if you want a new group
to be created if it did not already exist.
`nested_names` is a list of strings which represent group names.
Each group name will be nested inside of the previous group name.
`name_attr` is the group attribute that is being used to
represent the group's name. Default is 'id', but some SVGs may
contain custom name labels, like 'inkscape:label'.
Returns the request group. If the requested group did not
exist, this function will return a None value.
"""
group = self.tree.getroot()
# Drill down through the names until we find the desired group
while len(nested_names):
prev_group = group
next_name = nested_names.pop(0)
for elem in group.iterfind(SVG_GROUP_TAG, SVG_NAMESPACE):
if elem.get(name_attr) == next_name:
group = elem
break
if prev_group is group:
# The nested group could not be found, so we return None
return None
return group
def get_or_add_group(self, nested_names, name_attr='id'):
"""Get a group from the tree, or add a new one with the given
name structure.
`nested_names` is a list of strings which represent group names.
Each group name will be nested inside of the previous group name.
`name_attr` is the group attribute that is being used to
represent the group's name. Default is 'id', but some SVGs may
contain custom name labels, like 'inkscape:label'.
Returns the requested group. If the requested group did not
exist, this function will create it, as well as all parent
groups that it requires. All created groups will be left with
blank attributes.
"""
group = self.tree.getroot()
# Drill down through the names until we find the desired group
while len(nested_names):
prev_group = group
next_name = nested_names.pop(0)
for elem in group.iterfind(SVG_GROUP_TAG, SVG_NAMESPACE):
if elem.get(name_attr) == next_name:
group = elem
break
if prev_group is group:
# The group we're looking for does not exist, so let's
# create the group structure
nested_names.insert(0, next_name)
while nested_names:
next_name = nested_names.pop(0)
group = self.add_group({'id': next_name}, group)
# Now nested_names will be empty, so the topmost
# while-loop will end
return group
def add_group(self, group_attribs=None, parent=None):
"""Add an empty group element to the SVG."""
if parent is None:
parent = self.tree.getroot()
elif not self.contains_group(parent):
warnings.warn('The requested group {0} does not belong to '
'this Document'.format(parent))
if group_attribs is None:
group_attribs = {}
else:
group_attribs = group_attribs.copy()
return SubElement(parent, '{{{0}}}g'.format(
SVG_NAMESPACE['svg']), group_attribs)
def __repr__(self):
return etree.tostring(self.tree.getroot()).decode()
def pretty(self, **kwargs):
return parseString(repr(self)).toprettyxml(**kwargs)
def save(self, filepath, prettify=False, **kwargs):
with open(filepath, 'w+') as output_svg:
if prettify:
output_svg.write(self.pretty(**kwargs))
else:
output_svg.write(repr(self))
def display(self, filepath=None):
"""Displays/opens the doc using the OS's default application."""
if filepath is None:
if self.original_filepath is None: # created from empty Document
orig_name, ext = 'unnamed', '.svg'
else:
orig_name, ext = \
os.path.splitext(os.path.basename(self.original_filepath))
tmp_name = orig_name + '_' + str(time()).replace('.', '-') + ext
filepath = os.path.join(gettempdir(), tmp_name)
# write to a (by default temporary) file
with open(filepath, 'w') as output_svg:
output_svg.write(repr(self))
open_in_browser(filepath)

View File

@ -31,7 +31,7 @@ def rgb2hex(rgb):
>>> rgb2hex((0,0,255))
'#0000FF'
"""
return ('#%02x%02x%02x' % rgb).upper()
return ('#%02x%02x%02x' % tuple(rgb)).upper()
def isclose(a, b, rtol=1e-5, atol=1e-8):

View File

@ -1,196 +1,110 @@
"""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)."""
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
import re
import numpy as np
import warnings
# Internal dependencies
from .path import Path, Line, QuadraticBezier, CubicBezier, Arc
from .path import Path
COMMANDS = set('MmZzLlHhVvCcSsQqTtAa')
UPPERCASE = set('MZLHVCSQTA')
COMMAND_RE = re.compile("([MmZzLlHhVvCcSsQqTtAa])")
FLOAT_RE = re.compile("[-+]?[0-9]*\.?[0-9]+(?:[eE][-+]?[0-9]+)?")
def parse_path(pathdef, current_pos=0j, tree_element=None):
return Path(pathdef, current_pos=current_pos, tree_element=tree_element)
def _tokenize_path(pathdef):
for x in COMMAND_RE.split(pathdef):
if x in COMMANDS:
yield x
for token in FLOAT_RE.findall(x):
yield token
def parse_path(pathdef, current_pos=0j):
# In the SVG specs, initial movetos are absolute, even if
# specified as 'm'. This is the default behavior here as well.
# But if you pass in a current_pos variable, the initial moveto
# will be relative to that current_pos. This is useful.
elements = list(_tokenize_path(pathdef))
# Reverse for easy use of .pop()
elements.reverse()
segments = Path()
start_pos = None
command = None
while elements:
if elements[-1] in COMMANDS:
# New command.
last_command = command # Used by S and T
command = elements.pop()
absolute = command in UPPERCASE
command = command.upper()
def _check_num_parsed_values(values, allowed):
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))
elif allowed[0] != 1:
warnings.warn('Expected {0} values, found {1}: {2}'.format(allowed[0], len(values), values))
else:
# If this element starts with numbers, it is an implicit command
# and we don't change the command. Check that it's allowed:
if command is None:
raise ValueError("Unallowed implicit command in %s, position %s" % (
pathdef, len(pathdef.split()) - len(elements)))
warnings.warn('Expected 1 value, found {0}: {1}'.format(len(values), values))
return False
return True
if command == 'M':
# Moveto command.
x = elements.pop()
y = elements.pop()
pos = float(x) + float(y) * 1j
if absolute:
current_pos = pos
else:
current_pos += pos
# when M is called, reset start_pos
# This behavior of Z is defined in svg spec:
# http://www.w3.org/TR/SVG/paths.html#PathDataClosePathCommand
start_pos = current_pos
def _parse_transform_substr(transform_substr):
# Implicit moveto commands are treated as lineto commands.
# So we set command to lineto here, in case there are
# further implicit commands after this moveto.
command = 'L'
type_str, value_str = transform_substr.split('(')
value_str = value_str.replace(',', ' ')
values = list(map(float, filter(None, value_str.split(' '))))
elif command == 'Z':
# Close path
if not (current_pos == start_pos):
segments.append(Line(current_pos, start_pos))
segments.closed = True
current_pos = start_pos
start_pos = None
command = None # You can't have implicit commands after closing.
transform = np.identity(3)
if 'matrix' in type_str:
if not _check_num_parsed_values(values, [6]):
return transform
elif command == 'L':
x = elements.pop()
y = elements.pop()
pos = float(x) + float(y) * 1j
if not absolute:
pos += current_pos
segments.append(Line(current_pos, pos))
current_pos = pos
transform[0:2, 0:3] = np.array([values[0:6:2], values[1:6:2]])
elif command == 'H':
x = elements.pop()
pos = float(x) + current_pos.imag * 1j
if not absolute:
pos += current_pos.real
segments.append(Line(current_pos, pos))
current_pos = pos
elif 'translate' in transform_substr:
if not _check_num_parsed_values(values, [1, 2]):
return transform
elif command == 'V':
y = elements.pop()
pos = current_pos.real + float(y) * 1j
if not absolute:
pos += current_pos.imag * 1j
segments.append(Line(current_pos, pos))
current_pos = pos
transform[0, 2] = values[0]
if len(values) > 1:
transform[1, 2] = values[1]
elif command == 'C':
control1 = float(elements.pop()) + float(elements.pop()) * 1j
control2 = float(elements.pop()) + float(elements.pop()) * 1j
end = float(elements.pop()) + float(elements.pop()) * 1j
elif 'scale' in transform_substr:
if not _check_num_parsed_values(values, [1, 2]):
return transform
if not absolute:
control1 += current_pos
control2 += current_pos
end += current_pos
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
segments.append(CubicBezier(current_pos, control1, control2, end))
current_pos = end
elif 'rotate' in transform_substr:
if not _check_num_parsed_values(values, [1, 3]):
return transform
elif command == 'S':
# Smooth curve. First control point is the "reflection" of
# the second control point in the previous path.
angle = values[0] * np.pi / 180.0
if len(values) == 3:
offset = values[1:3]
else:
offset = (0, 0)
tf_offset = np.identity(3)
tf_offset[0:2, 2:3] = np.array([[offset[0]], [offset[1]]])
tf_rotate = np.identity(3)
tf_rotate[0:2, 0:2] = np.array([[np.cos(angle), -np.sin(angle)], [np.sin(angle), np.cos(angle)]])
tf_offset_neg = np.identity(3)
tf_offset_neg[0:2, 2:3] = np.array([[-offset[0]], [-offset[1]]])
if last_command not in 'CS':
# 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.
control1 = current_pos
else:
# The first control point is assumed to be the reflection of
# the second control point on the previous command relative
# to the current point.
control1 = current_pos + current_pos - segments[-1].control2
transform = tf_offset.dot(tf_rotate).dot(tf_offset_neg)
control2 = float(elements.pop()) + float(elements.pop()) * 1j
end = float(elements.pop()) + float(elements.pop()) * 1j
elif 'skewX' in transform_substr:
if not _check_num_parsed_values(values, [1]):
return transform
if not absolute:
control2 += current_pos
end += current_pos
transform[0, 1] = np.tan(values[0] * np.pi / 180.0)
segments.append(CubicBezier(current_pos, control1, control2, end))
current_pos = end
elif 'skewY' in transform_substr:
if not _check_num_parsed_values(values, [1]):
return transform
elif command == 'Q':
control = float(elements.pop()) + float(elements.pop()) * 1j
end = float(elements.pop()) + float(elements.pop()) * 1j
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))
if not absolute:
control += current_pos
end += current_pos
return transform
segments.append(QuadraticBezier(current_pos, control, end))
current_pos = end
elif command == 'T':
# Smooth curve. Control point is the "reflection" of
# the second control point in the previous path.
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:
return np.identity(3)
elif not isinstance(transform_str, str):
raise TypeError('Must provide a string to parse')
if last_command not in 'QT':
# 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.
control = current_pos
else:
# The control point is assumed to be the reflection of
# the control point on the previous command relative
# to the current point.
control = current_pos + current_pos - segments[-1].control
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))
end = float(elements.pop()) + float(elements.pop()) * 1j
if not absolute:
end += current_pos
segments.append(QuadraticBezier(current_pos, control, end))
current_pos = end
elif command == 'A':
radius = float(elements.pop()) + float(elements.pop()) * 1j
rotation = float(elements.pop())
arc = float(elements.pop())
sweep = float(elements.pop())
end = float(elements.pop()) + float(elements.pop()) * 1j
if not absolute:
end += current_pos
segments.append(Arc(current_pos, radius, rotation, arc, sweep, end))
current_pos = end
return segments
return total_transform

File diff suppressed because it is too large Load Diff

View File

@ -1,20 +1,24 @@
"""This submodule contains tools for creating svg files from paths and path
segments."""
"""This submodule: basic tools for creating svg files from path data.
See also the document.py submodule.
"""
# External dependencies:
from __future__ import division, absolute_import, print_function
from math import ceil
from os import getcwd, path as os_path, makedirs
from os import path as os_path, makedirs
from tempfile import gettempdir
from xml.dom.minidom import parse as md_xml_parse
from svgwrite import Drawing, text as txt
from time import time
from warnings import warn
import re
# Internal dependencies
from .path import Path, Line, is_path_segment
from .misctools import open_in_browser
# Used to convert a string colors (identified by single chars) to a list.
# color shorthand for inputting color list as string of chars.
color_dict = {'a': 'aqua',
'b': 'blue',
'c': 'cyan',
@ -57,8 +61,16 @@ def is3tuple(c):
def big_bounding_box(paths_n_stuff):
"""Finds a BB containing a collection of paths, Bezier path segments, and
points (given as complex numbers)."""
"""returns minimal upright bounding box.
Args:
paths_n_stuff: iterable of Paths, Bezier path segments, and
points (given as complex numbers).
Returns:
extrema of bounding box, (xmin, xmax, ymin, ymax)
"""
bbs = []
for thing in paths_n_stuff:
if is_path_segment(thing) or isinstance(thing, Path):
@ -71,9 +83,9 @@ def big_bounding_box(paths_n_stuff):
bbs.append((complexthing.real, complexthing.real,
complexthing.imag, complexthing.imag))
except ValueError:
raise TypeError(
"paths_n_stuff can only contains Path, CubicBezier, "
"QuadraticBezier, Line, and complex objects.")
raise TypeError("paths_n_stuff can only contains Path, "
"CubicBezier, QuadraticBezier, Line, "
"and complex objects.")
xmins, xmaxs, ymins, ymaxs = list(zip(*bbs))
xmin = min(xmins)
xmax = max(xmaxs)
@ -82,14 +94,15 @@ def big_bounding_box(paths_n_stuff):
return xmin, xmax, ymin, ymax
def disvg(paths=None, colors=None,
filename=os_path.join(getcwd(), 'disvg_output.svg'),
stroke_widths=None, nodes=None, node_colors=None, node_radii=None,
openinbrowser=True, timestamp=False,
margin_size=0.1, mindim=600, dimensions=None,
viewbox=None, text=None, text_path=None, font_size=None,
attributes=None, svg_attributes=None):
"""Takes in a list of paths and creates an SVG file containing said paths.
def disvg(paths=None, colors=None, filename=None, stroke_widths=None,
nodes=None, node_colors=None, node_radii=None,
openinbrowser=True, timestamp=None, margin_size=0.1,
mindim=600, dimensions=None, viewbox=None, text=None,
text_path=None, font_size=None, attributes=None,
svg_attributes=None, svgwrite_debug=False,
paths2Drawing=False, baseunit='px'):
"""Creates (and optionally displays) an SVG file.
REQUIRED INPUTS:
:param paths - a list of paths
@ -104,8 +117,10 @@ def disvg(paths=None, colors=None,
3) a list of rgb 3-tuples -- e.g. colors = [(255, 0, 0), ...].
:param filename - the desired location/filename of the SVG file
created (by default the SVG will be stored in the current working
directory and named 'disvg_output.svg').
created (by default the SVG will be named 'disvg_output.svg' or
'disvg_output_<timestamp>.svg' and stored in the temporary
directory returned by `tempfile.gettempdir()`. See `timestamp`
for information on the timestamp.
:param stroke_widths - a list of stroke_widths to use for paths
(default is 0.5% of the SVG's width or length)
@ -130,9 +145,11 @@ def disvg(paths=None, colors=None,
:param openinbrowser - Set to True to automatically open the created
SVG in the user's default web browser.
:param timestamp - if True, then the a timestamp will be appended to
the output SVG's filename. This will fix issues with rapidly opening
multiple SVGs in your browser.
:param timestamp - if true, then the a timestamp will be
appended to the output SVG's filename. This is meant as a
workaround for issues related to rapidly opening multiple
SVGs in your browser using `disvg`. This defaults to true if
`filename is None` and false otherwise.
:param margin_size - The min margin (empty area framing the collection
of paths) size used for creating the canvas and background of the SVG.
@ -140,30 +157,51 @@ def disvg(paths=None, colors=None,
:param mindim - The minimum dimension (height or width) of the output
SVG (default is 600).
:param dimensions - The display dimensions of the output SVG. Using
this will override the mindim parameter.
:param dimensions - The (x,y) display dimensions of the output SVG.
I.e. this specifies the `width` and `height` SVG attributes. Note that
these also can be used to specify units other than pixels. Using this
will override the `mindim` parameter.
:param viewbox - This specifies what rectangular patch of R^2 will be
viewable through the outputSVG. It should be input in the form
(min_x, min_y, width, height). This is different from the display
dimension of the svg, which can be set through mindim or dimensions.
:param viewbox - This specifies the coordinated system used in the svg.
The SVG `viewBox` attribute works together with the the `height` and
`width` attrinutes. Using these three attributes allows for shifting
and scaling of the SVG canvas without changing the any values other
than those in `viewBox`, `height`, and `width`. `viewbox` should be
input as a 4-tuple, (min_x, min_y, width, height), or a string
"min_x min_y width height". Using this will override the `mindim`
parameter.
:param attributes - a list of dictionaries of attributes for the input
paths. Note: This will override any other conflicting settings.
:param svg_attributes - a dictionary of attributes for output svg.
Note 1: This will override any other conflicting settings.
Note 2: Setting `svg_attributes={'debug': False}` may result in a
significant increase in speed.
:param svgwrite_debug - This parameter turns on/off `svgwrite`'s
debugging mode. By default svgwrite_debug=False. This increases
speed and also prevents `svgwrite` from raising of an error when not
all `svg_attributes` key-value pairs are understood.
:param paths2Drawing - If true, an `svgwrite.Drawing` object is
returned and no file is written. This `Drawing` can later be saved
using the `svgwrite.Drawing.save()` method.
NOTES:
-The unit of length here is assumed to be pixels in all variables.
* The `svg_attributes` parameter will override any other conflicting
settings.
-If this function is used multiple times in quick succession to
* Any `extra` parameters that `svgwrite.Drawing()` accepts can be
controlled by passing them in through `svg_attributes`.
* The unit of length here is assumed to be pixels in all variables.
* If this function is used multiple times in quick succession to
display multiple SVGs (all using the default filename), the
svgviewer/browser will likely fail to load some of the SVGs in time.
To fix this, use the timestamp attribute, or give the files unique
names, or use a pause command (e.g. time.sleep(1)) between uses.
SEE ALSO:
* document.py
"""
_default_relative_node_radius = 5e-3
@ -172,14 +210,17 @@ def disvg(paths=None, colors=None,
_default_node_color = '#ff0000' # red
_default_font_size = 12
# append directory to filename (if not included)
if os_path.dirname(filename) == '':
filename = os_path.join(getcwd(), filename)
if filename is None:
timestamp = True if timestamp is None else timestamp
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
if timestamp:
fbname, fext = os_path.splitext(filename)
dirname = os_path.dirname(filename)
tstamp = str(time()).replace('.', '')
stfilename = os_path.split(fbname)[1] + '_' + tstamp + fext
filename = os_path.join(dirname, stfilename)
@ -219,7 +260,15 @@ def disvg(paths=None, colors=None,
assert paths or nodes
stuff2bound = []
if viewbox:
szx, szy = viewbox[2:4]
if not isinstance(viewbox, str):
viewbox = '%s %s %s %s' % viewbox
if dimensions is None:
dimensions = viewbox.split(' ')[2:4]
elif dimensions:
dimensions = tuple(map(str, dimensions))
def strip_units(s):
return re.search(r'\d*\.?\d*', s.strip()).group()
viewbox = '0 0 %s %s' % tuple(map(strip_units, dimensions))
else:
if paths:
stuff2bound += paths
@ -266,21 +315,29 @@ def disvg(paths=None, colors=None,
dx += 2*margin_size*dx + extra_space_for_style
dy += 2*margin_size*dy + extra_space_for_style
viewbox = "%s %s %s %s" % (xmin, ymin, dx, dy)
if dimensions:
szx, szy = dimensions
if mindim is None:
szx = "{}{}".format(dx, baseunit)
szy = "{}{}".format(dy, baseunit)
else:
if dx > dy:
szx = str(mindim) + 'px'
szy = str(int(ceil(mindim * dy / dx))) + 'px'
szx = str(mindim) + baseunit
szy = str(int(ceil(mindim * dy / dx))) + baseunit
else:
szx = str(int(ceil(mindim * dx / dy))) + 'px'
szy = str(mindim) + 'px'
szx = str(int(ceil(mindim * dx / dy))) + baseunit
szy = str(mindim) + baseunit
dimensions = szx, szy
# Create an SVG file
if svg_attributes:
dwg = Drawing(filename=filename, **svg_attributes)
if svg_attributes is not None:
dimensions = (svg_attributes.get("width", dimensions[0]),
svg_attributes.get("height", dimensions[1]))
debug = svg_attributes.get("debug", svgwrite_debug)
dwg = Drawing(filename=filename, size=dimensions, debug=debug,
**svg_attributes)
else:
dwg = Drawing(filename=filename, size=(szx, szy), viewBox=viewbox)
dwg = Drawing(filename=filename, size=dimensions, debug=svgwrite_debug,
viewBox=viewbox)
# add paths
if paths:
@ -350,9 +407,9 @@ def disvg(paths=None, colors=None,
txter = dwg.add(dwg.text('', font_size=font_size[idx]))
txter.add(txt.TextPath('#'+pathid, s))
# save svg
if not os_path.exists(os_path.dirname(filename)):
makedirs(os_path.dirname(filename))
if paths2Drawing:
return dwg
dwg.save()
# re-open the svg, make the xml pretty, and save it again
@ -369,19 +426,56 @@ def disvg(paths=None, colors=None,
print(filename)
def wsvg(paths=None, colors=None,
filename=os_path.join(getcwd(), 'disvg_output.svg'),
stroke_widths=None, nodes=None, node_colors=None, node_radii=None,
openinbrowser=False, timestamp=False,
margin_size=0.1, mindim=600, dimensions=None,
viewbox=None, text=None, text_path=None, font_size=None,
attributes=None, svg_attributes=None):
"""Convenience function; identical to disvg() except that
openinbrowser=False by default. See disvg() docstring for more info."""
disvg(paths, colors=colors, filename=filename,
stroke_widths=stroke_widths, nodes=nodes,
node_colors=node_colors, node_radii=node_radii,
openinbrowser=openinbrowser, timestamp=timestamp,
margin_size=margin_size, mindim=mindim, dimensions=dimensions,
viewbox=viewbox, text=text, text_path=text_path, font_size=font_size,
attributes=attributes, svg_attributes=svg_attributes)
def wsvg(paths=None, colors=None, filename=None, stroke_widths=None,
nodes=None, node_colors=None, node_radii=None,
openinbrowser=False, timestamp=False, margin_size=0.1,
mindim=600, dimensions=None, viewbox=None, text=None,
text_path=None, font_size=None, attributes=None,
svg_attributes=None, svgwrite_debug=False,
paths2Drawing=False, baseunit='px'):
"""Create SVG and write to disk.
Note: This is identical to `disvg()` except that `openinbrowser`
is false by default and an assertion error is raised if `filename
is None`.
See `disvg()` docstring for more info.
"""
assert filename is not None
return disvg(paths, colors=colors, filename=filename,
stroke_widths=stroke_widths, nodes=nodes,
node_colors=node_colors, node_radii=node_radii,
openinbrowser=openinbrowser, timestamp=timestamp,
margin_size=margin_size, mindim=mindim,
dimensions=dimensions, viewbox=viewbox, text=text,
text_path=text_path, font_size=font_size,
attributes=attributes, svg_attributes=svg_attributes,
svgwrite_debug=svgwrite_debug,
paths2Drawing=paths2Drawing, baseunit=baseunit)
def paths2Drawing(paths=None, colors=None, filename=None,
stroke_widths=None, nodes=None, node_colors=None,
node_radii=None, openinbrowser=False, timestamp=False,
margin_size=0.1, mindim=600, dimensions=None,
viewbox=None, text=None, text_path=None,
font_size=None, attributes=None, svg_attributes=None,
svgwrite_debug=False, paths2Drawing=True, baseunit='px'):
"""Create and return `svg.Drawing` object.
Note: This is identical to `disvg()` except that `paths2Drawing`
is true by default and an assertion error is raised if `filename
is None`.
See `disvg()` docstring for more info.
"""
return disvg(paths, colors=colors, filename=filename,
stroke_widths=stroke_widths, nodes=nodes,
node_colors=node_colors, node_radii=node_radii,
openinbrowser=openinbrowser, timestamp=timestamp,
margin_size=margin_size, mindim=mindim,
dimensions=dimensions, viewbox=viewbox, text=text,
text_path=text_path, font_size=font_size,
attributes=attributes, svg_attributes=svg_attributes,
svgwrite_debug=svgwrite_debug,
paths2Drawing=paths2Drawing, baseunit=baseunit)

View File

@ -1,134 +0,0 @@
"""This submodule contains tools for creating path objects from SVG files.
The main tool being the svg2paths() function."""
# External dependencies
from __future__ import division, absolute_import, print_function
from xml.dom.minidom import parse
from os import path as os_path, getcwd
# Internal dependencies
from .parser import parse_path
def polyline2pathd(polyline_d):
"""converts the string from a polyline points-attribute to a string for a
Path object d-attribute"""
points = polyline_d.replace(', ', ',')
points = points.replace(' ,', ',')
points = points.split()
closed = points[0] == points[-1]
d = 'M' + points.pop(0).replace(',', ' ')
for p in points:
d += 'L' + p.replace(',', ' ')
if closed:
d += 'z'
return d
def polygon2pathd(polyline_d):
"""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)."""
points = polyline_d.replace(', ', ',')
points = points.replace(' ,', ',')
points = points.split()
reduntantly_closed = points[0] == points[-1]
d = 'M' + points[0].replace(',', ' ')
for p in points[1:]:
d += 'L' + p.replace(',', ' ')
# The `parse_path` call ignores redundant 'z' (closure) commands
# e.g. `parse_path('M0 0L100 100Z') == parse_path('M0 0L100 100L0 0Z')`
# This check ensures that an n-point polygon is converted to an n-Line path.
if reduntantly_closed:
d += 'L' + points[0].replace(',', ' ')
return d + 'z'
def svg2paths(svg_file_location,
convert_lines_to_paths=True,
convert_polylines_to_paths=True,
convert_polygons_to_paths=True,
return_svg_attributes=False):
"""
Converts an SVG file into a list of Path objects and a list of
dictionaries containing their attributes. This currently supports
SVG Path, Line, Polyline, and Polygon elements.
:param svg_file_location: the location of the svg file
:param convert_lines_to_paths: Set to False to disclude SVG-Line objects
(converted to Paths)
:param convert_polylines_to_paths: Set to False to disclude SVG-Polyline
objects (converted to Paths)
:param convert_polygons_to_paths: Set to False to disclude SVG-Polygon
objects (converted to Paths)
:param return_svg_attributes: Set to True and a dictionary of
svg-attributes will be extracted and returned
:return: list of Path objects, list of path attribute dictionaries, and
(optionally) a dictionary of svg-attributes
"""
if os_path.dirname(svg_file_location) == '':
svg_file_location = os_path.join(getcwd(), svg_file_location)
doc = parse(svg_file_location)
def dom2dict(element):
"""Converts DOM elements to dictionaries of attributes."""
keys = list(element.attributes.keys())
values = [val.value for val in list(element.attributes.values())]
return dict(list(zip(keys, values)))
# Use minidom to extract path strings from input SVG
paths = [dom2dict(el) for el in doc.getElementsByTagName('path')]
d_strings = [el['d'] for el in paths]
attribute_dictionary_list = paths
# Use minidom to extract polyline strings from input SVG, convert to
# path strings, add to list
if convert_polylines_to_paths:
plins = [dom2dict(el) for el in doc.getElementsByTagName('polyline')]
d_strings += [polyline2pathd(pl['points']) for pl in plins]
attribute_dictionary_list += plins
# Use minidom to extract polygon strings from input SVG, convert to
# path strings, add to list
if convert_polygons_to_paths:
pgons = [dom2dict(el) for el in doc.getElementsByTagName('polygon')]
d_strings += [polygon2pathd(pg['points']) for pg in pgons]
attribute_dictionary_list += pgons
if convert_lines_to_paths:
lines = [dom2dict(el) for el in doc.getElementsByTagName('line')]
d_strings += [('M' + l['x1'] + ' ' + l['y1'] +
'L' + l['x2'] + ' ' + l['y2']) for l in lines]
attribute_dictionary_list += lines
if return_svg_attributes:
svg_attributes = dom2dict(doc.getElementsByTagName('svg')[0])
doc.unlink()
path_list = [parse_path(d) for d in d_strings]
return path_list, attribute_dictionary_list, svg_attributes
else:
doc.unlink()
path_list = [parse_path(d) for d in d_strings]
return path_list, attribute_dictionary_list
def svg2paths2(svg_file_location,
convert_lines_to_paths=True,
convert_polylines_to_paths=True,
convert_polygons_to_paths=True,
return_svg_attributes=True):
"""Convenience function; identical to svg2paths() except that
return_svg_attributes=True by default. See svg2paths() docstring for more
info."""
return svg2paths(svg_file_location=svg_file_location,
convert_lines_to_paths=convert_lines_to_paths,
convert_polylines_to_paths=convert_polylines_to_paths,
convert_polygons_to_paths=convert_polygons_to_paths,
return_svg_attributes=return_svg_attributes)

199
svgpathtools/svg_io_sax.py Normal file
View File

@ -0,0 +1,199 @@
"""(Experimental) replacement for import/export functionality SAX
"""
# External dependencies
from __future__ import division, absolute_import, print_function
import os
from xml.etree.ElementTree import iterparse, Element, ElementTree, SubElement
import numpy as np
# Internal dependencies
from .parser import parse_path
from .parser import parse_transform
from .svg_to_paths import (path2pathd, ellipse2pathd, line2pathd,
polyline2pathd, polygon2pathd, rect2pathd)
from .misctools import open_in_browser
from .path import transform
# To maintain forward/backward compatibility
try:
string = basestring
except NameError:
string = str
NAME_SVG = "svg"
ATTR_VERSION = "version"
VALUE_SVG_VERSION = "1.1"
ATTR_XMLNS = "xmlns"
VALUE_XMLNS = "http://www.w3.org/2000/svg"
ATTR_XMLNS_LINK = "xmlns:xlink"
VALUE_XLINK = "http://www.w3.org/1999/xlink"
ATTR_XMLNS_EV = "xmlns:ev"
VALUE_XMLNS_EV = "http://www.w3.org/2001/xml-events"
ATTR_WIDTH = "width"
ATTR_HEIGHT = "height"
ATTR_VIEWBOX = "viewBox"
NAME_PATH = "path"
ATTR_DATA = "d"
ATTR_FILL = "fill"
ATTR_STROKE = "stroke"
ATTR_STROKE_WIDTH = "stroke-width"
ATTR_TRANSFORM = "transform"
VALUE_NONE = "none"
class SaxDocument:
def __init__(self, filename):
"""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
"""
self.root_values = {}
self.tree = []
# remember location of original svg file
if filename is not None and os.path.dirname(filename) == '':
self.original_filename = os.path.join(os.getcwd(), filename)
else:
self.original_filename = filename
if filename is not None:
self.sax_parse(filename)
def sax_parse(self, filename):
self.root_values = {}
self.tree = []
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:
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:
v = stack.pop()
values = v[0]
matrix = v[1]
def flatten_all_paths(self):
flat = []
for values in self.tree:
pathd = values['d']
matrix = values['matrix']
parsed_path = parse_path(pathd)
if matrix is not None:
transform(parsed_path, matrix)
flat.append(parsed_path)
return flat
def get_pathd_and_matrix(self):
flat = []
for values in self.tree:
pathd = values['d']
matrix = values['matrix']
flat.append((pathd, matrix))
return flat
def generate_dom(self):
root = Element(NAME_SVG)
root.set(ATTR_VERSION, VALUE_SVG_VERSION)
root.set(ATTR_XMLNS, VALUE_XMLNS)
root.set(ATTR_XMLNS_LINK, VALUE_XLINK)
root.set(ATTR_XMLNS_EV, VALUE_XMLNS_EV)
width = self.root_values.get(ATTR_WIDTH, None)
height = self.root_values.get(ATTR_HEIGHT, None)
if width is not None:
root.set(ATTR_WIDTH, width)
if height is not None:
root.set(ATTR_HEIGHT, height)
viewbox = self.root_values.get(ATTR_VIEWBOX, None)
if viewbox is not None:
root.set(ATTR_VIEWBOX, viewbox)
identity = np.identity(3)
for values in self.tree:
pathd = values.get('d', '')
matrix = values.get('matrix', None)
# path_value = parse_path(pathd)
path = SubElement(root, NAME_PATH)
if matrix is not None and not np.all(np.equal(matrix, identity)):
matrix_string = "matrix("
matrix_string += " "
matrix_string += string(matrix[0][0])
matrix_string += " "
matrix_string += string(matrix[1][0])
matrix_string += " "
matrix_string += string(matrix[0][1])
matrix_string += " "
matrix_string += string(matrix[1][1])
matrix_string += " "
matrix_string += string(matrix[0][2])
matrix_string += " "
matrix_string += string(matrix[1][2])
matrix_string += ")"
path.set(ATTR_TRANSFORM, matrix_string)
if ATTR_DATA in values:
path.set(ATTR_DATA, values[ATTR_DATA])
if ATTR_FILL in values:
path.set(ATTR_FILL, values[ATTR_FILL])
if ATTR_STROKE in values:
path.set(ATTR_STROKE, values[ATTR_STROKE])
return ElementTree(root)
def save(self, filename):
with open(filename, 'wb') as output_svg:
dom_tree = self.generate_dom()
dom_tree.write(output_svg)
def display(self, filename=None):
"""Displays/opens the doc using the OS's default application."""
if filename is None:
filename = 'display_temp.svg'
self.save(filename)
open_in_browser(filename)

View File

@ -0,0 +1,283 @@
"""This submodule contains tools for creating path objects from SVG files.
The main tool being the svg2paths() function."""
# External dependencies
from __future__ import division, absolute_import, print_function
from xml.dom.minidom import parse
import os
from io import StringIO
import re
try:
from os import PathLike as FilePathLike
except ImportError:
FilePathLike = str
# Internal dependencies
from .parser import parse_path
COORD_PAIR_TMPLT = re.compile(
r'([\+-]?\d*[\.\d]\d*[eE][\+-]?\d+|[\+-]?\d*[\.\d]\d*)' +
r'(?:\s*,\s*|\s+|(?=-))' +
r'([\+-]?\d*[\.\d]\d*[eE][\+-]?\d+|[\+-]?\d*[\.\d]\d*)'
)
def path2pathd(path):
return path.get('d', '')
def ellipse2pathd(ellipse):
"""converts the parameters from an ellipse or a circle to a string for a
Path object d-attribute"""
cx = ellipse.get('cx', 0)
cy = ellipse.get('cy', 0)
rx = ellipse.get('rx', None)
ry = ellipse.get('ry', None)
r = ellipse.get('r', None)
if r is not None:
rx = ry = float(r)
else:
rx = float(rx)
ry = float(ry)
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'
return d + 'z'
def polyline2pathd(polyline, is_polygon=False):
"""converts the string from a polyline points-attribute to a string for a
Path object d-attribute"""
if isinstance(polyline, str):
points = polyline
else:
points = COORD_PAIR_TMPLT.findall(polyline.get('points', ''))
closed = (float(points[0][0]) == float(points[-1][0]) and
float(points[0][1]) == float(points[-1][1]))
# The `parse_path` call ignores redundant 'z' (closure) commands
# e.g. `parse_path('M0 0L100 100Z') == parse_path('M0 0L100 100L0 0Z')`
# This check ensures that an n-point polygon is converted to an n-Line path.
if is_polygon and closed:
points.append(points[0])
d = 'M' + 'L'.join('{0} {1}'.format(x,y) for x,y in points)
if is_polygon or closed:
d += 'z'
return d
def polygon2pathd(polyline):
"""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)
def rect2pathd(rect):
"""Converts an SVG-rect element to a Path d-string.
The rectangle will start at the (x,y) coordinate specified by the
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 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"
"".format(x0, y0, x1, y1, x2, y2, x3, y3))
return d
def line2pathd(l):
return (
'M' + l.attrib.get('x1', '0') + ' ' + l.attrib.get('y1', '0')
+ 'L' + l.attrib.get('x2', '0') + ' ' + l.attrib.get('y2', '0')
)
def svg2paths(svg_file_location,
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):
"""Converts an SVG into a list of Path objects and attribute dictionaries.
Converts an SVG file into a list of Path objects and a list of
dictionaries containing their attributes. This currently supports
SVG Path, Line, Polyline, Polygon, Circle, and Ellipse elements.
Args:
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
svg-attributes will be extracted and returned. See also the
`svg2paths2()` function.
convert_circles_to_paths: Set to False to exclude SVG-Circle
elements (converted to Paths). By default circles are included as
paths of two `Arc` objects.
convert_ellipses_to_paths (bool): Set to False to exclude SVG-Ellipse
elements (converted to Paths). By default ellipses are included as
paths of two `Arc` objects.
convert_lines_to_paths (bool): Set to False to exclude SVG-Line elements
(converted to Paths)
convert_polylines_to_paths (bool): Set to False to exclude SVG-Polyline
elements (converted to Paths)
convert_polygons_to_paths (bool): Set to False to exclude SVG-Polygon
elements (converted to Paths)
convert_rectangles_to_paths (bool): Set to False to exclude SVG-Rect
elements (converted to Paths).
Returns:
list: The list of Path objects.
list: The list of corresponding path attribute dictionaries.
dict (optional): A dictionary of svg-attributes (see `svg2paths2()`).
"""
# strings are interpreted as file location everything else is treated as
# 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)
def dom2dict(element):
"""Converts DOM elements to dictionaries of attributes."""
keys = list(element.attributes.keys())
values = [val.value for val in list(element.attributes.values())]
return dict(list(zip(keys, values)))
# Use minidom to extract path strings from input SVG
paths = [dom2dict(el) for el in doc.getElementsByTagName('path')]
d_strings = [el['d'] for el in paths]
attribute_dictionary_list = paths
# Use minidom to extract polyline strings from input SVG, convert to
# path strings, add to list
if convert_polylines_to_paths:
plins = [dom2dict(el) for el in doc.getElementsByTagName('polyline')]
d_strings += [polyline2pathd(pl) for pl in plins]
attribute_dictionary_list += plins
# Use minidom to extract polygon strings from input SVG, convert to
# 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]
attribute_dictionary_list += pgons
if convert_lines_to_paths:
lines = [dom2dict(el) for el in doc.getElementsByTagName('line')]
d_strings += [('M' + l['x1'] + ' ' + l['y1'] +
'L' + l['x2'] + ' ' + l['y2']) for l in lines]
attribute_dictionary_list += lines
if convert_ellipses_to_paths:
ellipses = [dom2dict(el) for el in doc.getElementsByTagName('ellipse')]
d_strings += [ellipse2pathd(e) for e in ellipses]
attribute_dictionary_list += ellipses
if convert_circles_to_paths:
circles = [dom2dict(el) for el in doc.getElementsByTagName('circle')]
d_strings += [ellipse2pathd(c) for c in circles]
attribute_dictionary_list += circles
if convert_rectangles_to_paths:
rectangles = [dom2dict(el) for el in doc.getElementsByTagName('rect')]
d_strings += [rect2pathd(r) for r in rectangles]
attribute_dictionary_list += rectangles
if return_svg_attributes:
svg_attributes = dom2dict(doc.getElementsByTagName('svg')[0])
doc.unlink()
path_list = [parse_path(d) for d in d_strings]
return path_list, attribute_dictionary_list, svg_attributes
else:
doc.unlink()
path_list = [parse_path(d) for d in d_strings]
return path_list, attribute_dictionary_list
def svg2paths2(svg_file_location,
return_svg_attributes=True,
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
return_svg_attributes=True by default. See svg2paths() docstring for more
info."""
return svg2paths(svg_file_location=svg_file_location,
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)
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)

4
test/circle.svg Normal file
View File

@ -0,0 +1,4 @@
<?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">
<circle cx="100" cy="100" r="50"/>
</svg>

After

Width:  |  Height:  |  Size: 294 B

1
test/display_temp.svg Normal file
View File

@ -0,0 +1 @@
<svg height="100%" version="1.1" viewBox="0 0 365 365" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:ev="http://www.w3.org/2001/xml-events" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M10.0,50.0a40.0,40.0 0 1,0 80.0,0a40.0,40.0 0 1,0 -80.0,0" fill="red" stroke="black" transform="matrix( 1.5 0.0 0.0 0.5 -40.0 20.0)" /><path d="M 150,200 l -50,25" fill="black" stroke="black" transform="matrix( 1.5 0.0 0.0 0.5 -40.0 20.0)" /><path d="M 100 350 l 150 -300" fill="none" stroke="red" /><path d="M 250 50 l 150 300" fill="none" stroke="red" /><path d="M 175 200 l 150 0" fill="none" stroke="green" /><path d="M 100 350 q 150 -300 300 0" fill="none" stroke="blue" /><path d="M97.0,350.0a3.0,3.0 0 1,0 6.0,0a3.0,3.0 0 1,0 -6.0,0" fill="black" stroke="black" /><path d="M247.0,50.0a3.0,3.0 0 1,0 6.0,0a3.0,3.0 0 1,0 -6.0,0" fill="black" stroke="black" /><path d="M397.0,350.0a3.0,3.0 0 1,0 6.0,0a3.0,3.0 0 1,0 -6.0,0" fill="black" stroke="black" /><path d="M200 10L250 190L160 210z" fill="lime" stroke="purple" transform="matrix( 0.1 0.0 0.0 0.1 0.0 0.0)" /></svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

4
test/ellipse.svg Normal file
View File

@ -0,0 +1,4 @@
<?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">
<ellipse cx="100" cy="100" rx="50" ry="50"/>
</svg>

After

Width:  |  Height:  |  Size: 304 B

162
test/groups.svg Normal file
View File

@ -0,0 +1,162 @@
<?xml version="1.0" ?>
<svg
baseProfile="full"
version="1.1"
viewBox="0 0 365 365"
height="100%"
width="100%"
xmlns="http://www.w3.org/2000/svg"
xmlns:test="some://testuri">
<defs/>
<g
id="matrix group"
transform="matrix(1.5 0.0 0.0 0.5 -40.0 20.0)">
<path
d="M 183,183 l 0,-50"
fill="black"
stroke="black"
stroke-width="3"
test:name="path00"/>
<g
id="scale group"
transform="scale(1.25)">
<path
d="M 122,320 l -50,0"
fill="black"
stroke="black"
stroke-width="3"
test:name="path01"/>
<g
id="nested group - empty transform"
transform="">
<path
d="M 150,200 l -50,25"
fill="black"
stroke="black"
stroke-width="3"
test:name="path02"/>
</g>
<g
id="nested group - no transform">
<path
d="M 150,200 l -50,25"
fill="black"
stroke="black"
stroke-width="3"
test:name="path03"/>
</g>
<g
id="nested group - translate"
transform="translate(20)">
<path
d="M 150,200 l -50,25"
fill="black"
stroke="black"
stroke-width="3"
test:name="path04"/>
</g>
<g
id="nested group - translate xy"
transform="
translate(20, 30)">
<path
d="M 150,200 l -50,25"
fill="black"
stroke="black"
stroke-width="3"
test:name="path05"/>
</g>
</g>
<g
id="scale xy group"
transform="scale(0.5 1.5)">
<path
d="M 122,320 l -50,0"
fill="black"
stroke="black"
stroke-width="3"
test:name="path06"/>
</g>
<g
id="rotate group"
transform="rotate(20)">
<path
d="M 183,183 l 0,30"
fill="black"
stroke="black"
stroke-width="3"
test:name="path07"/>
</g>
<g
id="rotate xy group"
transform="rotate(45 183 183)">
<path
d="M 183,183 l 0,30"
fill="black"
stroke="black"
stroke-width="3"
test:name="path08"/>
</g>
<g
id="skew x group"
transform="skewX(5)">
<path
d="M 183,183 l 40,40"
fill="black"
stroke="black"
stroke-width="3"
test:name="path09"/>
</g>
<g
id="skew y group"
transform="skewY(5)">
<path
d="M 183,183 l 40,40"
fill="black"
stroke="black"
stroke-width="3"
test:name="path10"/>
<path
d="M 180,20 l -70,80"
fill="black"
stroke="black"
stroke-width="4"
transform="rotate(-40, 100, 100)"
test:name="path11"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.0 KiB

19
test/negative-scale.svg Normal file
View File

@ -0,0 +1,19 @@
<?xml version="1.0"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100mm" height="100mm" viewBox="-100 -200 500 500" xmlns="http://www.w3.org/2000/svg" version="1.1">
<g id="Sketch" transform="scale(1,-1)">
<path id="slot" d="
M 0 10
L 0 80
A 30 30 0 1 0 0 140
A 10 10 0 0 1 0 100
L 100 100
A 10 10 0 1 1 100 140
A 30 30 0 0 0 100 80
L 100 10
A 10 10 0 0 0 90 0
L 10 0
A 10 10 0 0 0 0 10
" stroke="#ff0000" stroke-width="0.35 px"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 665 B

View File

@ -1,5 +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 points="55.5,0 55.5, 50 105.5,50" style="stroke:purple;stroke-width:1"/>
<polygon points="0,0 0,100 100,100 0,0" style="stroke:purple;stroke-width:1"/>
<polygon points="0,0.0 0,-100 , 1.0e-1-1e2,0,0" style="stroke:purple;stroke-width:1"/>
</svg>

Before

Width:  |  Height:  |  Size: 422 B

After

Width:  |  Height:  |  Size: 431 B

7
test/rects.svg Normal file
View File

@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink">
<rect width="300" height="100" stroke='red'/>
<rect x="200" y="200" width="300" height="100" stroke='blue'/>
</svg>

After

Width:  |  Height:  |  Size: 207 B

View File

@ -1,7 +1,7 @@
from __future__ import division, absolute_import, print_function
import numpy as np
import unittest
from svgpathtools.bezier import *
from svgpathtools.bezier import bezier_point, bezier2polynomial, polynomial2bezier
from svgpathtools.path import bpoints2bezier

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 Document
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

@ -2,7 +2,7 @@
#------------------------------------------------------------------------------
from __future__ import division, absolute_import, print_function
import unittest
from svgpathtools import *
from svgpathtools import parse_path
class TestGeneration(unittest.TestCase):

265
test/test_groups.py Normal file
View File

@ -0,0 +1,265 @@
"""Tests related to SVG groups.
To run these tests, you can use (from root svgpathtools directory):
$ python -m unittest test.test_groups.TestGroups.test_group_flatten
"""
from __future__ import division, absolute_import, print_function
import unittest
from svgpathtools import Document, SVG_NAMESPACE, parse_path, Line, Arc
from os.path import join, dirname
import numpy as np
# When an assert fails, show the full error message, don't truncate it.
unittest.util._MAX_LENGTH = 999999999
def get_desired_path(name, paths):
return next(p for p in paths
if p.element.get('{some://testuri}name') == name)
class TestGroups(unittest.TestCase):
def check_values(self, v, z):
# Check that the components of 2D vector v match the components
# of complex number z
self.assertAlmostEqual(v[0], z.real)
self.assertAlmostEqual(v[1], z.imag)
def check_line(self, tf, v_s_vals, v_e_relative_vals, name, paths):
# Check that the endpoints of the line have been correctly transformed.
# * tf is the transform that should have been applied.
# * v_s_vals is a 2D list of the values of the line's start point
# * v_e_relative_vals is a 2D list of the values of the line's
# end point relative to the start point
# * name is the path name (value of the test:name attribute in
# the SVG document)
# * paths is the output of doc.paths()
v_s_vals.append(1.0)
v_e_relative_vals.append(0.0)
v_s = np.array(v_s_vals)
v_e = v_s + v_e_relative_vals
actual = get_desired_path(name, paths)
self.check_values(tf.dot(v_s), actual.start)
self.check_values(tf.dot(v_e), actual.end)
def test_group_transform(self):
# The input svg has a group transform of "scale(1,-1)", which
# can mess with Arc sweeps.
doc = Document(join(dirname(__file__), 'negative-scale.svg'))
path = doc.paths()[0]
self.assertEqual(path[0], Line(start=-10j, end=-80j))
self.assertEqual(path[1], Arc(start=-80j, radius=(30+30j), rotation=0.0, large_arc=True, sweep=True, end=-140j))
self.assertEqual(path[2], Arc(start=-140j, radius=(20+20j), rotation=0.0, large_arc=False, sweep=False, end=-100j))
self.assertEqual(path[3], Line(start=-100j, end=(100-100j)))
self.assertEqual(path[4], Arc(start=(100-100j), radius=(20+20j), rotation=0.0, large_arc=True, sweep=False, end=(100-140j)))
self.assertEqual(path[5], Arc(start=(100-140j), radius=(30+30j), rotation=0.0, large_arc=False, sweep=True, end=(100-80j)))
self.assertEqual(path[6], Line(start=(100-80j), end=(100-10j)))
self.assertEqual(path[7], Arc(start=(100-10j), radius=(10+10j), rotation=0.0, large_arc=False, sweep=True, end=(90+0j)))
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_group_flatten(self):
# Test the Document.paths() function against the
# groups.svg test file.
# There are 12 paths in that file, with various levels of being
# nested inside of group transforms.
# The check_line function is used to reduce the boilerplate,
# since all the tests are very similar.
# This test covers each of the different types of transforms
# that are specified by the SVG standard.
doc = Document(join(dirname(__file__), 'groups.svg'))
result = doc.paths()
self.assertEqual(12, len(result))
tf_matrix_group = np.array([[1.5, 0.0, -40.0],
[0.0, 0.5, 20.0],
[0.0, 0.0, 1.0]])
self.check_line(tf_matrix_group,
[183, 183], [0.0, -50],
'path00', result)
tf_scale_group = np.array([[1.25, 0.0, 0.0],
[0.0, 1.25, 0.0],
[0.0, 0.0, 1.0]])
self.check_line(tf_matrix_group.dot(tf_scale_group),
[122, 320], [-50.0, 0.0],
'path01', result)
self.check_line(tf_matrix_group.dot(tf_scale_group),
[150, 200], [-50, 25],
'path02', result)
self.check_line(tf_matrix_group.dot(tf_scale_group),
[150, 200], [-50, 25],
'path03', result)
tf_nested_translate_group = np.array([[1, 0, 20],
[0, 1, 0],
[0, 0, 1]])
self.check_line(tf_matrix_group.dot(tf_scale_group
).dot(tf_nested_translate_group),
[150, 200], [-50, 25],
'path04', result)
tf_nested_translate_xy_group = np.array([[1, 0, 20],
[0, 1, 30],
[0, 0, 1]])
self.check_line(tf_matrix_group.dot(tf_scale_group
).dot(tf_nested_translate_xy_group),
[150, 200], [-50, 25],
'path05', result)
tf_scale_xy_group = np.array([[0.5, 0, 0],
[0, 1.5, 0.0],
[0, 0, 1]])
self.check_line(tf_matrix_group.dot(tf_scale_xy_group),
[122, 320], [-50, 0],
'path06', result)
a_07 = 20.0*np.pi/180.0
tf_rotate_group = np.array([[np.cos(a_07), -np.sin(a_07), 0],
[np.sin(a_07), np.cos(a_07), 0],
[0, 0, 1]])
self.check_line(tf_matrix_group.dot(tf_rotate_group),
[183, 183], [0, 30],
'path07', result)
a_08 = 45.0*np.pi/180.0
tf_rotate_xy_group_R = np.array([[np.cos(a_08), -np.sin(a_08), 0],
[np.sin(a_08), np.cos(a_08), 0],
[0, 0, 1]])
tf_rotate_xy_group_T = np.array([[1, 0, 183],
[0, 1, 183],
[0, 0, 1]])
tf_rotate_xy_group = tf_rotate_xy_group_T.dot(
tf_rotate_xy_group_R).dot(
np.linalg.inv(tf_rotate_xy_group_T))
self.check_line(tf_matrix_group.dot(tf_rotate_xy_group),
[183, 183], [0, 30],
'path08', result)
a_09 = 5.0*np.pi/180.0
tf_skew_x_group = np.array([[1, np.tan(a_09), 0],
[0, 1, 0],
[0, 0, 1]])
self.check_line(tf_matrix_group.dot(tf_skew_x_group),
[183, 183], [40, 40],
'path09', result)
a_10 = 5.0*np.pi/180.0
tf_skew_y_group = np.array([[1, 0, 0],
[np.tan(a_10), 1, 0],
[0, 0, 1]])
self.check_line(tf_matrix_group.dot(tf_skew_y_group),
[183, 183], [40, 40],
'path10', result)
# This last test is for handling transforms that are defined as
# attributes of a <path> element.
a_11 = -40*np.pi/180.0
tf_path11_R = np.array([[np.cos(a_11), -np.sin(a_11), 0],
[np.sin(a_11), np.cos(a_11), 0],
[0, 0, 1]])
tf_path11_T = np.array([[1, 0, 100],
[0, 1, 100],
[0, 0, 1]])
tf_path11 = tf_path11_T.dot(tf_path11_R).dot(np.linalg.inv(tf_path11_T))
self.check_line(tf_matrix_group.dot(tf_skew_y_group).dot(tf_path11),
[180, 20], [-70, 80],
'path11', result)
def check_group_count(self, doc, expected_count):
count = 0
for _ in doc.tree.getroot().iter('{{{0}}}g'.format(SVG_NAMESPACE['svg'])):
count += 1
self.assertEqual(expected_count, count)
def test_nested_group(self):
# A bug in the flattened_paths_from_group() implementation made it so that only top-level
# groups could have their paths flattened. This is a regression test to make
# sure that when a nested group is requested, its paths can also be flattened.
doc = Document(join(dirname(__file__), 'groups.svg'))
result = doc.paths_from_group(['matrix group', 'scale group'])
self.assertEqual(len(result), 5)
def test_add_group(self):
# Test `Document.add_group()` function and related Document functions.
doc = Document(None)
self.check_group_count(doc, 0)
base_group = doc.add_group()
base_group.set('id', 'base_group')
self.assertTrue(doc.contains_group(base_group))
self.check_group_count(doc, 1)
child_group = doc.add_group(parent=base_group)
child_group.set('id', 'child_group')
self.assertTrue(doc.contains_group(child_group))
self.check_group_count(doc, 2)
grandchild_group = doc.add_group(parent=child_group)
grandchild_group.set('id', 'grandchild_group')
self.assertTrue(doc.contains_group(grandchild_group))
self.check_group_count(doc, 3)
sibling_group = doc.add_group(parent=base_group)
sibling_group.set('id', 'sibling_group')
self.assertTrue(doc.contains_group(sibling_group))
self.check_group_count(doc, 4)
# Test that we can retrieve each new group from the document
self.assertEqual(base_group, doc.get_or_add_group(['base_group']))
self.assertEqual(child_group, doc.get_or_add_group(
['base_group', 'child_group']))
self.assertEqual(grandchild_group, doc.get_or_add_group(
['base_group', 'child_group', 'grandchild_group']))
self.assertEqual(sibling_group, doc.get_or_add_group(
['base_group', 'sibling_group']))
# Create a new nested group
new_child = doc.get_or_add_group(
['base_group', 'new_parent', 'new_child'])
self.check_group_count(doc, 6)
self.assertEqual(new_child, doc.get_or_add_group(
['base_group', 'new_parent', 'new_child']))
new_leaf = doc.get_or_add_group(
['base_group', 'new_parent', 'new_child', 'new_leaf'])
self.assertEqual(new_leaf, doc.get_or_add_group([
'base_group', 'new_parent', 'new_child', 'new_leaf']))
self.check_group_count(doc, 7)
path_d = ('M 206.07112,858.41289 L 206.07112,-2.02031 '
'C -50.738,-81.14814 -20.36402,-105.87055 52.52793,-101.01525 '
'L 103.03556,0.0 '
'L 0.0,111.11678')
svg_path = doc.add_path(path_d, group=new_leaf)
self.assertEqual(path_d, svg_path.get('d'))
path = parse_path(path_d)
svg_path = doc.add_path(path, group=new_leaf)
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

@ -1,8 +1,23 @@
# 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 unittest
from svgpathtools import *
from svgpathtools import Path, Line, QuadraticBezier, CubicBezier, Arc, parse_path
import svgpathtools
import numpy as np
def construct_rotation_tf(a, x, y):
a = a * np.pi / 180.0
tf_offset = np.identity(3)
tf_offset[0:2, 2:3] = np.array([[x], [y]])
tf_rotate = np.identity(3)
tf_rotate[0:2, 0:2] = np.array([[np.cos(a), -np.sin(a)],
[np.sin(a), np.cos(a)]])
tf_offset_neg = np.identity(3)
tf_offset_neg[0:2, 2:3] = np.array([[-x], [-y]])
return tf_offset.dot(tf_rotate).dot(tf_offset_neg)
class TestParser(unittest.TestCase):
@ -17,11 +32,10 @@ class TestParser(unittest.TestCase):
# for Z command behavior when there is multiple subpaths
path1 = parse_path('M 0 0 L 50 20 M 100 100 L 300 100 L 200 300 z')
self.assertEqual(path1, Path(
Line(0 + 0j, 50 + 20j),
Line(100 + 100j, 300 + 100j),
Line(300 + 100j, 200 + 300j),
Line(200 + 300j, 100 + 100j)))
self.assertEqual(path1, Path(Line(0 + 0j, 50 + 20j),
Line(100 + 100j, 300 + 100j),
Line(300 + 100j, 200 + 300j),
Line(200 + 300j, 100 + 100j)))
path1 = parse_path('M 100 100 L 200 200')
path2 = parse_path('M100 100L200 200')
@ -33,46 +47,68 @@ class TestParser(unittest.TestCase):
path1 = parse_path("""M100,200 C100,100 250,100 250,200
S400,300 400,200""")
self.assertEqual(path1,
Path(CubicBezier(100 + 200j, 100 + 100j, 250 + 100j, 250 + 200j),
CubicBezier(250 + 200j, 250 + 300j, 400 + 300j, 400 + 200j)))
self.assertEqual(path1, Path(CubicBezier(100 + 200j,
100 + 100j,
250 + 100j,
250 + 200j),
CubicBezier(250 + 200j,
250 + 300j,
400 + 300j,
400 + 200j)))
path1 = parse_path('M100,200 C100,100 400,100 400,200')
self.assertEqual(path1,
Path(CubicBezier(100 + 200j, 100 + 100j, 400 + 100j, 400 + 200j)))
self.assertEqual(path1, Path(CubicBezier(100 + 200j,
100 + 100j,
400 + 100j,
400 + 200j)))
path1 = parse_path('M100,500 C25,400 475,400 400,500')
self.assertEqual(path1,
Path(CubicBezier(100 + 500j, 25 + 400j, 475 + 400j, 400 + 500j)))
self.assertEqual(path1, Path(CubicBezier(100 + 500j,
25 + 400j,
475 + 400j,
400 + 500j)))
path1 = parse_path('M100,800 C175,700 325,700 400,800')
self.assertEqual(path1,
Path(CubicBezier(100 + 800j, 175 + 700j, 325 + 700j, 400 + 800j)))
self.assertEqual(path1, Path(CubicBezier(100 + 800j,
175 + 700j,
325 + 700j,
400 + 800j)))
path1 = parse_path('M600,200 C675,100 975,100 900,200')
self.assertEqual(path1,
Path(CubicBezier(600 + 200j, 675 + 100j, 975 + 100j, 900 + 200j)))
self.assertEqual(path1, Path(CubicBezier(600 + 200j,
675 + 100j,
975 + 100j,
900 + 200j)))
path1 = parse_path('M600,500 C600,350 900,650 900,500')
self.assertEqual(path1,
Path(CubicBezier(600 + 500j, 600 + 350j, 900 + 650j, 900 + 500j)))
self.assertEqual(path1, Path(CubicBezier(600 + 500j,
600 + 350j,
900 + 650j,
900 + 500j)))
path1 = parse_path("""M600,800 C625,700 725,700 750,800
S875,900 900,800""")
self.assertEqual(path1,
Path(CubicBezier(600 + 800j, 625 + 700j, 725 + 700j, 750 + 800j),
CubicBezier(750 + 800j, 775 + 900j, 875 + 900j, 900 + 800j)))
self.assertEqual(path1, Path(CubicBezier(600 + 800j,
625 + 700j,
725 + 700j,
750 + 800j),
CubicBezier(750 + 800j,
775 + 900j,
875 + 900j,
900 + 800j)))
path1 = parse_path('M200,300 Q400,50 600,300 T1000,300')
self.assertEqual(path1,
Path(QuadraticBezier(200 + 300j, 400 + 50j, 600 + 300j),
QuadraticBezier(600 + 300j, 800 + 550j, 1000 + 300j)))
self.assertEqual(path1, Path(QuadraticBezier(200 + 300j,
400 + 50j,
600 + 300j),
QuadraticBezier(600 + 300j,
800 + 550j,
1000 + 300j)))
path1 = parse_path('M300,200 h-150 a150,150 0 1,0 150,-150 z')
self.assertEqual(path1,
Path(Line(300 + 200j, 150 + 200j),
Arc(150 + 200j, 150 + 150j, 0, 1, 0, 300 + 50j),
Line(300 + 50j, 300 + 200j)))
self.assertEqual(path1, Path(Line(300 + 200j, 150 + 200j),
Arc(150 + 200j, 150 + 150j, 0, 1, 0, 300 + 50j),
Line(300 + 50j, 300 + 200j)))
path1 = parse_path('M275,175 v-150 a150,150 0 0,0 -150,150 z')
self.assertEqual(path1,
@ -101,26 +137,32 @@ class TestParser(unittest.TestCase):
# Relative moveto:
path1 = parse_path('M 0 0 L 50 20 m 50 80 L 300 100 L 200 300 z')
self.assertEqual(path1, Path(
Line(0 + 0j, 50 + 20j),
Line(100 + 100j, 300 + 100j),
Line(300 + 100j, 200 + 300j),
Line(200 + 300j, 100 + 100j)))
self.assertEqual(path1, Path(Line(0 + 0j, 50 + 20j),
Line(100 + 100j, 300 + 100j),
Line(300 + 100j, 200 + 300j),
Line(200 + 300j, 100 + 100j)))
# Initial smooth and relative CubicBezier
path1 = parse_path("""M100,200 s 150,-100 150,0""")
self.assertEqual(path1,
Path(CubicBezier(100 + 200j, 100 + 200j, 250 + 100j, 250 + 200j)))
Path(CubicBezier(100 + 200j,
100 + 200j,
250 + 100j,
250 + 200j)))
# Initial smooth and relative QuadraticBezier
path1 = parse_path("""M100,200 t 150,0""")
self.assertEqual(path1,
Path(QuadraticBezier(100 + 200j, 100 + 200j, 250 + 200j)))
Path(QuadraticBezier(100 + 200j,
100 + 200j,
250 + 200j)))
# Relative QuadraticBezier
path1 = parse_path("""M100,200 q 0,0 150,0""")
self.assertEqual(path1,
Path(QuadraticBezier(100 + 200j, 100 + 200j, 250 + 200j)))
Path(QuadraticBezier(100 + 200j,
100 + 200j,
250 + 200j)))
def test_negative(self):
"""You don't need spaces before a minus-sign"""
@ -130,10 +172,117 @@ class TestParser(unittest.TestCase):
def test_numbers(self):
"""Exponents and other number format cases"""
# It can be e or E, the plus is optional, and a minimum of +/-3.4e38 must be supported.
# It can be e or E, the plus is optional, and a minimum of
# +/-3.4e38 must be supported.
path1 = parse_path('M-3.4e38 3.4E+38L-3.4E-38,3.4e-38')
path2 = Path(Line(-3.4e+38 + 3.4e+38j, -3.4e-38 + 3.4e-38j))
self.assertEqual(path1, path2)
def test_errors(self):
self.assertRaises(ValueError, parse_path, 'M 100 100 L 200 200 Z 100 200')
self.assertRaises(ValueError, parse_path,
'M 100 100 L 200 200 Z 100 200')
def test_transform(self):
tf_matrix = svgpathtools.parser.parse_transform(
'matrix(1.0 2.0 3.0 4.0 5.0 6.0)')
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]])
self.assertTrue(np.array_equal(expected_tf_matrix, tf_matrix))
# Try a test with no y specified
expected_tf_translate = np.identity(3)
expected_tf_translate[0, 2] = -36
self.assertTrue(np.array_equal(
expected_tf_translate,
svgpathtools.parser.parse_transform('translate(-36)')
))
# Now specify y
expected_tf_translate[1, 2] = 45.5
tf_translate = svgpathtools.parser.parse_transform(
'translate(-36 45.5)')
self.assertTrue(np.array_equal(expected_tf_translate, tf_translate))
# Try a test with no y specified
expected_tf_scale = np.identity(3)
expected_tf_scale[0, 0] = 10
expected_tf_scale[1, 1] = 10
self.assertTrue(np.array_equal(
expected_tf_scale,
svgpathtools.parser.parse_transform('scale(10)')
))
# Now specify y
expected_tf_scale[1, 1] = 0.5
tf_scale = svgpathtools.parser.parse_transform('scale(10 0.5)')
self.assertTrue(np.array_equal(expected_tf_scale, tf_scale))
tf_rotation = svgpathtools.parser.parse_transform('rotate(-10 50 100)')
expected_tf_rotation = construct_rotation_tf(-10, 50, 100)
self.assertTrue(np.array_equal(expected_tf_rotation, tf_rotation))
# Try a test with no offset specified
self.assertTrue(np.array_equal(
construct_rotation_tf(50, 0, 0),
svgpathtools.parser.parse_transform('rotate(50)')
))
expected_tf_skewx = np.identity(3)
expected_tf_skewx[0, 1] = np.tan(40.0 * np.pi/180.0)
tf_skewx = svgpathtools.parser.parse_transform('skewX(40)')
self.assertTrue(np.array_equal(expected_tf_skewx, tf_skewx))
expected_tf_skewy = np.identity(3)
expected_tf_skewy[1, 0] = np.tan(30.0 * np.pi / 180.0)
tf_skewy = svgpathtools.parser.parse_transform('skewY(30)')
self.assertTrue(np.array_equal(expected_tf_skewy, tf_skewy))
self.assertTrue(np.array_equal(
tf_rotation.dot(tf_translate).dot(tf_skewx).dot(tf_scale),
svgpathtools.parser.parse_transform(
"""rotate(-10 50 100)
translate(-36 45.5)
skewX(40)
scale(10 0.5)""")
))
def test_pathd_init(self):
path0 = Path('')
path1 = parse_path("M 100 100 L 300 100 L 200 300 z")
path2 = Path("M 100 100 L 300 100 L 200 300 z")
self.assertEqual(path1, path2)
path1 = parse_path("m 100 100 L 300 100 L 200 300 z", current_pos=50+50j)
path2 = Path("m 100 100 L 300 100 L 200 300 z")
self.assertNotEqual(path1, path2)
path1 = parse_path("m 100 100 L 300 100 L 200 300 z")
path2 = Path("m 100 100 L 300 100 L 200 300 z", current_pos=50 + 50j)
self.assertNotEqual(path1, path2)
path1 = parse_path("m 100 100 L 300 100 L 200 300 z", current_pos=50 + 50j)
path2 = Path("m 100 100 L 300 100 L 200 300 z", current_pos=50 + 50j)
self.assertEqual(path1, path2)
path1 = parse_path("m 100 100 L 300 100 L 200 300 z", 50+50j)
path2 = Path("m 100 100 L 300 100 L 200 300 z")
self.assertNotEqual(path1, path2)
path1 = parse_path("m 100 100 L 300 100 L 200 300 z")
path2 = Path("m 100 100 L 300 100 L 200 300 z", 50 + 50j)
self.assertNotEqual(path1, path2)
path1 = parse_path("m 100 100 L 300 100 L 200 300 z", 50 + 50j)
path2 = Path("m 100 100 L 300 100 L 200 300 z", 50 + 50j)
self.assertEqual(path1, path2)
def test_issue_99(self):
p = Path("M 100 250 S 200 200 200 250 300 300 300 250")
self.assertEqual(p.d(useSandT=True), 'M 100.0,250.0 S 200.0,200.0 200.0,250.0 S 300.0,300.0 300.0,250.0')
self.assertEqual(p.d(),
'M 100.0,250.0 C 100.0,250.0 200.0,200.0 200.0,250.0 C 200.0,300.0 300.0,300.0 300.0,250.0')
self.assertNotEqual(p.d(),
'M 100.0,250.0 C 100.0,250.0 200.0,200.0 200.0,250.0 C 200.0,250.0 300.0,300.0 300.0,250.0')

File diff suppressed because it is too large Load Diff

View File

@ -4,7 +4,7 @@ import unittest
import numpy as np
# Internal dependencies
from svgpathtools import *
from svgpathtools import rational_limit
class Test_polytools(unittest.TestCase):

29
test/test_sax_groups.py Normal file
View File

@ -0,0 +1,29 @@
from __future__ import division, absolute_import, print_function
import unittest
from svgpathtools import SaxDocument
from os.path import join, dirname
class TestSaxGroups(unittest.TestCase):
def check_values(self, v, z):
# Check that the components of 2D vector v match the components
# of complex number z
self.assertAlmostEqual(v[0], z.real)
self.assertAlmostEqual(v[1], z.imag)
def test_parse_display(self):
doc = SaxDocument(join(dirname(__file__), 'transforms.svg'))
# doc.display()
for i, node in enumerate(doc.tree):
values = node
path_value = values['d']
matrix = values['matrix']
self.assertTrue(values is not None)
self.assertTrue(path_value is not None)
if i == 0:
self.assertEqual(values['fill'], 'red')
if i == 8 or i == 7:
self.assertEqual(matrix, None)
if i == 9:
self.assertEqual(values['fill'], 'lime')

View File

@ -1,7 +1,16 @@
from __future__ import division, absolute_import, print_function
import unittest
from svgpathtools import *
from svgpathtools import Path, Line, Arc, svg2paths, svgstr2paths
from io import StringIO
from io import open # overrides build-in open for compatibility with python2
import os
from os.path import join, dirname
from sys import version_info
import tempfile
import shutil
from svgpathtools.svg_to_paths import rect2pathd
class TestSVG2Paths(unittest.TestCase):
def test_svg2paths_polygons(self):
@ -20,11 +29,108 @@ class TestSVG2Paths(unittest.TestCase):
# triangular quadrilateral (with a redundant 4th "closure" point)
path = paths[1]
path_correct = Path(Line(0+0j, 0+100j),
Line(0+100j, 100+100j),
Line(100+100j, 0+0j),
path_correct = Path(Line(0+0j, 0-100j),
Line(0-100j, 0.1-100j),
Line(0.1-100j, 0+0j),
Line(0+0j, 0+0j) # result of redundant point
)
self.assertTrue(path.isclosed())
self.assertTrue(len(path)==4)
self.assertTrue(path==path_correct)
def test_svg2paths_ellipses(self):
paths, _ = svg2paths(join(dirname(__file__), 'ellipse.svg'))
# ellipse tests
path_ellipse = paths[0]
path_ellipse_correct = Path(Arc(50+100j, 50+50j, 0.0, True, False, 150+100j),
Arc(150+100j, 50+50j, 0.0, True, False, 50+100j))
self.assertTrue(len(path_ellipse)==2)
self.assertTrue(path_ellipse==path_ellipse_correct)
self.assertTrue(path_ellipse.isclosed())
# circle tests
paths, _ = svg2paths(join(dirname(__file__), 'circle.svg'))
path_circle = paths[0]
path_circle_correct = Path(Arc(50+100j, 50+50j, 0.0, True, False, 150+100j),
Arc(150+100j, 50+50j, 0.0, True, False, 50+100j))
self.assertTrue(len(path_circle)==2)
self.assertTrue(path_circle==path_circle_correct)
self.assertTrue(path_circle.isclosed())
# test for issue #198 (circles not being closed)
svg = u"""<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg" width="40mm" height="40mm"
viewBox="0 0 40 40" version="1.1">
<g id="layer">
<circle id="c1" cx="20.000" cy="20.000" r="11.000" />
<circle id="c2" cx="20.000" cy="20.000" r="5.15" />
</g>
</svg>"""
tmpdir = tempfile.mkdtemp()
svgfile = os.path.join(tmpdir, 'test.svg')
with open(svgfile, 'w') as f:
f.write(svg)
paths, _ = svg2paths(svgfile)
self.assertEqual(len(paths), 2)
self.assertTrue(paths[0].isclosed())
self.assertTrue(paths[1].isclosed())
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")
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)
if __name__ == '__main__':
unittest.main()

49
test/transforms.svg Normal file
View File

@ -0,0 +1,49 @@
<?xml version="1.0" ?>
<svg
version="1.1"
viewBox="0 0 365 365"
height="100%"
width="100%"
xmlns="http://www.w3.org/2000/svg">
<g
id="matrix group"
transform="matrix(1.5 0.0 0.0 0.5 -40.0 20.0)" stroke="black" style="fill:red">
<circle cx="50" cy="50" r="40" stroke-width="3" />
<g id="scale group" transform="scale(1.25)"></g>
<g>
<path
d="M 150,200 l -50,25"
fill="black"
stroke="black"
stroke-width="3"
/>
</g>
</g>
<path id="lineAB" d="M 100 350 l 150 -300" stroke="red"
stroke-width="3" fill="none" />
<path id="lineBC" d="M 250 50 l 150 300" stroke="red"
stroke-width="3" fill="none" />
<path d="M 175 200 l 150 0" stroke="green" stroke-width="3"
fill="none" />
<path d="M 100 350 q 150 -300 300 0" stroke="blue"
stroke-width="5" fill="none" />
<!-- Mark relevant points -->
<g stroke="black" stroke-width="3" fill="black">
<circle id="pointA" cx="100" cy="350" r="3" />
<circle id="pointB" cx="250" cy="50" r="3" />
<circle id="pointC" cx="400" cy="350" r="3" />
</g>
<!-- Label the points -->
<g font-size="30" font-family="sans-serif" fill="black" stroke="none"
text-anchor="middle">
<text x="100" y="350" dx="-30">A</text>
<text x="250" y="50" dy="-10">B</text>
<text x="400" y="350" dx="30">C</text>
</g>
<g transform="scale(0.1)">
<polygon points="200,10 250,190 160,210" style="fill:lime;stroke:purple;stroke-width:1" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB