Files
wds/src/engine/tga.rs
T
2026-08-28 23:31:48 +02:00

147 lines
5.3 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! 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<u8>,
}
pub fn load(path: &str) -> Result<Image, String> {
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<Image, String> {
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<u8> = 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<Vec<u8>, 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
}
}