Skip to content

Commit

Permalink
Merge pull request #5417 from radarhere/contain
Browse files Browse the repository at this point in the history
Added ImageOps contain()
  • Loading branch information
hugovk committed May 1, 2021
2 parents 676f4db + c062410 commit 8a8ac60
Show file tree
Hide file tree
Showing 5 changed files with 119 additions and 25 deletions.
10 changes: 10 additions & 0 deletions Tests/test_imageops.py
Expand Up @@ -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)

Expand Down Expand Up @@ -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()
Expand Down
1 change: 1 addition & 0 deletions docs/reference/ImageOps.rst
Expand Up @@ -12,6 +12,7 @@ only work on L and RGB images.

.. autofunction:: autocontrast
.. autofunction:: colorize
.. autofunction:: contain
.. autofunction:: pad
.. autofunction:: crop
.. autofunction:: scale
Expand Down
64 changes: 64 additions & 0 deletions docs/releasenotes/8.3.0.rst
@@ -0,0 +1,64 @@
8.3.0
-----

Deprecations
============

TODO
^^^^

TODO

API Changes
===========

Changed WebP default "method" value when saving
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Previously, it was 0, for the best speed. The default has now been changed to 4, to
match WebP's default, for higher quality with still some speed optimisation.

Default resampling filter for special image modes
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Pillow 7.0 changed the default resampling filter to ``Image.BICUBIC``. However, as this
is not supported yet for images with a custom number of bits, the default filter for
those modes has been reverted to ``Image.NEAREST``.

ImageMorph incorrect mode errors
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

For ``apply()``, ``match()`` and ``get_on_pixels()``, if the image mode is not L, an
:py:exc:`Exception` was thrown. This has now been changed to a :py:exc:`ValueError`.

API Additions
=============

ImageOps.contain
^^^^^^^^^^^^^^^^

Returns a resized version of the image, set to the maximum width and height within
``size``, while maintaining the original aspect ratio.

To compare it to other ImageOps methods:

- :py:meth:`~PIL.ImageOps.fit` expands an image until is fills ``size``, cropping the
parts of the image that do not fit.
- :py:meth:`~PIL.ImageOps.pad` expands an image to fill ``size``, without cropping, but
instead filling the extra space with ``color``.
- :py:meth:`~PIL.ImageOps.contain` is similar to :py:meth:`~PIL.ImageOps.pad`, but it
does not fill the extra space. Instead, the original aspect ratio is maintained. So
unlike the other two methods, it is not guaranteed to return an image of ``size``.

Security
========

TODO

Other Changes
=============

TODO
^^^^

TODO
1 change: 1 addition & 0 deletions docs/releasenotes/index.rst
Expand Up @@ -14,6 +14,7 @@ expected to be backported to earlier versions.
.. toctree::
:maxdepth: 2

8.3.0
8.2.0
8.1.2
8.1.1
Expand Down
68 changes: 43 additions & 25 deletions src/PIL/ImageOps.py
Expand Up @@ -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
Expand All @@ -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


Expand All @@ -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.
"""
Expand Down Expand Up @@ -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
Expand Down

0 comments on commit 8a8ac60

Please sign in to comment.