LightCraft 纯 Rust 编解码层 lightcraft-codecs 深度解析:格式嗅探、线性光解码、ICC 色彩管理与带元数据嵌入的编码器

原创2026-10-08 12:35:411,644 阅读

LightCraft 纯 Rust 编解码层 lightcraft-codecs 深度解析:格式嗅探、线性光解码、ICC 色彩管理与带元数据嵌入的编码器

lightcraft-codecs 是 LightCraft(一个用纯 Rust 从零实现的 Lightroom 式摄影工具)的 L1 层标准图像编解码库,覆盖格式嗅探、解码到线性光(linear light)、色彩管理、缩略图快速路径,以及可嵌入 ICC/EXIF/XMP 的编码器。它全部由纯 Rust 代码构成、无 C 依赖、可构建到 wasm32-unknown-unknown,且对畸形输入保证不 panic。读完本文,你将掌握该库的完整 API 形态(sniff/decode/to_working/decode_thumbnail/encode_*)、各格式的编解码能力边界与颜色解释机制,并能在自己的管线中安全地做解码路由、缩略图加速和导出嵌入。

定位与核心设计原则

从 crates/codecs/README.md 与 crates/codecs/src/lib.rs 的模块文档看,该 crate 的设计目标可以归纳为四点:

  • 纯 Rust、可移植:Cargo.toml 中除 ravif(AVIF 编码)被限定在 [target.'cfg(not(target_arch = "wasm32"))'.dependencies] 下外,其余依赖均可用于 wasm;
  • 输出语义统一:所有解码器输出线性光的 Rgb32f,处于"源色域"(由 SourceSpace 描述),而非显示编码的 sRGB,为上层 develop 管线(工作空间为线性 Rec.2020 D65)服务;
  • 元数据无损透传:EXIF/XMP/ICC 原始 blob 原样带出,EXIF Orientation 只报告、不施加(lib.rs 中 Decoded::orientation 的注释明确写着 "Not applied");
  • 对畸形输入健壮:decode 用 std::panic::catch_unwind 兜住第三方解码器内部可能出现的 panic,统一转成 Error::Malformed;lib.rs 顶部用 #![forbid(unsafe_code)] 和一组 deny(clippy::…) lint 把"不 panic"做成可验证的编译期约束。

错误类型 Error 共五类:UnknownFormat、Unsupported(Format, &str)(如 AVIF/HEIF 解码、相机 RAW)、Malformed、TooLarge(超过 max_pixels)、Encode(lib.rs 中的 enum Error)。

API 总览:从字节到工作空间再写回文件

crates/codecs/README.md 给出的 "API at a glance" 是该库最短的完整工作流,原文示例为:

let fmt: Option<Format> = sniff(&bytes);                 // JPEG/PNG/TIFF/WebP/AVIF/HEIF/JXL/GIF/BMP/PSD/raw
let d: Decoded = decode(&bytes, DecodeOptions::default())?;
// d.image: Rgb32f, linear light in the source primaries (d.space: SourceSpace)
// d.alpha, d.bit_depth, d.float, d.orientation (EXIF value, NOT applied), d.exif/xmp/icc (raw blobs)
let working: Rgb32f = to_working(&d);                    // linear Rec.2020 D65 (Bradford)
let fast = decode(&bytes, DecodeOptions::fit(2048, 2048))?; // JPEG: DCT-domain 1/2..1/8 scaling
let t: Thumbnail = decode_thumbnail(&bytes, 256)?;       // embedded EXIF/MPF preview or scaled decode

let icc = icc::write_named(NamedSpace::DisplayP3);       // or icc::write_matrix_trc(&space, &Trc::…)
let meta = EncodeMeta { icc: Some(&icc), exif: Some(&exif), xmp: Some(&xmp) };
encode_jpeg(&EncodeImage::rgba8(&img), 90, ChromaSubsampling::S444, &meta)?;
encode_png(&EncodeImage::new(w, h, 4, Samples::U16(&px)), &meta)?;
encode_tiff(&EncodeImage::new(w, h, 3, Samples::F32(&px)), TiffCompression::Deflate, &meta)?;
encode_webp_lossless(&img, &meta)?;
encode_avif(&img, 70, 6, &meta)?;                        // native + feature `avif`

