@muhammara/native-core 7.0.0-beta.4 → 7.0.0-rc.2
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 +91 -38
- 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 +464 -101
- package/lib/recipe/appendPage.js +2 -0
- 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 +121 -24
- package/lib/recipe/image.js +385 -39
- 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 +363 -262
- package/lib/recipe/parameters.js +8 -1
- 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 +41 -8
- package/lib/recipe/text.helper.js +172 -8
- package/lib/recipe/text.js +294 -56
- package/lib/recipe/utils.js +17 -6
- package/lib/recipe/vector-line.js +101 -39
- package/lib/recipe/vector-polygon.js +18 -6
- package/lib/recipe/vector.helper.js +156 -14
- package/lib/recipe/vector.js +156 -52
- package/lib/recipe/xObjectForm.js +73 -10
- package/lib/recipe-constants.js +373 -0
- package/lib/recipe-options.js +47 -0
- package/muhammara.d.ts +3501 -160
- package/package.json +2 -3
package/lib/recipe/page.js
CHANGED
|
@@ -1,7 +1,40 @@
|
|
|
1
1
|
const muhammara = require("../muhammara");
|
|
2
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");
|
|
3
9
|
|
|
4
|
-
|
|
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
|
+
*/
|
|
5
38
|
function readPageTree(
|
|
6
39
|
parser,
|
|
7
40
|
objectID,
|
|
@@ -21,11 +54,21 @@ function readPageTree(
|
|
|
21
54
|
if (depth === 0 && values.Parent !== undefined) {
|
|
22
55
|
throw new Error("deletePage requires a valid page tree");
|
|
23
56
|
}
|
|
24
|
-
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
|
+
}
|
|
25
61
|
const mappedChildren = kids.toJSArray().map((entry) => {
|
|
62
|
+
// Kids must be references to page tree dictionaries with a /Type.
|
|
26
63
|
const reference = entry.toPDFIndirectObjectReference();
|
|
27
|
-
const childID = reference
|
|
28
|
-
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
|
+
}
|
|
29
72
|
const childValues = childDictionary.toJSObject();
|
|
30
73
|
const parentReference = childValues.Parent?.toPDFIndirectObjectReference();
|
|
31
74
|
if (
|
|
@@ -35,8 +78,11 @@ function readPageTree(
|
|
|
35
78
|
) {
|
|
36
79
|
throw new Error("deletePage requires a valid page tree");
|
|
37
80
|
}
|
|
38
|
-
const type = childValues.Type
|
|
39
|
-
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) {
|
|
40
86
|
return readPageTree(
|
|
41
87
|
parser,
|
|
42
88
|
childID,
|
|
@@ -84,7 +130,15 @@ function readPageTree(
|
|
|
84
130
|
};
|
|
85
131
|
}
|
|
86
132
|
|
|
87
|
-
/**
|
|
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
|
+
*/
|
|
88
142
|
function writePageTree(writer, copyingContext, node) {
|
|
89
143
|
node.children
|
|
90
144
|
.filter((child) => child.children && child.changed)
|
|
@@ -95,13 +149,13 @@ function writePageTree(writer, copyingContext, node) {
|
|
|
95
149
|
objectsContext.startModifiedIndirectObject(node.objectID);
|
|
96
150
|
const dictionary = objectsContext.startDictionary();
|
|
97
151
|
Object.keys(node.values).forEach((key) => {
|
|
98
|
-
if (key ===
|
|
152
|
+
if (key === PdfName.COUNT || key === PdfName.KIDS) return;
|
|
99
153
|
dictionary.writeKey(key);
|
|
100
154
|
copyingContext.copyDirectObjectAsIs(node.values[key]);
|
|
101
155
|
});
|
|
102
|
-
dictionary.writeKey(
|
|
156
|
+
dictionary.writeKey(PdfName.COUNT);
|
|
103
157
|
objectsContext.writeNumber(node.count);
|
|
104
|
-
dictionary.writeKey(
|
|
158
|
+
dictionary.writeKey(PdfName.KIDS);
|
|
105
159
|
objectsContext.startArray();
|
|
106
160
|
node.children.forEach((child) => {
|
|
107
161
|
objectsContext.writeIndirectObjectReference(
|
|
@@ -117,6 +171,10 @@ function writePageTree(writer, copyingContext, node) {
|
|
|
117
171
|
* alike - depth-first. Shared by every deletePage() pass that needs to walk
|
|
118
172
|
* the tree readPageTree() already built, instead of re-parsing or re-walking
|
|
119
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}
|
|
120
178
|
*/
|
|
121
179
|
function walkPageTree(tree, visit) {
|
|
122
180
|
const pending = [tree];
|
|
@@ -127,15 +185,17 @@ function walkPageTree(tree, visit) {
|
|
|
127
185
|
}
|
|
128
186
|
}
|
|
129
187
|
|
|
130
|
-
/**
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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) {
|
|
139
199
|
walkPageTree(tree, (node) => {
|
|
140
200
|
if (!node.children) return;
|
|
141
201
|
if (node.changed && node.generation !== 0) {
|
|
@@ -149,162 +209,20 @@ function assertSupportedModifiedGenerations(tree, pageLabels, root, writer) {
|
|
|
149
209
|
"deletePage does not support rewriting nonzero-generation objects",
|
|
150
210
|
);
|
|
151
211
|
}
|
|
152
|
-
// A writer with _setPageLabelsObject() (wasm) always attaches a new
|
|
153
|
-
// PageLabels entry through that dedicated hook and never rewrites the
|
|
154
|
-
// catalog object in place, so the catalog's own generation never matters
|
|
155
|
-
// there. Without it (native-core), writePageLabels() falls back to
|
|
156
|
-
// rewriting the catalog via startModifiedIndirectObject(rootID), which
|
|
157
|
-
// only supports generation 0.
|
|
158
|
-
if (pageLabels && !pageLabels.reference && !writer._setPageLabelsObject) {
|
|
159
|
-
if (root.getVersion() !== 0) {
|
|
160
|
-
throw new Error(
|
|
161
|
-
"deletePage does not support rewriting nonzero-generation objects",
|
|
162
|
-
);
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
/** Rejects retained structures that reference a deleted page. @private */
|
|
168
|
-
function assertNoDeletedPageReferences(
|
|
169
|
-
parser,
|
|
170
|
-
value,
|
|
171
|
-
deletedPageIDs,
|
|
172
|
-
skippedObjectIDs,
|
|
173
|
-
visited = new Set(),
|
|
174
|
-
depth = 0,
|
|
175
|
-
) {
|
|
176
|
-
if (!value) return;
|
|
177
|
-
if (depth > 1000) {
|
|
178
|
-
throw new Error("deletePage cannot validate deeply nested references");
|
|
179
|
-
}
|
|
180
|
-
const reference = value.toPDFIndirectObjectReference?.();
|
|
181
|
-
if (reference) {
|
|
182
|
-
const objectID = reference.getObjectID();
|
|
183
|
-
if (deletedPageIDs.has(objectID)) {
|
|
184
|
-
throw new Error(
|
|
185
|
-
"deletePage cannot remove a page referenced by retained document structures",
|
|
186
|
-
);
|
|
187
|
-
}
|
|
188
|
-
if (skippedObjectIDs.has(objectID) || visited.has(objectID)) return;
|
|
189
|
-
visited.add(objectID);
|
|
190
|
-
assertNoDeletedPageReferences(
|
|
191
|
-
parser,
|
|
192
|
-
parser.parseNewObject(objectID),
|
|
193
|
-
deletedPageIDs,
|
|
194
|
-
skippedObjectIDs,
|
|
195
|
-
visited,
|
|
196
|
-
depth + 1,
|
|
197
|
-
);
|
|
198
|
-
return;
|
|
199
|
-
}
|
|
200
|
-
const array =
|
|
201
|
-
value.toPDFArray?.() ||
|
|
202
|
-
(typeof value.toJSArray === "function" ? value : null);
|
|
203
|
-
if (array) {
|
|
204
|
-
array
|
|
205
|
-
.toJSArray()
|
|
206
|
-
.forEach((entry) =>
|
|
207
|
-
assertNoDeletedPageReferences(
|
|
208
|
-
parser,
|
|
209
|
-
entry,
|
|
210
|
-
deletedPageIDs,
|
|
211
|
-
skippedObjectIDs,
|
|
212
|
-
visited,
|
|
213
|
-
depth + 1,
|
|
214
|
-
),
|
|
215
|
-
);
|
|
216
|
-
return;
|
|
217
|
-
}
|
|
218
|
-
const dictionary =
|
|
219
|
-
value.toPDFDictionary?.() ||
|
|
220
|
-
(typeof value.toJSObject === "function" ? value : null);
|
|
221
|
-
if (dictionary) {
|
|
222
|
-
Object.values(dictionary.toJSObject()).forEach((entry) =>
|
|
223
|
-
assertNoDeletedPageReferences(
|
|
224
|
-
parser,
|
|
225
|
-
entry,
|
|
226
|
-
deletedPageIDs,
|
|
227
|
-
skippedObjectIDs,
|
|
228
|
-
visited,
|
|
229
|
-
depth + 1,
|
|
230
|
-
),
|
|
231
|
-
);
|
|
232
|
-
return;
|
|
233
|
-
}
|
|
234
|
-
const stream = value.toPDFStream?.();
|
|
235
|
-
if (stream) {
|
|
236
|
-
assertNoDeletedPageReferences(
|
|
237
|
-
parser,
|
|
238
|
-
stream.getDictionary(),
|
|
239
|
-
deletedPageIDs,
|
|
240
|
-
skippedObjectIDs,
|
|
241
|
-
visited,
|
|
242
|
-
depth + 1,
|
|
243
|
-
);
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
/** Checks retained document structures for deleted-page references. @private */
|
|
248
|
-
function validateDeletedPageReferences(
|
|
249
|
-
parser,
|
|
250
|
-
catalog,
|
|
251
|
-
rootID,
|
|
252
|
-
tree,
|
|
253
|
-
pageLabels,
|
|
254
|
-
deletedPageIDs,
|
|
255
|
-
sourcePageCount,
|
|
256
|
-
) {
|
|
257
|
-
const skippedObjectIDs = new Set([rootID]);
|
|
258
|
-
collectPageTreeObjectIDs(tree, skippedObjectIDs);
|
|
259
|
-
for (let pageIndex = 0; pageIndex < sourcePageCount; pageIndex += 1) {
|
|
260
|
-
skippedObjectIDs.add(parser.getPageObjectID(pageIndex));
|
|
261
|
-
}
|
|
262
|
-
const visited = new Set();
|
|
263
|
-
Object.entries(catalog)
|
|
264
|
-
.filter(([key]) => !["PageLabels", "Pages"].includes(key))
|
|
265
|
-
.forEach(([, value]) =>
|
|
266
|
-
assertNoDeletedPageReferences(
|
|
267
|
-
parser,
|
|
268
|
-
value,
|
|
269
|
-
deletedPageIDs,
|
|
270
|
-
skippedObjectIDs,
|
|
271
|
-
visited,
|
|
272
|
-
),
|
|
273
|
-
);
|
|
274
|
-
if (pageLabels) {
|
|
275
|
-
Object.entries(pageLabels.values)
|
|
276
|
-
.filter(([key]) => !["Kids", "Limits", "Nums"].includes(key))
|
|
277
|
-
.forEach(([, value]) =>
|
|
278
|
-
assertNoDeletedPageReferences(
|
|
279
|
-
parser,
|
|
280
|
-
value,
|
|
281
|
-
deletedPageIDs,
|
|
282
|
-
skippedObjectIDs,
|
|
283
|
-
visited,
|
|
284
|
-
),
|
|
285
|
-
);
|
|
286
|
-
}
|
|
287
|
-
// Walks the same tree readPageTree() already parsed - each retained leaf
|
|
288
|
-
// carries the page dictionary values readPageTree() read, so there is no
|
|
289
|
-
// need to re-parse every retained page's dictionary here.
|
|
290
|
-
walkPageTree(tree, (node) => {
|
|
291
|
-
const isLeaf = !node.children;
|
|
292
|
-
const skipKeys = isLeaf ? ["Parent"] : ["Count", "Kids", "Parent"];
|
|
293
|
-
Object.entries(node.values)
|
|
294
|
-
.filter(([key]) => !skipKeys.includes(key))
|
|
295
|
-
.forEach(([, value]) =>
|
|
296
|
-
assertNoDeletedPageReferences(
|
|
297
|
-
parser,
|
|
298
|
-
value,
|
|
299
|
-
deletedPageIDs,
|
|
300
|
-
skippedObjectIDs,
|
|
301
|
-
visited,
|
|
302
|
-
),
|
|
303
|
-
);
|
|
304
|
-
});
|
|
305
212
|
}
|
|
306
213
|
|
|
307
|
-
/**
|
|
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
|
+
*/
|
|
308
226
|
function collectPageLabels(
|
|
309
227
|
parser,
|
|
310
228
|
dictionary,
|
|
@@ -336,7 +254,7 @@ function collectPageLabels(
|
|
|
336
254
|
}
|
|
337
255
|
if (values.Kids) {
|
|
338
256
|
parser
|
|
339
|
-
.queryDictionaryObject(dictionary,
|
|
257
|
+
.queryDictionaryObject(dictionary, PdfName.KIDS)
|
|
340
258
|
.toPDFArray()
|
|
341
259
|
.toJSArray()
|
|
342
260
|
.forEach((entry) => {
|
|
@@ -354,7 +272,14 @@ function collectPageLabels(
|
|
|
354
272
|
return values;
|
|
355
273
|
}
|
|
356
274
|
|
|
357
|
-
/**
|
|
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
|
+
*/
|
|
358
283
|
function resolvePageLabelObject(parser, value) {
|
|
359
284
|
const visited = new Set();
|
|
360
285
|
let resolved = value;
|
|
@@ -369,7 +294,16 @@ function resolvePageLabelObject(parser, value) {
|
|
|
369
294
|
return resolved;
|
|
370
295
|
}
|
|
371
296
|
|
|
372
|
-
/**
|
|
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
|
+
*/
|
|
373
307
|
function readPageLabel(parser, value) {
|
|
374
308
|
const dictionary = value?.toPDFDictionary();
|
|
375
309
|
if (!dictionary) {
|
|
@@ -381,6 +315,9 @@ function readPageLabel(parser, value) {
|
|
|
381
315
|
const start = values.St ? resolvePageLabelObject(parser, values.St) : null;
|
|
382
316
|
const prefixHex = prefix?.toPDFHexString();
|
|
383
317
|
const prefixLiteral = prefix?.toPDFLiteralString();
|
|
318
|
+
if (values.P !== undefined && !prefixHex && !prefixLiteral) {
|
|
319
|
+
throw new Error("deletePage requires valid PageLabels entries");
|
|
320
|
+
}
|
|
384
321
|
return {
|
|
385
322
|
style: style?.toPDFName()?.value,
|
|
386
323
|
prefix:
|
|
@@ -394,7 +331,13 @@ function readPageLabel(parser, value) {
|
|
|
394
331
|
};
|
|
395
332
|
}
|
|
396
333
|
|
|
397
|
-
/**
|
|
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
|
+
*/
|
|
398
341
|
function writePageLabelObjects(objectsContext, entries) {
|
|
399
342
|
return entries.map((entry) => {
|
|
400
343
|
const objectID = objectsContext.startNewIndirectObject();
|
|
@@ -418,7 +361,17 @@ function writePageLabelObjects(objectsContext, entries) {
|
|
|
418
361
|
});
|
|
419
362
|
}
|
|
420
363
|
|
|
421
|
-
/**
|
|
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
|
+
*/
|
|
422
375
|
function writePageLabelsDictionary(
|
|
423
376
|
objectsContext,
|
|
424
377
|
copyingContext,
|
|
@@ -427,11 +380,12 @@ function writePageLabelsDictionary(
|
|
|
427
380
|
entries,
|
|
428
381
|
) {
|
|
429
382
|
Object.keys(values).forEach((key) => {
|
|
430
|
-
if (key ===
|
|
383
|
+
if (key === PdfName.KIDS || key === PdfName.LIMITS || key === PdfName.NUMS)
|
|
384
|
+
return;
|
|
431
385
|
dictionary.writeKey(key);
|
|
432
386
|
copyingContext.copyDirectObjectAsIs(values[key]);
|
|
433
387
|
});
|
|
434
|
-
dictionary.writeKey(
|
|
388
|
+
dictionary.writeKey(PdfName.NUMS);
|
|
435
389
|
objectsContext.startArray();
|
|
436
390
|
entries.forEach((entry) => {
|
|
437
391
|
objectsContext.writeNumber(entry.index);
|
|
@@ -440,7 +394,18 @@ function writePageLabelsDictionary(
|
|
|
440
394
|
objectsContext.endArray();
|
|
441
395
|
}
|
|
442
396
|
|
|
443
|
-
/**
|
|
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
|
+
*/
|
|
444
409
|
function preparePageLabels(
|
|
445
410
|
parser,
|
|
446
411
|
catalogDictionary,
|
|
@@ -509,15 +474,21 @@ function preparePageLabels(
|
|
|
509
474
|
}
|
|
510
475
|
});
|
|
511
476
|
return {
|
|
512
|
-
catalogValues,
|
|
513
477
|
reference,
|
|
514
478
|
values,
|
|
515
479
|
entries: normalized,
|
|
516
480
|
};
|
|
517
481
|
}
|
|
518
482
|
|
|
519
|
-
/**
|
|
520
|
-
|
|
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) {
|
|
521
492
|
if (!pageLabels) return;
|
|
522
493
|
|
|
523
494
|
const objectsContext = writer.getObjectsContext();
|
|
@@ -549,21 +520,7 @@ function writePageLabels(writer, copyingContext, rootID, pageLabels) {
|
|
|
549
520
|
);
|
|
550
521
|
objectsContext.endDictionary(labelsDictionary).endIndirectObject();
|
|
551
522
|
|
|
552
|
-
|
|
553
|
-
writer._setPageLabelsObject(labelsObjectID);
|
|
554
|
-
return;
|
|
555
|
-
}
|
|
556
|
-
|
|
557
|
-
objectsContext.startModifiedIndirectObject(rootID);
|
|
558
|
-
const dictionary = objectsContext.startDictionary();
|
|
559
|
-
Object.keys(pageLabels.catalogValues).forEach((key) => {
|
|
560
|
-
if (key === "PageLabels") return;
|
|
561
|
-
dictionary.writeKey(key);
|
|
562
|
-
copyingContext.copyDirectObjectAsIs(pageLabels.catalogValues[key]);
|
|
563
|
-
});
|
|
564
|
-
dictionary.writeKey("PageLabels");
|
|
565
|
-
objectsContext.writeIndirectObjectReference(labelsObjectID);
|
|
566
|
-
objectsContext.endDictionary(dictionary).endIndirectObject();
|
|
523
|
+
setCatalogEntry(writer, PdfName.PAGE_LABELS, labelsObjectID);
|
|
567
524
|
}
|
|
568
525
|
|
|
569
526
|
/**
|
|
@@ -573,8 +530,9 @@ function writePageLabels(writer, copyingContext, rootID, pageLabels) {
|
|
|
573
530
|
* @name createPage
|
|
574
531
|
* @function
|
|
575
532
|
* @memberof Recipe#
|
|
576
|
-
* @param {number|
|
|
577
|
-
* 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.
|
|
578
536
|
* @param {number} [pageHeight] - The page height, or rotation (90) when page size name given.
|
|
579
537
|
* @param {object} [margins] - page margin definitions.
|
|
580
538
|
* @param {number} [margins.left] - Left margin.
|
|
@@ -582,6 +540,7 @@ function writePageLabels(writer, copyingContext, rootID, pageLabels) {
|
|
|
582
540
|
* @param {number} [margins.top] - Top margin.
|
|
583
541
|
* @param {number} [margins.bottom] - Bottom margin.
|
|
584
542
|
* @returns {Recipe} The recipe instance.
|
|
543
|
+
* @throws {Error} If pages were deleted with deletePage() on this Recipe.
|
|
585
544
|
*/
|
|
586
545
|
exports.createPage = function createPage(pageWidth, pageHeight, margins) {
|
|
587
546
|
if (this.deletedPages?.size) {
|
|
@@ -620,7 +579,8 @@ exports.createPage = function createPage(pageWidth, pageHeight, margins) {
|
|
|
620
579
|
(this.metadata.pageCount ?? this.metadata.pages ?? 0) + 1;
|
|
621
580
|
const pageNumber = this.metadata.pageCount;
|
|
622
581
|
const dimensions = [0, 0, pageWidth, pageHeight];
|
|
623
|
-
const layout =
|
|
582
|
+
const layout =
|
|
583
|
+
pageWidth > pageHeight ? PageLayout.LANDSCAPE : PageLayout.PORTRAIT;
|
|
624
584
|
this.metadata[pageNumber] = {
|
|
625
585
|
pageNumber,
|
|
626
586
|
mediaBox: dimensions,
|
|
@@ -648,15 +608,55 @@ exports.createPage = function createPage(pageWidth, pageHeight, margins) {
|
|
|
648
608
|
return this;
|
|
649
609
|
};
|
|
650
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
|
+
|
|
651
640
|
/**
|
|
652
641
|
* Set the rotation of the current page.
|
|
653
642
|
* @name rotate
|
|
654
643
|
* @function
|
|
655
644
|
* @memberof Recipe#
|
|
656
|
-
* @param {number} rotation - The page rotation in degrees.
|
|
645
|
+
* @param {number} rotation - The page rotation in degrees, a multiple of 90.
|
|
657
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()`.
|
|
658
650
|
*/
|
|
659
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);
|
|
660
660
|
this.page.rotate = rotation;
|
|
661
661
|
this.metadata[this.pageNumber].rotate = rotation;
|
|
662
662
|
return this;
|
|
@@ -667,15 +667,20 @@ exports.rotate = function rotate(rotation) {
|
|
|
667
667
|
* @name setPageBox
|
|
668
668
|
* @function
|
|
669
669
|
* @memberof Recipe#
|
|
670
|
-
* @param {number} box - An `ePDFPageBox*` constant.
|
|
670
|
+
* @param {number|string} box - An `ePDFPageBox*` constant or a `PageBox` name.
|
|
671
671
|
* @param {number} left - The PDF left coordinate.
|
|
672
672
|
* @param {number} bottom - The PDF bottom coordinate.
|
|
673
673
|
* @param {number} right - The PDF right coordinate.
|
|
674
674
|
* @param {number} top - The PDF top coordinate.
|
|
675
675
|
* @returns {Recipe} The recipe instance.
|
|
676
676
|
* @throws {RangeError} If the page box constant is unknown.
|
|
677
|
+
* @throws {TypeError} If no page is active.
|
|
677
678
|
*/
|
|
678
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
|
+
}
|
|
679
684
|
const boxes = {
|
|
680
685
|
[muhammara.ePDFPageBoxMediaBox]: "mediaBox",
|
|
681
686
|
[muhammara.ePDFPageBoxCropBox]: "cropBox",
|
|
@@ -697,18 +702,19 @@ exports.setPageBox = function setPageBox(box, left, bottom, right, top) {
|
|
|
697
702
|
mediaBox: pageBox,
|
|
698
703
|
width,
|
|
699
704
|
height,
|
|
700
|
-
layout: width > height ?
|
|
705
|
+
layout: width > height ? PageLayout.LANDSCAPE : PageLayout.PORTRAIT,
|
|
701
706
|
});
|
|
702
707
|
}
|
|
703
708
|
return this;
|
|
704
709
|
};
|
|
705
710
|
|
|
706
711
|
/**
|
|
707
|
-
* Finish a page
|
|
712
|
+
* Finish a page. Without an active page this does nothing.
|
|
708
713
|
* @name endPage
|
|
709
714
|
* @function
|
|
710
715
|
* @memberof Recipe#
|
|
711
716
|
* @returns {Recipe} The recipe instance.
|
|
717
|
+
* @throws {Error} If the page cannot be written.
|
|
712
718
|
*/
|
|
713
719
|
exports.endPage = function endPage() {
|
|
714
720
|
if (!this.page) {
|
|
@@ -736,10 +742,17 @@ exports.endPage = function endPage() {
|
|
|
736
742
|
* @name editPage
|
|
737
743
|
* @function
|
|
738
744
|
* @memberof Recipe#
|
|
739
|
-
* @param {number} pageNumber - The page number to be edited.
|
|
745
|
+
* @param {number} pageNumber - The one-based page number to be edited.
|
|
740
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.
|
|
741
749
|
*/
|
|
742
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
|
+
}
|
|
743
756
|
const pdfWriter = this.writer;
|
|
744
757
|
const pageIndex = pageNumber - 1;
|
|
745
758
|
const pageModifier = new muhammara.PDFPageModifier(
|
|
@@ -762,9 +775,9 @@ exports.editPage = function editPage(pageNumber) {
|
|
|
762
775
|
const startX = mediaBox[0];
|
|
763
776
|
const startY = mediaBox[1];
|
|
764
777
|
const textOptions = {
|
|
765
|
-
font: this.writer.getFontForFile(this.fonts
|
|
778
|
+
font: this.writer.getFontForFile(this.fonts.helvetica.b),
|
|
766
779
|
size: 50,
|
|
767
|
-
colorspace:
|
|
780
|
+
colorspace: Colorspace.GRAY,
|
|
768
781
|
color: 0x00,
|
|
769
782
|
};
|
|
770
783
|
context.writeText(
|
|
@@ -801,21 +814,105 @@ exports.editPage = function editPage(pageNumber) {
|
|
|
801
814
|
return this;
|
|
802
815
|
};
|
|
803
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
|
+
|
|
804
887
|
/**
|
|
805
888
|
* Delete one or more pages from an existing PDF.
|
|
806
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.
|
|
807
899
|
* @name deletePage
|
|
808
900
|
* @function
|
|
809
901
|
* @memberof Recipe#
|
|
810
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.
|
|
811
906
|
* @returns {Recipe} The recipe instance.
|
|
907
|
+
* @throws {TypeError} If options is not an object or pruneReferences is not
|
|
908
|
+
* a boolean.
|
|
812
909
|
* @throws {RangeError} If a page number does not identify an original page.
|
|
813
910
|
* @throws {Error} If the Recipe has no existing source, has ended, would delete
|
|
814
|
-
* every page,
|
|
815
|
-
* retained
|
|
816
|
-
*
|
|
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.
|
|
817
914
|
*/
|
|
818
|
-
exports.deletePage = function deletePage(pageNumbers) {
|
|
915
|
+
exports.deletePage = function deletePage(pageNumbers, options) {
|
|
819
916
|
if (this.ended) {
|
|
820
917
|
throw new Error("Cannot delete a page after endPDF");
|
|
821
918
|
}
|
|
@@ -831,6 +928,16 @@ exports.deletePage = function deletePage(pageNumbers) {
|
|
|
831
928
|
if (this.pagesCreated) {
|
|
832
929
|
throw new Error("deletePage cannot be combined with createPage");
|
|
833
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
|
+
}
|
|
834
941
|
pageNumbers = Array.isArray(pageNumbers) ? pageNumbers : [pageNumbers];
|
|
835
942
|
const deletedPages = new Set(this.deletedPages || []);
|
|
836
943
|
pageNumbers.forEach((pageNumber) => {
|
|
@@ -846,67 +953,48 @@ exports.deletePage = function deletePage(pageNumbers) {
|
|
|
846
953
|
if (deletedPages.size >= this.sourcePageCount) {
|
|
847
954
|
throw new Error("At least one page must remain in the PDF");
|
|
848
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
|
+
}
|
|
849
963
|
this.deletedPages = deletedPages;
|
|
964
|
+
this.pruneReferences = prune;
|
|
850
965
|
return this;
|
|
851
966
|
};
|
|
852
967
|
|
|
853
|
-
/**
|
|
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
|
+
*/
|
|
854
978
|
exports._deletePages = function _deletePages() {
|
|
855
979
|
if (!this.deletedPages?.size) return this;
|
|
856
980
|
|
|
857
981
|
const copyingContext = this.writer.createPDFCopyingContextForModifiedFile();
|
|
858
982
|
try {
|
|
859
|
-
const
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
const rootID = rootReference.getObjectID();
|
|
863
|
-
const catalogDictionary = parser.parseNewObject(rootID).toPDFDictionary();
|
|
864
|
-
const catalog = catalogDictionary.toJSObject();
|
|
865
|
-
const pagesReference = catalog.Pages.toPDFIndirectObjectReference();
|
|
866
|
-
const pageLabels = preparePageLabels(
|
|
867
|
-
parser,
|
|
868
|
-
catalogDictionary,
|
|
983
|
+
const deletion = planPageDeletion(
|
|
984
|
+
this,
|
|
985
|
+
copyingContext,
|
|
869
986
|
this.deletedPages,
|
|
870
|
-
this.
|
|
871
|
-
);
|
|
872
|
-
const modifiedPageIDs = new Set(
|
|
873
|
-
Array.from(this.modifiedSourcePages, (pageNumber) =>
|
|
874
|
-
parser.getPageObjectID(pageNumber - 1),
|
|
875
|
-
),
|
|
987
|
+
Boolean(this.pruneReferences),
|
|
876
988
|
);
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
retainedPageIDs: new Set(),
|
|
881
|
-
};
|
|
882
|
-
const tree = readPageTree(
|
|
883
|
-
parser,
|
|
884
|
-
pagesReference.getObjectID(),
|
|
885
|
-
this.deletedPages,
|
|
886
|
-
modifiedPageIDs,
|
|
887
|
-
pageState,
|
|
888
|
-
pagesReference.getVersion(),
|
|
889
|
-
);
|
|
890
|
-
pageState.retainedPageIDs.forEach((objectID) =>
|
|
891
|
-
pageState.deletedPageIDs.delete(objectID),
|
|
892
|
-
);
|
|
893
|
-
assertSupportedModifiedGenerations(
|
|
894
|
-
tree,
|
|
895
|
-
pageLabels,
|
|
896
|
-
rootReference,
|
|
989
|
+
writePageTree(this.writer, copyingContext, deletion.tree);
|
|
990
|
+
writePageLabels(this.writer, copyingContext, deletion.pageLabels);
|
|
991
|
+
pruneDeletedPageReferences(
|
|
897
992
|
this.writer,
|
|
993
|
+
copyingContext,
|
|
994
|
+
deletion.catalog,
|
|
995
|
+
deletion.plan,
|
|
996
|
+
deletion.deletedPageIDs,
|
|
898
997
|
);
|
|
899
|
-
validateDeletedPageReferences(
|
|
900
|
-
parser,
|
|
901
|
-
catalog,
|
|
902
|
-
rootID,
|
|
903
|
-
tree,
|
|
904
|
-
pageLabels,
|
|
905
|
-
pageState.deletedPageIDs,
|
|
906
|
-
this.sourcePageCount,
|
|
907
|
-
);
|
|
908
|
-
writePageTree(this.writer, copyingContext, tree);
|
|
909
|
-
writePageLabels(this.writer, copyingContext, rootID, pageLabels);
|
|
910
998
|
} finally {
|
|
911
999
|
copyingContext.end();
|
|
912
1000
|
}
|
|
@@ -943,6 +1031,15 @@ exports._deletePages = function _deletePages() {
|
|
|
943
1031
|
return this;
|
|
944
1032
|
};
|
|
945
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
|
+
*/
|
|
946
1043
|
exports._resumePageRotation = function _resumePageRotation(
|
|
947
1044
|
pageNumber,
|
|
948
1045
|
context,
|
|
@@ -984,11 +1081,15 @@ exports._resumePageRotation = function _resumePageRotation(
|
|
|
984
1081
|
* @name pageInfo
|
|
985
1082
|
* @function
|
|
986
1083
|
* @memberof Recipe#
|
|
987
|
-
* @param {number} pageNumber - The page number.
|
|
1084
|
+
* @param {number} pageNumber - The one-based page number.
|
|
988
1085
|
* @returns {RecipePageInfo} The page information.
|
|
1086
|
+
* @throws {TypeError} If the page is unknown.
|
|
989
1087
|
*/
|
|
990
1088
|
exports.pageInfo = function pageInfo(pageNumber) {
|
|
991
1089
|
const pageInfo = this.metadata[pageNumber];
|
|
1090
|
+
if (!pageInfo) {
|
|
1091
|
+
throw new TypeError(`Unknown page number: ${pageNumber}`);
|
|
1092
|
+
}
|
|
992
1093
|
return {
|
|
993
1094
|
width: pageInfo.width,
|
|
994
1095
|
height: pageInfo.height,
|