u
This commit is contained in:
80
src/services/zplService.js
Normal file
80
src/services/zplService.js
Normal file
@@ -0,0 +1,80 @@
|
||||
import { createCanvas, loadImage } from 'canvas';
|
||||
|
||||
/**
|
||||
* Converts a PNG image buffer into Zebra ZPL II Graphic Field (^GFA) format.
|
||||
* Uses Zebra's alternative compression scheme (ACS run-length encoding on hex)
|
||||
* to minimize payload size over the network and serial/USB ports.
|
||||
*
|
||||
* @param {Buffer} pngBuffer - Rendered 1-bit or RGBA PNG buffer.
|
||||
* @param {number} [quantity=1] - Number of copies to print (^PQ).
|
||||
* @returns {Promise<string>} Full ZPL string ready to send to a Zebra printer.
|
||||
*/
|
||||
export async function pngToZpl(pngBuffer, quantity = 1) {
|
||||
const img = await loadImage(pngBuffer);
|
||||
const canvas = createCanvas(img.width, img.height);
|
||||
const ctx = canvas.getContext('2d');
|
||||
ctx.drawImage(img, 0, 0);
|
||||
const imgData = ctx.getImageData(0, 0, img.width, img.height);
|
||||
const { data, width: w, height: h } = imgData;
|
||||
|
||||
const bytesPerRow = Math.ceil(w / 8);
|
||||
const totalBytes = bytesPerRow * h;
|
||||
const buffer = new Uint8Array(totalBytes);
|
||||
|
||||
for (let y = 0; y < h; y++) {
|
||||
for (let x = 0; x < w; x++) {
|
||||
const idx = (y * w + x) * 4;
|
||||
const r = data[idx];
|
||||
const g = data[idx + 1];
|
||||
const b = data[idx + 2];
|
||||
const a = data[idx + 3];
|
||||
// Ink threshold: non-transparent and dark pixels are black (1)
|
||||
const isBlack = a > 128 && (r * 0.299 + g * 0.587 + b * 0.114) < 128;
|
||||
if (isBlack) {
|
||||
const byteIdx = y * bytesPerRow + Math.floor(x / 8);
|
||||
const bitIdx = 7 - (x % 8);
|
||||
buffer[byteIdx] |= 1 << bitIdx;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const hexMap = Array.from({ length: 256 }, (_, i) => i.toString(16).padStart(2, '0').toUpperCase());
|
||||
let hex = '';
|
||||
for (let i = 0; i < totalBytes; i++) {
|
||||
hex += hexMap[buffer[i]];
|
||||
}
|
||||
|
||||
// Zebra ACS run-length encoding:
|
||||
// Repeated characters are replaced by letters:
|
||||
// 'G'..'Y' = 1..19 repeats
|
||||
// 'g'..'y' = 20..380 repeats (step 20)
|
||||
// 'z' = 400 repeats
|
||||
let acs = '';
|
||||
const re = /([0-9A-F])\1{2,}/g;
|
||||
let match = re.exec(hex);
|
||||
let offset = 0;
|
||||
while (match) {
|
||||
acs += hex.substring(offset, match.index);
|
||||
let l = match[0].length;
|
||||
while (l >= 400) {
|
||||
acs += 'z';
|
||||
l -= 400;
|
||||
}
|
||||
if (l >= 20) {
|
||||
acs += '_ghijklmnopqrstuvwxy'[Math.floor(l / 20)];
|
||||
l = l % 20;
|
||||
}
|
||||
if (l) {
|
||||
acs += '_GHIJKLMNOPQRSTUVWXY'[l];
|
||||
}
|
||||
acs += match[1];
|
||||
offset = re.lastIndex;
|
||||
match = re.exec(hex);
|
||||
}
|
||||
acs += hex.substring(offset);
|
||||
|
||||
const qty = Number(quantity);
|
||||
const pq = Number.isInteger(qty) && qty > 1 ? `^PQ${qty}\n` : '';
|
||||
return `^XA^PW${w}^LL${h}^LS0^FT0,0^GFA,${totalBytes},${totalBytes},${bytesPerRow},${acs}\n${pq}^XZ`;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user