cduck/drawsvg

"paint-first" not working

Closed this issue · 3 comments

I don't know if it is a bug or a feature request:

I have tried "paint-order" and "paint_order" for arguments I want to use for... paint... order... and well... it doesn't appear to work during the rasterization process. See the below animations. I increase the stroke width from each image (by what's supposed to be 1/10 of a px).

gif_out_0
gif_out_1

versus what should be the corresponding SVGs below

pc5dsvg_0_0
pc5dsvg_0_19
pc5dsvg_0_39
pc5dsvg_1_0
pc5dsvg_1_19
pc5dsvg_1_39

So far I really like this library. Is there any potential support for this (if so, any idea of the time frame), or is it a bug? Any other potential ideas for how I could accomplish the same effect as a stroke width, paint-fill-first paths without "paint-first" if it's not going to be supported?

EDIT - I'll add some more code to help make it reproducible:

import xml.etree.ElementTree as et
from pathlib import Path
import re
from svgpathtools import parse_path

class SVG:
    def __init__(self, filename, used_tags=None):
        self.svg = Path(filename)
        self._used_tags = used_tags
        if not self.svg.exists():
            raise FileNotFound("SVG file not found.")
        self.elements_root = self.populate()
        self.paths = self._collect_map_nodes_with_paths()

    @staticmethod
    def get_tag_namespace(element):
        m = re.match(r'\{.*\}', element.tag)
        return m.group(0) if m else ''

    @staticmethod
    def get_tag_name(element):
        return SVG._remove_namespace(element)

    @staticmethod
    def _remove_namespace(element):
        return element.tag.removeprefix(SVG.get_tag_namespace(element))

    def _element_generator(self):
        it = et.iterparse(self.svg, events=('start', 'end'))
        for evt, el in it:
            # if evt == 'start':
            check_list = []
            if self._used_tags is not None:
                check_list = [1 for e in self._used_tags if e in SVG.get_tag_name(el)]
            if self._used_tags is None or len(check_list) > 0:
                yield el

    def populate(self):
        _map = None
        stack = []

        for element in self._element_generator():
            styles = element.attrib.get("style", None)
            if styles:
                styles = styles.strip().split(";")
                styles = styles[:-1] if len(styles) % 2 == 1 else styles
                for style in styles:
                    k, v = style.strip().split(":")
                    element.attrib[k.strip()] = v.strip()
                del element.attrib["style"]

            if _map is None:
                _map = Node(self, element)
                stack.append(_map)
                continue

            if len(stack) > 0:
                # check if the elements are closing
                # <path> </path> for instance
                if element == stack[-1].element_tree_node:
                    child = stack.pop()
                    if child is not _map:
                        stack[-1].children.append(child)
                # else just add the element to the map
                else:
                    node = Node(self, element, parent=stack[-1])
                    stack.append(node)
        
        return _map

    def _collect_map_nodes_with_paths(self, node=None):
        node = node if node is not None else self.elements_root
        paths = []
        stack = [node]

        while len(stack) > 0:
            node = stack.pop()
            stack += node.children
            if node.path:
                paths.append(node)

        return paths


class Node:
    def __init__(self, _map, element_tree_node, parent=None, children=None):
        self.root = _map
        self.element_tree_node = element_tree_node
        self.parent = parent
        self.children = list() if children is None else children
        self.lower_case_attributes()

        d = element_tree_node.attrib.get("d", None)
        if d:
            self.path = parse_path(d)
            self.bbox = BoundingBox(self.path)

    def lower_case_attributes(self):
        _attributes = self.element_tree_node.attrib
        ignore_attributes = [
            "d"
        ]
        self.element_tree_node.attrib = {k.lower(): v.lower() if k.lower() not in ignore_attributes else v for k, v in _attributes.items()}

    def rasterization_styles(self) -> dict:
        ignore_attributes = [
            "d"
        ]

        return {k: v for k, v in self.element_tree_node.attrib.items() if k not in ignore_attributes}


class BoundingBox:
    def __init__(self, path):
        self.path = path
        xmin, xmax, ymin, ymax = path.bbox()
        self.xmax = xmax
        self.ymax = ymax
        self.xmin = xmin
        self.ymin = ymin
        self.width = self.xmax - self.xmin
        self.height = self.ymax - self.ymin
        self.center_point = (self.xmin + (self.width / 2), self.ymin + (self.height / 2))



if __name__ == "__main__":
    import drawsvg as draw
    import imageio.v2 as imageio

    tags = ["path"]
    svg = SVG("pc5dsvg_0_0.svg", used_tags=tags)
    paths = svg.paths

    for i, path in enumerate(paths):
        attributes = path.rasterization_styles()
        attributes['fill'] = f"red"
        attributes['stroke'] = f"green"
        attributes['paint-order'] = f"stroke"
        attributes['paint_order'] = f"stroke"
        images = []
        print(attributes)
        for j in range(40):
            attributes['stroke-width'] = f"{j / 10}"
            d = draw.Drawing(f"{path.bbox.width + (j / 10)}", f"{path.bbox.height + (j / 10)}")
            d.set_pixel_scale(1)
            d.view_box = (path.bbox.xmin - (j / 10 / 2), path.bbox.ymin - (j / 10 / 2), path.bbox.width + (j / 10), path.bbox.height + (j / 10))

            p = draw.Path(**attributes)
            p.args["d"] = path.element_tree_node.attrib.get("d")
            d.append(p)

            d.save_svg(f"{svg.svg.stem}_{i}_{j}.svg")
            d.save_png(f"{svg.svg.stem}_{i}_{j}.png")
            images.append(imageio.imread(f"{svg.svg.stem}_{i}_{j}.png"))
        imageio.mimsave(f"{svg.svg.stem}_{i}.gif", images)

Everything below "name" is the code used to generate the gifs and related svgs

Thank you!

Hi! Thanks for reporting this. It looks like a limitation of the rendering backend. paint-order is an SVG 2 feature but the current rendering backend is CairoSVG which only supports SVG 1.1.

I don't have the time right now to add any better backend to drawsvg but try out resvg or Inkscape (its command-line tool) and please share code snippets (or even a PR) if either works for you.

To keep things organized, I'm going to close this issue as a duplicate of #102.


By the way, drawsvg supports key-frame animations and exporting them to GIF if you want to simplify your code.

Hi! Thanks for reporting this. It looks like a limitation of the rendering backend. paint-order is an SVG 2 feature but the current rendering backend is CairoSVG which only supports SVG 1.1.

I don't have the time right now to add any better backend to drawsvg but try out resvg or Inkscape (its command-line tool) and please share code snippets (or even a PR) if either works for you.

To keep things organized, I'm going to close this issue as a duplicate of #102.

By the way, drawsvg supports key-frame animations and exporting them to GIF if you want to simplify your code.

RIP! Thank you. So sad that this is a limitation; hopefully one day SVG 2 will be supported by CairoSVG. I was using Cairo, which is part of the reason why I changed to this, hoping for a "better" rasterizer (unknown to me at the time that Cairo was the driver and that the limitation was with SVG 1.1 and not that it was just garbage rasterizer). I'll try to keep you updated if I find a solution myself, but I am now looking for some SVG 2.0 rasterizer, soooo yah. Anyways, thanks again, and kudos to your incredible module!

@cduck I was able to get the proper output from resvg. I have other things grabbing my attention at this time, so I won't be implementing it into my project, yet, but when I do, I will try to throw in a patch for drawsvg. It wasn't anything super difficult; I just installed rust and installed resvg by downloading it from github, unzipping it, running cargo install on it, which then put the resvg cli on my path. Then I ran it...