Magnum::Trade::OpenExrImporter class new in Git master

OpenEXR importer plugin.

Imports OpenEXR (*.exr) images using the OpenEXR library. You can use OpenExrImageConverter to encode images into this format.

Usage

This plugin depends on the Trade library and is built if MAGNUM_WITH_OPENEXRIMPORTER is enabled when building Magnum Plugins. To use as a dynamic plugin, load "OpenExrImporter" via Corrade::PluginManager::Manager.

Additionally, if you're using Magnum as a CMake subproject, bundle the magnum-plugins and openexr repositories (pin OpenEXR at v3.0.1 at least) and do the following. OpenEXR depends on libdeflate (versions before 3.2.0 on zlib) and Imath, however it's capable of fetching those dependencies on its own so bundling them isn't necessary. If you want to use system-installed OpenEXR, omit the first part and point CMAKE_PREFIX_PATH to its installation dir if necessary.

# Disable unneeded functionality
set(PYILMBASE_ENABLE OFF CACHE BOOL "" FORCE)
set(IMATH_INSTALL_PKG_CONFIG OFF CACHE BOOL "" FORCE)
set(IMATH_INSTALL_SYM_LINK OFF CACHE BOOL "" FORCE)
set(OPENEXR_INSTALL OFF CACHE BOOL "" FORCE)
set(OPENEXR_INSTALL_DOCS OFF CACHE BOOL "" FORCE)
set(OPENEXR_INSTALL_EXAMPLES OFF CACHE BOOL "" FORCE)
set(OPENEXR_INSTALL_PKG_CONFIG OFF CACHE BOOL "" FORCE)
set(OPENEXR_INSTALL_TOOLS OFF CACHE BOOL "" FORCE)
# OPENEXR_BUILD_TOOLS used by 3.1+, OPENEXR_BUILD_UTILS by versions before
set(OPENEXR_BUILD_TOOLS OFF CACHE BOOL "" FORCE)
set(OPENEXR_BUILD_UTILS OFF CACHE BOOL "" FORCE)
# Otherwise OpenEXR uses C++14, and before OpenEXR 3.0.2 also forces C++14 on
# all libraries that link to it.
set(OPENEXR_CXX_STANDARD 11 CACHE STRING "" FORCE)
# OpenEXR implicitly bundles Imath. However, without this only the first CMake
# run will pass and subsequent runs will fail.
set(CMAKE_DISABLE_FIND_PACKAGE_Imath ON)
# These variables may be used by other projects, so ensure they're reset back
# to their original values after. OpenEXR forces CMAKE_DEBUG_POSTFIX to _d,
# which isn't desired outside of that library.
set(_PREV_BUILD_SHARED_LIBS ${BUILD_SHARED_LIBS})
set(_PREV_BUILD_TESTING ${BUILD_TESTING})
set(BUILD_SHARED_LIBS OFF)
set(BUILD_TESTING OFF)
set(CMAKE_DEBUG_POSTFIX "" CACHE STRING "" FORCE)
add_subdirectory(openexr EXCLUDE_FROM_ALL)
set(BUILD_SHARED_LIBS ${_PREV_BUILD_SHARED_LIBS})
set(BUILD_TESTING ${_PREV_BUILD_TESTING})
unset(CMAKE_DEBUG_POSTFIX CACHE)

set(MAGNUM_WITH_OPENEXRIMPORTER ON CACHE BOOL "" FORCE)
add_subdirectory(magnum-plugins EXCLUDE_FROM_ALL)

# So the dynamically loaded plugin gets built implicitly
add_dependencies(your-app MagnumPlugins::OpenExrImporter)

To use as a static plugin or as a dependency of another plugin with CMake, put FindMagnumPlugins.cmake and FindOpenEXR.cmake into your modules/ directory, request the OpenExrImporter component of the MagnumPlugins package in CMake and link to the MagnumPlugins::OpenExrImporter target:

find_package(MagnumPlugins REQUIRED OpenExrImporter)

# ...
target_link_libraries(your-app PRIVATE MagnumPlugins::OpenExrImporter)

See Downloading and building plugins, Plugin usage with CMake, Loading and using plugins and File format support for more information.

Building for Emscripten

Since version 3.2, the library can compile for Emscripten as well with the following additionally set before add_subdirectory(openexr). OpenEXR uses exceptions, so the plugin internally also enables them for itself and all code that links to it.

# Force bundled Imath and libdeflate so Emscripten doesn't try to find them
# among native libraries
set(OPENEXR_FORCE_INTERNAL_IMATH ON CACHE BOOL "" FORCE)
set(OPENEXR_FORCE_INTERNAL_DEFLATE ON CACHE BOOL "" FORCE)
# May want to keep enabled if you already use pthreads in Emscripten
set(OPENEXR_ENABLE_THREADING OFF CACHE BOOL "" FORCE)
# The table is literally all half-floats, so 256 kB of data
set(IMATH_HALF_USE_LOOKUP_TABLE OFF CACHE BOOL "" FORCE)

