// -*- 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 #include #include #include #include // 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 #include "dfd.h" #include // // 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 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(exrBuffer.data()), exrByteLength); } void ExrInput::open(ImageSpec& newspec) { assert(isp != nullptr && "ImageInput not properly opened"); { unsigned char versionData[tinyexr::kEXRVersionSize]; isp->read(reinterpret_cast(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(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(image.height); const auto width = static_cast(image.width); const auto numSourceChannels = static_cast(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, 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(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(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(outputBuffer), sizeof(defaultColor[0]), &defaultColor[0]); break; } case TINYEXR_PIXELTYPE_FLOAT: { float defaultColor[] = { 0.f, 0.f, 0.f, 1.f }; copyData(reinterpret_cast(outputBuffer), sizeof(defaultColor[0]), &defaultColor[0]); break; } case TINYEXR_PIXELTYPE_UINT: { uint32_t defaultColor[] = { 0, 0, 0, 1 }; copyData(reinterpret_cast(outputBuffer), sizeof(defaultColor[0]), &defaultColor[0]); break; } default: assert(false && "Internal error"); break; } }