From e5e7fdb4f6be0527185cab3a87bfc574b0afaa0c Mon Sep 17 00:00:00 2001 From: yoratoni Date: Fri, 7 Aug 2026 10:18:37 +0200 Subject: [PATCH] feat: icons generation from React95 icons & improved output dirs location --- .github/workflows/release.yml | 14 ++-- .vscode/settings.json | 7 ++ README.md | 2 +- electron-builder.yml | 9 ++- electron.vite.config.ts | 6 ++ package.json | 4 +- resources/icon.icns | Bin 0 -> 26080 bytes resources/icon.ico | Bin 0 -> 103369 bytes resources/icon.png | Bin 0 -> 13967 bytes resources/icons/128x128.png | Bin 0 -> 499 bytes resources/icons/16x16.png | Bin 0 -> 179 bytes resources/icons/24x24.png | Bin 0 -> 510 bytes resources/icons/256x256.png | Bin 0 -> 1219 bytes resources/icons/32x32.png | Bin 0 -> 251 bytes resources/icons/48x48.png | Bin 0 -> 531 bytes resources/icons/512x512.png | Bin 0 -> 4042 bytes resources/icons/64x64.png | Bin 0 -> 323 bytes scripts/generateIcons.ts | 77 ++++++++++++++++++++ scripts/lib/constants.ts | 109 ++++++++++++++++++++++++++++ scripts/lib/utils/icns.ts | 26 +++++++ scripts/lib/utils/ico.ts | 77 ++++++++++++++++++++ scripts/lib/utils/png.ts | 133 ++++++++++++++++++++++++++++++++++ scripts/lib/utils/raster.ts | 93 ++++++++++++++++++++++++ scripts/types/icons.d.ts | 26 +++++++ src/main/assets/icon.png | Bin 0 -> 1219 bytes src/main/config/windows.ts | 3 + src/main/types/env.d.ts | 1 + tsconfig.json | 5 ++ yarn.lock | 11 ++- 29 files changed, 590 insertions(+), 13 deletions(-) create mode 100644 resources/icon.icns create mode 100644 resources/icon.ico create mode 100644 resources/icon.png create mode 100644 resources/icons/128x128.png create mode 100644 resources/icons/16x16.png create mode 100644 resources/icons/24x24.png create mode 100644 resources/icons/256x256.png create mode 100644 resources/icons/32x32.png create mode 100644 resources/icons/48x48.png create mode 100644 resources/icons/512x512.png create mode 100644 resources/icons/64x64.png create mode 100644 scripts/generateIcons.ts create mode 100644 scripts/lib/constants.ts create mode 100644 scripts/lib/utils/icns.ts create mode 100644 scripts/lib/utils/ico.ts create mode 100644 scripts/lib/utils/png.ts create mode 100644 scripts/lib/utils/raster.ts create mode 100644 scripts/types/icons.d.ts create mode 100644 src/main/assets/icon.png create mode 100644 src/main/types/env.d.ts diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e72ec92..d7947ab 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -87,13 +87,13 @@ jobs: with: name: installers-${{ matrix.os }} path: | - dist/*.exe - dist/*.dmg - dist/*.AppImage - dist/*.deb - dist/*.zip - dist/*.blockmap - dist/latest*.yml + dist/release/*.exe + dist/release/*.dmg + dist/release/*.AppImage + dist/release/*.deb + dist/release/*.zip + dist/release/*.blockmap + dist/release/latest*.yml if-no-files-found: error retention-days: 7 diff --git a/.vscode/settings.json b/.vscode/settings.json index 56360d1..768c5eb 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -64,12 +64,19 @@ "browsable", "CHUNKID", "CHUNKIDS", + "downsample", "EXPORTMAP", "fguid", "gatherable", + "icns", + "ICONDIR", + "ICONDIRENTRY", + "iconutil", + "IEND", "konsole", "Nwnp", "OFPA", + "Paeth", "Qfecheck", "scrollview", "SERIALSIZES", diff --git a/README.md b/README.md index ebc3e74..4f4f82d 100644 --- a/README.md +++ b/README.md @@ -24,5 +24,5 @@ This project reproduces the Windows 95 look and feel, some of the icons, fonts, originate from Microsoft software and are not covered by the MIT License in [LICENSE](LICENSE), read [NOTICE.md](NOTICE.md) before forking this project or redistributing a build of it. -Cybearl is not affiliated with, endorsed by, or sponsored by Microsoft Corporation. Windows and +Cybearl is not affiliated with, endorsed by, or sponsored by Microsoft Corporation, Windows and Windows 95 are trademarks of Microsoft Corporation. diff --git a/electron-builder.yml b/electron-builder.yml index d772b4a..86016f9 100644 --- a/electron-builder.yml +++ b/electron-builder.yml @@ -3,11 +3,11 @@ productName: GitGame copyright: Copyright © ${author} directories: - output: dist - buildResources: build + output: dist/release + buildResources: resources files: - - out/**/* + - dist/app/**/* - package.json - "!**/*.map" @@ -20,6 +20,7 @@ publish: releaseType: release win: + icon: resources/icon.ico target: - target: nsis arch: [x64] @@ -33,6 +34,7 @@ nsis: deleteAppDataOnUninstall: false mac: + icon: resources/icon.icns category: public.app-category.developer-tools target: - target: dmg @@ -42,6 +44,7 @@ mac: gatekeeperAssess: false linux: + icon: resources/icons target: - target: AppImage arch: [x64] diff --git a/electron.vite.config.ts b/electron.vite.config.ts index 171ff2b..33f2724 100644 --- a/electron.vite.config.ts +++ b/electron.vite.config.ts @@ -11,6 +11,9 @@ const rendererRoot = path.resolve(srcRoot, "renderer") const react95IconsRoot = path.resolve(__dirname, "node_modules", "@react95", "icons", "png") const react95FontsRoot = path.resolve(__dirname, "node_modules", "react95", "dist", "fonts") +// Every bundle lands under a single output root +const outRoot = path.resolve(__dirname, "dist", "app") + /** * Path aliases for module resolution. */ @@ -26,9 +29,11 @@ const aliases = { export default defineConfig({ main: { resolve: { alias: aliases }, + build: { outDir: path.join(outRoot, "main") }, }, preload: { resolve: { alias: aliases }, + build: { outDir: path.join(outRoot, "preload") }, }, renderer: { root: path.resolve(__dirname, "src", "renderer"), @@ -36,6 +41,7 @@ export default defineConfig({ resolve: { alias: aliases }, plugins: [react(), tailwindcss()], build: { + outDir: path.join(outRoot, "renderer"), rollupOptions: { input: path.resolve(__dirname, "src", "renderer", "index.html"), }, diff --git a/package.json b/package.json index 4dfdf9f..2a9822f 100644 --- a/package.json +++ b/package.json @@ -17,13 +17,14 @@ "engines": { "node": ">=22.0.0" }, - "main": "./out/main/index.js", + "main": "./dist/app/main/index.js", "scripts": { "postinstall": "node node_modules/electron/install.js", "dev": "electron-vite dev", "build": "electron-vite build", "preview": "electron-vite preview", "typecheck": "tsc --noEmit", + "generate-icons": "tsx scripts/generateIcons.ts", "test": "npm run test:unit", "test:unit": "vitest", "lint": "biome check && eslint \"src/**/*.{ts,tsx}\"", @@ -61,6 +62,7 @@ "react": "^19.2.5", "react-dom": "^19.2.5", "tailwindcss": "^4.3.1", + "tsx": "^4.23.9", "typescript": "^5.8.3", "vite": "^7.0.0", "vite-tsconfig-paths": "^5.1.4", diff --git a/resources/icon.icns b/resources/icon.icns new file mode 100644 index 0000000000000000000000000000000000000000..f3ae840bb9832f9d9513e156b063d51fc1ed8c23 GIT binary patch literal 26080 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 literal 0 HcmV?d00001 diff --git a/resources/icon.ico b/resources/icon.ico new file mode 100644 index 0000000000000000000000000000000000000000..8d0765bbcaab446c5959bba7fef82962166d0fb4 GIT binary patch literal 103369 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 literal 0 HcmV?d00001 diff --git a/resources/icon.png b/resources/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..0bcad39a34899d822c5057c4801241e551491818 GIT binary patch literal 13967 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 yFJ6D;uunK>+Ky!zGejhhrqsb z7x&DJoxWsIQ)$EZ$u~=bpX+*TU8dqW368!LSU%sI8so^o(!g+G2KT*fYcm-bfd&FW z!whb{?AVHYg}v+$0fw;1iua2c3pH3F{DxwNC~+nZsN@+&g{5Ci*NT^zDnLaHxErQ? zdU%Fy$)A7be63HRq6Zc*d^^>{`s5W;zD)uXRIr<&gLTPa=S5I1!vR@_YQwpWHh+%` z!Gs>LH{5Dk&$OcMpEL_hOyG$?gwBRF-xUR6A{~_+I`)ON4`KWRQs&Jtc7p^%QK}T< z4@H=h;}|de3R;q;z}E;>@`0=2o_l3-VO$BS` znkR^!RG5;{%wUi`A^EX-4bOs{hJSYs2 d$9lF)yw&FB%=~q}vw*H(@O1TaS?83{1OP-ELeu~N literal 0 HcmV?d00001 diff --git a/resources/icons/24x24.png b/resources/icons/24x24.png new file mode 100644 index 0000000000000000000000000000000000000000..181e8d398b3e3efcd826b00a9fefc38a72840f34 GIT binary patch literal 510 zcmVf2G_XH7{PN(CCVF=D32-ZYoiO73YSQ8P=X0t3w5-gWX z$uvz7$1!@n-iWTlmIe%m!*!NrI2;bx@Auelw`U0FbUMX&JO&ZTIGIc^7z{EZT9g9t zDtEgbq9~Hdo6SZ*Jg!!&Z!mtQ-|x$F^Z8t?%q#S60*o=Rmf-s#ifct)LJI4A-_7P8jL2gJ4|Nebs;yuGEr0&;QtpS{mkDwPpWW-;FU*gDRi0A*JGS6o^5 z1K{EKW*s*Kk9^LRKJ$Ncwys*Ou5+;W`c2;*UwJusi5A@H4*&oF07*qoM6N<$g7ebg AlK=n! literal 0 HcmV?d00001 diff --git a/resources/icons/256x256.png b/resources/icons/256x256.png new file mode 100644 index 0000000000000000000000000000000000000000..458bc009b49f18f057551246eae7bfe79606d591 GIT binary patch literal 1219 zcmeAS@N?(olHy`uVBq!ia0y~yU<5K5893O0R7}x|GzJEiE>9Q7kcwMx?-~j<8}KkV z+TZnmoW5j!=ZG998bCtXwVoJ10u^!48RsgHR*7$&eVcp?*D?lQ$+-&(?; z1aycH1A_tvwduflm$h*pUNwZsOEUpo=>WtGEZ78VQ?UhVNIMs z1A_}lDGoD1QYQQj*KWOD^Ur~ClevPK3{a#2B+P-#013dsGo}ks{=5!Z{$Kv7%W%0e z!zJ)Deuy+!TQX?=YnFB9KZsu`LUuPJ!-VZ_`Vt5c0<2)h9nG42Ctf+r5m1PbwqsS; zePA)8hy8z9kf(_QeO4@7Vg{>zDGCy&9;k}1abD*JPr&A#QIrbIm0Gv z2C*1ihBtsDz$vptNP!(ohJxiX2i$U(3L6+4fI*}J%x)Oel46FB$K_zAU}V|1%pI)L sfKK3KV3+`-6gU|sF({B&WSH!f=ilnXe>vmzYLJ&aUHx3vIVCg!0GvyojsO4v literal 0 HcmV?d00001 diff --git a/resources/icons/32x32.png b/resources/icons/32x32.png new file mode 100644 index 0000000000000000000000000000000000000000..d9796f22f39687ef36018fb8523d32e4c039d3b9 GIT binary patch literal 251 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE1|*BCs=ffJL!K^(&PA>`F>?B!<;!1O&uZr6@^|e&E8GOcjVKbu5d|bs`o~RZ8@I1 zKO1=PGBi9}q&-2Cfg#SA`Q-D>x))d)PTf!}Rjf7)XQ=J?pqj%rf#;&#=~@OMhA+)O zn7tGy?&;jX`($x-#;vUHi>!0smPuQb_vUBI6kpN z=$boPHM|W}3}g@$;uTPoJ734~ssGXB^5@JGHhAhz&x`Q~`i8;N)z4*}Q$iB}p~_sC literal 0 HcmV?d00001 diff --git a/resources/icons/48x48.png b/resources/icons/48x48.png new file mode 100644 index 0000000000000000000000000000000000000000..6cd23e34e324a6355c7456f00111159661481b51 GIT binary patch literal 531 zcmV+u0_^>XP)m}#60wJQ8y^AiwbN|Y#3 z;&6eFpg^K#(J%?e%jMEFO+$Y2Jk9}XSDnT7egBj^uc{hSGz;Vi36FqT&E3Rbuh-5B z8irv6xZQ3p*8tkKb=Iou+KJ_{Z#`i<%n9ID{Z*t?BV@l_su5!w)i6E)m%gg1Md_uT zQmiGD=PjUTEyx1E;*Bwj75Y*&Fe`vD=rSja-MdjPm^kyfo`I}F>1$GQMmJgrwlc9>VU7~chuqC)^= ztMY)UDs2GPHb?{=5fVMkk7Km}Vx@8FFl%s4wDWr2~Yk% zfB?Wh<}KNjx6P~8GD9)|H;m~$A3w|UCA%64)z}V@Y@5Acwg7x&n^OBAe literal 0 HcmV?d00001 diff --git a/resources/icons/512x512.png b/resources/icons/512x512.png new file mode 100644 index 0000000000000000000000000000000000000000..6a6053acf5d2f7c3ac9995f385403627cbd5cf16 GIT binary patch literal 4042 zcmdT{T}WI<6h3?J#$83-#Zc5%aR@i`Eo_p92qt=4wC;Uq2*sy7E%YHTeK2hk76NhV zLug;ZW)ma{rBz6LYYVZY!KUTQreo5q=q!1KUVE`&g|VqvTLrI5S`I; zznOdHJKvm{-JN;o!2aI4nrCZ>sLo4wzd~dIS`@Fs<9_!0e~A*OyzUqJvQu}iPt;jE zYkMZny!Y+pv5Rj#)427uk7m84liU2UA;-&a=D#gd$8MaSeNK6pea@*Sa`O%n;N@YQ zb#&VQeSB;DrG)EvAR?g$YXdjZ%#Aoven>P#jsRUdN+%|nLd_vcEh^8n_!@yBqKRclH{~EO11NHZ5e=b~_uirUD-?C_ z{kA~e!h82PDPhQV6(U@1qcnW0XeL22TkI`%Aq~TCgkO@vaig@OojV!vX2gn+D^F^m zxB%Cph_(wfXP1M|8tJ=4TsLWYJ5(_GX@RwXlI|`tJ4?$BFyaJV?P4ThzlzvINp6~u zj8d^$jAfLmXYu4mIM;JAvM3fuaxwWhwJ4;U6GQEX_4R`zFE80!l6LO%x!FNkW#*8n zA^-=)GGQGUr0`#K;SSCtc%%e!B#|aF(l7HiF2ZUc&(ahffIvF86&ntRDZU%^4|W_O zOAY-sKUY8e_tN{^&%4FU^${X`xEc)^AR~d9jRHv%TA}I0x5AwKPo}@Lll1$if5cMr zf7)7n#keJ?N-$Z1Yf4xJ8AVoIL_wywLzQ|V!2LCnKBV95%AhyY`k2wmJlKYxMR?C( zd6-|bF~!B>i=(la`XD097hdoVWf<75Dc6-V7!X&W)t6|p)1Qvy7XNuwi?7*v3{6`) zU9nQX4T1QnE>kEN!vn)}2sD1P1;DmJD(X^~-8%x@;A&A<8l%>mz} zH~eQKyyu$tK#-~B+j`icOM4bIzBo}T?4B|s!+SpFrSo^w(R(NC;e9Tv62oA_?FAzW zR$n7Ee!J@co$KVjjj&IN+J83stilhR>P3gD{|y;}C!xmhqH>D*Kcn(KOi+L#iBpk5b0Pyx=3U zI21hj%#V@_+w5&Inl+Y!sZtp1SxG`OB!|Q!a~;4p2VfY%T%3VW5$%;3Z=3~0G zf8|R}n#Cyg(Q*}9U7!oE(_y{6(lyKjcOJJT2lNle;q`bZ_@=(BE{r73e4R=^pdPud Ncs={OKYTGW_&=Cpk+uK; literal 0 HcmV?d00001 diff --git a/resources/icons/64x64.png b/resources/icons/64x64.png new file mode 100644 index 0000000000000000000000000000000000000000..80d877627a5fb1c68f10eb5a4a96004e44c7eea2 GIT binary patch literal 323 zcmV-J0lfZ+P)#)&ibrAq<_6f0QG&F-ECDbbKw3Bl@OS(J0f3toI10vSR{{WU zD-e(5iv!?v1**3^0A2t>G8F)SKZ1(^)ILR3L7u(>%L8aXg0_R$0+ ({ + type, + image: renderIcon(sources, size), + })) + + return [ + [ + path.join(CONSTANTS.ICON_OUTPUT.packagingDir, "icon.png"), + encodePng(renderIcon(sources, CONSTANTS.ICON_MASTER_SIZE)), + ], + [ + path.join(CONSTANTS.ICON_OUTPUT.packagingDir, "icon.ico"), + encodeIco(CONSTANTS.ICO_SIZES.map(size => renderIcon(sources, size))), + ], + [path.join(CONSTANTS.ICON_OUTPUT.packagingDir, "icon.icns"), encodeIcns(icnsSlots)], + [CONSTANTS.ICON_OUTPUT.runtimeFile, encodePng(renderIcon(sources, CONSTANTS.ICON_RUNTIME_SIZE))], + ...CONSTANTS.LINUX_ICON_SIZES.map((size): [string, Buffer] => [ + path.join(CONSTANTS.ICON_OUTPUT.linuxDir, `${size}x${size}.png`), + encodePng(renderIcon(sources, size)), + ]), + ] +} + +/** + * Regenerates every packaging icon from the React95 source art. + */ +function generateIcons() { + const outputs = renderOutputs(readSources()) + + for (const [file, contents] of outputs) { + fs.mkdirSync(path.dirname(file), { recursive: true }) + fs.writeFileSync(file, contents) + + console.log(`${path.relative(process.cwd(), file).replace(/\\/g, "/")} (${contents.length} bytes)`) + } +} + +generateIcons() diff --git a/scripts/lib/constants.ts b/scripts/lib/constants.ts new file mode 100644 index 0000000..cc9337a --- /dev/null +++ b/scripts/lib/constants.ts @@ -0,0 +1,109 @@ +import { Buffer } from "node:buffer" +import path from "node:path" +import { fileURLToPath } from "node:url" + +/** + * The project root, resolved from this file's own location. + */ +const ROOT_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..") + +/** + * The constants for the build scripts. + */ +const CONSTANTS = { + /** + * Where the app art is taken from, the React95 set ships a hand-drawn 16x16 variant + * next to the 32x32 one every other size is scaled from. + */ + ICON_SOURCE: { + dir: path.join(ROOT_DIR, "node_modules", "@react95", "icons", "png"), + smallFile: "Computer3_16x16_4.png", + largeFile: "Computer3_32x32_4.png", + }, + + /** + * Where the generated icons land. + */ + ICON_OUTPUT: { + packagingDir: path.join(ROOT_DIR, "resources"), + linuxDir: path.join(ROOT_DIR, "resources", "icons"), + runtimeFile: path.join(ROOT_DIR, "src", "main", "assets", "icon.png"), + }, + + /** + * The size of the master PNG, which is what electron-builder falls back to for any + * platform left without a dedicated icon file. + */ + ICON_MASTER_SIZE: 1024, + + /** + * The size of the icon the main process hands to its windows. + */ + ICON_RUNTIME_SIZE: 256, + + /** + * The sizes packed into the Windows `.ico`. + */ + ICO_SIZES: [16, 24, 32, 48, 64, 128, 256], + + /** + * The sizes emitted as loose PNGs for Linux. + */ + LINUX_ICON_SIZES: [16, 24, 32, 48, 64, 128, 256, 512], + + /** + * The macOS `.icns` slots, each pairing an OSType with the size it holds, covering + * every 1x and 2x variant `iconutil` would emit. + */ + ICNS_SLOT_SIZES: { + icp4: 16, + icp5: 32, + ic11: 32, + ic12: 64, + ic07: 128, + ic13: 256, + ic08: 256, + ic09: 512, + ic14: 512, + ic10: 1024, + }, + + /** + * The eight byte signature every PNG file opens with. + */ + PNG_SIGNATURE: Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]), + + /** + * The number of bytes a pixel takes in the 8-bit RGBA buffers used throughout. + */ + PNG_BYTES_PER_PIXEL: 4, + + /** + * The CRC-32 lookup table guarding PNG chunks. + */ + PNG_CRC_TABLE: new Uint32Array(256).map((_, index) => { + let value = index + for (let bit = 0; bit < 8; bit++) value = value & 1 ? 0xedb88320 ^ (value >>> 1) : value >>> 1 + return value + }), + + /** + * The size of the `BITMAPINFOHEADER` opening every bitmap backed `.ico` entry. + */ + ICO_BITMAP_HEADER_SIZE: 40, + + /** + * The size of the `ICONDIR` opening an `.ico`, followed by one `ICONDIRENTRY` of + * `ICO_DIRECTORY_ENTRY_SIZE` bytes per packed image. + */ + ICO_DIRECTORY_SIZE: 6, + ICO_DIRECTORY_ENTRY_SIZE: 16, + + /** + * The smallest entry stored as a PNG rather than as a bitmap, which is how the + * format has carried its largest sizes since Vista. + */ + ICO_PNG_THRESHOLD: 256, +} as const + +export default CONSTANTS diff --git a/scripts/lib/utils/icns.ts b/scripts/lib/utils/icns.ts new file mode 100644 index 0000000..d2e8d10 --- /dev/null +++ b/scripts/lib/utils/icns.ts @@ -0,0 +1,26 @@ +import { Buffer } from "node:buffer" +import { encodePng } from "@scripts/lib/utils/png" +import type { IcnsSlot } from "@scripts/types/icons" + +/** + * Packs slots into a macOS `.icns`, one PNG backed chunk each. + * @param slots The slots to pack. + * @returns The ICNS bytes. + */ +export function encodeIcns(slots: IcnsSlot[]): Buffer { + const chunks = slots.map(({ type, image }) => { + const data = encodePng(image) + const chunk = Buffer.alloc(8 + data.length) + chunk.write(type, 0, "ascii") + chunk.writeUInt32BE(chunk.length, 4) + data.copy(chunk, 8) + + return chunk + }) + + const header = Buffer.alloc(8) + header.write("icns", 0, "ascii") + header.writeUInt32BE(header.length + chunks.reduce((total, chunk) => total + chunk.length, 0), 4) + + return Buffer.concat([header, ...chunks]) +} diff --git a/scripts/lib/utils/ico.ts b/scripts/lib/utils/ico.ts new file mode 100644 index 0000000..6021d33 --- /dev/null +++ b/scripts/lib/utils/ico.ts @@ -0,0 +1,77 @@ +import { Buffer } from "node:buffer" +import CONSTANTS from "@scripts/lib/constants" +import { encodePng } from "@scripts/lib/utils/png" +import type { RasterImage } from "@scripts/types/icons" + +/** + * Encodes an image as an ICO device independent bitmap, a bottom-up BGRA buffer trailed + * by the 1-bit transparency mask the format still requires. + * @param image The image to encode. + * @returns The bitmap bytes. + */ +function encodeBitmap(image: RasterImage): Buffer { + const stride = image.width * CONSTANTS.PNG_BYTES_PER_PIXEL + const maskStride = Math.ceil(image.width / 32) * 4 + const colors = Buffer.alloc(image.height * stride) + const mask = Buffer.alloc(image.height * maskStride) + + for (let y = 0; y < image.height; y++) { + const flipped = image.height - 1 - y + + for (let x = 0; x < image.width; x++) { + const source = (y * image.width + x) * CONSTANTS.PNG_BYTES_PER_PIXEL + const target = flipped * stride + x * CONSTANTS.PNG_BYTES_PER_PIXEL + + colors[target] = image.pixels[source + 2] + colors[target + 1] = image.pixels[source + 1] + colors[target + 2] = image.pixels[source] + colors[target + 3] = image.pixels[source + 3] + + if (!image.pixels[source + 3]) mask[flipped * maskStride + (x >> 3)] |= 0x80 >> (x & 7) + } + } + + const header = Buffer.alloc(CONSTANTS.ICO_BITMAP_HEADER_SIZE) + header.writeUInt32LE(CONSTANTS.ICO_BITMAP_HEADER_SIZE, 0) + header.writeInt32LE(image.width, 4) + header.writeInt32LE(image.height * 2, 8) + header.writeUInt16LE(1, 12) + header.writeUInt16LE(32, 14) + header.writeUInt32LE(colors.length + mask.length, 20) + + return Buffer.concat([header, colors, mask]) +} + +/** + * Packs images into a Windows `.ico`, storing the largest entries as PNG and everything + * below the threshold as a bitmap. + * @param images The images to pack, in ascending size order. + * @returns The ICO bytes. + */ +export function encodeIco(images: RasterImage[]): Buffer { + const entries = images.map(image => + image.width >= CONSTANTS.ICO_PNG_THRESHOLD ? encodePng(image) : encodeBitmap(image), + ) + + const directory = Buffer.alloc(CONSTANTS.ICO_DIRECTORY_SIZE + entries.length * CONSTANTS.ICO_DIRECTORY_ENTRY_SIZE) + directory.writeUInt16LE(1, 2) + directory.writeUInt16LE(entries.length, 4) + + let offset = directory.length + + entries.forEach((entry, index) => { + const position = CONSTANTS.ICO_DIRECTORY_SIZE + index * CONSTANTS.ICO_DIRECTORY_ENTRY_SIZE + + // A 256 pixel side is written as a zero, which is the widest the single byte can say + directory[position] = images[index].width % 256 + directory[position + 1] = images[index].height % 256 + + directory.writeUInt16LE(1, position + 4) + directory.writeUInt16LE(32, position + 6) + directory.writeUInt32LE(entry.length, position + 8) + directory.writeUInt32LE(offset, position + 12) + offset += entry.length + }) + + return Buffer.concat([directory, ...entries]) +} diff --git a/scripts/lib/utils/png.ts b/scripts/lib/utils/png.ts new file mode 100644 index 0000000..4883b98 --- /dev/null +++ b/scripts/lib/utils/png.ts @@ -0,0 +1,133 @@ +import { Buffer } from "node:buffer" +import zlib from "node:zlib" +import CONSTANTS from "@scripts/lib/constants" +import type { RasterImage } from "@scripts/types/icons" + +/** + * Computes the CRC-32 checksum PNG appends to each of its chunks. + * @param buffer The bytes to checksum. + * @returns The unsigned CRC-32 value. + */ +function crc32(buffer: Buffer): number { + let crc = 0xffffffff + for (const byte of buffer) crc = CONSTANTS.PNG_CRC_TABLE[(crc ^ byte) & 0xff] ^ (crc >>> 8) + + return (crc ^ 0xffffffff) >>> 0 +} + +/** + * Picks the reference byte for PNG's Paeth filter, the neighbor the predicted value + * lands closest to. + * @param left The byte one pixel to the left. + * @param above The byte one row up. + * @param upperLeft The byte one pixel left on the row above. + * @returns The chosen neighbor. + */ +function paethPredictor(left: number, above: number, upperLeft: number): number { + const estimate = left + above - upperLeft + const toLeft = Math.abs(estimate - left) + const toAbove = Math.abs(estimate - above) + const toUpperLeft = Math.abs(estimate - upperLeft) + + if (toLeft <= toAbove && toLeft <= toUpperLeft) return left + return toAbove <= toUpperLeft ? above : upperLeft +} + +/** + * Wraps chunk data in the length, type and CRC framing PNG expects. + * @param type The four character chunk type. + * @param data The chunk payload. + * @returns The framed chunk. + */ +function buildChunk(type: string, data: Buffer): Buffer { + const chunk = Buffer.alloc(12 + data.length) + chunk.writeUInt32BE(data.length, 0) + chunk.write(type, 4, "ascii") + data.copy(chunk, 8) + chunk.writeUInt32BE(crc32(chunk.subarray(4, 8 + data.length)), 8 + data.length) + + return chunk +} + +/** + * Decodes an 8-bit non-interlaced RGBA PNG into a flat pixel buffer. + * @param file The raw PNG bytes. + * @returns The decoded image. + */ +export function decodePng(file: Buffer): RasterImage { + const blocks: Buffer[] = [] + let width = 0 + let height = 0 + let offset = CONSTANTS.PNG_SIGNATURE.length + + while (offset < file.length) { + const length = file.readUInt32BE(offset) + const type = file.toString("ascii", offset + 4, offset + 8) + const data = file.subarray(offset + 8, offset + 8 + length) + + if (type === "IHDR") { + width = data.readUInt32BE(0) + height = data.readUInt32BE(4) + + if (data[8] !== 8 || data[9] !== 6 || data[12] !== 0) { + throw new Error("only 8-bit non-interlaced RGBA PNG sources are supported") + } + } else if (type === "IDAT") blocks.push(data) + + offset += 12 + length + } + + const raw = zlib.inflateSync(Buffer.concat(blocks)) + const stride = width * CONSTANTS.PNG_BYTES_PER_PIXEL + const pixels = Buffer.alloc(height * stride) + + for (let row = 0; row < height; row++) { + const filter = raw[row * (stride + 1)] + + for (let index = 0; index < stride; index++) { + const hasLeft = index >= CONSTANTS.PNG_BYTES_PER_PIXEL + const left = hasLeft ? pixels[row * stride + index - CONSTANTS.PNG_BYTES_PER_PIXEL] : 0 + const above = row > 0 ? pixels[(row - 1) * stride + index] : 0 + const upperLeft = + row > 0 && hasLeft ? pixels[(row - 1) * stride + index - CONSTANTS.PNG_BYTES_PER_PIXEL] : 0 + + let value = raw[row * (stride + 1) + 1 + index] + if (filter === 1) value += left + else if (filter === 2) value += above + else if (filter === 3) value += (left + above) >> 1 + else if (filter === 4) value += paethPredictor(left, above, upperLeft) + + pixels[row * stride + index] = value & 0xff + } + } + + return { width, height, pixels } +} + +/** + * Encodes an image as an 8-bit RGBA PNG, leaving every scan-line unfiltered since flat + * pixel art deflates well enough on its own. + * @param image The image to encode. + * @returns The PNG bytes. + */ +export function encodePng(image: RasterImage): Buffer { + const stride = image.width * CONSTANTS.PNG_BYTES_PER_PIXEL + const raw = Buffer.alloc(image.height * (stride + 1)) + + for (let row = 0; row < image.height; row++) { + image.pixels.copy(raw, row * (stride + 1) + 1, row * stride, (row + 1) * stride) + } + + const header = Buffer.alloc(13) + header.writeUInt32BE(image.width, 0) + header.writeUInt32BE(image.height, 4) + header[8] = 8 + header[9] = 6 + + return Buffer.concat([ + CONSTANTS.PNG_SIGNATURE, + buildChunk("IHDR", header), + buildChunk("IDAT", zlib.deflateSync(raw, { level: 9 })), + buildChunk("IEND", Buffer.alloc(0)), + ]) +} diff --git a/scripts/lib/utils/raster.ts b/scripts/lib/utils/raster.ts new file mode 100644 index 0000000..40d11dc --- /dev/null +++ b/scripts/lib/utils/raster.ts @@ -0,0 +1,93 @@ +import { Buffer } from "node:buffer" +import CONSTANTS from "@scripts/lib/constants" +import type { RasterImage } from "@scripts/types/icons" + +/** + * Blows an image up by an integer factor, duplicating pixels so the art keeps its hard + * edges instead of being smoothed into a blur. + * @param image The image to enlarge. + * @param factor The integer factor to enlarge it by. + * @returns The enlarged image. + */ +function scaleNearest(image: RasterImage, factor: number): RasterImage { + const width = image.width * factor + const height = image.height * factor + const pixels = Buffer.alloc(width * height * CONSTANTS.PNG_BYTES_PER_PIXEL) + + for (let y = 0; y < height; y++) { + for (let x = 0; x < width; x++) { + const source = + (Math.floor(y / factor) * image.width + Math.floor(x / factor)) * CONSTANTS.PNG_BYTES_PER_PIXEL + + image.pixels.copy( + pixels, + (y * width + x) * CONSTANTS.PNG_BYTES_PER_PIXEL, + source, + source + CONSTANTS.PNG_BYTES_PER_PIXEL, + ) + } + } + + return { width, height, pixels } +} + +/** + * Shrinks an image by an integer factor, averaging each source block over premultiplied + * alpha so transparent pixels never bleed their color into the edges. + * @param image The image to shrink. + * @param factor The integer factor to shrink it by. + * @returns The shrunk image. + */ +function downsampleBox(image: RasterImage, factor: number): RasterImage { + const width = image.width / factor + const height = image.height / factor + const pixels = Buffer.alloc(width * height * CONSTANTS.PNG_BYTES_PER_PIXEL) + const samples = factor * factor + + for (let y = 0; y < height; y++) { + for (let x = 0; x < width; x++) { + let red = 0 + let green = 0 + let blue = 0 + let alpha = 0 + + for (let dy = 0; dy < factor; dy++) { + for (let dx = 0; dx < factor; dx++) { + const source = ((y * factor + dy) * image.width + x * factor + dx) * CONSTANTS.PNG_BYTES_PER_PIXEL + const weight = image.pixels[source + 3] + + red += image.pixels[source] * weight + green += image.pixels[source + 1] * weight + blue += image.pixels[source + 2] * weight + alpha += weight + } + } + + const target = (y * width + x) * CONSTANTS.PNG_BYTES_PER_PIXEL + pixels[target] = alpha ? Math.round(red / alpha) : 0 + pixels[target + 1] = alpha ? Math.round(green / alpha) : 0 + pixels[target + 2] = alpha ? Math.round(blue / alpha) : 0 + pixels[target + 3] = Math.round(alpha / samples) + } + } + + return { width, height, pixels } +} + +/** + * Resizes an image to a square target size, going through the smallest whole multiple + * of the source the target divides into, so sizes that are an exact multiple stay pixel + * perfect and the rest only soften on their half pixel boundaries. + * @param image The image to resize. + * @param size The square size to resize to, in pixels. + * @returns The resized image. + */ +export function resizeIcon(image: RasterImage, size: number): RasterImage { + let multiplier = 1 + while ((image.width * multiplier) % size !== 0) multiplier++ + + const enlarged = multiplier > 1 ? scaleNearest(image, multiplier) : image + const factor = enlarged.width / size + + return factor > 1 ? downsampleBox(enlarged, factor) : enlarged +} diff --git a/scripts/types/icons.d.ts b/scripts/types/icons.d.ts new file mode 100644 index 0000000..09d4d82 --- /dev/null +++ b/scripts/types/icons.d.ts @@ -0,0 +1,26 @@ +/** + * A decoded raster image, its dimensions paired with a flat 8-bit RGBA pixel buffer. + */ +export type RasterImage = { + width: number + height: number + pixels: Buffer +} + +/** + * The two pieces of source art every packaging icon is rendered from, the hand-drawn + * 16x16 variant and the 32x32 one every other size is scaled from. + */ +export type IconSources = { + small: RasterImage + large: RasterImage +} + +/** + * A single macOS `.icns` entry, pairing the OSType naming the slot with the image + * filling it. + */ +export type IcnsSlot = { + type: string + image: RasterImage +} diff --git a/src/main/assets/icon.png b/src/main/assets/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..458bc009b49f18f057551246eae7bfe79606d591 GIT binary patch literal 1219 zcmeAS@N?(olHy`uVBq!ia0y~yU<5K5893O0R7}x|GzJEiE>9Q7kcwMx?-~j<8}KkV z+TZnmoW5j!=ZG998bCtXwVoJ10u^!48RsgHR*7$&eVcp?*D?lQ$+-&(?; z1aycH1A_tvwduflm$h*pUNwZsOEUpo=>WtGEZ78VQ?UhVNIMs z1A_}lDGoD1QYQQj*KWOD^Ur~ClevPK3{a#2B+P-#013dsGo}ks{=5!Z{$Kv7%W%0e z!zJ)Deuy+!TQX?=YnFB9KZsu`LUuPJ!-VZ_`Vt5c0<2)h9nG42Ctf+r5m1PbwqsS; zePA)8hy8z9kf(_QeO4@7Vg{>zDGCy&9;k}1abD*JPr&A#QIrbIm0Gv z2C*1ihBtsDz$vptNP!(ohJxiX2i$U(3L6+4fI*}J%x)Oel46FB$K_zAU}V|1%pI)L sfKK3KV3+`-6gU|sF({B&WSH!f=ilnXe>vmzYLJ&aUHx3vIVCg!0GvyojsO4v literal 0 HcmV?d00001 diff --git a/src/main/config/windows.ts b/src/main/config/windows.ts index 8695c5f..dbffd44 100644 --- a/src/main/config/windows.ts +++ b/src/main/config/windows.ts @@ -1,4 +1,5 @@ import path from "node:path" +import appIcon from "@main/assets/icon.png?asset" import CONSTANTS from "@main/lib/constants" import type { BrowserWindowConstructorOptions, WebPreferences } from "electron" @@ -39,6 +40,7 @@ const WINDOWS_CONFIG: { y: CONSTANTS.titleBarHeight / 2 - CONSTANTS.macOSTrafficLightsHeight / 2, }, acceptFirstMouse: true, + icon: appIcon, webPreferences: WEB_PREFERENCES, }, dialog: { @@ -50,6 +52,7 @@ const WINDOWS_CONFIG: { show: false, center: true, roundedCorners: false, + icon: appIcon, webPreferences: WEB_PREFERENCES, }, } diff --git a/src/main/types/env.d.ts b/src/main/types/env.d.ts new file mode 100644 index 0000000..d5822de --- /dev/null +++ b/src/main/types/env.d.ts @@ -0,0 +1 @@ +/// diff --git a/tsconfig.json b/tsconfig.json index 168d419..19d75cc 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -36,8 +36,13 @@ "moduleResolution": "bundler", /* Specify how TypeScript looks up a file from a given module specifier. */ "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ "paths": { /* Specify a set of entries that re-map imports to additional lookup locations. */ + // Scripts + "@scripts/*": ["./scripts/*"], + + // Source "@/*": ["./src/*"], + // Main process "@main/*": ["./src/main/*"], diff --git a/yarn.lock b/yarn.lock index a646341..693a128 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2264,7 +2264,7 @@ esbuild@^0.25.11: "@esbuild/win32-ia32" "0.25.12" "@esbuild/win32-x64" "0.25.12" -"esbuild@^0.27.0 || ^0.28.0": +"esbuild@^0.27.0 || ^0.28.0", esbuild@~0.28.0: version "0.28.1" resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.28.1.tgz#ef45b4634c9c9d97a296aea4114a5f9840f95578" integrity sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw== @@ -4148,6 +4148,15 @@ tslib@^2.4.0, tslib@^2.8.1: resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f" integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w== +tsx@^4.23.9: + version "4.23.9" + resolved "https://registry.yarnpkg.com/tsx/-/tsx-4.23.9.tgz#34857961fcfb44f22d05ae905fa32dc182608c33" + integrity sha512-6q8uTORRGauQVjqMQnKUucLFoeXZAfw6zKvG35GLbdKWbLdeOtZ3H4mhyA5mxuUd2o2cRTskhj59nLLQseUvUw== + dependencies: + esbuild "~0.28.0" + optionalDependencies: + fsevents "~2.3.3" + type-check@^0.4.0, type-check@~0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1"