import zlib from 'node:zlib'; import { optimizeImage } from 'wasm-image-optimization'; import { renderLabel } from 'react-canvas-label-editor'; const THUMB_W = 150; const THUMB_H = 80; const CRC_TABLE = (() => { const table = new Uint32Array(256); for (let n = 0; n < 256; n++) { let c = n; for (let k = 0; k < 8; k++) { c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1; } table[n] = c; } return table; })(); function crc32(buf) { let c = 0xffffffff; for (let i = 0; i < buf.length; i++) { c = CRC_TABLE[(c ^ buf[i]) & 0xff] ^ (c >>> 8); } return (c ^ 0xffffffff) >>> 0; } function pngChunk(type, data) { const b = Buffer.alloc(12 + data.length); b.writeUInt32BE(data.length, 0); b.write(type, 4, 4, 'ascii'); data.copy(b, 8); b.writeUInt32BE(crc32(b.subarray(4, 8 + data.length)), 8 + data.length); return b; } function encodeRgbaToPng(w, h, rgba) { const raw = Buffer.alloc((w * 4 + 1) * h); for (let y = 0; y < h; y++) { raw[y * (w * 4 + 1)] = 0; rgba.copy(raw, y * (w * 4 + 1) + 1, y * w * 4, (y + 1) * w * 4); } const idat = zlib.deflateSync(raw, { level: 9 }); const ihdr = Buffer.alloc(13); ihdr.writeUInt32BE(w, 0); ihdr.writeUInt32BE(h, 4); ihdr[8] = 8; ihdr[9] = 6; // RGBA return Buffer.concat([ Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]), pngChunk('IHDR', ihdr), pngChunk('IDAT', idat), pngChunk('IEND', Buffer.alloc(0)), ]); } /** * 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 scaled = await optimizeImage({ image: nativePng, width: THUMB_W, height: THUMB_H, fit: 'contain', format: 'raw', }); const w = THUMB_W; const h = THUMB_H; const sw = scaled.width; const sh = scaled.height; const offsetX = Math.floor((w - sw) / 2); const offsetY = Math.floor((h - sh) / 2); const data = Buffer.alloc(w * h * 4); for (let y = 0; y < sh; y++) { const dy = y + offsetY; if (dy < 0 || dy >= h) continue; for (let x = 0; x < sw; x++) { const dx = x + offsetX; if (dx < 0 || dx >= w) continue; const srcIdx = (y * sw + x) * 4; const dstIdx = (dy * w + dx) * 4; data[dstIdx] = scaled.data[srcIdx]; data[dstIdx + 1] = scaled.data[srcIdx + 1]; data[dstIdx + 2] = scaled.data[srcIdx + 2]; data[dstIdx + 3] = scaled.data[srcIdx + 3]; } } // 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 PNG via wasm-image-optimization. const rawPng = encodeRgbaToPng(w, h, rgba); const thumb = await optimizeImage({ image: rawPng, format: 'png', }); return Buffer.from(thumb.data); }