Debug School

rakesh kumar
rakesh kumar

Posted on

Compress Images During Upload in Rust (Actix): Faster Rendering for Large Vehicle/Brand Catalog

Step 1: Add image processing dependency
Update Cargo.toml:

tokio = { version = "1.44.1", features = ["fs", "io-util"] }
uuid = { version = "1.16.0", features = ["v4"] }
image = { version = "0.25.5", default-features = false, features = ["jpeg", "png", "webp", "gif"] }
Enter fullscreen mode Exit fullscreen mode

Step 2: Create reusable compression helper
In common.rs, add compress_uploaded_image:

Input: raw bytes + filename + max width/height + quality
Behavior:
decode image
resize if larger than target
if alpha channel -> save PNG
else -> save JPEG with quality
keep SVG/GIF as original
Use signature like:

pub fn compress_uploaded_image(
    raw: &[u8],
    original_filename: &str,
    max_width: u32,
    max_height: u32,
    jpeg_quality: u8,
) -> Result<(Vec<u8>, &'static str)>
Enter fullscreen mode Exit fullscreen mode

full fun code

pub fn compress_uploaded_image(
    raw: &[u8],
    original_filename: &str,
    max_width: u32,
    max_height: u32,
    jpeg_quality: u8,
) -> Result<(Vec<u8>, &'static str)> {
    let original_ext = sanitize_ext(original_filename);

    // Keep formats that are not handled by raster re-encode.
    if original_ext == ".svg" || original_ext == ".gif" {
        return Ok((raw.to_vec(), original_ext));
    }

    let decoded = match image::load_from_memory(raw) {
        Ok(img) => img,
        Err(_) => {
            // Keep original if decode fails instead of breaking upload.
            return Ok((raw.to_vec(), original_ext));
        }
    };

    let (w, h) = decoded.dimensions();
    let resized = if w > max_width || h > max_height {
        decoded.resize(max_width, max_height, FilterType::Lanczos3)
    } else {
        decoded
    };

    let has_alpha = matches!(
        resized.color(),
        ColorType::La8 | ColorType::La16 | ColorType::Rgba8 | ColorType::Rgba16 | ColorType::Rgba32F
    );

    let mut out = Vec::new();
    if has_alpha {
        let rgba = resized.to_rgba8();
        let (rw, rh) = rgba.dimensions();
        PngEncoder::new(&mut out)
            .write_image(rgba.as_raw(), rw, rh, ColorType::Rgba8.into())
            .map_err(error::ErrorInternalServerError)?;
        Ok((out, ".png"))
    } else {
        let rgb = resized.to_rgb8();
        JpegEncoder::new_with_quality(&mut out, jpeg_quality)
            .encode_image(&image::DynamicImage::ImageRgb8(rgb))
            .map_err(error::ErrorInternalServerError)?;
        Ok((out, ".jpg"))
    }
Enter fullscreen mode Exit fullscreen mode

Step 3: Apply in Brand Model upload handler
In brand_model.rs:

use super::common::{compress_uploaded_image, ensure_login_user_id, ApiResponse};
Enter fullscreen mode Exit fullscreen mode

Inside upload_brand_model_images_handler, read multipart into memory, compress, then write compressed bytes:

let mut raw = Vec::new();
while let Some(chunk) = field.next().await {
    let data = chunk.map_err(error::ErrorBadRequest)?;
    raw.extend_from_slice(&data);
}
if raw.is_empty() {
    continue;
}
let (compressed, ext) = compress_uploaded_image(&raw, &file_name, 1280, 1280, 76)?;

let stored_name = format!(
    "vehicle_image_{}_{}{}",
    chrono::Utc::now().format("%Y%m%d%H%M%S"),
    Uuid::new_v4().simple(),
    ext
);
let full_path = format!("uploads/brand_models/{}", stored_name);
let mut file = File::create(&full_path)
    .await
    .map_err(error::ErrorInternalServerError)?;
file.write_all(&compressed)
    .await
    .map_err(error::ErrorInternalServerError)?;

saved_files.push(stored_name);
if saved_files.len() >= 3 {
    break;
}
Enter fullscreen mode Exit fullscreen mode

full function code

pub async fn upload_brand_model_images_handler(
    mut payload: Multipart,
    session: Session,
) -> Result<HttpResponse> {
    ensure_login_user_id(&session)?;

    let mut saved_files: Vec<String> = Vec::new();

    while let Some(item) = payload.next().await {
        let mut field = item.map_err(error::ErrorBadRequest)?;

        let file_name = field
            .content_disposition()
            .and_then(|cd| cd.get_filename())
            .unwrap_or("upload.bin")
            .to_string();

        let mut raw = Vec::new();
        while let Some(chunk) = field.next().await {
            let data = chunk.map_err(error::ErrorBadRequest)?;
            raw.extend_from_slice(&data);
        }
        if raw.is_empty() {
            continue;
        }
        let (compressed, ext) = compress_uploaded_image(&raw, &file_name, 1280, 1280, 76)?;

        let stored_name = format!(
            "vehicle_image_{}_{}{}",
            chrono::Utc::now().format("%Y%m%d%H%M%S"),
            Uuid::new_v4().simple(),
            ext
        );
        let full_path = format!("uploads/brand_models/{}", stored_name);
        let mut file = File::create(&full_path)
            .await
            .map_err(error::ErrorInternalServerError)?;
        file.write_all(&compressed)
            .await
            .map_err(error::ErrorInternalServerError)?;

        saved_files.push(stored_name);
        if saved_files.len() >= 3 {
            break;
        }
    }

    let response = ApiResponse {
        message: "Images uploaded".to_string(),
        data: saved_files,
    };

    Ok(HttpResponse::Ok().json(response))
}
Enter fullscreen mode Exit fullscreen mode

Step 4: Apply in Vehicle partner image upload handler

In vehicle.rs:

use super::common::{compress_uploaded_image, ensure_login_user_id, ApiResponse};
Enter fullscreen mode Exit fullscreen mode
In upload_vehicle_partner_image_handler:

let mut raw = Vec::new();
while let Some(chunk) = field.next().await {
    let data = chunk.map_err(error::ErrorBadRequest)?;
    raw.extend_from_slice(&data);
}
if raw.is_empty() {
    continue;
}
let (compressed, ext) = compress_uploaded_image(&raw, &file_name, 1280, 1280, 76)?;
let timestamp = chrono::Utc::now().format("%Y%m%d%H%M%S");
let rand = Uuid::new_v4().simple().to_string();
let stored_name = format!("vehicle_image_{}_{}{}", timestamp, rand, ext);
let full_path = format!("uploads/brand_models/{}", stored_name);
let mut file = File::create(&full_path)
    .await
    .map_err(error::ErrorInternalServerError)?;
file.write_all(&compressed)
    .await
    .map_err(error::ErrorInternalServerError)?;

partner_image = Some(stored_name);
Enter fullscreen mode Exit fullscreen mode

full function

pub async fn upload_vehicle_partner_image_handler(
    mut payload: Multipart,
    session: Session,
) -> Result<HttpResponse> {
    ensure_login_user_id(&session)?;

    let mut partner_image: Option<String> = None;

    while let Some(item) = payload.next().await {
        let mut field = item.map_err(error::ErrorBadRequest)?;
        let field_name = field
            .content_disposition()
            .and_then(|cd| cd.get_name())
            .unwrap_or("")
            .to_string();
        if field_name != "partner_image" {
            continue;
        }

        let file_name = field
            .content_disposition()
            .and_then(|cd| cd.get_filename())
            .unwrap_or("upload.bin")
            .to_string();

        let mut raw = Vec::new();
        while let Some(chunk) = field.next().await {
            let data = chunk.map_err(error::ErrorBadRequest)?;
            raw.extend_from_slice(&data);
        }
        if raw.is_empty() {
            continue;
        }
        let (compressed, ext) = compress_uploaded_image(&raw, &file_name, 1280, 1280, 76)?;
        let timestamp = chrono::Utc::now().format("%Y%m%d%H%M%S");
        let rand = Uuid::new_v4().simple().to_string();
        let stored_name = format!("vehicle_image_{}_{}{}", timestamp, rand, ext);
        // Store partner image with admin vehicle images in same folder.
        let full_path = format!("uploads/brand_models/{}", stored_name);
        let mut file = File::create(&full_path)
            .await
            .map_err(error::ErrorInternalServerError)?;
        file.write_all(&compressed)
            .await
            .map_err(error::ErrorInternalServerError)?;

        partner_image = Some(stored_name);
        break;
    }

    #[derive(serde::Serialize)]
    struct UploadedPartnerImage {
        partner_image: Option<String>,
    }

    let response = ApiResponse {
        message: "Partner vehicle image uploaded".to_string(),
        data: UploadedPartnerImage { partner_image },
    };


Enter fullscreen mode Exit fullscreen mode

Ok(HttpResponse::Ok().json(response))
}

Step 5: Why this helps
Smaller files on disk
Faster image serving from /uploads
Better frontend rendering speed on list/gallery pages
Consistent optimized size for high-volume image CRUD

Top comments (0)