In addition to the above-disabled half-float conversion lookup table, there's 1.4 MB + 256 kB of tables used for DWA compression and decompression, and another 128 + 128 kB of tables used for B44 (de)compression, of which none can be easily controlled with a CMake option. If you know you won't need DWA or B44 compression with OpenExrImageConverter, you can replace all tables except the first two dwaCompressorNoOp and dwaCompressorToLinear in src/lib/OpenEXR/dwaLookups.h with the following (or replace dwaCompressorNoOp and dwaCompressorToLinear with a null pointer too if you know you won't need DWA loading either):

static const unsigned short* dwaCompressorToNonlinear = NULL;
static const unsigned int* closestDataOffset = NULL;
static const unsigned short* closestData = NULL;

And similarly replace the expTable in src/lib/OpenEXR/b44ExpLogTable (or also the logTable if you know you won't need B44 loading either):

const unsigned short* expTable = NULL;

Together with the disabled half-float conversion table this reduces the OpenExrImporter plugin size from 2.5 MB to ~800 kB. If OpenExrImageConverter is built against such patched code, attempting to compress DWA or B44 images with it will cause a crash, but other compression schemes will continue to work.

Behavior and limitations

Supports single-part scanline and tile-based 2D and cubemap images with optional mip levels and half-float, float and unsigned integer channels.

The plugin recognizes ImporterFlag::Quiet, which will cause all import warnings to be suppressed.

Channel mapping

Images containing R, G, B or A channels are imported as PixelFormat::R16F / RG16F / RGB16F / RGBA16F, PixelFormat::R32F / RG32F / RGB32F / RGBA32F or PixelFormat::R32UI / RG32UI / RGB32UI / RGBA32UI, all channels are expected to have the same type.

If neither of the color channels is present and a a Z channel is present instead, the image is imported as PixelFormat::Depth32F, expecting the channel to be of a float type.

If a file doesn't match ither of the above, custom channel mapping can be supplied in the configuration.

Data alignment, display and data windows

Image rows are always aligned to four bytes.

OpenEXR image metadata contain a display and data window, which can be used for annotating crop borders or specifying that the data is just a small portion of a larger image. The importer ignores the display window and imports everything that's inside the data window, without offseting it in any way.

Multilayer and multipart images, deep images

Images with custom layers (for example with separate channels for a left and right view) can be imported by specifying the layer configuration option.

Multipart and deep images are not supported right now — only the first part / sample gets imported.

Multilevel images

Tiled OpenEXR images with multiple mip levels are imported with the largest level first, with the size divided by 2 in each following level. For non-power-of-two sizes, the file format supports sizes rounded either up or down.

The file format doesn't have a builtin way to describe level count for incomplete mip chains, instead the file can have some tiles missing. This is detected on import and empty levels at the end are not counted into the reported level count, with a message printed if ImporterFlag::Verbose is enabled.

Ripmap files are imported as a single-level image right now.

Cube and lat/lon environment maps

A lat/long environment map is imported as a 2D image without any indication of its type.

OpenEXR stores cubemaps as 2D images with the height being six times of its width. For convenience these are imported as a 3D image with each face being one slice, in order +X, -X, +Y, -Y, +Z and -Z, and ImageFlag3D::CubeMap set. Cube maps can have mip levels as well, however because OpenEXR treats them internally as 2D images, it includes also levels with height < 6. Because those would result in a 3D image with zero height, the smallest levels are ignored, with a message printed if ImporterFlag::Verbose is enabled.

Plugin-specific configuration

It's possible to tune various options mainly for channel mapping through configuration(). See below for all options and their default values:

[configuration]
# Number of threads OpenEXR should use for reading and decompression. A value
# of 1 performs reading and decompression serially in the calling thread, 2
# adds one additional worker thread for decompression, etc. 0 sets it to the
# value returned by std::thread::hardware_concurrency().
#
# Note that while the amount of threads can be controlled per-file, OpenEXR
# has a global thread pool and its size will remain at the largest set value
# until the plugin is unloaded. OpenExrImageConverter shares the same thread
# pool.
threads=1

# Import channels of given layer
layer=

# Mapping of channel names to output image channels. All channels have to
# have the same type; empty names will cause that channel to not be imported
# at all, leaving the memory zeroed out. See also forceChannelCount.
r=R
g=G
b=B
a=A

# If the file doesn't contain any of the channel names specified in r, g, b
# or a, depth is tried instead, in which case it's expected to be a 32-bit
# float and is imported as PixelFormat::Depth32F.
depth=Z

# If any of the channels specified in r, g, b, a don't exist, they will be
# filled with the following values.
rFill=0.0
gFill=0.0
bFill=0.0
aFill=1.0

# Override channel count for RGBA. Allowed values are 0-4, with zero being
# the default where channel count corresponds to the last name in r, g, b, a
# that exists. So for example if a file has only R and B channels, it's
# imported as RGB with G filled with the gFill value.
forceChannelCount=0

# Override channel type for RGBA. Allowed values are FLOAT, HALF and UINT,
# empty value performs no conversion.
forceChannelType=

See Editing plugin-specific configuration for more information and an example showing how to edit the configuration values.

Enabling multithreading

On Linux it may happen that setting the threads option to something else than 1 will cause std::system_error to be thrown (or, worst case, crashing with a null function pointer call on some systems). There's no way to solve this from within the dynamically loaded module itself, the application has to be linked to pthread instead. With CMake it can be done like this:

find_package(Threads REQUIRED)
target_link_libraries(your-application PRIVATE Threads::Threads)

Base classes

class AbstractImporter
Base for importer plugins.

Constructors, destructors, conversion operators

OpenExrImporter(PluginManager::AbstractManager& manager, const Containers::StringView& plugin) explicit
Plugin manager constructor.