Basic Svg_Io_Sax (#66)

* Basic Svg_Io_Sax

* Update to binary write

* Fixed SaxSvg Load, tweaked test.

* Couple tweaks.

* Couple tweaks to fit the dom parser api better

* Switch to iterparse for speed

* Used None matrix for identity in default case.

* Test Update for None Matrix
pull/65/head^2
tatarize 2018-11-04 21:07:40 -08:00 committed by Andy Port
parent 2feb3c92b5
commit e91a35c3da
6 changed files with 281 additions and 1 deletions

View File

@ -13,6 +13,7 @@ from .polytools import polyroots, polyroots01, rational_limit, real, imag
from .misctools import hex2rgb, rgb2hex
from .smoothing import smoothed_path, smoothed_joint, is_differentiable, kinks
from .document import Document, CONVERSIONS, CONVERT_ONLY_PATHS, SVG_GROUP_TAG, SVG_NAMESPACE
from .svg_io_sax import SaxDocument
try:
from .svg_to_paths import svg2paths, svg2paths2

198
svgpathtools/svg_io_sax.py Normal file
View File

@ -0,0 +1,198 @@
"""(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
# 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 *
# To maintain forward/backward compatibility
try:
str = basestring
except NameError:
pass
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['points'])
elif 'polygon' == name:
values["d"] = polygon2pathd(values['points'])
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 += str(matrix[0][0])
matrix_string += " "
matrix_string += str(matrix[1][0])
matrix_string += " "
matrix_string += str(matrix[0][1])
matrix_string += " "
matrix_string += str(matrix[1][1])
matrix_string += " "
matrix_string += str(matrix[0][2])
matrix_string += " "
matrix_string += str(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)

1
test/display_temp.svg Normal file
View File

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

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -70,7 +70,8 @@
<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

30
test/test_sax_groups.py Normal file
View File

@ -0,0 +1,30 @@
from __future__ import division, absolute_import, print_function
import unittest
from svgpathtools import *
from os.path import join, dirname
import numpy as np
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')

49
test/transforms.svg Normal file
View File

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

After

Width:  |  Height:  |  Size: 1.5 KiB