Files
dashback/src/services/labelThumb.js
2026-10-28 01:46:51 +01:00

148 lines
4.6 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<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 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);
}