深度解析

PNG 的诞生:从 GIF 专利危机里走出来的无损格式

koboshiCo-founder
·6 分钟阅读
PNG 的诞生:从 GIF 专利危机里走出来的无损格式
概述

PNG 诞生于 1995 年的 GIF 专利危机。一群志愿者用大约四个月搭出雏形,靠滤波器预处理加 DEFLATE 压缩,最终取代 GIF 成为静态无损图像的首选。这里是从 Unisys 诉讼到十六进制签名的完整技术故事。

用十六进制编辑器打开一个 PNG 文件。前八个字节:

89 50 4E 47 0D 0A 1A 0A

这就是 PNG 文件签名。每个 PNG 解码器都会检查它。第一个字节 0x89 被刻意设得很高,是为了防止简单的文本编辑器把它当成 ASCII 处理。50 4E 47 是 ASCII 里的 "PNG"。0D 0A 是 DOS 换行符,1A 是 DOS 的文件结束标记,0A 是 Unix 换行符。设计者在签名里埋入这些换行字符,是为了让以文本模式进行 FTP 传输时文件立即损坏,迫使用户使用二进制模式。这只是开发者们被吓出来的过度反应,他们刚目睹一种受专利保护的格式沦为法律武器。

PNG 现在到处都是。截图、UI 素材、图表、Logo,任何需要反复保存后像素依旧分毫不差的图像,基本都用它。它比 Google 和 iPod 都老,至今仍是需要无损压缩时的默认选择。但它最初并不打算成为一种格式,一开始只是个临时方案,从没想当什么标准。

一场诉讼催生了新格式

1995 年 1 月,Unisys 宣布将对使用 GIF 格式的开发者强制执行 LZW 压缩专利(美国专利 4,558,302)。该专利覆盖了 Lempel-Ziv-Welch 算法,这是每个 GIF 编码器和解码器的核心压缩方法。

1995 年的互联网靠 GIF 运转。动态横幅、透明 Logo 和平铺背景都由 GIF 处理。Unisys 开始收授权费之后,商业软件厂商要面对许可费用,开源项目也受到威胁。GNU Image Manipulation Program(GIMP)直接受到影响,Web 开发社区的反应很激烈。

必须有种东西能替代 GIF 处理静态图像。需求很明确:没有专利、压缩率优于 GIF、支持真彩色、拥有真正的阿尔法通道而不是 GIF 那种只有一位的透明。它还必须足够简单,让一个开发者用一个周末就能写出解码器。

1995 年 4 月 4 日,Thomas Boutell 在 comp.graphics Usenet 新闻组发了一份提案。他称之为 PBF(Portable Bitmap Format)。这个名字没叫开。接下来的几个月里,一个邮件列表逐渐形成。贡献者包括 Tom Lane(Independent JPEG Group 负责人)、Lee Daniel Crocker、Alexander Lehmann,以及几十位其他人。到 1996 年 10 月 1 日,PNG 规范以 RFC 2083 的形式冻结。从想法到标准,整个过程大约花了十八个月。作为对比,JPEG 花了六年。

PNG 出现之前的选择

1995 年,可选的图像格式很有限,而且每个都有明显短板:

格式压缩色深透明专利风险典型用途
GIFLZW最多 256 色1 位,1 种颜色有(LZW)Web 图形、动画
JPEGDCT + Huffman24 位真彩色基础专利已过期照片
BMP无或 RLE最高 24 位Windows 壁纸
TIFFLZW、PackBits 等最高 48 位LZW 可选印刷、扫描
PCXRLE最高 24 位DOS 游戏、早期剪贴画

GIF 统治了 Web,但带着专利风险。它的 256 色调色板对图标和卡通来说够用,对照片则完全不行。它的透明是二元的:一个像素要么完全不透明,要么完全透明。没有柔和边缘,也没有投影。

JPEG 处理照片极为出色,但会不可逆地销毁数据。打开一张 JPEG,编辑,再保存一次,图像就开始劣化。JPEG 也完全不支持透明。对于需要把 Logo 放在纹理背景上的 Web 设计师来说,JPEG 毫无用处。

BMP 和 PCX 要么不压缩,要么几乎不压缩。1995 年一张 640 × 480 的 BMP 要吃掉 900 KB。在 28.8 kbps 的调制解调器上,下载需要四分多钟。

