动态缩略图生成
This commit is contained in:
1 parent
52bdbc9f3c
commit
c113089e2d
23 files changed
+1196
No files matched your search
@@ -0,0 +1,115 @@
|
||||
'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 };
|
||||
Reference in new issue
Block a user