How we verify
Every render can carry a report. These are the gates and what each checks. The suite is public because a guarantee you cannot inspect is a slogan.
The gates
- G0 · Input hygiene
- No presentation forms in the input (
U+FB50–FDFF,U+FE70–FEFF). Pre-shaped text is the commonest corruption, and it arrives looking fine. - G1 · Extraction round-trip
- The finished PDF is read back by three independent extractors: poppler (pdftotext), PyMuPDF and pdfium. Each must return every source word with no presentation forms. Words are compared as a bag on the letter skeleton, harakat stripped: harakat survive in the text layer, but their order relative to the base letter depends on the extractor, so that is reported separately and not gated. Tokens split at script boundaries. Two of three extractors must agree.
- G2 · Contextual forms
- Each word is re-shaped against the font that drew it and compared with its letters shaped one at a time. If the two match, the word was drawn with isolated letters, and the gate fails.
- G3 · Missing glyphs
- Every character in every run must have a glyph in the font that draws it. No notdef boxes, no silent fallback.
- G6 · Stacked-diacritic collision
- Vocalised runs are shaped and the bounding boxes of marks in the same cluster are compared. Overlap above 0.25 of the smaller box fails. Nothing else in the industry tests this.
- G7 · Font composition
- Arabic runs must be drawn by fonts with one glyph per letter. Fonts that split dotted letters into a dotless base plus a zero-advance dot mark — the Noto Arabic family — make every extractor break words at the dots (فاتورة comes back as ف ات ورة). Tanqeet substitutes a precomposed look-alike and reports it; with
fix_fontsoff the gate fails and names the font to use instead. - G4, G5 · Build gates
- Bidirectional and shaping conformance run in CI against the Unicode suites and the shaping engine's Arabic fixtures, pinned to our font hashes. They fail the build, not the request.
See it
The same vocalised word in the two faces this site ships: Amiri for body text, Scheherazade New for vocalised specimens. Both are on the safe list: one glyph per letter, and stacked marks that pass the collision gate.
Fonts
Precomposed fonts only, by default: one glyph per letter, so the text layer reads back as the word that was drawn. Fonts are vendored from pinned upstream releases with recorded hashes, never from a distribution package. All OFL-1.1, static instances only: a variable font embeds as Type 3 in Chromium's PDF output and wrecks the text layer.
Used as sent
- Amiri
- Amiri Quran
- Scheherazade New
- Lateef
- Harmattan
- Tajawal
- Almarai
- Vazirmatn
- Readex Pro
- Markazi Text
- El Messiri
- Baloo Bhaijaan 2
- Rubik
- Cairo
- IBM Plex Sans Arabic
Installed, substituted by default
These split dotted letters into a base plus a dot mark, so their text layer cannot round-trip (G7). Each is swapped for the precomposed face that reads the same, and the swap is reported.
| Requested | Drawn with |
|---|---|
| Noto Naskh Arabic | Amiri |
| Noto Kufi Arabic | Almarai |
| Noto Sans Arabic | Vazirmatn |
| Changa | Tajawal |
Text-layer repair
Every PDF is repaired after rendering, in four passes. This is the default mode, plain: it adds no /ActualText at all, and it is the choice that reads correctly in pdftotext (poppler), PyMuPDF, PDFBox and pdfminer. The other modes are in the docs.
- 1 · ToUnicode repair
- The producer's glyph-to-text map is rewritten: presentation-form code points become letters; ligature entries are written in the order extractors read them; and glyphs the producer could not name (NUL,
?, private-use code points) get their text from the producer's own cluster markup, which is then removed. - 2 · Mark reordering
- The shaping engine writes each harakah before its base letter, and extractors break the word at every mark. Inside every vocalised word the glyph operators are re-emitted bases first, then marks, each at its original position. Nothing moves on the page; the letters come back as one word and the marks follow as a detached cluster. This is why the round-trip gate compares the letter skeleton and reports harakat separately.
- 3 · Width compensation
- Where a font places the next letter beyond the previous letter's advance width (Amiri's ر followed by ي sits 0.12 em out), poppler ends the word there. The advance widths in the PDF are widened by the measured gap; glyph positions are explicit, so nothing moves.
- 4 · One object per word
- pdfium (Chrome, Edge) sorts a line's text objects by position and reverses each right-to-left run separately, with every harakah counted as a run boundary; it also drops an object that has no width, which a lone space is, and re-invents spaces from the gaps. So each word's letters, its baseline marks and the space after it are folded into one text object, positioned by the final advance widths; raised marks are drawn at the end of the line. Nothing moves, and pdfium now reads the same words as poppler and PyMuPDF.
Measured
Default mode, a six-line probe: plain, ligature-heavy and fully vocalised lines. A family passes when every line comes back as the words that were sent.
| Round-trips in | Families |
|---|---|
| poppler, PyMuPDF and pdfium | Amiri, Amiri Quran, Scheherazade New, Lateef, Harmattan, Tajawal, Almarai, Vazirmatn, Cairo, Readex Pro, Markazi Text, El Messiri, Rubik, Baloo Bhaijaan 2, IBM Plex Sans Arabic |
| Substituted by default (gate G7) | Noto Naskh Arabic, Noto Kufi Arabic, Noto Sans Arabic, Changa |
Measured 2026-09-14 on poppler 24.02, MuPDF 1.28 and pdfium 7999 (pypdfium2 5.13), Chromium engine. Every family on the safe list reads back every word in all three; G1 still asks for two of three, so one extractor's next release cannot fail a document the other two read. The substituted families decompose dotted letters into shared glyphs, which no text layer can name.
This site
This site is held to the same gates. Its Arabic runs through the suite in CI; if a page fails, the build fails.