Files
effectcraft/crates/media/tests/tga.rs

116 lines
5.0 KiB
Rust
Raw Permalink 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));
}