This commit is contained in:
seb
2026-10-03 23:13:36 +02:00
parent 1786879c4e
commit 9a7f345d8b
39 changed files with 4611 additions and 50 deletions

View File

@@ -4,6 +4,7 @@ const FETCH_TIMEOUT_MS = 15000;
const MAX_BYTES = 50 * 1024 * 1024; // 50MB
const MAX_EDGE = 1000;
const TRIM_THRESHOLD = 10;
const RESTORE_MARGIN = 5; // px restored per edge after trimming (up to what was trimmed)
const AVIF_QUALITY = 80;
const AVIF_EFFORT = 4;
@@ -79,7 +80,10 @@ export async function crawlImage(url, { signal } = {}) {
}
/**
* Trims transparent and white borders from an image buffer, resizes to max 1000px, and converts to AVIF.
* Trims transparent and white borders from an image buffer, restores up to
* RESTORE_MARGIN px of the original image on each edge (never more than was
* actually trimmed, never enlarging beyond the original), resizes to max 1000px,
* and converts to AVIF.
*
* @param {Buffer} inputBuffer - Raw image buffer
* @returns {Promise<{ buffer: Buffer, width: number, height: number, size: number }>}
@@ -88,56 +92,62 @@ export async function cropAndConvertToAvif(inputBuffer) {
// 1. Initial metadata and orientation correction
const baseSharp = sharp(inputBuffer, { failOn: 'none' }).rotate();
const initialMeta = await baseSharp.metadata();
const hasAlpha = Boolean(initialMeta.hasAlpha);
// Use PNG intermediate for trimming passes to preserve alpha
let currentBuffer = await baseSharp.png().toBuffer();
let currentMeta = initialMeta;
const originalBuffer = await baseSharp.png().toBuffer();
const origMeta = await sharp(originalBuffer).metadata();
// Cumulative crop box in original (oriented) pixel coordinates
let box = { left: 0, top: 0, width: origMeta.width, height: origMeta.height };
let currentBuffer = originalBuffer;
const trimPass = async (background) => {
try {
const trimmed = await sharp(currentBuffer)
.trim({ background, threshold: TRIM_THRESHOLD })
.png()
.toBuffer({ resolveWithObject: true });
if (trimmed.info.width > 0 && trimmed.info.height > 0) {
currentBuffer = trimmed.data;
box = {
left: box.left - (trimmed.info.trimOffsetLeft ?? 0),
top: box.top - (trimmed.info.trimOffsetTop ?? 0),
width: trimmed.info.width,
height: trimmed.info.height,
};
}
} catch {
// Keep currentBuffer
}
};
// 2. Step A: Trim transparent borders if alpha is present
if (currentMeta.hasAlpha) {
try {
const trimmed = await sharp(currentBuffer)
.trim({ background: { r: 0, g: 0, b: 0, alpha: 0 }, threshold: TRIM_THRESHOLD })
.png()
.toBuffer({ resolveWithObject: true });
if (trimmed.info.width > 0 && trimmed.info.height > 0) {
currentBuffer = trimmed.data;
currentMeta = trimmed.info;
}
} catch {
// Keep currentBuffer
}
}
if (hasAlpha) await trimPass({ r: 0, g: 0, b: 0, alpha: 0 });
// 3. Step B: Trim white borders
try {
const trimmed = await sharp(currentBuffer)
.trim({ background: '#ffffff', threshold: TRIM_THRESHOLD })
.png()
.toBuffer({ resolveWithObject: true });
if (trimmed.info.width > 0 && trimmed.info.height > 0) {
currentBuffer = trimmed.data;
currentMeta = trimmed.info;
}
} catch {
// Keep currentBuffer
}
await trimPass('#ffffff');
// 4. Step C: Trim transparent borders again (handles white border containing transparent padding)
if (currentMeta.hasAlpha) {
if (hasAlpha) await trimPass({ r: 0, g: 0, b: 0, alpha: 0 });
// 4b. Restore up to RESTORE_MARGIN px per edge from the original, clamped to original bounds
const left = Math.max(0, box.left - RESTORE_MARGIN);
const top = Math.max(0, box.top - RESTORE_MARGIN);
const right = Math.min(origMeta.width, box.left + box.width + RESTORE_MARGIN);
const bottom = Math.min(origMeta.height, box.top + box.height + RESTORE_MARGIN);
if (left !== box.left || top !== box.top || right - left !== box.width || bottom - top !== box.height) {
try {
const trimmed = await sharp(currentBuffer)
.trim({ background: { r: 0, g: 0, b: 0, alpha: 0 }, threshold: TRIM_THRESHOLD })
const restored = await sharp(originalBuffer)
.extract({ left, top, width: right - left, height: bottom - top })
.png()
.toBuffer({ resolveWithObject: true });
if (trimmed.info.width > 0 && trimmed.info.height > 0) {
currentBuffer = trimmed.data;
currentMeta = trimmed.info;
}
currentBuffer = restored.data;
} catch {
// Keep currentBuffer
}
}
const currentMeta = await sharp(currentBuffer).metadata();
// 5. Resize to max 1000px without enlargement
let finalPipeline = sharp(currentBuffer);