From 52b5843ba112a9cc60f3eae532547adb0ae0dd39 Mon Sep 17 00:00:00 2001 From: Michael Dolan Date: Sat, 19 Dec 2020 17:52:09 -0500 Subject: [PATCH] Initial work on OpenEXR support Signed-off-by: Michael Dolan --- share/cmake/modules/FindOpenEXR.cmake | 238 ++++++++++++++++++++++ src/apputils/CMakeLists.txt | 14 +- src/apputils/imageio.cpp | 282 ++++++++++++++++++++++++++ src/apputils/imageio.h | 54 +++++ 4 files changed, 587 insertions(+), 1 deletion(-) create mode 100644 share/cmake/modules/FindOpenEXR.cmake create mode 100644 src/apputils/imageio.cpp create mode 100644 src/apputils/imageio.h diff --git a/share/cmake/modules/FindOpenEXR.cmake b/share/cmake/modules/FindOpenEXR.cmake new file mode 100644 index 0000000000..91d4ea5503 --- /dev/null +++ b/share/cmake/modules/FindOpenEXR.cmake @@ -0,0 +1,238 @@ +# SPDX-License-Identifier: BSD-3-Clause +# Copyright Contributors to the OpenColorIO Project. +# +# Locate or install openexr +# +# Variables defined by this module: +# OpenEXR_FOUND - If FALSE, do not try to link to openexr +# OpenEXR_LIBRARY - OpenEXR library to link to +# OpenEXR_INCLUDE_DIR - Where to find OpenEXR and IlmBase headers +# OpenEXR_VERSION - The version of the library +# +# Imported targets defined by this module, if found: +# OpenEXR::IlmImf +# OpenEXR::IlmImfUtil +# IlmBase::Half +# IlmBase::Iex +# IlmBase::IexMath +# IlmBase::IlmThread +# IlmBase::Imath +# +# By default, the dynamic libraries of openexr will be found. To find the +# static ones instead, you must set the OpenEXR_STATIC_LIBRARY variable to +# TRUE before calling find_package(OpenEXR ...). +# +# If OpenEXR is not installed in a standard path, you can use the +# OpenEXR_ROOT variable to tell CMake where to find it. If it is not found +# and OCIO_INSTALL_EXT_PACKAGES is set to MISSING or ALL, OpenEXR will be +# downloaded, built, and statically-linked into libOpenColorIO at build time. +# + +# OpenEXR components may have the version in their name +set(_OpenEXR_LIB_VER "${OpenEXR_FIND_VERSION_MAJOR}_${OpenEXR_FIND_VERSION_MINOR}") + +############################################################################### +### Try to find package ### + +if(NOT OCIO_INSTALL_EXT_PACKAGES STREQUAL ALL) + set(_OpenEXR_REQUIRED_VARS OpenEXR_LIBRARY) + + if(NOT DEFINED OpenEXR_ROOT) + # Search for IlmBaseConfig.cmake + find_package(IlmBase ${OpenEXR_FIND_VERSION} CONFIG QUIET) + endif() + + if(OpenEXR_FOUND) + get_target_property(OpenEXR_LIBRARY IlmBase::OpenEXR LOCATION) + else() + list(APPEND _OpenEXR_REQUIRED_VARS OpenEXR_INCLUDE_DIR) + + # Search for IlmBase.pc + find_package(PkgConfig QUIET) + pkg_check_modules(PC_IlmBase QUIET "IlmBase>=${OpenEXR_FIND_VERSION}") + + # Find include directory + find_path(OpenEXR_INCLUDE_DIR + NAMES + OpenEXR/OpenEXR.h + HINTS + ${OpenEXR_ROOT} + ${PC_OpenEXR_INCLUDE_DIRS} + PATH_SUFFIXES + include + OpenEXR/include + ) + + # Lib names to search for + set(_OpenEXR_LIB_NAMES "OpenEXR-${_OpenEXR_LIB_VER}" OpenEXR) + if(BUILD_TYPE_DEBUG) + # Prefer Debug lib names + list(INSERT _OpenEXR_LIB_NAMES 0 "OpenEXR-${_OpenEXR_LIB_VER}_d") + endif() + + if(OpenEXR_STATIC_LIBRARY) + # Prefer static lib names + set(_OpenEXR_STATIC_LIB_NAMES + "${CMAKE_STATIC_LIBRARY_PREFIX}OpenEXR-${_OpenEXR_LIB_VER}${CMAKE_STATIC_LIBRARY_SUFFIX}" + "${CMAKE_STATIC_LIBRARY_PREFIX}OpenEXR${CMAKE_STATIC_LIBRARY_SUFFIX}" + ) + if(BUILD_TYPE_DEBUG) + # Prefer static Debug lib names + list(INSERT _OpenEXR_STATIC_LIB_NAMES 0 + "${CMAKE_STATIC_LIBRARY_PREFIX}OpenEXR-${_OpenEXR_LIB_VER}_d${CMAKE_STATIC_LIBRARY_SUFFIX}") + endif() + endif() + + # Find library + find_library(OpenEXR_LIBRARY + NAMES + ${_OpenEXR_STATIC_LIB_NAMES} + ${_OpenEXR_LIB_NAMES} + HINTS + ${OpenEXR_ROOT} + ${PC_OpenEXR_LIBRARY_DIRS} + PATH_SUFFIXES + lib64 lib + ) + + # Get version from config header file + if(OpenEXR_INCLUDE_DIR) + if(EXISTS "${OpenEXR_INCLUDE_DIR}/OpenEXR/IlmBaseConfig.h") + set(_OpenEXR_CONFIG "${OpenEXR_INCLUDE_DIR}/OpenEXR/IlmBaseConfig.h") + elseif(EXISTS "${OpenEXR_INCLUDE_DIR}/OpenEXR/OpenEXRConfig.h") + set(_OpenEXR_CONFIG "${OpenEXR_INCLUDE_DIR}/OpenEXR/OpenEXRConfig.h") + endif() + endif() + + if(_OpenEXR_CONFIG) + file(STRINGS "${_OpenEXR_CONFIG}" _OpenEXR_VER_SEARCH + REGEX "^[ \t]*#define[ \t]+(OPENEXR|ILMBASE)_VERSION_STRING[ \t]+\"[.0-9]+\".*$") + if(_OpenEXR_VER_SEARCH) + string(REGEX REPLACE ".*#define[ \t]+(OPENEXR|ILMBASE)_VERSION_STRING[ \t]+\"([.0-9]+)\".*" + "\\2" OpenEXR_VERSION "${_OpenEXR_VER_SEARCH}") + endif() + elseif(PC_OpenEXR_FOUND) + set(OpenEXR_VERSION "${PC_OpenEXR_VERSION}") + endif() + endif() + + # Override REQUIRED if package can be installed + if(OCIO_INSTALL_EXT_PACKAGES STREQUAL MISSING) + set(OpenEXR_FIND_REQUIRED FALSE) + endif() + + include(FindPackageHandleStandardArgs) + find_package_handle_standard_args(OpenEXR + REQUIRED_VARS + ${_OpenEXR_REQUIRED_VARS} + VERSION_VAR + OpenEXR_VERSION + ) +endif() + +############################################################################### +### Create target + +if (NOT TARGET IlmBase::OpenEXR) + add_library(IlmBase::OpenEXR UNKNOWN IMPORTED GLOBAL) + set(_OpenEXR_TARGET_CREATE TRUE) +endif() + +############################################################################### +### Install package from source ### + +if(NOT OpenEXR_FOUND) + include(ExternalProject) + include(GNUInstallDirs) + + if(APPLE) + set(CMAKE_OSX_DEPLOYMENT_TARGET ${CMAKE_OSX_DEPLOYMENT_TARGET}) + endif() + + set(_EXT_DIST_ROOT "${CMAKE_BINARY_DIR}/ext/dist") + set(_EXT_BUILD_ROOT "${CMAKE_BINARY_DIR}/ext/build") + + # Set find_package standard args + set(OpenEXR_FOUND TRUE) + set(OpenEXR_VERSION ${OpenEXR_FIND_VERSION}) + set(OpenEXR_INCLUDE_DIR "${_EXT_DIST_ROOT}/${CMAKE_INSTALL_INCLUDEDIR}") + + # Set the expected library name. "_d" is appended to Debug Windows builds + # <= OpenEXR 2.3.0. In newer versions, it is appended to Debug libs on + # all platforms. + if(BUILD_TYPE_DEBUG AND (WIN32 OR OpenEXR_VERSION VERSION_GREATER "2.3.0")) + set(_OpenEXR_LIB_SUFFIX "_d") + endif() + + set(OpenEXR_LIBRARY + "${_EXT_DIST_ROOT}/${CMAKE_INSTALL_LIBDIR}/${CMAKE_STATIC_LIBRARY_PREFIX}OpenEXR-${_OpenEXR_LIB_VER}${_OpenEXR_LIB_SUFFIX}${CMAKE_STATIC_LIBRARY_SUFFIX}") + + if(_OpenEXR_TARGET_CREATE) + if(UNIX) + set(OpenEXR_CXX_FLAGS "${OpenEXR_CXX_FLAGS} -fPIC") + endif() + + if(MSVC) + set(OpenEXR_CXX_FLAGS "${OpenEXR_CXX_FLAGS} /EHsc") + endif() + + string(STRIP "${OpenEXR_CXX_FLAGS}" OpenEXR_CXX_FLAGS) + + set(OpenEXR_CMAKE_ARGS + ${OpenEXR_CMAKE_ARGS} + -DCMAKE_BUILD_TYPE=${CMAKE_BUILD_TYPE} + -DCMAKE_CXX_FLAGS=${OpenEXR_CXX_FLAGS} + -DCMAKE_CXX_STANDARD=${CMAKE_CXX_STANDARD} + -DCMAKE_INSTALL_MESSAGE=${CMAKE_INSTALL_MESSAGE} + -DCMAKE_INSTALL_PREFIX=${_EXT_DIST_ROOT} + -DCMAKE_OBJECT_PATH_MAX=${CMAKE_OBJECT_PATH_MAX} + -DBUILD_SHARED_LIBS=OFF + -DBUILD_TESTING=OFF + -DOPENEXR_VIEWERS_ENABLE=OFF + -DPYILMBASE_ENABLE=OFF + ) + + if(CMAKE_TOOLCHAIN_FILE) + set(OpenEXR_CMAKE_ARGS + ${OpenEXR_CMAKE_ARGS} -DCMAKE_TOOLCHAIN_FILE=${CMAKE_TOOLCHAIN_FILE}) + endif() + + # Hack to let imported target be built from ExternalProject_Add + file(MAKE_DIRECTORY ${OpenEXR_INCLUDE_DIR}) + + ExternalProject_Add(ilmbase_install + GIT_REPOSITORY "https://github.com/openexr/openexr.git" + GIT_TAG "v${OpenEXR_VERSION}" + GIT_CONFIG advice.detachedHead=false + GIT_SHALLOW TRUE + PREFIX "${_EXT_BUILD_ROOT}/openexr" + BUILD_BYPRODUCTS ${OpenEXR_LIBRARY} + CMAKE_ARGS ${OpenEXR_CMAKE_ARGS} + PATCH_COMMAND + ${CMAKE_COMMAND} -P "${CMAKE_SOURCE_DIR}/share/cmake/scripts/PatchOpenEXR.cmake" + BUILD_COMMAND + ${CMAKE_COMMAND} --build . + --config ${CMAKE_BUILD_TYPE} + --target OpenEXR + INSTALL_COMMAND + ${CMAKE_COMMAND} -DCMAKE_INSTALL_CONFIG_NAME=${CMAKE_BUILD_TYPE} + -P "IlmBase/OpenEXR/cmake_install.cmake" + EXCLUDE_FROM_ALL TRUE + ) + + add_dependencies(IlmBase::OpenEXR ilmbase_install) + message(STATUS "Installing OpenEXR (IlmBase): ${OpenEXR_LIBRARY} (version \"${OpenEXR_VERSION}\")") + endif() +endif() + +############################################################################### +### Configure target ### + +if(_OpenEXR_TARGET_CREATE) + set_target_properties(IlmBase::OpenEXR PROPERTIES + IMPORTED_LOCATION ${OpenEXR_LIBRARY} + INTERFACE_INCLUDE_DIRECTORIES ${OpenEXR_INCLUDE_DIR} + ) + + mark_as_advanced(OpenEXR_INCLUDE_DIR OpenEXR_LIBRARY OpenEXR_VERSION) +endif() diff --git a/src/apputils/CMakeLists.txt b/src/apputils/CMakeLists.txt index d7eecdfd22..6fe656cab6 100644 --- a/src/apputils/CMakeLists.txt +++ b/src/apputils/CMakeLists.txt @@ -3,12 +3,24 @@ set(SOURCES argparse.cpp + imageio.cpp strutil.cpp ) add_library(apputils STATIC ${SOURCES}) -target_include_directories(apputils PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/..) +target_include_directories(apputils + PUBLIC + ${CMAKE_CURRENT_SOURCE_DIR}/.. +) + +target_link_libraries(apputils + PRIVATE + OpenColorIO + IlmBase::Half + IlmBase::Imath + OpenEXR::IlmImf +) set_target_properties(apputils PROPERTIES COMPILE_FLAGS "${PLATFORM_COMPILE_FLAGS}" diff --git a/src/apputils/imageio.cpp b/src/apputils/imageio.cpp new file mode 100644 index 0000000000..1468392197 --- /dev/null +++ b/src/apputils/imageio.cpp @@ -0,0 +1,282 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright Contributors to the OpenColorIO Project. + +#include + +#include "OpenEXR/half.h" +#include "OpenEXR/ImathBox.h" +#include "OpenEXR/ImfChannelList.h" +#include "OpenEXR/ImfFrameBuffer.h" +#include "OpenEXR/ImfHeader.h" +#include "OpenEXR/ImfInputFile.h" +#include "OpenEXR/ImfOutputFile.h" + +#include "imageio.h" + +namespace OCIO_NAMESPACE +{ + +namespace{ + +BitDepth pixelTypeToBitDepth(Imf::PixelType pixelType) +{ + BitDepth bitDepth; + + switch (pixelType) + { + case Imf::HALF: + bitDepth = BIT_DEPTH_F16; + break; + case Imf::FLOAT: + bitDepth = BIT_DEPTH_F32; + break; + default: + throw Exception(""); + } + + return bitDepth; +} + +Imf::PixelType bitDepthToPixelType(BitDepth bitDepth) +{ + Imf::PixelType pixelType; + + switch (bitDepth) + { + case BIT_DEPTH_F16: + pixelType = Imf::HALF; + break; + case BIT_DEPTH_F32: + pixelType = Imf::FLOAT; + break; + default: + throw Exception(""); + } + + return pixelType; +} + +size_t getPixelBytes(Imf::PixelType pixelType) +{ + size_t bytes; + + switch (pixelType) + { + case Imf::HALF: + bytes = sizeof(half); + break; + case Imf::FLOAT: + bytes = sizeof(float); + break; + default: + throw Exception(""); + } + + return bytes; +} + +size_t getPixelBytes(BitDepth bitDepth) +{ + size_t bytes; + + switch (bitDepth) + { + case BIT_DEPTH_UINT8: + bytes = sizeof(uint8_t); + break; + case BIT_DEPTH_UINT10: + case BIT_DEPTH_UINT12: + case BIT_DEPTH_UINT14: + case BIT_DEPTH_UINT16: + bytes = sizeof(uint16_t); + break; + case BIT_DEPTH_F16: + bytes = sizeof(half); + break; + case BIT_DEPTH_F32: + bytes = sizeof(float); + break; + default: + throw Exception(""); + } + + return bytes; +} + +ImageInfo getExrImageInfo(const Imf::InputFile & file) +{ + ImageInfo info; + + Imf::Header header = file.header(); + + Imath::Box2i dataWindow = header.dataWindow(); + info.x = dataWindow.min.x; + info.y = dataWindow.min.y; + info.width = dataWindow.max.x - dataWindow.min.x + 1; + info.height = dataWindow.max.y - dataWindow.min.y + 1; + + Imath::Box2i displayWindow = header.displayWindow(); + info.fullX = displayWindow.min.x; + info.fullY = displayWindow.min.y; + info.fullWidth = displayWindow.max.x - displayWindow.min.x + 1; + info.fullHeight = displayWindow.max.y - displayWindow.min.y + 1; + + Imf::ChannelList channels = header.channels(); + Imf::ChannelList::ConstIterator it = channels.begin(); + for (it; it != channels.end(); it++) + { + info.channelCount++; + info.channels.push_back( + { + it.name(), + pixelTypeToBitDepth(it.channel().type) + } + ); + } + + return info; +} + +ImageInfo getExrImageInfo(const std::string & filename) +{ + try + { + Imf::InputFile file(filename.c_str()); + return getExrImageInfo(file); + } + catch(const std::exception& e) + { + std::cerr << e.what() << '\n'; + } +} + +bool readExr(const std::string & filename, + const std::vector & channelNames, + BitDepth bitDepth, + void * pixels) +{ + bool ok = false; + + try + { + Imf::InputFile file(filename.c_str()); + ImageInfo info = getExrImageInfo(file); + + Imf::FrameBuffer frameBuffer; + + Imf::PixelType pixelType = bitDepthToPixelType(bitDepth); + size_t pixelBytes = getPixelBytes(bitDepth); + size_t xStride = pixelBytes * (size_t)info.channelCount; + size_t yStride = (size_t)info.width * xStride; + char * base = (char *)pixels - info.x * xStride - info.y * yStride; + + size_t channelOffset = 0; + for (const auto & channelName : channelNames) + { + frameBuffer.insert(channelName, + Imf::Slice(pixelType, + base + channelOffset, + xStride, + yStride)); + channelOffset += pixelBytes; + } + + file.setFrameBuffer(frameBuffer); + file.readPixels(info.y, info.y + info.height - 1); + ok = true; + } + catch(const std::exception& e) + { + std::cerr << e.what() << '\n'; + } + + return ok; +} + +bool writeExr(const std::string & filename, + const ImageInfo & info, + BitDepth bitDepth, + const void * pixels) +{ + bool ok = false; + + try + { + Imath::Box2i displayWindow( + Imath::V2i(info.fullX, + info.fullY), + Imath::V2i(info.fullX + info.fullWidth - 1, + info.fullY + info.fullHeight - 1) + ); + Imath::Box2i dataWindow( + Imath::V2i(info.x, + info.y), + Imath::V2i(info.x + info.width - 1, + info.y + info.height - 1) + ); + Imf::Header header(displayWindow, dataWindow); + + for (const auto & channel : info.channels) + { + header.channels().insert( + channel.name, + Imf::Channel(bitDepthToPixelType(channel.bitDepth)) + ); + } + + Imf::OutputFile file(filename.c_str(), header); + Imf::FrameBuffer frameBuffer; + + Imf::PixelType pixelType = bitDepthToPixelType(bitDepth); + size_t pixelBytes = getPixelBytes(bitDepth); + size_t xStride = pixelBytes * (size_t)info.channelCount; + size_t yStride = (size_t)info.width * xStride; + char * base = (char *)pixels - info.x * xStride - info.y * yStride; + + size_t channelOffset = 0; + for (const auto & channel : info.channels) + { + frameBuffer.insert(channel.name, + Imf::Slice(pixelType, + base + channelOffset, + xStride, + yStride)); + channelOffset += pixelBytes; + } + + file.setFrameBuffer(frameBuffer); + file.writePixels(info.height); + ok = true; + } + catch(const std::exception& e) + { + std::cerr << e.what() << '\n'; + } + + return ok; +} + +} // namespace + +ImageInfo GetImageInfo(const std::string & filename) +{ + return getExrImageInfo(filename); +} + +bool ReadImage(const std::string & filename, + const std::vector & channelNames, + BitDepth bitDepth, + void * pixels) +{ + return readExr(filename, channelNames, bitDepth, pixels); +} + +bool WriteImage(const std::string & filename, + const ImageInfo & info, + BitDepth bitDepth, + const void * pixels) +{ + return writeExr(filename, info, bitDepth, pixels); +} + +} // namespace OCIO_NAMESPACE diff --git a/src/apputils/imageio.h b/src/apputils/imageio.h new file mode 100644 index 0000000000..eea7b43cf6 --- /dev/null +++ b/src/apputils/imageio.h @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright Contributors to the OpenColorIO Project. + +#ifndef INCLUDED_OCIO_IMAGEIO_H +#define INCLUDED_OCIO_IMAGEIO_H + +#include +#include + +#include "OpenColorIO.h" + +namespace OCIO_NAMESPACE +{ + +struct ChannelInfo +{ + std::string name; + BitDepth bitDepth; +}; + +struct ImageInfo +{ + // Data window + int x; + int y; + int width; + int height; + + // Display window + int fullX; + int fullY; + int fullWidth; + int fullHeight; + + // Channels + int channelCount; + std::vector channels; +}; + +ImageInfo GetImageInfo(const std::string & filename); + +bool ReadImage(const std::string & filename, + const std::vector & channelNames, + BitDepth bitDepth, + void * pixels); + +bool WriteImage(const std::string & filename, + const ImageInfo & info, + BitDepth bitDepth, + const void * pixels); + +} // namespace OCIO_NAMESPACE + +#endif // INCLUDED_OCIO_IMAGEIO_H