sharp by wasm
This commit is contained in:
@@ -1,9 +1,59 @@
|
||||
import sharp from 'sharp';
|
||||
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.
|
||||
*
|
||||
@@ -32,17 +82,35 @@ export async function renderThumb({ 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 scaled = await optimizeImage({
|
||||
image: nativePng,
|
||||
width: THUMB_W,
|
||||
height: THUMB_H,
|
||||
fit: 'contain',
|
||||
format: 'raw',
|
||||
});
|
||||
|
||||
const { width: w, height: h } = info;
|
||||
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).
|
||||
@@ -68,10 +136,12 @@ export async function renderThumb({ width, height, objects }) {
|
||||
}
|
||||
}
|
||||
|
||||
// 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();
|
||||
// 4. Encode as PNG via wasm-image-optimization.
|
||||
const rawPng = encodeRgbaToPng(w, h, rgba);
|
||||
const thumb = await optimizeImage({
|
||||
image: rawPng,
|
||||
format: 'png',
|
||||
});
|
||||
|
||||
return thumb;
|
||||
return Buffer.from(thumb.data);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user