Skip to main content

kui_native/
decode.rs

1//! The runner's image decoder (backlog F138): the `image` crate the
2//! wallpaper behind a `Tinted` window already links (`ground.rs`), behind
3//! two functions, so an app that ships a photo or an animated GIF needs no
4//! decoder of its own.
5//!
6//! [`decode_image`] turns PNG, JPEG, WebP or GIF bytes into straight RGBA,
7//! the shape `Core::add_image`, `update_image` and `Launcher::icon` take.
8//! [`decode_animation`] keeps every frame of an animated GIF, PNG (APNG) or
9//! WebP with the time it shows for, and [`Animation::at`] says which frame
10//! a moment of the frame clock shows and when the next is due — what a
11//! view hands to `update_image` and `Ui::request_frame_at`, so a GIF plays
12//! with no thread and no frame owed between its changes.
13
14use std::io::Cursor;
15
16use image::{AnimationDecoder, ImageDecoder, ImageFormat};
17
18/// Decoded pixels: `width` by `height`, four bytes each (RGBA), row by row
19/// from the top left, alpha not premultiplied.
20#[derive(Clone, Debug, PartialEq, Eq)]
21pub struct Pixels {
22    pub width: u32,
23    pub height: u32,
24    pub rgba: Vec<u8>,
25}
26
27/// Every frame of an animated image, each the whole canvas (the
28/// file's own partial frames already composited, as a browser shows
29/// them), with how long each shows.
30#[derive(Clone, Debug, PartialEq)]
31pub struct Animation {
32    pub width: u32,
33    pub height: u32,
34    pub frames: Vec<AnimationFrame>,
35    /// How many times the whole sequence plays; `None` for ever, which is
36    /// what most GIFs say.
37    pub loops: Option<u32>,
38}
39
40/// One frame of an [`Animation`]: `width * height * 4` bytes of RGBA, and
41/// the seconds it shows for.
42#[derive(Clone, Debug, PartialEq)]
43pub struct AnimationFrame {
44    pub rgba: Vec<u8>,
45    pub delay: f64,
46}
47
48/// Which frame of an [`Animation`] a moment shows, from [`Animation::at`].
49#[derive(Clone, Copy, Debug, PartialEq)]
50pub struct Showing {
51    /// The frame's index in [`Animation::frames`].
52    pub index: usize,
53    /// Seconds after the start when the next frame is due; infinity once
54    /// a finite animation has played out, or for a single frame.
55    pub next: f64,
56}
57
58/// The shortest delay a GIF frame is shown for as written: browsers show
59/// a frame that says 0 or 10 ms for 100 ms, and GIFs are made for that.
60const GIF_FLOOR: f64 = 0.011;
61const GIF_SLOW: f64 = 0.1;
62
63fn format_of(bytes: &[u8]) -> Result<ImageFormat, String> {
64    let format = image::guess_format(bytes)
65        .map_err(|_| "not an image kui decodes: PNG, JPEG, WebP or GIF".to_string())?;
66    match format {
67        ImageFormat::Png | ImageFormat::Jpeg | ImageFormat::WebP | ImageFormat::Gif => Ok(format),
68        other => Err(format!(
69            "{other:?} is not a format kui decodes: PNG, JPEG, WebP or GIF"
70        )),
71    }
72}
73
74/// Decodes PNG, JPEG, WebP or GIF bytes to straight RGBA — an animated
75/// file's first frame. A failure names the format it could not read, or
76/// that the bytes are none of the four.
77pub fn decode_image(bytes: &[u8]) -> Result<Pixels, String> {
78    let format = format_of(bytes)?;
79    // The `image` crate's own ceiling holds its decode, which for a WebP
80    // or a JPEG is RGB: the RGBA it is turned into is a third again, past
81    // it (backlog FZ3: 176 bytes of WebP made 576 MiB). The header says
82    // what the RGBA will be, and that is held to the frame's ceiling.
83    let (w, h) = image::ImageReader::with_format(Cursor::new(bytes), format)
84        .into_dimensions()
85        .map_err(|e| format!("decoding a {format:?}: {e}"))?;
86    if frame_bytes(w, h) > MAX_FRAME_BYTES {
87        return Err(too_big(format, w, h));
88    }
89    let img = image::load_from_memory_with_format(bytes, format)
90        .map_err(|e| format!("decoding a {format:?}: {e}"))?
91        .into_rgba8();
92    Ok(Pixels {
93        width: img.width(),
94        height: img.height(),
95        rgba: img.into_raw(),
96    })
97}
98
99/// Decodes every frame of an animated GIF, PNG (APNG) or WebP, each the
100/// whole canvas with its delay; a still image — a JPEG, a PNG with one
101/// frame — is one frame that shows for ever. A GIF frame that asks for
102/// 10 ms or less shows for 100 ms, as browsers show it.
103///
104/// Every frame is kept decoded, so the whole is held to
105/// [`MAX_ANIMATION_BYTES`], and any one buffer to the `image` crate's
106/// own ceiling (512 MiB) — what [`decode_image`] is held to: past either
107/// it is an error, never an allocation of what a file's header claims
108/// (backlog FZ3: a 130-byte GIF asked for 11 GB).
109pub fn decode_animation(bytes: &[u8]) -> Result<Animation, String> {
110    let format = format_of(bytes)?;
111    let err = |e: image::ImageError| format!("decoding a {format:?}: {e}");
112    let (width, height, loops, frames) = match format {
113        ImageFormat::Gif => {
114            let mut d = image::codecs::gif::GifDecoder::new(Cursor::new(bytes)).map_err(err)?;
115            d.set_limits(image::Limits::default()).map_err(err)?;
116            let (w, h) = d.dimensions();
117            let loops = loops_of(d.loop_count());
118            (w, h, loops, d.into_frames())
119        }
120        ImageFormat::Png => {
121            let d = image::codecs::png::PngDecoder::with_limits(
122                Cursor::new(bytes),
123                image::Limits::default(),
124            )
125            .map_err(err)?;
126            if !d.is_apng().map_err(err)? {
127                return still(bytes);
128            }
129            let (w, h) = d.dimensions();
130            let a = d.apng().map_err(err)?;
131            let loops = loops_of(a.loop_count());
132            (w, h, loops, a.into_frames())
133        }
134        ImageFormat::WebP => {
135            let d = image::codecs::webp::WebPDecoder::new(Cursor::new(bytes)).map_err(err)?;
136            if !d.has_animation() {
137                return still(bytes);
138            }
139            let (w, h) = d.dimensions();
140            // The WebP decoder takes no limits: its canvas is held here.
141            if frame_bytes(w, h) > MAX_FRAME_BYTES {
142                return Err(too_big(format, w, h));
143            }
144            let loops = loops_of(d.loop_count());
145            (w, h, loops, d.into_frames())
146        }
147        _ => return still(bytes),
148    };
149    if frame_bytes(width, height) > MAX_FRAME_BYTES {
150        return Err(too_big(format, width, height));
151    }
152    let gif = format == ImageFormat::Gif;
153    let mut out: Vec<AnimationFrame> = Vec::new();
154    for f in frames {
155        let f = f.map_err(err)?;
156        if (out.len() as u64 + 1) * frame_bytes(width, height) > MAX_ANIMATION_BYTES {
157            return Err(format!(
158                "a {format:?} of {width}x{height} with more frames than {} MiB holds",
159                MAX_ANIMATION_BYTES >> 20
160            ));
161        }
162        let (num, den) = f.delay().numer_denom_ms();
163        let mut delay = f64::from(num) / f64::from(den.max(1)) / 1000.0;
164        if gif && delay < GIF_FLOOR {
165            delay = GIF_SLOW;
166        }
167        let (left, top) = (f.left(), f.top());
168        let buf = f.into_buffer();
169        let rgba = if (buf.width(), buf.height(), left, top) == (width, height, 0, 0) {
170            buf.into_raw()
171        } else {
172            // A decoder that hands back the changed rectangle alone: laid
173            // over the frame before it, which is what "not disposed" means.
174            let mut canvas = out
175                .last()
176                .map(|p| p.rgba.clone())
177                .unwrap_or_else(|| vec![0; width as usize * height as usize * 4]);
178            blit(&mut canvas, width, height, &buf, left, top);
179            canvas
180        };
181        out.push(AnimationFrame { rgba, delay });
182    }
183    if out.is_empty() {
184        return Err(format!("a {format:?} with no frames"));
185    }
186    Ok(Animation {
187        width,
188        height,
189        frames: out,
190        loops,
191    })
192}
193
194/// The most every frame of one [`decode_animation`] may hold together:
195/// 1 GiB, some 125 frames at 1080p, or a minute of a 640x480 GIF at 15
196/// fps.
197pub const MAX_ANIMATION_BYTES: u64 = 1 << 30;
198
199/// The most one frame's buffer may hold: the `image` crate's own default
200/// ceiling for an allocation, the one `decode_image` is held to.
201const MAX_FRAME_BYTES: u64 = 512 << 20;
202
203fn frame_bytes(w: u32, h: u32) -> u64 {
204    u64::from(w) * u64::from(h) * 4
205}
206
207fn too_big(format: ImageFormat, w: u32, h: u32) -> String {
208    format!(
209        "a {format:?} of {w}x{h} is more than one frame may hold ({} MiB)",
210        MAX_FRAME_BYTES >> 20
211    )
212}
213
214fn still(bytes: &[u8]) -> Result<Animation, String> {
215    let p = decode_image(bytes)?;
216    Ok(Animation {
217        width: p.width,
218        height: p.height,
219        frames: vec![AnimationFrame {
220            rgba: p.rgba,
221            delay: f64::INFINITY,
222        }],
223        loops: Some(1),
224    })
225}
226
227fn loops_of(count: image::metadata::LoopCount) -> Option<u32> {
228    match count {
229        image::metadata::LoopCount::Infinite => None,
230        image::metadata::LoopCount::Finite(n) => Some(n.get()),
231    }
232}
233
234fn blit(canvas: &mut [u8], w: u32, h: u32, src: &image::RgbaImage, left: u32, top: u32) {
235    for (y, row) in src.rows().enumerate() {
236        let cy = top as usize + y;
237        if cy >= h as usize {
238            break;
239        }
240        for (x, px) in row.enumerate() {
241            let cx = left as usize + x;
242            if cx >= w as usize {
243                break;
244            }
245            if px.0[3] != 0 {
246                let at = (cy * w as usize + cx) * 4;
247                canvas[at..at + 4].copy_from_slice(&px.0);
248            }
249        }
250    }
251}
252
253impl Animation {
254    /// The seconds one pass through every frame takes; infinity when a
255    /// frame shows for ever.
256    pub fn duration(&self) -> f64 {
257        self.frames.iter().map(|f| f.delay).sum()
258    }
259
260    /// The frame `elapsed` seconds after the animation started shows, and
261    /// when the next is due, in the same seconds: with the frame clock,
262    /// `at(ui.now() - started)`, then `update_image` when the index moved
263    /// and `request_frame_at(started + next)`. Before the start is the
264    /// first frame; after a finite animation has played out, the last,
265    /// with nothing due.
266    pub fn at(&self, elapsed: f64) -> Showing {
267        let last = self.frames.len().saturating_sub(1);
268        let total = self.duration();
269        if self.frames.len() < 2 || !total.is_finite() || total <= 0.0 {
270            return Showing {
271                index: 0,
272                next: f64::INFINITY,
273            };
274        }
275        let elapsed = elapsed.max(0.0);
276        let pass = (elapsed / total).floor();
277        if let Some(n) = self.loops
278            && pass >= f64::from(n)
279        {
280            return Showing {
281                index: last,
282                next: f64::INFINITY,
283            };
284        }
285        let mut t = pass * total;
286        for (i, f) in self.frames.iter().enumerate() {
287            t += f.delay;
288            if elapsed < t {
289                return Showing { index: i, next: t };
290            }
291        }
292        Showing {
293            index: last,
294            next: (pass + 1.0) * total + self.frames[0].delay,
295        }
296    }
297}
298
299#[cfg(test)]
300mod tests {
301    use super::*;
302    use gif::Repeat;
303    use image::{Rgba, RgbaImage};
304
305    /// A 4x2 GIF of three frames, red, green, then a 2x1 blue patch over
306    /// the green at (1, 1), at 50, 100 and 0 ms; `repeat` is its loop count.
307    fn gif(repeat: gif::Repeat) -> Vec<u8> {
308        let mut out = Vec::new();
309        {
310            let mut enc = gif::Encoder::new(&mut out, 4, 2, &[]).unwrap();
311            enc.set_repeat(repeat).unwrap();
312            let mut frame = |w: u16, h: u16, left: u16, top: u16, c: [u8; 4], cs: u16| {
313                let mut px: Vec<u8> = c.repeat(usize::from(w * h));
314                let mut f = gif::Frame::from_rgba(w, h, &mut px);
315                (f.left, f.top, f.delay) = (left, top, cs);
316                enc.write_frame(&f).unwrap();
317            };
318            frame(4, 2, 0, 0, [255, 0, 0, 255], 5);
319            frame(4, 2, 0, 0, [0, 255, 0, 255], 10);
320            frame(2, 1, 1, 1, [0, 0, 255, 255], 0);
321        }
322        out
323    }
324
325    fn png() -> Vec<u8> {
326        let img = RgbaImage::from_fn(3, 2, |x, y| Rgba([x as u8 * 80, y as u8 * 100, 7, 200]));
327        let mut out = Cursor::new(Vec::new());
328        img.write_to(&mut out, ImageFormat::Png).unwrap();
329        out.into_inner()
330    }
331
332    /// A header's canvas is not an allocation (backlog FZ3, from the first
333    /// fuzz round): fifteen bytes of GIF claiming 52428 by 17356 asked
334    /// `decode_animation` for 3.6 GB, which no limit stood between.
335    #[test]
336    fn a_canvas_past_the_ceiling_is_refused() {
337        let gif = b"GIF89a\xcc\xcc\xcc\x43\x3c\x00\x3c\x00\x3c";
338        assert!(decode_animation(gif).is_err());
339        assert!(decode_image(gif).is_err());
340        assert_eq!(frame_bytes(52428, 17356), 3_639_761_472);
341        assert!(frame_bytes(52428, 17356) > MAX_FRAME_BYTES);
342    }
343
344    #[test]
345    fn a_png_decodes_to_straight_rgba() {
346        let p = decode_image(&png()).unwrap();
347        assert_eq!((p.width, p.height), (3, 2));
348        assert_eq!(p.rgba.len(), 3 * 2 * 4);
349        assert_eq!(&p.rgba[4..8], &[80, 0, 7, 200], "not premultiplied");
350    }
351
352    #[test]
353    fn a_gif_decodes_to_its_first_frame_and_to_every_frame() {
354        let bytes = gif(Repeat::Infinite);
355        let first = decode_image(&bytes).unwrap();
356        assert_eq!((first.width, first.height), (4, 2));
357        assert_eq!(&first.rgba[..4], &[255, 0, 0, 255]);
358        let a = decode_animation(&bytes).unwrap();
359        assert_eq!(a.frames.len(), 3);
360        assert_eq!(a.loops, None);
361        let delays: Vec<f64> = a.frames.iter().map(|f| f.delay).collect();
362        assert_eq!(delays, vec![0.05, 0.1, 0.1], "a 0 ms frame shows for 100");
363        for f in &a.frames {
364            assert_eq!(f.rgba.len(), 4 * 2 * 4, "each frame the whole canvas");
365        }
366        let third = &a.frames[2].rgba;
367        assert_eq!(
368            &third[..4],
369            &[0, 255, 0, 255],
370            "the green kept under the patch"
371        );
372        assert_eq!(&third[(4 + 1) * 4..(4 + 1) * 4 + 4], &[0, 0, 255, 255]);
373    }
374
375    #[test]
376    fn at_walks_the_frames_and_loops() {
377        let a = decode_animation(&gif(Repeat::Infinite)).unwrap();
378        let near = |s: Showing, i: usize, next: f64| s.index == i && (s.next - next).abs() < 1e-9;
379        assert!(near(a.at(0.0), 0, 0.05));
380        assert!(near(a.at(0.07), 1, 0.15));
381        assert!(near(a.at(0.2), 2, 0.25));
382        assert!(near(a.at(0.26), 0, 0.30), "{:?}", a.at(0.26));
383        assert!(near(a.at(-1.0), 0, 0.05), "before the start");
384    }
385
386    #[test]
387    fn a_finite_gif_rests_on_its_last_frame() {
388        let a = decode_animation(&gif(Repeat::Finite(1))).unwrap();
389        assert_eq!(a.loops, Some(1));
390        assert_eq!(a.at(0.1).index, 1);
391        let end = a.at(0.3);
392        assert_eq!(end.index, 2);
393        assert!(end.next.is_infinite());
394    }
395
396    #[test]
397    fn a_still_is_one_frame_for_ever() {
398        let a = decode_animation(&png()).unwrap();
399        assert_eq!(a.frames.len(), 1);
400        let s = a.at(100.0);
401        assert_eq!(s.index, 0);
402        assert!(s.next.is_infinite());
403    }
404
405    #[test]
406    fn what_is_not_an_image_is_named() {
407        let e = decode_image(b"hello, world").unwrap_err();
408        assert!(e.contains("PNG, JPEG, WebP or GIF"), "{e}");
409        let mut cut = png();
410        cut.truncate(40);
411        let e = decode_image(&cut).unwrap_err();
412        assert!(e.contains("decoding a Png"), "{e}");
413    }
414}