grain.social is a photo sharing platform built on atproto.
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}