Skip to main content

ai_image/codecs/hdr/
decoder.rs

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