grain.social is a photo sharing platform built on atproto.
at main 4.3 kB view raw
1use anyhow::Result; 2use image::codecs::jpeg::JpegEncoder; 3use image::ExtendedColorType; 4use std::io::Cursor; 5use std::time::Instant; 6 7pub struct ResizeOptions { 8 pub width: u32, 9 pub height: u32, 10 pub max_size: usize, // in bytes 11 pub mode: String, 12 pub verbose: bool, 13} 14 15pub struct ResizeResult { 16 pub buffer: Vec<u8>, 17} 18 19pub fn do_resize(input: &[u8], opts: ResizeOptions) -> Result<ResizeResult> { 20 let start_time = Instant::now(); 21 if opts.verbose { 22 eprintln!("Starting image resize - input size: {} bytes, target: {}x{}, max_size: {} bytes", 23 input.len(), opts.width, opts.height, opts.max_size); 24 } 25 26 let img_load_start = Instant::now(); 27 let img = image::load_from_memory(input)?; 28 if opts.verbose { 29 eprintln!("Image loaded in {:?} - dimensions: {}x{}", img_load_start.elapsed(), img.width(), img.height()); 30 } 31 32 let resize_start = Instant::now(); 33 let resized = match opts.mode.as_str() { 34 "inside" => img.resize(opts.width, opts.height, image::imageops::FilterType::Triangle), 35 "cover" => img.resize_to_fill(opts.width, opts.height, image::imageops::FilterType::Triangle), 36 _ => img.resize(opts.width, opts.height, image::imageops::FilterType::Triangle), 37 }; 38 if opts.verbose { 39 eprintln!("Image resized in {:?} - new dimensions: {}x{}", resize_start.elapsed(), resized.width(), resized.height()); 40 } 41 42 // Binary search for the best quality that fits under max_size 43 let mut best_result: Option<ResizeResult> = None; 44 let mut min_quality = 1u8; 45 let mut max_quality = 100u8; 46 let mut iteration = 0; 47 48 let search_start = Instant::now(); 49 if opts.verbose { 50 eprintln!("Starting binary search for optimal quality"); 51 } 52 53 while max_quality > min_quality + 1 { 54 iteration += 1; 55 let quality = (min_quality + max_quality) / 2; 56 let encode_start = Instant::now(); 57 58 let mut buffer = Vec::new(); 59 { 60 let mut cursor = Cursor::new(&mut buffer); 61 let mut encoder = JpegEncoder::new_with_quality(&mut cursor, quality); 62 encoder.encode( 63 resized.as_bytes(), 64 resized.width(), 65 resized.height(), 66 ExtendedColorType::Rgb8, 67 )?; 68 } 69 70 let size = buffer.len(); 71 if opts.verbose { 72 eprintln!("Iteration {}: quality={}, size={} bytes, encoded in {:?}", 73 iteration, quality, size, encode_start.elapsed()); 74 } 75 76 if size <= opts.max_size { 77 min_quality = quality; 78 best_result = Some(ResizeResult { 79 buffer, 80 }); 81 if opts.verbose { 82 eprintln!(" -> Fits! New min_quality: {}", min_quality); 83 } 84 } else { 85 max_quality = quality; 86 if opts.verbose { 87 eprintln!(" -> Too large! New max_quality: {}", max_quality); 88 } 89 } 90 } 91 92 if opts.verbose { 93 eprintln!("Binary search completed in {:?} after {} iterations", search_start.elapsed(), iteration); 94 } 95 96 // Try with minimum quality if no result yet 97 if best_result.is_none() { 98 if opts.verbose { 99 eprintln!("No result found, trying minimum quality: {}", min_quality); 100 } 101 let final_encode_start = Instant::now(); 102 103 let mut buffer = Vec::new(); 104 { 105 let mut cursor = Cursor::new(&mut buffer); 106 let mut encoder = JpegEncoder::new_with_quality(&mut cursor, min_quality); 107 encoder.encode( 108 resized.as_bytes(), 109 resized.width(), 110 resized.height(), 111 ExtendedColorType::Rgb8, 112 )?; 113 } 114 115 if opts.verbose { 116 eprintln!("Final encode with quality {} completed in {:?}, size: {} bytes", 117 min_quality, final_encode_start.elapsed(), buffer.len()); 118 } 119 120 best_result = Some(ResizeResult { 121 buffer, 122 }); 123 } 124 125 if opts.verbose { 126 eprintln!("Total resize operation completed in {:?}", start_time.elapsed()); 127 } 128 best_result.ok_or_else(|| anyhow::anyhow!("Failed to compress image")) 129}