1use std::collections::HashMap;
5use std::sync::{Arc, Mutex};
6
7use deckcraft_model::style::PictureAdjust;
8use vello_cpu::Pixmap;
9
10const BUDGET: usize = 512 << 20;
11
12struct Entry {
13 _bytes: Arc<Vec<u8>>,
14 pm: Arc<Pixmap>,
15 stamp: u64,
16}
17
18#[derive(Default)]
19struct Cache {
20 map: HashMap<(usize, u32, u64), Entry>,
21 bytes: usize,
22 clock: u64,
23 bad: HashMap<usize, Arc<Vec<u8>>>,
24 anims: HashMap<(usize, u64), (Arc<Vec<u8>>, Option<Arc<Anim>>)>,
26}
27
28const GIF_SIDE: u32 = 1024;
32const GIF_BUDGET: usize = 64 << 20;
33const GIF_MAX_FRAMES: usize = 1000;
34const GIF_MAX_CANVAS: u64 = 4096 * 4096;
35const GIF_CACHE_BUDGET: usize = 192 << 20;
36
37struct Anim {
39 frames: Vec<Arc<Pixmap>>,
40 ends: Vec<f64>,
41}
42
43impl Anim {
44 fn bytes(&self) -> usize {
46 self.frames
47 .iter()
48 .map(|f| f.width() as usize * f.height() as usize * 4)
49 .sum()
50 }
51
52 fn at(&self, t: f64) -> (usize, f64) {
54 let total = self.ends.last().copied().unwrap_or(0.0);
55 if total <= 0.0 || !t.is_finite() {
56 return (0, 1.0);
57 }
58 let local = t.max(0.0) % total;
59 let i = self
60 .ends
61 .partition_point(|e| *e <= local)
62 .min(self.ends.len().saturating_sub(1));
63 (
64 i,
65 (self.ends.get(i).copied().unwrap_or(total) - local).max(0.001),
66 )
67 }
68}
69
70static CACHE: Mutex<Option<Cache>> = Mutex::new(None);
71
72fn with<R>(f: impl FnOnce(&mut Cache) -> R) -> R {
73 let mut g = CACHE.lock().unwrap_or_else(|e| e.into_inner());
74 f(g.get_or_insert_with(Cache::default))
75}
76
77fn adjust_key(a: &PictureAdjust) -> u64 {
78 let mut h: u64 = 1469598103934665603;
79 let mut mix = |v: u64| {
80 h ^= v;
81 h = h.wrapping_mul(1099511628211);
82 };
83 mix(a.brightness.to_bits());
84 mix(a.contrast.to_bits());
85 mix(a.saturation.unwrap_or(1.0).to_bits());
86 mix(a.grayscale as u64);
87 mix(a.sharpen.to_bits());
88 if let Some(c) = a.clear_color {
89 mix(((c.r as u64) << 16) | ((c.g as u64) << 8) | c.b as u64 | 1 << 40);
90 }
91 if let Some((d, l)) = duotone_rgb(a) {
92 for c in [d, l] {
93 mix(((c.r as u64) << 24)
94 | ((c.g as u64) << 16)
95 | ((c.b as u64) << 8)
96 | c.a as u64
97 | 1 << 41);
98 }
99 }
100 h
101}
102
103fn duotone_rgb(a: &PictureAdjust) -> Option<(deckcraft_color::Rgba, deckcraft_color::Rgba)> {
106 use deckcraft_model::style::ColorBase;
107 let (d, l) = a.duotone.as_ref()?;
108 let rgb = |c: &deckcraft_model::style::ColorRef, or: deckcraft_color::Rgba| match &c.base {
109 ColorBase::Rgb { rgb } if c.mods.is_empty() => *rgb,
110 _ => or,
111 };
112 Some((
113 rgb(d, deckcraft_color::Rgba::BLACK),
114 rgb(l, deckcraft_color::Rgba::WHITE),
115 ))
116}
117
118pub fn decode(bytes: &[u8]) -> Option<image::RgbaImage> {
121 decode_at(bytes, crate::metafile::DEFAULT_SIDE)
122}
123
124fn decode_at(bytes: &[u8], side: u32) -> Option<image::RgbaImage> {
126 if bytes.len() > 512 << 20 {
127 return None;
128 }
129 if crate::metafile::is_metafile(bytes) {
130 return crate::metafile::rasterize(bytes, side);
131 }
132 let img = image::load_from_memory(bytes).ok()?;
133 let (w, h) = (img.width(), img.height());
134 if w == 0 || h == 0 || (w as u64) * (h as u64) > 400_000_000 {
135 return None;
136 }
137 Some(img.to_rgba8())
138}
139
140pub fn decode_for(bytes: &Arc<Vec<u8>>, want: f64, adj: &PictureAdjust) -> Option<Arc<Pixmap>> {
142 let addr = Arc::as_ptr(bytes) as usize;
143 if with(|c| c.bad.contains_key(&addr)) {
144 return None;
145 }
146 let bucket = (want.max(16.0).log2().ceil() as u32).min(15);
148 let key = (addr, bucket, adjust_key(adj));
149 if let Some(pm) = with(|c| {
150 c.clock += 1;
151 let clock = c.clock;
152 c.map.get_mut(&key).map(|e| {
153 e.stamp = clock;
154 e.pm.clone()
155 })
156 }) {
157 return Some(pm);
158 }
159 let target = 1u32 << bucket;
160 let Some(mut img) = decode_at(bytes, target) else {
161 with(|c| {
162 c.bad.insert(addr, bytes.clone());
163 });
164 return None;
165 };
166 let (w, h) = img.dimensions();
167 if w.max(h) > target.max(16) && w.max(h) > 4096.min(target * 2) {
168 let k = target as f64 / w.max(h) as f64;
169 let (nw, nh) = (
170 ((w as f64 * k).round() as u32).max(1),
171 ((h as f64 * k).round() as u32).max(1),
172 );
173 img = image::imageops::resize(&img, nw, nh, image::imageops::FilterType::Triangle);
174 }
175 apply_adjust(&mut img, adj);
176 let (w, h) = img.dimensions();
177 let pm = pixmap(&img)?;
178 with(|c| {
179 c.clock += 1;
180 let n = w as usize * h as usize * 4;
181 c.bytes += n;
182 c.map.insert(
183 key,
184 Entry {
185 _bytes: bytes.clone(),
186 pm: pm.clone(),
187 stamp: c.clock,
188 },
189 );
190 while c.bytes > BUDGET && c.map.len() > 1 {
191 let Some(oldest) = c.map.iter().min_by_key(|(_, e)| e.stamp).map(|(k, _)| *k) else {
192 break;
193 };
194 if let Some(e) = c.map.remove(&oldest) {
195 c.bytes = c
196 .bytes
197 .saturating_sub(e.pm.width() as usize * e.pm.height() as usize * 4);
198 }
199 }
200 });
201 Some(pm)
202}
203
204fn pixmap(img: &image::RgbaImage) -> Option<Arc<Pixmap>> {
206 let (w, h) = img.dimensions();
207 if w > u16::MAX as u32 || h > u16::MAX as u32 {
208 return None;
209 }
210 let data: Vec<vello_cpu::color::PremulRgba8> = img
211 .pixels()
212 .map(|p| {
213 let a = p[3] as u16;
214 let m = |c: u8| ((c as u16 * a + 127) / 255) as u8;
215 vello_cpu::color::PremulRgba8 {
216 r: m(p[0]),
217 g: m(p[1]),
218 b: m(p[2]),
219 a: p[3],
220 }
221 })
222 .collect();
223 Some(Arc::new(Pixmap::from_parts(data, w as u16, h as u16)))
224}
225
226fn decode_anim(bytes: &[u8], adj: &PictureAdjust) -> Option<Anim> {
228 use image::{AnimationDecoder, ImageDecoder};
229 let mut dec = image::codecs::gif::GifDecoder::new(std::io::Cursor::new(bytes)).ok()?;
230 dec.set_limits(image::Limits::default()).ok()?;
231 let (w, h) = dec.dimensions();
232 if w == 0 || h == 0 || (w as u64) * (h as u64) > GIF_MAX_CANVAS {
233 return None;
234 }
235 let k = (GIF_SIDE as f64 / w.max(h) as f64).min(1.0);
236 let (nw, nh) = (
237 ((w as f64 * k).round() as u32).max(1),
238 ((h as f64 * k).round() as u32).max(1),
239 );
240 let size = nw as usize * nh as usize * 4;
241 let (mut frames, mut ends, mut t) = (Vec::new(), Vec::new(), 0.0);
242 for f in dec.into_frames() {
243 let Ok(f) = f else { break };
244 if frames.len() >= GIF_MAX_FRAMES || (frames.len() + 1) * size > GIF_BUDGET {
245 log::info!("animated GIF: keeping its first {} frames", frames.len());
246 break;
247 }
248 let (n, d) = f.delay().numer_denom_ms();
249 let ms = if d == 0 { 0.0 } else { n as f64 / d as f64 };
250 t += if ms <= 10.0 { 0.1 } else { ms / 1000.0 };
252 let mut img = f.into_buffer();
253 if img.dimensions() != (nw, nh) {
254 img = image::imageops::resize(&img, nw, nh, image::imageops::FilterType::Triangle);
255 }
256 apply_adjust(&mut img, adj);
257 frames.push(pixmap(&img)?);
258 ends.push(t);
259 }
260 (frames.len() > 1).then_some(Anim { frames, ends })
261}
262
263fn anim(bytes: &Arc<Vec<u8>>, adj: &PictureAdjust) -> Option<Arc<Anim>> {
265 if !bytes.starts_with(b"GIF8") {
266 return None;
267 }
268 let key = (Arc::as_ptr(bytes) as usize, adjust_key(adj));
269 if let Some(a) = with(|c| c.anims.get(&key).map(|e| e.1.clone())) {
270 return a;
271 }
272 let a = decode_anim(bytes, adj).map(Arc::new);
273 with(|c| {
274 let held: usize = c
275 .anims
276 .values()
277 .filter_map(|e| e.1.as_ref())
278 .map(|a| a.bytes())
279 .sum();
280 if held + a.as_ref().map_or(0, |a| a.bytes()) > GIF_CACHE_BUDGET || c.anims.len() >= 64 {
281 c.anims.clear();
282 }
283 c.anims.insert(key, (bytes.clone(), a.clone()));
284 });
285 a
286}
287
288pub fn gif_frame(bytes: &Arc<Vec<u8>>, adj: &PictureAdjust, t: f64) -> Option<(usize, f64)> {
291 anim(bytes, adj).map(|a| a.at(t))
292}
293
294pub(crate) fn gif_pixmap(bytes: &Arc<Vec<u8>>, adj: &PictureAdjust, t: f64) -> Option<Arc<Pixmap>> {
296 let a = anim(bytes, adj)?;
297 a.frames.get(a.at(t).0).cloned()
298}
299
300fn apply_adjust(img: &mut image::RgbaImage, a: &PictureAdjust) {
301 let sat = a.saturation.unwrap_or(1.0);
302 if a.brightness == 0.0
303 && a.contrast == 0.0
304 && sat == 1.0
305 && !a.grayscale
306 && a.clear_color.is_none()
307 && a.duotone.is_none()
308 {
309 return;
310 }
311 let duo = duotone_rgb(a);
312 let b = a.brightness.clamp(-1.0, 1.0) * 255.0;
313 let c = a.contrast.clamp(-1.0, 1.0);
314 let cf = if c >= 0.0 {
315 1.0 / (1.0 - c * 0.99)
316 } else {
317 1.0 + c
318 };
319 for p in img.pixels_mut() {
320 if let Some(cc) = a.clear_color
321 && (p[0] as i32 - cc.r as i32).abs() < 8
322 && (p[1] as i32 - cc.g as i32).abs() < 8
323 && (p[2] as i32 - cc.b as i32).abs() < 8
324 {
325 p[3] = 0;
326 continue;
327 }
328 let mut rgb = [p[0] as f64, p[1] as f64, p[2] as f64];
329 let y = 0.299 * rgb[0] + 0.587 * rgb[1] + 0.114 * rgb[2];
330 let s = if a.grayscale { 0.0 } else { sat };
331 for v in &mut rgb {
332 *v = y + (*v - y) * s;
333 *v = (*v - 128.0) * cf + 128.0 + b;
334 }
335 if let Some((d, l)) = duo {
337 let t = ((0.299 * rgb[0] + 0.587 * rgb[1] + 0.114 * rgb[2]) / 255.0).clamp(0.0, 1.0);
338 rgb = [
339 d.r as f64 + (l.r as f64 - d.r as f64) * t,
340 d.g as f64 + (l.g as f64 - d.g as f64) * t,
341 d.b as f64 + (l.b as f64 - d.b as f64) * t,
342 ];
343 }
344 for (i, v) in rgb.iter().enumerate() {
345 if let Some(ch) = p.0.get_mut(i) {
346 *ch = v.round().clamp(0.0, 255.0) as u8;
347 }
348 }
349 }
350}
351
352#[cfg(test)]
353mod tests {
354 #![allow(clippy::expect_used)]
355
356 use super::*;
357
358 #[test]
359 fn duotone_maps_luminance_between_its_two_colours() {
360 use deckcraft_color::Rgba;
361 use deckcraft_model::style::ColorRef;
362 let mut img = image::RgbaImage::from_fn(3, 1, |x, _| {
363 image::Rgba([[0, 0, 0, 255], [128, 128, 128, 200], [255, 255, 255, 255]][x as usize])
364 });
365 let adj = PictureAdjust {
366 duotone: Some((
367 ColorRef::rgb(Rgba::rgb(100, 40, 0)),
368 ColorRef::rgb(Rgba::rgb(250, 240, 200)),
369 )),
370 ..Default::default()
371 };
372 apply_adjust(&mut img, &adj);
373 assert_eq!(
374 img.get_pixel(0, 0).0,
375 [100, 40, 0, 255],
376 "black takes the dark colour"
377 );
378 assert_eq!(
379 img.get_pixel(2, 0).0,
380 [250, 240, 200, 255],
381 "white takes the light colour"
382 );
383 let mid = img.get_pixel(1, 0).0;
384 assert!(
385 (mid[0] as i32 - 175).abs() <= 1
386 && (mid[1] as i32 - 140).abs() <= 1
387 && (mid[2] as i32 - 100).abs() <= 1,
388 "{mid:?}"
389 );
390 assert_eq!(mid[3], 200, "alpha is kept");
391 let other = PictureAdjust {
393 duotone: Some((
394 ColorRef::rgb(Rgba::rgb(0, 0, 0)),
395 ColorRef::rgb(Rgba::rgb(255, 0, 0)),
396 )),
397 ..Default::default()
398 };
399 assert_ne!(adjust_key(&adj), adjust_key(&other));
400 assert_ne!(adjust_key(&adj), adjust_key(&PictureAdjust::default()));
401 }
402
403 #[test]
404 fn a_gif_of_many_tiny_frames_keeps_only_the_frame_cap() {
405 use image::{Delay, Frame, Rgba, RgbaImage};
406 let frames = (0..GIF_MAX_FRAMES + 200).map(|i| {
407 let c = if i % 2 == 0 {
408 [255, 0, 0, 255]
409 } else {
410 [0, 0, 255, 255]
411 };
412 Frame::from_parts(
413 RgbaImage::from_pixel(1, 1, Rgba(c)),
414 0,
415 0,
416 Delay::from_numer_denom_ms(20, 1),
417 )
418 });
419 let mut out = Vec::new();
420 image::codecs::gif::GifEncoder::new(&mut out)
421 .encode_frames(frames)
422 .expect("gif");
423 let a = decode_anim(&out, &PictureAdjust::default()).expect("animated");
424 assert_eq!(a.frames.len(), GIF_MAX_FRAMES);
425 assert_eq!(a.ends.len(), GIF_MAX_FRAMES);
426 }
427}