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" )
|
51 | || line.starts_with("+Y" )
|
52 | || line.starts_with("-X" )
|
53 | || line.starts_with("+X" )
|
54 | {
|
55 | let dimensions: Vec<&str> = line.split_whitespace().collect();
|
56 | if dimensions.len() != 4 {
|
57 | return Err(io::Error::new(
|
58 | io::ErrorKind::InvalidData,
|
59 | "Invalid HDR dimensions line" ,
|
60 | )
|
61 | .into());
|
62 | }
|
63 |
|
64 | let height_parsed = dimensions[1].parse::<usize>().ok();
|
65 | let width_parsed = dimensions[3].parse::<usize>().ok();
|
66 |
|
67 | if let (Some(width), Some(height)) = (width_parsed, height_parsed) {
|
68 | return Ok(ImageSize { width, height });
|
69 | }
|
70 |
|
71 | break;
|
72 | }
|
73 | }
|
74 |
|
75 | Err(io::Error::new(io::ErrorKind::InvalidData, "HDR dimensions not found" ).into())
|
76 | }
|
77 |
|
78 | pub fn matches(header: &[u8]) -> bool {
|
79 | let radiance_header: &'static [u8; 11] = b"#?RADIANCE \n" ;
|
80 | let rgbe_header: &'static [u8; 7] = b"#?RGBE \n" ;
|
81 |
|
82 | header.starts_with(needle:radiance_header) || header.starts_with(needle:rgbe_header)
|
83 | }
|
84 | |