Files
yara724api/src/helpers/simple-pdf-writer.ts
SepehrYahyaee 2fc6015213 PDF generation
2026-06-23 15:56:19 +03:30

167 lines
4.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
type PdfTextRun = {
pageIndex: number;
x: number;
y: number;
fontSize: number;
bold: boolean;
text: string;
};
/** Minimal PDF 1.4 writer (Helvetica) — no external dependencies. */
export class SimplePdfWriter {
private readonly pageWidth = 595.28;
private readonly pageHeight = 841.89;
private readonly marginX = 50;
private readonly marginTop = 50;
private readonly marginBottom = 50;
private runs: PdfTextRun[] = [];
private pageCount = 1;
private cursorY = this.pageHeight - this.marginTop;
private pageIndex = 0;
private ensureSpace(lineHeight: number): void {
if (this.cursorY - lineHeight < this.marginBottom) {
this.pageIndex += 1;
this.pageCount += 1;
this.cursorY = this.pageHeight - this.marginTop;
}
}
private addRun(text: string, fontSize: number, bold = false): void {
const lineHeight = fontSize + 4;
this.ensureSpace(lineHeight);
this.runs.push({
pageIndex: this.pageIndex,
x: this.marginX,
y: this.cursorY,
fontSize,
bold,
text,
});
this.cursorY -= lineHeight;
}
addTitle(text: string): void {
this.addRun(text, 16, true);
this.cursorY -= 4;
}
addSection(text: string): void {
this.cursorY -= 6;
this.addRun(text, 13, true);
this.cursorY -= 2;
}
addLine(text: string): void {
this.addRun(text, 10, false);
}
addKeyValue(label: string, value?: string | number | null): void {
const v =
value === undefined || value === null || value === ""
? "-"
: String(value);
this.addLine(`${label}: ${v}`);
}
addBlank(): void {
this.cursorY -= 8;
}
toBuffer(): Buffer {
const chunkById: string[] = [];
const offsets: number[] = [0];
let size = Buffer.byteLength("%PDF-1.4\n", "utf8");
const addObj = (content: string): number => {
const id = chunkById.length;
offsets.push(size);
const block = `${id} 0 obj\n${content}\nendobj\n`;
chunkById.push(block);
size += Buffer.byteLength(block, "utf8");
return id;
};
chunkById.push("%PDF-1.4\n");
const fontRegularId = addObj(
"<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica /Encoding /WinAnsiEncoding >>",
);
const fontBoldId = addObj(
"<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding >>",
);
const contentIds: number[] = [];
for (let p = 0; p < this.pageCount; p++) {
const pageRuns = this.runs.filter((r) => r.pageIndex === p);
const cmds: string[] = [];
for (const run of pageRuns) {
const font = run.bold ? "F2" : "F1";
cmds.push("BT");
cmds.push(`/${font} ${run.fontSize} Tf`);
cmds.push(`1 0 0 1 ${run.x.toFixed(2)} ${run.y.toFixed(2)} Tm`);
cmds.push(`${encodePdfText(run.text)} Tj`);
cmds.push("ET");
}
const stream = cmds.join("\n");
contentIds.push(
addObj(
`<< /Length ${Buffer.byteLength(stream, "utf8")} >>\nstream\n${stream}\nendstream`,
),
);
}
const pagesKidsToken = "__PAGES_KIDS__";
const pagesId = addObj(
`<< /Type /Pages /Kids [${pagesKidsToken}] /Count ${this.pageCount} >>`,
);
const pageIds: number[] = [];
for (let p = 0; p < this.pageCount; p++) {
pageIds.push(
addObj(
`<< /Type /Page /Parent ${pagesId} 0 R /Resources << /Font << /F1 ${fontRegularId} 0 R /F2 ${fontBoldId} 0 R >> >> /MediaBox [0 0 ${this.pageWidth} ${this.pageHeight}] /Contents ${contentIds[p]} 0 R >>`,
),
);
}
const catalogId = addObj(`<< /Type /Catalog /Pages ${pagesId} 0 R >>`);
chunkById[pagesId] = chunkById[pagesId].replace(
pagesKidsToken,
pageIds.map((id) => `${id} 0 R`).join(" "),
);
const body = chunkById.join("");
const bodyBuf = Buffer.from(body, "utf8");
const xrefAt = bodyBuf.length;
const xref: string[] = [
`xref\n0 ${offsets.length}\n0000000000 65535 f \n`,
];
for (let i = 1; i < offsets.length; i++) {
xref.push(`${String(offsets[i]).padStart(10, "0")} 00000 n \n`);
}
const trailer = `trailer\n<< /Size ${offsets.length} /Root ${catalogId} 0 R >>\nstartxref\n${xrefAt}\n%%EOF\n`;
return Buffer.concat([bodyBuf, Buffer.from(xref.join("") + trailer, "utf8")]);
}
}
/** PDF literal string for Helvetica (WinAnsi) — Latin text only. */
function encodePdfText(input: string): string {
const text = input.normalize("NFC");
const safe = [...text]
.map((ch) => {
const code = ch.charCodeAt(0);
if (code >= 0x20 && code <= 0x7e) return ch;
if (ch === "—" || ch === "") return "-";
if (/\s/.test(ch)) return " ";
return "";
})
.join("")
.replace(/\s+/g, " ")
.trim();
return `(${safe
.replace(/\\/g, "\\\\")
.replace(/\(/g, "\\(")
.replace(/\)/g, "\\)")})`;
}