2026年7月17日,开发者Maurycy在博客贴出两个示例文件。cats.jpg里一只猫从坐在草地慢慢变成趴在水泥地,cat.jpg里一只黑猫像是往镜头走近了几步。图片还没加载完,画面已经换了好几次——文件后缀却老老实实写着.jpg,不是GIF也不是视频。
他的结论很克制:这算不上新的动画格式,只是把JPEG标准的边界摸了一遍,顺带测出了浏览器解码器能撑到第几个scan就放弃。这件事该被记住的不是"JPEG居然能动",而是它暴露了一个多数解码器从没测试过的合法用法。
一张图片是怎么"变脸"的
渐进式JPEG本来是为了应付慢网速。图片先出一个模糊预览,再一层层补上细节。文件被拆成多个"扫描"(scan),每个scan声明自己要写哪个频段,先写最粗的DC分量,后面陆续补高频细节。
Maurycy发现,标准里没规定后面的scan必须"补细节"。它同样可以整段覆盖前一个scan已经画完的内容。他把几张分辨率相同的JPEG首尾相接,去掉多余的开始/结束标记,拼成一个文件。
慢网速下打开这张图,浏览器一段一段吃数据,画面就跟着一格一格跳成完全不同的内容。cats.jpg就是这么拼出来的。
90个scan不是标准值,是这次测出来的上限
朴素拼接有个硬伤:每张完整画面要占用多个scan(低分辨率预览、色度、亮度各来一遍),解码器撑不了几张就放弃。Maurycy测出的数字是9帧。
他后来改用标准里最简单的scan类型:只写DC分量,不带精度细化。这种"精简帧"体积小、scan数少,拼接后Chrome大约能处理到90个scan左右才被截断。Firefox的容忍度更高,但他没给出具体数字。
这个90不是JPEG规范写死的常量,只是这次测试环境下Chrome的表现。不同浏览器、不同版本的解码实现,上限本来就不是一个固定值。他推测这层限制可能是浏览器为了防止类似资源耗尽的问题设下的保护,但原文没有给出官方说明,这只是他的猜测。
文件里没有任何时间信息。画面切换的节奏完全由网络传输和解码速度决定,网速越快画面跳得越快,网速越慢反而更像慢动作,不是固定帧率播放。
对比:这四种格式差在哪
| 基线JPEG | 渐进式JPEG | GIF/视频 | 本次实验 | |
|---|---|---|---|---|
| scan/帧规则 | 单次完整写入 | 逐步补细节 | 逐帧+固定时序 | 整段覆盖前一scan |
| 时序信息 | 无 | 无,一次性渲染完 | 有帧率/时长 | 无,靠网速和解码速度决定节奏 |
| 解码上限 | 无特殊限制 | 无特殊限制 | 无特殊限制 | 受浏览器scan计数限制,本例Chrome约90 |
| 能否当动画用 | 否 | 否 | 是 | 否,画面节奏不可控 |
跟GIF或视频比,最大的差距不在画质,而在GIF和视频天生带着时序信息。JPEG的scan结构从设计之初就没打算干这个,这次实验只是找到了一个没被明确禁止的用法。
谁该关心这件事
浏览器和图像库的工程师是最该看这条线索的人。它提醒的不是"有没有漏洞",而是"有没有测过这种合法但反常的输入"——scan整段覆盖旧数据不违反标准,但多数解码器的测试集里大概率没有这类样本。
对浏览器解码器维护者来说,值得做的是把这类多帧覆盖式文件加进模糊测试和兼容性用例,看自己的实现是直接截断、卡死,还是正常渲染完。对图像处理库和图片服务的开发者来说,如果业务允许用户上传任意JPEG并做二次处理,值得留意超长scan序列会不会占用异常多的解码时间或内存——不是当成安全漏洞紧急修,而是纳入常规边界测试的一项。
普通网站开发者不用为此改动任何东西。这不是能替代GIF或视频的格式,画面切换靠网速摆布,用不到任何需要稳定播放效果的场景。
Maurycy公开了拼接代码merge.c,想验证自己用的解码库怎么处理,几分钟就能跑一遍。
接下来值得盯的,是有没有其他浏览器或图像库公开测试结果。目前只有Chrome和Firefox的粗略对比,libjpeg、Safari的表现原文没有提及,这块还看不清。
