I have a Ricoh SP 210SU: a small USB laser printer with a flatbed scanner on top. Ricoh makes drivers for Windows and Linux. For macOS, nothing. Plug it into a Mac and it shows up, macOS even offers a “Generic PCL” driver, and every job fails, because the printer does not speak PCL.
So I made it work: printing from any app, scanning in Image Capture and Preview, and printing from my iPhone. The driver’s output is byte for byte the same as Ricoh’s own Linux driver. It is open source: ricoh-sp210-macos.
Step 1: make it print at all
Ricoh’s Linux driver is a CUPS filter called rastertolilo: a 32-bit x86 Linux program that turns a page into whatever the printer understands. A Mac can’t run that directly. A Docker container can.
The first version was a bridge: a small Mac filter that posts the page to Ricoh’s filter running in a Linux container, and sends the result to the printer. It printed. It also needed Docker running, which no normal person wants for a printer. But it gave me something more useful than a working printer: a reference implementation I could feed any page and compare against.
Step 2: read the format
I opened rastertolilo in Ghidra. The binary was unstripped, so function names survived, which made it much easier. A print job turned out to be a simple stack of blocks, all big-endian:
GDIJ job header 0x78 bytes
GJET job extension 0xa8 bytes
per page:
GDIP page header 0x40 bytes
GDIB band 0x20-byte header + JBIG payload (one per 256 rows)
JIDG end of job
Each page is cut into bands of 256 rows. Each band is dithered to 1 bit per pixel and compressed with JBIG1, an old fax-era compression standard. I wrote the whole format up in FORMAT.md.
Then I wrote a native filter in C that produces the same stream, using jbigkit for the JBIG1 part.
Byte-identical, not “looks the same”
“The page looks right” is a weak test. I wanted proof, so tools/verify.sh renders test pages, runs Ricoh’s filter in the container and my filter on the same input, and requires the outputs to be identical, byte for byte. Getting there meant finding three quirks:
- jbigkit trims zero bytes that Ricoh’s encoder keeps. The JBIG arithmetic coder writes some 0x00 bytes before its final flush; jbigkit drops them, Ricoh’s encoder doesn’t. I added a small patch (
JBG_KEEP_TRAILING_ZEROS) to keep them. - Floating-point rounding. Ricoh converts each grey level with
trunc((g / 255.0) * 255.0)in old x87 arithmetic, which comes out one lower for 144 of the 256 levels. Matching that bit of history was necessary for identical output. - The dither pattern’s phase in the last band. Each band’s pattern starts at
band * rows_in_this_band, so the shorter last band of a page is offset.
With those, all test cases match. Then I swapped Ricoh’s dither table for my own clustered-dot screen, so the shipped driver contains no Ricoh code at all. The printer doesn’t care which pattern you use; the byte-for-byte check still runs with Ricoh’s table, built from your own copy of their filter.
Step 3: the scanner
The scanner was harder: Ricoh’s Linux package doesn’t cover it, and the open-source SANE backend for older Ricoh models gets no reply from this one. The only driver that speaks its protocol is the Windows one, so I opened RtScan64.dll in Ghidra.
The device has two USB interfaces: 0 is the printer, 1 is the scanner. The scanner takes commands as 4-byte numbers (status, wake, set parameters, start, finish, unlock) and a 32-byte parameter block with mode, resolution and area. After start, raw image rows come back over USB. The full protocol is in SCANNER.md.
Two things I only learned by testing on the real device:
- Skip the “driver close” and “unlock” commands and the printer’s display stays stuck on “Scanning (USB)...” after the scan.
- The tone curve the Windows driver sends seems to be ignored. White paper reads about 195 instead of 255, so the service stretches the levels itself.
To make macOS see it as a real scanner, the service speaks Apple’s AirScan (eSCL) protocol on 127.0.0.1, so it works in Image Capture, Preview and System Settings like any network scanner.
The bug that took the longest
Image Capture kept getting stuck on “Waiting for scanner” with error -9921, but only sometimes. The macOS logs showed the real error: EADDRNOTAVAIL, “Can’t assign requested address”. The service was announced over Bonjour on every network interface. When the answer came back on Wi-Fi, macOS bound the connection to the Wi-Fi interface and then tried to reach 127.0.0.1 through it, which can’t work. Announcing on the local interface only (dns-sd -lo) fixed it, and a regression test now checks it stays that way.
Step 4: make it usable by someone who isn’t me
A driver that needs a terminal is a driver for one person. So:
- An installer. A
.pkginside a disk image, with the steps drawn on the background. It isn’t signed or notarised (that needs a paid Apple developer account), so the disk image includes a link that opens System Settings exactly at the “Open Anyway” button. - No Python, no Homebrew. I rewrote the scanner service in Swift on Apple’s own frameworks (IOUSBHost for USB, ImageIO for PNG, JPEG and PDF), as one universal binary for Apple Silicon and Intel.
- Printing from iPhone and iPad. macOS shares printers, but only announces real AirPrint printers to iOS. The service now announces the shared queue as AirPrint while sharing is on. One more surprise: iPhones send the user name “mobile”, which macOS refused until the queue allowed all users.
- An uninstaller, because software that can’t be removed cleanly shouldn’t be installed.
Testing it like it matters
A printer driver reads whatever an app hands it, and a local web service is reachable by any web page in a browser. So the test suite includes malformed-input tests for the driver under AddressSanitizer, and checks that web pages can’t drive the scanner service and that it refuses ambiguous HTTP requests.
What I took from it
- Get a reference you can diff against before you write anything. The Docker bridge made “is my output correct?” a yes/no question instead of a feeling.
- Exact means exact. Two of the three quirks were rounding and phase details I would never have guessed; only the byte comparison found them.
- The last 20% is the product. Printing worked early. The installer, the Bonjour bug, iPhone printing and the tests are what turned it into something other people can use.
Have a Ricoh SP 210 series printer and a Mac? The installer is on GitHub.
Download the latest release