svgpathtools icon indicating copy to clipboard operation
svgpathtools copied to clipboard

Maintain ordering of the svg elements after parsing

Open ClementWalter opened this issue 3 years ago • 6 comments

This problem drove me crazy: the svg2paths function parses first the path and then add at the end depending on the options the other elements.

However, ordering of the elements in svg is not random but bears meaning.

For now I have rewritten the function like this:

"""
Rewrite the svgpathtools.svg2paths function to maintain the original ordering of the paths.
"""

from functools import wraps
from os import getcwd
from os import path as os_path
from xml.dom.minidom import parse

import svgpathtools
from svgpathtools.svg_to_paths import (
    ellipse2pathd,
    parse_path,
    polygon2pathd,
    polyline2pathd,
    rect2pathd,
)


@wraps(svgpathtools.svg2paths)
def svg2paths(
    svg_file_location,
    return_svg_attributes=False,
    convert_circles_to_paths=True,
    convert_ellipses_to_paths=True,
    convert_lines_to_paths=True,
    convert_polylines_to_paths=True,
    convert_polygons_to_paths=True,
    convert_rectangles_to_paths=True,
):
    if os_path.dirname(svg_file_location) == "":
        svg_file_location = os_path.join(getcwd(), svg_file_location)

    doc = parse(svg_file_location)

    def dom2dict(element):
        """Converts DOM elements to dictionaries of attributes."""
        keys = list(element.attributes.keys())
        values = [val.value for val in list(element.attributes.values())]
        return dict(list(zip(keys, values)))

    d_strings = []
    attribute_dictionary_list = []

    for node in doc.documentElement.childNodes:
        if not node.localName:
            continue
        node_dict = dom2dict(node)
        if node.localName == "path":
            d_strings += [node_dict["d"]]
            attribute_dictionary_list += [node_dict]
        elif node.localName == "circle":
            if convert_circles_to_paths:
                d_strings += [ellipse2pathd(node_dict)]
                attribute_dictionary_list += [node_dict]
        elif node.localName == "ellipse":
            if convert_ellipses_to_paths:
                d_strings += [ellipse2pathd(node_dict)]
                attribute_dictionary_list += [node_dict]
        elif node.localName == "line":
            if convert_lines_to_paths:
                d_strings += [
                    (
                        "M"
                        + node_dict["x1"]
                        + " "
                        + node_dict["y1"]
                        + "L"
                        + node_dict["x2"]
                        + " "
                        + node_dict["y2"]
                    )
                ]
                attribute_dictionary_list += [node_dict]
        elif node.localName == "polyline":
            if convert_polylines_to_paths:
                d_strings += [polyline2pathd(node_dict)]
                attribute_dictionary_list += [node_dict]
        elif node.localName == "polygon":
            if convert_polygons_to_paths:
                d_strings += [polygon2pathd(node_dict)]
                attribute_dictionary_list += [node_dict]
        elif node.localName == "rect":
            if convert_rectangles_to_paths:
                d_strings += [rect2pathd(node_dict)]
                attribute_dictionary_list += [node_dict]

    if return_svg_attributes:
        svg_attributes = dom2dict(doc.getElementsByTagName("svg")[0])
        doc.unlink()
        path_list = [parse_path(d) for d in d_strings]
        return path_list, attribute_dictionary_list, svg_attributes
    else:
        doc.unlink()
        path_list = [parse_path(d) for d in d_strings]
        return path_list, attribute_dictionary_list

If you want I can make a PR.

ClementWalter avatar Jan 14 '22 11:01 ClementWalter

Yeah, I'm surprised no one has commented on this before (as far as I recall). If you make a PR and include a unit test to check order is preserved, I'll merge it in.

What's motivates including an @wraps decorator?

mathandy avatar Jan 26 '22 02:01 mathandy

Are you interested in making a pull request for this @ClementWalter? Otherwise, I'll likely make one myself.

mathandy avatar Feb 04 '22 02:02 mathandy

@stjohn909 I actually did implement a solution for this. For now it is housed in the preserve-order branch.

I want to merge this branch in. First, svgpathtools needs of a test set of SVGs to test changes like this against.

mathandy avatar Jul 09 '22 21:07 mathandy

@mathandy Sorry I deleted my earlier comment, I'm kind of new at this and realized my ordering issue is with the path list returned in flattened_paths() in document.py.

Would an acceptable test SVG be something with a known layer order and examples of nested transforms?

stjohn909 avatar Jul 11 '22 11:07 stjohn909

@stjohn909 that's part of what I'm looking for.

If you take care of that part, I'll take care of a minimal implementation of the rest and we can get this merged in. The "rest" being to gather a set of SVG's that I can just check to make sure the same path data is being returned -- this is a bigger test issue related to more than just #165. If you have any examples you'd like me to include, please attach them here. In the near future I'll make a directory to store such pull requests and create an issue or request in the documentation for people to add any SVGs they want included by pull-request.

mathandy avatar Jul 11 '22 16:07 mathandy

I made my own version of the order-preserving svg2paths function, based on @mathandy's code. https://github.com/olivier-roche/svgpathtools/commit/09b1669fdcc3f632e04a985a329dd4157b8a6a0f

The changes I made are:

  • Add a line 'path': True to include_dict
  • Use doc.getElementsByTagName('*') instead of doc.documentElement.childNodes, to include not only direct children of the root XML element, but all the ancestors.

olivier-roche avatar Jun 04 '23 20:06 olivier-roche