nothings / stb

stb single-file public domain libraries for C/C++

Home Page:https://twitter.com/nothings

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

SDF incorrect with intersecting glyph segments

jamesthomasgriffin opened this issue · comments

This bug occurs when a glyph is defined by two or more intersecting simple closed curves. Take for example the letter f from a Noto font, (eg "NotoSansJP-Regular.ttf"), then the glyph is defined by a vertical piece and a horizontal piece. The signed distance field in the interior of the glyph near the intersection is incorrect, see attached images. This leads to artefacts around the intersection, they are just visible in the image, however can be more pronounced for smaller text and would be very pronounced for effects using sdf's, eg rendering edges.

The code used to reproduce is something like this (font loading and globals taken from sdf_test.c)

    char ch = 'f';
    float sdf_size = 256.0;
    float scale = stbtt_ScaleForPixelHeight(&font, sdf_size);

    int xoff, yoff, w, h, advance;
    unsigned char* sdf_data = stbtt_GetCodepointSDF(&font, scale, ch, padding, onedge_value, pixel_dist_scale, &w, &h, &xoff, &yoff);

    stbi_write_png("character_sdf.png", w, h, 1, sdf_data, 0);

    unsigned char* d = malloc(w * h);
    int x, y;
    for (x = 0; x < w; x++) {
        for (y = 0; y < h; y++) {
            int ix = x + w * y;
            float sdf_dist = stb_linear_remap(sdf_data[ix], onedge_value, onedge_value + pixel_dist_scale, 0, 1);
            float alpha = stb_linear_remap(sdf_dist, -0.5, 0.5, 0, 1);
            if (alpha > 1) alpha = 1;
            if (alpha < 0) alpha = 0;
            d[ix] = (unsigned char)(255 * (1 - alpha));                
        }
    }

    stbi_write_png("character_render.png", w, h, 1, d, 0);

The sdf in the interior of the 'f' should be the minimal distance to the outline, but the algorithm is including the distance to lines that have crossed to the interior.

character_sdf character_render character_render_edges

I believe that the only way to fix this properly is to recompute the outline of the character. However I propose a partial fix:

Edit my apologies, I was mistaken, disregard the below.
When computing min_dist in stbtt_GetGlyphSDF, instead compute the minimal distance for a single connected component, min_dist_component say. Then only update min_dist when a component is finished, either at the end of the loop, or when a STBTT_vmove is encountered. When the winding number indicates that we have an interior point then set min_dist = max(min_dist, min_dist_component).

This would not compute the Euclidean distance, however I believe it would remove artefacts and be good enough for edge rendering.

All of this makes sense, but FYI, traditionally TrueType fonts have non-self-intersecting glyphs. I don't know if it's part of the spec, but it was a de facto part of the spec, at least originally, although maybe fonts that break this assumption have become more common.

The only exception is when they're compound glyphs constructed by combining glyphs (e.g. accents created by simply drawing both the original letter and the accent, each of which will be non-self-intersecting, but once positioned, might overlap), in which case you do have a set of separate connected components and computing the min of each is correct, whereas we might be combining them into one big list, I don't remember.

Just to give some context, it doesn't use truetype fonts, but the Postscript print language has two commands, "stroke" and "fill". Fill will fill a path, and "stroke" will outline it. And of course it was a useful effect to be able to "stroke" a character and get an outline. But if you define the character with self-intersections, like your NotoSansJP "f", obviously that produces output that is NOT what you want. So it was, AFAICT, that's one of the reasons why it was traditional to use non-intersecting paths in fonts.

That does make sense. However according to this page from Apple (see section Intersecting Contours), intersections are allowed with a non-zero winding number to determine what gets included and what doesn't (so technically allowing for the symmetric difference of two components).

I suspect that this particular font has intersections because it's 'strokes' were generated by thickening a weighted path, while Apple cite adding a component to an O to form a Q as a reason for allowing intersections.

Would you mind leaving the issue open while I investigate some ideas for fixing it? I have an idea that Dijkstra's algorithm, or a variant of it, can be used on the resulting bitmap to approximate the correct result. This would only be applied when an intersection is detected (by using the winding number) so no impact on performance for non-intersecting contours.