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