Repository: python-pillow/Pillow

Issue:
ValueError: box offset can't be negative
### What did you do?
I wanted to transform a PNG file using `PIL.ImageOps.fit()`.

### What did you expect to happen?
I expected to get back the transformed image.

### What actually happened?
```
Traceback (most recent call last):
  File "example.py", line 6, in <module>
    PIL.ImageOps.fit(img, (600, 453), PIL.Image.ANTIALIAS)
  File ".../lib/python3.7/site-packages/PIL/ImageOps.py", line 445, in fit
    return image.resize(size, method, box=crop)
  File ".../lib/python3.7/site-packages/PIL/Image.py", line 1892, in resize
    return self._new(self.im.resize(size, resample, box))
ValueError: box offset can't be negative
```

This exception seems to be dependent on the size of the image `(1000, 755)` and the target size `(600, 453)`. In this case the value for `box` gets `(-5.684341886080802e-14, 0.0, 1000.0, 755.0)` which leads to the exception. (The attached PNG is not the original one where the problem happened in production but an arbitrarily constructed one having the same dimensions.)

### What are your OS, Python and Pillow versions?

* OS: MacOS
* Python: 2.7 and 3.7
* Pillow: 6.1.0



```python
import PIL.Image
import PIL.ImageOps

with open('example.png', 'rb') as reader:
    img = PIL.Image.open(reader)
    PIL.ImageOps.fit(img, (600, 453), PIL.Image.ANTIALIAS)
```

![example](https://user-images.githubusercontent.com/386619/65522942-d2fd3080-deeb-11e9-9544-fadf5ec7422c.png)



Discussion:
It started in Pillow 5.4.0. Git bisecting between 5.3.0 and 5.4.0 gives:

```
73eec9000d5fa0e88786a560882fba921cfc42df is the first bad commit
commit 73eec9000d5fa0e88786a560882fba921cfc42df
Date:   Wed Oct 17 01:57:55 2018 +0300

    Optimise ImageOps.fit by combining resize in crop

:040000 040000 d0967f545f67423f334dd2725678fc213432e780 76675c8bf4e9c6520b739e31b4df4d4f143a0632 M      src
bisect run success
```

Commit 73eec9000d5fa0e88786a560882fba921cfc42df is from PR https://github.com/python-pillow/Pillow/pull/3409.

I can reproduce the error simply with
```python
from PIL import Image, ImageOps
img = Image.new("RGB", (1000, 755))
ImageOps.fit(img, (1000, 755))
```

The ratio for this image is 1000.0 / 755 = 1.3245033112582782. Pillow currently treats the case where the ratios are equal the same as the case where the width needs to be cropped - https://github.com/python-pillow/Pillow/blob/0118a8fdb05b144f79ef48e5760dd7054f842811/src/PIL/ImageOps.py#L429

So calculating the width from the ratio gives 1.3245033112582782 * 755 = 1000.0000000000001. Pillow then tries to center this greater width, causing a negative x offset when cropping, leading to the error.

I've created PR #4088 to resolve this.

Resolving patch:
diff --git a/src/PIL/ImageOps.py b/src/PIL/ImageOps.py
--- a/src/PIL/ImageOps.py
+++ b/src/PIL/ImageOps.py
@@ -426,7 +426,11 @@ def fit(image, size, method=Image.NEAREST, bleed=0.0, centering=(0.5, 0.5)):
     output_ratio = float(size[0]) / size[1]
 
     # figure out if the sides or top/bottom will be cropped off
-    if live_size_ratio >= output_ratio:
+    if live_size_ratio == output_ratio:
+        # live_size is already the needed ratio
+        crop_width = live_size[0]
+        crop_height = live_size[1]
+    elif live_size_ratio >= output_ratio:
         # live_size is wider than what's needed, crop the sides
         crop_width = output_ratio * live_size[1]
         crop_height = live_size[1]


Test patch:
diff --git a/Tests/test_imageops.py b/Tests/test_imageops.py
--- a/Tests/test_imageops.py
+++ b/Tests/test_imageops.py
@@ -81,6 +81,16 @@ def test_1pxfit(self):
         newimg = ImageOps.fit(hopper("RGB").resize((100, 1)), (35, 35))
         self.assertEqual(newimg.size, (35, 35))
 
+    def test_fit_same_ratio(self):
+        # The ratio for this image is 1000.0 / 755 = 1.3245033112582782
+        # If the ratios are not acknowledged to be the same,
+        # and Pillow attempts to adjust the width to
+        # 1.3245033112582782 * 755 = 1000.0000000000001
+        # then centering this greater width causes a negative x offset when cropping
+        with Image.new("RGB", (1000, 755)) as im:
+            new_im = ImageOps.fit(im, (1000, 755))
+            self.assertEqual(new_im.size, (1000, 755))
+
     def test_pad(self):
         # Same ratio
         im = hopper()