Skip to content
This repository has been archived by the owner on Jun 17, 2022. It is now read-only.

Feature/negative offset fix #31

Merged
merged 10 commits into from
Nov 21, 2018
45 changes: 40 additions & 5 deletions src/frame_buffer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -67,6 +74,23 @@ impl<'a> FrameBuffer<'a> {
self.dimensions
}

/// Return the origin of the frame buffer.
pub fn origin(&self) -> (i32, i32) {
Copy link
Collaborator

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.

Copy link
Contributor Author

@norru norru Nov 11, 2018

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 the data_window.min header field.

Copy link
Collaborator

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.

Copy link
Contributor Author

@norru norru Nov 11, 2018

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?

Copy link
Contributor Author

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 derived offset) may be added as a parameter in all the insert_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.

Copy link
Collaborator

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.

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`.
Expand All @@ -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),
Copy link
Collaborator

Choose a reason for hiding this comment

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

Copy link
Contributor Author

@norru norru Nov 11, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From the behaviour observed I believe that the underlying API expects the data passed here, in insert_raw, to be pointing at the pixel mapped to (0,0) in the data_window rather than the "first" pixel in the framebuffer.

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 offset field in the native FrameBuffer API itself is that the client is expected to do the math itself and pass the correct pointer. If this is the case I believe this to be unergonomic and having the lib calculating it from the (x, y) offset to be much clearer and more practical.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the pixel mapped to (0,0) in the data_window rather than the "first" pixel in the framebuffer.

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?

Copy link
Contributor Author

@norru norru Nov 11, 2018

Choose a reason for hiding this comment

The 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 insert_raw call to be requiring a pointer to this specific pixel as its third parameter.

Also, to clarify, the "first pixel" in the frame buffer is the one mapped to coordinates (data_window.min.x, data_window.min.y). This pixel,, being at the origin, by definition, has offset 0 in the framebuffer.

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 display_window to be inconsequential in this context, but I may stand corrected.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
17 changes: 17 additions & 0 deletions src/header.rs
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,12 @@ impl Header {
self
}

/// Convenience method for the dimensions of the data window.
pub fn data_origin(&self) -> (i32, i32) {
let window = self.data_window();
(window.min.x, window.min.y)
}

/// Convenience method for the dimensions of the data window.
pub fn data_dimensions(&self) -> (u32, u32) {
let window = self.data_window();
Expand Down Expand Up @@ -280,6 +286,17 @@ impl Header {
Ok(())
}

/// Utility function to create a Box2i specifying its origin (bottom left) and size
pub fn box2i(x: i32, y: i32, width: u32, height: u32) -> Box2i {
Box2i {
min: CEXR_V2i { x, y },
max: CEXR_V2i {
x: x + width as i32 - 1,
y: y + height as i32 - 1,
},
}
}

// Factored out shared code from the validate_framebuffer_* methods above.
fn validate_channel(name: &str, h_chan: &Channel, fb_chan: &Channel) -> Result<()> {
if fb_chan.pixel_type != h_chan.pixel_type {
Expand Down
11 changes: 11 additions & 0 deletions src/input/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,17 @@ impl<'a> InputFile<'a> {
)));
}

// Validation
if self.header().data_origin() != framebuffer.origin() {
return Err(Error::Generic(format!(
"framebuffer origin {},{} does not match image origin {},{}",
framebuffer.origin().0,
framebuffer.origin().1,
self.header().data_origin().0,
self.header().data_origin().1
)));
}

self.header().validate_framebuffer_for_input(framebuffer)?;

// Set up the framebuffer with the image
Expand Down
13 changes: 11 additions & 2 deletions src/output/scanline_output_file.rs
Original file line number Diff line number Diff line change
Expand Up @@ -141,15 +141,24 @@ impl<'a> ScanlineOutputFile<'a> {

if self.header().data_dimensions() != framebuffer.dimensions() {
return Err(Error::Generic(format!(
"framebuffer size {}x{} does not match \
image dimensions {}x{}",
"framebuffer size {}x{} does not match image dimensions {}x{}",
framebuffer.dimensions().0,
framebuffer.dimensions().1,
self.header().data_dimensions().0,
self.header().data_dimensions().1
)));
}

if self.header().data_origin() != framebuffer.origin() {
return Err(Error::Generic(format!(
"framebuffer origin {}x{} does not match image origin {}x{}",
framebuffer.origin().0,
framebuffer.origin().1,
self.header().data_origin().0,
self.header().data_origin().1
)));
}

self.header().validate_framebuffer_for_output(framebuffer)?;

// Set up the framebuffer with the image
Expand Down
Binary file added tests/data/positive_window.exr
Binary file not shown.
26 changes: 0 additions & 26 deletions tests/negative_window.rs

This file was deleted.

111 changes: 111 additions & 0 deletions tests/non_zero_origin_window.rs
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);
}