Skip to content

Instantly share code, notes, and snippets.

@oneryalcin
Last active February 3, 2026 11:17
Show Gist options
  • Select an option

  • Save oneryalcin/ae6918ace20b371c57ec3d7afcc7fdda to your computer and use it in GitHub Desktop.

Select an option

Save oneryalcin/ae6918ace20b371c57ec3d7afcc7fdda to your computer and use it in GitHub Desktop.
pen2pptx: Convert .pen design files to PowerPoint presentations

pen2pptx

Convert .pen design files to PowerPoint presentations.

Installation

npm install pptxgenjs

Usage

node pen2pptx.js <input.json> [output.pptx]

How It Works

Option 1: Absolute Positioning (Recommended for slides)

Design your slide in .pen with layout: "none" and fixed dimensions (1920×1080). Each element has explicit x, y, width, height values.

.pen design (layout: "none")
    ↓
batch_get → JSON
    ↓
pen2pptx.js → PPTX

Option 2: Flex Layout (Requires extra step)

If your design uses flex layout (layout: "vertical" or layout: "horizontal"), you need to get computed positions first:

.pen design (flex layout)
    ↓
snapshot_layout → computed positions (JSON)
batch_get → content data (JSON)
    ↓
Merge positions + content
    ↓
pen2pptx.js → PPTX

Supported Elements

.pen type PPTX output
text Text box with font, size, color, weight
frame with fill Rectangle shape
rectangle Rectangle shape

Conversion Details

  • Dimensions: 1920×1080 px → 13.33" × 7.5" (16:9 slide)
  • Font size: px × 0.75 = pt
  • Colors: #RRGGBBRRGGBB
  • Font mapping: Geist→Calibri, Inter→Arial, Space Grotesk→Arial

Example

# Export from .pen via MCP batch_get
# Save as slide.json

# Convert to PPTX
node pen2pptx.js slide.json presentation.pptx

Limitations

  • No image support (yet)
  • No icon fonts (yet)
  • No gradients
  • Text width estimation may need tuning for some fonts
{
"fill": "#FFFFFF",
"height": 1080,
"layout": "none",
"name": "Example Slide",
"type": "frame",
"width": 1920,
"children": [
{
"type": "text",
"content": "Electric vehicle adoption is accelerating faster than expected",
"x": 60,
"y": 40,
"fontFamily": "Georgia",
"fontSize": 32,
"fontWeight": "700",
"fill": "#1a1a1a"
},
{
"type": "frame",
"x": 60,
"y": 100,
"width": 1800,
"height": 2,
"fill": "#1a1a1a"
},
{
"type": "frame",
"x": 60,
"y": 140,
"width": 280,
"height": 140,
"fill": "#F5F5F5"
},
{
"type": "text",
"content": "Battery cost decline",
"x": 80,
"y": 155,
"fontFamily": "Geist",
"fontSize": 12,
"fill": "#666666"
},
{
"type": "text",
"content": "-89%",
"x": 80,
"y": 180,
"fontFamily": "Geist",
"fontSize": 48,
"fontWeight": "700",
"fill": "#0066CC"
},
{
"type": "text",
"content": "since 2010",
"x": 80,
"y": 240,
"fontFamily": "Geist",
"fontSize": 12,
"fill": "#666666"
},
{
"type": "frame",
"x": 360,
"y": 140,
"width": 280,
"height": 140,
"fill": "#F5F5F5"
},
{
"type": "text",
"content": "Global EV market share",
"x": 380,
"y": 155,
"fontFamily": "Geist",
"fontSize": 12,
"fill": "#666666"
},
{
"type": "text",
"content": "18%",
"x": 380,
"y": 180,
"fontFamily": "Geist",
"fontSize": 48,
"fontWeight": "700",
"fill": "#0066CC"
},
{
"type": "text",
"content": "of new car sales",
"x": 380,
"y": 240,
"fontFamily": "Geist",
"fontSize": 12,
"fill": "#666666"
},
{
"type": "text",
"content": "Source: IEA Global EV Outlook 2024",
"x": 60,
"y": 1040,
"fontFamily": "Geist",
"fontSize": 10,
"fill": "#999999"
}
]
}
#!/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();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment