115 lines
4.8 KiB
TypeScript
115 lines
4.8 KiB
TypeScript
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
|
|
// Real images for the logo tests: a PNG written by hand, so a test can break one chunk, and a WebP
|
|
// that libwebp wrote. The kit's logo checks read structure and decode pixels, so a few magic bytes
|
|
// no longer stand in for an image.
|
|
|
|
import { deflateSync, inflateSync } from "node:zlib"
|
|
|
|
const 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
|
|
})()
|
|
|
|
const crc32 = (bytes: Uint8Array): number => {
|
|
let c = 0xffffffff
|
|
for (const byte of bytes) c = (TABLE[(c ^ byte) & 0xff] as number) ^ (c >>> 8)
|
|
return (c ^ 0xffffffff) >>> 0
|
|
}
|
|
|
|
const concat = (...parts: Uint8Array[]): Buffer => Buffer.concat(parts)
|
|
|
|
export const chunk = (type: string, data: Uint8Array): Buffer => {
|
|
const out = Buffer.alloc(12 + data.length)
|
|
out.writeUInt32BE(data.length, 0)
|
|
out.write(type, 4, "latin1")
|
|
out.set(data, 8)
|
|
out.writeUInt32BE(crc32(out.subarray(4, 8 + data.length)), 8 + data.length)
|
|
return out
|
|
}
|
|
|
|
const SIGNATURE = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a])
|
|
|
|
/** A PNG over `pixel(x, y)` -> [r, g, b, a]. */
|
|
export function pngOf(width: number, height: number, pixel: (x: number, y: number) => readonly number[]): Buffer {
|
|
const header = Buffer.alloc(13)
|
|
header.writeUInt32BE(width, 0)
|
|
header.writeUInt32BE(height, 4)
|
|
header.set([8, 6, 0, 0, 0], 8)
|
|
const rows = Buffer.alloc(height * (1 + width * 4))
|
|
for (let y = 0; y < height; y++)
|
|
for (let x = 0; x < width; x++) rows.set(pixel(x, y), y * (1 + width * 4) + 1 + x * 4)
|
|
return concat(SIGNATURE, chunk("IHDR", header), chunk("IDAT", deflateSync(rows)), chunk("IEND", new Uint8Array()))
|
|
}
|
|
|
|
/** A teal disc on transparent that fills the height. */
|
|
export const markPng = (width: number, height: number, colour: readonly number[] = [0x80, 0xcd, 0xc6, 255]): Buffer =>
|
|
pngOf(width, height, (x, y) => {
|
|
const dx = x - width / 2
|
|
const dy = y - height / 2
|
|
return dx * dx + dy * dy < (Math.min(width, height) / 2) ** 2 ? colour : [0, 0, 0, 0]
|
|
})
|
|
|
|
/** Pseudo-random pixels, which PNG cannot compress: a file about as large as its pixels. */
|
|
export function noisePng(width: number, height: number): Buffer {
|
|
let seed = 12345
|
|
const next = () => {
|
|
seed = (Math.imul(seed, 1103515245) + 12345) >>> 0
|
|
return seed >>> 24
|
|
}
|
|
return pngOf(width, height, () => [next(), next(), next(), 255])
|
|
}
|
|
|
|
/** A lossless WebP of a teal disc, 400 x 100, written by libwebp. */
|
|
export const MARK_WEBP = Buffer.from(
|
|
"UklGRvwAAABXRUJQVlA4TO8AAAAvj8EYEA8wzQM4xvMf8HBU27bT0DNEwpeCNJCGlC/hD9NvKuXe91InEf135EaSInlgeU8DlfmEzM4yACXbFuj4j/SHeNfSX9Nfnu/A/oT+QQToH6RHgfxN+hfPj8CEJzn1l/Qfjs821P+i/1V6Fqj/Rf+r47OefxgAlmFCTjxw6IfOOGVjV5zS2pWmBLu2Uzq7jlN6u85T8NphUv7KDdPK/6D3cdV7G3chH3dsH5OHjwnKxyToY6JVzNmKWd5FwlDkHkW2UuQ3RUZU5FBF1lXkaUFmV/QCiu5B0W8oOhRBT6PoghR9k6LTEvRmdrq5DAA=",
|
|
"base64",
|
|
)
|
|
|
|
export const dimensions = (png: Uint8Array): { width: number; height: number } => ({
|
|
width: Buffer.from(png).readUInt32BE(16),
|
|
height: Buffer.from(png).readUInt32BE(20),
|
|
})
|
|
|
|
/** The [r, g, b, a] of one pixel of an 8-bit RGBA, non-interlaced PNG (what the renderers write), by unfiltering its rows. */
|
|
export function pixelAt(png: Uint8Array, x: number, y: number): number[] {
|
|
const bytes = Buffer.from(png)
|
|
const { width, height } = dimensions(bytes)
|
|
if (bytes[24] !== 8 || bytes[25] !== 6 || bytes[28] !== 0)
|
|
throw new Error("pixelAt reads 8-bit RGBA PNGs without interlace")
|
|
const data: Buffer[] = []
|
|
for (let at = 8; at < bytes.length; ) {
|
|
const length = bytes.readUInt32BE(at)
|
|
if (bytes.toString("latin1", at + 4, at + 8) === "IDAT") data.push(bytes.subarray(at + 8, at + 8 + length))
|
|
at += 12 + length
|
|
}
|
|
const raw = inflateSync(Buffer.concat(data))
|
|
const stride = width * 4
|
|
const rows: Buffer[] = []
|
|
for (let row = 0; row < height; row++) {
|
|
const filter = raw[row * (stride + 1)] as number
|
|
const line = Buffer.from(raw.subarray(row * (stride + 1) + 1, (row + 1) * (stride + 1)))
|
|
const above = rows[row - 1]
|
|
for (let i = 0; i < stride; i++) {
|
|
const left = i >= 4 ? (line[i - 4] as number) : 0
|
|
const up = above ? (above[i] as number) : 0
|
|
const corner = above && i >= 4 ? (above[i - 4] as number) : 0
|
|
const predictor = [0, left, up, (left + up) >> 1, paeth(left, up, corner)][filter] as number
|
|
line[i] = ((line[i] as number) + predictor) & 0xff
|
|
}
|
|
rows.push(line)
|
|
}
|
|
const line = rows[y] as Buffer
|
|
return [line[x * 4], line[x * 4 + 1], line[x * 4 + 2], line[x * 4 + 3]] as number[]
|
|
}
|
|
|
|
function paeth(a: number, b: number, c: number): number {
|
|
const p = a + b - c
|
|
const [pa, pb, pc] = [Math.abs(p - a), Math.abs(p - b), Math.abs(p - c)]
|
|
return pa <= pb && pa <= pc ? a : pb <= pc ? b : c
|
|
}
|