imagesize/formats/hdr.rs
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
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)
}