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:
107
scripts/bench-avif-encode.mjs
Normal file
107
scripts/bench-avif-encode.mjs
Normal 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));
|
||||
83
scripts/export-pic-by-hash.mjs
Normal file
83
scripts/export-pic-by-hash.mjs
Normal file
@@ -0,0 +1,83 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Export pic.fullbinary by hash using dashback/.env MSSQL credentials.
|
||||
* Usage: node scripts/export-pic-by-hash.mjs <fullHash> [outPath]
|
||||
*/
|
||||
import { createRequire } from 'node:module';
|
||||
import { mkdir, writeFile } from 'node:fs/promises';
|
||||
import { dirname, resolve } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
const dotenv = require('../../dashback/node_modules/dotenv');
|
||||
const sql = require('../../dashback/node_modules/mssql');
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const dashfrontRoot = resolve(__dirname, '..');
|
||||
const dashbackEnv = resolve(dashfrontRoot, '../dashback/.env');
|
||||
|
||||
dotenv.config({ path: dashbackEnv, quiet: true });
|
||||
|
||||
const FULL_HASH_RE = /^[a-fA-F0-9]{64}$/;
|
||||
const hash = process.argv[2]?.trim();
|
||||
const outPath = resolve(
|
||||
process.argv[3] ?? resolve(dashfrontRoot, 'tmp/bench-source.avif'),
|
||||
);
|
||||
|
||||
if (!hash || !FULL_HASH_RE.test(hash)) {
|
||||
console.error('Usage: node scripts/export-pic-by-hash.mjs <64-char-hex-fullHash> [outPath]');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const config = {
|
||||
server: process.env.MSSQL_SERVER,
|
||||
database: process.env.MSSQL_DATABASE,
|
||||
user: process.env.MSSQL_USER,
|
||||
password: process.env.MSSQL_PASSWORD,
|
||||
port: Number(process.env.MSSQL_PORT) || 1433,
|
||||
options: {
|
||||
encrypt: process.env.MSSQL_ENCRYPT === 'true',
|
||||
trustServerCertificate: process.env.MSSQL_TRUST_SERVER_CERTIFICATE !== 'false',
|
||||
},
|
||||
};
|
||||
|
||||
if (!config.server || !config.database || !config.user || !config.password) {
|
||||
console.error('Missing MSSQL_* in dashback/.env');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const pool = await sql.connect(config);
|
||||
try {
|
||||
const result = await pool
|
||||
.request()
|
||||
.input('hash', sql.VarChar(64), hash.toLowerCase())
|
||||
.query(`
|
||||
SELECT w, h, size, fullbinary
|
||||
FROM pic
|
||||
WHERE hash = @hash
|
||||
`);
|
||||
|
||||
const row = result.recordset[0];
|
||||
if (!row?.fullbinary) {
|
||||
console.error(`No pic found for hash ${hash}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const buffer = Buffer.isBuffer(row.fullbinary)
|
||||
? row.fullbinary
|
||||
: Buffer.from(row.fullbinary);
|
||||
|
||||
await mkdir(dirname(outPath), { recursive: true });
|
||||
await writeFile(outPath, buffer);
|
||||
|
||||
console.log(JSON.stringify({
|
||||
outPath,
|
||||
hash: hash.toLowerCase(),
|
||||
w: row.w,
|
||||
h: row.h,
|
||||
size: row.size,
|
||||
bytes: buffer.length,
|
||||
}, null, 2));
|
||||
} finally {
|
||||
await pool.close();
|
||||
}
|
||||
Reference in New Issue
Block a user