sharp by wasm
This commit is contained in:
@@ -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}`,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user