/** * cli:scaffold-pwa — png.ts * * Deterministic solid-color PNG encoder — node:zlib deflateSync + a hand-rolled * CRC32 table, zero third-party dependencies. Emits valid 8-bit truecolor RGB * PNGs (color type 2, no interlace, filter None on every scanline). * * Used for the placeholder launcher icons (pwa-192/pwa-512/pwa-512-maskable/ * apple-touch-icon): a solid fill trivially satisfies the maskable safe zone, * and byte-determinism keeps re-runs diff-clean until the client ships real * artwork (the files are emitted skip-if-exists — dev-owned once replaced). */ import { deflateSync } from 'node:zlib' /** PNG signature: 89 50 4E 47 0D 0A 1A 0A. */ export const PNG_SIGNATURE = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]) const CRC_TABLE = (() => { const table = new Uint32Array(256) for (let n = 0; n < 256; n++) { let c = n for (let k = 0; k < 8; k++) { c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1 } table[n] = c >>> 0 } return table })() function crc32(buf: Buffer): number { let crc = 0xffffffff for (let i = 0; i < buf.length; i++) { crc = CRC_TABLE[(crc ^ buf[i]) & 0xff] ^ (crc >>> 8) } return (crc ^ 0xffffffff) >>> 0 } function chunk(type: string, data: Buffer): Buffer { const len = Buffer.alloc(4) len.writeUInt32BE(data.length, 0) const typeBuf = Buffer.from(type, 'ascii') const crcBuf = Buffer.alloc(4) crcBuf.writeUInt32BE(crc32(Buffer.concat([typeBuf, data])), 0) return Buffer.concat([len, typeBuf, data, crcBuf]) } /** Parse '#rrggbb' → [r, g, b]. Throws on any other shape. */ export function parseHexColor(rgbHex: string): [number, number, number] { const m = /^#([0-9a-fA-F]{6})$/.exec(rgbHex) if (!m) { throw new Error(`parseHexColor: expected '#rrggbb', got '${rgbHex}'`) } const v = parseInt(m[1], 16) return [(v >> 16) & 0xff, (v >> 8) & 0xff, v & 0xff] } /** * Encode a width×height solid-color PNG (8-bit RGB, no alpha, no interlace). * Deterministic: same inputs → byte-identical output. */ export function encodePng(width: number, height: number, rgbHex: string): Buffer { if (!Number.isInteger(width) || !Number.isInteger(height) || width <= 0 || height <= 0) { throw new Error(`encodePng: invalid dimensions ${width}x${height}`) } const [r, g, b] = parseHexColor(rgbHex) const ihdr = Buffer.alloc(13) ihdr.writeUInt32BE(width, 0) ihdr.writeUInt32BE(height, 4) ihdr[8] = 8 // bit depth ihdr[9] = 2 // color type 2 = truecolor RGB ihdr[10] = 0 // compression: deflate ihdr[11] = 0 // filter method 0 ihdr[12] = 0 // no interlace // Raw image data: per scanline, 1 filter byte (0 = None) + width*3 RGB bytes. const stride = 1 + width * 3 const raw = Buffer.alloc(height * stride) for (let y = 0; y < height; y++) { const row = y * stride raw[row] = 0 for (let x = 0; x < width; x++) { const px = row + 1 + x * 3 raw[px] = r raw[px + 1] = g raw[px + 2] = b } } const idat = deflateSync(raw, { level: 9 }) return Buffer.concat([ PNG_SIGNATURE, chunk('IHDR', ihdr), chunk('IDAT', idat), chunk('IEND', Buffer.alloc(0)), ]) }