@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.
Files changed (44) 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 +91 -38
  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 +464 -101
  15. package/lib/recipe/appendPage.js +2 -0
  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 +121 -24
  22. package/lib/recipe/image.js +385 -39
  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 +363 -262
  28. package/lib/recipe/parameters.js +8 -1
  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 +41 -8
  33. package/lib/recipe/text.helper.js +172 -8
  34. package/lib/recipe/text.js +294 -56
  35. package/lib/recipe/utils.js +17 -6
  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 +156 -14
  39. package/lib/recipe/vector.js +156 -52
  40. package/lib/recipe/xObjectForm.js +73 -10
  41. package/lib/recipe-constants.js +373 -0
  42. package/lib/recipe-options.js +47 -0
  43. package/muhammara.d.ts +3501 -160
  44. package/package.json +2 -3
@@ -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
- * Create a comment annotation
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 {'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.
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: "Text",
73
+ subtype: AnnotSubtype.TEXT,
35
74
  pageNumber: this.pageNumber,
36
- 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
+ },
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 coordinate x
84
- * @param {number} y - The coordinate y
85
- * @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.
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 {'invisible'|'hidden'|'print'|'nozoom'|'norotate'|'noview'|'readonly'|'locked'|'togglenoview'} [options.flag] - The flag property
92
- * @param {'Comment'|'Key'|'Note'|'Help'|'NewParagraph'|'Paragraph'|'Insert'} [options.icon='Note'] - The icon of annotation. Specific to text annotations. Default value: 'Note'
93
- * @param {number} [options.width] - Width
94
- * @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.
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
- * @param {number} [options.border] - The border width.
99
- * @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.
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 { 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);
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
- 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
+ );
130
256
 
131
- let nWidth = width;
132
- 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;
133
263
 
134
264
  if (!options.followOriginalPageRotation) {
135
265
  switch (rotate) {
136
266
  case 90:
137
- nWidth = height;
138
- nHeight = width;
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 = height;
147
- nHeight = width;
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
- 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;
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
- .writeKey("M")
192
- .writeLiteralStringValue(
193
- this.writer.createPDFDate(new Date(params.date)).toString(),
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 { border, color } = options;
232
-
233
- 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) {
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
- coordinates.forEach((coord) => {
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
- border = border || 0;
254
- if (!color) {
255
- switch (subtype) {
256
- case "Highlight":
257
- color = [255, 255, 0];
258
- break;
259
- case "StrikeOut":
260
- color = [255, 0, 0];
261
- break;
262
- case "Underline":
263
- color = [0, 255, 0];
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
- if (border != void 0) {
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
- .endArray()
283
- .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();
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
- .startArray()
291
- .writeNumber(rgb.r / 255)
292
- .writeNumber(rgb.g / 255)
293
- .writeNumber(rgb.b / 255)
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
- if (params.icon) {
300
- this.dictionaryContext.writeKey("Name").writeNameValue(params.icon);
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
- switch (name.toLowerCase()) {
401
- 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:
402
764
  return 1;
403
- case "hidden":
765
+ case AnnotFlag.HIDDEN:
404
766
  return 2;
405
- case "print":
767
+ case AnnotFlag.PRINT:
406
768
  return 4;
407
- case "nozoom":
769
+ case AnnotFlag.NO_ZOOM:
408
770
  return 8;
409
- case "norotate":
771
+ case AnnotFlag.NO_ROTATE:
410
772
  return 16;
411
- case "noview":
773
+ case AnnotFlag.NO_VIEW:
412
774
  return 32;
413
- case "readonly":
775
+ case AnnotFlag.READ_ONLY:
414
776
  return 64;
415
- case "locked":
777
+ case AnnotFlag.LOCKED:
416
778
  return 128;
417
- case "togglenoview":
779
+ case AnnotFlag.TOGGLE_NO_VIEW:
418
780
  return 256;
419
- // 1.7+
420
- // case 'lockedcontents':
421
- // return 512;
781
+ // PDF 1.7
782
+ case AnnotFlag.LOCKED_CONTENTS:
783
+ return 512;
422
784
  default:
423
- return 0;
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
  */