@muhammara/native-core 7.0.0-beta.1 → 7.0.0-beta.3
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 +4 -0
- package/lib/Recipe.js +119 -57
- package/lib/recipe/annotation.js +53 -4
- package/lib/recipe/appendPage.js +22 -9
- package/lib/recipe/graphics-state.js +20 -0
- package/lib/recipe/image.js +4 -0
- package/lib/recipe/info.js +14 -19
- package/lib/recipe/insertPage.js +15 -10
- package/lib/recipe/page.js +808 -11
- package/lib/recipe/parameters.js +2 -0
- package/lib/recipe/replaceText.js +5 -3
- package/lib/recipe/shapes.js +22 -1
- package/lib/recipe/text.helper.js +1 -1
- package/lib/recipe/text.js +106 -8
- package/lib/recipe/vector-polygon.js +11 -0
- package/lib/recipe/vector.helper.js +15 -3
- package/lib/recipe/vector.js +108 -10
- package/lib/recipe-info.js +10 -0
- package/muhammara.d.ts +472 -84
- package/package.json +1 -1
package/lib/recipe/page.js
CHANGED
|
@@ -1,5 +1,570 @@
|
|
|
1
1
|
const muhammara = require("../muhammara");
|
|
2
2
|
|
|
3
|
+
/** Builds the retained page tree while marking deleted leaf pages. @private */
|
|
4
|
+
function readPageTree(
|
|
5
|
+
parser,
|
|
6
|
+
objectID,
|
|
7
|
+
deletedPages,
|
|
8
|
+
modifiedPageIDs,
|
|
9
|
+
pageState,
|
|
10
|
+
generation = 0,
|
|
11
|
+
visited = new Set(),
|
|
12
|
+
depth = 0,
|
|
13
|
+
) {
|
|
14
|
+
if (depth > 1000 || visited.has(objectID)) {
|
|
15
|
+
throw new Error("deletePage requires a valid acyclic page tree");
|
|
16
|
+
}
|
|
17
|
+
visited.add(objectID);
|
|
18
|
+
const dictionary = parser.parseNewObject(objectID).toPDFDictionary();
|
|
19
|
+
const values = dictionary.toJSObject();
|
|
20
|
+
if (depth === 0 && values.Parent !== undefined) {
|
|
21
|
+
throw new Error("deletePage requires a valid page tree");
|
|
22
|
+
}
|
|
23
|
+
const kids = parser.queryDictionaryObject(dictionary, "Kids").toPDFArray();
|
|
24
|
+
const mappedChildren = kids.toJSArray().map((entry) => {
|
|
25
|
+
const reference = entry.toPDFIndirectObjectReference();
|
|
26
|
+
const childID = reference.getObjectID();
|
|
27
|
+
const childDictionary = parser.parseNewObject(childID).toPDFDictionary();
|
|
28
|
+
const childValues = childDictionary.toJSObject();
|
|
29
|
+
const parentReference = childValues.Parent?.toPDFIndirectObjectReference();
|
|
30
|
+
if (
|
|
31
|
+
!parentReference ||
|
|
32
|
+
parentReference.getObjectID() !== objectID ||
|
|
33
|
+
parentReference.getVersion() !== generation
|
|
34
|
+
) {
|
|
35
|
+
throw new Error("deletePage requires a valid page tree");
|
|
36
|
+
}
|
|
37
|
+
const type = childValues.Type.toPDFName().value;
|
|
38
|
+
if (type === "Pages") {
|
|
39
|
+
return readPageTree(
|
|
40
|
+
parser,
|
|
41
|
+
childID,
|
|
42
|
+
deletedPages,
|
|
43
|
+
modifiedPageIDs,
|
|
44
|
+
pageState,
|
|
45
|
+
reference.getVersion(),
|
|
46
|
+
visited,
|
|
47
|
+
depth + 1,
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
pageState.pageNumber += 1;
|
|
51
|
+
// Check deletion before the modified-generation restriction: a deleted
|
|
52
|
+
// page is dropped from the Kids array entirely, never rewritten, so its
|
|
53
|
+
// own generation is irrelevant even if it was also edited.
|
|
54
|
+
if (deletedPages.has(pageState.pageNumber)) {
|
|
55
|
+
pageState.deletedPageIDs.add(childID);
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
if (modifiedPageIDs.has(childID) && reference.getVersion() !== 0) {
|
|
59
|
+
throw new Error(
|
|
60
|
+
"deletePage does not support rewriting nonzero-generation objects",
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
pageState.retainedPageIDs.add(childID);
|
|
64
|
+
return {
|
|
65
|
+
objectID: childID,
|
|
66
|
+
generation: reference.getVersion(),
|
|
67
|
+
count: 1,
|
|
68
|
+
values: childValues,
|
|
69
|
+
};
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
const children = mappedChildren.filter((child) => child && child.count);
|
|
73
|
+
return {
|
|
74
|
+
objectID,
|
|
75
|
+
generation,
|
|
76
|
+
values,
|
|
77
|
+
children,
|
|
78
|
+
count: mappedChildren.reduce(
|
|
79
|
+
(count, child) => count + (child?.count || 0),
|
|
80
|
+
0,
|
|
81
|
+
),
|
|
82
|
+
changed: mappedChildren.some((child) => !child || child.changed),
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Writes changed page-tree nodes back to the modified PDF. @private */
|
|
87
|
+
function writePageTree(writer, copyingContext, node) {
|
|
88
|
+
node.children
|
|
89
|
+
.filter((child) => child.children && child.changed)
|
|
90
|
+
.forEach((child) => writePageTree(writer, copyingContext, child));
|
|
91
|
+
if (!node.changed) return;
|
|
92
|
+
|
|
93
|
+
const objectsContext = writer.getObjectsContext();
|
|
94
|
+
objectsContext.startModifiedIndirectObject(node.objectID);
|
|
95
|
+
const dictionary = objectsContext.startDictionary();
|
|
96
|
+
Object.keys(node.values).forEach((key) => {
|
|
97
|
+
if (key === "Count" || key === "Kids") return;
|
|
98
|
+
dictionary.writeKey(key);
|
|
99
|
+
copyingContext.copyDirectObjectAsIs(node.values[key]);
|
|
100
|
+
});
|
|
101
|
+
dictionary.writeKey("Count");
|
|
102
|
+
objectsContext.writeNumber(node.count);
|
|
103
|
+
dictionary.writeKey("Kids");
|
|
104
|
+
objectsContext.startArray();
|
|
105
|
+
node.children.forEach((child) => {
|
|
106
|
+
objectsContext.writeIndirectObjectReference(
|
|
107
|
+
child.objectID,
|
|
108
|
+
child.generation || 0,
|
|
109
|
+
);
|
|
110
|
+
});
|
|
111
|
+
objectsContext.endArray().endDictionary(dictionary).endIndirectObject();
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Visits every retained node in a page tree - Pages nodes and leaf pages
|
|
116
|
+
* alike - depth-first. Shared by every deletePage() pass that needs to walk
|
|
117
|
+
* the tree readPageTree() already built, instead of re-parsing or re-walking
|
|
118
|
+
* it independently.
|
|
119
|
+
*/
|
|
120
|
+
function walkPageTree(tree, visit) {
|
|
121
|
+
const pending = [tree];
|
|
122
|
+
while (pending.length) {
|
|
123
|
+
const node = pending.pop();
|
|
124
|
+
visit(node);
|
|
125
|
+
if (node.children) pending.push(...node.children);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Adds retained Pages-node object IDs to a set. @private */
|
|
130
|
+
function collectPageTreeObjectIDs(tree, objectIDs) {
|
|
131
|
+
walkPageTree(tree, (node) => {
|
|
132
|
+
if (node.children) objectIDs.add(node.objectID);
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** Rejects page-tree objects that cannot be rewritten safely. @private */
|
|
137
|
+
function assertSupportedModifiedGenerations(tree, pageLabels, root, writer) {
|
|
138
|
+
walkPageTree(tree, (node) => {
|
|
139
|
+
if (!node.children) return;
|
|
140
|
+
if (node.changed && node.generation !== 0) {
|
|
141
|
+
throw new Error(
|
|
142
|
+
"deletePage does not support rewriting nonzero-generation objects",
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
if (pageLabels?.reference && pageLabels.reference.getVersion() !== 0) {
|
|
147
|
+
throw new Error(
|
|
148
|
+
"deletePage does not support rewriting nonzero-generation objects",
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
// A writer with _setPageLabelsObject() (wasm) always attaches a new
|
|
152
|
+
// PageLabels entry through that dedicated hook and never rewrites the
|
|
153
|
+
// catalog object in place, so the catalog's own generation never matters
|
|
154
|
+
// there. Without it (native-core), writePageLabels() falls back to
|
|
155
|
+
// rewriting the catalog via startModifiedIndirectObject(rootID), which
|
|
156
|
+
// only supports generation 0.
|
|
157
|
+
if (pageLabels && !pageLabels.reference && !writer._setPageLabelsObject) {
|
|
158
|
+
if (root.getVersion() !== 0) {
|
|
159
|
+
throw new Error(
|
|
160
|
+
"deletePage does not support rewriting nonzero-generation objects",
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** Rejects retained structures that reference a deleted page. @private */
|
|
167
|
+
function assertNoDeletedPageReferences(
|
|
168
|
+
parser,
|
|
169
|
+
value,
|
|
170
|
+
deletedPageIDs,
|
|
171
|
+
skippedObjectIDs,
|
|
172
|
+
visited = new Set(),
|
|
173
|
+
depth = 0,
|
|
174
|
+
) {
|
|
175
|
+
if (!value) return;
|
|
176
|
+
if (depth > 1000) {
|
|
177
|
+
throw new Error("deletePage cannot validate deeply nested references");
|
|
178
|
+
}
|
|
179
|
+
const reference = value.toPDFIndirectObjectReference?.();
|
|
180
|
+
if (reference) {
|
|
181
|
+
const objectID = reference.getObjectID();
|
|
182
|
+
if (deletedPageIDs.has(objectID)) {
|
|
183
|
+
throw new Error(
|
|
184
|
+
"deletePage cannot remove a page referenced by retained document structures",
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
if (skippedObjectIDs.has(objectID) || visited.has(objectID)) return;
|
|
188
|
+
visited.add(objectID);
|
|
189
|
+
assertNoDeletedPageReferences(
|
|
190
|
+
parser,
|
|
191
|
+
parser.parseNewObject(objectID),
|
|
192
|
+
deletedPageIDs,
|
|
193
|
+
skippedObjectIDs,
|
|
194
|
+
visited,
|
|
195
|
+
depth + 1,
|
|
196
|
+
);
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
const array =
|
|
200
|
+
value.toPDFArray?.() ||
|
|
201
|
+
(typeof value.toJSArray === "function" ? value : null);
|
|
202
|
+
if (array) {
|
|
203
|
+
array
|
|
204
|
+
.toJSArray()
|
|
205
|
+
.forEach((entry) =>
|
|
206
|
+
assertNoDeletedPageReferences(
|
|
207
|
+
parser,
|
|
208
|
+
entry,
|
|
209
|
+
deletedPageIDs,
|
|
210
|
+
skippedObjectIDs,
|
|
211
|
+
visited,
|
|
212
|
+
depth + 1,
|
|
213
|
+
),
|
|
214
|
+
);
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
const dictionary =
|
|
218
|
+
value.toPDFDictionary?.() ||
|
|
219
|
+
(typeof value.toJSObject === "function" ? value : null);
|
|
220
|
+
if (dictionary) {
|
|
221
|
+
Object.values(dictionary.toJSObject()).forEach((entry) =>
|
|
222
|
+
assertNoDeletedPageReferences(
|
|
223
|
+
parser,
|
|
224
|
+
entry,
|
|
225
|
+
deletedPageIDs,
|
|
226
|
+
skippedObjectIDs,
|
|
227
|
+
visited,
|
|
228
|
+
depth + 1,
|
|
229
|
+
),
|
|
230
|
+
);
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
const stream = value.toPDFStream?.();
|
|
234
|
+
if (stream) {
|
|
235
|
+
assertNoDeletedPageReferences(
|
|
236
|
+
parser,
|
|
237
|
+
stream.getDictionary(),
|
|
238
|
+
deletedPageIDs,
|
|
239
|
+
skippedObjectIDs,
|
|
240
|
+
visited,
|
|
241
|
+
depth + 1,
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/** Checks retained document structures for deleted-page references. @private */
|
|
247
|
+
function validateDeletedPageReferences(
|
|
248
|
+
parser,
|
|
249
|
+
catalog,
|
|
250
|
+
rootID,
|
|
251
|
+
tree,
|
|
252
|
+
pageLabels,
|
|
253
|
+
deletedPageIDs,
|
|
254
|
+
sourcePageCount,
|
|
255
|
+
) {
|
|
256
|
+
const skippedObjectIDs = new Set([rootID]);
|
|
257
|
+
collectPageTreeObjectIDs(tree, skippedObjectIDs);
|
|
258
|
+
for (let pageIndex = 0; pageIndex < sourcePageCount; pageIndex += 1) {
|
|
259
|
+
skippedObjectIDs.add(parser.getPageObjectID(pageIndex));
|
|
260
|
+
}
|
|
261
|
+
const visited = new Set();
|
|
262
|
+
Object.entries(catalog)
|
|
263
|
+
.filter(([key]) => !["PageLabels", "Pages"].includes(key))
|
|
264
|
+
.forEach(([, value]) =>
|
|
265
|
+
assertNoDeletedPageReferences(
|
|
266
|
+
parser,
|
|
267
|
+
value,
|
|
268
|
+
deletedPageIDs,
|
|
269
|
+
skippedObjectIDs,
|
|
270
|
+
visited,
|
|
271
|
+
),
|
|
272
|
+
);
|
|
273
|
+
if (pageLabels) {
|
|
274
|
+
Object.entries(pageLabels.values)
|
|
275
|
+
.filter(([key]) => !["Kids", "Limits", "Nums"].includes(key))
|
|
276
|
+
.forEach(([, value]) =>
|
|
277
|
+
assertNoDeletedPageReferences(
|
|
278
|
+
parser,
|
|
279
|
+
value,
|
|
280
|
+
deletedPageIDs,
|
|
281
|
+
skippedObjectIDs,
|
|
282
|
+
visited,
|
|
283
|
+
),
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
// Walks the same tree readPageTree() already parsed - each retained leaf
|
|
287
|
+
// carries the page dictionary values readPageTree() read, so there is no
|
|
288
|
+
// need to re-parse every retained page's dictionary here.
|
|
289
|
+
walkPageTree(tree, (node) => {
|
|
290
|
+
const isLeaf = !node.children;
|
|
291
|
+
const skipKeys = isLeaf ? ["Parent"] : ["Count", "Kids", "Parent"];
|
|
292
|
+
Object.entries(node.values)
|
|
293
|
+
.filter(([key]) => !skipKeys.includes(key))
|
|
294
|
+
.forEach(([, value]) =>
|
|
295
|
+
assertNoDeletedPageReferences(
|
|
296
|
+
parser,
|
|
297
|
+
value,
|
|
298
|
+
deletedPageIDs,
|
|
299
|
+
skippedObjectIDs,
|
|
300
|
+
visited,
|
|
301
|
+
),
|
|
302
|
+
);
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/** Collects page-label number-tree entries. @private */
|
|
307
|
+
function collectPageLabels(
|
|
308
|
+
parser,
|
|
309
|
+
dictionary,
|
|
310
|
+
entries,
|
|
311
|
+
visited = new Set(),
|
|
312
|
+
objectID = 0,
|
|
313
|
+
depth = 0,
|
|
314
|
+
) {
|
|
315
|
+
if (depth > 1000 || (objectID && visited.has(objectID))) {
|
|
316
|
+
throw new Error("deletePage requires valid acyclic PageLabels");
|
|
317
|
+
}
|
|
318
|
+
if (objectID) visited.add(objectID);
|
|
319
|
+
const values = dictionary.toJSObject();
|
|
320
|
+
if (values.Nums) {
|
|
321
|
+
const numbersArray = resolvePageLabelObject(
|
|
322
|
+
parser,
|
|
323
|
+
values.Nums,
|
|
324
|
+
).toPDFArray();
|
|
325
|
+
const numbers = numbersArray.toJSArray();
|
|
326
|
+
for (let index = 0; index < numbers.length; index += 2) {
|
|
327
|
+
entries.push({
|
|
328
|
+
index: resolvePageLabelObject(parser, numbers[index]).toNumber(),
|
|
329
|
+
value: readPageLabel(
|
|
330
|
+
parser,
|
|
331
|
+
resolvePageLabelObject(parser, numbers[index + 1]),
|
|
332
|
+
),
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
if (values.Kids) {
|
|
337
|
+
parser
|
|
338
|
+
.queryDictionaryObject(dictionary, "Kids")
|
|
339
|
+
.toPDFArray()
|
|
340
|
+
.toJSArray()
|
|
341
|
+
.forEach((entry) => {
|
|
342
|
+
const objectID = entry.toPDFIndirectObjectReference().getObjectID();
|
|
343
|
+
collectPageLabels(
|
|
344
|
+
parser,
|
|
345
|
+
resolvePageLabelObject(parser, entry).toPDFDictionary(),
|
|
346
|
+
entries,
|
|
347
|
+
visited,
|
|
348
|
+
objectID,
|
|
349
|
+
depth + 1,
|
|
350
|
+
);
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
return values;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/** Resolves an indirect chain used by a page-label number tree. @private */
|
|
357
|
+
function resolvePageLabelObject(parser, value) {
|
|
358
|
+
const visited = new Set();
|
|
359
|
+
let resolved = value;
|
|
360
|
+
while (resolved?.toPDFIndirectObjectReference?.()) {
|
|
361
|
+
const objectID = resolved.toPDFIndirectObjectReference().getObjectID();
|
|
362
|
+
if (visited.size > 1000 || visited.has(objectID)) {
|
|
363
|
+
throw new Error("deletePage requires valid acyclic PageLabels");
|
|
364
|
+
}
|
|
365
|
+
visited.add(objectID);
|
|
366
|
+
resolved = parser.parseNewObject(objectID);
|
|
367
|
+
}
|
|
368
|
+
return resolved;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/** Reads a page-label dictionary into a serializable value. @private */
|
|
372
|
+
function readPageLabel(parser, value) {
|
|
373
|
+
const dictionary = value?.toPDFDictionary();
|
|
374
|
+
if (!dictionary) {
|
|
375
|
+
throw new Error("deletePage requires valid PageLabels entries");
|
|
376
|
+
}
|
|
377
|
+
const values = dictionary.toJSObject();
|
|
378
|
+
const style = values.S ? resolvePageLabelObject(parser, values.S) : null;
|
|
379
|
+
const prefix = values.P ? resolvePageLabelObject(parser, values.P) : null;
|
|
380
|
+
const start = values.St ? resolvePageLabelObject(parser, values.St) : null;
|
|
381
|
+
const prefixHex = prefix?.toPDFHexString();
|
|
382
|
+
const prefixLiteral = prefix?.toPDFLiteralString();
|
|
383
|
+
return {
|
|
384
|
+
style: style?.toPDFName()?.value,
|
|
385
|
+
prefix:
|
|
386
|
+
values.P === undefined
|
|
387
|
+
? undefined
|
|
388
|
+
: {
|
|
389
|
+
bytes: (prefixHex || prefixLiteral).toBytesArray(),
|
|
390
|
+
isHex: Boolean(prefixHex),
|
|
391
|
+
},
|
|
392
|
+
start: start?.toNumber(),
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/** Writes indirect objects for normalized page-label values. @private */
|
|
397
|
+
function writePageLabelObjects(objectsContext, entries) {
|
|
398
|
+
return entries.map((entry) => {
|
|
399
|
+
const objectID = objectsContext.startNewIndirectObject();
|
|
400
|
+
const dictionary = objectsContext.startDictionary();
|
|
401
|
+
if (entry.value.style !== undefined) {
|
|
402
|
+
dictionary.writeKey("S");
|
|
403
|
+
objectsContext.writeName(entry.value.style);
|
|
404
|
+
}
|
|
405
|
+
if (entry.value.prefix !== undefined) {
|
|
406
|
+
dictionary.writeKey("P");
|
|
407
|
+
objectsContext[
|
|
408
|
+
entry.value.prefix.isHex ? "writeHexString" : "writeLiteralString"
|
|
409
|
+
](entry.value.prefix.bytes);
|
|
410
|
+
}
|
|
411
|
+
if (entry.value.start !== undefined) {
|
|
412
|
+
dictionary.writeKey("St");
|
|
413
|
+
objectsContext.writeNumber(entry.value.start);
|
|
414
|
+
}
|
|
415
|
+
objectsContext.endDictionary(dictionary).endIndirectObject();
|
|
416
|
+
return { index: entry.index, objectID };
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/** Writes the number-tree dictionary for page labels. @private */
|
|
421
|
+
function writePageLabelsDictionary(
|
|
422
|
+
objectsContext,
|
|
423
|
+
copyingContext,
|
|
424
|
+
dictionary,
|
|
425
|
+
values,
|
|
426
|
+
entries,
|
|
427
|
+
) {
|
|
428
|
+
Object.keys(values).forEach((key) => {
|
|
429
|
+
if (key === "Kids" || key === "Limits" || key === "Nums") return;
|
|
430
|
+
dictionary.writeKey(key);
|
|
431
|
+
copyingContext.copyDirectObjectAsIs(values[key]);
|
|
432
|
+
});
|
|
433
|
+
dictionary.writeKey("Nums");
|
|
434
|
+
objectsContext.startArray();
|
|
435
|
+
entries.forEach((entry) => {
|
|
436
|
+
objectsContext.writeNumber(entry.index);
|
|
437
|
+
objectsContext.writeIndirectObjectReference(entry.objectID);
|
|
438
|
+
});
|
|
439
|
+
objectsContext.endArray();
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/** Reindexes page labels after removing source pages. @private */
|
|
443
|
+
function preparePageLabels(
|
|
444
|
+
parser,
|
|
445
|
+
catalogDictionary,
|
|
446
|
+
deletedPages,
|
|
447
|
+
sourcePageCount,
|
|
448
|
+
) {
|
|
449
|
+
const catalogValues = catalogDictionary.toJSObject();
|
|
450
|
+
if (!catalogValues.PageLabels) {
|
|
451
|
+
return null;
|
|
452
|
+
}
|
|
453
|
+
const pageLabelsValue = resolvePageLabelObject(
|
|
454
|
+
parser,
|
|
455
|
+
catalogValues.PageLabels,
|
|
456
|
+
);
|
|
457
|
+
if (pageLabelsValue.toPDFNull()) return null;
|
|
458
|
+
const labelsDictionary = pageLabelsValue.toPDFDictionary();
|
|
459
|
+
if (!labelsDictionary) {
|
|
460
|
+
throw new Error("deletePage requires PageLabels to be a number tree");
|
|
461
|
+
}
|
|
462
|
+
const entries = [];
|
|
463
|
+
const reference = catalogValues.PageLabels.toPDFIndirectObjectReference();
|
|
464
|
+
const values = collectPageLabels(
|
|
465
|
+
parser,
|
|
466
|
+
labelsDictionary,
|
|
467
|
+
entries,
|
|
468
|
+
new Set(),
|
|
469
|
+
reference?.getObjectID(),
|
|
470
|
+
);
|
|
471
|
+
const deletedIndices = new Set(
|
|
472
|
+
Array.from(deletedPages, (pageNumber) => pageNumber - 1),
|
|
473
|
+
);
|
|
474
|
+
const outputIndices = new Map();
|
|
475
|
+
let outputIndex = 0;
|
|
476
|
+
for (let index = 0; index < sourcePageCount; index += 1) {
|
|
477
|
+
if (deletedIndices.has(index)) continue;
|
|
478
|
+
outputIndices.set(index, outputIndex);
|
|
479
|
+
outputIndex += 1;
|
|
480
|
+
}
|
|
481
|
+
const sortedEntries = entries.sort((left, right) => left.index - right.index);
|
|
482
|
+
const normalized = [];
|
|
483
|
+
sortedEntries.forEach((entry, entryIndex) => {
|
|
484
|
+
const rangeEnd = Math.min(
|
|
485
|
+
sortedEntries[entryIndex + 1]?.index ?? sourcePageCount,
|
|
486
|
+
sourcePageCount,
|
|
487
|
+
);
|
|
488
|
+
let startsRun = true;
|
|
489
|
+
for (let index = entry.index; index < rangeEnd; index += 1) {
|
|
490
|
+
if (deletedIndices.has(index)) {
|
|
491
|
+
startsRun = true;
|
|
492
|
+
continue;
|
|
493
|
+
}
|
|
494
|
+
if (startsRun) {
|
|
495
|
+
const offset = index - entry.index;
|
|
496
|
+
normalized.push({
|
|
497
|
+
index: outputIndices.get(index),
|
|
498
|
+
value:
|
|
499
|
+
offset && entry.value.style !== undefined
|
|
500
|
+
? {
|
|
501
|
+
...entry.value,
|
|
502
|
+
start: (entry.value.start ?? 1) + offset,
|
|
503
|
+
}
|
|
504
|
+
: entry.value,
|
|
505
|
+
});
|
|
506
|
+
startsRun = false;
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
});
|
|
510
|
+
return {
|
|
511
|
+
catalogValues,
|
|
512
|
+
reference,
|
|
513
|
+
values,
|
|
514
|
+
entries: normalized,
|
|
515
|
+
};
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
/** Writes updated page labels and attaches them to the catalog. @private */
|
|
519
|
+
function writePageLabels(writer, copyingContext, rootID, pageLabels) {
|
|
520
|
+
if (!pageLabels) return;
|
|
521
|
+
|
|
522
|
+
const objectsContext = writer.getObjectsContext();
|
|
523
|
+
const entries = writePageLabelObjects(objectsContext, pageLabels.entries);
|
|
524
|
+
if (pageLabels.reference) {
|
|
525
|
+
objectsContext.startModifiedIndirectObject(
|
|
526
|
+
pageLabels.reference.getObjectID(),
|
|
527
|
+
);
|
|
528
|
+
const dictionary = objectsContext.startDictionary();
|
|
529
|
+
writePageLabelsDictionary(
|
|
530
|
+
objectsContext,
|
|
531
|
+
copyingContext,
|
|
532
|
+
dictionary,
|
|
533
|
+
pageLabels.values,
|
|
534
|
+
entries,
|
|
535
|
+
);
|
|
536
|
+
objectsContext.endDictionary(dictionary).endIndirectObject();
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
const labelsObjectID = objectsContext.startNewIndirectObject();
|
|
541
|
+
const labelsDictionary = objectsContext.startDictionary();
|
|
542
|
+
writePageLabelsDictionary(
|
|
543
|
+
objectsContext,
|
|
544
|
+
copyingContext,
|
|
545
|
+
labelsDictionary,
|
|
546
|
+
pageLabels.values,
|
|
547
|
+
entries,
|
|
548
|
+
);
|
|
549
|
+
objectsContext.endDictionary(labelsDictionary).endIndirectObject();
|
|
550
|
+
|
|
551
|
+
if (writer._setPageLabelsObject) {
|
|
552
|
+
writer._setPageLabelsObject(labelsObjectID);
|
|
553
|
+
return;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
objectsContext.startModifiedIndirectObject(rootID);
|
|
557
|
+
const dictionary = objectsContext.startDictionary();
|
|
558
|
+
Object.keys(pageLabels.catalogValues).forEach((key) => {
|
|
559
|
+
if (key === "PageLabels") return;
|
|
560
|
+
dictionary.writeKey(key);
|
|
561
|
+
copyingContext.copyDirectObjectAsIs(pageLabels.catalogValues[key]);
|
|
562
|
+
});
|
|
563
|
+
dictionary.writeKey("PageLabels");
|
|
564
|
+
objectsContext.writeIndirectObjectReference(labelsObjectID);
|
|
565
|
+
objectsContext.endDictionary(dictionary).endIndirectObject();
|
|
566
|
+
}
|
|
567
|
+
|
|
3
568
|
/**
|
|
4
569
|
* Create a new page, specifying either actual width and height, or the name
|
|
5
570
|
* of a supported page size (eg. 'letter', 'letter-size')
|
|
@@ -18,6 +583,9 @@ const muhammara = require("../muhammara");
|
|
|
18
583
|
* @returns {Recipe} The recipe instance.
|
|
19
584
|
*/
|
|
20
585
|
exports.createPage = function createPage(pageWidth, pageHeight, margins) {
|
|
586
|
+
if (this.deletedPages?.size) {
|
|
587
|
+
throw new Error("createPage cannot be combined with deletePage");
|
|
588
|
+
}
|
|
21
589
|
if (!pageWidth && !pageHeight) {
|
|
22
590
|
[pageWidth, pageHeight] = this.default.pageSize;
|
|
23
591
|
} else if (
|
|
@@ -67,6 +635,8 @@ exports.createPage = function createPage(pageWidth, pageHeight, margins) {
|
|
|
67
635
|
this.pageNumber = pageNumber;
|
|
68
636
|
this.pageContext = this.writer.startPageContentContext(this.page);
|
|
69
637
|
this.editingPage = false;
|
|
638
|
+
this.contextState = "active-new";
|
|
639
|
+
if (!this.isNewPDF) this.pagesCreated = true;
|
|
70
640
|
|
|
71
641
|
if (margins) {
|
|
72
642
|
this.margins(margins);
|
|
@@ -76,6 +646,61 @@ exports.createPage = function createPage(pageWidth, pageHeight, margins) {
|
|
|
76
646
|
return this;
|
|
77
647
|
};
|
|
78
648
|
|
|
649
|
+
/**
|
|
650
|
+
* Set the rotation of the current page.
|
|
651
|
+
* @name rotate
|
|
652
|
+
* @function
|
|
653
|
+
* @memberof Recipe#
|
|
654
|
+
* @param {number} rotation - The page rotation in degrees.
|
|
655
|
+
* @returns {Recipe} The recipe instance.
|
|
656
|
+
*/
|
|
657
|
+
exports.rotate = function rotate(rotation) {
|
|
658
|
+
this.page.rotate = rotation;
|
|
659
|
+
this.metadata[this.pageNumber].rotate = rotation;
|
|
660
|
+
return this;
|
|
661
|
+
};
|
|
662
|
+
|
|
663
|
+
/**
|
|
664
|
+
* Set a page box on the active new page.
|
|
665
|
+
* @name setPageBox
|
|
666
|
+
* @function
|
|
667
|
+
* @memberof Recipe#
|
|
668
|
+
* @param {number} box - An `ePDFPageBox*` constant.
|
|
669
|
+
* @param {number} left - The PDF left coordinate.
|
|
670
|
+
* @param {number} bottom - The PDF bottom coordinate.
|
|
671
|
+
* @param {number} right - The PDF right coordinate.
|
|
672
|
+
* @param {number} top - The PDF top coordinate.
|
|
673
|
+
* @returns {Recipe} The recipe instance.
|
|
674
|
+
* @throws {RangeError} If the page box constant is unknown.
|
|
675
|
+
*/
|
|
676
|
+
exports.setPageBox = function setPageBox(box, left, bottom, right, top) {
|
|
677
|
+
const boxes = {
|
|
678
|
+
[muhammara.ePDFPageBoxMediaBox]: "mediaBox",
|
|
679
|
+
[muhammara.ePDFPageBoxCropBox]: "cropBox",
|
|
680
|
+
[muhammara.ePDFPageBoxBleedBox]: "bleedBox",
|
|
681
|
+
[muhammara.ePDFPageBoxTrimBox]: "trimBox",
|
|
682
|
+
[muhammara.ePDFPageBoxArtBox]: "artBox",
|
|
683
|
+
};
|
|
684
|
+
if (!(box in boxes)) {
|
|
685
|
+
throw new RangeError(`Unknown page box: ${box}`);
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
const pageBox = [left, bottom, right, top];
|
|
689
|
+
this.page[boxes[box]] = pageBox;
|
|
690
|
+
if (box === muhammara.ePDFPageBoxMediaBox) {
|
|
691
|
+
const page = this.metadata[this.pageNumber];
|
|
692
|
+
const width = right - left;
|
|
693
|
+
const height = top - bottom;
|
|
694
|
+
Object.assign(page, {
|
|
695
|
+
mediaBox: pageBox,
|
|
696
|
+
width,
|
|
697
|
+
height,
|
|
698
|
+
layout: width > height ? "landscape" : "portrait",
|
|
699
|
+
});
|
|
700
|
+
}
|
|
701
|
+
return this;
|
|
702
|
+
};
|
|
703
|
+
|
|
79
704
|
/**
|
|
80
705
|
* Finish a page
|
|
81
706
|
* @name endPage
|
|
@@ -89,14 +714,16 @@ exports.endPage = function endPage() {
|
|
|
89
714
|
}
|
|
90
715
|
|
|
91
716
|
if (this.page.endContext) {
|
|
92
|
-
this.page.endContext();
|
|
717
|
+
if (this.contextState === "active-edit") this.page.endContext();
|
|
93
718
|
this.page.writePage();
|
|
94
719
|
} else {
|
|
95
720
|
this.writer.writePage(this.page);
|
|
96
721
|
}
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
722
|
+
this.page = null;
|
|
723
|
+
this.pageContext = null;
|
|
724
|
+
this.pageNumber = 0;
|
|
725
|
+
this.editingPage = false;
|
|
726
|
+
this.contextState = "idle";
|
|
100
727
|
|
|
101
728
|
return this;
|
|
102
729
|
};
|
|
@@ -121,6 +748,8 @@ exports.editPage = function editPage(pageNumber) {
|
|
|
121
748
|
this.pageNumber = pageNumber;
|
|
122
749
|
this.pageContext = pageModifier.startContext().getContext();
|
|
123
750
|
this.editingPage = true;
|
|
751
|
+
this.contextState = "active-edit";
|
|
752
|
+
this.modifiedSourcePages.add(pageNumber);
|
|
124
753
|
|
|
125
754
|
this._resumePageRotation(pageNumber);
|
|
126
755
|
|
|
@@ -169,6 +798,148 @@ exports.editPage = function editPage(pageNumber) {
|
|
|
169
798
|
return this;
|
|
170
799
|
};
|
|
171
800
|
|
|
801
|
+
/**
|
|
802
|
+
* Delete one or more pages from an existing PDF.
|
|
803
|
+
* Page numbers are one-based and refer to the original source document.
|
|
804
|
+
* @name deletePage
|
|
805
|
+
* @function
|
|
806
|
+
* @memberof Recipe#
|
|
807
|
+
* @param {number|number[]} pageNumbers - Page number or page numbers to delete.
|
|
808
|
+
* @returns {Recipe} The recipe instance.
|
|
809
|
+
* @throws {RangeError} If a page number does not identify an original page.
|
|
810
|
+
* @throws {Error} If the Recipe has no existing source, has ended, would delete
|
|
811
|
+
* every page, or combines deletion with page composition. Page-tree,
|
|
812
|
+
* retained-reference, and object-generation validation is deferred to
|
|
813
|
+
* endPDF(), which throws those errors during finalization.
|
|
814
|
+
*/
|
|
815
|
+
exports.deletePage = function deletePage(pageNumbers) {
|
|
816
|
+
if (this.ended) {
|
|
817
|
+
throw new Error("Cannot delete a page after endPDF");
|
|
818
|
+
}
|
|
819
|
+
if (this.isNewPDF) {
|
|
820
|
+
throw new Error("deletePage requires an existing PDF");
|
|
821
|
+
}
|
|
822
|
+
if (this.needToInsertPages) {
|
|
823
|
+
throw new Error("deletePage cannot be combined with insertPage");
|
|
824
|
+
}
|
|
825
|
+
if (this.pagesAppended) {
|
|
826
|
+
throw new Error("deletePage cannot be combined with appendPage");
|
|
827
|
+
}
|
|
828
|
+
if (this.pagesCreated) {
|
|
829
|
+
throw new Error("deletePage cannot be combined with createPage");
|
|
830
|
+
}
|
|
831
|
+
pageNumbers = Array.isArray(pageNumbers) ? pageNumbers : [pageNumbers];
|
|
832
|
+
const deletedPages = new Set(this.deletedPages || []);
|
|
833
|
+
pageNumbers.forEach((pageNumber) => {
|
|
834
|
+
if (
|
|
835
|
+
!Number.isInteger(pageNumber) ||
|
|
836
|
+
pageNumber < 1 ||
|
|
837
|
+
pageNumber > this.sourcePageCount
|
|
838
|
+
) {
|
|
839
|
+
throw new RangeError("pageNumber must identify an existing page");
|
|
840
|
+
}
|
|
841
|
+
deletedPages.add(pageNumber);
|
|
842
|
+
});
|
|
843
|
+
if (deletedPages.size >= this.sourcePageCount) {
|
|
844
|
+
throw new Error("At least one page must remain in the PDF");
|
|
845
|
+
}
|
|
846
|
+
this.deletedPages = deletedPages;
|
|
847
|
+
return this;
|
|
848
|
+
};
|
|
849
|
+
|
|
850
|
+
/** Applies queued page deletions during finalization. @private */
|
|
851
|
+
exports._deletePages = function _deletePages() {
|
|
852
|
+
if (!this.deletedPages?.size) return this;
|
|
853
|
+
|
|
854
|
+
const copyingContext = this.writer.createPDFCopyingContextForModifiedFile();
|
|
855
|
+
try {
|
|
856
|
+
const parser = copyingContext.getSourceDocumentParser();
|
|
857
|
+
const trailer = parser.getTrailer().toJSObject();
|
|
858
|
+
const rootReference = trailer.Root.toPDFIndirectObjectReference();
|
|
859
|
+
const rootID = rootReference.getObjectID();
|
|
860
|
+
const catalogDictionary = parser.parseNewObject(rootID).toPDFDictionary();
|
|
861
|
+
const catalog = catalogDictionary.toJSObject();
|
|
862
|
+
const pagesReference = catalog.Pages.toPDFIndirectObjectReference();
|
|
863
|
+
const pageLabels = preparePageLabels(
|
|
864
|
+
parser,
|
|
865
|
+
catalogDictionary,
|
|
866
|
+
this.deletedPages,
|
|
867
|
+
this.sourcePageCount,
|
|
868
|
+
);
|
|
869
|
+
const modifiedPageIDs = new Set(
|
|
870
|
+
Array.from(this.modifiedSourcePages, (pageNumber) =>
|
|
871
|
+
parser.getPageObjectID(pageNumber - 1),
|
|
872
|
+
),
|
|
873
|
+
);
|
|
874
|
+
const pageState = {
|
|
875
|
+
pageNumber: 0,
|
|
876
|
+
deletedPageIDs: new Set(),
|
|
877
|
+
retainedPageIDs: new Set(),
|
|
878
|
+
};
|
|
879
|
+
const tree = readPageTree(
|
|
880
|
+
parser,
|
|
881
|
+
pagesReference.getObjectID(),
|
|
882
|
+
this.deletedPages,
|
|
883
|
+
modifiedPageIDs,
|
|
884
|
+
pageState,
|
|
885
|
+
pagesReference.getVersion(),
|
|
886
|
+
);
|
|
887
|
+
pageState.retainedPageIDs.forEach((objectID) =>
|
|
888
|
+
pageState.deletedPageIDs.delete(objectID),
|
|
889
|
+
);
|
|
890
|
+
assertSupportedModifiedGenerations(
|
|
891
|
+
tree,
|
|
892
|
+
pageLabels,
|
|
893
|
+
rootReference,
|
|
894
|
+
this.writer,
|
|
895
|
+
);
|
|
896
|
+
validateDeletedPageReferences(
|
|
897
|
+
parser,
|
|
898
|
+
catalog,
|
|
899
|
+
rootID,
|
|
900
|
+
tree,
|
|
901
|
+
pageLabels,
|
|
902
|
+
pageState.deletedPageIDs,
|
|
903
|
+
this.sourcePageCount,
|
|
904
|
+
);
|
|
905
|
+
writePageTree(this.writer, copyingContext, tree);
|
|
906
|
+
writePageLabels(this.writer, copyingContext, rootID, pageLabels);
|
|
907
|
+
} finally {
|
|
908
|
+
copyingContext.end();
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
const retained = Object.keys(this.metadata)
|
|
912
|
+
.filter((key) => /^\d+$/.test(key))
|
|
913
|
+
.map(Number)
|
|
914
|
+
.filter((pageNumber) => !this.deletedPages.has(pageNumber))
|
|
915
|
+
.map((pageNumber) => this.metadata[pageNumber]);
|
|
916
|
+
Object.keys(this.metadata)
|
|
917
|
+
.filter((key) => /^\d+$/.test(key))
|
|
918
|
+
.forEach((key) => delete this.metadata[key]);
|
|
919
|
+
const pageNumberMap = new Map();
|
|
920
|
+
retained.forEach((page, index) => {
|
|
921
|
+
pageNumberMap.set(page.pageNumber, index + 1);
|
|
922
|
+
this.metadata[index + 1] = { ...page, pageNumber: index + 1 };
|
|
923
|
+
});
|
|
924
|
+
this.annotationsToWrite = this.annotationsToWrite
|
|
925
|
+
.filter((annotation) => pageNumberMap.has(annotation.pageNumber))
|
|
926
|
+
.map((annotation) => ({
|
|
927
|
+
...annotation,
|
|
928
|
+
pageNumber: pageNumberMap.get(annotation.pageNumber),
|
|
929
|
+
}));
|
|
930
|
+
const annotations = [];
|
|
931
|
+
pageNumberMap.forEach((newPageNumber, oldPageNumber) => {
|
|
932
|
+
if (this.annotations[oldPageNumber - 1]) {
|
|
933
|
+
annotations[newPageNumber - 1] = this.annotations[oldPageNumber - 1];
|
|
934
|
+
}
|
|
935
|
+
});
|
|
936
|
+
this.annotations = annotations;
|
|
937
|
+
this.metadata.pages = retained.length;
|
|
938
|
+
this.metadata.pageCount = retained.length;
|
|
939
|
+
this.deletedPages = null;
|
|
940
|
+
return this;
|
|
941
|
+
};
|
|
942
|
+
|
|
172
943
|
exports._resumePageRotation = function _resumePageRotation(
|
|
173
944
|
pageNumber,
|
|
174
945
|
context,
|
|
@@ -211,7 +982,7 @@ exports._resumePageRotation = function _resumePageRotation(
|
|
|
211
982
|
* @function
|
|
212
983
|
* @memberof Recipe#
|
|
213
984
|
* @param {number} pageNumber - The page number.
|
|
214
|
-
* @returns {
|
|
985
|
+
* @returns {RecipePageInfo} The page information.
|
|
215
986
|
*/
|
|
216
987
|
exports.pageInfo = function pageInfo(pageNumber) {
|
|
217
988
|
const pageInfo = this.metadata[pageNumber];
|
|
@@ -223,20 +994,39 @@ exports.pageInfo = function pageInfo(pageNumber) {
|
|
|
223
994
|
};
|
|
224
995
|
};
|
|
225
996
|
|
|
997
|
+
/**
|
|
998
|
+
* Get information about the current page.
|
|
999
|
+
* @name getCurrentPageInfo
|
|
1000
|
+
* @function
|
|
1001
|
+
* @memberof Recipe#
|
|
1002
|
+
* @returns {RecipePageInfo|null} The current page information, or null when no page has been created or edited.
|
|
1003
|
+
*/
|
|
1004
|
+
exports.getCurrentPageInfo = function getCurrentPageInfo() {
|
|
1005
|
+
const pageNumber =
|
|
1006
|
+
this.pageNumber || this.metadata?.pageCount || this.metadata?.pages;
|
|
1007
|
+
return pageNumber ? this.pageInfo(pageNumber) : null;
|
|
1008
|
+
};
|
|
1009
|
+
|
|
226
1010
|
/**
|
|
227
1011
|
* Pause the current page content context.
|
|
228
1012
|
* @name pauseContext
|
|
229
1013
|
* @function
|
|
230
1014
|
* @memberof Recipe#
|
|
231
|
-
* @returns {
|
|
1015
|
+
* @returns {Recipe} The recipe instance.
|
|
1016
|
+
* @throws {Error} If there is no active page content context.
|
|
232
1017
|
*/
|
|
233
1018
|
exports.pauseContext = function pauseContext() {
|
|
234
|
-
if (this.
|
|
1019
|
+
if (this.contextState === "active-edit") {
|
|
235
1020
|
this.page.endContext();
|
|
236
|
-
|
|
237
|
-
|
|
1021
|
+
this.pageContext = null;
|
|
1022
|
+
this.contextState = "paused-edit";
|
|
1023
|
+
} else if (this.contextState === "active-new") {
|
|
238
1024
|
this.writer.pausePageContentContext(this.pageContext);
|
|
1025
|
+
this.contextState = "paused-new";
|
|
1026
|
+
} else {
|
|
1027
|
+
throw new Error("No active page content context to pause");
|
|
239
1028
|
}
|
|
1029
|
+
return this;
|
|
240
1030
|
};
|
|
241
1031
|
|
|
242
1032
|
/**
|
|
@@ -244,13 +1034,20 @@ exports.pauseContext = function pauseContext() {
|
|
|
244
1034
|
* @name resumeContext
|
|
245
1035
|
* @function
|
|
246
1036
|
* @memberof Recipe#
|
|
247
|
-
* @returns {
|
|
1037
|
+
* @returns {Recipe} The recipe instance.
|
|
1038
|
+
* @throws {Error} If there is no paused page content context.
|
|
248
1039
|
*/
|
|
249
1040
|
exports.resumeContext = function resumeContext() {
|
|
250
|
-
if (
|
|
1041
|
+
if (this.contextState === "paused-edit") {
|
|
251
1042
|
this.pageContext = this.page.startContext().getContext();
|
|
252
1043
|
this._resumePageRotation();
|
|
1044
|
+
this.contextState = "active-edit";
|
|
1045
|
+
} else if (this.contextState === "paused-new") {
|
|
1046
|
+
this.contextState = "active-new";
|
|
1047
|
+
} else {
|
|
1048
|
+
throw new Error("No paused page content context to resume");
|
|
253
1049
|
}
|
|
1050
|
+
return this;
|
|
254
1051
|
};
|
|
255
1052
|
|
|
256
1053
|
/**
|