Coverage Report

Created: 2026-06-18 07:57

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/image/src/codecs/jpeg/decoder.rs
Line
Count
Source
1
use std::io::{BufRead, Seek};
2
3
use crate::color::ColorType;
4
use crate::error::{
5
    DecodingError, ImageError, ImageResult, LimitError, UnsupportedError, UnsupportedErrorKind,
6
};
7
use crate::io::decoder::DecodedMetadataHint;
8
use crate::io::image_reader_type::SpecCompliance;
9
use crate::io::{DecodedImageAttributes, DecoderPreparedImage, FormatAttributes};
10
use crate::{ImageDecoder, ImageFormat, Limits};
11
12
type ZuneColorSpace = zune_core::colorspace::ColorSpace;
13
14
/// JPEG decoder
15
pub struct JpegDecoder<R> {
16
    decoder: zune_jpeg::JpegDecoder<R>,
17
    limits: Limits,
18
    header: Option<HeaderData>,
19
}
20
21
struct HeaderData {
22
    orig_color_space: ZuneColorSpace,
23
    width: u16,
24
    height: u16,
25
}
26
27
impl<R: BufRead + Seek> JpegDecoder<R> {
28
    /// Create a new decoder that decodes from the stream `r`
29
0
    pub fn new(r: R) -> JpegDecoder<R> {
30
0
        Self::with_spec_compliance(r, SpecCompliance::default())
31
0
    }
32
33
    /// Create a new decoder with the given spec compliance mode.
34
0
    pub(crate) fn with_spec_compliance(r: R, spec: SpecCompliance) -> JpegDecoder<R> {
35
0
        let options = zune_core::options::DecoderOptions::default()
36
0
            .set_strict_mode(matches!(spec, SpecCompliance::Strict))
37
0
            .set_max_width(usize::MAX)
38
0
            .set_max_height(usize::MAX);
39
40
0
        let decoder = zune_jpeg::JpegDecoder::new_with_options(r, options);
41
        // Limits are disabled by default in the constructor for all decoders
42
0
        let limits = Limits::no_limits();
43
44
0
        JpegDecoder {
45
0
            decoder,
46
0
            header: None,
47
0
            limits,
48
0
        }
49
0
    }
50
51
0
    fn ensure_headers(&mut self) -> ImageResult<(&mut zune_jpeg::JpegDecoder<R>, &HeaderData)> {
52
0
        if self.header.is_none() {
53
            // Adjust ensure_headers if we do not run this in the constructor!
54
0
            self.decoder
55
0
                .decode_headers()
56
0
                .map_err(ImageError::from_jpeg)?;
57
58
            // now that we've decoded the headers we can `.unwrap()`
59
            // all these functions that only fail if called before decoding the headers
60
0
            let (width, height) = self.decoder.dimensions().unwrap();
61
            // JPEG can only express dimensions up to 65535x65535, so this conversion cannot fail
62
0
            let width: u16 = width.try_into().unwrap();
63
0
            let height: u16 = height.try_into().unwrap();
64
65
0
            let orig_color_space = self
66
0
                .decoder
67
0
                .input_colorspace()
68
0
                .expect("headers were decoded");
69
70
            // configure the decoder color output based on the header information. By default it
71
            // would do its own color space defaults and we want to setup those that are compatible
72
            // with our wish of sRGB outputs.
73
0
            self.decoder.set_options({
74
0
                let requested_color = match orig_color_space {
75
                    ZuneColorSpace::RGB
76
                    | ZuneColorSpace::RGBA
77
                    | ZuneColorSpace::Luma
78
0
                    | ZuneColorSpace::LumaA => orig_color_space,
79
                    // Late failure
80
0
                    _ => ZuneColorSpace::RGB,
81
                };
82
83
0
                self.decoder
84
0
                    .options()
85
0
                    .jpeg_set_out_colorspace(requested_color)
86
            });
87
88
0
            self.header = Some(HeaderData {
89
0
                orig_color_space,
90
0
                width,
91
0
                height,
92
0
            });
93
0
        }
94
95
        // `unwrap` instead of match is used here due to lifetime overlaps in the codeflow
96
        // otherwise, we could not quick return anyways.
97
0
        let header = self.header.as_ref().unwrap();
98
        // Headers are already decoded in `new()` right now, so this is a no-op.
99
0
        Ok((&mut self.decoder, header))
100
0
    }
101
}
102
103
impl<R: BufRead + Seek> ImageDecoder for JpegDecoder<R> {
104
0
    fn format_attributes(&self) -> FormatAttributes {
105
0
        FormatAttributes {
106
0
            // As per specification, once we start with MCUs we can only have restarts. Also all
107
0
            // our methods currently seek of their own accord anyways, it's just important to
108
0
            // uphold this if we do not buffer the whole file.
109
0
            icc: DecodedMetadataHint::InHeader,
110
0
            exif: DecodedMetadataHint::InHeader,
111
0
            xmp: DecodedMetadataHint::InHeader,
112
0
            iptc: DecodedMetadataHint::InHeader,
113
0
            ..FormatAttributes::default()
114
0
        }
115
0
    }
116
117
0
    fn prepare_image(&mut self) -> ImageResult<DecoderPreparedImage> {
118
0
        let (_, header) = self.ensure_headers()?;
119
0
        Ok(DecoderPreparedImage::new(
120
0
            header.width.into(),
121
0
            header.height.into(),
122
0
            ColorType::from_jpeg(header.orig_color_space),
123
0
        ))
124
0
    }
125
126
0
    fn icc_profile(&mut self) -> ImageResult<Option<Vec<u8>>> {
127
0
        let (decoder, _) = self.ensure_headers()?;
128
0
        Ok(decoder.icc_profile())
129
0
    }
130
131
0
    fn exif_metadata(&mut self) -> ImageResult<Option<Vec<u8>>> {
132
0
        let (decoder, _) = self.ensure_headers()?;
133
0
        Ok(decoder.exif().cloned())
134
0
    }
135
136
0
    fn xmp_metadata(&mut self) -> ImageResult<Option<Vec<u8>>> {
137
0
        let (decoder, _) = self.ensure_headers()?;
138
0
        Ok(decoder.xmp().cloned())
139
0
    }
140
141
0
    fn iptc_metadata(&mut self) -> ImageResult<Option<Vec<u8>>> {
142
0
        let (decoder, _) = self.ensure_headers()?;
143
0
        Ok(decoder.iptc().cloned())
144
0
    }
145
146
0
    fn read_image(&mut self, buf: &mut [u8]) -> ImageResult<DecodedImageAttributes> {
147
0
        let layout = self.prepare_image()?;
148
149
0
        let advertised_len = layout.total_bytes();
150
0
        let actual_len = buf.len() as u64;
151
152
0
        if actual_len != advertised_len {
153
0
            return Err(ImageError::Decoding(DecodingError::new(
154
0
                ImageFormat::Jpeg.into(),
155
0
                format!(
156
0
                    "Length of the decoded data {actual_len} \
157
0
                    doesn't match the advertised dimensions of the image \
158
0
                    that imply length {advertised_len}"
159
0
                ),
160
0
            )));
161
0
        }
162
163
0
        let (decoder, _) = self.ensure_headers()?;
164
0
        decoder.decode_into(buf).map_err(|err| {
165
0
            ImageError::Decoding(DecodingError::new(ImageFormat::Jpeg.into(), err))
166
0
        })?;
167
168
0
        Ok(DecodedImageAttributes {
169
0
            ..DecodedImageAttributes::default()
170
0
        })
171
0
    }
172
173
0
    fn set_limits(&mut self, limits: Limits) -> ImageResult<()> {
174
0
        limits.check_support(&crate::LimitSupport::default())?;
175
0
        let layout = self.prepare_image()?;
176
0
        limits.check_layout_dimensions(&layout)?;
177
0
        self.limits = limits;
178
0
        Ok(())
179
0
    }
180
}
181
182
impl ColorType {
183
0
    fn from_jpeg(colorspace: ZuneColorSpace) -> ColorType {
184
0
        let colorspace = to_supported_color_space(colorspace);
185
        use zune_core::colorspace::ColorSpace::*;
186
0
        match colorspace {
187
            // As of zune-jpeg 0.3.13 the output is always 8-bit,
188
            // but support for 16-bit JPEG might be added in the future.
189
0
            RGB => ColorType::Rgb8,
190
0
            RGBA => ColorType::Rgba8,
191
0
            Luma => ColorType::L8,
192
0
            LumaA => ColorType::La8,
193
            // to_supported_color_space() doesn't return any of the other variants
194
0
            _ => unreachable!(),
195
        }
196
0
    }
197
}
198
199
0
fn to_supported_color_space(orig: ZuneColorSpace) -> ZuneColorSpace {
200
    use zune_core::colorspace::ColorSpace::*;
201
0
    match orig {
202
0
        RGB | RGBA | Luma | LumaA => orig,
203
        // the rest is not supported by `image` so it will be converted to RGB during decoding
204
0
        _ => RGB,
205
    }
206
0
}
207
208
impl ImageError {
209
0
    fn from_jpeg(err: zune_jpeg::errors::DecodeErrors) -> ImageError {
210
        use zune_jpeg::errors::DecodeErrors::*;
211
0
        match err {
212
0
            Unsupported(desc) => ImageError::Unsupported(UnsupportedError::from_format_and_kind(
213
0
                ImageFormat::Jpeg.into(),
214
0
                UnsupportedErrorKind::GenericFeature(format!("{desc:?}")),
215
0
            )),
216
0
            LargeDimensions(_) => ImageError::Limits(LimitError::from_kind(
217
0
                crate::error::LimitErrorKind::DimensionError,
218
0
            )),
219
0
            err => ImageError::Decoding(DecodingError::new(ImageFormat::Jpeg.into(), err)),
220
        }
221
0
    }
222
}
223
224
#[cfg(test)]
225
mod tests {
226
    use super::*;
227
    use std::{fs, io::Cursor};
228
229
    #[test]
230
    fn test_exif_orientation() {
231
        let data = fs::read("tests/images/jpg/portrait_2.jpg").unwrap();
232
        let decoder = JpegDecoder::new(Cursor::new(data));
233
234
        let mut image = crate::DynamicImage::new_luma8(0, 0);
235
        let mut reader = crate::ImageReader::from_decoder(Box::new(decoder));
236
        let meta = reader.decode_to_dynimage(&mut image).unwrap();
237
238
        assert_eq!(
239
            meta.attributes().orientation.unwrap(),
240
            crate::metadata::Orientation::FlipHorizontal
241
        );
242
    }
243
244
    #[test]
245
    fn test_strict_vs_lenient_spec_compliance() {
246
        let mut image = fs::read("tests/images/jpg/progressive/cat.jpg").unwrap();
247
        image.truncate(image.len() - 1000); // simulate a truncated image
248
249
        // Default (lenient) mode: truncated image should be accepted
250
        let mut decoder = JpegDecoder::new(Cursor::new(&image));
251
        let layout = decoder.prepare_image().unwrap();
252
        let mut buffer = vec![0u8; layout.total_bytes() as usize];
253
        assert!(decoder.read_image(&mut buffer).is_ok());
254
255
        // Strict mode: truncated image should be rejected
256
        let mut decoder =
257
            JpegDecoder::with_spec_compliance(Cursor::new(&image), SpecCompliance::Strict);
258
        let layout = decoder.prepare_image().unwrap();
259
        let mut buffer = vec![0u8; layout.total_bytes() as usize];
260
        assert!(decoder.read_image(&mut buffer).is_err());
261
    }
262
}