'use strict'; const assert = require('node:assert/strict'); const fs = require('node:fs'); const vm = require('node:vm'); const { encode } = require('../js/apng-encoder'); const { encode: encodeWebp } = require('../js/animated-webp-encoder'); const profilesModule = require('../js/animated-profiles'); function chunks(buffer) { const result = []; let offset = 8; while (offset < buffer.length) { const length = buffer.readUInt32BE(offset); const type = buffer.toString('ascii', offset + 4, offset + 8); result.push({ type, data: buffer.subarray(offset + 8, offset + 8 + length) }); offset += length + 12; } return result; } const red = new Uint8Array(16); const blue = new Uint8Array(16); for (let index = 0; index < 16; index += 4) { red.set([255, 0, 0, 255], index); blue.set([0, 0, 255, 255], index); } const png = encode({ width: 2, height: 2, frames: [ { rgba: red, delayNumerator: 125, delayDenominator: 1000 }, { rgba: blue, delayNumerator: 125, delayDenominator: 1000 }, ], }); assert.deepEqual([...png.subarray(0, 8)], [137, 80, 78, 71, 13, 10, 26, 10]); const parsed = chunks(png); assert.deepEqual(parsed.map((entry) => entry.type), ['IHDR', 'acTL', 'fcTL', 'IDAT', 'fcTL', 'fdAT', 'IEND']); assert.equal(parsed.find((entry) => entry.type === 'acTL').data.readUInt32BE(0), 2); assert.equal(parsed[2].data.readUInt32BE(0), 0); assert.equal(parsed[4].data.readUInt32BE(0), 1); assert.equal(parsed[5].data.readUInt32BE(0), 2); // WebP 帧必须是 { webp, duration } 结构:直接塞 Buffer 会让编码器读到 undefined 后报错。 assert.throws( () => encodeWebp({ width: 2, height: 2, plays: 0, frames: [Buffer.alloc(4), Buffer.alloc(4)] }), /WebP 帧数据缺失/, ); // 档位表是可以自由增删的配置:只校验结构约束,不锁死具体数值, // 否则每次调档位都要连着改测试。 const priorities = profilesModule.ANIMATED_PRIORITIES; assert.ok(priorities[profilesModule.DEFAULT_PRIORITY], '默认优先级必须存在于配置里'); for (const [name, plan] of Object.entries(priorities)) { assert.ok(Array.isArray(plan) && plan.length >= 2, name + ' 至少要有两档'); for (const item of plan) { assert.ok(Array.isArray(item) && item.length === 3, name + ' 每项必须是 [细节分辨率, 帧率, 质量]'); const [detailSize, fps, quality] = item; assert.ok(Number.isInteger(detailSize) && detailSize > 0 && detailSize <= 400, name + ' 细节分辨率必须是 1-400 的整数'); assert.ok(Number.isFinite(fps) && fps > 0, name + ' 帧率必须是正数'); assert.ok(quality > 0 && quality <= 1, name + ' 编码质量必须落在 (0,1] 内'); } const list = profilesModule.animatedProfiles(name); assert.equal(list.length, plan.length, name + ' 档位数必须跟随配置长度'); list.forEach((profile, index) => { assert.equal(profile.size, plan[index][0], name + ' 第 ' + (index + 1) + ' 档分辨率'); assert.equal(profile.fps, plan[index][1], name + ' 第 ' + (index + 1) + ' 档帧率'); assert.equal(profile.quality, plan[index][2], name + ' 第 ' + (index + 1) + ' 档质量'); }); } // 未知优先级回落到默认档位表,不能抛错。 assert.deepEqual( profilesModule.animatedProfiles('unknown'), profilesModule.animatedProfiles(profilesModule.DEFAULT_PRIORITY), ); for (const root of ['.', 'release/UNIVERSAL_SpineViewer']) { const html = fs.readFileSync(root + '/viewer/spine.html', 'utf8'); const css = fs.readFileSync(root + '/viewer/spine.css', 'utf8'); const script = html.match(/