藏在JPEG里30年:图片居然能变成动画

藏在JPEG里30年:图片居然能变成动画

JPEG图像技术历史趣味发现

数据源:Lobsters + web research

一则”随口一问”,炸出了30年前的隐藏能力

7月17日,知名安全研究者 lcamtuf(Michał Zalewski)在 Mastodon 上发了一条随意的想法:

“progressive JPEG 在加载时先发低频数据,再逐渐补清晰。我打赌可以反过来用——做一张’退化 JPEG’,先看起来挺好,慢慢变成糟糕的东西。”

这条帖子没有引起技术圈的大规模讨论——但有人看见了,而且动手了。

两天后,开发者 Maurycy 在技术社区 Lobsters 上发布了他的成果:不仅能做”退化图片”,还能让一张 JPEG 文件像 GIF 一样动起来。 这篇帖子拿下 △95 分,成为当日最高票内容。

regressive JPEG 示例:一张猫的图片在加载过程中从"草地上的猫"切换为"水泥地上的另一只猫"

▲ 来源:maurycyz.com。同一个 JPEG 文件,加载到不同阶段显示不同的猫——浏览器以为自己还在”逐步变清晰”,实际上内容已经被偷换了。

JPEG 标准诞生于 1992 年,迄今已经 34 年。从数码相机到手机相册到网页图片,全世界每天有数十亿张 JPEG 被查看、传输、存储。而就在上个月,有人发现这个标准里藏着一个从来没人用过的能力——做动画


先发模糊的,再补清晰的——一个天才设计

要理解这个发现,笔者得先解释一个上世纪 90 年代的设计。

早期上网的人应该记得:网速慢的时候,打开一张大图,画面是从上往下一行一行刷出来的。这叫做”基线 JPEG”——数据按顺序排列,浏览器收到多少就显示多少。

后来 JPEG 标准增加了一个选项,叫做”渐进式 JPEG”。它的工作方式完全不同:先把图片的”轮廓”发过去,再逐渐补充细节。

打个生活化的比方:

你朋友在信号很差的山里,想发一张合影给你。如果按老办法,你只能看到照片从上往下一格一格地刷出来。但如果用”渐进式”,你朋友可以先发一张 1/16 大小的模糊缩略图——你一眼就能认出这是合影。接着再发细节数据,人物表情、衣服纹理、背景树叶,一点一点清晰起来。

这个设计在技术上的实现方式,是把图片数据拆成多个”批次”(标准里叫 scan)。每个批次带一个标签,说明自己负责哪个精度范围。第一批只包含最粗略的信息,后续批次补充更高频的细节。

关键在于:标准规定了每批数据要声明自己覆盖哪个范围,但从来没有规定——后续批次描述的内容必须和前面的属于同一张图。


就像快递单没写”必须同一个人收货”

lcamtuf 的原始思路是”退化 JPEG”:第一批数据放一张好看的图,后续批次逐步覆盖成丑陋的东西。比如一张美食照片,加载到一半变成了发霉的食物。

但 Maurycy 发现可以走得更远。

既然后续批次可以覆盖之前渲染的内容,那为什么不直接把多个不同的图片拼进同一个文件里?具体做法简单到令人惊讶:

把多张同样尺寸的 JPEG 图片的”数据段”首尾相接,去掉中间多余的标记头,浏览器就会把它们当成同一张图片的多个”精度层”依次渲染。

类比一下:

你给快递公司发了一串包裹,每个包裹里的发货单都写着”收货地址:李四家,物品:照片”。快递员一个接一个送过去,每次都更新李四家的”照片”。但李四收到的第二张照片和第一张完全不一样——快递系统只检查”照片”这个类别对不对,不检查是不是同一张。

浏览器也一样。它检查每一批数据的标签(精度范围、色彩通道)是否符合 JPEG 标准,但从不检查”这张图跟前面的是不是同一个内容”。它忠实地把新数据覆盖到旧画面上,于是——

你看到的画面就变了。


从”偷换图片”到”播放视频”

第一版方案有一个限制:一张正常的渐进式 JPEG 包含大约 10 个批次(scan)。大多数浏览器的解码器在收到大约 10 个批次后就会认为”这张图应该已经加载完了”,拒绝继续接收。这意味着只能塞进八九帧画面,远不够做动画。

Maurycy 继续优化。他发现可以做一个”最小化批次”:每个帧只用一次 DC 扫描(只包含最基础的颜色信息,不含细节)。这样一张图片的大小只有完整版的 1/16 清晰度,但作为一帧动画已经足够。

用这种方法,Chrome 浏览器可以在放弃之前渲染大约 90 帧,Firefox 的耐心更高。90 帧,对一段短视频来说足够了。

用 JPEG 文件实现的"视频":一只黑猫从远处走向镜头

▲ 来源:maurycyz.com。一张静态 JPEG 文件,利用渐进式批次的覆盖特性,在加载过程中逐帧播放出黑猫行走的动画。这是完全符合 JPEG 标准的文件,任何浏览器都能打开。


这个东西有什么用?

坦诚地说——没什么用

这个 hack 有一个致命缺陷:无法控制播放速度。因为动画的”帧率”完全取决于网速——下载快就播得快,下载慢就播得慢。它没有 GIF 或视频格式里的时间控制机制。

而且大多数图片查看软件在图片”加载完成”后就会停止显示,不会循环播放。想要看到动画效果,必须在网速较慢的条件下逐步加载,或者使用特殊的展示方式。

但这不重要。

真正有意思的是这个发现本身:一个 30 年前就写好的国际标准,30 年来被无数工程师阅读、实现、使用,却没有一个人注意到它可以做动画——直到有人随口问了一句。


被忽视的标准,与技术好奇心的胜利

笔者觉得这个故事有种特别的魅力。

JPEG 标准——由国际标准化组织(ISO)发布,几千页的技术文档,被全球所有图片软件实现——它就在那里,一动不动,34 年。它的每一行规范都是公开的,任何人都可以阅读。

渐进式 JPEG 的”扫描覆盖”机制从一开始就写在标准里。lcamtuf 不是发现了什么新的漏洞,他只是问了一个标准从来没说过”不能做”的问题:如果后面扫描的数据和前面不一样,会发生什么?

标准回答不了这个问题——因为它根本没想过有人会这么干。

而 Maurycy 甚至没有写什么复杂的程序。他的代码就是一个短小的 C 文件,做的事情本质上就是”把几张图的中间部分拼在一起,去掉头尾标记”。真正的难度在想到可以这么干

技术圈有太多的讨论围绕着”最佳实践""性能优化""架构设计”。偶尔出现这样一个故事——有人翻出了一份 30 年前的老文档,指着一行字说”这里没说不让”——让人想起黑客精神的本来面目。


写在最后

如果你有兴趣自己试试,Maurycy 在他的博客上公开了全部代码和示例图片。一张几百 KB 的 JPEG 文件,用浏览器打开,它会在加载过程中慢慢”活”过来。

当然,这不是什么颠覆性的技术突破。它不会取代 GIF,不会改变图片格式的格局。但它是那种让人会心一笑的发现——就像在旧房子的地板下发现一扇从来没人打开过的暗门,门后面没什么宝藏,但打开它的那一刻,本身就是一种快乐。

参考链接:

  • Lobsters 讨论: Regressive JPEGs
  • lcamtuf 原始 Mastodon 提问
  • Maurycy 技术博客: Regressive JPEGs