https://github.com/fstark/png2mo5
Convert any PNG to a Thomson MO5 image — CIELAB dithering, ZX0 compression, 6809 viewer, cassette tape output. C++23.
https://github.com/fstark/png2mo5
6809 6809-assembly color-quantization color-quantization-algorithms dithering image-processing mo5 retro-computing thomson thomson-mo5 vintage-computer vintage-computers
Last synced: 2 days ago
JSON representation
Convert any PNG to a Thomson MO5 image — CIELAB dithering, ZX0 compression, 6809 viewer, cassette tape output. C++23.
- Host: GitHub
- URL: https://github.com/fstark/png2mo5
- Owner: fstark
- Created: 2026-05-14T20:54:16.000Z (about 1 month ago)
- Default Branch: main
- Last Pushed: 2026-05-16T22:38:23.000Z (30 days ago)
- Last Synced: 2026-05-16T23:42:10.901Z (30 days ago)
- Topics: 6809, 6809-assembly, color-quantization, color-quantization-algorithms, dithering, image-processing, mo5, retro-computing, thomson, thomson-mo5, vintage-computer, vintage-computers
- Language: C++
- Homepage:
- Size: 1.29 MB
- Stars: 0
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- Agents: AGENTS.md
Awesome Lists containing this project
README
# png2mo5
https://github.com/user-attachments/assets/ce33dc24-45bc-4362-9a23-4df7d69dfb26
Convert any PNG into something a 1984 Thomson MO5 can actually display.
More details at the [companion blog post](https://stark.fr/blog/png2mo5/)
The MO5 has a fixed 16-color palette and a brutal constraint: each group of 8 horizontal pixels can only use 2 colors. That's it. No tricks, no workarounds, just 2 colors per block.
And the palette looks like this:

The converter optionally works in CIELAB perceptual color space — it brute-forces all 136 possible color pairs for every 8-pixel block, simulating Floyd-Steinberg error diffusion inside each candidate to find the pair that minimizes outgoing perceptual error. Then it commits the winner and diffuses the quantization error (damped ~90%) across the image. The result is surprisingly good for 16 KB of video RAM.
C++23. Single-file tools. No dependencies beyond stb headers and a ZX0 compressor.
## Examples
| Original | MO5 (320×200, 16 colors, 2 per block) |
|----------|----------------------------------------|
|  |  |
|  |  |
Compressed sizes (ZX0, from 16 KB raw):
| Image | `.mo5z` size |
|-------|-------------|
| lena | 10.3 KB |
| birds | 12.6 KB |
The `.mo5z` format splits color and pixel data into three separate ZX0-compressed streams — colors first, so a viewer on real hardware can progressively reveal the image during decompression.
## What's in the repo
| Tool | What it does |
|------|-------------|
| **png2mo5** | Image -> MO5. Handles resize, palette quantization, dithering, outputs raw bins + preview PNG |
| **mo5z** | Compresses MO5 pixel+color banks into a single `.mo5z` file (three ZX0 streams) |
| **mo5z2png** | Decompresses a `.mo5z` back to a viewable PNG |
| **k7tool** | Creates and inspects `.k7` cassette tape images for the MO5 |
| **mo5zviewer** | 6809 assembly viewer (309 bytes) — boots from tape, decompresses, displays. That's it |
The full pipeline: `png2mo5` -> `mo5z` -> `k7tool` bundles the viewer + compressed image into a `.k7` cassette file you can load in an emulator.
## Doing your own images
```bash
# 0. (optional) Preview the conversion before committing
# -> lena_preview.png (preview is the default, no flags needed)
png2mo5/png2mo5 lena.png
# 1. Convert PNG to MO5 raw banks (8000 bytes each)
# -> lena_pixels.bin, lena_colors.bin
png2mo5/png2mo5 lena.png --bin
# 2. Compress into a .mo5z (three ZX0 streams, ~10 KB)
# -> lena.mo5z
mo5z/mo5z lena_pixels.bin lena_colors.bin -o lena
# 3. Glue the viewer + compressed image into a single binary
# -> lena.bin (stanalone binary, loads at $2800, exec at $2800)
cat mo5zviewer/mo5zviewer.bin lena.mo5z > lena.bin
# 4. (optional) Wrap it in a .k7 cassette file for use in an emulator
k7tool/k7tool -o lena.k7 lena.bin:0x2800:0x2800
```
---
All the code in this repo — including the 6809 assembly — was written by Claude Opus 4.6 under my supervision.