@muhammara/native-core 7.0.0-beta.4 → 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.
Files changed (43) hide show
  1. package/README.md +57 -7
  2. package/THIRD_PARTY_NOTICES.md +39 -0
  3. package/index.js +120 -1
  4. package/lib/PDFRStreamForBuffer.js +52 -7
  5. package/lib/PDFRStreamForFile.js +56 -7
  6. package/lib/PDFStreamForResponse.js +22 -5
  7. package/lib/PDFWStreamForBuffer.js +49 -10
  8. package/lib/PDFWStreamForFile.js +26 -5
  9. package/lib/Recipe.js +79 -28
  10. package/lib/content-stream.js +503 -0
  11. package/lib/css-colors.js +149 -0
  12. package/lib/font-text.js +1461 -0
  13. package/lib/glyph-list.js +1127 -0
  14. package/lib/recipe/annotation.js +464 -101
  15. package/lib/recipe/appendPage.js +2 -0
  16. package/lib/recipe/colors.js +132 -35
  17. package/lib/recipe/coordinate.js +82 -16
  18. package/lib/recipe/encrypt.js +32 -13
  19. package/lib/recipe/font.js +70 -25
  20. package/lib/recipe/graphics-state.js +3 -2
  21. package/lib/recipe/htmlToTextObjects.js +121 -24
  22. package/lib/recipe/image.js +34 -9
  23. package/lib/recipe/info.js +93 -22
  24. package/lib/recipe/insertPage.js +12 -3
  25. package/lib/recipe/overlay.js +4 -2
  26. package/lib/recipe/page-references.js +436 -0
  27. package/lib/recipe/page.js +363 -262
  28. package/lib/recipe/parameters.js +8 -1
  29. package/lib/recipe/replaceText.js +464 -30
  30. package/lib/recipe/shapes.js +418 -45
  31. package/lib/recipe/split.js +6 -2
  32. package/lib/recipe/table.js +41 -8
  33. package/lib/recipe/text.helper.js +172 -8
  34. package/lib/recipe/text.js +294 -56
  35. package/lib/recipe/utils.js +17 -6
  36. package/lib/recipe/vector-line.js +101 -39
  37. package/lib/recipe/vector-polygon.js +18 -6
  38. package/lib/recipe/vector.helper.js +148 -6
  39. package/lib/recipe/vector.js +150 -51
  40. package/lib/recipe/xObjectForm.js +73 -10
  41. package/lib/recipe-constants.js +373 -0
  42. package/muhammara.d.ts +3469 -155
  43. package/package.json +1 -1
@@ -0,0 +1,436 @@
1
+ // Finds and prunes references to deleted pages in the structures deletePage()
2
+ // retains: the catalog, retained pages and every object reachable from them.
3
+
4
+ const REFERENCED_PAGE_ERROR =
5
+ "deletePage cannot remove a page referenced by retained document structures";
6
+
7
+ // Holders pruning cannot rewrite, named for the error message.
8
+ const UNSUPPORTED_HOLDERS = Object.freeze({
9
+ stream: "a stream dictionary",
10
+ pages: "the page tree",
11
+ pageLabels: "the page labels",
12
+ });
13
+
14
+ /**
15
+ * @private
16
+ * @param {Object} value - A PDF object.
17
+ * @param {Set<number>} deletedPageIDs - Object IDs of the deleted pages.
18
+ * @returns {boolean} Whether the value is a reference to a deleted page.
19
+ */
20
+ function isDeletedPageReference(value, deletedPageIDs) {
21
+ const reference = value?.toPDFIndirectObjectReference?.();
22
+ return Boolean(reference && deletedPageIDs.has(reference.getObjectID()));
23
+ }
24
+
25
+ /**
26
+ * @private
27
+ * @param {Object} value - A PDF object.
28
+ * @returns {Object|null} The value as an array, or null.
29
+ */
30
+ function asArray(value) {
31
+ return (
32
+ value.toPDFArray?.() ||
33
+ (typeof value.toJSArray === "function" ? value : null)
34
+ );
35
+ }
36
+
37
+ /**
38
+ * @private
39
+ * @param {Object} value - A PDF object.
40
+ * @returns {Object|null} The value as a dictionary, or null.
41
+ */
42
+ function asDictionary(value) {
43
+ return (
44
+ value.toPDFDictionary?.() ||
45
+ (typeof value.toJSObject === "function" ? value : null)
46
+ );
47
+ }
48
+
49
+ /**
50
+ * A pruned value is a reference to a deleted page, or a destination array
51
+ * whose page is a deleted page. Pruning replaces it with null.
52
+ * @private
53
+ * @param {Object} value - A PDF object.
54
+ * @param {Set<number>} deletedPageIDs - Object IDs of the deleted pages.
55
+ * @returns {boolean} Whether pruning drops the whole value.
56
+ */
57
+ function isPrunedValue(value, deletedPageIDs) {
58
+ if (isDeletedPageReference(value, deletedPageIDs)) return true;
59
+ const array = value.toPDFIndirectObjectReference?.() ? null : asArray(value);
60
+ if (!array) return false;
61
+ const entries = array.toJSArray();
62
+ return (
63
+ entries.length > 0 && isDeletedPageReference(entries[0], deletedPageIDs)
64
+ );
65
+ }
66
+
67
+ /**
68
+ * Walk the direct and indirect values reachable from retained structures and
69
+ * record which holder directly contains a deleted-page reference. A holder is
70
+ * the catalog entry, retained page or indirect object whose own value holds
71
+ * the reference.
72
+ * @private
73
+ * @param {Object} parser - The source PDF parser.
74
+ * @param {Object} [value] - The PDF object to scan.
75
+ * @param {Object} holder - The holder the value belongs to.
76
+ * @param {Object} scan - Shared scan state: deletedPageIDs, skippedObjectIDs,
77
+ * visited, prune and the holders found.
78
+ * @param {number} [depth=0] - The nesting depth, limited to 1000.
79
+ * @returns {void}
80
+ * @throws {Error} If a retained structure references a deleted page and
81
+ * pruning is off, or nesting is too deep.
82
+ */
83
+ function scanValue(parser, value, holder, scan, depth = 0) {
84
+ if (!value) return;
85
+ if (depth > 1000) {
86
+ throw new Error("deletePage cannot validate deeply nested references");
87
+ }
88
+ const reference = value.toPDFIndirectObjectReference?.();
89
+ if (reference) {
90
+ const objectID = reference.getObjectID();
91
+ if (scan.deletedPageIDs.has(objectID)) {
92
+ if (!scan.prune) throw new Error(REFERENCED_PAGE_ERROR);
93
+ scan.holders.push(holder);
94
+ return;
95
+ }
96
+ if (scan.skippedObjectIDs.has(objectID) || scan.visited.has(objectID)) {
97
+ return;
98
+ }
99
+ scan.visited.add(objectID);
100
+ const object = parser.parseNewObject(objectID);
101
+ const stream = object?.toPDFStream?.();
102
+ scanValue(
103
+ parser,
104
+ stream ? stream.getDictionary() : object,
105
+ stream
106
+ ? { kind: "stream", objectID }
107
+ : { kind: "object", objectID, generation: reference.getVersion() },
108
+ scan,
109
+ depth + 1,
110
+ );
111
+ return;
112
+ }
113
+ const array = asArray(value);
114
+ if (array) {
115
+ array
116
+ .toJSArray()
117
+ .forEach((entry) => scanValue(parser, entry, holder, scan, depth + 1));
118
+ return;
119
+ }
120
+ const dictionary = asDictionary(value);
121
+ if (dictionary) {
122
+ Object.values(dictionary.toJSObject()).forEach((entry) =>
123
+ scanValue(parser, entry, holder, scan, depth + 1),
124
+ );
125
+ return;
126
+ }
127
+ const stream = value.toPDFStream?.();
128
+ if (stream) {
129
+ scanValue(
130
+ parser,
131
+ stream.getDictionary(),
132
+ { kind: "stream" },
133
+ scan,
134
+ depth + 1,
135
+ );
136
+ }
137
+ }
138
+
139
+ /**
140
+ * Find the retained structures that reference deleted pages. Without
141
+ * pruning, the first reference throws. With pruning, the result lists the
142
+ * objects and catalog entries to rewrite.
143
+ * @private
144
+ * @param {Object} parser - The source PDF parser.
145
+ * @param {Object} deletion - The deletion: catalog entries, rootID, the page
146
+ * tree built by readPageTree(), prepared pageLabels, deletedPageIDs,
147
+ * modifiedPageIDs and sourcePageCount.
148
+ * @param {boolean} prune - Whether to plan pruning instead of throwing.
149
+ * @returns {{objects: Map<number, number>, catalogKeys: string[]}} The
150
+ * object IDs to rewrite with their generations, and the catalog keys to
151
+ * replace.
152
+ * @throws {Error} If a reference cannot be pruned, or pruning is off and a
153
+ * retained structure references a deleted page.
154
+ */
155
+ function planDeletedPageReferences(parser, deletion, prune) {
156
+ const scan = {
157
+ deletedPageIDs: deletion.deletedPageIDs,
158
+ skippedObjectIDs: new Set([deletion.rootID]),
159
+ visited: new Set(),
160
+ prune,
161
+ holders: [],
162
+ };
163
+ const pagesNodeIDs = new Set();
164
+ const pending = [deletion.tree];
165
+ while (pending.length) {
166
+ const node = pending.pop();
167
+ if (node.children) {
168
+ pagesNodeIDs.add(node.objectID);
169
+ pending.push(...node.children);
170
+ }
171
+ }
172
+ pagesNodeIDs.forEach((objectID) => scan.skippedObjectIDs.add(objectID));
173
+ for (let pageIndex = 0; pageIndex < deletion.sourcePageCount; pageIndex++) {
174
+ scan.skippedObjectIDs.add(parser.getPageObjectID(pageIndex));
175
+ }
176
+
177
+ Object.entries(deletion.catalog)
178
+ .filter(([key]) => !["PageLabels", "Pages"].includes(key))
179
+ .forEach(([key, value]) =>
180
+ scanValue(parser, value, { kind: "catalog", key }, scan),
181
+ );
182
+ if (deletion.pageLabels) {
183
+ Object.entries(deletion.pageLabels.values)
184
+ .filter(([key]) => !["Kids", "Limits", "Nums"].includes(key))
185
+ .forEach(([, value]) =>
186
+ scanValue(parser, value, { kind: "pageLabels" }, scan),
187
+ );
188
+ }
189
+ const retained = [deletion.tree];
190
+ while (retained.length) {
191
+ const node = retained.pop();
192
+ const isLeaf = !node.children;
193
+ const skipKeys = isLeaf ? ["Parent"] : ["Count", "Kids", "Parent"];
194
+ const holder = isLeaf
195
+ ? { kind: "page", objectID: node.objectID, generation: node.generation }
196
+ : { kind: "pages" };
197
+ Object.entries(node.values)
198
+ .filter(([key]) => !skipKeys.includes(key))
199
+ .forEach(([, value]) => scanValue(parser, value, holder, scan));
200
+ if (node.children) retained.push(...node.children);
201
+ }
202
+
203
+ const objects = new Map();
204
+ const catalogKeys = [];
205
+ scan.holders.forEach((holder) => {
206
+ if (holder.kind === "catalog") {
207
+ if (!catalogKeys.includes(holder.key)) catalogKeys.push(holder.key);
208
+ return;
209
+ }
210
+ if (
211
+ holder.kind === "page" &&
212
+ deletion.modifiedPageIDs.has(holder.objectID)
213
+ ) {
214
+ throw new Error(
215
+ "deletePage cannot prune references held by a page edited in this Recipe",
216
+ );
217
+ }
218
+ if (holder.kind !== "object" && holder.kind !== "page") {
219
+ throw new Error(
220
+ `deletePage cannot prune references held by ${UNSUPPORTED_HOLDERS[holder.kind]}`,
221
+ );
222
+ }
223
+ if (holder.generation !== 0) {
224
+ throw new Error(
225
+ "deletePage does not support rewriting nonzero-generation objects",
226
+ );
227
+ }
228
+ objects.set(holder.objectID, holder.generation);
229
+ });
230
+ return { objects, catalogKeys };
231
+ }
232
+
233
+ /**
234
+ * @private
235
+ * @param {Object} value - A direct PDF object.
236
+ * @param {Set<number>} deletedPageIDs - Object IDs of the deleted pages.
237
+ * @returns {boolean} Whether the value directly contains a pruned value.
238
+ */
239
+ function containsPrunedValue(value, deletedPageIDs) {
240
+ if (isPrunedValue(value, deletedPageIDs)) return true;
241
+ if (value.toPDFIndirectObjectReference?.()) return false;
242
+ const array = asArray(value);
243
+ if (array) {
244
+ return array
245
+ .toJSArray()
246
+ .some((entry) => containsPrunedValue(entry, deletedPageIDs));
247
+ }
248
+ const dictionary = asDictionary(value);
249
+ if (dictionary) {
250
+ return Object.values(dictionary.toJSObject()).some((entry) =>
251
+ containsPrunedValue(entry, deletedPageIDs),
252
+ );
253
+ }
254
+ return false;
255
+ }
256
+
257
+ /**
258
+ * Plan how to write a direct value with every pruned value removed:
259
+ * dictionary entries are dropped and array elements become null. A changed
260
+ * dictionary nested inside the value is written right away as its own
261
+ * indirect object and referenced from the plan, so writing the value never
262
+ * nests dictionaries, which the Wasm objects context does not support.
263
+ * @private
264
+ * @param {Object} objectsContext - The writer's objects context.
265
+ * @param {Object} copyingContext - Copies unchanged values.
266
+ * @param {Object} value - A direct PDF object that is not itself pruned.
267
+ * @param {Set<number>} deletedPageIDs - Object IDs of the deleted pages.
268
+ * @param {boolean} [nested=false] - Whether the value is inside another.
269
+ * @returns {Object} The write plan: copy, null, ref, array or dict.
270
+ */
271
+ function planPrunedValue(
272
+ objectsContext,
273
+ copyingContext,
274
+ value,
275
+ deletedPageIDs,
276
+ nested = false,
277
+ ) {
278
+ if (isPrunedValue(value, deletedPageIDs)) return { kind: "null" };
279
+ if (!containsPrunedValue(value, deletedPageIDs)) {
280
+ return { kind: "copy", value };
281
+ }
282
+ const array = asArray(value);
283
+ if (array) {
284
+ return {
285
+ kind: "array",
286
+ items: array
287
+ .toJSArray()
288
+ .map((entry) =>
289
+ planPrunedValue(
290
+ objectsContext,
291
+ copyingContext,
292
+ entry,
293
+ deletedPageIDs,
294
+ true,
295
+ ),
296
+ ),
297
+ };
298
+ }
299
+ const entries = Object.entries(asDictionary(value).toJSObject())
300
+ .filter(([, entry]) => !isPrunedValue(entry, deletedPageIDs))
301
+ .map(([key, entry]) => [
302
+ key,
303
+ planPrunedValue(
304
+ objectsContext,
305
+ copyingContext,
306
+ entry,
307
+ deletedPageIDs,
308
+ true,
309
+ ),
310
+ ]);
311
+ const plan = { kind: "dict", entries };
312
+ if (!nested) return plan;
313
+ const objectID = objectsContext.startNewIndirectObject();
314
+ writePrunedPlan(objectsContext, copyingContext, plan);
315
+ objectsContext.endIndirectObject();
316
+ return { kind: "ref", objectID };
317
+ }
318
+
319
+ /**
320
+ * Write a plan built by planPrunedValue().
321
+ * @private
322
+ * @param {Object} objectsContext - The writer's objects context.
323
+ * @param {Object} copyingContext - Copies unchanged values.
324
+ * @param {Object} plan - The write plan.
325
+ * @returns {void}
326
+ */
327
+ function writePrunedPlan(objectsContext, copyingContext, plan) {
328
+ if (plan.kind === "copy") {
329
+ copyingContext.copyDirectObjectAsIs(plan.value);
330
+ } else if (plan.kind === "null") {
331
+ objectsContext.writeKeyword("null");
332
+ } else if (plan.kind === "ref") {
333
+ objectsContext.writeIndirectObjectReference(plan.objectID);
334
+ } else if (plan.kind === "array") {
335
+ objectsContext.startArray();
336
+ plan.items.forEach((item) =>
337
+ writePrunedPlan(objectsContext, copyingContext, item),
338
+ );
339
+ objectsContext.endArray();
340
+ } else {
341
+ const dictionary = objectsContext.startDictionary();
342
+ plan.entries.forEach(([key, item]) => {
343
+ dictionary.writeKey(key);
344
+ writePrunedPlan(objectsContext, copyingContext, item);
345
+ });
346
+ objectsContext.endDictionary(dictionary);
347
+ }
348
+ }
349
+
350
+ /**
351
+ * Replace a catalog entry when the catalog is written. The native writer
352
+ * rewrites the catalog through its OnCatalogWrite event; the Wasm modifier
353
+ * through _setCatalogEntry().
354
+ * @private
355
+ * @param {Object} writer - The PDF writer.
356
+ * @param {string} key - The catalog key.
357
+ * @param {number} objectID - The new value's object ID, or 0 for null.
358
+ * @returns {void}
359
+ */
360
+ function setCatalogEntry(writer, key, objectID) {
361
+ if (writer._setCatalogEntry) {
362
+ writer._setCatalogEntry(key, objectID);
363
+ return;
364
+ }
365
+ if (!writer._catalogEntries) {
366
+ const entries = new Map();
367
+ writer._catalogEntries = entries;
368
+ writer.requireCatalogUpdate();
369
+ writer.getEvents().on("OnCatalogWrite", (event) => {
370
+ const catalog = event.catalogDictionaryContext;
371
+ entries.forEach((id, name) => {
372
+ catalog.writeKey(name);
373
+ if (id) catalog.writeObjectReferenceValue(id);
374
+ else writer.getObjectsContext().writeKeyword("null");
375
+ });
376
+ });
377
+ }
378
+ writer._catalogEntries.set(key, objectID);
379
+ }
380
+
381
+ /**
382
+ * Rewrite the planned objects and catalog entries without their references
383
+ * to deleted pages.
384
+ * @private
385
+ * @param {Object} writer - The PDF writer.
386
+ * @param {Object} copyingContext - Copies unchanged values.
387
+ * @param {Object} catalog - The catalog entries.
388
+ * @param {{objects: Map<number, number>, catalogKeys: string[]}} plan - The
389
+ * result of planDeletedPageReferences().
390
+ * @param {Set<number>} deletedPageIDs - Object IDs of the deleted pages.
391
+ * @returns {void}
392
+ */
393
+ function pruneDeletedPageReferences(
394
+ writer,
395
+ copyingContext,
396
+ catalog,
397
+ plan,
398
+ deletedPageIDs,
399
+ ) {
400
+ const objectsContext = writer.getObjectsContext();
401
+ const parser = copyingContext.getSourceDocumentParser();
402
+ plan.objects.forEach((generation, objectID) => {
403
+ const plan = planPrunedValue(
404
+ objectsContext,
405
+ copyingContext,
406
+ parser.parseNewObject(objectID),
407
+ deletedPageIDs,
408
+ );
409
+ objectsContext.startModifiedIndirectObject(objectID);
410
+ writePrunedPlan(objectsContext, copyingContext, plan);
411
+ objectsContext.endIndirectObject();
412
+ });
413
+ plan.catalogKeys.forEach((key) => {
414
+ const value = catalog[key];
415
+ if (isPrunedValue(value, deletedPageIDs)) {
416
+ setCatalogEntry(writer, key, 0);
417
+ return;
418
+ }
419
+ const plan = planPrunedValue(
420
+ objectsContext,
421
+ copyingContext,
422
+ value,
423
+ deletedPageIDs,
424
+ );
425
+ const objectID = objectsContext.startNewIndirectObject();
426
+ writePrunedPlan(objectsContext, copyingContext, plan);
427
+ objectsContext.endIndirectObject();
428
+ setCatalogEntry(writer, key, objectID);
429
+ });
430
+ }
431
+
432
+ module.exports = {
433
+ planDeletedPageReferences,
434
+ pruneDeletedPageReferences,
435
+ setCatalogEntry,
436
+ };