Files
Termix/scripts/generate-icons.mjs
T

117 lines
3.4 KiB
JavaScript

import sharp from "sharp";
import { readFileSync, writeFileSync, mkdirSync, copyFileSync } from "fs";
import { join, dirname } from "path";
import { fileURLToPath } from "url";
const __dirname = dirname(fileURLToPath(import.meta.url));
const root = join(__dirname, "..");
const publicDir = join(root, "public");
const iconsDir = join(publicDir, "icons");
mkdirSync(iconsDir, { recursive: true });
const svgBuffer = readFileSync(join(publicDir, "icon-square.svg"));
const render = (size) =>
sharp(svgBuffer, { density: 72 * Math.max(1, size / 1024) })
.resize(size, size)
.png({ compressionLevel: 9 })
.toBuffer();
const pngSizes = [16, 24, 32, 48, 64, 128, 256, 512, 1024];
const png = Object.fromEntries(
await Promise.all(pngSizes.map(async (s) => [s, await render(s)])),
);
for (const size of pngSizes) {
writeFileSync(join(iconsDir, `${size}x${size}.png`), png[size]);
}
writeFileSync(join(publicDir, "icon.png"), png[1024]);
writeFileSync(join(publicDir, "full-icon.png"), png[1024]);
writeFileSync(join(publicDir, "icon-mac.png"), png[512]);
writeFileSync(
join(publicDir, "favicon.ico"),
buildIco(
[16, 32, 48].map((s) => png[s]),
[16, 32, 48],
),
);
const winSizes = [16, 24, 32, 48, 64, 128, 256];
writeFileSync(
join(publicDir, "icon.ico"),
buildIco(
winSizes.map((s) => png[s]),
winSizes,
),
);
writeFileSync(
join(publicDir, "icon.icns"),
buildIcns([
["icp4", png[16]],
["icp5", png[32]],
["icp6", png[64]],
["ic07", png[128]],
["ic08", png[256]],
["ic09", png[512]],
["ic10", png[1024]],
["ic11", png[32]],
["ic12", png[64]],
["ic13", png[256]],
["ic14", png[512]],
]),
);
copyFileSync(join(publicDir, "icon.ico"), join(iconsDir, "icon.ico"));
copyFileSync(join(publicDir, "icon.icns"), join(iconsDir, "icon.icns"));
// Transparent foreground from public/icon.svg; macOS colors this template.
const traySvg = readFileSync(join(iconsDir, "trayTemplate.svg"));
for (const scale of [1, 2]) {
await sharp(traySvg, { density: 72 * scale })
.resize(16 * scale, 16 * scale)
.png()
.toFile(join(iconsDir, `trayTemplate${scale === 2 ? "@2x" : ""}.png`));
}
console.log("Icons generated from public/icon-square.svg");
function buildIco(pngBuffers, sizes) {
const headerSize = 6;
const dirEntrySize = 16;
let offset = headerSize + pngBuffers.length * dirEntrySize;
const entries = pngBuffers.map((buf, i) => {
const entry = { size: sizes[i], buf, offset };
offset += buf.length;
return entry;
});
const ico = Buffer.alloc(offset);
ico.writeUInt16LE(0, 0);
ico.writeUInt16LE(1, 2);
ico.writeUInt16LE(entries.length, 4);
entries.forEach((e, i) => {
const base = headerSize + i * dirEntrySize;
ico.writeUInt8(e.size > 255 ? 0 : e.size, base);
ico.writeUInt8(e.size > 255 ? 0 : e.size, base + 1);
ico.writeUInt16LE(1, base + 4);
ico.writeUInt16LE(32, base + 6);
ico.writeUInt32LE(e.buf.length, base + 8);
ico.writeUInt32LE(e.offset, base + 12);
e.buf.copy(ico, e.offset);
});
return ico;
}
function buildIcns(chunks) {
const parts = chunks.map(([type, data]) => {
const head = Buffer.alloc(8);
head.write(type, 0, "ascii");
head.writeUInt32BE(data.length + 8, 4);
return Buffer.concat([head, data]);
});
const body = Buffer.concat(parts);
const head = Buffer.alloc(8);
head.write("icns", 0, "ascii");
head.writeUInt32BE(body.length + 8, 4);
return Buffer.concat([head, body]);
}