Files
2026-06-14 19:09:18 +01:00

321 lines
13 KiB
C++

// -*- tab-width: 4; -*-
// vi: set sw=2 ts=4 expandtab:
// Copyright 2022 The Khronos Group Inc.
// SPDX-License-Identifier: Apache-2.0
#include "imageio.h"
#include <array>
#include <cassert>
#include <optional>
#include <string_view>
#include <vector>
// TEXR is not defined in tinyexr.h. Current GitHub tinyexr master uses
// assert. The version in astc-encoder must be old.
#define TEXR_ASSERT(x) assert(x)
#define TINYEXR_IMPLEMENTATION
#include "tinyexr.h"
#include <KHR/khr_df.h>
#include "dfd.h"
#include <fmt/format.h>
//
// Documentation on the OpenEXR format can be found at:
// https://openexr.readthedocs.io/en/latest/
// More information about OpenEXR including sample images can be found at:
// https://www.openexr.com/
//
class ExrInput final : public ImageInput {
public:
ExrInput() : ImageInput("exr") {
// InitEXRVersion(&version); // No need to call, no such function
InitEXRHeader(&header);
InitEXRImage(&image);
}
~ExrInput() {
// FreeEXRVersion(&version); // No need to call, no such function
if (image.width != 0 && image.height != 0) {
FreeEXRImage(&image);
}
FreeEXRHeader(&header);
FreeEXRErrorMessage(err);
}
virtual void open(ImageSpec&) override;
virtual void readImage(void* buffer, size_t bufferByteCount,
uint32_t subimage = 0, uint32_t miplevel = 0,
const FormatDescriptor& targetFormat = FormatDescriptor()) override;
/// Read a single scanline (all channels) of native data into contiguous
/// memory.
virtual void readNativeScanline(void* /*buffer*/, size_t /*bufferByteCount*/,
uint32_t /*y*/, uint32_t /*z*/,
uint32_t /*subimage*/, uint32_t /*miplevel*/) override { };
void slurp();
private:
std::vector<unsigned char> exrBuffer;
private:
EXRVersion version;
EXRHeader header;
EXRImage image;
int ec = 0;
const char* err = nullptr;
};
ImageInput* exrInputCreate() {
return new ExrInput;
}
const char* exrInputExtensions[] = { "exr", nullptr };
void ExrInput::slurp() {
size_t exrByteLength;
isp->seekg(0, isp->end);
exrByteLength = isp->tellg();
isp->seekg(0, isp->beg);
exrBuffer.resize(exrByteLength);
isp->read(reinterpret_cast<char*>(exrBuffer.data()), exrByteLength);
}
void ExrInput::open(ImageSpec& newspec) {
assert(isp != nullptr && "ImageInput not properly opened");
{
unsigned char versionData[tinyexr::kEXRVersionSize];
isp->read(reinterpret_cast<char*>(versionData), tinyexr::kEXRVersionSize);
if (isp->fail())
throwOnReadFailure();
isp->seekg(0);
ec = ParseEXRVersionFromMemory(&version, versionData, tinyexr::kEXRVersionSize);
if (ec == TINYEXR_ERROR_INVALID_MAGIC_NUMBER)
throw different_format();
if (ec != TINYEXR_SUCCESS)
throw std::runtime_error(fmt::format("EXR version decode error: {}.", ec));
}
// It's an EXR file
slurp();
ec = ParseEXRHeaderFromMemory(&header, &version, exrBuffer.data(), exrBuffer.size(), &err);
if (ec != TINYEXR_SUCCESS)
throw std::runtime_error(fmt::format("EXR header decode error: {} - {}.", ec, err));
// Determine file data format
uint32_t bitDepth = 0;
ImageInputFormatType formatType;
int qualifiers = 0;
for (int i = 0; i < header.num_channels; i++) {
const auto type = header.channels[i].pixel_type;
switch (type) {
case TINYEXR_PIXELTYPE_FLOAT:
bitDepth = std::max(bitDepth, 32u);
formatType = ImageInputFormatType::exr_float;
qualifiers = KHR_DF_SAMPLE_DATATYPE_SIGNED | KHR_DF_SAMPLE_DATATYPE_FLOAT;
break;
case TINYEXR_PIXELTYPE_HALF:
bitDepth = std::max(bitDepth, 16u);
formatType = ImageInputFormatType::exr_float;
qualifiers = KHR_DF_SAMPLE_DATATYPE_SIGNED | KHR_DF_SAMPLE_DATATYPE_FLOAT;
break;
case TINYEXR_PIXELTYPE_UINT:
bitDepth = std::max(bitDepth, 32u);
formatType = ImageInputFormatType::exr_uint;
qualifiers = 0;
break;
default:
throw std::runtime_error(fmt::format("EXR header decode error: Not supported pixel type: {}.", type));
}
}
const uint32_t width = header.data_window[2] - header.data_window[0] + 1;
const uint32_t height = header.data_window[3] - header.data_window[1] + 1;
// Use "chromaticities" attribute, if present, to determine color primaries
khr_df_primaries_e colorPrimaries = KHR_DF_PRIMARIES_UNSPECIFIED;
for (int i = 0; i < header.num_custom_attributes; ++i) {
auto attributeName = std::string_view(header.custom_attributes[i].name);
if (attributeName == "chromaticities") {
int expectedSize = 8 * sizeof(float);
if (header.custom_attributes[i].size != expectedSize) {
throw std::runtime_error(fmt::format("EXR chromaticities attribute decode error: Expected size {} but got {}.",
expectedSize, header.custom_attributes[i].size));
}
const float* chromaticities = (const float*)header.custom_attributes[i].value;
Primaries primaries;
primaries.Rx = chromaticities[0];
primaries.Ry = chromaticities[1];
primaries.Gx = chromaticities[2];
primaries.Gy = chromaticities[3];
primaries.Bx = chromaticities[4];
primaries.By = chromaticities[5];
primaries.Wx = chromaticities[6];
primaries.Wy = chromaticities[7];
colorPrimaries = findMapping(&primaries, 0.002f);
}
}
images.emplace_back(ImageSpec(
width,
height,
1,
// We make TinyEXR decode to top-left.
ImageSpec::Origin(ImageSpec::Origin::eLeft, ImageSpec::Origin::eTop),
header.num_channels,
bitDepth,
static_cast<khr_df_sample_datatype_qualifiers_e>(qualifiers),
KHR_DF_TRANSFER_LINEAR,
colorPrimaries,
KHR_DF_MODEL_RGBSDA),
formatType);
newspec = spec();
}
/// @brief Read an entire image into contiguous memory performing conversions
/// to @a requestFormat.
///
/// Supported conversions are half->[half,float,uint], float->float, and uint->uint.
void ExrInput::readImage(void* outputBuffer, size_t bufferByteCount,
uint32_t subimage, uint32_t miplevel,
const FormatDescriptor& requestFormat) {
assert(subimage == 0); (void) subimage;
assert(miplevel == 0); (void) miplevel;
const auto& targetFormat = requestFormat.isUnknown() ? spec().format() : requestFormat;
// Determine and verify requested format conversions
if (!targetFormat.sameUnitAllChannels() || targetFormat.samples.empty())
throw std::runtime_error(fmt::format("EXR load error: "
"Requested format conversion to different channels is not supported."));
const auto targetBitDepth = targetFormat.samples[0].bitLength + 1;
const bool targetL = targetFormat.samples[0].qualifierLinear;
const bool targetE = targetFormat.samples[0].qualifierExponent;
const bool targetS = targetFormat.samples[0].qualifierSigned;
const bool targetF = targetFormat.samples[0].qualifierFloat;
// TinyEXR only supports half->[half,float,uint], float->float, uint->uint conversions
int requestedType = 0;
if (targetBitDepth == 16 && !targetE && !targetL && targetS && targetF)
requestedType = TINYEXR_PIXELTYPE_HALF;
else if (targetBitDepth == 32 && !targetE && !targetL && targetS && targetF)
requestedType = TINYEXR_PIXELTYPE_FLOAT;
else if (targetBitDepth == 32 && !targetE && !targetL && !targetS && !targetF)
requestedType = TINYEXR_PIXELTYPE_UINT;
else
throw std::runtime_error(fmt::format("EXR load error: "
"Requested format conversion to {}-bit{}{}{}{} is not supported.",
targetBitDepth,
targetL ? " Linear" : "",
targetE ? " Exponent" : "",
targetS ? " Signed" : "",
targetF ? " Float" : ""));
for (int i = 0; i < header.num_channels; ++i) {
header.requested_pixel_types[i] = requestedType;
if (header.pixel_types[i] != TINYEXR_PIXELTYPE_HALF && header.pixel_types[i] != requestedType)
throw std::runtime_error(fmt::format("EXR load error: "
"Requested format conversion from the input type is not supported."));
}
// Load image version
EXRVersion exr_version;
ec = ParseEXRVersionFromMemory(&exr_version, exrBuffer.data(), exrBuffer.size());
if (ec != TINYEXR_SUCCESS)
throw std::runtime_error(
fmt::format("EXR load error: {} - {}.", ec, "Failed to parse EXR version"));
if (exr_version.multipart || exr_version.non_image)
throw std::runtime_error(
fmt::format("EXR load error: {}.", "Unsupported EXR version (2.0)"));
// Load image data
// TinyEXR decodes images so that the first bytes in the returned buffer
// are the top-left corner of the image with line_order == 0 "increasing Y"
// and the bottom-left corner otherwise, "decreasing Y". Force a top-left
// origin regardless of the line_order in the file. See
// https://github.com/syoyo/tinyexr/issues/213 for more information.
header.line_order = 0;
ec = LoadEXRImageFromMemory(&image, &header, exrBuffer.data(), exrBuffer.size(), &err);
if (ec != TINYEXR_SUCCESS)
throw std::runtime_error(fmt::format("EXR load error: {} - {}.", ec, err));
const auto height = static_cast<uint32_t>(image.height);
const auto width = static_cast<uint32_t>(image.width);
const auto numSourceChannels = static_cast<uint32_t>(image.num_channels);
const auto numTargetChannels = targetFormat.channelCount();
const auto expectedBufferByteCount = height * width * numTargetChannels * targetBitDepth / 8;
if (bufferByteCount != expectedBufferByteCount)
throw std::runtime_error(fmt::format("EXR load error: "
"Provided target buffer size is {} does not match the expected value: {}.", bufferByteCount, expectedBufferByteCount));
// Find the RGBA channels
std::array<std::optional<uint32_t>, 4> channels;
for (uint32_t i = 0; i < numSourceChannels; ++i) {
if (std::strcmp(header.channels[i].name, "R") == 0)
channels[0] = i;
else if (std::strcmp(header.channels[i].name, "G") == 0)
channels[1] = i;
else if (std::strcmp(header.channels[i].name, "B") == 0)
channels[2] = i;
else if (std::strcmp(header.channels[i].name, "A") == 0)
channels[3] = i;
else
warning(fmt::format("EXR load warning: Unrecognized channel \"{}\" is ignored.", header.channels[i].name));
// TODO: check for 1 channel "Y" and make greyscale texture.
// TODO: check for "Y", "RY" and "BY" (luminance/chroma) and reject as unsupported.
// TODO: check for "AR", "AG", "AB" and make texture with pre-multipled alpha provided there is also an A channel? Or reject?
}
// Copy the data
const auto copyData = [&](unsigned char* ptr, uint32_t dataSize, const void* defaultColor) {
const auto sourcePtr = [&](uint32_t channel, uint32_t x, uint32_t y) {
return reinterpret_cast<const unsigned char*>(image.images[channel] + (y * width + x) * dataSize);
};
for (uint32_t y = 0; y < height; ++y) {
for (uint32_t x = 0; x < width; ++x) {
auto* targetPixel = ptr + (y * width * numTargetChannels + x * numTargetChannels) * dataSize;
for (uint32_t c = 0; c < numTargetChannels; ++c) {
if (channels[c].has_value()) {
std::memcpy(targetPixel + c * dataSize, sourcePtr(*channels[c], x, y), dataSize);
} else {
std::memcpy(targetPixel + c * dataSize, static_cast<const uint8_t*>(defaultColor) + c * dataSize, dataSize);
}
}
}
}
};
switch (requestedType) {
case TINYEXR_PIXELTYPE_HALF: {
uint16_t defaultColor[] = { 0x0000, 0x0000, 0x0000, 0x3C00 }; // { 0.h, 0.h, 0.h,1.h }
copyData(reinterpret_cast<unsigned char*>(outputBuffer), sizeof(defaultColor[0]), &defaultColor[0]);
break;
}
case TINYEXR_PIXELTYPE_FLOAT: {
float defaultColor[] = { 0.f, 0.f, 0.f, 1.f };
copyData(reinterpret_cast<unsigned char*>(outputBuffer), sizeof(defaultColor[0]), &defaultColor[0]);
break;
}
case TINYEXR_PIXELTYPE_UINT: {
uint32_t defaultColor[] = { 0, 0, 0, 1 };
copyData(reinterpret_cast<unsigned char*>(outputBuffer), sizeof(defaultColor[0]), &defaultColor[0]);
break;
}
default:
assert(false && "Internal error");
break;
}
}