image/codecs/hdr/
decoder.rs

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