Fix overlapping text and improve Arabic rendering in PDFs

Update PDF rendering logic to use fontkit for Arabic shaping and RTL reordering, eliminating pre-shaping and resolving text overlap issues.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 32b8a89e-ae1f-4a09-ae50-3a53ae3f3477
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/Ow4s0aa
Replit-Helium-Checkpoint-Created: true
This commit is contained in:
riyadhafraa
2026-05-03 19:01:50 +00:00
parent 6cbd5968e5
commit e0824bc6a0
3 changed files with 146 additions and 58 deletions
@@ -1024,15 +1024,19 @@ test("PDF content: title label, rowColor tint, dropped isHighlighted, fontColor"
void ascii;
});
test("PDF Arabic shaping: title renders in connected presentation forms (not isolated)", async () => {
test("PDF Arabic shaping: title renders in connected, correctly-ordered Arabic", async () => {
// Regression for the bug where Arabic in the printed PDF appeared as
// disconnected, visually-reversed letters (e.g. "الاجتماعات" →
// "تاعامتجالا"). PDFKit has no built-in HarfBuzz, so we now run a
// pre-shaping pass that converts base Arabic letters (U+0600..U+06FF)
// to their contextual presentation forms (FE80..FEFF, plus FEFB
// lam-alef ligature) BEFORE handing text to bidi + PDFKit. We assert
// the rendered PDF contains presentation-form codepoints in its
// embedded ToUnicode CMap (proves shaped glyphs were drawn).
// disconnected, visually-reversed letters (e.g. "قائمة حضور" →
// "ةمئاقروضح"). The renderer now hands RAW Unicode (logical order,
// U+0600..06FF base letters) to PDFKit, which forwards it to fontkit
// along with `features: ['rtla', 'rclt', 'calt', 'liga', ...]`.
// Fontkit performs shaping AND visual right-to-left reorder at the
// glyph layer, so the PDF content stream contains glyph IDs, not
// characters. Per PDF 1.7 §9.10, every embedded TrueType font carries
// a ToUnicode CMap that maps each emitted glyph back to its LOGICAL
// base codepoint — that is what we assert here. (Pre-shaped output
// would put U+FExx presentation-form codepoints in the CMap; the
// current pipeline must NOT do that.)
// End-to-end: render a real PDF and assert the inflated streams
// (which include the embedded ToUnicode CMap) reference at least one
@@ -1081,13 +1085,25 @@ test("PDF Arabic shaping: title renders in connected presentation forms (not iso
cursor = end + "\nendstream".length;
}
const allDecoded = decoded.join("\n");
// Match any FE80..FEFF or FB50..FDFF hex literal — these are the
// Arabic Presentation Forms ranges. Without shaping none would appear.
const presFormsRegex = /<(?:FE[89A-F][0-9A-F]|FEF[0-9A-F]|FE[7][0-9A-F]|FB[5-9A-F][0-9A-F]|FC[0-9A-F]{2}|FD[0-9A-F]{2})>/i;
// Assert (a) the embedded ToUnicode CMap references at least one base
// Arabic codepoint (U+0600..06FF) — proves Arabic content was drawn
// and survived round-tripping — and (b) the CMap does NOT reference
// any Arabic Presentation Forms (FE70..FEFF / FB50..FDFF). The
// presence of presentation-form codepoints in the CMap would mean
// someone re-introduced the pre-shaping pass and the PDF would
// render visually mirrored again.
const baseArabicRegex = /<(?:06[0-9A-F]{2})>/i;
assert.match(
allDecoded,
presFormsRegex,
"rendered PDF must reference Arabic Presentation Forms codepoints (proves shaping ran before PDFKit)",
baseArabicRegex,
"rendered PDF must reference base Arabic codepoints in its ToUnicode CMap",
);
const presFormsRegex = /<(?:FE[789A-F][0-9A-F]|FB[5-9A-F][0-9A-F]|FC[0-9A-F]{2}|FD[0-9A-F]{2})>/i;
const presMatch = allDecoded.match(presFormsRegex);
assert.equal(
presMatch,
null,
`rendered PDF must NOT reference Arabic Presentation Forms in its ToUnicode CMap — found ${presMatch?.[0]} (regression: pre-shaping has been re-introduced and the PDF will render visually mirrored)`,
);
});