Merge pull request #4231 from uploadcare/box-in-thumbnail

Fix thumbnail geometry when DCT scaling is used
This commit is contained in:
Alexander Karpinsky 2019-12-25 15:41:40 +03:00 committed by GitHub
commit b5d06baa5f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 52 additions and 15 deletions

View File

@ -13,7 +13,11 @@ class TestImageDraft(PillowTestCase):
im = Image.new(in_mode, in_size)
data = tostring(im, "JPEG")
im = fromstring(data)
im.draft(req_mode, req_size)
mode, box = im.draft(req_mode, req_size)
scale, _ = im.decoderconfig
self.assertEqual(box[:2], (0, 0))
self.assertTrue((im.width - scale) < box[2] <= im.width)
self.assertTrue((im.height - scale) < box[3] <= im.height)
return im
def test_size(self):

View File

@ -1,6 +1,6 @@
from PIL import Image
from .helper import PillowTestCase, hopper
from .helper import PillowTestCase, fromstring, hopper, tostring
class TestImageThumbnail(PillowTestCase):
@ -58,3 +58,15 @@ class TestImageThumbnail(PillowTestCase):
with Image.open("Tests/images/hopper.jpg") as im:
im.thumbnail((64, 64))
self.assertEqual(im.size, (64, 64))
def test_DCT_scaling_edges(self):
# Make an image with red borders and size (N * 8) + 1 to cross DCT grid
im = Image.new("RGB", (257, 257), "red")
im.paste(Image.new("RGB", (235, 235)), (11, 11))
thumb = fromstring(tostring(im, "JPEG", quality=99, subsampling=0))
thumb.thumbnail((32, 32), Image.BICUBIC)
ref = im.resize((32, 32), Image.BICUBIC)
# This is still JPEG, some error is present. Without the fix it is 11.5
self.assert_image_similar(thumb, ref, 1.5)

View File

@ -58,6 +58,20 @@ and :py:meth:`~PIL.ImageOps.fit` functions.
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
^^^^^^^^^^^^^^^^^^^^^^^^^^
If the :py:meth:`~PIL.Image.Image.draft` method has no effect, it returns ``None``.
If it does have an effect, then it previously returned the image itself.
However, unlike other `chain methods`_, :py:meth:`~PIL.Image.Image.draft` does not
return a modified version of the image, but modifies it in-place. So instead, if
:py:meth:`~PIL.Image.Image.draft` has an effect, Pillow will now return a tuple
of the image mode and a co-ordinate box. The box is the original coordinates in the
bounds of resulting image. This may be useful in a subsequent
:py:meth:`~PIL.Image.Image.resize` call.
.. _chain methods: https://en.wikipedia.org/wiki/Method_chaining
API Changes
===========
@ -104,8 +118,12 @@ Use instead:
with Image.open("hopper.png") as im:
im.save("out.jpg")
Better thumbnail aspect ratio preservation
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Better thumbnail geometry
^^^^^^^^^^^^^^^^^^^^^^^^^
When calculating the new dimensions in :py:meth:`~PIL.Image.Image.thumbnail`,
round to the nearest integer, instead of always rounding down.
This better preserves the original aspect ratio.
When the image width or height is not divisible by 8 the last row and column
in the image get the correct weight after JPEG DCT scaling.

View File

@ -1126,11 +1126,14 @@ class Image:
"""
Configures the image file loader so it returns a version of the
image that as closely as possible matches the given mode and
size. For example, you can use this method to convert a color
size. For example, you can use this method to convert a color
JPEG to greyscale while loading it.
If any changes are made, returns a tuple with the chosen ``mode`` and
``box`` with coordinates of the original image within the altered one.
Note that this method modifies the :py:class:`~PIL.Image.Image` object
in place. If the image has already been loaded, this method has no
in place. If the image has already been loaded, this method has no
effect.
Note: This method is not implemented for most images. It is
@ -2143,14 +2146,17 @@ class Image:
x = max(round(x * size[1] / y), 1)
y = round(size[1])
size = x, y
box = None
if size == self.size:
return
self.draft(None, size)
res = self.draft(None, size)
if res is not None:
box = res[1]
if self.size != size:
im = self.resize(size, resample)
im = self.resize(size, resample, box=box)
self.im = im.im
self._size = size

View File

@ -122,11 +122,6 @@ class ImageFile(Image.Image):
self.fp.close()
raise
def draft(self, mode, size):
"""Set draft mode"""
pass
def get_format_mimetype(self):
if self.custom_mimetype:
return self.custom_mimetype

View File

@ -413,7 +413,8 @@ class JpegImageFile(ImageFile.ImageFile):
return
d, e, o, a = self.tile[0]
scale = 0
scale = 1
original_size = self.size
if a[0] == "RGB" and mode in ["L", "YCbCr"]:
self.mode = mode
@ -436,7 +437,8 @@ class JpegImageFile(ImageFile.ImageFile):
self.tile = [(d, e, o, a)]
self.decoderconfig = (scale, 0)
return self
box = (0, 0, original_size[0] / float(scale), original_size[1] / float(scale))
return (self.mode, box)
def load_djpeg(self):