227 lines
6.5 KiB
JavaScript
227 lines
6.5 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
const fs = require("fs");
|
|
const os = require("os");
|
|
const path = require("path");
|
|
const { spawnSync } = require("child_process");
|
|
|
|
const root = path.resolve(__dirname, "..");
|
|
const sourceImagePath = path.join(root, "chatlab-web", "frontend", "public", "company-logo.jpg");
|
|
const outputDir = path.join(root, "electron-launcher", "build");
|
|
const outputIco = path.join(outputDir, "icon.ico");
|
|
const outputPng = path.join(outputDir, "icon.png");
|
|
const sizes = [16, 24, 32, 48, 64, 128, 256];
|
|
const electronNodeModules = path.join(root, "electron-launcher", "node_modules");
|
|
const electronUserData =
|
|
process.env.CHATLAB_ICON_USER_DATA || path.join(os.tmpdir(), "chatlab-icon-renderer-user-data");
|
|
|
|
function electronApi() {
|
|
if (process.versions.electron) {
|
|
return require("electron");
|
|
}
|
|
return require(path.join(electronNodeModules, "electron"));
|
|
}
|
|
|
|
function resolveElectronBinary() {
|
|
return electronApi();
|
|
}
|
|
|
|
function writeIco(entries, destination) {
|
|
const headerSize = 6;
|
|
const entrySize = 16;
|
|
let offset = headerSize + entries.length * entrySize;
|
|
const header = Buffer.alloc(offset);
|
|
|
|
header.writeUInt16LE(0, 0);
|
|
header.writeUInt16LE(1, 2);
|
|
header.writeUInt16LE(entries.length, 4);
|
|
|
|
for (const [index, entry] of entries.entries()) {
|
|
const pos = headerSize + index * entrySize;
|
|
header.writeUInt8(entry.size === 256 ? 0 : entry.size, pos);
|
|
header.writeUInt8(entry.size === 256 ? 0 : entry.size, pos + 1);
|
|
header.writeUInt8(0, pos + 2);
|
|
header.writeUInt8(0, pos + 3);
|
|
header.writeUInt16LE(1, pos + 4);
|
|
header.writeUInt16LE(32, pos + 6);
|
|
header.writeUInt32LE(entry.png.length, pos + 8);
|
|
header.writeUInt32LE(offset, pos + 12);
|
|
offset += entry.png.length;
|
|
}
|
|
|
|
fs.writeFileSync(destination, Buffer.concat([header, ...entries.map((entry) => entry.png)]));
|
|
}
|
|
|
|
async function renderSourcePng() {
|
|
const { app, BrowserWindow, nativeImage } = electronApi();
|
|
await app.whenReady();
|
|
|
|
const imageBytes = fs.readFileSync(sourceImagePath);
|
|
const dataUri = `data:image/jpeg;base64,${imageBytes.toString("base64")}`;
|
|
const html = `<!doctype html>
|
|
<html>
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<style>
|
|
html, body {
|
|
width: 256px;
|
|
height: 256px;
|
|
margin: 0;
|
|
overflow: hidden;
|
|
background: transparent;
|
|
}
|
|
|
|
body {
|
|
display: grid;
|
|
place-items: center;
|
|
}
|
|
|
|
img {
|
|
display: block;
|
|
width: 256px;
|
|
height: 256px;
|
|
object-fit: contain;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<img src="${dataUri}" alt="">
|
|
</body>
|
|
</html>`;
|
|
|
|
const win = new BrowserWindow({
|
|
show: false,
|
|
width: 256,
|
|
height: 256,
|
|
transparent: true,
|
|
backgroundColor: "#00000000",
|
|
resizable: false,
|
|
webPreferences: {
|
|
backgroundThrottling: false,
|
|
contextIsolation: true,
|
|
offscreen: true,
|
|
sandbox: true,
|
|
},
|
|
});
|
|
|
|
await win.loadURL(`data:text/html;charset=utf-8,${encodeURIComponent(html)}`);
|
|
await win.webContents.executeJavaScript(`
|
|
new Promise((resolve, reject) => {
|
|
const image = document.querySelector("img");
|
|
if (!image) {
|
|
reject(new Error("Icon image element was not created"));
|
|
return;
|
|
}
|
|
if (image.complete && image.naturalWidth > 0) {
|
|
setTimeout(resolve, 80);
|
|
return;
|
|
}
|
|
image.onload = () => setTimeout(resolve, 80);
|
|
image.onerror = () => reject(new Error("Icon SVG failed to render"));
|
|
})
|
|
`);
|
|
|
|
const captured = await win.webContents.capturePage({ x: 0, y: 0, width: 256, height: 256 });
|
|
win.destroy();
|
|
|
|
const normalized = nativeImage
|
|
.createFromBuffer(captured.toPNG())
|
|
.resize({ width: 256, height: 256, quality: "best" });
|
|
|
|
return normalized.toPNG();
|
|
}
|
|
|
|
async function mainElectron() {
|
|
const { app } = electronApi();
|
|
app.disableHardwareAcceleration();
|
|
app.setPath("userData", electronUserData);
|
|
app.commandLine.appendSwitch("disable-gpu");
|
|
app.commandLine.appendSwitch("disable-gpu-compositing");
|
|
app.commandLine.appendSwitch("disable-software-rasterizer");
|
|
app.commandLine.appendSwitch("disk-cache-dir", path.join(electronUserData, "cache"));
|
|
|
|
if (!fs.existsSync(sourceImagePath)) {
|
|
throw new Error(`Missing icon source: ${sourceImagePath}`);
|
|
}
|
|
|
|
fs.mkdirSync(outputDir, { recursive: true });
|
|
|
|
const { nativeImage } = electronApi();
|
|
const sourcePng = await renderSourcePng();
|
|
const sourceImage = nativeImage.createFromBuffer(sourcePng);
|
|
const pngEntries = sizes.map((size) => ({
|
|
size,
|
|
png: sourceImage.resize({ width: size, height: size, quality: "best" }).toPNG(),
|
|
}));
|
|
|
|
fs.writeFileSync(outputPng, sourcePng);
|
|
writeIco(pngEntries, outputIco);
|
|
|
|
console.log(`Generated ${path.relative(root, outputIco)} (${sizes.join(", ")} px)`);
|
|
console.log(`Generated ${path.relative(root, outputPng)} (256 px)`);
|
|
app.quit();
|
|
}
|
|
|
|
function mainNode() {
|
|
const rendererDir = fs.mkdtempSync(path.join(os.tmpdir(), "chatlab-icon-renderer-"));
|
|
const userDataDir = fs.mkdtempSync(path.join(os.tmpdir(), "chatlab-icon-user-data-"));
|
|
fs.mkdirSync(rendererDir, { recursive: true });
|
|
fs.writeFileSync(
|
|
path.join(rendererDir, "package.json"),
|
|
JSON.stringify({ name: "chatlab-icon-renderer", main: "main.js" }, null, 2),
|
|
);
|
|
fs.writeFileSync(
|
|
path.join(rendererDir, "main.js"),
|
|
`process.env.CHATLAB_ICON_RENDER = "1";\nrequire(${JSON.stringify(__filename)});\n`,
|
|
);
|
|
|
|
const electronBinary = resolveElectronBinary();
|
|
const env = { ...process.env };
|
|
delete env.ELECTRON_RUN_AS_NODE;
|
|
env.NODE_PATH = [electronNodeModules, process.env.NODE_PATH].filter(Boolean).join(path.delimiter);
|
|
env.ELECTRON_DISABLE_CRASHPAD = "1";
|
|
env.ELECTRON_ENABLE_LOGGING = "0";
|
|
env.ELECTRON_DISABLE_SECURITY_WARNINGS = "true";
|
|
env.CHATLAB_ICON_USER_DATA = userDataDir;
|
|
|
|
const result = spawnSync(
|
|
electronBinary,
|
|
[
|
|
rendererDir,
|
|
"--disable-crash-reporter",
|
|
"--disable-gpu",
|
|
"--disable-gpu-compositing",
|
|
`--user-data-dir=${userDataDir}`,
|
|
`--disk-cache-dir=${path.join(userDataDir, "cache")}`,
|
|
],
|
|
{
|
|
cwd: root,
|
|
stdio: "inherit",
|
|
windowsHide: true,
|
|
env,
|
|
},
|
|
);
|
|
|
|
fs.rmSync(rendererDir, { recursive: true, force: true });
|
|
fs.rmSync(userDataDir, { recursive: true, force: true });
|
|
|
|
if (result.error) {
|
|
throw result.error;
|
|
}
|
|
process.exit(result.status ?? 1);
|
|
}
|
|
|
|
if (process.versions.electron && process.env.CHATLAB_ICON_RENDER === "1") {
|
|
mainElectron().catch((error) => {
|
|
console.error(error);
|
|
process.exitCode = 1;
|
|
try {
|
|
electronApi().app.quit();
|
|
} catch {
|
|
process.exit(1);
|
|
}
|
|
});
|
|
} else {
|
|
mainNode();
|
|
}
|