结合 src/lib.rs 的签名,可以把关键结构补全为:

DecodeOptions(lib.rs):

字段 默认值 说明
max_size: Option<(u32, u32)> None 结果框大小(绝不上采样)。JPEG 走 DCT 域 1/2、1/4、1/8 缩放后再做线性光重采样;其他格式按源分辨率全解后重采样
max_pixels: u64 1 << 30(1 Gpx) 超过该像素数的源图直接返回 Error::TooLarge

Decoded 除 image: Rgb32f 外还带 alpha(straight 未预乘,0..1)、space: SourceSpace、bit_depth(8/16/32…)、float(源文件为浮点)、grayscale/cmyk 标志、orientation(1..=8,未施加)、exif/xmp/icc 原始 blob,以及 width/height 与 source_width/source_height(缩放解码时两者不同)。Decoded 还提供了两个便捷出口:to_working()(转线性 Rec.2020 D65)与 to_srgb8()(显示编码 8-bit sRGB + alpha,色域裁剪)。

RAW 路由:sniff 对 Format::RawTiffLike(DNG、CR2、NEF、ARW、PEF、ORF、RW2、SRW 等 TIFF 结构 RAW,src/sniff.rs 的注释还列了 3FR、IIQ、ERF)与 Format::RawOther(CR3、RAF、CRW、MRW、X3F)只负责识别;decode 对它们返回 Error::Unsupported,由引擎转交 lightcraft-raw 处理(lib.rs 中 decode_unguarded 的 RawTiffLike | RawOther 分支)。

格式嗅探:magic 号、BMFF brand 与 RAW 启发式

sniff(b: &[u8]) -> Option<Format>(src/sniff.rs)只需文件头几个 KiB,按以下顺序判断:

  1. 简单 magic:FF D8 FF → JPEG;\x89PNG\r\n\x1a\n → PNG;GIF87a/GIF89a;RIFF????WEBP → WebP;FF 0A(裸 codestream)或 ISO-BMFF ftyp 前导 00 00 00 0C JXL … → JXL;8BPS + 版本 1/2 → PSD;FUJIFILMCCD-RAW/\0MRM/FOVb → 富士 RAF 等 RawOther;IIRO/IIRS/MMOR/IIU\0 → Olympus ORF / Panasonic RW2 这类私有 magic 的 RawTiffLike;
  2. BMFF(ftyp box,偏移 4..8):sniff_bmff 在主 brand 之后的 4 字符合集内依次查 crx (Canon CR3 → RawOther)、jxl 、avif/avis,以及一组 HEIC/HEIF brand(heic/heix/hevc/hevx/heim/heis/hevm/hevs/mif1/msf1);
  3. 经典 TIFF(II*\0/MM\0*):这里做最复杂的判断——tiff_is_raw 先检查偏移 8 处的 CR 签名(CR2),再打开 IFD0 做启发式:发现 DNGVersion(0xC612)、Photometric 为 CFA/LinearRaw(32803/34892)、Compression 为 raw 专用值(32767/32769/32770/34316/34713/65000/65535)即判 RAW,否则最多遍历 16 个 IFD(含 SubIFD 0x014A)。特别地,针对索尼 M/S 无损 ARW 保留 dummy CFA 但存线性 YCbCr 的怪癖,还要求同时满足 Make=SONY、存在 0x828e、SamplesPerPixel=3、BitsPerSample=6、PlanarConfig=3 的组合,避免把普通索尼 TIFF 误判为 RAW(见 src/sniff.rs 中 sony 变量附近的注释,以及 sony_linear_ycbcr_subifd_is_raw_but_ordinary_tiff_is_not 测试);
  4. BMP:BM 头且 DIB 长度属于 12/16/40/52/56/64/108/124 之一。

