Add ktx
This commit is contained in:
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user