Added support for saving PNG files with palette.

Based on (heavily modified) patch by troelsk.

Closes #12505.



git-svn-id: https://svn.wxwidgets.org/svn/wx/wxWidgets/trunk@66552 c3d73ce0-8a6f-49c7-b76d-6d57e0e08775
This commit is contained in:
Dimitri Schoolwerth
2011-01-03 22:22:16 +00:00
parent 41514cc474
commit 8ee313d2be
5 changed files with 294 additions and 43 deletions

View File

@@ -452,6 +452,7 @@ All (GUI):
- Added support for saving TGA files. - Added support for saving TGA files.
- Added wxArtProvider returning higher quality icons from Tango project. - Added wxArtProvider returning higher quality icons from Tango project.
- wxPropertyGrid: Added "HasAlpha" attribute for wxColourProperty. - wxPropertyGrid: Added "HasAlpha" attribute for wxColourProperty.
- Added support for saving PNG files with palette (troelsk).
GTK: GTK:

View File

@@ -33,7 +33,8 @@ enum
{ {
wxPNG_TYPE_COLOUR = 0, wxPNG_TYPE_COLOUR = 0,
wxPNG_TYPE_GREY = 2, wxPNG_TYPE_GREY = 2,
wxPNG_TYPE_GREY_RED = 3 wxPNG_TYPE_GREY_RED = 3,
wxPNG_TYPE_PALETTE = 4
}; };
class WXDLLIMPEXP_CORE wxPNGHandler: public wxImageHandler class WXDLLIMPEXP_CORE wxPNGHandler: public wxImageHandler

View File

