86 lines
3.4 KiB
JavaScript
86 lines
3.4 KiB
JavaScript
// 把若干张 WebP 静帧封装成一个动态 WebP(ANMF 全帧、不混合)。
|
|
// 与主插件 js/animated-webp-encoder.js 是同一份实现,只是出口兼容 <script> 加载。
|
|
(function () {
|
|
'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) {
|
|
if (!webp) throw new Error('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;
|
|
}
|
|
|
|
const api = { encode };
|
|
if (typeof window !== 'undefined') window.SpineAnimatedWebpEncoder = api;
|
|
else if (typeof module !== 'undefined' && module.exports) module.exports = api;
|
|
|
|
})();
|