File size: 3,754 Bytes
c0af099
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
/**
 * Generate electron/build-resources/icon.ico and icon.icns from the
 * 1024×1024 icon.png master. Run after changing icon.png:
 *
 *   npm run generate-icons
 *
 * The outputs are COMMITTED — electron-builder auto-discovers them in
 * directories.buildResources and uses them as-is. We don't rely on
 * electron-builder's own PNG→ICO conversion because it emits a single
 * 256×256 PNG-compressed entry, which several Windows shell surfaces
 * (Explorer small views, NSIS, taskbar) can't render — they fall back to
 * the default Electron icon. png2icons with forWinExe=true stores the
 * 48/32/24/16 entries as classic BMP, which is what Windows expects.
 */
import { readFileSync, writeFileSync } from "node:fs";
import { dirname, join } from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";
import png2icons from "png2icons";

// Standard Windows app-icon sizes (Microsoft recommends 16/24/32/48/256;
// 64/128 cover intermediate DPI scaling).
export const REQUIRED_ICO_SIZES = [16, 24, 32, 48, 64, 128, 256];

// The ten standard macOS representations (16 → 512@2x). is32/il32 (+ the
// l8mk/s8mk masks png2icons emits alongside) are the legacy non-retina
// 16/32 forms; ic07–ic14 cover 128 → 512@2x.
export const REQUIRED_ICNS_TYPES = [
  "is32",
  "il32",
  "ic07",
  "ic08",
  "ic09",
  "ic10",
  "ic11",
  "ic12",
  "ic13",
  "ic14",
];

/**
 * Parse an ICO buffer's ICONDIR into [{size, isPng}] — isPng distinguishes
 * PNG-compressed payloads from classic BMP entries.
 */
export function listIcoEntries(ico) {
  const entries = [];
  for (let i = 0; i < ico.readUInt16LE(4); i++) {
    const entry = 6 + i * 16;
    const width = ico[entry];
    const offset = ico.readUInt32LE(entry + 12);
    entries.push({
      size: width === 0 ? 256 : width,
      isPng: ico.readUInt32BE(offset) === 0x89504e47, // \x89PNG
    });
  }
  return entries;
}

/** List the 4-char OSType of every chunk in an ICNS buffer. */
export function listIcnsTypes(icns) {
  const types = [];
  for (let p = 8; p < icns.length; p += icns.readUInt32BE(p + 4)) {
    types.push(icns.toString("ascii", p, p + 4));
  }
  return types;
}

/**
 * Convert a PNG buffer into { ico, icns } buffers, asserting the emitted
 * size sets so a png2icons upgrade that changes them fails loudly.
 */
export function generateIcons(input) {
  // forWinExe: true → 48/32/24/16 stored as classic BMP entries (required
  // by Windows shell small-icon surfaces), ≥64 PNG-compressed. 0 = lossless.
  const ico = png2icons.createICO(input, png2icons.BICUBIC2, 0, false, true);
  const icns = png2icons.createICNS(input, png2icons.BICUBIC2, 0);
  if (!ico || !icns) throw new Error("png2icons failed to convert icon.png");

  const icoSizes = listIcoEntries(ico).map((entry) => entry.size);
  for (const size of REQUIRED_ICO_SIZES) {
    if (!icoSizes.includes(size)) throw new Error(`icon.ico missing ${size}px`);
  }
  const icnsTypes = listIcnsTypes(icns);
  for (const type of REQUIRED_ICNS_TYPES) {
    if (!icnsTypes.includes(type)) throw new Error(`icon.icns missing ${type}`);
  }

  return { ico, icns };
}

const isMainModule =
  process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href;

if (isMainModule) {
  const resDir = join(
    dirname(fileURLToPath(import.meta.url)),
    "..",
    "electron",
    "build-resources",
  );
  const { ico, icns } = generateIcons(readFileSync(join(resDir, "icon.png")));
  writeFileSync(join(resDir, "icon.ico"), ico);
  writeFileSync(join(resDir, "icon.icns"), icns);
  console.log(
    `icon.ico (${listIcoEntries(ico)
      .map((entry) => entry.size)
      .join("/")}px, ${ico.length} B), ` +
      `icon.icns (${listIcnsTypes(icns).join(",")}, ${icns.length} B)`,
  );
}