1
0
forked from Yara724/api
Files
yara724-api/src/helpers/persian-pdf-document.ts
SepehrYahyaee 2fc6015213 PDF generation
2026-06-23 15:56:19 +03:30

153 lines
4.1 KiB
TypeScript

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<typeof PDFDocument> & {
addTitle: (text: string) => void;
addSection: (text: string) => void;
addKeyValue: (label: string, value?: string | number | null) => void;
addBlank: (lines?: number) => void;
};
function drawRtlLine(
doc: InstanceType<typeof PDFDocument>,
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<typeof PDFDocument>,
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<typeof PDFDocument>,
): Promise<Buffer> {
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();
});
}