1use crate::vision::{V_MERGE, V_PATCH, V_PATCH_IN, V_TEMPORAL};
16use base64::Engine as _;
17use image::RgbImage;
18use image::imageops::FilterType;
19
20pub const MIN_PIXELS: usize = 65536;
22pub const MAX_PIXELS: usize = 16_777_216;
23const FACTOR: usize = V_PATCH * V_MERGE; pub struct PreppedImage {
26 pub patches: Vec<f32>,
28 pub gh: usize,
29 pub gw: usize,
30}
31
32impl PreppedImage {
33 pub fn n_tokens(&self) -> usize {
35 self.gh * self.gw / (V_MERGE * V_MERGE)
36 }
37}
38
39pub fn smart_resize(h: usize, w: usize) -> Result<(usize, usize), String> {
42 if h < 2 || w < 2 {
43 return Err(format!("image too small: {w}x{h}"));
44 }
45 let ar = h.max(w) as f64 / h.min(w) as f64;
46 if ar > 200.0 {
47 return Err(format!("aspect ratio {ar:.0} exceeds 200"));
48 }
49 let f = FACTOR as f64;
50 let (hf, wf) = (h as f64, w as f64);
51 let mut h_bar = ((hf / f).round() * f).max(f);
52 let mut w_bar = ((wf / f).round() * f).max(f);
53 if h_bar * w_bar > MAX_PIXELS as f64 {
54 let beta = (hf * wf / MAX_PIXELS as f64).sqrt();
55 h_bar = ((hf / beta / f).floor() * f).max(f);
56 w_bar = ((wf / beta / f).floor() * f).max(f);
57 } else if h_bar * w_bar < MIN_PIXELS as f64 {
58 let beta = (MIN_PIXELS as f64 / (hf * wf)).sqrt();
59 h_bar = (hf * beta / f).ceil() * f;
60 w_bar = (wf * beta / f).ceil() * f;
61 }
62 Ok((h_bar as usize, w_bar as usize))
63}
64
65fn decode_frame(bytes: &[u8]) -> Result<(RgbImage, usize, usize), String> {
68 let img = image::load_from_memory(bytes).map_err(|e| format!("image decode: {e}"))?;
69 let rgb = img.to_rgb8();
70 let (w, h) = (rgb.width() as usize, rgb.height() as usize);
71 let (rh, rw) = smart_resize(h, w)?;
72 let resized = image::imageops::resize(&rgb, rw as u32, rh as u32, FilterType::CatmullRom);
73 Ok((resized, rh / V_PATCH, rw / V_PATCH))
74}
75
76fn fill_slot(rows: &mut [f32], frame: &RgbImage, gh: usize, gw: usize, t: usize) {
79 let inv = 1.0f32 / 127.5;
80 for py in 0..gh {
81 for px in 0..gw {
82 let row = &mut rows[(py * gw + px) * V_PATCH_IN..(py * gw + px + 1) * V_PATCH_IN];
83 for c in 0..3 {
84 let base = c * V_TEMPORAL * V_PATCH * V_PATCH + t * V_PATCH * V_PATCH;
85 for ph in 0..V_PATCH {
86 for pw in 0..V_PATCH {
87 let p =
88 frame.get_pixel((px * V_PATCH + pw) as u32, (py * V_PATCH + ph) as u32);
89 row[base + ph * V_PATCH + pw] = p.0[c] as f32 * inv - 1.0;
90 }
91 }
92 }
93 }
94 }
95}
96
97pub fn prep_image_bytes(bytes: &[u8]) -> Result<PreppedImage, String> {
100 let (frame, gh, gw) = decode_frame(bytes)?;
101 let mut patches = vec![0f32; gh * gw * V_PATCH_IN];
102 for t in 0..V_TEMPORAL {
103 fill_slot(&mut patches, &frame, gh, gw, t);
104 }
105 Ok(PreppedImage { patches, gh, gw })
106}
107
108pub fn prep_data_uri(uri: &str) -> Result<PreppedImage, String> {
110 let bytes = decode_data_uri(uri)?;
111 prep_image_bytes(&bytes)
112}
113
114pub struct VisionUnit {
118 pub prep: PreppedImage,
119 pub video: Option<usize>,
121}
122
123pub struct PreppedVideo {
128 pub groups: Vec<PreppedImage>,
129 pub timestamps: Vec<f32>,
130}
131
132pub fn video_max_pixels() -> usize {
135 std::env::var("MEMRA_VIDEO_MAX_PIXELS")
136 .ok()
137 .and_then(|v| v.parse().ok())
138 .unwrap_or(2_097_152)
139}
140pub const VID_MIN_PIXELS: usize = 4096;
141pub const VID_MAX_FRAMES: usize = 32;
143
144pub const GIF_MAX_FRAMES: usize = 512;
156pub const GIF_MAX_TOTAL_PIXELS: usize = 1 << 26; fn smart_resize_video(frames: usize, h: usize, w: usize) -> Result<(usize, usize), String> {
160 if h < 2 || w < 2 {
161 return Err(format!("frame too small: {w}x{h}"));
162 }
163 let ar = h.max(w) as f64 / h.min(w) as f64;
164 if ar > 200.0 {
165 return Err(format!("aspect ratio {ar:.0} exceeds 200"));
166 }
167 let f = FACTOR as f64;
168 let (hf, wf) = (h as f64, w as f64);
169 let t_bar = ((frames as f64 / V_TEMPORAL as f64).round() * V_TEMPORAL as f64).max(2.0);
170 let mut h_bar = ((hf / f).round() * f).max(f);
171 let mut w_bar = ((wf / f).round() * f).max(f);
172 let (min_px, max_px) = (VID_MIN_PIXELS as f64, video_max_pixels() as f64);
173 if t_bar * h_bar * w_bar > max_px {
174 let beta = (frames as f64 * hf * wf / max_px).sqrt();
175 h_bar = ((hf / beta / f).floor() * f).max(f);
176 w_bar = ((wf / beta / f).floor() * f).max(f);
177 } else if t_bar * h_bar * w_bar < min_px {
178 let beta = (min_px / (frames as f64 * hf * wf)).sqrt();
179 h_bar = (hf * beta / f).ceil() * f;
180 w_bar = (wf * beta / f).ceil() * f;
181 }
182 Ok((h_bar as usize, w_bar as usize))
183}
184
185pub fn prep_video_gif(bytes: &[u8]) -> Result<PreppedVideo, String> {
190 use image::AnimationDecoder;
191 use image::ImageDecoder as _;
192 let dec = image::codecs::gif::GifDecoder::new(std::io::Cursor::new(bytes))
193 .map_err(|e| format!("gif decode: {e}"))?;
194 let (cw, ch) = dec.dimensions();
200 let canvas_px = (cw as usize) * (ch as usize);
201 if canvas_px == 0 {
202 return Err("gif has an empty canvas".into());
203 }
204 let max_frames = GIF_MAX_FRAMES.min(GIF_MAX_TOTAL_PIXELS / canvas_px);
205 if max_frames == 0 {
206 return Err(format!(
207 "gif canvas {cw}x{ch} exceeds the decode budget ({GIF_MAX_TOTAL_PIXELS} px)"
208 ));
209 }
210 let mut frames: Vec<(RgbImage, f32)> = Vec::new(); let mut t = 0f32;
212 for fr in dec.into_frames() {
213 if frames.len() >= max_frames {
214 return Err(format!(
215 "gif exceeds the decode budget: more than {max_frames} frames at {cw}x{ch} \
216 (ceiling {GIF_MAX_FRAMES} frames / {GIF_MAX_TOTAL_PIXELS} total px)"
217 ));
218 }
219 let fr = fr.map_err(|e| format!("gif frame: {e}"))?;
220 let (num, den) = fr.delay().numer_denom_ms();
221 let dt = if den == 0 {
222 100.0
223 } else {
224 num as f32 / den as f32
225 } / 1000.0;
226 frames.push((
227 image::DynamicImage::ImageRgba8(fr.into_buffer()).to_rgb8(),
228 t,
229 ));
230 t += dt.max(0.01);
231 }
232 if frames.is_empty() {
233 return Err("gif has no frames".into());
234 }
235 if frames.len() == 1 {
237 let f0 = frames[0].clone();
238 frames.push((f0.0, f0.1));
239 }
240 let total = frames.len();
242 let take = total.min(VID_MAX_FRAMES) & !1;
243 let picked: Vec<usize> = (0..take)
244 .map(|i| i * total / take) .collect();
246 let (h, w) = (frames[0].0.height() as usize, frames[0].0.width() as usize);
247 let (rh, rw) = smart_resize_video(take, h, w)?;
248 let (gh, gw) = (rh / V_PATCH, rw / V_PATCH);
249 let mut groups = Vec::with_capacity(take / 2);
250 let mut timestamps = Vec::with_capacity(take / 2);
251 for g in 0..take / 2 {
252 let (a, b) = (picked[2 * g], picked[2 * g + 1]);
253 let mut patches = vec![0f32; gh * gw * V_PATCH_IN];
254 for (slot, idx) in [(0usize, a), (1usize, b)] {
255 let resized = image::imageops::resize(
256 &frames[idx].0,
257 rw as u32,
258 rh as u32,
259 FilterType::CatmullRom,
260 );
261 fill_slot(&mut patches, &resized, gh, gw, slot);
262 }
263 groups.push(PreppedImage { patches, gh, gw });
264 timestamps.push(frames[a].1);
265 }
266 Ok(PreppedVideo { groups, timestamps })
267}
268
269pub fn decode_data_uri(uri: &str) -> Result<Vec<u8>, String> {
271 let rest = uri
272 .strip_prefix("data:")
273 .ok_or_else(|| "expected data: URI (http fetch requires MEMRA_FETCH_URLS=1)".to_string())?;
274 let (meta, payload) = rest
275 .split_once(',')
276 .ok_or_else(|| "malformed data URI: no comma".to_string())?;
277 if !meta.ends_with(";base64") {
278 return Err("data URI must be base64-encoded".into());
279 }
280 base64::engine::general_purpose::STANDARD
281 .decode(payload.trim())
282 .map_err(|e| format!("base64 decode: {e}"))
283}
284
285#[cfg(test)]
286mod tests {
287 use super::*;
288
289 #[test]
290 fn smart_resize_multiples_and_budget() {
291 let (h, w) = smart_resize(1080, 1920).unwrap();
293 assert_eq!(h % 32, 0);
294 assert_eq!(w % 32, 0);
295 assert!(h * w >= MIN_PIXELS && h * w <= MAX_PIXELS);
296 let (h, w) = smart_resize(64, 64).unwrap();
298 assert!(h * w >= MIN_PIXELS);
299 let (h, w) = smart_resize(8000, 12000).unwrap();
301 assert!(h * w <= MAX_PIXELS);
302 assert!(smart_resize(10, 4000).is_err()); }
304
305 #[test]
306 fn patchify_shape_and_order() {
307 let mut img = RgbImage::new(64, 64);
309 for (x, y, p) in img.enumerate_pixels_mut() {
310 *p = image::Rgb([x as u8, y as u8, 200]);
311 }
312 let mut buf = std::io::Cursor::new(Vec::new());
313 img.write_to(&mut buf, image::ImageFormat::Png).unwrap();
314 let prep = prep_image_bytes(buf.get_ref()).unwrap();
315 assert_eq!(prep.patches.len(), prep.gh * prep.gw * V_PATCH_IN);
316 assert_eq!(prep.gh % V_MERGE, 0);
317 assert_eq!(prep.gw % V_MERGE, 0);
318 let row = &prep.patches[0..V_PATCH_IN];
320 let slot = V_PATCH * V_PATCH;
321 for c in 0..3 {
322 let b = c * V_TEMPORAL * slot;
323 assert_eq!(row[b..b + slot], row[b + slot..b + 2 * slot]);
324 }
325 assert!(prep.patches.iter().all(|v| (-1.0..=1.0).contains(v)));
327 }
328
329 fn crafted_gif(w: u16, h: u16, frames: usize) -> Vec<u8> {
334 let mut b = Vec::new();
335 b.extend_from_slice(b"GIF89a");
336 b.extend_from_slice(&w.to_le_bytes());
337 b.extend_from_slice(&h.to_le_bytes());
338 b.push(0x80); b.push(0); b.push(0); b.extend_from_slice(&[0, 0, 0, 0xFF, 0xFF, 0xFF]); for _ in 0..frames {
343 b.push(0x2C); b.extend_from_slice(&0u16.to_le_bytes()); b.extend_from_slice(&0u16.to_le_bytes()); b.extend_from_slice(&1u16.to_le_bytes()); b.extend_from_slice(&1u16.to_le_bytes()); b.push(0); b.push(0x02); b.extend_from_slice(&[0x02, 0x44, 0x01]); b.push(0x00); }
353 b.push(0x3B); b
355 }
356
357 #[test]
358 fn gif_decode_bomb_is_refused_before_full_expansion() {
359 fn expect_err(bytes: &[u8]) -> String {
363 match prep_video_gif(bytes) {
364 Err(e) => e,
365 Ok(_) => panic!("decode-bomb GIF was accepted"),
366 }
367 }
368 let bomb = crafted_gif(2000, 2000, 64);
369 assert!(bomb.len() < 2048, "the bomb itself is tiny on the wire");
370 let err = expect_err(&bomb);
371 assert!(err.contains("decode budget"), "{err}");
372
373 let ok = crafted_gif(2000, 2000, 4);
375 let vid = prep_video_gif(&ok).unwrap();
376 assert_eq!(vid.groups.len(), 2); let err = expect_err(&crafted_gif(8, 8, GIF_MAX_FRAMES + 8));
380 assert!(err.contains("decode budget"), "{err}");
381
382 let err = expect_err(&crafted_gif(0xFFFF, 0xFFFF, 1));
384 assert!(err.contains("exceeds the decode budget"), "{err}");
385 }
386
387 #[test]
388 fn data_uri_roundtrip() {
389 let png = {
390 let img = RgbImage::new(32, 32);
391 let mut buf = std::io::Cursor::new(Vec::new());
392 img.write_to(&mut buf, image::ImageFormat::Png).unwrap();
393 buf.into_inner()
394 };
395 let uri = format!(
396 "data:image/png;base64,{}",
397 base64::engine::general_purpose::STANDARD.encode(&png)
398 );
399 let prep = prep_data_uri(&uri).unwrap();
400 assert_eq!(prep.n_tokens(), prep.gh * prep.gw / 4);
401 assert!(decode_data_uri("http://x/y.png").is_err());
402 }
403}