diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e8d9483342..ebe1210567 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -363,7 +363,7 @@ jobs: CLANG_FORMAT_EXE=clang-format-22 EXTRA_DEP_PACKAGES="clang-format-22" - - desc: latest releases gcc15 C++23 py3.12 avx2 exr3.4 ocio2.4 + - desc: latest releases gcc15 C++23 py3.12 avx2 exr3.5 ocio2.4 nametag: linux-latest-releases runner: ubuntu-24.04 # cc_compiler: gcc-13 @@ -372,7 +372,7 @@ jobs: fmt_ver: 12.2.0 fmt_commit: 1be298e1bd68957e4cd352e1f676f00e07dcfb57 opencolorio_ver: v2.5.2 - openexr_ver: v3.4.15 + openexr_ver: v3.5.0 pybind11_ver: v3.1.0 python_ver: "3.12" oiio_python_bindings_backend: both @@ -406,7 +406,8 @@ jobs: cxx_std: 26 fmt_ver: main opencolorio_ver: main - openexr_ver: main + openexr_ver: v3.5.1 + # TODO: bump openexr_ver back to "main" after lj2k bug is fixed pybind11_ver: master python_ver: "3.12" oiio_python_bindings_backend: both @@ -470,7 +471,7 @@ jobs: setenvs: export USE_OPENVDB=0 openjph_CMAKE_C_COMPILER=gcc openjph_CMAKE_CXX_COMPILER=g++ - - desc: Linux ARM latest releases gcc14 C++20 py3.12 exr3.4 ocio2.4 + - desc: Linux ARM latest releases gcc14 C++20 py3.12 exr3.5 ocio2.4 nametag: linux-arm-latest-releases runner: ubuntu-24.04-arm cc_compiler: gcc-14 @@ -479,7 +480,7 @@ jobs: fmt_ver: 12.2.0 fmt_commit: 1be298e1bd68957e4cd352e1f676f00e07dcfb57 opencolorio_ver: v2.5.2 - openexr_ver: v3.4.15 + openexr_ver: v3.5.0 pybind11_ver: v3.1.0 python_ver: "3.12" oiio_python_bindings_backend: both @@ -495,7 +496,7 @@ jobs: USE_OPENVDB=0 nanobind_GIT_TAG=v3.0.0 nanobind_GIT_COMMIT=1bd3139721d6176dfb10979b7219ccb58ef8aafa - - desc: Linux ARM latest releases clang18 C++20 py3.12 exr3.4 ocio2.4 + - desc: Linux ARM latest releases clang18 C++20 py3.12 exr3.5 ocio2.4 nametag: linux-arm-latest-releases-clang runner: ubuntu-24.04-arm cc_compiler: clang-18 @@ -504,7 +505,7 @@ jobs: fmt_ver: 12.2.0 fmt_commit: 1be298e1bd68957e4cd352e1f676f00e07dcfb57 opencolorio_ver: v2.5.2 - openexr_ver: v3.4.15 + openexr_ver: v3.5.0 pybind11_ver: v3.1.0 python_ver: "3.12" oiio_python_bindings_backend: both diff --git a/src/build-scripts/build_openexr.bash b/src/build-scripts/build_openexr.bash index 43e5fb1aa3..e0c76cceab 100755 --- a/src/build-scripts/build_openexr.bash +++ b/src/build-scripts/build_openexr.bash @@ -11,7 +11,7 @@ set -ex # Which OpenEXR to retrieve, how to build it OPENEXR_REPO=${OPENEXR_REPO:=https://github.com/AcademySoftwareFoundation/openexr.git} -OPENEXR_VERSION=${OPENEXR_VERSION:=v3.4.15} +OPENEXR_VERSION=${OPENEXR_VERSION:=v3.5.0} # Where to install the final results LOCAL_DEPS_DIR=${LOCAL_DEPS_DIR:=${PWD}/ext} @@ -64,4 +64,3 @@ export OpenEXR_ROOT=$OPENEXR_INSTALL_DIR export OPENEXR_ROOT=$OPENEXR_INSTALL_DIR export OPENEXR_LIBRARY_DIR=$OPENEXR_INSTALL_DIR/lib export LD_LIBRARY_PATH=$OPENEXR_ROOT/lib:$LD_LIBRARY_PATH - diff --git a/src/cmake/build_OpenEXR.cmake b/src/cmake/build_OpenEXR.cmake index 03bd63f9a9..86b29e96da 100644 --- a/src/cmake/build_OpenEXR.cmake +++ b/src/cmake/build_OpenEXR.cmake @@ -3,10 +3,10 @@ # https://github.com/AcademySoftwareFoundation/OpenImageIO -set_cache (OpenEXR_BUILD_VERSION 3.4.15 "OpenEXR version for local builds") +set_cache (OpenEXR_BUILD_VERSION 3.5.0 "OpenEXR version for local builds") set (OpenEXR_GIT_REPOSITORY "https://github.com/AcademySoftwareFoundation/OpenEXR") set (OpenEXR_GIT_TAG "v${OpenEXR_BUILD_VERSION}") -set (OpenEXR_GIT_COMMIT "e71cdd5d30a146dcb56c5e4c576d9e9d3c45f4fb") +set (OpenEXR_GIT_COMMIT "5d838e01770cadb8f2a36c0d17018452fa7a3c0c") set_cache (OpenEXR_BUILD_SHARED_LIBS ${LOCAL_BUILD_SHARED_LIBS_DEFAULT} DOC "Should a local OpenEXR build, if necessary, build shared libraries" ADVANCED) diff --git a/src/cmake/testing.cmake b/src/cmake/testing.cmake index 92a76c84c8..e0991fb5be 100644 --- a/src/cmake/testing.cmake +++ b/src/cmake/testing.cmake @@ -469,6 +469,31 @@ macro (oiio_add_all_tests) IMAGEDIR openexr-images URL http://github.com/AcademySoftwareFoundation/openexr-images) endif () + + # HTJ2K was introduced in OpenEXR v.3.4.0 + if (OpenEXR_VERSION VERSION_GREATER_EQUAL 3.4.0) + if (TEST openexr-compression) + set_property (TEST openexr-compression APPEND PROPERTY ENVIRONMENT + "OIIO_OPENEXR_HTJ2K_SUPPORT=1") + endif() + if (TEST openexr-compression.core) + set_property (TEST openexr-compression.core APPEND PROPERTY + ENVIRONMENT "OIIO_OPENEXR_HTJ2K_SUPPORT=1") + endif() + endif() + # New compression tests of zstd and lj2k require OpenEXR main branch, + # or latest release or at least V3.5.0 + if (OpenEXR_VERSION VERSION_GREATER_EQUAL 3.5.0) + if (TEST openexr-compression) + set_property (TEST openexr-compression APPEND PROPERTY ENVIRONMENT + "OIIO_OPENEXR_LJ2K_ZSTD_SUPPORT=1") + endif () + if (TEST openexr-compression.core) + set_property (TEST openexr-compression.core APPEND PROPERTY + ENVIRONMENT "OIIO_OPENEXR_LJ2K_ZSTD_SUPPORT=1") + endif () + endif () + # Regression test (compiles its own helper and generates its own image) # for a partial edge-tile heap overflow in the OpenEXR readers. oiio_add_tests (openexr-partialtile) diff --git a/src/doc/builtinplugins.md b/src/doc/builtinplugins.md index a354c56050..6a8c9467d6 100644 --- a/src/doc/builtinplugins.md +++ b/src/doc/builtinplugins.md @@ -1615,8 +1615,11 @@ The official OpenEXR site is . * - ``compression`` - string - one of: ``"none"``, ``"rle"``, ``"zip"``, ``"zips"``, ``"piz"``, - ``"pxr24"``, ``"b44"``, ``"b44a"``, ``"dwaa"``, ``"dwab"``, ``"htj2k256"`` or ``"htj2k32"``. - (``"htj2k256"`` and ``"htj2k32"`` are only supported with OpenEXR 3.4 or later.) + ``"pxr24"``, ``"b44"``, ``"b44a"``, ``"dwaa"``, ``"dwab"``, + ``"htj2k256"``, ``"htj2k32"``, ``"lj2k"``, or ``"zstd"``. + (``"htj2k256"`` and ``"htj2k32"`` are only supported with OpenEXR 3.4 + or later; ``"lj2k"`` and ``"zstd"`` are only supported with OpenEXR + 3.5 or later.) If the writer receives a request for a compression type it does not recognize or is not supported by the version of OpenEXR on the system, it will use ``"zip"`` by default. For ``"dwaa"`` and @@ -1625,7 +1628,15 @@ The official OpenEXR site is . default DWA compression value is 45.) For ``"zip"`` and ``"zips"`` compression, a level from 1 to 9 may be appended (the default is ``"zip:4"``), but note that this is only honored when building - against OpenEXR 3.1.3 or later. + against OpenEXR 3.1.3 or later. For ``"lj2k"`` (the lossy variant of + ``"htj2k256"``), a quality from 1 to 150 may be appended, like + ``"lj2k:100"`` (the default is ``"lj2k:110"``). Values from 97 to 150 + are an OpenEXR-specific extension of the normal 1 to 97 range, giving + the higher quality needed for 32-bit float images; values between 90 + and 110 are recommended for a balance of quality and size. For + ``"zstd"``, a compression level from 1 to 22 may be appended, like + ``"zstd:10"`` (the default is ``"zstd:5"``). For both ``"lj2k"`` and + ``"zstd"``, an out-of-range value falls back to the default. * - ``textureformat`` - string - ``"Plain Texture"`` for MIP-mapped OpenEXR files, ``"CubeFace @@ -3339,4 +3350,3 @@ of the z-buffer. Zfile files use the file extension {file}`.zfile`. ``` [st 2065-4]: https://pub.smpte.org/pub/st2065-4/ - diff --git a/src/openexr.imageio/exrinput.cpp b/src/openexr.imageio/exrinput.cpp index 0eedff26c7..41f225620b 100644 --- a/src/openexr.imageio/exrinput.cpp +++ b/src/openexr.imageio/exrinput.cpp @@ -464,11 +464,22 @@ OpenEXRInput::PartInfo::parse_header(OpenEXRInput* in, #endif #ifdef IMF_HTJ2K32_COMPRESSION case Imf::HTJ2K32_COMPRESSION: comp = "htj2k32"; break; +#endif +#ifdef IMF_LJ2K_COMPRESSION + case Imf::LJ2K_COMPRESSION: comp = "lj2k"; break; +#endif +#ifdef IMF_ZSTD_COMPRESSION + case Imf::ZSTD_COMPRESSION: comp = "zstd"; break; #endif default: break; } - if (comp) + if (comp) { spec.attribute("compression", comp); + } else { + in->errorfmt("Unknown compression code {}", + int(compressattr->value())); + return false; + } if (spec.tile_width == 0 && !spec.deep) { // Scanline file: advertise how many scanlines are packed into // each compressed chunk, so that callers can align their reads @@ -480,6 +491,9 @@ OpenEXRInput::PartInfo::parse_header(OpenEXRInput* in, if (scansperchunk > 1) spec.attribute("oiio:RowsPerChunk", scansperchunk); } + } else { + in->errorfmt("No compression information found"); + return false; } for (auto hit = header->begin(); hit != header->end(); ++hit) { @@ -1105,8 +1119,7 @@ OpenEXRInput::seek_subimage(int subimage, int miplevel) if (m_input_multipart) header = &(m_input_multipart->header(subimage)); if (!part.parse_header(this, header)) { - errorfmt("Could not seek to subimage={}: unable to parse header", - subimage, miplevel); + // Any errors in parse_header will already have called errorfmt return false; } part.initialized = true; diff --git a/src/openexr.imageio/exrinput_c.cpp b/src/openexr.imageio/exrinput_c.cpp index 620304767c..639711295b 100644 --- a/src/openexr.imageio/exrinput_c.cpp +++ b/src/openexr.imageio/exrinput_c.cpp @@ -16,6 +16,7 @@ #include "exr_pvt.h" +#include #include #include "imageio_pvt.h" @@ -644,11 +645,24 @@ OpenEXRCoreInput::PartInfo::parse_header(OpenEXRCoreInput* in, #endif #ifdef IMF_HTJ2K32_COMPRESSION case EXR_COMPRESSION_HTJ2K32: comp = "htj2k32"; break; +#endif +#ifdef IMF_LJ2K_COMPRESSION + case EXR_COMPRESSION_LJ2K: comp = "lj2k"; break; +#endif +#ifdef IMF_ZSTD_COMPRESSION + case EXR_COMPRESSION_ZSTD: comp = "zstd"; break; #endif default: break; } - if (comp) + if (comp) { spec.attribute("compression", comp); + } else { + in->errorfmt("Unknown compression code {}", int(comptype)); + return false; + } + } else { + in->errorfmt("No compression information found"); + return false; } if (spec.tile_width == 0) { @@ -1195,8 +1209,7 @@ OpenEXRCoreInput::seek_subimage(int subimage, int miplevel) PartInfo& part(m_parts[subimage]); if (!part.initialized) { if (!part.parse_header(this, m_exr_context, subimage, miplevel)) { - errorfmt("Could not seek to subimage={}: unable to parse header", - subimage, miplevel); + // Any errors in parse_header will already have called errorfmt return false; } part.initialized = true; diff --git a/src/openexr.imageio/exroutput.cpp b/src/openexr.imageio/exroutput.cpp index 3a30a41afc..17fd4eee55 100644 --- a/src/openexr.imageio/exroutput.cpp +++ b/src/openexr.imageio/exroutput.cpp @@ -1019,6 +1019,22 @@ OpenEXROutput::spec_to_header(ImageSpec& spec, int subimage, spec.erase_attribute("openexr:dwaCompressionLevel"); } +#if OPENEXR_CODED_VERSION >= 30500 + // OpenEXR 3.5.0 added the support for LJ2K, the lossy variant of HTJ2K256. + // Value from 97 to 150 are OpenEXR specific of the normal range of 1 till + // 97. This extended range is available to achieve higher quality needed + // for 32-bit float images. Values between 90 and 110 are recommended for + // a balance of quality and size. + if (Strutil::istarts_with(comp, "lj2k")) { + header.lossyHTJ2KQuality() = (qual >= 1 && qual <= 150) ? qual : 110; + } + + // OpenEXR 3.5.0 added an API for setting the zstd compression/quality + // level in the Header object. Default value is 5. + if (Strutil::istarts_with(comp, "zstd")) { + header.zstdCompressionLevel() = (qual >= 1 && qual <= 22) ? qual : 5; + } +#endif // Default to increasingY line order if (!spec.find_attribute("openexr:lineOrder")) spec.attribute("openexr:lineOrder", "increasingY"); @@ -1268,6 +1284,14 @@ OpenEXROutput::put_parameter(const std::string& name, TypeDesc type, #ifdef IMF_HTJ2K32_COMPRESSION else if (Strutil::iequals(str, "htj2k32")) header.compression() = Imf::HTJ2K32_COMPRESSION; +#endif +#ifdef IMF_LJ2K_COMPRESSION + else if (Strutil::iequals(str, "lj2k")) + header.compression() = Imf::LJ2K_COMPRESSION; +#endif +#ifdef IMF_ZSTD_COMPRESSION + else if (Strutil::iequals(str, "zstd")) + header.compression() = Imf::ZSTD_COMPRESSION; #endif } return true; diff --git a/testsuite/openexr-compression/ref/out-exr3.1.txt b/testsuite/openexr-compression/ref/out-exr3.1.txt new file mode 100644 index 0000000000..e1389e32af --- /dev/null +++ b/testsuite/openexr-compression/ref/out-exr3.1.txt @@ -0,0 +1,30 @@ +out-none.exr compression none +out-rle.exr compression rle +out-zips.exr compression zips +out-zip.exr compression zip +out-piz.exr compression piz +out-pxr24.exr compression pxr24 +out-b44.exr compression b44 +out-b44a.exr compression b44a +out-dwaa.exr compression dwaa +out-dwab.exr compression dwab +Comparing "out-none.exr" and "ref/out-zip.exr" +PASS +Comparing "out-rle.exr" and "ref/out-zip.exr" +PASS +Comparing "out-zips.exr" and "ref/out-zip.exr" +PASS +Comparing "out-zip.exr" and "ref/out-zip.exr" +PASS +Comparing "out-piz.exr" and "ref/out-zip.exr" +PASS +Comparing "out-pxr24.exr" and "ref/out-zip.exr" +PASS +Comparing "out-b44.exr" and "ref/out-b44.exr" +PASS +Comparing "out-b44a.exr" and "ref/out-b44.exr" +PASS +Comparing "out-dwaa.exr" and "ref/out-dwaa.exr" +PASS +Comparing "out-dwab.exr" and "ref/out-dwaa.exr" +PASS diff --git a/testsuite/openexr-compression/ref/out-exr3.4.txt b/testsuite/openexr-compression/ref/out-exr3.4.txt new file mode 100644 index 0000000000..70ea194e87 --- /dev/null +++ b/testsuite/openexr-compression/ref/out-exr3.4.txt @@ -0,0 +1,36 @@ +out-none.exr compression none +out-rle.exr compression rle +out-zips.exr compression zips +out-zip.exr compression zip +out-piz.exr compression piz +out-pxr24.exr compression pxr24 +out-b44.exr compression b44 +out-b44a.exr compression b44a +out-dwaa.exr compression dwaa +out-dwab.exr compression dwab +out-htj2k32.exr compression htj2k32 +out-htj2k256.exr compression htj2k256 +Comparing "out-none.exr" and "ref/out-zip.exr" +PASS +Comparing "out-rle.exr" and "ref/out-zip.exr" +PASS +Comparing "out-zips.exr" and "ref/out-zip.exr" +PASS +Comparing "out-zip.exr" and "ref/out-zip.exr" +PASS +Comparing "out-piz.exr" and "ref/out-zip.exr" +PASS +Comparing "out-pxr24.exr" and "ref/out-zip.exr" +PASS +Comparing "out-b44.exr" and "ref/out-b44.exr" +PASS +Comparing "out-b44a.exr" and "ref/out-b44.exr" +PASS +Comparing "out-dwaa.exr" and "ref/out-dwaa.exr" +PASS +Comparing "out-dwab.exr" and "ref/out-dwaa.exr" +PASS +Comparing "out-htj2k32.exr" and "ref/out-zip.exr" +PASS +Comparing "out-htj2k256.exr" and "ref/out-zip.exr" +PASS diff --git a/testsuite/openexr-compression/ref/out-lj2k.exr b/testsuite/openexr-compression/ref/out-lj2k.exr new file mode 100644 index 0000000000..7478e6291a Binary files /dev/null and b/testsuite/openexr-compression/ref/out-lj2k.exr differ diff --git a/testsuite/openexr-compression/ref/out.txt b/testsuite/openexr-compression/ref/out.txt index e1389e32af..eda68ff448 100644 --- a/testsuite/openexr-compression/ref/out.txt +++ b/testsuite/openexr-compression/ref/out.txt @@ -8,6 +8,10 @@ out-b44.exr compression b44 out-b44a.exr compression b44a out-dwaa.exr compression dwaa out-dwab.exr compression dwab +out-htj2k32.exr compression htj2k32 +out-htj2k256.exr compression htj2k256 +out-lj2k.exr compression lj2k +out-zstd.exr compression zstd Comparing "out-none.exr" and "ref/out-zip.exr" PASS Comparing "out-rle.exr" and "ref/out-zip.exr" @@ -28,3 +32,11 @@ Comparing "out-dwaa.exr" and "ref/out-dwaa.exr" PASS Comparing "out-dwab.exr" and "ref/out-dwaa.exr" PASS +Comparing "out-htj2k32.exr" and "ref/out-zip.exr" +PASS +Comparing "out-htj2k256.exr" and "ref/out-zip.exr" +PASS +Comparing "out-lj2k.exr" and "ref/out-lj2k.exr" +PASS +Comparing "out-zstd.exr" and "ref/out-zip.exr" +PASS diff --git a/testsuite/openexr-compression/run.py b/testsuite/openexr-compression/run.py index d4019d9778..0e1882db46 100755 --- a/testsuite/openexr-compression/run.py +++ b/testsuite/openexr-compression/run.py @@ -4,6 +4,7 @@ # SPDX-License-Identifier: Apache-2.0 # https://github.com/AcademySoftwareFoundation/OpenImageIO +import os imagedir = "../common" @@ -20,6 +21,11 @@ "dwab", ] +if os.getenv("OIIO_OPENEXR_HTJ2K_SUPPORT") == "1": + compressions += ["htj2k32", "htj2k256"] +if os.getenv("OIIO_OPENEXR_LJ2K_ZSTD_SUPPORT") == "1": + compressions += ["lj2k", "zstd"] + anymatch = True outputs = [] for c in compressions: