Tincture.Font.OpenType.GPOS (Tincture v0.1.0)

Copy Markdown View Source

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

begin_guardrail_scope()

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

end_guardrail_scope(previous_skip_count)

@spec end_guardrail_scope(term()) :: :ok

End a guardrail-counting scope, restoring what begin_guardrail_scope/0 returned.

guardrail_scope_key()

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

guardrail_skips()

@spec guardrail_skips() :: non_neg_integer()

How many GPOS subtables were skipped by a guardrail in the current scope.

parse_gpos_pair_kerns(data, table_records, cmap_by_code)