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 newhight
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.