Metadata-Version: 2.4
Name: a7a
Version: 4.0.0
Summary: a7a: The worst lossless image format ever made.
Author-email: Eyad Mohammed <eyadj@proton.me>
Classifier: Programming Language :: Python :: 3
Classifier: Operating System :: OS Independent
Classifier: Topic :: Multimedia :: Graphics :: Graphics Conversion
Requires-Python: >=3.7
Description-Content-Type: text/markdown
Requires-Dist: numpy
Requires-Dist: Pillow
Provides-Extra: zstd
Requires-Dist: zstandard; extra == "zstd"

# a7a — The Worst Image Format Ever Made

> A lossless image codec that reinvents PNG, badly, on purpose, in Python, with zero encoders in the wild, zero hardware support, zero browser support, and a magic number that was broken for who knows how long because someone typed `b'a7a'` instead of `b'a7a\x00'`, hence the name a7a.

If you're looking for an image format to actually use, this isn't it. If you're looking for an image format that exists so you can say you built an image format from the byte layout up, filters, adaptive row selection, palette quantization, YUV chroma subsampling, multiple pluggable entropy coders, a CLI, a GUI, the works, congratulations, you're home.

---

## Table of Contents

- [Why](#why)
- [What a7a Actually Is](#what-a7a-actually-is)
- [File Layout](#file-layout)
- [How Encoding Works](#how-encoding-works)
  - [1. Color Mode Selection](#1-color-mode-selection)
  - [2. Row Filtering (the PNG trick)](#2-row-filtering-the-png-trick)
  - [3. Compression Backends](#3-compression-backends)
  - [4. Fast Path vs. Full Search](#4-fast-path-vs-full-search)
- [How Decoding Works](#how-decoding-works)
- [Installation](#installation)
- [Usage](#usage)
  - [CLI](#cli)
  - [Python API](#python-api)
  - [GUI Tools](#gui-tools)
- [Color Modes](#color-modes)
- [Compression Types](#compression-types)
- [Options Reference](#options-reference)
- [Performance Characteristics](#performance-characteristics)
- [Known Limitations (a.k.a. Why This Is Terrible)](#known-limitations-aka-why-this-is-terrible)
- [The Magic Number Incident](#the-magic-number-incident)
- [FAQ](#faq)
- [Project Structure](#project-structure)
- [License](#license)

---

## Why

No good reason. This project exists to answer questions nobody asked:

- What if PNG's per-row adaptive filtering, but you also let it pick between four different general-purpose compressors per image?
- What if you bolted on a hand-rolled YUV 4:2:0 path with no motion compensation, no DCT, none of the things that make chroma subsampling actually worth it in a still-image codec?
- What if the palette mode just... quietly refuses to help unless the image happens to have 256 colors or fewer and full opacity?
- What if someone made a file format named a7a and then made it stand by its name?

The honest answer to "why does this format exist" is: it doesn't need to. WebP exists. AVIF exists. Even PNG, which this format is 90% a worse reimplementation of, exists. a7a is what happens when you build the thing anyway.

## What a7a Actually Is

At its core, a7a is a **PNG-style lossless raster format**:

1. Take an image.
2. Optionally convert its color representation (RGB → YUV420, RGB → indexed palette).
3. Run PNG-style adaptive per-row prediction filtering over the raw pixel bytes to make them more compressible.
4. Feed the filtered bytes into a general-purpose compressor (zlib, LZMA, or Zstandard).
5. Wrap the result in a small custom binary header.

That's it. There is no entropy coding tailored to images (no arithmetic coding, no wavelet transforms, no block-based DCT like JPEG/AVIF/HEIC use). It's PNG's filtering idea glued to whichever off-the-shelf compressor happens to win a brute-force size contest, with a palette mode and a half-hearted YUV420 mode thrown in for variety.

It also makes images about 20x bigger by size, have fun with that info.
## File Layout

The header is packed with `struct` format `'>4sBBBBII'`  big-endian, 14 bytes total:

| Bytes | Field | Type | Description |
|---|---|---|---|
| 0–3 | `magic` | `4s` | Must be exactly `b'a7a\x00'` |
| 4 | `version` | `B` (uint8) | Currently `4` |
| 5 | `color_mode` | `B` (uint8) | See [Color Modes](#color-modes) |
| 6 | `comp_type` | `B` (uint8) | See [Compression Types](#compression-types) |
| 7 | *(reserved)* | `B` (uint8) | Always `0`, unused |
| 8–11 | `width` | `I` (uint32) | Image width in pixels |
| 12–15 | `height` | `I` (uint32) | Image height in pixels |

Wait, that's 16 bytes, not 14, `'>4sBBBBII'` is `4 + 1+1+1+1 + 4+4` = **16 bytes**. `HEADER_SIZE` is computed via `struct.calcsize`, so it's always correct regardless of what this README claims; if you're auditing byte counts, trust the code, not prose.

**If `color_mode == PALETTE`:** immediately after the header comes:

| Bytes | Field | Description |
|---|---|---|
| 2 | `palette_count` | uint16, big-endian, number of palette entries |
| `palette_count × 3` | `palette` | RGB triplets, one per palette entry |

**After that** (or immediately after the header for non-palette modes): the compressed pixel data, running to end-of-file. There is no length-prefix on the compressed blob and no checksum/CRC anywhere in the format. If the file is truncated or corrupted, decoding either throws deep inside `zlib`/`lzma` or silently produces garbage. There is no way to verify integrity short of decoding successfully. This is, charitably, "no worse than a raw memory dump" and, less charitably, "one bit-flip away from an unhandled exception."

## How Encoding Works

### 1. Color Mode Selection

Encoding starts by asking two questions:

- **Does the image have real transparency?** (checked by inspecting the alpha channel for any non-255 value — not just "is it RGBA mode," an actually-opaque RGBA image is treated as opaque)
- **Is `try_all_methods` set?**

If the image has real alpha, `ColorMode.RGBA` is the *only* candidate ever considered, for every compression search path, always. Alpha carries real information, and every other color mode in this codec either drops it entirely or (in palette's case) refuses to encode transparent images at all. This is one of the few genuinely sound engineering decisions in the entire file, and it's enforced with a comment that's more confident than anything else in the codebase.

If there's no alpha, the candidate set can include:

- `RGB` — always included, the baseline
- `YUV420` — included if `chroma_subsampling=True`
- `PALETTE` — included if `speed <= 6` or `force_palette=True`, and only actually used if `_try_palette` succeeds (≤256 unique colors after median-cut quantization)

### 2. Row Filtering (the PNG trick)

Every color mode's raw pixel bytes get run through `_adaptive_filter`, which is a direct lift of PNG's per-scanline prediction filters:

- `NONE` — pass the row through unchanged
- `SUB` — subtract the pixel to the left
- `UP` — subtract the pixel directly above
- `AVERAGE` — subtract the average of left and above
- `PAETH` — subtract whichever of left/above/upper-left the Paeth predictor thinks is the best guess

For **every row**, the encoder tries all five filters, scores each by summed absolute residual (a cheap stand-in for "how compressible is this," explicitly *not* a real entropy estimate), and keeps the lowest-scoring one. The chosen filter ID is written as a single byte before each row's filtered data. This is exactly PNG's own approach, just written in Python/NumPy instead of C, decades later, without the ecosystem, without the tooling, and without the part where every browser on Earth can decode it.

`AVERAGE` and `PAETH` reconstruction on decode is a genuine serial dependency — each pixel's unfiltering depends on the previous reconstructed pixel — so it can't be vectorized away. The decoder drops to a plain Python loop for those two filters specifically, which the source comments clock at roughly 3–5x faster than doing the same loop with NumPy scalar indexing, and still describe honestly as "a mitigation, not a fix." For large images with rows that prefer AVERAGE or PAETH, this loop is the single biggest bottleneck in the whole codec.

### 3. Compression Backends

Filtered bytes go into one of four possible compressors:

| Backend | Notes |
|---|---|
| `ZLIB` | Stock `zlib.compress`. The universal fallback — cannot fail, always available. |
| `ZLIB_FILTERED` | `zlib` with `Z_FILTERED` strategy, tuned for exactly this kind of filtered-residual data. Default. |
| `LZMA` | Better ratio, much slower. Only tried at `speed <= 5`. |
| `ZSTD` | Requires the optional `zstandard` package. Only tried at `speed <= 7`, and only if the package is actually importable. |

### 4. Fast Path vs. Full Search

- **Fast path** (`try_all_methods=False`, the default via `--fast` on the CLI's encode-fast flag): pick one color mode (RGBA or RGB based on alpha), filter it, compress it once with whatever `options.compression` says. No search, no comparison, no waste. This is what batch jobs and interactive tools should use, according to the source's own comments.
- **Full search** (`try_all_methods=True`): build every viable color-mode candidate, filter each one, then for each candidate try every compressor allowed at the configured `speed`, and keep whichever `(color_mode, compressor)` combination produced the smallest final byte count. This is the "make it as small as possible, don't care how long it takes" mode.

There is a single shared `_best_compression` helper used by both paths specifically so the "which compressor wins" logic can't drift into two different implementations that quietly disagree — a bug class the source comments call out by name as the kind of thing that causes "why does zstd never get picked" mysteries months later.

## How Decoding Works

1. Read and validate the 16-byte header. Reject anything whose magic doesn't match `a7a\x00` exactly, and reject any version outside `[MIN_SUPPORTED_VERSION, MAX_SUPPORTED_VERSION]` (currently both pinned to `4`) rather than guessing at a layout it's never seen.
2. If `color_mode == PALETTE`, read the palette table.
3. Decompress the remaining bytes using whatever `comp_type` the header says.
4. Un-filter every row/plane using the inverse of whichever filter byte precedes it.
5. Reassemble into a `PIL.Image` according to color mode: direct RGB/RGBA, palette lookup, or YUV420→RGB upsampling and color conversion.

There is no fallback path for a version mismatch. It fails loudly and immediately, which — credit where due — is the correct behavior for a format with no ecosystem to be backward-compatible for.

## Installation

```bash
pip install numpy pillow
# optional, for the ZSTD compression backend:
pip install zstandard
```

No `setup.py`, no `pyproject.toml`, no PyPI package. This is three scripts and a codec module. You clone it, you `cd` into it, you run it.

## Usage

### CLI

```bash
# Encode with defaults (full search, zlib_filtered, level 9)
python a7a.py encode input.png output.a7a

# Encode fast (single color mode, single compressor pass)
python a7a.py encode input.png output.a7a --fast

# Encode with a specific compressor and level
python a7a.py encode input.png output.a7a --compression lzma --level 9

# Force palette mode (only works if the image actually qualifies)
python a7a.py encode input.png output.a7a --palette

# Enable YUV 4:2:0 chroma subsampling as a candidate
python a7a.py encode input.png output.a7a --yuv420

# Control search depth (0 = try everything and go slow, 9 = fastest, larger)
python a7a.py encode input.png output.a7a --speed 0

# Decode back to any format Pillow understands
python a7a.py decode output.a7a restored.png

# Inspect a file without fully decoding it
python a7a.py info output.a7a
```

`info` output looks like:

```
Width: 1920, Height: 1080
File size: 2841044 bytes
Color mode: RGB
Compression: ZLIB_FILTERED
Version: 4 
```

### Python API

```python
from a7a import encode_a7a, decode_a7a, get_a7a_info, a7aOptions, CompressionType

# Simple encode, write straight to disk
encode_a7a("photo.png", "photo.a7a")

# Full control
opts = a7aOptions(
    compression=CompressionType.LZMA,
    compression_level=9,
    speed=0,               # try everything
    try_all_methods=True,
    chroma_subsampling=True,
)
encode_a7a("photo.png", "photo.a7a", opts)

# Encode to bytes instead of a file
data = encode_a7a("photo.png", options=opts)

# Decode
img = decode_a7a("photo.a7a")
img.save("restored.png")

# Metadata without a full decode
info = get_a7a_info("photo.a7a")
print(info["color_mode"], info["compression"], info["width"], info["height"])
```

`encode_a7a` also accepts a `PIL.Image.Image` directly instead of a path, if you're generating images in-memory and don't want a round trip through disk.

### GUI Tools

Two throwaway Tkinter apps ship alongside the codec:

- **`a7a_convert.py`** — file picker → pick any image → encodes it to `.a7a` next to the original → shows original size, new size, and compression ratio, with a popup on success or failure.
- **`a7a_preview.py`** — file picker → pick a `.a7a` file → decodes it, thumbnails it into a 380×350 canvas, and shows resolution + file size above the preview.

Both are ~60-line scripts with no error recovery beyond a `messagebox.showerror` on exception. They exist so you don't have to touch the CLI to sanity-check a file. They are not, and were never meant to be, real applications.

## Color Modes

| Value | Name | Channels | Notes |
|---|---|---|---|
| 0 | `RGBA` | 4 | Only used when the image has genuine transparency. Only lossless option once alpha matters, so it's never allowed to lose a size comparison to something lossy. |
| 1 | `RGB` | 3 | The baseline. Always a candidate for opaque images. |
| 2 | `PALETTE` | 1 (indexed) | Median-cut quantized to ≤256 colors. Refuses to activate on images with more colors than that, or any transparency at all. |
| 4 | `YUV444` | 3 | Defined in the enum, has full conversion functions (`_rgb_to_yuv444` / `_yuv444_to_rgb`), but **is never selected by the encoder** — nothing in `encode_a7a` ever appends a `YUV444` candidate. It exists in the format spec and the decoder can handle it if it ever shows up, but nothing currently writes it. Make of that what you will. |
| 5 | `YUV420` | 3 (subsampled) | Chroma subsampled 2:1 in both dimensions. Opt-in via `chroma_subsampling=True`. No motion estimation, no DCT — this is *not* what makes JPEG/video codecs efficient, it's just fewer chroma samples to filter and compress. |

Note the enum values themselves aren't contiguous (0, 1, 2, then a jump to 4, 5) — `3` is simply unused. Nothing reads meaning into that; it's just how the `IntEnum` was declared.

## Compression Types

| Value | Name | Backend | Availability |
|---|---|---|---|
| 0 | `ZLIB` | `zlib.compress` | Always. The unconditional last-resort fallback if every other candidate compressor throws. |
| 1 | `ZLIB_FILTERED` | `zlib` with `Z_FILTERED` strategy | Always. Default. |
| 2 | `LZMA` | `lzma.compress` | Always (stdlib), but only auto-tried in a full search when `speed <= 5` — it's slow. |
| 3 | `ZSTD` | `zstandard` package | Only if installed; only auto-tried when `speed <= 7`. Raises `RuntimeError` if you explicitly request it and the package isn't present. |

## Options Reference

`a7aOptions` (all fields optional, shown with defaults):

| Field | Default | Meaning |
|---|---|---|
| `compression` | `ZLIB_FILTERED` | Compressor used on the fast path; ignored (superseded by search) on the full-search path except as a starting bias. |
| `compression_level` | `9` | 0–9 for zlib/lzma, 1–22 for zstd. Validated in `__post_init__`; out-of-range raises `ValueError` immediately. |
| `speed` | `5` | Controls which compressors get tried during a full search (see table above) — lower is slower/smaller, higher is faster/larger. Only matters when `try_all_methods=True`. |
| `force_palette` | `False` | Forces palette mode to be attempted regardless of `speed`. Still silently skipped if the image doesn't qualify (>256 colors, or has transparency). |
| `try_all_methods` | `False` | `True` = full search across color modes and compressors. `False` = fast path, one shot. |
| `chroma_subsampling` | `False` | Adds `YUV420` as a search candidate when doing a full search on an opaque image. Does nothing on the fast path or on images with transparency. |

## Performance Characteristics

- **Fast path**: one filter pass (still per-row-adaptive, so still O(width × height × 5) filter evaluations even in "fast" mode — the *search* is skipped, not the filtering), one compression pass. Reasonable for interactive use.
- **Full search**: N color-mode candidates × M compressors, each involving a full filter pass and a full compression pass. For an opaque image with palette and YUV420 both eligible, that's up to 3 color modes × up to 3 compressors = 9 full encode passes for one image. At `speed=0` on a large photo, expect this to be slow — slower than opening the file in literally any other tool that exists.
- **PAETH/AVERAGE decode**: pure-Python per-pixel loop, unavoidable due to the serial dependency chain. This is the dominant cost on decode for any image whose rows favor those two filters, and it scales linearly with width per row, with real per-element Python overhead. Large PAETH-heavy images will decode noticeably slower than they encode.
- **No SIMD, no multiprocessing, no GPU path.** Everything is single-threaded NumPy plus, where NumPy can't help, single-threaded pure Python.

## Known Limitations (a.k.a. Why This Is Terrible)

- **No ecosystem.** Every browser, OS thumbnailer, image viewer, and CDN on Earth has never heard of `.a7a` and never will.
- **No integrity checking.** No CRC, no checksum, anywhere in the format. Corruption is discovered by crashing, if you're lucky, or by silently decoding into wrong pixels, if you're not.
- **No length-prefixed compressed section.** The compressed payload just runs to EOF. Concatenating anything after a valid `.a7a` file, or truncating one, breaks decoding in ways the format has no way to detect gracefully.
- **`YUV444` is dead code from the encoder's perspective.** Fully implemented conversion math, a reserved enum value, decoder support — and nothing ever produces one. It's here in case you want to construct one by hand for some reason.
- **Palette mode is opportunistic, not guaranteed.** Asking for `--palette` doesn't mean you get palette mode; it means the encoder will *try*, and silently fall back to RGB if the image doesn't quantize cleanly to ≤256 colors or has any transparency.
- **No animation, no multi-frame support, no metadata (EXIF, ICC profiles, color space tags — none of it).** This format encodes exactly one static raster image and nothing else about it.
- **Compression is entirely general-purpose.** There's no image-aware entropy coding beyond PNG-style row prediction. On photographic content this will reliably lose to WebP, AVIF, or even well-tuned PNG, because none of the actual hard problems modern image codecs solve (transform coding, rate-distortion optimization, real entropy coding) are attempted here.
- **Full search mode is combinatorially wasteful** by design, trading CPU time for marginal size gains, with no early-exit heuristic beyond "have I already tried a candidate."
- **The GUI tools have essentially no input validation** beyond Tkinter's file-type filters and a generic try/except around the core call.

## The Magic Number Incident

For posterity, because the fix is preserved directly in the source as a comment: the file's magic number is `b'a7a\x00'`, four bytes, padded with a null. It used to be `b'a7a'` — three bytes. The header format string is `'>4sBBBBII'`, which packs a 4-byte field no matter what you hand it: too-short input gets silently null-padded by `struct.pack`, and `struct.unpack` hands that padded value straight back out on decode.

Which means: every file ever written by the 3-byte-magic version of this codec encoded a header whose on-disk magic bytes were, in fact, already `b'a7a\x00'` — but the code was checking newly-decoded files for equality against the *3-byte* literal `b'a7a'`, which no longer matched anything `struct.unpack` ever returns for a 4-byte field. Every single file this codec had ever written became permanently, unconditionally undecodable by its own decoder. Not corrupted — perfectly intact, byte-for-byte identical to what a correct implementation would have produced — just rejected at the front door by a string comparison that could never succeed.

The fix was one character: add `\x00`. The lesson, per the comment left directly above `MAGIC` in the source, was to actually think about what `struct.pack`'s field-width semantics do to values that don't match the declared width, instead of assuming Python would either error out or preserve the literal you handed it.

## FAQ

**Should I use this for anything real?**
No.

**Is it actually the worst image format ever made?**
Objectively, no — there is no size or speed benchmark in existence, no test suite, no comparison numbers anywhere in this repository backing that claim up. Subjectively, the title is right there in the project name, and nothing in this README is here to argue with it. Also WEBP exists, meaning there are worse image formats.

**Does it beat PNG on file size?**
Sometimes, on some images, with `try_all_methods=True` and a slow `speed` setting, LZMA or Zstandard can out-compress zlib's deflate on the same filtered data that PNG itself would produce. That is a statement about general-purpose compressor quality, not about anything this codec invented.

**Why is there a working GUI for a format nothing else can open?**
So you don't have to type `python a7a.py decode file.a7a out.png` just to look at a picture you made five minutes ago.

**Is the format spec documented anywhere else?**
No. This README and the `a7a.py` source are the entire specification. There is no RFC, no formal spec document, no reference outside this repository.

**Can I add support for animation / metadata / a real entropy coder?**
Nothing stops you. The header has one reserved byte and nothing else earmarked for extension. You'd be bumping `VERSION`, extending `HEADER_FORMAT`, and accepting that every existing `.a7a` file (all zero of them, in practice) becomes unreadable by the new build unless you also maintain the old parsing path.

## Project Structure

```
.
├── a7a.py            # Core codec: header, filters, color conversions,
│                      # compression backends, encode/decode, CLI
├── a7a_convert.py    # Tkinter GUI: pick an image, encode to .a7a
├── a7a_preview.py    # Tkinter GUI: pick a .a7a file, preview it
└── README.md         # This document
```

## License

Not specified. Assume all rights reserved until I decide otherwise — which, given the state of everything else in this project, may never happen.

## Contributing
Contributions are welcome. Open a PR; if it doesn't break the header format, it'll probably get merged.

Thank you! <3
