pdf-codec 1.3.1 → 1.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -0
- package/dist/afm-widths-BoeTOK2r.cjs +826 -0
- package/dist/afm-widths-Dxucrg7D.js +779 -0
- package/dist/afm-widths-DyDq56Ph.d.cts +19 -0
- package/dist/afm-widths-DyDq56Ph.d.ts +19 -0
- package/dist/afm-widths.cjs +4 -0
- package/dist/afm-widths.d.cts +2 -0
- package/dist/afm-widths.d.ts +2 -0
- package/dist/afm-widths.js +2 -0
- package/dist/assets/stix-two-math-font.cjs +5 -0
- package/dist/assets/stix-two-math-font.d.cts +4 -0
- package/dist/assets/stix-two-math-font.d.ts +4 -0
- package/dist/assets/stix-two-math-font.js +4 -0
- package/dist/bytes/crc32.cjs +21 -0
- package/dist/bytes/crc32.d.cts +4 -0
- package/dist/bytes/crc32.d.ts +4 -0
- package/dist/bytes/crc32.js +20 -0
- package/dist/bytes/flate.cjs +53 -0
- package/dist/bytes/flate.d.cts +12 -0
- package/dist/bytes/flate.d.ts +12 -0
- package/dist/bytes/flate.js +49 -0
- package/dist/bytes/reader.cjs +60 -0
- package/dist/bytes/reader.d.cts +20 -0
- package/dist/bytes/reader.d.ts +20 -0
- package/dist/bytes/reader.js +58 -0
- package/dist/bytes/writer.cjs +37 -0
- package/dist/bytes/writer.d.cts +13 -0
- package/dist/bytes/writer.d.ts +13 -0
- package/dist/bytes/writer.js +35 -0
- package/dist/cmap.cjs +110 -0
- package/dist/cmap.d.cts +8 -0
- package/dist/cmap.d.ts +8 -0
- package/dist/cmap.js +109 -0
- package/dist/codec.cjs +34 -0
- package/dist/codec.d.cts +167 -0
- package/dist/codec.d.ts +167 -0
- package/dist/codec.js +32 -0
- package/dist/content-read.cjs +126 -0
- package/dist/content-read.d.cts +21 -0
- package/dist/content-read.d.ts +21 -0
- package/dist/content-read.js +125 -0
- package/dist/content-write.cjs +138 -0
- package/dist/content-write.d.cts +24 -0
- package/dist/content-write.d.ts +24 -0
- package/dist/content-write.js +137 -0
- package/dist/diagnostics.cjs +21 -0
- package/dist/diagnostics.d.cts +19 -0
- package/dist/diagnostics.d.ts +19 -0
- package/dist/diagnostics.js +18 -0
- package/dist/document.cjs +162 -0
- package/dist/document.d.cts +12 -0
- package/dist/document.d.ts +12 -0
- package/dist/document.js +161 -0
- package/dist/encoding.cjs +5 -0
- package/dist/encoding.d.cts +6 -0
- package/dist/encoding.d.ts +6 -0
- package/dist/encoding.js +2 -0
- package/dist/filters.cjs +223 -0
- package/dist/filters.d.cts +14 -0
- package/dist/filters.d.ts +14 -0
- package/dist/filters.js +218 -0
- package/dist/font-read.cjs +199 -0
- package/dist/font-read.d.cts +23 -0
- package/dist/font-read.d.ts +23 -0
- package/dist/font-read.js +198 -0
- package/dist/font-style.cjs +35 -0
- package/dist/font-style.d.cts +14 -0
- package/dist/font-style.d.ts +14 -0
- package/dist/font-style.js +34 -0
- package/dist/fonts.cjs +124 -0
- package/dist/fonts.d.cts +15 -0
- package/dist/fonts.d.ts +15 -0
- package/dist/fonts.js +122 -0
- package/dist/formula.cjs +0 -0
- package/dist/formula.d.cts +10 -0
- package/dist/formula.d.ts +10 -0
- package/dist/formula.js +0 -0
- package/dist/image/jpeg-info.cjs +71 -0
- package/dist/image/jpeg-info.d.cts +12 -0
- package/dist/image/jpeg-info.d.ts +12 -0
- package/dist/image/jpeg-info.js +70 -0
- package/dist/image/png-decode.cjs +181 -0
- package/dist/image/png-decode.d.cts +14 -0
- package/dist/image/png-decode.d.ts +14 -0
- package/dist/image/png-decode.js +180 -0
- package/dist/image/png-encode.cjs +64 -0
- package/dist/image/png-encode.d.cts +8 -0
- package/dist/image/png-encode.d.ts +8 -0
- package/dist/image/png-encode.js +63 -0
- package/dist/image/png-filter.cjs +95 -0
- package/dist/image/png-filter.d.cts +6 -0
- package/dist/image/png-filter.d.ts +6 -0
- package/dist/image/png-filter.js +93 -0
- package/dist/images-read.cjs +250 -0
- package/dist/images-read.d.cts +13 -0
- package/dist/images-read.d.ts +13 -0
- package/dist/images-read.js +249 -0
- package/dist/index.cjs +45 -5912
- package/dist/index.d.cts +21 -483
- package/dist/index.d.ts +21 -483
- package/dist/index.js +18 -5885
- package/dist/interpret.cjs +587 -0
- package/dist/interpret.d.cts +84 -0
- package/dist/interpret.d.ts +84 -0
- package/dist/interpret.js +586 -0
- package/dist/lexer.cjs +180 -0
- package/dist/lexer.d.cts +29 -0
- package/dist/lexer.d.ts +29 -0
- package/dist/lexer.js +179 -0
- package/dist/math-cmap.cjs +81 -0
- package/dist/math-cmap.d.cts +6 -0
- package/dist/math-cmap.d.ts +6 -0
- package/dist/math-cmap.js +80 -0
- package/dist/math-content-write.cjs +83 -0
- package/dist/math-content-write.d.cts +11 -0
- package/dist/math-content-write.d.ts +11 -0
- package/dist/math-content-write.js +81 -0
- package/dist/math-font-write.cjs +100 -0
- package/dist/math-font-write.d.cts +19 -0
- package/dist/math-font-write.d.ts +19 -0
- package/dist/math-font-write.js +99 -0
- package/dist/math-font.cjs +103 -0
- package/dist/math-font.d.cts +25 -0
- package/dist/math-font.d.ts +25 -0
- package/dist/math-font.js +102 -0
- package/dist/math-hmtx.cjs +17 -0
- package/dist/math-hmtx.d.cts +8 -0
- package/dist/math-hmtx.d.ts +8 -0
- package/dist/math-hmtx.js +16 -0
- package/dist/math-table.cjs +117 -0
- package/dist/math-table.d.cts +45 -0
- package/dist/math-table.d.ts +45 -0
- package/dist/math-table.js +116 -0
- package/dist/math-types-Ba8qAf1p.d.cts +82 -0
- package/dist/math-types-Ba8qAf1p.d.ts +82 -0
- package/dist/math-types.cjs +0 -0
- package/dist/math-types.d.cts +2 -0
- package/dist/math-types.d.ts +2 -0
- package/dist/math-types.js +0 -0
- package/dist/matrix-B3c2_a2f.d.cts +18 -0
- package/dist/matrix-B3c2_a2f.d.ts +18 -0
- package/dist/matrix.cjs +93 -0
- package/dist/matrix.d.cts +2 -0
- package/dist/matrix.d.ts +2 -0
- package/dist/matrix.js +83 -0
- package/dist/measure.cjs +54 -0
- package/dist/measure.d.cts +20 -0
- package/dist/measure.d.ts +20 -0
- package/dist/measure.js +53 -0
- package/dist/objects.cjs +100 -0
- package/dist/objects.d.cts +58 -0
- package/dist/objects.d.ts +58 -0
- package/dist/objects.js +84 -0
- package/dist/parse.cjs +230 -0
- package/dist/parse.d.cts +13 -0
- package/dist/parse.d.ts +13 -0
- package/dist/parse.js +228 -0
- package/dist/predictors.cjs +64 -0
- package/dist/predictors.d.cts +13 -0
- package/dist/predictors.d.ts +13 -0
- package/dist/predictors.js +62 -0
- package/dist/read.cjs +418 -0
- package/dist/read.d.cts +21 -0
- package/dist/read.d.ts +21 -0
- package/dist/read.js +413 -0
- package/dist/serialize.cjs +69 -0
- package/dist/serialize.d.cts +8 -0
- package/dist/serialize.d.ts +8 -0
- package/dist/serialize.js +66 -0
- package/dist/sfnt.cjs +52 -0
- package/dist/sfnt.d.cts +17 -0
- package/dist/sfnt.d.ts +17 -0
- package/dist/sfnt.js +46 -0
- package/dist/text-layout.cjs +224 -0
- package/dist/text-layout.d.cts +37 -0
- package/dist/text-layout.d.ts +37 -0
- package/dist/text-layout.js +222 -0
- package/dist/util/abort.cjs +7 -0
- package/dist/util/abort.d.cts +4 -0
- package/dist/util/abort.d.ts +4 -0
- package/dist/util/abort.js +6 -0
- package/dist/util/base64.cjs +48 -0
- package/dist/util/base64.d.cts +5 -0
- package/dist/util/base64.d.ts +5 -0
- package/dist/util/base64.js +46 -0
- package/dist/winansi.cjs +5 -0
- package/dist/winansi.d.cts +20 -0
- package/dist/winansi.d.ts +20 -0
- package/dist/winansi.js +2 -0
- package/dist/write.cjs +421 -0
- package/dist/write.d.cts +16 -0
- package/dist/write.d.ts +16 -0
- package/dist/write.js +419 -0
- package/dist/xref.cjs +325 -0
- package/dist/xref.d.cts +21 -0
- package/dist/xref.d.ts +21 -0
- package/dist/xref.js +324 -0
- package/package.json +9 -1
package/dist/xref.cjs
ADDED
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
const require_bytes_reader = require("./bytes/reader.cjs");
|
|
3
|
+
const require_lexer = require("./lexer.cjs");
|
|
4
|
+
const require_objects = require("./objects.cjs");
|
|
5
|
+
const require_filters = require("./filters.cjs");
|
|
6
|
+
const require_parse = require("./parse.cjs");
|
|
7
|
+
//#region src/xref.ts
|
|
8
|
+
const STARTXREF_BYTES = new TextEncoder().encode("startxref");
|
|
9
|
+
const OBJ_BYTES = new TextEncoder().encode("obj");
|
|
10
|
+
const TRAILER_BYTES = new TextEncoder().encode("trailer");
|
|
11
|
+
const DELIMITER_BYTES = /* @__PURE__ */ new Set([
|
|
12
|
+
40,
|
|
13
|
+
41,
|
|
14
|
+
60,
|
|
15
|
+
62,
|
|
16
|
+
91,
|
|
17
|
+
93,
|
|
18
|
+
123,
|
|
19
|
+
125,
|
|
20
|
+
47,
|
|
21
|
+
37
|
|
22
|
+
]);
|
|
23
|
+
const MAX_XREF_SECTIONS = 64;
|
|
24
|
+
function readXref(bytes, sink) {
|
|
25
|
+
const startxrefOffset = findStartxrefOffset(bytes);
|
|
26
|
+
if (startxrefOffset !== void 0) {
|
|
27
|
+
const { entries, trailer } = walkXrefChain(bytes, startxrefOffset, sink);
|
|
28
|
+
if (isTableUsable(bytes, entries, trailer)) return {
|
|
29
|
+
entries,
|
|
30
|
+
trailer
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
sink({
|
|
34
|
+
code: "pdf/xref-recovered",
|
|
35
|
+
severity: "warning",
|
|
36
|
+
message: "the document's own cross-reference data was missing, unreadable, or did not check out against the bytes it points at; rebuilt the table by scanning the file for \"N G obj\" headers"
|
|
37
|
+
});
|
|
38
|
+
return recoverXrefByLinearScan(bytes, sink);
|
|
39
|
+
}
|
|
40
|
+
function findLastOccurrence(bytes, needle) {
|
|
41
|
+
outer: for (let i = bytes.length - needle.length; i >= 0; i--) {
|
|
42
|
+
for (let j = 0; j < needle.length; j++) if (bytes[i + j] !== needle[j]) continue outer;
|
|
43
|
+
return i;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
function findStartxrefOffset(bytes) {
|
|
47
|
+
const at = findLastOccurrence(bytes, STARTXREF_BYTES);
|
|
48
|
+
if (at === void 0) return;
|
|
49
|
+
const reader = new require_bytes_reader.ByteReader(bytes);
|
|
50
|
+
reader.seek(at + STARTXREF_BYTES.length);
|
|
51
|
+
const token = require_lexer.nextToken(reader);
|
|
52
|
+
return token?.kind === "number" ? token.value : void 0;
|
|
53
|
+
}
|
|
54
|
+
function isTableUsable(bytes, entries, trailer) {
|
|
55
|
+
const root = require_objects.dictGet(trailer, "Root");
|
|
56
|
+
if (root?.kind !== "ref") return false;
|
|
57
|
+
const rootEntry = entries.get(root.num);
|
|
58
|
+
if (rootEntry === void 0) return false;
|
|
59
|
+
if (rootEntry.type === "compressed") return true;
|
|
60
|
+
return looksLikeObjectHeaderAt(bytes, rootEntry.offset, root.num);
|
|
61
|
+
}
|
|
62
|
+
function looksLikeObjectHeaderAt(bytes, offset, expectedNum) {
|
|
63
|
+
if (offset < 0 || offset >= bytes.length) return false;
|
|
64
|
+
const reader = new require_bytes_reader.ByteReader(bytes);
|
|
65
|
+
reader.seek(offset);
|
|
66
|
+
const numTok = require_lexer.nextToken(reader);
|
|
67
|
+
const genTok = require_lexer.nextToken(reader);
|
|
68
|
+
const objTok = require_lexer.nextToken(reader);
|
|
69
|
+
return numTok?.kind === "number" && numTok.value === expectedNum && genTok?.kind === "number" && objTok?.kind === "keyword" && objTok.value === "obj";
|
|
70
|
+
}
|
|
71
|
+
function walkXrefChain(bytes, startOffset, sink) {
|
|
72
|
+
const merged = /* @__PURE__ */ new Map();
|
|
73
|
+
const mergedTrailerEntries = /* @__PURE__ */ new Map();
|
|
74
|
+
const visited = /* @__PURE__ */ new Set();
|
|
75
|
+
let offset = startOffset;
|
|
76
|
+
let sections = 0;
|
|
77
|
+
while (offset !== void 0 && !visited.has(offset) && sections < MAX_XREF_SECTIONS) {
|
|
78
|
+
visited.add(offset);
|
|
79
|
+
sections++;
|
|
80
|
+
const section = readXrefSection(bytes, offset, sink);
|
|
81
|
+
if (section === void 0) {
|
|
82
|
+
sink({
|
|
83
|
+
code: "pdf/xref-entry-invalid",
|
|
84
|
+
severity: "warning",
|
|
85
|
+
message: `no readable cross-reference section at offset ${String(offset)}`
|
|
86
|
+
});
|
|
87
|
+
break;
|
|
88
|
+
}
|
|
89
|
+
for (const [num, entry] of section.entries) if (!merged.has(num)) merged.set(num, entry);
|
|
90
|
+
for (const [key, value] of section.trailer.entries) if (!mergedTrailerEntries.has(key)) mergedTrailerEntries.set(key, value);
|
|
91
|
+
offset = section.prevOffset;
|
|
92
|
+
}
|
|
93
|
+
return {
|
|
94
|
+
entries: merged,
|
|
95
|
+
trailer: require_objects.pdfDict(mergedTrailerEntries)
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
function readXrefSection(bytes, offset, sink) {
|
|
99
|
+
const reader = new require_bytes_reader.ByteReader(bytes);
|
|
100
|
+
reader.seek(offset);
|
|
101
|
+
const mark = reader.mark();
|
|
102
|
+
const token = require_lexer.nextToken(reader);
|
|
103
|
+
if (token?.kind === "keyword" && token.value === "xref") return readClassicXrefSection(reader, sink);
|
|
104
|
+
reader.reset(mark);
|
|
105
|
+
const indirect = require_parse.parseIndirectObject(reader, sink);
|
|
106
|
+
if (indirect?.value.kind === "stream") return readXrefStreamSection(indirect.value.dict, indirect.value.raw, sink);
|
|
107
|
+
}
|
|
108
|
+
function readClassicXrefSection(reader, sink) {
|
|
109
|
+
const entries = /* @__PURE__ */ new Map();
|
|
110
|
+
for (;;) {
|
|
111
|
+
const mark = reader.mark();
|
|
112
|
+
const startTok = require_lexer.nextToken(reader);
|
|
113
|
+
if (startTok?.kind !== "number") {
|
|
114
|
+
reader.reset(mark);
|
|
115
|
+
break;
|
|
116
|
+
}
|
|
117
|
+
const countTok = require_lexer.nextToken(reader);
|
|
118
|
+
if (countTok?.kind !== "number") {
|
|
119
|
+
sink({
|
|
120
|
+
code: "pdf/xref-entry-invalid",
|
|
121
|
+
severity: "warning",
|
|
122
|
+
message: "classic xref subsection header was not \"start count\""
|
|
123
|
+
});
|
|
124
|
+
reader.reset(mark);
|
|
125
|
+
break;
|
|
126
|
+
}
|
|
127
|
+
for (let i = 0; i < countTok.value; i++) {
|
|
128
|
+
const offsetTok = require_lexer.nextToken(reader);
|
|
129
|
+
const genTok = require_lexer.nextToken(reader);
|
|
130
|
+
const typeTok = require_lexer.nextToken(reader);
|
|
131
|
+
if (offsetTok?.kind !== "number" || genTok?.kind !== "number" || typeTok?.kind !== "keyword") {
|
|
132
|
+
sink({
|
|
133
|
+
code: "pdf/xref-entry-invalid",
|
|
134
|
+
severity: "warning",
|
|
135
|
+
message: `malformed classic xref entry for object ${String(startTok.value + i)}`
|
|
136
|
+
});
|
|
137
|
+
break;
|
|
138
|
+
}
|
|
139
|
+
if (typeTok.value === "n") entries.set(startTok.value + i, {
|
|
140
|
+
type: "offset",
|
|
141
|
+
offset: offsetTok.value,
|
|
142
|
+
gen: genTok.value
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
const trailerTok = require_lexer.nextToken(reader);
|
|
147
|
+
if (trailerTok?.kind !== "keyword" || trailerTok.value !== "trailer") {
|
|
148
|
+
sink({
|
|
149
|
+
code: "pdf/xref-entry-invalid",
|
|
150
|
+
severity: "warning",
|
|
151
|
+
message: "classic xref section was not followed by a \"trailer\" keyword"
|
|
152
|
+
});
|
|
153
|
+
return {
|
|
154
|
+
entries,
|
|
155
|
+
trailer: require_objects.pdfDict({}),
|
|
156
|
+
prevOffset: void 0
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
const trailerValue = require_parse.parseValue(reader, sink);
|
|
160
|
+
const trailer = require_objects.asDict(trailerValue) ?? require_objects.pdfDict({});
|
|
161
|
+
return {
|
|
162
|
+
entries,
|
|
163
|
+
trailer,
|
|
164
|
+
prevOffset: require_objects.asNumber(require_objects.dictGet(trailer, "Prev"))
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
function readXrefStreamSection(dict, raw, sink) {
|
|
168
|
+
const decoded = require_filters.decodeStream(raw, dict, sink);
|
|
169
|
+
const widths = require_objects.asArray(require_objects.dictGet(dict, "W"));
|
|
170
|
+
const w0 = widths !== void 0 ? require_objects.asNumber(widths[0]) ?? 0 : 1;
|
|
171
|
+
const w1 = widths !== void 0 ? require_objects.asNumber(widths[1]) ?? 0 : 1;
|
|
172
|
+
const w2 = widths !== void 0 ? require_objects.asNumber(widths[2]) ?? 0 : 1;
|
|
173
|
+
const rowLength = w0 + w1 + w2;
|
|
174
|
+
const size = require_objects.asNumber(require_objects.dictGet(dict, "Size")) ?? 0;
|
|
175
|
+
const indexArray = require_objects.asArray(require_objects.dictGet(dict, "Index"));
|
|
176
|
+
const ranges = [];
|
|
177
|
+
if (indexArray !== void 0) for (let i = 0; i + 1 < indexArray.length; i += 2) {
|
|
178
|
+
const start = require_objects.asNumber(indexArray[i]);
|
|
179
|
+
const count = require_objects.asNumber(indexArray[i + 1]);
|
|
180
|
+
if (start !== void 0 && count !== void 0) ranges.push([start, count]);
|
|
181
|
+
}
|
|
182
|
+
else ranges.push([0, size]);
|
|
183
|
+
const entries = /* @__PURE__ */ new Map();
|
|
184
|
+
let rowOffset = 0;
|
|
185
|
+
for (const [start, count] of ranges) for (let i = 0; i < count; i++) {
|
|
186
|
+
rowOffset += rowLength;
|
|
187
|
+
const base = rowOffset - rowLength;
|
|
188
|
+
if (rowLength === 0 || base + rowLength > decoded.bytes.length) {
|
|
189
|
+
sink({
|
|
190
|
+
code: "pdf/xref-entry-invalid",
|
|
191
|
+
severity: "warning",
|
|
192
|
+
message: `xref stream ran out of data before object ${String(start + i)}`
|
|
193
|
+
});
|
|
194
|
+
break;
|
|
195
|
+
}
|
|
196
|
+
const type = w0 === 0 ? 1 : readBigEndian(decoded.bytes, base, w0);
|
|
197
|
+
const field2 = readBigEndian(decoded.bytes, base + w0, w1);
|
|
198
|
+
const field3 = readBigEndian(decoded.bytes, base + w0 + w1, w2);
|
|
199
|
+
const objNum = start + i;
|
|
200
|
+
if (type === 0) continue;
|
|
201
|
+
else if (type === 1) entries.set(objNum, {
|
|
202
|
+
type: "offset",
|
|
203
|
+
offset: field2,
|
|
204
|
+
gen: field3
|
|
205
|
+
});
|
|
206
|
+
else if (type === 2) entries.set(objNum, {
|
|
207
|
+
type: "compressed",
|
|
208
|
+
streamObjNum: field2,
|
|
209
|
+
indexInStream: field3
|
|
210
|
+
});
|
|
211
|
+
else sink({
|
|
212
|
+
code: "pdf/xref-entry-invalid",
|
|
213
|
+
severity: "warning",
|
|
214
|
+
message: `xref stream row for object ${String(objNum)} has unrecognised type ${String(type)}`
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
return {
|
|
218
|
+
entries,
|
|
219
|
+
trailer: dict,
|
|
220
|
+
prevOffset: require_objects.asNumber(require_objects.dictGet(dict, "Prev"))
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
function readBigEndian(bytes, offset, width) {
|
|
224
|
+
let value = 0;
|
|
225
|
+
for (let i = 0; i < width; i++) value = value * 256 + (bytes[offset + i] ?? 0);
|
|
226
|
+
return value;
|
|
227
|
+
}
|
|
228
|
+
function isBoundaryByte(byte) {
|
|
229
|
+
return byte === void 0 || require_bytes_reader.isAsciiWhitespace(byte) || DELIMITER_BYTES.has(byte);
|
|
230
|
+
}
|
|
231
|
+
function findAllKeywordPositions(bytes, needle) {
|
|
232
|
+
const positions = [];
|
|
233
|
+
outer: for (let i = 0; i <= bytes.length - needle.length; i++) {
|
|
234
|
+
if (!isBoundaryByte(bytes[i - 1])) continue;
|
|
235
|
+
for (let j = 0; j < needle.length; j++) if (bytes[i + j] !== needle[j]) continue outer;
|
|
236
|
+
if (!isBoundaryByte(bytes[i + needle.length])) continue;
|
|
237
|
+
positions.push(i);
|
|
238
|
+
}
|
|
239
|
+
return positions;
|
|
240
|
+
}
|
|
241
|
+
function isDigitByte(byte) {
|
|
242
|
+
return byte !== void 0 && byte >= 48 && byte <= 57;
|
|
243
|
+
}
|
|
244
|
+
function scanBackWhile(bytes, end, predicate) {
|
|
245
|
+
let i = end;
|
|
246
|
+
while (i > 0 && predicate(bytes[i - 1])) i--;
|
|
247
|
+
return i;
|
|
248
|
+
}
|
|
249
|
+
function scanObjectHeaderBackward(bytes, objKeywordStart) {
|
|
250
|
+
const genEnd = scanBackWhile(bytes, objKeywordStart, require_bytes_reader.isAsciiWhitespace);
|
|
251
|
+
const genStart = scanBackWhile(bytes, genEnd, isDigitByte);
|
|
252
|
+
if (genStart === genEnd) return;
|
|
253
|
+
const numEnd = scanBackWhile(bytes, genStart, require_bytes_reader.isAsciiWhitespace);
|
|
254
|
+
const numStart = scanBackWhile(bytes, numEnd, isDigitByte);
|
|
255
|
+
if (numStart === numEnd) return;
|
|
256
|
+
const decoder = new TextDecoder("latin1");
|
|
257
|
+
return {
|
|
258
|
+
num: Number(decoder.decode(bytes.subarray(numStart, numEnd))),
|
|
259
|
+
gen: Number(decoder.decode(bytes.subarray(genStart, genEnd))),
|
|
260
|
+
headerStart: numStart
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
function recoverXrefByLinearScan(bytes, sink) {
|
|
264
|
+
const entries = /* @__PURE__ */ new Map();
|
|
265
|
+
for (const objPos of findAllKeywordPositions(bytes, OBJ_BYTES)) {
|
|
266
|
+
const header = scanObjectHeaderBackward(bytes, objPos);
|
|
267
|
+
if (header === void 0) continue;
|
|
268
|
+
entries.set(header.num, {
|
|
269
|
+
type: "offset",
|
|
270
|
+
offset: header.headerStart,
|
|
271
|
+
gen: header.gen
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
let lastCatalogRef;
|
|
275
|
+
let lastXrefTrailerDict;
|
|
276
|
+
for (const [num, entry] of entries) {
|
|
277
|
+
if (entry.type !== "offset") continue;
|
|
278
|
+
const reader = new require_bytes_reader.ByteReader(bytes);
|
|
279
|
+
reader.seek(entry.offset);
|
|
280
|
+
const indirect = require_parse.parseIndirectObject(reader, sink);
|
|
281
|
+
if (indirect === void 0) continue;
|
|
282
|
+
const dict = require_objects.asDict(indirect.value);
|
|
283
|
+
if (dict === void 0) continue;
|
|
284
|
+
if (require_objects.isName(require_objects.dictGet(dict, "Type"), "Catalog")) lastCatalogRef = require_objects.pdfRef(num, entry.gen);
|
|
285
|
+
if (require_objects.isName(require_objects.dictGet(dict, "Type"), "XRef")) lastXrefTrailerDict = dict;
|
|
286
|
+
if (indirect.value.kind === "stream" && require_objects.isName(require_objects.dictGet(dict, "Type"), "ObjStm")) registerObjStmContents(num, indirect.value.dict, indirect.value.raw, entries, sink);
|
|
287
|
+
}
|
|
288
|
+
return {
|
|
289
|
+
entries,
|
|
290
|
+
trailer: findRecoveredTrailer(bytes, sink) ?? lastXrefTrailerDict ?? (lastCatalogRef !== void 0 ? require_objects.pdfDict({ Root: lastCatalogRef }) : require_objects.pdfDict({}))
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
function registerObjStmContents(streamObjNum, dict, raw, entries, sink) {
|
|
294
|
+
const decoded = require_filters.decodeStream(raw, dict, sink);
|
|
295
|
+
const n = require_objects.asNumber(require_objects.dictGet(dict, "N")) ?? 0;
|
|
296
|
+
const reader = new require_bytes_reader.ByteReader(decoded.bytes);
|
|
297
|
+
let index = 0;
|
|
298
|
+
for (let i = 0; i < n; i++) {
|
|
299
|
+
const numTok = require_lexer.nextToken(reader);
|
|
300
|
+
const offTok = require_lexer.nextToken(reader);
|
|
301
|
+
if (numTok?.kind !== "number" || offTok?.kind !== "number") {
|
|
302
|
+
sink({
|
|
303
|
+
code: "pdf/xref-entry-invalid",
|
|
304
|
+
severity: "warning",
|
|
305
|
+
message: `recovered object stream ${String(streamObjNum)} header is truncated at entry ${String(i)}`
|
|
306
|
+
});
|
|
307
|
+
break;
|
|
308
|
+
}
|
|
309
|
+
if (!entries.has(numTok.value)) entries.set(numTok.value, {
|
|
310
|
+
type: "compressed",
|
|
311
|
+
streamObjNum,
|
|
312
|
+
indexInStream: index
|
|
313
|
+
});
|
|
314
|
+
index++;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
function findRecoveredTrailer(bytes, sink) {
|
|
318
|
+
const at = findLastOccurrence(bytes, TRAILER_BYTES);
|
|
319
|
+
if (at === void 0) return;
|
|
320
|
+
const reader = new require_bytes_reader.ByteReader(bytes);
|
|
321
|
+
reader.seek(at + TRAILER_BYTES.length);
|
|
322
|
+
return require_objects.asDict(require_parse.parseValue(reader, sink));
|
|
323
|
+
}
|
|
324
|
+
//#endregion
|
|
325
|
+
exports.readXref = readXref;
|
package/dist/xref.d.cts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { PdfDiagnosticSink } from "./diagnostics.cjs";
|
|
2
|
+
import { PdfDict } from "./objects.cjs";
|
|
3
|
+
//#region src/xref.d.ts
|
|
4
|
+
interface XrefOffsetEntry {
|
|
5
|
+
readonly type: 'offset';
|
|
6
|
+
readonly offset: number;
|
|
7
|
+
readonly gen: number;
|
|
8
|
+
}
|
|
9
|
+
interface XrefCompressedEntry {
|
|
10
|
+
readonly type: 'compressed';
|
|
11
|
+
readonly streamObjNum: number;
|
|
12
|
+
readonly indexInStream: number;
|
|
13
|
+
}
|
|
14
|
+
type XrefEntry = XrefOffsetEntry | XrefCompressedEntry;
|
|
15
|
+
interface XrefTable {
|
|
16
|
+
readonly entries: ReadonlyMap<number, XrefEntry>;
|
|
17
|
+
readonly trailer: PdfDict;
|
|
18
|
+
}
|
|
19
|
+
declare function readXref(bytes: Uint8Array<ArrayBuffer>, sink: PdfDiagnosticSink): XrefTable;
|
|
20
|
+
//#endregion
|
|
21
|
+
export { XrefCompressedEntry, XrefEntry, XrefOffsetEntry, XrefTable, readXref };
|
package/dist/xref.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { PdfDiagnosticSink } from "./diagnostics.js";
|
|
2
|
+
import { PdfDict } from "./objects.js";
|
|
3
|
+
//#region src/xref.d.ts
|
|
4
|
+
interface XrefOffsetEntry {
|
|
5
|
+
readonly type: 'offset';
|
|
6
|
+
readonly offset: number;
|
|
7
|
+
readonly gen: number;
|
|
8
|
+
}
|
|
9
|
+
interface XrefCompressedEntry {
|
|
10
|
+
readonly type: 'compressed';
|
|
11
|
+
readonly streamObjNum: number;
|
|
12
|
+
readonly indexInStream: number;
|
|
13
|
+
}
|
|
14
|
+
type XrefEntry = XrefOffsetEntry | XrefCompressedEntry;
|
|
15
|
+
interface XrefTable {
|
|
16
|
+
readonly entries: ReadonlyMap<number, XrefEntry>;
|
|
17
|
+
readonly trailer: PdfDict;
|
|
18
|
+
}
|
|
19
|
+
declare function readXref(bytes: Uint8Array<ArrayBuffer>, sink: PdfDiagnosticSink): XrefTable;
|
|
20
|
+
//#endregion
|
|
21
|
+
export { XrefCompressedEntry, XrefEntry, XrefOffsetEntry, XrefTable, readXref };
|
package/dist/xref.js
ADDED
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
import { ByteReader, isAsciiWhitespace } from "./bytes/reader.js";
|
|
2
|
+
import { nextToken } from "./lexer.js";
|
|
3
|
+
import { asArray, asDict, asNumber, dictGet, isName, pdfDict, pdfRef } from "./objects.js";
|
|
4
|
+
import { decodeStream } from "./filters.js";
|
|
5
|
+
import { parseIndirectObject, parseValue } from "./parse.js";
|
|
6
|
+
//#region src/xref.ts
|
|
7
|
+
const STARTXREF_BYTES = new TextEncoder().encode("startxref");
|
|
8
|
+
const OBJ_BYTES = new TextEncoder().encode("obj");
|
|
9
|
+
const TRAILER_BYTES = new TextEncoder().encode("trailer");
|
|
10
|
+
const DELIMITER_BYTES = /* @__PURE__ */ new Set([
|
|
11
|
+
40,
|
|
12
|
+
41,
|
|
13
|
+
60,
|
|
14
|
+
62,
|
|
15
|
+
91,
|
|
16
|
+
93,
|
|
17
|
+
123,
|
|
18
|
+
125,
|
|
19
|
+
47,
|
|
20
|
+
37
|
|
21
|
+
]);
|
|
22
|
+
const MAX_XREF_SECTIONS = 64;
|
|
23
|
+
function readXref(bytes, sink) {
|
|
24
|
+
const startxrefOffset = findStartxrefOffset(bytes);
|
|
25
|
+
if (startxrefOffset !== void 0) {
|
|
26
|
+
const { entries, trailer } = walkXrefChain(bytes, startxrefOffset, sink);
|
|
27
|
+
if (isTableUsable(bytes, entries, trailer)) return {
|
|
28
|
+
entries,
|
|
29
|
+
trailer
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
sink({
|
|
33
|
+
code: "pdf/xref-recovered",
|
|
34
|
+
severity: "warning",
|
|
35
|
+
message: "the document's own cross-reference data was missing, unreadable, or did not check out against the bytes it points at; rebuilt the table by scanning the file for \"N G obj\" headers"
|
|
36
|
+
});
|
|
37
|
+
return recoverXrefByLinearScan(bytes, sink);
|
|
38
|
+
}
|
|
39
|
+
function findLastOccurrence(bytes, needle) {
|
|
40
|
+
outer: for (let i = bytes.length - needle.length; i >= 0; i--) {
|
|
41
|
+
for (let j = 0; j < needle.length; j++) if (bytes[i + j] !== needle[j]) continue outer;
|
|
42
|
+
return i;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
function findStartxrefOffset(bytes) {
|
|
46
|
+
const at = findLastOccurrence(bytes, STARTXREF_BYTES);
|
|
47
|
+
if (at === void 0) return;
|
|
48
|
+
const reader = new ByteReader(bytes);
|
|
49
|
+
reader.seek(at + STARTXREF_BYTES.length);
|
|
50
|
+
const token = nextToken(reader);
|
|
51
|
+
return token?.kind === "number" ? token.value : void 0;
|
|
52
|
+
}
|
|
53
|
+
function isTableUsable(bytes, entries, trailer) {
|
|
54
|
+
const root = dictGet(trailer, "Root");
|
|
55
|
+
if (root?.kind !== "ref") return false;
|
|
56
|
+
const rootEntry = entries.get(root.num);
|
|
57
|
+
if (rootEntry === void 0) return false;
|
|
58
|
+
if (rootEntry.type === "compressed") return true;
|
|
59
|
+
return looksLikeObjectHeaderAt(bytes, rootEntry.offset, root.num);
|
|
60
|
+
}
|
|
61
|
+
function looksLikeObjectHeaderAt(bytes, offset, expectedNum) {
|
|
62
|
+
if (offset < 0 || offset >= bytes.length) return false;
|
|
63
|
+
const reader = new ByteReader(bytes);
|
|
64
|
+
reader.seek(offset);
|
|
65
|
+
const numTok = nextToken(reader);
|
|
66
|
+
const genTok = nextToken(reader);
|
|
67
|
+
const objTok = nextToken(reader);
|
|
68
|
+
return numTok?.kind === "number" && numTok.value === expectedNum && genTok?.kind === "number" && objTok?.kind === "keyword" && objTok.value === "obj";
|
|
69
|
+
}
|
|
70
|
+
function walkXrefChain(bytes, startOffset, sink) {
|
|
71
|
+
const merged = /* @__PURE__ */ new Map();
|
|
72
|
+
const mergedTrailerEntries = /* @__PURE__ */ new Map();
|
|
73
|
+
const visited = /* @__PURE__ */ new Set();
|
|
74
|
+
let offset = startOffset;
|
|
75
|
+
let sections = 0;
|
|
76
|
+
while (offset !== void 0 && !visited.has(offset) && sections < MAX_XREF_SECTIONS) {
|
|
77
|
+
visited.add(offset);
|
|
78
|
+
sections++;
|
|
79
|
+
const section = readXrefSection(bytes, offset, sink);
|
|
80
|
+
if (section === void 0) {
|
|
81
|
+
sink({
|
|
82
|
+
code: "pdf/xref-entry-invalid",
|
|
83
|
+
severity: "warning",
|
|
84
|
+
message: `no readable cross-reference section at offset ${String(offset)}`
|
|
85
|
+
});
|
|
86
|
+
break;
|
|
87
|
+
}
|
|
88
|
+
for (const [num, entry] of section.entries) if (!merged.has(num)) merged.set(num, entry);
|
|
89
|
+
for (const [key, value] of section.trailer.entries) if (!mergedTrailerEntries.has(key)) mergedTrailerEntries.set(key, value);
|
|
90
|
+
offset = section.prevOffset;
|
|
91
|
+
}
|
|
92
|
+
return {
|
|
93
|
+
entries: merged,
|
|
94
|
+
trailer: pdfDict(mergedTrailerEntries)
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
function readXrefSection(bytes, offset, sink) {
|
|
98
|
+
const reader = new ByteReader(bytes);
|
|
99
|
+
reader.seek(offset);
|
|
100
|
+
const mark = reader.mark();
|
|
101
|
+
const token = nextToken(reader);
|
|
102
|
+
if (token?.kind === "keyword" && token.value === "xref") return readClassicXrefSection(reader, sink);
|
|
103
|
+
reader.reset(mark);
|
|
104
|
+
const indirect = parseIndirectObject(reader, sink);
|
|
105
|
+
if (indirect?.value.kind === "stream") return readXrefStreamSection(indirect.value.dict, indirect.value.raw, sink);
|
|
106
|
+
}
|
|
107
|
+
function readClassicXrefSection(reader, sink) {
|
|
108
|
+
const entries = /* @__PURE__ */ new Map();
|
|
109
|
+
for (;;) {
|
|
110
|
+
const mark = reader.mark();
|
|
111
|
+
const startTok = nextToken(reader);
|
|
112
|
+
if (startTok?.kind !== "number") {
|
|
113
|
+
reader.reset(mark);
|
|
114
|
+
break;
|
|
115
|
+
}
|
|
116
|
+
const countTok = nextToken(reader);
|
|
117
|
+
if (countTok?.kind !== "number") {
|
|
118
|
+
sink({
|
|
119
|
+
code: "pdf/xref-entry-invalid",
|
|
120
|
+
severity: "warning",
|
|
121
|
+
message: "classic xref subsection header was not \"start count\""
|
|
122
|
+
});
|
|
123
|
+
reader.reset(mark);
|
|
124
|
+
break;
|
|
125
|
+
}
|
|
126
|
+
for (let i = 0; i < countTok.value; i++) {
|
|
127
|
+
const offsetTok = nextToken(reader);
|
|
128
|
+
const genTok = nextToken(reader);
|
|
129
|
+
const typeTok = nextToken(reader);
|
|
130
|
+
if (offsetTok?.kind !== "number" || genTok?.kind !== "number" || typeTok?.kind !== "keyword") {
|
|
131
|
+
sink({
|
|
132
|
+
code: "pdf/xref-entry-invalid",
|
|
133
|
+
severity: "warning",
|
|
134
|
+
message: `malformed classic xref entry for object ${String(startTok.value + i)}`
|
|
135
|
+
});
|
|
136
|
+
break;
|
|
137
|
+
}
|
|
138
|
+
if (typeTok.value === "n") entries.set(startTok.value + i, {
|
|
139
|
+
type: "offset",
|
|
140
|
+
offset: offsetTok.value,
|
|
141
|
+
gen: genTok.value
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
const trailerTok = nextToken(reader);
|
|
146
|
+
if (trailerTok?.kind !== "keyword" || trailerTok.value !== "trailer") {
|
|
147
|
+
sink({
|
|
148
|
+
code: "pdf/xref-entry-invalid",
|
|
149
|
+
severity: "warning",
|
|
150
|
+
message: "classic xref section was not followed by a \"trailer\" keyword"
|
|
151
|
+
});
|
|
152
|
+
return {
|
|
153
|
+
entries,
|
|
154
|
+
trailer: pdfDict({}),
|
|
155
|
+
prevOffset: void 0
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
const trailerValue = parseValue(reader, sink);
|
|
159
|
+
const trailer = asDict(trailerValue) ?? pdfDict({});
|
|
160
|
+
return {
|
|
161
|
+
entries,
|
|
162
|
+
trailer,
|
|
163
|
+
prevOffset: asNumber(dictGet(trailer, "Prev"))
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
function readXrefStreamSection(dict, raw, sink) {
|
|
167
|
+
const decoded = decodeStream(raw, dict, sink);
|
|
168
|
+
const widths = asArray(dictGet(dict, "W"));
|
|
169
|
+
const w0 = widths !== void 0 ? asNumber(widths[0]) ?? 0 : 1;
|
|
170
|
+
const w1 = widths !== void 0 ? asNumber(widths[1]) ?? 0 : 1;
|
|
171
|
+
const w2 = widths !== void 0 ? asNumber(widths[2]) ?? 0 : 1;
|
|
172
|
+
const rowLength = w0 + w1 + w2;
|
|
173
|
+
const size = asNumber(dictGet(dict, "Size")) ?? 0;
|
|
174
|
+
const indexArray = asArray(dictGet(dict, "Index"));
|
|
175
|
+
const ranges = [];
|
|
176
|
+
if (indexArray !== void 0) for (let i = 0; i + 1 < indexArray.length; i += 2) {
|
|
177
|
+
const start = asNumber(indexArray[i]);
|
|
178
|
+
const count = asNumber(indexArray[i + 1]);
|
|
179
|
+
if (start !== void 0 && count !== void 0) ranges.push([start, count]);
|
|
180
|
+
}
|
|
181
|
+
else ranges.push([0, size]);
|
|
182
|
+
const entries = /* @__PURE__ */ new Map();
|
|
183
|
+
let rowOffset = 0;
|
|
184
|
+
for (const [start, count] of ranges) for (let i = 0; i < count; i++) {
|
|
185
|
+
rowOffset += rowLength;
|
|
186
|
+
const base = rowOffset - rowLength;
|
|
187
|
+
if (rowLength === 0 || base + rowLength > decoded.bytes.length) {
|
|
188
|
+
sink({
|
|
189
|
+
code: "pdf/xref-entry-invalid",
|
|
190
|
+
severity: "warning",
|
|
191
|
+
message: `xref stream ran out of data before object ${String(start + i)}`
|
|
192
|
+
});
|
|
193
|
+
break;
|
|
194
|
+
}
|
|
195
|
+
const type = w0 === 0 ? 1 : readBigEndian(decoded.bytes, base, w0);
|
|
196
|
+
const field2 = readBigEndian(decoded.bytes, base + w0, w1);
|
|
197
|
+
const field3 = readBigEndian(decoded.bytes, base + w0 + w1, w2);
|
|
198
|
+
const objNum = start + i;
|
|
199
|
+
if (type === 0) continue;
|
|
200
|
+
else if (type === 1) entries.set(objNum, {
|
|
201
|
+
type: "offset",
|
|
202
|
+
offset: field2,
|
|
203
|
+
gen: field3
|
|
204
|
+
});
|
|
205
|
+
else if (type === 2) entries.set(objNum, {
|
|
206
|
+
type: "compressed",
|
|
207
|
+
streamObjNum: field2,
|
|
208
|
+
indexInStream: field3
|
|
209
|
+
});
|
|
210
|
+
else sink({
|
|
211
|
+
code: "pdf/xref-entry-invalid",
|
|
212
|
+
severity: "warning",
|
|
213
|
+
message: `xref stream row for object ${String(objNum)} has unrecognised type ${String(type)}`
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
return {
|
|
217
|
+
entries,
|
|
218
|
+
trailer: dict,
|
|
219
|
+
prevOffset: asNumber(dictGet(dict, "Prev"))
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
function readBigEndian(bytes, offset, width) {
|
|
223
|
+
let value = 0;
|
|
224
|
+
for (let i = 0; i < width; i++) value = value * 256 + (bytes[offset + i] ?? 0);
|
|
225
|
+
return value;
|
|
226
|
+
}
|
|
227
|
+
function isBoundaryByte(byte) {
|
|
228
|
+
return byte === void 0 || isAsciiWhitespace(byte) || DELIMITER_BYTES.has(byte);
|
|
229
|
+
}
|
|
230
|
+
function findAllKeywordPositions(bytes, needle) {
|
|
231
|
+
const positions = [];
|
|
232
|
+
outer: for (let i = 0; i <= bytes.length - needle.length; i++) {
|
|
233
|
+
if (!isBoundaryByte(bytes[i - 1])) continue;
|
|
234
|
+
for (let j = 0; j < needle.length; j++) if (bytes[i + j] !== needle[j]) continue outer;
|
|
235
|
+
if (!isBoundaryByte(bytes[i + needle.length])) continue;
|
|
236
|
+
positions.push(i);
|
|
237
|
+
}
|
|
238
|
+
return positions;
|
|
239
|
+
}
|
|
240
|
+
function isDigitByte(byte) {
|
|
241
|
+
return byte !== void 0 && byte >= 48 && byte <= 57;
|
|
242
|
+
}
|
|
243
|
+
function scanBackWhile(bytes, end, predicate) {
|
|
244
|
+
let i = end;
|
|
245
|
+
while (i > 0 && predicate(bytes[i - 1])) i--;
|
|
246
|
+
return i;
|
|
247
|
+
}
|
|
248
|
+
function scanObjectHeaderBackward(bytes, objKeywordStart) {
|
|
249
|
+
const genEnd = scanBackWhile(bytes, objKeywordStart, isAsciiWhitespace);
|
|
250
|
+
const genStart = scanBackWhile(bytes, genEnd, isDigitByte);
|
|
251
|
+
if (genStart === genEnd) return;
|
|
252
|
+
const numEnd = scanBackWhile(bytes, genStart, isAsciiWhitespace);
|
|
253
|
+
const numStart = scanBackWhile(bytes, numEnd, isDigitByte);
|
|
254
|
+
if (numStart === numEnd) return;
|
|
255
|
+
const decoder = new TextDecoder("latin1");
|
|
256
|
+
return {
|
|
257
|
+
num: Number(decoder.decode(bytes.subarray(numStart, numEnd))),
|
|
258
|
+
gen: Number(decoder.decode(bytes.subarray(genStart, genEnd))),
|
|
259
|
+
headerStart: numStart
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
function recoverXrefByLinearScan(bytes, sink) {
|
|
263
|
+
const entries = /* @__PURE__ */ new Map();
|
|
264
|
+
for (const objPos of findAllKeywordPositions(bytes, OBJ_BYTES)) {
|
|
265
|
+
const header = scanObjectHeaderBackward(bytes, objPos);
|
|
266
|
+
if (header === void 0) continue;
|
|
267
|
+
entries.set(header.num, {
|
|
268
|
+
type: "offset",
|
|
269
|
+
offset: header.headerStart,
|
|
270
|
+
gen: header.gen
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
let lastCatalogRef;
|
|
274
|
+
let lastXrefTrailerDict;
|
|
275
|
+
for (const [num, entry] of entries) {
|
|
276
|
+
if (entry.type !== "offset") continue;
|
|
277
|
+
const reader = new ByteReader(bytes);
|
|
278
|
+
reader.seek(entry.offset);
|
|
279
|
+
const indirect = parseIndirectObject(reader, sink);
|
|
280
|
+
if (indirect === void 0) continue;
|
|
281
|
+
const dict = asDict(indirect.value);
|
|
282
|
+
if (dict === void 0) continue;
|
|
283
|
+
if (isName(dictGet(dict, "Type"), "Catalog")) lastCatalogRef = pdfRef(num, entry.gen);
|
|
284
|
+
if (isName(dictGet(dict, "Type"), "XRef")) lastXrefTrailerDict = dict;
|
|
285
|
+
if (indirect.value.kind === "stream" && isName(dictGet(dict, "Type"), "ObjStm")) registerObjStmContents(num, indirect.value.dict, indirect.value.raw, entries, sink);
|
|
286
|
+
}
|
|
287
|
+
return {
|
|
288
|
+
entries,
|
|
289
|
+
trailer: findRecoveredTrailer(bytes, sink) ?? lastXrefTrailerDict ?? (lastCatalogRef !== void 0 ? pdfDict({ Root: lastCatalogRef }) : pdfDict({}))
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
function registerObjStmContents(streamObjNum, dict, raw, entries, sink) {
|
|
293
|
+
const decoded = decodeStream(raw, dict, sink);
|
|
294
|
+
const n = asNumber(dictGet(dict, "N")) ?? 0;
|
|
295
|
+
const reader = new ByteReader(decoded.bytes);
|
|
296
|
+
let index = 0;
|
|
297
|
+
for (let i = 0; i < n; i++) {
|
|
298
|
+
const numTok = nextToken(reader);
|
|
299
|
+
const offTok = nextToken(reader);
|
|
300
|
+
if (numTok?.kind !== "number" || offTok?.kind !== "number") {
|
|
301
|
+
sink({
|
|
302
|
+
code: "pdf/xref-entry-invalid",
|
|
303
|
+
severity: "warning",
|
|
304
|
+
message: `recovered object stream ${String(streamObjNum)} header is truncated at entry ${String(i)}`
|
|
305
|
+
});
|
|
306
|
+
break;
|
|
307
|
+
}
|
|
308
|
+
if (!entries.has(numTok.value)) entries.set(numTok.value, {
|
|
309
|
+
type: "compressed",
|
|
310
|
+
streamObjNum,
|
|
311
|
+
indexInStream: index
|
|
312
|
+
});
|
|
313
|
+
index++;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
function findRecoveredTrailer(bytes, sink) {
|
|
317
|
+
const at = findLastOccurrence(bytes, TRAILER_BYTES);
|
|
318
|
+
if (at === void 0) return;
|
|
319
|
+
const reader = new ByteReader(bytes);
|
|
320
|
+
reader.seek(at + TRAILER_BYTES.length);
|
|
321
|
+
return asDict(parseValue(reader, sink));
|
|
322
|
+
}
|
|
323
|
+
//#endregion
|
|
324
|
+
export { readXref };
|