Tincture.Font.Context (Tincture v0.1.0)

Copy Markdown View Source

A measurement context — how wide a string will be, in any font a document knows.

Tincture.Font.text_width/3 is pure, and can only resolve the standard 14 fonts and AFM files on disk. An embedded TrueType font has no AFM: its metrics are parsed out of the file at registration time and live on the Tincture.PDF.t/0 struct. So a pure function cannot measure one, and for a long time the layout and typography layer — which measured through text_width/3 — simply raised unknown font for every embedded font.

A context closes that gap. It carries the embedded metrics alongside the static ones, so anything holding a document can measure any font that document can draw:

context = Context.from_pdf(pdf)
Context.text_width(context, "Body", 11, "Hello")

Callers that already have a %Tincture.PDF{}Tincture.text_paragraph/6, Tincture.Layout.Table.render/6, Tincture.Layout.Box.flow_text/7 — build one themselves, so using an embedded font for layout needs nothing extra. Build one by hand only when measuring outside a document, or when constructing a Tincture.Typography.RichText.t/0 up front.

What it measures

For an embedded font: per-glyph advance widths from hmtx, resolved through cmap, scaled by the font's units-per-em, with GPOS pair kerning applied and Unicode variation sequences honoured. That is the same arithmetic the drawing path uses, so a measured width matches what is actually rendered.

For everything else it defers to Tincture.Font.text_width/3.

Name precedence

A name registered as an embedded font and also naming a standard font resolves to the standard metrics, which is what the pre-existing drawing path did. Registering a TTF as "Helvetica" is therefore not a way to override the built-in metrics.

Summary

Functions

Build a context from a document, carrying every font registered on it.

The codepoint within a grapheme that participates in kerning.

Whether this context can measure the given font without falling back.

Measure text, reporting whether the font could actually be resolved.

An empty context, resolving only the standard 14 fonts and AFM files.

The GPOS kerning adjustment between two codepoints, in font design units.

The width of text in font_name at size, in points.

Types

t()

@type t() :: %Tincture.Font.Context{embedded: %{optional(String.t()) => map()}}

Functions

from_pdf(arg1)

@spec from_pdf(map()) :: t()

Build a context from a document, carrying every font registered on it.

kerning_codepoint(grapheme)

@spec kerning_codepoint(String.t()) :: integer() | nil

The codepoint within a grapheme that participates in kerning.

A grapheme cluster can carry combining marks and variation selectors, none of which take part in a kerning pair. This picks the base character.

measurable?(context, font_name)

@spec measurable?(t(), String.t()) :: boolean()

Whether this context can measure the given font without falling back.

measure(context, font_name, size, text)

@spec measure(t(), String.t(), number(), String.t()) ::
  {:ok, float()} | {:unresolved, float()}

Measure text, reporting whether the font could actually be resolved.

Returns {:ok, width} when the width is real, and {:unresolved, estimate} when this context has never heard of the font — carrying a rough width from the point size so a caller can carry on and decide later.

This exists because rich text is measured when it is built, which may be before the document it will be drawn into is known. An embedded font is unresolvable at that moment but perfectly resolvable at layout time, and is indistinguishable from a typo until then. Callers use this to hold the question open rather than guessing, and Tincture.Typography.RichText.remeasure/2 closes it.

new()

@spec new() :: t()

An empty context, resolving only the standard 14 fonts and AFM files.

Equivalent to measuring through Tincture.Font.text_width/3 directly.

pair_kerning_units(left, right, gpos_pair_kerns)

@spec pair_kerning_units(integer() | nil, integer() | nil, map()) :: integer()

The GPOS kerning adjustment between two codepoints, in font design units.

Exposed because the drawing path positions each grapheme individually when kerning is on, and so needs the adjustment for one pair at a time rather than the total across a string.

text_width(context, font_name, size, text, opts \\ [])

@spec text_width(t(), String.t(), number(), String.t(), keyword()) :: float()

The width of text in font_name at size, in points.

Options

  • :on_unknown — what to do with a font this context cannot resolve. :raise (the default) lets Tincture.Font.text_width/3 raise, which catches a mistyped font name during layout. :estimate returns a rough width from the point size instead, which is what the drawing path needs: Tincture.set_font/3 does not validate, so a document can already be drawing with a font nothing knows about, and a raise there would turn a cosmetic problem into a crash.