TIFF 强大且灵活,但灵活性也正是它的问题。TIFF 文件可以使用十几种不同的压缩方案、色彩空间和位深。写一个通用 TIFF 解码器是论文级别的项目,不是周末就能 hack 出来的。

PNG 的设计目标很精准:取代 GIF 处理静态图像,压缩率超过 GIF,加入真彩色和真正的透明,并且永远免费。

PNG 的压缩过程

PNG 压缩分为两个阶段。单独看,每一步都没什么特别。合在一起,效果却很好。

第一阶段:滤波

在任何压缩发生之前,PNG 先把原始像素数据送进一个滤波器。滤波器本身不压缩任何内容,它只是重新排列数据,让下一阶段能压得更好。

图像是一个字节网格。在一张晴朗天空的照片里,相邻像素几乎相同。但原始字节流仍然为每个通道存储 120, 121, 120, 122, 119。差值很小:+1, -1, +2, -3。如果你存储差值而不是绝对值,结果数字会聚集在零附近。这正是压缩算法最擅长的。

PNG 为每条扫描行定义了五种滤波类型:

滤波类型名称作用
0None存储原始字节
1Sub存储与前一个像素的差值
2Up存储与上方像素的差值
3Average存储与 Sub 和 Up 平均值的差值
4Paeth存储与最佳预测值(Sub/Up/对角线)的差值

编码器对每条扫描行尝试全部五种滤波,然后选择在第二阶段之后输出最小的那种。这就是为什么 PNG 编码器可能很慢:它在暴力搜索滤波组合。但解码很快,滤波类型存在文件里,解码器只需应用逆操作。

第二阶段:DEFLATE

滤波之后,数据用 DEFLATE 压缩,gzip 和 ZIP 文件用的也是同一种算法。DEFLATE 是 LZ77(滑动窗口重复字符串消除)和 Huffman 编码(为频繁出现的符号分配变长前缀码)的组合。

结果:一张典型的截图或 UI 图形,可以压到未压缩 BMP 的三到五分之一。照片用 PNG 压缩通常比 JPEG 大五到十倍,但每个像素都可恢复。压缩从设计上就是无损的:没有数据被丢弃,只去除冗余。

作为参考,一张 1920 × 1080 的原始 RGB 截图是 6.2 MB。同一张截图存成 PNG 通常会降到 800 KB 到 1.5 MB。同一张图像用 JPEG 质量 90 是 300 到 500 KB,但重复保存十次就会引入可见瑕疵。

PNG 增加了什么

PNG 不只是一个没有专利的 GIF 克隆。它增加了 Web 设计师从 1993 年起就一直想要的功能。

真彩色。PNG 支持 24 位 RGB(1670 万色)和 48 位深彩色。没有调色板限制。一张 PNG 照片可以显示人眼能分辨的每一种颜色。

阿尔法通道。PNG 支持 8 位阿尔法,每个像素 256 级透明。阴影可以从完全不透明平滑过渡到完全透明。圆角按钮可以在任何背景上抗锯齿。GIF 只提供 1 位透明:一种颜色要么全开,要么全关。两者完全是两码事。

Adam7 交错。PNG 可以把像素按 7 轮交错的顺序存储。浏览器在收到文件的 1/64 后就能渲染出粗略预览,然后逐步细化。与 GIF 的逐行交错不同,Adam7 从第一轮就把细节分布到整幅图像上。第三轮之后图像已可辨认,第七轮之后完全精确。

伽马校正。PNG 在元数据里存储伽马值。在 Mac(伽马 1.8)上创建的图像,在 Windows PC(伽马 2.2)上也能正确显示,无需手动校色。这在 1990 年代跨平台一致性还很罕见时,是个真实存在的问题。

CRC 校验和。每个 PNG 数据块都带有 CRC-32 校验和。损坏的下载能被立即检测出来,不会渲染成半张图像。

用文件签名识别 PNG

别信 .png 扩展名。读前八个字节,检查签名。

精确的字节布局:

