Dan Pastusek's Blog | Home

Writing a Printer Driver in JavaScript

Published 2022-02-01 Updated: 2023-11-15

I originally published this article on the KubeSail blog.


What the hell! Why?

I agree, writing a printer driver in JavaScript sounds ridiculous. And I'm really not the type to do low-level coding exercises for fun. I usually prefer designing and building full products using high level languages. But I bought this Polono label printer off Amazon for ~$140, which was a steal compared to the Zebra models that start around $500. Zebra is the industry standard, and their printers come with their own programming language which is supported by the big shipping carrier's software like UPS, FedEx, etc. This language, called ZPL (Zebra Programming Language), is simply lines of ASCII text sent over a Serial connection that give instructions on how to print the label. Braindead simple. Based on positive reviews from other Amazon customers using it with their shipping software, I had high hopes that this cheap label printer would also support ZPL.

ZPL Example

Trying the driver out of the box

When the printer arrived, I plugged it into a spare Raspberry Pi, and... success! Linux recognized it as a printer and it shows up as /dev/usb/lp0. It doesn't show up as a printer in the linux GUI, but that's ok — printing in Linux is always a bit tricky. I eventually remember that Linux uses a printing system called CUPS, and figured this just needs to be configured first. I don't see it in the list of available printers, so I try installing the Linux driver for the PL60. Simple enough, it's a tarball with an install script. I get a warning of

