import sharp from 'sharp'; import { renderLabel } from 'react-canvas-label-editor'; const THUMB_W = 150; const THUMB_H = 80; /** * Renders a 150×80 thumbnail for a label template. * * Workflow: * 1. Render the label at its native pixel resolution via renderLabel(). * 2. Resize to fit within 150×80 preserving aspect ratio (fit: 'contain'). * Letterbox areas remain transparent. * 3. Process raw RGBA pixels: * - Transparent letterbox padding (alpha < 128) → fully transparent (0,0,0,0). * - Label pixels (alpha >= 128) → fully opaque (alpha=255) with 4-bit greyscale * (16 levels: 0, 17, 34, ..., 255) for smooth anti-aliased text and barcodes. * 4. Output as palette PNG with transparency support. * * The result is a "4-bit grey + 1-bit alpha" image: * letterbox area is transparent, and the label area is anti-aliased 4-bit greyscale. * * @param {object} opts * @param {number} opts.width - Label width in pixels. * @param {number} opts.height - Label height in pixels. * @param {Array} opts.objects - Label editor objects array. * @returns {Promise} PNG buffer. */ export async function renderThumb({ width, height, objects }) { // 1. Render at native resolution → RGBA PNG buffer const nativePng = await renderLabel({ width, height, objects }); // 2. Resize to fit within 150×80, preserving aspect ratio. // Transparent background for letterbox padding. const { data, info } = await sharp(nativePng) .ensureAlpha() .resize(THUMB_W, THUMB_H, { fit: 'contain', background: { r: 0, g: 0, b: 0, alpha: 0 }, kernel: sharp.kernel.lanczos3, }) .raw() .toBuffer({ resolveWithObject: true }); const { width: w, height: h } = info; // 3. Per-pixel: transparent letterbox remains transparent (alpha=0), // label area is opaque (alpha=255) with 4-bit greyscale (16 levels). const rgba = Buffer.alloc(w * h * 4); for (let i = 0; i < w * h; i++) { const base = i * 4; const a = data[base + 3]; if (a < 128) { // Transparent letterbox area — leave as (0,0,0,0) } else { const factor = a === 255 ? 1 : 255 / a; const r = Math.min(255, Math.round(data[base] * factor)); const g = Math.min(255, Math.round(data[base + 1] * factor)); const b = Math.min(255, Math.round(data[base + 2] * factor)); const grey = 0.299 * r + 0.587 * g + 0.114 * b; // Quantize to 4-bit grey (16 levels: 0, 17, 34, ..., 255) const qGrey = Math.round(grey / 17) * 17; rgba[base] = qGrey; rgba[base + 1] = qGrey; rgba[base + 2] = qGrey; rgba[base + 3] = 255; } } // 4. Encode as palette PNG with transparency support (16 greys + 1 transparent entry). const thumb = await sharp(rgba, { raw: { width: w, height: h, channels: 4 } }) .png({ palette: true, colours: 17, compressionLevel: 9 }) .toBuffer(); return thumb; }