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

Anti-aliased edges of patterns with non-integer width/height #539

Closed
or opened this issue Sep 13, 2022 · 11 comments
Closed

Anti-aliased edges of patterns with non-integer width/height #539

or opened this issue Sep 13, 2022 · 11 comments
Labels

Comments

@or
Copy link

or commented Sep 13, 2022

Hallo, I've noticed that patterns result in some anti-aliasing issues at the edges, if they have non-integer dimensions and the shapes inside the pattern go right against the edge.

To illustrate the case I made a large white rectangle covered by a rectangle on top, which has a pattern of 20.13x20.13 containing just a black rectangle filling the pattern exactly. In the PNG result you can see the white shining through, but the whole area should be black.
(Note: the viewBox is only 0 0 200 200, but the image has to be larger to see the effect, so its width and height are set to 1000)

Any of these workarounds make it render correctly:

  • the pattern has a width of 20x20
  • the rectangle inside the pattern extends past the pattern dimensions, e.g. from -1,-1 to 21,21
  • the rectangle inside the pattern uses shape-rendering "crispEdges"

but I believe the result should be the same without any of them.

Perhaps the pattern should be copied on all four sides while rendering its content?

pattern-anti-aliasing

pattern-anti-aliasing

@RazrFalcon
Copy link
Owner

Yes, this is sort of expected. resvg renders a pattern to an image first and then uses it as a fill source. And an image has an integer size.

Not sure how it can be fixed. Manually tiling patterns would be a performance nightmare.
Based on a quick experiment, the "correct" output can be produced by rounding pattern's size down instead of up. This way a tile will become 100x100px and not 101x101px as it is now. Feels like a hack and a breaking change, but this is the only idea I have.

Also note that the SVG spec doesn't specify this behaviour (afaik). Meaning every app/library can render patters in whatever way they want. Writing a reproducible SVG is impossible.

@or
Copy link
Author

or commented Sep 13, 2022

Right, I meant putting 4 copies of the pattern around it when rendering to the image. Still a bit of a performance hit, but probably not huge?

If it's only the right and bottom pixel from ceiling the width and height, then perhaps it's even enough to do it with a copy to the right and bottom?

@RazrFalcon
Copy link
Owner

I'm not sure what exactly are you trying to do. The reason you see gaps is because the tile is 101x101, but the fill area is 100.65x100.65 So the right and bottom edges are semitransparent.

@or
Copy link
Author

or commented Sep 13, 2022

I think just considering the vector space the pattern of 20.13 x 20.13 should tile correctly and the result ought to be seamless.

I understand that there are difficulties to do this correctly in pixel space. Manually rendering each instance of the pattern is far too slow, of course. There might be a way to generate relevant versions of the 101 x 101 image based on the shifts that can accumulate (e.g. if the width is x.5 there'd be one with offset 0.0, one with 0.5, they'd alternate, for x.333 there'd be three, etc.), but that also would be complicated and probably not worth it.

But I think the result can still be improved with a single 101 x 101 image if it is rendered as if the pattern repeated to the right of it and below it, and while drawing the following example I realized it'd also have to be repeated to the bottom-right to get that last pixel right:

pattern-example

The blue square is the rectangle of pixels after ceiling the width and height to the nearest integer. In the first case there's the 0.35 pixel of transparency that the edge of the actual pattern is anti-aliased with, if I understanding you correctly.
In the lower case the pattern is repeated around the first one only for the purpose of rendering to the blue image, but this time the 0.35 pixel is filled with the actual continuation of the pattern.

My example is just a minimal case, but of course there might be more complex continuations and other colours that the right edge of the pattern then gets correctly anti-aliased against.

@or
Copy link
Author

or commented Sep 13, 2022

Chrome and Inkscape render this seamlessly, also with more complex patterns, which is how I noticed it.
I don't know their implementations, but I'd guess they do something like that if they also repeat a pre-rendered image with integer width and height.

@RazrFalcon
Copy link
Owner

Well, I don't have such low-level control over patterns. I simply delegate it to tiny-skia and the output is exactly the one I would expect.

I just checked what librsvg does, and it simply rounds down. No magic involved.

@or
Copy link
Author

or commented Sep 13, 2022

Interesting, that definitely is simpler and faster, and probably good enough.
But it seems like rounding down or up would be more accurate, with the continuation in the case of rounding up. I'll try to grok the code a bit more and give that a whirl sometime, maybe I can make a case for it. :)

In the meantime: would you consider flooring the pixmap dimensions, so the anti-aliasing issue goes away?

@RazrFalcon
Copy link
Owner

Yes, in the next release whenever it will be.

I just want to clarify once again that getting a reproducible output from SVG is a fruitless endeavor. There are no such thing as "a correctly rendered" SVG. The only "solution" is to keep your SVG as simple as possible.

@or
Copy link
Author

or commented Sep 14, 2022

Awesome, thanks. And thank you for your work in general, I recently replaced a rather slow and hefty workflow using a headless chromium process to convert SVGs with resvg, and the results are lovely. This pattern issue is the only problem I have run into thus far.

I get that there won't be an absolute truth in reducing vector graphics to a pixel representation, but I still think the goal can be to be as faithful as possible, and I like that resvg has a focus on correctness and edge cases (and I think these aren't even edge cases).

So I tried to dig a bit deeper and compare it to inkscape and librsvg... and don't get me wrong, I'm not at all suggesting that resvg has to follow suit no matter what, but it's still worth checking out. Apologies in advance for the long post!

I made this test SVG:

