golang / freetype

The Freetype font rasterizer in the Go programming language.
Other
778 stars 183 forks source link

discrepancy in scale factor conversion between freetype and truetype packages #85

Open dmitshur opened 2 years ago

dmitshur commented 2 years ago

I noticed there's sometimes a tiny discrepancy in font metrics as computed by freetype.Context and truetype.NewFace.

As a reproducible example, using the Go Mono font of size 86.4 exactly, at 72.0 DPI, the advance width for the glyph 'H' differs by 1/64 (the smallest value a fixed.Int26_6 can represent).

See the complete program on the Go Playground. Its output:

advance width of 'H' via truetype: 51:55
advance width of 'H' via freetype: 51:54

I've tracked it down and found the root cause. When computing the scale factor, the float64 → fixed 26.6 conversion is done differently between those two packages. In truetype, it rounds to the nearest 26.6 fixed point value:

scale:      fixed.Int26_6(0.5 + (opts.size() * opts.dpi() * 64 / 72)),

But in freetype, it uses the floor:

c.scale = fixed.Int26_6(c.fontSize * c.dpi * (64.0 / 72.0))

Between those two, it seems taking the nearest value is the better behavior, so I'll send a PR that adjusts freetype to fix this discrepancy. CC @nigeltao.

nigeltao commented 2 years ago

it seems taking the nearest value is the better behavior

It's been a while since I remembered how this all works. Do you know what the C FreeType library does re round-down versus round-to-nearest? Does C FreeType even have a similar concept?? C FreeType and Go FreeType don't necessarily have identical APIs (even after accounting for C vs Go idioms)...

If you don't know, that's fine, I can dig into it. It's just that, if you already know, it'd save me some work.

FWIW, golang.org/x/image/font/opentype also rounds to nearest. https://github.com/golang/image/blob/a66eb6448b8d7557efb0c974c8d4d72085371c58/font/opentype/opentype.go#L111 says

scale:   fixed.Int26_6(0.5 + (opts.Size * opts.DPI * 64 / 72)),

so changing Go's freetype.Context (a 2010-ish era concept?? predating fixed.Int26_6) to round-to-nearest is probably the most consistent thing to do...

dmitshur commented 2 years ago

Thanks for taking a look.

I haven’t looked at the C FreeType code, so I don’t know which it uses offhand. I wouldn’t mind trying to look later on if it can help.

dmitshur commented 2 years ago

I took a look.

Specifically, I was looking over the C FreeType API and trying to see if there's something that accepts a font size in floating point and converts to fixed point. From what I was able to find, the C FreeType API doesn't have that: it largely accepts integers for DPI and fixed point for points, or integers for pixels. This means I wasn't able to find direct precedent for the Go API to follow in this particular situation.

I did find a few places that in spirit seem to support the general idea of rounding to a nearest value rather than truncating, including:

There are also mentions of a couple exceptions due to historical reasons, such ascender being rounded up to an integer value, and descender rounded down to an integer value. But those were the only two exceptions in terms of unusual rounding that I spotted.

nigeltao commented 2 years ago

Copy-pasting a pull-request comment https://github.com/golang/freetype/pull/86#issuecomment-962523402 here:


I just noticed there's a Context.PointToFixed method that does the same conversion:

return fixed.Int26_6(x * float64(c.dpi) * (64.0 / 72.0))

We should not change one without also changing the other, as that would fix #85 but introduce another inconsistency within this package. Hmm.

dmitshur commented 1 year ago

To update this issue with the latest PR status: the comment above is resolved in https://github.com/golang/freetype/pull/86#issuecomment-970275372.