#[allow(dead_code)]
#[derive(Debug, Clone, PartialEq)]
pub enum PixelFormat {
Rgb8,
Rgba8,
GrayF32,
}
#[allow(dead_code)]
#[derive(Debug, Clone)]
pub struct TextureExportConfig {
pub gamma: f32,
pub format: PixelFormat,
pub flip_on_export: bool,
}
#[allow(dead_code)]
#[derive(Debug, Clone)]
pub struct TextureBuffer {
pub width: u32,
pub height: u32,
pub format: PixelFormat,
pub data: Vec<u8>,
}
pub type EncodedTexture = Vec<u8>;
#[allow(dead_code)]
pub fn default_texture_config() -> TextureExportConfig {
TextureExportConfig {
gamma: 1.0,
format: PixelFormat::Rgb8,
flip_on_export: false,
}
}
#[allow(dead_code)]
pub fn new_texture_buffer(width: u32, height: u32, format: PixelFormat) -> TextureBuffer {
let bytes_per_pixel = match &format {
PixelFormat::Rgb8 => 3,
PixelFormat::Rgba8 => 4,
PixelFormat::GrayF32 => 4,
};
let size = (width * height) as usize * bytes_per_pixel;
TextureBuffer {
width,
height,
format,
data: vec![0u8; size],
}
}
#[allow(dead_code)]
pub fn set_pixel_rgb(buf: &mut TextureBuffer, x: u32, y: u32, r: u8, g: u8, b: u8) {
if buf.format != PixelFormat::Rgb8 {
return;
}
if x >= buf.width || y >= buf.height {
return;
}
let idx = ((y * buf.width + x) * 3) as usize;
if idx + 2 < buf.data.len() {
buf.data[idx] = r;
buf.data[idx + 1] = g;
buf.data[idx + 2] = b;
}
}
#[allow(dead_code)]
pub fn get_pixel_rgb(buf: &TextureBuffer, x: u32, y: u32) -> (u8, u8, u8) {
if buf.format != PixelFormat::Rgb8 {
return (0, 0, 0);
}
if x >= buf.width || y >= buf.height {
return (0, 0, 0);
}
let idx = ((y * buf.width + x) * 3) as usize;
if idx + 2 < buf.data.len() {
(buf.data[idx], buf.data[idx + 1], buf.data[idx + 2])
} else {
(0, 0, 0)
}
}
#[allow(dead_code)]
pub fn texture_width(buf: &TextureBuffer) -> u32 {
buf.width
}
#[allow(dead_code)]
pub fn texture_height(buf: &TextureBuffer) -> u32 {
buf.height
}
#[allow(dead_code)]
pub fn pixel_count(buf: &TextureBuffer) -> usize {
(buf.width * buf.height) as usize
}
#[allow(dead_code)]
pub fn encode_ppm_rgb(buf: &TextureBuffer) -> EncodedTexture {
if buf.format != PixelFormat::Rgb8 {
return Vec::new();
}
let header = format!("P6\n{} {}\n255\n", buf.width, buf.height);
let mut out = header.into_bytes();
out.extend_from_slice(&buf.data);
out
}
#[allow(dead_code)]
pub fn encode_ppm_gray(buf: &TextureBuffer) -> EncodedTexture {
if buf.format != PixelFormat::GrayF32 {
return Vec::new();
}
let header = format!("P5\n{} {}\n255\n", buf.width, buf.height);
let mut out = header.into_bytes();
let n = (buf.width * buf.height) as usize;
for i in 0..n {
let offset = i * 4;
if offset + 3 < buf.data.len() {
let bytes: [u8; 4] = [
buf.data[offset],
buf.data[offset + 1],
buf.data[offset + 2],
buf.data[offset + 3],
];
let f = f32::from_le_bytes(bytes).clamp(0.0, 1.0);
out.push((f * 255.0) as u8);
} else {
out.push(0);
}
}
out
}
#[allow(dead_code)]
pub fn texture_to_raw_bytes(buf: &TextureBuffer) -> Vec<u8> {
buf.data.clone()
}
#[allow(dead_code)]
pub fn raw_bytes_to_texture(
bytes: &[u8],
width: u32,
height: u32,
format: PixelFormat,
) -> Option<TextureBuffer> {
let bpp = match &format {
PixelFormat::Rgb8 => 3usize,
PixelFormat::Rgba8 => 4,
PixelFormat::GrayF32 => 4,
};
let expected = (width * height) as usize * bpp;
if bytes.len() != expected {
return None;
}
Some(TextureBuffer {
width,
height,
format,
data: bytes.to_vec(),
})
}
#[allow(dead_code)]
pub fn resize_texture(buf: &TextureBuffer, new_w: u32, new_h: u32) -> TextureBuffer {
let bpp: usize = match &buf.format {
PixelFormat::Rgb8 => 3,
PixelFormat::Rgba8 => 4,
PixelFormat::GrayF32 => {
return buf.clone();
}
};
let mut out = vec![0u8; (new_w * new_h) as usize * bpp];
for ny in 0..new_h {
for nx in 0..new_w {
let src_x = (nx * buf.width / new_w.max(1)).min(buf.width.saturating_sub(1));
let src_y = (ny * buf.height / new_h.max(1)).min(buf.height.saturating_sub(1));
let src_idx = ((src_y * buf.width + src_x) as usize) * bpp;
let dst_idx = ((ny * new_w + nx) as usize) * bpp;
if src_idx + bpp <= buf.data.len() && dst_idx + bpp <= out.len() {
out[dst_idx..dst_idx + bpp].copy_from_slice(&buf.data[src_idx..src_idx + bpp]);
}
}
}
TextureBuffer {
width: new_w,
height: new_h,
format: buf.format.clone(),
data: out,
}
}
#[allow(dead_code)]
pub fn flip_vertical(buf: &mut TextureBuffer) {
let bpp: usize = match &buf.format {
PixelFormat::Rgb8 => 3,
PixelFormat::Rgba8 => 4,
PixelFormat::GrayF32 => 4,
};
let row_bytes = buf.width as usize * bpp;
let h = buf.height as usize;
for y in 0..(h / 2) {
let top = y * row_bytes;
let bot = (h - 1 - y) * row_bytes;
for x in 0..row_bytes {
buf.data.swap(top + x, bot + x);
}
}
}
#[allow(dead_code)]
pub fn apply_texture_gamma(buf: &mut TextureBuffer, gamma: f32) {
if buf.format != PixelFormat::Rgb8 {
return;
}
if (gamma - 1.0).abs() < 1e-6 {
return;
}
let inv = 1.0 / gamma.max(0.01);
for byte in buf.data.iter_mut() {
let f = (*byte as f32 / 255.0).powf(inv);
*byte = (f.clamp(0.0, 1.0) * 255.0) as u8;
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_config_has_gamma_one() {
let cfg = default_texture_config();
assert!((cfg.gamma - 1.0).abs() < 1e-6);
assert_eq!(cfg.format, PixelFormat::Rgb8);
}
#[test]
fn new_buffer_correct_size_rgb8() {
let buf = new_texture_buffer(4, 4, PixelFormat::Rgb8);
assert_eq!(buf.data.len(), 4 * 4 * 3);
}
#[test]
fn new_buffer_correct_size_rgba8() {
let buf = new_texture_buffer(2, 3, PixelFormat::Rgba8);
assert_eq!(buf.data.len(), 2 * 3 * 4);
}
#[test]
fn set_and_get_pixel_rgb() {
let mut buf = new_texture_buffer(8, 8, PixelFormat::Rgb8);
set_pixel_rgb(&mut buf, 3, 2, 100, 150, 200);
let (r, g, b) = get_pixel_rgb(&buf, 3, 2);
assert_eq!((r, g, b), (100, 150, 200));
}
#[test]
fn get_pixel_out_of_bounds_returns_zero() {
let buf = new_texture_buffer(4, 4, PixelFormat::Rgb8);
assert_eq!(get_pixel_rgb(&buf, 100, 100), (0, 0, 0));
}
#[test]
fn texture_width_and_height() {
let buf = new_texture_buffer(16, 32, PixelFormat::Rgb8);
assert_eq!(texture_width(&buf), 16);
assert_eq!(texture_height(&buf), 32);
}
#[test]
fn pixel_count_correct() {
let buf = new_texture_buffer(10, 5, PixelFormat::Rgb8);
assert_eq!(pixel_count(&buf), 50);
}
#[test]
fn encode_ppm_rgb_starts_with_header() {
let mut buf = new_texture_buffer(2, 2, PixelFormat::Rgb8);
set_pixel_rgb(&mut buf, 0, 0, 255, 0, 0);
let ppm = encode_ppm_rgb(&buf);
let header = "P6\n2 2\n255\n";
assert!(ppm.starts_with(header.as_bytes()));
}
#[test]
fn encode_ppm_rgb_wrong_format_empty() {
let buf = new_texture_buffer(2, 2, PixelFormat::Rgba8);
assert!(encode_ppm_rgb(&buf).is_empty());
}
#[test]
fn encode_ppm_gray_starts_with_header() {
let buf = new_texture_buffer(2, 2, PixelFormat::GrayF32);
let ppm = encode_ppm_gray(&buf);
let header = "P5\n2 2\n255\n";
assert!(ppm.starts_with(header.as_bytes()));
}
#[test]
fn encode_ppm_gray_wrong_format_empty() {
let buf = new_texture_buffer(2, 2, PixelFormat::Rgb8);
assert!(encode_ppm_gray(&buf).is_empty());
}
#[test]
fn texture_to_raw_bytes_round_trip() {
let mut buf = new_texture_buffer(4, 4, PixelFormat::Rgb8);
set_pixel_rgb(&mut buf, 1, 1, 50, 100, 150);
let raw = texture_to_raw_bytes(&buf);
let restored = raw_bytes_to_texture(&raw, 4, 4, PixelFormat::Rgb8).expect("should succeed");
let (r, g, b) = get_pixel_rgb(&restored, 1, 1);
assert_eq!((r, g, b), (50, 100, 150));
}
#[test]
fn raw_bytes_to_texture_wrong_size_returns_none() {
let bad = vec![0u8; 10];
assert!(raw_bytes_to_texture(&bad, 4, 4, PixelFormat::Rgb8).is_none());
}
#[test]
fn resize_texture_changes_dimensions() {
let buf = new_texture_buffer(8, 8, PixelFormat::Rgb8);
let resized = resize_texture(&buf, 4, 4);
assert_eq!(texture_width(&resized), 4);
assert_eq!(texture_height(&resized), 4);
}
#[test]
fn flip_vertical_changes_pixels() {
let mut buf = new_texture_buffer(2, 2, PixelFormat::Rgb8);
set_pixel_rgb(&mut buf, 0, 0, 255, 0, 0);
set_pixel_rgb(&mut buf, 0, 1, 0, 0, 255);
flip_vertical(&mut buf);
let (r, _, _) = get_pixel_rgb(&buf, 0, 0);
assert_eq!(r, 0, "top row should now be blue (r=0)");
let (r2, _, _) = get_pixel_rgb(&buf, 0, 1);
assert_eq!(r2, 255, "bottom row should now be red (r=255)");
}
#[test]
fn apply_texture_gamma_noop_at_one() {
let mut buf = new_texture_buffer(2, 2, PixelFormat::Rgb8);
set_pixel_rgb(&mut buf, 0, 0, 128, 64, 32);
apply_texture_gamma(&mut buf, 1.0);
let (r, g, b) = get_pixel_rgb(&buf, 0, 0);
assert_eq!((r, g, b), (128, 64, 32));
}
#[test]
fn apply_texture_gamma_changes_values() {
let mut buf = new_texture_buffer(1, 1, PixelFormat::Rgb8);
set_pixel_rgb(&mut buf, 0, 0, 128, 128, 128);
apply_texture_gamma(&mut buf, 2.2);
let (r, _, _) = get_pixel_rgb(&buf, 0, 0);
assert!(r > 128, "gamma < 1 exponent should brighten");
}
}