alexheretic / ab-glyph

Rust API for loading, scaling, positioning and rasterizing OpenType font glyphs
Apache License 2.0
372 stars 24 forks source link

Glyph smudging with small fonts #77

Closed jean-airoldie closed 1 year ago

jean-airoldie commented 1 year ago

Hi,

I wrote a small 2d lib which implements font rendering. I use ab_glyph_rasterizer to rasterize TrueType fonts, which works pretty well. However under specifc combinations of font size and font type I get a weird issue where the font doesn't rasterize properly and it create a smudging effect.

I have included a repository that replicates this issue. The README explains how it works in details.

In context, it results in these kinds of visual artifacts: Screenshot from 2023-03-27 19-18-02

jean-airoldie commented 1 year ago

After some experimentation, I find that this issue only happen with font size below or equal to 12 points using the ubuntu ttf font. So this might be anti-aliasing limitation rather than a bug.

jean-airoldie commented 1 year ago

Nevermind, this appears to be a bug. If I purposely use a wider rasterizer, you can see the smudging extend well beyond the end of the glyph itself.

Here's an example of a normal vs. smudged character:

ubuntu_o_12pts ubuntu_t_12pts

You can clearly see that gray smudge extending to the full width of the image.

alexheretic commented 1 year ago

Digging up a ubuntu.ttf font and using it with the image example (tweaked to similar settings) I see no such issue.

When I observe the outline coords the _abglyph image example computes, for example the letter t. I get

reset: 5x9
line: (1.0588459,2.2016497) (3.225665,2.2016497)
line: (3.225665,2.2016497) (3.225665,3.059578)
line: (3.225665,3.059578) (1.0588459,3.059578)
line: (1.0588459,3.059578) (1.0588459,5.699358)
quad: (1.0588459,5.699358) (1.0588459,6.1283226) (1.1248404,6.408799)
quad: (1.1248404,6.408799) (1.1908349,6.6892757) (1.322824,6.8487625)
quad: (1.322824,6.8487625) (1.454813,7.0082493) (1.6527963,7.0742435)
quad: (1.6527963,7.0742435) (1.85078,7.1402383) (2.114758,7.1402383)
quad: (2.114758,7.1402383) (2.5767193,7.1402383) (2.857196,7.035747)
quad: (2.857196,7.035747) (3.1376727,6.9312553) (3.2476635,6.8872595)
line: (3.2476635,6.8872595) (3.4456472,7.7341886)
quad: (3.4456472,7.7341886) (3.2916598,7.811182) (2.906692,7.9266725)
quad: (2.906692,7.9266725) (2.521724,8.042163) (2.0267653,8.042163)
quad: (2.0267653,8.042163) (1.4438138,8.042163) (1.0643454,7.8936753)
quad: (1.0643454,7.8936753) (0.68487704,7.7451878) (0.45389628,7.4482126)
quad: (0.45389628,7.4482126) (0.22291553,7.1512375) (0.12942332,6.7167735)
quad: (0.12942332,6.7167735) (0.03593111,6.2823095) (0.03593111,5.710357)
line: (0.03593111,5.710357) (0.03593111,0.60678244)
line: (0.03593111,0.60678244) (1.0588459,0.4307971)
line: (1.0588459,0.4307971) (1.0588459,2.2016497)
line: (1.0588459,2.2016497) (1.0588459,2.2016497)

Plugging that into your example mostly renders fine. (Though I would suggest you only set the alpha pixel value with the rasterizer coverage rather than r, g, b and alpha as you are doing as this will over-darken non-100% covered areas. E.g. use [255, 255, 255, coverage] for each pixel.).

This suggests the issue is with your outline coords.

jean-airoldie commented 1 year ago

Digging up a ubuntu.ttf font and using it with the image example (tweaked to similar settings) I see no such issue.

We use a different offset to do the compuation. From what i recall of your implementation you use a rounded x and y offset https://github.com/alexheretic/ab-glyph/blob/7491c2be4e49ea18d0de53e226810cdd529d211d/glyph/src/outlined.rs#L94

...but In my own implementation I use an exact x offset and a rounded y offset because it results in sharper text. So obviously your coordinates will differ from mine.

Plugging that into your example mostly renders fine. (Though I would suggest you only set the alpha pixel value with the rasterizer coverage rather than r, g, b and alpha as you are doing as this will over-darken non-100% covered areas. E.g. use [255, 255, 255, coverage] for each pixel.).

Noted.

This suggests the issue is with your outline coords.

How could the outline coordinates that I've provited result in this smudging that exceeds beyond the coordinates? I will try to find the specific command that causes this to happen.

alexheretic commented 1 year ago

I think dodgy outlines can do that, if the outside becomes the inside. ab_glyph-rasterizer is quite low-level, it needs a well formed outline to work properly.

Regarding rounding, ab_glyph uses exact subpixel draw offsets. Out of interest, where are you seeing this rounding?

On Tue, 28 Mar 2023, 02:36 jean-airoldie, @.***> wrote:

Digging up a ubuntu.ttf font and using it with the image example (tweaked to similar settings) I see no such issue.

