const PQ_M1: f64 = 2610.0 / 16384.0;
const PQ_M2: f64 = 2523.0 / 4096.0 * 128.0;
const PQ_C1: f64 = 3424.0 / 4096.0;
const PQ_C2: f64 = 2413.0 / 4096.0 * 32.0;
const PQ_C3: f64 = 2392.0 / 4096.0 * 32.0;
const HLG_A: f64 = 0.17883277;
const HLG_B: f64 = 0.28466892;
const HLG_C: f64 = 0.55991073;
fn pq_oetf_f64(y: f64) -> f64 {
if y <= 0.0 {
return 0.0;
}
let ym1 = y.powf(PQ_M1);
((PQ_C1 + PQ_C2 * ym1) / (1.0 + PQ_C3 * ym1)).powf(PQ_M2)
}
fn pq_eotf_f64(e: f64) -> f64 {
if e <= 0.0 {
return 0.0;
}
let em2 = e.powf(1.0 / PQ_M2);
let num = (em2 - PQ_C1).max(0.0);
let den = PQ_C2 - PQ_C3 * em2;
if den <= 0.0 {
return 0.0;
}
(num / den).powf(1.0 / PQ_M1)
}
fn hlg_oetf_f64(e: f64) -> f64 {
if e <= 0.0 {
return 0.0;
}
if e <= 1.0 / 12.0 {
(3.0 * e).sqrt()
} else {
HLG_A * (12.0 * e - HLG_B).ln() + HLG_C
}
}
fn hlg_inv_oetf_f64(v: f64) -> f64 {
if v <= 0.0 {
return 0.0;
}
if v <= 0.5 {
v * v / 3.0
} else {
((v - HLG_C) / HLG_A).exp() / 12.0 + HLG_B / 12.0
}
}
const PQ_TOL: f32 = 1e-5;
const HLG_TOL: f32 = 1e-5;
#[test]
fn pq_oetf_matches_st2084_formula() {
let test_nits: &[f32] = &[
0.0, 0.001, 0.01, 0.05, 0.1, 0.5, 1.0, 5.0, 10.0, 50.0, 100.0, 203.0, 500.0, 1000.0,
4000.0, 10000.0,
];
for &nits in test_nits {
let linear = nits / 10000.0;
let zt = linear_srgb::tf::linear_to_pq(linear);
let reference = pq_oetf_f64(linear as f64) as f32;
let err = (zt - reference).abs();
assert!(
err < PQ_TOL,
"PQ OETF at {nits} nits: linear-srgb={zt}, ST.2084 f64={reference}, err={err}"
);
}
}
#[test]
fn pq_eotf_matches_st2084_formula() {
let test_pq: &[f32] = &[0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.58, 0.6, 0.7, 0.8, 0.9, 1.0];
for &pq in test_pq {
let zt = linear_srgb::tf::pq_to_linear(pq);
let reference = pq_eotf_f64(pq as f64) as f32;
let err = (zt - reference).abs();
assert!(
err < PQ_TOL,
"PQ EOTF at PQ={pq}: linear-srgb={zt}, ST.2084 f64={reference}, err={err}"
);
}
}
#[test]
fn pq_roundtrip_preserves_value() {
for i in 0..=20 {
let linear = i as f32 / 20.0;
let pq = linear_srgb::tf::linear_to_pq(linear);
let back = linear_srgb::tf::pq_to_linear(pq);
let err = (back - linear).abs();
assert!(
err < PQ_TOL,
"PQ roundtrip at linear={linear}: back={back}, err={err}"
);
}
}
#[test]
fn pq_known_reference_points() {
let sdr_pq = linear_srgb::tf::linear_to_pq(203.0 / 10000.0);
assert!(
(sdr_pq - 0.58).abs() < 0.01,
"SDR white (203 nits) should be PQ ~0.58, got {sdr_pq}"
);
let peak_pq = linear_srgb::tf::linear_to_pq(1.0);
assert!(
(peak_pq - 1.0).abs() < 1e-5,
"10000 nits should be PQ 1.0, got {peak_pq}"
);
let black_pq = linear_srgb::tf::linear_to_pq(0.0);
assert!(
black_pq.abs() < 0.001,
"0 nits should be PQ ~0, got {black_pq}"
);
}
#[test]
fn hlg_oetf_matches_bt2100_formula() {
let test_vals: &[f32] = &[0.0, 0.001, 0.01, 1.0 / 12.0, 0.1, 0.25, 0.5, 0.75, 1.0];
for &v in test_vals {
let zt = linear_srgb::tf::linear_to_hlg(v);
let reference = hlg_oetf_f64(v as f64) as f32;
let err = (zt - reference).abs();
assert!(
err < HLG_TOL,
"HLG OETF at {v}: linear-srgb={zt}, BT.2100 f64={reference}, err={err}"
);
}
}
#[test]
fn hlg_inv_oetf_matches_bt2100_formula() {
let test_vals: &[f32] = &[0.0, 0.1, 0.25, 0.5, 0.75, 1.0];
for &v in test_vals {
let zt = linear_srgb::tf::hlg_to_linear(v);
let reference = hlg_inv_oetf_f64(v as f64) as f32;
let err = (zt - reference).abs();
assert!(
err < HLG_TOL,
"HLG inv OETF at {v}: linear-srgb={zt}, BT.2100 f64={reference}, err={err}"
);
}
}
#[test]
fn hlg_roundtrip_preserves_value() {
for i in 0..=20 {
let linear = i as f32 / 20.0;
let hlg = linear_srgb::tf::linear_to_hlg(linear);
let back = linear_srgb::tf::hlg_to_linear(hlg);
let err = (back - linear).abs();
assert!(
err < HLG_TOL,
"HLG roundtrip at linear={linear}: back={back}, err={err}"
);
}
}
#[cfg(feature = "std")]
#[test]
fn libm_matches_std_for_zentone_math() {
let test_vals = [0.001f32, 0.01, 0.1, 0.5, 1.0, 2.0, 4.0, 10.0];
for &x in &test_vals {
let std_pow = x.powf(2.4);
let libm_pow = libm::powf(x, 2.4);
assert!(
(std_pow - libm_pow).abs() < 1e-6,
"powf({x}, 2.4): std={std_pow}, libm={libm_pow}"
);
let std_log2 = x.log2();
let libm_log2 = libm::log2f(x);
assert!(
(std_log2 - libm_log2).abs() < 1e-6,
"log2({x}): std={std_log2}, libm={libm_log2}"
);
let std_sqrt = x.sqrt();
let libm_sqrt = libm::sqrtf(x);
assert!(
(std_sqrt - libm_sqrt).abs() < 1e-7,
"sqrt({x}): std={std_sqrt}, libm={libm_sqrt}"
);
if x < 5.0 {
let std_exp = x.exp();
let libm_exp = libm::expf(x);
assert!(
(std_exp - libm_exp).abs() / std_exp.abs().max(1e-10) < 1e-6,
"exp({x}): std={std_exp}, libm={libm_exp}"
);
}
}
}