Skip to content

Commit

Permalink
Add manual definition of the image center
Browse files Browse the repository at this point in the history
  • Loading branch information
pohlm01 committed Jun 27, 2024
1 parent 366bd9b commit d518429
Show file tree
Hide file tree
Showing 3 changed files with 165 additions and 3 deletions.
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -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"]
Expand Down
118 changes: 117 additions & 1 deletion src/image.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -19,11 +19,14 @@ impl<T: ObjectStore> ImageThumbs<T> {
stem: &str,
format: ImageFormat,
force_override: bool,
center: (f32, f32),
) -> ThumbsResult<Vec<ImageDetails>> {
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()
Expand Down Expand Up @@ -79,3 +82,116 @@ impl<T: ObjectStore> ImageThumbs<T> {
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);
}
}
48 changes: 47 additions & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,42 @@ impl<T: ObjectStore> ImageThumbs<T> {
&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
}
Expand Down Expand Up @@ -160,11 +196,19 @@ impl<T: ObjectStore> ImageThumbs<T> {
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
}
Expand Down Expand Up @@ -328,6 +372,7 @@ mod tests {
"penguin",
ImageFormat::Jpeg,
false,
(0.5, 0.5),
)
.await
.unwrap();
Expand All @@ -350,6 +395,7 @@ mod tests {
"penguin",
ImageFormat::Png,
false,
(0.5, 0.5),
)
.await
.unwrap();
Expand Down

0 comments on commit d518429

Please sign in to comment.