imagesize/formats/
hdr.rs

1use std::io::{self, BufRead, Seek, SeekFrom};
2
3use crate::{util::read_line_capped, ImageResult, ImageSize};
4
5pub fn size<R: BufRead + Seek>(reader: &mut R) -> ImageResult<ImageSize> {
6    reader.seek(SeekFrom::Start(0))?;
7
8    // Read the first line and check if it's a valid HDR format identifier
9    // Only read max of 11 characters which is max for longest valid header
10    let format_identifier = read_line_capped(reader, 11)?;
11
12    if !format_identifier.starts_with("#?RADIANCE") && !format_identifier.starts_with("#?RGBE") {
13        return Err(
14            io::Error::new(io::ErrorKind::InvalidData, "Invalid HDR format identifier").into(),
15        );
16    }
17
18    loop {
19        // Assuming no line will ever go above 256. Just a random guess at the moment.
20        // If a line goes over the capped length we will return InvalidData which I think
21        // is better than potentially reading a malicious file and exploding memory usage.
22        let line = read_line_capped(reader, 256)?;
23
24        if line.trim().is_empty() {
25            continue;
26        }
27
28        // HDR image dimensions can be stored in 8 different ways based on orientation
29        // Using EXIF orientation as a reference:
30        // https://web.archive.org/web/20220924095433/https://sirv.sirv.com/website/exif-orientation-values.jpg
31        //
32        // -Y N +X M => Standard orientation (EXIF 1)
33        // -Y N -X M => Flipped horizontally (EXIF 2)
34        // +Y N -X M => Flipped vertically and horizontally (EXIF 3)
35        // +Y N +X M => Flipped vertically (EXIF 4)
36        // +X M -Y N => Rotate 90 CCW and flip vertically (EXIF 5)
37        // -X M -Y N => Rotate 90 CCW (EXIF 6)
38        // -X M +Y N => Rotate 90 CW and flip vertically (EXIF 7)
39        // +X M +Y N => Rotate 90 CW (EXIF 8)
40        //
41        // For EXIF 1-4 we can treat the dimensions the same. Flipping horizontally/vertically does not change them.
42        // For EXIF 5-8 we need to swap width and height because the image was rotated 90/270 degrees.
43        //
44        // Because of the ordering and rotations I believe that means that lines that start with Y will always
45        // be read as `height` then `width` and ones that start with X will be read as `width` then `height,
46        // but since any line that starts with X is rotated 90 degrees they will be flipped. Essentially this
47        // means that no matter whether the line starts with X or Y, it will be read as height then width.
48
49        // Extract width and height information
50        if line.starts_with("-Y") || line.starts_with("+Y") || line.starts_with("-X") || line.starts_with("+X") {
51            let dimensions: Vec<&str> = line.split_whitespace().collect();
52            if dimensions.len() != 4 {
53                return Err(io::Error::new(
54                    io::ErrorKind::InvalidData,
55                    "Invalid HDR dimensions line",
56                )
57                .into());
58            }
59
60            let height_parsed = dimensions[1].parse::<usize>().ok();
61            let width_parsed = dimensions[3].parse::<usize>().ok();
62
63            if let (Some(width), Some(height)) = (width_parsed, height_parsed) {
64                return Ok(ImageSize { width, height });
65            }
66
67            break;
68        }
69    }
70
71    Err(io::Error::new(io::ErrorKind::InvalidData, "HDR dimensions not found").into())
72}
73
74pub fn matches(header: &[u8]) -> bool {
75    let radiance_header = b"#?RADIANCE\n";
76    let rgbe_header = b"#?RGBE\n";
77
78    header.starts_with(radiance_header) || header.starts_with(rgbe_header)
79}