@muhammara/native-core 7.0.0-beta.3 → 7.0.0-rc.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.
Files changed (43) hide show
  1. package/README.md +57 -7
  2. package/THIRD_PARTY_NOTICES.md +39 -0
  3. package/index.js +120 -1
  4. package/lib/PDFRStreamForBuffer.js +52 -7
  5. package/lib/PDFRStreamForFile.js +56 -7
  6. package/lib/PDFStreamForResponse.js +22 -5
  7. package/lib/PDFWStreamForBuffer.js +49 -10
  8. package/lib/PDFWStreamForFile.js +26 -5
  9. package/lib/Recipe.js +86 -30
  10. package/lib/content-stream.js +503 -0
  11. package/lib/css-colors.js +149 -0
  12. package/lib/font-text.js +1461 -0
  13. package/lib/glyph-list.js +1127 -0
  14. package/lib/recipe/annotation.js +482 -105
  15. package/lib/recipe/appendPage.js +8 -1
  16. package/lib/recipe/colors.js +132 -35
  17. package/lib/recipe/coordinate.js +82 -16
  18. package/lib/recipe/encrypt.js +32 -13
  19. package/lib/recipe/font.js +70 -25
  20. package/lib/recipe/graphics-state.js +3 -2
  21. package/lib/recipe/htmlToTextObjects.js +154 -21
  22. package/lib/recipe/image.js +34 -9
  23. package/lib/recipe/info.js +93 -22
  24. package/lib/recipe/insertPage.js +12 -3
  25. package/lib/recipe/overlay.js +4 -2
  26. package/lib/recipe/page-references.js +436 -0
  27. package/lib/recipe/page.js +379 -275
  28. package/lib/recipe/parameters.js +11 -2
  29. package/lib/recipe/replaceText.js +464 -30
  30. package/lib/recipe/shapes.js +418 -45
  31. package/lib/recipe/split.js +6 -2
  32. package/lib/recipe/table.js +260 -173
  33. package/lib/recipe/text.helper.js +176 -10
  34. package/lib/recipe/text.js +420 -115
  35. package/lib/recipe/utils.js +66 -3
  36. package/lib/recipe/vector-line.js +101 -39
  37. package/lib/recipe/vector-polygon.js +18 -6
  38. package/lib/recipe/vector.helper.js +150 -13
  39. package/lib/recipe/vector.js +150 -53
  40. package/lib/recipe/xObjectForm.js +73 -10
  41. package/lib/recipe-constants.js +373 -0
  42. package/muhammara.d.ts +3487 -152
  43. package/package.json +1 -1
@@ -1,25 +1,85 @@
1
+ const {
2
+ AnnotSubtype,
3
+ AnnotIcon,
4
+ AnnotFlag,
5
+ Colorspace,
6
+ Coordinate,
7
+ } = require("../recipe-constants");
8
+ const { cssColors } = require("../css-colors");
9
+
10
+ /**
11
+ * Encodes annotation text as a PDF text string, so characters outside
12
+ * PDFDocEncoding are written as UTF-16BE instead of raw UTF-8 bytes.
13
+ * @private
14
+ * @param {Object} writer - The PDF writer.
15
+ * @param {string} [value] - The text to encode.
16
+ * @returns {number[]} The encoded string bytes.
17
+ */
18
+ function textString(writer, value) {
19
+ var text = writer.createPDFTextString();
20
+ text.fromString(String(value ?? ""));
21
+ return text.toBytesArray();
22
+ }
23
+
24
+ /**
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
+
1
41
  /**
2
- * Create a comment annotation
42
+ * Create a comment annotation: a Text annotation with the Comment icon. It is
43
+ * written when the PDF ends.
3
44
  * @name comment
4
45
  * @function
5
46
  * @memberof Recipe#
6
47
  * @param {string} [text=''] - The text content
7
- * @param {number} x - The coordinate x
8
- * @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()`.
9
51
  * @param {Object} [options] - The options
10
52
  * @param {string} [options.title] - The title.
11
53
  * @param {string} [options.date] - The date.
12
54
  * @param {boolean} [options.open=false] - Open the annotation by default?
13
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..."
14
56
  * @param {Array} [options.replies] - Array of annotation replies, each with text and optional title, date, subject, richText, and flag.
15
- * @param {'invisible'|'hidden'|'print'|'nozoom'|'norotate'|'noview'|'readonly'|'locked'|'togglenoview'} [options.flag] - The flag property
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.
16
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.
17
68
  */
