// Minimal single-page PDF writer — no dependency (jspdf/pdf-lib) needed. // Builds a valid PDF byte-for-byte using the base14 Helvetica font, which every // PDF reader supports without embedding. Good enough for text/table documents // like a payslip breakdown; not a general-purpose PDF library. export interface PdfLine { text: string x: number y: number size?: number bold?: boolean color?: [number, number, number] // 0-1 RGB } const PAGE_WIDTH = 612 // US Letter, points const PAGE_HEIGHT = 792 function escapePdfText(s: string): string { return s.replace(/\\/g, "\\\\").replace(/\(/g, "\\(").replace(/\)/g, "\\)") } /** Builds a one-page PDF from absolutely-positioned text lines (origin bottom-left, like PDF space). */ export function buildSimplePdf(lines: PdfLine[]): Blob { const content: string[] = ["q"] let currentFont: "F1" | "F2" | null = null let currentSize: number | null = null let currentColor: string | null = null for (const line of lines) { const font = line.bold ? "F2" : "F1" const size = line.size ?? 11 const [r, g, b] = line.color ?? [0.07, 0.07, 0.07] const colorKey = `${r},${g},${b}` if (colorKey !== currentColor) { content.push(`${r.toFixed(3)} ${g.toFixed(3)} ${b.toFixed(3)} rg`) currentColor = colorKey } content.push("BT") if (font !== currentFont || size !== currentSize) { content.push(`/${font} ${size} Tf`) currentFont = font currentSize = size } content.push(`${line.x.toFixed(2)} ${line.y.toFixed(2)} Td`) content.push(`(${escapePdfText(line.text)}) Tj`) content.push("ET") } content.push("Q") const contentStream = content.join("\n") const objects: string[] = [] objects.push("<< /Type /Catalog /Pages 2 0 R >>") objects.push("<< /Type /Pages /Kids [3 0 R] /Count 1 >>") objects.push( `<< /Type /Page /Parent 2 0 R /MediaBox [0 0 ${PAGE_WIDTH} ${PAGE_HEIGHT}] /Resources << /Font << /F1 4 0 R /F2 5 0 R >> >> /Contents 6 0 R >>` ) objects.push("<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>") objects.push("<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica-Bold >>") objects.push(`<< /Length ${contentStream.length} >>\nstream\n${contentStream}\nendstream`) let pdf = "%PDF-1.4\n" const offsets: number[] = [0] objects.forEach((obj, i) => { offsets.push(pdf.length) pdf += `${i + 1} 0 obj\n${obj}\nendobj\n` }) const xrefStart = pdf.length let xref = `xref\n0 ${objects.length + 1}\n0000000000 65535 f \n` for (let i = 1; i <= objects.length; i++) { xref += `${offsets[i].toString().padStart(10, "0")} 00000 n \n` } pdf += xref pdf += `trailer\n<< /Size ${objects.length + 1} /Root 1 0 R >>\nstartxref\n${xrefStart}\n%%EOF` return new Blob([pdf], { type: "application/pdf" }) } export function downloadPdf(blob: Blob, filename: string) { const url = URL.createObjectURL(blob) const a = document.createElement("a") a.href = url a.download = filename a.click() URL.revokeObjectURL(url) }