纯 MoonBit 实现的 ICC 色彩管理引擎:解析 ICC profile 并执行真实色空间变换
Dependencies

| 已有 | 缺什么 |
|---|---|
| 色彩空间数值转换(sRGB↔HSL↔XYZ) | 按 ICC profile 做设备相关变换 |
| 能解析 ICC profile 元数据的库 | 用 profile 做实际色变换 |
| 图像编解码 | 图像进入/离开色彩工作流时的色彩管理环节 |
moon add qqq66668/moon_iccimport {
"qqq66668/moon_icc",
}let data = @fs.read_file_to_bytes("sRGB Color Space Profile.icc")
let header = @moon_icc.parse_header(data)
println(header.device_class) // "mntr"
println(header.color_space) // "RGB "
println(header.pcs) // "XYZ "
println(header.date_time.to_iso_string()) // "1998-02-09T06:49:00"
let tags = @moon_icc.parse_tag_table(data, header)
for tag in tags {
println("\{tag.signature} 偏移=\{tag.offset} 长度=\{tag.size}")
}let data = @fs.read_file_to_bytes("sRGB Color Space Profile.icc")
let profile = @moon_icc.Profile::parse(data)
let transform = profile.matrix_trc()
// sRGB 中灰 (0.5, 0.5, 0.5) → PCS XYZ
let xyz = transform.to_pcs(0.5, 0.5, 0.5)
// xyz = (0.20640003241890215, 0.21403939749252843, 0.17660683523992873)
// 再转到 Lab
let lab = @moon_icc.xyz_to_lab(xyz)
println(lab.l) // 53.585...let src = @moon_icc.Profile::parse(@fs.read_file_to_bytes("sRGB Color Space Profile.icc"))
let dst = @moon_icc.Profile::parse(@fs.read_file_to_bytes("display.icc"))
// 媒体相对比色意图
let link = @moon_icc.ColorTransform::new(src, dst)
let (r, g, b) = link.transform(0.5, 0.5, 0.5)let rgb = @moon_icc.Profile::parse(@fs.read_file_to_bytes("sRGB Color Space Profile.icc"))
let cmyk = @moon_icc.Profile::parse(@fs.read_file_to_bytes("printer.icc"))
let link = @moon_icc.ColorTransform::new(rgb, cmyk)
// 源 3 通道 → 目标 4 通道,用通道数无关的 transform_values
let out = link.transform_values([ 0.5, 0.5, 0.5 ]) // 4 个 CMYK 分量moon run cmd/main --target wasm-gc -- link testdata/srgb.icc testdata/display.icc
moon run cmd/main --target wasm-gc -- link testdata/srgb.icc testdata/cmyk_printer.iccsrc_channels 3
dst_channels 3
link 0 0 0 0
link 1 9 9 9
...src_channels 3
dst_channels 4
link 0 0 0 0 255
link 1 1 2 1 255
...源设备值 → PCS → 打样设备值 → PCS → 显示设备值
A2B B2A A2B B2Alet chain = @moon_icc.ColorTransform::proofing(
@moon_icc.Profile::parse(@fs.read_file_to_bytes("sRGB Color Space Profile.icc")),
@moon_icc.Profile::parse(@fs.read_file_to_bytes("printer.icc")), // 打样:CMYK 印刷机
@moon_icc.Profile::parse(@fs.read_file_to_bytes("display.icc")), // 显示设备
1, // 源/显示侧意图:媒体相对比色
1, // 打样意图:也可用 3(绝对比色)
)
let (proof_values, shown) = chain.evaluate([ 0.5, 0.5, 0.5 ])
// proof_values = 印这张图会用到的 CMYK 四个分量
// shown = 屏幕上应显示的 RGBmoon run cmd/main --target wasm-gc -- proof testdata/srgb.icc testdata/cmyk_printer.icc testdata/display.iccproof_intent 1
proof_channels 4
display_channels 3
proof 0 | 0 0 0 255 | 41 41 40
proof 51 | 13 17 16 224 | 61 60 59let source = @moon_icc.Profile::parse(@fs.read_file_to_bytes("sRGB Color Space Profile.icc"))
let target = @moon_icc.Profile::parse(@fs.read_file_to_bytes("printer.icc"))
// 源设备值 → PCS XYZ(统一入口:matrix/TRC 型与 LUT 型 profile 都适用)
let xyz = source.device_to_pcs(1, [ 0.0, 1.0, 0.0 ]) // sRGB 纯绿
let de = target.round_trip_delta_e(1, xyz) // 往返 ΔE76
let inside = target.in_gamut(1, xyz) // 按自适应容差判定
println(de) // 55.83...
println(inside) // false —— SWOP CMYK 覆盖不了 sRGB 纯绿moon run cmd/main --target wasm-gc -- gamut testdata/srgb.icc testdata/cmyk_printer.icctolerance 5.591375629781349
gamut 0 0 0 0 de=12.88 out ← 纸张到不了 L*=0
gamut 1 255 255 255 de=3.42 in ← 纸白可表达
gamut 3 0 255 0 de=55.83 out ← SWOP CMYK 覆盖不了 sRGB 纯绿
gamut 12 68 68 68 de=5.19 in ← 中性灰在色域内moon run cmd/main --target wasm-gc -- parse testdata/srgb.icc
moon run cmd/main --target wasm-gc -- transform testdata/srgb.icc
moon run cmd/main --target wasm-gc -- link testdata/srgb.icc testdata/display.icc
moon run cmd/main --target wasm-gc -- link testdata/srgb.icc testdata/cmyk_printer.icc
moon run cmd/main --target wasm-gc -- link testdata/srgb.icc testdata/cmyk_printer.icc saturation
moon run cmd/main --target wasm-gc -- link testdata/srgb.icc testdata/cmyk_printer.icc absolute
moon run cmd/main --target wasm-gc -- proof testdata/srgb.icc testdata/cmyk_printer.icc testdata/display.icc
moon run cmd/main --target wasm-gc -- gamut testdata/srgb.icc testdata/cmyk_printer.icc
moon run cmd/main --target wasm-gc -- lut testdata/cmyk_printer.icc
moon run cmd/main --target wasm-gc -- lut testdata/test4.iccsize 3144
version 2.1
class mntr
colorspace RGB
pcs XYZ
datetime 1998-02-09T06:49:00
intent 0
illuminant 0.964202880859375 1 0.8249053955078125
tagcount 17
cprt 336 51
desc 388 108
wtpt 496 20
...sample 0.25 0.049063689759460294 0.050879655743278275 0.041981500061132944 ...
sample 0.5 0.20640003241890215 0.21403939749252843 0.17660683523992873 ...
sample 1 0.9642791748046875 0.999969482421875 0.8250885009765625 ...| 功能 | 状态 |
|---|---|
| ICC profile 二进制解析(头部 + tag 表,含边界校验) | ✅ 已完成并验证 |
| tag 数据解析(XYZ / curv / para) | ✅ 已完成并验证 |
| matrix/TRC 类型 profile → PCS XYZ 变换 | ✅ 已完成并验证 |
| XYZ ↔ Lab 转换与 CIE76 色差 | ✅ 已完成并验证 |
| 逆变换(PCS XYZ → 设备 RGB)与矩阵求逆 | ✅ 已完成并验证 |
| 多 profile 链接(源设备 → PCS → 目标设备) | ✅ 已完成并验证 |
| 媒体相对比色意图 | ✅ 已完成并验证 |
| CLUT 型 profile 变换(mft1 / mft2,CMYK 打印机等) | ✅ 已完成并验证 |
| 跨类型链接(matrix/TRC ↔ LUT 任意组合) | ✅ 已完成并验证 |
| 渲染意图:感知 / 媒体相对比色 / 饱和度 | ✅ 已完成并验证 |
| 渲染意图:绝对比色(PCS 白点缩放) | ✅ 已完成并验证(对 lcms2 197/200 完全一致、最大 1 级) |
| mAB / mBA(ICC v4 LUT) | ✅ 已完成并验证 |
| PCS 为 XYZ 的 LUT 型 profile | ✅ 已完成并验证(v4 样本;v2 路径无样本,见「明确不做」) |
| 软打样链(源 → 打样设备往返 → 显示器) | ✅ 已完成并验证(对 lcms2 proofing 最大偏差 1 级) |
| 色域判断(往返 ΔE76 + 自适应容差) | ✅ 已完成并验证 |
| 类型 | 名称 | 表值位深 |
|---|---|---|
| mft1 | lut8Type | 8 位(表固定 256 项) |
| mft2 | lut16Type | 16 位(表项数在文件里显式给出) |
let profile = @moon_icc.Profile::parse(@fs.read_file_to_bytes("printer.icc"))
// 设备值按 profile 输入色空间的通道顺序给出,0..1
let lab = profile.device_to_lab(1, [ 0.0, 0.0, 0.0, 0.5 ]) // CMYK 50% 黑moon run cmd/main --target wasm-gc -- lut testdata/cmyk_printer.icc| 版本 | 位深 | L 满量程 | a/b 满量程 |
|---|---|---|---|
| v2 | 16 | 0xFF00 = 65280 | 0xFF00 |
| v2 | 8 | 0xFF = 255 | 0xFF |
| v4 | 16 | 0xFFFF = 65535 | 0xFFFF |
| 意图 | 编号 | 设备→PCS | PCS→设备 |
|---|---|---|---|
| 感知 | 0 | A2B0 | B2A0 |
| 媒体相对比色 | 1 | A2B1 | B2A1 |
| 饱和度 | 2 | A2B2 | B2A2 |
| 类型 | 名称 | 方向 |
|---|---|---|
| mAB | lutAToBType | 设备 → PCS |
| mBA | lutBToAType | PCS → 设备 |
mAB:A 曲线 → CLUT → M 曲线 → 矩阵 → B 曲线
mBA:B 曲线 → 矩阵 → M 曲线 → CLUT → A 曲线| 类型 | 内容 | 长度 |
|---|---|---|
| curv 项数 0 | 恒等 | 8 + 4 |
| curv 项数 1 | 单个 gamma | 8 + 4 + 2 |
| curv 项数 n>1 | 查找表 | 8 + 4 + 2n |
| para | 函数类型 + 参数 | 8 + 4 + 4×(1/3/4/5/7) |
| 插值选择 | 与 lcms2 的一致率(40 样本) |
|---|---|
| mAB 四面体 + mBA 三线性(本库) | 见下表 |
| 两者对调 | 2 项测试变红,A2B0 差 3 级、B2A0 差 4 级 |
| tag | 方向 | 完全一致 | 差 1 级 | 最大偏差 |
|---|---|---|---|---|
| A2B0 | 设备 → PCS | 40/40 | 0 | 0 级 |
| A2B1 | 设备 → PCS | 40/40 | 0 | 0 级 |
| B2A0 | PCS → 设备 | 38/40 | 2 | 1 级 |
| B2A1 | PCS → 设备 | 39/40 | 1 | 1 级 |
| PCS | 编码 | 1.0 对应 | 上限 |
|---|---|---|---|
| Lab (v2) | 16 位线性 | — | 0xFF00(L=100) |
| Lab (v4) | 16 位线性 | — | 0xFFFF(L=100) |
| XYZ (v2 与 v4 相同) | u1Fixed15Number | 0x8000 | 0xFFFF = 1.99997 |
A2B0:XYZ = 设备值 × D50
B2A0:设备值 = XYZ / 1.99997…
组合:设备输出 = 设备输入 × D50 / 1.99997…| 方向 | 与 lcms2 完全一致 | 最大偏差 | 与解析期望的最大偏差 |
|---|---|---|---|
| 设备 → PCS Lab | 26/26 | 0 级 | — |
| 组合(设备 → PCS → 设备) | 26/26 | 0 级 | 1 级(量化) |
| lcms2 flags | 与本库完全一致 | 最大偏差 |
|---|---|---|
| NONE(默认) | 3942/4096 | 4 级 |
| NOOPTIMIZE | 4042/4096 | 3 级 |
| FORCE_CLUT | 3370/4096 | 11 级 |
| 偏差 | 例数 | 目标线性值范围 | lcms2 输出 |
|---|---|---|---|
| 1 级 | 39 | 1.9e-4 .. 9.5e-1(全范围) | 7 .. 248 |
| 2 级 | 9 | 6.7e-5 .. 1.8e-4(极暗) | 2 .. 6 |
| 3 级 | 6 | 8.7e-5 .. 1.2e-4(极暗) | 3 .. 4 |
python tools/gen_golden.py # 生成链接金标(lcms2 NOOPTIMIZE)
python tools/gen_clut_golden.py # 生成 CLUT 金标(网格节点 + lcms2)
python tools/gen_cross_link_golden.py # 生成跨类型链接金标
python tools/gen_intent_golden.py # 生成四种渲染意图金标
python tools/gen_v4lut_golden.py # 生成 ICC v4 LUT(mAB/mBA)金标
python tools/gen_xyz_lut_profile.py # 合成 PCS 为 XYZ 的 LUT profile(并用 lcms2 复核)
python tools/gen_xyz_golden.py # 生成合成 XYZ profile 的金标
python tools/lcms2_flags_check.py # 复现 flags 对比表
python tools/lcms2_deviation_report.py # 复现链接偏差分布表
python tools/lcms2_intent_check.py # 复现"哪些 profile 能区分意图"
python tools/diff_oracle.py wasm-gc # 独立 Python 规范实现差分
python tools/make_demo_video.py # 重新生成演示视频moon run cmd/main --target wasm-gc -- link testdata/srgb.icc testdata/cmyk_printer.icc saturation| 编号 | 名称 | 命令行 | 对 LUT 型 profile 的效果 |
|---|---|---|---|
| 0 | 感知 | perceptual | 用 A2B0/B2A0 |
| 1 | 媒体相对比色(默认) | relative | 用 A2B1/B2A1 |
| 2 | 饱和度 | saturation | 用 A2B2/B2A2 |
| 3 | 绝对比色 | absolute | 用 A2B1/B2A1,外加 PCS 白点缩放 |
linear = TRC(encoded) # 先线性化
XYZ = M · linear # 再乘矩阵实测教训:用错误的字节序读 sRGB Color Space Profile.icm,会把 3144 字节读成 1208745984,且 acsp 签名读成乱码。
moon test --target wasm-gc # 85 项测试
moon test --target js # 同一套测试,js 后端
moon test --target native # 同一套测试,native 后端
python tools/diff_oracle.py wasm-gc # 外部 oracle 差分
python tools/diff_oracle.py js # 同上,js 后端
python tools/verify_publish.py # 在全新工程里实拉已发布包并跑真实变换
python tools/check_readme_examples.py # 把本文件的示例代码逐块实跑并核对其中公布的数值
python tools/gen_proof_golden.py # 重新生成软打样金标(需要 MSYS2 gcc 编译 lcms2)check_readme_examples.py 与 verify_publish.py 不放进 CI 的 push 门禁: 它们从 mooncakes.io 拉取已发布版本,而 push 门禁跑的是当前源码—— 若改了示例但尚未发布,二者会不一致而产生误报。这两个脚本用于发布前后的人工验证。
| 核心功能 | 覆盖它的判据 |
|---|---|
| profile 解析(头部 + tag 表 + 边界校验) | header_wbtest.mbt(合成字节流,含畸形输入);tools/diff_oracle.py 对 5 个真实 profile 逐字段比对 |
| matrix/TRC 变换与逆变换 | transform_wbtest.mbt;link_golden_wbtest.mbt(4096 彩色样本对 lcms2);差分脚本逐位比对 |
| LUT 变换(mft1/mft2) | clut_wbtest.mbt(625 个网格节点 + 300 个 lcms2 CMYK 样本);tools/gen_clut_golden.py 反解输入表定位节点 |
| LUT 变换(mAB /mBA ,v4) | v4lut_wbtest.mbt(节点判据读原始字节 + 40 样本对 lcms2);tools/diff_oracle.py 独立结构对拍 |
| PCS 为 XYZ 的 LUT | xyz_lut_wbtest.mbt(u1Fixed15 编码常数判据 + 26 样本双向对拍 + 解析期望式) |
| 多 profile 链接(四种跨类型组合) | cross_link_wbtest.mbt + cross_link_golden_wbtest.mbt(sRGB ↔ CMYK 各 300 样本) |
| 渲染意图(感知 / 相对 / 饱和度 / 绝对) | intent_wbtest.mbt + intent_golden_wbtest.mbt(200 样本 × 四意图);绝对比色另断言「与相对比色必须不同」以防缩放失效 |
| 软打样链 | proofing_wbtest.mbt(自洽性 + 物理正确性)+ proof_golden_wbtest.mbt(lcms2 cmsCreateProofingTransform 金标,最大偏差 1 级) |
| 色域判断 | proofing_wbtest.mbt(中性轴基线、灰/白 in、黑与原色 out、判定与 ΔE 自洽) |
| 命令行示例与端到端可用性 | cmd/main 四子命令;tools/verify_publish.py 在仓库之外的空工程里 moon add 实拉并运行 |
| 植入的错误 | 单元测试反应 |
|---|---|
| renderingIntent 读取位置错位 | 3 项变红,外部差分 12 处不一致 |
| TRC 与矩阵顺序颠倒 | 5 项变红(白点偏离 D50 达 17.3 色差) |
| 矩阵求逆漏掉转置 | 5 项变红(lcms2 一致率跌到 0%) |
| CLUT 维度步长顺序写反 | 4 项变红(节点偏差 91 个 L 单位,lcms2 差 93 级) |
| LUT 侧 PCS 不做 Lab↔XYZ 转换 | 2 项变红(偏差 255 级,即完全错) |
| mAB/mBA 矩阵段按"8 字节类型头 + 矩阵"读 | 5 项变红(偏差 215–255 级) |
| 取消曲线段 4 字节对齐 | 1 项变红(合成 tag 判据报出 ..cu 非法曲线类型) |
| 交换两个方向的插值方式 | 2 项变红(A2B0 差 3 级、B2A0 差 4 级) |
| mBA 阶段顺序颠倒(矩阵与 M 曲线互换) | 1 项变红(偏差 255 级) |
| 忽略矩阵的偏移项 | 1 项变红(偏差 255 级) |
| XYZ PCS 解码误按 u1Fixed16(数字 32768→65535) | 3 项变红(节点判据报出 2 倍偏差,端到端差 61–64 级) |
| XYZ PCS 编码(逆方向)误按 u1Fixed16 | 1 项变红(组合变换差 127 级) |
| 缺省的曲线段返回空数组而非透传 | 3 项变红(合成 profile 的最大输入处整体输出 0) |
| 项目 | 语言 | 许可证 | 参考范围 |
|---|---|---|---|
| Little-CMS (lcms2) | C | MIT | ICC 规范的工程化解读、tag 处理策略 |
lcms2 采用 MIT 许可证,与本项目 Apache-2.0 兼容。本项目未链接、未编译、未复制其代码; 仓库内只包含它的测试用 profile testdata/test4.icc(用于建立 ICC v4 LUT 判据), 其来源、许可与 MIT 声明全文见 testdata/README.md。
pub suberror IccError {
Truncated(expected~ : Int, offset~ : Int, actual~ : Int)
BadSignature(found~ : String)
SizeMismatch(declared~ : Int, actual~ : Int)
TagOutOfBounds(tag~ : String, offset~ : Int, size~ : Int)
BadTagTable(count~ : Int, offset~ : Int)
BadTagData(tag~ : String, reason~ : String)
OutOfRange(what~ : String, value~ : Int)
}pub struct BigEndianReader {
data : Bytes
pos : Int
}pub struct ColorTransform {
source : SourceStage
destination : DestinationStage
intent : Int
pcs_scale : XyzNumber
}fn ColorTransform::proofing(source : Profile, proof : Profile, display : Profile, intent : Int, proof_intent : Int) -> ProofTransform raise IccError| 参数 | 含义 |
|---|---|
| source | 图像所在设备(如 sRGB 相机/显示器) |
| proof | 要模拟的目标设备(如 CMYK 印刷机) |
| display | 观察者实际使用的显示设备 |
| intent | 源与显示侧意图(通常 1 = 媒体相对比色) |
| proof_intent | 打样 profile 两个方向的意图(通常 1,或 3 = 绝对比色) |
fn ColorTransform::transform(self : ColorTransform, r : Double, g : Double, b : Double) -> (Double, Double, Double)fn ColorTransform::with_intent(source : Profile, destination : Profile, intent : Int) -> ColorTransform raise IccError| 编号 | 意图 | 对 LUT 型 profile 的效果 |
|---|---|---|
| 0 | 感知 | 用 A2B0/B2A0 |
| 1 | 媒体相对比色 | 用 A2B1/B2A1 |
| 2 | 饱和度 | 用 A2B2/B2A2 |
| 3 | 绝对比色 | 用 A2B1/B2A1,外加 PCS 白点缩放 |
scale = diag(源媒体白点 / 目标媒体白点)pub struct IccDateTime {
year : Int
month : Int
day : Int
hour : Int
minute : Int
second : Int
}pub struct IccHeader {
size : Int
cmm_type : Int
major_version : Int
minor_version : Int
device_class : String
color_space : String
pcs : String
date_time : IccDateTime
platform : String
flags : Int
manufacturer : Int
model : Int
attributes : Int
rendering_intent : Int
illuminant : XyzNumber
creator : Int
profile_id : Bytes
}type InverseCurvepub struct InverseMatrixTrc {
inverse_matrix : Matrix3
inv_r : InverseCurve
inv_g : InverseCurve
inv_b : InverseCurve
}pub struct Lab {
l : Double
a : Double
b : Double
}fn MatrixTrcTransform::to_pcs(self : MatrixTrcTransform, r : Double, g : Double, b : Double) -> XyzNumberpub struct ProofTransform {
source : SourceStage
proof_dest : DestinationStage
proof_source : SourceStage
display : DestinationStage
source_scale : XyzNumber
display_scale : XyzNumber
intent : Int
proof_intent : Int
}源设备值 --A2B--> PCS --B2A--> 打样设备值 --A2B--> PCS --B2A--> 显示设备值fn ProofTransform::evaluate(self : ProofTransform, inputs : Array[Double]) -> (Array[Double], Array[Double])pub struct XyzNumber {
x : Double
y : Double
z : Double
}| 版本 | 编码 | L 满量程 | a/b 满量程 |
|---|---|---|---|
| v2 | 16 位 | 0xFF00 = 65280 | 0xFF00 |
| v2 | 8 位 | 0xFF = 255 | 0xFF |
| v4 | 16 位 | 0xFFFF = 65535 | 0xFFFF |
fn intent_to_a2b_tag(intent : Int) -> String| 意图 | 编号 | tag |
|---|---|---|
| 感知 | 0 | A2B0 |
| 媒体相对比色 | 1 | A2B1 |
| 饱和度 | 2 | A2B2 |
| 绝对比色 | 3 | 无独立 tag,用 A2B1 再加白点调整 |
fn rendering_intent_name(intent : Int) -> Stringfn s15f16_to_double(raw : Int) -> DoubleInstall
Download zip纯 MoonBit 实现的 ICC 色彩管理引擎:解析 ICC profile 并执行真实色空间变换
Dependencies