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
        }
    }
}