Skip to content

Commit

Permalink
feat: support loading ultrahdr jpegs
Browse files Browse the repository at this point in the history
  • Loading branch information
Tom94 committed Mar 6, 2025
1 parent d538cb4 commit 1f65761
Show file tree
Hide file tree
Showing 6 changed files with 258 additions and 0 deletions.
1 change: 1 addition & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,7 @@ set(TEV_SOURCES
include/tev/imageio/StbiHdrImageSaver.h src/imageio/StbiHdrImageSaver.cpp
include/tev/imageio/StbiImageLoader.h src/imageio/StbiImageLoader.cpp
include/tev/imageio/StbiLdrImageSaver.h src/imageio/StbiLdrImageSaver.cpp
include/tev/imageio/UltraHdrImageLoader.h src/imageio/UltraHdrImageLoader.cpp

include/tev/Box.h src/Box.cpp
include/tev/Channel.h src/Channel.cpp
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@ $ cpack --config build/CPackConfig.cmake

- __EXR__ (via [OpenEXR](https://github.com/wjakob/openexr))
- __HDR__, __PNG__, __JPEG__, BMP, GIF, PIC, PNM, PSD, TGA (via [stb_image](https://github.com/wjakob/nanovg/blob/master/src/stb_image.h))
- __Ultra HDR JPEG__ (e.g. pictures from newer Android phones; via [libultrahdr](https://github.com/google/libultrahdr))
- __PFM__ (compatible with [Netbpm](http://www.pauldebevec.com/Research/HDR/PFM/))
- __QOI__ (via [qoi](https://github.com/phoboslab/qoi). Shoutout to [Tiago Chaves](https://github.com/laurelkeys) for adding support!)
- __DDS__ (via [DirectXTex](https://github.com/microsoft/DirectXTex); Windows only. Shoutout to [Craig Kolb](https://github.com/cek) for adding support!)
Expand Down
39 changes: 39 additions & 0 deletions include/tev/imageio/UltraHdrImageLoader.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/*
* tev -- the EXR viewer
*
* Copyright (C) 2025 Thomas Müller <contact@tom94.net>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

#pragma once

#include <tev/Image.h>
#include <tev/imageio/ImageLoader.h>

#include <istream>

namespace tev {

class UltraHdrImageLoader : public ImageLoader {
public:
UltraHdrImageLoader();

bool canLoadFile(std::istream& iStream) const override;
Task<std::vector<ImageData>>
load(std::istream& iStream, const fs::path& path, const std::string& channelSelector, int priority) const override;

std::string name() const override { return "Ultra HDR"; }
};

} // namespace tev
1 change: 1 addition & 0 deletions src/HelpWindow.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,7 @@ HelpWindow::HelpWindow(Widget* parent, bool supportsHdr, function<void()> closeC
addLibrary(about, "libheif", "HEIF and avif file format decoder and encoder");
addLibrary(about, "Little-CMS", "FOSS CMM engine. Fast transforms between ICC profiles.");
#endif
addLibrary(about, "libultrahdr", "Ultra HDR JPEG image format library");
addLibrary(about, "NanoGUI", "Small GUI library");
addLibrary(about, "NanoVG", "Small vector graphics library");
addLibrary(about, "OpenEXR", "High dynamic-range (HDR) image file format");
Expand Down
3 changes: 3 additions & 0 deletions src/imageio/ImageLoader.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@
#include <tev/imageio/PfmImageLoader.h>
#include <tev/imageio/QoiImageLoader.h>
#include <tev/imageio/StbiImageLoader.h>
#include <tev/imageio/UltraHdrImageLoader.h>

#ifdef _WIN32
# include <tev/imageio/DdsImageLoader.h>
#endif
Expand All @@ -49,6 +51,7 @@ const vector<unique_ptr<ImageLoader>>& ImageLoader::getLoaders() {
imageLoaders.emplace_back(new HeifImageLoader());
#endif
imageLoaders.emplace_back(new QoiImageLoader());
imageLoaders.emplace_back(new UltraHdrImageLoader());
imageLoaders.emplace_back(new StbiImageLoader());
return imageLoaders;
};
Expand Down
213 changes: 213 additions & 0 deletions src/imageio/UltraHdrImageLoader.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
/*
* tev -- the EXR viewer
*
* Copyright (C) 2025 Thomas Müller <contact@tom94.net>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

#include <tev/Common.h>
#include <tev/ThreadPool.h>
#include <tev/imageio/UltraHdrImageLoader.h>

#include <ImfChromaticities.h>

#include <ultrahdr_api.h>

using namespace nanogui;
using namespace std;

namespace tev {

UltraHdrImageLoader::UltraHdrImageLoader() {}

bool UltraHdrImageLoader::canLoadFile(istream& iStream) const {
uint8_t header[3] = {};
iStream.read((char*)header, 3);

// Early return if not a JPEG
if (header[0] != 0xFF || header[1] != 0xD8 || header[2] != 0xFF) {
iStream.clear();
iStream.seekg(0);
return false;
}

// TODO: avoid loading the whole file to memory just to check whether ultrahdr can load it. At least we only have to do this for JPG
// images... and hopefully our caches stay hot for when the image *actually* gets loaded later on.
iStream.seekg(0, ios_base::end);
int64_t fileSize = iStream.tellg();
iStream.clear();
iStream.seekg(0);

vector<char> buffer(fileSize);
iStream.read(buffer.data(), fileSize);

iStream.clear();
iStream.seekg(0);

return is_uhdr_image(buffer.data(), (int)fileSize);
}

static bool isOkay(uhdr_error_info_t status) { return status.error_code == UHDR_CODEC_OK; }

static string toString(uhdr_error_info_t status) {
if (isOkay(status)) {
return "Okay";
} else if (status.has_detail) {
return fmt::format("Error #{}: {}", (uint32_t)status.error_code, status.detail);
} else {
return fmt::format("Error #{}", (uint32_t)status.error_code);
}
}

static string toString(uhdr_color_gamut_t cg) {
switch (cg) {
case UHDR_CG_UNSPECIFIED: return "Unspecified";
case UHDR_CG_BT_709: return "BT.709";
case UHDR_CG_BT_2100: return "BT.2100";
case UHDR_CG_DISPLAY_P3: return "Display P3";
default: return "Unknown";
}
}

Task<vector<ImageData>> UltraHdrImageLoader::load(istream& iStream, const fs::path&, const string& channelSelector, int priority) const {
vector<ImageData> result;

iStream.seekg(0, ios_base::end);
int64_t fileSize = iStream.tellg();
iStream.clear();
iStream.seekg(0);

vector<char> buffer(fileSize);
iStream.read(buffer.data(), fileSize);

auto decoder = uhdr_create_decoder();
if (!decoder) {
throw runtime_error{"Could not create UltraHDR decoder."};
}

ScopeGuard decoderGuard{[decoder] { uhdr_release_decoder(decoder); }};

uhdr_compressed_image_t uhdr_image;
uhdr_image.data = buffer.data();
uhdr_image.data_sz = fileSize;
uhdr_image.capacity = fileSize;
uhdr_image.cg = UHDR_CG_UNSPECIFIED;
uhdr_image.ct = UHDR_CT_UNSPECIFIED;
uhdr_image.range = UHDR_CR_UNSPECIFIED;

if (auto status = uhdr_dec_set_image(decoder, &uhdr_image); !isOkay(status)) {
throw runtime_error{fmt::format("Failed to set image: {}", toString(status))};
}

if (auto status = uhdr_dec_set_out_img_format(decoder, UHDR_IMG_FMT_64bppRGBAHalfFloat); !isOkay(status)) {
throw runtime_error{fmt::format("Failed to set output format: {}", toString(status))};
}

if (auto status = uhdr_dec_set_out_color_transfer(decoder, UHDR_CT_LINEAR); !isOkay(status)) {
throw runtime_error{fmt::format("Failed to set output color transfer: {}", toString(status))};
}

if (auto status = uhdr_decode(decoder); !isOkay(status)) {
throw runtime_error{fmt::format("Failed to decode: {}", toString(status))};
}

uhdr_raw_image_t* decoded_image = uhdr_get_decoded_image(decoder);
if (!decoded_image) {
throw runtime_error{"No decoded image."};
}

auto readImage = [](uhdr_raw_image_t* image, int priority) -> Task<ImageData> {
if (image->fmt != UHDR_IMG_FMT_64bppRGBAHalfFloat) {
throw runtime_error{"Decoded image is not UHDR_IMG_FMT_64bppRGBAHalfFloat."};
}

Vector2i size = {(int)image->w, (int)image->h};
if (size.x() <= 0 || size.y() <= 0) {
throw runtime_error{"Invalid image size."};
}

const int numChannels = 4;

ImageData imageData;
imageData.channels = makeNChannels(numChannels, size);
imageData.hasPremultipliedAlpha = false;

size_t numPixels = (size_t)size.x() * size.y();
vector<float> src(numPixels * numChannels);

auto data = reinterpret_cast<half*>(image->planes[UHDR_PLANE_PACKED]);
size_t samplesPerLine = image->stride[UHDR_PLANE_PACKED] * numChannels;

co_await ThreadPool::global().parallelForAsync<int>(
0,
size.y(),
[&](int y) {
for (int x = 0; x < size.x(); ++x) {
size_t i = y * (size_t)size.x() + x;
auto typedData = reinterpret_cast<const half*>(data + y * samplesPerLine);
int baseIdx = x * numChannels;

for (int c = 0; c < numChannels; ++c) {
imageData.channels[c].at(i) = typedData[baseIdx + c];
}
}
},
priority
);

// Convert to Rec.709 if necessary
Imf::Chromaticities rec709; // default constructor yields rec709 (sRGB) primaries
Imf::Chromaticities chroma;

tlog::debug(fmt::format("Ultra HDR image has color gamut: {}", toString(image->cg)));

switch (image->cg) {
case UHDR_CG_DISPLAY_P3:
chroma = {
{0.6800f, 0.3200f },
{0.2650f, 0.6900f },
{0.1500f, 0.0600f },
{0.31271f, 0.32902f}
};
break;
case UHDR_CG_BT_2100:
chroma = {
{0.7080f, 0.2920f },
{0.1700f, 0.7970f },
{0.1310f, 0.0460f },
{0.31271f, 0.32902f}
};
break;
case UHDR_CG_UNSPECIFIED: tlog::warning() << "Ultra HDR image has unspecified color gamut. Assuming BT.709."; break;
case UHDR_CG_BT_709: break;
default: tlog::warning() << "Ultra HDR image has invalid color gamut. Assuming BT.709."; break;
}

if (chroma != rec709) {
Imath::M44f M = Imf::RGBtoXYZ(chroma, 1) * Imf::XYZtoRGB(rec709, 1);
for (int m = 0; m < 4; ++m) {
for (int n = 0; n < 4; ++n) {
imageData.toRec709.m[m][n] = M.x[m][n];
}
}
}

co_return imageData;
};

result.emplace_back(co_await readImage(decoded_image, priority));
co_return result;
}

} // namespace tev

0 comments on commit 1f65761

Please sign in to comment.