Skip to content

Commit

Permalink
Merge pull request #1 from y-mx-b/improve-error-handling
Browse files Browse the repository at this point in the history
Improve error handling
  • Loading branch information
quietkiro authored Sep 7, 2024
2 parents c39d66e + edff0a2 commit c70dd1a
Show file tree
Hide file tree
Showing 2 changed files with 105 additions and 29 deletions.
10 changes: 8 additions & 2 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,23 @@ use quickstitch::gui;
use quickstitch::stitcher::splitter::split_image;
use quickstitch::stitcher::{image_loader, splitter};

fn main() {
fn main() -> anyhow::Result<()> {
// dbg!(image_loader::find_images("./testing/sample"));
// let mut image = image_loader::load_images("./testing/sample", None).unwrap();
// let splitpoints = splitter::find_splitpoints_debug(&mut image, 5000, 5, 240);
let now = Instant::now();
let image = image_loader::load_images("./testing/sample", Some(800)).unwrap();
let image = image_loader::load_images(
image_loader::find_images("./testing/sample")?.as_ref(),
Some(800),
)
.unwrap();
println!("Images loaded in {:.2?}", now.elapsed());
let now = Instant::now();
let splitpoints = splitter::find_splitpoints(&image, 5000, 5, 242);
println!("Found splitpoints in {:.2?}", now.elapsed());
let now = Instant::now();
split_image(&image, &splitpoints, PathBuf::from("./testing/output"), 100);
println!("Split image in {:.2?}", now.elapsed());

Ok(())
}
124 changes: 97 additions & 27 deletions src/stitcher/image_loader.rs
Original file line number Diff line number Diff line change
@@ -1,25 +1,72 @@
//! This module is for all methods involved in getting selected images loaded into memory.
use anyhow::anyhow;
use image::{
image_dimensions, imageops::FilterType::Lanczos3, GenericImage, ImageReader, RgbImage,
error::ImageError, image_dimensions, imageops::FilterType::Lanczos3, GenericImage, ImageReader,
RgbImage,
};
use rayon::iter::{IntoParallelIterator, IntoParallelRefIterator, ParallelIterator};
use std::{
fs::read_dir,
io,
path::{Path, PathBuf},
time::Instant,
};
use rayon::iter::{IntoParallelIterator, ParallelIterator};
use std::{fs::read_dir, path::PathBuf, time::Instant};
use thiserror::Error;

#[derive(Error, Debug)]
/// Errors related to loading images.
pub enum ImageLoaderError {
// IO Errors
#[error("Could not find the provided file or directory")]
NotFound,
#[error("Insufficient permissions to access the provided file or directory")]
PermissionDenied,

// Logical Errors
#[error("No images were found in the selected directory")]
NoImagesInDirectory,
#[error("Expected a directory")]
ExpectedDirectory,

// upstream errors
#[error("{0}")]
ImageError(ImageError),
#[error("{0}")]
IoError(io::Error),
}

impl From<ImageError> for ImageLoaderError {
fn from(value: ImageError) -> Self {
Self::ImageError(value)
}
}

impl From<io::Error> for ImageLoaderError {
fn from(value: io::Error) -> Self {
use io::ErrorKind as Kind;

match value.kind() {
Kind::NotFound => ImageLoaderError::NotFound,
Kind::PermissionDenied => ImageLoaderError::PermissionDenied,
// add more cases as required
_ => ImageLoaderError::IoError(value),
}
}
}

/// Finds all `.jpg`, `.jpeg`, `.png` and `.webp` images within a directory.
///
/// Throws an error if:
/// - The directory is invalid or does not contain any images.
/// - The directory does not contain any jpg, jpeg, png, or webp images.
pub fn find_images(directory_path: &str) -> anyhow::Result<Vec<PathBuf>> {
pub fn find_images(directory_path: impl AsRef<Path>) -> Result<Vec<PathBuf>, ImageLoaderError> {
// create pathbuf, check if path is a directory
let path = directory_path.as_ref();
if !path.is_dir() {
return Err(ImageLoaderError::ExpectedDirectory);
}

// get images
let mut images: Vec<_> = read_dir(directory_path)?
.into_iter()
.map(|file| file.unwrap().path())
Expand All @@ -31,19 +78,20 @@ pub fn find_images(directory_path: &str) -> anyhow::Result<Vec<PathBuf>> {
_ => false,
})
.collect();

// if no images were found
if images.is_empty() {
return Err(ImageLoaderError::NoImagesInDirectory.into());
return Err(ImageLoaderError::NoImagesInDirectory);
}
images.sort_by(|a, b| {
natord::compare(
a.file_name().unwrap().to_str().unwrap(),
b.file_name().unwrap().to_str().unwrap(),
)
});

// sort images by natural order
images.sort_by(|a, b| natord::compare(&a.display().to_string(), &b.display().to_string()));

// return images
Ok(images)
}

/// Loads any jpg, jpeg, png or webp images in the given directory into memory and resizing all images to a set width.
/// Loads the images at the provided paths into a single image strip.
///
/// If the `width` parameter is set to `None`, the width of the image with the smallest width will be used.
/// Otherwise, the given width will be used.
Expand All @@ -52,11 +100,19 @@ pub fn find_images(directory_path: &str) -> anyhow::Result<Vec<PathBuf>> {
/// - The directory is invalid or does not contain any images.
/// - The directory does not contain any jpg, jpeg, png, or webp images.
/// - An image cannot be opened.
pub fn load_images(directory_path: &str, width: Option<u32>) -> anyhow::Result<RgbImage> {
let dimensions = find_images(directory_path)?
.into_iter()
.map(|image| image_dimensions(image).map_err(|e| anyhow!(e)))
.collect::<anyhow::Result<Vec<(u32, u32)>>>()?;
pub fn load_images(
paths: &[impl AsRef<Path>],
width: Option<u32>,
) -> Result<RgbImage, ImageLoaderError> {
// get a vec of path refs from the generic parameter
let paths = paths.iter().map(|p| p.as_ref()).collect::<Vec<&Path>>();

let dimensions = paths
.iter()
.map(|&image| image_dimensions(image).map_err(|e| ImageLoaderError::from(e)))
.collect::<Result<Vec<(u32, u32)>, ImageLoaderError>>()?;

// the width to resize images to
let width = match width {
Some(v) => v,
None => {
Expand All @@ -68,26 +124,40 @@ pub fn load_images(directory_path: &str, width: Option<u32>) -> anyhow::Result<R
dimensions.iter().map(|pair| pair.0).min().unwrap()
}
};

// the height to resize images to
let height = dimensions.iter().map(|pair| pair.1).max().unwrap();
let images: Vec<RgbImage> = find_images(directory_path)?
.into_par_iter()
.map(|image_path| {

// load images
let images: Vec<RgbImage> = paths
.par_iter()
.map(|&image_path| {
let image = ImageReader::open(image_path)?
.decode()
.map_err(|e| anyhow!(e))?;
.map_err(|e| ImageLoaderError::from(e))?;

if image.width() == width {
return Ok(image.into());
// noop if widths match
Ok(image.into())
} else {
// resize image otherwise
// let height = width as f32 * image.height() as f32 / image.width() as f32;
Ok(image.resize(width, height, Lanczos3).into())
}
// let height = width as f32 * image.height() as f32 / image.width() as f32;
Ok(image.resize(width, height, Lanczos3).into())
})
.collect::<anyhow::Result<Vec<RgbImage>>>()?;
.collect::<Result<Vec<RgbImage>, ImageLoaderError>>()?;

// combine all images into one big strip
let mut combined_image = RgbImage::new(width, images.iter().map(|image| image.height()).sum());
let mut height_cursor = 0;

for i in images {
// This should never throw an error because the combined image height is set to the sum of all image heights.
combined_image.copy_from(&i, 0, height_cursor)?;
combined_image
.copy_from(&i, 0, height_cursor)
.expect("all according to keikaku");
height_cursor += i.height();
}

Ok(combined_image)
}

0 comments on commit c70dd1a

Please sign in to comment.