image/codecs/jpeg/
decoder.rs

1use std::io::{BufRead, Seek};
2use std::marker::PhantomData;
3
4use crate::color::ColorType;
5use crate::error::{
6    DecodingError, ImageError, ImageResult, LimitError, UnsupportedError, UnsupportedErrorKind,
7};
8use crate::metadata::Orientation;
9use crate::{ImageDecoder, ImageFormat, Limits};
10
11type ZuneColorSpace = zune_core::colorspace::ColorSpace;
12
13/// JPEG decoder
14pub struct JpegDecoder<R> {
15    input: Vec<u8>,
16    orig_color_space: ZuneColorSpace,
17    width: u16,
18    height: u16,
19    limits: Limits,
20    orientation: Option<Orientation>,
21    // For API compatibility with the previous jpeg_decoder wrapper.
22    // Can be removed later, which would be an API break.
23    phantom: PhantomData<R>,
24}
25
26impl<R: BufRead + Seek> JpegDecoder<R> {
27    /// Create a new decoder that decodes from the stream ```r```
28    pub fn new(r: R) -> ImageResult<JpegDecoder<R>> {
29        let mut input = Vec::new();
30        let mut r = r;
31        r.read_to_end(&mut input)?;
32        let options = zune_core::options::DecoderOptions::default()
33            .set_strict_mode(false)
34            .set_max_width(usize::MAX)
35            .set_max_height(usize::MAX);
36        let mut decoder = zune_jpeg::JpegDecoder::new_with_options(input.as_slice(), options);
37        decoder.decode_headers().map_err(ImageError::from_jpeg)?;
38        // now that we've decoded the headers we can `.unwrap()`
39        // all these functions that only fail if called before decoding the headers
40        let (width, height) = decoder.dimensions().unwrap();
41        // JPEG can only express dimensions up to 65535x65535, so this conversion cannot fail
42        let width: u16 = width.try_into().unwrap();
43        let height: u16 = height.try_into().unwrap();
44        let orig_color_space = decoder.get_output_colorspace().unwrap();
45        // Limits are disabled by default in the constructor for all decoders
46        let limits = Limits::no_limits();
47        Ok(JpegDecoder {
48            input,
49            orig_color_space,
50            width,
51            height,
52            limits,
53            orientation: None,
54            phantom: PhantomData,
55        })
56    }
57}
58
59impl<R: BufRead + Seek> ImageDecoder for JpegDecoder<R> {
60    fn dimensions(&self) -> (u32, u32) {
61        (u32::from(self.width), u32::from(self.height))
62    }
63
64    fn color_type(&self) -> ColorType {
65        ColorType::from_jpeg(self.orig_color_space)
66    }
67
68    fn icc_profile(&mut self) -> ImageResult<Option<Vec<u8>>> {
69        let options = zune_core::options::DecoderOptions::default()
70            .set_strict_mode(false)
71            .set_max_width(usize::MAX)
72            .set_max_height(usize::MAX);
73        let mut decoder = zune_jpeg::JpegDecoder::new_with_options(&self.input, options);
74        decoder.decode_headers().map_err(ImageError::from_jpeg)?;
75        Ok(decoder.icc_profile())
76    }
77
78    fn exif_metadata(&mut self) -> ImageResult<Option<Vec<u8>>> {
79        let options = zune_core::options::DecoderOptions::default()
80            .set_strict_mode(false)
81            .set_max_width(usize::MAX)
82            .set_max_height(usize::MAX);
83        let mut decoder = zune_jpeg::JpegDecoder::new_with_options(&self.input, options);
84        decoder.decode_headers().map_err(ImageError::from_jpeg)?;
85        let exif = decoder.exif().cloned();
86
87        self.orientation = Some(
88            exif.as_ref()
89                .and_then(|exif| Orientation::from_exif_chunk(exif))
90                .unwrap_or(Orientation::NoTransforms),
91        );
92
93        Ok(exif)
94    }
95
96    fn orientation(&mut self) -> ImageResult<Orientation> {
97        // `exif_metadata` caches the orientation, so call it if `orientation` hasn't been set yet.
98        if self.orientation.is_none() {
99            let _ = self.exif_metadata()?;
100        }
101        Ok(self.orientation.unwrap())
102    }
103
104    fn read_image(self, buf: &mut [u8]) -> ImageResult<()> {
105        let advertised_len = self.total_bytes();
106        let actual_len = buf.len() as u64;
107
108        if actual_len != advertised_len {
109            return Err(ImageError::Decoding(DecodingError::new(
110                ImageFormat::Jpeg.into(),
111                format!(
112                    "Length of the decoded data {actual_len} \
113                    doesn't match the advertised dimensions of the image \
114                    that imply length {advertised_len}"
115                ),
116            )));
117        }
118
119        let mut decoder = new_zune_decoder(&self.input, self.orig_color_space, self.limits);
120        decoder.decode_into(buf).map_err(ImageError::from_jpeg)?;
121        Ok(())
122    }
123
124    fn set_limits(&mut self, limits: Limits) -> ImageResult<()> {
125        limits.check_support(&crate::LimitSupport::default())?;
126        let (width, height) = self.dimensions();
127        limits.check_dimensions(width, height)?;
128        self.limits = limits;
129        Ok(())
130    }
131
132    fn read_image_boxed(self: Box<Self>, buf: &mut [u8]) -> ImageResult<()> {
133        (*self).read_image(buf)
134    }
135}
136
137impl ColorType {
138    fn from_jpeg(colorspace: ZuneColorSpace) -> ColorType {
139        let colorspace = to_supported_color_space(colorspace);
140        use zune_core::colorspace::ColorSpace::*;
141        match colorspace {
142            // As of zune-jpeg 0.3.13 the output is always 8-bit,
143            // but support for 16-bit JPEG might be added in the future.
144            RGB => ColorType::Rgb8,
145            RGBA => ColorType::Rgba8,
146            Luma => ColorType::L8,
147            LumaA => ColorType::La8,
148            // to_supported_color_space() doesn't return any of the other variants
149            _ => unreachable!(),
150        }
151    }
152}
153
154fn to_supported_color_space(orig: ZuneColorSpace) -> ZuneColorSpace {
155    use zune_core::colorspace::ColorSpace::*;
156    match orig {
157        RGB | RGBA | Luma | LumaA => orig,
158        // the rest is not supported by `image` so it will be converted to RGB during decoding
159        _ => RGB,
160    }
161}
162
163fn new_zune_decoder(
164    input: &[u8],
165    orig_color_space: ZuneColorSpace,
166    limits: Limits,
167) -> zune_jpeg::JpegDecoder<&[u8]> {
168    let target_color_space = to_supported_color_space(orig_color_space);
169    let mut options = zune_core::options::DecoderOptions::default()
170        .jpeg_set_out_colorspace(target_color_space)
171        .set_strict_mode(false);
172    options = options.set_max_width(match limits.max_image_width {
173        Some(max_width) => max_width as usize, // u32 to usize never truncates
174        None => usize::MAX,
175    });
176    options = options.set_max_height(match limits.max_image_height {
177        Some(max_height) => max_height as usize, // u32 to usize never truncates
178        None => usize::MAX,
179    });
180    zune_jpeg::JpegDecoder::new_with_options(input, options)
181}
182
183impl ImageError {
184    fn from_jpeg(err: zune_jpeg::errors::DecodeErrors) -> ImageError {
185        use zune_jpeg::errors::DecodeErrors::*;
186        match err {
187            Unsupported(desc) => ImageError::Unsupported(UnsupportedError::from_format_and_kind(
188                ImageFormat::Jpeg.into(),
189                UnsupportedErrorKind::GenericFeature(format!("{desc:?}")),
190            )),
191            LargeDimensions(_) => ImageError::Limits(LimitError::from_kind(
192                crate::error::LimitErrorKind::DimensionError,
193            )),
194            err => ImageError::Decoding(DecodingError::new(ImageFormat::Jpeg.into(), err)),
195        }
196    }
197}
198
199#[cfg(test)]
200mod tests {
201    use super::*;
202    use std::{fs, io::Cursor};
203
204    #[test]
205    fn test_exif_orientation() {
206        let data = fs::read("tests/images/jpg/portrait_2.jpg").unwrap();
207        let mut decoder = JpegDecoder::new(Cursor::new(data)).unwrap();
208        assert_eq!(decoder.orientation().unwrap(), Orientation::FlipHorizontal);
209    }
210}