import { existsSync } from "node:fs"; import { join } from "node:path"; import type PDFDocumentType from "pdfkit"; import { isMostlyAscii, pdfKitRtlKeyValue, pdfKitRtlParagraph, } from "./persian-pdf-text"; // pdfkit publishes CJS (`module.exports = PDFDocument`) without a `.default` export. // eslint-disable-next-line @typescript-eslint/no-require-imports const PDFDocument = require("pdfkit") as typeof PDFDocumentType; const PAGE_WIDTH = 595.28; const MARGIN = 50; const CONTENT_WIDTH = PAGE_WIDTH - MARGIN * 2; const LABEL_COLUMN_WIDTH = CONTENT_WIDTH * 0.44; const VALUE_COLUMN_WIDTH = CONTENT_WIDTH * 0.5; const VALUE_COLUMN_X = MARGIN; const COLON_X = MARGIN + VALUE_COLUMN_WIDTH + 4; const LABEL_COLUMN_X = COLON_X + 10; function resolveFontFile(variant: "regular" | "bold"): string { const file = variant === "bold" ? "NotoKufiArabic-Bold.ttf" : "NotoKufiArabic-Regular.ttf"; const candidates = [ join(process.cwd(), "assets", "fonts", file), join(process.cwd(), "dist", "fonts", file), join(process.cwd(), "dist", "assets", "fonts", file), join("/usr/share/fonts/truetype/noto", file), ]; for (const path of candidates) { if (existsSync(path)) return path; } throw new Error( `Persian PDF font not found (${file}). Place it under assets/fonts/ or install noto fonts on the host.`, ); } export type PersianPdfDocument = InstanceType & { addTitle: (text: string) => void; addSection: (text: string) => void; addKeyValue: (label: string, value?: string | number | null) => void; addBlank: (lines?: number) => void; }; function drawRtlLine( doc: InstanceType, text: string, fontSize: number, bold: boolean, ): void { const y = doc.y; doc .font(bold ? "FaBold" : "FaRegular") .fontSize(fontSize) .text(pdfKitRtlParagraph(text), MARGIN, y, { width: CONTENT_WIDTH, align: "right", lineBreak: false, }); doc.moveDown(fontSize > 12 ? 0.55 : 0.45); } function drawKeyValueRow( doc: InstanceType, label: string, value: string, ): void { const y = doc.y; const { label: faLabel, value: faValue } = pdfKitRtlKeyValue(label, value); if (isMostlyAscii(faValue)) { doc.font("Helvetica").fontSize(10).text(faValue, VALUE_COLUMN_X, y, { width: VALUE_COLUMN_WIDTH, align: "left", lineBreak: false, }); } else { doc.font("FaRegular").fontSize(10).text(faValue, VALUE_COLUMN_X, y, { width: VALUE_COLUMN_WIDTH, align: "left", lineBreak: false, }); } doc.font("Helvetica").fontSize(10).text(":", COLON_X, y, { lineBreak: false, }); doc.font("FaRegular").fontSize(10).text(faLabel, LABEL_COLUMN_X, y, { width: LABEL_COLUMN_WIDTH, align: "right", lineBreak: false, }); doc.moveDown(0.5); } export function createPersianPdfDocument(): PersianPdfDocument { const doc = new PDFDocument({ size: "A4", margin: MARGIN, autoFirstPage: true, }) as PersianPdfDocument; doc.registerFont("FaRegular", resolveFontFile("regular")); doc.registerFont("FaBold", resolveFontFile("bold")); doc.font("FaRegular"); doc.addTitle = (text: string) => { doc.moveDown(0.2); drawRtlLine(doc, text, 16, true); doc.font("FaRegular").fontSize(10); doc.moveDown(0.15); }; doc.addSection = (text: string) => { doc.moveDown(0.5); drawRtlLine(doc, text, 13, true); doc.font("FaRegular").fontSize(10); doc.moveDown(0.1); }; doc.addKeyValue = (label: string, value?: string | number | null) => { const v = value === undefined || value === null || value === "" ? "-" : String(value); drawKeyValueRow(doc, label, v); }; doc.addBlank = (lines = 1) => { doc.moveDown(lines * 0.35); }; return doc; } export function persianPdfToBuffer( doc: InstanceType, ): Promise { return new Promise((resolve, reject) => { const chunks: Buffer[] = []; doc.on("data", (chunk: Buffer) => chunks.push(chunk)); doc.on("end", () => resolve(Buffer.concat(chunks))); doc.on("error", reject); doc.end(); }); }