//! TGA-Decoder → fertig dekodiertes RGBA8-`Image`. //! //! Neutral: kennt weder GPU noch UI noch Fonts. Liefert *immer* RGBA8, //! Ursprung oben-links, vollständig konvertiert — der Aufrufer macht keine //! Nachbearbeitung, nur das Sampling (und damit die UV-Konvention) gehört ihm. //! //! Unterstützt True-Color unkomprimiert (Typ 2) und RLE (Typ 10) mit 24 //! oder 32 bpp, sowie Graustufen (Typ 3/11, 8 bpp → zu RGBA expandiert) für //! Font-Maps. Paletten-TGAs gibt es nicht mehr (wds ist 15-bit True-Color). pub struct Image { pub width: u32, pub height: u32, /// `width * height * 4`, Ursprung oben-links. pub rgba: Vec, } pub fn load(path: &str) -> Result { let bytes = std::fs::read(path).map_err(|e| format!("tga load {path}: {e}"))?; decode(&bytes).map_err(|e| format!("tga {path}: {e}")) } pub fn decode(d: &[u8]) -> Result { if d.len() < 18 { return Err("Header zu kurz".into()); } let id_len = d[0] as usize; let cmap_type = d[1]; let image_type = d[2]; let width = u16::from_le_bytes([d[12], d[13]]) as u32; let height = u16::from_le_bytes([d[14], d[15]]) as u32; let depth = d[16]; let descriptor = d[17]; if cmap_type != 0 { return Err("Color-Map-TGAs nicht unterstützt (True-Color only)".into()); } // Pixeldaten beginnen nach Header + ID-Feld (Color-Map ist leer). let off = 18 + id_len; let channels = match (image_type, depth) { (2 | 10, 24) => 3, (2 | 10, 32) => 4, (3 | 11, 8) => 1, (t, b) => return Err(format!("nicht unterstützt: Typ {t}, {b} bpp")), }; let n = (width as usize) * (height as usize); if n == 0 { return Err("Nullgröße".into()); } // Rohpixel (channels Bytes je Pixel), bei RLE entpackt. let raw_len = n * channels; let raw: Vec = match image_type { 2 | 3 => d.get(off..off + raw_len).ok_or("Pixeldaten zu kurz")?.to_vec(), 10 | 11 => decode_rle(d.get(off..).ok_or("RLE-Daten fehlen")?, raw_len, channels)?, _ => unreachable!(), }; // → RGBA8. True-Color liegt im TGA als BGR(A) vor; Graustufen replizieren. let mut rgba = vec![0u8; n * 4]; for i in 0..n { let (r, g, b, a) = match channels { 1 => { let v = raw[i]; (v, v, v, 255) } 3 => (raw[i * 3 + 2], raw[i * 3 + 1], raw[i * 3], 255), 4 => (raw[i * 4 + 2], raw[i * 4 + 1], raw[i * 4], raw[i * 4 + 3]), _ => unreachable!(), }; let o = i * 4; rgba[o] = r; rgba[o + 1] = g; rgba[o + 2] = b; rgba[o + 3] = a; } // Ursprung oben-links erzwingen. Descriptor-Bit 5 gesetzt = top-origin. if (descriptor >> 5) & 1 == 0 { flip_vertical(&mut rgba, width as usize, height as usize); } Ok(Image { width, height, rgba }) } /// TGA-RLE: Pakete operieren auf ganzen Pixeln (`ch` Bytes). fn decode_rle(src: &[u8], raw_len: usize, ch: usize) -> Result, String> { let mut out = Vec::with_capacity(raw_len); let mut si = 0; while out.len() < raw_len { let header = *src.get(si).ok_or("RLE: Quelle zu kurz")?; si += 1; let count = (header & 0x7f) as usize + 1; if header & 0x80 != 0 { // RLE-Paket: ein Pixel count-mal. let px = src.get(si..si + ch).ok_or("RLE: Pixel zu kurz")?; si += ch; for _ in 0..count { out.extend_from_slice(px); } } else { // Raw-Paket: count Pixel am Stück. let bytes = count * ch; let chunk = src.get(si..si + bytes).ok_or("RLE: Chunk zu kurz")?; si += bytes; out.extend_from_slice(chunk); } } out.truncate(raw_len); Ok(out) } fn flip_vertical(rgba: &mut [u8], w: usize, h: usize) { let row = w * 4; for y in 0..h / 2 { let (a, b) = (y * row, (h - 1 - y) * row); for x in 0..row { rgba.swap(a + x, b + x); } } } #[cfg(test)] mod tests { use super::*; #[test] fn bgr_to_rgba_uncompressed() { // Minimal-TGA: Typ 2, 2×1, 24 bpp, bottom-origin (descriptor 0). // Zwei Pixel BGR (10,20,30) und (40,50,60). let mut d = vec![0u8; 18]; d[2] = 2; d[12] = 2; d[14] = 1; d[16] = 24; d.extend_from_slice(&[10, 20, 30, 40, 50, 60]); let img = decode(&d).unwrap(); assert_eq!((img.width, img.height), (2, 1)); assert_eq!(&img.rgba[0..4], &[30, 20, 10, 255]); assert_eq!(&img.rgba[4..8], &[60, 50, 40, 255]); } #[test] fn rle_truecolor_expands() { // Typ 10, 4×1, 24 bpp: ein RLE-Paket (4× BGR 1,2,3). let mut d = vec![0u8; 18]; d[2] = 10; d[12] = 4; d[14] = 1; d[16] = 24; d.push(0x80 | 3); // RLE, count = 4 d.extend_from_slice(&[1, 2, 3]); // ein Pixel let img = decode(&d).unwrap(); assert_eq!(img.rgba.len(), 4 * 4); assert!(img.rgba.chunks(4).all(|p| p == [3, 2, 1, 255])); } #[test] fn decodes_real_placeholder() { let bytes = std::fs::read("assets/textures/placeholder.tga").unwrap(); let img = decode(&bytes).unwrap(); assert_eq!((img.width, img.height), (256, 256)); assert_eq!(img.rgba.len(), 256 * 256 * 4); assert!(img.rgba.chunks(4).all(|p| p[3] == 255)); // 24bpp → Alpha 255 } }