Changed Atom to Use LibPng instead of OpenImageIO

Merge pull request #4006 from aws-lumberyard-dev/Atom/santora/UseLibPng

OpenImageIO is a huge library and overkill for Atom's needs, which is to just save and load png files at this time. Here we remove all dependence on OpenImageIO and use libpng instead. I introduced a PngFile wrapper class to assist with loading and saving png files.

Note that there is a bit more work to do before merging these changes. I am still working on adding libpng to the 3rdParty package system. Currently these changes only reflect the addition of the Windows package. We'll make the other platform packages available before merging.
monroegm-disable-blank-issue-2
santorac 4 years ago committed by GitHub
commit f765dc0942
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -18,6 +18,7 @@
#include <Atom/Utils/DdsFile.h>
#include <Atom/Utils/PpmFile.h>
#include <Atom/Utils/PngFile.h>
#include <AzCore/Serialization/Json/JsonUtils.h>
#include <AzCore/Jobs/JobFunction.h>
@ -34,21 +35,12 @@
#include <AzCore/Preprocessor/EnumReflectUtils.h>
#include <AzCore/Console/Console.h>
#if defined(OPEN_IMAGE_IO_ENABLED)
// OpenImageIO/fmath.h(2271,5): error C4777: 'fprintf' : format string '%zd' requires an argument of type 'unsigned __int64', but variadic
// argument 5 has type 'OpenImageIO_v2_1::span_strided<const float,-1>::index_type'
AZ_PUSH_DISABLE_WARNING(4777, "-Wunknown-warning-option")
#include <OpenImageIO/imageio.h>
AZ_POP_DISABLE_WARNING
#endif
namespace AZ
{
namespace Render
{
AZ_ENUM_DEFINE_REFLECT_UTILITIES(FrameCaptureResult);
#if defined(OPEN_IMAGE_IO_ENABLED)
AZ_CVAR(unsigned int,
r_pngCompressionLevel,
3, // A compression level of 3 seems like the best default in terms of file size and saving speeds
@ -97,28 +89,22 @@ namespace AZ
jobCompletion.StartAndWaitForCompletion();
}
using namespace OIIO;
AZStd::unique_ptr<ImageOutput> out = ImageOutput::create(outputFilePath.c_str());
if (out)
{
ImageSpec spec(
readbackResult.m_imageDescriptor.m_size.m_width,
readbackResult.m_imageDescriptor.m_size.m_height,
numChannels
);
spec.attribute("png:compressionLevel", r_pngCompressionLevel);
Utils::PngFile image = Utils::PngFile::Create(readbackResult.m_imageDescriptor.m_size, readbackResult.m_imageDescriptor.m_format, *buffer);
if (out->open(outputFilePath.c_str(), spec))
{
out->write_image(TypeDesc::UINT8, buffer->data());
out->close();
return FrameCaptureOutputResult{FrameCaptureResult::Success, AZStd::nullopt};
}
Utils::PngFile::SaveSettings saveSettings;
saveSettings.m_compressionLevel = r_pngCompressionLevel;
// We should probably strip alpha to save space, especially for automated test screenshots. Alpha is left in to maintain
// prior behavior, changing this is out of scope for the current task. Note, it would have bit of a cascade effect where
// AtomSampleViewer's ScriptReporter assumes an RGBA image.
saveSettings.m_stripAlpha = false;
if(image && image.Save(outputFilePath.c_str(), saveSettings))
{
return FrameCaptureOutputResult{FrameCaptureResult::Success, AZStd::nullopt};
}
return FrameCaptureOutputResult{FrameCaptureResult::InternalError, "Unable to save frame capture output to " + outputFilePath};
return FrameCaptureOutputResult{FrameCaptureResult::InternalError, "Unable to save frame capture output to '" + outputFilePath + "'"};
}
#endif
FrameCaptureOutputResult DdsFrameCaptureOutput(
const AZStd::string& outputFilePath, const AZ::RPI::AttachmentReadback::ReadbackResult& readbackResult)
@ -502,7 +488,6 @@ namespace AZ
m_result = ddsFrameCapture.m_result;
m_latestCaptureInfo = ddsFrameCapture.m_errorMessage.value_or("");
}
#if defined(OPEN_IMAGE_IO_ENABLED)
else if (extension == "png")
{
if (readbackResult.m_imageDescriptor.m_format == RHI::Format::R8G8B8A8_UNORM ||
@ -523,7 +508,6 @@ namespace AZ
m_result = FrameCaptureResult::UnsupportedFormat;
}
}
#endif
else
{
m_latestCaptureInfo = AZStd::string::format("Only supports saving image to ppm or dds files");

@ -12,12 +12,5 @@ endif()
set(LY_BUILD_DEPENDENCIES
PRIVATE
3rdParty::OpenImageIO
3rdParty::ilmbase
)
# [GFX-TODO] Add macro defintion in OpenImageIO 3rd party find cmake file
set(LY_COMPILE_DEFINITIONS
PRIVATE
OPEN_IMAGE_IO_ENABLED
)

@ -24,6 +24,7 @@ ly_add_target(
Gem::Atom_RHI.Public
PUBLIC
Gem::Atom_RHI.Reflect
3rdParty::libpng
)
################################################################################
@ -43,6 +44,7 @@ if(PAL_TRAIT_BUILD_TESTS_SUPPORTED)
BUILD_DEPENDENCIES
PRIVATE
AZ::AzTest
AZ::AzFramework
Gem::Atom_Utils.Static
)
ly_add_googletest(

@ -0,0 +1,98 @@
/*
* Copyright (c) Contributors to the Open 3D Engine Project.
* For complete copyright and license terms please see the LICENSE at the root of this distribution.
*
* SPDX-License-Identifier: Apache-2.0 OR MIT
*
*/
#pragma once
#include <AzCore/std/string/string.h>
#include <AzCore/std/containers/vector.h>
#include <AzCore/std/containers/unordered_map.h>
#include <AtomCore/std/containers/array_view.h>
#include <AzCore/std/functional.h>
#include <Atom/RHI.Reflect/Size.h>
#include <Atom/RHI.Reflect/Format.h>
namespace AZ
{
namespace Utils
{
//! This is a light wrapper class for libpng, to load and save .png files.
//! Functionality is limited, feel free to add more features as needed.
class PngFile
{
public:
using ErrorHandler = AZStd::function<void(const char*)>;
struct LoadSettings
{
ErrorHandler m_errorHandler{}; //!< optional callback function describing any errors that are encountered
bool m_stripAlpha = false; //!< the alpha channel will be skipped, loading an RGBA image as RGB
LoadSettings() {}; // clang errors out if this is not provided.
};
struct SaveSettings
{
ErrorHandler m_errorHandler{}; //!< optional callback function describing any errors that are encountered
bool m_stripAlpha = false; //!< the alpha channel will be skipped, saving an RGBA buffer as RGB
int m_compressionLevel = 6; //!< this is the zlib compression level. See png_set_compression_level in png.h
SaveSettings() {}; // clang errors out if this is not provided.
};
// To keep things simple for now we limit all images to RGB and RGBA, 8 bits per channel.
enum class Format
{
Unknown,
RGB,
RGBA
};
//! @return the loaded PngFile or an invalid PngFile if there was an error.
static PngFile Load(const char* path, LoadSettings loadSettings = {});
//! Create a PngFile from an RHI data buffer.
//! @param size the dimensions of the image (m_depth is not used, assumed to be 1)
//! @param format indicates the pixel format represented by @data. Only a limited set of formats are supported, see implementation.
//! @param data the buffer of image data. The size of the buffer must match the @size and @format parameters.
//! @param errorHandler optional callback function describing any errors that are encountered
//! @return the created PngFile or an invalid PngFile if there was an error.
static PngFile Create(const RHI::Size& size, RHI::Format format, AZStd::array_view<uint8_t> data, ErrorHandler errorHandler = {});
static PngFile Create(const RHI::Size& size, RHI::Format format, AZStd::vector<uint8_t>&& data, ErrorHandler errorHandler = {});
PngFile() = default;
AZ_DEFAULT_MOVE(PngFile)
//! @return true if the save operation was successful
bool Save(const char* path, SaveSettings saveSettings = {});
bool IsValid() const;
operator bool() const { return IsValid(); }
uint32_t GetWidth() const { return m_width; }
uint32_t GetHeight() const { return m_height; }
Format GetBufferFormat() const { return m_bufferFormat; }
const AZStd::vector<uint8_t>& GetBuffer() const { return m_buffer; }
//! Returns a r-value reference that can be moved. This will invalidate the PngFile.
AZStd::vector<uint8_t>&& TakeBuffer();
private:
AZ_DEFAULT_COPY(PngFile)
static const int HeaderSize = 8;
static void DefaultErrorHandler(const char* message);
uint32_t m_width = 0;
uint32_t m_height = 0;
int32_t m_bitDepth = 0;
Format m_bufferFormat = Format::Unknown;
AZStd::vector<uint8_t> m_buffer;
};
}
} // namespace AZ

@ -0,0 +1,324 @@
/*
* Copyright (c) Contributors to the Open 3D Engine Project.
* For complete copyright and license terms please see the LICENSE at the root of this distribution.
*
* SPDX-License-Identifier: Apache-2.0 OR MIT
*
*/
#include <Atom/Utils/PngFile.h>
#include <png.h>
namespace AZ
{
namespace Utils
{
namespace
{
void PngImage_user_error_fn(png_structp png_ptr, png_const_charp error_msg)
{
PngFile::ErrorHandler* errorHandler = reinterpret_cast<PngFile::ErrorHandler*>(png_get_error_ptr(png_ptr));
(*errorHandler)(error_msg);
}
void PngImage_user_warning_fn(png_structp /*png_ptr*/, png_const_charp warning_msg)
{
AZ_Warning("PngFile", false, "%s", warning_msg);
}
}
PngFile PngFile::Create(const RHI::Size& size, RHI::Format format, AZStd::array_view<uint8_t> data, ErrorHandler errorHandler)
{
return Create(size, format, AZStd::vector<uint8_t>{data.begin(), data.end()}, errorHandler);
}
PngFile PngFile::Create(const RHI::Size& size, RHI::Format format, AZStd::vector<uint8_t>&& data, ErrorHandler errorHandler)
{
if (!errorHandler)
{
errorHandler = [](const char* message) { DefaultErrorHandler(message); };
}
PngFile image;
if (RHI::Format::R8G8B8A8_UNORM == format)
{
if (size.m_width * size.m_height * 4 == data.size())
{
image.m_width = size.m_width;
image.m_height = size.m_height;
image.m_bitDepth = 8;
image.m_bufferFormat = PngFile::Format::RGBA;
image.m_buffer = AZStd::move(data);
}
else
{
errorHandler("Invalid arguments. Buffer size does not match the image dimensions.");
}
}
else
{
errorHandler(AZStd::string::format("Cannot create PngFile with unsupported format %s", AZ::RHI::ToString(format)).c_str());
}
return image;
}
PngFile PngFile::Load(const char* path, LoadSettings loadSettings)
{
if (!loadSettings.m_errorHandler)
{
loadSettings.m_errorHandler = [path](const char* message) { DefaultErrorHandler(AZStd::string::format("Could not load file '%s'. %s", path, message).c_str()); };
}
// For documentation of this code, see http://www.libpng.org/pub/png/libpng-1.4.0-manual.pdf chapter 3
FILE* fp = NULL;
azfopen(&fp, path, "rb"); // return type differs across platforms so can't do inside if
if (!fp)
{
loadSettings.m_errorHandler("Cannot open file.");
return {};
}
png_byte header[HeaderSize] = {};
if (fread(header, 1, HeaderSize, fp) != HeaderSize)
{
fclose(fp);
loadSettings.m_errorHandler("Invalid png header.");
return {};
}
bool isPng = !png_sig_cmp(header, 0, HeaderSize);
if (!isPng)
{
fclose(fp);
loadSettings.m_errorHandler("Invalid png header.");
return {};
}
png_voidp user_error_ptr = &loadSettings.m_errorHandler;
png_error_ptr user_error_fn = PngImage_user_error_fn;
png_error_ptr user_warning_fn = PngImage_user_warning_fn;
png_structp png_ptr = png_create_read_struct(PNG_LIBPNG_VER_STRING, user_error_ptr, user_error_fn, user_warning_fn);
if (!png_ptr)
{
fclose(fp);
loadSettings.m_errorHandler("png_create_read_struct failed.");
return {};
}
png_infop info_ptr = png_create_info_struct(png_ptr);
if (!info_ptr)
{
png_destroy_read_struct(&png_ptr, (png_infopp)NULL, (png_infopp)NULL);
fclose(fp);
loadSettings.m_errorHandler("png_create_info_struct failed.");
return {};
}
png_infop end_info = png_create_info_struct(png_ptr);
if (!end_info)
{
png_destroy_read_struct(&png_ptr, &info_ptr, (png_infopp)NULL);
fclose(fp);
loadSettings.m_errorHandler("png_create_info_struct failed.");
return {};
}
AZ_PUSH_DISABLE_WARNING(4611, "-Wunknown-warning-option") // Disables "interaction between '_setjmp' and C++ object destruction is non-portable". See https://docs.microsoft.com/en-us/cpp/preprocessor/warning?view=msvc-160
if (setjmp(png_jmpbuf(png_ptr)))
{
png_destroy_read_struct(&png_ptr, &info_ptr, &end_info);
fclose(fp);
// We don't report an error message here because the user_error_fn should have done that already.
return {};
}
AZ_POP_DISABLE_WARNING
png_init_io(png_ptr, fp);
png_set_sig_bytes(png_ptr, HeaderSize);
png_set_keep_unknown_chunks(png_ptr, PNG_HANDLE_CHUNK_NEVER, NULL, 0);
// To keep things simple for now we limit all images to RGB and RGBA, 8 bits per channel
int png_transforms = PNG_TRANSFORM_PACKING | // Expand 1, 2 and 4-bit samples to bytes
PNG_TRANSFORM_STRIP_16 | // Reduce 16 bit samples to 8 bits
PNG_TRANSFORM_GRAY_TO_RGB;
if (loadSettings.m_stripAlpha)
{
png_transforms |= PNG_TRANSFORM_STRIP_ALPHA;
}
png_read_png(png_ptr, info_ptr, png_transforms, NULL);
// Note that libpng will allocate row_pointers for us. If we want to manage the memory ourselves, we need to call png_set_rows.
// In that case we would have to use the low level interface: png_read_info, png_read_image, and png_read_end.
png_bytep* row_pointers = png_get_rows(png_ptr, info_ptr);
PngFile pngFile;
int colorType = 0;
png_get_IHDR(png_ptr, info_ptr, &pngFile.m_width, &pngFile.m_height, &pngFile.m_bitDepth, &colorType, NULL, NULL, NULL);
uint32_t bytesPerPixel = 0;
switch (colorType)
{
case PNG_COLOR_TYPE_RGB:
pngFile.m_bufferFormat = PngFile::Format::RGB;
bytesPerPixel = 3;
break;
case PNG_COLOR_TYPE_RGBA:
pngFile.m_bufferFormat = PngFile::Format::RGBA;
bytesPerPixel = 4;
break;
case PNG_COLOR_TYPE_PALETTE:
// Handles cases where the image uses 1, 2, or 4 bit samples.
// Note bytesPerPixel is 3 because we use PNG_TRANSFORM_PACKING
pngFile.m_bufferFormat = PngFile::Format::RGB;
bytesPerPixel = 3;
break;
default:
AZ_Assert(false, "The png transforms should have ensured a pixel format of RGB or RGBA, 8 bits per channel");
png_destroy_read_struct(&png_ptr, &info_ptr, (png_infopp)NULL);
fclose(fp);
loadSettings.m_errorHandler("Unsupported pixel format.");
return {};
}
// In the future we could use the low-level interface to avoid copying the image (and provide progress callbacks)
pngFile.m_buffer.set_capacity(pngFile.m_width * pngFile.m_height * bytesPerPixel);
for (uint32_t rowIndex = 0; rowIndex < pngFile.m_height; ++rowIndex)
{
png_bytep row = row_pointers[rowIndex];
pngFile.m_buffer.insert(pngFile.m_buffer.end(), row, row + (pngFile.m_width * bytesPerPixel));
}
png_destroy_read_struct(&png_ptr, &info_ptr, &end_info);
fclose(fp);
return pngFile;
}
bool PngFile::Save(const char* path, SaveSettings saveSettings)
{
if (!saveSettings.m_errorHandler)
{
saveSettings.m_errorHandler = [path](const char* message) { DefaultErrorHandler(AZStd::string::format("Could not save file '%s'. %s", path, message).c_str()); };
}
if (!IsValid())
{
saveSettings.m_errorHandler("This PngFile is invalid.");
return false;
}
// For documentation of this code, see http://www.libpng.org/pub/png/libpng-1.4.0-manual.pdf chapter 4
FILE* fp = NULL;
azfopen(&fp, path, "wb"); // return type differs across platforms so can't do inside if
if (!fp)
{
saveSettings.m_errorHandler("Cannot open file.");
return false;
}
png_voidp user_error_ptr = &saveSettings.m_errorHandler;
png_error_ptr user_error_fn = PngImage_user_error_fn;
png_error_ptr user_warning_fn = PngImage_user_warning_fn;
png_structp png_ptr = png_create_write_struct(PNG_LIBPNG_VER_STRING, user_error_ptr, user_error_fn, user_warning_fn);
if (!png_ptr)
{
fclose(fp);
saveSettings.m_errorHandler("png_create_write_struct failed.");
return false;
}
png_infop info_ptr = png_create_info_struct(png_ptr);
if (!info_ptr)
{
png_destroy_write_struct(&png_ptr, (png_infopp)NULL);
fclose(fp);
saveSettings.m_errorHandler("png_destroy_write_struct failed.");
return false;
}
AZ_PUSH_DISABLE_WARNING(4611, "-Wunknown-warning-option") // Disables "interaction between '_setjmp' and C++ object destruction is non-portable". See https://docs.microsoft.com/en-us/cpp/preprocessor/warning?view=msvc-160
if (setjmp(png_jmpbuf(png_ptr)))
{
png_destroy_write_struct(&png_ptr, &info_ptr);
fclose(fp);
// We don't report an error message here because the user_error_fn should have done that already.
return false;
}
AZ_POP_DISABLE_WARNING
png_init_io(png_ptr, fp);
int colorType = 0;
if (saveSettings.m_stripAlpha || m_bufferFormat == PngFile::Format::RGB)
{
colorType = PNG_COLOR_TYPE_RGB;
}
else
{
colorType = PNG_COLOR_TYPE_RGBA;
}
int transforms = PNG_TRANSFORM_IDENTITY;
if (saveSettings.m_stripAlpha && m_bufferFormat == PngFile::Format::RGBA)
{
transforms |= PNG_TRANSFORM_STRIP_FILLER_AFTER;
}
png_set_IHDR(png_ptr, info_ptr, m_width, m_height, m_bitDepth, colorType, PNG_INTERLACE_NONE, PNG_COMPRESSION_TYPE_DEFAULT, PNG_FILTER_TYPE_DEFAULT);
png_set_compression_level(png_ptr, saveSettings.m_compressionLevel);
const uint32_t bytesPerPixel = (m_bufferFormat == PngFile::Format::RGB) ? 3 : 4;
AZStd::vector<uint8_t*> rows;
rows.reserve(m_height);
for (uint32_t i = 0; i < m_height; ++i)
{
rows.push_back(m_buffer.begin() + m_width * bytesPerPixel * i);
}
png_set_rows(png_ptr, info_ptr, rows.begin());
png_write_png(png_ptr, info_ptr, transforms, NULL);
png_destroy_write_struct(&png_ptr, &info_ptr);
fclose(fp);
return true;
}
void PngFile::DefaultErrorHandler(const char* message)
{
AZ_Error("PngFile", false, "%s", message);
}
bool PngFile::IsValid() const
{
return
!m_buffer.empty() &&
m_width > 0 &&
m_height > 0 &&
m_bitDepth > 0;
}
AZStd::vector<uint8_t>&& PngFile::TakeBuffer()
{
return AZStd::move(m_buffer);
}
} // namespace Utils
}// namespace AZ

@ -0,0 +1,313 @@
/*
* Copyright (c) Contributors to the Open 3D Engine Project.
* For complete copyright and license terms please see the LICENSE at the root of this distribution.
*
* SPDX-License-Identifier: Apache-2.0 OR MIT
*
*/
#include <Atom/Utils/PngFile.h>
#include <AzCore/UnitTest/TestTypes.h>
#include <AzCore/Math/Color.h>
#include <AzCore/std/containers/array.h>
#include <AzCore/IO/FileIO.h>
#include <AzCore/IO/Path/Path.h>
#include <AzFramework/IO/LocalFileIO.h>
namespace UnitTest
{
using namespace AZ::Utils;
class PngFileTests
: public AllocatorsFixture
{
protected:
AZ::IO::Path m_testImageFolder;
AZ::IO::Path m_tempPngFilePath;
AZStd::vector<uint8_t> m_primaryColors3x1;
AZStd::unique_ptr<AZ::IO::FileIOBase> m_localFileIO;
void SetUp() override
{
AllocatorsFixture::SetUp();
m_testImageFolder = AZ::IO::Path(AZ::Test::GetEngineRootPath()) / AZ::IO::Path("Gems/Atom/Utils/Code/Tests/PngTestImages", '/');
m_tempPngFilePath = m_testImageFolder / "temp.png";
m_localFileIO.reset(aznew AZ::IO::LocalFileIO());
AZ::IO::FileIOBase::SetInstance(m_localFileIO.get());
AZ::IO::FileIOBase::GetInstance()->Remove(m_tempPngFilePath.c_str());
m_primaryColors3x1 = {
255u, 0u, 0u, 255u,
0u, 255u, 0u, 255u,
0u, 0u, 255u, 255u
};
}
void TearDown() override
{
m_testImageFolder = AZ::IO::Path{};
m_tempPngFilePath = AZ::IO::Path{};
m_primaryColors3x1 = AZStd::vector<uint8_t>{};
AZ::IO::FileIOBase::SetInstance(nullptr);
m_localFileIO.reset();
AllocatorsFixture::TearDown();
}
struct Color3 : public AZStd::array<uint8_t, 3>
{
using Base = AZStd::array<uint8_t, 3>;
Color3(uint8_t r, uint8_t g, uint8_t b) : Base({r, g, b}) {}
Color3(const uint8_t* raw) : Base({raw[0], raw[1], raw[2]}) {}
};
struct Color4 : public AZStd::array<uint8_t, 4>
{
using Base = AZStd::array<uint8_t, 4>;
Color4(uint8_t r, uint8_t g, uint8_t b, uint8_t a) : Base({r, g, b, a}) {}
Color4(const uint8_t* raw) : Base({raw[0], raw[1], raw[2], raw[3]}) {}
};
};
TEST_F(PngFileTests, LoadRgb)
{
PngFile image = PngFile::Load((m_testImageFolder / "ColorChart_rgb.png").c_str());
EXPECT_TRUE(image.IsValid());
EXPECT_EQ(image.GetBufferFormat(), PngFile::Format::RGB);
EXPECT_EQ(image.GetWidth(), 3);
EXPECT_EQ(image.GetHeight(), 2);
EXPECT_EQ(image.GetBuffer().size(), 18);
EXPECT_EQ(Color3(image.GetBuffer().begin() + 0), Color3(255u, 0u, 0u));
EXPECT_EQ(Color3(image.GetBuffer().begin() + 3), Color3(0u, 255u, 0u));
EXPECT_EQ(Color3(image.GetBuffer().begin() + 6), Color3(0u, 0u, 255u));
EXPECT_EQ(Color3(image.GetBuffer().begin() + 9), Color3(255u, 255u, 0u));
EXPECT_EQ(Color3(image.GetBuffer().begin() + 12), Color3(0u, 255u, 255u));
EXPECT_EQ(Color3(image.GetBuffer().begin() + 15), Color3(255u, 0u, 255u));
}
TEST_F(PngFileTests, LoadRgba)
{
PngFile image = PngFile::Load((m_testImageFolder / "ColorChart_rgba.png").c_str());
EXPECT_TRUE(image.IsValid());
EXPECT_EQ(image.GetBufferFormat(), PngFile::Format::RGBA);
EXPECT_EQ(image.GetWidth(), 3);
EXPECT_EQ(image.GetHeight(), 2);
EXPECT_EQ(image.GetBuffer().size(), 24);
EXPECT_EQ(Color4(image.GetBuffer().begin() + 0), Color4(255u, 0u, 0u, 200u));
EXPECT_EQ(Color4(image.GetBuffer().begin() + 4), Color4(0u, 255u, 0u, 150u));
EXPECT_EQ(Color4(image.GetBuffer().begin() + 8), Color4(0u, 0u, 255u, 100u));
EXPECT_EQ(Color4(image.GetBuffer().begin() + 12), Color4(255u, 255u, 0u, 125u));
EXPECT_EQ(Color4(image.GetBuffer().begin() + 16), Color4(0u, 255u, 255u, 175u));
EXPECT_EQ(Color4(image.GetBuffer().begin() + 20), Color4(255u, 0u, 255u, 75u));
}
TEST_F(PngFileTests, LoadRgbaStripAlpha)
{
PngFile::LoadSettings loadSettings;
loadSettings.m_stripAlpha = true;
PngFile image = PngFile::Load((m_testImageFolder / "ColorChart_rgba.png").c_str(), loadSettings);
// Note these checks are identical to the LoadRgb test.
EXPECT_TRUE(image.IsValid());
EXPECT_EQ(image.GetBufferFormat(), PngFile::Format::RGB);
EXPECT_EQ(image.GetWidth(), 3);
EXPECT_EQ(image.GetHeight(), 2);
EXPECT_EQ(image.GetBuffer().size(), 18);
EXPECT_EQ(Color3(image.GetBuffer().begin() + 0), Color3(255u, 0u, 0u));
EXPECT_EQ(Color3(image.GetBuffer().begin() + 3), Color3(0u, 255u, 0u));
EXPECT_EQ(Color3(image.GetBuffer().begin() + 6), Color3(0u, 0u, 255u));
EXPECT_EQ(Color3(image.GetBuffer().begin() + 9), Color3(255u, 255u, 0u));
EXPECT_EQ(Color3(image.GetBuffer().begin() + 12), Color3(0u, 255u, 255u));
EXPECT_EQ(Color3(image.GetBuffer().begin() + 15), Color3(255u, 0u, 255u));
}
TEST_F(PngFileTests, LoadColorPaletteTwoBits)
{
PngFile image = PngFile::Load((m_testImageFolder / "ColorPalette_2bit.png").c_str());
EXPECT_TRUE(image.IsValid());
EXPECT_EQ(image.GetBufferFormat(), PngFile::Format::RGB);
EXPECT_EQ(image.GetWidth(), 1);
EXPECT_EQ(image.GetHeight(), 3);
EXPECT_EQ(image.GetBuffer().size(), 9);
EXPECT_EQ(Color3(image.GetBuffer().begin() + 0), Color3(255u, 0u, 0u));
EXPECT_EQ(Color3(image.GetBuffer().begin() + 3), Color3(0u, 255u, 0u));
EXPECT_EQ(Color3(image.GetBuffer().begin() + 6), Color3(0u, 0u, 255u));
}
TEST_F(PngFileTests, LoadGrayscaleOneBit)
{
PngFile image = PngFile::Load((m_testImageFolder / "GrayPalette_1bit.png").c_str());
EXPECT_TRUE(image.IsValid());
EXPECT_EQ(image.GetBufferFormat(), PngFile::Format::RGB);
EXPECT_EQ(image.GetWidth(), 1);
EXPECT_EQ(image.GetHeight(), 2);
EXPECT_EQ(image.GetBuffer().size(), 6);
EXPECT_EQ(Color3(image.GetBuffer().begin() + 0), Color3(0u, 0u, 0u));
EXPECT_EQ(Color3(image.GetBuffer().begin() + 3), Color3(255u, 255u, 255u));
}
TEST_F(PngFileTests, LoadRgba64Bits)
{
PngFile image = PngFile::Load((m_testImageFolder / "Gradient_rgb_16bpc.png").c_str());
EXPECT_TRUE(image.IsValid());
EXPECT_EQ(image.GetBufferFormat(), PngFile::Format::RGB);
EXPECT_EQ(image.GetWidth(), 5);
EXPECT_EQ(image.GetHeight(), 1);
EXPECT_EQ(image.GetBuffer().size(), 15);
// The values in this file are 30.0f, 30.1f, 30.2f, 30.3f, 30.4f. But we use PNG_TRANSFORM_STRIP_16 to reduce them to 8 bits per channel for simplicity.
EXPECT_EQ(Color3(image.GetBuffer().begin() + 0), Color3(76u, 0u, 0u));
EXPECT_EQ(Color3(image.GetBuffer().begin() + 3), Color3(77u, 0u, 0u));
EXPECT_EQ(Color3(image.GetBuffer().begin() + 6), Color3(77u, 0u, 0u));
EXPECT_EQ(Color3(image.GetBuffer().begin() + 9), Color3(77u, 0u, 0u));
EXPECT_EQ(Color3(image.GetBuffer().begin() + 12), Color3(77u, 0u, 0u));
}
TEST_F(PngFileTests, CreateCopy)
{
AZStd::vector<uint8_t> data = m_primaryColors3x1;
PngFile savedImage = PngFile::Create(AZ::RHI::Size{3, 1, 0}, AZ::RHI::Format::R8G8B8A8_UNORM, data);
EXPECT_TRUE(savedImage.IsValid());
EXPECT_EQ(savedImage.GetWidth(), 3);
EXPECT_EQ(savedImage.GetHeight(), 1);
EXPECT_EQ(savedImage.GetBuffer(), data);
}
TEST_F(PngFileTests, CreateMove)
{
AZStd::vector<uint8_t> data = m_primaryColors3x1;
PngFile savedImage = PngFile::Create(AZ::RHI::Size{3, 1, 0}, AZ::RHI::Format::R8G8B8A8_UNORM, AZStd::move(data));
EXPECT_TRUE(savedImage.IsValid());
EXPECT_EQ(savedImage.GetWidth(), 3);
EXPECT_EQ(savedImage.GetHeight(), 1);
EXPECT_EQ(savedImage.GetBuffer(), m_primaryColors3x1);
EXPECT_TRUE(data.empty()); // The data should have been moved
}
TEST_F(PngFileTests, SaveRgba)
{
PngFile savedImage = PngFile::Create(AZ::RHI::Size{3, 1, 0}, AZ::RHI::Format::R8G8B8A8_UNORM, m_primaryColors3x1);
bool result = savedImage.Save(m_tempPngFilePath.c_str());
EXPECT_TRUE(result);
PngFile loadedImage = PngFile::Load(m_tempPngFilePath.c_str());
EXPECT_TRUE(loadedImage.IsValid());
EXPECT_EQ(loadedImage.GetBufferFormat(), savedImage.GetBufferFormat());
EXPECT_EQ(loadedImage.GetWidth(), savedImage.GetWidth());
EXPECT_EQ(loadedImage.GetHeight(), savedImage.GetHeight());
EXPECT_EQ(loadedImage.GetBuffer(), savedImage.GetBuffer());
}
TEST_F(PngFileTests, SaveRgbaStripAlpha)
{
PngFile savedImage = PngFile::Create(AZ::RHI::Size{3, 1, 0}, AZ::RHI::Format::R8G8B8A8_UNORM, m_primaryColors3x1);
PngFile::SaveSettings saveSettings;
saveSettings.m_stripAlpha = true;
bool result = savedImage.Save(m_tempPngFilePath.c_str(), saveSettings);
EXPECT_TRUE(result);
// The alpha was stripped when saving. Now we load the data without stripping anything and should find
// that there is no alpha channel.
PngFile loadedImage = PngFile::Load(m_tempPngFilePath.c_str());
// The dimensions are the same...
EXPECT_TRUE(loadedImage.IsValid());
EXPECT_EQ(loadedImage.GetWidth(), savedImage.GetWidth());
EXPECT_EQ(loadedImage.GetHeight(), savedImage.GetHeight());
// ... but the format is different
EXPECT_NE(loadedImage.GetBufferFormat(), savedImage.GetBufferFormat());
EXPECT_EQ(loadedImage.GetBufferFormat(), PngFile::Format::RGB);
// ... and the loaded data is smaller
EXPECT_NE(loadedImage.GetBuffer(), savedImage.GetBuffer());
EXPECT_EQ(Color3(loadedImage.GetBuffer().begin() + 0), Color3(255u, 0u, 0u));
EXPECT_EQ(Color3(loadedImage.GetBuffer().begin() + 3), Color3(0u, 255u, 0u));
EXPECT_EQ(Color3(loadedImage.GetBuffer().begin() + 6), Color3(0u, 0u, 255u));
}
TEST_F(PngFileTests, Error_CreateUnsupportedFormat)
{
AZStd::vector<uint8_t> data = m_primaryColors3x1;
AZStd::string gotErrorMessage;
PngFile savedImage = PngFile::Create(AZ::RHI::Size{3, 1, 0}, AZ::RHI::Format::R32_UINT, data,
[&gotErrorMessage](const char* errorMessage) { gotErrorMessage = errorMessage; });
EXPECT_FALSE(savedImage.IsValid());
EXPECT_TRUE(gotErrorMessage.find("unsupported format R32_UINT") != AZStd::string::npos);
}
TEST_F(PngFileTests, Error_CreateIncorrectBufferSize)
{
AZStd::vector<uint8_t> data = m_primaryColors3x1;
AZStd::string gotErrorMessage;
PngFile savedImage = PngFile::Create(AZ::RHI::Size{3, 2, 0}, AZ::RHI::Format::R8G8B8A8_UNORM, data,
[&gotErrorMessage](const char* errorMessage) { gotErrorMessage = errorMessage; });
EXPECT_FALSE(savedImage.IsValid());
EXPECT_TRUE(gotErrorMessage.find("does not match") != AZStd::string::npos);
}
TEST_F(PngFileTests, Error_LoadFileNotFound)
{
AZStd::string gotErrorMessage;
PngFile::LoadSettings loadSettings;
loadSettings.m_errorHandler = [&gotErrorMessage](const char* errorMessage) { gotErrorMessage = errorMessage; };
PngFile image = PngFile::Load((m_testImageFolder / "DoesNotExist.png").c_str(), loadSettings);
EXPECT_FALSE(image.IsValid());
EXPECT_TRUE(gotErrorMessage.find("not open file") != AZStd::string::npos);
}
TEST_F(PngFileTests, Error_LoadEmptyFile)
{
AZStd::string gotErrorMessage;
PngFile::LoadSettings loadSettings;
loadSettings.m_errorHandler = [&gotErrorMessage](const char* errorMessage) { gotErrorMessage = errorMessage; };
PngFile image = PngFile::Load((m_testImageFolder / "EmptyFile.png").c_str(), loadSettings);
EXPECT_FALSE(image.IsValid());
EXPECT_TRUE(gotErrorMessage.find("Invalid png header") != AZStd::string::npos);
}
TEST_F(PngFileTests, Error_LoadNotPngFile)
{
AZStd::string gotErrorMessage;
PngFile::LoadSettings loadSettings;
loadSettings.m_errorHandler = [&gotErrorMessage](const char* errorMessage) { gotErrorMessage = errorMessage; };
PngFile image = PngFile::Load((m_testImageFolder / "ColorChart_rgba.jpg").c_str(), loadSettings);
EXPECT_FALSE(image.IsValid());
EXPECT_TRUE(gotErrorMessage.find("Invalid png header") != AZStd::string::npos);
}
TEST_F(PngFileTests, Error_SaveInvalidPngFile)
{
AZStd::string gotErrorMessage;
PngFile::SaveSettings saveSettings;
saveSettings.m_errorHandler = [&gotErrorMessage](const char* errorMessage) { gotErrorMessage = errorMessage; };
PngFile savedImage;
bool result = savedImage.Save(m_tempPngFilePath.c_str(), saveSettings);
EXPECT_FALSE(result);
EXPECT_TRUE(gotErrorMessage.find("PngFile is invalid") != AZStd::string::npos);
EXPECT_FALSE(AZ::IO::FileIOBase::GetInstance()->Exists(m_tempPngFilePath.c_str()));
}
}

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:af0d0d079354495ff96aa266ecb4092d679e6c5a952e8b2d4ee743e538d54809
size 126

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:01cad8dd75c9a26169e858960817424362c16cbf2a8df8bc55857f6172ce2526
size 834

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:6ddb5f70eaf72fe09add6cc7cdf7cbba2d327ab312bb9bde73d7f456f80b8d6e
size 141

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:b50e93bbbd320d69752df2c25b4118c0198226a0d230781a6ed93f89b7eea053
size 142

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:cb56e8bf15a4727bcebba579cb97a36e1f26e4a4870b2b4a38bdbd4b4bd076fd
size 127

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:710fd5c80af49a42249a471ded6c9119be79d6748218073bb8297ffb4b4e62d1
size 137

@ -23,6 +23,7 @@ set(FILES
Include/Atom/Utils/ImGuiShaderMetrics.inl
Include/Atom/Utils/ImGuiTransientAttachmentProfiler.h
Include/Atom/Utils/ImGuiTransientAttachmentProfiler.inl
Include/Atom/Utils/PngFile.h
Include/Atom/Utils/PpmFile.h
Include/Atom/Utils/StableDynamicArray.h
Include/Atom/Utils/StableDynamicArray.inl
@ -31,6 +32,7 @@ set(FILES
Include/Atom/Utils/AssetCollectionAsyncLoader.h
Source/DdsFile.cpp
Source/ImageComparison.cpp
Source/PngFile.cpp
Source/PpmFile.cpp
Source/Utils.cpp
Source/AssetCollectionAsyncLoader.cpp

@ -8,5 +8,6 @@
set(FILES
Tests/ImageComparisonTests.cpp
Tests/PngFileTests.cpp
Tests/StableDynamicArrayTests.cpp
)

@ -24,6 +24,7 @@ ly_associate_package(PACKAGE_NAME PhysX-4.1.2.29882248-rev3-android TARGETS Phy
ly_associate_package(PACKAGE_NAME mikkelsen-1.0.0.4-android TARGETS mikkelsen PACKAGE_HASH 075e8e4940884971063b5a9963014e2e517246fa269c07c7dc55b8cf2cd99705)
ly_associate_package(PACKAGE_NAME googletest-1.8.1-rev4-android TARGETS googletest PACKAGE_HASH 95671be75287a61c9533452835c3647e9c1b30f81b34b43bcb0ec1997cc23894)
ly_associate_package(PACKAGE_NAME googlebenchmark-1.5.0-rev2-android TARGETS GoogleBenchmark PACKAGE_HASH 20b46e572211a69d7d94ddad1c89ec37bb958711d6ad4025368ac89ea83078fb)
ly_associate_package(PACKAGE_NAME libpng-1.6.37-rev1-android TARGETS libpng PACKAGE_HASH 51d3ec1559c5595196c11e11674cf5745989d3073bf33dabc6697e3eee77a1cc)
ly_associate_package(PACKAGE_NAME libsamplerate-0.2.1-rev2-android TARGETS libsamplerate PACKAGE_HASH bf13662afe65d02bcfa16258a4caa9b875534978227d6f9f36c9cfa92b3fb12b)
ly_associate_package(PACKAGE_NAME OpenSSL-1.1.1b-rev1-android TARGETS OpenSSL PACKAGE_HASH 4036d4019d722f0e1b7a1621bf60b5a17ca6a65c9c78fd8701cee1131eec8480)
ly_associate_package(PACKAGE_NAME zlib-1.2.11-rev2-android TARGETS zlib PACKAGE_HASH 85b730b97176772538cfcacd6b6aaf4655fc2d368d134d6dd55e02f28f183826)

@ -36,6 +36,7 @@ ly_associate_package(PACKAGE_NAME googletest-1.8.1-rev4-linux
ly_associate_package(PACKAGE_NAME googlebenchmark-1.5.0-rev2-linux TARGETS GoogleBenchmark PACKAGE_HASH 4038878f337fc7e0274f0230f71851b385b2e0327c495fc3dd3d1c18a807928d)
ly_associate_package(PACKAGE_NAME unwind-1.2.1-linux TARGETS unwind PACKAGE_HASH 3453265fb056e25432f611a61546a25f60388e315515ad39007b5925dd054a77)
ly_associate_package(PACKAGE_NAME qt-5.15.2-rev5-linux TARGETS Qt PACKAGE_HASH 76b395897b941a173002845c7219a5f8a799e44b269ffefe8091acc048130f28)
ly_associate_package(PACKAGE_NAME libpng-1.6.37-rev1-linux TARGETS libpng PACKAGE_HASH 896451999f1de76375599aec4b34ae0573d8d34620d9ab29cc30b8739c265ba6)
ly_associate_package(PACKAGE_NAME libsamplerate-0.2.1-rev2-linux TARGETS libsamplerate PACKAGE_HASH 41643c31bc6b7d037f895f89d8d8d6369e906b92eff42b0fe05ee6a100f06261)
ly_associate_package(PACKAGE_NAME OpenSSL-1.1.1b-rev2-linux TARGETS OpenSSL PACKAGE_HASH b779426d1e9c5ddf71160d5ae2e639c3b956e0fb5e9fcaf9ce97c4526024e3bc)
ly_associate_package(PACKAGE_NAME DirectXShaderCompilerDxc-1.6.2104-o3de-rev3-linux TARGETS DirectXShaderCompilerDxc PACKAGE_HASH 88c4a359325d749bc34090b9ac466424847f3b71ba0de15045cf355c17c07099)

@ -38,6 +38,7 @@ ly_associate_package(PACKAGE_NAME googletest-1.8.1-rev4-mac
ly_associate_package(PACKAGE_NAME googlebenchmark-1.5.0-rev2-mac TARGETS GoogleBenchmark PACKAGE_HASH ad25de0146769c91e179953d845de2bec8ed4a691f973f47e3eb37639381f665)
ly_associate_package(PACKAGE_NAME OpenSSL-1.1.1b-rev1-mac TARGETS OpenSSL PACKAGE_HASH 28adc1c0616ac0482b2a9d7b4a3a3635a1020e87b163f8aba687c501cf35f96c)
ly_associate_package(PACKAGE_NAME qt-5.15.2-rev5-mac TARGETS Qt PACKAGE_HASH 9d25918351898b308ded3e9e571fff6f26311b2071aeafd00dd5b249fdf53f7e)
ly_associate_package(PACKAGE_NAME libpng-1.6.37-mac TARGETS libpng PACKAGE_HASH 1ad76cd038ccc1f288f83c5fe2859a0f35c5154e1fe7658e1230cc428d318a8b)
ly_associate_package(PACKAGE_NAME libsamplerate-0.2.1-rev2-mac TARGETS libsamplerate PACKAGE_HASH b912af40c0ac197af9c43d85004395ba92a6a859a24b7eacd920fed5854a97fe)
ly_associate_package(PACKAGE_NAME zlib-1.2.11-rev2-mac TARGETS zlib PACKAGE_HASH 21714e8a6de4f2523ee92a7f52d51fbee29c5f37ced334e00dc3c029115b472e)
ly_associate_package(PACKAGE_NAME squish-ccr-deb557d-rev1-mac TARGETS squish-ccr PACKAGE_HASH 155bfbfa17c19a9cd2ef025de14c5db598f4290045d5b0d83ab58cb345089a77)

@ -41,6 +41,7 @@ ly_associate_package(PACKAGE_NAME d3dx12-headers-rev1-windows
ly_associate_package(PACKAGE_NAME pyside2-qt-5.15.1-rev2-windows TARGETS pyside2 PACKAGE_HASH c90f3efcc7c10e79b22a33467855ad861f9dbd2e909df27a5cba9db9fa3edd0f)
ly_associate_package(PACKAGE_NAME openimageio-2.1.16.0-rev2-windows TARGETS OpenImageIO PACKAGE_HASH 85a2a6cf35cbc4c967c56ca8074babf0955c5b490c90c6e6fd23c78db99fc282)
ly_associate_package(PACKAGE_NAME qt-5.15.2-rev4-windows TARGETS Qt PACKAGE_HASH a4634caaf48192cad5c5f408504746e53d338856148285057274f6a0ccdc071d)
ly_associate_package(PACKAGE_NAME libpng-1.6.37-rev1-windows TARGETS libpng PACKAGE_HASH aa20c894fbd7cdaea585a54e37620b3454a7e414a58128acd68ccf6fe76c47d6)
ly_associate_package(PACKAGE_NAME libsamplerate-0.2.1-rev2-windows TARGETS libsamplerate PACKAGE_HASH dcf3c11a96f212a52e2c9241abde5c364ee90b0f32fe6eeb6dcdca01d491829f)
ly_associate_package(PACKAGE_NAME OpenMesh-8.1-rev1-windows TARGETS OpenMesh PACKAGE_HASH 1c1df639358526c368e790dfce40c45cbdfcfb1c9a041b9d7054a8949d88ee77)
ly_associate_package(PACKAGE_NAME civetweb-1.8-rev1-windows TARGETS civetweb PACKAGE_HASH 36d0e58a59bcdb4dd70493fb1b177aa0354c945b06c30416348fd326cf323dd4)

@ -25,6 +25,7 @@ ly_associate_package(PACKAGE_NAME PhysX-4.1.2.29882248-rev3-ios TARGETS PhysX
ly_associate_package(PACKAGE_NAME mikkelsen-1.0.0.4-ios TARGETS mikkelsen PACKAGE_HASH 976aaa3ccd8582346132a10af253822ccc5d5bcc9ea5ba44d27848f65ee88a8a)
ly_associate_package(PACKAGE_NAME googletest-1.8.1-rev4-ios TARGETS googletest PACKAGE_HASH 2f121ad9784c0ab73dfaa58e1fee05440a82a07cc556bec162eeb407688111a7)
ly_associate_package(PACKAGE_NAME googlebenchmark-1.5.0-rev2-ios TARGETS GoogleBenchmark PACKAGE_HASH c2ffaed2b658892b1bcf81dee4b44cd1cb09fc78d55584ef5cb8ab87f2d8d1ae)
ly_associate_package(PACKAGE_NAME libpng-1.6.37-ios TARGETS libpng PACKAGE_HASH 18a8217721083c4dc46514105be43ca764fa9c994a74aa0b57766ea6f8187e7b)
ly_associate_package(PACKAGE_NAME libsamplerate-0.2.1-rev2-ios TARGETS libsamplerate PACKAGE_HASH 7656b961697f490d4f9c35d2e61559f6fc38c32102e542a33c212cd618fc2119)
ly_associate_package(PACKAGE_NAME OpenSSL-1.1.1b-rev1-ios TARGETS OpenSSL PACKAGE_HASH cd0dfce3086a7172777c63dadbaf0ac3695b676119ecb6d0614b5fb1da03462f)
ly_associate_package(PACKAGE_NAME zlib-1.2.11-rev2-ios TARGETS zlib PACKAGE_HASH a59fc0f83a02c616b679799310e9d86fde84514c6d2acefa12c6def0ae4a880c)

Loading…
Cancel
Save