use crate::Matrix3x3;
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct DualIlluminantCalibration {
pub matrix_d50: Matrix3x3,
pub matrix_illuminant_a: Matrix3x3,
}
impl DualIlluminantCalibration {
#[must_use]
pub fn new(matrix_d50: Matrix3x3, matrix_illuminant_a: Matrix3x3) -> Self {
Self {
matrix_d50,
matrix_illuminant_a,
}
}
#[must_use]
pub fn interpolate_matrix_by_cct(&self, cct_kelvin: f64) -> Matrix3x3 {
let cct = cct_kelvin.max(1.0);
let inv_cct = 1.0 / cct;
let inv_d50 = 1.0 / 5000.0;
let inv_a = 1.0 / 2850.0;
let w = ((inv_cct - inv_d50) / (inv_a - inv_d50)).clamp(0.0, 1.0);
let w_inv = 1.0 - w;
let mut result = [[0.0_f64; 3]; 3];
for row in 0..3 {
for col in 0..3 {
result[row][col] =
w * self.matrix_illuminant_a[row][col] + w_inv * self.matrix_d50[row][col];
}
}
result
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct DngColorProfile {
pub description: String,
pub dual_illuminant: Option<DualIlluminantCalibration>,
pub forward_matrix: Matrix3x3,
}
impl DngColorProfile {
#[must_use]
pub fn new(description: String, forward_matrix: Matrix3x3) -> Self {
Self {
description,
dual_illuminant: None,
forward_matrix,
}
}
#[must_use]
pub fn with_dual_illuminant(description: String, d50: Matrix3x3, illum_a: Matrix3x3) -> Self {
Self {
description,
forward_matrix: d50,
dual_illuminant: Some(DualIlluminantCalibration::new(d50, illum_a)),
}
}
#[must_use]
pub fn matrix_for_cct(&self, cct_kelvin: f64) -> Matrix3x3 {
match &self.dual_illuminant {
Some(di) => di.interpolate_matrix_by_cct(cct_kelvin),
None => self.forward_matrix,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn d50_matrix() -> Matrix3x3 {
[[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]]
}
fn illum_a_matrix() -> Matrix3x3 {
[[2.0, 0.0, 0.0], [0.0, 2.0, 0.0], [0.0, 0.0, 2.0]]
}
#[test]
fn test_dual_illuminant_d50_limit() {
let di = DualIlluminantCalibration::new(d50_matrix(), illum_a_matrix());
let mat = di.interpolate_matrix_by_cct(5000.0);
for row in 0..3 {
for col in 0..3 {
let expected = d50_matrix()[row][col];
assert!(
(mat[row][col] - expected).abs() < 1e-10,
"at 5000 K matrix[{row}][{col}] should be {expected}, got {}",
mat[row][col]
);
}
}
}
#[test]
fn test_dual_illuminant_illum_a_limit() {
let di = DualIlluminantCalibration::new(d50_matrix(), illum_a_matrix());
let mat = di.interpolate_matrix_by_cct(2850.0);
for row in 0..3 {
for col in 0..3 {
let expected = illum_a_matrix()[row][col];
assert!(
(mat[row][col] - expected).abs() < 1e-10,
"at 2850 K matrix[{row}][{col}] should be {expected}, got {}",
mat[row][col]
);
}
}
}
#[test]
fn test_dual_illuminant_midpoint() {
let inv_mid = (1.0_f64 / 2850.0 + 1.0_f64 / 5000.0) / 2.0;
let cct_mid = 1.0 / inv_mid;
let di = DualIlluminantCalibration::new(d50_matrix(), illum_a_matrix());
let mat = di.interpolate_matrix_by_cct(cct_mid);
assert!(
(mat[0][0] - 1.5).abs() < 1e-10,
"midpoint [0][0] should be 1.5, got {}",
mat[0][0]
);
assert!(
mat[0][1].abs() < 1e-10,
"off-diagonal [0][1] should be 0.0, got {}",
mat[0][1]
);
}
#[test]
fn test_dng_color_profile_with_dual_illuminant() {
let profile = DngColorProfile::with_dual_illuminant(
"Test".to_string(),
d50_matrix(),
illum_a_matrix(),
);
assert!(profile.dual_illuminant.is_some());
let mat = profile.matrix_for_cct(5000.0);
assert!((mat[0][0] - 1.0).abs() < 1e-10);
let mat_a = profile.matrix_for_cct(2850.0);
assert!((mat_a[0][0] - 2.0).abs() < 1e-10);
}
#[test]
fn test_dual_illuminant_cct_above_d50_clamped() {
let di = DualIlluminantCalibration::new(d50_matrix(), illum_a_matrix());
let mat = di.interpolate_matrix_by_cct(10_000.0);
assert!((mat[0][0] - 1.0).abs() < 1e-10);
}
#[test]
fn test_dual_illuminant_cct_below_a_clamped() {
let di = DualIlluminantCalibration::new(d50_matrix(), illum_a_matrix());
let mat = di.interpolate_matrix_by_cct(1000.0);
assert!((mat[0][0] - 2.0).abs() < 1e-10);
}
}