Add ktx
This commit is contained in:
@@ -0,0 +1,320 @@
|
||||
// -*- 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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user