@strav/pdf 0.4.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/README.md +79 -0
  2. package/package.json +51 -0
  3. package/src/color/cie.ts +61 -0
  4. package/src/color/color.ts +77 -0
  5. package/src/color/conversion.ts +26 -0
  6. package/src/color/device.ts +37 -0
  7. package/src/color/devicen.ts +74 -0
  8. package/src/color/icc.ts +103 -0
  9. package/src/color/index.ts +15 -0
  10. package/src/color/separation.ts +94 -0
  11. package/src/color/space.ts +47 -0
  12. package/src/content/content_stream.ts +373 -0
  13. package/src/content/graphics_state.ts +64 -0
  14. package/src/content/index.ts +16 -0
  15. package/src/content/operators.ts +70 -0
  16. package/src/content/path.ts +51 -0
  17. package/src/content/resources.ts +119 -0
  18. package/src/content/text_object.ts +140 -0
  19. package/src/document/catalog.ts +16 -0
  20. package/src/document/index.ts +13 -0
  21. package/src/document/object_table.ts +67 -0
  22. package/src/document/page.ts +74 -0
  23. package/src/document/page_tree.ts +78 -0
  24. package/src/document/pdf_document.ts +310 -0
  25. package/src/document/types.ts +65 -0
  26. package/src/document/xref.ts +68 -0
  27. package/src/ext-gstate/ext_gstate.ts +69 -0
  28. package/src/ext-gstate/index.ts +2 -0
  29. package/src/fonts/cff.ts +123 -0
  30. package/src/fonts/cid_encoding.ts +45 -0
  31. package/src/fonts/cmap_table.ts +180 -0
  32. package/src/fonts/font.ts +342 -0
  33. package/src/fonts/glyf.ts +59 -0
  34. package/src/fonts/hmtx.ts +21 -0
  35. package/src/fonts/index.ts +20 -0
  36. package/src/fonts/name_table.ts +50 -0
  37. package/src/fonts/os2.ts +41 -0
  38. package/src/fonts/sfnt.ts +224 -0
  39. package/src/fonts/standard_14.ts +132 -0
  40. package/src/fonts/subset.ts +221 -0
  41. package/src/fonts/to_unicode.ts +82 -0
  42. package/src/fonts/win_ansi.ts +69 -0
  43. package/src/images/image.ts +111 -0
  44. package/src/images/index.ts +6 -0
  45. package/src/images/jpeg.ts +103 -0
  46. package/src/images/png.ts +239 -0
  47. package/src/images/smask.ts +24 -0
  48. package/src/index.ts +57 -0
  49. package/src/metadata/index.ts +3 -0
  50. package/src/metadata/info_dict.ts +28 -0
  51. package/src/metadata/xmp.ts +110 -0
  52. package/src/objects/encode.ts +77 -0
  53. package/src/objects/index.ts +43 -0
  54. package/src/objects/indirect_ref.ts +17 -0
  55. package/src/objects/name.ts +50 -0
  56. package/src/objects/number.ts +43 -0
  57. package/src/objects/string.ts +136 -0
  58. package/src/objects/types.ts +86 -0
  59. package/src/output/buffer_sink.ts +40 -0
  60. package/src/output/byte_sink.ts +12 -0
  61. package/src/output/index.ts +3 -0
  62. package/src/output/stream_sink.ts +62 -0
  63. package/src/patterns/index.ts +10 -0
  64. package/src/patterns/shading.ts +162 -0
  65. package/src/patterns/tiling_pattern.ts +68 -0
  66. package/src/standards/context.ts +10 -0
  67. package/src/standards/index.ts +23 -0
  68. package/src/standards/pdf_a.ts +23 -0
  69. package/src/standards/pdf_x.ts +31 -0
  70. package/src/streams/ascii85.ts +61 -0
  71. package/src/streams/ascii_hex.ts +33 -0
  72. package/src/streams/flate.ts +17 -0
  73. package/src/streams/index.ts +9 -0
  74. package/src/streams/stream.ts +66 -0
  75. package/src/util/ascii.ts +63 -0
  76. package/src/util/binary.ts +71 -0
  77. package/src/util/errors.ts +61 -0
  78. package/src/util/index.ts +10 -0
  79. package/src/util/units.ts +24 -0
  80. package/tsconfig.json +5 -0
