Skip to content

Printer driver

Fons van der Plas edited this page Feb 16, 2020 · 3 revisions

The Onion Omega 2+ has a tiny amount of storage: 32MB! How can we fit our client firmware on there?

onion omega 2+

tiny computer has tiny brain

From the start, I had decided that I want to use a high-level programming language where HTTP requests are easy - Python 3 with the requests package (C# is not directly supported on the Omega). I also wanted to be able to remotely update the firmware. Since Python runs directly on source files (without compilation), this is done using git!

Installing Linux, Python 3 (with pip), requests and git left the disk 80% occupied. And we haven't installed the printer driver yet! ๐Ÿ™Š The default printer driver for Linux, CUPS, is 10MB in size, and too slow - this seemed like a good excuse to write my own printer driver. (This might not have been the most sensible solution, but it was fun!)

Step 1: How does printing work with CUPS?

When calling the print command in Linux (lp), CUPS figures out which chain of programs (called filters) to run, depending on the source file and the destination device. For example, when you run lp cat.png, and a Hoin HOP H58 printer is installed as default, CUPS will say (after some digital bureaucracy):

[Job 3865] 2 filters for job:
[Job 3865] imagetoraster (image/png to application/vnd.cups-raster, cost 100)
[Job 3865] rastertozj (application/vnd.cups-raster to printer/MYRECEIPTPRINTER, cost 0)

i.e. it will convert the PNG file to an intermediate raster file (specific to CUPS), and then to a format specific to our printer, zj, which is streamed over USB to the device.

Both of these filters are C programs, and their source code is public: imagetoraster.c, rastertozj.c.

Unfortunately, they both use the CUPS headers, so we can't just use the two filters without installing CUPS. After some experiments with stripping down the C program, I decided to change the approach completely: we recreate the PNG -> zj conversion in C#, and run it on the printi server. The resulting binary zj file (i.e. the set of printer commands) just needs to be fetched by the Omega, and sent to the printer over USB.

Step 2: Recreating the driver

We want to create a single program that does the same as the combination of imagetoraster.c and rastertozj.c. It's not just a matter of translating C to Python: both are hard to read, complicated programs with lots of references to external code. ๐Ÿ˜” Instead, we focus on the output of these filters, and write our own program that produces the same output. This is called reverse engineering.

I first wrote this in Python (for quick experimenting), and later in C# (to run on the server).

imagetoraster

This filter is essentially a Bayes ditherer, so we can recreate the functionality using the hitherdither package by Henrik Blidh. Easy!

See imageToRaster

rastertozj

The output of this filter is a list of commands (i.e. bytes) that are sent to the printer. By looking at the script, and by looking at the hexdump of its output, we find that a zj always begins and ends with 0x1b 0x40. This is how the receipt printer knows that the rest should be treated as an image, instead of printing the data as text.

hexdump of output

We see that the image data is being split into slices of 24 lines of pixels, which are sent one by one. Each slice starts with the slice start command (0x1d 0x76 0x30 0x00), followed by the number of bytes in that slice, and the number of lines in the slice (both as 16-bit unsigned ints). Then follows the slice data, where each pixel is one bit (0 = white, 1 = black).

Once this secret code is figured out, it is easy to write your own driver!

See printRaster

I still have the very first printi mini printis! Here you see some of the bugs in the first Python experiments:

glitchy printi glitchy printi

In these receipts, you can see:

  • writing plain ASCII to the printer (these become letters!)
  • the printi logo printed using CUPS
  • printing over the same roll again to save paper ๐Ÿ“œ
  • forgetting the file start command (image data is printed as text)
  • accidentally flipping every bit (black and white are swapped, making my hair white)
  • accidentally reversing every byte (all 8-pixel columns are reversed, making my hair spiky)

Step 3: Rewriting in C#

Once the Python script is done, it is easy to write a C# version. I decided to also write the dithering code myself, which turned out to be very fun! Besides the standard Bayes ditherer, I also wrote a Burkes ditherer, which made the pictures very pretty! ๐Ÿ–ผ

See Rasterizer

More image processing is underway!

printi & printi mini

printi mini (58mm, with Burkes dithering) and printi (80mm, with Bayes dithering)