photocraft-codecs 全解析:纯 Rust 平面光栅编解码器的对称性、保真度与安全设计
photocraft-codecs 全解析:纯 Rust 平面光栅编解码器的对称性、保真度与安全设计
photocraft-codecs 是 PhotoCraft(纯 Rust 重写的 Adobe Photoshop 开源实现)中负责平面光栅图像格式读写的独立 crate。它不依赖工作区中任何其他 crate,自带 Image 类型,可编译到 wasm32-unknown-unknown,且完全不做 I/O(解码输入 &<a href="https://link.gitcode.com/i/708ca5597cd00e922114b85737489758" target="_blank">u8]、编码输出 Vec<u8>)。本文基于 [crates/codecs/README.md 与源码实现,系统讲解其“对称性、深度与元数据保持、诚实保真”三大设计原则,逐一给出 14 种格式的能力矩阵、保真警告机制、解压炸弹防护 Limits 以及测试策略,并标注每一条结论对应的源码路径,方便你在实际接入或二次开发时直接对照。
一、设计哲学:对称性、深度保持与诚实保真
1. 对称性(Symmetric)——避免 GIMP 的历史之坑
项目的架构文档在 docs/architecture.md 中专门讨论了“Avoiding GIMP's hole”:GIMP 因为把“像素深度、颜色模型、图层即像素缓冲区”等基本属性硬编码进内核,导致 8 位转 16/32 位浮点处理整整拖到 2018 年的 2.10 版本。photocraft-codecs 把这一教训落实为一条可测试的硬规则:
每一种我们能写入的格式,我们也一定能读取。
唯一的例外被集中登记在 ASYMMETRIC_EXCEPTIONS 常量中,且每一条都附带原因(见 crates/codecs/src/format.rs):
- AVIF:编码用纯 Rust 的
ravif,但解码需要 C 语言实现的dav1d,所以读取保持不支持;写入被放在非默认aviffeature 之后。 - HEIF/HEIC:解码依赖可选的
photocraft-heifcrate(内部是纯 Rust、无unsafe的heic-rs),而写入需要一个 HEVC 编码器——成熟实现(x265、libheif)全是 C,因此写入保持不支持。
测试套件会断言“除 ASYMMETRIC_EXCEPTIONS 之外,每一个启用的格式都可读可写”,从机制上锁死对称性回归。
2. 深度保持(Depth-preserving)与元数据保持(Metadata-preserving)
图像可以持有 U8、U16、F16、F32 四种采样类型(见 crates/codecs/src/image.rs),每种格式保留它所能容纳的深度。ICC、EXIF、XMP、DPI 和文本等元数据,只要目标格式支持就全部保留(Metadata 结构见 crates/codecs/src/image.rs)。
3. 诚实保真(Honest)——警告与实际编码使用同一份方案
这是整个 crate 最值得称道的工程点:caps(format) 如实声明每种格式能装下什么;fidelity_warnings(&image, format) 在写入之前就列出这次导出会丢失什么。关键在于 crates/codecs/src/fidelity.rs 顶部注释明确写道:
encode和fidelity_warnings共享同一个plan,所以警告恰好出现在编码器真正会转换或丢弃数据的时候。
也就是说,警告不是“大致估计”,而是与编码路径共用同一个转换方案,警告出现 ⇔ 文件真的会变。解码入口方向则用 Image::warnings 列出解码结果里看不到的东西:被跳过的帧/页,或提前结束的数据(DecodeWarning 定义见 crates/codecs/src/image.rs)。
一个最小可运行示例(README 原样保留):
use photocraft_codecs::*;
let img = decode(&bytes)?; // detect + decode (default Limits)
for w in fidelity_warnings(&img, Format::Jpeg) { // e.g. "16-bit will be reduced to 8-bit"
println!("{w}");
}
let out = encode(&img, Format::Tiff, &EncodeOptions::default())?;
二、能力矩阵(默认构建)
下表完整继承自 README,覆盖默认 feature 构建下的全部 14 种格式。read/write 两列反映当前构建的 feature 开关(例如 avif、heif 默认关闭)。
| Format | Read | Write | Depths (native) | Layouts (native) | Alpha | ICC | EXIF | XMP | DPI | Text | Lossy write | Backend |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| PNG | yes | yes | U8, U16 | Gray, GrayA, RGB, RGBA | yes | yes (iCCP) | yes (eXIf) | yes (iTXt XML:com.adobe.xmp) |
yes (pHYs) | yes (tEXt/zTXt/iTXt) | no | png |
| JPEG | yes | yes | U8 | Gray, RGB, CMYK | no | yes (multi-segment APP2) | yes (APP1) | yes (APP1) | yes (JFIF) | no | yes | zune-jpeg / jpeg-encoder |
| TIFF | yes | yes | U8, U16, F32 | all six (CMYK, CMYK+A included) | yes | yes (tag 34675) | no | yes (tag 700) | yes | yes (Description, Make, Model, Software, DateTime, Artist, Copyright) | no | tiff |
| WebP | yes | yes (lossless only) | U8 | RGB, RGBA | yes | yes | yes | yes | no | no | no | image-webp |
| GIF | yes | yes | U8 | RGBA | 1-bit | no | no | no | no | no | yes (256-colour palette) | image |
| BMP | yes | yes | U8 | RGB, RGBA | yes | no | no | no | no | no | no | image |
| TGA | yes | yes | U8 | Gray, GrayA, RGB, RGBA | yes | no | no | no | no | no | no | image |
| ICO | yes | yes | U8 | RGBA (at most 256×256) | yes | no | no | no | no | no | no | image |
| Netpbm (PBM/PGM/PPM/PAM/PFM) | yes | yes | U8, U16, F32 (float: PFM gray/RGB only) | all six (PAM CMYK/CMYK_ALPHA) |
yes (PAM) | no | no | no | no | no | no | built in |
| QOI | yes | yes | U8 | RGB, RGBA | yes | no | no | no | no | no | no | image |
| OpenEXR | yes | yes | F16, F32 | Y, YA, RGB, RGBA | yes | no | no | no | no | no | no | exr |
| Radiance HDR | yes | yes | F32 | RGB | no | no | no | no | no | no | yes (RGBE) | image |
| AVIF | no | only with feature avif |
U8 | RGB, RGBA | yes | no | no | no | no | no | yes | image + ravif |
| HEIF/HEIC | with feature heif |
no | U8, U16 (10/12-bit decodes to U16) | RGB, RGBA | yes (auxiliary alpha) | yes (colr prof) |
yes | yes | no | no | n/a | heic-rs |
表中的能力声明由 caps(format) 函数集中产生(crates/codecs/src/format.rs),每个 FormatCaps 结构体还额外包含 animation(容器是否可容纳多帧/多页)与 lossy(编码器是否有损)两个字段(定义见 crates/codecs/src/format.rs)。
“Native”的含义:数据按原样存储并读回、不做转换;任何其他情况都由编码方案(encode plan)转换,并在丢失信息时由 fidelity_warnings 报告。READme 明确了四类转换规则:
- 浮点图像写入整型格式时,若该格式支持 U16 则降到 U16;数值超出 0..1 时触发
RangeClipped警告。 - 整型数据写入纯浮点格式(EXR、HDR)时无损转成 F32。
- CMYK 用非色彩管理的朴素公式转成 RGB(实现见 crates/codecs/src/image.rs 的
cmyk_to_rgb/rgb_to_cmyk)。此时触发CmykConverted且 ICC 会被丢弃——因为 CMYK profile 描述的不是 RGB 数据。 - 灰度写入只支持 RGB 的格式时无损扩展成 RGB,不产生警告。
Image::convert 应用同样的 ICC 规则:只有颜色模型不变时才保留 profile(crates/codecs/src/image.rs,测试 icc_dropped_on_model_change 验证了这一点)。
三、格式识别与能力声明的源码实现
1. 魔数检测(detect)
detect(bytes) 通过文件头魔数识别格式(crates/codecs/src/format.rs):
- PNG:
\x89PNG\r\n\x1a\n;JPEG:FF D8 FF;TIFF(含 BigTIFF 的II+\0/MM\0+两种新魔数)。 - WebP:
RIFF+ 偏移 8 处的WEBP;GIF:GIF87a/GIF89a;QOI:qoif;OpenEXR:v/1\x01;HDR:#?RADIANCE/#?RGBE。 - HEIF/AVIF:解析
ftypbox 的品牌列表(avif/avis判定为 AVIF;heic/heix/mif1/miaf等判定为 HEIF,见is_avif_ftyp/is_heif_ftyp)。即使序列文件(hevc/msf1)也会被识别,以便打开时给出“不支持”而不是“无法识别的格式”。 - BMP:
BM;ICO:00 00 01 00头;Netpbm:P1–P7/PF/Pf。 - TGA 没有魔数,因此放到最后,通过 18 字节头部合理性判断(
looks_like_tga):图像类型、调色板字段、位深、宽高非零、描述字节高位为 0,以及TRUEVISION-XFILE.\0尾部签名。
from_extension 则按扩展名(大小写不敏感、容忍前导点和完整路径)查表,Format::extensions() 的第一个扩展名即写入时的首选扩展名。
2. 能力表如何随 feature 变化
caps 中 read/write 两列直接由编译期 feature 决定,例如:
Format::Avif => FormatCaps { read: false, write: cfg!(feature = "avif"), lossy: true, ..base },
Format::Heif => FormatCaps { read: cfg!(feature = "heif"), write: false, depths: &[S::U8, S::U16], icc: true, exif: true, xmp: true, lossy: true, ..base },
(crates/codecs/src/format.rs)。这意味着同一份代码在默认构建下,AVIF 既不可读也不可写,对称性保证依然成立;打开 HEIF 时解码器桩会明确报出“本构建未包含 HEIC/HEIF 支持”。对应 feature 定义见 crates/codecs/Cargo.toml。
四、已知不对称与逐格式限制
1. 相机原始文件(Camera Raw)
DNG、CR2、NEF、ARW 等 TIFF 结构的原始文件会被识别并拒绝,返回 CodecError::Unsupported:它们是传感器数据而非平面图像。开发与解码由 photocraft-raw 负责,photocraft-io 负责路由。
2. HEIF/HEIC(只读,feature heif)
解码位于可选 crate photocraft-heif 中,由非默认 heif feature 开启(官方构建启用,发行方自选)。关闭时 HEIF 仍能被检测,解码返回 CodecError::Unsupported(提示信息为 “HEIC/HEIF support isn't included in this build”)。开启后,iPhone 和 Mac 照片通过纯 Rust、无 unsafe 的 heic-rs 解码:支持单张与网格平铺照片、8 到 12 位、辅助 alpha、ICC、EXIF、XMP。容器的 irot/imir/clap 会被应用,同时 EXIF Orientation 被改写成 1——因为 HEIF 的 EXIF 标签只是重复容器信息,两者都应用会把照片转两次。灰度解码为 RGB。图像序列、叠加层、身份派生、多层 HEVC 以及部分 4:2:2/4:4:4 流返回 Unsupported 或 Malformed,永远不会输出错误像素。写入需要 HEVC 编码器而成熟实现均为 C,因此 HEIF 被列入 ASYMMETRIC_EXCEPTIONS。
3. AVIF
编码走纯 Rust 的 ravif;解码需要 C 的 dav1d。因此 AVIF 保持读不支持,写入由非默认 avif feature 门控。默认构建下既不读也不写,对称性保证成立,同时列入 ASYMMETRIC_EXCEPTIONS。
4. 有损 WebP
没有纯 Rust 的有损 WebP 编码器,因此总是写无损 WebP;当 webp_lossless: false 时返回 CodecError::Unsupported。读取则同时支持有损与无损文件。
5. 动画与多页文件
APNG、动画 GIF/WebP、多页 TIFF 都只解码第一帧/第一页,写入单帧。FormatCaps::animation 标记能容纳多帧的容器;解码后的图像在 Image::warnings 中携带 DecodeWarning::MoreFrames/MorePages(文件声明总数时给出 total)。帧数与 TIFF 目录数不经过解码即可统计;降分辨率 TIFF 目录与透明度掩模不算页。任一 TIFF 页仍可通过 decode_tiff_page 打开(photocraft_io::import_tiff_page 同理)。tiff_info 会列出主 IFD 链及其 SubIFD(tag 330)中每一页的尺寸、种类(页/降采样副本/掩模)与存储方式,遍历使用 seen-set、深度限制和 10000 目录上限(crates/codecs/src/lib.rs)。
6. TIFF 中的 EXIF
以子 IFD 而非 blob 形式存储,当前尚未保留,因此 caps.exif = false for TIFF,并产生警告。
7. 方向(Orientation)处理
JPEG 的 EXIF tag 274、PNG 的 eXIf、WebP,以及 TIFF/BigTIFF 解码页自身的方向标签,在解码时像 Photoshop 一样自动应用:像素以正立状态返回,EXIF/XMP 方向元数据被改写为 1(DecodeOptions::keep_orientation 可退出)。编码器永远写 Orientation = 1(upright_exif、upright_xmp 只改动标签值本身并保留其原类型),保证正立像素永远不会被二次旋转。
解析是严格的:IFD0 落在 8 字节头内、或在 next-IFD 指针前被截断、Orientation 不是恰好一个 SHORT 或 LONG、或值越界,一律按 1 读取;出现重复条目时第一个生效(与 libexif 一致)。方向 5–8 会交换宽高,此时 Limits 会针对正立后的尺寸再次校验(5–8 交换宽高,见 crates/codecs/src/lib.rs),旋转缓冲通过 try_reserve 可失败分配而非崩溃(crates/codecs/src/orientation.rs)。Image::oriented 对任意布局与深度按并行 band 旋转,README 给出实测:24 MP RGB8 在 release 构建约 10 ms(并行通过 rayon,wasm 单线程,见 crates/codecs/Cargo.toml)。upright_exif 会把所有格式良好的重复 Orientation 条目都改写为 1,防止“后写者胜”的读取器再次旋转(crates/codecs/src/orientation.rs)。
8. JPEG 细节
- 仅 8 位;两个解码后端都不支持 12 位。
- 图像数据中途截断的文件宽容解码(baseline JPEG 缺失行以灰色补出),但绝不静默:图像携带
DecodeWarning::Truncated。在扫描数据开始前就结束的文件则是错误。 - CMYK 永远以 4:4:4 写入(子采样 CMYK 不通用),采用 Adobe 反相 CMYK 并带 APP14 标记。
- 不写文本(没有 COM 段支持)。
- EXIF 与 XMP 必须各自装进一个 64 KiB 的 APP1 段,否则编码器报错;不写扩展 XMP。
fidelity_warnings对应地产生MetadataTooLarge警告(crates/codecs/src/fidelity.rs)。
9. TIFF 解码能力
- 经典 TIFF 与 BigTIFF(版本 43、8 字节偏移)、两种字节序、strips 或 tiles、planar configuration 1 或 2。
- 支持 1/2/4 位灰度、8/16/32/64 位整数(32/64 位降为 U16)、F16、F32、F64(转为 F32)、WhiteIsZero(#691:色调不再反转)、调色板图像(按 RGB 打开)、关联 alpha(转为 straight)与额外样本(除非第一个是 alpha 否则丢弃)。
- 每个目录都可达:
tiff_info列出主 IFD 链及其 SubIFD(tag 330),含每页尺寸、种类(页/降采样副本/掩模)与存储方式,遍历受 seen-set、深度限制与 10000 目录上限保护;decode_tiff_page可打开任意一个。decode像 Photoshop 一样打开第一个全分辨率页(跳过缩略图)。 - 带状解码:图像按最终尺寸一次性分配,每条 strip 或每行 tiles 直接解入其中,原生目标上并行执行,每个 worker 至多一个 strip/tile 大小的临时缓冲。README 实测(release):30 MP RGB8 下 Deflate 345 → 32 ms、LZW 428 → 43 ms。图像数据被文件结尾截断时以
DecodeWarning::Truncated解码;文件尺寸远小于声明尺寸(超过任何压缩可能扩大的程度)时,在分配之前就拒绝。 - YCbCr、CIE Lab、有符号整数以及
tiffcrate 缺失的压缩(CCITT、JPEG-in-TIFF)返回Unsupported。
10. TIFF 编码
默认写经典 TIFF;当 EncodeOptions::tiff_bigtiff 被设置、或文件可能超过 4 GiB 时自动写 BigTIFF(8 字节偏移)。
11. EXR
- 从数据窗口读取第一个有效图层(全分辨率)。
- 通道名按后缀匹配,所以
layer.R视为R。 - 不支持子采样通道与 deep 数据(deep 文件经
decode_deep_exr展开为DeepImage,普通解码则合成扁平图像并带DeepFlattened警告,见 crates/codecs/src/lib.rs)。 - 写入只提供无损压缩:None、RLE、ZIP1、ZIP16、PIZ。
12. Netpbm 写类型选择
按图像内容自动挑选子类型:浮点 → PFM,灰度 → P5,RGB → P6,带 alpha 或 CMYK → P7(PAM)。所以写 .pbm 文件时,实际产出的是与数据最匹配的那一种。
13. 颜色转换
CMYK↔RGB、RGB→灰度的转换是朴素公式(非色彩管理,Rec. 709 亮度权重见 crates/codecs/src/image.rs);色彩管理转换归属 photocraft-color crate。
五、解码安全:Limits 与解压炸弹防护
DecodeOptions { limits: Limits { max_width, max_height, max_pixels, max_alloc }, .. } 守护整个解码过程(结构定义见 crates/codecs/src/options.rs):
- 头尺寸在像素缓冲分配之前检查;
- 预算同时传递给底层解码器作为分配上限;
- 违反即返回
CodecError::LimitExceeded。
默认值:单边 262144 像素(2^18)、总计 2^30 像素、8 GiB 分配上限(32 位目标如 wasm 降为 2 GiB)。默认值选择考虑到了 Photoshop 用户会打开的大文档——一个 20000² 的 RGBA 16 位图像约 3.2 GB(见 Limits::default 注释,crates/codecs/src/options.rs)。Limits::none() 提供“近乎无限”的配置(仍受 usize 溢出检查约束)。校验函数 check_bytes 还额外做 usize 溢出检查,避免字节数在分配前就溢出内存寻址能力(crates/codecs/src/options.rs)。
值得一提的还有 finish_decode 中的最终防线:对无法预先检查头部的解码器,解码完成后仍会执行一次 Limits::check;方向 5–8 交换宽高后,还会对正立尺寸再查一次(crates/codecs/src/lib.rs)。
六、编码选项与默认值速查
EncodeOptions(crates/codecs/src/options.rs)集中了所有编码参数:
| 字段 | 默认值 | 说明 |
|---|---|---|
jpeg_quality |
90 | JPEG 质量 1..=100(启用时也是 AVIF 质量) |
jpeg_chroma_subsampling |
true | JPEG 4:2:0 色度子采样(false = 4:4:4) |
png_compression |
Default | PNG zlib 档位:None / Fast / Default / Best |
png_interlaced |
false | 写 Adam7 隔行 PNG |
webp_lossless |
true | WebP 无损(VP8L)或有损 |
webp_quality |
80 | 有损 WebP 质量 0..=100(照片典型 75–85) |
tiff_compression |
Deflate | TIFF strip 压缩:None / Lzw / Deflate / PackBits |
tiff_bigtiff |
false | 始终写 BigTIFF;否则仅在可能超过 4 GiB 时 |
exr_compression |
Zip16 | EXR 无损压缩:None / Rle / Zip1 / Zip16 / Piz |
embed_icc |
true | 格式支持时嵌入 ICC profile |
embed_metadata |
true | 格式支持时嵌入 EXIF/XMP/DPI/text |
FidelityWarning 的完整清单见 crates/codecs/src/fidelity.rs,其中 WriteUnsupported 与 DimensionsExceeded 是致命警告(is_fatal() 为 true),会让编码直接失败;其余都是告知性的。所有警告都实现了 Display,可直接面向用户展示,例如“16-bit will be reduced to 8-bit”“HDR values outside 0..1 will be clipped”“CMYK will be converted to RGB (not colour-managed)”。
七、测试策略:用测试锁死设计承诺
cargo test -p photocraft-codecs 覆盖(README 原列):
- 完整往返矩阵:每个可写格式 × 4 种采样类型 × 6 种布局;无损格式要求精确一致,有损格式要求高于 PSNR 阈值。
- 逐格式元数据保留:对照
caps断言。 - 魔数检测,包括反例(误判防护)。
- 保真警告,并交叉校验“警告与实际往返结果一致”(这正是“诚实”承诺的自动化验证)。
- 对称能力保证。
- 畸形输入:截断、位翻转与 proptest 随机字节。
- Adam7 PNG 对照渐进式 PNG 与
imagecrate 作为 oracle。 - 限制强制(limit enforcement)。
如果仓库根目录存在 corpus/pngsuite/*.png,其中每个文件都会与 image crate 的解码器逐像素比对;不存在时该测试自动跳过。feature 对应的真实语料测试还可通过 cargo xtask test-corpus 开启(crates/codecs/Cargo.toml)。
八、工程约束一览
源码层面对安全性的要求相当严格,crates/codecs/src/lib.rs 顶部即声明:
#![forbid(unsafe_code)]
#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)]
即全 crate 禁止 unsafe、禁止 unwrap/expect/panic 等可能在畸形输入上触发崩溃的写法——解码器面对任何输入都返回 CodecError::Malformed 而不是 panic(错误类型定义见 crates/codecs/src/error.rs)。配合 #![deny(...)] 的 clippy 规则,这从编译期就杜绝了“错误像素”与“意外 panic”两大类缺陷。
九、上层如何消费这个 crate
photocraft-codecs 是独立的“平面图像编解码”基础设施,上层通过它获得统一的分辨率、深度、色彩空间与元数据语义,再交给后续处理管线。值得注意的衔接点:
- 相机原始文件被此处拒绝后,由
photocraft-raw解码显影,photocraft-io负责路由; - TIFF 的 Photoshop 资源块(tag 34377)与图层数据(tag 37724
ImageSourceData)在此处保持不透明传递,由photocraft-io经photocraft-psd读写(见Metadata中photoshop_resources/photoshop_layers字段,crates/codecs/src/image.rs); - 色彩管理转换(CMYK↔RGB、灰度等)在
photocraft-color中做,本 crate 只提供朴素公式兜底。
这套“对称 + 深度保持 + 诚实警告 + 严格限制 + 无 unsafe”的组合,正是 PhotoCraft 把 Photoshop 级图像处理带到纯 Rust 世界时,在文件 I/O 边界上给出的工程答案。