Initial upload for secondary development
This commit is contained in:
226
scripts/make-icon.cjs
Normal file
226
scripts/make-icon.cjs
Normal file
@@ -0,0 +1,226 @@
|
||||
#!/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();
|
||||
}
|
||||
Reference in New Issue
Block a user