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(
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#[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#[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#[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
262fn byte_at(value: u32, shift: u32) -> u8 {
264 u8::try_from((value >> shift) & 0xFF).expect("masked to 8 bits")
265}
266
267fn 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]); 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()); bytes.extend_from_slice(&0_u16.to_le_bytes()); 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 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 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");
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 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");
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 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 bytes.extend_from_slice(&[0, 0, 255]);
508
509 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]); 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 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, });
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; let written = write_tga_to_vec(&tga).expect("write should succeed");
590 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 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]); 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; 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}