diff --git a/CHANGELOG.md b/CHANGELOG.md index 7bab8503..4e8cc7d5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ### Unreleased +- Fix text with `horizontalScaling` being wrapped, indented, aligned and laid out in columns as if the `width`, `indent` and `columnGap` were scaled too, so scaled text no longer runs past the right edge or stops short of it, and fix `align: 'justify'`, the decorations and `boundsOfString()` counting the word spacing of scaled text at 100% - Fix wrapped text with `align: 'center'` sitting left of the middle of the line when `characterSpacing` or `wordSpacing` is set, and with `align: 'right'` running past the right edge by the `wordSpacing` of each gap between words - Fix `boundsOfString()` with `lineBreak: false` leaving out `wordSpacing`, so its width now matches the rendered text and its underline - Fix the underline, strike, link and goTo of wrapped text, and `boundsOfString()` with a `width`, counting `characterSpacing` and `wordSpacing` twice, so they no longer run past the text and match the same text drawn with `lineBreak: false` diff --git a/lib/line_wrapper.js b/lib/line_wrapper.js index ee2f26bf..54790408 100644 --- a/lib/line_wrapper.js +++ b/lib/line_wrapper.js @@ -10,21 +10,16 @@ class LineWrapper extends EventEmitter { super(); this.document = document; this.horizontalScaling = options.horizontalScaling || 100; - this.indent = ((options.indent || 0) * this.horizontalScaling) / 100; + this.indent = options.indent || 0; this.indentAllLines = options.indentAllLines || false; this.characterSpacing = ((options.characterSpacing || 0) * this.horizontalScaling) / 100; this.wordSpacing = ((options.wordSpacing === 0) * this.horizontalScaling) / 100; this.columns = options.columns || 1; - this.columnGap = - ((options.columnGap != null ? options.columnGap : 18) * - this.horizontalScaling) / - 100; // 1/4 inch + this.columnGap = options.columnGap != null ? options.columnGap : 18; // 1/4 inch this.lineWidth = - ((options.width * this.horizontalScaling) / 100 - - this.columnGap * (this.columns - 1)) / - this.columns; + (options.width - this.columnGap * (this.columns - 1)) / this.columns; this.spaceLeft = this.lineWidth; this.startX = this.document.x; this.startY = this.document.y; @@ -207,7 +202,7 @@ class LineWrapper extends EventEmitter { // override options from previous continued fragments this.horizontalScaling = options.horizontalScaling || 100; if (options.indent != null) { - this.indent = (options.indent * this.horizontalScaling) / 100; + this.indent = options.indent; } if (options.characterSpacing != null) { this.characterSpacing = diff --git a/lib/mixins/text.js b/lib/mixins/text.js index 70062e13..d02fe396 100644 --- a/lib/mixins/text.js +++ b/lib/mixins/text.js @@ -135,6 +135,7 @@ export default { ({ x, y } = this); const lineGap = options.lineGap ?? this._lineGap ?? 0; const lineHeight = this.currentLineHeight(true) + lineGap; + const horizontalScaling = options.horizontalScaling || 100; let contentWidth = 0; // Convert text to a string string = String(string ?? ''); @@ -167,7 +168,9 @@ export default { const spaceWidth = this.widthOfString(' ') + characterSpacing; wordSpacing = Math.max( 0, - (options.lineWidth - textWidth) / Math.max(1, words.length - 1) - + ((options.lineWidth - textWidth) / + Math.max(1, words.length - 1)) * + (100 / horizontalScaling) - spaceWidth, ); } @@ -176,7 +179,7 @@ export default { contentWidth = Math.max( contentWidth, this.widthOfString(text, options) + - wordSpacing * (options.wordCount - 1), + (wordSpacing * (options.wordCount - 1) * horizontalScaling) / 100, ); } }); @@ -189,7 +192,9 @@ export default { // whitespace runs were collapsed above, so each gap is one character const trimmed = line.trim(); const space = /\s/g; - while (space.test(trimmed)) lineWidth += options.wordSpacing; + while (space.test(trimmed)) { + lineWidth += (options.wordSpacing * horizontalScaling) / 100; + } } this.y += lineHeight; contentWidth = Math.max(contentWidth, lineWidth); @@ -503,9 +508,11 @@ export default { words = text.trim().split(/\s+/); textWidth = this.widthOfString(text.replace(/\s+/g, ''), options); var spaceWidth = this.widthOfString(' ') + characterSpacing; + // the gap is in page units, but word spacing is scaled by Tz wordSpacing = Math.max( 0, - (options.lineWidth - textWidth) / Math.max(1, words.length - 1) - + ((options.lineWidth - textWidth) / Math.max(1, words.length - 1)) * + (100 / horizontalScaling) - spaceWidth, ); break; @@ -554,7 +561,7 @@ export default { options.wordCount ?? (text.trim() ? text.trim().split(/\s+/).length : 0); const renderedWidth = measuredWidth + - wordSpacing * (measuredWordCount - 1) + + (wordSpacing * (measuredWordCount - 1) * horizontalScaling) / 100 + characterSpacing * (text.length - 1); // create link annotations if the link option is given diff --git a/tests/unit/text.spec.js b/tests/unit/text.spec.js index cd5df975..1f5ce531 100644 --- a/tests/unit/text.spec.js +++ b/tests/unit/text.spec.js @@ -268,6 +268,9 @@ Q ['center', { characterSpacing: 4 }, '182.628', '317.372'], ['center', { wordSpacing: 5 }, '205.628', '294.372'], ['right', { wordSpacing: 5 }, '361.256', '450'], + ['right', { horizontalScaling: 80 }, '387.00', '450'], + ['center', { horizontalScaling: 120 }, '202.75', '297.24'], + ['right', { horizontalScaling: 120, wordSpacing: 5 }, '343.50', '450'], ])('%s aligned text with %o', (align, spacing, start, end) => { const docData = logData(document); @@ -289,6 +292,60 @@ Q ), ]); }); + test.each([ + ['wrapped', { width: 400 }], + ['lineBreak false', { lineBreak: false }], + ])( + 'bounds of %s text with horizontalScaling and wordSpacing', + (_name, options) => { + const bounds = document.boundsOfString('Hello big world', 50, 50, { + ...options, + horizontalScaling: 80, + wordSpacing: 6, + }); + + // the glyphs and both word gaps are scaled to 80% + expect(bounds.width.toFixed(2)).toBe('72.60'); + }, + ); + + test.each([50, 150])( + 'wraps text with horizontalScaling %i at the given width', + (horizontalScaling) => { + const bounds = document.boundsOfString( + 'The quick brown fox jumps over the lazy dog while the five boxing wizards jump quickly', + 50, + 50, + { width: 200, horizontalScaling }, + ); + + expect(bounds.width).toBeLessThanOrEqual(200); + expect(bounds.width).toBeGreaterThan(180); + }, + ); + + test('justified text with horizontalScaling fills the line', () => { + const docData = logData(document); + const text = + 'The quick brown fox jumps over the lazy dog while the five boxing wizards jump quickly'; + const options = { + width: 200, + align: 'justify', + underline: true, + horizontalScaling: 120, + }; + const bounds = document.boundsOfString(text, 50, 50, options); + + document.text(text, 50, 50, options); + document.end(); + + // the first line reaches the right edge, plus its trailing space + expect(bounds.width.toFixed(2)).toBe('204.00'); + expect(docData).toContainChunk([ + 'stream', + /50 [\d.]+ m\n254\.00\d* [\d.]+ l/, + ]); + }); }); describe('text with structure parent links', () => {