Setting the file. One moment. Drawio To PNG · Drawio · github/awesome-copilot · Skills Docsscripts/drawio-to-png.mjs
JavaScript·361 lines·12 KB
14
import
{ join, basename, dirname, resolve }
from
"path"
;
15import { spawnSync } from "child_process";
16import { inflateRawSync } from "zlib";
17import puppeteer from "puppeteer-core";
18
19// --- Build HTML that uses the official draw.io viewer for rendering ---
20function buildViewerHtml(rawFileContent) {
21 // Escape for embedding in a JS template literal
22 const escaped = rawFileContent
23 .replace(/\\/g, "\\\\")
24 .replace(/`/g, "\\`")
25 .replace(/\$/g, "\\$");
26
27 // The official draw.io viewer (viewer-static.min.js) contains the full mxGraph
28 // rendering engine — it handles orthogonal edge routing, all shape types,
29 // container layouts, and compressed/uncompressed diagram formats.
30 return `<!DOCTYPE html>
31<html>
32<head>
33 <meta charset="utf-8">
34 <style>
35 * { margin: 0; padding: 0; }
36 body { background: white; }
37 </style>
38</head>
39<body>
40 <div id="diagram-host"></div>
41 <script>
42 // Prepare diagram XML and set up the viewer target div
43 (function() {
44 var raw = \`${escaped}\`;
45
46 // Wrap raw mxGraphModel in mxfile if needed (viewer expects mxfile format)
47 var xmlStr = raw.trim();
48 if (xmlStr.startsWith('<mxGraphModel')) {
49 xmlStr = '<mxfile><diagram name="Page-1">' + xmlStr + '</diagram></mxfile>';
50 } else if (!xmlStr.startsWith('<mxfile')) {
51 // Assume it's already an mxfile or a diagram element
52 if (xmlStr.startsWith('<diagram')) {
53 xmlStr = '<mxfile>' + xmlStr + '</mxfile>';
54 }
55 }
56
57 var config = {
58 xml: xmlStr,
59 highlight: "none",
60 nav: false,
61 resize: true,
62 toolbar: null,
63 "toolbar-nohide": true,
64 edit: null,
65 lightbox: false,
66 "auto-fit": true,
67 "check-visible-state": false
68 };
69
70 var div = document.createElement('div');
71 div.className = 'mxgraph';
72 div.setAttribute('data-mxgraph', JSON.stringify(config));
73 document.getElementById('diagram-host').appendChild(div);
74 })();
75
76 // Poll until the viewer renders the diagram (viewer script loaded separately)
77 window.__pollStarted = false;
78 window.__startPoll = function() {
79 if (window.__pollStarted) return;
80 window.__pollStarted = true;
81 // Explicitly trigger viewer processing
82 if (typeof GraphViewer !== 'undefined' && GraphViewer.processElements) {
83 GraphViewer.processElements();
84 }
85 (function poll() {
86 // Viewer places SVG directly inside .mxgraph div
87 var mxDiv = document.querySelector('.mxgraph');
88 if (mxDiv) {
89 var svg = mxDiv.querySelector('svg');
90 if (svg) {
91 var rect = mxDiv.getBoundingClientRect();
92 if (rect.width > 10 && rect.height > 10) {
93 window.__renderComplete = true;
94 window.__renderWidth = rect.width;
95 window.__renderHeight = rect.height;
96 return;
97 }
98 }
99 }
100 setTimeout(poll, 150);
101 })();
102 };
103 </script>
104</body>
105</html>`;
106}
107
108// --- Extract mxGraph XML from .drawio input (supports mxGraphModel and mxfile) ---
109function extractMxGraphModelXml(inputXml) {
110 const trimmed = inputXml.trim();
111
112 if (trimmed.startsWith("<mxGraphModel")) {
113 return trimmed;
114 }
115
116 const diagramMatch = trimmed.match(/<diagram\b[^>]*>([\s\S]*?)<\/diagram>/i);
117 if (!diagramMatch) {
118 throw new Error("Unsupported .drawio format: missing <mxGraphModel> or <diagram> content");
119 }
120
121 const diagramContent = diagramMatch[1].trim();
122
123 if (diagramContent.startsWith("<mxGraphModel")) {
124 return diagramContent;
125 }
126
127 // draw.io compressed diagrams are base64(deflateRaw(encodeURIComponent(xml)))
128 try {
129 const inflated = inflateRawSync(Buffer.from(diagramContent, "base64")).toString("utf-8");
130 const decoded = decodeURIComponent(inflated);
131 if (!decoded.trim().startsWith("<mxGraphModel")) {
132 throw new Error("decoded content is not mxGraphModel XML");
133 }
134 return decoded;
135 } catch (err) {
136 throw new Error(`Failed to decode compressed <diagram> content: ${err.message}`);
137 }
138}
139
140function resolveRenderer(rawArgs) {
141 let renderer = "auto";
142 const args = [];
143
144 for (const arg of rawArgs) {
145 if (arg.startsWith("--renderer=")) {
146 renderer = arg.substring("--renderer=".length).trim().toLowerCase();
147 continue;
148 }
149 args.push(arg);
150 }
151
152 if (!["auto", "cli", "viewer"].includes(renderer)) {
153 throw new Error(`Invalid renderer '${renderer}'. Use auto, cli, or viewer.`);
154 }
155
156 return { renderer, args };
157}
158
159function findDrawioCliPath() {
160 const envPath = process.env.DRAWIO_PATH;
161 if (envPath) {
162 try {
163 if (statSync(envPath).isFile()) return envPath;
164 } catch { /* ignore */ }
165 }
166
167 const candidates = [
168 "C:\\Program Files\\draw.io\\draw.io.exe",
169 "C:\\Program Files (x86)\\draw.io\\draw.io.exe",
170 "/Applications/draw.io.app/Contents/MacOS/draw.io",
171 "/usr/bin/drawio",
172 "/usr/local/bin/drawio",
173 ];
174
175 for (const p of candidates) {
176 try {
177 if (statSync(p).isFile()) return p;
178 } catch { /* ignore */ }
179 }
180
181 const locator = process.platform === "win32" ? "where" : "which";
182 const names = process.platform === "win32" ? ["drawio", "draw.io"] : ["drawio"];
183
184 for (const name of names) {
185 const probe = spawnSync(locator, [name], { encoding: "utf-8" });
186 if (probe.status === 0 && probe.stdout) {
187 const first = probe.stdout.split(/\r?\n/).map(line => line.trim()).find(Boolean);
188 if (first) return first;
189 }
190 }
191
192 return null;
193}
194
195function exportWithDrawioCli(drawioPath, input, output) {
196 const args = ["-x", "-f", "png", "-e", "-b", "10", "-o", output, input];
197 const result = spawnSync(drawioPath, args, { encoding: "utf-8" });
198 if (result.status !== 0) {
199 const stderr = (result.stderr || "").trim();
200 const stdout = (result.stdout || "").trim();
201 throw new Error(stderr || stdout || `draw.io CLI failed with exit code ${result.status}`);
202 }
203}
204
205// --- Main ---
206async function main() {
207 const parsed = resolveRenderer(process.argv.slice(2));
208 const renderer = parsed.renderer;
209 const args = parsed.args;
210
211 let files = [];
212 if (args[0] === "--dir") {
213 const dir = resolve(args[1] || ".");
214 files = readdirSync(dir)
215 .filter(f => f.endsWith(".drawio"))
216 .map(f => ({
217 input: join(dir, f),
218 output: join(dir, f.replace(/\.drawio$/, ".drawio.png"))
219 }));
220 } else if (args[0]) {
221 const input = resolve(args[0]);
222 const output = args[1] || input.replace(/\.drawio$/, ".drawio.png");
223 files = [{ input, output }];
224 } else {
225 console.error("Usage: node drawio-to-png.mjs <input.drawio> [output.png]");
226 console.error(" node drawio-to-png.mjs --dir <directory>");
227 console.error(" node drawio-to-png.mjs --renderer=cli|auto|custom <input.drawio> [output.png]");
228 process.exit(1);
229 }
230
231 if (files.length === 0) {
232 console.log("No .drawio files found.");
233 return;
234 }
235
236 const drawioCliPath = findDrawioCliPath();
237
238 // --- Path 1: draw.io CLI (best fidelity, no network needed) ---
239 if (renderer === "cli" || (renderer === "auto" && drawioCliPath)) {
240 if (!drawioCliPath) {
241 console.error("draw.io CLI not found. Install draw.io desktop or set DRAWIO_PATH.");
242 process.exit(1);
243 }
244 console.log(`Using renderer: draw.io CLI (${basename(drawioCliPath)})`);
245 for (const { input, output } of files) {
246 console.log(`Rendering: ${basename(input)}`);
247 try {
248 exportWithDrawioCli(drawioCliPath, input, output);
249 let kb = "?";
250 try {
251 kb = (statSync(output).size / 1024).toFixed(0);
252 } catch { /* ignore size read errors */ }
253 console.log(` -> ${basename(output)} (${kb} KB)`);
254 } catch (err) {
255 console.error(` Error rendering ${basename(input)}: ${err.message}`);
256 }
257 }
258 console.log("Done.");
259 return;
260 }
261
262 // --- Path 2: Official draw.io viewer in headless browser ---
263 // Find browser
264 const browserPaths = [
265 process.env.CHROME_PATH,
266 process.env.EDGE_PATH,
267 "C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe",
268 "C:\\Program Files\\Microsoft\\Edge\\Application\\msedge.exe",
269 "C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe",
270 "C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe",
271 "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
272 "/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge",
273 "/usr/bin/google-chrome",
274 "/usr/bin/chromium-browser",
275 "/usr/bin/microsoft-edge",
276 ].filter(Boolean);
277
278 let execPath;
279 for (const p of browserPaths) {
280 try {
281 if (statSync(p).isFile()) { execPath = p; break; }
282 } catch { /* not found */ }
283 }
284
285 if (!execPath) {
286 console.error("No browser found. Set CHROME_PATH or EDGE_PATH environment variable.");
287 process.exit(1);
288 }
289
290 console.log(`Using renderer: draw.io viewer (${basename(execPath)})`);
291
292 const browser = await puppeteer.launch({
293 executablePath: execPath,
294 headless: true,
295 args: ["--no-sandbox", "--disable-setuid-sandbox", "--disable-gpu"],
296 });
297
298 for (const { input, output } of files) {
299 console.log(`Rendering: ${basename(input)}`);
300 try {
301 const rawContent = readFileSync(input, "utf-8");
302 const html = buildViewerHtml(rawContent);
303
304 const page = await browser.newPage();
305 await page.setViewport({ width: 2400, height: 1600, deviceScaleFactor: 2 });
306
307 // Set HTML content first (sets up the .mxgraph div with diagram XML)
308 await page.setContent(html, { waitUntil: "domcontentloaded" });
309
310 // Load the official draw.io viewer JS via addScriptTag (more reliable than inline src)
311 const VIEWER_URL = "https://viewer.diagrams.net/js/viewer-static.min.js";
312 try {
313 await page.addScriptTag({ url: VIEWER_URL });
314 } catch (scriptErr) {
315 throw new Error(`Failed to load draw.io viewer JS: ${scriptErr.message}`);
316 }
317
318 // Start polling for the rendered diagram
319 await page.evaluate(() => window.__startPoll());
320
321 // Wait for the viewer to finish rendering
322 await page.waitForFunction(() => window.__renderComplete === true, { timeout: 30000 });
323
324 // Check rendering succeeded
325 const viewerOk = await page.evaluate(() => window.__renderWidth > 0);
326 if (!viewerOk) {
327 throw new Error("draw.io viewer failed to load or render (check network access)");
328 }
329
330 // Take element screenshot of just the diagram div for exact bounds
331 const containerHandle = await page.$('.mxgraph');
332 let pngBuffer;
333
334 if (containerHandle) {
335 pngBuffer = await containerHandle.screenshot({ type: "png" });
336 } else {
337 // Fallback: full-page screenshot
338 const dims = await page.evaluate(() => ({
339 w: Math.ceil(window.__renderWidth),
340 h: Math.ceil(window.__renderHeight)
341 }));
342 pngBuffer = await page.screenshot({
343 type: "png",
344 clip: { x: 0, y: 0, width: dims.w + 20, height: dims.h + 20 },
345 });
346 }
347
348 writeFileSync(output, pngBuffer);
349 console.log(` -> ${basename(output)} (${(pngBuffer.length / 1024).toFixed(0)} KB)`);
350
351 await page.close();
352 } catch (err) {
353 console.error(` Error rendering ${basename(input)}: ${err.message}`);
354 }
355 }
356
357 await browser.close();
358 console.log("Done.");
359}
360
361main().catch(err => { console.error(err); process.exit(1); });