From 4c2ffb0c5b9f6f38596fc9291c698ddfc01794f7 Mon Sep 17 00:00:00 2001 From: GenevensiS <66968533+G-e-n-e-v-e-n-s-i-S@users.noreply.github.com> Date: Wed, 25 Jun 2025 12:23:10 +0200 Subject: [PATCH] Add support for working with PNG "Description" text chunk Add wxIMAGE_OPTION_PNG_DESCRIPTION which is filled with the contents of the (first) "Description" chunk from the PNG file when loading it and saved into such (iTXt) chunk when saving. Fixes #25556. Closes #25565. --- include/wx/imagpng.h | 1 + interface/wx/image.h | 11 +++++++++ interface/wx/imagpng.h | 1 + src/common/imagpng.cpp | 56 +++++++++++++++++++++++++++++++++++++++++- tests/image/image.cpp | 28 +++++++++++++++++++++ 5 files changed, 96 insertions(+), 1 deletion(-) diff --git a/include/wx/imagpng.h b/include/wx/imagpng.h index ae4155b734..2d85b8adac 100644 --- a/include/wx/imagpng.h +++ b/include/wx/imagpng.h @@ -27,6 +27,7 @@ #define wxIMAGE_OPTION_PNG_COMPRESSION_MEM_LEVEL wxT("PngZM") #define wxIMAGE_OPTION_PNG_COMPRESSION_STRATEGY wxT("PngZS") #define wxIMAGE_OPTION_PNG_COMPRESSION_BUFFER_SIZE wxT("PngZB") +#define wxIMAGE_OPTION_PNG_DESCRIPTION wxT("PngDescription") enum { diff --git a/interface/wx/image.h b/interface/wx/image.h index f719a6dfe8..1d73be6e59 100644 --- a/interface/wx/image.h +++ b/interface/wx/image.h @@ -148,6 +148,7 @@ enum wxImagePNGType #define wxIMAGE_OPTION_PNG_COMPRESSION_MEM_LEVEL wxString("PngZM") #define wxIMAGE_OPTION_PNG_COMPRESSION_STRATEGY wxString("PngZS") #define wxIMAGE_OPTION_PNG_COMPRESSION_BUFFER_SIZE wxString("PngZB") +#define wxIMAGE_OPTION_PNG_DESCRIPTION wxString("PngDescription") #define wxIMAGE_OPTION_TIFF_BITSPERSAMPLE wxString("BitsPerSample") #define wxIMAGE_OPTION_TIFF_SAMPLESPERPIXEL wxString("SamplesPerPixel") @@ -1322,6 +1323,16 @@ public: its own comment. If there is only a comment in the first frame of a GIF it will not be repeated in other frames. + Options specific to wxPNGHandler: + @li @c wxIMAGE_OPTION_PNG_DESCRIPTION: The contents of this option + will be converted to an uncompressed iTXt chunk with the + key: "Description", and written to the PNG file upon saving. + Contents of tXTt and iTXt chunks with the key: "Description" are + also automatically retrieved upon loading a PNG file, and stored + in this option. If multiple chunks with this key are present, + only one is retrieved. + @since 3.3.1 + @param name The name of the option, case-insensitive. @return diff --git a/interface/wx/imagpng.h b/interface/wx/imagpng.h index 04a8854017..750eb80b5f 100644 --- a/interface/wx/imagpng.h +++ b/interface/wx/imagpng.h @@ -12,6 +12,7 @@ #define wxIMAGE_OPTION_PNG_COMPRESSION_MEM_LEVEL wxT("PngZM") #define wxIMAGE_OPTION_PNG_COMPRESSION_STRATEGY wxT("PngZS") #define wxIMAGE_OPTION_PNG_COMPRESSION_BUFFER_SIZE wxT("PngZB") +#define wxIMAGE_OPTION_PNG_DESCRIPTION wxT("PngDescription") /* These are already in interface/wx/image.h They were likely put there as a stopgap, but they've been there long enough diff --git a/src/common/imagpng.cpp b/src/common/imagpng.cpp index 6850bce1f1..4be4df58bd 100644 --- a/src/common/imagpng.cpp +++ b/src/common/imagpng.cpp @@ -37,6 +37,8 @@ #include +#define wxIMAGE_OPTION_PNG_DESCRIPTION_KEY "Description" + // ---------------------------------------------------------------------------- // local functions // ---------------------------------------------------------------------------- @@ -189,7 +191,7 @@ PNGLINKAGEMODE wx_PNG_warning(png_structp png_ptr, png_const_charp message) wxPNGInfoStruct *info = png_ptr ? WX_PNG_INFO(png_ptr) : nullptr; if ( !info || info->verbose ) { - wxLogWarning( wxString::FromAscii(message) ); + wxLogWarning( wxString::FromUTF8(message) ); } } @@ -342,6 +344,40 @@ wxPNGImageData::DoLoadPNGFile(wxImage* image, wxPNGInfoStruct& wxinfo) return; png_read_image( png_ptr, lines ); + + // load "Description" text chunk + png_textp text_ptr; + const int num_comments = png_get_text( png_ptr, info_ptr, &text_ptr, nullptr ); + for (int i = 0; i < num_comments; ++i) + { + const wxString& key = wxString::From8BitData(text_ptr[i].key); + if (key == wxIMAGE_OPTION_PNG_DESCRIPTION_KEY) + { + wxString description; + switch (text_ptr[i].compression) + { + case PNG_TEXT_COMPRESSION_zTXt: + case PNG_TEXT_COMPRESSION_NONE: + // tEXt chunk: uses Latin-1 encoding. + description = wxString::From8BitData(text_ptr[i].text); + break; + + case PNG_ITXT_COMPRESSION_zTXt: + case PNG_ITXT_COMPRESSION_NONE: + // iTXt chunk: uses UTF-8 encoding. + description = wxString::FromUTF8(text_ptr[i].text); + break; + + default: + // Invalid type, should we report it? Probably not worth it. + break; + } + + if (!description.empty()) + image->SetOption(wxIMAGE_OPTION_PNG_DESCRIPTION, description); + } + } + png_read_end( png_ptr, info_ptr ); #if wxUSE_PALETTE @@ -737,6 +773,24 @@ bool wxPNGHandler::SaveFile( wxImage *image, wxOutputStream& stream, bool verbos png_set_pHYs( png_ptr, info_ptr, resX, resY, PNG_RESOLUTION_METER ); png_set_sBIT( png_ptr, info_ptr, &sig_bit ); + + // save "Description" text chunk + if (image->HasOption(wxIMAGE_OPTION_PNG_DESCRIPTION)) + { + const wxString& description = image->GetOption(wxIMAGE_OPTION_PNG_DESCRIPTION); + + png_text text; + text.key = const_cast(wxIMAGE_OPTION_PNG_DESCRIPTION_KEY); + text.lang = nullptr; + text.lang_key = nullptr; + text.compression = PNG_ITXT_COMPRESSION_NONE; + const auto& buf = description.utf8_str(); + text.text = const_cast(buf.data()); + text.itxt_length = buf.length(); + text.text_length = 0; + png_set_text( png_ptr, info_ptr, &text, 1 ); + } + png_write_info( png_ptr, info_ptr ); png_set_shift( png_ptr, &sig_bit ); png_set_packing( png_ptr ); diff --git a/tests/image/image.cpp b/tests/image/image.cpp index b0d0c9459f..50336c838d 100644 --- a/tests/image/image.cpp +++ b/tests/image/image.cpp @@ -1069,6 +1069,34 @@ TEST_CASE_METHOD(ImageHandlersInit, "wxImage::SavePNG", "[image]") } +static void TestPNGDescription(const wxString& description) +{ + wxImage image("horse.png"); + + image.SetOption(wxIMAGE_OPTION_PNG_DESCRIPTION, description); + wxMemoryOutputStream memOut; + REQUIRE(image.SaveFile(memOut, wxBITMAP_TYPE_PNG)); + + wxMemoryInputStream memIn(memOut); + REQUIRE(image.LoadFile(memIn)); + + CHECK(image.GetOption(wxIMAGE_OPTION_PNG_DESCRIPTION) == description); +} + +TEST_CASE_METHOD(ImageHandlersInit, "wxImage::PNGDescription", "[image]") +{ + // Test writing a description and reading it back. + TestPNGDescription("Providing the PNG a pneumatic puma as a present"); + + // Test writing and reading a description again but with non-ASCII characters. + TestPNGDescription("Тестирование 테스트 一 二 三"); + + // Test writing and reading a description again but with a long description. + TestPNGDescription(wxString(wxT('a'), 256) + + wxString(wxT('b'), 256) + + wxString(wxT('c'), 256)); +} + #if wxUSE_LIBTIFF static void TestTIFFImage(const wxString& option, int value, const wxImage *compareImage = nullptr)