加入awebp编码支持

This commit is contained in:
tianmo committed 2026-09-19 10:36:32 +08:00
1 parent eb2a7f7d91
commit 081d06c70e
22 files changed
+392 -11

No files matched your search

+52
View File
@@ -0,0 +1,52 @@
'use strict';
const assert = require('node:assert/strict');
const { encode } = require('../js/animated-webp-encoder');
function chunk(type, payload) {
const output = Buffer.alloc(8 + payload.length + (payload.length & 1));
output.write(type, 0, 4, 'ascii');
output.writeUInt32LE(payload.length, 4);
payload.copy(output, 8);
return output;
}
function stillWebp() {
const body = Buffer.concat([Buffer.from('WEBP'), chunk('VP8 ', Buffer.from([1, 2, 3, 4]))]);
const output = Buffer.alloc(8 + body.length);
output.write('RIFF', 0, 4, 'ascii');
output.writeUInt32LE(body.length, 4);
body.copy(output, 8);
return output;
}
function chunks(buffer) {
const result = [];
for (let offset = 12; offset + 8 <= buffer.length;) {
const size = buffer.readUInt32LE(offset + 4);
result.push({ type: buffer.toString('ascii', offset, offset + 4), payload: buffer.subarray(offset + 8, offset + 8 + size) });
offset += 8 + size + (size & 1);
}
return result;
}
const output = encode({
width: 400,
height: 300,
frames: [{ webp: stillWebp(), duration: 50 }, { webp: stillWebp(), duration: 75 }],
});
assert.equal(output.toString('ascii', 0, 4), 'RIFF');
assert.equal(output.readUInt32LE(4), output.length - 8);
assert.equal(output.toString('ascii', 8, 12), 'WEBP');
const parsed = chunks(output);
assert.deepEqual(parsed.map((entry) => entry.type), ['VP8X', 'ANIM', 'ANMF', 'ANMF']);
assert.equal(parsed[0].payload[0], 0x02, 'VP8X must declare animation without alpha');
assert.deepEqual([...parsed[0].payload.subarray(4, 7)], [143, 1, 0]);
assert.deepEqual([...parsed[0].payload.subarray(7, 10)], [43, 1, 0]);
assert.equal(parsed[1].payload[3], 255, 'animation background must be opaque');
assert.deepEqual([...parsed[2].payload.subarray(12, 15)], [50, 0, 0]);
assert.equal(parsed[2].payload[15], 0x02, 'full frames must replace without blending');
assert.equal(parsed[2].payload.toString('ascii', 16, 20), 'VP8 ');
assert.throws(() => encode({ width: 1, height: 1, frames: [{ webp: Buffer.alloc(0), duration: 50 }] }), /at least two frames/);
console.log('PASS: animated WebP RIFF structure, timing and opaque full-frame mode');
+6 -1
View File
@@ -57,7 +57,11 @@ for (const root of ['.', 'release/UNIVERSAL_SpineViewer']) {
assert.match(script, /const outputSize = profile\.size \|\| size/);
assert.match(script, /return smallest;/);
assert.doesNotMatch(script, /throw new Error\(t\('viewer\.animatedCannotFit'/);
assert.match(script, /extension: '\.png'/);
assert.match(script, /require\('\.\.\/js\/animated-webp-encoder\.js'\)/);
assert.match(script, /async function encodeAnimatedWebpToLimit/);
assert.match(script, /canvas\.toDataURL\('image\/webp', profile\.quality\)/);
assert.match(script, /extension: options\.format === 'apng' \? '\.png' : '\.webp'/);
assert.match(script, /for \(let alpha = 3; alpha < rgba\.length; alpha \+= 4\) rgba\[alpha\] = 255/);
assert.match(script, /const totalFrames = Math\.max\(1, frameOf\(animationLength\)\)/);
assert.match(script, /start: startFrame \/ FPS/);
assert.match(html, /id="animatedThumbStart" type="number" min="0" step="1"/);
@@ -67,6 +71,7 @@ for (const root of ['.', 'release/UNIVERSAL_SpineViewer']) {
assert.doesNotMatch(html, /id="animatedThumbQuality"/);
assert.doesNotMatch(html, /viewer\.secondsUnit/);
assert.match(html, /id="animatedThumbDialog"/);
assert.match(html, /id="animatedThumbFormat"><option value="webp">AWebP \(\.webp\)<\/option><option value="apng">APNG \(\.png\)<\/option>/);
assert.match(css, /\.animated-thumb-form \{[^}]*grid-template-columns: 96px minmax\(0, 1fr\)/);
assert.match(css, /\.animated-thumb-form input\[type="number"\]/);
}