@muhammara/native-core 7.0.0-alpha.1 → 7.0.0-beta.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.
@@ -81,6 +81,8 @@ exports._setParameters = function _setParameters() {
81
81
  };
82
82
 
83
83
  this._margin = Object.assign({}, this.default.pageMargin);
84
+ this.contextState = "idle";
85
+ this.modifiedSourcePages = new Set();
84
86
 
85
87
  // Object.assign(this, options);
86
88
  };
@@ -0,0 +1,66 @@
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 One-based page number.
14
+ */
15
+ exports.replaceText = function replaceText(text, replacement, pageNumber) {
16
+ if (typeof text !== "string" || typeof replacement !== "string") {
17
+ throw new TypeError("replaceText expects text and replacement strings");
18
+ }
19
+ if (!Number.isInteger(pageNumber) || pageNumber < 1) {
20
+ throw new TypeError("replaceText expects a positive integer page number");
21
+ }
22
+
23
+ var pageIndex = pageNumber - 1;
24
+ var page = this.pdfReader.parsePage(pageIndex).getDictionary();
25
+ var contents = page.queryObject("Contents");
26
+
27
+ if (
28
+ !contents ||
29
+ contents.getType() !== this.muhammara.ePDFObjectIndirectObjectReference
30
+ ) {
31
+ throw new Error("replaceText supports pages with one content stream");
32
+ }
33
+
34
+ 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",
51
+ );
52
+
53
+ if (replaced === source) {
54
+ return this;
55
+ }
56
+
57
+ var objectsContext = this.writer.getObjectsContext();
58
+ var replacementObjectId = objectsContext.startNewIndirectObject();
59
+ var replacementStream = objectsContext.startUnfilteredPDFStream();
60
+
61
+ replacementStream.getWriteStream().write(Array.from(Buffer.from(replaced)));
62
+ objectsContext.endPDFStream(replacementStream).endIndirectObject();
63
+ this.writer.replaceObject(pageIndex, contentsObjectId, replacementObjectId);
64
+ this.modifiedSourcePages.add(pageNumber);
65
+ return this;
66
+ };
@@ -155,7 +155,10 @@ exports.n_gon = function n_gon(cx, cy, radius, sides = 3, options = {}) {
155
155
 
156
156
  const ngon = _n_gon(sides, cx, cy, radius, options);
157
157
 
158
- this.polygon(ngon, options);
158
+ const drawOptions = Object.assign({}, options, { link: undefined });
159
+ this.polygon(ngon, drawOptions);
160
+ if (options.link)
161
+ this.link(options.link, cx - radius, cy - radius, radius * 2, radius * 2);
159
162
 
160
163
  if (options.rotationVertice) {
161
164
  delete options["rotationOrigin"]; // cleanup n-gon generated point
@@ -220,6 +223,7 @@ exports.star = function star(cx, cy, radius, points = 5, options = {}) {
220
223
  }
221
224
 
222
225
  const starOptions = Object.assign({}, options);
226
+ delete starOptions.link;
223
227
 
224
228
  if (odd(points)) {
225
229
  starPath = _oddStar(_n_gon(points, cx, cy, radius, options));
@@ -265,6 +269,9 @@ exports.star = function star(cx, cy, radius, points = 5, options = {}) {
265
269
 
266
270
  this.polygon(starPath, starOptions);
267
271
 
272
+ if (options.link)
273
+ this.link(options.link, cx - radius, cy - radius, radius * 2, radius * 2);
274
+
268
275
  if (options.debug) {
269
276
  this.circle(cx, cy, radius, { width: 1, stroke: "#00ff00" });
270
277
  this.circle(cx, cy, 2, { fill: "#ff0000" });
@@ -375,6 +382,7 @@ exports.triangle = function triangle(x, y, traits, options = {}) {
375
382
  let traitID = options.traitID || options.traitsID || "sss";
376
383
  let position = options.position ? options.position.toLowerCase() : "default";
377
384
  let triopts = Object.assign({}, options);
385
+ delete triopts.link;
378
386
 
379
387
  if (traits.length !== 3) {
380
388
  throw new Error(
@@ -433,6 +441,19 @@ exports.triangle = function triangle(x, y, traits, options = {}) {
433
441
  }
434
442
 
435
443
  this.polygon(trigon, triopts);
444
+ if (options.link) {
445
+ const xs = trigon.map((point) => point[0]);
446
+ const ys = trigon.map((point) => point[1]);
447
+ const left = Math.min(...xs);
448
+ const top = Math.min(...ys);
449
+ this.link(
450
+ options.link,
451
+ left,
452
+ top,
453
+ Math.max(...xs) - left,
454
+ Math.max(...ys) - top,
455
+ );
456
+ }
436
457
 
437
458
  if (options.debug) {
438
459
  let angle = triopts.rotation || 0;
@@ -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;
@@ -1,5 +1,5 @@
1
1
  const charSpacing = function charSpacing(text, charSpace) {
2
- let txt = text.trim();
2
+ let txt = text.replace(/^(?:(?!\u00a0)\s)+|(?:(?!\u00a0)\s)+$/g, "");
3
3
  return txt.length ? (txt.length - 1) * charSpace : 0;
4
4
  };
5
5
 
@@ -2,6 +2,7 @@ const LineBreaker = require("linebreak");
2
2
  const { Word, Line, Column } = require("./text.helper");
3
3
  const { htmlToTextObjects } = require("./htmlToTextObjects");
4
4
  const { Color, xObjectForm } = require("./xObjectForm");
5
+ const { linkPdf } = require("./annotation");
5
6
  const muhammara = require("../muhammara");
6
7
  const { UsedFont } = require("../muhammara");
7
8
 
@@ -258,6 +259,8 @@ exports.text = function text(text = "", x, y, options = {}) {
258
259
  return this;
259
260
  }
260
261
  options = _initOptions(this, x, y, options);
262
+ const linkX = this.x;
263
+ const linkY = this.y;
261
264
 
262
265
  const targetAnnotations = options;
263
266
  const originCoord = this._calibrateCoordinate(
@@ -273,6 +276,7 @@ exports.text = function text(text = "", x, y, options = {}) {
273
276
  originCoord.ny,
274
277
  );
275
278
  pathOptions.html = options.html;
279
+ pathOptions.link = options.link;
276
280
  pathOptions.hilite = options.hilite;
277
281
 
278
282
  // save text state for continued text?
@@ -294,6 +298,7 @@ exports.text = function text(text = "", x, y, options = {}) {
294
298
  textBox,
295
299
  pathOptions,
296
300
  );
301
+ const linkAnnotations = [];
297
302
 
298
303
  if (!textBox.width) {
299
304
  textBox.width = toWriteTextObjects[0].lineWidth;
@@ -316,7 +321,7 @@ exports.text = function text(text = "", x, y, options = {}) {
316
321
  toWriteTextObjects = clippedText.textObjects;
317
322
  textBox.textHeight = getTextBoxHeight(toWriteTextObjects);
318
323
 
319
- if (clippedText.remainder.length) {
324
+ if (clippedText.clipped) {
320
325
  clipResult = {
321
326
  remainder: clippedText.remainder,
322
327
  linesWritten: clippedText.linesWritten,
@@ -641,6 +646,15 @@ exports.text = function text(text = "", x, y, options = {}) {
641
646
  const x = next_x || getStartX(content.startX, content);
642
647
  const y = currentY;
643
648
  next_x = writeText(context, x, y, content);
649
+ if (content.writeOptions.link) {
650
+ linkAnnotations.push({
651
+ url: content.writeOptions.link,
652
+ left: x,
653
+ bottom: y,
654
+ width: next_x ? next_x - x : content.lineWidth,
655
+ height: content.lineHeight,
656
+ });
657
+ }
644
658
  });
645
659
  // The line offset from the last line in the
646
660
  // group determines Y positioning for next line.
@@ -739,6 +753,15 @@ exports.text = function text(text = "", x, y, options = {}) {
739
753
  const x = next_x || getStartX(content.startX, content);
740
754
  const y = currentY;
741
755
  next_x = writeText(context, x, y, content);
756
+ if (content.writeOptions.link) {
757
+ linkAnnotations.push({
758
+ url: content.writeOptions.link,
759
+ left: x,
760
+ bottom: y,
761
+ width: next_x ? next_x - x : content.lineWidth,
762
+ height: content.lineHeight,
763
+ });
764
+ }
742
765
  }
743
766
 
744
767
  // Flush any left over text objects.
@@ -754,6 +777,16 @@ exports.text = function text(text = "", x, y, options = {}) {
754
777
  }
755
778
  }
756
779
 
780
+ linkAnnotations.forEach((annotation) => {
781
+ linkPdf(
782
+ this,
783
+ annotation.url,
784
+ annotation.left,
785
+ annotation.bottom,
786
+ annotation.width,
787
+ annotation.height,
788
+ );
789
+ });
757
790
  return this;
758
791
  };
759
792
 
@@ -798,6 +831,13 @@ exports._layoutText = function _layoutText(textObjects, textBox, pathOptions) {
798
831
 
799
832
  let firstLineHeight;
800
833
  let toWriteTextObjects = [];
834
+ /** Reports whether a layout node contains content other than the break sentinel. */
835
+ const hasRenderableContent = (textObject) =>
836
+ (textObject.value !== undefined &&
837
+ textObject.value !== null &&
838
+ textObject.value !== "" &&
839
+ textObject.value !== "[@@DONOT_RENDER_THIS@@]") ||
840
+ textObject.childs?.some(hasRenderableContent);
801
841
 
802
842
  const writeValue = (textObject) => {
803
843
  textObject.lineID = textObject.lineID || Date.now() * Math.random();
@@ -832,6 +872,7 @@ exports._layoutText = function _layoutText(textObjects, textBox, pathOptions) {
832
872
  textObject.layer++;
833
873
 
834
874
  textObject.currentIndex = 0;
875
+ let prependValue = textObject.prependValue;
835
876
 
836
877
  textObject.childs.forEach((child) => {
837
878
  if (textObject.tag == "ul") {
@@ -852,11 +893,17 @@ exports._layoutText = function _layoutText(textObjects, textBox, pathOptions) {
852
893
  child.layer = textObject.layer - 1;
853
894
  }
854
895
  }
855
- if (textObject.prependValue) {
856
- child.prependValue = !["ol", "ul"].includes(textObject.tag)
857
- ? textObject.prependValue
858
- : child.prependValue;
859
- textObject.indent = 2 * textObject.layer;
896
+ if (
897
+ prependValue &&
898
+ !["ol", "ul"].includes(child.tag) &&
899
+ hasRenderableContent(child)
900
+ ) {
901
+ child.prependValue = prependValue;
902
+ prependValue = null;
903
+ textObject.indent =
904
+ textObject.tag == "li"
905
+ ? 2 * textObject.layer
906
+ : textObject.indent || 2 * textObject.layer;
860
907
  }
861
908
  if (textObject.indent) {
862
909
  child.indent = child.indent || textObject.indent;
@@ -866,6 +913,7 @@ exports._layoutText = function _layoutText(textObjects, textBox, pathOptions) {
866
913
  child.sizeRatios = [...textObject.sizeRatios, child.sizeRatio];
867
914
  }
868
915
  child.styles = Object.assign(child.styles, textObject.styles);
916
+ child.link = child.link || textObject.link;
869
917
 
870
918
  child.isBold = textObject.isBold ? textObject.isBold : child.isBold;
871
919
  child.isItalic = textObject.isItalic
@@ -887,7 +935,55 @@ exports._layoutText = function _layoutText(textObjects, textBox, pathOptions) {
887
935
  writeValue(textObject);
888
936
  });
889
937
 
890
- return { toWriteTextObjects: toWriteTextObjects, textHeight: totalHeight };
938
+ const normalizedTextObjects = [];
939
+ let pendingBreaks = [];
940
+ const replacementLineIDs = new Map();
941
+ /** Converts pending break sentinels into line state without rendering their text. */
942
+ const appendPendingBreaks = (nextTextObject) => {
943
+ const previous = normalizedTextObjects[normalizedTextObjects.length - 1];
944
+ if (previous) previous.lineComplete = true;
945
+ const blankBreaks =
946
+ previous && nextTextObject ? pendingBreaks.slice(1) : pendingBreaks;
947
+ blankBreaks.forEach((breakObject, index) => {
948
+ normalizedTextObjects.push({
949
+ ...breakObject,
950
+ lineID: previous ? pendingBreaks[index].lineID : breakObject.lineID,
951
+ text: "",
952
+ lineComplete: true,
953
+ lineWidth: 0,
954
+ textWidth: 0,
955
+ });
956
+ });
957
+ if (nextTextObject && previous) {
958
+ replacementLineIDs.set(
959
+ nextTextObject.lineID,
960
+ pendingBreaks[pendingBreaks.length - 1].lineID,
961
+ );
962
+ }
963
+ pendingBreaks = [];
964
+ };
965
+ toWriteTextObjects.forEach((textObject, index, objects) => {
966
+ const sentinel = textObject.text.trim() == "[@@DONOT_RENDER_THIS@@]";
967
+ const beforeSentinel =
968
+ textObject.text.trim() == "" &&
969
+ objects[index + 1]?.text.trim() == "[@@DONOT_RENDER_THIS@@]";
970
+ if (beforeSentinel) return;
971
+ if (sentinel) {
972
+ pendingBreaks.push(textObject);
973
+ return;
974
+ }
975
+ if (pendingBreaks.length) appendPendingBreaks(textObject);
976
+ textObject.lineID =
977
+ replacementLineIDs.get(textObject.lineID) || textObject.lineID;
978
+ normalizedTextObjects.push(textObject);
979
+ });
980
+ if (pendingBreaks.length) appendPendingBreaks();
981
+ toWriteTextObjects = normalizedTextObjects;
982
+
983
+ return {
984
+ toWriteTextObjects: toWriteTextObjects,
985
+ textHeight: getTextBoxHeight(toWriteTextObjects) || totalHeight,
986
+ };
891
987
  };
892
988
 
893
989
  function getTextBoxHeight(textObjs) {
@@ -949,6 +1045,7 @@ function clipTextToBox(textObjs, availableHeight) {
949
1045
  return {
950
1046
  textObjects: visibleLines.flat(),
951
1047
  linesWritten,
1048
+ clipped: remainderLines.length > 0,
952
1049
  // Preserve line boundaries so a remainder can be written into another box.
953
1050
  remainder: remainderLines
954
1051
  .map((currentLine) => currentLine.map((textObj) => textObj.text).join(""))
@@ -1195,6 +1292,7 @@ function makeTextObjects(self, textObject = {}, pathOptions, textBox = {}) {
1195
1292
  ? textBox.textAlign.split(" ")
1196
1293
  : [];
1197
1294
  const writeOptions = Object.assign({}, pathOptions, {
1295
+ link: textObject.link || pathOptions.link,
1198
1296
  color: textObject.styles.color,
1199
1297
  opacity: parseFloat(textObject.styles.opacity || pathOptions.opacity || 1),
1200
1298
  underline: textObject.underline || pathOptions.underline,
@@ -1216,7 +1314,7 @@ function makeTextObjects(self, textObject = {}, pathOptions, textBox = {}) {
1216
1314
  const indent = textObject.indent || 0;
1217
1315
 
1218
1316
  const lineMaxWidth = textBox.width
1219
- ? textBox.width - textBox.paddingLeft - textBox.paddingRight - indent
1317
+ ? textBox.width - textBox.paddingLeft - textBox.paddingRight
1220
1318
  : null;
1221
1319
  let remainderWidth = lineMaxWidth;
1222
1320
  let newLine;
@@ -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;
@@ -122,6 +122,7 @@ exports.polygon = function polygon(coordinates = [], options = {}) {
122
122
  height,
123
123
  pathOptions,
124
124
  (ctx, xObject) => {
125
+ ctx.gs(xObject.getGsName(pathOptions.strokeGsId));
125
126
  ctx.w(pathOptions.width);
126
127
  setPathOptions(ctx);
127
128
  xObject.stroke(colorModel);
@@ -131,5 +132,15 @@ exports.polygon = function polygon(coordinates = [], options = {}) {
131
132
  );
132
133
  }
133
134
 
135
+ if (options.link) {
136
+ this.link(
137
+ options.link,
138
+ boundBox[0] - margin,
139
+ boundBox[1] - margin,
140
+ width,
141
+ height,
142
+ );
143
+ }
144
+
134
145
  return this;
135
146
  };
@@ -30,9 +30,21 @@ exports._getPathOptions = function _getPathOptions(
30
30
  width: 2,
31
31
  align: options.align,
32
32
  };
33
+ const lineStyle = this.current.lineStyle || {};
34
+
35
+ if (lineStyle.width !== undefined) pathOptions.width = lineStyle.width;
36
+ if (lineStyle.cap !== undefined) pathOptions.lineCap = lineStyle.cap;
37
+ if (lineStyle.join !== undefined) pathOptions.lineJoin = lineStyle.join;
38
+ if (lineStyle.miterLimit !== undefined)
39
+ pathOptions.miterLimit = lineStyle.miterLimit;
40
+ if (lineStyle.dash !== undefined) pathOptions.dash = lineStyle.dash;
41
+ if (lineStyle.dashPhase !== undefined)
42
+ pathOptions.dashPhase = lineStyle.dashPhase;
43
+ if (pathOptions.dash === undefined) pathOptions.dash = [];
44
+ if (pathOptions.dashPhase === undefined) pathOptions.dashPhase = 0;
33
45
 
34
46
  if (options.opacity == void 0 || isNaN(options.opacity)) {
35
- options.opacity = 1;
47
+ options.opacity = this.current.opacity ?? 1;
36
48
  } else {
37
49
  options.opacity =
38
50
  options.opacity < 0 ? 0 : options.opacity > 1 ? 1 : options.opacity;
@@ -95,8 +107,8 @@ exports._getPathOptions = function _getPathOptions(
95
107
  }
96
108
 
97
109
  // Page 127 of PDF 1.7 specification
98
- pathOptions.dash = Array.isArray(options.dash) ? options.dash : [];
99
- pathOptions.dashPhase = !isNaN(options.dashPhase) ? options.dashPhase : 0;
110
+ if (Array.isArray(options.dash)) pathOptions.dash = options.dash;
111
+ if (!isNaN(options.dashPhase)) pathOptions.dashPhase = options.dashPhase;
100
112
  if (pathOptions.dash[0] == 0 && pathOptions.dash[1] == 0) {
101
113
  pathOptions.dash = []; // no dash, solid unbroken line
102
114
  pathOptions.dashPhase = 0;