Skip to content

Commit 5427ce1

Browse files
mugulmdlgritz
authored andcommitted
feat(jpeg): Support reading Ultra HDR images (#4484)
Initial feature request: #4424 Add support in the `jpeg` plugin for reading Ultra HDR images using the reference codec `libultrahdr`: https://github.com/google/libultrahdr In short, "ultra hdr" images are a clever extension of JPEG where the image file really is an old school JPEG file and will be interpreted correctly as such by old readers not aware of ultra hdr, but readers that are aware will see an extra piece of metadata that contains a gain map, that when applied to the base layer then yields an HDR image. Pretty clever approach! Images used for testing during development: https://github.com/MishaalRahmanGH/Ultra_HDR_Samples --------- Signed-off-by: loicvital <[email protected]>
1 parent 8da9ce1 commit 5427ce1

File tree

11 files changed

+273
-0
lines changed

11 files changed

+273
-0
lines changed

.github/workflows/ci.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ jobs:
104104
setenvs: export USE_ICC=1 USE_OPENVDB=0
105105
OIIO_EXTRA_CPP_ARGS="-fp-model=precise"
106106
FREETYPE_VERSION=VER-2-13-0
107+
DISABLE_libuhdr=1
107108
# For icc, use fp-model precise to eliminate needless LSB errors
108109
# that make test results differ from other platforms.
109110
- desc: icx/C++17 py3.9 exr3.1 ocio2.2 qt5.15
@@ -120,8 +121,12 @@ jobs:
120121
simd: "avx2,f16c"
121122
setenvs: export USE_OPENVDB=0
122123
OPENCOLORIO_CXX=g++
124+
UHDR_CMAKE_C_COMPILER=gcc
125+
UHDR_CMAKE_CXX_COMPILER=g++
123126
# OCIO doesn't build with icx, so we have to force the ocio build
124127
# to use g++.
128+
# Building libuhdr with icx results in test failures
129+
# so we force using gcc/g++.
125130
- desc: sanitizers
126131
nametag: sanitizer
127132
runner: ubuntu-latest

src/cmake/build_libuhdr.cmake

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
# Copyright Contributors to the OpenImageIO project.
2+
# SPDX-License-Identifier: Apache-2.0
3+
# https://github.com/AcademySoftwareFoundation/OpenImageIO
4+
5+
######################################################################
6+
# libuhdr by hand!
7+
######################################################################
8+
9+
set_cache (libuhdr_BUILD_VERSION 1.2.0 "libuhdr version for local builds")
10+
set (libuhdr_GIT_REPOSITORY "https://github.com/google/libultrahdr")
11+
set (libuhdr_GIT_TAG "v${libuhdr_BUILD_VERSION}")
12+
13+
set_cache (libuhdr_BUILD_SHARED_LIBS OFF
14+
DOC "Should execute a local libuhdr build, if necessary, build shared libraries" ADVANCED)
15+
16+
if (TARGET libjpeg-turbo::jpeg)
17+
# We've had some trouble with libuhdr finding the JPEG resources it needs to
18+
# build if we're using libjpeg-turbo, libuhdr needs an extra nudge.
19+
get_target_property(JPEG_INCLUDE_DIR JPEG::JPEG INTERFACE_INCLUDE_DIRECTORIES)
20+
get_target_property(JPEG_LIBRARY JPEG::JPEG INTERFACE_LINK_LIBRARIES)
21+
endif ()
22+
23+
set_cache (UHDR_CMAKE_C_COMPILER ${CMAKE_C_COMPILER} "libuhdr build C compiler override" ADVANCED)
24+
set_cache (UHDR_CMAKE_CXX_COMPILER ${CMAKE_CXX_COMPILER} "libuhdr build C++ compiler override" ADVANCED)
25+
26+
build_dependency_with_cmake(libuhdr
27+
VERSION ${libuhdr_BUILD_VERSION}
28+
GIT_REPOSITORY ${libuhdr_GIT_REPOSITORY}
29+
GIT_TAG ${libuhdr_GIT_TAG}
30+
CMAKE_ARGS
31+
-D BUILD_SHARED_LIBS=${libuhdr_BUILD_SHARED_LIBS}
32+
-D CMAKE_INSTALL_LIBDIR=lib
33+
-D CMAKE_POSITION_INDEPENDENT_CODE=ON
34+
-D UHDR_BUILD_EXAMPLES=FALSE
35+
-D UHDR_BUILD_DEPS=FALSE
36+
-D UHDR_ENABLE_LOGS=TRUE
37+
-D JPEG_INCLUDE_DIR=${JPEG_INCLUDE_DIR}
38+
-D JPEG_LIBRARY=${JPEG_LIBRARY}
39+
-D CMAKE_C_COMPILER=${UHDR_CMAKE_C_COMPILER}
40+
-D CMAKE_CXX_COMPILER=${UHDR_CMAKE_CXX_COMPILER}
41+
)
42+
43+
if (WIN32)
44+
file (GLOB _lib_files "${libuhdr_LOCAL_BUILD_DIR}/Release/*.lib")
45+
file (COPY ${_lib_files} DESTINATION ${libuhdr_LOCAL_INSTALL_DIR}/lib)
46+
unset (_lib_files)
47+
file (GLOB _header_files "${libuhdr_LOCAL_SOURCE_DIR}/ultrahdr_api.h")
48+
file (COPY ${_header_files} DESTINATION ${libuhdr_LOCAL_INSTALL_DIR}/include)
49+
unset (_header_files)
50+
endif ()
51+
52+
set (libuhdr_ROOT ${libuhdr_LOCAL_INSTALL_DIR})
53+
54+
find_package(libuhdr REQUIRED)
55+
56+
set (libuhdr_VERSION ${libuhdr_BUILD_VERSION})
57+
58+
if (libuhdr_BUILD_SHARED_LIBS)
59+
install_local_dependency_libs (uhdr uhdr)
60+
endif ()

src/cmake/externalpackages.cmake

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,10 @@ else ()
8686
endif ()
8787

8888

89+
# Ultra HDR
90+
checked_find_package (libuhdr)
91+
92+
8993
checked_find_package (TIFF REQUIRED
9094
VERSION_MIN 4.0)
9195
alias_library_if_not_exists (TIFF::TIFF TIFF::tiff)
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# Module to find libuhdr
2+
#
3+
# Copyright Contributors to the OpenImageIO project.
4+
# SPDX-License-Identifier: Apache-2.0
5+
# https://github.com/AcademySoftwareFoundation/OpenImageIO
6+
#
7+
# This module defines the following variables:
8+
#
9+
# libuhdr_FOUND True if libuhdr was found.
10+
# LIBUHDR_INCLUDE_DIR Where to find libuhdr headers
11+
# LIBUHDR_LIBRARY Library for uhdr
12+
13+
include (FindPackageHandleStandardArgs)
14+
15+
find_path(LIBUHDR_INCLUDE_DIR
16+
NAMES
17+
ultrahdr_api.h
18+
PATH_SUFFIXES
19+
include
20+
)
21+
22+
find_library(LIBUHDR_LIBRARY uhdr
23+
PATH_SUFFIXES
24+
lib
25+
)
26+
27+
find_package_handle_standard_args (libuhdr
28+
REQUIRED_VARS LIBUHDR_INCLUDE_DIR
29+
LIBUHDR_LIBRARY
30+
)

src/cmake/testing.cmake

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,9 @@ macro (oiio_add_all_tests)
263263
oiio_add_tests (iff
264264
ENABLEVAR ENABLE_IFF
265265
IMAGEDIR oiio-images URL "Recent checkout of OpenImageIO-images")
266+
oiio_add_tests (jpeg-ultrahdr
267+
FOUNDVAR libuhdr_FOUND
268+
IMAGEDIR oiio-images URL "Recent checkout of OpenImageIO-images")
266269
oiio_add_tests (jpeg2000
267270
FOUNDVAR OPENJPEG_FOUND
268271
IMAGEDIR oiio-images URL "Recent checkout of OpenImageIO-images")

src/doc/builtinplugins.rst

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1105,6 +1105,18 @@ via the `ImageInput::set_ioproxy()` method and the special
11051105
mode and do not support tiled image input or output.
11061106

11071107

1108+
**Ultra HDR**
1109+
1110+
JPEG input also suports Ultra HDR images.
1111+
Ultra HDR is an image format that encodes a high dynamic range image
1112+
in a JPEG image file by including a gain map in addition to the
1113+
primary image.
1114+
See https://developer.android.com/media/platform/hdr-image-format for
1115+
a complete reference on the Ultra HDR image format.
1116+
In the specific case of reading an Ultra HDR image, JPEG input will also
1117+
support alpha channels and high dynamic range imagery (`half` pixels).
1118+
1119+
11081120

11091121
|
11101122

src/jpeg.imageio/CMakeLists.txt

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,19 @@
22
# SPDX-License-Identifier: Apache-2.0
33
# https://github.com/AcademySoftwareFoundation/OpenImageIO
44

5+
if (libuhdr_FOUND)
6+
set (UHDR_DEFS USE_UHDR)
7+
else ()
8+
set (LIBUHDR_INCLUDE_DIR "")
9+
set (LIBUHDR_LIBRARY "")
10+
set (UHDR_DEFS "")
11+
endif ()
12+
513
add_oiio_plugin (jpeginput.cpp jpegoutput.cpp
14+
INCLUDE_DIRS ${LIBUHDR_INCLUDE_DIR}
615
LINK_LIBRARIES
716
$<TARGET_NAME_IF_EXISTS:libjpeg-turbo::jpeg>
817
$<TARGET_NAME_IF_EXISTS:JPEG::JPEG>
18+
${LIBUHDR_LIBRARY}
19+
DEFINITIONS "${UHDR_DEFS}"
920
)

src/jpeg.imageio/jpeg_pvt.h

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,10 @@ extern "C" {
3232
# define OIIO_JPEG_LIB_VERSION JPEG_LIB_VERSION
3333
#endif
3434

35+
#if defined(USE_UHDR)
36+
# include <ultrahdr_api.h>
37+
#endif
38+
3539

3640
OIIO_PLUGIN_NAMESPACE_BEGIN
3741

@@ -95,6 +99,10 @@ class JpgInput final : public ImageInput {
9599
jvirt_barray_ptr* m_coeffs;
96100
std::vector<unsigned char> m_cmyk_buf; // For CMYK translation
97101
std::unique_ptr<ImageSpec> m_config; // Saved copy of configuration spec
102+
bool m_is_uhdr; // Is interpreted as Ultra HDR image
103+
#if defined(USE_UHDR)
104+
uhdr_codec_private_t* m_uhdr_dec;
105+
#endif
98106

99107
void init()
100108
{
@@ -106,6 +114,10 @@ class JpgInput final : public ImageInput {
106114
m_jerr.jpginput = this;
107115
ioproxy_clear();
108116
m_config.reset();
117+
m_is_uhdr = false;
118+
#if defined(USE_UHDR)
119+
m_uhdr_dec = NULL;
120+
#endif
109121
}
110122

111123
// Rummage through the JPEG "APP1" marker pointed to by buf, decoding
@@ -117,6 +129,8 @@ class JpgInput final : public ImageInput {
117129

118130
bool read_icc_profile(j_decompress_ptr cinfo, ImageSpec& spec);
119131

132+
bool read_uhdr(Filesystem::IOProxy* ioproxy);
133+
120134
void close_file() { init(); }
121135

122136
friend class JpgOutput;

src/jpeg.imageio/jpeginput.cpp

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -329,6 +329,14 @@ JpgInput::open(const std::string& name, ImageSpec& newspec)
329329

330330
read_icc_profile(&m_cinfo, m_spec); /// try to read icc profile
331331

332+
// Try to interpret as Ultra HDR image.
333+
// The libultrahdr API requires to load the whole file content in memory
334+
// therefore we first check for the presence of the "hdrgm:Version" metadata
335+
// to avoid this costly process when not necessary.
336+
// https://developer.android.com/media/platform/hdr-image-format#signal_of_the_format
337+
if (m_spec.find_attribute("hdrgm:Version"))
338+
m_is_uhdr = read_uhdr(m_io);
339+
332340
newspec = m_spec;
333341
return true;
334342
}
@@ -406,6 +414,86 @@ JpgInput::read_icc_profile(j_decompress_ptr cinfo, ImageSpec& spec)
406414

407415

408416

417+
bool
418+
JpgInput::read_uhdr(Filesystem::IOProxy* ioproxy)
419+
{
420+
#if defined(USE_UHDR)
421+
// Read entire file content into buffer.
422+
const size_t buffer_size = ioproxy->size();
423+
std::vector<unsigned char> buffer(buffer_size);
424+
ioproxy->pread(buffer.data(), buffer_size, 0);
425+
426+
// Check if this is an actual Ultra HDR image.
427+
const bool detect_uhdr = is_uhdr_image(buffer.data(), buffer.size());
428+
if (!detect_uhdr)
429+
return false;
430+
431+
// Create Ultra HDR decoder.
432+
// Do not forget to release it once we don't need it,
433+
// i.e if this function returns false
434+
// or when we call close().
435+
m_uhdr_dec = uhdr_create_decoder();
436+
437+
// Prepare decoder input.
438+
// Note: we currently do not override any of the
439+
// default settings.
440+
uhdr_compressed_image_t uhdr_compressed;
441+
uhdr_compressed.data = buffer.data();
442+
uhdr_compressed.data_sz = buffer.size();
443+
uhdr_compressed.capacity = buffer.size();
444+
uhdr_dec_set_image(m_uhdr_dec, &uhdr_compressed);
445+
446+
// Decode Ultra HDR image
447+
// and check for decoding errors.
448+
uhdr_error_info_t err_info = uhdr_decode(m_uhdr_dec);
449+
450+
if (err_info.error_code != UHDR_CODEC_OK) {
451+
errorfmt("Ultra HDR decoding failed with error code {}",
452+
int(err_info.error_code));
453+
if (err_info.has_detail != 0)
454+
errorfmt("Additional error details: {}", err_info.detail);
455+
uhdr_release_decoder(m_uhdr_dec);
456+
return false;
457+
}
458+
459+
// Update spec with decoded image properties.
460+
// Note: we currently only support a subset of all possible
461+
// Ultra HDR image formats.
462+
uhdr_raw_image_t* uhdr_raw = uhdr_get_decoded_image(m_uhdr_dec);
463+
464+
int nchannels;
465+
TypeDesc desc;
466+
switch (uhdr_raw->fmt) {
467+
case UHDR_IMG_FMT_32bppRGBA8888:
468+
nchannels = 4;
469+
desc = TypeDesc::UINT8;
470+
break;
471+
case UHDR_IMG_FMT_64bppRGBAHalfFloat:
472+
nchannels = 4;
473+
desc = TypeDesc::HALF;
474+
break;
475+
case UHDR_IMG_FMT_24bppRGB888:
476+
nchannels = 3;
477+
desc = TypeDesc::UINT8;
478+
break;
479+
default:
480+
errorfmt("Unsupported Ultra HDR image format: {}", int(uhdr_raw->fmt));
481+
uhdr_release_decoder(m_uhdr_dec);
482+
return false;
483+
}
484+
485+
ImageSpec newspec = ImageSpec(uhdr_raw->w, uhdr_raw->h, nchannels, desc);
486+
newspec.extra_attribs = std::move(m_spec.extra_attribs);
487+
m_spec = newspec;
488+
489+
return true;
490+
#else
491+
return false;
492+
#endif
493+
}
494+
495+
496+
409497
static void
410498
cmyk_to_rgb(int n, const unsigned char* cmyk, size_t cmyk_stride,
411499
unsigned char* rgb, size_t rgb_stride)
@@ -453,6 +541,28 @@ JpgInput::read_native_scanline(int subimage, int miplevel, int y, int /*z*/,
453541
OIIO_DASSERT(m_next_scanline == 0 && current_subimage() == subimage);
454542
}
455543

544+
#if defined(USE_UHDR)
545+
if (m_is_uhdr) {
546+
uhdr_raw_image_t* uhdr_raw = uhdr_get_decoded_image(m_uhdr_dec);
547+
548+
unsigned int nbytes;
549+
switch (uhdr_raw->fmt) {
550+
case UHDR_IMG_FMT_32bppRGBA8888: nbytes = 4; break;
551+
case UHDR_IMG_FMT_64bppRGBAHalfFloat: nbytes = 8; break;
552+
case UHDR_IMG_FMT_24bppRGB888: nbytes = 3; break;
553+
default: return false;
554+
}
555+
556+
const size_t row_size = uhdr_raw->stride[UHDR_PLANE_PACKED] * nbytes;
557+
unsigned char* top_left = static_cast<unsigned char*>(
558+
uhdr_raw->planes[UHDR_PLANE_PACKED]);
559+
unsigned char* row_data_start = top_left + row_size * y;
560+
memcpy(data, row_data_start, row_size);
561+
562+
return true;
563+
}
564+
#endif
565+
456566
// Set up our custom error handler
457567
if (setjmp(m_jerr.setjmp_buffer)) {
458568
// Jump to here if there's a libjpeg internal error
@@ -494,6 +604,11 @@ JpgInput::close()
494604
if (m_decomp_create)
495605
jpeg_destroy_decompress(&m_cinfo);
496606
m_decomp_create = false;
607+
#if defined(USE_UHDR)
608+
if (m_is_uhdr)
609+
uhdr_release_decoder(m_uhdr_dec);
610+
m_is_uhdr = false;
611+
#endif
497612
close_file();
498613
}
499614
init(); // Reset to initial state
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
4080 x 3072, 4 channel, float jpeg
2+
Stats Min: 0.000000 0.000000 0.000000 1.000000 (float)
3+
Stats Max: 1.000000 1.000000 1.000000 1.000000 (float)
4+
Stats Avg: 0.068257 0.077759 0.100931 1.000000 (float)
5+
Stats StdDev: 0.100425 0.102336 0.107618 0.000000 (float)
6+
Stats NanCount: 0 0 0 0
7+
Stats InfCount: 0 0 0 0
8+
Stats FiniteCount: 12533760 12533760 12533760 12533760
9+
Constant: No
10+
Monochrome: No

0 commit comments

Comments
 (0)