|
#!/usr/bin/env node |
|
/** |
|
* pen2pptx - Convert .pen design files to PowerPoint presentations |
|
* |
|
* Usage: |
|
* node pen2pptx.js <input.json> [output.pptx] |
|
* |
|
* Input: JSON exported from .pen file via batch_get (with layout: "none") |
|
* Output: PowerPoint file (.pptx) |
|
* |
|
* Supports: |
|
* - Frames/rectangles with fill and stroke |
|
* - Text with font, size, color, weight |
|
* - Absolute positioning (1920x1080 → 16:9 slide) |
|
* |
|
* Requires: npm install pptxgenjs |
|
*/ |
|
|
|
const PptxGenJS = require("pptxgenjs"); |
|
const fs = require("fs"); |
|
const path = require("path"); |
|
|
|
// Slide dimensions: 1920x1080 design → 13.33" x 7.5" (16:9) |
|
const SLIDE_W = 13.33; |
|
const SLIDE_H = 7.5; |
|
const DESIGN_W = 1920; |
|
const DESIGN_H = 1080; |
|
|
|
// Conversion utilities |
|
const toX = px => (px / DESIGN_W) * SLIDE_W; |
|
const toY = px => (px / DESIGN_H) * SLIDE_H; |
|
const toW = px => (px / DESIGN_W) * SLIDE_W; |
|
const toH = px => (px / DESIGN_H) * SLIDE_H; |
|
const toPt = px => Math.round(px * 0.75); // px to points |
|
const toColor = hex => hex?.replace("#", "").toUpperCase(); |
|
|
|
// Font mapping (design fonts → safe PowerPoint fonts) |
|
const FONT_MAP = { |
|
"Geist": "Calibri", |
|
"JetBrains Mono": "Consolas", |
|
"Inter": "Arial", |
|
"SF Pro": "Arial", |
|
}; |
|
|
|
function mapFont(fontFamily) { |
|
return FONT_MAP[fontFamily] || fontFamily || "Arial"; |
|
} |
|
|
|
// Add a single node to the slide |
|
function addNode(slide, node) { |
|
if (!node || node.enabled === false) return; |
|
|
|
const x = toX(node.x || 0); |
|
const y = toY(node.y || 0); |
|
const w = node.width ? toW(node.width) : undefined; |
|
const h = node.height ? toH(node.height) : undefined; |
|
|
|
// Handle text nodes |
|
if (node.type === "text" && node.content) { |
|
const fontSize = node.fontSize || 14; |
|
|
|
// Calculate width based on content length if not specified |
|
const charWidth = fontSize * 0.008; |
|
const estimatedW = Math.min(12, Math.max(1, node.content.length * charWidth)); |
|
const textW = w || estimatedW; |
|
|
|
// Estimate height based on potential line wrapping |
|
const charsPerLine = textW / charWidth; |
|
const lines = Math.ceil(node.content.length / charsPerLine); |
|
const lineHeight = toPt(fontSize) / 72 * 1.4; |
|
const textH = h || Math.max(0.3, lines * lineHeight); |
|
|
|
slide.addText(node.content, { |
|
x, y, |
|
w: textW, |
|
h: textH, |
|
fontSize: toPt(fontSize), |
|
fontFace: mapFont(node.fontFamily), |
|
color: toColor(node.fill) || "000000", |
|
bold: node.fontWeight === "bold" || node.fontWeight === "700", |
|
valign: "top", |
|
wrap: true, |
|
}); |
|
return; |
|
} |
|
|
|
// Handle frame/rectangle nodes |
|
if ((node.type === "frame" || node.type === "rectangle") && node.fill) { |
|
const opts = { |
|
x, y, |
|
w: w || 1, |
|
h: h || 1, |
|
fill: { color: toColor(node.fill) }, |
|
}; |
|
|
|
if (node.stroke?.fill) { |
|
opts.line = { |
|
color: toColor(node.stroke.fill), |
|
width: node.stroke.thickness || 1, |
|
}; |
|
} |
|
|
|
if (node.cornerRadius) { |
|
const radius = Array.isArray(node.cornerRadius) ? node.cornerRadius[0] : node.cornerRadius; |
|
if (radius > 0) { |
|
opts.rectRadius = Math.min(0.5, radius / 100); |
|
} |
|
} |
|
|
|
slide.addShape("rect", opts); |
|
} |
|
} |
|
|
|
// Generate PPTX from slide data |
|
function generatePptx(slideData, outputPath) { |
|
const pptx = new PptxGenJS(); |
|
pptx.layout = "LAYOUT_WIDE"; // 16:9 |
|
|
|
const slide = pptx.addSlide(); |
|
|
|
// Set background |
|
if (slideData.fill) { |
|
slide.background = { color: toColor(slideData.fill) }; |
|
} |
|
|
|
// Add all child nodes |
|
if (slideData.children) { |
|
for (const node of slideData.children) { |
|
addNode(slide, node); |
|
} |
|
} |
|
|
|
return pptx.writeFile({ fileName: outputPath }); |
|
} |
|
|
|
// CLI entry point |
|
async function main() { |
|
const args = process.argv.slice(2); |
|
|
|
if (args.length === 0) { |
|
console.log("Usage: node pen2pptx.js <input.json> [output.pptx]"); |
|
console.log("\nInput: JSON exported from .pen file via batch_get"); |
|
console.log("Output: PowerPoint file (default: output.pptx)"); |
|
process.exit(1); |
|
} |
|
|
|
const inputPath = args[0]; |
|
const outputPath = args[1] || "output.pptx"; |
|
|
|
if (!fs.existsSync(inputPath)) { |
|
console.error(`Error: Input file not found: ${inputPath}`); |
|
process.exit(1); |
|
} |
|
|
|
try { |
|
const jsonData = JSON.parse(fs.readFileSync(inputPath, "utf-8")); |
|
|
|
// Handle array wrapper (batch_get returns array) |
|
const slideData = Array.isArray(jsonData) ? jsonData[0] : jsonData; |
|
|
|
await generatePptx(slideData, outputPath); |
|
console.log(`Created: ${outputPath}`); |
|
} catch (err) { |
|
console.error("Error:", err.message); |
|
process.exit(1); |
|
} |
|
} |
|
|
|
main(); |