Files
NetMesh/electron/plugins/contributionIconRasterizer.cjs

116 lines
4.5 KiB
JavaScript
Raw Permalink Normal View History

"use strict";
const { randomUUID } = require("node:crypto");
const DEFAULT_RASTER_TIMEOUT_MS = 5_000;
function withTimeout(promise, timeoutMs, message) {
let timer;
return Promise.race([
promise,
new Promise((_, reject) => {
timer = setTimeout(() => reject(new Error(message)), timeoutMs);
}),
]).finally(() => clearTimeout(timer));
}
function browserRasterizeExpression(payload) {
return `(async (payload) => {
const image = new Image();
image.decoding = "sync";
if (payload.mimeType === "image/svg+xml") {
const bytes = Uint8Array.from(atob(payload.source), (character) => character.charCodeAt(0));
const source = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
const document = new DOMParser().parseFromString(source, "image/svg+xml");
const root = document.documentElement;
if (root.localName !== "svg" || document.querySelector("parsererror")) {
throw new Error("Plugin contribution SVG icon is invalid");
}
root.setAttribute("width", String(payload.width));
root.setAttribute("height", String(payload.height));
const normalized = new XMLSerializer().serializeToString(root);
image.src = "data:image/svg+xml;charset=utf-8," + encodeURIComponent(normalized);
} else {
image.src = "data:" + payload.mimeType + ";base64," + payload.source;
}
await image.decode();
if (image.naturalWidth !== payload.width || image.naturalHeight !== payload.height) {
throw new Error("Plugin contribution icon dimensions changed during decoding");
}
const scale = Math.min(1, payload.maxEdge / image.naturalWidth, payload.maxEdge / image.naturalHeight);
const width = Math.max(1, Math.round(image.naturalWidth * scale));
const height = Math.max(1, Math.round(image.naturalHeight * scale));
const canvas = document.createElement("canvas");
canvas.width = width;
canvas.height = height;
const context = canvas.getContext("2d", { alpha: true, desynchronized: false });
if (!context) throw new Error("Plugin contribution icon canvas is unavailable");
context.clearRect(0, 0, width, height);
context.drawImage(image, 0, 0, width, height);
return canvas.toDataURL("image/png");
})(${JSON.stringify(payload)})`;
}
function createIsolatedContributionIconRasterizer(options) {
const BrowserWindow = options?.BrowserWindow;
const randomUUIDFn = options?.randomUUID ?? randomUUID;
const timeoutMs = options?.timeoutMs ?? DEFAULT_RASTER_TIMEOUT_MS;
return async ({ body, mimeType, width, height, maxEdge }) => {
if (typeof BrowserWindow !== "function") {
throw new Error("Isolated plugin contribution icon rasterizer is unavailable");
}
const partition = `plugin-icon-raster-${randomUUIDFn()}`;
const win = new BrowserWindow({
show: false,
width: maxEdge,
height: maxEdge,
webPreferences: {
partition,
sandbox: true,
spellcheck: false,
contextIsolation: true,
nodeIntegration: false,
webSecurity: true,
allowRunningInsecureContent: false,
backgroundThrottling: false,
},
});
try {
win.removeMenu?.();
win.webContents?.session?.webRequest?.onBeforeRequest?.(
{ urls: ["http://*/*", "https://*/*", "file://*/*", "ftp://*/*"] },
(_details, callback) => callback({ cancel: true }),
);
await withTimeout(
Promise.resolve(win.loadURL("data:text/html;charset=utf-8,%3Cmeta%20http-equiv%3D%22Content-Security-Policy%22%20content%3D%22default-src%20'none'%3B%20img-src%20data%3A%3B%22%3E")),
timeoutMs,
"Plugin contribution icon rasterizer did not load in time",
);
const result = await withTimeout(
Promise.resolve(win.webContents.executeJavaScript(browserRasterizeExpression({
source: body.toString("base64"),
mimeType,
width,
height,
maxEdge,
}), true)),
timeoutMs,
"Plugin contribution icon rasterization timed out",
);
const prefix = "data:image/png;base64,";
if (typeof result !== "string" || !result.startsWith(prefix)) {
throw new Error("Plugin contribution icon rasterizer returned an invalid result");
}
return Buffer.from(result.slice(prefix.length), "base64");
} finally {
try { win.destroy(); } catch {}
}
};
}
module.exports = {
DEFAULT_RASTER_TIMEOUT_MS,
browserRasterizeExpression,
createIsolatedContributionIconRasterizer,
};