pub mod options;
pub mod presets;
pub mod save;
pub mod transform;
pub mod utils;
pub mod watermark;
use crate::monitoring::{increment_processed_images, observe_image_processing_duration};
use crate::processing::options::ParsedOptions;
use crate::processing::watermark::CachedWatermark;
use bytes::Bytes;
use libvips::VipsImage;
use std::time::Instant;
use thiserror::Error;
use tracing::debug;
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum ProcessingError {
#[error(transparent)]
Transform(#[from] transform::TransformError),
#[error(transparent)]
Watermark(#[from] watermark::WatermarkError),
#[error(transparent)]
Save(#[from] save::SaveError),
#[error("processed image would be {width}x{height}, over the {limit}px result dimension limit")]
ResultTooLarge { width: i32, height: i32, limit: u32 },
}
const MAX_LOAD_SHRINK: u32 = 8;
const MIN_LOAD_SHRINK: f64 = 1.5;
fn load_shrink_ratio(parsed_options: &ParsedOptions, src_width: u32, src_height: u32) -> Option<f64> {
if parsed_options.raw {
return None;
}
if parsed_options.trim.is_some() {
return None;
}
let resize = parsed_options.resize.as_ref()?;
if src_width == 0 || src_height == 0 {
return None;
}
let (available_width, available_height) = match parsed_options.crop.as_ref() {
Some(crop) => (
if crop.width == 0 {
src_width
} else {
crop.width.min(src_width)
},
if crop.height == 0 {
src_height
} else {
crop.height.min(src_height)
},
),
None => (src_width, src_height),
};
let grow =
f64::from(parsed_options.dpr.unwrap_or(1.0).max(1.0)) * f64::from(parsed_options.zoom.unwrap_or(1.0).max(1.0));
let forced = resize.resizing_type == "force";
let target_width = if forced && resize.width == 0 {
f64::from(available_width)
} else {
(f64::from(resize.width) * grow).max(f64::from(parsed_options.min_width.unwrap_or(0)))
};
let target_height = if forced && resize.height == 0 {
f64::from(available_height)
} else {
(f64::from(resize.height) * grow).max(f64::from(parsed_options.min_height.unwrap_or(0)))
};
let mut ratio = f64::INFINITY;
if target_width >= 1.0 {
ratio = ratio.min(f64::from(available_width) / target_width);
}
if target_height >= 1.0 {
ratio = ratio.min(f64::from(available_height) / target_height);
}
(ratio.is_finite() && ratio >= MIN_LOAD_SHRINK).then_some(ratio)
}
pub fn load_shrink_factor(parsed_options: &ParsedOptions, src_width: u32, src_height: u32) -> u32 {
let Some(ratio) = load_shrink_ratio(parsed_options, src_width, src_height) else {
return 1;
};
let mut factor = 1;
while factor * 2 <= MAX_LOAD_SHRINK && f64::from(factor * 2) <= ratio {
factor *= 2;
}
factor
}
pub fn load_scale_factor(parsed_options: &ParsedOptions, src_width: u32, src_height: u32) -> Option<f64> {
let scale = 1.0 / load_shrink_ratio(parsed_options, src_width, src_height)?;
(scale > 0.0 && scale < 1.0).then_some(scale)
}
pub fn process_image(
mut img: VipsImage,
mut parsed_options: ParsedOptions,
source_bytes: &Bytes,
watermark: Option<&CachedWatermark>,
) -> Result<Bytes, ProcessingError> {
let start = Instant::now();
debug!("Starting image processing with options: {:?}", parsed_options);
if let Some(dpr) = parsed_options.dpr {
if dpr > 1.0 {
debug!("Applying DPR scaling: {}", dpr);
if let Some(ref mut resize) = parsed_options.resize {
debug!(
"Scaling resize dimensions from {}x{} to {}x{}",
resize.width,
resize.height,
(resize.width as f32 * dpr).round() as u32,
(resize.height as f32 * dpr).round() as u32
);
resize.width = (resize.width as f32 * dpr).round() as u32;
resize.height = (resize.height as f32 * dpr).round() as u32;
}
if let Some(ref mut padding) = parsed_options.padding {
debug!(
"Scaling padding from {:?} to {:?}",
padding,
(
(padding.0 as f32 * dpr).round() as u32,
(padding.1 as f32 * dpr).round() as u32,
(padding.2 as f32 * dpr).round() as u32,
(padding.3 as f32 * dpr).round() as u32
)
);
padding.0 = (padding.0 as f32 * dpr).round() as u32;
padding.1 = (padding.1 as f32 * dpr).round() as u32;
padding.2 = (padding.2 as f32 * dpr).round() as u32;
padding.3 = (padding.3 as f32 * dpr).round() as u32;
}
}
}
debug!("Loaded image: {}x{}", img.get_width(), img.get_height());
if parsed_options.auto_rotate {
debug!("Applying EXIF auto-rotation");
img = transform::apply_exif_rotation(source_bytes.as_ref(), img)?;
}
if let Some(ref trim) = parsed_options.trim {
debug!("Applying trim: {:?}", trim);
img = transform::apply_trim(img, trim)?;
}
if let Some(crop) = parsed_options.crop {
debug!("Applying crop: {:?}", crop);
img = transform::crop_image(img, crop)?;
}
let mut resolved_resize_dims: Option<(u32, u32)> = None;
if let Some(ref resize) = parsed_options.resize {
let src_width = img.get_width() as u32;
let src_height = img.get_height() as u32;
let (target_w, target_h) = transform::resolve_resize_dimensions(resize, src_width, src_height)?;
debug!(
"Applying resize {:?} resolved to {}x{} from source {}x{}",
resize, target_w, target_h, src_width, src_height
);
resolved_resize_dims = Some((target_w, target_h));
img = transform::apply_resize(
img,
resize,
&parsed_options.gravity,
parsed_options.resizing_algorithm.as_deref(),
parsed_options.enlarge,
)?;
}
if parsed_options.min_width.is_some() || parsed_options.min_height.is_some() {
debug!(
"Applying min dimensions: min_width={:?}, min_height={:?}",
parsed_options.min_width, parsed_options.min_height
);
img = transform::apply_min_dimensions(
img,
parsed_options.min_width,
parsed_options.min_height,
parsed_options.resizing_algorithm.as_deref(),
)?;
}
if let Some(zoom) = parsed_options.zoom {
debug!("Applying zoom: {}", zoom);
img = transform::apply_zoom(img, zoom, parsed_options.resizing_algorithm.as_deref())?;
}
if parsed_options.extend {
debug!("Applying extend option");
if let Some((target_w, target_h)) = resolved_resize_dims {
if img.get_width() < target_w as i32 || img.get_height() < target_h as i32 {
let extend_w = target_w.max(img.get_width() as u32);
let extend_h = target_h.max(img.get_height() as u32);
img = transform::extend_image(
img,
extend_w,
extend_h,
&parsed_options.gravity,
&parsed_options.background,
)?;
}
}
}
if let Some((top, right, bottom, left)) = parsed_options.padding {
debug!("Applying padding: {:?}", (top, right, bottom, left));
img = transform::apply_padding(img, top, right, bottom, left, &parsed_options.background)?;
}
if let Some(rotation) = parsed_options.rotation {
debug!("Applying rotation: {}", rotation);
img = transform::apply_rotation(img, rotation)?;
}
if let Some(flip) = parsed_options.flip {
debug!("Applying flip: {:?}", flip);
img = transform::apply_flip(img, flip)?;
}
if let Some(adjust) = parsed_options.adjust {
debug!("Applying color adjustments: {:?}", adjust);
img = transform::apply_adjust(img, adjust)?;
}
if let Some(sigma) = parsed_options.blur {
debug!("Applying blur with sigma: {}", sigma);
img = transform::apply_blur(img, sigma)?;
}
if let Some(sigma) = parsed_options.sharpen {
debug!("Applying sharpen with sigma: {}", sigma);
img = transform::apply_sharpen(img, sigma)?;
}
if let Some(amount) = parsed_options.pixelate {
debug!("Applying pixelate with amount: {}", amount);
img = transform::apply_pixelate(img, amount, parsed_options.resizing_algorithm.as_deref())?;
}
if let Some(ref watermark_opts) = parsed_options.watermark {
if let Some(watermark) = watermark {
debug!("Applying watermark with options: {:?}", watermark_opts);
img = watermark::apply_watermark(
img,
watermark,
watermark_opts,
parsed_options.resizing_algorithm.as_deref(),
)?;
}
}
let output_format = parsed_options.format.as_deref().unwrap_or("jpeg");
if let Some(bg_color) = parsed_options.background {
if output_format == "jpeg" {
debug!("Applying background color for JPEG output: {:?}", bg_color);
img = transform::apply_background_color(img, bg_color)?;
}
}
if let Some(limit) = parsed_options.max_result_dimension {
let (width, height) = (img.get_width(), img.get_height());
if width.max(height) as u32 > limit.get() {
debug!(
"Result {}x{} exceeds max_result_dimension {}",
width,
height,
limit.get()
);
return Err(ProcessingError::ResultTooLarge {
width,
height,
limit: limit.get(),
});
}
}
let quality = parsed_options
.quality
.or_else(|| parsed_options.save.format_quality.get(output_format).copied())
.unwrap_or(85);
let output_vec = save::save_image_with_options(img, output_format, quality, &parsed_options.save)?;
let output_bytes = Bytes::from(output_vec);
debug!("Image processing complete");
let duration = start.elapsed().as_secs_f64();
observe_image_processing_duration(output_format, duration);
increment_processed_images(output_format);
Ok(output_bytes)
}
#[cfg(test)]
mod tests;