use crate::cicp::{ColourPrimaries, TransferCharacteristics};
use crate::oklab::{Gamut, linear_rgb_to_oklab};
use crate::transfer::{
SDR_REFERENCE_WHITE_NITS, adobe_rgb_eotf, bt2020_pq_to_sdr, prophoto_rgb_eotf, srgb_eotf,
};
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum ToneMap {
Reinhard {
reference_white_nits: f64,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SourceTransfer {
Srgb,
AdobeRgb,
ProPhotoRgb,
Bt2020Pq,
}
impl SourceTransfer {
#[must_use]
pub fn eotf(self, x: f64) -> f64 {
match self {
SourceTransfer::Srgb => srgb_eotf(x),
SourceTransfer::AdobeRgb => adobe_rgb_eotf(x),
SourceTransfer::ProPhotoRgb => prophoto_rgb_eotf(x),
SourceTransfer::Bt2020Pq => bt2020_pq_to_sdr(x),
}
}
#[must_use]
pub fn cicp(self) -> Option<TransferCharacteristics> {
match self {
SourceTransfer::Srgb => Some(TransferCharacteristics::Srgb),
SourceTransfer::Bt2020Pq => Some(TransferCharacteristics::Pq),
SourceTransfer::AdobeRgb | SourceTransfer::ProPhotoRgb => None,
}
}
#[must_use]
pub fn tonemap(self) -> Option<ToneMap> {
match self {
SourceTransfer::Bt2020Pq => Some(ToneMap::Reinhard {
reference_white_nits: SDR_REFERENCE_WHITE_NITS,
}),
_ => None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SourceProfile {
pub gamut: Gamut,
pub transfer: SourceTransfer,
}
impl SourceProfile {
pub const SRGB: Self = Self {
gamut: Gamut::Srgb,
transfer: SourceTransfer::Srgb,
};
pub const DISPLAY_P3: Self = Self {
gamut: Gamut::DisplayP3,
transfer: SourceTransfer::Srgb,
};
pub const ADOBE_RGB: Self = Self {
gamut: Gamut::AdobeRgb,
transfer: SourceTransfer::AdobeRgb,
};
pub const BT2020: Self = Self {
gamut: Gamut::Bt2020,
transfer: SourceTransfer::Bt2020Pq,
};
pub const PROPHOTO_RGB: Self = Self {
gamut: Gamut::ProPhotoRgb,
transfer: SourceTransfer::ProPhotoRgb,
};
#[must_use]
pub fn colour_primaries(self) -> Option<ColourPrimaries> {
match self.gamut {
Gamut::Srgb => Some(ColourPrimaries::Bt709),
Gamut::DisplayP3 => Some(ColourPrimaries::DisplayP3),
Gamut::Bt2020 => Some(ColourPrimaries::Bt2020),
Gamut::AdobeRgb | Gamut::ProPhotoRgb => None,
}
}
#[must_use]
pub fn transfer_characteristics(self) -> Option<TransferCharacteristics> {
self.transfer.cicp()
}
#[must_use]
pub fn tonemap(self) -> Option<ToneMap> {
self.transfer.tonemap()
}
#[must_use]
pub fn eotf(self, x: f64) -> f64 {
self.transfer.eotf(x)
}
#[must_use]
pub fn gamma_rgb_to_oklab(self, rgb: [f64; 3]) -> [f64; 3] {
let linear = [self.eotf(rgb[0]), self.eotf(rgb[1]), self.eotf(rgb[2])];
linear_rgb_to_oklab(linear, self.gamut)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn srgb_decomposes_to_cicp_axes() {
let p = SourceProfile::SRGB;
assert_eq!(p.colour_primaries(), Some(ColourPrimaries::Bt709));
assert_eq!(
p.transfer_characteristics(),
Some(TransferCharacteristics::Srgb)
);
assert_eq!(p.tonemap(), None);
}
#[test]
fn bt2020_bundles_primaries_pq_and_reinhard() {
let p = SourceProfile::BT2020;
assert_eq!(p.colour_primaries(), Some(ColourPrimaries::Bt2020));
assert_eq!(
p.transfer_characteristics(),
Some(TransferCharacteristics::Pq)
);
assert_eq!(
p.tonemap(),
Some(ToneMap::Reinhard {
reference_white_nits: 203.0
})
);
}
#[test]
fn adobe_and_prophoto_have_no_cicp_code_points() {
for p in [SourceProfile::ADOBE_RGB, SourceProfile::PROPHOTO_RGB] {
assert_eq!(p.colour_primaries(), None);
assert_eq!(p.transfer_characteristics(), None);
assert_eq!(p.tonemap(), None);
}
assert_eq!(
SourceProfile::DISPLAY_P3.colour_primaries(),
Some(ColourPrimaries::DisplayP3)
);
}
#[test]
fn eotf_is_encoder_exact_per_gamut() {
assert_eq!(SourceProfile::ADOBE_RGB.eotf(0.5), 0.5_f64.powf(2.2));
assert_eq!(SourceProfile::PROPHOTO_RGB.eotf(0.5), 0.5_f64.powf(1.8));
assert_eq!(SourceProfile::BT2020.eotf(0.5), bt2020_pq_to_sdr(0.5));
}
#[test]
fn matches_chromahash_gamma_pipeline_vectors() {
let cases: &[([f64; 3], [f64; 3])] = &[
(
[1.0, 0.0, 0.0],
[0.6279553606145517, 0.224863061065974, 0.12584629853073515],
),
(
[0.5, 0.5, 0.5],
[
0.5981807266228486,
0.000000000048424320109319297,
0.000000022296533230825588,
],
),
];
for &(gamma_rgb, want) in cases {
let got = SourceProfile::SRGB.gamma_rgb_to_oklab(gamma_rgb);
for (i, (&g, &w)) in got.iter().zip(want.iter()).enumerate() {
assert!((g - w).abs() < 1e-9, "oklab[{i}] = {g}, want {w}");
}
}
}
}