Repository: python-pillow/Pillow

Issue:
Suggestion: option to pad instead of crop in ImageOps.fit
I recently wanted to use Pillow to generate thumbnails of a directory of images. I wanted each thumbnail to be exactly 300x200, but preserve the aspect ratio of the original image by padding one of the dimensions.

I thought [`PIL.ImageOps.fit`](https://pillow.readthedocs.io/en/latest/reference/ImageOps.html?highlight=ImageOps#PIL.ImageOps.fit) might do exactly what I wanted, but it crops rather than padding.

Would there be interest in an option to pad rather than crop? Or another method? I'd be happy to contribute if so.

PIL 1.1.7, Python 3.6.5


Discussion:
Here is another method -
```python
from PIL import Image
im = Image.open("test.png")
w, h = 300, 200
ratio = im.width / im.height
if ratio == w / h:
	out = im.resize((w, h))
else:
	out = Image.new(im.mode, (w, h))
	if ratio > w / h:
		new_h = int(im.height / im.width * w)
		im = im.resize((w, new_h))
		out.paste(im, (0, int((h - new_h) / 2)))
	else:
		new_w = int(im.width / im.height * h)
		im = im.resize((new_w, h))
		out.paste(im, (int((w - new_w) / 2), 0))
```
Yep, that's a way to do it. My proposal would be to either fold that in to `PIL.ImageOps.fit` or make it another method on `ImageOps`.
Oh, apologies - when you said 'Or another method?', I interpreted that differently.
I have created PR #3364 to add a method for 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
@@ -221,6 +221,50 @@ def colorize(image, black, white, mid=None, blackpoint=0,
     return _lut(image, red + green + blue)
 
 
+def pad(image, size, method=Image.NEAREST, color=None, centering=(0.5, 0.5)):
+    """
+    Returns a sized and padded version of the image, expanded to fill the
+    requested aspect ratio and size.
+
+    :param image: The image to size and crop.
+    :param size: The requested output size in pixels, given as a
+                 (width, height) tuple.
+    :param method: What resampling method to use. Default is
+                   :py:attr:`PIL.Image.NEAREST`.
+    :param color: The background color of the padded image.
+    :param centering: Control the position of the original image within the
+                      padded version.
+                          (0.5, 0.5) will keep the image centered
+                          (0, 0) will keep the image aligned to the top left
+                          (1, 1) will keep the image aligned to the bottom
+                          right
+    :return: An image.
+    """
+
+    im_ratio = image.width / image.height
+    dest_ratio = float(size[0]) / size[1]
+
+    if im_ratio == dest_ratio:
+        out = image.resize(size, resample=method)
+    else:
+        out = Image.new(image.mode, size, color)
+        if im_ratio > dest_ratio:
+            new_height = int(image.height / image.width * size[0])
+            if new_height != size[1]:
+                image = image.resize((size[0], new_height), resample=method)
+
+            y = int((size[1] - new_height) * max(0, min(centering[1], 1)))
+            out.paste(image, (0, y))
+        else:
+            new_width = int(image.width / image.height * size[1])
+            if new_width != size[0]:
+                image = image.resize((new_width, size[1]), resample=method)
+
+            x = int((size[0] - new_width) * max(0, min(centering[0], 1)))
+            out.paste(image, (x, 0))
+    return out
+
+
 def crop(image, border=0):
     """
     Remove border from image.  The same amount of pixels are removed


Test patch:
diff --git a/Tests/images/imageops_pad_h_0.jpg b/Tests/images/imageops_pad_h_0.jpg
new file mode 100644
Binary files /dev/null and b/Tests/images/imageops_pad_h_0.jpg differ
diff --git a/Tests/images/imageops_pad_h_1.jpg b/Tests/images/imageops_pad_h_1.jpg
new file mode 100644
Binary files /dev/null and b/Tests/images/imageops_pad_h_1.jpg differ
diff --git a/Tests/images/imageops_pad_h_2.jpg b/Tests/images/imageops_pad_h_2.jpg
new file mode 100644
Binary files /dev/null and b/Tests/images/imageops_pad_h_2.jpg differ
diff --git a/Tests/images/imageops_pad_v_0.jpg b/Tests/images/imageops_pad_v_0.jpg
new file mode 100644
Binary files /dev/null and b/Tests/images/imageops_pad_v_0.jpg differ
diff --git a/Tests/images/imageops_pad_v_1.jpg b/Tests/images/imageops_pad_v_1.jpg
new file mode 100644
Binary files /dev/null and b/Tests/images/imageops_pad_v_1.jpg differ
diff --git a/Tests/images/imageops_pad_v_2.jpg b/Tests/images/imageops_pad_v_2.jpg
new file mode 100644
Binary files /dev/null and b/Tests/images/imageops_pad_v_2.jpg differ
diff --git a/Tests/test_imageops.py b/Tests/test_imageops.py
--- a/Tests/test_imageops.py
+++ b/Tests/test_imageops.py
@@ -24,6 +24,9 @@ def test_sanity(self):
         ImageOps.colorize(hopper("L"), (0, 0, 0), (255, 255, 255))
         ImageOps.colorize(hopper("L"), "black", "white")
 
+        ImageOps.pad(hopper("L"), (128, 128))
+        ImageOps.pad(hopper("RGB"), (128, 128))
+
         ImageOps.crop(hopper("L"), 1)
         ImageOps.crop(hopper("RGB"), 1)
 
@@ -70,6 +73,27 @@ def test_1pxfit(self):
         newimg = ImageOps.fit(hopper("RGB").resize((100, 1)), (35, 35))
         self.assertEqual(newimg.size, (35, 35))
 
+    def test_pad(self):
+        # Same ratio
+        im = hopper()
+        new_size = (im.width * 2, im.height * 2)
+        new_im = ImageOps.pad(im, new_size)
+        self.assertEqual(new_im.size, new_size)
+
+        for label, color, new_size in [
+            ("h", None, (im.width * 4, im.height * 2)),
+            ("v", "#f00", (im.width * 2, im.height * 4))
+        ]:
+            for i, centering in enumerate([(0,0), (0.5, 0.5), (1, 1)]):
+                new_im = ImageOps.pad(im, new_size,
+                                      color=color, centering=centering)
+                self.assertEqual(new_im.size, new_size)
+
+                target = Image.open(
+                    "Tests/images/imageops_pad_"+label+"_"+str(i)+".jpg")
+                self.assert_image_similar(new_im, target, 6)
+
+
     def test_pil163(self):
         # Division by zero in equalize if < 255 pixels in image (@PIL163)