181 lines
5.3 KiB
TypeScript
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();
|