Last active
October 16, 2024 22:42
-
-
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
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* 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 [""]; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* 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."); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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!