/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 | | } |