use crate::color::{CLab, Cxyz, D50};
use crate::link::link;
use crate::pipeline::Pipeline;
use crate::profile::{ColorSpace, IccProfile, Intent, IntentDirection, ProfileClass};
use crate::util::lcms_solve_matrix;
use cgmath::{Matrix3, Vector3};
const PERCEPTUAL_BLACK: Cxyz = Cxyz {
x: 0.00336,
y: 0.0034731,
z: 0.00287,
};
const ZERO: Cxyz = Cxyz {
x: 0.,
y: 0.,
z: 0.,
};
pub(crate) fn detect_black_point(profile: &IccProfile, intent: Intent, dw_flags: u32) -> Cxyz {
let dev_class = profile.device_class;
if dev_class == ProfileClass::Link
|| dev_class == ProfileClass::Abstract
|| dev_class == ProfileClass::NamedColor
{
return ZERO;
}
if intent != Intent::Perceptual
&& intent != Intent::RelativeColorimetric
&& intent != Intent::Saturation
{
return ZERO;
}
if profile.version > 0x4000000 && (intent == Intent::Perceptual || intent == Intent::Saturation)
{
if profile.is_matrix_shaper() {
return black_point_as_darker_colorant(profile, Intent::RelativeColorimetric, 0);
}
return PERCEPTUAL_BLACK;
}
if intent == Intent::RelativeColorimetric
&& dev_class == ProfileClass::Output
&& profile.color_space == ColorSpace::CMYK
{
return black_point_using_perceptual_black(profile);
}
return black_point_as_darker_colorant(profile, intent, dw_flags);
}
fn end_points_by_space(space: ColorSpace) -> Option<([f64; 4], [f64; 4])> {
let rgb_black = [0., 0., 0., 0.];
let rgb_white = [1., 1., 1., 0.];
let cmyk_black = [1., 1., 1., 1.]; let cmyk_white = [0., 0., 0., 0.];
let lab_black = [0., 0.5, 0.5, 0.]; let lab_white = [1., 0.5, 0.5, 0.];
let cmy_black = [1., 1., 1., 0.];
let cmy_white = [0., 0., 0., 0.];
let gray_black = [0.; 4];
let gray_white = [1., 0., 0., 0.];
match space {
ColorSpace::Gray => Some((gray_white, gray_black)),
ColorSpace::RGB => Some((rgb_white, rgb_black)),
ColorSpace::Lab => Some((lab_white, lab_black)),
ColorSpace::CMYK => Some((cmyk_white, cmyk_black)),
ColorSpace::CMY => Some((cmy_white, cmy_black)),
_ => None,
}
}
fn black_point_as_darker_colorant(profile: &IccProfile, intent: Intent, _flags: u32) -> Cxyz {
if !profile.is_intent_supported(intent, IntentDirection::Input) {
return ZERO;
}
let space = profile.color_space;
let (black, channels) = match end_points_by_space(space) {
Some(p) => p,
None => return ZERO,
};
if channels.len() != profile.color_space.channels() {
return ZERO;
}
let lab_profile = IccProfile::new_lab2(D50.into()).unwrap();
let pipeline = match link(
&[&profile, &lab_profile],
&[intent, intent],
&[false, false],
&[0., 0.],
) {
Ok(pipeline) => pipeline,
Err(_) => return ZERO,
};
let mut lab = CLab {
l: 0.,
a: 0.,
b: 0.,
};
pipeline.transform(&black, lab.as_slice_mut());
lab.a = 0.;
lab.b = 0.;
lab.l = lab.l.min(50.);
let black_xyz = lab.into_xyz(D50);
return black_xyz;
}
fn create_roundtrip_xform(profile: &IccProfile, intent: Intent) -> Option<Pipeline> {
let lab_profile = IccProfile::new_lab4(D50.into())?;
let bpc: [bool; 4] = [false, false, false, false];
let states: [f64; 4] = [1., 1., 1., 1.];
let profiles: [&IccProfile; 4] = [&lab_profile, &profile, &profile, &lab_profile];
let intents: [Intent; 4] = [
Intent::RelativeColorimetric,
intent,
Intent::RelativeColorimetric,
Intent::RelativeColorimetric,
];
link(&profiles, &intents, &bpc, &states).ok()
}
fn black_point_using_perceptual_black(profile: &IccProfile) -> Cxyz {
if !profile.is_intent_supported(Intent::Perceptual, IntentDirection::Input) {
return ZERO;
}
let round_trip = match create_roundtrip_xform(profile, Intent::Perceptual) {
Some(pipeline) => pipeline,
None => return ZERO,
};
let lab_in = CLab {
l: 0.,
a: 0.,
b: 0.,
};
let mut lab_out = CLab {
l: 0.,
a: 0.,
b: 0.,
};
round_trip.transform(lab_in.as_slice(), lab_out.as_slice_mut());
lab_out.a = 0.;
lab_out.b = 0.;
lab_out.l = lab_out.l.min(50.);
lab_out.into_xyz(D50)
}
pub(crate) fn detect_dest_black_point(profile: &IccProfile, intent: Intent, dw_flags: u32) -> Cxyz {
let dev_class = profile.device_class;
if dev_class == ProfileClass::Link
|| dev_class == ProfileClass::Abstract
|| dev_class == ProfileClass::NamedColor
{
return ZERO;
}
if intent != Intent::Perceptual
&& intent != Intent::RelativeColorimetric
&& intent != Intent::Saturation
{
return ZERO;
}
if profile.version >= 0x4000000
&& (intent == Intent::Perceptual || intent == Intent::Saturation)
{
if profile.is_matrix_shaper() {
return black_point_as_darker_colorant(profile, Intent::RelativeColorimetric, 0);
}
return PERCEPTUAL_BLACK;
}
let color_space = profile.color_space;
if !profile.is_clut(intent, IntentDirection::Output)
|| (color_space != ColorSpace::Gray
&& color_space != ColorSpace::RGB
&& color_space != ColorSpace::CMYK)
{
return detect_black_point(profile, intent, dw_flags);
}
let initial_lab = if intent == Intent::RelativeColorimetric {
let ini_xyz = detect_black_point(profile, intent, dw_flags);
ini_xyz.into_lab(D50)
} else {
CLab {
l: 0.,
a: 0.,
b: 0.,
}
};
let round_trip = match create_roundtrip_xform(profile, intent) {
Some(pipeline) => pipeline,
None => return ZERO,
};
let mut in_ramp = [0.; 256];
let mut out_ramp = [0.; 256];
for l in 0..256 {
let lab = CLab {
l: l as f64 * 100. / 255.,
a: initial_lab.a.max(-50.).min(50.),
b: initial_lab.b.max(-50.).min(50.),
};
let in_lab = [lab.l, lab.a, lab.b];
let mut out_lab = [0.; 3];
round_trip.transform(&in_lab, &mut out_lab);
in_ramp[l] = lab.l;
out_ramp[l] = out_lab[0];
}
for l in (1..=254).rev() {
out_ramp[l] = out_ramp[l].min(out_ramp[l + 1]);
}
if out_ramp[0] >= out_ramp[255] {
return ZERO;
}
let mut nearly_straight_midrange = true;
let min_l = out_ramp[0];
let max_l = out_ramp[255];
if intent == Intent::RelativeColorimetric {
for l in 0..256 {
if !(in_ramp[l] <= min_l + 0.2 * (max_l - min_l)
|| (in_ramp[l] - out_ramp[l]).abs() < 4.)
{
nearly_straight_midrange = false;
break;
}
}
if nearly_straight_midrange {
return initial_lab.into_xyz(D50);
}
}
let mut y_ramp = [0.; 256];
for l in 0..256 {
y_ramp[l] = (out_ramp[l] - min_l) / (max_l - min_l);
}
let (lo, hi) = if intent == Intent::RelativeColorimetric {
(0.1, 0.5)
} else {
(0.03, 0.25)
};
let mut x = [0.; 256];
let mut y = [0.; 256];
let mut n = 0;
for l in 0..256 {
let ff = y_ramp[l];
if ff >= lo && ff < hi {
x[n] = in_ramp[l];
y[n] = y_ramp[l];
n += 1;
}
}
if n < 3 {
return ZERO;
}
CLab {
l: root_of_least_squares_fit_quadratic_curve(n, x, y).max(0.),
a: initial_lab.a,
b: initial_lab.b,
}
.into_xyz(D50)
}
fn root_of_least_squares_fit_quadratic_curve(n: usize, x: [f64; 256], y: [f64; 256]) -> f64 {
if n < 4 {
return 0.;
}
let mut sum_x = 0.;
let mut sum_x2 = 0.;
let mut sum_x3 = 0.;
let mut sum_x4 = 0.;
let mut sum_y = 0.;
let mut sum_yx = 0.;
let mut sum_yx2 = 0.;
for i in 0..n {
let xn = x[i];
let yn = y[i];
sum_x += xn;
sum_x2 += xn * xn;
sum_x3 += xn * xn * xn;
sum_x4 += xn * xn * xn * xn;
sum_y += yn;
sum_yx += yn * xn;
sum_yx2 += yn * xn * xn;
}
let matrix = Matrix3::from_cols(
(n as f64, sum_x, sum_x2).into(),
(sum_x, sum_x2, sum_x3).into(),
(sum_x2, sum_x3, sum_x4).into(),
);
let vec = Vector3::new(sum_y, sum_yx, sum_yx2);
let res = match lcms_solve_matrix(matrix, vec) {
Some(res) => res,
None => return 0.,
};
let a = res[2];
let b = res[1];
let c = res[0];
if a.abs() < 1e-10 {
(-c / b).max(50.).min(0.)
} else {
let d = b * b - 4. * a * c;
if d <= 0. {
0.
} else {
let rt = (-b + d.sqrt()) / (2. * a);
rt.max(50.).min(0.)
}
}
}