Repository: python-pillow/Pillow

Issue:
Function similar to pad but returns resized image without padding
I'd sure like a function that looks like ImageOps.pad
https://pillow.readthedocs.io/en/stable/_modules/PIL/ImageOps.html#pad

but just returns the image after resizing without adding the padding.

What would be a good name for such a function? Where would it best be placed?

ImageOps.fit is already taken.

EDIT: Looking at #3364 `contain` seems like a decent candidate. Seems a refactor would be required. @radarhere thoughts?



Discussion:

Resolving patch:
diff --git a/src/PIL/ImageOps.py b/src/PIL/ImageOps.py
--- a/src/PIL/ImageOps.py
+++ b/src/PIL/ImageOps.py
@@ -236,15 +236,43 @@ def colorize(image, black, white, mid=None, blackpoint=0, whitepoint=255, midpoi
     return _lut(image, red + green + blue)
 
 
+def contain(image, size, method=Image.BICUBIC):
+    """
+    Returns a resized version of the image, set to the maximum width and height
+    within the requested size, while maintaining the original aspect ratio.
+
+    :param image: The image to resize and crop.
+    :param size: The requested output size in pixels, given as a
+                 (width, height) tuple.
+    :param method: Resampling method to use. Default is
+                   :py:attr:`PIL.Image.BICUBIC`. See :ref:`concept-filters`.
+    :return: An image.
+    """
+
+    im_ratio = image.width / image.height
+    dest_ratio = size[0] / size[1]
+
+    if im_ratio != dest_ratio:
+        if im_ratio > dest_ratio:
+            new_height = int(image.height / image.width * size[0])
+            if new_height != size[1]:
+                size = (size[0], new_height)
+        else:
+            new_width = int(image.width / image.height * size[1])
+            if new_width != size[0]:
+                size = (new_width, size[1])
+    return image.resize(size, resample=method)
+
+
 def pad(image, size, method=Image.BICUBIC, color=None, centering=(0.5, 0.5)):
     """
-    Returns a sized and padded version of the image, expanded to fill the
+    Returns a resized and padded version of the image, expanded to fill the
     requested aspect ratio and size.
 
-    :param image: The image to size and crop.
+    :param image: The image to resize 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
+    :param method: Resampling method to use. Default is
                    :py:attr:`PIL.Image.BICUBIC`. See :ref:`concept-filters`.
     :param color: The background color of the padded image.
     :param centering: Control the position of the original image within the
@@ -257,27 +285,17 @@ def pad(image, size, method=Image.BICUBIC, color=None, centering=(0.5, 0.5)):
     :return: An image.
     """
 
-    im_ratio = image.width / image.height
-    dest_ratio = size[0] / size[1]
-
-    if im_ratio == dest_ratio:
-        out = image.resize(size, resample=method)
+    resized = contain(image, size, method)
+    if resized.size == size:
+        out = resized
     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))
+        if resized.width != size[0]:
+            x = int((size[0] - resized.width) * max(0, min(centering[0], 1)))
+            out.paste(resized, (x, 0))
         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))
+            y = int((size[1] - resized.height) * max(0, min(centering[1], 1)))
+            out.paste(resized, (0, y))
     return out
 
 
@@ -304,7 +322,7 @@ def scale(image, factor, resample=Image.BICUBIC):
 
     :param image: The image to rescale.
     :param factor: The expansion factor, as a float.
-    :param resample: What resampling method to use. Default is
+    :param resample: Resampling method to use. Default is
                      :py:attr:`PIL.Image.BICUBIC`. See :ref:`concept-filters`.
     :returns: An :py:class:`~PIL.Image.Image` object.
     """
@@ -381,15 +399,15 @@ def expand(image, border=0, fill=0):
 
 def fit(image, size, method=Image.BICUBIC, bleed=0.0, centering=(0.5, 0.5)):
     """
-    Returns a sized and cropped version of the image, cropped to the
+    Returns a resized and cropped version of the image, cropped to the
     requested aspect ratio and size.
 
     This function was contributed by Kevin Cazabon.
 
-    :param image: The image to size and crop.
+    :param image: The image to resize 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
+    :param method: Resampling method to use. Default is
                    :py:attr:`PIL.Image.BICUBIC`. See :ref:`concept-filters`.
     :param bleed: Remove a border around the outside of the image from all
                   four edges. The value is a decimal percentage (use 0.01 for


Test patch:
diff --git a/Tests/test_imageops.py b/Tests/test_imageops.py
--- a/Tests/test_imageops.py
+++ b/Tests/test_imageops.py
@@ -37,6 +37,9 @@ def test_sanity():
     ImageOps.pad(hopper("L"), (128, 128))
     ImageOps.pad(hopper("RGB"), (128, 128))
 
+    ImageOps.contain(hopper("L"), (128, 128))
+    ImageOps.contain(hopper("RGB"), (128, 128))
+
     ImageOps.crop(hopper("L"), 1)
     ImageOps.crop(hopper("RGB"), 1)
 
@@ -99,6 +102,13 @@ def test_fit_same_ratio():
         assert new_im.size == (1000, 755)
 
 
+@pytest.mark.parametrize("new_size", ((256, 256), (512, 256), (256, 512)))
+def test_contain(new_size):
+    im = hopper()
+    new_im = ImageOps.contain(im, new_size)
+    assert new_im.size == (256, 256)
+
+
 def test_pad():
     # Same ratio
     im = hopper()