Skip to main content

valo_codec/
options.rs

1//! What a caller asks of a decode, and what an opened image reports back.
2use crate::DecodeError;
3
4/// DecodeOptions says how large a decode may be and what the caller wants out of it.
5#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
6pub struct DecodeOptions {
7    /// `max_size` asks for frames no larger than this box, keeping the aspect ratio.
8    ///
9    /// Decoders never upscale: the GPU does that for free at draw time, while decoding larger
10    /// only costs memory. A native decoder scales while it decodes, which is the point of asking
11    /// here rather than resizing afterwards. `None` decodes at the intrinsic size.
12    pub max_size: Option<[u32; 2]>,
13
14    /// `mipmaps` asks for a full mip chain, for images that will be drawn smaller than decoded.
15    pub mipmaps: bool,
16
17    /// `limits` bound the input and every frame, for untrusted bytes.
18    pub limits: DecodeLimits,
19}
20
21impl DecodeOptions {
22    /// `fit` is the output size for a source of `source` pixels under `max_size`.
23    pub fn fit(&self, source: [u32; 2]) -> [u32; 2] {
24        match self.max_size {
25            Some(max) => fit_within(source, max),
26            None => source,
27        }
28    }
29}
30
31/// `fit_within` scales `source` down to fit `bounds`, keeping its aspect ratio and never
32/// returning a zero side. A source that already fits is returned unchanged.
33pub fn fit_within(source: [u32; 2], bounds: [u32; 2]) -> [u32; 2] {
34    if source[0] <= bounds[0] && source[1] <= bounds[1] {
35        return source;
36    }
37    let by_width = [bounds[0], scaled(source[1], source[0], bounds[0])];
38    if by_width[1] <= bounds[1] {
39        by_width
40    } else {
41        [scaled(source[0], source[1], bounds[1]), bounds[1]]
42    }
43}
44
45/// `value * to / from`, rounded, and at least one.
46fn scaled(value: u32, from: u32, to: u32) -> u32 {
47    let from = u64::from(from.max(1));
48    let scaled = (u64::from(value) * u64::from(to) + from / 2) / from;
49    u32::try_from(scaled).unwrap_or(u32::MAX).max(1)
50}
51
52/// DecodeLimits bounds what a decode may consume, so hostile bytes cannot exhaust memory.
53#[derive(Clone, Copy, Debug, PartialEq, Eq)]
54pub struct DecodeLimits {
55    /// `max_encoded_bytes` bounds the input file.
56    pub max_encoded_bytes: usize,
57
58    /// `max_pixels` bounds one frame at its source size, before any downscale.
59    pub max_pixels: u64,
60
61    /// `max_frames` bounds the frame count an animation may declare.
62    pub max_frames: u32,
63}
64
65impl Default for DecodeLimits {
66    fn default() -> Self {
67        Self {
68            max_encoded_bytes: 256 * 1024 * 1024,
69            max_pixels: 64 * 1024 * 1024,
70            max_frames: 10_000,
71        }
72    }
73}
74
75impl DecodeLimits {
76    /// `check_source_size` rejects an empty or too-large frame before any pixels are allocated.
77    pub fn check_source_size(self, size: [u32; 2]) -> Result<(), DecodeError> {
78        if size.contains(&0) {
79            return Err(DecodeError::InvalidData("empty image".into()));
80        }
81        if u64::from(size[0]) * u64::from(size[1]) > self.max_pixels {
82            return Err(DecodeError::LimitExceeded("decoded pixels"));
83        }
84        Ok(())
85    }
86
87    /// `check_frame_count` rejects an image with no frames or more than the limit.
88    pub fn check_frame_count(self, count: u32) -> Result<(), DecodeError> {
89        if count == 0 {
90            return Err(DecodeError::InvalidData("image has no frames".into()));
91        }
92        if count > self.max_frames {
93            return Err(DecodeError::LimitExceeded("animation frames"));
94        }
95        Ok(())
96    }
97}
98
99/// ImageInfo is what is known about an image once it is open, before any frame is decoded.
100#[derive(Clone, Copy, Debug, PartialEq, Eq)]
101pub struct ImageInfo {
102    /// `size` is the size every frame will have: the oriented source size fitted to
103    /// [`DecodeOptions::max_size`].
104    pub size: [u32; 2],
105
106    /// `frame_count` is one for a still image.
107    pub frame_count: u32,
108
109    /// `repetition` is how the animation repeats after its first pass.
110    pub repetition: Repetition,
111}
112
113/// Repetition is how an animation continues once it has played through.
114#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
115pub enum Repetition {
116    /// Once plays a single pass, which is also what a still image does.
117    #[default]
118    Once,
119
120    /// Times plays this many more passes after the first.
121    Times(u32),
122
123    /// Forever never stops, which is what most animated files ask for.
124    Forever,
125}
126
127#[cfg(test)]
128mod tests {
129    use super::*;
130
131    #[test]
132    fn fitting_keeps_aspect_and_never_upscales() {
133        assert_eq!(fit_within([400, 200], [800, 800]), [400, 200]);
134        assert_eq!(fit_within([400, 200], [100, 100]), [100, 50]);
135        assert_eq!(fit_within([200, 400], [100, 100]), [50, 100]);
136        assert_eq!(fit_within([4000, 1], [100, 100]), [100, 1]);
137        assert_eq!(fit_within([3, 3], [2, 1]), [1, 1]);
138    }
139}