@muhammara/native-core 7.0.0-beta.4 → 7.0.0-rc.2

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 (44) hide show
  1. package/README.md +57 -7
  2. package/THIRD_PARTY_NOTICES.md +39 -0
  3. package/index.js +120 -1
  4. package/lib/PDFRStreamForBuffer.js +52 -7
  5. package/lib/PDFRStreamForFile.js +56 -7
  6. package/lib/PDFStreamForResponse.js +22 -5
  7. package/lib/PDFWStreamForBuffer.js +49 -10
  8. package/lib/PDFWStreamForFile.js +26 -5
  9. package/lib/Recipe.js +91 -38
  10. package/lib/content-stream.js +503 -0
  11. package/lib/css-colors.js +149 -0
  12. package/lib/font-text.js +1461 -0
  13. package/lib/glyph-list.js +1127 -0
  14. package/lib/recipe/annotation.js +464 -101
  15. package/lib/recipe/appendPage.js +2 -0
  16. package/lib/recipe/colors.js +132 -35
  17. package/lib/recipe/coordinate.js +82 -16
  18. package/lib/recipe/encrypt.js +32 -13
  19. package/lib/recipe/font.js +70 -25
  20. package/lib/recipe/graphics-state.js +3 -2
  21. package/lib/recipe/htmlToTextObjects.js +121 -24
  22. package/lib/recipe/image.js +385 -39
  23. package/lib/recipe/info.js +93 -22
  24. package/lib/recipe/insertPage.js +12 -3
  25. package/lib/recipe/overlay.js +4 -2
  26. package/lib/recipe/page-references.js +436 -0
  27. package/lib/recipe/page.js +363 -262
  28. package/lib/recipe/parameters.js +8 -1
  29. package/lib/recipe/replaceText.js +464 -30
  30. package/lib/recipe/shapes.js +418 -45
  31. package/lib/recipe/split.js +6 -2
  32. package/lib/recipe/table.js +41 -8
  33. package/lib/recipe/text.helper.js +172 -8
  34. package/lib/recipe/text.js +294 -56
  35. package/lib/recipe/utils.js +17 -6
  36. package/lib/recipe/vector-line.js +101 -39
  37. package/lib/recipe/vector-polygon.js +18 -6
  38. package/lib/recipe/vector.helper.js +156 -14
  39. package/lib/recipe/vector.js +156 -52
  40. package/lib/recipe/xObjectForm.js +73 -10
  41. package/lib/recipe-constants.js +373 -0
  42. package/lib/recipe-options.js +47 -0
  43. package/muhammara.d.ts +3501 -160
  44. package/package.json +2 -3
@@ -0,0 +1,1461 @@
1
+ // Decodes text-showing operand bytes through a page font and encodes text back
2
+ // into the same font. Mirrored in packages/wasm/lib/font-text.js; keep both
3
+ // files identical apart from the module syntax.
4
+
5
+ var contentStream = require("./content-stream");
6
+ var glyphs = require("./glyph-list").glyphs;
7
+
8
+ // PDF dictionary keys this module reads.
9
+ var PdfName = Object.freeze({
10
+ BASE_ENCODING: "BaseEncoding",
11
+ BASE_FONT: "BaseFont",
12
+ DIFFERENCES: "Differences",
13
+ ENCODING: "Encoding",
14
+ FIRST_CHAR: "FirstChar",
15
+ FONT: "Font",
16
+ FONT_DESCRIPTOR: "FontDescriptor",
17
+ FONT_FILE: "FontFile",
18
+ FONT_FILE3: "FontFile3",
19
+ LAST_CHAR: "LastChar",
20
+ SUBTYPE: "Subtype",
21
+ TO_UNICODE: "ToUnicode",
22
+ WIDTHS: "Widths",
23
+ });
24
+
25
+ // Standard 14 fonts with their own built-in, non-Latin encodings. Their codes
26
+ // decode only through a `/ToUnicode` CMap or `/Differences`.
27
+ var SymbolicStandardFont = Object.freeze({
28
+ SYMBOL: "Symbol",
29
+ ZAPF_DINGBATS: "ZapfDingbats",
30
+ });
31
+
32
+ // Limits on the font data one decoding call reads, so untrusted fonts cannot
33
+ // make decoding inflate or parse unbounded data. A larger ToUnicode CMap is
34
+ // ignored, and the font decodes through its encoding instead.
35
+ var MAX_TO_UNICODE_BYTES = 4 * 1024 * 1024;
36
+ var MAX_CALL_TO_UNICODE_BYTES = 32 * 1024 * 1024;
37
+ // Most `bfrange` codes one call may expand across all its CMaps. A CMap that
38
+ // would pass it is ignored as a whole.
39
+ var MAX_CALL_RANGE_ENTRIES = 0x200000;
40
+ // Most fonts, and most decode-map entries across them, a reader keeps cached;
41
+ // the least recently used are dropped first.
42
+ var MAX_CACHED_FONTS = 256;
43
+ var MAX_CACHED_ENTRIES = 0x200000;
44
+ var MAX_DIFFERENCES_ITEMS = 512;
45
+
46
+ // Most PDF objects one decoding call reads when the caller sets no tighter
47
+ // `maxParsedObjects` limit; matches the extractor's ceiling.
48
+ var MAX_PARSED_OBJECTS = 1000000;
49
+
50
+ // Font subtypes that change how codes are read.
51
+ var FontSubtype = Object.freeze({
52
+ TYPE0: "Type0",
53
+ TYPE1: "Type1",
54
+ TYPE3: "Type3",
55
+ MM_TYPE1: "MMType1",
56
+ TRUE_TYPE: "TrueType",
57
+ });
58
+
59
+ // Font encoding names this module interprets.
60
+ var FontEncoding = Object.freeze({
61
+ WIN_ANSI: "WinAnsiEncoding",
62
+ MAC_ROMAN: "MacRomanEncoding",
63
+ STANDARD: "StandardEncoding",
64
+ IDENTITY_H: "Identity-H",
65
+ IDENTITY_V: "Identity-V",
66
+ });
67
+
68
+ // ToUnicode CMap sections this module reads.
69
+ var CMapSection = Object.freeze({
70
+ CODESPACE_RANGE: "codespacerange",
71
+ BF_CHAR: "bfchar",
72
+ BF_RANGE: "bfrange",
73
+ });
74
+
75
+ // Keywords that open and close each CMap section.
76
+ var CMapKeyword = Object.freeze({
77
+ BEGIN_CODESPACE_RANGE: "begincodespacerange",
78
+ END_CODESPACE_RANGE: "endcodespacerange",
79
+ BEGIN_BF_CHAR: "beginbfchar",
80
+ END_BF_CHAR: "endbfchar",
81
+ BEGIN_BF_RANGE: "beginbfrange",
82
+ END_BF_RANGE: "endbfrange",
83
+ });
84
+
85
+ var SECTION_BY_BEGIN_KEYWORD = new Map([
86
+ [CMapKeyword.BEGIN_CODESPACE_RANGE, CMapSection.CODESPACE_RANGE],
87
+ [CMapKeyword.BEGIN_BF_CHAR, CMapSection.BF_CHAR],
88
+ [CMapKeyword.BEGIN_BF_RANGE, CMapSection.BF_RANGE],
89
+ ]);
90
+
91
+ var END_KEYWORDS = new Set([
92
+ CMapKeyword.END_CODESPACE_RANGE,
93
+ CMapKeyword.END_BF_CHAR,
94
+ CMapKeyword.END_BF_RANGE,
95
+ ]);
96
+
97
+ // ToUnicode CMap token kinds.
98
+ var CMapToken = Object.freeze({
99
+ HEX: "hex",
100
+ NAME: "name",
101
+ KEYWORD: "keyword",
102
+ ARRAY_START: "[",
103
+ ARRAY_END: "]",
104
+ });
105
+
106
+ var ASCII_NAMES = (
107
+ "space exclam quotedbl numbersign dollar percent ampersand quotesingle " +
108
+ "parenleft parenright asterisk plus comma hyphen period slash zero one two " +
109
+ "three four five six seven eight nine colon semicolon less equal greater " +
110
+ "question at A B C D E F G H I J K L M N O P Q R S T U V W X Y Z " +
111
+ "bracketleft backslash bracketright asciicircum underscore grave a b c d e " +
112
+ "f g h i j k l m n o p q r s t u v w x y z braceleft bar braceright " +
113
+ "asciitilde"
114
+ ).split(" ");
115
+
116
+ var LATIN1_NAMES = (
117
+ "nbspace exclamdown cent sterling currency yen brokenbar section dieresis " +
118
+ "copyright ordfeminine guillemotleft logicalnot sfthyphen registered macron " +
119
+ "degree plusminus twosuperior threesuperior acute mu paragraph " +
120
+ "periodcentered cedilla onesuperior ordmasculine guillemotright onequarter " +
121
+ "onehalf threequarters questiondown Agrave Aacute Acircumflex Atilde " +
122
+ "Adieresis Aring AE Ccedilla Egrave Eacute Ecircumflex Edieresis Igrave " +
123
+ "Iacute Icircumflex Idieresis Eth Ntilde Ograve Oacute Ocircumflex Otilde " +
124
+ "Odieresis multiply Oslash Ugrave Uacute Ucircumflex Udieresis Yacute Thorn " +
125
+ "germandbls agrave aacute acircumflex atilde adieresis aring ae ccedilla " +
126
+ "egrave eacute ecircumflex edieresis igrave iacute icircumflex idieresis " +
127
+ "eth ntilde ograve oacute ocircumflex otilde odieresis divide oslash ugrave " +
128
+ "uacute ucircumflex udieresis yacute thorn ydieresis"
129
+ ).split(" ");
130
+
131
+ /**
132
+ * Build a 256-entry glyph-name table from ASCII plus high-byte overrides.
133
+ *
134
+ * @param {Object<number, string>} overrides Code-to-name overrides.
135
+ * @param {string[]} [high] Names for codes 0x80 to 0xFF, `.` for unused.
136
+ * @returns {Array<string|undefined>} Glyph names indexed by code.
137
+ */
138
+ function encodingTable(overrides, high) {
139
+ var table = new Array(256);
140
+ var index;
141
+ for (index = 0; index < ASCII_NAMES.length; index++) {
142
+ table[0x20 + index] = ASCII_NAMES[index];
143
+ }
144
+ for (index = 0; high && index < high.length; index++) {
145
+ table[0x80 + index] = high[index] === "." ? undefined : high[index];
146
+ }
147
+ return Object.assign(table, overrides);
148
+ }
149
+
150
+ var WIN_ANSI = encodingTable(
151
+ {},
152
+ (
153
+ "Euro . quotesinglbase florin quotedblbase ellipsis dagger daggerdbl " +
154
+ "circumflex perthousand Scaron guilsinglleft OE . Zcaron . . quoteleft " +
155
+ "quoteright quotedblleft quotedblright bullet endash emdash tilde " +
156
+ "trademark scaron guilsinglright oe . zcaron Ydieresis space"
157
+ )
158
+ .split(" ")
159
+ .concat(LATIN1_NAMES.slice(1)),
160
+ );
161
+ WIN_ANSI[0xad] = "hyphen";
162
+
163
+ var MAC_ROMAN = encodingTable(
164
+ {},
165
+ (
166
+ "Adieresis Aring Ccedilla Eacute Ntilde Odieresis Udieresis aacute " +
167
+ "agrave acircumflex adieresis atilde aring ccedilla eacute egrave " +
168
+ "ecircumflex edieresis iacute igrave icircumflex idieresis ntilde oacute " +
169
+ "ograve ocircumflex odieresis otilde uacute ugrave ucircumflex udieresis " +
170
+ "dagger degree cent sterling section bullet paragraph germandbls " +
171
+ "registered copyright trademark acute dieresis notequal AE Oslash " +
172
+ "infinity plusminus lessequal greaterequal yen mu partialdiff summation " +
173
+ "product pi integral ordfeminine ordmasculine Omega ae oslash " +
174
+ "questiondown exclamdown logicalnot radical florin approxequal Delta " +
175
+ "guillemotleft guillemotright ellipsis space Agrave Atilde Otilde OE oe " +
176
+ "endash emdash quotedblleft quotedblright quoteleft quoteright divide " +
177
+ "lozenge ydieresis Ydieresis fraction currency guilsinglleft " +
178
+ "guilsinglright fi fl daggerdbl periodcentered quotesinglbase " +
179
+ "quotedblbase perthousand Acircumflex Ecircumflex Aacute Edieresis " +
180
+ "Egrave Iacute Icircumflex Idieresis Igrave Oacute Ocircumflex apple " +
181
+ "Ograve Uacute Ucircumflex Ugrave dotlessi circumflex tilde macron breve " +
182
+ "dotaccent ring cedilla hungarumlaut ogonek caron"
183
+ ).split(" "),
184
+ );
185
+
186
+ var STANDARD = encodingTable({
187
+ 0x27: "quoteright",
188
+ 0x60: "quoteleft",
189
+ 0xa1: "exclamdown",
190
+ 0xa2: "cent",
191
+ 0xa3: "sterling",
192
+ 0xa4: "fraction",
193
+ 0xa5: "yen",
194
+ 0xa6: "florin",
195
+ 0xa7: "section",
196
+ 0xa8: "currency",
197
+ 0xa9: "quotesingle",
198
+ 0xaa: "quotedblleft",
199
+ 0xab: "guillemotleft",
200
+ 0xac: "guilsinglleft",
201
+ 0xad: "guilsinglright",
202
+ 0xae: "fi",
203
+ 0xaf: "fl",
204
+ 0xb1: "endash",
205
+ 0xb2: "dagger",
206
+ 0xb3: "daggerdbl",
207
+ 0xb4: "periodcentered",
208
+ 0xb6: "paragraph",
209
+ 0xb7: "bullet",
210
+ 0xb8: "quotesinglbase",
211
+ 0xb9: "quotedblbase",
212
+ 0xba: "quotedblright",
213
+ 0xbb: "guillemotright",
214
+ 0xbc: "ellipsis",
215
+ 0xbd: "perthousand",
216
+ 0xbf: "questiondown",
217
+ 0xc1: "grave",
218
+ 0xc2: "acute",
219
+ 0xc3: "circumflex",
220
+ 0xc4: "tilde",
221
+ 0xc5: "macron",
222
+ 0xc6: "breve",
223
+ 0xc7: "dotaccent",
224
+ 0xc8: "dieresis",
225
+ 0xca: "ring",
226
+ 0xcb: "cedilla",
227
+ 0xcd: "hungarumlaut",
228
+ 0xce: "ogonek",
229
+ 0xcf: "caron",
230
+ 0xd0: "emdash",
231
+ 0xe1: "AE",
232
+ 0xe3: "ordfeminine",
233
+ 0xe8: "Lslash",
234
+ 0xe9: "Oslash",
235
+ 0xea: "OE",
236
+ 0xeb: "ordmasculine",
237
+ 0xf1: "ae",
238
+ 0xf5: "dotlessi",
239
+ 0xf8: "lslash",
240
+ 0xf9: "oslash",
241
+ 0xfa: "oe",
242
+ 0xfb: "germandbls",
243
+ });
244
+
245
+ var BASE_ENCODINGS = new Map([
246
+ [FontEncoding.WIN_ANSI, WIN_ANSI],
247
+ [FontEncoding.MAC_ROMAN, MAC_ROMAN],
248
+ [FontEncoding.STANDARD, STANDARD],
249
+ ]);
250
+
251
+ /**
252
+ * Map a glyph name to Unicode text, following the Adobe Glyph List naming
253
+ * rules for `uniXXXX`, `uXXXX`, suffixes, and ligature components.
254
+ *
255
+ * @param {string} name Glyph name.
256
+ * @returns {string|undefined} Unicode text, or undefined when unknown.
257
+ */
258
+ function glyphNameToUnicode(name) {
259
+ var base = name.split(".")[0];
260
+ if (base.indexOf("_") !== -1) {
261
+ var parts = base.split("_").map(glyphNameToUnicode);
262
+ return parts.every(Boolean) ? parts.join("") : undefined;
263
+ }
264
+ var known = glyphs();
265
+ if (base in known) return known[base];
266
+ var match = /^uni((?:[0-9A-F]{4})+)$/.exec(base);
267
+ if (match) {
268
+ var text = "";
269
+ for (var offset = 0; offset < match[1].length; offset += 4) {
270
+ var unit = parseInt(match[1].slice(offset, offset + 4), 16);
271
+ // The glyph list rules exclude surrogates.
272
+ if (isSurrogate(unit)) return undefined;
273
+ text += String.fromCharCode(unit);
274
+ }
275
+ return text;
276
+ }
277
+ match = /^u([0-9A-F]{4,6})$/.exec(base);
278
+ if (match) {
279
+ var codePoint = parseInt(match[1], 16);
280
+ if (codePoint <= 0x10ffff && !isSurrogate(codePoint)) {
281
+ return String.fromCodePoint(codePoint);
282
+ }
283
+ }
284
+ return undefined;
285
+ }
286
+
287
+ /**
288
+ * Check whether a code point is a UTF-16 surrogate.
289
+ *
290
+ * @param {number} codePoint Code point.
291
+ * @returns {boolean} True for U+D800 to U+DFFF.
292
+ */
293
+ function isSurrogate(codePoint) {
294
+ return codePoint >= 0xd800 && codePoint <= 0xdfff;
295
+ }
296
+
297
+ /**
298
+ * Decode big-endian UTF-16 bytes.
299
+ *
300
+ * @param {string} bytes One-byte string.
301
+ * @returns {string} Decoded text.
302
+ */
303
+ function utf16be(bytes) {
304
+ var text = "";
305
+ for (var index = 0; index + 1 < bytes.length; index += 2) {
306
+ text += String.fromCharCode(
307
+ (bytes.charCodeAt(index) << 8) | bytes.charCodeAt(index + 1),
308
+ );
309
+ }
310
+ return text;
311
+ }
312
+
313
+ /**
314
+ * Split a CMap into tokens: hex strings, names, brackets, and keywords.
315
+ *
316
+ * @param {string} source One-byte CMap stream.
317
+ * @returns {Array<{type: string, value: string}>} Tokens.
318
+ */
319
+ function cmapTokens(source) {
320
+ var tokens = [];
321
+ // Every branch matches in one pass: an unclosed literal string runs to the
322
+ // end instead of being rescanned from each "(", which would be quadratic.
323
+ var pattern =
324
+ /%[^\r\n]*|<([0-9A-Fa-f\s]*)>|\/([^\s()<>[\]{}/%]*)|([[\]])|\((?:\\[\s\S]|[^\\)])*\)?|([^\s()<>[\]{}/%]+)/g;
325
+ var match;
326
+ while ((match = pattern.exec(source))) {
327
+ if (match[1] !== undefined) {
328
+ tokens.push({
329
+ type: CMapToken.HEX,
330
+ value: contentStream.hexBytes(match[1]),
331
+ });
332
+ } else if (match[2] !== undefined) {
333
+ tokens.push({ type: CMapToken.NAME, value: match[2] });
334
+ } else if (match[3] !== undefined) {
335
+ tokens.push({ type: match[3], value: match[3] });
336
+ } else if (match[4] !== undefined) {
337
+ tokens.push({ type: CMapToken.KEYWORD, value: match[4] });
338
+ }
339
+ }
340
+ return tokens;
341
+ }
342
+
343
+ /**
344
+ * Add one to the last UTF-16 code unit of a destination string.
345
+ *
346
+ * @param {string} value Destination text.
347
+ * @param {number} offset Amount to add.
348
+ * @returns {string} Offset text.
349
+ */
350
+ function offsetDestination(value, offset) {
351
+ return (
352
+ value.slice(0, -1) +
353
+ String.fromCharCode(value.charCodeAt(value.length - 1) + offset)
354
+ );
355
+ }
356
+
357
+ /**
358
+ * Format a code as a one-byte string of the given length.
359
+ *
360
+ * @param {number} code Numeric code.
361
+ * @param {number} length Byte length.
362
+ * @returns {string} One-byte string.
363
+ */
364
+ function codeBytes(code, length) {
365
+ var bytes = "";
366
+ for (var index = length - 1; index >= 0; index--) {
367
+ bytes += String.fromCharCode((code >>> (index * 8)) & 0xff);
368
+ }
369
+ return bytes;
370
+ }
371
+
372
+ /**
373
+ * Read a code from a one-byte string.
374
+ *
375
+ * @param {string} bytes One-byte string.
376
+ * @returns {number} Numeric code.
377
+ */
378
+ function codeValue(bytes) {
379
+ var code = 0;
380
+ for (var index = 0; index < bytes.length; index++) {
381
+ code = code * 256 + bytes.charCodeAt(index);
382
+ }
383
+ return code;
384
+ }
385
+
386
+ // Upper bound on the codes all `bfrange` entries of one CMap may expand to,
387
+ // so a hostile CMap cannot stall decoding. Two-byte codes need at most 0x10000.
388
+ var MAX_RANGE_ENTRIES = 0x20000;
389
+
390
+ /**
391
+ * Parse a ToUnicode CMap.
392
+ *
393
+ * @param {string} source One-byte CMap stream.
394
+ * @returns {{codespace: Array<{low: string, high: string}>, map: Map<string,
395
+ * string>, rangeEntries: number}} Codespace ranges, code-to-text mappings
396
+ * keyed by code bytes, and how many codes its `bfrange` entries expanded to.
397
+ */
398
+ function parseToUnicode(source) {
399
+ var tokens = cmapTokens(source);
400
+ var codespace = [];
401
+ var map = new Map();
402
+ var mode = null;
403
+ var operands = [];
404
+ var rangeBudget = MAX_RANGE_ENTRIES;
405
+
406
+ /**
407
+ * Apply one CMap token to the section being read.
408
+ *
409
+ * @param {{type: string, value: string}} token CMap token.
410
+ * @returns {void}
411
+ */
412
+ function readToken(token) {
413
+ if (token.type === CMapToken.KEYWORD) {
414
+ if (SECTION_BY_BEGIN_KEYWORD.has(token.value)) {
415
+ mode = SECTION_BY_BEGIN_KEYWORD.get(token.value);
416
+ operands = [];
417
+ } else if (END_KEYWORDS.has(token.value)) {
418
+ mode = null;
419
+ }
420
+ return;
421
+ }
422
+ if (!mode) return;
423
+ if (mode === CMapSection.BF_RANGE && token.type === CMapToken.ARRAY_START) {
424
+ operands.push([]);
425
+ return;
426
+ }
427
+ if (mode === CMapSection.BF_RANGE && token.type === CMapToken.ARRAY_END) {
428
+ flushRange();
429
+ return;
430
+ }
431
+ var last = operands[operands.length - 1];
432
+ if (
433
+ mode === CMapSection.BF_RANGE &&
434
+ Array.isArray(last) &&
435
+ operands.length === 3
436
+ ) {
437
+ last.push(token);
438
+ return;
439
+ }
440
+ operands.push(token);
441
+
442
+ if (mode === CMapSection.CODESPACE_RANGE && operands.length === 2) {
443
+ if (operands[0].value.length === operands[1].value.length) {
444
+ codespace.push({ low: operands[0].value, high: operands[1].value });
445
+ }
446
+ operands = [];
447
+ } else if (mode === CMapSection.BF_CHAR && operands.length === 2) {
448
+ var text = destinationText(operands[1]);
449
+ if (text !== undefined) map.set(operands[0].value, text);
450
+ operands = [];
451
+ } else if (
452
+ mode === CMapSection.BF_RANGE &&
453
+ operands.length === 3 &&
454
+ !Array.isArray(operands[2])
455
+ ) {
456
+ flushRange();
457
+ }
458
+ }
459
+
460
+ tokens.forEach(readToken);
461
+
462
+ /** Apply the collected `bfrange` operands. */
463
+ function flushRange() {
464
+ var low = operands[0];
465
+ var high = operands[1];
466
+ var destination = operands[2];
467
+ operands = [];
468
+ if (
469
+ !low ||
470
+ !high ||
471
+ !destination ||
472
+ low.type !== CMapToken.HEX ||
473
+ high.type !== CMapToken.HEX
474
+ ) {
475
+ return;
476
+ }
477
+ var length = low.value.length;
478
+ var start = codeValue(low.value);
479
+ var end = Math.min(codeValue(high.value), start + rangeBudget - 1);
480
+ rangeBudget -= Math.max(0, end - start + 1);
481
+ var first = Array.isArray(destination)
482
+ ? undefined
483
+ : destinationText(destination);
484
+ for (var code = start; code <= end; code++) {
485
+ var text;
486
+ if (Array.isArray(destination)) {
487
+ var item = destination[code - start];
488
+ text = item && destinationText(item);
489
+ } else if (destination.type === CMapToken.HEX) {
490
+ text =
491
+ first === undefined
492
+ ? undefined
493
+ : offsetDestination(first, code - start);
494
+ } else {
495
+ text = code === start ? destinationText(destination) : undefined;
496
+ }
497
+ if (text !== undefined) map.set(codeBytes(code, length), text);
498
+ }
499
+ }
500
+
501
+ return {
502
+ codespace: codespace,
503
+ map: map,
504
+ rangeEntries: MAX_RANGE_ENTRIES - rangeBudget,
505
+ };
506
+ }
507
+
508
+ /**
509
+ * Convert a `bfchar` or `bfrange` destination token to text.
510
+ *
511
+ * @param {{type: string, value: string}} token Destination token.
512
+ * @returns {string|undefined} Text, or undefined when not decodable.
513
+ */
514
+ function destinationText(token) {
515
+ if (token.type === CMapToken.NAME) return glyphNameToUnicode(token.value);
516
+ if (token.type !== CMapToken.HEX || !token.value.length) return undefined;
517
+ // Destinations are UTF-16BE; some producers write one byte, such as <41>.
518
+ var bytes = token.value.length % 2 ? "\x00" + token.value : token.value;
519
+ return utf16be(bytes);
520
+ }
521
+
522
+ /**
523
+ * Split operand bytes into character codes.
524
+ *
525
+ * @param {string} bytes One-byte operand string.
526
+ * @param {Array<{low: string, high: string}>} codespace Codespace ranges.
527
+ * @param {number} fallbackLength Code length without a matching range.
528
+ * @returns {string[]} Codes as one-byte strings.
529
+ */
530
+ function splitCodes(bytes, codespace, fallbackLength) {
531
+ var codes = [];
532
+ var index = 0;
533
+ while (index < bytes.length) {
534
+ var length = 0;
535
+ for (var size = 1; size <= 4 && !length; size++) {
536
+ var candidate = bytes.substr(index, size);
537
+ if (candidate.length !== size) break;
538
+ for (var range = 0; range < codespace.length; range++) {
539
+ if (inCodespace(candidate, codespace[range])) {
540
+ length = size;
541
+ break;
542
+ }
543
+ }
544
+ }
545
+ length = length || fallbackLength;
546
+ codes.push(bytes.substr(index, length));
547
+ index += length;
548
+ }
549
+ return codes;
550
+ }
551
+
552
+ /**
553
+ * Check whether a code lies in a codespace range, byte by byte.
554
+ *
555
+ * @param {string} code Code bytes.
556
+ * @param {{low: string, high: string}} range Codespace range.
557
+ * @returns {boolean} True when every byte lies between the range bounds.
558
+ */
559
+ function inCodespace(code, range) {
560
+ if (code.length !== range.low.length) return false;
561
+ for (var index = 0; index < code.length; index++) {
562
+ var byte = code.charCodeAt(index);
563
+ if (
564
+ byte < range.low.charCodeAt(index) ||
565
+ byte > range.high.charCodeAt(index)
566
+ ) {
567
+ return false;
568
+ }
569
+ }
570
+ return true;
571
+ }
572
+
573
+ /**
574
+ * Create a codec that decodes and encodes text for one font.
575
+ *
576
+ * @param {object} description Font description.
577
+ * @param {boolean} description.composite True for Type0 fonts.
578
+ * @param {boolean} description.identity True for Type0 fonts with an
579
+ * `Identity-H` or `Identity-V` encoding.
580
+ * @param {object|null} description.toUnicode Parsed ToUnicode CMap.
581
+ * @param {string|null} description.baseEncoding Simple-font base encoding
582
+ * name, or null when the font's built-in encoding is unknown.
583
+ * @param {Array<number|string>} description.differences Flattened
584
+ * `/Differences` array.
585
+ * @param {function(): {values: number[]|null, firstChar: number, malformed:
586
+ * boolean}} description.readWidths Reads a simple font's `/Widths`; called on
587
+ * the first encode, since only encoding needs them.
588
+ * @returns {{decode: function(string): string, encode: function(string,
589
+ * string): string, size: number}} Font codec; `encode` takes the font resource
590
+ * name for its errors, and `size` is its decode map's entry count.
591
+ */
592
+ function createFontCodec(description) {
593
+ var toUnicode = description.toUnicode || {
594
+ codespace: [],
595
+ map: new Map(),
596
+ rangeEntries: 0,
597
+ };
598
+ var codeLength = description.composite ? 2 : 1;
599
+ var codespace = toUnicode.codespace;
600
+ if (!description.composite) {
601
+ codespace = [{ low: "\x00", high: "\xff" }];
602
+ } else if (description.identity) {
603
+ // Identity-H and Identity-V read every code as two bytes, whatever
604
+ // codespace the ToUnicode CMap declares.
605
+ codespace = [{ low: "\x00\x00", high: "\xff\xff" }];
606
+ }
607
+ // A composite font decodes through its CMap alone, so it shares the parsed
608
+ // map. A simple font reads one-byte codes, so it keeps just those.
609
+ var decodeMap = description.composite ? toUnicode.map : new Map();
610
+
611
+ if (!description.composite) {
612
+ toUnicode.map.forEach(addOneByteCode);
613
+ var names =
614
+ description.baseEncoding === null
615
+ ? new Array(256)
616
+ : (BASE_ENCODINGS.get(description.baseEncoding) || STANDARD).slice();
617
+ var code = 0;
618
+ for (var index = 0; index < description.differences.length; index++) {
619
+ var entry = description.differences[index];
620
+ if (typeof entry === "number") {
621
+ code = entry;
622
+ } else if (code < 256) {
623
+ names[code++] = entry;
624
+ }
625
+ }
626
+ for (code = 0; code < names.length; code++) {
627
+ var bytes = String.fromCharCode(code);
628
+ var text = names[code] && glyphNameToUnicode(names[code]);
629
+ if (text !== undefined && !decodeMap.has(bytes)) {
630
+ decodeMap.set(bytes, text);
631
+ }
632
+ }
633
+ }
634
+
635
+ // Built on the first encode; extraction only decodes.
636
+ var encodeMap = null;
637
+ var longestText = 1;
638
+ var widths = null;
639
+
640
+ /**
641
+ * Copy a one-byte ToUnicode mapping into a simple font's decode map.
642
+ *
643
+ * @param {string} text Decoded text.
644
+ * @param {string} bytes Code bytes.
645
+ * @returns {void}
646
+ */
647
+ function addOneByteCode(text, bytes) {
648
+ if (bytes.length === 1) decodeMap.set(bytes, text);
649
+ }
650
+
651
+ /**
652
+ * Read the font's widths once, when encoding first needs them.
653
+ *
654
+ * @returns {{values: number[]|null, firstChar: number, malformed: boolean}}
655
+ * Widths.
656
+ */
657
+ function fontWidths() {
658
+ if (!widths) widths = description.readWidths();
659
+ return widths;
660
+ }
661
+
662
+ /**
663
+ * Record the first code that shows `text`, so encoding can reverse the
664
+ * decode map. Codes without a glyph in the font are skipped.
665
+ *
666
+ * @param {string} text Decoded text.
667
+ * @param {string} bytes Code bytes.
668
+ * @returns {void}
669
+ */
670
+ function addEncoding(text, bytes) {
671
+ if (!text || encodeMap.has(text) || !hasGlyph(bytes)) return;
672
+ encodeMap.set(text, bytes);
673
+ longestText = Math.max(longestText, text.length);
674
+ }
675
+
676
+ /**
677
+ * Check a simple-font code against `/Widths`; subset fonts write zero for
678
+ * glyphs they leave out.
679
+ *
680
+ * @param {string} bytes Code bytes.
681
+ * @returns {boolean} True when the font can show the code.
682
+ */
683
+ function hasGlyph(bytes) {
684
+ if (description.composite) return true;
685
+ var font = fontWidths();
686
+ if (!font.values) return true;
687
+ var width = font.values[bytes.charCodeAt(0) - font.firstChar];
688
+ return typeof width === "number" && width > 0;
689
+ }
690
+
691
+ /**
692
+ * Decode operand bytes to Unicode text.
693
+ *
694
+ * @param {string} bytes Operand bytes, one character per byte.
695
+ * @returns {string} Text; codes the font does not map become U+FFFD.
696
+ */
697
+ function decode(bytes) {
698
+ var codes = splitCodes(bytes, codespace, codeLength);
699
+ var text = "";
700
+ for (var index = 0; index < codes.length; index++) {
701
+ var decoded = decodeMap.get(codes[index]);
702
+ text += decoded === undefined ? "�" : decoded;
703
+ }
704
+ return text;
705
+ }
706
+
707
+ /**
708
+ * Encode text to operand bytes, preferring the longest mapped text at each
709
+ * position so ligature glyphs are reused.
710
+ *
711
+ * @param {string} text Unicode text.
712
+ * @param {string} fontName Font resource name, for errors.
713
+ * @returns {string} Operand bytes, one character per byte.
714
+ * @throws {Error} If the font's `/Widths` is malformed or the font has no
715
+ * glyph for a character.
716
+ */
717
+ function encode(text, fontName) {
718
+ if (!description.composite && fontWidths().malformed) {
719
+ throw new Error("font " + fontName + " has a malformed /Widths array");
720
+ }
721
+ if (!encodeMap) {
722
+ encodeMap = new Map();
723
+ decodeMap.forEach(addEncoding);
724
+ }
725
+ var bytes = "";
726
+ var missing = [];
727
+ var index = 0;
728
+ while (index < text.length) {
729
+ var matched = false;
730
+ for (
731
+ var size = Math.min(longestText, text.length - index);
732
+ size > 0;
733
+ size--
734
+ ) {
735
+ var code = encodeMap.get(text.substr(index, size));
736
+ if (code !== undefined) {
737
+ bytes += code;
738
+ index += size;
739
+ matched = true;
740
+ break;
741
+ }
742
+ }
743
+ if (!matched) {
744
+ var character = String.fromCodePoint(text.codePointAt(index));
745
+ var quoted = JSON.stringify(character);
746
+ if (missing.indexOf(quoted) === -1) missing.push(quoted);
747
+ index += character.length;
748
+ }
749
+ }
750
+ if (missing.length) {
751
+ throw new Error(
752
+ "font " + fontName + " has no glyph for " + missing.join(", "),
753
+ );
754
+ }
755
+ return bytes;
756
+ }
757
+
758
+ return { decode: decode, encode: encode, size: decodeMap.size };
759
+ }
760
+
761
+ /**
762
+ * Decode operand bytes of a font that cannot be resolved or read.
763
+ *
764
+ * @param {string} bytes Operand bytes, one character per byte.
765
+ * @returns {string} One U+FFFD per byte.
766
+ */
767
+ function decodeUnreadable(bytes) {
768
+ return "�".repeat(bytes.length);
769
+ }
770
+
771
+ /**
772
+ * Refuse to encode text for a font that cannot be resolved or read.
773
+ *
774
+ * @param {string} text Unicode text.
775
+ * @param {string|null} fontName Font resource name, or null when no font is
776
+ * selected.
777
+ * @returns {never} Never returns.
778
+ * @throws {Error} Always.
779
+ */
780
+ function encodeUnreadable(text, fontName) {
781
+ throw new Error(
782
+ fontName === null
783
+ ? "no font is selected"
784
+ : "font " + fontName + " cannot be read",
785
+ );
786
+ }
787
+
788
+ /**
789
+ * Codec for text whose font cannot be resolved or read.
790
+ */
791
+ var UNREADABLE_CODEC = Object.freeze({
792
+ decode: decodeUnreadable,
793
+ encode: encodeUnreadable,
794
+ size: 0,
795
+ });
796
+
797
+ /**
798
+ * Read a resolved dictionary entry.
799
+ *
800
+ * @param {object} reader PDF reader or parser.
801
+ * @param {object} dictionary PDF dictionary.
802
+ * @param {string} key Entry name.
803
+ * @returns {object|null} Resolved object, or null when missing.
804
+ */
805
+ function entry(reader, dictionary, key) {
806
+ if (!dictionary || !dictionary.exists(key)) return null;
807
+ return reader.queryDictionaryObject(dictionary, key) || null;
808
+ }
809
+
810
+ /**
811
+ * Read a resolved array as a list of PDF objects, skipping items that cannot
812
+ * be resolved.
813
+ *
814
+ * @param {object} reader PDF reader or parser.
815
+ * @param {object} types PDF object type constants.
816
+ * @param {object|null} array PDF array.
817
+ * @param {number} limit Most items to read.
818
+ * @returns {Array<object|undefined>} Resolved items; unresolvable items are
819
+ * undefined.
820
+ */
821
+ function arrayItems(reader, types, array, limit) {
822
+ var items = [];
823
+ if (!array || array.getType() !== types.ePDFObjectArray) return items;
824
+ var length = Math.min(array.getLength(), limit);
825
+ for (var index = 0; index < length; index++) {
826
+ items.push(reader.queryArrayObject(array, index));
827
+ }
828
+ return items;
829
+ }
830
+
831
+ /**
832
+ * Check whether a PDF object is a number.
833
+ *
834
+ * @param {object} types PDF object type constants.
835
+ * @param {object|undefined} item PDF object.
836
+ * @returns {boolean} True for integers and reals.
837
+ */
838
+ function isNumber(types, item) {
839
+ return Boolean(
840
+ item &&
841
+ (item.getType() === types.ePDFObjectInteger ||
842
+ item.getType() === types.ePDFObjectReal),
843
+ );
844
+ }
845
+
846
+ /**
847
+ * Check whether a simple font's codes follow a built-in encoding that cannot
848
+ * be read here: the Symbol and ZapfDingbats standard fonts, Type 1 fonts whose
849
+ * program is embedded, and Type 3 fonts, which have no base encoding. Their codes decode only through `/ToUnicode` or
850
+ * `/Differences`. Other fonts, including TrueType subsets flagged Symbolic,
851
+ * fall back to a standard encoding.
852
+ *
853
+ * @param {object} reader PDF reader or parser.
854
+ * @param {object} types PDF object type constants.
855
+ * @param {object} font Font dictionary.
856
+ * @param {string|null} subtypeName Font subtype.
857
+ * @returns {boolean} True when the built-in encoding is unknown.
858
+ */
859
+ function usesBuiltInEncoding(reader, types, font, subtypeName) {
860
+ var baseFont = entry(reader, font, PdfName.BASE_FONT);
861
+ if (baseFont && baseFont.getType() === types.ePDFObjectName) {
862
+ var name = baseFont.value.replace(/^[A-Z]{6}\+/, "");
863
+ if (Object.values(SymbolicStandardFont).includes(name)) return true;
864
+ }
865
+ // A Type 3 font's codes mean only what its /Differences names.
866
+ if (subtypeName === FontSubtype.TYPE3) return true;
867
+ if (
868
+ subtypeName !== FontSubtype.TYPE1 &&
869
+ subtypeName !== FontSubtype.MM_TYPE1
870
+ ) {
871
+ return false;
872
+ }
873
+ var descriptor = entry(reader, font, PdfName.FONT_DESCRIPTOR);
874
+ return Boolean(
875
+ descriptor &&
876
+ descriptor.getType() === types.ePDFObjectDictionary &&
877
+ (descriptor.exists(PdfName.FONT_FILE) ||
878
+ descriptor.exists(PdfName.FONT_FILE3)),
879
+ );
880
+ }
881
+
882
+ /**
883
+ * Read a `/Differences` array as its codes and glyph names, skipping items
884
+ * that are neither.
885
+ *
886
+ * @param {object} reader PDF reader or parser.
887
+ * @param {object} types PDF object type constants.
888
+ * @param {object|null} array `/Differences` array.
889
+ * @returns {Array<number|string>} Codes and glyph names in order.
890
+ */
891
+ function differencesEntries(reader, types, array) {
892
+ var items = arrayItems(reader, types, array, MAX_DIFFERENCES_ITEMS);
893
+ var entries = [];
894
+ for (var index = 0; index < items.length; index++) {
895
+ var item = items[index];
896
+ if (isNumber(types, item)) {
897
+ entries.push(Number(item.value));
898
+ } else if (item && item.getType() === types.ePDFObjectName) {
899
+ entries.push(item.value);
900
+ }
901
+ }
902
+ return entries;
903
+ }
904
+
905
+ /**
906
+ * Read and parse a font's ToUnicode CMap within the call's budgets. A stream
907
+ * shared by several fonts is read and parsed once per call, but each font
908
+ * pays its bytes and expanded ranges, so sharing never changes a result. A
909
+ * CMap that does not fit the remaining budgets is ignored as a whole.
910
+ *
911
+ * @param {object} reader PDF reader or parser.
912
+ * @param {object} types PDF object type constants.
913
+ * @param {object} font Font dictionary.
914
+ * @param {{toUnicodeBytes: number, rangeEntries: number, constrained:
915
+ * boolean, cmaps: Map<number, {parsed: object|null, bytes: number}>}} call
916
+ * Decoding call budgets and its parsed CMaps; `constrained` is set when a
917
+ * budget, not the CMap itself, dropped the CMap.
918
+ * @returns {object|null} Parsed CMap, or null when missing, unreadable, or
919
+ * over the limits.
920
+ * @throws {Error} If reading exceeds the extraction limits.
921
+ */
922
+ function readToUnicode(reader, types, font, call) {
923
+ try {
924
+ var stream = entry(reader, font, PdfName.TO_UNICODE);
925
+ if (!stream || stream.getType() !== types.ePDFObjectStream) return null;
926
+ var reference = font.queryObject(PdfName.TO_UNICODE);
927
+ var objectId =
928
+ reference.getType() === types.ePDFObjectIndirectObjectReference
929
+ ? reference.toPDFIndirectObjectReference().getObjectID()
930
+ : null;
931
+ var cmap = objectId === null ? undefined : call.cmaps.get(objectId);
932
+ if (!cmap) {
933
+ var limit = Math.min(MAX_TO_UNICODE_BYTES, call.toUnicodeBytes);
934
+ var source = contentStream.readStreamString(reader, stream, limit);
935
+ if (source === null && limit < MAX_TO_UNICODE_BYTES) {
936
+ // Too big for what is left of the budget; its real size is unknown.
937
+ call.constrained = true;
938
+ return null;
939
+ }
940
+ cmap =
941
+ source === null
942
+ ? { parsed: null, bytes: MAX_TO_UNICODE_BYTES }
943
+ : { parsed: parseToUnicode(source), bytes: source.length };
944
+ if (objectId !== null) call.cmaps.set(objectId, cmap);
945
+ }
946
+ var ranges = cmap.parsed ? cmap.parsed.rangeEntries : 0;
947
+ if (cmap.bytes > call.toUnicodeBytes || ranges > call.rangeEntries) {
948
+ call.constrained = true;
949
+ return null;
950
+ }
951
+ call.toUnicodeBytes -= cmap.bytes;
952
+ call.rangeEntries -= ranges;
953
+ return cmap.parsed;
954
+ } catch (error) {
955
+ if (contentStream.isExtractionLimitError(error)) throw error;
956
+ return null;
957
+ }
958
+ }
959
+
960
+ /**
961
+ * Read a simple font's `/Widths` array.
962
+ *
963
+ * @param {object} reader PDF reader or parser.
964
+ * @param {object} types PDF object type constants.
965
+ * @param {object} widths `/Widths` entry.
966
+ * @param {object|null} firstChar `/FirstChar` entry.
967
+ * @param {object|null} lastChar `/LastChar` entry.
968
+ * @returns {number[]|null} Widths from `/FirstChar` on, or null when the
969
+ * array is malformed: not an array, without a numeric `/FirstChar`, longer
970
+ * than 256 codes, shorter than `/FirstChar` to `/LastChar`, or holding an item
971
+ * that is not a number.
972
+ */
973
+ function readWidths(reader, types, widths, firstChar, lastChar) {
974
+ if (
975
+ widths.getType() !== types.ePDFObjectArray ||
976
+ !isNumber(types, firstChar) ||
977
+ widths.getLength() > 256 ||
978
+ (isNumber(types, lastChar) &&
979
+ widths.getLength() < Number(lastChar.value) - Number(firstChar.value) + 1)
980
+ ) {
981
+ return null;
982
+ }
983
+ var items = arrayItems(reader, types, widths, 256);
984
+ var values = [];
985
+ for (var index = 0; index < items.length; index++) {
986
+ if (!isNumber(types, items[index])) return null;
987
+ values.push(Number(items[index].value));
988
+ }
989
+ return values;
990
+ }
991
+
992
+ /**
993
+ * Create a reader for a simple font's `/Widths`, used on the first encode.
994
+ * Extraction never calls it, so it does not count against extraction limits.
995
+ *
996
+ * @param {object} reader PDF reader or parser.
997
+ * @param {object} types PDF object type constants.
998
+ * @param {object} font Font dictionary.
999
+ * @returns {function(): {values: number[]|null, firstChar: number, malformed:
1000
+ * boolean}} Reads the widths; `values` is null when the font has none.
1001
+ */
1002
+ function widthsReader(reader, types, font) {
1003
+ /**
1004
+ * Read the font's widths.
1005
+ *
1006
+ * @returns {{values: number[]|null, firstChar: number, malformed:
1007
+ * boolean}} Widths.
1008
+ */
1009
+ function read() {
1010
+ var widths = entry(reader, font, PdfName.WIDTHS);
1011
+ if (!widths) return { values: null, firstChar: 0, malformed: false };
1012
+ var firstChar = entry(reader, font, PdfName.FIRST_CHAR);
1013
+ var values = readWidths(
1014
+ reader,
1015
+ types,
1016
+ widths,
1017
+ firstChar,
1018
+ entry(reader, font, PdfName.LAST_CHAR),
1019
+ );
1020
+ return {
1021
+ values: values,
1022
+ firstChar: isNumber(types, firstChar) ? Number(firstChar.value) : 0,
1023
+ malformed: values === null,
1024
+ };
1025
+ }
1026
+ return read;
1027
+ }
1028
+
1029
+ /**
1030
+ * Describe one font dictionary for `createFontCodec`. Parts that cannot be
1031
+ * read are left out, so the codec falls back to the rest.
1032
+ *
1033
+ * @param {object} reader PDF reader or parser.
1034
+ * @param {object} types PDF object type constants.
1035
+ * @param {object} font Font dictionary.
1036
+ * @param {{toUnicodeBytes: number, constrained: boolean}} call Decoding call
1037
+ * budget.
1038
+ * @returns {object} Font description.
1039
+ */
1040
+ function describeFont(reader, types, font, call) {
1041
+ var subtype = entry(reader, font, PdfName.SUBTYPE);
1042
+ var subtypeName =
1043
+ subtype && subtype.getType() === types.ePDFObjectName
1044
+ ? subtype.value
1045
+ : null;
1046
+ var encoding = entry(reader, font, PdfName.ENCODING);
1047
+ var encodingName =
1048
+ encoding && encoding.getType() === types.ePDFObjectName
1049
+ ? encoding.value
1050
+ : null;
1051
+ var description = {
1052
+ composite: subtypeName === FontSubtype.TYPE0,
1053
+ identity:
1054
+ encodingName === FontEncoding.IDENTITY_H ||
1055
+ encodingName === FontEncoding.IDENTITY_V,
1056
+ toUnicode: readToUnicode(reader, types, font, call),
1057
+ baseEncoding: null,
1058
+ differences: [],
1059
+ readWidths: widthsReader(reader.plain || reader, types, font),
1060
+ };
1061
+
1062
+ if (description.composite) return description;
1063
+
1064
+ var builtIn = usesBuiltInEncoding(reader, types, font, subtypeName);
1065
+ if (encodingName) {
1066
+ description.baseEncoding = encodingName;
1067
+ } else if (encoding && encoding.getType() === types.ePDFObjectDictionary) {
1068
+ var base = entry(reader, encoding, PdfName.BASE_ENCODING);
1069
+ if (base && base.getType() === types.ePDFObjectName) {
1070
+ description.baseEncoding = base.value;
1071
+ } else if (!builtIn) {
1072
+ description.baseEncoding = FontEncoding.STANDARD;
1073
+ }
1074
+ description.differences = differencesEntries(
1075
+ reader,
1076
+ types,
1077
+ entry(reader, encoding, PdfName.DIFFERENCES),
1078
+ );
1079
+ } else if (!builtIn) {
1080
+ description.baseEncoding =
1081
+ subtypeName === FontSubtype.TRUE_TYPE
1082
+ ? FontEncoding.WIN_ANSI
1083
+ : FontEncoding.STANDARD;
1084
+ }
1085
+
1086
+ return description;
1087
+ }
1088
+
1089
+ // Per-reader codec cache by font object ID, least recently used first. Each
1090
+ // entry records what building the codec cost, so a later call pays the same
1091
+ // from its own budgets and gets the same result as if nothing were cached.
1092
+ // Keyed weakly so ended readers are collected.
1093
+ var readerCodecs = new WeakMap();
1094
+
1095
+ /**
1096
+ * Get the codec cache for a reader.
1097
+ *
1098
+ * @param {object} reader PDF reader or parser.
1099
+ * @returns {{entries: Map<number, {codec: object, objects: number,
1100
+ * toUnicodeBytes: number, rangeEntries: number, size: number}>, size:
1101
+ * number}} Cached codecs by font object ID, and their total decode-map size.
1102
+ */
1103
+ function codecCache(reader) {
1104
+ var cache = readerCodecs.get(reader);
1105
+ if (!cache) {
1106
+ cache = { entries: new Map(), size: 0 };
1107
+ readerCodecs.set(reader, cache);
1108
+ }
1109
+ return cache;
1110
+ }
1111
+
1112
+ /**
1113
+ * Get a cached codec and mark it most recently used.
1114
+ *
1115
+ * @param {{entries: Map, size: number}} cache Codec cache.
1116
+ * @param {number} objectId Font object ID.
1117
+ * @returns {object|undefined} Cache entry.
1118
+ */
1119
+ function cachedCodec(cache, objectId) {
1120
+ var cached = cache.entries.get(objectId);
1121
+ if (cached) {
1122
+ cache.entries.delete(objectId);
1123
+ cache.entries.set(objectId, cached);
1124
+ }
1125
+ return cached;
1126
+ }
1127
+
1128
+ /**
1129
+ * Cache a codec, dropping the least recently used ones past the limits.
1130
+ *
1131
+ * @param {{entries: Map, size: number}} cache Codec cache.
1132
+ * @param {number} objectId Font object ID.
1133
+ * @param {object} entry Cache entry with its `size`.
1134
+ * @returns {void}
1135
+ */
1136
+ function cacheCodec(cache, objectId, entry) {
1137
+ cache.entries.set(objectId, entry);
1138
+ cache.size += entry.size;
1139
+ while (
1140
+ cache.entries.size > MAX_CACHED_FONTS ||
1141
+ cache.size > MAX_CACHED_ENTRIES
1142
+ ) {
1143
+ var oldest = cache.entries.keys().next().value;
1144
+ cache.size -= cache.entries.get(oldest).size;
1145
+ cache.entries.delete(oldest);
1146
+ }
1147
+ }
1148
+
1149
+ /**
1150
+ * Wrap a reader so that decoding stops after reading `maxObjects` PDF objects.
1151
+ *
1152
+ * @param {object} reader PDF reader or parser.
1153
+ * @param {number} maxObjects Most PDF objects to read.
1154
+ * @returns {object} Reader with the methods decoding uses, `used()` and
1155
+ * `charge(count)` for the objects read so far and cached work, and `plain`,
1156
+ * the uncounted reader.
1157
+ */
1158
+ function countingReader(reader, maxObjects) {
1159
+ var used = 0;
1160
+
1161
+ /**
1162
+ * Count objects read.
1163
+ *
1164
+ * @param {number} count Number of objects.
1165
+ * @returns {void}
1166
+ * @throws {Error} If the budget is spent.
1167
+ */
1168
+ function charge(count) {
1169
+ used += count;
1170
+ if (used > maxObjects) throw contentStream.extractionLimitError();
1171
+ }
1172
+
1173
+ /**
1174
+ * Report how many objects were counted.
1175
+ *
1176
+ * @returns {number} Objects counted so far.
1177
+ */
1178
+ function usedObjects() {
1179
+ return used;
1180
+ }
1181
+
1182
+ /**
1183
+ * Read a dictionary entry, counting it.
1184
+ *
1185
+ * @param {object} dictionary PDF dictionary.
1186
+ * @param {string} key Entry name.
1187
+ * @returns {object|undefined} Resolved entry.
1188
+ */
1189
+ function queryDictionaryObject(dictionary, key) {
1190
+ charge(1);
1191
+ return reader.queryDictionaryObject(dictionary, key);
1192
+ }
1193
+
1194
+ /**
1195
+ * Read an array item, counting it.
1196
+ *
1197
+ * @param {object} array PDF array.
1198
+ * @param {number} index Item index.
1199
+ * @returns {object|undefined} Resolved item.
1200
+ */
1201
+ function queryArrayObject(array, index) {
1202
+ charge(1);
1203
+ return reader.queryArrayObject(array, index);
1204
+ }
1205
+
1206
+ /**
1207
+ * Parse an indirect object, counting it.
1208
+ *
1209
+ * @param {number} objectId Object ID.
1210
+ * @returns {object} Parsed object.
1211
+ */
1212
+ function parseNewObject(objectId) {
1213
+ charge(1);
1214
+ return reader.parseNewObject(objectId);
1215
+ }
1216
+
1217
+ /**
1218
+ * Parse a page dictionary, counting it.
1219
+ *
1220
+ * @param {number} pageIndex Zero-based page index.
1221
+ * @returns {object} Page dictionary.
1222
+ */
1223
+ function parsePageDictionary(pageIndex) {
1224
+ charge(1);
1225
+ return reader.parsePageDictionary(pageIndex);
1226
+ }
1227
+
1228
+ /**
1229
+ * Start reading a stream, counting it.
1230
+ *
1231
+ * @param {object} stream PDF stream.
1232
+ * @returns {object} Stream reader.
1233
+ */
1234
+ function startReadingFromStream(stream) {
1235
+ charge(1);
1236
+ return reader.startReadingFromStream(stream);
1237
+ }
1238
+
1239
+ return {
1240
+ plain: reader,
1241
+ charge: charge,
1242
+ used: usedObjects,
1243
+ queryDictionaryObject: queryDictionaryObject,
1244
+ queryArrayObject: queryArrayObject,
1245
+ parseNewObject: parseNewObject,
1246
+ parsePageDictionary: parsePageDictionary,
1247
+ startReadingFromStream: startReadingFromStream,
1248
+ };
1249
+ }
1250
+
1251
+ /**
1252
+ * Build the codec for one font resource. A font that cannot be resolved or
1253
+ * read decodes to U+FFFD and cannot encode.
1254
+ *
1255
+ * @param {object} reader Counting reader for this call.
1256
+ * @param {object} types PDF object type constants.
1257
+ * @param {object|null} fonts Font resource dictionary.
1258
+ * @param {string} name Font resource name.
1259
+ * @param {{cache: object, charged: Set<number>, cmaps: Map, toUnicodeBytes:
1260
+ * number, rangeEntries: number, constrained: boolean}} call The reader's codec
1261
+ * cache, fonts already paid for in this call, the CMaps parsed in it, and its
1262
+ * remaining budgets.
1263
+ * @returns {object} Font codec.
1264
+ * @throws {Error} If reading the font exceeds the extraction limits.
1265
+ */
1266
+ function fontCodec(reader, types, fonts, name, call) {
1267
+ try {
1268
+ if (!fonts || typeof name !== "string" || !name || !fonts.exists(name)) {
1269
+ return UNREADABLE_CODEC;
1270
+ }
1271
+ var reference = fonts.queryObject(name);
1272
+ var objectId =
1273
+ reference.getType() === types.ePDFObjectIndirectObjectReference
1274
+ ? reference.toPDFIndirectObjectReference().getObjectID()
1275
+ : null;
1276
+ var cached = objectId === null ? null : cachedCodec(call.cache, objectId);
1277
+ if (cached && call.charged.has(objectId)) return cached.codec;
1278
+ if (
1279
+ cached &&
1280
+ cached.toUnicodeBytes <= call.toUnicodeBytes &&
1281
+ cached.rangeEntries <= call.rangeEntries
1282
+ ) {
1283
+ reader.charge(cached.objects);
1284
+ call.toUnicodeBytes -= cached.toUnicodeBytes;
1285
+ call.rangeEntries -= cached.rangeEntries;
1286
+ call.charged.add(objectId);
1287
+ return cached.codec;
1288
+ }
1289
+
1290
+ var objects = reader.used();
1291
+ var toUnicodeBytes = call.toUnicodeBytes;
1292
+ var rangeEntries = call.rangeEntries;
1293
+ call.constrained = false;
1294
+ var font = reader.queryDictionaryObject(fonts, name);
1295
+ var description =
1296
+ font && font.getType() === types.ePDFObjectDictionary
1297
+ ? describeFont(reader, types, font, call)
1298
+ : null;
1299
+ var codec = description ? createFontCodec(description) : UNREADABLE_CODEC;
1300
+ if (objectId !== null) {
1301
+ call.charged.add(objectId);
1302
+ // A codec built from a squeezed budget is not what a fresh call gets.
1303
+ if (!call.constrained) {
1304
+ cacheCodec(call.cache, objectId, {
1305
+ codec: codec,
1306
+ objects: reader.used() - objects,
1307
+ toUnicodeBytes: toUnicodeBytes - call.toUnicodeBytes,
1308
+ rangeEntries: rangeEntries - call.rangeEntries,
1309
+ size: codec.size,
1310
+ });
1311
+ }
1312
+ }
1313
+ return codec;
1314
+ } catch (error) {
1315
+ if (contentStream.isExtractionLimitError(error)) throw error;
1316
+ return UNREADABLE_CODEC;
1317
+ }
1318
+ }
1319
+
1320
+ /**
1321
+ * Create a font codec lookup for a page. Codecs are cached per reader by font
1322
+ * object, so fonts shared across pages and calls are read once. Each call
1323
+ * still pays a cached font's cost from its own budgets, so the result never
1324
+ * depends on earlier calls.
1325
+ *
1326
+ * @param {object} reader PDF reader or parser.
1327
+ * @param {object} types PDF object type constants.
1328
+ * @param {number} pageIndex Zero-based page index.
1329
+ * @param {number} [maxParsedObjects] Most PDF objects decoding may read;
1330
+ * defaults to the extractor's ceiling.
1331
+ * @returns {function(string): {decode: function(string): string, encode:
1332
+ * function(string): string}} Codec lookup by font resource name; `encode`
1333
+ * errors name that resource.
1334
+ * @throws {Error} If reading the page's fonts exceeds `maxParsedObjects`.
1335
+ */
1336
+ function createPageFontLookup(reader, types, pageIndex, maxParsedObjects) {
1337
+ var call = {
1338
+ cache: codecCache(reader),
1339
+ charged: new Set(),
1340
+ cmaps: new Map(),
1341
+ toUnicodeBytes: MAX_CALL_TO_UNICODE_BYTES,
1342
+ rangeEntries: MAX_CALL_RANGE_ENTRIES,
1343
+ constrained: false,
1344
+ };
1345
+ var counted = countingReader(
1346
+ reader,
1347
+ Math.min(maxParsedObjects || MAX_PARSED_OBJECTS, MAX_PARSED_OBJECTS),
1348
+ );
1349
+ var fonts = null;
1350
+ try {
1351
+ var resources = contentStream.inheritedResources(
1352
+ counted,
1353
+ types,
1354
+ counted.parsePageDictionary(pageIndex),
1355
+ );
1356
+ fonts = entry(counted, resources, PdfName.FONT);
1357
+ if (fonts && fonts.getType() !== types.ePDFObjectDictionary) fonts = null;
1358
+ } catch (error) {
1359
+ if (contentStream.isExtractionLimitError(error)) throw error;
1360
+ fonts = null;
1361
+ }
1362
+ var codecs = new Map();
1363
+
1364
+ /**
1365
+ * Get the codec for a font resource name on this page.
1366
+ *
1367
+ * @param {string|null} name Font resource name, or null before any `Tf`.
1368
+ * @returns {{decode: function(string): string, encode: function(string):
1369
+ * string}} Font codec.
1370
+ */
1371
+ function lookupFont(name) {
1372
+ if (!codecs.has(name)) {
1373
+ var codec = fontCodec(counted, types, fonts, name, call);
1374
+ codecs.set(name, {
1375
+ decode: codec.decode,
1376
+ encode: encodeAs(codec, typeof name === "string" && name ? name : null),
1377
+ });
1378
+ }
1379
+ return codecs.get(name);
1380
+ }
1381
+
1382
+ return lookupFont;
1383
+ }
1384
+
1385
+ /**
1386
+ * Bind a codec's `encode` to the font resource name its errors report.
1387
+ *
1388
+ * @param {{encode: function(string, string): string}} codec Font codec.
1389
+ * @param {string|null} name Font resource name on the current page.
1390
+ * @returns {function(string): string} Encoder.
1391
+ */
1392
+ function encodeAs(codec, name) {
1393
+ /**
1394
+ * Encode text through the font.
1395
+ *
1396
+ * @param {string} text Unicode text.
1397
+ * @returns {string} Operand bytes, one character per byte.
1398
+ */
1399
+ function encode(text) {
1400
+ return codec.encode(text, name);
1401
+ }
1402
+ return encode;
1403
+ }
1404
+
1405
+ /**
1406
+ * Read the `decodeText` option of `extractPageText()`.
1407
+ *
1408
+ * @param {*} options Extraction options, or undefined.
1409
+ * @returns {boolean} False when decoding is turned off.
1410
+ * @throws {TypeError} If `options` is not an object or `decodeText` is not a
1411
+ * boolean.
1412
+ */
1413
+ function decodeTextOption(options) {
1414
+ if (options === undefined) return true;
1415
+ if (
1416
+ options === null ||
1417
+ typeof options !== "object" ||
1418
+ Array.isArray(options)
1419
+ ) {
1420
+ throw new TypeError("Text extraction options must be an object");
1421
+ }
1422
+ if (options.decodeText === undefined) return true;
1423
+ if (typeof options.decodeText !== "boolean") {
1424
+ throw new TypeError("decodeText must be a boolean");
1425
+ }
1426
+ return options.decodeText;
1427
+ }
1428
+
1429
+ /**
1430
+ * Add decoded Unicode `text` to extracted text elements.
1431
+ *
1432
+ * @param {object} reader PDF reader.
1433
+ * @param {object} types PDF object type constants.
1434
+ * @param {number} pageIndex Zero-based page index.
1435
+ * @param {Array<{content: string, fontResource: string}>} elements Extracted
1436
+ * elements.
1437
+ * @param {{maxParsedObjects?: number}} [limits] The caller's extraction limits.
1438
+ * @returns {Array<object>} The same elements with `text` set.
1439
+ * @throws {Error} If decoding reads more PDF objects than `maxParsedObjects`
1440
+ * allows.
1441
+ */
1442
+ function decodeTextElements(reader, types, pageIndex, elements, limits) {
1443
+ if (!elements.length) return elements;
1444
+ var fonts = createPageFontLookup(
1445
+ reader,
1446
+ types,
1447
+ pageIndex,
1448
+ limits && limits.maxParsedObjects,
1449
+ );
1450
+ for (var index = 0; index < elements.length; index++) {
1451
+ var element = elements[index];
1452
+ element.text = fonts(element.fontResource).decode(element.content);
1453
+ }
1454
+ return elements;
1455
+ }
1456
+
1457
+ module.exports = {
1458
+ createPageFontLookup: createPageFontLookup,
1459
+ decodeTextElements: decodeTextElements,
1460
+ decodeTextOption: decodeTextOption,
1461
+ };