@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
@@ -8,8 +8,12 @@ const utils = require("./utils");
8
8
  * @function
9
9
  * @memberof Recipe#
10
10
  * @param {string} [outputDir=''] - The path for the output PDFs.
11
- * @param {string} [prefix] - The output filename prefix. Defaults to the source filename.
12
- * @returns {Recipe} The recipe instance.
11
+ * @param {string} [prefix] - The output filename prefix. Defaults to the
12
+ * source filename; pass one for a Buffer source, which has no filename.
13
+ * @returns {Recipe} The recipe instance. Each page is written to
14
+ * `<outputDir>/<prefix>-<pageNumber>.pdf`.
15
+ * @throws {Error} If the source reader was released by endPDF(), or an output
16
+ * file cannot be written.
13
17
  */
14
18
  exports.split = function split(outputDir = "", prefix) {
15
19
  prefix = prefix || this.filename;
@@ -1,7 +1,15 @@
1
1
  var { htmlToTextObjects } = require("./htmlToTextObjects");
2
2
  var { cloneOptions: clone } = require("./utils");
3
+ var { LineCap, TableRowNth } = require("../recipe-constants");
3
4
 
4
- /** Converts a table cell style into text options. */
5
+ /**
6
+ * Convert a table cell style into text options: a copy of the options with
7
+ * the cell entry moved to `textBox`.
8
+ * @private
9
+ * @param {Object} options - The table, header, row or column options.
10
+ * @param {string} [cell='cell'] - The key holding the cell style, "cell" or "hcell".
11
+ * @returns {Object} The text options.
12
+ */
5
13
  function getCellOptions(options, cell = "cell") {
6
14
  var cellOptions = clone(options);
7
15
 
@@ -13,7 +21,16 @@ function getCellOptions(options, cell = "cell") {
13
21
  return cellOptions;
14
22
  }
15
23
 
16
- /** Measures the same text and outer box height that text() will draw. */
24
+ /**
25
+ * Measure the outer box height text() will draw for a cell.
26
+ * @private
27
+ * @param {Recipe} self - The recipe instance.
28
+ * @param {string} text - The cell text or HTML.
29
+ * @param {Object} column - The table layout column: x, y and width.
30
+ * @param {Object} options - The cell text options.
31
+ * @returns {number} The box height, including padding and minimum height.
32
+ * @throws {TypeError} If no page is active.
33
+ */
17
34
  function getCellHeight(self, text, column, options) {
18
35
  var colOptions = self._merge(options, { textBox: { width: column.width } });
19
36
  var originCoord = self._calibrateCoordinate(
@@ -44,7 +61,19 @@ function getCellHeight(self, text, column, options) {
44
61
  );
45
62
  }
46
63
 
47
- /** Draws a completed table segment without duplicating its bottom edge. */
64
+ /**
65
+ * Draw the border of a completed table segment without duplicating its
66
+ * bottom edge.
67
+ * @private
68
+ * @param {Recipe} self - The recipe instance.
69
+ * @param {number} x - The segment left.
70
+ * @param {number} y - The segment top.
71
+ * @param {number} width - The segment width.
72
+ * @param {number} height - The segment height; nothing is drawn when 0.
73
+ * @param {number[]} rowLines - The y of every row bottom.
74
+ * @param {Object} options - The table options; `border` enables drawing.
75
+ * @returns {void}
76
+ */
48
77
  function drawTableBorder(self, x, y, width, height, rowLines, options) {
49
78
  // A segment without rows has nothing to enclose.
50
79
  if (!options.border || height <= 0) {
@@ -54,7 +83,7 @@ function drawTableBorder(self, x, y, width, height, rowLines, options) {
54
83
  {},
55
84
  options.border === true ? {} : options.border,
56
85
  // Keep borders from extending outside of the enclosing box.
57
- { lineCap: "butt" },
86
+ { lineCap: LineCap.BUTT },
58
87
  );
59
88
  if (!borderOptions.width) {
60
89
  borderOptions.width = 0.5;
@@ -92,6 +121,9 @@ function drawTableBorder(self, x, y, width, height, rowLines, options) {
92
121
  * of the configured `columns`, otherwise every field found in any record, in
93
122
  * first-seen order.
94
123
  * @private
124
+ * @param {Object[]} contents - The table records.
125
+ * @param {Object} options - The table options.
126
+ * @returns {string[]} The field names, in column order.
95
127
  */
96
128
  function tableFields(contents, options) {
97
129
  if (options.order && options.order.length) {
@@ -166,9 +198,10 @@ function tableFields(contents, options) {
166
198
  * a 'position' property indicating the [x,y] coordinates where the next table for the remaining data should start.
167
199
  * @param {object} [options.row] - text properties to be applied to all cells in a table row.
168
200
  * @param {object} [options.row.cell] - All textBox options from the 'text' interface can be used here.
169
- * @param {string} [options.row.nth] - 'even|odd', indicating that the properties should be applied only to
170
- * 'even' or 'odd' rows.
201
+ * @param {Recipe.TableRowNth} [options.row.nth] - A `Recipe.TableRowNth` value, indicating that the
202
+ * properties should be applied only to 'even' or 'odd' rows.
171
203
  * @returns {Recipe} The recipe instance.
204
+ * @throws {TypeError} If no page is active.
172
205
  * @throws {RangeError} If the overflow callback continues into an area too small
173
206
  * for the pending row and its repeated header. Return true to stop, or provide
174
207
  * enough space; rows are not split and the callback is called once per overflow.
@@ -248,12 +281,12 @@ exports.table = function table(x, y, contents, options = {}) {
248
281
  rowOptions = getCellOptions(options.row);
249
282
 
250
283
  switch (options.row.nth) {
251
- case "even":
284
+ case TableRowNth.EVEN:
252
285
  nth = (row) => {
253
286
  return row % 2 === 0;
254
287
  };
255
288
  break;
256
- case "odd":
289
+ case TableRowNth.ODD:
257
290
  nth = (row) => {
258
291
  return row % 2 !== 0;
259
292
  };
@@ -1,13 +1,29 @@
1
1
  var { cloneOptions, resolveFontSize } = require("./utils");
2
+ var { HorizontalAlign, VerticalAlign } = require("../recipe-constants");
2
3
 
4
+ /**
5
+ * The width character spacing adds between the characters of a text.
6
+ * @private
7
+ * @param {string} text - The text.
8
+ * @param {number} charSpace - The spacing added after each character but the last.
9
+ * @returns {number} The added width.
10
+ */
3
11
  const charSpacing = function charSpacing(text, charSpace) {
4
- let txt = text.replace(/^(?:(?!\u00a0)\s)+|(?:(?!\u00a0)\s)+$/g, "");
5
- return txt.length ? (txt.length - 1) * charSpace : 0;
12
+ var characterCount = Array.from(String(text)).length;
13
+ return characterCount ? (characterCount - 1) * charSpace : 0;
6
14
  };
7
15
 
8
16
  // Have to set up word as a constant, then export it below
9
17
  // so that Line can see it. Otherwise, an error is thrown.
10
18
 
19
+ /**
20
+ * A word used by Recipe text layout.
21
+ * @name Word
22
+ * @class
23
+ * @memberof Recipe#
24
+ * @param {string} word - The word, a single space measured as "o".
25
+ * @param {Object} pathOptions - The resolved text options: font, size and charSpace.
26
+ */
11
27
  const Word = class Word {
12
28
  constructor(word, pathOptions) {
13
29
  this._value = word;
@@ -16,10 +32,17 @@ const Word = class Word {
16
32
  this._text = word === " " ? "o" : word; // allows space to get an actual dimension
17
33
  }
18
34
 
35
+ /**
36
+ * @returns {string} The word text.
37
+ */
19
38
  get value() {
20
39
  return this._value;
21
40
  }
22
41
 
42
+ /**
43
+ * The measured text box, including character spacing; cached.
44
+ * @returns {Object} xMin, yMin, xMax, yMax, width and height.
45
+ */
23
46
  get dimensions() {
24
47
  if (this._dimensions) {
25
48
  return this._dimensions;
@@ -32,14 +55,26 @@ const Word = class Word {
32
55
  return this._dimensions;
33
56
  }
34
57
 
58
+ /**
59
+ * @returns {boolean} Whether this is the last word of its line.
60
+ */
35
61
  get last() {
36
62
  return this._last;
37
63
  }
38
64
 
65
+ /**
66
+ * @returns {number} The width character spacing adds to the word.
67
+ */
39
68
  get charSpacing() {
40
69
  return charSpacing(this._text, this._pathOptions.charSpace);
41
70
  }
42
71
 
72
+ /**
73
+ * Mark the word as the last of its line, trimming trailing space and
74
+ * measuring it again.
75
+ * @param {boolean} [value=true] - Whether the word is last.
76
+ * @returns {void}
77
+ */
43
78
  lastWord(value = true) {
44
79
  // indicate last word in line (for justification)
45
80
  this._last = value;
@@ -75,9 +110,19 @@ exports.Word = Word; // ... now export Word to the rest of the library.
75
110
  * @param {number} size - The font size.
76
111
  * @param {Object} pathOptions - The resolved text options.
77
112
  */
113
+ // The width of a line without a text box. No text reaches it, so such a line
114
+ // accepts every word without measuring.
115
+ const UNBOUNDED_LINE_WIDTH = 999999999;
116
+
78
117
  exports.Line = class Line {
118
+ /**
119
+ * @param {number} [width] - The available width; unlimited when omitted.
120
+ * @param {number} [height] - A fixed line height.
121
+ * @param {number} [size] - The font size; defaults to the text options size.
122
+ * @param {Object} pathOptions - The resolved text options.
123
+ */
79
124
  constructor(width, height, size, pathOptions) {
80
- this._width = width || 999999999;
125
+ this._width = width || UNBOUNDED_LINE_WIDTH;
81
126
  this._height = height;
82
127
  this._pathOptions = pathOptions;
83
128
  this.size = size || pathOptions.size;
@@ -85,41 +130,75 @@ exports.Line = class Line {
85
130
  this.wordObjects = [];
86
131
  }
87
132
 
133
+ /**
134
+ * Replace the line ID; falsy values are ignored.
135
+ * @param {number} id - The new ID.
136
+ */
88
137
  set lineID(id) {
89
138
  if (id) {
90
139
  this._lineID = id;
91
140
  }
92
141
  }
93
142
 
143
+ /**
144
+ * @returns {number} The line ID, grouping objects laid out on one line.
145
+ */
94
146
  get lineID() {
95
147
  return this._lineID;
96
148
  }
97
149
 
150
+ /**
151
+ * @param {Word} wordObject - The word to append.
152
+ * @returns {void}
153
+ */
98
154
  addWord(wordObject) {
99
155
  this.wordObjects.push(wordObject);
100
156
  }
101
157
 
158
+ /**
159
+ * Append leading spaces.
160
+ * @param {number} amount - The number of spaces.
161
+ * @returns {void}
162
+ */
102
163
  indent(amount) {
103
164
  for (let i = 0; i < amount; i++) {
104
165
  this.addWord(new Word(" ", this._pathOptions));
105
166
  }
106
167
  }
107
168
 
169
+ /**
170
+ * Mark the final word as the last of the line.
171
+ * @returns {void}
172
+ */
108
173
  markLastWord() {
109
174
  if (this.wordObjects.length > 0) {
110
175
  this.wordObjects[this.wordObjects.length - 1].lastWord();
111
176
  }
112
177
  }
113
178
 
179
+ /**
180
+ * @returns {Word|undefined} The final word, or undefined for an empty line.
181
+ */
114
182
  get lastWord() {
115
183
  return this.wordObjects[this.wordObjects.length - 1];
116
184
  }
117
185
 
186
+ /**
187
+ * @param {string} text - The text.
188
+ * @returns {number} The width character spacing adds to the text.
189
+ */
118
190
  charSpacing(text) {
119
191
  return charSpacing(text, this._pathOptions.charSpace);
120
192
  }
121
193
 
194
+ /**
195
+ * @param {Word} wordObject - The word to test.
196
+ * @returns {boolean} Whether the line still fits its width with the word appended.
197
+ */
122
198
  canFit(wordObject) {
199
+ // Measuring the whole line for every word is quadratic in its length, and
200
+ // a line without a text box never wraps.
201
+ if (this._width >= UNBOUNDED_LINE_WIDTH) return true;
123
202
  const tempValue = this.value + wordObject.value;
124
203
  const toWidth =
125
204
  this._pathOptions.font.calculateTextDimensions(tempValue, this.size)
@@ -127,6 +206,11 @@ exports.Line = class Line {
127
206
  return toWidth <= this.width;
128
207
  }
129
208
 
209
+ /**
210
+ * Replace the final word and mark the new one as last.
211
+ * @param {Word|string} wordObject - The replacement word.
212
+ * @returns {void}
213
+ */
130
214
  replaceLastWord(wordObject) {
131
215
  if (typeof wordObject === "string") {
132
216
  wordObject = new Word(wordObject, this._pathOptions);
@@ -136,14 +220,23 @@ exports.Line = class Line {
136
220
  wordObject.lastWord();
137
221
  }
138
222
 
223
+ /**
224
+ * @returns {Word[]} The words of the line.
225
+ */
139
226
  get words() {
140
227
  return this.wordObjects;
141
228
  }
142
229
 
230
+ /**
231
+ * @returns {number} The width of one space, measured as "o".
232
+ */
143
233
  get spaceWidth() {
144
234
  return this._pathOptions.font.calculateTextDimensions("o", this.size).width;
145
235
  }
146
236
 
237
+ /**
238
+ * @returns {string} The text of the line.
239
+ */
147
240
  get value() {
148
241
  const value = this.wordObjects.reduce((string, word) => {
149
242
  string += word.value;
@@ -152,6 +245,9 @@ exports.Line = class Line {
152
245
  return value;
153
246
  }
154
247
 
248
+ /**
249
+ * @returns {number} The measured width of the line text.
250
+ */
155
251
  get currentWidth() {
156
252
  return (
157
253
  this._pathOptions.font.calculateTextDimensions(this.value, this.size)
@@ -159,6 +255,9 @@ exports.Line = class Line {
159
255
  );
160
256
  }
161
257
 
258
+ /**
259
+ * @returns {number} The sum of the measured word widths.
260
+ */
162
261
  get textWidth() {
163
262
  return this.wordObjects.reduce((width, word) => {
164
263
  width += word.dimensions.xMax;
@@ -166,11 +265,17 @@ exports.Line = class Line {
166
265
  }, 0);
167
266
  }
168
267
 
268
+ /**
269
+ * @returns {number} The available width.
270
+ */
169
271
  get width() {
170
272
  return this._width;
171
273
  }
172
274
 
173
275
  // dynamic adjust height based on word height?
276
+ /**
277
+ * @returns {number} The fixed height, or the measured text height plus 20.
278
+ */
174
279
  get height() {
175
280
  if (this._height) {
176
281
  return this._height;
@@ -187,6 +292,12 @@ exports.Line = class Line {
187
292
  * Get the offset for a text box.
188
293
  * @private
189
294
  * @todo handle page margin and padding
295
+ * @param {Object} textBox - The laid-out text box: width, height, textHeight,
296
+ * firstLineHeight and isSimpleText.
297
+ * @param {Object} [options] - The text options.
298
+ * @param {string} [options.align] - A `Recipe.HorizontalAlign` value,
299
+ * optionally followed by a space and a `Recipe.VerticalAlign` value.
300
+ * @returns {{offsetX: number, offsetY: number}} The offset from the placement point.
190
301
  */
191
302
  exports._getTextBoxOffset = function _getTextBoxOffset(textBox, options = {}) {
192
303
  let offsetX = 0;
@@ -196,10 +307,10 @@ exports._getTextBoxOffset = function _getTextBoxOffset(textBox, options = {}) {
196
307
  const alignments = options.align.split(" ");
197
308
  if (alignments[0]) {
198
309
  switch (alignments[0]) {
199
- case "center":
310
+ case HorizontalAlign.CENTER:
200
311
  offsetX = (-1 * width) / 2;
201
312
  break;
202
- case "right":
313
+ case HorizontalAlign.RIGHT:
203
314
  offsetX = -width;
204
315
  break;
205
316
  default:
@@ -208,12 +319,12 @@ exports._getTextBoxOffset = function _getTextBoxOffset(textBox, options = {}) {
208
319
  if (alignments[1]) {
209
320
  height = height || textHeight;
210
321
  switch (alignments[1]) {
211
- case "center":
322
+ case VerticalAlign.CENTER:
212
323
  offsetY = textBox.isSimpleText
213
324
  ? -textBox.firstLineHeight / 2
214
325
  : height / 2 + offsetY;
215
326
  break;
216
- case "bottom":
327
+ case VerticalAlign.BOTTOM:
217
328
  offsetY = height + offsetY;
218
329
  break;
219
330
  default:
@@ -237,9 +348,14 @@ exports._getTextBoxOffset = function _getTextBoxOffset(textBox, options = {}) {
237
348
  * @param {string} [options.font='helvetica'] - name of font from which measurements are to be taken
238
349
  * @param {number} [options.size=14] - size of font to be used in taking measurements
239
350
  * @param {number} [options.charSpace=0] - character spacing being applied to the given text.
351
+ * @param {boolean} [options.bold] - Measure with the bold style of the font.
352
+ * @param {boolean} [options.italic] - Measure with the italic style of the font.
240
353
  * @returns {Object} measurement components of given text: width, height, xMin, xMax, yMin, yMax
354
+ * @throws {Error} If the font file cannot be loaded.
241
355
  */
242
356
  exports.textDimensions = function textDimensions(text, options = {}) {
357
+ // null options act like omitted options.
358
+ if (options === null) options = {};
243
359
  const font = this._getFont(options);
244
360
  let dimensions = {};
245
361
  let charSpaces = 0;
@@ -270,6 +386,15 @@ exports.textDimensions = function textDimensions(text, options = {}) {
270
386
  * @param {Object} [options] - The column options.
271
387
  */
272
388
  exports.Column = class Column {
389
+ /**
390
+ * @param {number} x - The x coordinate.
391
+ * @param {number} y - The y coordinate.
392
+ * @param {number} width - The column width.
393
+ * @param {number} [height] - The column height; unlimited when omitted.
394
+ * @param {string} [text=''] - The column heading; defaults to the field.
395
+ * @param {string} [field=''] - The associated data field.
396
+ * @param {Object} [options] - The column options; `cell` becomes the text box.
397
+ */
273
398
  constructor(x, y, width, height, text = "", field = "", options = {}) {
274
399
  this._x = x;
275
400
  this._y = y;
@@ -294,7 +419,10 @@ exports.Column = class Column {
294
419
  if (!this._options.header || typeof this._options.header === "boolean") {
295
420
  this._options.header = {
296
421
  bold: true,
297
- textBox: { padding: 2, textAlign: "center center" },
422
+ textBox: {
423
+ padding: 2,
424
+ textAlign: `${HorizontalAlign.CENTER} ${VerticalAlign.CENTER}`,
425
+ },
298
426
  };
299
427
  }
300
428
 
@@ -303,39 +431,75 @@ exports.Column = class Column {
303
431
  }
304
432
  }
305
433
 
434
+ /**
435
+ * @returns {number} The column width.
436
+ */
306
437
  get width() {
307
438
  return this._width;
308
439
  }
440
+ /**
441
+ * @returns {number} The column height.
442
+ */
309
443
  get height() {
310
444
  return this._height;
311
445
  }
446
+ /**
447
+ * @returns {number} The column x coordinate.
448
+ */
312
449
  get x() {
313
450
  return this._x;
314
451
  }
452
+ /**
453
+ * @param {number} x - The new x coordinate.
454
+ */
315
455
  set x(x) {
316
456
  this._x = x;
317
457
  }
458
+ /**
459
+ * @returns {number} The column y coordinate.
460
+ */
318
461
  get y() {
319
462
  return this._y;
320
463
  }
464
+ /**
465
+ * @returns {number[]} The column [x, y] position.
466
+ */
321
467
  get position() {
322
468
  return [this._x, this._y];
323
469
  }
470
+ /**
471
+ * @param {number[]} pos - The new [x, y] position.
472
+ */
324
473
  set position(pos) {
325
474
  [this._x, this._y] = pos;
326
475
  }
476
+ /**
477
+ * @returns {number} The gap after the column.
478
+ */
327
479
  get gap() {
328
480
  return this._gap;
329
481
  }
482
+ /**
483
+ * @param {number} gap - The new gap after the column.
484
+ */
330
485
  set gap(gap) {
331
486
  this._gap = gap;
332
487
  }
488
+ /**
489
+ * @returns {string} The associated data field.
490
+ */
333
491
  get field() {
334
492
  return this._field;
335
493
  }
494
+ /**
495
+ * @returns {string} The column heading.
496
+ */
336
497
  get text() {
337
498
  return this._text;
338
499
  }
500
+ /**
501
+ * @returns {Object} The column text options.
502
+ */
339
503
  get options() {
340
504
  return this._options;
341
505
  }