Compare commits

..

1 Commits

Author SHA1 Message Date
Andy Port 1579c544aa Revert "Arc line intersect, take 2 (#60)"
This reverts commit 2da39e4c02.
2018-08-22 00:27:31 -07:00
56 changed files with 1618 additions and 4018 deletions

View File

@ -1,14 +0,0 @@
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

View File

@ -1,65 +0,0 @@
# 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

View File

@ -1,34 +0,0 @@
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

View File

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

View File

@ -1,37 +0,0 @@
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/

View File

@ -1,42 +0,0 @@
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
.gitignore vendored
View File

@ -5,4 +5,3 @@ build
svgpathtools.egg-info
!.travis.yml
!/.gitignore
!/.github

8
.travis.yml Normal file
View File

@ -0,0 +1,8 @@
language: python
python:
- "2.7"
- "3.6"
install:
- pip install numpy svgwrite
script:
- python -m unittest discover test

View File

@ -18,7 +18,7 @@ example**. Feel free to make a pull-request too (see relevant section below).
## Submitting Pull-Requests
#### New features come with unittests and docstrings.
#### New features should come with unittests and docstrings.
If you want to add a cool/useful feature to svgpathtools, that's great! Just
make sure your pull-request includes both thorough unittests and well-written
docstrings. See relevant sections below on "Testing Style" and
@ -42,9 +42,8 @@ you want your code's variable names to match some official documentation, or
PEP8 guidelines contradict those present in this document).
* Include docstrings and in-line comments where appropriate. See
"Docstring Style" section below for more info.
* Use explicit, uncontracted names (e.g. `parse_transform` instead of
`parse_trafo`). Maybe the most important feature for a name is how easy it is
for a user to guess (after having seen other names used in `svgpathtools`).
* Use explicit, uncontracted names (e.g. "parse_transform" instead of
"parse_trafo"). The ideal names should be something a user can guess
* Use a capital 'T' denote a Path object's parameter, use a lower case 't' to
denote a Path segment's parameter. See the methods `Path.t2T` and `Path.T2t`
if you're unsure what I mean. In the ambiguous case, use either 't' or another

View File

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

515
README.md
View File

