"use strict"; /** Verify rendered icons: decode PNG (8-bit RGBA, non-interlaced) and check * color composition + whale centering. Usage: node tools/check-icon.js a.png */ const fs = require("node:fs"); const zlib = require("node:zlib"); function decodePng(buf) { if (buf.readUInt32BE(0) !== 0x89504e47) throw new Error("not a PNG"); let pos = 8; let width = 0; let height = 0; let bitDepth = 0; let colorType = 0; const idat = []; while (pos < buf.length) { const len = buf.readUInt32BE(pos); const type = buf.toString("ascii", pos + 4, pos + 8); const data = buf.subarray(pos + 8, pos + 8 + len); if (type === "IHDR") { width = data.readUInt32BE(0); height = data.readUInt32BE(4); bitDepth = data[8]; colorType = data[9]; } else if (type === "IDAT") idat.push(data); else if (type === "IEND") break; pos += 12 + len; } if (bitDepth !== 8 || colorType !== 6) throw new Error(`unsupported PNG: depth=${bitDepth} color=${colorType}`); const raw = zlib.inflateSync(Buffer.concat(idat)); const stride = width * 4; const out = Buffer.alloc(width * height * 4); const paeth = (a, b, c) => { const p = a + b - c; const pa = Math.abs(p - a); const pb = Math.abs(p - b); const pc = Math.abs(p - c); return pa <= pb && pa <= pc ? a : pb <= pc ? b : c; }; for (let y = 0; y < height; y++) { const filter = raw[y * (stride + 1)]; const row = raw.subarray(y * (stride + 1) + 1, (y + 1) * (stride + 1)); const prev = y > 0 ? out.subarray((y - 1) * stride, y * stride) : null; for (let x = 0; x < stride; x++) { const i = y * stride + x; const left = x >= 4 ? out[i - 4] : 0; const up = prev ? prev[x] : 0; const ul = prev && x >= 4 ? prev[x - 4] : 0; let v = row[x]; if (filter === 1) v = (v + left) & 255; else if (filter === 2) v = (v + up) & 255; else if (filter === 3) v = (v + ((left + up) >> 1)) & 255; else if (filter === 4) v = (v + paeth(left, up, ul)) & 255; out[i] = v; } } return { width, height, pixels: out }; } const file = process.argv[2]; const { width, height, pixels } = decodePng(fs.readFileSync(file)); let transparent = 0; let white = 0; let blue = 0; let black = 0; let other = 0; let minX = Infinity; let maxX = -Infinity; let minY = Infinity; let maxY = -Infinity; for (let y = 0; y < height; y++) { for (let x = 0; x < width; x++) { const i = (y * width + x) * 4; const r = pixels[i]; const g = pixels[i + 1]; const b = pixels[i + 2]; const a = pixels[i + 3]; if (a < 16) { transparent++; continue; } if (r > 220 && g > 220 && b > 220) { white++; if (x < minX) minX = x; if (x > maxX) maxX = x; if (y < minY) minY = y; if (y > maxY) maxY = y; } else if (b > 180 && b > r + 40 && g > r) blue++; else if (r < 60 && g < 60 && b < 60) black++; else other++; } } const total = width * height; const pct = (n) => `${((n / total) * 100).toFixed(1)}%`; console.log(`${file}: ${width}×${height}`); console.log(` transparent=${pct(transparent)} white=${pct(white)} blue=${pct(blue)} black=${pct(black)} other=${pct(other)}`); if (white > 0) { const cx = (minX + maxX) / 2 / width; const cy = (minY + maxY) / 2 / height; console.log(` white mass bbox: x ${minX}..${maxX}, y ${minY}..${maxY}; center=(${cx.toFixed(2)}, ${cy.toFixed(2)}) of canvas`); }