Regressive JPEGs
Regressive JPEGs
Regressive JPEGs: 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. Regressive JPEGs(退化式 JPEG):2026-07-17。JPEG 文件的一个很酷的特性是,它提供了优先保存低频分量的选项。这意味着当图像尚未完全下载时,它会以低分辨率显示,而不是直接被截断。在文件中,这是通过将压缩数据拆分为多个“扫描(scans)”来实现的,每个扫描前都有一个头部信息。
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] FF DA - “扫描开始”标记 00 0C - 大端长度字段(12 字节),包含自身 03 - 扫描中的通道数 (3) 01 - 第一个包含通道的全局 ID 00 - Huffman 表索引 #1 (DC: 0, AC: 0) 02 - 第二个包含通道的全局 ID 10 - Huffman 表索引 #2 (DC: 1, AC: 0) 03 - 第三个包含通道的全局 ID 10 - Huffman 表索引 #2 (DC: 0, AC: 0) 00 - 起始 DCT 区间 (DC) 00 - 结束 DCT 区间 (也是 DC) 01 - 精度:半精度,无预存数据。 f8ad 512d d3f1 cd96 - Huffman 编码的 DCT 系数 bcb0 58df 53d5 5d97 […以及更多内容]
… 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 ……这一部分包含了所有三个颜色通道的最低(DC)傅里叶区间。这三个颜色通道是 YCbCr 而不是常见的 RGB。亮度(Y)被分离出来是因为它必须保持高质量,而颜色信息即使处理得比较粗糙,视觉效果依然良好。大致上: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. 扫描 #0 包含图像的极低分辨率预览。扫描 #1 增加了亮度的一些细节。扫描 #2 到 #5 包含完整的低精度数据。扫描 #4 的频谱范围比较特殊,因为它填补了 #1 留下的空白。这样,#5 就拥有了完整的四分之一精度数据作为基础。扫描 #6 到 #9 增加了最后缺失的部分,使图像达到全质量。
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. 鉴于我之前提到颜色不那么重要,我的例子中先出现颜色数据可能看起来很奇怪:这是因为色度是以半分辨率(四分之一像素数)保存的。因此,完整的色度数据(Cr + Cb)所占的权重仅为亮度的一半。
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. 由于每次扫描都明确设置了其频谱范围,因此完全可以构建一个 JPEG 文件,让后续的扫描覆盖已经渲染的图像数据。实际上,这非常容易做到:将多个相同分辨率的图像拼接在一起,并过滤掉“图像开始”、“帧开始”和“图像结束”标记。这可以在十六进制编辑器中完成,但我使用了一个简单的 C 程序。
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. 当通过慢速网络传输时,这个拼接文件会在多个图像之间切换:点击在新标签页打开。但是,大多数解码器在扫描一定次数后会放弃:我认为这是为了避免类似“压缩炸弹”的问题……但这导致它无法支持超过 9 帧,这对于制作一段像样的动画来说是不够的。
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. 为了实现这一点,我必须尽量减少每一帧中的扫描次数。最简单的想法是从仅包含单次扫描的基准(baseline)JPEG 开始。……但这行不通:在渐进模式下,一次扫描不能同时包含 AC(大于 0 的区间)和 DC(区间 0)数据。基准模式下不存在此限制,但基准解码器在第一次扫描后就会停止。
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. 由于 AC 数据必须跟在 DC 数据之后,最小的“渐进式”JPEG 必须包含一个仅有 DC 的扫描。因为 DCT 是在 16x16 的块上运行的,所以这样的图像不会是纯色:它将是原始分辨率的 1/16。
| 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. 通过这种方式,我可以让 Chrome 在放弃前渲染大约 90 帧。像 Firefox 这样的其他浏览器更有耐心,但 90 次扫描的图像似乎在几乎所有地方都能运行。额外的好处是,这避免了简单尝试中出现的重影现象:重影是因为 AC 扫描本应是用来细化旧数据的。通常,这允许图像在不增加文件大小的情况下包含多个精度级别……但这与我的技巧不太兼容。
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: 如果文件仅包含 DC 扫描而没有实际的渐进过程,这就不是问题。由于“仅 DC”帧是符合标准的图像,创建它们不需要任何特殊操作:
cat > frame.scans<<EOF
# DC only scan:
0,1,2:0-0,0,0;
# and nothing else
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. 利用这些方法,可以将整段视频打包进一张图片中:点击在新标签页打开。除了非传统的“瑞克摇(rickrolls)”和其他恶作剧之外,这没有实际应用价值:因为无法添加时间信息,播放完全取决于网络延迟。
… although there is a lot of fun to be had using partial rendering: This is a pure HTML video using