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: