svgelements
does high fidelity SVG parsing and geometric rendering. The goal is to successfully and correctly process SVG for use with any scripts that may need or want to use SVG files as geometric data.
This is both facilitated by, and results in, very useful elements within the SVG spec: Path, Matrix, Angle, Length, Color, Point and other SVG and CSS Elements. The SVG spec defines a variety of elements which generally interoperate. In order to have a robust experience with SVGs we must be able to correctly deal with the parsing and interactions of these elements.
This project began as part of meerK40t
which does SVG loading of files for laser cutting. It attempts to more fully map out the SVG specification, objects, and paths, while remaining easy to use and largely backwards compatible. These elements are quite useful in their own right. For example, the zooming and panning within meerK40t
is done using the SVG matrix which more robust than the wxPython one. Internal console commands within meerK40t
allows specifying robustly parsed angles of rotation, colors of objects, and naively uses the Path() and SVGImage objects. The ability to have these robustly manipulated with affine transformations provides considerable utility. There is significant utility in the interactions between these objects, however if you just want to robustly parse some SVG and convert the data to your own structures that is entirely reasonable.
Without robust SVG parsing you'll find repeated edge cases of some svg files that do not parse correctly. svgelements
aims to avoid those pitfalls with robust adherence to the SVG spec.
This module is under a MIT License. https://github.com/meerk40t/svgelements/blob/master/LICENSE
pip install svgelements
Then in a script:
from svgelements import *
None.
However, there are some soft dependencies, with some common additions do modify the functionality slightly. If scipy
is installed then the arc length code quickly provide the exact correct answer. Some of the SVGImage code is able to load the images if given access to PIL/Pillow. And if numpy
exists there's a special npoint()
command to do lightning fast linearization for Shapes.
svgelements
is compatible with Python 3+. Support for 2.7 was dropped at Python 2 End-Of-Life January 1, 2020.
We remain nominally backwards compatible with svg.path
, passing the same robust tests from that project. There may be number of breaking changes. However, since svgelements
permit a lot of leeway in what is accepted and how it's accepted it will have a huge degree of compatibility with projects seen and unseen.
The goal of this project is to provide SVG spec-like elements and structures. Conforming to the SVG standard 1.1 and elements of 2.0. These provide much of the implementation decisions, with regard to the implementation of the objects. If there is a question on implementation and the SVG documentation has a methodology, that is the preferred methodology. If the SVG spec says one thing, and svgelements
does something else, that is a bug.
The primary goal of this project is to make a more robust version of svg.path
to fully parse SVG files. This requires including other elements like Point
, Matrix
, and Color
, etc. with clear emphasis on conforming to the SVG spec in all ways that realworld uses for SVG demands.
svgelements
should conform to the SVG Conforming Interpreter class (2.5.4. Conforming SVG Interpreters):
An SVG interpreter is a program which can parse and process SVG document fragments. Examples of SVG interpreters are server-side transcoding tools or optimizer (e.g., a tool which converts SVG content into modified SVG content) or analysis tools (e.g., a tool which extracts the text content from SVG content, or a validity checker).
Real world functionality demands we must correctly and reasonably provide reading, transcoding, and manipulation of SVG content.
The svgelements code should not include any hard dependencies. It should remain a single file with emphasis on allowing projects to merely include a copy of svgelements.py
to do any SVG parsing required.
SVG is a huge spec and bleeds into a lot of areas. Many of these are supported some are not.
- Robust SVG parsing.
- Basic SVG writing
- SVG/CSS Lengths:
px
,pt
,pc
,cm
,mm
,in
,%
- SVG/CSS Color: keyword,
#rrggbb
,#rgb
,rgb(r,g,b)
,rgb(r%,g%,b%)
,rgba(r,g,b,a)
,hsl(hue, s, l)
- SVG/CSS Matrix
- Full matrix support. All objects have a
.transform
object with all cascading matrix operations. Including viewport.
- Full matrix support. All objects have a
- SVG Viewport. - Correctly processes viewports, including
preserveAspectRatio
. - CSS Angle -
deg
,rad
,grad
,turn
. Full use of these withinrotate(<angle>)
transformation command. - SVG Shape: Rect - full parsing of
x
,y
,rx
,ry
,width
,height
, presentation attributes in length/percent form. - SVG Shape: Circle - full parsing of
cx
,cy
,r
, presentation attributes in length/percent form. - SVG Shape: Ellipse - full parsing of
cx
,cy
,rx
,ry
, presentation attributes in length/percent form. - SVG Shape: Polygon - full parsing of
.points
- SVG Shape: Polyline - full parsing of
.points
- SVG Shape: Line - full parsing of
x0
,y0
,x1
,y1
presentation attributes in length/percent form.- (Internally this is called
SimpleLine
sinceLine
is aPathSegment
type.)
- (Internally this is called
- SVG Shape: Path - Perfect path_d parsing. Relative/Absolute, Smooth BezierCurve, preservation of original segment form.
- PathSegments with advanced geometric functions. eg.
point()
,npoint()
,bbox()
reverse()
- Transformation of Shapes and Paths, within groups as well as with respect to the SVG Viewport.
- First order use of
.stroke
,.fill
and.stroke_width
for all Shapes.Stroke
andFill
are colors (Fill may return aPattern
, or Gradient-type in future versions, but is currently always a Color), andstroke_width
is a length/percent value (will be rendered to a float during parsing). - SVG Spec deconstruction of basic shapes into Paths within regard to the SVG 2.0 spec. Path(shape) or shape.d()
Group
objects. Container class.clipPath
objects, these are assigned as a.clip_path
to any object that referenced them.<defs>
and<use>
functionality within the parsing tree.- Accurate referencing of objects in the ShadowDOM.
Pattern
objects. These are parsed they are not currently assigned.Text
objects. The lack of a font engine makes this class more of a parsed stub class.Image
createsSVGImage
objects which will load Images ifPillow
is installed with a call to.load()
. Correct parsing ofx
,y
,width
,height
andviewbox
.Desc
description object.Title
description object.- Nested
SVG
objects. (Caveats see Non-Supported). - CSS Styling.
Some things are currently not supported.
- Full CSS/DOM specific parsing and modifications.
- Full CSS StyleSheet. Stylesheets should be read anywhere in the file and styled all matching objects even those already parsed. We accept Styling that occurs before the objects.
- Color: OS Specific System colors.
Script
and Scripting.RadialGradient
FillsLinearGradient
FillsPattern
linking to Fill the IRI linked object.a
hyperlink text objectsSwitch
elements.Marker
elements.Symbol
elements.Masking
elements.TextPath
elements.Metadata
elements.- Nesting of
SVG
elements within anImage
object. em
,ex
length and font engine requiring code. (the height of 'm' and 'x' is unknown).- Slicing of SVG geometry, outside of viewbox.
- Slicing of SVG geometry, within clipPath
- External Loading of SVG files.
- External loading of SVGz files.
- External loading of CSS data from another file.
- SVG Animation
- Styling based on Descendant, Child, FirstChild, Sibling, Attribute, AttributeWithValue.
Glyph
- Dropped in SVG 2.0tref
- Dropped in SVG 2.0
The primary function of svgelements
is to parse svg files. There are two main functions to facilitate this
def parse(source,
reify=True,
ppi=DEFAULT_PPI,
width=1,
height=1,
color="black",
transform=None,
context=None):
This parse function takes in values that cannot be known to the SVG but which are essential to the the rendering of the shapes. Parsing will pre-apply things like the relative translation by the viewport. It will solve the structural changes for the with the <use>
and <defs>
, and any items that are known SVG elements will be turned into their requisite values and parsed accordingly. So the .fill
and .stroke
of a Path
will be filled in with a type of Color
and the .transform
of the Shape will be a type of Matrix
. The .values
for all the SVGElement
will have the relevant inherited values. This permits parsing to deal with even unknown types of objects within the SVG by falling back to something akin to DOM parsing of the file. In cases of <use>
and <defs>
these unknown elements can still reference other. Since this structural shadow tree will be solved during the parse.
parse()
is a static function which takes a source
file or stream of svg data to be parsed. This will return an SVG
object which is a type of Group
. There are several values which can be configured with other values as needed. reify
determines whether the parsed elements in the SVG
should have their transform matrix applied or not. This includes the effective matrix resulting from viewport.
The ppi
value defaults to 96
as this is quite common some other graphics programs use 72
and other values are permitted. Since there's nothing directly in the SVG spec setting this value and other places can vary with their value here. We can't predetermine this value. However it regulates all the relationships between physical values like a 1in by 1in rect
to the unitless pixel values of the SVG.
The width
and height
values are unknown to the SVG parsing. This is the physical view size of the svg itself. This often will have little impact in the SVG rendering however sometimes things widths are set to 100%
or heights to 50%
and those are according to the spec relative to the actual view we're using. The svg has no direct access to this. If we have a ppi
value these height and width can be set to absolute units like 6in
or any other acceptable Length
value that can be solved with ppi
.
The color
is the value of the currentColor
within the SVG spec. Usually the default stroke and fill values are set but in some cases these are set to currentColor
which is a property of the CSS outside the scope of the SVG. In this case that color needs to be provided. It will be a rare edge case.
The transform
value is typical CSS/SVG transform matrix code to be preappended to the matrix before even the viewbox. If you need to set some units or apply something to the entire svg without changing things within the CSS this value becomes important. Especially when dealing with edge cases like the difference of transform
applied directly to the SVG
tag itself.
The context
permits giving a context of already set values that are come from outside the current svg context, such as we would find if we had SVG files embedded into SVG files.
The second function within parsing that matters is the .elements()
this is a function that exists on any SVG
object and will flatten the elements yielding them in order.
Here's an example parser with elements().
for element in svg.elements():
try:
if element.values['visibility'] == 'hidden':
continue
except (KeyError, AttributeError):
pass
if isinstance(element, SVGText):
elements.append(element)
elif isinstance(element, Path):
if len(element) != 0:
elements.append(element)
elif isinstance(element, Shape):
e = Path(element)
e.reify() # In some cases the shape could not have reified, the path must.
if len(e) != 0:
elements.append(e)
elif isinstance(element, SVGImage):
try:
element.load(os.path.dirname(pathname))
if element.image is not None:
elements.append(element)
except OSError:
pass
Here a few things are checked. The element.values for ['visibility'] is checked if it's hidden it is not added to our flat object list. Texts are specific added. Paths are only added if they have PathSegments
and are not completely blank. Any Shape object is converted to a Path() object and reified. Any SVGImage objects are loaded. This is a soft dependency on PIL/Pillow to load images stored within SVG. The SVG .elements()
function can also take a conditional function that well be used to test each element before yielding it. In most cases we don't want every single type of thing an svg can produce. We might just want all the Path objects so we check for any Path and include that but also for any non-Path Shape and convert that to a path. pathname
is an attempt to get the local directory for loading relative path images.
Circa 1.9.0+ some basic SVG writing was added. Any SVGElement
object will permit you to call write_xml(filename)
the expectation is that you save svg files. If you specify an svgz
file it will gzip the save stream providing you with a svgz
file. If you want the xml for a different purpose you may also call string_xml
which provides the object as a string.
>>> Group(id="group").string_xml()
'<g id="group" />'
This is not intended to be perfect, the project itself is potentially lossy and CSS tables and style tags will be gone, Use
objects will be replaced with their real objects and these modifications are not able to be restored. The primary purpose of this project is to read correct geometric data.
>>> SVG().write_xml("empty.svg")
Writes a file called empty.svg
.
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:ev="http://www.w3.org/2001/xml-events" width="100%" height="100%" />
Or getting slightly more complex:
s = SVG()
s.append(Rect(0,0,"2in", "2in"))
s.write_xml("rect.svg")
Produces a file called rect.svg
.
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:ev="http://www.w3.org/2001/xml-events" width="100%" height="100%">
<rect width="2in" height="2in" />
</svg>
Writing should work, but this is not the initial purpose of the library. Some non-parsed elements will restore to the file correctly, others may be lost. However, errors in this should still be reported. The reading is quite well vetted and robust, the writing may have issues needing to be corrected.
The versatility of the project is provided through expansive and highly intuitive dunder methods, and robust parsing of object parameters. Points, PathSegments, Paths, Shapes, Subpaths can be multiplied by a matrix. We can add Shapes, Paths, PathSegments, and Subpaths together. And many non-declared but functionally understandable elements are automatically parsed. Such as adding strings of path_d characters to a Path or multiplying an element by the SVG Transform string elements.
While many objects perform a lot of interoperations, a lot many svg elements are designed to also work independently, and be independently useful.
Points define a single location in 2D space. The Point class is intended to take a wide variety of different initial definitions to wrap them into being a point.
- Point(x,y)
- (x,y)
- [x,y]
- "x, y"
- x + yj (complex number)
- a class with .x and .y as methods.
Most objects requiring a point will wrap that object with the included Point class meaning any of these initial arguments is acceptable. Including independent x and y parameters, a tuple of x and y, a list of x and y, a string that parses akin to points within polyline
objects, complex numbers with a real x and imag y values. And any class with .x
or .y
attributes.
>>> Point(10,10) * "rotate(90)"
Point(-10,10)
Matrices define affine transformations of 2d space and objects.
- Matrix.scale(s)
- Matrix.scale(sx,sy)
- Matrix.scale(sx,sy,px,py)
- Matrix.rotate(angle)
- Matrix.rotate(angle, px, py
- Matrix.skew_x(angle)
- Matrix.skew_x(angle, px, py)
- Matrix.skew_y(angle)
- Matrix.skew_y(angle, px, py)
- Matrix.translate(tx)
- Matrix.translate(tx, ty)
- Transform string values.
- "scale(s)"
- "scale(sx,sy)"
- "translate(20,20) scale(2)"
- "rotate(0.25 turns)"
- Any valid SVG or CSS transform string will be accepted as a matrix.
>>> Matrix("rotate(100grad)")
Matrix(0, 1, -1, 0, 0, 0)
The matrix class also supports Length translates for x, and y. In some instances, CSS transforms permit length transforms so "translate(20cm, 200mm)" are valid transformations. However, these will cause issues for objects which require non-native units so it is expected that .render()
will be called on these before they are used in some manner.
Paths define sequences of PathSegments that can map out any path element in SVG.
- Path() object
- String path_d value.
>>> Path() + "M0,0z"
Path(Move(end=Point(0,0)), Close(start=Point(0,0), end=Point(0,0)))
Angles define various changes in direction.
- Angle.degrees(degree_angle)
- Angle.radians(radians_angle)
- Angle.turns(turns)
- Angle.gradians(gradian_angles)
- CSS angle string.
- "20deg"
- "0.3turns"
- "1rad"
- "100grad"
>>> Point(0,100) * "rotate(1turn)"
Point(0,100)
>>> Point(0,100) * "rotate(0.5turn)"
Point(-0,-100)
Colors define object color.
- XHTML color names: "red", "blue", "dark grey", etc.
- 3 digit hex: "#F00"
- 4 digit hex: "#FF00"
- 6 digit hex: "#FF0000"
- 8 digit hex: "#FFFF0000"
- "RGB(r,g,b)"
- "RGB(r%, g%, b%)"
>>> Circle(stroke="yellow")
Circle(center=Point(0,0), r=1, stroke="#ffff00")
Lengths define the amount of linear space between two things.
- "20cm"
- "200mm"
- "3in"
- Length('200mm')
Parse an SVG file:
>>> svg = SVG.parse(file)
>>> list(svg.elements())
Make a PathSegment
>>> Line((20,20), (40,40))
Line(start=Point(20,20), end=Point(40,40))
Rotate a PathSegment:
>>> Line((20,20), (40,40)) * Matrix.rotate(Angle.degrees(45))
Line(start=Point(0,28.284271247462), end=Point(0,56.568542494924))
Rotate a PathSegment with a parsed matrix:
>>> Line((20,20), (40,40)) * Matrix("Rotate(45)")
Line(start=Point(0,28.284271247462), end=Point(0,56.568542494924))
Rotate a PathSegment with an implied parsed matrix:
>>> Line((20,20), (40,40)) * "Rotate(45)"
Line(start=Point(0,28.284271247462), end=Point(0,56.568542494924))
Rotate a Partial Path with an implied matrix: (Note: The SVG does not allow us to specify a start point for this invalid path)
>>> Path("L 40,40") * "Rotate(45)"
Path(Line(end=Point(40,40)), transform=Matrix(0.707106781187, 0.707106781187, -0.707106781187, 0.707106781187, 0, 0), stroke='None', fill='None')
>>> abs(Path("L 40,40") * "Rotate(45)")
Path(Line(end=Point(0,56.568542494924)), stroke='None', fill='None')
Since Move() is a qualified element we can postpend the SVG text:
>>> (Move((20,20)) + "L 40,40")
Path(Move(end=Point(20,20)), Line(start=Point(20,20), end=Point(40,40)), stroke='None', fill='None')
Define the entire qualified path:
>>> Path("M 20,20 L 40,40")"
Path(Move(end=Point(20,20)), Line(start=Point(20,20), end=Point(40,40)))
Combine individual PathSegments together:
>>> Move((2,2)) + Close()
Path(Move(end=Point(2,2)), Close())
Print that as SVG path_d object:
>>> print(Move((2,2)) + Close())
M 2,2 Z
Scale a path:
>>> Path("M1,1 1,2 2,2 2,1z") * "scale(2)"
Path(Move(end=Point(1,1)), Line(start=Point(1,1), end=Point(1,2)), Line(start=Point(1,2), end=Point(2,2)), Line(start=Point(2,2), end=Point(2,1)), Close(start=Point(2,1), end=Point(1,1)), transform=Matrix(2, 0, 0, 2, 0, 0), stroke='None', fill='None')
Print that:
>>> print(Path("M1,1 1,2 2,2 2,1z") * "scale(2)")
M 2,2 L 2,4 L 4,4 L 4,2 Z
Reverse a scaled path:
>>> p = (Path("M1,1 1,2 2,2 2,1z") * "scale(2)")
>>> p.reverse()
Path(Move(end=Point(2,1)), Line(start=Point(2,1), end=Point(2,2)), Line(start=Point(2,2), end=Point(1,2)), Line(start=Point(1,2), end=Point(1,1)), Close(start=Point(1,1), end=Point(2,1)), transform=Matrix(2, 0, 0, 2, 0, 0), stroke='None', fill='None')
>>> print(p)
M 4,2 L 4,4 L 2,4 L 2,2 Z
Query length of paths:
>>> QuadraticBezier("0,0", "50,50", "100,0").length()
114.7793574696319
Apply a translations:
>>> Path('M 0,0 Q 50,50 100,0') * "translate(40,40)"
Path(Move(end=Point(0,0)), QuadraticBezier(start=Point(0,0), control=Point(50,50), end=Point(100,0)), transform=Matrix(1, 0, 0, 1, 40, 40), stroke='None', fill='None')
>>> abs(Path('M 0,0 Q 50,50 100,0') * "translate(40,40)")
Path(Move(end=Point(40,40)), QuadraticBezier(start=Point(40,40), control=Point(90,90), end=Point(140,40)), stroke='None', fill='None')
Query lengths of translated paths:
>>> (Path('M 0,0 Q 50,50 100,0') * "translate(40,40)").length()
114.7793574696319
>>> Path('M 0,0 Q 50,50 100,0').length()
114.7793574696319
Query a subpath:
>>> Path('M 0,0 Q 50,50 100,0 M 20,20 v 20 h 20 v-20 h-20 z').subpath(1).d()
'M 20,20 L 20,40 L 40,40 L 40,20 L 20,20 Z'
Reverse a subpath:
>>> p = Path('M 0,0 Q 50,50 100,0 M 20,20 v 20 h 20 v-20 h-20 z')
>>> print(p)
M 0,0 Q 50,50 100,0 M 20,20 L 20,40 L 40,40 L 40,20 L 20,20 Z
>>> p.subpath(1).reverse()
Path(Move(start=Point(100,0), end=Point(20,20)), Line(start=Point(20,20), end=Point(40,20)), Line(start=Point(40,20), end=Point(40,40)), Line(start=Point(40,40), end=Point(20,40)), Line(start=Point(20,40), end=Point(20,20)), Close(start=Point(20,20), end=Point(20,20)))
>>> print(p)
M 0,0 Q 50,50 100,0 M 20,20 L 40,20 L 40,40 L 20,40 L 20,20 Z
Query a bounding box:
>>> QuadraticBezier("0,0", "50,50", "100,0").bbox()
(0.0, 0.0, 100.0, 50.0)
Query a translated bounding box:
>>> (Path('M 0,0 Q 50,50 100,0') * "translate(40,40)").bbox()
(40.0, 40.0, 140.0, 90.0)
Query a translated path's untranslated bounding box.
>>> (Path('M 0,0 Q 50,50 100,0') * "translate(40,40)").bbox(transformed=False)
(0.0, 0.0, 100.0, 50.0)
Add a path and shape:
>>> print(Path("M10,10z") + Circle("12,12", 2))
M 10,10 Z M 14,12 A 2,2 0 0,1 12,14 A 2,2 0 0,1 10,12 A 2,2 0 0,1 12,10 A 2,2 0 0,1 14,12 Z
Add two shapes, and query their bounding boxes:
>>> (Circle() + Rect()).bbox()
(-1.0, -1.0, 1.0, 1.0)
Add two shapes and query their length:
>>> (Circle() + Rect()).length()
10.283185307179586
>>> tau + 4
10.283185307179586
Etc.
The elements are the core functionality of this class. These are svg-based objects which interact in coherent ways.
The Path element is based on regebro's code and methods from the svg.path
project. The primary methodology is to use different PathSegment classes for each segment within a pathd code. These should always have a high degree of backwards compatibility. And for most purposes importing the relevant classes from svgelements
should be highly compatible with any existing code.
For this reason svgelements
tests include svg.path
tests in this project. And while the Point class accepts and works like a complex
it is not actually a complex. This permits code from other projects to quickly port without requiring an extensive rewrite. But, the custom class allows for improvements like making the Matrix
object easy.
Path(*segments)
Just as with svg.path
the Path
class is a mutable sequence, and it behaves like a list.
You can add to it and replace path segments etc:
>>> path = Path(Line(100+100j,300+100j), Line(100+100j,300+100j))
>>> path.append(QuadraticBezier(300+100j, 200+200j, 200+300j))
>>> print(path)
L 300,100 L 300,100 Q 200,200 200,300
>>> path[1] = Line(200+100j,300+100j)
>>> print(path)
L 300,100 L 300,100 Q 200,200 200,300
>>> del path[1]
>>> print(path)
L 300,100 Q 200,200 200,300
>>> path = Move() + path
>>> print(path)
M 100,100 L 300,100 Q 200,200 200,300
The path object also has a d()
method that will return the
SVG representation of the Path segments:
>>> path.d()
'M 100,100 L 300,100 Q
200,200 200,300'
The d() parameter also takes a value for relative:
>>> path.d(relative=True)
'm 100,100 l 200,0 q -100,100 -100,200'
More modern and preferred methods are to simply use path_d strings where needed.
>>> print(Path("M0,0v1h1v-1z"))
M 0,0 L 0,1 L 1,1 L 1,0 Z
And to use scaling factors as needed.
>>> (Path("M0,0v1h1v-1z") * "scale(20)").bbox()
(0.0, 0.0, 20.0, 20.0)
A Path
object
that is a collection of the PathSegment objects. These can be defined by combining a PathSegment with another PathSegment initializing it with Path()
or Path(*segments)
or Path(<svg_text>)
.
Subpaths provide a window into a Path object. These are backed by the Path they are created from and consequently operations performed on them apply to that part of the path.
>>> p = Path('M 0,0 Q 50,50 100,0 M 20,20 v 20 h 20 v-20 h-20 z')
>>> print(p)
M 0,0 Q 50,50 100,0 M 20,20 L 20,40 L 40,40 L 40,20 L 20,20 Z
>>> q = p.subpath(1)
>>> q *= "scale(2)"
>>> print(p)
M 0,0 Q 50,50 100,0 M 40,40 L 40,80 L 80,80 L 80,40 L 40,40 Z
or likewise .reverse()
(notice the path will go 80,40 first rather than 40,80.)
>>> q.reverse()
>>> print(p)
M 0,0 Q 50,50 100,0 M 40,40 L 80,40 L 80,80 L 40,80 L 40,40 Z
There are 6 PathSegment objects:
Line
, Arc
, CubicBezier
, QuadraticBezier
, Move
and Close
. These have a 1:1 correspondence to the commands in a pathd
.
>>> from svgelements import Path, Line, Arc, CubicBezier, QuadraticBezier, Close
All of these objects have a .point()
function which will return the
coordinates of a point on the path, where the point is given as a floating
point value where 0.0
is the start of the path and 1.0
is end.
You can calculate the length of a Path or its segments with the .length()
function. For CubicBezier and Arc segments this is done by geometric approximation and for this reason may be very slow. You can make it faster by passing in an error
option to the method. If you don't pass in error, it defaults to 1e-12
. While the project has no dependencies, if you have scipy
installed the Arc.length() function will use to the hypergeometric exact formula contained and will quickly return with the exact answer.
>>> CubicBezier(300+100j, 100+100j, 200+200j, 200+300j).length(error=1e-5)
297.2208145656899
CubicBezier and Arc also has a min_depth
option that specifies the
minimum recursion depth. This is set to 5 by default, resulting in using a
minimum of 32 segments for the calculation. Setting it to 0 is a bad idea for
CubicBeziers, as they may become approximated to a straight line.
Line.length()
and QuadraticBezier.length()
also takes these
parameters, but they unneeded as direct values rather than approximations are returned.
CubicBezier and QuadraticBezier also have is_smooth_from(previous)
methods, that checks if the segment is a "smooth" segment compared to the
given segment.
Unlike svg.path
the preferred method of getting a Path from a pathd
string is
as an argument:
>>> from svgelements import Path
>>> Path('M 100 100 L 300 100')
Path(Move(end=Point(100,100)), Line(start=Point(100,100), end=Point(300,100)))
These are the SVG PathSegment classes. See the SVG specifications <http://www.w3.org/TR/SVG/paths.html>
_ for more information on what each
parameter means.
-
Move(start, end)
The move object describes a move to the start of the next subpath. It may lack a start position but not an end position. -
Close(start, end)
The close object describes a close path element. It will have a length if and only if the end point is not equal to the subpath start point. Neither the start point or end point is required. -
Line(start, end)
The line object describes a line moving straight from one point to the next point. -
Arc(start, radius, rotation, arc, sweep, end)
The arc object describes an arc across a circular path. This supports multiple types of parameterizations. The given default there is compatible withsvg.path
and has a complex radius. It is also valid to divide radius intorx
andry
or Arc(start, end, center, prx, pry, sweep) where start, end, center, prx, pry are points and sweep is the radians value of the arc distance traveled. -
QuadraticBezier(start, control, end)
the quadratic bezier object describes a single control point bezier curve. -
CubicBezier(start, control1, control2, end)
the cubic bezier curve object describes a two control point bezier curve.
This SVG path example draws a triangle:
>>> path1 = Path('M 100 100 L 300 100 L 200 300 z')
You can format SVG paths in many different ways, all valid paths should be accepted:
>>> path2 = Path('M100,100L300,100L200,300z')
And these paths should be equal:
>>> path1 == path2
True
You can also build a path from objects:
>>> path3 = Path(Move(100 + 100j), Line(100 + 100j, 300 + 100j), Line(300 + 100j, 200 + 300j), Close(200 + 300j, 100 + 100j))
And it should again be equal to the first path::
>>> path1 == path3
True
Paths are mutable sequences, you can slice and append::
>>> path1.append(QuadraticBezier(300+100j, 200+200j, 200+300j))
>>> len(path1[2:]) == 3
True
Note that there is no protection against you creating paths that are invalid. You can for example have a Close command that doesn't end at the path start:
>>> wrong = Path(Line(100+100j,200+100j), Close(200+300j, 0))
>>> wrong.d()
'L 200,100 Z'
SVG 1.1, 7.15.3 defines the matrix form as:
[a c e]
[b d f]
Since we are delegating to SVG spec for such things, this is how it is implemented in elements.
To be compatible with SVG 1.1 and SVG 2.0 the matrix class provided has all the SVG functions as well as the CSS functions:
- translate(x,[y])
- translateX(x)
- translateY(y)
- scale(x,[y])
- scaleX(x)
- scaleY(y)
- skew(x,[y])
- skewX(x)
- skewY(y)
Since we have compatibility with CSS for the SVG 2.0 spec compatibility we can perform length translations:
>>> Point(0,0) * Matrix("Translate(1cm,1cm)")
Point('1cm','1cm')
Do note, however that this isn't an intended purpose. Points are expected in native units. You should render the Matrix prior to using it. This means you must give it the correct units to translate the information from one form to another.
>>> Point(0,0) * (Matrix("Translate(1cm,1cm)").render(ppi=96.0))
Point(37.795296,37.795296)
We can also rotate by turns
, grad
, deg
, rad
which are permitted CSS angles:
>>> Point(10,0) * Matrix("Rotate(1turn)")
Point(10,-0)
>>> Point(10,0) * Matrix("Rotate(400grad)")
Point(10,-0)
>>> Point(10,0) * Matrix("Rotate(360deg)")
Point(10,-0)
A goal of this project is to provide a robust modifications of Path objects including matrix transformations. This is done by three major shifts from svg.path
s methods.
- Points are not stored as complex numbers. These are stored as Point objects, which have backwards compatibility with complex numbers, without the data actually being backed by a
complex
. - A matrix is added which conforms to the SVGMatrix element. The matrix contains valid versions of all the affine transformations elements required by the SVG Spec.
- The
Arc
object is fundamentally backed by a different point-based parameterization.
The objects themselves have robust dunder methods. So if you have a path object you may simply multiply it by a matrix.
>>> Path(Line(0+0j, 100+100j)) * Matrix.scale(2)
Path(Line(start=Point(0,0), end=Point(100,100)), transform=Matrix(2, 0, 0, 2, 0, 0), stroke='None', fill='None')
Or rotate a parsed path.
>>> Path("M0,0L100,100") * Matrix.rotate(30)
Path(Move(end=Point(0,0)), Line(start=Point(0,0), end=Point(100,100)), transform=Matrix(0.154251449888, -0.988031624093, 0.988031624093, 0.154251449888, 0, 0))
Or modify an SVG path.
>>> str(Path("M0,0L100,100") * Matrix.rotate(30))
'M 0,0 L 114.228,-83.378'
The Matrix objects can be used to modify points:
>>> Point(100,100) * Matrix("scale(2)")
Point(200,200)
>>> Point(100,100) * (Matrix("scale(2)") * Matrix("Translate(40,40)"))
Point(240,240)
Do note that the order of operations for matrices matters:
>>> Point(100,100) * (Matrix("Translate(40,40)") * Matrix("scale(2)"))
Point(280,280)
The first version is:
>>> (Matrix("scale(2)") * Matrix("Translate(40,40)"))
Matrix(2, 0, 0, 2, 40, 40)
The second is:
>>>> (Matrix("Translate(40,40)") * Matrix("scale(2)"))
Matrix(2, 0, 0, 2, 80, 80)
This is:
>>>> Point(100,100) * Matrix("Matrix(2,0,0,2,80,80)")
Point(280,280)
>>> node = { 'd': "M0,0 100,0, 0,100 z", 'transform': "scale(0.5)"}
>>> print(Path(node['d']) * Matrix(node['transform']))
M 0,0 L 50,0 L 0,50 Z
There is need in many applications to append a transformation for the viewbox, height, width. So as to prevent a variety of errors where the expected size is vastly different from the actual size. If we have a viewbox of "0 0 100 100" but the height and width show that to be 50cm wide, then a path "M25,50L75,50" within that viewbox has a real size of length of 25cm which can be quite different from 50 (unitless value).
This conversion is done through the Viewbox
object. This operation is automatically done for during SVG parsing.
Viewbox objects have a call to .transform()
which will provide the string for an equivalent transformation for the given viewbox.
The Viewbox.transform()
code conforms to the algorithm given in SVG 1.1 7.2, SVG 2.0 8.2 'equivalent transform of an SVG viewport.' This will also fully implement the preserveAspectRatio
, xMidYMid
, and meetOrSlice
values for the viewboxes.
Another important SVG elements are the shapes. While all of these can be converted to paths. They can serve some usages in their original form. There are methods to deform a rectangle that simple don't exist in the path form of that object.
- Rect
- Ellipse
- Circle
- Line (SimpleLine)
- Polyline
- Polygon
The Line shape is converted into a shape called SimpleLine to not interfere with the Line(PathSegment).
A Shape is said to be equal to another Shape or a Path if they decompose to same Path.
>>> Circle() == Ellipse()
True
>>> Rect() == Path('m0,0h1v1h-1z')
True
Rectangles are defined by x, y and height, width. Within SVG there are also rounded corners defined with rx
and ry
.
>>> Rect(10,10,8,4).d()
'M 10,10 L 18,10 L 18,14 L 10,14 Z'
Much like all the paths these shapes also contain a .d()
function that produces the path data for them. This could then be wrapped into a Path().
>>> print(Path(Rect(10,10,8,4).d()) * "rotate(0.5turns)")
M -10,-10 L -18,-10 L -18,-14 L -10,-14 Z
Or simply passed to the Path:
>>> print(Path(Rect(10,10,8,4)) * "rotate(0.5turns)")
M -10,-10 L -18,-10 L -18,-14 L -10,-14 L -10,-10 Z
Or simply multiplied by the matrix itself:
>>> print(Rect(10,10,8,4) * "rotate(0.5turns)")
Rect(x=10, y=10, width=8, height=4, transform=Matrix(-1, 0, -0, -1, 0, 0), stroke='None', fill='None')
And you can equally decompose that Shape:
>>> (Rect(10,10,8,4) * "rotate(0.5turns)").d()
'M -10,-10 L -18,-10 L -18,-14 L -10,-14 L -10,-10 Z'
Matrices can be applied to Rect objects directly.
>>> from svgelements import *
>>> Rect(10,10,8,4) * "rotate(0.5turns)"
Rect(x=10, y=10, width=8, height=4, transform=Matrix(-1, 0, -0, -1, 0, 0), stroke='None', fill='None')
>>> Rect(10,10,8,4) * "rotate(0.25turns)"
Rect(x=10, y=10, width=8, height=4, transform=Matrix(0, 1, -1, 0, 0, 0))
Rotated Rects produce path_d strings.:
>>> Rect(10,10,8,4) * "rotate(14deg)"
Rect(x=10, y=10, width=8, height=4, transform=Matrix(0.970295726276, 0.2419218956, -0.2419218956, 0.970295726276, 0, 0))
>>> (Rect(10,10,8,4) * "rotate(14deg)").d()
'M 7.28373830676,12.1221762188 L 15.046104117,14.0575513836 L 14.0784165346,17.9387342887 L 6.31605072436,16.0033591239 Z'
This also works with rx
and ry
:
(Note: the path will now contain Arcs)
>>> (Rect(10,10,8,4, 2, 1) * "rotate(0.25turns)").d()
'M -10,12 L -10,16 A 2,1 90 0,1 -11,18 L -13,18 A 2,1 90 0,1 -14,16 L -14,12 A 2,1 90 0,1 -13,10 L -11,10 A 2,1 90 0,1 -10,12 Z'
You can also decompose the shapes in relative modes:
>>> (Rect(10,10,8,4, 2, 1) * "rotate(0.25turns)").d(relative=True)
'm -10,12 l 1.77636E-15,4 a 2,1 90 0,1 -1,2 l -2,0 a 2,1 90 0,1 -1,-2 l -1.77636E-15,-4 a 2,1 90 0,1 1,-2 l 2,0 a 2,1 90 0,1 1,2 z'
Ellipses and Circles are different shapes but since a circle is a particular kind of Ellipse much of the functionality here is duplicated.
While the objects are different they can be checked for equivalency:
>>> Ellipse(center=(0,0), rx=10, ry=10) == Circle(center="0,0", r=10.0)
True
SimpleLine is renamed from the SVG form of Line
since we already have Line
objects as PathSegment
.
>>> s = SimpleLine(0,0,200,200)
>>> s
SimpleLine(x1=0.0, y1=0.0, x2=200.0, y2=200.0)
>>> s *= "rotate(45)"
>>> s
SimpleLine(x1=0.0, y1=0.0, x2=200.0, y2=200.0, transform=Matrix(0.707106781187, 0.707106781187, -0.707106781187, 0.707106781187, 0, 0))
>>> abs(s)
SimpleLine(x1=0.0, y1=0.0, x2=2.842170943040401e-14, y2=282.842712474619, stroke='None', fill='None')
>>> s.d()
'M 0,0 L 2.84217094304E-14,282.842712475
The difference here is polylines are not closed while Polygons are closed.
>>> p = Polygon(0,0, 100,0, 100,100, 0,100)
>>> p *= "scale(2)"
>>> p.d()
'M 0,0, L 200,0, L 200,200, L 0,200 Z'
and the same for Polyline:
>>> p = Polyline(0,0, 100,0, 100,100, 0,100)
>>> p *= "scale(2)"
>>> p.d()
'M 0,0, L 200,0, L 200,200, L 0,200'
You can just append a "z" to the polyline path though.
>>> Path(Polyline((20,0), (10,10), 0)) + "z" == Polygon("20,0 10,10 0,0")
True
The conversion of lengths to utilizes another element Length
It provides conversions for mm
, cm
, in
, px
, pt
, pc
, %
. You can also parse an element like the string '25mm' calling Length('25mm').value(ppi=96) and get the expected results. You can also call Length('25mm').in_inches()
which will return 25mm in inches. This can be independently useful when dealing with lengths, etc.
>>> Length('25mm').in_inches()
0.9842525
Color is another fundamental element within SVG that is also useful elsewhere. The object contains an 'int' as 'value' in RGBA order, storing alpha in the 8 least signficant bits. It parses all the SVG color functions.
If we get the .fill
or .stroke
of an object. This can be expressed in many ways, and needs to be converted to a consistent form. We could have a 3, 4, 6, or 8 digit hex. rgb(r,g,b) value, a static dictionary name or percent rgb(r,g,b). And must be properly parsed according to the spec.
>>> Color("red").hex
'#ff0000'
>>> Color('red').red
255
>>>Color('hsl(120, 100%, 50%)')
Color('#00ff00')
>>> c = Color('hsl(120, 100%, 50%)')
>>> c.blue = 50
>>> c
Color('#00ff32')
In addition you can set various properties of a particular color. Check distances to other colors.
>>> Color.distance('red', 'lightred')
25.179356624028344
>>> Color.distance('red', 'blue')
403.97524676643246
>>> Color('red').distance_to('blue')
403.97524676643246
Angle is backed by a 'float' and contains all the CSS angle values. 'deg', 'rad', 'grad', 'turn'.
>>> Angle.degrees(360).as_radians
Angle(6.283185307180)
The Angle element is used automatically with the Skew and Rotate for matrix.
>>> Point(100,100) * Matrix("SkewX(0.05turn)")
Point(132.491969623291,100)
Point is used in all the SVG path segment objects. With regard to svg.path
it is not back by, but implements all the same functionality as a complex
and will take a complex as an input. This is so that older svg.path
code will remain valid. While also allowing for additional functionality like finding a distance.
>>> Point(0+100j).distance_to([0,0])
100.0
The class supports complex
subscribable elements, .x
and .y
methods, and .imag
and .real
. As well as providing several of these indexing methods.
It includes a number of point functions like:
move_towards(point,float)
: Move this point towards the other point. with an amount [0,1]distance_to(point)
: Calculate the Euclidean distance to the other point.angle_to(point)
: Calculate the angle to the given point.polar_to(angle,distance)
: Return a point via polar coords at the angle and distance.reflected_across(point)
: Returns a point reflected across another point. (Smooth bezier curves use this).
This for example takes the 0,0 point turns 1/8th of a turn, and moves forward by 5cm.
>>> Point(0).polar_to(Angle.turns(0.125), Length("5cm").value(ppi=96))
Point(133.626550492764,133.626550492764)
The Path element of this project is based in part on the regebro/svg.path
( https://github.com/regebro/svg.path ) project. It is also may be based, in part, on some elements of mathandy/svgpathtools
( https://github.com/mathandy/svgpathtools ).