Create thumbnails from JPG files

Let's write a CLI tool that renders small "thumbnail" version of images matching a pattern. And to make it more interesting than a boring old shell script, let's do in with a bit of concurrency and a nice user experience!

This guide assumes you've already read the other two guides and now want to dive into some of the more advanced features.

This is an adaptation from this example from the Rust Cookbook.

Create a Cargo project

Let's start a new project called "thumbify": cargo new --bin thumbify.

You'll find a Cargo.toml file that contains:

[package]
name = "thumbify"
version = "0.1.0"
authors = ["Your Name <your@email.address>"]
edition = "2018"

[dependencies]

As always, add quicli and structopt to your dependencies:

quicli = "0.4"
structopt = "0.2"

Since we need to resize images, we'll also need to import a library that can do that. Let's use the one called "image." It sounds like something that you can do picture-related things with.

image = "0.18"

Import quicli and image

Let's load invite our new friends:

use quicli::prelude::*;
use structopt::StructOpt;

What other stuff do we need? Maybe something from the image crate? Don't worry: You don't need to know that upfront, and it's fine to add more later on.

Write a CLI struct

Alright! Here we go:

/// Make some thumbnails
#[derive(Debug, StructOpt)]
struct Cli {
    #[structopt(flatten)]
    verbosity: Verbosity,

So far so typical for a quicli app. Just the same as the example from the Getting Started guide.

Now, let's do a bit of thinking, though: What do our users need to specify so we can thumbify their images? Maybe a path to their directory that contains their images? Or maybe a pattern that matches only some of their files?

That 'pattern' idea does sounds more powerful, let's go with that. But it also sounds more complicated, so we'll see how difficult it will be to implement. In any case, let's also provide a default value.

    /// Which files?
    #[structopt(default_value = "*.jpg")]
    pattern: String,

Next up: How large should these thumbnails be? A non-negative integer seems like a good choice.

    /// How long should the longest edge of the thumbnail be?
    #[structopt(long = "max-size", short = "s", default_value = "300")]
    size: u32,

Anything else? Ah, yes, actually: Let's also add an option to specify where to save those thumbnails!

    /// Where do you want to save the thumbnails?
    #[structopt(long = "output", short = "o", default_value = "thumbnails")]
    thumb_dir: String,

And a flag to specify wether we want to clean it before creating our thumbnails!

    /// Should we clean the output directory?
    #[structopt(long="clean-dir")]
    clean_dir: bool,

There we go. Oh, wait, let's not forget to close that struct definition:

}

Yeah, now we're done.

Implement all the features

Onto implementing features!

fn main() -> CliResult {
    let args = Cli::from_args();

Quick interlude before we really get started: Do you remember how we set up logging before, by calling a method with the name of our crate? We'll do a more fancy approach here and use the env! macro to get the name from cargo itself!

    args.verbosity.setup_env_logger(&env!("CARGO_PKG_NAME"))?;

Globs

First, the good news: We don't need to care about the file path/name pattern matching stuff. quicli contains a glob function, that, given something like *.jpg, images/*.jpg, or even foo/**/bar*.gif, gives you a list of all the file paths that match the pattern.

    let files = glob(&args.pattern)?;

Before creating any of the thumbnails, let's clean-up the output directory, if requested by the caller. quicli provides remove_dir_all to clean any directory tree you'd like!

    let thumb_dir = std::path::Path::new(&args.thumb_dir);
    if args.clean_dir && thumb_dir.exists() {
        remove_dir_all(&thumb_dir)?;
    }

Now we're ready to (re)create the output directory. (another function quicli gives you):

    create_dir(&thumb_dir)?;

Great, that was the first step. If you're proud of that, this is your chance to yell it from the mountain tops!

    info!("Saving {} thumbnails into {:?}...", files.len(), args.thumb_dir);

(Assumes people in the village are using -vv to get 'info' level logs.)

Image resizing

Okay, on to the actual image file processing bit. This is where we use the image crate to scale the files.

Let's write a function that takes our image path, the settings we got from the CLI arguments, and returns a Result that is either Ok but contains no data (it saves the new image file directly and doesn't return its data to us), or is Err and carries some information about what went wrong. quicli contains a type alias for the usual Result, that automatically sets the Err variant to failure's Error type. This will save you some typing in the common case.

use std::path::Path;

fn make_thumbnail(
    original: &Path,
    thumb_dir: &str,
    longest_edge: u32,
) -> Result<(), Error> {

What a pretty function signature! You try to make the signatures of these helper functions as readable as possible, as they can often serve as documentation. You are of course still free to add a documentation comment with more information about what you intend them to be used for!

And while image's API documentation is a bit lacking right now, the usage of the image crate is quite simple. We open the image file, and call resize on it:

    let img = image::open(&original)?;
    let thumbnail = img.resize(longest_edge, longest_edge, image::FilterType::Nearest);

Now, let's create the JPG file in our thumbnails directory:

    use std::path::PathBuf;
    use std::fs::File;

    let thumb_name = original
        .file_name()
        .ok_or_else(|| format_err!("couldn't read file name of {:?}", original))?;
    let thumb_path = PathBuf::from(thumb_dir)
        .join(thumb_name)
        .with_extension("jpg");

    let mut output_file = File::create(thumb_path)?;

Right now, this creates a thumbnail with the same name as the original file. This is a good point to add some features to make this more clever/customizable!

And finally, save our thumbnail

    thumbnail.save(&mut output_file, image::JPEG)?;

Great, our job here is done! If all went well, we just saved thumbnail file! (Otherwise, the ? will exit the function and return the error.) We don't even need to return any data here, so let's just say everything is fine:

    Ok(())
}

Concurrency

It's safe to assume that we are doing some iterating over this files list. Something like this probably:

files
    .iter()
    .map(|f| make_thumbnail(f, &args.thumb_dir, args.size))

But wait: Do you remember how we said we wanted to render these thumbnails in parallel? Good for us that one of Rust's slogans is "Fearless Concurrency", then!

All we need to do is change the above .iter() to .par_iter() and thanks to the power of Rayon (which quilci includes) we are good to go!

    let thumbnails = files
        .par_iter()
        .map(|path| {
            make_thumbnail(path, &args.thumb_dir, args.size)

Ah, wait, before we get ahead of ourselves, let's talk a bit about what we want to do here. What if make_thumbnail fails? Do we just want to ignore the errors? Quit the whole program? A good middle-ground might be logging the errors. This way, the user still sees that the program wasn't really successful, but also gets as many thumbnails as possible.

Luckily, this is also the place where we have the most information: We know all the arguments that we gave make_thumbnail as well as its error message. Let's use .map_err to capture the error and log something:

            .map_err(|e| error!("failed to resize {} ({})", path.display(), e))
        });

Good. Now, the user will see some errors as they occur. Maybe even a few at the same time!

Let's also do a "final" message when we are finished, showing how many files we were able to thumbify. For that, let's count the files we could resize as 1 and errors as 0, and sum them up:

    let thumbnail_count: i32 = thumbnails
        .map(|x| if x.is_ok() { 1 } else { 0 })
        .sum();

Sweet. Now, let's print that number and we are done!

    println!(
        "{} of {} files successfully thumbyfied!",
        thumbnail_count,
        files.len()
    );

    Ok(())
}

Give it a spin!

  1. Save the following images into a directory called rust_memes 1 2 3
  2. cargo run -- "rust_memes/*"
  3. ???
  4. PROFIT!