// Rasterise extensions/docx-editor/icon.svg to PNGs. // // npm run icons (from addon-build/docx-editor/) // // SVG is what the dock and the catalogue use; the PNGs are for everywhere // that can't take one — a favicon fallback, a listing thumbnail, an OS file // association later on. There is no sharp/resvg on this machine, so the // rasteriser is Electron itself: a hidden window, the SVG scaled to fill it, // and capturePage(). Re-run it whenever icon.svg changes. import { execFileSync } from "node:child_process"; import { fileURLToPath } from "node:url"; import fs from "node:fs"; import path from "node:path"; import os from "node:os"; const here = path.dirname(fileURLToPath(import.meta.url)); const ROOT = path.resolve(here, "../.."); const EXT = path.resolve(ROOT, "bundled-addons/docx-editor"); const SIZES = [16, 32, 48, 128, 256]; const electron = path.join(ROOT, "node_modules", "electron", "dist", process.platform === "win32" ? "electron.exe" : "electron"); if (!fs.existsSync(electron)) { console.error(`electron not found at ${electron} — run npm install in TheseusNavigator first`); process.exit(2); } // The main script runs inside Electron; it is written to a temp dir rather // than committed, because it is scaffolding for this script and nothing else. const work = fs.mkdtempSync(path.join(os.tmpdir(), "docx-icons-")); const svg = fs.readFileSync(path.join(EXT, "icon.svg"), "utf8"); for (const size of SIZES) { fs.writeFileSync(path.join(work, `page-${size}.html`), ` ${svg}`); } fs.writeFileSync(path.join(work, "main.js"), ` const { app, BrowserWindow } = require("electron"); const fs = require("fs"), path = require("path"); const work = ${JSON.stringify(work)}; const out = ${JSON.stringify(EXT)}; const sizes = ${JSON.stringify(SIZES)}; app.disableHardwareAcceleration(); // One window, resized per icon. Creating and destroying an offscreen window // per size raced with itself: the next loadFile came back ERR_FAILED while // the previous one was still tearing down its compositor. app.whenReady().then(async () => { const win = new BrowserWindow({ width: 256, height: 256, show: false, frame: false, transparent: true, useContentSize: true, webPreferences: { offscreen: true }, }); for (const size of sizes) { win.setContentSize(size, size); await win.loadFile(path.join(work, "page-" + size + ".html")); // Offscreen windows paint asynchronously and the first frame is often // the blank one, so wait for a couple before capturing. await new Promise((r) => { let n = 0; const onPaint = () => { if (++n >= 2) { win.webContents.off("paint", onPaint); r(); } }; win.webContents.on("paint", onPaint); setTimeout(r, 1500); }); const img = await win.webContents.capturePage(); fs.writeFileSync(path.join(out, "icon-" + size + ".png"), img.toPNG()); } win.destroy(); app.quit(); }).catch((e) => { console.error("icon render failed:", e); app.exit(1); }); `); fs.writeFileSync(path.join(work, "package.json"), JSON.stringify({ name: "docx-icons", main: "main.js" })); execFileSync(electron, [work], { stdio: "inherit" }); for (const size of SIZES) { const f = path.join(EXT, `icon-${size}.png`); console.log(`${path.relative(ROOT, f)} ${fs.existsSync(f) ? (fs.statSync(f).size / 1024).toFixed(1) + " KB" : "MISSING"}`); } fs.rmSync(work, { recursive: true, force: true }); // The dock and the catalogue card read `icon` out of addon.json, and both // take a data URL (this is how Aegis ships its mark). Deriving it from // icon.svg here means there is one drawing, not two that drift apart. const minified = svg .replace(//g, "") .replace(/[\s\S]*?<\/title>/g, "") .replace(/\s+/g, " ") .replace(/>\s+</g, "><") .trim(); const dataUrl = "data:image/svg+xml;utf8," + encodeURIComponent(minified); const manifestPath = path.join(EXT, "addon.json"); const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8")); if (manifest.icon !== dataUrl) { manifest.icon = dataUrl; fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + "\n"); console.log(`addon.json icon updated (${dataUrl.length} chars)`); } else { console.log("addon.json icon already current"); }