#!/usr/bin/env -S deno run --allow-read --allow-write import { parseArgs } from "jsr:@std/cli@1.0.9/parse-args"; import { dirname, resolve } from "jsr:@std/path@1.0.8"; import { PDFDocument, StandardFonts, rgb, degrees, PageSizes, } from "npm:pdf-lib@1.17.1"; // === Constants === const FONT_MAP = { "Helvetica": StandardFonts.Helvetica, "HelveticaBold": StandardFonts.HelveticaBold, "HelveticaOblique": StandardFonts.HelveticaOblique, "TimesRoman": StandardFonts.TimesRoman, "TimesBold": StandardFonts.TimesRomanBold, "Courier": StandardFonts.Courier, "CourierBold": StandardFonts.CourierBold, }; const PAGE_SIZES = { "A4": PageSizes.A4, "Letter": PageSizes.Letter, "Legal": PageSizes.Legal, }; function getColor(color) { if (!color) return rgb(0, 0, 0); return rgb(color.r, color.g, color.b); } async function main() { const args = parseArgs(Deno.args, { boolean: ["verbose"], alias: { v: "verbose" }, }); if (args._.length < 2) { console.error("Usage: generate-scratch.ts "); Deno.exit(1); } const specPath = String(args._[0]); const outputPath = String(args._[1]); const specDir = dirname(resolve(specPath)); const specText = await Deno.readTextFile(specPath); const spec = JSON.parse(specText); const pdfDoc = await PDFDocument.create(); // Embed all standard fonts upfront const fonts = new Map(); for (const [name, fontName] of Object.entries(FONT_MAP)) { fonts.set(name, await pdfDoc.embedFont(fontName)); } for (const pageSpec of spec.pages) { let size = PageSizes.Letter; if (pageSpec.size && PAGE_SIZES[pageSpec.size]) { size = PAGE_SIZES[pageSpec.size]; } else if (Array.isArray(pageSpec.size)) { size = pageSpec.size; } const page = pdfDoc.addPage(size); for (const el of pageSpec.elements) { if (el.type === "text") { const font = fonts.get(el.font || "Helvetica") || fonts.get("Helvetica"); page.drawText(el.text, { x: el.x, y: el.y, size: el.fontSize || 12, font: font, color: getColor(el.color), maxWidth: el.maxWidth, lineHeight: el.lineHeight, rotate: el.rotate ? degrees(el.rotate) : undefined, }); } else if (el.type === "rectangle") { page.drawRectangle({ x: el.x, y: el.y, width: el.width, height: el.height, color: el.color ? getColor(el.color) : undefined, borderColor: el.borderColor ? getColor(el.borderColor) : undefined, borderWidth: el.borderWidth || 1, opacity: el.opacity, }); } else if (el.type === "line") { page.drawLine({ start: { x: el.startX, y: el.startY }, end: { x: el.endX, y: el.endY }, color: getColor(el.color), thickness: el.thickness || 1, opacity: el.opacity, }); } else if (el.type === "image") { const imagePath = resolve(specDir, el.path); const imageData = await Deno.readFile(imagePath); const ext = el.path.toLowerCase().split(".").pop(); let image; if (ext === "png") image = await pdfDoc.embedPng(imageData); else image = await pdfDoc.embedJpg(imageData); const dims = image.scale(1); page.drawImage(image, { x: el.x, y: el.y, width: el.width || dims.width, height: el.height || dims.height, opacity: el.opacity, rotate: el.rotate ? degrees(el.rotate) : undefined, }); } else if (el.type === "table") { const font = fonts.get("Helvetica"); const fontSize = el.fontSize || 10; const rowHeight = el.rowHeight || 20; const padding = el.padding || 5; const borderColor = el.borderColor ? getColor(el.borderColor) : rgb(0, 0, 0); let currentY = el.y; for (let rowIndex = 0; rowIndex < el.rows.length; rowIndex++) { const row = el.rows[rowIndex]; let currentX = el.x; for (let colIndex = 0; colIndex < row.length; colIndex++) { const cellWidth = el.columnWidths[colIndex] || 100; const cellText = row[colIndex] || ""; // Draw cell background for header if (rowIndex === 0 && el.headerBackground) { page.drawRectangle({ x: currentX, y: currentY - rowHeight, width: cellWidth, height: rowHeight, color: getColor(el.headerBackground), }); } // Draw cell border page.drawRectangle({ x: currentX, y: currentY - rowHeight, width: cellWidth, height: rowHeight, borderColor, borderWidth: 0.5, }); // Draw cell text page.drawText(cellText, { x: currentX + padding, y: currentY - rowHeight + padding + 2, size: fontSize, font, color: rgb(0, 0, 0), }); currentX += cellWidth; } currentY -= rowHeight; } } } } const pdfBytes = await pdfDoc.save(); await Deno.writeFile(outputPath, pdfBytes); console.log(`Created: ${outputPath}`); } main();