操控JPEG渐进扫描实现嵌入式动画
渐进式JPEG的“逆向玩法”
JPEG格式有个实用特性:支持优先保存低频分量。这意味着图片无需完全下载,就能先显示低分辨率版本,而非只呈现残缺的局部。
实现这一特性的原理,是将压缩数据拆分为多个“扫描段”,每个扫描段前都有头部标识。以下是一张示例图片的首个扫描段解析:
FF DA - “扫描段起始”标记 00 0C - 大端序长度字段(共12字节,含自身) 03 - 扫描覆盖的通道数(3个) 01 - 第一个通道的全局ID 00 - 霍夫曼(Huffman)表索引#1(直流分量DC:0,交流分量AC:0) 02 - 第二个通道的全局ID 10 - 霍夫曼表索引#2(直流分量DC:1,交流分量AC:0) 03 - 第三个通道的全局ID 10 - 霍夫曼表索引#2(直流分量DC:0,交流分量AC:0) 00 - DCT(离散余弦变换)起始频点(直流分量DC) 00 - DCT结束频点(同样为直流分量DC) 01 - 精度:半精度,无前置数据 f8ad 512d d3f1 cd96 bcb0 58df 53d5 5d97……(后续为大量霍夫曼编码的DCT系数)
这个扫描段包含了三个颜色通道的最低频(直流分量DC)傅里叶频点。简单来说,Y代表亮度(近似绿色G),Cb代表蓝色差(B-G),Cr代表红色差(R-G)。
首个扫描段之后,文件中还有8个扫描段,用于补充剩余数据:
| 扫描段编号 | 涉及通道 | DCT频点范围 | 精度 |
|---|---|---|---|
| 0 | Y、Cb、Cr | 0 - 0 | 半精度(-1位) |
| 1 | Y | 1 - 5 | 四分之一精度(-2位) |
| 2 | Cb | 1 - 63 | 半精度 |
| 3 | Cr | 1 - 63 | 半精度 |
| 4 | Y | 6 - 63 | 四分之一精度 |
| 5 | Y | 1 - 63 | 半精度 |
| 6 | Y、Cr、Cb | 0 - 0 | 全精度 |
| 7 | Cr | 1 - 63 | 全精度 |
| 8 | Cb | 1 - 63 | 全精度 |
| 9 | Y | 1 - 63 | 全精度 |
扫描段#0提供了极低分辨率的图片预览; 扫描段#1为亮度通道补充细节; 扫描段#2至#5包含完整的低精度数据; 扫描段#4的频点范围比较特殊,它是为了填补扫描段#1留下的空白,让扫描段#5能基于完整的四分之一精度数据进行构建; 扫描段#6至#9则补充最后缺失的信息,将图片提升至全精度。
由于每个扫描段都会明确指定频点范围,理论上可以构造出一种特殊JPEG文件,让后续扫描段覆盖已渲染的图像数据。
实现起来其实非常简单: 将多张分辨率相同的图片拼接,同时过滤掉图像起始、帧起始和图像结束标记。可以用十六进制编辑器手动操作,我则写了一个简易的C程序来完成。
在低速网络环境下加载时,这个拼接后的文件会在多张图片之间切换:

不过,大多数解码器会在扫描段达到一定数量后停止工作: 我推测这是为了防范类似“zip炸弹”的攻击,但这也导致这种方法最多只能实现9帧切换,不足以构成完整动画。
要实现更多帧,就得尽量减少每帧的扫描段数量。最简单的思路是从仅含单个扫描段的基线JPEG入手,但这种方法行不通:
在渐进模式下,一个扫描段不能同时包含交流分量AC(频点大于0)和直流分量DC(频点为0)。基线模式没有这个限制,但基线解码器会在第一个扫描段结束后停止解析。
由于交流分量必须跟随直流分量,最小的“渐进式”JPEG只能是仅含直流分量的扫描段。又因为DCT以16×16块为单位处理,这类图像并非纯色,而是原始分辨率的1/16:
| 扫描段编号 | 涉及通道 | DCT频点范围 | 精度 |
|---|---|---|---|
| 0 | Y、Cb、Cr | 0 - 0 | 全精度 |
用这种方法,我成功让Chrome浏览器渲染了约90帧后才停止。Firefox等浏览器的容忍度更高,但90个扫描段的文件几乎能在所有主流浏览器中正常工作。
额外的好处是,这种方法避免了直接拼接时出现的“重影”问题——重影是因为交流分量扫描段本应优化已有数据,通常这能让图片在不增加文件体积的前提下包含多精度层级,但与我的玩法不兼容。而如果文件只包含无递进关系的直流分量扫描段,就不会有这个问题。
由于“仅含直流分量”的帧符合JPEG标准,生成这类文件无需特殊工具:
cat >frame.scans <<EOF
# 仅含直流分量的扫描段:0,1,2:0-0,0,0; # 无其他内容
EOF
jpegtran -scans frame.scans -outfile out.jpg in.jpg
借助这种方式,甚至能把一整段视频塞进单个图片文件:

除了制作非常规的“瑞克roll”或其他恶作剧,这种玩法没有实际应用价值——因为无法添加时间信息,播放节奏完全取决于网络延迟。
不过,利用局部渲染特性可以玩出不少花样: 比如这个纯HTML视频,只用了
当然,数据不一定非要硬编码: 这个交互式单页应用就完全没有使用CSS或JavaScript。
相关链接:
- /projects/bad_jpeg/merge.c:生成上述图片所用的代码