Files
how-to-vulkan/ktx/tools/imageio/png.imageio/pnginput.cc
T
2026-06-14 19:09:18 +01:00

412 lines
15 KiB
C++

// -*- 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));
}
}
}
}
}
}