Files
pdf-odin-llm/font_type1.odin
T

1534 lines
46 KiB
Odin
Raw Normal View History

2026-09-25 23:32:29 +02:00
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
// <encrypted> 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 <code> /<name> 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 <code> /<name> 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 `<n> 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 `<len>` 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 <index> <len> RD <bytes> 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 `/<name> <len> RD <bytes> 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
}