strip: Unable to recognize the format of the input file '/usr/lib/cups/filter/raster-tspl'`

but then a few lines down see

Install Complete

Hooray! Now I see the PL60 listed in the CUPS admin interface. This seems promising!


Well, after clicking through the CUPS interface to add the printer, and trying to print a test page, we see a failed job. The printer now has a Status of Idle - "File "/usr/lib/cups/filter/raster-tspl" not available: No such file or directory". Bummer, perhaps that warning from the install script wasn't something we could ignore.

Hello TSPL

I dug into all the files in the driver's tarball and see that there's a filter and ppd directory. After some Googling, I learn that PPD files describe the capabilities of each printer, such as paper size, color space, etc. Filters are the executables that convert one document format to another, for example, turning a PNG image into an ASCII ZPL file that can be sent to the printer. But in this tarball's filter directory there are only x64 and x86 filters.


I really want to turn this Raspberry Pi into a permanent shipping station, so at this point, I'm pretty upset that these "Linux drivers" are just pre-compiled x64 and x86 binaries, so I start the Amazon return process and head to dinner. After all, I've got to ship our PiBoxes out soon, so I'll just bite the bullet and spend the money on a Zebra. But I've got 2 days until the Zebra arrives, so after dinner my mind is still wondering if there's a way to compile these drivers for arm64. Googling terms like raster tspl pl60 return very little of value. I even dug through the PPD file above, looking for clues.

*Manufacturer: "POLONO"
*ModelName: "POLONO PL60"
*ShortNickName: "HPRT N41"

Clues indeed! As it turns out, this printer is really a re-branded clone of the HPRT N41, or even it's sibling HPRT SL42! If you've read this far, you've probably realized that these printers do not support ZPL. The support page for the HPRT printer has a lot more technical details than Polono's site, and clues me into realizing that these printers instead support TSPL, which is a printer programming language much like ZPL. Only this language is from a printer manufacturer called TSC, which is based in Taiwan

I finally stumble upon some PHP code for talking to TSPL printers which gives me hope! Lots of stars, and some interesting notes about writing directly to the printer, e.g. php hello-world.php > /dev/usb/lp0. This did get my printer to spit out paper, but the pages were blank 😢. But finally, I came across the Linux SDK TSPL for SL42 and even found a TSPL/TSPL2 Programming Manual!

Deciding to write my own driver

Even though I found the SDK, the libs folder inside were still pre-compiled for x86 or x64. This SDK seemed so promising, but why not give out the full source code?! Ok, it was time to get my hands dirty, and start reading through the 204-page TSPL Programming Manual. As I started reading I had a big realization that the only filter included in the driver above was raster-tspl. The "raster" part of that name tells me that they took a shortcut and only wrote drivers for printing raster images, such as bitmaps, PNGs, etc.

For shipping our PiBoxes, we decided on using EasyPost, which has a fantastic API for comparing shipping rates across carriers. When you purchase a shipping label through them, they give you the output in either a PDF, PNG, or ZPL format. ZPL would be ideal, since it's just ASCII text which can easily be modified, or even stored in a database along side each order. But that would require some sort of ZPL to TSPL converter script, which doesn't seem to exist, according to Google. At least not yet — leave a comment or send me a message if you want to help write one!

I had to make a choice of whether I wanted to take the same "raster only" shortcut or if I should try and write this converter script. This would require handling every ZPL function, and implementing at least enough TSPL functions (like rendering QR codes, barcodes, various ASCII fonts, etc) in order to print a scannable shipping label. I've got a whole business to run, and we are not in the shipping label industry — obviously I chose the shortcut.

Raster to TSPL in Javascript

Ok, now that I had a clear objective, I wanted to see if I could get a reference TSPL output file from a known PNG input file. Thankfully, Macs also use CUPS (Apple maintains it after all), and I was able to install the Polono printer driver on my Mac (rather humorously, my Mac is an ARM M1 chip, so it's emulating the x64 driver in Rosetta). CUPS has a helpful command line utility, cupsfilter that lets you run the filter script, and save the output to a file rather than straight to the printer.

cupsfilter test.png -p /Library/Printers/PPDs/Contents/Resources/PL60.tspl.ppd -m printer/foo -e > out.tspl

Awesome! I was able to take my test.png as an input, and get back an out.tspl file. I even copied this file over to the Raspberry Pi and tried writing out out via cat out.tspl > /dev/usb/lp0 and lo and behold... it printed the image! 🎉🎉🎉

Taking a look at the .tspl file in a text editor, we see a file that is mostly text lines, with one gigantic binary line. I even tried modifying some things, like adding a SPEED 2 line, and was able to get it to print a bit faster or slower, depending on the number. This is amazing... we are now communicating directly with the printer!

SIZE 99.8 mm, 149.9 mm
BITMAP 0,0,100,1198,1,���...���

My only question now: how does the filter turn the PNG image into that BITMAP line full of binary data? It really doesn't look like a standard bitmap format. I looked in the TSPL programming manual for the "BITMAP" function, and it actually didn't seem that complicated! It's a thermal printer, so each pixel is either black or white, with no grayscale or colors to worry about. And each pixel is represented by a single bit.


From our BITMAP 0,0,100,1198,1,���...��� line above, we see that the binary data starts at X coordinate 0, Y coordinate 0, contains 100 bytes of data across, and 1198 dots in length. This lines up with what we know, since there are 8 bits in a byte for a total of 800 dots in width, and the specs state 203 dpi (8 dots/mm), so ~4 inches in width. Same with the 1196 dots in length — this comes out to an image that's 4"x6", or a standard shipping label size. Excellent.

Converting the Image to a BITMAP Command

I couldn't find any scripts that turn raw images into TSPL bitmaps, so I set out to write my own. It can't be that hard, right? Well, aside from a few off-by-one errors in my logic during testing, it really wasn't! Here's my full code. It just has one dependency, JIMP (JavaScript Image Manipulation Program), which is a package that can load images natively in JavaScript, and has a method to read the pixel value at any specific X,Y coordinate. So all that's required is to loop through each pixel in a row (X direction), and determine if that pixel should be added to a byte as a 1 (white pixel) or a 0 (black pixel), making sure to add the current byte to the buffer every 8 pixels (or bits). Then run that in a loop for all the rows of pixels in the image (Y direction)

At the end you have a Buffer of bytes containing pixel data that you can add to the end of your BITMAP line. To verify my results, I re-created the image that's the same as the example from the manual. I start with the same 16x16 image, but paint it on a canvas that's 800x1198, the same as our output label so we can get a 1-to-1 pixel mapping when we print.

Hex editor

Now, whether we run this image through our script using node print.js test.png or the manufacturer's driver using the cupsfilter command above, we end up with the same binary file to send to the printer. We can inspect it using a hex editor:

Hex editor

And sure enough, we get 16 bits (2 bytes) of zeros, (00 00 in hex), followed by 98 bytes of ones (FF FF FF FF FF ...). Each area selected in blue is the start of a new row of pixels (100 bytes). Let's convert these first few rows of hex back to binary, and see if it looks like the image we expect.

Hex editor

Hot damn, that looks like our test image! At the end of our JS script, we can even send the resulting file directly to the printer

fs.writeFileSync("/dev/usb/lp0", label);

and out comes a label. Now I just have a wrapper script that asks EasyPost for the PNG labels, saves the image to disk, and runs my printer script. The whole script takes ~2 seconds on a Raspberry Pi, which isn't as fast as compiled code, but plenty fast for our needs.

What's next?

With a bit more work, I believe someone could turn this into a viable startup. After trying Stamps.com, Dymo, PirateShip, and a handful of others, I am convinced there are no good shipping hardware & software combos out there. All of it feels overly proprietary, and none of it is hackable or interfaces with the database I already have. I would love to see someone selling a kit with:

Here's my shipping station that checks all of those boxes. A lot of it is internal scripts that are hacked together, so I haven't taken the time to polish it and make it generic enough to release. If you want to see more details in a future blog post, please let me know!

Printer Station

Shameless Plug

If you want your very own label powered by this shipping station (well, along with a really cool product!), you can now pre-order a PiBox. It arrives loaded with everything you need to self-host apps at home. New orders are estimated to ship in July.