Files
photocraft/crates/codecs/tests/tga.rs
T
F4LCON 75978a72f0 Targa: RLE packets end with their scanline (#6382) (#6386)
The image crate's Targa encoder let run-length packets run on from the end of
one row into the next. TGA 2.0 says a packet must not cross a scanline, and
readers that decode a row at a time misread such files: Pillow fails on them
with "buffer overrun", and texture tools and game engines reject them.

Targa is now written by our own encoder (codecs::tga), as Photoshop writes
it: RLE true colour or grayscale, every packet within its row, rows stored
bottom-up (the origin readers that ignore the descriptor assume), the alpha
bits in the descriptor and the TGA 2.0 footer. Reading stays with the image
crate.
2026-10-16 20:15:32 -06:00

92 lines
4.2 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.
//! Targa writing (#6382): run-length packets end with their scanline, rows are stored bottom-up,
//! and the file reads back exactly.
use photocraft_codecs::{ChannelLayout, EncodeOptions, Format, Image, decode_as, encode};
/// The packets of a run-length encoded Targa body, as (pixels, is a run), from just after the
/// 18-byte header (no image ID or colour map) up to `pixels` pixels.
fn packets(file: &[u8], bpp: usize, pixels: usize) -> Vec<(usize, bool)> {
let mut at = 18;
let mut seen = 0;
let mut out = Vec::new();
while seen < pixels {
let head = file[at];
let count = usize::from(head & 0x7f) + 1;
let run = head & 0x80 != 0;
at += 1 + if run { bpp } else { count * bpp };
seen += count;
out.push((count, run));
}
out
}
/// A `w`×`h` image whose rows mix long runs (wider than a packet), short ones and noise, and
/// whose last pixel equals the next row's first, so a packet would run on if it could.
fn sample(w: u32, h: u32, layout: ChannelLayout) -> Image {
let n = layout.channels();
let mut data = Vec::new();
for y in 0..h {
for x in 0..w {
let v = match x {
_ if x < 150 => 40,
_ if x % 3 == 0 => (x * 7 + y * 13) as u8,
_ => 200,
};
let px = [v, v.wrapping_add(y as u8), v ^ 0x5a, 255 - v];
data.extend_from_slice(&px[..n]);
}
}
Image::from_u8(w, h, layout, data).unwrap()
}
#[test]
fn rle_packets_end_with_their_scanline() {
for layout in [ChannelLayout::Gray, ChannelLayout::GrayA, ChannelLayout::Rgb, ChannelLayout::Rgba] {
let (w, h) = (300u32, 4u32);
let file = encode(&sample(w, h, layout), Format::Tga, &EncodeOptions::default()).unwrap();
let bpp = layout.channels();
let mut col = 0;
for (count, _) in packets(&file, bpp, (w * h) as usize) {
assert!(count <= 128, "{layout:?}: a packet holds at most 128 pixels");
assert!(col + count <= w as usize, "{layout:?}: a packet runs from column {col} past the row's end");
col = (col + count) % w as usize;
}
// A solid image is one run per row (three packets of 128, 128 and 44), not one long run.
let solid = Image::from_u8(w, h, layout, vec![90; (w * h) as usize * bpp]).unwrap();
let file = encode(&solid, Format::Tga, &EncodeOptions::default()).unwrap();
let p = packets(&file, bpp, (w * h) as usize);
assert_eq!(p.len(), 3 * h as usize, "{layout:?}");
assert!(p.iter().all(|&(_, run)| run));
}
}
#[test]
fn the_file_is_bottom_up_rle_with_its_alpha_bits_and_the_tga2_footer() {
let mut data = vec![0u8; 2 * 2 * 4];
// Top row red, bottom row blue, opaque.
for (i, px) in data.as_chunks_mut::<4>().0.iter_mut().enumerate() {
*px = if i < 2 { [255, 0, 0, 255] } else { [0, 0, 255, 255] };
}
let file = encode(&Image::from_u8(2, 2, ChannelLayout::Rgba, data).unwrap(), Format::Tga, &EncodeOptions::default()).unwrap();
assert_eq!(file[2], 10, "run-length true colour");
assert_eq!((file[16], file[17]), (32, 8), "32 bits per pixel, 8 alpha bits, bottom-left origin");
// The first stored row is the bottom one: a run of two blue pixels, in BGRA.
assert_eq!(&file[18..23], &[0x81, 255, 0, 0, 255]);
assert!(file.ends_with(b"TRUEVISION-XFILE.\0"));
let gray = encode(&Image::from_u8(2, 1, ChannelLayout::Gray, vec![1, 2]).unwrap(), Format::Tga, &EncodeOptions::default()).unwrap();
assert_eq!((gray[2], gray[16], gray[17]), (11, 8, 0), "run-length grayscale, no alpha bits");
}
#[test]
fn every_layout_reads_back_exactly() {
for layout in [ChannelLayout::Gray, ChannelLayout::GrayA, ChannelLayout::Rgb, ChannelLayout::Rgba] {
for (w, h) in [(1, 1), (300, 4), (129, 3), (5, 130)] {
let img = sample(w, h, layout);
let file = encode(&img, Format::Tga, &EncodeOptions::default()).unwrap();
let back = decode_as(Format::Tga, &file).unwrap();
assert_eq!((back.dimensions(), back.layout()), ((w, h), layout), "{layout:?} {w}x{h}");
assert_eq!(back.data(), img.data(), "{layout:?} {w}x{h}");
}
}
}