meerk40t / svgelements

SVG Parsing for Elements, Paths, and other SVG Objects.
MIT License
135 stars 29 forks source link

Format `svgelements.SVG` as SVG text? #206

Closed amacapri closed 1 year ago

amacapri commented 1 year ago

What is the best way to format an svgelements.SVG instance as SVG text?

aminghz commented 1 year ago

I also have this question. The feature does not exist in the current repo. Correct?

tatarize commented 1 year ago

You might be able to run each element back through a library like svgwrite but generally the goal is very solid geometric parsing of the files and wasn't preservation or generation of svg files.

I do have a project on the backburner that is intended to allow reading, writing, and bootstrapping the DOM (and modifications) but it's sub-beta level still ( https://github.com/meerk40t/svgio ) largely based on #87 issue here and the notable amount of built in deficit that comes from parsing being treated as parsing the render-tree without actually parsing the DOM tree. svgelements are not 1:1 with nodes, therefore you'd need to write out all the properties with some considerable knowledge of svg to do this.

Outside of that you'd likely need to code one up. I suppose I could actually include a pretty basic one. Though it would likely lack a number of features and there's a number of lossy operations that would simply be lost. For example due to the method of parsing CSS is applied to the actual objects so they get the right colors, but you'd save out the actual CSS modified objects and get a somewhat simple output.

tatarize commented 1 year ago

Something like this would do it.

I'll see if I can get most of it working, since this stuff does come up here and there. I just need to be clear that it's lossy and not perfect. And then just write it fairly well.

https://github.com/meerk40t/svgelements/pull/209

