Coverage Report

Created: 2026-02-26 07:34

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
use std::marker::PhantomData;
3
4
use zune_core::bytestream::ZCursor;
5
6
use crate::color::ColorType;
7
use crate::error::{
8
    DecodingError, ImageError, ImageResult, LimitError, UnsupportedError, UnsupportedErrorKind,
9
};
10
use crate::io::image_reader_type::SpecCompliance;
11
use crate::metadata::Orientation;
12
use crate::{ImageDecoder, ImageFormat, Limits};
13
14
type ZuneColorSpace = zune_core::colorspace::ColorSpace;
15
16
/// JPEG decoder
17
pub struct JpegDecoder<R> {
18
    input: Vec<u8>,
19
    orig_color_space: ZuneColorSpace,
20
    spec_compliance: SpecCompliance,
21
    width: u16,
22
    height: u16,
23
    limits: Limits,
24
    orientation: Option<Orientation>,
25
    // For API compatibility with the previous jpeg_decoder wrapper.
26
    // Can be removed later, which would be an API break.
27
    phantom: PhantomData<R>,
28
}
29
30
impl<R: BufRead + Seek> JpegDecoder<R> {
31
    /// Create a new decoder that decodes from the stream ```r```
32
12.3k
    pub fn new(r: R) -> ImageResult<JpegDecoder<R>> {
33
12.3k
        let mut input = Vec::new();
34
12.3k
        let mut r = r;
35
12.3k
        r.read_to_end(&mut input)?;
36
12.3k
        let options = zune_core::options::DecoderOptions::default()
37
12.3k
            .set_strict_mode(false)
38
12.3k
            .set_max_width(usize::MAX)
39
12.3k
            .set_max_height(usize::MAX);
40
12.3k
        let mut decoder =
41
12.3k
            zune_jpeg::JpegDecoder::new_with_options(ZCursor::new(input.as_slice()), options);
42
12.3k
        decoder.decode_headers().map_err(ImageError::from_jpeg)?;
43
        // now that we've decoded the headers we can `.unwrap()`
44
        // all these functions that only fail if called before decoding the headers
45
8.54k
        let (width, height) = decoder.dimensions().unwrap();
46
        // JPEG can only express dimensions up to 65535x65535, so this conversion cannot fail
47
8.54k
        let width: u16 = width.try_into().unwrap();
48
8.54k
        let height: u16 = height.try_into().unwrap();
49
8.54k
        let orig_color_space = decoder.input_colorspace().expect("headers were decoded");
50
51
        // Now configure the decoder color output.
52
8.54k
        decoder.set_options({
53
8.54k
            let requested_color = match orig_color_space {
54
                ZuneColorSpace::RGB
55
                | ZuneColorSpace::RGBA
56
                | ZuneColorSpace::Luma
57
5.45k
                | ZuneColorSpace::LumaA => orig_color_space,
58
                // Late failure
59
3.09k
                _ => ZuneColorSpace::RGB,
60
            };
61
62
8.54k
            decoder.options().jpeg_set_out_colorspace(requested_color)
63
        });
64
65
        // Limits are disabled by default in the constructor for all decoders
66
8.54k
        let limits = Limits::no_limits();
67
8.54k
        Ok(JpegDecoder {
68
8.54k
            input,
69
8.54k
            orig_color_space,
70
8.54k
            spec_compliance: SpecCompliance::default(),
71
8.54k
            width,
72
8.54k
            height,
73
8.54k
            limits,
74
8.54k
            orientation: None,
75
8.54k
            phantom: PhantomData,
76
8.54k
        })
77
12.3k
    }
78
79
    /// Create a new decoder with the given spec compliance mode.
80
12.3k
    pub(crate) fn new_with_spec_compliance(
81
12.3k
        r: R,
82
12.3k
        spec: SpecCompliance,
83
12.3k
    ) -> ImageResult<JpegDecoder<R>> {
84
12.3k
        let mut decoder = Self::new(r)?;
85
8.54k
        decoder.spec_compliance = spec;
86
8.54k
        Ok(decoder)
87
12.3k
    }
88
}
89
90
impl<R: BufRead + Seek> ImageDecoder for JpegDecoder<R> {
91
42.6k
    fn dimensions(&self) -> (u32, u32) {
92
42.6k
        (u32::from(self.width), u32::from(self.height))
93
42.6k
    }
94
95
34.1k
    fn color_type(&self) -> ColorType {
96
34.1k
        ColorType::from_jpeg(self.orig_color_space)
97
34.1k
    }
98
99
0
    fn icc_profile(&mut self) -> ImageResult<Option<Vec<u8>>> {
100
0
        let options = zune_core::options::DecoderOptions::default()
101
0
            .set_strict_mode(self.spec_compliance == SpecCompliance::Strict)
102
0
            .set_max_width(usize::MAX)
103
0
            .set_max_height(usize::MAX);
104
0
        let mut decoder =
105
0
            zune_jpeg::JpegDecoder::new_with_options(ZCursor::new(&self.input), options);
106
0
        decoder.decode_headers().map_err(ImageError::from_jpeg)?;
107
0
        Ok(decoder.icc_profile())
108
0
    }
109
110
0
    fn exif_metadata(&mut self) -> ImageResult<Option<Vec<u8>>> {
111
0
        let options = zune_core::options::DecoderOptions::default()
112
0
            .set_strict_mode(self.spec_compliance == SpecCompliance::Strict)
113
0
            .set_max_width(usize::MAX)
114
0
            .set_max_height(usize::MAX);
115
0
        let mut decoder =
116
0
            zune_jpeg::JpegDecoder::new_with_options(ZCursor::new(&self.input), options);
117
0
        decoder.decode_headers().map_err(ImageError::from_jpeg)?;
118
0
        let exif = decoder.exif().cloned();
119
120
        self.orientation = Some(
121
0
            exif.as_ref()
122
0
                .and_then(|exif| Orientation::from_exif_chunk(exif))
123
0
                .unwrap_or(Orientation::NoTransforms),
124
        );
125
126
0
        Ok(exif)
127
0
    }
128
129
0
    fn xmp_metadata(&mut self) -> ImageResult<Option<Vec<u8>>> {
130
0
        let options = zune_core::options::DecoderOptions::default()
131
0
            .set_strict_mode(self.spec_compliance == SpecCompliance::Strict)
132
0
            .set_max_width(usize::MAX)
133
0
            .set_max_height(usize::MAX);
134
0
        let mut decoder =
135
0
            zune_jpeg::JpegDecoder::new_with_options(ZCursor::new(&self.input), options);
136
0
        decoder.decode_headers().map_err(ImageError::from_jpeg)?;
137
138
0
        Ok(decoder.xmp().cloned())
139
0
    }
140
141
0
    fn iptc_metadata(&mut self) -> ImageResult<Option<Vec<u8>>> {
142
0
        let options = zune_core::options::DecoderOptions::default()
143
0
            .set_strict_mode(self.spec_compliance == SpecCompliance::Strict)
144
0
            .set_max_width(usize::MAX)
145
0
            .set_max_height(usize::MAX);
146
0
        let mut decoder =
147
0
            zune_jpeg::JpegDecoder::new_with_options(ZCursor::new(&self.input), options);
148
0
        decoder.decode_headers().map_err(ImageError::from_jpeg)?;
149
150
0
        Ok(decoder.iptc().cloned())
151
0
    }
152
153
0
    fn orientation(&mut self) -> ImageResult<Orientation> {
154
        // `exif_metadata` caches the orientation, so call it if `orientation` hasn't been set yet.
155
0
        if self.orientation.is_none() {
156
0
            let _ = self.exif_metadata()?;
157
0
        }
158
0
        Ok(self.orientation.unwrap())
159
0
    }
160
161
8.53k
    fn read_image(self, buf: &mut [u8]) -> ImageResult<()> {
162
8.53k
        let advertised_len = self.total_bytes();
163
8.53k
        let actual_len = buf.len() as u64;
164
165
8.53k
        if actual_len != advertised_len {
166
0
            return Err(ImageError::Decoding(DecodingError::new(
167
0
                ImageFormat::Jpeg.into(),
168
0
                format!(
169
0
                    "Length of the decoded data {actual_len} \
170
0
                    doesn't match the advertised dimensions of the image \
171
0
                    that imply length {advertised_len}"
172
0
                ),
173
0
            )));
174
8.53k
        }
175
176
8.53k
        let mut decoder = new_zune_decoder(
177
8.53k
            &self.input,
178
8.53k
            self.orig_color_space,
179
8.53k
            self.spec_compliance == SpecCompliance::Strict,
180
8.53k
            self.limits,
181
        );
182
8.53k
        decoder.decode_into(buf).map_err(ImageError::from_jpeg)?;
183
5.50k
        Ok(())
184
8.53k
    }
185
186
8.53k
    fn set_limits(&mut self, limits: Limits) -> ImageResult<()> {
187
8.53k
        limits.check_support(&crate::LimitSupport::default())?;
188
8.53k
        let (width, height) = self.dimensions();
189
8.53k
        limits.check_dimensions(width, height)?;
190
8.53k
        self.limits = limits;
191
8.53k
        Ok(())
192
8.53k
    }
193
194
8.53k
    fn read_image_boxed(self: Box<Self>, buf: &mut [u8]) -> ImageResult<()> {
195
8.53k
        (*self).read_image(buf)
196
8.53k
    }
197
}
198
199
impl ColorType {
200
34.1k
    fn from_jpeg(colorspace: ZuneColorSpace) -> ColorType {
201
34.1k
        let colorspace = to_supported_color_space(colorspace);
202
        use zune_core::colorspace::ColorSpace::*;
203
34.1k
        match colorspace {
204
            // As of zune-jpeg 0.3.13 the output is always 8-bit,
205
            // but support for 16-bit JPEG might be added in the future.
206
12.5k
            RGB => ColorType::Rgb8,
207
0
            RGBA => ColorType::Rgba8,
208
21.6k
            Luma => ColorType::L8,
209
0
            LumaA => ColorType::La8,
210
            // to_supported_color_space() doesn't return any of the other variants
211
0
            _ => unreachable!(),
212
        }
213
34.1k
    }
214
}
215
216
42.6k
fn to_supported_color_space(orig: ZuneColorSpace) -> ZuneColorSpace {
217
    use zune_core::colorspace::ColorSpace::*;
218
42.6k
    match orig {
219
27.2k
        RGB | RGBA | Luma | LumaA => orig,
220
        // the rest is not supported by `image` so it will be converted to RGB during decoding
221
15.4k
        _ => RGB,
222
    }
223
42.6k
}
224
225
8.53k
fn new_zune_decoder(
226
8.53k
    input: &[u8],
227
8.53k
    orig_color_space: ZuneColorSpace,
228
8.53k
    strict_mode: bool,
229
8.53k
    limits: Limits,
230
8.53k
) -> zune_jpeg::JpegDecoder<ZCursor<&[u8]>> {
231
8.53k
    let target_color_space = to_supported_color_space(orig_color_space);
232
8.53k
    let mut options = zune_core::options::DecoderOptions::default()
233
8.53k
        .jpeg_set_out_colorspace(target_color_space)
234
8.53k
        .set_strict_mode(strict_mode);
235
8.53k
    options = options.set_max_width(match limits.max_image_width {
236
0
        Some(max_width) => max_width as usize, // u32 to usize never truncates
237
8.53k
        None => usize::MAX,
238
    });
239
8.53k
    options = options.set_max_height(match limits.max_image_height {
240
0
        Some(max_height) => max_height as usize, // u32 to usize never truncates
241
8.53k
        None => usize::MAX,
242
    });
243
8.53k
    zune_jpeg::JpegDecoder::new_with_options(ZCursor::new(input), options)
244
8.53k
}
245
246
impl ImageError {
247
6.79k
    fn from_jpeg(err: zune_jpeg::errors::DecodeErrors) -> ImageError {
248
        use zune_jpeg::errors::DecodeErrors::*;
249
6.79k
        match err {
250
0
            Unsupported(desc) => ImageError::Unsupported(UnsupportedError::from_format_and_kind(
251
0
                ImageFormat::Jpeg.into(),
252
0
                UnsupportedErrorKind::GenericFeature(format!("{desc:?}")),
253
0
            )),
254
0
            LargeDimensions(_) => ImageError::Limits(LimitError::from_kind(
255
0
                crate::error::LimitErrorKind::DimensionError,
256
0
            )),
257
6.79k
            err => ImageError::Decoding(DecodingError::new(ImageFormat::Jpeg.into(), err)),
258
        }
259
6.79k
    }
260
}
261
262
#[cfg(test)]
263
mod tests {
264
    use super::*;
265
    use std::{fs, io::Cursor};
266
267
    #[test]
268
    fn test_exif_orientation() {
269
        let data = fs::read("tests/images/jpg/portrait_2.jpg").unwrap();
270
        let mut decoder = JpegDecoder::new(Cursor::new(data)).unwrap();
271
        assert_eq!(decoder.orientation().unwrap(), Orientation::FlipHorizontal);
272
    }
273
274
    #[test]
275
    fn test_strict_vs_lenient_spec_compliance() {
276
        let mut image = fs::read("tests/images/jpg/progressive/cat.jpg").unwrap();
277
        image.truncate(image.len() - 1000); // simulate a truncated image
278
279
        // Default (lenient) mode: truncated image should be accepted
280
        let decoder = JpegDecoder::new(Cursor::new(&image)).unwrap();
281
        let mut buffer = vec![0u8; decoder.total_bytes() as usize];
282
        assert!(decoder.read_image(&mut buffer).is_ok());
283
284
        // Strict mode: truncated image should be rejected
285
        let decoder =
286
            JpegDecoder::new_with_spec_compliance(Cursor::new(&image), SpecCompliance::Strict)
287
                .unwrap();
288
        let mut buffer = vec![0u8; decoder.total_bytes() as usize];
289
        assert!(decoder.read_image(&mut buffer).is_err());
290
    }
291
}