use base64::{Engine as _, engine::general_purpose::STANDARD};
use serde_json::Value;
use std::sync::atomic::{AtomicU64, Ordering};
#[derive(Debug, Clone)]
pub struct ImageCompressionConfig {
pub enabled: bool,
pub max_dimension: u32,
pub quality: u8,
pub min_size_bytes: usize,
}
impl Default for ImageCompressionConfig {
fn default() -> Self {
Self {
enabled: false, max_dimension: 1536,
quality: 75,
min_size_bytes: 50_000,
}
}
}
#[derive(Debug, Clone)]
pub struct ImageCompressResult {
pub data: String,
pub media_type: String,
pub original_bytes: usize,
pub compressed_bytes: usize,
pub tokens_saved: usize,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ImageClass {
Screenshot,
Diagram,
Photo,
Unknown,
}
#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
pub struct ImageStats {
pub images_processed: u64,
pub images_compressed: u64,
pub bytes_saved: u64,
pub tokens_saved: u64,
}
static IMAGES_PROCESSED: AtomicU64 = AtomicU64::new(0);
static IMAGES_COMPRESSED: AtomicU64 = AtomicU64::new(0);
static BYTES_SAVED: AtomicU64 = AtomicU64::new(0);
static TOKENS_SAVED: AtomicU64 = AtomicU64::new(0);
pub fn compress_anthropic_images(doc: &mut Value, config: &ImageCompressionConfig) -> usize {
if !config.enabled {
return 0;
}
let Some(messages) = doc.get_mut("messages").and_then(Value::as_array_mut) else {
return 0;
};
let mut count = 0;
for msg in messages.iter_mut() {
if let Some(content) = msg.get_mut("content").and_then(Value::as_array_mut) {
for block in content.iter_mut() {
if compress_anthropic_image_block(block, config) {
count += 1;
}
}
}
}
count
}
pub fn compress_openai_images(doc: &mut Value, config: &ImageCompressionConfig) -> usize {
if !config.enabled {
return 0;
}
let Some(messages) = doc.get_mut("messages").and_then(Value::as_array_mut) else {
return 0;
};
let mut count = 0;
for msg in messages.iter_mut() {
if let Some(content) = msg.get_mut("content").and_then(Value::as_array_mut) {
for part in content.iter_mut() {
if compress_openai_image_part(part, config) {
count += 1;
}
}
}
}
count
}
pub fn stats() -> ImageStats {
ImageStats {
images_processed: IMAGES_PROCESSED.load(Ordering::Relaxed),
images_compressed: IMAGES_COMPRESSED.load(Ordering::Relaxed),
bytes_saved: BYTES_SAVED.load(Ordering::Relaxed),
tokens_saved: TOKENS_SAVED.load(Ordering::Relaxed),
}
}
fn compress_anthropic_image_block(block: &mut Value, config: &ImageCompressionConfig) -> bool {
let block_type = block.get("type").and_then(Value::as_str);
if block_type != Some("image") {
return false;
}
let source = block.get("source");
let source_type = source.and_then(|s| s.get("type")).and_then(Value::as_str);
if source_type != Some("base64") {
return false;
}
let data = source
.and_then(|s| s.get("data"))
.and_then(Value::as_str)
.unwrap_or("");
let media_type = source
.and_then(|s| s.get("media_type"))
.and_then(Value::as_str)
.unwrap_or("image/png");
IMAGES_PROCESSED.fetch_add(1, Ordering::Relaxed);
let Ok(decoded) = STANDARD.decode(data) else {
return false;
};
if decoded.len() < config.min_size_bytes {
return false;
}
if let Some(result) = compress_image_bytes(&decoded, media_type, config) {
let new_source = serde_json::json!({
"type": "base64",
"media_type": result.media_type,
"data": result.data,
});
block["source"] = new_source;
IMAGES_COMPRESSED.fetch_add(1, Ordering::Relaxed);
BYTES_SAVED.fetch_add(
result
.original_bytes
.saturating_sub(result.compressed_bytes) as u64,
Ordering::Relaxed,
);
TOKENS_SAVED.fetch_add(result.tokens_saved as u64, Ordering::Relaxed);
true
} else {
false
}
}
fn compress_openai_image_part(part: &mut Value, config: &ImageCompressionConfig) -> bool {
let part_type = part.get("type").and_then(Value::as_str);
if part_type != Some("image_url") {
return false;
}
let image_url = part.get("image_url");
let detail = image_url
.and_then(|iu| iu.get("detail"))
.and_then(Value::as_str)
.unwrap_or("auto");
if detail == "high" || detail == "low" {
return false; }
let url = image_url
.and_then(|iu| iu.get("url"))
.and_then(Value::as_str)
.unwrap_or("");
if !url.starts_with("data:image/") {
return false;
}
IMAGES_PROCESSED.fetch_add(1, Ordering::Relaxed);
let Some((media_type, data)) = parse_data_uri(url) else {
return false;
};
let Ok(decoded) = STANDARD.decode(data) else {
return false;
};
if decoded.len() < config.min_size_bytes {
return false;
}
if let Some(result) = compress_image_bytes(&decoded, &media_type, config) {
let new_url = format!("data:{};base64,{}", result.media_type, result.data);
part["image_url"]["url"] = Value::String(new_url);
IMAGES_COMPRESSED.fetch_add(1, Ordering::Relaxed);
BYTES_SAVED.fetch_add(
result
.original_bytes
.saturating_sub(result.compressed_bytes) as u64,
Ordering::Relaxed,
);
TOKENS_SAVED.fetch_add(result.tokens_saved as u64, Ordering::Relaxed);
true
} else {
false
}
}
fn compress_image_bytes(
data: &[u8],
_media_type: &str,
config: &ImageCompressionConfig,
) -> Option<ImageCompressResult> {
let (width, height) = detect_dimensions(data)?;
let original_bytes = data.len();
let (target_w, target_h) = if width > config.max_dimension || height > config.max_dimension {
let scale = config.max_dimension as f64 / width.max(height) as f64;
(
(width as f64 * scale) as u32,
(height as f64 * scale) as u32,
)
} else {
if original_bytes < config.min_size_bytes * 2 {
return None;
}
(width, height)
};
let original_tokens = estimate_vision_tokens(width, height);
let target_tokens = estimate_vision_tokens(target_w, target_h);
let tokens_saved = original_tokens.saturating_sub(target_tokens);
if tokens_saved < 50 {
return None; }
if width > config.max_dimension || height > config.max_dimension {
let encoded = STANDARD.encode(data);
return Some(ImageCompressResult {
data: encoded,
media_type: _media_type.to_string(),
original_bytes,
compressed_bytes: original_bytes,
tokens_saved,
});
}
None
}
fn detect_dimensions(data: &[u8]) -> Option<(u32, u32)> {
if data.len() < 24 {
return None;
}
if data.starts_with(b"\x89PNG\r\n\x1a\n") {
let width = u32::from_be_bytes([data[16], data[17], data[18], data[19]]);
let height = u32::from_be_bytes([data[20], data[21], data[22], data[23]]);
return Some((width, height));
}
if data.len() >= 2 && data[0] == 0xFF && data[1] == 0xD8 {
let mut i = 2;
while i + 9 < data.len() {
if data[i] != 0xFF {
i += 1;
continue;
}
let marker = data[i + 1];
if (0xC0..=0xC3).contains(&marker) {
let height = u16::from_be_bytes([data[i + 5], data[i + 6]]) as u32;
let width = u16::from_be_bytes([data[i + 7], data[i + 8]]) as u32;
return Some((width, height));
}
let len = u16::from_be_bytes([data[i + 2], data[i + 3]]) as usize;
i += 2 + len;
}
}
if data.len() >= 30 && &data[0..4] == b"RIFF" && &data[8..12] == b"WEBP" {
if &data[12..16] == b"VP8 " && data.len() >= 30 {
let width = (u16::from_le_bytes([data[26], data[27]]) & 0x3FFF) as u32;
let height = (u16::from_le_bytes([data[28], data[29]]) & 0x3FFF) as u32;
return Some((width, height));
}
if &data[12..16] == b"VP8L" && data.len() >= 25 {
let bits = u32::from_le_bytes([data[21], data[22], data[23], data[24]]);
let width = (bits & 0x3FFF) + 1;
let height = ((bits >> 14) & 0x3FFF) + 1;
return Some((width, height));
}
if &data[12..16] == b"VP8X" && data.len() >= 30 {
let width = (u32::from_le_bytes([data[24], data[25], data[26], 0]) & 0xFFFFFF) + 1;
let height = (u32::from_le_bytes([data[27], data[28], data[29], 0]) & 0xFFFFFF) + 1;
return Some((width, height));
}
}
None
}
fn estimate_vision_tokens(width: u32, height: u32) -> usize {
((width as usize) * (height as usize)) / 750
}
fn parse_data_uri(uri: &str) -> Option<(String, &str)> {
let rest = uri.strip_prefix("data:")?;
let semi = rest.find(';')?;
let media_type = &rest[..semi];
let after_semi = &rest[semi + 1..];
let data = after_semi.strip_prefix("base64,")?;
Some((media_type.to_string(), data))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn detect_png_dimensions() {
let mut png = vec![0x89, b'P', b'N', b'G', 0x0D, 0x0A, 0x1A, 0x0A]; png.extend_from_slice(&[0, 0, 0, 13]); png.extend_from_slice(b"IHDR"); png.extend_from_slice(&100u32.to_be_bytes()); png.extend_from_slice(&50u32.to_be_bytes()); png.extend_from_slice(&[8, 2, 0, 0, 0]);
assert_eq!(detect_dimensions(&png), Some((100, 50)));
}
#[test]
fn estimate_tokens_1080p() {
let tokens = estimate_vision_tokens(1920, 1080);
assert_eq!(tokens, 2764); }
#[test]
fn estimate_tokens_resized() {
let tokens = estimate_vision_tokens(1024, 576);
assert_eq!(tokens, 786); }
#[test]
fn parse_data_uri_valid() {
let uri = "data:image/png;base64,iVBORw0KGgo=";
let (media, data) = parse_data_uri(uri).unwrap();
assert_eq!(media, "image/png");
assert_eq!(data, "iVBORw0KGgo=");
}
#[test]
fn parse_data_uri_invalid() {
assert!(parse_data_uri("https://example.com/img.png").is_none());
assert!(parse_data_uri("not-a-data-uri").is_none());
}
#[test]
fn config_default_is_opt_in() {
let config = ImageCompressionConfig::default();
assert!(!config.enabled);
assert_eq!(config.max_dimension, 1536);
assert_eq!(config.quality, 75);
}
#[test]
fn skip_small_images() {
let config = ImageCompressionConfig {
enabled: true,
min_size_bytes: 50_000,
..Default::default()
};
let mut png = vec![0x89, b'P', b'N', b'G', 0x0D, 0x0A, 0x1A, 0x0A];
png.extend_from_slice(&[0, 0, 0, 13]);
png.extend_from_slice(b"IHDR");
png.extend_from_slice(&100u32.to_be_bytes());
png.extend_from_slice(&50u32.to_be_bytes());
png.extend_from_slice(&[8, 2, 0, 0, 0]);
png.resize(1000, 0);
assert!(compress_image_bytes(&png, "image/png", &config).is_none());
}
#[test]
fn openai_respects_detail_high() {
let config = ImageCompressionConfig {
enabled: true,
..Default::default()
};
let mut doc = serde_json::json!({
"messages": [{
"role": "user",
"content": [{
"type": "image_url",
"image_url": {
"url": "data:image/png;base64,abc",
"detail": "high"
}
}]
}]
});
assert_eq!(compress_openai_images(&mut doc, &config), 0);
}
#[test]
fn openai_skips_remote_urls() {
let config = ImageCompressionConfig {
enabled: true,
..Default::default()
};
let mut doc = serde_json::json!({
"messages": [{
"role": "user",
"content": [{
"type": "image_url",
"image_url": {
"url": "https://example.com/img.png"
}
}]
}]
});
assert_eq!(compress_openai_images(&mut doc, &config), 0);
}
}