Compare commits
222 Commits
ElementTre
...
master
Author | SHA1 | Date |
---|---|---|
Andrew Port | fcb648b9bb | |
Andrew Port | ec546a71d4 | |
Andrew Port | bc930005c2 | |
Andrew Port | ae9b79e77a | |
Andrew Port | 289ee6ecb4 | |
Andrew Port | 592fe3a525 | |
Andrew Port | 81870e1f85 | |
Andrew Port | 6015a97090 | |
Andrew Port | 788b2b43a2 | |
Andrew Port | b282094b53 | |
Andrew Port | 229773ff9d | |
Kaspar Emanuel | a16a060c27 | |
Sebastian Kuzminsky | e94483510e | |
Sebastian Kuzminsky | 6abda09d1c | |
Andrew Port | 5c73056420 | |
Andrew Port | 4f5d8f3bf2 | |
Andrew Port | c4d98afc68 | |
Andrew Port | 9c69e45d6e | |
Andrew Port | dc2f6e90cc | |
Andrew Port | 96676b7697 | |
Andrew Port | 2a1cb735e9 | |
Andrew Port | 3eb21161cf | |
Andrew Port | 31b6f3dd90 | |
Andrew Port | d9515ea399 | |
Andrew Port | 944ccf5e89 | |
Tatarize | b6e5a623ea | |
Tatarize | 4c6abc5820 | |
Andrew Port | b8dfb6770a | |
Andrew Port | 0e17702d04 | |
Andrew Port | 8df19f1c12 | |
Andrew Port | 9ac7f62515 | |
Andrew Port | d8a6e5e509 | |
Andrew Port | 73c887a8a3 | |
Andrew Port | f7e074339d | |
Andrew Port | d9f5a2a781 | |
Andrew Port | 740e2bf991 | |
Andrew Port | 8cbe6f0f81 | |
Andrew Port | b82530aaac | |
Andrew Port | e8792f4d2d | |
Andrew Port | d3a66f0bbd | |
Andrew Port | 356d86df78 | |
Andrew Port | a989c9831d | |
FlyingSamson | 07f46d41f8 | |
FlyingSamson | 2fc016d48f | |
FlyingSamson | aacd5fa96d | |
FlyingSamson | db5200f460 | |
FlyingSamson | a473ee3f4c | |
FlyingSamson | 02a223c220 | |
FlyingSamson | 68e0d1f30d | |
FlyingSamson | a743e0293c | |
FlyingSamson | 1771fbfb06 | |
FlyingSamson | 33f4639bbf | |
FlyingSamson | 50b335f3da | |
FlyingSamson | ccdd10212c | |
FlyingSamson | ce43c75cd8 | |
chanicpanic | 19df25b99b | |
Andrew Port | c84c897bf2 | |
Andrew Port | ac138b8e5d | |
Andrew Port | 2dc06df20f | |
Andrew Port | 72fa3dcf17 | |
Catherine Holloway | 6c655ad220 | |
Andrew Port | 5037fac574 | |
Andrew Port | abd99f0846 | |
Andrew Port | d1421d6286 | |
Andrew Port | 8ad7458b31 | |
Andrew Port | 1b8caeec71 | |
Andrew Port | 002e691686 | |
Andrew Port | 3576591e08 | |
Andrew Port | ca094feea9 | |
Andrew Port | 3a6711a5e7 | |
Andrew Port | bbf75d0b5a | |
Andrew Port | 5b9ee30544 | |
Andrew Port | b35488efb0 | |
Andrew Port | d97519ffa3 | |
Andrew Port | 5a3bb8fca8 | |
Andrew Port | d998587a32 | |
Andrew Port | b8579b2c12 | |
Andrew Port | 05a2d271b7 | |
Andrew Port | a69898f83b | |
Andrew Port | 09ce497a4f | |
Julian Rüth | 39d3ba713f | |
Andrew Port | e4c7b53f62 | |
Andrew Port | 8f4b1fee00 | |
Andrew Port | a78ecf4290 | |
Andrew Port | 73e0ae2b21 | |
Andrew Port | da5286f79e | |
Andrew Port | 8b8ac6c9fe | |
Andrew Port | 44d08b6737 | |
Andrew Port | 60984969a7 | |
Andrew Port | 3b33445c25 | |
Andrew Port | c4b77697f2 | |
Andrew Port | 56bbba0bd1 | |
Andrew Port | 4f685e732a | |
Andrew Port | b8f4e71f5b | |
Andrew Port | e993ff95c5 | |
Andrew Port | 1b503a7b2f | |
Andrew Port | e0f212a334 | |
Andrew Port | be33f182fb | |
Andrew Port | 81ff41f881 | |
Andrew Port | 5be6b258e1 | |
Andrew Port | add170c926 | |
Andrew Port | 7d22c76e10 | |
Andrew Port | f19c473c69 | |
Andrew Port | bd55d303de | |
Andrew Port | 43c0d2d807 | |
Andrew Port | ecd39743ab | |
Andrew Port | cfc97e0ce2 | |
Andrew Port | 426ce7d56c | |
Andrew Port | 9c2e403036 | |
Andrew Port | 767413c896 | |
Andrew Port | ed207f2241 | |
Andrew Port | c5f49de5fe | |
Andrew Port | dac4600e6f | |
Andrew Port | 3d22a7cf52 | |
Andrew Port | a67de75137 | |
Andrew Port | 5ea0fb226d | |
Andrew Port | 8dc12d4efc | |
Vrroom | baba1d18b2 | |
Wes Bouaziz | 1e5bfb4252 | |
Nathan Hurst | a0fc28849c | |
tatarize | 3a1fe8695d | |
Andrew Port | 091394b5e3 | |
Andrew Port | 561c89ad47 | |
Andrew Port | ee58270f66 | |
Andrew Port | 30f517e735 | |
Andrew Port | 44e88d54e5 | |
Andrew Port | f9febbd85e | |
Andrew Port | 1c9363d426 | |
Andrew Port | 69e2e27efb | |
Andrew Port | ceffdc4a5b | |
Andrew Port | 2fb96b0906 | |
Andrew Port | 07771be9bf | |
Andrew Port | 5aeb6e3bf7 | |
Andrew Port | 0f4c9c598a | |
tatarize | b3d9544624 | |
Andrew Port | 45dc873f82 | |
Andrew Port | cae729bd48 | |
Andrew Port | 110acc9e00 | |
Andrew Port | 7a183c4e3c | |
Andrew Port | 9fa559a070 | |
Andrew Port | 1b4ed34ac9 | |
Andrew Port | 772d6698bf | |
Andrew Port | bdbd976e0a | |
Andy Port | d354b8ffe4 | |
Andy Port | 8f92e43f58 | |
Andy Port | 4342501591 | |
tatarize | 0c9dd318aa | |
Andy Port | 1a4807e929 | |
Andy Port | 12c8d07bad | |
Andy Port | 22f3dafe87 | |
Andy Port | 945ae49967 | |
tatarize | 90dfeb7b13 | |
Andy Port | c89c68f421 | |
Andy Port | ab44fcd564 | |
Andy Port | 4b7f17c7bd | |
Andy Port | 70534a6b6c | |
Andy Port | 1f7503aabd | |
Andy Port | 445899b2eb | |
Andy Port | d673176347 | |
NataliaTs | b503b9b3a5 | |
Andy Port | b714ff872d | |
Sebastian Kuzminsky | 685f9a6eaf | |
Matthew Carruth | 5ae88df6d5 | |
David Romero | 90f8f76185 | |
Grey | f99f9d6bb3 | |
Antoine Beyeler | b117f85811 | |
ugultopu | 538b8777e1 | |
Antoine Beyeler | b767536e38 | |
Sebastian Kuzminsky | 2eb8fb62ed | |
Sebastian Kuzminsky | 929202aa62 | |
taoari | c7b6c030a6 | |
Sebastian Kuzminsky | 8457dc01ee | |
skef | fd7348a1df | |
Andy | ae42197d10 | |
Sumeet P | b4e211fd79 | |
Andy Port | 58d48029ac | |
Sebastian Kuzminsky | b37e74f5f3 | |
tatarize | e91a35c3da | |
Sebastian Kuzminsky | 2feb3c92b5 | |
Andy Port | d810653b63 | |
Andy Port | 9e218b2b3b | |
Andy Port | 74a881a181 | |
Andy Port | a86be9d306 | |
Andy Port | ee8eda5aae | |
Andy Port | aa03a4aecb | |
Andy Port | 95179a6bfa | |
Andy Port | 7fa103e533 | |
Andy | f77f94db5b | |
Andy Port | 165372562e | |
Sebastian Kuzminsky | 2da39e4c02 | |
Andy Port | 4bc146fd62 | |
Andy Port | 0955278f73 | |
Andy Port | 7ebc56a831 | |
Andy Port | b1dfc9e8f7 | |
Andy Port | ee5ab1813b | |
Andy Port | 40a515ee63 | |
Andy Port | 3d1a225503 | |
Michael X. Grey | 360d6b224c | |
Andy | ccc9ee6ae1 | |
Andy | b47345da91 | |
Andy | be946b8d92 | |
Andy | 2b1670460e | |
Andy | 7fc7e45113 | |
Andy | 8d5023939e | |
Andy | fb916596e2 | |
Andy | fd521748fa | |
Andy | 2cb56c50c5 | |
Andy | 31a8d2ac11 | |
Andy | 850055fa57 | |
Andy | a2ea4a0e80 | |
Andy | fd95b5609f | |
mdejean | f932036fb5 | |
Andy Port | 18b1337877 | |
Andy Port | 55661d18a4 | |
Andy Port | 304c0bbe1d | |
Andy Port | 72d7467896 | |
Orion Elenzil | ee656c7de0 | |
Andy Port | 0827206953 | |
Andy Port | eafe3682b9 | |
Andy Port | 6394415108 | |
Orion Elenzil | 1ba9d45b35 | |
Orion Elenzil | d21a66aff0 |
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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/
|
|
@ -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 }}
|
|
@ -1,4 +1,8 @@
|
||||||
*.pyc
|
*.pyc
|
||||||
.*
|
.*
|
||||||
/svgpathtools/nonunittests
|
/svgpathtools/nonunittests
|
||||||
!/.gitignore
|
build
|
||||||
|
svgpathtools.egg-info
|
||||||
|
!.travis.yml
|
||||||
|
!/.gitignore
|
||||||
|
!/.github
|
||||||
|
|
|
@ -18,7 +18,7 @@ example**. Feel free to make a pull-request too (see relevant section below).
|
||||||
|
|
||||||
## Submitting Pull-Requests
|
## Submitting Pull-Requests
|
||||||
|
|
||||||
#### New features should come with unittests and docstrings.
|
#### New features come with unittests and docstrings.
|
||||||
If you want to add a cool/useful feature to svgpathtools, that's great! Just
|
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
|
make sure your pull-request includes both thorough unittests and well-written
|
||||||
docstrings. See relevant sections below on "Testing Style" and
|
docstrings. See relevant sections below on "Testing Style" and
|
||||||
|
@ -42,8 +42,9 @@ you want your code's variable names to match some official documentation, or
|
||||||
PEP8 guidelines contradict those present in this document).
|
PEP8 guidelines contradict those present in this document).
|
||||||
* Include docstrings and in-line comments where appropriate. See
|
* Include docstrings and in-line comments where appropriate. See
|
||||||
"Docstring Style" section below for more info.
|
"Docstring Style" section below for more info.
|
||||||
* Use explicit, uncontracted names (e.g. "parse_transform" instead of
|
* Use explicit, uncontracted names (e.g. `parse_transform` instead of
|
||||||
"parse_trafo"). The ideal names should be something a user can guess
|
`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
|
* 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`
|
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
|
if you're unsure what I mean. In the ambiguous case, use either 't' or another
|
||||||
|
|
201
README.ipynb
|
@ -2,11 +2,12 @@
|
||||||
"cells": [
|
"cells": [
|
||||||
{
|
{
|
||||||
"cell_type": "markdown",
|
"cell_type": "markdown",
|
||||||
"metadata": {
|
"metadata": {},
|
||||||
"deletable": true,
|
|
||||||
"editable": true
|
|
||||||
},
|
|
||||||
"source": [
|
"source": [
|
||||||
|
"[![Donate](https://img.shields.io/badge/donate-paypal-brightgreen)](https://www.paypal.com/donate?business=4SKJ27AM4EYYA&no_recurring=0&item_name=Support+the+creator+of+svgpathtools?++He%27s+a+student+and+would+appreciate+it.&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",
|
"# svgpathtools\n",
|
||||||
"\n",
|
"\n",
|
||||||
"svgpathtools is a collection of tools for manipulating and analyzing SVG Path objects and Bézier curves.\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",
|
"## Prerequisites\n",
|
||||||
"- **numpy**\n",
|
"- **numpy**\n",
|
||||||
"- **svgwrite**\n",
|
"- **svgwrite**\n",
|
||||||
|
"- **scipy** (optional but recommended for performance)\n",
|
||||||
"\n",
|
"\n",
|
||||||
"## Setup\n",
|
"## Setup\n",
|
||||||
"\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",
|
"```bash\n",
|
||||||
"$ pip install svgpathtools\n",
|
"$ pip install svgpathtools\n",
|
||||||
"``` \n",
|
"``` \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",
|
"You can download the source from Github and install by using the command (from inside the folder containing setup.py):\n",
|
||||||
"\n",
|
"\n",
|
||||||
"```bash\n",
|
"```bash\n",
|
||||||
|
@ -91,9 +82,7 @@
|
||||||
"cell_type": "code",
|
"cell_type": "code",
|
||||||
"execution_count": 1,
|
"execution_count": 1,
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"collapsed": true,
|
"collapsed": true
|
||||||
"deletable": true,
|
|
||||||
"editable": true
|
|
||||||
},
|
},
|
||||||
"outputs": [],
|
"outputs": [],
|
||||||
"source": [
|
"source": [
|
||||||
|
@ -103,11 +92,7 @@
|
||||||
{
|
{
|
||||||
"cell_type": "code",
|
"cell_type": "code",
|
||||||
"execution_count": 2,
|
"execution_count": 2,
|
||||||
"metadata": {
|
"metadata": {},
|
||||||
"collapsed": false,
|
|
||||||
"deletable": true,
|
|
||||||
"editable": true
|
|
||||||
},
|
|
||||||
"outputs": [
|
"outputs": [
|
||||||
{
|
{
|
||||||
"name": "stdout",
|
"name": "stdout",
|
||||||
|
@ -146,10 +131,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cell_type": "markdown",
|
"cell_type": "markdown",
|
||||||
"metadata": {
|
"metadata": {},
|
||||||
"deletable": true,
|
|
||||||
"editable": true
|
|
||||||
},
|
|
||||||
"source": [
|
"source": [
|
||||||
"The ``Path`` class is a mutable sequence, so it behaves much like a list.\n",
|
"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."
|
"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",
|
"cell_type": "code",
|
||||||
"execution_count": 3,
|
"execution_count": 3,
|
||||||
"metadata": {
|
"metadata": {},
|
||||||
"collapsed": false,
|
|
||||||
"deletable": true,
|
|
||||||
"editable": true
|
|
||||||
},
|
|
||||||
"outputs": [
|
"outputs": [
|
||||||
{
|
{
|
||||||
"name": "stdout",
|
"name": "stdout",
|
||||||
|
@ -226,10 +204,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cell_type": "markdown",
|
"cell_type": "markdown",
|
||||||
"metadata": {
|
"metadata": {},
|
||||||
"deletable": true,
|
|
||||||
"editable": true
|
|
||||||
},
|
|
||||||
"source": [
|
"source": [
|
||||||
"### Reading SVGSs\n",
|
"### Reading SVGSs\n",
|
||||||
"\n",
|
"\n",
|
||||||
|
@ -240,11 +215,7 @@
|
||||||
{
|
{
|
||||||
"cell_type": "code",
|
"cell_type": "code",
|
||||||
"execution_count": 4,
|
"execution_count": 4,
|
||||||
"metadata": {
|
"metadata": {},
|
||||||
"collapsed": false,
|
|
||||||
"deletable": true,
|
|
||||||
"editable": true
|
|
||||||
},
|
|
||||||
"outputs": [
|
"outputs": [
|
||||||
{
|
{
|
||||||
"name": "stdout",
|
"name": "stdout",
|
||||||
|
@ -277,10 +248,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cell_type": "markdown",
|
"cell_type": "markdown",
|
||||||
"metadata": {
|
"metadata": {},
|
||||||
"deletable": true,
|
|
||||||
"editable": true
|
|
||||||
},
|
|
||||||
"source": [
|
"source": [
|
||||||
"### Writing SVGSs (and some geometric functions and methods)\n",
|
"### Writing SVGSs (and some geometric functions and methods)\n",
|
||||||
"\n",
|
"\n",
|
||||||
|
@ -291,11 +259,7 @@
|
||||||
{
|
{
|
||||||
"cell_type": "code",
|
"cell_type": "code",
|
||||||
"execution_count": 5,
|
"execution_count": 5,
|
||||||
"metadata": {
|
"metadata": {},
|
||||||
"collapsed": false,
|
|
||||||
"deletable": true,
|
|
||||||
"editable": true
|
|
||||||
},
|
|
||||||
"outputs": [],
|
"outputs": [],
|
||||||
"source": [
|
"source": [
|
||||||
"# Let's make a new SVG that's identical to the first\n",
|
"# Let's make a new SVG that's identical to the first\n",
|
||||||
|
@ -304,20 +268,14 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cell_type": "markdown",
|
"cell_type": "markdown",
|
||||||
"metadata": {
|
"metadata": {},
|
||||||
"deletable": true,
|
|
||||||
"editable": true
|
|
||||||
},
|
|
||||||
"source": [
|
"source": [
|
||||||
"![output1.svg](output1.svg)"
|
"![output1.svg](output1.svg)"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cell_type": "markdown",
|
"cell_type": "markdown",
|
||||||
"metadata": {
|
"metadata": {},
|
||||||
"deletable": true,
|
|
||||||
"editable": true
|
|
||||||
},
|
|
||||||
"source": [
|
"source": [
|
||||||
"There will be many more examples of writing and displaying path data below.\n",
|
"There will be many more examples of writing and displaying path data below.\n",
|
||||||
"\n",
|
"\n",
|
||||||
|
@ -334,11 +292,7 @@
|
||||||
{
|
{
|
||||||
"cell_type": "code",
|
"cell_type": "code",
|
||||||
"execution_count": 6,
|
"execution_count": 6,
|
||||||
"metadata": {
|
"metadata": {},
|
||||||
"collapsed": false,
|
|
||||||
"deletable": true,
|
|
||||||
"editable": true
|
|
||||||
},
|
|
||||||
"outputs": [
|
"outputs": [
|
||||||
{
|
{
|
||||||
"name": "stdout",
|
"name": "stdout",
|
||||||
|
@ -374,10 +328,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cell_type": "markdown",
|
"cell_type": "markdown",
|
||||||
"metadata": {
|
"metadata": {},
|
||||||
"deletable": true,
|
|
||||||
"editable": true
|
|
||||||
},
|
|
||||||
"source": [
|
"source": [
|
||||||
"### Bezier curves as NumPy polynomial objects\n",
|
"### 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",
|
"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",
|
"cell_type": "code",
|
||||||
"execution_count": 7,
|
"execution_count": 7,
|
||||||
"metadata": {
|
"metadata": {},
|
||||||
"collapsed": false,
|
|
||||||
"deletable": true,
|
|
||||||
"editable": true
|
|
||||||
},
|
|
||||||
"outputs": [
|
"outputs": [
|
||||||
{
|
{
|
||||||
"name": "stdout",
|
"name": "stdout",
|
||||||
|
@ -442,10 +389,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cell_type": "markdown",
|
"cell_type": "markdown",
|
||||||
"metadata": {
|
"metadata": {},
|
||||||
"deletable": true,
|
|
||||||
"editable": true
|
|
||||||
},
|
|
||||||
"source": [
|
"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",
|
"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",
|
"\n",
|
||||||
|
@ -461,11 +405,7 @@
|
||||||
{
|
{
|
||||||
"cell_type": "code",
|
"cell_type": "code",
|
||||||
"execution_count": 8,
|
"execution_count": 8,
|
||||||
"metadata": {
|
"metadata": {},
|
||||||
"collapsed": false,
|
|
||||||
"deletable": true,
|
|
||||||
"editable": true
|
|
||||||
},
|
|
||||||
"outputs": [
|
"outputs": [
|
||||||
{
|
{
|
||||||
"name": "stdout",
|
"name": "stdout",
|
||||||
|
@ -510,10 +450,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cell_type": "markdown",
|
"cell_type": "markdown",
|
||||||
"metadata": {
|
"metadata": {},
|
||||||
"deletable": true,
|
|
||||||
"editable": true
|
|
||||||
},
|
|
||||||
"source": [
|
"source": [
|
||||||
"### Translations (shifts), reversing orientation, and normal vectors"
|
"### Translations (shifts), reversing orientation, and normal vectors"
|
||||||
]
|
]
|
||||||
|
@ -521,11 +458,7 @@
|
||||||
{
|
{
|
||||||
"cell_type": "code",
|
"cell_type": "code",
|
||||||
"execution_count": 9,
|
"execution_count": 9,
|
||||||
"metadata": {
|
"metadata": {},
|
||||||
"collapsed": false,
|
|
||||||
"deletable": true,
|
|
||||||
"editable": true
|
|
||||||
},
|
|
||||||
"outputs": [],
|
"outputs": [],
|
||||||
"source": [
|
"source": [
|
||||||
"# Speaking of tangents, let's add a normal vector to the picture\n",
|
"# Speaking of tangents, let's add a normal vector to the picture\n",
|
||||||
|
@ -551,20 +484,14 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cell_type": "markdown",
|
"cell_type": "markdown",
|
||||||
"metadata": {
|
"metadata": {},
|
||||||
"deletable": true,
|
|
||||||
"editable": true
|
|
||||||
},
|
|
||||||
"source": [
|
"source": [
|
||||||
"![vectorframes.svg](vectorframes.svg)"
|
"![vectorframes.svg](vectorframes.svg)"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cell_type": "markdown",
|
"cell_type": "markdown",
|
||||||
"metadata": {
|
"metadata": {},
|
||||||
"deletable": true,
|
|
||||||
"editable": true
|
|
||||||
},
|
|
||||||
"source": [
|
"source": [
|
||||||
"### Rotations and Translations"
|
"### Rotations and Translations"
|
||||||
]
|
]
|
||||||
|
@ -572,11 +499,7 @@
|
||||||
{
|
{
|
||||||
"cell_type": "code",
|
"cell_type": "code",
|
||||||
"execution_count": 10,
|
"execution_count": 10,
|
||||||
"metadata": {
|
"metadata": {},
|
||||||
"collapsed": false,
|
|
||||||
"deletable": true,
|
|
||||||
"editable": true
|
|
||||||
},
|
|
||||||
"outputs": [],
|
"outputs": [],
|
||||||
"source": [
|
"source": [
|
||||||
"# Let's take a Line and an Arc and make some pictures\n",
|
"# Let's take a Line and an Arc and make some pictures\n",
|
||||||
|
@ -599,20 +522,14 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cell_type": "markdown",
|
"cell_type": "markdown",
|
||||||
"metadata": {
|
"metadata": {},
|
||||||
"deletable": true,
|
|
||||||
"editable": true
|
|
||||||
},
|
|
||||||
"source": [
|
"source": [
|
||||||
"![decorated_ellipse.svg](decorated_ellipse.svg)"
|
"![decorated_ellipse.svg](decorated_ellipse.svg)"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cell_type": "markdown",
|
"cell_type": "markdown",
|
||||||
"metadata": {
|
"metadata": {},
|
||||||
"deletable": true,
|
|
||||||
"editable": true
|
|
||||||
},
|
|
||||||
"source": [
|
"source": [
|
||||||
"### arc length and inverse arc length\n",
|
"### arc length and inverse arc length\n",
|
||||||
"\n",
|
"\n",
|
||||||
|
@ -622,11 +539,7 @@
|
||||||
{
|
{
|
||||||
"cell_type": "code",
|
"cell_type": "code",
|
||||||
"execution_count": 11,
|
"execution_count": 11,
|
||||||
"metadata": {
|
"metadata": {},
|
||||||
"collapsed": false,
|
|
||||||
"deletable": true,
|
|
||||||
"editable": true
|
|
||||||
},
|
|
||||||
"outputs": [],
|
"outputs": [],
|
||||||
"source": [
|
"source": [
|
||||||
"# First we'll load the path data from the file test.svg\n",
|
"# First we'll load the path data from the file test.svg\n",
|
||||||
|
@ -664,20 +577,14 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cell_type": "markdown",
|
"cell_type": "markdown",
|
||||||
"metadata": {
|
"metadata": {},
|
||||||
"deletable": true,
|
|
||||||
"editable": true
|
|
||||||
},
|
|
||||||
"source": [
|
"source": [
|
||||||
"![output2.svg](output2.svg)"
|
"![output2.svg](output2.svg)"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cell_type": "markdown",
|
"cell_type": "markdown",
|
||||||
"metadata": {
|
"metadata": {},
|
||||||
"deletable": true,
|
|
||||||
"editable": true
|
|
||||||
},
|
|
||||||
"source": [
|
"source": [
|
||||||
"### Intersections between Bezier curves"
|
"### Intersections between Bezier curves"
|
||||||
]
|
]
|
||||||
|
@ -685,11 +592,7 @@
|
||||||
{
|
{
|
||||||
"cell_type": "code",
|
"cell_type": "code",
|
||||||
"execution_count": 12,
|
"execution_count": 12,
|
||||||
"metadata": {
|
"metadata": {},
|
||||||
"collapsed": false,
|
|
||||||
"deletable": true,
|
|
||||||
"editable": true
|
|
||||||
},
|
|
||||||
"outputs": [],
|
"outputs": [],
|
||||||
"source": [
|
"source": [
|
||||||
"# Let's find all intersections between redpath and the other \n",
|
"# Let's find all intersections between redpath and the other \n",
|
||||||
|
@ -706,20 +609,14 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cell_type": "markdown",
|
"cell_type": "markdown",
|
||||||
"metadata": {
|
"metadata": {},
|
||||||
"deletable": true,
|
|
||||||
"editable": true
|
|
||||||
},
|
|
||||||
"source": [
|
"source": [
|
||||||
"![output_intersections.svg](output_intersections.svg)"
|
"![output_intersections.svg](output_intersections.svg)"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cell_type": "markdown",
|
"cell_type": "markdown",
|
||||||
"metadata": {
|
"metadata": {},
|
||||||
"deletable": true,
|
|
||||||
"editable": true
|
|
||||||
},
|
|
||||||
"source": [
|
"source": [
|
||||||
"### An Advanced Application: Offsetting Paths\n",
|
"### An Advanced Application: Offsetting Paths\n",
|
||||||
"Here we'll find the [offset curve](https://en.wikipedia.org/wiki/Parallel_curve) for a few paths."
|
"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",
|
"cell_type": "code",
|
||||||
"execution_count": 13,
|
"execution_count": 13,
|
||||||
"metadata": {
|
"metadata": {},
|
||||||
"collapsed": false,
|
|
||||||
"deletable": true,
|
|
||||||
"editable": true
|
|
||||||
},
|
|
||||||
"outputs": [],
|
"outputs": [],
|
||||||
"source": [
|
"source": [
|
||||||
"from svgpathtools import parse_path, Line, Path, wsvg\n",
|
"from svgpathtools import parse_path, Line, Path, wsvg\n",
|
||||||
|
@ -772,20 +665,14 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cell_type": "markdown",
|
"cell_type": "markdown",
|
||||||
"metadata": {
|
"metadata": {},
|
||||||
"deletable": true,
|
|
||||||
"editable": true
|
|
||||||
},
|
|
||||||
"source": [
|
"source": [
|
||||||
"![offset_curves.svg](offset_curves.svg)"
|
"![offset_curves.svg](offset_curves.svg)"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cell_type": "markdown",
|
"cell_type": "markdown",
|
||||||
"metadata": {
|
"metadata": {},
|
||||||
"deletable": true,
|
|
||||||
"editable": true
|
|
||||||
},
|
|
||||||
"source": [
|
"source": [
|
||||||
"## Compatibility Notes for users of svg.path (v2.0)\n",
|
"## Compatibility Notes for users of svg.path (v2.0)\n",
|
||||||
"\n",
|
"\n",
|
||||||
|
@ -806,9 +693,7 @@
|
||||||
"cell_type": "code",
|
"cell_type": "code",
|
||||||
"execution_count": null,
|
"execution_count": null,
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"collapsed": true,
|
"collapsed": true
|
||||||
"deletable": true,
|
|
||||||
"editable": true
|
|
||||||
},
|
},
|
||||||
"outputs": [],
|
"outputs": [],
|
||||||
"source": []
|
"source": []
|
||||||
|
@ -823,16 +708,16 @@
|
||||||
"language_info": {
|
"language_info": {
|
||||||
"codemirror_mode": {
|
"codemirror_mode": {
|
||||||
"name": "ipython",
|
"name": "ipython",
|
||||||
"version": 2
|
"version": 3
|
||||||
},
|
},
|
||||||
"file_extension": ".py",
|
"file_extension": ".py",
|
||||||
"mimetype": "text/x-python",
|
"mimetype": "text/x-python",
|
||||||
"name": "python",
|
"name": "python",
|
||||||
"nbconvert_exporter": "python",
|
"nbconvert_exporter": "python",
|
||||||
"pygments_lexer": "ipython2",
|
"pygments_lexer": "ipython3",
|
||||||
"version": "2.7.12"
|
"version": "3.7.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"nbformat": 4,
|
"nbformat": 4,
|
||||||
"nbformat_minor": 0
|
"nbformat_minor": 1
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,515 @@
|
||||||
|
[![Donate](https://img.shields.io/badge/donate-paypal-brightgreen)](https://www.paypal.com/donate?business=4SKJ27AM4EYYA&no_recurring=0&item_name=Support+the+creator+of+svgpathtools?++He%27s+a+student+and+would+appreciate+it.&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
|
||||||
|
|
||||||
|
```
|
635
README.rst
|
@ -1,635 +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:
|
|
||||||
for k in range(steps):
|
|
||||||
t = k / float(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.
|
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
# Security Policy
|
||||||
|
|
||||||
|
## Reporting a Vulnerability
|
||||||
|
|
||||||
|
To report any security vulnerability, email andyaport@gmail.com
|
|
@ -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
|
|
|
@ -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
|
|
||||||
|
|
|
@ -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)
|
|
|
@ -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.")
|
|
|
@ -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
|
|
|
@ -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)
|
|
|
@ -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#########################################################################
|
|
||||||
|
|
||||||
|
|
|
@ -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)
|
|
|
@ -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)
|
|
|
@ -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)
|
|
|
@ -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 |
|
@ -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&no_recurring=0&item_name=Support+the+creator+of+svgpathtools?++He%27s+a+student+and+would+appreciate+it.&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 |
|
@ -6,9 +6,9 @@ easily be generalized to paths containing `Line` and `QuadraticBezier` objects
|
||||||
also.
|
also.
|
||||||
Note: The relevant matrix transformation for quadratics can be found in the
|
Note: The relevant matrix transformation for quadratics can be found in the
|
||||||
svgpathtools.bezier module."""
|
svgpathtools.bezier module."""
|
||||||
|
from __future__ import print_function
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from svgpathtools import *
|
from svgpathtools import bezier_point, Path, bpoints2bezier, polynomial2bezier
|
||||||
|
|
||||||
|
|
||||||
class HigherOrderBezier:
|
class HigherOrderBezier:
|
||||||
|
@ -38,10 +38,10 @@ def points_in_each_seg_slow(path, tvals):
|
||||||
|
|
||||||
def points_in_each_seg(path, tvals):
|
def points_in_each_seg(path, tvals):
|
||||||
"""Compute seg.point(t) for each seg in path and each t in 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
|
A = np.array([[-1, 3, -3, 1], # transforms cubic bez to standard poly
|
||||||
[ 3, -6, 3, 0],
|
[ 3, -6, 3, 0],
|
||||||
[-3, 3, 0, 0],
|
[-3, 3, 0, 0],
|
||||||
[ 1, 0, 0, 0]])
|
[ 1, 0, 0, 0]])
|
||||||
B = [seg.bpoints() for seg in path]
|
B = [seg.bpoints() for seg in path]
|
||||||
return np.dot(B, np.dot(A, np.power(tvals, [[3],[2],[1],[0]])))
|
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 = points_in_each_seg(testpath, tvals)
|
||||||
pts_check = points_in_each_seg_slow(testpath, tvals)
|
pts_check = points_in_each_seg_slow(testpath, tvals)
|
||||||
print np.max(pts - pts_check)
|
print(np.max(pts - pts_check))
|
||||||
|
|
|
@ -7,7 +7,8 @@ Path.continuous_subpaths() method to split a paths into a list of its
|
||||||
continuous subpaths.
|
continuous subpaths.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from svgpathtools import *
|
from svgpathtools import Path, Line
|
||||||
|
|
||||||
|
|
||||||
def path1_is_contained_in_path2(path1, path2):
|
def path1_is_contained_in_path2(path1, path2):
|
||||||
assert path2.isclosed() # This question isn't well-defined otherwise
|
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
|
# find a point that's definitely outside path2
|
||||||
xmin, xmax, ymin, ymax = path2.bbox()
|
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
|
a = path1.start # pick an arbitrary point in path1
|
||||||
AB_line = Path(Line(A, B))
|
ab_line = Path(Line(a, b))
|
||||||
number_of_intersections = len(AB_line.intersect(path2))
|
number_of_intersections = len(ab_line.intersect(path2))
|
||||||
if number_of_intersections % 2: # if number of intersections is odd
|
if number_of_intersections % 2: # if number of intersections is odd
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
|
|
|
@ -1,13 +1,16 @@
|
||||||
from svgpathtools import *
|
from svgpathtools import disvg, Line, CubicBezier
|
||||||
|
from scipy.optimize import fminbound
|
||||||
|
|
||||||
# create some example paths
|
# create some example paths
|
||||||
path1 = CubicBezier(1,2+3j,3-5j,4+1j)
|
path1 = CubicBezier(1,2+3j,3-5j,4+1j)
|
||||||
path2 = path1.rotated(60).translated(3)
|
path2 = path1.rotated(60).translated(3)
|
||||||
|
|
||||||
# find minimizer
|
|
||||||
from scipy.optimize import fminbound
|
|
||||||
def dist(t):
|
def dist(t):
|
||||||
return path1.radialrange(path2.point(t))[0][0]
|
return path1.radialrange(path2.point(t))[0][0]
|
||||||
|
|
||||||
|
|
||||||
|
# find minimizer
|
||||||
T2 = fminbound(dist, 0, 1)
|
T2 = fminbound(dist, 0, 1)
|
||||||
|
|
||||||
# Let's do a visual check
|
# Let's do a visual check
|
||||||
|
|
|
@ -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>
|
|
@ -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 |
|
@ -1,2 +1,3 @@
|
||||||
numpy
|
numpy
|
||||||
svgwrite
|
svgwrite
|
||||||
|
scipy
|
39
setup.py
|
@ -3,18 +3,17 @@ import codecs
|
||||||
import os
|
import os
|
||||||
|
|
||||||
|
|
||||||
VERSION = '1.3.2'
|
VERSION = '1.6.1'
|
||||||
AUTHOR_NAME = 'Andy Port'
|
AUTHOR_NAME = 'Andy Port'
|
||||||
AUTHOR_EMAIL = 'AndyAPort@gmail.com'
|
AUTHOR_EMAIL = 'AndyAPort@gmail.com'
|
||||||
|
GITHUB = 'https://github.com/mathandy/svgpathtools'
|
||||||
|
|
||||||
|
_here = os.path.abspath(os.path.dirname(__file__))
|
||||||
|
|
||||||
|
|
||||||
def read(*parts):
|
def read(relative_path):
|
||||||
"""
|
"""Reads file at relative path, returning contents as string."""
|
||||||
Build an absolute path from *parts* and and return the contents of the
|
with codecs.open(os.path.join(_here, relative_path), "rb", "utf-8") as f:
|
||||||
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:
|
|
||||||
return f.read()
|
return f.read()
|
||||||
|
|
||||||
|
|
||||||
|
@ -23,26 +22,32 @@ setup(name='svgpathtools',
|
||||||
version=VERSION,
|
version=VERSION,
|
||||||
description=('A collection of tools for manipulating and analyzing SVG '
|
description=('A collection of tools for manipulating and analyzing SVG '
|
||||||
'Path objects and Bezier curves.'),
|
'Path objects and Bezier curves.'),
|
||||||
long_description=read("README.rst"),
|
long_description=read("README.md"),
|
||||||
# long_description=open('README.rst').read(),
|
long_description_content_type='text/markdown',
|
||||||
author=AUTHOR_NAME,
|
author=AUTHOR_NAME,
|
||||||
author_email=AUTHOR_EMAIL,
|
author_email=AUTHOR_EMAIL,
|
||||||
url='https://github.com/mathandy/svgpathtools',
|
url=GITHUB,
|
||||||
download_url = 'http://github.com/mathandy/svgpathtools/tarball/'+VERSION,
|
download_url='{}/releases/download/{}/svgpathtools-{}-py2.py3-none-any.whl'
|
||||||
|
''.format(GITHUB, VERSION, VERSION),
|
||||||
license='MIT',
|
license='MIT',
|
||||||
|
install_requires=['numpy', 'svgwrite', 'scipy'],
|
||||||
install_requires=['numpy', 'svgwrite'],
|
|
||||||
platforms="OS Independent",
|
platforms="OS Independent",
|
||||||
# test_suite='tests',
|
|
||||||
requires=['numpy', 'svgwrite'],
|
|
||||||
keywords=['svg', 'svg path', 'svg.path', 'bezier', 'parse svg path', 'display svg'],
|
keywords=['svg', 'svg path', 'svg.path', 'bezier', 'parse svg path', 'display svg'],
|
||||||
classifiers = [
|
classifiers=[
|
||||||
"Development Status :: 4 - Beta",
|
"Development Status :: 4 - Beta",
|
||||||
"Intended Audience :: Developers",
|
"Intended Audience :: Developers",
|
||||||
"License :: OSI Approved :: MIT License",
|
"License :: OSI Approved :: MIT License",
|
||||||
"Operating System :: OS Independent",
|
"Operating System :: OS Independent",
|
||||||
"Programming Language :: Python :: 2",
|
"Programming Language :: Python :: 2",
|
||||||
"Programming Language :: Python :: 3",
|
"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 :: Multimedia :: Graphics :: Editors :: Vector-Based",
|
||||||
"Topic :: Scientific/Engineering",
|
"Topic :: Scientific/Engineering",
|
||||||
"Topic :: Scientific/Engineering :: Image Recognition",
|
"Topic :: Scientific/Engineering :: Image Recognition",
|
||||||
|
|
|
@ -1,664 +0,0 @@
|
||||||
Metadata-Version: 1.1
|
|
||||||
Name: svgpathtools
|
|
||||||
Version: 1.3.2
|
|
||||||
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.2
|
|
||||||
Description-Content-Type: UNKNOWN
|
|
||||||
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
|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
|
|
||||||
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
|
|
|
@ -1,40 +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/misctools.py
|
|
||||||
svgpathtools/parser.py
|
|
||||||
svgpathtools/path.py
|
|
||||||
svgpathtools/paths2svg.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/requires.txt
|
|
||||||
svgpathtools.egg-info/top_level.txt
|
|
||||||
test/circle.svg
|
|
||||||
test/ellipse.svg
|
|
||||||
test/polygons.svg
|
|
||||||
test/rects.svg
|
|
||||||
test/test.svg
|
|
||||||
test/test_bezier.py
|
|
||||||
test/test_generation.py
|
|
||||||
test/test_parsing.py
|
|
||||||
test/test_path.py
|
|
||||||
test/test_polytools.py
|
|
||||||
test/test_svg2paths.py
|
|
|
@ -1 +0,0 @@
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||
svgpathtools
|
|
|
@ -8,12 +8,15 @@ from .path import (Path, Line, QuadraticBezier, CubicBezier, Arc,
|
||||||
closest_point_in_path, farthest_point_in_path,
|
closest_point_in_path, farthest_point_in_path,
|
||||||
path_encloses_pt, bbox2path, polygon, polyline)
|
path_encloses_pt, bbox2path, polygon, polyline)
|
||||||
from .parser import parse_path
|
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 .polytools import polyroots, polyroots01, rational_limit, real, imag
|
||||||
from .misctools import hex2rgb, rgb2hex
|
from .misctools import hex2rgb, rgb2hex
|
||||||
from .smoothing import smoothed_path, smoothed_joint, is_differentiable, kinks
|
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:
|
try:
|
||||||
from .svg2paths import svg2paths, svg2paths2
|
from .svg_to_paths import svg2paths, svg2paths2, svgstr2paths
|
||||||
except ImportError:
|
except ImportError:
|
||||||
pass
|
pass
|
||||||
|
|
|
@ -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)
|
|
@ -31,7 +31,7 @@ def rgb2hex(rgb):
|
||||||
>>> rgb2hex((0,0,255))
|
>>> rgb2hex((0,0,255))
|
||||||
'#0000FF'
|
'#0000FF'
|
||||||
"""
|
"""
|
||||||
return ('#%02x%02x%02x' % rgb).upper()
|
return ('#%02x%02x%02x' % tuple(rgb)).upper()
|
||||||
|
|
||||||
|
|
||||||
def isclose(a, b, rtol=1e-5, atol=1e-8):
|
def isclose(a, b, rtol=1e-5, atol=1e-8):
|
||||||
|
|
|
@ -1,196 +1,110 @@
|
||||||
"""This submodule contains the path_parse() function used to convert SVG path
|
"""This submodule contains the path_parse() function used to convert SVG path
|
||||||
element d-strings into svgpathtools Path objects.
|
element d-strings into svgpathtools Path objects.
|
||||||
Note: This file was taken (nearly) as is from the svg.path module
|
Note: This file was taken (nearly) as is from the svg.path module (v 2.0)."""
|
||||||
(v 2.0)."""
|
|
||||||
|
|
||||||
# External dependencies
|
# External dependencies
|
||||||
from __future__ import division, absolute_import, print_function
|
from __future__ import division, absolute_import, print_function
|
||||||
import re
|
import numpy as np
|
||||||
|
import warnings
|
||||||
|
|
||||||
# Internal dependencies
|
# Internal dependencies
|
||||||
from .path import Path, Line, QuadraticBezier, CubicBezier, Arc
|
from .path import Path
|
||||||
|
|
||||||
|
|
||||||
COMMANDS = set('MmZzLlHhVvCcSsQqTtAa')
|
def parse_path(pathdef, current_pos=0j, tree_element=None):
|
||||||
UPPERCASE = set('MZLHVCSQTA')
|
return Path(pathdef, current_pos=current_pos, tree_element=tree_element)
|
||||||
|
|
||||||
COMMAND_RE = re.compile("([MmZzLlHhVvCcSsQqTtAa])")
|
|
||||||
FLOAT_RE = re.compile("[-+]?[0-9]*\.?[0-9]+(?:[eE][-+]?[0-9]+)?")
|
|
||||||
|
|
||||||
|
|
||||||
def _tokenize_path(pathdef):
|
def _check_num_parsed_values(values, allowed):
|
||||||
for x in COMMAND_RE.split(pathdef):
|
if not any(num == len(values) for num in allowed):
|
||||||
if x in COMMANDS:
|
if len(allowed) > 1:
|
||||||
yield x
|
warnings.warn('Expected one of the following number of values {0}, but found {1} values instead: {2}'
|
||||||
for token in FLOAT_RE.findall(x):
|
.format(allowed, len(values), values))
|
||||||
yield token
|
elif allowed[0] != 1:
|
||||||
|
warnings.warn('Expected {0} values, found {1}: {2}'.format(allowed[0], len(values), values))
|
||||||
|
|
||||||
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:
|
else:
|
||||||
# If this element starts with numbers, it is an implicit command
|
warnings.warn('Expected 1 value, found {0}: {1}'.format(len(values), values))
|
||||||
# and we don't change the command. Check that it's allowed:
|
return False
|
||||||
if command is None:
|
return True
|
||||||
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
|
def _parse_transform_substr(transform_substr):
|
||||||
# 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.
|
type_str, value_str = transform_substr.split('(')
|
||||||
# So we set command to lineto here, in case there are
|
value_str = value_str.replace(',', ' ')
|
||||||
# further implicit commands after this moveto.
|
values = list(map(float, filter(None, value_str.split(' '))))
|
||||||
command = 'L'
|
|
||||||
|
|
||||||
elif command == 'Z':
|
transform = np.identity(3)
|
||||||
# Close path
|
if 'matrix' in type_str:
|
||||||
if not (current_pos == start_pos):
|
if not _check_num_parsed_values(values, [6]):
|
||||||
segments.append(Line(current_pos, start_pos))
|
return transform
|
||||||
segments.closed = True
|
|
||||||
current_pos = start_pos
|
|
||||||
start_pos = None
|
|
||||||
command = None # You can't have implicit commands after closing.
|
|
||||||
|
|
||||||
elif command == 'L':
|
transform[0:2, 0:3] = np.array([values[0:6:2], values[1:6:2]])
|
||||||
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':
|
elif 'translate' in transform_substr:
|
||||||
x = elements.pop()
|
if not _check_num_parsed_values(values, [1, 2]):
|
||||||
pos = float(x) + current_pos.imag * 1j
|
return transform
|
||||||
if not absolute:
|
|
||||||
pos += current_pos.real
|
|
||||||
segments.append(Line(current_pos, pos))
|
|
||||||
current_pos = pos
|
|
||||||
|
|
||||||
elif command == 'V':
|
transform[0, 2] = values[0]
|
||||||
y = elements.pop()
|
if len(values) > 1:
|
||||||
pos = current_pos.real + float(y) * 1j
|
transform[1, 2] = values[1]
|
||||||
if not absolute:
|
|
||||||
pos += current_pos.imag * 1j
|
|
||||||
segments.append(Line(current_pos, pos))
|
|
||||||
current_pos = pos
|
|
||||||
|
|
||||||
elif command == 'C':
|
elif 'scale' in transform_substr:
|
||||||
control1 = float(elements.pop()) + float(elements.pop()) * 1j
|
if not _check_num_parsed_values(values, [1, 2]):
|
||||||
control2 = float(elements.pop()) + float(elements.pop()) * 1j
|
return transform
|
||||||
end = float(elements.pop()) + float(elements.pop()) * 1j
|
|
||||||
|
|
||||||
if not absolute:
|
x_scale = values[0]
|
||||||
control1 += current_pos
|
y_scale = values[1] if (len(values) > 1) else x_scale
|
||||||
control2 += current_pos
|
transform[0, 0] = x_scale
|
||||||
end += current_pos
|
transform[1, 1] = y_scale
|
||||||
|
|
||||||
segments.append(CubicBezier(current_pos, control1, control2, end))
|
elif 'rotate' in transform_substr:
|
||||||
current_pos = end
|
if not _check_num_parsed_values(values, [1, 3]):
|
||||||
|
return transform
|
||||||
|
|
||||||
elif command == 'S':
|
angle = values[0] * np.pi / 180.0
|
||||||
# Smooth curve. First control point is the "reflection" of
|
if len(values) == 3:
|
||||||
# the second control point in the previous path.
|
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':
|
transform = tf_offset.dot(tf_rotate).dot(tf_offset_neg)
|
||||||
# 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
|
elif 'skewX' in transform_substr:
|
||||||
end = float(elements.pop()) + float(elements.pop()) * 1j
|
if not _check_num_parsed_values(values, [1]):
|
||||||
|
return transform
|
||||||
|
|
||||||
if not absolute:
|
transform[0, 1] = np.tan(values[0] * np.pi / 180.0)
|
||||||
control2 += current_pos
|
|
||||||
end += current_pos
|
|
||||||
|
|
||||||
segments.append(CubicBezier(current_pos, control1, control2, end))
|
elif 'skewY' in transform_substr:
|
||||||
current_pos = end
|
if not _check_num_parsed_values(values, [1]):
|
||||||
|
return transform
|
||||||
|
|
||||||
elif command == 'Q':
|
transform[1, 0] = np.tan(values[0] * np.pi / 180.0)
|
||||||
control = float(elements.pop()) + float(elements.pop()) * 1j
|
else:
|
||||||
end = float(elements.pop()) + float(elements.pop()) * 1j
|
# 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:
|
return transform
|
||||||
control += current_pos
|
|
||||||
end += current_pos
|
|
||||||
|
|
||||||
segments.append(QuadraticBezier(current_pos, control, end))
|
|
||||||
current_pos = end
|
|
||||||
|
|
||||||
elif command == 'T':
|
def parse_transform(transform_str):
|
||||||
# Smooth curve. Control point is the "reflection" of
|
"""Converts a valid SVG transformation string into a 3x3 matrix.
|
||||||
# the second control point in the previous path.
|
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':
|
total_transform = np.identity(3)
|
||||||
# If there is no previous command or if the previous command
|
transform_substrs = transform_str.split(')')[:-1] # Skip the last element, because it should be empty
|
||||||
# was not an Q, q, T or t, assume the first control point is
|
for substr in transform_substrs:
|
||||||
# coincident with the current point.
|
total_transform = total_transform.dot(_parse_transform_substr(substr))
|
||||||
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
|
return total_transform
|
||||||
|
|
||||||
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
|
|
||||||
|
|
1563
svgpathtools/path.py
|
@ -1,20 +1,24 @@
|
||||||
"""This submodule contains tools for creating svg files from paths and path
|
"""This submodule: basic tools for creating svg files from path data.
|
||||||
segments."""
|
|
||||||
|
See also the document.py submodule.
|
||||||
|
"""
|
||||||
|
|
||||||
# External dependencies:
|
# External dependencies:
|
||||||
from __future__ import division, absolute_import, print_function
|
from __future__ import division, absolute_import, print_function
|
||||||
from math import ceil
|
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 xml.dom.minidom import parse as md_xml_parse
|
||||||
from svgwrite import Drawing, text as txt
|
from svgwrite import Drawing, text as txt
|
||||||
from time import time
|
from time import time
|
||||||
from warnings import warn
|
from warnings import warn
|
||||||
|
import re
|
||||||
|
|
||||||
# Internal dependencies
|
# Internal dependencies
|
||||||
from .path import Path, Line, is_path_segment
|
from .path import Path, Line, is_path_segment
|
||||||
from .misctools import open_in_browser
|
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',
|
color_dict = {'a': 'aqua',
|
||||||
'b': 'blue',
|
'b': 'blue',
|
||||||
'c': 'cyan',
|
'c': 'cyan',
|
||||||
|
@ -57,8 +61,16 @@ def is3tuple(c):
|
||||||
|
|
||||||
|
|
||||||
def big_bounding_box(paths_n_stuff):
|
def big_bounding_box(paths_n_stuff):
|
||||||
"""Finds a BB containing a collection of paths, Bezier path segments, and
|
"""returns minimal upright bounding box.
|
||||||
points (given as complex numbers)."""
|
|
||||||
|
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 = []
|
bbs = []
|
||||||
for thing in paths_n_stuff:
|
for thing in paths_n_stuff:
|
||||||
if is_path_segment(thing) or isinstance(thing, Path):
|
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,
|
bbs.append((complexthing.real, complexthing.real,
|
||||||
complexthing.imag, complexthing.imag))
|
complexthing.imag, complexthing.imag))
|
||||||
except ValueError:
|
except ValueError:
|
||||||
raise TypeError(
|
raise TypeError("paths_n_stuff can only contains Path, "
|
||||||
"paths_n_stuff can only contains Path, CubicBezier, "
|
"CubicBezier, QuadraticBezier, Line, "
|
||||||
"QuadraticBezier, Line, and complex objects.")
|
"and complex objects.")
|
||||||
xmins, xmaxs, ymins, ymaxs = list(zip(*bbs))
|
xmins, xmaxs, ymins, ymaxs = list(zip(*bbs))
|
||||||
xmin = min(xmins)
|
xmin = min(xmins)
|
||||||
xmax = max(xmaxs)
|
xmax = max(xmaxs)
|
||||||
|
@ -82,14 +94,15 @@ def big_bounding_box(paths_n_stuff):
|
||||||
return xmin, xmax, ymin, ymax
|
return xmin, xmax, ymin, ymax
|
||||||
|
|
||||||
|
|
||||||
def disvg(paths=None, colors=None,
|
def disvg(paths=None, colors=None, filename=None, stroke_widths=None,
|
||||||
filename=os_path.join(getcwd(), 'disvg_output.svg'),
|
nodes=None, node_colors=None, node_radii=None,
|
||||||
stroke_widths=None, nodes=None, node_colors=None, node_radii=None,
|
openinbrowser=True, timestamp=None, margin_size=0.1,
|
||||||
openinbrowser=True, timestamp=False,
|
mindim=600, dimensions=None, viewbox=None, text=None,
|
||||||
margin_size=0.1, mindim=600, dimensions=None,
|
text_path=None, font_size=None, attributes=None,
|
||||||
viewbox=None, text=None, text_path=None, font_size=None,
|
svg_attributes=None, svgwrite_debug=False,
|
||||||
attributes=None, svg_attributes=None, svgwrite_debug=False):
|
paths2Drawing=False, baseunit='px'):
|
||||||
"""Takes in a list of paths and creates an SVG file containing said paths.
|
"""Creates (and optionally displays) an SVG file.
|
||||||
|
|
||||||
REQUIRED INPUTS:
|
REQUIRED INPUTS:
|
||||||
:param paths - a list of paths
|
: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), ...].
|
3) a list of rgb 3-tuples -- e.g. colors = [(255, 0, 0), ...].
|
||||||
|
|
||||||
:param filename - the desired location/filename of the SVG file
|
:param filename - the desired location/filename of the SVG file
|
||||||
created (by default the SVG will be stored in the current working
|
created (by default the SVG will be named 'disvg_output.svg' or
|
||||||
directory and named 'disvg_output.svg').
|
'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
|
:param stroke_widths - a list of stroke_widths to use for paths
|
||||||
(default is 0.5% of the SVG's width or length)
|
(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
|
:param openinbrowser - Set to True to automatically open the created
|
||||||
SVG in the user's default web browser.
|
SVG in the user's default web browser.
|
||||||
|
|
||||||
:param timestamp - if True, then the a timestamp will be appended to
|
:param timestamp - if true, then the a timestamp will be
|
||||||
the output SVG's filename. This will fix issues with rapidly opening
|
appended to the output SVG's filename. This is meant as a
|
||||||
multiple SVGs in your browser.
|
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
|
:param margin_size - The min margin (empty area framing the collection
|
||||||
of paths) size used for creating the canvas and background of the SVG.
|
of paths) size used for creating the canvas and background of the SVG.
|
||||||
|
@ -140,13 +157,19 @@ def disvg(paths=None, colors=None,
|
||||||
:param mindim - The minimum dimension (height or width) of the output
|
:param mindim - The minimum dimension (height or width) of the output
|
||||||
SVG (default is 600).
|
SVG (default is 600).
|
||||||
|
|
||||||
:param dimensions - The display dimensions of the output SVG. Using
|
:param dimensions - The (x,y) display dimensions of the output SVG.
|
||||||
this will override the mindim parameter.
|
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
|
:param viewbox - This specifies the coordinated system used in the svg.
|
||||||
viewable through the outputSVG. It should be input in the form
|
The SVG `viewBox` attribute works together with the the `height` and
|
||||||
(min_x, min_y, width, height). This is different from the display
|
`width` attrinutes. Using these three attributes allows for shifting
|
||||||
dimension of the svg, which can be set through mindim or dimensions.
|
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
|
:param attributes - a list of dictionaries of attributes for the input
|
||||||
paths. Note: This will override any other conflicting settings.
|
paths. Note: This will override any other conflicting settings.
|
||||||
|
@ -157,6 +180,10 @@ def disvg(paths=None, colors=None,
|
||||||
debugging mode. By default svgwrite_debug=False. This increases
|
debugging mode. By default svgwrite_debug=False. This increases
|
||||||
speed and also prevents `svgwrite` from raising of an error when not
|
speed and also prevents `svgwrite` from raising of an error when not
|
||||||
all `svg_attributes` key-value pairs are understood.
|
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:
|
NOTES:
|
||||||
* The `svg_attributes` parameter will override any other conflicting
|
* The `svg_attributes` parameter will override any other conflicting
|
||||||
|
@ -172,6 +199,9 @@ def disvg(paths=None, colors=None,
|
||||||
svgviewer/browser will likely fail to load some of the SVGs in time.
|
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
|
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.
|
names, or use a pause command (e.g. time.sleep(1)) between uses.
|
||||||
|
|
||||||
|
SEE ALSO:
|
||||||
|
* document.py
|
||||||
"""
|
"""
|
||||||
|
|
||||||
_default_relative_node_radius = 5e-3
|
_default_relative_node_radius = 5e-3
|
||||||
|
@ -180,14 +210,17 @@ def disvg(paths=None, colors=None,
|
||||||
_default_node_color = '#ff0000' # red
|
_default_node_color = '#ff0000' # red
|
||||||
_default_font_size = 12
|
_default_font_size = 12
|
||||||
|
|
||||||
# append directory to filename (if not included)
|
if filename is None:
|
||||||
if os_path.dirname(filename) == '':
|
timestamp = True if timestamp is None else timestamp
|
||||||
filename = os_path.join(getcwd(), filename)
|
filename = os_path.join(gettempdir(), 'disvg_output.svg')
|
||||||
|
|
||||||
|
dirname = os_path.abspath(os_path.dirname(filename))
|
||||||
|
if not os_path.exists(dirname):
|
||||||
|
makedirs(dirname)
|
||||||
|
|
||||||
# append time stamp to filename
|
# append time stamp to filename
|
||||||
if timestamp:
|
if timestamp:
|
||||||
fbname, fext = os_path.splitext(filename)
|
fbname, fext = os_path.splitext(filename)
|
||||||
dirname = os_path.dirname(filename)
|
|
||||||
tstamp = str(time()).replace('.', '')
|
tstamp = str(time()).replace('.', '')
|
||||||
stfilename = os_path.split(fbname)[1] + '_' + tstamp + fext
|
stfilename = os_path.split(fbname)[1] + '_' + tstamp + fext
|
||||||
filename = os_path.join(dirname, stfilename)
|
filename = os_path.join(dirname, stfilename)
|
||||||
|
@ -227,7 +260,15 @@ def disvg(paths=None, colors=None,
|
||||||
assert paths or nodes
|
assert paths or nodes
|
||||||
stuff2bound = []
|
stuff2bound = []
|
||||||
if viewbox:
|
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:
|
else:
|
||||||
if paths:
|
if paths:
|
||||||
stuff2bound += paths
|
stuff2bound += paths
|
||||||
|
@ -274,25 +315,28 @@ def disvg(paths=None, colors=None,
|
||||||
dx += 2*margin_size*dx + extra_space_for_style
|
dx += 2*margin_size*dx + extra_space_for_style
|
||||||
dy += 2*margin_size*dy + extra_space_for_style
|
dy += 2*margin_size*dy + extra_space_for_style
|
||||||
viewbox = "%s %s %s %s" % (xmin, ymin, dx, dy)
|
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:
|
else:
|
||||||
if dx > dy:
|
if dx > dy:
|
||||||
szx = str(mindim) + 'px'
|
szx = str(mindim) + baseunit
|
||||||
szy = str(int(ceil(mindim * dy / dx))) + 'px'
|
szy = str(int(ceil(mindim * dy / dx))) + baseunit
|
||||||
else:
|
else:
|
||||||
szx = str(int(ceil(mindim * dx / dy))) + 'px'
|
szx = str(int(ceil(mindim * dx / dy))) + baseunit
|
||||||
szy = str(mindim) + 'px'
|
szy = str(mindim) + baseunit
|
||||||
|
dimensions = szx, szy
|
||||||
|
|
||||||
# Create an SVG file
|
# Create an SVG file
|
||||||
if svg_attributes is not None:
|
if svg_attributes is not None:
|
||||||
szx = svg_attributes.get("width", szx)
|
dimensions = (svg_attributes.get("width", dimensions[0]),
|
||||||
szy = svg_attributes.get("height", szy)
|
svg_attributes.get("height", dimensions[1]))
|
||||||
debug = svg_attributes.get("debug", svgwrite_debug)
|
debug = svg_attributes.get("debug", svgwrite_debug)
|
||||||
dwg = Drawing(filename=filename, size=(szx, szy), debug=debug,
|
dwg = Drawing(filename=filename, size=dimensions, debug=debug,
|
||||||
**svg_attributes)
|
**svg_attributes)
|
||||||
else:
|
else:
|
||||||
dwg = Drawing(filename=filename, size=(szx, szy), debug=svgwrite_debug,
|
dwg = Drawing(filename=filename, size=dimensions, debug=svgwrite_debug,
|
||||||
viewBox=viewbox)
|
viewBox=viewbox)
|
||||||
|
|
||||||
# add paths
|
# add paths
|
||||||
|
@ -363,9 +407,9 @@ def disvg(paths=None, colors=None,
|
||||||
txter = dwg.add(dwg.text('', font_size=font_size[idx]))
|
txter = dwg.add(dwg.text('', font_size=font_size[idx]))
|
||||||
txter.add(txt.TextPath('#'+pathid, s))
|
txter.add(txt.TextPath('#'+pathid, s))
|
||||||
|
|
||||||
# save svg
|
if paths2Drawing:
|
||||||
if not os_path.exists(os_path.dirname(filename)):
|
return dwg
|
||||||
makedirs(os_path.dirname(filename))
|
|
||||||
dwg.save()
|
dwg.save()
|
||||||
|
|
||||||
# re-open the svg, make the xml pretty, and save it again
|
# re-open the svg, make the xml pretty, and save it again
|
||||||
|
@ -382,20 +426,56 @@ def disvg(paths=None, colors=None,
|
||||||
print(filename)
|
print(filename)
|
||||||
|
|
||||||
|
|
||||||
def wsvg(paths=None, colors=None,
|
def wsvg(paths=None, colors=None, filename=None, stroke_widths=None,
|
||||||
filename=os_path.join(getcwd(), 'disvg_output.svg'),
|
nodes=None, node_colors=None, node_radii=None,
|
||||||
stroke_widths=None, nodes=None, node_colors=None, node_radii=None,
|
openinbrowser=False, timestamp=False, margin_size=0.1,
|
||||||
openinbrowser=False, timestamp=False,
|
mindim=600, dimensions=None, viewbox=None, text=None,
|
||||||
margin_size=0.1, mindim=600, dimensions=None,
|
text_path=None, font_size=None, attributes=None,
|
||||||
viewbox=None, text=None, text_path=None, font_size=None,
|
svg_attributes=None, svgwrite_debug=False,
|
||||||
attributes=None, svg_attributes=None, svgwrite_debug=False):
|
paths2Drawing=False, baseunit='px'):
|
||||||
"""Convenience function; identical to disvg() except that
|
"""Create SVG and write to disk.
|
||||||
openinbrowser=False by default. See disvg() docstring for more info."""
|
|
||||||
disvg(paths, colors=colors, filename=filename,
|
Note: This is identical to `disvg()` except that `openinbrowser`
|
||||||
stroke_widths=stroke_widths, nodes=nodes,
|
is false by default and an assertion error is raised if `filename
|
||||||
node_colors=node_colors, node_radii=node_radii,
|
is None`.
|
||||||
openinbrowser=openinbrowser, timestamp=timestamp,
|
|
||||||
margin_size=margin_size, mindim=mindim, dimensions=dimensions,
|
See `disvg()` docstring for more info.
|
||||||
viewbox=viewbox, text=text, text_path=text_path, font_size=font_size,
|
"""
|
||||||
attributes=attributes, svg_attributes=svg_attributes,
|
assert filename is not None
|
||||||
svgwrite_debug=svgwrite_debug)
|
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)
|
||||||
|
|
|
@ -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)
|
|
@ -4,8 +4,13 @@ The main tool being the svg2paths() function."""
|
||||||
# External dependencies
|
# External dependencies
|
||||||
from __future__ import division, absolute_import, print_function
|
from __future__ import division, absolute_import, print_function
|
||||||
from xml.dom.minidom import parse
|
from xml.dom.minidom import parse
|
||||||
from os import path as os_path, getcwd
|
import os
|
||||||
|
from io import StringIO
|
||||||
import re
|
import re
|
||||||
|
try:
|
||||||
|
from os import PathLike as FilePathLike
|
||||||
|
except ImportError:
|
||||||
|
FilePathLike = str
|
||||||
|
|
||||||
# Internal dependencies
|
# Internal dependencies
|
||||||
from .parser import parse_path
|
from .parser import parse_path
|
||||||
|
@ -18,12 +23,16 @@ COORD_PAIR_TMPLT = re.compile(
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def path2pathd(path):
|
||||||
|
return path.get('d', '')
|
||||||
|
|
||||||
|
|
||||||
def ellipse2pathd(ellipse):
|
def ellipse2pathd(ellipse):
|
||||||
"""converts the parameters from an ellipse or a circle to a string for a
|
"""converts the parameters from an ellipse or a circle to a string for a
|
||||||
Path object d-attribute"""
|
Path object d-attribute"""
|
||||||
|
|
||||||
cx = ellipse.get('cx', None)
|
cx = ellipse.get('cx', 0)
|
||||||
cy = ellipse.get('cy', None)
|
cy = ellipse.get('cy', 0)
|
||||||
rx = ellipse.get('rx', None)
|
rx = ellipse.get('rx', None)
|
||||||
ry = ellipse.get('ry', None)
|
ry = ellipse.get('ry', None)
|
||||||
r = ellipse.get('r', None)
|
r = ellipse.get('r', None)
|
||||||
|
@ -42,13 +51,17 @@ def ellipse2pathd(ellipse):
|
||||||
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'
|
||||||
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
|
return d + 'z'
|
||||||
|
|
||||||
|
|
||||||
def polyline2pathd(polyline_d, is_polygon=False):
|
def polyline2pathd(polyline, is_polygon=False):
|
||||||
"""converts the string from a polyline points-attribute to a string for a
|
"""converts the string from a polyline points-attribute to a string for a
|
||||||
Path object d-attribute"""
|
Path object d-attribute"""
|
||||||
points = COORD_PAIR_TMPLT.findall(polyline_d)
|
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
|
closed = (float(points[0][0]) == float(points[-1][0]) and
|
||||||
float(points[0][1]) == float(points[-1][1]))
|
float(points[0][1]) == float(points[-1][1]))
|
||||||
|
|
||||||
|
@ -64,13 +77,13 @@ def polyline2pathd(polyline_d, is_polygon=False):
|
||||||
return d
|
return d
|
||||||
|
|
||||||
|
|
||||||
def polygon2pathd(polyline_d):
|
def polygon2pathd(polyline):
|
||||||
"""converts the string from a polygon points-attribute to a string
|
"""converts the string from a polygon points-attribute to a string
|
||||||
for a Path object d-attribute.
|
for a Path object d-attribute.
|
||||||
Note: For a polygon made from n points, the resulting path will be
|
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).
|
composed of n lines (even if some of these lines have length zero).
|
||||||
"""
|
"""
|
||||||
return polyline2pathd(polyline_d, True)
|
return polyline2pathd(polyline, True)
|
||||||
|
|
||||||
|
|
||||||
def rect2pathd(rect):
|
def rect2pathd(rect):
|
||||||
|
@ -78,17 +91,49 @@ def rect2pathd(rect):
|
||||||
|
|
||||||
The rectangle will start at the (x,y) coordinate specified by the
|
The rectangle will start at the (x,y) coordinate specified by the
|
||||||
rectangle object and proceed counter-clockwise."""
|
rectangle object and proceed counter-clockwise."""
|
||||||
x0, y0 = float(rect.get('x', 0)), float(rect.get('y', 0))
|
x, y = float(rect.get('x', 0)), float(rect.get('y', 0))
|
||||||
w, h = float(rect["width"]), float(rect["height"])
|
w, h = float(rect.get('width', 0)), float(rect.get('height', 0))
|
||||||
x1, y1 = x0 + w, y0
|
if 'rx' in rect or 'ry' in rect:
|
||||||
x2, y2 = x0 + w, y0 + h
|
|
||||||
x3, y3 = x0, y0 + h
|
# if only one, rx or ry, is present, use that value for both
|
||||||
|
# https://developer.mozilla.org/en-US/docs/Web/SVG/Element/rect
|
||||||
|
rx = rect.get('rx', None)
|
||||||
|
ry = rect.get('ry', None)
|
||||||
|
if rx is None:
|
||||||
|
rx = ry or 0.
|
||||||
|
if ry is None:
|
||||||
|
ry = rx or 0.
|
||||||
|
rx, ry = float(rx), float(ry)
|
||||||
|
|
||||||
|
d = "M {} {} ".format(x + rx, y) # right of p0
|
||||||
|
d += "L {} {} ".format(x + w - rx, y) # go to p1
|
||||||
|
d += "A {} {} 0 0 1 {} {} ".format(rx, ry, x+w, y+ry) # arc for p1
|
||||||
|
d += "L {} {} ".format(x+w, y+h-ry) # above p2
|
||||||
|
d += "A {} {} 0 0 1 {} {} ".format(rx, ry, x+w-rx, y+h) # arc for p2
|
||||||
|
d += "L {} {} ".format(x+rx, y+h) # right of p3
|
||||||
|
d += "A {} {} 0 0 1 {} {} ".format(rx, ry, x, y+h-ry) # arc for p3
|
||||||
|
d += "L {} {} ".format(x, y+ry) # below p0
|
||||||
|
d += "A {} {} 0 0 1 {} {} z".format(rx, ry, x+rx, y) # arc for p0
|
||||||
|
return d
|
||||||
|
|
||||||
|
x0, y0 = x, y
|
||||||
|
x1, y1 = x + w, y
|
||||||
|
x2, y2 = x + w, y + h
|
||||||
|
x3, y3 = x, y + h
|
||||||
|
|
||||||
d = ("M{} {} L {} {} L {} {} L {} {} z"
|
d = ("M{} {} L {} {} L {} {} L {} {} z"
|
||||||
"".format(x0, y0, x1, y1, x2, y2, x3, y3))
|
"".format(x0, y0, x1, y1, x2, y2, x3, y3))
|
||||||
|
|
||||||
return d
|
return d
|
||||||
|
|
||||||
|
|
||||||
|
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,
|
def svg2paths(svg_file_location,
|
||||||
return_svg_attributes=False,
|
return_svg_attributes=False,
|
||||||
convert_circles_to_paths=True,
|
convert_circles_to_paths=True,
|
||||||
|
@ -104,7 +149,9 @@ def svg2paths(svg_file_location,
|
||||||
SVG Path, Line, Polyline, Polygon, Circle, and Ellipse elements.
|
SVG Path, Line, Polyline, Polygon, Circle, and Ellipse elements.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
svg_file_location (string): the location of the svg file
|
svg_file_location (string or file-like object): the location of the
|
||||||
|
svg file on disk or a file-like object containing the content of a
|
||||||
|
svg file
|
||||||
return_svg_attributes (bool): Set to True and a dictionary of
|
return_svg_attributes (bool): Set to True and a dictionary of
|
||||||
svg-attributes will be extracted and returned. See also the
|
svg-attributes will be extracted and returned. See also the
|
||||||
`svg2paths2()` function.
|
`svg2paths2()` function.
|
||||||
|
@ -128,8 +175,10 @@ def svg2paths(svg_file_location,
|
||||||
list: The list of corresponding path attribute dictionaries.
|
list: The list of corresponding path attribute dictionaries.
|
||||||
dict (optional): A dictionary of svg-attributes (see `svg2paths2()`).
|
dict (optional): A dictionary of svg-attributes (see `svg2paths2()`).
|
||||||
"""
|
"""
|
||||||
if os_path.dirname(svg_file_location) == '':
|
# strings are interpreted as file location everything else is treated as
|
||||||
svg_file_location = os_path.join(getcwd(), svg_file_location)
|
# file-like object and passed to the xml parser directly
|
||||||
|
from_filepath = isinstance(svg_file_location, str) or isinstance(svg_file_location, FilePathLike)
|
||||||
|
svg_file_location = os.path.abspath(svg_file_location) if from_filepath else svg_file_location
|
||||||
|
|
||||||
doc = parse(svg_file_location)
|
doc = parse(svg_file_location)
|
||||||
|
|
||||||
|
@ -148,14 +197,14 @@ def svg2paths(svg_file_location,
|
||||||
# path strings, add to list
|
# path strings, add to list
|
||||||
if convert_polylines_to_paths:
|
if convert_polylines_to_paths:
|
||||||
plins = [dom2dict(el) for el in doc.getElementsByTagName('polyline')]
|
plins = [dom2dict(el) for el in doc.getElementsByTagName('polyline')]
|
||||||
d_strings += [polyline2pathd(pl['points']) for pl in plins]
|
d_strings += [polyline2pathd(pl) for pl in plins]
|
||||||
attribute_dictionary_list += plins
|
attribute_dictionary_list += plins
|
||||||
|
|
||||||
# Use minidom to extract polygon strings from input SVG, convert to
|
# Use minidom to extract polygon strings from input SVG, convert to
|
||||||
# path strings, add to list
|
# path strings, add to list
|
||||||
if convert_polygons_to_paths:
|
if convert_polygons_to_paths:
|
||||||
pgons = [dom2dict(el) for el in doc.getElementsByTagName('polygon')]
|
pgons = [dom2dict(el) for el in doc.getElementsByTagName('polygon')]
|
||||||
d_strings += [polygon2pathd(pg['points']) for pg in pgons]
|
d_strings += [polygon2pathd(pg) for pg in pgons]
|
||||||
attribute_dictionary_list += pgons
|
attribute_dictionary_list += pgons
|
||||||
|
|
||||||
if convert_lines_to_paths:
|
if convert_lines_to_paths:
|
||||||
|
@ -209,3 +258,26 @@ def svg2paths2(svg_file_location,
|
||||||
convert_polylines_to_paths=convert_polylines_to_paths,
|
convert_polylines_to_paths=convert_polylines_to_paths,
|
||||||
convert_polygons_to_paths=convert_polygons_to_paths,
|
convert_polygons_to_paths=convert_polygons_to_paths,
|
||||||
convert_rectangles_to_paths=convert_rectangles_to_paths)
|
convert_rectangles_to_paths=convert_rectangles_to_paths)
|
||||||
|
|
||||||
|
|
||||||
|
def svgstr2paths(svg_string,
|
||||||
|
return_svg_attributes=False,
|
||||||
|
convert_circles_to_paths=True,
|
||||||
|
convert_ellipses_to_paths=True,
|
||||||
|
convert_lines_to_paths=True,
|
||||||
|
convert_polylines_to_paths=True,
|
||||||
|
convert_polygons_to_paths=True,
|
||||||
|
convert_rectangles_to_paths=True):
|
||||||
|
"""Convenience function; identical to svg2paths() except that it takes the
|
||||||
|
svg object as string. See svg2paths() docstring for more
|
||||||
|
info."""
|
||||||
|
# wrap string into StringIO object
|
||||||
|
svg_file_obj = StringIO(svg_string)
|
||||||
|
return svg2paths(svg_file_location=svg_file_obj,
|
||||||
|
return_svg_attributes=return_svg_attributes,
|
||||||
|
convert_circles_to_paths=convert_circles_to_paths,
|
||||||
|
convert_ellipses_to_paths=convert_ellipses_to_paths,
|
||||||
|
convert_lines_to_paths=convert_lines_to_paths,
|
||||||
|
convert_polylines_to_paths=convert_polylines_to_paths,
|
||||||
|
convert_polygons_to_paths=convert_polygons_to_paths,
|
||||||
|
convert_rectangles_to_paths=convert_rectangles_to_paths)
|
|
@ -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 |
|
@ -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 |
|
@ -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 |
|
@ -1,7 +1,7 @@
|
||||||
from __future__ import division, absolute_import, print_function
|
from __future__ import division, absolute_import, print_function
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import unittest
|
import unittest
|
||||||
from svgpathtools.bezier import *
|
from svgpathtools.bezier import bezier_point, bezier2polynomial, polynomial2bezier
|
||||||
from svgpathtools.path import bpoints2bezier
|
from svgpathtools.path import bpoints2bezier
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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)
|
|
@ -2,7 +2,7 @@
|
||||||
#------------------------------------------------------------------------------
|
#------------------------------------------------------------------------------
|
||||||
from __future__ import division, absolute_import, print_function
|
from __future__ import division, absolute_import, print_function
|
||||||
import unittest
|
import unittest
|
||||||
from svgpathtools import *
|
from svgpathtools import parse_path
|
||||||
|
|
||||||
|
|
||||||
class TestGeneration(unittest.TestCase):
|
class TestGeneration(unittest.TestCase):
|
||||||
|
|
|
@ -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'))
|
|
@ -1,8 +1,23 @@
|
||||||
# Note: This file was taken mostly as is from the svg.path module (v 2.0)
|
# Note: This file was taken mostly as is from the svg.path module (v 2.0)
|
||||||
#------------------------------------------------------------------------------
|
|
||||||
from __future__ import division, absolute_import, print_function
|
from __future__ import division, absolute_import, print_function
|
||||||
import unittest
|
import unittest
|
||||||
from svgpathtools import *
|
from svgpathtools import 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):
|
class TestParser(unittest.TestCase):
|
||||||
|
@ -17,11 +32,10 @@ class TestParser(unittest.TestCase):
|
||||||
|
|
||||||
# for Z command behavior when there is multiple subpaths
|
# 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')
|
path1 = parse_path('M 0 0 L 50 20 M 100 100 L 300 100 L 200 300 z')
|
||||||
self.assertEqual(path1, Path(
|
self.assertEqual(path1, Path(Line(0 + 0j, 50 + 20j),
|
||||||
Line(0 + 0j, 50 + 20j),
|
Line(100 + 100j, 300 + 100j),
|
||||||
Line(100 + 100j, 300 + 100j),
|
Line(300 + 100j, 200 + 300j),
|
||||||
Line(300 + 100j, 200 + 300j),
|
Line(200 + 300j, 100 + 100j)))
|
||||||
Line(200 + 300j, 100 + 100j)))
|
|
||||||
|
|
||||||
path1 = parse_path('M 100 100 L 200 200')
|
path1 = parse_path('M 100 100 L 200 200')
|
||||||
path2 = parse_path('M100 100L200 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
|
path1 = parse_path("""M100,200 C100,100 250,100 250,200
|
||||||
S400,300 400,200""")
|
S400,300 400,200""")
|
||||||
self.assertEqual(path1,
|
self.assertEqual(path1, Path(CubicBezier(100 + 200j,
|
||||||
Path(CubicBezier(100 + 200j, 100 + 100j, 250 + 100j, 250 + 200j),
|
100 + 100j,
|
||||||
CubicBezier(250 + 200j, 250 + 300j, 400 + 300j, 400 + 200j)))
|
250 + 100j,
|
||||||
|
250 + 200j),
|
||||||
|
CubicBezier(250 + 200j,
|
||||||
|
250 + 300j,
|
||||||
|
400 + 300j,
|
||||||
|
400 + 200j)))
|
||||||
|
|
||||||
path1 = parse_path('M100,200 C100,100 400,100 400,200')
|
path1 = parse_path('M100,200 C100,100 400,100 400,200')
|
||||||
self.assertEqual(path1,
|
self.assertEqual(path1, Path(CubicBezier(100 + 200j,
|
||||||
Path(CubicBezier(100 + 200j, 100 + 100j, 400 + 100j, 400 + 200j)))
|
100 + 100j,
|
||||||
|
400 + 100j,
|
||||||
|
400 + 200j)))
|
||||||
|
|
||||||
path1 = parse_path('M100,500 C25,400 475,400 400,500')
|
path1 = parse_path('M100,500 C25,400 475,400 400,500')
|
||||||
self.assertEqual(path1,
|
self.assertEqual(path1, Path(CubicBezier(100 + 500j,
|
||||||
Path(CubicBezier(100 + 500j, 25 + 400j, 475 + 400j, 400 + 500j)))
|
25 + 400j,
|
||||||
|
475 + 400j,
|
||||||
|
400 + 500j)))
|
||||||
|
|
||||||
path1 = parse_path('M100,800 C175,700 325,700 400,800')
|
path1 = parse_path('M100,800 C175,700 325,700 400,800')
|
||||||
self.assertEqual(path1,
|
self.assertEqual(path1, Path(CubicBezier(100 + 800j,
|
||||||
Path(CubicBezier(100 + 800j, 175 + 700j, 325 + 700j, 400 + 800j)))
|
175 + 700j,
|
||||||
|
325 + 700j,
|
||||||
|
400 + 800j)))
|
||||||
|
|
||||||
path1 = parse_path('M600,200 C675,100 975,100 900,200')
|
path1 = parse_path('M600,200 C675,100 975,100 900,200')
|
||||||
self.assertEqual(path1,
|
self.assertEqual(path1, Path(CubicBezier(600 + 200j,
|
||||||
Path(CubicBezier(600 + 200j, 675 + 100j, 975 + 100j, 900 + 200j)))
|
675 + 100j,
|
||||||
|
975 + 100j,
|
||||||
|
900 + 200j)))
|
||||||
|
|
||||||
path1 = parse_path('M600,500 C600,350 900,650 900,500')
|
path1 = parse_path('M600,500 C600,350 900,650 900,500')
|
||||||
self.assertEqual(path1,
|
self.assertEqual(path1, Path(CubicBezier(600 + 500j,
|
||||||
Path(CubicBezier(600 + 500j, 600 + 350j, 900 + 650j, 900 + 500j)))
|
600 + 350j,
|
||||||
|
900 + 650j,
|
||||||
|
900 + 500j)))
|
||||||
|
|
||||||
path1 = parse_path("""M600,800 C625,700 725,700 750,800
|
path1 = parse_path("""M600,800 C625,700 725,700 750,800
|
||||||
S875,900 900,800""")
|
S875,900 900,800""")
|
||||||
self.assertEqual(path1,
|
self.assertEqual(path1, Path(CubicBezier(600 + 800j,
|
||||||
Path(CubicBezier(600 + 800j, 625 + 700j, 725 + 700j, 750 + 800j),
|
625 + 700j,
|
||||||
CubicBezier(750 + 800j, 775 + 900j, 875 + 900j, 900 + 800j)))
|
725 + 700j,
|
||||||
|
750 + 800j),
|
||||||
|
CubicBezier(750 + 800j,
|
||||||
|
775 + 900j,
|
||||||
|
875 + 900j,
|
||||||
|
900 + 800j)))
|
||||||
|
|
||||||
path1 = parse_path('M200,300 Q400,50 600,300 T1000,300')
|
path1 = parse_path('M200,300 Q400,50 600,300 T1000,300')
|
||||||
self.assertEqual(path1,
|
self.assertEqual(path1, Path(QuadraticBezier(200 + 300j,
|
||||||
Path(QuadraticBezier(200 + 300j, 400 + 50j, 600 + 300j),
|
400 + 50j,
|
||||||
QuadraticBezier(600 + 300j, 800 + 550j, 1000 + 300j)))
|
600 + 300j),
|
||||||
|
QuadraticBezier(600 + 300j,
|
||||||
|
800 + 550j,
|
||||||
|
1000 + 300j)))
|
||||||
|
|
||||||
path1 = parse_path('M300,200 h-150 a150,150 0 1,0 150,-150 z')
|
path1 = parse_path('M300,200 h-150 a150,150 0 1,0 150,-150 z')
|
||||||
self.assertEqual(path1,
|
self.assertEqual(path1, Path(Line(300 + 200j, 150 + 200j),
|
||||||
Path(Line(300 + 200j, 150 + 200j),
|
Arc(150 + 200j, 150 + 150j, 0, 1, 0, 300 + 50j),
|
||||||
Arc(150 + 200j, 150 + 150j, 0, 1, 0, 300 + 50j),
|
Line(300 + 50j, 300 + 200j)))
|
||||||
Line(300 + 50j, 300 + 200j)))
|
|
||||||
|
|
||||||
path1 = parse_path('M275,175 v-150 a150,150 0 0,0 -150,150 z')
|
path1 = parse_path('M275,175 v-150 a150,150 0 0,0 -150,150 z')
|
||||||
self.assertEqual(path1,
|
self.assertEqual(path1,
|
||||||
|
@ -101,26 +137,32 @@ class TestParser(unittest.TestCase):
|
||||||
|
|
||||||
# Relative moveto:
|
# Relative moveto:
|
||||||
path1 = parse_path('M 0 0 L 50 20 m 50 80 L 300 100 L 200 300 z')
|
path1 = parse_path('M 0 0 L 50 20 m 50 80 L 300 100 L 200 300 z')
|
||||||
self.assertEqual(path1, Path(
|
self.assertEqual(path1, Path(Line(0 + 0j, 50 + 20j),
|
||||||
Line(0 + 0j, 50 + 20j),
|
Line(100 + 100j, 300 + 100j),
|
||||||
Line(100 + 100j, 300 + 100j),
|
Line(300 + 100j, 200 + 300j),
|
||||||
Line(300 + 100j, 200 + 300j),
|
Line(200 + 300j, 100 + 100j)))
|
||||||
Line(200 + 300j, 100 + 100j)))
|
|
||||||
|
|
||||||
# Initial smooth and relative CubicBezier
|
# Initial smooth and relative CubicBezier
|
||||||
path1 = parse_path("""M100,200 s 150,-100 150,0""")
|
path1 = parse_path("""M100,200 s 150,-100 150,0""")
|
||||||
self.assertEqual(path1,
|
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
|
# Initial smooth and relative QuadraticBezier
|
||||||
path1 = parse_path("""M100,200 t 150,0""")
|
path1 = parse_path("""M100,200 t 150,0""")
|
||||||
self.assertEqual(path1,
|
self.assertEqual(path1,
|
||||||
Path(QuadraticBezier(100 + 200j, 100 + 200j, 250 + 200j)))
|
Path(QuadraticBezier(100 + 200j,
|
||||||
|
100 + 200j,
|
||||||
|
250 + 200j)))
|
||||||
|
|
||||||
# Relative QuadraticBezier
|
# Relative QuadraticBezier
|
||||||
path1 = parse_path("""M100,200 q 0,0 150,0""")
|
path1 = parse_path("""M100,200 q 0,0 150,0""")
|
||||||
self.assertEqual(path1,
|
self.assertEqual(path1,
|
||||||
Path(QuadraticBezier(100 + 200j, 100 + 200j, 250 + 200j)))
|
Path(QuadraticBezier(100 + 200j,
|
||||||
|
100 + 200j,
|
||||||
|
250 + 200j)))
|
||||||
|
|
||||||
def test_negative(self):
|
def test_negative(self):
|
||||||
"""You don't need spaces before a minus-sign"""
|
"""You don't need spaces before a minus-sign"""
|
||||||
|
@ -130,10 +172,117 @@ class TestParser(unittest.TestCase):
|
||||||
|
|
||||||
def test_numbers(self):
|
def test_numbers(self):
|
||||||
"""Exponents and other number format cases"""
|
"""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')
|
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))
|
path2 = Path(Line(-3.4e+38 + 3.4e+38j, -3.4e-38 + 3.4e-38j))
|
||||||
self.assertEqual(path1, path2)
|
self.assertEqual(path1, path2)
|
||||||
|
|
||||||
def test_errors(self):
|
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')
|
||||||
|
|
1448
test/test_path.py
|
@ -4,7 +4,7 @@ import unittest
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
# Internal dependencies
|
# Internal dependencies
|
||||||
from svgpathtools import *
|
from svgpathtools import rational_limit
|
||||||
|
|
||||||
|
|
||||||
class Test_polytools(unittest.TestCase):
|
class Test_polytools(unittest.TestCase):
|
||||||
|
|
|
@ -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')
|
|
@ -1,7 +1,16 @@
|
||||||
from __future__ import division, absolute_import, print_function
|
from __future__ import division, absolute_import, print_function
|
||||||
import unittest
|
import unittest
|
||||||
from svgpathtools import *
|
from svgpathtools import 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 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):
|
class TestSVG2Paths(unittest.TestCase):
|
||||||
def test_svg2paths_polygons(self):
|
def test_svg2paths_polygons(self):
|
||||||
|
@ -50,3 +59,78 @@ class TestSVG2Paths(unittest.TestCase):
|
||||||
self.assertTrue(len(path_circle)==2)
|
self.assertTrue(len(path_circle)==2)
|
||||||
self.assertTrue(path_circle==path_circle_correct)
|
self.assertTrue(path_circle==path_circle_correct)
|
||||||
self.assertTrue(path_circle.isclosed())
|
self.assertTrue(path_circle.isclosed())
|
||||||
|
|
||||||
|
# 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()
|
||||||
|
|
|
@ -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 |