@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,1167 @@
|
|
|
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
|
+
* A PDF's document metadata, read and written back without touching a
|
|
18
|
+
* byte of its page content (AGL-3331). Section numbers are ISO 32000-2.
|
|
19
|
+
*
|
|
20
|
+
* ## What is read
|
|
21
|
+
*
|
|
22
|
+
* The document information dictionary the trailer's `/Info` names
|
|
23
|
+
* (§14.3.3), the catalog's `/Metadata` XMP stream (§14.3.2), the page count
|
|
24
|
+
* (`/Root /Pages /Count`), the version, and whether the file is encrypted
|
|
25
|
+
* (§7.6) or signed (§12.8) — the two things that make it read-only.
|
|
26
|
+
*
|
|
27
|
+
* ## How the file is navigated
|
|
28
|
+
*
|
|
29
|
+
* From the last `startxref`, through every cross-reference section on the
|
|
30
|
+
* `/Prev` chain, newest first, the first definition of an object winning
|
|
31
|
+
* (§7.5.6 incremental updates):
|
|
32
|
+
*
|
|
33
|
+
* - classic `xref` tables with their `trailer` (§7.5.4, §7.5.5);
|
|
34
|
+
* - cross-reference streams (§7.5.8), Flate-compressed with a PNG predictor
|
|
35
|
+
* as most writers leave them;
|
|
36
|
+
* - hybrid files, whose table's trailer points at a supplementary stream
|
|
37
|
+
* with `/XRefStm` (§7.5.8.4);
|
|
38
|
+
* - linearized files (Annex F), which need no special case: their final
|
|
39
|
+
* `startxref` names the first-page section, whose `/Prev` is the rest;
|
|
40
|
+
* - objects compressed into object streams (§7.5.7).
|
|
41
|
+
*
|
|
42
|
+
* A file whose cross-reference data does not hold up — a bad offset, a
|
|
43
|
+
* section that does not parse, a `/Prev` loop — is still READ, from a
|
|
44
|
+
* bounded scan for `N G obj` headers and the last trailer, but it is never
|
|
45
|
+
* WRITTEN: an update chained onto a table we had to guess at could make the
|
|
46
|
+
* file worse for every reader that trusted the table.
|
|
47
|
+
*
|
|
48
|
+
* ## How an edit is written
|
|
49
|
+
*
|
|
50
|
+
* As an incremental update (§7.5.6): the new object definitions, a new
|
|
51
|
+
* cross-reference section of the kind the file already ends with, and a
|
|
52
|
+
* trailer whose `/Prev` points at the old one — all APPENDED. The original
|
|
53
|
+
* bytes are a byte-for-byte prefix of the output, which is what guarantees
|
|
54
|
+
* page content, fonts and images are untouched, and what lets any reader
|
|
55
|
+
* that ignores the update still open the file as it was. An encrypted file
|
|
56
|
+
* (its strings are ciphertext) or a signed one (the update would break the
|
|
57
|
+
* signature) is refused with an {@link EmbeddedWriteError}.
|
|
58
|
+
*/ import { decodePdfStream, decodePdfTextString, encodePdfTextString, indexOfBytes, lastIndexOfBytes, latin1Bytes, latin1Text, PDF_NULL, PdfLexer, pdfNameFromText, pdfNameText, PdfParser, serializePdfObject } from "./pdf-objects.js";
|
|
59
|
+
import { EmbeddedWriteError } from "./types.js";
|
|
60
|
+
/** Cross-reference sections followed along `/Prev` before giving up. */ const MAX_SECTIONS = 512;
|
|
61
|
+
/** Cross-reference entries kept; far more than a 25 MB file can hold. */ const MAX_XREF_ENTRIES = 1 << 21;
|
|
62
|
+
/** Decoded size of one cross-reference or object stream. */ const MAX_STRUCTURE_BYTES = 64 * 1024 * 1024;
|
|
63
|
+
/** Decoded size of the XMP stream. */ const MAX_XMP_BYTES = 16 * 1024 * 1024;
|
|
64
|
+
/** Objects one object stream may declare. */ const MAX_OBJSTM_OBJECTS = 1 << 20;
|
|
65
|
+
/** `N G obj` headers the repair scan records. */ const MAX_SCAN_OBJECTS = 1 << 21;
|
|
66
|
+
/** Objects being loaded at once (a `/Length` inside a stream inside …). */ const MAX_LOAD_NESTING = 16;
|
|
67
|
+
/** Links followed from a reference to a reference. */ const MAX_REF_CHAIN = 16;
|
|
68
|
+
/** AcroForm field nodes visited looking for a signature. */ const MAX_FIELD_NODES = 20000;
|
|
69
|
+
/** Longest non-string Info value shown as PDF syntax. */ const MAX_VALUE_TEXT = 4096;
|
|
70
|
+
/** Annex C.2: the most indirect objects a conforming reader must handle. */ const MAX_OBJECT_NUMBER = 8388607;
|
|
71
|
+
const DATE_KEYS = new Set([
|
|
72
|
+
'CreationDate',
|
|
73
|
+
'ModDate'
|
|
74
|
+
]);
|
|
75
|
+
const ascii = (text)=>latin1Bytes(text);
|
|
76
|
+
const STARTXREF = ascii('startxref');
|
|
77
|
+
const OBJ = ascii('obj');
|
|
78
|
+
const TRAILER = ascii('trailer');
|
|
79
|
+
const XREF_TYPE = ascii('/XRef');
|
|
80
|
+
const OBJSTM_TYPE = ascii('/ObjStm');
|
|
81
|
+
const CATALOG_TYPE = ascii('/Catalog');
|
|
82
|
+
const BYTE_RANGE = ascii('/ByteRange');
|
|
83
|
+
function isDigit(b) {
|
|
84
|
+
return b >= 0x30 && b <= 0x39;
|
|
85
|
+
}
|
|
86
|
+
function isSpace(b) {
|
|
87
|
+
return b === 0x00 || b === 0x09 || b === 0x0a || b === 0x0c || b === 0x0d || b === 0x20;
|
|
88
|
+
}
|
|
89
|
+
/** Whether `b` can continue a keyword — so `pattern` found before it is only a prefix. */ function continuesWord(b) {
|
|
90
|
+
if (b < 0) return false;
|
|
91
|
+
return !isSpace(b) && !'()<>[]{}/%'.includes(String.fromCharCode(b));
|
|
92
|
+
}
|
|
93
|
+
/** A non-negative safe integer, or `null`. */ function intValue(obj) {
|
|
94
|
+
return (obj == null ? void 0 : obj.kind) === 'number' && Number.isSafeInteger(obj.value) && obj.value >= 0 ? obj.value : null;
|
|
95
|
+
}
|
|
96
|
+
function isInt(token) {
|
|
97
|
+
return token.type === 'number' && /^\d+$/.test(token.text);
|
|
98
|
+
}
|
|
99
|
+
function asDict(obj) {
|
|
100
|
+
if ((obj == null ? void 0 : obj.kind) === 'dict') return obj;
|
|
101
|
+
if ((obj == null ? void 0 : obj.kind) === 'stream') return obj.dict;
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
function nameIs(obj, name) {
|
|
105
|
+
return (obj == null ? void 0 : obj.kind) === 'name' && obj.name === name;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* One part of a read, so a failure in it costs only that part. Nothing
|
|
109
|
+
* below is expected to throw — it is bounded and total — but a read of
|
|
110
|
+
* hostile bytes is where "expected" deserves a backstop.
|
|
111
|
+
*/ function attempt(read, fallback) {
|
|
112
|
+
try {
|
|
113
|
+
return read();
|
|
114
|
+
} catch (unused) {
|
|
115
|
+
return fallback;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
/** §7.5.2: `%PDF-x.y`, which Acrobat accepts anywhere in the first 1024 bytes. */ function findHeader(bytes) {
|
|
119
|
+
const offset = indexOfBytes(bytes, ascii('%PDF-'), 0, 1024 + 5);
|
|
120
|
+
if (offset < 0) return null;
|
|
121
|
+
const head = latin1Text(bytes.subarray(offset + 5, offset + 16));
|
|
122
|
+
const match = /^(\d{1,2})\.(\d{1,2})/.exec(head);
|
|
123
|
+
return {
|
|
124
|
+
offset,
|
|
125
|
+
version: match ? `${Number(match[1])}.${Number(match[2])}` : null
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
/** The byte offset after the file's LAST `startxref` (§7.5.5), or `null`. */ function findStartxref(bytes) {
|
|
129
|
+
const at = lastIndexOfBytes(bytes, STARTXREF);
|
|
130
|
+
if (at < 0) return null;
|
|
131
|
+
const token = new PdfLexer(bytes, at + STARTXREF.length).next();
|
|
132
|
+
return isInt(token) ? token.value : null;
|
|
133
|
+
}
|
|
134
|
+
/** A big-endian unsigned field of a cross-reference stream entry. */ function readField(data, at, width) {
|
|
135
|
+
var _data_;
|
|
136
|
+
let value = 0;
|
|
137
|
+
for(let i = 0; i < width; i++)value = value * 256 + ((_data_ = data[at + i]) != null ? _data_ : 0);
|
|
138
|
+
return value;
|
|
139
|
+
}
|
|
140
|
+
/** The `N G` before an `obj` keyword at `at`, walking back over at most a few bytes. */ function headerBefore(bytes, at) {
|
|
141
|
+
var _bytes_j;
|
|
142
|
+
let j = at - 1;
|
|
143
|
+
const skipSpace = ()=>{
|
|
144
|
+
var _bytes_j, _bytes_j1;
|
|
145
|
+
const stop = j - 64;
|
|
146
|
+
if (!isSpace((_bytes_j = bytes[j]) != null ? _bytes_j : -1)) return false;
|
|
147
|
+
while(j > stop && isSpace((_bytes_j1 = bytes[j]) != null ? _bytes_j1 : -1))j--;
|
|
148
|
+
return true;
|
|
149
|
+
};
|
|
150
|
+
const digits = (max)=>{
|
|
151
|
+
var _bytes_j, _bytes_k;
|
|
152
|
+
const end = j;
|
|
153
|
+
while(end - j < max && isDigit((_bytes_j = bytes[j]) != null ? _bytes_j : -1))j--;
|
|
154
|
+
if (end === j) return null;
|
|
155
|
+
let value = 0;
|
|
156
|
+
for(let k = j + 1; k <= end; k++)value = value * 10 + (((_bytes_k = bytes[k]) != null ? _bytes_k : 0x30) - 0x30);
|
|
157
|
+
return value;
|
|
158
|
+
};
|
|
159
|
+
if (!skipSpace()) return null;
|
|
160
|
+
const gen = digits(5);
|
|
161
|
+
if (gen === null || !skipSpace()) return null;
|
|
162
|
+
const num = digits(10);
|
|
163
|
+
if (num === null) return null;
|
|
164
|
+
if (j >= 0 && continuesWord((_bytes_j = bytes[j]) != null ? _bytes_j : -1)) return null;
|
|
165
|
+
return {
|
|
166
|
+
num,
|
|
167
|
+
gen,
|
|
168
|
+
offset: j + 1
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* The file, navigated. Objects load lazily and are cached; everything a
|
|
173
|
+
* reader asks of it returns `null` rather than throwing.
|
|
174
|
+
*/ let PdfFile = class PdfFile {
|
|
175
|
+
/** The `/Root` reference and the catalog it names. */ root() {
|
|
176
|
+
var _this_trailer;
|
|
177
|
+
const ref = (_this_trailer = this.trailer) == null ? void 0 : _this_trailer.entries.get('Root');
|
|
178
|
+
if ((ref == null ? void 0 : ref.kind) !== 'ref') return null;
|
|
179
|
+
const catalog = this.resolve(ref);
|
|
180
|
+
return (catalog == null ? void 0 : catalog.kind) === 'dict' ? {
|
|
181
|
+
ref,
|
|
182
|
+
catalog
|
|
183
|
+
} : null;
|
|
184
|
+
}
|
|
185
|
+
/** The `/Info` dictionary, when the newest trailer names one. */ info() {
|
|
186
|
+
var _this_trailer;
|
|
187
|
+
const info = this.resolve((_this_trailer = this.trailer) == null ? void 0 : _this_trailer.entries.get('Info'));
|
|
188
|
+
return (info == null ? void 0 : info.kind) === 'dict' ? info : null;
|
|
189
|
+
}
|
|
190
|
+
encrypted() {
|
|
191
|
+
return this.trailers.some((trailer)=>trailer.entries.has('Encrypt'));
|
|
192
|
+
}
|
|
193
|
+
getObject(num) {
|
|
194
|
+
if (!Number.isSafeInteger(num) || num < 0) return null;
|
|
195
|
+
const cached = this.cache.get(num);
|
|
196
|
+
if (cached !== undefined) return cached;
|
|
197
|
+
if (this.loading.has(num) || this.loading.size >= MAX_LOAD_NESTING) return null;
|
|
198
|
+
this.loading.add(num);
|
|
199
|
+
let obj;
|
|
200
|
+
try {
|
|
201
|
+
obj = this.loadObject(num);
|
|
202
|
+
} finally{
|
|
203
|
+
this.loading.delete(num);
|
|
204
|
+
}
|
|
205
|
+
this.cache.set(num, obj);
|
|
206
|
+
return obj;
|
|
207
|
+
}
|
|
208
|
+
loadObject(num) {
|
|
209
|
+
const entry = this.entries.get(num);
|
|
210
|
+
if ((entry == null ? void 0 : entry.type) === 0) return null;
|
|
211
|
+
if ((entry == null ? void 0 : entry.type) === 1) {
|
|
212
|
+
const obj = this.parseAt(entry.offset, num);
|
|
213
|
+
if (obj) return obj;
|
|
214
|
+
this.repaired = true;
|
|
215
|
+
} else if ((entry == null ? void 0 : entry.type) === 2) {
|
|
216
|
+
const obj = this.loadCompressed(entry.stream, entry.index, num);
|
|
217
|
+
if (obj) return obj;
|
|
218
|
+
this.repaired = true;
|
|
219
|
+
}
|
|
220
|
+
// Not in the table (a dangling reference is legal), or not where the
|
|
221
|
+
// table says: look for it by scanning.
|
|
222
|
+
const header = this.scan().latest.get(num);
|
|
223
|
+
if (!header) return null;
|
|
224
|
+
this.repaired = true;
|
|
225
|
+
return this.parseAt(header.offset, num);
|
|
226
|
+
}
|
|
227
|
+
parseAt(offset, num) {
|
|
228
|
+
const tryAt = (at)=>{
|
|
229
|
+
if (!(at >= 0 && at < this.bytes.length)) return null;
|
|
230
|
+
const parser = new PdfParser(this.bytes, at, {
|
|
231
|
+
resolveLength: (ref)=>{
|
|
232
|
+
var _this_resolve;
|
|
233
|
+
return intValue((_this_resolve = this.resolve(ref)) != null ? _this_resolve : undefined);
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
const parsed = parser.parseIndirectObject();
|
|
237
|
+
return parsed && parsed.num === num ? parsed.obj : null;
|
|
238
|
+
};
|
|
239
|
+
const obj = tryAt(offset);
|
|
240
|
+
if (obj || this.headerOffset === 0) return obj;
|
|
241
|
+
// Offsets counted from the header rather than the file's first byte.
|
|
242
|
+
const shifted = tryAt(offset + this.headerOffset);
|
|
243
|
+
if (shifted) this.repaired = true;
|
|
244
|
+
return shifted;
|
|
245
|
+
}
|
|
246
|
+
loadCompressed(stream, index, num) {
|
|
247
|
+
const objstm = this.objectStream(stream);
|
|
248
|
+
if (!objstm) return null;
|
|
249
|
+
let at = objstm.numbers[index] === num ? index : objstm.numbers.indexOf(num);
|
|
250
|
+
if (at < 0) return null;
|
|
251
|
+
const offset = objstm.offsets[at];
|
|
252
|
+
if (offset === undefined) return null;
|
|
253
|
+
at = objstm.first + offset;
|
|
254
|
+
if (at >= objstm.data.length) return null;
|
|
255
|
+
return new PdfParser(objstm.data, at).parseObject();
|
|
256
|
+
}
|
|
257
|
+
/** §7.5.7: an object stream's header of `N` number/offset pairs. */ objectStream(num) {
|
|
258
|
+
var _this_objectStreams_get;
|
|
259
|
+
var _this_entries_get;
|
|
260
|
+
if (this.objectStreams.has(num)) return (_this_objectStreams_get = this.objectStreams.get(num)) != null ? _this_objectStreams_get : null;
|
|
261
|
+
this.objectStreams.set(num, null);
|
|
262
|
+
// A stream is never itself compressed; one claiming to be is a loop.
|
|
263
|
+
if (((_this_entries_get = this.entries.get(num)) == null ? void 0 : _this_entries_get.type) === 2) return null;
|
|
264
|
+
const obj = this.getObject(num);
|
|
265
|
+
if ((obj == null ? void 0 : obj.kind) !== 'stream') return null;
|
|
266
|
+
const n = intValue(obj.dict.entries.get('N'));
|
|
267
|
+
const first = intValue(obj.dict.entries.get('First'));
|
|
268
|
+
if (n === null || first === null || n > MAX_OBJSTM_OBJECTS) return null;
|
|
269
|
+
const data = decodePdfStream(obj, this.resolve, MAX_STRUCTURE_BYTES);
|
|
270
|
+
if (!data || first > data.length) return null;
|
|
271
|
+
const lexer = new PdfLexer(data, 0, first);
|
|
272
|
+
const numbers = [];
|
|
273
|
+
const offsets = [];
|
|
274
|
+
for(let i = 0; i < n; i++){
|
|
275
|
+
const a = lexer.next();
|
|
276
|
+
const b = lexer.next();
|
|
277
|
+
if (!isInt(a) || !isInt(b)) break;
|
|
278
|
+
numbers.push(a.value);
|
|
279
|
+
offsets.push(b.value);
|
|
280
|
+
}
|
|
281
|
+
const objstm = {
|
|
282
|
+
data,
|
|
283
|
+
first,
|
|
284
|
+
numbers,
|
|
285
|
+
offsets
|
|
286
|
+
};
|
|
287
|
+
this.objectStreams.set(num, objstm);
|
|
288
|
+
return objstm;
|
|
289
|
+
}
|
|
290
|
+
/** The next free object number for an appended object. */ nextObjectNumber() {
|
|
291
|
+
var _intValue;
|
|
292
|
+
var _this_trailer;
|
|
293
|
+
let next = Math.max(1, (_intValue = intValue((_this_trailer = this.trailer) == null ? void 0 : _this_trailer.entries.get('Size'))) != null ? _intValue : 0);
|
|
294
|
+
for (const num of this.entries.keys())if (num >= next) next = num + 1;
|
|
295
|
+
return next;
|
|
296
|
+
}
|
|
297
|
+
// -------------------------------------------------------------------------
|
|
298
|
+
// The cross-reference chain
|
|
299
|
+
/** Reads the `/Prev` chain from the last `startxref`. `false` when it does not hold up. */ readXref() {
|
|
300
|
+
var _ref;
|
|
301
|
+
var _this_sections_;
|
|
302
|
+
const start = findStartxref(this.bytes);
|
|
303
|
+
if (start === null) return false;
|
|
304
|
+
this.startxref = start;
|
|
305
|
+
const visited = new Set();
|
|
306
|
+
let offset = start;
|
|
307
|
+
while(offset !== null){
|
|
308
|
+
if (visited.has(offset) || visited.size >= MAX_SECTIONS) {
|
|
309
|
+
// A /Prev loop: what was read stands, but the file is not sound.
|
|
310
|
+
this.repaired = true;
|
|
311
|
+
break;
|
|
312
|
+
}
|
|
313
|
+
visited.add(offset);
|
|
314
|
+
const section = this.readSection(offset);
|
|
315
|
+
if (!section) return false;
|
|
316
|
+
const local = new Map(section.entries);
|
|
317
|
+
if (section.kind === 'table') {
|
|
318
|
+
// §7.5.8.4: a hybrid file's table lists the objects a PDF 1.4
|
|
319
|
+
// reader can find; the stream its /XRefStm names adds the ones in
|
|
320
|
+
// object streams, which the table marks free.
|
|
321
|
+
const stmOffset = intValue(section.trailer.entries.get('XRefStm'));
|
|
322
|
+
if (stmOffset !== null && !visited.has(stmOffset)) {
|
|
323
|
+
visited.add(stmOffset);
|
|
324
|
+
const stm = this.readSection(stmOffset);
|
|
325
|
+
if ((stm == null ? void 0 : stm.kind) !== 'stream') return false;
|
|
326
|
+
for (const [num, entry] of stm.entries){
|
|
327
|
+
var _local_get;
|
|
328
|
+
if (((_local_get = local.get(num)) == null ? void 0 : _local_get.type) !== 1) local.set(num, entry);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
for (const [num, entry] of local){
|
|
333
|
+
if (!this.entries.has(num)) this.entries.set(num, entry);
|
|
334
|
+
}
|
|
335
|
+
if (this.entries.size > MAX_XREF_ENTRIES) return false;
|
|
336
|
+
this.sections.push(section);
|
|
337
|
+
this.trailers.push(section.trailer);
|
|
338
|
+
const prev = intValue(section.trailer.entries.get('Prev'));
|
|
339
|
+
// Some writers put /Prev 0 on a file's only section; 0 is the header.
|
|
340
|
+
offset = prev ? prev : null;
|
|
341
|
+
}
|
|
342
|
+
this.trailer = (_ref = (_this_sections_ = this.sections[0]) == null ? void 0 : _this_sections_.trailer) != null ? _ref : null;
|
|
343
|
+
return this.trailer !== null;
|
|
344
|
+
}
|
|
345
|
+
readSection(offset) {
|
|
346
|
+
if (!(offset >= 0 && offset < this.bytes.length)) return null;
|
|
347
|
+
const lexer = new PdfLexer(this.bytes, offset);
|
|
348
|
+
return lexer.peekKeyword('xref') ? this.readTable(lexer, offset) : this.readXrefStream(offset);
|
|
349
|
+
}
|
|
350
|
+
/** §7.5.4: `xref`, subsections of `start count` and 20-byte entries, then `trailer`. */ readTable(lexer, offset) {
|
|
351
|
+
lexer.next();
|
|
352
|
+
const entries = [];
|
|
353
|
+
while(!lexer.peekKeyword('trailer')){
|
|
354
|
+
const startToken = lexer.next();
|
|
355
|
+
const countToken = lexer.next();
|
|
356
|
+
if (!isInt(startToken) || !isInt(countToken)) return null;
|
|
357
|
+
let start = startToken.value;
|
|
358
|
+
const count = countToken.value;
|
|
359
|
+
if (entries.length + count > MAX_XREF_ENTRIES) return null;
|
|
360
|
+
for(let i = 0; i < count; i++){
|
|
361
|
+
const a = lexer.next();
|
|
362
|
+
const b = lexer.next();
|
|
363
|
+
const t = lexer.next();
|
|
364
|
+
if (!isInt(a) || !isInt(b) || t.type !== 'keyword') return null;
|
|
365
|
+
if (t.text !== 'n' && t.text !== 'f') return null;
|
|
366
|
+
// A common writer bug numbers the first subsection from 1 although
|
|
367
|
+
// it opens with object 0's free-list head; pdf.js and Poppler shift it.
|
|
368
|
+
if (i === 0 && start === 1 && t.text === 'f' && a.value === 0 && b.value === 65535) {
|
|
369
|
+
start = 0;
|
|
370
|
+
}
|
|
371
|
+
entries.push([
|
|
372
|
+
start + i,
|
|
373
|
+
t.text === 'n' && a.value > 0 ? {
|
|
374
|
+
type: 1,
|
|
375
|
+
offset: a.value,
|
|
376
|
+
gen: b.value
|
|
377
|
+
} : {
|
|
378
|
+
type: 0
|
|
379
|
+
}
|
|
380
|
+
]);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
lexer.next();
|
|
384
|
+
const trailer = new PdfParser(this.bytes, lexer.pos).parseObject();
|
|
385
|
+
if ((trailer == null ? void 0 : trailer.kind) !== 'dict') return null;
|
|
386
|
+
return {
|
|
387
|
+
offset,
|
|
388
|
+
kind: 'table',
|
|
389
|
+
trailer,
|
|
390
|
+
entries
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
/**
|
|
394
|
+
* §7.5.8: an indirect stream object with `/Type /XRef`. Its entries are
|
|
395
|
+
* `/W`-wide big-endian fields, listed for the `/Index` ranges (default
|
|
396
|
+
* `[0 Size]`). Its dictionary entries are direct (§7.5.8.2), so nothing
|
|
397
|
+
* here resolves a reference.
|
|
398
|
+
*/ readXrefStream(offset) {
|
|
399
|
+
var _intValue;
|
|
400
|
+
const parsed = new PdfParser(this.bytes, offset).parseIndirectObject();
|
|
401
|
+
if ((parsed == null ? void 0 : parsed.obj.kind) !== 'stream') return null;
|
|
402
|
+
const stream = parsed.obj;
|
|
403
|
+
const dict = stream.dict.entries;
|
|
404
|
+
if (!nameIs(dict.get('Type'), 'XRef')) return null;
|
|
405
|
+
const direct = (obj)=>obj;
|
|
406
|
+
const data = decodePdfStream(stream, direct, MAX_STRUCTURE_BYTES);
|
|
407
|
+
const w = dict.get('W');
|
|
408
|
+
if (!data || (w == null ? void 0 : w.kind) !== 'array' || w.items.length < 3) return null;
|
|
409
|
+
const widths = w.items.slice(0, 3).map((item)=>intValue(item));
|
|
410
|
+
const [w0, w1, w2] = widths;
|
|
411
|
+
if (w0 == null || w1 == null || w2 == null) return null;
|
|
412
|
+
if (w0 > 4 || w1 > 8 || w2 > 8 || w0 + w1 + w2 === 0) return null;
|
|
413
|
+
const size = (_intValue = intValue(dict.get('Size'))) != null ? _intValue : 0;
|
|
414
|
+
const index = dict.get('Index');
|
|
415
|
+
const ranges = (index == null ? void 0 : index.kind) === 'array' ? index.items.map((item)=>intValue(item)) : [
|
|
416
|
+
0,
|
|
417
|
+
size
|
|
418
|
+
];
|
|
419
|
+
if (ranges.length % 2 || ranges.some((value)=>value === null)) return null;
|
|
420
|
+
const width = w0 + w1 + w2;
|
|
421
|
+
const entries = [];
|
|
422
|
+
let at = 0;
|
|
423
|
+
for(let r = 0; r < ranges.length; r += 2){
|
|
424
|
+
var _ranges_r, _ranges_;
|
|
425
|
+
const first = (_ranges_r = ranges[r]) != null ? _ranges_r : 0;
|
|
426
|
+
const count = (_ranges_ = ranges[r + 1]) != null ? _ranges_ : 0;
|
|
427
|
+
for(let i = 0; i < count && at + width <= data.length; i++, at += width){
|
|
428
|
+
if (entries.length >= MAX_XREF_ENTRIES) return null;
|
|
429
|
+
// §7.5.8.3: a missing type field means type 1.
|
|
430
|
+
const type = w0 ? readField(data, at, w0) : 1;
|
|
431
|
+
const a = readField(data, at + w0, w1);
|
|
432
|
+
const b = readField(data, at + w0 + w1, w2);
|
|
433
|
+
entries.push([
|
|
434
|
+
first + i,
|
|
435
|
+
type === 1 ? {
|
|
436
|
+
type: 1,
|
|
437
|
+
offset: a,
|
|
438
|
+
gen: b
|
|
439
|
+
} : type === 2 ? {
|
|
440
|
+
type: 2,
|
|
441
|
+
stream: a,
|
|
442
|
+
index: b
|
|
443
|
+
} : {
|
|
444
|
+
type: 0
|
|
445
|
+
}
|
|
446
|
+
]);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
return {
|
|
450
|
+
offset,
|
|
451
|
+
kind: 'stream',
|
|
452
|
+
trailer: stream.dict,
|
|
453
|
+
entries
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
// -------------------------------------------------------------------------
|
|
457
|
+
// Repair
|
|
458
|
+
/** Every `N G obj` header in the file, bounded; the last one per number wins. */ scan() {
|
|
459
|
+
if (this.scanned) return this.scanned;
|
|
460
|
+
const headers = [];
|
|
461
|
+
const latest = new Map();
|
|
462
|
+
const { bytes } = this;
|
|
463
|
+
for(let at = indexOfBytes(bytes, OBJ); at >= 0 && headers.length < MAX_SCAN_OBJECTS; at = indexOfBytes(bytes, OBJ, at + OBJ.length)){
|
|
464
|
+
var _bytes_;
|
|
465
|
+
if (continuesWord((_bytes_ = bytes[at + OBJ.length]) != null ? _bytes_ : -1)) continue;
|
|
466
|
+
const header = headerBefore(bytes, at);
|
|
467
|
+
if (!header) continue;
|
|
468
|
+
headers.push(header);
|
|
469
|
+
latest.set(header.num, header);
|
|
470
|
+
}
|
|
471
|
+
this.scanned = {
|
|
472
|
+
headers,
|
|
473
|
+
latest
|
|
474
|
+
};
|
|
475
|
+
return this.scanned;
|
|
476
|
+
}
|
|
477
|
+
/** The object whose header is the last one before `at`. */ objectAround(at) {
|
|
478
|
+
const { headers } = this.scan();
|
|
479
|
+
let lo = 0;
|
|
480
|
+
let hi = headers.length - 1;
|
|
481
|
+
let found = null;
|
|
482
|
+
while(lo <= hi){
|
|
483
|
+
const mid = lo + hi >> 1;
|
|
484
|
+
const header = headers[mid];
|
|
485
|
+
if (!header) break;
|
|
486
|
+
if (header.offset < at) {
|
|
487
|
+
found = header;
|
|
488
|
+
lo = mid + 1;
|
|
489
|
+
} else {
|
|
490
|
+
hi = mid - 1;
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
return found;
|
|
494
|
+
}
|
|
495
|
+
/**
|
|
496
|
+
* Every occurrence of `pattern` as a whole token: not continued by a
|
|
497
|
+
* regular character, and — for a keyword, which unlike a name does not
|
|
498
|
+
* open with its own delimiter — not preceded by one either.
|
|
499
|
+
*/ *occurrences(pattern) {
|
|
500
|
+
const keyword = pattern[0] !== 0x2f;
|
|
501
|
+
let count = 0;
|
|
502
|
+
for(let at = indexOfBytes(this.bytes, pattern); at >= 0 && count < MAX_SCAN_OBJECTS; at = indexOfBytes(this.bytes, pattern, at + pattern.length)){
|
|
503
|
+
var _this_bytes_, _this_bytes_1;
|
|
504
|
+
count++;
|
|
505
|
+
if (continuesWord((_this_bytes_ = this.bytes[at + pattern.length]) != null ? _this_bytes_ : -1)) continue;
|
|
506
|
+
if (keyword && continuesWord((_this_bytes_1 = this.bytes[at - 1]) != null ? _this_bytes_1 : -1)) continue;
|
|
507
|
+
yield at;
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
/**
|
|
511
|
+
* Rebuilds the object table from a scan when the cross-reference data
|
|
512
|
+
* does not hold up, as Poppler and pdf.js do: the last definition of each
|
|
513
|
+
* object in file order, including those in object streams, and the last
|
|
514
|
+
* trailer (classic or cross-reference stream) whose `/Root` is a
|
|
515
|
+
* dictionary — or, failing that, the last `/Type /Catalog` object.
|
|
516
|
+
*/ rebuild() {
|
|
517
|
+
this.repaired = true;
|
|
518
|
+
this.entries.clear();
|
|
519
|
+
this.cache.clear();
|
|
520
|
+
this.objectStreams.clear();
|
|
521
|
+
this.sections.length = 0;
|
|
522
|
+
this.trailer = null;
|
|
523
|
+
const { headers } = this.scan();
|
|
524
|
+
for (const h of headers)this.entries.set(h.num, {
|
|
525
|
+
type: 1,
|
|
526
|
+
offset: h.offset,
|
|
527
|
+
gen: h.gen
|
|
528
|
+
});
|
|
529
|
+
// Object streams add their objects at the stream's own position, so
|
|
530
|
+
// the last definition in file order still wins.
|
|
531
|
+
const compressed = [];
|
|
532
|
+
for (const at of this.occurrences(OBJSTM_TYPE)){
|
|
533
|
+
const header = this.objectAround(at);
|
|
534
|
+
if (!header) continue;
|
|
535
|
+
const objstm = this.objectStream(header.num);
|
|
536
|
+
objstm == null ? void 0 : objstm.numbers.forEach((num, index)=>compressed.push({
|
|
537
|
+
at: header.offset,
|
|
538
|
+
num,
|
|
539
|
+
entry: {
|
|
540
|
+
type: 2,
|
|
541
|
+
stream: header.num,
|
|
542
|
+
index
|
|
543
|
+
}
|
|
544
|
+
}));
|
|
545
|
+
}
|
|
546
|
+
if (compressed.length) {
|
|
547
|
+
compressed.sort((a, b)=>a.at - b.at);
|
|
548
|
+
this.entries.clear();
|
|
549
|
+
this.cache.clear();
|
|
550
|
+
let next = 0;
|
|
551
|
+
for (const h of headers){
|
|
552
|
+
var _ref;
|
|
553
|
+
var _compressed_next;
|
|
554
|
+
for(; next < compressed.length && ((_ref = (_compressed_next = compressed[next]) == null ? void 0 : _compressed_next.at) != null ? _ref : Infinity) < h.offset; next++){
|
|
555
|
+
const def = compressed[next];
|
|
556
|
+
if (def) this.entries.set(def.num, def.entry);
|
|
557
|
+
}
|
|
558
|
+
this.entries.set(h.num, {
|
|
559
|
+
type: 1,
|
|
560
|
+
offset: h.offset,
|
|
561
|
+
gen: h.gen
|
|
562
|
+
});
|
|
563
|
+
}
|
|
564
|
+
for(; next < compressed.length; next++){
|
|
565
|
+
const def = compressed[next];
|
|
566
|
+
if (def) this.entries.set(def.num, def.entry);
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
const candidates = [];
|
|
570
|
+
for (const at of this.occurrences(TRAILER)){
|
|
571
|
+
const dict = new PdfParser(this.bytes, at + TRAILER.length).parseObject();
|
|
572
|
+
if ((dict == null ? void 0 : dict.kind) === 'dict') candidates.push({
|
|
573
|
+
at,
|
|
574
|
+
dict
|
|
575
|
+
});
|
|
576
|
+
}
|
|
577
|
+
for (const at of this.occurrences(XREF_TYPE)){
|
|
578
|
+
const header = this.objectAround(at);
|
|
579
|
+
const obj = header && this.getObject(header.num);
|
|
580
|
+
if ((obj == null ? void 0 : obj.kind) === 'stream' && nameIs(obj.dict.entries.get('Type'), 'XRef')) {
|
|
581
|
+
candidates.push({
|
|
582
|
+
at,
|
|
583
|
+
dict: obj.dict
|
|
584
|
+
});
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
candidates.sort((a, b)=>a.at - b.at);
|
|
588
|
+
for (const candidate of candidates)this.trailers.push(candidate.dict);
|
|
589
|
+
for(let i = candidates.length - 1; i >= 0 && !this.trailer; i--){
|
|
590
|
+
var _candidates_i, _this_resolve;
|
|
591
|
+
const dict = (_candidates_i = candidates[i]) == null ? void 0 : _candidates_i.dict;
|
|
592
|
+
const root = dict == null ? void 0 : dict.entries.get('Root');
|
|
593
|
+
if (dict && (root == null ? void 0 : root.kind) === 'ref' && ((_this_resolve = this.resolve(root)) == null ? void 0 : _this_resolve.kind) === 'dict') {
|
|
594
|
+
this.trailer = dict;
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
if (this.trailer) return;
|
|
598
|
+
let catalog = null;
|
|
599
|
+
for (const at of this.occurrences(CATALOG_TYPE)){
|
|
600
|
+
const header = this.objectAround(at);
|
|
601
|
+
const obj = header && this.getObject(header.num);
|
|
602
|
+
if (header && (obj == null ? void 0 : obj.kind) === 'dict' && nameIs(obj.entries.get('Type'), 'Catalog')) {
|
|
603
|
+
catalog = {
|
|
604
|
+
kind: 'ref',
|
|
605
|
+
num: header.num,
|
|
606
|
+
gen: header.gen
|
|
607
|
+
};
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
let checked = 0;
|
|
611
|
+
for (const [num, entry] of this.entries){
|
|
612
|
+
if (entry.type !== 2 || ++checked > 100000) continue;
|
|
613
|
+
const obj = this.getObject(num);
|
|
614
|
+
if ((obj == null ? void 0 : obj.kind) === 'dict' && nameIs(obj.entries.get('Type'), 'Catalog')) {
|
|
615
|
+
catalog = {
|
|
616
|
+
kind: 'ref',
|
|
617
|
+
num,
|
|
618
|
+
gen: 0
|
|
619
|
+
};
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
if (catalog) {
|
|
623
|
+
var _ref1;
|
|
624
|
+
var _candidates_at;
|
|
625
|
+
const last = (_candidates_at = candidates.at(-1)) == null ? void 0 : _candidates_at.dict;
|
|
626
|
+
const entries = new Map((_ref1 = last == null ? void 0 : last.entries) != null ? _ref1 : []);
|
|
627
|
+
entries.set('Root', catalog);
|
|
628
|
+
this.trailer = {
|
|
629
|
+
kind: 'dict',
|
|
630
|
+
entries
|
|
631
|
+
};
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
constructor(bytes, headerOffset, headerVersion){
|
|
635
|
+
this.bytes = bytes;
|
|
636
|
+
this.headerOffset = headerOffset;
|
|
637
|
+
this.headerVersion = headerVersion;
|
|
638
|
+
this.entries = new Map();
|
|
639
|
+
/** The sections on the `/Prev` chain, newest first. */ this.sections = [];
|
|
640
|
+
/** Every trailer dictionary seen; `/Encrypt` in any of them counts. */ this.trailers = [];
|
|
641
|
+
/** The newest trailer — the one whose `/Root` and `/Info` are current. */ this.trailer = null;
|
|
642
|
+
/** The offset the final `startxref` gives. */ this.startxref = -1;
|
|
643
|
+
/**
|
|
644
|
+
* Set when anything had to be found some way other than through a sound
|
|
645
|
+
* cross-reference chain. A repaired file is read, never written.
|
|
646
|
+
*/ this.repaired = false;
|
|
647
|
+
this.cache = new Map();
|
|
648
|
+
this.loading = new Set();
|
|
649
|
+
this.objectStreams = new Map();
|
|
650
|
+
this.scanned = null;
|
|
651
|
+
/** Follows references to a direct object; a dangling one is `null` (§7.3.10). */ this.resolve = (obj)=>{
|
|
652
|
+
let value = obj;
|
|
653
|
+
for(let i = 0; (value == null ? void 0 : value.kind) === 'ref' && i < MAX_REF_CHAIN; i++){
|
|
654
|
+
var _this_getObject;
|
|
655
|
+
value = (_this_getObject = this.getObject(value.num)) != null ? _this_getObject : PDF_NULL;
|
|
656
|
+
}
|
|
657
|
+
return (value == null ? void 0 : value.kind) === 'ref' ? PDF_NULL : value;
|
|
658
|
+
};
|
|
659
|
+
}
|
|
660
|
+
};
|
|
661
|
+
/** Opens the file: its cross-reference chain, or a rebuild from a scan. */ function openPdf(bytes) {
|
|
662
|
+
const header = findHeader(bytes);
|
|
663
|
+
if (!header) return null;
|
|
664
|
+
const file = new PdfFile(bytes, header.offset, header.version);
|
|
665
|
+
if (!file.readXref() || !file.root()) file.rebuild();
|
|
666
|
+
return file;
|
|
667
|
+
}
|
|
668
|
+
// ---------------------------------------------------------------------------
|
|
669
|
+
// Dates (§7.9.4)
|
|
670
|
+
/**
|
|
671
|
+
* `D:YYYYMMDDHHmmSSOHH'mm'`, every field after the year optional, the
|
|
672
|
+
* apostrophes forgiven (writers disagree on the trailing one). `Z` may be
|
|
673
|
+
* followed by a redundant `00'00'`.
|
|
674
|
+
*/ const PDF_DATE = /^(?:D:)?(\d{4})(\d{2})?(\d{2})?(\d{2})?(\d{2})?(\d{2})?(?:([Zz])(?:00'?(?:00'?)?)?|([+-])(\d{2})'?(?:(\d{2})'?)?)?$/;
|
|
675
|
+
/**
|
|
676
|
+
* A PDF date as ISO 8601, or `null` when it is not one. Missing month and
|
|
677
|
+
* day default to 01 (§7.9.4); a time is shown only when the date has one,
|
|
678
|
+
* and a zone only when both were recorded — never invented.
|
|
679
|
+
*/ export function pdfDateToIso(text) {
|
|
680
|
+
const trimmed = text.trim();
|
|
681
|
+
const match = PDF_DATE.exec(trimmed);
|
|
682
|
+
if (!match) return null;
|
|
683
|
+
const [, year, month = '01', day = '01', hour, minute = '00', second = '00', utc, sign, zh, zm = '00'] = match;
|
|
684
|
+
const inRange = (value, min, max)=>value === undefined || Number(value) >= min && Number(value) <= max;
|
|
685
|
+
if (!inRange(month, 1, 12) || !inRange(day, 1, 31) || !inRange(hour, 0, 23) || !inRange(minute, 0, 59) || !inRange(second, 0, 59) || !inRange(zh, 0, 23) || !inRange(zm, 0, 59)) {
|
|
686
|
+
return null;
|
|
687
|
+
}
|
|
688
|
+
let iso = `${year}-${month}-${day}`;
|
|
689
|
+
if (hour === undefined) return iso;
|
|
690
|
+
iso += `T${hour}:${minute}:${second}`;
|
|
691
|
+
if (sign && zh !== undefined) return `${iso}${sign}${zh}:${zm}`;
|
|
692
|
+
return utc ? `${iso}Z` : iso;
|
|
693
|
+
}
|
|
694
|
+
const ISO_DATE = /^(\d{4})-(\d{2})-(\d{2})(?:T(\d{2}):(\d{2})(?::(\d{2})(?:\.\d+)?)?(Z|[+-]\d{2}:\d{2})?)?$/;
|
|
695
|
+
/** ISO 8601 as a PDF date (§7.9.4), or `null` when it is not ISO. */ export function isoToPdfDate(value) {
|
|
696
|
+
const match = ISO_DATE.exec(value.trim());
|
|
697
|
+
if (!match) return null;
|
|
698
|
+
const [, year, month, day, hour, minute, second = '00', zone] = match;
|
|
699
|
+
let date = `D:${year}${month}${day}`;
|
|
700
|
+
if (hour === undefined) return date;
|
|
701
|
+
date += `${hour}${minute}${second}`;
|
|
702
|
+
if (zone === 'Z') return `${date}Z`;
|
|
703
|
+
if (zone) return `${date}${zone.slice(0, 3)}'${zone.slice(4)}'`;
|
|
704
|
+
return date;
|
|
705
|
+
}
|
|
706
|
+
// ---------------------------------------------------------------------------
|
|
707
|
+
// Reading
|
|
708
|
+
function readInfo(file, info) {
|
|
709
|
+
const out = [];
|
|
710
|
+
if (!info) return out;
|
|
711
|
+
for (const [key, raw] of info.entries){
|
|
712
|
+
const value = file.resolve(raw);
|
|
713
|
+
const name = pdfNameText(key);
|
|
714
|
+
if (!value || value.kind === 'null' || value.kind === 'stream') continue;
|
|
715
|
+
if (value.kind === 'string') {
|
|
716
|
+
const text = decodePdfTextString(value.bytes);
|
|
717
|
+
const date = DATE_KEYS.has(key) ? pdfDateToIso(text) : null;
|
|
718
|
+
out.push({
|
|
719
|
+
name,
|
|
720
|
+
value: date != null ? date : text,
|
|
721
|
+
editable: true
|
|
722
|
+
});
|
|
723
|
+
} else if (value.kind === 'name') {
|
|
724
|
+
out.push({
|
|
725
|
+
name,
|
|
726
|
+
value: pdfNameText(value.name),
|
|
727
|
+
editable: false
|
|
728
|
+
});
|
|
729
|
+
} else {
|
|
730
|
+
const text = serializePdfObject(value);
|
|
731
|
+
out.push({
|
|
732
|
+
name,
|
|
733
|
+
value: text.length > MAX_VALUE_TEXT ? `${text.slice(0, MAX_VALUE_TEXT)}…` : text,
|
|
734
|
+
editable: false
|
|
735
|
+
});
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
return out;
|
|
739
|
+
}
|
|
740
|
+
/** XMP packets are UTF-8, -16 or -32 (XMP part 1 §7.1); PDF writers use UTF-8. */ function xmpText(data) {
|
|
741
|
+
if (data[0] === 0xfe && data[1] === 0xff) return new TextDecoder('utf-16be').decode(data);
|
|
742
|
+
if (data[0] === 0xff && data[1] === 0xfe) return new TextDecoder('utf-16le').decode(data);
|
|
743
|
+
return new TextDecoder('utf-8').decode(data);
|
|
744
|
+
}
|
|
745
|
+
function readXmp(file, catalog) {
|
|
746
|
+
const metadata = file.resolve(catalog == null ? void 0 : catalog.entries.get('Metadata'));
|
|
747
|
+
if ((metadata == null ? void 0 : metadata.kind) !== 'stream') return null;
|
|
748
|
+
const data = decodePdfStream(metadata, file.resolve, MAX_XMP_BYTES);
|
|
749
|
+
return data ? xmpText(data) : null;
|
|
750
|
+
}
|
|
751
|
+
function readPageCount(file, catalog) {
|
|
752
|
+
const pages = asDict(file.resolve(catalog == null ? void 0 : catalog.entries.get('Pages')));
|
|
753
|
+
return intValue(file.resolve(pages == null ? void 0 : pages.entries.get('Count')));
|
|
754
|
+
}
|
|
755
|
+
function readVersion(file, catalog) {
|
|
756
|
+
const header = file.headerVersion;
|
|
757
|
+
const version = file.resolve(catalog == null ? void 0 : catalog.entries.get('Version'));
|
|
758
|
+
const match = (version == null ? void 0 : version.kind) === 'name' ? /^(\d{1,2})\.(\d{1,2})$/.exec(version.name) : null;
|
|
759
|
+
if (!match) return header;
|
|
760
|
+
const catalogVersion = `${Number(match[1])}.${Number(match[2])}`;
|
|
761
|
+
if (!header) return catalogVersion;
|
|
762
|
+
const [hMajor = 0, hMinor = 0] = header.split('.').map(Number);
|
|
763
|
+
const later = Number(match[1]) * 100 + Number(match[2]) > hMajor * 100 + hMinor;
|
|
764
|
+
return later ? catalogVersion : header;
|
|
765
|
+
}
|
|
766
|
+
/**
|
|
767
|
+
* Whether the file is, or may be, signed. A signature dictionary cannot
|
|
768
|
+
* live in an object stream — its `/ByteRange` must exclude `/Contents` in
|
|
769
|
+
* the file's own bytes — so a raw scan for `/ByteRange` finds every real
|
|
770
|
+
* signature and some false alarms, which is the side to err on. The
|
|
771
|
+
* catalog checks catch signed-looking structure the scan cannot.
|
|
772
|
+
*/ function readSigned(file, catalog) {
|
|
773
|
+
var _intValue;
|
|
774
|
+
if (indexOfBytes(file.bytes, BYTE_RANGE) >= 0) return true;
|
|
775
|
+
if (!catalog) return false;
|
|
776
|
+
// §12.8.4: /Perms holds DocMDP and usage-rights (UR3) signatures.
|
|
777
|
+
if (catalog.entries.has('Perms')) return true;
|
|
778
|
+
const acroForm = asDict(file.resolve(catalog.entries.get('AcroForm')));
|
|
779
|
+
if (!acroForm) return false;
|
|
780
|
+
// §12.7.3 Table 225: bit 1 SignaturesExist, bit 2 AppendOnly.
|
|
781
|
+
const flags = (_intValue = intValue(file.resolve(acroForm.entries.get('SigFlags')))) != null ? _intValue : 0;
|
|
782
|
+
if (flags & 3) return true;
|
|
783
|
+
const fields = file.resolve(acroForm.entries.get('Fields'));
|
|
784
|
+
if ((fields == null ? void 0 : fields.kind) !== 'array') return false;
|
|
785
|
+
const stack = fields.items.map((node)=>({
|
|
786
|
+
node,
|
|
787
|
+
sig: false
|
|
788
|
+
}));
|
|
789
|
+
const seen = new Set();
|
|
790
|
+
for(let visited = 0; stack.length; visited++){
|
|
791
|
+
// Unsure after this many nodes: say signed.
|
|
792
|
+
if (visited > MAX_FIELD_NODES) return true;
|
|
793
|
+
const next = stack.pop();
|
|
794
|
+
if (!next) break;
|
|
795
|
+
if (next.node.kind === 'ref') {
|
|
796
|
+
if (seen.has(next.node.num)) continue;
|
|
797
|
+
seen.add(next.node.num);
|
|
798
|
+
}
|
|
799
|
+
const field = asDict(file.resolve(next.node));
|
|
800
|
+
if (!field) continue;
|
|
801
|
+
// /FT is inheritable (§12.7.4.1), so a kid inherits its parent's type.
|
|
802
|
+
const ft = file.resolve(field.entries.get('FT'));
|
|
803
|
+
const sig = ft ? nameIs(ft, 'Sig') : next.sig;
|
|
804
|
+
// A value that is there but does not resolve still counts: unsure.
|
|
805
|
+
const value = field.entries.get('V');
|
|
806
|
+
if (sig && value && value.kind !== 'null') return true;
|
|
807
|
+
const kids = file.resolve(field.entries.get('Kids'));
|
|
808
|
+
if ((kids == null ? void 0 : kids.kind) === 'array') {
|
|
809
|
+
for (const kid of kids.items)stack.push({
|
|
810
|
+
node: kid,
|
|
811
|
+
sig
|
|
812
|
+
});
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
return false;
|
|
816
|
+
}
|
|
817
|
+
/**
|
|
818
|
+
* The metadata a PDF carries, or `null` when the bytes are not a PDF or
|
|
819
|
+
* nothing in them could be found — neither a catalog nor an Info
|
|
820
|
+
* dictionary. Never throws.
|
|
821
|
+
*/ export function readPdf(bytes) {
|
|
822
|
+
const file = attempt(()=>openPdf(bytes), null);
|
|
823
|
+
if (!file) return null;
|
|
824
|
+
const catalog = attempt(()=>{
|
|
825
|
+
var _ref;
|
|
826
|
+
var _file_root;
|
|
827
|
+
return (_ref = (_file_root = file.root()) == null ? void 0 : _file_root.catalog) != null ? _ref : null;
|
|
828
|
+
}, null);
|
|
829
|
+
const info = attempt(()=>file.info(), null);
|
|
830
|
+
if (!catalog && !info) return null;
|
|
831
|
+
const encrypted = attempt(()=>file.encrypted(), true);
|
|
832
|
+
return {
|
|
833
|
+
info: encrypted ? [] : attempt(()=>readInfo(file, info), []),
|
|
834
|
+
xmp: encrypted ? null : attempt(()=>readXmp(file, catalog), null),
|
|
835
|
+
pageCount: attempt(()=>readPageCount(file, catalog), null),
|
|
836
|
+
version: attempt(()=>readVersion(file, catalog), file.headerVersion),
|
|
837
|
+
encrypted,
|
|
838
|
+
signed: attempt(()=>readSigned(file, catalog), true)
|
|
839
|
+
};
|
|
840
|
+
}
|
|
841
|
+
// ---------------------------------------------------------------------------
|
|
842
|
+
// Writing
|
|
843
|
+
const DAMAGED = "This PDF's internal index is damaged, so an edit cannot be written into it safely.";
|
|
844
|
+
/** An Info key as name bytes, matching an existing entry's spelling when there is one. */ function infoKey(entries, key) {
|
|
845
|
+
for (const existing of entries.keys()){
|
|
846
|
+
if (pdfNameText(existing) === key) return existing;
|
|
847
|
+
}
|
|
848
|
+
const name = pdfNameFromText(key);
|
|
849
|
+
if (!key || key.includes('\u0000') || name.length > 127) {
|
|
850
|
+
// Annex C.2: 127 bytes is the longest name a reader must accept.
|
|
851
|
+
throw new EmbeddedWriteError(`"${key.slice(0, 40)}" cannot be used as a PDF field name.`);
|
|
852
|
+
}
|
|
853
|
+
return name;
|
|
854
|
+
}
|
|
855
|
+
function infoValue(key, value) {
|
|
856
|
+
if (DATE_KEYS.has(key)) {
|
|
857
|
+
const date = isoToPdfDate(value);
|
|
858
|
+
if (date) return {
|
|
859
|
+
kind: 'string',
|
|
860
|
+
bytes: latin1Bytes(date),
|
|
861
|
+
hex: false
|
|
862
|
+
};
|
|
863
|
+
}
|
|
864
|
+
// §14.3.3 Table 349: /Trapped is a name.
|
|
865
|
+
if (key === 'Trapped' && /^(?:True|False|Unknown)$/.test(value)) {
|
|
866
|
+
return {
|
|
867
|
+
kind: 'name',
|
|
868
|
+
name: value
|
|
869
|
+
};
|
|
870
|
+
}
|
|
871
|
+
return encodePdfTextString(value);
|
|
872
|
+
}
|
|
873
|
+
function concat(chunks) {
|
|
874
|
+
let length = 0;
|
|
875
|
+
for (const chunk of chunks)length += chunk.length;
|
|
876
|
+
const out = new Uint8Array(length);
|
|
877
|
+
let at = 0;
|
|
878
|
+
for (const chunk of chunks){
|
|
879
|
+
out.set(chunk, at);
|
|
880
|
+
at += chunk.length;
|
|
881
|
+
}
|
|
882
|
+
return out;
|
|
883
|
+
}
|
|
884
|
+
function dictObject(ref, dict) {
|
|
885
|
+
return {
|
|
886
|
+
ref,
|
|
887
|
+
expect: dict,
|
|
888
|
+
body: latin1Bytes(`${ref.num} ${ref.gen} obj\n${serializePdfObject(dict)}\nendobj\n`)
|
|
889
|
+
};
|
|
890
|
+
}
|
|
891
|
+
/** §14.3.2: an uncompressed metadata stream, so any tool can find the packet. */ function metadataObject(ref, data) {
|
|
892
|
+
const dict = {
|
|
893
|
+
kind: 'dict',
|
|
894
|
+
entries: new Map([
|
|
895
|
+
[
|
|
896
|
+
'Type',
|
|
897
|
+
{
|
|
898
|
+
kind: 'name',
|
|
899
|
+
name: 'Metadata'
|
|
900
|
+
}
|
|
901
|
+
],
|
|
902
|
+
[
|
|
903
|
+
'Subtype',
|
|
904
|
+
{
|
|
905
|
+
kind: 'name',
|
|
906
|
+
name: 'XML'
|
|
907
|
+
}
|
|
908
|
+
],
|
|
909
|
+
[
|
|
910
|
+
'Length',
|
|
911
|
+
{
|
|
912
|
+
kind: 'number',
|
|
913
|
+
value: data.length,
|
|
914
|
+
text: String(data.length)
|
|
915
|
+
}
|
|
916
|
+
]
|
|
917
|
+
])
|
|
918
|
+
};
|
|
919
|
+
return {
|
|
920
|
+
ref,
|
|
921
|
+
expect: {
|
|
922
|
+
kind: 'stream',
|
|
923
|
+
dict,
|
|
924
|
+
data
|
|
925
|
+
},
|
|
926
|
+
body: concat([
|
|
927
|
+
latin1Bytes(`${ref.num} ${ref.gen} obj\n${serializePdfObject(dict)}\nstream\n`),
|
|
928
|
+
data,
|
|
929
|
+
latin1Bytes('\nendstream\nendobj\n')
|
|
930
|
+
])
|
|
931
|
+
};
|
|
932
|
+
}
|
|
933
|
+
/** Consecutive object numbers grouped into `[start, count]` subsections. */ function subsections(items) {
|
|
934
|
+
const runs = [];
|
|
935
|
+
for (const item of [
|
|
936
|
+
...items
|
|
937
|
+
].sort((a, b)=>a.num - b.num)){
|
|
938
|
+
const run = runs.at(-1);
|
|
939
|
+
if (run && run.start + run.items.length === item.num) run.items.push(item);
|
|
940
|
+
else runs.push({
|
|
941
|
+
start: item.num,
|
|
942
|
+
items: [
|
|
943
|
+
item
|
|
944
|
+
]
|
|
945
|
+
});
|
|
946
|
+
}
|
|
947
|
+
return runs;
|
|
948
|
+
}
|
|
949
|
+
function sameObject(actual, expected) {
|
|
950
|
+
if (!actual) return false;
|
|
951
|
+
if (expected.kind === 'stream') {
|
|
952
|
+
if (actual.kind !== 'stream') return false;
|
|
953
|
+
const { data } = expected;
|
|
954
|
+
return actual.data.length === data.length && actual.data.every((b, i)=>b === data[i]) && serializePdfObject(actual.dict) === serializePdfObject(expected.dict);
|
|
955
|
+
}
|
|
956
|
+
return serializePdfObject(actual) === serializePdfObject(expected);
|
|
957
|
+
}
|
|
958
|
+
/**
|
|
959
|
+
* Writes an edit as an incremental update (§7.5.6) appended to `bytes`.
|
|
960
|
+
* Returns the new file; `bytes` is a prefix of it. Throws
|
|
961
|
+
* {@link EmbeddedWriteError} for a file that is not a PDF, is encrypted or
|
|
962
|
+
* signed, or whose cross-reference data had to be repaired to be read —
|
|
963
|
+
* and, as a last line, when the output does not read back as written.
|
|
964
|
+
*/ export function writePdf(bytes, update) {
|
|
965
|
+
var _update_info;
|
|
966
|
+
var _file_sections_;
|
|
967
|
+
const file = attempt(()=>openPdf(bytes), null);
|
|
968
|
+
if (!file) throw new EmbeddedWriteError('This file could not be read as a PDF, so it was left as it was.');
|
|
969
|
+
if (file.encrypted()) {
|
|
970
|
+
throw new EmbeddedWriteError('This PDF is encrypted, so its details cannot be changed.');
|
|
971
|
+
}
|
|
972
|
+
const root = attempt(()=>file.root(), null);
|
|
973
|
+
if (attempt(()=>{
|
|
974
|
+
var _ref;
|
|
975
|
+
return readSigned(file, (_ref = root == null ? void 0 : root.catalog) != null ? _ref : null);
|
|
976
|
+
}, true)) {
|
|
977
|
+
throw new EmbeddedWriteError('This PDF is digitally signed; changing it would invalidate the signature.');
|
|
978
|
+
}
|
|
979
|
+
const patch = Object.entries((_update_info = update.info) != null ? _update_info : {});
|
|
980
|
+
if (!patch.length && update.xmp === undefined) return bytes.slice();
|
|
981
|
+
const trailer = file.trailer;
|
|
982
|
+
if (!root || !trailer) throw new EmbeddedWriteError(DAMAGED);
|
|
983
|
+
let next = file.nextObjectNumber();
|
|
984
|
+
const objects = [];
|
|
985
|
+
// The Info dictionary: every entry it has, the patch applied in place.
|
|
986
|
+
let infoRef = trailer.entries.get('Info');
|
|
987
|
+
if (patch.length) {
|
|
988
|
+
var _ref;
|
|
989
|
+
const current = asDict(file.resolve(infoRef));
|
|
990
|
+
const entries = new Map((_ref = current == null ? void 0 : current.entries) != null ? _ref : []);
|
|
991
|
+
for (const [key, value] of patch){
|
|
992
|
+
if (value === undefined) continue;
|
|
993
|
+
const name = infoKey(entries, key);
|
|
994
|
+
if (value === null) entries.delete(name);
|
|
995
|
+
else entries.set(name, infoValue(name, value));
|
|
996
|
+
}
|
|
997
|
+
const ref = (infoRef == null ? void 0 : infoRef.kind) === 'ref' ? infoRef : {
|
|
998
|
+
kind: 'ref',
|
|
999
|
+
num: next++,
|
|
1000
|
+
gen: 0
|
|
1001
|
+
};
|
|
1002
|
+
infoRef = ref;
|
|
1003
|
+
objects.push(dictObject(ref, {
|
|
1004
|
+
kind: 'dict',
|
|
1005
|
+
entries
|
|
1006
|
+
}));
|
|
1007
|
+
}
|
|
1008
|
+
// The XMP stream: redefined in place, or added and linked from a
|
|
1009
|
+
// redefinition of the catalog that keeps every other entry it had.
|
|
1010
|
+
if (update.xmp !== undefined) {
|
|
1011
|
+
const data = new TextEncoder().encode(update.xmp);
|
|
1012
|
+
const existing = root.catalog.entries.get('Metadata');
|
|
1013
|
+
const ref = (existing == null ? void 0 : existing.kind) === 'ref' ? existing : {
|
|
1014
|
+
kind: 'ref',
|
|
1015
|
+
num: next++,
|
|
1016
|
+
gen: 0
|
|
1017
|
+
};
|
|
1018
|
+
objects.push(metadataObject(ref, data));
|
|
1019
|
+
if ((existing == null ? void 0 : existing.kind) !== 'ref') {
|
|
1020
|
+
const entries = new Map(root.catalog.entries);
|
|
1021
|
+
entries.set('Metadata', ref);
|
|
1022
|
+
objects.push(dictObject(root.ref, {
|
|
1023
|
+
kind: 'dict',
|
|
1024
|
+
entries
|
|
1025
|
+
}));
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
// Everything above resolved through the table; only now is it known
|
|
1029
|
+
// whether any of it had to be found another way.
|
|
1030
|
+
if (file.repaired || file.headerOffset !== 0 || file.startxref <= 0) {
|
|
1031
|
+
throw new EmbeddedWriteError(DAMAGED);
|
|
1032
|
+
}
|
|
1033
|
+
const nums = new Set(objects.map((o)=>o.ref.num));
|
|
1034
|
+
if (nums.size !== objects.length || objects.some((o)=>o.ref.num < 1 || o.ref.gen > 65535) || next > MAX_OBJECT_NUMBER) {
|
|
1035
|
+
throw new EmbeddedWriteError(DAMAGED);
|
|
1036
|
+
}
|
|
1037
|
+
// §7.5.6: the update starts on a line of its own.
|
|
1038
|
+
const last = bytes[bytes.length - 1];
|
|
1039
|
+
const chunks = [
|
|
1040
|
+
bytes
|
|
1041
|
+
];
|
|
1042
|
+
if (last !== 0x0a && last !== 0x0d) chunks.push(latin1Bytes('\n'));
|
|
1043
|
+
let offset = bytes.length + (chunks.length - 1);
|
|
1044
|
+
const placed = [];
|
|
1045
|
+
for (const object of objects){
|
|
1046
|
+
placed.push({
|
|
1047
|
+
num: object.ref.num,
|
|
1048
|
+
gen: object.ref.gen,
|
|
1049
|
+
offset
|
|
1050
|
+
});
|
|
1051
|
+
chunks.push(object.body);
|
|
1052
|
+
offset += object.body.length;
|
|
1053
|
+
}
|
|
1054
|
+
const xrefOffset = offset;
|
|
1055
|
+
if (xrefOffset > 0xffffffff) {
|
|
1056
|
+
throw new EmbeddedWriteError('This PDF is too large to take an edit.');
|
|
1057
|
+
}
|
|
1058
|
+
const trailerEntries = new Map();
|
|
1059
|
+
const setSize = (size)=>trailerEntries.set('Size', {
|
|
1060
|
+
kind: 'number',
|
|
1061
|
+
value: size,
|
|
1062
|
+
text: String(size)
|
|
1063
|
+
});
|
|
1064
|
+
const size = Math.max(next, ...placed.map((p)=>p.num + 1));
|
|
1065
|
+
setSize(size);
|
|
1066
|
+
trailerEntries.set('Root', root.ref);
|
|
1067
|
+
if (infoRef) trailerEntries.set('Info', infoRef);
|
|
1068
|
+
trailerEntries.set('Prev', {
|
|
1069
|
+
kind: 'number',
|
|
1070
|
+
value: file.startxref,
|
|
1071
|
+
text: String(file.startxref)
|
|
1072
|
+
});
|
|
1073
|
+
const id = file.sections.map((s)=>s.trailer.entries.get('ID')).find(Boolean);
|
|
1074
|
+
if (id) trailerEntries.set('ID', id);
|
|
1075
|
+
if (((_file_sections_ = file.sections[0]) == null ? void 0 : _file_sections_.kind) === 'stream') {
|
|
1076
|
+
// §7.5.8: a cross-reference stream, uncompressed, listing itself.
|
|
1077
|
+
const xrefNum = next;
|
|
1078
|
+
if (xrefNum > MAX_OBJECT_NUMBER) throw new EmbeddedWriteError(DAMAGED);
|
|
1079
|
+
placed.push({
|
|
1080
|
+
num: xrefNum,
|
|
1081
|
+
gen: 0,
|
|
1082
|
+
offset: xrefOffset
|
|
1083
|
+
});
|
|
1084
|
+
const runs = subsections(placed);
|
|
1085
|
+
const data = new Uint8Array(placed.length * 7);
|
|
1086
|
+
let at = 0;
|
|
1087
|
+
for (const run of runs){
|
|
1088
|
+
for (const entry of run.items){
|
|
1089
|
+
data[at] = 1;
|
|
1090
|
+
data[at + 1] = entry.offset >>> 24 & 0xff;
|
|
1091
|
+
data[at + 2] = entry.offset >>> 16 & 0xff;
|
|
1092
|
+
data[at + 3] = entry.offset >>> 8 & 0xff;
|
|
1093
|
+
data[at + 4] = entry.offset & 0xff;
|
|
1094
|
+
data[at + 5] = entry.gen >>> 8 & 0xff;
|
|
1095
|
+
data[at + 6] = entry.gen & 0xff;
|
|
1096
|
+
at += 7;
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
const int = (value)=>({
|
|
1100
|
+
kind: 'number',
|
|
1101
|
+
value,
|
|
1102
|
+
text: String(value)
|
|
1103
|
+
});
|
|
1104
|
+
const dict = new Map([
|
|
1105
|
+
[
|
|
1106
|
+
'Type',
|
|
1107
|
+
{
|
|
1108
|
+
kind: 'name',
|
|
1109
|
+
name: 'XRef'
|
|
1110
|
+
}
|
|
1111
|
+
]
|
|
1112
|
+
]);
|
|
1113
|
+
setSize(Math.max(size, xrefNum + 1));
|
|
1114
|
+
for (const [key, value] of trailerEntries)dict.set(key, value);
|
|
1115
|
+
dict.set('W', {
|
|
1116
|
+
kind: 'array',
|
|
1117
|
+
items: [
|
|
1118
|
+
int(1),
|
|
1119
|
+
int(4),
|
|
1120
|
+
int(2)
|
|
1121
|
+
]
|
|
1122
|
+
});
|
|
1123
|
+
dict.set('Index', {
|
|
1124
|
+
kind: 'array',
|
|
1125
|
+
items: runs.flatMap((run)=>[
|
|
1126
|
+
int(run.start),
|
|
1127
|
+
int(run.items.length)
|
|
1128
|
+
])
|
|
1129
|
+
});
|
|
1130
|
+
dict.set('Length', int(data.length));
|
|
1131
|
+
chunks.push(latin1Bytes(`${xrefNum} 0 obj\n${serializePdfObject({
|
|
1132
|
+
kind: 'dict',
|
|
1133
|
+
entries: dict
|
|
1134
|
+
})}\nstream\n`), data, latin1Bytes(`\nendstream\nendobj\nstartxref\n${xrefOffset}\n%%EOF\n`));
|
|
1135
|
+
} else {
|
|
1136
|
+
// §7.5.4: each entry exactly 20 bytes, `nnnnnnnnnn ggggg n` and a
|
|
1137
|
+
// two-byte end of line.
|
|
1138
|
+
let table = 'xref\n';
|
|
1139
|
+
for (const run of subsections(placed)){
|
|
1140
|
+
table += `${run.start} ${run.items.length}\n`;
|
|
1141
|
+
for (const entry of run.items){
|
|
1142
|
+
table += `${String(entry.offset).padStart(10, '0')} ${String(entry.gen).padStart(5, '0')} n\r\n`;
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
table += `trailer\n${serializePdfObject({
|
|
1146
|
+
kind: 'dict',
|
|
1147
|
+
entries: trailerEntries
|
|
1148
|
+
})}\n`;
|
|
1149
|
+
table += `startxref\n${xrefOffset}\n%%EOF\n`;
|
|
1150
|
+
chunks.push(latin1Bytes(table));
|
|
1151
|
+
}
|
|
1152
|
+
const output = concat(chunks);
|
|
1153
|
+
// Read the result back the way the next reader will. Anything short of
|
|
1154
|
+
// a sound chain holding exactly what was written means the update is
|
|
1155
|
+
// not what it claims to be, and the original stays as it was.
|
|
1156
|
+
const check = attempt(()=>openPdf(output), null);
|
|
1157
|
+
const verified = check !== null && attempt(()=>{
|
|
1158
|
+
var _check_root;
|
|
1159
|
+
return ((_check_root = check.root()) == null ? void 0 : _check_root.ref.num) === root.ref.num && objects.every((o)=>sameObject(check.getObject(o.ref.num), o.expect)) && !check.repaired && check.sections.length === file.sections.length + 1;
|
|
1160
|
+
}, false);
|
|
1161
|
+
if (!verified) {
|
|
1162
|
+
throw new EmbeddedWriteError('The edited PDF did not read back as written, so the file was left as it was.');
|
|
1163
|
+
}
|
|
1164
|
+
return output;
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
//# sourceMappingURL=pdf.js.map
|