use std::sync::LazyLock;
macro_rules! srgb_transfer {
($decode:ident, $encode:ident, $float:ty) => {
pub fn $decode(c: $float) -> $float {
if c <= 0.04045 {
c / 12.92
} else {
((c + 0.055) / 1.055).powf(2.4)
}
}
pub fn $encode(c: $float) -> $float {
if c <= 0.0031308 {
12.92 * c
} else {
1.055 * c.powf(1. / 2.4) - 0.055
}
}
};
}
srgb_transfer!(srgb_to_linear, linear_to_srgb, f64);
srgb_transfer!(srgb_to_linear_f32, linear_to_srgb_f32, f32);
pub fn linear_to_srgb8(c: f64) -> u8 {
static TABLE: LazyLock<Thresholds<f64>> = LazyLock::new(|| Thresholds::<f64>::new(linear_to_srgb8_curve));
TABLE.byte(c, linear_to_srgb8_curve)
}
#[allow(
clippy::cast_possible_truncation,
reason = "a float to byte cast saturates, which is the clamping the curve is defined with"
)]
fn linear_to_srgb8_curve(c: f64) -> u8 {
(linear_to_srgb(c.clamp(0., 1.)) * 255.).round() as u8
}
pub fn linear_to_srgb8_f32(c: f32) -> u8 {
static TABLE: LazyLock<Thresholds<f32>> =
LazyLock::new(|| Thresholds::<f32>::new(linear_to_srgb8_f32_curve));
TABLE.byte(c, linear_to_srgb8_f32_curve)
}
#[allow(
clippy::cast_possible_truncation,
reason = "a float to integer cast saturates, which is the clamping the curve is defined with"
)]
fn linear_to_srgb8_f32_curve(c: f32) -> u8 {
((linear_to_srgb_f32(c) * 255. + 0.5) as i32).clamp(0, 255) as u8
}
fn table_byte(i: usize) -> u8 {
u8::try_from(i).unwrap_or(u8::MAX)
}
pub fn srgb8_to_linear(byte: u8) -> f64 {
static TABLE: LazyLock<[f64; 256]> =
LazyLock::new(|| std::array::from_fn(|i| srgb_to_linear(f64::from(table_byte(i)) / 255.)));
TABLE
.get(usize::from(byte))
.copied()
.unwrap_or_else(|| srgb_to_linear(f64::from(byte) / 255.))
}
pub fn srgb8_to_linear_f32(byte: u8) -> f32 {
static TABLE: LazyLock<[f32; 256]> =
LazyLock::new(|| std::array::from_fn(|i| srgb_to_linear_f32(f32::from(table_byte(i)) / 255.)));
TABLE
.get(usize::from(byte))
.copied()
.unwrap_or_else(|| srgb_to_linear_f32(f32::from(byte) / 255.))
}
struct Thresholds<F> {
at: [F; 255],
}
macro_rules! thresholds {
($float:ty, $bits:ty, $band:expr) => {
impl Thresholds<$float> {
const BAND: $float = $band;
fn new(curve: fn($float) -> u8) -> Self {
let one = (1.0 as $float).to_bits();
Thresholds {
at: std::array::from_fn(|i| {
let k = u8::try_from(i).unwrap_or(u8::MAX).saturating_add(1);
let (mut lo, mut hi): ($bits, $bits) = (0, one);
while hi.abs_diff(lo) > 1 {
let mid = lo.midpoint(hi);
if curve(<$float>::from_bits(mid)) >= k {
hi = mid;
} else {
lo = mid;
}
}
<$float>::from_bits(hi)
}),
}
}
fn byte(&self, c: $float, curve: fn($float) -> u8) -> u8 {
if !(c > 0.0 && c < 1.0) {
return curve(c);
}
let k = self.at.partition_point(|&t| t <= c);
let near = |i: usize| {
self.at
.get(i)
.is_some_and(|&t| (c - t).abs() <= t * Self::BAND)
};
if near(k) || k.checked_sub(1).is_some_and(near) {
return curve(c);
}
u8::try_from(k).unwrap_or(u8::MAX)
}
}
};
}
thresholds!(f64, u64, 1.0 / 16_777_216.0);
thresholds!(f32, u32, 1.0 / 65_536.0);