@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,10 +1,17 @@
1
1
  const LineBreaker = require("linebreak");
2
2
  const { Word, Line, Column } = require("./text.helper");
3
- const { htmlToTextObjects } = require("./htmlToTextObjects");
3
+ const { htmlToTextObjects, HtmlTag } = require("./htmlToTextObjects");
4
4
  const { Color, xObjectForm } = require("./xObjectForm");
5
5
  const { linkPdf } = require("./annotation");
6
6
  const muhammara = require("../muhammara");
7
7
  const { UsedFont } = require("../muhammara");
8
+ const {
9
+ TextWrap,
10
+ TextAlign,
11
+ VerticalAlign,
12
+ HorizontalAlign,
13
+ Colorspace,
14
+ } = require("../recipe-constants");
8
15
 
9
16
  // Table indicating how to specify coloration of elements
10
17
  // -------------------------------------------------------------------
@@ -40,6 +47,15 @@ const { UsedFont } = require("../muhammara");
40
47
  // return target
41
48
  // }
42
49
 
50
+ /**
51
+ * Merge text options: arrays and non-object values from `source` replace those
52
+ * in `target`, and nested objects are merged. `source` objects are updated in
53
+ * place; the result is a new object.
54
+ * @private
55
+ * @param {Object} target - The base options.
56
+ * @param {Object} source - The overriding options.
57
+ * @returns {Object} The merged options.
58
+ */
43
59
  exports._merge = function merge(target, source) {
44
60
  // Iterate through `source` properties and if an `Object` set property to merge of `target` and `source` properties
45
61
  for (const key of Object.keys(source)) {
@@ -63,6 +79,32 @@ exports._merge = function merge(target, source) {
63
79
  return Object.assign({}, target, source);
64
80
  };
65
81
 
82
+ /**
83
+ * Reject a character spacing that cannot become a `Tc` operand.
84
+ * @private
85
+ * @param {Object} [options] - The text options of one text() call.
86
+ * @returns {void}
87
+ * @throws {TypeError} If `options.charSpace` is given and is not a finite number.
88
+ */
89
+ function _validateCharSpace(options) {
90
+ const charSpace = options?.charSpace ?? 0;
91
+ if (!Number.isFinite(charSpace)) {
92
+ throw new TypeError("charSpace must be a finite number");
93
+ }
94
+ }
95
+
96
+ /**
97
+ * Resolve the text position and options for text(): a call without
98
+ * coordinates continues the flow or starts at the margins, a call with them
99
+ * starts a new text box; previous options are merged in.
100
+ * @private
101
+ * @param {Recipe} self - The recipe instance.
102
+ * @param {number|"center"|Object} [x] - The x coordinate, or the options.
103
+ * @param {number|"center"} [y] - The y coordinate.
104
+ * @param {Object} [options] - The text options.
105
+ * @returns {Object} The merged text options.
106
+ * @throws {TypeError} If no page is active.
107
+ */
66
108
  function _initOptions(self, x = {}, y, options = {}) {
67
109
  // This allows user to skip providing x/y coordinates
68
110
  if (typeof x === "object") {
@@ -138,10 +180,25 @@ function _initOptions(self, x = {}, y, options = {}) {
138
180
  return mergedOpts;
139
181
  }
140
182
 
183
+ /**
184
+ * Whether a value is missing or an empty plain object.
185
+ * @private
186
+ * @param {*} obj - The value.
187
+ * @returns {boolean} True for a falsy value or an empty plain object.
188
+ */
141
189
  function isEmpty(obj) {
142
190
  return !obj || (Object.keys(obj).length === 0 && obj.constructor === Object);
143
191
  }
144
192
 
193
+ /**
194
+ * Wrap plain text as a one-element list of text layout objects, the same
195
+ * shape htmlToTextObjects() returns.
196
+ * @private
197
+ * @param {string} text - The text.
198
+ * @param {number} size - The font size.
199
+ * @param {Object} options - The text options: font, bold and italic.
200
+ * @returns {Object[]} The text layout objects.
201
+ */
145
202
  exports._makeTextObject = function _makeTextObject(text, size, options) {
146
203
  return [
147
204
  {
@@ -162,6 +219,14 @@ exports._makeTextObject = function _makeTextObject(text, size, options) {
162
219
  ];
163
220
  };
164
221
 
222
+ /**
223
+ * Build the text box from the textBox options; without them the text is
224
+ * simple, page-wide text.
225
+ * @private
226
+ * @param {Object} options - The text options.
227
+ * @returns {Object} The text box: width, height, lineHeight, padding,
228
+ * minHeight, style, textAlign, clipping and wrap settings.
229
+ */
165
230
  exports._makeTextBox = function _makeTextBox(options) {
166
231
  return isEmpty(options.textBox)
167
232
  ? {
@@ -171,7 +236,7 @@ exports._makeTextBox = function _makeTextBox(options) {
171
236
  lineHeight: 0,
172
237
  padding: 0,
173
238
  minHeight: 0,
174
- wrap: "auto",
239
+ wrap: TextWrap.AUTO,
175
240
  }
176
241
  : {
177
242
  width: options.textBox.width || 100,
@@ -184,7 +249,9 @@ exports._makeTextBox = function _makeTextBox(options) {
184
249
  clipIfExceedsBox: options.textBox.clipIfExceedsBox,
185
250
  onClip: options.textBox.onClip,
186
251
  wrap:
187
- options.textBox.wrap !== undefined ? options.textBox.wrap : "auto",
252
+ options.textBox.wrap !== undefined
253
+ ? options.textBox.wrap
254
+ : TextWrap.AUTO,
188
255
  };
189
256
  };
190
257
 
@@ -195,8 +262,8 @@ exports._makeTextBox = function _makeTextBox(options) {
195
262
  * @todo support break words
196
263
  * @memberof Recipe#
197
264
  * @param {string} [text=''] - The text content
198
- * @param {number} x - The coordinate x
199
- * @param {number} y - The coordinate y
265
+ * @param {number|"center"|Object} [x] - The coordinate x, or the options to continue at the current position
266
+ * @param {number|"center"} [y] - The coordinate y
200
267
  * @param {Object} [options] - The options
201
268
  * @param {string|number[]} [options.color] - Text color (HexColor, PercentColor or DecimalColor)
202
269
  * @param {number} [options.opacity=1] - opacity
@@ -206,8 +273,8 @@ exports._makeTextBox = function _makeTextBox(options) {
206
273
  * @param {number} [options.size=14] - The font size
207
274
  * @param {number} [options.charSpace=0] - space to be added between characters, units in points.
208
275
  * @param {string} [options.align='left top'] - This is the alignment of the text in relationship to its position
209
- * coordinates, specified as 'horizontal vertical', where horizontal is either 'left', 'center' or 'right
210
- * and vertical is either 'top', 'center' or bottom.
276
+ * coordinates, specified as 'horizontal vertical': a `Recipe.HorizontalAlign` value, optionally followed by a
277
+ * space and a `Recipe.VerticalAlign` value.
211
278
  * @param {Object|Boolean} [options.highlight] - Text markup annotation.
212
279
  * @param {Object|Boolean} [options.underline] - Text markup annotation.
213
280
  * @param {Object|Boolean} [options.strikeOut] - Text markup annotation.
@@ -230,11 +297,11 @@ exports._makeTextBox = function _makeTextBox(options) {
230
297
  * @param {number} [options.textBox.minHeight=0] - Text Box minimum height
231
298
  * @param {number|number[]} [options.textBox.padding=0] - Text Box padding, [top, right, bottom, left]
232
299
  * @param {number} [options.textBox.lineHeight=0] - Text Box line height
233
- * @param {string|Boolean} [options.textBox.wrap='auto'] - Text wrapping mechanism, may be true, false,
234
- * 'auto', 'clip', 'trim', 'ellipsis'. All the option values that are not equivalent to 'auto' dictate
300
+ * @param {Recipe.TextWrap|Boolean} [options.textBox.wrap='auto'] - Text wrapping mechanism, may be true, false,
301
+ * or a `Recipe.TextWrap` value: 'auto', 'clip', 'trim', 'ellipsis'. All the option values that are not equivalent to 'auto' dictate
235
302
  * how the text which does not fit on a line is to be truncated. True is equivalent to 'auto'. False is equivalent to 'ellipsis'.
236
303
  * @param {string} [options.textBox.textAlign='left top'] - Alignment inside text box, specified as 'horizontal vertical',
237
- * where horizontal is one of: 'left', 'center', 'right', 'justify' and vertical is one of: 'top', 'center', 'bottom'.
304
+ * where horizontal is a `Recipe.TextAlign` value and vertical a `Recipe.VerticalAlign` value.
238
305
  * @param {boolean} [options.textBox.clipIfExceedsBox=false] - Render only complete lines that fit within the text box height.
239
306
  * @param {function} [options.textBox.onClip] - Called as onClip(recipe, result) when clipping leaves text unrendered.
240
307
  * Do not call endPage() or endPDF() in this callback because the text operation is still active.
@@ -248,16 +315,31 @@ exports._makeTextBox = function _makeTextBox(options) {
248
315
  * @param {string} [options.title] - Title of annotation
249
316
  * @param {boolean} [options.open=false] - Open the annotation. Annotation will be closed by default. Specific to text annotations; subtype='Text'
250
317
  * @param {boolean} [options.richText] - Rich text in annotation
251
- * @param {'invisible'|'hidden'|'print'|'nozoom'|'norotate'|'noview'|'readonly'|'locked'|'togglenoview'} [options.flag] - The annotation flag.
252
- * @param {'Comment'|'Key'|'Note'|'Help'|'NewParagraph'|'Paragraph'|'Insert'} [options.icon='Note'] - The icon of annotation. Specific to text annotations.
318
+ * @param {Recipe.AnnotFlag} [options.flag] - The annotation flag, a `Recipe.AnnotFlag` value.
319
+ * @param {Recipe.AnnotIcon} [options.icon='Note'] - The icon of annotation, a `Recipe.AnnotIcon` value. Specific to text annotations.
253
320
  * @param {string} [options.date] - Date of text to show up on annotation
254
321
  * @param {string} [options.subject] - Subject of annotation.
255
- * @returns {Recipe} The recipe instance.
322
+ * @param {string} [options.link] - Make the text open this URL.
323
+ * @returns {Recipe} The recipe instance. Without an active page nothing is drawn.
324
+ * @throws {TypeError} If `options.charSpace` or `options.rotation` is not a finite number; nothing is drawn.
325
+ * @throws {Error} If an overflow callback names an undefined layout, or a font cannot be loaded.
256
326
  */
257
327
  exports.text = function text(text = "", x, y, options = {}) {
328
+ // null options act like omitted options.
329
+ if (options === null) options = {};
258
330
  if (!this.pageContext) {
259
331
  return this;
260
332
  }
333
+ // Validate before _initOptions moves the text position or resets the flow.
334
+ const rawOptions = (typeof x === "object" ? x : options) || {};
335
+ _validateCharSpace(rawOptions);
336
+ // Reject invalid markup annotations before any text is drawn.
337
+ for (let key in rawOptions) {
338
+ if (this._getTextMarkupAnnotationSubtype(key) && rawOptions[key]) {
339
+ const markup = typeof rawOptions[key] === "object" ? rawOptions[key] : {};
340
+ this._validateAnnot({ ...markup, flag: rawOptions.flag });
341
+ }
342
+ }
261
343
  options = _initOptions(this, x, y, options);
262
344
  const linkX = this.x;
263
345
  const linkY = this.y;
@@ -361,16 +443,15 @@ exports.text = function text(text = "", x, y, options = {}) {
361
443
  ? toWriteTextObjects[0].writeOptions.alignVertical
362
444
  : undefined
363
445
  ) {
364
- case "center":
446
+ case VerticalAlign.CENTER:
365
447
  textYpos -=
366
448
  (textBox.height - textBox.textHeight) / 2 - textBox.paddingTop;
367
449
  break;
368
- case "bottom":
450
+ case VerticalAlign.BOTTOM:
369
451
  textYpos -= textBox.height - textBox.textHeight - textBox.paddingBottom;
370
452
  break;
371
453
  }
372
454
 
373
- let context = this.pageContext;
374
455
  let currentY = textYpos - textBox.paddingTop;
375
456
  let boxTop = currentY;
376
457
  let currentLineID;
@@ -389,10 +470,10 @@ exports.text = function text(text = "", x, y, options = {}) {
389
470
  let spaceWidth = content.text.endsWith(" ") ? content.spaceWidth : 0;
390
471
  let offsetX;
391
472
  switch (content.writeOptions.alignHorizontal) {
392
- case "center":
473
+ case TextAlign.CENTER:
393
474
  offsetX = (textBox.width - currentLineWidth) / 2;
394
475
  break;
395
- case "right":
476
+ case TextAlign.RIGHT:
396
477
  offsetX =
397
478
  textBox.width -
398
479
  textBox.paddingRight -
@@ -412,9 +493,22 @@ exports.text = function text(text = "", x, y, options = {}) {
412
493
  if (options.underline) {
413
494
  const underlineY = y - options.textHeight * 0.1;
414
495
  const width = options.lineWidth;
496
+ ctx.q();
497
+ if (options.colorModel.xObject) {
498
+ this._setSeparationColor(
499
+ options.colorModel.xObject,
500
+ options.colorModel,
501
+ true,
502
+ );
503
+ }
415
504
  ctx
416
- .q()
417
- .drawPath(x, underlineY, x + width, underlineY, options)
505
+ .drawPath(
506
+ x,
507
+ underlineY,
508
+ x + width,
509
+ underlineY,
510
+ this._devicePathOptions(options),
511
+ )
418
512
  .Q();
419
513
  }
420
514
  };
@@ -424,9 +518,22 @@ exports.text = function text(text = "", x, y, options = {}) {
424
518
  if (options.strikeOut) {
425
519
  const strikeOutY = y + options.textHeight * 0.2;
426
520
  const width = options.lineWidth;
521
+ ctx.q();
522
+ if (options.colorModel.xObject) {
523
+ this._setSeparationColor(
524
+ options.colorModel.xObject,
525
+ options.colorModel,
526
+ true,
527
+ );
528
+ }
427
529
  ctx
428
- .q()
429
- .drawPath(x, strikeOutY, x + width, strikeOutY, options)
530
+ .drawPath(
531
+ x,
532
+ strikeOutY,
533
+ x + width,
534
+ strikeOutY,
535
+ this._devicePathOptions(options),
536
+ )
430
537
  .Q();
431
538
  }
432
539
  };
@@ -474,7 +581,7 @@ exports.text = function text(text = "", x, y, options = {}) {
474
581
  }
475
582
  };
476
583
 
477
- const writeText = (context, x, y, wto) => {
584
+ const writeText = (x, y, wto) => {
478
585
  const options = wto.writeOptions;
479
586
  const { lineWidth, lineHeight, text, baseline } = wto;
480
587
  let next_x = 0;
@@ -500,12 +607,12 @@ exports.text = function text(text = "", x, y, options = {}) {
500
607
  // The hiliting rectangle cannot use the text box line
501
608
  // width when justification is activated because the
502
609
  // spaces between words is calculated dynamically.
503
- if (options.alignHorizontal === "justify") {
610
+ if (options.alignHorizontal === TextAlign.JUSTIFY) {
504
611
  bxWidth = justify(nx, x, wto, textBox) - x;
505
612
 
506
613
  // Except for 'right' alignment cases, have to consider
507
614
  // text on line ending with spaces to tweak box width.
508
- } else if (options.alignHorizontal !== "right") {
615
+ } else if (options.alignHorizontal !== TextAlign.RIGHT) {
509
616
  if (text.endsWith(" ")) {
510
617
  bxWidth += wto.spaceWidth;
511
618
  }
@@ -521,14 +628,18 @@ exports.text = function text(text = "", x, y, options = {}) {
521
628
  }
522
629
 
523
630
  // Note that the last line of a text box ignores justification.
524
- const _justify = options.alignHorizontal === "justify" && !wto.lastLine;
631
+ const _justify =
632
+ options.alignHorizontal === TextAlign.JUSTIFY && !wto.lastLine;
525
633
 
526
634
  // write directly to page when not dealing with opacity, rotation and special colorspace.
527
635
  if (
528
636
  options.opacity === 1 &&
529
- options.colorspace !== "separation" &&
637
+ options.colorspace !== Colorspace.SEPARATION &&
530
638
  (options.rotation === 0 || options.rotation === undefined)
531
639
  ) {
640
+ // Read the context here: a hilite rectangle or an earlier line may
641
+ // have paused an edited page, which resumes into a new context.
642
+ const context = this.pageContext;
532
643
  context.q();
533
644
 
534
645
  if (_justify) {
@@ -544,7 +655,7 @@ exports.text = function text(text = "", x, y, options = {}) {
544
655
  },
545
656
  );
546
657
  } else {
547
- if (textBox.wrap !== "auto") {
658
+ if (textBox.wrap !== TextWrap.AUTO) {
548
659
  // This applies a clipping region around the text
549
660
  context
550
661
  .m(nx, y + lineHeight)
@@ -613,7 +724,7 @@ exports.text = function text(text = "", x, y, options = {}) {
613
724
  var markupBottom = y - textHeight * 0.2;
614
725
  var markupWidth = _justify ? next_x - lineX : currentLineWidth;
615
726
  var markupHeight = textHeight * 1.4;
616
- if (textBox.wrap === "clip") {
727
+ if (textBox.wrap === TextWrap.CLIP) {
617
728
  // Clipped runs retain the first overflowing word, so measure the
618
729
  // drawn text instead of using the preceding fitting line's width.
619
730
  var markupRight = Math.min(
@@ -642,7 +753,6 @@ exports.text = function text(text = "", x, y, options = {}) {
642
753
  height: markupHeight,
643
754
  width: markupWidth,
644
755
  text: markupOption.text || "",
645
- _textHeight: textHeight,
646
756
  // add options to annotation
647
757
  title: title || "",
648
758
  open: Boolean(open),
@@ -652,9 +762,10 @@ exports.text = function text(text = "", x, y, options = {}) {
652
762
  date: date || "",
653
763
  subject: subject || "",
654
764
  });
765
+ // annot() takes the rectangle's top-left corner.
655
766
  const { ox, oy } = this._reverseCoordinate(
656
767
  markupLeft,
657
- markupBottom,
768
+ markupBottom + markupHeight,
658
769
  );
659
770
 
660
771
  this.annot(ox, oy, subtype, markupOption);
@@ -669,7 +780,7 @@ exports.text = function text(text = "", x, y, options = {}) {
669
780
  if (!content.writeOptions.link) return;
670
781
  var left = x;
671
782
  var width = nextX ? nextX - x : content.lineWidth;
672
- if (textBox.wrap === "clip") {
783
+ if (textBox.wrap === TextWrap.CLIP) {
673
784
  var right = Math.min(
674
785
  x + new Word(content.text, content.writeOptions).dimensions.xMax,
675
786
  nx + textBox.width,
@@ -693,7 +804,7 @@ exports.text = function text(text = "", x, y, options = {}) {
693
804
  toWriteContents.forEach((content) => {
694
805
  const x = next_x || getStartX(content.startX, content);
695
806
  const y = currentY;
696
- next_x = writeText(context, x, y, content);
807
+ next_x = writeText(x, y, content);
697
808
  queueTextLink(content, x, y, next_x);
698
809
  });
699
810
  // The line offset from the last line in the
@@ -751,7 +862,6 @@ exports.text = function text(text = "", x, y, options = {}) {
751
862
  updateTextBox(this._columns[columnIndex - 1]);
752
863
  updateVertical = false;
753
864
  }
754
- context = this.pageContext; // in case user changed page
755
865
  }
756
866
 
757
867
  if (updateVertical) {
@@ -791,7 +901,7 @@ exports.text = function text(text = "", x, y, options = {}) {
791
901
  const content = toWriteContents[ii];
792
902
  const x = next_x || getStartX(content.startX, content);
793
903
  const y = currentY;
794
- next_x = writeText(context, x, y, content);
904
+ next_x = writeText(x, y, content);
795
905
  queueTextLink(content, x, y, next_x);
796
906
  }
797
907
 
@@ -813,7 +923,14 @@ exports.text = function text(text = "", x, y, options = {}) {
813
923
  return this;
814
924
  };
815
925
 
816
- /** Writes pending text links before a callback can change the active page. @private */
926
+ /**
927
+ * Write pending text links before a callback can change the active page.
928
+ * @private
929
+ * @param {Recipe} recipe - The recipe instance.
930
+ * @param {Object[]} annotations - Pending {url, left, bottom, width, height}
931
+ * links; emptied.
932
+ * @returns {void}
933
+ */
817
934
  function flushTextLinks(recipe, annotations) {
818
935
  for (var annotation of annotations.splice(0)) {
819
936
  linkPdf(
@@ -827,13 +944,25 @@ function flushTextLinks(recipe, annotations) {
827
944
  }
828
945
  }
829
946
 
947
+ /**
948
+ * Lay out text objects in a text box: resolve padding and wrap, walk HTML
949
+ * children (list bullets, numbering and indentation), split them into lines
950
+ * and turn line breaks into line state.
951
+ * @private
952
+ * @param {Object[]} textObjects - The text layout objects.
953
+ * @param {Object} textBox - The text box; wrap and padding are normalized in place.
954
+ * @param {Object} pathOptions - The resolved text options.
955
+ * @returns {{toWriteTextObjects: Object[], textHeight: number}} The laid-out
956
+ * runs and the total text height.
957
+ * @throws {Error} If a font cannot be loaded.
958
+ */
830
959
  exports._layoutText = function _layoutText(textObjects, textBox, pathOptions) {
831
960
  let totalHeight = 0;
832
961
  // allow user to treat wrap as boolean
833
962
  if (textBox.wrap === true) {
834
- textBox.wrap = "auto";
963
+ textBox.wrap = TextWrap.AUTO;
835
964
  } else if (textBox.wrap === false) {
836
- textBox.wrap = "ellipsis";
965
+ textBox.wrap = TextWrap.ELLIPSIS;
837
966
  }
838
967
 
839
968
  // Allows user to enter a single number which will be used for all text box sides,
@@ -935,34 +1064,36 @@ exports._layoutText = function _layoutText(textObjects, textBox, pathOptions) {
935
1064
  textObject.currentIndex = 0;
936
1065
  let prependValue = textObject.prependValue;
937
1066
 
1067
+ const tag = (textObject.tag || "").toLowerCase();
938
1068
  textObject.childs.forEach((child) => {
939
- if (textObject.tag == "ul") {
1069
+ const childTag = (child.tag || "").toLowerCase();
1070
+ if (tag === HtmlTag.UL) {
940
1071
  child.prependValue = "* ";
941
1072
  child.layer = textObject.layer + 1;
942
1073
  // child.indent = 4 * child.layer;
943
1074
  }
944
- if (textObject.tag == "ol") {
945
- if (child.tag != "ol") {
1075
+ if (tag === HtmlTag.OL) {
1076
+ if (childTag !== HtmlTag.OL) {
946
1077
  textObject.currentIndex++;
947
1078
  child.prependValue = `${textObject.currentIndex.toString()}. `;
948
1079
  }
949
1080
  child.layer = textObject.layer + 1;
950
1081
  // child.indent = 4 * child.layer;
951
1082
  }
952
- if (textObject.tag == "li") {
953
- if (child.tag == "ol" || child.tag == "ul") {
1083
+ if (tag === HtmlTag.LI) {
1084
+ if (childTag === HtmlTag.OL || childTag === HtmlTag.UL) {
954
1085
  child.layer = textObject.layer - 1;
955
1086
  }
956
1087
  }
957
1088
  if (
958
1089
  prependValue &&
959
- !["ol", "ul"].includes(child.tag) &&
1090
+ ![HtmlTag.OL, HtmlTag.UL].includes(childTag) &&
960
1091
  hasRenderableContent(child)
961
1092
  ) {
962
1093
  child.prependValue = prependValue;
963
1094
  prependValue = null;
964
1095
  textObject.indent =
965
- textObject.tag == "li"
1096
+ tag === HtmlTag.LI
966
1097
  ? 2 * textObject.layer
967
1098
  : textObject.indent || 2 * textObject.layer;
968
1099
  }
@@ -1052,6 +1183,13 @@ exports._layoutText = function _layoutText(textObjects, textBox, pathOptions) {
1052
1183
  };
1053
1184
  };
1054
1185
 
1186
+ /**
1187
+ * The height of laid-out text: the sum of the line heights, counting each
1188
+ * line once.
1189
+ * @private
1190
+ * @param {Object[]} textObjs - The laid-out runs.
1191
+ * @returns {number} The text height.
1192
+ */
1055
1193
  function getTextBoxHeight(textObjs) {
1056
1194
  let previousLineID;
1057
1195
  let height = 0;
@@ -1075,6 +1213,15 @@ function getTextBoxHeight(textObjs) {
1075
1213
  return height;
1076
1214
  }
1077
1215
 
1216
+ /**
1217
+ * Keep the complete lines that fit a height.
1218
+ * @private
1219
+ * @param {Object[]} textObjs - The laid-out runs.
1220
+ * @param {number} availableHeight - The height available for text.
1221
+ * @returns {{textObjects: Object[], linesWritten: number, clipped: boolean,
1222
+ * remainder: string}} The fitting runs, their line count, whether lines were
1223
+ * dropped, and the dropped text with its line breaks.
1224
+ */
1078
1225
  function clipTextToBox(textObjs, availableHeight) {
1079
1226
  const lines = [];
1080
1227
  let line = [];
@@ -1119,6 +1266,15 @@ function clipTextToBox(textObjs, availableHeight) {
1119
1266
  };
1120
1267
  }
1121
1268
 
1269
+ /**
1270
+ * The PDF position of a text box, after its alignment offset.
1271
+ * @private
1272
+ * @param {Recipe} self - The recipe instance.
1273
+ * @param {Object} textBox - The laid-out text box.
1274
+ * @param {Object} pathOptions - The resolved text options.
1275
+ * @returns {number[]} The [x, y] PDF position.
1276
+ * @throws {TypeError} If no page is active.
1277
+ */
1122
1278
  function getTextBoxPosition(self, textBox, pathOptions) {
1123
1279
  const { offsetX, offsetY } = self._getTextBoxOffset(textBox, pathOptions);
1124
1280
  const { nx, ny } = self._calibrateCoordinate(
@@ -1130,6 +1286,16 @@ function getTextBoxPosition(self, textBox, pathOptions) {
1130
1286
  return [nx, ny];
1131
1287
  }
1132
1288
 
1289
+ /**
1290
+ * Draw the text box border and background from its style.
1291
+ * @private
1292
+ * @param {Recipe} self - The recipe instance.
1293
+ * @param {number} nx - The PDF x of the box.
1294
+ * @param {number} ny - The PDF y of the first line.
1295
+ * @param {Object} textBox - The laid-out text box with its style.
1296
+ * @param {Object} pathOptions - The resolved text options, for rotation.
1297
+ * @returns {void}
1298
+ */
1133
1299
  function drawTextBox(self, nx, ny, textBox, pathOptions) {
1134
1300
  const textBoxWidth = textBox.width; //+ textBox.paddingLeft + textBox.paddingRight;
1135
1301
  let borderRadius = textBox.style ? textBox.style.borderRadius : 0;
@@ -1159,6 +1325,7 @@ function drawTextBox(self, nx, ny, textBox, pathOptions) {
1159
1325
  * @param {Object[]} wto is a write object
1160
1326
  * @param {Object} textBox holds text box properties
1161
1327
  * @param {Function} [position] used to place given word at a postion on the line
1328
+ * @returns {number} The x where the next run on the line starts.
1162
1329
  */
1163
1330
  function justify(left, x, wto, textBox, position) {
1164
1331
  // For some reason, textWidth is smaller than lineWidth. My suspicions lie in the fact
@@ -1215,6 +1382,15 @@ function justify(left, x, wto, textBox, position) {
1215
1382
  return word.value.endsWith(" ") ? x : x - spaceBetweenWords;
1216
1383
  }
1217
1384
 
1385
+ /**
1386
+ * The word between the previous break and the next one.
1387
+ * @private
1388
+ * @param {string} text - The text being broken.
1389
+ * @param {Object} brk - The line break opportunity: position and required.
1390
+ * @param {number} previousPosition - The position of the previous break.
1391
+ * @param {Object} pathOptions - The resolved text options.
1392
+ * @returns {Word} The word; trimmed at a required break.
1393
+ */
1218
1394
  function nextWord(text, brk, previousPosition, pathOptions) {
1219
1395
  let nextWord = text.slice(previousPosition, brk.position);
1220
1396
 
@@ -1226,10 +1402,21 @@ function nextWord(text, brk, previousPosition, pathOptions) {
1226
1402
  return new Word(nextWord, pathOptions);
1227
1403
  }
1228
1404
 
1405
+ /**
1406
+ * Handle the first word that does not fit a line when wrapping is off: CLIP
1407
+ * keeps it for the clipping region, ELLIPSIS shortens it with "…", and TRIM
1408
+ * drops it.
1409
+ * @private
1410
+ * @param {Object} textBox - The text box with its wrap mode.
1411
+ * @param {Line} line - The full line; updated in place.
1412
+ * @param {Word} word - The word that did not fit.
1413
+ * @param {Object} pathOptions - The resolved text options.
1414
+ * @returns {void}
1415
+ */
1229
1416
  function elideNonFittingText(textBox, line, word, pathOptions) {
1230
- if (textBox.wrap === "clip") {
1417
+ if (textBox.wrap === TextWrap.CLIP) {
1231
1418
  line.addWord(word);
1232
- } else if (textBox.wrap === "ellipsis") {
1419
+ } else if (textBox.wrap === TextWrap.ELLIPSIS) {
1233
1420
  // This is more complicated than the other no-wrap options.
1234
1421
  // It makes an initial attempt to take the word that was
1235
1422
  // too big and make it shrink in size until it and the
@@ -1260,6 +1447,17 @@ function elideNonFittingText(textBox, line, word, pathOptions) {
1260
1447
  }
1261
1448
  }
1262
1449
 
1450
+ /**
1451
+ * Turn a finished line into a laid-out run and record the line.
1452
+ * @private
1453
+ * @param {Line[]} lines - The finished lines; the line is appended.
1454
+ * @param {Line} line - The line.
1455
+ * @param {number} lineID - The ID tying the first HTML line to its group.
1456
+ * @param {Object} textBox - The laid-out text box.
1457
+ * @param {Object} [options] - html, lastLine, lineComplete, wordCount,
1458
+ * totalTextWidth and writeOptions.
1459
+ * @returns {Object} The run: text, line metrics and justification data.
1460
+ */
1263
1461
  function makeTextObject(lines, line, lineID, textBox, options = {}) {
1264
1462
  const lineHeight = line.height;
1265
1463
  const spaceSz =
@@ -1296,6 +1494,15 @@ function makeTextObject(lines, line, lineID, textBox, options = {}) {
1296
1494
  };
1297
1495
  }
1298
1496
 
1497
+ /**
1498
+ * Carry justification totals across runs that share a line.
1499
+ * @private
1500
+ * @param {Line} line - The line being finished.
1501
+ * @param {Object[]} textObjects - The runs laid out so far; updated in place.
1502
+ * @param {number} wordCount - The words counted so far on the line.
1503
+ * @param {number} totalTextWidth - The text width counted so far on the line.
1504
+ * @returns {number[]} The updated [wordCount, totalTextWidth].
1505
+ */
1299
1506
  function bindTextToLine(line, textObjects, wordCount, totalTextWidth) {
1300
1507
  // Apply justification information to previous text objects.
1301
1508
  if (wordCount > 0) {
@@ -1333,6 +1540,18 @@ function bindTextToLine(line, textObjects, wordCount, totalTextWidth) {
1333
1540
  return [wordCount, totalTextWidth];
1334
1541
  }
1335
1542
 
1543
+ /**
1544
+ * Break one text layout object into lines that fit the text box, continuing
1545
+ * any unfinished flowed lines.
1546
+ * @private
1547
+ * @param {Recipe} self - The recipe instance.
1548
+ * @param {Object} [textObject] - The text layout object.
1549
+ * @param {Object} pathOptions - The resolved text options.
1550
+ * @param {Object} [textBox] - The text box; line and baseline heights are set in place.
1551
+ * @returns {{toWriteTextObjects: Object[], paragraphHeight: number}} The runs,
1552
+ * including the carried-over flowed ones, and the paragraph height.
1553
+ * @throws {Error} If the font cannot be loaded.
1554
+ */
1336
1555
  function makeTextObjects(self, textObject = {}, pathOptions, textBox = {}) {
1337
1556
  const toWriteTextObjects = [...self._previousTextObjects];
1338
1557
  let text =
@@ -1463,7 +1682,7 @@ function makeTextObjects(self, textObject = {}, pathOptions, textBox = {}) {
1463
1682
  }
1464
1683
  } else {
1465
1684
  // remove any trailing space on previous word so right justification works appropriately
1466
- if (previousWord && textBox.wrap === "auto") {
1685
+ if (previousWord && textBox.wrap === TextWrap.AUTO) {
1467
1686
  newLine.replaceLastWord(previousWord.value.trim());
1468
1687
  }
1469
1688
 
@@ -1492,7 +1711,7 @@ function makeTextObjects(self, textObject = {}, pathOptions, textBox = {}) {
1492
1711
  }
1493
1712
 
1494
1713
  // now deal with text line wrap (what happens to text that doesn't fit in line)
1495
- if (textBox.wrap !== "auto") {
1714
+ if (textBox.wrap !== TextWrap.AUTO) {
1496
1715
  flushLine = true;
1497
1716
  elideNonFittingText(textBox, newLine, word, pathOptions);
1498
1717
 
@@ -1566,7 +1785,7 @@ function makeTextObjects(self, textObject = {}, pathOptions, textBox = {}) {
1566
1785
  }
1567
1786
  }
1568
1787
 
1569
- let isLastLine = alignHorizontal === "justify" && !self._flow;
1788
+ let isLastLine = alignHorizontal === TextAlign.JUSTIFY && !self._flow;
1570
1789
 
1571
1790
  if (!flushLine) {
1572
1791
  [wordCount, totalTextWidth] = bindTextToLine(
@@ -1602,6 +1821,14 @@ function makeTextObjects(self, textObject = {}, pathOptions, textBox = {}) {
1602
1821
  };
1603
1822
  }
1604
1823
 
1824
+ /**
1825
+ * Mark the last run as ending its line: trim it, mark its last word and
1826
+ * optionally move the next line down.
1827
+ * @private
1828
+ * @param {Object[]} toWriteTextObjects - The runs; the last one is updated.
1829
+ * @param {number|null} [lines=null] - The line offset for the next line.
1830
+ * @returns {void}
1831
+ */
1605
1832
  function markLineComplete(toWriteTextObjects, lines = null) {
1606
1833
  // Get last element in text objects and mark it.
1607
1834
  const textObj = toWriteTextObjects[toWriteTextObjects.length - 1];
@@ -1627,8 +1854,9 @@ function markLineComplete(toWriteTextObjects, lines = null) {
1627
1854
  exports.movedown = function movedown(lines = 1, returnCoords = false) {
1628
1855
  if (!this._flow || this._previousTextObjects.length === 0) {
1629
1856
  this._previousTextObjects = [];
1630
- this.y += this._lineHeight * lines;
1631
- this.x = this.box.x;
1857
+ // Before any text is written there is no cursor or line height yet.
1858
+ this.y = (this.y || 0) + (this._lineHeight || 14) * lines;
1859
+ this.x = this.box ? this.box.x : this.x || 0;
1632
1860
  } else {
1633
1861
  // This handles continuous text positioning
1634
1862
  markLineComplete(this._previousTextObjects, lines);
@@ -1639,6 +1867,15 @@ exports.movedown = function movedown(lines = 1, returnCoords = false) {
1639
1867
  return returnCoords ? [this.x, this.y] : this;
1640
1868
  };
1641
1869
 
1870
+ /**
1871
+ * Move a column layout so its first column starts at a point, keeping the
1872
+ * column spacing.
1873
+ * @private
1874
+ * @param {Column[]} columns - The layout columns; updated in place.
1875
+ * @param {number} x - The new x of the first column.
1876
+ * @param {number} y - The new y of the first column.
1877
+ * @returns {void}
1878
+ */
1642
1879
  function adjustcolumnPosition(columns, x, y) {
1643
1880
  const ydiff = y - columns[0].y;
1644
1881
  for (const column of columns) {
@@ -1653,16 +1890,17 @@ function adjustcolumnPosition(columns, x, y) {
1653
1890
  * @function
1654
1891
  * @memberof Recipe#
1655
1892
  * @param {number|string} id - The identifier to be associated with the layout. (See 'text' layout option)
1656
- * @param {number} x - The coordinate x used to position text columns on page. When zero, left margin used.
1657
- * @param {number} y - The coordinate y used to position text columns on page. When zero, top margin used.
1658
- * @param {number} width - The width of a text column. When zero, space between left and right margin used.
1659
- * @param {number} height - The height of a text column. When zero, space between top and bottom margin used.
1893
+ * @param {number} [x] - The coordinate x used to position text columns on page. When zero or omitted, left margin used.
1894
+ * @param {number} [y] - The coordinate y used to position text columns on page. When zero or omitted, top margin used.
1895
+ * @param {number} [width] - The width of a text column. When zero or omitted, space between left and right margin used.
1896
+ * @param {number} [height] - The height of a text column. When zero or omitted, space between top and bottom margin used.
1660
1897
  * @param {object} [options] - The options.
1661
1898
  * @param {number} [options.columns] - Represents the number of columns in which to divide the given width.
1662
1899
  * @param {number} [options.gap=18] - Defines the separation between layout columns, units in points.
1663
1900
  * @param {boolean} [options.reset] - True indicates that the a new layout should be produced for the given
1664
1901
  * layout id, so any previous layout associated with the given id will be lost.
1665
1902
  * @returns {Recipe} The recipe instance.
1903
+ * @throws {TypeError} If width or height is omitted while no page is active.
1666
1904
  */
1667
1905
  exports.layout = function layout(id, x, y, width, height, options = {}) {
1668
1906
  this._layouts = this._layouts || {};