From e91a35c3da8c5185f08943f909c0ba35817e957c Mon Sep 17 00:00:00 2001 From: tatarize Date: Sun, 4 Nov 2018 21:07:40 -0800 Subject: [PATCH] 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 --- svgpathtools/__init__.py | 1 + svgpathtools/svg_io_sax.py | 198 +++++++++++++++++++++++++++++++++++++ test/display_temp.svg | 1 + test/groups.svg | 3 +- test/test_sax_groups.py | 30 ++++++ test/transforms.svg | 49 +++++++++ 6 files changed, 281 insertions(+), 1 deletion(-) create mode 100644 svgpathtools/svg_io_sax.py create mode 100644 test/display_temp.svg create mode 100644 test/test_sax_groups.py create mode 100644 test/transforms.svg diff --git a/svgpathtools/__init__.py b/svgpathtools/__init__.py index 109374a..bb01801 100644 --- a/svgpathtools/__init__.py +++ b/svgpathtools/__init__.py @@ -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 diff --git a/svgpathtools/svg_io_sax.py b/svgpathtools/svg_io_sax.py new file mode 100644 index 0000000..1c46f51 --- /dev/null +++ b/svgpathtools/svg_io_sax.py @@ -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) diff --git a/test/display_temp.svg b/test/display_temp.svg new file mode 100644 index 0000000..a03b577 --- /dev/null +++ b/test/display_temp.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/test/groups.svg b/test/groups.svg index 1787617..119ff71 100644 --- a/test/groups.svg +++ b/test/groups.svg @@ -70,7 +70,8 @@ + transform=" + translate(20, 30)"> + + + + + + + + + + + + + + + + + + + + + + A + B + C + + + + + + + \ No newline at end of file