Yes We Scan: the backstory
It starts with a printervention
I started out wanting to make an old Canon photo printer usable by my parents and in-laws. So I made printervention.app, which I wrote about before.
While working on that app, I remembered I also had an old Canon USB scanner tucked away on a shelf. The last time I’d used that, I think I’d resorted to setting up a Windows 95 VM. But the same technology that powers printervention.app would clearly work for scanners too.
How does it work?
The core of both apps is the amazing v86, which emulates an x86 CPU — and the whole machine around it — in a browser. It compiles machine code to WebAssembly modules at runtime, which is rather clever and makes the whole thing tolerably quick.
So I set up an emulated v86 machine to run SANE (Scanner Access Now Easy) on Alpine Linux, discreetly, in your browser.
The interface between your browser and SANE is provided by a small custom C program that Claude helped write. This connects to the scanner, provides a JSON dump of its settings we can use to build a settings UI, and streams scan data over the hypervisor console (hvc0) in v86.
Back in the browser, that scan data is received and either written to a <canvas> context (when previewing), or sent to a Web Worker running wasm-mozjpeg or fflate, compressing it to JPEG or PNG on-the-fly.
Bridging v86 and WebUSB
I’d already got the USB side of things working for printervention.app:
- USB/IP runs on the Linux machine. It packages up outgoing USB data into TCP packets, and unwraps USB data out of incoming TCP packets. As far as SANE can tell, it’s just connected to an ordinary USB scanner over ordinary USB.
- tcpip.js runs on the JavaScript side, mainly by compiling lwIP to WebAssembly. The v86 machine has an emulated network card, but what emerges from that into the JavaScript environment is raw Ethernet frames. Since the browser exposes no network stack to turn this L2 Ethernet traffic back into L4 TCP/IP traffic, this is the job tcpip.js takes on.
- Lastly, the reconstituted USB/IP packets are bridged to your computer’s USB via the browser’s WebUSB API, thanks to a JavaScript module for which I must again credit Claude.
What’s next
The app has only been tested on my CanoScan LiDE 100, but I have some hope that it will work for a range of other models. If you try it, do let me know. I must repeat my apology that I haven’t so far open-sourced any part of the code that I don’t have to.
George MacKerron
May 2026