Your Arabic PDF Looks Perfect but Isn't Searchable: The Hidden Cost of RTL Text in PDF Generation

Your Arabic PDF renders beautifully on screen. Every ligature connects, every harakat sits in the right place, the text flows right-to-left exactly as…

Your Arabic PDF renders beautifully on screen. Every ligature connects, every harakat sits in the right place, the text flows right-to-left exactly as expected. Then a user copies their name from an invoice and pastes gibberish. An ATS rejects a CV because the extracted text reads backwards. A regulator's search tool finds zero matches in a document that clearly contains the Arabic words they searched for.

In a test across thirteen popular Arabic fonts, only one produced searchable text. The other twelve looked perfect but failed text extraction completely.

Layer 1: Why non-browser PDF engines still break Arabic shaping and bidi

PDFKit, which powers many Node.js PDF generators, dumps glyphs in string order without running the Unicode bidi algorithm or handling direction at all [1]. An Arabic word like "شركة" gets written left-to-right in the PDF content stream, so it appears reversed on the page unless you pre-process the text. A maintainer explicitly stated that RTL is not supported [2].

@react-pdf/renderer inherits this same root cause. Right-aligning the block puts it at the right edge but does not reorder the text for right-to-left reading [1]. Ligatures drop or misplace during bidi reordering because the textkit engine deduplicates repeated ligature clusters by glyph ID [3]. A reproduction case mixes Arabic words sharing a "شر" ligature with LTR content like a company name and "50%" — exactly the pattern you see in bilingual invoices [3].

String-reversal hacks are the fragile workaround developers reach for next. Reverse the string before passing it to the engine, and the LTR output looks correct on screen. This breaks cursive connections in Arabic and Farsi, drops harakat onto wrong letters, and falls apart entirely for mixed-direction paragraphs [1]. The arabic-reshaper library defaults to deleting harakat precisely because bidi get_display would otherwise misplace them [4].

Community patches exist. The @react-pdf-rtl wrapper applies bidi logic and glyph reshaping before rendering [1]. But these sit outside the core project, lag behind releases, and still cannot guarantee ligature preservation through the full pipeline. For openhtmltopdf, you must explicitly wire an ICU bidi splitter and reorderer into the PdfRendererBuilder, plus set a default RTL text direction, before Arabic documents render correctly at all [5].

ReportLab follows the same pattern: raw Arabic Unicode renders left-to-right unless you preprocess with arabic_reshaper and python-bidi [6]. The Python stack works for simple documents but becomes unwieldy for mixed-content layouts with embedded LTR numbers and names. Farsi and Urdu extend the problem further with characters like Pe, Che, Zhe, and Gaf in blocks U+0750-077F and U+08A0-08FF, plus Nasta'liq calligraphy that no standard reshaping tool supports [6].

If you are using PDFKit, @react-pdf/renderer, ReportLab, or any engine that does not run HarfBuzz or equivalent shaping, you are in Layer 1. The text looks wrong on screen, and fixing that is hard enough that many developers stop there.

Layer 2: Headless Chrome renders correctly but stores presentation forms

The second layer traps developers who solved Layer 1. Headless Chrome uses HarfBuzz, which shapes Arabic perfectly. The screen output is flawless. The PDF is not.

Most Arabic fonts write Unicode presentation forms — characters in the U+FB50–FEFF range — instead of the original Arabic letters into the PDF content stream. These presentation forms are the visual variants: initial, medial, final, isolated shapes, plus ligatures like lam-alef. They exist for compatibility with legacy systems and should not appear in modern Unicode text. When a PDF generator stores them instead of the source characters, the ToUnicode map cannot trace shaped glyphs back to searchable text.

Thirteen fonts were measured in the test. Only Amiri produced text you could search, select, and copy correctly. The other twelve, including widely used web fonts, all failed. Every font except Amiri failed extraction in some way.

QA passes because the screenshot looks perfect. Accessibility checkers pass because alt text exists. Search indexing fails silently. Regulatory compliance tools fail silently. Customer support tickets arrive months later: "I can't copy my address from the invoice."

The PDF looks correct because Chromium shaped it correctly. What you probably skipped was opening the PDF structure and checking whether the glyphs map back to real Unicode characters or just presentation forms that look right on screen.

How to test for the presentation-forms trap in your own pipeline

You need a round-trip extraction test. Generate your PDF, extract the text with a tool like pdfplumber, and compare it against the source string — keeping in mind that extraction itself can be fragile and font-dependent [7]. One available CI tool exits with code 1 on failure and supports --json output for assertions.

Test with mixed content deliberately. Combine Arabic words that share ligatures with LTR content — company names, percentages, phone numbers — because these are exactly the cases where bidi and shaping interact [3]. Check harakat preservation separately. Most pipelines silently delete harakat rather than risk misplacement, so your test must verify their presence explicitly [4].

Run this test in CI before every deploy that touches fonts, templates, or rendering engines.

What actually works: browser-grade shaping plus verified fonts

Use a real browser engine for shaping, not PDFKit or string hacks. HarfBuzz in Chromium handles the bidi algorithm, ligature substitution, and mark positioning correctly. The engine must then write the original Unicode characters to the PDF, not their presentation forms.

Embed fonts known for correct ToUnicode maps. Amiri passed the test and is the author's production choice. Remove font pickers from Arabic document flows — hardcode the safe font rather than letting design systems substitute an untested alternative. Add round-trip extraction tests to CI before any font change, because a font that works for Latin text may fail completely for Arabic.

Avoid the self-managed Puppeteer tax. Running Chromium for PDF generation in production means RAM limits, cold starts, font binary management, CORS restrictions, and serverless package size limits [8]. Each of these becomes harder with Arabic fonts, which are larger than Latin equivalents and may require specific system font configurations that differ between your laptop and your deployment environment.

How a hosted renderer removes the Puppeteer tax for RTL documents

MarkupGo is a hosted API covering image and PDF conversion [8], so you never manage Chromium binaries, font files, or serverless packaging [8]. You can specify a verified font like Amiri directly in your HTML template's CSS. There are no serverless package size limits, no cold starts, and no font binary management on your infrastructure [8].

The same API covers image and PDF conversion, so an Arabic social graphic and an Arabic invoice can share one template [8]. MarkupGo's blog and docs cover reusable templates alongside automation with Make.com and Google Sheets, so non-technical team members can trigger generation [8].

For documents that originate outside HTML, the Office-to-PDF API converts over 130 formats including .docx, .xlsx, and .pptx, with options for landscape orientation, page ranges, and form-field export [9]. Merging multiple files and setting custom PDF properties are also supported [9]. This covers the case where your Arabic content starts in Word or Excel before you need a production PDF.

Next step: run the extraction test on your last Arabic PDF

Pick one generated Arabic PDF from production. Extract text with pdfplumber or similar [7]. Compare extracted text against source — check ligatures, harakat, direction. If the lam-alef ligature extracts backwards, if harakat are missing, if mixed Arabic-English passages read left-to-right where they should read right-to-left, your pipeline is in Layer 2 and searchability is broken.

If it fails, switch engine and font before your next deploy.

Sources

  1. techresolve.blog
  2. github.com
  3. github.com
  4. arabic-reshaper · PyPI (pypi.org)
  5. github.com
  6. textarabi.com
  7. stackoverflow.com
  8. markupgo.com
  9. markupgo.com