freetype icon indicating copy to clipboard operation
freetype copied to clipboard

discrepancy in scale factor conversion between freetype and truetype packages

Open dmitshur opened this issue 4 years ago • 4 comments

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.

dmitshur avatar Oct 24 '21 01:10 dmitshur

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...

nigeltao avatar Oct 27 '21 03:10 nigeltao

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 avatar Oct 27 '21 05:10 dmitshur

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.

dmitshur avatar Oct 31 '21 18:10 dmitshur

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.

nigeltao avatar Nov 16 '21 08:11 nigeltao

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

dmitshur avatar Oct 29 '22 19:10 dmitshur