@muhammara/native-core 7.0.0-beta.3 → 7.0.0-rc.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 +57 -7
- package/THIRD_PARTY_NOTICES.md +39 -0
- package/index.js +120 -1
- package/lib/PDFRStreamForBuffer.js +52 -7
- package/lib/PDFRStreamForFile.js +56 -7
- package/lib/PDFStreamForResponse.js +22 -5
- package/lib/PDFWStreamForBuffer.js +49 -10
- package/lib/PDFWStreamForFile.js +26 -5
- package/lib/Recipe.js +86 -30
- package/lib/content-stream.js +503 -0
- package/lib/css-colors.js +149 -0
- package/lib/font-text.js +1461 -0
- package/lib/glyph-list.js +1127 -0
- package/lib/recipe/annotation.js +482 -105
- package/lib/recipe/appendPage.js +8 -1
- package/lib/recipe/colors.js +132 -35
- package/lib/recipe/coordinate.js +82 -16
- package/lib/recipe/encrypt.js +32 -13
- package/lib/recipe/font.js +70 -25
- package/lib/recipe/graphics-state.js +3 -2
- package/lib/recipe/htmlToTextObjects.js +154 -21
- package/lib/recipe/image.js +34 -9
- package/lib/recipe/info.js +93 -22
- package/lib/recipe/insertPage.js +12 -3
- package/lib/recipe/overlay.js +4 -2
- package/lib/recipe/page-references.js +436 -0
- package/lib/recipe/page.js +379 -275
- package/lib/recipe/parameters.js +11 -2
- package/lib/recipe/replaceText.js +464 -30
- package/lib/recipe/shapes.js +418 -45
- package/lib/recipe/split.js +6 -2
- package/lib/recipe/table.js +260 -173
- package/lib/recipe/text.helper.js +176 -10
- package/lib/recipe/text.js +420 -115
- package/lib/recipe/utils.js +66 -3
- package/lib/recipe/vector-line.js +101 -39
- package/lib/recipe/vector-polygon.js +18 -6
- package/lib/recipe/vector.helper.js +150 -13
- package/lib/recipe/vector.js +150 -53
- package/lib/recipe/xObjectForm.js +73 -10
- package/lib/recipe-constants.js +373 -0
- package/muhammara.d.ts +3487 -152
- package/package.json +1 -1
package/lib/recipe/page.js
CHANGED
|
@@ -1,6 +1,40 @@
|
|
|
1
1
|
const muhammara = require("../muhammara");
|
|
2
|
+
const { PAGE_CONTEXT_STATE } = require("./utils");
|
|
3
|
+
const { PageLayout, Colorspace } = require("../recipe-constants");
|
|
4
|
+
const {
|
|
5
|
+
planDeletedPageReferences,
|
|
6
|
+
pruneDeletedPageReferences,
|
|
7
|
+
setCatalogEntry,
|
|
8
|
+
} = require("./page-references");
|
|
2
9
|
|
|
3
|
-
|
|
10
|
+
// PDF dictionary keys and names the page-tree and page-label code reads.
|
|
11
|
+
const PdfName = Object.freeze({
|
|
12
|
+
PAGES: "Pages",
|
|
13
|
+
KIDS: "Kids",
|
|
14
|
+
COUNT: "Count",
|
|
15
|
+
PARENT: "Parent",
|
|
16
|
+
PAGE_LABELS: "PageLabels",
|
|
17
|
+
LIMITS: "Limits",
|
|
18
|
+
NUMS: "Nums",
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Build the retained page tree while marking deleted leaf pages.
|
|
23
|
+
* @private
|
|
24
|
+
* @param {Object} parser - The source PDF parser.
|
|
25
|
+
* @param {number} objectID - The object ID of the Pages node to read.
|
|
26
|
+
* @param {Set<number>} deletedPages - One-based page numbers to delete.
|
|
27
|
+
* @param {Set<number>} modifiedPageIDs - Object IDs of pages edited in this Recipe.
|
|
28
|
+
* @param {Object} pageState - Running state: pageNumber, deletedPageIDs and
|
|
29
|
+
* retainedPageIDs, updated as leaves are visited.
|
|
30
|
+
* @param {number} [generation=0] - The generation of the node's reference.
|
|
31
|
+
* @param {Set<number>} [visited] - Pages nodes already read, to reject cycles.
|
|
32
|
+
* @param {number} [depth=0] - The nesting depth, limited to 1000.
|
|
33
|
+
* @returns {Object} The node: objectID, generation, values, retained
|
|
34
|
+
* children, retained page count and whether it changed.
|
|
35
|
+
* @throws {Error} If the page tree is cyclic, too deep or inconsistent, or a
|
|
36
|
+
* modified page has a nonzero generation.
|
|
37
|
+
*/
|
|
4
38
|
function readPageTree(
|
|
5
39
|
parser,
|
|
6
40
|
objectID,
|
|
@@ -20,11 +54,21 @@ function readPageTree(
|
|
|
20
54
|
if (depth === 0 && values.Parent !== undefined) {
|
|
21
55
|
throw new Error("deletePage requires a valid page tree");
|
|
22
56
|
}
|
|
23
|
-
const kids = parser.queryDictionaryObject(dictionary, "Kids")
|
|
57
|
+
const kids = parser.queryDictionaryObject(dictionary, "Kids")?.toPDFArray();
|
|
58
|
+
if (!kids) {
|
|
59
|
+
throw new Error("deletePage requires a valid page tree");
|
|
60
|
+
}
|
|
24
61
|
const mappedChildren = kids.toJSArray().map((entry) => {
|
|
62
|
+
// Kids must be references to page tree dictionaries with a /Type.
|
|
25
63
|
const reference = entry.toPDFIndirectObjectReference();
|
|
26
|
-
const childID = reference
|
|
27
|
-
const childDictionary =
|
|
64
|
+
const childID = reference?.getObjectID();
|
|
65
|
+
const childDictionary =
|
|
66
|
+
childID === undefined
|
|
67
|
+
? null
|
|
68
|
+
: parser.parseNewObject(childID)?.toPDFDictionary();
|
|
69
|
+
if (!childDictionary) {
|
|
70
|
+
throw new Error("deletePage requires a valid page tree");
|
|
71
|
+
}
|
|
28
72
|
const childValues = childDictionary.toJSObject();
|
|
29
73
|
const parentReference = childValues.Parent?.toPDFIndirectObjectReference();
|
|
30
74
|
if (
|
|
@@ -34,8 +78,11 @@ function readPageTree(
|
|
|
34
78
|
) {
|
|
35
79
|
throw new Error("deletePage requires a valid page tree");
|
|
36
80
|
}
|
|
37
|
-
const type = childValues.Type
|
|
38
|
-
if (type ===
|
|
81
|
+
const type = childValues.Type?.toPDFName()?.value;
|
|
82
|
+
if (type === undefined) {
|
|
83
|
+
throw new Error("deletePage requires a valid page tree");
|
|
84
|
+
}
|
|
85
|
+
if (type === PdfName.PAGES) {
|
|
39
86
|
return readPageTree(
|
|
40
87
|
parser,
|
|
41
88
|
childID,
|
|
@@ -83,7 +130,15 @@ function readPageTree(
|
|
|
83
130
|
};
|
|
84
131
|
}
|
|
85
132
|
|
|
86
|
-
/**
|
|
133
|
+
/**
|
|
134
|
+
* Write changed page-tree nodes back to the modified PDF with their new
|
|
135
|
+
* Count and Kids.
|
|
136
|
+
* @private
|
|
137
|
+
* @param {Object} writer - The PDF writer.
|
|
138
|
+
* @param {Object} copyingContext - Copies the unchanged entries.
|
|
139
|
+
* @param {Object} node - A node built by readPageTree().
|
|
140
|
+
* @returns {void}
|
|
141
|
+
*/
|
|
87
142
|
function writePageTree(writer, copyingContext, node) {
|
|
88
143
|
node.children
|
|
89
144
|
.filter((child) => child.children && child.changed)
|
|
@@ -94,13 +149,13 @@ function writePageTree(writer, copyingContext, node) {
|
|
|
94
149
|
objectsContext.startModifiedIndirectObject(node.objectID);
|
|
95
150
|
const dictionary = objectsContext.startDictionary();
|
|
96
151
|
Object.keys(node.values).forEach((key) => {
|
|
97
|
-
if (key ===
|
|
152
|
+
if (key === PdfName.COUNT || key === PdfName.KIDS) return;
|
|
98
153
|
dictionary.writeKey(key);
|
|
99
154
|
copyingContext.copyDirectObjectAsIs(node.values[key]);
|
|
100
155
|
});
|
|
101
|
-
dictionary.writeKey(
|
|
156
|
+
dictionary.writeKey(PdfName.COUNT);
|
|
102
157
|
objectsContext.writeNumber(node.count);
|
|
103
|
-
dictionary.writeKey(
|
|
158
|
+
dictionary.writeKey(PdfName.KIDS);
|
|
104
159
|
objectsContext.startArray();
|
|
105
160
|
node.children.forEach((child) => {
|
|
106
161
|
objectsContext.writeIndirectObjectReference(
|
|
@@ -116,6 +171,10 @@ function writePageTree(writer, copyingContext, node) {
|
|
|
116
171
|
* alike - depth-first. Shared by every deletePage() pass that needs to walk
|
|
117
172
|
* the tree readPageTree() already built, instead of re-parsing or re-walking
|
|
118
173
|
* it independently.
|
|
174
|
+
* @private
|
|
175
|
+
* @param {Object} tree - The root node built by readPageTree().
|
|
176
|
+
* @param {function(Object): void} visit - Called with every node.
|
|
177
|
+
* @returns {void}
|
|
119
178
|
*/
|
|
120
179
|
function walkPageTree(tree, visit) {
|
|
121
180
|
const pending = [tree];
|
|
@@ -126,15 +185,17 @@ function walkPageTree(tree, visit) {
|
|
|
126
185
|
}
|
|
127
186
|
}
|
|
128
187
|
|
|
129
|
-
/**
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
188
|
+
/**
|
|
189
|
+
* Reject page-tree and page-label objects that cannot be rewritten safely:
|
|
190
|
+
* changed objects must have generation 0. The catalog is never rewritten in
|
|
191
|
+
* place, so its generation does not matter.
|
|
192
|
+
* @private
|
|
193
|
+
* @param {Object} tree - The root node built by readPageTree().
|
|
194
|
+
* @param {Object|null} pageLabels - The result of preparePageLabels().
|
|
195
|
+
* @returns {void}
|
|
196
|
+
* @throws {Error} If a changed object has a nonzero generation.
|
|
197
|
+
*/
|
|
198
|
+
function assertSupportedModifiedGenerations(tree, pageLabels) {
|
|
138
199
|
walkPageTree(tree, (node) => {
|
|
139
200
|
if (!node.children) return;
|
|
140
201
|
if (node.changed && node.generation !== 0) {
|
|
@@ -148,162 +209,20 @@ function assertSupportedModifiedGenerations(tree, pageLabels, root, writer) {
|
|
|
148
209
|
"deletePage does not support rewriting nonzero-generation objects",
|
|
149
210
|
);
|
|
150
211
|
}
|
|
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
212
|
}
|
|
305
213
|
|
|
306
|
-
/**
|
|
214
|
+
/**
|
|
215
|
+
* Collect the entries of a page-label number tree.
|
|
216
|
+
* @private
|
|
217
|
+
* @param {Object} parser - The source PDF parser.
|
|
218
|
+
* @param {Object} dictionary - The number-tree node.
|
|
219
|
+
* @param {Object[]} entries - Receives {index, value} for every label.
|
|
220
|
+
* @param {Set<number>} [visited] - Nodes already read, to reject cycles.
|
|
221
|
+
* @param {number} [objectID=0] - The object ID of this node, if indirect.
|
|
222
|
+
* @param {number} [depth=0] - The nesting depth, limited to 1000.
|
|
223
|
+
* @returns {Object} The entries of this node's dictionary.
|
|
224
|
+
* @throws {Error} If the tree is cyclic, too deep or has invalid entries.
|
|
225
|
+
*/
|
|
307
226
|
function collectPageLabels(
|
|
308
227
|
parser,
|
|
309
228
|
dictionary,
|
|
@@ -335,7 +254,7 @@ function collectPageLabels(
|
|
|
335
254
|
}
|
|
336
255
|
if (values.Kids) {
|
|
337
256
|
parser
|
|
338
|
-
.queryDictionaryObject(dictionary,
|
|
257
|
+
.queryDictionaryObject(dictionary, PdfName.KIDS)
|
|
339
258
|
.toPDFArray()
|
|
340
259
|
.toJSArray()
|
|
341
260
|
.forEach((entry) => {
|
|
@@ -353,7 +272,14 @@ function collectPageLabels(
|
|
|
353
272
|
return values;
|
|
354
273
|
}
|
|
355
274
|
|
|
356
|
-
/**
|
|
275
|
+
/**
|
|
276
|
+
* Follow a chain of indirect references to the object it ends at.
|
|
277
|
+
* @private
|
|
278
|
+
* @param {Object} parser - The source PDF parser.
|
|
279
|
+
* @param {Object} value - The PDF object or reference.
|
|
280
|
+
* @returns {Object} The resolved object.
|
|
281
|
+
* @throws {Error} If the chain is cyclic or longer than 1000 references.
|
|
282
|
+
*/
|
|
357
283
|
function resolvePageLabelObject(parser, value) {
|
|
358
284
|
const visited = new Set();
|
|
359
285
|
let resolved = value;
|
|
@@ -368,7 +294,16 @@ function resolvePageLabelObject(parser, value) {
|
|
|
368
294
|
return resolved;
|
|
369
295
|
}
|
|
370
296
|
|
|
371
|
-
/**
|
|
297
|
+
/**
|
|
298
|
+
* Read a page-label dictionary into a plain value.
|
|
299
|
+
* @private
|
|
300
|
+
* @param {Object} parser - The source PDF parser.
|
|
301
|
+
* @param {Object} value - The page-label dictionary.
|
|
302
|
+
* @returns {{style: (string|undefined), prefix: (Object|undefined),
|
|
303
|
+
* start: (number|undefined)}} The label style name, prefix bytes with their
|
|
304
|
+
* string kind, and start number.
|
|
305
|
+
* @throws {Error} If the value is not a dictionary.
|
|
306
|
+
*/
|
|
372
307
|
function readPageLabel(parser, value) {
|
|
373
308
|
const dictionary = value?.toPDFDictionary();
|
|
374
309
|
if (!dictionary) {
|
|
@@ -380,6 +315,9 @@ function readPageLabel(parser, value) {
|
|
|
380
315
|
const start = values.St ? resolvePageLabelObject(parser, values.St) : null;
|
|
381
316
|
const prefixHex = prefix?.toPDFHexString();
|
|
382
317
|
const prefixLiteral = prefix?.toPDFLiteralString();
|
|
318
|
+
if (values.P !== undefined && !prefixHex && !prefixLiteral) {
|
|
319
|
+
throw new Error("deletePage requires valid PageLabels entries");
|
|
320
|
+
}
|
|
383
321
|
return {
|
|
384
322
|
style: style?.toPDFName()?.value,
|
|
385
323
|
prefix:
|
|
@@ -393,7 +331,13 @@ function readPageLabel(parser, value) {
|
|
|
393
331
|
};
|
|
394
332
|
}
|
|
395
333
|
|
|
396
|
-
/**
|
|
334
|
+
/**
|
|
335
|
+
* Write one indirect page-label dictionary per entry.
|
|
336
|
+
* @private
|
|
337
|
+
* @param {Object} objectsContext - The writer objects context.
|
|
338
|
+
* @param {Object[]} entries - The normalized {index, value} entries.
|
|
339
|
+
* @returns {Object[]} The {index, objectID} of every written label.
|
|
340
|
+
*/
|
|
397
341
|
function writePageLabelObjects(objectsContext, entries) {
|
|
398
342
|
return entries.map((entry) => {
|
|
399
343
|
const objectID = objectsContext.startNewIndirectObject();
|
|
@@ -417,7 +361,17 @@ function writePageLabelObjects(objectsContext, entries) {
|
|
|
417
361
|
});
|
|
418
362
|
}
|
|
419
363
|
|
|
420
|
-
/**
|
|
364
|
+
/**
|
|
365
|
+
* Write a flat page-label number tree: the copied extra entries and a Nums
|
|
366
|
+
* array referencing the written labels.
|
|
367
|
+
* @private
|
|
368
|
+
* @param {Object} objectsContext - The writer objects context.
|
|
369
|
+
* @param {Object} copyingContext - Copies the extra entries.
|
|
370
|
+
* @param {Object} dictionary - The dictionary context to write into.
|
|
371
|
+
* @param {Object} values - The source number-tree entries.
|
|
372
|
+
* @param {Object[]} entries - The {index, objectID} of every label.
|
|
373
|
+
* @returns {void}
|
|
374
|
+
*/
|
|
421
375
|
function writePageLabelsDictionary(
|
|
422
376
|
objectsContext,
|
|
423
377
|
copyingContext,
|
|
@@ -426,11 +380,12 @@ function writePageLabelsDictionary(
|
|
|
426
380
|
entries,
|
|
427
381
|
) {
|
|
428
382
|
Object.keys(values).forEach((key) => {
|
|
429
|
-
if (key ===
|
|
383
|
+
if (key === PdfName.KIDS || key === PdfName.LIMITS || key === PdfName.NUMS)
|
|
384
|
+
return;
|
|
430
385
|
dictionary.writeKey(key);
|
|
431
386
|
copyingContext.copyDirectObjectAsIs(values[key]);
|
|
432
387
|
});
|
|
433
|
-
dictionary.writeKey(
|
|
388
|
+
dictionary.writeKey(PdfName.NUMS);
|
|
434
389
|
objectsContext.startArray();
|
|
435
390
|
entries.forEach((entry) => {
|
|
436
391
|
objectsContext.writeNumber(entry.index);
|
|
@@ -439,7 +394,18 @@ function writePageLabelsDictionary(
|
|
|
439
394
|
objectsContext.endArray();
|
|
440
395
|
}
|
|
441
396
|
|
|
442
|
-
/**
|
|
397
|
+
/**
|
|
398
|
+
* Reindex the page labels for the pages that remain after deletion, keeping
|
|
399
|
+
* each label's numbering continuous.
|
|
400
|
+
* @private
|
|
401
|
+
* @param {Object} parser - The source PDF parser.
|
|
402
|
+
* @param {Object} catalogDictionary - The catalog dictionary.
|
|
403
|
+
* @param {Set<number>} deletedPages - One-based page numbers to delete.
|
|
404
|
+
* @param {number} sourcePageCount - The source page count.
|
|
405
|
+
* @returns {Object|null} The PageLabels reference, the number tree values
|
|
406
|
+
* and the normalized entries; null without page labels.
|
|
407
|
+
* @throws {Error} If PageLabels is not a valid number tree.
|
|
408
|
+
*/
|
|
443
409
|
function preparePageLabels(
|
|
444
410
|
parser,
|
|
445
411
|
catalogDictionary,
|
|
@@ -508,15 +474,21 @@ function preparePageLabels(
|
|
|
508
474
|
}
|
|
509
475
|
});
|
|
510
476
|
return {
|
|
511
|
-
catalogValues,
|
|
512
477
|
reference,
|
|
513
478
|
values,
|
|
514
479
|
entries: normalized,
|
|
515
480
|
};
|
|
516
481
|
}
|
|
517
482
|
|
|
518
|
-
/**
|
|
519
|
-
|
|
483
|
+
/**
|
|
484
|
+
* Write the updated page labels and attach them to the catalog.
|
|
485
|
+
* @private
|
|
486
|
+
* @param {Object} writer - The PDF writer.
|
|
487
|
+
* @param {Object} copyingContext - Copies unchanged entries.
|
|
488
|
+
* @param {Object|null} pageLabels - The result of preparePageLabels().
|
|
489
|
+
* @returns {void}
|
|
490
|
+
*/
|
|
491
|
+
function writePageLabels(writer, copyingContext, pageLabels) {
|
|
520
492
|
if (!pageLabels) return;
|
|
521
493
|
|
|
522
494
|
const objectsContext = writer.getObjectsContext();
|
|
@@ -548,21 +520,7 @@ function writePageLabels(writer, copyingContext, rootID, pageLabels) {
|
|
|
548
520
|
);
|
|
549
521
|
objectsContext.endDictionary(labelsDictionary).endIndirectObject();
|
|
550
522
|
|
|
551
|
-
|
|
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();
|
|
523
|
+
setCatalogEntry(writer, PdfName.PAGE_LABELS, labelsObjectID);
|
|
566
524
|
}
|
|
567
525
|
|
|
568
526
|
/**
|
|
@@ -572,8 +530,9 @@ function writePageLabels(writer, copyingContext, rootID, pageLabels) {
|
|
|
572
530
|
* @name createPage
|
|
573
531
|
* @function
|
|
574
532
|
* @memberof Recipe#
|
|
575
|
-
* @param {number|
|
|
576
|
-
* Known named medium sizes: executive, folio, legal, letter, ledger, tabloid, a0-a10, b0-b10, c0-c10, ra0-ra4, sra0-
|
|
533
|
+
* @param {number|Recipe.PageSize} [pageWidth] - The page width, or a `Recipe.PageSize` name.
|
|
534
|
+
* Known named medium sizes: executive, folio, legal, letter, ledger, tabloid, a0-a10, b0-b10, c0-c10, ra0-ra4, sra0-sra4.
|
|
535
|
+
* Unknown names use the default letter size.
|
|
577
536
|
* @param {number} [pageHeight] - The page height, or rotation (90) when page size name given.
|
|
578
537
|
* @param {object} [margins] - page margin definitions.
|
|
579
538
|
* @param {number} [margins.left] - Left margin.
|
|
@@ -581,6 +540,7 @@ function writePageLabels(writer, copyingContext, rootID, pageLabels) {
|
|
|
581
540
|
* @param {number} [margins.top] - Top margin.
|
|
582
541
|
* @param {number} [margins.bottom] - Bottom margin.
|
|
583
542
|
* @returns {Recipe} The recipe instance.
|
|
543
|
+
* @throws {Error} If pages were deleted with deletePage() on this Recipe.
|
|
584
544
|
*/
|
|
585
545
|
exports.createPage = function createPage(pageWidth, pageHeight, margins) {
|
|
586
546
|
if (this.deletedPages?.size) {
|
|
@@ -615,10 +575,12 @@ exports.createPage = function createPage(pageWidth, pageHeight, margins) {
|
|
|
615
575
|
}
|
|
616
576
|
}
|
|
617
577
|
// from 0
|
|
618
|
-
this.metadata.pageCount
|
|
578
|
+
this.metadata.pageCount =
|
|
579
|
+
(this.metadata.pageCount ?? this.metadata.pages ?? 0) + 1;
|
|
619
580
|
const pageNumber = this.metadata.pageCount;
|
|
620
581
|
const dimensions = [0, 0, pageWidth, pageHeight];
|
|
621
|
-
const layout =
|
|
582
|
+
const layout =
|
|
583
|
+
pageWidth > pageHeight ? PageLayout.LANDSCAPE : PageLayout.PORTRAIT;
|
|
622
584
|
this.metadata[pageNumber] = {
|
|
623
585
|
pageNumber,
|
|
624
586
|
mediaBox: dimensions,
|
|
@@ -635,7 +597,7 @@ exports.createPage = function createPage(pageWidth, pageHeight, margins) {
|
|
|
635
597
|
this.pageNumber = pageNumber;
|
|
636
598
|
this.pageContext = this.writer.startPageContentContext(this.page);
|
|
637
599
|
this.editingPage = false;
|
|
638
|
-
this.contextState =
|
|
600
|
+
this.contextState = PAGE_CONTEXT_STATE.ACTIVE_NEW;
|
|
639
601
|
if (!this.isNewPDF) this.pagesCreated = true;
|
|
640
602
|
|
|
641
603
|
if (margins) {
|
|
@@ -646,15 +608,55 @@ exports.createPage = function createPage(pageWidth, pageHeight, margins) {
|
|
|
646
608
|
return this;
|
|
647
609
|
};
|
|
648
610
|
|
|
611
|
+
// setPageBox() also accepts the PageBox names.
|
|
612
|
+
const PAGE_BOX_CONSTANTS = {
|
|
613
|
+
[muhammara.PageBox.MEDIA]: muhammara.ePDFPageBoxMediaBox,
|
|
614
|
+
[muhammara.PageBox.CROP]: muhammara.ePDFPageBoxCropBox,
|
|
615
|
+
[muhammara.PageBox.BLEED]: muhammara.ePDFPageBoxBleedBox,
|
|
616
|
+
[muhammara.PageBox.TRIM]: muhammara.ePDFPageBoxTrimBox,
|
|
617
|
+
[muhammara.PageBox.ART]: muhammara.ePDFPageBoxArtBox,
|
|
618
|
+
};
|
|
619
|
+
|
|
620
|
+
/**
|
|
621
|
+
* Validate a page rotation. PDF allows only multiples of 90 degrees.
|
|
622
|
+
* @private
|
|
623
|
+
* @param {*} page - The active page, if any.
|
|
624
|
+
* @param {*} rotation - The requested rotation.
|
|
625
|
+
* @throws {TypeError} If no page is active or `rotation` is not a number.
|
|
626
|
+
* @throws {RangeError} If `rotation` is not a multiple of 90.
|
|
627
|
+
*/
|
|
628
|
+
function checkRotation(page, rotation) {
|
|
629
|
+
if (!page) {
|
|
630
|
+
throw new TypeError("rotate requires an active page");
|
|
631
|
+
}
|
|
632
|
+
if (typeof rotation !== "number") {
|
|
633
|
+
throw new TypeError("Rotation is not set to a number");
|
|
634
|
+
}
|
|
635
|
+
if (!Number.isInteger(rotation / 90)) {
|
|
636
|
+
throw new RangeError("Rotation must be a multiple of 90 degrees");
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
|
|
649
640
|
/**
|
|
650
641
|
* Set the rotation of the current page.
|
|
651
642
|
* @name rotate
|
|
652
643
|
* @function
|
|
653
644
|
* @memberof Recipe#
|
|
654
|
-
* @param {number} rotation - The page rotation in degrees.
|
|
645
|
+
* @param {number} rotation - The page rotation in degrees, a multiple of 90.
|
|
655
646
|
* @returns {Recipe} The recipe instance.
|
|
647
|
+
* @throws {TypeError} If no page is active or `rotation` is not a number.
|
|
648
|
+
* @throws {RangeError} If `rotation` is not a multiple of 90.
|
|
649
|
+
* @throws {Error} If the active page was opened with `editPage()`.
|
|
656
650
|
*/
|
|
657
651
|
exports.rotate = function rotate(rotation) {
|
|
652
|
+
// A page modifier keeps the source page's /Rotate, so the rotation would
|
|
653
|
+
// only change Recipe's coordinate bookkeeping.
|
|
654
|
+
if (this.editingPage) {
|
|
655
|
+
throw new Error(
|
|
656
|
+
"rotate() is only available on pages created with createPage()",
|
|
657
|
+
);
|
|
658
|
+
}
|
|
659
|
+
checkRotation(this.page, rotation);
|
|
658
660
|
this.page.rotate = rotation;
|
|
659
661
|
this.metadata[this.pageNumber].rotate = rotation;
|
|
660
662
|
return this;
|
|
@@ -665,15 +667,20 @@ exports.rotate = function rotate(rotation) {
|
|
|
665
667
|
* @name setPageBox
|
|
666
668
|
* @function
|
|
667
669
|
* @memberof Recipe#
|
|
668
|
-
* @param {number} box - An `ePDFPageBox*` constant.
|
|
670
|
+
* @param {number|string} box - An `ePDFPageBox*` constant or a `PageBox` name.
|
|
669
671
|
* @param {number} left - The PDF left coordinate.
|
|
670
672
|
* @param {number} bottom - The PDF bottom coordinate.
|
|
671
673
|
* @param {number} right - The PDF right coordinate.
|
|
672
674
|
* @param {number} top - The PDF top coordinate.
|
|
673
675
|
* @returns {Recipe} The recipe instance.
|
|
674
676
|
* @throws {RangeError} If the page box constant is unknown.
|
|
677
|
+
* @throws {TypeError} If no page is active.
|
|
675
678
|
*/
|
|
676
679
|
exports.setPageBox = function setPageBox(box, left, bottom, right, top) {
|
|
680
|
+
box = PAGE_BOX_CONSTANTS[box] ?? box;
|
|
681
|
+
if ([left, bottom, right, top].some((value) => typeof value === "bigint")) {
|
|
682
|
+
throw new TypeError("setPageBox coordinates must be numbers");
|
|
683
|
+
}
|
|
677
684
|
const boxes = {
|
|
678
685
|
[muhammara.ePDFPageBoxMediaBox]: "mediaBox",
|
|
679
686
|
[muhammara.ePDFPageBoxCropBox]: "cropBox",
|
|
@@ -695,18 +702,19 @@ exports.setPageBox = function setPageBox(box, left, bottom, right, top) {
|
|
|
695
702
|
mediaBox: pageBox,
|
|
696
703
|
width,
|
|
697
704
|
height,
|
|
698
|
-
layout: width > height ?
|
|
705
|
+
layout: width > height ? PageLayout.LANDSCAPE : PageLayout.PORTRAIT,
|
|
699
706
|
});
|
|
700
707
|
}
|
|
701
708
|
return this;
|
|
702
709
|
};
|
|
703
710
|
|
|
704
711
|
/**
|
|
705
|
-
* Finish a page
|
|
712
|
+
* Finish a page. Without an active page this does nothing.
|
|
706
713
|
* @name endPage
|
|
707
714
|
* @function
|
|
708
715
|
* @memberof Recipe#
|
|
709
716
|
* @returns {Recipe} The recipe instance.
|
|
717
|
+
* @throws {Error} If the page cannot be written.
|
|
710
718
|
*/
|
|
711
719
|
exports.endPage = function endPage() {
|
|
712
720
|
if (!this.page) {
|
|
@@ -714,7 +722,8 @@ exports.endPage = function endPage() {
|
|
|
714
722
|
}
|
|
715
723
|
|
|
716
724
|
if (this.page.endContext) {
|
|
717
|
-
if (this.contextState ===
|
|
725
|
+
if (this.contextState === PAGE_CONTEXT_STATE.ACTIVE_EDIT)
|
|
726
|
+
this.page.endContext();
|
|
718
727
|
this.page.writePage();
|
|
719
728
|
} else {
|
|
720
729
|
this.writer.writePage(this.page);
|
|
@@ -723,7 +732,7 @@ exports.endPage = function endPage() {
|
|
|
723
732
|
this.pageContext = null;
|
|
724
733
|
this.pageNumber = 0;
|
|
725
734
|
this.editingPage = false;
|
|
726
|
-
this.contextState =
|
|
735
|
+
this.contextState = PAGE_CONTEXT_STATE.IDLE;
|
|
727
736
|
|
|
728
737
|
return this;
|
|
729
738
|
};
|
|
@@ -733,10 +742,17 @@ exports.endPage = function endPage() {
|
|
|
733
742
|
* @name editPage
|
|
734
743
|
* @function
|
|
735
744
|
* @memberof Recipe#
|
|
736
|
-
* @param {number} pageNumber - The page number to be edited.
|
|
745
|
+
* @param {number} pageNumber - The one-based page number to be edited.
|
|
737
746
|
* @returns {Recipe} The recipe instance.
|
|
747
|
+
* @throws {Error} If the Recipe was not constructed from an existing PDF.
|
|
748
|
+
* @throws {Error} If the page does not exist in the source PDF.
|
|
738
749
|
*/
|
|
739
750
|
exports.editPage = function editPage(pageNumber) {
|
|
751
|
+
if (this.isNewPDF) {
|
|
752
|
+
throw new Error(
|
|
753
|
+
"editPage requires a Recipe constructed from an existing PDF",
|
|
754
|
+
);
|
|
755
|
+
}
|
|
740
756
|
const pdfWriter = this.writer;
|
|
741
757
|
const pageIndex = pageNumber - 1;
|
|
742
758
|
const pageModifier = new muhammara.PDFPageModifier(
|
|
@@ -748,7 +764,7 @@ exports.editPage = function editPage(pageNumber) {
|
|
|
748
764
|
this.pageNumber = pageNumber;
|
|
749
765
|
this.pageContext = pageModifier.startContext().getContext();
|
|
750
766
|
this.editingPage = true;
|
|
751
|
-
this.contextState =
|
|
767
|
+
this.contextState = PAGE_CONTEXT_STATE.ACTIVE_EDIT;
|
|
752
768
|
this.modifiedSourcePages.add(pageNumber);
|
|
753
769
|
|
|
754
770
|
this._resumePageRotation(pageNumber);
|
|
@@ -759,9 +775,9 @@ exports.editPage = function editPage(pageNumber) {
|
|
|
759
775
|
const startX = mediaBox[0];
|
|
760
776
|
const startY = mediaBox[1];
|
|
761
777
|
const textOptions = {
|
|
762
|
-
font: this.writer.getFontForFile(this.fonts
|
|
778
|
+
font: this.writer.getFontForFile(this.fonts.helvetica.b),
|
|
763
779
|
size: 50,
|
|
764
|
-
colorspace:
|
|
780
|
+
colorspace: Colorspace.GRAY,
|
|
765
781
|
color: 0x00,
|
|
766
782
|
};
|
|
767
783
|
context.writeText(
|
|
@@ -798,21 +814,105 @@ exports.editPage = function editPage(pageNumber) {
|
|
|
798
814
|
return this;
|
|
799
815
|
};
|
|
800
816
|
|
|
817
|
+
/**
|
|
818
|
+
* Read the page tree and check that the queued deletions can be applied:
|
|
819
|
+
* the page tree and page labels can be rewritten, and every retained
|
|
820
|
+
* reference to a deleted page can be pruned or there is none.
|
|
821
|
+
* @private
|
|
822
|
+
* @param {Object} recipe - The Recipe.
|
|
823
|
+
* @param {Object} copyingContext - A copying context for the modified file.
|
|
824
|
+
* @param {Set<number>} deletedPages - One-based page numbers to delete.
|
|
825
|
+
* @param {boolean} prune - Whether to prune references to deleted pages.
|
|
826
|
+
* @returns {Object} The page tree, page labels, catalog entries, deleted
|
|
827
|
+
* page object IDs and the pruning plan.
|
|
828
|
+
* @throws {Error} If the deletion cannot be applied.
|
|
829
|
+
*/
|
|
830
|
+
function planPageDeletion(recipe, copyingContext, deletedPages, prune) {
|
|
831
|
+
const parser = copyingContext.getSourceDocumentParser();
|
|
832
|
+
const trailer = parser.getTrailer().toJSObject();
|
|
833
|
+
const rootID = trailer.Root.toPDFIndirectObjectReference().getObjectID();
|
|
834
|
+
const catalogDictionary = parser.parseNewObject(rootID).toPDFDictionary();
|
|
835
|
+
const catalog = catalogDictionary.toJSObject();
|
|
836
|
+
const pagesReference = catalog.Pages.toPDFIndirectObjectReference();
|
|
837
|
+
const pageLabels = preparePageLabels(
|
|
838
|
+
parser,
|
|
839
|
+
catalogDictionary,
|
|
840
|
+
deletedPages,
|
|
841
|
+
recipe.sourcePageCount,
|
|
842
|
+
);
|
|
843
|
+
const modifiedPageIDs = new Set(
|
|
844
|
+
Array.from(recipe.modifiedSourcePages, (pageNumber) =>
|
|
845
|
+
parser.getPageObjectID(pageNumber - 1),
|
|
846
|
+
),
|
|
847
|
+
);
|
|
848
|
+
const pageState = {
|
|
849
|
+
pageNumber: 0,
|
|
850
|
+
deletedPageIDs: new Set(),
|
|
851
|
+
retainedPageIDs: new Set(),
|
|
852
|
+
};
|
|
853
|
+
const tree = readPageTree(
|
|
854
|
+
parser,
|
|
855
|
+
pagesReference.getObjectID(),
|
|
856
|
+
deletedPages,
|
|
857
|
+
modifiedPageIDs,
|
|
858
|
+
pageState,
|
|
859
|
+
pagesReference.getVersion(),
|
|
860
|
+
);
|
|
861
|
+
pageState.retainedPageIDs.forEach((objectID) =>
|
|
862
|
+
pageState.deletedPageIDs.delete(objectID),
|
|
863
|
+
);
|
|
864
|
+
assertSupportedModifiedGenerations(tree, pageLabels);
|
|
865
|
+
const plan = planDeletedPageReferences(
|
|
866
|
+
parser,
|
|
867
|
+
{
|
|
868
|
+
catalog,
|
|
869
|
+
rootID,
|
|
870
|
+
tree,
|
|
871
|
+
pageLabels,
|
|
872
|
+
deletedPageIDs: pageState.deletedPageIDs,
|
|
873
|
+
modifiedPageIDs,
|
|
874
|
+
sourcePageCount: recipe.sourcePageCount,
|
|
875
|
+
},
|
|
876
|
+
prune,
|
|
877
|
+
);
|
|
878
|
+
return {
|
|
879
|
+
catalog,
|
|
880
|
+
tree,
|
|
881
|
+
pageLabels,
|
|
882
|
+
deletedPageIDs: pageState.deletedPageIDs,
|
|
883
|
+
plan,
|
|
884
|
+
};
|
|
885
|
+
}
|
|
886
|
+
|
|
801
887
|
/**
|
|
802
888
|
* Delete one or more pages from an existing PDF.
|
|
803
889
|
* Page numbers are one-based and refer to the original source document.
|
|
890
|
+
*
|
|
891
|
+
* By default a page that retained structures still reference - outline
|
|
892
|
+
* items, link annotations and named destinations, form widgets, tagged-PDF
|
|
893
|
+
* structure elements or the open action - cannot be deleted. With
|
|
894
|
+
* `pruneReferences`, those references are removed instead: a destination
|
|
895
|
+
* that targets a deleted page becomes null (so outline items keep their
|
|
896
|
+
* title and children, and links do nothing), and every other direct
|
|
897
|
+
* reference to a deleted page is dropped. Pruning applies to every queued
|
|
898
|
+
* deletion once any deletePage() call enables it.
|
|
804
899
|
* @name deletePage
|
|
805
900
|
* @function
|
|
806
901
|
* @memberof Recipe#
|
|
807
902
|
* @param {number|number[]} pageNumbers - Page number or page numbers to delete.
|
|
903
|
+
* @param {Object} [options] - Deletion options.
|
|
904
|
+
* @param {boolean} [options.pruneReferences=false] - Remove references to the
|
|
905
|
+
* deleted pages from retained structures instead of refusing the deletion.
|
|
808
906
|
* @returns {Recipe} The recipe instance.
|
|
907
|
+
* @throws {TypeError} If options is not an object or pruneReferences is not
|
|
908
|
+
* a boolean.
|
|
809
909
|
* @throws {RangeError} If a page number does not identify an original page.
|
|
810
910
|
* @throws {Error} If the Recipe has no existing source, has ended, would delete
|
|
811
|
-
* every page,
|
|
812
|
-
* retained
|
|
813
|
-
*
|
|
911
|
+
* every page, combines deletion with page composition, or the page tree,
|
|
912
|
+
* page labels or retained references cannot be rewritten. A failed call
|
|
913
|
+
* leaves the queued deletions unchanged.
|
|
814
914
|
*/
|
|
815
|
-
exports.deletePage = function deletePage(pageNumbers) {
|
|
915
|
+
exports.deletePage = function deletePage(pageNumbers, options) {
|
|
816
916
|
if (this.ended) {
|
|
817
917
|
throw new Error("Cannot delete a page after endPDF");
|
|
818
918
|
}
|
|
@@ -828,6 +928,16 @@ exports.deletePage = function deletePage(pageNumbers) {
|
|
|
828
928
|
if (this.pagesCreated) {
|
|
829
929
|
throw new Error("deletePage cannot be combined with createPage");
|
|
830
930
|
}
|
|
931
|
+
if (
|
|
932
|
+
options !== undefined &&
|
|
933
|
+
(options === null || typeof options !== "object" || Array.isArray(options))
|
|
934
|
+
) {
|
|
935
|
+
throw new TypeError("deletePage expects an options object");
|
|
936
|
+
}
|
|
937
|
+
const pruneReferences = options?.pruneReferences;
|
|
938
|
+
if (pruneReferences !== undefined && typeof pruneReferences !== "boolean") {
|
|
939
|
+
throw new TypeError("deletePage pruneReferences must be a boolean");
|
|
940
|
+
}
|
|
831
941
|
pageNumbers = Array.isArray(pageNumbers) ? pageNumbers : [pageNumbers];
|
|
832
942
|
const deletedPages = new Set(this.deletedPages || []);
|
|
833
943
|
pageNumbers.forEach((pageNumber) => {
|
|
@@ -843,67 +953,48 @@ exports.deletePage = function deletePage(pageNumbers) {
|
|
|
843
953
|
if (deletedPages.size >= this.sourcePageCount) {
|
|
844
954
|
throw new Error("At least one page must remain in the PDF");
|
|
845
955
|
}
|
|
956
|
+
const prune = Boolean(this.pruneReferences || pruneReferences);
|
|
957
|
+
const copyingContext = this.writer.createPDFCopyingContextForModifiedFile();
|
|
958
|
+
try {
|
|
959
|
+
planPageDeletion(this, copyingContext, deletedPages, prune);
|
|
960
|
+
} finally {
|
|
961
|
+
copyingContext.end();
|
|
962
|
+
}
|
|
846
963
|
this.deletedPages = deletedPages;
|
|
964
|
+
this.pruneReferences = prune;
|
|
847
965
|
return this;
|
|
848
966
|
};
|
|
849
967
|
|
|
850
|
-
/**
|
|
968
|
+
/**
|
|
969
|
+
* Apply the queued page deletions during finalization: rewrite the page tree
|
|
970
|
+
* and page labels, prune references to deleted pages when enabled, and
|
|
971
|
+
* renumber metadata and pending annotations.
|
|
972
|
+
* @private
|
|
973
|
+
* @returns {Recipe} The recipe instance.
|
|
974
|
+
* @throws {Error} If the page tree, page labels or retained references
|
|
975
|
+
* cannot be rewritten safely, for example after editing a page that holds
|
|
976
|
+
* a pruned reference.
|
|
977
|
+
*/
|
|
851
978
|
exports._deletePages = function _deletePages() {
|
|
852
979
|
if (!this.deletedPages?.size) return this;
|
|
853
980
|
|
|
854
981
|
const copyingContext = this.writer.createPDFCopyingContextForModifiedFile();
|
|
855
982
|
try {
|
|
856
|
-
const
|
|
857
|
-
|
|
858
|
-
|
|
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,
|
|
983
|
+
const deletion = planPageDeletion(
|
|
984
|
+
this,
|
|
985
|
+
copyingContext,
|
|
866
986
|
this.deletedPages,
|
|
867
|
-
this.
|
|
868
|
-
);
|
|
869
|
-
const modifiedPageIDs = new Set(
|
|
870
|
-
Array.from(this.modifiedSourcePages, (pageNumber) =>
|
|
871
|
-
parser.getPageObjectID(pageNumber - 1),
|
|
872
|
-
),
|
|
987
|
+
Boolean(this.pruneReferences),
|
|
873
988
|
);
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
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,
|
|
989
|
+
writePageTree(this.writer, copyingContext, deletion.tree);
|
|
990
|
+
writePageLabels(this.writer, copyingContext, deletion.pageLabels);
|
|
991
|
+
pruneDeletedPageReferences(
|
|
894
992
|
this.writer,
|
|
993
|
+
copyingContext,
|
|
994
|
+
deletion.catalog,
|
|
995
|
+
deletion.plan,
|
|
996
|
+
deletion.deletedPageIDs,
|
|
895
997
|
);
|
|
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
998
|
} finally {
|
|
908
999
|
copyingContext.end();
|
|
909
1000
|
}
|
|
@@ -940,6 +1031,15 @@ exports._deletePages = function _deletePages() {
|
|
|
940
1031
|
return this;
|
|
941
1032
|
};
|
|
942
1033
|
|
|
1034
|
+
/**
|
|
1035
|
+
* Apply the page rotation to an edited page's content context, so Recipe
|
|
1036
|
+
* coordinates stay upright on rotated source pages.
|
|
1037
|
+
* @private
|
|
1038
|
+
* @param {number} [pageNumber] - The one-based page number; defaults to the active page.
|
|
1039
|
+
* @param {Object} [context] - The content context; defaults to the page context.
|
|
1040
|
+
* @returns {Recipe} The recipe instance.
|
|
1041
|
+
* @throws {TypeError} If the page is unknown.
|
|
1042
|
+
*/
|
|
943
1043
|
exports._resumePageRotation = function _resumePageRotation(
|
|
944
1044
|
pageNumber,
|
|
945
1045
|
context,
|
|
@@ -981,11 +1081,15 @@ exports._resumePageRotation = function _resumePageRotation(
|
|
|
981
1081
|
* @name pageInfo
|
|
982
1082
|
* @function
|
|
983
1083
|
* @memberof Recipe#
|
|
984
|
-
* @param {number} pageNumber - The page number.
|
|
1084
|
+
* @param {number} pageNumber - The one-based page number.
|
|
985
1085
|
* @returns {RecipePageInfo} The page information.
|
|
1086
|
+
* @throws {TypeError} If the page is unknown.
|
|
986
1087
|
*/
|
|
987
1088
|
exports.pageInfo = function pageInfo(pageNumber) {
|
|
988
1089
|
const pageInfo = this.metadata[pageNumber];
|
|
1090
|
+
if (!pageInfo) {
|
|
1091
|
+
throw new TypeError(`Unknown page number: ${pageNumber}`);
|
|
1092
|
+
}
|
|
989
1093
|
return {
|
|
990
1094
|
width: pageInfo.width,
|
|
991
1095
|
height: pageInfo.height,
|
|
@@ -1016,13 +1120,13 @@ exports.getCurrentPageInfo = function getCurrentPageInfo() {
|
|
|
1016
1120
|
* @throws {Error} If there is no active page content context.
|
|
1017
1121
|
*/
|
|
1018
1122
|
exports.pauseContext = function pauseContext() {
|
|
1019
|
-
if (this.contextState ===
|
|
1123
|
+
if (this.contextState === PAGE_CONTEXT_STATE.ACTIVE_EDIT) {
|
|
1020
1124
|
this.page.endContext();
|
|
1021
1125
|
this.pageContext = null;
|
|
1022
|
-
this.contextState =
|
|
1023
|
-
} else if (this.contextState ===
|
|
1126
|
+
this.contextState = PAGE_CONTEXT_STATE.PAUSED_EDIT;
|
|
1127
|
+
} else if (this.contextState === PAGE_CONTEXT_STATE.ACTIVE_NEW) {
|
|
1024
1128
|
this.writer.pausePageContentContext(this.pageContext);
|
|
1025
|
-
this.contextState =
|
|
1129
|
+
this.contextState = PAGE_CONTEXT_STATE.PAUSED_NEW;
|
|
1026
1130
|
} else {
|
|
1027
1131
|
throw new Error("No active page content context to pause");
|
|
1028
1132
|
}
|
|
@@ -1038,12 +1142,12 @@ exports.pauseContext = function pauseContext() {
|
|
|
1038
1142
|
* @throws {Error} If there is no paused page content context.
|
|
1039
1143
|
*/
|
|
1040
1144
|
exports.resumeContext = function resumeContext() {
|
|
1041
|
-
if (this.contextState ===
|
|
1145
|
+
if (this.contextState === PAGE_CONTEXT_STATE.PAUSED_EDIT) {
|
|
1042
1146
|
this.pageContext = this.page.startContext().getContext();
|
|
1043
1147
|
this._resumePageRotation();
|
|
1044
|
-
this.contextState =
|
|
1045
|
-
} else if (this.contextState ===
|
|
1046
|
-
this.contextState =
|
|
1148
|
+
this.contextState = PAGE_CONTEXT_STATE.ACTIVE_EDIT;
|
|
1149
|
+
} else if (this.contextState === PAGE_CONTEXT_STATE.PAUSED_NEW) {
|
|
1150
|
+
this.contextState = PAGE_CONTEXT_STATE.ACTIVE_NEW;
|
|
1047
1151
|
} else {
|
|
1048
1152
|
throw new Error("No paused page content context to resume");
|
|
1049
1153
|
}
|