Resize image using Rust

image FilterType open height width resize save match

Create the crate

For this example we create a crate called resize-image and then add image as a dependency.

cargo new resize-image
cd resize-image
cargo add image

Or manually update Cargo.toml:

[dependencies]
image = "0.24"

How to use this crate?

We run the command like this:

cargo run ~/Pictures/locust_5.jpg resized.png 400 nearest
  • The 1st parameter is the path to the original file.
  • The 2nd parameter is the path to the new file. As you can see, the file extension does not have to be the same. The code will automatically recognize the file format based on the file extension.
  • The 3rd parameter is the new width of the image. The new hight will be calculated based on this and the aspect ratio of the original file.
  • The 4th and last parameter is the FilterType In the code I've created a mapping from a string to the actual type in that enum.

We need to provide this last parameter in order to tell the resize method how to do its magic. From the sample image I can see that the "nearest" method is 10-30 times faster than the others, but it also creates the worse image.

The code

The src/main.rs file then looks like this:

examples/resize-image/src/main.rs

use image::imageops::FilterType;

fn main() {
    let (infile, outfile, width, filter) = get_args();

    let img = image::open(infile).unwrap();
    println!("Original width={}, height={}", img.width(), img.height());

    let height = width * img.height() / img.width();

    println!("Resizing to: width={}, height={}", width, height);

    let scaled = img.resize(width, height, filter);
    scaled.save(outfile).unwrap();
}

fn get_args() -> (String, String, u32, FilterType) {
    let args = std::env::args().collect::<Vec<String>>();
    if args.len() != 5 {
        eprintln!("Usage: {} INFILE OUTFILE WIDTH FILTER", args[0]);
        std::process::exit(1);
    }
    (
        args[1].to_owned(),
        args[2].to_owned(),
        args[3].parse().unwrap(),
        get_filter_type(&args[4]),
    )
}

fn get_filter_type(name: &str) -> FilterType {
    match name {
        "nearest" => FilterType::Nearest,
        "triangle" => FilterType::Triangle,
        "cubic" => FilterType::CatmullRom,
        "gauss" => FilterType::Gaussian,
        "lanczos" => FilterType::Lanczos3,
        _ => FilterType::Nearest,
    }
}

We have two functions, the get_args function will look at the command line and return the 4 parameters. The first two will be simple strings. The 3rd one is converted to a u32 (unsigned integer with 32 bits). The 4th parameter is expected to be a string that is then converted to a FilterType the pattern matching implemented in the get_filter_type function.

The image::open function reads the content of the image file in the memory. It return ImageResult structure with a DynamicImage in it.

This has a width and a height method, both returning u32 values.

The aspect ratio of the original images is calculated by img.height() / img.width().

Then we use the resize method to do the actual work.

Finally save will save the new image in format appropriate to the file extension we gave to it.

Resize image with error checking

In the previous solution I did not have any error checking. In several places I used unwrap. One of the improvements suggested was to add some input validation and error checking. I did not want to make the original solution even more complex, so I created a separate one that contains error handling. This code also allows the user to provide only 3 parameters and use FilterType::Nearest as the default.

examples/resize-image-with-error-checking/src/main.rs

use image::imageops::FilterType;

fn main() {
    let (infile, outfile, width, filter) = get_args();

    let img = match image::open(&infile) {
        Ok(val) => val,
        Err(err) => {
            eprintln!("Could not read the image file '{}' {}", infile, err);
            std::process::exit(1);
        }
    };

    println!("Original width={}, height={}", img.width(), img.height());

    let height = width * img.height() / img.width();

    println!("Resizing to: width={}, height={}", width, height);

    let scaled = img.resize(width, height, filter);
    match scaled.save(&outfile) {
        Ok(_) => {}
        Err(err) => eprintln!("Could not save file '{}' {}", outfile, err),
    };
}

fn get_args() -> (String, String, u32, FilterType) {
    let args = std::env::args().collect::<Vec<String>>();
    if args.len() != 4 && args.len() != 5 {
        usage(&args[0]);
    }

    let width: u32 = match args[3].parse() {
        Ok(value) => value,
        Err(err) => {
            eprintln!(
                "Invalid parameter for width: '{}'. It must be an integer",
                err
            );
            usage(&args[0])
        }
    };

    let filter_name = if args.len() == 5 { &args[4] } else { "default" };

    (
        args[1].to_owned(),
        args[2].to_owned(),
        width,
        get_filter_type(filter_name),
    )
}

fn get_filter_type(name: &str) -> FilterType {
    match name {
        "nearest" => FilterType::Nearest,
        "triangle" => FilterType::Triangle,
        "cubic" => FilterType::CatmullRom,
        "gauss" => FilterType::Gaussian,
        "lanczos" => FilterType::Lanczos3,
        _ => FilterType::Nearest,
    }
}

fn usage(name: &str) -> ! {
    eprintln!("Usage: {} INFILE OUTFILE WIDTH FILTER", name);
    std::process::exit(1);
}

In most cases the change was simple replacing the unwrap call by the use of the match. One case, however was particularly interesting.

In the get_args function we have this code:

    let width: u32 = match args[3].parse() {
        Ok(value) => value,
        Err(err) => {
            eprintln!(
                "Invalid parameter for width: '{}'. It must be an integer",
                err
            );
            usage(&args[0])
        }
    };

Here we expect both arms of the match to return u32, but the Err arm calls a function that always exits and thus it should not return anything. At first I declared it this way, but Rust complained that the Err arm returns () and not the expected u32.

fn usage(name: &str) {
    eprintln!("Usage: {} INFILE OUTFILE WIDTH FILTER", name);
    std::process::exit(1);
}

The solution I found was adding an exclamation mark where we would have declared the return values:

fn usage(name: &str) -> ! {
    eprintln!("Usage: {} INFILE OUTFILE WIDTH FILTER", name);
    std::process::exit(1);
}

Apparently functions that never return are called Diverging functions.

Conclusion

It is quite easy to resize an image. Let's see what else can we do easily.

For example we can crop and image.

Related Pages

Image - a crate to generate and manipulate images in Rust
Diverging Functions - functions that never return
Crop image using Rust

Author

Gabor Szabo (szabgab)

Gabor Szabo, the author of the Rust Maven web site maintains several Open source projects in Rust and while he still feels he has tons of new things to learn about Rust he already offers training courses in Rust and still teaches Python, Perl, git, GitHub, GitLab, CI, and testing.

Gabor Szabo