Skip to main content

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