use crate::mlaf::fmla;
#[allow(unused_imports)]
use num_traits::Float;
#[allow(clippy::excessive_precision)]
const SRGB_LINEAR_THRESHOLD: f64 = 12.92 * 0.003_041_282_560_127_521;
pub(crate) const SRGB_LINEAR_THRESHOLD_F32: f32 = SRGB_LINEAR_THRESHOLD as f32;
#[allow(clippy::excessive_precision)]
const LINEAR_THRESHOLD: f64 = 0.003_041_282_560_127_521;
pub(crate) const LINEAR_THRESHOLD_F32: f32 = LINEAR_THRESHOLD as f32;
const LINEAR_SCALE: f64 = 1.0 / 12.92;
pub(crate) const LINEAR_SCALE_F32: f32 = LINEAR_SCALE as f32;
const SRGB_A: f64 = 0.055_010_718_947_586_6;
const SRGB_A_F32: f32 = SRGB_A as f32;
const SRGB_A_PLUS_1: f64 = 1.055_010_718_947_586_6;
const SRGB_A_PLUS_1_F32: f32 = SRGB_A_PLUS_1 as f32;
const GAMMA: f64 = 2.4;
const INV_GAMMA: f64 = 1.0 / GAMMA;
const INV_GAMMA_F32: f32 = INV_GAMMA as f32;
#[inline]
pub fn srgb_to_linear_f64(gamma: f64) -> f64 {
if gamma < 0.0 {
0.0
} else if gamma < SRGB_LINEAR_THRESHOLD {
gamma * LINEAR_SCALE
} else if gamma < 1.0 {
((gamma + SRGB_A) / SRGB_A_PLUS_1).powf(GAMMA)
} else {
1.0
}
}
#[inline]
pub fn srgb_to_linear(gamma: f32) -> f32 {
if gamma < 0.0 {
0.0
} else if gamma < SRGB_LINEAR_THRESHOLD_F32 {
gamma * LINEAR_SCALE_F32
} else if gamma < 1.0 {
((gamma + SRGB_A_F32) / SRGB_A_PLUS_1_F32).powf(GAMMA as f32)
} else {
1.0
}
}
#[inline]
pub fn linear_to_srgb_f64(linear: f64) -> f64 {
if linear < 0.0 {
0.0
} else if linear < LINEAR_THRESHOLD {
linear * 12.92
} else if linear < 1.0 {
fmla(SRGB_A_PLUS_1, linear.powf(INV_GAMMA), -SRGB_A)
} else {
1.0
}
}
#[inline]
pub fn linear_to_srgb(linear: f32) -> f32 {
if linear < 0.0 {
0.0
} else if linear < LINEAR_THRESHOLD_F32 {
linear * 12.92
} else if linear < 1.0 {
fmla(SRGB_A_PLUS_1_F32, linear.powf(INV_GAMMA_F32), -SRGB_A_F32)
} else {
1.0
}
}
#[inline]
#[allow(dead_code)] pub fn srgb_to_linear_fast(gamma: f32) -> f32 {
crate::rational_poly::srgb_to_linear_fast(gamma)
}
#[inline]
#[allow(dead_code)] pub fn linear_to_srgb_fast(linear: f32) -> f32 {
crate::rational_poly::linear_to_srgb_fast(linear)
}
#[inline]
pub fn srgb_to_linear_extended(gamma: f32) -> f32 {
let sign = gamma.signum();
let abs_v = gamma.abs();
if abs_v < SRGB_LINEAR_THRESHOLD_F32 {
gamma * LINEAR_SCALE_F32
} else {
sign * ((abs_v + SRGB_A_F32) / SRGB_A_PLUS_1_F32).powf(GAMMA as f32)
}
}
#[inline]
pub fn linear_to_srgb_extended(linear: f32) -> f32 {
let sign = linear.signum();
let abs_v = linear.abs();
if abs_v < LINEAR_THRESHOLD_F32 {
linear * 12.92
} else {
sign * fmla(SRGB_A_PLUS_1_F32, abs_v.powf(INV_GAMMA_F32), -SRGB_A_F32)
}
}
#[inline]
pub fn linear_to_srgb_u8(linear: f32) -> u8 {
let idx = (linear.clamp(0.0, 1.0) * 4095.0 + 0.5) as usize & 0xFFF;
crate::const_luts::linear_to_srgb_u8()[idx]
}
#[inline]
pub fn srgb_u16_to_linear(value: u16) -> f32 {
#[cfg(feature = "std")]
{
crate::u16_lut::decode_lut()[value as usize]
}
#[cfg(not(feature = "std"))]
{
crate::rational_poly::srgb_to_linear_fast(value as f32 / 65535.0)
}
}
#[inline]
pub fn linear_to_srgb_u16(linear: f32) -> u16 {
let srgb = crate::rational_poly::linear_to_srgb_fast(linear);
(srgb * 65535.0 + 0.5).clamp(0.0, 65535.0) as u16
}
#[inline]
pub fn linear_to_srgb_u16_fast(linear: f32) -> u16 {
#[cfg(feature = "std")]
{
let idx =
(linear.clamp(0.0, 1.0).sqrt() * crate::u16_lut::ENCODE_SQRT_SCALE + 0.5) as usize;
crate::u16_lut::encode_lut()[idx.min(crate::u16_lut::ENCODE_LUT_N - 1)]
}
#[cfg(not(feature = "std"))]
{
let srgb = crate::rational_poly::linear_to_srgb_fast(linear);
(srgb * 65535.0 + 0.5).clamp(0.0, 65535.0) as u16
}
}
#[inline]
pub fn gamma_to_linear(encoded: f32, gamma: f32) -> f32 {
if encoded <= 0.0 {
0.0
} else if encoded >= 1.0 {
1.0
} else {
encoded.powf(gamma)
}
}
#[inline]
pub fn linear_to_gamma(linear: f32, gamma: f32) -> f32 {
if linear <= 0.0 {
0.0
} else if linear >= 1.0 {
1.0
} else {
linear.powf(1.0 / gamma)
}
}
#[inline]
pub fn gamma_to_linear_f64(encoded: f64, gamma: f64) -> f64 {
if encoded <= 0.0 {
0.0
} else if encoded >= 1.0 {
1.0
} else {
encoded.powf(gamma)
}
}
#[inline]
pub fn linear_to_gamma_f64(linear: f64, gamma: f64) -> f64 {
if linear <= 0.0 {
0.0
} else if linear >= 1.0 {
1.0
} else {
linear.powf(1.0 / gamma)
}
}
static SRGB_U8_TO_LINEAR_LUT: [f32; 256] = {
const THRESHOLD: f64 = 12.92 * 0.003_041_282_560_127_521; const A: f64 = 0.055_010_718_947_586_6;
const A_PLUS_1: f64 = 1.055_010_718_947_586_6;
let mut lut = [0.0f32; 256];
let mut i = 0;
while i < 256 {
let srgb = i as f64 / 255.0;
let linear = if srgb <= THRESHOLD {
srgb / 12.92
} else {
let base = (srgb + A) / A_PLUS_1;
let sq = base * base; let target = sq;
let mut x = 0.5f64;
let mut iter = 0;
while iter < 100 {
let x4 = x * x * x * x;
let x5 = x4 * x;
x = x - (x5 - target) / (5.0 * x4);
iter += 1;
}
sq * x };
lut[i] = linear as f32;
i += 1;
}
lut
};
#[inline]
fn get_lut() -> &'static [f32; 256] {
&SRGB_U8_TO_LINEAR_LUT
}
#[inline]
pub fn srgb_u8_to_linear(value: u8) -> f32 {
get_lut()[value as usize]
}
#[inline]
pub fn srgb_u8_to_linear_x8(srgb: [u8; 8]) -> [f32; 8] {
let lut = get_lut();
[
lut[srgb[0] as usize],
lut[srgb[1] as usize],
lut[srgb[2] as usize],
lut[srgb[3] as usize],
lut[srgb[4] as usize],
lut[srgb[5] as usize],
lut[srgb[6] as usize],
lut[srgb[7] as usize],
]
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_srgb_to_linear_boundaries() {
assert_eq!(srgb_to_linear(-0.1), 0.0);
assert_eq!(srgb_to_linear(0.0), 0.0);
assert_eq!(srgb_to_linear(1.0), 1.0);
assert_eq!(srgb_to_linear(1.1), 1.0);
}
#[test]
fn test_linear_to_srgb_boundaries() {
assert_eq!(linear_to_srgb(-0.1), 0.0);
assert_eq!(linear_to_srgb(0.0), 0.0);
assert_eq!(linear_to_srgb(1.0), 1.0);
assert_eq!(linear_to_srgb(1.1), 1.0);
}
#[test]
fn test_roundtrip_f32() {
for i in 0..=255 {
let srgb = i as f32 / 255.0;
let linear = srgb_to_linear(srgb);
let back = linear_to_srgb(linear);
assert!(
(srgb - back).abs() < 1e-5,
"Roundtrip failed for {}: {} -> {} -> {}",
i,
srgb,
linear,
back
);
}
}
#[test]
fn test_roundtrip_f64() {
for i in 0..=255 {
let srgb = i as f64 / 255.0;
let linear = srgb_to_linear_f64(srgb);
let back = linear_to_srgb_f64(linear);
assert!(
(srgb - back).abs() < 1e-10,
"Roundtrip failed for {}: {} -> {} -> {}",
i,
srgb,
linear,
back
);
}
}
#[test]
fn test_linear_segment() {
let test_val = 0.02f32;
let linear = srgb_to_linear(test_val);
let expected = test_val / 12.92;
assert!((linear - expected).abs() < 1e-7);
}
#[test]
fn test_known_values() {
let linear = srgb_to_linear(0.5);
assert!((linear - 0.214).abs() < 0.001);
let srgb = linear_to_srgb(0.18);
assert!((srgb - 0.46).abs() < 0.01);
}
#[test]
#[allow(deprecated)]
fn test_u8_conversion() {
assert_eq!(srgb_u8_to_linear(0), 0.0);
assert_eq!(linear_to_srgb_u8(0.0), 0);
assert_eq!(linear_to_srgb_u8(1.0), 255);
for i in 0..=255u8 {
let linear = srgb_u8_to_linear(i);
let back = linear_to_srgb_u8(linear);
assert!(
(i as i32 - back as i32).abs() <= 1,
"u8 roundtrip failed for {}",
i
);
}
}
#[test]
fn test_u16_conversion() {
assert_eq!(srgb_u16_to_linear(0), 0.0);
assert_eq!(srgb_u16_to_linear(65535), 1.0);
assert_eq!(linear_to_srgb_u16(0.0), 0);
assert_eq!(linear_to_srgb_u16(1.0), 65535);
assert_eq!(linear_to_srgb_u16(-0.1), 0);
assert_eq!(linear_to_srgb_u16(1.1), 65535);
let mut prev = 0.0f32;
for i in 0..=65535u16 {
let linear = srgb_u16_to_linear(i);
assert!(
linear >= prev,
"non-monotonic at {}: {} < {}",
i,
linear,
prev
);
prev = linear;
}
for i in 0..=255u16 {
let val = i * 257; let linear = srgb_u16_to_linear(val);
let back = linear_to_srgb_u16(linear);
assert_eq!(
val, back,
"u16 roundtrip failed for {val}: {val} -> {linear} -> {back}",
);
}
for i in 0..=255u16 {
let val = i * 257;
let linear = srgb_u16_to_linear(val);
let back = linear_to_srgb_u16_fast(linear);
let diff = (val as i32 - back as i32).unsigned_abs();
assert!(
diff <= 1,
"u16 fast roundtrip failed for {val}: {val} -> {linear} -> {back} (diff {diff})",
);
}
assert_eq!(linear_to_srgb_u16(srgb_u16_to_linear(65535)), 65535);
}
#[test]
fn test_custom_gamma_boundaries() {
assert_eq!(gamma_to_linear(-0.1, 2.2), 0.0);
assert_eq!(gamma_to_linear(0.0, 2.2), 0.0);
assert_eq!(gamma_to_linear(1.0, 2.2), 1.0);
assert_eq!(gamma_to_linear(1.1, 2.2), 1.0);
assert_eq!(linear_to_gamma(-0.1, 2.2), 0.0);
assert_eq!(linear_to_gamma(0.0, 2.2), 0.0);
assert_eq!(linear_to_gamma(1.0, 2.2), 1.0);
assert_eq!(linear_to_gamma(1.1, 2.2), 1.0);
}
#[test]
fn test_custom_gamma_known_values() {
let linear = gamma_to_linear(0.5, 2.2);
assert!(
(linear - 0.2176).abs() < 0.001,
"gamma_to_linear(0.5, 2.2) = {}, expected ~0.2176",
linear
);
let encoded = linear_to_gamma(0.2176, 2.2);
assert!(
(encoded - 0.5).abs() < 0.01,
"linear_to_gamma(0.2176, 2.2) = {}, expected ~0.5",
encoded
);
}
#[test]
fn test_custom_gamma_roundtrip() {
for gamma in [1.8, 2.0, 2.2, 2.4, 2.6] {
for i in 0..=255 {
let encoded = i as f32 / 255.0;
let linear = gamma_to_linear(encoded, gamma);
let back = linear_to_gamma(linear, gamma);
assert!(
(encoded - back).abs() < 1e-5,
"Roundtrip failed for gamma={}, value={}: {} -> {} -> {}",
gamma,
i,
encoded,
linear,
back
);
}
}
}
#[test]
fn test_custom_gamma_f64_precision() {
let encoded = 0.5_f64;
let gamma = 2.2_f64;
let linear = gamma_to_linear_f64(encoded, gamma);
let back = linear_to_gamma_f64(linear, gamma);
assert!(
(encoded - back).abs() < 1e-14,
"f64 roundtrip: {} -> {} -> {}",
encoded,
linear,
back
);
}
#[test]
fn test_srgb_to_linear_fast_boundaries() {
assert_eq!(srgb_to_linear_fast(-0.1), 0.0);
assert_eq!(srgb_to_linear_fast(0.0), 0.0);
assert_eq!(srgb_to_linear_fast(1.0), 1.0);
assert_eq!(srgb_to_linear_fast(1.1), 1.0);
}
#[test]
fn test_linear_to_srgb_fast_boundaries() {
assert_eq!(linear_to_srgb_fast(-0.1), 0.0);
assert_eq!(linear_to_srgb_fast(0.0), 0.0);
assert_eq!(linear_to_srgb_fast(1.0), 1.0);
assert_eq!(linear_to_srgb_fast(1.1), 1.0);
}
#[test]
fn test_fast_vs_powf() {
for i in 0..=255 {
let srgb = i as f32 / 255.0;
let exact = srgb_to_linear(srgb);
let fast = srgb_to_linear_fast(srgb);
assert!(
(exact - fast).abs() < 1e-5,
"srgb_to_linear_fast mismatch at {}/255: exact={}, fast={}, diff={}",
i,
exact,
fast,
(exact - fast).abs()
);
}
for i in 0..=255 {
let linear = i as f32 / 255.0;
let exact = linear_to_srgb(linear);
let fast = linear_to_srgb_fast(linear);
assert!(
(exact - fast).abs() < 1e-5,
"linear_to_srgb_fast mismatch at {}/255: exact={}, fast={}, diff={}",
i,
exact,
fast,
(exact - fast).abs()
);
}
}
#[test]
fn test_fast_roundtrip() {
for i in 0..=255 {
let srgb = i as f32 / 255.0;
let linear = srgb_to_linear_fast(srgb);
let back = linear_to_srgb_fast(linear);
assert!(
(srgb - back).abs() < 1e-4,
"Fast roundtrip failed at {}/255: {} -> {} -> {}, diff={}",
i,
srgb,
linear,
back,
(srgb - back).abs()
);
}
}
#[test]
fn test_fast_linear_segment() {
let test_val = 0.02f32;
let fast = srgb_to_linear_fast(test_val);
let expected = test_val / 12.92;
assert!((fast - expected).abs() < 1e-7);
}
}