use crate::message::{image_media_type, Block};
use anyhow::{bail, Context, Result};
use std::path::Path;
pub const MAX_BYTES: usize = 5 * 1024 * 1024;
pub const MAX_EDGE: u32 = 1568;
const JPEG_QUALITY: u8 = 85;
pub fn block_from_path(path: &Path) -> Result<Option<Block>> {
let Some(media_type) = image_media_type(path) else {
return Ok(None);
};
let bytes = std::fs::read(path).with_context(|| format!("reading {}", path.display()))?;
let name = path.file_name().map(|n| n.to_string_lossy().into_owned());
let dims = image::image_dimensions(path).ok();
let oversized = dims.is_some_and(|(w, h)| w.max(h) > MAX_EDGE);
if !oversized && bytes.len() <= MAX_BYTES {
return Ok(Some(Block::image(media_type, &bytes, name)));
}
let img = image::load_from_memory(&bytes)
.with_context(|| format!("{} is named as an image but did not decode", path.display()))?;
let img = img.thumbnail(MAX_EDGE, MAX_EDGE);
let mut out = Vec::new();
img.to_rgb8()
.write_with_encoder(image::codecs::jpeg::JpegEncoder::new_with_quality(
&mut out,
JPEG_QUALITY,
))
.context("re-encoding a resized image")?;
if out.len() > MAX_BYTES {
bail!(
"{} is {} after resizing to {MAX_EDGE}px and stays above the {} limit",
path.display(),
human(out.len()),
human(MAX_BYTES),
);
}
Ok(Some(Block::image("image/jpeg", &out, name)))
}
fn human(bytes: usize) -> String {
if bytes >= 1024 * 1024 {
format!("{:.1} MB", bytes as f64 / (1024.0 * 1024.0))
} else {
format!("{:.0} KB", bytes as f64 / 1024.0)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::message::Block;
fn png(w: u32, h: u32) -> Vec<u8> {
let img = image::RgbImage::from_fn(w, h, |x, y| {
image::Rgb([(x % 256) as u8, (y % 256) as u8, 128])
});
let mut out = Vec::new();
image::DynamicImage::ImageRgb8(img)
.write_to(&mut std::io::Cursor::new(&mut out), image::ImageFormat::Png)
.unwrap();
out
}
fn write(dir: &std::path::Path, name: &str, bytes: &[u8]) -> std::path::PathBuf {
let p = dir.join(name);
std::fs::write(&p, bytes).unwrap();
p
}
#[test]
fn an_image_that_already_fits_is_passed_through_byte_for_byte() {
let dir = std::env::temp_dir().join(format!("mecha-img-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let bytes = png(64, 48);
let p = write(&dir, "small.png", &bytes);
let block = block_from_path(&p).unwrap().unwrap();
let Block::Image {
media_type,
data,
source,
} = block
else {
panic!("expected an image block")
};
assert_eq!(media_type, "image/png", "the source format is kept");
assert_eq!(source.as_deref(), Some("small.png"));
use base64::Engine as _;
let decoded = base64::engine::general_purpose::STANDARD
.decode(&data)
.unwrap();
assert_eq!(decoded, bytes, "the original bytes, not a re-encode");
std::fs::remove_dir_all(&dir).ok();
}
#[test]
fn an_oversized_image_is_resized_and_re_encoded() {
let dir = std::env::temp_dir().join(format!("mecha-img-big-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let p = write(&dir, "huge.png", &png(4000, 2000));
let block = block_from_path(&p).unwrap().unwrap();
let Block::Image {
media_type, data, ..
} = block
else {
panic!("expected an image block")
};
assert_eq!(media_type, "image/jpeg", "a resize re-encodes");
use base64::Engine as _;
let decoded = base64::engine::general_purpose::STANDARD
.decode(&data)
.unwrap();
assert!(decoded.len() <= MAX_BYTES, "under the provider cap");
let (w, h) = image::load_from_memory(&decoded)
.map(|i| {
(
image::GenericImageView::width(&i),
image::GenericImageView::height(&i),
)
})
.unwrap();
assert!(
w.max(h) <= MAX_EDGE,
"long edge {w}x{h} bounded by {MAX_EDGE}"
);
assert_eq!(w * 2000, h * 4000, "aspect ratio preserved, not stretched");
std::fs::remove_dir_all(&dir).ok();
}
#[test]
fn a_file_that_is_not_an_image_is_none_rather_than_an_error() {
let dir = std::env::temp_dir().join(format!("mecha-img-pdf-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let p = write(&dir, "report.pdf", b"%PDF-1.4");
assert!(block_from_path(&p).unwrap().is_none());
std::fs::remove_dir_all(&dir).ok();
}
#[test]
fn a_file_named_png_that_is_not_one_fails_loudly() {
let dir = std::env::temp_dir().join(format!("mecha-img-lie-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let p = write(&dir, "lie.png", &vec![7u8; 6 * 1024 * 1024]);
let err = block_from_path(&p).unwrap_err().to_string();
assert!(err.contains("did not decode"), "got: {err}");
std::fs::remove_dir_all(&dir).ok();
}
}