@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.
- package/README.md +79 -0
- package/package.json +51 -0
- package/src/color/cie.ts +61 -0
- package/src/color/color.ts +77 -0
- package/src/color/conversion.ts +26 -0
- package/src/color/device.ts +37 -0
- package/src/color/devicen.ts +74 -0
- package/src/color/icc.ts +103 -0
- package/src/color/index.ts +15 -0
- package/src/color/separation.ts +94 -0
- package/src/color/space.ts +47 -0
- package/src/content/content_stream.ts +373 -0
- package/src/content/graphics_state.ts +64 -0
- package/src/content/index.ts +16 -0
- package/src/content/operators.ts +70 -0
- package/src/content/path.ts +51 -0
- package/src/content/resources.ts +119 -0
- package/src/content/text_object.ts +140 -0
- package/src/document/catalog.ts +16 -0
- package/src/document/index.ts +13 -0
- package/src/document/object_table.ts +67 -0
- package/src/document/page.ts +74 -0
- package/src/document/page_tree.ts +78 -0
- package/src/document/pdf_document.ts +310 -0
- package/src/document/types.ts +65 -0
- package/src/document/xref.ts +68 -0
- package/src/ext-gstate/ext_gstate.ts +69 -0
- package/src/ext-gstate/index.ts +2 -0
- package/src/fonts/cff.ts +123 -0
- package/src/fonts/cid_encoding.ts +45 -0
- package/src/fonts/cmap_table.ts +180 -0
- package/src/fonts/font.ts +342 -0
- package/src/fonts/glyf.ts +59 -0
- package/src/fonts/hmtx.ts +21 -0
- package/src/fonts/index.ts +20 -0
- package/src/fonts/name_table.ts +50 -0
- package/src/fonts/os2.ts +41 -0
- package/src/fonts/sfnt.ts +224 -0
- package/src/fonts/standard_14.ts +132 -0
- package/src/fonts/subset.ts +221 -0
- package/src/fonts/to_unicode.ts +82 -0
- package/src/fonts/win_ansi.ts +69 -0
- package/src/images/image.ts +111 -0
- package/src/images/index.ts +6 -0
- package/src/images/jpeg.ts +103 -0
- package/src/images/png.ts +239 -0
- package/src/images/smask.ts +24 -0
- package/src/index.ts +57 -0
- package/src/metadata/index.ts +3 -0
- package/src/metadata/info_dict.ts +28 -0
- package/src/metadata/xmp.ts +110 -0
- package/src/objects/encode.ts +77 -0
- package/src/objects/index.ts +43 -0
- package/src/objects/indirect_ref.ts +17 -0
- package/src/objects/name.ts +50 -0
- package/src/objects/number.ts +43 -0
- package/src/objects/string.ts +136 -0
- package/src/objects/types.ts +86 -0
- package/src/output/buffer_sink.ts +40 -0
- package/src/output/byte_sink.ts +12 -0
- package/src/output/index.ts +3 -0
- package/src/output/stream_sink.ts +62 -0
- package/src/patterns/index.ts +10 -0
- package/src/patterns/shading.ts +162 -0
- package/src/patterns/tiling_pattern.ts +68 -0
- package/src/standards/context.ts +10 -0
- package/src/standards/index.ts +23 -0
- package/src/standards/pdf_a.ts +23 -0
- package/src/standards/pdf_x.ts +31 -0
- package/src/streams/ascii85.ts +61 -0
- package/src/streams/ascii_hex.ts +33 -0
- package/src/streams/flate.ts +17 -0
- package/src/streams/index.ts +9 -0
- package/src/streams/stream.ts +66 -0
- package/src/util/ascii.ts +63 -0
- package/src/util/binary.ts +71 -0
- package/src/util/errors.ts +61 -0
- package/src/util/index.ts +10 -0
- package/src/util/units.ts +24 -0
- 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 }
|
package/src/fonts/cff.ts
ADDED
|
@@ -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
|
+
}
|