#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub(crate) struct Orientation(u8);
impl Orientation {
pub(crate) const UPRIGHT: Orientation = Orientation(1);
pub(crate) fn is_upright(self) -> bool {
self.0 == 1
}
pub(crate) fn swaps_axes(self) -> bool {
self.0 >= 5
}
pub(crate) fn display_dims(self, w: usize, h: usize) -> (usize, usize) {
if self.swaps_axes() { (h, w) } else { (w, h) }
}
pub(crate) fn from_exif_app1(data: &[u8]) -> Option<Orientation> {
parse_tiff_orientation(data.strip_prefix(b"Exif\0\0")?)
}
pub(crate) fn from_exif_payload(data: &[u8]) -> Option<Orientation> {
parse_tiff_orientation(data.strip_prefix(b"Exif\0\0").unwrap_or(data))
}
#[cfg_attr(not(feature = "avif"), allow(dead_code))]
pub(crate) fn from_rot_mirror(angle_ccw: u8, mirror: Option<u8>) -> Orientation {
Orientation(match (angle_ccw & 3, mirror.map(|m| m & 1)) {
(0, None) => 1,
(1, None) => 8,
(2, None) => 3,
(3, None) => 6,
(0, Some(1)) => 2,
(1, Some(1)) => 7,
(2, Some(1)) => 4,
(3, Some(1)) => 5,
(0, Some(_)) => 4,
(1, Some(_)) => 5,
(2, Some(_)) => 2,
(3, Some(_)) => 7,
_ => unreachable!("angle is masked to 0..=3"),
})
}
}
fn parse_tiff_orientation(tiff: &[u8]) -> Option<Orientation> {
let big_endian = match tiff.get(0..2)? {
b"II" => false,
b"MM" => true,
_ => return None,
};
let u16_at = |off: usize| -> Option<u16> {
let b: [u8; 2] = tiff.get(off..off.checked_add(2)?)?.try_into().ok()?;
Some(if big_endian {
u16::from_be_bytes(b)
} else {
u16::from_le_bytes(b)
})
};
let u32_at = |off: usize| -> Option<u32> {
let b: [u8; 4] = tiff.get(off..off.checked_add(4)?)?.try_into().ok()?;
Some(if big_endian {
u32::from_be_bytes(b)
} else {
u32::from_le_bytes(b)
})
};
if u16_at(2)? != 42 {
return None;
}
let ifd0 = u32_at(4)? as usize;
let entries = u16_at(ifd0)? as usize;
for i in 0..entries.min(512) {
let e = ifd0.checked_add(2)?.checked_add(i * 12)?;
if u16_at(e)? == 0x0112 {
if u16_at(e + 2)? != 3 || u32_at(e + 4)? != 1 {
return None;
}
let v = u16_at(e + 8)?;
return (1..=8).contains(&v).then_some(Orientation(v as u8));
}
}
None
}
const SCAN_CAP: usize = 1024 * 1024;
pub(crate) struct JpegMeta {
pub(crate) orientation: Orientation,
pub(crate) icc: Option<Vec<u8>>,
pub(crate) progressive: bool,
}
impl JpegMeta {
pub(crate) const NONE: JpegMeta = JpegMeta {
orientation: Orientation::UPRIGHT,
icc: None,
progressive: false,
};
}
#[derive(Default)]
struct IccAssembler {
chunks: Vec<Option<Vec<u8>>>,
broken: bool,
}
impl IccAssembler {
fn add(&mut self, body: &[u8]) {
if self.broken {
return;
}
let Some(rest) = body.strip_prefix(b"ICC_PROFILE\0") else {
return;
};
if rest.len() < 2 {
return;
}
let (seq, count, data) = (rest[0] as usize, rest[1] as usize, &rest[2..]);
if seq == 0 || count == 0 || seq > count {
self.broken = true;
return;
}
if self.chunks.is_empty() {
self.chunks = vec![None; count];
}
if self.chunks.len() != count || self.chunks[seq - 1].is_some() {
self.broken = true;
return;
}
self.chunks[seq - 1] = Some(data.to_vec());
}
fn finish(self) -> Option<Vec<u8>> {
if self.broken || self.chunks.is_empty() {
return None;
}
let mut out = Vec::new();
for c in self.chunks {
out.extend_from_slice(&c?);
}
(!out.is_empty()).then_some(out)
}
}
pub(crate) fn scan_jpeg_meta<R: std::io::BufRead>(
reader: &mut R,
prefix: &mut Vec<u8>,
want_icc: bool,
) -> JpegMeta {
prefix.clear();
let mut meta = JpegMeta::NONE;
let mut exif_seen = false;
let mut icc = IccAssembler::default();
let mut take = |prefix: &mut Vec<u8>, n: usize| -> Option<usize> {
let start = prefix.len();
if start + n > SCAN_CAP {
return None;
}
let mut remaining = n;
while remaining > 0 {
let chunk = match reader.fill_buf() {
Ok(b) => b,
Err(e) if e.kind() == std::io::ErrorKind::Interrupted => continue,
Err(_) => return None,
};
if chunk.is_empty() {
return None; }
let k = chunk.len().min(remaining);
prefix.extend_from_slice(&chunk[..k]);
reader.consume(k);
remaining -= k;
}
Some(start)
};
'walk: {
let Some(soi) = take(prefix, 2) else {
break 'walk;
};
if prefix[soi..] != [0xFF, 0xD8] {
break 'walk;
}
loop {
let Some(m) = take(prefix, 2) else {
break 'walk;
};
if prefix[m] != 0xFF {
break 'walk;
}
let mut marker = prefix[m + 1];
while marker == 0xFF {
let Some(b) = take(prefix, 1) else {
break 'walk;
};
marker = prefix[b];
}
if marker == 0xDA || marker == 0xD9 {
break 'walk;
}
if marker == 0x01 || (0xD0..=0xD7).contains(&marker) {
continue;
}
if marker == 0xC2 {
meta.progressive = true;
}
let Some(l) = take(prefix, 2) else {
break 'walk;
};
let len = u16::from_be_bytes([prefix[l], prefix[l + 1]]) as usize;
if len < 2 {
break 'walk;
}
let Some(body) = take(prefix, len - 2) else {
break 'walk;
};
if marker == 0xE1 && !exif_seen && prefix[body..].starts_with(b"Exif\0\0") {
exif_seen = true;
meta.orientation =
Orientation::from_exif_app1(&prefix[body..]).unwrap_or(Orientation::UPRIGHT);
if !want_icc {
break 'walk;
}
} else if want_icc && marker == 0xE2 {
icc.add(&prefix[body..]);
}
}
}
if want_icc {
meta.icc = icc.finish();
}
meta
}
pub(crate) fn apply_orientation(
src: &[u8],
w: usize,
h: usize,
channels: usize,
o: Orientation,
dst: &mut Vec<u8>,
) -> (usize, usize) {
debug_assert!(src.len() >= w * h * channels);
let (dw, dh) = o.display_dims(w, h);
if dst.len() < dw * dh * channels {
dst.resize(dw * dh * channels, 0);
}
dst.truncate(dw * dh * channels);
match channels {
3 => orient_pixels::<3>(src, w, h, o, dst),
4 => orient_pixels::<4>(src, w, h, o, dst),
_ => apply_orientation_reference(src, w, h, channels, o, dst),
}
(dw, dh)
}
fn orient_pixels<const C: usize>(src: &[u8], w: usize, h: usize, o: Orientation, dst: &mut [u8]) {
match o.0 {
1 => dst.copy_from_slice(&src[..w * h * C]),
2 => {
for (drow, srow) in dst
.chunks_exact_mut(w * C)
.zip(src[..w * h * C].chunks_exact(w * C))
{
for (d, s) in drow.chunks_exact_mut(C).zip(srow.chunks_exact(C).rev()) {
d.copy_from_slice(s);
}
}
}
3 => {
for (d, s) in dst
.chunks_exact_mut(C)
.zip(src[..w * h * C].chunks_exact(C).rev())
{
d.copy_from_slice(s);
}
}
4 => {
for (drow, srow) in dst
.chunks_exact_mut(w * C)
.zip(src[..w * h * C].chunks_exact(w * C).rev())
{
drow.copy_from_slice(srow);
}
}
5 => orient_tiles::<C>(src, w, h, h, w, dst, |x, y, _w, _h| (y, x)),
6 => orient_tiles::<C>(src, w, h, h, w, dst, |x, y, _w, h| (y, h - 1 - x)),
7 => orient_tiles::<C>(src, w, h, h, w, dst, |x, y, w, h| (w - 1 - y, h - 1 - x)),
_ => orient_tiles::<C>(src, w, h, h, w, dst, |x, y, w, _h| (w - 1 - y, x)),
}
}
#[inline]
fn orient_tiles<const C: usize>(
src: &[u8],
w: usize,
h: usize,
dw: usize,
dh: usize,
dst: &mut [u8],
f: impl Fn(usize, usize, usize, usize) -> (usize, usize) + Copy,
) {
const B: usize = 64;
for ty in (0..dh).step_by(B) {
let y_end = (ty + B).min(dh);
for tx in (0..dw).step_by(B) {
let x_end = (tx + B).min(dw);
for y in ty..y_end {
let row = &mut dst[(y * dw + tx) * C..(y * dw + x_end) * C];
for (px, x) in row.chunks_exact_mut(C).zip(tx..x_end) {
let (sx, sy) = f(x, y, w, h);
let s = (sy * w + sx) * C;
px.copy_from_slice(&src[s..s + C]);
}
}
}
}
}
fn apply_orientation_reference(
src: &[u8],
w: usize,
h: usize,
channels: usize,
o: Orientation,
dst: &mut [u8],
) {
let (dw, dh) = o.display_dims(w, h);
let src_xy: fn(usize, usize, usize, usize) -> (usize, usize) = match o.0 {
1 => |x, y, _w, _h| (x, y),
2 => |x, y, w, _h| (w - 1 - x, y),
3 => |x, y, w, h| (w - 1 - x, h - 1 - y),
4 => |x, y, _w, h| (x, h - 1 - y),
5 => |x, y, _w, _h| (y, x),
6 => |x, y, _w, h| (y, h - 1 - x),
7 => |x, y, w, h| (w - 1 - y, h - 1 - x),
_ => |x, y, w, _h| (w - 1 - y, x),
};
for y in 0..dh {
let row = &mut dst[y * dw * channels..(y + 1) * dw * channels];
for (x, px) in row.chunks_exact_mut(channels).enumerate() {
let (sx, sy) = src_xy(x, y, w, h);
let s = (sy * w + sx) * channels;
px.copy_from_slice(&src[s..s + channels]);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
type U16Bytes = fn(u16) -> [u8; 2];
type U32Bytes = fn(u32) -> [u8; 4];
fn app1(orientation: u16, big_endian: bool) -> Vec<u8> {
let mut v = b"Exif\0\0".to_vec();
let (u16b, u32b): (U16Bytes, U32Bytes) = if big_endian {
(u16::to_be_bytes, u32::to_be_bytes)
} else {
(u16::to_le_bytes, u32::to_le_bytes)
};
v.extend(if big_endian { *b"MM" } else { *b"II" });
v.extend(u16b(42));
v.extend(u32b(8)); v.extend(u16b(1)); v.extend(u16b(0x0112)); v.extend(u16b(3)); v.extend(u32b(1)); v.extend(u16b(orientation));
v.extend(u16b(0)); v.extend(u32b(0)); v
}
#[test]
fn parses_both_endiannesses_and_all_values() {
for be in [false, true] {
for o in 1..=8u16 {
assert_eq!(
Orientation::from_exif_app1(&app1(o, be)),
Some(Orientation(o as u8)),
"o={o} be={be}"
);
}
}
}
#[test]
fn malformed_exif_is_none_not_an_error() {
let good = app1(6, false);
let cases: Vec<Vec<u8>> = vec![
b"".to_vec(),
b"Exif\0\0".to_vec(),
b"Exif\0\0XX".to_vec(),
good[..good.len() - 13].to_vec(), app1(0, false), app1(9, true),
b"http://ns.adobe.com/xap/1.0/\0<x/>".to_vec(), {
let mut v = app1(6, false);
v[12] = 0xFF; v
},
b"Exif\0\0II\x2a\x00\xff\xff\xff\xff".to_vec(),
];
for (i, c) in cases.iter().enumerate() {
assert_eq!(Orientation::from_exif_app1(c), None, "case {i}");
}
}
#[test]
fn quirky_typed_orientation_entries_are_rejected() {
let mut long_typed = app1(6, false);
long_typed[18] = 4; assert_eq!(Orientation::from_exif_app1(&long_typed), None);
let mut multi_count = app1(6, false);
multi_count[20] = 2; assert_eq!(Orientation::from_exif_app1(&multi_count), None);
}
fn seg(marker: u8, body: &[u8]) -> Vec<u8> {
let mut v = vec![0xFF, marker];
v.extend(((body.len() + 2) as u16).to_be_bytes());
v.extend(body);
v
}
fn assert_rechains(prefix: &[u8], rest: &[u8], original: &[u8]) {
let mut rejoined = prefix.to_vec();
rejoined.extend_from_slice(rest);
assert_eq!(rejoined, original);
}
#[test]
fn scanner_skips_xmp_and_first_exif_wins() {
let mut jpeg = vec![0xFF, 0xD8];
jpeg.extend(seg(0xE0, b"JFIF\0"));
jpeg.extend(seg(0xE1, b"http://ns.adobe.com/xap/1.0/\0<x/>"));
jpeg.extend(seg(0xE1, &app1(6, false)));
jpeg.extend(seg(0xFE, b"comment"));
jpeg.extend(b"\xFF\xDBrest of the stream");
let mut r = &jpeg[..];
let mut prefix = Vec::new();
assert_eq!(
scan_jpeg_meta(&mut r, &mut prefix, false).orientation,
Orientation(6),
"XMP APP1 before the Exif must not mask it"
);
assert_rechains(&prefix, r, &jpeg);
let no_tag = {
let mut v = b"Exif\0\0II".to_vec();
v.extend(42u16.to_le_bytes());
v.extend(8u32.to_le_bytes());
v.extend(0u16.to_le_bytes()); v.extend(0u32.to_le_bytes()); v
};
let mut jpeg = vec![0xFF, 0xD8];
jpeg.extend(seg(0xE1, &no_tag));
jpeg.extend(seg(0xE1, &app1(3, false)));
jpeg.extend(b"\xFF\xDAtail");
let mut r = &jpeg[..];
assert_eq!(
scan_jpeg_meta(&mut r, &mut prefix, false).orientation,
Orientation::UPRIGHT
);
assert_rechains(&prefix, r, &jpeg);
}
#[test]
fn scanner_gives_up_losslessly() {
let mut jpeg = vec![0xFF, 0xD8];
while jpeg.len() <= SCAN_CAP {
jpeg.extend(seg(0xFE, &vec![0xAB; 60_000]));
}
jpeg.extend(seg(0xE1, &app1(6, false)));
jpeg.extend(b"tail");
let mut r = &jpeg[..];
let mut prefix = Vec::new();
assert_eq!(
scan_jpeg_meta(&mut r, &mut prefix, false).orientation,
Orientation::UPRIGHT
);
assert!(prefix.len() <= SCAN_CAP);
assert_rechains(&prefix, r, &jpeg);
let png = b"\x89PNG\r\n\x1a\n....".to_vec();
let mut r = &png[..];
assert_eq!(
scan_jpeg_meta(&mut r, &mut prefix, false).orientation,
Orientation::UPRIGHT
);
assert_rechains(&prefix, r, &png);
let jpeg = b"\xFF\xD8\xFF\xE1\x00\x01junk".to_vec();
let mut r = &jpeg[..];
assert_eq!(
scan_jpeg_meta(&mut r, &mut prefix, false).orientation,
Orientation::UPRIGHT
);
assert_rechains(&prefix, r, &jpeg);
let jpeg = b"\xFF\xD8\xFF\xE1\x00\x30Exif".to_vec();
let mut r = &jpeg[..];
assert_eq!(
scan_jpeg_meta(&mut r, &mut prefix, false).orientation,
Orientation::UPRIGHT
);
assert_rechains(&prefix, r, &jpeg);
}
fn app2_icc(seq: u8, count: u8, data: &[u8]) -> Vec<u8> {
let mut v = b"ICC_PROFILE\0".to_vec();
v.push(seq);
v.push(count);
v.extend(data);
v
}
fn jpeg_stream(segments: &[(u8, Vec<u8>)]) -> Vec<u8> {
let mut jpeg = vec![0xFF, 0xD8];
for (marker, body) in segments {
jpeg.extend(seg(*marker, body));
}
jpeg.extend(seg(0xDB, &[0u8; 4])); jpeg.extend(b"\xFF\xDAentropy...");
jpeg
}
fn scan_icc(jpeg: &[u8]) -> (Orientation, Option<Vec<u8>>) {
let mut r = jpeg;
let mut prefix = Vec::new();
let m = scan_jpeg_meta(&mut r, &mut prefix, true);
assert_rechains(&prefix, r, jpeg);
(m.orientation, m.icc)
}
#[test]
fn icc_chain_reassembles_in_sequence_order() {
let profile: Vec<u8> = (0..600u32).map(|i| (i % 251) as u8).collect();
let jpeg = jpeg_stream(&[(0xE2, app2_icc(1, 1, &profile))]);
assert_eq!(scan_icc(&jpeg).1.as_deref(), Some(&profile[..]));
let jpeg = jpeg_stream(&[
(0xE2, app2_icc(2, 2, &profile[300..])),
(0xE1, app1(6, false)),
(0xE2, app2_icc(1, 2, &profile[..300])),
]);
let (o, icc) = scan_icc(&jpeg);
assert_eq!(o, Orientation(6), "orientation and ICC coexist");
assert_eq!(icc.as_deref(), Some(&profile[..]));
let mut jpeg = vec![0xFF, 0xD8];
jpeg.extend(seg(0xDB, &[0u8; 4]));
jpeg.extend(seg(0xE2, &app2_icc(1, 1, &profile)));
jpeg.extend(b"\xFF\xDAentropy");
assert_eq!(scan_icc(&jpeg).1.as_deref(), Some(&profile[..]));
}
#[test]
fn half_megabyte_icc_chain_reassembles() {
let profile: Vec<u8> = (0..570_000u32).map(|i| (i % 251) as u8).collect();
let chunks: Vec<&[u8]> = profile.chunks(65_000).collect();
let count = chunks.len() as u8;
let segments: Vec<(u8, Vec<u8>)> = chunks
.iter()
.enumerate()
.map(|(i, c)| (0xE2, app2_icc(i as u8 + 1, count, c)))
.collect();
let jpeg = jpeg_stream(&segments);
assert_eq!(scan_icc(&jpeg).1.as_deref(), Some(&profile[..]));
}
#[test]
fn broken_icc_chains_yield_no_profile() {
let d = [7u8; 40];
let cases: Vec<Vec<(u8, Vec<u8>)>> = vec![
vec![(0xE2, app2_icc(1, 2, &d))], vec![(0xE2, app2_icc(1, 1, &d)), (0xE2, app2_icc(1, 1, &d))], vec![(0xE2, app2_icc(1, 1, &d)), (0xE2, app2_icc(2, 2, &d))], vec![(0xE2, app2_icc(0, 1, &d))], vec![(0xE2, app2_icc(2, 1, &d))], vec![(0xE2, app2_icc(1, 1, &[]))], vec![(0xE2, b"ICC_PROFILE\0".to_vec())], ];
for (i, segs) in cases.iter().enumerate() {
assert_eq!(scan_icc(&jpeg_stream(segs)).1, None, "case {i}");
}
let profile = [9u8; 64];
let jpeg = jpeg_stream(&[
(0xE2, b"FPXR\0not-icc".to_vec()),
(0xE2, app2_icc(1, 1, &profile)),
]);
assert_eq!(scan_icc(&jpeg).1.as_deref(), Some(&profile[..]));
let jpeg = jpeg_stream(&[
(0xE2, b"ICC_PROFILE\0".to_vec()),
(0xE2, app2_icc(1, 1, &profile)),
]);
assert_eq!(scan_icc(&jpeg).1.as_deref(), Some(&profile[..]));
}
#[test]
fn stray_standalone_markers_do_not_end_the_walk() {
let profile = [3u8; 48];
let mut jpeg = vec![0xFF, 0xD8, 0xFF, 0xD0, 0xFF, 0x01]; jpeg.extend(seg(0xE1, &app1(6, false)));
jpeg.extend(seg(0xE2, &app2_icc(1, 1, &profile)));
jpeg.extend(b"\xFF\xDAtail");
let mut r = &jpeg[..];
let mut prefix = Vec::new();
let m = scan_jpeg_meta(&mut r, &mut prefix, true);
assert_eq!(m.orientation, Orientation(6));
assert_eq!(m.icc.as_deref(), Some(&profile[..]));
assert_rechains(&prefix, r, &jpeg);
}
#[test]
fn icc_is_not_collected_when_unwanted() {
let jpeg = jpeg_stream(&[(0xE2, app2_icc(1, 1, &[5u8; 16]))]);
let mut r = &jpeg[..];
let mut prefix = Vec::new();
let m = scan_jpeg_meta(&mut r, &mut prefix, false);
assert_eq!(m.icc, None);
assert_rechains(&prefix, r, &jpeg);
}
#[test]
fn scanner_skips_marker_fill_bytes() {
let mut jpeg = vec![0xFF, 0xD8, 0xFF, 0xFF]; jpeg.extend(seg(0xE1, &app1(6, false)));
jpeg.extend(b"\xFF\xDAtail");
let mut r = &jpeg[..];
let mut prefix = Vec::new();
assert_eq!(
scan_jpeg_meta(&mut r, &mut prefix, false).orientation,
Orientation(6)
);
assert_rechains(&prefix, r, &jpeg);
}
#[test]
fn every_orientation_matches_its_anchor() {
let (w, h) = (3usize, 2usize);
let src: Vec<u8> = (0..(w * h) as u8).collect();
let expect: [(u8, Vec<u8>); 8] = [
(1, vec![0, 1, 2, 3, 4, 5]),
(2, vec![2, 1, 0, 5, 4, 3]), (3, vec![5, 4, 3, 2, 1, 0]), (4, vec![3, 4, 5, 0, 1, 2]), (5, vec![0, 3, 1, 4, 2, 5]), (6, vec![3, 0, 4, 1, 5, 2]), (7, vec![5, 2, 4, 1, 3, 0]), (8, vec![2, 5, 1, 4, 0, 3]), ];
for (o, want) in expect {
let mut dst = Vec::new();
let (dw, dh) = apply_orientation(&src, w, h, 1, Orientation(o), &mut dst);
let swap = o >= 5;
assert_eq!((dw, dh), if swap { (h, w) } else { (w, h) }, "o={o}");
assert_eq!(dst, want, "o={o}");
}
}
#[test]
fn rot_mirror_table_matches_composition() {
let (w, h) = (4usize, 3usize);
let src: Vec<u8> = (0..(w * h) as u8).collect();
let rot = [1u8, 8, 3, 6];
for angle in 0..4u8 {
for mirror in [None, Some(0u8), Some(1u8)] {
let (mut a, mut b) = (Vec::new(), Vec::new());
let (rw, rh) =
apply_orientation(&src, w, h, 1, Orientation(rot[angle as usize]), &mut a);
let (want, ww, wh) = match mirror {
None => (a.clone(), rw, rh),
Some(m) => {
let flip = if m == 1 { 2 } else { 4 };
let (fw, fh) = apply_orientation(&a, rw, rh, 1, Orientation(flip), &mut b);
(b.clone(), fw, fh)
}
};
let composed = Orientation::from_rot_mirror(angle, mirror);
let mut got = Vec::new();
let (gw, gh) = apply_orientation(&src, w, h, 1, composed, &mut got);
assert_eq!((gw, gh), (ww, wh), "angle={angle} mirror={mirror:?}");
assert_eq!(got, want, "angle={angle} mirror={mirror:?}");
}
}
}
#[test]
fn specialized_orientation_matches_reference() {
for &(w, h) in &[
(1usize, 1usize),
(2, 3),
(5, 4),
(31, 17),
(64, 64),
(65, 63),
(129, 64),
] {
for &channels in &[1usize, 3, 4] {
let src: Vec<u8> = (0..w * h * channels)
.map(|i| (i * 89 % 251) as u8)
.collect();
for o in 1..=8u8 {
let mut got = Vec::new();
let (dw, dh) =
apply_orientation(&src, w, h, channels, Orientation(o), &mut got);
let mut want = vec![0u8; dw * dh * channels];
apply_orientation_reference(&src, w, h, channels, Orientation(o), &mut want);
assert_eq!(got, want, "o={o} {w}x{h} c={channels}");
}
}
}
}
#[test]
#[ignore]
fn bench_orient() {
for (w, h) in [(512usize, 340usize), (340, 512), (512, 512), (120, 90)] {
let src: Vec<u8> = (0..w * h * 3).map(|i| (i % 251) as u8).collect();
let mut dst = Vec::new();
for o in [2u8, 3, 6, 8] {
for _ in 0..3 {
apply_orientation(&src, w, h, 3, Orientation(o), &mut dst);
}
let n = 200;
let t = std::time::Instant::now();
for _ in 0..n {
apply_orientation(&src, w, h, 3, Orientation(o), &mut dst);
}
let us = t.elapsed().as_secs_f64() * 1e6 / n as f64;
println!("orient o={o} {w}x{h}: {us:.1}µs");
}
}
}
#[test]
fn transforms_compose_and_preserve_pixels() {
let (w, h) = (5usize, 4usize);
let src: Vec<u8> = (0..w * h * 3).map(|i| (i * 37 % 251) as u8).collect();
let (mut a, mut b, mut c) = (Vec::new(), Vec::new(), Vec::new());
apply_orientation(&src, w, h, 3, Orientation(3), &mut a);
apply_orientation(&src, w, h, 3, Orientation(2), &mut b);
apply_orientation(&b, w, h, 3, Orientation(4), &mut c);
assert_eq!(a, c, "rot180 == flipH∘flipV");
apply_orientation(&src, w, h, 3, Orientation(6), &mut a);
apply_orientation(&a, h, w, 3, Orientation(8), &mut b);
assert_eq!(b, src, "rot90 then rot270 is identity");
apply_orientation(&src, w, h, 3, Orientation(5), &mut a);
apply_orientation(&a, h, w, 3, Orientation(5), &mut b);
assert_eq!(b, src, "transpose is an involution");
}
}