use super::misc::mktag;
use crate::error::{Error, Result};
use crate::tag::Tag;
use crate::value::Value;
fn radiance_tag_name(key: &str) -> Option<&'static str> {
Some(match key {
"software" => "Software",
"view" => "View",
"format" => "Format",
"exposure" => "Exposure",
"gamma" => "Gamma",
"colorcorr" => "ColorCorrection",
"pixaspect" => "PixelAspectRatio",
"primaries" => "ColorPrimaries",
_ => return None,
})
}
fn orientation_print_conv(orient: &str) -> Option<&'static str> {
Some(match orient {
"-Y +X" => "Horizontal (normal)",
"-Y -X" => "Mirror horizontal",
"+Y -X" => "Rotate 180",
"+Y +X" => "Mirror vertical",
"+X -Y" => "Mirror horizontal and rotate 270 CW",
"+X +Y" => "Rotate 90 CW",
"-X +Y" => "Mirror horizontal and rotate 90 CW",
"-X -Y" => "Rotate 270 CW",
_ => return None,
})
}
fn dynamic_tag_name(key: &str) -> Option<String> {
let name: String = key
.chars()
.filter(|c| c.is_ascii_alphanumeric() || *c == '-' || *c == '_')
.collect();
if name.chars().count() <= 1 {
return None;
}
let mut chars = name.chars();
let first = chars.next()?;
Some(format!("{}{}", first.to_uppercase(), chars.as_str()))
}
fn split_key_value(line: &str) -> Option<(&str, &str)> {
let eq = line.rfind('=')?;
let value = line[eq + 1..].trim_start_matches(|c: char| c.is_ascii_whitespace());
Some((&line[..eq], value))
}
pub fn read_hdr(data: &[u8]) -> Result<Vec<Tag>> {
let Some(first_end) = data.iter().position(|b| *b == b'\n') else {
return Err(Error::InvalidData("not a Radiance HDR file".into()));
};
let magic = &data[..first_end];
if magic != b"#?RADIANCE" && magic != b"#?RGBE" {
return Err(Error::InvalidData("not a Radiance HDR file".into()));
}
let mut tags = Vec::new();
let mut lines = data[first_end..]
.split(|b| *b == b'\n')
.skip(1)
.map(crate::encoding::decode_utf8_or_latin1);
for line in lines.by_ref() {
if line.is_empty() || line.len() >= 4096 {
break;
}
if let Some(rest) = line.strip_prefix('#') {
let comment = rest.trim_start_matches(|c: char| c.is_ascii_whitespace());
if !comment.is_empty() {
tags.push(mktag(
"Radiance",
"Comment",
"Comment",
Value::String(comment.to_string()),
));
}
continue;
}
let Some((key, value)) = split_key_value(&line) else {
tags.push(mktag(
"Radiance",
"Command",
"Command",
Value::String(line.clone()),
));
continue;
};
let key = key.to_lowercase();
let name = match radiance_tag_name(&key) {
Some(name) => name.to_string(),
None => match dynamic_tag_name(&key) {
Some(name) => name,
None => continue,
},
};
tags.push(mktag(
"Radiance",
&name,
&name,
Value::String(value.to_string()),
));
}
if let Some(line) = lines.next() {
if let Some((orient, height, width)) = parse_resolution(&line) {
let print = orientation_print_conv(&orient)
.map(str::to_string)
.unwrap_or(orient);
tags.push(mktag(
"Radiance",
"Orientation",
"Orientation",
Value::String(print),
));
tags.push(mktag(
"File",
"ImageHeight",
"Image Height",
Value::U32(height),
));
tags.push(mktag(
"File",
"ImageWidth",
"Image Width",
Value::U32(width),
));
}
}
Ok(tags)
}
fn parse_resolution(line: &str) -> Option<(String, u32, u32)> {
let bytes: Vec<char> = line.chars().collect();
let mut i = 0;
while i < bytes.len() {
let Some((axis1, mut j)) = match_axis(&bytes, i) else {
i += 1;
continue;
};
j = skip_spaces(&bytes, j);
let Some((first, mut j)) = match_digits(&bytes, j) else {
i += 1;
continue;
};
j = skip_spaces(&bytes, j);
let Some((axis2, mut j)) = match_axis(&bytes, j) else {
i += 1;
continue;
};
j = skip_spaces(&bytes, j);
let Some((second, _)) = match_digits(&bytes, j) else {
i += 1;
continue;
};
return Some((format!("{axis1} {axis2}"), first, second));
}
None
}
fn match_axis(chars: &[char], i: usize) -> Option<(String, usize)> {
let sign = *chars.get(i)?;
let axis = *chars.get(i + 1)?;
if matches!(sign, '-' | '+') && matches!(axis, 'X' | 'Y') {
Some((format!("{sign}{axis}"), i + 2))
} else {
None
}
}
fn skip_spaces(chars: &[char], mut i: usize) -> usize {
while i < chars.len() && chars[i].is_whitespace() {
i += 1;
}
i
}
fn match_digits(chars: &[char], i: usize) -> Option<(u32, usize)> {
let mut j = i;
while j < chars.len() && chars[j].is_ascii_digit() {
j += 1;
}
if j == i {
return None;
}
chars[i..j]
.iter()
.collect::<String>()
.parse()
.ok()
.map(|v| (v, j))
}