Skip to content

fix(textkit): merge flattened runs in input order - #3614

Open
iliev-blnk wants to merge 1 commit into
diegomura:masterfrom
iliev-blnk:fix/flatten-substituted-font
Open

iliev-blnk wants to merge 1 commit into
diegomura:masterfrom
iliev-blnk:fix/flatten-substituted-font

Conversation

@iliev-blnk

Copy link
Copy Markdown

Fixes #3552

Cause

flattenRegularRuns merged the open runs in the order they were pushed onto the stack, which is the order they start in. A run that starts mid-string therefore overrode every run that was already open.

Before #3527, that didn't matter for fonts: preprocessRuns removed font from the original runs (omitFont). #3527 dropped that pass and moved substitutedRuns to the end of the list instead, so that the substituted font would win. But flatten never looked at list order. For the first fragment of a <Text> the substitution run is pushed last, and it wins. Every later fragment starts mid-string, gets pushed after the substitution run covering it, and brings back the raw font stack. Characters missing from the stack's first font then render as .notdef.

Fix

The stack now holds run indices kept in input order, so a later run in the list overrides an earlier one wherever each starts. That is the precedence preprocessRuns already assumes. Attributes are still merged only when a segment is emitted (#3524).

Ending a run now removes it by index rather than by attributes identity. Before, two runs sharing an attributes object both left the stack when the first of them ended.

Repro

The issue's script (fontFamily: ['NoPercent', 'Latin'], where the first font has no %):

e(Text, null, 'single string: 69%'),
e(Text, null, 'two fragments: ', '69', '%'),
master this branch
two fragments: 69□ two fragments: 69%

pdftotext on master gives two fragments: 69; with this change it gives two fragments: 69%.

Tests

  • flatten: a later run overrides an earlier one that starts before it; runs sharing an attributes object.
  • preprocessRuns: substituted attributes win over original runs that start mid-string.
  • All three fail on master and pass here. vitest run: 247 files, 3669 tests passing.

flatten merged the active runs in the order they started, so a run
starting mid-string overrode every run that was already open. Since
preprocessRuns stopped omitting the font from the original runs and put
the substitution runs last instead, any text fragment after the first
one brought back the raw font stack, and characters missing from its
first font rendered as .notdef.

Keep the active runs sorted by input index, so later runs win wherever
they start. Removing runs by index also stops two runs that share an
attributes object from both ending at the first one's end.

Fixes diegomura#3552
@vercel

vercel Bot commented Oct 28, 2026

Copy link
Copy Markdown

@iliev-blnk is attempting to deploy a commit to the diegomura Team on Vercel.

A member of the Team first needs to authorize it.

@changeset-bot

changeset-bot Bot commented Oct 28, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: abc34f4

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 8 packages
Name Type
@react-pdf/textkit Patch
@react-pdf/layout Patch
@react-pdf/render Patch
@react-pdf/renderer Patch
@react-pdf/examples Patch
@react-pdf/math Patch
@react-pdf/mermaid Patch
@react-pdf/ui Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

textkit 7.0.1 regression: multi-child <Text> loses font substitution at fragment boundaries ({value}% renders tofu)

1 participant