Files
movies-diary/crates/adapters/image-converter/src/handler.rs
Gabriel Kaszewski f262417971
Some checks failed
CI / Check / Test (push) Failing after 46s
refactor: rename ImageStorage → ObjectStorage
2026-06-03 01:33:08 +02:00

106 lines
3.0 KiB
Rust

use std::sync::Arc;
use async_trait::async_trait;
use domain::{
errors::DomainError,
events::DomainEvent,
ports::{EventHandler, ImageRefCommand, ObjectStorage},
};
use crate::Format;
pub struct ImageConversionHandler {
storage: Arc<dyn ObjectStorage>,
image_ref: Arc<dyn ImageRefCommand>,
format: Format,
}
impl ImageConversionHandler {
pub fn new(
storage: Arc<dyn ObjectStorage>,
image_ref: Arc<dyn ImageRefCommand>,
format: Format,
) -> Self {
Self {
storage,
image_ref,
format,
}
}
}
#[async_trait]
impl EventHandler for ImageConversionHandler {
async fn handle(&self, event: &DomainEvent) -> Result<(), DomainError> {
let key = match event {
DomainEvent::ImageStored { key } => key.clone(),
_ => return Ok(()),
};
if key.ends_with(".avif") || key.ends_with(".webp") {
return Ok(());
}
let bytes = self.storage.get(&key).await?;
let format = self.format;
let converted = tokio::task::spawn_blocking(move || convert(bytes, format))
.await
.map_err(|e| DomainError::InfrastructureError(e.to_string()))?
.map_err(DomainError::InfrastructureError)?;
let ext = format.extension();
let new_key = format!("{key}{ext}");
self.storage.store(&new_key, &converted).await?;
if let Err(e) = self.image_ref.swap(&key, &new_key).await {
tracing::error!("swap failed for {key} → {new_key}: {e}");
return Err(e);
}
if let Err(e) = self.storage.delete(&key).await {
tracing::warn!("failed to delete old image key {key}: {e}");
}
tracing::info!("converted {key} → {new_key}");
Ok(())
}
}
fn convert(bytes: Vec<u8>, format: Format) -> Result<Vec<u8>, String> {
let img = image::load_from_memory(&bytes).map_err(|e| e.to_string())?;
match format {
Format::Avif => {
let rgba = img.to_rgba8();
let width = rgba.width() as usize;
let height = rgba.height() as usize;
let pixels: Vec<ravif::RGBA8> = rgba
.pixels()
.map(|p| ravif::RGBA8 {
r: p.0[0],
g: p.0[1],
b: p.0[2],
a: p.0[3],
})
.collect();
let result = ravif::Encoder::new()
.with_quality(80.0)
.with_speed(6)
.encode_rgba(ravif::Img::new(&pixels, width, height))
.map_err(|e| e.to_string())?;
Ok(result.avif_file.to_vec())
}
Format::Webp => {
let rgba = img.to_rgba8();
let (width, height) = (rgba.width(), rgba.height());
let encoder = webp::Encoder::from_rgba(rgba.as_raw(), width, height);
Ok(encoder.encode(80.0).to_vec())
}
}
}
#[cfg(test)]
#[path = "tests/handler.rs"]
mod tests;