1 | use std::io::{self, BufRead, Seek, SeekFrom}; |
2 | |
3 | use crate::{util::read_line_capped, ImageResult, ImageSize}; |
4 | |
5 | pub 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 | |
74 | pub 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 | |