Skip to content

fix: decode 16.16 Fixed with /65536 instead of /65535#857

Open
spokodev wants to merge 1 commit into
opentypejs:masterfrom
spokodev:fix/fixed-point-divisor
Open

fix: decode 16.16 Fixed with /65536 instead of /65535#857
spokodev wants to merge 1 commit into
opentypejs:masterfrom
spokodev:fix/fixed-point-divisor

Conversation

@spokodev

Copy link
Copy Markdown

getFixed (src/parse.mjs) decodes a 16.16 Fixed value's fractional part with / 65535:

function getFixed(dataView, offset) {
    const decimal = dataView.getInt16(offset, false);
    const fraction = dataView.getUint16(offset + 2, false);
    return decimal + fraction / 65535;   // should be / 65536
}

Per the OpenType Data Types spec, Fixed is a "32-bit signed fixed-point number (16.16)" — the 16-bit fraction is a binary fraction over 2^16 = 65536, not 65535. The sibling parseF2Dot14 in the same file correctly divides by 16384 = 2^14, which isolates this as an off-by-one in getFixed.

Effect

getFixed backs post.italicAngle, head.fontRevision, the fvar axis min/default/max + instance coordinates, and the STAT table values. They are all off by up to 1/65536 (~1.5e-5), and they do not round-trip: the encode path scales by 65536 (src/types.mjs, src/tables/post.mjs, src/tables/fvar.mjs), so parse → write → parse drifts.

Concrete real fonts (macOS system fonts):

Font post.italicAngle (current) correct (/65536)
Arial Narrow Italic -9.699992370489051 -9.699996948242188
Times New Roman Italic -16.332982375829708 -16.332992553710938

The magnitude is small — it does not cross a glyph-metric/rendering threshold — so this is a spec-conformance / round-trip-consistency fix, not a rendering change.

Fix

getFixed: / 65535/ 65536. The two STAT parse fixtures (test/tables/stat.spec.mjs) asserted the old /65535 values (e.g. raw Fixed 01 23 45 67 = 291 + 17767/65535 = 291.2711070…); updated to the spec value 291 + 17767/65536 = 291.2711029…. They fail on the current code and pass with the fix; the full suite (342) and lint are green.

getFixed divided the 16-bit fractional part by 65535, but a 16.16 Fixed
value's fraction is a binary fraction over 2^16 = 65536 (the sibling
parseF2Dot14 correctly uses 2^14). The encode path scales by 65536, so a
parse -> write -> parse round-trip drifted and parsed values disagreed
with fonttools.

This affects post.italicAngle, head.fontRevision, fvar axis min/default/
max and instance coordinates, and STAT values: e.g. Arial Narrow Italic
italicAngle parses as -9.699992 instead of the correct -9.699997. The
error is up to 1/65536 per value.

Update the STAT parse fixtures, which asserted the /65535 values.
@ILOVEPIE

ILOVEPIE commented Jun 24, 2026 via email

Copy link
Copy Markdown
Member

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants