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