sharp by wasm

This commit is contained in:
seb
2026-10-28 01:46:51 +01:00
parent 1c5385061c
commit e7884c03ef
20 changed files with 530 additions and 364 deletions

View File

@@ -10,7 +10,7 @@ import {
restoreDefaultSkills,
restoreDefaultApplets,
} from '../../tools/vfs.js';
import sharp from 'sharp';
import { optimizeImage } from 'wasm-image-optimization';
// #region Helpers
@@ -286,74 +286,79 @@ function buildMetaOverlay({ width, height, size, mime, filename }, outWidth, out
* @returns {Promise<{ buffer: Buffer, contentType: string } | null>} null when not rasterisable
*/
async function renderThumbnail(buffer, { mime, filename }) {
const pipeline = sharp(buffer, { animated: false, failOn: 'none' });
let meta;
try {
meta = await pipeline.metadata();
meta = await optimizeImage({ image: buffer, format: 'none' });
} catch {
return null;
}
if (!meta?.width || !meta?.height) return null;
const origW = meta?.originalWidth;
const origH = meta?.originalHeight;
if (!origW || !origH) return null;
let scaled = await pipeline
.resize({
width: THUMB_BOX,
height: THUMB_BOX,
fit: 'inside',
// Downscale only. Upscaling is handled explicitly below, so a 60x40 image ends up at
// 100px (the THUMB_MIN_EDGE rule) rather than being blown up to fill the 250 box.
withoutEnlargement: true,
})
.png()
.toBuffer();
// Downscale to fit inside THUMB_BOX without enlargement, then explicitly upscale
// anything whose larger edge is still under THUMB_MIN_EDGE.
const downScale = Math.min(1, THUMB_BOX / origW, THUMB_BOX / origH);
let outWidth = Math.max(1, Math.round(origW * downScale));
let outHeight = Math.max(1, Math.round(origH * downScale));
let scaledMeta = await sharp(scaled).metadata();
// Upscale small images so they do not render as an unreadable speck.
const largerEdge = Math.max(scaledMeta.width, scaledMeta.height);
const largerEdge = Math.max(outWidth, outHeight);
if (largerEdge < THUMB_MIN_EDGE) {
const factor = THUMB_MIN_EDGE / largerEdge;
scaled = await sharp(scaled)
.resize({
width: Math.max(1, Math.round(scaledMeta.width * factor)),
height: Math.max(1, Math.round(scaledMeta.height * factor)),
fit: 'fill',
})
.png()
.toBuffer();
scaledMeta = await sharp(scaled).metadata();
outWidth = Math.max(1, Math.round(outWidth * factor));
outHeight = Math.max(1, Math.round(outHeight * factor));
}
const outWidth = scaledMeta.width;
const outHeight = scaledMeta.height;
const overlay = buildMetaOverlay(
{
width: meta.width,
height: meta.height,
size: buffer.length,
mime,
filename,
},
outWidth,
{
width: origW,
height: origH,
size: buffer.length,
mime,
filename,
},
outWidth,
outHeight
);
// `null` means the thumbnail is too small to carry a legible metadata band (e.g. an extreme
// aspect ratio collapsing to a few pixels tall). Serve the plain scaled image instead.
if (!overlay) {
return {
buffer: await sharp(scaled)[THUMB_FORMAT]({ quality: THUMB_QUALITY }).toBuffer(),
contentType: `image/${THUMB_FORMAT}`,
};
const plain = await optimizeImage({
image: buffer,
width: outWidth,
height: outHeight,
fit: 'fill',
format: THUMB_FORMAT,
quality: THUMB_QUALITY,
});
return {
buffer: Buffer.from(plain.data),
contentType: `image/${THUMB_FORMAT}`,
};
}
const composite = await sharp(scaled)
.composite([{ input: overlay, top: 0, left: 0 }])
[THUMB_FORMAT]({ quality: THUMB_QUALITY })
.toBuffer();
const scaledPng = await optimizeImage({
image: buffer,
width: outWidth,
height: outHeight,
fit: 'fill',
format: 'png',
});
const b64 = Buffer.from(scaledPng.data).toString('base64');
const overlayStr = overlay.toString('utf8');
const compositeSvg = overlayStr.replace(
/(<svg[^>]*>)/,
`$1<image href="data:image/png;base64,${b64}" x="0" y="0" width="${outWidth}" height="${outHeight}"/>`
);
const composite = await optimizeImage({
image: compositeSvg,
format: THUMB_FORMAT,
quality: THUMB_QUALITY,
});
return {
buffer: composite,
buffer: Buffer.from(composite.data),
contentType: `image/${THUMB_FORMAT}`,
};
}