@ -1,515 +0,0 @@
[![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 Normal file
View File

@ -0,0 +1,635 @@
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.

View File

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

BIN
dist/svgpathtools-1.0.1.tar.gz vendored Normal file

Binary file not shown.

BIN
dist/svgpathtools-1.2.1.tar.gz vendored Normal file

Binary file not shown.

BIN
dist/svgpathtools-1.2.2.tar.gz vendored Normal file

Binary file not shown.

BIN
dist/svgpathtools-1.2.3.tar.gz vendored Normal file

Binary file not shown.

BIN
dist/svgpathtools-1.2.4.tar.gz vendored Normal file

Binary file not shown.

Binary file not shown.

BIN
dist/svgpathtools-1.2.5.tar.gz vendored Normal file

Binary file not shown.

Binary file not shown.

BIN
dist/svgpathtools-1.2.6.tar.gz vendored Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
dist/svgpathtools-1.3.1.tar.gz vendored Normal file

Binary file not shown.

Binary file not shown.

BIN
dist/svgpathtools-1.3.2.tar.gz vendored Normal file

Binary file not shown.

BIN
dist/svgpathtools-1.3.tar.gz vendored Normal file

Binary file not shown.

View File

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

Before

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -8,7 +8,7 @@ Note: The relevant matrix transformation for quadratics can be found in the
svgpathtools.bezier module."""
from __future__ import print_function
import numpy as np
from svgpathtools import bezier_point, Path, bpoints2bezier, polynomial2bezier
from svgpathtools import *
class HigherOrderBezier:

View File

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

View File

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

View File

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

View File

@ -1,22 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 1.1 KiB

View File

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

View File

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

View File

@ -8,15 +8,13 @@ from .path import (Path, Line, QuadraticBezier, CubicBezier, Arc,
closest_point_in_path, farthest_point_in_path,
path_encloses_pt, bbox2path, polygon, polyline)
from .parser import parse_path
from .paths2svg import disvg, wsvg, paths2Drawing
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
from .document import (Document, CONVERSIONS, CONVERT_ONLY_PATHS,
SVG_GROUP_TAG, SVG_NAMESPACE)
from .svg_io_sax import SaxDocument
from .document import Document, CONVERSIONS, CONVERT_ONLY_PATHS, SVG_GROUP_TAG, SVG_NAMESPACE
try:
from .svg_to_paths import svg2paths, svg2paths2, svgstr2paths
from .svg_to_paths import svg2paths, svg2paths2
except ImportError:
pass

View File

@ -13,16 +13,17 @@ An Historic Note:
Example:
Typical usage looks something like the following.
>> from svgpathtools import Document
>> from svgpathtools import *
>> doc = Document('my_file.html')
>> for path in doc.paths():
>> results = doc.flatten_all_paths()
>> for result in results:
>> path = result.path
>> # Do something with the transformed Path object.
>> foo(path)
>> # Inspect the raw SVG element, e.g. change its attributes
>> foo(path.element)
>> element = result.element
>> # Inspect the raw SVG element. This gives access to the
>> # path's attributes
>> 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')
@ -39,12 +40,7 @@ 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
@ -52,17 +48,13 @@ 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
from .path import *
# To maintain forward/backward compatibility
try:
string = basestring
str = basestring
except NameError:
string = str
try:
from os import PathLike
except ImportError:
PathLike = string
pass
# Let xml.etree.ElementTree know about the SVG namespace
SVG_NAMESPACE = {'svg': 'http://www.w3.org/2000/svg'}
@ -82,7 +74,7 @@ CONVERT_ONLY_PATHS = {'path': path2pathd}
SVG_GROUP_TAG = 'svg:g'
def flattened_paths(group, group_filter=lambda x: True,
def flatten_all_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
@ -91,7 +83,7 @@ def flattened_paths(group, group_filter=lambda x: True,
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
Document.flatten_group(group) to flatten a specific nested group into
the root coordinates.
Args:
@ -109,8 +101,6 @@ def flattened_paths(group, group_filter=lambda x: True,
# 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
@ -134,7 +124,10 @@ def flattened_paths(group, group_filter=lambda x: True,
stack = [new_stack_element(group, np.identity(3))]
FlattenedPath = collections.namedtuple('FlattenedPath',
['path', 'element', 'transform'])
paths = []
while stack:
top = stack.pop()
@ -147,18 +140,15 @@ def flattened_paths(group, group_filter=lambda x: True,
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)
paths.append(FlattenedPath(path, path_elem, path_tf))
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,
def flatten_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.
@ -184,53 +174,15 @@ def flattened_paths_from_group(group_to_flatten, root, recursive=True,
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,
return flatten_all_paths(root, desired_group_filter, path_filter,
path_conversions, group_search_xpath)
class Document:
def __init__(self, filepath=None):
def __init__(self, filename):
"""A container for a DOM-style SVG document.
The `Document` class provides a simple interface to modify and analyze
@ -238,61 +190,47 @@ class Document:
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.
The Path output 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.
filename (str): The filename of the DOM-style object.
"""
# 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'))
# 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:
# parse svg to ElementTree object
self.tree = etree.parse(filepath)
self.tree = etree.parse(filename)
else:
self.tree = etree.ElementTree(Element('svg'))
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,
def flatten_all_paths(self, group_filter=lambda x: True,
path_filter=lambda x: True,
path_conversions=CONVERSIONS):
"""Forward the tree of this document into the more general
flatten_all_paths function and return the result."""
return flatten_all_paths(self.tree.getroot(), group_filter,
path_filter, path_conversions)
def paths_from_group(self, group, recursive=True, group_filter=lambda x: True,
def flatten_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 all(isinstance(s, str) for s in group):
# If we're given a list of strings, assume it represents a
# nested sequence
group = self.get_group(group)
group = self.get_or_add_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,
return flatten_group(group, self.tree.getroot(), recursive,
group_filter, path_filter, path_conversions)
def add_path(self, path, attribs=None, group=None):
@ -304,7 +242,7 @@ class Document:
# 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):
elif all(isinstance(elem, str) for elem in group):
group = self.get_or_add_group(group)
elif not isinstance(group, Element):
@ -323,7 +261,7 @@ class Document:
path_svg = path.d()
elif is_path_segment(path):
path_svg = Path(path).d()
elif isinstance(path, string):
elif isinstance(path, str):
# Assume this is a valid d-string.
# TODO: Should we sanity check the input string?
path_svg = path
@ -344,37 +282,6 @@ class Document:
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.
@ -430,33 +337,18 @@ class Document:
return SubElement(parent, '{{{0}}}g'.format(
SVG_NAMESPACE['svg']), group_attribs)
def __repr__(self):
return etree.tostring(self.tree.getroot()).decode()
def save(self, filename):
with open(filename, 'w') as output_svg:
output_svg.write(etree.tostring(self.tree.getroot()))
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):
def display(self, filename=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)
if filename is None:
filename = self.original_filename
# write to a (by default temporary) file
with open(filepath, 'w') as output_svg:
output_svg.write(repr(self))
with open(filename, 'w') as output_svg:
output_svg.write(etree.tostring(self.tree.getroot()))
open_in_browser(filepath)
open_in_browser(filename)

View File

@ -4,15 +4,205 @@ Note: This file was taken (nearly) as is from the svg.path module (v 2.0)."""
# External dependencies
from __future__ import division, absolute_import, print_function
import re
import numpy as np
import warnings
# Internal dependencies
from .path import Path
from .path import Path, Line, QuadraticBezier, CubicBezier, Arc
# To maintain forward/backward compatibility
try:
str = basestring
except NameError:
pass
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, tree_element=None):
return Path(pathdef, current_pos=current_pos, tree_element=tree_element)
# 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()
if tree_element is None:
segments = Path()
else:
segments = Path(tree_element=tree_element)
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
command = None
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
def _check_num_parsed_values(values, allowed):

File diff suppressed because it is too large Load Diff

View File

@ -1,24 +1,20 @@
"""This submodule: basic tools for creating svg files from path data.
See also the document.py submodule.
"""
"""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 path as os_path, makedirs
from tempfile import gettempdir
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
import re
# Internal dependencies
from .path import Path, Line, is_path_segment
from .misctools import open_in_browser
# color shorthand for inputting color list as string of chars.
# Used to convert a string colors (identified by single chars) to a list.
color_dict = {'a': 'aqua',
'b': 'blue',
'c': 'cyan',
@ -61,16 +57,8 @@ def is3tuple(c):
def big_bounding_box(paths_n_stuff):
"""returns minimal upright bounding box.
Args:
paths_n_stuff: iterable of Paths, Bezier path segments, and
points (given as complex numbers).
Returns:
extrema of bounding box, (xmin, xmax, ymin, ymax)
"""
"""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):
@ -83,9 +71,9 @@ def big_bounding_box(paths_n_stuff):
bbs.append((complexthing.real, complexthing.real,
complexthing.imag, complexthing.imag))
except ValueError:
raise TypeError("paths_n_stuff can only contains Path, "
"CubicBezier, QuadraticBezier, Line, "
"and complex objects.")
raise TypeError(
"paths_n_stuff can only contains Path, CubicBezier, "
"QuadraticBezier, Line, and complex objects.")
xmins, xmaxs, ymins, ymaxs = list(zip(*bbs))
xmin = min(xmins)
xmax = max(xmaxs)
@ -94,15 +82,14 @@ def big_bounding_box(paths_n_stuff):
return xmin, xmax, ymin, ymax
def disvg(paths=None, colors=None, filename=None, stroke_widths=None,
nodes=None, node_colors=None, node_radii=None,
openinbrowser=True, timestamp=None, margin_size=0.1,
mindim=600, dimensions=None, viewbox=None, text=None,
text_path=None, font_size=None, attributes=None,
svg_attributes=None, svgwrite_debug=False,
paths2Drawing=False, baseunit='px'):
"""Creates (and optionally displays) an SVG file.
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, svgwrite_debug=False):
"""Takes in a list of paths and creates an SVG file containing said paths.
REQUIRED INPUTS:
:param paths - a list of paths
@ -117,10 +104,8 @@ def disvg(paths=None, colors=None, filename=None, stroke_widths=None,
3) a list of rgb 3-tuples -- e.g. colors = [(255, 0, 0), ...].
:param filename - the desired location/filename of the SVG file
created (by default the SVG will be named 'disvg_output.svg' or
'disvg_output_<timestamp>.svg' and stored in the temporary
directory returned by `tempfile.gettempdir()`. See `timestamp`
for information on the timestamp.
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)
@ -145,11 +130,9 @@ def disvg(paths=None, colors=None, filename=None, stroke_widths=None,
:param openinbrowser - Set to True to automatically open the created
SVG in the user's default web browser.
:param timestamp - if true, then the a timestamp will be
appended to the output SVG's filename. This is meant as a
workaround for issues related to rapidly opening multiple
SVGs in your browser using `disvg`. This defaults to true if
`filename is None` and false otherwise.
:param 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.
@ -157,19 +140,13 @@ def disvg(paths=None, colors=None, filename=None, stroke_widths=None,
:param mindim - The minimum dimension (height or width) of the output
SVG (default is 600).
:param dimensions - The (x,y) display dimensions of the output SVG.
I.e. this specifies the `width` and `height` SVG attributes. Note that
these also can be used to specify units other than pixels. Using this
will override the `mindim` parameter.
:param dimensions - The display dimensions of the output SVG. Using
this will override the mindim parameter.
:param viewbox - This specifies the coordinated system used in the svg.
The SVG `viewBox` attribute works together with the the `height` and
`width` attrinutes. Using these three attributes allows for shifting
and scaling of the SVG canvas without changing the any values other
than those in `viewBox`, `height`, and `width`. `viewbox` should be
input as a 4-tuple, (min_x, min_y, width, height), or a string
"min_x min_y width height". Using this will override the `mindim`
parameter.
:param 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.
@ -181,10 +158,6 @@ def disvg(paths=None, colors=None, filename=None, stroke_widths=None,
speed and also prevents `svgwrite` from raising of an error when not
all `svg_attributes` key-value pairs are understood.
:param paths2Drawing - If true, an `svgwrite.Drawing` object is
returned and no file is written. This `Drawing` can later be saved
using the `svgwrite.Drawing.save()` method.
NOTES:
* The `svg_attributes` parameter will override any other conflicting
settings.
@ -199,9 +172,6 @@ def disvg(paths=None, colors=None, filename=None, stroke_widths=None,
svgviewer/browser will likely fail to load some of the SVGs in time.
To fix this, use the timestamp attribute, or give the files unique
names, or use a pause command (e.g. time.sleep(1)) between uses.
SEE ALSO:
* document.py
"""
_default_relative_node_radius = 5e-3
@ -210,17 +180,14 @@ def disvg(paths=None, colors=None, filename=None, stroke_widths=None,
_default_node_color = '#ff0000' # red
_default_font_size = 12
if filename is None:
timestamp = True if timestamp is None else timestamp
filename = os_path.join(gettempdir(), 'disvg_output.svg')
dirname = os_path.abspath(os_path.dirname(filename))
if not os_path.exists(dirname):
makedirs(dirname)
# append 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)
@ -260,15 +227,7 @@ def disvg(paths=None, colors=None, filename=None, stroke_widths=None,
assert paths or nodes
stuff2bound = []
if viewbox:
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))
szx, szy = viewbox[2:4]
else:
if paths:
stuff2bound += paths
@ -315,28 +274,25 @@ def disvg(paths=None, colors=None, filename=None, stroke_widths=None,
dx += 2*margin_size*dx + extra_space_for_style
dy += 2*margin_size*dy + extra_space_for_style
viewbox = "%s %s %s %s" % (xmin, ymin, dx, dy)
if mindim is None:
szx = "{}{}".format(dx, baseunit)
szy = "{}{}".format(dy, baseunit)
if dimensions:
szx, szy = dimensions
else:
if dx > dy:
szx = str(mindim) + baseunit
szy = str(int(ceil(mindim * dy / dx))) + baseunit
szx = str(mindim) + 'px'
szy = str(int(ceil(mindim * dy / dx))) + 'px'
else:
szx = str(int(ceil(mindim * dx / dy))) + baseunit
szy = str(mindim) + baseunit
dimensions = szx, szy
szx = str(int(ceil(mindim * dx / dy))) + 'px'
szy = str(mindim) + 'px'
# Create an SVG file
if svg_attributes is not None:
dimensions = (svg_attributes.get("width", dimensions[0]),
svg_attributes.get("height", dimensions[1]))
szx = svg_attributes.get("width", szx)
szy = svg_attributes.get("height", szy)
debug = svg_attributes.get("debug", svgwrite_debug)
dwg = Drawing(filename=filename, size=dimensions, debug=debug,
dwg = Drawing(filename=filename, size=(szx, szy), debug=debug,
**svg_attributes)
else:
dwg = Drawing(filename=filename, size=dimensions, debug=svgwrite_debug,
dwg = Drawing(filename=filename, size=(szx, szy), debug=svgwrite_debug,
viewBox=viewbox)
# add paths
@ -407,9 +363,9 @@ def disvg(paths=None, colors=None, filename=None, stroke_widths=None,
txter = dwg.add(dwg.text('', font_size=font_size[idx]))
txter.add(txt.TextPath('#'+pathid, s))
if paths2Drawing:
return dwg
# 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
@ -426,56 +382,20 @@ def disvg(paths=None, colors=None, filename=None, stroke_widths=None,
print(filename)
def wsvg(paths=None, colors=None, filename=None, stroke_widths=None,
nodes=None, node_colors=None, node_radii=None,
openinbrowser=False, timestamp=False, margin_size=0.1,
mindim=600, dimensions=None, viewbox=None, text=None,
text_path=None, font_size=None, attributes=None,
svg_attributes=None, svgwrite_debug=False,
paths2Drawing=False, baseunit='px'):
"""Create SVG and write to disk.
Note: This is identical to `disvg()` except that `openinbrowser`
is false by default and an assertion error is raised if `filename
is None`.
See `disvg()` docstring for more info.
"""
assert filename is not None
return disvg(paths, colors=colors, filename=filename,
stroke_widths=stroke_widths, nodes=nodes,
node_colors=node_colors, node_radii=node_radii,
openinbrowser=openinbrowser, timestamp=timestamp,
margin_size=margin_size, mindim=mindim,
dimensions=dimensions, viewbox=viewbox, text=text,
text_path=text_path, font_size=font_size,
attributes=attributes, svg_attributes=svg_attributes,
svgwrite_debug=svgwrite_debug,
paths2Drawing=paths2Drawing, baseunit=baseunit)
def paths2Drawing(paths=None, colors=None, filename=None,
stroke_widths=None, nodes=None, node_colors=None,
node_radii=None, openinbrowser=False, timestamp=False,
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,
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,
viewbox=None, text=None, text_path=None, font_size=None,
attributes=None, svg_attributes=None, svgwrite_debug=False):
"""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,
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)
svgwrite_debug=svgwrite_debug)

View File

@ -1,199 +0,0 @@
"""(Experimental) replacement for import/export functionality SAX
"""
# External dependencies
from __future__ import division, absolute_import, print_function
import os
from xml.etree.ElementTree import iterparse, Element, ElementTree, SubElement
import numpy as np
# Internal dependencies
from .parser import parse_path
from .parser import parse_transform
from .svg_to_paths import (path2pathd, ellipse2pathd, line2pathd,
polyline2pathd, polygon2pathd, rect2pathd)
from .misctools import open_in_browser
from .path import transform
# To maintain forward/backward compatibility
try:
string = basestring
except NameError:
string = str
NAME_SVG = "svg"
ATTR_VERSION = "version"
VALUE_SVG_VERSION = "1.1"
ATTR_XMLNS = "xmlns"
VALUE_XMLNS = "http://www.w3.org/2000/svg"
ATTR_XMLNS_LINK = "xmlns:xlink"
VALUE_XLINK = "http://www.w3.org/1999/xlink"
ATTR_XMLNS_EV = "xmlns:ev"
VALUE_XMLNS_EV = "http://www.w3.org/2001/xml-events"
ATTR_WIDTH = "width"
ATTR_HEIGHT = "height"
ATTR_VIEWBOX = "viewBox"
NAME_PATH = "path"
ATTR_DATA = "d"
ATTR_FILL = "fill"
ATTR_STROKE = "stroke"
ATTR_STROKE_WIDTH = "stroke-width"
ATTR_TRANSFORM = "transform"
VALUE_NONE = "none"
class SaxDocument:
def __init__(self, filename):
"""A container for a SAX SVG light tree objects document.
This class provides functions for extracting SVG data into Path objects.
Args:
filename (str): The filename of the SVG file
"""
self.root_values = {}
self.tree = []
# remember location of original svg file
if filename is not None and os.path.dirname(filename) == '':
self.original_filename = os.path.join(os.getcwd(), filename)
else:
self.original_filename = filename
if filename is not None:
self.sax_parse(filename)
def sax_parse(self, filename):
self.root_values = {}
self.tree = []
stack = []
values = {}
matrix = None
for event, elem in iterparse(filename, events=('start', 'end')):
if event == 'start':
stack.append((values, matrix))
if matrix is not None:
matrix = matrix.copy() # copy of matrix
current_values = values
values = {}
values.update(current_values) # copy of dictionary
attrs = elem.attrib
values.update(attrs)
name = elem.tag[28:]
if "style" in attrs:
for equate in attrs["style"].split(";"):
equal_item = equate.split(":")
values[equal_item[0]] = equal_item[1]
if "transform" in attrs:
transform_matrix = parse_transform(attrs["transform"])
if matrix is None:
matrix = np.identity(3)
matrix = transform_matrix.dot(matrix)
if "svg" == name:
current_values = values
values = {}
values.update(current_values)
self.root_values = current_values
continue
elif "g" == name:
continue
elif 'path' == name:
values['d'] = path2pathd(values)
elif 'circle' == name:
values["d"] = ellipse2pathd(values)
elif 'ellipse' == name:
values["d"] = ellipse2pathd(values)
elif 'line' == name:
values["d"] = line2pathd(values)
elif 'polyline' == name:
values["d"] = polyline2pathd(values)
elif 'polygon' == name:
values["d"] = polygon2pathd(values)
elif 'rect' == name:
values["d"] = rect2pathd(values)
else:
continue
values["matrix"] = matrix
values["name"] = name
self.tree.append(values)
else:
v = stack.pop()
values = v[0]
matrix = v[1]
def flatten_all_paths(self):
flat = []
for values in self.tree:
pathd = values['d']
matrix = values['matrix']
parsed_path = parse_path(pathd)
if matrix is not None:
transform(parsed_path, matrix)
flat.append(parsed_path)
return flat
def get_pathd_and_matrix(self):
flat = []
for values in self.tree:
pathd = values['d']
matrix = values['matrix']
flat.append((pathd, matrix))
return flat
def generate_dom(self):
root = Element(NAME_SVG)
root.set(ATTR_VERSION, VALUE_SVG_VERSION)
root.set(ATTR_XMLNS, VALUE_XMLNS)
root.set(ATTR_XMLNS_LINK, VALUE_XLINK)
root.set(ATTR_XMLNS_EV, VALUE_XMLNS_EV)
width = self.root_values.get(ATTR_WIDTH, None)
height = self.root_values.get(ATTR_HEIGHT, None)
if width is not None:
root.set(ATTR_WIDTH, width)
if height is not None:
root.set(ATTR_HEIGHT, height)
viewbox = self.root_values.get(ATTR_VIEWBOX, None)
if viewbox is not None:
root.set(ATTR_VIEWBOX, viewbox)
identity = np.identity(3)
for values in self.tree:
pathd = values.get('d', '')
matrix = values.get('matrix', None)
# path_value = parse_path(pathd)
path = SubElement(root, NAME_PATH)
if matrix is not None and not np.all(np.equal(matrix, identity)):
matrix_string = "matrix("
matrix_string += " "
matrix_string += string(matrix[0][0])
matrix_string += " "
matrix_string += string(matrix[1][0])
matrix_string += " "
matrix_string += string(matrix[0][1])
matrix_string += " "
matrix_string += string(matrix[1][1])
matrix_string += " "
matrix_string += string(matrix[0][2])
matrix_string += " "
matrix_string += string(matrix[1][2])
matrix_string += ")"
path.set(ATTR_TRANSFORM, matrix_string)
if ATTR_DATA in values:
path.set(ATTR_DATA, values[ATTR_DATA])
if ATTR_FILL in values:
path.set(ATTR_FILL, values[ATTR_FILL])
if ATTR_STROKE in values:
path.set(ATTR_STROKE, values[ATTR_STROKE])
return ElementTree(root)
def save(self, filename):
with open(filename, 'wb') as output_svg:
dom_tree = self.generate_dom()
dom_tree.write(output_svg)
def display(self, filename=None):
"""Displays/opens the doc using the OS's default application."""
if filename is None:
filename = 'display_temp.svg'
self.save(filename)
open_in_browser(filename)

View File

@ -4,13 +4,8 @@ The main tool being the svg2paths() function."""
# External dependencies
from __future__ import division, absolute_import, print_function
from xml.dom.minidom import parse
import os
from io import StringIO
from os import path as os_path, getcwd
import re
try:
from os import PathLike as FilePathLike
except ImportError:
FilePathLike = str
# Internal dependencies
from .parser import parse_path
@ -22,17 +17,15 @@ COORD_PAIR_TMPLT = re.compile(
r'([\+-]?\d*[\.\d]\d*[eE][\+-]?\d+|[\+-]?\d*[\.\d]\d*)'
)
def path2pathd(path):
return path.get('d', '')
def ellipse2pathd(ellipse):
"""converts the parameters from an ellipse or a circle to a string for a
Path object d-attribute"""
cx = ellipse.get('cx', 0)
cy = ellipse.get('cy', 0)
cx = ellipse.get('cx', None)
cy = ellipse.get('cy', None)
rx = ellipse.get('rx', None)
ry = ellipse.get('ry', None)
r = ellipse.get('r', None)
@ -51,17 +44,13 @@ 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'
return d + 'z'
return d
def polyline2pathd(polyline, is_polygon=False):
def polyline2pathd(polyline_d, is_polygon=False):
"""converts the string from a polyline points-attribute to a string for a
Path object d-attribute"""
if isinstance(polyline, str):
points = polyline
else:
points = COORD_PAIR_TMPLT.findall(polyline.get('points', ''))
points = COORD_PAIR_TMPLT.findall(polyline_d)
closed = (float(points[0][0]) == float(points[-1][0]) and
float(points[0][1]) == float(points[-1][1]))
@ -77,13 +66,13 @@ def polyline2pathd(polyline, is_polygon=False):
return d
def polygon2pathd(polyline):
def polygon2pathd(polyline_d):
"""converts the string from a polygon points-attribute to a string
for a Path object d-attribute.
Note: For a polygon made from n points, the resulting path will be
composed of n lines (even if some of these lines have length zero).
"""
return polyline2pathd(polyline, True)
return polyline2pathd(polyline_d, True)
def rect2pathd(rect):
@ -91,48 +80,18 @@ def rect2pathd(rect):
The rectangle will start at the (x,y) coordinate specified by the
rectangle object and proceed counter-clockwise."""
x, y = float(rect.get('x', 0)), float(rect.get('y', 0))
x0, y0 = float(rect.get('x', 0)), float(rect.get('y', 0))
w, h = float(rect.get('width', 0)), float(rect.get('height', 0))
if 'rx' in rect or 'ry' in rect:
# if only one, rx or ry, is present, use that value for both
# https://developer.mozilla.org/en-US/docs/Web/SVG/Element/rect
rx = rect.get('rx', None)
ry = rect.get('ry', None)
if rx is None:
rx = ry or 0.
if ry is None:
ry = rx or 0.
rx, ry = float(rx), float(ry)
d = "M {} {} ".format(x + rx, y) # right of p0
d += "L {} {} ".format(x + w - rx, y) # go to p1
d += "A {} {} 0 0 1 {} {} ".format(rx, ry, x+w, y+ry) # arc for p1
d += "L {} {} ".format(x+w, y+h-ry) # above p2
d += "A {} {} 0 0 1 {} {} ".format(rx, ry, x+w-rx, y+h) # arc for p2
d += "L {} {} ".format(x+rx, y+h) # right of p3
d += "A {} {} 0 0 1 {} {} ".format(rx, ry, x, y+h-ry) # arc for p3
d += "L {} {} ".format(x, y+ry) # below p0
d += "A {} {} 0 0 1 {} {} z".format(rx, ry, x+rx, y) # arc for p0
return d
x0, y0 = x, y
x1, y1 = x + w, y
x2, y2 = x + w, y + h
x3, y3 = x, y + h
x1, y1 = x0 + w, y0
x2, y2 = x0 + w, y0 + h
x3, y3 = x0, y0 + h
d = ("M{} {} L {} {} L {} {} L {} {} z"
"".format(x0, y0, x1, y1, x2, y2, x3, y3))
return d
def line2pathd(l):
return (
'M' + l.attrib.get('x1', '0') + ' ' + l.attrib.get('y1', '0')
+ 'L' + l.attrib.get('x2', '0') + ' ' + l.attrib.get('y2', '0')
)
return 'M' + l['x1'] + ' ' + l['y1'] + 'L' + l['x2'] + ' ' + l['y2']
def svg2paths(svg_file_location,
return_svg_attributes=False,
@ -149,9 +108,7 @@ def svg2paths(svg_file_location,
SVG Path, Line, Polyline, Polygon, Circle, and Ellipse elements.
Args:
svg_file_location (string or file-like object): the location of the
svg file on disk or a file-like object containing the content of a
svg file
svg_file_location (string): the location of the svg file
return_svg_attributes (bool): Set to True and a dictionary of
svg-attributes will be extracted and returned. See also the
`svg2paths2()` function.
@ -175,10 +132,8 @@ def svg2paths(svg_file_location,
list: The list of corresponding path attribute dictionaries.
dict (optional): A dictionary of svg-attributes (see `svg2paths2()`).
"""
# strings are interpreted as file location everything else is treated as
# file-like object and passed to the xml parser directly
from_filepath = isinstance(svg_file_location, str) or isinstance(svg_file_location, FilePathLike)
svg_file_location = os.path.abspath(svg_file_location) if from_filepath else svg_file_location
if os_path.dirname(svg_file_location) == '':
svg_file_location = os_path.join(getcwd(), svg_file_location)
doc = parse(svg_file_location)
@ -197,14 +152,14 @@ def svg2paths(svg_file_location,
# path strings, add to list
if convert_polylines_to_paths:
plins = [dom2dict(el) for el in doc.getElementsByTagName('polyline')]
d_strings += [polyline2pathd(pl) for pl in plins]
d_strings += [polyline2pathd(pl['points']) for pl in plins]
attribute_dictionary_list += plins
# Use minidom to extract polygon strings from input SVG, convert to
# path strings, add to list
if convert_polygons_to_paths:
pgons = [dom2dict(el) for el in doc.getElementsByTagName('polygon')]
d_strings += [polygon2pathd(pg) for pg in pgons]
d_strings += [polygon2pathd(pg['points']) for pg in pgons]
attribute_dictionary_list += pgons
if convert_lines_to_paths:
@ -258,26 +213,3 @@ def svg2paths2(svg_file_location,
convert_polylines_to_paths=convert_polylines_to_paths,
convert_polygons_to_paths=convert_polygons_to_paths,
convert_rectangles_to_paths=convert_rectangles_to_paths)
def svgstr2paths(svg_string,
return_svg_attributes=False,
convert_circles_to_paths=True,
convert_ellipses_to_paths=True,
convert_lines_to_paths=True,
convert_polylines_to_paths=True,
convert_polygons_to_paths=True,
convert_rectangles_to_paths=True):
"""Convenience function; identical to svg2paths() except that it takes the
svg object as string. See svg2paths() docstring for more
info."""
# wrap string into StringIO object
svg_file_obj = StringIO(svg_string)
return svg2paths(svg_file_location=svg_file_obj,
return_svg_attributes=return_svg_attributes,
convert_circles_to_paths=convert_circles_to_paths,
convert_ellipses_to_paths=convert_ellipses_to_paths,
convert_lines_to_paths=convert_lines_to_paths,
convert_polylines_to_paths=convert_polylines_to_paths,
convert_polygons_to_paths=convert_polygons_to_paths,
convert_rectangles_to_paths=convert_rectangles_to_paths)

View File

@ -1 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -70,8 +70,7 @@
<g
id="nested group - translate xy"
transform="
translate(20, 30)">
transform="translate(20, 30)">
<path
d="M 150,200 l -50,25"

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

@ -1,19 +0,0 @@
<?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>

Before

Width:  |  Height:  |  Size: 665 B

View File

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

View File

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

View File

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

View File

@ -1,19 +1,10 @@
"""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 svgpathtools import *
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)
@ -35,7 +26,7 @@ class TestGroups(unittest.TestCase):
# 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()
# * paths is the output of doc.flatten_all_paths()
v_s_vals.append(1.0)
v_e_relative_vals.append(0.0)
v_s = np.array(v_s_vals)
@ -43,27 +34,11 @@ class TestGroups(unittest.TestCase):
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))
self.check_values(tf.dot(v_s), actual.path.start)
self.check_values(tf.dot(v_e), actual.path.end)
def test_group_flatten(self):
# Test the Document.paths() function against the
# Test the Document.flatten_all_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.
@ -73,7 +48,7 @@ class TestGroups(unittest.TestCase):
# that are specified by the SVG standard.
doc = Document(join(dirname(__file__), 'groups.svg'))
result = doc.paths()
result = doc.flatten_all_paths()
self.assertEqual(12, len(result))
tf_matrix_group = np.array([[1.5, 0.0, -40.0],
@ -190,14 +165,6 @@ class TestGroups(unittest.TestCase):
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)
@ -256,10 +223,3 @@ class TestGroups(unittest.TestCase):
path = parse_path(path_d)
svg_path = doc.add_path(path, group=new_leaf)
self.assertEqual(path_d, svg_path.get('d'))
# Test that paths are added to the correct group
new_sibling = doc.get_or_add_group(
['base_group', 'new_parent', 'new_sibling'])
doc.add_path(path, group=new_sibling)
self.assertEqual(len(new_sibling), 1)
self.assertEqual(path_d, new_sibling[0].get('d'))

View File

@ -1,9 +1,8 @@
# Note: This file was taken mostly as is from the svg.path module (v 2.0)
from __future__ import division, absolute_import, print_function
import unittest
from svgpathtools import Path, Line, QuadraticBezier, CubicBezier, Arc, parse_path
from svgpathtools import *
import svgpathtools
import numpy as np
@ -248,41 +247,3 @@ class TestParser(unittest.TestCase):
skewX(40)
scale(10 0.5)""")
))
def test_pathd_init(self):
path0 = Path('')
path1 = parse_path("M 100 100 L 300 100 L 200 300 z")
path2 = Path("M 100 100 L 300 100 L 200 300 z")
self.assertEqual(path1, path2)
path1 = parse_path("m 100 100 L 300 100 L 200 300 z", current_pos=50+50j)
path2 = Path("m 100 100 L 300 100 L 200 300 z")
self.assertNotEqual(path1, path2)
path1 = parse_path("m 100 100 L 300 100 L 200 300 z")
path2 = Path("m 100 100 L 300 100 L 200 300 z", current_pos=50 + 50j)
self.assertNotEqual(path1, path2)
path1 = parse_path("m 100 100 L 300 100 L 200 300 z", current_pos=50 + 50j)
path2 = Path("m 100 100 L 300 100 L 200 300 z", current_pos=50 + 50j)
self.assertEqual(path1, path2)
path1 = parse_path("m 100 100 L 300 100 L 200 300 z", 50+50j)
path2 = Path("m 100 100 L 300 100 L 200 300 z")
self.assertNotEqual(path1, path2)
path1 = parse_path("m 100 100 L 300 100 L 200 300 z")
path2 = Path("m 100 100 L 300 100 L 200 300 z", 50 + 50j)
self.assertNotEqual(path1, path2)
path1 = parse_path("m 100 100 L 300 100 L 200 300 z", 50 + 50j)
path2 = Path("m 100 100 L 300 100 L 200 300 z", 50 + 50j)
self.assertEqual(path1, path2)
def test_issue_99(self):
p = Path("M 100 250 S 200 200 200 250 300 300 300 250")
self.assertEqual(p.d(useSandT=True), 'M 100.0,250.0 S 200.0,200.0 200.0,250.0 S 300.0,300.0 300.0,250.0')
self.assertEqual(p.d(),
'M 100.0,250.0 C 100.0,250.0 200.0,200.0 200.0,250.0 C 200.0,300.0 300.0,300.0 300.0,250.0')
self.assertNotEqual(p.d(),
'M 100.0,250.0 C 100.0,250.0 200.0,200.0 200.0,250.0 C 200.0,250.0 300.0,300.0 300.0,250.0')

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

@ -1,16 +1,7 @@
from __future__ import division, absolute_import, print_function
import unittest
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 svgpathtools import *
from os.path import join, dirname
from sys import version_info
import tempfile
import shutil
from svgpathtools.svg_to_paths import rect2pathd
class TestSVG2Paths(unittest.TestCase):
def test_svg2paths_polygons(self):
@ -59,78 +50,3 @@ class TestSVG2Paths(unittest.TestCase):
self.assertTrue(len(path_circle)==2)
self.assertTrue(path_circle==path_circle_correct)
self.assertTrue(path_circle.isclosed())
# test for issue #198 (circles not being closed)
svg = u"""<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg" width="40mm" height="40mm"
viewBox="0 0 40 40" version="1.1">
<g id="layer">
<circle id="c1" cx="20.000" cy="20.000" r="11.000" />
<circle id="c2" cx="20.000" cy="20.000" r="5.15" />
</g>
</svg>"""
tmpdir = tempfile.mkdtemp()
svgfile = os.path.join(tmpdir, 'test.svg')
with open(svgfile, 'w') as f:
f.write(svg)
paths, _ = svg2paths(svgfile)
self.assertEqual(len(paths), 2)
self.assertTrue(paths[0].isclosed())
self.assertTrue(paths[1].isclosed())
shutil.rmtree(tmpdir)
def test_rect2pathd(self):
non_rounded = {"x":"10", "y":"10", "width":"100","height":"100"}
self.assertEqual(rect2pathd(non_rounded), 'M10.0 10.0 L 110.0 10.0 L 110.0 110.0 L 10.0 110.0 z')
rounded = {"x":"10", "y":"10", "width":"100","height":"100", "rx":"15", "ry": "12"}
self.assertEqual(rect2pathd(rounded), "M 25.0 10.0 L 95.0 10.0 A 15.0 12.0 0 0 1 110.0 22.0 L 110.0 98.0 A 15.0 12.0 0 0 1 95.0 110.0 L 25.0 110.0 A 15.0 12.0 0 0 1 10.0 98.0 L 10.0 22.0 A 15.0 12.0 0 0 1 25.0 10.0 z")
def test_from_file_path_string(self):
"""Test reading svg from file provided as path"""
paths, _ = svg2paths(join(dirname(__file__), 'polygons.svg'))
self.assertEqual(len(paths), 2)
def test_from_file_path(self):
"""Test reading svg from file provided as pathlib POSIXPath"""
if version_info >= (3, 6):
import pathlib
paths, _ = svg2paths(pathlib.Path(__file__).parent / 'polygons.svg')
self.assertEqual(len(paths), 2)
def test_from_file_object(self):
"""Test reading svg from file object that has already been opened"""
with open(join(dirname(__file__), 'polygons.svg'), 'r') as file:
paths, _ = svg2paths(file)
self.assertEqual(len(paths), 2)
def test_from_stringio(self):
"""Test reading svg object contained in a StringIO object"""
with open(join(dirname(__file__), 'polygons.svg'),
'r', encoding='utf-8') as file:
# read entire file into string
file_content = file.read()
# prepare stringio object
file_as_stringio = StringIO(file_content)
paths, _ = svg2paths(file_as_stringio)
self.assertEqual(len(paths), 2)
def test_from_string(self):
"""Test reading svg object contained in a string"""
with open(join(dirname(__file__), 'polygons.svg'),
'r', encoding='utf-8') as file:
# read entire file into string
file_content = file.read()
paths, _ = svgstr2paths(file_content)
self.assertEqual(len(paths), 2)
if __name__ == '__main__':
unittest.main()

View File

@ -1,49 +0,0 @@
<?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>

Before

Width:  |  Height:  |  Size: 1.5 KiB