@muhammara/native-core 7.0.0-beta.2 → 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 +7 -2
- package/lib/recipe/annotation.js +19 -5
- package/lib/recipe/appendPage.js +6 -1
- package/lib/recipe/htmlToTextObjects.js +37 -1
- package/lib/recipe/page.js +16 -13
- package/lib/recipe/parameters.js +3 -1
- package/lib/recipe/table.js +223 -169
- package/lib/recipe/text.helper.js +4 -2
- package/lib/recipe/text.js +130 -63
- package/lib/recipe/utils.js +52 -0
- package/lib/recipe/vector.helper.js +2 -7
- package/lib/recipe/vector.js +0 -2
- package/muhammara.d.ts +29 -8
- package/package.json +1 -1
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
|
-
*
|
|
267
|
-
*
|
|
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();
|
package/lib/recipe/annotation.js
CHANGED
|
@@ -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) :
|
|
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) {
|
package/lib/recipe/appendPage.js
CHANGED
|
@@ -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)],
|
package/lib/recipe/page.js
CHANGED
|
@@ -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
|
|
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 =
|
|
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 ===
|
|
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 =
|
|
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 =
|
|
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 ===
|
|
1022
|
+
if (this.contextState === PAGE_CONTEXT_STATE.ACTIVE_EDIT) {
|
|
1020
1023
|
this.page.endContext();
|
|
1021
1024
|
this.pageContext = null;
|
|
1022
|
-
this.contextState =
|
|
1023
|
-
} else if (this.contextState ===
|
|
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 =
|
|
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 ===
|
|
1044
|
+
if (this.contextState === PAGE_CONTEXT_STATE.PAUSED_EDIT) {
|
|
1042
1045
|
this.pageContext = this.page.startContext().getContext();
|
|
1043
1046
|
this._resumePageRotation();
|
|
1044
|
-
this.contextState =
|
|
1045
|
-
} else if (this.contextState ===
|
|
1046
|
-
this.contextState =
|
|
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
|
}
|
package/lib/recipe/parameters.js
CHANGED
|
@@ -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 =
|
|
86
|
+
this.contextState = PAGE_CONTEXT_STATE.IDLE;
|
|
85
87
|
this.modifiedSourcePages = new Set();
|
|
86
88
|
|
|
87
89
|
// Object.assign(this, options);
|