linebender / resvg

An SVG rendering library.
Apache License 2.0
2.84k stars 229 forks source link

Incorrect background merging (antialias) #764

Closed astotrzynascie closed 6 months ago

astotrzynascie commented 6 months ago

Hello.

I'm using resvg as a C library. I need to generate textures (from svg files). Can't generate it directly in GPU (resvg doesn't support it). Providing background is also not an option, because background is dynamic, in GPU memory, and I would have to copy it back-and-forth each frame. So, I figured out that I need a texture that is generated on fully 'transparent' background and will fit to any other.

From what I've seen, in Qt example filling surface goes this way:

        qImg.fill(Qt::transparent);

As I'm not using Qt, checked that Qt::transparent is just black with alpha set to 0. So I started with filling the surface with zeroes:

        unsigned char *surface = new unsigned char[surface_bytes];
        memset(surface, 0, surface_bytes);

        resvg_render(tree, resvg_transform_identity(), width, height, (char*)surface);

But I noticed, that on white background antialiased edges looks like antialiased against black background.

white

My experiment with changing the surface made me sure that during some processing there is an ordinary average involved. For example, I changed the red component:

        for (size_t i = 0; i < width * height; ++i)
            {  surface[i * 4] = 255; surface[i * 4 + 1] = 0; surface[i * 4 + 2] = 0; surface[i * 4 + 3] = 0; }

...and the edges near background turned red.

red

So it looks like the fact that alpha is zero is ignored. When processing pixels all components seem to be taken under consideration, so when new value for red component is being calculated, alpha is ignored, ordinary average is being used and the edges gets redish.

Given the surface, it is impossible to declare it fully transparent. It leads to conclusion that rasterization this way will always process 'color of background' and ignore the fact that alpha is zero. And with all zeroes, edges get darker.

Workaround: Generate image 4x bigger, with

RESVG_IMAGE_RENDERING_OPTIMIZE_SPEED, RESVG_SHAPE_RENDERING_CRISP_EDGES, RESVG_SHAPE_RENDERING_CRISP_EDGES

(which gives no antialiased edges) and then manually shrink the image but using weighted sums, with alpha values as weights.

proper

Edges are not contaminated with any 'background color', they contain just original components with appropriate alpha, and the texture looks good blended with any background.

Anyway, I'd like to see some fix to resvg which will make such a workaround unnecessary. And, as I don't use Qt, I'm curious if 'Qt::transparent' also makes antialiased edges darker (instead of preserving the color). It should (or bytes in surface are somehow magically marked to be really transparent and algorithm handles it).

RazrFalcon commented 6 months ago

resvg produces an image with a premultiplied alpha. Do you account for that in your code?

PS: I'm not sure why it has anything to do with Qt::transparent. You don't need Qt to use this library.

astotrzynascie commented 6 months ago

Oh, I noticed:

 * @param pixmap Pixmap data. Should have width*height*4 size and contain
 *               premultiplied RGBA8888 pixels.

but somehow, I didn't think of the result being premultiplied...

Changed

    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

to

    glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA);

and it looks good (funny, I could swear I tested other blend modes). Sorry for bothering, thanks a lot!

RazrFalcon commented 6 months ago

No problems.