/**
* PNG-кодек без зависимостей (zlib из node:zlib): RGBA-пиксели <-> PNG-буфер.
* Энкодер пишет color type 6 (RGBA), bit depth 8, без чересстрочности;
* декодер читает ровно этот формат.
*/
import { deflateSync, inflateSync } from 'node:zlib';
// CRC32 для чанков PNG.
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) {
let c = 0xffffffff;
for (const b of buf) {
c = CRC_TABLE[(c ^ b) & 0xff] ^ (c >>> 8);
}
return (c ^ 0xffffffff) >>> 0;
}
function chunk(type, data) {
const len = Buffer.alloc(4);
len.writeUInt32BE(data.length);
const body = Buffer.concat([Buffer.from(type, 'ascii'), data]);
const crc = Buffer.alloc(4);
crc.writeUInt32BE(crc32(body));
return Buffer.concat([len, body, crc]);
}
/** rgba — Uint8Array длиной w*h*4. Возвращает PNG-буфер. */
export function encodePng(width, height, rgba) {
// Сырые строки: байт фильтра (0 = None) + строка пикселей.
const raw = Buffer.alloc(height * (width * 4 + 1));
for (let y = 0; y < height; y++) {
raw[y * (width * 4 + 1)] = 0;
rgba.copy(raw, y * (width * 4 + 1) + 1, y * width * 4, (y + 1) * width * 4);
}
const ihdr = Buffer.alloc(13);
ihdr.writeUInt32BE(width, 0);
ihdr.writeUInt32BE(height, 4);
ihdr[8] = 8; // bit depth
ihdr[9] = 6; // RGBA
ihdr[10] = 0;
ihdr[11] = 0;
ihdr[12] = 0;
return Buffer.concat([
Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]),
chunk('IHDR', ihdr),
chunk('IDAT', deflateSync(raw, { level: 9 })),
chunk('IEND', Buffer.alloc(0))
]);
}
/** PNG-буфер -> { width, height, rgba: Buffer }. Поддержан только color type 6, 8 бит. */
export function decodePng(buf) {
if (buf.readUInt32BE(0) !== 0x89504e47) throw new Error('не PNG');
let pos = 8, width = 0, height = 0, bitDepth = 0, 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);
pos += 12 + len;
}
if (colorType !== 6 || bitDepth !== 8) {
throw new Error(`PNG colorType=${colorType} bitDepth=${bitDepth} — жду RGBA 8-бит`);
}
const raw = inflateSync(Buffer.concat(idat));
const stride = width * 4;
const rgba = Buffer.alloc(height * stride);
let prev = Buffer.alloc(stride);
for (let y = 0; y < height; y++) {
const filter = raw[y * (stride + 1)];
const line = raw.subarray(y * (stride + 1) + 1, (y + 1) * (stride + 1));
const cur = rgba.subarray(y * stride, (y + 1) * stride);
for (let x = 0; x < stride; x++) {
const a = x >= 4 ? cur[x - 4] : 0;
const b = prev[x];
const c = x >= 4 ? prev[x - 4] : 0;
let v = line[x];
if (filter === 1) v += a;
else if (filter === 2) v += b;
else if (filter === 3) v += (a + b) >> 1;
else if (filter === 4) {
const p = a + b - c, pa = Math.abs(p - a), pb = Math.abs(p - b), pc = Math.abs(p - c);
v += pa <= pb && pa <= pc ? a : pb <= pc ? b : c;
}
cur[x] = v & 0xff;
}
prev = cur;
}
return { width, height, rgba };
}