Files
atlas-seller-kit/apps/worker/test/media-aspect-ratio.test.mjs
T

187 lines
9.0 KiB
JavaScript

import assert from 'node:assert/strict';
import { test } from 'node:test';
import sharp from 'sharp';
import { GetObjectCommand, PutObjectCommand } from '@aws-sdk/client-s3';
import { renderThreeFour, runAspectRatioDerivative } from '../src/media-aspect-ratio.ts';
const assertColorNear = (actual, expected) => {
for (let channel = 0; channel < 3; channel++)
assert.ok(Math.abs(actual[channel] - expected[channel]) <= 6, `${actual} 与 ${expected} 色差过大`);
};
const readPixels = async (output) => {
const { data, info } = await sharp(output).raw().toBuffer({ resolveWithObject: true });
assert.equal(info.width, 1200);
assert.equal(info.height, 1600);
assert.equal(info.channels, 3);
return (x, y) => [...data.subarray((y * info.width + x) * 3, (y * info.width + x) * 3 + 3)];
};
test('正方形商品图等比缩放后,上下扩充处使用红色边缘色', async () => {
const source = await sharp({
create: { width: 400, height: 400, channels: 3, background: '#ff0000' },
}).png().toBuffer();
const pixel = await readPixels(await renderThreeFour(source));
assertColorNear(pixel(600, 0), [255, 0, 0]);
assertColorNear(pixel(600, 800), [255, 0, 0]);
assertColorNear(pixel(600, 1599), [255, 0, 0]);
});
test('上下边缘颜色不同,各自填充对应的一侧', async () => {
const source = await sharp({
create: { width: 400, height: 400, channels: 3, background: '#0000ff' },
}).composite([
{ input: { create: { width: 400, height: 48, channels: 3, background: '#ff0000' } }, left: 0, top: 0 },
{ input: { create: { width: 400, height: 48, channels: 3, background: '#808080' } }, left: 0, top: 352 },
]).png().toBuffer();
const pixel = await readPixels(await renderThreeFour(source));
assertColorNear(pixel(600, 0), [255, 0, 0]);
assertColorNear(pixel(600, 800), [0, 0, 255]);
assertColorNear(pixel(600, 1599), [128, 128, 128]);
});
test('同一边缘有多种颜色时,接缝相接且远处不形成竖向条纹', async () => {
const source = await sharp({
create: { width: 400, height: 400, channels: 3, background: '#808080' },
}).composite([
{ input: { create: { width: 200, height: 48, channels: 3, background: '#ff0000' } }, left: 0, top: 0 },
{ input: { create: { width: 200, height: 48, channels: 3, background: '#0000ff' } }, left: 200, top: 0 },
{ input: { create: { width: 200, height: 48, channels: 3, background: '#ffff00' } }, left: 0, top: 352 },
{ input: { create: { width: 200, height: 48, channels: 3, background: '#00ff00' } }, left: 200, top: 352 },
]).png().toBuffer();
const pixel = await readPixels(await renderThreeFour(source));
for (const [x, topColor, bottomColor] of [
[300, [255, 0, 0], [255, 255, 0]],
[900, [0, 0, 255], [0, 255, 0]],
]) {
assertColorNear(pixel(x, 199), topColor);
assertColorNear(pixel(x, 199), pixel(x, 200));
assertColorNear(pixel(x, 1400), pixel(x, 1399));
assertColorNear(pixel(x, 1400), bottomColor);
}
assertColorNear(pixel(300, 0), pixel(900, 0));
assertColorNear(pixel(300, 1599), pixel(900, 1599));
assertColorNear(pixel(300, 0), [128, 0, 128]);
assertColorNear(pixel(300, 1599), [128, 255, 0]);
});
test('竖长图片左右扩充处分别使用左绿右黄的边缘色', async () => {
const source = await sharp({
create: { width: 300, height: 800, channels: 3, background: '#0000ff' },
}).composite([
{ input: { create: { width: 36, height: 800, channels: 3, background: '#00ff00' } }, left: 0, top: 0 },
{ input: { create: { width: 36, height: 800, channels: 3, background: '#ffff00' } }, left: 264, top: 0 },
]).png().toBuffer();
const pixel = await readPixels(await renderThreeFour(source));
assertColorNear(pixel(0, 800), [0, 255, 0]);
assertColorNear(pixel(600, 800), [0, 0, 255]);
assertColorNear(pixel(1199, 800), [255, 255, 0]);
});
test('左右边缘沿高度变色时,接缝相接且远处不形成横向条纹', async () => {
const source = await sharp({
create: { width: 300, height: 800, channels: 3, background: '#808080' },
}).composite([
{ input: { create: { width: 36, height: 400, channels: 3, background: '#00ff00' } }, left: 0, top: 0 },
{ input: { create: { width: 36, height: 400, channels: 3, background: '#0000ff' } }, left: 0, top: 400 },
{ input: { create: { width: 36, height: 400, channels: 3, background: '#ffff00' } }, left: 264, top: 0 },
{ input: { create: { width: 36, height: 400, channels: 3, background: '#ff00ff' } }, left: 264, top: 400 },
]).png().toBuffer();
const pixel = await readPixels(await renderThreeFour(source));
for (const [y, leftColor, rightColor] of [
[400, [0, 255, 0], [255, 255, 0]],
[1200, [0, 0, 255], [255, 0, 255]],
]) {
assertColorNear(pixel(299, y), leftColor);
assertColorNear(pixel(299, y), pixel(300, y));
assertColorNear(pixel(900, y), pixel(899, y));
assertColorNear(pixel(900, y), rightColor);
}
assertColorNear(pixel(0, 400), pixel(0, 1200));
assertColorNear(pixel(1199, 400), pixel(1199, 1200));
assertColorNear(pixel(0, 400), [0, 128, 128]);
assertColorNear(pixel(1199, 400), [255, 128, 128]);
});
test('透明边缘以白底取色', async () => {
const source = await sharp({
create: { width: 400, height: 400, channels: 4, background: '#00000000' },
}).composite([{ input: {
create: { width: 200, height: 200, channels: 3, background: '#ff0000' },
}, left: 100, top: 100 }]).png().toBuffer();
const pixel = await readPixels(await renderThreeFour(source));
assertColorNear(pixel(600, 0), [255, 255, 255]);
assertColorNear(pixel(600, 800), [255, 0, 0]);
assertColorNear(pixel(600, 1599), [255, 255, 255]);
});
test('取色和留白遵循 EXIF 旋转后的显示方向', async () => {
const source = await sharp({
create: { width: 400, height: 200, channels: 3, background: '#0000ff' },
}).composite([
{ input: { create: { width: 400, height: 24, channels: 3, background: '#ff0000' } }, left: 0, top: 0 },
{ input: { create: { width: 400, height: 24, channels: 3, background: '#808080' } }, left: 0, top: 176 },
]).jpeg({ quality: 100 }).withMetadata({ orientation: 6 }).toBuffer();
const pixel = await readPixels(await renderThreeFour(source));
assertColorNear(pixel(0, 800), [128, 128, 128]);
assertColorNear(pixel(1199, 800), [255, 0, 0]);
});
test('拒绝不受支持的来源格式', async () => {
const source = await sharp({
create: { width: 8, height: 8, channels: 3, background: '#ff0000' },
}).gif().toBuffer();
await assert.rejects(renderThreeFour(source), /格式不受支持/);
});
test('任务读取原图、写入独立派生素材,并完成持久化状态', async () => {
const source = await sharp({
create: { width: 400, height: 400, channels: 3, background: '#ff0000' },
}).png().toBuffer();
const writes = [];
const storage = { send: async (command) => {
if (command instanceof GetObjectCommand) return {
Body: (async function* () { yield source; })(),
};
if (command instanceof PutObjectCommand) {
writes.push(command.input);
return {};
}
throw new Error('未知存储操作');
} };
let status = 'queued';
let outputAssetId = null;
const queries = [];
const database = { workspaceTransaction: async (_workspaceId, work) => work({
query: async (sql, args) => {
queries.push({ sql, args });
if (sql.includes('SELECT d.status')) return { rows: [{
status,
storage_provider: 'local_minio',
bucket: 'test-bucket',
object_key: 'original.png',
}] };
if (sql.includes("SET status = 'running'")) status = 'running';
if (sql.includes("SET status = 'succeeded'")) status = 'succeeded';
if (sql.includes('SELECT id FROM atlas.media_assets')) return { rows: [] };
if (sql.includes('INSERT INTO atlas.media_assets')) return { rows: [{ id: 'output-asset' }] };
if (sql.includes("result = jsonb_build_object")) outputAssetId = args[1];
return { rows: [] };
},
}) };
const job = {
workspaceId: 'workspace-id',
payload: { derivative_id: 'derivative-id' },
};
await runAspectRatioDerivative(database, storage, job);
assert.equal(status, 'succeeded');
assert.equal(outputAssetId, 'output-asset');
assert.equal(writes.length, 1);
assert.equal(writes[0].Bucket, 'test-bucket');
assert.match(writes[0].Key, /^derivatives\/workspace-id\/three-four\/[a-f0-9]{64}\.jpg$/);
assert.deepEqual(await sharp(writes[0].Body).metadata().then(({ width, height }) => [width, height]), [1200, 1600]);
await runAspectRatioDerivative(database, storage, job);
assert.equal(writes.length, 1);
assert.ok(queries.some(({ sql }) => sql.includes('UPDATE atlas.operations')));
});