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, Decoding, FramePixels, FrameReader, ImageInfo, OpenError,
26    OpenRequest,
27};
28
29/// SoftwareDecoder reads the formats this crate was built with, entirely on the CPU.
30///
31/// Register it after a platform decoder so it only sees what the platform declined, or alone on
32/// platforms without one.
33#[derive(Clone, Copy, Debug, Default)]
34pub struct SoftwareDecoder;
35
36impl Decoder for SoftwareDecoder {
37    fn name(&self) -> &'static str {
38        "software"
39    }
40
41    fn open<'a>(
42        &'a self,
43        request: &'a OpenRequest,
44    ) -> Decoding<'a, Result<Box<dyn FrameReader>, OpenError>> {
45        // Every step of this decoder runs on the calling thread, so the answer is ready at once.
46        Box::pin(std::future::ready(
47            SoftwareReader::open(request).map(|reader| Box::new(reader) as Box<dyn FrameReader>),
48        ))
49    }
50}
51
52/// One opened image. Still images are re-decoded from the retained bytes on every frame request
53/// (they have one frame); animations keep the compositing iterator between frames.
54struct SoftwareReader {
55    encoded: Arc<[u8]>,
56    probe: Probe,
57    color: ColorSpace,
58    limits: image::Limits,
59    output_size: [u32; 2],
60    #[cfg(animation)]
61    animation: Option<FrameStream>,
62    next: u32,
63}
64
65impl SoftwareReader {
66    fn open(request: &OpenRequest) -> Result<Self, OpenError> {
67        let limits = request.options.limits;
68        let probe = probe::probe(&request.encoded, limits)?;
69        let color = ColorSpace::detect(&probe, &request.encoded)?;
70        let image_limits = image_limits(limits);
71        #[cfg(animation)]
72        let animation = probe
73            .animated
74            .then(|| FrameStream::new(request.encoded.clone(), probe.format, image_limits.clone()));
75        Ok(Self {
76            output_size: request.options.fit(probe.oriented_size),
77            encoded: request.encoded.clone(),
78            probe,
79            color,
80            limits: image_limits,
81            #[cfg(animation)]
82            animation,
83            next: 0,
84        })
85    }
86
87    fn decode_still(&self) -> Result<DynamicImage, DecodeError> {
88        let mut reader =
89            image::ImageReader::with_format(Cursor::new(self.encoded.clone()), self.probe.format);
90        reader.limits(self.limits.clone());
91        reader.decode().map_err(decode_error)
92    }
93
94    #[cfg(animation)]
95    fn decoded(&mut self, index: u32) -> Result<(DynamicImage, Duration), DecodeError> {
96        match &mut self.animation {
97            Some(stream) => {
98                let (frame, duration) = stream.frame(index)?;
99                Ok((DynamicImage::ImageRgba8(frame), duration))
100            }
101            None => Ok((self.decode_still()?, Duration::ZERO)),
102        }
103    }
104
105    #[cfg(not(animation))]
106    fn decoded(&mut self, _index: u32) -> Result<(DynamicImage, Duration), DecodeError> {
107        Ok((self.decode_still()?, Duration::ZERO))
108    }
109
110    fn finish(&self, mut image: DynamicImage) -> Result<PixelBuffer, DecodeError> {
111        image.apply_orientation(self.probe.orientation);
112        if [image.width(), image.height()] != self.output_size {
113            let [width, height] = self.output_size;
114            image = image.resize_exact(width, height, image::imageops::FilterType::Triangle);
115        }
116        let rgba = self.color.to_srgb_rgba8(image)?;
117        let layout = PixelLayout::packed(self.output_size, PixelFormat::Rgba8, AlphaType::Straight);
118        Ok(PixelBuffer::new(layout, rgba)?)
119    }
120}
121
122impl FrameReader for SoftwareReader {
123    fn info(&self) -> ImageInfo {
124        ImageInfo {
125            size: self.output_size,
126            frame_count: self.probe.frame_count,
127            repetition: self.probe.repetition,
128        }
129    }
130
131    fn next_frame(&mut self) -> Decoding<'_, Result<DecodedFrame, DecodeError>> {
132        Box::pin(std::future::ready(self.decode_next()))
133    }
134}
135
136impl SoftwareReader {
137    fn decode_next(&mut self) -> Result<DecodedFrame, DecodeError> {
138        let index = self.next;
139        self.next = (index + 1) % self.probe.frame_count;
140        let (image, duration) = self.decoded(index)?;
141        Ok(DecodedFrame {
142            pixels: FramePixels::Cpu(self.finish(image)?),
143            duration,
144        })
145    }
146}
147
148/// The image crate's own allocation guard, sized from ours: a frame at `max_pixels` in the
149/// widest intermediate the crate uses (16 bytes a pixel) must still be allowed through.
150fn image_limits(limits: valo_codec::DecodeLimits) -> image::Limits {
151    let mut image_limits = image::Limits::default();
152    image_limits.max_alloc = Some(limits.max_pixels.saturating_mul(16));
153    image_limits
154}
155
156/// Maps the image crate's errors onto ours once a format has been accepted.
157pub(crate) fn decode_error(error: image::ImageError) -> DecodeError {
158    use image::ImageError;
159    match error {
160        ImageError::Decoding(error) => DecodeError::InvalidData(error.to_string()),
161        ImageError::Limits(_) => DecodeError::LimitExceeded("software decoder allocation"),
162        ImageError::Unsupported(error) => DecodeError::Failed(error.to_string()),
163        other => DecodeError::Failed(other.to_string()),
164    }
165}