Photocraft 光栅格式体系:photocraft-codecs 的格式检测、能力声明、解码限额与分层 TIFF/WebP 实现

原创2026-10-08 17:11:301,894 阅读
文章标签:图像处理桌面应用

Photocraft 光栅格式体系:photocraft-codecs 的格式检测、能力声明、解码限额与分层 TIFF/WebP 实现

本篇基于 book/src/formats/raster-formats.md 讲解 Photocraft(纯 Rust 实现的 Photoshop 兼容编辑器)中 photocraft-codecs 这一光栅格式层:格式检测与能力声明、解压炸弹防护的解码限额、Photoshop 分层 TIFF 的私有标签透传机制,以及自研 VP8 编码器背后的有损 WebP 写入原理。读完后你能掌握:各格式读/写能力与元数据支持的查询方式、DecodeOptions/EncodeOptions 的参数含义与默认值,以及 CLI 与 MCP 命令中如何控制 TIFF 分层导出。

格式层总览:检测、能力与限额都由 photocraft-codecs 负责

photocraft-codecs 为平面(无图层)光栅图像提供格式检测、能力声明、解码限额、元数据处理和编码。默认构建声明了对 PNG、JPEG、TIFF、WebP、GIF、BMP、TGA、ICO、Netpbm/PFM、QOI、OpenEXR、Radiance HDR 的读写支持。两个 feature 门控的例外:

  • HEIF/HEIC(iPhone 与 Mac 照片)只读,由纯 Rust 的 heic-rs 通过可选的 photocraft-heif crate 解码。这是构建期选择,位于 cargo feature heif 之后(cargo build -p photocraft --features heif):官方构建包含它;未启用时 HEIC 文件仍会被识别出来,打开时会报告“该构建未包含 HEIC 支持”,而不是识别失败。
  • AVIF 编码可选,解码未实现:编码走纯 Rust 的 ravif,解码需要 C 编写的 dav1d,因此默认构建中 AVIF 既不可读也不可写(写能力由非默认 feature avif 开启)。

从 crates/codecs/src/format.rs 的源码可以验证这些声明:Format::ALL 列出全部 14 种格式;caps(format) 返回每个格式的 FormatCaps(读/写、原生位深、原生通道布局、alpha、ICC、EXIF、XMP、DPI、文本、动画、是否有损);ASYMMETRIC_EXCEPTIONS 常量以注释形式记录了 AVIF 与 HEIF 这两个“启用但故意不对称”的例外及原因,测试套件会断言其余所有启用格式都同时可读可写。文档同时提醒一句关键原则:不要仅凭文件扩展名推断保真度,样本深度、通道布局、alpha、ICC/EXIF/XMP、DPI 与动画行为都应以 format.rs 中的能力声明为准。

格式识别基于魔数,实现位于 detect()(crates/codecs/src/format.rs):PNG 的 \x89PNG\r\n\x1a\n、JPEG 的 FF D8 FF、TIFF 的 II*\0/MM\0*/II+\0/MM\0+(BigTIFF)、RIFF+WEBP、GIF87a/GIF89a、QOI 的 qoif、EXR 的 v2/1/1、#?RADIANCE/#?RGBE、ISO-BMFF 容器则按 ftyp brand 区分 AVIF(avif/avis)与 HEIF(heic、heix 等)。没有魔数的 TGA 在最后按头部合理性(图像类型、色深、色表一致性)兜底识别。from_extension() 则提供按扩展名(含 .jfif、.icb、.pfm 等全部别名)查询格式的能力,扩展名表由 Format::extensions() 声明,第一个即写文件时的首选扩展名。

分层 TIFF:用两个私有标签携带 Photoshop 图层数据

Photoshop 保存的分层 TIFF 把图层信息放在两个私有标签里:

  • 37724(ImageSourceData):与 PSD 相同的 layer 段,前置字符串 Adobe Photoshop Document Data Block;
  • 34377:PSD 的 image resources。

photocraft-codecs 把这两段原样作为不透明字节放在 Metadata 中透传;photocraft-io 再把它们与 TIFF 的像素一起重建为 PSD 模型,交给 PSD 导入器。结果是:分组、蒙版、图层样式、文字、形状、智能对象和调整图层从 TIFF 打开时与从 PSD 打开完全一致,写回也走同一条路径。

