How to resize an image
Short answer: The image crate: image::open the file, call resize_exact(width, height, FilterType::Lanczos3), and save the result. When the target is a box the image has to fit inside rather than exact dimensions, use resize instead.
The setup
Section titled “The setup”Start from a fresh project — cargo new resize-demo — and declare the crate:
[dependencies]image = "0.25.10"One line gets the default format features: PNG, JPEG, GIF, WebP and friends. The examples below need a source image, so the first block makes one — 240×120, black on the left, white on the right, with the edge in the middle. A hard step shows what a filter does far better than a photo. If you already have a file, skip this block and swap step.png for its path everywhere below.
use image::{ImageBuffer, Rgb};
fn main() -> Result<(), image::ImageError> { let img: ImageBuffer<Rgb<u8>, Vec<u8>> = ImageBuffer::from_fn(240, 120, |x, _y| { if x < 120 { Rgb([0, 0, 0]) } else { Rgb([255, 255, 255]) } }); img.save("step.png")?; println!("wrote step.png {}x{}", img.width(), img.height()); Ok(())}wrote step.png 240x120image::open guesses the format from the file extension, and save works the same way — .png in, .png out. Each block below is a complete main: paste them into src/main.rs one at a time, in order, and run cargo run after each. The later blocks read step.png back, so keep them all in the same directory.
Resizing to exact dimensions
Section titled “Resizing to exact dimensions”use image::imageops::FilterType;
fn main() -> Result<(), image::ImageError> { let source = image::open("step.png")?; let small = source.resize_exact(120, 60, FilterType::Lanczos3); small.save("step-small.png")?; println!( "resize_exact(120, 60, Lanczos3) -> {}x{}", small.width(), small.height() ); Ok(())}resize_exact(120, 60, Lanczos3) -> 120x60resize_exact takes the target width and height and does exactly that — 240×120 in, 120×60 out. Nothing is cropped and nothing is padded; ask for dimensions that don’t match the source’s ratio and the image stretches. The last argument picks the filter, which decides how the in-between pixels are computed.
Picking a filter
Section titled “Picking a filter”FilterType has five variants, and the difference shows up at the edge. This resizes the step image to 120×60 with each filter and prints the red channel of the ten pixels around the edge — x = 55 to 64, on row 30:
use image::imageops::FilterType;use image::GenericImageView;
fn main() -> Result<(), image::ImageError> { let source = image::open("step.png")?; for filter in [ FilterType::Nearest, FilterType::Triangle, FilterType::CatmullRom, FilterType::Gaussian, FilterType::Lanczos3, ] { let resized = source.resize_exact(120, 60, filter); let row: Vec<u8> = (55..65).map(|x| resized.get_pixel(x, 30).0[0]).collect(); println!("{filter:?}: {row:?}"); } Ok(())}Nearest: [0, 0, 0, 0, 0, 255, 255, 255, 255, 255]Triangle: [0, 0, 0, 0, 32, 223, 255, 255, 255, 255]CatmullRom: [0, 0, 0, 0, 17, 238, 255, 255, 255, 255]Gaussian: [0, 0, 0, 0, 38, 217, 255, 255, 255, 255]Lanczos3: [0, 0, 1, 0, 14, 241, 255, 254, 255, 255]Every filter puts the edge between the same two pixels; what changes is the transition. Nearest copies one source pixel per output pixel, so the jump stays one pixel wide — blocky, and the right choice for pixel art. Triangle blends a 2×2 neighbourhood, Gaussian blurs a shade more, and CatmullRom goes sharper than both. Lanczos3 is sharper still, 14 straight to 241, but it rings: the stray 1 on the black side and the 254 on the white side are the negative lobes of its windowed-sinc kernel pulling the flat colour areas off their values. Sharpest edges, occasional halo — that is the trade, and it is why Lanczos3 is the sensible default for photographs while flat graphics often look cleaner under CatmullRom.
Keeping the aspect ratio
Section titled “Keeping the aspect ratio”resize_exact is one of four methods on DynamicImage, and the other three handle the aspect ratio differently. Same 240×120 source, asked for 100×100:
use image::imageops::FilterType;
fn main() -> Result<(), image::ImageError> { let source = image::open("step.png")?; println!("source {}x{}", source.width(), source.height());
let fitted = source.resize(100, 100, FilterType::Lanczos3); println!("resize(100, 100) -> {}x{}", fitted.width(), fitted.height());
let exact = source.resize_exact(100, 100, FilterType::Lanczos3); println!("resize_exact(100, 100) -> {}x{}", exact.width(), exact.height());
let thumb = source.thumbnail(100, 100); println!("thumbnail(100, 100) -> {}x{}", thumb.width(), thumb.height());
let filled = source.resize_to_fill(100, 100, FilterType::Lanczos3); println!( "resize_to_fill(100, 100) -> {}x{}", filled.width(), filled.height() ); Ok(())}source 240x120resize(100, 100) -> 100x50resize_exact(100, 100) -> 100x100thumbnail(100, 100) -> 100x50resize_to_fill(100, 100) -> 100x100resize treats its arguments as a box: the image is scaled to the largest size that fits inside, so 240×120 into a 100×100 box comes out 100×50. resize_to_fill does the opposite — scale to cover, then crop the overflow — which is what a fixed-size thumbnail slot wants. thumbnail fits like resize but takes no filter: it uses a fast integer algorithm where each source pixel contributes to exactly one target pixel, and the crate’s docs warn it “may give aliasing artifacts if new size is close to old size”. For a one-off resize reach for resize; for a folder of previews, thumbnail skips the filter maths.
Related
Section titled “Related”- How to read a file into bytes — an image file is bytes before it is pixels
- How to write a file atomically — so a crashed resize can’t truncate the original
- Reading and writing files — the rest of the chapter