///|
/// 软打样与色域判断。
///
/// 这两件事是同一套 PCS 往返能力的两个用法:
///
/// - **软打样**:源设备 → PCS → 「目标设备(打样)往返」 → PCS → 显示设备。
/// 中间那次往返把超出目标设备色域的颜色按该设备的实际能力裁掉,
/// 于是屏幕上看到的就是印刷后的损失。
/// - **色域判断**:PCS → 目标设备 → PCS 的往返误差(ΔE76)。
/// 色域内的颜色往返应几乎无损;被裁剪的颜色往返会明显偏离。
///|
/// 软打样链。
///
/// ```text
/// 源设备值 --A2B--> PCS --B2A--> 打样设备值 --A2B--> PCS --B2A--> 显示设备值
/// ```
///
/// 打样 profile 的两个方向都用 `proof_intent`;源与显示用 `intent`。
/// 绝对比色会在两个 PCS 交界处引入白点缩放(见 `ColorTransform` 的说明),
/// 缩放取相邻两侧的媒体白点之比。
pub struct ProofTransform {
/// 源侧:源设备值 → PCS。
source : SourceStage
/// 打样侧:PCS → 打样设备值。
proof_dest : DestinationStage
/// 打样侧:打样设备值 → PCS。
proof_source : SourceStage
/// 显示侧:PCS → 显示设备值。
display : DestinationStage
/// 源 → 打样 交界处的 PCS 缩放(绝对比色时非 1)。
source_scale : XyzNumber
/// 打样 → 显示 交界处的 PCS 缩放(绝对比色时非 1)。
display_scale : XyzNumber
/// 源/显示侧使用的意图。
intent : Int32
/// 打样 profile 使用的意图。
proof_intent : Int32
}
///|
/// 按分量缩放 PCS XYZ。
fn scale_xyz(xyz : XyzNumber, scale : XyzNumber) -> XyzNumber {
XyzNumber::new(xyz.x * scale.x, xyz.y * scale.y, xyz.z * scale.z)
}
///|
/// 两个媒体白点的分量比 `diag(a / b)`,用作绝对比色的 PCS 缩放。
fn white_scale(a : XyzNumber, b : XyzNumber) -> XyzNumber {
XyzNumber::new(a.x / b.x, a.y / b.y, a.z / b.z)
}
///|
/// 构造软打样链。
///
/// | 参数 | 含义 |
/// |------|------|
/// | `source` | 图像所在设备(如 sRGB 相机/显示器) |
/// | `proof` | 要模拟的目标设备(如 CMYK 印刷机) |
/// | `display` | 观察者实际使用的显示设备 |
/// | `intent` | 源与显示侧意图(通常 1 = 媒体相对比色) |
/// | `proof_intent` | 打样 profile 两个方向的意图(通常 1,或 3 = 绝对比色) |
pub fn ColorTransform::proofing(
source : Profile,
proof : Profile,
display : Profile,
intent : Int32,
proof_intent : Int32,
) -> ProofTransform raise IccError {
if intent < 0 || intent > 3 {
raise BadTagData(
tag="renderingIntent",
reason="源/显示侧意图编号应在 0..3,实际 \{intent}",
)
}
if proof_intent < 0 || proof_intent > 3 {
raise BadTagData(
tag="proofingIntent",
reason="打样意图编号应在 0..3,实际 \{proof_intent}",
)
}
let src_white = source.media_white_point()
let proof_white = proof.media_white_point()
let disp_white = display.media_white_point()
let ones = XyzNumber::new(1.0, 1.0, 1.0)
{
source: build_source(source, intent),
proof_dest: build_destination(proof, proof_intent),
proof_source: build_source(proof, proof_intent),
display: build_destination(display, intent),
source_scale: if intent == 3 {
white_scale(src_white, proof_white)
} else {
ones
},
display_scale: if proof_intent == 3 {
white_scale(proof_white, disp_white)
} else {
ones
},
intent,
proof_intent,
}
}
///|
/// 打样链求值:返回 `(打样设备值, 显示设备值)`。
///
/// 两个结果都有用:前者是「这张图印在目标设备上会用哪些墨量」,
/// 后者是「屏幕上应该显示成什么」。
pub fn ProofTransform::evaluate(
self : ProofTransform,
inputs : Array[Double],
) -> (Array[Double], Array[Double]) {
let pcs0 = scale_xyz(self.source.to_pcs(inputs), self.source_scale)
let proof_values = self.proof_dest.from_pcs(pcs0)
let pcs1 = scale_xyz(
self.proof_source.to_pcs(proof_values),
self.display_scale,
)
(proof_values, self.display.from_pcs(pcs1))
}
///|
/// 打样链求值(只取显示设备值)。
pub fn ProofTransform::transform_values(
self : ProofTransform,
inputs : Array[Double],
) -> Array[Double] {
let (_, shown) = self.evaluate(inputs)
shown
}
///|
/// 源/显示侧使用的意图编号。
pub fn ProofTransform::intent(self : ProofTransform) -> Int32 {
self.intent
}
///|
/// 打样 profile 使用的意图编号。
pub fn ProofTransform::proof_intent(self : ProofTransform) -> Int32 {
self.proof_intent
}
///|
/// 目标阶段的设备通道数(如 CMYK 为 4)。
fn DestinationStage::device_channels(self : DestinationStage) -> Int32 {
match self {
MatrixTrcDest(_) => 3
LutDest(stage) => stage.table.output_channels()
}
}
///|
/// 打样设备值 → 设备值通道数(如 CMYK 为 4)。
pub fn ProofTransform::proof_channels(self : ProofTransform) -> Int32 {
self.proof_dest.device_channels()
}
///|
/// 显示设备值通道数。
pub fn ProofTransform::display_channels(self : ProofTransform) -> Int32 {
self.display.device_channels()
}
///|
/// **颜色映射误差**:PCS → 设备 → PCS 的往返 ΔE76。
///
/// 这个量衡量的是「同一个 profile 的 A2B 与 B2A 有多不互逆」——
/// 它们本就是两张独立的表,CMYK 还受 GCR 与总墨量限制。
///
/// **它不用于色域判定**(那是 `gamut.mbt` 的球面边界描述符的职责)。
/// 之所以保留这个 API:它是打样链与设备特性的一手指标,也用于在
/// README 中量化「映射误差」与「真实越界」的区别。
pub fn Profile::round_trip_delta_e(
self : Profile,
intent : Int32,
xyz : XyzNumber,
) -> Double raise IccError {
let dest = build_destination(self, intent)
let src = build_source(self, intent)
let device = dest.from_pcs(xyz)
let back = src.to_pcs(device)
delta_e76(xyz_to_lab(xyz), xyz_to_lab(back))
}
///|
/// 中性轴上的**映射误差基线**:若干中性灰往返 ΔE76 的中位数。
///
/// 本机 Agfa SWOP CMYK profile 实测约 4.5 ΔE——这全部是映射误差,
/// 与色域无关。上一版曾把它当色域判定的容差基准,实测证明那条路走不通
/// (见 `gamut.mbt` 顶部的量化说明)。
pub fn Profile::neutral_mapping_error(
self : Profile,
intent : Int32,
) -> Double raise IccError {
let probe : Array[Double] = [20.0, 30.0, 40.0, 50.0, 60.0, 70.0, 80.0]
let errs : Array[Double] = []
for l in probe {
errs.push(
self.round_trip_delta_e(intent, lab_to_xyz(Lab::new(l, 0.0, 0.0))),
)
}
errs.sort()
errs[errs.length() / 2]
}