mathandy / svgpathtools

A collection of tools for manipulating and analyzing SVG Path objects and Bezier curves.
MIT License
557 stars 142 forks source link

Maintain ordering of the svg elements after parsing #165

Open ClementWalter opened 2 years ago

ClementWalter commented 2 years ago

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.

mathandy commented 2 years ago

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 commented 2 years ago

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

mathandy commented 2 years ago

@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.

stjohn909 commented 2 years ago

@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 _flattenedpaths() in document.py.

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

mathandy commented 2 years ago

@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.

olivier-roche commented 1 year ago

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: