Files
effectcraft/crates/media/tests/it/tga.rs
T
aiongg 0173abde8c CI: dev-profile gate, one test binary per crate, nextest, line tables only (#2235)
* Dev profile: line tables only for workspace crates, no debug info for dependencies

* xtask ci: dev-profile gate, nextest, tests without examples, wasm --keep-going

* One integration-test binary per crate

* xtask ci: run effectcraft-gpu's shared-device tests in one cargo test process

* xtask ci: run every stage and report all failures

* xtask ci: name the stage type (clippy type_complexity)

* xtask ci: run every test pass and report all failures

* cargo fmt
2026-10-23 12:59:51 -04:00

116 lines
5.0 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.
//! Tiny synthetic Targa files exercise import without external fixtures or codecs.
use std::path::Path;
use std::sync::Arc;
use effectcraft_media::{MediaPool, is_importable, probe_bytes};
use effectcraft_project::{AlphaMode, Footage, FootageKind};
use effectcraft_time::Tick;
/// A classic 2×2 TGA without a footer. True-colour pixels are stored BGR(A); RLE
/// packets repeat each row's colour twice. `pixels` is always in top-down RGBA order.
fn tga(pixels: [[u8; 4]; 4], alpha: bool, rle: bool, top: bool) -> Vec<u8> {
let mut bytes = vec![0; 18];
bytes[2] = if rle { 10 } else { 2 };
bytes[12] = 2;
bytes[14] = 2;
bytes[16] = if alpha { 32 } else { 24 };
bytes[17] = if top { 0x20 } else { 0 } | if alpha { 8 } else { 0 };
let rows = if top { [[0, 1], [2, 3]] } else { [[2, 3], [0, 1]] };
for row in rows {
if rle {
assert_eq!(pixels[row[0]], pixels[row[1]]);
bytes.push(0x81); // A two-pixel run.
}
for &i in row.iter().take(if rle { 1 } else { 2 }) {
let [r, g, b, a] = pixels[i];
bytes.extend_from_slice(&[b, g, r]);
if alpha {
bytes.push(a);
}
}
}
bytes
}
#[test]
fn tga_raw_and_rle_preserve_colours_orientation_and_alpha() {
for alpha in [false, true] {
for rle in [false, true] {
for top in [false, true] {
let pixels = if rle {
[[255, 0, 0, 128], [255, 0, 0, 128], [0, 0, 255, 255], [0, 0, 255, 255]]
} else {
[[255, 0, 0, 128], [0, 255, 0, 255], [0, 0, 255, 64], [255, 255, 255, 0]]
};
let path = if top { "/frame.TGA" } else { "/frame.tga" };
assert!(is_importable(Path::new(path)));
let bytes: Arc<[u8]> = tga(pixels, alpha, rle, top).into();
let footage = probe_bytes(path, bytes.clone()).unwrap();
assert_eq!(footage.kind, FootageKind::Still);
assert_eq!((footage.width, footage.height), (2, 2));
assert_eq!(footage.codec, "TGA");
assert_eq!(footage.alpha, if alpha { AlphaMode::Straight } else { AlphaMode::Ignore });
assert!(footage.has_video && !footage.has_audio);
let pool = MediaPool::new();
pool.add_bytes(path, bytes);
let image = pool.frame_at(&footage, Tick::ZERO).unwrap();
for (actual, [r, g, b, a]) in image.data.iter().zip(pixels) {
let a = if alpha { f32::from(a) / 255.0 } else { 1.0 };
let expected = [f32::from(r) / 255.0 * a, f32::from(g) / 255.0 * a, f32::from(b) / 255.0 * a, a];
assert!(actual.iter().zip(expected).all(|(a, b)| (a - b).abs() < 1e-6), "{actual:?} != {expected:?}");
}
}
}
}
}
#[test]
fn tga_sequence_decodes_each_frame_from_memory() {
let pool = MediaPool::new();
pool.set_read_ahead(false);
let paths = ["/shot_0001.tga", "/shot_0002.tga"];
let colours = [[255, 0, 0, 255], [0, 0, 255, 255]];
let mut footage = None;
for (path, colour) in paths.iter().zip(colours) {
let bytes: Arc<[u8]> = tga([colour; 4], false, true, true).into();
footage.get_or_insert_with(|| probe_bytes(path, bytes.clone()).unwrap());
pool.add_bytes(path, bytes);
}
let mut footage = Footage { kind: FootageKind::Sequence, sequence: paths.iter().map(|s| s.to_string()).collect(), ..footage.unwrap() };
footage.sync_sequence_duration();
for (n, expected) in [[1.0, 0.0, 0.0, 1.0], [0.0, 0.0, 1.0, 1.0]].into_iter().enumerate() {
let image = pool.frame_at(&footage, footage.frame_rate.tick_of(n as i64)).unwrap();
assert_eq!(image.get(0, 0), expected);
}
}
#[test]
fn malformed_tga_returns_contextual_errors() {
for bytes in [vec![], vec![0; 17], vec![0; 18]] {
let error = probe_bytes("/broken.tga", bytes.into()).unwrap_err().to_string();
assert!(error.contains("/broken.tga"), "{error}");
}
let mut bytes = tga([[255, 0, 0, 255]; 4], true, false, true);
bytes.truncate(19); // Valid metadata, truncated pixel data.
let bytes: Arc<[u8]> = bytes.into();
let footage = probe_bytes("/truncated.tga", bytes.clone()).unwrap();
let pool = MediaPool::new();
pool.add_bytes(&footage.path, bytes);
let error = pool.frame_at(&footage, Tick::ZERO).unwrap_err().to_string();
assert!(error.contains("/truncated.tga"), "{error}");
}
#[test]
fn a_recognised_signature_takes_precedence_over_the_tga_extension() {
let mut bytes = std::io::Cursor::new(Vec::new());
image::DynamicImage::new_rgb8(3, 2).write_to(&mut bytes, image::ImageFormat::Png).unwrap();
let bytes: Arc<[u8]> = bytes.into_inner().into();
let footage = probe_bytes("/actually-png.tga", bytes.clone()).unwrap();
assert_eq!(footage.codec, "PNG");
let pool = MediaPool::new();
pool.add_bytes(&footage.path, bytes);
let image = pool.frame_at(&footage, Tick::ZERO).unwrap();
assert_eq!((image.width, image.height), (3, 2));
}