Skip to content

Instantly share code, notes, and snippets.

@skylarmt
Last active October 16, 2024 22:42
Show Gist options
  • Save skylarmt/5595c25ac7e79b46dd8ed09043c51cac to your computer and use it in GitHub Desktop.
Save skylarmt/5595c25ac7e79b46dd8ed09043c51cac to your computer and use it in GitHub Desktop.
Node.js code to take an image (via Jimp library) and convert it to label printer commands for ZPL, EPL2, or TSPL
/**
* Get the raw text commands to send to the printer for EPL, TSPL, or ZPL.
* @param {jimp image} image, 300 DPI
* @param {string} printerLang "epl2", "tspl2", or "zpl"
* @param {number} printerDpi 200 or 300, don't use 203
* @param {number} labelWidthMM
* @param {number} labelHeightMM
* @param {number} labelGapMM Gap between labels on roll
* @param {number} labelDensity Print darkness
* @param {number} labelVerticalOffset Adjust the positioning if the top or bottom of the label are cut off or go to the next label
* @param {string} mediaType Set the label gap sense type: "web", "mark", "continuous"
* @returns {array} Things to write to the printer as-is and in order to make it print correctly. See sendRawDataToPrintQueue
*/
function getRawPrinterCode(image, printerLang, printerDpi, labelWidthMM, labelHeightMM, labelGapMM, labelDensity, labelVerticalOffset, mediaType) {
image.greyscale();
if (printerDpi == 200) {
image.resize(image.bitmap.width / 1.5, image.bitmap.height / 1.5);
}
if (typeof mediaType != "string") {
mediaType = "web";
}
// Figure out how many more pixels we need per row to make the row have a whole number of bytes
let imageRowLength = image.bitmap.width;
let requiredRowLength = imageRowLength + ((Math.ceil(image.bitmap.width / 8) - (image.bitmap.width / 8)) * 8);
if (imageRowLength != requiredRowLength) {
console.log("Resizing image to be even number of bytes wide");
const Jimp = require("jimp");
var newImg = new Jimp(requiredRowLength, image.bitmap.height, "#FFFFFF");
newImg.blit(image, 0, 0, 0, 0, image.bitmap.width, image.bitmap.height);
image = newImg;
}
// convert to binary
let threshold = 127;
let columnIndex = 0;
let binaryImg = new Uint8Array((image.bitmap.width * image.bitmap.height) / 8);
let byteIndex = 0;
let byte = [];
for (let i = 0; i < image.bitmap.data.length - 4; i += 4) {
let r = image.bitmap.data[i];
let g = image.bitmap.data[i + 1];
let b = image.bitmap.data[i + 2];
let a = image.bitmap.data[i + 3];
if (r + g + b > threshold * 3 || a < threshold) {
if (printerLang == "zpl") {
byte.push(0);
} else {
byte.push(1);
}
} else {
if (printerLang == "zpl") {
byte.push(1);
} else {
byte.push(0);
}
}
columnIndex++;
while (columnIndex == imageRowLength && byte.length < 8) {
byte.push(printerLang == "zpl" ? 1 : 0);
}
if (byte.length == 8) {
binaryImg[byteIndex] = parseInt(byte.join(""), 2);
byte = [];
byteIndex++;
}
}
// center image on label
let labelWidthInDots = labelWidthMM * (printerDpi == 200 ? 8 : 11.8);
let labelHeightInDots = labelHeightMM * (printerDpi == 200 ? 8 : 11.8);
let labelGapInDots = labelGapMM * (printerDpi == 200 ? 8 : 11.8);
let xOffset = Math.max(0, Math.ceil((labelWidthInDots - image.bitmap.width) / 2));
let yOffset = Math.max(0, Math.ceil(((labelHeightInDots - image.bitmap.height) / 2) + (labelVerticalOffset * (printerDpi == 200 ? 8 : 11.8))));
if (printerLang == "epl2") {
var mediaGapCommand = `Q${labelHeightInDots},${labelGapInDots}`;
switch (mediaType) {
case "mark":
mediaGapCommand = `Q${labelHeightInDots},B${labelGapInDots},0`;
break;
case "continuous":
mediaGapCommand = `Q${labelHeightInDots},0`;
break;
case "web":
case "gap":
default:
mediaGapCommand = `Q${labelHeightInDots},${labelGapInDots}`;
break;
}
return [`N\nq${labelWidthInDots}\n${mediaGapCommand}\nD${Math.min(15, labelDensity)}\nGW${xOffset},${yOffset},${Math.ceil(image.bitmap.width / 8)},${image.bitmap.height},`,
binaryImg,
"\nP1\n"
];
} else if (printerLang == "tspl2") {
var mediaGapCommand = `GAP ${labelGapMM} mm,0 mm`;
switch (mediaType) {
case "mark":
mediaGapCommand = `BLINE ${labelGapMM} mm,${labelGapMM} mm`;
break;
case "continuous":
mediaGapCommand = `GAP 0,0\nSET CUTTER 1`; // Also enable label cutter if present
break;
case "web":
case "gap":
default:
mediaGapCommand = `GAP ${labelGapMM} mm,0 mm`;
break;
}
/*
* SIZE: set label size
* CLS: clear image buffer
* LIMITFEED: max label length before stopping feed/erroring out
* SPEED: print speed, inches per second
* DENSITY: print darkness (0-15)
* BITMAP: draw image x,y,width in bytes,height in rows/dots,mode (0=overwrite),bitmap data (binary)
* PRINT: print the label (1 label set, 1 copy)
*/
return [
`SIZE ${labelWidthMM} mm,${labelHeightMM} mm\nCLS\n${mediaGapCommand}\nLIMITFEED 200 mm\nSPEED 4\nDENSITY ${Math.min(15, labelDensity)}\nBITMAP ${xOffset},${yOffset},${Math.ceil(image.bitmap.width / 8)},${image.bitmap.height},0,`,
binaryImg,
"\nPRINT 1,1\n"
];
} else if (printerLang == "zpl") {
let hexImg = Buffer.from(binaryImg).toString('hex').toUpperCase();
// Do some compression on repeating Fs and 0s, shrinks label size to about 10-30% of original byte array size so it transmits and prints faster
let hexCompressedImg = hexImg;
for (let i = 0; i < 2; i++) { // Do a couple compression passes, save another ~100 bytes or so
hexCompressedImg = hexCompressedImg.replaceAll("0000", "J0").replaceAll("FFFF", "JF").replaceAll("J0J0", "N0").replaceAll("JFJF", "NF");
hexCompressedImg = hexCompressedImg.replaceAll("N0N0", "V0").replaceAll("NFNF", "VF").replaceAll("V0V0V0V0V0", "j0").replaceAll("VFVFVFVFVF", "jF");
hexCompressedImg = hexCompressedImg.replaceAll("j0j0", "n0").replaceAll("jFjF", "nF").replaceAll("n0n0", "v0").replaceAll("nFnF", "vF");
hexCompressedImg = hexCompressedImg.replaceAll("V0V0N0", "h0").replaceAll("VFVFNF", "hF");
hexCompressedImg = hexCompressedImg.replaceAll("v0v0v0v0v0", "z0z0z0z0").replaceAll("vFvFvFvFvF", "zFzFzFzF");
hexCompressedImg = hexCompressedImg.replaceAll("V0000", "Y0").replaceAll("VFFFF", "YF").replaceAll("V000", "X0").replaceAll("VFFF", "XF").replaceAll("V00", "W0").replaceAll("VFF", "WF");
hexCompressedImg = hexCompressedImg.replaceAll("J0000", "M0").replaceAll("JFFFF", "MF").replaceAll("J000", "L0").replaceAll("JFFF", "LF").replaceAll("J00", "K0").replaceAll("JFF", "KF");
hexCompressedImg = hexCompressedImg.replaceAll("N0000", "Q0").replaceAll("NFFFF", "QF").replaceAll("N000", "P0").replaceAll("NFFF", "PF").replaceAll("N00", "O0").replaceAll("NFF", "OF");
hexCompressedImg = hexCompressedImg.replaceAll("V0V0V0V0J0", "i0N0").replaceAll("VFVFVFVFJF", "iFNF");
hexCompressedImg = hexCompressedImg.replaceAll(/(?<![g-zG-Y])000/g, "I0").replaceAll(/(?<![g-zG-Y])FFF/g, "IF");
hexCompressedImg = hexCompressedImg.replaceAll("j0V0V0", "k0R0").replaceAll("jFVFVF", "kFRF");
hexCompressedImg = hexCompressedImg.replaceAll("N0J0", "R0").replaceAll("NFJF", "RF");
hexCompressedImg = hexCompressedImg.replaceAll("N0M0", "U0").replaceAll("NFMF", "UF");
hexCompressedImg = hexCompressedImg.replaceAll("N0L0", "T0").replaceAll("NFLF", "TF");
hexCompressedImg = hexCompressedImg.replaceAll("V0V0V0", "h0N0").replaceAll("VFVFVF", "hFNF");
hexCompressedImg = hexCompressedImg.replaceAll("R00", "T0").replaceAll("RFF", "TF");
}
/*
* ^XA: Start label
* ^PO: Set print orientation (^PON=normal, ^POI=invert)
* ^MU: Set units to dots, set DPI
* ^JZ: Enable (^JZY) or disable (^JZN) auto reprint after error
* ^MN: Set media edge detection (Y/W: web sensor, M: black mark sensor, N: continuous media)
* ^MM: Set after-print label treatment (T: facilitate tearing off label, C: cut label)
* ^MF: _F_eed to next label edge when when turning on and closing printer
* ^PW: Set print (label) width
* ^PR: Set print speed, blank label feed speed, and backfeed speed (inches per second)
* ~SD: Set label darkness (range 00-30)
* ^LH: Set label home position to 0,0 (top-left corner)
* ^FO: Set image offset relative to label home
* ^GF: Send graphics data (A=ASCII hex format),total binary byte count,total graphic byte count,bytes per row,data
* ^FS: End of image data field
* ^XZ: End label (trigger label printing)
*/
var mediaEdge = "W";
var afterPrint = "T"; // Move label to tear-off position after print
switch (mediaType) {
case "mark":
mediaEdge = "M";
afterPrint = "T";
break;
case "continuous":
mediaEdge = "N";
afterPrint = "C"; // Cut label after print
break;
case "web":
case "gap":
default:
mediaEdge = "W";
afterPrint = "T";
break;
}
var zpl = `^XA^PON^MUd,${printerDpi},${printerDpi}^JZY^MN${mediaEdge}^MM${afterPrint}^MFF,F^PW${labelWidthInDots}^PR3,6,3~SD${(labelDensity + "").padStart(2, '0')}^LH0,0^FO${xOffset},${yOffset}^GFA,${Math.ceil(image.bitmap.width / 8) * image.bitmap.height},${Math.ceil(image.bitmap.width / 8) * image.bitmap.height},${Math.ceil(image.bitmap.width / 8)},${hexCompressedImg}^FS^XZ`;
return [zpl];
}
return [""];
}
/**
* Send output of getRawPrinterCode to this to print it. The printer variable is the print queue name.
* For Windows, uses RawPrint.exe from here because Windows can't be normal: https://mendelson.org/windowsrawprint.html
*/
async function sendRawDataToPrintQueue(data, printer) {
const fs = require("fs/promises");
let filename = require('path').join(require('os').tmpdir(), 'print_' + require('crypto').randomBytes(8).toString('hex') + '.raw');
let file = await fs.open(filename, 'w');
for (var i = 0; i < data.length; i++) {
if (i == 0) {
await file.writeFile(data[i]);
} else {
await file.appendFile(data[i]);
}
}
await file.close();
if (process.platform == "linux") {
require('child_process').execSync(`lp -d "${printer}" -o raw "${filename}"`);
await fs.unlink(filename);
} else if (process.platform == "win32") {
require('child_process').exec(`RawPrint.exe "${printer}" "${filename}"`, {
windowsHide: true
}, function () {
fs.unlink(filename);
});
} else {
console.error("Unsupported printer configuration: Use a supported operating system or change your printer settings.");
}
}
@skylarmt
Copy link
Author

Use this code under your pick of license: GPL, LGPL, MPL, BSD, or Apache

Shameless plug for the software it came from: PostalPoint Retail Shipping, cross-platform Linux-first desktop application for businesses that want to quickly and easily sell postal services to the general public for an extra revenue stream: gas stations, hotels, copy and print shops, etc. Surprisingly lucrative!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment