注意:本文所述的方法不适用于 PNG 格式的文件及 JPEG 中大于 64 KB 的 ICC。
在数字图像中,标记色彩空间的一种方式是嵌入 ICC 文件。许多图像格式如 JPEG、HEIC、TIFF 都支持内嵌 ICC 文件,但这些格式存储 ICC 的方式各不相同,比如 JPEG 放在 APP2 标记段中,HEIC 放在 ‘colr’ box 中,如果按照不同的格式先解析,再从输出中提取 ICC 部分,即繁琐,又容易遗漏。
比如 JPEG 的拓展 ISO 21496-1 HDR 图像,使用多个 JPEG 流拼接,MPF 标记位置。这种文件使用 PIL 等库的默认方法来解析,往往只能获得第一段 JPEG 的内容,无法完整提取后面可能存在的 ICC 文件。
有没有一种方法可以不关心图像格式,直接从二进制数据中提取出 ICC 文件呢?ICC 文件是自描述的,有非常明显的特征和结构化的数据帮助我们直接识别和提取它,只要它没有在文件的字节流中被压缩过,我们就可以尝试直接识别与提取。
PNG 等特殊情况
在 PNG 文件格式中,ICC profile 存储在一个名为 iCCP (Embedded ICC profile) 的数据块(Chunk)中。这个数据块的具体结构如下:
- Profile 名称:1-79 字节的 ASCII 字符串。
- Null 分隔符:1 字节(0x00)。
- 压缩方法:1 字节(目前规定只能是 0x00,代表 zlib/deflate 压缩)。
- 压缩后的 Profile 数据:剩余的字节全是经过 zlib 压缩的 ICC 数据。
由于压缩的存在,无法再从字节流中找到 'acsp' 等特征,也就无法直接提取。
TIFF 可以对文件使用 ZIP 等压缩,但不会影响图像以外的部分,仍可识别到完整无压缩的 ICC 字节流。
另外,JPEG 中用于存储 ICC 数据的 APP2 Marker 数据块,最大能容纳约 64KB 的有效载荷。如果 ICC 文件过于复杂,则会被切片分段存储,提取出的连续字节流中会混入 JPEG 的 APP2 标记头数据,导致提取出的 ICC 文件损坏且无法解析。
ICC 文件的结构特点
根据 ICC 规范(ICC.1:2022),任何合法的 ICC 文件都包含一个 128 字节的文件头,其中有两个字段非常关键:
- 字节 0~3:文件总大小,以
uInt32Number(无符号 32 位整数,大端序)存储。 - 字节 36~39:固定的签名
'acsp'(十六进制61 63 73 70)。
这意味着,无论 ICC 文件嵌入在何种文件中,只要我们能在二进制流中找到 'acsp' 签名,向前偏移 36 字节就能定位到 ICC 文件的开头,再读取前 4 字节即可知道整个 ICC 文件的大小,从而完整地提取出来。
以下是一个简单的提取思路:
- 以二进制方式扫描整个文件,搜索
b'acsp',位置不能在文件的前 36 字节。 - 对每个匹配位置,向前 36 字节找到开头,并读取四个字节,转化为无符号的 32 位整数。
- 检查这个整数是否合理,它必须大于 128,并小于文件的剩余长度。
- 按照该整数,从开头位置读取出完整字节流。
- 简单的验证是否是 ICC 文件。
- 向后继续查找。
简单验证方法
为了确认提取出的的确是一个 ICC 文件,我们可以进一步尝试解析一下提取出的这堆字节。比如文件头中的版本号和设备类。
- 字节 8~11:版本号,字节 8 存储主要版本号,字节 9 的高四位和低四位分别存后两位版本号,别的应当为 0。
- 字节 12~15:设备类,使用
ascii存储四位字符,常见的有mntr(显示设备),scnr(输入设备)等。
这些相对比较固定的字段可以用来简单的验证是否真是一个 ICC 文件,仅依靠 b'acsp' 的纯理论碰撞概率是 $1/2^{32}$,即便在各种格式的近百张图像的测试中,没有出现过误判和漏判的情况。但仍需提醒,这样简单粗暴的方法只能用来快速提取 ICC 文件,不适合用在任何严肃的用途中,且不能用于提取 PNG 文件中的 ICC 或 JPEG 中大于 64 KB 的 ICC。
Python 实现
这里是以上内容的简单 Python 实现,可以扫描并提取输入文件中的一个或多个 ICC 文件,然后简单的解析其版本号和设备类。
uv run extract_icc.py image.jpg
一些小发现
从各种地方提取出的 ICC 文件中,有以下的小发现:
- 一个基本的 RGB ICC 文件的经验大小大概是 530 字节,包含文件头,描述,Copyright,三基色 XYZ,白点 XYZ,参数化的传递函数和色适应矩阵。
- 经典的 sRGB ICC 文件有 3 KB,主要原因是使用了 1D-LUT 形式的传递函数,即便三通道相互复用,仍需要记录 1024 个点,占用了 2060 个字节。这几乎和 B 站视频的封面图片文件一样大,使用 AVIF 格式存储的 B 站视频封面大概只有 3-4 KB,它使用 CICP 标识色彩空间而非 ICC,能够节约大量空间(相对已经很小的图片文件来说)。
- 一些包含了具体转换意图的 ICC 文件可能有 30 KB 大,主要是庞大的 3D-LUT(比如
A2B0),以及用于描述 HDR 下变换的部分。这甚至有可能比单独存一个 Gainmap 都大,半分辨率的灰度 Gainmap 使用先进编码后可能只占到几 KB。
这是 ICC 色彩管理系统学习路上的第 0 步(也可能是第 -1 步),接下来,会分享 ICC 色彩管理的学习笔记和心得。