GPOS mark offsets (xOffset/yOffset) are effectively ignored at render time — Arabic dots / combining marks misplaced (4.9.0)
Maintainers usually reply within 1 day
Nobody has claimed this yet.
Assessment
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Newbie friendliness
- 76/100
- Issue type
- Bug
- Clarity
- Clearly specified
- Activity status
- Active
- Tech stack
- react, typescript
- Domain
- backend
Research direction
Start by locating textkit's resolveRunYOffset and the render package's renderGlyphs and _renderGlyphs entry points. Trace how GPOS xOffset/yOffset values are scaled and preserved, then compare generated PDFs with fontkit's native RTL positions using the Arabic reproduction and the reported Thai strings. Done means marks match fontkit placement without regressing the zero-code-point bidi behavior from #3555.
Written by the indexing model from the issue text.
Description
Describe the bug
GPOS mark positioning (xOffset / yOffset from fontkit) is effectively ignored when glyphs are drawn. Marks end up at the pen position instead of where the font places them. In Arabic this is visible on letters whose dots are separate zero-advance glyphs in Noto Sans Arabic. For example, the two dots of final yeh in في / العربي are drawn beside the letter instead of under its bowl.
Versions: @react-pdf/renderer 4.9.0 (@react-pdf/render 4.7.0, @react-pdf/textkit 7.0.1), fontkit 2.0.4, Node 22.
To Reproduce
import React from 'react'
import { Document, Page, Text, Font, renderToFile } from '@react-pdf/renderer'
Font.register({ family: 'AR', src: 'https://fonts.gstatic.com/s/notosansarabic/v33/nwpxtLGrOAZMl5nJ_wfgRg3DrWFZWsnVBJ_sS6tlqHHFlhQ5l3sQWIHPqzCfyGyvuw.ttf' })
renderToFile(
<Document><Page style={{ padding: 20 }}>
<Text style={{ fontFamily: 'AR', fontSize: 40 }}>في دول الخليج العربي</Text>
</Page></Document>,
'out.pdf',
)
Compare with the same string shaped by fontkit directly (font.layout(s, undefined, undefined, undefined, 'rtl'), then draw each glyph path at x + xOffset, y - yOffset). In the PDF the yeh dots are offset sideways; with fontkit's own positions they sit under the bowl.
Root cause (as far as we can tell)
textkitscalePositionsconvertsxOffset/yOffsetto points (× fontSize / unitsPerEm).textkitresolveRunYOffsetthen overwrites every position'syOffsetwith the sub/super offset (attributes.yOffset * unitsPerEm, in font units). So the GPOSyOffsetis always lost.renderrenderGlyphsconvertsxAdvanceto 1/1000 em (× 1000 / fontSize) but passesxOffset/yOffsetthrough unchanged._renderGlyphsthen doesx + pos.xOffset * scalewithscale = fontSize / 1000. A value already in points gets multiplied byfontSize / 1000again, so at 10pt the offset ends up about 100× too small.
Fix that worked for us
In resolveRunYOffset, keep the GPOS offsets and convert them into the unit render expects:
const toPerMille = run.attributes?.fontSize ? 1000 / run.attributes.fontSize : 0;
const positions = run.positions.map((p) => Object.assign({}, p, {
xOffset: (p.xOffset || 0) * toPerMille,
yOffset: yOffset + (p.yOffset || 0) * toPerMille,
}));
With this, together with keeping zero-code-point mark glyphs during bidi reordering (see #3555), glyph positions matched fontkit's native RTL layout exactly. That held for 8 Arabic test strings, and Thai stacked tone marks also matched.
Expected behavior
Mark glyphs are placed using the font's GPOS offsets, the same as fontkit's own layout.
- Dominant language
- TypeScript
- Stars
- 16.8k
- Forks
- 1.3k
- Avg merge
- 11m
- Merged PRs (30d)
- 2
Getting set up
- No Dockerfile or Docker Compose file
- No pull request template
- Read the contributing guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from diegomura/react-pdf
-
@react-pdf/font: published types import fontkit, but @types/fontkit is only a devDependencyPossibly taken @iliev-blnk claimed this 12 days ago. Open
Difficulty 2/5 1-3 hours Newbie friendliness 68/100
Maintainers usually reply within 1 day
-
Unable to set flexShrink to 0Possibly taken @adarshsm claimed this 11 days ago. Open
Difficulty 2/5 1-3 hours Newbie friendliness 86/100
Maintainers usually reply within 1 day
-
@react-pdf/hyphenate: import-only exports map throws ERR_PACKAGE_PATH_NOT_EXPORTED in require-condition resolvers (every tsx script loading renderer >= 4.7 crashes)Possibly taken @cruzdanilo claimed this 40 days ago. Open
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
diegomura/react-pdf#3551 · 2 reactions ·
Maintainers usually reply within 1 day
-
Difficulty 3/5 1-2 days Newbie friendliness 72/100
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 48/100
diegomura/react-pdf#3606 · 3 reactions ·
Maintainers usually reply within 1 day
All issues in diegomura/react-pdf
Similar issues
-
Difficulty 2/5 1-3 hours Newbie friendliness 75/100
Maintainers usually reply within 1 day
-
Difficulty 1/5 Under an hour Newbie friendliness 83/100
Maintainers usually reply within 1 day
-
🔊 a11y
Difficulty 2/5 1-3 hours Newbie friendliness 82/100
betagouv/mon-entreprise#4726 ·
Maintainers usually reply within 9 days
-
Difficulty 2/5 1-3 hours Newbie friendliness 68/100
Deltares/fews-pi-requests#357 ·
Maintainers usually reply within 1 day
-
bug confirmed js v6
Difficulty 2/5 1-3 hours Newbie friendliness 85/100
Maintainers usually reply within 1 day