B. 二分定位 + 邻域采样(我推荐) 把配置的语义从"这就是要生成的 10 档"改成"允许搜索的参数梯度"(比如分辨率梯度、帧率梯度、质量梯度三张表)。运行时: 1. 用每个组合只编 2 帧的方式粗估体积(很便宜); 2. 靠单调性做二分,几轮就找到"体积刚好不超过目标"的参数点; 3. 在这个点周围取 10 个组合(略高、略低各几个),完整生成。 成本大概是十几次估算编码 + 10 档完整编码,和现在基本持平甚至更省(估算只编 2 帧)。效果是候选真正围绕你输入的目标分布——比如你填 30 KB,出来的就是 22、26、29、31、35 KB 这种,而不是 5 KB 和 300 KB 各一个。 代价是你现在这套配置格式要推翻重做——从"精确档位表"变成"参数范围表"
79 lines
3.1 KiB
JavaScript
79 lines
3.1 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) {
|
|
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;
|
|
}
|
|
|
|
module.exports = { encode };
|