This commit is contained in:
2026-06-14 19:09:18 +01:00
parent 14bd1a9271
commit 13fa90a0e9
3958 changed files with 999286 additions and 4 deletions
+111
View File
@@ -0,0 +1,111 @@
# Copyright 2022 The Khronos Group Inc.
# SPDX-License-Identifier: Apache-2.0
set( PLUGIN_SOURCES
exr.imageio/exrinput.cc
jpg.imageio/jpginput.cc
npbm.imageio/npbminput.cc
png.imageio/pnginput.cc
png.imageio/pngoutput.cc
)
set( PLUGIN_HEADERS
)
set( EXTERNAL
"${PROJECT_SOURCE_DIR}/external"
)
set( EXR_PLUGIN_EXTERNAL_SOURCES
)
set( EXR_PLUGIN_EXTERNAL_HEADERS
${EXTERNAL}/astc-encoder/Source/ThirdParty/tinyexr.h
)
set( JPG_PLUGIN_EXTERNAL_SOURCES
${EXTERNAL}/basisu/encoder/jpgd.cpp
)
set( JPG_PLUGIN_EXTERNAL_HEADERS
${EXTERNAL}/basisu/encoder/jpgd.h
)
set( PNG_PLUGIN_EXTERNAL_SOURCES
${EXTERNAL}/lodepng/lodepng.cpp
)
set( PNG_PLUGIN_EXTERNAL_HEADERS
${EXTERNAL}/lodepng/lodepng.h
)
add_library( imageio STATIC
formatdesc.h
image.hpp
imagecodec.hpp
imagespan.hpp
imageinput.cc
imageio.cc
imageio.h
imageio_utility.h
imageoutput.cc
${PLUGIN_HEADERS}
${PLUGIN_SOURCES}
${EXR_PLUGIN_EXTERNAL_HEADERS}
${EXR_PLUGIN_EXTERNAL_SOURCES}
${JPG_PLUGIN_EXTERNAL_HEADERS}
${JPG_PLUGIN_EXTERNAL_SOURCES}
${PNG_PLUGIN_EXTERNAL_HEADERS}
${PNG_PLUGIN_EXTERNAL_SOURCES}
)
source_group(TREE "${CMAKE_CURRENT_LIST_DIR}" PREFIX "Source Files" FILES ${PLUGIN_SOURCES})
source_group(TREE "${CMAKE_CURRENT_LIST_DIR}" PREFIX "Header Files" FILES ${PLUGIN_HEADERS})
source_group("Header Files/exr.imageio" FILES
${EXR_PLUGIN_EXTERNAL_SOURCES}
)
source_group("Header Files/exr.imageio" FILES
${EXR_PLUGIN_EXTERNAL_HEADERS}
)
source_group("Source Files/jpg.imageio" FILES
${JPG_PLUGIN_EXTERNAL_SOURCES}
)
source_group("Header Files/jpg.imageio" FILES
${JPG_PLUGIN_EXTERNAL_HEADERS}
)
source_group("Source Files/png.imageio" FILES
${PNG_PLUGIN_EXTERNAL_SOURCES}
)
source_group("Header Files/png.imageio" FILES
${PNG_PLUGIN_EXTERNAL_HEADERS}
)
target_include_directories(
imageio
PUBLIC
.
PRIVATE
$<TARGET_PROPERTY:objUtil,INTERFACE_INCLUDE_DIRECTORIES>
)
target_include_directories(
imageio
SYSTEM
PUBLIC
${PROJECT_SOURCE_DIR}/other_include
PRIVATE
"${PROJECT_SOURCE_DIR}/external/astc-encoder/Source/ThirdParty"
"${PROJECT_SOURCE_DIR}/external/basisu"
"${PROJECT_SOURCE_DIR}/external/dfdutils"
"${PROJECT_SOURCE_DIR}/external/lodepng"
)
target_compile_definitions(
imageio
PRIVATE
$<TARGET_PROPERTY:ktx,INTERFACE_COMPILE_DEFINITIONS>
)
target_compile_features(imageio PUBLIC cxx_std_17)
target_link_libraries(imageio fmt::fmt)
set_target_properties(imageio PROPERTIES
CXX_VISIBILITY_PRESET ${STATIC_APP_LIB_SYMBOL_VISIBILITY}
)
+320
View File
@@ -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;
}
}
+765
View File
@@ -0,0 +1,765 @@
/* -*- tab-width: 4; -*- */
/* vi: set sw=2 ts=4 expandtab: */
/*
* Copyright 2023 The Khronos Group Inc.
* SPDX-License-Identifier: Apache-2.0
*/
/**
* @internal
* @file
* @~English
*
* @brief Data Format Descriptor for imageio.
*
* @author Mark Callow
*/
#include <exception>
#include <iterator>
#include <string>
#include <vector>
#include <KHR/khr_df.h>
/// @brief Image format descriptor
///
/// Based on Khronos Data Format specification. Omits the parts needed for
/// serialization (size, descriptorType, etc.) and, since there is no worry
/// about writing & reading across compilers and platforms, uses a struct
/// instead of the khr\_df.h macros.
///
/// Note that @e samples are not @e channels, a.k.a @e components. @e samples
/// represent a series of contiguous bits in the bitstream representing
/// a pixel of the image. Since the various ImageInput derived classes
/// convert incoming data to local endianness most @e channels need only
/// a single sample. Only formats such as those with a shared exponent
/// need multiple samples per component.
///
/// This descriptor is way more general than is needed by the current set of
/// supported input formats for which there will always be one sample per
/// channel. We use this because it is familiar from use elsewhere in
/// KTX-Software and because of the large number of useful enums provided
/// by khr\_df.h.
///
/// @note This class uses the Data Format Specification nomenclature of
/// @e channel for consistency with thar spec. Elsewhere in KTX-Software
/// @e component is widely used.
///
struct FormatDescriptor {
/// @internal
/// @brief Basic descriptor.
struct basicDescriptor {
uint32_t colorModel: 8;
uint32_t colorPrimaries: 8;
uint32_t transferFunction: 8;
uint32_t dataFlags: 8;
uint32_t texelBlockDimension0: 8;
uint32_t texelBlockDimension1: 8;
uint32_t texelBlockDimension2: 8;
uint32_t texelBlockDimension3: 8;
uint32_t bytesPlane0: 8;
uint32_t bytesPlane1: 8;
uint32_t bytesPlane2: 8;
uint32_t bytesPlane3: 8;
uint32_t bytesPlane4: 8;
uint32_t bytesPlane5: 8;
uint32_t bytesPlane6: 8;
uint32_t bytesPlane7: 8;
/// @brief Default constructor
basicDescriptor() {
memset(this, 0, sizeof(*this));
}
/// @brief Constructor for unpacked, non-compressed textures.
basicDescriptor(uint32_t pixelByteCount,
khr_df_transfer_e t = KHR_DF_TRANSFER_UNSPECIFIED,
khr_df_primaries_e p = KHR_DF_PRIMARIES_BT709,
khr_df_model_e m = KHR_DF_MODEL_RGBSDA,
khr_df_flags_e f = KHR_DF_FLAG_ALPHA_STRAIGHT) {
colorModel = m;
colorPrimaries = p;
transferFunction = t;
dataFlags = f;
texelBlockDimension0 = 0; // Uncompressed means only 1x1x1x1 blocks.
texelBlockDimension1 = 0;
texelBlockDimension2 = 0;
texelBlockDimension3 = 0;
bytesPlane0 = pixelByteCount;
bytesPlane1 = bytesPlane2 = bytesPlane3 = 0;
bytesPlane4 = bytesPlane5 = bytesPlane6 = bytesPlane7 = 0;
}
bool operator==(const basicDescriptor& rhs) const {
const uint32_t* a = reinterpret_cast<const uint32_t*>(this);
const uint32_t* b = reinterpret_cast<const uint32_t*>(&rhs);
for (uint32_t i = 0; i < 4; i++) {
if (a[i] != b[i]) return false;
}
return true;
}
bool operator!=(const basicDescriptor& rhs) const {
return !(*this == rhs);
}
khr_df_model_e model() const noexcept {
return static_cast<khr_df_model_e>(colorModel);
}
khr_df_primaries_e primaries() const noexcept {
return static_cast<khr_df_primaries_e>(colorPrimaries);
}
khr_df_transfer_e transfer() const noexcept {
return static_cast<khr_df_transfer_e>(transferFunction);
}
khr_df_flags_e flags() const noexcept {
return static_cast<khr_df_flags_e>(dataFlags);
}
} basic;
/// @internal
/// @brief Extended descriptor.
///
/// In a true DFD this would be an extension descriptor type complete with
/// size, vendorId, descriptorType, etc.
struct extendedDescriptor {
uint32_t channelCount; /// Saved channel count to avoid having to loop
/// over samples to figure out the count.
bool sameUnitAllChannels = false; /// All samples have same types and sizes.
float oeGamma = -1; /// Power function exponent used when the image was
/// encoded, if one was used. -1 otherwise.
/// @internal
/// @brief ICC profile descriptor.
struct iccProfileDescriptor {
std::string name;
std::vector<uint8_t> profile;
iccProfileDescriptor() { }
iccProfileDescriptor(std::string& n, uint8_t* p, size_t ps) {
name = n;
profile.resize(ps);
profile.insert(profile.begin(), p, &p[ps]);
}
bool operator==(const iccProfileDescriptor& rhs) const {
bool result = !this->name.compare(rhs.name);
return result && (this->profile == rhs.profile);
}
bool operator!=(const iccProfileDescriptor& rhs) const {
return !(*this == rhs);
}
} iccProfile;
extendedDescriptor(uint32_t cc = 0) :
channelCount(cc) { }
bool operator==(const extendedDescriptor& rhs) const {
if (this->channelCount != rhs.channelCount) return false;
if (this->sameUnitAllChannels != rhs.sameUnitAllChannels) return false;
if (this->oeGamma != rhs.oeGamma) return false;
return this->iccProfile == rhs.iccProfile;
}
bool operator!=(const extendedDescriptor& rhs) const {
return !(*this == rhs);
}
} extended;
struct sample {
uint32_t bitOffset: 16;
uint32_t bitLength: 8;
// uint32_t channelType: 8;
uint32_t channelType: 4;
uint32_t qualifierLinear: 1;
uint32_t qualifierExponent: 1;
uint32_t qualifierSigned: 1;
uint32_t qualifierFloat: 1;
uint32_t samplePosition0: 8;
uint32_t samplePosition1: 8;
uint32_t samplePosition2: 8;
uint32_t samplePosition3: 8;
uint32_t lower;
uint32_t upper;
bool operator==(const sample& rhs) const {
const uint32_t* a = reinterpret_cast<const uint32_t*>(this);
const uint32_t* b = reinterpret_cast<const uint32_t*>(&rhs);
for (uint32_t i = 0; i < 4; i++) {
if (a[i] != b[i]) return false;
}
return true;
}
/// @brief Constructs an uninitialized sample object
sample() = default;
/// @brief Construct a sample with default sampleUpper and sampleLower.
///
/// For uncompressed formats. Handle integer data as normalized. For
/// unsigned use the full range of the number of bits. For signed set
/// sampleUpper and sampleLower so 0 is representable.
sample(uint32_t chanType,
uint32_t bLength, uint32_t offset,
khr_df_sample_datatype_qualifiers_e dataType
= static_cast<khr_df_sample_datatype_qualifiers_e>(0),
khr_df_transfer_e tf = KHR_DF_TRANSFER_UNSPECIFIED,
khr_df_model_e m = KHR_DF_MODEL_RGBSDA) {
bitOffset = offset;
bitLength = bLength - 1;
channelType = chanType;
if (channelType == 3 && m != KHR_DF_MODEL_XYZW) {
/// XYZW does not have an alpha chennel. *_ALPHA has the same
/// value for all other 4-channel-capable uncompressed models.
channelType = KHR_DF_CHANNEL_RGBSDA_ALPHA;
}
qualifierFloat = (dataType & KHR_DF_SAMPLE_DATATYPE_FLOAT) != 0;
qualifierSigned = (dataType & KHR_DF_SAMPLE_DATATYPE_SIGNED) != 0;
qualifierExponent = (dataType & KHR_DF_SAMPLE_DATATYPE_EXPONENT) != 0;
qualifierLinear = (dataType & KHR_DF_SAMPLE_DATATYPE_LINEAR) != 0;
if (tf > KHR_DF_TRANSFER_LINEAR
&& channelType == KHR_DF_CHANNEL_RGBSDA_ALPHA) {
qualifierLinear = 1;
}
union {
uint32_t i;
float f;
} uLower, uUpper;
if (qualifierFloat) {
if (qualifierSigned) {
uUpper.f = 1.0f;
uLower.f = -1.0f;
} else {
uUpper.f = 1.0f;
uLower.f = 0.0f;
}
} else {
if (qualifierSigned) {
// signed normalized
if (bitLength > 32) {
uUpper.i = 0x7FFFFFFF;
} else {
uUpper.i = (1U << (bLength - 1)) - 1;
}
uLower.i = ~uUpper.i;
uLower.i += 1;
} else {
// unsigned normalized
if (bitLength > 32) {
uUpper.i = 0xFFFFFFFFU;
} else {
uUpper.i = (uint32_t)((1U << bLength) - 1U);
}
uLower.i = 0U;
}
}
lower = uLower.i;
upper = uUpper.i;
}
/// @brief Construct a sample with custom sampleLower and sampleUpper.
///
/// For uncompressed formats.
sample(uint32_t chanType,
uint32_t bitLength, uint32_t offset,
uint32_t sampleLower, uint32_t sampleUpper,
khr_df_sample_datatype_qualifiers_e dataType
= static_cast<khr_df_sample_datatype_qualifiers_e>(0),
khr_df_transfer_e tf = KHR_DF_TRANSFER_UNSPECIFIED,
khr_df_model_e m = KHR_DF_MODEL_RGBSDA)
: sample(chanType, bitLength, offset, dataType, tf, m)
{
if (qualifierFloat) {
throw std::runtime_error(
"Invalid use of constructor for float data"
);
}
lower = sampleLower;
upper = sampleUpper;
}
};
std::vector<sample> samples;
/// @brief Default constructor
///
/// Will have zero samples which means format unknown.
FormatDescriptor() { }
/// @brief Constructor for unpacked, non-compressed data.
///
/// All channels have the same number of bits and basic data type.
/// As all wide data types will be in local endianness we need only
/// one sample per channel.
///
/// Data is assumed to be unsigned normalized. @c sampleUpper will be
/// set to the max value representable by @a channelBitLength.
///
/// @c channelType will be set to the standard channel types for @a channelCount
/// and @a m.
FormatDescriptor(uint32_t channelCount, uint32_t channelBitLength,
khr_df_sample_datatype_qualifiers_e dt
= static_cast<khr_df_sample_datatype_qualifiers_e>(0),
khr_df_transfer_e t = KHR_DF_TRANSFER_UNSPECIFIED,
khr_df_primaries_e p = KHR_DF_PRIMARIES_BT709,
khr_df_model_e m = KHR_DF_MODEL_RGBSDA,
khr_df_flags_e f = KHR_DF_FLAG_ALPHA_STRAIGHT)
: basic((channelBitLength * channelCount) / 8, t, p, m, f),
extended(channelCount)
{
for (uint32_t s = 0; s < channelCount; s++) {
samples.push_back(sample(s, channelBitLength,
s * channelBitLength,
dt, t, m));
}
if (m == KHR_DF_MODEL_YUVSDA && channelCount == 2) {
samples[1].channelType = KHR_DF_CHANNEL_YUVSDA_ALPHA;
}
extended.sameUnitAllChannels = true;
}
/// @brief Constructor for unpacked, non-compressed data with custom
/// sampleLower and sampleUpper
///
/// All channels have the same number of bits and basic data type.
/// Use this for unnormalized integer data or normalized data that does not
/// use the full range representable by @a channelBitLength.
FormatDescriptor(uint32_t channelCount, uint32_t channelBitLength,
uint32_t sampleLower, uint32_t sampleUpper,
khr_df_sample_datatype_qualifiers_e dt
= static_cast<khr_df_sample_datatype_qualifiers_e>(0),
khr_df_transfer_e t = KHR_DF_TRANSFER_UNSPECIFIED,
khr_df_primaries_e p = KHR_DF_PRIMARIES_BT709,
khr_df_model_e m = KHR_DF_MODEL_RGBSDA,
khr_df_flags_e f = KHR_DF_FLAG_ALPHA_STRAIGHT)
: basic((channelBitLength * channelCount) / 8, t, p, m, f),
extended(channelCount)
{
for (uint32_t s = 0; s < channelCount; s++) {
samples.push_back(sample(s, channelBitLength,
s * channelBitLength,
sampleLower, sampleUpper,
dt, t, m));
}
if (m == KHR_DF_MODEL_YUVSDA && channelCount == 2) {
samples[1].channelType = KHR_DF_CHANNEL_YUVSDA_ALPHA;
}
extended.sameUnitAllChannels = true;
}
static uint32_t totalBits(uint32_t sampleCount, std::vector<uint32_t>& bits) {
uint32_t totalBits = 0;
for (uint32_t s = 0; s < sampleCount; s++) {
totalBits += bits[s];
}
return totalBits;
}
/// @brief Constructor for non-compressed textures with varying bit lengths or channel types.
///
/// Each channel has the same basic data type.
FormatDescriptor(uint32_t channelCount,
std::vector<uint32_t>& channelBitLengths,
std::vector<khr_df_model_channels_e>& channelTypes,
khr_df_sample_datatype_qualifiers_e dt
= static_cast<khr_df_sample_datatype_qualifiers_e>(0),
khr_df_transfer_e t = KHR_DF_TRANSFER_UNSPECIFIED,
khr_df_primaries_e p = KHR_DF_PRIMARIES_BT709,
khr_df_model_e m = KHR_DF_MODEL_RGBSDA,
khr_df_flags_e f = KHR_DF_FLAG_ALPHA_STRAIGHT)
: basic(totalBits(channelCount, channelBitLengths) >> 3, t, p, m, f),
extended(channelCount)
{
if (channelCount > channelBitLengths.size()
|| channelCount > channelTypes.size()) {
throw std::runtime_error(
"Not enough channelBits or channelType specfications."
);
}
uint32_t bitOffset = 0;
bool bitLengthsEqual = true;
uint32_t firstBitLength = channelBitLengths[0];
for (uint32_t s = 0; s < channelCount; s++) {
samples.push_back(sample(channelTypes[s], channelBitLengths[s],
bitOffset, dt, t, m));
bitOffset += channelBitLengths[s];
if (firstBitLength != channelBitLengths[s]) {
bitLengthsEqual = false;
}
}
if (bitLengthsEqual) {
extended.sameUnitAllChannels = true;
}
if (m == KHR_DF_MODEL_YUVSDA && channelCount == 2) {
samples[1].channelType = KHR_DF_CHANNEL_YUVSDA_ALPHA;
}
}
/// @brief Constructor for non-compressed textures with varying bit lengths
/// or channel types and custom sampleLower and sampleUpper.
///
/// Each channel has the same basic data type. Use this for unnormalized
/// integer data or normalized data that does not use the full bit range.
FormatDescriptor(uint32_t channelCount,
std::vector<uint32_t>& channelBitLengths,
std::vector<khr_df_model_channels_e>& channelTypes,
std::vector<uint32_t>& samplesLower,
std::vector<uint32_t>& samplesUpper,
khr_df_sample_datatype_qualifiers_e dt
= static_cast<khr_df_sample_datatype_qualifiers_e>(0),
khr_df_transfer_e t = KHR_DF_TRANSFER_UNSPECIFIED,
khr_df_primaries_e p = KHR_DF_PRIMARIES_BT709,
khr_df_model_e m = KHR_DF_MODEL_RGBSDA,
khr_df_flags_e f = KHR_DF_FLAG_ALPHA_STRAIGHT)
: basic(totalBits(channelCount, channelBitLengths) >> 3, t, p, m, f),
extended(channelCount)
{
if (channelCount > channelBitLengths.size()
|| channelCount > channelTypes.size()) {
throw std::runtime_error(
"Not enough channelBits or channelType specfications."
);
}
uint32_t bitOffset = 0;
bool bitLengthsEqual = true;
uint32_t firstBitLength = channelBitLengths[0];
for (uint32_t s = 0; s < channelCount; s++) {
samples.push_back(sample(channelTypes[s], channelBitLengths[s],
samplesLower[s], samplesUpper[s],
bitOffset, dt, t, m));
bitOffset += channelBitLengths[s];
if (firstBitLength != channelBitLengths[s]) {
bitLengthsEqual = false;
}
}
if (bitLengthsEqual) {
extended.sameUnitAllChannels = true;
}
if (m == KHR_DF_MODEL_YUVSDA && channelCount == 2) {
samples[1].channelType = KHR_DF_CHANNEL_YUVSDA_ALPHA;
}
}
/// @brief Constructor for non-compressed, shared exponent format.
///
/// Each channel is a floating point. All channels share the same exponent
/// and have the same number of mantissa bits.
// TODO: Handle whether there is an implicit 1 and a sign bit.
FormatDescriptor(uint32_t channelCount,
uint32_t mantissaBitLength,
uint32_t exponentBitLength,
khr_df_sample_datatype_qualifiers_e dt
= static_cast<khr_df_sample_datatype_qualifiers_e>(0),
khr_df_transfer_e t = KHR_DF_TRANSFER_LINEAR,
khr_df_primaries_e p = KHR_DF_PRIMARIES_BT709,
khr_df_model_e m = KHR_DF_MODEL_RGBSDA,
khr_df_flags_e f = KHR_DF_FLAG_ALPHA_STRAIGHT)
: basic((channelCount * mantissaBitLength + exponentBitLength) >> 3,
t, p, m, f),
extended(channelCount)
{
if (dt & KHR_DF_SAMPLE_DATATYPE_FLOAT) {
throw std::runtime_error(
"DATATYPE_FLOAT is set for a shared exponent format");
}
for (uint32_t s = 0; s < channelCount; s++) {
uint32_t sampleLower = 0, sampleUpper;
// sampleUpper and sampleLower values for the mantissa should be
// set to indicate the representation of 1.0 and 0.0 (for unsigned
// formats) or -1.0 (for signed formats) respectively when the
// exponent is in a 0 position after any bias has been corrected.
// If there is an implicit 1 bit, these values for the mantissa
// will exceed what can be represented in the number of available
// mantissa bits.
sampleUpper = 1U << mantissaBitLength;
samples.push_back(sample(s, mantissaBitLength,
s * mantissaBitLength,
sampleLower, sampleUpper,
dt, t, m));
// The sampleLower for the exponent should indicate the exponent
// bias. That is, the mantissa should be scaled by two raised to
// the power of the stored exponent minus this sampleLower value.
//
// The sampleUpper for the exponent indicates the maximum legal
// exponent value. Values above this are used to encode infinities
// and not-a-number (NaN) values. sampleUpper can therefore be used
// to indicate whether or not the format supports these encodings.
//sampleLower = exponentBias;
//sampleUpper = maxLegalExponentValue;
samples.push_back(sample(s, exponentBitLength,
channelCount * mantissaBitLength,
sampleLower, sampleUpper,
static_cast<khr_df_sample_datatype_qualifiers_e>(dt | KHR_DF_SAMPLE_DATATYPE_EXPONENT),
t, m));
}
extended.sameUnitAllChannels = true;
}
/// @brief Constructor from pre-constructed basic and sample descriptors
FormatDescriptor(
FormatDescriptor::basicDescriptor basic,
std::vector<FormatDescriptor::sample> samples_)
: basic(basic),
extended(static_cast<uint32_t>(samples_.size())),
samples(std::move(samples_))
{
extended.sameUnitAllChannels = true;
if (!samples.empty()) {
for (uint32_t i = 1; i < static_cast<uint32_t>(samples.size()); ++i) {
if (samples[0].bitLength != samples[i].bitLength
|| samples[0].qualifierLinear != samples[i].qualifierLinear
|| samples[0].qualifierExponent != samples[i].qualifierExponent
|| samples[0].qualifierSigned != samples[i].qualifierSigned
|| samples[0].qualifierFloat != samples[i].qualifierFloat) {
extended.sameUnitAllChannels = false;
break;
}
}
}
}
bool isUnknown() const noexcept {
return samples.size() == 0;
}
bool sameUnitAllChannels() const noexcept {
return extended.sameUnitAllChannels;
}
bool operator==(const FormatDescriptor& rhs) const {
if (this->basic != rhs.basic) return false;
if (this->extended != rhs.extended) return false;
return this->samples == rhs.samples;
}
bool operator!=(const FormatDescriptor& rhs) const {
return !(*this == rhs);
}
khr_df_model_e model() const noexcept {
return basic.model();
}
khr_df_primaries_e primaries() const noexcept {
return basic.primaries();
}
khr_df_transfer_e transfer() const noexcept {
return basic.transfer();
}
khr_df_flags_e flags() const noexcept {
return basic.flags();
}
float oeGamma() const noexcept {
return extended.oeGamma;
}
const std::string& iccProfileName() const noexcept {
return extended.iccProfile.name;
}
const std::vector<uint8_t>& iccProfile() const noexcept {
return extended.iccProfile.profile;
}
void setModel(khr_df_model_e m) {
basic.colorModel = m;
}
void setPrimaries(khr_df_primaries_e p) {
basic.colorPrimaries = p;
}
void setTransfer(khr_df_transfer_e t) {
khr_df_transfer_e oldOetf = transfer();
basic.transferFunction = t;
if ((oldOetf <= KHR_DF_TRANSFER_LINEAR) != (t <= KHR_DF_TRANSFER_LINEAR))
{
std::vector<sample>::iterator sit = samples.begin();
for (; sit < samples.end(); sit++) {
if (sit->channelType == KHR_DF_CHANNEL_RGBSDA_ALPHA) {
sit->qualifierLinear = t > KHR_DF_TRANSFER_LINEAR;
}
}
}
}
uint32_t pixelByteCount() const noexcept {
return basic.bytesPlane0;
}
uint32_t sampleCount() const noexcept {
return static_cast<uint32_t>(samples.size());
}
uint32_t sampleBitLength(uint32_t s) const noexcept {
return samples[s].bitLength + 1;
}
// TODO: remove?
uint32_t sampleByteCount(uint32_t s) const noexcept {
// Use integer division so 0 is returned when length is < a byte.
return sampleBitLength(s) / 8;
}
uint32_t sampleUpper(uint32_t s) const noexcept {
return samples[s].upper;
}
uint32_t channelCount() const noexcept {
return extended.channelCount;
}
uint32_t channelBitLength(khr_df_model_channels_e c) const {
std::vector<sample>::const_iterator it = samples.begin();
uint32_t bitLength = 0;
for (; it < samples.end(); it++) {
if (it->channelType == static_cast<uint32_t>(c)) {
bitLength += it->bitLength + 1;
}
}
if (bitLength == 0) {
throw std::runtime_error("No such channel.");
}
return bitLength;
}
uint32_t channelBitLength() const {
if (channelCount() == 1)
return samples[0].bitLength + 1;
if (!extended.sameUnitAllChannels) {
throw std::runtime_error(
"Differing size channels. Specify channel to query."
);
}
return channelBitLength(KHR_DF_CHANNEL_RGBSDA_R);
}
uint32_t largestChannelBitLength() const {
uint32_t maxBitLength = 0;
for (uint32_t i = 0; i < 16; ++i) {
uint32_t bitLength = 0;
for (const auto& sample : samples)
if (sample.channelType == i)
bitLength += sample.bitLength + 1;
if (bitLength > maxBitLength)
maxBitLength = bitLength;
}
return maxBitLength;
}
bool anyChannelBitLengthNotEqual(uint32_t bitLength) const {
for (uint32_t i = 0; i < 16; ++i) {
uint32_t channelBitLength = 0;
for (const auto& sample : samples)
if (sample.channelType == i)
channelBitLength += sample.bitLength + 1;
if (bitLength != channelBitLength)
return true;
}
return false;
}
khr_df_sample_datatype_qualifiers_e
channelDataType(khr_df_model_channels_e c) const {
// TODO: Fix for shared exponent case...
std::vector<sample>::const_iterator it = samples.begin();
for (; it < samples.end(); it++) {
if (it->channelType == static_cast<uint32_t>(c)) {
return static_cast<khr_df_sample_datatype_qualifiers_e>
(it->channelType & KHR_DF_SAMPLEMASK_QUALIFIERS);
}
}
throw std::runtime_error("No such channel.");
}
khr_df_sample_datatype_qualifiers_e channelDataType() const {
if (!extended.sameUnitAllChannels) {
throw std::runtime_error(
"Differing size channels. Specify channel to query."
);
}
return channelDataType(KHR_DF_CHANNEL_RGBSDA_R);
}
uint32_t channelUpper() const {
if (extended.channelCount != samples.size()) {
throw std::runtime_error(
"Multiple samples per channel. Call sampleUpper(uint32_t s)."
);
}
if (!extended.sameUnitAllChannels) {
throw std::runtime_error(
"Differing size channels. Call sampleUpper(uint32_t s)."
);
}
return sampleUpper(KHR_DF_CHANNEL_RGBSDA_R);
}
void updateSampleInfo(uint32_t channelCount, uint32_t channelBitLength,
uint32_t sampleLower, uint32_t sampleUpper,
khr_df_sample_datatype_qualifiers_e dt
= static_cast<khr_df_sample_datatype_qualifiers_e>(0))
{
samples.clear();
for (uint32_t s = 0; s < channelCount; s++) {
samples.push_back(sample(s, channelBitLength,
s * channelBitLength,
sampleLower, sampleUpper,
dt,
basic.transfer(),
basic.model()));
}
if (basic.model() == KHR_DF_MODEL_YUVSDA && channelCount == 2) {
samples[1].channelType = KHR_DF_CHANNEL_YUVSDA_ALPHA;
}
extended.channelCount = channelCount;
extended.sameUnitAllChannels = true;
}
void updateSampleBitCounts(std::vector<uint32_t>& bits) {
uint32_t b, s;
uint32_t offset = 0;
for (b = 0, s = 0; s < samples.size(); s++) {
samples[s].bitLength = bits[b] - 1;
samples[s].bitOffset = offset;
offset += bits[b];
if (b < bits.size() - 1)
b++;
// else set remaining sample sizes to last available bits value.
}
}
void updateChannelCount(uint32_t newCount) {
if (newCount == extended.channelCount)
return;
if (extended.channelCount != samples.size()) {
// TODO: Either fix error handling or implement
throw std::runtime_error(
"changeChannelCount not supported when # samples != # channels");
}
extended.channelCount = newCount;
if (newCount < samples.size()) {
samples.erase(samples.begin() + newCount, samples.end());
return;
}
uint32_t firstNewIndex = static_cast<uint32_t>(samples.size());
uint32_t offset = samples.back().bitOffset
+ samples.back().bitLength + 1;
samples.resize(newCount, samples.back());
std::vector<sample>::iterator sit = samples.begin() + firstNewIndex;
for (; sit < samples.end(); sit++) {
sit->bitOffset = offset;
offset += (sit->bitLength + 1);
}
}
[[nodiscard]] const sample* find(khr_df_model_channels_e channel) const {
for (const auto& sample : samples)
if (sample.channelType == static_cast<uint32_t>(channel))
return &sample;
return nullptr;
}
void removeLastChannel() {
const auto numChannels = static_cast<uint32_t>(samples.size());
assert(numChannels > 1);
assert(basic.bytesPlane0 % numChannels == 0);
samples.pop_back();
basic.bytesPlane0 = basic.bytesPlane0 / numChannels * (numChannels - 1u);
if (extended.channelCount != 0)
--extended.channelCount;
}
friend std::ostream& operator<< (std::ostream& o, khr_df_sample_datatype_qualifiers_e q) {
if (q & KHR_DF_SAMPLE_DATATYPE_SIGNED)
o << " signed ";
if (q & KHR_DF_SAMPLE_DATATYPE_FLOAT) {
if (!(q & KHR_DF_SAMPLE_DATATYPE_SIGNED))
o << " ";
o << "float ";
}
return o;
}
};
File diff suppressed because it is too large Load Diff
+715
View File
@@ -0,0 +1,715 @@
// -*- tab-width: 4; -*-
// vi: set sw=2 ts=4 expandtab:
// Copyright 2023-2024 The Khronos Group Inc.
// Copyright 2023-2024 RasterGrid Kft.
// SPDX-License-Identifier: Apache-2.0
//!
//! @internal
//! @~English
//! @file
//!
//! @brief Internal Image Codec class
//!
#pragma once
#include <algorithm>
#include <functional>
#include <limits>
#include <type_traits>
#include <map>
#include <vector>
#include <assert.h>
#ifdef _MSC_VER
#pragma warning(push)
#pragma warning(disable : 4201)
#endif
#include <glm/gtc/packing.hpp>
#ifdef _MSC_VER
#pragma warning(pop)
#endif
#include "imageio_utility.h"
#include "vkformat_enum.h"
class ImageCodec {
public:
struct TexelBlockCodec {
};
ImageCodec() {
flags.valid = false;
flags.isBlockCompressed = false;
flags.isPacked = false;
flags.isFloat = false;
flags.isFloatHalf = false;
flags.isSigned = false;
flags.isNormalized = false;
}
ImageCodec(VkFormat vkFormat, uint32_t typeSize, const uint32_t* dfd) {
flags.valid = true;
flags.isBlockCompressed = false;
flags.isPacked = false;
flags.isFloat = false;
flags.isFloatHalf = false;
flags.isSigned = false;
flags.isNormalized = false;
const auto* bdfd = dfd + 1;
const auto model = khr_df_model_e(KHR_DFDVAL(bdfd, MODEL));
texelBlockDimensions[0] = KHR_DFDVAL(bdfd, TEXELBLOCKDIMENSION0) + 1;
texelBlockDimensions[1] = KHR_DFDVAL(bdfd, TEXELBLOCKDIMENSION1) + 1;
texelBlockDimensions[2] = KHR_DFDVAL(bdfd, TEXELBLOCKDIMENSION2) + 1;
texelBlockDimensions[3] = KHR_DFDVAL(bdfd, TEXELBLOCKDIMENSION3) + 1;
packedElementByteSize = typeSize;
// Packed element size must always be 1, 2, or 4 currently
// (for block compressed formats the element size is considered 1 by convention)
switch (packedElementByteSize) {
case 1: codec.getPackedElement = getPackedElement<uint8_t>; break;
case 2: codec.getPackedElement = getPackedElement<uint16_t>; break;
case 4: codec.getPackedElement = getPackedElement<uint32_t>; break;
default: flags.valid = false; return;
}
// We initialize the packed element count here based on the first plane's size and the element size
const auto firstPlaneBytes = KHR_DFDVAL(bdfd, BYTESPLANE0);
packedElementCount = firstPlaneBytes / packedElementByteSize;
// We can do the above because we do not currently support multiple planes
if (KHR_DFDVAL(bdfd, BYTESPLANE1) != 0) {
flags.valid = false;
return;
}
// If packedElementCount is zero, then there's something wrong with bytesPlane0 being zero
if (packedElementCount == 0) {
flags.valid = false;
return;
}
// By default we do not have directly accessible channels
// (e.g. for block compressed we can only access packed data)
channels = 0;
switch (model) {
case KHR_DF_MODEL_RGBSDA: [[fallthrough]];
case KHR_DF_MODEL_YUVSDA: [[fallthrough]];
case KHR_DF_MODEL_YIQSDA: [[fallthrough]];
case KHR_DF_MODEL_LABSDA: [[fallthrough]];
case KHR_DF_MODEL_CMYKA: [[fallthrough]];
case KHR_DF_MODEL_XYZW: [[fallthrough]];
case KHR_DF_MODEL_HSVA_ANG: [[fallthrough]];
case KHR_DF_MODEL_HSLA_ANG: [[fallthrough]];
case KHR_DF_MODEL_HSVA_HEX: [[fallthrough]];
case KHR_DF_MODEL_HSLA_HEX: [[fallthrough]];
case KHR_DF_MODEL_YCGCOA: [[fallthrough]];
case KHR_DF_MODEL_YCCBCCRC: [[fallthrough]];
case KHR_DF_MODEL_ICTCP: [[fallthrough]];
case KHR_DF_MODEL_CIEXYZ: [[fallthrough]];
case KHR_DF_MODEL_CIEXYY:
// These color models are handled as simple per-channel texel blocks
switch (vkFormat) {
case VK_FORMAT_E5B9G9R9_UFLOAT_PACK32:
flags.isFloat = true;
channels = 3;
codec.decodeFLOAT = decodeFLOAT_E9B5G5R5;
break;
case VK_FORMAT_B10G11R11_UFLOAT_PACK32:
channels = 3;
codec.decodeFLOAT = decodeFLOAT_B10G11R11;
break;
case VK_FORMAT_R16G16_SFIXED5_NV:
channels = 2;
codec.decodeFLOAT = decodeFLOAT_SFIXED5_NV<2>;
break;
case VK_FORMAT_D16_UNORM_S8_UINT:
flags.isNormalized = true;
channels = 2;
codec.decodeUINT = decodeUINT_D16_S8;
codec.decodeFLOAT = decodeFLOAT_D16_S8;
break;
case VK_FORMAT_X8_D24_UNORM_PACK32:
flags.isNormalized = true;
channels = 1;
codec.decodeUINT = decodeUINT_D24;
codec.decodeFLOAT = decodeFLOAT_D24;
break;
case VK_FORMAT_D24_UNORM_S8_UINT:
flags.isNormalized = true;
channels = 2;
codec.decodeUINT = decodeUINT_D24_S8;
codec.decodeFLOAT = decodeFLOAT_D24_S8;
break;
case VK_FORMAT_D32_SFLOAT_S8_UINT:
channels = 2;
codec.decodeFLOAT = decodeFLOAT_D32_S8;
break;
default: {
// For other formats we only support cases where the number formats match across the samples
const auto sampleCount = KHR_DFDSAMPLECOUNT(bdfd);
const auto firstDataType = KHR_DFDSVAL(bdfd, 0, QUALIFIERS) & ~KHR_DF_SAMPLE_DATATYPE_LINEAR;
const auto firstBitLength = KHR_DFDSVAL(bdfd, 0, BITLENGTH) + 1;
const auto sampleUpper = KHR_DFDSVAL(bdfd, 0, SAMPLEUPPER);
flags.isFloat = (firstDataType & KHR_DF_SAMPLE_DATATYPE_FLOAT) != 0;
flags.isFloatHalf = flags.isFloat && (firstBitLength == 16);
flags.isSigned = (firstDataType & KHR_DF_SAMPLE_DATATYPE_SIGNED) != 0;
flags.isNormalized = sampleUpper != (flags.isFloat ? imageio::bit_cast<uint32_t>(1.f) : 1u);
// Channel count matches sample count for these cases
channels = sampleCount;
if (firstDataType & KHR_DF_SAMPLE_DATATYPE_EXPONENT) {
// No other shared exponent formats are supported other than the ones handled explicitly
flags.valid = false;
return;
}
for (uint32_t i = 0; i < sampleCount; ++i) {
const auto dataType = KHR_DFDSVAL(bdfd, i, QUALIFIERS) & ~KHR_DF_SAMPLE_DATATYPE_LINEAR;
const auto bitLength = KHR_DFDSVAL(bdfd, i, BITLENGTH) + 1;
// If not all elements match the packed element byte size then this is a packed format
if (bitLength != firstBitLength || bitLength != packedElementByteSize * 8) {
flags.isPacked = true;
}
// We do not support mixed component types here as all special cases are handled outside
if (dataType != firstDataType) {
flags.valid = false;
return;
}
// For floats we also require either 32-bit or 16-bit float as all other
// special cases are currently handled outside
if (flags.isFloat && (bitLength != 16) && (bitLength != 32)) {
flags.valid = false;
return;
}
}
if (flags.isFloatHalf) {
// Data is a vector of FP16 values
switch (sampleCount) {
case 1: codec.decodeFLOAT = decodeFLOAT_FP16Vec<1>; break;
case 2: codec.decodeFLOAT = decodeFLOAT_FP16Vec<2>; break;
case 3: codec.decodeFLOAT = decodeFLOAT_FP16Vec<3>; break;
case 4: codec.decodeFLOAT = decodeFLOAT_FP16Vec<4>; break;
default: flags.valid = false; return;
}
} else if (flags.isFloat) {
// Data is a vector of FP32 values
switch (sampleCount) {
case 1: codec.decodeFLOAT = decodeFLOAT_FP32Vec<1>; break;
case 2: codec.decodeFLOAT = decodeFLOAT_FP32Vec<2>; break;
case 3: codec.decodeFLOAT = decodeFLOAT_FP32Vec<3>; break;
case 4: codec.decodeFLOAT = decodeFLOAT_FP32Vec<4>; break;
default: flags.valid = false; return;
}
} else if (flags.isPacked) {
// Data is packed so use the more general decoders
for (uint32_t i = 0; i < sampleCount; ++i) {
const auto bitOffset = KHR_DFDSVAL(bdfd, i, BITOFFSET);
const auto bitLength = KHR_DFDSVAL(bdfd, i, BITLENGTH) + 1;
SampleInfo sampleInfo{};
sampleInfo.elementIndex = (bitOffset >> 3) / packedElementByteSize;
sampleInfo.bitOffset = bitOffset % (packedElementByteSize << 3);
sampleInfo.bitLength = bitLength;
// If any of the samples straddle the packed elements then there's something wrong
if (sampleInfo.bitOffset + sampleInfo.bitLength > (packedElementByteSize << 3)) {
flags.valid = false;
return;
}
packedSampleInfo.emplace_back(sampleInfo);
}
switch (packedElementByteSize) {
case 1:
// 8-bit packed elements
if (flags.isSigned) {
codec.decodeSINT = decodeSINT_SINTPacked<int8_t>;
if (flags.isNormalized)
codec.decodeFLOAT = decodeFLOAT_SINTPacked<int8_t>;
} else {
codec.decodeUINT = decodeUINT_UINTPacked<uint8_t>;
if (flags.isNormalized)
codec.decodeFLOAT = decodeFLOAT_UINTPacked<uint8_t>;
}
break;
case 2:
// 16-bit packed elements
if (flags.isSigned) {
codec.decodeSINT = decodeSINT_SINTPacked<int16_t>;
if (flags.isNormalized)
codec.decodeFLOAT = decodeFLOAT_SINTPacked<int16_t>;
} else {
codec.decodeUINT = decodeUINT_UINTPacked<uint16_t>;
if (flags.isNormalized)
codec.decodeFLOAT = decodeFLOAT_UINTPacked<uint16_t>;
}
break;
case 4:
// 32-bit packed elements
if (flags.isSigned) {
codec.decodeSINT = decodeSINT_SINTPacked<int32_t>;
if (flags.isNormalized)
codec.decodeFLOAT = decodeFLOAT_SINTPacked<int32_t>;
} else {
codec.decodeUINT = decodeUINT_UINTPacked<uint32_t>;
if (flags.isNormalized)
codec.decodeFLOAT = decodeFLOAT_UINTPacked<uint32_t>;
}
break;
default:
flags.valid = false;
return;
}
} else {
// Data is not packed so we can use the optimized decoders
if (flags.isSigned) {
switch (packedElementByteSize) {
case 1:
// 8-bit signed integer
switch (sampleCount) {
case 1: codec.decodeSINT = decodeSINT_SINTVec<int8_t, 1>; break;
case 2: codec.decodeSINT = decodeSINT_SINTVec<int8_t, 2>; break;
case 3: codec.decodeSINT = decodeSINT_SINTVec<int8_t, 3>; break;
case 4: codec.decodeSINT = decodeSINT_SINTVec<int8_t, 4>; break;
default: flags.valid = false; return;
}
if (flags.isNormalized) {
switch (sampleCount) {
case 1: codec.decodeFLOAT = decodeFLOAT_SINTVec<int8_t, 1>; break;
case 2: codec.decodeFLOAT = decodeFLOAT_SINTVec<int8_t, 2>; break;
case 3: codec.decodeFLOAT = decodeFLOAT_SINTVec<int8_t, 3>; break;
case 4: codec.decodeFLOAT = decodeFLOAT_SINTVec<int8_t, 4>; break;
default: break;
}
}
break;
case 2:
// 16-bit signed integer
switch (sampleCount) {
case 1: codec.decodeSINT = decodeSINT_SINTVec<int16_t, 1>; break;
case 2: codec.decodeSINT = decodeSINT_SINTVec<int16_t, 2>; break;
case 3: codec.decodeSINT = decodeSINT_SINTVec<int16_t, 3>; break;
case 4: codec.decodeSINT = decodeSINT_SINTVec<int16_t, 4>; break;
default: flags.valid = false; return;
}
if (flags.isNormalized) {
switch (sampleCount) {
case 1: codec.decodeFLOAT = decodeFLOAT_SINTVec<int16_t, 1>; break;
case 2: codec.decodeFLOAT = decodeFLOAT_SINTVec<int16_t, 2>; break;
case 3: codec.decodeFLOAT = decodeFLOAT_SINTVec<int16_t, 3>; break;
case 4: codec.decodeFLOAT = decodeFLOAT_SINTVec<int16_t, 4>; break;
default: break;
}
}
break;
case 4:
// 32-bit signed integer
switch (sampleCount) {
case 1: codec.decodeSINT = decodeSINT_SINTVec<int32_t, 1>; break;
case 2: codec.decodeSINT = decodeSINT_SINTVec<int32_t, 2>; break;
case 3: codec.decodeSINT = decodeSINT_SINTVec<int32_t, 3>; break;
case 4: codec.decodeSINT = decodeSINT_SINTVec<int32_t, 4>; break;
default: flags.valid = false; return;
}
if (flags.isNormalized) {
switch (sampleCount) {
case 1: codec.decodeFLOAT = decodeFLOAT_SINTVec<int32_t, 1>; break;
case 2: codec.decodeFLOAT = decodeFLOAT_SINTVec<int32_t, 2>; break;
case 3: codec.decodeFLOAT = decodeFLOAT_SINTVec<int32_t, 3>; break;
case 4: codec.decodeFLOAT = decodeFLOAT_SINTVec<int32_t, 4>; break;
default: break;
}
}
break;
default:
flags.valid = false;
return;
}
} else {
switch (packedElementByteSize) {
case 1:
// 8-bit unsigned integer
switch (sampleCount) {
case 1: codec.decodeUINT = decodeUINT_UINTVec<uint8_t, 1>; break;
case 2: codec.decodeUINT = decodeUINT_UINTVec<uint8_t, 2>; break;
case 3: codec.decodeUINT = decodeUINT_UINTVec<uint8_t, 3>; break;
case 4: codec.decodeUINT = decodeUINT_UINTVec<uint8_t, 4>; break;
default: flags.valid = false; return;
}
if (flags.isNormalized) {
switch (sampleCount) {
case 1: codec.decodeFLOAT = decodeFLOAT_UINTVec<uint8_t, 1>; break;
case 2: codec.decodeFLOAT = decodeFLOAT_UINTVec<uint8_t, 2>; break;
case 3: codec.decodeFLOAT = decodeFLOAT_UINTVec<uint8_t, 3>; break;
case 4: codec.decodeFLOAT = decodeFLOAT_UINTVec<uint8_t, 4>; break;
default: break;
}
}
break;
case 2:
// 16-bit unsigned integer
switch (sampleCount) {
case 1: codec.decodeUINT = decodeUINT_UINTVec<uint16_t, 1>; break;
case 2: codec.decodeUINT = decodeUINT_UINTVec<uint16_t, 2>; break;
case 3: codec.decodeUINT = decodeUINT_UINTVec<uint16_t, 3>; break;
case 4: codec.decodeUINT = decodeUINT_UINTVec<uint16_t, 4>; break;
default: flags.valid = false; return;
}
if (flags.isNormalized) {
switch (sampleCount) {
case 1: codec.decodeFLOAT = decodeFLOAT_UINTVec<uint16_t, 1>; break;
case 2: codec.decodeFLOAT = decodeFLOAT_UINTVec<uint16_t, 2>; break;
case 3: codec.decodeFLOAT = decodeFLOAT_UINTVec<uint16_t, 3>; break;
case 4: codec.decodeFLOAT = decodeFLOAT_UINTVec<uint16_t, 4>; break;
default: break;
}
}
break;
case 4:
// 32-bit unsigned integer
switch (sampleCount) {
case 1: codec.decodeUINT = decodeUINT_UINTVec<uint32_t, 1>; break;
case 2: codec.decodeUINT = decodeUINT_UINTVec<uint32_t, 2>; break;
case 3: codec.decodeUINT = decodeUINT_UINTVec<uint32_t, 3>; break;
case 4: codec.decodeUINT = decodeUINT_UINTVec<uint32_t, 4>; break;
default: flags.valid = false; return;
}
if (flags.isNormalized) {
switch (sampleCount) {
case 1: codec.decodeFLOAT = decodeFLOAT_UINTVec<uint32_t, 1>; break;
case 2: codec.decodeFLOAT = decodeFLOAT_UINTVec<uint32_t, 2>; break;
case 3: codec.decodeFLOAT = decodeFLOAT_UINTVec<uint32_t, 3>; break;
case 4: codec.decodeFLOAT = decodeFLOAT_UINTVec<uint32_t, 4>; break;
default: break;
}
}
break;
default:
flags.valid = false;
return;
}
}
}
break;
}
}
break;
case KHR_DF_MODEL_BC1A: [[fallthrough]];
case KHR_DF_MODEL_BC2: [[fallthrough]];
case KHR_DF_MODEL_BC3: [[fallthrough]];
case KHR_DF_MODEL_BC4: [[fallthrough]];
case KHR_DF_MODEL_BC5: [[fallthrough]];
case KHR_DF_MODEL_BC6H: [[fallthrough]];
case KHR_DF_MODEL_BC7: [[fallthrough]];
case KHR_DF_MODEL_ETC1: [[fallthrough]];
case KHR_DF_MODEL_ETC2: [[fallthrough]];
case KHR_DF_MODEL_ASTC: [[fallthrough]];
case KHR_DF_MODEL_PVRTC: [[fallthrough]];
case KHR_DF_MODEL_PVRTC2:
// These color models are handled is raw compressed blocks
flags.isBlockCompressed = true;
break;
case KHR_DF_MODEL_UASTC:
// UASTC needs special handling
packedElementByteSize = 1;
packedElementCount = 16;
flags.isBlockCompressed = true;
break;
case KHR_DF_MODEL_ETC1S:
// ETC1S (as used by BasisLZ) is not supported directly
flags.valid = false;
return;
default:
flags.valid = false;
return;
}
texelBlockByteSize = packedElementByteSize * packedElementCount;
// If we couldn't determine the texel block size then something went wrong.
if (texelBlockByteSize == 0) {
flags.valid = false;
return;
}
}
operator bool() const { return flags.valid; }
constexpr bool isBlockCompressed() const { return flags.isBlockCompressed; }
constexpr bool isPacked() const { return flags.isPacked; }
constexpr bool isFloat() const { return flags.isFloat; }
constexpr bool isFloatHalf() const { return flags.isFloatHalf; }
constexpr bool isSigned() const { return flags.isSigned; }
constexpr bool isNormalized() const { return flags.isNormalized; }
constexpr bool canDecodeUINT() const { return codec.decodeUINT != nullptr; }
constexpr bool canDecodeSINT() const { return codec.decodeSINT != nullptr; }
constexpr bool canDecodeFLOAT() const { return codec.decodeFLOAT != nullptr; }
glm::uvec4 getTexelBlockDimensions() const { return texelBlockDimensions; }
constexpr uint32_t getPackedElementByteSize() const { return packedElementByteSize; }
constexpr uint32_t getPackedElementCount() const { return packedElementCount; }
constexpr uint32_t getTexelBlockByteSize() const { return texelBlockByteSize; }
constexpr uint32_t getChannelCount() const { return channels; }
glm::uvec4 pixelToTexelBlockSize(glm::uvec4 pixelSize) const {
return (pixelSize + texelBlockDimensions - glm::uvec4(1, 1, 1, 1)) / texelBlockDimensions;
}
uint32_t getPackedElement(const void* ptr, uint32_t index) const { return codec.getPackedElement(this, ptr, index); }
glm::uvec4 decodeUINT(const void* ptr) const { return codec.decodeUINT(this, ptr); }
glm::ivec4 decodeSINT(const void* ptr) const { return codec.decodeSINT(this, ptr); }
glm::vec4 decodeFLOAT(const void* ptr) const { return codec.decodeFLOAT(this, ptr); }
private:
struct {
uint32_t valid : 1;
uint32_t isBlockCompressed : 1;
uint32_t isPacked : 1;
uint32_t isFloat : 1;
uint32_t isFloatHalf : 1;
uint32_t isSigned : 1;
uint32_t isNormalized : 1;
} flags;
glm::uvec4 texelBlockDimensions = glm::uvec4(0, 0, 0, 0);
uint32_t packedElementByteSize = 0;
uint32_t packedElementCount = 0;
uint32_t texelBlockByteSize = 0;
uint32_t channels = 0;
struct SampleInfo {
uint32_t elementIndex;
uint32_t bitOffset;
uint32_t bitLength;
};
std::vector<SampleInfo> packedSampleInfo = {};
typedef uint32_t (*GetPackedElement)(const ImageCodec*, const void*, uint32_t);
typedef glm::uvec4 (*DecodeUINT)(const ImageCodec*, const void*);
typedef glm::ivec4 (*DecodeSINT)(const ImageCodec*, const void*);
typedef glm::vec4 (*DecodeFLOAT)(const ImageCodec*, const void*);
struct {
GetPackedElement getPackedElement = nullptr;
DecodeUINT decodeUINT = nullptr;
DecodeSINT decodeSINT = nullptr;
DecodeFLOAT decodeFLOAT = nullptr;
} codec = {};
template <typename TYPE>
static uint32_t getPackedElement(const ImageCodec* codec, const void* ptr, uint32_t index) {
static_assert(std::is_unsigned_v<TYPE>);
(void)codec; // silences unused parameter warnings in release builds
assert(sizeof(TYPE) == codec->getPackedElementByteSize());
auto data = reinterpret_cast<const TYPE*>(ptr);
return data[index];
}
static glm::vec4 decodeFLOAT_E9B5G5R5(const ImageCodec*, const void* ptr) {
auto data = reinterpret_cast<const uint32_t*>(ptr);
return glm::vec4(glm::unpackF3x9_E1x5(data[0]), 1.f);
}
static glm::vec4 decodeFLOAT_B10G11R11(const ImageCodec*, const void* ptr) {
auto data = reinterpret_cast<const uint32_t*>(ptr);
auto value = glm::unpackF2x11_1x10(data[0]);
// Need to handle NaN and infinity as special cases, because GLM "swallows" them
const uint32_t exponentShifts[] = { 6, 11 + 6, 22 + 5 };
const uint32_t exponentMask = 0x1F;
const uint32_t mantissaShifts[] = { 0, 11, 22 };
const uint32_t mantissaMasks[] = { 0x3F, 0x3F, 0x1F };
for (uint32_t channel = 0; channel < 3; ++channel) {
const uint32_t exponent = (data[0] >> exponentShifts[channel]) & exponentMask;
const uint32_t mantissa = (data[0] >> mantissaShifts[channel]) & mantissaMasks[channel];
if (exponent == 31) {
if (mantissa == 0) {
value[channel] = std::numeric_limits<float>::infinity();
} else {
value[channel] = std::numeric_limits<float>::quiet_NaN();
}
}
}
return glm::vec4(value, 1.f);
}
static glm::uvec4 decodeUINT_D16_S8(const ImageCodec*, const void* ptr) {
auto data = reinterpret_cast<const uint16_t*>(ptr);
return glm::uvec4(data[0], data[1] & 0xFF, 0, 0);
}
static glm::vec4 decodeFLOAT_D16_S8(const ImageCodec*, const void* ptr) {
auto data = reinterpret_cast<const uint16_t*>(ptr);
return glm::vec4(imageio::convertUNORMToFloat(data[0], 16), static_cast<float>(data[1] & 0xFF), 0.f, 1.f);
}
static glm::uvec4 decodeUINT_D24(const ImageCodec*, const void* ptr) {
auto data = reinterpret_cast<const uint32_t*>(ptr);
return glm::uvec4(data[0] & 0xFFFFFF, 0, 0, 0);
}
static glm::vec4 decodeFLOAT_D24(const ImageCodec*, const void* ptr) {
auto data = reinterpret_cast<const uint32_t*>(ptr);
return glm::vec4(imageio::convertUNORMToFloat(data[0] & 0xFFFFFF, 24), 0.f, 0.f, 1.f);
}
static glm::uvec4 decodeUINT_D24_S8(const ImageCodec*, const void* ptr) {
auto data = reinterpret_cast<const uint32_t*>(ptr);
return glm::uvec4(data[0] >> 8, data[0] & 0xFF, 0, 0);
}
static glm::vec4 decodeFLOAT_D24_S8(const ImageCodec*, const void* ptr) {
auto data = reinterpret_cast<const uint32_t*>(ptr);
return glm::vec4(imageio::convertUNORMToFloat(data[0] >> 8, 24), static_cast<float>(data[0] & 0xFF), 0.f, 1.f);
}
static glm::vec4 decodeFLOAT_D32_S8(const ImageCodec*, const void* ptr) {
auto data_float = reinterpret_cast<const float*>(ptr);
auto data_uint = reinterpret_cast<const uint32_t*>(ptr);
return glm::vec4(data_float[0], static_cast<float>(data_uint[1] & 0xFF), 0.f, 1.f);
}
template <int COMPONENTS>
static glm::vec4 decodeFLOAT_FP32Vec(const ImageCodec*, const void* ptr) {
static_assert((COMPONENTS > 0) && (COMPONENTS <= 4));
auto data = reinterpret_cast<const float*>(ptr);
glm::vec4 result(0.f, 0.f, 0.f, 1.f);
for (int i = 0; i < COMPONENTS; ++i)
result[i] = data[i];
return result;
}
template <int COMPONENTS>
static glm::vec4 decodeFLOAT_FP16Vec(const ImageCodec*, const void* ptr) {
static_assert((COMPONENTS > 0) && (COMPONENTS <= 4));
auto data = reinterpret_cast<const uint16_t*>(ptr);
glm::vec4 result(0.f, 0.f, 0.f, 1.f);
for (int i = 0; i < COMPONENTS; ++i)
result[i] = imageio::half_to_float(data[i]);
return result;
}
template <int COMPONENTS>
static glm::vec4 decodeFLOAT_SFIXED5_NV(const ImageCodec*, const void* ptr) {
static_assert((COMPONENTS > 0) && (COMPONENTS <= 4));
auto data = reinterpret_cast<const int16_t*>(ptr);
glm::vec4 result(0.f, 0.f, 0.f, 1.f);
for (int i = 0; i < COMPONENTS; ++i)
result[i] = data[i] / 32.f;
return result;
}
template <typename TYPE, int COMPONENTS>
static glm::uvec4 decodeUINT_UINTVec(const ImageCodec*, const void* ptr) {
static_assert(std::is_unsigned_v<TYPE>);
static_assert((COMPONENTS > 0) && (COMPONENTS <= 4));
auto data = reinterpret_cast<const TYPE*>(ptr);
glm::uvec4 result(0, 0, 0, 0);
for (int i = 0; i < COMPONENTS; ++i)
result[i] = data[i];
return result;
}
template <typename TYPE, int COMPONENTS>
static glm::vec4 decodeFLOAT_UINTVec(const ImageCodec*, const void* ptr) {
static_assert(std::is_unsigned_v<TYPE>);
static_assert((COMPONENTS > 0) && (COMPONENTS <= 4));
auto data = reinterpret_cast<const TYPE*>(ptr);
const auto upper = static_cast<float>((256u << sizeof(TYPE)) - 1u);
glm::vec4 result(0.f, 0.f, 0.f, 1.f);
for (int i = 0; i < COMPONENTS; ++i)
result[i] = static_cast<float>(data[i]) / upper;
return result;
}
template <typename TYPE, int COMPONENTS>
static glm::ivec4 decodeSINT_SINTVec(const ImageCodec*, const void* ptr) {
static_assert(std::is_signed_v<TYPE>);
static_assert((COMPONENTS > 0) && (COMPONENTS <= 4));
auto data = reinterpret_cast<const TYPE*>(ptr);
glm::ivec4 result(0, 0, 0, 0);
for (int i = 0; i < COMPONENTS; ++i)
result[i] = data[i];
return result;
}
template <typename TYPE, int COMPONENTS>
static glm::vec4 decodeFLOAT_SINTVec(const ImageCodec*, const void* ptr) {
static_assert(std::is_signed_v<TYPE>);
static_assert((COMPONENTS > 0) && (COMPONENTS <= 4));
auto data = reinterpret_cast<const TYPE*>(ptr);
const auto upper = static_cast<float>((128u << sizeof(TYPE)) - 1u);
glm::vec4 result(0.f, 0.f, 0.f, 1.f);
for (int i = 0; i < COMPONENTS; ++i)
result[i] = std::max(static_cast<float>(data[i]) / upper, -1.f);
return result;
}
template <typename TYPE>
static glm::uvec4 decodeUINT_UINTPacked(const ImageCodec* codec, const void* ptr) {
static_assert(std::is_unsigned_v<TYPE>);
auto data = reinterpret_cast<const TYPE*>(ptr);
glm::uvec4 result(0, 0, 0, 0);
for (std::size_t i = 0; i < codec->packedSampleInfo.size(); ++i) {
const auto& info = codec->packedSampleInfo[i];
result[static_cast<glm::length_t>(i)] = (data[info.elementIndex] >> info.bitOffset) & ((1u << info.bitLength) - 1u);
}
return result;
}
template <typename TYPE>
static glm::vec4 decodeFLOAT_UINTPacked(const ImageCodec* codec, const void* ptr) {
static_assert(std::is_unsigned_v<TYPE>);
auto data = reinterpret_cast<const TYPE*>(ptr);
glm::vec4 result(0.f, 0.f, 0.f, 1.f);
for (std::size_t i = 0; i < codec->packedSampleInfo.size(); ++i) {
const auto& info = codec->packedSampleInfo[i];
const auto upper = static_cast<float>((1u << info.bitLength) - 1u);
uint32_t rawValue = (data[info.elementIndex] >> info.bitOffset) & ((1u << info.bitLength) - 1u);
result[static_cast<glm::length_t>(i)] = static_cast<float>(rawValue) / upper;
}
return result;
}
template <typename TYPE>
static glm::ivec4 decodeSINT_SINTPacked(const ImageCodec* codec, const void* ptr) {
static_assert(std::is_signed_v<TYPE>);
auto data = reinterpret_cast<const std::make_unsigned_t<TYPE>*>(ptr);
glm::ivec4 result(0, 0, 0, 0);
for (std::size_t i = 0; i < codec->packedSampleInfo.size(); ++i) {
const auto& info = codec->packedSampleInfo[i];
uint32_t rawValue = (data[info.elementIndex] >> info.bitOffset) & ((1u << info.bitLength) - 1u);
result[static_cast<glm::length_t>(i)] = imageio::sign_extend(rawValue, info.bitLength);
}
return result;
}
template <typename TYPE>
static glm::vec4 decodeFLOAT_SINTPacked(const ImageCodec* codec, const void* ptr) {
static_assert(std::is_signed_v<TYPE>);
auto data = reinterpret_cast<const std::make_unsigned_t<TYPE>*>(ptr);
glm::vec4 result(0.f, 0.f, 0.f, 1.f);
for (std::size_t i = 0; i < codec->packedSampleInfo.size(); ++i) {
const auto& info = codec->packedSampleInfo[i];
const auto upper = static_cast<float>((1u << (info.bitLength - 1)) - 1u);
uint32_t rawValue = (data[info.elementIndex] >> info.bitOffset) & ((1u << info.bitLength) - 1u);
result[static_cast<glm::length_t>(i)] = static_cast<float>(imageio::sign_extend(rawValue, info.bitLength)) / upper;
}
return result;
}
};
+354
View File
@@ -0,0 +1,354 @@
// -*- tab-width: 4; -*-
// vi: set sw=2 ts=4 expandtab:
// Copyright 2022 The Khronos Group Inc.
// SPDX-License-Identifier: Apache-2.0
//!
//! @internal
//! @~English
//! @file
//!
//! @brief ImageInput:open function
//!
#include "imageio.h"
#include "imageio_utility.h"
#include "platform_utils.h"
#include <iomanip>
#include <map>
#include <stdarg.h>
#include <stdexcept>
#include <filesystem>
// Search for and instantiate a plugin that can read the format
// and open the file.
std::unique_ptr<ImageInput>
ImageInput::open(const std::string& filename,
const ImageSpec* /*config*/,
WarningCallbackFunction wcb)
//Filesystem::IOProxy* ioproxy, string_view plugin_searchpath)
{
// Populate inputFormats.
if (Imageio::inputFormats.empty()) {
Imageio::catalogBuiltinPlugins();
assert(!Imageio::inputFormats.empty() && "No image input plugins compiled in.");
}
std::ifstream ifs;
std::unique_ptr<std::stringstream> buffer;
ImageInput::Creator createFunction = nullptr;
const std::string* fn;
const std::string sn("stdin");
bool doBuffer = true;
if (filename.compare("-")) {
// Regular file.
// Check file exists, before looking for a suitable plugin.
// MS's STL has `open` overloads that accept wchar_t to handle
// Window's Unicode file names.
ifs.open(std::filesystem::path(DecodeUTF8Path(filename)), std::ios::binary | std::ios::in);
if (ifs.fail()) {
throw std::runtime_error(
fmt::format("Open of \"{}\" failed. {}",
filename, strerror(errno))
);
}
// Extract the file extension from the filename (without the leading dot)
Imageio::string format = filename.substr(filename.find_last_of('.')+1);
format.tolower();
// Look for a plugin for that extension.
if (!format.empty()) {
// First try the plug-in that matches the extension
Imageio::InputPluginMap::const_iterator found
= Imageio::inputFormats.find(format);
if (found != Imageio::inputFormats.end()) {
createFunction = found->second;
}
}
fn = &filename;
} else {
// cin (stdin)
#if defined(_WIN32)
// Set "stdin" to have binary mode. There is no way to this via cin.
(void)_setmode( _fileno( stdin ), _O_BINARY );
// Windows shells set the FILE_SYNCHRONOUS_IO_NONALERT option when
// creating pipes. Cygwin since 3.4.x does the same thing, a change
// which affects anything dependent on it, e.g. Git for Windows
// (since 2.41.0) and MSYS2. When this option is set, cin.seekg(0)
// erroneously returns success. Always buffer.
doBuffer = true;
#else
// Can we seek in this cin?
std::cin.seekg(0);
doBuffer = std::cin.fail();
#endif
if (doBuffer) {
// Can't seek. Buffer stdin. This is a potentially large buffer.
// Must avoid copy. If use stack variable for ss, it's streambuf
// will also be on the stack and lost after this function exits
// even with std::move.
buffer =
std::unique_ptr<std::stringstream>(new std::stringstream);
*buffer << std::cin.rdbuf();
buffer->seekg(0, std::ios::beg);
}
fn = &sn;
}
// Remember which formats we've already tried, so we don't double dip.
std::vector<ImageInput::Creator> formatsTried;
std::string specificError;
std::unique_ptr<ImageInput> in;
if (createFunction) {
formatsTried.push_back(createFunction);
// Try our guess.
try {
in = std::unique_ptr<ImageInput>(createFunction());
in->connectCallback(wcb);
ImageSpec tmpspec;
in->open(*fn, ifs, buffer, tmpspec);
return in;
} catch (const std::runtime_error& e) {
// Oops, it failed. Apparently, this file can't be
// opened with this II.
if (in) {
specificError = e.what();
in.reset();
}
}
}
// A plugin designated for the requested extension either couldn't be
// found or was unable to open the file or there is no requested extension
// due to stdin. Try every plugin and see if one will open the file.
for (auto&& plugin : Imageio::inputFormats) {
// If we already tried this create function, don't do it again
if (std::find(formatsTried.begin(), formatsTried.end(), plugin.second)
!= formatsTried.end()) {
continue;
}
formatsTried.push_back(plugin.second); // remember
createFunction = plugin.second;
try {
in = std::unique_ptr<ImageInput>(createFunction());
in->connectCallback(wcb);
ImageSpec tmpspec;
in->open(*fn, ifs, buffer, tmpspec);
return in;
} catch (...) {
if (in.get()) in.reset();
continue;
}
}
if (!specificError.empty()) {
// Pass along any specific error message we got from our
// best guess of the format.
throw std::runtime_error(specificError);
} else {
throw std::runtime_error(
fmt::format(
"No image plugin recognized the format of \"{}\". "
"Is it a file format that we don't know about?\n",
filename.compare("-") ? filename : "the data on stdin")
);
}
}
/// @brief Open a file for image input.
///
/// Default implementation for derived classes.
void ImageInput::open(const std::string& filename, ImageSpec& newspec)
{
close(); // previously opened file.
if (filename.compare("-")) {
// Regular file
_filename = filename;
file.open(filename);
if (file.fail()) {
throw std::runtime_error(
fmt::format("Open of \"{}\" failed. {}",
filename, strerror(errno))
);
}
isp = &file;
open(newspec);
} else {
// stdin
_filename = "stdin";
#if defined(_WIN32)
/* Set "stdin" to have binary mode */
(void)_setmode( _fileno( stdin ), _O_BINARY );
#endif
// Can we seek in this cin?
std::cin.seekg(0);
if (std::cin.fail()) {
// Can't seek. Buffer stdin. This is a potentially large buffer.
// The class uses a unique_ptr due to the constraints of supporting
// the static create function. See comment in that function above.
buffer =
std::unique_ptr<std::stringstream>(new std::stringstream);
*buffer << std::cin.rdbuf();
isp = buffer.get();
open(newspec);
} else {
open(std::cin, newspec);
}
}
}
/// @brief Read an entire image into contiguous memory performing conversions
/// to @a format.
///
/// Default implementation for derived classes. Input channel values are scaled
/// from the input's channelUpper value to the max representable value of the
/// output format (unorm8 or unorm16).
///
/// Supported conversions are
/// - bit scaling
/// - unorm8<->unorm16
void
ImageInput::readScanline(void* pBufferOut, size_t bufferByteCount,
uint32_t y, uint32_t z,
uint32_t subimage, uint32_t miplevel,
const FormatDescriptor& format)
{
const auto& targetFormat = format.isUnknown() ? spec().format() : format;
const auto targetBitLength = targetFormat.largestChannelBitLength();
const auto requestBits = std::max(imageio::bit_ceil(targetBitLength), 8u);
if (requestBits != 8 && requestBits != 16)
throw std::runtime_error(fmt::format(
"Requested decode into {}-bit format is not supported.",
requestBits)
);
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;
// Only UNORM requests are supported here.
if (targetE || targetL || targetS || targetF)
throw std::runtime_error(fmt::format(
"Requested format conversion to {}-bit{}{}{}{} is not supported.",
requestBits,
targetL ? " Linear" : "",
targetE ? " Exponent" : "",
targetS ? " Signed" : "",
targetF ? " Float" : "")
);
seekSubimage(subimage, miplevel);
size_t imageByteCount = (requestBits * spec().imageChannelCount()) / 8;
if (imageByteCount < bufferByteCount)
throw buffer_too_small();
uint8_t* pNativeBuffer;
if (targetFormat.channelBitLength() != spec().format().channelBitLength()) {
if (spec().format().channelBitLength() == 16) {
if (nativeBuffer16.size() < spec().imageChannelCount())
nativeBuffer16.resize(spec().imageChannelCount());
pNativeBuffer = reinterpret_cast<uint8_t*>(nativeBuffer16.data());
bufferByteCount = nativeBuffer16.size() * sizeof(uint16_t);
} else {
if (nativeBuffer8.size() < spec().imageChannelCount())
nativeBuffer8.resize(spec().imageChannelCount());
pNativeBuffer = nativeBuffer8.data();
bufferByteCount = nativeBuffer16.size() * sizeof(uint8_t);
}
} else {
pNativeBuffer = static_cast<uint8_t*>(pBufferOut);
}
readNativeScanline(pNativeBuffer, bufferByteCount, y, z, subimage, miplevel);
if (reinterpret_cast<uint16_t*>(pNativeBuffer) == nativeBuffer16.data()) {
rescale(static_cast<uint8_t*>(pBufferOut),
static_cast<uint8_t>(targetFormat.channelUpper()),
nativeBuffer16.data(),
static_cast<uint16_t>(spec().format().channelUpper()),
spec().imageChannelCount());
} else if (pNativeBuffer == nativeBuffer8.data()) {
rescale(static_cast<uint16_t*>(pBufferOut),
static_cast<uint16_t>(targetFormat.channelUpper()),
nativeBuffer8.data(),
static_cast<uint8_t>(spec().format().channelUpper()),
spec().imageChannelCount());
} else if (targetFormat.channelUpper() != spec().format().channelUpper()) {
if (spec().format().channelBitLength() == 16) {
rescale(static_cast<uint16_t*>(pBufferOut),
static_cast<uint16_t>(targetFormat.channelUpper()),
static_cast<uint16_t*>(pBufferOut),
static_cast<uint16_t>(spec().format().channelUpper()),
spec().imageChannelCount());
} else {
rescale(static_cast<uint8_t*>(pBufferOut),
static_cast<uint8_t>(targetFormat.channelUpper()),
static_cast<uint8_t*>(pBufferOut),
static_cast<uint8_t>(spec().format().channelUpper()),
spec().imageChannelCount());
}
}
}
/// @brief Read an entire image into contiguous memory performing conversions
/// to @a format.
///
/// Default implementation for derived classes.
///
/// @sa readScanline() for support conversions.
void
ImageInput::readImage(void* pBuffer, size_t bufferByteCount,
uint32_t subimage, uint32_t miplevel,
const FormatDescriptor& format)
{
const auto& targetFormat = format.isUnknown() ? spec().format() : format;
size_t outScanlineByteCount
= targetFormat.pixelByteCount() * spec().width();
if (bufferByteCount < outScanlineByteCount * spec().height())
throw buffer_too_small();
uint8_t* pDst = static_cast<uint8_t*>(pBuffer);
for (uint32_t y = 0; y < spec().height(); y++) {
readScanline(pDst, bufferByteCount,
y, 0, subimage, miplevel, targetFormat);
pDst += outScanlineByteCount;
bufferByteCount -= outScanlineByteCount;
}
}
void ImageInput::throwOnReadFailure()
{
if (isp->eof()) {
throw std::runtime_error("Unexpected end-of-file.");
} else {
throw std::runtime_error(
fmt::format("I/O error reading file: {}", strerror(errno))
);
}
}
void ImageInput::warning(const std::string& wmsg)
{
if (sendWarning) {
sendWarning(wmsg);
}
}
void ImageInput::fwarning(const std::string& wmsg)
{
std::string fwmsg = _filename + ": ";
fwmsg += wmsg;
warning(fwmsg);
}
+118
View File
@@ -0,0 +1,118 @@
// -*- tab-width: 4; -*-
// vi: set sw=2 ts=4 expandtab:
// Copyright 2022 The Khronos Group Inc.
// SPDX-License-Identifier: Apache-2.0
//!
//! @internal
//! @~English
//! @file
//!
//! @brief Create plugin maps.
//!
#include "imageio.h"
#include <algorithm>
#include <cctype>
#include <iomanip>
#include <iostream>
#include <map>
#include <string>
#include <sstream>
#include <stdexcept>
#include <vector>
#include <stdarg.h>
#define PLUGENTRY(name) \
ImageInput* name##InputCreate(); \
ImageOutput* name##OutputCreate(); \
extern const char* name##InputExtensions[]; \
extern const char* name##OutputExtensions[];
#define PLUGENTRY_RO(name) \
ImageInput* name##InputCreate(); \
extern const char* name##InputExtensions[];
#define PLUGENTRY_WO(name) \
ImageOutput* name##OutputCreate(); \
extern const char* name##OutputExtensions[];
PLUGENTRY_RO(exr);
PLUGENTRY_RO(jpeg);
PLUGENTRY_RO(npbm);
PLUGENTRY(png)
namespace Imageio {
// These combine extensions and format names into a single map.
InputPluginMap inputFormats;
OutputPluginMap outputFormats;
void
declareImageioFormat(const std::string& formatname,
ImageInput::Creator inputCreator,
const char** inputExtensions,
ImageOutput::Creator outputCreator,
const char** outputExtensions)
{
if (inputCreator) {
for (const char** e = inputExtensions; e && *e; ++e) {
string ext(*e);
ext.tolower();
if (inputFormats.find(ext) == inputFormats.end()) {
inputFormats[ext] = inputCreator;
}
}
if (inputFormats.find(formatname) == inputFormats.end())
inputFormats[formatname] = inputCreator;
}
if (outputCreator) {
for (const char** e = outputExtensions; e && *e; ++e) {
string ext(*e);
ext.tolower();
if (outputFormats.find(ext) == outputFormats.end()) {
outputFormats[ext] = outputCreator;
}
}
if (outputFormats.find(formatname) == outputFormats.end())
outputFormats[formatname] = outputCreator;
}
}
void
catalogBuiltinPlugins()
{
#define DECLAREPLUG(name) \
declareImageioFormat( \
#name, (ImageInput::Creator)name##InputCreate, \
name##InputExtensions, \
(ImageOutput::Creator)name##OutputCreate, \
name##OutputExtensions)
#define DECLAREPLUG_RO(name) \
declareImageioFormat( \
#name, (ImageInput::Creator)name##InputCreate, \
name##InputExtensions, \
nullptr, nullptr)
#define DECLAREPLUG_WO(name) \
declareImageioFormat( \
nullptr, nullptr, \
#name, (ImageOutput::Creator)name##OutputCreate, \
name##OutputExtensions)
#if !defined(DISABLE_OPENEXR)
DECLAREPLUG_RO (exr);
#endif
#if !defined(DISABLE_JPEG)
DECLAREPLUG_RO(jpeg);
#endif
#if !defined(DISABLE_NPBM)
DECLAREPLUG_RO (npbm);
#endif
#if !defined(DISABLE_PNG)
DECLAREPLUG (png);
#endif
// 'Raw' format is not in the catalog as an explicit API is a better fit
}
} // namespace Imageio
+693
View File
@@ -0,0 +1,693 @@
// -*- tab-width: 4; -*-
// vi: set sw=2 ts=4 expandtab:
// Copyright 2022 The Khronos Group Inc.
// SPDX-License-Identifier: Apache-2.0
//!
//! @internal
//! @~English
//! @file
//!
//! @brief Base classes for image input and output plugins
//!
//! The API for these classes is inspired by that of OpenImageIO. We don't use
//! OIIO because
//! - the total size, with all its dependencies, is 128 Mb. There is no easy
//! way, i.e. via cmake comnfiguration, to omit plugins that are of no
//! interest.
//! - it takes between 40m and 1hr on the CI services to build it and all
//! its dependencies
//! - I have consistently been unable to build the vcpkg version of it for
//! all the platforms we need. vcpkg is the only package manager whose
//! installed products are redistributable.
//!
#pragma once
#include "stdafx.h"
#include <algorithm>
#include <cctype>
#include <iostream>
#include <fstream>
#include <functional>
#include <limits>
#include <map>
#include <string>
#include <sstream>
#include <vector>
#include <memory>
#include <fmt/format.h>
#include <math.h>
#include "formatdesc.h"
using stride_t = int64_t;
const stride_t AutoStride = std::numeric_limits<stride_t>::min();
typedef bool (*ProgressCallback)(void *opaque_data, float portion_done);
class ImageSpec {
public:
struct Origin {
uint8_t x;
uint8_t y;
uint8_t z;
static const uint8_t eLeft = 0;
static const uint8_t eRight = 1;
static const uint8_t eTop = 0;
static const uint8_t eBottom = 1;
static const uint8_t eFront = 0;
static const uint8_t eBack = 1;
static const uint8_t eUnspecified = 0xff;
// This is the most common origin among image file formats, hence
// the no arg constructor sets it. If unspecified, use the 3 arg ctor.
Origin() : x(eLeft), y(eTop), z(eFront) { };
Origin(uint8_t _x, uint8_t _y) : x(_x), y(_y), z(eFront) { }
Origin(uint8_t _x, uint8_t _y, uint8_t _z) : x(_x), y(_y), z(_z) { }
bool unspecified() {
return x == eUnspecified || y == eUnspecified || z == eUnspecified;
}
};
protected:
FormatDescriptor formatDesc;
uint32_t imageWidth; ///< width of the pixel data
uint32_t imageHeight; ///< height of the pixel data
uint32_t imageDepth; ///< depth of pixel data, >1 indicates a "volume"
Origin imageOrigin; ///< logical corner of image that is the first pixel in the data stream
public:
ImageSpec() : imageWidth(0), imageHeight(0),
imageDepth(0), imageOrigin() { }
ImageSpec(uint32_t w, uint32_t h, uint32_t d, FormatDescriptor& formatDesc)
: ImageSpec(w, h, d, Origin(), formatDesc) { }
ImageSpec(uint32_t w, uint32_t h, uint32_t d, Origin&& o,
FormatDescriptor formatDesc)
: formatDesc(std::move(formatDesc)),
imageWidth(w), imageHeight(h), imageDepth(d), imageOrigin(o) { }
ImageSpec(uint32_t w, uint32_t h, uint32_t d,
uint32_t channelCount, uint32_t channelBitCount,
khr_df_sample_datatype_qualifiers_e dt
= static_cast<khr_df_sample_datatype_qualifiers_e>(0),
khr_df_transfer_e t = KHR_DF_TRANSFER_UNSPECIFIED,
khr_df_primaries_e p = KHR_DF_PRIMARIES_BT709,
khr_df_model_e m = KHR_DF_MODEL_RGBSDA,
khr_df_flags_e f = KHR_DF_FLAG_ALPHA_STRAIGHT)
: ImageSpec(w, h, d, Origin(), channelCount,
channelBitCount, dt, t, p, m, f) { }
ImageSpec(uint32_t w, uint32_t h, uint32_t d, Origin&& o,
uint32_t channelCount, uint32_t channelBitCount,
khr_df_sample_datatype_qualifiers_e dt
= static_cast<khr_df_sample_datatype_qualifiers_e>(0),
khr_df_transfer_e t = KHR_DF_TRANSFER_UNSPECIFIED,
khr_df_primaries_e p = KHR_DF_PRIMARIES_BT709,
khr_df_model_e m = KHR_DF_MODEL_RGBSDA,
khr_df_flags_e f = KHR_DF_FLAG_ALPHA_STRAIGHT)
: formatDesc(channelCount, channelBitCount, dt, t, p, m, f),
imageWidth(w), imageHeight(h), imageDepth(d), imageOrigin(o) { }
ImageSpec(uint32_t w, uint32_t h, uint32_t d,
uint32_t channelCount, uint32_t channelBitCount,
uint32_t channelLower, uint32_t channelUpper,
khr_df_sample_datatype_qualifiers_e dt
= static_cast<khr_df_sample_datatype_qualifiers_e>(0),
khr_df_transfer_e t = KHR_DF_TRANSFER_UNSPECIFIED,
khr_df_primaries_e p = KHR_DF_PRIMARIES_BT709,
khr_df_model_e m = KHR_DF_MODEL_RGBSDA,
khr_df_flags_e f = KHR_DF_FLAG_ALPHA_STRAIGHT)
: ImageSpec(w, h, d, Origin(), channelCount,
channelBitCount,channelLower, channelUpper,
dt, t, p, m, f) { }
ImageSpec(uint32_t w, uint32_t h, uint32_t d, Origin&& o,
uint32_t channelCount, uint32_t channelBitCount,
uint32_t channelLower, uint32_t channelUpper,
khr_df_sample_datatype_qualifiers_e dt
= static_cast<khr_df_sample_datatype_qualifiers_e>(0),
khr_df_transfer_e t = KHR_DF_TRANSFER_UNSPECIFIED,
khr_df_primaries_e p = KHR_DF_PRIMARIES_BT709,
khr_df_model_e m = KHR_DF_MODEL_RGBSDA,
khr_df_flags_e f = KHR_DF_FLAG_ALPHA_STRAIGHT)
: formatDesc(channelCount, channelBitCount,
channelLower, channelUpper,
dt, t, p, m, f),
imageWidth(w), imageHeight(h), imageDepth(d), imageOrigin(o) { }
ImageSpec(uint32_t w, uint32_t h, uint32_t d,
uint32_t channelCount, std::vector<uint32_t>& channelBitLengths,
std::vector<khr_df_model_channels_e>& channelTypes,
khr_df_sample_datatype_qualifiers_e dt
= static_cast<khr_df_sample_datatype_qualifiers_e>(0),
khr_df_transfer_e t = KHR_DF_TRANSFER_UNSPECIFIED,
khr_df_primaries_e p = KHR_DF_PRIMARIES_BT709,
khr_df_model_e m = KHR_DF_MODEL_RGBSDA,
khr_df_flags_e f = KHR_DF_FLAG_ALPHA_STRAIGHT)
: ImageSpec(w, h, d, Origin(), channelCount,
channelBitLengths, channelTypes, dt, t, p, m, f) { }
ImageSpec(uint32_t w, uint32_t h, uint32_t d, Origin&& o,
uint32_t channelCount, std::vector<uint32_t>& channelBitLengths,
std::vector<khr_df_model_channels_e>& channelTypes,
khr_df_sample_datatype_qualifiers_e dt
= static_cast<khr_df_sample_datatype_qualifiers_e>(0),
khr_df_transfer_e t = KHR_DF_TRANSFER_UNSPECIFIED,
khr_df_primaries_e p = KHR_DF_PRIMARIES_BT709,
khr_df_model_e m = KHR_DF_MODEL_RGBSDA,
khr_df_flags_e f = KHR_DF_FLAG_ALPHA_STRAIGHT)
: formatDesc(channelCount, channelBitLengths,
channelTypes, dt, t, p, m, f),
imageWidth(w), imageHeight(h), imageDepth(d), imageOrigin(o) { }
FormatDescriptor& format() { return formatDesc; }
const FormatDescriptor& format() const { return formatDesc; }
uint32_t width() const noexcept { return imageWidth; }
uint32_t height() const noexcept { return imageHeight; }
uint32_t depth() const noexcept { return imageDepth; }
const Origin& origin() const noexcept { return imageOrigin; }
void setWidth(uint32_t w) { imageWidth = w; }
void setHeight(uint32_t h) { imageHeight = h; }
void setDepth(uint32_t d) { imageDepth = d; }
void setOrigin(const Origin& o) { imageOrigin = o; }
size_t imagePixelCount() const noexcept {
return depth() * width() * height();
};
size_t imageChannelCount() const noexcept {
return imagePixelCount() * format().channelCount();
};
size_t imageByteCount() const noexcept {
return imagePixelCount() * format().pixelByteCount();
};
size_t scanlineByteCount() const noexcept {
return width() * format().pixelByteCount();
}
size_t scanlineChannelCount() const noexcept {
return width() * format().channelCount();
}
};
constexpr bool operator==(const ImageSpec::Origin& lhs, const ImageSpec::Origin& rhs) {
return lhs.x == rhs.x && lhs.y == rhs.y && lhs.z == rhs.z;
}
constexpr bool operator!=(const ImageSpec::Origin& lhs, const ImageSpec::Origin& rhs) {
return lhs.x != rhs.x || lhs.y != rhs.y || lhs.z != rhs.z;
}
[[nodiscard]] inline std::string toString(const ImageSpec::Origin& o) noexcept {
std::string str;
switch (o.x) {
case ImageSpec::Origin::eLeft: str = "left"; break;
case ImageSpec::Origin::eRight: str = "right"; break;
case ImageSpec::Origin::eUnspecified: str = "unspecified"; break;
default: assert(false && "Invalid origin.x");
}
str += ",";
switch (o.y) {
case ImageSpec::Origin::eTop: str += "top"; break;
case ImageSpec::Origin::eBottom: str += "bottom"; break;
case ImageSpec::Origin::eUnspecified: str += "unspecified"; break;
default: assert(false && "Invalid origin.y");
}
str += ",";
switch (o.z) {
case ImageSpec::Origin::eFront: str += "front"; break;
case ImageSpec::Origin::eBack: str += "back"; break;
case ImageSpec::Origin::eUnspecified: str += "unspecified"; break;
default: assert(false && "Invalid origin.z");
}
return str;
}
typedef std::function<void(const std::string&)> WarningCallbackFunction;
enum class ImageInputFormatType {
png_l,
png_la,
png_rgb,
png_rgba,
exr_uint,
exr_float,
npbm,
jpg,
};
inline const char* toString(ImageInputFormatType type) {
switch (type) {
case ImageInputFormatType::png_l:
return "png_l";
case ImageInputFormatType::png_la:
return "png_la";
case ImageInputFormatType::png_rgb:
return "png_rgb";
case ImageInputFormatType::png_rgba:
return "png_rgba";
case ImageInputFormatType::exr_uint:
return "exr_uint";
case ImageInputFormatType::exr_float:
return "exr_float";
case ImageInputFormatType::npbm:
return "npbm";
case ImageInputFormatType::jpg:
return "jpg";
}
assert(false && "Invalid ImageInputFormatType enum value");
return "<<invalid>>";
}
class ImageInput {
protected:
std::ifstream file;
std::unique_ptr<std::stringstream> buffer;
std::istream* isp = nullptr;
std::string name;
std::string _filename;
std::vector<uint16_t> nativeBuffer16;
std::vector<uint8_t> nativeBuffer8;
struct imageInfo {
ImageSpec spec;
ImageInputFormatType formatType;
size_t filepos;
imageInfo(ImageSpec&& is, ImageInputFormatType formatType, size_t pos = 0)
: spec(is), formatType(formatType), filepos(pos) { }
};
std::vector<imageInfo> images; ///<
uint32_t curSubimage = std::numeric_limits<uint32_t>::max();
uint32_t curMiplevel = std::numeric_limits<uint32_t>::max();
WarningCallbackFunction sendWarning = nullptr;
public:
using unique_ptr = std::unique_ptr<ImageInput>;
/// @brief Create an ImageInput subclass instance that is able to read the
/// given file and open it.
///
/// The `config`, if not nullptr, points to an ImageSpec giving hints,
/// requests, or special instructions. ImageInput implementations are
/// free to not respond to any such requests, so the default
/// implementation is just to ignore `config`.
///
/// `open()` will first try to make an ImageInput corresponding to
/// the format implied by the file extension (for example, `"foo.tif"`
/// will try the TIFF plugin), but if one is not found or if the
/// inferred one does not open the file, every known ImageInput type
/// will be tried until one is found that will open the file.
///
/// @param[in] filename The name of the file to open.
///
/// @param[in] config Optional pointer to an ImageSpec whose metadata
/// contains "configuration hints."
///
/// @returns
/// A `unique_ptr` that will close and free the ImageInput when
/// it exits scope or is reset. The pointer will be empty if the
/// required writer was not able to be created. If the open fails,
/// the `unique_ptr` will be empty. An error can be retrieved by
/// ImageInput::geterror().
static unique_ptr open(const std::string& filename,
const ImageSpec *config=nullptr,
WarningCallbackFunction wcb=nullptr);
//Filesystem::IOProxy* ioproxy = nullptr,
//string_view plugin_searchpath="");
virtual ~ImageInput() { close(); }
// TODO: is config necessary?
virtual void open (const std::string& filename, ImageSpec& newspec);
virtual void open (const std::string& filename, ImageSpec& newspec,
const ImageSpec& /*config*/) {
return open(filename, newspec);
}
virtual void close() {
if (isp == buffer.get()) {
buffer.reset();
} else if (file.is_open()) {
file.close();
}
isp = nullptr;
}
void connectCallback(WarningCallbackFunction wcb) {
sendWarning = wcb;
}
protected:
ImageInput(std::string&& name) : name(name) { }
virtual void open (std::ifstream&& ifs, ImageSpec& newspec) {
file = std::move(ifs);
isp = &file;
open(newspec);
}
virtual void open (std::ifstream& ifs, ImageSpec& newspec,
const ImageSpec& /*config*/) {
return open(ifs, newspec);
}
virtual void open (std::unique_ptr<std::stringstream>&& iss,
ImageSpec& newspec) {
buffer = std::move(iss);
isp = buffer.get();
open(newspec);
}
virtual void open (std::unique_ptr<std::stringstream>&& iss,
ImageSpec& newspec,
const ImageSpec& /*config*/) {
return open(std::move(iss), newspec);
}
virtual void open (std::istream& cin_, ImageSpec& newspec) {
isp = &cin_;
open(newspec);
}
virtual void open (std::istream& cin_, ImageSpec& newspec,
const ImageSpec& /*config*/) {
return open(cin_, newspec);
}
virtual void open(ImageSpec& newspec) = 0;
//virtual void open(ImageSpec& newspec, const ImageSpec& config) = 0;
std::ifstream& getFile() { return file; }
std::unique_ptr<std::stringstream>& getBuffer() { return buffer; }
void throwOnReadFailure();
void warning(const std::string& wmsg);
void fwarning(const std::string& wmsg);
private:
/** @internal
* @brief Open file or stringstream.
*
* This is solely to support the static open(). Subclasses should have no need
* to override.
*
* std::move is used because if the open is successful the class object needs to retain the
* fstream or stringstream that would otherwise disappear when open() exits. The catch
* clauses std::move the stream info back to the caller so it can keep searching for a plugin.
*/
void open(const std::string& filename, std::ifstream& ifs,
std::unique_ptr<std::stringstream>& bufferIn,
ImageSpec& newspec) {
_filename = filename; // Purely so warnings can include the file name.
if (ifs.is_open()) {
try {
open(std::move(ifs), newspec);
} catch (...) {
ifs = std::move(getFile());
ifs.clear();
ifs.seekg(0);
throw;
}
} else if (bufferIn.get() != nullptr) {
try {
open(std::move(bufferIn), newspec);
} catch (...) {
bufferIn = std::move(getBuffer());
bufferIn->clear();
bufferIn.get()->seekg(0);
throw;
}
} else {
try {
open(std::cin, newspec);
} catch (...) {
std::cin.clear();
std::cin.seekg(0);
throw;
}
}
}
public:
virtual const std::string& formatName(void) const { return name; }
virtual const std::string& filename(void) const { return _filename; }
// Return a reference to the ImageSpec of the current image.
// This default method assumes no subimages.
virtual const ImageSpec& spec (void) const {
return images[0].spec;
}
// Return the FormatType of the current image.
// This default method assumes no subimages.
virtual ImageInputFormatType formatType (void) const {
return images[0].formatType;
}
// Return a full copy of the ImageSpec of the designated subimage & level.
// If there is no such subimage and miplevel it returns an ImageSpec
// whose format returns true for isUnknown().
// This default method assumes no subimages.
virtual ImageSpec spec (uint32_t /*subimage*/, uint32_t /*miplevel=0*/) {
ImageSpec ret;
if (curSubimage < images.size()) {
ret = images[curSubimage].spec;
}
return ret;
}
// Return a copy of the ImageSpec but only the dimension and type fields.
// TODO: Determine if this is necessary.
virtual ImageSpec spec_dimensions (uint32_t /*subimage*/, uint32_t /*miplevel=0*/) {
return spec();
}
virtual uint32_t currentSubimage(void) const { return curSubimage; }
virtual uint32_t currentMiplevel(void) const { return curMiplevel; }
virtual uint32_t subimageCount(void) const { return 1; }
virtual uint32_t miplevelCount(void) const { return 1; }
virtual bool seekSubimage(uint32_t subimage, uint32_t miplevel = 0) {
// Default implementation assumes no support for subimages or
// mipmaps, so there is no work to do.
return subimage == currentSubimage() && miplevel == currentMiplevel();
}
/// Read an entire image into contiguous memory performing conversions to
/// @a requestFormat.
///
/// @TODO @a requestFormat allows callers to request almost unlimited
/// possible conversions compared to the original format. The current
/// plug-ins only provide a handful of conversions and those available
/// vary by plug-in. Plug-ins must throw an exception when an
/// unsupported conversion is requested. As a work in progress this is
/// okay but we need to rationalize all this such as
///
/// 1. a subset of all possible conversions supported by every plug-in
/// 2. conversion-specific exceptions so caller can tell what didn't work.
///
/// Commonly supported transformations are bit scaling and changing the
/// channel count, both adding and removing channels. See the derived
/// classes for the specific coversions supported.
///
virtual void readImage(void* buffer, size_t bufferByteCount,
uint32_t subimage = 0, uint32_t miplevel = 0,
const FormatDescriptor& requestFormat = FormatDescriptor());
/// @brief Read a scanline into contiguous memory performing conversions to
/// @a requestFormat.
///
/// Supported conversions in the default implementation are uint->uint for
/// 8- & 16-bit values.
///
/// @sa See readImage for information about handling of requestFormat.
virtual void readScanline(void* buffer, size_t bufferByteCount,
uint32_t y, uint32_t z,
uint32_t subimage, uint32_t miplevel,
const FormatDescriptor& requestFormat = FormatDescriptor());
/// 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 = 0,
uint32_t subimage = 0, uint32_t miplevel = 0) = 0;
template<class Tr, class Tw>
inline static void
rescale(Tw* write, const Tr* read, size_t nvals, Tr max)
{
if (max) {
float multiplier = static_cast<float>(std::numeric_limits<Tw>::max()) / max;
for (size_t i = 0; i < nvals; i++) {
write[i] = static_cast<Tw>(roundf(read[i] * multiplier));
}
}
}
template<class Tr, class Tw>
inline static void
rescale(Tw* write, Tw maxw, const Tr* read, Tr maxr, size_t nvals)
{
if (maxr) {
float multiplier = static_cast<float>(maxw) / maxr;
for (size_t i = 0; i < nvals; i++) {
write[i] = static_cast<Tw>(roundf(read[i] * multiplier));
}
}
}
class different_format : public std::runtime_error {
public:
different_format() : std::runtime_error("") { }
};
class invalid_file : public std::runtime_error {
public:
invalid_file(std::string error)
: std::runtime_error("Invalid file: " + error) { }
};
class buffer_too_small : public std::runtime_error {
public:
buffer_too_small() : std::runtime_error("Image buffer too small.") { }
};
typedef ImageInput* (*Creator)();
};
class ImageOutput {
public:
/// unique_ptr to an ImageOutput.
using unique_ptr = std::unique_ptr<ImageOutput>;
static unique_ptr create (const std::string& name);
protected:
ImageOutput(std::string&& name) : name(name) { }
public:
virtual ~ImageOutput () { };
/// Return the name of the format implemented by this class.
virtual const std::string& formatName(void) const { return name; }
/// Query if feature is supported.
virtual int supports (std::string /*feature*/) const {
return false;
}
/// Modes passed to the `open()` call.
enum OpenMode { Create, AppendSubimage, AppendMIPLevel };
///
/// @param name The name of the image file to open.
/// @param newspec The ImageSpec describing the resolution, data
/// types, etc.
/// @param mode Specifies whether the purpose of the `open` is
/// to create/truncate the file (default: `Create`),
/// append another subimage (`AppendSubimage`), or
/// append another MIP level (`AppendMIPLevel`).
/// @returns `true` upon success, or `false` upon failure.
virtual void open (const std::string& name, const ImageSpec& newspec,
OpenMode mode=Create) = 0;
/// Return a reference to the image format specification of the current
/// subimage. Note that the contents of the spec are invalid before
/// `open()` or after `close()`.
const ImageSpec &spec (void) const { return imageSpec; }
/// Closes the currently open file associated with this ImageOutput and
/// frees any memory or resources associated with it.
virtual void close () = 0;
///
/// @param y/z The y & z coordinates of the scanline.
/// @param format A FormatDescriptor describing @a data.
/// @param data Pointer to the pixel data.
/// @param xstride The distance in bytes between successive
/// pixels in @a data (or `AutoStride`).
virtual void writeScanline (int y, int z, const FormatDescriptor& format,
const void *data, stride_t xstride=AutoStride);
///
/// @param format A FormatDescriptor describing @a data.
/// @param data Pointer to the pixel data.
/// @param xstride/ystride/zstride
/// The distance in bytes between successive pixels,
/// scanlines, and image planes (or `AutoStride`).
/// @param progress_callback/progress_callback_data
/// Optional progress callback.
/// @returns `true` upon success, or `false` upon failure.
virtual void writeImage (const FormatDescriptor& format, const void *data,
stride_t xstride=AutoStride,
stride_t ystride=AutoStride,
stride_t zstride=AutoStride,
ProgressCallback progress_callback=nullptr,
void *progress_callback_data=nullptr);
/// Specify a reduced-resolution ("thumbnail") version of the image.
/// Note that many image formats may require the thumbnail to be
/// specified prior to writing the pixels.
///
//virtual void setThumbnail(const Image& thumb) { return false; }
/// Read the pixels of the current subimage of @a in, and write it as the
/// next subimage of `*this`, in a way that is efficient and does not
/// alter pixel values, if at all possible. Both @a in and `this` must
/// be a properly-opened `ImageInput and `ImageOutput`, respectively,
/// and their current images must match in size and number of channels.
///
/// If a particular ImageOutput implementation does not supply a
/// `copy_image` method, it will inherit the default implementation,
/// which is to simply read scanlines from @a in and write them
/// to `*this`.
///
/// @param in A pointer to the open `ImageInput` to read from.
virtual void copyImage (ImageInput *in);
/// Call signature of a function that creates and returns an
/// `ImageOutput*`.
typedef ImageOutput* (*Creator)();
protected:
ImageSpec imageSpec; ///< format spec of the currently open image
std::string name;
};
namespace Imageio {
class string : public std::string {
public:
using std::string::string;
string tolower() {
std::transform(begin(), end(), begin(),
[](unsigned char c) {
return static_cast<char>(std::tolower(c));
});
return *this;
}
string (const std::string& s) : std::string(s) {}
};
typedef std::map<std::string, ImageInput::Creator> InputPluginMap;
extern InputPluginMap inputFormats;
typedef std::map<std::string, ImageOutput::Creator> OutputPluginMap;
extern OutputPluginMap outputFormats;
void catalogBuiltinPlugins();
}
+302
View File
@@ -0,0 +1,302 @@
// Copyright 2022-2023 The Khronos Group Inc.
// Copyright 2022-2023 RasterGrid Kft.
// SPDX-License-Identifier: Apache-2.0
#pragma once
#include <array>
#include <algorithm>
#ifdef _MSC_VER
#pragma warning(push)
#pragma warning(disable : 4201)
#endif
#include <glm/gtc/packing.hpp>
#ifdef _MSC_VER
#pragma warning(pop)
#endif
// -----------------------------------------------------------------------------
namespace imageio {
// C++20 - std::bit_cast
template <class To, class From>
[[nodiscard]] constexpr inline To bit_cast(const From& src) noexcept {
static_assert(sizeof(To) == sizeof(From));
static_assert(std::is_trivially_copyable_v<From>);
static_assert(std::is_trivially_copyable_v<To>);
static_assert(std::is_trivially_constructible_v<To>);
To dst;
std::memcpy(&dst, &src, sizeof(To));
return dst;
}
// C++20 - std::bit_ceil
template <typename T>
[[nodiscard]] constexpr inline T bit_ceil(T x) noexcept {
x -= 1;
for (uint32_t i = 0; i < sizeof(x) * 8; ++i)
if (1u << i > x)
return 1u << i;
return 0;
}
// --- Half utilities ----------------------------------------------------------
// Based on https://gist.github.com/rygorous/eb3a019b99fdaa9c3064
union FP32 {
uint32_t u;
float f;
struct P {
uint32_t Mantissa : 23;
uint32_t Exponent : 8;
uint32_t Sign : 1;
} p;
};
union FP16 {
uint16_t u;
struct P {
uint16_t Mantissa : 10;
uint16_t Exponent : 5;
uint16_t Sign : 1;
} p;
};
inline float half_to_float(uint16_t value) {
FP16 h;
h.u = value;
static const FP32 magic = {113 << 23};
static const uint32_t shifted_exp = 0x7c00 << 13; // exponent mask after shift
FP32 o;
o.u = (h.u & 0x7fff) << 13; // exponent/mantissa bits
uint32_t exp = shifted_exp & o.u; // just the exponent
o.u += (127 - 15) << 23; // exponent adjust
// handle exponent special cases
if (exp == shifted_exp) // Inf/NaN?
o.u += (128 - 16) << 23; // extra exp adjust
else if (exp == 0) { // Zero/Denormal?
o.u += 1 << 23; // extra exp adjust
o.f -= magic.f; // renormalize
}
o.u |= (h.u & 0x8000) << 16; // sign bit
return o.f;
}
inline uint16_t float_to_half(float value) {
FP32 f;
f.f = value;
FP16 o = {0};
// Based on ISPC reference code (with minor modifications)
if (f.p.Exponent == 0) // Signed zero/denormal (which will underflow)
o.p.Exponent = 0;
else if (f.p.Exponent == 255) { // Inf or NaN (all exponent bits set)
o.p.Exponent = 31;
o.p.Mantissa = f.p.Mantissa ? 0x200 : 0; // NaN->qNaN and Inf->Inf
} else { // Normalized number
// Exponent unbias the single, then bias the halfp
int newexp = f.p.Exponent - 127 + 15;
if (newexp >= 31) // Overflow, return signed infinity
o.p.Exponent = 31;
else if (newexp <= 0) { // Underflow
if ((14 - newexp) <= 24) { // Mantissa might be non-zero
uint32_t mant = f.p.Mantissa | 0x800000; // Hidden 1 bit
o.p.Mantissa = mant >> (14 - newexp);
if ((mant >> (13 - newexp)) & 1) // Check for rounding
o.u++; // Round, might overflow into exp bit, but this is OK
}
} else {
o.p.Exponent = newexp;
o.p.Mantissa = f.p.Mantissa >> 13;
if (f.p.Mantissa & 0x1000) // Check for rounding
o.u++; // Round, might overflow to inf, this is OK
}
}
o.p.Sign = f.p.Sign;
return o.u;
}
// -----------------------------------------------------------------------------
inline int32_t sign_extend(uint32_t value, uint32_t numBits) {
assert(numBits <= 32);
uint32_t signBitMask = 1u << (numBits - 1);
if (value & signBitMask) {
uint32_t signExtendedValue = value | ~(signBitMask - 1);
int32_t result;
std::memcpy(&result, &signExtendedValue, sizeof(result));
return result;
} else {
return static_cast<int32_t>(value);
}
}
template <typename T>
[[nodiscard]] constexpr inline T extract_bits(const void* data, uint32_t offset, uint32_t numBits) {
assert(numBits <= sizeof(T) * 8);
const auto* source = static_cast<const uint8_t*>(data);
std::array<uint8_t, sizeof(T)> target{0};
for (uint32_t i = 0; i < numBits; ++i) {
const auto sourceBitIndex = offset + i;
const auto sourceByteIndex = sourceBitIndex / 8;
const auto sourceBitSubByteIndex = sourceBitIndex % 8;
const auto sourceBitSubByteMask = 1u << sourceBitSubByteIndex;
const auto sourceBitValue = (source[sourceByteIndex] & sourceBitSubByteMask) != 0;
const auto targetBitIndex = i;
const auto targetByteIndex = targetBitIndex / 8;
const auto targetBitSubByteIndex = targetBitIndex % 8;
target[targetByteIndex] |= sourceBitValue ? 1u << targetBitSubByteIndex : 0u;
}
return bit_cast<T>(target);
}
[[nodiscard]] inline uint32_t convertFloatToUNORM(float value, uint32_t numBits) {
assert(numBits > 0 && numBits <= 32);
if (std::isnan(value))
return 0;
if (value < 0.f)
return 0;
if (value > 1.f)
return (1u << numBits) - 1u;
return static_cast<uint32_t>(value * static_cast<float>((1u << numBits) - 1u) + 0.5f);
}
[[nodiscard]] inline float convertSFloatToFloat(uint32_t rawBits, uint32_t numBits) {
assert(numBits == 16 || numBits == 32);
if (numBits == 16)
return half_to_float(static_cast<uint16_t>(rawBits));
if (numBits == 32)
return bit_cast<float>(rawBits);
return 0;
}
[[nodiscard]] inline float convertUFloatToFloat(uint32_t rawBits, uint32_t numBits) {
assert(numBits == 10 || numBits == 11);
if (numBits == 10)
return glm::detail::packed10bitToFloat(rawBits);
else if (numBits == 11)
return glm::detail::packed11bitToFloat(rawBits);
return 0;
}
[[nodiscard]] inline float convertSIntToFloat(uint32_t rawBits, uint32_t numBits) {
assert(numBits > 0 && numBits <= 32);
const auto signBit = (rawBits & 1u << (numBits - 1)) != 0;
const auto valueBits = rawBits & ~(1u << (numBits - 1));
const auto signedValue = static_cast<int32_t>(valueBits) * (signBit ? -1 : 1);
return static_cast<float>(signedValue);
}
[[nodiscard]] inline float convertUIntToFloat(uint32_t rawBits, uint32_t numBits) {
assert(numBits > 0 && numBits <= 32); (void) numBits;
return static_cast<float>(rawBits);
}
[[nodiscard]] inline float convertSNORMToFloat(uint32_t rawBits, uint32_t numBits) {
assert(numBits > 0 && numBits <= 32);
const auto upper = static_cast<float>((1u << (numBits - 1u)) - 1u);
return std::max(static_cast<float>(rawBits) / upper, -1.f);
}
[[nodiscard]] inline float convertUNORMToFloat(uint32_t rawBits, uint32_t numBits) {
assert(numBits > 0 && numBits <= 32);
const auto upper = static_cast<float>((1u << numBits) - 1u);
return static_cast<float>(rawBits) / upper;
}
[[nodiscard]] inline uint32_t convertSFloatToUInt(uint32_t rawBits, uint32_t numBits) {
assert(numBits == 16 || numBits == 32);
if (numBits == 16)
return static_cast<uint32_t>(half_to_float(static_cast<uint16_t>(rawBits)));
if (numBits == 32)
return static_cast<uint32_t>(bit_cast<float>(rawBits));
return 0;
}
[[nodiscard]] inline uint32_t convertUFloatToUInt(uint32_t rawBits, uint32_t numBits) {
assert(numBits == 10 || numBits == 11 || numBits == 14);
(void) rawBits;
(void) numBits;
assert(false && "Not yet implemented");
return 0;
}
[[nodiscard]] inline uint32_t convertSIntToUInt(uint32_t rawBits, uint32_t numBits) {
assert(numBits > 0 && numBits <= 32);
(void) rawBits;
(void) numBits;
assert(false && "Not yet implemented");
return 0;
}
[[nodiscard]] inline uint32_t convertUIntToUInt(uint32_t rawBits, uint32_t numBits) {
assert(numBits > 0 && numBits <= 32);
(void) numBits;
return rawBits;
}
[[nodiscard]] constexpr inline uint32_t convertUNORM(uint32_t rawBits, uint32_t sourceBits, uint32_t targetBits) noexcept {
assert(sourceBits > 0 && sourceBits <= 32);
assert(targetBits > 0 && targetBits <= 32);
rawBits &= (1u << sourceBits) - 1u;
if (targetBits == sourceBits) {
return rawBits;
} else if (targetBits >= sourceBits) {
// Upscale with "left bit replication" to fill in the least significant bits
uint64_t result = 0;
for (uint32_t i = 0; i < targetBits; i += sourceBits)
result |= static_cast<uint64_t>(rawBits) << (targetBits - i) >> sourceBits;
return static_cast<uint32_t>(result);
} else {
// Downscale with rounding: Check the most significant bit that was dropped: 1 -> up, 0 -> down
const auto msDroppedBitIndex = sourceBits - targetBits - 1u;
const auto msDroppedBitValue = rawBits & (1u << msDroppedBitIndex);
if (msDroppedBitValue)
// Min stops the 'overflow' if every targetBit is saturated and we would round up
return std::min((rawBits >> (sourceBits - targetBits)) + 1u, (1u << targetBits) - 1u);
else
return rawBits >> (sourceBits - targetBits);
}
}
[[nodiscard]] constexpr inline uint32_t convertUINT(uint32_t rawBits, uint32_t sourceBits, uint32_t targetBits) noexcept {
assert(sourceBits > 0 && sourceBits <= 32);
assert(targetBits > 0 && targetBits <= 32);
const auto targetValueMask = targetBits == 32 ? std::numeric_limits<uint32_t>::max() : (1u << targetBits) - 1u;
const auto sourceValueMask = sourceBits == 32 ? std::numeric_limits<uint32_t>::max() : (1u << sourceBits) - 1u;
rawBits &= sourceValueMask;
if (targetBits < sourceBits)
rawBits &= targetValueMask;
return rawBits;
}
[[nodiscard]] constexpr inline uint32_t convertSINT(uint32_t rawBits, uint32_t sourceBits, uint32_t targetBits) noexcept {
assert(sourceBits > 1 && sourceBits <= 32);
assert(targetBits > 1 && targetBits <= 32);
const auto sourceSignBitIndex = sourceBits - 1u;
const auto sourceSignMask = 1u << sourceSignBitIndex;
const auto sign = (rawBits & sourceSignMask) != 0;
const auto sourceValueBits = sourceBits - 1u;
const auto sourceValueMask = (1u << sourceValueBits) - 1u;
const auto sourceValue = rawBits & sourceValueMask;
const auto targetSignBitIndex = targetBits - 1u;
const auto targetValueBits = targetBits - 1u;
const auto targetValueMask = (1u << targetValueBits) - 1u;
uint32_t result = 0;
result |= (sign ? 1u : 0u) << targetSignBitIndex;
if (targetBits < sourceBits)
result |= sourceValue & targetValueMask;
else
result |= sourceValue;
return result;
}
} // namespace imageio
+97
View File
@@ -0,0 +1,97 @@
// -*- tab-width: 4; -*-
// vi: set sw=2 ts=4 expandtab:
// Copyright 2022 The Khronos Group Inc.
// SPDX-License-Identifier: Apache-2.0
//!
//! @internal
//! @~English
//! @file
//!
//! @brief ImageOutput class implementation
//!
#include "imageio.h"
#include <algorithm>
#include <cctype>
#include <iomanip>
#include <iostream>
#include <map>
#include <string>
#include <sstream>
#include <stdexcept>
#include <vector>
#include <stdarg.h>
std::unique_ptr<ImageOutput>
ImageOutput::create(const std::string& filename)
{
std::unique_ptr<ImageOutput> out;
if (filename.empty()) { // Can't even guess if no name given
throw std::runtime_error(
"ImageOutput::create() called with no filename"
);
}
// Populate outputFormats.
if (Imageio::outputFormats.empty()) {
Imageio::catalogBuiltinPlugins();
assert(!Imageio::outputFormats.empty()
&& "No image output plugins compiled in.");
}
// Extract the file extension from the filename (without the leading dot)
Imageio::string format = filename.substr(filename.find_last_of('.')+1);
if (format.empty()) {
// If the file had no extension, maybe it was itself the format name
format = filename;
}
format.tolower();
ImageOutput::Creator createFunction = nullptr;
// See if it's already in the table. If not, scan all plugins we can
// find to populate the table.
Imageio::OutputPluginMap::const_iterator found
= Imageio::outputFormats.find(format);
if (found != Imageio::outputFormats.end()) {
createFunction = found->second;
} else {
throw std::runtime_error(
fmt::format("Could not find a format writer for \"{}\". "
"Is it a file format that we don't know about?",
filename));
}
assert(createFunction != nullptr);
out = std::unique_ptr<ImageOutput>(createFunction());
return out;
}
void
ImageOutput::writeScanline(int /*y*/, int /*z*/,
const FormatDescriptor& /*format*/,
const void* /*data*/, stride_t /*xstride*/)
{
}
void
ImageOutput::writeImage(const FormatDescriptor& /*format*/,
const void* /*data*/,
stride_t /*xstride*/,
stride_t /*ystride*/,
stride_t /*zstride*/,
ProgressCallback /*progress_callback*/,
void* /*progress_callback_data*/)
{
}
void
ImageOutput::copyImage(ImageInput* /*in*/)
{
}
+186
View File
@@ -0,0 +1,186 @@
// -*- tab-width: 4; -*-
// vi: set sw=2 ts=4 expandtab:
// Copyright 2023-2024 The Khronos Group Inc.
// Copyright 2023-2024 RasterGrid Kft.
// SPDX-License-Identifier: Apache-2.0
//!
//! @internal
//! @~English
//! @file
//!
//! @brief Internal Image Span container and iterator classes
//!
#pragma once
#include <iterator>
#include "imagecodec.hpp"
class ImageSpan {
public:
template <typename PTR = uint8_t*>
class TexelBlockPtr {
public:
using pointer = PTR;
TexelBlockPtr(PTR ptr, const ImageSpan& span) noexcept : ptr(ptr), span(span) {}
uint32_t getPackedElement(uint32_t index) const { return span.imageCodec().getPackedElement(ptr, index); }
glm::uvec4 decodeUINT() const { return span.imageCodec().decodeUINT(ptr); }
glm::ivec4 decodeSINT() const { return span.imageCodec().decodeSINT(ptr); }
glm::vec4 decodeFLOAT() const { return span.imageCodec().decodeFLOAT(ptr); }
const ImageCodec& imageCodec() const { return span.codec; }
constexpr bool isBlockCompressed() const { return span.imageCodec().isBlockCompressed(); }
constexpr bool isPacked() const { return span.imageCodec().isPacked(); }
constexpr bool isFloat() const { return span.imageCodec().isFloat(); }
constexpr bool isFloatHalf() const { return span.imageCodec().isFloatHalf(); }
constexpr bool isSigned() const { return span.imageCodec().isSigned(); }
constexpr bool isNormalized() const { return span.imageCodec().isNormalized(); }
constexpr bool canDecodeUINT() const { return span.imageCodec().canDecodeUINT(); }
constexpr bool canDecodeSINT() const { return span.imageCodec().canDecodeSINT(); }
constexpr bool canDecodeFLOAT() const { return span.imageCodec().canDecodeFLOAT(); }
glm::uvec4 getTexelBlockDimensions() const { return span.imageCodec().getTexelBlockDimensions(); }
constexpr uint32_t getPackedElementByteSize() const { return span.imageCodec().getPackedElementByteSize(); }
constexpr uint32_t getPackedElementCount() const { return span.imageCodec().getPackedElementCount(); }
constexpr uint32_t getTexelBlockByteSize() const { return span.imageCodec().getTexelBlockByteSize(); }
constexpr uint32_t getChannelCount() const { return span.imageCodec().getChannelCount(); }
std::ptrdiff_t getTexelBlockByteOffset() const {
return ptr - span.data();
}
glm::uvec4 getTexelBlockLocation() const {
const std::ptrdiff_t blockPitch = span.imageCodec().getTexelBlockByteSize();
const std::ptrdiff_t rowPitch = span.getTexelBlockWidth() * blockPitch;
const std::ptrdiff_t slicePitch = span.getTexelBlockHeight() * rowPitch;
glm::uvec4 loc;
auto diff = getTexelBlockByteOffset();
loc.w = 0;
loc.z = static_cast<uint32_t>(diff / slicePitch);
diff = diff % slicePitch;
loc.y = static_cast<uint32_t>(diff / rowPitch);
diff = diff % rowPitch;
loc.x = static_cast<uint32_t>(diff / blockPitch);
return loc;
}
glm::uvec4 getPixelLocation() const {
return getTexelBlockLocation() * span.imageCodec().getTexelBlockDimensions();
}
private:
PTR ptr;
const ImageSpan& span;
};
template <typename TBPTR = TexelBlockPtr<uint8_t*>, bool REVERSE = false>
class Iterator {
public:
using iterator_category = std::random_access_iterator_tag;
using difference_type = std::ptrdiff_t;
using value_type = TBPTR;
using pointer = TBPTR;
using reference = TBPTR;
Iterator& operator+=(difference_type rhs) noexcept { ptr = advance(rhs); return *this; }
Iterator& operator-=(difference_type rhs) noexcept { ptr = advance(-rhs); return *this; }
reference operator*() const { return reference(ptr, span); }
pointer operator->() const { return pointer(ptr, span); }
reference operator[](difference_type rhs) const { return *advance(rhs); }
Iterator& operator++() noexcept { ptr = advance(1); return *this; }
Iterator& operator--() noexcept { ptr = advance(-1); return *this; }
Iterator operator++(int) const noexcept { return Iterator(span, advance(1), stride); }
Iterator operator--(int) const noexcept { return Iterator(span, advance(-1), stride); }
difference_type operator-(const Iterator& rhs) const noexcept { return diff(rhs.ptr); }
Iterator operator+(difference_type rhs) const noexcept { return Iterator(span, advance(rhs), stride); }
Iterator operator-(difference_type rhs) const noexcept { return Iterator(span, advance(-rhs), stride); }
friend Iterator operator+(difference_type lhs, const Iterator& rhs) { return Iterator(rhs.span, rhs.advance(lhs), rhs.stride); }
friend Iterator operator-(difference_type lhs, const Iterator& rhs) { return Iterator(rhs.span, rhs.advance(-lhs), rhs.stride); }
bool operator==(const Iterator& rhs) const noexcept { return (ptr == rhs.ptr) != REVERSE; }
bool operator!=(const Iterator& rhs) const noexcept { return (ptr != rhs.ptr) != REVERSE; }
bool operator>(const Iterator& rhs) const noexcept { return (ptr > rhs.ptr) != REVERSE; }
bool operator<(const Iterator& rhs) const noexcept { return (ptr < rhs.ptr) != REVERSE; }
bool operator>=(const Iterator& rhs) const noexcept { return (ptr >= rhs.ptr) != REVERSE; }
bool operator<=(const Iterator& rhs) const noexcept { return (ptr <= rhs.ptr) != REVERSE; }
private:
const ImageSpan& span;
typename TBPTR::pointer ptr;
const uint32_t stride;
Iterator(const ImageSpan& span, typename TBPTR::pointer ptr, uint32_t stride) noexcept
: span(span), ptr(ptr), stride(stride) {}
typename TBPTR::pointer advance(difference_type diff) const noexcept {
return ptr + (REVERSE ? -1 : +1) * diff * stride;
}
difference_type diff(pointer other) const noexcept {
return (REVERSE ? -1 : +1) * (ptr - other) / stride;
}
friend class ImageSpan;
};
using value_type = TexelBlockPtr<uint8_t*>;
using size_type = std::size_t;
using difference_type = std::ptrdiff_t;
using reference = TexelBlockPtr<uint8_t*>;
using const_reference = TexelBlockPtr<const uint8_t*>;
using pointer = TexelBlockPtr<uint8_t*>;
using const_pointer = TexelBlockPtr<const uint8_t*>;
using iterator = Iterator<TexelBlockPtr<uint8_t*>, false>;
using const_iterator = Iterator<TexelBlockPtr<const uint8_t*>, false>;
using reverse_iterator = Iterator<TexelBlockPtr<uint8_t*>, true>;
using const_reverse_iterator = Iterator<TexelBlockPtr<const uint8_t*>, true>;
ImageSpan(uint32_t width, uint32_t height, uint32_t depth, void* pixels, const ImageCodec& imageCodec)
: texelBlockWidth((width + imageCodec.getTexelBlockDimensions()[0] - 1) / imageCodec.getTexelBlockDimensions()[0]),
texelBlockHeight((height + imageCodec.getTexelBlockDimensions()[1] - 1) / imageCodec.getTexelBlockDimensions()[1]),
texelBlockDepth((depth + imageCodec.getTexelBlockDimensions()[2] - 1) / imageCodec.getTexelBlockDimensions()[2]),
pixels(reinterpret_cast<uint8_t*>(pixels)), codec(imageCodec) {}
const ImageCodec& imageCodec() const { return codec; }
reference at(uint32_t blockX, uint32_t blockY, uint32_t blockZ) {
return reference(pixels + texelBlockByteOffset(blockX, blockY, blockZ), *this);
}
const_reference at(uint32_t blockX, uint32_t blockY, uint32_t blockZ) const {
return const_reference(pixels + texelBlockByteOffset(blockX, blockY, blockZ), *this);
}
iterator begin() noexcept { return iterator(*this, pixels, codec.getTexelBlockByteSize()); }
const_iterator begin() const noexcept { return const_iterator(*this, pixels, codec.getTexelBlockByteSize()); }
const_iterator cbegin() const noexcept { return const_iterator(*this, pixels, codec.getTexelBlockByteSize()); }
iterator end() noexcept { return iterator(*this, pixels, codec.getTexelBlockByteSize()) + size(); }
const_iterator end() const noexcept { return const_iterator(*this, pixels, codec.getTexelBlockByteSize()) + size(); }
const_iterator cend() const noexcept { return const_iterator(*this, pixels, codec.getTexelBlockByteSize()) + size(); }
reverse_iterator rbegin() noexcept { return reverse_iterator(*this, pixels, codec.getTexelBlockByteSize()) - size() + 1; }
const_reverse_iterator rbegin() const noexcept { return const_reverse_iterator(*this, pixels, codec.getTexelBlockByteSize()) - size() + 1; }
const_reverse_iterator crbegin() const noexcept { return const_reverse_iterator(*this, pixels, codec.getTexelBlockByteSize()) - size() + 1; }
reverse_iterator rend() noexcept { return reverse_iterator(*this, pixels , codec.getTexelBlockByteSize()) + 1; }
const_reverse_iterator rend() const noexcept { return const_reverse_iterator(*this, pixels, codec.getTexelBlockByteSize()) + 1; }
const_reverse_iterator crend() const noexcept { return const_reverse_iterator(*this, pixels, codec.getTexelBlockByteSize()) + 1; }
uint8_t* data() noexcept { return pixels; }
const uint8_t* data() const noexcept { return pixels; }
constexpr size_type size() const noexcept { return texelBlockWidth * texelBlockHeight; }
constexpr std::ptrdiff_t byteSize() const { return size() * codec.getTexelBlockByteSize(); }
constexpr uint32_t getTexelBlockWidth() const { return texelBlockWidth; }
constexpr uint32_t getTexelBlockHeight() const { return texelBlockHeight; }
constexpr uint32_t getTexelBlockDepth() const { return texelBlockDepth; }
private:
const uint32_t texelBlockWidth;
const uint32_t texelBlockHeight;
const uint32_t texelBlockDepth;
uint8_t* const pixels;
const ImageCodec& codec;
constexpr std::ptrdiff_t texelBlockByteOffset(uint32_t blockX, uint32_t blockY, uint32_t blockZ) const {
return (blockX + blockY * texelBlockWidth + blockZ * texelBlockWidth * texelBlockHeight) * codec.getTexelBlockByteSize();
}
};
+372
View File
@@ -0,0 +1,372 @@
// -*- tab-width: 4; -*-
// vi: set sw=2 ts=4 expandtab:
// Copyright 2022 The Khronos Group Inc.
// SPDX-License-Identifier: Apache-2.0
/**
* @internal
* @~English
* @file
*
* @brief ImageInput from JPEG format files.
*
* The following has a very useful summary of the metadata in JPEG files and
* its handling.
* https://docs.oracle.com/javase/8/docs/api/javax/imageio/metadata/doc-files/jpeg_metadata.html
* This plug-in currently only handles 1 & 3 component images. 1 component
* is luminance. 3 component is YCbCr which the plug-in converts to RGB.
*
* @author Mark Callow.
*/
#include "imageio.h"
#include <iostream>
#include <sstream>
#include <stdexcept>
#include "encoder/jpgd.h"
using namespace jpgd;
class myjpgdstream : public jpeg_decoder_file_stream {
public:
void open(std::istream* pInputStream) {
isp = pInputStream;
}
int read(uint8_t* pBuf, int max_bytes_to_read, bool* pEOF_flag) {
if (!isp)
return -1;
if (isp->eof())
{
*pEOF_flag = true;
return 0;
}
if (isp->fail())
return -1;
isp->read(reinterpret_cast<char*>(pBuf), max_bytes_to_read);
// eof check must come first as fail bit is also set when there
// aren't enough characters to satisfy the full request.
if (isp->eof())
*pEOF_flag = true;
else if (isp->fail())
return -1;
return static_cast<int>(isp->gcount());
}
void rewind() {
isp->seekg(0);
}
protected:
std::istream* isp = nullptr;
};
#if defined(FORMAT_JPGD_STATUS_AS_INTEGER)
// Adding to a namespace outside its package is not recommended but
// since the decoder is part of Basis Universal ...
namespace jpgd {
int format_as(jpgd_status s) { return fmt::underlying(s); }
}
#else // be more user friendly.
template <> struct fmt::formatter<jpgd_status>: formatter<string_view> {
// parse is inherited from formatter<string_view>.
template <typename FormatContext>
auto format(jpgd_status s, FormatContext& ctx) const -> decltype(ctx.out()) {
string_view name;
switch (s) {
case JPGD_SUCCESS: name = "JPGD_SUCCESS"; break;
case JPGD_FAILED: name = "JPGD_FAILED"; break;
case JPGD_DONE: name = "JPGD_DONE"; break;
case JPGD_BAD_DHT_COUNTS: name = "JPGD_BAD_DHT_COUNTS"; break;
case JPGD_BAD_DHT_INDEX: name = "JPGD_BAD_DHT_INDEX"; break;
case JPGD_BAD_DHT_MARKER: name = "JPGD_BAD_DHT_MARKER"; break;
case JPGD_BAD_DQT_MARKER: name = "JPGD_BAD_DQT_MARKER"; break;
case JPGD_BAD_DQT_TABLE: name = "JPGD_BAD_DQT_TABLE"; break;
case JPGD_BAD_PRECISION: name = "JPGD_BAD_PRECISION"; break;
case JPGD_BAD_HEIGHT: name = "JPGD_BAD_HEIGHT"; break;
case JPGD_BAD_WIDTH: name = "JPGD_BAD_WIDTH"; break;
case JPGD_TOO_MANY_COMPONENTS: name = "JPGD_TOO_MANY_COMPONENTS"; break;
case JPGD_BAD_SOF_LENGTH: name = "JPGD_BAD_SOF_LENGTH"; break;
case JPGD_BAD_VARIABLE_MARKER: name = "JPGD_BAD_VARIABLE_MARKER"; break;
case JPGD_BAD_DRI_LENGTH: name = "JPGD_BAD_DRI_LENGTH"; break;
case JPGD_BAD_SOS_LENGTH: name = "JPGD_BAD_SOS_LENGTH"; break;
case JPGD_BAD_SOS_COMP_ID: name = "JPGD_BAD_SOS_COMP_ID"; break;
case JPGD_W_EXTRA_BYTES_BEFORE_MARKER:
name = "JPGD_W_EXTRA_BYTES_BEFORE_MARKER";
break;
case JPGD_NO_ARITHMITIC_SUPPORT:
name = "JPGD_NO_ARITHMITIC_SUPPORT";
break;
case JPGD_UNEXPECTED_MARKER: name = "JPGD_UNEXPECTED_MARKER"; break;
case JPGD_NOT_JPEG: name = "JPGD_NOT_JPEG"; break;
case JPGD_UNSUPPORTED_MARKER: name = "JPGD_UNSUPPORTED_MARKER"; break;
case JPGD_BAD_DQT_LENGTH: name = "JPGD_BAD_DQT_LENGTH"; break;
case JPGD_TOO_MANY_BLOCKS: name = "JPGD_TOO_MANY_BLOCKS"; break;
case JPGD_UNDEFINED_QUANT_TABLE:
name = "JPGD_UNDEFINED_QUANT_TABLE";
break;
case JPGD_UNDEFINED_HUFF_TABLE: name = "JPGD_UNDEFINED_HUFF_TABLE"; break;
case JPGD_NOT_SINGLE_SCAN: name = "JPGD_NOT_SINGLE_SCAN"; break;
case JPGD_UNSUPPORTED_COLORSPACE:
name = "JPGD_UNSUPPORTED_COLORSPACE";
break;
case JPGD_UNSUPPORTED_SAMP_FACTORS:
name = "JPGD_UNSUPPORTED_SAMP_FACTORS";
break;
case JPGD_DECODE_ERROR: name = "JPGD_DECODE_ERROR"; break;
case JPGD_BAD_RESTART_MARKER: name = "JPGD_BAD_RESTART_MARKER"; break;
case JPGD_BAD_SOS_SPECTRAL: name = "JPGD_BAD_SOS_SPECTRAL"; break;
case JPGD_BAD_SOS_SUCCESSIVE: name = "JPGD_BAD_SOS_SUCCESSIVE"; break;
case JPGD_STREAM_READ: name = "JPGD_STREAM_READ"; break;
case JPGD_NOTENOUGHMEM: name = "JPGD_NOTENOUGHMEM"; break;
case JPGD_TOO_MANY_SCANS: name = "JPGD_TOO_MANY_SCANS"; break;
}
return formatter<string_view>::format(name, ctx);
}
};
#endif
class JpegInput final : public ImageInput {
public:
JpegInput() : ImageInput("jpeg") {}
virtual ~JpegInput() { close(); }
virtual void open(ImageSpec& newspec) override;
virtual void close() override {
decodingBegun = false;
if (pJd) delete pJd;
}
virtual void readImage(void* buffer, size_t bufferByteCount,
uint subimage, uint miplevel,
const FormatDescriptor& targetFormat) override;
virtual void readScanline(void* buffer, size_t bufferByteCount,
uint y, uint z,
uint usubimage, uint miplevel,
const FormatDescriptor& targetFormat) override;
/// Read a single scanline (all channels) of native data into contiguous
/// memory.
virtual void readNativeScanline(void* /*buffer*/,
size_t /*bufferByteCount*/,
uint /*y*/, uint /*z*/,
uint /*subimage*/, uint /*miplevel*/) override { };
protected:
void readHeader();
myjpgdstream jstream;
jpeg_decoder* pJd = nullptr;
uint32_t nextScanline = 0;
bool decodingBegun = false;
};
ImageInput*
jpegInputCreate()
{
return new JpegInput;
}
const char* jpegInputExtensions[] = { "jpg", "jpeg", nullptr };
void
JpegInput::open(ImageSpec& newspec)
{
assert(isp != nullptr && "ImageInput not properly opened");
jstream.open(isp);
readHeader();
newspec = spec();
nextScanline = 0;
}
// This doesn't read the APP0 chunk. Although JFIF specs gamma = 1.0, most
// JPEG files are EXIF so this considers all JPEG files to be sRGB.
void
JpegInput::readHeader()
{
pJd = new jpeg_decoder(&jstream, jpeg_decoder::cFlagLinearChromaFiltering);
jpgd_status errorCode = pJd->get_error_code();
if (errorCode != JPGD_SUCCESS) {
if (errorCode == JPGD_NOT_JPEG) {
throw different_format();
} else if (errorCode == JPGD_NOTENOUGHMEM) {
throw std::runtime_error("JPEG decoder out of memory");
} else {
throw std::runtime_error(
fmt::format("JPEG decode failed: {}", errorCode)
);
}
}
// At this point we cannot use
// - jd.get_bytes_per_pixel
// - jd.get_bytes_per_scan_line
// because the underlying variables are not initialized until
// begin_decoding is called. In any case these are not helpful as they
// return what the decode() method will return not what is in the file.
images.emplace_back(ImageSpec(pJd->get_width(), pJd->get_height(), 1,
ImageSpec::Origin(ImageSpec::Origin::eLeft, ImageSpec::Origin::eTop),
pJd->get_num_components(), 8, // component bit length
static_cast<khr_df_sample_datatype_qualifiers_e>(0),
KHR_DF_TRANSFER_SRGB,
KHR_DF_PRIMARIES_BT709),
ImageInputFormatType::jpg);
}
/// @brief Read an image scanline into contiguous memory performing conversions
/// to @a format.
///
/// Supported conversions are
/// - changing channel count
/// - [GREY,RGB]->[GREY,RGB,RGBA]
/// When reducing to 1 channel it calculates luma for GREY from R,G & B.
/// When increasing from 1 it makes a luminance texture, R=G=B=GREY.
/// ALPHA is set to 1.0 when converting to 4 channels.
/// 2- and 4-channel inputs are not supported.
void
JpegInput::readScanline(void* bufferOut, size_t bufferByteCount, uint y, uint,
uint, uint,
const FormatDescriptor& format)
{
const auto& targetFormat = format.isUnknown() ? spec().format() : format;
const auto requestBits = targetFormat.largestChannelBitLength();
if (requestBits != 8)
throw std::runtime_error(fmt::format(
"Requested decode into {}-bit format is not supported.",
requestBits));
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;
// Only UNORM requests are allowed for JPEG inputs
if (targetE || targetL || targetS || targetF)
throw std::runtime_error(fmt::format(
"Requested format conversion to {}-bit{}{}{}{} is not supported.",
requestBits,
targetL ? " Linear" : "",
targetE ? " Exponent" : "",
targetS ? " Signed" : "",
targetF ? " Float" : ""));
if (y >= spec().height())
y = spec().height() - 1;
if (y != nextScanline)
throw std::runtime_error("Random scanline seeking not yet implemented.");
if (!decodingBegun) {
if (pJd == nullptr)
throw std::runtime_error("No file opened.");
pJd->begin_decoding();
decodingBegun = true;
}
const uint8_t* pScanline = 0;
uint32_t scanlineByteCount;
int decodeStatus =
pJd->decode((const void**)&pScanline, &scanlineByteCount) ;
if (decodeStatus != JPGD_SUCCESS) {
assert(decodeStatus != JPGD_DONE);
// Only other decodeStatus is JPGD_FAILED;
throw std::runtime_error(
fmt::format("JPEG decode failed: {}", pJd->get_error_code())
);
}
const auto targetChannelCount = targetFormat.extended.channelCount;
if (targetChannelCount == 2)
throw std::runtime_error(fmt::format(
"Requested decode into 2 channels is not supported.")
);
uint8_t* pDst = static_cast<uint8_t*>(bufferOut);
uint32_t inputChannelCount = spec().format().extended.channelCount;
// decode() returns a bufferOut of 1 channel (for grayscale input)
// or 4 channels. Despite this decode() does not support 4 channel
// inputs. Nor does it support 2 channel inputs.
// TODO: Extend the following when decode supports 2- and 4-channel inputs.
if ((targetChannelCount == 1 && inputChannelCount == 1)
|| (targetChannelCount == 4 && inputChannelCount == 3))
{
if (bufferByteCount < scanlineByteCount)
throw buffer_too_small();
memcpy(bufferOut, pScanline, scanlineByteCount);
} else if (inputChannelCount == 1) {
if (targetChannelCount == 3) {
if (bufferByteCount < scanlineByteCount * 3)
throw buffer_too_small();
for (uint x = 0; x < spec().width(); x++) {
uint8 luma = pScanline[x];
pDst[0] = luma;
pDst[1] = luma;
pDst[2] = luma;
pDst += 3;
}
} else { // targetChannelCount = 4
if (bufferByteCount < scanlineByteCount * 4)
throw buffer_too_small();
for (uint x = 0; x < spec().width(); x++)
{
uint8 luma = pScanline[x];
pDst[0] = luma;
pDst[1] = luma;
pDst[2] = luma;
pDst[3] = 255;
pDst += 4;
}
}
} else if (inputChannelCount == 3) {
if (targetChannelCount == 1) {
if (bufferByteCount < spec().width())
throw buffer_too_small();
const int YR = 19595, YG = 38470, YB = 7471;
for (uint x = 0; x < spec().width(); x++) {
int r = pScanline[x * 4 + 0];
int g = pScanline[x * 4 + 1];
int b = pScanline[x * 4 + 2];
*pDst++ = static_cast<uint8>((r * YR + g * YG + b * YB + 32768) >> 16);
}
} else { //targetChannelCount = 3
if (bufferByteCount < spec().width() * 3)
throw buffer_too_small();
for (uint x = 0; x < spec().width(); x++) {
pDst[0] = pScanline[x * 4 + 0];
pDst[1] = pScanline[x * 4 + 1];
pDst[2] = pScanline[x * 4 + 2];
pDst += 3;
}
}
}
nextScanline++;
}
/// @brief Read an entire image into contiguous memory performing conversions
/// to @a format.
///
/// @sa readScanline() for supported conversions
void JpegInput::readImage(void* bufferOut, size_t bufferByteCount,
uint subimage, uint miplevel,
const FormatDescriptor& format)
{
pJd->begin_decoding();
decodingBegun = true;
ImageInput::readImage(bufferOut, bufferByteCount,
subimage, miplevel,
format);
}
+460
View File
@@ -0,0 +1,460 @@
// -*- 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 <cstdlib>
#include <fstream>
#include <iterator>
#include <string>
/** @internal
* @~English
* @file
*
* @brief ImageInput from netpbm format files (.pam, .pbm, .pgm or .ppm).
*
* Plain formats (magic numbers 'P1', 'P2' & 'P3') are not supported.
*
* PPM and PGM specify that sample values are encoded with the BT.709 OETF.
* They do not indicate that bt.709 only applies when maxval <= 255 so this
* class always reports OETF as bt.709 for color and grayscale. The
* specifications also say that both sRGB and linear encoding are often used.
* Since there is no metadata to indicate a differing OETF this loader always
* assumes bt.709.
*
* Documentation on the netpbm formats can be found at:
* http://netpbm.sourceforge.net/doc/
*
* @author Mark Callow
*/
class NpbmInput final : public ImageInput {
public:
NpbmInput() : ImageInput("npbm") { }
virtual void open(ImageSpec& newspec) override;
virtual void close() override;
//virtual bool read_native_scanline(uint32_t subimage, uint32_t miplevel, int y, int z,
// void* data) override;
virtual void readImage(void* buffer, size_t bufferByteCount,
uint32_t subimage, uint32_t miplevel,
const FormatDescriptor& targetFormat) 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;
virtual uint32_t subimageCount(void) const override {
return static_cast<uint32_t>(images.size());
}
virtual bool seekSubimage(uint32_t subimage, uint32_t miplevel = 0) override;
virtual const ImageSpec& spec(void) const override {
return images[curSubimage].spec;
}
using ImageInput::spec;
private:
std::string currentLine;
std::istream::iostate exceptionsIn;
size_t pos;
unsigned int curImageScanline;
using ImageInput::rescale;
void readImageHeaders();
void parseAHeader();
enum class filetype {PGM, PPM};
void parseGPHeader(filetype ftype);
void nextLine();
void nextToken();
void skipComments(char comment = '#');
void swap(void* pBuffer, size_t nvals);
};
ImageInput*
npbmInputCreate()
{
return new NpbmInput;
}
const char* npbmInputExtensions[] = { "pam", "pbm", "pgm", "ppm", nullptr };
void
NpbmInput::open(ImageSpec& newspec)
{
assert(isp != nullptr && "istream not initialized");
currentLine = "";
pos = 0;
isp->exceptions(std::istream::failbit | std::istream::badbit | std::istream::eofbit );
readImageHeaders();
seekSubimage(0);
newspec = spec();
}
void
NpbmInput::close() {
isp->exceptions(exceptionsIn);
ImageInput::close();
}
static int
tupleComponentCount(const char* tupleType)
{
if (strcmp(tupleType, "BLACKANDWHITE") == 0)
return 1;
else if (strcmp(tupleType, "GRAYSCALE") == 0)
return 1;
else if (strcmp(tupleType, "GRAYSCALE_ALPHA") == 0)
return 2;
else if (strcmp(tupleType, "RGB") == 0)
return 3;
else if (strcmp(tupleType, "RGB_ALPHA") == 0)
return 4;
else
return -1;
}
inline void
NpbmInput::nextLine()
{
std::getline(*isp, currentLine);
pos = 0;
}
inline void
NpbmInput::nextToken()
{
while (1) {
while (isspace(currentLine[pos]))
pos++;
if (pos != currentLine.size())
break;
else
nextLine();
}
}
inline void
NpbmInput::skipComments(char comment)
{
while (1) {
nextToken();
if (currentLine[pos] == comment)
nextLine();
else
break;
}
}
void
NpbmInput::readImageHeaders()
{
for(;;) {
try {
// MagicNumber
// If not an NPBM file, there may not be a line terminator.
currentLine.resize(3);
isp->read(&currentLine[0], 3);
if (!currentLine.compare("P7\n")) {
parseAHeader();
} else if (!currentLine.compare("P5\n")) {
parseGPHeader(filetype::PGM);
//images.back().spec.colortype = colortype_e::Luminance;
} else if (!currentLine.compare("P6\n")) {
parseGPHeader(filetype::PPM);
} else if (!currentLine.compare("P1\n")
|| !currentLine.compare("P2\n")
|| !currentLine.compare("P3\n")) {
throw std::runtime_error("Plain netpbm formats are not supported.");
} else if (!currentLine.compare("P4\n")) {
throw std::runtime_error(".pbm files are not supported.");
} else {
throw different_format();
}
images.back().filepos = isp->tellg(); // Save image data start pos.
// We've only read the header. Seek to the expected end
// of the image.
isp->seekg(images.back().spec.imageByteCount(), isp->cur);
} catch (const std::istream::failure&) {
throwOnReadFailure();
}
// Check if there is any more data in the file.
try {
isp->peek();
} catch (const std::istream::failure&) {
if (isp->eof()) {
return;
} else {
throwOnReadFailure();
}
}
}
}
/*
* SwapEndian16: Swaps endianness in an array of 16-bit values
*/
static void
swapEndian16(uint16_t* pData16, size_t nvals)
{
for (size_t i = 0; i < nvals; ++i)
{
uint16_t x = *pData16;
*pData16++ = (x << 8) | (x >> 8);
}
}
static const union foo { uint16_t x; uint8_t c; } bar{1};
#define IS_LITTLE_ENDIAN (bar.c)
bool
littleendian(void) noexcept
{
return bar.c;
}
void
NpbmInput::swap(void* pBuffer, size_t nvals)
{
if (spec().format().channelBitLength(KHR_DF_CHANNEL_RGBSDA_R) == 16) {
// If 2 bytes, MSB is first.
if (littleendian()) {
swapEndian16((uint16_t*)pBuffer, nvals);
}
}
}
void
NpbmInput::readNativeScanline(void* bufferOut, size_t bufferByteCount,
uint32_t y, uint32_t z,
uint32_t subimage, uint32_t miplevel)
{
if (isp == nullptr)
throw std::runtime_error("istream not initialized");
if (z > 1)
throw std::runtime_error("npbm does not support 3d images.");
if (bufferByteCount < spec().scanlineByteCount())
throw buffer_too_small();
seekSubimage(subimage, miplevel);
if (y != curImageScanline)
isp->seekg(images[currentSubimage()].filepos + (spec().scanlineByteCount() * y),
isp->beg);
isp->read((char*)bufferOut, spec().scanlineByteCount());
swap(bufferOut, spec().scanlineByteCount());
}
bool
NpbmInput::seekSubimage(uint32_t subimage, uint32_t miplevel)
{
if (subimage == currentSubimage() && miplevel == currentMiplevel())
return true;
if (subimage >= images.size() || miplevel > 0)
return false;
isp->seekg(images[subimage].filepos, isp->beg);
curSubimage = subimage;
curMiplevel = miplevel;
curImageScanline = 0;
return true;
}
///
/// @internal
/// @~English
/// @brief parse the header of a PGM or PPM file.
///
/// @param [in] src pointer to FILE stream to read
/// @param [out] width reference to a var in which to write the image width.
/// @param [out] height reference to a var in which to write the image height
/// @param [out] maxval reference to a var in which to write the maxval.
///
/// @exception invalid_file if there is no width or height, if maxval is not
/// an integer or if maxval is out of range.
///
void
NpbmInput::parseAHeader()
{
#define MAX_TUPLETYPE_SIZE 20
#define xtupletype_sscanf_fmt(ms) tupletype_sscanf_fmt(ms)
#define tupletype_sscanf_fmt(ms) "TUPLTYPE %"#ms"s"
char tupleType[MAX_TUPLETYPE_SIZE+1]; // +1 for terminating NUL.
uint32_t width = 0, height = 0;
unsigned int numFieldsFound = 0;
uint32_t componentCount = 0;
uint32_t tCompCount = 0;
uint32_t maxVal = 0;
for (;;) {
nextLine();
skipComments();
if (currentLine.compare("ENDHDR") == 0)
break;
const char* cur_line_cs = currentLine.c_str();
if (sscanf(cur_line_cs, "HEIGHT %u", &height))
numFieldsFound++;
else if (sscanf(cur_line_cs, "WIDTH %u", &width))
numFieldsFound++;
else if (sscanf(cur_line_cs, "DEPTH %u", &componentCount))
numFieldsFound++;
else if (sscanf(cur_line_cs, "MAXVAL %u", &maxVal))
numFieldsFound++;
else if (sscanf(cur_line_cs, xtupletype_sscanf_fmt(MAX_TUPLETYPE_SIZE),
tupleType))
numFieldsFound++;
};
if (numFieldsFound < 5) {
throw invalid_file("Missing fields in pam header.");
}
if ((tCompCount = tupleComponentCount(tupleType)) < 1) {
throw invalid_file(
fmt::format("Invalid TUPLTYPE: {}.", tupleType)
);
}
if (componentCount < tCompCount) {
throw invalid_file(
fmt::format("Mismatched TUPLTYPE, {}, and DEPTH, {}.",
tupleType, componentCount)
);
}
if (maxVal <= 0 || maxVal >= (1<<16)) {
throw std::runtime_error(
fmt::format("Max color component value must be > 0 && < 65536. "
"It is {}", maxVal)
);
}
images.emplace_back(ImageSpec(width, height, 1, componentCount,
maxVal > 255 ? 16 : 8,
0U, maxVal,
static_cast<khr_df_sample_datatype_qualifiers_e>(0),
KHR_DF_TRANSFER_ITU,
KHR_DF_PRIMARIES_BT709,
tCompCount < 3
? KHR_DF_MODEL_YUVSDA
: KHR_DF_MODEL_RGBSDA),
ImageInputFormatType::npbm);
}
///
/// @internal
/// @~English
/// @brief parse the header of a PGM or PPM file.
///
/// @param [in] src pointer to FILE stream to read
/// @param [out] width reference to a var in which to write the image width.
/// @param [out] height reference to a var in which to write the image height
/// @param [out] maxval reference to a var in which to write the maxval.
///
/// @exception invalid_file if there is no width or height, if maxval is not
/// an integer or if maxval is out of range.
///
void NpbmInput::parseGPHeader(filetype ftype)
{
nextLine();
skipComments();
uint32_t numvals, width, height, maxVal;
numvals = sscanf(currentLine.c_str(), "%u %u",
&width, &height);
if (numvals != 2) {
throw invalid_file("width or height is missing.");
}
if (width <= 0 || height <= 0) {
throw invalid_file("width or height is negative.");
}
nextLine();
skipComments();
numvals = sscanf(currentLine.c_str(), "%d", &maxVal);
if (numvals == 0) {
throw invalid_file("maxval must be an integer.");
}
if (maxVal <= 0 || maxVal >= (1<<16)) {
throw invalid_file("Max color component value must be > 0 && < 65536.");
}
images.emplace_back(ImageSpec(width, height, 1,
ImageSpec::Origin(ImageSpec::Origin::eLeft, ImageSpec::Origin::eTop),
ftype == filetype::PPM ? 3 : 1, 8,
0, maxVal,
static_cast<khr_df_sample_datatype_qualifiers_e>(0),
KHR_DF_TRANSFER_ITU,
KHR_DF_PRIMARIES_BT709,
ftype == filetype::PPM
? KHR_DF_MODEL_RGBSDA
: KHR_DF_MODEL_YUVSDA),
ImageInputFormatType::npbm);
}
/// @brief Read an entire image into contiguous memory performing conversions
/// to @a requestFormat.
///
/// @sa ImageInput::readScanline() for supported conversions.
void
NpbmInput::readImage(void* pBuffer, size_t bufferByteCount,
uint32_t subimage, uint32_t miplevel,
const FormatDescriptor& format)
{
const FormatDescriptor* targetFormat;
if (isp == nullptr)
throw std::runtime_error("No open input stream");
if (bufferByteCount < spec().imageByteCount())
throw buffer_too_small();
if (format.isUnknown())
targetFormat = &spec().format();
else
targetFormat = &format;
if (*targetFormat != spec().format()) {
// Use default function which reads a scanline at a time to avoid
// having to buffer entire image for conversion.
ImageInput::readImage(pBuffer, bufferByteCount,
subimage, miplevel,
format);
return;
}
try {
seekSubimage(subimage, miplevel);
isp->read((char*)pBuffer, spec().imageByteCount());
curImageScanline = spec().height();
swap(pBuffer, spec().imageChannelCount());
} catch (const std::istream::failure&) {
throwOnReadFailure();
}
}
+411
View File
@@ -0,0 +1,411 @@
// -*- tab-width: 4; -*-
// vi: set sw=2 ts=4 expandtab:
// Copyright 2022 The Khronos Group Inc.
// SPDX-License-Identifier: Apache-2.0
/**
* @internal
* @~English
* @file
*
* @brief ImageInput from PNG format files.
*
* @author Mark Callow
*/
#include "imageio.h"
#include <array>
#include <iterator>
#include <sstream>
#include <stdexcept>
#include "imageio_utility.h"
#include "lodepng.h"
#include <KHR/khr_df.h>
#include "dfd.h"
class PngInput final : public ImageInput {
public:
PngInput() : ImageInput("png") {}
virtual ~PngInput() { close(); }
virtual void open(ImageSpec& newspec) override;
virtual void close() override {
decodingBegun = false;
}
virtual void readImage(void* bufferOut, size_t bufferByteCount,
uint32_t subimage, uint32_t miplevel,
const FormatDescriptor& targetFormat) 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 { };
protected:
void readHeader();
void slurp();
std::vector<char> pngBuffer;
lodepng::State state;
void* pIdat;
size_t idatsize;
bool colorConvert = false;
uint32_t nextScanline = 0;
bool decodingBegun = false;
};
ImageInput*
pngInputCreate()
{
return new PngInput;
}
const char* pngInputExtensions[] = { "png", nullptr };
void
PngInput::open(ImageSpec& newspec)
{
assert(isp != nullptr && "ImageInput not properly opened");
readHeader();
newspec = spec();
nextScanline = 0;
}
void PngInput::slurp()
{
size_t pngByteLength;
isp->seekg(0, isp->end);
pngByteLength = isp->tellg();
isp->seekg(0, isp->beg);
pngBuffer.resize(pngByteLength);
isp->read(pngBuffer.data(), pngByteLength);
}
void
PngInput::readHeader()
{
// Unfortunately LoadPNG doesn't believe in stdio. The functions
// we need either read from memory or take a file name. To avoid
// a potentially unnecessary slurp of the whole file check the
// signature ourselves.
uint8_t pngsig[8] = {
0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a
};
uint8_t filesig[sizeof(pngsig)];
isp->read((char *)filesig, sizeof(pngsig));
if (isp->fail())
throwOnReadFailure();
if (memcmp(filesig, pngsig, sizeof(pngsig)))
throw different_format();
// It's a PNG file.
isp->seekg(0, isp->beg);
// Slurp it into memory so we can use lodepng_inspect, to determine
// the data type, and lodepng_chunk_find.
//
// Why no special case for when we've already read the file into a
// stringstream (i.e. buffer.get() == isp)? Because the only way to access
// such data is to call stringstream::str() which makes a copy. So treat
// everything the same. For the same reason we slurp into a vector not
// a stringstream here.
slurp();
unsigned int lodepngError;
uint32_t w, h;
lodepngError = lodepng_decode_chunks(&pIdat, &idatsize, &w, &h, &state,
(const uint8_t*)pngBuffer.data(),
pngBuffer.size());
if (lodepngError) {
throw std::runtime_error(
fmt::format("PNG decode chunks error: {}.",
lodepng_error_text(lodepngError))
);
}
// Tell the decoder to produce the same color type as the file. Exceptions
// to this are made later.
lodepng_color_mode_copy(&state.info_raw, &state.info_png.color);
uint32_t componentCount = 0;
uint32_t bitDepth = state.info_png.color.bitdepth;
// Initialisation here keeps compilers happy in the LCT_MAX_OCTET cases.
khr_df_model_e colorModel = KHR_DF_MODEL_RGBSDA;
switch (state.info_png.color.colortype) {
case LCT_GREY:
if (state.info_png.color.key_defined) {
state.info_raw.colortype = LCT_GREY_ALPHA;
componentCount = 2;
colorModel = KHR_DF_MODEL_YUVSDA;
} else {
componentCount = 1;
colorModel = KHR_DF_MODEL_YUVSDA;
}
break;
case LCT_RGB:
if (state.info_png.color.key_defined) {
state.info_raw.colortype = LCT_RGBA;
componentCount = 4;
colorModel = KHR_DF_MODEL_RGBSDA;
} else {
componentCount = 3;
colorModel = KHR_DF_MODEL_RGBSDA;
}
break;
case LCT_PALETTE: {
// color.key_defined is not set for paletted. tRNS info is written
// directly into the palette. To determine the colortype to expand to
// here we need to check if there is a tRNS chunk.
const unsigned char *pTrnsChunk = nullptr;
// 1st chunk after header
const unsigned char* pFirstChunk = (unsigned char*)&pngBuffer[33];
pTrnsChunk = lodepng_chunk_find_const(pFirstChunk,
(unsigned char*)&pngBuffer[pngBuffer.size()-1],
"tRNS");
if (pTrnsChunk) {
state.info_raw.colortype = LCT_RGBA;
componentCount = 4;
colorModel = KHR_DF_MODEL_RGBSDA;
} else {
state.info_raw.colortype = LCT_RGB;
componentCount = 3;
colorModel = KHR_DF_MODEL_RGBSDA;
}
// There are no paletted texture formats, except an ancient one in
// OpenGL ES 1 & 2 so, rather than complicate the users of imageio
// with handling for them, cause them to be expanded to 8 bits by
// this reader and issue a warning.
if (state.info_png.color.bitdepth < 8) {
bitDepth = 8; // This value will be set in the ImageSpec and
// eventually passed back to readImage().
}
fwarning(fmt::format("Expanding {}-bit paletted image to {}",
state.info_png.color.bitdepth,
state.info_raw.colortype == LCT_RGBA ? "R8G8B8A8" : "R8G8B8"));
}
break;
case LCT_GREY_ALPHA:
componentCount = 2;
colorModel = KHR_DF_MODEL_YUVSDA;
break;
case LCT_RGBA:
colorModel = KHR_DF_MODEL_RGBSDA;
componentCount = 4;
break;
case LCT_MAX_OCTET_VALUE:
break;
}
ImageInputFormatType formatType;
switch (state.info_png.color.colortype) {
case LCT_GREY:
formatType = ImageInputFormatType::png_l;
break;
case LCT_GREY_ALPHA:
formatType = ImageInputFormatType::png_la;
break;
case LCT_RGB:
formatType = ImageInputFormatType::png_rgb;
break;
case LCT_RGBA:
formatType = ImageInputFormatType::png_rgba;
break;
case LCT_PALETTE:
formatType = ImageInputFormatType::png_rgba;
break;
case LCT_MAX_OCTET_VALUE:
break;
}
images.emplace_back(ImageSpec(w, h, 1,
ImageSpec::Origin(ImageSpec::Origin::eLeft, ImageSpec::Origin::eTop),
componentCount,
bitDepth,
static_cast<khr_df_sample_datatype_qualifiers_e>(0),
KHR_DF_TRANSFER_UNSPECIFIED,
// PNG spec. says BT.709 primaries are a
// reasonable default.
KHR_DF_PRIMARIES_BT709,
colorModel),
formatType);
// This is ugly. FIXME:
FormatDescriptor& format = const_cast<FormatDescriptor&>(spec().format());
if (state.info_png.iccp_defined) {
format.setPrimaries(KHR_DF_PRIMARIES_UNSPECIFIED);
format.setTransfer(KHR_DF_TRANSFER_UNSPECIFIED);
format.extended.iccProfile.name = state.info_png.iccp_name;
format.extended.iccProfile.profile.resize(state.info_png.iccp_profile_size);
format.extended.iccProfile.profile.insert(
format.extended.iccProfile.profile.begin(),
state.info_png.iccp_profile,
&state.info_png.iccp_profile[state.info_png.iccp_profile_size]);
if (format.extended.iccProfile.name == "ITUR_2100_PQ_FULL") {
format.setPrimaries(KHR_DF_PRIMARIES_BT2020);
format.setTransfer(KHR_DF_TRANSFER_PQ_EOTF);
}
} else if (state.info_png.srgb_defined) {
// srgb_intent is a guide for the user/application when applying
// a color transform during rendering, especially when
// gamut mapping. It does not affect the meaning or value
// of the image pixels so there is nothing to do here.
format.setTransfer(KHR_DF_TRANSFER_SRGB);
format.setPrimaries(KHR_DF_PRIMARIES_SRGB);
} else if (state.info_png.gama_defined) {
format.setTransfer(KHR_DF_TRANSFER_UNSPECIFIED);
// The value in the gAMA chunk is the exponent of the power curve
// used for encoding the image, i.e. the OETF, * 100000.
format.extended.oeGamma = (float)state.info_png.gama_gamma / 100000;
} else {
format.setTransfer(KHR_DF_TRANSFER_UNSPECIFIED);
}
if (state.info_png.chrm_defined
&& !state.info_png.srgb_defined && !state.info_png.iccp_defined) {
Primaries primaries;
primaries.Rx = (float)state.info_png.chrm_red_x / 100000;
primaries.Ry = (float)state.info_png.chrm_red_y / 100000;
primaries.Gx = (float)state.info_png.chrm_green_x / 100000;
primaries.Gy = (float)state.info_png.chrm_green_y / 100000;
primaries.Bx = (float)state.info_png.chrm_blue_x / 100000;
primaries.By = (float)state.info_png.chrm_blue_y / 100000;
primaries.Wx = (float)state.info_png.chrm_white_x / 100000;
primaries.Wy = (float)state.info_png.chrm_white_y / 100000;
format.setPrimaries(findMapping(&primaries, 0.002f));
}
}
/// @brief Read an entire image into contiguous memory performing conversions
/// to @a format.
///
/// Supported conversions are
/// - bit scaling
/// - unorm\<=8->[unorm8,unorm16]
/// - unorm8<->unorm16
/// - changing channel count
/// - [GREY,GREY_ALPHA,RGB,RGBA]->[GREY,GREY_ALPHA,RGB,RGBA]
/// When reducing to 1 or 2 channels it takes the R channel for GREY.
/// When increasing from 1 or 2 channels it makes a luminance texture,
/// R=G=B=GREY. ALPHA goes to A and vice versa. If none in the source,
/// 1.0 is used.
///
/// If the PNG file has an sBit chunk the normalized results are adjusted
/// accordingly.
void
PngInput::readImage(void* bufferOut, size_t bufferOutByteCount,
uint32_t /*subimage*/, uint32_t /*miplevel*/,
const FormatDescriptor& format)
{
const auto& targetFormat = format.isUnknown() ? spec().format() : format;
const auto channelCount = targetFormat.channelCount();
const auto height = spec().height();
const auto width = spec().width();
const auto targetBitLength = targetFormat.largestChannelBitLength();
const auto requestBits = std::max(imageio::bit_ceil(targetBitLength), 8u);
if (requestBits != 8 && requestBits != 16)
throw std::runtime_error(fmt::format(
"PNG decode error: Requested decode into {}-bit format is not supported.",
requestBits)
);
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;
// Only UNORM requests are allowed for PNG inputs
if (targetE || targetL || targetS || targetF)
throw std::runtime_error(fmt::format(
"PNG decode error: Requested format conversion to {}-bit{}{}{}{} is not supported.",
requestBits,
targetL ? " Linear" : "",
targetE ? " Exponent" : "",
targetS ? " Signed" : "",
targetF ? " Float" : "")
);
state.info_raw.bitdepth = requestBits;
state.info_raw.colortype = [&]{
switch (targetFormat.channelCount()) {
case 1:
return LCT_GREY;
case 2:
return LCT_GREY_ALPHA;
case 3:
return LCT_RGB;
case 4:
return LCT_RGBA;
}
throw std::runtime_error(fmt::format(
"PNG decode error: Requested decode into {} channels is not supported.",
targetFormat.channelCount())
);
}();
auto lodepngError = lodepng_finish_decode(
(unsigned char*)bufferOut,
bufferOutByteCount,
width,
height,
&state,
pIdat,
idatsize);
if (lodepngError)
throw std::runtime_error(fmt::format(
"PNG decode error: {}.", lodepng_error_text(lodepngError)));
// TODO: Detect endianness
// if constexpr (std::endian::native == std::endian::little)
if (requestBits == 16) {
// LodePNG loads 16 bit channels in big endian order
auto* data = (unsigned char*) bufferOut;
for (size_t i = 0; i < bufferOutByteCount; i += 2)
std::swap(*(data + i), *(data + i + 1));
}
if (state.info_png.sbit_defined) {
// Recalculate the UNORM values based on sBit information to ensure best loading/rounding
// result regardless of what the png file's writer saved
std::array<uint32_t, 4> sBits{
state.info_png.sbit_r,
state.info_png.sbit_g,
state.info_png.sbit_b,
state.info_png.sbit_a,
};
// state.info_png reflects the input file not any changes made by
// lodepng_finish_decode, which supports adding and removing the alpha channel
// in 8-bit images. sBits will be zero for added channels. Scan for such.
for (uint32_t c = 0; c < channelCount; ++c) {
if (sBits[c] == 0) // channel added.
sBits[c] = requestBits;
}
for (uint32_t y = 0; y < height; ++y) {
for (uint32_t x = 0; x < width; ++x) {
for (uint32_t c = 0; c < channelCount; ++c) {
const auto index = y * width * channelCount + x * channelCount + c;
if (requestBits == 8) {
auto& value = *(reinterpret_cast<uint8_t*>(bufferOut) + index);
value = static_cast<uint8_t>(imageio::convertUNORM(value >> (8 - sBits[c]), sBits[c], 8));
} else { // requestBits == 16
auto& value = *(reinterpret_cast<uint16_t*>(bufferOut) + index);
value = static_cast<uint16_t>(imageio::convertUNORM(value >> (16 - sBits[c]), sBits[c], 16));
}
}
}
}
}
}
@@ -0,0 +1,51 @@
// -*- tab-width: 4; -*-
// vi: set sw=2 ts=4 expandtab:
// Copyright 2022 The Khronos Group Inc.
// SPDX-License-Identifier: Apache-2.0
/**
* @internal
* @~English
* @file
*
* @brief ImageOutput to PNG format files.
*
* @author Mark Callow
*/
#include "imageio.h"
#include <iterator>
#include <sstream>
#include <stdexcept>
#include "lodepng.h"
#include <KHR/khr_df.h>
#include "dfd.h"
class pngOutput final : public ImageOutput {
public:
pngOutput() : ImageOutput("png") {}
virtual ~pngOutput() { close(); }
virtual void close() override { }
virtual void open(const std::string& name, const ImageSpec& newspec,
OpenMode mode) override;
protected:
};
ImageOutput*
pngOutputCreate()
{
return new pngOutput;
}
const char* pngOutputExtensions[] = { "png", nullptr };
void
pngOutput::open(const std::string& /*name*/, const ImageSpec& /*newspec*/,
OpenMode /*mode*/)
{
}