diff --git a/artifacts/api-server/assets/fonts/DINNextLTArabic-Bold.ttf b/artifacts/api-server/assets/fonts/DINNextLTArabic-Bold.ttf new file mode 100644 index 00000000..dc79397c Binary files /dev/null and b/artifacts/api-server/assets/fonts/DINNextLTArabic-Bold.ttf differ diff --git a/artifacts/api-server/assets/fonts/DINNextLTArabic-Regular.ttf b/artifacts/api-server/assets/fonts/DINNextLTArabic-Regular.ttf new file mode 100644 index 00000000..75f718d5 Binary files /dev/null and b/artifacts/api-server/assets/fonts/DINNextLTArabic-Regular.ttf differ diff --git a/artifacts/api-server/assets/fonts/HelveticaNeue-Bold.ttf b/artifacts/api-server/assets/fonts/HelveticaNeue-Bold.ttf new file mode 100644 index 00000000..a9189019 Binary files /dev/null and b/artifacts/api-server/assets/fonts/HelveticaNeue-Bold.ttf differ diff --git a/artifacts/api-server/assets/fonts/HelveticaNeue-Regular.ttf b/artifacts/api-server/assets/fonts/HelveticaNeue-Regular.ttf new file mode 100644 index 00000000..4c2cfa4b Binary files /dev/null and b/artifacts/api-server/assets/fonts/HelveticaNeue-Regular.ttf differ diff --git a/artifacts/api-server/assets/fonts/HelveticaNeueLTArabic-Bold.ttf b/artifacts/api-server/assets/fonts/HelveticaNeueLTArabic-Bold.ttf new file mode 100644 index 00000000..2d211807 Binary files /dev/null and b/artifacts/api-server/assets/fonts/HelveticaNeueLTArabic-Bold.ttf differ diff --git a/artifacts/api-server/assets/fonts/HelveticaNeueLTArabic-Regular.ttf b/artifacts/api-server/assets/fonts/HelveticaNeueLTArabic-Regular.ttf new file mode 100644 index 00000000..88c2a2ac Binary files /dev/null and b/artifacts/api-server/assets/fonts/HelveticaNeueLTArabic-Regular.ttf differ diff --git a/artifacts/api-server/assets/fonts/Majalla-Bold.ttf b/artifacts/api-server/assets/fonts/Majalla-Bold.ttf new file mode 100644 index 00000000..c01a8820 Binary files /dev/null and b/artifacts/api-server/assets/fonts/Majalla-Bold.ttf differ diff --git a/artifacts/api-server/assets/fonts/Majalla-Regular.ttf b/artifacts/api-server/assets/fonts/Majalla-Regular.ttf new file mode 100644 index 00000000..f4586ec0 Binary files /dev/null and b/artifacts/api-server/assets/fonts/Majalla-Regular.ttf differ diff --git a/artifacts/api-server/assets/fonts/Tajawal-Bold.ttf b/artifacts/api-server/assets/fonts/Tajawal-Bold.ttf new file mode 100644 index 00000000..081ac580 Binary files /dev/null and b/artifacts/api-server/assets/fonts/Tajawal-Bold.ttf differ diff --git a/artifacts/api-server/assets/fonts/Tajawal-Regular.ttf b/artifacts/api-server/assets/fonts/Tajawal-Regular.ttf new file mode 100644 index 00000000..747f56cf Binary files /dev/null and b/artifacts/api-server/assets/fonts/Tajawal-Regular.ttf differ diff --git a/artifacts/api-server/src/lib/pdf-renderer.ts b/artifacts/api-server/src/lib/pdf-renderer.ts index cfc5866b..eb5e2c9c 100644 --- a/artifacts/api-server/src/lib/pdf-renderer.ts +++ b/artifacts/api-server/src/lib/pdf-renderer.ts @@ -145,6 +145,16 @@ const FONT_FILES = { arabicSansBold: "NotoSansArabic-Bold.ttf", latinRegular: "DejaVuSans.ttf", latinBold: "DejaVuSans-Bold.ttf", + dinNextRegular: "DINNextLTArabic-Regular.ttf", + dinNextBold: "DINNextLTArabic-Bold.ttf", + tajawalRegular: "Tajawal-Regular.ttf", + tajawalBold: "Tajawal-Bold.ttf", + helvNeueLTArRegular: "HelveticaNeueLTArabic-Regular.ttf", + helvNeueLTArBold: "HelveticaNeueLTArabic-Bold.ttf", + helvNeueRegular: "HelveticaNeue-Regular.ttf", + helvNeueBold: "HelveticaNeue-Bold.ttf", + majallaRegular: "Majalla-Regular.ttf", + majallaBold: "Majalla-Bold.ttf", } as const; type FontFileKey = keyof typeof FONT_FILES; @@ -163,15 +173,9 @@ type FamilyMapping = { latinBold: FontFileKey; }; -// Web fonts (DIN Next LT Arabic, Tajawal, Helvetica Neue LT Arabic, -// Helvetica Neue, Majalla) are not bundled into the PDF renderer -// because we do not ship their font files server-side. Instead we map -// each web family to the closest visual stand-in already bundled here: -// sans-serif Arabic families render with NotoSansArabic; serif/Naskh -// stand-ins use NotoNaskhArabic; pure-Latin families (Helvetica Neue) -// fall back to the Naskh pair so Arabic glyphs in mixed strings still -// render correctly. This keeps PDFs readable without ballooning the -// server bundle. +// Every user-facing font family now maps to its actual bundled font +// files. The user uploaded all font assets so the PDF renders with the +// exact typeface chosen in settings — no more Noto stand-ins. const FAMILY_MAP: Record = { system: { arabicRegular: "naskhRegular", @@ -180,34 +184,34 @@ const FAMILY_MAP: Record = { latinBold: "latinBold", }, "DIN Next LT Arabic": { - arabicRegular: "arabicSansRegular", - arabicBold: "arabicSansBold", - latinRegular: "latinRegular", - latinBold: "latinBold", + arabicRegular: "dinNextRegular", + arabicBold: "dinNextBold", + latinRegular: "dinNextRegular", + latinBold: "dinNextBold", }, Tajawal: { - arabicRegular: "arabicSansRegular", - arabicBold: "arabicSansBold", - latinRegular: "latinRegular", - latinBold: "latinBold", + arabicRegular: "tajawalRegular", + arabicBold: "tajawalBold", + latinRegular: "tajawalRegular", + latinBold: "tajawalBold", }, "Helvetica Neue LT Arabic": { - arabicRegular: "arabicSansRegular", - arabicBold: "arabicSansBold", - latinRegular: "latinRegular", - latinBold: "latinBold", + arabicRegular: "helvNeueLTArRegular", + arabicBold: "helvNeueLTArBold", + latinRegular: "helvNeueLTArRegular", + latinBold: "helvNeueLTArBold", }, "Helvetica Neue": { arabicRegular: "naskhRegular", arabicBold: "naskhBold", - latinRegular: "latinRegular", - latinBold: "latinBold", + latinRegular: "helvNeueRegular", + latinBold: "helvNeueBold", }, Majalla: { - arabicRegular: "naskhRegular", - arabicBold: "naskhBold", - latinRegular: "latinRegular", - latinBold: "latinBold", + arabicRegular: "majallaRegular", + arabicBold: "majallaBold", + latinRegular: "majallaRegular", + latinBold: "majallaBold", }, }; @@ -658,36 +662,46 @@ export async function renderSchedulePdf(input: RenderPdfInput): Promise function drawFooter(): void { const longDate = formatLongDate(input.date); - const labelSize = Math.max(11, Math.round(input.font.fontSize * 0.95)); - const dateSize = Math.max(11, Math.round(input.font.fontSize * 0.95)); - const footerHeight = labelSize * 1.4 + dateSize * 1.4; + const sz = Math.max(11, Math.round(input.font.fontSize * 0.95)); + const lineStep = sz * 1.5; + const footerHeight = lineStep * 2 + 4; const footerWidth = doc.page.width - doc.page.margins.left - doc.page.margins.right; const footerY = doc.page.height - doc.page.margins.bottom - footerHeight; const footerAlign: "left" | "right" = baseDir === "rtl" ? "right" : "left"; - drawMixedLine(doc, input.labels.recordedBy, { - x: doc.page.margins.left, - y: footerY, + + // Temporarily zero-out the bottom margin AND save/restore doc.y so + // PDFKit never auto-paginates when we render near the page bottom. + const savedBottom = doc.page.margins.bottom; + const savedY = doc.y; + doc.page.margins.bottom = 0; + + // --- Label line ("مقيد" / "Recorded by") --- + const labelIsArabic = [...input.labels.recordedBy].filter(isArabicChar).length * 2 > input.labels.recordedBy.length; + const labelFont = fontKeyFor(labelIsArabic, "bold", mapping); + const features = featuresFor(labelIsArabic); + doc.font(labelFont).fontSize(sz).fillColor("black"); + doc.text(input.labels.recordedBy, doc.page.margins.left, footerY, { width: footerWidth, - fontSize: labelSize, - weight: "bold", align: footerAlign, - baseDirection: baseDir, - mapping, + lineBreak: false, + ...(features ? { features } : {}), }); - drawMixedLine(doc, longDate, { - x: doc.page.margins.left, - y: footerY + labelSize * 1.4, + + // --- Date line ("3 May 2026") --- + // Reset doc.y BEFORE the second call so PDFKit doesn't think + // we've exceeded the page boundary. + doc.y = footerY; + const dateFont = fontKeyFor(false, "regular", mapping); + doc.font(dateFont).fontSize(sz).fillColor("black"); + doc.text(longDate, doc.page.margins.left, footerY + lineStep, { width: footerWidth, - fontSize: dateSize, - weight: "regular", align: footerAlign, - // Force LTR for the date — the long-form date is always Latin - // ("3 May 2026"), so an RTL base direction would visually reorder - // the runs ("May 2026 3"). Alignment still mirrors the locale. - baseDirection: "ltr", - mapping, + lineBreak: false, }); + + doc.page.margins.bottom = savedBottom; + doc.y = savedY; } // ---- Table layout ----