feat(excalidraw): improve diagram quality with canvas measurement, validation, and conventions
Replace the charCount * fontSize * 0.55 text sizing heuristic with canvas-based measurement (graceful fallback when native deps unavailable). Add validate.mjs for automated spatial checks (text overflow, arrow-text collisions, element overlap). Update element format reference with sizing rules, label guidelines, and arrow routing conventions. Add verification step to SKILL.md workflow. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,90 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Export an Excalidraw JSON file to PNG using Playwright + the official Excalidraw library.
|
||||
*
|
||||
* Usage: node export_png.mjs <input.excalidraw> [output.png]
|
||||
*
|
||||
* All rendering happens locally. Diagram data never leaves the machine.
|
||||
* The Excalidraw JS library is fetched from esm.sh CDN (code only, not user data).
|
||||
*/
|
||||
|
||||
import { createRequire } from "module";
|
||||
import { readFileSync, writeFileSync, copyFileSync } from "fs";
|
||||
import { createServer } from "http";
|
||||
import { join, extname, dirname } from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const RUNTIME_DIR = join(__dirname, ".export-runtime");
|
||||
const HTML_PATH = join(__dirname, "export.html");
|
||||
|
||||
// Resolve playwright from the runtime directory, not the script's location
|
||||
const require = createRequire(join(RUNTIME_DIR, "node_modules", "playwright", "index.mjs"));
|
||||
const { chromium } = await import(join(RUNTIME_DIR, "node_modules", "playwright", "index.mjs"));
|
||||
|
||||
const inputPath = process.argv[2];
|
||||
if (!inputPath) {
|
||||
console.error("Usage: node export_png.mjs <input.excalidraw> [output.png]");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const outputPath = process.argv[3] || inputPath.replace(/\.excalidraw$/, ".png");
|
||||
|
||||
// Set up a temp serving directory
|
||||
const SERVE_DIR = join(__dirname, ".export-tmp");
|
||||
const { mkdirSync, rmSync } = await import("fs");
|
||||
mkdirSync(SERVE_DIR, { recursive: true });
|
||||
copyFileSync(HTML_PATH, join(SERVE_DIR, "export.html"));
|
||||
copyFileSync(inputPath, join(SERVE_DIR, "diagram.excalidraw"));
|
||||
|
||||
const MIME = {
|
||||
".html": "text/html",
|
||||
".json": "application/json",
|
||||
".excalidraw": "application/json",
|
||||
};
|
||||
|
||||
const server = createServer((req, res) => {
|
||||
const file = join(SERVE_DIR, req.url === "/" ? "export.html" : req.url);
|
||||
try {
|
||||
const data = readFileSync(file);
|
||||
res.writeHead(200, { "Content-Type": MIME[extname(file)] || "application/octet-stream" });
|
||||
res.end(data);
|
||||
} catch {
|
||||
res.writeHead(404);
|
||||
res.end("Not found");
|
||||
}
|
||||
});
|
||||
|
||||
server.listen(0, "127.0.0.1", async () => {
|
||||
const port = server.address().port;
|
||||
|
||||
let browser;
|
||||
try {
|
||||
browser = await chromium.launch({ headless: true });
|
||||
const page = await browser.newPage();
|
||||
|
||||
page.on("pageerror", err => console.error("Page error:", err.message));
|
||||
|
||||
await page.goto(`http://127.0.0.1:${port}`);
|
||||
|
||||
await page.waitForFunction(
|
||||
() => document.title.startsWith("READY") || document.title.startsWith("ERROR"),
|
||||
{ timeout: 30000 }
|
||||
);
|
||||
|
||||
const title = await page.title();
|
||||
if (title.startsWith("ERROR")) {
|
||||
console.error("Export failed:", title);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const dataUrl = await page.evaluate(() => window.__PNG_DATA__);
|
||||
const base64 = dataUrl.replace(/^data:image\/png;base64,/, "");
|
||||
writeFileSync(outputPath, Buffer.from(base64, "base64"));
|
||||
console.log(outputPath);
|
||||
} finally {
|
||||
if (browser) await browser.close();
|
||||
server.close();
|
||||
rmSync(SERVE_DIR, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user