Redimensionar imágenes por lotes en Rust

El crate image lee y escribe los formatos habituales sin depender de bibliotecas del sistema. Combinado con rayon, generar miniaturas de una carpeta entera son unas veinte líneas y usa todos los núcleos.

Las dependencias

cargo add image
cargo add rayon
[dependencies]
image = "0.25.10"
rayon = "1.12.0"

Miniaturas de toda una carpeta

use image::imageops::FilterType;
use rayon::prelude::*;
use std::fs;
use std::path::PathBuf;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    fs::create_dir_all("miniaturas")?;

    let fotos: Vec<PathBuf> = fs::read_dir("fotos")?
        .filter_map(|e| e.ok())
        .map(|e| e.path())
        .filter(|p| p.extension().is_some_and(|x| x == "jpg"))
        .collect();

    let hechas: Vec<String> = fotos
        .par_iter()
        .filter_map(|ruta| {
            let img = image::open(ruta).ok()?;
            let mini = img.resize(320, 320, FilterType::Lanczos3);
            let nombre = ruta.file_name()?.to_string_lossy().into_owned();
            mini.save(format!("miniaturas/{nombre}")).ok()?;
            Some(format!("{nombre}: {}x{} -> {}x{}", img.width(), img.height(), mini.width(), mini.height()))
        })
        .collect();

    let mut ordenadas = hechas;
    ordenadas.sort();
    for l in &ordenadas { println!("{l}"); }
    println!("{} miniaturas generadas", ordenadas.len());
    Ok(())
}
cargo run --releasefoto1.jpg: 1600x1200 -> 320x240
foto2.jpg: 2400x1600 -> 320x213
foto3.jpg: 800x800 -> 320x320
3 miniaturas generadas

Fíjate en los tamaños resultantes: ninguno es 320×320 salvo el que ya era cuadrado. resize interpreta los dos números como un marco máximo y conserva la proporción, encajando la imagen dentro. Si lo que quieres es forzar las dimensiones exactas, deformando la imagen, el método es resize_exact.

El par_iter() hace que cada foto se procese en un núcleo distinto, que es donde esto gana de verdad: redimensionar es costoso y las fotos son independientes entre sí. Es el patrón del ejemplo de rayon aplicado a algo útil.

El formato de salida sale de la extensión. mini.save("x.png") escribe un PNG y mini.save("x.jpg") un JPEG, sin que haya que indicarlo. Como aquí se reutiliza el nombre original, las miniaturas salen en el mismo formato que las fotos. Para forzar otro, se cambia la extensión o se usa save_with_format.

Operaciones habituales

Qué quieres Cómo
Abrir cualquier formato image::open(ruta)?
Encajar en un marco img.resize(an, al, filtro)
Tamaño exacto, deformando img.resize_exact(an, al, filtro)
Recortar img.crop_imm(x, y, an, al)
Girar img.rotate90()
Blanco y negro img.grayscale()
Guardar img.save("salida.png")?
El filtro importa, y no poco. FilterType::Nearest es el más rápido y el que peor se ve al reducir; Lanczos3, el usado aquí, es el más lento y el de mejor calidad. Para miniaturas que va a mirar una persona, Lanczos3 o CatmullRom; para una vista previa que se descarta enseguida, Triangle basta. La diferencia de tiempo entre el más rápido y el mejor puede ser de varias veces.

El código de esta página se compiló y ejecutó con rustc 1.96.1, image 0.25.10 y rayon 1.12.0 antes de publicarla, sobre tres imágenes reales; la salida está copiada de la ejecución. Documentación oficial: image en docs.rs.