⏱️ Lectura: 11 min
A normal JPEG gets cut off when the download stops halfway through. The progressive JPEG format, by contrast, lets you save the data in layers, from coarse to fine, so that a partially loaded image already looks complete at low resolution. The developer who goes by Maurycy published a technique on July 17, 2026 that takes this idea to the extreme: concatenating dozens of different images inside a single .jpg file so that, while it loads, the browser shows them one after another as if it were a video.
📑 En este artículo
The technique doesn’t use video codecs or containers like MP4, nor a single line of JavaScript. It only abuses a little-known detail of the JPEG standard: each scan inside the file declares its own data range, and nothing prevents a later scan from overwriting the pixels drawn by the previous one.
TL;DR
- Maurycy published a technique on July 17, 2026 to hide video inside a progressive JPEG.
- The trick concatenates several same-sized images into a single file by exploiting the format’s scans.
- Chrome renders around 90 scans before giving up on the load; Firefox tolerates more before bailing out.
- Each DC-only scan stores only bin 0, which reduces each frame to 1/16 of the original resolution.
- jpegtran, part of libjpeg-turbo, generates these DC-only scans using a scan specification file.
- There’s no way to control the timing between frames: playback depends entirely on the network.
- The author published merge.c, the source code used to generate the example files.
What happened
On July 17, 2026, Maurycy published a technical article on his personal blog documenting how to abuse progressive JPEG scans to hide video inside a single image file. The technique doesn’t require video codecs or containers like MP4 or WebM: the whole effect happens because the browser interprets the file as a normal JPEG image, scan after scan.
The result is .jpg files that, while downloading (or loading artificially slowly), show a sequence of different images one after another, as if it were an animation. The author himself demonstrated the trick with a clip of the “Bad Apple” meme and with an interactive single-page application, with no CSS or JavaScript, published at badapple.rose.systems.
Context and history
To understand the trick, you need to understand what makes progressive JPEG special compared to baseline JPEG, the classic mode. In baseline mode, the data for each 8×8 pixel block is stored in full, one after another. If the download is cut off halfway, the bottom half of the image simply doesn’t appear.
Progressive mode solves that problem by splitting the data into multiple scans. Each scan carries a fragment of the frequency information (the Discrete Cosine Transform, or DCT, coefficients) for one or more channels. The first scan carries only the DC coefficient, basically the average color of each block, and the following scans add AC coefficients of increasingly higher frequencies until the image is complete. The practical result: a partially loaded image already looks complete, though blurry.
JPEG splits color into three YCbCr channels instead of RGB: Y is luminance (brightness) and Cb/Cr are chrominance (color). The human eye is more sensitive to changes in brightness than in color, so encoders usually store Cb and Cr at half resolution, which reduces their size even at full precision.
| Scan | Channels | DCT bin range | Precision |
|---|---|---|---|
| 0 | Y, Cb, Cr | 0-0 (DC) | 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 (DC) | Full |
| 7 | Cr | 1-63 | Full |
| 8 | Cb | 1-63 | Full |
| 9 | Y | 1-63 | Full |
Nine or ten progressive refinement scans is normal for a well-encoded JPEG. That’s the crack the trick exploits: each scan explicitly declares its own DCT bin range and its own channels. Nothing in the standard prevents a later scan from redeclaring the 0-0 range with a completely different image, overwriting what the previous scan had already drawn.
Technical details of progressive JPEG
Building the file is, according to the author’s own description, surprisingly simple: concatenate several JPEG images of the same width and height, filtering out each one’s start and end markers (start-of-image, start-of-frame, end-of-image) to leave only the scan data. The browser can’t distinguish a file with nine refinement scans of the same photo from a file with nine scans that are actually nine different photos: it simply processes them in order and overwrites each DCT range as declared.
That first attempt has two problems. The first is compatibility: browsers limit how many scans they process per file, apparently to prevent a zip-bomb-style problem. Maurycy measured that Chrome renders around 90 scans before giving up on the load, while Firefox has more patience. That gives, at most, about 90 frames per file if each frame occupies a single scan.
📌 Note: The scan limit isn’t formally documented in the JPEG standard: it’s an implementation decision by each browser to prevent resource abuse, similar to the limits against zip bombs in decompressors.
The second problem is visual: if each frame occupies several progressive scans (DC plus AC refinements), a ghosting effect appears, because AC scans are designed to refine DC data already drawn, not to replace it with a different image. The result looks blurry, with artifacts from the previous frame overlapping.
The solution the author found is more elegant: use a single DC-only scan per frame, in baseline mode inside a progressive container. The standard requires that in progressive mode a scan can’t mix DC data (bin 0) and AC data (bin 1 onward) at the same time, so the smallest possible DC-only progressive scan is already, by itself, a valid and complete JPEG image. Since the DCT operates on 16×16 pixel blocks with typical chroma subsampling, each frame ends up at 1/16 of the original resolution: blurry, but without ghosting.
cat > frame.scans <<'EOF'
0,1,2:0-0,0,0;
EOF
jpegtran -scans frame.scans -outfile frame01.jpg entrada01.jpg
The frame.scans file tells jpegtran to generate a single scan (channels 0, 1, and 2, meaning Y, Cb, and Cr) with DCT bin range 0-0 and full precision. The result, frame01.jpg, is a valid single-scan progressive JPEG image.
How to start testing it
To reproduce the trick you need jpegtran, part of the libjpeg-turbo package. Installation varies by operating system:
# Linux (Debian/Ubuntu)
sudo apt install libjpeg-turbo-progs
# macOS (Homebrew)
brew install jpeg-turbo
# Windows (PowerShell with Chocolatey)
choco install libjpeg-turbo
With jpegtran installed, the workflow has three steps: resize all the source frames to the same width and height, convert each one to a DC-only JPEG with the command above, and concatenate the scan data (without the intermediate SOI, SOF, and EOI markers) into a single final file. A script like this automates the last step:
import sys
SOI, EOI = b"\xff\xd8", b"\xff\xd9"
def extraer_scan(datos):
inicio = datos.index(SOI) + len(SOI)
fin = datos.index(EOI, inicio)
return datos[inicio:fin]
def construir_video_jpeg(rutas_frames, salida):
scans = [extraer_scan(open(ruta, "rb").read()) for ruta in rutas_frames]
with open(salida, "wb") as out:
out.write(SOI)
for scan in scans:
out.write(scan)
out.write(EOI)
if __name__ == "__main__":
construir_video_jpeg(sys.argv[2:], sys.argv[1])
Run as python3 concat_frames.py salida.jpg frame01.jpg frame02.jpg frame03.jpg, the script produces a single file with all the DC-only scans chained together. Open the result with the network throttled from the browser’s DevTools to see how each scan overwrites the previous one.
To confirm that a frame ended up as DC-only rather than fully progressive, you can inspect it with exiftool:
exiftool -EncodingProcess frame01.jpg
# Encoding Process: Progressive DCT, Huffman coding
sequenceDiagram
participant N as Browser
participant S as Server
N->>S: requests video.jpg
S-->>N: scan 0 (DC, image A)
Note over N: draws image A at low resolution
S-->>N: scan 1 (DC, image B)
Note over N: overwrites with image B
S-->>N: scan 2 (DC, image C)
Note over N: overwrites with image C
Note over N,S: loading continues until the browser's scan limit
Impact and analysis
To be clear about what this is and what it isn’t: it’s not a security vulnerability or an exploit. It’s a creative use, unforeseen by the standard’s designers, of a legitimate progressive JPEG feature. Maurycy himself sums it up by saying that, beyond unconventional rickrolls and trolling, the technique has no real practical application, because there’s no way to encode timing information inside the file: playback speed depends entirely on how fast, or slow, the bytes arrive over the network.
Still, the technique illustrates something interesting about file format design: the more layers of flexibility a standard has (in this case, that each scan can redeclare its own data range), the more surface remains available for uses its original authors never imagined. It’s the same principle behind other classic polyglot file tricks, files valid in two formats at once, except here the other format is, in practice, improvised video inside a still image.
💭 Key takeaway: Chrome’s scan limit isn’t a number fixed by the JPEG standard: it’s a defensive barrier the browser uses against files that try to expand resource usage disproportionately.
What’s next
Maurycy left a thread open for further exploration of the trick: an interactive single-page application, with no CSS or JavaScript, that uses the same progressive JPEG principle with content loaded dynamically instead of precompiled. The author admits in the article that the demo still has flaws and that he’s still investigating it. It remains to be seen whether any browser adjusts its scan limit, or its handling of redundant scans, in response to this kind of publication, something that has already happened before with other polyglot file tricks and lax image format parsers.
For now, the source code that generates these files, merge.c, is published and available for anyone who wants to experiment with their own frame sequences.
📖 Summary on Telegram: View summary
Try it yourself: install libjpeg-turbo, generate three or four DC-only frames with jpegtran, and concatenate them with the script above to see in your own browser how many scans it can handle before giving up.
Frequently Asked Questions
Is this a security vulnerability?
No. There’s no code execution or data leakage: it’s an unconventional use of a legitimate progressive JPEG standard feature, done out of technical curiosity and for trolling purposes.
Why does Chrome stop showing frames after a certain point?
Because it limits how many scans it processes per file, a defensive measure similar to the limits against zip-bomb-style attacks. Maurycy measured Chrome’s limit at around 90 scans.
Can you control the video’s playback speed?
Not directly. The file carries no timing information: speed depends on how fast the browser receives each scan over the network.
What’s the difference between a DC-only scan and a fully progressive one?
A DC-only scan stores only the zero-frequency coefficient of each block, giving a blurry image at 1/16 resolution but without later AC refinements, avoiding the ghosting effect.
What tool is used to generate these files?
jpegtran, included in libjpeg-turbo, using the -scans option to manually define the DCT bin range and precision of each scan.
Does it work the same in every browser?
No. Chrome tolerates around 90 scans according to the author’s measurements, while Firefox allows more before abandoning the load, though the exact behavior can change between versions.
References
- Regressive JPEGs (maurycyz.com): original article with the full technical explanation and demos.
- merge.c: source code used by the author to concatenate the images’ scans.
- JPEG (Wikipedia): general background on the format, its baseline and progressive modes, and DCT encoding.
- libjpeg-turbo (GitHub): official repository of the library that includes jpegtran.
📱 Like this content? Follow @programacion on Telegram for daily tech content in Spanish: quick summaries, fresh content every day.
Imagen destacada: Foto de Mika Baumeister en Unsplash
0 Comments