BlackHalo logo
Published on

退化JPEG:将视频塞进一张图片

Authors
  • Name
    vitaut
    maurycyz.com
一组在JPEG文件中嵌入多帧动画的示意图,展示网络加载时画面逐帧切换的效果
头图来源: Wikimedia Commons(请在文件页查看原作者与许可条款)

JPEG渐进模式的工作原理

JPEG文件支持渐进式编码,其核心是将图像数据分割成多个“扫描”(scan),每个扫描携带不同频段的信息。网络传输时,客户端先解码低频扫描获得低分辨率预览,然后继续下载高频扫描逐步完善细节。这种设计原本是为了改善用户体验,让大图在完全下载前即可大致可见。

扫描的结构

每个扫描以 FF DA 标记开头,后面跟着长度、包含的颜色通道、使用的霍夫曼表索引以及DCT频段范围。例如,第一个扫描可以只包含直流分量(DC,即0频段的系数),这样图像显示为16×16像素块的色块,分辨率极低。后续扫描逐步添加交流分量(AC,1-63频段),直至完整图像。

颜色通道采用YCbCr空间,其中Y(亮度)最重要,Cb/Cr(色度)精度可降低。作者给出的一个典型渐进JPEG包含10个扫描:先有DC扫描提供半精度预览,然后亮度用1-5频段细化,接着色度全频段以半精度填充,最后全精度覆盖,以此实现质量平滑提升。

将多个图像嵌入同一个JPEG

作者发现了渐进模式的一个漏洞:既然每个扫描可以独立指定DCT频段范围,理论上可以构造文件,让后续扫描覆盖已渲染的数据,从而显示完全不同的图像。实现方法很简单:将多个尺寸相同的JPEG文件拼接,去掉SOI、SOF和EOI标记,保留每个文件中的扫描段。通过网络加载时,解码器会依次处理这些扫描,导致画面在每个“帧”切换。

但这种做法很快遇到瓶颈:多数解码器对扫描数量有限制(比如Chrome约90个),用于真正的动画远远不够。为了解决这个问题,需要最小化每帧所使用的扫描数量。

仅含DC扫描的帧

最简单的方式是让每个“帧”只包含单个DC扫描(即只有直流分量)。在渐进模式下,扫描不能同时包含DC和AC数据,但可以单独使用DC扫描。这样每帧的DCT块大小为16×16,得到1/16分辨率的缩略图。由于去除了AC扫描,避免了“鬼影”现象(AC扫描本应细化前一帧的数据,叠加后会产生混叠)。

通过命令行工具 jpegtran 可以轻松生成此类文件:

jpegtran -scans frame.scans -outfile out.jpg in.jpg

其中 frame.scans 指定DC-only扫描。作者用这种方法将90个不同画面放入一个JPEG,在Chrome中可正常播放,Firefox支持更多。

实际演示与趣味应用

作者给出了一个纯粹基于HTML <dialog> 标签的视频演示网站(badapple.rose.systems),展示“bad apple”字符画动画全部嵌入单张JPEG。另外还提供了一个无需CSS和JavaScript的交互式单页应用,完全依赖JPEG的渐进渲染。

文章最后总结:这毫无实际用途——无法添加时间信息,播放速度完全取决于网络延迟。但它是一次对JPEG格式边界的精彩探索,适合用于恶作剧(例如rickroll)或技术炫技。相关代码(merge.c)已公开,可供感兴趣的人研究。

原标题:Regressive JPEGs。 HN 原始发布时间:2026年7月18日星期六。当前记录为 655 分、65 条评论。

阅读原文 · 查看 HN 讨论