Files
asepharyana-hub-tools/backend/workers/src/image/resize.rs
T
asepharyanaandKilo d4bbde9320 feat: implement image tools (compress, resize, convert) and PDF tools (merge, split, images-to-pdf)
Image tools now actually process images:
- Compress: re-encode JPEG with configurable quality
- Resize: Lanczos3 resize with fit modes (inside/fill/crop)
- Convert: cross-format conversion (JPEG/PNG/WebP/GIF/BMP)

PDF tools:
- Merge: combine multiple PDFs into one document
- Split: extract page ranges via pagespec (1-3,5,7-9)
- Images to PDF: embed images as JPEG in A4 pages with scaling
- Compress: re-save PDF with optimized cross-reference table

Also fix metrics format to emit default zero values.

Co-Authored-By: Kilo <kilo@kilo.ai>
2026-07-24 19:52:39 +07:00

78 lines
2.7 KiB
Rust

use std::path::PathBuf;
use image::{DynamicImage, ImageFormat};
use image::imageops::FilterType;
use crate::nats::progress::ProgressReporter;
pub async fn process(
img: &DynamicImage,
options: &serde_json::Value,
output_dir: &PathBuf,
_progress: &ProgressReporter,
) -> Result<PathBuf, Box<dyn std::error::Error + Send + Sync>> {
let width = options.get("width").and_then(|v| v.as_u64()).map(|v| v as u32);
let height = options.get("height").and_then(|v| v.as_u64()).map(|v| v as u32);
let quality = options.get("quality").and_then(|v| v.as_u64()).unwrap_or(85) as u8;
let fit = options.get("fit").and_then(|v| v.as_str()).unwrap_or("inside");
let fmt = options.get("format").and_then(|v| v.as_str()).unwrap_or("jpeg");
let (new_w, new_h) = match (width, height) {
(Some(w), Some(h)) => (w, h),
(Some(w), None) => {
let ratio = w as f64 / img.width() as f64;
(w, (img.height() as f64 * ratio).round() as u32)
}
(None, Some(h)) => {
let ratio = h as f64 / img.height() as f64;
((img.width() as f64 * ratio).round() as u32, h)
}
(None, None) => (img.width(), img.height()),
};
let new_w = new_w.max(1).min(10000);
let new_h = new_h.max(1).min(10000);
let resized = match fit {
"fill" => img.resize_exact(new_w, new_h, FilterType::Lanczos3),
"crop" => img.resize_to_fill(new_w, new_h, FilterType::Lanczos3),
_ => img.resize(new_w, new_h, FilterType::Lanczos3), // "inside" = fit within bounds
};
let image_format = match fmt {
"png" => ImageFormat::Png,
"webp" => ImageFormat::WebP,
"gif" => ImageFormat::Gif,
"bmp" => ImageFormat::Bmp,
_ => ImageFormat::Jpeg,
};
let ext = match image_format {
ImageFormat::Jpeg => "jpg",
ImageFormat::Png => "png",
ImageFormat::WebP => "webp",
ImageFormat::Gif => "gif",
ImageFormat::Bmp => "bmp",
_ => "bin",
};
let output_path = output_dir.join(format!("resized.{}", ext));
// Save with appropriate encoder
match image_format {
ImageFormat::Jpeg => {
let mut rgb = resized.to_rgb8();
let mut file = std::fs::File::create(&output_path)?;
let mut encoder = image::codecs::jpeg::JpegEncoder::new_with_quality(&mut file, quality);
encoder.encode(rgb.as_raw(), resized.width(), resized.height(), image::ExtendedColorType::Rgb8)?;
}
ImageFormat::Png | ImageFormat::WebP | ImageFormat::Gif | ImageFormat::Bmp => {
resized.save(&output_path)?;
}
_ => {
resized.save(&output_path)?;
}
}
Ok(output_path)
}