From 6e7a00d29b4b4c066975cba839b63d06ca406790 Mon Sep 17 00:00:00 2001 From: youdie006 Date: Wed, 7 Oct 2026 08:43:56 +0900 Subject: [PATCH] Lay out horizontally scaled text in page units The line wrapper scaled the width, indent and column gap by horizontalScaling, while widthOfString already returns scaled widths, so the scaling cancelled out: text wrapped at the same words as at 100% and ran past the right edge above 100% (or stopped short below it), and right and center alignment were off by the same factor. Justified spacing and the word spacing in decorations and boundsOfString were also computed at 100% and then scaled by Tz. --- CHANGELOG.md | 1 + lib/line_wrapper.js | 13 +++------- lib/mixins/text.js | 17 ++++++++---- tests/unit/text.spec.js | 57 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 74 insertions(+), 14 deletions(-) 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', () => {