Add alpha blending for wxImage::Paste

Add test cases for wxImage::Paste.

Closes #12458.

Co-Authored-By: Rachel Mark <kramdar@gmail.com>
This commit is contained in:
Eric Raijmakers 2020-09-23 11:56:58 +02:00 committed by Vadim Zeitlin
parent 8ae9987a29
commit 6e8da8641c
13 changed files with 490 additions and 26 deletions

View File

@ -73,6 +73,16 @@ enum wxImageResizeQuality
wxIMAGE_QUALITY_HIGH = 4
};
// Constants for wxImage::Paste() for specifying alpha blending option.
enum wxImageAlphaBlendMode
{
// Overwrite the original alpha values with the ones being pasted.
wxIMAGE_ALPHA_BLEND_OVER = 0,
// Compose the original alpha values with the ones being pasted.
wxIMAGE_ALPHA_BLEND_COMPOSE = 1
};
// alpha channel values: fully transparent, default threshold separating
// transparent pixels from opaque for a few functions dealing with alpha and
// fully opaque
@ -348,9 +358,12 @@ public:
wxImage Size( const wxSize& size, const wxPoint& pos,
int r = -1, int g = -1, int b = -1 ) const;
// pastes image into this instance and takes care of
// the mask colour and out of bounds problems
void Paste( const wxImage &image, int x, int y );
// Copy the data of the given image to the specified position of this one
// taking care of the out of bounds problems. Mask is respected, but alpha
// is simply replaced by default, use wxIMAGE_ALPHA_BLEND_COMPOSE to
// combine it with the original image alpha values if needed.
void Paste(const wxImage& image, int x, int y,
wxImageAlphaBlendMode alphaBlend = wxIMAGE_ALPHA_BLEND_OVER);
// return the new image with size width*height
wxImage Scale( int width, int height,

View File

@ -60,6 +60,21 @@ enum wxImageResizeQuality
wxIMAGE_QUALITY_HIGH
};
/**
Constants for wxImage::Paste() for specifying alpha blending option.
@since 3.2.0
*/
enum wxImageAlphaBlendMode
{
/// Overwrite the original alpha values with the ones being pasted.
wxIMAGE_ALPHA_BLEND_OVER = 0,
/// Compose the original alpha values with the ones being pasted.
wxIMAGE_ALPHA_BLEND_COMPOSE = 1
};
/**
Possible values for PNG image type option.
@ -803,8 +818,17 @@ public:
/**
Copy the data of the given @a image to the specified position in this image.
Takes care of the mask colour and out of bounds problems.
@param alphaBlend
This parameter (new in wx 3.2.0) determines whether the alpha values
of the original image replace (default) or are composed with the
alpha channel of this image. Notice that alpha blending overrides
the mask handling.
*/
void Paste(const wxImage& image, int x, int y);
void Paste(const wxImage& image, int x, int y,
wxImageAlphaBlendMode alphaBlend = wxIMAGE_ALPHA_BLEND_OVER);
/**
Replaces the colour specified by @e r1,g1,b1 by the colour @e r2,g2,b2.

View File

@ -1608,7 +1608,9 @@ wxImage wxImage::Size( const wxSize& size, const wxPoint& pos,
return image;
}
void wxImage::Paste( const wxImage &image, int x, int y )
void
wxImage::Paste(const wxImage & image, int x, int y,
wxImageAlphaBlendMode alphaBlend)
{
wxCHECK_RET( IsOk(), wxT("invalid image") );
wxCHECK_RET( image.IsOk(), wxT("invalid image") );
@ -1639,15 +1641,18 @@ void wxImage::Paste( const wxImage &image, int x, int y )
if (width < 1) return;
if (height < 1) return;
bool copiedPixels = false;
// If we can, copy the data using memcpy() as this is the fastest way. But
// for this the image being pasted must have "compatible" mask with this
// one meaning that either it must not have one at all or it must use the
// same masked colour.
if ( !image.HasMask() ||
// for this we must not do alpha compositing and the image being pasted
// must have "compatible" mask with this one meaning that either it must
// not have one at all or it must use the same masked colour.
if (alphaBlend == wxIMAGE_ALPHA_BLEND_OVER &&
(!image.HasMask() ||
((HasMask() &&
(GetMaskRed()==image.GetMaskRed()) &&
(GetMaskGreen()==image.GetMaskGreen()) &&
(GetMaskBlue()==image.GetMaskBlue()))) )
(GetMaskBlue()==image.GetMaskBlue())))) )
{
const unsigned char* source_data = image.GetData() + 3*(xx + yy*image.GetWidth());
int source_step = image.GetWidth()*3;
@ -1660,6 +1665,8 @@ void wxImage::Paste( const wxImage &image, int x, int y )
source_data += source_step;
target_data += target_step;
}
copiedPixels = true;
}
// Copy over the alpha channel from the original image
@ -1668,21 +1675,69 @@ void wxImage::Paste( const wxImage &image, int x, int y )
if ( !HasAlpha() )
InitAlpha();
const unsigned char* source_data = image.GetAlpha() + xx + yy*image.GetWidth();
int source_step = image.GetWidth();
const unsigned char*
alpha_source_data = image.GetAlpha() + xx + yy * image.GetWidth();
const int source_step = image.GetWidth();
unsigned char* target_data = GetAlpha() + (x+xx) + (y+yy)*M_IMGDATA->m_width;
int target_step = M_IMGDATA->m_width;
unsigned char*
alpha_target_data = GetAlpha() + (x + xx) + (y + yy) * M_IMGDATA->m_width;
const int target_step = M_IMGDATA->m_width;
for (int j = 0; j < height; j++,
source_data += source_step,
target_data += target_step)
switch (alphaBlend)
{
memcpy( target_data, source_data, width );
case wxIMAGE_ALPHA_BLEND_OVER:
{
// Copy just the alpha values.
for (int j = 0; j < height; j++,
alpha_source_data += source_step,
alpha_target_data += target_step)
{
memcpy(alpha_target_data, alpha_source_data, width);
}
break;
}
case wxIMAGE_ALPHA_BLEND_COMPOSE:
{
const unsigned char*
source_data = image.GetData() + 3 * (xx + yy * image.GetWidth());
unsigned char*
target_data = GetData() + 3 * ((x + xx) + (y + yy) * M_IMGDATA->m_width);
// Combine the alpha values but also apply alpha blending to
// the pixels themselves while we copy them.
for (int j = 0; j < height; j++,
alpha_source_data += source_step,
alpha_target_data += target_step,
source_data += 3 * source_step,
target_data += 3 * target_step)
{
for (int i = 0; i < width; i++)
{
float source_alpha = alpha_source_data[i] / 255.0f;
float light_left = (alpha_target_data[i] / 255.0f) * (1.0f - source_alpha);
float result_alpha = source_alpha + light_left;
alpha_target_data[i] = (unsigned char)((result_alpha * 255) +0.5);
for (int c = 3 * i; c < 3 * (i + 1); c++)
{
target_data[c] =
(unsigned char)(((source_data[c] * source_alpha +
target_data[c] * light_left) /
result_alpha) + 0.5);
}
}
}
copiedPixels = true;
break;
}
}
}
if (!HasMask() && image.HasMask())
// If we hadn't copied them yet we must need to take the mask of the image
// being pasted into account.
if (!copiedPixels)
{
unsigned char r = image.GetMaskRed();
unsigned char g = image.GetMaskGreen();

View File

@ -1454,6 +1454,321 @@ void ImageTestCase::ScaleCompare()
#endif //wxUSE_IMAGE
TEST_CASE("wxImage::Paste", "[image][paste]")
{
const static char* squares_xpm[] =
{
"9 9 7 1",
" c None",
"y c #FF0000",
"r c #FF0000",
"g c #00FF00",
"b c #0000FF",
"o c #FF6600",
"w c #FFFFFF",
"rrrrwgggg",
"rrrrwgggg",
"rrrrwgggg",
"rrrrwgggg",
"wwwwwwwww",
"bbbbwoooo",
"bbbbwoooo",
"bbbbwoooo",
"bbbbwoooo"
};
const static char* toggle_equal_size_xpm[] =
{
"9 9 2 1",
" c None",
"y c #FF0000",
"y y y y y",
" y y y y ",
"y y y y y",
" y y y y ",
"y y y y y",
" y y y y ",
"y y y y y",
" y y y y ",
"y y y y y",
};
SECTION("Paste same size image")
{
wxImage actual(squares_xpm);
wxImage paste(toggle_equal_size_xpm);
wxImage expected(toggle_equal_size_xpm);
actual.Paste(paste, 0, 0);
CHECK_THAT(actual, RGBSameAs(expected));
}
SECTION("Paste larger image")
{
const static char* toggle_larger_size_xpm[] =
{
"13 13 2 1",
" c None",
"y c #FF0000",
"y y y y y y y",
" y y y y y y ",
"y y y y y y y",
" y y y y y y ",
"y y y y y y y",
" y y y y y y ",
"y y y y y y y",
" y y y y y y ",
"y y y y y y y",
" y y y y y y ",
"y y y y y y y",
" y y y y y y ",
"y y y y y y y",
};
wxImage actual(squares_xpm);
wxImage paste(toggle_larger_size_xpm);
wxImage expected(toggle_equal_size_xpm);
actual.Paste(paste, -2, -2);
CHECK_THAT(actual, RGBSameAs(expected));
}
SECTION("Paste smaller image")
{
const static char* toggle_smaller_size_xpm[] =
{
"5 5 2 1",
" c None",
"y c #FF0000",
"y y y",
" y y ",
"y y y",
" y y ",
"y y y",
};
const static char* expected_xpm[] =
{
"9 9 7 1",
" c None",
"y c #FF0000",
"r c #FF0000",
"g c #00FF00",
"b c #0000FF",
"o c #FF6600",
"w c #FFFFFF",
"rrrrwgggg",
"rrrrwgggg",
"rry y ygg",
"rr y y gg",
"wwy y yww",
"bb y y oo",
"bby y yoo",
"bbbbwoooo",
"bbbbwoooo"
};
wxImage actual(squares_xpm);
wxImage paste(toggle_smaller_size_xpm);
wxImage expected(expected_xpm);
actual.Paste(paste, 2, 2);
CHECK_THAT(actual, RGBSameAs(expected));
}
SECTION("Paste beyond top left corner")
{
const static char* expected_xpm[] =
{
"9 9 7 1",
" c None",
"y c #FF0000",
"r c #FF0000",
"g c #00FF00",
"b c #0000FF",
"o c #FF6600",
"w c #FFFFFF",
"oooowgggg",
"oooowgggg",
"oooowgggg",
"oooowgggg",
"wwwwwwwww",
"bbbbwoooo",
"bbbbwoooo",
"bbbbwoooo",
"bbbbwoooo"
};
wxImage actual(squares_xpm);
wxImage paste(squares_xpm);
wxImage expected(expected_xpm);
actual.Paste(paste, -5, -5);
CHECK_THAT(actual, RGBSameAs(expected));
}
SECTION("Paste beyond top right corner")
{
const static char* expected_xpm[] =
{
"9 9 7 1",
" c None",
"y c #FF0000",
"r c #FF0000",
"g c #00FF00",
"b c #0000FF",
"o c #FF6600",
"w c #FFFFFF",
"rrrrwbbbb",
"rrrrwbbbb",
"rrrrwbbbb",
"rrrrwbbbb",
"wwwwwwwww",
"bbbbwoooo",
"bbbbwoooo",
"bbbbwoooo",
"bbbbwoooo"
};
wxImage actual(squares_xpm);
wxImage paste(squares_xpm);
wxImage expected(expected_xpm);
actual.Paste(paste, 5, -5);
CHECK_THAT(actual, RGBSameAs(expected));
}
SECTION("Paste beyond bottom right corner")
{
const static char* expected_xpm[] =
{
"9 9 7 1",
" c None",
"y c #FF0000",
"r c #FF0000",
"g c #00FF00",
"b c #0000FF",
"o c #FF6600",
"w c #FFFFFF",
"rrrrwgggg",
"rrrrwgggg",
"rrrrwgggg",
"rrrrwgggg",
"wwwwwwwww",
"bbbbwrrrr",
"bbbbwrrrr",
"bbbbwrrrr",
"bbbbwrrrr"
};
wxImage actual(squares_xpm);
wxImage paste(squares_xpm);
wxImage expected(expected_xpm);
actual.Paste(paste, 5, 5);
CHECK_THAT(actual, RGBSameAs(expected));
}
SECTION("Paste beyond bottom left corner")
{
const static char* expected_xpm[] =
{
"9 9 7 1",
" c None",
"y c #FF0000",
"r c #FF0000",
"g c #00FF00",
"b c #0000FF",
"o c #FF6600",
"w c #FFFFFF",
"rrrrwgggg",
"rrrrwgggg",
"rrrrwgggg",
"rrrrwgggg",
"wwwwwwwww",
"ggggwoooo",
"ggggwoooo",
"ggggwoooo",
"ggggwoooo"
};
wxImage actual(squares_xpm);
wxImage paste(squares_xpm);
wxImage expected(expected_xpm);
actual.Paste(paste, -5, 5);
CHECK_THAT(actual, RGBSameAs(expected));
}
wxImage::AddHandler(new wxPNGHandler());
wxImage background("image/paste_input_background.png");
CHECK(background.IsOk());
wxImage opaque_square("image/paste_input_overlay_transparent_border_opaque_square.png");
CHECK(opaque_square.IsOk());
wxImage transparent_square("image/paste_input_overlay_transparent_border_semitransparent_square.png");
CHECK(transparent_square.IsOk());
wxImage transparent_circle("image/paste_input_overlay_transparent_border_semitransparent_circle.png");
CHECK(transparent_circle.IsOk());
SECTION("Paste fully opaque image onto blank image without alpha")
{
wxImage actual(background.GetSize());
actual.Paste(background, 0, 0, wxIMAGE_ALPHA_BLEND_COMPOSE);
CHECK_THAT(actual, RGBSameAs(background));
CHECK(!actual.HasAlpha());
}
SECTION("Paste fully opaque image onto blank image with alpha")
{
wxImage actual(background.GetSize());
actual.InitAlpha();
actual.Paste(background, 0, 0, wxIMAGE_ALPHA_BLEND_COMPOSE);
CHECK_THAT(actual, RGBSameAs(background));
CHECK_THAT(actual, CenterAlphaPixelEquals(wxALPHA_OPAQUE));
}
SECTION("Paste fully transparent image")
{
wxImage actual = background.Copy();
wxImage transparent(actual.GetSize());
transparent.InitAlpha();
memset(transparent.GetAlpha(), 0, transparent.GetWidth() * transparent.GetHeight());
actual.Paste(transparent, 0, 0, wxIMAGE_ALPHA_BLEND_COMPOSE);
CHECK_THAT(actual, RGBSameAs(background));
CHECK_THAT(actual, CenterAlphaPixelEquals(wxALPHA_OPAQUE));
}
SECTION("Paste image with transparent region")
{
wxImage actual = background.Copy();
actual.Paste(opaque_square, 0, 0, wxIMAGE_ALPHA_BLEND_COMPOSE);
CHECK_THAT(actual, RGBSameAs(wxImage("image/paste_result_background_plus_overlay_transparent_border_opaque_square.png")));
CHECK_THAT(actual, CenterAlphaPixelEquals(wxALPHA_OPAQUE));
}
SECTION("Paste image with semi transparent region")
{
wxImage actual = background.Copy();
actual.Paste(transparent_square, 0, 0, wxIMAGE_ALPHA_BLEND_COMPOSE);
CHECK_THAT(actual, RGBSameAs(wxImage("image/paste_result_background_plus_overlay_transparent_border_semitransparent_square.png")));
CHECK_THAT(actual, CenterAlphaPixelEquals(wxALPHA_OPAQUE));
}
SECTION("Paste two semi transparent images on top of background")
{
wxImage actual = background.Copy();
actual.Paste(transparent_circle, 0, 0, wxIMAGE_ALPHA_BLEND_COMPOSE);
actual.Paste(transparent_square, 0, 0, wxIMAGE_ALPHA_BLEND_COMPOSE);
CHECK_THAT(actual, RGBSimilarTo(wxImage("image/paste_result_background_plus_circle_plus_square.png"), 1));
CHECK_THAT(actual, CenterAlphaPixelEquals(wxALPHA_OPAQUE));
}
SECTION("Paste two semi transparent images together first, then on top of background")
{
wxImage circle = transparent_circle.Copy();
wxImage actual = background.Copy();
circle.Paste(transparent_square, 0, 0, wxIMAGE_ALPHA_BLEND_COMPOSE);
actual.Paste(circle, 0, 0, wxIMAGE_ALPHA_BLEND_COMPOSE);
// When applied in this order, two times a rounding difference is triggered.
CHECK_THAT(actual, RGBSimilarTo(wxImage("image/paste_result_background_plus_circle_plus_square.png"), 2));
CHECK_THAT(actual, CenterAlphaPixelEquals(wxALPHA_OPAQUE));
}
SECTION("Paste semitransparent image over transparent image")
{
wxImage actual(transparent_circle.GetSize());
actual.InitAlpha();
memset(actual.GetAlpha(), 0, actual.GetWidth() * actual.GetHeight());
actual.Paste(transparent_circle, 0, 0, wxIMAGE_ALPHA_BLEND_COMPOSE);
CHECK_THAT(actual, CenterAlphaPixelEquals(192));
actual.Paste(transparent_square, 0, 0, wxIMAGE_ALPHA_BLEND_COMPOSE);
CHECK_THAT(actual, RGBSimilarTo(wxImage("image/paste_result_no_background_square_over_circle.png"), 1));
CHECK_THAT(actual, CenterAlphaPixelEquals(224));
}
}
/*
TODO: add lots of more tests to wxImage functions

Binary file not shown.

After

Width:  |  Height:  |  Size: 410 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 263 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 330 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 265 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 560 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -30,8 +30,9 @@ namespace Catch
class ImageRGBMatcher : public Catch::MatcherBase<wxImage>
{
public:
ImageRGBMatcher(const wxImage& image)
ImageRGBMatcher(const wxImage& image, int tolerance)
: m_image(image)
, m_tolerance(tolerance)
{
}
@ -53,7 +54,8 @@ public:
{
for ( int y = 0; y < m_image.GetHeight(); ++y )
{
if ( *d1 != *d2 )
const unsigned char diff = *d1 > * d2 ? *d1 - *d2 : *d2 - *d1;
if (diff > m_tolerance)
{
m_diffDesc.Printf
(
@ -72,11 +74,11 @@ public:
}
}
// We should never get here as we know that the images are different
// and so should have returned from inside the loop above.
wxFAIL_MSG("unreachable");
// We can only get here when the images are different AND we've not exited the
// method from the loop. That implies the tolerance must have caused this.
wxASSERT_MSG(m_tolerance > 0, "Unreachable without tolerance");
return false;
return true;
}
std::string describe() const wxOVERRIDE
@ -92,12 +94,67 @@ public:
private:
const wxImage m_image;
const int m_tolerance;
mutable wxString m_diffDesc;
};
inline ImageRGBMatcher RGBSameAs(const wxImage& image)
{
return ImageRGBMatcher(image);
return ImageRGBMatcher(image, 0);
}
// Allows small differences (within given tolerance) for r, g, and b values.
inline ImageRGBMatcher RGBSimilarTo(const wxImage& image, int tolerance)
{
return ImageRGBMatcher(image, tolerance);
}
class ImageAlphaMatcher : public Catch::MatcherBase<wxImage>
{
public:
ImageAlphaMatcher(unsigned char alpha)
: m_alpha(alpha)
{
}
bool match(const wxImage& other) const wxOVERRIDE
{
if (!other.HasAlpha())
{
m_diffDesc = "no alpha data";
return false;
}
unsigned char center_alpha =
*(other.GetAlpha() + (other.GetWidth() / 2) + (other.GetHeight() / 2 * other.GetWidth()));
if (m_alpha != center_alpha)
{
m_diffDesc.Printf("got alpha %u", center_alpha);
return false;
}
return true;
}
std::string describe() const wxOVERRIDE
{
std::string desc;
if (!m_diffDesc.empty())
desc = m_diffDesc.ToStdString(wxConvUTF8);
return desc;
}
private:
const unsigned char m_alpha;
mutable wxString m_diffDesc;
};
inline ImageAlphaMatcher CenterAlphaPixelEquals(unsigned char alpha)
{
return ImageAlphaMatcher(alpha);
}
#endif // _WX_TESTS_TESTIMAGE_H_