package pdf import "core:mem" import "core:slice" // Type 1 font programs (Adobe, "Adobe Type 1 Font Format", 1990). // // Bytes in, outlines out. Nothing here knows what a PDF is; a Type 1 program // is the same file whether it arrived as a /FontFile stream, a .pfb on disk or // a .pfa pasted into a PostScript job, and keeping this file ignorant of the // difference is what makes it testable from byte literals. // // SHAPE OF THE FILE // // %!FontType1-1.0 ... cleartext PostScript: /FontMatrix, /Encoding // currentfile eexec // the Private dict: /lenIV, /Subrs, /CharStrings // 512 zeros, cleartomark trailer // // The encrypted portion is enciphered with the "eexec" stream cipher keyed at // r = 55665, whose first four plaintext bytes are random padding. It arrives // either as raw binary (.pfb, and PDF /FontFile streams) or as hex ASCII // (.pfa); both are accepted, told apart by whether the first four bytes are hex // digits -- which works because Adobe's own encryptor is required to choose // padding that keeps the two forms distinguishable. Each charstring inside is // enciphered again with the same cipher keyed at r = 4330, with /lenIV bytes // (4 unless the font says otherwise) of padding to drop. // // A .pfb wraps the three parts in segment headers -- 0x80, a type byte, then a // little-endian length -- which are stripped and the payloads concatenated // before anything else looks at the bytes. // // WHAT SUBSETTERS DO TO THESE FILES // // Font programs are the most reliably damaged data in a PDF, because every // producer rewrites them. So: a /Subrs or /CharStrings count that disagrees // with the entries that follow is ignored in favour of the entries; a // charstring shorter than /lenIV yields an empty glyph rather than an error; an // eexec block that stops in the middle yields the glyphs decoded up to that // point; and a font with no eexec section at all -- some are shipped with that // layer already stripped -- is read as though its Private dict were the // cleartext it is. // --------------------------------------------------------------------------- // Limits. Every one of these bounds something an attacker controls. // --------------------------------------------------------------------------- // Tokens read from one cleartext or Private section. These are data, not // programs, so the cap only ever fires on garbage -- which is exactly what the // tail of a truncated eexec block decrypts to. @(private) MAX_T1_TOKENS :: 1 << 20 // Glyph indices are u16 everywhere in this layer, so the dictionary stops one // short of where the index would stop being representable. @(private) MAX_T1_GLYPHS :: 0xFFFF @(private) MAX_T1_SUBRS :: 1 << 16 // The charstring interpreter's bounds. The format allows 24 operands and a // call depth of 10; the margin absorbs the fonts that exceed the spec without // letting a hostile one run away. @(private) T1_STACK_MAX :: 48 @(private) T1_PS_MAX :: 32 @(private) T1_MAX_CALL_DEPTH :: 30 // Charstring bytes executed for one glyph, subroutine calls included. @(private) T1_MAX_FUEL :: 1 << 17 // Points a FLEX collects: a reference point plus the six that make two curves. @(private) T1_FLEX_POINTS :: 7 // The two cipher keys, and the constants they share (Type 1 spec, 7.3). @(private) T1_EEXEC_KEY :: 55665 @(private) T1_CHARSTRING_KEY :: 4330 // --------------------------------------------------------------------------- // The parsed font // --------------------------------------------------------------------------- // T1_Name_Entry is one glyph name and the glyph index it belongs to, in a table // sorted by name so that a lookup is a binary search. A simple Type 1 font is // addressed entirely by name, so this is the hot path. @(private) T1_Name_Entry :: struct { name: string, gid: u16, } // Type1_Font is the parsed form of a font program. Declared here so that Font can // reference it; the reader that fills it in owns this file and adds the fields // it needs. Nothing in this file may depend on Document or on any PDF type -- // a font program is bytes in and outlines out, and stays separately testable. Type1_Font :: struct { // The whole program with any PFB segment headers removed. When the input // was not PFB-wrapped this aliases the caller's bytes rather than copying // them, so `data` passed to `t1_parse` must outlive the font: the built-in // encoding's glyph names point into it. data: []byte, // The decrypted Private dictionary, always a buffer of this font's own. // Glyph names, charstrings and subroutines point into it. priv: []byte, // /FontName, for diagnostics only. name: string, // /FontMatrix, defaulting to the 1000-units-per-em [0.001 0 0 0.001 0 0]. mtx: Matrix, // The program's built-in encoding: 256 glyph names, "" where the font // leaves a code undefined. Meaningful only when `has_encoding` is set. encoding: [256]string, has_encoding: bool, // /lenIV: bytes of padding at the head of every decrypted charstring. // Negative means the charstrings are not enciphered at all. len_iv: int, subrs: [][]byte, names: []string, chars: [][]byte, // `names` sorted, for t1_gid_for_name. by_name: []T1_Name_Entry, // Advances are memoised: laying out a page asks for the same handful of // glyphs thousands of times, and each answer costs a charstring run. advances: []f64, adv_known: []bool, // Set when the eexec block or a charstring ran off the end of the input. // The font is still usable; it simply holds fewer glyphs than it claims. truncated: bool, } // --------------------------------------------------------------------------- // Parsing // --------------------------------------------------------------------------- // t1_parse reads a Type 1 font program: PFB-wrapped, raw binary or PFA. // // The returned font and everything it points at are allocated with `allocator` // and belong to the caller; nothing is freed individually. `data` is referenced, // not copied, so it must outlive the font. // // An error comes back only when there is genuinely nothing to hand over -- no // charstring could be read at all. A font that is damaged but still holds one // glyph parses successfully, because a page that draws that glyph should draw // it. @(private) t1_parse :: proc(data: []byte, allocator := context.allocator) -> (^Type1_Font, Error) { if len(data) == 0 { return nil, .Bad_Type1 } f := new(Type1_Font, allocator) if f == nil { return nil, .Out_Of_Memory } f.mtx = Matrix{0.001, 0, 0, 0.001, 0, 0} f.len_iv = 4 raw, raw_owned := t1_unwrap_pfb(data, allocator) f.data = raw // The cleartext is everything before `eexec`. For a font that has none, // stop at the first thing that introduces binary data instead, so the // tokeniser never walks over charstring bytes. clear_end := len(raw) eexec_at := t1_find_token(raw, "eexec") if eexec_at >= 0 { clear_end = eexec_at } if p := t1_find_token(raw[:clear_end], "/CharStrings"); p >= 0 { clear_end = p } if p := t1_find_token(raw[:clear_end], "/Subrs"); p >= 0 { clear_end = p } t1_parse_cleartext(f, raw[:clear_end]) // The encrypted region: everything after `eexec` and the whitespace that // follows it. Adobe requires the first byte of the ciphertext not to be // whitespace precisely so that skipping it is unambiguous (Type 1 spec, // 7.2). body := raw encrypted := false if eexec_at >= 0 { i := eexec_at + len("eexec") for i < len(raw) && t1_is_space(raw[i]) { i += 1 } body = raw[i:] encrypted = true } // A font whose eexec layer has already been stripped still says `eexec`. // Twelve plaintext bytes surviving encipherment is not a thing that // happens, so finding the token means the region is not enciphered. if encrypted && t1_find_token(body, "/CharStrings") >= 0 { encrypted = false } // `f.priv` is always the whole allocation, so it can be handed back to the // allocator; `priv_body` is the part after the four bytes of random // padding at the head of the eexec plaintext. priv_body: []byte switch { case encrypted && t1_is_hex_eexec(body): buf := make([]byte, len(body) / 2 + 1, allocator) n := t1_hex_decode(buf, body) t1_decipher(buf[:n], T1_EEXEC_KEY) f.priv = buf priv_body = n >= 4 ? buf[4:n] : buf[n:n] case encrypted: f.priv = t1_copy(body, allocator) t1_decipher(f.priv, T1_EEXEC_KEY) priv_body = len(f.priv) >= 4 ? f.priv[4:] : f.priv[len(f.priv):] case: f.priv = t1_copy(body, allocator) priv_body = f.priv } ctx := T1_Parse_Ctx{ f = f, subrs = make([dynamic][]byte, allocator), names = make([dynamic]string, allocator), chars = make([dynamic][]byte, allocator), } t1_parse_private(&ctx, priv_body) if len(ctx.chars) == 0 { delete(ctx.subrs) delete(ctx.names) delete(ctx.chars) delete(f.priv, allocator) if raw_owned { delete(raw, allocator) } free(f, allocator) return nil, .Bad_Type1 } f.subrs = ctx.subrs[:] f.names = ctx.names[:] f.chars = ctx.chars[:] n := len(f.names) f.by_name = make([]T1_Name_Entry, n, allocator) for i in 0 ..< n { f.by_name[i] = T1_Name_Entry{name = f.names[i], gid = u16(i)} } slice.sort_by(f.by_name, proc(a, b: T1_Name_Entry) -> bool { return a.name < b.name }) f.advances = make([]f64, n, allocator) f.adv_known = make([]bool, n, allocator) return f, .None } // t1_glyph_count is the number of charstrings the font holds. Glyph indices // are positions in the /CharStrings dictionary, in the order it was written. @(private) t1_glyph_count :: proc(f: ^Type1_Font) -> int { if f == nil { return 0 } return len(f.chars) } // t1_font_matrix maps glyph space to text space. Outlines from t1_glyph_path // are in glyph space and have not been through it. @(private) t1_font_matrix :: proc(f: ^Type1_Font) -> Matrix { if f == nil { return Matrix{0.001, 0, 0, 0.001, 0, 0} } return f.mtx } // t1_builtin_encoding is the font program's own encoding, or nil when it // declares none -- which the caller must read as "use StandardEncoding", // not as "this font has no glyphs at those codes". @(private) t1_builtin_encoding :: proc(f: ^Type1_Font) -> ^[256]string { if f == nil || !f.has_encoding { return nil } return &f.encoding } // t1_gid_for_name resolves a glyph name against the /CharStrings dictionary. @(private) t1_gid_for_name :: proc(f: ^Type1_Font, name: string) -> (u16, bool) { if f == nil || len(name) == 0 { return 0, false } lo, hi := 0, len(f.by_name) for lo < hi { mid := lo + (hi - lo) / 2 if f.by_name[mid].name < name { lo = mid + 1 } else { hi = mid } } if lo < len(f.by_name) && f.by_name[lo].name == name { return f.by_name[lo].gid, true } return 0, false } // t1_glyph_advance is the width `hsbw` or `sbw` declares, in glyph space. // Zero for a glyph that declares none, which is what a viewer advances by. @(private) t1_glyph_advance :: proc(f: ^Type1_Font, gid: u16) -> f64 { if f == nil || int(gid) >= len(f.chars) { return 0 } i := int(gid) if i < len(f.adv_known) && f.adv_known[i] { return f.advances[i] } ip := T1_Interp{width_only = true} t1_run(f, i, nil, Point{0, 0}, &ip) w := ip.have_width ? ip.width : 0 if !is_finite(w) { w = 0 } if i < len(f.adv_known) { f.advances[i] = w f.adv_known[i] = true } return w } // t1_glyph_path appends one glyph's outline, in glyph space, to `out`. // // A glyph that cannot be interpreted appends whatever it managed to draw and // still returns .None: a broken glyph is a blank, never a failed page. Only an // index that names no glyph is an error. @(private) t1_glyph_path :: proc(f: ^Type1_Font, gid: u16, out: ^Path) -> Error { if f == nil || out == nil { return .Invalid_Argument } if int(gid) >= len(f.chars) { return .No_Glyph } ip := T1_Interp{allow_seac = true} t1_run(f, int(gid), out, Point{0, 0}, &ip) if !ip.seac { return .None } // seac composes an accented glyph out of two others named by their // StandardEncoding codes -- the one place a font program refers to an // encoding it does not carry. The table is font_standard.odin's, which is // the same table the format's Annex D defines. asb := ip.seac_args[0] adx := ip.seac_args[1] ady := ip.seac_args[2] if !is_finite(asb) || !is_finite(adx) || !is_finite(ady) { return .None } bcode := int(ip.seac_args[3]) acode := int(ip.seac_args[4]) std := encoding_standard() if bcode >= 0 && bcode < 256 { if bgid, ok := t1_gid_for_name(f, std[bcode]); ok { base := T1_Interp{} t1_run(f, int(bgid), out, Point{0, 0}, &base) } } if acode >= 0 && acode < 256 { if agid, ok := t1_gid_for_name(f, std[acode]); ok { // adx/ady position the accent's left sidebearing point relative to // the COMPOSITE's left sidebearing point, not its origin -- so the // composite's own sidebearing, which `ip.sbx` holds, is part of the // offset, and `asb` (the accent's sidebearing, which the accent's // own hsbw is about to apply again) is taken back out. // // Getting this wrong is invisible in the many fonts whose accented // glyphs have a zero sidebearing and glaring in the ones that do // not: dropping `ip.sbx` moves the dieresis off the I by the width // of the sidebearing in 104 of the 448 seac composites in the // Bitstream and URW faces this was checked against. acc := T1_Interp{} t1_run(f, int(agid), out, Point{ip.sbx + adx - asb, ady}, &acc) } } return .None } // --------------------------------------------------------------------------- // Container: PFB segments, eexec, and the two ciphers // --------------------------------------------------------------------------- @(private = "file") t1_copy :: proc(src: []byte, allocator: mem.Allocator) -> []byte { if len(src) == 0 { return nil } out := make([]byte, len(src), allocator) if len(out) != len(src) { return nil } copy(out, src) return out } // t1_unwrap_pfb strips .pfb segment headers and concatenates the payloads. // Returns the input untouched, and owned = false, for anything else. @(private = "file") t1_unwrap_pfb :: proc(data: []byte, allocator: mem.Allocator) -> (out: []byte, owned: bool) { if len(data) < 6 || data[0] != 0x80 { return data, false } // Two passes so the join is exactly sized: a declared length is a file // value and must not be believed as an allocation size. total := 0 for pass in 0 ..< 2 { pos := 0 n := 0 for pos + 6 <= len(data) { if data[pos] != 0x80 { break } kind := data[pos + 1] // 3 is the end-of-file marker; anything else is not a segment. if kind != 1 && kind != 2 { break } seg := int(u32(data[pos + 2]) | u32(data[pos + 3]) << 8 | u32(data[pos + 4]) << 16 | u32(data[pos + 5]) << 24) pos += 6 if seg > len(data) - pos { seg = len(data) - pos } if seg <= 0 { continue } if pass == 0 { total += seg } else { copy(out[n:], data[pos:pos + seg]) n += seg } pos += seg } if pass == 0 { if total == 0 { return data, false } out = make([]byte, total, allocator) if len(out) != total { return data, false } } } return out, true } // t1_decipher runs the eexec cipher over `buf` in place (Type 1 spec, 7.3). @(private = "file") t1_decipher :: proc(buf: []byte, key: u16) { r := u32(key) for i in 0 ..< len(buf) { c := u32(buf[i]) buf[i] = u8(c) ~ u8(r >> 8) r = ((c + r) * 52845 + 22719) & 0xFFFF } } // t1_is_hex_eexec tells the .pfa form from the binary one. Adobe requires the // random padding to be chosen so that the first four ciphertext bytes are not // all hex digits, which is what makes this test safe rather than a guess. @(private = "file") t1_is_hex_eexec :: proc(body: []byte) -> bool { if len(body) < 4 { return false } for i in 0 ..< 4 { if _, ok := hex_value(body[i]); !ok { return false } } return true } // t1_hex_decode decodes the hex-ASCII eexec form into `dst`, returning how many // bytes it wrote. Decoding stops at the first character that is neither hex nor // whitespace, which in a .pfa is the `cleartomark` trailer. @(private = "file") t1_hex_decode :: proc(dst, src: []byte) -> int { n := 0 hi: u8 have_hi := false for c in src { if t1_is_space(c) { continue } v, ok := hex_value(c) if !ok { break } if !have_hi { hi = v have_hi = true continue } if n >= len(dst) { break } dst[n] = hi << 4 | v n += 1 have_hi = false } return n } // t1_decrypt_charstring deciphers one charstring in place and drops its /lenIV // bytes of padding. A charstring shorter than the padding yields an empty // glyph, which draws nothing -- the alternative is a slice out of range. @(private = "file") t1_decrypt_charstring :: proc(buf: []byte, len_iv: int) -> []byte { if len_iv < 0 { return buf } t1_decipher(buf, T1_CHARSTRING_KEY) if len(buf) <= len_iv { return buf[len(buf):] } return buf[len_iv:] } // --------------------------------------------------------------------------- // A PostScript-shaped tokeniser // // Only enough of the syntax to find the handful of constructs that matter. // Strings and hex strings come back whole so that their contents can never be // mistaken for keywords: a /Notice is allowed to contain the word "eexec". // --------------------------------------------------------------------------- @(private = "file") T1_Lexer :: struct { src: []byte, pos: int, } @(private = "file") t1_is_space :: proc "contextless" (c: byte) -> bool { return c == 0 || c == 9 || c == 10 || c == 12 || c == 13 || c == 32 } @(private = "file") t1_is_special :: proc "contextless" (c: byte) -> bool { switch c { case '(', ')', '<', '>', '[', ']', '{', '}', '/', '%': return true } return false } @(private = "file") t1_next :: proc(lx: ^T1_Lexer) -> (tok: []byte, ok: bool) { for { for lx.pos < len(lx.src) && t1_is_space(lx.src[lx.pos]) { lx.pos += 1 } if lx.pos >= len(lx.src) { return nil, false } if lx.src[lx.pos] != '%' { break } for lx.pos < len(lx.src) && lx.src[lx.pos] != '\n' && lx.src[lx.pos] != '\r' { lx.pos += 1 } } start := lx.pos switch lx.src[lx.pos] { case '/': lx.pos += 1 for lx.pos < len(lx.src) && !t1_is_space(lx.src[lx.pos]) && !t1_is_special(lx.src[lx.pos]) { lx.pos += 1 } case '(': depth := 0 for lx.pos < len(lx.src) { c := lx.src[lx.pos] lx.pos += 1 switch c { case '\\': lx.pos += 1 case '(': depth += 1 case ')': depth -= 1 } if depth == 0 { break } } if lx.pos > len(lx.src) { lx.pos = len(lx.src) } case '<': lx.pos += 1 for lx.pos < len(lx.src) && lx.src[lx.pos] != '>' { lx.pos += 1 } if lx.pos < len(lx.src) { lx.pos += 1 } case ')', '>', '[', ']', '{', '}': lx.pos += 1 case: for lx.pos < len(lx.src) && !t1_is_space(lx.src[lx.pos]) && !t1_is_special(lx.src[lx.pos]) { lx.pos += 1 } } if lx.pos == start { // Nothing consumed would stall the caller's loop; a delimiter with no // body is still one byte of progress. lx.pos += 1 } return lx.src[start:lx.pos], true } @(private = "file") t1_eq :: proc "contextless" (tok: []byte, s: string) -> bool { return string(tok) == s } // t1_tok_int reads a plain decimal integer, which is all the constructs here // ever need. Values far past anything meaningful are rejected rather than // wrapped, because every one of them is a length or a count. @(private = "file") t1_tok_int :: proc "contextless" (tok: []byte) -> (int, bool) { if len(tok) == 0 { return 0, false } i := 0 neg := false if tok[0] == '+' || tok[0] == '-' { neg = tok[0] == '-' i = 1 } if i >= len(tok) { return 0, false } v := 0 for ; i < len(tok); i += 1 { c := tok[i] if c < '0' || c > '9' { return 0, false } if v > 1 << 30 { return 0, false } v = v * 10 + int(c - '0') } return neg ? -v : v, true } @(private = "file") t1_next_int :: proc(lx: ^T1_Lexer) -> (int, bool) { tok, ok := t1_next(lx) if !ok { return 0, false } return t1_tok_int(tok) } // t1_find_token locates a whitespace-delimited keyword. Delimiting matters: // `/Subrs` is not to be found inside `/SubrsCount`, and a bare substring search // finds `eexec` inside the middle of a word. @(private = "file") t1_find_token :: proc(hay: []byte, needle: string) -> int { if len(needle) == 0 || len(hay) < len(needle) { return -1 } for i in 0 ..= len(hay) - len(needle) { if hay[i] != needle[0] { continue } if string(hay[i:i + len(needle)]) != needle { continue } if i > 0 { p := hay[i - 1] if !t1_is_space(p) && p != ')' && p != ']' && p != '}' && p != '>' { continue } } j := i + len(needle) if j < len(hay) { c := hay[j] if !t1_is_space(c) && !t1_is_special(c) { continue } } return i } return -1 } // --------------------------------------------------------------------------- // The cleartext portion // --------------------------------------------------------------------------- @(private = "file") t1_parse_cleartext :: proc(f: ^Type1_Font, src: []byte) { lx := T1_Lexer{src = src} for fuel := MAX_T1_TOKENS; fuel > 0; fuel -= 1 { tok, ok := t1_next(&lx) if !ok { break } switch { case t1_eq(tok, "/FontMatrix"): t1_read_matrix(f, &lx) case t1_eq(tok, "/Encoding"): t1_read_encoding(f, &lx) case t1_eq(tok, "/FontName"): if name, nok := t1_next(&lx); nok && len(name) > 1 && name[0] == '/' { f.name = string(name[1:]) } } } } // t1_read_matrix reads `[a b c d e f]`. A matrix that is not invertible, or // that carries a NaN, is discarded in favour of the 1000-per-em default: it // would otherwise collapse every glyph in the font to a point. @(private = "file") t1_read_matrix :: proc(f: ^Type1_Font, lx: ^T1_Lexer) { open, ok := t1_next(lx) if !ok || (!t1_eq(open, "[") && !t1_eq(open, "{")) { return } vals: [6]f64 n := 0 for n < 6 { tok, tok_ok := t1_next(lx) if !tok_ok || t1_eq(tok, "]") || t1_eq(tok, "}") { return } num, num_ok := parse_number(string(tok)) v, v_ok := to_number(num) if !num_ok || !v_ok { return } vals[n] = v n += 1 } m := Matrix{vals[0], vals[1], vals[2], vals[3], vals[4], vals[5]} if !matrix_is_finite(m) || matrix_determinant(m) == 0 { return } f.mtx = m } // t1_read_encoding reads both spellings the format allows: the // `StandardEncoding` shorthand, and the `256 array ... dup / put` // form that every subsetter emits. @(private = "file") t1_read_encoding :: proc(f: ^Type1_Font, lx: ^T1_Lexer) { tok, ok := t1_next(lx) if !ok { return } if t1_eq(tok, "StandardEncoding") { f.encoding = encoding_standard()^ f.has_encoding = true return } depth := 0 for fuel := MAX_T1_TOKENS; fuel > 0; fuel -= 1 { t, tok_ok := t1_next(lx) if !tok_ok { return } switch { case t1_eq(t, "{"): depth += 1 continue case t1_eq(t, "}"): depth -= 1 continue } if depth <= 0 && (t1_eq(t, "def") || t1_eq(t, "readonly") || t1_eq(t, "eexec")) { return } if depth != 0 || !t1_eq(t, "dup") { continue } // `dup / put`. The three tokens are read together, and // anything that does not fit the shape is stepped over rather than // consumed: a font is allowed to `dup` for other reasons. save := lx.pos code, code_ok := t1_next_int(lx) name, name_ok := t1_next(lx) put, put_ok := t1_next(lx) if !code_ok || !name_ok || !put_ok || code < 0 || code > 255 || len(name) < 2 || name[0] != '/' || !t1_eq(put, "put") { lx.pos = save continue } f.encoding[code] = string(name[1:]) f.has_encoding = true } } // --------------------------------------------------------------------------- // The Private dictionary // --------------------------------------------------------------------------- @(private = "file") T1_Parse_Ctx :: struct { f: ^Type1_Font, subrs: [dynamic][]byte, names: [dynamic]string, chars: [dynamic][]byte, } @(private = "file") t1_parse_private :: proc(ctx: ^T1_Parse_Ctx, src: []byte) { ctx.f.len_iv = t1_scan_len_iv(src) lx := T1_Lexer{src = src} subrs_open := false fuel := MAX_T1_TOKENS for fuel > 0 { fuel -= 1 tok, ok := t1_next(&lx) if !ok { return } switch { case t1_eq(tok, "/CharStrings"): t1_skip_to_begin(&lx) t1_read_charstrings(ctx, &lx, &fuel) return case t1_eq(tok, "/Subrs"): // The count that follows is not read at all: the array grows to fit // the entries that turn up, which is what a subsetter that dropped // some without rewriting the count needs, and what stops a count of // a billion from being an allocation. subrs_open = true case subrs_open && t1_eq(tok, "dup"): t1_read_subr(ctx, &lx) } } } // t1_scan_len_iv finds /lenIV before any binary data can be reached, so that a // charstring byte pattern cannot be mistaken for the key. @(private = "file") t1_scan_len_iv :: proc(src: []byte) -> int { limit := len(src) if p := t1_find_token(src, "/Subrs"); p >= 0 { limit = min(limit, p) } if p := t1_find_token(src, "/CharStrings"); p >= 0 { limit = min(limit, p) } at := t1_find_token(src[:limit], "/lenIV") if at < 0 { return 4 } lx := T1_Lexer{src = src[:limit], pos = at} if _, ok := t1_next(&lx); !ok { return 4 } v, ok := t1_next_int(&lx) // 0..16 is the padding a font can mean. A negative value is how the tools // that strip the charstring cipher record that they did. if !ok || v > 16 || v < -1 { return 4 } return v } // t1_skip_to_begin steps over the ` dict dup begin` that introduces the // /CharStrings dictionary. A font that writes it differently is not followed // off a cliff: the lexer is put back where it was and the entries are read from // there. @(private = "file") t1_skip_to_begin :: proc(lx: ^T1_Lexer) { save := lx.pos for _ in 0 ..< 32 { tok, ok := t1_next(lx) if !ok { break } if t1_eq(tok, "begin") { return } if len(tok) > 0 && tok[0] == '/' { break } } lx.pos = save } // t1_read_binary takes the `` bytes that follow an RD token. Exactly one // delimiter separates the two, and the byte after it is data even when it looks // like whitespace. // // A length that runs past the end of the input is clamped rather than rejected: // that is a truncated eexec block, and the partial charstring it yields draws // what it can. The clamp always leaves the lexer at the end of the input, so // the truncation is recorded by the token read that follows, in // t1_read_charstrings. @(private = "file") t1_read_binary :: proc(lx: ^T1_Lexer, n: int) -> ([]byte, bool) { if lx.pos >= len(lx.src) || n < 0 { return nil, false } lx.pos += 1 take := min(n, len(lx.src) - lx.pos) out := lx.src[lx.pos:lx.pos + take] lx.pos += take return out, true } // t1_read_subr reads one `dup RD NP`. The RD token's name // is whatever the font defined it as -- RD, -|, or anything else -- so it is // read and discarded rather than matched. @(private = "file") t1_read_subr :: proc(ctx: ^T1_Parse_Ctx, lx: ^T1_Lexer) { save := lx.pos idx, idx_ok := t1_next_int(lx) n, n_ok := t1_next_int(lx) if !idx_ok || !n_ok || idx < 0 || n < 0 || idx >= MAX_T1_SUBRS { lx.pos = save return } if _, ok := t1_next(lx); !ok { lx.pos = save return } blob, blob_ok := t1_read_binary(lx, n) if !blob_ok { lx.pos = save return } if idx >= len(ctx.subrs) { resize(&ctx.subrs, idx + 1) } ctx.subrs[idx] = t1_decrypt_charstring(blob, ctx.f.len_iv) } // t1_read_charstrings reads `/ RD ND` until `end`. @(private = "file") t1_read_charstrings :: proc(ctx: ^T1_Parse_Ctx, lx: ^T1_Lexer, fuel: ^int) { for fuel^ > 0 { fuel^ -= 1 tok, ok := t1_next(lx) if !ok { ctx.f.truncated = true return } if t1_eq(tok, "end") { return } if len(tok) < 2 || tok[0] != '/' { continue } save := lx.pos n, n_ok := t1_next_int(lx) if !n_ok || n < 0 { lx.pos = save continue } // Both of these mean the dictionary stopped in the middle of an // entry, which is what a truncated eexec block looks like from here. if _, rd_ok := t1_next(lx); !rd_ok { ctx.f.truncated = true return } blob, blob_ok := t1_read_binary(lx, n) if !blob_ok { ctx.f.truncated = true return } if len(ctx.names) >= MAX_T1_GLYPHS { return } append(&ctx.names, string(tok[1:])) append(&ctx.chars, t1_decrypt_charstring(blob, ctx.f.len_iv)) } } // --------------------------------------------------------------------------- // The charstring interpreter (Type 1 spec, chapter 6) // --------------------------------------------------------------------------- @(private = "file") T1_Interp :: struct { f: ^Type1_Font, out: ^Path, stack: [T1_STACK_MAX]f64, sp: int, // Values an OtherSubr left behind for the `pop`s that follow it. The // format models this as the PostScript operand stack, which a charstring // can only reach through `pop`. res: [T1_PS_MAX]f64, nres: int, ires: int, // Where the glyph is being drawn. `origin` is non-zero only for the accent // half of a seac, and translates the whole outline because every Type 1 // path operator is relative. cur: Point, origin: Point, sbx: f64, width: f64, have_width: bool, // A subpath has been begun and not yet closed. open: bool, in_flex: bool, // Where the pen stood when the FLEX began; nothing is emitted while its // points are collected, so this is still the subpath's live end. flex_start: Point, flex: [T1_FLEX_POINTS]Point, nflex: int, fuel: int, stop: bool, allow_seac: bool, seac: bool, seac_args: [5]f64, width_only: bool, } @(private = "file") t1_run :: proc(f: ^Type1_Font, gid: int, out: ^Path, origin: Point, ip: ^T1_Interp) { if gid < 0 || gid >= len(f.chars) { return } ip.f = f ip.out = out ip.origin = origin ip.cur = origin ip.fuel = T1_MAX_FUEL t1_exec(ip, f.chars[gid], 0) // Glyph outlines are closed shapes, so whatever the charstring left open -- // whether it ended at `endchar`, ran out of bytes, or hit a bound -- is // closed here. t1_close_subpath(ip) } @(private = "file") t1_push :: proc(ip: ^T1_Interp, v: f64) { if ip.sp >= len(ip.stack) { // The format allows 24 operands. Past our margin the charstring is // corrupt, and carrying on would mis-align every operator after it. ip.stop = true return } ip.stack[ip.sp] = is_finite(v) ? v : 0 ip.sp += 1 } @(private = "file") t1_args :: proc(ip: ^T1_Interp, n: int) -> ([]f64, bool) { if ip.sp < n { return nil, false } return ip.stack[ip.sp - n:ip.sp], true } @(private = "file") t1_ensure_open :: proc(ip: ^T1_Interp) { if ip.open { return } if ip.out != nil { path_move_to(ip.out, ip.cur.x, ip.cur.y) } ip.open = true } // t1_close_subpath ends the current subpath. Type 1 `closepath` leaves the // current point where it was, unlike the content stream's `h`, so `cur` is // untouched here. @(private = "file") t1_close_subpath :: proc(ip: ^T1_Interp) { if !ip.open { return } if ip.out != nil { path_close(ip.out) } ip.open = false } @(private = "file") t1_rmoveto :: proc(ip: ^T1_Interp, dx, dy: f64) { nx := ip.cur.x + dx ny := ip.cur.y + dy if !is_finite(nx) || !is_finite(ny) { ip.stop = true return } ip.cur = Point{nx, ny} if ip.in_flex { // Inside a FLEX the seven movetos are control points being collected, // not seven separate subpaths. A reader that draws them as movetos // loses the curve entirely and the glyph goes visibly lumpy. if ip.nflex < len(ip.flex) { ip.flex[ip.nflex] = ip.cur } ip.nflex += 1 return } // Glyph outlines are always closed, and fonts that leave the `closepath` // out are common enough that closing here rather than trusting them is the // only way a stroked text render mode looks right. t1_close_subpath(ip) if ip.out != nil { path_move_to(ip.out, nx, ny) } ip.open = true } @(private = "file") t1_rlineto :: proc(ip: ^T1_Interp, dx, dy: f64) { nx := ip.cur.x + dx ny := ip.cur.y + dy if !is_finite(nx) || !is_finite(ny) { ip.stop = true return } t1_ensure_open(ip) if ip.out != nil { path_line_to(ip.out, nx, ny) } ip.cur = Point{nx, ny} } @(private = "file") t1_rrcurveto :: proc(ip: ^T1_Interp, dx1, dy1, dx2, dy2, dx3, dy3: f64) { x1 := ip.cur.x + dx1 y1 := ip.cur.y + dy1 x2 := x1 + dx2 y2 := y1 + dy2 x3 := x2 + dx3 y3 := y2 + dy3 t1_curve_abs(ip, Point{x1, y1}, Point{x2, y2}, Point{x3, y3}) } @(private = "file") t1_curve_abs :: proc(ip: ^T1_Interp, c1, c2, e: Point) { if !is_finite(c1.x) || !is_finite(c1.y) || !is_finite(c2.x) || !is_finite(c2.y) || !is_finite(e.x) || !is_finite(e.y) { ip.stop = true return } t1_ensure_open(ip) if ip.out != nil { path_curve_to(ip.out, c1.x, c1.y, c2.x, c2.y, e.x, e.y) } ip.cur = e } @(private = "file") t1_exec :: proc(ip: ^T1_Interp, code: []byte, depth: int) { if depth > T1_MAX_CALL_DEPTH { ip.stop = true return } i := 0 for i < len(code) { if ip.stop { return } ip.fuel -= 1 if ip.fuel <= 0 { ip.stop = true return } b := code[i] i += 1 // 32..255 begin a number; 0..31 are the operators (Type 1 spec, 6.2). if b >= 32 { v: f64 switch { case b <= 246: v = f64(int(b) - 139) case b <= 250: if i >= len(code) { ip.stop = true return } v = f64((int(b) - 247) * 256 + int(code[i]) + 108) i += 1 case b <= 254: if i >= len(code) { ip.stop = true return } v = f64(-(int(b) - 251) * 256 - int(code[i]) - 108) i += 1 case: if i + 4 > len(code) { ip.stop = true return } u := u32(code[i]) << 24 | u32(code[i + 1]) << 16 | u32(code[i + 2]) << 8 | u32(code[i + 3]) v = f64(i32(u)) i += 4 } t1_push(ip, v) continue } switch b { case 1, 3: // hstem, vstem: hints, which this reader does not use. ip.sp = 0 case 4: // vmoveto if a, ok := t1_args(ip, 1); ok { t1_rmoveto(ip, 0, a[0]) } ip.sp = 0 case 5: // rlineto if a, ok := t1_args(ip, 2); ok { t1_rlineto(ip, a[0], a[1]) } ip.sp = 0 case 6: // hlineto if a, ok := t1_args(ip, 1); ok { t1_rlineto(ip, a[0], 0) } ip.sp = 0 case 7: // vlineto if a, ok := t1_args(ip, 1); ok { t1_rlineto(ip, 0, a[0]) } ip.sp = 0 case 8: // rrcurveto if a, ok := t1_args(ip, 6); ok { t1_rrcurveto(ip, a[0], a[1], a[2], a[3], a[4], a[5]) } ip.sp = 0 case 9: // closepath t1_close_subpath(ip) ip.sp = 0 case 10: // callsubr if ip.sp < 1 { ip.sp = 0 break } ip.sp -= 1 idx := int(ip.stack[ip.sp]) if idx < 0 || idx >= len(ip.f.subrs) || len(ip.f.subrs[idx]) == 0 { break } t1_exec(ip, ip.f.subrs[idx], depth + 1) case 11: // return return case 13: // hsbw if a, ok := t1_args(ip, 2); ok { ip.sbx = a[0] ip.width = a[1] ip.have_width = true // The left sidebearing is where the outline starts, so setting // the current point here is what translates the whole glyph. ip.cur = Point{ip.origin.x + a[0], ip.origin.y} } ip.sp = 0 if ip.width_only && ip.have_width { ip.stop = true return } case 14: // endchar // The open subpath is closed by t1_run rather than here, because a // charstring that simply runs out of bytes has to be closed too. ip.sp = 0 ip.stop = true return case 21: // rmoveto if a, ok := t1_args(ip, 2); ok { t1_rmoveto(ip, a[0], a[1]) } ip.sp = 0 case 22: // hmoveto if a, ok := t1_args(ip, 1); ok { t1_rmoveto(ip, a[0], 0) } ip.sp = 0 case 30: // vhcurveto if a, ok := t1_args(ip, 4); ok { t1_rrcurveto(ip, 0, a[0], a[1], a[2], a[3], 0) } ip.sp = 0 case 31: // hvcurveto if a, ok := t1_args(ip, 4); ok { t1_rrcurveto(ip, a[0], 0, a[1], a[2], 0, a[3]) } ip.sp = 0 case 12: if i >= len(code) { ip.stop = true return } b2 := code[i] i += 1 if t1_escape(ip, b2) { return } case: // An operator this reader does not know is a byte of corruption, // not a reason to abandon the glyph: clear the operands it would // have taken and carry on. ip.sp = 0 } } } // t1_escape runs the two-byte operators. Returns true when the caller must // stop executing this charstring. @(private = "file") t1_escape :: proc(ip: ^T1_Interp, op: byte) -> bool { switch op { case 0, 1, 2: // dotsection, vstem3, hstem3: hints. ip.sp = 0 case 6: // seac if a, ok := t1_args(ip, 5); ok && ip.allow_seac { ip.seac = true for k in 0 ..< 5 { ip.seac_args[k] = a[k] } } ip.sp = 0 ip.stop = true return true case 7: // sbw if a, ok := t1_args(ip, 4); ok { ip.sbx = a[0] ip.width = a[2] ip.have_width = true ip.cur = Point{ip.origin.x + a[0], ip.origin.y + a[1]} } ip.sp = 0 if ip.width_only && ip.have_width { ip.stop = true return true } case 12: // div if ip.sp < 2 { ip.sp = 0 break } den := ip.stack[ip.sp - 1] num := ip.stack[ip.sp - 2] ip.sp -= 2 // A zero denominator gives an infinity or a NaN, which t1_push turns // into a zero: this is the only arithmetic in the interpreter, so it // is the only place a non-finite number can enter the operand stack. t1_push(ip, num / den) case 16: // callothersubr t1_othersubr(ip) case 17: // pop v := 0.0 if ip.ires < ip.nres { v = ip.res[ip.ires] ip.ires += 1 } t1_push(ip, v) case 33: // setcurrentpoint // The operands restate a current point the interpreter already holds: // the FLEX sequence ends `pop pop setcurrentpoint`, handing back the // two values OtherSubr 0 left. Honouring them could only move the pen // somewhere the outline never went, so they are dropped. ip.in_flex = false ip.sp = 0 case: ip.sp = 0 } return false } // t1_othersubr implements `callothersubr`, which is where the font program // calls out to PostScript procedures the interpreter is expected to know. // // Skipping it is not an option. OtherSubrs 0-2 are FLEX, which every serif face // uses on the shallow curves of stems and bowls; a reader that treats the seven // collected movetos as movetos draws those curves as a stack of disconnected // points. OtherSubr 3 is hint replacement, whose one argument has to come back // out through the `pop` that follows so the `callsubr` after it reaches the // right subroutine. @(private = "file") t1_othersubr :: proc(ip: ^T1_Interp) { if ip.sp < 2 { ip.sp = 0 return } othersubr := int(ip.stack[ip.sp - 1]) n := int(ip.stack[ip.sp - 2]) ip.sp -= 2 if n < 0 { n = 0 } if n > ip.sp { n = ip.sp } args := ip.stack[ip.sp - n:ip.sp] ip.sp -= n ip.nres = 0 ip.ires = 0 switch othersubr { case 0: t1_end_flex(ip) case 1: ip.in_flex = true ip.nflex = 0 ip.flex_start = ip.cur case 2: // Collect one FLEX point. The rmoveto that precedes it has already // recorded it, so there is nothing left to do. case 3: // Hint replacement: the subroutine number is handed straight back. A // reader that answered 3 instead -- the value the non-interpreting // fallback pushes -- would run the do-nothing subroutine every font // ships as Subrs[3] and lose the replacement. ip.nres = 1 ip.res[0] = len(args) > 0 ? args[0] : 3 case: // An OtherSubr this reader does not know: leave its arguments for the // pops that follow, which is what the PostScript procedure it stands in // for would have left on the stack. cnt := min(len(args), len(ip.res)) for k in 0 ..< cnt { ip.res[k] = args[k] } ip.nres = cnt } } // t1_end_flex turns the seven collected points into the two curves they // describe. The first is a reference point the hinting machinery uses and the // outline ignores; the other six are two cubics' control and end points. @(private = "file") t1_end_flex :: proc(ip: ^T1_Interp) { if !ip.in_flex { return } ip.in_flex = false // Nothing was emitted while the points were collected, so the subpath is // still sitting where the FLEX began. if !ip.open { if ip.out != nil { path_move_to(ip.out, ip.flex_start.x, ip.flex_start.y) } ip.open = true } if ip.nflex >= T1_FLEX_POINTS { t1_curve_abs(ip, ip.flex[1], ip.flex[2], ip.flex[3]) t1_curve_abs(ip, ip.flex[4], ip.flex[5], ip.flex[6]) } else if ip.nflex > 0 { // A FLEX that collected the wrong number of points is malformed, but // the pen did move: joining the ends with a straight segment keeps the // outline in one piece instead of leaving a gap across the glyph. if ip.out != nil { path_line_to(ip.out, ip.cur.x, ip.cur.y) } } // The two values `pop pop setcurrentpoint` is about to consume. ip.res[0] = ip.cur.x ip.res[1] = ip.cur.y ip.nres = 2 ip.ires = 0 }