#pragma region CPL License
/*
Nuclex Native Framework
Copyright (C) 2002-2021 Nuclex Development Labs
This library is free software; you can redistribute it and/or
modify it under the terms of the IBM Common Public License as
published by the IBM Corporation; either version 1.0 of the
License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
IBM Common Public License for more details.
You should have received a copy of the IBM Common Public
License along with this library
*/
#pragma endregion // CPL License
// If the library is compiled as a DLL, this ensures symbols are exported
#define NUCLEX_PIXELS_SOURCE 1
#include "PngBitmapCodec.h"
#if defined(NUCLEX_PIXELS_HAVE_LIBPNG)
#include "Nuclex/Pixels/Storage/VirtualFile.h"
#include "Nuclex/Pixels/Errors/FileFormatError.h"
#include "Nuclex/Pixels/PixelFormats/PixelFormatQuery.h"
#include "Nuclex/Pixels/PixelFormats/PixelFormatConverter.h"
#include "Nuclex/Support/ScopeGuard.h"
#include "LibPngHelpers.h"
namespace {
// ------------------------------------------------------------------------------------------- //
/// Handles an error occuring while a PNG is being read
/// PNG main structure, unused
/// Describes the error that has occurred
///
///
/// libpng is a C library, but its error handling scheme expects this function to never
/// return (either it calls abort() or longjmp()). To allow this, all memory libpng
/// allocates must be tracked in the png_struct and there must be no open ends on
/// the stack when the error handler is called.
///
///
/// This gives us all the guarantees we need to fire a C++ exception right through
/// libpng back to our original call site.
///
///
void handlePngError(::png_struct *png, const char *errorMessage) {
(void)png;
throw Nuclex::Pixels::Errors::FileFormatError(errorMessage);
}
// ------------------------------------------------------------------------------------------- //
/// Handles a warning being issues by libpng
/// PNG main structure, unused
/// Describes the warning, unused
void handlePngWarning(::png_struct *png, const char *warningMessage) {
(void)png;
(void)warningMessage;
}
// ------------------------------------------------------------------------------------------- //
/// Interpolates between a minimum and maximum value
///
/// Type of value that will be interpolated, assumed to be an integer
///
/// Interpolation point between 0.0 .. 1.0
/// Minimum value, will be returned at 0.0
/// Maximum value, will be returned at 1.0
/// The interpolated value
template
TValue lerpInclusive(float t, TValue min, TValue max) {
float interpolated = static_cast(max - min) * t + 0.5f;
return min + static_cast(interpolated);
}
// ------------------------------------------------------------------------------------------- //
} // anonymous namespace
namespace Nuclex { namespace Pixels { namespace Storage { namespace Png {
// ------------------------------------------------------------------------------------------- //
void PngBitmapCodec::Save(
const Bitmap &bitmap, VirtualFile &target,
float compressionEffortHint /* = 0.75f */, float outputQualityHint /* = 0.95f */
) const {
(void)outputQualityHint;
// Allocate the main LibPNG structure. It contains all pointers to user-defined
// functions (IO, error handling and custom chunk processing, etc.)
{
::png_struct *pngWrite = ::png_create_write_struct(
PNG_LIBPNG_VER_STRING, nullptr, nullptr, nullptr
);
if(pngWrite == nullptr) {
throw std::bad_alloc();
}
ON_SCOPE_EXIT {
::png_destroy_write_struct(&pngWrite, nullptr);
};
// Install a custom error handler function that simply throws a C++ exception.
// LibPNG is one of the few C libraries designed to allow exceptions passing through
// because it's based on setjmp()/longjmp().
::png_set_error_fn(pngWrite, nullptr, &handlePngError, &handlePngWarning);
{
// We also need the info structure. This holds all importing informations describing
// the image's dimensions, pixel format, palette, gamma etc.
::png_info *pngInfo = ::png_create_info_struct(pngWrite);
if(pngInfo == nullptr) {
throw std::bad_alloc();
}
ON_SCOPE_EXIT {
::png_destroy_info_struct(pngWrite, &pngInfo);
};
// Install a custom read function. This is used to read data from the virtual
// file. The read environment emulates a file cursor.
PngWriteEnvironment environment(*pngWrite, target);
// Honor the caller's wish for the effort to put into compressing the image file
// If libpng is compiled without this, the unused parameter warning is desirable.
#if defined(PNG_WRITE_CUSTOMIZE_COMPRESSION_SUPPORTED)
{
::png_set_compression_level(
pngWrite,
lerpInclusive(compressionEffortHint, int(0), int(9))
);
}
#endif
const BitmapMemory &memory = bitmap.Access();
// Determine the storage pixel format and the parameters that need to be passed
// to libpng to correctly output a PNG in that pixel format
PixelFormat storagePixelFormat;
int colorType, bitDepth;
{
using Nuclex::Pixels::PixelFormats::PixelFormatQuery;
// We generate either 8 bit or 16 bit PNGs with the criterion that we always
// store the whole channel and data never gets lost.
if(PixelFormatQuery::CountWidestChannelBits(memory.PixelFormat) >= 9) {
bitDepth = 16;
} else {
bitDepth = 8;
}
// PNG files support only 4 color channel combinations that are relevant
// to us. Select the one that doesn't lose data and is the closest to
// the pixel format we're trying to save.
std::size_t channelCount = CountChannels(memory.PixelFormat);
if(PixelFormatQuery::HasAlphaChannel(memory.PixelFormat)) {
if(channelCount == 2) {
colorType = PNG_COLOR_TYPE_GRAY_ALPHA;
if(bitDepth == 8) {
storagePixelFormat = PixelFormat::R8_A8_Unsigned;
} else {
storagePixelFormat = PixelFormat::R16_A16_Unsigned_Native16;
}
} else {
colorType = PNG_COLOR_TYPE_RGB_ALPHA;
if(bitDepth == 8) {
storagePixelFormat = PixelFormat::R8_G8_B8_A8_Unsigned;
} else {
storagePixelFormat = PixelFormat::R16_G16_B16_A16_Unsigned_Native16;
}
}
} else {
if(channelCount == 1) {
colorType = PNG_COLOR_TYPE_GRAY;
if(bitDepth == 8) {
storagePixelFormat = PixelFormat::R8_Unsigned;
} else {
storagePixelFormat = PixelFormat::R16_Unsigned_Native16;
}
} else {
colorType = PNG_COLOR_TYPE_RGB;
if(bitDepth == 8) {
storagePixelFormat = PixelFormat::R8_G8_B8_Unsigned;
} else {
storagePixelFormat = PixelFormat::R16_G16_B16_A16_Unsigned_Native16;
}
}
}
}
// The 'IHDR' chunk (image header) contains vital image metadata like the width, height
// and color depth the image is stored as. We can provide this data easily with
// the informations we gathered above.
::png_set_IHDR(
pngWrite,
pngInfo,
static_cast<::png_uint_32>(memory.Width),
static_cast<::png_uint_32>(memory.Height),
static_cast(bitDepth),
static_cast(colorType),
PNG_INTERLACE_NONE,
PNG_COMPRESSION_TYPE_DEFAULT,
PNG_FILTER_TYPE_DEFAULT
);
// The sBIT chunk is a small 'hint' for the decoder how many bits in each color channel
// were used by the source image. If we save a 10 bit image, LibPNG would force us to
// scale all color channels to 16 bits depth, but we can at least place a hint in
// the image file that remarks that it was upscaled from 10 bit color channels.
#if defined(PNG_sBIT_SUPPORTED)
{
using Nuclex::Pixels::PixelFormats::PixelFormatQuery;
::png_color_8 significantBitCounts;
significantBitCounts.red = static_cast<::png_byte>(
PixelFormatQuery::CountRedBits(memory.PixelFormat).value_or(0)
);
significantBitCounts.green = static_cast<::png_byte>(
PixelFormatQuery::CountGreenBits(memory.PixelFormat).value_or(0)
);
significantBitCounts.blue = static_cast<::png_byte>(
PixelFormatQuery::CountBlueBits(memory.PixelFormat).value_or(0)
);
significantBitCounts.alpha = static_cast<::png_byte>(
PixelFormatQuery::CountAlphaBits(memory.PixelFormat).value_or(0)
);
if(significantBitCounts.red > significantBitCounts.green) {
if(significantBitCounts.blue > significantBitCounts.red) {
significantBitCounts.gray = significantBitCounts.blue;
} else {
significantBitCounts.gray = significantBitCounts.red;
}
} else {
if(significantBitCounts.blue > significantBitCounts.green) {
significantBitCounts.gray = significantBitCounts.blue;
} else {
significantBitCounts.gray = significantBitCounts.green;
}
}
::png_set_sBIT(pngWrite, pngInfo, &significantBitCounts);
}
#endif
// Let LibPNG write the image informations to the file.
::png_write_info(pngWrite, pngInfo);
if constexpr(NUCLEX_PIXELS_LITTLE_ENDIAN) {
if(bitDepth >= 9) {
::png_set_swap(pngWrite);
}
}
// Can we save the image directly from the bitmap's data?
// We can only do this if the bitmap's pixel format is natively supported by LibPNG.
if(storagePixelFormat == memory.PixelFormat) {
// Finally, build an array of row addresses for libpng and use it to load
// the whole image in one call. This minimizes the number of method calls and
// should be the most efficient method to get the pixels into the Bitmap.
{
std::vector<::png_byte *> rowAddresses;
{
rowAddresses.reserve(memory.Height);
std::uint8_t *rowStartPointer = reinterpret_cast(memory.Pixels);
for(std::size_t index = 0; index < memory.Height; ++index) {
rowAddresses.push_back(rowStartPointer);
rowStartPointer += memory.Stride;
}
}
// Save entire bitmap. Error handling via assigned error handler (-> exceptions)
::png_write_image(pngWrite, &rowAddresses[0]);
//::png_write_rows(pngWrite, &rowAddresses[0], memory.Height);
}
} else { // Direct save impossible, need pixel format conversion
using Nuclex::Pixels::PixelFormats::PixelFormatConverter;
// Allocate memory for 1 row (we're converting the pixel format of the image
// row by row, this should yield good performance without wasting megabytes of memory)
std::vector rowBytes(CountRequiredBytes(storagePixelFormat, memory.Width));
std::size_t pngRowByteCount = ::png_get_rowbytes(pngWrite, pngInfo);
if(pngRowByteCount > rowBytes.size()) {
rowBytes.resize(pngRowByteCount);
}
PixelFormatConverter::ConvertRowFunction *convertRow = (
PixelFormatConverter::GetRowConverter(memory.PixelFormat, storagePixelFormat)
);
// Convert each row of the image to the pixel format LibPNG can save and let
// LibPNG buffer or encode it in the new .png file
const std::uint8_t *sourceRowStart = (
reinterpret_cast(memory.Pixels)
);
for(std::size_t rowIndex = 0; rowIndex < memory.Height; ++rowIndex) {
convertRow(
sourceRowStart, // + CountBitsPerPixel(memory.PixelFormat),
rowBytes.data(), // + CountBitsPerPixel(storagePixelFormat),
memory.Width
);
::png_write_row(pngWrite, rowBytes.data());
sourceRowStart += memory.Stride;
}
} // if pixel format conversion needed for save
// We submitted all image pixels to LibPNG, tell it that we're done and to flush
// all output. This guarantees that the IO interface will have received a full
// image file containing all pixels and possible footer bytes.
::png_write_end(pngWrite, nullptr); // nullptr = no PNG info record (comments etc.)
#if defined(PNG_WRITE_FLUSH_SUPPORTED)
//::png_write_flush(pngWrite); // I don't think this is necessary.
#endif
} // pngInfo and pngWriteEnvironment scope
} // pngWrite scope
}
// ------------------------------------------------------------------------------------------- //
}}}} // namespace Nuclex::Pixels::Storage::Png
#endif //defined(NUCLEX_PIXELS_HAVE_LIBPNG)