Bytes 0-7:   签名 89 50 4E 47 0D 0A 1A 0A
Bytes 8-11:  数据块长度(大端 uint32)
Bytes 12-15: 数据块类型:"IHDR"(图像头部)
Bytes 16-19: 图像宽度(大端 uint32)
Bytes 20-23: 图像高度(大端 uint32)
Byte 24:     位深
Byte 25:     色彩类型
Byte 26:     压缩方法(始终为 0)
Byte 27:     滤波方法(始终为 0)
Byte 28:     交错方法(0 或 1)

浏览器里的 TypeScript:

async function isPng(file: File): Promise<boolean> {
  const buffer = await file.slice(0, 8).arrayBuffer()
  const bytes = new Uint8Array(buffer)

  const signature = [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]
  return bytes.length === 8 && bytes.every((b, i) => b === signature[i])
}

Python

def is_png(path: str) -> bool:
    with open(path, "rb") as f:
        header = f.read(8)

    return header == b"\x89PNG\r\n\x1a\n"

用标准库的更高级做法:

import imghdr

if imghdr.what("image.png") == "png":
    pass

或者使用 Pillow:

from PIL import Image

try:
    with Image.open("image.png") as img:
        is_png = img.format == "PNG"
except Exception:
    is_png = False

Go

func isPng(path string) bool {
	f, err := os.Open(path)
	if err != nil {
		return false
	}
	defer f.Close()

	buf := make([]byte, 8)
	if _, err := f.Read(buf); err != nil {
		return false
	}

	return bytes.Equal(buf, []byte{0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a})
}

PHP

function isPng(string $path): bool {
    $header = file_get_contents($path, false, null, 0, 8);
    return $header === "\x89PNG\r\n\x1a\n";
}

使用 fileinfo

$finfo = new finfo(FILEINFO_MIME_TYPE);
$mime = $finfo->file('image.png');
// image/png

ImageMagick CLI

magick identify -verbose image.png | grep "Format:"
# Format: PNG (Portable Network Graphics)

或者简单一点:

file image.png
# image.png: PNG image data, 1200 x 675, 8-bit/color RGBA, non-interlaced

PNG 的局限

PNG 并不完美。它的设计选择是权衡,有些直到今天还在让我们付出代价。

没有内置动画。PNG 工作组明确拒绝了动画。他们想在增加复杂性之前,先把静态图像问题彻底解决。结果是动画 GIF 又存活了二十年。APNG(Animated PNG)最终在 2004 年被标准化,但浏览器支持直到 2010 年代末才趋于完善。即使今天,动画 GIF 的数量仍然以数量级优势超过 APNG。

照片文件体积过大。一张 1200 万像素的照片存成 PNG 通常是 15 MB 到 25 MB。用 JPEG 质量 90 则只有 3 MB 到 5 MB。PNG 的无损压缩无法与基于 DCT 的心理视觉丢弃竞争。对于照片,PNG 是错误的工具。

不支持 CMYK。PNG 只支持 RGB。需要 CMYK 分色的印刷工作流程必须把 PNG 转成 TIFF 或 JPEG。这是刻意为之,设计者聚焦于屏幕显示,而非印刷,但它限制了 PNG 在专业出版中的实用性。

解码比 JPEG 慢。PNG 解码需要对每条扫描行进行逆滤波和 DEFLATE 解压。JPEG 解码则可以高度并行化,并且已被硬件深度优化。在移动设备上,一张大 PNG 的渲染时间可能是同等分辨率 JPEG 的两到三倍,拖慢 Largest Contentful Paint。

没有渐进质量。与 JPEG 2000 或 JPEG XL 不同,PNG 无法从截断的文件生成低质量预览。要么你有完整的文件,要么什么都没有。Adam7 交错有助于生成粗略预览,但它不会减少总文件大小。

为什么 PNG 从未取代 JPEG

这是关于图像格式最常见的误解。PNG 和 JPEG 从来不是在做同一份工作。

JPEG 是一种有损心理视觉压缩器。它丢弃的是你的眼睛大概率不会注意到的数据。它为连续色调照片设计,也就是那些具有平滑渐变、精细纹理和自然光照的图像。在这个领域,JPEG 在尺寸-质量曲线上三十三年后仍然无人能敌。

PNG 是一种无损数据压缩器。它保留每一位。它为离散色调图像设计,包括截图、UI 元素、图表、Logo、文字叠加,其中锐利边缘和精确颜色至关重要。在这个领域,PNG 是标准。

