@muhammara/native-core 7.0.0-alpha.1 → 7.0.0-beta.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/index.js CHANGED
@@ -23,6 +23,51 @@ exports.createMuhammara = function createMuhammara(muhammara) {
23
23
  eventParams.writer = this;
24
24
  this.getEvents().emit(eventName, eventParams);
25
25
  };
26
+ muhammara.PDFWriter.prototype.replaceObject = function (
27
+ pageIndex,
28
+ sourceObjectId,
29
+ replacementObjectId,
30
+ options,
31
+ ) {
32
+ if (options && options.scope === "global") {
33
+ var copyingContext = this.createPDFCopyingContextForModifiedFile();
34
+ var pageCount = copyingContext.getSourceDocumentParser().getPagesCount();
35
+
36
+ copyingContext.end();
37
+ for (var index = 0; index < pageCount; ++index) {
38
+ this.replaceObject(index, sourceObjectId, replacementObjectId);
39
+ }
40
+ return this;
41
+ }
42
+
43
+ var copyingContext = this.createPDFCopyingContextForModifiedFile();
44
+ var parser = copyingContext.getSourceDocumentParser();
45
+ var pageObjectId = parser.getPageObjectID(pageIndex);
46
+ var pageObject = parser.parsePage(pageIndex).getDictionary().toJSObject();
47
+ var objectsContext = this.getObjectsContext();
48
+ var pageDictionary;
49
+
50
+ objectsContext.startModifiedIndirectObject(pageObjectId);
51
+ pageDictionary = objectsContext.startDictionary();
52
+
53
+ Object.getOwnPropertyNames(pageObject).forEach(function (key) {
54
+ var value = pageObject[key];
55
+
56
+ pageDictionary.writeKey(key);
57
+ if (
58
+ value.getType() === muhammara.ePDFObjectIndirectObjectReference &&
59
+ value.toPDFIndirectObjectReference().getObjectID() === sourceObjectId
60
+ ) {
61
+ pageDictionary.writeObjectReferenceValue(replacementObjectId);
62
+ } else {
63
+ copyingContext.copyDirectObjectAsIs(value);
64
+ }
65
+ });
66
+
67
+ objectsContext.endDictionary(pageDictionary).endIndirectObject();
68
+ copyingContext.end();
69
+ return this;
70
+ };
26
71
  muhammara.PDFStreamForResponse = require("./lib/PDFStreamForResponse");
27
72
  muhammara.PDFWStreamForFile = require("./lib/PDFWStreamForFile");
28
73
  muhammara.PDFRStreamForFile = require("./lib/PDFRStreamForFile");
package/lib/Recipe.js CHANGED
@@ -11,7 +11,7 @@ const streams = require("memory-streams");
11
11
  * @param {string|Buffer} src - The file path or Buffer of the source file.
12
12
  * @param {string} [output] - The path of the output file uses src if its not a buffer.
13
13
  * @param {Object} [options] - The options for pdfDoc
14
- * @param {number} [options.version] - The pdf version
14
+ * @param {number} [options.version] - The pdf version: 1.0 through 1.7 or 2.0, defaults to 1.7
15
15
  * @param {string} [options.author] - The author
16
16
  * @param {string} [options.title] - The title
17
17
  * @param {string} [options.subject] - The subject
@@ -121,7 +121,7 @@ class Recipe {
121
121
  }
122
122
 
