skills/pdf-generator/scripts/generate-scratch.ts

181 lines
5.3 KiB
TypeScript

#!/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 <spec.json> <output.pdf>");
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();