18
69
  exports.comment = function comment(text = "", x, y, options = {}) {
70
+ this._validateAnnot(options);
71
+ validateAnnotationPosition(x, y, options);
19
72
  this.annotationsToWrite.push({
20
- subtype: "Text",
73
+ subtype: AnnotSubtype.TEXT,
21
74
  pageNumber: this.pageNumber,
22
- args: { text, x, y, options: Object.assign({ icon: "Comment" }, options) },
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
+ },
23
83
  replies: options.replies,
24
84
  });
25
85
  return this;
@@ -31,20 +91,44 @@ exports.comment = function comment(text = "", x, y, options = {}) {
31
91
  * @function
32
92
  * @memberof Recipe#
33
93
  * @param {string} url - The URL to open.
34
- * @param {number} x - The top-left x coordinate.
35
- * @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.
36
96
  * @param {number} width - The link width.
37
97
  * @param {number} height - The link height.
38
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.
39
101
  */
40
102
  exports.link = function link(url, x, y, width, height) {
41
103
  const { nx, ny } = this._calibrateCoordinate(x, y, 0, -height);
42
104
  return linkPdf(this, url, nx, ny, width, height);
43
105
  };
44
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
+ */
45
121
  function linkPdf(recipe, url, left, bottom, width, height) {
46
122
  recipe.pauseContext();
47
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");
48
132
  recipe.writer.attachURLLinktoCurrentPage(
49
133
  url,
50
134
  left,
@@ -61,31 +145,52 @@ function linkPdf(recipe, url, left, bottom, width, height) {
61
145
  Object.defineProperty(exports, "linkPdf", { value: linkPdf });
62
146
 
63
147
  /**
64
- * Create an annotation
148
+ * Create an annotation. It is written when the PDF ends.
65
149
  * @name annot
66
150
  * @function
67
151
  * @memberof Recipe#
68
152
  * @todo support for rich text RC
69
- * @param {number} x - The coordinate x
70
- * @param {number} y - The coordinate y
71
- * @param {string} subtype - The markup annotation type 'Text'|'Link'|'FreeText'|'Line'|'Square'|'Circle'|'Polygon'|'PolyLine'|'Highlight'|'Underline'|'Squiggly'|'StrikeOut'|'Caret'|'Stamp'|'Ink'|'Popup'|'FileAttachment'|'Sound'|'Movie'|'Screen'|'Widget'|'PrinterMark'|'TrapNet'|'Watermark'|'3D'|'Redact'|'Projection'|'RichMedia'
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.
72
159
  * @param {Object} [options] - The options
73
160
  * @param {string} [options.text=''] - The annotation content.
161
+ * @param {string} [options.contents] - The content when `text` is empty.
74
162
  * @param {string} [options.title] - The title.
75
163
  * @param {boolean} [options.open=false] - Open the annotation. Annotation will be closed by default. Specific to text annotations; subtype='Text'
76
164
  * @param {boolean} [options.richText] - Rich text
77
- * @param {'invisible'|'hidden'|'print'|'nozoom'|'norotate'|'noview'|'readonly'|'locked'|'togglenoview'} [options.flag] - The flag property
78
- * @param {'Comment'|'Key'|'Note'|'Help'|'NewParagraph'|'Paragraph'|'Insert'} [options.icon='Note'] - The icon of annotation. Specific to text annotations. Default value: 'Note'
79
- * @param {number} [options.width] - Width
80
- * @param {number} [options.height] - Height
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.
81
172
  * @param {string} [options.date] - Date of annotation
82
173
  * @param {string} [options.subject] - The subject.
83
- * @param {Array} [options.replies] - Array of annotation replies
84
- * @param {number} [options.border] - The border width.
85
- * @param {string|number[]} [options.color] - The annotation color.
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.
86
189
  * @param {number} [options.opacity=1] - Annotation opacity from 0 (transparent) to 1 (opaque).
87
190
  * @param {boolean} [options.followOriginalPageRotation=false] - Preserve the original page rotation when positioning the annotation.
88
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.
89
194
  */
90
195
  exports.annot = function annot(
91
196
  x,
@@ -93,7 +198,10 @@ exports.annot = function annot(
93
198
  subtype,
94
199
  options = { text: "", width: 0, height: 0 },
95
200
  ) {
96
- const { text, width, height, replies } = options;
201
+ const { width, height, replies } = options;
202
+ const text = options.text || options.contents;
203
+ this._validateAnnot(options);
204
+ validateAnnotationPosition(x, y, options);
97
205
  this.annotationsToWrite.push({
98
206
  subtype,
99
207
  args: { text, x, y, width, height, options },
@@ -105,23 +213,59 @@ exports.annot = function annot(
105
213
 
106
214
  // TODO: allow non-markup annots to be associated with markup annotations
107
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
+ */
108
222
  exports._attachNonMarkupAnnot = function _attachNonMarkupAnnot() {};
109
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
+ */
110
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;
111
243
  const { x, y, width, height, options, reply } = args;
112
244
  let { text } = args;
113
245
  this._startDictionary(pageNumber);
114
246
  const { rotate } = this.metadata[pageNumber];
115
- let { nx, ny } = this._calibrateCoordinateForAnnots(x, y, 0, 0, pageNumber);
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
+ );
116
256
 
117
- let nWidth = width;
118
- let nHeight = height;
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;
119
263
 
120
264
  if (!options.followOriginalPageRotation) {
121
265
  switch (rotate) {
122
266
  case 90:
123
- nWidth = height;
124
- nHeight = width;
267
+ nWidth = rectHeight;
268
+ nHeight = rectWidth;
125
269
  nx = nx - nWidth;
126
270
  break;
127
271
  case 180:
@@ -129,8 +273,8 @@ exports._annot = function _annot(subtype, args = {}, pageNumber, ref) {
129
273
  ny = ny - nHeight;
130
274
  break;
131
275
  case 270:
132
- nWidth = height;
133
- nHeight = width;
276
+ nWidth = rectHeight;
277
+ nHeight = rectWidth;
134
278
  ny = ny - nHeight;
135
279
  break;
136
280
  default:
@@ -152,13 +296,18 @@ exports._annot = function _annot(subtype, args = {}, pageNumber, ref) {
152
296
  const ey = nHeight ? nHeight : 0;
153
297
  const position = [nx, ny, nx + ex, ny + ey];
154
298
 
299
+ params.flag = options.flag ?? options.flags ?? "";
155
300
  if (reply && ref) {
156
- text = reply.text;
301
+ text = reply.text || reply.contents;
157
302
  params.title = reply.title || params.title;
158
303
  params.date = reply.date || params.date;
159
304
  params.subject = reply.subject || params.subject;
160
305
  params.richText = Boolean(reply.richText);
161
- params.flag = reply.flag || params.flag;
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;
162
311
  }
163
312
 
164
313
  this.dictionaryContext
@@ -171,13 +320,16 @@ exports._annot = function _annot(subtype, args = {}, pageNumber, ref) {
171
320
  .writeKey("Rect")
172
321
  .writeRectangleValue(position)
173
322
  .writeKey("Subj")
174
- .writeLiteralStringValue(params.subject)
323
+ .writeLiteralStringValue(textString(this.writer, params.subject))
175
324
  .writeKey("T")
176
- .writeLiteralStringValue(params.title || "")
177
- .writeKey("M")
178
- .writeLiteralStringValue(
179
- this.writer.createPDFDate(new Date(params.date)).toString(),
180
- )
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
181
333
  .writeKey("Open")
182
334
  .writeBooleanValue(params.open)
183
335
  .writeKey("F")
@@ -194,16 +346,16 @@ exports._annot = function _annot(subtype, args = {}, pageNumber, ref) {
194
346
  */
195
347
  if (text && params.richText) {
196
348
  const richText =
197
- text.substring(0, 5) !== "<?xml" ? contentToRC(text) : params.richText;
349
+ text.substring(0, 5) !== "<?xml" ? contentToRC(text) : text;
198
350
  const richTextContent = richText;
199
351
  this.dictionaryContext
200
352
  .writeKey("RC")
201
- .writeLiteralStringValue(richTextContent);
353
+ .writeLiteralStringValue(textString(this.writer, richTextContent));
202
354
  } else if (text) {
203
355
  const textContent = text;
204
356
  this.dictionaryContext
205
357
  .writeKey("Contents")
206
- .writeLiteralStringValue(textContent);
358
+ .writeLiteralStringValue(textString(this.writer, textContent));
207
359
  }
208
360
 
209
361
  if (reply && ref) {
@@ -214,80 +366,87 @@ exports._annot = function _annot(subtype, args = {}, pageNumber, ref) {
214
366
  .writeNameValue("R");
215
367
  }
216
368
 
217
- let { border, color } = options;
218
-
219
- if (this._getTextMarkupAnnotationSubtype(subtype)) {
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) {
220
390
  this.dictionaryContext.writeKey("QuadPoints");
221
- const { _textHeight } = options;
222
- const annotHeight = height;
223
- const bx = nx;
224
- const by = ny + (_textHeight ? 0 : -annotHeight);
225
- const coordinates = [
226
- [bx, by + annotHeight],
227
- [bx + nWidth, by + annotHeight],
228
- [bx, by],
229
- [bx + nWidth, by],
230
- ];
231
391
  this.objectsContext.startArray();
232
- coordinates.forEach((coord) => {
233
- coord.forEach((point) => {
234
- this.objectsContext.writeNumber(Math.round(point));
235
- });
236
- });
392
+ quadPoints.forEach((point) => this.objectsContext.writeNumber(point));
237
393
  this.objectsContext.endArray().endLine();
394
+ }
238
395
 
239
- border = border || 0;
240
- if (!color) {
241
- switch (subtype) {
242
- case "Highlight":
243
- color = [255, 255, 0];
244
- break;
245
- case "StrikeOut":
246
- color = [255, 0, 0];
247
- break;
248
- case "Underline":
249
- color = [0, 255, 0];
250
- break;
251
- case "Squiggly":
252
- color = [0, 255, 0];
253
- break;
254
- default:
255
- color = [0, 0, 0];
256
- break;
257
- }
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;
258
407
  }
259
408
  }
260
409
 
261
- if (border != void 0) {
410
+ // A negative width writes no /Border, as in Wasm.
411
+ if (border.width !== undefined && border.width >= 0) {
262
412
  this.dictionaryContext.writeKey("Border");
263
413
  this.objectsContext
264
414
  .startArray()
265
415
  .writeNumber(0)
266
416
  .writeNumber(0)
267
- .writeNumber(border)
268
- .endArray()
269
- .endLine();
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();
270
424
  }
271
425
 
272
426
  if (color) {
273
- const rgb = this._colorNumberToRGB(this._transformColor(color));
274
427
  this.dictionaryContext.writeKey("C");
275
- this.objectsContext
276
- .startArray()
277
- .writeNumber(rgb.r / 255)
278
- .writeNumber(rgb.g / 255)
279
- .writeNumber(rgb.b / 255)
280
- .endArray()
281
- .endLine();
428
+ this.objectsContext.startArray();
429
+ annotationColorComponents(this, color).forEach((component) =>
430
+ this.objectsContext.writeNumber(component),
431
+ );
432
+ this.objectsContext.endArray().endLine();
282
433
  }
283
434
 
284
435
  /* Display Icon */
285
- if (params.icon) {
286
- this.dictionaryContext.writeKey("Name").writeNameValue(params.icon);
436
+ const icon = params.icon || params.name;
437
+ if (icon) {
438
+ this.dictionaryContext.writeKey("Name").writeNameValue(icon);
287
439
  }
288
440
  return this._endDictionary(pageNumber);
289
441
  };
290
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
+ */
291
450
  exports._writeAnnotations = function _writeAnnotations() {
292
451
  this.annotationsToWrite.forEach((annot) => {
293
452
  const ref = this._annot(annot.subtype, annot.args, annot.pageNumber);
@@ -308,6 +467,14 @@ exports._writeAnnotations = function _writeAnnotations() {
308
467
  });
309
468
  };
310
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
+ */
311
478
  exports._writeAnnotation = function _writeAnnotation(pageIndex) {
312
479
  const pdfWriter = this.writer;
313
480
  const copyingContext = pdfWriter.createPDFCopyingContextForModifiedFile();
@@ -353,12 +520,24 @@ exports._writeAnnotation = function _writeAnnotation(pageIndex) {
353
520
  copyingContext.end();
354
521
  };
355
522
 
523
+ /**
524
+ * Start a new indirect object holding a dictionary for an annotation.
525
+ * @private
526
+ * @returns {void}
527
+ */
356
528
  exports._startDictionary = function _startDictionary() {
357
529
  this.objectsContext = this.writer.getObjectsContext();
358
530
  this.dictionaryObject = this.objectsContext.startNewIndirectObject();
359
531
  this.dictionaryContext = this.objectsContext.startDictionary();
360
532
  };
361
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
+ */
362
541
  exports._endDictionary = function _endDictionary(pageNumber) {
363
542
  this.objectsContext.endDictionary(this.dictionaryContext).endIndirectObject();
364
543
  const pageIndex = pageNumber - 1;
@@ -368,6 +547,13 @@ exports._endDictionary = function _endDictionary(pageNumber) {
368
547
  return this.dictionaryObject;
369
548
  };
370
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
+ */
371
557
  exports._getTextMarkupAnnotationSubtype =
372
558
  function _getTextMarkupAnnotationSubtype(subtype = "") {
373
559
  const matchedSubtype = this.textMarkupAnnotations.find((item) => {
@@ -376,44 +562,235 @@ exports._getTextMarkupAnnotationSubtype =
376
562
  return matchedSubtype;
377
563
  };
378
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
+
379
750
  /**
380
751
  * Get Flag Bit by Name
381
752
  * @description 12.5.3 Annotation Flags
382
753
  * @private
383
- * @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.
384
758
  */
385
759
  function getFlagBitNumberByName(name) {
386
- switch (name.toLowerCase()) {
387
- case "invisible":
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:
388
764
  return 1;
389
- case "hidden":
765
+ case AnnotFlag.HIDDEN:
390
766
  return 2;
391
- case "print":
767
+ case AnnotFlag.PRINT:
392
768
  return 4;
393
- case "nozoom":
769
+ case AnnotFlag.NO_ZOOM:
394
770
  return 8;
395
- case "norotate":
771
+ case AnnotFlag.NO_ROTATE:
396
772
  return 16;
397
- case "noview":
773
+ case AnnotFlag.NO_VIEW:
398
774
  return 32;
399
- case "readonly":
775
+ case AnnotFlag.READ_ONLY:
400
776
  return 64;
401
- case "locked":
777
+ case AnnotFlag.LOCKED:
402
778
  return 128;
403
- case "togglenoview":
779
+ case AnnotFlag.TOGGLE_NO_VIEW:
404
780
  return 256;
405
- // 1.7+
406
- // case 'lockedcontents':
407
- // return 512;
781
+ // PDF 1.7
782
+ case AnnotFlag.LOCKED_CONTENTS:
783
+ return 512;
408
784
  default:
409
- return 0;
785
+ throw new Error(`Unknown annotation flag (${name})`);
410
786
  }
411
787
  }
412
788
 
413
789
  /**
414
790
  * Text Strings to Rich Text Strings
415
791
  * @todo Fix display issue for ol/ul in richText
416
- * @param {string} content
792
+ * @param {string} content - The XHTML fragment to wrap.
793
+ * @returns {string} The rich text XML document for the RC entry.
417
794
  * @private
418
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'.
419
796
  */