From 7e75907f54930bdbc6cc80e219b60bea6fc80c91 Mon Sep 17 00:00:00 2001 From: JUN Date: Mon, 21 Sep 2026 02:06:05 +0900 Subject: [PATCH 1/2] feat(brand): render the dashboard and documentation favicons from the app icon vector MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit docs-site/public/favicon.png was the dark variant of the mark: white on transparency, 36225 of 36864 pixels carrying alpha but every one of them RGB (255,255,255), exactly one fully opaque pixel, corner alpha 3. Composited on white it is a white square, and a browser tab strip is light by default, so the documentation site effectively had no favicon in light mode. favicon.ico beside it carried the same artwork at 16, 32 and 48. gui/public/favicon.png looked right at 128 but was a bitmap no source could regenerate, and it was the shaded artwork whose engraved prompt all but disappears at 16 and 32. Both are now rendered from desktop/src-tauri/icons/icon.svg at the names, sizes and formats the two sites already reference, so neither page nor config changes. The renderer, the RGBA re-encode and the ICO packer move into scripts/lib/icon-render.ts so the two generators share them rather than keeping a second place for the alpha bug to come back. The test decodes pixels because an opaque corner alone is not enough: a plain white square has an opaque corner and is still invisible. It requires an opaque light corner and at least 10% of the image to be opaque pixels whose luminance differs from that corner by more than 64. Both ways of being invisible were applied and each turns it red. Starlight's logo-light.png and logo-dark.png stay as they are — they are the artwork the vector was traced from. og.png also stays, with its own pre-existing mismatch recorded: the config declares 1200x630 and the file is 1536x1024. --- desktop/scripts/generate-icons.ts | 152 +-------------- .../010_favicons.md | 54 ++++++ docs-site/public/favicon.ico | Bin 3460 -> 2617 bytes docs-site/public/favicon.png | Bin 16096 -> 5407 bytes gui/public/favicon.png | Bin 16089 -> 3512 bytes package.json | 2 + scripts/brand-favicons.ts | 97 ++++++++++ scripts/lib/icon-render.ts | 162 ++++++++++++++++ scripts/test-layout/layout.json | 1 + tests/ci-workflows/brand-favicons.test.ts | 174 ++++++++++++++++++ tests/fixtures/test-layout-expected.json | 1 + 11 files changed, 494 insertions(+), 149 deletions(-) create mode 100644 devlog/_plan/260921_brand_icon_and_menu_bar_mark/010_favicons.md create mode 100644 scripts/brand-favicons.ts create mode 100644 scripts/lib/icon-render.ts create mode 100644 tests/ci-workflows/brand-favicons.test.ts diff --git a/desktop/scripts/generate-icons.ts b/desktop/scripts/generate-icons.ts index 04ab6e7c803..3acfdc8bb92 100644 --- a/desktop/scripts/generate-icons.ts +++ b/desktop/scripts/generate-icons.ts @@ -17,7 +17,7 @@ import { mkdtempSync, mkdirSync, readFileSync, writeFileSync, rmSync, existsSync import { tmpdir } from "node:os"; import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; -import { deflateSync, inflateSync } from "node:zlib"; +import { buildIco, render as renderSvg } from "../../scripts/lib/icon-render"; const desktopDir = dirname(dirname(fileURLToPath(import.meta.url))); const iconsDir = join(desktopDir, "src-tauri", "icons"); @@ -71,155 +71,9 @@ const TRAY_OUTPUT = "tray/icon.png"; const TRAY_SIZE = 44; const traySource = join(iconsDir, "tray", "icon.svg"); +/** Render at `size` from `from`, defaulting to the app icon vector. */ function render(size: number, out: string, from: string = source): void { - const result = spawnSync("rsvg-convert", ["-w", String(size), "-h", String(size), from, "-o", out]); - if (result.status !== 0) { - const detail = result.error?.message ?? result.stderr?.toString().trim() ?? "unknown error"; - throw new Error(`rsvg-convert failed for ${size}px: ${detail}`); - } - writeFileSync(out, toRgba(readFileSync(out))); -} - -const PNG_SIGNATURE = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]); - -const CRC_TABLE = (() => { - const table = new Int32Array(256); - for (let n = 0; n < 256; n += 1) { - let c = n; - for (let k = 0; k < 8; k += 1) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1; - table[n] = c; - } - return table; -})(); - -function crc32(bytes: Buffer): number { - let c = -1; - for (const byte of bytes) c = CRC_TABLE[(c ^ byte) & 0xff]! ^ (c >>> 8); - return (c ^ -1) >>> 0; -} - -function chunk(type: string, data: Buffer): Buffer { - const head = Buffer.alloc(8); - head.writeUInt32BE(data.length, 0); - head.write(type, 4, "latin1"); - const crc = Buffer.alloc(4); - crc.writeUInt32BE(crc32(Buffer.concat([head.subarray(4), data])), 0); - return Buffer.concat([head, data, crc]); -} - -/** Undo one PNG scanline filter in place, given the already reconstructed row above. */ -function unfilter(kind: number, row: Buffer, prior: Buffer, stride: number): void { - for (let i = 0; i < row.length; i += 1) { - const a = i >= stride ? row[i - stride]! : 0; - const b = prior[i]!; - const c = i >= stride ? prior[i - stride]! : 0; - let add = 0; - if (kind === 1) add = a; - else if (kind === 2) add = b; - else if (kind === 3) add = (a + b) >> 1; - else if (kind === 4) { - const p = a + b - c; - const pa = Math.abs(p - a); - const pb = Math.abs(p - b); - const pc = Math.abs(p - c); - add = pa <= pb && pa <= pc ? a : pb <= pc ? b : c; - } else if (kind !== 0) throw new Error(`unknown PNG filter ${kind}`); - row[i] = (row[i]! + add) & 0xff; - } -} - -/** - * Re-encode an 8-bit truecolour PNG as truecolour-with-alpha, and leave one that already has - * alpha untouched. - * - * This exists because the icon has an opaque backdrop. librsvg notices that nothing in the render - * is transparent and drops the alpha channel, which is a valid PNG and a broken icon: Tauri's - * `generate_context!` rejects a window icon that is not RGBA, so `bun run build` fails at - * compile time with "icon ... is not RGBA". Leaving that to the renderer would also make the - * committed bytes depend on which librsvg drew them. - */ -function toRgba(bytes: Buffer): Buffer { - if (!bytes.subarray(0, 8).equals(PNG_SIGNATURE)) throw new Error("rsvg-convert did not emit a PNG"); - - let header: { width: number; height: number } | null = null; - let colourType = -1; - const idat: Buffer[] = []; - for (let at = 8; at + 8 <= bytes.length; ) { - const length = bytes.readUInt32BE(at); - const type = bytes.subarray(at + 4, at + 8).toString("latin1"); - const data = bytes.subarray(at + 8, at + 8 + length); - if (type === "IHDR") { - colourType = data[9]!; - if (data[8] !== 8 || data[12] !== 0) throw new Error("PNG is not 8-bit non-interlaced"); - header = { width: data.readUInt32BE(0), height: data.readUInt32BE(4) }; - } else if (type === "IDAT") idat.push(Buffer.from(data)); - else if (type === "IEND") break; - at += 12 + length; - } - if (colourType === 6) return bytes; - if (colourType !== 2 || header === null) throw new Error(`unexpected PNG colour type ${colourType}`); - - const { width, height } = header; - const raw = inflateSync(Buffer.concat(idat)); - const stride = width * 3; - const out = Buffer.alloc(height * (1 + width * 4)); - let prior = Buffer.alloc(stride); - for (let y = 0; y < height; y += 1) { - const start = y * (1 + stride); - const row = Buffer.from(raw.subarray(start + 1, start + 1 + stride)); - unfilter(raw[start]!, row, prior, 3); - const target = y * (1 + width * 4); - out[target] = 0; - for (let x = 0; x < width; x += 1) { - row.copy(out, target + 1 + x * 4, x * 3, x * 3 + 3); - out[target + 1 + x * 4 + 3] = 0xff; - } - prior = row; - } - - const ihdr = Buffer.alloc(13); - ihdr.writeUInt32BE(width, 0); - ihdr.writeUInt32BE(height, 4); - ihdr[8] = 8; - ihdr[9] = 6; - return Buffer.concat([ - PNG_SIGNATURE, - chunk("IHDR", ihdr), - chunk("IDAT", deflateSync(out, { level: 9 })), - chunk("IEND", Buffer.alloc(0)), - ]); -} - -/** - * Pack PNGs into an ICO. - * - * Written here rather than shelled out because the alternative is ImageMagick, and adding a - * system dependency to regenerate an icon is a worse trade than 30 lines of a container format - * that has not changed in decades. A 256px entry records its dimension as 0, which is how ICO - * spells "256". - */ -function buildIco(entries: Array<{ size: number; bytes: Buffer }>): Buffer { - const header = Buffer.alloc(6); - header.writeUInt16LE(0, 0); // reserved - header.writeUInt16LE(1, 2); // type: icon - header.writeUInt16LE(entries.length, 4); - - const directory = Buffer.alloc(16 * entries.length); - let offset = header.length + directory.length; - entries.forEach((entry, index) => { - const at = index * 16; - directory.writeUInt8(entry.size >= 256 ? 0 : entry.size, at); - directory.writeUInt8(entry.size >= 256 ? 0 : entry.size, at + 1); - directory.writeUInt8(0, at + 2); // palette colours - directory.writeUInt8(0, at + 3); // reserved - directory.writeUInt16LE(1, at + 4); // colour planes - directory.writeUInt16LE(32, at + 6); // bits per pixel - directory.writeUInt32LE(entry.bytes.length, at + 8); - directory.writeUInt32LE(offset, at + 12); - offset += entry.bytes.length; - }); - - return Buffer.concat([header, directory, ...entries.map(entry => entry.bytes)]); + renderSvg(size, out, from); } /** diff --git a/devlog/_plan/260921_brand_icon_and_menu_bar_mark/010_favicons.md b/devlog/_plan/260921_brand_icon_and_menu_bar_mark/010_favicons.md new file mode 100644 index 00000000000..6e09e5c5973 --- /dev/null +++ b/devlog/_plan/260921_brand_icon_and_menu_bar_mark/010_favicons.md @@ -0,0 +1,54 @@ +# The dashboard and documentation favicons + +The desktop app now renders its icon and its menu bar image from one traced vector. The two +favicons the product serves are still hand-made rasters with no source, and one of them is broken +in a way the file itself does not show. + +`docs-site/public/favicon.png` is the dark variant of the mark: white on transparency, 192px, +36225 of 36864 pixels carrying some alpha but every one of them RGB `(255,255,255)`, exactly one +fully opaque pixel, corner `(255,255,255,3)`. Composited on white it is a white square. A browser +tab strip is light by default and Starlight names the favicon unconditionally, so the documentation +site effectively has no favicon in light mode. `favicon.ico` beside it carries the same artwork at +16, 32 and 48, also with no fully opaque pixel. + +`gui/public/favicon.png` is the light composition and looks right at 128px, but it is a bitmap no +source can regenerate, and it is the shaded artwork rather than the flat mark: its engraved prompt +all but disappears at 16 and 32. Rendering the vector at 128 differs from it in 68% of pixels. That +is a visible simplification, not only a deduplication, and it is the same trade the app icon made. + +This unit covers the favicons and nothing else. Starlight's `logo-light.png` and `logo-dark.png` +stay independent 512px rasters — they are the brand artwork the vector was traced from, not +derived assets. `og.png` also stays, and carries a separate pre-existing defect worth its own +scope: `docs-site/astro.config.mjs` declares it 1200x630 while the committed file is 1536x1024. + +## Shape of the change + +- `scripts/lib/icon-render.ts` — new. The renderer, the RGBA re-encode and the ICO packer move here + out of `desktop/scripts/generate-icons.ts`, which keeps its size tables and imports them. Two + generators sharing one renderer is the point; a second copy of the PNG re-encode would be a + second place for the alpha bug to come back. +- `scripts/brand-favicons.ts` — new. Renders `desktop/src-tauri/icons/icon.svg` into + `gui/public/favicon.png` (128), `docs-site/public/favicon.png` (192) and + `docs-site/public/favicon.ico` (16, 32, 48) — the names, sizes and formats the two sites already + reference, so no page or config changes. `--check` regenerates into scratch and compares bytes, + the same contract the desktop set has. +- `package.json` — `favicons` and `favicons:check`. +- `tests/ci-workflows/brand-favicons.test.ts` — new, registered in `scripts/test-layout/layout.json` + and `tests/fixtures/test-layout-expected.json`. Asserts the declared sizes match what the two + sites ask for, that each committed favicon is that size with its alpha channel intact, that the + ICO carries exactly the declared sizes as embedded PNGs, and that both favicons read on a light + tab. + + That last one is why the test decodes pixels. An opaque corner alone is not enough: a plain white + square has an opaque corner and is still invisible. So it requires an opaque light corner **and** + at least 10% of the image to be opaque pixels whose luminance differs from that corner by more + than 64 — the mark actually being there. + +## Acceptance + +1. `bun run favicons:check` reports every favicon matching the source. +2. The new test fails on both ways of being invisible, checked by applying each: the + white-on-transparent artwork this replaces gives 3 pass / 1 fail, and a solid `#fcfcfc` square + gives 3 pass / 1 fail. The generated favicons give 4 pass / 0 fail. +3. `bun run privacy:scan`, `bun run structure:check` and the two test-layout guards stay green. + diff --git a/docs-site/public/favicon.ico b/docs-site/public/favicon.ico index de1689b20be73a948d7e88a269202eb0e9a2e49b..8f120715e2d30d784c5edc8e42ec5b1272931e13 100644 GIT binary patch delta 2537 zcmVZ00031AON5N001@s001B$00031AOKAR007nj001yB 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 delta 3375 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 FV1gk%STFzp diff --git a/docs-site/public/favicon.png b/docs-site/public/favicon.png index 7a741511cff9b9a19f07b1288f528a858584946d..6c26bfafb00b611d0c884e90876e43de7530e7a2 100644 GIT binary patch literal 5407 zcmb`L=Q~{A*T%<~FbrZ4(Q9-;wCIf9jow9~_mYt4qxW8-6JNsUHKO+KRhqaKG#0iezDJ5`(EpF?^w9D3NaxKApig%R#R2f!@hg}_ki)Rqu#13J^()f-Y{ zSdba=;zc^*EejXbi-A3h!N*-!V?mQ+LF3r5GvovRq}inVZeh;kO?&O+@4Mf=o4q)! zU?ruOT_6~U6a7Y2ArJEbP@K3>9hrZF zbY6qd-QsW(=kKh^QYG(MffTr{Yrf|AJNO($fsN~lV0h2Zu7Fl2H#duKbi}d> zi5xb+csMJ1$mY`joQ?fc=@XBT`+p8++wuIl0qCy96-)n%gZW?g7WmQEWgo2sZ?4db zS7bP=n;q!xGrGB19oeq=d4;Lp+Jaa2Ckv$m#?p0Uejmw^BG2E` zza4vFzufF(f3`b*9mOIi6^Pl2-o5@)_j0Y{J-U7*nOf+ZP$z#kro=w@4|W?zsid!P zcKF!_#P{-T7OGAE6Q}}{l2DP6xFVqq*Rt39|Ls=Qbyxn|c-Sin@t66wKmDJyY@f`4 zsY8L?Lak+$2QLs~^Jk&Pey&27-spy>_G^FV*@UFejpoW1w=>^JY?wRm+VCDT9R4G} z-eo~V*Y(^lkN$XbvXIkxv0&cCsbf`mI?CT8EMz~n@0m|4?p?anU=t07&*5`)VlX?& zdD!$f1`QQS`8EDKpVfJLGj?-%bmj}vxEvsn9Fscf#a-_p#UTV4$>a~r%%|+g>lQwQ zKb8^LD|l~(uj02@Ysn(F4to}1xWVu!CY#s%OLYpAtk7?(Kl1aZPkjMTF$54wUe47H zP9hvmdL~_4W9j>sdk-Z=p|UrJLLT_MLHu5CX;|bE-;8EG|Ln2Ry^g;-wz0aJKN8e) zcWFLZUf=uk9vj}=^Js>^lRn=KgyzdwQ79Ashl_dR6?#kcCuuQ(o~}%PMo#0#4tqm! zB_kL#LU&?AwerQL#|k7DU4Pd1Y1qz>bQ~--IozCj?tDvRk(0&^Z?0M{RZAc9=(*cH zzm|M|Ec|}muiT@-jKbPg(ZMkd8Chv$ei<5)a)mN=Cy={K5~=?^TQ2EyteRvQcsfKc zSi{$Q!Rfc&?4}jYT`wHy*{W~dHc+d4c6L8Eqie)TKzp|VZMb60g+hbcr>w7WmGfL{39t$t|J@n z{2VwZaNM?6cT${I=3za zzY?Y=G0Lvr(5uk<7}PfIwlM*;Y4;2gih@mzAK69-x%6S4;}KC=`=9SahmkTn2|`k7 z@FTPFn_?R`%*oaf@onu7HlSF)zK*orLftDg$_#t@gT|?QAf)RS-GP3G+pRNKVd1<$ z6e*pMI%Mc?40h#wb93|HUmlr<+qe8^m`5WxMZrX+9gNt|%)PpklYha_Hpe1Y?yH(7 z&PV9!ToiO^dVxzpA0deq?<>w7{zqv%O1zM#IQNXU3GvRil-ix{Y`wVmjx@EUwSfq19>@(+;Ox#R|AL zd8S;aNHI^iFyL66I)uRFT^b2Hoa~s}LBADLtTw1)KlRvFCjFWotJZF!9&*PgHM<22 zA-AWWjukE?Mq~6^dCgBt=pNw+*4|_hx@^;OoiSuP z$D#1WQwz_~v(H(b%*WH@+tnrw{i~h>#8!_VK^P3%VOOxtm+|(u?xw=l`1C>hOJype zNHC5JKDVdS;>Ds6hP~GJ72&?CQwyVoDE~Y!j#bE=&qj24cf{YFQq*IQ&Qs+&q=GU% zJztBW=_Q^eI3Tr?{S4#n?ZYvbr0%Ml8C)#&oX z{&VRRCkw7G?qb}NV09Wgt+zUW9)j4~R{|3T>l(SUcr5LFM@)`m2FC;l@`J0eoSt9m zzy<@1k!ScPg=mQuT=|vYE42BH0R%<%R@PuT?=DyDN(sWnooWv^SKm%v zbg3+#CFye@3~B!)6eAB_zlDtKvp@bQB3YJpScdssUi4QB!Bf{cbJe$_ZH-#3GrNE1 zId5nz-V{EOF7_P!29*maQ{YyIwyxvP3sXBt^g3ED;n-N*9jAU-haZ5xqS7=18KtzD zDF!MH7tHVCJvr(6VaZKgJm>U#$mSS?YMkla!aFC$Y(ya`D^^o>PTsr5TfGL3@b0D@C^LHB}nkU;N6pSD&>Y3Nn zVKW%NXbqeireQytOm7WDc~P(gJdU+em~mjYvC{98Y+DS-vrXb-QoVZPdAmJWE~@TT zPq4O0@?}t(UTB8icj>gYuN1@44LK9MKIc+OX_!WcHM({%4wy&_p~4u~Bk*yu@GreTYTVn`?= zLv*;bIC=*I=UX*77R!uEdD6qL8GG3u34!cEQbxVXc9nOpGW4~%0%YlS+gat>f-Q)| zYG(lRVNOB&F12m!6wISX-hPo91mq~^TmJ}+fJjRh1vYI2labBs{9t;jnA({_wD_5~ z(Q_IcvHv;_Nmm+dRF-yZ^>$3jS>-mmgK!Dp<8xkL#)M_hmmG@dxiu^Y-G##dd?IF} zM?g0>w+khJE%B%lxmEDLUq3G%oI)H+p1d}yFCp;o5{B)3kL)pV=jM>_@_7$p(Sy5d z3=EgfG#V9bPE2#}qKvNHh(c2=)Xzb`vAQ?CWEdYGzo5pmEBP>`3qE;(swencQVh=A zPgSTIUt1caP}Z1bc$g$+5S-zJ)s83SmY$ZUfo%d5BA0%Ndt>P#Xa85U(}JpmhGqQE zuUNn2{RI74bH3txv}`B_#HySl z%QAspQgjM)iu5)lQhCHdK4LkS-HAoPoXC5ow`DWQEF?2API){?4Uzv=4E1ESY3&y#r`Qod2mR?P!N$6z?7CFS%u{xCFfB4`Y@6$@ za$WtyW*R#@L7`k8_0L|(I5A-VQ*?fXKgx=VN0qW87cagz2X{WK+C95gpuO~20fUx= zbcO$dPD(ZDi0A7JsuASiBQ3hvcq~UpA3zqExcj)zf4I1kVb zhdG;;03w@9DN_CL6gyK2)p7y4BGTcM{i`}LZ72PZf(iV63lCHnKr>%WAs~@ZG|sy(_t)o zYwW{^r%u%$)Uq7kW+l(^AD36D54m9*2-&+7Rri)ixc+BNuLVrw>wze~#aHd`khAqH zi{9da%a6sDV0QSXgAZJ^1l%<5#KVyG1ikpbu`w^+Vh;bFMN>YV3zgwZOn)aWNlyLx zJTy&!X?^H_Ff>sh*|h-gNAStPLrERe{FPK+MQK^j2n?VJyo3Ax{u({g0rd9%5?IdH zbYXU;FjAPChjK!uwv=Bp-jVU8C?6J~EZb7fo1JE*96s(5nw|4vp$XowCj1g)?Gv}H zx=j}ewdT&X@XL~W++sm%OOg1@cSV?j$vFyOjkOf1pQWN=vnh1bo;$~3!H zwbRUj2&|hk&iM9a^Va0IzZg<*3gvaR_TKoGlFq3gBg==6o~OqFD0vQ|N9kidQT-8+ zRI`jw_6-J_-D;~^l`x>Z84I;gX3L$OHe%+4swNr>piqF$qX9%qSZpvFCfOKhZs(uBv?SO`=Roe`4Smm_n;b<841ge7ZZygyXuJf&+@ zJU|2H(c_Y5W|&uA*2`{jEQlnS2=pLd!pC{Oz@G)dN{UC!lh^7z&rR)>i3^*_p5_4# zauyNK)+o{I>a|Gv72!yQ+~3aS=_w*e>$Nx&6BDg|G|z0LBQy9(Nfe@pP6_{39*ts? z*J?Z+t6T|A=}jTd6Cufycsx@?ac?||E`cJOHeqSuHIO6F%C7sD1A*%o8CK+^Mo52zsL@gp;1yH;HB9^F02Ve#w6^x;y0j+GEn`u11Bfr1>!iu zz5aDigKAnUboi=ZSBFcpGPF!dLdwxI*upNIxzkA_8%NFeS3opHz;m;AuH{w8mleF$)^dWbhEWM$%15Gb1@`d8|D%&WVdc*`t zMM}5^J+6r&{>+vqbF4>td4?QVJI6|qvulD4rA!E+-(6jNz zYkKUh!eui$T{ti!H!n9gKmW=tZCaFKYomg-S{oK$wAPQL9pSc{3`%uBT&yd4+rV97 zRl7_@;6q0C1%i*(d7kP+V(+zNyNGpG`9vs5_|U#yMoKD4G7Hm+UkalTA~h^QNaa(0 zBg3%bA6Y|MHF6whaDKe5WPK%mEn$Bp+d$THb14PZH$wjBj&)nn^)6iB5~z2eQ8;AJ z?$WsVn%@q_Sl3vz>1mz%;qJ&Ey_3(Er0ejexl+QBiCbeHc9By@%A+W#q@q0T!mw4( zZznd+5tCrcT~MVgKRMLv4J{@?khK`*6xVIWauinF^w)ASnK}fKs^@YM@@~8TtodOs ztIgQeAiAoe=JV6f_9BodU)V_x|40WflVTpP{mI>}i``SCd-EsRT;()x(I#G64splH zYuor(bjJv748VtkC=`|aJocMhOy}WRM{8|S{4KyQZ03zjiPrptEhUaO;mFepKufwk z28|j{|5JOEY=PeeEsN4&9bIm5xhPOVcxs6%s=Dq-%lQ`I-I`G*mw_Ko3(&3T4R|;? zab=|Q1I2)AZ}JP=$i^>m5@=se@KDCjMCX^v;|YD`jNqBSQn@d!sTp_rVglmiyhLw! zn3D^A_8)WlPz0tdh8dy~1hryz35hU*!i_T;}_W)5>FvM%tN?sd(I*^j@Th+5y>#;HEkTs zm=!%DN-`~Y$U+`waKi5hyu|F0!D^ri7uU7+7KMy&3tRhZF@VU*-cs?X7&7g5g(|6W sfG{Q?I1J1LW%?i%)=>lD1;4ymi?B0iHWT{r~^~ literal 16096 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 diff --git a/package.json b/package.json index b8671c59b0e..cfabb4378af 100644 --- a/package.json +++ b/package.json @@ -49,6 +49,8 @@ "typecheck": "bun x tsc --noEmit", "audit:high": "bun audit --audit-level=high && cd gui && bun audit --audit-level=high", "privacy:scan": "bun scripts/privacy-scan.ts", + "favicons": "bun scripts/brand-favicons.ts", + "favicons:check": "bun scripts/brand-favicons.ts --check", "skill:surface": "bun scripts/generate-ocx-skill-surface.ts", "skill:surface:check": "bun scripts/generate-ocx-skill-surface.ts --check", "structure:index": "bun scripts/structure-ssot.ts --fix", diff --git a/scripts/brand-favicons.ts b/scripts/brand-favicons.ts new file mode 100644 index 00000000000..043cbb3222e --- /dev/null +++ b/scripts/brand-favicons.ts @@ -0,0 +1,97 @@ +#!/usr/bin/env bun +/** + * Render the favicons the dashboard and the documentation site serve from the app icon vector. + * + * They used to be hand-made bitmaps with no source, and one of them was wrong in a way the file + * itself does not show: docs-site/public/favicon.png was the dark variant of the mark, white on + * transparency, 98.3% covered. A browser tab strip is light by default, so the documentation + * site's favicon was white on white. The .ico beside it carried the same artwork at 16, 32 and 48. + * + * Sizes and names here are the ones gui/index.html and docs-site/astro.config.mjs already + * reference, so nothing on either site has to change to pick these up. + * + * --check regenerates into a temporary directory and compares byte for byte, the same contract + * desktop/scripts/generate-icons.ts has for the app icon set. + */ +import { mkdtempSync, readFileSync, writeFileSync, rmSync, existsSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { basename, dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; +import { buildIco, render } from "./lib/icon-render"; + +const repoRoot = dirname(dirname(fileURLToPath(import.meta.url))); +const source = join(repoRoot, "desktop", "src-tauri", "icons", "icon.svg"); + +/** Favicon PNGs the two sites reference, by path relative to the repository root. */ +const FAVICON_SIZES: Record = { + "gui/public/favicon.png": 128, + "docs-site/public/favicon.png": 192, +}; + +/** The documentation site's .ico, which Starlight names directly, and the sizes it carries. */ +const ICO_OUTPUT = "docs-site/public/favicon.ico"; +const ICO_SIZES = [16, 32, 48]; + +/** Scratch files are flat, so a nested output name becomes one path segment. */ +function scratchName(output: string): string { + return output.replaceAll("/", "__"); +} + +function generateInto(target: string): string[] { + const produced: string[] = []; + for (const [output, size] of Object.entries(FAVICON_SIZES)) { + render(size, join(target, scratchName(output)), source); + produced.push(output); + } + + const parts: Array<{ size: number; bytes: Buffer }> = []; + for (const size of ICO_SIZES) { + const scratch = join(target, `.ico-${size}.png`); + render(size, scratch, source); + parts.push({ size, bytes: readFileSync(scratch) }); + rmSync(scratch, { force: true }); + } + writeFileSync(join(target, scratchName(ICO_OUTPUT)), buildIco(parts)); + produced.push(ICO_OUTPUT); + + return produced; +} + +function main(): number { + if (!existsSync(source)) { + console.error(`[favicons] missing source: ${source}`); + return 1; + } + const check = process.argv.includes("--check"); + const scratch = mkdtempSync(join(tmpdir(), "ocx-favicons-")); + try { + const produced = generateInto(scratch); + if (!check) { + // Written in one pass at the end, so a renderer failure part way through cannot leave one + // site on the new mark and the other on the old one. + for (const output of produced) { + writeFileSync(join(repoRoot, output), readFileSync(join(scratch, scratchName(output)))); + } + console.log(`[favicons] regenerated ${produced.length} favicons from ${basename(source)}`); + return 0; + } + + const drifted = produced.filter(output => { + const committed = join(repoRoot, output); + if (!existsSync(committed)) return true; + return !readFileSync(join(scratch, scratchName(output))).equals(readFileSync(committed)); + }); + if (drifted.length > 0) { + console.error(`[favicons] these do not match the app icon vector: ${drifted.join(", ")}`); + console.error("[favicons] regenerate with: bun run favicons"); + return 1; + } + console.log(`[favicons] ${produced.length} favicons match the source`); + return 0; + } finally { + rmSync(scratch, { recursive: true, force: true }); + } +} + +process.exit(main()); + diff --git a/scripts/lib/icon-render.ts b/scripts/lib/icon-render.ts new file mode 100644 index 00000000000..89ab2783add --- /dev/null +++ b/scripts/lib/icon-render.ts @@ -0,0 +1,162 @@ +/** + * Rendering and packing shared by the icon generators. + * + * Two generators read the same vector: desktop/scripts/generate-icons.ts builds the app icon set + * and the menu bar image, and scripts/brand-favicons.ts builds the favicons the dashboard and the + * documentation site serve. They shared nothing at first, and the PNG re-encode below is exactly + * the kind of detail that would have been copied and then fixed in one place only. + */ +import { spawnSync } from "node:child_process"; +import { readFileSync, writeFileSync } from "node:fs"; +import { deflateSync, inflateSync } from "node:zlib"; + +export function render(size: number, out: string, from: string): void { + const result = spawnSync("rsvg-convert", ["-w", String(size), "-h", String(size), from, "-o", out]); + if (result.status !== 0) { + const detail = result.error?.message ?? result.stderr?.toString().trim() ?? "unknown error"; + throw new Error(`rsvg-convert failed for ${size}px: ${detail}`); + } + writeFileSync(out, toRgba(readFileSync(out))); +} + +const PNG_SIGNATURE = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]); + +const CRC_TABLE = (() => { + const table = new Int32Array(256); + for (let n = 0; n < 256; n += 1) { + let c = n; + for (let k = 0; k < 8; k += 1) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1; + table[n] = c; + } + return table; +})(); + +function crc32(bytes: Buffer): number { + let c = -1; + for (const byte of bytes) c = CRC_TABLE[(c ^ byte) & 0xff]! ^ (c >>> 8); + return (c ^ -1) >>> 0; +} + +function chunk(type: string, data: Buffer): Buffer { + const head = Buffer.alloc(8); + head.writeUInt32BE(data.length, 0); + head.write(type, 4, "latin1"); + const crc = Buffer.alloc(4); + crc.writeUInt32BE(crc32(Buffer.concat([head.subarray(4), data])), 0); + return Buffer.concat([head, data, crc]); +} + +/** Undo one PNG scanline filter in place, given the already reconstructed row above. */ +function unfilter(kind: number, row: Buffer, prior: Buffer, stride: number): void { + for (let i = 0; i < row.length; i += 1) { + const a = i >= stride ? row[i - stride]! : 0; + const b = prior[i]!; + const c = i >= stride ? prior[i - stride]! : 0; + let add = 0; + if (kind === 1) add = a; + else if (kind === 2) add = b; + else if (kind === 3) add = (a + b) >> 1; + else if (kind === 4) { + const p = a + b - c; + const pa = Math.abs(p - a); + const pb = Math.abs(p - b); + const pc = Math.abs(p - c); + add = pa <= pb && pa <= pc ? a : pb <= pc ? b : c; + } else if (kind !== 0) throw new Error(`unknown PNG filter ${kind}`); + row[i] = (row[i]! + add) & 0xff; + } +} + +/** + * Re-encode an 8-bit truecolour PNG as truecolour-with-alpha, and leave one that already has + * alpha untouched. + * + * This exists because the icon has an opaque backdrop. librsvg notices that nothing in the render + * is transparent and drops the alpha channel, which is a valid PNG and a broken icon: Tauri's + * `generate_context!` rejects a window icon that is not RGBA, so `bun run build` fails at + * compile time with "icon ... is not RGBA". Leaving that to the renderer would also make the + * committed bytes depend on which librsvg drew them. + */ +export function toRgba(bytes: Buffer): Buffer { + if (!bytes.subarray(0, 8).equals(PNG_SIGNATURE)) throw new Error("rsvg-convert did not emit a PNG"); + + let header: { width: number; height: number } | null = null; + let colourType = -1; + const idat: Buffer[] = []; + for (let at = 8; at + 8 <= bytes.length; ) { + const length = bytes.readUInt32BE(at); + const type = bytes.subarray(at + 4, at + 8).toString("latin1"); + const data = bytes.subarray(at + 8, at + 8 + length); + if (type === "IHDR") { + colourType = data[9]!; + if (data[8] !== 8 || data[12] !== 0) throw new Error("PNG is not 8-bit non-interlaced"); + header = { width: data.readUInt32BE(0), height: data.readUInt32BE(4) }; + } else if (type === "IDAT") idat.push(Buffer.from(data)); + else if (type === "IEND") break; + at += 12 + length; + } + if (colourType === 6) return bytes; + if (colourType !== 2 || header === null) throw new Error(`unexpected PNG colour type ${colourType}`); + + const { width, height } = header; + const raw = inflateSync(Buffer.concat(idat)); + const stride = width * 3; + const out = Buffer.alloc(height * (1 + width * 4)); + let prior = Buffer.alloc(stride); + for (let y = 0; y < height; y += 1) { + const start = y * (1 + stride); + const row = Buffer.from(raw.subarray(start + 1, start + 1 + stride)); + unfilter(raw[start]!, row, prior, 3); + const target = y * (1 + width * 4); + out[target] = 0; + for (let x = 0; x < width; x += 1) { + row.copy(out, target + 1 + x * 4, x * 3, x * 3 + 3); + out[target + 1 + x * 4 + 3] = 0xff; + } + prior = row; + } + + const ihdr = Buffer.alloc(13); + ihdr.writeUInt32BE(width, 0); + ihdr.writeUInt32BE(height, 4); + ihdr[8] = 8; + ihdr[9] = 6; + return Buffer.concat([ + PNG_SIGNATURE, + chunk("IHDR", ihdr), + chunk("IDAT", deflateSync(out, { level: 9 })), + chunk("IEND", Buffer.alloc(0)), + ]); +} + +/** + * Pack PNGs into an ICO. + * + * Written here rather than shelled out because the alternative is ImageMagick, and adding a + * system dependency to regenerate an icon is a worse trade than 30 lines of a container format + * that has not changed in decades. A 256px entry records its dimension as 0, which is how ICO + * spells "256". + */ +export function buildIco(entries: Array<{ size: number; bytes: Buffer }>): Buffer { + const header = Buffer.alloc(6); + header.writeUInt16LE(0, 0); // reserved + header.writeUInt16LE(1, 2); // type: icon + header.writeUInt16LE(entries.length, 4); + + const directory = Buffer.alloc(16 * entries.length); + let offset = header.length + directory.length; + entries.forEach((entry, index) => { + const at = index * 16; + directory.writeUInt8(entry.size >= 256 ? 0 : entry.size, at); + directory.writeUInt8(entry.size >= 256 ? 0 : entry.size, at + 1); + directory.writeUInt8(0, at + 2); // palette colours + directory.writeUInt8(0, at + 3); // reserved + directory.writeUInt16LE(1, at + 4); // colour planes + directory.writeUInt16LE(32, at + 6); // bits per pixel + directory.writeUInt32LE(entry.bytes.length, at + 8); + directory.writeUInt32LE(offset, at + 12); + offset += entry.bytes.length; + }); + + return Buffer.concat([header, directory, ...entries.map(entry => entry.bytes)]); +} diff --git a/scripts/test-layout/layout.json b/scripts/test-layout/layout.json index 1e5d3154b1c..b0a1b3b81ea 100644 --- a/scripts/test-layout/layout.json +++ b/scripts/test-layout/layout.json @@ -303,6 +303,7 @@ "bridge-terminal-singleness.test.ts": "adapters", "bridge.test.ts": "adapters", "buffered-response-shape-guards.test.ts": "adapters", + "brand-favicons.test.ts": "ci-workflows", "build-desktop-icon-set.test.ts": "ci-workflows", "build-release-changelog.test.ts": "ci-workflows", "bump-dev-version.test.ts": "ci-workflows", diff --git a/tests/ci-workflows/brand-favicons.test.ts b/tests/ci-workflows/brand-favicons.test.ts new file mode 100644 index 00000000000..b782975da96 --- /dev/null +++ b/tests/ci-workflows/brand-favicons.test.ts @@ -0,0 +1,174 @@ +import { describe, expect, test } from "bun:test"; +import { readFileSync } from "node:fs"; +import { inflateSync } from "node:zlib"; +import { join } from "node:path"; +import { repoPath } from "../helpers/repo-root"; + +/** + * The favicons the dashboard and the documentation site serve are generated from the app icon + * vector by scripts/brand-favicons.ts. That script's --check compares byte for byte and needs + * rsvg-convert, which CI does not have, so what is asserted here is the shape of the committed + * set: the sizes the sites reference, the alpha channel, and the one property the old artwork + * failed. + * + * That property is worth naming. The documentation favicon used to be the dark variant of the + * mark — white on transparency — so on a light browser tab strip it was white on white and the + * site effectively had no favicon. Nothing about the file looks wrong until it is composited. + */ +const GENERATOR = repoPath("scripts/brand-favicons.ts"); + +function generatorSource(): string { + return readFileSync(GENERATOR, "utf8"); +} + +function block(source: string, opening: string, closing: string): string { + const start = source.indexOf(opening); + expect(start, `${opening} is missing from brand-favicons.ts`).toBeGreaterThan(-1); + const end = source.indexOf(closing, start + opening.length); + expect(end, `${opening} is not terminated in brand-favicons.ts`).toBeGreaterThan(-1); + return source.slice(start + opening.length, end); +} + +const PNG_SIGNATURE = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]); + +/** Truecolour with alpha. */ +const RGBA = 6; + +function ihdr(bytes: Buffer): { width: number; height: number; colourType: number } { + expect(bytes.subarray(0, 8).equals(PNG_SIGNATURE)).toBe(true); + expect(bytes.subarray(12, 16).toString("latin1")).toBe("IHDR"); + return { width: bytes.readUInt32BE(16), height: bytes.readUInt32BE(20), colourType: bytes[25]! }; +} + +/** Decode an 8-bit RGBA PNG into one flat pixel buffer. */ +function decode(bytes: Buffer): { width: number; height: number; pixels: Buffer } { + const { width, height, colourType } = ihdr(bytes); + expect(colourType).toBe(RGBA); + const idat: Buffer[] = []; + for (let at = 8; at + 8 <= bytes.length; ) { + const length = bytes.readUInt32BE(at); + const type = bytes.subarray(at + 4, at + 8).toString("latin1"); + if (type === "IDAT") idat.push(Buffer.from(bytes.subarray(at + 8, at + 8 + length))); + if (type === "IEND") break; + at += 12 + length; + } + const raw = inflateSync(Buffer.concat(idat)); + const stride = width * 4; + const pixels = Buffer.alloc(height * stride); + let prior = Buffer.alloc(stride); + for (let y = 0; y < height; y += 1) { + const start = y * (1 + stride); + const filter = raw[start]!; + const row = Buffer.from(raw.subarray(start + 1, start + 1 + stride)); + for (let i = 0; i < stride; i += 1) { + const a = i >= 4 ? row[i - 4]! : 0; + const b = prior[i]!; + const c = i >= 4 ? prior[i - 4]! : 0; + let add = 0; + if (filter === 1) add = a; + else if (filter === 2) add = b; + else if (filter === 3) add = (a + b) >> 1; + else if (filter === 4) { + const guess = a + b - c; + const pa = Math.abs(guess - a); + const pb = Math.abs(guess - b); + const pc = Math.abs(guess - c); + add = pa <= pb && pa <= pc ? a : pb <= pc ? b : c; + } + row[i] = (row[i]! + add) & 0xff; + } + row.copy(pixels, y * stride); + prior = row; + } + return { width, height, pixels }; +} + +/** Rec. 709 luminance of the pixel starting at ''at''. */ +function luminance(pixels: Buffer, at: number): number { + return 0.2126 * pixels[at]! + 0.7152 * pixels[at + 1]! + 0.0722 * pixels[at + 2]!; +} + +describe("brand favicons", () => { + const source = generatorSource(); + const pngs = new Map( + [...block(source, "const FAVICON_SIZES: Record = {", "};") + .matchAll(/"([^"]+)":\s*(\d+)/g)].map(m => [m[1]!, Number(m[2])]), + ); + const icoOutput = /const ICO_OUTPUT = "([^"]+)";/.exec(source)?.[1]; + const icoSizes = block(source, "const ICO_SIZES = [", "]") + .split(",") + .map(part => Number(part.trim())) + .filter(n => Number.isFinite(n)); + + test("the generator still declares the favicons the sites reference", () => { + expect(pngs.size).toBeGreaterThan(1); + expect(icoOutput).toBeTruthy(); + expect(icoSizes.length).toBeGreaterThan(2); + + // The sizes are not free choices: these are the numbers the two sites already ask for. + expect(readFileSync(repoPath("gui/index.html"), "utf8")).toContain('href="/favicon.png"'); + const astro = readFileSync(repoPath("docs-site/astro.config.mjs"), "utf8"); + expect(astro).toContain('favicon: "/favicon.ico"'); + expect(astro).toContain('sizes: "192x192"'); + expect(pngs.get("docs-site/public/favicon.png")).toBe(192); + }); + + test("every declared favicon is committed at its declared size with alpha intact", () => { + const wrong: string[] = []; + for (const [output, size] of pngs) { + const { width, height, colourType } = ihdr(readFileSync(repoPath(output))); + if (width !== size || height !== size) wrong.push(`${output}: ${width}x${height} != ${size}`); + if (colourType !== RGBA) wrong.push(`${output}: colour type ${colourType}`); + } + expect(wrong).toEqual([]); + }); + + test("the documentation .ico carries exactly the declared sizes as PNGs", () => { + const ico = readFileSync(repoPath(icoOutput!)); + expect(ico.readUInt16LE(0)).toBe(0); + expect(ico.readUInt16LE(2)).toBe(1); + expect(ico.readUInt16LE(4)).toBe(icoSizes.length); + const seen: number[] = []; + for (let i = 0; i < icoSizes.length; i += 1) { + const entry = 6 + i * 16; + const length = ico.readUInt32LE(entry + 8); + const offset = ico.readUInt32LE(entry + 12); + expect(offset + length).toBeLessThanOrEqual(ico.length); + const { width, height } = ihdr(ico.subarray(offset, offset + length)); + expect(width).toBe(height); + expect(ico[entry]).toBe(width % 256); + seen.push(width); + } + expect(seen).toEqual(icoSizes); + }); + + /** + * The regression the generator exists for. Both favicons are now drawn on the opaque backdrop + * the app icon uses, so they composite against a light tab strip instead of disappearing into + * it. The documentation artwork this replaces was white on transparency with a corner alpha of + * 3 and no fully opaque pixel anywhere. + */ + test("both favicons show on a light tab instead of vanishing into it", () => { + const failures: string[] = []; + for (const output of pngs.keys()) { + const { width, height, pixels } = decode(readFileSync(repoPath(output))); + + // An opaque light corner is the backdrop rather than transparency. + const corner = luminance(pixels, 0); + if (pixels[3] !== 255) failures.push(output + ": corner alpha " + pixels[3]); + if (corner < 200) failures.push(output + ": corner luminance " + corner.toFixed(0)); + + // A backdrop alone is not a favicon. An opaque white square passes the check above and is + // still invisible, so the mark has to actually be there: count the pixels that survive + // compositing and differ from the backdrop enough to read at tab size. + let ink = 0; + for (let at = 0; at < pixels.length; at += 4) { + if (pixels[at + 3]! < 128) continue; + if (Math.abs(luminance(pixels, at) - corner) > 64) ink += 1; + } + const coverage = ink / (width * height); + if (coverage < 0.1) failures.push(output + ": only " + (coverage * 100).toFixed(1) + "% ink"); + } + expect(failures, "these favicons vanish on a light tab strip").toEqual([]); + }); +}); diff --git a/tests/fixtures/test-layout-expected.json b/tests/fixtures/test-layout-expected.json index d0ff792be6d..d5413609fb8 100644 --- a/tests/fixtures/test-layout-expected.json +++ b/tests/fixtures/test-layout-expected.json @@ -134,6 +134,7 @@ "bridge-terminal-singleness.test.ts": "adapters", "bridge.test.ts": "adapters", "buffered-response-shape-guards.test.ts": "adapters", + "brand-favicons.test.ts": "ci-workflows", "build-desktop-icon-set.test.ts": "ci-workflows", "build-release-changelog.test.ts": "ci-workflows", "bump-dev-version.test.ts": "ci-workflows", From 3fa172975952d0d7358016101f56d5770fafb738 Mon Sep 17 00:00:00 2001 From: JUN Date: Mon, 21 Sep 2026 02:06:52 +0900 Subject: [PATCH 2/2] docs(brand): show the documentation favicon before and after on a light tab --- assets/pr-screenshots/favicon-light-tab.png | Bin 0 -> 11933 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 assets/pr-screenshots/favicon-light-tab.png diff --git a/assets/pr-screenshots/favicon-light-tab.png b/assets/pr-screenshots/favicon-light-tab.png new file mode 100644 index 0000000000000000000000000000000000000000..803ab3b32adf7620dc4d8c8e131138fe690f3b46 GIT binary patch literal 11933 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 tnhls(%;s;=?tf