@doccloak/core 0.12.1 → 0.13.0
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/CHANGELOG.md +333 -0
- package/NOTICE +5 -0
- package/README.md +131 -9
- package/dist/dom/errors.d.ts +1 -1
- package/dist/dom/ocr.d.ts +33 -7
- package/dist/dom/ocr.js +98 -4
- package/dist/dom/office.d.ts +20 -6
- package/dist/dom/office.js +44 -11
- package/dist/engine.js +4 -5
- package/dist/env.d.ts +3 -3
- package/dist/pdf/cmap.d.ts +83 -0
- package/dist/pdf/cmap.js +659 -0
- package/dist/pdf/encodings.d.ts +49 -0
- package/dist/pdf/encodings.js +181 -0
- package/dist/pdf/extract.d.ts +106 -0
- package/dist/pdf/extract.js +1064 -0
- package/dist/pdf/fallback-fonts.d.ts +146 -0
- package/dist/pdf/fallback-fonts.js +312 -0
- package/dist/pdf/fit.d.ts +35 -0
- package/dist/pdf/fit.js +60 -0
- package/dist/pdf/fonts.d.ts +82 -0
- package/dist/pdf/fonts.js +465 -0
- package/dist/pdf/glyphlist.d.ts +37 -0
- package/dist/pdf/glyphlist.js +485 -0
- package/dist/pdf/index.d.ts +21 -0
- package/dist/pdf/index.js +7 -0
- package/dist/pdf/lexer.d.ts +61 -0
- package/dist/pdf/lexer.js +554 -0
- package/dist/pdf/objects.d.ts +57 -0
- package/dist/pdf/objects.js +217 -0
- package/dist/pdf/oracle.d.ts +149 -0
- package/dist/pdf/oracle.js +384 -0
- package/dist/pdf/raster.d.ts +45 -0
- package/dist/pdf/raster.js +89 -0
- package/dist/pdf/read.d.ts +36 -0
- package/dist/pdf/read.js +65 -0
- package/dist/pdf/serializer.d.ts +21 -0
- package/dist/pdf/serializer.js +106 -0
- package/dist/pdf/standard-metrics.d.ts +27 -0
- package/dist/pdf/standard-metrics.js +72 -0
- package/dist/pdf/surgery.d.ts +131 -0
- package/dist/pdf/surgery.js +847 -0
- package/dist/pdf/textstate.d.ts +77 -0
- package/dist/pdf/textstate.js +280 -0
- package/dist/pdf/types.d.ts +191 -0
- package/dist/pdf/types.js +11 -0
- package/dist/pdf/verify.d.ts +33 -0
- package/dist/pdf/verify.js +446 -0
- package/dist/pdf/write.d.ts +52 -0
- package/dist/pdf/write.js +644 -0
- package/dist/pipeline.d.ts +15 -0
- package/dist/pipeline.js +123 -8
- package/dist/providers/bardsai.d.ts +1 -1
- package/dist/providers/bardsai.js +5 -1
- package/dist/providers/gliner-base.d.ts +1 -1
- package/dist/providers/gliner-base.js +5 -1
- package/dist/providers/gliner.d.ts +1 -1
- package/dist/providers/gliner.js +5 -1
- package/dist/regex/rules.data.js +13 -8
- package/dist/types.d.ts +17 -2
- package/dist/types.js +17 -0
- package/dist/worker-protocol.d.ts +11 -0
- package/dist/worker-protocol.js +41 -8
- package/fonts/liberation/LICENSE +102 -0
- package/fonts/liberation/LiberationMono-Bold.ttf +0 -0
- package/fonts/liberation/LiberationMono-BoldItalic.ttf +0 -0
- package/fonts/liberation/LiberationMono-Italic.ttf +0 -0
- package/fonts/liberation/LiberationMono-Regular.ttf +0 -0
- package/fonts/liberation/LiberationSans-Bold.ttf +0 -0
- package/fonts/liberation/LiberationSans-BoldItalic.ttf +0 -0
- package/fonts/liberation/LiberationSans-Italic.ttf +0 -0
- package/fonts/liberation/LiberationSans-Regular.ttf +0 -0
- package/fonts/liberation/LiberationSerif-Bold.ttf +0 -0
- package/fonts/liberation/LiberationSerif-BoldItalic.ttf +0 -0
- package/fonts/liberation/LiberationSerif-Italic.ttf +0 -0
- package/fonts/liberation/LiberationSerif-Regular.ttf +0 -0
- package/package.json +11 -2
- package/rules/pl.json +7 -3
- package/rules/universal.json +6 -5
package/dist/pdf/cmap.js
ADDED
|
@@ -0,0 +1,659 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @doccloak/core/pdf - CMap parser, decoder and ToUnicode serializer (T208).
|
|
3
|
+
*
|
|
4
|
+
* Handles the CMap syntax of ISO 32000-1 section 9.7.5 (embedded CMap
|
|
5
|
+
* streams) and 9.10.3 (ToUnicode CMaps): codespace ranges, cidrange /
|
|
6
|
+
* cidchar, bfchar / bfrange (both the incrementing and the array form,
|
|
7
|
+
* multi-code-unit UTF-16BE destinations with surrogates), /WMode, /CMapName.
|
|
8
|
+
* `usecmap` and predefined CMaps other than Identity-H/V are refused: the
|
|
9
|
+
* caller reports the font as unredactable rather than guessing.
|
|
10
|
+
*
|
|
11
|
+
* The parser is strict: malformed input throws CMapParseError, it never
|
|
12
|
+
* returns a partial map silently. A missing codespace section is the one
|
|
13
|
+
* tolerated defect (common in real ToUnicode CMaps): the ranges are then
|
|
14
|
+
* derived from the byte lengths of the codes seen.
|
|
15
|
+
*/
|
|
16
|
+
import { latin1, parsePdfNumber } from "./lexer.js";
|
|
17
|
+
export class CMapParseError extends Error {
|
|
18
|
+
constructor(message) {
|
|
19
|
+
super(message);
|
|
20
|
+
this.name = 'CMapParseError';
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
/** Largest number of entries one range (cidrange / bfrange) may expand to. Guards against memory bombs. */
|
|
24
|
+
export const MAX_RANGE_ENTRIES = 65536;
|
|
25
|
+
/** Upper bound for the total number of mapped codes; a 4-byte codespace could otherwise be enumerated by many ranges. */
|
|
26
|
+
const MAX_TOTAL_ENTRIES = 1 << 20;
|
|
27
|
+
const WHITESPACE = new Set([0x00, 0x09, 0x0a, 0x0c, 0x0d, 0x20]);
|
|
28
|
+
const DELIMITERS = new Set([0x28, 0x29, 0x3c, 0x3e, 0x5b, 0x5d, 0x7b, 0x7d, 0x2f, 0x25]);
|
|
29
|
+
function isWhitespace(b) {
|
|
30
|
+
return WHITESPACE.has(b);
|
|
31
|
+
}
|
|
32
|
+
function isRegular(b) {
|
|
33
|
+
return !WHITESPACE.has(b) && !DELIMITERS.has(b);
|
|
34
|
+
}
|
|
35
|
+
function hexDigit(c) {
|
|
36
|
+
if (c >= 0x30 && c <= 0x39)
|
|
37
|
+
return c - 0x30;
|
|
38
|
+
if (c >= 0x41 && c <= 0x46)
|
|
39
|
+
return c - 0x41 + 10;
|
|
40
|
+
if (c >= 0x61 && c <= 0x66)
|
|
41
|
+
return c - 0x61 + 10;
|
|
42
|
+
return -1;
|
|
43
|
+
}
|
|
44
|
+
class Tokenizer {
|
|
45
|
+
constructor(bytes) {
|
|
46
|
+
this.pos = 0;
|
|
47
|
+
this.bytes = bytes;
|
|
48
|
+
}
|
|
49
|
+
next() {
|
|
50
|
+
const b = this.bytes;
|
|
51
|
+
// whitespace and comments
|
|
52
|
+
while (this.pos < b.length) {
|
|
53
|
+
const c = b[this.pos];
|
|
54
|
+
if (isWhitespace(c))
|
|
55
|
+
this.pos++;
|
|
56
|
+
else if (c === 0x25) {
|
|
57
|
+
while (this.pos < b.length && b[this.pos] !== 0x0a && b[this.pos] !== 0x0d)
|
|
58
|
+
this.pos++;
|
|
59
|
+
}
|
|
60
|
+
else
|
|
61
|
+
break;
|
|
62
|
+
}
|
|
63
|
+
if (this.pos >= b.length)
|
|
64
|
+
return { type: 'eof' };
|
|
65
|
+
const start = this.pos;
|
|
66
|
+
const c = b[this.pos];
|
|
67
|
+
if (c === 0x5b) {
|
|
68
|
+
this.pos++;
|
|
69
|
+
return { type: 'arrayOpen' };
|
|
70
|
+
}
|
|
71
|
+
if (c === 0x5d) {
|
|
72
|
+
this.pos++;
|
|
73
|
+
return { type: 'arrayClose' };
|
|
74
|
+
}
|
|
75
|
+
if (c === 0x3c) {
|
|
76
|
+
if (b[this.pos + 1] === 0x3c) {
|
|
77
|
+
this.pos += 2;
|
|
78
|
+
return { type: 'dictOpen' };
|
|
79
|
+
}
|
|
80
|
+
return this.hexString();
|
|
81
|
+
}
|
|
82
|
+
if (c === 0x3e) {
|
|
83
|
+
if (b[this.pos + 1] === 0x3e) {
|
|
84
|
+
this.pos += 2;
|
|
85
|
+
return { type: 'dictClose' };
|
|
86
|
+
}
|
|
87
|
+
throw new CMapParseError(`stray '>' at offset ${this.pos}`);
|
|
88
|
+
}
|
|
89
|
+
if (c === 0x28)
|
|
90
|
+
return this.literalString();
|
|
91
|
+
if (c === 0x29)
|
|
92
|
+
throw new CMapParseError(`stray ')' at offset ${this.pos}`);
|
|
93
|
+
if (c === 0x2f)
|
|
94
|
+
return this.name();
|
|
95
|
+
if (c === 0x7b || c === 0x7d) {
|
|
96
|
+
this.pos++;
|
|
97
|
+
return { type: 'keyword', value: String.fromCharCode(c) };
|
|
98
|
+
}
|
|
99
|
+
while (this.pos < b.length && isRegular(b[this.pos]))
|
|
100
|
+
this.pos++;
|
|
101
|
+
const word = latin1(b.subarray(start, this.pos));
|
|
102
|
+
if (word.length === 0)
|
|
103
|
+
throw new CMapParseError(`unexpected byte 0x${c.toString(16)} at offset ${start}`);
|
|
104
|
+
const first = word.charCodeAt(0);
|
|
105
|
+
if ((first >= 0x30 && first <= 0x39) || first === 0x2b || first === 0x2d || first === 0x2e) {
|
|
106
|
+
const value = parsePdfNumber(word);
|
|
107
|
+
if (value !== null)
|
|
108
|
+
return { type: 'number', value };
|
|
109
|
+
}
|
|
110
|
+
return { type: 'keyword', value: word };
|
|
111
|
+
}
|
|
112
|
+
name() {
|
|
113
|
+
const b = this.bytes;
|
|
114
|
+
this.pos++; // '/'
|
|
115
|
+
let out = '';
|
|
116
|
+
while (this.pos < b.length && isRegular(b[this.pos])) {
|
|
117
|
+
const c = b[this.pos];
|
|
118
|
+
if (c === 0x23 && this.pos + 2 < b.length) {
|
|
119
|
+
const h1 = hexDigit(b[this.pos + 1]);
|
|
120
|
+
const h2 = hexDigit(b[this.pos + 2]);
|
|
121
|
+
if (h1 >= 0 && h2 >= 0) {
|
|
122
|
+
out += String.fromCharCode((h1 << 4) | h2);
|
|
123
|
+
this.pos += 3;
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
out += String.fromCharCode(c);
|
|
128
|
+
this.pos++;
|
|
129
|
+
}
|
|
130
|
+
return { type: 'name', value: out };
|
|
131
|
+
}
|
|
132
|
+
hexString() {
|
|
133
|
+
const b = this.bytes;
|
|
134
|
+
const start = this.pos;
|
|
135
|
+
this.pos++; // '<'
|
|
136
|
+
const digits = [];
|
|
137
|
+
for (;;) {
|
|
138
|
+
if (this.pos >= b.length)
|
|
139
|
+
throw new CMapParseError(`unterminated hex string at offset ${start}`);
|
|
140
|
+
const c = b[this.pos];
|
|
141
|
+
if (c === 0x3e)
|
|
142
|
+
break;
|
|
143
|
+
const d = hexDigit(c);
|
|
144
|
+
if (d >= 0)
|
|
145
|
+
digits.push(d);
|
|
146
|
+
else if (!isWhitespace(c))
|
|
147
|
+
throw new CMapParseError(`invalid hex digit 0x${c.toString(16)} at offset ${this.pos}`);
|
|
148
|
+
this.pos++;
|
|
149
|
+
}
|
|
150
|
+
this.pos++; // '>'
|
|
151
|
+
if (digits.length % 2 === 1)
|
|
152
|
+
digits.push(0);
|
|
153
|
+
const bytes = new Uint8Array(digits.length / 2);
|
|
154
|
+
for (let i = 0; i < bytes.length; i++)
|
|
155
|
+
bytes[i] = (digits[2 * i] << 4) | digits[2 * i + 1];
|
|
156
|
+
return { type: 'string', bytes };
|
|
157
|
+
}
|
|
158
|
+
literalString() {
|
|
159
|
+
const b = this.bytes;
|
|
160
|
+
const start = this.pos;
|
|
161
|
+
this.pos++; // '('
|
|
162
|
+
let depth = 1;
|
|
163
|
+
const out = [];
|
|
164
|
+
while (this.pos < b.length) {
|
|
165
|
+
const c = b[this.pos++];
|
|
166
|
+
if (c === 0x5c) {
|
|
167
|
+
const n = b[this.pos++];
|
|
168
|
+
switch (n) {
|
|
169
|
+
case 0x6e:
|
|
170
|
+
out.push(0x0a);
|
|
171
|
+
break;
|
|
172
|
+
case 0x72:
|
|
173
|
+
out.push(0x0d);
|
|
174
|
+
break;
|
|
175
|
+
case 0x74:
|
|
176
|
+
out.push(0x09);
|
|
177
|
+
break;
|
|
178
|
+
case 0x62:
|
|
179
|
+
out.push(0x08);
|
|
180
|
+
break;
|
|
181
|
+
case 0x66:
|
|
182
|
+
out.push(0x0c);
|
|
183
|
+
break;
|
|
184
|
+
case 0x0d:
|
|
185
|
+
if (b[this.pos] === 0x0a)
|
|
186
|
+
this.pos++;
|
|
187
|
+
break;
|
|
188
|
+
case 0x0a: break;
|
|
189
|
+
default: {
|
|
190
|
+
if (n >= 0x30 && n <= 0x37) {
|
|
191
|
+
let v = n - 0x30;
|
|
192
|
+
for (let k = 0; k < 2 && b[this.pos] >= 0x30 && b[this.pos] <= 0x37; k++)
|
|
193
|
+
v = v * 8 + (b[this.pos++] - 0x30);
|
|
194
|
+
out.push(v & 0xff);
|
|
195
|
+
}
|
|
196
|
+
else if (n !== undefined) {
|
|
197
|
+
out.push(n);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
else if (c === 0x28) {
|
|
203
|
+
depth++;
|
|
204
|
+
out.push(c);
|
|
205
|
+
}
|
|
206
|
+
else if (c === 0x29) {
|
|
207
|
+
depth--;
|
|
208
|
+
if (depth === 0)
|
|
209
|
+
return { type: 'string', bytes: Uint8Array.from(out) };
|
|
210
|
+
out.push(c);
|
|
211
|
+
}
|
|
212
|
+
else {
|
|
213
|
+
out.push(c);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
throw new CMapParseError(`unterminated literal string at offset ${start}`);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
const SECTION_KEYWORDS = new Set([
|
|
220
|
+
'begincodespacerange', 'endcodespacerange',
|
|
221
|
+
'begincidrange', 'endcidrange',
|
|
222
|
+
'begincidchar', 'endcidchar',
|
|
223
|
+
'beginbfchar', 'endbfchar',
|
|
224
|
+
'beginbfrange', 'endbfrange',
|
|
225
|
+
'beginnotdefrange', 'endnotdefrange',
|
|
226
|
+
'beginnotdefchar', 'endnotdefchar',
|
|
227
|
+
]);
|
|
228
|
+
function bigEndian(bytes) {
|
|
229
|
+
let v = 0;
|
|
230
|
+
for (let i = 0; i < bytes.length; i++)
|
|
231
|
+
v = v * 256 + bytes[i];
|
|
232
|
+
return v;
|
|
233
|
+
}
|
|
234
|
+
/** UTF-16BE destination bytes -> JS string (a JS string is UTF-16, so surrogate pairs come out as one code point). */
|
|
235
|
+
function decodeUtf16BE(bytes) {
|
|
236
|
+
let s = '';
|
|
237
|
+
let i = 0;
|
|
238
|
+
for (; i + 1 < bytes.length; i += 2)
|
|
239
|
+
s += String.fromCharCode((bytes[i] << 8) | bytes[i + 1]);
|
|
240
|
+
// A trailing single byte (1-byte destination, or an odd-length one) is one code unit.
|
|
241
|
+
if (i < bytes.length)
|
|
242
|
+
s += String.fromCharCode(bytes[i]);
|
|
243
|
+
return s;
|
|
244
|
+
}
|
|
245
|
+
/** Increment the last UTF-16 code unit of a destination string (bfrange incrementing form). */
|
|
246
|
+
function incrementLastUnit(dst, by) {
|
|
247
|
+
if (dst.length === 0)
|
|
248
|
+
return dst;
|
|
249
|
+
const last = dst.charCodeAt(dst.length - 1) + by;
|
|
250
|
+
if (last > 0xffff)
|
|
251
|
+
throw new CMapParseError('bfrange destination overflows a UTF-16 code unit');
|
|
252
|
+
return dst.slice(0, -1) + String.fromCharCode(last);
|
|
253
|
+
}
|
|
254
|
+
class Parser {
|
|
255
|
+
constructor(bytes) {
|
|
256
|
+
this.stack = [];
|
|
257
|
+
this.totalEntries = 0;
|
|
258
|
+
this.codespace = [];
|
|
259
|
+
this.cidMap = null;
|
|
260
|
+
this.unicodeMap = null;
|
|
261
|
+
this.vertical = false;
|
|
262
|
+
this.codeLengths = new Set();
|
|
263
|
+
this.tok = new Tokenizer(bytes);
|
|
264
|
+
}
|
|
265
|
+
run() {
|
|
266
|
+
for (;;) {
|
|
267
|
+
const t = this.tok.next();
|
|
268
|
+
if (t.type === 'eof')
|
|
269
|
+
break;
|
|
270
|
+
if (t.type === 'keyword')
|
|
271
|
+
this.keyword(t.value);
|
|
272
|
+
else
|
|
273
|
+
this.stack.push(this.operand(t));
|
|
274
|
+
if (this.stack.length > 64)
|
|
275
|
+
this.stack.splice(0, this.stack.length - 64);
|
|
276
|
+
}
|
|
277
|
+
// A missing endcmap is tolerated (pdf.js keeps the map it has read); an unterminated section is not.
|
|
278
|
+
this.deriveCodespace();
|
|
279
|
+
}
|
|
280
|
+
/** Read one operand (composites included) from a token already fetched. */
|
|
281
|
+
operand(t) {
|
|
282
|
+
switch (t.type) {
|
|
283
|
+
case 'number': return { kind: 'number', value: t.value };
|
|
284
|
+
case 'name': return { kind: 'name', value: t.value };
|
|
285
|
+
case 'string': return { kind: 'string', bytes: t.bytes };
|
|
286
|
+
case 'arrayOpen': return this.array();
|
|
287
|
+
case 'dictOpen': return this.dict();
|
|
288
|
+
case 'arrayClose': throw new CMapParseError("stray ']'");
|
|
289
|
+
case 'dictClose': throw new CMapParseError("stray '>>'");
|
|
290
|
+
case 'keyword': throw new CMapParseError(`unexpected keyword '${t.value}' inside a composite`);
|
|
291
|
+
case 'eof': throw new CMapParseError('unexpected end of CMap inside a composite');
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
array() {
|
|
295
|
+
const items = [];
|
|
296
|
+
for (;;) {
|
|
297
|
+
const t = this.tok.next();
|
|
298
|
+
if (t.type === 'arrayClose')
|
|
299
|
+
return { kind: 'array', items };
|
|
300
|
+
if (t.type === 'eof')
|
|
301
|
+
throw new CMapParseError('unterminated array');
|
|
302
|
+
items.push(this.operand(t));
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
dict() {
|
|
306
|
+
let depth = 1;
|
|
307
|
+
while (depth > 0) {
|
|
308
|
+
const t = this.tok.next();
|
|
309
|
+
if (t.type === 'eof')
|
|
310
|
+
throw new CMapParseError('unterminated dictionary');
|
|
311
|
+
if (t.type === 'dictOpen')
|
|
312
|
+
depth++;
|
|
313
|
+
else if (t.type === 'dictClose')
|
|
314
|
+
depth--;
|
|
315
|
+
else if (t.type === 'keyword' && SECTION_KEYWORDS.has(t.value))
|
|
316
|
+
throw new CMapParseError(`'${t.value}' inside a dictionary`);
|
|
317
|
+
}
|
|
318
|
+
return { kind: 'dict' };
|
|
319
|
+
}
|
|
320
|
+
keyword(kw) {
|
|
321
|
+
switch (kw) {
|
|
322
|
+
case 'begincmap':
|
|
323
|
+
case 'endcmap':
|
|
324
|
+
break;
|
|
325
|
+
case 'usecmap':
|
|
326
|
+
throw new CMapParseError('usecmap not supported');
|
|
327
|
+
case 'def':
|
|
328
|
+
this.def();
|
|
329
|
+
break;
|
|
330
|
+
case 'begincodespacerange':
|
|
331
|
+
this.section('endcodespacerange', 2, (items) => this.codespaceEntry(items));
|
|
332
|
+
break;
|
|
333
|
+
case 'begincidrange':
|
|
334
|
+
this.section('endcidrange', 3, (items) => this.cidRangeEntry(items));
|
|
335
|
+
break;
|
|
336
|
+
case 'begincidchar':
|
|
337
|
+
this.section('endcidchar', 2, (items) => this.cidCharEntry(items));
|
|
338
|
+
break;
|
|
339
|
+
case 'beginbfchar':
|
|
340
|
+
this.section('endbfchar', 2, (items) => this.bfCharEntry(items));
|
|
341
|
+
break;
|
|
342
|
+
case 'beginbfrange':
|
|
343
|
+
this.section('endbfrange', 3, (items) => this.bfRangeEntry(items));
|
|
344
|
+
break;
|
|
345
|
+
case 'beginnotdefrange':
|
|
346
|
+
this.section('endnotdefrange', 3, () => undefined);
|
|
347
|
+
break;
|
|
348
|
+
case 'beginnotdefchar':
|
|
349
|
+
this.section('endnotdefchar', 2, () => undefined);
|
|
350
|
+
break;
|
|
351
|
+
default:
|
|
352
|
+
if (SECTION_KEYWORDS.has(kw))
|
|
353
|
+
throw new CMapParseError(`'${kw}' without a matching begin`);
|
|
354
|
+
// PostScript plumbing (findresource, dict, begin, end, pop, ...): ignored.
|
|
355
|
+
break;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
def() {
|
|
359
|
+
if (this.stack.length < 2)
|
|
360
|
+
return;
|
|
361
|
+
const value = this.stack.pop();
|
|
362
|
+
const key = this.stack.pop();
|
|
363
|
+
if (key.kind !== 'name')
|
|
364
|
+
return;
|
|
365
|
+
if (key.value === 'WMode' && value.kind === 'number')
|
|
366
|
+
this.vertical = value.value === 1;
|
|
367
|
+
else if (key.value === 'CMapName' && value.kind === 'name')
|
|
368
|
+
this.name = value.value;
|
|
369
|
+
}
|
|
370
|
+
/** Read `arity`-tuples of operands until `endKeyword`; every entry goes through `handle`. */
|
|
371
|
+
section(endKeyword, arity, handle) {
|
|
372
|
+
const beginKeyword = 'begin' + endKeyword.slice(3);
|
|
373
|
+
this.stack.length = 0;
|
|
374
|
+
const items = [];
|
|
375
|
+
for (;;) {
|
|
376
|
+
const t = this.tok.next();
|
|
377
|
+
if (t.type === 'eof')
|
|
378
|
+
throw new CMapParseError(`unterminated ${beginKeyword} section`);
|
|
379
|
+
if (t.type === 'keyword') {
|
|
380
|
+
if (t.value === endKeyword) {
|
|
381
|
+
if (items.length !== 0)
|
|
382
|
+
throw new CMapParseError(`incomplete entry before ${endKeyword}`);
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
throw new CMapParseError(`unexpected keyword '${t.value}' inside ${beginKeyword}`);
|
|
386
|
+
}
|
|
387
|
+
items.push(this.operand(t));
|
|
388
|
+
if (items.length === arity) {
|
|
389
|
+
handle(items);
|
|
390
|
+
items.length = 0;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
code(op, what) {
|
|
395
|
+
if (op.kind !== 'string')
|
|
396
|
+
throw new CMapParseError(`${what} must be a hex string`);
|
|
397
|
+
const length = op.bytes.length;
|
|
398
|
+
if (length < 1 || length > 4)
|
|
399
|
+
throw new CMapParseError(`${what} must be 1..4 bytes, got ${length}`);
|
|
400
|
+
return { value: bigEndian(op.bytes), length };
|
|
401
|
+
}
|
|
402
|
+
range(lo, hi, what) {
|
|
403
|
+
const l = this.code(lo, `${what} low`);
|
|
404
|
+
const h = this.code(hi, `${what} high`);
|
|
405
|
+
// Bounds of different byte lengths (<00> <FFFF>): pdf.js takes the high bound's length.
|
|
406
|
+
const length = Math.max(l.length, h.length);
|
|
407
|
+
if (h.value < l.value)
|
|
408
|
+
throw new CMapParseError(`${what} high < low`);
|
|
409
|
+
return { low: l.value, high: h.value, length };
|
|
410
|
+
}
|
|
411
|
+
budget(count) {
|
|
412
|
+
if (count > MAX_RANGE_ENTRIES)
|
|
413
|
+
throw new CMapParseError(`range of ${count} entries exceeds the ${MAX_RANGE_ENTRIES} limit`);
|
|
414
|
+
this.totalEntries += count;
|
|
415
|
+
if (this.totalEntries > MAX_TOTAL_ENTRIES)
|
|
416
|
+
throw new CMapParseError('CMap maps too many codes');
|
|
417
|
+
}
|
|
418
|
+
integer(op, what) {
|
|
419
|
+
if (op.kind !== 'number' || !Number.isInteger(op.value) || op.value < 0)
|
|
420
|
+
throw new CMapParseError(`${what} must be a non-negative integer`);
|
|
421
|
+
return op.value;
|
|
422
|
+
}
|
|
423
|
+
codespaceEntry([lo, hi]) {
|
|
424
|
+
const r = this.range(lo, hi, 'codespace range');
|
|
425
|
+
this.codespace.push({ numBytes: r.length, low: r.low, high: r.high });
|
|
426
|
+
this.codeLengths.add(r.length);
|
|
427
|
+
}
|
|
428
|
+
cidRangeEntry([lo, hi, dst]) {
|
|
429
|
+
const r = this.range(lo, hi, 'cidrange');
|
|
430
|
+
const cid = this.integer(dst, 'cidrange destination');
|
|
431
|
+
this.budget(r.high - r.low + 1);
|
|
432
|
+
const map = (this.cidMap ?? (this.cidMap = new Map()));
|
|
433
|
+
for (let c = r.low; c <= r.high; c++)
|
|
434
|
+
map.set(c, cid + (c - r.low));
|
|
435
|
+
this.codeLengths.add(r.length);
|
|
436
|
+
}
|
|
437
|
+
cidCharEntry([src, dst]) {
|
|
438
|
+
const c = this.code(src, 'cidchar code');
|
|
439
|
+
const cid = this.integer(dst, 'cidchar destination');
|
|
440
|
+
this.budget(1);
|
|
441
|
+
(this.cidMap ?? (this.cidMap = new Map())).set(c.value, cid);
|
|
442
|
+
this.codeLengths.add(c.length);
|
|
443
|
+
}
|
|
444
|
+
destination(op, what) {
|
|
445
|
+
if (op.kind !== 'string')
|
|
446
|
+
throw new CMapParseError(`${what} must be a hex string`);
|
|
447
|
+
return decodeUtf16BE(op.bytes);
|
|
448
|
+
}
|
|
449
|
+
bfCharEntry([src, dst]) {
|
|
450
|
+
const c = this.code(src, 'bfchar code');
|
|
451
|
+
const text = this.destination(dst, 'bfchar destination');
|
|
452
|
+
this.budget(1);
|
|
453
|
+
(this.unicodeMap ?? (this.unicodeMap = new Map())).set(c.value, text);
|
|
454
|
+
this.codeLengths.add(c.length);
|
|
455
|
+
}
|
|
456
|
+
bfRangeEntry([lo, hi, dst]) {
|
|
457
|
+
const r = this.range(lo, hi, 'bfrange');
|
|
458
|
+
const count = r.high - r.low + 1;
|
|
459
|
+
this.budget(count);
|
|
460
|
+
const map = (this.unicodeMap ?? (this.unicodeMap = new Map()));
|
|
461
|
+
if (dst.kind === 'array') {
|
|
462
|
+
// pdf.js maps what is there when the array is shorter or longer than the range.
|
|
463
|
+
const n = Math.min(count, dst.items.length);
|
|
464
|
+
for (let i = 0; i < n; i++)
|
|
465
|
+
map.set(r.low + i, this.destination(dst.items[i], 'bfrange array item'));
|
|
466
|
+
}
|
|
467
|
+
else {
|
|
468
|
+
const base = this.destination(dst, 'bfrange destination');
|
|
469
|
+
for (let i = 0; i < count; i++)
|
|
470
|
+
map.set(r.low + i, incrementLastUnit(base, i));
|
|
471
|
+
}
|
|
472
|
+
this.codeLengths.add(r.length);
|
|
473
|
+
}
|
|
474
|
+
/** No codespacerange section: cover the full range of every code length that appeared. */
|
|
475
|
+
deriveCodespace() {
|
|
476
|
+
if (this.codespace.length > 0)
|
|
477
|
+
return;
|
|
478
|
+
const lengths = [...this.codeLengths].sort((a, b) => a - b);
|
|
479
|
+
for (const n of lengths)
|
|
480
|
+
this.codespace.push({ numBytes: n, low: 0, high: 2 ** (8 * n) - 1 });
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
/** Parse an embedded CMap / ToUnicode stream. Throws CMapParseError on malformed input or `usecmap`. */
|
|
484
|
+
export function parseCMap(bytes) {
|
|
485
|
+
const p = new Parser(bytes);
|
|
486
|
+
p.run();
|
|
487
|
+
const cmap = {
|
|
488
|
+
codespace: p.codespace,
|
|
489
|
+
cidMap: p.cidMap,
|
|
490
|
+
unicodeMap: p.unicodeMap,
|
|
491
|
+
vertical: p.vertical,
|
|
492
|
+
codeLengths: p.codeLengths,
|
|
493
|
+
};
|
|
494
|
+
if (p.name !== undefined)
|
|
495
|
+
cmap.name = p.name;
|
|
496
|
+
return cmap;
|
|
497
|
+
}
|
|
498
|
+
/** The 2-byte Identity CMap: every code is its own CID. */
|
|
499
|
+
export function identityCMap(vertical) {
|
|
500
|
+
return {
|
|
501
|
+
codespace: [{ numBytes: 2, low: 0, high: 0xffff }],
|
|
502
|
+
cidMap: null,
|
|
503
|
+
unicodeMap: null,
|
|
504
|
+
vertical,
|
|
505
|
+
name: vertical ? 'Identity-V' : 'Identity-H',
|
|
506
|
+
codeLengths: new Set([2]),
|
|
507
|
+
};
|
|
508
|
+
}
|
|
509
|
+
/** Only Identity-H / Identity-V are built in; any other predefined name is refused by the caller. */
|
|
510
|
+
export function predefinedCMap(name) {
|
|
511
|
+
if (name === 'Identity-H')
|
|
512
|
+
return identityCMap(false);
|
|
513
|
+
if (name === 'Identity-V')
|
|
514
|
+
return identityCMap(true);
|
|
515
|
+
return undefined;
|
|
516
|
+
}
|
|
517
|
+
/**
|
|
518
|
+
* Split a string operand into character codes (ISO 32000-1 9.7.6.3). For each
|
|
519
|
+
* position the shortest byte length whose codespace range contains the value
|
|
520
|
+
* wins; with no full match, the shortest range that matches on the first byte
|
|
521
|
+
* decides the length; otherwise 1 byte. Unmapped codes are still returned, the
|
|
522
|
+
* caller treats them as notdef. A code cut short by the end of the string takes
|
|
523
|
+
* the remaining bytes.
|
|
524
|
+
*/
|
|
525
|
+
export function splitCodes(cmap, bytes) {
|
|
526
|
+
const out = [];
|
|
527
|
+
const ranges = cmap.codespace;
|
|
528
|
+
let fixed = 0;
|
|
529
|
+
if (ranges.length === 0)
|
|
530
|
+
fixed = cmap.codeLengths.size === 1 ? [...cmap.codeLengths][0] : 1;
|
|
531
|
+
let offset = 0;
|
|
532
|
+
while (offset < bytes.length) {
|
|
533
|
+
let length = 0;
|
|
534
|
+
if (fixed > 0) {
|
|
535
|
+
length = fixed;
|
|
536
|
+
}
|
|
537
|
+
else {
|
|
538
|
+
let value = 0;
|
|
539
|
+
for (let n = 1; n <= 4 && length === 0; n++) {
|
|
540
|
+
if (offset + n > bytes.length)
|
|
541
|
+
break;
|
|
542
|
+
value = value * 256 + bytes[offset + n - 1];
|
|
543
|
+
for (const r of ranges) {
|
|
544
|
+
if (r.numBytes === n && value >= r.low && value <= r.high) {
|
|
545
|
+
length = n;
|
|
546
|
+
break;
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
if (length === 0) {
|
|
551
|
+
const first = bytes[offset];
|
|
552
|
+
for (const r of ranges) {
|
|
553
|
+
const shift = 2 ** (8 * (r.numBytes - 1));
|
|
554
|
+
const lowFirst = Math.floor(r.low / shift);
|
|
555
|
+
const highFirst = Math.floor(r.high / shift);
|
|
556
|
+
if (first >= lowFirst && first <= highFirst && (length === 0 || r.numBytes < length))
|
|
557
|
+
length = r.numBytes;
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
if (length === 0)
|
|
561
|
+
length = 1;
|
|
562
|
+
}
|
|
563
|
+
if (offset + length > bytes.length)
|
|
564
|
+
length = bytes.length - offset;
|
|
565
|
+
out.push({ code: bigEndian(bytes.subarray(offset, offset + length)), length, offset });
|
|
566
|
+
offset += length;
|
|
567
|
+
}
|
|
568
|
+
return out;
|
|
569
|
+
}
|
|
570
|
+
/** CID of a code: identity when the CMap has no cid entries, 0 (notdef) when the code is unmapped. */
|
|
571
|
+
export function cidOf(cmap, code) {
|
|
572
|
+
if (cmap.cidMap === null)
|
|
573
|
+
return code;
|
|
574
|
+
return cmap.cidMap.get(code) ?? 0;
|
|
575
|
+
}
|
|
576
|
+
/** Unicode text of a code from a ToUnicode CMap; undefined when unmapped (or when the CMap has no bf entries). */
|
|
577
|
+
export function unicodeOf(cmap, code) {
|
|
578
|
+
return cmap.unicodeMap?.get(code);
|
|
579
|
+
}
|
|
580
|
+
function hex(value, bytes) {
|
|
581
|
+
return value.toString(16).toUpperCase().padStart(bytes * 2, '0');
|
|
582
|
+
}
|
|
583
|
+
function hexUtf16BE(text) {
|
|
584
|
+
let s = '';
|
|
585
|
+
for (let i = 0; i < text.length; i++)
|
|
586
|
+
s += hex(text.charCodeAt(i), 2);
|
|
587
|
+
return s;
|
|
588
|
+
}
|
|
589
|
+
function asciiBytes(text) {
|
|
590
|
+
const out = new Uint8Array(text.length);
|
|
591
|
+
for (let i = 0; i < text.length; i++)
|
|
592
|
+
out[i] = text.charCodeAt(i) & 0xff;
|
|
593
|
+
return out;
|
|
594
|
+
}
|
|
595
|
+
/**
|
|
596
|
+
* Write a ToUnicode CMap holding exactly the given codes. One full-width
|
|
597
|
+
* codespace range per code length present, bfchar entries in groups of at most
|
|
598
|
+
* 100, UTF-16BE destinations. Output is deterministic (sorted by length, then
|
|
599
|
+
* code; later duplicates win) and reads back with parseCMap to the same map.
|
|
600
|
+
*/
|
|
601
|
+
export function serializeToUnicode(entries, options) {
|
|
602
|
+
const byKey = new Map();
|
|
603
|
+
for (const e of entries) {
|
|
604
|
+
if (!Number.isInteger(e.length) || e.length < 1 || e.length > 4)
|
|
605
|
+
throw new CMapParseError(`code length must be 1..4, got ${e.length}`);
|
|
606
|
+
const max = 2 ** (8 * e.length) - 1;
|
|
607
|
+
if (!Number.isInteger(e.code) || e.code < 0 || e.code > max)
|
|
608
|
+
throw new CMapParseError(`code ${e.code} does not fit in ${e.length} byte(s)`);
|
|
609
|
+
byKey.set(`${e.length}:${e.code}`, e);
|
|
610
|
+
}
|
|
611
|
+
const sorted = [...byKey.values()].filter((e) => e.unicode.length > 0).sort((a, b) => a.length - b.length || a.code - b.code);
|
|
612
|
+
const lengths = [...new Set(sorted.map((e) => e.length))].sort((a, b) => a - b);
|
|
613
|
+
const name = options?.name ?? 'DocCloak-ToUnicode';
|
|
614
|
+
const lines = [
|
|
615
|
+
'/CIDInit /ProcSet findresource begin',
|
|
616
|
+
'12 dict begin',
|
|
617
|
+
'begincmap',
|
|
618
|
+
'/CIDSystemInfo << /Registry (Adobe) /Ordering (UCS) /Supplement 0 >> def',
|
|
619
|
+
`/CMapName /${escapeName(name)} def`,
|
|
620
|
+
'/CMapType 2 def',
|
|
621
|
+
];
|
|
622
|
+
// Codespace: the source CMap's ranges when given (they cannot overlap, unlike full ranges of
|
|
623
|
+
// different byte lengths), else the full range of every code length that occurs.
|
|
624
|
+
const source = (options?.codespace ?? []).filter((r) => lengths.includes(r.numBytes));
|
|
625
|
+
const covered = (e) => source.some((r) => r.numBytes === e.length && e.code >= r.low && e.code <= r.high);
|
|
626
|
+
if (source.length > 0 && sorted.every(covered)) {
|
|
627
|
+
lines.push(`${source.length} begincodespacerange`);
|
|
628
|
+
for (const r of source)
|
|
629
|
+
lines.push(`<${hex(r.low, r.numBytes)}> <${hex(r.high, r.numBytes)}>`);
|
|
630
|
+
lines.push('endcodespacerange');
|
|
631
|
+
}
|
|
632
|
+
else if (lengths.length > 0) {
|
|
633
|
+
lines.push(`${lengths.length} begincodespacerange`);
|
|
634
|
+
for (const n of lengths)
|
|
635
|
+
lines.push(`<${hex(0, n)}> <${hex(2 ** (8 * n) - 1, n)}>`);
|
|
636
|
+
lines.push('endcodespacerange');
|
|
637
|
+
}
|
|
638
|
+
for (let i = 0; i < sorted.length; i += 100) {
|
|
639
|
+
const group = sorted.slice(i, i + 100);
|
|
640
|
+
lines.push(`${group.length} beginbfchar`);
|
|
641
|
+
for (const e of group)
|
|
642
|
+
lines.push(`<${hex(e.code, e.length)}> <${hexUtf16BE(e.unicode)}>`);
|
|
643
|
+
lines.push('endbfchar');
|
|
644
|
+
}
|
|
645
|
+
lines.push('endcmap', 'CMapName currentdict /CMap defineresource pop', 'end', 'end', '');
|
|
646
|
+
return asciiBytes(lines.join('\n'));
|
|
647
|
+
}
|
|
648
|
+
/** PDF name escaping (#xx for delimiters, whitespace and non-ASCII bytes). */
|
|
649
|
+
function escapeName(name) {
|
|
650
|
+
let out = '';
|
|
651
|
+
for (let i = 0; i < name.length; i++) {
|
|
652
|
+
const c = name.charCodeAt(i) & 0xff;
|
|
653
|
+
if (c < 0x21 || c > 0x7e || DELIMITERS.has(c) || c === 0x23)
|
|
654
|
+
out += '#' + hex(c, 1);
|
|
655
|
+
else
|
|
656
|
+
out += String.fromCharCode(c);
|
|
657
|
+
}
|
|
658
|
+
return out;
|
|
659
|
+
}
|