Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

### Unreleased

- Reset `continued` wrapping state when a `text` call is given an explicit x/y position, so the coordinate is treated as absolute instead of being offset by a previous continued run (#1641)
- [BREAKING CHANGE] Restrict AcroForm options to documented mappings and explicit escape hatches.
- [BREAKING CHANGE] Stop automatically uppercasing annotation option keys.
- Do not mutate options passed to `doc.annotate()` and its convenience methods (link, note, strike, lineAnnotation, rectAnnotation, ellipseAnnotation, textAnnotation, fileAnnotation)
Expand Down
8 changes: 7 additions & 1 deletion docs/text.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ below.
* `strike` - whether to strike out the text
* `oblique` - whether to slant the text (angle in degrees or `true`)
* `baseline` - the vertical alignment of the text with respect to its insertion point (values as [canvas textBaseline](https://www.w3schools.com/tags/canvas_textbaseline.asp))
* `continued` - whether the text segment will be followed immediately by another segment. Useful for changing styling in the middle of a paragraph.
* `continued` - whether the text segment will be followed immediately by another segment. Useful for changing styling in the middle of a paragraph. Note that this is a *forward-looking* flag set on the current segment to make the **next** `text` call continue from where this one left off — the continuing call itself does not need the flag. See [Rich Text](#rich-text) below.
* `features` - an array of [OpenType feature tags](https://www.microsoft.com/typography/otspec/featuretags.htm) to apply. Can also be provided as an object with features as keys and boolean values. If not provided, a set of defaults is used. To deactivate default font features, you have to explicitly set them to false (`{ liga: false }`). When providing an empty array the default font features will still be used.

Additionally, the fill and stroke color and opacity methods described in the
Expand Down Expand Up @@ -173,6 +173,12 @@ Here is the output:

![4]()

Because `continued` is forward-looking, the wrapping state stays active until a `text` call
*without* `continued: true` consumes it. To end a run explicitly — for example the last
iteration of a loop — set `continued: false` on the final segment. Alternatively, giving the
next `text` call an explicit `x` or `y` position starts it fresh: such a call ignores the
leftover state and draws at the absolute coordinates you pass, rather than continuing inline.

To cancel a link in rich text set the `link` option to `null`.

doc.fillColor('red')
Expand Down
13 changes: 13 additions & 0 deletions lib/mixins/text.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,19 @@ export default {
},

_text(text, x, y, options, lineCallback) {
// A call that explicitly positions itself (an x or y is given) and is not
// itself a continued segment should start fresh, discarding any wrapping
// state left behind by a previous `continued: true` run. Otherwise the
// passed coordinate would be offset by the previous run's inline position
// instead of being treated as absolute (see issue #1641).
const rawOptions = x && typeof x === 'object' ? x : options || {};
const hasExplicitPosition =
(x != null && typeof x !== 'object') || y != null;
if (hasExplicitPosition && !rawOptions.continued) {
this._wrapper = null;
this._textOptions = null;
}

options = this._initOptions(x, y, options);

// Convert text to a string
Expand Down
40 changes: 40 additions & 0 deletions tests/unit/text.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,46 @@ Q

expect(docData).toContainText({ text: 'text with null x' });
});

// Capture the absolute x each fragment is actually drawn at.
const spyFragmentX = () => {
const positions = [];
const original = document._fragment.bind(document);
document._fragment = (text, x, y, options) => {
if (text && String(text).trim()) {
positions.push({ text: String(text).trim(), x });
}
return original(text, x, y, options);
};
return positions;
};

test('explicit position resets a previous continued run (#1641)', () => {
const positions = spyFragmentX();

['AAA ', 'BBB ', 'CCC '].forEach((t) =>
document.text(t, { continued: true }),
);
// A non-continued call with an explicit x should be absolute, not offset
// by the inline position left behind by the continued run above.
document.text('FRESH', 400);
document.end();

const fresh = positions.find((p) => p.text === 'FRESH');
expect(fresh.x).toBe(400);
});

test('continued run without coordinates still flows inline', () => {
const positions = spyFragmentX();

document.text('AAA', { continued: true });
document.text('BBB');
document.end();

const a = positions.find((p) => p.text === 'AAA');
const b = positions.find((p) => p.text === 'BBB');
expect(b.x).toBeGreaterThan(a.x);
});
});

describe('text with structure parent links', () => {
Expand Down
Loading