Skip to content

Commit

Permalink
fix: preserve aspect ratio in thumbnail
Browse files Browse the repository at this point in the history
  • Loading branch information
fabien-brulport committed Jul 18, 2024
1 parent e46776c commit ea0faa8
Show file tree
Hide file tree
Showing 3 changed files with 100 additions and 13 deletions.
77 changes: 76 additions & 1 deletion src/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,42 @@ use {
crate::{errors::OpenSlideError, Result, Size},
fast_image_resize as fr,
image::{RgbImage, RgbaImage},
std::{iter::zip, num::NonZeroU32},
std::{cmp, iter::zip, num::NonZeroU32},
};

#[cfg(feature = "image")]
pub fn preserve_aspect_ratio(size: &Size, dimension: &Size) -> Size {
// Code adapted from https://pillow.readthedocs.io/en/latest/_modules/PIL/Image.html#Image.thumbnail
fn round_aspect<F: FnMut(f32) -> f32>(number: f32, mut key: F) -> u32 {
cmp::max(
cmp::min_by_key(number.floor() as u32, number.ceil() as u32, |n| {
key(*n as f32).round() as u32
}),
1,
)
}
let w = size.w as f32;
let h = size.h as f32;
let aspect: f32 = dimension.w as f32 / dimension.h as f32;
if { w / h } >= aspect {
Size {
w: round_aspect(h * aspect, |n| (aspect - n / h).abs()),
h: h as u32,
}
} else {
Size {
w: w as u32,
h: round_aspect(w / aspect, |n| {
if n == 0. {
0.
} else {
(aspect - w / n).abs()
}
}),
}
}
}

#[cfg(feature = "image")]
pub(crate) fn resize_rgb_image(image: RgbImage, new_size: &Size) -> Result<RgbImage> {
let src_image = fr::Image::from_vec_u8(
Expand Down Expand Up @@ -81,3 +114,45 @@ pub fn _bgra_to_rgb(image: &RgbaImage) -> RgbImage {
}
rgb_image
}

#[cfg(test)]
#[cfg(feature = "image")]
mod tests {
use super::*;
#[test]
fn test_preserve_aspect_ratio() {
assert_eq!(
preserve_aspect_ratio(&Size { w: 100, h: 100 }, &Size { w: 50, h: 50 }),
Size { w: 100, h: 100 }
);
assert_eq!(
preserve_aspect_ratio(&Size { w: 100, h: 100 }, &Size { w: 25, h: 50 }),
Size { w: 50, h: 100 }
);
assert_eq!(
// Edge case
preserve_aspect_ratio(&Size { w: 1, h: 1 }, &Size { w: 25, h: 50 }),
Size { w: 1, h: 1 }
);
assert_eq!(
// Edge case
preserve_aspect_ratio(&Size { w: 100, h: 200 }, &Size { w: 1, h: 1 }),
Size { w: 100, h: 100 }
);
assert_eq!(
// Edge case
preserve_aspect_ratio(&Size { w: 0, h: 5 }, &Size { w: 1, h: 10}),
Size { w: 0, h: 1 }
);
assert_eq!(
// Not round ratio
preserve_aspect_ratio(&Size { w: 33, h: 100 }, &Size { w: 12, h: 13 }),
Size { w: 33, h: 35 }
);
assert_eq!(
// Not round ratio
preserve_aspect_ratio(&Size { w: 33, h: 15 }, &Size { w: 12, h: 13 }),
Size { w: 13, h: 15 }
);
}
}
12 changes: 8 additions & 4 deletions src/wrapper.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@ use std::path::Path;
#[cfg(feature = "image")]
use {
crate::{
utils::{_bgra_to_rgb, _bgra_to_rgba_inplace, resize_rgb_image, resize_rgba_image},
utils::{
_bgra_to_rgb, _bgra_to_rgba_inplace, preserve_aspect_ratio, resize_rgb_image,
resize_rgba_image,
},
Address,
},
image::{RgbImage, RgbaImage},
Expand Down Expand Up @@ -281,9 +284,9 @@ impl OpenSlide {
level,
address: Address { x: 0, y: 0 },
};

let image = self.read_image_rgba(&region)?;
let image = resize_rgba_image(image, size)?;
let size = preserve_aspect_ratio(&size, &dimension_level0);
let image = resize_rgba_image(image, &size)?;

Ok(image)
}
Expand All @@ -310,7 +313,8 @@ impl OpenSlide {
};

let image = self.read_image_rgb(&region)?;
let image = resize_rgb_image(image, size)?;
let size = preserve_aspect_ratio(&size, &dimension_level0);
let image = resize_rgb_image(image, &size)?;

Ok(image)
}
Expand Down
24 changes: 16 additions & 8 deletions tests/openslide.rs
Original file line number Diff line number Diff line change
Expand Up @@ -242,27 +242,35 @@ fn test_slide_read_image_rgba(#[case] filename: &Path) {
}

#[rstest]
#[case(boxes_tiff())]
#[case((boxes_tiff(), &Size { w: 96, h: 80 }))]
#[cfg(feature = "image")]
fn test_thumbnail_rgba(#[case] filename: &Path) {
fn test_thumbnail_rgba(#[case] filename_and_size: (&Path, &Size)) {
let (filename, expected_size) = filename_and_size;
let slide = OpenSlide::new(filename).unwrap();

let size = Size { w: 100, h: 80 };
let thumbnail = slide.thumbnail_rgba(&size).unwrap();
assert_eq!(thumbnail.dimensions(), (size.w, size.h));
assert_eq!(thumbnail.len(), (size.h * size.w * 4) as usize);
assert_eq!(thumbnail.dimensions(), (expected_size.w, expected_size.h));
assert_eq!(
thumbnail.len(),
(expected_size.h * expected_size.w * 4) as usize
);
}

#[rstest]
#[case(boxes_tiff())]
#[case((boxes_tiff(), &Size { w: 96, h: 80 }))]
#[cfg(feature = "image")]
fn test_thumbnail_rgb(#[case] filename: &Path) {
fn test_thumbnail_rgb(#[case] filename_and_size: (&Path, &Size)) {
let (filename, expected_size) = filename_and_size;
let slide = OpenSlide::new(filename).unwrap();

let size = Size { w: 100, h: 80 };
let thumbnail = slide.thumbnail_rgb(&size).unwrap();
assert_eq!(thumbnail.dimensions(), (size.w, size.h));
assert_eq!(thumbnail.len(), (size.h * size.w * 3) as usize);
assert_eq!(thumbnail.dimensions(), (expected_size.w, expected_size.h));
assert_eq!(
thumbnail.len(),
(expected_size.h * expected_size.w * 3) as usize
);
}

#[rstest]
Expand Down

0 comments on commit ea0faa8

Please sign in to comment.