html5_picture/
core.rs

1use {
2    crate::{
3        html5::Picture, path, utils, webp::processor::BatchParameter,
4        webp::processor::Parameter as ProcessorParameter, webp::WebpParameter,
5    },
6    clap::{crate_authors, crate_version, Parser},
7    fs_extra::dir::{
8        copy_with_progress, move_dir_with_progress, CopyOptions, TransitProcess,
9    },
10    indicatif::MultiProgress,
11    log::error,
12    queue::Queue,
13    std::{path::PathBuf, sync::Arc},
14};
15
16#[cfg(debug_assertions)]
17use log::debug;
18
19type Step = fn(&mut State);
20
21/// Converts the images (currently png only) of the input folder to webp format.
22/// It also has the ability to create multiple versions of the input images
23/// having different sizes. See -s for further details.
24/// Additionally it automatically generates HTML5 <picture> tag files for you
25/// to be able to integrate them in a webpage easily.
26///
27/// Depends on cwebp, so make sure webp is installed on your pc!
28///
29/// Example:
30/// html5-picture ./assets 3;
31/// Input image dimensions: 6000x962;
32/// Scaled images count: 3;
33/// Resulting converted images:
34///     original_filename        6000x962;
35///     original_filename-w4500  4500x751;
36///     original_filename-w3000  3000x501;
37///     original_filename-w1500  1500x250;
38#[derive(Parser, Debug, Clone)]
39#[clap(
40    version = crate_version!(),
41    author = crate_authors!(", "),
42)]
43pub struct Config {
44    /// The directory containing all images that should be processed.
45    pub input_dir: PathBuf,
46    /// The source image width is divided by this option (value + 1). Afterwards
47    /// the source image is scaled (keeping the aspect ratio) to these widths
48    /// before convertion.
49    /// Useful if you want to have multiple sizes of the image on the webpage
50    /// for different breakpoints.
51    pub scaled_images_count: u8,
52    /// Installs the converted and sized pictures into the given folder.
53    #[clap(short)]
54    pub install_images_into: Option<PathBuf>,
55    /// The destination folder of HTML5 picture tag files.
56    #[clap(short)]
57    pub picture_tags_output_folder: Option<PathBuf>,
58    /// Can be used in combination with -p, sets the mountpoint for links in
59    /// the HTML tags.
60    #[clap(short)]
61    pub mountpoint: Option<PathBuf>,
62    /// If true, existing files are overwritten if install-images-into is set.
63    #[clap(short, long)]
64    pub force_overwrite: bool,
65    /// Defines the quality of cwebp conversion.
66    #[clap(short)]
67    pub quality_webp: Option<u8>,
68    /// If set, the processing is done single threaded.
69    #[clap(short)]
70    pub single_threaded: bool,
71}
72
73/// Contains the application state and config.
74pub struct State {
75    pub config: Config,
76    pub file_names_to_convert: Vec<PathBuf>,
77    pub current_step: usize,
78    pub max_progress_steps: usize,
79}
80
81impl State {
82    /// Creates a new instance of the application state.
83    pub fn new(config: Config, max_progress_steps: usize) -> Self {
84        Self {
85            config,
86            file_names_to_convert: vec![],
87            current_step: 0,
88            max_progress_steps,
89        }
90    }
91
92    /// Small wrapper around the original dequeue function that automatically
93    /// calculates the current application step.
94    pub fn dequeue(&mut self, queue: &mut Queue<Step>) -> Option<Step> {
95        self.current_step = self.max_progress_steps + 1 - queue.len();
96        queue.dequeue()
97    }
98
99    /// Returns the prefix that is used in the ProgressBars.
100    pub fn get_prefix(&self) -> String {
101        format!("{}/{}", self.current_step, self.max_progress_steps)
102    }
103}
104
105/// Collects all png files in the given input folder.
106pub fn collect_file_names(state: &mut State) {
107    let pb = utils::create_spinner();
108    pb.set_prefix(state.get_prefix());
109    pb.set_message("Collecting files to convert...");
110    state.file_names_to_convert = crate::collect_png_file_names(
111        &state.config.input_dir,
112        Some(pb.clone()),
113    );
114    pb.finish_with_message(format!(
115        "Collected {} files!",
116        &state.file_names_to_convert.len(),
117    ));
118}
119
120/// Recreates the folder structure of the input directory in the output directory.
121pub fn create_all_output_directories(state: &mut State) {
122    let pb = utils::create_spinner();
123    pb.set_prefix(state.get_prefix());
124    let message = if state.config.install_images_into.is_some() {
125        "Creating all temporary output directories..."
126    } else {
127        "Creating all output directories..."
128    };
129    pb.set_message(message);
130    crate::fs::create_output_directories(
131        &state.config.input_dir,
132        &state.file_names_to_convert,
133        Some(pb.clone()),
134    );
135    let message = if state.config.install_images_into.is_some() {
136        "Created all temporary output directories!"
137    } else {
138        "Created all output directories!"
139    };
140    pb.finish_with_message(message);
141}
142
143/// Copies the input folder to the working directory.
144pub fn copy_originals_to_output(state: &mut State) {
145    let pb = utils::create_progressbar(0);
146    let pb_clone = pb.clone();
147    let force_overwrite = state.config.force_overwrite;
148    let progress_handler = move |process_info: TransitProcess| {
149        pb_clone.set_length(process_info.total_bytes);
150        pb_clone.set_position(process_info.copied_bytes);
151        if force_overwrite {
152            return fs_extra::dir::TransitProcessResult::Overwrite;
153        }
154        fs_extra::dir::TransitProcessResult::Skip
155    };
156    pb.set_prefix(state.get_prefix());
157    pb.set_message("Copying original files...");
158    let mut copy_options = CopyOptions::new();
159    copy_options.content_only = true;
160    copy_options.skip_exist = true;
161    if let Err(msg) = copy_with_progress(
162        &state.config.input_dir,
163        path::get_output_working_dir(&state.config.input_dir).unwrap(),
164        &copy_options,
165        progress_handler,
166    ) {
167        error!("{}", msg.to_string());
168    }
169    pb.finish_with_message("Successfully copied original images!");
170}
171
172/// Resizes and converts all input images.
173pub fn process_images(state: &mut State) {
174    let webp_params = WebpParameter::new(state.config.quality_webp);
175    let params = ProcessorParameter {
176        webp_parameter: webp_params,
177        input: state.config.input_dir.clone(),
178        output_dir: PathBuf::new(),
179        scaled_images_count: state.config.scaled_images_count,
180        single_threaded: state.config.single_threaded,
181    };
182    let batch_params = BatchParameter {
183        single_params: params,
184    };
185    let mp = Arc::new(MultiProgress::new());
186    let batch_processor = crate::webp::processor::BatchProcessor::new(
187        batch_params,
188        Some(Arc::clone(&mp)),
189    );
190    let pb = utils::create_spinner();
191    pb.set_prefix(state.get_prefix());
192    pb.set_message("Converting files...");
193    batch_processor.run(&state.file_names_to_convert);
194    pb.finish_with_message("Finished :-)");
195}
196
197/// Installs all images that have been converted to the given install folder.
198pub fn install_images_into(state: &mut State) {
199    let pb = utils::create_progressbar(0);
200    match &state.config.install_images_into {
201        None => return,
202        Some(p) => {
203            if !p.is_dir() {
204                if let Err(msg) = std::fs::create_dir_all(p) {
205                    pb.abandon_with_message(format!(
206                        "Could not create folder: {}",
207                        msg.to_string()
208                    ));
209                }
210            }
211        }
212    }
213    pb.set_prefix(state.get_prefix());
214    let install_path =
215        state.config.install_images_into.as_ref().unwrap().to_str();
216    let install_string = match install_path {
217        Some(s) => s,
218        None => {
219            pb.abandon_with_message("Invalid install_images_into parameter!");
220            return;
221        }
222    };
223    let force_overwrite = state.config.force_overwrite;
224    let pb_clone = pb.clone();
225    let progress_handler = move |process_info: TransitProcess| {
226        pb_clone.set_length(process_info.total_bytes);
227        pb_clone.set_position(process_info.copied_bytes);
228        if force_overwrite {
229            return fs_extra::dir::TransitProcessResult::Overwrite;
230        }
231        fs_extra::dir::TransitProcessResult::Skip
232    };
233    pb.set_message(format!("Installing files to {}...", &install_string));
234    let mut copy_options = CopyOptions::new();
235    copy_options.content_only = true;
236    copy_options.skip_exist = true;
237    if let Err(msg) = move_dir_with_progress(
238        path::get_output_working_dir(&state.config.input_dir).unwrap(),
239        state.config.install_images_into.as_ref().unwrap(),
240        &copy_options,
241        progress_handler,
242    ) {
243        error!("{}", msg.to_string());
244    }
245    pb.finish_with_message(format!(
246        "Successfully installed images to {}!",
247        state.config.install_images_into.as_ref().unwrap().display()
248    ));
249}
250
251/// Saves the html `<picture>` tags to the folder given by the options.
252pub fn save_html_picture_tags(state: &mut State) {
253    let pb =
254        utils::create_progressbar(state.file_names_to_convert.len() as u64);
255    pb.set_prefix(state.get_prefix());
256    pb.set_message("Writing HTML picture tag files...");
257
258    if let None = &state.config.picture_tags_output_folder {
259        pb.abandon_with_message(
260            "Parameter picture_tags_output_folder not set!",
261        );
262        return;
263    }
264
265    for file_name in &state.file_names_to_convert {
266        use std::io::prelude::*;
267        let mut output_name = file_name.clone();
268        output_name.set_extension("html");
269        let output_tag_file_name =
270            match crate::path::create_output_file_name_with_output_dir(
271                &state.config.picture_tags_output_folder.as_ref().unwrap(),
272                &state.config.input_dir,
273                &output_name,
274            ) {
275                Ok(name) => name,
276                Err(msg) => {
277                    pb.abandon_with_message(format!("{}", msg.to_string()));
278                    return;
279                }
280            };
281
282        #[cfg(debug_assertions)]
283        debug!("{:#?}", output_tag_file_name);
284
285        if std::path::Path::new(&output_tag_file_name).exists()
286            && !state.config.force_overwrite
287        {
288            #[cfg(debug_assertions)]
289            debug!("Skipping file {:#?}", output_tag_file_name);
290            continue;
291        }
292
293        let parent_folder = match output_tag_file_name.parent() {
294            Some(p) => p,
295            None => {
296                pb.abandon_with_message(format!(
297                    "No parent folder available for {}",
298                    output_tag_file_name.display()
299                ));
300                return;
301            }
302        };
303        let is_folder = match std::fs::metadata(parent_folder) {
304            Ok(v) => v.is_dir(),
305            Err(_) => false,
306        };
307        if !is_folder {
308            if let Err(msg) = std::fs::create_dir_all(parent_folder) {
309                error!(
310                    "Parent folder could not be created: {}",
311                    msg.to_string()
312                );
313                return;
314            }
315        }
316
317        let mut pic =
318            Picture::from(&file_name, state.config.scaled_images_count)
319                .unwrap();
320
321        if let Some(mountpoint) = &state.config.mountpoint {
322            for source in &mut pic.sources {
323                source.srcset =
324                    match crate::path::create_output_file_name_with_output_dir(
325                        &mountpoint,
326                        &state.config.input_dir,
327                        &PathBuf::from(&source.srcset),
328                    ) {
329                        Ok(name) => String::from(name.to_str().unwrap()),
330                        Err(msg) => {
331                            pb.abandon_with_message(format!(
332                                "{}",
333                                msg.to_string()
334                            ));
335                            return;
336                        }
337                    };
338            }
339            pic.fallback_uri =
340                match crate::path::create_output_file_name_with_output_dir(
341                    &mountpoint,
342                    &state.config.input_dir,
343                    &PathBuf::from(&pic.fallback_uri),
344                ) {
345                    Ok(name) => String::from(name.to_str().unwrap()),
346                    Err(msg) => {
347                        pb.abandon_with_message(format!("{}", msg.to_string()));
348                        return;
349                    }
350                };
351        }
352
353        let mut html_file = match std::fs::File::create(output_tag_file_name) {
354            Ok(f) => f,
355            Err(msg) => {
356                error!("{}", msg.to_string());
357                return;
358            }
359        };
360        if let Err(msg) =
361            html_file.write_all(pic.to_html_string(None, "").as_bytes())
362        {
363            error!("{}", msg.to_string());
364        };
365        pb.inc(1);
366    }
367    pb.finish_with_message(format!(
368        "Successfully wrote HTML picture tag files to: {}",
369        &state
370            .config
371            .picture_tags_output_folder
372            .as_ref()
373            .unwrap()
374            .display()
375    ));
376}