@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.
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 +86 -30
  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 +482 -105
  15. package/lib/recipe/appendPage.js +8 -1
  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 +154 -21
  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 +379 -275
  28. package/lib/recipe/parameters.js +11 -2
  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 +260 -173
  33. package/lib/recipe/text.helper.js +176 -10
  34. package/lib/recipe/text.js +420 -115
  35. package/lib/recipe/utils.js +66 -3
  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 +150 -13
  39. package/lib/recipe/vector.js +150 -53
  40. package/lib/recipe/xObjectForm.js +73 -10
  41. package/lib/recipe-constants.js +373 -0
  42. package/muhammara.d.ts +3487 -152
  43. package/package.json +1 -1
@@ -1,3 +1,6 @@
1
+ const { PAGE_CONTEXT_STATE } = require("./utils");
2
+ const { PageSize } = require("../recipe-constants");
3
+
1
4
  this.mediumSizes = {
2
5
  executive: [521.86, 756.0], // north american
3
6
  folio: [612.0, 936.0],
@@ -55,6 +58,12 @@ this.mediumSizes = {
55
58
  sra4: [637.8, 907.09],
56
59
  };
57
60
 
61
+ /**
62
+ * Initialize the page defaults (letter size, one-inch margins), the drawing
63
+ * position and the page-context state.
64
+ * @private
65
+ * @returns {void}
66
+ */
58
67
  exports._setParameters = function _setParameters() {
59
68
  this.debug = false;
60
69
  this.metadata = {
@@ -62,7 +71,7 @@ exports._setParameters = function _setParameters() {
62
71
  };
63
72
 
64
73
  let defaultMargin = 72; // 1 inch.
65
- let defaultSize = "letter";
74
+ let defaultSize = PageSize.LETTER;
66
75
 
67
76
  this.default = {
68
77
  pageSize: this.mediumSizes[defaultSize],
@@ -81,7 +90,7 @@ exports._setParameters = function _setParameters() {
81
90
  };
82
91
 
83
92
  this._margin = Object.assign({}, this.default.pageMargin);
84
- this.contextState = "idle";
93
+ this.contextState = PAGE_CONTEXT_STATE.IDLE;
85
94
  this.modifiedSourcePages = new Set();
86
95
 
87
96
  // Object.assign(this, options);
@@ -1,9 +1,382 @@
1
- function escapePDFLiteralString(value) {
2
- return value.replace(/([\\()])/g, "\\$1");
1
+ var contentStream = require("../content-stream");
2
+ var fontText = require("../font-text");
3
+
4
+ var PdfOperator = contentStream.PdfOperator;
5
+ var TokenKind = contentStream.TokenKind;
6
+ var decodeName = contentStream.decodeName;
7
+ var forEachOperation = contentStream.forEachOperation;
8
+ var tokenKind = contentStream.tokenKind;
9
+
10
+ // PDF dictionary keys and names this module reads.
11
+ var PdfName = Object.freeze({
12
+ RESOURCES: "Resources",
13
+ CONTENTS: "Contents",
14
+ LENGTH: "Length",
15
+ FILTER: "Filter",
16
+ DECODE_PARMS: "DecodeParms",
17
+ XOBJECT: "XObject",
18
+ SUBTYPE: "Subtype",
19
+ FORM: "Form",
20
+ });
21
+
22
+ /**
23
+ * Replace the operands of `Tj` operators whose text, decoded through the
24
+ * active font, equals `text`. Everything else is kept byte for byte.
25
+ *
26
+ * @private
27
+ * @param {string} source Latin-1 content stream.
28
+ * @param {string} text Text to replace.
29
+ * @param {string} replacement Replacement text.
30
+ * @param {function(string): object} fonts Font codec lookup by resource
31
+ * name.
32
+ * @returns {string} Latin-1 content stream.
33
+ * @throws {Error} If a matched font has no glyph for a replacement
34
+ * character.
35
+ */
36
+ function replaceShownText(source, text, replacement, fonts) {
37
+ var result = "";
38
+ var copied = 0;
39
+ var font = null;
40
+ var fontStack = [];
41
+ var encoded = new Map();
42
+
43
+ /**
44
+ * Track the font and rewrite one matching `Tj` operand.
45
+ *
46
+ * @private
47
+ * @param {{operator: string, operands: Array<{token: string, start:
48
+ * number, end: number}>}} operation Content-stream operation.
49
+ * @returns {void}
50
+ */
51
+ function replaceOperation(operation) {
52
+ var operator = operation.operator;
53
+ var operands = operation.operands;
54
+ if (operator === PdfOperator.SAVE_STATE) fontStack.push(font);
55
+ if (operator === PdfOperator.RESTORE_STATE && fontStack.length) {
56
+ font = fontStack.pop();
57
+ }
58
+ if (
59
+ operator === PdfOperator.SET_FONT &&
60
+ operands.length === 2 &&
61
+ tokenKind(operands[0].token) === TokenKind.NAME
62
+ ) {
63
+ font = decodeName(operands[0].token);
64
+ }
65
+ var kind = operands.length === 1 ? tokenKind(operands[0].token) : null;
66
+ if (
67
+ operator !== PdfOperator.SHOW_TEXT ||
68
+ (kind !== TokenKind.LITERAL_STRING && kind !== TokenKind.HEX_STRING)
69
+ ) {
70
+ return;
71
+ }
72
+ var operand = operands[0];
73
+ var codec = fonts(font);
74
+ var bytes = contentStream.stringBytes(operand.token);
75
+ if (codec.decode(bytes) !== text) return;
76
+ if (!encoded.has(codec)) {
77
+ try {
78
+ encoded.set(codec, codec.encode(replacement));
79
+ } catch (error) {
80
+ throw new Error(
81
+ "replaceText cannot write the replacement: " + error.message,
82
+ );
83
+ }
84
+ }
85
+ result +=
86
+ source.slice(copied, operand.start) +
87
+ contentStream.stringToken(encoded.get(codec), kind);
88
+ copied = operand.end;
89
+ }
90
+
91
+ forEachOperation(source, replaceOperation);
92
+ return result + source.slice(copied);
93
+ }
94
+
95
+ /**
96
+ * Remove text-showing operators from a page content stream. `'` and `"`
97
+ * keep their line advance and spacing effects as `T*`, `Tw`, and `Tc`.
98
+ *
99
+ * @private
100
+ * @param {string} source Latin-1 content stream.
101
+ * @returns {{content: string, xObjectNames: string[]}} Latin-1 content
102
+ * stream without shown text, and the XObject names it paints with `Do`.
103
+ */
104
+ function removeTextShowingOperators(source) {
105
+ var result = "";
106
+ var xObjectNames = [];
107
+ var operandStart = 0;
108
+
109
+ /**
110
+ * Copy one operation, dropping shown text.
111
+ *
112
+ * @private
113
+ * @param {{operator: string, operands: Array<{token: string}>, end:
114
+ * number}} operation Content-stream operation.
115
+ * @returns {void}
116
+ */
117
+ function removeOperation(operation) {
118
+ var operator = operation.operator;
119
+ var operands = [];
120
+ for (var index = 0; index < operation.operands.length; index++) {
121
+ operands.push(operation.operands[index].token);
122
+ }
123
+ if (
124
+ operator === PdfOperator.PAINT_XOBJECT &&
125
+ operands.length &&
126
+ tokenKind(operands[0]) === TokenKind.NAME
127
+ ) {
128
+ xObjectNames.push(decodeName(operands[0]));
129
+ }
130
+
131
+ if (
132
+ operator === PdfOperator.SHOW_TEXT ||
133
+ operator === PdfOperator.SHOW_TEXT_ARRAY
134
+ ) {
135
+ result += " ";
136
+ } else if (operator === PdfOperator.NEXT_LINE_SHOW_TEXT) {
137
+ result += " T*";
138
+ } else if (operator === PdfOperator.SPACING_NEXT_LINE_SHOW_TEXT) {
139
+ // `aw ac string "`; a malformed one is skipped by viewers, so drop it.
140
+ if (operands.length === 3)
141
+ result += " " + operands[0] + " Tw " + operands[1] + " Tc T*";
142
+ else result += " ";
143
+ } else {
144
+ result += source.slice(operandStart, operation.end);
145
+ }
146
+ operandStart = operation.end;
147
+ }
148
+
149
+ forEachOperation(source, removeOperation);
150
+ return {
151
+ content: result + source.slice(operandStart),
152
+ xObjectNames: xObjectNames,
153
+ };
154
+ }
155
+
156
+ /**
157
+ * Resolve an object through an indirect reference.
158
+ *
159
+ * @param {Recipe} recipe Recipe with an open source reader.
160
+ * @param {object} object PDF object or indirect reference.
161
+ * @returns {object} Resolved PDF object.
162
+ */
163
+ function resolve(recipe, object) {
164
+ if (
165
+ object &&
166
+ object.getType() === recipe.muhammara.ePDFObjectIndirectObjectReference
167
+ ) {
168
+ return recipe.pdfReader.parseNewObject(
169
+ object.toPDFIndirectObjectReference().getObjectID(),
170
+ );
171
+ }
172
+ return object;
173
+ }
174
+
175
+ /**
176
+ * Look up a dictionary entry and resolve it.
177
+ *
178
+ * @param {Recipe} recipe Recipe with an open source reader.
179
+ * @param {object} dictionary PDF dictionary.
180
+ * @param {string} key Entry name.
181
+ * @returns {object|null} Resolved PDF object, or null when missing.
182
+ */
183
+ function lookup(recipe, dictionary, key) {
184
+ return dictionary && dictionary.exists(key)
185
+ ? resolve(recipe, dictionary.queryObject(key))
186
+ : null;
187
+ }
188
+
189
+ /**
190
+ * Collect the content stream object IDs of a page, in drawing order, and the
191
+ * page's resources dictionary, including inherited resources.
192
+ *
193
+ * @param {Recipe} recipe Recipe with an open source reader.
194
+ * @param {number} pageIndex Zero-based page index.
195
+ * @private
196
+ * @returns {{streamIds: number[], resources: object|null}} Page content.
197
+ * @throws {Error} If `Contents` holds a direct stream.
198
+ */
199
+ function pageContent(recipe, pageIndex) {
200
+ var muhammara = recipe.muhammara;
201
+ var page = recipe.pdfReader.parsePage(pageIndex).getDictionary();
202
+ var resources = contentStream.inheritedResources(
203
+ recipe.pdfReader,
204
+ muhammara,
205
+ page,
206
+ );
207
+
208
+ var contents = page.exists(PdfName.CONTENTS)
209
+ ? page.queryObject(PdfName.CONTENTS)
210
+ : null;
211
+ var resolved = resolve(recipe, contents);
212
+ var entries = [];
213
+ if (contents && resolved.getType() === muhammara.ePDFObjectArray) {
214
+ var array = resolved.toPDFArray();
215
+ for (var index = 0; index < array.getLength(); index++) {
216
+ entries.push(array.queryObject(index));
217
+ }
218
+ } else if (contents) {
219
+ entries.push(contents);
220
+ }
221
+
222
+ return {
223
+ resources: resources,
224
+ streamIds: entries.map(function (entry) {
225
+ if (entry.getType() !== muhammara.ePDFObjectIndirectObjectReference) {
226
+ throw new Error("removeText supports indirect page content streams");
227
+ }
228
+ return entry.toPDFIndirectObjectReference().getObjectID();
229
+ }),
230
+ };
231
+ }
232
+
233
+ /**
234
+ * Read and decode one content stream as a Latin-1 string.
235
+ *
236
+ * @param {Recipe} recipe Recipe with an open source reader.
237
+ * @param {number} objectId Stream object ID.
238
+ * @returns {string} Decoded stream bytes, one character per byte.
239
+ */
240
+ function readContentStream(recipe, objectId) {
241
+ return contentStream.readStreamString(
242
+ recipe.pdfReader,
243
+ recipe.pdfReader.parseNewObject(objectId).toPDFStream(),
244
+ );
245
+ }
246
+
247
+ /**
248
+ * Write a new unfiltered stream and point the page at it.
249
+ *
250
+ * @param {Recipe} recipe Recipe in modify mode.
251
+ * @param {number} pageIndex Zero-based page index.
252
+ * @param {number} objectId Object ID to replace on this page.
253
+ * @param {string} content Latin-1 stream content.
254
+ * @private
255
+ * @returns {void}
256
+ */
257
+ function replaceContentStream(recipe, pageIndex, objectId, content) {
258
+ var objectsContext = recipe.writer.getObjectsContext();
259
+ var replacementObjectId = objectsContext.startNewIndirectObject();
260
+ var replacementStream = objectsContext.startUnfilteredPDFStream();
261
+
262
+ replacementStream.getWriteStream().write(Buffer.from(content, "latin1"));
263
+ objectsContext.endPDFStream(replacementStream).endIndirectObject();
264
+ recipe.writer.replaceObject(pageIndex, objectId, replacementObjectId);
265
+ }
266
+
267
+ /**
268
+ * Overwrite a source stream object in place with new unfiltered content,
269
+ * keeping every dictionary entry except the length and filters.
270
+ *
271
+ * @param {Recipe} recipe Recipe in modify mode.
272
+ * @param {object} copyingContext Copying context for the modified file.
273
+ * @param {number} objectId Stream object ID.
274
+ * @param {string} content Latin-1 stream content.
275
+ * @private
276
+ * @returns {void}
277
+ */
278
+ function rewriteStream(recipe, copyingContext, objectId, content) {
279
+ var source = recipe.pdfReader
280
+ .parseNewObject(objectId)
281
+ .toPDFStream()
282
+ .getDictionary()
283
+ .toJSObject();
284
+ var objectsContext = recipe.writer.getObjectsContext();
285
+ objectsContext.startModifiedIndirectObject(objectId);
286
+ var dictionary = objectsContext.startDictionary();
287
+
288
+ Object.keys(source).forEach(function (key) {
289
+ if (
290
+ key === PdfName.LENGTH ||
291
+ key === PdfName.FILTER ||
292
+ key === PdfName.DECODE_PARMS
293
+ )
294
+ return;
295
+ dictionary.writeKey(key);
296
+ copyingContext.copyDirectObjectAsIs(source[key]);
297
+ });
298
+
299
+ var stream = objectsContext.startUnfilteredPDFStream(dictionary);
300
+ stream.getWriteStream().write(Buffer.from(content, "latin1"));
301
+ objectsContext.endPDFStream(stream).endIndirectObject();
3
302
  }
4
303
 
5
304
  /**
6
- * Replace literal text-showing operands in a page's single content stream.
305
+ * Collect the Form XObjects a content stream paints, following nested forms.
306
+ *
307
+ * @param {Recipe} recipe Recipe with an open source reader.
308
+ * @param {string[]} names XObject names used with `Do`.
309
+ * @param {object|null} resources Resources dictionary for those names.
310
+ * @param {Map<number, string>} forms Collected form IDs and stripped content.
311
+ * @private
312
+ * @returns {void}
313
+ */
314
+ function collectForms(recipe, names, resources, forms) {
315
+ var xObjects = lookup(recipe, resources, PdfName.XOBJECT);
316
+ names.forEach(function (name) {
317
+ var reference =
318
+ xObjects && xObjects.exists(name) && xObjects.queryObject(name);
319
+ if (
320
+ !reference ||
321
+ reference.getType() !== recipe.muhammara.ePDFObjectIndirectObjectReference
322
+ ) {
323
+ return;
324
+ }
325
+ var objectId = reference.toPDFIndirectObjectReference().getObjectID();
326
+ if (forms.has(objectId)) return;
327
+
328
+ var dictionary = recipe.pdfReader
329
+ .parseNewObject(objectId)
330
+ .toPDFStream()
331
+ .getDictionary();
332
+ var subtype = lookup(recipe, dictionary, PdfName.SUBTYPE);
333
+ if (!subtype || subtype.toPDFName().value !== PdfName.FORM) return;
334
+
335
+ var source = readContentStream(recipe, objectId);
336
+ var stripped = removeTextShowingOperators(source);
337
+ forms.set(objectId, stripped.content === source ? null : stripped.content);
338
+ collectForms(
339
+ recipe,
340
+ stripped.xObjectNames,
341
+ lookup(recipe, dictionary, PdfName.RESOURCES) || resources,
342
+ forms,
343
+ );
344
+ });
345
+ }
346
+
347
+ /**
348
+ * Validate a one-based page number argument.
349
+ *
350
+ * @param {Recipe} recipe Recipe with an open source reader.
351
+ * @param {*} pageNumber Candidate page number.
352
+ * @param {string} methodName Method name used in errors.
353
+ * @private
354
+ * @returns {void}
355
+ * @throws {TypeError} If the page number is not a positive integer.
356
+ * @throws {RangeError} If the source has no such page.
357
+ */
358
+ function assertPageNumber(recipe, pageNumber, methodName) {
359
+ if (!Number.isInteger(pageNumber) || pageNumber < 1) {
360
+ throw new TypeError(methodName + " expects a positive integer page number");
361
+ }
362
+ if (!recipe.pdfReader || pageNumber > recipe.pdfReader.getPagesCount()) {
363
+ throw new RangeError(
364
+ methodName + " page " + pageNumber + " does not exist",
365
+ );
366
+ }
367
+ }
368
+
369
+ /**
370
+ * Replace text shown with `Tj` in a page's single content stream. Each operand
371
+ * is decoded through the font selected by `Tf` (its `/ToUnicode` CMap, then
372
+ * its `/Encoding` and `/Differences`), and a match is compared with `text`.
373
+ * The replacement is encoded through the same font and written back as a
374
+ * literal or hex string, like the original operand. Only whole `Tj` operands
375
+ * match; `TJ`, `'`, and `"` operands and text split across operators are left
376
+ * unchanged.
377
+ *
378
+ * The replacement can only use glyphs the font already has. Embedded subset
379
+ * fonts usually carry just the glyphs of their original text.
7
380
  *
8
381
  * @name replaceText
9
382
  * @function
@@ -11,18 +384,22 @@ function escapePDFLiteralString(value) {
11
384
  * @param {string} text Text to replace.
12
385
  * @param {string} replacement Replacement text.
13
386
  * @param {number} pageNumber One-based page number.
387
+ * @returns {Recipe} The Recipe instance.
388
+ * @throws {TypeError} If text or replacement is not a string, or if the page
389
+ * number is not a positive integer.
390
+ * @throws {RangeError} If the source document has no such page.
391
+ * @throws {Error} If the page does not have one indirect content stream, or
392
+ * the matched font cannot be read, has a malformed `/Widths` array, or has
393
+ * no glyph for a replacement character.
14
394
  */
15
395
  exports.replaceText = function replaceText(text, replacement, pageNumber) {
16
396
  if (typeof text !== "string" || typeof replacement !== "string") {
17
397
  throw new TypeError("replaceText expects text and replacement strings");
18
398
  }
19
- if (!Number.isInteger(pageNumber) || pageNumber < 1) {
20
- throw new TypeError("replaceText expects a positive integer page number");
21
- }
22
-
399
+ assertPageNumber(this, pageNumber, "replaceText");
23
400
  var pageIndex = pageNumber - 1;
24
401
  var page = this.pdfReader.parsePage(pageIndex).getDictionary();
25
- var contents = page.queryObject("Contents");
402
+ var contents = page.queryObject(PdfName.CONTENTS);
26
403
 
27
404
  if (
28
405
  !contents ||
@@ -32,35 +409,92 @@ exports.replaceText = function replaceText(text, replacement, pageNumber) {
32
409
  }
33
410
 
34
411
  var contentsObjectId = contents.toPDFIndirectObjectReference().getObjectID();
35
- var stream = this.pdfReader.parseNewObject(contentsObjectId).toPDFStream();
36
- var streamReader = this.pdfReader.startReadingFromStream(stream);
37
- var chunks = [];
38
-
39
- while (streamReader.notEnded()) {
40
- chunks.push(Buffer.from(streamReader.read(65536)));
41
- }
42
-
43
- var source = Buffer.concat(chunks).toString("latin1");
44
- var textPattern = new RegExp(
45
- "\\(" + escapePDFLiteralString(text) + "\\)(\\s+Tj\\b)",
46
- "g",
47
- );
48
- var replaced = source.replace(
49
- textPattern,
50
- "(" + escapePDFLiteralString(replacement) + ")$1",
412
+ var source = readContentStream(this, contentsObjectId);
413
+ var replaced = replaceShownText(
414
+ source,
415
+ text,
416
+ replacement,
417
+ fontText.createPageFontLookup(this.pdfReader, this.muhammara, pageIndex),
51
418
  );
52
419
 
53
420
  if (replaced === source) {
54
421
  return this;
55
422
  }
56
423
 
57
- var objectsContext = this.writer.getObjectsContext();
58
- var replacementObjectId = objectsContext.startNewIndirectObject();
59
- var replacementStream = objectsContext.startUnfilteredPDFStream();
424
+ replaceContentStream(this, pageIndex, contentsObjectId, replaced);
425
+ this.modifiedSourcePages.add(pageNumber);
426
+ return this;
427
+ };
428
+
429
+ /**
430
+ * Remove all shown text from a page, for example before placing a fresh OCR
431
+ * text layer. Text-showing operators (`Tj`, `TJ`, `'`, `"`) are dropped;
432
+ * graphics, images, and text state are kept. Annotation appearances are not
433
+ * changed.
434
+ *
435
+ * The page's source content streams are rewritten in place, so a stream shared
436
+ * with another page loses its text there too. Content added with `editPage()`
437
+ * in the same Recipe is kept.
438
+ *
439
+ * @name removeText
440
+ * @function
441
+ * @memberof Recipe#
442
+ * @param {number} pageNumber One-based page number.
443
+ * @param {RemoveTextOptions} [options] Removal options.
444
+ * @param {boolean} [options.forms=false] Also remove text from the Form
445
+ * XObjects the page paints, including nested forms. Other pages that paint
446
+ * the same form lose that text too.
447
+ * @returns {Recipe} The Recipe instance.
448
+ * @throws {TypeError} If the page number is not a positive integer, or the
449
+ * options are not an object.
450
+ * @throws {RangeError} If the source document has no such page.
451
+ * @throws {Error} If the page's `Contents` holds a direct stream.
452
+ */
453
+ exports.removeText = function removeText(pageNumber, options) {
454
+ assertPageNumber(this, pageNumber, "removeText");
455
+ if (
456
+ options !== undefined &&
457
+ (options === null || typeof options !== "object")
458
+ ) {
459
+ throw new TypeError("removeText expects an options object");
460
+ }
461
+
462
+ var recipe = this;
463
+ var page = pageContent(this, pageNumber - 1);
464
+ var source = page.streamIds
465
+ .map(function (objectId) {
466
+ return readContentStream(recipe, objectId);
467
+ })
468
+ .join("\n");
469
+ var stripped = removeTextShowingOperators(source);
470
+ var forms = new Map();
471
+ if (options && options.forms) {
472
+ collectForms(this, stripped.xObjectNames, page.resources, forms);
473
+ }
474
+
475
+ // Rewrite each source stream once per Recipe; the result never changes.
476
+ if (!this.textRemovedStreamIds) this.textRemovedStreamIds = new Set();
477
+ var rewritten = this.textRemovedStreamIds;
478
+ var copyingContext = this.writer.createPDFCopyingContextForModifiedFile();
479
+ if (stripped.content !== source && !rewritten.has(page.streamIds[0])) {
480
+ page.streamIds.forEach(function (objectId, index) {
481
+ rewriteStream(
482
+ recipe,
483
+ copyingContext,
484
+ objectId,
485
+ index === 0 ? stripped.content : "",
486
+ );
487
+ rewritten.add(objectId);
488
+ });
489
+ }
490
+ forms.forEach(function (content, objectId) {
491
+ if (content !== null && !rewritten.has(objectId)) {
492
+ rewriteStream(recipe, copyingContext, objectId, content);
493
+ rewritten.add(objectId);
494
+ }
495
+ });
496
+ copyingContext.end();
60
497
 
61
- replacementStream.getWriteStream().write(Array.from(Buffer.from(replaced)));
62
- objectsContext.endPDFStream(replacementStream).endIndirectObject();
63
- this.writer.replaceObject(pageIndex, contentsObjectId, replacementObjectId);
64
498
  this.modifiedSourcePages.add(pageNumber);
65
499
  return this;
66
500
  };