Merge branch 'master' into box-in-thumbnail
# Conflicts: # docs/releasenotes/7.0.0.rst
|
@ -5,6 +5,9 @@ Changelog (Pillow)
|
||||||
7.0.0 (unreleased)
|
7.0.0 (unreleased)
|
||||||
------------------
|
------------------
|
||||||
|
|
||||||
|
- Better thumbnail aspect ratio preservation #4256
|
||||||
|
[homm]
|
||||||
|
|
||||||
- Add La mode packing and unpacking #4248
|
- Add La mode packing and unpacking #4248
|
||||||
[homm]
|
[homm]
|
||||||
|
|
||||||
|
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 11 KiB |
|
@ -754,7 +754,7 @@ class TestFileGif(PillowTestCase):
|
||||||
def test_getdata(self):
|
def test_getdata(self):
|
||||||
# test getheader/getdata against legacy values
|
# test getheader/getdata against legacy values
|
||||||
# Create a 'P' image with holes in the palette
|
# 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.putpalette(ImagePalette.ImagePalette("RGB"))
|
||||||
im.info = {"background": 0}
|
im.info = {"background": 0}
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
from PIL import Image
|
||||||
|
|
||||||
from .helper import PillowTestCase, hopper
|
from .helper import PillowTestCase, hopper
|
||||||
|
|
||||||
|
|
||||||
|
@ -13,7 +15,7 @@ class TestImageGetData(PillowTestCase):
|
||||||
|
|
||||||
def test_roundtrip(self):
|
def test_roundtrip(self):
|
||||||
def getdata(mode):
|
def getdata(mode):
|
||||||
im = hopper(mode).resize((32, 30))
|
im = hopper(mode).resize((32, 30), Image.NEAREST)
|
||||||
data = im.getdata()
|
data = im.getdata()
|
||||||
return data[0], len(data), len(list(data))
|
return data[0], len(data), len(list(data))
|
||||||
|
|
||||||
|
|
|
@ -212,6 +212,11 @@ class TestImagingCoreResampleAccuracy(PillowTestCase):
|
||||||
for channel in case.split():
|
for channel in case.split():
|
||||||
self.check_case(channel, self.make_sample(data, (12, 12)))
|
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):
|
class CoreResampleConsistencyTest(PillowTestCase):
|
||||||
def make_case(self, mode, fill):
|
def make_case(self, mode, fill):
|
||||||
|
|
|
@ -150,3 +150,12 @@ class TestImageResize(PillowTestCase):
|
||||||
# Test unknown resampling filter
|
# Test unknown resampling filter
|
||||||
with hopper() as im:
|
with hopper() as im:
|
||||||
self.assertRaises(ValueError, im.resize, (10, 10), "unknown")
|
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)))
|
||||||
|
|
|
@ -43,6 +43,11 @@ class TestImageThumbnail(PillowTestCase):
|
||||||
im.thumbnail((33, 33))
|
im.thumbnail((33, 33))
|
||||||
self.assertEqual(im.size, (21, 33)) # ratio is 0.6363636364
|
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):
|
def test_no_resize(self):
|
||||||
# Check that draft() can resize the image to the destination size
|
# Check that draft() can resize the image to the destination size
|
||||||
with Image.open("Tests/images/hopper.jpg") as im:
|
with Image.open("Tests/images/hopper.jpg") as im:
|
||||||
|
|
|
@ -159,7 +159,8 @@ class TestImageDraw(PillowTestCase):
|
||||||
# Arrange
|
# Arrange
|
||||||
im = Image.new("RGB", (W, H))
|
im = Image.new("RGB", (W, H))
|
||||||
draw = ImageDraw.Draw(im)
|
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
|
# Act
|
||||||
draw.bitmap((10, 10), small)
|
draw.bitmap((10, 10), small)
|
||||||
|
|
|
@ -24,7 +24,7 @@ class TestImageFile(PillowTestCase):
|
||||||
def test_parser(self):
|
def test_parser(self):
|
||||||
def roundtrip(format):
|
def roundtrip(format):
|
||||||
|
|
||||||
im = hopper("L").resize((1000, 1000))
|
im = hopper("L").resize((1000, 1000), Image.NEAREST)
|
||||||
if format in ("MSP", "XBM"):
|
if format in ("MSP", "XBM"):
|
||||||
im = im.convert("1")
|
im = im.convert("1")
|
||||||
|
|
||||||
|
|
|
@ -47,6 +47,17 @@ Setting the size of TIFF images
|
||||||
Setting the size of a TIFF image directly (eg. ``im.size = (256, 256)``) throws
|
Setting the size of a TIFF image directly (eg. ``im.size = (256, 256)``) throws
|
||||||
an error. Use ``Image.resize`` instead.
|
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
|
Image.draft() return value
|
||||||
^^^^^^^^^^^^^^^^^^^^^^^^^^
|
^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
@ -106,3 +117,9 @@ Use instead:
|
||||||
|
|
||||||
with Image.open("hopper.png") as im:
|
with Image.open("hopper.png") as im:
|
||||||
im.save("out.jpg")
|
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.
|
||||||
|
|
|
@ -1764,7 +1764,7 @@ class Image:
|
||||||
|
|
||||||
return m_im
|
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.
|
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`,
|
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.BILINEAR`, :py:attr:`PIL.Image.HAMMING`,
|
||||||
:py:attr:`PIL.Image.BICUBIC` or :py:attr:`PIL.Image.LANCZOS`.
|
:py:attr:`PIL.Image.BICUBIC` or :py:attr:`PIL.Image.LANCZOS`.
|
||||||
If omitted, or if the image has mode "1" or "P", it is
|
Default filter is :py:attr:`PIL.Image.BICUBIC`.
|
||||||
set :py:attr:`PIL.Image.NEAREST`.
|
If the image has mode "1" or "P", it is
|
||||||
|
always set to :py:attr:`PIL.Image.NEAREST`.
|
||||||
See: :ref:`concept-filters`.
|
See: :ref:`concept-filters`.
|
||||||
:param box: An optional 4-tuple of floats giving the region
|
:param box: An optional 4-tuple of floats giving the region
|
||||||
of the source image which should be scaled.
|
of the source image which should be scaled.
|
||||||
|
@ -1845,7 +1846,7 @@ class Image:
|
||||||
environment), or :py:attr:`PIL.Image.BICUBIC`
|
environment), or :py:attr:`PIL.Image.BICUBIC`
|
||||||
(cubic spline interpolation in a 4x4 environment).
|
(cubic spline interpolation in a 4x4 environment).
|
||||||
If omitted, or if the image has mode "1" or "P", it is
|
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
|
:param expand: Optional expansion flag. If true, expands the output
|
||||||
image to make it large enough to hold the entire rotated image.
|
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
|
If false or omitted, make the output image the same size as the
|
||||||
|
@ -2141,10 +2142,10 @@ class Image:
|
||||||
x, y = self.size
|
x, y = self.size
|
||||||
if x > size[0]:
|
if x > size[0]:
|
||||||
y = max(round(y * size[0] / x), 1)
|
y = max(round(y * size[0] / x), 1)
|
||||||
x = size[0]
|
x = round(size[0])
|
||||||
if y > size[1]:
|
if y > size[1]:
|
||||||
x = max(round(x * size[1] / y), 1)
|
x = max(round(x * size[1] / y), 1)
|
||||||
y = size[1]
|
y = round(size[1])
|
||||||
size = x, y
|
size = x, y
|
||||||
box = None
|
box = None
|
||||||
|
|
||||||
|
|
|
@ -221,7 +221,7 @@ def colorize(image, black, white, mid=None, blackpoint=0, whitepoint=255, midpoi
|
||||||
return _lut(image, red + green + blue)
|
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
|
Returns a sized and padded version of the image, expanded to fill the
|
||||||
requested aspect ratio and size.
|
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
|
:param size: The requested output size in pixels, given as a
|
||||||
(width, height) tuple.
|
(width, height) tuple.
|
||||||
:param method: What resampling method to use. Default is
|
: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 color: The background color of the padded image.
|
||||||
:param centering: Control the position of the original image within the
|
:param centering: Control the position of the original image within the
|
||||||
padded version.
|
padded version.
|
||||||
|
@ -280,7 +280,7 @@ def crop(image, border=0):
|
||||||
return image.crop((left, top, image.size[0] - right, image.size[1] - bottom))
|
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.
|
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
|
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 image: The image to rescale.
|
||||||
:param factor: The expansion factor, as a float.
|
:param factor: The expansion factor, as a float.
|
||||||
:param resample: An optional resampling filter. Same values possible as
|
:param resample: What resampling method to use. Default is
|
||||||
in the PIL.Image.resize function.
|
:py:attr:`PIL.Image.BICUBIC`. See :ref:`concept-filters`.
|
||||||
:returns: An :py:class:`~PIL.Image.Image` object.
|
:returns: An :py:class:`~PIL.Image.Image` object.
|
||||||
"""
|
"""
|
||||||
if factor == 1:
|
if factor == 1:
|
||||||
|
@ -363,7 +363,7 @@ def expand(image, border=0, fill=0):
|
||||||
return out
|
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
|
Returns a sized and cropped version of the image, cropped to the
|
||||||
requested aspect ratio and size.
|
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
|
:param size: The requested output size in pixels, given as a
|
||||||
(width, height) tuple.
|
(width, height) tuple.
|
||||||
:param method: What resampling method to use. Default is
|
: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
|
: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
|
four edges. The value is a decimal percentage (use 0.01 for
|
||||||
one percent). The default value is 0 (no border).
|
one percent). The default value is 0 (no border).
|
||||||
|
|
|
@ -13,7 +13,7 @@ struct filter {
|
||||||
|
|
||||||
static inline double box_filter(double x)
|
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 1.0;
|
||||||
return 0.0;
|
return 0.0;
|
||||||
}
|
}
|
||||||
|
|