Skip to main content

a7a: The worst lossless image format ever made.

Project description

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

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
6 comp_type B (uint8) See 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

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

# 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

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

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

a7a-4.0.0.tar.gz (28.3 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

a7a-4.0.0-py3-none-any.whl (20.1 kB view details)

Uploaded Python 3

File details

Details for the file a7a-4.0.0.tar.gz.

File metadata

  • Download URL: a7a-4.0.0.tar.gz
  • Upload date:
  • Size: 28.3 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.13.5

File hashes

Hashes for a7a-4.0.0.tar.gz
Algorithm Hash digest
SHA256 e80289cd06f7d5c2b88a478eb74b6ec52e2c54179286e9b97cf44a3b6ec9c9a7
MD5 ccf38d917a29f5af2d91ccf5e6042f37
BLAKE2b-256 376a460268664cb3e1ad8e3173bced32ea228327f1849bfac7a01f6c4cdda160

See more details on using hashes here.

File details

Details for the file a7a-4.0.0-py3-none-any.whl.

File metadata

  • Download URL: a7a-4.0.0-py3-none-any.whl
  • Upload date:
  • Size: 20.1 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.13.5

File hashes

Hashes for a7a-4.0.0-py3-none-any.whl
Algorithm Hash digest
SHA256 c944d1e24991992c0f512ba1f7256612bc6fa9be9010595dc90b48aa38b10228
MD5 f0f1df306135fc57ba5f4ef125442132
BLAKE2b-256 6958694768ae001ab8c57ec08752819d68416c24838e6d008872867f5f18e609

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page