Skip to content

Commit e37edb3

Browse files
authored
feat: Implement copy_image for OpenEXR (#4004)
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 <andychancse@gmail.com>
1 parent 41a26ca commit e37edb3

File tree

7 files changed

+338
-184
lines changed

7 files changed

+338
-184
lines changed

src/cmake/testing.cmake

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -280,6 +280,9 @@ macro (oiio_add_all_tests)
280280
set (all_openexr_tests
281281
openexr-suite openexr-multires openexr-chroma
282282
openexr-v2 openexr-window perchannel oiiotool-deep)
283+
if (USE_PYTHON AND NOT SANITIZE)
284+
list (APPEND all_openexr_tests openexr-copy)
285+
endif ()
283286
if (OpenEXR_VERSION VERSION_GREATER_EQUAL 3.1.10)
284287
# OpenEXR 3.1.10 is the first release where the exr core library
285288
# properly supported all compression types (DWA in particular).

src/openexr.imageio/exr_pvt.h

Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,17 @@
66

77

88
#include <OpenImageIO/Imath.h>
9+
#include <OpenImageIO/filesystem.h>
10+
#include <OpenImageIO/imageio.h>
911
#include <OpenImageIO/platform.h>
1012
#include <OpenImageIO/string_view.h>
1113
#include <OpenImageIO/typedesc.h>
1214

15+
#include <ImathBox.h>
16+
#include <OpenEXR/IexThrowErrnoExc.h>
1317
#include <OpenEXR/ImfChannelList.h>
18+
#include <OpenEXR/ImfIO.h>
19+
#include <OpenEXR/ImfRgbaFile.h>
1420

1521
#ifdef OPENEXR_VERSION_MAJOR
1622
# define OPENEXR_CODED_VERSION \
@@ -65,4 +71,192 @@ split_name(string_view fullname, string_view& layer, string_view& suffix)
6571
}
6672

6773

