Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

2 fixes for usvg Tree writer #672

Merged
merged 3 commits into from
Oct 31, 2023
Merged

Conversation

LaurenzV
Copy link
Contributor

Firstly, I fixed an issue where a call to .unwrap would cause a panic with the following svg file (from the test suite):

 <svg id="svg1" viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg"
     xmlns:xlink="http://www.w3.org/1999/xlink">
    <title>With invalid child via `use`</title>

    <defs id="defs1">
        <g id="g1">
            <path id="path1" d="M 100 15 l 50 160 l -130 -100 l 160 0 l -130 100 z"/>
        </g>
        <clipPath id="clip1">
            <use id="use1" xlink:href="#g1"/>
        </clipPath>
    </defs>
    <rect id="rect1" x="0" y="0" width="200" height="200" fill="red" clip-path="url(#clip1)"/>

    <!-- image frame -->
    <rect id="frame" x="1" y="1" width="198" height="198" fill="none" stroke="black"/>
</svg>

I guess it's not really relevant since it's invalid, but I would say it's still better to just ignore it than let it crash in that case.

Secondly, the writer failed to write the xmlns:xlink attribute for images inside of masks because they weren't checked properly.

Comment on lines +949 to +960

if let Some(ref mask) = g.mask {
if has_xlink(&mask.root) {
return true;
}

if let Some(ref sub_mask) = mask.mask {
if has_xlink(&sub_mask.root) {
return true;
}
}
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess it would be nice to reuse the node.subroots method here, but it looks like this one just allows me to pass a function to make changes to the node itself instead of giving me a way of iterating over the nodes itself and return a boolean from that.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah... I haven't figured out a better API.

@LaurenzV
Copy link
Contributor Author

LaurenzV commented Oct 28, 2023

I realized another issue, the following SVG will also not be rendered properly:

<svg id="svg1" viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg"
     font-family="Noto Sans" font-size="80">
    <title>Linear gradient on text</title>

    <linearGradient id="lg1">
        <stop id="stop1" offset="0" stop-color="white"/>
        <stop id="stop2" offset="1" stop-color="black"/>
    </linearGradient>
    <text id="text1" x="100" y="120" text-anchor="middle" fill="url(#lg1)">Text</text>

    <!-- image frame -->
    <rect id="frame" x="1" y="1" width="198" height="198" fill="none" stroke="black"/>
</svg>

The reason being that since the paint is on a text element, the fill/stroke will be cloned as part of the paint_server_to_user_space_on_use but the ID will be left empty. So when writing it to an SVG file again, the gradient ID will be empty as well, and we can't reference it. I think one approach to fixing this would be that when writing the usvg tree, we simply iterate over all nodes and generate a new unique id for all elements where this is missing. I guess we could also override the IDs for existing ones to make sure they really are unique and to also make them more consistent (e.g. paths would get the IDs "path1", "path2", "path3", patterns would get the IDs "patt1", "patt2", "patt3", etc.). Would this be an acceptable change to you or is there a good reason to preserve the IDs of the original SVG file?

@LaurenzV
Copy link
Contributor Author

LaurenzV commented Oct 28, 2023

So, turns out that there was another bug, because the assumption that a group element in a clip path only contains one element seems to be wrong if the clip path contains more than 2 (converted) text elements. This is why I just went for a for loop now instead of checking for the first child only and then unwrapping.

@RazrFalcon
Copy link
Collaborator

I guess it's not really relevant since it's invalid, but I would say it's still better to just ignore it than let it crash in that case.

No, it is... Just to clarify, usvg CLI tool is used mainly for debugging. It's alpha quality at best. You should not use it. I simply have no time to work on it. Only resvg is considered stable and well tested.

because the assumption that a group element in a clip path only contains one element seems to be wrong if the clip path contains more than 2 (converted) text elements

How is this possible? A clip path with a group is always manually generated and therefore can only have 1 element.

@LaurenzV
Copy link
Contributor Author

LaurenzV commented Oct 30, 2023

No, it is... Just to clarify, usvg CLI tool is used mainly for debugging. It's alpha quality at best. You should not use it. I simply have no time to work on it. Only resvg is considered stable and well tested.

Well, it doesn't seem too bad from what I've seen. And I would need this for my use case, so if you're okay with accepting PRs I'd like to work on improving it!

How is this possible? A clip path with a group is always manually generated and therefore can only have 1 element.

I'll look into it.

@RazrFalcon
Copy link
Collaborator

And I would need this for my use case, so if you're okay with accepting PRs I'd like to work on improving it!

Sure. I just wanted to clarify that only usvg the library is heavily tested. usvg CLI is not.

@LaurenzV
Copy link
Contributor Author

How is this possible? A clip path with a group is always manually generated and therefore can only have 1 element.

So, if I understood it right, in the normal case a clip path should have a root element (which is a group) and then a number of children consisting of paths/texts (except for this one edge case mentioned in the comments).

https://github.com/RazrFalcon/resvg/blob/7cb1ec4d2801a2d86791c3afb91d933939df83fa/crates/usvg/src/writer.rs#L594-L596

However, when converting the texts to paths, we create a new group node which contains all of the text nodes, and thus if the text elements were part of a clip path, they are not the direct children of the clip path root anymore. Instead, the clip path root will have the group as a child that contains all text nodes.

https://github.com/RazrFalcon/resvg/blob/7cb1ec4d2801a2d86791c3afb91d933939df83fa/crates/usvg-text-layout/src/lib.rs#L62-L66

What's the best course of action here? Should I leave the for loop or should we change how text nodes are appended to the parent? I guess the second one would be better so that we can keep the edge case of a clip path root having another group as a child really be an edge case?

@LaurenzV
Copy link
Contributor Author

I changed the code with a possible fix. Let me know what you think.

Regarding the gradient problem, I will probably leave that one for a different PR since it requires a bit more thinking from my side.

@RazrFalcon
Copy link
Collaborator

Hm... I genuinely do not remember why we have a group in a clip path. For one, clipPath doesn't allow groups to begin with. So the comment is correct, it's a usvg hack. And it affects rendering as well.

I think we should update usvg writer and not usvg-text-layout here. Ideally, we should flatten text in clip paths, aka remove groups and styles, but it would be a more complicated task. And even better, ClipPath should have a list of paths, instead of a having a tree of nodes. It would make it a bit special, compared to other containers, but it logically more correct.
And we should definitely start adding tests to usvg...

@LaurenzV
Copy link
Contributor Author

Okay, I'll see what I can do.

@RazrFalcon
Copy link
Collaborator

Updating usvg writer would be enough. I will look into other stuff later.

@LaurenzV
Copy link
Contributor Author

LaurenzV commented Oct 31, 2023

Updating usvg writer would be enough. I will look into other stuff later.

So basically just using a for loop, like I did originally?

@LaurenzV
Copy link
Contributor Author

And even better, ClipPath should have a list of paths, instead of a having a tree of nodes.

Actually, isn't the problem here that text/path elements can have other clip paths as well? In this case, we need to wrap the text/path inside of another group, because only groups can have other clip paths as well.

@LaurenzV
Copy link
Contributor Author

I reverted the change to usvg-text-layout.

@RazrFalcon
Copy link
Collaborator

So basically just using a for loop, like I did originally?

Yep.

@LaurenzV
Copy link
Contributor Author

LaurenzV commented Oct 31, 2023

Should be done, I also added a comment so that we don't forget about it. So from my side this is ready. 😄

@RazrFalcon RazrFalcon merged commit 2f599e9 into linebender:master Oct 31, 2023
3 checks passed
@RazrFalcon
Copy link
Collaborator

Thanks!

@LaurenzV LaurenzV deleted the panic-fix branch October 31, 2023 17:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants