@aglyn/aglyn 1.0.0-beta.197 → 1.0.0-beta.199
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/package.json +11 -11
- package/src/lib/app-utils/docs-help.generated.d.ts +1 -1
- package/src/lib/app-utils/docs-help.generated.js +1 -0
- package/src/lib/app-utils/docs-help.generated.js.map +1 -1
- package/src/lib/app-utils/docs-index.generated.js +3 -3
- package/src/lib/app-utils/docs-index.generated.js.map +1 -1
- package/src/lib/app-utils/media-embedded-fields.d.ts +471 -0
- package/src/lib/app-utils/media-embedded-fields.js +619 -0
- package/src/lib/app-utils/media-embedded-fields.js.map +1 -0
- package/src/lib/app-utils/media-embedded-metadata/exif.d.ts +160 -0
- package/src/lib/app-utils/media-embedded-metadata/exif.js +1267 -0
- package/src/lib/app-utils/media-embedded-metadata/exif.js.map +1 -0
- package/src/lib/app-utils/media-embedded-metadata/gif.d.ts +44 -0
- package/src/lib/app-utils/media-embedded-metadata/gif.js +134 -0
- package/src/lib/app-utils/media-embedded-metadata/gif.js.map +1 -0
- package/src/lib/app-utils/media-embedded-metadata/heif.d.ts +23 -0
- package/src/lib/app-utils/media-embedded-metadata/heif.js +354 -0
- package/src/lib/app-utils/media-embedded-metadata/heif.js.map +1 -0
- package/src/lib/app-utils/media-embedded-metadata/icc.d.ts +22 -0
- package/src/lib/app-utils/media-embedded-metadata/icc.js +85 -0
- package/src/lib/app-utils/media-embedded-metadata/icc.js.map +1 -0
- package/src/lib/app-utils/media-embedded-metadata/image-blocks.d.ts +145 -0
- package/src/lib/app-utils/media-embedded-metadata/image-blocks.js +200 -0
- package/src/lib/app-utils/media-embedded-metadata/image-blocks.js.map +1 -0
- package/src/lib/app-utils/media-embedded-metadata/index.d.ts +93 -0
- package/src/lib/app-utils/media-embedded-metadata/index.js +744 -0
- package/src/lib/app-utils/media-embedded-metadata/index.js.map +1 -0
- package/src/lib/app-utils/media-embedded-metadata/iptc.d.ts +59 -0
- package/src/lib/app-utils/media-embedded-metadata/iptc.js +529 -0
- package/src/lib/app-utils/media-embedded-metadata/iptc.js.map +1 -0
- package/src/lib/app-utils/media-embedded-metadata/jpeg.d.ts +43 -0
- package/src/lib/app-utils/media-embedded-metadata/jpeg.js +456 -0
- package/src/lib/app-utils/media-embedded-metadata/jpeg.js.map +1 -0
- package/src/lib/app-utils/media-embedded-metadata/mp4.d.ts +37 -0
- package/src/lib/app-utils/media-embedded-metadata/mp4.js +621 -0
- package/src/lib/app-utils/media-embedded-metadata/mp4.js.map +1 -0
- package/src/lib/app-utils/media-embedded-metadata/ooxml.d.ts +131 -0
- package/src/lib/app-utils/media-embedded-metadata/ooxml.js +1058 -0
- package/src/lib/app-utils/media-embedded-metadata/ooxml.js.map +1 -0
- package/src/lib/app-utils/media-embedded-metadata/pdf-objects.d.ts +223 -0
- package/src/lib/app-utils/media-embedded-metadata/pdf-objects.js +936 -0
- package/src/lib/app-utils/media-embedded-metadata/pdf-objects.js.map +1 -0
- package/src/lib/app-utils/media-embedded-metadata/pdf.d.ts +82 -0
- package/src/lib/app-utils/media-embedded-metadata/pdf.js +1167 -0
- package/src/lib/app-utils/media-embedded-metadata/pdf.js.map +1 -0
- package/src/lib/app-utils/media-embedded-metadata/png.d.ts +41 -0
- package/src/lib/app-utils/media-embedded-metadata/png.js +512 -0
- package/src/lib/app-utils/media-embedded-metadata/png.js.map +1 -0
- package/src/lib/app-utils/media-embedded-metadata/svg.d.ts +30 -0
- package/src/lib/app-utils/media-embedded-metadata/svg.js +154 -0
- package/src/lib/app-utils/media-embedded-metadata/svg.js.map +1 -0
- package/src/lib/app-utils/media-embedded-metadata/tiff.d.ts +40 -0
- package/src/lib/app-utils/media-embedded-metadata/tiff.js +158 -0
- package/src/lib/app-utils/media-embedded-metadata/tiff.js.map +1 -0
- package/src/lib/app-utils/media-embedded-metadata/types.d.ts +60 -0
- package/src/lib/app-utils/media-embedded-metadata/types.js +32 -0
- package/src/lib/app-utils/media-embedded-metadata/types.js.map +1 -0
- package/src/lib/app-utils/media-embedded-metadata/webm.d.ts +26 -0
- package/src/lib/app-utils/media-embedded-metadata/webm.js +417 -0
- package/src/lib/app-utils/media-embedded-metadata/webm.js.map +1 -0
- package/src/lib/app-utils/media-embedded-metadata/webp.d.ts +37 -0
- package/src/lib/app-utils/media-embedded-metadata/webp.js +272 -0
- package/src/lib/app-utils/media-embedded-metadata/webp.js.map +1 -0
- package/src/lib/app-utils/media-embedded-metadata/xml.d.ts +231 -0
- package/src/lib/app-utils/media-embedded-metadata/xml.js +598 -0
- package/src/lib/app-utils/media-embedded-metadata/xml.js.map +1 -0
- package/src/lib/app-utils/media-embedded-metadata/xmp.d.ts +100 -0
- package/src/lib/app-utils/media-embedded-metadata/xmp.js +1475 -0
- package/src/lib/app-utils/media-embedded-metadata/xmp.js.map +1 -0
- package/src/lib/app-utils/media-embedded-metadata/zip.d.ts +93 -0
- package/src/lib/app-utils/media-embedded-metadata/zip.js +531 -0
- package/src/lib/app-utils/media-embedded-metadata/zip.js.map +1 -0
- package/src/lib/app-utils/name-search.d.ts +12 -0
- package/src/lib/app-utils/name-search.js +11 -0
- package/src/lib/app-utils/name-search.js.map +1 -1
- package/src/lib/foundation/definitions/platform.types.d.ts +11 -0
- package/src/lib/foundation/definitions/platform.types.js.map +1 -1
- package/src/server.d.ts +1 -0
- package/src/server.js +7 -0
- package/src/server.js.map +1 -1
|
@@ -0,0 +1,936 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/ /**
|
|
17
|
+
* The object layer under the PDF metadata reader and writer (AGL-3331).
|
|
18
|
+
*
|
|
19
|
+
* - A tokenizer and parser for the COS syntax of ISO 32000-2 §7.2–§7.3:
|
|
20
|
+
* names, literal and hexadecimal strings, numbers, arrays, dictionaries,
|
|
21
|
+
* indirect references, `stream`…`endstream` bodies, booleans and null.
|
|
22
|
+
* - A serializer that writes a parsed object back out faithfully, so a
|
|
23
|
+
* dictionary the writer re-emits (a catalog gaining `/Metadata`, an Info
|
|
24
|
+
* dictionary with one entry changed) keeps every other entry as it was.
|
|
25
|
+
* - The text-string codec of §7.9.2.2 (PDFDocEncoding, UTF-16BE, UTF-8).
|
|
26
|
+
* - FlateDecode with the §7.4.4.4 predictors — the only filter a reader of
|
|
27
|
+
* cross-reference streams, object streams and metadata streams needs.
|
|
28
|
+
*
|
|
29
|
+
* ## Hostile input
|
|
30
|
+
*
|
|
31
|
+
* Every byte here comes from an uploaded file. Nothing in this module
|
|
32
|
+
* throws on malformed input: the parser returns `null` for what it cannot
|
|
33
|
+
* read, nesting is capped at {@link PDF_MAX_DEPTH}, one object may spend at
|
|
34
|
+
* most {@link PDF_MAX_TOKENS} tokens, every offset is checked against the
|
|
35
|
+
* buffer, and inflation is capped by the caller's `maxOutputLength`.
|
|
36
|
+
*/ import { constants, inflateRawSync, inflateSync } from "zlib";
|
|
37
|
+
export const PDF_NULL = {
|
|
38
|
+
kind: 'null'
|
|
39
|
+
};
|
|
40
|
+
/** Deepest array/dictionary nesting the parser follows. */ export const PDF_MAX_DEPTH = 64;
|
|
41
|
+
/** Tokens one object may consume before the parser gives up on it. */ export const PDF_MAX_TOKENS = 2000000;
|
|
42
|
+
/** Longest name or bare keyword kept; the rest of the run is skipped. */ const MAX_WORD = 4096;
|
|
43
|
+
// §7.2.3 character classes: 0 regular, 1 white-space, 2 delimiter.
|
|
44
|
+
const CHAR_CLASS = new Uint8Array(256);
|
|
45
|
+
for (const b of [
|
|
46
|
+
0x00,
|
|
47
|
+
0x09,
|
|
48
|
+
0x0a,
|
|
49
|
+
0x0c,
|
|
50
|
+
0x0d,
|
|
51
|
+
0x20
|
|
52
|
+
])CHAR_CLASS[b] = 1;
|
|
53
|
+
for (const c of '()<>[]{}/%')CHAR_CLASS[c.charCodeAt(0)] = 2;
|
|
54
|
+
/** A byte of `bytes`, or -1 outside `[0, end)`. */ function byteAt(bytes, i, end = bytes.length) {
|
|
55
|
+
var _bytes_i;
|
|
56
|
+
return i >= 0 && i < end ? (_bytes_i = bytes[i]) != null ? _bytes_i : -1 : -1;
|
|
57
|
+
}
|
|
58
|
+
export function isPdfWhitespace(b) {
|
|
59
|
+
return b >= 0 && b < 256 && CHAR_CLASS[b] === 1;
|
|
60
|
+
}
|
|
61
|
+
function isRegular(b) {
|
|
62
|
+
return b >= 0 && b < 256 && CHAR_CLASS[b] === 0;
|
|
63
|
+
}
|
|
64
|
+
function hexValue(b) {
|
|
65
|
+
if (b >= 0x30 && b <= 0x39) return b - 0x30;
|
|
66
|
+
if (b >= 0x41 && b <= 0x46) return b - 0x37;
|
|
67
|
+
if (b >= 0x61 && b <= 0x66) return b - 0x57;
|
|
68
|
+
return -1;
|
|
69
|
+
}
|
|
70
|
+
/** Bytes as a binary string, one char per byte. */ export function latin1Text(bytes) {
|
|
71
|
+
let out = '';
|
|
72
|
+
for(let i = 0; i < bytes.length; i += 8192){
|
|
73
|
+
out += String.fromCharCode(...bytes.subarray(i, i + 8192));
|
|
74
|
+
}
|
|
75
|
+
return out;
|
|
76
|
+
}
|
|
77
|
+
/** A binary string (chars 0–255) as bytes. */ export function latin1Bytes(text) {
|
|
78
|
+
const out = new Uint8Array(text.length);
|
|
79
|
+
for(let i = 0; i < text.length; i++)out[i] = text.charCodeAt(i) & 0xff;
|
|
80
|
+
return out;
|
|
81
|
+
}
|
|
82
|
+
/** The first index of `needle` in `bytes[from, to)`, or -1. */ export function indexOfBytes(bytes, needle, from = 0, to = bytes.length) {
|
|
83
|
+
const first = needle[0];
|
|
84
|
+
if (first === undefined) return -1;
|
|
85
|
+
const last = Math.min(to, bytes.length) - needle.length;
|
|
86
|
+
outer: for(let i = Math.max(0, from); i <= last; i++){
|
|
87
|
+
if (bytes[i] !== first) continue;
|
|
88
|
+
for(let j = 1; j < needle.length; j++){
|
|
89
|
+
if (bytes[i + j] !== needle[j]) continue outer;
|
|
90
|
+
}
|
|
91
|
+
return i;
|
|
92
|
+
}
|
|
93
|
+
return -1;
|
|
94
|
+
}
|
|
95
|
+
/** The last index of `needle` in `bytes` starting at or before `from`. */ export function lastIndexOfBytes(bytes, needle, from = bytes.length - needle.length) {
|
|
96
|
+
const first = needle[0];
|
|
97
|
+
if (first === undefined) return -1;
|
|
98
|
+
outer: for(let i = Math.min(from, bytes.length - needle.length); i >= 0; i--){
|
|
99
|
+
if (bytes[i] !== first) continue;
|
|
100
|
+
for(let j = 1; j < needle.length; j++){
|
|
101
|
+
if (bytes[i + j] !== needle[j]) continue outer;
|
|
102
|
+
}
|
|
103
|
+
return i;
|
|
104
|
+
}
|
|
105
|
+
return -1;
|
|
106
|
+
}
|
|
107
|
+
/** A growable byte buffer for string bodies of unknown length. */ let ByteSink = class ByteSink {
|
|
108
|
+
push(b) {
|
|
109
|
+
if (this.length === this.buf.length) {
|
|
110
|
+
const next = new Uint8Array(this.buf.length * 2);
|
|
111
|
+
next.set(this.buf);
|
|
112
|
+
this.buf = next;
|
|
113
|
+
}
|
|
114
|
+
this.buf[this.length++] = b;
|
|
115
|
+
}
|
|
116
|
+
bytes() {
|
|
117
|
+
return this.buf.slice(0, this.length);
|
|
118
|
+
}
|
|
119
|
+
constructor(){
|
|
120
|
+
this.buf = new Uint8Array(64);
|
|
121
|
+
this.length = 0;
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
const NUMBER = /^[+-]?(?:\d+\.?\d*|\.\d+)$/;
|
|
125
|
+
const INTEGER = /^\d+$/;
|
|
126
|
+
/** The §7.2 tokenizer. `end` is exclusive; nothing is read past it. */ export class PdfLexer {
|
|
127
|
+
at(i) {
|
|
128
|
+
return byteAt(this.bytes, i, this.end);
|
|
129
|
+
}
|
|
130
|
+
/** Skips white-space and `%` comments (§7.2.4). */ skipSpace() {
|
|
131
|
+
for(;;){
|
|
132
|
+
const b = this.at(this.pos);
|
|
133
|
+
if (b < 0) return;
|
|
134
|
+
if (isPdfWhitespace(b)) {
|
|
135
|
+
this.pos++;
|
|
136
|
+
} else if (b === 0x25) {
|
|
137
|
+
while(this.pos < this.end){
|
|
138
|
+
const c = this.at(this.pos);
|
|
139
|
+
if (c === 0x0a || c === 0x0d) break;
|
|
140
|
+
this.pos++;
|
|
141
|
+
}
|
|
142
|
+
} else {
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
/** Whether `word` is the next token, as a whole keyword. */ peekKeyword(word) {
|
|
148
|
+
this.skipSpace();
|
|
149
|
+
for(let i = 0; i < word.length; i++){
|
|
150
|
+
if (this.at(this.pos + i) !== word.charCodeAt(i)) return false;
|
|
151
|
+
}
|
|
152
|
+
return !isRegular(this.at(this.pos + word.length));
|
|
153
|
+
}
|
|
154
|
+
next() {
|
|
155
|
+
this.skipSpace();
|
|
156
|
+
const b = this.at(this.pos);
|
|
157
|
+
if (b < 0) return {
|
|
158
|
+
type: 'eof'
|
|
159
|
+
};
|
|
160
|
+
switch(b){
|
|
161
|
+
case 0x2f:
|
|
162
|
+
return this.name();
|
|
163
|
+
case 0x28:
|
|
164
|
+
return this.literalString();
|
|
165
|
+
case 0x3c:
|
|
166
|
+
if (this.at(this.pos + 1) === 0x3c) {
|
|
167
|
+
this.pos += 2;
|
|
168
|
+
return {
|
|
169
|
+
type: 'punct',
|
|
170
|
+
text: '<<'
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
return this.hexString();
|
|
174
|
+
case 0x3e:
|
|
175
|
+
if (this.at(this.pos + 1) === 0x3e) {
|
|
176
|
+
this.pos += 2;
|
|
177
|
+
return {
|
|
178
|
+
type: 'punct',
|
|
179
|
+
text: '>>'
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
this.pos++;
|
|
183
|
+
return {
|
|
184
|
+
type: 'keyword',
|
|
185
|
+
text: '>'
|
|
186
|
+
};
|
|
187
|
+
case 0x5b:
|
|
188
|
+
this.pos++;
|
|
189
|
+
return {
|
|
190
|
+
type: 'punct',
|
|
191
|
+
text: '['
|
|
192
|
+
};
|
|
193
|
+
case 0x5d:
|
|
194
|
+
this.pos++;
|
|
195
|
+
return {
|
|
196
|
+
type: 'punct',
|
|
197
|
+
text: ']'
|
|
198
|
+
};
|
|
199
|
+
case 0x7b:
|
|
200
|
+
this.pos++;
|
|
201
|
+
return {
|
|
202
|
+
type: 'punct',
|
|
203
|
+
text: '{'
|
|
204
|
+
};
|
|
205
|
+
case 0x7d:
|
|
206
|
+
this.pos++;
|
|
207
|
+
return {
|
|
208
|
+
type: 'punct',
|
|
209
|
+
text: '}'
|
|
210
|
+
};
|
|
211
|
+
case 0x29:
|
|
212
|
+
this.pos++;
|
|
213
|
+
return {
|
|
214
|
+
type: 'keyword',
|
|
215
|
+
text: ')'
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
let text = '';
|
|
219
|
+
while(isRegular(this.at(this.pos))){
|
|
220
|
+
if (text.length < MAX_WORD) text += String.fromCharCode(this.at(this.pos));
|
|
221
|
+
this.pos++;
|
|
222
|
+
}
|
|
223
|
+
if (NUMBER.test(text)) {
|
|
224
|
+
const value = Number(text);
|
|
225
|
+
if (Number.isFinite(value)) return {
|
|
226
|
+
type: 'number',
|
|
227
|
+
value,
|
|
228
|
+
text
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
return {
|
|
232
|
+
type: 'keyword',
|
|
233
|
+
text
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
/** §7.3.5: a name, with `#xx` escapes decoded to bytes. */ name() {
|
|
237
|
+
this.pos++;
|
|
238
|
+
let name = '';
|
|
239
|
+
while(isRegular(this.at(this.pos))){
|
|
240
|
+
let b = this.at(this.pos);
|
|
241
|
+
if (b === 0x23) {
|
|
242
|
+
const hi = hexValue(this.at(this.pos + 1));
|
|
243
|
+
const lo = hexValue(this.at(this.pos + 2));
|
|
244
|
+
if (hi >= 0 && lo >= 0) {
|
|
245
|
+
b = hi * 16 + lo;
|
|
246
|
+
this.pos += 2;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
if (name.length < MAX_WORD) name += String.fromCharCode(b);
|
|
250
|
+
this.pos++;
|
|
251
|
+
}
|
|
252
|
+
return {
|
|
253
|
+
type: 'name',
|
|
254
|
+
name
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* §7.3.4.2: a literal string. Balanced parentheses nest; `\` escapes
|
|
259
|
+
* `n r t b f ( ) \`, one to three octal digits, and an end-of-line (a
|
|
260
|
+
* line continuation, which contributes nothing). An unescaped CR, LF or
|
|
261
|
+
* CRLF reads as a single LF. An unterminated string ends at the buffer.
|
|
262
|
+
*/ literalString() {
|
|
263
|
+
this.pos++;
|
|
264
|
+
const out = new ByteSink();
|
|
265
|
+
let depth = 1;
|
|
266
|
+
while(this.pos < this.end){
|
|
267
|
+
const b = this.at(this.pos++);
|
|
268
|
+
if (b === 0x28) {
|
|
269
|
+
depth++;
|
|
270
|
+
out.push(b);
|
|
271
|
+
} else if (b === 0x29) {
|
|
272
|
+
if (--depth === 0) break;
|
|
273
|
+
out.push(b);
|
|
274
|
+
} else if (b === 0x5c) {
|
|
275
|
+
const e = this.at(this.pos++);
|
|
276
|
+
switch(e){
|
|
277
|
+
case 0x6e:
|
|
278
|
+
out.push(0x0a);
|
|
279
|
+
break;
|
|
280
|
+
case 0x72:
|
|
281
|
+
out.push(0x0d);
|
|
282
|
+
break;
|
|
283
|
+
case 0x74:
|
|
284
|
+
out.push(0x09);
|
|
285
|
+
break;
|
|
286
|
+
case 0x62:
|
|
287
|
+
out.push(0x08);
|
|
288
|
+
break;
|
|
289
|
+
case 0x66:
|
|
290
|
+
out.push(0x0c);
|
|
291
|
+
break;
|
|
292
|
+
case 0x0d:
|
|
293
|
+
if (this.at(this.pos) === 0x0a) this.pos++;
|
|
294
|
+
break;
|
|
295
|
+
case 0x0a:
|
|
296
|
+
break;
|
|
297
|
+
case -1:
|
|
298
|
+
break;
|
|
299
|
+
default:
|
|
300
|
+
if (e >= 0x30 && e <= 0x37) {
|
|
301
|
+
let code = e - 0x30;
|
|
302
|
+
for(let k = 0; k < 2; k++){
|
|
303
|
+
const d = this.at(this.pos);
|
|
304
|
+
if (d < 0x30 || d > 0x37) break;
|
|
305
|
+
code = code * 8 + (d - 0x30);
|
|
306
|
+
this.pos++;
|
|
307
|
+
}
|
|
308
|
+
out.push(code & 0xff);
|
|
309
|
+
} else {
|
|
310
|
+
out.push(e);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
} else if (b === 0x0d) {
|
|
314
|
+
if (this.at(this.pos) === 0x0a) this.pos++;
|
|
315
|
+
out.push(0x0a);
|
|
316
|
+
} else {
|
|
317
|
+
out.push(b);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
return {
|
|
321
|
+
type: 'string',
|
|
322
|
+
bytes: out.bytes(),
|
|
323
|
+
hex: false
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
327
|
+
* §7.3.4.3: a hexadecimal string. White-space is ignored, an odd final
|
|
328
|
+
* digit is followed by an implied 0, and any other stray byte is skipped
|
|
329
|
+
* rather than ending the string.
|
|
330
|
+
*/ hexString() {
|
|
331
|
+
this.pos++;
|
|
332
|
+
const out = new ByteSink();
|
|
333
|
+
let high = -1;
|
|
334
|
+
while(this.pos < this.end){
|
|
335
|
+
const b = this.at(this.pos++);
|
|
336
|
+
if (b === 0x3e) break;
|
|
337
|
+
const v = hexValue(b);
|
|
338
|
+
if (v < 0) continue;
|
|
339
|
+
if (high < 0) {
|
|
340
|
+
high = v;
|
|
341
|
+
} else {
|
|
342
|
+
out.push(high * 16 + v);
|
|
343
|
+
high = -1;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
if (high >= 0) out.push(high * 16);
|
|
347
|
+
return {
|
|
348
|
+
type: 'string',
|
|
349
|
+
bytes: out.bytes(),
|
|
350
|
+
hex: true
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
constructor(bytes, pos = 0, end = bytes.length){
|
|
354
|
+
this.bytes = bytes;
|
|
355
|
+
this.end = Math.max(0, Math.min(end, bytes.length));
|
|
356
|
+
this.pos = Math.max(0, pos);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
/** Keywords that end whatever is being parsed; seeing one means a truncated container. */ const STRUCTURAL = new Set([
|
|
360
|
+
'obj',
|
|
361
|
+
'endobj',
|
|
362
|
+
'stream',
|
|
363
|
+
'endstream',
|
|
364
|
+
'xref',
|
|
365
|
+
'trailer',
|
|
366
|
+
'startxref'
|
|
367
|
+
]);
|
|
368
|
+
const ENDSTREAM = latin1Bytes('endstream');
|
|
369
|
+
/**
|
|
370
|
+
* The §7.3 object parser over {@link PdfLexer}. Returns `null` for what it
|
|
371
|
+
* cannot read rather than throwing; see the module doc for the limits.
|
|
372
|
+
*/ export class PdfParser {
|
|
373
|
+
get pos() {
|
|
374
|
+
return this.lexer.pos;
|
|
375
|
+
}
|
|
376
|
+
/** One direct object at the current position. */ parseObject() {
|
|
377
|
+
this.failed = false;
|
|
378
|
+
this.budget = PDF_MAX_TOKENS;
|
|
379
|
+
const obj = this.object(this.lexer.next(), 0);
|
|
380
|
+
return this.failed ? null : obj;
|
|
381
|
+
}
|
|
382
|
+
/**
|
|
383
|
+
* `num gen obj … endobj` at the current position (§7.3.10). A dictionary
|
|
384
|
+
* followed by `stream` becomes a {@link PdfStream} (§7.3.8). `endobj` is
|
|
385
|
+
* not required: plenty of writers forget it, and nothing depends on it.
|
|
386
|
+
*/ parseIndirectObject() {
|
|
387
|
+
const numToken = this.lexer.next();
|
|
388
|
+
const genToken = this.lexer.next();
|
|
389
|
+
if (!isIntegerToken(numToken) || !isIntegerToken(genToken)) return null;
|
|
390
|
+
if (!this.lexer.peekKeyword('obj')) return null;
|
|
391
|
+
this.lexer.next();
|
|
392
|
+
this.failed = false;
|
|
393
|
+
this.budget = PDF_MAX_TOKENS;
|
|
394
|
+
const start = this.lexer.pos;
|
|
395
|
+
const token = this.lexer.next();
|
|
396
|
+
let obj;
|
|
397
|
+
if (token.type === 'keyword' && token.text === 'endobj') {
|
|
398
|
+
obj = PDF_NULL;
|
|
399
|
+
} else {
|
|
400
|
+
obj = this.object(token, 0);
|
|
401
|
+
if (this.failed || !obj) {
|
|
402
|
+
this.lexer.pos = start;
|
|
403
|
+
return null;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
if (obj.kind === 'dict' && this.lexer.peekKeyword('stream')) {
|
|
407
|
+
this.lexer.next();
|
|
408
|
+
obj = this.streamBody(obj);
|
|
409
|
+
}
|
|
410
|
+
if (this.lexer.peekKeyword('endobj')) this.lexer.next();
|
|
411
|
+
return {
|
|
412
|
+
num: numToken.value,
|
|
413
|
+
gen: genToken.value,
|
|
414
|
+
obj
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
object(token, depth) {
|
|
418
|
+
if (--this.budget < 0 || depth > PDF_MAX_DEPTH) {
|
|
419
|
+
this.failed = true;
|
|
420
|
+
return null;
|
|
421
|
+
}
|
|
422
|
+
switch(token.type){
|
|
423
|
+
case 'number':
|
|
424
|
+
{
|
|
425
|
+
if (INTEGER.test(token.text)) {
|
|
426
|
+
const mark = this.lexer.pos;
|
|
427
|
+
const gen = this.lexer.next();
|
|
428
|
+
if (isIntegerToken(gen)) {
|
|
429
|
+
const r = this.lexer.next();
|
|
430
|
+
if (r.type === 'keyword' && r.text === 'R') {
|
|
431
|
+
return {
|
|
432
|
+
kind: 'ref',
|
|
433
|
+
num: token.value,
|
|
434
|
+
gen: gen.value
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
this.lexer.pos = mark;
|
|
439
|
+
}
|
|
440
|
+
return {
|
|
441
|
+
kind: 'number',
|
|
442
|
+
value: token.value,
|
|
443
|
+
text: token.text
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
case 'name':
|
|
447
|
+
return {
|
|
448
|
+
kind: 'name',
|
|
449
|
+
name: token.name
|
|
450
|
+
};
|
|
451
|
+
case 'string':
|
|
452
|
+
return {
|
|
453
|
+
kind: 'string',
|
|
454
|
+
bytes: token.bytes,
|
|
455
|
+
hex: token.hex
|
|
456
|
+
};
|
|
457
|
+
case 'keyword':
|
|
458
|
+
if (token.text === 'true') return {
|
|
459
|
+
kind: 'boolean',
|
|
460
|
+
value: true
|
|
461
|
+
};
|
|
462
|
+
if (token.text === 'false') return {
|
|
463
|
+
kind: 'boolean',
|
|
464
|
+
value: false
|
|
465
|
+
};
|
|
466
|
+
if (token.text === 'null') return PDF_NULL;
|
|
467
|
+
return null;
|
|
468
|
+
case 'punct':
|
|
469
|
+
if (token.text === '[') return this.array(depth);
|
|
470
|
+
if (token.text === '<<') return this.dict(depth);
|
|
471
|
+
return null;
|
|
472
|
+
default:
|
|
473
|
+
return null;
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
array(depth) {
|
|
477
|
+
const items = [];
|
|
478
|
+
for(;;){
|
|
479
|
+
const mark = this.lexer.pos;
|
|
480
|
+
const token = this.lexer.next();
|
|
481
|
+
if (token.type === 'eof') break;
|
|
482
|
+
if (token.type === 'punct' && token.text === ']') break;
|
|
483
|
+
if (token.type === 'keyword' && STRUCTURAL.has(token.text)) {
|
|
484
|
+
this.lexer.pos = mark;
|
|
485
|
+
break;
|
|
486
|
+
}
|
|
487
|
+
const item = this.object(token, depth + 1);
|
|
488
|
+
if (this.failed) return null;
|
|
489
|
+
if (item) items.push(item);
|
|
490
|
+
}
|
|
491
|
+
return {
|
|
492
|
+
kind: 'array',
|
|
493
|
+
items
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
dict(depth) {
|
|
497
|
+
const entries = new Map();
|
|
498
|
+
for(;;){
|
|
499
|
+
const mark = this.lexer.pos;
|
|
500
|
+
const token = this.lexer.next();
|
|
501
|
+
if (token.type === 'eof') break;
|
|
502
|
+
if (token.type === 'punct' && token.text === '>>') break;
|
|
503
|
+
if (token.type === 'keyword' && STRUCTURAL.has(token.text)) {
|
|
504
|
+
this.lexer.pos = mark;
|
|
505
|
+
break;
|
|
506
|
+
}
|
|
507
|
+
if (token.type !== 'name') {
|
|
508
|
+
// A key that is not a name: skip it (and whatever it opened).
|
|
509
|
+
this.object(token, depth + 1);
|
|
510
|
+
if (this.failed) return null;
|
|
511
|
+
continue;
|
|
512
|
+
}
|
|
513
|
+
const valueMark = this.lexer.pos;
|
|
514
|
+
const valueToken = this.lexer.next();
|
|
515
|
+
if (valueToken.type === 'punct' && valueToken.text === '>>') break;
|
|
516
|
+
if (valueToken.type === 'eof' || valueToken.type === 'keyword' && STRUCTURAL.has(valueToken.text)) {
|
|
517
|
+
this.lexer.pos = valueMark;
|
|
518
|
+
break;
|
|
519
|
+
}
|
|
520
|
+
const value = this.object(valueToken, depth + 1);
|
|
521
|
+
if (this.failed) return null;
|
|
522
|
+
// §7.3.7 leaves duplicate keys undefined; the first one wins, as in
|
|
523
|
+
// Poppler, whose `pdfinfo` is what these files are checked against.
|
|
524
|
+
if (value && !entries.has(token.name)) entries.set(token.name, value);
|
|
525
|
+
}
|
|
526
|
+
return {
|
|
527
|
+
kind: 'dict',
|
|
528
|
+
entries
|
|
529
|
+
};
|
|
530
|
+
}
|
|
531
|
+
/**
|
|
532
|
+
* §7.3.8.1: the data starts after the EOL that ends the `stream` keyword
|
|
533
|
+
* (CRLF or LF; a lone CR is tolerated) and runs for `/Length` bytes. When
|
|
534
|
+
* `/Length` is missing, unresolvable or does not land on `endstream`, the
|
|
535
|
+
* data runs to the next `endstream` instead, less the EOL before it.
|
|
536
|
+
*/ streamBody(dict) {
|
|
537
|
+
const { bytes } = this;
|
|
538
|
+
const end = this.lexer.end;
|
|
539
|
+
let start = this.lexer.pos;
|
|
540
|
+
if (byteAt(bytes, start, end) === 0x0d) start++;
|
|
541
|
+
if (byteAt(bytes, start, end) === 0x0a) start++;
|
|
542
|
+
const length = this.streamLength(dict);
|
|
543
|
+
if (length !== null && start + length <= end) {
|
|
544
|
+
const probe = new PdfLexer(bytes, start + length, end);
|
|
545
|
+
if (probe.peekKeyword('endstream')) {
|
|
546
|
+
probe.next();
|
|
547
|
+
this.lexer.pos = probe.pos;
|
|
548
|
+
return {
|
|
549
|
+
kind: 'stream',
|
|
550
|
+
dict,
|
|
551
|
+
data: bytes.subarray(start, start + length)
|
|
552
|
+
};
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
const found = indexOfBytes(bytes, ENDSTREAM, start, end);
|
|
556
|
+
let stop = found < 0 ? end : found;
|
|
557
|
+
if (byteAt(bytes, stop - 1, end) === 0x0a && stop > start) stop--;
|
|
558
|
+
if (byteAt(bytes, stop - 1, end) === 0x0d && stop > start) stop--;
|
|
559
|
+
this.lexer.pos = found < 0 ? end : found + ENDSTREAM.length;
|
|
560
|
+
return {
|
|
561
|
+
kind: 'stream',
|
|
562
|
+
dict,
|
|
563
|
+
data: bytes.subarray(start, stop)
|
|
564
|
+
};
|
|
565
|
+
}
|
|
566
|
+
streamLength(dict) {
|
|
567
|
+
var _ref;
|
|
568
|
+
const value = dict.entries.get('Length');
|
|
569
|
+
let length = null;
|
|
570
|
+
if ((value == null ? void 0 : value.kind) === 'number') length = value.value;
|
|
571
|
+
else if ((value == null ? void 0 : value.kind) === 'ref') length = (_ref = this.options.resolveLength == null ? void 0 : this.options.resolveLength.call(this.options, value)) != null ? _ref : null;
|
|
572
|
+
return length !== null && Number.isSafeInteger(length) && length >= 0 ? length : null;
|
|
573
|
+
}
|
|
574
|
+
constructor(bytes, pos = 0, options = {}, end = bytes.length){
|
|
575
|
+
this.bytes = bytes;
|
|
576
|
+
this.options = options;
|
|
577
|
+
this.budget = PDF_MAX_TOKENS;
|
|
578
|
+
this.failed = false;
|
|
579
|
+
this.lexer = new PdfLexer(bytes, pos, end);
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
function isIntegerToken(token) {
|
|
583
|
+
return token.type === 'number' && INTEGER.test(token.text);
|
|
584
|
+
}
|
|
585
|
+
// ---------------------------------------------------------------------------
|
|
586
|
+
// Serializing
|
|
587
|
+
const HEX = '0123456789ABCDEF';
|
|
588
|
+
/** §7.3.5: regular characters 0x21–0x7E other than `#` are written as-is. */ export function serializePdfName(name) {
|
|
589
|
+
let out = '/';
|
|
590
|
+
for(let i = 0; i < name.length; i++){
|
|
591
|
+
const b = name.charCodeAt(i) & 0xff;
|
|
592
|
+
if (b > 0x20 && b < 0x7f && b !== 0x23 && CHAR_CLASS[b] === 0) {
|
|
593
|
+
out += String.fromCharCode(b);
|
|
594
|
+
} else {
|
|
595
|
+
out += '#' + HEX.charAt(b >> 4) + HEX.charAt(b & 15);
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
return out;
|
|
599
|
+
}
|
|
600
|
+
/**
|
|
601
|
+
* A string as a literal (§7.3.4.2) or, when it was read as one, a hex
|
|
602
|
+
* string (§7.3.4.3). Literal bytes outside printable ASCII are written as
|
|
603
|
+
* three-digit octal escapes so the output is 7-bit clean and byte-exact.
|
|
604
|
+
*/ export function serializePdfString(value) {
|
|
605
|
+
if (value.hex) {
|
|
606
|
+
let out = '<';
|
|
607
|
+
for (const b of value.bytes)out += HEX.charAt(b >> 4) + HEX.charAt(b & 15);
|
|
608
|
+
return out + '>';
|
|
609
|
+
}
|
|
610
|
+
let out = '(';
|
|
611
|
+
for (const b of value.bytes){
|
|
612
|
+
if (b === 0x28 || b === 0x29 || b === 0x5c) out += '\\' + String.fromCharCode(b);
|
|
613
|
+
else if (b === 0x0a) out += '\\n';
|
|
614
|
+
else if (b === 0x0d) out += '\\r';
|
|
615
|
+
else if (b === 0x09) out += '\\t';
|
|
616
|
+
else if (b >= 0x20 && b < 0x7f) out += String.fromCharCode(b);
|
|
617
|
+
else out += '\\' + b.toString(8).padStart(3, '0');
|
|
618
|
+
}
|
|
619
|
+
return out + ')';
|
|
620
|
+
}
|
|
621
|
+
/**
|
|
622
|
+
* An object as PDF syntax, in a binary string (one char per byte). Streams
|
|
623
|
+
* cannot be nested inside another object (§7.3.8.1), so one met here is
|
|
624
|
+
* written as `null`; a top-level stream is laid out by its writer.
|
|
625
|
+
*/ export function serializePdfObject(obj, depth = 0) {
|
|
626
|
+
if (depth > PDF_MAX_DEPTH) return 'null';
|
|
627
|
+
switch(obj.kind){
|
|
628
|
+
case 'null':
|
|
629
|
+
return 'null';
|
|
630
|
+
case 'boolean':
|
|
631
|
+
return obj.value ? 'true' : 'false';
|
|
632
|
+
case 'number':
|
|
633
|
+
return NUMBER.test(obj.text) ? obj.text : String(obj.value);
|
|
634
|
+
case 'string':
|
|
635
|
+
return serializePdfString(obj);
|
|
636
|
+
case 'name':
|
|
637
|
+
return serializePdfName(obj.name);
|
|
638
|
+
case 'ref':
|
|
639
|
+
return `${obj.num} ${obj.gen} R`;
|
|
640
|
+
case 'array':
|
|
641
|
+
return '[' + obj.items.map((item)=>serializePdfObject(item, depth + 1)).join(' ') + ']';
|
|
642
|
+
case 'dict':
|
|
643
|
+
{
|
|
644
|
+
const entries = [];
|
|
645
|
+
for (const [key, value] of obj.entries){
|
|
646
|
+
entries.push(serializePdfName(key) + ' ' + serializePdfObject(value, depth + 1));
|
|
647
|
+
}
|
|
648
|
+
return '<<' + entries.join(' ') + '>>';
|
|
649
|
+
}
|
|
650
|
+
case 'stream':
|
|
651
|
+
return 'null';
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
// ---------------------------------------------------------------------------
|
|
655
|
+
// Text strings (§7.9.2.2)
|
|
656
|
+
/**
|
|
657
|
+
* PDFDocEncoding (Annex D.2) where it differs from Latin-1: 0x18–0x1F are
|
|
658
|
+
* spacing accents and 0x80–0xA0 are typographic marks. 0x7F, 0x9F and 0xAD
|
|
659
|
+
* are undefined and read as their Latin-1 code points, as pdf.js does.
|
|
660
|
+
*/ const PDF_DOC_ENCODING = {
|
|
661
|
+
0x18: 0x02d8,
|
|
662
|
+
0x19: 0x02c7,
|
|
663
|
+
0x1a: 0x02c6,
|
|
664
|
+
0x1b: 0x02d9,
|
|
665
|
+
0x1c: 0x02dd,
|
|
666
|
+
0x1d: 0x02db,
|
|
667
|
+
0x1e: 0x02da,
|
|
668
|
+
0x1f: 0x02dc,
|
|
669
|
+
0x80: 0x2022,
|
|
670
|
+
0x81: 0x2020,
|
|
671
|
+
0x82: 0x2021,
|
|
672
|
+
0x83: 0x2026,
|
|
673
|
+
0x84: 0x2014,
|
|
674
|
+
0x85: 0x2013,
|
|
675
|
+
0x86: 0x0192,
|
|
676
|
+
0x87: 0x2044,
|
|
677
|
+
0x88: 0x2039,
|
|
678
|
+
0x89: 0x203a,
|
|
679
|
+
0x8a: 0x2212,
|
|
680
|
+
0x8b: 0x2030,
|
|
681
|
+
0x8c: 0x201e,
|
|
682
|
+
0x8d: 0x201c,
|
|
683
|
+
0x8e: 0x201d,
|
|
684
|
+
0x8f: 0x2018,
|
|
685
|
+
0x90: 0x2019,
|
|
686
|
+
0x91: 0x201a,
|
|
687
|
+
0x92: 0x2122,
|
|
688
|
+
0x93: 0xfb01,
|
|
689
|
+
0x94: 0xfb02,
|
|
690
|
+
0x95: 0x0141,
|
|
691
|
+
0x96: 0x0152,
|
|
692
|
+
0x97: 0x0160,
|
|
693
|
+
0x98: 0x0178,
|
|
694
|
+
0x99: 0x017d,
|
|
695
|
+
0x9a: 0x0131,
|
|
696
|
+
0x9b: 0x0142,
|
|
697
|
+
0x9c: 0x0153,
|
|
698
|
+
0x9d: 0x0161,
|
|
699
|
+
0x9e: 0x017e,
|
|
700
|
+
0xa0: 0x20ac
|
|
701
|
+
};
|
|
702
|
+
function utf16Text(bytes, start, littleEndian) {
|
|
703
|
+
let out = '';
|
|
704
|
+
for(let i = start; i + 1 < bytes.length; i += 2){
|
|
705
|
+
var _bytes_i, _bytes_;
|
|
706
|
+
const a = (_bytes_i = bytes[i]) != null ? _bytes_i : 0;
|
|
707
|
+
const b = (_bytes_ = bytes[i + 1]) != null ? _bytes_ : 0;
|
|
708
|
+
out += String.fromCharCode(littleEndian ? b * 256 + a : a * 256 + b);
|
|
709
|
+
}
|
|
710
|
+
// Lone surrogates from a truncated pair become U+FFFD.
|
|
711
|
+
return out.toWellFormed();
|
|
712
|
+
}
|
|
713
|
+
/**
|
|
714
|
+
* A text string's bytes as text (§7.9.2.2): UTF-16BE after a FE FF byte
|
|
715
|
+
* order mark, UTF-8 after EF BB BF (PDF 2.0), PDFDocEncoding otherwise.
|
|
716
|
+
* UTF-16LE after FF FE is not in the standard but is written by enough
|
|
717
|
+
* software that reading it as PDFDocEncoding ("ÿþ…") helps nobody.
|
|
718
|
+
* Language escapes (ESC lang ESC, §7.9.2.2.1) and trailing NULs are removed.
|
|
719
|
+
*/ export function decodePdfTextString(bytes) {
|
|
720
|
+
let text;
|
|
721
|
+
if (bytes[0] === 0xfe && bytes[1] === 0xff) {
|
|
722
|
+
text = utf16Text(bytes, 2, false);
|
|
723
|
+
} else if (bytes[0] === 0xff && bytes[1] === 0xfe) {
|
|
724
|
+
text = utf16Text(bytes, 2, true);
|
|
725
|
+
} else if (bytes[0] === 0xef && bytes[1] === 0xbb && bytes[2] === 0xbf) {
|
|
726
|
+
text = new TextDecoder('utf-8').decode(bytes.subarray(3));
|
|
727
|
+
} else {
|
|
728
|
+
var _PDF_DOC_ENCODING_b;
|
|
729
|
+
text = '';
|
|
730
|
+
for (const b of bytes)text += String.fromCharCode((_PDF_DOC_ENCODING_b = PDF_DOC_ENCODING[b]) != null ? _PDF_DOC_ENCODING_b : b);
|
|
731
|
+
}
|
|
732
|
+
return stripLanguageEscapes(text).replace(TRAILING_NULS, '');
|
|
733
|
+
}
|
|
734
|
+
const ESC = String.fromCharCode(0x1b);
|
|
735
|
+
const TRAILING_NULS = new RegExp(String.fromCharCode(0) + '+$');
|
|
736
|
+
/** §7.9.2.2.1: ESC, a 2-letter language, an optional 2-letter country, ESC. */ function stripLanguageEscapes(text) {
|
|
737
|
+
let out = '';
|
|
738
|
+
let at = 0;
|
|
739
|
+
for(;;){
|
|
740
|
+
const open = text.indexOf(ESC, at);
|
|
741
|
+
if (open < 0) break;
|
|
742
|
+
const close = text.indexOf(ESC, open + 1);
|
|
743
|
+
if (close < 0 || close - open > 9) {
|
|
744
|
+
out += text.slice(at, open + 1);
|
|
745
|
+
at = open + 1;
|
|
746
|
+
continue;
|
|
747
|
+
}
|
|
748
|
+
out += text.slice(at, open);
|
|
749
|
+
at = close + 1;
|
|
750
|
+
}
|
|
751
|
+
return out + text.slice(at);
|
|
752
|
+
}
|
|
753
|
+
/**
|
|
754
|
+
* Text as a text string: a literal when every char is printable ASCII (or
|
|
755
|
+
* tab, CR, LF, which escape cleanly and mean the same in PDFDocEncoding),
|
|
756
|
+
* otherwise UTF-16BE with a byte order mark, written as hex (§7.9.2.2).
|
|
757
|
+
*/ export function encodePdfTextString(text) {
|
|
758
|
+
if (/^[\x20-\x7e\t\n\r]*$/.test(text)) {
|
|
759
|
+
return {
|
|
760
|
+
kind: 'string',
|
|
761
|
+
bytes: latin1Bytes(text),
|
|
762
|
+
hex: false
|
|
763
|
+
};
|
|
764
|
+
}
|
|
765
|
+
const units = text.toWellFormed();
|
|
766
|
+
const bytes = new Uint8Array(2 + units.length * 2);
|
|
767
|
+
bytes[0] = 0xfe;
|
|
768
|
+
bytes[1] = 0xff;
|
|
769
|
+
for(let i = 0; i < units.length; i++){
|
|
770
|
+
const unit = units.charCodeAt(i);
|
|
771
|
+
bytes[2 + i * 2] = unit >> 8;
|
|
772
|
+
bytes[3 + i * 2] = unit & 0xff;
|
|
773
|
+
}
|
|
774
|
+
return {
|
|
775
|
+
kind: 'string',
|
|
776
|
+
bytes,
|
|
777
|
+
hex: true
|
|
778
|
+
};
|
|
779
|
+
}
|
|
780
|
+
const UTF8_STRICT = new TextDecoder('utf-8', {
|
|
781
|
+
fatal: true
|
|
782
|
+
});
|
|
783
|
+
/**
|
|
784
|
+
* A name's bytes as display text: UTF-8 when they are valid UTF-8 (PDF 2.0
|
|
785
|
+
* §7.3.5 recommends it), Latin-1 otherwise.
|
|
786
|
+
*/ export function pdfNameText(name) {
|
|
787
|
+
let ascii = true;
|
|
788
|
+
for(let i = 0; i < name.length && ascii; i++)ascii = name.charCodeAt(i) < 0x80;
|
|
789
|
+
if (ascii) return name;
|
|
790
|
+
try {
|
|
791
|
+
return UTF8_STRICT.decode(latin1Bytes(name));
|
|
792
|
+
} catch (unused) {
|
|
793
|
+
return name;
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
/** Display text as name bytes (UTF-8), the inverse of {@link pdfNameText}. */ export function pdfNameFromText(text) {
|
|
797
|
+
return latin1Text(new TextEncoder().encode(text));
|
|
798
|
+
}
|
|
799
|
+
function asArray(obj, resolve) {
|
|
800
|
+
const value = resolve(obj);
|
|
801
|
+
if (!value || value.kind === 'null') return [];
|
|
802
|
+
return value.kind === 'array' ? value.items : [
|
|
803
|
+
value
|
|
804
|
+
];
|
|
805
|
+
}
|
|
806
|
+
function intParam(dict, key, fallback) {
|
|
807
|
+
const value = dict == null ? void 0 : dict.entries.get(key);
|
|
808
|
+
return (value == null ? void 0 : value.kind) === 'number' && Number.isInteger(value.value) ? value.value : fallback;
|
|
809
|
+
}
|
|
810
|
+
/**
|
|
811
|
+
* zlib inflation capped at `maxOutputLength`. A strict pass first; a stream
|
|
812
|
+
* truncated or missing its Adler-32 (common, and harmless to the data) is
|
|
813
|
+
* retried with a sync flush, and one written without a zlib header as raw
|
|
814
|
+
* deflate. `null` when none of them produces anything.
|
|
815
|
+
*/ export function inflatePdf(data, maxOutputLength) {
|
|
816
|
+
const options = {
|
|
817
|
+
maxOutputLength
|
|
818
|
+
};
|
|
819
|
+
const lenient = {
|
|
820
|
+
maxOutputLength,
|
|
821
|
+
finishFlush: constants.Z_SYNC_FLUSH
|
|
822
|
+
};
|
|
823
|
+
const attempts = [
|
|
824
|
+
()=>inflateSync(data, options),
|
|
825
|
+
()=>inflateSync(data, lenient),
|
|
826
|
+
()=>inflateRawSync(data, lenient)
|
|
827
|
+
];
|
|
828
|
+
for (const attempt of attempts){
|
|
829
|
+
try {
|
|
830
|
+
const out = attempt();
|
|
831
|
+
return new Uint8Array(out.buffer, out.byteOffset, out.byteLength);
|
|
832
|
+
} catch (unused) {
|
|
833
|
+
// The next attempt is more lenient; a size-cap error fails them all.
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
return null;
|
|
837
|
+
}
|
|
838
|
+
/**
|
|
839
|
+
* §7.4.4.4: undo a TIFF (2) or PNG (10–15) predictor. PNG rows each carry
|
|
840
|
+
* their own filter-type byte, so the Predictor value past 10 is only a hint.
|
|
841
|
+
* `null` for parameters this cannot honor.
|
|
842
|
+
*/ export function unpredict(data, params) {
|
|
843
|
+
const predictor = intParam(params, 'Predictor', 1);
|
|
844
|
+
if (predictor === 1) return data;
|
|
845
|
+
const colors = intParam(params, 'Colors', 1);
|
|
846
|
+
const bpc = intParam(params, 'BitsPerComponent', 8);
|
|
847
|
+
const columns = intParam(params, 'Columns', 1);
|
|
848
|
+
if (colors < 1 || colors > 32 || ![
|
|
849
|
+
1,
|
|
850
|
+
2,
|
|
851
|
+
4,
|
|
852
|
+
8,
|
|
853
|
+
16
|
|
854
|
+
].includes(bpc)) return null;
|
|
855
|
+
if (columns < 1 || columns > 1 << 24) return null;
|
|
856
|
+
const bpp = Math.max(1, Math.ceil(colors * bpc / 8));
|
|
857
|
+
const rowLength = Math.ceil(colors * bpc * columns / 8);
|
|
858
|
+
if (predictor === 2) {
|
|
859
|
+
if (bpc !== 8) return null;
|
|
860
|
+
const out = data.slice();
|
|
861
|
+
for(let row = 0; row < out.length; row += rowLength){
|
|
862
|
+
for(let i = row + bpp; i < Math.min(row + rowLength, out.length); i++){
|
|
863
|
+
var _out_i, _out_;
|
|
864
|
+
out[i] = ((_out_i = out[i]) != null ? _out_i : 0) + ((_out_ = out[i - bpp]) != null ? _out_ : 0) & 0xff;
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
return out;
|
|
868
|
+
}
|
|
869
|
+
if (predictor < 10 || predictor > 15) return null;
|
|
870
|
+
const rows = Math.ceil(data.length / (rowLength + 1));
|
|
871
|
+
// Every row writes at most its own input, so the output is never longer
|
|
872
|
+
// than the input — a hostile /Columns cannot make this allocate more.
|
|
873
|
+
const out = new Uint8Array(Math.min(rows * rowLength, data.length));
|
|
874
|
+
let written = 0;
|
|
875
|
+
for(let row = 0; row < rows; row++){
|
|
876
|
+
var _data_src;
|
|
877
|
+
const src = row * (rowLength + 1);
|
|
878
|
+
const type = (_data_src = data[src]) != null ? _data_src : 0;
|
|
879
|
+
const base = row * rowLength;
|
|
880
|
+
const available = Math.min(rowLength, data.length - src - 1);
|
|
881
|
+
for(let i = 0; i < available; i++){
|
|
882
|
+
var _data_, _out_1, _out_2, _out_3;
|
|
883
|
+
const raw = (_data_ = data[src + 1 + i]) != null ? _data_ : 0;
|
|
884
|
+
const left = i >= bpp ? (_out_1 = out[base + i - bpp]) != null ? _out_1 : 0 : 0;
|
|
885
|
+
const up = row > 0 ? (_out_2 = out[base - rowLength + i]) != null ? _out_2 : 0 : 0;
|
|
886
|
+
const upLeft = row > 0 && i >= bpp ? (_out_3 = out[base - rowLength + i - bpp]) != null ? _out_3 : 0 : 0;
|
|
887
|
+
let value;
|
|
888
|
+
switch(type){
|
|
889
|
+
case 1:
|
|
890
|
+
value = raw + left;
|
|
891
|
+
break;
|
|
892
|
+
case 2:
|
|
893
|
+
value = raw + up;
|
|
894
|
+
break;
|
|
895
|
+
case 3:
|
|
896
|
+
value = raw + (left + up >> 1);
|
|
897
|
+
break;
|
|
898
|
+
case 4:
|
|
899
|
+
{
|
|
900
|
+
const p = left + up - upLeft;
|
|
901
|
+
const pa = Math.abs(p - left);
|
|
902
|
+
const pb = Math.abs(p - up);
|
|
903
|
+
const pc = Math.abs(p - upLeft);
|
|
904
|
+
value = raw + (pa <= pb && pa <= pc ? left : pb <= pc ? up : upLeft);
|
|
905
|
+
break;
|
|
906
|
+
}
|
|
907
|
+
default:
|
|
908
|
+
value = raw;
|
|
909
|
+
}
|
|
910
|
+
out[base + i] = value & 0xff;
|
|
911
|
+
}
|
|
912
|
+
written = base + Math.max(0, available);
|
|
913
|
+
}
|
|
914
|
+
return out.subarray(0, written);
|
|
915
|
+
}
|
|
916
|
+
/**
|
|
917
|
+
* A stream's decoded data, or `null` when it uses a filter other than
|
|
918
|
+
* FlateDecode (§7.4.4) or does not decode within `maxOutputLength`.
|
|
919
|
+
*/ export function decodePdfStream(stream, resolve, maxOutputLength) {
|
|
920
|
+
const filters = asArray(stream.dict.entries.get('Filter'), resolve);
|
|
921
|
+
const params = asArray(stream.dict.entries.get('DecodeParms'), resolve);
|
|
922
|
+
if (filters.length > 8) return null;
|
|
923
|
+
let data = stream.data;
|
|
924
|
+
for(let i = 0; i < filters.length && data; i++){
|
|
925
|
+
const filter = resolve(filters[i]);
|
|
926
|
+
if ((filter == null ? void 0 : filter.kind) !== 'name') return null;
|
|
927
|
+
if (filter.name !== 'FlateDecode' && filter.name !== 'Fl') return null;
|
|
928
|
+
const param = resolve(params[i]);
|
|
929
|
+
data = inflatePdf(data, maxOutputLength);
|
|
930
|
+
if (data) data = unpredict(data, (param == null ? void 0 : param.kind) === 'dict' ? param : null);
|
|
931
|
+
}
|
|
932
|
+
if (data && data.length > maxOutputLength) return null;
|
|
933
|
+
return data;
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
//# sourceMappingURL=pdf-objects.js.map
|