use crate::cicp::TransferCharacteristics;
pub const SDR_REFERENCE_WHITE_NITS: f64 = 203.0;
pub const PQ_PEAK_NITS: f64 = 10_000.0;
#[must_use]
pub fn srgb_eotf(x: f64) -> f64 {
if x <= 0.04045 {
x / 12.92
} else {
((x + 0.055) / 1.055).powf(2.4)
}
}
#[must_use]
pub fn srgb_oetf(x: f64) -> f64 {
if x <= 0.0031308 {
12.92 * x
} else {
1.055 * x.powf(1.0 / 2.4) - 0.055
}
}
#[must_use]
pub fn adobe_rgb_eotf(x: f64) -> f64 {
x.powf(2.2)
}
#[must_use]
pub fn adobe_rgb_eotf_standard(x: f64) -> f64 {
x.powf(563.0 / 256.0)
}
#[must_use]
pub fn prophoto_rgb_eotf(x: f64) -> f64 {
x.powf(1.8)
}
#[must_use]
pub fn prophoto_rgb_eotf_standard(x: f64) -> f64 {
if x < 1.0 / 32.0 {
x / 16.0
} else {
x.powf(1.8)
}
}
#[must_use]
pub fn pq_eotf(x: f64) -> f64 {
const M1: f64 = 0.1593017578125;
const M2: f64 = 78.84375;
const C1: f64 = 0.8359375;
const C2: f64 = 18.8515625;
const C3: f64 = 18.6875;
let n = x.powf(1.0 / M2);
let num = (n - C1).max(0.0);
let den = C2 - C3 * n;
let y_normalized = (num / den).powf(1.0 / M1);
y_normalized * PQ_PEAK_NITS
}
#[must_use]
pub fn bt2020_pq_to_sdr(x: f64) -> f64 {
let l = pq_eotf(x) / SDR_REFERENCE_WHITE_NITS;
l / (1.0 + l)
}
#[must_use]
pub fn eotf_for(tc: TransferCharacteristics) -> Option<fn(f64) -> f64> {
match tc {
TransferCharacteristics::Srgb => Some(srgb_eotf),
TransferCharacteristics::Pq | TransferCharacteristics::Bt2020_10 => Some(bt2020_pq_to_sdr),
TransferCharacteristics::Bt709
| TransferCharacteristics::Hlg
| TransferCharacteristics::Unspecified => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn srgb_roundtrip() {
for &x in &[0.0, 0.01, 0.04045, 0.1, 0.5, 0.9, 1.0] {
let back = srgb_oetf(srgb_eotf(x));
assert!((back - x).abs() < 1e-4, "sRGB roundtrip at {x}: {back}");
}
}
#[test]
fn srgb_piecewise_continuous_at_thresholds() {
assert!((srgb_eotf(0.04045) - 0.04045 / 12.92).abs() < 1e-12);
assert!((srgb_oetf(0.0031308) - 12.92 * 0.0031308).abs() < 1e-12);
}
#[test]
fn boundaries_map_zero_and_one() {
for f in [
srgb_eotf as fn(f64) -> f64,
srgb_oetf,
adobe_rgb_eotf,
adobe_rgb_eotf_standard,
prophoto_rgb_eotf,
prophoto_rgb_eotf_standard,
] {
assert_eq!(f(0.0), 0.0);
assert!((f(1.0) - 1.0).abs() < 1e-12, "f(1.0) should be 1.0");
}
}
#[test]
fn adobe_and_prophoto_encoder_exact_vs_standard_differ() {
let x = 0.5;
assert!((adobe_rgb_eotf(x) - adobe_rgb_eotf_standard(x)).abs() > 1e-4);
assert!((prophoto_rgb_eotf(0.02) - prophoto_rgb_eotf_standard(0.02)).abs() > 1e-4);
}
#[test]
fn prophoto_standard_toe_is_linear() {
assert!((prophoto_rgb_eotf_standard(1.0 / 64.0) - (1.0 / 64.0) / 16.0).abs() < 1e-15);
}
#[test]
fn pq_eotf_endpoints() {
assert_eq!(pq_eotf(0.0), 0.0);
assert!(
(pq_eotf(1.0) - PQ_PEAK_NITS).abs() < 1e-6,
"{}",
pq_eotf(1.0)
);
}
#[test]
fn bt2020_pq_to_sdr_matches_reference_formula() {
fn reference(x: f64) -> f64 {
const M1: f64 = 0.1593017578125;
const M2: f64 = 78.84375;
const C1: f64 = 0.8359375;
const C2: f64 = 18.8515625;
const C3: f64 = 18.6875;
let n = x.powf(1.0 / M2);
let num = (n - C1).max(0.0);
let den = C2 - C3 * n;
let y = (num / den).powf(1.0 / M1);
let l = y * 10000.0 / 203.0;
l / (1.0 + l)
}
for &x in &[0.0, 0.05, 0.1, 0.25, 0.5, 0.6, 0.75, 0.9, 1.0] {
assert!((bt2020_pq_to_sdr(x) - reference(x)).abs() < 1e-12, "at {x}");
}
}
#[test]
fn bt2020_pq_full_signal_near_one() {
let max = bt2020_pq_to_sdr(1.0);
assert!(max > 0.9 && max < 1.0, "PQ(1.0) → {max}");
}
#[test]
fn eotf_for_dispatch() {
assert!(eotf_for(TransferCharacteristics::Srgb).is_some());
assert!(eotf_for(TransferCharacteristics::Pq).is_some());
assert!(eotf_for(TransferCharacteristics::Bt2020_10).is_some());
assert!(eotf_for(TransferCharacteristics::Hlg).is_none());
assert!(eotf_for(TransferCharacteristics::Bt709).is_none());
let f = eotf_for(TransferCharacteristics::Srgb).unwrap();
assert_eq!(f(0.5), srgb_eotf(0.5));
}
}