You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Rendering Arabic text produces a PDF whose visible glyphs look mostly correct, but the underlying text layer (what pdfjs-dist's getTextContent(), copy/paste, browser search, or an ATS resume parser reads) is corrupted: specific Arabic letters are silently swapped for a different letter that shares the same base glyph shape but a different dot count/placement (e.g. ت↔ث, ح↔ج, ب↔ن, ي↔ى). Word spacing is also frequently dropped or a stray & is inserted.
This reproduces with pure Arabic text alone — no Latin text needs to be mixed in. Mixing in Latin runs (company names, tool names) makes the visual layout additionally worse (missing space between a Latin run and the following Arabic word), but the letter-substitution corruption in the extracted text happens either way.
Because the glyphs still look approximately right at a glance, this is easy to miss visually — it only shows up when the text layer is read programmatically (copy/paste, Ctrl+F, screen readers, ATS parsers). For a resume/CV generator this is a serious, silent-corruption bug: a PDF can look fine and still fail every downstream text-based use.
To Reproduce
Two standalone repros below (no app code involved, just @react-pdf/renderer + a registered Arabic font — tested with Noto Naskh Arabic, a static Google-Fonts style TTF).
Repro 1 — pure Arabic, no Latin mixed in
import{Document,Page,Text,Font,renderToBuffer}from'@react-pdf/renderer';importfsfrom'node:fs';Font.register({family: 'NotoNaskhArabic',fonts: [{src: 'NotoNaskhArabic-Regular.ttf',fontWeight: 400}],});constdoc=(<Document><Pagesize="A4"style={{padding: 32,fontFamily: 'NotoNaskhArabic',fontSize: 12,direction: 'rtl'}}><Text>
عملت كمهندسة برمجيات لمدة ثلاث سنوات. حاصلة على درجة البكالوريوس في علوم
الحاسوب. أخصائية توظيف تقني. استخدمت أدوات مثل الطلب يوميًا.
</Text></Page></Document>);constbuffer=awaitrenderToBuffer(doc);fs.writeFileSync('out.pdf',buffer);
Extracting the text layer with pdfjs-dist (getTextContent()) gives:
عملت كمهندسة برمجي ات لمدة ث&لات & سنوات. حاصلة على درحة النكالورثوس فى علوم الجاسوت. احصابية ثوظي ف ثقنى. اسيجدمت|.دوات مي&ل الطلت ث ومي ًا
Compare a few words directly (input → extracted):
Input
Extracted
What changed
ثلاث (three)
ث&لات
stray & inserted, trailing ث → ت
درجة (degree)
درحة
ج → ح
البكالوريوس (bachelor's)
النكالورثوس
ب→ن, ي→ث
الحاسوب (computer)
الجاسوت
ح→ج, ب→ت
أخصائية (specialist)
احصابية
خ→ح, ئ→ب
توظيف (recruitment)
ثوظيف
leading ت → ث
استخدمت (used, v.)
اسيجدمت
ت→ي, خ→ج
Every substitution pairs letters that share the same base contour and differ only by dot count/placement (ب ت ث are one shape with 1 dot below / 2 dots above / 3 dots above; ج ح خ are one shape with a dot below / none / a dot above). That pattern strongly suggests the code that maps a shaped glyph back to a Unicode codepoint (for the PDF's ToUnicode CMap / text layer) is picking the wrong sibling glyph from the font's glyph table — not a bidi-ordering problem, since the visual glyphs render close to correctly.
Repro 2 — Arabic mixed with Latin runs (closer to a real-world use case: names of companies/tools inside Arabic prose)
import{Document,Page,Text,View,Font,renderToBuffer}from'@react-pdf/renderer';importfsfrom'node:fs';Font.register({family: 'NotoNaskhArabic',fonts: [{src: 'NotoNaskhArabic-Regular.ttf',fontWeight: 400}]});Font.register({family: 'Roboto',fonts: [{src: 'Roboto-Regular.ttf',fontWeight: 400}]});constdoc=(<Document><Pagesize="A4"style={{padding: 32,fontFamily: 'NotoNaskhArabic',fontSize: 12,direction: 'rtl'}}><Viewstyle={{marginBottom: 10}}><Text>
عملت كمهندسة برمجيات في <Textstyle={{fontFamily: 'Roboto'}}>Meridian Fintech</Text> لمدة ثلاث سنوات.
</Text></View><Viewstyle={{marginBottom: 10}}><Text>
استخدمت أدوات مثل <Textstyle={{fontFamily: 'Roboto'}}>GitHub</Text> و
<Textstyle={{fontFamily: 'Roboto'}}>LinkedIn</Text> و
<Textstyle={{fontFamily: 'Roboto'}}>Notion</Text> يوميًا.
</Text></View></Page></Document>);constbuffer=awaitrenderToBuffer(doc);fs.writeFileSync('out.pdf',buffer);
Rendered PDF (visual, screenshot at 2x scale) — glyphs look roughly right, but notice there is no space between each Latin run and the following Arabic و ("and"): it reads GitHubوLinkedInوNotion glued together instead of GitHub و LinkedIn و Notion.
Extracted text layer for this one:
عملت كمهندسة ب رمج يات في|Meridian Fintech| |.لمدة ثلات سنوات||حاصلة علي درحة الن كالورثوس من| |University of Texas at Austin| |.في علوم الجاسوت||-حصاب-ية ثوظيف ثقني|اسيجدمت ا-دوات ميل| |GitHub| |و|LinkedIn| |و|Notion| |.ثوميًا
Same class of letter-substitution corruption as Repro 1, plus the missing-space layout issue around the Latin runs.
Expected behavior
The extracted text layer should exactly match the input string (same letters, same spacing), and there should be a space between a Latin run and an adjacent Arabic word, matching what's visually laid out.
Environment
@react-pdf/renderer: 4.8.1 (latest on npm at time of filing)
Font tested: Noto Naskh Arabic (static TTF). A teammate on the same project also tried Amiri (different foundry, not Noto-derived) and the letter-substitution corruption did not go away — it just moved to a different text run on the same page, which is why we suspect this is a stateful/accumulated bug (possibly in glyph subsetting or the ToUnicode CMap building — this repo recently switched from a forked pdfkit to the upstream pdfkit package in refactor: replace @react-pdf/pdfkit fork with upstream pdfkit #3509, also ~2 days before filing) rather than something specific to one font file.
Node: v24.14.0, Windows
Additional context
We're building CV/resume PDFs in multiple languages including Arabic. This is blocking that launch since a resume that silently loses letters in its text layer would fail ATS parsing and copy/paste for real job seekers — worse than an outright crash, since nothing visibly indicates the document is broken. Happy to help narrow this down further (e.g. bisecting recent textkit/pdfkit changes) if it's useful — just let me know where to look.
Describe the bug
Rendering Arabic text produces a PDF whose visible glyphs look mostly correct, but the underlying text layer (what
pdfjs-dist'sgetTextContent(), copy/paste, browser search, or an ATS resume parser reads) is corrupted: specific Arabic letters are silently swapped for a different letter that shares the same base glyph shape but a different dot count/placement (e.g.ت↔ث,ح↔ج,ب↔ن,ي↔ى). Word spacing is also frequently dropped or a stray&is inserted.This reproduces with pure Arabic text alone — no Latin text needs to be mixed in. Mixing in Latin runs (company names, tool names) makes the visual layout additionally worse (missing space between a Latin run and the following Arabic word), but the letter-substitution corruption in the extracted text happens either way.
Because the glyphs still look approximately right at a glance, this is easy to miss visually — it only shows up when the text layer is read programmatically (copy/paste,
Ctrl+F, screen readers, ATS parsers). For a resume/CV generator this is a serious, silent-corruption bug: a PDF can look fine and still fail every downstream text-based use.To Reproduce
Two standalone repros below (no app code involved, just
@react-pdf/renderer+ a registered Arabic font — tested with Noto Naskh Arabic, a static Google-Fonts style TTF).Repro 1 — pure Arabic, no Latin mixed in
Extracting the text layer with
pdfjs-dist(getTextContent()) gives:Compare a few words directly (input → extracted):
ثلاث(three)ث&لات&inserted, trailingث→تدرجة(degree)درحةج→حالبكالوريوس(bachelor's)النكالورثوسب→ن,ي→ثالحاسوب(computer)الجاسوتح→ج,ب→تأخصائية(specialist)احصابيةخ→ح,ئ→بتوظيف(recruitment)ثوظيفت→ثاستخدمت(used, v.)اسيجدمتت→ي,خ→جEvery substitution pairs letters that share the same base contour and differ only by dot count/placement (
ب ت ثare one shape with 1 dot below / 2 dots above / 3 dots above;ج ح خare one shape with a dot below / none / a dot above). That pattern strongly suggests the code that maps a shaped glyph back to a Unicode codepoint (for the PDF'sToUnicodeCMap / text layer) is picking the wrong sibling glyph from the font's glyph table — not a bidi-ordering problem, since the visual glyphs render close to correctly.Repro 2 — Arabic mixed with Latin runs (closer to a real-world use case: names of companies/tools inside Arabic prose)
Rendered PDF (visual, screenshot at 2x scale) — glyphs look roughly right, but notice there is no space between each Latin run and the following Arabic
و("and"): it readsGitHubوLinkedInوNotionglued together instead ofGitHub و LinkedIn و Notion.Extracted text layer for this one:
Same class of letter-substitution corruption as Repro 1, plus the missing-space layout issue around the Latin runs.
Expected behavior
The extracted text layer should exactly match the input string (same letters, same spacing), and there should be a space between a Latin run and an adjacent Arabic word, matching what's visually laid out.
Environment
@react-pdf/renderer: 4.8.1 (latest on npm at time of filing)@react-pdf/textkit: 7.0.1 (latest, published 2 days before filing — includes the run-level bidi rewrite from Bidi issue with RTL languages (Hebrew, Arabic) #2900/aeaa7a76b)ToUnicodeCMap building — this repo recently switched from a forkedpdfkitto the upstreampdfkitpackage in refactor: replace @react-pdf/pdfkit fork with upstream pdfkit #3509, also ~2 days before filing) rather than something specific to one font file.Additional context
We're building CV/resume PDFs in multiple languages including Arabic. This is blocking that launch since a resume that silently loses letters in its text layer would fail ATS parsing and copy/paste for real job seekers — worse than an outright crash, since nothing visibly indicates the document is broken. Happy to help narrow this down further (e.g. bisecting recent
textkit/pdfkitchanges) if it's useful — just let me know where to look.