use std::sync::OnceLock;
pub fn srgb_to_linear(byte: u8) -> f32 {
static TABLE: OnceLock<[f32; 256]> = OnceLock::new();
let table = TABLE.get_or_init(|| {
std::array::from_fn(|i| {
let c = i as f32 / 255.0;
if c <= 0.040_45 {
c / 12.92
} else {
((c + 0.055) / 1.055).powf(2.4)
}
})
});
table[byte as usize]
}
pub fn linear_to_srgb_byte(linear: f32) -> u8 {
let c = linear.clamp(0.0, 1.0);
let encoded = if c <= 0.003_130_8 {
c * 12.92
} else {
1.055 * c.powf(1.0 / 2.4) - 0.055
};
(encoded * 255.0 + 0.5).clamp(0.0, 255.0) as u8
}
pub fn downsample_half(src: &[u8], src_w: u32, src_h: u32) -> (u32, u32, Vec<u8>) {
let dst_w = (src_w / 2).max(1);
let dst_h = (src_h / 2).max(1);
let mut dst = vec![0u8; dst_w as usize * dst_h as usize * 4];
for y in 0..dst_h {
for x in 0..dst_w {
let x0 = (x * 2).min(src_w - 1);
let x1 = (x * 2 + 1).min(src_w - 1);
let y0 = (y * 2).min(src_h - 1);
let y1 = (y * 2 + 1).min(src_h - 1);
let (mut r, mut g, mut b, mut a) = (0.0f32, 0.0f32, 0.0f32, 0.0f32);
for (sy, sx) in [(y0, x0), (y0, x1), (y1, x0), (y1, x1)] {
let i = ((sy as usize * src_w as usize) + sx as usize) * 4;
let sa = src[i + 3] as f32 / 255.0;
r += srgb_to_linear(src[i]) * sa;
g += srgb_to_linear(src[i + 1]) * sa;
b += srgb_to_linear(src[i + 2]) * sa;
a += sa;
}
let o = ((y as usize * dst_w as usize) + x as usize) * 4;
if a > 0.0 {
dst[o] = linear_to_srgb_byte(r / a);
dst[o + 1] = linear_to_srgb_byte(g / a);
dst[o + 2] = linear_to_srgb_byte(b / a);
}
dst[o + 3] = ((a / 4.0) * 255.0 + 0.5).clamp(0.0, 255.0) as u8;
}
}
(dst_w, dst_h, dst)
}
pub fn resample_area(src: &[u8], src_w: u32, src_h: u32, dst_w: u32, dst_h: u32) -> Vec<u8> {
let (sw, sh) = (src_w as usize, src_h as usize);
let (dw, dh) = (dst_w as usize, dst_h as usize);
if sw == 0 || sh == 0 || dw == 0 || dh == 0 || src.len() < sw * sh * 4 {
return Vec::new();
}
let x_ratio = sw as f32 / dw as f32;
let y_ratio = sh as f32 / dh as f32;
let mut dst = vec![0u8; dw * dh * 4];
for dy in 0..dh {
let sy0 = dy as f32 * y_ratio;
let sy1 = sy0 + y_ratio;
for dx in 0..dw {
let sx0 = dx as f32 * x_ratio;
let sx1 = sx0 + x_ratio;
let (mut r, mut g, mut b, mut a, mut total) = (0.0f32, 0.0f32, 0.0f32, 0.0f32, 0.0f32);
for sy in (sy0.floor() as usize)..(sy1.ceil() as usize).min(sh) {
let wy = (sy1.min(sy as f32 + 1.0) - sy0.max(sy as f32)).max(0.0);
if wy <= 0.0 {
continue;
}
for sx in (sx0.floor() as usize)..(sx1.ceil() as usize).min(sw) {
let wx = (sx1.min(sx as f32 + 1.0) - sx0.max(sx as f32)).max(0.0);
if wx <= 0.0 {
continue;
}
let w = wx * wy;
let i = (sy * sw + sx) * 4;
let sa = src[i + 3] as f32 / 255.0;
r += srgb_to_linear(src[i]) * sa * w;
g += srgb_to_linear(src[i + 1]) * sa * w;
b += srgb_to_linear(src[i + 2]) * sa * w;
a += sa * w;
total += w;
}
}
let o = (dy * dw + dx) * 4;
if a > 0.0 {
dst[o] = linear_to_srgb_byte(r / a);
dst[o + 1] = linear_to_srgb_byte(g / a);
dst[o + 2] = linear_to_srgb_byte(b / a);
}
if total > 0.0 {
dst[o + 3] = ((a / total) * 255.0 + 0.5).clamp(0.0, 255.0) as u8;
}
}
}
dst
}
pub fn fit_within(width: u32, height: u32, max_edge: u32) -> Option<(u32, u32)> {
if max_edge == 0 || width == 0 || height == 0 || width.max(height) <= max_edge {
return None;
}
let scale = max_edge as f64 / width.max(height) as f64;
let w = ((width as f64 * scale).round() as u32).max(1);
let h = ((height as f64 * scale).round() as u32).max(1);
Some((w, h))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn srgb_round_trips_through_linear() {
for b in [0u8, 1, 55, 128, 200, 254, 255] {
assert_eq!(linear_to_srgb_byte(srgb_to_linear(b)), b, "byte {b}");
}
}
#[test]
fn black_and_white_average_to_mid_luminance_not_mid_byte() {
let src = vec![0, 0, 0, 255, 255, 255, 255, 255];
let out = resample_area(&src, 2, 1, 1, 1);
let mid = linear_to_srgb_byte(0.5);
assert_eq!(out[0], mid);
assert!(
out[0] > 180,
"linear-light average should be ~188, got {}",
out[0]
);
}
#[test]
fn transparent_neighbour_does_not_vote_on_colour() {
let src = vec![255, 0, 0, 255, 0, 0, 0, 0];
let out = resample_area(&src, 2, 1, 1, 1);
assert_eq!(&out[0..3], &[255, 0, 0]);
assert_eq!(out[3], 128, "alpha is the plain area average");
}
#[test]
fn resample_to_same_size_is_lossless() {
let src: Vec<u8> = (0..(4 * 3 * 4)).map(|i| (i * 7 % 256) as u8).collect();
let out = resample_area(&src, 4, 3, 4, 3);
assert_eq!(out, src);
}
#[test]
fn resample_produces_the_requested_size() {
let src = vec![200u8; 10 * 10 * 4];
for (w, h) in [(3u32, 3u32), (7, 2), (1, 1), (10, 4)] {
let out = resample_area(&src, 10, 10, w, h);
assert_eq!(out.len(), (w * h * 4) as usize, "{w}x{h}");
}
}
#[test]
fn a_flat_colour_survives_any_ratio() {
let src = [70u8, 130, 180, 255].repeat(9 * 9);
let out = resample_area(&src, 9, 9, 4, 4);
for px in out.chunks(4) {
assert_eq!(px, &[70, 130, 180, 255]);
}
}
#[test]
fn degenerate_inputs_return_empty_rather_than_panicking() {
assert!(resample_area(&[], 0, 0, 4, 4).is_empty());
assert!(resample_area(&[1, 2, 3, 4], 1, 1, 0, 4).is_empty());
assert!(
resample_area(&[1, 2], 4, 4, 2, 2).is_empty(),
"short buffer"
);
}
#[test]
fn halving_matches_area_resample_for_even_sizes() {
let src: Vec<u8> = (0..(8 * 8 * 4)).map(|i| (i % 251) as u8).collect();
let (w, h, half) = downsample_half(&src, 8, 8);
let area = resample_area(&src, 8, 8, 4, 4);
assert_eq!((w, h), (4, 4));
for (a, b) in half.iter().zip(area.iter()) {
assert!(a.abs_diff(*b) <= 1, "half={a} area={b}");
}
}
#[test]
fn fit_within_preserves_aspect_and_never_returns_zero() {
assert_eq!(fit_within(4000, 3000, 2000), Some((2000, 1500)));
assert_eq!(fit_within(3000, 4000, 2000), Some((1500, 2000)));
assert_eq!(fit_within(100, 100, 2000), None, "already fits");
assert_eq!(
fit_within(4000, 1, 100),
Some((100, 1)),
"short side clamps to 1"
);
assert_eq!(fit_within(0, 10, 100), None);
}
}