package pdf import "core:mem" import "core:mem/virtual" import "core:slice" import "core:strings" import "core:unicode/utf8" // CMaps (ISO 32000-2, 9.7.5): the mapping from bytes in a string to CIDs, and // the /ToUnicode mapping from character codes to text. // // A CMap is variable-width: the codespace ranges say how many bytes the next // code takes, and the same font can mix one- and two-byte codes. Decoding a // string therefore cannot be a simple loop over bytes. // // The file is written in a PostScript-like syntax, which the COS tokeniser in // lexer.odin already reads: a CMap is a run of ` dst` triples wrapped // in `beginXXX`/`endXXX` keywords. Only the sections that carry a mapping are // interpreted; the surrounding `findresource`/`dict`/`begin`/`def` scaffolding // is stepped over, and so is any section this reader does not know. // // PREDEFINED CMAPS -- THE ONE DELIBERATE GAP // // Identity-H and Identity-V are built in and exact. The CJK registries // (Adobe-Japan1-*, Adobe-GB1-*, Adobe-CNS1-*, Adobe-Korea1-*, UniJIS-UCS2-H // and the rest) are tables of tens of thousands of entries each; shipping them // would multiply the size of this library several times over for a class of // document it cannot otherwise render anyway, so they are NOT included. Naming // one of them yields a two-byte identity CMap with `predefined_fallback` set // on the returned Cmap and a `.Bad_Resource` repair recorded on the Document. // That is the right shape for the overwhelmingly common case -- a two-byte // encoding whose CIDs happen to be within a constant of the codes -- and wrong // in detail. A caller that cares must test the flag; nothing else in the // library can tell the difference. // // Everything a Cmap points at is allocated from the allocator passed to // `cmap_parse` / `cmap_identity`, or from the Document's arena when it came // from `cmap_load`. Nothing here is freed individually. // How far a /UseCMap chain is followed, and how deep a nest of CMap streams // `cmap_load` will open. Legitimate files use one level; more than this is a // file trying to make the loader recurse. MAX_CMAP_USE_DEPTH :: 8 // Tokens read from one CMap before parsing gives up. A CMap is a data file, // not a program, so this only ever fires on hostile input. MAX_CMAP_TOKENS :: 1 << 22 // Entries of each kind (singles, ranges, texts) one CMap may hold. MAX_CMAP_ENTRIES :: 1 << 17 // Bytes of UTF-8 destination text one CMap may hold. MAX_CMAP_TEXT_BYTES :: 1 << 22 // Codes one `bfrange` may be expanded into. `<0000> <0020>` is legal // syntax and would otherwise cost 65536 entries per line; a file may write // many such lines. MAX_CMAP_RANGE_CODES :: 1 << 16 // UTF-16 code units in one destination string. A ligature or a decomposed // sequence is a handful; anything longer is not text. MAX_CMAP_DST_RUNES :: 32 // Overlapping ranges are malformed but occur, so a range lookup that lands // between two entries scans back this far before giving up. CMAP_RANGE_BACKSCAN :: 32 // Cmap_Codespace is one codespace range, all of whose codes are `nbytes` long. Cmap_Codespace :: struct { lo: u32, hi: u32, nbytes: int, } // Cmap_Range maps a contiguous run of codes onto consecutive destinations. Cmap_Range :: struct { lo: u32, hi: u32, dst: u32, } // Cmap_Single maps one code. Kept separate from ranges because `cidchar` and // `bfchar` sections are common and dense enough that a binary search over // singles beats widening every one into a range. Cmap_Single :: struct { code: u32, dst: u32, } // Cmap_Text maps one code to a string, which /ToUnicode needs: a single glyph // can stand for a ligature ("ffi") or a decomposed sequence. Cmap_Text :: struct { code: u32, // Offset and length into Cmap.text_pool, as UTF-8. off: u32, len: u32, } Cmap :: struct { name: string, // 0 horizontal, 1 vertical. wmode: int, // Sorted, so lookup is a binary search. codespace: []Cmap_Codespace, singles: []Cmap_Single, ranges: []Cmap_Range, // /ToUnicode only. texts: []Cmap_Text, text_pool: []u8, // /UseCMap, applied when this CMap has no entry of its own. parent: ^Cmap, // True for the Identity-H / Identity-V CMaps, which need no tables. identity: bool, // True when a predefined CMap this library does not ship was asked for and // a two-byte identity mapping was substituted. See the note above: the // result is usable but is not the mapping the file named. predefined_fallback: bool, } // --------------------------------------------------------------------------- // Loading // --------------------------------------------------------------------------- // cmap_load builds a CMap from the object a font names it with: a `Name` for a // predefined CMap, or a stream holding an embedded one. The result belongs to // the Document. // // A name this library does not ship falls back to a two-byte identity mapping // (see the file comment); only an object that is neither a name nor a stream // is an error, because there is then genuinely nothing to hand back. cmap_load :: proc(doc: ^Document, obj: Object) -> (^Cmap, Error) { ctx := Cmap_Load_Ctx{} return cmap_load_ctx(doc, obj, &ctx) } // cmap_parse reads a CMap out of bytes -- an embedded CMap stream's decoded // payload, or a /ToUnicode stream. // // `doc` is used only to resolve a `usecmap` operand and to record repairs, and // may be nil. The returned Cmap and everything it points at are allocated with // `allocator` and belong to the caller. cmap_parse :: proc(doc: ^Document, data: []byte, allocator := context.allocator) -> (^Cmap, Error) { return cmap_parse_data(doc, data, nil, allocator) } // cmap_identity returns the Identity-H (or Identity-V) CMap: two-byte codes, // CID equal to code. Allocated with `allocator` and owned by the caller. cmap_identity :: proc(vertical: bool, allocator := context.allocator) -> ^Cmap { cm := new(Cmap, allocator) if cm == nil { return nil } cs := make([]Cmap_Codespace, 1, allocator) if cs == nil { free(cm, allocator) return nil } cs[0] = Cmap_Codespace{lo = 0, hi = 0xFFFF, nbytes = 2} cm.name = vertical ? "Identity-V" : "Identity-H" cm.wmode = vertical ? 1 : 0 cm.identity = true cm.codespace = cs return cm } @(private = "file") Cmap_Load_Ctx :: struct { depth: int, // Stream objects already open further up the stack. A /UseCMap cycle is // two streams naming each other, which the depth cap alone would also // stop, but stopping at the repeat keeps the diagnosis honest. seen: [MAX_CMAP_USE_DEPTH]rawptr, nseen: int, } @(private = "file") cmap_load_ctx :: proc(doc: ^Document, obj: Object, ctx: ^Cmap_Load_Ctx) -> (^Cmap, Error) { allocator := context.allocator if doc != nil { allocator = doc.allocator } #partial switch v in resolve(doc, obj) { case Name: cm := cmap_predefined(doc, string(v), allocator) if cm == nil { return nil, .Out_Of_Memory } return cm, .None case ^Stream: if ctx.depth >= MAX_CMAP_USE_DEPTH { add_repair(doc, .Recursion_Limit_Hit, 0, v.ref) return nil, .Bad_Cmap } for i in 0 ..< ctx.nseen { if ctx.seen[i] == rawptr(v) { add_repair(doc, .Bad_Resource, 0, v.ref) return nil, .Bad_Cmap } } if ctx.nseen < len(ctx.seen) { ctx.seen[ctx.nseen] = rawptr(v) ctx.nseen += 1 } ctx.depth += 1 defer ctx.depth -= 1 // The dictionary's /UseCMap is read before the body so that the body's // own codespace ranges, if it has any, win over the inherited ones. parent: ^Cmap if use := dict_get_raw(v.dict, "UseCMap"); use != nil { parent, _ = cmap_load_ctx(doc, use, ctx) } data, derr := stream_data(doc, v) if derr != .None { return nil, .Bad_Cmap } cm, perr := cmap_parse_data(doc, data, parent, allocator) if cm == nil { return nil, perr } // ISO 32000-2, 9.7.5.3: the stream dictionary may carry /WMode and // /CMapName too. The body has already spoken if it set them. if w, ok := to_int(dict_get(doc, v.dict, "WMode")); ok && cm.wmode == 0 { cm.wmode = w != 0 ? 1 : 0 } if cm.name == "" { if n := dict_name(doc, v.dict, "CMapName"); n != "" { cm.name = n } } return cm, .None } return nil, .Bad_Cmap } // cmap_predefined resolves a predefined CMap name. Only the two identity CMaps // are shipped; see the file comment for why, and what the others become. @(private = "file") cmap_predefined :: proc(doc: ^Document, name: string, allocator: mem.Allocator) -> ^Cmap { switch name { case "Identity-H": return cmap_identity(false, allocator) case "Identity-V": return cmap_identity(true, allocator) } // Every predefined vertical CMap's name ends in "-V", which is enough to // get the writing mode right even when the mapping cannot be. cm := cmap_identity(strings.has_suffix(name, "-V"), allocator) if cm == nil { return nil } cm.name = strings.clone(name, allocator) cm.predefined_fallback = true add_repair(doc, .Bad_Resource, 0) return cm } // --------------------------------------------------------------------------- // Lookup // --------------------------------------------------------------------------- // cmap_lookup maps a character code to its destination value: a CID for an // encoding CMap, and nothing at all for a /ToUnicode CMap, whose destinations // are strings and are read with `cmap_lookup_text`. // // A code with no entry of its own is looked for in the /UseCMap parent. cmap_lookup :: proc(cm: ^Cmap, code: u32) -> (dst: u32, ok: bool) { c := cm for _ in 0 ..< MAX_CMAP_USE_DEPTH { if c == nil { break } if d, found := cmap_lookup_local(c, code); found { return d, true } // An identity CMap has no tables at all; it answers for every code. if c.identity { return code, true } c = c.parent } return 0, false } // cmap_lookup_local answers from this CMap's own tables only. @(private = "file") cmap_lookup_local :: proc(c: ^Cmap, code: u32) -> (dst: u32, ok: bool) { if i, found := cmap_find_single(c.singles, code); found { return c.singles[i].dst, true } if i, found := cmap_find_range(c.ranges, code); found { r := c.ranges[i] // In u64 so that a range whose destination runs off the top of u32 // cannot overflow the arithmetic on the way. return u32(u64(r.dst) + u64(code - r.lo)), true } return 0, false } // cmap_lookup_text maps a character code to the text it stands for, as UTF-8. // This is the /ToUnicode direction, and the string may be several code points // long: one glyph can be a ligature or a decomposed sequence. // // The returned string is a view into the Cmap and lives exactly as long as it // does. It is not allocated, and must not be freed. cmap_lookup_text :: proc(cm: ^Cmap, code: u32) -> (utf8: string, ok: bool) { c := cm for _ in 0 ..< MAX_CMAP_USE_DEPTH { if c == nil { break } if i, found := cmap_find_text(c.texts, code); found { t := c.texts[i] end := int(t.off) + int(t.len) if end <= len(c.text_pool) { return string(c.text_pool[t.off:end]), true } } c = c.parent } return "", false } // cmap_next_code reads the next character code from a string. // // The code's width is whatever the codespace ranges say it is, so a CMap that // mixes one- and two-byte codes segments correctly. Bytes that match no range // still consume a whole code's worth, so a caller looping over a string always // makes progress; `nbytes` is 0 only for an empty string. How wide that // invalid code is follows ISO 32000-2, 9.7.6.3 -- see below. cmap_next_code :: proc(cm: ^Cmap, s: []byte) -> (code: u32, nbytes: int) { if len(s) == 0 { return 0, 0 } // With no CMap at all a code is a byte, which is what a simple font wants. if cm == nil { return u32(s[0]), 1 } best := 0 shortest := 0 for cs in cm.codespace { if cs.nbytes < 1 || cs.nbytes > 4 { continue } if shortest == 0 || cs.nbytes < shortest { shortest = cs.nbytes } // The longest match wins: a two-byte range and a one-byte range can // both admit the first byte, and the wider code is the intended one. if cs.nbytes > best && cs.nbytes <= len(s) && cmap_codespace_contains(cs, s[:cs.nbytes]) { best = cs.nbytes } } if best > 0 { return cmap_be_code(s[:best]), best } // No range admits these bytes as a code. ISO 32000-2, 9.7.6.3 gives two // rules for that case, and they do not consume the same number of bytes: // // a) the first byte matches the first byte of no range at all -- then // "the range having the shortest codes shall be chosen"; // b) otherwise there is a partial match, and the code so far is matched // "against the beginnings of all longer codespace ranges until the // longest such partial match has been found. The length of the codes // in the chosen codespace range determines the total number of bytes // to consume". // // Rule (b) is the one that matters: a lead byte that opens a two-byte // range but is not completed by the byte after it must still consume two. // Consuming one instead does not merely drop a glyph -- every code after // it in the string is read at the wrong offset, so one bad byte turns the // rest of the line into rubbish. n := 0 for k := 1; k <= 4 && k <= len(s); k += 1 { w := 0 for cs in cm.codespace { if cs.nbytes <= k || cs.nbytes > 4 { continue } if !cmap_codespace_prefix(cs, s[:k]) { continue } // Same tie-break as a complete match above: where two ranges are // equally partial matches, the wider code is the intended one. w = max(w, cs.nbytes) } if w == 0 { break } n = w } if n == 0 { n = shortest if n == 0 { // No codespace section at all. Two bytes for an identity CMap, // which is what Identity-H means, and one byte otherwise. n = cm.identity ? 2 : 1 } } n = min(n, len(s)) return cmap_be_code(s[:n]), n } @(private = "file") cmap_codespace_contains :: proc(cs: Cmap_Codespace, s: []byte) -> bool { if len(s) != cs.nbytes { return false } return cmap_codespace_bytes_in(cs, s) } // cmap_codespace_prefix reports whether `s` could be the beginning of a code // in this range: the range is wider than `s`, and every byte of `s` is inside // its position's interval. That is the partial match of 9.7.6.3 b). @(private = "file") cmap_codespace_prefix :: proc(cs: Cmap_Codespace, s: []byte) -> bool { if len(s) >= cs.nbytes { return false } return cmap_codespace_bytes_in(cs, s) } // cmap_codespace_bytes_in tests the leading bytes of a code against the range. // ISO 32000-2, 9.7.6.2: a code is inside a codespace range when *each* of its // bytes is inside the corresponding byte's range, which is not the same as the // numeric interval -- <8140> <9ffc> excludes 0x8200. @(private = "file") cmap_codespace_bytes_in :: proc(cs: Cmap_Codespace, s: []byte) -> bool { for i in 0 ..< min(len(s), cs.nbytes) { sh := uint(8 * (cs.nbytes - 1 - i)) lo := byte((cs.lo >> sh) & 0xFF) hi := byte((cs.hi >> sh) & 0xFF) if s[i] < lo || s[i] > hi { return false } } return true } @(private = "file") cmap_be_code :: proc(s: []byte) -> u32 { v: u32 for b in s[:min(len(s), 4)] { v = v << 8 | u32(b) } return v } @(private = "file") cmap_find_single :: proc(a: []Cmap_Single, code: u32) -> (int, bool) { lo, hi := 0, len(a) for lo < hi { mid := lo + (hi - lo) / 2 if a[mid].code < code { lo = mid + 1 } else { hi = mid } } if lo < len(a) && a[lo].code == code { return lo, true } return 0, false } @(private = "file") cmap_find_text :: proc(a: []Cmap_Text, code: u32) -> (int, bool) { lo, hi := 0, len(a) for lo < hi { mid := lo + (hi - lo) / 2 if a[mid].code < code { lo = mid + 1 } else { hi = mid } } if lo < len(a) && a[lo].code == code { return lo, true } return 0, false } @(private = "file") cmap_find_range :: proc(a: []Cmap_Range, code: u32) -> (int, bool) { // The greatest index whose `lo` is at or below the code, then back up: the // ranges are sorted by `lo` but a malformed CMap may overlap them, so the // first candidate is not always the one that contains the code. lo, hi := 0, len(a) for lo < hi { mid := lo + (hi - lo) / 2 if a[mid].lo <= code { lo = mid + 1 } else { hi = mid } } steps := 0 for i := lo - 1; i >= 0 && steps < CMAP_RANGE_BACKSCAN; i -= 1 { steps += 1 if a[i].hi >= code { return i, true } } return 0, false } // --------------------------------------------------------------------------- // Parsing // --------------------------------------------------------------------------- @(private = "file") Cmap_Section :: enum u8 { None, Codespace, Cidrange, Cidchar, Bfrange, Bfchar, // A `beginXXX` this reader does not interpret. Its contents are stepped // over rather than being mistaken for the enclosing CMap's operands. Skip, } @(private = "file") Cmap_Operand_Kind :: enum u8 { None, Num, Str, Nam, Arr } @(private = "file") Cmap_Operand :: struct { kind: Cmap_Operand_Kind, num: i64, str: []byte, nam: string, arr: [][]byte, } // Build entries carry the order they were written in, so that a code defined // twice -- a broad `bfrange` and then a `bfchar` correcting one code out of // it, which real producers emit -- keeps the later definition. @(private = "file") Cmap_Build_Single :: struct { code: u32, dst: u32, seq: u32, } @(private = "file") Cmap_Build_Text :: struct { code: u32, off: u32, len: u32, seq: u32, } @(private = "file") Cmap_Builder :: struct { doc: ^Document, allocator: mem.Allocator, // where the finished Cmap goes scratch: mem.Allocator, // dies with the parse codespace: [dynamic]Cmap_Codespace, singles: [dynamic]Cmap_Build_Single, ranges: [dynamic]Cmap_Range, texts: [dynamic]Cmap_Build_Text, pool: [dynamic]u8, name: string, wmode: int, has_wmode: bool, use_name: string, seq: u32, } @(private = "file") cmap_parse_data :: proc( doc: ^Document, data: []byte, parent: ^Cmap, allocator: mem.Allocator, ) -> (^Cmap, Error) { // Tokenising a CMap materialises every hex string in it, and nearly all of // those are read for a number and thrown away. They go in an arena that is // dropped at the end of the parse rather than into the Document's, which // cannot free them. scratch: virtual.Arena if virtual.arena_init_growing(&scratch) != nil { return nil, .Out_Of_Memory } defer virtual.arena_destroy(&scratch) sa := virtual.arena_allocator(&scratch) b := Cmap_Builder{ doc = doc, allocator = allocator, scratch = sa, codespace = make([dynamic]Cmap_Codespace, sa), singles = make([dynamic]Cmap_Build_Single, sa), ranges = make([dynamic]Cmap_Range, sa), texts = make([dynamic]Cmap_Build_Text, sa), pool = make([dynamic]u8, sa), } cmap_run(&b, data) cm := new(Cmap, allocator) if cm == nil { return nil, .Out_Of_Memory } cm.parent = parent // A `usecmap` operator in the body names a predefined CMap. It cannot name // an embedded one -- there is no resource machinery here to look it up in // -- so this can never recurse back into a stream, and the /UseCMap of the // stream dictionary, which can, has already had its say. if b.use_name != "" && cm.parent == nil { cm.parent = cmap_predefined(doc, b.use_name, allocator) } cmap_finish(&b, cm) return cm, .None } // cmap_run is the whole of the CMap grammar this reader understands. @(private = "file") cmap_run :: proc(b: ^Cmap_Builder, data: []byte) { lex := lexer_make(data) section := Cmap_Section.None pend: [3]Cmap_Operand np := 0 // The last two operands seen outside a section, which is all that `def` // and `usecmap` need to look back at. op0, op1: Cmap_Operand for fuel := MAX_CMAP_TOKENS; fuel > 0; fuel -= 1 { tok := lexer_next(&lex, b.scratch) if tok.kind == .EOF { // An unterminated section simply ends here, with whatever it had. return } if tok.kind == .Keyword { // A section this reader does not interpret runs to its own // `endXXX`, or to the next section: the operators inside it belong // to it, and a `/WMode 1 def` sitting in a `usematrix` section is // not this CMap's writing mode. if section == .Skip && !strings.has_prefix(tok.text, "end") && !strings.has_prefix(tok.text, "begin") { continue } // Any other keyword closes an open section, terminated or not: a // file that writes `begincidchar` without closing the // `begincidrange` before it has still said where the first ended. section = .None np = 0 switch tok.text { case "endcmap": return case "begincodespacerange": section = .Codespace case "begincidrange": section = .Cidrange case "begincidchar": section = .Cidchar case "beginbfrange": section = .Bfrange case "beginbfchar": section = .Bfchar case "usecmap": if op1.kind == .Nam { b.use_name = op1.nam } case "def": if op0.kind == .Nam && op0.nam == "WMode" && op1.kind == .Num { b.wmode = op1.num != 0 ? 1 : 0 b.has_wmode = true } if op0.kind == .Nam && op0.nam == "CMapName" && op1.kind == .Nam { b.name = strings.clone(op1.nam, b.allocator) } case "begincmap": // Scaffolding; the sections inside it are what matter. case: // `beginnotdefrange`, `beginusematrix`, `begincidsysteminfo`: // sections whose contents must not be read as anything else. // Bare `begin` is PostScript scaffolding, not a section. if len(tok.text) > len("begin") && strings.has_prefix(tok.text, "begin") { section = .Skip } } op0, op1 = Cmap_Operand{}, Cmap_Operand{} continue } if section == .Skip { continue } o := cmap_operand(&lex, tok, b.scratch) if section == .None { op0, op1 = op1, o continue } if o.kind == .None { continue } pend[np] = o np += 1 arity := 3 if section == .Cidrange || section == .Bfrange else 2 if np >= arity { cmap_emit(b, section, pend[:arity]) np = 0 } } } @(private = "file") cmap_operand :: proc(lex: ^Lexer, tok: Token, allocator: mem.Allocator) -> Cmap_Operand { #partial switch tok.kind { case .String: return Cmap_Operand{kind = .Str, str = tok.bytes} case .Integer, .Real: return Cmap_Operand{kind = .Num, num = tok.int_value} case .Name: return Cmap_Operand{kind = .Nam, nam = tok.text} case .Array_Open: return cmap_operand_array(lex, allocator) } return Cmap_Operand{} } // cmap_operand_array reads the `[ ...]` destination form of bfrange. @(private = "file") cmap_operand_array :: proc(lex: ^Lexer, allocator: mem.Allocator) -> Cmap_Operand { items := make([dynamic][]byte, allocator) for _ in 0 ..< MAX_CMAP_RANGE_CODES { tok := lexer_next(lex, allocator) #partial switch tok.kind { case .EOF, .Array_Close: return Cmap_Operand{kind = .Arr, arr = items[:]} case .String: append(&items, tok.bytes) case .Keyword: // The array never closed. Hand the keyword back to the main loop // so the section it opens is not swallowed. lex.pos = tok.start return Cmap_Operand{kind = .Arr, arr = items[:]} } } return Cmap_Operand{kind = .Arr, arr = items[:]} } @(private = "file") cmap_emit :: proc(b: ^Cmap_Builder, section: Cmap_Section, ops: []Cmap_Operand) { switch section { case .Codespace: if len(ops) < 2 || ops[0].kind != .Str || ops[1].kind != .Str { return } lo, hi := ops[0].str, ops[1].str // The two ends of a codespace range are required to be the same width; // when they are not, the narrower one is the only width both describe. n := min(len(lo), len(hi)) if n < 1 { return } n = min(n, 4) if len(b.codespace) >= MAX_CMAP_ENTRIES { return } cs := Cmap_Codespace{lo = cmap_be_code(lo[:n]), hi = cmap_be_code(hi[:n]), nbytes = n} // A range that runs backwards in any byte position describes nothing. for i in 0 ..< n { sh := uint(8 * (n - 1 - i)) if byte((cs.lo >> sh) & 0xFF) > byte((cs.hi >> sh) & 0xFF) { return } } append(&b.codespace, cs) case .Cidrange: if len(ops) < 3 || ops[0].kind != .Str || ops[1].kind != .Str || ops[2].kind != .Num { return } lo := cmap_be_code(ops[0].str) hi := cmap_be_code(ops[1].str) if lo > hi { return } if ops[2].num < 0 || ops[2].num > 0xFFFF_FFFF { return } if len(b.ranges) >= MAX_CMAP_ENTRIES { return } append(&b.ranges, Cmap_Range{lo = lo, hi = hi, dst = u32(ops[2].num)}) case .Cidchar: if len(ops) < 2 || ops[0].kind != .Str || ops[1].kind != .Num { return } if ops[1].num < 0 || ops[1].num > 0xFFFF_FFFF { return } cmap_add_single(b, cmap_be_code(ops[0].str), u32(ops[1].num)) case .Bfrange: if len(ops) < 3 || ops[0].kind != .Str || ops[1].kind != .Str { return } lo := cmap_be_code(ops[0].str) hi := cmap_be_code(ops[1].str) if lo > hi { return } count := u64(hi) - u64(lo) + 1 count = min(count, MAX_CMAP_RANGE_CODES) #partial switch ops[2].kind { case .Str: cmap_add_bfrange_string(b, lo, int(count), ops[2].str) case .Arr: n := min(int(count), len(ops[2].arr)) for i in 0 ..< n { cmap_add_text(b, lo + u32(i), ops[2].arr[i]) } } case .Bfchar: if len(ops) < 2 || ops[0].kind != .Str || ops[1].kind != .Str { return } cmap_add_text(b, cmap_be_code(ops[0].str), ops[1].str) case .None, .Skip: // Not reachable: the caller only emits for a data section. } } // cmap_add_bfrange_string expands the ` ` form. ISO 32000-2, // 9.10.3: the destination is incremented once per code, in its last code unit. @(private = "file") cmap_add_bfrange_string :: proc(b: ^Cmap_Builder, lo: u32, count: int, dst: []byte) { runes: [MAX_CMAP_DST_RUNES]rune n := cmap_utf16_runes(dst, runes[:]) if n == 0 { return } base := i64(runes[n - 1]) buf: [MAX_CMAP_DST_RUNES * 4]byte for i in 0 ..< count { r := rune(base + i64(i)) // Past the end of Unicode, or onto a surrogate: the source range ran // further than its destination could follow. if r > 0x10FFFF || (r >= 0xD800 && r <= 0xDFFF) { r = utf8.RUNE_ERROR } runes[n - 1] = r k := 0 for j in 0 ..< n { enc, sz := utf8.encode_rune(runes[j]) copy(buf[k:], enc[:sz]) k += sz } // Stop as soon as the entry could not be stored. The caps are one // level down, in cmap_add_text_bytes, and this loop is what has to be // bounded by them: `<0000> <0020>` is 65536 iterations, the // caps are full after two such lines, and nothing bounds the number of // lines but the token fuel -- 1.4 million of them fit, which is half // an hour of expanding codes that are all thrown away. A 30 KB file // compresses down to that. if !cmap_add_text_bytes(b, lo + u32(i), buf[:k]) { return } } } // cmap_add_text records a UTF-16BE destination string as UTF-8. @(private = "file") cmap_add_text :: proc(b: ^Cmap_Builder, code: u32, dst: []byte) { runes: [MAX_CMAP_DST_RUNES]rune n := cmap_utf16_runes(dst, runes[:]) if n == 0 { return } buf: [MAX_CMAP_DST_RUNES * 4]byte k := 0 for j in 0 ..< n { enc, sz := utf8.encode_rune(runes[j]) copy(buf[k:], enc[:sz]) k += sz } cmap_add_text_bytes(b, code, buf[:k]) } // cmap_add_text_bytes stores one code -> text mapping, and reports whether it // did: a caller expanding a range needs to know that the CMap is full, or it // goes on generating entries that are all discarded. @(private = "file") cmap_add_text_bytes :: proc(b: ^Cmap_Builder, code: u32, utf8_bytes: []byte) -> bool { if len(utf8_bytes) == 0 { return false } if len(b.texts) >= MAX_CMAP_ENTRIES { return false } if len(b.pool) + len(utf8_bytes) > MAX_CMAP_TEXT_BYTES { return false } off := u32(len(b.pool)) append(&b.pool, ..utf8_bytes) append(&b.texts, Cmap_Build_Text{code = code, off = off, len = u32(len(utf8_bytes)), seq = b.seq}) b.seq += 1 return true } @(private = "file") cmap_add_single :: proc(b: ^Cmap_Builder, code: u32, dst: u32) { if len(b.singles) >= MAX_CMAP_ENTRIES { return } append(&b.singles, Cmap_Build_Single{code = code, dst = dst, seq = b.seq}) b.seq += 1 } // cmap_utf16_runes decodes a UTF-16BE destination string. Unpaired surrogates // and a trailing half code unit each become U+FFFD, so the result is always // encodable as valid UTF-8 (ISO 32000-2, 9.10.3 requires the destination of a // /ToUnicode mapping to be UTF-16BE). @(private = "file") cmap_utf16_runes :: proc(src: []byte, out: []rune) -> int { n := 0 i := 0 for i + 1 < len(src) && n < len(out) { u := u16(src[i]) << 8 | u16(src[i + 1]) i += 2 switch { case u >= 0xD800 && u <= 0xDBFF: if i + 1 < len(src) { v := u16(src[i]) << 8 | u16(src[i + 1]) if v >= 0xDC00 && v <= 0xDFFF { i += 2 out[n] = rune(0x10000 + (u32(u - 0xD800) << 10 | u32(v - 0xDC00))) n += 1 continue } } out[n] = utf8.RUNE_ERROR n += 1 case u >= 0xDC00 && u <= 0xDFFF: out[n] = utf8.RUNE_ERROR n += 1 case: out[n] = rune(u) n += 1 } } if i < len(src) && n < len(out) { out[n] = utf8.RUNE_ERROR n += 1 } return n } // --------------------------------------------------------------------------- // Finishing // --------------------------------------------------------------------------- @(private = "file") cmap_finish :: proc(b: ^Cmap_Builder, cm: ^Cmap) { cm.name = b.name cm.wmode = b.wmode if !b.has_wmode && cm.parent != nil { cm.wmode = cm.parent.wmode } if len(b.codespace) > 0 { cs := make([]Cmap_Codespace, len(b.codespace), b.allocator) if cs != nil { copy(cs, b.codespace[:]) cm.codespace = cs } } else if cm.parent != nil { // ISO 32000-2, 9.7.5.3: a CMap that declares no codespace ranges takes // the ones of the CMap it uses. Sharing the parent's slice is safe -- // nothing here ever mutates a finished Cmap. cm.codespace = cm.parent.codespace } slice.sort_by(b.singles[:], proc(x, y: Cmap_Build_Single) -> bool { return x.code < y.code || (x.code == y.code && x.seq < y.seq) }) slice.sort_by(b.texts[:], proc(x, y: Cmap_Build_Text) -> bool { return x.code < y.code || (x.code == y.code && x.seq < y.seq) }) slice.sort_by(b.ranges[:], proc(x, y: Cmap_Range) -> bool { return x.lo < y.lo }) if n := len(b.singles); n > 0 { out := make([]Cmap_Single, n, b.allocator) if out != nil { k := 0 for i in 0 ..< n { // Sorted by (code, seq), so the last of a run is the latest // definition and the one that wins. if i + 1 < n && b.singles[i + 1].code == b.singles[i].code { continue } out[k] = Cmap_Single{code = b.singles[i].code, dst = b.singles[i].dst} k += 1 } cm.singles = out[:k] } } if n := len(b.texts); n > 0 { out := make([]Cmap_Text, n, b.allocator) if out != nil { k := 0 for i in 0 ..< n { if i + 1 < n && b.texts[i + 1].code == b.texts[i].code { continue } out[k] = Cmap_Text{code = b.texts[i].code, off = b.texts[i].off, len = b.texts[i].len} k += 1 } cm.texts = out[:k] } } if n := len(b.ranges); n > 0 { out := make([]Cmap_Range, n, b.allocator) if out != nil { copy(out, b.ranges[:]) cm.ranges = out } } if n := len(b.pool); n > 0 && len(cm.texts) > 0 { pool := make([]u8, n, b.allocator) if pool != nil { copy(pool, b.pool[:]) cm.text_pool = pool } else { cm.texts = nil } } }