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