74+
75+
// Custom file input stream, copying code from the class StdIFStream in OpenEXR,
76+
// which would have been used if we just provided a filename. The difference is
77+
// that this can handle UTF-8 file paths on all platforms.
78+
class OpenEXRInputStream final : public Imf::IStream {
79+
public:
80+
OpenEXRInputStream(const char* filename, Filesystem::IOProxy* io)
81+
: Imf::IStream(filename)
82+
, m_io(io)
83+
{
84+
if (!io || io->mode() != Filesystem::IOProxy::Read)
85+
throw Iex::IoExc("File input failed.");
86+
}
87+
bool read(char c[], int n) override
88+
{
89+
OIIO_DASSERT(m_io);
90+
if (m_io->read(c, n) != size_t(n))
91+
throw Iex::IoExc("Unexpected end of file.");
92+
return n;
93+
}
94+
#if OIIO_USING_IMATH >= 3
95+
uint64_t tellg() override { return m_io->tell(); }
96+
void seekg(uint64_t pos) override
97+
{
98+
if (!m_io->seek(pos))
99+
throw Iex::IoExc("File input failed.");
100+
}
101+
#else
102+
Imath::Int64 tellg() override { return m_io->tell(); }
103+
void seekg(Imath::Int64 pos) override
104+
{
105+
if (!m_io->seek(pos))
106+
throw Iex::IoExc("File input failed.");
107+
}
108+
#endif
109+
void clear() override {}
110+
111+
private:
112+
Filesystem::IOProxy* m_io = nullptr;
113+
};
114+
115+
116+
117+
class OpenEXRInput final : public ImageInput {
118+
public:
119+
OpenEXRInput();
120+
~OpenEXRInput() override { close(); }
121+
const char* format_name(void) const override { return "openexr"; }
122+
int supports(string_view feature) const override
123+
{
124+
return (feature == "arbitrary_metadata"
125+
|| feature == "exif" // Because of arbitrary_metadata
126+
|| feature == "iptc" // Because of arbitrary_metadata
127+
|| feature == "ioproxy");
128+
}
129+
bool valid_file(Filesystem::IOProxy* ioproxy) const override;
130+
bool open(const std::string& name, ImageSpec& newspec,
131+
const ImageSpec& config) override;
132+
bool open(const std::string& name, ImageSpec& newspec) override
133+
{
134+
return open(name, newspec, ImageSpec());
135+
}
136+
bool close() override;
137+
int current_subimage(void) const override { return m_subimage; }
138+
int current_miplevel(void) const override { return m_miplevel; }
139+
bool seek_subimage(int subimage, int miplevel) override;
140+
ImageSpec spec(int subimage, int miplevel) override;
141+
ImageSpec spec_dimensions(int subimage, int miplevel) override;
142+
bool read_native_scanline(int subimage, int miplevel, int y, int z,
143+
void* data) override;
144+
bool read_native_scanlines(int subimage, int miplevel, int ybegin, int yend,
145+
int z, void* data) override;
146+
bool read_native_scanlines(int subimage, int miplevel, int ybegin, int yend,
147+
int z, int chbegin, int chend,
148+
void* data) override;
149+
bool read_native_tile(int subimage, int miplevel, int x, int y, int z,
150+
void* data) override;
151+
bool read_native_tiles(int subimage, int miplevel, int xbegin, int xend,
152+
int ybegin, int yend, int zbegin, int zend,
153+
void* data) override;
154+
bool read_native_tiles(int subimage, int miplevel, int xbegin, int xend,
155+
int ybegin, int yend, int zbegin, int zend,
156+
int chbegin, int chend, void* data) override;
157+
bool read_native_deep_scanlines(int subimage, int miplevel, int ybegin,
158+
int yend, int z, int chbegin, int chend,
159+
DeepData& deepdata) override;
160+
bool read_native_deep_tiles(int subimage, int miplevel, int xbegin,
161+
int xend, int ybegin, int yend, int zbegin,
162+
int zend, int chbegin, int chend,
163+
DeepData& deepdata) override;
164+
165+
bool set_ioproxy(Filesystem::IOProxy* ioproxy) override
166+
{
167+
m_io = ioproxy;
168+
return true;
169+
}
170+
171+
private:
172+
struct PartInfo {
173+
std::atomic_bool initialized;
174+
ImageSpec spec;
175+
int topwidth; ///< Width of top mip level
176+
int topheight; ///< Height of top mip level
177+
int levelmode; ///< The level mode
178+
int roundingmode; ///< Rounding mode
179+
bool cubeface; ///< It's a cubeface environment map
180+
bool luminance_chroma; ///< It's a luminance chroma image
181+
int nmiplevels; ///< How many MIP levels are there?
182+
Imath::Box2i top_datawindow;
183+
Imath::Box2i top_displaywindow;
184+
std::vector<Imf::PixelType> pixeltype; ///< Imf pixel type for each chan
185+
std::vector<int> chanbytes; ///< Size (in bytes) of each channel
186+
187+
PartInfo()
188+
: initialized(false)
189+
{
190+
}
191+
PartInfo(const PartInfo& p)
192+
: initialized((bool)p.initialized)
193+
, spec(p.spec)
194+
, topwidth(p.topwidth)
195+
, topheight(p.topheight)
196+
, levelmode(p.levelmode)
197+
, roundingmode(p.roundingmode)
198+
, cubeface(p.cubeface)
199+
, luminance_chroma(p.luminance_chroma)
200+
, nmiplevels(p.nmiplevels)
201+
, top_datawindow(p.top_datawindow)
202+
, top_displaywindow(p.top_displaywindow)
203+
, pixeltype(p.pixeltype)
204+
, chanbytes(p.chanbytes)
205+
{
206+
}
207+
~PartInfo() {}
208+
bool parse_header(OpenEXRInput* in, const Imf::Header* header);
209+
bool query_channels(OpenEXRInput* in, const Imf::Header* header);
210+
void compute_mipres(int miplevel, ImageSpec& spec) const;
211+
};
212+
friend struct PartInfo;
213+
214+
std::vector<PartInfo> m_parts; ///< Image parts
215+
OpenEXRInputStream* m_input_stream; ///< Stream for input file
216+
Imf::MultiPartInputFile* m_input_multipart; ///< Multipart input
217+
Imf::InputPart* m_scanline_input_part;
218+
Imf::TiledInputPart* m_tiled_input_part;
219+
Imf::DeepScanLineInputPart* m_deep_scanline_input_part;
220+
Imf::DeepTiledInputPart* m_deep_tiled_input_part;
221+
Imf::RgbaInputFile* m_input_rgba;
222+
Filesystem::IOProxy* m_io = nullptr;
223+
std::unique_ptr<Filesystem::IOProxy> m_local_io;
224+
int m_subimage; ///< What subimage are we looking at?
225+
int m_nsubimages; ///< How many subimages are there?
226+
int m_miplevel; ///< What MIP level are we looking at?
227+
std::vector<float> m_missingcolor; ///< Color for missing tile/scanline
228+
229+
void init()
230+
{
231+
m_input_stream = NULL;
232+
m_input_multipart = NULL;
233+
m_scanline_input_part = NULL;
234+
m_tiled_input_part = NULL;
235+
m_deep_scanline_input_part = NULL;
236+
m_deep_tiled_input_part = NULL;
237+
m_input_rgba = NULL;
238+
m_subimage = -1;
239+
m_miplevel = -1;
240+
m_io = nullptr;
241+
m_local_io.reset();
242+
m_missingcolor.clear();
243+
}
244+
245+
bool read_native_tiles_individually(int subimage, int miplevel, int xbegin,
246+
int xend, int ybegin, int yend,
247+
int zbegin, int zend, int chbegin,
248+
int chend, void* data, stride_t xstride,
249+
stride_t ystride);
250+
251+
// Fill in with 'missing' color/pattern.
252+
void fill_missing(int xbegin, int xend, int ybegin, int yend, int zbegin,
253+
int zend, int chbegin, int chend, void* data,
254+
stride_t xstride, stride_t ystride);
255+
256+
// Prepare friend function for copyPixels
257+
friend class OpenEXROutput;
258+
};
259+
260+
261+
68262
OIIO_PLUGIN_NAMESPACE_END

0 commit comments

Comments
 (0)