we (web engine): Experimental web browser project to understand the limits of Claude
at main 624 lines 18 kB view raw
1//! Pixel format types and RGBA8 conversion. 2//! 3//! Provides a common `Image` type (always RGBA8) and functions to convert 4//! from various source formats: grayscale, grayscale+alpha, RGB, RGBA, 5//! and indexed (palette) color. 6 7use std::fmt; 8 9// --------------------------------------------------------------------------- 10// Error type 11// --------------------------------------------------------------------------- 12 13/// Errors that can occur during image operations. 14#[derive(Debug, Clone, PartialEq, Eq)] 15pub enum ImageError { 16 /// Image dimensions are zero. 17 ZeroDimension { width: u32, height: u32 }, 18 /// Pixel data length does not match expected dimensions. 19 DataLengthMismatch { expected: usize, actual: usize }, 20 /// Palette index is out of bounds. 21 PaletteIndexOutOfBounds { index: u8, palette_len: usize }, 22 /// Palette is empty. 23 EmptyPalette, 24 /// Generic decoding error. 25 Decode(String), 26} 27 28impl fmt::Display for ImageError { 29 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 30 match self { 31 Self::ZeroDimension { width, height } => { 32 write!(f, "zero dimension: {width}x{height}") 33 } 34 Self::DataLengthMismatch { expected, actual } => { 35 write!(f, "data length mismatch: expected {expected}, got {actual}") 36 } 37 Self::PaletteIndexOutOfBounds { index, palette_len } => { 38 write!( 39 f, 40 "palette index {index} out of bounds (palette has {palette_len} entries)" 41 ) 42 } 43 Self::EmptyPalette => write!(f, "empty palette"), 44 Self::Decode(msg) => write!(f, "decode error: {msg}"), 45 } 46 } 47} 48 49pub type Result<T> = std::result::Result<T, ImageError>; 50 51// --------------------------------------------------------------------------- 52// Image 53// --------------------------------------------------------------------------- 54 55/// An image stored as RGBA8 pixel data (4 bytes per pixel). 56/// 57/// Pixels are stored in row-major order, top-to-bottom, left-to-right. 58#[derive(Debug, Clone, PartialEq, Eq)] 59pub struct Image { 60 /// Width in pixels. 61 pub width: u32, 62 /// Height in pixels. 63 pub height: u32, 64 /// RGBA8 pixel data: `4 * width * height` bytes. 65 pub data: Vec<u8>, 66} 67 68impl Image { 69 /// Create an image from pre-validated RGBA8 data. 70 pub fn new(width: u32, height: u32, data: Vec<u8>) -> Result<Self> { 71 if width == 0 || height == 0 { 72 return Err(ImageError::ZeroDimension { width, height }); 73 } 74 let expected = (width as usize) * (height as usize) * 4; 75 if data.len() != expected { 76 return Err(ImageError::DataLengthMismatch { 77 expected, 78 actual: data.len(), 79 }); 80 } 81 Ok(Self { 82 width, 83 height, 84 data, 85 }) 86 } 87 88 /// Total number of pixels. 89 pub fn pixel_count(&self) -> usize { 90 self.width as usize * self.height as usize 91 } 92} 93 94// --------------------------------------------------------------------------- 95// Conversion functions 96// --------------------------------------------------------------------------- 97 98/// Convert grayscale (1 channel) pixel data to an RGBA8 `Image`. 99/// 100/// Each grayscale byte maps to (G, G, G, 255). 101pub fn from_grayscale(width: u32, height: u32, gray: &[u8]) -> Result<Image> { 102 if width == 0 || height == 0 { 103 return Err(ImageError::ZeroDimension { width, height }); 104 } 105 let pixel_count = width as usize * height as usize; 106 if gray.len() != pixel_count { 107 return Err(ImageError::DataLengthMismatch { 108 expected: pixel_count, 109 actual: gray.len(), 110 }); 111 } 112 let mut data = Vec::with_capacity(pixel_count * 4); 113 for &g in gray { 114 data.push(g); 115 data.push(g); 116 data.push(g); 117 data.push(255); 118 } 119 Ok(Image { 120 width, 121 height, 122 data, 123 }) 124} 125 126/// Convert grayscale+alpha (2 channels) pixel data to an RGBA8 `Image`. 127/// 128/// Each pair of bytes (G, A) maps to (G, G, G, A). 129pub fn from_grayscale_alpha(width: u32, height: u32, ga: &[u8]) -> Result<Image> { 130 if width == 0 || height == 0 { 131 return Err(ImageError::ZeroDimension { width, height }); 132 } 133 let pixel_count = width as usize * height as usize; 134 if ga.len() != pixel_count * 2 { 135 return Err(ImageError::DataLengthMismatch { 136 expected: pixel_count * 2, 137 actual: ga.len(), 138 }); 139 } 140 let mut data = Vec::with_capacity(pixel_count * 4); 141 for pair in ga.chunks_exact(2) { 142 let g = pair[0]; 143 let a = pair[1]; 144 data.push(g); 145 data.push(g); 146 data.push(g); 147 data.push(a); 148 } 149 Ok(Image { 150 width, 151 height, 152 data, 153 }) 154} 155 156/// Convert RGB (3 channels) pixel data to an RGBA8 `Image`. 157/// 158/// Each triple (R, G, B) maps to (R, G, B, 255). 159pub fn from_rgb(width: u32, height: u32, rgb: &[u8]) -> Result<Image> { 160 if width == 0 || height == 0 { 161 return Err(ImageError::ZeroDimension { width, height }); 162 } 163 let pixel_count = width as usize * height as usize; 164 if rgb.len() != pixel_count * 3 { 165 return Err(ImageError::DataLengthMismatch { 166 expected: pixel_count * 3, 167 actual: rgb.len(), 168 }); 169 } 170 let mut data = Vec::with_capacity(pixel_count * 4); 171 for triple in rgb.chunks_exact(3) { 172 data.push(triple[0]); 173 data.push(triple[1]); 174 data.push(triple[2]); 175 data.push(255); 176 } 177 Ok(Image { 178 width, 179 height, 180 data, 181 }) 182} 183 184/// Convert RGBA (4 channels) pixel data to an RGBA8 `Image`. 185/// 186/// This is the identity conversion — validates dimensions and length. 187pub fn from_rgba(width: u32, height: u32, rgba: Vec<u8>) -> Result<Image> { 188 Image::new(width, height, rgba) 189} 190 191/// Convert indexed-color pixel data to an RGBA8 `Image`. 192/// 193/// `palette` is a flat array of RGB triples (3 bytes per entry). 194/// `indices` contains one palette index per pixel. 195pub fn from_indexed(width: u32, height: u32, palette: &[u8], indices: &[u8]) -> Result<Image> { 196 if width == 0 || height == 0 { 197 return Err(ImageError::ZeroDimension { width, height }); 198 } 199 if palette.is_empty() { 200 return Err(ImageError::EmptyPalette); 201 } 202 let palette_len = palette.len() / 3; 203 let pixel_count = width as usize * height as usize; 204 if indices.len() != pixel_count { 205 return Err(ImageError::DataLengthMismatch { 206 expected: pixel_count, 207 actual: indices.len(), 208 }); 209 } 210 let mut data = Vec::with_capacity(pixel_count * 4); 211 for &idx in indices { 212 if (idx as usize) >= palette_len { 213 return Err(ImageError::PaletteIndexOutOfBounds { 214 index: idx, 215 palette_len, 216 }); 217 } 218 let offset = idx as usize * 3; 219 data.push(palette[offset]); 220 data.push(palette[offset + 1]); 221 data.push(palette[offset + 2]); 222 data.push(255); 223 } 224 Ok(Image { 225 width, 226 height, 227 data, 228 }) 229} 230 231/// Convert indexed-color pixel data with per-entry alpha to an RGBA8 `Image`. 232/// 233/// `palette` is a flat array of RGB triples (3 bytes per entry). 234/// `alpha` provides alpha values for palette entries (may be shorter than palette; 235/// missing entries default to 255). 236/// `indices` contains one palette index per pixel. 237pub fn from_indexed_alpha( 238 width: u32, 239 height: u32, 240 palette: &[u8], 241 alpha: &[u8], 242 indices: &[u8], 243) -> Result<Image> { 244 if width == 0 || height == 0 { 245 return Err(ImageError::ZeroDimension { width, height }); 246 } 247 if palette.is_empty() { 248 return Err(ImageError::EmptyPalette); 249 } 250 let palette_len = palette.len() / 3; 251 let pixel_count = width as usize * height as usize; 252 if indices.len() != pixel_count { 253 return Err(ImageError::DataLengthMismatch { 254 expected: pixel_count, 255 actual: indices.len(), 256 }); 257 } 258 let mut data = Vec::with_capacity(pixel_count * 4); 259 for &idx in indices { 260 if (idx as usize) >= palette_len { 261 return Err(ImageError::PaletteIndexOutOfBounds { 262 index: idx, 263 palette_len, 264 }); 265 } 266 let offset = idx as usize * 3; 267 let a = alpha.get(idx as usize).copied().unwrap_or(255); 268 data.push(palette[offset]); 269 data.push(palette[offset + 1]); 270 data.push(palette[offset + 2]); 271 data.push(a); 272 } 273 Ok(Image { 274 width, 275 height, 276 data, 277 }) 278} 279 280// --------------------------------------------------------------------------- 281// Tests 282// --------------------------------------------------------------------------- 283 284#[cfg(test)] 285mod tests { 286 use super::*; 287 288 // -- Image::new tests -- 289 290 #[test] 291 fn image_new_valid() { 292 let data = vec![0; 2 * 3 * 4]; // 2x3, 4 bytes per pixel 293 let img = Image::new(2, 3, data).unwrap(); 294 assert_eq!(img.width, 2); 295 assert_eq!(img.height, 3); 296 assert_eq!(img.pixel_count(), 6); 297 assert_eq!(img.data.len(), 24); 298 } 299 300 #[test] 301 fn image_new_zero_width() { 302 assert!(matches!( 303 Image::new(0, 5, vec![]), 304 Err(ImageError::ZeroDimension { 305 width: 0, 306 height: 5 307 }) 308 )); 309 } 310 311 #[test] 312 fn image_new_zero_height() { 313 assert!(matches!( 314 Image::new(5, 0, vec![]), 315 Err(ImageError::ZeroDimension { 316 width: 5, 317 height: 0 318 }) 319 )); 320 } 321 322 #[test] 323 fn image_new_data_length_mismatch() { 324 let err = Image::new(2, 2, vec![0; 10]).unwrap_err(); 325 assert!(matches!( 326 err, 327 ImageError::DataLengthMismatch { 328 expected: 16, 329 actual: 10 330 } 331 )); 332 } 333 334 // -- Grayscale tests -- 335 336 #[test] 337 fn grayscale_basic() { 338 let img = from_grayscale(2, 1, &[0, 128]).unwrap(); 339 assert_eq!(img.width, 2); 340 assert_eq!(img.height, 1); 341 assert_eq!(img.data, vec![0, 0, 0, 255, 128, 128, 128, 255]); 342 } 343 344 #[test] 345 fn grayscale_white() { 346 let img = from_grayscale(1, 1, &[255]).unwrap(); 347 assert_eq!(img.data, vec![255, 255, 255, 255]); 348 } 349 350 #[test] 351 fn grayscale_zero_dimension() { 352 assert!(matches!( 353 from_grayscale(0, 1, &[]), 354 Err(ImageError::ZeroDimension { .. }) 355 )); 356 } 357 358 #[test] 359 fn grayscale_data_mismatch() { 360 assert!(matches!( 361 from_grayscale(2, 2, &[0, 1, 2]), 362 Err(ImageError::DataLengthMismatch { 363 expected: 4, 364 actual: 3 365 }) 366 )); 367 } 368 369 // -- Grayscale+Alpha tests -- 370 371 #[test] 372 fn grayscale_alpha_basic() { 373 let img = from_grayscale_alpha(1, 2, &[100, 200, 50, 128]).unwrap(); 374 assert_eq!(img.data, vec![100, 100, 100, 200, 50, 50, 50, 128]); 375 } 376 377 #[test] 378 fn grayscale_alpha_zero_dimension() { 379 assert!(matches!( 380 from_grayscale_alpha(1, 0, &[]), 381 Err(ImageError::ZeroDimension { .. }) 382 )); 383 } 384 385 #[test] 386 fn grayscale_alpha_data_mismatch() { 387 assert!(matches!( 388 from_grayscale_alpha(2, 1, &[0, 1, 2]), 389 Err(ImageError::DataLengthMismatch { 390 expected: 4, 391 actual: 3 392 }) 393 )); 394 } 395 396 // -- RGB tests -- 397 398 #[test] 399 fn rgb_basic() { 400 let img = from_rgb(1, 2, &[255, 0, 0, 0, 255, 0]).unwrap(); 401 assert_eq!(img.data, vec![255, 0, 0, 255, 0, 255, 0, 255]); 402 } 403 404 #[test] 405 fn rgb_zero_dimension() { 406 assert!(matches!( 407 from_rgb(0, 0, &[]), 408 Err(ImageError::ZeroDimension { .. }) 409 )); 410 } 411 412 #[test] 413 fn rgb_data_mismatch() { 414 assert!(matches!( 415 from_rgb(2, 1, &[0, 1, 2, 3, 4]), 416 Err(ImageError::DataLengthMismatch { 417 expected: 6, 418 actual: 5 419 }) 420 )); 421 } 422 423 // -- RGBA tests -- 424 425 #[test] 426 fn rgba_basic() { 427 let data = vec![10, 20, 30, 40, 50, 60, 70, 80]; 428 let img = from_rgba(2, 1, data.clone()).unwrap(); 429 assert_eq!(img.data, data); 430 } 431 432 #[test] 433 fn rgba_zero_dimension() { 434 assert!(matches!( 435 from_rgba(0, 1, vec![]), 436 Err(ImageError::ZeroDimension { .. }) 437 )); 438 } 439 440 #[test] 441 fn rgba_data_mismatch() { 442 assert!(matches!( 443 from_rgba(1, 1, vec![0, 1, 2]), 444 Err(ImageError::DataLengthMismatch { 445 expected: 4, 446 actual: 3 447 }) 448 )); 449 } 450 451 // -- Indexed tests -- 452 453 #[test] 454 fn indexed_basic() { 455 let palette = [255, 0, 0, 0, 255, 0, 0, 0, 255]; // red, green, blue 456 let indices = [0, 1, 2, 0]; 457 let img = from_indexed(2, 2, &palette, &indices).unwrap(); 458 assert_eq!( 459 img.data, 460 vec![ 461 255, 0, 0, 255, // red 462 0, 255, 0, 255, // green 463 0, 0, 255, 255, // blue 464 255, 0, 0, 255, // red 465 ] 466 ); 467 } 468 469 #[test] 470 fn indexed_zero_dimension() { 471 assert!(matches!( 472 from_indexed(0, 1, &[0, 0, 0], &[]), 473 Err(ImageError::ZeroDimension { .. }) 474 )); 475 } 476 477 #[test] 478 fn indexed_empty_palette() { 479 assert!(matches!( 480 from_indexed(1, 1, &[], &[0]), 481 Err(ImageError::EmptyPalette) 482 )); 483 } 484 485 #[test] 486 fn indexed_out_of_bounds() { 487 let palette = [255, 0, 0]; // 1 entry 488 assert!(matches!( 489 from_indexed(1, 1, &palette, &[1]), 490 Err(ImageError::PaletteIndexOutOfBounds { 491 index: 1, 492 palette_len: 1 493 }) 494 )); 495 } 496 497 #[test] 498 fn indexed_data_mismatch() { 499 let palette = [0, 0, 0]; 500 assert!(matches!( 501 from_indexed(2, 2, &palette, &[0, 0]), 502 Err(ImageError::DataLengthMismatch { 503 expected: 4, 504 actual: 2 505 }) 506 )); 507 } 508 509 // -- Indexed+Alpha tests -- 510 511 #[test] 512 fn indexed_alpha_basic() { 513 let palette = [255, 0, 0, 0, 255, 0]; // red, green 514 let alpha = [128, 64]; 515 let indices = [0, 1]; 516 let img = from_indexed_alpha(2, 1, &palette, &alpha, &indices).unwrap(); 517 assert_eq!(img.data, vec![255, 0, 0, 128, 0, 255, 0, 64]); 518 } 519 520 #[test] 521 fn indexed_alpha_missing_defaults_to_255() { 522 let palette = [255, 0, 0, 0, 255, 0]; // red, green 523 let alpha = [128]; // only first entry has alpha 524 let indices = [0, 1]; 525 let img = from_indexed_alpha(2, 1, &palette, &alpha, &indices).unwrap(); 526 assert_eq!(img.data, vec![255, 0, 0, 128, 0, 255, 0, 255]); 527 } 528 529 #[test] 530 fn indexed_alpha_empty_alpha() { 531 let palette = [10, 20, 30]; 532 let alpha: &[u8] = &[]; 533 let indices = [0]; 534 let img = from_indexed_alpha(1, 1, &palette, alpha, &indices).unwrap(); 535 assert_eq!(img.data, vec![10, 20, 30, 255]); 536 } 537 538 // -- Error Display tests -- 539 540 #[test] 541 fn error_display() { 542 assert_eq!( 543 ImageError::ZeroDimension { 544 width: 0, 545 height: 5 546 } 547 .to_string(), 548 "zero dimension: 0x5" 549 ); 550 assert_eq!( 551 ImageError::DataLengthMismatch { 552 expected: 100, 553 actual: 50 554 } 555 .to_string(), 556 "data length mismatch: expected 100, got 50" 557 ); 558 assert_eq!( 559 ImageError::PaletteIndexOutOfBounds { 560 index: 10, 561 palette_len: 5 562 } 563 .to_string(), 564 "palette index 10 out of bounds (palette has 5 entries)" 565 ); 566 assert_eq!(ImageError::EmptyPalette.to_string(), "empty palette"); 567 assert_eq!( 568 ImageError::Decode("bad header".to_string()).to_string(), 569 "decode error: bad header" 570 ); 571 } 572 573 // -- Larger images -- 574 575 #[test] 576 fn grayscale_larger_image() { 577 let width = 10; 578 let height = 10; 579 let gray: Vec<u8> = (0..100).collect(); 580 let img = from_grayscale(width, height, &gray).unwrap(); 581 assert_eq!(img.data.len(), 400); 582 // Spot-check first and last pixel 583 assert_eq!(&img.data[0..4], &[0, 0, 0, 255]); 584 assert_eq!(&img.data[396..400], &[99, 99, 99, 255]); 585 } 586 587 #[test] 588 fn rgb_larger_image() { 589 let width = 4; 590 let height = 4; 591 let rgb: Vec<u8> = (0..48).collect(); 592 let img = from_rgb(width, height, &rgb).unwrap(); 593 assert_eq!(img.data.len(), 64); 594 // First pixel: R=0, G=1, B=2, A=255 595 assert_eq!(&img.data[0..4], &[0, 1, 2, 255]); 596 // Second pixel: R=3, G=4, B=5, A=255 597 assert_eq!(&img.data[4..8], &[3, 4, 5, 255]); 598 } 599 600 // -- 1x1 minimum images -- 601 602 #[test] 603 fn single_pixel_all_formats() { 604 // Grayscale 605 let img = from_grayscale(1, 1, &[42]).unwrap(); 606 assert_eq!(img.data, vec![42, 42, 42, 255]); 607 608 // Grayscale+Alpha 609 let img = from_grayscale_alpha(1, 1, &[42, 100]).unwrap(); 610 assert_eq!(img.data, vec![42, 42, 42, 100]); 611 612 // RGB 613 let img = from_rgb(1, 1, &[10, 20, 30]).unwrap(); 614 assert_eq!(img.data, vec![10, 20, 30, 255]); 615 616 // RGBA 617 let img = from_rgba(1, 1, vec![10, 20, 30, 40]).unwrap(); 618 assert_eq!(img.data, vec![10, 20, 30, 40]); 619 620 // Indexed 621 let img = from_indexed(1, 1, &[10, 20, 30], &[0]).unwrap(); 622 assert_eq!(img.data, vec![10, 20, 30, 255]); 623 } 624}