78 lines
2.9 KiB
JavaScript
78 lines
2.9 KiB
JavaScript
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<Buffer>} 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;
|
||
}
|