use crate::black_point::{detect_black_point, detect_dest_black_point};
use crate::color::{CxyY, Cxyz, D50};
use crate::pipeline::{Pipeline, PipelineError, PipelineStage};
use crate::profile::{ColorSpace, IccProfile, Intent, ProfileClass};
use crate::util::{lcms_mat3_eval, lcms_mat3_per};
use cgmath::prelude::*;
use cgmath::{Matrix3, Vector3};
use std::fmt;
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub enum LinkError {
Pipeline(PipelineError),
IncompatibleSpaces(usize, ColorSpace, ColorSpace),
AbsoluteIntentError(usize),
NoDeviceLinkLut(usize),
NoInputLut(usize),
NoOutputLut(usize),
}
impl From<PipelineError> for LinkError {
fn from(this: PipelineError) -> LinkError {
LinkError::Pipeline(this)
}
}
impl fmt::Display for LinkError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
LinkError::Pipeline(err) => write!(f, "{}", err),
LinkError::IncompatibleSpaces(i, a, b) => write!(
f,
"incompatible color spaces at index {}: converting from {} to {}",
i, a, b
),
LinkError::AbsoluteIntentError(i) => {
write!(f, "absolute intent error for profile at index {}", i)
}
LinkError::NoDeviceLinkLut(i) => {
write!(f, "missing device link LUT for profile at index {}", i)
}
LinkError::NoInputLut(i) => write!(f, "missing input LUT for profile at index {}", i),
LinkError::NoOutputLut(i) => write!(f, "missing output LUT for profile at index {}", i),
}
}
}
impl ColorSpace {
pub fn is_compatible_with(self, other: ColorSpace) -> bool {
if self == other {
true
} else if self == ColorSpace::S4Color && other == ColorSpace::CMYK
|| self == ColorSpace::CMYK && other == ColorSpace::S4Color
{
true
} else if self == ColorSpace::XYZ && other == ColorSpace::Lab
|| self == ColorSpace::Lab && other == ColorSpace::XYZ
{
true
} else {
false
}
}
}
pub fn link(
profiles: &[&IccProfile],
intents: &[Intent],
black_point_compensation: &[bool],
adaptation_states: &[f64],
) -> Result<Pipeline, LinkError> {
let mut pipeline = Pipeline::new();
let mut current_cs = profiles[0].color_space;
for i in 0..profiles.len() {
let profile = &profiles[i];
let intent = intents[i];
let mut bpc = black_point_compensation[i];
if intent == Intent::AbsoluteColorimetric {
bpc = false;
}
if intent == Intent::Perceptual || intent == Intent::Saturation {
if profile.version() >= (4, 0) {
bpc = true;
}
}
let is_device_link = profile.device_class == ProfileClass::Link.into()
|| profile.device_class == ProfileClass::Abstract.into();
let use_as_input = if i == 0 && !is_device_link {
true
} else {
current_cs != ColorSpace::XYZ && current_cs != ColorSpace::Lab
};
let (cs_in, cs_out) = if use_as_input || is_device_link {
(profile.color_space, profile.pcs)
} else {
(profile.pcs, profile.color_space)
};
if !cs_in.is_compatible_with(current_cs) {
return Err(LinkError::IncompatibleSpaces(i, cs_in, current_cs));
}
if is_device_link
|| (profile.device_class == ProfileClass::NamedColor && profiles.len() == 1)
{
let mut profile_lut = match profile.device_link_lut(intent) {
Some(lut) => lut,
None => return Err(LinkError::NoDeviceLinkLut(i)),
};
let (m, off) = if profile.device_class == ProfileClass::Abstract && i > 0 {
compute_conversion(i as u32, profiles, intent, bpc, adaptation_states[i])?
} else {
(Matrix3::identity(), Vector3::zero())
};
add_conversion(&mut pipeline, i, current_cs, cs_in, m, off)?;
pipeline.append(&mut profile_lut)?;
} else if use_as_input {
let mut profile_lut = match profile.input_lut(intent) {
Some(lut) => lut,
None => return Err(LinkError::NoInputLut(i)),
};
pipeline.append(&mut profile_lut)?;
} else {
let mut profile_lut = match profile.output_lut(intent) {
Some(lut) => lut,
None => return Err(LinkError::NoOutputLut(i)),
};
let (m, off) =
compute_conversion(i as u32, profiles, intent, bpc, adaptation_states[i])?;
add_conversion(&mut pipeline, i, current_cs, cs_in, m, off)?;
pipeline.append(&mut profile_lut)?;
}
current_cs = cs_out;
}
Ok(pipeline)
}
fn compute_conversion(
i: u32,
profiles: &[&IccProfile],
intent: Intent,
bpc: bool,
adaptation_state: f64,
) -> Result<(Matrix3<f64>, Vector3<f64>), LinkError> {
let i = i as usize;
if intent == Intent::AbsoluteColorimetric {
let prev_profile = &profiles[i - 1];
let profile = &profiles[i];
let wp_in = prev_profile.media_white_point();
let cam_in = prev_profile.adaptation_matrix();
let wp_out = profile.media_white_point();
let cam_out = profile.adaptation_matrix();
Ok((
compute_absolute_intent(adaptation_state, wp_in, cam_in, wp_out, cam_out, i)?,
Vector3::zero(),
))
} else if bpc {
let bp_in = detect_black_point(&profiles[i - 1], intent, 0);
let bp_out = detect_dest_black_point(&profiles[i], intent, 0);
if bp_in != bp_out {
let (m, off) = compute_black_point_compensation(bp_in, bp_out);
Ok((m, off / Cxyz::MAX_ENCODABLE))
} else {
Ok((Matrix3::identity(), Vector3::zero()))
}
} else {
Ok((Matrix3::identity(), Vector3::zero()))
}
}
fn is_empty_layer(m: Matrix3<f64>, off: Vector3<f64>) -> bool {
let ident = Matrix3::<f64>::identity();
let mut diff = 0.;
for i in 0..9 {
diff += (m[i / 3][i % 3] - ident[i / 3][i % 3]).abs();
}
for i in 0..3 {
diff += off[i].abs();
}
diff < 0.002
}
fn add_conversion(
result: &mut Pipeline,
i: usize,
in_pcs: ColorSpace,
out_pcs: ColorSpace,
mat: Matrix3<f64>,
off: Vector3<f64>,
) -> Result<(), LinkError> {
match (in_pcs, out_pcs) {
(ColorSpace::XYZ, ColorSpace::XYZ) => {
if !is_empty_layer(mat, off) {
result.append_stage(PipelineStage::new_matrix3(mat, Some(off)))?;
}
}
(ColorSpace::XYZ, ColorSpace::Lab) => {
if !is_empty_layer(mat, off) {
result.append_stage(PipelineStage::new_matrix3(mat, Some(off)))?;
}
result.append_stage(PipelineStage::new_xyz_to_lab())?;
}
(ColorSpace::Lab, ColorSpace::XYZ) => {
result.append_stage(PipelineStage::new_lab_to_xyz())?;
if !is_empty_layer(mat, off) {
result.append_stage(PipelineStage::new_matrix3(mat, Some(off)))?;
}
}
(ColorSpace::Lab, ColorSpace::Lab) => {
if !is_empty_layer(mat, off) {
result.append_stage(PipelineStage::new_lab_to_xyz())?;
result.append_stage(PipelineStage::new_matrix3(mat, Some(off)))?;
result.append_stage(PipelineStage::new_xyz_to_lab())?;
}
}
_ => {
if in_pcs != out_pcs {
return Err(LinkError::IncompatibleSpaces(i, in_pcs, out_pcs));
}
}
}
Ok(())
}
fn compute_black_point_compensation(
black_point_in: Cxyz,
black_point_out: Cxyz,
) -> (Matrix3<f64>, Vector3<f64>) {
let tx = black_point_in.x - D50.x;
let ty = black_point_in.y - D50.y;
let tz = black_point_in.z - D50.z;
let ax = (black_point_out.x - D50.x) / tx;
let ay = (black_point_out.y - D50.y) / ty;
let az = (black_point_out.z - D50.z) / tz;
let bx = -D50.x * (black_point_out.x - black_point_in.x) / tx;
let by = -D50.y * (black_point_out.y - black_point_in.y) / ty;
let bz = -D50.z * (black_point_out.z - black_point_in.z) / tz;
(
Matrix3::from_diagonal((ax, ay, az).into()),
Vector3::new(bx, by, bz),
)
}
fn compute_absolute_intent(
adaptation_state: f64,
wp_in: Cxyz,
cam_in: Matrix3<f64>,
wp_out: Cxyz,
cam_out: Matrix3<f64>,
i: usize,
) -> Result<Matrix3<f64>, LinkError> {
if adaptation_state == 1. {
Ok(Matrix3::from_diagonal(
(wp_in.x / wp_out.x, wp_in.y / wp_out.y, wp_in.z / wp_out.z).into(),
))
} else {
let scale = Matrix3::from_diagonal(
(wp_in.x / wp_out.x, wp_in.y / wp_out.y, wp_in.z / wp_out.z).into(),
);
if adaptation_state == 0. {
let m2 = lcms_mat3_per(cam_out, scale);
let cam_in_inv = match cam_in.invert() {
Some(m) => m,
None => return Err(LinkError::AbsoluteIntentError(i)),
};
Ok(lcms_mat3_per(m2, cam_in_inv))
} else {
let m2 = match cam_in.invert() {
Some(m) => m,
None => return Err(LinkError::AbsoluteIntentError(i)),
};
let m3 = lcms_mat3_per(m2, scale);
let temp_src = match chad_to_temp(cam_in) {
Some(v) => v,
None => return Err(LinkError::AbsoluteIntentError(i)),
};
let temp_dest = match chad_to_temp(cam_out) {
Some(v) => v,
None => return Err(LinkError::AbsoluteIntentError(i)),
};
if temp_src < 0. || temp_dest < 0. {
return Err(LinkError::AbsoluteIntentError(i));
}
if scale.is_identity() && (temp_src - temp_dest).abs() < 0.01 {
return Ok(Matrix3::identity());
}
let temp = (1. - adaptation_state) * temp_dest + adaptation_state * temp_src;
let mixed_chad = match temp_to_chad(temp) {
Some(m) => m,
None => return Err(LinkError::AbsoluteIntentError(i)),
};
Ok(lcms_mat3_per(m3, mixed_chad))
}
}
}
fn chad_to_temp(chad: Matrix3<f64>) -> Option<f64> {
let inverse = match chad.invert() {
Some(inverse) => inverse,
None => return None,
};
let d50_xyz = D50;
let s = Vector3::new(d50_xyz.x, d50_xyz.y, d50_xyz.z);
let d = lcms_mat3_eval(inverse, s);
let dest: CxyY = Cxyz {
x: d.x,
y: d.y,
z: d.z,
}
.into();
dest.to_temp()
}
fn temp_to_chad(temp: f64) -> Option<Matrix3<f64>> {
let chromaticity_of_white: Cxyz = match CxyY::from_temp(temp) {
Some(c) => c.into(),
None => return None,
};
chromaticity_of_white.adaptation_matrix(D50, None)
}