Regressive JPEGs
Regressive JPEGs
Originally posted on Maurycy's blog (2026-07-17)
One of the most intriguing aspects of the JPEG format is the ability to prioritize the storage of low-frequency components. This mechanism ensures that as an image downloads, the viewer sees a blurry, low-resolution version that gradually sharpens, rather than a file that simply loads from top to bottom.
The Mechanics of Scans
This effect is achieved by partitioning compressed data into several "scans," each preceded by its own header.
Anatomy of a Scan
Consider the first scan of a typical image. The byte sequence looks like this:
FF DAThe "start of scan" marker.00 0CLength field (12 bytes, Big endian), including the header itself.03Number of channels included in this scan.01Global ID for the first channel.00Huffman table index #1 (DC: 0, AC: 0).02Global ID for the second channel.10Huffman table index #2 (DC: 1, AC: 0).03Global ID for the third channel.10Huffman table index #2 (DC: 0, AC: 0).00The starting DCT bin (DC).00The ending DCT bin (also DC).01Precision: half, with no prior data.
Following this header, we find the actual Huffman coded DCT coefficients:
f8ad 512d d3f1 cd96 bcb0 58df 53d5 5d97 ...
This specific scan captures only the lowest (DC) Fourier bin across all three color channels.
Color and Luminance
In JPEG compression, the luminance () is treated with higher priority because the human eye is more sensitive to brightness than color. Chrominance ( and ) can be significantly compressed without a perceived loss in quality.
The relationship is roughly:
Example Scan Sequence
A typical progressive file might contain ten scans to fully reconstruct the image:
| Scan # | Channels | DCT Bin Range | Precision |
|---|---|---|---|
| 0 | Y, Cb, Cr | 0 - 0 | Half (-1 bit) |
| 1 | Y | 1 - 5 | Quarter (-2 bits) |
| 2 | Cb | 1 - 63 | Half |
| 3 | Cr | 1 - 63 | Half |
| 4 | Y | 6 - 63 | Quarter |
| 5 | Y | 1 - 63 | Half |
| 6 | Y, Cr, Cb | 0 - 0 | Full |
| 7 | Cr | 1 - 63 | Full |
| 8 | Cb | 1 - 63 | Full |
| 9 | Y | 1 - 63 | Full |
Key observations:
- Scan 0 provides the initial low-res preview.
- Scans 2-5 provide low-precision data.
- Scan 4 fills a specific spectral gap left by Scan 1, allowing Scan 5 to build upon a complete quarter-precision foundation.
- Scans 6-9 add the final bit of precision for maximum quality.
It seems counterintuitive to prioritize color data early, but since chrominance is saved at half resolution (meaning 1/4 the pixels), the total weight of is only half that of the channel.
The "Regressive" Hack
Because each scan defines its own spectral range, it is possible to create a JPEG where subsequent scans overwrite previously rendered pixels.
While this can be done via a hex editor, a simple C program is more efficient. When loaded over a slow connection, the browser will cycle through the different images as scans arrive.
The "Zip Bomb" Limitation
Most decoders stop processing after a certain number of scans (usually around 9) to prevent resource-exhaustion attacks (similar to zip bombs). This makes creating a smooth animation impossible with standard frames.
Optimizing for Frame Count
To bypass the scan limit, we must minimize the scans per frame.
- Attempt 1: Use baseline JPEGs (single scan). Failed. Baseline decoders stop after the first scan.
- Attempt 2: Use progressive mode with one scan containing both AC and DC data. Failed. Progressive scans cannot mix AC (bins ) and DC (bin ) data.
The Solution: The smallest valid progressive scan is a DC-only scan. Since DCT operates on blocks, a DC-only image results in a resolution of the original.
Optimized Scan Structure:
- Scan 0: Channels (Y, Cb, Cr), Bins (0-0), Precision (Full).
By using this method, Chrome can render roughly 90 frames before quitting. Firefox is even more lenient. This also eliminates "ghosting" because we aren't refining old AC data; we are simply replacing DC blocks.
Implementation Steps
- Create DC-only scan files.
- Use
jpegtranto pack them. - Concatenate the resulting scans.
# Define the DC-only scan parameters
cat frame.scans EOF
# DC only scan: 0,1,2:0-0,0,0;
EOF
# Use jpegtran to generate the output
jpegtran -scans frame.scans -outfile out.jpg in.jpg
Conclusion and Applications
By packing these DC-only scans, you can essentially embed a video within a single image file.
Note: This has virtually no practical utility. There is no metadata for timing, meaning the "framerate" is determined entirely by the network's latency.
However, it allows for some creative experimentation:
- Trolling: Unconventional Rickrolls.
- HTML "Video": Using
<dialog>tags to create a video effect (seebadapple.rose.systems). - JS-less Apps: An interactive single-page application with no CSS or JavaScript.
For those interested in the technical implementation, the source code is available at /projects/bad_jpeg/merge.c.