We use a different offset to do the compuation. From what i recall of your implementation you use a rounded x and y offset

https://github.com/alexheretic/ab-glyph/blob/7491c2be4e49ea18d0de53e226810cdd529d211d/glyph/src/outlined.rs#L94

...but In my own implementation I use an exact x offset and a rounded y offset because it results in sharper text. So obviously your coordinates will differ from mine.

Plugging that into your example mostly renders fine. (Though I would suggest you only set the alpha pixel value with the rasterizer coverage rather than r, g, b and alpha as you are doing as this will over-darken non-100% covered areas. E.g. use [255, 255, 255, coverage] for each pixel.).

Noted.

This suggests the issue is with your outline coords.

How could the outline coordinates that I've provited result in this smudging that exceeds beyond the coordinates? I will try to find the specific command that causes this to happen.

— Reply to this email directly, view it on GitHub https://github.com/alexheretic/ab-glyph/issues/77#issuecomment-1486082615, or unsubscribe https://github.com/notifications/unsubscribe-auth/AARZHV3KGIW6VMJRODLMJY3W6I6BRANCNFSM6AAAAAAWJY6TYQ . You are receiving this because you commented.Message ID: @.***>

jean-airoldie commented 1 year ago

So the callgraph looks like this:

https://github.com/alexheretic/ab-glyph/blob/7491c2be4e49ea18d0de53e226810cdd529d211d/glyph/src/outlined.rs#L94 https://github.com/alexheretic/ab-glyph/blob/7491c2be4e49ea18d0de53e226810cdd529d211d/glyph/src/outlined.rs#L57 https://github.com/alexheretic/ab-glyph/blob/7491c2be4e49ea18d0de53e226810cdd529d211d/glyph/src/outlined.rs#L18

Where use you this

https://github.com/alexheretic/ab-glyph/blob/7491c2be4e49ea18d0de53e226810cdd529d211d/glyph/src/outlined.rs#L28

Now in my use case the position of the glyph is always on the pixel boudary so x_fract & x_trunc becomes zero, so its just a floor & ceiling rounding.

jean-airoldie commented 1 year ago

If i contrast with my implementation, is use a similar rounding method to determine the size of the rasterizer, but the offset i use when I scale the font coordinates (pos.x * scale + offset) is exact. This results in sharper text.

jean-airoldie commented 1 year ago

In practice I use an exact x offset and a rounded y offset because most fonts are not pixel perfect, and using an exact y results in fonts of visibly mismatched vertical height. Hopefully this is somewhat clear.

jean-airoldie commented 1 year ago

I'm talking about this offset specifically:

https://github.com/alexheretic/ab-glyph/blob/7491c2be4e49ea18d0de53e226810cdd529d211d/glyph/src/outlined.rs#L116

jean-airoldie commented 1 year ago

I think dodgy outlines can do that, if the outside becomes the inside. ab_glyph-rasterizer is quite low-level, it needs a well formed outline to work properly.

Since I'm using an exact offset, my coordinates often start or stop at 0, so I'm guessing this is what can cause this malformed outline. I'll try to isolate.

jean-airoldie commented 1 year ago

I have isolated the cause of the issue:

$ diff fonts/ubuntu_t_12pts.txt fonts/ubuntu_t_12pts_minimized.txt
< quad: (0.090990186,6.827832) (0.0,6.4049954) (0.0,5.8483496)
< line: (0.0,5.8483496) (0.0,0.88135576)
< line: (0.0,0.88135576) (0.9955397,0.71008015)
---
> quad: (0.090990186,6.827832) (0.00001,6.4049954) (0.00001,5.8483496)
> line: (0.00001,5.8483496) (0.00001,0.88135576)
> line: (0.00001,0.88135576) (0.9955397,0.71008015)

fonts/ubuntu_t_12pts.txt

ubuntu_t_12pts

fonts/ubuntu_t_12pts_minimized.txt

ubuntu_t_12pts_minimized

It seems that for some reason using exactly zero in the coordinates causes this issue.

alexheretic commented 1 year ago

That's the calculation of the pixel bounds, not the offset. Of course the pixel bounds are integers, the rasterizer works on a w*h integer coverage grid. The offset is simply made relative to the pixel bounds, their subpixel positions aren't rounded at all.

On Tue, 28 Mar 2023, 03:11 jean-airoldie, @.***> wrote:

I think dodgy outlines can do that, if the outside becomes the inside. ab_glyph-rasterizer is quite low-level, it needs a well formed outline to work properly.

Since I'm using an exact offset, my coordinates often start or stop at 0, so I'm guessing this is what can cause this malformed outline. I'll try to isolate.

— Reply to this email directly, view it on GitHub https://github.com/alexheretic/ab-glyph/issues/77#issuecomment-1486104818, or unsubscribe https://github.com/notifications/unsubscribe-auth/AARZHV5ZQS6MARTTXL4WEL3W6JCENANCNFSM6AAAAAAWJY6TYQ . You are receiving this because you commented.Message ID: @.***>

jean-airoldie commented 1 year ago

The offset is simply made relative to the pixel bounds, their subpixel positions aren't rounded at all.

