rakata_formats/tga/
mod.rs

1//! TGA (Targa) reader and writer.
2//!
3//! This module provides a Rust-centric TGA API for KotOR texture workflows:
4//! - parse many TGA source variants into a normalized top-left RGBA8 buffer,
5//! - write lossless-passthrough or canonical output.
6//!
7//! Parsing is backed by `tinytga` for robust header/color-map/RLE handling.
8//!
9//! ## KotOR Notes
10//! - Reader defaults target canonical vanilla KotOR (K1) image types
11//!   (`1`, `2`, `3`, `9`, `10`) and reject non-canonical variants such as
12//!   grayscale RLE (`type 11`) unless compatibility mode is explicitly enabled.
13//! - Writer default: lossless passthrough when pixels are unmodified (the 18-byte
14//!   header and raw image sections are re-emitted verbatim). For new files or
15//!   edited pixels, output is canonical uncompressed true-color (type `2`, 32-bit,
16//!   top-left origin). K1 ignores `image_type` and `image_descriptor` entirely
17//!   (confirmed Ghidra evidence -- see `docs/notes/texture_formats.md`).
18//!
19//! ## Format Layout
20//! ```text
21//! +------------------------------+ 0x0000
22//! | Header (18 bytes)            |
23//! +------------------------------+
24//! | Image ID (id_len bytes)      |
25//! +------------------------------+
26//! | Optional color map           |
27//! +------------------------------+
28//! | Pixel data                   |
29//! +------------------------------+
30//! | Optional footer/extensions   |
31//! +------------------------------+
32//! ```
33
34mod reader;
35mod writer;
36
37pub use reader::{
38    read_tga, read_tga_from_bytes, read_tga_from_bytes_with_options, read_tga_with_options,
39};
40pub use writer::{write_tga, write_tga_to_vec};
41
42use thiserror::Error;
43use tinytga::{Bpp, ParseError};
44
45use crate::binary::{DecodeBinary, EncodeBinary};
46
47/// Parsed TGA data category from the source header.
48#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
49pub enum TgaDataType {
50    /// No image data.
51    NoData,
52    /// Color-mapped image data.
53    ColorMapped,
54    /// True-color image data.
55    TrueColor,
56    /// Black-and-white (grayscale) image data.
57    BlackAndWhite,
58}
59
60/// Source TGA compression mode.
61#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
62pub enum TgaCompression {
63    /// Uncompressed image data.
64    Uncompressed,
65    /// Run-length encoded image data.
66    Rle,
67}
68
69/// Source TGA image origin.
70#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
71pub enum TgaOrigin {
72    /// Origin at bottom-left.
73    BottomLeft,
74    /// Origin at bottom-right.
75    BottomRight,
76    /// Origin at top-left.
77    TopLeft,
78    /// Origin at top-right.
79    TopRight,
80}
81
82/// Source TGA bit depth.
83#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
84pub enum TgaBitsPerPixel {
85    /// 8 bits per pixel.
86    Bits8,
87    /// 16 bits per pixel.
88    Bits16,
89    /// 24 bits per pixel.
90    Bits24,
91    /// 32 bits per pixel.
92    Bits32,
93}
94
95impl TgaBitsPerPixel {
96    pub(super) fn try_from_tinytga(value: Bpp) -> Result<Self, TgaBinaryError> {
97        match value {
98            Bpp::Bits8 => Ok(Self::Bits8),
99            Bpp::Bits16 => Ok(Self::Bits16),
100            Bpp::Bits24 => Ok(Self::Bits24),
101            Bpp::Bits32 => Ok(Self::Bits32),
102            _ => Err(TgaBinaryError::InvalidHeader(
103                "unsupported bits-per-pixel value in source header".into(),
104            )),
105        }
106    }
107
108    /// Returns the numeric bit depth.
109    pub fn bits(self) -> u8 {
110        match self {
111            Self::Bits8 => 8,
112            Self::Bits16 => 16,
113            Self::Bits24 => 24,
114            Self::Bits32 => 32,
115        }
116    }
117}
118
119/// Source TGA header metadata captured during parsing.
120#[derive(Debug, Clone, PartialEq, Eq, Hash)]
121pub struct TgaHeader {
122    /// Image ID length field.
123    pub id_len: u8,
124    /// Whether the source declared a color map.
125    pub has_color_map: bool,
126    /// Source data type.
127    pub data_type: TgaDataType,
128    /// Source compression mode.
129    pub compression: TgaCompression,
130    /// Source color-map start index.
131    pub color_map_start: u16,
132    /// Source color-map entry count.
133    pub color_map_len: u16,
134    /// Source color-map entry depth.
135    pub color_map_depth: Option<TgaBitsPerPixel>,
136    /// Source X origin.
137    pub x_origin: u16,
138    /// Source Y origin.
139    pub y_origin: u16,
140    /// Image width in pixels.
141    pub width: u16,
142    /// Image height in pixels.
143    pub height: u16,
144    /// Source pixel depth for image data entries.
145    pub pixel_depth: TgaBitsPerPixel,
146    /// Source origin interpretation.
147    pub image_origin: TgaOrigin,
148    /// Source alpha channel depth field.
149    pub alpha_channel_depth: u8,
150}
151
152/// In-memory TGA image.
153///
154/// Pixel data is always normalized to top-left RGBA8888 row-major order.
155#[derive(Debug, Clone, PartialEq, Eq)]
156pub struct Tga {
157    /// Source header metadata.
158    pub header: TgaHeader,
159    /// Source image ID bytes.
160    pub image_id: Vec<u8>,
161    /// Normalized RGBA8888 pixel buffer.
162    pub rgba_pixels: Vec<u8>,
163}
164
165impl Tga {
166    /// Creates a canonical RGBA image.
167    pub fn new_rgba(width: u16, height: u16, rgba_pixels: Vec<u8>) -> Result<Self, TgaBinaryError> {
168        validate_rgba_len(width, height, &rgba_pixels)?;
169
170        Ok(Self {
171            header: TgaHeader {
172                id_len: 0,
173                has_color_map: false,
174                data_type: TgaDataType::TrueColor,
175                compression: TgaCompression::Uncompressed,
176                color_map_start: 0,
177                color_map_len: 0,
178                color_map_depth: None,
179                x_origin: 0,
180                y_origin: 0,
181                width,
182                height,
183                pixel_depth: TgaBitsPerPixel::Bits32,
184                image_origin: TgaOrigin::TopLeft,
185                alpha_channel_depth: 8,
186            },
187            image_id: Vec::new(),
188            rgba_pixels,
189        })
190    }
191}
192
193impl DecodeBinary for Tga {
194    type Error = TgaBinaryError;
195
196    fn decode_binary(bytes: &[u8]) -> Result<Self, Self::Error> {
197        read_tga_from_bytes(bytes)
198    }
199}
200
201impl EncodeBinary for Tga {
202    type Error = TgaBinaryError;
203
204    fn encode_binary(&self) -> Result<Vec<u8>, Self::Error> {
205        write_tga_to_vec(self)
206    }
207}
208
209/// Errors produced while parsing or serializing TGA data.
210#[derive(Debug, Error)]
211pub enum TgaBinaryError {
212    /// I/O read/write failure.
213    #[error(transparent)]
214    Io(#[from] std::io::Error),
215    /// TGA parser-level error from `tinytga`.
216    #[error("TGA parse error: {0:?}")]
217    Parse(ParseError),
218    /// Invalid or unsupported source header/data relationship.
219    #[error("invalid TGA header: {0}")]
220    InvalidHeader(String),
221    /// Invalid image data body.
222    #[error("invalid TGA data: {0}")]
223    InvalidData(String),
224    /// Value cannot fit target integer width.
225    #[error("value overflow while handling field `{0}`")]
226    ValueOverflow(&'static str),
227}
228
229impl From<ParseError> for TgaBinaryError {
230    fn from(value: ParseError) -> Self {
231        Self::Parse(value)
232    }
233}
234
235/// TGA reader option set.
236#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
237pub struct TgaReadOptions {
238    /// Input policy for accepted source variants.
239    pub input: TgaReadMode,
240}
241
242impl Default for TgaReadOptions {
243    fn default() -> Self {
244        Self {
245            input: TgaReadMode::CanonicalK1,
246        }
247    }
248}
249
250/// TGA reader input policy.
251#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
252pub enum TgaReadMode {
253    /// Accept only vanilla-safe K1 source variants.
254    ///
255    /// This mode admits image types `1`, `2`, `3`, `9`, and `10` and keeps
256    /// true-color RLE constrained to 24/32-bit payloads.
257    CanonicalK1,
258    /// Accept broader TGA variants for tooling compatibility.
259    Compatibility,
260}
261
262pub(super) fn checked_pixel_count(width: usize, height: usize) -> Result<usize, TgaBinaryError> {
263    width
264        .checked_mul(height)
265        .ok_or(TgaBinaryError::ValueOverflow("pixel count"))
266}
267
268pub(super) fn validate_rgba_len(
269    width: u16,
270    height: u16,
271    rgba: &[u8],
272) -> Result<(), TgaBinaryError> {
273    let pixel_count = checked_pixel_count(usize::from(width), usize::from(height))?;
274    let expected_len = pixel_count
275        .checked_mul(4)
276        .ok_or(TgaBinaryError::ValueOverflow("rgba length"))?;
277
278    if expected_len != rgba.len() {
279        return Err(TgaBinaryError::InvalidData(format!(
280            "RGBA length mismatch: expected {expected_len}, got {}",
281            rgba.len()
282        )));
283    }
284
285    Ok(())
286}