Skip to main content

valo_codec_software/
lib.rs

1//! Portable image decoding on the CPU, the fallback behind any native decoder.
2//!
3//! Formats are compiled in by Cargo feature (`png`, `jpeg`, `gif`, `webp`, `bmp`). Every frame
4//! comes out oriented, resized to the request's `max_size`, and converted to 8-bit sRGB: embedded
5//! ICC profiles, PNG gamma/chromaticity and CICP metadata are all applied rather than ignored.
6//! Animations are composited frame by frame; only the current composition is held in memory.
7
8#![warn(missing_docs)]
9
10#[cfg(animation)]
11mod animation;
12mod color;
13mod probe;
14
15#[cfg(animation)]
16use animation::FrameStream;
17use color::ColorSpace;
18use image::DynamicImage;
19use probe::Probe;
20use std::io::Cursor;
21use std::sync::Arc;
22use std::time::Duration;
23use valo::{AlphaType, PixelBuffer, PixelFormat, PixelLayout};
24use valo_codec::{
25    DecodeError, DecodedFrame, Decoder, FramePixels, FrameReader, ImageInfo, OpenError, OpenRequest,
26};
27
28/// SoftwareDecoder reads the formats this crate was built with, entirely on the CPU.
29///
30/// Register it after a platform decoder so it only sees what the platform declined, or alone on
31/// platforms without one.
32#[derive(Clone, Copy, Debug, Default)]
33pub struct SoftwareDecoder;
34
35impl Decoder for SoftwareDecoder {
36    fn name(&self) -> &'static str {
37        "software"
38    }
39
40    fn open(&self, request: &OpenRequest) -> Result<Box<dyn FrameReader>, OpenError> {
41        Ok(Box::new(SoftwareReader::open(request)?))
42    }
43}
44
45/// One opened image. Still images are re-decoded from the retained bytes on every frame request
46/// (they have one frame); animations keep the compositing iterator between frames.
47struct SoftwareReader {
48    encoded: Arc<[u8]>,
49    probe: Probe,
50    color: ColorSpace,
51    limits: image::Limits,
52    output_size: [u32; 2],
53    #[cfg(animation)]
54    animation: Option<FrameStream>,
55    next: u32,
56}
57
58impl SoftwareReader {
59    fn open(request: &OpenRequest) -> Result<Self, OpenError> {
60        let limits = request.options.limits;
61        let probe = probe::probe(&request.encoded, limits)?;
62        let color = ColorSpace::detect(&probe, &request.encoded)?;
63        let image_limits = image_limits(limits);
64        #[cfg(animation)]
65        let animation = probe
66            .animated
67            .then(|| FrameStream::new(request.encoded.clone(), probe.format, image_limits.clone()));
68        Ok(Self {
69            output_size: request.options.fit(probe.oriented_size),
70            encoded: request.encoded.clone(),
71            probe,
72            color,
73            limits: image_limits,
74            #[cfg(animation)]
75            animation,
76            next: 0,
77        })
78    }
79
80    fn decode_still(&self) -> Result<DynamicImage, DecodeError> {
81        let mut reader =
82            image::ImageReader::with_format(Cursor::new(self.encoded.clone()), self.probe.format);
83        reader.limits(self.limits.clone());
84        reader.decode().map_err(decode_error)
85    }
86
87    #[cfg(animation)]
88    fn decoded(&mut self, index: u32) -> Result<(DynamicImage, Duration), DecodeError> {
89        match &mut self.animation {
90            Some(stream) => {
91                let (frame, duration) = stream.frame(index)?;
92                Ok((DynamicImage::ImageRgba8(frame), duration))
93            }
94            None => Ok((self.decode_still()?, Duration::ZERO)),
95        }
96    }
97
98    #[cfg(not(animation))]
99    fn decoded(&mut self, _index: u32) -> Result<(DynamicImage, Duration), DecodeError> {
100        Ok((self.decode_still()?, Duration::ZERO))
101    }
102
103    fn finish(&self, mut image: DynamicImage) -> Result<PixelBuffer, DecodeError> {
104        image.apply_orientation(self.probe.orientation);
105        if [image.width(), image.height()] != self.output_size {
106            let [width, height] = self.output_size;
107            image = image.resize_exact(width, height, image::imageops::FilterType::Triangle);
108        }
109        let rgba = self.color.to_srgb_rgba8(image)?;
110        let layout = PixelLayout::packed(self.output_size, PixelFormat::Rgba8, AlphaType::Straight);
111        Ok(PixelBuffer::new(layout, rgba)?)
112    }
113}
114
115impl FrameReader for SoftwareReader {
116    fn info(&self) -> ImageInfo {
117        ImageInfo {
118            size: self.output_size,
119            frame_count: self.probe.frame_count,
120            repetition: self.probe.repetition,
121        }
122    }
123
124    fn next_frame(&mut self) -> Result<DecodedFrame, DecodeError> {
125        let index = self.next;
126        self.next = (index + 1) % self.probe.frame_count;
127        let (image, duration) = self.decoded(index)?;
128        Ok(DecodedFrame {
129            pixels: FramePixels::Cpu(self.finish(image)?),
130            duration,
131        })
132    }
133}
134
135/// The image crate's own allocation guard, sized from ours: a frame at `max_pixels` in the
136/// widest intermediate the crate uses (16 bytes a pixel) must still be allowed through.
137fn image_limits(limits: valo_codec::DecodeLimits) -> image::Limits {
138    let mut image_limits = image::Limits::default();
139    image_limits.max_alloc = Some(limits.max_pixels.saturating_mul(16));
140    image_limits
141}
142
143/// Maps the image crate's errors onto ours once a format has been accepted.
144pub(crate) fn decode_error(error: image::ImageError) -> DecodeError {
145    use image::ImageError;
146    match error {
147        ImageError::Decoding(error) => DecodeError::InvalidData(error.to_string()),
148        ImageError::Limits(_) => DecodeError::LimitExceeded("software decoder allocation"),
149        ImageError::Unsupported(error) => DecodeError::Failed(error.to_string()),
150        other => DecodeError::Failed(other.to_string()),
151    }
152}