Repository: python-pillow/Pillow

Issue:
alpha_composite should be like paste and allow negative destinations
If you try to use `alpha_composite` with a negative dest, it produces an error message. For example:

```python
from PIL import Image
solid_green = Image.new("RGBA",(100,100),(0,255,0,255))
semidark = Image.new("RGBA",(100,100),(0,0,0,127))

ac_result = solid_green.copy()
ac_result.alpha_composite(semidark,(-50,50))
ac_result.show()
# => ValueError: Destination must be non-negative
```

Contrast this with `paste,` which allows negative destinations:

```python
from PIL import Image
solid_green = Image.new("RGBA",(100,100),(0,255,0,255))
semidark = Image.new("RGBA",(100,100),(0,0,0,127))

paste_result = solid_green.copy()
paste_result.paste(semidark,(-50,50),semidark)
paste_result.show() # Works but colors aren't alpha composited
```

Negatives do indicate clipping on the left or top but note that `alpha_composite` (like `paste`) already allows clipping on the right and bottom.

```python
from PIL import Image
solid_green = Image.new("RGBA",(100,100),(0,255,0,255))
semidark = Image.new("RGBA",(100,100),(0,0,0,127))

ac_result = solid_green.copy()
ac_result.alpha_composite(semidark,(50,50))
ac_result.show() # Works great!
```

Until this issue is addressed, here is a work around:

```python
def alpha_composite_workaround(image0, image1, dest):
    x1,y1 = dest
    if x1 >= 0 and y1 >= 0:
        image0.alpha_composite(image1, dest=(x1, y1))
    else:
        temp = Image.new("RGBA", image0.size, (0, 0, 0, 0))
        temp.paste(image1, (x1, y1))
        image0.alpha_composite(temp)

from PIL import Image
solid_green = Image.new("RGBA",(100,100),(0,255,0,255))
semidark = Image.new("RGBA",(100,100),(0,0,0,127))

ac_result = solid_green.copy()
alpha_composite_workaround(ac_result,semidark,(-50,50))
ac_result.show()
```


Discussion:
`alpha_composite` was added in #2595 by @wiredfool. Did he have any thoughts on this?

Resolving patch:
diff --git a/src/PIL/Image.py b/src/PIL/Image.py
--- a/src/PIL/Image.py
+++ b/src/PIL/Image.py
@@ -1544,8 +1544,6 @@ def alpha_composite(self, im, dest=(0, 0), source=(0, 0)):
             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


Test patch:
diff --git a/Tests/test_image.py b/Tests/test_image.py
--- a/Tests/test_image.py
+++ b/Tests/test_image.py
@@ -344,6 +344,12 @@ def test_alpha_inplace(self):
         assert_image_equal(offset.crop((64, 64, 127, 127)), target.crop((0, 0, 63, 63)))
         assert offset.size == (128, 128)
 
+        # with negative offset
+        offset = src.copy()
+        offset.alpha_composite(over, (-64, -64))
+        assert_image_equal(offset.crop((0, 0, 63, 63)), target.crop((64, 64, 127, 127)))
+        assert offset.size == (128, 128)
+
         # offset and crop
         box = src.copy()
         box.alpha_composite(over, (64, 64), (0, 0, 32, 32))
@@ -367,8 +373,6 @@ def test_alpha_inplace(self):
             source.alpha_composite(over, 0)
         with pytest.raises(ValueError):
             source.alpha_composite(over, (0, 0), 0)
-        with pytest.raises(ValueError):
-            source.alpha_composite(over, (0, -1))
         with pytest.raises(ValueError):
             source.alpha_composite(over, (0, 0), (0, -1))