///|
/// ICC v4 的 `mAB ` / `mBA ` LUT(`lutAToBType` / `lutBToAType`)解析与求值。
///
/// `mft1`/`mft2`(v2 的 `lut8Type`/`lut16Type`)在 `clut.mbt` 实现,
/// 本文件只处理 v4 的这两种新类型。
///
/// ## tag 数据布局
///
/// | 偏移 | 大小 | 内容 |
/// |------|------|------|
/// | 0..3 | 4 | 类型签名 `mAB ` 或 `mBA ` |
/// | 4..7 | 4 | 保留 |
/// | 8 | 1 | 输入通道数 |
/// | 9 | 1 | 输出通道数 |
/// | 10..11 | 2 | 保留 |
/// | 12..31 | 20 | 5 个偏移:B、矩阵、M、CLUT、A(均相对 tag 起始) |
///
/// 五个偏移指向的段位于其后,顺序**不固定**(`mBA` 的书写顺序与 `mAB` 相反),
/// 必须按偏移寻址而不是按顺序读。
///
/// ## 与 `mft1`/`mft2` 的差别(实现时最容易踩的地方)
///
/// 1. **矩阵段没有类型签名头**。偏移直接指向 9 个 `s15Fixed16` 矩阵项,
/// 紧随其后是 3 个 `s15Fixed16` 偏移项,共 48 字节。
/// 本文件初次实现时按"先跳 8 字节类型头"读,结果把 M 段的 `para`
/// 签名当成了矩阵偏移项,与参考实现的偏差最大达 10 级
/// (形如 28769.44 的"偏移"就是 `'para'` 四个字节被当成定点数)。
/// 2. **曲线段内嵌在 tag 里,没有长度字段**。类型签名后紧跟的 4 字节是
/// 保留位而不是长度;段长度必须按类型从内容推算(见 `curve_segment_len`)。
/// 对拍样本 `test4.icc` 的曲线段保留位恰为 0,若当成长度读会直接读错位。
/// 3. **曲线段要补齐到 4 字节边界**(相对文件绝对位置)。
///
/// ## 处理顺序
///
/// - `mAB `(设备 → PCS):A 曲线 → CLUT → M 曲线 → 矩阵 → B 曲线
/// - `mBA `(PCS → 设备):B 曲线 → 矩阵 → M 曲线 → CLUT → A 曲线
///
/// A/M/B 三段都可以缺省(偏移为 0),CLUT 也可以缺省;
/// 每条曲线的通道数由方向决定:`mAB` 的 A 用输入通道数、M/B 用输出通道数,
/// `mBA` 的 B/M 用输入通道数、A 用输出通道数。这与 lcms2 的读取代码一致。
///
/// ## 插值方式
///
/// 参考实现在两个方向用的插值**不同**,本文件照此实现并已用对拍验证:
///
/// - `mAB `(设备作索引)用**四面体**插值;
/// - `mBA `(PCS Lab 作索引)用**三线性**插值。
///
/// 依据:lcms2 `cmsintrp.c` 对 3 输入 CLUT 默认选四面体,
/// 而 `cmsio1.c` 的 `_cmsReadOutputLUT` 在 PCS 为 Lab 时会显式改回三线性
/// (代码注释称这是"controversial stuff")。用实测数据验证:
/// 把 `mAB ` 换成三线性、或把 `mBA ` 换成四面体,与参考实现的一致率都会下降。
///|
/// v4 LUT 的方向。
pub enum V4LutKind {
/// `mAB `:设备 → PCS。
AToB
/// `mBA `:PCS → 设备。
BToA
}
///|
/// 一个已解析的 `mAB ` / `mBA ` LUT。
pub struct V4Lut {
/// 方向,决定各段的处理顺序与曲线通道数。
kind : V4LutKind
/// 输入通道数。
input_channels : Int32
/// 输出通道数。
output_channels : Int32
/// A 曲线(可为空数组表示缺省)。
a_curves : Array[ToneCurve]
/// M 曲线(可为空数组表示缺省)。
m_curves : Array[ToneCurve]
/// B 曲线(可为空数组表示缺省)。
b_curves : Array[ToneCurve]
/// 3×3 矩阵(行主序 9 项);空数组表示缺省。
matrix : Array[Double]
/// 矩阵的 3 个偏移项;无矩阵时为空。
offsets : Array[Double]
/// 每一维的网格点数;空数组表示缺省 CLUT。
grid : Array[Int32]
/// CLUT 表值的位深:1(u8)或 2(u16);无 CLUT 时为 0。
precision : Int32
/// CLUT 节点值,按 (输入格点组合, 输出通道) 连续存放,归一化到 [0,1]。
clut : Array[Double]
}
///|
/// 解析 `mAB ` / `mBA ` tag。
pub fn parse_v4_lut(
data : Bytes,
entry : IccTagEntry,
kind : V4LutKind,
) -> V4Lut raise IccError {
let (type_sig, body_at) = tag_type_and_data(data, entry)
let want = match kind {
AToB => "mAB "
BToA => "mBA "
}
if type_sig != want {
raise BadTagData(
tag=entry.signature,
reason="期望类型 \"\{want}\",实际 \"\{type_sig}\"",
)
}
let base = entry.offset
let tag_end = entry.offset + entry.size
let r = BigEndianReader::new(data)
r.seek(body_at)
let input_channels = r.read_u8()
let output_channels = r.read_u8()
r.skip(2)
let off_b = r.read_u32()
let off_mat = r.read_u32()
let off_m = r.read_u32()
let off_c = r.read_u32()
let off_a = r.read_u32()
if input_channels < 1 ||
output_channels < 1 ||
input_channels > 15 ||
output_channels > 15 {
raise BadTagData(
tag=entry.signature,
reason="通道数非法: 输入 \{input_channels} 输出 \{output_channels}",
)
}
// 曲线段:条数由方向决定
let (a_count, m_count, b_count) = match kind {
AToB => (input_channels, output_channels, output_channels)
BToA => (output_channels, input_channels, input_channels)
}
// 偏移必须落在 tag 范围内
let offs : Array[Int32] = [off_b, off_mat, off_m, off_c, off_a]
for i = 0; i < offs.length(); i = i + 1 {
let o = offs[i]
if o != 0 && (o < 24 || base + o >= tag_end) {
raise BadTagData(
tag=entry.signature,
reason="第 \{i} 个段偏移 \{o} 超出 tag 范围(长度 \{entry.size})",
)
}
}
let a_curves = read_curve_set(data, entry, off_a, a_count)
let m_curves = read_curve_set(data, entry, off_m, m_count)
let b_curves = read_curve_set(data, entry, off_b, b_count)
// 矩阵段(无类型签名头)
let matrix : Array[Double] = []
let offsets : Array[Double] = []
if off_mat != 0 {
if base + off_mat + 48 > tag_end {
raise BadTagData(
tag=entry.signature,
reason="矩阵段越界(偏移 \{off_mat},需要 48 字节)",
)
}
let mr = BigEndianReader::new(data)
mr.seek(base + off_mat)
for i = 0; i < 9; i = i + 1 {
matrix.push(mr.read_s15f16())
}
for i = 0; i < 3; i = i + 1 {
offsets.push(mr.read_s15f16())
}
}
// CLUT 段
let grid : Array[Int32] = []
let clut : Array[Double] = []
let mut precision = 0
if off_c != 0 {
let cr = BigEndianReader::new(data)
cr.seek(base + off_c)
for i = 0; i < 16; i = i + 1 {
let g = cr.read_u8()
if i < input_channels {
if g < 2 {
raise BadTagData(
tag=entry.signature,
reason="CLUT 第 \{i} 维网格点数 \{g} 小于 2,无法插值",
)
}
grid.push(g)
}
}
precision = cr.read_u8()
cr.skip(3)
if precision != 1 && precision != 2 {
raise BadTagData(
tag=entry.signature,
reason="CLUT 表值位深 \{precision} 字节非法(仅支持 1 或 2)",
)
}
// 节点数:profile 是外部不可信输入,先算并设上限,避免畸形值导致巨量分配
let mut nodes_per_output = 1
for i = 0; i < input_channels; i = i + 1 {
nodes_per_output = nodes_per_output * grid[i]
if nodes_per_output > 1_000_000 {
raise BadTagData(
tag=entry.signature,
reason="CLUT 节点数超过上限,疑似畸形 profile",
)
}
}
let count = nodes_per_output * output_channels
for i = 0; i < count; i = i + 1 {
clut.push(
if precision == 1 {
cr.read_u8().to_double() / 255.0
} else {
cr.read_u16().to_double() / 65535.0
},
)
}
}
{
kind,
input_channels,
output_channels,
a_curves,
m_curves,
b_curves,
matrix,
offsets,
grid,
precision,
clut,
}
}
///|
/// 读一组内嵌曲线。
///
/// `off` 为相对 tag 起始的偏移,0 表示缺省。曲线段没有长度字段,
/// 每条曲线的长度由 `curve_segment_len` 从内容推算,再补齐到 4 字节边界。
fn read_curve_set(
data : Bytes,
entry : IccTagEntry,
off : Int32,
count : Int32,
) -> Array[ToneCurve] raise IccError {
let out : Array[ToneCurve] = []
if off == 0 {
return out
}
let base = entry.offset
let tag_end = entry.offset + entry.size
let mut p = base + off
for i = 0; i < count; i = i + 1 {
if p + 8 > tag_end {
raise BadTagData(
tag=entry.signature,
reason="曲线段第 \{i} 条越界(偏移 \{p - base})",
)
}
let sub : IccTagEntry = {
signature: entry.signature,
offset: p,
size: tag_end - p,
}
out.push(parse_tone_curve(data, sub))
let len = curve_segment_len(data, p, entry.signature)
p = p + len
// 补齐到 4 字节边界(相对文件绝对位置,与 lcms2 的 _cmsReadAlignment 一致)
p = (p + 3) / 4 * 4
}
out
}
///|
/// 算出一条内嵌曲线的总长度(含 8 字节类型头,不含对齐补齐)。
///
/// 曲线段**没有长度字段**:类型签名之后的 4 字节是保留位。
/// 因此只能按类型从内容推算:
///
/// | 类型 | 内容 |
/// |------|------|
/// | `curv` 项数 0 | 8 + 4 |
/// | `curv` 项数 1 | 8 + 4 + 2(单个 gamma) |
/// | `curv` 项数 n>1 | 8 + 4 + 2n |
/// | `para` | 8 + 4 + 4×参数个数(1/3/4/5/7) |
fn curve_segment_len(
data : Bytes,
at : Int32,
tag : String,
) -> Int32 raise IccError {
let r = BigEndianReader::new(data)
r.seek(at)
let sig = r.read_signature()
r.skip(4)
match sig {
"curv" => {
let count = r.read_u32()
if count < 0 {
raise BadTagData(tag~, reason="curv 项数非法")
}
if count == 0 {
12
} else if count == 1 {
14
} else {
12 + count * 2
}
}
"para" => {
let ft = r.read_u16()
r.skip(2)
let n = match ft {
0 => 1
1 => 3
2 => 4
3 => 5
4 => 7
_ =>
raise BadTagData(tag~, reason="不支持的 para 函数类型 \{ft}")
}
12 + n * 4
}
_ =>
raise BadTagData(
tag~,
reason="内嵌曲线类型 \"\{sig}\" 不支持(仅 curv 与 para)",
)
}
}
///|
/// 对一组曲线求值:通道数不足时按恒等补齐,结果夹到 [0,1]。
///
/// 夹紧是必要的:lcms2 的 16 位定点管线在每一级之后都把值限制在
/// [0,65535]。实测 `mBA` 的 M 曲线在超色域输入下会算出负值,
/// 不夹紧会与参考实现出现数十级的偏差。
///
/// **曲线集缺省(长度为 0)时必须原样透传**,不能返回空数组——
/// `mAB `/`mBA ` 的 A/M/B 三段都可以缺省,中间某一级缺省时若把数组清空,
/// 后续阶段就拿不到通道值(本项目真实踩到:一个只有 A 曲线 + CLUT 的
/// 合成 profile 在最大输入处整体输出 0)。
fn apply_curve_set(
curves : Array[ToneCurve],
values : Array[Double],
) -> Array[Double] {
let out : Array[Double] = []
if curves.length() == 0 {
for v in values {
out.push(v)
}
return out
}
for i = 0; i < curves.length(); i = i + 1 {
let v = if i < values.length() { values[i] } else { 0.0 }
out.push(clamp01(curves[i].evaluate(v)))
}
out
}
///|
/// 对 mAB/mBA 的 LUT 求值。
///
/// 输入按该方向的入口通道数给出(`mAB ` 为设备值,`mBA ` 为 PCS Lab 编码),
/// 输出按出口通道数给出,全部归一化到 [0,1]。
pub fn V4Lut::evaluate(self : V4Lut, inputs : Array[Double]) -> Array[Double] {
match self.kind {
AToB => {
let x = apply_curve_set(self.a_curves, inputs)
let y = if self.clut.length() > 0 {
self.interpolate_clut(x, true)
} else {
x
}
let y = apply_curve_set(self.m_curves, y)
let y = self.apply_matrix(y)
apply_curve_set(self.b_curves, y)
}
BToA => {
let y = apply_curve_set(self.b_curves, inputs)
let y = self.apply_matrix(y)
let y = apply_curve_set(self.m_curves, y)
let x = if self.clut.length() > 0 {
self.interpolate_clut(y, false)
} else {
y
}
apply_curve_set(self.a_curves, x)
}
}
}
///|
/// 应用矩阵与偏移项(仅 3 通道;缺省时原样返回)。
fn V4Lut::apply_matrix(self : V4Lut, values : Array[Double]) -> Array[Double] {
if self.matrix.length() != 9 || values.length() < 3 {
return values
}
let x = clamp01(values[0])
let y = clamp01(values[1])
let z = clamp01(values[2])
[
clamp01(
self.matrix[0] * x +
self.matrix[1] * y +
self.matrix[2] * z +
self.offsets[0],
),
clamp01(
self.matrix[3] * x +
self.matrix[4] * y +
self.matrix[5] * z +
self.offsets[1],
),
clamp01(
self.matrix[6] * x +
self.matrix[7] * y +
self.matrix[8] * z +
self.offsets[2],
),
]
}
///|
/// 在 CLUT 上插值。`tetrahedral` 为真时用四面体(仅 3 输入),否则用三线性。
fn V4Lut::interpolate_clut(
self : V4Lut,
values : Array[Double],
tetrahedral : Bool,
) -> Array[Double] {
let ic = self.input_channels
let oc = self.output_channels
// 各维基点、小数部分与步长(单位:表项)
let base : Array[Int32] = []
let frac : Array[Double] = []
let stride : Array[Int32] = []
for i = 0; i < ic; i = i + 1 {
let g = self.grid[i]
let v = clamp01(if i < values.length() { values[i] } else { 0.0 })
let pos = v * (g - 1).to_double()
let mut b = pos.to_int()
if b > g - 2 {
b = g - 2
}
if b < 0 {
b = 0
}
base.push(b)
frac.push(pos - b.to_double())
let mut s = oc
for j = i + 1; j < ic; j = j + 1 {
s = s * self.grid[j]
}
stride.push(s)
}
if tetrahedral && ic == 3 {
return self.tetrahedral(base, frac, stride)
}
let out : Array[Double] = []
for c = 0; c < oc; c = c + 1 {
out.push(0.0)
}
let corners = 1 << ic
for mask = 0; mask < corners; mask = mask + 1 {
let mut weight = 1.0
let mut node = 0
for i = 0; i < ic; i = i + 1 {
if ((mask >> i) & 1) == 1 {
weight = weight * frac[i]
node = node + (base[i] + 1) * stride[i]
} else {
weight = weight * (1.0 - frac[i])
node = node + base[i] * stride[i]
}
}
if weight != 0.0 {
for c = 0; c < oc; c = c + 1 {
out[c] = out[c] + weight * self.clut[node + c]
}
}
}
out
}
///|
/// 3 维 CLUT 的四面体(Sakamoto)插值。
///
/// 与 lcms2 `cmsintrp.c` 的 `TetrahedralInterpFloat` 同构:
/// 按 `rx/ry/rz` 的大小关系选六个四面体之一,用 4 个角点做线性组合。
/// 注意输入恰为 1 时该维"不进位"(`X1 = X0`),这一细节处理错会让
/// 白色点差出十几级。
fn V4Lut::tetrahedral(
self : V4Lut,
base : Array[Int32],
frac : Array[Double],
stride : Array[Int32],
) -> Array[Double] {
let oc = self.output_channels
let x0 = base[0]
let y0 = base[1]
let z0 = base[2]
let rx = frac[0]
let ry = frac[1]
let rz = frac[2]
let x1 = x0 + 1
let y1 = y0 + 1
let z1 = z0 + 1
let sx = stride[0]
let sy = stride[1]
let sz = stride[2]
let cell = fn(x : Int32, y : Int32, z : Int32, c : Int32) -> Double {
self.clut[x * sx + y * sy + z * sz + c]
}
let c0 : Array[Double] = []
for c = 0; c < oc; c = c + 1 {
c0.push(cell(x0, y0, z0, c))
}
// 六个四面体按 (rx,ry,rz) 的大小关系选择
let out : Array[Double] = []
for c = 0; c < oc; c = c + 1 {
let mut v1 = 0.0
let mut v2 = 0.0
let mut v3 = 0.0
if rx >= ry && ry >= rz {
v1 = cell(x1, y0, z0, c) - c0[c]
v2 = cell(x1, y1, z0, c) - cell(x1, y0, z0, c)
v3 = cell(x1, y1, z1, c) - cell(x1, y1, z0, c)
} else if rx >= rz && rz >= ry {
v1 = cell(x1, y0, z0, c) - c0[c]
v2 = cell(x1, y1, z1, c) - cell(x1, y0, z1, c)
v3 = cell(x1, y0, z1, c) - cell(x1, y0, z0, c)
} else if rz >= rx && rx >= ry {
v1 = cell(x1, y0, z1, c) - cell(x0, y0, z1, c)
v2 = cell(x1, y1, z1, c) - cell(x1, y0, z1, c)
v3 = cell(x0, y0, z1, c) - c0[c]
} else if ry >= rx && rx >= rz {
v1 = cell(x1, y1, z0, c) - cell(x0, y1, z0, c)
v2 = cell(x0, y1, z0, c) - c0[c]
v3 = cell(x1, y1, z1, c) - cell(x1, y1, z0, c)
} else if ry >= rz && rz >= rx {
v1 = cell(x1, y1, z1, c) - cell(x0, y1, z1, c)
v2 = cell(x0, y1, z0, c) - c0[c]
v3 = cell(x0, y1, z1, c) - cell(x0, y1, z0, c)
} else {
v1 = cell(x1, y1, z1, c) - cell(x0, y1, z1, c)
v2 = cell(x0, y1, z1, c) - cell(x0, y0, z1, c)
v3 = cell(x0, y0, z1, c) - c0[c]
}
out.push(clamp01(c0[c] + v1 * rx + v2 * ry + v3 * rz))
}
out
}
///|
/// LUT 型 tag 的统一入口。
///
/// v2 的 `mft1`/`mft2` 与 v4 的 `mAB `/`mBA ` 数据布局差别很大,
/// 但对外语义相同:"一组归一化输入 → 一组归一化输出"。
/// 统一成一个枚举后,链接逻辑(`transform.mbt`)不必区分二者。
pub enum LutTable {
/// v2 的 `mft1`(8 位表值)或 `mft2`(16 位表值)。
LutV2(Clut)
/// v4 的 `mAB ` / `mBA `。
LutV4(V4Lut)
}
///|
/// 解析**设备 → PCS** 方向的 LUT tag。
pub fn parse_a2b_table(
data : Bytes,
entry : IccTagEntry,
) -> LutTable raise IccError {
let (type_sig, _) = tag_type_and_data(data, entry)
if type_sig == "mft1" || type_sig == "mft2" {
return LutV2(parse_clut_tag(data, entry))
}
if type_sig == "mAB " {
return LutV4(parse_v4_lut(data, entry, AToB))
}
raise BadTagData(
tag=entry.signature,
reason="A2B 方向的 LUT 类型 \"\{type_sig}\" 不支持(仅 mft1 / mft2 / \"mAB \")",
)
}
///|
/// 解析**PCS → 设备** 方向的 LUT tag。
pub fn parse_b2a_table(
data : Bytes,
entry : IccTagEntry,
) -> LutTable raise IccError {
let (type_sig, _) = tag_type_and_data(data, entry)
if type_sig == "mft1" || type_sig == "mft2" {
return LutV2(parse_clut_tag(data, entry))
}
if type_sig == "mBA " {
return LutV4(parse_v4_lut(data, entry, BToA))
}
raise BadTagData(
tag=entry.signature,
reason="B2A 方向的 LUT 类型 \"\{type_sig}\" 不支持(仅 mft1 / mft2 / \"mBA \")",
)
}
///|
/// 求值。
pub fn LutTable::evaluate(
self : LutTable,
inputs : Array[Double],
) -> Array[Double] {
match self {
LutV2(c) => c.evaluate(inputs)
LutV4(v) => v.evaluate(inputs)
}
}
///|
/// LUT 的类型签名(诊断输出用)。
pub fn LutTable::type_name(self : LutTable) -> String {
match self {
LutV2(c) => if c.bit_depth == 8 { "mft1" } else { "mft2" }
LutV4(v) =>
match v.kind {
AToB => "mAB "
BToA => "mBA "
}
}
}
///|
/// 网格点数描述。
///
/// `mft1`/`mft2` 每一维的网格点数相同,只报一个值;
/// `mAB `/`mBA ` 每一维可以不同,逐维列出。
pub fn LutTable::grid_text(self : LutTable) -> String {
match self {
LutV2(c) => "\{c.grid_points}"
LutV4(v) =>
if v.grid.length() == 0 {
"无 CLUT"
} else {
let b = StringBuilder()
for i = 0; i < v.grid.length(); i = i + 1 {
if i > 0 {
b.write_char('x')
}
b.write_string(v.grid[i].to_string())
}
b.to_string()
}
}
}
///|
/// 输入通道数。
pub fn LutTable::input_channels(self : LutTable) -> Int32 {
match self {
LutV2(c) => c.input_channels
LutV4(v) => v.input_channels
}
}
///|
/// 输出通道数。
pub fn LutTable::output_channels(self : LutTable) -> Int32 {
match self {
LutV2(c) => c.output_channels
LutV4(v) => v.output_channels
}
}
///|
/// 表值位深(`clut_output_to_lab` / `clut_output_to_xyz` 需要)。
///
/// v2 的 `mft1`/`mft2` 由类型决定(8 / 16 位);
/// v4 的 `mAB `/`mBA ` 由 CLUT 段的精度字节决定(1 字节 → 8 位,2 字节 → 16 位),
/// 因为这个位深同时决定 **XYZ PCS 的折算系数**(8 位满量程是 128,16 位是 32768)。
pub fn LutTable::bit_depth(self : LutTable) -> Int32 {
match self {
LutV2(c) => c.bit_depth
LutV4(v) => if v.precision == 1 { 8 } else { 16 }
}
}
///|
/// 该表给出的 PCS Lab 是否按 **v2 编码** 解释。
///
/// `mft1`/`mft2` 用 profile 版本判定(v2 的上限是 0xFF00);
/// `mAB `/`mBA ` 是 v4 独有的类型,一律是 v4 口径。
pub fn LutTable::pcs_is_v2(self : LutTable, profile_is_v2 : Bool) -> Bool {
match self {
LutV2(_) => profile_is_v2
LutV4(_) => false
}
}