两种格式各管一摊:

使用场景正确格式原因
照片JPEG/AVIF有损压缩可以小五到十倍
截图PNG/WebP无损保留文字锐度
带透明的 LogoPNG/WebP阿尔法通道 + 无损边缘
UI 图标PNG/SVG体积小,颜色精确匹配
科学数据可视化PNG渐变图例不产生瑕疵
印刷就绪图像TIFF/JPEG XL支持 CMYK、高位深

PNG 从来没想过取代 JPEG。它只是找到了不同的用武之地。

PNG 现在的位置

2025 年 Web Almanac 的数据显示,PNG 约占 Web 上所有服务图像的 22%。这比峰值有所下降,WebP 和 AVIF 正在蚕食份额,但 PNG 仍然是所有浏览器、图像编辑器、操作系统都能直接打开的备用格式。

WebP(Google,2010)同时支持有损和无损模式,外加动画和透明。无损 WebP 通常比同等 PNG 小 20% 到 30%。浏览器支持自 2020 年起已全面普及。对于新项目,WebP 无损在大多数情况下是 PNG 的务实替代。

AVIF(AOM,2019)实现了更优的压缩率,但其无损模式的编码和解码速度都比 PNG 慢。AVIF 也缺少对某些高级 PNG 功能的完整浏览器支持,比如 16 位通道和嵌入式伽马校正。

SVG 在简单的矢量图形领域统治了图标和 Logo,但栅格化的复杂图形仍然需要 PNG。

现实是,PNG 不会消失。它是安全默认值,是你导出文件时用来保证收件人能打开它的格式。它是图像格式的 QWERTY 键盘,不是最优的,但人人都会用。

PNG 的未来

PNG 是一个已完成的标准。规范自 2003 年以来没有实质性变化。这种稳定性是特性,不是缺陷。你在 1997 年创建的 PNG,在任何现代浏览器里打开,渲染结果完全一致。

但 PNG 周围的生态系统仍在演进:

APNG 正在逐步普及。Safari 从 2014 年起就支持它。Chrome 和 Firefox 随后跟上。Discord、Slack 和 Twitter 都原生渲染 APNG。对于短的 UI 动画,比如加载转圈、反应表情、状态指示器,APNG 正以更小的文件和更好的色彩保真度取代动画 GIF。

PNG 优化工具 持续进步。oxipngpngcrushzopfli 可以通过暴力搜索更好的滤波组合和 DEFLATE 参数,把 PNG 文件再削减 10% 到 30%。对于高流量网站,把每张 PNG 过一遍 oxipng 已是标准做法。

PNG 作为容器。一些现代工作流程把 ICC 色彩配置文件、EXIF 元数据甚至 XMP 数据嵌入 PNG 数据块。PNG 已经成为一种轻量级归档格式,不如 TIFF 丰富,但可移植性强得多。

长期来看,PNG 将与 WebP、AVIF 和 JPEG XL 共存多年。它占据了一个没有其他格式能完全覆盖的细分领域:无损、无专利、到处都支持、简单到足以让一个开发者在一周内根据规范写出解码器。这种组合很难被取代。

PNG 的诞生,是因为开发者不想再给 GIF 的专利费买单。一场诉讼威胁到了开放网络,一群人在业余时间做出了替代品。他们没打算造一个能存续三十年的标准,只是想解决一个具体问题:在不付授权费的情况下,把一张透明 Logo 放到网页上。

这个临时方案比预期活得更久。PNG 给 Web 带来了真彩色图形、平滑透明和文件完整性检查。它不是最小的格式,也不是解码最快的,动画和照片都不擅长。但当你需要确保存进去的每一个像素,日后取出来还是原样,你通常还是会选 PNG。

不是所有图片生来就是 PNG。如果你手里的 JPG 需要透明通道或无损编辑,直接在浏览器里就能转换,不用装任何软件,数据也不会离开你的设备。JPG 转 PNG在本地完成这件事。做 Web 项目时如果文件体积让人头疼,JPG 转 WebP能在不牺牲画质的情况下把文件变小。而当你需要 favicon 图标时,JPG 转 ICO能把照片变成多种尺寸的 ICO 文件。

更多推荐阅读