Add ktx
This commit is contained in:
@@ -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}
|
||||
)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
};
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -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*/)
|
||||
{
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
};
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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(¤tLine[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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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*/)
|
||||
{
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user