'use strict'; /** * Worker-thread entry point for the sharp crop/trim/resize/AVIF pipeline. * * This pipeline runs up to six sequential sharp passes (PNG intermediate, trim, * extract, resize, AVIF encode) whose JS glue cost lands on the thread that owns * the pipeline. Running it here keeps that cost off the main event loop, which * otherwise stalls HTTP handlers and every SSE client (heartbeats included). * * Protocol: the parent sends `{ id, buffer }` (an ArrayBuffer, transferred) and * the worker replies `{ id, ok: true, buffer, width, height, size }` (buffer * transferred back) or `{ id, ok: false, error, errorName }`. */ import { parentPort } from 'worker_threads'; import sharp from 'sharp'; 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; /** * Copies a Buffer into a standalone, transferable ArrayBuffer. * * `Buffer.from(buffer)` / `Uint8Array.prototype.subarray` can alias the shared * 8 KB Buffer pool, and transferring pooled memory would corrupt every other * Buffer in the process. A fresh `Uint8Array` is always backed by its own * ArrayBuffer, so it is always safe to hand to `postMessage(..., [transfer])`. * * @param {Buffer} buffer * @returns {ArrayBuffer} */ function toTransferable(buffer) { return Uint8Array.from(buffer).buffer; } /** * 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 }>} */ 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 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 (hasAlpha) await trimPass({ r: 0, g: 0, b: 0, alpha: 0 }); // 3. Step B: Trim white borders await trimPass('#ffffff'); // 4. Step C: Trim transparent borders again (handles white border containing transparent padding) 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 restored = await sharp(originalBuffer) .extract({ left, top, width: right - left, height: bottom - top }) .png() .toBuffer({ resolveWithObject: true }); currentBuffer = restored.data; } catch { // Keep currentBuffer } } const currentMeta = await sharp(currentBuffer).metadata(); // 5. Resize to max 1000px without enlargement let finalPipeline = sharp(currentBuffer); if (currentMeta.width > MAX_EDGE || currentMeta.height > MAX_EDGE) { finalPipeline = finalPipeline.resize(MAX_EDGE, MAX_EDGE, { fit: 'inside', withoutEnlargement: true, }); } // 6. Convert to AVIF const avifResult = await finalPipeline .avif({ quality: AVIF_QUALITY, effort: AVIF_EFFORT }) .toBuffer({ resolveWithObject: true }); return { buffer: avifResult.data, width: avifResult.info.width, height: avifResult.info.height, size: avifResult.info.size, }; } // Boot handshake. The parent awaits this before queuing the first job, so a worker // whose imports (sharp/libvips) fail surfaces as a start-up error rather than as an // inexplicable per-job failure. parentPort.postMessage({ ready: true }); parentPort.on('message', async (msg) => { const { id, buffer } = msg ?? {}; try { const result = await cropAndConvertToAvif(Buffer.from(buffer)); const out = toTransferable(result.buffer); parentPort.postMessage( { id, ok: true, buffer: out, width: result.width, height: result.height, size: result.size, }, [out] ); } catch (err) { parentPort.postMessage({ id, ok: false, error: err?.message || String(err), errorName: err?.name || 'Error', stack: err?.stack, }); } });