@muhammara/native-core 7.0.0-beta.3 → 7.0.0-beta.4

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/lib/Recipe.js CHANGED
@@ -263,8 +263,9 @@ class Recipe {
263
263
  /**
264
264
  * End the pdfDoc. Finalization happens once; later calls do not rewrite the
265
265
  * PDF and invoke the callback with the completed output when applicable.
266
- * A failed finalization retires the Recipe and later calls rethrow the
267
- * original error.
266
+ * An active page is finished first, so a forgotten endPage() does not cost
267
+ * that page. A failed finalization retires the Recipe and later calls
268
+ * rethrow the original error.
268
269
  * @function
269
270
  * @memberof Recipe
270
271
  * @param {function} [callback] - The callback function.
@@ -292,6 +293,10 @@ class Recipe {
292
293
  if (deletingPages && this.page) {
293
294
  throw new Error("Finish the current page before endPDF");
294
295
  }
296
+ // The writer cannot finalize around an open content stream: it drops the
297
+ // page and emits a catalog with no page tree. Finish the page for the
298
+ // caller instead, before finalization retires the Recipe.
299
+ if (this.page) this.endPage();
295
300
  try {
296
301
  this._deletePages();
297
302
  this._writeInfo();
@@ -1,3 +1,17 @@
1
+ /**
2
+ * Encodes annotation text as a PDF text string, so characters outside
3
+ * PDFDocEncoding are written as UTF-16BE instead of raw UTF-8 bytes.
4
+ * @private
5
+ * @param {Object} writer - The PDF writer.
6
+ * @param {string} [value] - The text to encode.
7
+ * @returns {number[]} The encoded string bytes.
8
+ */
9
+ function textString(writer, value) {
10
+ var text = writer.createPDFTextString();
11
+ text.fromString(String(value ?? ""));
12
+ return text.toBytesArray();
13
+ }
14
+
1
15
  /**
2
16
  * Create a comment annotation
3
17
  * @name comment
@@ -171,9 +185,9 @@ exports._annot = function _annot(subtype, args = {}, pageNumber, ref) {
171
185
  .writeKey("Rect")
172
186
  .writeRectangleValue(position)
173
187
  .writeKey("Subj")
174
- .writeLiteralStringValue(params.subject)
188
+ .writeLiteralStringValue(textString(this.writer, params.subject))
175
189
  .writeKey("T")
176
- .writeLiteralStringValue(params.title || "")
190
+ .writeLiteralStringValue(textString(this.writer, params.title))
177
191
  .writeKey("M")
178
192
  .writeLiteralStringValue(
179
193
  this.writer.createPDFDate(new Date(params.date)).toString(),
@@ -194,16 +208,16 @@ exports._annot = function _annot(subtype, args = {}, pageNumber, ref) {
194
208
  */
195
209
  if (text && params.richText) {
196
210
  const richText =
197
- text.substring(0, 5) !== "<?xml" ? contentToRC(text) : params.richText;
211
+ text.substring(0, 5) !== "<?xml" ? contentToRC(text) : text;
198
212
  const richTextContent = richText;
199
213
  this.dictionaryContext
200
214
  .writeKey("RC")
201
- .writeLiteralStringValue(richTextContent);
215
+ .writeLiteralStringValue(textString(this.writer, richTextContent));
202
216
  } else if (text) {
203
217
  const textContent = text;
204
218
  this.dictionaryContext
205
219
  .writeKey("Contents")
206
- .writeLiteralStringValue(textContent);
220
+ .writeLiteralStringValue(textString(this.writer, textContent));
207
221
  }
208
222
 
209
223
  if (reply && ref) {
@@ -2,7 +2,8 @@ const muhammara = require("../muhammara");
2
2
  const utils = require("./utils");
3
3
 
4
4
  /**
5
- * Append pages from the other pdf to the current pdf
5
+ * Append pages from the other pdf to the current pdf. An active page is
6
+ * finished first, so appended pages follow it in the output.
6
7
  * @name appendPage
7
8
  * @function
8
9
  * @memberof Recipe#
@@ -55,6 +56,10 @@ exports.appendPage = function appendPage(pdfSrc, pages = []) {
55
56
  }
56
57
  return range.map(transformPageNumber);
57
58
  });
59
+ // Appending writes whole pages, which the writer cannot do around an open
60
+ // content stream. Close the active page only once the selection is known to
61
+ // be valid, so a rejected selection leaves the page exactly as it was.
62
+ if (this.page) this.endPage();
58
63
  if (pages.length > 0) {
59
64
  utils.appendPDFPagesFromPDFWithAnnotations(this.writer, pdfSrc, {
60
65
  specificRanges: pages,
@@ -10,7 +10,6 @@ const DOMParser = require("@xmldom/xmldom").DOMParser;
10
10
  * @returns {Object[]} The parsed text layout objects.
11
11
  */
12
12
  exports.htmlToTextObjects = function (htmlCodes, options = {}) {
13
- htmlCodes = htmlCodes.replace(/<br\/?>/g, "<p>[@@DONOT_RENDER_THIS@@]</p>");
14
13
  const nodes = new DOMParser().parseFromString(
15
14
  `<html>${htmlCodes}</html>`,
16
15
  "text/html",
@@ -39,6 +38,33 @@ function needsLineBreaker(tagName = "") {
39
38
  return lineBreakers.includes(tagName);
40
39
  }
41
40
 
41
+ /**
42
+ * Reports whether text in `node` begins a visual line: after a line break or
43
+ * block element, or at the start of a block. Only such text drops its
44
+ * leading whitespace; text that follows inline content keeps one space.
45
+ * @private
46
+ */
47
+ function startsLine(node) {
48
+ const previous = node.previousSibling;
49
+ if (previous) {
50
+ const tag = (previous.tagName || "").toLowerCase();
51
+ return tag === "br" || needsLineBreaker(tag) || ["ul", "ol"].includes(tag);
52
+ }
53
+ const parent = node.parentNode;
54
+ const parentTag = (
55
+ parent && parent.tagName ? parent.tagName : ""
56
+ ).toLowerCase();
57
+ if (
58
+ !parent ||
59
+ !parentTag ||
60
+ parentTag === "html" ||
61
+ needsLineBreaker(parentTag)
62
+ ) {
63
+ return true;
64
+ }
65
+ return startsLine(parent);
66
+ }
67
+
42
68
  function isBoldTag(tagName = "") {
43
69
  const boldTags = ["b", "strong"];
44
70
  return boldTags.includes(tagName);
@@ -84,6 +110,14 @@ function parseNode(node, options) {
84
110
  }
85
111
  }
86
112
  let value = node.data ? node.data.replace(/^\s*/gm, "") : null;
113
+ if (value !== null && /^\s/.test(node.data) && !startsLine(node)) {
114
+ value = " " + value;
115
+ }
116
+ // Whitespace before a line break would only pad the end of the line.
117
+ const next = node.nextSibling;
118
+ if (value !== null && next && /^br$/i.test(next.tagName || "")) {
119
+ value = value.replace(/\s+$/, "");
120
+ }
87
121
  if (value && value.charCodeAt(0) == 8203) {
88
122
  // zero width space
89
123
  value = value.substring(1);
@@ -99,6 +133,8 @@ function parseNode(node, options) {
99
133
  attributes,
100
134
  styles,
101
135
  needsLineBreaker: needsLineBreaker(node.tagName),
136
+ // An explicit line break; text layout ends the current line here.
137
+ lineBreak: /^br$/i.test(node.tagName || ""),
102
138
  size: options.size,
103
139
  sizeRatio: getFontSizeRatio(node.tagName),
104
140
  sizeRatios: [getFontSizeRatio(node.tagName)],
@@ -1,4 +1,5 @@
1
1
  const muhammara = require("../muhammara");
2
+ const { PAGE_CONTEXT_STATE } = require("./utils");
2
3
 
3
4
  /** Builds the retained page tree while marking deleted leaf pages. @private */
4
5
  function readPageTree(
@@ -615,7 +616,8 @@ exports.createPage = function createPage(pageWidth, pageHeight, margins) {
615
616
  }
616
617
  }
617
618
  // from 0
618
- this.metadata.pageCount += 1;
619
+ this.metadata.pageCount =
620
+ (this.metadata.pageCount ?? this.metadata.pages ?? 0) + 1;
619
621
  const pageNumber = this.metadata.pageCount;
620
622
  const dimensions = [0, 0, pageWidth, pageHeight];
621
623
  const layout = pageWidth > pageHeight ? "landscape" : "portrait";
@@ -635,7 +637,7 @@ exports.createPage = function createPage(pageWidth, pageHeight, margins) {
635
637
  this.pageNumber = pageNumber;
636
638
  this.pageContext = this.writer.startPageContentContext(this.page);
637
639
  this.editingPage = false;
638
- this.contextState = "active-new";
640
+ this.contextState = PAGE_CONTEXT_STATE.ACTIVE_NEW;
639
641
  if (!this.isNewPDF) this.pagesCreated = true;
640
642
 
641
643
  if (margins) {
@@ -714,7 +716,8 @@ exports.endPage = function endPage() {
714
716
  }
715
717
 
716
718
  if (this.page.endContext) {
717
- if (this.contextState === "active-edit") this.page.endContext();
719
+ if (this.contextState === PAGE_CONTEXT_STATE.ACTIVE_EDIT)
720
+ this.page.endContext();
718
721
  this.page.writePage();
719
722
  } else {
720
723
  this.writer.writePage(this.page);
@@ -723,7 +726,7 @@ exports.endPage = function endPage() {
723
726
  this.pageContext = null;
724
727
  this.pageNumber = 0;
725
728
  this.editingPage = false;
726
- this.contextState = "idle";
729
+ this.contextState = PAGE_CONTEXT_STATE.IDLE;
727
730
 
728
731
  return this;
729
732
  };
@@ -748,7 +751,7 @@ exports.editPage = function editPage(pageNumber) {
748
751
  this.pageNumber = pageNumber;
749
752
  this.pageContext = pageModifier.startContext().getContext();
750
753
  this.editingPage = true;
751
- this.contextState = "active-edit";
754
+ this.contextState = PAGE_CONTEXT_STATE.ACTIVE_EDIT;
752
755
  this.modifiedSourcePages.add(pageNumber);
753
756
 
754
757
  this._resumePageRotation(pageNumber);
@@ -1016,13 +1019,13 @@ exports.getCurrentPageInfo = function getCurrentPageInfo() {
1016
1019
  * @throws {Error} If there is no active page content context.
1017
1020
  */
1018
1021
  exports.pauseContext = function pauseContext() {
1019
- if (this.contextState === "active-edit") {
1022
+ if (this.contextState === PAGE_CONTEXT_STATE.ACTIVE_EDIT) {
1020
1023
  this.page.endContext();
1021
1024
  this.pageContext = null;
1022
- this.contextState = "paused-edit";
1023
- } else if (this.contextState === "active-new") {
1025
+ this.contextState = PAGE_CONTEXT_STATE.PAUSED_EDIT;
1026
+ } else if (this.contextState === PAGE_CONTEXT_STATE.ACTIVE_NEW) {
1024
1027
  this.writer.pausePageContentContext(this.pageContext);
1025
- this.contextState = "paused-new";
1028
+ this.contextState = PAGE_CONTEXT_STATE.PAUSED_NEW;
1026
1029
  } else {
1027
1030
  throw new Error("No active page content context to pause");
1028
1031
  }
@@ -1038,12 +1041,12 @@ exports.pauseContext = function pauseContext() {
1038
1041
  * @throws {Error} If there is no paused page content context.
1039
1042
  */
1040
1043
  exports.resumeContext = function resumeContext() {
1041
- if (this.contextState === "paused-edit") {
1044
+ if (this.contextState === PAGE_CONTEXT_STATE.PAUSED_EDIT) {
1042
1045
  this.pageContext = this.page.startContext().getContext();
1043
1046
  this._resumePageRotation();
1044
- this.contextState = "active-edit";
1045
- } else if (this.contextState === "paused-new") {
1046
- this.contextState = "active-new";
1047
+ this.contextState = PAGE_CONTEXT_STATE.ACTIVE_EDIT;
1048
+ } else if (this.contextState === PAGE_CONTEXT_STATE.PAUSED_NEW) {
1049
+ this.contextState = PAGE_CONTEXT_STATE.ACTIVE_NEW;
1047
1050
  } else {
1048
1051
  throw new Error("No paused page content context to resume");
1049
1052
  }
@@ -1,3 +1,5 @@
1
+ const { PAGE_CONTEXT_STATE } = require("./utils");
2
+
1
3
  this.mediumSizes = {
2
4
  executive: [521.86, 756.0], // north american
3
5
  folio: [612.0, 936.0],
@@ -81,7 +83,7 @@ exports._setParameters = function _setParameters() {
81
83
  };
82
84
 
83
85
  this._margin = Object.assign({}, this.default.pageMargin);
84
- this.contextState = "idle";
86
+ this.contextState = PAGE_CONTEXT_STATE.IDLE;
85
87
  this.modifiedSourcePages = new Set();
86
88
 
87
89
  // Object.assign(this, options);