Something went wrong. Try again.
we (web engine): Experimental web browser project to understand the limits of Claude
Something went wrong. Try again.
Rust
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625//! Pixel format types and RGBA8 conversion.//!//! Provides a common `Image` type (always RGBA8) and functions to convert//! from various source formats: grayscale, grayscale+alpha, RGB, RGBA,//! and indexed (palette) color.
use std::fmt;
// ---------------------------------------------------------------------------// Error type// ---------------------------------------------------------------------------
/// Errors that can occur during image operations.#[derive(Debug, Clone, PartialEq, Eq)]pub enum ImageError { /// Image dimensions are zero. ZeroDimension { width: u32, height: u32 }, /// Pixel data length does not match expected dimensions. DataLengthMismatch { expected: usize, actual: usize }, /// Palette index is out of bounds. PaletteIndexOutOfBounds { index: u8, palette_len: usize }, /// Palette is empty. EmptyPalette, /// Generic decoding error. Decode(String),}
impl fmt::Display for ImageError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::ZeroDimension { width, height } => { write!(f, "zero dimension: {width}x{height}") } Self::DataLengthMismatch { expected, actual } => { write!(f, "data length mismatch: expected {expected}, got {actual}") } Self::PaletteIndexOutOfBounds { index, palette_len } => { write!( f, "palette index {index} out of bounds (palette has {palette_len} entries)" ) } Self::EmptyPalette => write!(f, "empty palette"), Self::Decode(msg) => write!(f, "decode error: {msg}"), } }}
pub type Result<T> = std::result::Result<T, ImageError>;
// ---------------------------------------------------------------------------// Image// ---------------------------------------------------------------------------
/// An image stored as RGBA8 pixel data (4 bytes per pixel).////// Pixels are stored in row-major order, top-to-bottom, left-to-right.#[derive(Debug, Clone, PartialEq, Eq)]pub struct Image { /// Width in pixels. pub width: u32, /// Height in pixels. pub height: u32, /// RGBA8 pixel data: `4 * width * height` bytes. pub data: Vec<u8>,}
impl Image { /// Create an image from pre-validated RGBA8 data. pub fn new(width: u32, height: u32, data: Vec<u8>) -> Result<Self> { if width == 0 || height == 0 { return Err(ImageError::ZeroDimension { width, height }); } let expected = (width as usize) * (height as usize) * 4; if data.len() != expected { return Err(ImageError::DataLengthMismatch { expected, actual: data.len(), }); } Ok(Self { width, height, data, }) }
/// Total number of pixels. pub fn pixel_count(&self) -> usize { self.width as usize * self.height as usize }}
// ---------------------------------------------------------------------------// Conversion functions// ---------------------------------------------------------------------------
/// Convert grayscale (1 channel) pixel data to an RGBA8 `Image`.////// Each grayscale byte maps to (G, G, G, 255).pub fn from_grayscale(width: u32, height: u32, gray: &[u8]) -> Result<Image> { if width == 0 || height == 0 { return Err(ImageError::ZeroDimension { width, height }); } let pixel_count = width as usize * height as usize; if gray.len() != pixel_count { return Err(ImageError::DataLengthMismatch { expected: pixel_count, actual: gray.len(), }); } let mut data = Vec::with_capacity(pixel_count * 4); for &g in gray { data.push(g); data.push(g); data.push(g); data.push(255); } Ok(Image { width, height, data, })}
/// Convert grayscale+alpha (2 channels) pixel data to an RGBA8 `Image`.////// Each pair of bytes (G, A) maps to (G, G, G, A).pub fn from_grayscale_alpha(width: u32, height: u32, ga: &[u8]) -> Result<Image> { if width == 0 || height == 0 { return Err(ImageError::ZeroDimension { width, height }); } let pixel_count = width as usize * height as usize; if ga.len() != pixel_count * 2 { return Err(ImageError::DataLengthMismatch { expected: pixel_count * 2, actual: ga.len(), }); } let mut data = Vec::with_capacity(pixel_count * 4); for pair in ga.chunks_exact(2) { let g = pair[0]; let a = pair[1]; data.push(g); data.push(g); data.push(g); data.push(a); } Ok(Image { width, height, data, })}
/// Convert RGB (3 channels) pixel data to an RGBA8 `Image`.////// Each triple (R, G, B) maps to (R, G, B, 255).pub fn from_rgb(width: u32, height: u32, rgb: &[u8]) -> Result<Image> { if width == 0 || height == 0 { return Err(ImageError::ZeroDimension { width, height }); } let pixel_count = width as usize * height as usize; if rgb.len() != pixel_count * 3 { return Err(ImageError::DataLengthMismatch { expected: pixel_count * 3, actual: rgb.len(), }); } let mut data = Vec::with_capacity(pixel_count * 4); for triple in rgb.chunks_exact(3) { data.push(triple[0]); data.push(triple[1]); data.push(triple[2]); data.push(255); } Ok(Image { width, height, data, })}
/// Convert RGBA (4 channels) pixel data to an RGBA8 `Image`.////// This is the identity conversion — validates dimensions and length.pub fn from_rgba(width: u32, height: u32, rgba: Vec<u8>) -> Result<Image> { Image::new(width, height, rgba)}
/// Convert indexed-color pixel data to an RGBA8 `Image`.////// `palette` is a flat array of RGB triples (3 bytes per entry)./// `indices` contains one palette index per pixel.pub fn from_indexed(width: u32, height: u32, palette: &[u8], indices: &[u8]) -> Result<Image> { if width == 0 || height == 0 { return Err(ImageError::ZeroDimension { width, height }); } if palette.is_empty() { return Err(ImageError::EmptyPalette); } let palette_len = palette.len() / 3; let pixel_count = width as usize * height as usize; if indices.len() != pixel_count { return Err(ImageError::DataLengthMismatch { expected: pixel_count, actual: indices.len(), }); } let mut data = Vec::with_capacity(pixel_count * 4); for &idx in indices { if (idx as usize) >= palette_len { return Err(ImageError::PaletteIndexOutOfBounds { index: idx, palette_len, }); } let offset = idx as usize * 3; data.push(palette[offset]); data.push(palette[offset + 1]); data.push(palette[offset + 2]); data.push(255); } Ok(Image { width, height, data, })}
/// Convert indexed-color pixel data with per-entry alpha to an RGBA8 `Image`.////// `palette` is a flat array of RGB triples (3 bytes per entry)./// `alpha` provides alpha values for palette entries (may be shorter than palette;/// missing entries default to 255)./// `indices` contains one palette index per pixel.pub fn from_indexed_alpha( width: u32, height: u32, palette: &[u8], alpha: &[u8], indices: &[u8],) -> Result<Image> { if width == 0 || height == 0 { return Err(ImageError::ZeroDimension { width, height }); } if palette.is_empty() { return Err(ImageError::EmptyPalette); } let palette_len = palette.len() / 3; let pixel_count = width as usize * height as usize; if indices.len() != pixel_count { return Err(ImageError::DataLengthMismatch { expected: pixel_count, actual: indices.len(), }); } let mut data = Vec::with_capacity(pixel_count * 4); for &idx in indices { if (idx as usize) >= palette_len { return Err(ImageError::PaletteIndexOutOfBounds { index: idx, palette_len, }); } let offset = idx as usize * 3; let a = alpha.get(idx as usize).copied().unwrap_or(255); data.push(palette[offset]); data.push(palette[offset + 1]); data.push(palette[offset + 2]); data.push(a); } Ok(Image { width, height, data, })}
// ---------------------------------------------------------------------------// Tests// ---------------------------------------------------------------------------
#[cfg(test)]mod tests { use super::*;
// -- Image::new tests --
#[test] fn image_new_valid() { let data = vec![0; 2 * 3 * 4]; // 2x3, 4 bytes per pixel let img = Image::new(2, 3, data).unwrap(); assert_eq!(img.width, 2); assert_eq!(img.height, 3); assert_eq!(img.pixel_count(), 6); assert_eq!(img.data.len(), 24); }
#[test] fn image_new_zero_width() { assert!(matches!( Image::new(0, 5, vec![]), Err(ImageError::ZeroDimension { width: 0, height: 5 }) )); }
#[test] fn image_new_zero_height() { assert!(matches!( Image::new(5, 0, vec![]), Err(ImageError::ZeroDimension { width: 5, height: 0 }) )); }
#[test] fn image_new_data_length_mismatch() { let err = Image::new(2, 2, vec![0; 10]).unwrap_err(); assert!(matches!( err, ImageError::DataLengthMismatch { expected: 16, actual: 10 } )); }
// -- Grayscale tests --
#[test] fn grayscale_basic() { let img = from_grayscale(2, 1, &[0, 128]).unwrap(); assert_eq!(img.width, 2); assert_eq!(img.height, 1); assert_eq!(img.data, vec![0, 0, 0, 255, 128, 128, 128, 255]); }
#[test] fn grayscale_white() { let img = from_grayscale(1, 1, &[255]).unwrap(); assert_eq!(img.data, vec![255, 255, 255, 255]); }
#[test] fn grayscale_zero_dimension() { assert!(matches!( from_grayscale(0, 1, &[]), Err(ImageError::ZeroDimension { .. }) )); }
#[test] fn grayscale_data_mismatch() { assert!(matches!( from_grayscale(2, 2, &[0, 1, 2]), Err(ImageError::DataLengthMismatch { expected: 4, actual: 3 }) )); }
// -- Grayscale+Alpha tests --
#[test] fn grayscale_alpha_basic() { let img = from_grayscale_alpha(1, 2, &[100, 200, 50, 128]).unwrap(); assert_eq!(img.data, vec![100, 100, 100, 200, 50, 50, 50, 128]); }
#[test] fn grayscale_alpha_zero_dimension() { assert!(matches!( from_grayscale_alpha(1, 0, &[]), Err(ImageError::ZeroDimension { .. }) )); }
#[test] fn grayscale_alpha_data_mismatch() { assert!(matches!( from_grayscale_alpha(2, 1, &[0, 1, 2]), Err(ImageError::DataLengthMismatch { expected: 4, actual: 3 }) )); }
// -- RGB tests --
#[test] fn rgb_basic() { let img = from_rgb(1, 2, &[255, 0, 0, 0, 255, 0]).unwrap(); assert_eq!(img.data, vec![255, 0, 0, 255, 0, 255, 0, 255]); }
#[test] fn rgb_zero_dimension() { assert!(matches!( from_rgb(0, 0, &[]), Err(ImageError::ZeroDimension { .. }) )); }
#[test] fn rgb_data_mismatch() { assert!(matches!( from_rgb(2, 1, &[0, 1, 2, 3, 4]), Err(ImageError::DataLengthMismatch { expected: 6, actual: 5 }) )); }
// -- RGBA tests --
#[test] fn rgba_basic() { let data = vec![10, 20, 30, 40, 50, 60, 70, 80]; let img = from_rgba(2, 1, data.clone()).unwrap(); assert_eq!(img.data, data); }
#[test] fn rgba_zero_dimension() { assert!(matches!( from_rgba(0, 1, vec![]), Err(ImageError::ZeroDimension { .. }) )); }
#[test] fn rgba_data_mismatch() { assert!(matches!( from_rgba(1, 1, vec![0, 1, 2]), Err(ImageError::DataLengthMismatch { expected: 4, actual: 3 }) )); }
// -- Indexed tests --
#[test] fn indexed_basic() { let palette = [255, 0, 0, 0, 255, 0, 0, 0, 255]; // red, green, blue let indices = [0, 1, 2, 0]; let img = from_indexed(2, 2, &palette, &indices).unwrap(); assert_eq!( img.data, vec![ 255, 0, 0, 255, // red 0, 255, 0, 255, // green 0, 0, 255, 255, // blue 255, 0, 0, 255, // red ] ); }
#[test] fn indexed_zero_dimension() { assert!(matches!( from_indexed(0, 1, &[0, 0, 0], &[]), Err(ImageError::ZeroDimension { .. }) )); }
#[test] fn indexed_empty_palette() { assert!(matches!( from_indexed(1, 1, &[], &[0]), Err(ImageError::EmptyPalette) )); }
#[test] fn indexed_out_of_bounds() { let palette = [255, 0, 0]; // 1 entry assert!(matches!( from_indexed(1, 1, &palette, &[1]), Err(ImageError::PaletteIndexOutOfBounds { index: 1, palette_len: 1 }) )); }
#[test] fn indexed_data_mismatch() { let palette = [0, 0, 0]; assert!(matches!( from_indexed(2, 2, &palette, &[0, 0]), Err(ImageError::DataLengthMismatch { expected: 4, actual: 2 }) )); }
// -- Indexed+Alpha tests --
#[test] fn indexed_alpha_basic() { let palette = [255, 0, 0, 0, 255, 0]; // red, green let alpha = [128, 64]; let indices = [0, 1]; let img = from_indexed_alpha(2, 1, &palette, &alpha, &indices).unwrap(); assert_eq!(img.data, vec![255, 0, 0, 128, 0, 255, 0, 64]); }
#[test] fn indexed_alpha_missing_defaults_to_255() { let palette = [255, 0, 0, 0, 255, 0]; // red, green let alpha = [128]; // only first entry has alpha let indices = [0, 1]; let img = from_indexed_alpha(2, 1, &palette, &alpha, &indices).unwrap(); assert_eq!(img.data, vec![255, 0, 0, 128, 0, 255, 0, 255]); }
#[test] fn indexed_alpha_empty_alpha() { let palette = [10, 20, 30]; let alpha: &[u8] = &[]; let indices = [0]; let img = from_indexed_alpha(1, 1, &palette, alpha, &indices).unwrap(); assert_eq!(img.data, vec![10, 20, 30, 255]); }
// -- Error Display tests --
#[test] fn error_display() { assert_eq!( ImageError::ZeroDimension { width: 0, height: 5 } .to_string(), "zero dimension: 0x5" ); assert_eq!( ImageError::DataLengthMismatch { expected: 100, actual: 50 } .to_string(), "data length mismatch: expected 100, got 50" ); assert_eq!( ImageError::PaletteIndexOutOfBounds { index: 10, palette_len: 5 } .to_string(), "palette index 10 out of bounds (palette has 5 entries)" ); assert_eq!(ImageError::EmptyPalette.to_string(), "empty palette"); assert_eq!( ImageError::Decode("bad header".to_string()).to_string(), "decode error: bad header" ); }
// -- Larger images --
#[test] fn grayscale_larger_image() { let width = 10; let height = 10; let gray: Vec<u8> = (0..100).collect(); let img = from_grayscale(width, height, &gray).unwrap(); assert_eq!(img.data.len(), 400); // Spot-check first and last pixel assert_eq!(&img.data[0..4], &[0, 0, 0, 255]); assert_eq!(&img.data[396..400], &[99, 99, 99, 255]); }
#[test] fn rgb_larger_image() { let width = 4; let height = 4; let rgb: Vec<u8> = (0..48).collect(); let img = from_rgb(width, height, &rgb).unwrap(); assert_eq!(img.data.len(), 64); // First pixel: R=0, G=1, B=2, A=255 assert_eq!(&img.data[0..4], &[0, 1, 2, 255]); // Second pixel: R=3, G=4, B=5, A=255 assert_eq!(&img.data[4..8], &[3, 4, 5, 255]); }
// -- 1x1 minimum images --
#[test] fn single_pixel_all_formats() { // Grayscale let img = from_grayscale(1, 1, &[42]).unwrap(); assert_eq!(img.data, vec![42, 42, 42, 255]);
// Grayscale+Alpha let img = from_grayscale_alpha(1, 1, &[42, 100]).unwrap(); assert_eq!(img.data, vec![42, 42, 42, 100]);
// RGB let img = from_rgb(1, 1, &[10, 20, 30]).unwrap(); assert_eq!(img.data, vec![10, 20, 30, 255]);
// RGBA let img = from_rgba(1, 1, vec![10, 20, 30, 40]).unwrap(); assert_eq!(img.data, vec![10, 20, 30, 40]);
// Indexed let img = from_indexed(1, 1, &[10, 20, 30], &[0]).unwrap(); assert_eq!(img.data, vec![10, 20, 30, 255]); }}