Skip to main content

document_graphics/
images.rs

1//! Bounded portable image decoding, with optional isolated region/native decoding.
2//! Byte decoding does not access files, fonts, networking or OS codec services.
3use crate::Raster;
4use image::{ImageDecoder, ImageReader};
5use std::{
6    io::Cursor,
7    sync::atomic::{AtomicBool, Ordering},
8};
9
10#[cfg(all(feature = "image-worker", unix))]
11mod backend;
12#[cfg(all(feature = "image-native", target_os = "macos"))]
13mod macos;
14#[cfg(all(feature = "image-worker", unix))]
15mod png_rows;
16#[cfg(all(feature = "image-worker", unix))]
17mod portable;
18#[cfg(feature = "image-worker")]
19mod protocol;
20#[cfg(feature = "image-worker")]
21mod regions;
22#[cfg(all(feature = "image-worker", unix))]
23mod svg;
24#[cfg(feature = "image-worker")]
25pub mod worker;
26#[cfg(feature = "image-worker")]
27pub use regions::*;
28#[cfg(feature = "image-worker")]
29pub const MAX_FILE_BYTES: u64 = 256 * 1024 * 1024;
30#[cfg(all(feature = "image-worker", unix))]
31const SVG_SOURCE_LIMIT: usize = 32 * 1024 * 1024;
32#[cfg(all(feature = "image-worker", unix))]
33const PORTABLE_PIXEL_LIMIT: u64 = 16 * 1024 * 1024;
34pub const CANCELLED: &str = "Image request cancelled";
35#[cfg(feature = "image-worker")]
36fn check(cancel: &AtomicBool) -> Result<(), String> {
37    if cancel.load(Ordering::Relaxed) {
38        Err(CANCELLED.into())
39    } else {
40        Ok(())
41    }
42}
43
44#[derive(Clone, Copy, Debug)]
45pub enum Format {
46    Png,
47    Jpeg,
48}
49#[derive(Clone, Copy, Debug, PartialEq, Eq)]
50#[non_exhaustive]
51pub enum Error {
52    Invalid,
53    Limit,
54    Cancelled,
55}
56impl std::fmt::Display for Error {
57    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
58        f.write_str(match self {
59            Self::Invalid => "Invalid image",
60            Self::Limit => "Image exceeds decoding limits",
61            Self::Cancelled => CANCELLED,
62        })
63    }
64}
65impl std::error::Error for Error {}
66
67/// Limits for in-process PNG/JPEG thumbnails. For large files or hard deadlines,
68/// use the image worker instead. Larger configured values cannot exceed the hard
69/// ceilings: 32 MiB encoded, 16 megapixels, 128 MiB decoded and 4 megapixels output.
70#[derive(Clone, Copy, Debug)]
71pub struct Limits {
72    pub encoded_bytes: usize,
73    pub source_edge: u32,
74    pub source_pixels: u64,
75    pub decoded_bytes: u64,
76    pub output_pixels: u64,
77}
78impl Default for Limits {
79    fn default() -> Self {
80        Self {
81            encoded_bytes: 8 * 1024 * 1024,
82            source_edge: 8192,
83            source_pixels: 4_000_000,
84            decoded_bytes: 32 * 1024 * 1024,
85            output_pixels: 1_000_000,
86        }
87    }
88}
89impl Limits {
90    fn valid(self) -> bool {
91        self.encoded_bytes > 0
92            && self.encoded_bytes <= 32 * 1024 * 1024
93            && self.source_edge > 0
94            && self.source_edge <= 32768
95            && self.source_pixels > 0
96            && self.source_pixels <= 16 * 1024 * 1024
97            && self.decoded_bytes > 0
98            && self.decoded_bytes <= 128 * 1024 * 1024
99            && self.output_pixels > 0
100            && self.output_pixels <= 4 * 1024 * 1024
101    }
102}
103/// Decode a fitted, orientation-corrected still image to straight RGBA. No upscaling.
104/// Run off the UI thread. Cancellation is checked around codec calls, not inside
105/// them; allocation and source limits are enforced before full decoding.
106pub fn decode(
107    bytes: &[u8],
108    format: Format,
109    bounds: (u32, u32),
110    limits: Limits,
111    cancel: &AtomicBool,
112) -> Result<Raster, Error> {
113    let check = || {
114        if cancel.load(Ordering::Relaxed) {
115            Err(Error::Cancelled)
116        } else {
117            Ok(())
118        }
119    };
120    check()?;
121    if !limits.valid() || bytes.len() > limits.encoded_bytes || bounds.0 == 0 || bounds.1 == 0 {
122        return Err(Error::Limit);
123    }
124    let format = match format {
125        Format::Png => image::ImageFormat::Png,
126        Format::Jpeg => image::ImageFormat::Jpeg,
127    };
128    let mut codec_limits = image::Limits::default();
129    codec_limits.max_image_width = Some(limits.source_edge);
130    codec_limits.max_image_height = Some(limits.source_edge);
131    codec_limits.max_alloc = Some(limits.decoded_bytes);
132    let mut reader = ImageReader::with_format(Cursor::new(bytes), format);
133    reader.limits(codec_limits);
134    let mut decoder = reader.into_decoder().map_err(image_error)?;
135    let (width, height) = decoder.dimensions();
136    if width == 0
137        || height == 0
138        || width > limits.source_edge
139        || height > limits.source_edge
140        || u64::from(width) * u64::from(height) > limits.source_pixels
141        || decoder.total_bytes() > limits.decoded_bytes
142    {
143        return Err(Error::Limit);
144    }
145    let orientation = decoder.orientation().map_err(image_error)?;
146    check()?;
147    let mut image = image::DynamicImage::from_decoder(decoder).map_err(image_error)?;
148    check()?;
149    image.apply_orientation(orientation);
150    let (original_width, original_height) = (image.width(), image.height());
151    let scale = (bounds.0 as f64 / original_width as f64)
152        .min(bounds.1 as f64 / original_height as f64)
153        .min(1.)
154        .min(
155            (limits.output_pixels as f64 / (original_width as f64 * original_height as f64)).sqrt(),
156        );
157    let mut size = (
158        (original_width as f64 * scale).floor().max(1.) as u32,
159        (original_height as f64 * scale).floor().max(1.) as u32,
160    );
161    // Rounding a very narrow axis up to one pixel must not exceed the budget.
162    size.0 = size.0.min(limits.output_pixels as u32);
163    size.1 = size
164        .1
165        .min((limits.output_pixels / u64::from(size.0)) as u32);
166    let image = if size != (original_width, original_height) {
167        image.thumbnail_exact(size.0, size.1)
168    } else {
169        image
170    };
171    check()?;
172    Ok(Raster {
173        width: image.width(),
174        height: image.height(),
175        original_width,
176        original_height,
177        rgba: image.into_rgba8().into_raw(),
178    })
179}
180fn image_error(error: image::ImageError) -> Error {
181    if matches!(error, image::ImageError::Limits(_)) {
182        Error::Limit
183    } else {
184        Error::Invalid
185    }
186}
187
188#[cfg(test)]
189mod tests {
190    use super::*;
191    fn png() -> Vec<u8> {
192        let image =
193            image::RgbaImage::from_fn(37, 19, |x, y| image::Rgba([x as u8, y as u8, 92, 128]));
194        let mut bytes = Cursor::new(Vec::new());
195        image.write_to(&mut bytes, image::ImageFormat::Png).unwrap();
196        bytes.into_inner()
197    }
198    #[test]
199    fn preserves_pixels_and_applies_caller_budgets_before_decode() {
200        let bytes = png();
201        let cancel = AtomicBool::new(false);
202        let limits = Limits::default();
203        let result = decode(&bytes, Format::Png, (100, 100), limits, &cancel).unwrap();
204        assert_eq!((result.width, result.height), (37, 19));
205        assert_eq!(&result.rgba[..4], &[0, 0, 92, 128]);
206        for limits in [
207            Limits {
208                encoded_bytes: 1,
209                ..limits
210            },
211            Limits {
212                source_pixels: 10,
213                ..limits
214            },
215            Limits {
216                decoded_bytes: 1,
217                ..limits
218            },
219            Limits {
220                output_pixels: 0,
221                ..limits
222            },
223        ] {
224            assert!(matches!(
225                decode(&bytes, Format::Png, (100, 100), limits, &cancel),
226                Err(Error::Limit)
227            ));
228        }
229        for (width, height) in [(1, 200), (200, 1)] {
230            let mut encoded = Cursor::new(Vec::new());
231            image::RgbaImage::new(width, height)
232                .write_to(&mut encoded, image::ImageFormat::Png)
233                .unwrap();
234            let result = decode(
235                encoded.get_ref(),
236                Format::Png,
237                (1000, 1000),
238                Limits {
239                    output_pixels: 3,
240                    ..limits
241                },
242                &cancel,
243            )
244            .unwrap();
245            assert!(result.width > 0 && result.height > 0);
246            assert!(u64::from(result.width) * u64::from(result.height) <= 3);
247        }
248        let result = decode(
249            &bytes,
250            Format::Png,
251            (100, 100),
252            Limits {
253                output_pixels: 100,
254                ..limits
255            },
256            &cancel,
257        )
258        .unwrap();
259        assert!(result.width * result.height <= 100);
260        assert!(matches!(
261            decode(
262                &bytes,
263                Format::Png,
264                (100, 100),
265                limits,
266                &AtomicBool::new(true)
267            ),
268            Err(Error::Cancelled)
269        ));
270    }
271}