#![allow(dead_code)]
use crate::error::{CalibrationError, CalibrationResult};
pub const PQ_PEAK_NITS: f64 = 10_000.0;
const PQ_M1: f64 = 0.159_301_758;
const PQ_M2: f64 = 78.843_75;
const PQ_C1: f64 = 0.835_937_5;
const PQ_C2: f64 = 18.851_563;
const PQ_C3: f64 = 18.6875;
const HLG_GAMMA: f64 = 1.2;
const HLG_A: f64 = 0.178_832_77;
const HLG_B: f64 = 0.284_668_92;
const HLG_C: f64 = 0.559_910_73;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum HdrTf {
Pq,
Hlg,
}
impl HdrTf {
#[must_use]
pub fn name(&self) -> &'static str {
match self {
Self::Pq => "ST-2084 PQ",
Self::Hlg => "ARIB STD-B67 HLG",
}
}
}
#[must_use]
pub fn pq_encode(luminance_nits: f64) -> f64 {
let y = (luminance_nits / PQ_PEAK_NITS).clamp(0.0, 1.0);
let ym = y.powf(PQ_M1);
let num = PQ_C1 + PQ_C2 * ym;
let den = 1.0 + PQ_C3 * ym;
(num / den).powf(PQ_M2)
}
#[must_use]
pub fn pq_decode(code: f64) -> f64 {
let code = code.clamp(0.0, 1.0);
let vm = code.powf(1.0 / PQ_M2);
let num = (vm - PQ_C1).max(0.0);
let den = PQ_C2 - PQ_C3 * vm;
if den <= 0.0 {
return 0.0;
}
(num / den).powf(1.0 / PQ_M1) * PQ_PEAK_NITS
}
#[must_use]
pub fn hlg_encode(e: f64) -> f64 {
let e = e.max(0.0);
if e <= 1.0 / 12.0 {
(3.0 * e).sqrt()
} else {
HLG_A * (12.0 * e - HLG_B).ln() + HLG_C
}
}
#[must_use]
pub fn hlg_decode(e: f64) -> f64 {
let e = e.clamp(0.0, 1.0);
if e <= 0.5 {
e * e / 3.0
} else {
(((e - HLG_C) / HLG_A).exp() + HLG_B) / 12.0
}
}
#[must_use]
pub fn pq_remap_peak(code: f64, src_peak: f64, dst_peak: f64) -> f64 {
if src_peak <= 0.0 || dst_peak <= 0.0 {
return code.clamp(0.0, 1.0);
}
let linear = pq_decode(code);
let scaled = linear * (dst_peak / src_peak);
pq_encode(scaled.min(PQ_PEAK_NITS))
}
#[derive(Debug, Clone)]
pub struct LuminanceMeasurement {
pub code: f64,
pub measured_nits: f64,
}
#[derive(Debug, Clone)]
pub struct HdrCalibrationResult {
pub tf: HdrTf,
pub peak_nits: f64,
pub lut_size: usize,
pub correction: Vec<f64>,
pub avg_error: f64,
pub max_error: f64,
}
impl HdrCalibrationResult {
#[must_use]
pub fn apply(&self, code: f64) -> f64 {
let code = code.clamp(0.0, 1.0);
if self.lut_size < 2 {
return code;
}
let scale = (self.lut_size - 1) as f64;
let pos = code * scale;
let lo = pos.floor() as usize;
let hi = (lo + 1).min(self.lut_size - 1);
let frac = pos - lo as f64;
self.correction[lo] * (1.0 - frac) + self.correction[hi] * frac
}
}
#[derive(Debug, Clone)]
pub struct HdrCalibrator {
pub tf: HdrTf,
pub peak_nits: f64,
pub lut_size: usize,
}
impl HdrCalibrator {
#[must_use]
pub fn new(tf: HdrTf, peak_nits: f64, lut_size: usize) -> Self {
Self {
tf,
peak_nits,
lut_size: lut_size.max(2),
}
}
#[must_use]
pub fn hdr10_1000nit() -> Self {
Self::new(HdrTf::Pq, 1000.0, 1024)
}
#[must_use]
pub fn hlg_broadcast() -> Self {
Self::new(HdrTf::Hlg, 1000.0, 1024)
}
pub fn calibrate(
&self,
measurements: &[LuminanceMeasurement],
) -> CalibrationResult<HdrCalibrationResult> {
if measurements.len() < 2 {
return Err(CalibrationError::InsufficientData(
"HDR calibration requires at least 2 luminance measurements".to_string(),
));
}
for w in measurements.windows(2) {
if w[1].code < w[0].code {
return Err(CalibrationError::InvalidMeasurement(
"Measurements must be sorted by ascending code".to_string(),
));
}
}
let n = self.lut_size;
let mut correction = Vec::with_capacity(n);
let mut total_err = 0.0_f64;
let mut max_err = 0.0_f64;
for idx in 0..n {
let ideal_code = idx as f64 / (n - 1) as f64;
let ideal_nits = self.decode_to_nits(ideal_code);
let corrected_code = self.inverse_interpolate(measurements, ideal_nits);
correction.push(corrected_code);
let err = (corrected_code - ideal_code).abs();
total_err += err;
if err > max_err {
max_err = err;
}
}
Ok(HdrCalibrationResult {
tf: self.tf,
peak_nits: self.peak_nits,
lut_size: n,
correction,
avg_error: total_err / n as f64,
max_error: max_err,
})
}
fn decode_to_nits(&self, code: f64) -> f64 {
match self.tf {
HdrTf::Pq => {
let abs_nits = pq_decode(code);
abs_nits.min(self.peak_nits)
}
HdrTf::Hlg => {
hlg_decode(code) * self.peak_nits
}
}
}
fn inverse_interpolate(&self, measurements: &[LuminanceMeasurement], target_nits: f64) -> f64 {
let first_nits = measurements.first().map(|m| m.measured_nits).unwrap_or(0.0);
let last_nits = measurements.last().map(|m| m.measured_nits).unwrap_or(0.0);
if target_nits <= first_nits {
return measurements.first().map(|m| m.code).unwrap_or(0.0);
}
if target_nits >= last_nits {
return measurements.last().map(|m| m.code).unwrap_or(1.0);
}
for w in measurements.windows(2) {
let lo = &w[0];
let hi = &w[1];
if target_nits >= lo.measured_nits && target_nits <= hi.measured_nits {
let dn = hi.measured_nits - lo.measured_nits;
if dn.abs() < 1e-12 {
return lo.code;
}
let frac = (target_nits - lo.measured_nits) / dn;
return lo.code + frac * (hi.code - lo.code);
}
}
measurements.last().map(|m| m.code).unwrap_or(1.0)
}
}
#[derive(Debug, Clone)]
pub struct Hdr10Metadata {
pub display_primaries: [(u16, u16); 3],
pub white_point: (u16, u16),
pub max_mastering_luminance: u32,
pub min_mastering_luminance: u32,
pub max_cll: u16,
pub max_fall: u16,
}
impl Hdr10Metadata {
#[must_use]
pub fn p3_d65_1000nit() -> Self {
Self {
display_primaries: [
(34_000, 16_000), (13_250, 34_500), (7_500, 3_000), ],
white_point: (15_635, 16_450), max_mastering_luminance: 10_000_000, min_mastering_luminance: 10, max_cll: 1_000,
max_fall: 400,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_pq_encode_decode_roundtrip() {
for &nits in &[0.005_f64, 1.0, 100.0, 1_000.0, 4_000.0] {
let code = pq_encode(nits);
let back = pq_decode(code);
let rel = (back - nits).abs() / (nits + 1.0);
assert!(
rel < 1e-9,
"PQ round-trip failed for {nits} nits: code={code:.6}, decoded={back:.4}"
);
}
let code = pq_encode(10_000.0);
let back = pq_decode(code);
let rel = (back - 10_000.0).abs() / 10_001.0;
assert!(
rel < 1e-4,
"PQ round-trip failed for 10000 nits: code={code:.6}, decoded={back:.4}"
);
}
#[test]
fn test_pq_zero_luminance_encodes_to_zero() {
let code = pq_encode(0.0);
let decoded = pq_decode(0.0);
assert!(
code.abs() < 0.01,
"PQ encode(0) should be near 0, got {code}"
);
assert!(
decoded.abs() < 0.01,
"PQ decode(0) should be near 0 nits, got {decoded}"
);
}
#[test]
fn test_pq_encode_monotone() {
let values = [0.0, 10.0, 100.0, 1_000.0, 4_000.0, 10_000.0];
let encoded: Vec<f64> = values.iter().map(|&v| pq_encode(v)).collect();
for w in encoded.windows(2) {
assert!(w[1] > w[0], "PQ encode must be monotonically increasing");
}
}
#[test]
fn test_hlg_encode_decode_roundtrip() {
for &e in &[0.0_f64, 0.01, 0.1, 0.5, 1.0] {
let code = hlg_encode(e);
let back = hlg_decode(code);
let err = (back - e).abs();
assert!(
err < 1e-9,
"HLG round-trip failed for e={e}: code={code:.6}, decoded={back:.9}, err={err:.2e}"
);
}
let c1 = hlg_encode(1.0);
let c2 = hlg_encode(2.0);
assert!(c2 > c1, "HLG encode must be monotone beyond 1.0");
}
#[test]
fn test_hlg_encode_monotone() {
let values = [0.0, 0.05, 0.1, 0.5, 1.0, 2.0, 4.0];
let encoded: Vec<f64> = values.iter().map(|&v| hlg_encode(v)).collect();
for w in encoded.windows(2) {
assert!(w[1] > w[0], "HLG encode must be monotonically increasing");
}
}
#[test]
fn test_pq_remap_peak_identity() {
let code = pq_encode(500.0);
let remapped = pq_remap_peak(code, 1_000.0, 1_000.0);
assert!(
(remapped - code).abs() < 1e-9,
"Remapping to same peak should be identity: {code:.6} vs {remapped:.6}"
);
}
#[test]
fn test_pq_remap_peak_lower() {
let code_4k = pq_encode(3_000.0);
let remapped = pq_remap_peak(code_4k, 4_000.0, 1_000.0);
let code_1k = pq_encode(1_000.0);
assert!(
remapped <= code_1k + 1e-9,
"Remapped code {remapped:.6} must be ≤ {code_1k:.6}"
);
}
#[test]
fn test_hdr_calibrator_ideal_display_is_identity() {
let cal = HdrCalibrator::hdr10_1000nit();
let measurements: Vec<LuminanceMeasurement> = (0..=100)
.map(|i| {
let code = i as f64 / 100.0;
let nits = pq_decode(code).min(1_000.0);
LuminanceMeasurement {
code,
measured_nits: nits,
}
})
.collect();
let result = cal
.calibrate(&measurements)
.expect("Calibration should succeed");
for i in 0..result.lut_size {
let ideal = i as f64 / (result.lut_size - 1) as f64;
let corrected = result.correction[i];
if pq_decode(ideal) >= 1_000.0 {
continue;
}
assert!(
(corrected - ideal).abs() < 0.05,
"Ideal display correction [{i}]: ideal={ideal:.4}, corrected={corrected:.4}"
);
}
}
#[test]
fn test_hdr_calibrator_requires_at_least_2_measurements() {
let cal = HdrCalibrator::hdr10_1000nit();
let single = vec![LuminanceMeasurement {
code: 0.5,
measured_nits: 100.0,
}];
let result = cal.calibrate(&single);
assert!(
result.is_err(),
"Should fail with fewer than 2 measurements"
);
}
#[test]
fn test_hdr_calibration_apply_clamps() {
let cal = HdrCalibrator::hdr10_1000nit();
let measurements: Vec<LuminanceMeasurement> = vec![
LuminanceMeasurement {
code: 0.0,
measured_nits: 0.0,
},
LuminanceMeasurement {
code: 1.0,
measured_nits: 1_000.0,
},
];
let result = cal
.calibrate(&measurements)
.expect("Calibration should succeed");
let lo = result.apply(-0.1);
let hi = result.apply(1.5);
assert!((0.0..=1.0).contains(&lo), "apply must clamp low: {lo}");
assert!((0.0..=1.0).contains(&hi), "apply must clamp high: {hi}");
}
#[test]
fn test_hdr10_metadata_construction() {
let meta = Hdr10Metadata::p3_d65_1000nit();
assert_eq!(meta.max_cll, 1_000);
assert_eq!(meta.max_fall, 400);
}
#[test]
fn test_hdr_tf_names() {
assert_eq!(HdrTf::Pq.name(), "ST-2084 PQ");
assert_eq!(HdrTf::Hlg.name(), "ARIB STD-B67 HLG");
}
}