@muhammara/native-core 7.0.0-beta.3 → 7.0.0-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +86 -30
- 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 +482 -105
- package/lib/recipe/appendPage.js +8 -1
- 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 +154 -21
- package/lib/recipe/image.js +34 -9
- 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 +379 -275
- package/lib/recipe/parameters.js +11 -2
- 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 +260 -173
- package/lib/recipe/text.helper.js +176 -10
- package/lib/recipe/text.js +420 -115
- package/lib/recipe/utils.js +66 -3
- package/lib/recipe/vector-line.js +101 -39
- package/lib/recipe/vector-polygon.js +18 -6
- package/lib/recipe/vector.helper.js +150 -13
- package/lib/recipe/vector.js +150 -53
- package/lib/recipe/xObjectForm.js +73 -10
- package/lib/recipe-constants.js +373 -0
- package/muhammara.d.ts +3487 -152
- package/package.json +1 -1
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` 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;
|
|
@@ -379,7 +460,6 @@ exports.text = function text(text = "", x, y, options = {}) {
|
|
|
379
460
|
let columnIndex = 1;
|
|
380
461
|
|
|
381
462
|
toWriteTextObjects.some((toWriteTextObject, index) => {
|
|
382
|
-
const isHTML = toWriteTextObject.writeOptions.html;
|
|
383
463
|
const { text, lineHeight, lineWidth, lineID, spaceWidth } =
|
|
384
464
|
toWriteTextObject;
|
|
385
465
|
|
|
@@ -390,10 +470,10 @@ exports.text = function text(text = "", x, y, options = {}) {
|
|
|
390
470
|
let spaceWidth = content.text.endsWith(" ") ? content.spaceWidth : 0;
|
|
391
471
|
let offsetX;
|
|
392
472
|
switch (content.writeOptions.alignHorizontal) {
|
|
393
|
-
case
|
|
473
|
+
case TextAlign.CENTER:
|
|
394
474
|
offsetX = (textBox.width - currentLineWidth) / 2;
|
|
395
475
|
break;
|
|
396
|
-
case
|
|
476
|
+
case TextAlign.RIGHT:
|
|
397
477
|
offsetX =
|
|
398
478
|
textBox.width -
|
|
399
479
|
textBox.paddingRight -
|
|
@@ -413,9 +493,22 @@ exports.text = function text(text = "", x, y, options = {}) {
|
|
|
413
493
|
if (options.underline) {
|
|
414
494
|
const underlineY = y - options.textHeight * 0.1;
|
|
415
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
|
+
}
|
|
416
504
|
ctx
|
|
417
|
-
.
|
|
418
|
-
|
|
505
|
+
.drawPath(
|
|
506
|
+
x,
|
|
507
|
+
underlineY,
|
|
508
|
+
x + width,
|
|
509
|
+
underlineY,
|
|
510
|
+
this._devicePathOptions(options),
|
|
511
|
+
)
|
|
419
512
|
.Q();
|
|
420
513
|
}
|
|
421
514
|
};
|
|
@@ -425,9 +518,22 @@ exports.text = function text(text = "", x, y, options = {}) {
|
|
|
425
518
|
if (options.strikeOut) {
|
|
426
519
|
const strikeOutY = y + options.textHeight * 0.2;
|
|
427
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
|
+
}
|
|
428
529
|
ctx
|
|
429
|
-
.
|
|
430
|
-
|
|
530
|
+
.drawPath(
|
|
531
|
+
x,
|
|
532
|
+
strikeOutY,
|
|
533
|
+
x + width,
|
|
534
|
+
strikeOutY,
|
|
535
|
+
this._devicePathOptions(options),
|
|
536
|
+
)
|
|
431
537
|
.Q();
|
|
432
538
|
}
|
|
433
539
|
};
|
|
@@ -475,7 +581,7 @@ exports.text = function text(text = "", x, y, options = {}) {
|
|
|
475
581
|
}
|
|
476
582
|
};
|
|
477
583
|
|
|
478
|
-
const writeText = (
|
|
584
|
+
const writeText = (x, y, wto) => {
|
|
479
585
|
const options = wto.writeOptions;
|
|
480
586
|
const { lineWidth, lineHeight, text, baseline } = wto;
|
|
481
587
|
let next_x = 0;
|
|
@@ -484,6 +590,9 @@ exports.text = function text(text = "", x, y, options = {}) {
|
|
|
484
590
|
// nothing to write, so simply escape.
|
|
485
591
|
return next_x;
|
|
486
592
|
}
|
|
593
|
+
// The opacity and rotation path below resets x to the text box edge;
|
|
594
|
+
// text-markup annotations need the line's own start.
|
|
595
|
+
const lineX = x;
|
|
487
596
|
|
|
488
597
|
if (options.underline || options.strikeOut) {
|
|
489
598
|
options.lineWidth = lineWidth;
|
|
@@ -498,12 +607,12 @@ exports.text = function text(text = "", x, y, options = {}) {
|
|
|
498
607
|
// The hiliting rectangle cannot use the text box line
|
|
499
608
|
// width when justification is activated because the
|
|
500
609
|
// spaces between words is calculated dynamically.
|
|
501
|
-
if (options.alignHorizontal ===
|
|
610
|
+
if (options.alignHorizontal === TextAlign.JUSTIFY) {
|
|
502
611
|
bxWidth = justify(nx, x, wto, textBox) - x;
|
|
503
612
|
|
|
504
613
|
// Except for 'right' alignment cases, have to consider
|
|
505
614
|
// text on line ending with spaces to tweak box width.
|
|
506
|
-
} else if (options.alignHorizontal !==
|
|
615
|
+
} else if (options.alignHorizontal !== TextAlign.RIGHT) {
|
|
507
616
|
if (text.endsWith(" ")) {
|
|
508
617
|
bxWidth += wto.spaceWidth;
|
|
509
618
|
}
|
|
@@ -519,14 +628,18 @@ exports.text = function text(text = "", x, y, options = {}) {
|
|
|
519
628
|
}
|
|
520
629
|
|
|
521
630
|
// Note that the last line of a text box ignores justification.
|
|
522
|
-
const _justify =
|
|
631
|
+
const _justify =
|
|
632
|
+
options.alignHorizontal === TextAlign.JUSTIFY && !wto.lastLine;
|
|
523
633
|
|
|
524
634
|
// write directly to page when not dealing with opacity, rotation and special colorspace.
|
|
525
635
|
if (
|
|
526
636
|
options.opacity === 1 &&
|
|
527
|
-
options.colorspace !==
|
|
637
|
+
options.colorspace !== Colorspace.SEPARATION &&
|
|
528
638
|
(options.rotation === 0 || options.rotation === undefined)
|
|
529
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;
|
|
530
643
|
context.q();
|
|
531
644
|
|
|
532
645
|
if (_justify) {
|
|
@@ -542,7 +655,7 @@ exports.text = function text(text = "", x, y, options = {}) {
|
|
|
542
655
|
},
|
|
543
656
|
);
|
|
544
657
|
} else {
|
|
545
|
-
if (textBox.wrap !==
|
|
658
|
+
if (textBox.wrap !== TextWrap.AUTO) {
|
|
546
659
|
// This applies a clipping region around the text
|
|
547
660
|
context
|
|
548
661
|
.m(nx, y + lineHeight)
|
|
@@ -607,20 +720,39 @@ exports.text = function text(text = "", x, y, options = {}) {
|
|
|
607
720
|
|
|
608
721
|
const { textHeight } = options;
|
|
609
722
|
|
|
723
|
+
var markupLeft = lineX;
|
|
724
|
+
var markupBottom = y - textHeight * 0.2;
|
|
725
|
+
var markupWidth = _justify ? next_x - lineX : currentLineWidth;
|
|
726
|
+
var markupHeight = textHeight * 1.4;
|
|
727
|
+
if (textBox.wrap === TextWrap.CLIP) {
|
|
728
|
+
// Clipped runs retain the first overflowing word, so measure the
|
|
729
|
+
// drawn text instead of using the preceding fitting line's width.
|
|
730
|
+
var markupRight = Math.min(
|
|
731
|
+
lineX + new Word(text, options).dimensions.xMax,
|
|
732
|
+
nx + textBox.width,
|
|
733
|
+
);
|
|
734
|
+
var markupTop = Math.min(markupBottom + markupHeight, y + lineHeight);
|
|
735
|
+
markupLeft = Math.max(markupLeft, nx);
|
|
736
|
+
markupBottom = Math.max(markupBottom, y);
|
|
737
|
+
markupWidth = markupRight - markupLeft;
|
|
738
|
+
markupHeight = markupTop - markupBottom;
|
|
739
|
+
if (markupWidth <= 0 || markupHeight <= 0) return next_x;
|
|
740
|
+
}
|
|
741
|
+
|
|
610
742
|
for (let key in targetAnnotations) {
|
|
611
743
|
const subtype = this._getTextMarkupAnnotationSubtype(key);
|
|
612
|
-
if (subtype) {
|
|
744
|
+
if (subtype && targetAnnotations[key]) {
|
|
745
|
+
// Copy so the caller's markup options are not modified.
|
|
613
746
|
const markupOption =
|
|
614
747
|
typeof targetAnnotations[key] != "object"
|
|
615
748
|
? {}
|
|
616
|
-
: targetAnnotations[key];
|
|
749
|
+
: { ...targetAnnotations[key] };
|
|
617
750
|
const { title, open, richText, flag, icon, date, subject } =
|
|
618
751
|
targetAnnotations;
|
|
619
752
|
Object.assign(markupOption, {
|
|
620
|
-
height:
|
|
621
|
-
width:
|
|
753
|
+
height: markupHeight,
|
|
754
|
+
width: markupWidth,
|
|
622
755
|
text: markupOption.text || "",
|
|
623
|
-
_textHeight: textHeight,
|
|
624
756
|
// add options to annotation
|
|
625
757
|
title: title || "",
|
|
626
758
|
open: Boolean(open),
|
|
@@ -630,7 +762,11 @@ exports.text = function text(text = "", x, y, options = {}) {
|
|
|
630
762
|
date: date || "",
|
|
631
763
|
subject: subject || "",
|
|
632
764
|
});
|
|
633
|
-
|
|
765
|
+
// annot() takes the rectangle's top-left corner.
|
|
766
|
+
const { ox, oy } = this._reverseCoordinate(
|
|
767
|
+
markupLeft,
|
|
768
|
+
markupBottom + markupHeight,
|
|
769
|
+
);
|
|
634
770
|
|
|
635
771
|
this.annot(ox, oy, subtype, markupOption);
|
|
636
772
|
}
|
|
@@ -639,22 +775,37 @@ exports.text = function text(text = "", x, y, options = {}) {
|
|
|
639
775
|
return next_x;
|
|
640
776
|
};
|
|
641
777
|
|
|
778
|
+
/** Queues a text link limited to the run's visible clipping region. */
|
|
779
|
+
var queueTextLink = function (content, x, y, nextX) {
|
|
780
|
+
if (!content.writeOptions.link) return;
|
|
781
|
+
var left = x;
|
|
782
|
+
var width = nextX ? nextX - x : content.lineWidth;
|
|
783
|
+
if (textBox.wrap === TextWrap.CLIP) {
|
|
784
|
+
var right = Math.min(
|
|
785
|
+
x + new Word(content.text, content.writeOptions).dimensions.xMax,
|
|
786
|
+
nx + textBox.width,
|
|
787
|
+
);
|
|
788
|
+
left = Math.max(left, nx);
|
|
789
|
+
width = right - left;
|
|
790
|
+
if (width <= 0) return;
|
|
791
|
+
}
|
|
792
|
+
linkAnnotations.push({
|
|
793
|
+
url: content.writeOptions.link,
|
|
794
|
+
left,
|
|
795
|
+
bottom: y,
|
|
796
|
+
width,
|
|
797
|
+
height: content.lineHeight,
|
|
798
|
+
});
|
|
799
|
+
};
|
|
800
|
+
|
|
642
801
|
if (!isContinued) {
|
|
643
802
|
// flush out current line before processing next one
|
|
644
803
|
let next_x = 0;
|
|
645
804
|
toWriteContents.forEach((content) => {
|
|
646
805
|
const x = next_x || getStartX(content.startX, content);
|
|
647
806
|
const y = currentY;
|
|
648
|
-
next_x = writeText(
|
|
649
|
-
|
|
650
|
-
linkAnnotations.push({
|
|
651
|
-
url: content.writeOptions.link,
|
|
652
|
-
left: x,
|
|
653
|
-
bottom: y,
|
|
654
|
-
width: next_x ? next_x - x : content.lineWidth,
|
|
655
|
-
height: content.lineHeight,
|
|
656
|
-
});
|
|
657
|
-
}
|
|
807
|
+
next_x = writeText(x, y, content);
|
|
808
|
+
queueTextLink(content, x, y, next_x);
|
|
658
809
|
});
|
|
659
810
|
// The line offset from the last line in the
|
|
660
811
|
// group determines Y positioning for next line.
|
|
@@ -683,6 +834,7 @@ exports.text = function text(text = "", x, y, options = {}) {
|
|
|
683
834
|
}
|
|
684
835
|
|
|
685
836
|
if (overflow && this._overflowNotifier) {
|
|
837
|
+
flushTextLinks(this, linkAnnotations);
|
|
686
838
|
let orders = this._overflowNotifier(this);
|
|
687
839
|
if (orders === true) {
|
|
688
840
|
return true; // stop processing remaining text.
|
|
@@ -710,7 +862,6 @@ exports.text = function text(text = "", x, y, options = {}) {
|
|
|
710
862
|
updateTextBox(this._columns[columnIndex - 1]);
|
|
711
863
|
updateVertical = false;
|
|
712
864
|
}
|
|
713
|
-
context = this.pageContext; // in case user changed page
|
|
714
865
|
}
|
|
715
866
|
|
|
716
867
|
if (updateVertical) {
|
|
@@ -723,18 +874,16 @@ exports.text = function text(text = "", x, y, options = {}) {
|
|
|
723
874
|
|
|
724
875
|
const startX = nx + currentLineWidth;
|
|
725
876
|
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
if (text != "[@@DONOT_RENDER_THIS@@]") {
|
|
729
|
-
toWriteTextObject.startX = toWriteTextObject.startX || startX;
|
|
730
|
-
toWriteContents.push(toWriteTextObject);
|
|
731
|
-
}
|
|
877
|
+
toWriteTextObject.startX = toWriteTextObject.startX || startX;
|
|
878
|
+
toWriteContents.push(toWriteTextObject);
|
|
732
879
|
|
|
733
880
|
// To handle text that has been split in middle of word,
|
|
734
|
-
// need to decide if current text ends with a space.
|
|
881
|
+
// need to decide if current text ends with a space. HTML segments carry
|
|
882
|
+
// their own separating space, so adding one unconditionally in HTML mode
|
|
883
|
+
// measured every line a space wider than it is drawn and pushed aligned
|
|
884
|
+
// lines left.
|
|
735
885
|
currentLineWidth +=
|
|
736
|
-
lineWidth +
|
|
737
|
-
(isHTML || toWriteTextObject.text.endsWith(" ") ? spaceWidth : 0);
|
|
886
|
+
lineWidth + (toWriteTextObject.text.endsWith(" ") ? spaceWidth : 0);
|
|
738
887
|
|
|
739
888
|
// Processing last text object?
|
|
740
889
|
if (index === toWriteTextObjects.length - 1) {
|
|
@@ -752,16 +901,8 @@ exports.text = function text(text = "", x, y, options = {}) {
|
|
|
752
901
|
const content = toWriteContents[ii];
|
|
753
902
|
const x = next_x || getStartX(content.startX, content);
|
|
754
903
|
const y = currentY;
|
|
755
|
-
next_x = writeText(
|
|
756
|
-
|
|
757
|
-
linkAnnotations.push({
|
|
758
|
-
url: content.writeOptions.link,
|
|
759
|
-
left: x,
|
|
760
|
-
bottom: y,
|
|
761
|
-
width: next_x ? next_x - x : content.lineWidth,
|
|
762
|
-
height: content.lineHeight,
|
|
763
|
-
});
|
|
764
|
-
}
|
|
904
|
+
next_x = writeText(x, y, content);
|
|
905
|
+
queueTextLink(content, x, y, next_x);
|
|
765
906
|
}
|
|
766
907
|
|
|
767
908
|
// Flush any left over text objects.
|
|
@@ -772,31 +913,56 @@ exports.text = function text(text = "", x, y, options = {}) {
|
|
|
772
913
|
});
|
|
773
914
|
|
|
774
915
|
if (clipResult && typeof textBox.onClip === "function") {
|
|
775
|
-
// The
|
|
916
|
+
// The callback may finish the page or start drawing on another one.
|
|
917
|
+
flushTextLinks(this, linkAnnotations);
|
|
776
918
|
textBox.onClip(this, clipResult);
|
|
777
919
|
}
|
|
778
920
|
}
|
|
779
921
|
|
|
780
|
-
linkAnnotations
|
|
922
|
+
flushTextLinks(this, linkAnnotations);
|
|
923
|
+
return this;
|
|
924
|
+
};
|
|
925
|
+
|
|
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
|
+
*/
|
|
934
|
+
function flushTextLinks(recipe, annotations) {
|
|
935
|
+
for (var annotation of annotations.splice(0)) {
|
|
781
936
|
linkPdf(
|
|
782
|
-
|
|
937
|
+
recipe,
|
|
783
938
|
annotation.url,
|
|
784
939
|
annotation.left,
|
|
785
940
|
annotation.bottom,
|
|
786
941
|
annotation.width,
|
|
787
942
|
annotation.height,
|
|
788
943
|
);
|
|
789
|
-
}
|
|
790
|
-
|
|
791
|
-
};
|
|
944
|
+
}
|
|
945
|
+
}
|
|
792
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
|
+
*/
|
|
793
959
|
exports._layoutText = function _layoutText(textObjects, textBox, pathOptions) {
|
|
794
960
|
let totalHeight = 0;
|
|
795
961
|
// allow user to treat wrap as boolean
|
|
796
962
|
if (textBox.wrap === true) {
|
|
797
|
-
textBox.wrap =
|
|
963
|
+
textBox.wrap = TextWrap.AUTO;
|
|
798
964
|
} else if (textBox.wrap === false) {
|
|
799
|
-
textBox.wrap =
|
|
965
|
+
textBox.wrap = TextWrap.ELLIPSIS;
|
|
800
966
|
}
|
|
801
967
|
|
|
802
968
|
// Allows user to enter a single number which will be used for all text box sides,
|
|
@@ -831,19 +997,52 @@ exports._layoutText = function _layoutText(textObjects, textBox, pathOptions) {
|
|
|
831
997
|
|
|
832
998
|
let firstLineHeight;
|
|
833
999
|
let toWriteTextObjects = [];
|
|
834
|
-
/** Reports whether a layout node contains
|
|
1000
|
+
/** Reports whether a layout node contains text, not only line breaks. */
|
|
835
1001
|
const hasRenderableContent = (textObject) =>
|
|
836
1002
|
(textObject.value !== undefined &&
|
|
837
1003
|
textObject.value !== null &&
|
|
838
|
-
textObject.value !== ""
|
|
839
|
-
textObject.value !== "[@@DONOT_RENDER_THIS@@]") ||
|
|
1004
|
+
textObject.value !== "") ||
|
|
840
1005
|
textObject.childs?.some(hasRenderableContent);
|
|
841
1006
|
|
|
1007
|
+
/** Adds laid-out objects, recording the first line height. */
|
|
1008
|
+
const addLaidOutObjects = (newToWriteObjects, paragraphHeight) => {
|
|
1009
|
+
toWriteTextObjects = [...toWriteTextObjects, ...newToWriteObjects];
|
|
1010
|
+
|
|
1011
|
+
if (!firstLineHeight) {
|
|
1012
|
+
this._lineHeight = firstLineHeight = toWriteTextObjects[0].lineHeight;
|
|
1013
|
+
if (!this._firstLineHeight) {
|
|
1014
|
+
this._firstLineHeight = this._lineHeight; // used in textbox coordinate computation
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
totalHeight += paragraphHeight;
|
|
1018
|
+
};
|
|
1019
|
+
|
|
842
1020
|
const writeValue = (textObject) => {
|
|
843
1021
|
textObject.lineID = textObject.lineID || Date.now() * Math.random();
|
|
844
|
-
textObject.lineID =
|
|
845
|
-
|
|
846
|
-
|
|
1022
|
+
textObject.lineID =
|
|
1023
|
+
textObject.needsLineBreaker || textObject.lineBreak
|
|
1024
|
+
? Date.now() * Math.random()
|
|
1025
|
+
: textObject.lineID;
|
|
1026
|
+
if (textObject.lineBreak) {
|
|
1027
|
+
// A line break lays out as one empty line sized like its text; the
|
|
1028
|
+
// normalization below turns it into line state, never into text.
|
|
1029
|
+
const { toWriteTextObjects: newToWriteObjects, paragraphHeight } =
|
|
1030
|
+
makeTextObjects(
|
|
1031
|
+
this,
|
|
1032
|
+
Object.assign({}, textObject, { value: "" }),
|
|
1033
|
+
pathOptions,
|
|
1034
|
+
textBox,
|
|
1035
|
+
);
|
|
1036
|
+
const lineBreak = newToWriteObjects[newToWriteObjects.length - 1];
|
|
1037
|
+
lineBreak.lineBreak = true;
|
|
1038
|
+
// Flowed text marks the last word of the previous line, so an empty
|
|
1039
|
+
// break line still carries one empty word.
|
|
1040
|
+
if (!lineBreak.wordsInLine.length) {
|
|
1041
|
+
lineBreak.wordsInLine.push(new Word("", pathOptions));
|
|
1042
|
+
}
|
|
1043
|
+
addLaidOutObjects(newToWriteObjects, paragraphHeight);
|
|
1044
|
+
return;
|
|
1045
|
+
}
|
|
847
1046
|
// Want to allow empty string to pass through. Undefined and null elements, stay out!
|
|
848
1047
|
if (textObject.value !== undefined && textObject.value !== null) {
|
|
849
1048
|
textObject.styles.color = textObject.styles.color
|
|
@@ -851,16 +1050,7 @@ exports._layoutText = function _layoutText(textObjects, textBox, pathOptions) {
|
|
|
851
1050
|
: pathOptions.color;
|
|
852
1051
|
const { toWriteTextObjects: newToWriteObjects, paragraphHeight } =
|
|
853
1052
|
makeTextObjects(this, textObject, pathOptions, textBox);
|
|
854
|
-
|
|
855
|
-
toWriteTextObjects = [...toWriteTextObjects, ...newToWriteObjects];
|
|
856
|
-
|
|
857
|
-
if (!firstLineHeight) {
|
|
858
|
-
this._lineHeight = firstLineHeight = toWriteTextObjects[0].lineHeight;
|
|
859
|
-
if (!this._firstLineHeight) {
|
|
860
|
-
this._firstLineHeight = this._lineHeight; // used in textbox coordinate computation
|
|
861
|
-
}
|
|
862
|
-
}
|
|
863
|
-
totalHeight += paragraphHeight;
|
|
1053
|
+
addLaidOutObjects(newToWriteObjects, paragraphHeight);
|
|
864
1054
|
}
|
|
865
1055
|
if (textObject.tag && textObject.childs.length) {
|
|
866
1056
|
// console.log(textObject);
|
|
@@ -874,34 +1064,36 @@ exports._layoutText = function _layoutText(textObjects, textBox, pathOptions) {
|
|
|
874
1064
|
textObject.currentIndex = 0;
|
|
875
1065
|
let prependValue = textObject.prependValue;
|
|
876
1066
|
|
|
1067
|
+
const tag = (textObject.tag || "").toLowerCase();
|
|
877
1068
|
textObject.childs.forEach((child) => {
|
|
878
|
-
|
|
1069
|
+
const childTag = (child.tag || "").toLowerCase();
|
|
1070
|
+
if (tag === HtmlTag.UL) {
|
|
879
1071
|
child.prependValue = "* ";
|
|
880
1072
|
child.layer = textObject.layer + 1;
|
|
881
1073
|
// child.indent = 4 * child.layer;
|
|
882
1074
|
}
|
|
883
|
-
if (
|
|
884
|
-
if (
|
|
1075
|
+
if (tag === HtmlTag.OL) {
|
|
1076
|
+
if (childTag !== HtmlTag.OL) {
|
|
885
1077
|
textObject.currentIndex++;
|
|
886
1078
|
child.prependValue = `${textObject.currentIndex.toString()}. `;
|
|
887
1079
|
}
|
|
888
1080
|
child.layer = textObject.layer + 1;
|
|
889
1081
|
// child.indent = 4 * child.layer;
|
|
890
1082
|
}
|
|
891
|
-
if (
|
|
892
|
-
if (
|
|
1083
|
+
if (tag === HtmlTag.LI) {
|
|
1084
|
+
if (childTag === HtmlTag.OL || childTag === HtmlTag.UL) {
|
|
893
1085
|
child.layer = textObject.layer - 1;
|
|
894
1086
|
}
|
|
895
1087
|
}
|
|
896
1088
|
if (
|
|
897
1089
|
prependValue &&
|
|
898
|
-
![
|
|
1090
|
+
![HtmlTag.OL, HtmlTag.UL].includes(childTag) &&
|
|
899
1091
|
hasRenderableContent(child)
|
|
900
1092
|
) {
|
|
901
1093
|
child.prependValue = prependValue;
|
|
902
1094
|
prependValue = null;
|
|
903
1095
|
textObject.indent =
|
|
904
|
-
|
|
1096
|
+
tag === HtmlTag.LI
|
|
905
1097
|
? 2 * textObject.layer
|
|
906
1098
|
: textObject.indent || 2 * textObject.layer;
|
|
907
1099
|
}
|
|
@@ -931,14 +1123,18 @@ exports._layoutText = function _layoutText(textObjects, textBox, pathOptions) {
|
|
|
931
1123
|
});
|
|
932
1124
|
}
|
|
933
1125
|
};
|
|
1126
|
+
// Top-level nodes share a line, as children of a block element do, so
|
|
1127
|
+
// inline runs outside any element are not split onto separate lines.
|
|
1128
|
+
const topLevelLineID = Date.now() * Math.random();
|
|
934
1129
|
textObjects.forEach((textObject) => {
|
|
1130
|
+
textObject.lineID = textObject.lineID || topLevelLineID;
|
|
935
1131
|
writeValue(textObject);
|
|
936
1132
|
});
|
|
937
1133
|
|
|
938
1134
|
const normalizedTextObjects = [];
|
|
939
1135
|
let pendingBreaks = [];
|
|
940
1136
|
const replacementLineIDs = new Map();
|
|
941
|
-
/** Converts pending
|
|
1137
|
+
/** Converts pending line breaks into line state: blank lines and line IDs. */
|
|
942
1138
|
const appendPendingBreaks = (nextTextObject) => {
|
|
943
1139
|
const previous = normalizedTextObjects[normalizedTextObjects.length - 1];
|
|
944
1140
|
if (previous) previous.lineComplete = true;
|
|
@@ -963,12 +1159,13 @@ exports._layoutText = function _layoutText(textObjects, textBox, pathOptions) {
|
|
|
963
1159
|
pendingBreaks = [];
|
|
964
1160
|
};
|
|
965
1161
|
toWriteTextObjects.forEach((textObject, index, objects) => {
|
|
966
|
-
|
|
967
|
-
const
|
|
1162
|
+
// Whitespace that only precedes a line break would start a blank line.
|
|
1163
|
+
const beforeLineBreak =
|
|
1164
|
+
!textObject.lineBreak &&
|
|
968
1165
|
textObject.text.trim() == "" &&
|
|
969
|
-
objects[index + 1]?.
|
|
970
|
-
if (
|
|
971
|
-
if (
|
|
1166
|
+
objects[index + 1]?.lineBreak;
|
|
1167
|
+
if (beforeLineBreak) return;
|
|
1168
|
+
if (textObject.lineBreak) {
|
|
972
1169
|
pendingBreaks.push(textObject);
|
|
973
1170
|
return;
|
|
974
1171
|
}
|
|
@@ -986,6 +1183,13 @@ exports._layoutText = function _layoutText(textObjects, textBox, pathOptions) {
|
|
|
986
1183
|
};
|
|
987
1184
|
};
|
|
988
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
|
+
*/
|
|
989
1193
|
function getTextBoxHeight(textObjs) {
|
|
990
1194
|
let previousLineID;
|
|
991
1195
|
let height = 0;
|
|
@@ -1009,6 +1213,15 @@ function getTextBoxHeight(textObjs) {
|
|
|
1009
1213
|
return height;
|
|
1010
1214
|
}
|
|
1011
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
|
+
*/
|
|
1012
1225
|
function clipTextToBox(textObjs, availableHeight) {
|
|
1013
1226
|
const lines = [];
|
|
1014
1227
|
let line = [];
|
|
@@ -1053,6 +1266,15 @@ function clipTextToBox(textObjs, availableHeight) {
|
|
|
1053
1266
|
};
|
|
1054
1267
|
}
|
|
1055
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
|
+
*/
|
|
1056
1278
|
function getTextBoxPosition(self, textBox, pathOptions) {
|
|
1057
1279
|
const { offsetX, offsetY } = self._getTextBoxOffset(textBox, pathOptions);
|
|
1058
1280
|
const { nx, ny } = self._calibrateCoordinate(
|
|
@@ -1064,6 +1286,16 @@ function getTextBoxPosition(self, textBox, pathOptions) {
|
|
|
1064
1286
|
return [nx, ny];
|
|
1065
1287
|
}
|
|
1066
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
|
+
*/
|
|
1067
1299
|
function drawTextBox(self, nx, ny, textBox, pathOptions) {
|
|
1068
1300
|
const textBoxWidth = textBox.width; //+ textBox.paddingLeft + textBox.paddingRight;
|
|
1069
1301
|
let borderRadius = textBox.style ? textBox.style.borderRadius : 0;
|
|
@@ -1093,6 +1325,7 @@ function drawTextBox(self, nx, ny, textBox, pathOptions) {
|
|
|
1093
1325
|
* @param {Object[]} wto is a write object
|
|
1094
1326
|
* @param {Object} textBox holds text box properties
|
|
1095
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.
|
|
1096
1329
|
*/
|
|
1097
1330
|
function justify(left, x, wto, textBox, position) {
|
|
1098
1331
|
// For some reason, textWidth is smaller than lineWidth. My suspicions lie in the fact
|
|
@@ -1149,6 +1382,15 @@ function justify(left, x, wto, textBox, position) {
|
|
|
1149
1382
|
return word.value.endsWith(" ") ? x : x - spaceBetweenWords;
|
|
1150
1383
|
}
|
|
1151
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
|
+
*/
|
|
1152
1394
|
function nextWord(text, brk, previousPosition, pathOptions) {
|
|
1153
1395
|
let nextWord = text.slice(previousPosition, brk.position);
|
|
1154
1396
|
|
|
@@ -1160,10 +1402,21 @@ function nextWord(text, brk, previousPosition, pathOptions) {
|
|
|
1160
1402
|
return new Word(nextWord, pathOptions);
|
|
1161
1403
|
}
|
|
1162
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
|
+
*/
|
|
1163
1416
|
function elideNonFittingText(textBox, line, word, pathOptions) {
|
|
1164
|
-
if (textBox.wrap ===
|
|
1417
|
+
if (textBox.wrap === TextWrap.CLIP) {
|
|
1165
1418
|
line.addWord(word);
|
|
1166
|
-
} else if (textBox.wrap ===
|
|
1419
|
+
} else if (textBox.wrap === TextWrap.ELLIPSIS) {
|
|
1167
1420
|
// This is more complicated than the other no-wrap options.
|
|
1168
1421
|
// It makes an initial attempt to take the word that was
|
|
1169
1422
|
// too big and make it shrink in size until it and the
|
|
@@ -1194,6 +1447,17 @@ function elideNonFittingText(textBox, line, word, pathOptions) {
|
|
|
1194
1447
|
}
|
|
1195
1448
|
}
|
|
1196
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
|
+
*/
|
|
1197
1461
|
function makeTextObject(lines, line, lineID, textBox, options = {}) {
|
|
1198
1462
|
const lineHeight = line.height;
|
|
1199
1463
|
const spaceSz =
|
|
@@ -1230,6 +1494,15 @@ function makeTextObject(lines, line, lineID, textBox, options = {}) {
|
|
|
1230
1494
|
};
|
|
1231
1495
|
}
|
|
1232
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
|
+
*/
|
|
1233
1506
|
function bindTextToLine(line, textObjects, wordCount, totalTextWidth) {
|
|
1234
1507
|
// Apply justification information to previous text objects.
|
|
1235
1508
|
if (wordCount > 0) {
|
|
@@ -1267,6 +1540,18 @@ function bindTextToLine(line, textObjects, wordCount, totalTextWidth) {
|
|
|
1267
1540
|
return [wordCount, totalTextWidth];
|
|
1268
1541
|
}
|
|
1269
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
|
+
*/
|
|
1270
1555
|
function makeTextObjects(self, textObject = {}, pathOptions, textBox = {}) {
|
|
1271
1556
|
const toWriteTextObjects = [...self._previousTextObjects];
|
|
1272
1557
|
let text =
|
|
@@ -1297,7 +1582,8 @@ function makeTextObjects(self, textObject = {}, pathOptions, textBox = {}) {
|
|
|
1297
1582
|
opacity: parseFloat(textObject.styles.opacity || pathOptions.opacity || 1),
|
|
1298
1583
|
underline: textObject.underline || pathOptions.underline,
|
|
1299
1584
|
strikeOut: textObject.strikeOut || pathOptions.strikeOut,
|
|
1300
|
-
size
|
|
1585
|
+
// Text outside any HTML element has no inherited size of its own.
|
|
1586
|
+
size: size,
|
|
1301
1587
|
alignHorizontal: alignHorizontal,
|
|
1302
1588
|
alignVertical: alignVertical,
|
|
1303
1589
|
font: self._getFont(textObject),
|
|
@@ -1339,7 +1625,7 @@ function makeTextObjects(self, textObject = {}, pathOptions, textBox = {}) {
|
|
|
1339
1625
|
const previousLine = toWriteTextObjects[end];
|
|
1340
1626
|
let lineComplete, fini;
|
|
1341
1627
|
|
|
1342
|
-
if (text === "" && !self._flow) {
|
|
1628
|
+
if (text === "" && !self._flow && !textObject.lineBreak) {
|
|
1343
1629
|
// turning off flow with empty text so
|
|
1344
1630
|
previousLine.lastLine = true; // need to make previous line, the last.
|
|
1345
1631
|
}
|
|
@@ -1396,7 +1682,7 @@ function makeTextObjects(self, textObject = {}, pathOptions, textBox = {}) {
|
|
|
1396
1682
|
}
|
|
1397
1683
|
} else {
|
|
1398
1684
|
// remove any trailing space on previous word so right justification works appropriately
|
|
1399
|
-
if (previousWord && textBox.wrap ===
|
|
1685
|
+
if (previousWord && textBox.wrap === TextWrap.AUTO) {
|
|
1400
1686
|
newLine.replaceLastWord(previousWord.value.trim());
|
|
1401
1687
|
}
|
|
1402
1688
|
|
|
@@ -1425,7 +1711,7 @@ function makeTextObjects(self, textObject = {}, pathOptions, textBox = {}) {
|
|
|
1425
1711
|
}
|
|
1426
1712
|
|
|
1427
1713
|
// now deal with text line wrap (what happens to text that doesn't fit in line)
|
|
1428
|
-
if (textBox.wrap !==
|
|
1714
|
+
if (textBox.wrap !== TextWrap.AUTO) {
|
|
1429
1715
|
flushLine = true;
|
|
1430
1716
|
elideNonFittingText(textBox, newLine, word, pathOptions);
|
|
1431
1717
|
|
|
@@ -1499,7 +1785,7 @@ function makeTextObjects(self, textObject = {}, pathOptions, textBox = {}) {
|
|
|
1499
1785
|
}
|
|
1500
1786
|
}
|
|
1501
1787
|
|
|
1502
|
-
let isLastLine = alignHorizontal ===
|
|
1788
|
+
let isLastLine = alignHorizontal === TextAlign.JUSTIFY && !self._flow;
|
|
1503
1789
|
|
|
1504
1790
|
if (!flushLine) {
|
|
1505
1791
|
[wordCount, totalTextWidth] = bindTextToLine(
|
|
@@ -1535,6 +1821,14 @@ function makeTextObjects(self, textObject = {}, pathOptions, textBox = {}) {
|
|
|
1535
1821
|
};
|
|
1536
1822
|
}
|
|
1537
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
|
+
*/
|
|
1538
1832
|
function markLineComplete(toWriteTextObjects, lines = null) {
|
|
1539
1833
|
// Get last element in text objects and mark it.
|
|
1540
1834
|
const textObj = toWriteTextObjects[toWriteTextObjects.length - 1];
|
|
@@ -1560,8 +1854,9 @@ function markLineComplete(toWriteTextObjects, lines = null) {
|
|
|
1560
1854
|
exports.movedown = function movedown(lines = 1, returnCoords = false) {
|
|
1561
1855
|
if (!this._flow || this._previousTextObjects.length === 0) {
|
|
1562
1856
|
this._previousTextObjects = [];
|
|
1563
|
-
|
|
1564
|
-
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;
|
|
1565
1860
|
} else {
|
|
1566
1861
|
// This handles continuous text positioning
|
|
1567
1862
|
markLineComplete(this._previousTextObjects, lines);
|
|
@@ -1572,6 +1867,15 @@ exports.movedown = function movedown(lines = 1, returnCoords = false) {
|
|
|
1572
1867
|
return returnCoords ? [this.x, this.y] : this;
|
|
1573
1868
|
};
|
|
1574
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
|
+
*/
|
|
1575
1879
|
function adjustcolumnPosition(columns, x, y) {
|
|
1576
1880
|
const ydiff = y - columns[0].y;
|
|
1577
1881
|
for (const column of columns) {
|
|
@@ -1586,16 +1890,17 @@ function adjustcolumnPosition(columns, x, y) {
|
|
|
1586
1890
|
* @function
|
|
1587
1891
|
* @memberof Recipe#
|
|
1588
1892
|
* @param {number|string} id - The identifier to be associated with the layout. (See 'text' layout option)
|
|
1589
|
-
* @param {number} x - The coordinate x used to position text columns on page. When zero, left margin used.
|
|
1590
|
-
* @param {number} y - The coordinate y used to position text columns on page. When zero, top margin used.
|
|
1591
|
-
* @param {number} width - The width of a text column. When zero, space between left and right margin used.
|
|
1592
|
-
* @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.
|
|
1593
1897
|
* @param {object} [options] - The options.
|
|
1594
1898
|
* @param {number} [options.columns] - Represents the number of columns in which to divide the given width.
|
|
1595
1899
|
* @param {number} [options.gap=18] - Defines the separation between layout columns, units in points.
|
|
1596
1900
|
* @param {boolean} [options.reset] - True indicates that the a new layout should be produced for the given
|
|
1597
1901
|
* layout id, so any previous layout associated with the given id will be lost.
|
|
1598
1902
|
* @returns {Recipe} The recipe instance.
|
|
1903
|
+
* @throws {TypeError} If width or height is omitted while no page is active.
|
|
1599
1904
|
*/
|
|
1600
1905
|
exports.layout = function layout(id, x, y, width, height, options = {}) {
|
|
1601
1906
|
this._layouts = this._layouts || {};
|