From d5184294612148e50a098fde7cdb60b5d7832ebb Mon Sep 17 00:00:00 2001 From: Maximilian Pohl Date: Thu, 27 Jun 2024 11:58:29 +0200 Subject: [PATCH] Add manual definition of the image center --- Cargo.toml | 2 +- src/image.rs | 118 ++++++++++++++++++++++++++++++++++++++++++++++++++- src/lib.rs | 48 ++++++++++++++++++++- 3 files changed, 165 insertions(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 4a51dec..ce20d7e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "image_thumbs" -version = "0.3.0" +version = "0.4.0" edition = "2021" repository = "https://github.com/tweedegolf/image-thumbs" keywords = ["GCS", "image", "thumbnails"] diff --git a/src/image.rs b/src/image.rs index 93da1a4..f97285e 100644 --- a/src/image.rs +++ b/src/image.rs @@ -3,7 +3,7 @@ use std::io::Cursor; use image::codecs::jpeg::JpegEncoder; use image::codecs::png; use image::codecs::png::{CompressionType, PngEncoder}; -use image::imageops; +use image::{imageops, DynamicImage}; use image::{load_from_memory_with_format, ImageFormat}; use object_store::path::Path; use object_store::ObjectStore; @@ -19,11 +19,14 @@ impl ImageThumbs { stem: &str, format: ImageFormat, force_override: bool, + center: (f32, f32), ) -> ThumbsResult> { let image = load_from_memory_with_format(&bytes, format)?; let mut res = Vec::with_capacity(self.settings.len()); for params in self.settings.iter() { + let image = crop_aspect_ratio_with_center(&image, params.size, center); + let naming_pattern = params .naming_pattern .clone() @@ -79,3 +82,116 @@ impl ImageThumbs { Ok(res) } } + +fn crop_aspect_ratio_with_center( + image: &DynamicImage, + target_size: (u32, u32), + center: (f32, f32), +) -> DynamicImage { + let orig_aspect_ratio = image.width() as f32 / image.height() as f32; + let target_aspect_ratio = target_size.0 as f32 / target_size.1 as f32; + + let (crop_width, crop_height) = if orig_aspect_ratio > target_aspect_ratio { + ( + target_aspect_ratio * image.height() as f32, + image.height() as f32, + ) + } else if orig_aspect_ratio < target_aspect_ratio { + ( + image.width() as f32, + image.width() as f32 / target_aspect_ratio, + ) + } else { + (image.width() as f32, image.height() as f32) + }; + + let x = (image.width() as f32 * center.0 - crop_width * 0.5).round(); + let x = if x < 0. { + 0_u32 + } else if x > (image.width() as f32 - crop_width) { + image.width() - crop_width as u32 + } else { + x as u32 + }; + + let y = (image.height() as f32 * center.1 - crop_height * 0.5).round(); + let y = if y < 0. { + 0_u32 + } else if y > (image.height() as f32 - crop_height) { + image.height() - crop_height as u32 + } else { + y as u32 + }; + + image.crop_imm(x, y, crop_width.round() as u32, crop_height.round() as u32) +} + +#[cfg(test)] +mod test { + use image::{ColorType, DynamicImage}; + + use crate::image::crop_aspect_ratio_with_center; + + #[test] + fn crop_center_1() { + let image = DynamicImage::new(100, 100, ColorType::L8); + + let cropped = crop_aspect_ratio_with_center(&image, (10, 10), (0.5, 0.5)); + assert_eq!(cropped.width(), 100, "As the source and target aspect ratio is the same, the image should not crop be cropped"); + assert_eq!(cropped.height(), 100, "As the source and target aspect ratio is the same, the image should not crop be cropped"); + + let cropped = crop_aspect_ratio_with_center(&image, (200, 200), (0.5, 0.5)); + assert_eq!(cropped.width(), 100, "As the source and target aspect ratio is the same, the image should not crop be cropped"); + assert_eq!(cropped.height(), 100, "As the source and target aspect ratio is the same, the image should not crop be cropped"); + + let cropped = crop_aspect_ratio_with_center(&image, (10, 10), (0.9, 1.)); + assert_eq!(cropped.width(), 100, "As the source and target aspect ratio is the same, the image should not crop be cropped"); + assert_eq!(cropped.height(), 100, "As the source and target aspect ratio is the same, the image should not crop be cropped"); + + let cropped = crop_aspect_ratio_with_center(&image, (10, 10), (0., 0.5)); + assert_eq!(cropped.width(), 100, "As the source and target aspect ratio is the same, the image should not crop be cropped"); + assert_eq!(cropped.height(), 100, "As the source and target aspect ratio is the same, the image should not crop be cropped"); + } + + #[test] + fn crop_center_2() { + let image = DynamicImage::new(100, 150, ColorType::L8); + + let cropped = crop_aspect_ratio_with_center(&image, (10, 15), (0.5, 0.5)); + assert_eq!(cropped.width(), 100, "As the source and target aspect ratio is the same, the image should not crop be cropped"); + assert_eq!(cropped.height(), 150, "As the source and target aspect ratio is the same, the image should not crop be cropped"); + + let cropped = crop_aspect_ratio_with_center(&image, (15, 10), (0.5, 0.5)); + assert_eq!(cropped.width(), 100); + assert_eq!(cropped.height(), 67); + + let cropped = crop_aspect_ratio_with_center(&image, (15, 10), (0., 1.)); + assert_eq!(cropped.width(), 100); + assert_eq!(cropped.height(), 66); + + let cropped = crop_aspect_ratio_with_center(&image, (15, 10), (0.9, 0.1)); + assert_eq!(cropped.width(), 100); + assert_eq!(cropped.height(), 67); + } + + #[test] + fn crop_center_3() { + let image = DynamicImage::new(150, 100, ColorType::L8); + + let cropped = crop_aspect_ratio_with_center(&image, (15, 10), (0.5, 0.5)); + assert_eq!(cropped.width(), 150, "As the source and target aspect ratio is the same, the image should not crop be cropped"); + assert_eq!(cropped.height(), 100, "As the source and target aspect ratio is the same, the image should not crop be cropped"); + + let cropped = crop_aspect_ratio_with_center(&image, (10, 15), (0.5, 0.5)); + assert_eq!(cropped.width(), 67); + assert_eq!(cropped.height(), 100); + + let cropped = crop_aspect_ratio_with_center(&image, (10, 15), (0., 1.)); + assert_eq!(cropped.width(), 67); + assert_eq!(cropped.height(), 100); + + let cropped = crop_aspect_ratio_with_center(&image, (10, 15), (0.9, 0.1)); + assert_eq!(cropped.width(), 66); + assert_eq!(cropped.height(), 100); + } +} diff --git a/src/lib.rs b/src/lib.rs index e258257..15ff5f6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -132,6 +132,42 @@ impl ImageThumbs { &image.stem, image.format, force_override, + (0.5, 0.5), + ) + .await + } + + /// Gets one image from the object storage, creates thumbnails for it, and puts them in the + /// `dest_dir` directory. + /// This function allows providing a manual definition of the image center, i.e., the most + /// relevant part, to avoid cutting it of. + /// + /// # Arguments + /// * `file` - image to create thumbnails for. + /// + /// * `dest_dir` - directory to store all created thumbnails. + /// This directory will be checked for already existent thumbnails if `force_override` is false. + /// + /// * `force_override` - if `true` it will override already existent files with the same name. + /// If false, it will preserve already existent files. + /// + /// # `center` - (width, height) in percent (i.e., between 0 and 1) where to place the center of + /// the image, if the edges need to be cut off. + pub async fn create_thumbs_man_center( + &self, + file: &str, + dest_dir: &str, + force_override: bool, + center: (f32, f32), + ) -> ThumbsResult<()> { + let image = self.download_image(file).await?; + self.create_thumbs_from_bytes( + image.bytes, + dest_dir, + &image.stem, + image.format, + force_override, + center, ) .await } @@ -160,11 +196,19 @@ impl ImageThumbs { image_name: &str, format: ImageFormat, force_override: bool, + center: (f32, f32), ) -> ThumbsResult<()> { let dest_dir = Path::parse(dest_dir)?; let thumbs = self - .create_thumb_images_from_bytes(bytes, dest_dir, image_name, format, force_override) + .create_thumb_images_from_bytes( + bytes, + dest_dir, + image_name, + format, + force_override, + center, + ) .await?; self.upload_thumbs(thumbs).await } @@ -328,6 +372,7 @@ mod tests { "penguin", ImageFormat::Jpeg, false, + (0.5, 0.5), ) .await .unwrap(); @@ -350,6 +395,7 @@ mod tests { "penguin", ImageFormat::Png, false, + (0.5, 0.5), ) .await .unwrap();