1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154
use std::cmp::Ordering;
use crate::{OrderBy, TakeFrom, ImageFilesBuilder, FileProperties, image::DynamicImage, FileLocation};
use image::ImageFormat;
/// A set of image files, storing some file properties internally.
///
/// The files can be sorted and truncated according to the options supplied, and then converted
/// into a vector of [`DynamicImage`] structs which will contain the actual image data for all
/// files.
///
/// Construct using the [`ImageFilesBuilder`] struct.
///
/// See crate-level documentation for examples.
pub struct ImageFiles<P: FileProperties> {
file_list: Vec<P>
}
impl<P: FileProperties> ImageFiles<P> {
/// Create a new [ImageFilesBuilder] for selecting files
pub fn builder<L: FileLocation<P>>() -> ImageFilesBuilder<P, L> {
ImageFilesBuilder::default()
}
pub(crate) fn new(file_list: Vec<P>) -> Self {
Self { file_list }
}
/// Return an array of the accepted file extensions.
///
/// These match image formats that can be processed by this crate. Adding individual files
/// with other extensions will fail, and files with other extensions will be ignored if adding
/// whole directories.
pub fn allowed_extensions() -> [&'static str; 5] {
["jpg", "jpeg", "png", "gif", "bmp"]
}
/// Get the "main" extension used by a format.
///
/// This is subjective in nature. One thing that is known is that these are all contained in
/// the set of usable formats in [ImageFiles::allowed_extensions].
pub fn get_main_extension(format: ImageFormat) -> Option<&'static str> {
match format {
ImageFormat::Jpeg => Some("jpg"),
ImageFormat::Png => Some("png"),
ImageFormat::Gif => Some("gif"),
ImageFormat::Bmp => Some("bmp"),
_ => None
}
}
/// Get the number of files in the current working set
pub fn file_count(&self) -> usize {
self.file_list.len()
}
/// Get the total size, in bytes, of all files in the set
pub fn total_size(&self) -> u64 {
let mut total = 0;
for file in self.file_list.iter() {
total += file.file_size();
}
total
}
/// Sorts the files according to the options supplied, and truncates the set to the
/// number of files requested by the user
pub fn sort_and_truncate_by(
mut self,
number_of_files: usize,
order_by: OrderBy,
take_from: TakeFrom,
reverse: bool
) -> Result<Self, String> {
// Verify at least n images were found, where n is the number requested
if self.file_list.len() < number_of_files {
return Err(
format!("Requested {} files, found {}", number_of_files, self.file_list.len()));
}
// Sort the files by the order given, putting files at the start of the vector according to
// whether we should take from the default end (most recently updated or alphabetically
// first) or take from the other end (oldest update or alphabetically last)
match (order_by, take_from) {
(OrderBy::Latest, TakeFrom::Start) => {
self.file_list.sort_unstable_by(|a, b|
a.modify_time().cmp(&b.modify_time()).reverse());
},
(OrderBy::Latest, TakeFrom::End) => {
self.file_list.sort_unstable_by_key(|a| a.modify_time());
},
(OrderBy::Alphabetic, TakeFrom::Start) => {
self.file_list.sort_unstable_by(|a, b| {
let Some(a_path) = a.full_path() else { return Ordering::Equal };
let Some(b_path) = b.full_path() else { return Ordering::Equal };
a_path.cmp(b_path)
});
},
(OrderBy::Alphabetic, TakeFrom::End) => {
self.file_list.sort_unstable_by(|a, b| {
let Some(a_path) = a.full_path() else { return Ordering::Equal };
let Some(b_path) = b.full_path() else { return Ordering::Equal };
a_path.cmp(b_path).reverse()
});
}
}
self.file_list.truncate(number_of_files);
// 'Natural' order of selected files based on date is oldest to newest, which is the reverse
// of the order generated above, or alphabetically earliest to latest, which is the same
// as the order from above
let reverse_order = reverse ^ (order_by == OrderBy::Latest);
// Revert to chronological order, unless the reverse order was requested
if reverse_order {
self.file_list.reverse();
}
// Return updated self
Ok(self)
}
/// Load the image data from the files in the set, and return a vector of [`DynamicImage`].
/// The result can then be stitched together.
pub fn into_image_contents(self, print_info: bool) -> Result<Vec<DynamicImage>, String> {
let mut images = Vec::with_capacity(self.file_list.len());
for file in self.file_list {
let image = file.into_image_contents(print_info)?;
images.push(image);
}
Ok(images)
}
/// Suggest an output format to use for saving the stitch result after loading and stitching
/// the image files in this set.
///
/// If all files use the same format, this will be the suggestion. If the formats vary, or
/// if there are no files in the set, then [None] will be returned.
pub fn common_format_in_sources(&self) -> Option<ImageFormat> {
if self.file_list.is_empty() {
return None;
}
let mut all_formats = self.file_list.iter().map(|file_data| {
file_data.infer_format()
});
let first_format = all_formats.next().unwrap();
match all_formats.all(|fmt| fmt == first_format) {
true => first_format,
false => None
}
}
}