From e37edb336b38787d43a2b655f6254dd9f4d2402e Mon Sep 17 00:00:00 2001 From: Andy Chan Date: Mon, 18 Mar 2024 14:57:12 -0700 Subject: [PATCH] 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 --- src/cmake/testing.cmake | 3 + src/openexr.imageio/exr_pvt.h | 194 ++++++++++++++++++ src/openexr.imageio/exrinput.cpp | 184 ----------------- src/openexr.imageio/exroutput.cpp | 56 +++++ testsuite/openexr-copy/ref/out.txt | 10 + testsuite/openexr-copy/run.py | 40 ++++ .../openexr-copy/src/test_recompression.py | 35 ++++ 7 files changed, 338 insertions(+), 184 deletions(-) create mode 100644 testsuite/openexr-copy/ref/out.txt create mode 100644 testsuite/openexr-copy/run.py create mode 100644 testsuite/openexr-copy/src/test_recompression.py diff --git a/src/cmake/testing.cmake b/src/cmake/testing.cmake index 8b26cb46aa..2cd32dc58f 100644 --- a/src/cmake/testing.cmake +++ b/src/cmake/testing.cmake @@ -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). diff --git a/src/openexr.imageio/exr_pvt.h b/src/openexr.imageio/exr_pvt.h index ba7ee75e2b..635971eb86 100644 --- a/src/openexr.imageio/exr_pvt.h +++ b/src/openexr.imageio/exr_pvt.h @@ -6,11 +6,17 @@ #include +#include +#include #include #include #include +#include +#include #include +#include +#include #ifdef OPENEXR_VERSION_MAJOR # define OPENEXR_CODED_VERSION \ @@ -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 pixeltype; ///< Imf pixel type for each chan + std::vector 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 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 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 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 diff --git a/src/openexr.imageio/exrinput.cpp b/src/openexr.imageio/exrinput.cpp index 7523a2b642..3c9933c7bc 100644 --- a/src/openexr.imageio/exrinput.cpp +++ b/src/openexr.imageio/exrinput.cpp @@ -51,7 +51,6 @@ OIIO_GCC_PRAGMA(GCC diagnostic ignored "-Wunused-parameter") #include #include #include -#include #include #include #include @@ -80,189 +79,6 @@ OIIO_PRAGMA_VISIBILITY_POP OIIO_PLUGIN_NAMESPACE_BEGIN -// 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 pixeltype; ///< Imf pixel type for each chan - std::vector 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 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 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 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); -}; - - // Obligatory material to make this a recognizable imageio plugin: OIIO_PLUGIN_EXPORTS_BEGIN diff --git a/src/openexr.imageio/exroutput.cpp b/src/openexr.imageio/exroutput.cpp index 8e3dd21562..ccc32bc78a 100644 --- a/src/openexr.imageio/exroutput.cpp +++ b/src/openexr.imageio/exroutput.cpp @@ -20,6 +20,7 @@ #include #include +#include "exr_pvt.h" #define OPENEXR_CODED_VERSION \ (OPENEXR_VERSION_MAJOR * 10000 + OPENEXR_VERSION_MINOR * 100 \ + OPENEXR_VERSION_PATCH) @@ -118,6 +119,7 @@ class OpenEXROutput final : public ImageOutput { bool open(const std::string& name, int subimages, const ImageSpec* specs) override; bool close() override; + bool copy_image(ImageInput* in) override; bool write_scanline(int y, int z, TypeDesc format, const void* data, stride_t xstride) override; bool write_scanlines(int ybegin, int yend, int z, TypeDesc format, @@ -1403,6 +1405,60 @@ OpenEXROutput::write_scanline(int y, int z, TypeDesc format, const void* data, +bool +OpenEXROutput::copy_image(ImageInput* in) +{ + if (in && !strcmp(in->format_name(), "openexr")) { + if (OpenEXRInput* exr_in = dynamic_cast(in)) { + // Copy over pixels without decompression. + try { + if (m_output_scanline && exr_in->m_scanline_input_part) { + m_output_scanline->copyPixels( + *exr_in->m_scanline_input_part); + return true; + } else if (m_output_tiled && exr_in->m_tiled_input_part + && m_nmiplevels == 0) { + m_output_tiled->copyPixels(*exr_in->m_tiled_input_part); + return true; + } else if (m_scanline_output_part + && exr_in->m_scanline_input_part) { + m_scanline_output_part->copyPixels( + *exr_in->m_scanline_input_part); + return true; + } else if (m_tiled_output_part && exr_in->m_tiled_input_part + && m_nmiplevels == 0) { + m_tiled_output_part->copyPixels( + *exr_in->m_tiled_input_part); + return true; + } else if (m_deep_scanline_output_part + && exr_in->m_deep_scanline_input_part) { + m_deep_scanline_output_part->copyPixels( + *exr_in->m_deep_scanline_input_part); + return true; + } else if (m_deep_tiled_output_part + && exr_in->m_deep_tiled_input_part + && m_nmiplevels == 0) { + m_deep_tiled_output_part->copyPixels( + *exr_in->m_deep_tiled_input_part); + return true; + } + } catch (const std::exception& e) { + errorf( + "Failed OpenEXR copy: %s, falling back to the default image copy routine.", + e.what()); + return false; + } catch (...) { // catch-all for edge cases or compiler bugs + errorf( + "Failed OpenEXR copy: unknown exception, falling back to the default image copy routine."); + return false; + } + } + } + return ImageOutput::copy_image(in); +} + + + bool OpenEXROutput::write_scanlines(int ybegin, int yend, int z, TypeDesc format, const void* data, stride_t xstride, diff --git a/testsuite/openexr-copy/ref/out.txt b/testsuite/openexr-copy/ref/out.txt new file mode 100644 index 0000000000..a7a87a14c7 --- /dev/null +++ b/testsuite/openexr-copy/ref/out.txt @@ -0,0 +1,10 @@ +Comparing "compressed-b44.exr" and "ref/compressed-b44.exr" +PASS +Comparing "compressed-b44a.exr" and "ref/compressed-b44a.exr" +PASS +Comparing "compressed-dwaa.exr" and "ref/compressed-dwaa.exr" +PASS +Comparing "compressed-dwab.exr" and "ref/compressed-dwab.exr" +PASS +Comparing "compressed-pxr24.exr" and "ref/compressed-pxr24.exr" +PASS diff --git a/testsuite/openexr-copy/run.py b/testsuite/openexr-copy/run.py new file mode 100644 index 0000000000..49418c6acd --- /dev/null +++ b/testsuite/openexr-copy/run.py @@ -0,0 +1,40 @@ +#!/usr/bin/env python + +# Copyright Contributors to the OpenImageIO project. +# SPDX-License-Identifier: Apache-2.0 +# https://github.com/AcademySoftwareFoundation/OpenImageIO + +import os, sys + +#################################################################### +# This test exercises oiiotool functionality that is mostly about +# copying pixels from one image to another. +#################################################################### + +# Capture error output +redirect = " >> out.txt 2>&1 " + +# Create some test images we need +command += oiiotool("../common/tahoe-tiny.tif --compression pxr24 -o ref/compressed-pxr24.exr") +command += oiiotool("../common/tahoe-tiny.tif --compression b44 -o ref/compressed-b44.exr") +command += oiiotool("../common/tahoe-tiny.tif --compression b44a -o ref/compressed-b44a.exr") +command += oiiotool("../common/tahoe-tiny.tif --compression dwaa -o ref/compressed-dwaa.exr") +command += oiiotool("../common/tahoe-tiny.tif --compression dwab -o ref/compressed-dwab.exr") + +# Run the recompression test script +command += pythonbin + " src/test_recompression.py;" + +# Outputs to check against references +outputs = [ + "compressed-pxr24.exr", + "compressed-b44.exr", + "compressed-b44a.exr", + "compressed-dwaa.exr", + "compressed-dwab.exr", +] + +# OpenEXRInputCore is not supported yet. TODO: update this once it's implemented. +if os.environ.get("OPENIMAGEIO_OPTIONS") == "openexr:core=1": + outputs = [] + +#print "Running this command:\n" + command + "\n" diff --git a/testsuite/openexr-copy/src/test_recompression.py b/testsuite/openexr-copy/src/test_recompression.py new file mode 100644 index 0000000000..7547804ca0 --- /dev/null +++ b/testsuite/openexr-copy/src/test_recompression.py @@ -0,0 +1,35 @@ +#!/usr/bin/env python + +import OpenImageIO as oiio + +# Read the one subimage from input then write it to output using +# copy_image, thus skipping both decompression and recompression steps +# (Just copy one subimage, one MIP level.) +def copy_image (in_filename, out_filename) : + input = oiio.ImageInput.open (in_filename) + if not input : + print ('Could not open "' + in_filename + '"') + print ("\tError: ", oiio.geterror()) + print () + return + outspec = input.spec() + output = oiio.ImageOutput.create (out_filename) + if not output : + print ("Could not create ImageOutput for", out_filename) + return + ok = output.open (out_filename, outspec) + if not ok : + print ("Could not open", out_filename) + return + ok = output.copy_image(input) + input.close () + output.close () + if ok : + print ("Copied", in_filename, "to", out_filename) + +# Copy lossy compressed image, this should not recompress the image again (loss of data). +copy_image("ref/compressed-pxr24.exr", "compressed-pxr24.exr") +copy_image("ref/compressed-b44.exr", "compressed-b44.exr") +copy_image("ref/compressed-b44a.exr", "compressed-b44a.exr") +copy_image("ref/compressed-dwaa.exr", "compressed-dwaa.exr") +copy_image("ref/compressed-dwab.exr", "compressed-dwab.exr") \ No newline at end of file