Tincture.Font.CFF (Tincture v0.1.0)

Copy Markdown View Source

Primitives for the Compact Font Format container.

CFF (Adobe TN#5176) is the PostScript-outline format carried inside an OpenType font's CFF table. This module owns the container: INDEX structures and DICT operand encoding. It deliberately does not interpret what those operands mean — that is the caller's job, and the two callers want different things:

  • Tincture.Font.TTF reads a CFF table for metadata (font name, FontBBox, StemV, weight).
  • Tincture.PDF.Serialize rewrites a CFF table when subsetting an embedded OpenType font, which additionally needs byte offsets and sizes so it can patch the top DICT.

Both previously carried their own copy of these primitives, in modules that never referenced each other. Five of the eight duplicated functions had already drifted — most consequentially parse_dict_number/1, where operator 255 (16.16 fixed) produced a bare float in one copy and a normalised integer in the other for the same input bytes. The reconciled behaviour is documented per function below.

Every function returns :error rather than raising: font files are untrusted input and a malformed table must degrade, not crash the render.

Summary

Types

A decoded INDEX.

Functions

Decode a packed run of INDEX offsets, each off_size bytes wide.

Convert a raw 16.16 fixed-point integer to a number.

Returns true when list never decreases.

Trim a string value read from a font table, returning nil when it is empty.

Parse a single DICT operand from the head of data.

Parse a CFF INDEX from the start of data.

Slice INDEX object data using an already-decoded offset list.

Parse a CFF real (BCD) number, the payload following operator 30.

Types

index()

@type index() :: %{
  objects: [binary()],
  rest: binary(),
  size: non_neg_integer(),
  offsets: [non_neg_integer()],
  objects_data_offset: non_neg_integer()
}

A decoded INDEX.

size is the INDEX's total byte length and objects_data_offset the offset from the start of the INDEX to the first object's data. Both are needed to patch offsets when rewriting a CFF table; readers can ignore them.

Functions

cff_sid_to_string(string_index, sid)

decode_index_offsets(bin, off_size)

@spec decode_index_offsets(binary(), 1..4) :: {:ok, [non_neg_integer()]} | :error

Decode a packed run of INDEX offsets, each off_size bytes wide.

off_size is bounded to 1..4 by the CFF specification. The bound is enforced here rather than left to callers: with off_size == 0 the decode loop would consume nothing per iteration and never terminate.

extract_cff_escaped_operator_operand(top_dict, escaped_operator)

extract_cff_operator_operand(top_dict, operator)

extract_cff_operator_operands(top_dict, operator)

fetch_cff_metadata(data, table_records)

fetch_cff_top_dict(data, table_records)

fixed_16_16_to_number(raw_value)

@spec fixed_16_16_to_number(integer()) :: number()

Convert a raw 16.16 fixed-point integer to a number.

Whole values come back as integers so that a FontBBox of 1 does not become 1.0 purely because of how it was encoded.

nondecreasing?(list)

@spec nondecreasing?([number()]) :: boolean()

Returns true when list never decreases.

Used to validate INDEX offset tables, where a decrease would mean an object with negative length.

normalize_name_value(value)

@spec normalize_name_value(term()) :: String.t() | nil

Trim a string value read from a font table, returning nil when it is empty.

Font tables routinely carry padded or blank strings where a field is unset; nil is more useful downstream than an empty binary.

parse_dict_number(arg1)

@spec parse_dict_number(binary()) :: {:ok, number(), binary()} | :error

Parse a single DICT operand from the head of data.

Handles every CFF DICT number encoding: single-byte (32..246), two-byte positive (247..250) and negative (251..254), 16-bit (28), 32-bit (29), 16.16 fixed (255) and real/BCD (30).

Operator 255 is normalised through fixed_16_16_to_number/1, so a whole value comes back as an integer rather than a float. The other historical copy returned a bare value / 65_536, which made 1 and 1.0 depend on which module happened to parse the byte.

parse_index(arg1)

@spec parse_index(binary()) :: {:ok, index()} | :error

Parse a CFF INDEX from the start of data.

An INDEX is a count, an offset size, count + 1 offsets, then the object data. A zero count is legal and yields no objects.

parse_index_objects(offsets, count, objects_and_rest)

@spec parse_index_objects([non_neg_integer()], pos_integer(), binary()) ::
  {:ok, [binary()], binary(), non_neg_integer()} | :error

Slice INDEX object data using an already-decoded offset list.

Returns the objects, the remaining binary after the INDEX, and the size of the object data region.

CFF INDEX offsets are 1-based, so the first must be at least 1 and the list must be nondecreasing. Those two conditions together also guarantee every object has a non-negative length, so no per-object check is needed.

parse_real_number(data)

@spec parse_real_number(binary()) :: {:ok, number(), binary(), pos_integer()} | :error

Parse a CFF real (BCD) number, the payload following operator 30.

Returns the value, the remaining binary, and how many bytes the encoding consumed. The byte count matters when rewriting a DICT in place, where an operand's span has to be known to patch around it.