Files
2026-09-25 23:32:29 +02:00

958 lines
34 KiB
Odin

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 `<lo> <hi> 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> <ffff> <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 `[<s0> <s1> ...]` 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 `<lo> <hi> <dst>` 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> <ffff> <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
}
}
}