Files

1464 lines
52 KiB
Odin
Raw Permalink Normal View History

2026-09-25 23:32:29 +02:00
package pdf
import "core:mem"
import "core:slice"
import "core:unicode/utf8"
// Fonts (ISO 32000-2, 9.5 - 9.10).
//
// A Font ties a PDF font dictionary to a glyph source. The three font-program
// readers (font_cff.odin, font_truetype.odin, font_type1.odin) know nothing
// about PDF: they turn bytes into outlines and are separately testable. This
// file is where the format's encoding, width and CID machinery lives.
//
// A font that fails to load still produces a Font with widths, so that text
// lays out and extracts correctly even when no glyph can be drawn. Missing
// glyphs draw nothing; they never abort a page.
//
// THE THREE SHAPES A PDF FONT COMES IN
//
// simple one byte per code, 256 codes, an encoding of glyph NAMES, and
// /Widths indexed from /FirstChar. Type1, MMType1 and TrueType.
// composite a CMap turns a variable-width code into a CID, the descendant
// font turns the CID into a glyph, and /W gives the widths.
// Type3 glyphs are content streams, so this file only records where
// they are; the interpreter draws them. Its /FontMatrix is the
// font's own and is not 1/1000.
//
// Everything a Font points at is allocated from the Document's arena and dies
// with it. Fonts are cached on the Document keyed by the address of their
// dictionary, because a page that sets the same font two hundred times must
// build it once.
//
// WHAT IS PRECOMPUTED AND WHY
//
// A simple font's code -> glyph and code -> width tables are built once, at
// load: 256 entries each, resolved through the whole encoding cascade. Text
// showing then costs one array index per character rather than a walk through
// the encoding, the charset and two cmap subtables. A composite font cannot do
// the same -- its CID space is 65536 wide -- so its lookups stay on the hot
// path and are binary searches.
// Codes a simple font has, and the width of the two precomputed tables.
@(private)
FONT_CODES :: 256
// Entries kept from a /W or /W2 array. The arrays are attacker-controlled and
// the `c [w ...]` form costs one entry per width, so both are capped well
// above any real CJK font and far below anything that hurts.
@(private)
MAX_CID_WIDTHS :: 1 << 16
// Items read from one /W or /W2 array before the walk gives up. Two per
// entry in the range form, plus the array elements themselves.
@(private)
MAX_W_ITEMS :: 1 << 17
// u16 entries kept from a /CIDToGIDMap stream. CIDs are 16-bit, so this is
// the format's own ceiling rather than an invented one.
@(private)
MAX_CID_TO_GID :: 65536
// Entries read from a /Differences array. Each is one code, so more than a
// few hundred is either damage or an attack.
@(private)
MAX_DIFFERENCES :: 4096
// /MissingWidth and every width from a file are clamped to this in absolute
// value. A width of 1e18 turns a text matrix into an infinity three
// multiplications later, and the geometry below the device line may not see
// one. 1000 is an em; ten thousand ems is already absurd.
@(private)
MAX_GLYPH_WIDTH :: 10_000_000.0
Font_Kind :: enum u8 {
Type1,
TrueType,
Type3,
Type0, // composite; the descendant carries the program
}
// Cid_Width is one run of the /W array: CIDs `lo` through `hi` are `width`
// wide, in glyph space.
Cid_Width :: struct {
lo, hi: u32,
width: f64,
}
// Cid_Vmetric is one run of the /W2 array: CIDs `lo` through `hi` advance by
// `w1y` vertically and have their origin displaced by (`v1x`, `v1y`) from the
// horizontal origin. ISO 32000-2, 9.7.4.3.
Cid_Vmetric :: struct {
lo, hi: u32,
w1y: f64,
v1x, v1y: f64,
}
// Font_Code is one decoded character code, ready to draw and to extract.
Font_Code :: struct {
code: u32, // the raw code taken from the string
cid: u32, // equal to `code` for a simple font
gid: u32, // glyph index in the program; 0 is .notdef
width: f64, // glyph space, so 1/1000 text space for everything but Type3
unicode: rune, // -1 when the font offers none
nbytes: int, // bytes of the string this code consumed
is_space: bool, // a single-byte code 32: word spacing applies
// Position vector, glyph space: where the glyph's horizontal origin sits
// relative to its vertical one. Zero unless the font is vertical, in which
// case `width` is the VERTICAL displacement w1y (normally negative) and
// this is what moves the glyph off the vertical baseline. 9.7.4.3.
vx, vy: f64,
}
Font :: struct {
doc: ^Document,
dict: ^Dict,
kind: Font_Kind,
base_font: string,
// -- simple fonts (9.6) ----------------------------------------------
first_char: int,
last_char: int,
// /Widths, indexed from first_char, in glyph space.
widths: []f64,
missing_width: f64,
// The resolved glyph name for each code, or "" where the encoding gives
// none. This is the whole of a simple font's encoding after /BaseEncoding
// and /Differences have been applied over the program's built-in one.
enc_names: [256]string,
// Code to glyph and code to width, resolved once at load. See the header.
code_gid: [FONT_CODES]u32,
code_width: [FONT_CODES]f64,
// -- composite fonts (9.7) -------------------------------------------
// Code to CID.
cmap: ^Cmap,
// CID to GID: /CIDToGIDMap, empty when it is /Identity.
cid_to_gid: []u16,
cid_widths: []Cid_Width,
default_width: f64,
vertical: bool,
// /DW2 and /W2, for vertical writing.
default_vy: f64,
default_w1: f64,
cid_vmetrics: []Cid_Vmetric,
// The descendant font dictionary, kept for callers that need /CIDSystemInfo.
descendant: ^Dict,
// -- the glyph program ------------------------------------------------
cff: ^Cff_Font,
ttf: ^TrueType_Font,
t1: ^Type1_Font,
// Glyph space to text space. 1/1000 for Type1 and CFF, 1/unitsPerEm for
// TrueType, and whatever /FontMatrix says for Type3.
mtx: Matrix,
embedded: bool,
// -- Type3 (9.6.4) ----------------------------------------------------
// Type3 glyphs are content streams, so drawing one re-enters the
// interpreter rather than the rasteriser.
char_procs: ^Dict,
t3_resources: ^Dict,
// -- text extraction --------------------------------------------------
to_unicode: ^Cmap,
// -- descriptor flags (9.8.2) -----------------------------------------
symbolic: bool,
serif: bool,
fixed_pitch: bool,
italic: bool,
// Set when the program could not be read at all; widths still work.
program_failed: bool,
// The standard-14 metrics this /BaseFont resolves to, or -1 for a font
// that is not one of them. A code with no /Widths entry is measured with
// these before it falls back to /MissingWidth.
std_index: int,
}
// ---------------------------------------------------------------------------
// Loading
// ---------------------------------------------------------------------------
// font_load builds a Font from a font dictionary. The result belongs to the
// Document and is cached on it, so loading the same dictionary twice is one
// map lookup.
//
// An error comes back only when there is no dictionary at all. Everything
// else degrades: a font program that will not parse leaves `program_failed`
// set and the widths and encodings intact, because a page whose glyphs cannot
// be drawn must still lay its text out and extract it.
font_load :: proc(doc: ^Document, d: ^Dict) -> (^Font, Error) {
if doc == nil || d == nil {
return nil, .Invalid_Argument
}
key := uintptr(rawptr(d))
if cached, ok := doc.font_cache[key]; ok {
return cached, .None
}
f := new(Font, doc.allocator)
f.doc = doc
f.dict = d
f.mtx = Matrix{0.001, 0, 0, 0.001, 0, 0}
f.std_index = -1
f.default_width = 1000
// ISO 32000-2, 9.7.4.3: /DW2 defaults to [880 -1000].
f.default_vy = 880
f.default_w1 = -1000
// Cached before the body runs. Nothing here recurses into font_load
// today, but a /Resources cycle reaching back to this font would be the
// file's choice, not ours, and this makes it a lookup rather than a loop.
if doc.font_cache == nil {
doc.font_cache = make(map[uintptr]^Font, 16, doc.allocator)
}
doc.font_cache[key] = f
f.base_font = dict_name(doc, d, "BaseFont")
f.kind = font_kind(doc, d)
if idx, ok := std_font_lookup(f.base_font); ok {
f.std_index = idx
}
if tu := dict_get(doc, d, "ToUnicode"); tu != nil {
if _, is_stream := tu.(^Stream); is_stream {
f.to_unicode, _ = cmap_load(doc, tu)
}
}
switch f.kind {
case .Type0:
font_load_type0(doc, f, d)
case .Type3:
font_load_type3(doc, f, d)
case .Type1, .TrueType:
font_load_simple(doc, f, d)
}
return f, .None
}
// font_matrix maps glyph space to text space.
//
// Every font but Type3 measures glyphs in thousandths of a text space unit,
// whatever the program's own units are -- the readers hand back outlines in
// their own units and this is the matrix that reconciles them. A Type3 font
// carries its own /FontMatrix and may use any units at all.
font_matrix :: proc(f: ^Font) -> Matrix {
if f == nil {
return Matrix{0.001, 0, 0, 0.001, 0, 0}
}
return f.mtx
}
// font_is_type3 reports whether drawing a glyph means running a content
// stream rather than filling an outline.
font_is_type3 :: proc(f: ^Font) -> bool {
return f != nil && f.kind == .Type3
}
// font_glyph_path appends one glyph's outline, in the program's glyph space,
// to `out`. `font_matrix` maps that space to text space.
//
// A font with no usable program, and a Type3 font (whose glyphs are content
// streams), report .No_Glyph. So does a glyph index the program does not
// have. None of those are page-level failures: the caller draws nothing and
// advances the pen.
font_glyph_path :: proc(f: ^Font, gid: u32, out: ^Path) -> Error {
if f == nil || out == nil {
return .Invalid_Argument
}
if f.kind == .Type3 {
return .No_Glyph
}
if gid > 0xFFFF {
// Every one of the three programs indexes glyphs with a u16.
return .No_Glyph
}
g := u16(gid)
// A font may hold two programs at once: an OpenType wrapper whose
// outlines are CFF is parsed by both readers, and only the CFF can draw.
if f.ttf != nil && tt_has_glyf(f.ttf) {
return tt_glyph_path(f.ttf, g, out)
}
if f.cff != nil {
return cff_glyph_path(f.cff, g, out)
}
if f.t1 != nil {
return t1_glyph_path(f.t1, g, out)
}
return .No_Glyph
}
// font_kind reads /Subtype, and guesses when it is missing or nonsense.
//
// The guess matters: a font dictionary with no /Subtype is malformed but
// perfectly readable, and its other keys say unambiguously what it is.
@(private)
font_kind :: proc(doc: ^Document, d: ^Dict) -> Font_Kind {
switch dict_name(doc, d, "Subtype") {
case "Type0": return .Type0
case "Type3": return .Type3
case "TrueType": return .TrueType
case "Type1", "MMType1": return .Type1
}
if dict_has(d, "DescendantFonts") { return .Type0 }
if dict_has(d, "CharProcs") || dict_has(d, "FontMatrix") { return .Type3 }
return .Type1
}
// ---------------------------------------------------------------------------
// Simple fonts (9.6)
// ---------------------------------------------------------------------------
@(private)
font_load_simple :: proc(doc: ^Document, f: ^Font, d: ^Dict) {
desc := dict_dict(doc, d, "FontDescriptor")
font_read_descriptor(doc, f, desc)
font_load_program(doc, f, desc)
font_set_matrix(f)
font_build_encoding(doc, f, d)
font_read_widths(doc, f, d)
for code in 0 ..< FONT_CODES {
f.code_gid[code] = font_simple_gid(f, u8(code))
f.code_width[code] = font_simple_width(f, u8(code))
}
}
// font_read_descriptor takes the flags and /MissingWidth off the descriptor.
//
// A font with no descriptor at all is one of the standard 14 (9.8.1 excuses
// them), and the two symbolic ones are symbolic whatever a missing dictionary
// does not say.
@(private)
font_read_descriptor :: proc(doc: ^Document, f: ^Font, desc: ^Dict) {
if desc == nil {
if f.std_index >= 0 {
f.symbolic = std_font_is_symbolic(f.std_index)
}
return
}
flags := u32(dict_int(doc, desc, "Flags", 0))
// 9.8.2, table 121. Bit positions are 1-based in the table.
f.fixed_pitch = flags & (1 << 0) != 0
f.serif = flags & (1 << 1) != 0
f.italic = flags & (1 << 6) != 0
// Producers set both Symbolic and Nonsymbolic often enough that the pair
// has to be read as a whole: a font that claims both is claiming nothing,
// and a Latin text font is the safer reading of nothing.
f.symbolic = flags & (1 << 2) != 0 && flags & (1 << 5) == 0
f.missing_width = font_clamp_width(dict_num(doc, desc, "MissingWidth", 0))
}
// font_read_widths reads /FirstChar, /LastChar and /Widths.
//
// /LastChar is recorded and is deliberately NOT used to bound the lookup: the
// array's own length is. A /LastChar that disagrees with the array is far
// commoner than an over-long array, and honouring it would throw away widths
// that are sitting right there.
@(private)
font_read_widths :: proc(doc: ^Document, f: ^Font, d: ^Dict) {
f.first_char = int(dict_int(doc, d, "FirstChar", 0))
f.last_char = int(dict_int(doc, d, "LastChar", -1))
a := dict_array(doc, d, "Widths")
if a == nil {
return
}
// Only 256 codes exist, so nothing past the 256th entry can ever be
// indexed however long the array is.
n := min(array_len(a), FONT_CODES)
if n <= 0 {
return
}
w := make([]f64, n, doc.allocator)
for i in 0 ..< n {
w[i] = font_clamp_width(array_num(doc, a, i, 0))
}
f.widths = w
}
// font_simple_width is the width of one code, in glyph space.
//
// The order is the format's, with one addition. /Widths first; then the
// standard-14 metrics, which are what a font dictionary with no /Widths at
// all is relying on (9.6.2.2 allows omitting them for the fourteen); then
// /MissingWidth, but only when the descriptor actually carries one; and only
// then the embedded program's own advance. The last step is not in the
// format, and is what saves a subset font that shipped no /Widths and no
// descriptor: without it every glyph on the page would advance by zero and
// the line would collapse into one spot.
@(private)
font_simple_width :: proc(f: ^Font, code: u8) -> f64 {
i := int(code) - f.first_char
if i >= 0 && i < len(f.widths) {
return f.widths[i]
}
name := f.enc_names[code]
if f.std_index >= 0 {
if w, ok := std_font_width(f.std_index, name); ok {
return w
}
}
if f.missing_width != 0 {
return f.missing_width
}
if w, ok := font_program_advance(f, f.code_gid[code]); ok {
return w
}
return f.missing_width
}
// font_program_advance is the advance the embedded program declares for a
// glyph, converted into the 1/1000 units every width outside a Type3 font is
// in. ok is false when there is no program, or when it declares nothing.
@(private)
font_program_advance :: proc(f: ^Font, gid: u32) -> (f64, bool) {
if gid > 0xFFFF {
return 0, false
}
g := u16(gid)
switch {
case f.ttf != nil && tt_has_glyf(f.ttf):
upem := tt_units_per_em(f.ttf)
if upem <= 0 || !is_finite(upem) {
return 0, false
}
a := tt_glyph_advance(f.ttf, g) * 1000 / upem
return font_clamp_width(a), a != 0
case f.cff != nil:
// A CFF's advances are in charstring units, which its FontMatrix maps
// to text space; scaling by 1000 puts them back in the thousandths
// every PDF width is written in.
a := cff_glyph_advance(f.cff, g) * cff_font_matrix(f.cff).a * 1000
return font_clamp_width(a), a != 0
case f.t1 != nil:
a := t1_glyph_advance(f.t1, g) * t1_font_matrix(f.t1).a * 1000
return font_clamp_width(a), a != 0
}
return 0, false
}
// ---------------------------------------------------------------------------
// The encoding of a simple font (9.6.5)
//
// Four sources, each overriding the one before it:
//
// 1. the font program's own built-in encoding,
// 2. the standard-14 built-in encoding, when there is no program,
// 3. /BaseEncoding, or a bare name in /Encoding,
// 4. /Differences.
//
// A non-symbolic font that still has nothing after (2) uses StandardEncoding;
// a symbolic one deliberately does not, because inventing Latin names for a
// dingbat font would map every code to a glyph it does not have. A TrueType
// program contributes nothing to (1) at all: its built-in encoding is a cmap
// keyed by character, not by name, and it is consulted in font_tt_gid rather
// than here.
// ---------------------------------------------------------------------------
@(private)
font_build_encoding :: proc(doc: ^Document, f: ^Font, d: ^Dict) {
base: ^[256]string
if f.t1 != nil {
base = t1_builtin_encoding(f.t1)
} else if f.cff != nil {
base = cff_builtin_encoding(f.cff)
}
if base == nil && f.std_index >= 0 {
base = std_font_builtin_encoding(f.std_index)
}
enc := dict_get(doc, d, "Encoding")
named: ^[256]string
diffs: ^Array
#partial switch v in enc {
case Name:
named = encoding_by_name(string(v))
case ^Dict:
if bn := dict_name(doc, v, "BaseEncoding"); bn != "" {
named = encoding_by_name(bn)
}
diffs = dict_array(doc, v, "Differences")
}
if named != nil {
base = named
}
if base == nil && !f.symbolic {
base = encoding_standard()
}
if base != nil {
f.enc_names = base^
}
if diffs == nil {
return
}
// 9.6.5.1: a /Differences array is numbers and names interleaved. A
// number sets the code the next name applies to; each name advances it.
code := 0
n := min(array_len(diffs), MAX_DIFFERENCES)
for i in 0 ..< n {
#partial switch v in array_get(doc, diffs, i) {
case i64:
code = int(v)
case f64:
// A real where an integer belongs is malformed, and is how at
// least one generator writes "1.0".
if is_finite(v) && v >= -1e6 && v <= 1e6 {
code = int(v)
}
case Name:
if code >= 0 && code < FONT_CODES {
f.enc_names[code] = string(v)
}
code += 1
}
}
}
// font_simple_gid resolves one character code to a glyph index.
@(private)
font_simple_gid :: proc(f: ^Font, code: u8) -> u32 {
if f.kind == .Type3 {
// A Type3 glyph is found by name in /CharProcs; there is no program
// to index. The code stands in for a glyph id so that the rest of the
// pipeline has something to carry.
return u32(code)
}
name := f.enc_names[code]
// A TrueType-shaped program is character-keyed and has its own rules.
if f.ttf != nil {
if g, ok := font_tt_gid(f, code, name); ok {
return g
}
}
if name != "" {
if f.cff != nil {
if g, ok := cff_gid_for_name(f.cff, name); ok {
return u32(g)
}
}
if f.t1 != nil {
if g, ok := t1_gid_for_name(f.t1, name); ok {
return u32(g)
}
}
// The same character often has two names -- "nbspace" and "space",
// "uni0041" and "A" -- and an encoding and a charset that disagree
// about which to use would otherwise lose the glyph entirely.
if r := glyph_name_to_rune(name); r >= 0 {
if alt := rune_to_glyph_name(r); alt != "" && alt != name {
if f.cff != nil {
if g, ok := cff_gid_for_name(f.cff, alt); ok {
return u32(g)
}
}
if f.t1 != nil {
if g, ok := t1_gid_for_name(f.t1, alt); ok {
return u32(g)
}
}
}
}
}
// A CFF's own Encoding maps codes to glyphs directly, which reaches
// glyphs its charset does not name -- the shape a symbolic Type1C subset
// usually has.
if f.cff != nil {
if g, ok := cff_gid_for_code(f.cff, code); ok {
return u32(g)
}
// A CID-keyed CFF embedded as a SIMPLE font's program has neither
// names nor an encoding (CFF 12), so both routes above are dead ends
// and every glyph would be lost. Producers do emit this -- a
// CIDFontType0C reused under a /Type1 dictionary -- and reading the
// code as a CID through the charset is what recovers it.
if f.cff.is_cid {
if g, ok := cff_gid_for_cid(f.cff, u16(code)); ok {
return u32(g)
}
}
}
return 0
}
// font_tt_gid is the TrueType half of a simple font's code -> glyph mapping.
// It is the most error-prone rule in PDF text, so it is written out in full.
//
// ISO 32000-2, 9.6.5.4:
//
// A SYMBOLIC font's /Encoding is ignored. The code is looked up in the
// font's (3,0) Microsoft Symbol cmap, whose keys are conventionally 0xF000
// plus the byte -- but only conventionally, so the bare code is tried too
// (tt_gid_for_symbol does both). Failing that, the (1,0) Macintosh Roman
// cmap is tried with the raw code. This is the rule that makes Wingdings,
// Symbol and every icon-font subset come out right, and applying the
// /Encoding to one of them instead maps every code to a glyph the font does
// not have: the page comes out blank or, worse, full of the wrong glyphs.
//
// A NON-SYMBOLIC font goes the long way round: the code selects a glyph
// NAME through the encoding, and the name is then resolved by three routes
// in turn. Through the AGL to a Unicode value and that value through the
// (3,1) cmap; through the `post` table, which names this font's own glyphs
// and so answers a subsetter's "g42" that the AGL never will; and through
// MacRomanEncoding to a Mac Roman CODE and that code through the (1,0)
// cmap, which is the only route a font carrying nothing but a (1,0) table
// has. All three are indirections, and all three are needed: the cmaps are
// keyed by character or by Mac code, and the /Differences are written in
// names.
//
// Two departures from the letter of that, both of which every viewer makes:
//
// * a code that the symbolic route does not resolve still tries the name
// route, and the other way about. A font's flags are frequently wrong,
// and a fallback that only fires when the primary route found nothing
// cannot make a correct font worse.
// * a font with NO usable cmap at all -- which a CIDFontType2 subset
// re-used as a simple font, and a stripped TrueType, both are -- treats
// the code as a glyph index. It is the only remaining interpretation, and
// it is what the producers of those files expect.
@(private)
font_tt_gid :: proc(f: ^Font, code: u8, name: string) -> (u32, bool) {
tt := f.ttf
if f.symbolic {
if g, ok := tt_gid_for_symbol(tt, code); ok {
return u32(g), true
}
if g, ok := tt_gid_for_mac_code(tt, code); ok {
return u32(g), true
}
}
if name != "" {
if r := glyph_name_to_rune(name); r >= 0 {
if g, ok := tt_gid_for_unicode(tt, r); ok {
return u32(g), true
}
}
if g, ok := tt_gid_for_name(tt, name); ok {
return u32(g), true
}
// The middle route of 9.6.5.4: the name's position in
// MacRomanEncoding is the key the (1,0) cmap is written in. It is the
// only route a Mac-authored or subsetted font -- one cmap, (1,0), and
// a format 3 `post` with no names in it -- has above ASCII, and
// without it such a code falls through to the raw-code fallback
// below, which over 0x80-0xFF returns a real but different glyph: the
// page renders and is silently wrong.
//
// The clause lists this before `post`; it is tried after because
// `post` names a glyph in THIS font directly while this is an
// indirection through a legacy encoding, so when the two disagree the
// font's own names are the better answer. The order only matters when
// both can answer, and this route exists for the case where `post`
// cannot.
if mac_code, ok := encoding_macroman_code(name); ok {
if g, gok := tt_gid_for_mac_code(tt, mac_code); gok {
return u32(g), true
}
}
}
if !f.symbolic {
if g, ok := tt_gid_for_symbol(tt, code); ok {
return u32(g), true
}
if g, ok := tt_gid_for_mac_code(tt, code); ok {
return u32(g), true
}
}
// The code read as a character. Over ASCII every Latin text encoding
// agrees with Unicode, so this is what a font whose /Encoding was omitted
// means -- and it is also the only route left for the very common subset
// that sets the Symbolic flag but ships a (3,1) cmap and nothing else.
if g, ok := tt_gid_for_unicode(tt, rune(code)); ok {
return u32(g), true
}
if len(tt.cmap_unicode) == 0 && len(tt.cmap_symbol) == 0 && len(tt.cmap_mac) == 0 {
if int(code) < tt_glyph_count(tt) {
return u32(code), true
}
}
return 0, false
}
// ---------------------------------------------------------------------------
// Type3 (9.6.4)
// ---------------------------------------------------------------------------
// font_load_type3 records where a Type3 font's glyphs are. It does not run
// them: a Type3 glyph is a content stream, and drawing one re-enters the
// interpreter with this font's /Resources in scope.
@(private)
font_load_type3 :: proc(doc: ^Document, f: ^Font, d: ^Dict) {
f.char_procs = dict_dict(doc, d, "CharProcs")
f.t3_resources = dict_dict(doc, d, "Resources")
// 9.6.4: /FontMatrix is required and is emphatically not 1/1000 -- it is
// how a Type3 font declares its own glyph space, and a font drawn in
// 1-unit coordinates has [0.001 0 0 0.001 0 0] nowhere in it. A matrix
// that is missing, unreadable or singular would make every glyph
// infinite or invisible, so it degrades to the usual thousandths.
f.mtx = Matrix{0.001, 0, 0, 0.001, 0, 0}
if a := dict_array(doc, d, "FontMatrix"); array_len(a) >= 6 {
m := Matrix{
a = array_num(doc, a, 0, 0), b = array_num(doc, a, 1, 0),
c = array_num(doc, a, 2, 0), d = array_num(doc, a, 3, 0),
e = array_num(doc, a, 4, 0), f = array_num(doc, a, 5, 0),
}
if matrix_is_finite(m) && matrix_determinant(m) != 0 {
f.mtx = m
} else {
add_repair(doc, .Bad_Font_Object, 0)
}
}
font_read_descriptor(doc, f, dict_dict(doc, d, "FontDescriptor"))
font_build_encoding(doc, f, d)
font_read_widths(doc, f, d)
for code in 0 ..< FONT_CODES {
f.code_gid[code] = u32(code)
// A Type3 width is in the font's own glyph space, so there is no
// standard-14 metric and no program advance to fall back on: a code
// with no /Widths entry is genuinely zero wide.
i := code - f.first_char
f.code_width[code] = i >= 0 && i < len(f.widths) ? f.widths[i] : f.missing_width
}
}
// font_type3_proc is the content stream that draws one code, or nil.
//
// Not part of the five procedures the contract names; the interpreter needs
// it, and it is a lookup that belongs to the encoding rather than to the
// content stream.
font_type3_proc :: proc(f: ^Font, code: u32) -> ^Stream {
if f == nil || f.kind != .Type3 || f.char_procs == nil || code >= FONT_CODES {
return nil
}
name := f.enc_names[code]
if name == "" {
return nil
}
return dict_stream(f.doc, f.char_procs, name)
}
// ---------------------------------------------------------------------------
// Composite fonts (9.7)
// ---------------------------------------------------------------------------
@(private)
font_load_type0 :: proc(doc: ^Document, f: ^Font, d: ^Dict) {
// The encoding CMap turns bytes into CIDs. A Type0 font without one is
// undrawable, so Identity-H stands in: two-byte codes taken as CIDs is
// what the overwhelming majority of real Type0 fonts use anyway.
if enc := dict_get(doc, d, "Encoding"); enc != nil {
cm, err := cmap_load(doc, enc)
if err == .None && cm != nil {
f.cmap = cm
}
}
if f.cmap == nil {
add_repair(doc, .Bad_Font_Object, 0)
f.cmap = cmap_identity(false, doc.allocator)
}
f.vertical = f.cmap != nil && f.cmap.wmode == 1
cid := font_descendant(doc, d)
if cid == nil {
// No descendant: nothing maps a CID to a glyph and nothing gives a
// width. The font still decodes text, which is what /ToUnicode needs.
add_repair(doc, .Bad_Font_Object, 0)
return
}
f.descendant = cid
desc := dict_dict(doc, cid, "FontDescriptor")
font_read_descriptor(doc, f, desc)
font_load_program(doc, f, desc)
font_set_matrix(f)
f.default_width = font_clamp_width(dict_num(doc, cid, "DW", 1000))
if a := dict_array(doc, cid, "W"); a != nil {
font_parse_w(doc, f, a)
}
if f.vertical {
if a := dict_array(doc, cid, "DW2"); array_len(a) >= 2 {
vy := array_num(doc, a, 0, 880)
w1 := array_num(doc, a, 1, -1000)
if is_finite(vy) && is_finite(w1) {
f.default_vy = font_clamp_width(vy)
f.default_w1 = font_clamp_width(w1)
}
}
if a := dict_array(doc, cid, "W2"); a != nil {
font_parse_w2(doc, f, a)
}
}
font_load_cid_to_gid(doc, f, cid)
}
// font_descendant is /DescendantFonts[0]. The format requires a one-element
// array; a producer that writes the dictionary directly means the same thing
// and is common enough to accept.
@(private)
font_descendant :: proc(doc: ^Document, d: ^Dict) -> ^Dict {
o := dict_get(doc, d, "DescendantFonts")
#partial switch v in o {
case ^Dict:
return v
case ^Array:
return array_dict(doc, v, 0)
}
return nil
}
// font_load_cid_to_gid reads /CIDToGIDMap: /Identity, or a stream of
// big-endian u16 glyph indices indexed by CID (9.7.4.2).
@(private)
font_load_cid_to_gid :: proc(doc: ^Document, f: ^Font, cid: ^Dict) {
o := dict_get(doc, cid, "CIDToGIDMap")
stm, is_stream := o.(^Stream)
if !is_stream {
return
}
data, err := stream_data(doc, stm)
if err != .None || len(data) < 2 {
add_repair(doc, .Bad_Font_Object, 0, stm.ref)
return
}
n := min(len(data) / 2, MAX_CID_TO_GID)
m := make([]u16, n, doc.allocator)
for i in 0 ..< n {
m[i] = u16(data[2 * i]) << 8 | u16(data[2 * i + 1])
}
f.cid_to_gid = m
}
// font_cid_gid maps a CID to a glyph index.
@(private)
font_cid_gid :: proc(f: ^Font, cid: u32) -> u32 {
if len(f.cid_to_gid) > 0 {
// A /CIDToGIDMap that stops short is not saying "identity from here
// on"; it is saying those CIDs have no glyph. 9.7.4.2.
if int(cid) < len(f.cid_to_gid) {
return u32(f.cid_to_gid[cid])
}
return 0
}
if f.cff != nil {
// A CID-keyed CFF's charset is the CID -> GID map; a name-keyed one
// reached through Identity-H has the CID as its glyph index, which is
// what cff_gid_for_cid answers for it.
if cid > 0xFFFF {
return 0
}
if g, ok := cff_gid_for_cid(f.cff, u16(cid)); ok {
return u32(g)
}
return 0
}
return cid
}
// font_parse_w reads the /W array (9.7.4.3), which has two forms that alternate
// freely inside one array:
//
// c [w1 w2 ... wn] CIDs c through c+n-1
// c_first c_last w every CID in the range
@(private)
font_parse_w :: proc(doc: ^Document, f: ^Font, a: ^Array) {
out := make([dynamic]Cid_Width, 0, 16, doc.allocator)
i := 0
n := array_len(a)
fuel := MAX_W_ITEMS
for i < n && fuel > 0 && len(out) < MAX_CID_WIDTHS {
fuel -= 1
first, ok := font_cid_operand(doc, a, i)
if !ok {
// Not a number where one belongs. Stepping over one item rather
// than abandoning the array keeps the widths after the damage.
i += 1
continue
}
i += 1
if i >= n {
break
}
#partial switch v in array_get(doc, a, i) {
case ^Array:
i += 1
m := array_len(v)
for k in 0 ..< m {
if len(out) >= MAX_CID_WIDTHS {
break
}
fuel -= 1
if fuel <= 0 {
break
}
c := u64(first) + u64(k)
if c > 0xFFFF {
break
}
font_push_width(&out, u32(c), u32(c), font_clamp_width(array_num(doc, v, k, 0)))
}
case:
last, ok2 := font_cid_operand(doc, a, i)
i += 1
if i >= n || !ok2 {
break
}
w := font_clamp_width(array_num(doc, a, i, 0))
i += 1
lo, hi := first, last
if hi < lo {
// A backwards range is meaningless as written; read either
// way round it names the same glyphs.
lo, hi = hi, lo
}
font_push_width(&out, lo, hi, w)
}
}
if len(out) == 0 {
return
}
f.cid_widths = font_flatten(out[:], doc.allocator)
}
// font_push_width appends a run, merging it into the previous one when they
// are adjacent and equally wide. The `c [w w w ...]` form is one entry per
// CID as written, and a CJK font writes tens of thousands of them; merging
// turns the common runs of equal widths into single entries and costs one
// comparison.
@(private)
font_push_width :: proc(out: ^[dynamic]Cid_Width, lo, hi: u32, w: f64) {
if len(out) > 0 {
last := &out[len(out) - 1]
if last.width == w && last.hi + 1 == lo && last.hi < hi {
last.hi = hi
return
}
}
append(out, Cid_Width{lo = lo, hi = hi, width = w})
}
// font_parse_w2 reads /W2, the vertical counterpart of /W. Its entries are
// triples rather than single numbers:
//
// c [w1y v1x v1y w1y v1x v1y ...]
// c_first c_last w1y v1x v1y
@(private)
font_parse_w2 :: proc(doc: ^Document, f: ^Font, a: ^Array) {
out := make([dynamic]Cid_Vmetric, 0, 8, doc.allocator)
i := 0
n := array_len(a)
fuel := MAX_W_ITEMS
for i < n && fuel > 0 && len(out) < MAX_CID_WIDTHS {
fuel -= 1
first, ok := font_cid_operand(doc, a, i)
if !ok {
i += 1
continue
}
i += 1
if i >= n {
break
}
#partial switch v in array_get(doc, a, i) {
case ^Array:
i += 1
m := array_len(v) / 3
for k in 0 ..< m {
if len(out) >= MAX_CID_WIDTHS {
break
}
fuel -= 1
if fuel <= 0 {
break
}
c := u64(first) + u64(k)
if c > 0xFFFF {
break
}
append(&out, Cid_Vmetric{
lo = u32(c), hi = u32(c),
w1y = font_clamp_width(array_num(doc, v, 3 * k, 0)),
v1x = font_clamp_width(array_num(doc, v, 3 * k + 1, 0)),
v1y = font_clamp_width(array_num(doc, v, 3 * k + 2, 0)),
})
}
case:
last, ok2 := font_cid_operand(doc, a, i)
i += 1
if !ok2 || i + 2 >= n {
break
}
lo, hi := first, last
if hi < lo {
lo, hi = hi, lo
}
append(&out, Cid_Vmetric{
lo = lo, hi = hi,
w1y = font_clamp_width(array_num(doc, a, i, 0)),
v1x = font_clamp_width(array_num(doc, a, i + 1, 0)),
v1y = font_clamp_width(array_num(doc, a, i + 2, 0)),
})
i += 3
}
}
if len(out) == 0 {
return
}
f.cid_vmetrics = font_flatten(out[:], doc.allocator)
}
// font_cid_operand reads array element `i` as a CID. Anything that is not a
// number in range fails rather than being coerced: a /W array that has gone
// out of step must not manufacture a range covering the whole CID space.
@(private)
font_cid_operand :: proc(doc: ^Document, a: ^Array, i: int) -> (u32, bool) {
v, ok := to_number(array_get(doc, a, i))
if !ok || !is_finite(v) || v < 0 || v > 0xFFFF {
return 0, false
}
return u32(v), true
}
// font_flatten turns a set of CID runs into an equivalent set that is sorted
// and disjoint, so that a lookup is a binary search and nothing else.
//
// /W and /W2 are meant to name disjoint ranges, and real files overlap them:
// the shape it always takes is a broad run with narrower corrections written
// over parts of it. The rule applied here is that where two runs share a CID,
// the one that STARTS LATER owns it -- the correction is always the later
// start -- and the earlier run keeps what is left, which may be a piece on
// each side of the correction.
//
// The sweep below is a stack of the runs covering the CID it has reached, most
// recently started on top. That top is the run that owns the CIDs from here
// until either it ends or a new run starts, which is exactly one output entry;
// runs that ended while they were shadowed are dropped from the stack when
// they surface. Two runs that start at the same CID are a contradiction the
// file has to answer for, and which of them wins is not defined.
//
// Doing this once, at load, is what lets the lookup be exact. Resolving the
// overlap at lookup time instead means scanning backwards from where the
// binary search lands, and the distance is unbounded: a file that writes one
// run over the whole CID space and then a hundred corrections inside it puts
// the answer a hundred entries back. Either that scan is bounded and sometimes
// wrong, or it is unbounded and sometimes very slow. This is neither.
//
// The result holds at most twice as many runs as it was given: every entry
// emitted ends either at some run's `hi` or at some run's `lo`, and there are
// only so many of each.
@(private)
font_flatten :: proc(src: []$T, allocator: mem.Allocator) -> []T {
if len(src) <= 1 {
return src
}
slice.sort_by(src, proc(x, y: T) -> bool {
return x.lo < y.lo
})
out := make([dynamic]T, 0, len(src) + 1, allocator)
// Scratch, and freed here: the depth is the file's to choose and the
// Document's arena never gives anything back.
stack := make([dynamic]T, 0, 16, context.allocator)
defer delete(stack)
cur: u32 = 0
for e in src {
if e.hi < e.lo {
continue
}
if e.lo > cur {
font_flatten_emit(&out, &stack, &cur, e.lo - 1)
}
append(&stack, e)
}
// Each pass either pops a run or emits one, and the counts of both are
// bounded by the input, so the drain has fuel rather than a proof that a
// `hi` of 0xFFFFFFFF cannot wrap `cur` back to zero.
for fuel := 2 * len(src) + 2; fuel > 0 && len(stack) > 0; fuel -= 1 {
top := stack[len(stack) - 1]
if top.hi < cur {
pop(&stack)
continue
}
font_flatten_emit(&out, &stack, &cur, top.hi)
}
return out[:]
}
// font_flatten_emit writes out the CIDs from `cur` through `limit`, taking
// each one's value from the innermost run still covering it, and leaves `cur`
// just past `limit`. CIDs no run covers produce no entry at all, which is what
// makes the caller fall back to /DW.
//
// Every iteration either pops the stack or reaches `limit`, so the loop is
// bounded by the stack's depth without needing a counter to say so.
@(private)
font_flatten_emit :: proc(out, stack: ^[dynamic]$T, cur: ^u32, limit: u32) {
for cur^ <= limit {
if len(stack) > 0 && stack[len(stack) - 1].hi < cur^ {
pop(stack)
continue
}
if len(stack) == 0 {
break
}
e := stack[len(stack) - 1]
e.lo = cur^
e.hi = min(e.hi, limit)
append(out, e)
cur^ = e.hi + 1
}
cur^ = max(cur^, limit + 1)
}
// font_find_range is a binary search over disjoint, sorted runs.
@(private)
font_find_range :: proc(a: []$T, cid: u32) -> (int, bool) {
lo, hi := 0, len(a)
for lo < hi {
mid := lo + (hi - lo) / 2
if a[mid].lo <= cid {
lo = mid + 1
} else {
hi = mid
}
}
if lo > 0 && cid <= a[lo - 1].hi {
return lo - 1, true
}
return 0, false
}
// font_cid_width is the horizontal advance of one CID, in glyph space.
@(private)
font_cid_width :: proc(f: ^Font, cid: u32) -> f64 {
if i, ok := font_find_range(f.cid_widths, cid); ok {
return f.cid_widths[i].width
}
return f.default_width
}
// font_cid_vmetric is the vertical displacement and position vector of one
// CID. `w0` is the horizontal width, which the default position vector needs.
@(private)
font_cid_vmetric :: proc(f: ^Font, cid: u32, w0: f64) -> (w1y, vx, vy: f64) {
if i, ok := font_find_range(f.cid_vmetrics, cid); ok {
e := f.cid_vmetrics[i]
return e.w1y, e.v1x, e.v1y
}
// 9.7.4.3: with no /W2 entry the position vector is (w0/2, DW2[0]) and
// the displacement is (0, DW2[1]).
return f.default_w1, w0 / 2, f.default_vy
}
// ---------------------------------------------------------------------------
// Embedded font programs (9.9)
// ---------------------------------------------------------------------------
// font_load_program reads /FontFile, /FontFile2 or /FontFile3 out of the
// descriptor.
//
// A program that will not parse is recorded and dropped: `program_failed` is
// set, no error is returned, and the font goes on to build its widths and its
// encoding as if it had never had a program. Text that cannot be drawn must
// still lay out and extract, and a subsetter that truncated its own font file
// is not a reason to lose the page.
@(private)
font_load_program :: proc(doc: ^Document, f: ^Font, desc: ^Dict) {
if desc == nil {
return
}
kind := Font_Kind.Type1
stm := dict_stream(doc, desc, "FontFile")
if stm == nil {
if s2 := dict_stream(doc, desc, "FontFile2"); s2 != nil {
stm = s2
kind = .TrueType
}
}
sub := ""
if stm == nil {
if s3 := dict_stream(doc, desc, "FontFile3"); s3 != nil {
stm = s3
sub = dict_name(doc, s3.dict, "Subtype")
// /OpenType is an sfnt wrapper whose outlines may be either kind;
// the sniff below settles it from the bytes.
kind = sub == "OpenType" ? .TrueType : .Type1
}
}
if stm == nil {
return
}
data, err := stream_data(doc, stm)
if err != .None || len(data) == 0 {
f.program_failed = true
add_repair(doc, .Bad_Font_Object, 0, stm.ref)
return
}
if font_parse_program(doc, f, data, kind, sub) {
f.embedded = true
return
}
f.program_failed = true
add_repair(doc, .Bad_Font_Object, 0, stm.ref)
}
// font_parse_program turns the bytes of an embedded program into a reader.
//
// The declared subtype is a hint, not a fact. /FontFile3 /Type1C files that
// are really OpenType, /FontFile2 entries holding a bare CFF and /FontFile
// entries holding a PFB are all in circulation, and every one of them is
// unambiguous from its first four bytes -- so the magic decides and the
// dictionary only breaks ties.
@(private)
font_parse_program :: proc(doc: ^Document, f: ^Font, data: []byte, kind: Font_Kind, sub: string) -> bool {
switch {
case font_is_sfnt(data):
ttf, err := tt_parse(data, doc.allocator)
if err != .None || ttf == nil {
return false
}
f.ttf = ttf
if tt_has_glyf(ttf) {
return true
}
// An OpenType font with no `glyf` keeps its outlines in a `CFF `
// table. The sfnt wrapper still carries the cmap and hmtx, so both
// readers stay live: the TrueType one answers character lookups and
// the CFF one draws.
cff_data := tt_cff_data(ttf)
if len(cff_data) == 0 {
return false
}
cff, cerr := cff_parse(cff_data, doc.allocator)
if cerr != .None || cff == nil {
return false
}
f.cff = cff
return true
case font_is_cff(data):
cff, err := cff_parse(data, doc.allocator)
if err != .None || cff == nil {
return false
}
f.cff = cff
return true
case font_is_type1(data):
t1, err := t1_parse(data, doc.allocator)
if err != .None || t1 == nil {
return false
}
f.t1 = t1
return true
}
// No magic matched. Believe the dictionary, which is all that is left.
if kind == .TrueType || sub == "OpenType" {
ttf, err := tt_parse(data, doc.allocator)
if err == .None && ttf != nil {
f.ttf = ttf
return true
}
return false
}
if sub == "Type1C" || sub == "CIDFontType0C" {
cff, err := cff_parse(data, doc.allocator)
if err == .None && cff != nil {
f.cff = cff
return true
}
return false
}
t1, err := t1_parse(data, doc.allocator)
if err == .None && t1 != nil {
f.t1 = t1
return true
}
return false
}
@(private)
font_is_sfnt :: proc(data: []byte) -> bool {
if len(data) < 4 {
return false
}
tag := u32(data[0]) << 24 | u32(data[1]) << 16 | u32(data[2]) << 8 | u32(data[3])
return tag == TT_TAG_V1 || tag == TT_TAG_V2 || tag == TT_TAG_TRUE ||
tag == TT_TAG_OTTO || tag == TT_TAG_TTCF
}
// font_is_cff matches a bare CFF header: major version 1, minor 0, and a
// header size that is at least the four bytes of the header itself. The
// offSize byte that follows must be 1..4. CFF 6.
@(private)
font_is_cff :: proc(data: []byte) -> bool {
return len(data) >= 4 && data[0] == 1 && data[1] == 0 &&
data[2] >= 4 && data[3] >= 1 && data[3] <= 4
}
// font_is_type1 matches the two containers a Type1 program arrives in: a PFB
// segment header (0x80 then a segment type) or the "%!" of a PFA/PostScript
// clear-text header.
@(private)
font_is_type1 :: proc(data: []byte) -> bool {
if len(data) < 2 {
return false
}
if data[0] == 0x80 && data[1] >= 1 && data[1] <= 3 {
return true
}
return data[0] == '%' && data[1] == '!'
}
// font_set_matrix fixes the glyph space the program's outlines are in.
@(private)
font_set_matrix :: proc(f: ^Font) {
switch {
// A CFF carries its own matrix and it wins wherever there is one: an
// OpenType wrapper leaves both readers live, and it is the CFF that draws.
case f.cff != nil:
m := cff_font_matrix(f.cff)
f.mtx = matrix_is_finite(m) && matrix_determinant(m) != 0 ? m : Matrix{0.001, 0, 0, 0.001, 0, 0}
case f.ttf != nil:
upem := tt_units_per_em(f.ttf)
if upem <= 0 || !is_finite(upem) {
upem = 1000
}
f.mtx = Matrix{1 / upem, 0, 0, 1 / upem, 0, 0}
case f.t1 != nil:
m := t1_font_matrix(f.t1)
f.mtx = matrix_is_finite(m) && matrix_determinant(m) != 0 ? m : Matrix{0.001, 0, 0, 0.001, 0, 0}
case:
f.mtx = Matrix{0.001, 0, 0, 0.001, 0, 0}
}
}
// ---------------------------------------------------------------------------
// Decoding a string
// ---------------------------------------------------------------------------
// font_decode splits a PDF string into character codes and fills in
// everything the interpreter and a text device need about each one.
//
// `out` is CLEARED first, so it may be a scratch buffer reused across every
// string on the page. One byte is one code for a simple font; a composite
// font's codes are as wide as its CMap's codespace ranges say, which is not
// necessarily two and not necessarily constant within one string.
font_decode :: proc(f: ^Font, s: []byte, out: ^[dynamic]Font_Code) {
if out == nil {
return
}
clear(out)
if f == nil || len(s) == 0 {
return
}
i := 0
for i < len(s) {
fc: Font_Code
if f.kind == .Type0 {
code, n := cmap_next_code(f.cmap, s[i:])
// cmap_next_code consumes at least one byte for a non-empty
// string, but the loop's termination must not depend on a
// promise made in another file.
fc.nbytes = max(n, 1)
fc.code = code
if cid, ok := cmap_lookup(f.cmap, code); ok {
fc.cid = cid
}
fc.gid = font_cid_gid(f, fc.cid)
w0 := font_cid_width(f, fc.cid)
fc.width = w0
if f.vertical {
w1y, vx, vy := font_cid_vmetric(f, fc.cid, w0)
fc.width = w1y
fc.vx, fc.vy = vx, vy
}
} else {
b := s[i]
fc.code = u32(b)
fc.cid = u32(b)
fc.nbytes = 1
fc.gid = f.code_gid[b]
fc.width = f.code_width[b]
}
// 9.3.3: word spacing applies to a single-byte code 32 and to nothing
// else. A two-byte 0x0020 in a Type0 font is a perfectly ordinary
// CID, and taking it for a space stretches CJK text at random.
fc.is_space = fc.nbytes == 1 && fc.code == 32
fc.unicode = font_code_unicode(f, fc)
append(out, fc)
i += fc.nbytes
}
}
// font_code_unicode is the character a code stands for, or -1.
//
// /ToUnicode is authoritative when it is there: it is the producer telling us
// what it meant, and it is the only route that works for a subset font whose
// glyph names are "g17". Failing that a simple font's glyph name is turned
// into a code point through the AGL, which covers every font that uses real
// names. The `post` table is the same trick for a symbolic TrueType that has
// no PDF encoding at all.
@(private)
font_code_unicode :: proc(f: ^Font, fc: Font_Code) -> rune {
if f.to_unicode != nil {
if txt, ok := cmap_lookup_text(f.to_unicode, fc.code); ok && len(txt) > 0 {
r, _ := utf8.decode_rune_in_string(txt)
if r != utf8.RUNE_ERROR {
return r
}
}
}
if f.kind == .Type0 {
// A Type0 font may wrap a name-keyed CFF, in which case the glyph
// still has a name even though the code never went through one.
if f.cff != nil && fc.gid <= 0xFFFF {
if name := cff_glyph_name(f.cff, u16(fc.gid)); name != "" {
if r := glyph_name_to_rune(name); r >= 0 {
return r
}
}
}
} else if fc.code < FONT_CODES {
if name := f.enc_names[fc.code]; name != "" {
if r := glyph_name_to_rune(name); r >= 0 {
return r
}
}
}
if f.ttf != nil && fc.gid > 0 && int(fc.gid) < len(f.ttf.names) {
if r := glyph_name_to_rune(f.ttf.names[fc.gid]); r >= 0 {
return r
}
}
// Last resort, and only for a non-symbolic single-byte code: over ASCII
// every Latin text encoding agrees with Unicode, so a font that offered
// no other answer still extracts as readable text. A symbolic font is
// excluded because its code 0x41 is a dingbat, not an "A".
if !f.symbolic && fc.nbytes == 1 && fc.code >= 0x20 && fc.code < 0x7F {
return rune(fc.code)
}
return -1
}
// font_clamp_width keeps a width from the file finite and within an absurd
// but harmless range. A NaN here reaches the text matrix and from there the
// geometry, which is not allowed to see one.
@(private)
font_clamp_width :: proc(w: f64) -> f64 {
if !is_finite(w) {
return 0
}
return clamp(w, -MAX_GLYPH_WIDTH, MAX_GLYPH_WIDTH)
}