You use the pixel bound calculation as your offset that you use when scaling up.

This is rounded. https://github.com/alexheretic/ab-glyph/blob/7491c2be4e49ea18d0de53e226810cdd529d211d/glyph/src/outlined.rs#L94

Therefore this is also rounded https://github.com/alexheretic/ab-glyph/blob/7491c2be4e49ea18d0de53e226810cdd529d211d/glyph/src/outlined.rs#L116

jean-airoldie commented 1 year ago

I don't do that, i use the exact pixel bounds calculated when oulining the glyph therefore this result in a different outline. Anyway this is besides the point.

alexheretic commented 1 year ago

But it isn't rounded. It is a difference, between the absolute position and the pixel bounds. So it is an exact position relative to the coverage grid. Rounding is a kind of data loss and that isn't happening here.

This maybe is important because if you are not calculating the whole-pixel bounds properly you are probably not using ab_glyph_rasterizer correctly. It renders on a grid of whole-pixels that should be 1-to-1 with screen pixels. The min size is the exact bounds widened to an integer w & h. So if you're using the non-int exact-bounds to calculate the offset here then the outline positions will probably be wrong. All coords during rasterization are relative to the whole-pixel bounds of the Rasterizer.

On Tue, 28 Mar 2023, 03:22 jean-airoldie, @.***> wrote:

The offset is simply made relative to the pixel bounds, their subpixel positions aren't rounded at all.

You use the pixel bound calculation as your offset that you use when scaling up.

This is rounded.

https://github.com/alexheretic/ab-glyph/blob/7491c2be4e49ea18d0de53e226810cdd529d211d/glyph/src/outlined.rs#L94

Therefore this is also rounded

https://github.com/alexheretic/ab-glyph/blob/7491c2be4e49ea18d0de53e226810cdd529d211d/glyph/src/outlined.rs#L116

— Reply to this email directly, view it on GitHub https://github.com/alexheretic/ab-glyph/issues/77#issuecomment-1486112024, or unsubscribe https://github.com/notifications/unsubscribe-auth/AARZHV2EAYCRHTFK25F7WITW6JDQBANCNFSM6AAAAAAWJY6TYQ . You are receiving this because you commented.Message ID: @.***>

jean-airoldie commented 1 year ago

The reason you haven't experienced my issue before, is because your (x, y) coordinates that you obtain after scaling are not aligned with a pixel boundary. If they were aligned, then some of your (x, y) coordinates you be 0.0. If we look back at the ubuntu ttf that you used, it produced the following commands:

reset: 5x9
line: (1.0588459,2.2016497) (3.225665,2.2016497)
line: (3.225665,2.2016497) (3.225665,3.059578)
line: (3.225665,3.059578) (1.0588459,3.059578)
line: (1.0588459,3.059578) (1.0588459,5.699358)
quad: (1.0588459,5.699358) (1.0588459,6.1283226) (1.1248404,6.408799)
quad: (1.1248404,6.408799) (1.1908349,6.6892757) (1.322824,6.8487625)
quad: (1.322824,6.8487625) (1.454813,7.0082493) (1.6527963,7.0742435)
quad: (1.6527963,7.0742435) (1.85078,7.1402383) (2.114758,7.1402383)
quad: (2.114758,7.1402383) (2.5767193,7.1402383) (2.857196,7.035747)
quad: (2.857196,7.035747) (3.1376727,6.9312553) (3.2476635,6.8872595)
line: (3.2476635,6.8872595) (3.4456472,7.7341886)
quad: (3.4456472,7.7341886) (3.2916598,7.811182) (2.906692,7.9266725)
quad: (2.906692,7.9266725) (2.521724,8.042163) (2.0267653,8.042163)
quad: (2.0267653,8.042163) (1.4438138,8.042163) (1.0643454,7.8936753)
quad: (1.0643454,7.8936753) (0.68487704,7.7451878) (0.45389628,7.4482126)
quad: (0.45389628,7.4482126) (0.22291553,7.1512375) (0.12942332,6.7167735)
quad: (0.12942332,6.7167735) (0.03593111,6.2823095) (0.03593111,5.710357)
line: (0.03593111,5.710357) (0.03593111,0.60678244)
line: (0.03593111,0.60678244) (1.0588459,0.4307971)
line: (1.0588459,0.4307971) (1.0588459,2.2016497)
line: (1.0588459,2.2016497) (1.0588459,2.2016497)

You can see that at no point is there a 0.0 coordinate for either the x or the y axis. If you used an exact offset for either the x or the y axis, then at least some of the points would be 0.0.

Conceptually using an exact offset here would be the equivalent of starting the outline at the point (0.0, 0.0) instead of whatever point the original author intended (human error). The exact offset in this case would be calculated by using the glyph bounding box exactly without rounding.

So In my implementation I use two sets of bounds, exact and rounded. I use the exact bounds as the offset for the scale * x + offset calculation and I use the rounded ones to determine the size of the rasterizer. This results in crisper text.

jean-airoldie commented 1 year ago

Anyway,

Thanks for your time, I'll fix this issue some other way.