每个 Format 变体还自带 extension()、mime()、can_decode()(JXL 取决于 jxl feature;AVIF/HEIF/RAW 恒为 false)与 is_raw() 辅助方法。Format 的识别被 src/sniff.rs 末尾的 magic_numbers 测试用一组最小字节序列逐条覆盖,包括 II*\0\x10\0\0\0CR\x02\0 这种 CR2 样例。

解码管线:双 JPEG 解码器路由与"线性光收尾"

decode(src/lib.rs)先 sniff,再按格式分发;以 JPEG 为例(src/jpeg.rs)可以看到典型的"先扫 marker、再选解码器"两步:

  1. parse_markers 沿 SOI 之后的段(segment)走,解析 SOFn 头(宽高、分量数、位深、sof 类型、首个 scan 的分量数),并收集 EXIF(APP1 Exif\0\0)、XMP(APP1 http://ns.adobe.com/xap/1.0/\0)、ICC(APP2 ICC_PROFILE\0 多 chunk 按序拼回)、Adobe transform(APP14)、MPF(APP2 MPF\0)。遇到段间垃圾字节时按下一个 0xFF 重同步而不是直接失败;
  2. 解码器路由:常规 8-bit RGB/灰度用 zune-jpeg(最快);而 CMYK/YCCK(4 分量)、请求 DCT 缩放、12-bit(precision > 8)或非交织顺序扫描(sof 为 0xC0/0xC1 且首 scan 分量数少于总分量数,zune-jpeg 0.5 在此场景会误判)则改用 jpeg-decoder;
  3. finish 收尾(src/convert.rs)是所有解码器共享的尾段:把设备样本(U8/U16/F32)按 SourceSpace 解释并线性化(8-bit 用 256 项 LUT、16-bit 用 65536 项 LUT、float 直接求值),分离 straight alpha、对预乘 alpha 反解、从 EXIF 摘要或容器 tag 取 orientation,最后按需在线性光域做缩放(缩放因子 ≥2 用 Box 滤波,否则 Mitchell,见 fit 调用),产出 Decoded。

颜色解释的决策树在 rgb_or_gray(src/convert.rs)中清晰可见,优先级为:

  1. 有 ICC 且是 RGB/Gray matrix/TRC → 精确应用(TRC 线性化 + colorants → XYZ D50),origin = IccMatrixTrc;
  2. 有 ICC 但需要完整 CMS(LUT-based、CMYK 等)→ 走 cms_to_linear_rec2020 直接转线性 Rec.2020,origin = IccCms;
  3. 有 ICC 但解析/应用失败 → sRGB 兜底并标记 IccUnsupported(原始 blob 仍原样保留在 d.icc 中);
  4. 容器级颜色描述(PNG chunk、JXL enum、EXIF R03 Adobe RGB hint)→ Container;
  5. 无任何颜色信息 → 假定 sRGB(Untagged);无 tag 的 float 数据按惯例假定线性(trc = Linear)。

CMYK 无 profile 时走 naive 公式 RGB = (1-C)(1-K)… 并按 sRGB 曲线解释,origin = Naive(对应 README "Decoders never guess silently" 的立场:所有降级都通过 SourceSpace::origin 显式暴露,而不是静默处理)。

缩略图快速路径:嵌入预览优先,DCT 缩放兜底

decode_thumbnail(bytes, max_edge) 是网格缩略图入口,decode_thumbnail_with(bytes, &ThumbnailOptions) 提供三个可调参数(src/lib.rs):

参数 默认 说明
max_edge — 结果长边不超过该值
min_embedded_edge 等于 max_edge 接受长边 ≥ 该值的嵌入预览(按预览自身更小尺寸返回)。降到 160 可即时拿到相机常见 160px EXIF 缩略图作为占位
max_pixels 64_000_000(64 MP) 兜底解码的源图像素预算;非 JPEG 格式会先在源分辨率分配内存,所以输出尺寸不约束内存。超限返回 Error::TooLarge,但嵌入 JPEG 预览仍可在不解码全图的前提下使用

实现上(src/jpeg.rs 的 embedded_thumbnail),候选来自两处:EXIF IFD1 的 JPEGInterchangeFormat/Length 缩略图,以及 MPF(CIPA DC-007)列表中的预览(mpf_images 解析 APP2 内 TIFF 的 0xB002 条目,最多 16 条记录)。选择策略是"最小够用":预览必须与主图宽高比一致(2% 容差,防止 letterbox/crop),优先取覆盖 max_edge 的最小预览,否则取不小于 min_edge 的最大者;选中后用 decode 带 DCT 缩放再 fit 到目标尺寸。结果 Thumbnail 是 8-bit sRGB 带 alpha 的 Rgba8,附 orientation、source(ExifThumbnail/MpfPreview/Scaled 三态)与源图尺寸。

tests/roundtrip.rs 用手工构造的 EXIF/MPF 字节验证了整条路径:thumbnail_prefers_embedded_exif 断言 800×600 主图 + 200×150 EXIF 缩略图在请求 128 时走 ExifThumbnail、请求 256 时退回 Scaled;thumbnail_from_mpf_preview 验证了手工拼接 MPF APP2 段的文件能被正确路由;thumbnail_accepts_smaller_preview_when_asked 验证 min_embedded_edge: 160 时 160×120 的小预览被接受。

色彩:SourceSpace、ICC 解析与 profile 生成

README 的 "Colour" 一节与 src/space.rs 完全对应:SourceSpace 持有 named: Option<NamedSpace>、to_xyz_d50: Mat3(线性源 RGB → CIE XYZ,相对 D50 即 ICC PCS)、trc: Option<[Trc; 3]> 和 origin: SpaceOrigin;to_space(dst) 在未知命名色域时用 dst.from_xyz() × Bradford(D50→dst白点) × to_xyz_d50 合成矩阵。Trc 枚举覆盖 ICC curv/para 的全部形态(Linear/Srgb/Gamma(g)/Rec709/Parametric(1|3|4|5|7 参数)/Table(u16 表)),负值做镜像、>1 做外推以兼容扩展范围 float 数据,from_linear 对表型曲线用二分求逆(src/space.rs 中 trc_inverse 测试验证了正反向误差 < 2e-3)。

NamedSpace 只收录五个:sRGB、Display P3、Adobe RGB (1998)、ProPhoto (ROMM)、Rec.2020,通过 D50 色品矩阵识别(recognize 容差 0.004,容忍 s15Fixed16 舍入与厂商差异)。

ICC 侧(src/icc.rs)基于 moxcms:

  • parse 解析 v2/v4 profile,curv/para TRC 与 chad 都支持;RGB matrix-shaper(PCS 为 XYZ)提取为 IccKind::MatrixTrc { to_xyz_d50, trc },灰度 profile 提取 IccKind::Gray,其余(LUT/A2B、CMYK)归 NeedsCms;
  • cms_to_linear_rec2020 用完整 CMS 变换把设备样本直接送进"线性 Rec.2020"目标 profile,分块(每块 65536 行)限内存,parallel feature 下走 rayon 并行,整体包在 catch_unwind 里;测试 cms_path_matches_matrix_path 断言 CMS 路径与 matrix 路径结果一致到 2e-3;
  • write_matrix_trc(&space, &trc) 生成 v4 display-class profile(D50 PCS、Bradford chad、Perceptual intent),write_named(NamedSpace) 是其标准曲线快捷形式,用于导出时嵌入;
  • 一个容易忽略的细节是 align_tags:moxcms 编码时把 tag 数据"贴回"打包,奇数长度 tag(如字符数为奇的 mluc 描述)会使后续所有 tag 错位,违反 ICC.1 §7.3.1 的 4 字节对齐,某些阅读器(如 macOS ImageIO 读 PNG iCCP)会因此拒收;该函数把 tag 重排到 4 字节边界并清零 profile ID(MD5)以表示"未计算",tags_are_aligned 测试逐 tag 校验偏移均为 4 的倍数。

编码器:统一的 EncodeImage/EncodeMeta 与每格式限制

编码入口在 src/encode.rs。输入是 EncodeImage { width, height, channels: 1..4, samples },Samples 可为 U8/U16/F32(整数样本按显示编码 0..=MAX 理解,float 按原值写入);EncodeMeta 携带 icc: Option<&[u8]>、exif: Option<&[u8]>(TIFF 结构、从 II/MM 开始、不含 Exif\0\0 前缀)、xmp: Option<&str> 和 ppi: Option<u16>(打印分辨率,落到 JPEG JFIF density / PNG pHYs / TIFF X/YResolution,见 tests/roundtrip.rs 的 print_resolution_is_written:300 ppi 时 PNG pHYs 为 11811 px/m、单位 metre)。

各格式要点:

  • JPEG(encode_jpeg(img, quality 1..=100, ChromaSubsampling, meta)):仅 8-bit,alpha 丢弃;宽高 ≤ 65535。4:4:4/4:2:0 走自研的并行编码器 src/jpeg_par.rs(全核 restart-interval 分带),4:2:2 走 jpeg-encoder 顺序编码。元数据分段:JFIF APP0(含 ppi)、EXIF APP1(> 65527 字节即报错,因为单个 APP1 段上限 64 KiB)、XMP APP1(拼接 XMP 命名空间前缀后 > 65533 报错,不支持 extended XMP)、ICC APP2 按 ICC.1 Annex B.4 拆 ≤ 65519 字节的 chunk(ICC_PROFILE\0 + 1 基序号 + chunk 总数,最多 255 段)。代码注释明确指出:jpeg-encoder 的优化表模式会写出非交织 scan,部分解码器(含 zune-jpeg 0.5)处理不好,因此编码器固定使用标准 Huffman 表的交织 baseline 换取最大兼容性——README 的 "Gaps" 一节同时声明不支持算术编码。
  • PNG(encode_png):8/16-bit、Gray/GA/RGB/RGBA;float 输入直接拒绝(需先量化);嵌 iCCP、eXIf、iTXt XMP(XML:com.adobe.xmp);压缩级别固定 Fast。
  • TIFF(encode_tiff):8/16-bit 整数或 32-bit float;TiffCompression 为 None/Lzw/Deflate(默认,DeflateLevel::Fast)/PackBits;整数 + LZW/Deflate 时自动加 Predictor::Horizontal;嵌 ICC(tag 34675)与 XMP(tag 700),不写 EXIF——README 与源码注释一致地建议用 lightcraft-tiff 的 writer 处理 EXIF;灰 + alpha 因编码器无 GrayA 色彩类型而展开为 RGBA 并写 ExtraSamples = [2](unassociated)。
  • WebP(encode_webp_lossless):仅无损、8-bit;ICC/EXIF/XMP 走扩展(VP8X)容器。
  • AVIF(encode_avif(img, quality, speed 1..=10, meta)):ravif/rav1e,仅 native 目标 + avif feature(默认开启,wasm32 上是 no-op,返回 Error::Encode);8-bit 输入按 4:4:4、10-bit AV1 编码;16-bit 输入先在 crate 内手工转 BT.601 full-range 10-bit YCbCr 平面(avif_10bit,保留 10 bit 精度);EXIF 可嵌入,ICC 不支持(muxer 用 nclx 标 sRGB);float 输入报错。

格式支持矩阵

完整继承 crates/codecs/README.md 的 "Formats" 表:

格式 解码 编码 备注
JPEG 是(zune-jpeg;jpeg-decoder 用于 DCT 缩放、CMYK/YCCK、12-bit、非交织 scan) 是(jpeg-encoder,baseline,4:4:4/4:2:2/4:2:0) EXIF(APP1)、XMP(APP1,≤ 64 KiB)、ICC(APP2 多 chunk)、Adobe APP14、MPF 预览
PNG 是,1–16-bit、调色板、tRNS、隔行 8/16-bit,gray/GA/RGB/RGBA iCCP、eXIf、iTXt XMP;无 ICC 时遵从 sRGB/cICP/cHRM/gAMA
TIFF 8/16-bit 整型(32/64-bit 整型截到 16)、16/32/64-bit float、gray/RGB/CMYK/palette、alpha(关联/非关联)、planar、LZW/Deflate/PackBits/JPEG 8/16-bit、32-bit float;none/LZW/Deflate/PackBits ICC(34675)、XMP(700)、Orientation;仅首个 IFD;不写 EXIF(用 lightcraft-tiff)
WebP 有损 + 无损、alpha、动画首帧 仅无损 ICCP/EXIF/XMP chunk
GIF / BMP 是(GIF 首帧) — 经 image
PSD / PSB 合并合成图:8/16/32-bit gray、RGB、CMYK、indexed、duotone(按 gray);raw/RLE — ICC(1039)、EXIF(1058)、XMP(1060);不支持 ZIP 压缩合成图、Lab 与 1-bit
JPEG XL 是(jxl-oxide,feature jxl,默认开) — enum 色彩直接渲染到线性 Rec.2020;ICC 走本库 ICC 路径;orientation 由解码器施加(报告为 1)
AVIF 否 是(ravif/rav1e,仅 native,feature avif) 8-bit sRGB、EXIF;muxer 不支持 ICC
HEIC/HEIF 否(仅嗅探) — 见下文限制

依赖与许可证策略

README 列出全部依赖均为宽松许可(README 称已用 cargo info / cargo tree 核验):

Crate 许可证 用途
zune-jpeg, zune-core MIT OR Apache-2.0 OR Zlib JPEG 解码
jpeg-decoder MIT OR Apache-2.0 缩放/CMYK JPEG 解码
jpeg-encoder (MIT OR Apache-2.0) AND IJG JPEG 编码(IJG:宽松许可,文档保留署名)
png MIT OR Apache-2.0 PNG
tiff MIT TIFF
image-webp MIT OR Apache-2.0 WebP
image(仅 gif, bmp) MIT OR Apache-2.0 GIF/BMP
moxcms BSD-3-Clause OR Apache-2.0 ICC 解析/写入、CMS
jxl-oxide MIT OR Apache-2.0 JPEG XL 解码
ravif(+ rav1e, avif-serialize) BSD-3-Clause / BSD-2-Clause AVIF 编码(仅 native;编译较重)

README 还明确记录了刻意不采用的部分:imazen 系列的 AGPL 组件(heic、zenjpeg、jxl-encoder、rav1d-safe)、GPL/C 的 jpegxl-rs / imagequant、C 的 dav1d / libheif——这是该 crate 能保持"纯 Rust + 宽松许可 + 可编译进分发产物"的前提。

性能基准与复现方式

README 给出的数字来自 Apple M 系列、release 构建、合成 24 MP JPEG(q90、4:2:0):

操作 耗时
全量解码 → 线性 Rgb32f ~92 ms(≈ 260 MP/s)
decode fit 2048(DCT 1/2) ~81 ms
256 缩略图,缩放解码(DCT 1/8) ~54 ms(熵解码瓶颈)
256 缩略图,来自嵌入 EXIF/MPF 预览 ~0.6 ms
to_working 矩阵 ~23 ms
PNG / TIFF-deflate 编码(fast 级别) ~430 ms / ~900 ms

注意这些是 README 声明的合成基准,非真实照片数据;要自行复现,运行 tests/bench.rs 顶部的命令:

cargo test --release -p lightcraft-codecs --test bench -- --ignored --nocapture

该基准生成 6000×4000 的合成图(平滑渐变 + 确定性噪声模拟照片熵),依次测量编码、全量解码、fit 2048、无预览/带 320×213 EXIF 预览的缩略图、to_working 与 PNG/TIFF 编码,并断言相机型文件的缩略图确实走了 ExifThumbnail 路径。

已知限制(Gaps / limitations)

继承 README 的 "Gaps / limitations",结合源码逐条说明:

  1. AVIF 与 HEIC 解码:当前不存在宽松许可的纯 Rust AV1/HEVC 解码器(dav1d/libheif 是 C,rav1d-safe 与 imazen heic 是 AGPL);两类文件可被嗅探并以 Error::Unsupported 拒绝,README 指出未来可为 HEIC 增加"RAW 式嵌入预览"的缩略图路径;
  2. JPEG:算术编码不受支持(两个解码器皆然);扩展 XMP(> 64 KiB)不读不写;EXIF > 64 KiB 不写;编码器固定标准 Huffman 表(交织 baseline)以保兼容;
  3. 缩略图延迟下限:< 15 ms 的缩略图需要嵌入预览长边 ≥ 请求边长(EXIF IFD1 或 MPF);多数相机只存 160px EXIF 缩略图,此时用 decode_thumbnail_with 配 min_embedded_edge: 160 拿即时占位,否则 24 MP 文件走 DCT 缩放路径约 50 ms;
  4. TIFF:仅解码首张图像;编码时不重新输出 EXIF;不支持 Lab / 未压缩 YCbCr;
  5. PSD:仅合并合成图(无图层);不支持 ZIP 压缩合成图;
  6. wasm32 健壮性:unwinding 目标上第三方解码器内部 panic 会被 catch_unwind 捕获;wasm32(panic = abort)则依赖本 crate 自身代码的属性测试(proptest)与解码器自身的加固。

验证方式:roundtrip、corpus 与 fuzz 测试

该 crate 的测试布局(crates/codecs/tests/)值得参照,因为它把"可验证性"作为功能的一部分:

  • tests/roundtrip.rs:每个格式"编码 → 解码"闭环。例如 jpeg_roundtrip_with_metadata 断言三种 subsampling 下 EXIF/XMP/ICC 字节级原样往返、orientation=6 只透传不施加、PSNR > 32 dB;png_8_and_16_bit_exact 验证 16-bit PNG 线性化后相邻色阶(30000 vs 30001)仍严格保持顺序;p3_icc_to_working 用手工计算的 DISPLAY_P3.to_space(&REC2020) 矩阵核对 to_working 结果(< 1e-3);unusable_icc_falls_back_to_srgb_with_flag 验证坏 ICC 时 IccUnsupported 兜底且原始 blob 保留;tiff_depths_and_compressions 覆盖 4 种压缩 × 8/16-bit/float 深度,float 连超范围值都精确往返;
  • tests/corpus.rs 与 tests/fuzz.rs:面向真实语料和属性测试(proptest)的健壮性验证,对应 lib.rs 顶部"任何解码器不对畸形输入 panic"的承诺;
  • tests/bench.rs:前文所述的可复现基准。

此外 examples/jpeg_bench.rs 提供独立的 JPEG 编码/解码示例程序,便于脱离测试框架观察行为。

小结

lightcraft-codecs 把"标准图像格式进出 LightCraft 管线"这一层做成了职责单一的 L1 库:sniff 用纯字节启发式(含 BMFF brand 与 TIFF raw 深度检查)完成识别与 RAW 路由;decode 统一产出源色域线性光 Rgb32f 并透传全部元数据;SourceSpace::origin 把每次颜色解释(精确 ICC 矩阵、CMS 兜底、容器 hint、sRGB 假定、降级)显式化而非静默猜测;decode_thumbnail 用"嵌入预览优先、DCT 缩放兜底、像素预算可配"的策略把缩略图成本从几十毫秒压到亚毫秒;编码器则以统一的 EncodeImage/EncodeMeta 覆盖 JPEG/PNG/TIFF/无损 WebP/AVIF 并严格按各格式规范嵌入 ICC/EXIF/XMP/ppi。其取舍同样清晰:以纯 Rust、宽松许可为硬约束,暂不覆盖 AVIF/HEIC 解码,JPEG 算术编码与扩展 XMP 明确放弃,并在 README 中逐条声明——这些边界对评估该 crate 是否适用于你自己的管线同样是重要的选型依据。

登录后查看全文
lightcraft