image/codecs/hdr/
decoder.rs

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