Enhance article photo handling and API documentation.

- Updated API documentation to reflect changes in article data structure.
- Improved ArticlePhoto component to support dynamic sizing and object fit options.
- Added performance logging for image upload in ArticlePhotosDialog and ArticlePhotoTile components.
- Enhanced ArticleTile to conditionally render photos and adjusted styling for better layout.
- Optimized image preparation process with configurable AVIF encoding speed.
This commit is contained in:
seb
2026-08-01 04:32:49 +02:00
parent 701487f525
commit 09fb1da335
10 changed files with 368 additions and 36 deletions

View File

@@ -0,0 +1,107 @@
#!/usr/bin/env node
/**
* Benchmark AVIF encode options (+ JPEG/WebP reference) on a real fixture.
* Usage: node scripts/bench-avif-encode.mjs [path-to-avif]
*/
import { readFile } from 'node:fs/promises';
import { resolve, dirname } from 'node:path';
import { fileURLToPath, pathToFileURL } from 'node:url';
import decodeAvif, { init as initAvifDecode } from '@jsquash/avif/decode.js';
import encodeAvif, { init as initAvifEncode } from '@jsquash/avif/encode.js';
import encodeJpeg, { init as initJpegEncode } from '@jsquash/jpeg/encode.js';
import encodeWebp, { init as initWebpEncode } from '@jsquash/webp/encode.js';
const __dirname = dirname(fileURLToPath(import.meta.url));
const root = resolve(__dirname, '..');
const RUNS = 3;
const fixturePath = resolve(
process.argv[2] ?? resolve(root, 'tmp/bench-source.avif'),
);
async function compileWasm(relPath) {
const bytes = await readFile(resolve(root, relPath));
return WebAssembly.compile(bytes);
}
function median(values) {
const sorted = [...values].sort((a, b) => a - b);
return sorted[Math.floor(sorted.length / 2)];
}
async function timeEncode(label, fn) {
const times = [];
let bytes = 0;
for (let i = 0; i < RUNS; i += 1) {
const start = performance.now();
const buf = await fn();
times.push(performance.now() - start);
bytes = buf.byteLength ?? buf.length;
}
return {
label,
encodeMs: Math.round(median(times)),
outputBytes: bytes,
};
}
console.log('init wasm…');
await initAvifDecode(await compileWasm('node_modules/@jsquash/avif/codec/dec/avif_dec.wasm'));
await initAvifEncode(await compileWasm('node_modules/@jsquash/avif/codec/enc/avif_enc.wasm'));
await initJpegEncode(await compileWasm('node_modules/@jsquash/jpeg/codec/enc/mozjpeg_enc.wasm'));
await initWebpEncode(await compileWasm('node_modules/@jsquash/webp/codec/enc/webp_enc.wasm'));
const raw = await readFile(fixturePath);
console.log(`fixture: ${fixturePath} (${raw.byteLength} bytes)`);
const decodeStarted = performance.now();
const imageData = await decodeAvif(raw.buffer.slice(raw.byteOffset, raw.byteOffset + raw.byteLength));
const decodeMs = Math.round(performance.now() - decodeStarted);
console.log(`decode: ${decodeMs}ms → ${imageData.width}×${imageData.height}`);
// Warmup
await encodeAvif(imageData, { quality: 80, speed: 10 });
const variants = [
{ label: 'avif q80/s6 (baseline)', fn: () => encodeAvif(imageData, { quality: 80, speed: 6 }) },
{ label: 'avif q80/s8', fn: () => encodeAvif(imageData, { quality: 80, speed: 8 }) },
{ label: 'avif q80/s10', fn: () => encodeAvif(imageData, { quality: 80, speed: 10 }) },
{ label: 'avif q70/s8', fn: () => encodeAvif(imageData, { quality: 70, speed: 8 }) },
{ label: 'avif q70/s10', fn: () => encodeAvif(imageData, { quality: 70, speed: 10 }) },
{ label: 'jpeg q80 (ref)', fn: () => encodeJpeg(imageData, { quality: 80 }) },
{ label: 'webp q80 (ref)', fn: () => encodeWebp(imageData, { quality: 80 }) },
];
const rows = [];
for (const variant of variants) {
process.stdout.write(`… ${variant.label}\n`);
rows.push(await timeEncode(variant.label, variant.fn));
}
const baseline = rows.find((row) => row.label.includes('baseline'));
console.log('\nresults (median of 3 runs):');
console.log(
'label'.padEnd(28),
'encodeMs'.padStart(10),
'bytes'.padStart(10),
'vsBaseMs'.padStart(10),
'vsBaseSz'.padStart(10),
);
for (const row of rows) {
const vsMs = baseline ? `${(row.encodeMs / baseline.encodeMs).toFixed(2)}x` : '-';
const vsSz = baseline ? `${(row.outputBytes / baseline.outputBytes).toFixed(2)}x` : '-';
console.log(
row.label.padEnd(28),
String(row.encodeMs).padStart(10),
String(row.outputBytes).padStart(10),
vsMs.padStart(10),
vsSz.padStart(10),
);
}
console.log(JSON.stringify({
fixturePath,
decodeMs,
size: `${imageData.width}×${imageData.height}`,
rows,
}, null, 2));