Files
2026-09-19 10:36:32 +08:00

78 lines
3.0 KiB
JavaScript

'use strict';
function writeUint24LE(buffer, offset, value) {
buffer[offset] = value & 0xff;
buffer[offset + 1] = (value >>> 8) & 0xff;
buffer[offset + 2] = (value >>> 16) & 0xff;
}
function chunk(type, payload) {
const padding = payload.length & 1;
const output = Buffer.alloc(8 + payload.length + padding);
output.write(type, 0, 4, 'ascii');
output.writeUInt32LE(payload.length, 4);
payload.copy(output, 8);
return output;
}
function frameData(webp) {
const input = Buffer.from(webp);
if (input.length < 20 || input.toString('ascii', 0, 4) !== 'RIFF' || input.toString('ascii', 8, 12) !== 'WEBP') {
throw new Error('Invalid WebP frame');
}
const parts = [];
let hasAlpha = false;
let offset = 12;
while (offset + 8 <= input.length) {
const type = input.toString('ascii', offset, offset + 4);
const length = input.readUInt32LE(offset + 4);
const end = offset + 8 + length + (length & 1);
if (end > input.length) throw new Error('Truncated WebP frame');
if (type === 'ALPH' || type === 'VP8 ' || type === 'VP8L') parts.push(input.subarray(offset, end));
if (type === 'ALPH') hasAlpha = true;
offset = end;
}
if (!parts.some((part) => part.toString('ascii', 0, 4) === 'VP8 ' || part.toString('ascii', 0, 4) === 'VP8L')) {
throw new Error('WebP frame has no image data');
}
return { data: Buffer.concat(parts), hasAlpha };
}
function encode(options) {
const width = Number(options && options.width);
const height = Number(options && options.height);
const frames = options && options.frames;
if (!Number.isInteger(width) || width < 1 || width > 0x1000000 || !Number.isInteger(height) || height < 1 || height > 0x1000000) {
throw new RangeError('Animated WebP dimensions are invalid');
}
if (!Array.isArray(frames) || frames.length < 2) throw new RangeError('Animated WebP requires at least two frames');
const preparedFrames = frames.map((frame) => ({ frame, ...frameData(frame.webp) }));
const vp8x = Buffer.alloc(10);
vp8x[0] = 0x02 | (preparedFrames.some(({ hasAlpha }) => hasAlpha) ? 0x10 : 0);
writeUint24LE(vp8x, 4, width - 1);
writeUint24LE(vp8x, 7, height - 1);
const anim = Buffer.alloc(6);
const background = options.background || [30, 30, 30, 255];
anim.set([background[2] || 0, background[1] || 0, background[0] || 0, background[3] === undefined ? 255 : background[3]], 0);
anim.writeUInt16LE(Math.max(0, Math.min(0xffff, options.plays || 0)), 4);
const chunks = [chunk('VP8X', vp8x), chunk('ANIM', anim)];
for (const { frame, data } of preparedFrames) {
const header = Buffer.alloc(16);
writeUint24LE(header, 6, width - 1);
writeUint24LE(header, 9, height - 1);
writeUint24LE(header, 12, Math.max(1, Math.min(0xffffff, Math.round(frame.duration))));
header[15] = 0x02;
chunks.push(chunk('ANMF', Buffer.concat([header, data])));
}
const body = Buffer.concat([Buffer.from('WEBP'), ...chunks]);
const output = Buffer.alloc(8 + body.length);
output.write('RIFF', 0, 4, 'ascii');
output.writeUInt32LE(body.length, 4);
body.copy(output, 8);
return output;
}
module.exports = { encode };