@f-o-t/pdf 0.3.9 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -0
- package/dist/{index-kjz7by1m.js → index-18zt1kda.js} +20 -3
- package/dist/{index-kjz7by1m.js.map → index-18zt1kda.js.map} +3 -3
- package/dist/index-35skpe4f.js +920 -0
- package/dist/index-35skpe4f.js.map +13 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -2
- package/dist/index.js.map +1 -1
- package/dist/plugins/editing/index.d.ts +7 -0
- package/dist/plugins/editing/index.d.ts.map +1 -1
- package/dist/plugins/editing/index.js +10 -911
- package/dist/plugins/editing/index.js.map +3 -7
- package/dist/plugins/parsing/index.js +1 -1
- package/dist/plugins/parsing/reader.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,920 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// src/plugins/editing/parser.ts
|
|
3
|
+
function findStartXref(pdfStr) {
|
|
4
|
+
const idx = pdfStr.lastIndexOf("startxref");
|
|
5
|
+
if (idx === -1)
|
|
6
|
+
throw new Error("Cannot find startxref in PDF");
|
|
7
|
+
const after = pdfStr.slice(idx + 9).trim().split(/[\r\n\s]/)[0];
|
|
8
|
+
return parseInt(after, 10);
|
|
9
|
+
}
|
|
10
|
+
function parseTrailer(pdfStr) {
|
|
11
|
+
const startxrefIdx = pdfStr.lastIndexOf("startxref");
|
|
12
|
+
const trailerIdx = pdfStr.lastIndexOf("trailer");
|
|
13
|
+
let dictStr;
|
|
14
|
+
if (trailerIdx !== -1 && trailerIdx < startxrefIdx) {
|
|
15
|
+
dictStr = pdfStr.slice(trailerIdx, startxrefIdx);
|
|
16
|
+
} else {
|
|
17
|
+
const xrefOffset = findStartXref(pdfStr);
|
|
18
|
+
const xrefObjStr = pdfStr.slice(xrefOffset, xrefOffset + 4096);
|
|
19
|
+
const dictStart = xrefObjStr.indexOf("<<");
|
|
20
|
+
if (dictStart === -1) {
|
|
21
|
+
throw new Error("Cannot find trailer or xref stream dictionary in PDF");
|
|
22
|
+
}
|
|
23
|
+
const dictEnd = findMatchingDictEnd(xrefObjStr, dictStart);
|
|
24
|
+
if (dictEnd === -1) {
|
|
25
|
+
throw new Error("Cannot find end of xref stream dictionary");
|
|
26
|
+
}
|
|
27
|
+
dictStr = xrefObjStr.slice(dictStart, dictEnd + 2);
|
|
28
|
+
}
|
|
29
|
+
const rootMatch = dictStr.match(/\/Root\s+(\d+)\s+\d+\s+R/);
|
|
30
|
+
if (!rootMatch)
|
|
31
|
+
throw new Error("Cannot find Root ref in trailer");
|
|
32
|
+
const sizeMatch = dictStr.match(/\/Size\s+(\d+)/);
|
|
33
|
+
if (!sizeMatch)
|
|
34
|
+
throw new Error("Cannot find Size in trailer");
|
|
35
|
+
const infoMatch = dictStr.match(/\/Info\s+(\d+)\s+\d+\s+R/);
|
|
36
|
+
const prevMatch = dictStr.match(/\/Prev\s+(\d+)/);
|
|
37
|
+
return {
|
|
38
|
+
root: parseInt(rootMatch[1], 10),
|
|
39
|
+
size: parseInt(sizeMatch[1], 10),
|
|
40
|
+
info: infoMatch ? parseInt(infoMatch[1], 10) : null,
|
|
41
|
+
prevXref: prevMatch ? parseInt(prevMatch[1], 10) : findStartXref(pdfStr)
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
function extractObjectDictContent(pdfStr, objNum) {
|
|
45
|
+
const objRegex = new RegExp(`(?:^|\\s)${objNum}\\s+0\\s+obj`, "m");
|
|
46
|
+
const match = pdfStr.match(objRegex);
|
|
47
|
+
if (!match || match.index === undefined) {
|
|
48
|
+
throw new Error(`Cannot find object ${objNum} in PDF`);
|
|
49
|
+
}
|
|
50
|
+
const searchStart = match.index + match[0].length;
|
|
51
|
+
const dictStart = pdfStr.indexOf("<<", searchStart);
|
|
52
|
+
if (dictStart === -1 || dictStart > searchStart + 200) {
|
|
53
|
+
throw new Error(`Cannot find dictionary start for object ${objNum}`);
|
|
54
|
+
}
|
|
55
|
+
const dictEnd = findMatchingDictEnd(pdfStr, dictStart);
|
|
56
|
+
if (dictEnd === -1) {
|
|
57
|
+
throw new Error(`Cannot find dictionary end for object ${objNum}`);
|
|
58
|
+
}
|
|
59
|
+
return pdfStr.slice(dictStart + 2, dictEnd);
|
|
60
|
+
}
|
|
61
|
+
function findPageObjects(pdfStr, rootNum) {
|
|
62
|
+
const rootContent = extractObjectDictContent(pdfStr, rootNum);
|
|
63
|
+
const pagesMatch = rootContent.match(/\/Pages\s+(\d+)\s+\d+\s+R/);
|
|
64
|
+
if (!pagesMatch)
|
|
65
|
+
throw new Error("Cannot find Pages ref in Root catalog");
|
|
66
|
+
const pagesNum = parseInt(pagesMatch[1], 10);
|
|
67
|
+
return collectPageLeafs(pdfStr, pagesNum, new Set);
|
|
68
|
+
}
|
|
69
|
+
function collectPageLeafs(pdfStr, objNum, visited) {
|
|
70
|
+
if (visited.has(objNum))
|
|
71
|
+
return [];
|
|
72
|
+
visited.add(objNum);
|
|
73
|
+
const content = extractObjectDictContent(pdfStr, objNum);
|
|
74
|
+
const typeMatch = content.match(/\/Type\s+\/(\w+)/);
|
|
75
|
+
if (typeMatch?.[1] === "Page") {
|
|
76
|
+
return [objNum];
|
|
77
|
+
}
|
|
78
|
+
const kidsMatch = content.match(/\/Kids\s*\[([^\]]+)\]/);
|
|
79
|
+
if (!kidsMatch) {
|
|
80
|
+
return [objNum];
|
|
81
|
+
}
|
|
82
|
+
const refs = [];
|
|
83
|
+
const refRegex = /(\d+)\s+\d+\s+R/g;
|
|
84
|
+
let m;
|
|
85
|
+
while ((m = refRegex.exec(kidsMatch[1])) !== null) {
|
|
86
|
+
refs.push(parseInt(m[1], 10));
|
|
87
|
+
}
|
|
88
|
+
const pages = [];
|
|
89
|
+
for (const ref of refs) {
|
|
90
|
+
pages.push(...collectPageLeafs(pdfStr, ref, visited));
|
|
91
|
+
}
|
|
92
|
+
return pages;
|
|
93
|
+
}
|
|
94
|
+
function getMediaBox(pdfStr, pageObjNum) {
|
|
95
|
+
const visited = new Set;
|
|
96
|
+
let objNum = pageObjNum;
|
|
97
|
+
while (objNum !== null && !visited.has(objNum)) {
|
|
98
|
+
visited.add(objNum);
|
|
99
|
+
const content = extractObjectDictContent(pdfStr, objNum);
|
|
100
|
+
const mediaBoxMatch = content.match(/\/MediaBox\s*\[\s*([\d.]+)\s+([\d.]+)\s+([\d.]+)\s+([\d.]+)\s*\]/);
|
|
101
|
+
if (mediaBoxMatch) {
|
|
102
|
+
return [
|
|
103
|
+
parseFloat(mediaBoxMatch[1]),
|
|
104
|
+
parseFloat(mediaBoxMatch[2]),
|
|
105
|
+
parseFloat(mediaBoxMatch[3]),
|
|
106
|
+
parseFloat(mediaBoxMatch[4])
|
|
107
|
+
];
|
|
108
|
+
}
|
|
109
|
+
const parentMatch = content.match(/\/Parent\s+(\d+)\s+\d+\s+R/);
|
|
110
|
+
objNum = parentMatch ? parseInt(parentMatch[1], 10) : null;
|
|
111
|
+
}
|
|
112
|
+
throw new Error(`Cannot find MediaBox for page object ${pageObjNum}`);
|
|
113
|
+
}
|
|
114
|
+
function parsePdfStructure(pdfStr) {
|
|
115
|
+
const xrefOffset = findStartXref(pdfStr);
|
|
116
|
+
const trailer = parseTrailer(pdfStr);
|
|
117
|
+
const rootContent = extractObjectDictContent(pdfStr, trailer.root);
|
|
118
|
+
const pagesMatch = rootContent.match(/\/Pages\s+(\d+)\s+\d+\s+R/);
|
|
119
|
+
if (!pagesMatch)
|
|
120
|
+
throw new Error("Cannot find Pages ref in Root catalog");
|
|
121
|
+
const pagesNum = parseInt(pagesMatch[1], 10);
|
|
122
|
+
const pageNums = findPageObjects(pdfStr, trailer.root);
|
|
123
|
+
const pageDictContents = pageNums.map((pn) => extractObjectDictContent(pdfStr, pn));
|
|
124
|
+
return {
|
|
125
|
+
xrefOffset,
|
|
126
|
+
rootNum: trailer.root,
|
|
127
|
+
infoNum: trailer.info,
|
|
128
|
+
size: trailer.size,
|
|
129
|
+
pagesNum,
|
|
130
|
+
pageNums,
|
|
131
|
+
rootDictContent: rootContent,
|
|
132
|
+
pageDictContents
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
function findMatchingDictEnd(str, startPos) {
|
|
136
|
+
let depth = 0;
|
|
137
|
+
let i = startPos;
|
|
138
|
+
while (i < str.length - 1) {
|
|
139
|
+
if (str[i] === "(") {
|
|
140
|
+
i++;
|
|
141
|
+
while (i < str.length && str[i] !== ")") {
|
|
142
|
+
if (str[i] === "\\")
|
|
143
|
+
i++;
|
|
144
|
+
i++;
|
|
145
|
+
}
|
|
146
|
+
i++;
|
|
147
|
+
} else if (str[i] === "<" && str[i + 1] === "<") {
|
|
148
|
+
depth++;
|
|
149
|
+
i += 2;
|
|
150
|
+
} else if (str[i] === ">" && str[i + 1] === ">") {
|
|
151
|
+
depth--;
|
|
152
|
+
if (depth === 0)
|
|
153
|
+
return i;
|
|
154
|
+
i += 2;
|
|
155
|
+
} else {
|
|
156
|
+
i++;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return -1;
|
|
160
|
+
}
|
|
161
|
+
function findMatchingArrayEnd(str, startPos) {
|
|
162
|
+
let depth = 0;
|
|
163
|
+
let i = startPos;
|
|
164
|
+
while (i < str.length) {
|
|
165
|
+
if (str[i] === "(") {
|
|
166
|
+
i++;
|
|
167
|
+
while (i < str.length && str[i] !== ")") {
|
|
168
|
+
if (str[i] === "\\")
|
|
169
|
+
i++;
|
|
170
|
+
i++;
|
|
171
|
+
}
|
|
172
|
+
i++;
|
|
173
|
+
} else if (str[i] === "[") {
|
|
174
|
+
depth++;
|
|
175
|
+
i++;
|
|
176
|
+
} else if (str[i] === "]") {
|
|
177
|
+
depth--;
|
|
178
|
+
if (depth === 0)
|
|
179
|
+
return i;
|
|
180
|
+
i++;
|
|
181
|
+
} else {
|
|
182
|
+
i++;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return -1;
|
|
186
|
+
}
|
|
187
|
+
function parseResourcesDict(pageContent, pdfStr) {
|
|
188
|
+
const result = {};
|
|
189
|
+
const inlineMatch = pageContent.match(/\/Resources\s*<</);
|
|
190
|
+
if (inlineMatch) {
|
|
191
|
+
const resIdx = pageContent.indexOf("/Resources");
|
|
192
|
+
const resStart = pageContent.indexOf("<<", resIdx);
|
|
193
|
+
const resEnd = findMatchingDictEnd(pageContent, resStart);
|
|
194
|
+
if (resEnd === -1) {
|
|
195
|
+
throw new Error("Cannot find end of Resources dictionary");
|
|
196
|
+
}
|
|
197
|
+
const resContent = pageContent.slice(resStart + 2, resEnd);
|
|
198
|
+
return parseResourceEntries(resContent);
|
|
199
|
+
}
|
|
200
|
+
const refMatch = pageContent.match(/\/Resources\s+(\d+)\s+\d+\s+R/);
|
|
201
|
+
if (refMatch) {
|
|
202
|
+
const objNum = parseInt(refMatch[1], 10);
|
|
203
|
+
const objContent = extractObjectDictContent(pdfStr, objNum);
|
|
204
|
+
return parseResourceEntries(objContent);
|
|
205
|
+
}
|
|
206
|
+
return result;
|
|
207
|
+
}
|
|
208
|
+
function mergeResourcesDicts(existing, additions) {
|
|
209
|
+
const result = { ...existing };
|
|
210
|
+
for (const [resType, addValue] of Object.entries(additions)) {
|
|
211
|
+
if (!result[resType]) {
|
|
212
|
+
result[resType] = addValue;
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
const existingValue = result[resType];
|
|
216
|
+
if (existingValue.startsWith("[")) {
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
if (existingValue.startsWith("<<")) {
|
|
220
|
+
result[resType] = mergeDictEntries(existingValue, addValue);
|
|
221
|
+
} else {
|
|
222
|
+
throw new Error(`Unexpected resource format for ${resType}: ${existingValue}`);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
return result;
|
|
226
|
+
}
|
|
227
|
+
function mergeDictEntries(existing, additions) {
|
|
228
|
+
const existingEntries = extractDictEntries(existing);
|
|
229
|
+
const additionEntries = extractDictEntries(additions);
|
|
230
|
+
const merged = { ...existingEntries, ...additionEntries };
|
|
231
|
+
const entries = Object.entries(merged).map(([name, ref]) => `${name} ${ref}`).join(" ");
|
|
232
|
+
return `<< ${entries} >>`;
|
|
233
|
+
}
|
|
234
|
+
function extractDictEntries(dict) {
|
|
235
|
+
const entries = {};
|
|
236
|
+
const inner = dict.replace(/^<<\s*/, "").replace(/\s*>>$/, "");
|
|
237
|
+
const regex = /(\/[^\s<>\[\]()\/]+)\s+(\d+\s+\d+\s+R)/g;
|
|
238
|
+
let match;
|
|
239
|
+
while ((match = regex.exec(inner)) !== null) {
|
|
240
|
+
entries[match[1]] = match[2];
|
|
241
|
+
}
|
|
242
|
+
return entries;
|
|
243
|
+
}
|
|
244
|
+
function parseResourceEntries(content) {
|
|
245
|
+
const result = {};
|
|
246
|
+
const resourceTypes = [
|
|
247
|
+
"/Font",
|
|
248
|
+
"/XObject",
|
|
249
|
+
"/ExtGState",
|
|
250
|
+
"/ColorSpace",
|
|
251
|
+
"/Pattern",
|
|
252
|
+
"/Shading",
|
|
253
|
+
"/ProcSet"
|
|
254
|
+
];
|
|
255
|
+
for (const resType of resourceTypes) {
|
|
256
|
+
const pattern = new RegExp(`${resType.replace(/\//g, "\\/")}\\s+([<\\[])`);
|
|
257
|
+
const match = content.match(pattern);
|
|
258
|
+
if (!match)
|
|
259
|
+
continue;
|
|
260
|
+
const idx = match.index;
|
|
261
|
+
let valueStart = idx + resType.length;
|
|
262
|
+
while (valueStart < content.length && /\s/.test(content[valueStart])) {
|
|
263
|
+
valueStart++;
|
|
264
|
+
}
|
|
265
|
+
if (content[valueStart] === "<" && content[valueStart + 1] === "<") {
|
|
266
|
+
const dictEnd = findMatchingDictEnd(content, valueStart);
|
|
267
|
+
if (dictEnd === -1) {
|
|
268
|
+
throw new Error(`Cannot find end of ${resType} dictionary`);
|
|
269
|
+
}
|
|
270
|
+
result[resType] = content.slice(valueStart, dictEnd + 2);
|
|
271
|
+
} else if (content[valueStart] === "[") {
|
|
272
|
+
const arrayEnd = findMatchingArrayEnd(content, valueStart);
|
|
273
|
+
if (arrayEnd === -1) {
|
|
274
|
+
throw new Error(`Cannot find end of ${resType} array`);
|
|
275
|
+
}
|
|
276
|
+
result[resType] = content.slice(valueStart, arrayEnd + 1);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
return result;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// src/plugins/editing/page.ts
|
|
283
|
+
function parseHexColor(hex) {
|
|
284
|
+
if (!hex)
|
|
285
|
+
return null;
|
|
286
|
+
const m = hex.match(/^#?([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})$/);
|
|
287
|
+
if (!m)
|
|
288
|
+
return null;
|
|
289
|
+
return [
|
|
290
|
+
parseInt(m[1], 16) / 255,
|
|
291
|
+
parseInt(m[2], 16) / 255,
|
|
292
|
+
parseInt(m[3], 16) / 255
|
|
293
|
+
];
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
class PdfPageImpl {
|
|
297
|
+
width;
|
|
298
|
+
height;
|
|
299
|
+
pageObjNum;
|
|
300
|
+
originalDictContent;
|
|
301
|
+
operators = [];
|
|
302
|
+
imageRefs = new Map;
|
|
303
|
+
fontObjNum = 0;
|
|
304
|
+
get dirty() {
|
|
305
|
+
return this.operators.length > 0;
|
|
306
|
+
}
|
|
307
|
+
constructor(pageObjNum, width, height, originalDictContent) {
|
|
308
|
+
this.pageObjNum = pageObjNum;
|
|
309
|
+
this.width = width;
|
|
310
|
+
this.height = height;
|
|
311
|
+
this.originalDictContent = originalDictContent;
|
|
312
|
+
}
|
|
313
|
+
drawText(text, options) {
|
|
314
|
+
const { x, y, size = 12, color } = options;
|
|
315
|
+
const rgb = parseHexColor(color);
|
|
316
|
+
if (rgb) {
|
|
317
|
+
this.operators.push(`${rgb[0].toFixed(3)} ${rgb[1].toFixed(3)} ${rgb[2].toFixed(3)} rg`);
|
|
318
|
+
}
|
|
319
|
+
const escaped = text.replace(/\\/g, "\\\\").replace(/\(/g, "\\(").replace(/\)/g, "\\)");
|
|
320
|
+
this.operators.push("BT");
|
|
321
|
+
this.operators.push(`/F1 ${size} Tf`);
|
|
322
|
+
this.operators.push(`${x} ${y} Td`);
|
|
323
|
+
this.operators.push(`(${escaped}) Tj`);
|
|
324
|
+
this.operators.push("ET");
|
|
325
|
+
}
|
|
326
|
+
drawRectangle(options) {
|
|
327
|
+
const { x, y, width, height, color, borderColor, borderWidth } = options;
|
|
328
|
+
const fillRgb = parseHexColor(color);
|
|
329
|
+
const strokeRgb = parseHexColor(borderColor);
|
|
330
|
+
if (fillRgb) {
|
|
331
|
+
this.operators.push(`${fillRgb[0].toFixed(3)} ${fillRgb[1].toFixed(3)} ${fillRgb[2].toFixed(3)} rg`);
|
|
332
|
+
}
|
|
333
|
+
if (strokeRgb) {
|
|
334
|
+
this.operators.push(`${strokeRgb[0].toFixed(3)} ${strokeRgb[1].toFixed(3)} ${strokeRgb[2].toFixed(3)} RG`);
|
|
335
|
+
}
|
|
336
|
+
if (borderWidth !== undefined) {
|
|
337
|
+
this.operators.push(`${borderWidth} w`);
|
|
338
|
+
}
|
|
339
|
+
this.operators.push(`${x} ${y} ${width} ${height} re`);
|
|
340
|
+
if (fillRgb && strokeRgb) {
|
|
341
|
+
this.operators.push("B");
|
|
342
|
+
} else if (fillRgb) {
|
|
343
|
+
this.operators.push("f");
|
|
344
|
+
} else if (strokeRgb) {
|
|
345
|
+
this.operators.push("S");
|
|
346
|
+
} else {
|
|
347
|
+
this.operators.push("f");
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
drawImage(image, options) {
|
|
351
|
+
const { x, y, width, height } = options;
|
|
352
|
+
const imgName = `Im${image.objectNumber}`;
|
|
353
|
+
this.imageRefs.set(imgName, image.objectNumber);
|
|
354
|
+
this.operators.push("q");
|
|
355
|
+
this.operators.push(`${width} 0 0 ${height} ${x} ${y} cm`);
|
|
356
|
+
this.operators.push(`/${imgName} Do`);
|
|
357
|
+
this.operators.push("Q");
|
|
358
|
+
}
|
|
359
|
+
buildContentStream() {
|
|
360
|
+
const content = this.operators.join(`
|
|
361
|
+
`);
|
|
362
|
+
return new TextEncoder().encode(content);
|
|
363
|
+
}
|
|
364
|
+
getImageRefs() {
|
|
365
|
+
return new Map(this.imageRefs);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// src/plugins/editing/document.ts
|
|
370
|
+
var BYTE_RANGE_PLACEHOLDER = "0 0000000000 0000000000 0000000000";
|
|
371
|
+
var DEFAULT_SIGNATURE_LENGTH = 16384;
|
|
372
|
+
var latin1Encoder = new TextEncoder;
|
|
373
|
+
var latin1Decoder = new TextDecoder("latin1");
|
|
374
|
+
var CONTENTS_MARKER = latin1Encoder.encode("/Contents <");
|
|
375
|
+
var BYTE_RANGE_MARKER = latin1Encoder.encode("/ByteRange [");
|
|
376
|
+
function findLastBytes(data, pattern) {
|
|
377
|
+
outer:
|
|
378
|
+
for (let i = data.length - pattern.length;i >= 0; i--) {
|
|
379
|
+
for (let j = 0;j < pattern.length; j++) {
|
|
380
|
+
if (data[i + j] !== pattern[j])
|
|
381
|
+
continue outer;
|
|
382
|
+
}
|
|
383
|
+
return i;
|
|
384
|
+
}
|
|
385
|
+
return -1;
|
|
386
|
+
}
|
|
387
|
+
function parsePngIhdr(data) {
|
|
388
|
+
const sig = [137, 80, 78, 71, 13, 10, 26, 10];
|
|
389
|
+
for (let i = 0;i < sig.length; i++) {
|
|
390
|
+
if (data[i] !== sig[i])
|
|
391
|
+
throw new Error("Not a valid PNG file");
|
|
392
|
+
}
|
|
393
|
+
const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
|
|
394
|
+
const chunkType = latin1Decoder.decode(data.slice(12, 16));
|
|
395
|
+
if (chunkType !== "IHDR")
|
|
396
|
+
throw new Error("First PNG chunk is not IHDR");
|
|
397
|
+
return {
|
|
398
|
+
width: view.getUint32(16),
|
|
399
|
+
height: view.getUint32(20),
|
|
400
|
+
bitDepth: data[24],
|
|
401
|
+
colorType: data[25]
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
function extractIdatData(data) {
|
|
405
|
+
const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
|
|
406
|
+
const chunks = [];
|
|
407
|
+
let offset = 8;
|
|
408
|
+
while (offset < data.length) {
|
|
409
|
+
const chunkLen = view.getUint32(offset);
|
|
410
|
+
const chunkType = latin1Decoder.decode(data.slice(offset + 4, offset + 8));
|
|
411
|
+
if (chunkType === "IDAT") {
|
|
412
|
+
chunks.push(data.slice(offset + 8, offset + 8 + chunkLen));
|
|
413
|
+
}
|
|
414
|
+
offset += 12 + chunkLen;
|
|
415
|
+
}
|
|
416
|
+
if (chunks.length === 0)
|
|
417
|
+
throw new Error("No IDAT chunks found in PNG");
|
|
418
|
+
const totalLen = chunks.reduce((s, c) => s + c.length, 0);
|
|
419
|
+
const result = new Uint8Array(totalLen);
|
|
420
|
+
let pos = 0;
|
|
421
|
+
for (const chunk of chunks) {
|
|
422
|
+
result.set(chunk, pos);
|
|
423
|
+
pos += chunk.length;
|
|
424
|
+
}
|
|
425
|
+
return result;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
class PdfDocumentImpl {
|
|
429
|
+
originalData;
|
|
430
|
+
pdfStr;
|
|
431
|
+
structure;
|
|
432
|
+
pages = [];
|
|
433
|
+
nextObjNum;
|
|
434
|
+
fontObjNum;
|
|
435
|
+
embeddedImages = [];
|
|
436
|
+
constructor(data) {
|
|
437
|
+
this.originalData = data;
|
|
438
|
+
this.pdfStr = latin1Decoder.decode(data);
|
|
439
|
+
this.structure = parsePdfStructure(this.pdfStr);
|
|
440
|
+
this.nextObjNum = this.structure.size;
|
|
441
|
+
this.fontObjNum = this.nextObjNum++;
|
|
442
|
+
for (let i = 0;i < this.structure.pageNums.length; i++) {
|
|
443
|
+
const pageNum = this.structure.pageNums[i];
|
|
444
|
+
const mediaBox = getMediaBox(this.pdfStr, pageNum);
|
|
445
|
+
const width = mediaBox[2] - mediaBox[0];
|
|
446
|
+
const height = mediaBox[3] - mediaBox[1];
|
|
447
|
+
const dictContent = this.structure.pageDictContents[i];
|
|
448
|
+
const page = new PdfPageImpl(pageNum, width, height, dictContent);
|
|
449
|
+
page.fontObjNum = this.fontObjNum;
|
|
450
|
+
this.pages.push(page);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
get pageCount() {
|
|
454
|
+
return this.pages.length;
|
|
455
|
+
}
|
|
456
|
+
getPage(index) {
|
|
457
|
+
if (index < 0 || index >= this.pages.length) {
|
|
458
|
+
throw new Error(`Page index ${index} out of range (0-${this.pages.length - 1})`);
|
|
459
|
+
}
|
|
460
|
+
return this.pages[index];
|
|
461
|
+
}
|
|
462
|
+
embedPng(data) {
|
|
463
|
+
const ihdr = parsePngIhdr(data);
|
|
464
|
+
const idatData = extractIdatData(data);
|
|
465
|
+
const objNum = this.nextObjNum++;
|
|
466
|
+
this.embeddedImages.push({
|
|
467
|
+
objNum,
|
|
468
|
+
width: ihdr.width,
|
|
469
|
+
height: ihdr.height,
|
|
470
|
+
idatData,
|
|
471
|
+
colorType: ihdr.colorType,
|
|
472
|
+
bitDepth: ihdr.bitDepth
|
|
473
|
+
});
|
|
474
|
+
return {
|
|
475
|
+
objectNumber: objNum,
|
|
476
|
+
width: ihdr.width,
|
|
477
|
+
height: ihdr.height
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
save() {
|
|
481
|
+
return this.buildIncrementalUpdate(false).pdf;
|
|
482
|
+
}
|
|
483
|
+
saveWithPlaceholder(options) {
|
|
484
|
+
return this.buildIncrementalUpdate(true, options);
|
|
485
|
+
}
|
|
486
|
+
buildIncrementalUpdate(withSignature, sigOptions) {
|
|
487
|
+
const objects = [];
|
|
488
|
+
let currentNextObj = this.nextObjNum;
|
|
489
|
+
const anyDirty = this.pages.some((p) => p.dirty);
|
|
490
|
+
if (anyDirty || this.embeddedImages.length > 0) {
|
|
491
|
+
objects.push({
|
|
492
|
+
objNum: this.fontObjNum,
|
|
493
|
+
content: "<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>"
|
|
494
|
+
});
|
|
495
|
+
}
|
|
496
|
+
for (const img of this.embeddedImages) {
|
|
497
|
+
const colorSpace = img.colorType === 2 ? "/DeviceRGB" : img.colorType === 0 ? "/DeviceGray" : "/DeviceRGB";
|
|
498
|
+
const colors = img.colorType === 2 ? 3 : 1;
|
|
499
|
+
const bpc = img.bitDepth;
|
|
500
|
+
objects.push({
|
|
501
|
+
objNum: img.objNum,
|
|
502
|
+
content: [
|
|
503
|
+
"<< /Type /XObject",
|
|
504
|
+
"/Subtype /Image",
|
|
505
|
+
`/Width ${img.width}`,
|
|
506
|
+
`/Height ${img.height}`,
|
|
507
|
+
`/ColorSpace ${colorSpace}`,
|
|
508
|
+
`/BitsPerComponent ${bpc}`,
|
|
509
|
+
"/Filter /FlateDecode",
|
|
510
|
+
`/DecodeParms << /Predictor 15 /Colors ${colors} /BitsPerComponent ${bpc} /Columns ${img.width} >>`,
|
|
511
|
+
`/Length ${img.idatData.length}`,
|
|
512
|
+
">>"
|
|
513
|
+
].join(`
|
|
514
|
+
`),
|
|
515
|
+
streamData: img.idatData
|
|
516
|
+
});
|
|
517
|
+
}
|
|
518
|
+
const wrapperStreamMap = new Map;
|
|
519
|
+
const contentStreamMap = new Map;
|
|
520
|
+
for (const page of this.pages) {
|
|
521
|
+
if (!page.dirty)
|
|
522
|
+
continue;
|
|
523
|
+
const wrapperObjNum = currentNextObj++;
|
|
524
|
+
wrapperStreamMap.set(page.pageObjNum, wrapperObjNum);
|
|
525
|
+
const wrapperData = latin1Encoder.encode("q");
|
|
526
|
+
objects.push({
|
|
527
|
+
objNum: wrapperObjNum,
|
|
528
|
+
content: `<< /Length ${wrapperData.length} >>`,
|
|
529
|
+
streamData: wrapperData
|
|
530
|
+
});
|
|
531
|
+
const contentObjNum = currentNextObj++;
|
|
532
|
+
contentStreamMap.set(page.pageObjNum, contentObjNum);
|
|
533
|
+
const pageStreamData = page.buildContentStream();
|
|
534
|
+
const prefixedData = new Uint8Array(2 + pageStreamData.length);
|
|
535
|
+
prefixedData[0] = 81;
|
|
536
|
+
prefixedData[1] = 10;
|
|
537
|
+
prefixedData.set(pageStreamData, 2);
|
|
538
|
+
objects.push({
|
|
539
|
+
objNum: contentObjNum,
|
|
540
|
+
content: `<< /Length ${prefixedData.length} >>`,
|
|
541
|
+
streamData: prefixedData
|
|
542
|
+
});
|
|
543
|
+
}
|
|
544
|
+
for (const page of this.pages) {
|
|
545
|
+
if (!page.dirty && !this.hasImagesForPage(page))
|
|
546
|
+
continue;
|
|
547
|
+
let pageContent = page.originalDictContent;
|
|
548
|
+
if (page.dirty) {
|
|
549
|
+
const contentObjNum = contentStreamMap.get(page.pageObjNum);
|
|
550
|
+
const wrapperObjNum = wrapperStreamMap.get(page.pageObjNum);
|
|
551
|
+
if (pageContent.match(/\/Contents\s/)) {
|
|
552
|
+
pageContent = pageContent.replace(/\/Contents\s+(\d+\s+\d+\s+R)/, `/Contents [${wrapperObjNum} 0 R $1 ${contentObjNum} 0 R]`);
|
|
553
|
+
pageContent = pageContent.replace(/\/Contents\s*\[([^\]]+)\]/, (match, inner) => {
|
|
554
|
+
if (inner.includes(`${contentObjNum} 0 R`))
|
|
555
|
+
return match;
|
|
556
|
+
return `/Contents [${wrapperObjNum} 0 R ${inner.trim()} ${contentObjNum} 0 R]`;
|
|
557
|
+
});
|
|
558
|
+
} else {
|
|
559
|
+
pageContent += `
|
|
560
|
+
/Contents ${contentObjNum} 0 R`;
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
const newResourceParts = [];
|
|
564
|
+
newResourceParts.push(`/Font << /F1 ${this.fontObjNum} 0 R >>`);
|
|
565
|
+
const imageRefs = page.dirty ? page.getImageRefs() : new Map;
|
|
566
|
+
if (imageRefs.size > 0) {
|
|
567
|
+
const xobjEntries = Array.from(imageRefs.entries()).map(([name, objNum]) => `/${name} ${objNum} 0 R`).join(" ");
|
|
568
|
+
newResourceParts.push(`/XObject << ${xobjEntries} >>`);
|
|
569
|
+
}
|
|
570
|
+
const newResources = {};
|
|
571
|
+
for (const part of newResourceParts) {
|
|
572
|
+
const [resType, ...rest] = part.split(/\s+/);
|
|
573
|
+
if (resType) {
|
|
574
|
+
newResources[resType] = rest.join(" ");
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
const existingResources = parseResourcesDict(pageContent, this.pdfStr);
|
|
578
|
+
const mergedResources = mergeResourcesDicts(existingResources, newResources);
|
|
579
|
+
const resourceEntries = Object.entries(mergedResources).map(([name, value]) => `${name} ${value}`).join(`
|
|
580
|
+
`);
|
|
581
|
+
if (pageContent.match(/\/Resources\s*<</)) {
|
|
582
|
+
const resIdx = pageContent.indexOf("/Resources");
|
|
583
|
+
const resStart = pageContent.indexOf("<<", resIdx);
|
|
584
|
+
if (resStart !== -1) {
|
|
585
|
+
const resEnd = findMatchingDictEndInContent(pageContent, resStart);
|
|
586
|
+
if (resEnd !== -1) {
|
|
587
|
+
pageContent = pageContent.slice(0, resStart) + `<< ${resourceEntries} >>` + pageContent.slice(resEnd + 2);
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
} else if (pageContent.match(/\/Resources\s+\d+\s+\d+\s+R/)) {
|
|
591
|
+
pageContent = pageContent.replace(/\/Resources\s+\d+\s+\d+\s+R/, `/Resources << ${resourceEntries} >>`);
|
|
592
|
+
} else {
|
|
593
|
+
pageContent += `
|
|
594
|
+
/Resources << ${resourceEntries} >>`;
|
|
595
|
+
}
|
|
596
|
+
objects.push({
|
|
597
|
+
objNum: page.pageObjNum,
|
|
598
|
+
content: `<<${pageContent}
|
|
599
|
+
>>`
|
|
600
|
+
});
|
|
601
|
+
}
|
|
602
|
+
let sigObjNum = 0;
|
|
603
|
+
let widgetObjNum = 0;
|
|
604
|
+
let acroFormObjNum = 0;
|
|
605
|
+
if (withSignature && sigOptions) {
|
|
606
|
+
sigObjNum = currentNextObj++;
|
|
607
|
+
widgetObjNum = currentNextObj++;
|
|
608
|
+
acroFormObjNum = currentNextObj++;
|
|
609
|
+
const signatureLength = sigOptions.signatureLength ?? DEFAULT_SIGNATURE_LENGTH;
|
|
610
|
+
const reason = sigOptions.reason ?? "Digitally signed";
|
|
611
|
+
const name = sigOptions.name ?? "Digital Signature";
|
|
612
|
+
const location = sigOptions.location ?? "";
|
|
613
|
+
const contactInfo = sigOptions.contactInfo ?? "";
|
|
614
|
+
const signingTime = formatPdfDate(new Date);
|
|
615
|
+
const contentsPlaceholder = "0".repeat(signatureLength * 2);
|
|
616
|
+
const sigParts = [
|
|
617
|
+
"<< /Type /Sig",
|
|
618
|
+
"/Filter /Adobe.PPKLite",
|
|
619
|
+
"/SubFilter /adbe.pkcs7.detached",
|
|
620
|
+
`/ByteRange [${BYTE_RANGE_PLACEHOLDER}]`,
|
|
621
|
+
`/Contents <${contentsPlaceholder}>`,
|
|
622
|
+
`/Reason ${pdfString(reason)}`,
|
|
623
|
+
`/M ${pdfString(signingTime)}`,
|
|
624
|
+
`/Name ${pdfString(name)}`
|
|
625
|
+
];
|
|
626
|
+
if (location)
|
|
627
|
+
sigParts.push(`/Location ${pdfString(location)}`);
|
|
628
|
+
if (contactInfo)
|
|
629
|
+
sigParts.push(`/ContactInfo ${pdfString(contactInfo)}`);
|
|
630
|
+
sigParts.push(">>");
|
|
631
|
+
objects.push({ objNum: sigObjNum, content: sigParts.join(`
|
|
632
|
+
`) });
|
|
633
|
+
const sigPageIdx = Math.min(Math.max(sigOptions.appearancePage ?? 0, 0), this.pages.length - 1);
|
|
634
|
+
const sigPageNum = this.structure.pageNums[sigPageIdx];
|
|
635
|
+
objects.push({
|
|
636
|
+
objNum: widgetObjNum,
|
|
637
|
+
content: [
|
|
638
|
+
"<< /Type /Annot",
|
|
639
|
+
"/Subtype /Widget",
|
|
640
|
+
"/FT /Sig",
|
|
641
|
+
"/Rect [0 0 0 0]",
|
|
642
|
+
`/V ${sigObjNum} 0 R`,
|
|
643
|
+
`/T ${pdfString("Signature1")}`,
|
|
644
|
+
"/F 4",
|
|
645
|
+
`/P ${sigPageNum} 0 R`,
|
|
646
|
+
">>"
|
|
647
|
+
].join(`
|
|
648
|
+
`)
|
|
649
|
+
});
|
|
650
|
+
objects.push({
|
|
651
|
+
objNum: acroFormObjNum,
|
|
652
|
+
content: [
|
|
653
|
+
"<< /Type /AcroForm",
|
|
654
|
+
"/SigFlags 3",
|
|
655
|
+
`/Fields [${widgetObjNum} 0 R]`,
|
|
656
|
+
">>"
|
|
657
|
+
].join(`
|
|
658
|
+
`)
|
|
659
|
+
});
|
|
660
|
+
let rootContent = this.structure.rootDictContent;
|
|
661
|
+
rootContent = rootContent.replace(/\/AcroForm\s+\d+\s+\d+\s+R/g, "");
|
|
662
|
+
rootContent = rootContent.replace(/\/Perms\s*<<[^>]*>>/g, "");
|
|
663
|
+
rootContent = rootContent.replace(/\/Perms\s+\d+\s+\d+\s+R/g, "");
|
|
664
|
+
objects.push({
|
|
665
|
+
objNum: this.structure.rootNum,
|
|
666
|
+
content: `<<${rootContent}
|
|
667
|
+
/AcroForm ${acroFormObjNum} 0 R
|
|
668
|
+
>>`
|
|
669
|
+
});
|
|
670
|
+
const sigPage = this.pages[sigPageIdx];
|
|
671
|
+
let pageContent;
|
|
672
|
+
const existingPageObj = objects.find((o) => o.objNum === sigPage.pageObjNum);
|
|
673
|
+
if (existingPageObj) {
|
|
674
|
+
pageContent = existingPageObj.content.slice(2, existingPageObj.content.length - 2);
|
|
675
|
+
} else {
|
|
676
|
+
pageContent = sigPage.originalDictContent;
|
|
677
|
+
}
|
|
678
|
+
if (pageContent.includes("/Annots")) {
|
|
679
|
+
const bracketEnd = pageContent.indexOf("]", pageContent.indexOf("/Annots"));
|
|
680
|
+
pageContent = pageContent.slice(0, bracketEnd) + ` ${widgetObjNum} 0 R` + pageContent.slice(bracketEnd);
|
|
681
|
+
} else {
|
|
682
|
+
pageContent += `
|
|
683
|
+
/Annots [${widgetObjNum} 0 R]`;
|
|
684
|
+
}
|
|
685
|
+
if (existingPageObj) {
|
|
686
|
+
existingPageObj.content = `<<${pageContent}
|
|
687
|
+
>>`;
|
|
688
|
+
} else {
|
|
689
|
+
objects.push({
|
|
690
|
+
objNum: sigPage.pageObjNum,
|
|
691
|
+
content: `<<${pageContent}
|
|
692
|
+
>>`
|
|
693
|
+
});
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
const newSize = currentNextObj;
|
|
697
|
+
const appendParts = [];
|
|
698
|
+
const objectOffsets = [];
|
|
699
|
+
let currentOffset = this.originalData.length;
|
|
700
|
+
for (const obj of objects) {
|
|
701
|
+
const sep = latin1Encoder.encode(`
|
|
702
|
+
`);
|
|
703
|
+
appendParts.push(sep);
|
|
704
|
+
currentOffset += sep.length;
|
|
705
|
+
objectOffsets.push({ objNum: obj.objNum, offset: currentOffset });
|
|
706
|
+
if (obj.streamData) {
|
|
707
|
+
const header = latin1Encoder.encode(`${obj.objNum} 0 obj
|
|
708
|
+
${obj.content}
|
|
709
|
+
stream
|
|
710
|
+
`);
|
|
711
|
+
appendParts.push(header);
|
|
712
|
+
currentOffset += header.length;
|
|
713
|
+
appendParts.push(obj.streamData);
|
|
714
|
+
currentOffset += obj.streamData.length;
|
|
715
|
+
const footer = latin1Encoder.encode(`
|
|
716
|
+
endstream
|
|
717
|
+
endobj
|
|
718
|
+
`);
|
|
719
|
+
appendParts.push(footer);
|
|
720
|
+
currentOffset += footer.length;
|
|
721
|
+
} else {
|
|
722
|
+
const objBytes = latin1Encoder.encode(`${obj.objNum} 0 obj
|
|
723
|
+
${obj.content}
|
|
724
|
+
endobj
|
|
725
|
+
`);
|
|
726
|
+
appendParts.push(objBytes);
|
|
727
|
+
currentOffset += objBytes.length;
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
const xrefOffset = currentOffset;
|
|
731
|
+
const xrefStr = buildXrefTable(objectOffsets);
|
|
732
|
+
const xrefBytes = latin1Encoder.encode(xrefStr);
|
|
733
|
+
appendParts.push(xrefBytes);
|
|
734
|
+
currentOffset += xrefBytes.length;
|
|
735
|
+
const trailerLines = ["<<", `/Size ${newSize}`, `/Root ${this.structure.rootNum} 0 R`];
|
|
736
|
+
if (this.structure.infoNum !== null) {
|
|
737
|
+
trailerLines.push(`/Info ${this.structure.infoNum} 0 R`);
|
|
738
|
+
}
|
|
739
|
+
trailerLines.push(`/Prev ${this.structure.xrefOffset}`, ">>");
|
|
740
|
+
const trailerStr = `trailer
|
|
741
|
+
${trailerLines.join(`
|
|
742
|
+
`)}
|
|
743
|
+
startxref
|
|
744
|
+
${xrefOffset}
|
|
745
|
+
%%EOF`;
|
|
746
|
+
const trailerBytes = latin1Encoder.encode(trailerStr);
|
|
747
|
+
appendParts.push(trailerBytes);
|
|
748
|
+
const totalAppendLength = appendParts.reduce((s, p) => s + p.length, 0);
|
|
749
|
+
const result = new Uint8Array(this.originalData.length + totalAppendLength);
|
|
750
|
+
result.set(this.originalData, 0);
|
|
751
|
+
let pos = this.originalData.length;
|
|
752
|
+
for (const part of appendParts) {
|
|
753
|
+
result.set(part, pos);
|
|
754
|
+
pos += part.length;
|
|
755
|
+
}
|
|
756
|
+
let byteRange = [0, 0, 0, 0];
|
|
757
|
+
if (withSignature) {
|
|
758
|
+
const br = updateByteRangeInPlace(result);
|
|
759
|
+
return { pdf: result, byteRange: br };
|
|
760
|
+
}
|
|
761
|
+
return { pdf: result, byteRange };
|
|
762
|
+
}
|
|
763
|
+
hasImagesForPage(page) {
|
|
764
|
+
return page.dirty && page.getImageRefs().size > 0;
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
function buildXrefTable(entries) {
|
|
768
|
+
const sorted = [...entries].sort((a, b) => a.objNum - b.objNum);
|
|
769
|
+
const subsections = [];
|
|
770
|
+
for (const entry of sorted) {
|
|
771
|
+
const last = subsections[subsections.length - 1];
|
|
772
|
+
if (last && entry.objNum === last.start + last.offsets.length) {
|
|
773
|
+
last.offsets.push(entry.offset);
|
|
774
|
+
} else {
|
|
775
|
+
subsections.push({ start: entry.objNum, offsets: [entry.offset] });
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
let result = `xref
|
|
779
|
+
`;
|
|
780
|
+
for (const sub of subsections) {
|
|
781
|
+
result += `${sub.start} ${sub.offsets.length}
|
|
782
|
+
`;
|
|
783
|
+
for (const offset of sub.offsets) {
|
|
784
|
+
result += `${String(offset).padStart(10, "0")} 00000 n
|
|
785
|
+
`;
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
return result;
|
|
789
|
+
}
|
|
790
|
+
function formatPdfDate(date) {
|
|
791
|
+
const y = date.getUTCFullYear();
|
|
792
|
+
const m = String(date.getUTCMonth() + 1).padStart(2, "0");
|
|
793
|
+
const d = String(date.getUTCDate()).padStart(2, "0");
|
|
794
|
+
const h = String(date.getUTCHours()).padStart(2, "0");
|
|
795
|
+
const min = String(date.getUTCMinutes()).padStart(2, "0");
|
|
796
|
+
const s = String(date.getUTCSeconds()).padStart(2, "0");
|
|
797
|
+
return `D:${y}${m}${d}${h}${min}${s}Z`;
|
|
798
|
+
}
|
|
799
|
+
function pdfString(str) {
|
|
800
|
+
const escaped = str.replace(/\\/g, "\\\\").replace(/\(/g, "\\(").replace(/\)/g, "\\)");
|
|
801
|
+
return `(${escaped})`;
|
|
802
|
+
}
|
|
803
|
+
function updateByteRangeInPlace(pdf) {
|
|
804
|
+
const contentsIdx = findLastBytes(pdf, CONTENTS_MARKER);
|
|
805
|
+
if (contentsIdx === -1)
|
|
806
|
+
throw new Error("Cannot find Contents in signature");
|
|
807
|
+
const contentsStart = contentsIdx + CONTENTS_MARKER.length;
|
|
808
|
+
let contentsEnd = contentsStart;
|
|
809
|
+
while (contentsEnd < pdf.length && pdf[contentsEnd] !== 62)
|
|
810
|
+
contentsEnd++;
|
|
811
|
+
if (contentsEnd >= pdf.length)
|
|
812
|
+
throw new Error("Cannot find end of Contents hex");
|
|
813
|
+
const br = [
|
|
814
|
+
0,
|
|
815
|
+
contentsStart - 1,
|
|
816
|
+
contentsEnd + 1,
|
|
817
|
+
pdf.length - (contentsEnd + 1)
|
|
818
|
+
];
|
|
819
|
+
const brIdx = findLastBytes(pdf, BYTE_RANGE_MARKER);
|
|
820
|
+
if (brIdx === -1)
|
|
821
|
+
throw new Error("Cannot find ByteRange in PDF");
|
|
822
|
+
const brStart = brIdx + BYTE_RANGE_MARKER.length;
|
|
823
|
+
let brEnd = brStart;
|
|
824
|
+
while (brEnd < pdf.length && pdf[brEnd] !== 93)
|
|
825
|
+
brEnd++;
|
|
826
|
+
if (brEnd >= pdf.length)
|
|
827
|
+
throw new Error("Cannot find end of ByteRange");
|
|
828
|
+
const placeholderLen = brEnd - brStart;
|
|
829
|
+
const brValueStr = `${br[0]} ${br[1]} ${br[2]} ${br[3]}`.padEnd(placeholderLen, " ");
|
|
830
|
+
pdf.set(latin1Encoder.encode(brValueStr), brStart);
|
|
831
|
+
return br;
|
|
832
|
+
}
|
|
833
|
+
function findMatchingDictEndInContent(str, startPos) {
|
|
834
|
+
let depth = 0;
|
|
835
|
+
let i = startPos;
|
|
836
|
+
while (i < str.length - 1) {
|
|
837
|
+
if (str[i] === "(") {
|
|
838
|
+
i++;
|
|
839
|
+
while (i < str.length && str[i] !== ")") {
|
|
840
|
+
if (str[i] === "\\")
|
|
841
|
+
i++;
|
|
842
|
+
i++;
|
|
843
|
+
}
|
|
844
|
+
i++;
|
|
845
|
+
} else if (str[i] === "<" && str[i + 1] === "<") {
|
|
846
|
+
depth++;
|
|
847
|
+
i += 2;
|
|
848
|
+
} else if (str[i] === ">" && str[i + 1] === ">") {
|
|
849
|
+
depth--;
|
|
850
|
+
if (depth === 0)
|
|
851
|
+
return i;
|
|
852
|
+
i += 2;
|
|
853
|
+
} else {
|
|
854
|
+
i++;
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
return -1;
|
|
858
|
+
}
|
|
859
|
+
function findByteRange(pdfData) {
|
|
860
|
+
const contentsIdx = findLastBytes(pdfData, CONTENTS_MARKER);
|
|
861
|
+
if (contentsIdx === -1)
|
|
862
|
+
throw new Error("Could not find Contents in PDF");
|
|
863
|
+
const contentsStart = contentsIdx + CONTENTS_MARKER.length;
|
|
864
|
+
let contentsEnd = contentsStart;
|
|
865
|
+
while (contentsEnd < pdfData.length && pdfData[contentsEnd] !== 62)
|
|
866
|
+
contentsEnd++;
|
|
867
|
+
if (contentsEnd >= pdfData.length)
|
|
868
|
+
throw new Error("Could not find end of Contents field");
|
|
869
|
+
const placeholderLength = contentsEnd - contentsStart;
|
|
870
|
+
const byteRange = [
|
|
871
|
+
0,
|
|
872
|
+
contentsStart - 1,
|
|
873
|
+
contentsEnd + 1,
|
|
874
|
+
pdfData.length - (contentsEnd + 1)
|
|
875
|
+
];
|
|
876
|
+
return { byteRange, contentsStart, contentsEnd, placeholderLength };
|
|
877
|
+
}
|
|
878
|
+
function extractBytesToSign(pdfData, byteRange) {
|
|
879
|
+
const [offset1, length1, offset2, length2] = byteRange;
|
|
880
|
+
if (offset1 < 0 || length1 <= 0 || offset2 <= 0 || length2 <= 0) {
|
|
881
|
+
throw new Error(`Invalid ByteRange values: [${byteRange.join(", ")}]`);
|
|
882
|
+
}
|
|
883
|
+
if (offset1 + length1 > pdfData.length || offset2 + length2 > pdfData.length) {
|
|
884
|
+
throw new Error("ByteRange exceeds PDF data size");
|
|
885
|
+
}
|
|
886
|
+
const chunk1 = pdfData.subarray(offset1, offset1 + length1);
|
|
887
|
+
const chunk2 = pdfData.subarray(offset2, offset2 + length2);
|
|
888
|
+
const result = new Uint8Array(chunk1.length + chunk2.length);
|
|
889
|
+
result.set(chunk1, 0);
|
|
890
|
+
result.set(chunk2, chunk1.length);
|
|
891
|
+
return result;
|
|
892
|
+
}
|
|
893
|
+
function embedSignature(pdfData, signature) {
|
|
894
|
+
const { contentsStart, placeholderLength } = findByteRange(pdfData);
|
|
895
|
+
const hexChars = [];
|
|
896
|
+
for (let i = 0;i < signature.length; i++) {
|
|
897
|
+
hexChars.push(signature[i].toString(16).padStart(2, "0"));
|
|
898
|
+
}
|
|
899
|
+
const signatureHex = hexChars.join("");
|
|
900
|
+
if (signatureHex.length > placeholderLength) {
|
|
901
|
+
throw new Error(`Signature too large: ${signatureHex.length} hex chars, placeholder is ${placeholderLength}`);
|
|
902
|
+
}
|
|
903
|
+
const paddedHex = signatureHex.padEnd(placeholderLength, "0");
|
|
904
|
+
const hexBytes = new TextEncoder().encode(paddedHex);
|
|
905
|
+
pdfData.set(hexBytes, contentsStart);
|
|
906
|
+
return pdfData;
|
|
907
|
+
}
|
|
908
|
+
// src/plugins/editing/index.ts
|
|
909
|
+
function loadPdf(data) {
|
|
910
|
+
return new PdfDocumentImpl(data);
|
|
911
|
+
}
|
|
912
|
+
function countPdfPages(data) {
|
|
913
|
+
const pdfStr = new TextDecoder("latin1").decode(data);
|
|
914
|
+
const structure = parsePdfStructure(pdfStr);
|
|
915
|
+
return structure.pageNums.length;
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
export { PdfDocumentImpl, findByteRange, extractBytesToSign, embedSignature, loadPdf, countPdfPages };
|
|
919
|
+
|
|
920
|
+
//# debugId=C149D920AE5494F964756E2164756E21
|