Files
effectcraft/crates/media/tests/it/exif_orientation.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

85 lines
3.6 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.
//! A still's Exif orientation (a camera held sideways, a mirrored scan) is applied on import: the
//! footage has the size the picture is seen at, and its frames are the picture the right way up.
use std::sync::Arc;
use effectcraft_media::{MediaPool, probe_bytes};
use effectcraft_project::FootageKind;
use effectcraft_time::Tick;
const RED: [f32; 3] = [1.0, 0.0, 0.0];
const GREEN: [f32; 3] = [0.0, 1.0, 0.0];
const BLUE: [f32; 3] = [0.0, 0.0, 1.0];
const WHITE: [f32; 3] = [1.0, 1.0, 1.0];
/// A 32×16 JPEG with red, green / blue, white quarters, tagged with Exif orientation `tag`
/// (`None`: no Exif at all).
fn quartered_jpeg(tag: Option<u8>) -> Vec<u8> {
let img = image::RgbImage::from_fn(32, 16, |x, y| {
let c = match (x < 16, y < 8) {
(true, true) => RED,
(false, true) => GREEN,
(true, false) => BLUE,
(false, false) => WHITE,
};
image::Rgb(c.map(|v| (v * 255.0) as u8))
});
let mut jpeg = Vec::new();
image::codecs::jpeg::JpegEncoder::new_with_quality(&mut jpeg, 100).encode_image(&img).expect("encode");
let Some(tag) = tag else { return jpeg };
// APP1 "Exif": a big-endian TIFF header and one IFD holding Orientation (0x0112, SHORT).
let mut app1 = vec![0xFF, 0xE1, 0x00, 0x22];
app1.extend_from_slice(b"Exif\0\0MM\0\x2A\0\0\0\x08");
app1.extend_from_slice(&[0x00, 0x01, 0x01, 0x12, 0x00, 0x03, 0x00, 0x00, 0x00, 0x01, 0x00, tag, 0x00, 0x00]);
app1.extend_from_slice(&[0x00; 4]);
// (Straight after the SOI marker.)
jpeg.splice(2..2, app1);
jpeg
}
/// The footage size and the colours of its four corners (top-left, top-right, bottom-left,
/// bottom-right), as imported.
fn imported(tag: Option<u8>) -> ((u32, u32), [[f32; 3]; 4]) {
let name = format!("oriented-{tag:?}.jpg");
let bytes: Arc<[u8]> = quartered_jpeg(tag).into();
let f = probe_bytes(&name, bytes.clone()).expect("probe");
assert_eq!(f.kind, FootageKind::Still);
let pool = MediaPool::new();
pool.add_bytes(&name, bytes);
let img = pool.frame_at(&f, Tick::ZERO).expect("frame");
assert_eq!((img.width, img.height), (f.width, f.height), "orientation {tag:?}: the frame has the probed size");
let (w, h) = (img.width as usize, img.height as usize);
let at = |x: usize, y: usize| {
let p = img.data[y * w + x];
[p[0], p[1], p[2]]
};
((f.width, f.height), [at(2, 2), at(w - 3, 2), at(2, h - 3), at(w - 3, h - 3)])
}
#[test]
fn exif_orientation_is_applied_to_stills() {
let (landscape, portrait) = ((32, 16), (16, 32));
let cases = [
(None, landscape, [RED, GREEN, BLUE, WHITE]),
(Some(1), landscape, [RED, GREEN, BLUE, WHITE]),
(Some(2), landscape, [GREEN, RED, WHITE, BLUE]),
(Some(3), landscape, [WHITE, BLUE, GREEN, RED]),
(Some(4), landscape, [BLUE, WHITE, RED, GREEN]),
(Some(5), portrait, [RED, BLUE, GREEN, WHITE]),
(Some(6), portrait, [BLUE, RED, WHITE, GREEN]),
(Some(7), portrait, [WHITE, GREEN, BLUE, RED]),
(Some(8), portrait, [GREEN, WHITE, RED, BLUE]),
// (Not an orientation: the picture as stored.)
(Some(0), landscape, [RED, GREEN, BLUE, WHITE]),
(Some(9), landscape, [RED, GREEN, BLUE, WHITE]),
];
for (tag, size, corners) in cases {
let (got_size, got) = imported(tag);
assert_eq!(got_size, size, "orientation {tag:?}: size");
for (i, (g, want)) in got.iter().zip(corners).enumerate() {
let near = g.iter().zip(want).all(|(a, b)| (a - b).abs() < 0.2);
assert!(near, "orientation {tag:?}: corner {i} is {g:?}, expected {want:?}");
}
}
}