But this is clever - just smash them together. Low frequency of one image concatenated with high frequency from another. This works surprisingly well!
That's basically the server telling the client 'That data I just sent you, well now replace it with this new thing'.
No JavaScript needed, and can work with plain http and jpeg
Easy enough to add a delay() each frame if your server is python/nodejs/PHP/whatever
Works fine for me in desktop Firefox. But on mobile iOS the “whole video within a jpeg” is 3 frames, all of which are nearly entirely solid color brown->orange->red with a vague cat silhouette. The color changes each frame, so you can tell it’s “working” but it’s certainly not what I’d call a video.
I was surprised when I pulled it up on my desktop and it did actually play like a video. Wonder if it’s causing some weird iOS image decoding glitch/edge case.
You can use Service Worker to emulate a slow connection :)
This also reminded me of MRI where low frequency is acquired first in a space called k-space
A progress bar for something that’s loading in parallel over the same network, to give the user an idea of how much the delay is?
> so playback is entirely dependent on network delay
Ultimately true, but I set up my server to send each "frame" separately, with a fixed delay between each. Each frame is small so unless your network is unusually slow, the timing is set by my server.
Obviously the demonstrations that rely on server-side timing don't work through archive.org.
[1] https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/...
[2] https://www.ioccc.org/2013/mills/index.html
The "Refresh" header is encoded backwards on this line:
https://github.com/ioccc-src/winner/blob/619f554bbdb19e5003a...
"Hmm how can we use this for fingerprinting"
And it is possible to losslessly transcode JPEG to progressive.
Lossless transcoding to JPEG XL gives even more space savings though.
Maybe you just don't notice? It can be pretty invisible sometimes. I sometimes notice that image soon after page load an image is slightly blurry, and then another pass "sharpens" it. Yeah it's not like in the "old times" when the first progressive level was almost unreadable, but there's still value in sending a lower resolution version of image in 30% of the total file-size, basically for free
Edit: the format also supports region-of-interest decoding and I suspect you can make some cool maps or fractal images with both features. But I think they're not quite prioritizing implementing that right now.
This is how we defeat skynet: by sending each other pictures of cats.
https://en.wikipedia.org/wiki/Motion_JPEG
I've been here screaming "Motion JPEG EXISTS and is well supported in browsers" the entire time those gif hacks were popular. I've built a bunch of cool stuff with it.
https://github.com/donatj/mjpeg-php/blob/master/mjpeg.php
https://github.com/donatj/imgboard/blob/master/main.go
The PHP example here is an illustration of how easy it is to pull off, and the Go example is part of a JavaScript-free multiuser drawing board.
The observations you are reporting are from images you created yourself and you know are in fact progressive jpegs? (not "regressive", although that's funny). There are of course other techniques to start with a lower resolution image that do "work" (in the sense of actually displaying a lower resolution image first at least).
I was about to say: I'm sure I've seen it work a t some point? I imagine it's a valuable thing to add for the web though. It would be really cool if you could use the same image source for thumbnail and full image, and the browser both just figures out how much to download based on pixel size and can resume previously partially downloaded images.
And yeah, the tiling isn't implemented anywhere yet, jxl doesn't really get enough funding for that. But it'll be really cool once it does since it also makes it really useful for giant images of geographic data. I don't know if it combines with streaming downloads as well, but it would be crazy cool if we effectively got OpenSeaDragon[0] support inside an image format
During the week, well, it would be unfair to call it LinkedInified, but it can often feel like a somewhat higher tier of that sort of strata. Plenty of good stuff in there still, but much more “serious business”.
But if better compression for storage or you can verify progressive serves faster then it is of course a benefit.
I guess the point I am making is that most people think: I heard it's somehow better so lets use it.
Still the question is, does it help? Trying to access an average web app will probably take minutes before the browser may even see an image. If you do everything possible to render reasonably fast on very slow speeds, then progressive is nice. On a fast connection I don't think the average user will notice the difference.
2026-07-17
One of the cool features of JPEG files is that there's the option to save low frequency components first. This means that a partially downloaded image will be displayed at low resolution instead of being cut off.
In the file, this works by breaking up the compressed data into multiple "scans", each prefixed with a header. Here's the first scan of a representive image:
FF DA - "start of scan" marker 00 0C - Big endian length field (12 bytes) Includes itself 03 - Number of channels in scan (3) 01 - Global id of first included channel 00 - Huffman table index #1 (DC: 0, AC: 0) 02 - Global id of second included channel 10 - Huffman table index #2 (DC: 1, AC: 0) 03 - Global id of third included channel 10 - Huffman table index #2 (DC: 0, AC: 0) 00 - Starting DCT bin (DC) 00 - Ending DCT bin (also DC) 01 - Precision: half, no pre-existing data.
f8ad 512d d3f1 cd96 - Huffman coded DCT coefficients bcb0 58df 53d5 5d97 [...and a lot more]
... this one includes the lowest (DC) Fourier bin for all three color channels.
The three color channels are YCbCr instead of the usual RGB. The luminance (Y) seperated because it must be high quality, but the color can be fudged quite a bit while looking fine.
Very roughly: Y = G, Cb = B - G, Cr = R - G
After it, the file contains eight more scans to fill in the rest of the data:
| Scan number | 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 |
Scan #0 contains a very low resolution preview of the image.
Scan #1 adds some details to the luminance.
Scans number two through five contain full low precision data.
Scan 4 has an unusual spectral range because it's filling in the gap left by #1. That way, number 5 has full quarter precision data to build on.
Scans six through nine add the final missing bit to bring the image to full quality.
Given what I said about color being less important, it might seem weird that my example has the color data first: This works because the the chrominance is saved at half resolution (quarter pixel count). As a result, full chrominance data (Cr + Cb) only weighs half as much as luminance.
Since each scan explicitly sets its spectral range, it should be possible to construct a JPEG file where future scans overwrite already rendered image data.
Actually, it's very easy to do this:
Concatenate multiple images with the same resolution and filter out the start-of-image, start-of-frame and end-of-image markers. This can be done in a hex editor, but I used a quick and dirty C program.
When served over a slow network, this concatenated file will switch between multiple images:
Click to open in new tab
But, most decoders will give up after some number of scans: I think this is done to avoid a zip bomb style problem... but it prevents this from working on more than 9 frames, which is not enough for a proper animation.
To do that, I'd have to minimize the number of scans in each frame. The simplest idea is to start with baseline JPEGs that only have a single scan.
... but it doesn't work:
In progressive mode, a scan can't contain both AC (bins above 0) and DC (bin 0) data at the same time. This limitation doesn't exist for baseline mode, but the baseline decoder stops after the first scan.
Since AC data must follow DC data, the smallest possible "progressive" JPEG contains a single DC-only scan. Because the DCT runs on 16x16 blocks, such an image won't a solid color:
it'll be 1/16th of the original resolution.
| Scan number | Channels | DCT bins | Precision |
|---|---|---|---|
| 0 | Y Cb Cr | 0 - 0 | Full |
Doing this, I can get Chrome to render around 90 frames before giving up. Other browsers like Firefox have more patience, but a 90 scan image seems to work almost everywhere.
As a bonus, this avoids the ghosting of the naive attempt: that happened because AC scans are supposed to refine old data. Normally, this allows images to include multiple precision levels without inflating file size... but doesn't play nicely with my tricks.
If the file only includes DC scans with no actual progression, this isn't a problem.
Since a "DC-only" frame is a standards-compliant images, creating them doesn't require anything special:
cat > frame.scans<<EOF # DC only scan: 0,1,2:0-0,0,0;
EOF jpegtran -scans frame.scans -outfile out.jpg in.jpg
Using these, it's possible to pack a whole video inside a single image:
Click to open in new tab
Besides unconventional rickrolls and other trolling, this has no practical applications: there's no way to add timing information, so playback is entirely dependent on network delay.
... although there is a lot of fun to be had using partial rendering:
This is a pure HTML video using
But a lot of people do have slow connections sometimes.
I mean that the "remove a word" 'symbol' is a 'word' that represents the verb he was trying to invoke, by sounding like it.
Birds chirp, bees buzz, moderators, toilets flush.