diegomura / diegomura/react-pdf

Arabic text layer corrupted (wrong Unicode codepoints) even without mixing Latin text

Open
#3,533 1 comment 0 reactions 0 assignees View on GitHub
Dominant language
TypeScript
Stars
16.8k
Forks
1.3k
Avg merge
5h 6m
Merged PRs (30d)
52

Description

**Describe the bug**

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

```tsx
import { Document, Page, Text, Font, renderToBuffer } from '@react-pdf/renderer';
import fs from 'node:fs';

Font.register({
family: 'NotoNaskhArabic',
fonts: [{ src: 'NotoNaskhArabic-Regular.ttf', fontWeight: 400 }],
});

const doc = (



عملت كمهندسة برمجيات لمدة ثلاث سنوات. حاصلة على درجة البكالوريوس في علوم
الحاسوب. أخصائية توظيف تقني. استخدمت أدوات مثل الطلب يوميًا.



);

const buffer = await renderToBuffer(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)

```tsx
import { Document, Page, Text, View, Font, renderToBuffer } from '@react-pdf/renderer';
import fs from 'node:fs';

Font.register({ family: 'NotoNaskhArabic', fonts: [{ src: 'NotoNaskhArabic-Regular.ttf', fontWeight: 400 }] });
Font.register({ family: 'Roboto', fonts: [{ src: 'Roboto-Regular.ttf', fontWeight: 400 }] });

const doc = (




عملت كمهندسة برمجيات في Meridian Fintech لمدة ثلاث سنوات.




استخدمت أدوات مثل GitHub و
LinkedIn و
Notion يوميًا.




);

const buffer = await renderToBuffer(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)
- `@react-pdf/textkit`: 7.0.1 (latest, published 2 days before filing — includes the run-level bidi rewrite from #2900/`aeaa7a76b`)
- 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 #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.

Contributor guide

Open the contributing guide

Research direction

Start with the standalone renderToBuffer repros and inspect the resulting text through pdfjs-dist getTextContent(). Trace the textkit and upstream pdfkit paths involved in glyph subsetting and ToUnicode generation, using the reported recent changes as possible bisect points. Done means Arabic extraction preserves every input letter and space, including around Latin runs.

Written by the indexing model from the issue text.

Assessment

Tech stack
node.js, react, typescript
Domain
backend, tooling
Issue type
Bug
Difficulty
5/5
Estimated time
Over a week
Activity status
Active
Clarity
Needs clarification
Newbie friendliness
38/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.