logo
Kurashizu Blogwhere ideas flow
  • Home
  • News
  • Blog
  • About
  • Home
  • News
  • Blog
  • About

© 2026 Kurashizu. All rights reserved.

Admin·New Post·Service Status
Powered by Cloudflare·04.11.26
← Back to news

Regressive JPEGs

Original article
maurycyz.com|570 points|59 comments|by vitaut|Jul 18, 2026

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 DA →\rightarrow→ The "start of scan" marker.
  • 00 0C →\rightarrow→ Length field (12 bytes, Big endian), including the header itself.
  • 03 →\rightarrow→ Number of channels included in this scan.
  • 01 →\rightarrow→ Global ID for the first channel.
  • 00 →\rightarrow→ Huffman table index #1 (DC: 0, AC: 0).
  • 02 →\rightarrow→ Global ID for the second channel.
  • 10 →\rightarrow→ Huffman table index #2 (DC: 1, AC: 0).
  • 03 →\rightarrow→ Global ID for the third channel.
  • 10 →\rightarrow→ Huffman table index #2 (DC: 0, AC: 0).
  • 00 →\rightarrow→ The starting DCT bin (DC).
  • 00 →\rightarrow→ The ending DCT bin (also DC).
  • 01 →\rightarrow→ Precision: 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 (Y\text{Y}Y) is treated with higher priority because the human eye is more sensitive to brightness than color. Chrominance (Cb\text{Cb}Cb and Cr\text{Cr}Cr) can be significantly compressed without a perceived loss in quality.

The relationship is roughly: Y≈G\text{Y} \approx \text{G}Y≈G Cb=B−G\text{Cb} = \text{B} - \text{G}Cb=B−G Cr=R−G\text{Cr} = \text{R} - \text{G}Cr=R−G

Example Scan Sequence

A typical progressive file might contain ten scans to fully reconstruct the image:

Scan #ChannelsDCT Bin RangePrecision
0Y, Cb, Cr0 - 0Half (-1 bit)
1Y1 - 5Quarter (-2 bits)
2Cb1 - 63Half
3Cr1 - 63Half
4Y6 - 63Quarter
5Y1 - 63Half
6Y, Cr, Cb0 - 0Full
7Cr1 - 63Full
8Cb1 - 63Full
9Y1 - 63Full

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 Cb+Cr\text{Cb} + \text{Cr}Cb+Cr is only half that of the Y\text{Y}Y 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.

Conceptual animation of switching images

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). →\rightarrow→ Failed. Baseline decoders stop after the first scan.
  • Attempt 2: Use progressive mode with one scan containing both AC and DC data. →\rightarrow→ Failed. Progressive scans cannot mix AC (bins >0> 0>0) and DC (bin 000) data.

The Solution: The smallest valid progressive scan is a DC-only scan. Since DCT operates on 16×1616 \times 1616×16 blocks, a DC-only image results in a resolution 1/16th1/16\text{th}1/16th 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 jpegtran to 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:

  1. Trolling: Unconventional Rickrolls.
  2. HTML "Video": Using <dialog> tags to create a video effect (see badapple.rose.systems).
  3. 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.