Skip to content

Commit

Permalink
feat: Implement copy_image for OpenEXR (#4004)
Browse files Browse the repository at this point in the history
Base on the [OIIO slack channel
discussion](https://academysoftwarefdn.slack.com/archives/C05782U3806/p1689031780561469),
this PR implements the `copy_image` function for the OpenEXR format. The
main motivation of this PR was due to a recompression issue where
copying a lossy compressed EXR image (pixels are decoded and then
encoded again), which can cause the image quality to downgrade on each
iteration.

- `OpenEXROutput` is now a friend class of `OpenEXRInput`. Since
`OpenEXROutput::copy_image()` needs to access some of the private
members in `OpenEXRInput` (This approach is referencing the
implementation in `JpegInput` and `JpegOutput`).
- The `OpenEXRInputStream` and `OpenEXRInput` class have been moved from
`exrinput.cpp` to `exr_pvt.h`. As `OpenEXRInput` depends on
`OpenEXRInputStream`, so both have to be moved together. Such relocation
is necessary due to the above reason, i.e. `OpenEXROutput::copy_image()`
needs to access some of the private members in `OpenEXRInput`.
- And the implementation itself, `OpenEXROutput::copy_image()` overrides
the base class method just like `JpegOutput` does.

The `OpenEXROutput::copy_image()` implementation took the advantages of
the existing OpenEXR function `Imf::OutputFile::copyPixels`, quoting the
comment from the function:
>  //--------------------------------------------------------------
    // Shortcut to copy all pixels from an InputFile into this file,
    // without uncompressing and then recompressing the pixel data.
    // This file's header must be compatible with the InputFile's
    // header:  The two header's "dataWindow", "compression",
    // "lineOrder" and "channels" attributes must be the same.
    //--------------------------------------------------------------
    IMF_EXPORT void copyPixels (InputFile &in);

When the output image is copied from an input image, the pixel data can
only be located in one of the input parts. So it checks if both input
and output part is initialized (non-null), then invoke the `copyPixels`
function in place.

All the error handling is done within the OpenEXR `copyPixels` call.
There is a try-catch block in OIIO side that emits an error message and
fallback to the default image copy routine, e.g.
`ImageOutput::copy_image`.

Note that currently, this doesn't do anything optimized for the
"core library" code path.

---------

Signed-off-by: Andy Chan <[email protected]>
  • Loading branch information
tkchanat authored Mar 18, 2024
1 parent 41a26ca commit e37edb3
Show file tree
Hide file tree
Showing 7 changed files with 338 additions and 184 deletions.
3 changes: 3 additions & 0 deletions src/cmake/testing.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,9 @@ macro (oiio_add_all_tests)
set (all_openexr_tests
openexr-suite openexr-multires openexr-chroma
openexr-v2 openexr-window perchannel oiiotool-deep)
if (USE_PYTHON AND NOT SANITIZE)
list (APPEND all_openexr_tests openexr-copy)
endif ()
if (OpenEXR_VERSION VERSION_GREATER_EQUAL 3.1.10)
# OpenEXR 3.1.10 is the first release where the exr core library
# properly supported all compression types (DWA in particular).
Expand Down
194 changes: 194 additions & 0 deletions src/openexr.imageio/exr_pvt.h
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,17 @@


#include <OpenImageIO/Imath.h>
#include <OpenImageIO/filesystem.h>
#include <OpenImageIO/imageio.h>
#include <OpenImageIO/platform.h>
#include <OpenImageIO/string_view.h>
#include <OpenImageIO/typedesc.h>

#include <ImathBox.h>
#include <OpenEXR/IexThrowErrnoExc.h>
#include <OpenEXR/ImfChannelList.h>
#include <OpenEXR/ImfIO.h>
#include <OpenEXR/ImfRgbaFile.h>

#ifdef OPENEXR_VERSION_MAJOR
# define OPENEXR_CODED_VERSION \
Expand Down Expand Up @@ -65,4 +71,192 @@ split_name(string_view fullname, string_view& layer, string_view& suffix)
}



// Custom file input stream, copying code from the class StdIFStream in OpenEXR,
// which would have been used if we just provided a filename. The difference is
// that this can handle UTF-8 file paths on all platforms.
class OpenEXRInputStream final : public Imf::IStream {
public:
OpenEXRInputStream(const char* filename, Filesystem::IOProxy* io)
: Imf::IStream(filename)
, m_io(io)
{
if (!io || io->mode() != Filesystem::IOProxy::Read)
throw Iex::IoExc("File input failed.");
}
bool read(char c[], int n) override
{
OIIO_DASSERT(m_io);
if (m_io->read(c, n) != size_t(n))
throw Iex::IoExc("Unexpected end of file.");
return n;
}
#if OIIO_USING_IMATH >= 3
uint64_t tellg() override { return m_io->tell(); }
void seekg(uint64_t pos) override
{
if (!m_io->seek(pos))
throw Iex::IoExc("File input failed.");
}
#else
Imath::Int64 tellg() override { return m_io->tell(); }
void seekg(Imath::Int64 pos) override
{
if (!m_io->seek(pos))
throw Iex::IoExc("File input failed.");
}
#endif
void clear() override {}

private:
Filesystem::IOProxy* m_io = nullptr;
};



class OpenEXRInput final : public ImageInput {
public:
OpenEXRInput();
~OpenEXRInput() override { close(); }
const char* format_name(void) const override { return "openexr"; }
int supports(string_view feature) const override
{
return (feature == "arbitrary_metadata"
|| feature == "exif" // Because of arbitrary_metadata
|| feature == "iptc" // Because of arbitrary_metadata
|| feature == "ioproxy");
}
bool valid_file(Filesystem::IOProxy* ioproxy) const override;
bool open(const std::string& name, ImageSpec& newspec,
const ImageSpec& config) override;
bool open(const std::string& name, ImageSpec& newspec) override
{
return open(name, newspec, ImageSpec());
}
bool close() override;
int current_subimage(void) const override { return m_subimage; }
int current_miplevel(void) const override { return m_miplevel; }
bool seek_subimage(int subimage, int miplevel) override;
ImageSpec spec(int subimage, int miplevel) override;
ImageSpec spec_dimensions(int subimage, int miplevel) override;
bool read_native_scanline(int subimage, int miplevel, int y, int z,
void* data) override;
bool read_native_scanlines(int subimage, int miplevel, int ybegin, int yend,
int z, void* data) override;
bool read_native_scanlines(int subimage, int miplevel, int ybegin, int yend,
int z, int chbegin, int chend,
void* data) override;
bool read_native_tile(int subimage, int miplevel, int x, int y, int z,
void* data) override;
bool read_native_tiles(int subimage, int miplevel, int xbegin, int xend,
int ybegin, int yend, int zbegin, int zend,
void* data) override;
bool read_native_tiles(int subimage, int miplevel, int xbegin, int xend,
int ybegin, int yend, int zbegin, int zend,
int chbegin, int chend, void* data) override;
bool read_native_deep_scanlines(int subimage, int miplevel, int ybegin,
int yend, int z, int chbegin, int chend,
DeepData& deepdata) override;
bool read_native_deep_tiles(int subimage, int miplevel, int xbegin,
int xend, int ybegin, int yend, int zbegin,
int zend, int chbegin, int chend,
DeepData& deepdata) override;

bool set_ioproxy(Filesystem::IOProxy* ioproxy) override
{
m_io = ioproxy;
return true;
}

private:
struct PartInfo {
std::atomic_bool initialized;
ImageSpec spec;
int topwidth; ///< Width of top mip level
int topheight; ///< Height of top mip level
int levelmode; ///< The level mode
int roundingmode; ///< Rounding mode
bool cubeface; ///< It's a cubeface environment map
bool luminance_chroma; ///< It's a luminance chroma image
int nmiplevels; ///< How many MIP levels are there?
Imath::Box2i top_datawindow;
Imath::Box2i top_displaywindow;
std::vector<Imf::PixelType> pixeltype; ///< Imf pixel type for each chan
std::vector<int> chanbytes; ///< Size (in bytes) of each channel

PartInfo()
: initialized(false)
{
}
PartInfo(const PartInfo& p)
: initialized((bool)p.initialized)
, spec(p.spec)
, topwidth(p.topwidth)
, topheight(p.topheight)
, levelmode(p.levelmode)
, roundingmode(p.roundingmode)
, cubeface(p.cubeface)
, luminance_chroma(p.luminance_chroma)
, nmiplevels(p.nmiplevels)
, top_datawindow(p.top_datawindow)
, top_displaywindow(p.top_displaywindow)
, pixeltype(p.pixeltype)
, chanbytes(p.chanbytes)
{
}
~PartInfo() {}
bool parse_header(OpenEXRInput* in, const Imf::Header* header);
bool query_channels(OpenEXRInput* in, const Imf::Header* header);
void compute_mipres(int miplevel, ImageSpec& spec) const;
};
friend struct PartInfo;

std::vector<PartInfo> m_parts; ///< Image parts
OpenEXRInputStream* m_input_stream; ///< Stream for input file
Imf::MultiPartInputFile* m_input_multipart; ///< Multipart input
Imf::InputPart* m_scanline_input_part;
Imf::TiledInputPart* m_tiled_input_part;
Imf::DeepScanLineInputPart* m_deep_scanline_input_part;
Imf::DeepTiledInputPart* m_deep_tiled_input_part;
Imf::RgbaInputFile* m_input_rgba;
Filesystem::IOProxy* m_io = nullptr;
std::unique_ptr<Filesystem::IOProxy> m_local_io;
int m_subimage; ///< What subimage are we looking at?
int m_nsubimages; ///< How many subimages are there?
int m_miplevel; ///< What MIP level are we looking at?
std::vector<float> m_missingcolor; ///< Color for missing tile/scanline

void init()
{
m_input_stream = NULL;
m_input_multipart = NULL;
m_scanline_input_part = NULL;
m_tiled_input_part = NULL;
m_deep_scanline_input_part = NULL;
m_deep_tiled_input_part = NULL;
m_input_rgba = NULL;
m_subimage = -1;
m_miplevel = -1;
m_io = nullptr;
m_local_io.reset();
m_missingcolor.clear();
}

bool read_native_tiles_individually(int subimage, int miplevel, int xbegin,
int xend, int ybegin, int yend,
int zbegin, int zend, int chbegin,
int chend, void* data, stride_t xstride,
stride_t ystride);

// Fill in with 'missing' color/pattern.
void fill_missing(int xbegin, int xend, int ybegin, int yend, int zbegin,
int zend, int chbegin, int chend, void* data,
stride_t xstride, stride_t ystride);

// Prepare friend function for copyPixels
friend class OpenEXROutput;
};



OIIO_PLUGIN_NAMESPACE_END
Loading

0 comments on commit e37edb3

Please sign in to comment.