Parser for the OpenType GPOS table: glyph positioning.
Tincture reads one thing from GPOS - pair kerning, in both its formats.
Format 1 lists explicit glyph pairs; format 2 declares glyph classes and a
class1 * class2 adjustment matrix.
Guardrails
The class matrix is the attack surface. A font declares class1_count and
class2_count and the parser is expected to expand their product. A
malformed or hostile font can declare a product large enough to exhaust
memory, so expansion is capped and oversized subtables are skipped with a log
line rather than expanded.
Skips are counted in a process-local scope. The counter is incremented from
seven places deep in the subtable parsers and read once at the end; threading
it back through those with chains would reshape the control flow of the
code that produces kerning pairs. Callers must bracket a parse with
begin_guardrail_scope/0 and end_guardrail_scope/1, which makes nesting
safe.
Summary
Functions
Begin a guardrail-counting scope, returning the previous count to restore.
End a guardrail-counting scope, restoring what begin_guardrail_scope/0
returned.
The process dictionary key the guardrail counter is stored under.
How many GPOS subtables were skipped by a guardrail in the current scope.
Functions
@spec begin_guardrail_scope() :: term()
Begin a guardrail-counting scope, returning the previous count to restore.
The counter is process-local because it is incremented from seven places deep
inside the GPOS pair and class parsers and read once at the end; threading it
back through those with chains would reshape the control flow of the code
that produces kerning pairs.
Callers must pair this with end_guardrail_scope/1 in an after block. That
makes the counter re-entrancy-safe: without it a nested parse would zero an
enclosing parse's accumulated count and silently under-report.
@spec end_guardrail_scope(term()) :: :ok
End a guardrail-counting scope, restoring what begin_guardrail_scope/0
returned.
@spec guardrail_scope_key() :: term()
The process dictionary key the guardrail counter is stored under.
Exposed so tests can assert on the counter's lifecycle without hardcoding a module name. A hardcoded key silently stops matching when this module moves, which turns the isolation tests into assertions about a key nothing writes - they then pass for the wrong reason. That has now happened twice.
@spec guardrail_skips() :: non_neg_integer()
How many GPOS subtables were skipped by a guardrail in the current scope.