golang/freetype

discrepancy in scale factor conversion between freetype and truetype packages

dmitshur opened this issue · 5 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.

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

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.

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.

Copy-pasting a pull-request comment #86 (comment) 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.

To update this issue with the latest PR status: the comment above is resolved in #86 (comment).