diff --git a/Tests/test_image_draft.py b/Tests/test_image_draft.py index 5d92ee797..a185b3a63 100644 --- a/Tests/test_image_draft.py +++ b/Tests/test_image_draft.py @@ -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): diff --git a/Tests/test_image_thumbnail.py b/Tests/test_image_thumbnail.py index 5806e2a08..e1857207b 100644 --- a/Tests/test_image_thumbnail.py +++ b/Tests/test_image_thumbnail.py @@ -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) diff --git a/docs/releasenotes/7.0.0.rst b/docs/releasenotes/7.0.0.rst index ac4fcb922..cf88dfa64 100644 --- a/docs/releasenotes/7.0.0.rst +++ b/docs/releasenotes/7.0.0.rst @@ -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. diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 770dc3c93..24687d06f 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -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 diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py index a343bd43c..c8d6b6ba3 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -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 diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py index 37f0bfe2f..82fc12e10 100644 --- a/src/PIL/JpegImagePlugin.py +++ b/src/PIL/JpegImagePlugin.py @@ -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):