Closed amacapri closed 1 year ago
I also have this question. The feature does not exist in the current repo. Correct?
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.
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))
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.
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.
This was added in 1.9.x
What is the best way to format an
svgelements.SVG
instance as SVG text?