从 crates/psd/src/tiff.rs 可以看到字节序转换的实现:Intel 序(II)文件中 Photoshop 会把整个块结构做字节翻转(键名变成 MIB8、ryaL 这样的反转形式,整型与浮点均为小端),而像素样本保持大端。photocraft-psd 的 tiff 模块在两种字节序之间逐字段转换,ImageSourceData::from_bytes 把任一字节的输入归一化为大端 PSD 模型;布局未知的块会被丢弃并给出警告,而不是把字节翻转过的数据交给其他读取器。对应测试见 crates/codecs/tests/tiff_photoshop.rs(四种布局 × 四种压缩下的标签往返、手工构造的双字节序/BigTIFF 文件、任意截断与位翻转下的健壮性),模糊测试入口在 crates/psd/fuzz/fuzz_targets/image_source_data.rs。

文档也明确了分层 TIFF 的限制与触发方式:

  • 一个 TIFF 只能容纳一个透明度样本,因此多余的 alpha 通道不会被写入;Lab 文档按平面(无图层)保存。
  • 交互保存时,若偏好设置「文件处理」中 Ask Before Saving Layered TIFF Files 开启,Save As 会询问“包含图层会增大文件体积”(备选“丢弃图层并另存副本”)。
  • 脚本与 Agent 路径默认写平面 TIFF,除非显式要求图层:photocraft-cli 的 convert、run、batch 加 --tiff-layers 参数(见 apps/photocraft-cli/src/lib.rs 中三条子命令的参数表),或 save/batch/export 命令及 MCP doc_save 中传 "tiffLayers": true。

测试 apps/photocraft-cli/tests/cli.rs 中的 tiff_output_is_flat_unless_tiff_layers_is_given 验证了这一点:不带 --tiff-layers 输出的 TIFF 只有 1 层,带上后为 2 层。

WebP:无损用现成库,有损是自研的 RFC 6386 VP8 编码器

WebP 两条编码路径分工明确(实现在 crates/codecs/src/codecs/webp.rs):

  • 无损(VP8L):由 image-webp 编解码;
  • 有损:由 PhotoCraft 自己的 VP8 关键帧编码器写入,位于 crates/codecs/src/codecs/vp8/,是对 RFC 6386 的 clean-room 实现:
    • 输入先做 BT.601 4:2:0 转换(色度按 2×2 块平均,见 Yuv::from_rgb);
    • 16×16 宏块内,亮度在 4 种整块模式与 16 种 4×4 子块模式(B_PRED)之间按重构误差选择,色度用 4 种模式之一;
    • 前向 DCT 与亮度 DC 的 Walsh-Hadamard 变换,配位精确的逆变换,保证重建与解码器看到的完全一致;
    • 量化使用 RFC 的 dc_qlookup/ac_qlookup 表;token 编码采用按帧更新概率的方式(先统计本帧 token 分支,仅在估计值明显降低编码代价时才写入更新);
    • 无系数的宏块标记为 skip,平坦区域几乎零成本。

关键参数在 crates/codecs/src/options.rs 的 EncodeOptions 中:

字段 含义 默认值
webp_lossless true 走 VP8L,false 走自研 VP8 true
webp_quality 有损质量 0–100,采用 libwebp 的量表 80

webp_quality 通过 quantizer_index()(crates/codecs/src/codecs/vp8/mod.rs)映射为 VP8 量化器索引 0–127:曲线是温和的幂函数,quality 90 ≈ 11、75 ≈ 28、50 ≈ 60、25 ≈ 92。有损文件的最大边长受 VP8 帧头 14 位限制,为 MAX_DIMENSION = 16383;Format::WebP::max_dimensions() 在编码入口即按 16384×16384 拦截越界。

有损输出的容器结构由 riff() 组织:无附加数据时写裸 VP8 chunk;有 alpha 或元数据时写扩展形式 VP8X(flag 位:ICC 0x20、ALPHA 0x10、EXIF 0x08、XMP 0x04),alpha 以无损形式放在 ALPH chunk(仅当存在半透明像素时),ICC/EXIF/XMP 各有独立 chunk。解码侧应用该帧的环路滤波;整条链路不涉及任何 C 库。往返与保真度由 crates/codecs/tests/webp_lossy.rs 覆盖。

