@muhammara/native-core 7.0.0-beta.4 → 7.0.0-rc.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +57 -7
- package/THIRD_PARTY_NOTICES.md +39 -0
- package/index.js +120 -1
- package/lib/PDFRStreamForBuffer.js +52 -7
- package/lib/PDFRStreamForFile.js +56 -7
- package/lib/PDFStreamForResponse.js +22 -5
- package/lib/PDFWStreamForBuffer.js +49 -10
- package/lib/PDFWStreamForFile.js +26 -5
- package/lib/Recipe.js +91 -38
- package/lib/content-stream.js +503 -0
- package/lib/css-colors.js +149 -0
- package/lib/font-text.js +1461 -0
- package/lib/glyph-list.js +1127 -0
- package/lib/recipe/annotation.js +464 -101
- package/lib/recipe/appendPage.js +2 -0
- package/lib/recipe/colors.js +132 -35
- package/lib/recipe/coordinate.js +82 -16
- package/lib/recipe/encrypt.js +32 -13
- package/lib/recipe/font.js +70 -25
- package/lib/recipe/graphics-state.js +3 -2
- package/lib/recipe/htmlToTextObjects.js +121 -24
- package/lib/recipe/image.js +385 -39
- package/lib/recipe/info.js +93 -22
- package/lib/recipe/insertPage.js +12 -3
- package/lib/recipe/overlay.js +4 -2
- package/lib/recipe/page-references.js +436 -0
- package/lib/recipe/page.js +363 -262
- package/lib/recipe/parameters.js +8 -1
- package/lib/recipe/replaceText.js +464 -30
- package/lib/recipe/shapes.js +418 -45
- package/lib/recipe/split.js +6 -2
- package/lib/recipe/table.js +41 -8
- package/lib/recipe/text.helper.js +172 -8
- package/lib/recipe/text.js +294 -56
- package/lib/recipe/utils.js +17 -6
- package/lib/recipe/vector-line.js +101 -39
- package/lib/recipe/vector-polygon.js +18 -6
- package/lib/recipe/vector.helper.js +156 -14
- package/lib/recipe/vector.js +156 -52
- package/lib/recipe/xObjectForm.js +73 -10
- package/lib/recipe-constants.js +373 -0
- package/lib/recipe-options.js +47 -0
- package/muhammara.d.ts +3501 -160
- package/package.json +2 -3
package/lib/recipe/text.js
CHANGED
|
@@ -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:
|
|
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
|
|
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'
|
|
210
|
-
* and
|
|
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 {
|
|
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
|
|
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 {
|
|
252
|
-
* @param {
|
|
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
|
-
* @
|
|
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
|
|
446
|
+
case VerticalAlign.CENTER:
|
|
365
447
|
textYpos -=
|
|
366
448
|
(textBox.height - textBox.textHeight) / 2 - textBox.paddingTop;
|
|
367
449
|
break;
|
|
368
|
-
case
|
|
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
|
|
473
|
+
case TextAlign.CENTER:
|
|
393
474
|
offsetX = (textBox.width - currentLineWidth) / 2;
|
|
394
475
|
break;
|
|
395
|
-
case
|
|
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
|
-
.
|
|
417
|
-
|
|
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
|
-
.
|
|
429
|
-
|
|
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 = (
|
|
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 ===
|
|
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 !==
|
|
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 =
|
|
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 !==
|
|
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 !==
|
|
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 ===
|
|
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 ===
|
|
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(
|
|
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(
|
|
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
|
-
/**
|
|
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 =
|
|
963
|
+
textBox.wrap = TextWrap.AUTO;
|
|
835
964
|
} else if (textBox.wrap === false) {
|
|
836
|
-
textBox.wrap =
|
|
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
|
-
|
|
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 (
|
|
945
|
-
if (
|
|
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 (
|
|
953
|
-
if (
|
|
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
|
-
![
|
|
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
|
-
|
|
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 ===
|
|
1417
|
+
if (textBox.wrap === TextWrap.CLIP) {
|
|
1231
1418
|
line.addWord(word);
|
|
1232
|
-
} else if (textBox.wrap ===
|
|
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 ===
|
|
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 !==
|
|
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 ===
|
|
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
|
-
|
|
1631
|
-
this.
|
|
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 || {};
|