diff --git a/PIL/Image.py b/PIL/Image.py index 6ebff771d..dd97c26bd 100644 --- a/PIL/Image.py +++ b/PIL/Image.py @@ -1376,6 +1376,54 @@ class Image(object): else: self.im.paste(im, box) + def alpha_composite(self, im, dest=(0,0), source=(0,0)): + """ 'In-place' analog of Image.alpha_composite. Composites an image + onto this image. + + :param im: image to composite over this one + :param dest: Optional 2 tuple (top, left) specifying the upper + left corner in this (destination) image. + :param source: Optional 2 (top, left) tuple for the upper left + corner in the overlay source image, or 4 tuple (top, left, bottom, + right) for the bounds of the source rectangle + + Performance Note: Not currently implemented in-place in the core layer. + """ + + if not isinstance(source, tuple): + raise ValueError("Source must be a tuple") + if not isinstance(dest, tuple): + raise ValueError("Destination must be a tuple") + if not len(source) in (2, 4): + raise ValueError("Source must be a 2 or 4-tuple") + if not len(dest) == 2: + raise ValueError("Destination must be a 2-tuple") + if min(source) < 0: + raise ValueError("Source must be non-negative") + if min(dest) < 0: + raise ValueError("Destination must be non-negative") + + if len(source) == 2: + source = source + im.size + + # over image, crop if it's not the whole thing. + if source == (0,0) + im.size: + overlay = im + else: + overlay = im.crop(source) + + # target for the paste + box = dest + (dest[0] + overlay.width, dest[1] + overlay.height) + + # destination image. don't copy if we're using the whole image. + if dest == (0,0) + self.size: + background = self + else: + background = self.crop(box) + + result = alpha_composite(background, overlay) + self.paste(result, box) + def point(self, lut, mode=None): """ Maps this image through a lookup table or function. diff --git a/Tests/test_image.py b/Tests/test_image.py index ea813cd84..1f9c4d798 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -202,6 +202,44 @@ class TestImage(PillowTestCase): img_colors = sorted(img.getcolors()) self.assertEqual(img_colors, expected_colors) + def test_alpha_inplace(self): + src = Image.new('RGBA', (128,128), 'blue') + + over = Image.new('RGBA', (128,128), 'red') + mask = hopper('L') + over.putalpha(mask) + + target = Image.alpha_composite(src, over) + + # basic + full = src.copy() + full.alpha_composite(over) + self.assert_image_equal(full, target) + + # with offset down to right + offset = src.copy() + offset.alpha_composite(over, (64, 64)) + self.assert_image_equal(offset.crop((64, 64, 127, 127)), + target.crop((0, 0, 63, 63))) + self.assertEqual(offset.size, (128, 128)) + + # offset and crop + box = src.copy() + box.alpha_composite(over, (64, 64), (0, 0, 32, 32)) + self.assert_image_equal(box.crop((64, 64, 96, 96)), + target.crop((0, 0, 32, 32))) + self.assert_image_equal(box.crop((96, 96, 128, 128)), + src.crop((0, 0, 32, 32))) + self.assertEqual(box.size, (128, 128)) + + # source point + source = src.copy() + source.alpha_composite(over, (32, 32), (32, 32, 96, 96)) + + self.assert_image_equal(source.crop((32, 32, 96, 96)), + target.crop((32, 32, 96, 96))) + self.assertEqual(source.size, (128, 128)) + def test_registered_extensions_uninitialized(self): # Arrange Image._initialized = 0 diff --git a/docs/reference/Image.rst b/docs/reference/Image.rst index 6dcc816e5..3335cb3da 100644 --- a/docs/reference/Image.rst +++ b/docs/reference/Image.rst @@ -104,6 +104,8 @@ An instance of the :py:class:`~PIL.Image.Image` class has the following methods. Unless otherwise stated, all methods return a new instance of the :py:class:`~PIL.Image.Image` class, holding the resulting image. + +.. automethod:: PIL.Image.Image.alpha_composite .. automethod:: PIL.Image.Image.convert The following example converts an RGB image (linearly calibrated according to