Turning an e-reader into a network printer

title
Turning an e-reader into a network printer
type
summary
summary
Nishant Joshi makes an Xteink X3 show up in the macOS print dialog by speaking IPP, and fits a 300 dpi page into 400 KB of RAM
tags
hardware, embedded, e-paper, networking
created
2026-09-13
updated
2026-09-13

Nishant Joshi bought an Xteink X3 e-reader because the fine print said it was fully programmable, and started extending the open CrossPoint firmware (see freeink) with a new boot animation, dice rolled by shaking the reader, and a LinkedIn QR code for networking events. Getting files onto it meant joining the reader's hotspot and using a small upload page in a browser. His way out: if it looks like paper, it should behave like paper, so it should be possible to print on it. The result is a printer called penguin that shows up in the macOS print dialog. The post's own title is "How to build a f**king printer".

Pretending to be a printer

What a Mac expects at the other end is the Internet Printing Protocol. IPP runs over HTTP: Get-Printer-Attributes asks what the printer supports and Print-Job submits work. Joshi's printer advertises monochrome at 300 dpi, one copy, one-sided, A5 and Letter paper, media type stationery, and an output bin named face-up. It accepts Apple raster and PWG raster, so the Mac renders the document to pixels before sending it and the reader never parses a PDF.

Discovery goes over Bonjour as an _ipp._tcp service carrying the accepted formats and the job URL. Driverless printing on macOS also needed the _universal subtype, which the Arduino mDNS wrapper doesn't expose, so that part calls ESP-IDF's mDNS API directly.

Where does a page go?

A Letter page at 300 dpi is 2,550 by 3,300 pixels, about 8.4 MB at one byte per pixel. The X3's ESP32-C3 has 400 KB of RAM, 16 KB of it reserved for cache. With Wi-Fi running and a page image allocated, 6.8 KB of heap was left. Memory-mapping a file on the SD card, his first idea, isn't available: the chip's memory mapping covers flash, not SD files.

The fix was to use the display as the storage. The display driver already holds a framebuffer for the screen, and the firmware had been building a second full image only to copy it across. The incoming raster now goes through decode, scale-to-fit and dither to black and white one row at a time, and each finished row is written straight into the display's framebuffer, reusing the working memory for the next row. His decoder already worked row by row, so the change was making the scaler hand over finished rows as well.

Image-buffer memory went from about 113 KB to about 62 KB, which left room for the network stack's socket buffers. He first let the page appear in bands, like paper feeding out, but each partial e-paper refresh took about half a second, so the page now appears all at once.

The first print

A manga page opened in Preview, penguin in the printer list, and about a second later (from memory, he says) the page on the reader. It took one evening. The printer server runs on the reader itself, either joined to a Wi-Fi network or on its own hotspot, literate-penguin. Finished pages are saved as BMP files through the firmware's existing screenshot writer, and they can be browsed on the reader afterwards. The output tray is a folder.

The code is in his CrossPoint fork. Beyond the build, the post is a compact example of two general tricks: implementing the protocol a mainstream OS already speaks, so no driver or app is needed on the sending side, and streaming through a buffer you already own instead of allocating a second one. It also takes the paper-computer idea from the other end: instead of scanning paper into the computer, the computer prints onto something that behaves like paper.