@@ -0,0 +1,65 @@
1
+ /** Public value types for the document layer (spec §6). */
2
+
3
+ /** A page size in PDF points. */
4
+ export interface PageSize {
5
+ widthPt: number
6
+ heightPt: number
7
+ }
8
+
9
+ /** A rectangle in PDF points, origin bottom-left. */
10
+ export interface Rect {
11
+ x: number
12
+ y: number
13
+ w: number
14
+ h: number
15
+ }
16
+
17
+ /** Conformance target. `null` = plain PDF 1.7. Enforced in M11. */
18
+ export type ConformanceLevel = 'PDF/A-2b' | 'PDF/X-4' | null
19
+
20
+ /** Document metadata written to the Info dictionary (spec §14.1). */
21
+ export interface DocumentInfo {
22
+ title?: string
23
+ author?: string
24
+ subject?: string
25
+ keywords?: string
26
+ creator?: string
27
+ }
28
+
29
+ export interface CreateOptions {
30
+ info?: DocumentInfo
31
+ /** Conformance target (validated at save in M11; accepted now). */
32
+ conformance?: ConformanceLevel
33
+ /**
34
+ * Fixed creation date for deterministic output (spec §16.3). Defaults to
35
+ * `new Date()` at save time.
36
+ */
37
+ creationDate?: Date
38
+ /**
39
+ * Fixed 16-byte document ID for deterministic output. Hex string of 32
40
+ * chars, or raw bytes. Defaults to a content-derived hash.
41
+ */
42
+ documentId?: string | Uint8Array
43
+ }
44
+
45
+ export interface AddPageOptions {
46
+ size: PageSize
47
+ rotation?: 0 | 90 | 180 | 270
48
+ }
49
+
50
+ /** Output intent (spec §9.3) — embeds a destination ICC profile. */
51
+ export interface OutputIntentConfig {
52
+ /** `GTS_PDFX` (print) or `GTS_PDFA1` (archival). */
53
+ subtype: 'GTS_PDFX' | 'GTS_PDFA1' | string
54
+ outputConditionIdentifier: string
55
+ outputCondition?: string
56
+ registryName?: string
57
+ info?: string
58
+ /** Raw ICC profile bytes (CMYK or Gray for PDF/X). */
59
+ destOutputProfile: Uint8Array
60
+ }
61
+
62
+ /** Convert a {@link Rect} to a PDF box array `[llx lly urx ury]`. */
63
+ export function rectToBox(r: Rect): [number, number, number, number] {
64
+ return [r.x, r.y, r.x + r.w, r.y + r.h]
65
+ }
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Serialize pass: file header, body, classic cross-reference table, and
3
+ * trailer (spec §6.5). v1 emits a classic xref table only; cross-reference
4
+ * streams are deferred to v1.1 (this module is the seam for that swap).
5
+ */
6
+
7
+ import { ascii } from '../util/ascii.ts'
8
+ import type { ByteSink } from '../output/byte_sink.ts'
9
+ import type { IndirectRef } from '../objects/types.ts'
10
+ import { arr, dict, num } from '../objects/types.ts'
11
+ import { hexBytes } from '../objects/string.ts'
12
+ import { encodeObject } from '../objects/encode.ts'
13
+ import type { ObjectTable } from './object_table.ts'
14
+
15
+ // "%PDF-1.7\n" then a comment of four >=128 bytes so transfer tools treat the
16
+ // file as binary (ISO 32000-1 §7.5.2).
17
+ const HEADER = Uint8Array.from([
18
+ 0x25, 0x50, 0x44, 0x46, 0x2d, 0x31, 0x2e, 0x37, 0x0a, // %PDF-1.7\n
19
+ 0x25, 0xe2, 0xe3, 0xcf, 0xd3, 0x0a, // %âãÏÓ\n
20
+ ])
21
+
22
+ function pad10(n: number): string {
23
+ return String(n).padStart(10, '0')
24
+ }
25
+
26
+ export interface SerializeInput {
27
+ table: ObjectTable
28
+ root: IndirectRef
29
+ info: IndirectRef
30
+ /** Permanent id and per-save id (each exactly 16 bytes). */
31
+ id: [Uint8Array, Uint8Array]
32
+ sink: ByteSink
33
+ }
34
+
35
+ export function serializeDocument(input: SerializeInput): void {
36
+ const { table, root, info, id, sink } = input
37
+
38
+ sink.write(HEADER)
39
+
40
+ // Body. Offsets are recorded at the byte position of "N 0 obj".
41
+ const offsets = new Map<number, number>()
42
+ for (const [n, value] of table.entries()) {
43
+ offsets.set(n, sink.length)
44
+ sink.write(ascii(`${n} 0 obj\n`))
45
+ sink.write(encodeObject(value))
46
+ sink.write(ascii('\nendobj\n'))
47
+ }
48
+
49
+ const size = table.size // includes object 0
50
+ const xrefOffset = sink.length
51
+
52
+ // Classic xref table. Every entry line is exactly 20 bytes (incl. "\r\n").
53
+ let xref = `xref\n0 ${size}\n0000000000 65535 f\r\n`
54
+ for (let n = 1; n < size; n++) {
55
+ xref += `${pad10(offsets.get(n)!)} 00000 n\r\n`
56
+ }
57
+ sink.write(ascii(xref))
58
+
59
+ const trailer = dict({
60
+ Size: num(size),
61
+ Root: root,
62
+ Info: info,
63
+ ID: arr([hexBytes(id[0]), hexBytes(id[1])]),
64
+ })
65
+ sink.write(ascii('trailer\n'))
66
+ sink.write(encodeObject(trailer))
67
+ sink.write(ascii(`\nstartxref\n${xrefOffset}\n%%EOF\n`))
68
+ }
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Extended graphics state (spec §13). Transparency lives in an ExtGState
3
+ * object selected with the `gs` operator: stroke/fill constant alpha and a
4
+ * blend mode. The object is registered in `/Resources /ExtGState`.
5
+ */
6
+
7
+ import { dict, name, num } from '../objects/types.ts'
8
+ import type { PdfObject } from '../objects/types.ts'
9
+ import type { ObjectTable } from '../document/object_table.ts'
10
+
11
+ export type BlendMode =
12
+ | 'Normal'
13
+ | 'Multiply'
14
+ | 'Screen'
15
+ | 'Overlay'
16
+ | 'Darken'
17
+ | 'Lighten'
18
+ | 'ColorDodge'
19
+ | 'ColorBurn'
20
+ | 'HardLight'
21
+ | 'SoftLight'
22
+ | 'Difference'
23
+ | 'Exclusion'
24
+ | 'Hue'
25
+ | 'Saturation'
26
+ | 'Color'
27
+ | 'Luminosity'
28
+
29
+ export interface ExtGStateOptions {
30
+ /** Stroking constant alpha `CA` in [0,1]. */
31
+ strokeAlpha?: number
32
+ /** Non-stroking (fill) constant alpha `ca` in [0,1]. */
33
+ fillAlpha?: number
34
+ /** Blend mode `BM` (default `Normal`). */
35
+ blendMode?: BlendMode
36
+ }
37
+
38
+ class ExtGState {
39
+ readonly id: string
40
+
41
+ constructor(
42
+ private readonly opts: ExtGStateOptions,
43
+ tag: number
44
+ ) {
45
+ const parts = [
46
+ opts.strokeAlpha !== undefined ? `CA${opts.strokeAlpha}` : '',
47
+ opts.fillAlpha !== undefined ? `ca${opts.fillAlpha}` : '',
48
+ opts.blendMode ?? '',
49
+ ]
50
+ this.id = `gs:${parts.join('|')}:${tag}`
51
+ }
52
+
53
+ build(_table: ObjectTable): PdfObject {
54
+ const d = dict({ Type: name('ExtGState') })
55
+ if (this.opts.strokeAlpha !== undefined) d.entries.set('CA', num(this.opts.strokeAlpha))
56
+ if (this.opts.fillAlpha !== undefined) d.entries.set('ca', num(this.opts.fillAlpha))
57
+ if (this.opts.blendMode) d.entries.set('BM', name(this.opts.blendMode))
58
+ return d
59
+ }
60
+ }
61
+
62
+ let gsCounter = 0
63
+
64
+ /** Create an ExtGState handle (use with `ContentStream.setExtGState`). */
65
+ export function extGState(opts: ExtGStateOptions): ExtGState {
66
+ return new ExtGState(opts, gsCounter++)
67
+ }
68
+
69
+ export type { ExtGState }
@@ -0,0 +1,2 @@
1
+ export { extGState } from './ext_gstate.ts'
2
+ export type { ExtGState, ExtGStateOptions, BlendMode } from './ext_gstate.ts'
@@ -0,0 +1,123 @@
1
+ /**
2
+ * Minimal CFF (`CFF `) table parsing (spec §10.1, Adobe TN #5176).
3
+ *
4
+ * Milestone 7 embeds the CFF program **whole** (CFF subsetting is deferred per
5
+ * spec §23 decision 1), so we only need: the PostScript name, whether the CFF
6
+ * is CID-keyed, and the glyph count (CharStrings INDEX count). Charstring
7
+ * outline interpretation is out of scope until CFF subsetting lands.
8
+ */
9
+
10
+ export interface CffInfo {
11
+ /** PostScript name from the Name INDEX. */
12
+ name: string
13
+ /** True if the Top DICT has a ROS operator (CID-keyed CFF). */
14
+ isCID: boolean
15
+ /** Number of glyphs (CharStrings INDEX count). */
16
+ numGlyphs: number
17
+ }
18
+
19
+ interface IndexResult {
20
+ items: Uint8Array[]
21
+ /** Byte offset just past this INDEX. */
22
+ end: number
23
+ }
24
+
25
+ function readOffset(b: Uint8Array, pos: number, size: number): number {
26
+ let v = 0
27
+ for (let i = 0; i < size; i++) v = (v << 8) | b[pos + i]!
28
+ return v >>> 0
29
+ }
30
+
31
+ /** Read a CFF INDEX at `pos`. */
32
+ function readIndex(b: Uint8Array, pos: number): IndexResult {
33
+ const count = (b[pos]! << 8) | b[pos + 1]!
34
+ if (count === 0) return { items: [], end: pos + 2 }
35
+ const offSize = b[pos + 2]!
36
+ const offArr = pos + 3
37
+ const dataBase = offArr + (count + 1) * offSize - 1
38
+ const items: Uint8Array[] = []
39
+ for (let i = 0; i < count; i++) {
40
+ const start = readOffset(b, offArr + i * offSize, offSize)
41
+ const next = readOffset(b, offArr + (i + 1) * offSize, offSize)
42
+ items.push(b.subarray(dataBase + start, dataBase + next))
43
+ }
44
+ const end = dataBase + readOffset(b, offArr + count * offSize, offSize)
45
+ return { items, end }
46
+ }
47
+
48
+ /** Parse a Top DICT, returning the CharStrings offset and CID-keyed flag. */
49
+ function parseTopDict(d: Uint8Array): { charStrings: number; isCID: boolean } {
50
+ let charStrings = 0
51
+ let isCID = false
52
+ const operands: number[] = []
53
+ let i = 0
54
+ while (i < d.length) {
55
+ const b0 = d[i]!
56
+ if (b0 <= 21) {
57
+ // Operator (12 = escape → two-byte operator).
58
+ if (b0 === 12) {
59
+ const op2 = d[i + 1]!
60
+ if (op2 === 30) isCID = true // ROS
61
+ i += 2
62
+ } else {
63
+ if (b0 === 17) charStrings = operands[operands.length - 1] ?? 0 // CharStrings
64
+ i += 1
65
+ }
66
+ operands.length = 0
67
+ } else if (b0 === 28) {
68
+ operands.push(((d[i + 1]! << 8) | d[i + 2]!) << 16 >> 16)
69
+ i += 3
70
+ } else if (b0 === 29) {
71
+ operands.push(
72
+ ((d[i + 1]! << 24) | (d[i + 2]! << 16) | (d[i + 3]! << 8) | d[i + 4]!) | 0
73
+ )
74
+ i += 5
75
+ } else if (b0 === 30) {
76
+ // Real number: BCD nibbles until 0xf terminator.
77
+ i += 1
78
+ while (i < d.length) {
79
+ const hi = d[i]! >> 4
80
+ const lo = d[i]! & 0xf
81
+ i += 1
82
+ if (hi === 0xf || lo === 0xf) break
83
+ }
84
+ operands.push(0)
85
+ } else if (b0 >= 32 && b0 <= 246) {
86
+ operands.push(b0 - 139)
87
+ i += 1
88
+ } else if (b0 >= 247 && b0 <= 250) {
89
+ operands.push((b0 - 247) * 256 + d[i + 1]! + 108)
90
+ i += 2
91
+ } else if (b0 >= 251 && b0 <= 254) {
92
+ operands.push(-(b0 - 251) * 256 - d[i + 1]! - 108)
93
+ i += 2
94
+ } else {
95
+ i += 1 // reserved (22..27, 31, 255) — skip defensively
96
+ }
97
+ }
98
+ return { charStrings, isCID }
99
+ }
100
+
101
+ export function parseCff(cff: Uint8Array): CffInfo {
102
+ const hdrSize = cff[2]!
103
+ const nameIdx = readIndex(cff, hdrSize)
104
+ const topIdx = readIndex(cff, nameIdx.end)
105
+
106
+ const first = nameIdx.items[0]
107
+ let name = 'CFFFont'
108
+ if (first) {
109
+ let s = ''
110
+ for (const ch of first) s += String.fromCharCode(ch)
111
+ name = s
112
+ }
113
+
114
+ const top = topIdx.items[0]
115
+ if (!top) return { name, isCID: false, numGlyphs: 0 }
116
+ const { charStrings, isCID } = parseTopDict(top)
117
+
118
+ let numGlyphs = 0
119
+ if (charStrings > 0 && charStrings + 2 <= cff.length) {
120
+ numGlyphs = (cff[charStrings]! << 8) | cff[charStrings + 1]!
121
+ }
122
+ return { name, isCID, numGlyphs }
123
+ }
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Identity-H CID encoding helpers (spec §10.5). With Identity-H the CID equals
3
+ * the glyph index and codes are 2-byte big-endian. This builds the `/W` width
4
+ * array (compact consecutive runs) keyed by CID.
5
+ */
6
+
7
+ import { arr, num } from '../objects/types.ts'
8
+ import type { PdfArray } from '../objects/types.ts'
9
+
10
+ /** Encode a glyph-index sequence as 2-byte big-endian codes. */
11
+ export function encodeIdentityH(gids: number[]): Uint8Array {
12
+ const out = new Uint8Array(gids.length * 2)
13
+ for (let i = 0; i < gids.length; i++) {
14
+ out[i * 2] = (gids[i]! >> 8) & 0xff
15
+ out[i * 2 + 1] = gids[i]! & 0xff
16
+ }
17
+ return out
18
+ }
19
+
20
+ /**
21
+ * Build the CIDFont `/W` array for the used glyphs. Scales font-unit advances
22
+ * to PDF glyph space (1000-unit em). Consecutive CIDs collapse to the
23
+ * `c [w1 w2 …]` form: `[ c0 [w w w] c5 [w] … ]`.
24
+ */
25
+ export function buildWidthsArray(
26
+ usedGids: number[],
27
+ advanceFontUnits: (gid: number) => number,
28
+ unitsPerEm: number
29
+ ): PdfArray {
30
+ const sorted = [...new Set(usedGids)].sort((a, b) => a - b)
31
+ const scale = 1000 / unitsPerEm
32
+ const items = []
33
+ for (let i = 0; i < sorted.length; ) {
34
+ const start = sorted[i]!
35
+ const run: number[] = [Math.round(advanceFontUnits(start) * scale)]
36
+ let j = i
37
+ while (j + 1 < sorted.length && sorted[j + 1] === sorted[j]! + 1) {
38
+ j++
39
+ run.push(Math.round(advanceFontUnits(sorted[j]!) * scale))
40
+ }
41
+ items.push(num(start), arr(run.map(num)))
42
+ i = j + 1
43
+ }
44
+ return arr(items)
45
+ }
@@ -0,0 +1,180 @@
1
+ /**
2
+ * `cmap` table parsing (spec §10.2). Maps Unicode code points to glyph
3
+ * indices. Subtable formats 0, 4, 6 and 12 are supported — enough for the
4
+ * Unicode BMP (format 4), full Unicode (format 12), and legacy fonts.
5
+ *
6
+ * The best available Unicode subtable is selected; symbol fonts (3,0) are
7
+ * also tried with the 0xF000 private-use offset.
8
+ */
9
+
10
+ import { BinaryReader } from '../util/binary.ts'
11
+ import { UnsupportedFontError } from '../util/errors.ts'
12
+
13
+ export interface CmapLookup {
14
+ /** Glyph index for a code point, or 0 (.notdef) if unmapped. */
15
+ gidFor(codePoint: number): number
16
+ }
17
+
18
+ function parseFormat0(b: Uint8Array, o: number): CmapLookup {
19
+ const table = b.subarray(o + 6, o + 6 + 256)
20
+ return { gidFor: cp => (cp < 256 ? table[cp]! : 0) }
21
+ }
22
+
23
+ function parseFormat4(b: Uint8Array, o: number): CmapLookup {
24
+ const r = new BinaryReader(b, o)
25
+ r.seek(o + 6)
26
+ const segX2 = r.u16()
27
+ const segs = segX2 / 2
28
+ const endBase = o + 14
29
+ const startBase = endBase + segX2 + 2
30
+ const deltaBase = startBase + segX2
31
+ const rangeBase = deltaBase + segX2
32
+ const u16 = (base: number, i: number) =>
33
+ (b[base + i * 2]! << 8) | b[base + i * 2 + 1]!
34
+ return {
35
+ gidFor(cp) {
36
+ if (cp > 0xffff) return 0
37
+ for (let i = 0; i < segs; i++) {
38
+ if (cp > u16(endBase, i)) continue
39
+ const start = u16(startBase, i)
40
+ if (cp < start) return 0
41
+ const delta = u16(deltaBase, i)
42
+ const ro = u16(rangeBase, i)
43
+ if (ro === 0) return (cp + delta) & 0xffff
44
+ // glyphId address = rangeBase+i*2 + ro + (cp-start)*2
45
+ const gi = rangeBase + i * 2 + ro + (cp - start) * 2
46
+ const g = (b[gi]! << 8) | b[gi + 1]!
47
+ return g === 0 ? 0 : (g + delta) & 0xffff
48
+ }
49
+ return 0
50
+ },
51
+ }
52
+ }
53
+
54
+ function parseFormat6(b: Uint8Array, o: number): CmapLookup {
55
+ const first = (b[o + 6]! << 8) | b[o + 7]!
56
+ const count = (b[o + 8]! << 8) | b[o + 9]!
57
+ return {
58
+ gidFor(cp) {
59
+ const i = cp - first
60
+ if (i < 0 || i >= count) return 0
61
+ const a = o + 10 + i * 2
62
+ return (b[a]! << 8) | b[a + 1]!
63
+ },
64
+ }
65
+ }
66
+
67
+ function parseFormat12(b: Uint8Array, o: number): CmapLookup {
68
+ const r = new BinaryReader(b, o + 12)
69
+ const nGroups = r.u32()
70
+ const base = o + 16
71
+ return {
72
+ gidFor(cp) {
73
+ let lo = 0
74
+ let hi = nGroups - 1
75
+ while (lo <= hi) {
76
+ const mid = (lo + hi) >> 1
77
+ const g = base + mid * 12
78
+ const start = (b[g]! << 24) | (b[g + 1]! << 16) | (b[g + 2]! << 8) | b[g + 3]!
79
+ const end = (b[g + 4]! << 24) | (b[g + 5]! << 16) | (b[g + 6]! << 8) | b[g + 7]!
80
+ if (cp < start >>> 0) hi = mid - 1
81
+ else if (cp > end >>> 0) lo = mid + 1
82
+ else {
83
+ const sg = (b[g + 8]! << 24) | (b[g + 9]! << 16) | (b[g + 10]! << 8) | b[g + 11]!
84
+ return ((sg >>> 0) + (cp - (start >>> 0))) & 0xffffffff
85
+ }
86
+ }
87
+ return 0
88
+ },
89
+ }
90
+ }
91
+
92
+ /** Format 13: like 12, but every code in a group maps to the SAME glyph
93
+ * (used by LastResort and many fallback fonts). */
94
+ function parseFormat13(b: Uint8Array, o: number): CmapLookup {
95
+ const r = new BinaryReader(b, o + 12)
96
+ const nGroups = r.u32()
97
+ const base = o + 16
98
+ return {
99
+ gidFor(cp) {
100
+ let lo = 0
101
+ let hi = nGroups - 1
102
+ while (lo <= hi) {
103
+ const mid = (lo + hi) >> 1
104
+ const g = base + mid * 12
105
+ const start =
106
+ ((b[g]! << 24) | (b[g + 1]! << 16) | (b[g + 2]! << 8) | b[g + 3]!) >>> 0
107
+ const end =
108
+ ((b[g + 4]! << 24) | (b[g + 5]! << 16) | (b[g + 6]! << 8) | b[g + 7]!) >>> 0
109
+ if (cp < start) hi = mid - 1
110
+ else if (cp > end) lo = mid + 1
111
+ else return ((b[g + 8]! << 24) | (b[g + 9]! << 16) | (b[g + 10]! << 8) | b[g + 11]!) >>> 0
112
+ }
113
+ return 0
114
+ },
115
+ }
116
+ }
117
+
118
+ function parseSubtable(b: Uint8Array, o: number): CmapLookup | null {
119
+ const format = (b[o]! << 8) | b[o + 1]!
120
+ switch (format) {
121
+ case 0:
122
+ return parseFormat0(b, o)
123
+ case 4:
124
+ return parseFormat4(b, o)
125
+ case 6:
126
+ return parseFormat6(b, o)
127
+ case 12:
128
+ return parseFormat12(b, o)
129
+ case 13:
130
+ return parseFormat13(b, o)
131
+ default:
132
+ return null
133
+ }
134
+ }
135
+
136
+ /** Parse a `cmap` table and return a Unicode lookup. */
137
+ export function parseCmap(cmap: Uint8Array): CmapLookup {
138
+ const numTables = (cmap[2]! << 8) | cmap[3]!
139
+ type Cand = { platform: number; encoding: number; offset: number }
140
+ const cands: Cand[] = []
141
+ for (let i = 0; i < numTables; i++) {
142
+ const rec = 4 + i * 8
143
+ const platform = (cmap[rec]! << 8) | cmap[rec + 1]!
144
+ const encoding = (cmap[rec + 2]! << 8) | cmap[rec + 3]!
145
+ const offset =
146
+ (cmap[rec + 4]! << 24) |
147
+ (cmap[rec + 5]! << 16) |
148
+ (cmap[rec + 6]! << 8) |
149
+ cmap[rec + 7]!
150
+ cands.push({ platform, encoding, offset: offset >>> 0 })
151
+ }
152
+
153
+ // Preference: full Unicode → BMP Unicode → any Unicode → symbol.
154
+ const pick = (p: number, e: number) =>
155
+ cands.find(c => c.platform === p && c.encoding === e)
156
+ const order = [
157
+ pick(3, 10),
158
+ pick(0, 6),
159
+ pick(0, 4),
160
+ pick(3, 1),
161
+ pick(0, 3),
162
+ pick(0, 2),
163
+ pick(0, 1),
164
+ pick(0, 0),
165
+ pick(3, 0), // symbol
166
+ ]
167
+
168
+ for (const cand of order) {
169
+ if (!cand) continue
170
+ const sub = parseSubtable(cmap, cand.offset)
171
+ if (!sub) continue
172
+ if (cand.platform === 3 && cand.encoding === 0) {
173
+ // Symbol cmap: glyphs live in the 0xF000 private-use block.
174
+ return { gidFor: cp => sub.gidFor(cp) || sub.gidFor(0xf000 + (cp & 0xff)) }
175
+ }
176
+ return sub
177
+ }
178
+
179
+ throw new UnsupportedFontError('Font has no usable Unicode cmap subtable')
180
+ }