#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 "Nuclex/Pixels/Storage/BitmapSerializer.h"
#include "Nuclex/Pixels/Storage/VirtualFile.h"
#include "Nuclex/Pixels/Storage/BitmapCodec.h"
#include "Nuclex/Pixels/Errors/FileFormatError.h"
#include "Nuclex/Support/Text/StringConverter.h"
// Disable things that have not yet made their way into
// the GitHub repository. If you want these, use the Subversion repo!
#undef NUCLEX_PIXELS_HAVE_OPENEXR
#undef NUCLEX_PIXELS_HAVE_LIBWEBP
#undef NUCLEX_PIXELS_HAVE_LIBAVIF
#if defined(NUCLEX_PIXELS_HAVE_LIBWEBP)
#include "WebP/WebPBitmapCodec.h"
#endif
#if defined(NUCLEX_PIXELS_HAVE_LIBTIFF)
#include "Tiff/TiffBitmapCodec.h"
#endif
#if defined(NUCLEX_PIXELS_HAVE_LIBPNG)
#include "Png/PngBitmapCodec.h"
#endif
#if defined(NUCLEX_PIXELS_HAVE_LIBJPEG)
#include "Jpeg/JpegBitmapCodec.h"
#endif
#if defined(NUCLEX_PIXELS_HAVE_OPENEXR)
#include "Exr/ExrBitmapCodec.h"
#endif
namespace {
// ------------------------------------------------------------------------------------------- //
/// Invalid size marker for the most recent codec indices
constexpr std::size_t InvalidIndex = std::size_t(-1);
// ------------------------------------------------------------------------------------------- //
/// Helper used to pass information through lambda methods
struct FileAndBitmap {
/// File the bitmap serializer has been tasked with reading
public: const Nuclex::Pixels::Storage::VirtualFile *File;
/// Container that receives the loaded bitmap if successful
public: std::optional Bitmap;
/// Bitmap into whih the TryLoad() methods will load the pixels
public: Nuclex::Pixels::Bitmap *TargetBitmap;
};
// ------------------------------------------------------------------------------------------- //
/// Helper used to pass information through lambda methods
struct FileAndBitmapInfo {
/// File the bitmap serializer has been tasked with reading
public: const Nuclex::Pixels::Storage::VirtualFile *File;
/// Information container that will be filled if successful
public: std::optional BitmapInfo;
};
// ------------------------------------------------------------------------------------------- //
} // anonymous namespace
namespace Nuclex { namespace Pixels { namespace Storage {
// ------------------------------------------------------------------------------------------- //
BitmapSerializer::BitmapSerializer() :
mostRecentCodecIndex(InvalidIndex),
secondMostRecentCodecIndex(InvalidIndex) {
#if defined(NUCLEX_PIXELS_HAVE_LIBWEBP)
RegisterCodec(std::make_unique());
#endif
#if defined(NUCLEX_PIXELS_HAVE_LIBTIFF)
RegisterCodec(std::make_unique());
#endif
#if defined(NUCLEX_PIXELS_HAVE_LIBPNG)
RegisterCodec(std::make_unique());
#endif
#if defined(NUCLEX_PIXELS_HAVE_LIBJPEG)
RegisterCodec(std::make_unique());
#endif
#if defined(NUCLEX_PIXELS_HAVE_OPENEXR)
RegisterCodec(std::make_unique());
#endif
}
// ------------------------------------------------------------------------------------------- //
BitmapSerializer::~BitmapSerializer() {}
// ------------------------------------------------------------------------------------------- //
void BitmapSerializer::RegisterCodec(std::unique_ptr &&codec) {
std::size_t codecCount = this->codecs.size();
// This should be a one-liner, but clang has a nonsensical warning then typeid()
// is called with an expression that needs to be evaluated at runtime :-(
const BitmapCodec &newCodec = *codec.get();
const std::type_info &newType = typeid(newCodec);
// Make sure this exact type isn't registered yet
for(std::size_t index = 0; index < codecCount; ++index) {
const BitmapCodec &checkedCodec = *this->codecs[index].get();
const std::type_info &existingType = typeid(checkedCodec);
if(newType == existingType) {
throw std::runtime_error(u8"Codec already registered");
}
}
const std::vector &extensions = codec->GetFileExtensions();
// Register the new codec into our list
this->codecs.push_back(std::move(codec));
// Update the extension lookup map for quick codec finding
std::size_t extensionCount = extensions.size();
for(std::size_t index = 0; index < extensionCount; ++index) {
using Nuclex::Support::Text::StringConverter;
const std::string &extension = extensions[index];
std::string::size_type extensionLength = extension.length();
if(extensionLength > 0) {
if(extension[0] == '.') {
if(extensionLength > 1) {
std::string lowerExtension = StringConverter::FoldedLowercaseFromUtf8(
extension.substr(1)
);
this->codecsByExtension.insert(
ExtensionCodecIndexMap::value_type(lowerExtension, codecCount)
);
}
} else {
std::string lowerExtension = StringConverter::FoldedLowercaseFromUtf8(extension);
this->codecsByExtension.insert(
ExtensionCodecIndexMap::value_type(lowerExtension, codecCount)
);
}
}
}
}
// ------------------------------------------------------------------------------------------- //
std::optional BitmapSerializer::TryReadInfo(
const VirtualFile &file, const std::string &extensionHint /* = std::string() */
) const {
FileAndBitmapInfo fileProvider;
fileProvider.File = &file;
bool wasLoaded = tryCodecsInOptimalOrder(
extensionHint,
[](const BitmapCodec &codec, const std::string &extension, FileAndBitmapInfo &fileAndBitmap) {
fileAndBitmap.BitmapInfo = std::move(
codec.TryReadInfo(*fileAndBitmap.File, extension)
);
return fileAndBitmap.BitmapInfo.has_value();
},
fileProvider
);
if(wasLoaded) {
return fileProvider.BitmapInfo;
} else {
return std::optional();
}
}
// ------------------------------------------------------------------------------------------- //
std::optional BitmapSerializer::TryReadInfo(const std::string &path) const {
std::string::size_type extensionDotIndex = path.find_last_of('.');
#if defined(NUCLEX_PIXELS_WINDOWS)
std::string::size_type lastPathSeparatorIndex = path.find_last_of('\\');
#else
std::string::size_type lastPathSeparatorIndex = path.find_last_of('/');
#endif
// Check if the provided path contains a file extension and if so, pass it along to
// the CanLoad() method as a hint (this speeds up codec search)
if(extensionDotIndex != std::string::npos) {
bool dotBelongsToFilename = (
(lastPathSeparatorIndex == std::string::npos) ||
(extensionDotIndex > lastPathSeparatorIndex)
);
if(dotBelongsToFilename) {
std::unique_ptr file = VirtualFile::OpenRealFileForReading(path, true);
return TryReadInfo(*file.get(), path.substr(extensionDotIndex + 1));
}
}
// The specified file has no extension, so do not provide the extension hint
{
std::unique_ptr file = VirtualFile::OpenRealFileForReading(path, true);
return TryReadInfo(*file.get());
}
}
// ------------------------------------------------------------------------------------------- //
bool BitmapSerializer::CanLoad(
const VirtualFile &file, const std::string &extensionHint /* = std::string() */
) const {
FileAndBitmap fileProvider;
fileProvider.File = &file;
return tryCodecsInOptimalOrder(
extensionHint,
[](const BitmapCodec &codec, const std::string &extension, FileAndBitmap &fileAndBitmap) {
return codec.CanLoad(*fileAndBitmap.File, extension);
},
fileProvider
);
}
// ------------------------------------------------------------------------------------------- //
bool BitmapSerializer::CanLoad(const std::string &path) const {
std::string::size_type extensionDotIndex = path.find_last_of('.');
#if defined(NUCLEX_PIXELS_WINDOWS)
std::string::size_type lastPathSeparatorIndex = path.find_last_of('\\');
#else
std::string::size_type lastPathSeparatorIndex = path.find_last_of('/');
#endif
// Check if the provided path contains a file extension and if so, pass it along to
// the CanLoad() method as a hint (this speeds up codec search)
if(extensionDotIndex != std::string::npos) {
bool dotBelongsToFilename = (
(lastPathSeparatorIndex == std::string::npos) ||
(extensionDotIndex > lastPathSeparatorIndex)
);
if(dotBelongsToFilename) {
std::unique_ptr file = VirtualFile::OpenRealFileForReading(path, true);
return CanLoad(*file.get(), path.substr(extensionDotIndex + 1));
}
}
// The specified file has no extension, so do not provide the extension hint
{
std::unique_ptr file = VirtualFile::OpenRealFileForReading(path, true);
return CanLoad(*file.get());
}
}
// ------------------------------------------------------------------------------------------- //
Bitmap BitmapSerializer::Load(
const VirtualFile &file, const std::string &extensionHint /* = std::string() */
) const {
FileAndBitmap fileProvider;
fileProvider.File = &file;
bool wasLoaded = tryCodecsInOptimalOrder(
extensionHint,
[](const BitmapCodec &codec, const std::string &extension, FileAndBitmap &fileAndBitmap) {
std::optional loadedBitmap = codec.TryLoad(*fileAndBitmap.File, extension);
if(loadedBitmap.has_value()) {
fileAndBitmap.Bitmap = std::move(loadedBitmap);
return true;
} else {
return false;
}
},
fileProvider
);
if(wasLoaded) {
return fileProvider.Bitmap.value();
} else {
throw Errors::FileFormatError(u8"File format not supported by any registered codec");
}
}
// ------------------------------------------------------------------------------------------- //
Bitmap BitmapSerializer::Load(const std::string &path) const {
std::string::size_type extensionDotIndex = path.find_last_of('.');
#if defined(NUCLEX_PIXELS_WINDOWS)
std::string::size_type lastPathSeparatorIndex = path.find_last_of('\\');
#else
std::string::size_type lastPathSeparatorIndex = path.find_last_of('/');
#endif
// Check if the provided path contains a file extension and if so, pass it along to
// the CanLoad() method as a hint (this speeds up codec search)
if(extensionDotIndex != std::string::npos) {
bool dotBelongsToFilename = (
(lastPathSeparatorIndex == std::string::npos) ||
(extensionDotIndex > lastPathSeparatorIndex)
);
if(dotBelongsToFilename) {
std::unique_ptr file = VirtualFile::OpenRealFileForReading(path, true);
return Load(*file.get(), path.substr(extensionDotIndex + 1));
}
}
// The specified file has no extension, so do not provide the extension hint
{
std::unique_ptr file = VirtualFile::OpenRealFileForReading(path, true);
return Load(*file.get());
}
}
// ------------------------------------------------------------------------------------------- //
void BitmapSerializer::Reload(
Bitmap &exactFittingBitmap,
const VirtualFile &file, const std::string &extensionHint /* = std::string() */
) const {
FileAndBitmap fileProvider;
fileProvider.File = &file;
fileProvider.TargetBitmap = &exactFittingBitmap;
bool wasLoaded = tryCodecsInOptimalOrder(
extensionHint,
[](const BitmapCodec &codec, const std::string &extension, FileAndBitmap &fileAndBitmap) {
if(codec.TryReload(*fileAndBitmap.TargetBitmap, *fileAndBitmap.File, extension)) {
return true;
} else {
return false;
}
},
fileProvider
);
if(!wasLoaded) {
throw Errors::FileFormatError("File format not supported by any registered codec");
}
}
// ------------------------------------------------------------------------------------------- //
void BitmapSerializer::Reload(Bitmap &exactFittingBitmap, const std::string &path) const {
std::string::size_type extensionDotIndex = path.find_last_of('.');
#if defined(NUCLEX_PIXELS_WINDOWS)
std::string::size_type lastPathSeparatorIndex = path.find_last_of('\\');
#else
std::string::size_type lastPathSeparatorIndex = path.find_last_of('/');
#endif
// Check if the provided path contains a file extension and if so, pass it along to
// the CanLoad() method as a hint (this speeds up codec search)
if(extensionDotIndex != std::string::npos) {
bool dotBelongsToFilename = (
(lastPathSeparatorIndex == std::string::npos) ||
(extensionDotIndex > lastPathSeparatorIndex)
);
if(dotBelongsToFilename) {
std::unique_ptr file = VirtualFile::OpenRealFileForReading(path, true);
Reload(exactFittingBitmap, *file.get(), path.substr(extensionDotIndex + 1));
return;
}
}
// The specified file has no extension, so do not provide the extension hint
{
std::unique_ptr file = VirtualFile::OpenRealFileForReading(path, true);
Reload(exactFittingBitmap, *file.get());
return;
}
}
// ------------------------------------------------------------------------------------------- //
void BitmapSerializer::Save(
const Bitmap &bitmap, VirtualFile &file, const std::string &extension,
float compressionEffortHint /* = 0.75f */, float outputQualityHint /* = 0.95f */
) const {
const BitmapCodec &codec = getSavingCodecForExtension(extension);
codec.Save(bitmap, file, compressionEffortHint, outputQualityHint);
}
// ------------------------------------------------------------------------------------------- //
void BitmapSerializer::Save(
const Bitmap &bitmap, const std::string &path,
const std::string &extension /* = std::string() */,
float compressionEffortHint /* = 0.75f */, float outputQualityHint /* = 0.95f */
) const {
// Only try to pick the extension out of the specified path if the caller hasn't
// explicitly filled the extension parameter
if(extension.empty()) {
std::string::size_type extensionDotIndex = path.find_last_of('.');
#if defined(NUCLEX_PIXELS_WINDOWS)
std::string::size_type lastPathSeparatorIndex = path.find_last_of('\\');
#else
std::string::size_type lastPathSeparatorIndex = path.find_last_of('/');
#endif
// Check if the provided path contains a file extension and if so, use it to select
// the bitmap codec to attempt to save the image file with.
if(extensionDotIndex != std::string::npos) {
bool dotBelongsToFilename = (
(lastPathSeparatorIndex == std::string::npos) ||
(extensionDotIndex > lastPathSeparatorIndex)
);
if(dotBelongsToFilename) {
std::string detectedExtension = path.substr(extensionDotIndex + 1);
const BitmapCodec &codec = getSavingCodecForExtension(detectedExtension);
std::unique_ptr file = VirtualFile::OpenRealFileForWriting(path, true);
codec.Save(bitmap, *file.get(), compressionEffortHint, outputQualityHint);
return;
}
}
// No explicit file extension specified and the target path didn't have one either,
// so we've got no clue which image file format the caller wants us to use...
std::string message(u8"Could not deduce saved image file format from file extension '", 62);
message.append(path);
message.push_back(u8'\'');
throw Errors::FileFormatError(message);
}
// Extension was specified explicitly, look it up
{
const BitmapCodec &codec = getSavingCodecForExtension(extension);
std::unique_ptr file = VirtualFile::OpenRealFileForWriting(path, true);
codec.Save(bitmap, *file.get(), compressionEffortHint, outputQualityHint);
}
}
// ------------------------------------------------------------------------------------------- //
const BitmapCodec &BitmapSerializer::getSavingCodecForExtension(
const std::string &extension
) const {
using Nuclex::Support::Text::StringConverter;
// Do a lookup for the codec responsible for the specified extension
std::string foldedLowercaseExtension = StringConverter::FoldedLowercaseFromUtf8(extension);
ExtensionCodecIndexMap::const_iterator iterator = this->codecsByExtension.find(
foldedLowercaseExtension
);
// If no registered codec is associated with the specified extension, we fail.
// Since this method is used when saving, we can use a tailored error message.
if(iterator == this->codecsByExtension.end()) {
std::string message(u8"No codec registered to save image file with extension '", 55);
message.append(extension);
message.push_back(u8'\'');
throw Errors::FileFormatError(message);
}
// If a codec has been registered to this file extension, it might still be that
// the codec is a read-only implementation of the file format, so check this.
const BitmapCodec &codec = *this->codecs[iterator->second].get();
if(!codec.CanSave()) {
std::string message(u8"Codec '", 7);
message.append(codec.GetName());
message.append(u8"' does not support image saving");
throw Errors::FileFormatError(message);
}
return codec;
}
// ------------------------------------------------------------------------------------------- //
template
bool BitmapSerializer::tryCodecsInOptimalOrder(
const std::string &extension,
bool (*tryCodecCallback)(
const BitmapCodec &codec, const std::string &extension, TOutput &result
),
TOutput &result
) const {
std::size_t hintCodecIndex;
// If an extension hint was provided, try the codec registered for the extension first
if(extension.empty()) {
hintCodecIndex = InvalidIndex;
} else {
using Nuclex::Support::Text::StringConverter;
std::string foldedLowercaseExtension = StringConverter::FoldedLowercaseFromUtf8(extension);
ExtensionCodecIndexMap::const_iterator iterator = (
this->codecsByExtension.find(foldedLowercaseExtension)
);
if(iterator == this->codecsByExtension.end()) {
hintCodecIndex = InvalidIndex;
} else {
hintCodecIndex = iterator->second;
if(tryCodecCallback(*this->codecs[hintCodecIndex].get(), extension, result)) {
updateMostRecentCodecIndex(hintCodecIndex);
return true;
}
}
}
// Look up the two most recently used codecs (we don't care about race conditions here,
// in the rare case of one occurring, we'll simple be a little less efficient and not
// have the right codec in the MRU list...
std::size_t mostRecent = this->mostRecentCodecIndex;
std::size_t secondMostRecent = this->secondMostRecentCodecIndex;
// Try the most recently used codec. It may be set to 'InvalidIndex' if this
// is the first call to Load(). Don't try if it's the same as the extension hint.
if((mostRecent != InvalidIndex) && (mostRecent != hintCodecIndex)) {
if(tryCodecCallback(*this->codecs[mostRecentCodecIndex].get(), extension, result)) {
updateMostRecentCodecIndex(mostRecent);
return true;
}
}
// Try the second most recently used logic. It, too, may be set to 'InvalidIndex'.
// Also avoid retrying codecs we already tried.
if(
(secondMostRecent != InvalidIndex) &&
(secondMostRecent != mostRecent) &&
(secondMostRecent != hintCodecIndex)
) {
if(tryCodecCallback(*this->codecs[secondMostRecent].get(), extension, result)) {
updateMostRecentCodecIndex(secondMostRecent);
return true;
}
}
// Hint was not provided or wrong, most recently used codecs didn't work,
// so go through all remaining codecs.
std::size_t codecCount = this->codecs.size();
for(std::size_t index = 0; index < codecCount; ++index) {
if((index == mostRecent) || (index == secondMostRecent) || (index == hintCodecIndex)) {
continue;
}
if(tryCodecCallback(*this->codecs[index].get(), extension, result)) {
updateMostRecentCodecIndex(secondMostRecent);
return true;
}
}
// No codec can load the file, we give up
return false;
}
// ------------------------------------------------------------------------------------------- //
void BitmapSerializer::updateMostRecentCodecIndex(std::size_t codecIndex) const {
this->secondMostRecentCodecIndex.store(
this->mostRecentCodecIndex.load(std::memory_order::memory_order_relaxed),
std::memory_order::memory_order_relaxed
);
this->mostRecentCodecIndex.store(codecIndex, std::memory_order::memory_order_relaxed);
}
// ------------------------------------------------------------------------------------------- //
}}} // namespace Nuclex::Pixels::Storage