123
123
  _getVersion(version) {
124
- const supportedVersions = [1.0, 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7];
124
+ const supportedVersions = [1.0, 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7, 2.0];
125
125
  if (!supportedVersions.includes(version)) {
126
126
  version = 1.7;
127
127
  }
@@ -149,12 +149,14 @@ class Recipe {
149
149
  */
150
150
  read(inSrc) {
151
151
  const isForExternal = inSrc ? true : false;
152
+ let pdfReader = null;
153
+ let isAdopted = false;
152
154
  try {
153
155
  let src = isForExternal ? inSrc : this.src;
154
156
  if (this.isBufferSrc) {
155
157
  src = new muhammara.PDFRStreamForBuffer(this.src);
156
158
  }
157
- const pdfReader = muhammara.createReader(src, this.encryptOptions);
159
+ pdfReader = muhammara.createReader(src, this.encryptOptions);
158
160
  const pages = pdfReader.getPagesCount();
159
161
  if (pages == 0) {
160
162
  // broken or modify password protected
@@ -206,13 +208,50 @@ class Recipe {
206
208
  metadata[page.pageNumber] = page;
207
209
  }
208
210
  if (!isForExternal) {
211
+ this._releaseReader();
209
212
  this.pdfReader = pdfReader;
210
213
  this.metadata = metadata;
214
+ isAdopted = true;
211
215
  }
212
216
  return metadata;
213
217
  } catch (err) {
214
218
  throw new Error(err);
219
+ } finally {
220
+ // Only the recipe source reader outlives read(); anything else would keep
221
+ // the file open until the process exits.
222
+ if (pdfReader && !isAdopted) {
223
+ pdfReader.end();
224
+ }
225
+ }
226
+ }
227
+
228
+ /**
229
+ * Release the source reader and the file handle it holds.
230
+ * @private
231
+ * @returns {void}
232
+ */
233
+ _releaseReader() {
234
+ const pdfReader = this.pdfReader;
235
+ if (!pdfReader) {
236
+ return;
215
237
  }
238
+ this.pdfReader = null;
239
+ pdfReader.end();
240
+ }
241
+
242
+ /**
243
+ * Get the source reader, which endPDF() releases.
244
+ * @private
245
+ * @returns {Object} The source PDF reader.
246
+ * @throws {Error} If the reader was already released by endPDF().
247
+ */
248
+ _getReader() {
249
+ if (!this.pdfReader) {
250
+ throw new Error(
251
+ "The source PDF reader has been released by endPDF(). Read the source before ending the document.",
252
+ );
253
+ }
254
+ return this.pdfReader;
216
255
  }
217
256
 
218
257
  /**
@@ -256,6 +295,12 @@ class Recipe {
256
295
  this._writeInfo();
257
296
  this.writer.end();
258
297
  }
298
+
299
+ // Every step above may still read the source; _insertPages() and _encrypt()
300
+ // below rename the output, which is the source itself when no separate
301
+ // output was given, and Windows refuses that while the reader holds it.
302
+ this._releaseReader();
303
+
259
304
  if (this.needToInsertPages) {
260
305
  if (this.isBufferSrc) {
261
306
  // eslint-disable-next-line no-console
@@ -1,5 +1,5 @@
1
1
  const muhammara = require("../muhammara");
2
- const hummusUtils = require("./utils");
2
+ const utils = require("./utils");
3
3
 
4
4
  /**
5
5
  * Append pages from the other pdf to the current pdf
@@ -16,10 +16,19 @@ exports.appendPage = function appendPage(pdfSrc, pages = []) {
16
16
  }
17
17
  // Using stream so it can be closed to release reader resource (Issue #61)
18
18
  const instream = new muhammara.PDFRStreamForFile(pdfSrc);
19
- const pdfReader = muhammara.createReader(instream);
20
- const pageCount = pdfReader.getPagesCount();
21
- pdfReader.end();
22
- instream.close();
19
+ let pageCount;
20
+ try {
21
+ const pdfReader = muhammara.createReader(instream);
22
+ try {
23
+ pageCount = pdfReader.getPagesCount();
24
+ } finally {
25
+ pdfReader.end();
26
+ }
27
+ } finally {
28
+ // An unreadable source must not leave the file open, which would keep it
29
+ // locked on Windows for the life of the process.
30
+ instream.close();
31
+ }
23
32
 
24
33
  // prevent unmatched pagenumber
25
34
  const transformPageNumber = (pageNum) => {
@@ -35,11 +44,11 @@ exports.appendPage = function appendPage(pdfSrc, pages = []) {
35
44
  }
36
45
  });
37
46
  if (pages.length > 0) {
38
- hummusUtils.appendPDFPagesFromPDFWithAnnotations(this.writer, pdfSrc, {
47
+ utils.appendPDFPagesFromPDFWithAnnotations(this.writer, pdfSrc, {
39
48
  specificRanges: pages,
40
49
  });
41
50
  } else {
42
- hummusUtils.appendPDFPagesFromPDFWithAnnotations(this.writer, pdfSrc);
51
+ utils.appendPDFPagesFromPDFWithAnnotations(this.writer, pdfSrc);
43
52
  }
44
53
  return this;
45
54
  };
@@ -33,60 +33,63 @@ exports._readInfo = function _readInfo() {
33
33
  ? new muhammara.PDFRStreamForBuffer(this.src)
34
34
  : this.src;
35
35
  const copyCtx = this.writer.createPDFCopyingContext(copyFrom);
36
- const infoDict = copyCtx
37
- .getSourceDocumentParser()
38
- .queryDictionaryObject(
39
- copyCtx.getSourceDocumentParser().getTrailer(),
40
- "Info",
41
- );
36
+ try {
37
+ const infoDict = copyCtx
38
+ .getSourceDocumentParser()
39
+ .queryDictionaryObject(
40
+ copyCtx.getSourceDocumentParser().getTrailer(),
41
+ "Info",
42
+ );
42
43
 
43
- const oldInfo =
44
- infoDict && infoDict.toJSObject ? infoDict.toJSObject() : null;
44
+ const oldInfo =
45
+ infoDict && infoDict.toJSObject ? infoDict.toJSObject() : null;
45
46
 
46
- if (oldInfo) {
47
- this.infoDictionary = {};
48
- Object.getOwnPropertyNames(oldInfo).forEach((key) => {
49
- if (!oldInfo[key]) {
50
- return;
51
- }
52
- const oldInforSrc = this._parseObjectByType(oldInfo[key]);
53
- if (!oldInforSrc) {
54
- return;
55
- }
56
- switch (key) {
57
- case "Trapped":
58
- if (oldInforSrc && oldInforSrc.value) {
59
- this.infoDictionary.trapped = oldInforSrc.value;
60
- }
61
- break;
62
- case "CreationDate":
63
- if (oldInforSrc && oldInforSrc.value) {
64
- this.infoDictionary.creationDate = oldInforSrc.value;
65
- }
66
- break;
67
- case "ModDate":
68
- if (oldInforSrc && oldInforSrc.value) {
69
- this.infoDictionary.modDate = oldInforSrc.value;
70
- }
71
- break;
72
- case "Creator":
73
- if (oldInforSrc && oldInforSrc.toText) {
74
- this.infoDictionary.creator = oldInforSrc.toText();
75
- }
76
- break;
77
- case "Producer":
78
- if (oldInforSrc && oldInforSrc.toText) {
79
- this.infoDictionary.producer = oldInforSrc.toText();
80
- }
81
- break;
82
- default:
83
- if (oldInforSrc && oldInforSrc.toText) {
84
- this.infoDictionary[key.toLowerCase()] = oldInforSrc.toText();
85
- }
86
- }
87
- });
47
+ if (oldInfo) {
48
+ this.infoDictionary = {};
49
+ Object.getOwnPropertyNames(oldInfo).forEach((key) => {
50
+ if (!oldInfo[key]) {
51
+ return;
52
+ }
53
+ const oldInforSrc = this._parseObjectByType(oldInfo[key]);
54
+ if (!oldInforSrc) {
55
+ return;
56
+ }
57
+ switch (key) {
58
+ case "Trapped":
59
+ if (oldInforSrc && oldInforSrc.value) {
60
+ this.infoDictionary.trapped = oldInforSrc.value;
61
+ }
62
+ break;
63
+ case "CreationDate":
64
+ if (oldInforSrc && oldInforSrc.value) {
65
+ this.infoDictionary.creationDate = oldInforSrc.value;
66
+ }
67
+ break;
68
+ case "ModDate":
69
+ if (oldInforSrc && oldInforSrc.value) {
70
+ this.infoDictionary.modDate = oldInforSrc.value;
71
+ }
72
+ break;
73
+ case "Creator":
74
+ if (oldInforSrc && oldInforSrc.toText) {
75
+ this.infoDictionary.creator = oldInforSrc.toText();
76
+ }
77
+ break;
78
+ case "Producer":
79
+ if (oldInforSrc && oldInforSrc.toText) {
80
+ this.infoDictionary.producer = oldInforSrc.toText();
81
+ }
82
+ break;
83
+ default:
84
+ if (oldInforSrc && oldInforSrc.toText) {
85
+ this.infoDictionary[key.toLowerCase()] = oldInforSrc.toText();
86
+ }
87
+ }
88
+ });
89
+ }
90
+ } finally {
91
+ copyCtx.end();
88
92
  }
89
- copyCtx.end();
90
93
  }
91
94
 
92
95
  return this.infoDictionary;
@@ -168,7 +171,7 @@ exports._writeInfo = function _writeInfo() {
168
171
  infoDictionary.producer =
169
172
  "MuhammaraJS (https://github.com/julianhille/MuhammaraJS)";
170
173
  infoDictionary.creator =
171
- "Hummus-Recipe (https://github.com/chunyenHuang/hummusRecipe)";
174
+ "Muhammara-Recipe (https://github.com/julianhille/MuhammaraJS)";
172
175
 
173
176
  fields.forEach((item) => {
174
177
  let value = options[item.key];
@@ -225,7 +228,7 @@ exports.structure = function structure(output) {
225
228
  // const outputFileType = path.extname(output);
226
229
  const outputFile = fs.openSync(output, "w");
227
230
  const muhammara = this.muhammara;
228
- const pdfReader = this.pdfReader;
231
+ const pdfReader = this._getReader();
229
232
 
230
233
  const tabWidth = " ";
231
234
  const structures = [
@@ -331,7 +334,7 @@ exports._parseObjectByType = function _parseObjectByType(inObject) {
331
334
  return;
332
335
  }
333
336
  const muhammara = this.muhammara;
334
- const pdfReader = this.pdfReader;
337
+ const pdfReader = this._getReader();
335
338
  const type = inObject.getType();
336
339
  const label = muhammara.getTypeLabel(type);
337
340
  const saveToObject = this.pdfStructure || {};
@@ -1,6 +1,6 @@
1
1
  const muhammara = require("../muhammara");
2
2
  const fs = require("fs");
3
- const hummusUtils = require("./utils");
3
+ const utils = require("./utils");
4
4
  /**
5
5
  * Insert a page from the other pdf
6
6
  * @name insertPage
@@ -53,7 +53,7 @@ exports._insertPages = function _insertPages() {
53
53
  const toAppendPage = pageNumber - 1;
54
54
  if (toAppendPage >= 0) {
55
55
  const specificRanges = [[lastInsertedOriginal, toAppendPage]];
56
- hummusUtils.appendPDFPagesFromPDFWithAnnotations(pdfWriter, tmp, {
56
+ utils.appendPDFPagesFromPDFWithAnnotations(pdfWriter, tmp, {
57
57
  specificRanges,
58
58
  });
59
59
  }
@@ -65,11 +65,9 @@ exports._insertPages = function _insertPages() {
65
65
  const specificRanges = [
66
66
  [info.srcPageNumber - 1, info.srcPageNumber - 1],
67
67
  ];
68
- hummusUtils.appendPDFPagesFromPDFWithAnnotations(
69
- pdfWriter,
70
- info.pdfSrc,
71
- { specificRanges },
72
- );
68
+ utils.appendPDFPagesFromPDFWithAnnotations(pdfWriter, info.pdfSrc, {
69
+ specificRanges,
70
+ });
73
71
  });
74
72
  }
75
73
  });
@@ -0,0 +1,64 @@
1
+ function escapePDFLiteralString(value) {
2
+ return value.replace(/([\\()])/g, "\\$1");
3
+ }
4
+
5
+ /**
6
+ * Replace literal text-showing operands in a page's single content stream.
7
+ *
8
+ * @name replaceText
9
+ * @function
10
+ * @memberof Recipe#
11
+ * @param {string} text Text to replace.
12
+ * @param {string} replacement Replacement text.
13
+ * @param {number} [pageNumber=1] One-based page number.
14
+ */
15
+ exports.replaceText = function replaceText(text, replacement, pageNumber) {
16
+ pageNumber = pageNumber || 1;
17
+
18
+ if (typeof text !== "string" || typeof replacement !== "string") {
19
+ throw new TypeError("replaceText expects text and replacement strings");
20
+ }
21
+
22
+ var pageIndex = pageNumber - 1;
23
+ var page = this.pdfReader.parsePage(pageIndex).getDictionary();
24
+ var contents = page.queryObject("Contents");
25
+
26
+ if (
27
+ !contents ||
28
+ contents.getType() !== this.muhammara.ePDFObjectIndirectObjectReference
29
+ ) {
30
+ throw new Error("replaceText supports pages with one content stream");
31
+ }
32
+
33
+ var contentsObjectId = contents.toPDFIndirectObjectReference().getObjectID();
34
+ var stream = this.pdfReader.parseNewObject(contentsObjectId).toPDFStream();
35
+ var streamReader = this.pdfReader.startReadingFromStream(stream);
36
+ var chunks = [];
37
+
38
+ while (streamReader.notEnded()) {
39
+ chunks.push(Buffer.from(streamReader.read(65536)));
40
+ }
41
+
42
+ var source = Buffer.concat(chunks).toString("latin1");
43
+ var textPattern = new RegExp(
44
+ "\\(" + escapePDFLiteralString(text) + "\\)(\\s+Tj\\b)",
45
+ "g",
46
+ );
47
+ var replaced = source.replace(
48
+ textPattern,
49
+ "(" + escapePDFLiteralString(replacement) + ")$1",
50
+ );
51
+
52
+ if (replaced === source) {
53
+ return this;
54
+ }
55
+
56
+ var objectsContext = this.writer.getObjectsContext();
57
+ var replacementObjectId = objectsContext.startNewIndirectObject();
58
+ var replacementStream = objectsContext.startUnfilteredPDFStream();
59
+
60
+ replacementStream.getWriteStream().write(Array.from(Buffer.from(replaced)));
61
+ objectsContext.endPDFStream(replacementStream).endIndirectObject();
62
+ this.writer.replaceObject(pageIndex, contentsObjectId, replacementObjectId);
63
+ return this;
64
+ };
@@ -1,6 +1,6 @@
1
1
  const muhammara = require("../muhammara");
2
2
  const path = require("path");
3
- const hummusUtils = require("./utils");
3
+ const utils = require("./utils");
4
4
 
5
5
  /**
6
6
  * Split the pdf
@@ -16,11 +16,7 @@ exports.split = function split(outputDir = "", prefix) {
16
16
  for (let i = 0; i < this.metadata.pages; i++) {
17
17
  const newPdf = path.join(outputDir, `${prefix}-${i + 1}.pdf`);
18
18
  const pdfWriter = muhammara.createWriter(newPdf);
19
- hummusUtils.appendPDFPageFromPDFWithAnnotations(
20
- pdfWriter,
21
- this.pdfReader,
22
- i,
23
- );
19
+ utils.appendPDFPageFromPDFWithAnnotations(pdfWriter, this._getReader(), i);
24
20
  pdfWriter.end();
25
21
  }
26
22
  return this;
@@ -14,28 +14,30 @@ function appendPDFPageFromPDFWithAnnotations(
14
14
  pageNumber,
15
15
  ) {
16
16
  const cpyCxt = pdfWriter.createPDFCopyingContext(sourcePDFPath);
17
- const cpyCxtParser = cpyCxt.getSourceDocumentParser();
18
- const pageDictionary = cpyCxtParser.parsePageDictionary(pageNumber);
17
+ try {
18
+ const cpyCxtParser = cpyCxt.getSourceDocumentParser();
19
+ const pageDictionary = cpyCxtParser.parsePageDictionary(pageNumber);
19
20
 
20
- if (!pageDictionary.exists(ANNOTATION_PREFIX)) {
21
- cpyCxt.appendPDFPageFromPDF(pageNumber);
22
- } else {
23
- let reffedObjects;
24
- pdfWriter.getEvents().once("OnPageWrite", (params) => {
25
- params.pageDictionaryContext.writeKey(ANNOTATION_PREFIX);
26
- reffedObjects = cpyCxt.copyDirectObjectWithDeepCopy(
27
- pageDictionary.queryObject(ANNOTATION_PREFIX),
28
- );
29
- });
21
+ if (!pageDictionary.exists(ANNOTATION_PREFIX)) {
22
+ cpyCxt.appendPDFPageFromPDF(pageNumber);
23
+ } else {
24
+ let reffedObjects;
25
+ pdfWriter.getEvents().once("OnPageWrite", (params) => {
26
+ params.pageDictionaryContext.writeKey(ANNOTATION_PREFIX);
27
+ reffedObjects = cpyCxt.copyDirectObjectWithDeepCopy(
28
+ pageDictionary.queryObject(ANNOTATION_PREFIX),
29
+ );
30
+ });
30
31
 
31
- cpyCxt.appendPDFPageFromPDF(pageNumber);
32
+ cpyCxt.appendPDFPageFromPDF(pageNumber);
32
33
 
33
- if (reffedObjects && reffedObjects.length > 0) {
34
- cpyCxt.copyNewObjectsForDirectObject(reffedObjects);
34
+ if (reffedObjects && reffedObjects.length > 0) {
35
+ cpyCxt.copyNewObjectsForDirectObject(reffedObjects);
36
+ }
35
37
  }
38
+ } finally {
39
+ cpyCxt.end();
36
40
  }
37
-
38
- cpyCxt.end();
39
41
  }
40
42
 
41
43
  /**
@@ -51,21 +53,23 @@ function appendPDFPagesFromPDFWithAnnotations(
51
53
  options = {},
52
54
  ) {
53
55
  const cpyCxt = pdfWriter.createPDFCopyingContext(sourcePDFPath);
54
- const cpyCxtParser = cpyCxt.getSourceDocumentParser();
56
+ try {
57
+ const cpyCxtParser = cpyCxt.getSourceDocumentParser();
55
58
 
56
- if (options.specificRanges && options.specificRanges.length) {
57
- for (const [start, end] of options.specificRanges) {
58
- for (let i = start; i <= end; ++i) {
59
+ if (options.specificRanges && options.specificRanges.length) {
60
+ for (const [start, end] of options.specificRanges) {
61
+ for (let i = start; i <= end; ++i) {
62
+ appendPDFPageFromPDFWithAnnotations(pdfWriter, sourcePDFPath, i);
63
+ }
64
+ }
65
+ } else {
66
+ for (let i = 0; i < cpyCxtParser.getPagesCount(); ++i) {
59
67
  appendPDFPageFromPDFWithAnnotations(pdfWriter, sourcePDFPath, i);
60
68
  }
61
69
  }
62
- } else {
63
- for (let i = 0; i < cpyCxtParser.getPagesCount(); ++i) {
64
- appendPDFPageFromPDFWithAnnotations(pdfWriter, sourcePDFPath, i);
65
- }
70
+ } finally {
71
+ cpyCxt.end();
66
72
  }
67
-
68
- cpyCxt.end();
69
73
  }
70
74
 
71
75
  exports.ANNOTATION_PREFIX = ANNOTATION_PREFIX;
package/muhammara.d.ts CHANGED
@@ -327,6 +327,16 @@ declare namespace muhammara {
327
327
  export type PDFObjectType = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11;
328
328
  export function getTypeLabel(type: PDFObjectType): string;
329
329
 
330
+ export const ePDFPageContentItemText = 0;
331
+ export const ePDFPageContentItemPath = 1;
332
+ export const ePDFPageContentItemXObject = 2;
333
+ export const ePDFPageContentItemShading = 3;
334
+ export type PDFPageContentItemType =
335
+ | typeof ePDFPageContentItemText
336
+ | typeof ePDFPageContentItemPath
337
+ | typeof ePDFPageContentItemXObject
338
+ | typeof ePDFPageContentItemShading;
339
+
330
340
  export const ePDFPageBoxMediaBox = 0;
331
341
  export const ePDFPageBoxCropBox = 1;
332
342
  export const ePDFPageBoxBleedBox = 2;
@@ -400,6 +410,28 @@ declare namespace muhammara {
400
410
  textMatrix: [number, number, number, number, number, number];
401
411
  }
402
412
 
413
+ /** A content-stream operation that produces a page mark. */
414
+ export interface PDFPageContentItem {
415
+ type: PDFPageContentItemType;
416
+ operation: string;
417
+ }
418
+
419
+ /**
420
+ * Per-call extraction budget. Every field is clamped to a built-in ceiling,
421
+ * so a caller may tighten a limit but never raise it above the default.
422
+ * Omitted fields keep the ceiling.
423
+ */
424
+ export interface PDFExtractionLimits {
425
+ /** Extracted elements or items. Default and ceiling: 100000. */
426
+ maxElements?: number;
427
+ /** Pending operands per operator. Default and ceiling: 1024. */
428
+ maxOperands?: number;
429
+ /** Total extracted text. Default and ceiling: 16777216 (16 MiB). */
430
+ maxTextBytes?: number;
431
+ /** Content-stream objects parsed. Default and ceiling: 1000000. */
432
+ maxParsedObjects?: number;
433
+ }
434
+
403
435
  export interface RectangleDimension {
404
436
  width: Width;
405
437
  height: Height;
@@ -444,15 +476,33 @@ declare namespace muhammara {
444
476
  ): undefined | PDFObject;
445
477
  parseNewObject(objectId: number): PDFObject;
446
478
  getPageObjectID(objectId: number): number;
447
- parsePageDictionary(objectId: number): PDFDictionary;
448
- parsePage(page: number): PDFPageInput;
479
+ parsePageDictionary(pageIndex: number): PDFDictionary;
480
+ parsePage(pageIndex: number): PDFPageInput;
449
481
  /**
450
482
  * Returns text-showing operations in PDF content-stream drawing order.
451
- * Does not decode font character maps or calculate glyph bounds. For safety,
452
- * extraction rejects pages with more than 1,000,000 content objects, 100,000 text
453
- * operations, or 16 MiB of text.
483
+ * Does not decode font character maps or calculate glyph bounds.
484
+ *
485
+ * Throws when the page exceeds the extraction budget. `limits` may only
486
+ * tighten the defaults: higher values are clamped to the built-in ceilings
487
+ * of 1,000,000 content objects, 100,000 text operations, 1024 operands, and
488
+ * 16 MiB of text.
489
+ */
490
+ extractPageText(
491
+ pageIndex: number,
492
+ limits?: PDFExtractionLimits,
493
+ ): PDFTextElement[];
494
+ /**
495
+ * Returns every direct content-stream operation that produces a page mark.
496
+ * White-on-white content is included; non-painting operations are excluded.
497
+ *
498
+ * Shares `extractPageText`'s budget and clamping. `limits.maxTextBytes` is
499
+ * accepted for signature parity but has no effect here, because items carry
500
+ * an operator name rather than extracted text.
454
501
  */
455
- extractPageText(pageIndex: number): PDFTextElement[];
502
+ extractPageContentItems(
503
+ pageIndex: number,
504
+ limits?: PDFExtractionLimits,
505
+ ): PDFPageContentItem[];
456
506
  getObjectsCount(): number;
457
507
  isEncrypted(): boolean;
458
508
  getXrefSize(): number;
@@ -747,6 +797,16 @@ declare namespace muhammara {
747
797
  export type inInterPagesCallback = () => {};
748
798
 
749
799
  export interface PDFWriter {
800
+ /**
801
+ * Replace direct references to an object in a page dictionary.
802
+ * Available only when modifying an existing PDF.
803
+ */
804
+ replaceObject(
805
+ pageIndex: number,
806
+ sourceObjectId: number,
807
+ replacementObjectId: number,
808
+ options?: ObjectReplacementOptions,
809
+ ): this;
750
810
  end(): PDFWriter;
751
811
  createPage(x: PosX, y: PosY, width: Width, height: Height): PDFPage;
752
812
  createPage(): PDFPage;
@@ -845,6 +905,10 @@ declare namespace muhammara {
845
905
  ): void;
846
906
  }
847
907
 
908
+ export interface ObjectReplacementOptions {
909
+ scope?: "global";
910
+ }
911
+
848
912
  namespace Recipe {
849
913
  type CommentOptionsFlag =
850
914
  | "invisible"
@@ -1179,6 +1243,8 @@ declare namespace muhammara {
1179
1243
 
1180
1244
  editPage(pageNumber: number): Recipe;
1181
1245
 
1246
+ replaceText(text: string, replacement: string, pageNumber?: number): Recipe;
1247
+
1182
1248
  pageInfo(pageNumber: number): {
1183
1249
  width: number;
1184
1250
  height: number;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@muhammara/native-core",
3
- "version": "7.0.0-alpha.1",
3
+ "version": "7.0.0-beta.1",
4
4
  "description": "Shared JavaScript API for MuhammaraJS native packages",
5
5
  "homepage": "https://muhammarajs.readthedocs.io/",
6
6
  "license": "Apache-2.0",