| 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 | |