@muhammara/native-core 7.0.0-beta.4 → 7.0.0-rc.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.
- package/README.md +57 -7
- package/THIRD_PARTY_NOTICES.md +39 -0
- package/index.js +120 -1
- package/lib/PDFRStreamForBuffer.js +52 -7
- package/lib/PDFRStreamForFile.js +56 -7
- package/lib/PDFStreamForResponse.js +22 -5
- package/lib/PDFWStreamForBuffer.js +49 -10
- package/lib/PDFWStreamForFile.js +26 -5
- package/lib/Recipe.js +91 -38
- package/lib/content-stream.js +503 -0
- package/lib/css-colors.js +149 -0
- package/lib/font-text.js +1461 -0
- package/lib/glyph-list.js +1127 -0
- package/lib/recipe/annotation.js +464 -101
- package/lib/recipe/appendPage.js +2 -0
- package/lib/recipe/colors.js +132 -35
- package/lib/recipe/coordinate.js +82 -16
- package/lib/recipe/encrypt.js +32 -13
- package/lib/recipe/font.js +70 -25
- package/lib/recipe/graphics-state.js +3 -2
- package/lib/recipe/htmlToTextObjects.js +121 -24
- package/lib/recipe/image.js +385 -39
- package/lib/recipe/info.js +93 -22
- package/lib/recipe/insertPage.js +12 -3
- package/lib/recipe/overlay.js +4 -2
- package/lib/recipe/page-references.js +436 -0
- package/lib/recipe/page.js +363 -262
- package/lib/recipe/parameters.js +8 -1
- package/lib/recipe/replaceText.js +464 -30
- package/lib/recipe/shapes.js +418 -45
- package/lib/recipe/split.js +6 -2
- package/lib/recipe/table.js +41 -8
- package/lib/recipe/text.helper.js +172 -8
- package/lib/recipe/text.js +294 -56
- package/lib/recipe/utils.js +17 -6
- package/lib/recipe/vector-line.js +101 -39
- package/lib/recipe/vector-polygon.js +18 -6
- package/lib/recipe/vector.helper.js +156 -14
- package/lib/recipe/vector.js +156 -52
- package/lib/recipe/xObjectForm.js +73 -10
- package/lib/recipe-constants.js +373 -0
- package/lib/recipe-options.js +47 -0
- package/muhammara.d.ts +3501 -160
- package/package.json +2 -3
package/lib/recipe/annotation.js
CHANGED
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
const {
|
|
2
|
+
AnnotSubtype,
|
|
3
|
+
AnnotIcon,
|
|
4
|
+
AnnotFlag,
|
|
5
|
+
Colorspace,
|
|
6
|
+
Coordinate,
|
|
7
|
+
} = require("../recipe-constants");
|
|
8
|
+
const { cssColors } = require("../css-colors");
|
|
9
|
+
|
|
1
10
|
/**
|
|
2
11
|
* Encodes annotation text as a PDF text string, so characters outside
|
|
3
12
|
* PDFDocEncoding are written as UTF-16BE instead of raw UTF-8 bytes.
|
|
@@ -13,27 +22,64 @@ function textString(writer, value) {
|
|
|
13
22
|
}
|
|
14
23
|
|
|
15
24
|
/**
|
|
16
|
-
*
|
|
25
|
+
* Formats an annotation's `/M` modification date, as Wasm does: no date
|
|
26
|
+
* writes no `/M`, and text that is not a date is written as given, which the
|
|
27
|
+
* PDF specification allows for `/M`. A date is written in local time with its
|
|
28
|
+
* UTC offset.
|
|
29
|
+
* @private
|
|
30
|
+
* @param {Object} writer - The PDF writer.
|
|
31
|
+
* @param {string|number|Date} [value] - The date.
|
|
32
|
+
* @returns {string|undefined} The `/M` text, or undefined for no `/M`.
|
|
33
|
+
*/
|
|
34
|
+
function annotationDate(writer, value) {
|
|
35
|
+
if (value === undefined || value === null || value === "") return undefined;
|
|
36
|
+
var date = new Date(value);
|
|
37
|
+
if (Number.isNaN(date.valueOf())) return String(value);
|
|
38
|
+
return writer.createPDFDate(date).toString();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Create a comment annotation: a Text annotation with the Comment icon. It is
|
|
43
|
+
* written when the PDF ends.
|
|
17
44
|
* @name comment
|
|
18
45
|
* @function
|
|
19
46
|
* @memberof Recipe#
|
|
20
47
|
* @param {string} [text=''] - The text content
|
|
21
|
-
* @param {number} x - The coordinate x
|
|
22
|
-
* @param {number} y - The coordinate y
|
|
48
|
+
* @param {number|"center"} x - The coordinate x
|
|
49
|
+
* @param {number|"center"} y - The top coordinate y; (x, y) is the top-left
|
|
50
|
+
* corner, as for `annot()`.
|
|
23
51
|
* @param {Object} [options] - The options
|
|
24
52
|
* @param {string} [options.title] - The title.
|
|
25
53
|
* @param {string} [options.date] - The date.
|
|
26
54
|
* @param {boolean} [options.open=false] - Open the annotation by default?
|
|
27
55
|
* @param {boolean} [options.richText] - Display with rich text format, text will be transformed automatically, or you may pass in your own rich text starts with "<?xml..."
|
|
28
56
|
* @param {Array} [options.replies] - Array of annotation replies, each with text and optional title, date, subject, richText, and flag.
|
|
29
|
-
* @param {
|
|
57
|
+
* @param {Recipe.AnnotFlag} [options.flag] - The flag property, one of the `Recipe.AnnotFlag` values.
|
|
58
|
+
* @param {string|number[]} [options.color] - The annotation color, as for
|
|
59
|
+
* `annot()`.
|
|
60
|
+
* @param {number} [options.width] - The rectangle width, as for `annot()`.
|
|
61
|
+
* @param {number} [options.height] - The rectangle height; as for `annot()`,
|
|
62
|
+
* (x, y) is the top-left corner and the rectangle extends down from it.
|
|
63
|
+
* The other `annot()` options, such as `border`, `opacity`, and
|
|
64
|
+
* `quadPoints`, apply too; `contents` is used when `text` is empty.
|
|
30
65
|
* @returns {Recipe} The recipe instance.
|
|
66
|
+
* @throws {TypeError} If `options.color` is not a known color, or a
|
|
67
|
+
* position, size, opacity, border, or `quadPoints` value is invalid.
|
|
31
68
|
*/
|
|
32
69
|
exports.comment = function comment(text = "", x, y, options = {}) {
|
|
70
|
+
this._validateAnnot(options);
|
|
71
|
+
validateAnnotationPosition(x, y, options);
|
|
33
72
|
this.annotationsToWrite.push({
|
|
34
|
-
subtype:
|
|
73
|
+
subtype: AnnotSubtype.TEXT,
|
|
35
74
|
pageNumber: this.pageNumber,
|
|
36
|
-
args: {
|
|
75
|
+
args: {
|
|
76
|
+
text: text || options.contents,
|
|
77
|
+
x,
|
|
78
|
+
y,
|
|
79
|
+
width: options.width,
|
|
80
|
+
height: options.height,
|
|
81
|
+
options: Object.assign({ icon: AnnotIcon.COMMENT }, options),
|
|
82
|
+
},
|
|
37
83
|
replies: options.replies,
|
|
38
84
|
});
|
|
39
85
|
return this;
|
|
@@ -45,20 +91,44 @@ exports.comment = function comment(text = "", x, y, options = {}) {
|
|
|
45
91
|
* @function
|
|
46
92
|
* @memberof Recipe#
|
|
47
93
|
* @param {string} url - The URL to open.
|
|
48
|
-
* @param {number} x - The top-left x coordinate.
|
|
49
|
-
* @param {number} y - The top-left y coordinate.
|
|
94
|
+
* @param {number|"center"} x - The top-left x coordinate.
|
|
95
|
+
* @param {number|"center"} y - The top-left y coordinate.
|
|
50
96
|
* @param {number} width - The link width.
|
|
51
97
|
* @param {number} height - The link height.
|
|
52
98
|
* @returns {Recipe} The recipe instance.
|
|
99
|
+
* @throws {TypeError} If no page is active, `url` is not a string, or the
|
|
100
|
+
* rectangle is not finite.
|
|
53
101
|
*/
|
|
54
102
|
exports.link = function link(url, x, y, width, height) {
|
|
55
103
|
const { nx, ny } = this._calibrateCoordinate(x, y, 0, -height);
|
|
56
104
|
return linkPdf(this, url, nx, ny, width, height);
|
|
57
105
|
};
|
|
58
106
|
|
|
107
|
+
/**
|
|
108
|
+
* Attach a URL link at PDF coordinates, pausing the page content context
|
|
109
|
+
* while the writer adds the annotation.
|
|
110
|
+
* @private
|
|
111
|
+
* @param {Recipe} recipe - The recipe with an active page.
|
|
112
|
+
* @param {string} url - The URL to open.
|
|
113
|
+
* @param {number} left - The left edge in PDF points.
|
|
114
|
+
* @param {number} bottom - The bottom edge in PDF points.
|
|
115
|
+
* @param {number} width - The link width.
|
|
116
|
+
* @param {number} height - The link height.
|
|
117
|
+
* @returns {Recipe} The recipe instance.
|
|
118
|
+
* @throws {Error} If no page is active or the link cannot be attached.
|
|
119
|
+
* @throws {TypeError} If `url` is not a string or the rectangle is not finite.
|
|
120
|
+
*/
|
|
59
121
|
function linkPdf(recipe, url, left, bottom, width, height) {
|
|
60
122
|
recipe.pauseContext();
|
|
61
123
|
try {
|
|
124
|
+
// Reject what cannot form a PDF rectangle before writing, as Wasm does.
|
|
125
|
+
if (
|
|
126
|
+
typeof url !== "string" ||
|
|
127
|
+
![left, bottom, width, height, left + width, bottom + height].every(
|
|
128
|
+
Number.isFinite,
|
|
129
|
+
)
|
|
130
|
+
)
|
|
131
|
+
throw new TypeError("URL link requires a URL and valid PDF rectangle");
|
|
62
132
|
recipe.writer.attachURLLinktoCurrentPage(
|
|
63
133
|
url,
|
|
64
134
|
left,
|
|
@@ -75,31 +145,52 @@ function linkPdf(recipe, url, left, bottom, width, height) {
|
|
|
75
145
|
Object.defineProperty(exports, "linkPdf", { value: linkPdf });
|
|
76
146
|
|
|
77
147
|
/**
|
|
78
|
-
* Create an annotation
|
|
148
|
+
* Create an annotation. It is written when the PDF ends.
|
|
79
149
|
* @name annot
|
|
80
150
|
* @function
|
|
81
151
|
* @memberof Recipe#
|
|
82
152
|
* @todo support for rich text RC
|
|
83
|
-
* @param {number} x - The
|
|
84
|
-
* @param {number} y - The
|
|
85
|
-
*
|
|
153
|
+
* @param {number|"center"} x - The left edge of the annotation rectangle.
|
|
154
|
+
* @param {number|"center"} y - The top edge of the annotation rectangle. Like
|
|
155
|
+
* `rectangle()` and `link()`, (x, y) is the rectangle's top-left corner, and
|
|
156
|
+
* the rectangle extends `options.height` down from it.
|
|
157
|
+
* @param {Recipe.AnnotSubtype} subtype - The annotation subtype, one of the
|
|
158
|
+
* `Recipe.AnnotSubtype` values.
|
|
86
159
|
* @param {Object} [options] - The options
|
|
87
160
|
* @param {string} [options.text=''] - The annotation content.
|
|
161
|
+
* @param {string} [options.contents] - The content when `text` is empty.
|
|
88
162
|
* @param {string} [options.title] - The title.
|
|
89
163
|
* @param {boolean} [options.open=false] - Open the annotation. Annotation will be closed by default. Specific to text annotations; subtype='Text'
|
|
90
164
|
* @param {boolean} [options.richText] - Rich text
|
|
91
|
-
* @param {
|
|
92
|
-
* @param {
|
|
93
|
-
*
|
|
94
|
-
* @param {
|
|
165
|
+
* @param {Recipe.AnnotFlag} [options.flag] - The flag property, one of the `Recipe.AnnotFlag` values.
|
|
166
|
+
* @param {Recipe.AnnotIcon} [options.icon] - The icon of a Text annotation, one
|
|
167
|
+
* of the `Recipe.AnnotIcon` values. Viewers show 'Note' when it is omitted.
|
|
168
|
+
* @param {string} [options.name] - The `/Name` icon when `icon` is omitted.
|
|
169
|
+
* @param {number} [options.flags] - Flag bits when `flag` is omitted.
|
|
170
|
+
* @param {number} [options.width] - Width; a finite number of at least zero.
|
|
171
|
+
* @param {number} [options.height] - Height; a finite number of at least zero.
|
|
95
172
|
* @param {string} [options.date] - Date of annotation
|
|
96
173
|
* @param {string} [options.subject] - The subject.
|
|
97
|
-
* @param {Array} [options.replies] - Array of annotation replies
|
|
98
|
-
*
|
|
99
|
-
*
|
|
174
|
+
* @param {Array} [options.replies] - Array of annotation replies. A reply
|
|
175
|
+
* keeps its own `open`, `icon`, `name`, and `opacity`, and otherwise
|
|
176
|
+
* inherits the metadata of the annotation it answers.
|
|
177
|
+
* @param {number|Object} [options.border] - The border width, or an object
|
|
178
|
+
* with its `width` and `dash` pattern. A negative width writes no border;
|
|
179
|
+
* text markup annotations default to 0.
|
|
180
|
+
* @param {number} [options.borderWidth] - The border width; overrides
|
|
181
|
+
* `border.width`.
|
|
182
|
+
* @param {number[]} [options.borderDash] - The border dash pattern; overrides
|
|
183
|
+
* `border.dash`.
|
|
184
|
+
* @param {number[]} [options.quadPoints] - Quad points, eight numbers per
|
|
185
|
+
* quadrilateral, written as given instead of covering the rectangle.
|
|
186
|
+
* @param {string|number[]} [options.color] - The annotation color: a `#rrggbb`
|
|
187
|
+
* HexColor, a `%r,g,b` PercentColor, a DecimalColor array, a color registered
|
|
188
|
+
* with `chroma()`, or a CSS color name in any case.
|
|
100
189
|
* @param {number} [options.opacity=1] - Annotation opacity from 0 (transparent) to 1 (opaque).
|
|
101
190
|
* @param {boolean} [options.followOriginalPageRotation=false] - Preserve the original page rotation when positioning the annotation.
|
|
102
191
|
* @returns {Recipe} The recipe instance.
|
|
192
|
+
* @throws {TypeError} If `options.color` is not a known color, or a
|
|
193
|
+
* position, size, opacity, border, or `quadPoints` value is invalid.
|
|
103
194
|
*/
|
|
104
195
|
exports.annot = function annot(
|
|
105
196
|
x,
|
|
@@ -107,7 +198,10 @@ exports.annot = function annot(
|
|
|
107
198
|
subtype,
|
|
108
199
|
options = { text: "", width: 0, height: 0 },
|
|
109
200
|
) {
|
|
110
|
-
const {
|
|
201
|
+
const { width, height, replies } = options;
|
|
202
|
+
const text = options.text || options.contents;
|
|
203
|
+
this._validateAnnot(options);
|
|
204
|
+
validateAnnotationPosition(x, y, options);
|
|
111
205
|
this.annotationsToWrite.push({
|
|
112
206
|
subtype,
|
|
113
207
|
args: { text, x, y, width, height, options },
|
|
@@ -119,23 +213,59 @@ exports.annot = function annot(
|
|
|
119
213
|
|
|
120
214
|
// TODO: allow non-markup annots to be associated with markup annotations
|
|
121
215
|
// Link, Popup, Movie, Widget, Screen, PrinterMark, TrapNet, Watermark, 3D
|
|
216
|
+
/**
|
|
217
|
+
* Placeholder for associating non-markup annotations with markup ones; it
|
|
218
|
+
* currently does nothing.
|
|
219
|
+
* @private
|
|
220
|
+
* @returns {void}
|
|
221
|
+
*/
|
|
122
222
|
exports._attachNonMarkupAnnot = function _attachNonMarkupAnnot() {};
|
|
123
223
|
|
|
224
|
+
/**
|
|
225
|
+
* Write one queued annotation, or a reply to one, as an indirect object and
|
|
226
|
+
* register it on its page.
|
|
227
|
+
* @private
|
|
228
|
+
* @param {Recipe.AnnotSubtype} subtype - The annotation subtype.
|
|
229
|
+
* @param {Object} [args] - The queued x, y, width, height, text, options, and
|
|
230
|
+
* for a reply the reply entry.
|
|
231
|
+
* @param {number} pageNumber - The one-based page number.
|
|
232
|
+
* @param {number} [ref] - The object ID of the annotation a reply answers.
|
|
233
|
+
* @returns {number} The object ID of the written annotation.
|
|
234
|
+
* @throws {TypeError} If the page number is unknown.
|
|
235
|
+
*/
|
|
124
236
|
exports._annot = function _annot(subtype, args = {}, pageNumber, ref) {
|
|
237
|
+
// Write known subtypes with their PDF casing; the markup check below
|
|
238
|
+
// already matches them case-insensitively.
|
|
239
|
+
subtype =
|
|
240
|
+
Object.values(AnnotSubtype).find(
|
|
241
|
+
(known) => known.toLowerCase() === String(subtype).toLowerCase(),
|
|
242
|
+
) || subtype;
|
|
125
243
|
const { x, y, width, height, options, reply } = args;
|
|
126
244
|
let { text } = args;
|
|
127
245
|
this._startDictionary(pageNumber);
|
|
128
246
|
const { rotate } = this.metadata[pageNumber];
|
|
129
|
-
|
|
247
|
+
// (x, y) is the top-left corner, like rectangle() and link(): offset by the
|
|
248
|
+
// height to reach the bottom-left corner that PDF rectangles start from.
|
|
249
|
+
let { nx, ny } = this._calibrateCoordinateForAnnots(
|
|
250
|
+
x,
|
|
251
|
+
y,
|
|
252
|
+
0,
|
|
253
|
+
-(height || 0),
|
|
254
|
+
pageNumber,
|
|
255
|
+
);
|
|
130
256
|
|
|
131
|
-
|
|
132
|
-
|
|
257
|
+
// A missing size is zero, as in Wasm; undefined would write nan on rotated
|
|
258
|
+
// pages.
|
|
259
|
+
const rectWidth = width || 0;
|
|
260
|
+
const rectHeight = height || 0;
|
|
261
|
+
let nWidth = rectWidth;
|
|
262
|
+
let nHeight = rectHeight;
|
|
133
263
|
|
|
134
264
|
if (!options.followOriginalPageRotation) {
|
|
135
265
|
switch (rotate) {
|
|
136
266
|
case 90:
|
|
137
|
-
nWidth =
|
|
138
|
-
nHeight =
|
|
267
|
+
nWidth = rectHeight;
|
|
268
|
+
nHeight = rectWidth;
|
|
139
269
|
nx = nx - nWidth;
|
|
140
270
|
break;
|
|
141
271
|
case 180:
|
|
@@ -143,8 +273,8 @@ exports._annot = function _annot(subtype, args = {}, pageNumber, ref) {
|
|
|
143
273
|
ny = ny - nHeight;
|
|
144
274
|
break;
|
|
145
275
|
case 270:
|
|
146
|
-
nWidth =
|
|
147
|
-
nHeight =
|
|
276
|
+
nWidth = rectHeight;
|
|
277
|
+
nHeight = rectWidth;
|
|
148
278
|
ny = ny - nHeight;
|
|
149
279
|
break;
|
|
150
280
|
default:
|
|
@@ -166,13 +296,18 @@ exports._annot = function _annot(subtype, args = {}, pageNumber, ref) {
|
|
|
166
296
|
const ey = nHeight ? nHeight : 0;
|
|
167
297
|
const position = [nx, ny, nx + ex, ny + ey];
|
|
168
298
|
|
|
299
|
+
params.flag = options.flag ?? options.flags ?? "";
|
|
169
300
|
if (reply && ref) {
|
|
170
|
-
text = reply.text;
|
|
301
|
+
text = reply.text || reply.contents;
|
|
171
302
|
params.title = reply.title || params.title;
|
|
172
303
|
params.date = reply.date || params.date;
|
|
173
304
|
params.subject = reply.subject || params.subject;
|
|
174
305
|
params.richText = Boolean(reply.richText);
|
|
175
|
-
|
|
306
|
+
// Like Wasm, a reply keeps its own open state and icon when it has one.
|
|
307
|
+
params.flag = reply.flag || reply.flags || params.flag;
|
|
308
|
+
params.open = reply.open ?? params.open;
|
|
309
|
+
params.icon = reply.icon ?? params.icon;
|
|
310
|
+
params.name = reply.name ?? params.name;
|
|
176
311
|
}
|
|
177
312
|
|
|
178
313
|
this.dictionaryContext
|
|
@@ -187,11 +322,14 @@ exports._annot = function _annot(subtype, args = {}, pageNumber, ref) {
|
|
|
187
322
|
.writeKey("Subj")
|
|
188
323
|
.writeLiteralStringValue(textString(this.writer, params.subject))
|
|
189
324
|
.writeKey("T")
|
|
190
|
-
.writeLiteralStringValue(textString(this.writer, params.title))
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
325
|
+
.writeLiteralStringValue(textString(this.writer, params.title));
|
|
326
|
+
const date = annotationDate(this.writer, params.date);
|
|
327
|
+
if (date !== undefined) {
|
|
328
|
+
this.dictionaryContext
|
|
329
|
+
.writeKey("M")
|
|
330
|
+
.writeLiteralStringValue(textString(this.writer, date));
|
|
331
|
+
}
|
|
332
|
+
this.dictionaryContext
|
|
195
333
|
.writeKey("Open")
|
|
196
334
|
.writeBooleanValue(params.open)
|
|
197
335
|
.writeKey("F")
|
|
@@ -228,80 +366,87 @@ exports._annot = function _annot(subtype, args = {}, pageNumber, ref) {
|
|
|
228
366
|
.writeNameValue("R");
|
|
229
367
|
}
|
|
230
368
|
|
|
231
|
-
let {
|
|
232
|
-
|
|
233
|
-
|
|
369
|
+
let { color } = options;
|
|
370
|
+
const markup = Boolean(this._getTextMarkupAnnotationSubtype(subtype));
|
|
371
|
+
const border = annotationBorder(options, markup);
|
|
372
|
+
// Custom quad points are written as given; markup annotations otherwise
|
|
373
|
+
// cover their rectangle, in whole numbers.
|
|
374
|
+
const quadPoints =
|
|
375
|
+
options.quadPoints ||
|
|
376
|
+
(markup
|
|
377
|
+
? [
|
|
378
|
+
nx,
|
|
379
|
+
ny + nHeight,
|
|
380
|
+
nx + nWidth,
|
|
381
|
+
ny + nHeight,
|
|
382
|
+
nx,
|
|
383
|
+
ny,
|
|
384
|
+
nx + nWidth,
|
|
385
|
+
ny,
|
|
386
|
+
].map(Math.round)
|
|
387
|
+
: []);
|
|
388
|
+
|
|
389
|
+
if (quadPoints.length) {
|
|
234
390
|
this.dictionaryContext.writeKey("QuadPoints");
|
|
235
|
-
const { _textHeight } = options;
|
|
236
|
-
const annotHeight = height;
|
|
237
|
-
const bx = nx;
|
|
238
|
-
const by = ny + (_textHeight ? 0 : -annotHeight);
|
|
239
|
-
const coordinates = [
|
|
240
|
-
[bx, by + annotHeight],
|
|
241
|
-
[bx + nWidth, by + annotHeight],
|
|
242
|
-
[bx, by],
|
|
243
|
-
[bx + nWidth, by],
|
|
244
|
-
];
|
|
245
391
|
this.objectsContext.startArray();
|
|
246
|
-
|
|
247
|
-
coord.forEach((point) => {
|
|
248
|
-
this.objectsContext.writeNumber(Math.round(point));
|
|
249
|
-
});
|
|
250
|
-
});
|
|
392
|
+
quadPoints.forEach((point) => this.objectsContext.writeNumber(point));
|
|
251
393
|
this.objectsContext.endArray().endLine();
|
|
394
|
+
}
|
|
252
395
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
break;
|
|
265
|
-
case "Squiggly":
|
|
266
|
-
color = [0, 255, 0];
|
|
267
|
-
break;
|
|
268
|
-
default:
|
|
269
|
-
color = [0, 0, 0];
|
|
270
|
-
break;
|
|
271
|
-
}
|
|
396
|
+
if (markup && !color) {
|
|
397
|
+
switch (subtype) {
|
|
398
|
+
case AnnotSubtype.HIGHLIGHT:
|
|
399
|
+
color = [255, 255, 0];
|
|
400
|
+
break;
|
|
401
|
+
case AnnotSubtype.STRIKE_OUT:
|
|
402
|
+
color = [255, 0, 0];
|
|
403
|
+
break;
|
|
404
|
+
default:
|
|
405
|
+
color = [0, 255, 0];
|
|
406
|
+
break;
|
|
272
407
|
}
|
|
273
408
|
}
|
|
274
409
|
|
|
275
|
-
|
|
410
|
+
// A negative width writes no /Border, as in Wasm.
|
|
411
|
+
if (border.width !== undefined && border.width >= 0) {
|
|
276
412
|
this.dictionaryContext.writeKey("Border");
|
|
277
413
|
this.objectsContext
|
|
278
414
|
.startArray()
|
|
279
415
|
.writeNumber(0)
|
|
280
416
|
.writeNumber(0)
|
|
281
|
-
.writeNumber(border)
|
|
282
|
-
|
|
283
|
-
.
|
|
417
|
+
.writeNumber(border.width);
|
|
418
|
+
if (border.dash.length) {
|
|
419
|
+
this.objectsContext.startArray();
|
|
420
|
+
border.dash.forEach((value) => this.objectsContext.writeNumber(value));
|
|
421
|
+
this.objectsContext.endArray();
|
|
422
|
+
}
|
|
423
|
+
this.objectsContext.endArray().endLine();
|
|
284
424
|
}
|
|
285
425
|
|
|
286
426
|
if (color) {
|
|
287
|
-
const rgb = this._colorNumberToRGB(this._transformColor(color));
|
|
288
427
|
this.dictionaryContext.writeKey("C");
|
|
289
|
-
this.objectsContext
|
|
290
|
-
|
|
291
|
-
.writeNumber(
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
.endArray()
|
|
295
|
-
.endLine();
|
|
428
|
+
this.objectsContext.startArray();
|
|
429
|
+
annotationColorComponents(this, color).forEach((component) =>
|
|
430
|
+
this.objectsContext.writeNumber(component),
|
|
431
|
+
);
|
|
432
|
+
this.objectsContext.endArray().endLine();
|
|
296
433
|
}
|
|
297
434
|
|
|
298
435
|
/* Display Icon */
|
|
299
|
-
|
|
300
|
-
|
|
436
|
+
const icon = params.icon || params.name;
|
|
437
|
+
if (icon) {
|
|
438
|
+
this.dictionaryContext.writeKey("Name").writeNameValue(icon);
|
|
301
439
|
}
|
|
302
440
|
return this._endDictionary(pageNumber);
|
|
303
441
|
};
|
|
304
442
|
|
|
443
|
+
/**
|
|
444
|
+
* Write every queued annotation and its replies, then add them to the Annots
|
|
445
|
+
* arrays of their pages.
|
|
446
|
+
* @private
|
|
447
|
+
* @returns {void}
|
|
448
|
+
* @throws {Error} If an annotation or page cannot be written.
|
|
449
|
+
*/
|
|
305
450
|
exports._writeAnnotations = function _writeAnnotations() {
|
|
306
451
|
this.annotationsToWrite.forEach((annot) => {
|
|
307
452
|
const ref = this._annot(annot.subtype, annot.args, annot.pageNumber);
|
|
@@ -322,6 +467,14 @@ exports._writeAnnotations = function _writeAnnotations() {
|
|
|
322
467
|
});
|
|
323
468
|
};
|
|
324
469
|
|
|
470
|
+
/**
|
|
471
|
+
* Rewrite one page dictionary so its Annots array keeps the existing
|
|
472
|
+
* annotations and adds the ones written for it.
|
|
473
|
+
* @private
|
|
474
|
+
* @param {number} pageIndex - The zero-based page index.
|
|
475
|
+
* @returns {void}
|
|
476
|
+
* @throws {Error} If the page cannot be read or rewritten.
|
|
477
|
+
*/
|
|
325
478
|
exports._writeAnnotation = function _writeAnnotation(pageIndex) {
|
|
326
479
|
const pdfWriter = this.writer;
|
|
327
480
|
const copyingContext = pdfWriter.createPDFCopyingContextForModifiedFile();
|
|
@@ -367,12 +520,24 @@ exports._writeAnnotation = function _writeAnnotation(pageIndex) {
|
|
|
367
520
|
copyingContext.end();
|
|
368
521
|
};
|
|
369
522
|
|
|
523
|
+
/**
|
|
524
|
+
* Start a new indirect object holding a dictionary for an annotation.
|
|
525
|
+
* @private
|
|
526
|
+
* @returns {void}
|
|
527
|
+
*/
|
|
370
528
|
exports._startDictionary = function _startDictionary() {
|
|
371
529
|
this.objectsContext = this.writer.getObjectsContext();
|
|
372
530
|
this.dictionaryObject = this.objectsContext.startNewIndirectObject();
|
|
373
531
|
this.dictionaryContext = this.objectsContext.startDictionary();
|
|
374
532
|
};
|
|
375
533
|
|
|
534
|
+
/**
|
|
535
|
+
* End the annotation dictionary started by _startDictionary() and record its
|
|
536
|
+
* object ID for the page.
|
|
537
|
+
* @private
|
|
538
|
+
* @param {number} pageNumber - The one-based page number.
|
|
539
|
+
* @returns {number} The object ID of the annotation.
|
|
540
|
+
*/
|
|
376
541
|
exports._endDictionary = function _endDictionary(pageNumber) {
|
|
377
542
|
this.objectsContext.endDictionary(this.dictionaryContext).endIndirectObject();
|
|
378
543
|
const pageIndex = pageNumber - 1;
|
|
@@ -382,6 +547,13 @@ exports._endDictionary = function _endDictionary(pageNumber) {
|
|
|
382
547
|
return this.dictionaryObject;
|
|
383
548
|
};
|
|
384
549
|
|
|
550
|
+
/**
|
|
551
|
+
* Match a text markup annotation subtype case-insensitively.
|
|
552
|
+
* @private
|
|
553
|
+
* @param {string} [subtype] - The subtype to look up.
|
|
554
|
+
* @returns {string|undefined} The matching `Recipe.AnnotSubtype` markup
|
|
555
|
+
* value, or undefined when the subtype is not a text markup annotation.
|
|
556
|
+
*/
|
|
385
557
|
exports._getTextMarkupAnnotationSubtype =
|
|
386
558
|
function _getTextMarkupAnnotationSubtype(subtype = "") {
|
|
387
559
|
const matchedSubtype = this.textMarkupAnnotations.find((item) => {
|
|
@@ -390,44 +562,235 @@ exports._getTextMarkupAnnotationSubtype =
|
|
|
390
562
|
return matchedSubtype;
|
|
391
563
|
};
|
|
392
564
|
|
|
565
|
+
/**
|
|
566
|
+
* Rejects an unknown annotation flag when the annotation is queued, before the
|
|
567
|
+
* page is written.
|
|
568
|
+
* @private
|
|
569
|
+
* @param {object} options - Annotation options, with optional `replies`.
|
|
570
|
+
* @returns {void}
|
|
571
|
+
* @throws {Error} If a flag is neither a bit mask nor an AnnotFlag value.
|
|
572
|
+
*/
|
|
573
|
+
function validateAnnotationFlags(options) {
|
|
574
|
+
if (!options) return;
|
|
575
|
+
getFlagBitNumberByName(options.flag ?? options.flags);
|
|
576
|
+
(options.replies || []).forEach(validateAnnotationFlags);
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
/**
|
|
580
|
+
* Resolve the border width and dash an annotation writes, as Wasm does: a
|
|
581
|
+
* number `border` is the width, an object `border` gives `width` and `dash`,
|
|
582
|
+
* and `borderWidth` and `borderDash` override the object.
|
|
583
|
+
* @private
|
|
584
|
+
* @param {Object} options - Annotation options.
|
|
585
|
+
* @param {boolean} markup - Whether the subtype is a text markup annotation,
|
|
586
|
+
* whose border defaults to zero.
|
|
587
|
+
* @returns {{width: (number|undefined), dash: Array}} The border width, or
|
|
588
|
+
* undefined for no `/Border` entry, and the dash array.
|
|
589
|
+
*/
|
|
590
|
+
function annotationBorder(options, markup) {
|
|
591
|
+
var border = options.border;
|
|
592
|
+
if (typeof border === "number")
|
|
593
|
+
return { width: border, dash: options.borderDash ?? [] };
|
|
594
|
+
var object = border && typeof border === "object" ? border : {};
|
|
595
|
+
return {
|
|
596
|
+
width: options.borderWidth ?? object.width ?? (markup ? 0 : undefined),
|
|
597
|
+
dash: options.borderDash ?? object.dash ?? [],
|
|
598
|
+
};
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
/**
|
|
602
|
+
* Rejects annotation values that cannot be written as a valid PDF
|
|
603
|
+
* annotation, as Wasm does: `width` and `height` must be finite numbers of at
|
|
604
|
+
* least zero, `opacity` a finite number from 0 to 1 (also on replies), the
|
|
605
|
+
* border width a finite number, `borderDash` an array of finite numbers, and
|
|
606
|
+
* `quadPoints` an array of finite numbers, eight per quadrilateral. A missing
|
|
607
|
+
* size is zero.
|
|
608
|
+
* @private
|
|
609
|
+
* @param {Object} [options] - Annotation options, with optional `replies`.
|
|
610
|
+
* @returns {void}
|
|
611
|
+
* @throws {TypeError} If a value is invalid.
|
|
612
|
+
*/
|
|
613
|
+
function validateAnnotationValues(options) {
|
|
614
|
+
if (!options) return;
|
|
615
|
+
var invalid = function () {
|
|
616
|
+
return new TypeError("Invalid annotation options");
|
|
617
|
+
};
|
|
618
|
+
var finite = function (value) {
|
|
619
|
+
return typeof value === "number" && Number.isFinite(value);
|
|
620
|
+
};
|
|
621
|
+
[options.width, options.height].forEach(function (size) {
|
|
622
|
+
if (size !== undefined && size !== null && !(finite(size) && size >= 0))
|
|
623
|
+
throw invalid();
|
|
624
|
+
});
|
|
625
|
+
var border = annotationBorder(options, false);
|
|
626
|
+
if (border.width !== undefined && !finite(border.width)) throw invalid();
|
|
627
|
+
if (!Array.isArray(border.dash) || !border.dash.every(finite))
|
|
628
|
+
throw invalid();
|
|
629
|
+
var quadPoints = options.quadPoints;
|
|
630
|
+
if (
|
|
631
|
+
quadPoints !== undefined &&
|
|
632
|
+
quadPoints !== null &&
|
|
633
|
+
(!Array.isArray(quadPoints) ||
|
|
634
|
+
quadPoints.length % 8 !== 0 ||
|
|
635
|
+
!quadPoints.every(finite))
|
|
636
|
+
)
|
|
637
|
+
throw invalid();
|
|
638
|
+
[options].concat(options.replies || []).forEach(function (source) {
|
|
639
|
+
var opacity = (source || {}).opacity ?? 1;
|
|
640
|
+
if (!(finite(opacity) && opacity >= 0 && opacity <= 1)) throw invalid();
|
|
641
|
+
});
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
/**
|
|
645
|
+
* Rejects an annotation position that cannot form a finite PDF rectangle, as
|
|
646
|
+
* Wasm does: `x` and `y` must be finite numbers or `Recipe.Coordinate.CENTER`,
|
|
647
|
+
* and the far corner must stay finite. A numeric string would otherwise be
|
|
648
|
+
* concatenated into the rectangle.
|
|
649
|
+
* @private
|
|
650
|
+
* @param {number|"center"} x - The left coordinate.
|
|
651
|
+
* @param {number|"center"} y - The top coordinate.
|
|
652
|
+
* @param {Object} [options] - Annotation options with `width` and `height`.
|
|
653
|
+
* @returns {void}
|
|
654
|
+
* @throws {TypeError} If the position is invalid.
|
|
655
|
+
*/
|
|
656
|
+
function validateAnnotationPosition(x, y, options) {
|
|
657
|
+
var sizes = [(options || {}).width || 0, (options || {}).height || 0];
|
|
658
|
+
[x, y].forEach(function (value, index) {
|
|
659
|
+
if (value === Coordinate.CENTER) return;
|
|
660
|
+
if (!Number.isFinite(value) || !Number.isFinite(value + sizes[index]))
|
|
661
|
+
throw new TypeError("Invalid annotation options");
|
|
662
|
+
});
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
/**
|
|
666
|
+
* Check annotation options that would otherwise only fail when the
|
|
667
|
+
* annotation is written, so an invalid one is never queued.
|
|
668
|
+
* @private
|
|
669
|
+
* @param {Object} [options] - Annotation options, with optional `replies`.
|
|
670
|
+
* @returns {void}
|
|
671
|
+
* @throws {Error} If a flag is neither a bit mask nor an AnnotFlag value.
|
|
672
|
+
* @throws {TypeError} If `options.color` is not a known color, or a size,
|
|
673
|
+
* opacity, border, or `quadPoints` value is invalid.
|
|
674
|
+
*/
|
|
675
|
+
exports._validateAnnot = function _validateAnnot(options) {
|
|
676
|
+
validateAnnotationFlags(options);
|
|
677
|
+
validateAnnotationValues(options);
|
|
678
|
+
annotationColorComponents(this, (options || {}).color);
|
|
679
|
+
};
|
|
680
|
+
|
|
681
|
+
/**
|
|
682
|
+
* Resolve an annotation color to PDF color components, throwing instead of
|
|
683
|
+
* falling back to a default color so a typo never writes a wrong color.
|
|
684
|
+
* @private
|
|
685
|
+
* @param {Recipe} recipe - The Recipe with its registered colors.
|
|
686
|
+
* @param {string|number[]} [color] - A `#rrggbb` HexColor, a `%r,g,b`
|
|
687
|
+
* PercentColor, a gray, RGB, or CMYK DecimalColor array with values from 0 to
|
|
688
|
+
* 255, an RGB color name registered with `chroma()`, or a CSS color name in
|
|
689
|
+
* any case.
|
|
690
|
+
* @returns {number[]} One gray, three RGB, or four CMYK components from 0 to
|
|
691
|
+
* 1; empty when the color is omitted or empty.
|
|
692
|
+
* @throws {TypeError} If an array does not hold one, three, or four numbers
|
|
693
|
+
* from 0 to 255, or any other value is not a known color.
|
|
694
|
+
*/
|
|
695
|
+
function annotationColorComponents(recipe, color) {
|
|
696
|
+
if (color === undefined || color === null || color === "") return [];
|
|
697
|
+
if (Array.isArray(color)) {
|
|
698
|
+
if (
|
|
699
|
+
![1, 3, 4].includes(color.length) ||
|
|
700
|
+
!color.every((part) => Number.isFinite(part) && part >= 0 && part <= 255)
|
|
701
|
+
)
|
|
702
|
+
throw new TypeError(
|
|
703
|
+
"Annotation colors need one, three, or four numbers from 0 to 255",
|
|
704
|
+
);
|
|
705
|
+
return color.map((part) => part / 255);
|
|
706
|
+
}
|
|
707
|
+
var rgb = annotationColorCode(recipe, color);
|
|
708
|
+
if (rgb === undefined)
|
|
709
|
+
throw new TypeError(`Unknown annotation color (${String(color)})`);
|
|
710
|
+
return [(rgb >> 16) & 255, (rgb >> 8) & 255, rgb & 255].map(
|
|
711
|
+
(part) => part / 255,
|
|
712
|
+
);
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
/**
|
|
716
|
+
* Resolve an annotation color string to 0xRRGGBB. Registered Recipe colors
|
|
717
|
+
* win over CSS names, so `chroma()` can redefine one.
|
|
718
|
+
* @private
|
|
719
|
+
* @param {Recipe} recipe - The Recipe with its registered colors.
|
|
720
|
+
* @param {*} color - The color value.
|
|
721
|
+
* @returns {number|undefined} The color, or undefined when it is unknown.
|
|
722
|
+
*/
|
|
723
|
+
function annotationColorCode(recipe, color) {
|
|
724
|
+
if (typeof color !== "string") return undefined;
|
|
725
|
+
var registered = (recipe.knownColors || {})[Colorspace.RGB] || {};
|
|
726
|
+
if (Object.hasOwn(registered, color)) {
|
|
727
|
+
color = String(registered[color]);
|
|
728
|
+
if (/^[0-9a-f]{6}$/i.test(color)) color = `#${color}`;
|
|
729
|
+
} else if (Object.hasOwn(cssColors, color.toLowerCase())) {
|
|
730
|
+
return cssColors[color.toLowerCase()];
|
|
731
|
+
}
|
|
732
|
+
if (/^#[0-9a-f]{6}$/i.test(color)) return parseInt(color.slice(1), 16);
|
|
733
|
+
if (color.startsWith("%")) {
|
|
734
|
+
var parts = color.slice(1).split(",");
|
|
735
|
+
if (
|
|
736
|
+
parts.length === 3 &&
|
|
737
|
+
parts.every(
|
|
738
|
+
(part) =>
|
|
739
|
+
part.trim() !== "" && Number(part) >= 0 && Number(part) <= 100,
|
|
740
|
+
)
|
|
741
|
+
)
|
|
742
|
+
return parts.reduce(
|
|
743
|
+
(value, part) => (value << 8) | Math.round(Number(part) * 2.55),
|
|
744
|
+
0,
|
|
745
|
+
);
|
|
746
|
+
}
|
|
747
|
+
return undefined;
|
|
748
|
+
}
|
|
749
|
+
|
|
393
750
|
/**
|
|
394
751
|
* Get Flag Bit by Name
|
|
395
752
|
* @description 12.5.3 Annotation Flags
|
|
396
753
|
* @private
|
|
397
|
-
* @param {string} name
|
|
754
|
+
* @param {Recipe.AnnotFlag|number|string} [name] - A `Recipe.AnnotFlag` value,
|
|
755
|
+
* matched case-insensitively, or a non-negative integer bit mask.
|
|
756
|
+
* @returns {number} The flag bits; 0 when the flag is omitted or empty.
|
|
757
|
+
* @throws {Error} If `name` is neither a bit mask nor an AnnotFlag value.
|
|
398
758
|
*/
|
|
399
759
|
function getFlagBitNumberByName(name) {
|
|
400
|
-
|
|
401
|
-
|
|
760
|
+
if (name === undefined || name === null || name === "") return 0;
|
|
761
|
+
if (Number.isSafeInteger(name) && name >= 0) return name;
|
|
762
|
+
switch (String(name).toLowerCase()) {
|
|
763
|
+
case AnnotFlag.INVISIBLE:
|
|
402
764
|
return 1;
|
|
403
|
-
case
|
|
765
|
+
case AnnotFlag.HIDDEN:
|
|
404
766
|
return 2;
|
|
405
|
-
case
|
|
767
|
+
case AnnotFlag.PRINT:
|
|
406
768
|
return 4;
|
|
407
|
-
case
|
|
769
|
+
case AnnotFlag.NO_ZOOM:
|
|
408
770
|
return 8;
|
|
409
|
-
case
|
|
771
|
+
case AnnotFlag.NO_ROTATE:
|
|
410
772
|
return 16;
|
|
411
|
-
case
|
|
773
|
+
case AnnotFlag.NO_VIEW:
|
|
412
774
|
return 32;
|
|
413
|
-
case
|
|
775
|
+
case AnnotFlag.READ_ONLY:
|
|
414
776
|
return 64;
|
|
415
|
-
case
|
|
777
|
+
case AnnotFlag.LOCKED:
|
|
416
778
|
return 128;
|
|
417
|
-
case
|
|
779
|
+
case AnnotFlag.TOGGLE_NO_VIEW:
|
|
418
780
|
return 256;
|
|
419
|
-
// 1.7
|
|
420
|
-
|
|
421
|
-
|
|
781
|
+
// PDF 1.7
|
|
782
|
+
case AnnotFlag.LOCKED_CONTENTS:
|
|
783
|
+
return 512;
|
|
422
784
|
default:
|
|
423
|
-
|
|
785
|
+
throw new Error(`Unknown annotation flag (${name})`);
|
|
424
786
|
}
|
|
425
787
|
}
|
|
426
788
|
|
|
427
789
|
/**
|
|
428
790
|
* Text Strings to Rich Text Strings
|
|
429
791
|
* @todo Fix display issue for ol/ul in richText
|
|
430
|
-
* @param {string} content
|
|
792
|
+
* @param {string} content - The XHTML fragment to wrap.
|
|
793
|
+
* @returns {string} The rich text XML document for the RC entry.
|
|
431
794
|
* @private
|
|
432
795
|
* @description Supports XHTML elements: '<p>' | '<span>' | '<b>' | '<i>'. Supports CSS2 styles: 'text-align' | 'vertical-align' | 'font-size' | 'font-style' | 'font-weight' | 'font-family' | 'font' | 'color' | 'text-decoration' | 'font-stretch'.
|
|
433
796
|
*/
|