valo_codec/backend.rs
1//! The contract a codec library implements to plug into an [`ImageLoader`](crate::ImageLoader).
2use crate::{DecodeError, DecodeOptions, ImageInfo};
3use std::sync::Arc;
4use std::time::Duration;
5use valo::PixelBuffer;
6
7/// Decoder is one codec library, such as Apple's ImageIO or the portable `image` crate.
8///
9/// A loader tries its decoders in registration order and the first one that opens the bytes
10/// owns them for every frame. A decoder is a stateless registry shared across threads; the
11/// [`FrameReader`] it opens is per image and stays on the thread that opened it, so a reader may
12/// hold thread-affine platform objects.
13pub trait Decoder: Send + Sync {
14 /// `name` identifies this decoder in diagnostics when every decoder declines an image.
15 fn name(&self) -> &'static str;
16
17 /// `open` reads enough of the bytes to know the image's size and frame count.
18 ///
19 /// Return [`OpenError::Unsupported`] for a format or variant this decoder does not read, so
20 /// the next decoder gets its turn. Anything else stops the search.
21 fn open(&self, request: &OpenRequest) -> Result<Box<dyn FrameReader>, OpenError>;
22}
23
24/// OpenRequest is everything a decoder needs to open one image.
25#[derive(Clone)]
26pub struct OpenRequest {
27 /// `encoded` is the complete file. It is shared so a reader can keep it without copying.
28 pub encoded: Arc<[u8]>,
29
30 /// `options` bound the decode and say what size the caller wants.
31 pub options: DecodeOptions,
32
33 /// `device` is where a native decoder imports a texture it produced. Software decoders
34 /// ignore it.
35 pub device: wgpu::Device,
36}
37
38/// OpenError is why a decoder did not open an image.
39#[derive(Clone, Debug, PartialEq, Eq)]
40pub enum OpenError {
41 /// Unsupported means this decoder does not read the format or variant; the next decoder is
42 /// tried. The text names what was missing, for diagnostics.
43 Unsupported(String),
44
45 /// Failed means the image was recognised but cannot be decoded, or a limit was hit; no other
46 /// decoder is tried.
47 Failed(DecodeError),
48}
49
50impl From<DecodeError> for OpenError {
51 fn from(error: DecodeError) -> Self {
52 Self::Failed(error)
53 }
54}
55
56/// FrameReader is one opened image, yielding complete composited frames in order.
57///
58/// Animation composition — disposal, blending, partial frames — is the reader's job; every frame
59/// it returns is a full picture. Readers are used from one thread and dropped there.
60pub trait FrameReader {
61 /// `info` is the output size after [`DecodeOptions::max_size`], the frame count and how the
62 /// animation repeats. It is fixed at open and every frame must match it.
63 fn info(&self) -> ImageInfo;
64
65 /// `next_frame` decodes the next frame, wrapping to the first after the last.
66 fn next_frame(&mut self) -> Result<DecodedFrame, DecodeError>;
67}
68
69/// DecodedFrame is one complete frame as a decoder produced it, before it becomes an image.
70pub struct DecodedFrame {
71 /// `pixels` is where the frame lives: CPU memory or a texture on the request's device.
72 pub pixels: FramePixels,
73
74 /// `duration` is how long the frame stays on screen; zero for a still image or when the file
75 /// gives no delay.
76 pub duration: Duration,
77}
78
79/// FramePixels is what a decoder hands back — never an image; the loader makes those.
80pub enum FramePixels {
81 /// Cpu is owned samples in any layout [`PixelBuffer`] accepts; the loader uploads them.
82 Cpu(PixelBuffer),
83
84 /// Gpu is a texture already holding the frame on the request's device, so nothing is copied
85 /// back through the CPU. It must be 2D, single-sample, `Rgba8Unorm` or `Bgra8Unorm`, carry
86 /// `TEXTURE_BINDING`, and hold premultiplied sRGB samples. Whatever backs it must stay alive
87 /// until wgpu drops the texture; see `valo::import_metal_texture`.
88 Gpu(wgpu::Texture),
89}
90
91impl DecodedFrame {
92 /// `still` is a frame with no on-screen duration, the shape of every still image.
93 pub fn still(pixels: FramePixels) -> Self {
94 Self {
95 pixels,
96 duration: Duration::ZERO,
97 }
98 }
99}