diff --git a/CHANGES.rst b/CHANGES.rst index b4834a8da..c1eab0c2e 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 7.0.0 (unreleased) ------------------ +- Better thumbnail aspect ratio preservation #4256 + [homm] + - Add La mode packing and unpacking #4248 [homm] diff --git a/Tests/images/imageops_pad_h_0.jpg b/Tests/images/imageops_pad_h_0.jpg index f9fcb1cdb..7afbbb96a 100644 Binary files a/Tests/images/imageops_pad_h_0.jpg 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 index 4b9b9ebc4..b9bf8a49a 100644 Binary files a/Tests/images/imageops_pad_h_1.jpg 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 index 2c8224892..7e0eb9599 100644 Binary files a/Tests/images/imageops_pad_h_2.jpg 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 index caf435796..73a96c86c 100644 Binary files a/Tests/images/imageops_pad_v_0.jpg 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 index 4a6698e91..04545f817 100644 Binary files a/Tests/images/imageops_pad_v_1.jpg 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 index 792952bcd..f3e399d7b 100644 Binary files a/Tests/images/imageops_pad_v_2.jpg and b/Tests/images/imageops_pad_v_2.jpg differ diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index b17459a8b..bbd589ada 100644 --- a/Tests/test_file_gif.py +++ b/Tests/test_file_gif.py @@ -754,7 +754,7 @@ class TestFileGif(PillowTestCase): def test_getdata(self): # test getheader/getdata against legacy values # Create a 'P' image with holes in the palette - im = Image._wedge().resize((16, 16)) + im = Image._wedge().resize((16, 16), Image.NEAREST) im.putpalette(ImagePalette.ImagePalette("RGB")) im.info = {"background": 0} diff --git a/Tests/test_image_getdata.py b/Tests/test_image_getdata.py index d9bfcc7dd..18d381390 100644 --- a/Tests/test_image_getdata.py +++ b/Tests/test_image_getdata.py @@ -1,3 +1,5 @@ +from PIL import Image + from .helper import PillowTestCase, hopper @@ -13,7 +15,7 @@ class TestImageGetData(PillowTestCase): def test_roundtrip(self): def getdata(mode): - im = hopper(mode).resize((32, 30)) + im = hopper(mode).resize((32, 30), Image.NEAREST) data = im.getdata() return data[0], len(data), len(list(data)) diff --git a/Tests/test_image_resample.py b/Tests/test_image_resample.py index 3bb941438..e94fbfe94 100644 --- a/Tests/test_image_resample.py +++ b/Tests/test_image_resample.py @@ -212,6 +212,11 @@ class TestImagingCoreResampleAccuracy(PillowTestCase): for channel in case.split(): self.check_case(channel, self.make_sample(data, (12, 12))) + def test_box_filter_correct_range(self): + im = Image.new("RGB", (8, 8), "#1688ff").resize((100, 100), Image.BOX) + ref = Image.new("RGB", (100, 100), "#1688ff") + self.assert_image_equal(im, ref) + class CoreResampleConsistencyTest(PillowTestCase): def make_case(self, mode, fill): diff --git a/Tests/test_image_resize.py b/Tests/test_image_resize.py index 2538dd378..091655827 100644 --- a/Tests/test_image_resize.py +++ b/Tests/test_image_resize.py @@ -150,3 +150,12 @@ class TestImageResize(PillowTestCase): # Test unknown resampling filter with hopper() as im: self.assertRaises(ValueError, im.resize, (10, 10), "unknown") + + def test_default_filter(self): + for mode in "L", "RGB", "I", "F": + im = hopper(mode) + self.assertEqual(im.resize((20, 20), Image.BICUBIC), im.resize((20, 20))) + + for mode in "1", "P": + im = hopper(mode) + self.assertEqual(im.resize((20, 20), Image.NEAREST), im.resize((20, 20))) diff --git a/Tests/test_image_thumbnail.py b/Tests/test_image_thumbnail.py index 0f2955574..e1857207b 100644 --- a/Tests/test_image_thumbnail.py +++ b/Tests/test_image_thumbnail.py @@ -43,6 +43,11 @@ class TestImageThumbnail(PillowTestCase): im.thumbnail((33, 33)) self.assertEqual(im.size, (21, 33)) # ratio is 0.6363636364 + def test_float(self): + im = Image.new("L", (128, 128)) + im.thumbnail((99.9, 99.9)) + self.assertEqual(im.size, (100, 100)) + def test_no_resize(self): # Check that draft() can resize the image to the destination size with Image.open("Tests/images/hopper.jpg") as im: diff --git a/Tests/test_imagedraw.py b/Tests/test_imagedraw.py index d3791e0b1..4535a4838 100644 --- a/Tests/test_imagedraw.py +++ b/Tests/test_imagedraw.py @@ -159,7 +159,8 @@ class TestImageDraw(PillowTestCase): # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) - with Image.open("Tests/images/pil123rgba.png").resize((50, 50)) as small: + with Image.open("Tests/images/pil123rgba.png") as small: + small = small.resize((50, 50), Image.NEAREST) # Act draw.bitmap((10, 10), small) diff --git a/Tests/test_imagefile.py b/Tests/test_imagefile.py index 50c7d337b..f575c8c1a 100644 --- a/Tests/test_imagefile.py +++ b/Tests/test_imagefile.py @@ -24,7 +24,7 @@ class TestImageFile(PillowTestCase): def test_parser(self): def roundtrip(format): - im = hopper("L").resize((1000, 1000)) + im = hopper("L").resize((1000, 1000), Image.NEAREST) if format in ("MSP", "XBM"): im = im.convert("1") diff --git a/docs/releasenotes/7.0.0.rst b/docs/releasenotes/7.0.0.rst index f0612351f..08933eaf8 100644 --- a/docs/releasenotes/7.0.0.rst +++ b/docs/releasenotes/7.0.0.rst @@ -47,6 +47,17 @@ Setting the size of TIFF images Setting the size of a TIFF image directly (eg. ``im.size = (256, 256)``) throws an error. Use ``Image.resize`` instead. +Default resampling filter +^^^^^^^^^^^^^^^^^^^^^^^^^ + +The default resampling filter has been changed to the high-quality convolution +``Image.BICUBIC`` instead of ``Image.NEAREST``, for the :py:meth:`~PIL.Image.Image.resize` +method and the :py:meth:`~PIL.ImageOps.pad``, :py:meth:`~PIL.ImageOps.scale`` +and :py:meth:`~PIL.ImageOps.fit`` functions. +``Image.NEAREST`` is still always used for images in "P" and "1" modes. +See :ref:`concept-filters` to learn the difference. In short, +``Image.NEAREST`` is a very fast filter, but simple and low-quality. + Image.draft() return value ^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -106,3 +117,9 @@ Use instead: with Image.open("hopper.png") as im: im.save("out.jpg") + +Better thumbnail aspect ratio preservation +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +When calculating the new dimensions in ``Image.thumbnail``, round to the +nearest integer, instead of always rounding down. diff --git a/src/PIL/Image.py b/src/PIL/Image.py index aad8246db..b17832635 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -1764,7 +1764,7 @@ class Image: return m_im - def resize(self, size, resample=NEAREST, box=None): + def resize(self, size, resample=BICUBIC, box=None): """ Returns a resized copy of this image. @@ -1774,8 +1774,9 @@ class Image: one of :py:attr:`PIL.Image.NEAREST`, :py:attr:`PIL.Image.BOX`, :py:attr:`PIL.Image.BILINEAR`, :py:attr:`PIL.Image.HAMMING`, :py:attr:`PIL.Image.BICUBIC` or :py:attr:`PIL.Image.LANCZOS`. - If omitted, or if the image has mode "1" or "P", it is - set :py:attr:`PIL.Image.NEAREST`. + Default filter is :py:attr:`PIL.Image.BICUBIC`. + If the image has mode "1" or "P", it is + always set to :py:attr:`PIL.Image.NEAREST`. See: :ref:`concept-filters`. :param box: An optional 4-tuple of floats giving the region of the source image which should be scaled. @@ -1845,7 +1846,7 @@ class Image: environment), or :py:attr:`PIL.Image.BICUBIC` (cubic spline interpolation in a 4x4 environment). If omitted, or if the image has mode "1" or "P", it is - set :py:attr:`PIL.Image.NEAREST`. See :ref:`concept-filters`. + set to :py:attr:`PIL.Image.NEAREST`. See :ref:`concept-filters`. :param expand: Optional expansion flag. If true, expands the output image to make it large enough to hold the entire rotated image. If false or omitted, make the output image the same size as the @@ -2141,10 +2142,10 @@ class Image: x, y = self.size if x > size[0]: y = max(round(y * size[0] / x), 1) - x = size[0] + x = round(size[0]) if y > size[1]: x = max(round(x * size[1] / y), 1) - y = size[1] + y = round(size[1]) size = x, y box = None diff --git a/src/PIL/ImageOps.py b/src/PIL/ImageOps.py index 3ffe50806..3a6dabf5e 100644 --- a/src/PIL/ImageOps.py +++ b/src/PIL/ImageOps.py @@ -221,7 +221,7 @@ def colorize(image, black, white, mid=None, blackpoint=0, whitepoint=255, midpoi return _lut(image, red + green + blue) -def pad(image, size, method=Image.NEAREST, color=None, centering=(0.5, 0.5)): +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 requested aspect ratio and size. @@ -230,7 +230,7 @@ def pad(image, size, method=Image.NEAREST, color=None, centering=(0.5, 0.5)): :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`. + :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 padded version. @@ -280,7 +280,7 @@ def crop(image, border=0): return image.crop((left, top, image.size[0] - right, image.size[1] - bottom)) -def scale(image, factor, resample=Image.NEAREST): +def scale(image, factor, resample=Image.BICUBIC): """ Returns a rescaled image by a specific factor given in parameter. A factor greater than 1 expands the image, between 0 and 1 contracts the @@ -288,8 +288,8 @@ def scale(image, factor, resample=Image.NEAREST): :param image: The image to rescale. :param factor: The expansion factor, as a float. - :param resample: An optional resampling filter. Same values possible as - in the PIL.Image.resize function. + :param resample: What resampling method to use. Default is + :py:attr:`PIL.Image.BICUBIC`. See :ref:`concept-filters`. :returns: An :py:class:`~PIL.Image.Image` object. """ if factor == 1: @@ -363,7 +363,7 @@ def expand(image, border=0, fill=0): return out -def fit(image, size, method=Image.NEAREST, bleed=0.0, centering=(0.5, 0.5)): +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 requested aspect ratio and size. @@ -374,7 +374,7 @@ def fit(image, size, method=Image.NEAREST, bleed=0.0, centering=(0.5, 0.5)): :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`. + :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 one percent). The default value is 0 (no border). diff --git a/src/libImaging/Resample.c b/src/libImaging/Resample.c index d1a89e2ce..0dc08611d 100644 --- a/src/libImaging/Resample.c +++ b/src/libImaging/Resample.c @@ -13,7 +13,7 @@ struct filter { static inline double box_filter(double x) { - if (x >= -0.5 && x < 0.5) + if (x > -0.5 && x <= 0.5) return 1.0; return 0.0; }