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: &[u8; 11] = b"#?RADIANCE\n";
76 let rgbe_header: &[u8; 7] = b"#?RGBE\n";
77
78 header.starts_with(needle:radiance_header) || header.starts_with(needle:rgbe_header)
79}
80