use log::{debug, info};
use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex};
use half::f16 as F16;
#[derive(Debug, Clone)]
pub enum PixelBuffer {
U8(Vec<u8>), F16(Vec<F16>), F32(Vec<f32>), }
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum PixelFormat {
Rgba8, RgbaF16, RgbaF32, }
#[derive(Debug, Clone, PartialEq)]
pub enum FrameStatus {
Placeholder, Header, Loading, Loaded, Error, }
#[derive(Debug, Clone)]
struct FrameData {
buffer: PixelBuffer, pixel_format: PixelFormat,
width: usize,
height: usize,
status: FrameStatus,
}
#[derive(Debug, Clone)]
pub struct Frame {
data: Arc<Mutex<FrameData>>, filename: Option<PathBuf>, }
#[derive(Debug)]
pub enum FrameError {
Exr(String),
Image(String),
UnsupportedFormat(String),
NoFilename,
}
impl std::fmt::Display for FrameError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
FrameError::Exr(e) => write!(f, "EXR error: {}", e),
FrameError::Image(e) => write!(f, "Image error: {}", e),
FrameError::UnsupportedFormat(e) => write!(f, "Unsupported format: {}", e),
FrameError::NoFilename => write!(f, "No filename set"),
}
}
}
impl std::error::Error for FrameError {}
impl Frame {
pub fn new(width: usize, height: usize) -> Self {
let mut buffer_u8 = vec![0u8; width * height * 4];
for px in buffer_u8.chunks_mut(4) {
px.copy_from_slice(&[0, 100, 0, 255]); }
let data = FrameData {
buffer: PixelBuffer::U8(buffer_u8),
pixel_format: PixelFormat::Rgba8,
width,
height,
status: FrameStatus::Placeholder,
};
Self {
data: Arc::new(Mutex::new(data)),
filename: None,
}
}
pub fn new_unloaded(path: PathBuf) -> Self {
let buffer_u8 = vec![0, 100, 0, 255];
let data = FrameData {
buffer: PixelBuffer::U8(buffer_u8),
pixel_format: PixelFormat::Rgba8,
width: 1,
height: 1,
status: FrameStatus::Header, };
Self {
data: Arc::new(Mutex::new(data)),
filename: Some(path),
}
}
pub fn set_file(&mut self, path: PathBuf) {
self.filename = Some(path);
self.data.lock().unwrap().status = FrameStatus::Header;
}
pub fn file(&self) -> Option<&PathBuf> {
self.filename.as_ref()
}
fn try_claim_for_loading(&self) -> bool {
let mut data = self.data.lock().unwrap();
if data.status == FrameStatus::Header {
data.status = FrameStatus::Loading;
true
} else {
false }
}
pub fn load(&self) -> Result<(), FrameError> {
let path = self.filename.as_ref().ok_or(FrameError::NoFilename)?.clone();
if !self.try_claim_for_loading() {
return match self.status() {
FrameStatus::Loaded => Ok(()),
FrameStatus::Error => Err(FrameError::Image("Previously failed".into())),
_ => Ok(()), };
}
let ext = path
.extension()
.and_then(|s| s.to_str())
.map(|s| s.to_lowercase())
.unwrap_or_default();
let result = match ext.as_str() {
"exr" => self.load_exr(&path),
"hdr" => self.load_hdr(&path),
"png" | "jpg" | "jpeg" | "tif" | "tiff" | "tga" => self.load_image(&path),
_ => Err(FrameError::UnsupportedFormat(format!(".{}", ext))),
};
match result {
Ok(()) => {
self.data.lock().unwrap().status = FrameStatus::Loaded;
Ok(())
}
Err(e) => {
self.data.lock().unwrap().status = FrameStatus::Error;
Err(e)
}
}
}
fn load_exr<P: AsRef<Path>>(&self, path: P) -> Result<(), FrameError> {
use openexr::prelude::*;
debug!("Loading EXR: {}", path.as_ref().display());
let file = RgbaInputFile::new(path.as_ref(), 1)
.map_err(|e| FrameError::Exr(e.to_string()))?;
let header = file.header();
let data_window = header.data_window::<[i32; 4]>();
let width = (data_window[2] - data_window[0] + 1) as usize;
let height = (data_window[3] - data_window[1] + 1) as usize;
let channels = header.channels();
let pixel_type = channels
.iter()
.find(|(name, _)| *name == "R")
.map(|(_, ch)| ch.type_)
.unwrap_or(PixelType::Half.into());
drop(header);
drop(file);
if pixel_type == PixelType::Half.into() {
self.load_exr_half(path.as_ref(), width, height)
} else if pixel_type == PixelType::Float.into() {
self.load_exr_float(path.as_ref(), width, height)
} else {
info!("EXR UINT pixels detected, loading as f16");
self.load_exr_half(path.as_ref(), width, height)
}
}
fn load_exr_half(&self, path: &Path, width: usize, height: usize) -> Result<(), FrameError> {
use openexr::prelude::*;
let mut file = RgbaInputFile::new(path, 1)
.map_err(|e| FrameError::Exr(e.to_string()))?;
let header = file.header();
let data_window = header.data_window::<[i32; 4]>();
let y_min = data_window[1];
let y_max = data_window[3];
drop(header);
let mut pixels_rgba = vec![Rgba::from_f32(0.0, 0.0, 0.0, 0.0); width * height];
file.set_frame_buffer(&mut pixels_rgba, 1, width)
.map_err(|e| FrameError::Exr(e.to_string()))?;
unsafe {
file.read_pixels(y_min, y_max)
.map_err(|e| FrameError::Exr(e.to_string()))?;
}
let mut buffer_f16 = Vec::with_capacity(width * height * 4);
for pixel in pixels_rgba.iter() {
buffer_f16.push(pixel.r); buffer_f16.push(pixel.g);
buffer_f16.push(pixel.b);
buffer_f16.push(pixel.a);
}
let mut data = self.data.lock().unwrap();
data.buffer = PixelBuffer::F16(buffer_f16);
data.pixel_format = PixelFormat::RgbaF16;
data.width = width;
data.height = height;
debug!("Loaded EXR HALF: {}x{} (f16)", width, height);
Ok(())
}
fn load_exr_float(&self, path: &Path, width: usize, height: usize) -> Result<(), FrameError> {
use openexr::prelude::*;
let file = InputFile::new(path, 1)
.map_err(|e| FrameError::Exr(e.to_string()))?;
let header = file.header();
let data_window = *header.data_window::<[i32; 4]>();
let y_min = data_window[1];
let y_max = data_window[3];
drop(header);
let frame_rgba = Frame::new::<f32, _, _>(&["R", "G", "B", "A"], data_window)
.map_err(|e| FrameError::Exr(e.to_string()))?;
let (_file, mut frames) = file
.into_reader(vec![frame_rgba])
.map_err(|e| FrameError::Exr(e.to_string()))?
.read_pixels(y_min, y_max)
.map_err(|e| FrameError::Exr(e.to_string()))?;
let buffer_f32: Vec<f32> = frames.remove(0).into_vec();
let mut data = self.data.lock().unwrap();
data.buffer = PixelBuffer::F32(buffer_f32);
data.pixel_format = PixelFormat::RgbaF32;
data.width = width;
data.height = height;
debug!("Loaded EXR FLOAT: {}x{} (f32, native precision)", width, height);
Ok(())
}
fn load_hdr<P: AsRef<Path>>(&self, path: P) -> Result<(), FrameError> {
debug!("Loading HDR: {}", path.as_ref().display());
let img = image::open(path.as_ref())
.map_err(|e| FrameError::Image(e.to_string()))?;
let width = img.width() as usize;
let height = img.height() as usize;
let rgb_f32 = img.to_rgb32f();
let rgb_data = rgb_f32.as_raw();
let mut buffer_f32 = Vec::with_capacity(width * height * 4);
for chunk in rgb_data.chunks(3) {
buffer_f32.push(chunk[0]); buffer_f32.push(chunk[1]); buffer_f32.push(chunk[2]); buffer_f32.push(1.0); }
let mut data = self.data.lock().unwrap();
data.buffer = PixelBuffer::F32(buffer_f32);
data.pixel_format = PixelFormat::RgbaF32;
data.width = width;
data.height = height;
info!("Loaded HDR: {}x{} (HDR f32)", width, height);
Ok(())
}
fn load_image<P: AsRef<Path>>(&self, path: P) -> Result<(), FrameError> {
debug!("Loading image: {}", path.as_ref().display());
let img = image::open(path.as_ref())
.map_err(|e| FrameError::Image(e.to_string()))?;
let width = img.width() as usize;
let height = img.height() as usize;
let rgba = img.to_rgba8();
let mut data = self.data.lock().unwrap();
data.buffer = PixelBuffer::U8(rgba.into_raw());
data.pixel_format = PixelFormat::Rgba8;
data.width = width;
data.height = height;
Ok(())
}
pub fn mem(&self) -> usize {
let data = self.data.lock().unwrap();
match &data.buffer {
PixelBuffer::U8(vec) => vec.len(), PixelBuffer::F16(vec) => vec.len() * 2, PixelBuffer::F32(vec) => vec.len() * 4, }
}
pub fn status(&self) -> FrameStatus {
self.data.lock().unwrap().status.clone()
}
pub fn set_status(&self, status: FrameStatus) {
self.data.lock().unwrap().status = status;
}
pub fn pixel_buffer(&self) -> PixelBuffer {
self.data.lock().unwrap().buffer.clone()
}
pub fn pixel_format(&self) -> PixelFormat {
self.data.lock().unwrap().pixel_format
}
pub fn pixels(&self) -> Result<Vec<u8>, FrameError> {
let data = self.data.lock().unwrap();
match &data.buffer {
PixelBuffer::U8(vec) => Ok(vec.clone()),
PixelBuffer::F16(_) => Err(FrameError::UnsupportedFormat(
"Frame uses F16 format, use pixel_buffer() for HDR data".into()
)),
PixelBuffer::F32(_) => Err(FrameError::UnsupportedFormat(
"Frame uses F32 format, use pixel_buffer() for HDR data".into()
)),
}
}
#[allow(dead_code)]
pub fn buffer(&self) -> Arc<Mutex<Vec<u8>>> {
Arc::new(Mutex::new(self.pixels().unwrap()))
}
pub fn width(&self) -> usize {
self.data.lock().unwrap().width
}
pub fn height(&self) -> usize {
self.data.lock().unwrap().height
}
pub fn resolution(&self) -> (usize, usize) {
let data = self.data.lock().unwrap();
(data.width, data.height)
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::Path;
#[test]
fn test_frame_creation() {
let frame = Frame::new(1920, 1080);
assert_eq!(frame.width(), 1920);
assert_eq!(frame.height(), 1080);
assert_eq!(frame.status(), FrameStatus::Placeholder);
assert_eq!(frame.pixel_format(), PixelFormat::Rgba8);
}
#[test]
fn test_frame_with_file() {
let frame = Frame::new_unloaded(PathBuf::from("test.exr"));
assert_eq!(frame.status(), FrameStatus::Header);
assert!(frame.file().is_some());
assert_eq!(frame.file().unwrap(), Path::new("test.exr"));
}
#[test]
fn test_load_missing_file() {
let frame = Frame::new_unloaded(
PathBuf::from("/nonexistent/path/test.jpg")
);
let result = frame.load();
assert!(result.is_err());
assert_eq!(frame.status(), FrameStatus::Error);
}
#[test]
fn test_pixel_buffer_types() {
let buf_u8 = PixelBuffer::U8(vec![0u8; 1920 * 1080 * 4]);
match buf_u8 {
PixelBuffer::U8(v) => assert_eq!(v.len(), 1920 * 1080 * 4),
_ => panic!("Wrong variant"),
}
let buf_f16 = PixelBuffer::F16(vec![F16::ZERO; 1920 * 1080 * 4]);
match buf_f16 {
PixelBuffer::F16(v) => assert_eq!(v.len(), 1920 * 1080 * 4),
_ => panic!("Wrong variant"),
}
let buf_f32 = PixelBuffer::F32(vec![0.0f32; 1920 * 1080 * 4]);
match buf_f32 {
PixelBuffer::F32(v) => assert_eq!(v.len(), 1920 * 1080 * 4),
_ => panic!("Wrong variant"),
}
}
#[test]
fn test_status_transitions() {
let frame = Frame::new(100, 100);
assert_eq!(frame.status(), FrameStatus::Placeholder);
let frame = Frame::new_unloaded(PathBuf::from("test.png"));
assert_eq!(frame.status(), FrameStatus::Header);
let _ = frame.load();
assert_eq!(frame.status(), FrameStatus::Error);
}
}