Skip to main content

rakata_formats/tga/
reader.rs

1//! TGA (Targa) reader.
2
3use std::io::Read;
4
5use tinytga::{Bpp, Compression, DataType, ImageOrigin, RawTga};
6
7use super::{
8    checked_pixel_count, Tga, TgaBinaryError, TgaBitsPerPixel, TgaCompression, TgaDataType,
9    TgaHeader, TgaOrigin, TgaReadMode, TgaReadOptions,
10};
11
12/// Reads TGA data from a reader.
13///
14/// # Errors
15///
16/// The same as [`read_tga_with_options`] under the default options.
17#[cfg_attr(
18    feature = "tracing",
19    tracing::instrument(level = "debug", skip(reader))
20)]
21pub fn read_tga<R: Read>(reader: &mut R) -> Result<Tga, TgaBinaryError> {
22    read_tga_with_options(reader, TgaReadOptions::default())
23}
24
25/// Reads TGA data from a reader with explicit options.
26///
27/// # Errors
28///
29/// [`TgaBinaryError::Io`] when the stream will not read to end, and whatever
30/// [`read_tga_from_bytes_with_options`] reports for the bytes it collected.
31#[cfg_attr(
32    feature = "tracing",
33    tracing::instrument(level = "debug", skip(reader, options))
34)]
35pub fn read_tga_with_options<R: Read>(
36    reader: &mut R,
37    options: TgaReadOptions,
38) -> Result<Tga, TgaBinaryError> {
39    let mut bytes = Vec::new();
40    reader.read_to_end(&mut bytes)?;
41    read_tga_from_bytes_with_options(&bytes, options)
42}
43
44/// Reads TGA data from bytes.
45///
46/// # Errors
47///
48/// The same as [`read_tga_from_bytes_with_options`] under the default options.
49#[cfg_attr(
50    feature = "tracing",
51    tracing::instrument(level = "debug", skip(bytes), fields(bytes_len = bytes.len()))
52)]
53pub fn read_tga_from_bytes(bytes: &[u8]) -> Result<Tga, TgaBinaryError> {
54    read_tga_from_bytes_with_options(bytes, TgaReadOptions::default())
55}
56
57/// Reads TGA data from bytes with explicit options.
58///
59/// # Errors
60///
61/// [`TgaBinaryError::InvalidHeader`] when `bytes` are shorter than the header
62/// or its dimensions run past the end, [`TgaBinaryError::Parse`] for an image
63/// or colour-map type this reader has no decoder for, and
64/// [`TgaBinaryError::InvalidData`] when the pixel data is short for the
65/// dimensions declared. [`TgaBinaryError::ValueOverflow`] when those
66/// dimensions multiply past `usize`. TGA carries no magic, so a file that is
67/// not one fails on its declared layout rather than on a signature.
68#[cfg_attr(
69    feature = "tracing",
70    tracing::instrument(level = "debug", skip(bytes, options), fields(bytes_len = bytes.len()))
71)]
72pub fn read_tga_from_bytes_with_options(
73    bytes: &[u8],
74    options: TgaReadOptions,
75) -> Result<Tga, TgaBinaryError> {
76    let raw = RawTga::from_slice(bytes)?;
77    let source_header = raw.header();
78
79    if source_header.width == 0 || source_header.height == 0 {
80        return Err(TgaBinaryError::InvalidHeader(
81            "width/height must be non-zero".into(),
82        ));
83    }
84
85    if matches!(source_header.data_type, DataType::NoData) {
86        return Err(TgaBinaryError::InvalidHeader(
87            "image declares no pixel data".into(),
88        ));
89    }
90
91    validate_read_header_mode(&source_header, options.input)?;
92    validate_uncompressed_source_size(&raw)?;
93
94    let width = usize::from(source_header.width);
95    let height = usize::from(source_header.height);
96    let pixel_count = checked_pixel_count(width, height)?;
97    let mut rgba = vec![0_u8; pixel_count * 4];
98
99    for raw_pixel in raw.pixels() {
100        let mut x = usize::try_from(raw_pixel.position.x)
101            .map_err(|_| TgaBinaryError::InvalidData("pixel x position out of range".into()))?;
102        let y = usize::try_from(raw_pixel.position.y)
103            .map_err(|_| TgaBinaryError::InvalidData("pixel y position out of range".into()))?;
104
105        if x >= width || y >= height {
106            return Err(TgaBinaryError::InvalidData(
107                "pixel position outside declared bounds".into(),
108            ));
109        }
110
111        if matches!(
112            source_header.image_origin,
113            ImageOrigin::TopRight | ImageOrigin::BottomRight
114        ) {
115            x = width - 1 - x;
116        }
117
118        let source_color = resolve_raw_pixel_color(&raw, &source_header, raw_pixel.color)?;
119        let rgba_pixel = decode_raw_color(
120            source_color,
121            raw.color_bpp(),
122            source_header.alpha_channel_depth,
123        )?;
124
125        let offset = (y * width + x)
126            .checked_mul(4)
127            .ok_or(TgaBinaryError::ValueOverflow("rgba offset"))?;
128        rgba[offset..offset + 4].copy_from_slice(&rgba_pixel);
129    }
130
131    let header = map_header(source_header)?;
132    let image_id = raw.image_id().unwrap_or(&[]).to_vec();
133
134    Ok(Tga {
135        header,
136        image_id,
137        rgba_pixels: rgba,
138    })
139}
140
141fn validate_read_header_mode(
142    header: &tinytga::TgaHeader,
143    mode: TgaReadMode,
144) -> Result<(), TgaBinaryError> {
145    if matches!(mode, TgaReadMode::Compatibility) {
146        return Ok(());
147    }
148
149    let supported_type = matches!(
150        (header.data_type, header.compression),
151        (DataType::ColorMapped, Compression::Uncompressed)
152            | (DataType::ColorMapped, Compression::Rle)
153            | (DataType::TrueColor, Compression::Uncompressed)
154            | (DataType::TrueColor, Compression::Rle)
155            | (DataType::BlackAndWhite, Compression::Uncompressed)
156    );
157    if !supported_type {
158        return Err(TgaBinaryError::InvalidHeader(
159            "canonical K1 reader rejects unsupported TGA image type".into(),
160        ));
161    }
162
163    if !matches!(header.pixel_depth, Bpp::Bits8 | Bpp::Bits24 | Bpp::Bits32) {
164        return Err(TgaBinaryError::InvalidHeader(
165            "canonical K1 reader only supports 8/24/32-bit source pixel depth".into(),
166        ));
167    }
168
169    if matches!(
170        (header.data_type, header.compression),
171        (DataType::TrueColor, Compression::Rle)
172    ) && !matches!(header.pixel_depth, Bpp::Bits24 | Bpp::Bits32)
173    {
174        return Err(TgaBinaryError::InvalidHeader(
175            "canonical K1 reader requires true-color RLE payloads to be 24 or 32-bit".into(),
176        ));
177    }
178
179    Ok(())
180}
181
182fn map_header(source: tinytga::TgaHeader) -> Result<TgaHeader, TgaBinaryError> {
183    Ok(TgaHeader {
184        id_len: source.id_len,
185        has_color_map: source.has_color_map,
186        data_type: map_data_type(source.data_type),
187        compression: map_compression(source.compression),
188        color_map_start: source.color_map_start,
189        color_map_len: source.color_map_len,
190        color_map_depth: source
191            .color_map_depth
192            .map(TgaBitsPerPixel::try_from_tinytga)
193            .transpose()?,
194        x_origin: source.x_origin,
195        y_origin: source.y_origin,
196        width: source.width,
197        height: source.height,
198        pixel_depth: TgaBitsPerPixel::try_from_tinytga(source.pixel_depth)?,
199        image_origin: map_origin(source.image_origin),
200        alpha_channel_depth: source.alpha_channel_depth,
201    })
202}
203
204fn map_data_type(data_type: DataType) -> TgaDataType {
205    match data_type {
206        DataType::NoData => TgaDataType::NoData,
207        DataType::ColorMapped => TgaDataType::ColorMapped,
208        DataType::TrueColor => TgaDataType::TrueColor,
209        DataType::BlackAndWhite => TgaDataType::BlackAndWhite,
210    }
211}
212
213fn map_compression(compression: Compression) -> TgaCompression {
214    match compression {
215        Compression::Uncompressed => TgaCompression::Uncompressed,
216        Compression::Rle => TgaCompression::Rle,
217    }
218}
219
220fn map_origin(origin: ImageOrigin) -> TgaOrigin {
221    match origin {
222        ImageOrigin::BottomLeft => TgaOrigin::BottomLeft,
223        ImageOrigin::BottomRight => TgaOrigin::BottomRight,
224        ImageOrigin::TopLeft => TgaOrigin::TopLeft,
225        ImageOrigin::TopRight => TgaOrigin::TopRight,
226    }
227}
228
229fn resolve_raw_pixel_color(
230    raw: &RawTga<'_>,
231    source_header: &tinytga::TgaHeader,
232    raw_color: u32,
233) -> Result<u32, TgaBinaryError> {
234    let Some(color_map) = raw.color_map() else {
235        return Ok(raw_color);
236    };
237
238    let raw_index = usize::try_from(raw_color)
239        .map_err(|_| TgaBinaryError::InvalidData("color-map index overflow".into()))?;
240    let start = usize::from(source_header.color_map_start);
241    let length = usize::from(source_header.color_map_len);
242
243    let palette_index = raw_index.checked_sub(start).ok_or_else(|| {
244        TgaBinaryError::InvalidData(format!(
245            "color-map index {raw_index} below color_map_start {start}"
246        ))
247    })?;
248
249    if palette_index >= length {
250        return Err(TgaBinaryError::InvalidData(format!(
251            "color-map index {raw_index} out of range for length {length}"
252        )));
253    }
254
255    color_map.get_raw(palette_index).ok_or_else(|| {
256        TgaBinaryError::InvalidData(format!(
257            "missing color-map entry at palette index {palette_index}"
258        ))
259    })
260}
261
262/// Extracts bits `[shift .. shift+8)` from a `u32` as a `u8`.
263fn byte_at(value: u32, shift: u32) -> u8 {
264    u8::try_from((value >> shift) & 0xFF).expect("masked to 8 bits")
265}
266
267/// Extracts bits `[shift .. shift+5)` from a `u16` as a `u8`.
268fn bits5_at(value: u16, shift: u16) -> u8 {
269    u8::try_from((value >> shift) & 0x1F).expect("masked to 5 bits")
270}
271
272fn decode_raw_color(
273    raw_color: u32,
274    bpp: Bpp,
275    alpha_channel_depth: u8,
276) -> Result<[u8; 4], TgaBinaryError> {
277    match bpp {
278        Bpp::Bits8 => {
279            let v = byte_at(raw_color, 0);
280            Ok([v, v, v, 255])
281        }
282        Bpp::Bits16 => {
283            let value = u16::try_from(raw_color & 0xFFFF).expect("masked to 16 bits");
284            let blue = expand_5bit(bits5_at(value, 0));
285            let green = expand_5bit(bits5_at(value, 5));
286            let red = expand_5bit(bits5_at(value, 10));
287            let alpha = if alpha_channel_depth > 0 && (value & 0x8000) == 0 {
288                0
289            } else {
290                255
291            };
292            Ok([red, green, blue, alpha])
293        }
294        Bpp::Bits24 => Ok([
295            byte_at(raw_color, 16),
296            byte_at(raw_color, 8),
297            byte_at(raw_color, 0),
298            255,
299        ]),
300        Bpp::Bits32 => Ok([
301            byte_at(raw_color, 16),
302            byte_at(raw_color, 8),
303            byte_at(raw_color, 0),
304            byte_at(raw_color, 24),
305        ]),
306        _ => Err(TgaBinaryError::InvalidData(
307            "unsupported bits-per-pixel value in decoded source data".into(),
308        )),
309    }
310}
311
312fn expand_5bit(value: u8) -> u8 {
313    (value << 3) | (value >> 2)
314}
315
316fn validate_uncompressed_source_size(raw: &RawTga<'_>) -> Result<(), TgaBinaryError> {
317    if !matches!(raw.compression(), Compression::Uncompressed) {
318        return Ok(());
319    }
320
321    let width =
322        usize::try_from(raw.size().width).map_err(|_| TgaBinaryError::ValueOverflow("width"))?;
323    let height =
324        usize::try_from(raw.size().height).map_err(|_| TgaBinaryError::ValueOverflow("height"))?;
325    let pixel_count = checked_pixel_count(width, height)?;
326    let expected_bytes = pixel_count
327        .checked_mul(usize::from(raw.image_data_bpp().bytes()))
328        .ok_or(TgaBinaryError::ValueOverflow(
329            "uncompressed image data bytes",
330        ))?;
331
332    if raw.image_data().len() < expected_bytes {
333        return Err(TgaBinaryError::InvalidData(format!(
334            "truncated uncompressed image data: expected at least {expected_bytes} bytes, got {}",
335            raw.image_data().len()
336        )));
337    }
338
339    Ok(())
340}
341
342#[cfg(test)]
343mod tests {
344    use super::*;
345    use crate::tga::write_tga_to_vec;
346
347    const CUBE_TGA: &[u8] = include_bytes!(concat!(
348        env!("CARGO_MANIFEST_DIR"),
349        "/../../fixtures/textures/cube/cm_506ond.tga"
350    ));
351
352    struct TestHeaderSpec {
353        color_map_type: u8,
354        image_type: u8,
355        color_map_start: u16,
356        color_map_len: u16,
357        color_map_depth: u8,
358        width: u16,
359        height: u16,
360        pixel_depth: u8,
361        descriptor: u8,
362    }
363
364    fn make_tga_header(spec: TestHeaderSpec) -> Vec<u8> {
365        let mut bytes = Vec::new();
366        bytes.extend_from_slice(&[0]); // id length
367        bytes.extend_from_slice(&[spec.color_map_type]);
368        bytes.extend_from_slice(&[spec.image_type]);
369        bytes.extend_from_slice(&spec.color_map_start.to_le_bytes());
370        bytes.extend_from_slice(&spec.color_map_len.to_le_bytes());
371        bytes.extend_from_slice(&[spec.color_map_depth]);
372        bytes.extend_from_slice(&0_u16.to_le_bytes()); // x origin
373        bytes.extend_from_slice(&0_u16.to_le_bytes()); // y origin
374        bytes.extend_from_slice(&spec.width.to_le_bytes());
375        bytes.extend_from_slice(&spec.height.to_le_bytes());
376        bytes.extend_from_slice(&[spec.pixel_depth]);
377        bytes.extend_from_slice(&[spec.descriptor]);
378        bytes
379    }
380
381    #[test]
382    fn parses_tga_fixture() {
383        let tga = read_tga_from_bytes(CUBE_TGA).expect("fixture should parse");
384
385        assert_eq!(tga.header.width, 4);
386        assert_eq!(tga.header.height, 4);
387        assert_eq!(tga.rgba_pixels.len(), 4 * 4 * 4);
388    }
389
390    #[test]
391    fn roundtrip_canonical_rgba_tga() {
392        let mut tga = Tga::new_rgba(
393            2,
394            2,
395            vec![
396                255, 0, 0, 255, 0, 255, 0, 255, 0, 0, 255, 255, 255, 255, 0, 128,
397            ],
398        )
399        .expect("create tga");
400        tga.image_id = b"rakata-rs".to_vec();
401
402        let bytes = write_tga_to_vec(&tga).expect("write should succeed");
403        let parsed = read_tga_from_bytes(&bytes).expect("read should succeed");
404
405        assert_eq!(parsed.header.width, 2);
406        assert_eq!(parsed.header.height, 2);
407        assert_eq!(parsed.image_id, b"rakata-rs");
408        assert_eq!(parsed.rgba_pixels, tga.rgba_pixels);
409
410        // Canonical writer emits uncompressed true-color 32bpp top-left.
411        assert_eq!(bytes[2], 2);
412        assert_eq!(bytes[16], 32);
413        assert_eq!(bytes[17], 0x28);
414    }
415
416    #[test]
417    fn writer_is_deterministic_for_canonical_tga() {
418        let mut tga = Tga::new_rgba(
419            2,
420            2,
421            vec![
422                255, 0, 0, 255, 0, 255, 0, 255, 0, 0, 255, 255, 255, 255, 0, 128,
423            ],
424        )
425        .expect("create tga");
426        tga.image_id = b"rakata-rs".to_vec();
427
428        let first = write_tga_to_vec(&tga).expect("first write should succeed");
429        let second = write_tga_to_vec(&tga).expect("second write should succeed");
430        assert_eq!(first, second, "canonical TGA writer output drifted");
431    }
432
433    #[test]
434    fn rejects_truncated_header() {
435        let bytes = vec![0_u8; 17];
436        let err = read_tga_from_bytes(&bytes).expect_err("must fail");
437        assert!(matches!(
438            err,
439            TgaBinaryError::Parse(_) | TgaBinaryError::InvalidHeader(_)
440        ));
441    }
442
443    #[test]
444    fn normalizes_bottom_left_origin_to_top_left() {
445        let mut bytes = make_tga_header(TestHeaderSpec {
446            color_map_type: 0,
447            image_type: 2,
448            color_map_start: 0,
449            color_map_len: 0,
450            color_map_depth: 0,
451            width: 1,
452            height: 2,
453            pixel_depth: 24,
454            descriptor: 0x00,
455        });
456
457        // Stored as bottom row then top row for bottom-left origin.
458        bytes.extend_from_slice(&[255, 0, 0]); // blue (BGR)
459        bytes.extend_from_slice(&[0, 255, 255]); // yellow (BGR)
460
461        let tga = read_tga_from_bytes(&bytes).expect("parse should succeed");
462        assert_eq!(tga.rgba_pixels, vec![255, 255, 0, 255, 0, 0, 255, 255]);
463    }
464
465    #[test]
466    fn normalizes_top_right_origin_to_top_left() {
467        let mut bytes = make_tga_header(TestHeaderSpec {
468            color_map_type: 0,
469            image_type: 2,
470            color_map_start: 0,
471            color_map_len: 0,
472            color_map_depth: 0,
473            width: 2,
474            height: 1,
475            pixel_depth: 24,
476            descriptor: 0x30,
477        });
478
479        // Top-right origin: first pixel belongs to the right-most column.
480        bytes.extend_from_slice(&[0, 0, 255]); // red (BGR)
481        bytes.extend_from_slice(&[0, 255, 0]); // green (BGR)
482
483        let tga = read_tga_from_bytes(&bytes).expect("parse should succeed");
484        assert_eq!(
485            tga.rgba_pixels,
486            vec![0, 255, 0, 255, 255, 0, 0, 255],
487            "x-axis should be normalized to top-left row-major ordering"
488        );
489    }
490
491    #[test]
492    fn honors_color_map_start_index() {
493        // Type 1: uncompressed color-mapped image.
494        let mut bytes = make_tga_header(TestHeaderSpec {
495            color_map_type: 1,
496            image_type: 1,
497            color_map_start: 1,
498            color_map_len: 1,
499            color_map_depth: 24,
500            width: 1,
501            height: 1,
502            pixel_depth: 8,
503            descriptor: 0x20,
504        });
505
506        // One palette entry at logical index 1: red in BGR form.
507        bytes.extend_from_slice(&[0, 0, 255]);
508
509        // Pixel references logical palette index 1.
510        bytes.push(1);
511
512        let tga = read_tga_from_bytes(&bytes).expect("parse should succeed");
513        assert_eq!(tga.rgba_pixels, vec![255, 0, 0, 255]);
514    }
515
516    #[test]
517    fn rejects_truncated_uncompressed_pixel_data() {
518        let mut bytes = make_tga_header(TestHeaderSpec {
519            color_map_type: 0,
520            image_type: 2,
521            color_map_start: 0,
522            color_map_len: 0,
523            color_map_depth: 0,
524            width: 2,
525            height: 2,
526            pixel_depth: 24,
527            descriptor: 0x20,
528        });
529        bytes.extend_from_slice(&[0, 0, 0]); // too short; expected at least 12
530
531        let err = read_tga_from_bytes(&bytes).expect_err("must fail");
532        assert!(matches!(err, TgaBinaryError::InvalidData(_)));
533    }
534
535    #[test]
536    fn writer_rejects_mismatched_rgba_length() {
537        let tga = Tga {
538            header: TgaHeader {
539                id_len: 0,
540                has_color_map: false,
541                data_type: TgaDataType::TrueColor,
542                compression: TgaCompression::Uncompressed,
543                color_map_start: 0,
544                color_map_len: 0,
545                color_map_depth: None,
546                x_origin: 0,
547                y_origin: 0,
548                width: 2,
549                height: 2,
550                pixel_depth: TgaBitsPerPixel::Bits32,
551                image_origin: TgaOrigin::TopLeft,
552                alpha_channel_depth: 8,
553            },
554            image_id: Vec::new(),
555            rgba_pixels: vec![0_u8; 3],
556        };
557
558        let err = write_tga_to_vec(&tga).expect_err("must fail");
559        assert!(matches!(err, TgaBinaryError::InvalidData(_)));
560    }
561
562    #[test]
563    fn bits_enum_reports_depth_values() {
564        assert_eq!(TgaBitsPerPixel::Bits8.bits(), 8);
565        assert_eq!(TgaBitsPerPixel::Bits16.bits(), 16);
566        assert_eq!(TgaBitsPerPixel::Bits24.bits(), 24);
567        assert_eq!(TgaBitsPerPixel::Bits32.bits(), 32);
568    }
569
570    #[test]
571    fn canonical_output_when_pixels_edited() {
572        // Parse a non-canonical bottom-left 24bpp TGA, then edit a pixel.
573        let mut src_bytes = make_tga_header(TestHeaderSpec {
574            color_map_type: 0,
575            image_type: 2,
576            color_map_start: 0,
577            color_map_len: 0,
578            color_map_depth: 0,
579            width: 1,
580            height: 1,
581            pixel_depth: 24,
582            descriptor: 0x00, // non-canonical bottom-left
583        });
584        src_bytes.extend_from_slice(&[0, 0, 255]);
585
586        let mut tga = read_tga_from_bytes(&src_bytes).expect("parse should succeed");
587        tga.rgba_pixels[0] = 0; // edit pixel -- triggers canonical output
588
589        let written = write_tga_to_vec(&tga).expect("write should succeed");
590        // Canonical: image_type=2, pixel_depth=32, descriptor=0x28 (top-left, 8-bit alpha).
591        assert_eq!(written[2], 2, "canonical output must use image_type 2");
592        assert_eq!(written[16], 32, "canonical output must use 32bpp");
593        assert_eq!(
594            written[17], 0x28,
595            "canonical output must be top-left with 8-bit alpha"
596        );
597        assert_ne!(
598            written, src_bytes,
599            "edited output must not equal source bytes"
600        );
601    }
602
603    #[test]
604    fn compat_tga_uses_canonical_output() {
605        // When pixels ARE edited after a compat-mode parse, writer falls back to canonical.
606        let mut bytes = make_tga_header(TestHeaderSpec {
607            color_map_type: 0,
608            image_type: 11,
609            color_map_start: 0,
610            color_map_len: 0,
611            color_map_depth: 0,
612            width: 1,
613            height: 1,
614            pixel_depth: 8,
615            descriptor: 0x20,
616        });
617        bytes.extend_from_slice(&[0x80, 0x40]); // one-pixel grayscale RLE packet
618
619        let mut tga = read_tga_from_bytes_with_options(
620            &bytes,
621            TgaReadOptions {
622                input: TgaReadMode::Compatibility,
623            },
624        )
625        .expect("compat parse should succeed");
626        tga.rgba_pixels[0] ^= 1; // edit a pixel -- triggers canonical output
627
628        let written = write_tga_to_vec(&tga).expect("write should succeed");
629        assert_eq!(written[2], 2, "canonical output must use true-color type 2");
630        assert_eq!(written[16], 32, "canonical output must use 32bpp");
631        assert_eq!(
632            written[17], 0x28,
633            "canonical output must be top-left with 8-bit alpha"
634        );
635
636        let parsed = read_tga_from_bytes(&written).expect("canonical output should parse");
637        assert_eq!(parsed.rgba_pixels.len(), tga.rgba_pixels.len());
638    }
639
640    #[test]
641    fn canonical_reader_rejects_grayscale_rle_type11() {
642        let mut bytes = make_tga_header(TestHeaderSpec {
643            color_map_type: 0,
644            image_type: 11,
645            color_map_start: 0,
646            color_map_len: 0,
647            color_map_depth: 0,
648            width: 1,
649            height: 1,
650            pixel_depth: 8,
651            descriptor: 0x20,
652        });
653        bytes.extend_from_slice(&[0x80, 0x40]);
654
655        let err = read_tga_from_bytes(&bytes).expect_err("canonical parse must fail");
656        assert!(matches!(err, TgaBinaryError::InvalidHeader(_)));
657    }
658
659    #[test]
660    fn compatibility_reader_allows_grayscale_rle_type11() {
661        let mut bytes = make_tga_header(TestHeaderSpec {
662            color_map_type: 0,
663            image_type: 11,
664            color_map_start: 0,
665            color_map_len: 0,
666            color_map_depth: 0,
667            width: 1,
668            height: 1,
669            pixel_depth: 8,
670            descriptor: 0x20,
671        });
672        bytes.extend_from_slice(&[0x80, 0x40]);
673
674        let parsed = read_tga_from_bytes_with_options(
675            &bytes,
676            TgaReadOptions {
677                input: TgaReadMode::Compatibility,
678            },
679        )
680        .expect("compat parse must succeed");
681        assert_eq!(parsed.rgba_pixels.len(), 4);
682    }
683}