/src/image/src/codecs/hdr/decoder.rs
Line | Count | Source |
1 | | use std::io::{self, Read}; |
2 | | |
3 | | use std::num::{ParseFloatError, ParseIntError}; |
4 | | use std::{error, fmt}; |
5 | | |
6 | | use crate::error::{ |
7 | | DecodingError, ImageError, ImageFormatHint, ImageResult, UnsupportedError, UnsupportedErrorKind, |
8 | | }; |
9 | | use crate::io::image_reader_type::SpecCompliance; |
10 | | use crate::{ColorType, ImageDecoder, ImageFormat, Limits, Rgb}; |
11 | | |
12 | | /// Errors that can occur during decoding and parsing of a HDR image |
13 | | #[derive(Debug, Clone, PartialEq, Eq)] |
14 | | enum DecoderError { |
15 | | /// HDR's "#?RADIANCE" signature wrong or missing |
16 | | RadianceHdrSignatureInvalid, |
17 | | /// EOF before end of header |
18 | | TruncatedHeader, |
19 | | /// EOF instead of image dimensions |
20 | | TruncatedDimensions, |
21 | | /// The end of the header, if it exists, is far enough in the file that |
22 | | /// this is unlikely to be a valid image |
23 | | HeaderTooLong, |
24 | | |
25 | | /// A value couldn't be parsed |
26 | | UnparsableF32(LineType, ParseFloatError), |
27 | | /// A value couldn't be parsed |
28 | | UnparsableU32(LineType, ParseIntError), |
29 | | /// Not enough numbers in line |
30 | | LineTooShort(LineType), |
31 | | |
32 | | /// COLORCORR contains too many numbers in strict mode |
33 | | ExtraneousColorcorrNumbers, |
34 | | |
35 | | /// Dimensions line had too few elements |
36 | | DimensionsLineTooShort(usize, usize), |
37 | | /// Dimensions line had too many elements |
38 | | DimensionsLineTooLong(usize), |
39 | | |
40 | | /// The length of a scanline (1) wasn't a match for the specified length (2) |
41 | | WrongScanlineLength(usize, usize), |
42 | | /// A chain of run length instructions would produce a too large run |
43 | | OverlyLongRepeat, |
44 | | /// First pixel of a scanline is a run length marker |
45 | | FirstPixelRlMarker, |
46 | | } |
47 | | |
48 | | impl fmt::Display for DecoderError { |
49 | 0 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
50 | 0 | match self { |
51 | | DecoderError::RadianceHdrSignatureInvalid => { |
52 | 0 | f.write_str("Radiance HDR signature not found") |
53 | | } |
54 | 0 | DecoderError::TruncatedHeader => f.write_str("EOF in header"), |
55 | 0 | DecoderError::TruncatedDimensions => f.write_str("EOF in dimensions line"), |
56 | 0 | DecoderError::HeaderTooLong => f.write_fmt(format_args!( |
57 | 0 | "Header end not in the first {MAX_HEADER_LENGTH} bytes, unlikely to be valid image" |
58 | | )), |
59 | 0 | DecoderError::UnparsableF32(line, pe) => { |
60 | 0 | f.write_fmt(format_args!("Cannot parse {line} value as f32: {pe}")) |
61 | | } |
62 | 0 | DecoderError::UnparsableU32(line, pe) => { |
63 | 0 | f.write_fmt(format_args!("Cannot parse {line} value as u32: {pe}")) |
64 | | } |
65 | 0 | DecoderError::LineTooShort(line) => { |
66 | 0 | f.write_fmt(format_args!("Not enough numbers in {line}")) |
67 | | } |
68 | 0 | DecoderError::ExtraneousColorcorrNumbers => f.write_str("Extra numbers in COLORCORR"), |
69 | 0 | DecoderError::DimensionsLineTooShort(elements, expected) => f.write_fmt(format_args!( |
70 | 0 | "Dimensions line too short: have {elements} elements, expected {expected}" |
71 | | )), |
72 | 0 | DecoderError::DimensionsLineTooLong(expected) => f.write_fmt(format_args!( |
73 | 0 | "Dimensions line too long, expected {expected} elements" |
74 | | )), |
75 | 0 | DecoderError::WrongScanlineLength(len, expected) => f.write_fmt(format_args!( |
76 | 0 | "Wrong length of decoded scanline: got {len}, expected {expected}" |
77 | | )), |
78 | 0 | DecoderError::OverlyLongRepeat => f.write_str( |
79 | 0 | "Sequence of run length markers produces run longer than max image width", |
80 | | ), |
81 | | DecoderError::FirstPixelRlMarker => { |
82 | 0 | f.write_str("First pixel of a scanline shouldn't be run length marker") |
83 | | } |
84 | | } |
85 | 0 | } |
86 | | } |
87 | | |
88 | | impl From<DecoderError> for ImageError { |
89 | 3.70k | fn from(e: DecoderError) -> ImageError { |
90 | 3.70k | ImageError::Decoding(DecodingError::new(ImageFormat::Hdr.into(), e)) |
91 | 3.70k | } |
92 | | } |
93 | | |
94 | | impl error::Error for DecoderError { |
95 | 0 | fn source(&self) -> Option<&(dyn error::Error + 'static)> { |
96 | 0 | match self { |
97 | 0 | DecoderError::UnparsableF32(_, err) => Some(err), |
98 | 0 | DecoderError::UnparsableU32(_, err) => Some(err), |
99 | 0 | _ => None, |
100 | | } |
101 | 0 | } |
102 | | } |
103 | | |
104 | | /// Lines which contain parsable data that can fail |
105 | | #[derive(Debug, Copy, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)] |
106 | | enum LineType { |
107 | | Exposure, |
108 | | Pixaspect, |
109 | | Colorcorr, |
110 | | DimensionsHeight, |
111 | | DimensionsWidth, |
112 | | } |
113 | | |
114 | | impl fmt::Display for LineType { |
115 | 0 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
116 | 0 | f.write_str(match self { |
117 | 0 | LineType::Exposure => "EXPOSURE", |
118 | 0 | LineType::Pixaspect => "PIXASPECT", |
119 | 0 | LineType::Colorcorr => "COLORCORR", |
120 | 0 | LineType::DimensionsHeight => "height dimension", |
121 | 0 | LineType::DimensionsWidth => "width dimension", |
122 | | }) |
123 | 0 | } |
124 | | } |
125 | | |
126 | | /// Radiance HDR file signature |
127 | | pub const SIGNATURE: &[u8] = b"#?RADIANCE"; |
128 | | const SIGNATURE_LENGTH: usize = 10; |
129 | | |
130 | | /// An arbitrary and generous limit on the length of the image header. |
131 | | /// |
132 | | /// The HdrDecoder retains essentially the entire header in memory, because any |
133 | | /// line could be a custom attribute, so a limit is useful to avoid allocating |
134 | | /// too much. |
135 | | /// |
136 | | /// Older images produced by Radiance tools often included the commands used to |
137 | | /// generate the image;in particular, for composite images this could grow |
138 | | /// rather large: some historical images have headers of up to 2-3 kilobytes. |
139 | | const MAX_HEADER_LENGTH: usize = 1 << 16; |
140 | | |
141 | | /// An Radiance HDR decoder |
142 | | #[derive(Debug)] |
143 | | pub struct HdrDecoder<R> { |
144 | | r: R, |
145 | | meta: HdrMetadata, |
146 | | } |
147 | | |
148 | | /// Refer to [wikipedia](https://en.wikipedia.org/wiki/RGBE_image_format) |
149 | | #[repr(C)] |
150 | | #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] |
151 | | pub(crate) struct Rgbe8Pixel { |
152 | | /// Color components |
153 | | pub(crate) c: [u8; 3], |
154 | | /// Exponent |
155 | | pub(crate) e: u8, |
156 | | } |
157 | | |
158 | | /// Creates `Rgbe8Pixel` from components |
159 | 0 | pub(crate) fn rgbe8(r: u8, g: u8, b: u8, e: u8) -> Rgbe8Pixel { |
160 | 0 | Rgbe8Pixel { c: [r, g, b], e } |
161 | 0 | } |
162 | | |
163 | | impl Rgbe8Pixel { |
164 | | /// Converts `Rgbe8Pixel` into `Rgb<f32>` linearly |
165 | | #[inline] |
166 | 102M | pub(crate) fn to_hdr(self) -> Rgb<f32> { |
167 | | // Directly construct the exponent 2.0^{e - 128 - 8}; because the normal |
168 | | // exponent value range of f32, 1..=254, is slightly smaller than the |
169 | | // range for rgbe8 (1..=255), a special case is needed to create the |
170 | | // subnormal intermediate value 2^{e - 128} for e=1; the branch also |
171 | | // implements the special case mapping of e=0 to exp=0.0. |
172 | 102M | let exp = f32::from_bits(if self.e > 1 { |
173 | 84.7M | ((self.e - 1) as u32) << 23 |
174 | | } else { |
175 | 17.6M | (self.e as u32) << 22 |
176 | | }) * 0.00390625; |
177 | | |
178 | 102M | Rgb([ |
179 | 102M | exp * <f32 as From<_>>::from(self.c[0]), |
180 | 102M | exp * <f32 as From<_>>::from(self.c[1]), |
181 | 102M | exp * <f32 as From<_>>::from(self.c[2]), |
182 | 102M | ]) |
183 | 102M | } |
184 | | } |
185 | | |
186 | | impl<R: Read> HdrDecoder<R> { |
187 | | /// Reads Radiance HDR image header from stream ```r``` |
188 | | /// if the header is valid, creates `HdrDecoder` |
189 | | /// strict mode is enabled |
190 | 0 | pub fn new(reader: R) -> ImageResult<Self> { |
191 | 0 | HdrDecoder::with_strictness(reader, true) |
192 | 0 | } |
193 | | |
194 | | /// Allows reading old Radiance HDR images |
195 | | #[deprecated(note = "Use `new_with_spec_compliance(reader, SpecCompliance::Lenient)` instead")] |
196 | 0 | pub fn new_nonstrict(reader: R) -> ImageResult<Self> { |
197 | 0 | Self::with_strictness(reader, false) |
198 | 0 | } |
199 | | |
200 | | /// Create a new decoder with the given spec compliance mode. |
201 | 4.13k | pub(crate) fn new_with_spec_compliance(reader: R, spec: SpecCompliance) -> ImageResult<Self> { |
202 | 4.13k | Self::with_strictness(reader, spec == SpecCompliance::Strict) |
203 | 4.13k | } |
204 | | |
205 | | /// Reads Radiance HDR image header from stream `reader`, |
206 | | /// if the header is valid, creates `HdrDecoder`. |
207 | | /// |
208 | | /// strict enables strict mode |
209 | | /// |
210 | | /// Warning! Reading wrong file in non-strict mode could consume up to a few |
211 | | /// megabytes of memory before this errors, if the file is large enough. |
212 | 4.13k | pub fn with_strictness(mut reader: R, strict: bool) -> ImageResult<HdrDecoder<R>> { |
213 | 4.13k | let mut attributes = HdrMetadata::new(); |
214 | | |
215 | | // Limit the total header length, ensuring that the total memory allocated |
216 | | // for lines and is not much more than a constant multiple of the length. |
217 | | // Because a new entry in attributes.custom_attributes is made for each |
218 | | // line, the constant may be quite large (at least `size_of::<String>()`, |
219 | | // likely no more than 100 depending on allocation overhead.); but even |
220 | | // so, in total no more than a few MB will be allocated. |
221 | 4.13k | let mut remaining_limit = MAX_HEADER_LENGTH; |
222 | | { |
223 | | // scope to make borrowck happy |
224 | 4.13k | let r = &mut reader; |
225 | 4.13k | if strict { |
226 | 0 | let mut signature = [0; SIGNATURE_LENGTH]; |
227 | 0 | r.read_exact(&mut signature)?; |
228 | 0 | if signature != SIGNATURE { |
229 | 0 | return Err(DecoderError::RadianceHdrSignatureInvalid.into()); |
230 | 0 | } // no else |
231 | | // skip signature line ending |
232 | 0 | read_line_u8(r, remaining_limit)?; |
233 | 4.13k | } else { |
234 | 4.13k | // Old Radiance HDR files (*.pic) don't use signature |
235 | 4.13k | // Let them be parsed in non-strict mode |
236 | 4.13k | } |
237 | | // read header data until empty line |
238 | | loop { |
239 | 371k | match read_line_u8(r, remaining_limit)? { |
240 | | None => { |
241 | | // EOF before end of header |
242 | 1.39k | return Err(DecoderError::TruncatedHeader.into()); |
243 | | } |
244 | 370k | Some(line) => { |
245 | 370k | remaining_limit = remaining_limit.saturating_sub(line.len() + 1); |
246 | | |
247 | 370k | if line.is_empty() { |
248 | | // end of header |
249 | 2.48k | break; |
250 | 367k | } else if line[0] == b'#' { |
251 | | // line[0] will not panic, line.len() == 0 is false here |
252 | | // skip comments |
253 | 2.55k | continue; |
254 | 365k | } // no else |
255 | | // process attribute line |
256 | 365k | let line = String::from_utf8_lossy(&line[..]); |
257 | 365k | attributes.update_header_info(&line, strict)?; |
258 | | } // <= Some(line) |
259 | | } // match read_line_u8() |
260 | | } // loop |
261 | | } // scope to end borrow of reader |
262 | | // parse dimensions |
263 | 2.48k | let (width, height) = match read_line_u8(&mut reader, remaining_limit)? { |
264 | | None => { |
265 | | // EOF instead of image dimensions |
266 | 2 | return Err(DecoderError::TruncatedDimensions.into()); |
267 | | } |
268 | 2.47k | Some(dimensions) => { |
269 | 2.47k | let dimensions = String::from_utf8_lossy(&dimensions[..]); |
270 | 2.47k | parse_dimensions_line(&dimensions, strict)? |
271 | | } |
272 | | }; |
273 | | |
274 | | // color type is always rgb8 |
275 | 1.62k | if crate::utils::check_dimension_overflow(width, height, ColorType::Rgb8.bytes_per_pixel()) |
276 | | { |
277 | 1 | return Err(ImageError::Unsupported( |
278 | 1 | UnsupportedError::from_format_and_kind( |
279 | 1 | ImageFormat::Hdr.into(), |
280 | 1 | UnsupportedErrorKind::GenericFeature(format!( |
281 | 1 | "Image dimensions ({width}x{height}) are too large" |
282 | 1 | )), |
283 | 1 | ), |
284 | 1 | )); |
285 | 1.62k | } |
286 | | |
287 | 1.62k | Ok(HdrDecoder { |
288 | 1.62k | r: reader, |
289 | 1.62k | |
290 | 1.62k | meta: HdrMetadata { |
291 | 1.62k | width, |
292 | 1.62k | height, |
293 | 1.62k | ..attributes |
294 | 1.62k | }, |
295 | 1.62k | }) |
296 | 4.13k | } // end with_strictness |
297 | | |
298 | | /// Returns file metadata. Refer to `HdrMetadata` for details. |
299 | 0 | pub fn metadata(&self) -> HdrMetadata { |
300 | 0 | self.meta.clone() |
301 | 0 | } |
302 | | } |
303 | | |
304 | | impl<R: Read> ImageDecoder for HdrDecoder<R> { |
305 | 6.09k | fn dimensions(&self) -> (u32, u32) { |
306 | 6.09k | (self.meta.width, self.meta.height) |
307 | 6.09k | } |
308 | | |
309 | 6.09k | fn color_type(&self) -> ColorType { |
310 | 6.09k | ColorType::Rgb32F |
311 | 6.09k | } |
312 | | |
313 | 1.50k | fn set_limits(&mut self, mut limits: Limits) -> ImageResult<()> { |
314 | 1.50k | limits.check_support(&crate::LimitSupport::default())?; |
315 | 1.50k | limits.check_dimensions(self.meta.width, self.meta.height)?; |
316 | | |
317 | 1.50k | let scanline_space = (self.meta.width as u64) * (size_of::<Rgbe8Pixel>() as u64); |
318 | 1.50k | limits.reserve(scanline_space)?; |
319 | 1.49k | Ok(()) |
320 | 1.50k | } |
321 | | |
322 | 1.49k | fn read_image(mut self, buf: &mut [u8]) -> ImageResult<()> { |
323 | 1.49k | assert_eq!(u64::try_from(buf.len()), Ok(self.total_bytes())); |
324 | | |
325 | | // Don't read anything if image is empty |
326 | 1.49k | if self.meta.width == 0 || self.meta.height == 0 { |
327 | 48 | return Ok(()); |
328 | 1.44k | } |
329 | | |
330 | 1.44k | let mut scanline = vec![Default::default(); self.meta.width as usize]; |
331 | | |
332 | | const PIXEL_SIZE: usize = size_of::<Rgb<f32>>(); |
333 | 1.44k | let line_bytes = self.meta.width as usize * PIXEL_SIZE; |
334 | | |
335 | 1.44k | let chunks_iter = buf.chunks_exact_mut(line_bytes); |
336 | 127k | for chunk in chunks_iter { |
337 | | // read_scanline overwrites the entire buffer or returns an Err, |
338 | | // so not resetting the buffer here is ok. |
339 | 126k | read_scanline(&mut self.r, &mut scanline[..])?; |
340 | 125k | let dst_chunks = chunk.as_chunks_mut::<PIXEL_SIZE>().0.iter_mut(); |
341 | 102M | for (dst, &pix) in dst_chunks.zip(scanline.iter()) { |
342 | 102M | dst.copy_from_slice(bytemuck::cast_slice(&pix.to_hdr().0)); |
343 | 102M | } |
344 | | } |
345 | | |
346 | 190 | Ok(()) |
347 | 1.49k | } |
348 | | |
349 | 1.49k | fn read_image_boxed(self: Box<Self>, buf: &mut [u8]) -> ImageResult<()> { |
350 | 1.49k | (*self).read_image(buf) |
351 | 1.49k | } |
352 | | } |
353 | | |
354 | | // Precondition: buf.len() > 0 |
355 | 126k | fn read_scanline<R: Read>(r: &mut R, buf: &mut [Rgbe8Pixel]) -> ImageResult<()> { |
356 | 126k | assert!(!buf.is_empty()); |
357 | 126k | let width = buf.len(); |
358 | | // first 4 bytes in scanline allow to determine compression method |
359 | 126k | let fb = read_rgbe(r)?; |
360 | 126k | if fb.c[0] == 2 && fb.c[1] == 2 && fb.c[2] < 128 { |
361 | | // denormalized pixel value (2,2,<128,_) indicates new per component RLE method |
362 | | // decode_component guarantees that offset is within 0 .. width |
363 | | // therefore we can skip bounds checking here, but we will not |
364 | 164M | decode_component(r, width, |offset, value| buf[offset].c[0] = value)?; |
365 | 131M | decode_component(r, width, |offset, value| buf[offset].c[1] = value)?; |
366 | 82.5M | decode_component(r, width, |offset, value| buf[offset].c[2] = value)?; |
367 | 29.8M | decode_component(r, width, |offset, value| buf[offset].e = value)?; |
368 | | } else { |
369 | | // old RLE method (it was considered old around 1991, should it be here?) |
370 | 124k | decode_old_rle(r, fb, buf)?; |
371 | | } |
372 | 125k | Ok(()) |
373 | 126k | } |
374 | | |
375 | | #[inline(always)] |
376 | 8.71M | fn read_byte<R: Read>(r: &mut R) -> io::Result<u8> { |
377 | 8.71M | let mut buf = [0u8]; |
378 | 8.71M | r.read_exact(&mut buf[..])?; |
379 | 8.71M | Ok(buf[0]) |
380 | 8.71M | } |
381 | | |
382 | | // Guarantees that first parameter of set_component will be within pos .. pos+width |
383 | | #[inline] |
384 | 8.72k | fn decode_component<R: Read, S: FnMut(usize, u8)>( |
385 | 8.72k | r: &mut R, |
386 | 8.72k | width: usize, |
387 | 8.72k | mut set_component: S, |
388 | 8.72k | ) -> ImageResult<()> { |
389 | 8.72k | let mut buf = [0; 128]; |
390 | 8.72k | let mut pos = 0; |
391 | 4.51M | while pos < width { |
392 | | // increment position by a number of decompressed values |
393 | | pos += { |
394 | 4.51M | let rl = read_byte(r)?; |
395 | 4.51M | if rl <= 128 { |
396 | | // sanity check |
397 | 306k | if pos + rl as usize > width { |
398 | 134 | return Err(DecoderError::WrongScanlineLength(pos + rl as usize, width).into()); |
399 | 306k | } |
400 | | // read values |
401 | 306k | r.read_exact(&mut buf[0..rl as usize])?; |
402 | 4.22M | for (offset, &value) in buf[0..rl as usize].iter().enumerate() { |
403 | 4.22M | set_component(pos + offset, value); |
404 | 4.22M | } |
405 | 306k | rl as usize |
406 | | } else { |
407 | | // run |
408 | 4.20M | let rl = rl - 128; |
409 | | // sanity check |
410 | 4.20M | if pos + rl as usize > width { |
411 | 97 | return Err(DecoderError::WrongScanlineLength(pos + rl as usize, width).into()); |
412 | 4.20M | } |
413 | | // fill with same value |
414 | 4.20M | let value = read_byte(r)?; |
415 | 403M | for offset in 0..rl as usize { |
416 | 403M | set_component(pos + offset, value); |
417 | 403M | } |
418 | 4.20M | rl as usize |
419 | | } |
420 | | }; |
421 | | } |
422 | 7.94k | if pos != width { |
423 | 0 | return Err(DecoderError::WrongScanlineLength(pos, width).into()); |
424 | 7.94k | } |
425 | 7.94k | Ok(()) |
426 | 8.72k | } image::codecs::hdr::decoder::decode_component::<std::io::cursor::Cursor<&[u8]>, image::codecs::hdr::decoder::read_scanline<std::io::cursor::Cursor<&[u8]>>::{closure#0}>Line | Count | Source | 384 | 2.46k | fn decode_component<R: Read, S: FnMut(usize, u8)>( | 385 | 2.46k | r: &mut R, | 386 | 2.46k | width: usize, | 387 | 2.46k | mut set_component: S, | 388 | 2.46k | ) -> ImageResult<()> { | 389 | 2.46k | let mut buf = [0; 128]; | 390 | 2.46k | let mut pos = 0; | 391 | 1.84M | while pos < width { | 392 | | // increment position by a number of decompressed values | 393 | | pos += { | 394 | 1.84M | let rl = read_byte(r)?; | 395 | 1.84M | if rl <= 128 { | 396 | | // sanity check | 397 | 132k | if pos + rl as usize > width { | 398 | 23 | return Err(DecoderError::WrongScanlineLength(pos + rl as usize, width).into()); | 399 | 131k | } | 400 | | // read values | 401 | 131k | r.read_exact(&mut buf[0..rl as usize])?; | 402 | 1.85M | for (offset, &value) in buf[0..rl as usize].iter().enumerate() { | 403 | 1.85M | set_component(pos + offset, value); | 404 | 1.85M | } | 405 | 131k | rl as usize | 406 | | } else { | 407 | | // run | 408 | 1.70M | let rl = rl - 128; | 409 | | // sanity check | 410 | 1.70M | if pos + rl as usize > width { | 411 | 25 | return Err(DecoderError::WrongScanlineLength(pos + rl as usize, width).into()); | 412 | 1.70M | } | 413 | | // fill with same value | 414 | 1.70M | let value = read_byte(r)?; | 415 | 162M | for offset in 0..rl as usize { | 416 | 162M | set_component(pos + offset, value); | 417 | 162M | } | 418 | 1.70M | rl as usize | 419 | | } | 420 | | }; | 421 | | } | 422 | 2.28k | if pos != width { | 423 | 0 | return Err(DecoderError::WrongScanlineLength(pos, width).into()); | 424 | 2.28k | } | 425 | 2.28k | Ok(()) | 426 | 2.46k | } |
image::codecs::hdr::decoder::decode_component::<std::io::cursor::Cursor<&[u8]>, image::codecs::hdr::decoder::read_scanline<std::io::cursor::Cursor<&[u8]>>::{closure#2}>Line | Count | Source | 384 | 2.09k | fn decode_component<R: Read, S: FnMut(usize, u8)>( | 385 | 2.09k | r: &mut R, | 386 | 2.09k | width: usize, | 387 | 2.09k | mut set_component: S, | 388 | 2.09k | ) -> ImageResult<()> { | 389 | 2.09k | let mut buf = [0; 128]; | 390 | 2.09k | let mut pos = 0; | 391 | 912k | while pos < width { | 392 | | // increment position by a number of decompressed values | 393 | | pos += { | 394 | 910k | let rl = read_byte(r)?; | 395 | 910k | if rl <= 128 { | 396 | | // sanity check | 397 | 57.3k | if pos + rl as usize > width { | 398 | 45 | return Err(DecoderError::WrongScanlineLength(pos + rl as usize, width).into()); | 399 | 57.3k | } | 400 | | // read values | 401 | 57.3k | r.read_exact(&mut buf[0..rl as usize])?; | 402 | 807k | for (offset, &value) in buf[0..rl as usize].iter().enumerate() { | 403 | 807k | set_component(pos + offset, value); | 404 | 807k | } | 405 | 57.2k | rl as usize | 406 | | } else { | 407 | | // run | 408 | 853k | let rl = rl - 128; | 409 | | // sanity check | 410 | 853k | if pos + rl as usize > width { | 411 | 29 | return Err(DecoderError::WrongScanlineLength(pos + rl as usize, width).into()); | 412 | 853k | } | 413 | | // fill with same value | 414 | 853k | let value = read_byte(r)?; | 415 | 81.6M | for offset in 0..rl as usize { | 416 | 81.6M | set_component(pos + offset, value); | 417 | 81.6M | } | 418 | 853k | rl as usize | 419 | | } | 420 | | }; | 421 | | } | 422 | 1.88k | if pos != width { | 423 | 0 | return Err(DecoderError::WrongScanlineLength(pos, width).into()); | 424 | 1.88k | } | 425 | 1.88k | Ok(()) | 426 | 2.09k | } |
image::codecs::hdr::decoder::decode_component::<std::io::cursor::Cursor<&[u8]>, image::codecs::hdr::decoder::read_scanline<std::io::cursor::Cursor<&[u8]>>::{closure#3}>Line | Count | Source | 384 | 1.88k | fn decode_component<R: Read, S: FnMut(usize, u8)>( | 385 | 1.88k | r: &mut R, | 386 | 1.88k | width: usize, | 387 | 1.88k | mut set_component: S, | 388 | 1.88k | ) -> ImageResult<()> { | 389 | 1.88k | let mut buf = [0; 128]; | 390 | 1.88k | let mut pos = 0; | 391 | 321k | while pos < width { | 392 | | // increment position by a number of decompressed values | 393 | | pos += { | 394 | 320k | let rl = read_byte(r)?; | 395 | 320k | if rl <= 128 { | 396 | | // sanity check | 397 | 16.0k | if pos + rl as usize > width { | 398 | 43 | return Err(DecoderError::WrongScanlineLength(pos + rl as usize, width).into()); | 399 | 15.9k | } | 400 | | // read values | 401 | 15.9k | r.read_exact(&mut buf[0..rl as usize])?; | 402 | 321k | for (offset, &value) in buf[0..rl as usize].iter().enumerate() { | 403 | 321k | set_component(pos + offset, value); | 404 | 321k | } | 405 | 15.9k | rl as usize | 406 | | } else { | 407 | | // run | 408 | 304k | let rl = rl - 128; | 409 | | // sanity check | 410 | 304k | if pos + rl as usize > width { | 411 | 25 | return Err(DecoderError::WrongScanlineLength(pos + rl as usize, width).into()); | 412 | 304k | } | 413 | | // fill with same value | 414 | 304k | let value = read_byte(r)?; | 415 | 29.5M | for offset in 0..rl as usize { | 416 | 29.5M | set_component(pos + offset, value); | 417 | 29.5M | } | 418 | 304k | rl as usize | 419 | | } | 420 | | }; | 421 | | } | 422 | 1.67k | if pos != width { | 423 | 0 | return Err(DecoderError::WrongScanlineLength(pos, width).into()); | 424 | 1.67k | } | 425 | 1.67k | Ok(()) | 426 | 1.88k | } |
image::codecs::hdr::decoder::decode_component::<std::io::cursor::Cursor<&[u8]>, image::codecs::hdr::decoder::read_scanline<std::io::cursor::Cursor<&[u8]>>::{closure#1}>Line | Count | Source | 384 | 2.28k | fn decode_component<R: Read, S: FnMut(usize, u8)>( | 385 | 2.28k | r: &mut R, | 386 | 2.28k | width: usize, | 387 | 2.28k | mut set_component: S, | 388 | 2.28k | ) -> ImageResult<()> { | 389 | 2.28k | let mut buf = [0; 128]; | 390 | 2.28k | let mut pos = 0; | 391 | 1.44M | while pos < width { | 392 | | // increment position by a number of decompressed values | 393 | | pos += { | 394 | 1.43M | let rl = read_byte(r)?; | 395 | 1.43M | if rl <= 128 { | 396 | | // sanity check | 397 | 101k | if pos + rl as usize > width { | 398 | 23 | return Err(DecoderError::WrongScanlineLength(pos + rl as usize, width).into()); | 399 | 101k | } | 400 | | // read values | 401 | 101k | r.read_exact(&mut buf[0..rl as usize])?; | 402 | 1.24M | for (offset, &value) in buf[0..rl as usize].iter().enumerate() { | 403 | 1.24M | set_component(pos + offset, value); | 404 | 1.24M | } | 405 | 101k | rl as usize | 406 | | } else { | 407 | | // run | 408 | 1.33M | let rl = rl - 128; | 409 | | // sanity check | 410 | 1.33M | if pos + rl as usize > width { | 411 | 18 | return Err(DecoderError::WrongScanlineLength(pos + rl as usize, width).into()); | 412 | 1.33M | } | 413 | | // fill with same value | 414 | 1.33M | let value = read_byte(r)?; | 415 | 129M | for offset in 0..rl as usize { | 416 | 129M | set_component(pos + offset, value); | 417 | 129M | } | 418 | 1.33M | rl as usize | 419 | | } | 420 | | }; | 421 | | } | 422 | 2.09k | if pos != width { | 423 | 0 | return Err(DecoderError::WrongScanlineLength(pos, width).into()); | 424 | 2.09k | } | 425 | 2.09k | Ok(()) | 426 | 2.28k | } |
|
427 | | |
428 | | // Decodes scanline, places it into buf |
429 | | // Precondition: buf.len() > 0 |
430 | | // fb - first 4 bytes of scanline |
431 | 124k | fn decode_old_rle<R: Read>(r: &mut R, fb: Rgbe8Pixel, buf: &mut [Rgbe8Pixel]) -> ImageResult<()> { |
432 | 124k | assert!(!buf.is_empty()); |
433 | 124k | let width = buf.len(); |
434 | | // convenience function. |
435 | | // returns run length if pixel is a run length marker |
436 | | #[inline] |
437 | 393k | fn rl_marker(pix: Rgbe8Pixel) -> Option<usize> { |
438 | 393k | if pix.c == [1, 1, 1] { |
439 | 2.10k | Some(pix.e as usize) |
440 | | } else { |
441 | 391k | None |
442 | | } |
443 | 393k | } |
444 | | // first pixel in scanline should not be run length marker |
445 | | // it is error if it is |
446 | 124k | if rl_marker(fb).is_some() { |
447 | 7 | return Err(DecoderError::FirstPixelRlMarker.into()); |
448 | 124k | } |
449 | 124k | buf[0] = fb; // set first pixel of scanline |
450 | | |
451 | 124k | let mut x_off = 1; // current offset from beginning of a scanline |
452 | 124k | let mut rl_shift: u32 = 0; // current run length shift value |
453 | 124k | let mut prev_pixel = fb; |
454 | 393k | while x_off < width { |
455 | 269k | let pix = read_rgbe(r)?; |
456 | | // it's harder to forget to increase x_off if I write this this way. |
457 | | x_off += { |
458 | 269k | if let Some(rl) = rl_marker(pix) { |
459 | 2.10k | if rl_shift >= 32 { |
460 | 196 | if rl != 0 { |
461 | | // run length of >= 2^32 rl |
462 | 1 | return Err(DecoderError::OverlyLongRepeat.into()); |
463 | | } else { |
464 | 195 | 0 |
465 | | } |
466 | | } else { |
467 | | // rl_shift takes care of consecutive RL markers |
468 | 1.90k | let rl = rl << rl_shift; |
469 | 1.90k | rl_shift += 8; |
470 | 1.90k | if x_off + rl <= width { |
471 | | // do run |
472 | 176M | for b in &mut buf[x_off..x_off + rl] { |
473 | 176M | *b = prev_pixel; |
474 | 176M | } |
475 | | } else { |
476 | 43 | return Err(DecoderError::WrongScanlineLength(x_off + rl, width).into()); |
477 | | }; |
478 | 1.86k | rl // value to increase x_off by |
479 | | } |
480 | | } else { |
481 | 267k | rl_shift = 0; // chain of consecutive RL markers is broken |
482 | 267k | prev_pixel = pix; |
483 | 267k | buf[x_off] = pix; |
484 | 267k | 1 // value to increase x_off by |
485 | | } |
486 | | }; |
487 | | } |
488 | 123k | if x_off != width { |
489 | 0 | return Err(DecoderError::WrongScanlineLength(x_off, width).into()); |
490 | 123k | } |
491 | 123k | Ok(()) |
492 | 124k | } |
493 | | |
494 | 396k | fn read_rgbe<R: Read>(r: &mut R) -> io::Result<Rgbe8Pixel> { |
495 | 396k | let mut buf = [0u8; 4]; |
496 | 396k | r.read_exact(&mut buf[..])?; |
497 | 396k | Ok(Rgbe8Pixel { |
498 | 396k | c: [buf[0], buf[1], buf[2]], |
499 | 396k | e: buf[3], |
500 | 396k | }) |
501 | 396k | } |
502 | | |
503 | | /// Metadata for Radiance HDR image |
504 | | #[derive(Debug, Clone)] |
505 | | pub struct HdrMetadata { |
506 | | /// Width of decoded image. It could be either scanline length, |
507 | | /// or scanline count, depending on image orientation. |
508 | | pub width: u32, |
509 | | /// Height of decoded image. It depends on orientation too. |
510 | | pub height: u32, |
511 | | /// Orientation matrix. For standard orientation it is ((1,0),(0,1)) - left to right, top to bottom. |
512 | | /// First pair tells how resulting pixel coordinates change along a scanline. |
513 | | /// Second pair tells how they change from one scanline to the next. |
514 | | pub orientation: ((i8, i8), (i8, i8)), |
515 | | /// Divide color values by exposure to get to get physical radiance in |
516 | | /// watts/steradian/m<sup>2</sup> |
517 | | /// |
518 | | /// Image may not contain physical data, even if this field is set. |
519 | | pub exposure: Option<f32>, |
520 | | /// Divide color values by corresponding tuple member (r, g, b) to get to get physical radiance |
521 | | /// in watts/steradian/m<sup>2</sup> |
522 | | /// |
523 | | /// Image may not contain physical data, even if this field is set. |
524 | | pub color_correction: Option<(f32, f32, f32)>, |
525 | | /// Pixel height divided by pixel width |
526 | | pub pixel_aspect_ratio: Option<f32>, |
527 | | /// All lines contained in image header are put here. Ordering of lines is preserved. |
528 | | /// Lines in the form "key=value" are represented as ("key", "value"). |
529 | | /// All other lines are ("", "line") |
530 | | pub custom_attributes: Vec<(String, String)>, |
531 | | } |
532 | | |
533 | | impl HdrMetadata { |
534 | 4.13k | fn new() -> HdrMetadata { |
535 | 4.13k | HdrMetadata { |
536 | 4.13k | width: 0, |
537 | 4.13k | height: 0, |
538 | 4.13k | orientation: ((1, 0), (0, 1)), |
539 | 4.13k | exposure: None, |
540 | 4.13k | color_correction: None, |
541 | 4.13k | pixel_aspect_ratio: None, |
542 | 4.13k | custom_attributes: vec![], |
543 | 4.13k | } |
544 | 4.13k | } |
545 | | |
546 | | // Updates header info, in strict mode returns error for malformed lines (no '=' separator) |
547 | | // unknown attributes are skipped |
548 | 365k | fn update_header_info(&mut self, line: &str, strict: bool) -> ImageResult<()> { |
549 | | // split line at first '=' |
550 | | // old Radiance HDR files (*.pic) feature tabs in key, so vvv trim |
551 | 365k | let maybe_key_value = split_at_first(line, "=").map(|(key, value)| (key.trim(), value)); |
552 | | // save all header lines in custom_attributes |
553 | 365k | match maybe_key_value { |
554 | 24.2k | Some((key, val)) => self |
555 | 24.2k | .custom_attributes |
556 | 24.2k | .push((key.to_owned(), val.to_owned())), |
557 | 341k | None => self |
558 | 341k | .custom_attributes |
559 | 341k | .push((String::new(), line.to_owned())), |
560 | | } |
561 | | // parse known attributes |
562 | 365k | match maybe_key_value { |
563 | 24.2k | Some(("FORMAT", val)) => { |
564 | 483 | if val.trim() != "32-bit_rle_rgbe" { |
565 | | // XYZE isn't supported yet |
566 | 221 | return Err(ImageError::Unsupported( |
567 | 221 | UnsupportedError::from_format_and_kind( |
568 | 221 | ImageFormat::Hdr.into(), |
569 | 221 | UnsupportedErrorKind::Format(ImageFormatHint::Name(limit_string_len( |
570 | 221 | val, 20, |
571 | 221 | ))), |
572 | 221 | ), |
573 | 221 | )); |
574 | 262 | } |
575 | | } |
576 | 23.7k | Some(("EXPOSURE", val)) => { |
577 | 819 | match val.trim().parse::<f32>() { |
578 | 610 | Ok(v) => { |
579 | 610 | self.exposure = Some(self.exposure.unwrap_or(1.0) * v); // all encountered exposure values should be multiplied |
580 | 610 | } |
581 | 209 | Err(parse_error) => { |
582 | 209 | if strict { |
583 | 0 | return Err(DecoderError::UnparsableF32( |
584 | 0 | LineType::Exposure, |
585 | 0 | parse_error, |
586 | 0 | ) |
587 | 0 | .into()); |
588 | 209 | } // no else, skip this line in non-strict mode |
589 | | } |
590 | | } |
591 | | } |
592 | 22.9k | Some(("PIXASPECT", val)) => { |
593 | 1.09k | match val.trim().parse::<f32>() { |
594 | 819 | Ok(v) => { |
595 | 819 | self.pixel_aspect_ratio = Some(self.pixel_aspect_ratio.unwrap_or(1.0) * v); |
596 | 819 | // all encountered exposure values should be multiplied |
597 | 819 | } |
598 | 276 | Err(parse_error) => { |
599 | 276 | if strict { |
600 | 0 | return Err(DecoderError::UnparsableF32( |
601 | 0 | LineType::Pixaspect, |
602 | 0 | parse_error, |
603 | 0 | ) |
604 | 0 | .into()); |
605 | 276 | } // no else, skip this line in non-strict mode |
606 | | } |
607 | | } |
608 | | } |
609 | 21.8k | Some(("COLORCORR", val)) => { |
610 | 2.98k | let mut rgbcorr = [1.0, 1.0, 1.0]; |
611 | 2.98k | match parse_space_separated_f32(val, &mut rgbcorr, LineType::Colorcorr) { |
612 | 1.56k | Ok(extra_numbers) => { |
613 | 1.56k | if strict && extra_numbers { |
614 | 0 | return Err(DecoderError::ExtraneousColorcorrNumbers.into()); |
615 | 1.56k | } // no else, just ignore extra numbers |
616 | 1.56k | let (rc, gc, bc) = self.color_correction.unwrap_or((1.0, 1.0, 1.0)); |
617 | 1.56k | self.color_correction = |
618 | 1.56k | Some((rc * rgbcorr[0], gc * rgbcorr[1], bc * rgbcorr[2])); |
619 | | } |
620 | 1.42k | Err(err) => { |
621 | 1.42k | if strict { |
622 | 0 | return Err(err); |
623 | 1.42k | } // no else, skip malformed line in non-strict mode |
624 | | } |
625 | | } |
626 | | } |
627 | 341k | None => { |
628 | 341k | // old Radiance HDR files (*.pic) contain commands in a header |
629 | 341k | // just skip them |
630 | 341k | } |
631 | 18.8k | _ => { |
632 | 18.8k | // skip unknown attribute |
633 | 18.8k | } |
634 | | } // match attributes |
635 | 365k | Ok(()) |
636 | 365k | } |
637 | | } |
638 | | |
639 | 2.98k | fn parse_space_separated_f32(line: &str, vals: &mut [f32], line_tp: LineType) -> ImageResult<bool> { |
640 | 2.98k | let mut nums = line.split_whitespace(); |
641 | 7.45k | for val in vals.iter_mut() { |
642 | 7.45k | if let Some(num) = nums.next() { |
643 | 6.77k | match num.parse::<f32>() { |
644 | 6.03k | Ok(v) => *val = v, |
645 | 742 | Err(err) => return Err(DecoderError::UnparsableF32(line_tp, err).into()), |
646 | | } |
647 | | } else { |
648 | | // not enough numbers in line |
649 | 679 | return Err(DecoderError::LineTooShort(line_tp).into()); |
650 | | } |
651 | | } |
652 | 1.56k | Ok(nums.next().is_some()) |
653 | 2.98k | } |
654 | | |
655 | | // Parses dimension line "-Y height +X width" |
656 | | // returns (width, height) or error |
657 | 2.47k | fn parse_dimensions_line(line: &str, strict: bool) -> ImageResult<(u32, u32)> { |
658 | | const DIMENSIONS_COUNT: usize = 4; |
659 | | |
660 | 2.47k | let mut dim_parts = line.split_whitespace(); |
661 | 2.47k | let c1_tag = dim_parts |
662 | 2.47k | .next() |
663 | 2.47k | .ok_or(DecoderError::DimensionsLineTooShort(0, DIMENSIONS_COUNT))?; |
664 | 2.40k | let c1_str = dim_parts |
665 | 2.40k | .next() |
666 | 2.40k | .ok_or(DecoderError::DimensionsLineTooShort(1, DIMENSIONS_COUNT))?; |
667 | 2.14k | let c2_tag = dim_parts |
668 | 2.14k | .next() |
669 | 2.14k | .ok_or(DecoderError::DimensionsLineTooShort(2, DIMENSIONS_COUNT))?; |
670 | 2.12k | let c2_str = dim_parts |
671 | 2.12k | .next() |
672 | 2.12k | .ok_or(DecoderError::DimensionsLineTooShort(3, DIMENSIONS_COUNT))?; |
673 | 2.11k | if strict && dim_parts.next().is_some() { |
674 | | // extra data in dimensions line |
675 | 0 | return Err(DecoderError::DimensionsLineTooLong(DIMENSIONS_COUNT).into()); |
676 | 2.11k | } // no else |
677 | | // dimensions line is in the form "-Y 10 +X 20" |
678 | | // There are 8 possible orientations: +Y +X, +X -Y and so on |
679 | 2.11k | match (c1_tag, c2_tag) { |
680 | 2.11k | ("-Y", "+X") => { |
681 | | // Common orientation (left-right, top-down) |
682 | | // c1_str is height, c2_str is width |
683 | 1.82k | let height = c1_str |
684 | 1.82k | .parse::<u32>() |
685 | 1.82k | .map_err(|pe| DecoderError::UnparsableU32(LineType::DimensionsHeight, pe))?; |
686 | 1.73k | let width = c2_str |
687 | 1.73k | .parse::<u32>() |
688 | 1.73k | .map_err(|pe| DecoderError::UnparsableU32(LineType::DimensionsWidth, pe))?; |
689 | 1.62k | Ok((width, height)) |
690 | | } |
691 | 285 | _ => Err(ImageError::Unsupported( |
692 | 285 | UnsupportedError::from_format_and_kind( |
693 | 285 | ImageFormat::Hdr.into(), |
694 | 285 | UnsupportedErrorKind::GenericFeature(format!( |
695 | 285 | "Orientation {} {}", |
696 | 285 | limit_string_len(c1_tag, 4), |
697 | 285 | limit_string_len(c2_tag, 4) |
698 | 285 | )), |
699 | 285 | ), |
700 | 285 | )), |
701 | | } // final expression. Returns value |
702 | 2.47k | } |
703 | | |
704 | | // Returns string with no more than len+3 characters |
705 | 791 | fn limit_string_len(s: &str, len: usize) -> String { |
706 | 791 | let s_char_len = s.chars().count(); |
707 | 791 | if s_char_len > len { |
708 | 372 | s.chars().take(len).chain("...".chars()).collect() |
709 | | } else { |
710 | 419 | s.into() |
711 | | } |
712 | 791 | } |
713 | | |
714 | | // Splits string into (before separator, after separator) tuple |
715 | | // or None if separator isn't found |
716 | 365k | fn split_at_first<'a>(s: &'a str, separator: &str) -> Option<(&'a str, &'a str)> { |
717 | 365k | match s.find(separator) { |
718 | 340k | None | Some(0) => None, |
719 | 24.8k | Some(p) if p >= s.len() - separator.len() => None, |
720 | 24.2k | Some(p) => Some((&s[..p], &s[(p + separator.len())..])), |
721 | | } |
722 | 365k | } |
723 | | |
724 | | // Reads input until b"\n" or EOF |
725 | | // Returns vector of read bytes NOT including end of line characters |
726 | | // or return None to indicate end of file |
727 | | // Returns an error if the line would require more than max_len bytes |
728 | 374k | fn read_line_u8<R: Read>(r: &mut R, max_len: usize) -> ImageResult<Option<Vec<u8>>> { |
729 | | // keeping repeated redundant allocations to avoid added complexity of having a `&mut tmp` argument |
730 | | #[allow(clippy::disallowed_methods)] |
731 | 374k | let mut ret = Vec::with_capacity(16); |
732 | | loop { |
733 | 7.09M | let mut byte = [0]; |
734 | 7.09M | if r.read(&mut byte)? == 0 || byte[0] == b'\n' { |
735 | 374k | if ret.is_empty() && byte[0] != b'\n' { |
736 | 1.39k | return Ok(None); |
737 | 372k | } |
738 | 372k | return Ok(Some(ret)); |
739 | 6.72M | } |
740 | | |
741 | 6.72M | if ret.len() >= max_len { |
742 | 37 | return Err(DecoderError::HeaderTooLong.into()); |
743 | 6.72M | } |
744 | 6.72M | ret.push(byte[0]); |
745 | | } |
746 | 374k | } |
747 | | |
748 | | #[cfg(test)] |
749 | | mod tests { |
750 | | use std::{borrow::Cow, io::Cursor}; |
751 | | |
752 | | use super::*; |
753 | | |
754 | | #[test] |
755 | | fn split_at_first_test() { |
756 | | assert_eq!(split_at_first(&Cow::Owned(String::new()), "="), None); |
757 | | assert_eq!(split_at_first(&Cow::Owned("=".into()), "="), None); |
758 | | assert_eq!(split_at_first(&Cow::Owned("= ".into()), "="), None); |
759 | | assert_eq!( |
760 | | split_at_first(&Cow::Owned(" = ".into()), "="), |
761 | | Some((" ", " ")) |
762 | | ); |
763 | | assert_eq!( |
764 | | split_at_first(&Cow::Owned("EXPOSURE= ".into()), "="), |
765 | | Some(("EXPOSURE", " ")) |
766 | | ); |
767 | | assert_eq!( |
768 | | split_at_first(&Cow::Owned("EXPOSURE= =".into()), "="), |
769 | | Some(("EXPOSURE", " =")) |
770 | | ); |
771 | | assert_eq!( |
772 | | split_at_first(&Cow::Owned("EXPOSURE== =".into()), "=="), |
773 | | Some(("EXPOSURE", " =")) |
774 | | ); |
775 | | assert_eq!(split_at_first(&Cow::Owned("EXPOSURE".into()), ""), None); |
776 | | } |
777 | | |
778 | | #[test] |
779 | | fn read_line_u8_test() { |
780 | | let buf: Vec<_> = (&b"One\nTwo\nThree\nFour\n\n\n"[..]).into(); |
781 | | let input = &mut Cursor::new(buf); |
782 | | let read_line = |input: &mut Cursor<Vec<u8>>| -> Option<Vec<u8>> { |
783 | | read_line_u8(input, usize::MAX).unwrap() |
784 | | }; |
785 | | |
786 | | assert_eq!(&read_line(input).unwrap()[..], &b"One"[..]); |
787 | | assert_eq!(&read_line(input).unwrap()[..], &b"Two"[..]); |
788 | | assert_eq!(&read_line(input).unwrap()[..], &b"Three"[..]); |
789 | | assert_eq!(&read_line(input).unwrap()[..], &b"Four"[..]); |
790 | | assert_eq!(&read_line(input).unwrap()[..], &b""[..]); |
791 | | assert_eq!(&read_line(input).unwrap()[..], &b""[..]); |
792 | | assert_eq!(read_line(input), None); |
793 | | } |
794 | | |
795 | | #[test] |
796 | | fn dimension_overflow() { |
797 | | let data = b"#?RADIANCE\nFORMAT=32-bit_rle_rgbe\n\n -Y 4294967295 +X 4294967295"; |
798 | | |
799 | | assert!(HdrDecoder::new(Cursor::new(data)).is_err()); |
800 | | assert!( |
801 | | HdrDecoder::new_with_spec_compliance(Cursor::new(data), SpecCompliance::Lenient) |
802 | | .is_err() |
803 | | ); |
804 | | } |
805 | | } |