Files
pile/crates/pile-value/src/extract/blob/image/mod.rs
2026-03-28 11:20:16 -07:00

101 lines
2.4 KiB
Rust

use image::ImageFormat;
use mime::Mime;
use pile_config::Label;
use pile_io::AsyncReader;
use std::{io::Cursor, str::FromStr, sync::Arc};
use tracing::trace;
use transform::{CropTransformer, ImageTransformer, MaxDimTransformer};
mod transform;
use crate::{
extract::traits::{ExtractState, ObjectExtractor},
value::{ArcBytes, BinaryPileValue, PileValue},
};
pub struct ImageExtractor {
item: BinaryPileValue,
}
impl ImageExtractor {
pub fn new(item: &BinaryPileValue) -> Self {
Self { item: item.clone() }
}
async fn apply<T: ImageTransformer + Send + 'static>(
&self,
args: &str,
) -> Result<Option<PileValue>, std::io::Error> {
let transformer = match T::parse_args(args) {
Ok(t) => t,
Err(_) => return Ok(None),
};
let mime = self.item.mime().clone();
let bytes = self.item.read().await?.read_to_end().await?;
let Some(format) = ImageFormat::from_mime_type(&mime) else {
return Ok(Some(PileValue::Binary(BinaryPileValue::Blob {
mime,
bytes: ArcBytes(Arc::new(bytes)),
})));
};
let bytes_for_closure = bytes.clone();
let result = tokio::task::spawn_blocking(move || {
let mut img = image::load_from_memory_with_format(&bytes_for_closure, format)?;
transformer.transform(&mut img);
let mut out = Cursor::new(Vec::new());
img.write_to(&mut out, format)?;
let out_mime =
Mime::from_str(format.to_mime_type()).unwrap_or(mime::APPLICATION_OCTET_STREAM);
Ok::<_, image::ImageError>((out_mime, out.into_inner()))
})
.await?;
match result {
Ok((out_mime, out_bytes)) => Ok(Some(PileValue::Binary(BinaryPileValue::Blob {
mime: out_mime,
bytes: ArcBytes(Arc::new(out_bytes)),
}))),
Err(_) => Ok(Some(PileValue::Binary(BinaryPileValue::Blob {
mime,
bytes: ArcBytes(Arc::new(bytes)),
}))),
}
}
}
#[async_trait::async_trait]
impl ObjectExtractor for ImageExtractor {
async fn field(
&self,
_state: &ExtractState,
name: &Label,
args: Option<&str>,
) -> Result<Option<PileValue>, std::io::Error> {
let Some(args) = args else {
return Ok(None);
};
trace!(?args, "Getting field {name:?} from ImageExtractor",);
match name.as_str() {
"maxdim" => self.apply::<MaxDimTransformer>(args).await,
"crop" => self.apply::<CropTransformer>(args).await,
_ => Ok(None),
}
}
#[expect(clippy::unwrap_used)]
async fn fields(&self) -> Result<Vec<Label>, std::io::Error> {
Ok(vec![
Label::new("maxdim").unwrap(),
Label::new("crop").unwrap(),
])
}
}