@@ -55,7 +55,8 @@ enum wxImagePNGType
{ {
wxPNG_TYPE_COLOUR = 0, ///< Colour PNG image. wxPNG_TYPE_COLOUR = 0, ///< Colour PNG image.
wxPNG_TYPE_GREY = 2, ///< Greyscale PNG image converted from RGB. wxPNG_TYPE_GREY = 2, ///< Greyscale PNG image converted from RGB.
wxPNG_TYPE_GREY_RED = 3 ///< Greyscale PNG image using red as grey. wxPNG_TYPE_GREY_RED = 3, ///< Greyscale PNG image using red as grey.
wxPNG_TYPE_PALETTE = 4 ///< Palette encoding.
}; };
/** /**

View File

@@ -641,6 +641,26 @@ error:
return false; return false;
} }
// ----------------------------------------------------------------------------
// SaveFile() helpers
// ----------------------------------------------------------------------------
static int PaletteFind(const png_color& clr,
const png_color *pal, png_uint_16 palCount)
{
for (png_uint_16 i = 0; i < palCount; i++)
{
if ( (clr.red == pal[i].red)
&& (clr.green == pal[i].green)
&& (clr.blue == pal[i].blue))
{
return i;
}
}
return wxNOT_FOUND;
}
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
// writing PNGs // writing PNGs
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
@@ -693,18 +713,81 @@ bool wxPNGHandler::SaveFile( wxImage *image, wxOutputStream& stream, bool verbos
// explanation why this line is mandatory // explanation why this line is mandatory
png_set_write_fn( png_ptr, &wxinfo, wx_PNG_stream_writer, NULL); png_set_write_fn( png_ptr, &wxinfo, wx_PNG_stream_writer, NULL);
const int iColorType = image->HasOption(wxIMAGE_OPTION_PNG_FORMAT) const bool bHasPngFormatOption
= image->HasOption(wxIMAGE_OPTION_PNG_FORMAT);
int iColorType = bHasPngFormatOption
? image->GetOptionInt(wxIMAGE_OPTION_PNG_FORMAT) ? image->GetOptionInt(wxIMAGE_OPTION_PNG_FORMAT)
: wxPNG_TYPE_COLOUR; : wxPNG_TYPE_COLOUR;
const int iBitDepth = image->HasOption(wxIMAGE_OPTION_PNG_BITDEPTH)
? image->GetOptionInt(wxIMAGE_OPTION_PNG_BITDEPTH)
: 8;
bool bHasAlpha = image->HasAlpha(); bool bHasAlpha = image->HasAlpha();
bool bHasMask = image->HasMask(); bool bHasMask = image->HasMask();
bool bUseAlpha = bHasAlpha || bHasMask;
#if wxUSE_PALETTE
/*
Only save as an indexed image if the number of palette entries does not
exceed libpng's limit (256).
We assume here that we will need an extra palette entry if there's an
alpha or mask, regardless of whether a possibly needed conversion from
alpha to a mask fails (unlikely), or whether the mask colour already
can be found in the palette (more likely). In the latter case an extra
palette entry would not be required later on and the image could actually
be saved as a palettised PNG (instead now it will be saved as true colour).
A little bit of precision is lost, but at the benefit of a lot more
simplified code.
*/
bool bUsePalette =
(!bHasPngFormatOption || iColorType == wxPNG_TYPE_PALETTE)
&& image->HasPalette()
&& image->GetPalette().GetColoursCount()
+ ((bHasAlpha || bHasMask) ? 1 : 0) <= PNG_MAX_PALETTE_LENGTH;
wxImage temp_image(*image);
if (bUsePalette && image->HasAlpha() && !bHasMask)
{
/*
Only convert alpha to mask if saving as a palettised image was
explicitly requested. We don't want to lose alpha's precision
by converting to a mask just to be able to save palettised.
*/
if (iColorType == wxPNG_TYPE_PALETTE
&& temp_image.ConvertAlphaToMask())
{
image = &temp_image;
bHasMask = true;
bHasAlpha = image->HasAlpha();
}
else
{
bUsePalette = false;
iColorType = wxPNG_TYPE_COLOUR;
}
}
#else
bool bUsePalette = false;
#endif // wxUSE_PALETTE
bool bUseAlpha = !bUsePalette && (bHasAlpha || bHasMask);
png_color mask;
if (bHasMask)
{
mask.red = image->GetMaskRed();
mask.green = image->GetMaskGreen();
mask.blue = image->GetMaskBlue();
}
int iPngColorType; int iPngColorType;
#if wxUSE_PALETTE
if (bUsePalette)
{
iPngColorType = PNG_COLOR_TYPE_PALETTE;
iColorType = wxPNG_TYPE_PALETTE;
}
else
#endif // wxUSE_PALETTE
if ( iColorType==wxPNG_TYPE_COLOUR ) if ( iColorType==wxPNG_TYPE_COLOUR )
{ {
iPngColorType = bUseAlpha ? PNG_COLOR_TYPE_RGB_ALPHA iPngColorType = bUseAlpha ? PNG_COLOR_TYPE_RGB_ALPHA
@@ -731,11 +814,65 @@ bool wxPNGHandler::SaveFile( wxImage *image, wxOutputStream& stream, bool verbos
if (image->HasOption(wxIMAGE_OPTION_PNG_COMPRESSION_BUFFER_SIZE)) if (image->HasOption(wxIMAGE_OPTION_PNG_COMPRESSION_BUFFER_SIZE))
png_set_compression_buffer_size( png_ptr, image->GetOptionInt(wxIMAGE_OPTION_PNG_COMPRESSION_BUFFER_SIZE) ); png_set_compression_buffer_size( png_ptr, image->GetOptionInt(wxIMAGE_OPTION_PNG_COMPRESSION_BUFFER_SIZE) );
int iBitDepth = !bUsePalette && image->HasOption(wxIMAGE_OPTION_PNG_BITDEPTH)
? image->GetOptionInt(wxIMAGE_OPTION_PNG_BITDEPTH)
: 8;
png_set_IHDR( png_ptr, info_ptr, image->GetWidth(), image->GetHeight(), png_set_IHDR( png_ptr, info_ptr, image->GetWidth(), image->GetHeight(),
iBitDepth, iPngColorType, iBitDepth, iPngColorType,
PNG_INTERLACE_NONE, PNG_COMPRESSION_TYPE_BASE, PNG_INTERLACE_NONE, PNG_COMPRESSION_TYPE_BASE,
PNG_FILTER_TYPE_BASE); PNG_FILTER_TYPE_BASE);
#if wxUSE_PALETTE
if (bUsePalette)
{
const wxPalette& pal = image->GetPalette();
const int palCount = pal.GetColoursCount();
png_colorp palette = (png_colorp) malloc(
(palCount + 1 /*headroom for trans */) * sizeof(png_color));
if (!palette)
{
png_destroy_write_struct( &png_ptr, (png_infopp)NULL );
if (verbose)
{
wxLogError(_("Couldn't save PNG image."));
}
return false;
}
png_uint_16 i;
for (i = 0; i < palCount; ++i)
{
pal.GetRGB(i, &palette[i].red, &palette[i].green, &palette[i].blue);
}
png_uint_16 numPalette = palCount;
if (bHasMask)
{
int index = PaletteFind(mask, palette, numPalette);
if (index)
{
if (index == wxNOT_FOUND)
{
numPalette++;
index = palCount;
palette[index] = mask;
}
wxSwap(palette[0], palette[index]);
}
png_byte trans = 0;
png_set_tRNS(png_ptr, info_ptr, &trans, 1, NULL);
}
png_set_PLTE(png_ptr, info_ptr, palette, numPalette);
free (palette);
}
#endif // wxUSE_PALETTE
int iElements; int iElements;
png_color_8 sig_bit; png_color_8 sig_bit;
@@ -806,15 +943,6 @@ bool wxPNGHandler::SaveFile( wxImage *image, wxOutputStream& stream, bool verbos
int iHeight = image->GetHeight(); int iHeight = image->GetHeight();
int iWidth = image->GetWidth(); int iWidth = image->GetWidth();
unsigned char uchMaskRed = 0, uchMaskGreen = 0, uchMaskBlue = 0;
if ( bHasMask )
{
uchMaskRed = image->GetMaskRed();
uchMaskGreen = image->GetMaskGreen();
uchMaskBlue = image->GetMaskBlue();
}
unsigned char *pColors = image->GetData(); unsigned char *pColors = image->GetData();
for (int y = 0; y != iHeight; ++y) for (int y = 0; y != iHeight; ++y)
@@ -822,9 +950,10 @@ bool wxPNGHandler::SaveFile( wxImage *image, wxOutputStream& stream, bool verbos
unsigned char *pData = data; unsigned char *pData = data;
for (int x = 0; x != iWidth; x++) for (int x = 0; x != iWidth; x++)
{ {
unsigned char uchRed = *pColors++; png_color clr;
unsigned char uchGreen = *pColors++; clr.red = *pColors++;
unsigned char uchBlue = *pColors++; clr.green = *pColors++;
clr.blue = *pColors++;
switch ( iColorType ) switch ( iColorType )
{ {
@@ -833,13 +962,13 @@ bool wxPNGHandler::SaveFile( wxImage *image, wxOutputStream& stream, bool verbos
// fall through // fall through
case wxPNG_TYPE_COLOUR: case wxPNG_TYPE_COLOUR:
*pData++ = uchRed; *pData++ = clr.red;
if ( iBitDepth == 16 ) if ( iBitDepth == 16 )
*pData++ = 0; *pData++ = 0;
*pData++ = uchGreen; *pData++ = clr.green;
if ( iBitDepth == 16 ) if ( iBitDepth == 16 )
*pData++ = 0; *pData++ = 0;
*pData++ = uchBlue; *pData++ = clr.blue;
if ( iBitDepth == 16 ) if ( iBitDepth == 16 )
*pData++ = 0; *pData++ = 0;
break; break;
@@ -849,9 +978,9 @@ bool wxPNGHandler::SaveFile( wxImage *image, wxOutputStream& stream, bool verbos
// where do these coefficients come from? maybe we // where do these coefficients come from? maybe we
// should have image options for them as well? // should have image options for them as well?
unsigned uiColor = unsigned uiColor =
(unsigned) (76.544*(unsigned)uchRed + (unsigned) (76.544*(unsigned)clr.red +
150.272*(unsigned)uchGreen + 150.272*(unsigned)clr.green +
36.864*(unsigned)uchBlue); 36.864*(unsigned)clr.blue);
*pData++ = (unsigned char)((uiColor >> 8) & 0xFF); *pData++ = (unsigned char)((uiColor >> 8) & 0xFF);
if ( iBitDepth == 16 ) if ( iBitDepth == 16 )
@@ -860,10 +989,15 @@ bool wxPNGHandler::SaveFile( wxImage *image, wxOutputStream& stream, bool verbos
break; break;
case wxPNG_TYPE_GREY_RED: case wxPNG_TYPE_GREY_RED:
*pData++ = uchRed; *pData++ = clr.red;
if ( iBitDepth == 16 ) if ( iBitDepth == 16 )
*pData++ = 0; *pData++ = 0;
break; break;
case wxPNG_TYPE_PALETTE:
*pData++ = (unsigned char) PaletteFind(clr,
info_ptr->palette, info_ptr->num_palette);
break;
} }
if ( bUseAlpha ) if ( bUseAlpha )
@@ -874,9 +1008,9 @@ bool wxPNGHandler::SaveFile( wxImage *image, wxOutputStream& stream, bool verbos
if ( bHasMask ) if ( bHasMask )
{ {
if ( (uchRed == uchMaskRed) if ( (clr.red == mask.red)
&& (uchGreen == uchMaskGreen) && (clr.green == mask.green)
&& (uchBlue == uchMaskBlue) ) && (clr.blue == mask.blue) )
uchAlpha = 0; uchAlpha = 0;
} }

View File

@@ -24,6 +24,7 @@
#endif // WX_PRECOMP #endif // WX_PRECOMP
#include "wx/image.h" #include "wx/image.h"
#include "wx/palette.h"
#include "wx/url.h" #include "wx/url.h"
#include "wx/log.h" #include "wx/log.h"
#include "wx/mstream.h" #include "wx/mstream.h"
@@ -870,11 +871,33 @@ void ImageTestCase::CompareLoadedImage()
} }
static enum
void CompareImage(const wxImageHandler& handler, const wxImage& expected) {
wxIMAGE_HAVE_ALPHA = (1 << 0),
wxIMAGE_HAVE_PALETTE = (1 << 1)
};
static
void CompareImage(const wxImageHandler& handler, const wxImage& image,
int properties = 0, const wxImage *compareTo = NULL)
{ {
bool testAlpha = expected.HasAlpha();
wxBitmapType type = handler.GetType(); wxBitmapType type = handler.GetType();
const bool testPalette = (properties & wxIMAGE_HAVE_PALETTE) != 0;
/*
This is getting messy and should probably be transformed into a table
with image format features before it gets hairier.
*/
if ( testPalette
&& ( !(type == wxBITMAP_TYPE_BMP
|| type == wxBITMAP_TYPE_GIF
|| type == wxBITMAP_TYPE_PNG)
|| type == wxBITMAP_TYPE_XPM) )
{
return;
}
const bool testAlpha = (properties & wxIMAGE_HAVE_ALPHA) != 0;
if (testAlpha if (testAlpha
&& !(type == wxBITMAP_TYPE_PNG || type == wxBITMAP_TYPE_TGA) ) && !(type == wxBITMAP_TYPE_PNG || type == wxBITMAP_TYPE_TGA) )
{ {
@@ -895,7 +918,7 @@ void CompareImage(const wxImageHandler& handler, const wxImage& expected)
} }
wxMemoryOutputStream memOut; wxMemoryOutputStream memOut;
if ( !expected.SaveFile(memOut, type) ) if ( !image.SaveFile(memOut, type) )
{ {
// Unfortunately we can't know if the handler just doesn't support // Unfortunately we can't know if the handler just doesn't support
// saving images, or if it failed to save. // saving images, or if it failed to save.
@@ -908,30 +931,37 @@ void CompareImage(const wxImageHandler& handler, const wxImage& expected)
wxImage actual(memIn); wxImage actual(memIn);
CPPUNIT_ASSERT(actual.IsOk()); CPPUNIT_ASSERT(actual.IsOk());
CPPUNIT_ASSERT( actual.GetSize() == expected.GetSize() ); const wxImage *expected = compareTo ? compareTo : &image;
CPPUNIT_ASSERT( actual.GetSize() == expected->GetSize() );
unsigned bitsPerPixel = testPalette ? 8 : (testAlpha ? 32 : 24);
WX_ASSERT_MESSAGE WX_ASSERT_MESSAGE
( (
("Compare test '%s' for saving failed", handler.GetExtension()), ("Compare test '%s (%d-bit)' for saving failed",
handler.GetExtension(), bitsPerPixel),
memcmp(actual.GetData(), expected.GetData(), memcmp(actual.GetData(), expected->GetData(),
expected.GetWidth() * expected.GetHeight() * 3) == 0 expected->GetWidth() * expected->GetHeight() * 3) == 0
); );
#if wxUSE_PALETTE
CPPUNIT_ASSERT(actual.HasPalette()
== (testPalette || type == wxBITMAP_TYPE_XPM));
#endif
CPPUNIT_ASSERT( actual.HasAlpha() == testAlpha);
if (!testAlpha) if (!testAlpha)
{ {
return; return;
} }
CPPUNIT_ASSERT( actual.HasAlpha() );
WX_ASSERT_MESSAGE WX_ASSERT_MESSAGE
( (
("Compare alpha test '%s' for saving failed", handler.GetExtension()), ("Compare alpha test '%s' for saving failed", handler.GetExtension()),
memcmp(actual.GetAlpha(), expected.GetAlpha(), memcmp(actual.GetAlpha(), expected->GetAlpha(),
expected.GetWidth() * expected.GetHeight()) == 0 expected->GetWidth() * expected->GetHeight()) == 0
); );
} }
@@ -941,6 +971,19 @@ void ImageTestCase::CompareSavedImage()
CPPUNIT_ASSERT( expected24.IsOk() ); CPPUNIT_ASSERT( expected24.IsOk() );
CPPUNIT_ASSERT( !expected24.HasAlpha() ); CPPUNIT_ASSERT( !expected24.HasAlpha() );
unsigned long numColours = expected24.CountColours();
wxImage expected8 = expected24.ConvertToGreyscale();
numColours = expected8.CountColours();
unsigned char greys[256];
for (size_t i = 0; i < 256; ++i)
{
greys[i] = i;
}
wxPalette palette(256, greys, greys, greys);
expected8.SetPalette(palette);
expected8.SetOption(wxIMAGE_OPTION_BMP_FORMAT, wxBMP_8BPP_PALETTE);
// Create an image with alpha based on the loaded image // Create an image with alpha based on the loaded image
wxImage expected32(expected24); wxImage expected32(expected24);
expected32.SetAlpha(); expected32.SetAlpha();
@@ -961,9 +1004,80 @@ void ImageTestCase::CompareSavedImage()
{ {
wxImageHandler *handler = (wxImageHandler *) node->GetData(); wxImageHandler *handler = (wxImageHandler *) node->GetData();
#if wxUSE_PALETTE
CompareImage(*handler, expected8, wxIMAGE_HAVE_PALETTE);
#endif
CompareImage(*handler, expected24); CompareImage(*handler, expected24);
CompareImage(*handler, expected32); CompareImage(*handler, expected32, wxIMAGE_HAVE_ALPHA);
} }
expected8.LoadFile("horse.gif");
CPPUNIT_ASSERT( expected8.IsOk() );
CPPUNIT_ASSERT( expected8.HasPalette() );
expected8.SetAlpha();
width = expected8.GetWidth();
height = expected8.GetHeight();
for (int y = 0; y < height; ++y)
{
for (int x = 0; x < width; ++x)
{
expected8.SetAlpha(x, y, (x*y) & wxIMAGE_ALPHA_OPAQUE);
}
}
/*
The image contains 256 indexed colours and needs another palette entry
for storing the transparency index. This results in wanting 257 palette
entries but that amount is not supported by PNG, as such this image
should not contain a palette (but still have alpha) and be stored as a
true colour image instead.
*/
CompareImage(*wxImage::FindHandler(wxBITMAP_TYPE_PNG),
expected8, wxIMAGE_HAVE_ALPHA);
#if wxUSE_PALETTE
/*
Now do the same test again but remove one (random) palette entry. This
should result in saving the PNG with a palette.
*/
unsigned char red[256], green[256], blue[256];
const wxPalette& pal = expected8.GetPalette();
const int paletteCount = pal.GetColoursCount();
for (int i = 0; i < paletteCount; ++i)
{
expected8.GetPalette().GetRGB(i, &red[i], &green[i], &blue[i]);
}
wxPalette newPal(paletteCount - 1, red, green, blue);
expected8.Replace(
red[paletteCount-1], green[paletteCount-1], blue[paletteCount-1],
red[paletteCount-2], green[paletteCount-2], blue[paletteCount-2]);
expected8.SetPalette(newPal);
/*
Explicitly make known we want a palettised PNG. If we don't then this
particular image gets saved as a true colour image because there's an
alpha channel present and the PNG saver prefers to keep the alpha over
saving as a palettised image that has alpha converted to a mask.
*/
expected8.SetOption(wxIMAGE_OPTION_PNG_FORMAT, wxPNG_TYPE_PALETTE);
wxImage ref8 = expected8;
/*
Convert the alpha channel to a mask like the PNG saver does. Also convert
the colour used for transparency from 1,0,0 to 2,0,0. The latter gets
done by the PNG loader in search of an unused colour to use for
transparency (this should be fixed).
*/
ref8.ConvertAlphaToMask();
ref8.Replace(1, 0, 0, 2, 0, 0);
CompareImage(*wxImage::FindHandler(wxBITMAP_TYPE_PNG),
expected8, wxIMAGE_HAVE_PALETTE, &ref8);
#endif
} }
#endif //wxUSE_IMAGE #endif //wxUSE_IMAGE