Files
get_wechat/scripts/make-icon.cjs

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();
}