How’s that for a title? While that’s gibberish for most, if you’re the unlucky soul who’s been tasked with dumping an image to your Zebra thermal printer, it could be just what you’re looking for.
Clik here to view.

The ubiquitous Zebra thermal printer.
A little background
These thermal printers are commonly used in shipping environments to print out USPS, FedEx, and UPS labels. The printers generally speak two languages natively: EPL2 and ZPLII. The EPL (Eltron Programming Language) language is older than ZPL (Zebra Programming Language) but is also a bit simpler.
Zebra bought Eltron and has kept EPL around for backward compatibility reasons; the tired LP2844 only speaks EPL, but newer printers can speak both EPL and ZPL. While ZPL has advanced drawing features and proportional fonts, I tend to favor EPL just because the command set is simpler and it ends up getting the job done.
The EPL language consists of printer commands, one per line, in an ASCII text file. Each of the commands is described in the EPL Manual on Zebra’s site. For example, here’s a quick-and-dirty document that prints out some text on a 3″ x 1″ thermal label:
N q609 Q203,26 A26,26,0,5,1,2,N,"HI, MOM!" P1,1 |
Each command generally starts with a letter and is followed by comma-separated parameters, each of which are described in the manual. There are commands for drawing text, drawing lines, and drawing barcodes: the basic kind of stuff that you need to do in a warehouse envrionment.
OK. But what about images?
These printers come with a Windows printer driver that let them work with Windows like any other GDI-based printer. You can print a Word document, the driver translates it into a bitmap, and then the driver dishes out the image to the printer.
Sometimes, though, you want to use the EPL language (after all, your printer has tons of barcode formatting built into it already, so you might as well use that instead of buying a third-party library) while also dishing out a bitmap (such as your company’s logo). You look in the handy-dandy manual and see that you need to use the GW
command to send out an image, and you start by …
Image may be NSFW.
Clik here to view.
… scratching your head. Ugh, looks like we’ll have to do some math.
But not yet
First, though, let’s pound and poke the image that we want to draw into a format that we can work with. Since we’re dealing with a thermal printer, there is no concept of grayscale here: we either burn a dot (represented by bit 0
) or don’t burn one (represented by bit 1
). (If you’ve seen my post about printing images to an ESC/POS receipt printer, you’ll note that Epson’s convention is conveniently the direct opposite of this.)
There’s a good chance that the bitmap that we’re trying to draw is not monochrome. In a monochrome image, each pixel is either 100% black (burn a dot) or 100% white (don’t burn a dot). Our image is probably grayscale or even in color, so we need to figure out how to snap each pixel to either being pure black or pure white.
The way one figures out how to do this is to search for it on the Internet and hope that some graphics nerd has done this for you. And, happily, there’s apparently this thing called luma that will serve this purpose nicely:
/// <summary> /// Gets a <see cref="BitmapData"/> instace for a given image. /// </summary> /// <param name="bytes">The image data.</param> /// <returns>The <see cref="BitmapData"/> instance.</returns> private static BitmapData GetBitmapData(byte[] bytes) { using (var ms = new MemoryStream(bytes)) using (var bitmap = (Bitmap)Bitmap.FromStream(ms)) { var threshold = 127; var index = 0; var dimensions = bitmap.Width * bitmap.Height; var dots = new BitArray(dimensions); for (var y = 0; y < bitmap.Height; y++) { for (var x = 0; x < bitmap.Width; x++) { var color = bitmap.GetPixel(x, y); var luminance = (int)((color.R * 0.3) + (color.G * 0.59) + (color.B * 0.11)); dots[index] = luminance < threshold; index++; } } return new BitmapData() { Dots = dots, Height = bitmap.Height, Width = bitmap.Width }; } } |
Given our bitmap as an array of bytes, we load it into a GDI+ bitmap using .NET’s System.Drawing
namespace. Then we apply our luminance formula to determine how “bright” each pixel is, snapping it into a binary value. Then, we return a BitmapData
struct that just contains the properties that you see here: the bitmap height, the bitmap width, and the now-binary pixels of our image strung out in one long array.
That’s nice, but you’re avoiding the math
So now we need to generate the actual GW
command. The code here is remarkably similar to that of the article I wrote about sending images to ESC/POS printers. Here we go:
/// <summary> /// Inserts a GW command image. /// </summary> /// <param name="bw">The binary writer.</param> /// <param name="top">The top location.</param> /// <param name="left">The left location.</param> /// <param name="image">The image bytes.</param> private static void InsertImage(BinaryWriter bw, int top, int left, byte[] image) { var encoding = Encoding.ASCII; var data = GetBitmapData(image); var dots = data.Dots; var bytes = (int)Math.Ceiling((double)data.Width / 8); bw.Write(encoding.GetBytes(string.Format("GW{0},{1},{2},{3},", top, left, bytes, data.Height))); var imageWidth = data.Width; var canvasWidth = bytes * 8; for (int y = 0; y < data.Height; ++y) { for (int x = 0; x < canvasWidth; ) { byte s = 0; for (int b = 0; b < 8; ++b, ++x) { bool v = false; if (x < imageWidth) { int i = (y * data.Width) + x; v = data.Dots[i]; } s |= (byte)((v ? 0 : 1) << (7 - b)); } bw.Write(s); } } bw.WriteNewLine(); } |
So what the hell is going on here?
The first thing of note is that we calculate the p3
parameter by converting our bitmap width (in pixels) into a number of bytes. Since each pixel is represented by one bit (a 1 or 0), then each byte represents 8 pixels. That means that our image’s width must be a multiple of 8. We handle this by using Math.Ceiling
in the conversion so that we’ll end up just padding with extra white space if our bitmap width is not a multiple of 8.
The second thing of note is that we calculate the p4
parameter. This is referred to in the documentation as the “print length” which is just a confusing way of asking “how many dots tall is it”. This is just the height of our bitmap.
Finally, we need to dump out our pixel data. I’m using .NET’s BinaryWriter
class, so I have to write out data to the stream in bytes. The outer loop loops through each horizontal stripe of the bitmap, starting from the top. And the next loop draws each dot in that line, starting from the left. And the innermost loop fills up a byte, since we have to “gather” 8 pixels at once to write out as a byte to the BinaryWriter
. There’s an extra if
check there to account for the case where our bitmap image width is not a multiple of 8; if so, we need to make sure to pad the extra space instead of marching off to the next line in our dots
array.
The s |= (byte)((v ? 0 : 1) << (7 - b));
line looks terrifying but is really just working to build up the byte. I discussed the mechanics of this in detail in my post about printing images to an ESC/POS receipt printer.
If you open the file in Notepad, you’ll see gibberish. That’s because the image data you just encoded isn’t going to map neatly into ASCII characters, and this is where the design of the EPL language starts to break down. It’s nice that most of it can be expressed in simple ASCII characters and can be edited in a text editor, but this isn’t one of those cases. If you open the file in a text editor and save it, you might “bake” the binary image data portion it in the wrong encoding and end up with gibberish on your printer. Be sure to send it directly to the printer as is without loading it into a string
or StringBuilder
!
Putting it all together
In my case, I was sick of a certain provider constantly breaking their EPL label with every software update and instead wanted to dump their PNG version of the label (which they seem to actually test) directly to the printer on 4″ x 6″ stock. Given the bitmap as a byte[]
array, here’s a quick-and-dirty function to dump it out:
/// <summary> /// Converts an image, represented by the given binary payload, into an EPL document. /// </summary> /// <param name="source">The image to convert.</param> /// <returns>The EPL document payload.</returns> internal static byte[] AsEplImageDocument(this byte[] source) { using (var ms = new MemoryStream()) using (var bw = new BinaryWriter(ms, Encoding.ASCII)) { // Clear out any bogus commands bw.WriteNewLine(); // Start a new document bw.Write(Encoding.ASCII.GetBytes("N")); bw.WriteNewLine(); // Label width is 4" bw.Write(Encoding.ASCII.GetBytes("q812")); bw.WriteNewLine(); // Label height is 6" ... only important in ZB mode bw.Write(Encoding.ASCII.GetBytes("Q1218,20")); bw.WriteNewLine(); // From earlier in the article InsertImage(bw, 0, 0, source); // Print one copy of the label bw.Write(Encoding.ASCII.GetBytes("P1,1\n")); bw.WriteNewLine(); bw.Flush(); return ms.ToArray(); } } |
You can dish the resulting document out directly to the printer using the RawPrinterHelper
class from MSDN.
Hope this helps someone!