const LINEAR_SLOPE: f32 = 12.92;
const KNEE_ENCODED: f32 = 0.040_45;
const KNEE_LINEAR: f32 = 0.003_130_8;
const OFFSET: f32 = 0.055;
const GAMMA: f32 = 2.4;
#[must_use]
pub fn decode(encoded: f32) -> f32 {
if encoded <= KNEE_ENCODED {
encoded / LINEAR_SLOPE
} else {
((encoded + OFFSET) / (1.0 + OFFSET)).powf(GAMMA)
}
}
#[must_use]
pub fn encode(linear: f32) -> f32 {
if linear <= KNEE_LINEAR {
linear * LINEAR_SLOPE
} else {
(1.0 + OFFSET) * linear.powf(1.0 / GAMMA) - OFFSET
}
}
#[cfg(test)]
mod tests {
use super::*;
const TOLERANCE: f32 = 1e-6;
fn close(actual: f32, expected: f32) {
assert!(
(actual - expected).abs() <= TOLERANCE,
"{actual} is not {expected}"
);
}
#[test]
fn decode_matches_the_specification_on_both_arms() {
close(decode(0.0), 0.0);
close(decode(1.0), 1.0);
close(decode(0.5), 0.214_041_14);
close(decode(128.0 / 255.0), 0.215_860_5);
close(decode(0.2), 0.033_104_766);
close(decode(0.02), 0.02 / 12.92);
}
#[test]
fn encode_matches_the_specification_on_both_arms() {
close(encode(0.0), 0.0);
close(encode(1.0), 1.0);
close(encode(0.214_041_14), 0.5);
close(encode(0.5), 0.735_357);
close(encode(0.001), 0.012_92);
}
#[test]
fn every_byte_survives_a_round_trip() {
for byte in 0..=u8::MAX {
let encoded = f32::from(byte) / 255.0;
let back = (encode(decode(encoded)) * 255.0).round();
assert_eq!(back, f32::from(byte), "byte {byte}");
}
}
}