纯 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
...
### 命令行示例
仓库自带一个可运行的示例程序:
`parse` 输出 profile 的头部与 tag 表:
`transform` 输出固定样本点的变换结果(含中间调,便于对拍):
## 当前进度
| 功能 | 状态 |
|---|---|
| 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 任意组合) | ✅ 已完成并验证 |
| 渲染意图:感知 / 媒体相对比色 / 饱和度 | ✅ 已完成并验证 |
| 渲染意图:绝对比色 | **已知限制**,见下文(与相对比色相同路径) |
| `mAB` / `mBA`(ICC v4 LUT) | ✅ 已完成并验证 |
| PCS 为 `XYZ ` 的 LUT 型 profile | ✅ 已完成并验证(v4 样本;v2 路径无样本,见「明确不做」) |
## CLUT 型 profile 变换
大量真实世界的 profile(印刷、CMYK、扫描仪)不是矩阵/TRC 型,而是把整个
变换存在一张多维查找表里。本库支持 ICC v2 的两种 LUT 类型:
| 类型 | 名称 | 表值位深 |
|---|---|---|
| `mft1` | lut8Type | 8 位(表固定 256 项) |
| `mft2` | lut16Type | 16 位(表项数在文件里显式给出) |
处理顺序:**输入表 → 矩阵(仅 3 输入通道时)→ CLUT(多线性插值)→ 输出表**。
用法:
```moonbit
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 | 同 1(已知限制,见下) |
linear = TRC(encoded) # 先线性化
XYZ = M · linear # 再乘矩阵实测教训:用错误的字节序读 sRGB Color Space Profile.icm,会把 3144 字节读成 1208745984,且 acsp 签名读成乱码。
moon test --target wasm-gc # 78 项测试
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 后端| 植入的错误 | 单元测试反应 |
|---|---|
| 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
}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 | 绝对比色 | 同 1(已知限制,见下) |
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 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