Files
2026-09-13 08:30:18 +05:30

88 lines
3.0 KiB
TypeScript

// 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)
}