文档导出:统一的分层压平规则

所有把文档写入平面格式的路径(Save As、CLI、MCP doc_export、Layers to Files、Layer › Export As)都使用同一套压平规则:

  • 不支持透明的格式(JPEG、Radiance HDR):图像合成到白色背景上,与 Export As / Quick Export 行为一致;CMYK 文档中“白色”意味着没有墨。
  • 不能内嵌 ICC 配置文件的格式(GIF、BMP、TGA、QOI、ICO、Netpbm/PFM):来自其他色彩空间(linear light、Display P3 等)的 RGB 文档会通过颜色引擎以感知意图转换为 sRGB——因为无标签文件会被读取方当作 sRGB 解释。OpenEXR 与 Radiance HDR 则转换为 linear sRGB。

已实现的解码限额:在分配像素之前拦住炸弹

DecodeOptions::default() 会在分配解码像素缓冲之前用 Limits 校验头部声明的尺寸:

限额 默认值
最大宽度 262,144 像素
最大高度 262,144 像素
最大像素数 268,435,456 像素(2^30)
最大解码分配 2 GiB(32 位目标,如 wasm;64 位目标为 8 GiB,见下文)

crates/codecs/src/options.rs 中的 Limits 结构包含四个字段 max_width、max_height、max_pixels(width*height 上限)与 max_alloc(解码像素缓冲字节上限,并作为分配预算传给底层解码器)。默认值由 Default 实现给出:宽高各 2^18、像素 2^30,max_alloc 在 64 位目标上为 8 GiB、32 位目标上为 2 GiB——注释说明这一尺度是为了容得下 Photoshop 用户常见的大文档(例如 20000² 的 RGBA 16 位图即 3.2 GB)。各格式适配器把相容的分配限额传给 PNG、TIFF、WebP 与 image 系解码器,其余实现自行做显式的尺寸/缓冲校验;任一违反都返回 CodecError::LimitExceeded。

Limits::none() 供有明确理由放宽策略的调用方使用:它把四项上限都设为类型最大值,但仍受 usize 溢出检查约束(见 check_bytes() 中 usize::try_from(bytes) 的兜底),文档明确提示不应用于不受信任的输入。

测试(crates/codecs/tests/limits.rs)覆盖了:对每种可读写格式强制 max_pixels/max_width/max_height/max_alloc;边界值恰好等于限额时放行;Limits::none() 全格式放行;以及手工构造的“炸弹头部”——100000×100000 的 PNG IHDR、200000×200000 的 PPM、声明 65535×65535 的 JPEG SOF0、1000000×1000000 的 TIFF——全部在分配前被拒绝。解码完成路径上还有二次防线:crates/codecs/src/lib.rs 的 finish_decode() 在方向(orientation)处理前后都会重跑 Limits::check,因为方向 5–8 会交换宽高,非对称限额下原本放得下的图旋转后可能放不下。

多帧、多页与截断文件:警告而不是静默失败

当前的 codec API 把动画容器与多页 TIFF 表示为单个解码帧/页:打开时会附带一条导入警告,例如“only the first of 3 frames was imported”(内部对应 Image::warnings 中的 DecodeWarning::MoreFrames/MorePages,帧数不逐帧解码即可统计出总数)。TIFF 的任意一页仍可用 decode_tiff_page(以及 photocraft_io::import_tiff_page)单独打开,tiff_info 可列出主 IFD 链与 SubIFD 的每一页。

截断的 JPEG 则按宽容策略处理:文件在图像数据中途被截断时,带“数据提前结束”的警告打开(基线 JPEG 缺失的行以灰色补齐);如果文件在扫描数据开始之前就结束(即根本没有图像数据),则直接报错。更多格式级的细节——包括 HEIF 容器的 irot/imir/clap 应用、TIFF 的分带并行解码、EXR 通道按后缀匹配等——见 crates/codecs/README.md 与 format.rs 中的源码能力声明。

登录后查看全文
photocraft