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.178_832_77;
const HLG_B: f64 = 0.284_668_92;
const HLG_C: f64 = 0.559_910_73;
#[must_use]
pub fn pq_eotf(encoded: f64) -> f64 {
let encoded = encoded.clamp(0.0, 1.0);
let n = encoded.powf(1.0 / PQ_M2);
let num = (n - PQ_C1).max(0.0);
let den = PQ_C2 - (PQ_C3 * n);
if den <= 0.0 {
return 0.0;
}
(num / den).powf(1.0 / PQ_M1)
}
#[must_use]
pub fn pq_oetf(linear: f64) -> f64 {
let linear = linear.clamp(0.0, 1.0);
let n = linear.powf(PQ_M1);
((PQ_C1 + (PQ_C2 * n)) / (1.0 + (PQ_C3 * n))).powf(PQ_M2)
}
#[must_use]
pub fn hlg_oetf_inverse(encoded: f64) -> f64 {
let encoded = encoded.clamp(0.0, 1.0);
if encoded <= 0.5 {
(encoded * encoded) / 3.0
} else {
(((encoded - HLG_C) / HLG_A).exp() + HLG_B) / 12.0
}
}
#[must_use]
pub fn srgb_eotf(encoded: f64) -> f64 {
let encoded = encoded.clamp(0.0, 1.0);
if encoded <= 0.040_45 {
encoded / 12.92
} else {
((encoded + 0.055) / 1.055).powf(2.4)
}
}
#[must_use]
pub fn srgb_oetf(linear: f64) -> f64 {
let linear = linear.clamp(0.0, 1.0);
if linear <= 0.003_130_8 {
12.92 * linear
} else {
(1.055 * linear.powf(1.0 / 2.4)) - 0.055
}
}
#[must_use]
pub fn bt1886_oetf(linear: f64) -> f64 {
linear.max(0.0).powf(1.0 / 2.4)
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
#[repr(C)]
#[allow(
clippy::exhaustive_structs,
reason = "a value defined by its fields; there is nothing to add to it"
)]
pub struct ColorLut {
pub red: u16,
pub green: u16,
pub blue: u16,
pub reserved: u16,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(C)]
#[allow(
clippy::exhaustive_structs,
reason = "a value defined by its fields; there is nothing to add to it"
)]
pub struct ColorCtm {
pub matrix: [u64; 9],
}
const S31_32_ONE: u64 = 1 << 32;
const S31_32_SCALE: f64 = 4_294_967_296.0;
const S31_32_SIGN: u64 = 1 << 63;
const S31_32_MAX: u64 = (1 << 63) - 1;
const S31_32_MAX_AS_F64: f64 = 9_223_372_036_854_775_808.0;
#[must_use]
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
pub fn encode_s31_32(value: f64) -> u64 {
if value.is_nan() {
return 0;
}
let scaled = (value.abs() * S31_32_SCALE).round();
let magnitude = if scaled >= S31_32_MAX_AS_F64 {
S31_32_MAX
} else {
scaled as u64
};
magnitude | if value < 0.0 { S31_32_SIGN } else { 0 }
}
#[must_use]
pub fn decode_s31_32(encoded: u64) -> f64 {
#[allow(clippy::cast_precision_loss)] let magnitude = (encoded & !S31_32_SIGN) as f64 / S31_32_SCALE;
if encoded & S31_32_SIGN == 0 {
magnitude
} else {
-magnitude
}
}
#[must_use]
pub fn quantize_lut_value(normalized: f64) -> u16 {
if normalized.is_nan() || normalized <= 0.0 {
return 0;
}
if normalized >= 1.0 {
return u16::MAX;
}
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
let quantized = (normalized * f64::from(u16::MAX)).round() as u16;
quantized
}
pub fn fill_lut(out: &mut [ColorLut], curve: impl Fn(f64) -> f64) {
let last = out.len().saturating_sub(1);
for (index, entry) in out.iter_mut().enumerate() {
#[allow(clippy::cast_precision_loss)] let normalized = if last == 0 {
0.0
} else {
index as f64 / last as f64
};
let value = quantize_lut_value(curve(normalized));
*entry = ColorLut {
red: value,
green: value,
blue: value,
reserved: 0,
};
}
}
pub fn build_identity_lut(out: &mut [ColorLut]) {
fill_lut(out, |value| value);
}
pub fn build_pq_eotf_lut(out: &mut [ColorLut]) {
fill_lut(out, pq_eotf);
}
pub fn build_pq_oetf_lut(out: &mut [ColorLut]) {
fill_lut(out, pq_oetf);
}
pub fn build_hlg_oetf_inverse_lut(out: &mut [ColorLut]) {
fill_lut(out, hlg_oetf_inverse);
}
#[must_use]
pub fn build_identity_ctm() -> ColorCtm {
let mut matrix = [0_u64; 9];
matrix[0] = S31_32_ONE;
matrix[4] = S31_32_ONE;
matrix[8] = S31_32_ONE;
ColorCtm { matrix }
}
#[must_use]
pub fn build_bt2020_to_bt709_ctm() -> ColorCtm {
encode_matrix([
1.6605, -0.5876, -0.0728, -0.1246, 1.1329, -0.0083, -0.0182, -0.1006, 1.1187,
])
}
#[must_use]
pub fn build_bt709_to_bt2020_ctm() -> ColorCtm {
encode_matrix([
0.6274, 0.3293, 0.0433, 0.0691, 0.9195, 0.0114, 0.0164, 0.0880, 0.8956,
])
}
fn encode_matrix(coefficients: [f64; 9]) -> ColorCtm {
ColorCtm {
matrix: coefficients.map(encode_s31_32),
}
}