Should be the PR. Maybe be sure to write full unprocessed SVGElement nodes and a few more bits of house-keeping. While not a breaking backwards compatibility it would need a major version so 1.9.x.

    def tostring(self, node=None):
        from xml.etree.ElementTree import tostring

        return tostring(
            self._write_node(node if node is not None else self), encoding="unicode"
        )

    def write(self, f, node=None, pretty=True):
        from xml.etree.ElementTree import ElementTree

        if node is None:
            node = self
        root = self._write_node(node)
        if pretty:
            self._pretty_print(root)
        tree = ElementTree(root)
        if f.lower().endswith("svgz"):
            import gzip

            f = gzip.open(f, "wb")
        tree.write(f)

    def _write_node(self, node, xml_tree=None, viewport_transform=None):
        if hasattr(node, "values"):
            values = node.values
            values = node.values.get(SVG_STRUCT_ATTRIB, values)
        else:
            values = None

        def subxml(xml_tree, tag):
            from xml.etree.ElementTree import Element, SubElement

            if xml_tree is None:
                xml_tree = Element(tag)
            else:
                xml_tree = SubElement(xml_tree, tag)
            for key, value in values.items():
                if key in (
                    "tag",
                    SVG_STRUCT_ATTRIB,
                    SVG_ATTR_TRANSFORM,
                    SVG_ATTR_FILL,
                    SVG_ATTR_STROKE,
                    SVG_TAG_STYLE,
                ):
                    continue
                xml_tree.set(key, str(value))
            return xml_tree

        if isinstance(node, SVG):
            if xml_tree is None:
                xml_tree = subxml(xml_tree, SVG_NAME_TAG)
                xml_tree.set(SVG_ATTR_VERSION, SVG_VALUE_VERSION)
                xml_tree.set(SVG_ATTR_XMLNS, SVG_VALUE_XMLNS)
                xml_tree.set(SVG_ATTR_XMLNS_LINK, SVG_VALUE_XLINK)
                xml_tree.set(SVG_ATTR_XMLNS_EV, SVG_VALUE_XMLNS_EV)
            else:
                xml_tree = subxml(xml_tree, SVG_NAME_TAG)
            if node.x:
                xml_tree.set(SVG_ATTR_X, str(node.x))
            if node.y:
                xml_tree.set(SVG_ATTR_Y, str(node.y))
            if node.width:
                xml_tree.set(SVG_ATTR_WIDTH, str(node.width))
            if node.height:
                xml_tree.set(SVG_ATTR_HEIGHT, str(node.height))
            if node.viewbox:
                xml_tree.set(SVG_ATTR_VIEWBOX, str(node.viewbox))
            vt = node.viewbox_transform
            if vt:
                m = Matrix(vt)
                m.inverse()
                vt = m
            else:
                vt = None
            for child in node:
                self._write_node(child, xml_tree, vt)
        elif isinstance(node, Ellipse):
            xml_tree = subxml(xml_tree, SVG_TAG_ELLIPSE)
            if node.cx:
                xml_tree.set(SVG_ATTR_CENTER_X, str(node.cx))
            if node.cy:
                xml_tree.set(SVG_ATTR_CENTER_Y, str(node.cy))
            if node.rx:
                xml_tree.set(SVG_ATTR_RADIUS_X, str(node.rx))
            if node.ry:
                xml_tree.set(SVG_ATTR_RADIUS_Y, str(node.ry))
        elif isinstance(node, Circle):
            xml_tree = subxml(xml_tree, SVG_TAG_ELLIPSE)
            if node.cx:
                xml_tree.set(SVG_ATTR_CENTER_X, str(node.cx))
            if node.cy:
                xml_tree.set(SVG_ATTR_CENTER_Y, str(node.cy))
            if node.rx:
                xml_tree.set(SVG_ATTR_RADIUS, str(node.rx))
        elif isinstance(node, Image):
            xml_tree = subxml(xml_tree, SVG_TAG_IMAGE)
            from io import BytesIO
            from base64 import b64encode

            stream = BytesIO()
            node.image.save(stream, format="PNG")
            xml_tree.set(
                "xlink:href",
                f"data:image/png;base64,{b64encode(stream.getvalue()).decode('utf8')}",
            )
            if node.x:
                xml_tree.set(SVG_ATTR_X, str(node.x))
            if node.y:
                xml_tree.set(SVG_ATTR_Y, str(node.y))
            if node.width:
                xml_tree.set(SVG_ATTR_WIDTH, str(node.width))
            if node.height:
                xml_tree.set(SVG_ATTR_HEIGHT, str(node.height))
        elif isinstance(node, SimpleLine):
            xml_tree = subxml(xml_tree, SVG_TAG_LINE)
            if node.x1:
                xml_tree.set(SVG_ATTR_X1, str(node.x1))
            if node.y1:
                xml_tree.set(SVG_ATTR_Y1, str(node.y1))
            if node.x2:
                xml_tree.set(SVG_ATTR_X2, str(node.x2))
            if node.y2:
                xml_tree.set(SVG_ATTR_Y2, str(node.y2))
        elif isinstance(node, Path):
            xml_tree = subxml(xml_tree, SVG_TAG_PATH)
            xml_tree.set(SVG_ATTR_DATA, node.d(transformed=False))
        elif isinstance(node, Polyline):
            xml_tree = subxml(xml_tree, SVG_TAG_POLYLINE)
            xml_tree.set(
                SVG_ATTR_POINTS,
                " ".join([f"{e[0]} {e[1]}" for e in node.points]),
            )
        elif isinstance(node, Polygon):
            xml_tree = subxml(xml_tree, SVG_TAG_POLYGON)
            xml_tree.set(
                SVG_ATTR_POINTS,
                " ".join([f"{e[0]} {e[1]}" for e in node.points]),
            )
        elif isinstance(node, Rect):
            xml_tree = subxml(xml_tree, SVG_TAG_RECT)
            if node.x:
                xml_tree.set(SVG_ATTR_X, str(node.x))
            if node.y:
                xml_tree.set(SVG_ATTR_Y, str(node.y))
            if node.rx:
                xml_tree.set(SVG_ATTR_RADIUS_X, str(node.rx))
            if node.ry:
                xml_tree.set(SVG_ATTR_RADIUS_Y, str(node.ry))
            if node.width:
                xml_tree.set(SVG_ATTR_WIDTH, str(node.width))
            if node.height:
                xml_tree.set(SVG_ATTR_HEIGHT, str(node.height))
        elif isinstance(node, Text):
            xml_tree = subxml(xml_tree, SVG_TAG_TEXT)
            xml_tree.text = node.text
            if node.font_family:
                xml_tree.set(SVG_ATTR_FONT_FAMILY, node.font_family)
            if node.font_style:
                xml_tree.set(SVG_ATTR_FONT_STYLE, node.font_style)
            if node.font_variant:
                xml_tree.set(SVG_ATTR_FONT_VARIANT, node.font_variant)
            if node.font_stretch:
                xml_tree.set(SVG_ATTR_FONT_STRETCH, node.font_stretch)
            if node.font_size:
                xml_tree.set(SVG_ATTR_FONT_SIZE, str(node.font_size))
            if node.line_height:
                xml_tree.set("line_height", str(node.line_height))
            if node.anchor:
                xml_tree.set(SVG_ATTR_TEXT_ANCHOR, node.anchor)
        elif isinstance(node, Group):
            # This is a structural group node of elements. Recurse call to write values.
            xml_tree = subxml(xml_tree, SVG_TAG_GROUP)
            for child in node:
                self._write_node(child, xml_tree, viewport_transform)

        # Write Transform
        if hasattr(node, "transform") and not isinstance(node, Group):
            t = node.transform
            if viewport_transform:
                t = t * viewport_transform
            if not t.is_identity():
                xml_tree.set(
                    SVG_ATTR_TRANSFORM,
                    "matrix(%f, %f, %f, %f, %f, %f)" % (t.a, t.b, t.c, t.d, t.e, t.f),
                )

        # Write Stroke
        if hasattr(node, "stroke"):
            stroke = node.stroke
            stroke_opacity = stroke.opacity
            stroke = (
                str(abs(stroke))
                if stroke is not None and stroke.value is not None
                else SVG_VALUE_NONE
            )
            xml_tree.set(SVG_ATTR_STROKE, stroke)
            if stroke_opacity != 1.0 and stroke_opacity is not None:
                xml_tree.set(SVG_ATTR_STROKE_OPACITY, str(stroke_opacity))

            try:
                stroke_width = str(node.stroke_width)
                xml_tree.set(SVG_ATTR_STROKE_WIDTH, stroke_width)
            except AttributeError:
                pass

        # Write Fill
        if hasattr(node, "fill"):
            fill = node.fill
            fill_opacity = fill.opacity
            fill = (
                str(abs(fill))
                if fill is not None and fill.value is not None
                else SVG_VALUE_NONE
            )
            xml_tree.set(SVG_ATTR_FILL, fill)
            if fill_opacity != 1.0 and fill_opacity is not None:
                xml_tree.set(SVG_ATTR_FILL_OPACITY, str(fill_opacity))

        # Write id
        if hasattr(node, "id"):
            if node.id is not None:
                xml_tree.set(SVG_ATTR_ID, str(node.id))

        return xml_tree

    def _pretty_print(self, current, parent=None, index=-1, depth=0):
        for i, node in enumerate(current):
            self._pretty_print(node, current, i, depth + 1)
        if parent is not None:
            if index == 0:
                parent.text = "\n" + ("\t" * depth)
            else:
                parent[index - 1].tail = "\n" + ("\t" * depth)
            if index == len(parent) - 1:
                current.tail = "\n" + ("\t" * (depth - 1))
tatarize commented 1 year ago

The PR should work, though it'll likely require a number of extra tests. There might be edge conditions too though I'll likely just read and write a number of svgs and see if they are visually the same and approve them if that generally holds to be the case.

tatarize commented 1 year ago

Okay. Rather than putting out these little fires where I say it doesn't save but isn't too hard to write a save routine, I have a basic save routine in 1.9.0. SVG.write_xml(filename) will save and since it comes up svg.string_xml() will also convert to an xml string. If the filename is svgz it'll go ahead and compress that correctly for the file format.

I expect it to mostly work. The'll be some bugs but they should be less than if you folks tried to write the writer yourself.

tatarize commented 1 year ago

This was added in 1.9.x