'use strict'; const zlib = require('zlib'); const PNG_SIGNATURE = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]); const crcTable = new Uint32Array(256); for (let n = 0; n < 256; n++) { let value = n; for (let k = 0; k < 8; k++) value = (value & 1) ? (0xedb88320 ^ (value >>> 1)) : (value >>> 1); crcTable[n] = value >>> 0; } function crc32(buffer) { let value = 0xffffffff; for (const byte of buffer) value = crcTable[(value ^ byte) & 0xff] ^ (value >>> 8); return (value ^ 0xffffffff) >>> 0; } function chunk(type, data) { const name = Buffer.from(type, 'ascii'); const body = Buffer.from(data || []); const out = Buffer.allocUnsafe(12 + body.length); out.writeUInt32BE(body.length, 0); name.copy(out, 4); body.copy(out, 8); out.writeUInt32BE(crc32(Buffer.concat([name, body])), 8 + body.length); return out; } function paeth(left, above, upperLeft) { const estimate = left + above - upperLeft; const leftDistance = Math.abs(estimate - left); const aboveDistance = Math.abs(estimate - above); const cornerDistance = Math.abs(estimate - upperLeft); return leftDistance <= aboveDistance && leftDistance <= cornerDistance ? left : aboveDistance <= cornerDistance ? above : upperLeft; } function filterFrame(rgba, width, height) { const stride = width * 4; const result = Buffer.allocUnsafe((stride + 1) * height); const candidates = Array.from({ length: 5 }, () => Buffer.allocUnsafe(stride)); for (let y = 0; y < height; y++) { const rowStart = y * stride; let bestType = 0; let bestScore = Infinity; for (let type = 0; type < 5; type++) { const candidate = candidates[type]; let score = 0; for (let x = 0; x < stride; x++) { const raw = rgba[rowStart + x]; const left = x >= 4 ? rgba[rowStart + x - 4] : 0; const above = y > 0 ? rgba[rowStart - stride + x] : 0; const upperLeft = y > 0 && x >= 4 ? rgba[rowStart - stride + x - 4] : 0; let predicted = 0; if (type === 1) predicted = left; else if (type === 2) predicted = above; else if (type === 3) predicted = Math.floor((left + above) / 2); else if (type === 4) predicted = paeth(left, above, upperLeft); const filtered = (raw - predicted + 256) & 0xff; candidate[x] = filtered; score += Math.abs(filtered < 128 ? filtered : filtered - 256); } if (score < bestScore) { bestScore = score; bestType = type; } } const outputStart = y * (stride + 1); result[outputStart] = bestType; candidates[bestType].copy(result, outputStart + 1); } return result; } function encode(options) { const width = Number(options.width); const height = Number(options.height); const frames = options.frames || []; if (!Number.isInteger(width) || width <= 0 || !Number.isInteger(height) || height <= 0) throw new TypeError('APNG 尺寸无效'); if (!frames.length) throw new TypeError('APNG 至少需要一帧'); const ihdr = Buffer.alloc(13); ihdr.writeUInt32BE(width, 0); ihdr.writeUInt32BE(height, 4); ihdr[8] = 8; ihdr[9] = 6; // 8-bit RGBA const animationControl = Buffer.alloc(8); animationControl.writeUInt32BE(frames.length, 0); animationControl.writeUInt32BE(Math.max(0, Number(options.plays) || 0), 4); const chunks = [PNG_SIGNATURE, chunk('IHDR', ihdr), chunk('acTL', animationControl)]; let sequence = 0; for (let index = 0; index < frames.length; index++) { const frame = frames[index]; const rgba = Buffer.from(frame.rgba.buffer, frame.rgba.byteOffset, frame.rgba.byteLength); if (rgba.length !== width * height * 4) throw new TypeError('APNG 帧像素数量无效'); const control = Buffer.alloc(26); control.writeUInt32BE(sequence++, 0); control.writeUInt32BE(width, 4); control.writeUInt32BE(height, 8); control.writeUInt32BE(0, 12); control.writeUInt32BE(0, 16); control.writeUInt16BE(Math.max(1, Math.min(65535, Number(frame.delayNumerator) || 1)), 20); control.writeUInt16BE(Math.max(1, Math.min(65535, Number(frame.delayDenominator) || 1000)), 22); control[24] = 0; // keep the previous frame control[25] = 0; // replace the full canvas chunks.push(chunk('fcTL', control)); const compressed = zlib.deflateSync(filterFrame(rgba, width, height), { level: Math.max(0, Math.min(9, Number(options.compressionLevel) || 9)), }); if (index === 0) chunks.push(chunk('IDAT', compressed)); else { const frameData = Buffer.allocUnsafe(4 + compressed.length); frameData.writeUInt32BE(sequence++, 0); compressed.copy(frameData, 4); chunks.push(chunk('fdAT', frameData)); } } chunks.push(chunk('IEND')); return Buffer.concat(chunks); } module.exports = { encode };