use serde_json::{Map, Value};
#[derive(Debug, Clone, Copy, Default)]
pub struct CompactImageOptions {
pub max_dim: u32,
pub jpeg_quality: u8,
}
#[cfg(feature = "image")]
fn compact_defaults(opts: Option<&CompactImageOptions>) -> (u32, u8) {
let mut max_dim = 768u32;
let mut quality = 85u8;
let Some(opts) = opts else {
return (max_dim, quality);
};
if opts.max_dim > 0 {
max_dim = opts.max_dim;
}
if opts.jpeg_quality > 0 {
quality = opts.jpeg_quality;
}
(max_dim, quality)
}
#[cfg(feature = "image")]
pub fn compact_image_attachment(
mime_type: &str,
data_b64: &str,
opts: Option<&CompactImageOptions>,
) -> (String, String) {
use base64::Engine;
use image::codecs::jpeg::JpegEncoder;
use image::codecs::png::PngEncoder;
use image::imageops::FilterType;
use image::ExtendedColorType;
use image::ImageEncoder;
use image::ImageReader;
use std::io::Cursor;
if data_b64.is_empty() || !mime_type.starts_with("image/") {
return (mime_type.to_string(), data_b64.to_string());
}
let raw = match base64::engine::general_purpose::STANDARD.decode(data_b64) {
Ok(v) if !v.is_empty() => v,
_ => return (mime_type.to_string(), data_b64.to_string()),
};
let img = match ImageReader::new(Cursor::new(&raw))
.with_guessed_format()
.ok()
.and_then(|r| r.decode().ok())
{
Some(img) => img,
None => return (mime_type.to_string(), data_b64.to_string()),
};
let (max_dim, quality) = compact_defaults(opts);
let (w, h) = (img.width(), img.height());
if w <= max_dim && h <= max_dim {
return (mime_type.to_string(), data_b64.to_string());
}
let (nw, nh) = if w >= h {
if w > max_dim {
let nh = (h as u64 * max_dim as u64 / w as u64).max(1) as u32;
(max_dim, nh)
} else {
(w, h)
}
} else if h > max_dim {
let nw = (w as u64 * max_dim as u64 / h as u64).max(1) as u32;
(nw, max_dim)
} else {
(w, h)
};
let resized = img.resize_exact(nw, nh, FilterType::CatmullRom);
let rgba = resized.to_rgba8();
let (nw, nh) = (rgba.width(), rgba.height());
let pixels = rgba.as_raw();
let mut buf = Vec::new();
if mime_type == "image/png" {
if PngEncoder::new(&mut buf)
.write_image(pixels, nw, nh, ExtendedColorType::Rgba8)
.is_err()
{
return (mime_type.to_string(), data_b64.to_string());
}
return (
mime_type.to_string(),
base64::engine::general_purpose::STANDARD.encode(buf),
);
}
if JpegEncoder::new_with_quality(&mut buf, quality)
.encode(pixels, nw, nh, ExtendedColorType::Rgba8)
.is_err()
{
return (mime_type.to_string(), data_b64.to_string());
}
(
"image/jpeg".to_string(),
base64::engine::general_purpose::STANDARD.encode(buf),
)
}
#[cfg(not(feature = "image"))]
pub fn compact_image_attachment(
mime_type: &str,
data_b64: &str,
_opts: Option<&CompactImageOptions>,
) -> (String, String) {
(mime_type.to_string(), data_b64.to_string())
}
pub fn compact_attachments(
atts: &[Map<String, Value>],
opts: Option<&CompactImageOptions>,
) -> Vec<Map<String, Value>> {
if atts.is_empty() {
return atts.to_vec();
}
atts.iter()
.map(|att| {
let mut cp = att.clone();
let mime = cp
.get("mime_type")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let data = cp
.get("data")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
if !mime.is_empty() && !data.is_empty() {
let (out_mime, out_data) = compact_image_attachment(&mime, &data, opts);
cp.insert("mime_type".to_string(), Value::String(out_mime));
cp.insert("data".to_string(), Value::String(out_data));
}
cp
})
.collect()
}