Skip to main content

alpha2_extensions/animated/
mod.rs

1//! GIF, animated WebP, and APNG: every frame, with its own duration, built
2//! as its format defines.
3//!
4//! Probing reads only the frames' headers: the canvas, each frame's
5//! rectangle and delay. A file is refused there, before anything is
6//! allocated or decoded, when its canvas is larger than a provider opens,
7//! it has more than [`MAX_ANIMATION_FRAMES`] frames, or its frames together
8//! cover more than [`MAX_ANIMATION_PIXELS`]: the most one pass through it
9//! decodes and composites, since drawing a frame costs its own rectangle.
10//! Opening also decodes the file once, to check it is whole, and keeps what
11//! fits in the frame budget. The frames used last stay; a later request
12//! outside them decodes forward from the decoder's position, restarting the
13//! file for an earlier frame, and keeps the frames just before the one asked
14//! for as it goes, so playing backward restarts once per budget of frames.
15//!
16//! More readers of an open animation (two clips showing it at unrelated
17//! times) are made from it without reading or decoding the file again: each
18//! has a decoder of its own, so one never restarts another's, and they
19//! share the file, the check, and the kept frames, one frame budget for all
20//! of them.
21
22mod compositor;
23
24use std::collections::BTreeMap;
25use std::io::Cursor;
26use std::sync::{Arc, Mutex, MutexGuard, PoisonError};
27
28use alpha2_time::{MediaTime, TICKS_PER_SECOND};
29
30use alpha2_model::ProviderSource;
31
32use crate::api::{
33    Diagnostic, ExtensionError, Format, FramePixels, MAX_NORMALIZED_DELAY_MS, NORMALIZED_DELAY_MS,
34    ProbeReport, ResourceResolver, Timing, VisualFrame, VisualProvider, VisualSource, premultiply,
35};
36use crate::budget::{
37    DEFAULT_FRAME_BUDGET, MAX_ANIMATION_FRAMES, MAX_ANIMATION_PIXELS, MAX_CANVAS_PIXELS,
38    check_canvas,
39};
40use compositor::{Blend, Compositor, Dispose, PartialFrame};
41
42/// Reads GIF, animated WebP, and APNG. A still file of those formats is not
43/// this provider's: the host's image decoders read it.
44pub struct AnimatedImageProvider;
45
46impl VisualProvider for AnimatedImageProvider {
47    fn name(&self) -> &'static str {
48        "animated-image"
49    }
50
51    fn probe(&self, bytes: &[u8]) -> Option<Result<ProbeReport, ExtensionError>> {
52        let format = sniff(bytes)?;
53        Some(index(format, bytes).map(|index| index.report(format)))
54            .filter(|report| !matches!(report, Ok(report) if report.timing.frame_count() < 2))
55    }
56
57    fn open(
58        &self,
59        bytes: Arc<[u8]>,
60        _source: &ProviderSource,
61        _resolver: &dyn ResourceResolver,
62    ) -> Result<Box<dyn VisualSource>, ExtensionError> {
63        Ok(Box::new(AnimatedSource::open(bytes, DEFAULT_FRAME_BUDGET)?))
64    }
65}
66
67/// The format, from the bytes' signature (an APNG only when it declares an
68/// animation, a WebP only when its header does).
69fn sniff(bytes: &[u8]) -> Option<Format> {
70    if bytes.starts_with(b"GIF87a") || bytes.starts_with(b"GIF89a") {
71        return Some(Format::Gif);
72    }
73    if bytes.starts_with(b"\x89PNG\r\n\x1a\n") {
74        return png_is_animated(bytes).then_some(Format::Apng);
75    }
76    if bytes.len() >= 21 && &bytes[..4] == b"RIFF" && &bytes[8..12] == b"WEBP" {
77        // An extended header whose flags byte sets the animation bit.
78        return (&bytes[12..16] == b"VP8X" && bytes[20] & 0x02 != 0).then_some(Format::Webp);
79    }
80    None
81}
82
83/// Whether a PNG's chunks before its image data include an animation control.
84/// A chunk that would end past the end of the address space ends the walk:
85/// in wasm32 a length near 2^32 is most of it.
86fn png_is_animated(bytes: &[u8]) -> bool {
87    let mut at = 8_usize;
88    while bytes.len().saturating_sub(at) >= 8 {
89        let length = u32::from_be_bytes([bytes[at], bytes[at + 1], bytes[at + 2], bytes[at + 3]]);
90        let kind = &bytes[at + 4..at + 8];
91        if kind == b"acTL" {
92            return true;
93        }
94        if kind == b"IDAT" {
95            return false;
96        }
97        let Some(next) = usize::try_from(length)
98            .ok()
99            .and_then(|length| at.checked_add(12)?.checked_add(length))
100        else {
101            return false;
102        };
103        at = next;
104    }
105    false
106}
107
108/// What an animation's headers declare: its canvas, and each frame's
109/// pixels and delay.
110struct Index {
111    width: u32,
112    height: u32,
113    loop_count: Option<u32>,
114    /// Each frame's declared delay.
115    frames: Vec<Delay>,
116    /// The pixels of every frame's rectangle together.
117    pixels: u64,
118}
119
120impl Index {
121    /// A canvas checked to be one a provider opens.
122    fn new(width: u32, height: u32) -> Result<Self, ExtensionError> {
123        check_canvas(width, height)?;
124        Ok(Self {
125            width,
126            height,
127            loop_count: None,
128            frames: Vec::new(),
129            pixels: 0,
130        })
131    }
132
133    /// Adds a frame of `width` by `height` pixels shown for `delay`,
134    /// refusing it past the frame or pixel budget.
135    fn push(&mut self, width: u32, height: u32, delay: Delay) -> Result<(), ExtensionError> {
136        if self.frames.len() == MAX_ANIMATION_FRAMES {
137            return Err(ExtensionError::Budget(format!(
138                "more than {MAX_ANIMATION_FRAMES} frames"
139            )));
140        }
141        let pixels = u64::from(width) * u64::from(height);
142        if pixels > MAX_CANVAS_PIXELS {
143            return Err(ExtensionError::Budget(format!(
144                "a {width}x{height} frame is larger than {MAX_CANVAS_PIXELS} pixels"
145            )));
146        }
147        self.pixels += pixels;
148        if self.pixels > MAX_ANIMATION_PIXELS {
149            return Err(ExtensionError::Budget(format!(
150                "the frames together cover more than {MAX_ANIMATION_PIXELS} pixels"
151            )));
152        }
153        self.frames.push(delay);
154        Ok(())
155    }
156
157    fn report(&self, format: Format) -> ProbeReport {
158        let mut diagnostics = Vec::new();
159        let durations = self
160            .frames
161            .iter()
162            .enumerate()
163            .map(|(frame, delay)| {
164                if delay.ticks <= i64::from(MAX_NORMALIZED_DELAY_MS) * TICKS_PER_MS {
165                    diagnostics.push(Diagnostic::NormalizedDelay {
166                        frame,
167                        declared_ms: delay.ms,
168                    });
169                    i64::from(NORMALIZED_DELAY_MS) * TICKS_PER_MS
170                } else {
171                    delay.ticks
172                }
173            })
174            .collect();
175        ProbeReport {
176            format,
177            width: self.width,
178            height: self.height,
179            timing: Timing::Frames { durations },
180            loop_count: self.loop_count,
181            items: Vec::new(),
182            item: None,
183            color: None,
184            diagnostics,
185        }
186    }
187}
188
189/// Reads an animation's headers, without decoding or compositing a frame.
190fn index(format: Format, bytes: &[u8]) -> Result<Index, ExtensionError> {
191    let index = match format {
192        Format::Gif => gif_index(bytes)?,
193        Format::Apng => apng_index(bytes)?,
194        Format::Webp => WebpLayout::read(bytes)?.index()?,
195        other => {
196            return Err(ExtensionError::Unsupported(format!(
197                "{other:?} is not an animation"
198            )));
199        }
200    };
201    if index.frames.is_empty() {
202        return Err(ExtensionError::Malformed(
203            "the animation has no frames".into(),
204        ));
205    }
206    Ok(index)
207}
208
209fn gif_index(bytes: &[u8]) -> Result<Index, ExtensionError> {
210    let mut options = gif_options();
211    options.skip_frame_decoding(true);
212    let mut decoder = options.read_info(Cursor::new(bytes)).map_err(malformed)?;
213    let mut index = Index::new(u32::from(decoder.width()), u32::from(decoder.height()))?;
214    while let Some(frame) = decoder.next_frame_info().map_err(malformed)? {
215        let (width, height) = (u32::from(frame.width), u32::from(frame.height));
216        // Hundredths of a second.
217        let delay = Delay {
218            ticks: i64::from(frame.delay) * 10 * TICKS_PER_MS,
219            ms: u32::from(frame.delay) * 10,
220        };
221        index.push(width, height, delay)?;
222    }
223    // The loop extension counts repetitions after the first play (0 is for
224    // ever); without one, which the decoder reads as `Finite(0)`, the GIF
225    // plays once.
226    index.loop_count = Some(match decoder.repeat() {
227        gif::Repeat::Infinite => 0,
228        gif::Repeat::Finite(count) => u32::from(count) + 1,
229    });
230    Ok(index)
231}
232
233fn apng_index(bytes: &[u8]) -> Result<Index, ExtensionError> {
234    let mut reader = png::Decoder::new(Cursor::new(bytes))
235        .read_info()
236        .map_err(malformed)?;
237    let info = reader.info();
238    let mut index = Index::new(info.width, info.height)?;
239    index.loop_count = info.animation_control.map(|control| control.num_plays);
240    // The default image is a frame when its frame control comes before it.
241    if let Some(control) = info.frame_control {
242        index.push(control.width, control.height, apng_delay(&control))?;
243    }
244    loop {
245        // Skips the image data before it unread.
246        let control = match reader.next_frame_info() {
247            Ok(control) => *control,
248            Err(png::DecodingError::Parameter(_)) => break,
249            Err(error) => return Err(malformed(error)),
250        };
251        index.push(control.width, control.height, apng_delay(&control))?;
252    }
253    Ok(index)
254}
255
256/// A frame control's delay, `delay_num`/`delay_den` seconds; a zero
257/// denominator means hundredths. A denominator that divides 120 000 (24,
258/// 25, 30, 60, 1000, 30 000, ...) is exact in ticks; another rounds to the
259/// nearest tick, under 10 microseconds a frame.
260fn apng_delay(control: &png::FrameControl) -> Delay {
261    let den = if control.delay_den == 0 {
262        100
263    } else {
264        i64::from(control.delay_den)
265    };
266    let num = i64::from(control.delay_num);
267    let rounded = |per_second: i64| (num * per_second + den / 2) / den;
268    Delay {
269        ticks: rounded(TICKS_PER_SECOND),
270        ms: u32::try_from(rounded(1_000)).unwrap_or(u32::MAX),
271    }
272}
273
274/// How long a frame asks to be shown.
275#[derive(Clone, Copy)]
276struct Delay {
277    /// Exactly, in ticks, or to the nearest tick.
278    ticks: i64,
279    /// To the nearest millisecond, as a diagnostic reports it.
280    ms: u32,
281}
282
283/// Ticks in a millisecond.
284const TICKS_PER_MS: i64 = TICKS_PER_SECOND / 1_000;
285
286/// Premultiplied canvases kept between requests, within a byte budget: the
287/// ones used last stay.
288struct Kept {
289    /// Each frame kept, and when it was last used.
290    frames: BTreeMap<usize, (Arc<[u8]>, u64)>,
291    /// The frames kept, by when each was last used.
292    by_use: BTreeMap<u64, usize>,
293    clock: u64,
294    frame_bytes: usize,
295    budget: usize,
296}
297
298impl Kept {
299    fn new(frame_bytes: usize, budget: usize) -> Self {
300        Self {
301            frames: BTreeMap::new(),
302            by_use: BTreeMap::new(),
303            clock: 0,
304            frame_bytes,
305            budget,
306        }
307    }
308
309    /// How many frames fit the budget.
310    fn capacity(&self) -> usize {
311        self.budget / self.frame_bytes.max(1)
312    }
313
314    fn bytes(&self) -> usize {
315        self.frames.len() * self.frame_bytes
316    }
317
318    fn contains(&self, index: usize) -> bool {
319        self.frames.contains_key(&index)
320    }
321
322    /// A kept frame, now the one used last.
323    fn get(&mut self, index: usize) -> Option<Arc<[u8]>> {
324        self.clock += 1;
325        let (canvas, used) = self.frames.get_mut(&index)?;
326        self.by_use.remove(used);
327        *used = self.clock;
328        self.by_use.insert(self.clock, index);
329        Some(Arc::clone(canvas))
330    }
331
332    /// Keeps a frame as the one used last, letting go of the one used least
333    /// recently when the budget is full.
334    fn insert(&mut self, index: usize, canvas: Arc<[u8]>) {
335        if self.capacity() == 0 {
336            return;
337        }
338        if let Some((_, used)) = self.frames.remove(&index) {
339            self.by_use.remove(&used);
340        }
341        while self.frames.len() >= self.capacity() {
342            let Some((_, oldest)) = self.by_use.pop_first() else {
343                break;
344            };
345            self.frames.remove(&oldest);
346        }
347        self.clock += 1;
348        self.frames.insert(index, (canvas, self.clock));
349        self.by_use.insert(self.clock, index);
350    }
351}
352
353/// Decodes every frame of an indexed animation once, checking each is
354/// there, and keeps the premultiplied canvases that fit the frame budget.
355fn decode_whole(
356    format: Format,
357    bytes: &Arc<[u8]>,
358    index: &Index,
359    budget: usize,
360) -> Result<Kept, ExtensionError> {
361    let mut cursor = open_cursor(format, bytes)?;
362    let mut kept = Kept::new(index.width as usize * index.height as usize * 4, budget);
363    for at in 0..index.frames.len() {
364        if !cursor.advance()? {
365            return Err(ExtensionError::Malformed(
366                "the animation ended before a frame it listed".into(),
367            ));
368        }
369        crate::progress::progressed();
370        if at < kept.capacity() {
371            kept.insert(at, premultiplied(cursor.canvas()));
372        }
373    }
374    Ok(kept)
375}
376
377/// What the readers of one open animation share: its file, what its headers
378/// declare, and the frames kept between requests, within one frame budget.
379struct Animation {
380    format: Format,
381    bytes: Arc<[u8]>,
382    report: ProbeReport,
383    /// Kept frames, used by every reader; its lock is held only to look a
384    /// frame up or keep one, never while decoding.
385    frames: Mutex<Kept>,
386}
387
388impl Animation {
389    /// The kept frames. A reader that panicked holding them may have left
390    /// them half changed: every reader then fails, as the panicked one does.
391    fn kept(&self) -> Result<MutexGuard<'_, Kept>, ExtensionError> {
392        self.frames.lock().map_err(|_| {
393            ExtensionError::Malformed(
394                "another reader of this animation failed while keeping its frames".into(),
395            )
396        })
397    }
398
399    /// What the kept frames hold now, even when a reader panicked.
400    fn kept_bytes(&self) -> usize {
401        self.frames
402            .lock()
403            .unwrap_or_else(PoisonError::into_inner)
404            .bytes()
405    }
406}
407
408/// A reader of an open animation: the animation, shared with the other
409/// readers made from it, and this reader's own decoder.
410struct AnimatedSource {
411    shared: Arc<Animation>,
412    /// A decoder positioned at the frame after the last it produced.
413    cursor: Option<(Box<dyn FrameCursor>, usize)>,
414}
415
416impl AnimatedSource {
417    /// Opens an animation, keeping at most `budget` bytes of frames.
418    fn open(bytes: Arc<[u8]>, budget: usize) -> Result<Self, ExtensionError> {
419        let format = sniff(&bytes)
420            .ok_or_else(|| ExtensionError::Mismatch("not an animated GIF, WebP, or APNG".into()))?;
421        let index = index(format, &bytes)?;
422        if index.frames.len() < 2 {
423            return Err(ExtensionError::Mismatch(
424                "a still image, which the host's image decoders read".into(),
425            ));
426        }
427        let frames = decode_whole(format, &bytes, &index, budget)?;
428        Ok(Self {
429            shared: Arc::new(Animation {
430                format,
431                bytes,
432                report: index.report(format),
433                frames: Mutex::new(frames),
434            }),
435            cursor: None,
436        })
437    }
438
439    /// A frame's premultiplied canvas. One not kept is decoded forward from
440    /// this reader's decoder position, or from the file's start when it is
441    /// past the frame; the frames just before it that the budget holds are
442    /// kept on the way, so a request for an earlier frame (reverse or
443    /// ping-pong playback, two clips of one source) finds it without a
444    /// restart.
445    fn frame(&mut self, index: usize) -> Result<Arc<[u8]>, ExtensionError> {
446        let shared = Arc::clone(&self.shared);
447        let capacity = {
448            let mut kept = shared.kept()?;
449            if let Some(frame) = kept.get(index) {
450                return Ok(frame);
451            }
452            kept.capacity()
453        };
454        let restart = self.cursor.as_ref().is_none_or(|(_, next)| *next > index);
455        if restart {
456            self.cursor = Some((open_cursor(shared.format, &shared.bytes)?, 0));
457        }
458        let window = (index + 1).saturating_sub(capacity);
459        loop {
460            let (cursor, next) = self.cursor.as_mut().ok_or_else(|| {
461                ExtensionError::Malformed("the animation's decoder is gone".into())
462            })?;
463            if !cursor.advance()? {
464                return Err(ExtensionError::Malformed(
465                    "the animation ended before a frame it listed".into(),
466                ));
467            }
468            crate::progress::progressed();
469            let at = *next;
470            *next += 1;
471            // Frames on the way are drawn; only those the budget keeps are copied.
472            if at != index && (at < window || shared.kept()?.contains(at)) {
473                continue;
474            }
475            let canvas = premultiplied(cursor.canvas());
476            shared.kept()?.insert(at, Arc::clone(&canvas));
477            if at == index {
478                return Ok(canvas);
479            }
480        }
481    }
482}
483
484impl VisualSource for AnimatedSource {
485    fn report(&self) -> &ProbeReport {
486        &self.shared.report
487    }
488
489    fn render(&mut self, time: MediaTime, _fit: (u32, u32)) -> Result<VisualFrame, ExtensionError> {
490        let (index, start, end) = self.shared.report.timing.frame_at(time);
491        let pixels = self.frame(index)?;
492        Ok(VisualFrame {
493            width: self.shared.report.width,
494            height: self.shared.report.height,
495            pixels: FramePixels::Rgba8(pixels),
496            index,
497            start,
498            end,
499            diagnostics: Vec::new(),
500        })
501    }
502
503    fn resident_bytes(&self) -> usize {
504        let canvas = self.shared.report.width as usize * self.shared.report.height as usize * 4;
505        // What the readers share, and while this reader's decoder is positioned its canvas (the
506        // decoder reads the source's own file, so it adds no copy).
507        self.shared_bytes() + self.cursor.as_ref().map_or(0, |_| canvas)
508    }
509
510    /// A reader with no decoder yet: it decodes nothing until a frame it is
511    /// asked for is not kept, and then only from the file's start.
512    fn reader(&self) -> Option<Box<dyn VisualSource>> {
513        Some(Box::new(Self {
514            shared: Arc::clone(&self.shared),
515            cursor: None,
516        }))
517    }
518
519    /// The file and the kept frames.
520    fn shared_bytes(&self) -> usize {
521        self.shared.bytes.len() + self.shared.kept_bytes()
522    }
523}
524
525/// A straight canvas, premultiplied into a buffer of its own that frames share.
526fn premultiplied(canvas: &[u8]) -> Arc<[u8]> {
527    let mut shared = Arc::<[u8]>::from(canvas);
528    if let Some(pixels) = Arc::get_mut(&mut shared) {
529        premultiply(pixels);
530    }
531    shared
532}
533
534/// Draws an animation's frames in order onto one canvas.
535trait FrameCursor: Send {
536    /// Draws the next frame; `false` past the last.
537    fn advance(&mut self) -> Result<bool, ExtensionError>;
538    /// The whole canvas in straight RGBA8, as the last frame drawn left it.
539    fn canvas(&self) -> &[u8];
540}
541
542/// A decoder at an animation's first frame, reading the file it is given: a restart copies
543/// nothing.
544fn open_cursor(format: Format, bytes: &Arc<[u8]>) -> Result<Box<dyn FrameCursor>, ExtensionError> {
545    let bytes = Arc::clone(bytes);
546    match format {
547        Format::Gif => Ok(Box::new(GifCursor::new(bytes)?)),
548        Format::Apng => Ok(Box::new(ApngCursor::new(bytes)?)),
549        Format::Webp => Ok(Box::new(WebpCursor::new(bytes)?)),
550        other => Err(ExtensionError::Unsupported(format!(
551            "{other:?} is not an animation"
552        ))),
553    }
554}
555
556/// How GIFs are read: to RGBA, a frame's buffer no larger than the largest
557/// canvas.
558fn gif_options() -> gif::DecodeOptions {
559    let mut options = gif::DecodeOptions::new();
560    options.set_color_output(gif::ColorOutput::RGBA);
561    options.set_memory_limit(gif::MemoryLimit::Bytes(
562        std::num::NonZeroU64::new(MAX_CANVAS_PIXELS * 4).unwrap_or(std::num::NonZeroU64::MIN),
563    ));
564    options
565}
566
567fn malformed(error: impl std::fmt::Display) -> ExtensionError {
568    ExtensionError::Malformed(error.to_string())
569}
570
571struct GifCursor {
572    decoder: gif::Decoder<Cursor<Arc<[u8]>>>,
573    compositor: Compositor,
574}
575
576impl GifCursor {
577    fn new(bytes: Arc<[u8]>) -> Result<Self, ExtensionError> {
578        let decoder = gif_options()
579            .read_info(Cursor::new(bytes))
580            .map_err(malformed)?;
581        let compositor = Compositor::new(u32::from(decoder.width()), u32::from(decoder.height()))?;
582        Ok(Self {
583            decoder,
584            compositor,
585        })
586    }
587}
588
589impl FrameCursor for GifCursor {
590    fn canvas(&self) -> &[u8] {
591        self.compositor.canvas()
592    }
593
594    fn advance(&mut self) -> Result<bool, ExtensionError> {
595        let Some(frame) = self.decoder.read_next_frame().map_err(malformed)? else {
596            return Ok(false);
597        };
598        let partial = PartialFrame {
599            x: u32::from(frame.left),
600            y: u32::from(frame.top),
601            width: u32::from(frame.width),
602            height: u32::from(frame.height),
603            pixels: frame.buffer.to_vec(),
604            // A transparent index leaves the canvas as it is: GIF draws over.
605            blend: Blend::Over,
606            dispose: match frame.dispose {
607                gif::DisposalMethod::Background => Dispose::Background,
608                gif::DisposalMethod::Previous => Dispose::Previous,
609                _ => Dispose::None,
610            },
611        };
612        self.compositor.draw(&partial)?;
613        Ok(true)
614    }
615}
616
617struct ApngCursor {
618    reader: png::Reader<Cursor<Arc<[u8]>>>,
619    compositor: Compositor,
620    /// Whether the default image is not a frame, and has not been skipped.
621    skip_default: bool,
622    /// Whether the next frame drawn is the animation's first.
623    first: bool,
624    buffer: Vec<u8>,
625}
626
627impl ApngCursor {
628    fn new(bytes: Arc<[u8]>) -> Result<Self, ExtensionError> {
629        let mut decoder = png::Decoder::new(Cursor::new(bytes));
630        decoder.set_transformations(
631            png::Transformations::EXPAND
632                | png::Transformations::STRIP_16
633                | png::Transformations::ALPHA,
634        );
635        let reader = decoder.read_info().map_err(malformed)?;
636        let info = reader.info();
637        let compositor = Compositor::new(info.width, info.height)?;
638        let skip_default = info.frame_control.is_none();
639        let size = reader
640            .output_buffer_size()
641            .ok_or_else(|| ExtensionError::Budget("the PNG is too large".into()))?;
642        Ok(Self {
643            reader,
644            compositor,
645            skip_default,
646            first: true,
647            buffer: vec![0; size],
648        })
649    }
650}
651
652impl FrameCursor for ApngCursor {
653    fn canvas(&self) -> &[u8] {
654        self.compositor.canvas()
655    }
656
657    fn advance(&mut self) -> Result<bool, ExtensionError> {
658        loop {
659            let output = match self.reader.next_frame(&mut self.buffer) {
660                Ok(output) => output,
661                Err(png::DecodingError::Parameter(_)) => return Ok(false),
662                Err(error) => return Err(malformed(error)),
663            };
664            if std::mem::take(&mut self.skip_default) {
665                continue;
666            }
667            let control = self.reader.info().frame_control.ok_or_else(|| {
668                ExtensionError::Malformed("an APNG frame has no frame control".into())
669            })?;
670            let pixels = crate::raster::to_rgba8(
671                &self.buffer[..output.buffer_size()],
672                output.color_type,
673                output.width,
674                output.height,
675                output.line_size,
676            )?;
677            let first = std::mem::take(&mut self.first);
678            let partial = PartialFrame {
679                x: control.x_offset,
680                y: control.y_offset,
681                width: control.width,
682                height: control.height,
683                pixels,
684                blend: match control.blend_op {
685                    png::BlendOp::Over => Blend::Over,
686                    png::BlendOp::Source => Blend::Source,
687                },
688                dispose: match control.dispose_op {
689                    png::DisposeOp::None => Dispose::None,
690                    png::DisposeOp::Background => Dispose::Background,
691                    // The first frame has nothing before it to return to.
692                    png::DisposeOp::Previous if first => Dispose::Background,
693                    png::DisposeOp::Previous => Dispose::Previous,
694                },
695            };
696            self.compositor.draw(&partial)?;
697            return Ok(true);
698        }
699    }
700}
701
702/// An animated WebP, composited here from its frames as libwebp (and so
703/// browsers) composite it; `image-webp` decodes each frame's picture.
704struct WebpCursor {
705    bytes: Arc<[u8]>,
706    frames: Vec<WebpFrame>,
707    next: usize,
708    compositor: Compositor,
709}
710
711/// An animated WebP's extended header, animation header, and frame headers.
712struct WebpLayout {
713    width: u32,
714    height: u32,
715    /// The animation header's loop count: plays, 0 for ever.
716    loop_count: Option<u32>,
717    frames: Vec<WebpFrame>,
718}
719
720/// One `ANMF` chunk: where its picture goes and how, and its chunks' bytes.
721struct WebpFrame {
722    x: u32,
723    y: u32,
724    width: u32,
725    height: u32,
726    duration_ms: u32,
727    blend: Blend,
728    dispose: Dispose,
729    data: std::ops::Range<usize>,
730}
731
732/// The chunks of a RIFF body: kind and payload range.
733fn riff_chunks(bytes: &[u8], mut at: usize, end: usize) -> Vec<([u8; 4], std::ops::Range<usize>)> {
734    let mut chunks = Vec::new();
735    while at + 8 <= end {
736        let kind = [bytes[at], bytes[at + 1], bytes[at + 2], bytes[at + 3]];
737        let size = u32::from_le_bytes([bytes[at + 4], bytes[at + 5], bytes[at + 6], bytes[at + 7]])
738            as usize;
739        let start = at + 8;
740        let Some(stop) = start.checked_add(size).filter(|stop| *stop <= end) else {
741            break;
742        };
743        chunks.push((kind, start..stop));
744        at = stop + (size & 1);
745    }
746    chunks
747}
748
749fn le24(bytes: &[u8]) -> u32 {
750    u32::from(bytes[0]) | u32::from(bytes[1]) << 8 | u32::from(bytes[2]) << 16
751}
752
753impl WebpLayout {
754    fn read(bytes: &[u8]) -> Result<Self, ExtensionError> {
755        let end = bytes.len();
756        let chunks = riff_chunks(bytes, 12, end);
757        let header = chunks
758            .iter()
759            .find(|(kind, _)| kind == b"VP8X")
760            .filter(|(_, range)| range.len() >= 10)
761            .ok_or_else(|| ExtensionError::Malformed("the WebP has no extended header".into()))?;
762        let width = le24(&bytes[header.1.start + 4..]) + 1;
763        let height = le24(&bytes[header.1.start + 7..]) + 1;
764        let loop_count = chunks
765            .iter()
766            .find(|(kind, range)| kind == b"ANIM" && range.len() >= 6)
767            .map(|(_, range)| {
768                u32::from(u16::from_le_bytes([
769                    bytes[range.start + 4],
770                    bytes[range.start + 5],
771                ]))
772            });
773        let frames = chunks
774            .iter()
775            .filter(|(kind, range)| kind == b"ANMF" && range.len() >= 16)
776            .map(|(_, range)| {
777                let at = range.start;
778                let flags = bytes[at + 15];
779                WebpFrame {
780                    x: le24(&bytes[at..]) * 2,
781                    y: le24(&bytes[at + 3..]) * 2,
782                    width: le24(&bytes[at + 6..]) + 1,
783                    height: le24(&bytes[at + 9..]) + 1,
784                    duration_ms: le24(&bytes[at + 12..]),
785                    blend: if flags & 0x02 == 0 {
786                        Blend::OverWebp
787                    } else {
788                        Blend::Source
789                    },
790                    dispose: if flags & 0x01 == 0 {
791                        Dispose::None
792                    } else {
793                        Dispose::Background
794                    },
795                    data: at + 16..range.end,
796                }
797            })
798            .collect();
799        Ok(Self {
800            width,
801            height,
802            loop_count,
803            frames,
804        })
805    }
806
807    /// The canvas, and each frame, which must lie within it (RFC 9649).
808    fn index(&self) -> Result<Index, ExtensionError> {
809        let mut index = Index::new(self.width, self.height)?;
810        index.loop_count = self.loop_count;
811        for frame in &self.frames {
812            if frame.x + frame.width > self.width || frame.y + frame.height > self.height {
813                return Err(ExtensionError::Malformed(format!(
814                    "a {}x{} frame at ({}, {}) lies outside the {}x{} canvas",
815                    frame.width, frame.height, frame.x, frame.y, self.width, self.height
816                )));
817            }
818            let delay = Delay {
819                ticks: i64::from(frame.duration_ms) * TICKS_PER_MS,
820                ms: frame.duration_ms,
821            };
822            index.push(frame.width, frame.height, delay)?;
823        }
824        Ok(index)
825    }
826}
827
828impl WebpCursor {
829    fn new(bytes: Arc<[u8]>) -> Result<Self, ExtensionError> {
830        let layout = WebpLayout::read(&bytes)?;
831        let compositor = Compositor::new(layout.width, layout.height)?;
832        Ok(Self {
833            bytes,
834            frames: layout.frames,
835            next: 0,
836            compositor,
837        })
838    }
839
840    /// A frame's picture as a still WebP file of its own, decoded to
841    /// straight RGBA8.
842    fn picture(&self, frame: &WebpFrame) -> Result<Vec<u8>, ExtensionError> {
843        let chunks = riff_chunks(&self.bytes, frame.data.start, frame.data.end);
844        let wanted = |kind: &[u8; 4]| chunks.iter().find(|(chunk, _)| chunk == kind);
845        let alpha = wanted(b"ALPH");
846        let lossy = wanted(b"VP8 ");
847        let lossless = wanted(b"VP8L");
848        let mut body = Vec::new();
849        let mut push = |kind: &[u8; 4], payload: &[u8]| {
850            body.extend_from_slice(kind);
851            body.extend_from_slice(
852                &u32::try_from(payload.len())
853                    .unwrap_or(u32::MAX)
854                    .to_le_bytes(),
855            );
856            body.extend_from_slice(payload);
857            if payload.len() % 2 == 1 {
858                body.push(0);
859            }
860        };
861        match (lossless, lossy) {
862            (Some((_, range)), _) => push(b"VP8L", &self.bytes[range.clone()]),
863            (None, Some((_, range))) => {
864                // A lossy picture with an alpha plane needs an extended header saying so.
865                let mut header = vec![0_u8; 10];
866                if alpha.is_some() {
867                    header[0] = 0x10;
868                }
869                header[4..7].copy_from_slice(&(frame.width - 1).to_le_bytes()[..3]);
870                header[7..10].copy_from_slice(&(frame.height - 1).to_le_bytes()[..3]);
871                push(b"VP8X", &header);
872                if let Some((_, range)) = alpha {
873                    push(b"ALPH", &self.bytes[range.clone()]);
874                }
875                push(b"VP8 ", &self.bytes[range.clone()]);
876            }
877            (None, None) => {
878                return Err(ExtensionError::Malformed(
879                    "a WebP frame has no picture".into(),
880                ));
881            }
882        }
883        let mut file = b"RIFF".to_vec();
884        file.extend_from_slice(
885            &u32::try_from(body.len() + 4)
886                .unwrap_or(u32::MAX)
887                .to_le_bytes(),
888        );
889        file.extend_from_slice(b"WEBP");
890        file.extend_from_slice(&body);
891        let mut decoder = image_webp::WebPDecoder::new(Cursor::new(file)).map_err(malformed)?;
892        // A lossless picture's size is its own bitstream's; it must be the rectangle the frame
893        // declares.
894        if decoder.dimensions() != (frame.width, frame.height) {
895            let (width, height) = decoder.dimensions();
896            return Err(ExtensionError::Malformed(format!(
897                "a frame declares {}x{} and its picture is {width}x{height}",
898                frame.width, frame.height
899            )));
900        }
901        let mut buffer =
902            vec![
903                0;
904                decoder
905                    .output_buffer_size()
906                    .ok_or_else(|| ExtensionError::Budget("a WebP frame is too large".into()))?
907            ];
908        decoder.read_image(&mut buffer).map_err(malformed)?;
909        Ok(if decoder.has_alpha() {
910            buffer
911        } else {
912            buffer
913                .as_chunks::<3>()
914                .0
915                .iter()
916                .flat_map(|[red, green, blue]| [*red, *green, *blue, 255])
917                .collect()
918        })
919    }
920}
921
922impl FrameCursor for WebpCursor {
923    fn canvas(&self) -> &[u8] {
924        self.compositor.canvas()
925    }
926
927    fn advance(&mut self) -> Result<bool, ExtensionError> {
928        let Some(frame) = self.frames.get(self.next) else {
929            return Ok(false);
930        };
931        let partial = PartialFrame {
932            x: frame.x,
933            y: frame.y,
934            width: frame.width,
935            height: frame.height,
936            pixels: self.picture(frame)?,
937            blend: frame.blend,
938            dispose: frame.dispose,
939        };
940        self.next += 1;
941        self.compositor.draw(&partial)?;
942        Ok(true)
943    }
944}
945
946#[cfg(test)]
947mod tests;