<svg id="svg1" viewBox="0 0 570 570" xmlns="http://www.w3.org/2000/svg">
    <title>Pattern with non-integer width and height</title>

    <rect x="0" y="0" width="570" height="570" fill="#ffffff"/>

    <pattern id="patt1" width="30.00" height="30.00" patternUnits="userSpaceOnUse">
        <rect x="0" y="0" width="30.00" height="30.00" fill="#000000"/>
        <path d="M0,0L30.00,30.00" stroke="red" stroke-width="1"/>
        <path d="M30.00,0L0,30.00" stroke="red" stroke-width="1"/>
    </pattern>

    <!-- aligned with ideal grid point, ideal dimensions -->
    <rect id="rect3" x="30.00" y="30.00" width="150.00" height="150.00" fill="url(#patt1)"/>

    <pattern id="patt2" width="30.25" height="30.25" patternUnits="userSpaceOnUse">
        <rect x="0" y="0" width="30.25" height="30.25" fill="#000000"/>
        <path d="M0,0L30.25,30.25" stroke="red" stroke-width="1"/>
        <path d="M30.25,0L0,30.25" stroke="red" stroke-width="1"/>
    </pattern>

    <!-- aligned with ideal grid point, ideal dimensions -->
    <rect x="211.75" y="30.25"  width="151.25" height="151.25" fill="url(#patt2)"/>
    <!-- aligned with floored grid point, ideal dimensions -->
    <rect x="210.00" y="210.00" width="151.25" height="151.25" fill="url(#patt2)"/>
    <!-- aligned with ceiled grid point, rounded dimensions -->
    <rect x="217.00" y="403.00" width="151.25" height="151.25" fill="url(#patt2)"/>

    <pattern id="patt3" width="30.75" height="30.75" patternUnits="userSpaceOnUse">
        <rect x="0" y="0" width="30.75" height="30.75" fill="#000000"/>
        <path d="M0,0L30.75,30.75" stroke="red" stroke-width="1"/>
        <path d="M30.75,0L0,30.75" stroke="red" stroke-width="1"/>
    </pattern>

    <!-- aligned with ideal grid point, ideal dimensions -->
    <rect x="399.75" y="30.75"  width="153.75" height="153.75" fill="url(#patt3)"/>
    <!-- aligned with floored grid point, ideal dimensions -->
    <rect x="390.00" y="210.00" width="153.75" height="153.75" fill="url(#patt3)"/>
    <!-- aligned with ceiled grid point, ideal dimensions -->
    <rect x="403.00" y="403.00" width="153.75" height="153.75" fill="url(#patt3)"/>
</svg>

So there are three pattern dimensions, each with a red cross from the corners:

  • 30 x 30: that one is just an ideal integer case that should align obviously with an integer 30x30 grid.
  • 30.25 x 30.25: will run into sub pixel issues and can't easily align to an integer grid
  • 30.75 x 30.75: will run into sub pixel issues, would have to be rounded up, and can't easily align to an integer grid

The patterns are used in a rect of the ideal position and dimensions to show each pattern 5 times in width and height, so ideally we'd expect the crosses to align with the big rect as well.
However, for each of the non-integer pattern dimensions I also aligned the rects with the floored and ceiled grid points, e.g. with the 30x30 grid and the 31x31 grid, because they are the ones resvg appears to be using currently when it has to round the dimensions down or up respectively.

This is Inkscape's result, and only the first row is relevant, because it seems to align the pattern very well to the ideal rect position and dimensions (tho there seems to be a glitch in the top and bottom edges of the big rectangle):

test-case-inkscape

Also note that the pattern is not just repeating, however it is done, it shifts slightly to accomodate the sub-pixel issues and that makes sure the pattern ends up to fill the space as one'd hope:

inkscape-zoom-in

Similarly the result of librsvg, only the first row is relevant, it also aligns the rect to the ideal position and fills it properly, adjusting the pattern:

test-case-librsvg

Zooming in shows that the pattern here also shift slightly:

librsvg-zoom-in

resvg's output currently looks like this:

test-case-resvg

The ideal positions in the first row no longer align with the grid. For the first pattern the second rect in the column now aligns with the 30x30 grid, because the dimensions are rounded down, so that makes sense. But the pattern doesn't align with the dimensions of the rect either, because it's not integer. You can see that at the edges and the bottom right corner (the anti-aliasing with the white hides it a little, but it's still visible).
Similarly the 3rd rect in the third column now aligns with the 31x31 grid, but there the patterns are too big to fill the ideal dimensions, so you can see it is missing bits at the edges.

Finally, I compiled a resvg version that always floors the pixmap dimensions, which addresses the anti-aliasing issue we talked about, doing this in paint_server.rs:

    let img_size = usvg::Size::new((r.width() * sx).floor(), (r.height() * sy).floor())?.to_screen_size();

test-case-resvg-adjusted

The anti-aliasing is gone, and now both of the second rects in the second and third column align with the 30x30 grid, but the mismatch of the 30.75 x 30.75 pattern is even more pronounced.

So the flooring definitely is an improvement, but for many cases the result is not expected, namely large patterned areas where the shift becomes very noticeable or areas that align with the ideal pattern space but are positioned farther away from (0, 0).

I have no idea how inkscape and librsvg do it, but I think it's worth considering it. You'll have much more experience with SVG processing and the code, but I'll look into it a bit more, maybe I can figure out some details.

@RazrFalcon
Copy link
Owner

Thanks for the investigation and a test case. I do think that this is a bug, but for now I have no idea how to fix it.

@RazrFalcon
Copy link
Owner

Duplicate of #628

@RazrFalcon RazrFalcon marked this as a duplicate of #628 May 21, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

2 participants