diff --git a/include/wx/image.h b/include/wx/image.h index d53ff0b116..f73fa70fab 100644 --- a/include/wx/image.h +++ b/include/wx/image.h @@ -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, diff --git a/interface/wx/image.h b/interface/wx/image.h index 57bee46b41..a2c5b25f98 100644 --- a/interface/wx/image.h +++ b/interface/wx/image.h @@ -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. diff --git a/src/common/image.cpp b/src/common/image.cpp index 880c1c4ea7..8d935a1cfb 100644 --- a/src/common/image.cpp +++ b/src/common/image.cpp @@ -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(); diff --git a/tests/image/image.cpp b/tests/image/image.cpp index f76e79162c..aa67d7678f 100644 --- a/tests/image/image.cpp +++ b/tests/image/image.cpp @@ -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 diff --git a/tests/image/paste_input_background.png b/tests/image/paste_input_background.png new file mode 100644 index 0000000000..fd93b550ce Binary files /dev/null and b/tests/image/paste_input_background.png differ diff --git a/tests/image/paste_input_overlay_transparent_border_opaque_square.png b/tests/image/paste_input_overlay_transparent_border_opaque_square.png new file mode 100644 index 0000000000..fa32073e5c Binary files /dev/null and b/tests/image/paste_input_overlay_transparent_border_opaque_square.png differ diff --git a/tests/image/paste_input_overlay_transparent_border_semitransparent_circle.png b/tests/image/paste_input_overlay_transparent_border_semitransparent_circle.png new file mode 100644 index 0000000000..af6d42ffbd Binary files /dev/null and b/tests/image/paste_input_overlay_transparent_border_semitransparent_circle.png differ diff --git a/tests/image/paste_input_overlay_transparent_border_semitransparent_square.png b/tests/image/paste_input_overlay_transparent_border_semitransparent_square.png new file mode 100644 index 0000000000..d609cd5a45 Binary files /dev/null and b/tests/image/paste_input_overlay_transparent_border_semitransparent_square.png differ diff --git a/tests/image/paste_result_background_plus_circle_plus_square.png b/tests/image/paste_result_background_plus_circle_plus_square.png new file mode 100644 index 0000000000..20af553c7d Binary files /dev/null and b/tests/image/paste_result_background_plus_circle_plus_square.png differ diff --git a/tests/image/paste_result_background_plus_overlay_transparent_border_opaque_square.png b/tests/image/paste_result_background_plus_overlay_transparent_border_opaque_square.png new file mode 100644 index 0000000000..60da07730d Binary files /dev/null and b/tests/image/paste_result_background_plus_overlay_transparent_border_opaque_square.png differ diff --git a/tests/image/paste_result_background_plus_overlay_transparent_border_semitransparent_square.png b/tests/image/paste_result_background_plus_overlay_transparent_border_semitransparent_square.png new file mode 100644 index 0000000000..a6e6fef28e Binary files /dev/null and b/tests/image/paste_result_background_plus_overlay_transparent_border_semitransparent_square.png differ diff --git a/tests/image/paste_result_no_background_square_over_circle.png b/tests/image/paste_result_no_background_square_over_circle.png new file mode 100644 index 0000000000..d27564a130 Binary files /dev/null and b/tests/image/paste_result_no_background_square_over_circle.png differ diff --git a/tests/testimage.h b/tests/testimage.h index e922e36d63..f4f20d8ddc 100644 --- a/tests/testimage.h +++ b/tests/testimage.h @@ -30,8 +30,9 @@ namespace Catch class ImageRGBMatcher : public Catch::MatcherBase { 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 +{ +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_