Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

OpenEXR basic support #1475

Merged
merged 47 commits into from
Jul 14, 2021
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
0ad1732
first ever exr prototype (not even compiled once)
johannesvollmer May 9, 2021
2e4e73a
use u16 for now (can easily be changed back to f32 again)
johannesvollmer May 9, 2021
11a8c2a
the minorest improvements (minor cleanup)
johannesvollmer May 10, 2021
958a4ac
Merge branch 'master' of https://github.com/image-rs/image into opene…
johannesvollmer Jun 1, 2021
2d6cf83
use newer exr library version with relaxed reader and writer bounds
johannesvollmer Jun 1, 2021
f0cccc0
update supported formats documentation (add exr, use camel case)
johannesvollmer Jun 1, 2021
307f7be
use f32 in writer and invert y
johannesvollmer Jun 1, 2021
5e90731
make it compile, with new exr version. also do not require seekable w…
johannesvollmer Jun 3, 2021
c7a0ef7
fix panic triggering for unsupported color types
johannesvollmer Jun 3, 2021
4367914
add tests to openexr module, load alpha only if present in file or re…
johannesvollmer Jun 4, 2021
57e6d8e
improve documentation: mention that writer argument should be buffered
johannesvollmer Jun 4, 2021
5487eb1
cosmetic improvements
johannesvollmer Jun 4, 2021
7ea5607
try adding an exr fuzzing script, rename exr test functions
johannesvollmer Jun 4, 2021
9b3f3b7
add test images (lol forgot that)
johannesvollmer Jun 4, 2021
a9073d1
add openexr feature flag to rust ci workflow
johannesvollmer Jun 4, 2021
dc534ac
try fix fuzzer, update readme format table from lib.rs,
johannesvollmer Jun 4, 2021
dccc0fe
add exr to other existing tests
johannesvollmer Jun 4, 2021
ad151b4
add exr to even more existing tests and documentation
johannesvollmer Jun 4, 2021
6d43853
remove exr from protected workflows file
johannesvollmer Jun 4, 2021
bf08b3b
remove exr error from public api
johannesvollmer Jun 4, 2021
6712173
implement original_color_type, add encoder trait implementation, remo…
johannesvollmer Jun 4, 2021
4df8dc2
use alpha flag in constructor instead of mutating the flag (avoiding …
johannesvollmer Jun 4, 2021
9ee062b
add docs to encoder wrapper
johannesvollmer Jun 4, 2021
724cc0c
remove commented-out functions
johannesvollmer Jun 4, 2021
fa1bf36
documentation improvements
johannesvollmer Jun 6, 2021
072fcfd
simplify imports, clarify reader buffering in documentation and remov…
johannesvollmer Jun 6, 2021
a3b2bda
rename exr to openexr, move ImageBuffer type alias definitions to the…
johannesvollmer Jun 6, 2021
465f927
remove outdated todo comment
johannesvollmer Jun 6, 2021
7198c5a
use display window instead of loading only the layer data
johannesvollmer Jun 7, 2021
6f3ea90
allow aligned buffers, make progress pub(crate), make image type alia…
johannesvollmer Jun 7, 2021
4276b44
implement trivial review suggestions
johannesvollmer Jun 23, 2021
61784de
Merge branch 'master' of https://github.com/image-rs/image into opene…
johannesvollmer Jun 23, 2021
6d7c802
bridge disconnected docs sections
johannesvollmer Jun 24, 2021
b18339d
make helper functions private (plus re-implement the helper methods c…
johannesvollmer Jun 25, 2021
495e990
re-implement more helper methods copy&paste for external fuzzer test …
johannesvollmer Jun 25, 2021
b3b349e
re-implement even more helper methods copy&paste for external fuzzer …
johannesvollmer Jun 25, 2021
9838118
simplify helper methodsfor external fuzzer test script
johannesvollmer Jun 25, 2021
5126f1b
fix unused crate import error
johannesvollmer Jun 25, 2021
d0c6872
fix more compiler errrors
johannesvollmer Jun 25, 2021
83148a3
fix fuzzer script compile errors
johannesvollmer Jun 25, 2021
d67a08b
fix fuzzer script compile errors
johannesvollmer Jun 25, 2021
6d729c9
unfix removing "unused imports"
johannesvollmer Jun 25, 2021
57a83b5
use overflow-safe size checks
johannesvollmer Jul 14, 2021
ab811e5
also check buffer dimensions in decoder, not only encoder
johannesvollmer Jul 14, 2021
9c6d68f
panic in decoder instead of returning an error
johannesvollmer Jul 14, 2021
4710fd7
when checking decoder buffer size, use strict equality, as declared i…
johannesvollmer Jul 14, 2021
899431f
re-trigger CI
johannesvollmer Jul 14, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ rgb = { version = "0.8.25", optional = true }
mp4parse = { version = "0.11.5", optional = true }
dav1d = { version = "0.6.0", optional = true }
dcv-color-primitives = { version = "0.1.16", optional = true }
exr = { version = "1.2.0", optional = true }
color_quant = "1.1"

[dev-dependencies]
Expand All @@ -47,7 +48,7 @@ criterion = "0.3"

[features]
# TODO: Add "avif" to this list while preparing for 0.24.0
default = ["gif", "jpeg", "ico", "png", "pnm", "tga", "tiff", "webp", "bmp", "hdr", "dxt", "dds", "farbfeld", "jpeg_rayon"]
default = ["gif", "jpeg", "ico", "png", "pnm", "tga", "tiff", "webp", "bmp", "hdr", "dxt", "dds", "farbfeld", "jpeg_rayon", "openexr"]

ico = ["bmp", "png"]
pnm = []
Expand All @@ -58,6 +59,7 @@ hdr = ["scoped_threadpool"]
dxt = []
dds = ["dxt"]
farbfeld = []
openexr = ["exr"]

# Enables multi-threading.
# Requires latest stable Rust.
Expand Down
185 changes: 185 additions & 0 deletions src/codecs/openexr.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
//! Decoding of OpenEXR (.exr) Images
//!
//! OpenEXR is an image format that is widely used, especially in VFX,
//! because it supports lossless and lossy compression for float data.
//!
//! # Related Links
//! * <https://www.openexr.com/documentation.html> - The OpenEXR reference.

// # ROADMAP
// - [x] ImageDecoder
// - [x] ImageEncoder
// - [ ] GenericImageView?
// - [ ] GenericImage?
// - [x] Progress

// # MAYBE SOON
// - [ ] ImageDecoderExt::read_rect_with_progress
// - [ ] Layers -> Animation?


extern crate exr;
use exr::prelude::*;

use crate::{ImageDecoder, ImageResult, ColorType, Progress, image, ImageError, ImageFormat};
use std::io::{Write, Seek, BufRead, SeekFrom, Cursor};
use crate::error::{DecodingError, ImageFormatHint};
use self::exr::meta::header::Header;

/// An OpenEXR decoder
#[derive(Debug)]
pub struct ExrDecoder<R> {
exr_reader: exr::block::reader::Reader<R>,

// select a header that is rgb and not deep
header_index: usize,
}

impl<R: BufRead + Seek> ExrDecoder<R> {

/// Create a decoder. Consumes the first few bytes of the source to extract image dimensions.
pub fn read(mut source: R) -> ImageResult<Self> {

// read meta data, then wait for further instructions, keeping the file open and ready
let exr_reader = exr::block::read(source, false)?;

let header_index = exr_reader.headers().into_iter()
.position(|header|{
let has_rgb = ["R","G","B"].iter().all( // alpha will be optional
// check if r/g/b exists in the channels
|required| header.channels.list.iter()
.find(|chan| chan.name.eq(required)).is_some() // TODO eq_lowercase only if exrs supports it
);

!header.deep && has_rgb
})
.ok_or_else(|| ImageError::Decoding(DecodingError::new(
ImageFormatHint::Exact(ImageFormat::Exr),
"image does not contain non-deep rgb channels"
)))?;

Ok(Self { exr_reader, header_index })
}
}


impl<'a, R: 'a + BufRead + Seek> ImageDecoder<'a> for ExrDecoder<R> {
type Reader = Cursor<Vec<u8>>;

fn dimensions(&self) -> (u32, u32) {
let size = self.exr_reader.headers()[self.header_index].layer_size;
(size.width() as u32, size.height() as u32)
}

// TODO fn original_color_type(&self) -> ExtendedColorType {}

fn color_type(&self) -> ColorType {
// TODO adapt to actual exr color type
// let _channels = &self.exr_reader.headers()[self.header_index].channels;
ColorType::Rgba32F
}

/// Use `read_image` if possible,
/// as this method creates a whole new buffer to contain the entire image.
fn into_reader(self) -> ImageResult<Self::Reader> {
Ok(Cursor::new(image::decoder_to_vec(self)?)) // TODO no vec?
}

fn scanline_bytes(&self) -> u64 {
// we cannot always read individual scan lines for every file,
// as the tiles or lines in the file could be in random or reversed order.
// therefore we currently read all lines at once
// Todo: optimize for specific exr.line_order?
self.total_bytes()
}

fn read_image_with_progress<F: Fn(Progress)>(self, target: &mut [u8], progress_callback: F) -> ImageResult<()> {
let blocks_in_header = self.exr_reader.headers()[self.header_index].chunk_count as u64;
let height = self.dimensions().1 as usize;

let result = exr::prelude::read()
.no_deep_data().largest_resolution_level()
.rgba_channels(
|size, _channels|
(size, vec![0_f32; size.area()*4])
,

|(size, buffer), mut position, (r,g,b,a_or_1): (f32,f32,f32,f32)| {
// TODO white point chromaticities + srgb/linear conversion?
position.1 = height - (position.1 + 1); // openexr stores +y upwards, but we need +y downwards
let first_f32_index = position.flat_index_for_size(*size);
buffer[first_f32_index*4 .. (first_f32_index + 1)*4].copy_from_slice(&[r, g, b, a_or_1]);
}
)
.first_valid_layer() // TODO select exact layer by self.header_index?
.all_attributes()
.on_progress(move |progress| {
let mut prog = progress_callback;
prog(Progress::new((progress*blocks_in_header as f64) as u64, blocks_in_header)); // TODO precision errors?
johannesvollmer marked this conversation as resolved.
Show resolved Hide resolved
})
.from_chunks(self.exr_reader)?;

// TODO this copy is strictly not necessary, but the exr api is a little too simple for reading into a borrowed target slice
target.copy_from_slice(bytemuck::cast_slice(result.layer_data.channel_data.pixels.1.as_slice()));

// TODO keep meta data?
Ok(())
}
}



pub fn write_image(mut seekable_write: impl Write + Seek, bytes: &[u8], width: u32, height: u32, color_type: ColorType) -> ImageResult<()> {
let width = width as usize;
let height = height as usize;

let pixels: &[f32] = bytemuck::try_cast_slice(bytes).expect("image byte buffer must be aligned to f32");

match color_type {
ColorType::Rgb32F => {
exr::prelude::Image
Copy link
Contributor Author

Choose a reason for hiding this comment

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

OpenEXR supports many different compression methods, each with a different a trade-off between speed and compression ratio. By default, a fast but rather weak compression is used (plain run-length encoding). This could also use deflate compression, which is slower, but yields small images. What do you think?

::from_channels(
(width, height),
SpecificChannels::rgb(|mut pixel: Vec2<usize>| {
pixel.1 = height - (pixel.1 + 1); // openexr stores +y upwards, but we need +y downwards
let pixel_index = 3 * pixel.flat_index_for_size(Vec2(width, height));
(pixels[pixel_index], pixels[pixel_index+1], pixels[pixel_index+2])
})
)
.write()
// .on_progress(|progress| todo!())
.to_buffered(&mut seekable_write)?; // TODO BufWrite::new()?
}

ColorType::Rgba32F => {
exr::prelude::Image
::from_channels(
(width, height),
SpecificChannels::rgba(|mut pixel: Vec2<usize>| {
pixel.1 = height - (pixel.1 + 1); // openexr stores +y upwards, but we need +y downwards
let pixel_index = 4 * pixel.flat_index_for_size(Vec2(width, height));
(pixels[pixel_index], pixels[pixel_index+1], pixels[pixel_index+2], pixels[pixel_index+3])
})
)
.write()
// .on_progress(|progress| todo!())
.to_buffered(&mut seekable_write)?; // TODO BufWrite::new()?
}

_ => todo!()
}

Ok(())
}


impl From<exr::error::Error> for ImageError {
fn from(exr_error: Error) -> Self {
ImageError::Decoding(DecodingError::new(
ImageFormatHint::Exact(ImageFormat::Exr),
exr_error
))
}
}


18 changes: 18 additions & 0 deletions src/image.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,9 @@ pub enum ImageFormat {
/// An Image in Radiance HDR Format
Hdr,

/// An Image in OpenEXR Format
Exr,

/// An Image in farbfeld Format
Farbfeld,

Expand Down Expand Up @@ -94,6 +97,7 @@ impl ImageFormat {
"bmp" => ImageFormat::Bmp,
"ico" => ImageFormat::Ico,
"hdr" => ImageFormat::Hdr,
"exr" => ImageFormat::Exr,
"pbm" | "pam" | "ppm" | "pgm" => ImageFormat::Pnm,
"ff" | "farbfeld" => ImageFormat::Farbfeld,
_ => return None,
Expand Down Expand Up @@ -149,6 +153,7 @@ impl ImageFormat {
ImageFormat::Bmp => true,
ImageFormat::Ico => true,
ImageFormat::Hdr => true,
ImageFormat::Exr => true,
ImageFormat::Pnm => true,
ImageFormat::Farbfeld => true,
ImageFormat::Avif => true,
Expand All @@ -173,6 +178,7 @@ impl ImageFormat {
ImageFormat::Avif => true,
ImageFormat::WebP => false,
ImageFormat::Hdr => false,
ImageFormat::Exr => true,
ImageFormat::Dds => false,
ImageFormat::__NonExhaustive(marker) => match marker._private {},
}
Expand Down Expand Up @@ -200,6 +206,7 @@ impl ImageFormat {
ImageFormat::Bmp => &["bmp"],
ImageFormat::Ico => &["ico"],
ImageFormat::Hdr => &["hdr"],
ImageFormat::Exr => &["exr"],
ImageFormat::Farbfeld => &["ff"],
// According to: https://aomediacodec.github.io/av1-avif/#mime-registration
ImageFormat::Avif => &["avif"],
Expand Down Expand Up @@ -243,6 +250,10 @@ pub enum ImageOutputFormat {
/// An Image in TGA Format
Tga,

#[cfg(feature = "openexr")]
/// An Image in OpenEXR Format
Exr,

#[cfg(feature = "tiff")]
/// An Image in TIFF Format
Tiff,
Expand Down Expand Up @@ -279,6 +290,8 @@ impl From<ImageFormat> for ImageOutputFormat {
ImageFormat::Farbfeld => ImageOutputFormat::Farbfeld,
#[cfg(feature = "tga")]
ImageFormat::Tga => ImageOutputFormat::Tga,
#[cfg(feature = "openexr")]
ImageFormat::Exr => ImageOutputFormat::Exr,
#[cfg(feature = "tiff")]
ImageFormat::Tiff => ImageOutputFormat::Tiff,

Expand Down Expand Up @@ -511,6 +524,11 @@ pub struct Progress {
}

impl Progress {
/// Create Progress.
pub fn new(current: u64, total: u64) -> Self {
Self { current, total }
johannesvollmer marked this conversation as resolved.
Show resolved Hide resolved
}

/// A measure of completed decoding.
pub fn current(self) -> u64 {
self.current
Expand Down
11 changes: 10 additions & 1 deletion src/io/free_functions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ use crate::codecs::bmp;
use crate::codecs::gif;
#[cfg(feature = "hdr")]
use crate::codecs::hdr;
#[cfg(feature = "openexr")]
use crate::codecs::openexr;
#[cfg(feature = "ico")]
use crate::codecs::ico;
#[cfg(feature = "jpeg")]
Expand Down Expand Up @@ -81,6 +83,8 @@ pub fn load<R: BufRead + Seek>(r: R, format: ImageFormat) -> ImageResult<Dynamic
image::ImageFormat::Ico => DynamicImage::from_decoder(ico::IcoDecoder::new(r)?),
#[cfg(feature = "hdr")]
image::ImageFormat::Hdr => DynamicImage::from_decoder(hdr::HdrAdapter::new(BufReader::new(r))?),
#[cfg(feature = "openexr")]
image::ImageFormat::Exr => DynamicImage::from_decoder(openexr::ExrDecoder::read(r)?),
#[cfg(feature = "pnm")]
image::ImageFormat::Pnm => DynamicImage::from_decoder(pnm::PnmDecoder::new(BufReader::new(r))?),
HeroicKatora marked this conversation as resolved.
Show resolved Hide resolved
#[cfg(feature = "farbfeld")]
Expand Down Expand Up @@ -129,6 +133,8 @@ pub(crate) fn image_dimensions_with_format_impl<R: BufRead + Seek>(fin: R, forma
image::ImageFormat::Ico => ico::IcoDecoder::new(fin)?.dimensions(),
#[cfg(feature = "hdr")]
image::ImageFormat::Hdr => hdr::HdrAdapter::new(fin)?.dimensions(),
#[cfg(feature = "openexr")]
image::ImageFormat::Exr => openexr::ExrDecoder::read(fin)?.dimensions(),
#[cfg(feature = "pnm")]
image::ImageFormat::Pnm => {
pnm::PnmDecoder::new(fin)?.dimensions()
Expand Down Expand Up @@ -218,6 +224,8 @@ pub(crate) fn write_buffer_impl<W: std::io::Write>(
ImageOutputFormat::Farbfeld => farbfeld::FarbfeldEncoder::new(fout).write_image(buf, width, height, color),
#[cfg(feature = "tga")]
ImageOutputFormat::Tga => tga::TgaEncoder::new(fout).write_image(buf, width, height, color),
#[cfg(feature = "openexr")]
ImageOutputFormat::Exr => openexr::write_image(fout, buf, width, height, color),
#[cfg(feature = "tiff")]
ImageOutputFormat::Tiff => {
let mut cursor = std::io::Cursor::new(Vec::new());
Expand All @@ -240,7 +248,7 @@ pub(crate) fn write_buffer_impl<W: std::io::Write>(
}
}

static MAGIC_BYTES: [(&[u8], ImageFormat); 20] = [
static MAGIC_BYTES: [(&[u8], ImageFormat); 21] = [
(b"\x89PNG\r\n\x1a\n", ImageFormat::Png),
(&[0xff, 0xd8, 0xff], ImageFormat::Jpeg),
(b"GIF89a", ImageFormat::Gif),
Expand All @@ -261,6 +269,7 @@ static MAGIC_BYTES: [(&[u8], ImageFormat); 20] = [
(b"P7", ImageFormat::Pnm),
(b"farbfeld", ImageFormat::Farbfeld),
(b"\0\0\0 ftypavif", ImageFormat::Avif),
(&[0x76, 0x2f, 0x31, 0x01], ImageFormat::Exr), // = &exr::meta::magic_number::BYTES
];

/// Guess image format from memory block
Expand Down
9 changes: 6 additions & 3 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -184,14 +184,15 @@ pub mod flat;
/// | PNG | All supported color types | Same as decoding |
/// | JPEG | Baseline and progressive | Baseline JPEG |
/// | GIF | Yes | Yes |
/// | BMP | Yes | RGB8, RGBA8, Gray8, GrayA8 |
/// | BMP | Yes | Rgb8, Rgba8, Gray8, GrayA8 |
/// | ICO | Yes | Yes |
/// | TIFF | Baseline(no fax support) + LZW + PackBits | RGB8, RGBA8, Gray8 |
/// | TIFF | Baseline(no fax support) + LZW + PackBits | Rgb8, Rgba8, Gray8 |
/// | WebP | Lossy(Luma channel only) | No |
/// | AVIF | Only 8-bit | Lossy |
/// | PNM | PBM, PGM, PPM, standard PAM | Yes |
/// | DDS | DXT1, DXT3, DXT5 | No |
/// | TGA | Yes | RGB8, RGBA8, BGR8, BGRA8, Gray8, GrayA8 |
/// | TGA | Yes | Rgb8, Rgba8, Bgr8, Bgra8, Gray8, GrayA8 |
/// | OpenEXR | Rgb32F, Rgba32F (no dwa compression) | Rgb32F, Rgba32F (no dwa compression) |
/// | farbfeld | Yes | Yes |
///
/// ## A note on format specific features
Expand Down Expand Up @@ -244,6 +245,8 @@ pub mod codecs {
pub mod tiff;
#[cfg(feature = "webp")]
pub mod webp;
#[cfg(feature = "openexr")]
pub mod openexr;
}

#[cfg(feature = "avif-encoder")]
Expand Down