/* * 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 #include namespace AZ { namespace Utils { namespace { void PngImage_user_error_fn(png_structp png_ptr, png_const_charp error_msg) { PngFile::ErrorHandler* errorHandler = reinterpret_cast(png_get_error_ptr(png_ptr)); (*errorHandler)(error_msg); } void PngImage_user_warning_fn(png_structp /*png_ptr*/, [[maybe_unused]] 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 data, ErrorHandler errorHandler) { return Create(size, format, AZStd::vector{data.begin(), data.end()}, errorHandler); } PngFile PngFile::Create(const RHI::Size& size, RHI::Format format, AZStd::vector&& 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 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([[maybe_unused]] 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&& PngFile::TakeBuffer() { return AZStd::move(m_buffer); } } // namespace Utils }// namespace AZ