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