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) im = Image.new(in_mode, in_size)
data = tostring(im, "JPEG") data = tostring(im, "JPEG")
im = fromstring(data) 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 return im
def test_size(self): def test_size(self):

View File

@ -1,6 +1,6 @@
from PIL import Image from PIL import Image
from .helper import PillowTestCase, hopper from .helper import PillowTestCase, fromstring, hopper, tostring
class TestImageThumbnail(PillowTestCase): class TestImageThumbnail(PillowTestCase):
@ -58,3 +58,15 @@ class TestImageThumbnail(PillowTestCase):
with Image.open("Tests/images/hopper.jpg") as im: with Image.open("Tests/images/hopper.jpg") as im:
im.thumbnail((64, 64)) im.thumbnail((64, 64))
self.assertEqual(im.size, (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, See :ref:`concept-filters` to learn the difference. In short,
``Image.NEAREST`` is a very fast filter, but simple and low-quality. ``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 API Changes
=========== ===========
@ -104,8 +118,12 @@ 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 Better thumbnail geometry
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^
When calculating the new dimensions in :py:meth:`~PIL.Image.Image.thumbnail`, When calculating the new dimensions in :py:meth:`~PIL.Image.Image.thumbnail`,
round to the nearest integer, instead of always rounding down. 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 Configures the image file loader so it returns a version of the
image that as closely as possible matches the given mode and 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. 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 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. effect.
Note: This method is not implemented for most images. It is 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) x = max(round(x * size[1] / y), 1)
y = round(size[1]) y = round(size[1])
size = x, y size = x, y
box = None
if size == self.size: if size == self.size:
return return
self.draft(None, size) res = self.draft(None, size)
if res is not None:
box = res[1]
if self.size != size: if self.size != size:
im = self.resize(size, resample) im = self.resize(size, resample, box=box)
self.im = im.im self.im = im.im
self._size = size self._size = size

View File

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

View File

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