-
Notifications
You must be signed in to change notification settings - Fork 10
Feature/negative offset fix #31
Changes from all commits
6fd769a
2909c35
54f8266
2dcc6d7
61254e5
fc0af65
f1b4d5a
d52070b
9a6bc67
2721cc4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -41,21 +41,28 @@ use cexr_type_aliases::*; | |
/// Points to and describes in-memory image data for reading. | ||
pub struct FrameBuffer<'a> { | ||
handle: *mut CEXR_FrameBuffer, | ||
origin: (i32, i32), | ||
dimensions: (u32, u32), | ||
_phantom_1: PhantomData<CEXR_FrameBuffer>, | ||
_phantom_2: PhantomData<&'a mut [u8]>, | ||
} | ||
|
||
impl<'a> FrameBuffer<'a> { | ||
/// Creates an empty frame buffer with the given dimensions in pixels. | ||
/// Creates an empty frame buffer with the given dimensions and zero offset in pixels. | ||
pub fn new(width: u32, height: u32) -> Self { | ||
Self::new_with_origin(0, 0, width, height) | ||
} | ||
|
||
/// Creates an empty frame buffer with the given dimensions in pixels. | ||
pub fn new_with_origin(x: i32, y: i32, width: u32, height: u32) -> Self { | ||
assert!( | ||
width > 0 && height > 0, | ||
"FrameBuffers must be non-zero size in \ | ||
both dimensions." | ||
); | ||
FrameBuffer { | ||
handle: unsafe { CEXR_FrameBuffer_new() }, | ||
origin: (x, y), | ||
dimensions: (width, height), | ||
_phantom_1: PhantomData, | ||
_phantom_2: PhantomData, | ||
|
@@ -67,6 +74,23 @@ impl<'a> FrameBuffer<'a> { | |
self.dimensions | ||
} | ||
|
||
/// Return the origin of the frame buffer. | ||
pub fn origin(&self) -> (i32, i32) { | ||
self.origin | ||
} | ||
|
||
/// Return the offset index of the data pixel at 0,0 with reference to the data pixel at window.min.x, window.min.y | ||
pub fn origin_offset(&self) -> isize { | ||
let width = self.dimensions.0; | ||
let (x, y) = self.origin; | ||
-(x as isize + y as isize * width as isize) | ||
} | ||
|
||
/// Return the offset byte of the data pixel at 0,0 with reference to the data pixel at window.min.x, window.min.y | ||
fn origin_offset_byte<T: Sized>(&self) -> isize { | ||
self.origin_offset() * mem::size_of::<T>() as isize | ||
} | ||
|
||
/// Insert a single channel into the FrameBuffer. | ||
/// | ||
/// The channel will be given the name `name`. | ||
|
@@ -84,11 +108,12 @@ impl<'a> FrameBuffer<'a> { | |
); | ||
} | ||
let width = self.dimensions.0; | ||
let origin_offset = self.origin_offset_byte::<T>(); | ||
unsafe { | ||
self.insert_raw( | ||
name, | ||
T::pixel_type(), | ||
data.as_ptr() as *const c_char, | ||
(data.as_ptr() as *const c_char).offset(origin_offset), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you explain the logic behind this offset, and the similar one below? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. From the behaviour observed I believe that the underlying API expects the Also, in short: it works. As the reason why, no idea. I've seen this pattern in other APIs though. I can only guess that the reason why there is no There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I'm not sure what this means. Are you talking about the pixel at coordinates (0, 0) relative to the origin of the data window, or of the display window? What is the "'first'" pixel? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, it's the pixel at coordinates (0,0) relative to the origin of the data window, which in my case are negative. In this case, this pixel has a positive offset in the framebuffer. This pixel would have offset 0 if the origin of the data window were to be (0,0) - as it is in many common cases. There is strong evidence for the Also, to clarify, the "first pixel" in the frame buffer is the one mapped to coordinates I haven't checked what happens if the coordinates of the origin of the data window are positive, but this is out of the scope of this bug fix. It is likely to work but I haven't verified all the edge cases for UB. I'm going to make another test case for positive coordinates of the data window origin but I have higher priority on the negative origin case as I already have a lot of images with negative origin I am currently unable to load, but none with positive > 0 coordinates. I believe the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this makes sense, but I'll need to squint at the upstream documentation a bit more to be certain. Would like to hear @cessen's thoughts too. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @Ralith sounds very sensible - just let me know if you need further changes in the branch. |
||
(mem::size_of::<T>(), width as usize * mem::size_of::<T>()), | ||
(1, 1), | ||
0.0, | ||
|
@@ -117,12 +142,13 @@ impl<'a> FrameBuffer<'a> { | |
); | ||
} | ||
let width = self.dimensions.0; | ||
let origin_offset = self.origin_offset_byte::<T>(); | ||
for (name, (ty, offset)) in names.iter().zip(T::channels()) { | ||
unsafe { | ||
self.insert_raw( | ||
name, | ||
ty, | ||
(data.as_ptr() as *const c_char).offset(offset as isize), | ||
(data.as_ptr() as *const c_char).offset(origin_offset + offset as isize), | ||
(mem::size_of::<T>(), width as usize * mem::size_of::<T>()), | ||
(1, 1), | ||
0.0, | ||
|
@@ -201,6 +227,13 @@ pub struct FrameBufferMut<'a> { | |
} | ||
|
||
impl<'a> FrameBufferMut<'a> { | ||
/// Creates an empty frame buffer with the given dimensions and zero offset in pixels. | ||
pub fn new_with_origin(x: i32, y: i32, width: u32, height: u32) -> Self { | ||
FrameBufferMut { | ||
frame_buffer: FrameBuffer::new_with_origin(x, y, width, height), | ||
} | ||
} | ||
|
||
/// Creates an empty frame buffer with the given dimensions in pixels. | ||
pub fn new(width: u32, height: u32) -> Self { | ||
FrameBufferMut { | ||
|
@@ -232,11 +265,12 @@ impl<'a> FrameBufferMut<'a> { | |
); | ||
} | ||
let width = self.dimensions.0; | ||
let origin_offset = self.origin_offset_byte::<T>(); | ||
unsafe { | ||
self.insert_raw( | ||
name, | ||
T::pixel_type(), | ||
data.as_mut_ptr() as *mut c_char, | ||
(data.as_mut_ptr() as *mut c_char).offset(origin_offset), | ||
(mem::size_of::<T>(), width as usize * mem::size_of::<T>()), | ||
(1, 1), | ||
fill, | ||
|
@@ -271,12 +305,13 @@ impl<'a> FrameBufferMut<'a> { | |
); | ||
} | ||
let width = self.dimensions.0; | ||
let origin_offset = self.origin_offset_byte::<T>(); | ||
for (&(name, fill), (ty, offset)) in names_and_fills.iter().zip(T::channels()) { | ||
unsafe { | ||
self.insert_raw( | ||
name, | ||
ty, | ||
(data.as_mut_ptr() as *mut c_char).offset(offset as isize), | ||
(data.as_mut_ptr() as *mut c_char).offset(origin_offset + offset as isize), | ||
(mem::size_of::<T>(), width as usize * mem::size_of::<T>()), | ||
(1, 1), | ||
fill, | ||
|
This file was deleted.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,111 @@ | ||
extern crate half; | ||
extern crate openexr; | ||
extern crate openexr_sys; | ||
|
||
use half::f16; | ||
use openexr::{FrameBuffer, FrameBufferMut, Header, InputFile, ScanlineOutputFile}; | ||
use std::fs::File; | ||
|
||
// OpenEXR file data. | ||
const NEGATIVE_OFFSET: &[u8] = include_bytes!("data/negative_window.exr"); | ||
const POSITIVE_OFFSET: &[u8] = include_bytes!("data/positive_window.exr"); | ||
|
||
fn load_and_test_with_offset_window_read_multiple_channels(data: &[u8]) { | ||
let mut exr_file = InputFile::from_slice(data).unwrap(); | ||
|
||
let (width, height) = exr_file.header().data_dimensions(); | ||
let (x, y) = exr_file.header().data_origin(); | ||
|
||
let zero = f16::from_f32(0.0f32); | ||
|
||
let mut pixel_data = vec![(zero, zero, zero); (width * height) as usize]; | ||
|
||
let read_with_offset = |exr_file: &mut InputFile, pixel_data: &mut [_], ox, oy| { | ||
let mut fb = FrameBufferMut::new_with_origin(ox, oy, width, height); | ||
println!("Loading from buffer as {},{} {}x{}", ox, oy, width, height); | ||
fb.insert_channels(&[("R", 0.0), ("G", 0.0), ("B", 0.0)], pixel_data); | ||
println!("Reading pixels from {},{},{}", "R", "G", "B"); | ||
(exr_file.read_pixels(&mut fb), fb.origin_offset()) | ||
}; | ||
|
||
// let's try a few mismatched origins | ||
assert!(read_with_offset(&mut exr_file, &mut pixel_data, 0, 0) | ||
.0 | ||
.is_err()); | ||
assert!( | ||
read_with_offset(&mut exr_file, &mut pixel_data, x - 1, y - 1) | ||
.0 | ||
.is_err() | ||
); | ||
assert!(read_with_offset(&mut exr_file, &mut pixel_data, -x, -y) | ||
.0 | ||
.is_err()); | ||
// and then the real thing | ||
let (read_result, origin_offset) = read_with_offset(&mut exr_file, &mut pixel_data, x, y); | ||
assert!(read_result.is_ok()); | ||
// check the pixel value at coordinates (0,0) of the data window if 0,0 is within the frame buffer | ||
if origin_offset >= 0 { | ||
assert!(f32::abs(pixel_data[origin_offset as usize].0.to_f32() - 0.5f32) < 0.0001f32); | ||
} | ||
|
||
// we write the file back out with a different offset | ||
{ | ||
let mut fb = FrameBuffer::new_with_origin(-x, -y, width, height); | ||
println!("Loading buffer as {}x{}", width, height); | ||
let mut file = | ||
File::create("target/positive_window.exr").expect("Could not create output file"); | ||
let mut exr_file = ScanlineOutputFile::new( | ||
&mut file, | ||
&Header::new() | ||
.set_data_window(Header::box2i(-x, -y, width, height)) | ||
.set_display_window(Header::box2i(0, 0, width - 16, height - 16)) | ||
.add_channel("R", openexr::PixelType::HALF) | ||
.add_channel("G", openexr::PixelType::HALF) | ||
.add_channel("B", openexr::PixelType::HALF), | ||
) | ||
.unwrap(); | ||
|
||
fb.insert_channels(&["R", "G", "B"], &pixel_data); | ||
exr_file.write_pixels(&fb).unwrap(); | ||
} | ||
} | ||
|
||
fn load_and_test_with_offset_window_read_single_channel(data: &[u8]) { | ||
let mut exr_file = InputFile::from_slice(data).unwrap(); | ||
let (width, height) = exr_file.header().data_dimensions(); | ||
let (x, y) = exr_file.header().data_origin(); | ||
|
||
let zero = f16::from_f32(0.0f32); | ||
|
||
let mut pixel_data = vec![zero; (width * height) as usize]; | ||
|
||
let mut read_with_offset = |ox: i32, oy: i32| { | ||
let mut fb = FrameBufferMut::new_with_origin(ox, oy, width, height); | ||
println!( | ||
"Loading from buffer as ({} {}) {}x{}", | ||
ox, oy, width, height | ||
); | ||
fb.insert_channel("R", 0.0, &mut pixel_data); | ||
println!("Reading pixels from {},{},{}", "R", "G", "B"); | ||
exr_file.read_pixels(&mut fb) | ||
}; | ||
// let's try a few mismatched origins | ||
assert!(read_with_offset(0, 0).is_err()); | ||
assert!(read_with_offset(x - 1, y - 1).is_err()); | ||
assert!(read_with_offset(-x, -y).is_err()); | ||
// and then the correct one | ||
assert!(read_with_offset(x, y).is_ok()); | ||
} | ||
|
||
#[test] | ||
fn window_read_multiple_channels() { | ||
load_and_test_with_offset_window_read_multiple_channels(NEGATIVE_OFFSET); | ||
load_and_test_with_offset_window_read_multiple_channels(POSITIVE_OFFSET); | ||
} | ||
|
||
// with one channel only as well | ||
#[test] | ||
fn negative_window_read_single_channel() { | ||
load_and_test_with_offset_window_read_single_channel(NEGATIVE_OFFSET); | ||
load_and_test_with_offset_window_read_single_channel(POSITIVE_OFFSET); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's not clear which origin this is, what it's relative to, or why the framebuffer needs to be aware of it. Upstream does not seem to have a similar member or accessor.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This origin is needed to calculate the offset to the data pointer in
insert_raw
. Values of the origin are taken from thedata_window.min
header field.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The documentation, and indeed naming, should make it clear what this value is.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sounds good, what do you suggest?
data_window_origin
?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As an alternative, the
origin
field (or the derivedoffset
) may be added as a parameter in all theinsert_channel*
calls.Would it be more acceptable to you? I still prefer the option of it being a member of the framebuffer struct as it is easier to validate and less prone to errors.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't necessarily object to the approach taken here, I just haven't fully grasped how it works yet. I'm totally on board with papering over ill-conceived upstream APIs when it's feasible to do so and doesn't make things even more confusing elsewhere; we do after all already do similar-looking things with the
dimensions
field.