forked from Yara724/api
PDF generation
This commit is contained in:
152
src/helpers/persian-pdf-document.ts
Normal file
152
src/helpers/persian-pdf-document.ts
Normal file
@@ -0,0 +1,152 @@
|
||||
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();
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user