Fix Arabic text rendering in generated PDFs
Add Arabic text shaping functionality to convert base characters to their contextual presentation forms before bidi reordering and PDF rendering. Includes a new regression test to verify correct shaping by asserting the presence of presentation form codepoints in the PDF's embedded ToUnicode CMap. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66 Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Event-Id: 18d38e3a-cfe8-4b69-a02c-380283320b78 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:
@@ -1024,6 +1024,73 @@ 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 () => {
|
||||
// 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).
|
||||
|
||||
// End-to-end: render a real PDF and assert the inflated streams
|
||||
// (which include the embedded ToUnicode CMap) reference at least one
|
||||
// Arabic Presentation Forms-B codepoint. Without shaping the CMap
|
||||
// would only contain base U+06xx hex values.
|
||||
const shapedDate = "2099-05-04";
|
||||
const shapedMeeting = await api(adminCookie, "POST", "/api/executive-meetings", {
|
||||
titleAr: "اجتماع الاختبار",
|
||||
titleEn: "Shaping Smoke Test",
|
||||
meetingDate: shapedDate,
|
||||
attendees: [
|
||||
{ name: "محمد عبدالله", attendanceType: "internal", sortOrder: 0 },
|
||||
],
|
||||
});
|
||||
assert.equal(shapedMeeting.status, 201);
|
||||
created.meetingIds.push((await shapedMeeting.json()).id);
|
||||
|
||||
const res = await api(
|
||||
adminCookie,
|
||||
"GET",
|
||||
`/api/executive-meetings/pdf?date=${shapedDate}&lang=ar`,
|
||||
);
|
||||
assert.equal(res.status, 200);
|
||||
const buf = Buffer.from(await res.arrayBuffer());
|
||||
|
||||
// Inflate every FlateDecode stream and concat — same trick as the
|
||||
// existing PDF content test. ToUnicode CMaps are inflate-able and
|
||||
// contain pairs like `<XX> <FE8D>`, so a regex over the joined
|
||||
// payload is enough.
|
||||
let cursor = 0;
|
||||
const decoded = [];
|
||||
while (true) {
|
||||
const start = buf.indexOf("stream\n", cursor);
|
||||
if (start < 0) break;
|
||||
const end = buf.indexOf("\nendstream", start);
|
||||
if (end < 0) break;
|
||||
try {
|
||||
decoded.push(
|
||||
zlib
|
||||
.inflateSync(buf.slice(start + "stream\n".length, end))
|
||||
.toString("latin1"),
|
||||
);
|
||||
} catch {
|
||||
// not a deflate stream
|
||||
}
|
||||
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.match(
|
||||
allDecoded,
|
||||
presFormsRegex,
|
||||
"rendered PDF must reference Arabic Presentation Forms codepoints (proves shaping ran before PDFKit)",
|
||||
);
|
||||
});
|
||||
|
||||
test("PDF embeds the brand logo when set in global font settings", async () => {
|
||||
// Build a real 1x1 RGB PNG (png-js rejects most base64 fixtures).
|
||||
const u32 = (n) => {
|
||||
|
||||
Reference in New Issue
Block a user