diff --git a/CHANGES.rst b/CHANGES.rst index f9586184e..e26794af6 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -4,6 +4,30 @@ Changelog (Pillow) 4.3.0 (unreleased) ------------------ +- Fix ValueError in Exif/Tiff IFD #2719 + [wiredfool] + +- Use pathlib2 for Path objects on Python < 3.4 #2291 + [asergi] + +- Export only required properties in unsafe_ptrs #2740 + [homm] + +- Alpha composite fixes #2709 + [homm] + +- Faster Transpose operations, added 'Transverse' option #2730 + [homm] + +- Deprecate ImageOps undocumented functions gaussian_blur, gblur, unsharp_mask, usm and box_blur in favor of ImageFilter implementations #2735 + [homm] + +- Dependencies: Updated freetype to 2.8.1 #2741 + [radarhere] + +- Bug: Player skipped first image #2742 + [radarhere] + - Faster filter operations for Kernel, Gaussian, and Unsharp Mask filters #2679 [homm] diff --git a/PIL/Image.py b/PIL/Image.py index 156925c04..71cbb29c4 100644 --- a/PIL/Image.py +++ b/PIL/Image.py @@ -124,6 +124,16 @@ try: except ImportError: HAS_CFFI = False +try: + from pathlib import Path + HAS_PATHLIB = True +except ImportError: + try: + from pathlib2 import Path + HAS_PATHLIB = True + except ImportError: + HAS_PATHLIB = False + def isImageType(t): """ @@ -151,6 +161,7 @@ ROTATE_90 = 2 ROTATE_180 = 3 ROTATE_270 = 4 TRANSPOSE = 5 +TRANSVERSE = 6 # transforms AFFINE = 0 @@ -1404,9 +1415,9 @@ class Image(object): Performance Note: Not currently implemented in-place in the core layer. """ - if not isinstance(source, tuple): + if not isinstance(source, (list, tuple)): raise ValueError("Source must be a tuple") - if not isinstance(dest, tuple): + if not isinstance(dest, (list, tuple)): raise ValueError("Destination must be a tuple") if not len(source) in (2, 4): raise ValueError("Source must be a 2 or 4-tuple") @@ -1430,7 +1441,7 @@ class Image(object): box = dest + (dest[0] + overlay.width, dest[1] + overlay.height) # destination image. don't copy if we're using the whole image. - if dest == (0,0) + self.size: + if box == (0,0) + self.size: background = self else: background = self.crop(box) @@ -1875,11 +1886,9 @@ class Image(object): if isPath(fp): filename = fp open_fp = True - elif sys.version_info >= (3, 4): - from pathlib import Path - if isinstance(fp, Path): - filename = str(fp) - open_fp = True + elif HAS_PATHLIB and isinstance(fp, Path): + filename = str(fp) + open_fp = True if not filename and hasattr(fp, "name") and isPath(fp.name): # only set the name for metadata purposes filename = fp.name @@ -2174,8 +2183,8 @@ class Image(object): :param method: One of :py:attr:`PIL.Image.FLIP_LEFT_RIGHT`, :py:attr:`PIL.Image.FLIP_TOP_BOTTOM`, :py:attr:`PIL.Image.ROTATE_90`, - :py:attr:`PIL.Image.ROTATE_180`, :py:attr:`PIL.Image.ROTATE_270` or - :py:attr:`PIL.Image.TRANSPOSE`. + :py:attr:`PIL.Image.ROTATE_180`, :py:attr:`PIL.Image.ROTATE_270`, + :py:attr:`PIL.Image.TRANSPOSE` or :py:attr:`PIL.Image.TRANSVERSE`. :returns: Returns a flipped or rotated copy of this image. """ @@ -2516,13 +2525,8 @@ def open(fp, mode="r"): filename = "" if isPath(fp): filename = fp - else: - try: - from pathlib import Path - if isinstance(fp, Path): - filename = str(fp.resolve()) - except ImportError: - pass + elif HAS_PATHLIB and isinstance(fp, Path): + filename = str(fp.resolve()) if filename: fp = builtins.open(filename, "rb") diff --git a/PIL/ImageFilter.py b/PIL/ImageFilter.py index c89225484..100fea8bd 100644 --- a/PIL/ImageFilter.py +++ b/PIL/ImageFilter.py @@ -160,6 +160,26 @@ class GaussianBlur(MultibandFilter): return image.gaussian_blur(self.radius) +class BoxBlur(MultibandFilter): + """Blurs the image by setting each pixel to the average value of the pixels + in a square box extending radius pixels in each direction. + Supports float radius of arbitrary size. Uses an optimized implementation + which runs in linear time relative to the size of the image + for any radius value. + + :param radius: Size of the box in one direction. Radius 0 does not blur, + returns an identical image. Radius 1 takes 1 pixel + in each direction, i.e. 9 pixels in total. + """ + name = "BoxBlur" + + def __init__(self, radius): + self.radius = radius + + def filter(self, image): + return image.box_blur(self.radius) + + class UnsharpMask(MultibandFilter): """Unsharp mask filter. diff --git a/PIL/ImageOps.py b/PIL/ImageOps.py index 3681109c1..447d48aae 100644 --- a/PIL/ImageOps.py +++ b/PIL/ImageOps.py @@ -21,6 +21,7 @@ from . import Image from ._util import isStringType import operator import functools +import warnings # @@ -437,6 +438,13 @@ def solarize(image, threshold=128): def gaussian_blur(im, radius=None): """ PIL_usm.gblur(im, [radius])""" + warnings.warn( + 'PIL.ImageOps.gaussian_blur is deprecated. ' + 'Use PIL.ImageFilter.GaussianBlur instead. ' + 'This function will be removed in a future version.', + DeprecationWarning + ) + if radius is None: radius = 5.0 @@ -444,12 +452,30 @@ def gaussian_blur(im, radius=None): return im.im.gaussian_blur(radius) -gblur = gaussian_blur + +def gblur(im, radius=None): + """ PIL_usm.gblur(im, [radius])""" + + warnings.warn( + 'PIL.ImageOps.gblur is deprecated. ' + 'Use PIL.ImageFilter.GaussianBlur instead. ' + 'This function will be removed in a future version.', + DeprecationWarning + ) + + return gaussian_blur(im, radius) def unsharp_mask(im, radius=None, percent=None, threshold=None): """ PIL_usm.usm(im, [radius, percent, threshold])""" + warnings.warn( + 'PIL.ImageOps.unsharp_mask is deprecated. ' + 'Use PIL.ImageFilter.UnsharpMask instead. ' + 'This function will be removed in a future version.', + DeprecationWarning + ) + if radius is None: radius = 5.0 if percent is None: @@ -461,7 +487,18 @@ def unsharp_mask(im, radius=None, percent=None, threshold=None): return im.im.unsharp_mask(radius, percent, threshold) -usm = unsharp_mask + +def usm(im, radius=None, percent=None, threshold=None): + """ PIL_usm.usm(im, [radius, percent, threshold])""" + + warnings.warn( + 'PIL.ImageOps.usm is deprecated. ' + 'Use PIL.ImageFilter.UnsharpMask instead. ' + 'This function will be removed in a future version.', + DeprecationWarning + ) + + return unsharp_mask(im, radius, percent, threshold) def box_blur(image, radius): @@ -478,6 +515,13 @@ def box_blur(image, radius): in each direction, i.e. 9 pixels in total. :return: An image. """ + warnings.warn( + 'PIL.ImageOps.box_blur is deprecated. ' + 'Use PIL.ImageFilter.BoxBlur instead. ' + 'This function will be removed in a future version.', + DeprecationWarning + ) + image.load() return image._new(image.im.box_blur(radius)) diff --git a/PIL/PyAccess.py b/PIL/PyAccess.py index 09ce86af4..620efc4ff 100644 --- a/PIL/PyAccess.py +++ b/PIL/PyAccess.py @@ -49,8 +49,7 @@ class PyAccess(object): self.image8 = ffi.cast('unsigned char **', vals['image8']) self.image32 = ffi.cast('int **', vals['image32']) self.image = ffi.cast('unsigned char **', vals['image']) - self.xsize = vals['xsize'] - self.ysize = vals['ysize'] + self.xsize, self.ysize = img.im.size # Keep pointer to im object to prevent dereferencing. self._im = img.im diff --git a/PIL/TiffImagePlugin.py b/PIL/TiffImagePlugin.py index bd66f4a7a..e6ecabc1d 100644 --- a/PIL/TiffImagePlugin.py +++ b/PIL/TiffImagePlugin.py @@ -550,11 +550,28 @@ class ImageFileDirectory_v2(collections.MutableMapping): dest = self._tags_v1 if legacy_api else self._tags_v2 - if info.length == 1: - if legacy_api and self.tagtype[tag] in [5, 10]: + # Three branches: + # Spec'd length == 1, Actual length 1, store as element + # Spec'd length == 1, Actual > 1, Warn and truncate. Formerly barfed. + # No Spec, Actual length 1, Formerly (<4.2) returned a 1 element tuple. + # Don't mess with the legacy api, since it's frozen. + if ((info.length == 1) or + (info.length is None and len(values) == 1 and not legacy_api)): + # Don't mess with the legacy api, since it's frozen. + if legacy_api and self.tagtype[tag] in [5, 10]: # rationals values = values, - dest[tag], = values + try: + dest[tag], = values + except ValueError: + # We've got a builtin tag with 1 expected entry + warnings.warn( + "Metadata Warning, tag %s had too many entries: %s, expected 1" % ( + tag, len(values))) + dest[tag] = values[0] + else: + # Spec'd length > 1 or undefined + # Unspec'd, and length > 1 dest[tag] = values def __delitem__(self, tag): @@ -1011,8 +1028,10 @@ class TiffImageFile(ImageFile.ImageFile): args = rawmode, "" if JPEGTABLES in self.tag_v2: # Hack to handle abbreviated JPEG headers - # FIXME This will fail with more than one value - self.tile_prefix, = self.tag_v2[JPEGTABLES] + # Definition of JPEGTABLES is that the count + # is the number of bytes in the tables datastream + # so, it should always be 1 in our tag info + self.tile_prefix = self.tag_v2[JPEGTABLES] elif compression == "packbits": args = rawmode elif compression == "tiff_lzw": diff --git a/PIL/TiffTags.py b/PIL/TiffTags.py index 731d0ec8b..eba88ef8d 100644 --- a/PIL/TiffTags.py +++ b/PIL/TiffTags.py @@ -23,7 +23,7 @@ from collections import namedtuple class TagInfo(namedtuple("_TagInfo", "value name type length enum")): __slots__ = [] - def __new__(cls, value=None, name="unknown", type=None, length=0, enum=None): + def __new__(cls, value=None, name="unknown", type=None, length=None, enum=None): return super(TagInfo, cls).__new__( cls, value, name, type, length, enum or {}) @@ -142,6 +142,8 @@ TAGS_V2 = { 341: ("SMaxSampleValue", DOUBLE, 0), 342: ("TransferRange", SHORT, 6), + 347: ("JPEGTables", UNDEFINED, 1), + # obsolete JPEG tags 512: ("JPEGProc", SHORT, 1), 513: ("JPEGInterchangeFormat", LONG, 1), @@ -158,7 +160,10 @@ TAGS_V2 = { 531: ("YCbCrPositioning", SHORT, 1), 532: ("ReferenceBlackWhite", LONG, 0), + 700: ('XMP', BYTE, 1), + 33432: ("Copyright", ASCII, 1), + 34377: ('PhotoshopInfo', BYTE, 1), # FIXME add more tags here 34665: ("ExifIFD", SHORT, 1), @@ -188,8 +193,8 @@ TAGS_V2 = { 50741: ("MakerNoteSafety", SHORT, 1, {"Unsafe": 0, "Safe": 1}), 50780: ("BestQualityScale", RATIONAL, 1), - 50838: ("ImageJMetaDataByteCounts", LONG, 1), - 50839: ("ImageJMetaData", UNDEFINED, 1) + 50838: ("ImageJMetaDataByteCounts", LONG, 0), # Can be more than one + 50839: ("ImageJMetaData", UNDEFINED, 1) # see Issue #2006 } # Legacy Tags structure diff --git a/Scripts/player.py b/Scripts/player.py index dcf7d9307..6e6e21d20 100755 --- a/Scripts/player.py +++ b/Scripts/player.py @@ -22,13 +22,10 @@ from PIL import Image, ImageTk class UI(tkinter.Label): def __init__(self, master, im): - if isinstance(im, list): + self.im = im + if isinstance(self.im, list): # list of images - self.im = im[1:] - im = self.im[0] - else: - # sequence - self.im = im + im = self.im.pop(0) if im.mode == "1": self.image = ImageTk.BitmapImage(im, foreground="white") diff --git a/Tests/images/issue_2278.tif b/Tests/images/issue_2278.tif new file mode 100644 index 000000000..adac046db Binary files /dev/null and b/Tests/images/issue_2278.tif differ diff --git a/Tests/test_box_blur.py b/Tests/test_box_blur.py index d99847740..622b842d0 100644 --- a/Tests/test_box_blur.py +++ b/Tests/test_box_blur.py @@ -13,20 +13,6 @@ sample.putdata(sum([ ], [])) -class ImageMock(object): - def __init__(self): - self.im = self - - def load(self): - pass - - def _new(self, im): - return im - - def box_blur(self, radius, n): - return radius, n - - class TestBoxBlurApi(PillowTestCase): def test_imageops_box_blur(self): diff --git a/Tests/test_file_tiff_metadata.py b/Tests/test_file_tiff_metadata.py index 9d11e32ef..bb5768046 100644 --- a/Tests/test_file_tiff_metadata.py +++ b/Tests/test_file_tiff_metadata.py @@ -56,7 +56,7 @@ class TestFileTiffMetadata(PillowTestCase): loaded = Image.open(f) self.assertEqual(loaded.tag[ImageJMetaDataByteCounts], (len(bindata),)) - self.assertEqual(loaded.tag_v2[ImageJMetaDataByteCounts], len(bindata)) + self.assertEqual(loaded.tag_v2[ImageJMetaDataByteCounts], (len(bindata),)) self.assertEqual(loaded.tag[ImageJMetaData], bindata) self.assertEqual(loaded.tag_v2[ImageJMetaData], bindata) @@ -69,6 +69,16 @@ class TestFileTiffMetadata(PillowTestCase): loaded_double = loaded.tag[tag_ids['YawAngle']][0] self.assertAlmostEqual(loaded_double, doubledata) + # check with 2 element ImageJMetaDataByteCounts, issue #2006 + + info[ImageJMetaDataByteCounts] = (8, len(bindata) - 8) + img.save(f, tiffinfo=info) + loaded = Image.open(f) + + self.assertEqual(loaded.tag[ImageJMetaDataByteCounts], (8, len(bindata) - 8)) + self.assertEqual(loaded.tag_v2[ImageJMetaDataByteCounts], (8, len(bindata) - 8)) + + def test_read_metadata(self): img = Image.open('Tests/images/hopper_g4.tif') @@ -202,8 +212,8 @@ class TestFileTiffMetadata(PillowTestCase): im.save(out, tiffinfo=info, compression='raw') reloaded = Image.open(out) - self.assertEqual(0, reloaded.tag_v2[41988][0].numerator) - self.assertEqual(0, reloaded.tag_v2[41988][0].denominator) + self.assertEqual(0, reloaded.tag_v2[41988].numerator) + self.assertEqual(0, reloaded.tag_v2[41988].denominator) def test_expty_values(self): data = io.BytesIO( @@ -220,6 +230,27 @@ class TestFileTiffMetadata(PillowTestCase): self.fail("Should not be struct value error there.") self.assertIn(33432, info) + def test_PhotoshopInfo(self): + im = Image.open('Tests/images/issue_2278.tif') + + self.assertIsInstance(im.tag_v2[34377], bytes) + out = self.tempfile('temp.tiff') + im.save(out) + reloaded = Image.open(out) + self.assertIsInstance(reloaded.tag_v2[34377], bytes) + + def test_too_many_entries(self): + ifd = TiffImagePlugin.ImageFileDirectory_v2() + + # 277: ("SamplesPerPixel", SHORT, 1), + ifd._tagdata[277] = struct.pack('hh', 4,4) + ifd.tagtype[277] = TiffTags.SHORT + + try: + self.assert_warning(UserWarning, lambda: ifd[277]) + except ValueError: + self.fail("Invalid Metadata count should not cause a Value Error.") + if __name__ == '__main__': unittest.main() diff --git a/Tests/test_image.py b/Tests/test_image.py index 1209c9920..9ff36c646 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -2,7 +2,6 @@ from helper import unittest, PillowTestCase, hopper from PIL import Image import os -import sys class TestImage(PillowTestCase): @@ -72,10 +71,9 @@ class TestImage(PillowTestCase): def test_bad_mode(self): self.assertRaises(ValueError, Image.open, "filename", "bad mode") - @unittest.skipIf(sys.version_info < (3, 4), - "pathlib only available in Python 3.4 or later") + @unittest.skipUnless(Image.HAS_PATHLIB, "requires pathlib/pathlib2") def test_pathlib(self): - from pathlib import Path + from PIL.Image import Path im = Image.open(Path("Tests/images/hopper.jpg")) self.assertEqual(im.mode, "RGB") self.assertEqual(im.size, (128, 128)) diff --git a/Tests/test_image_filter.py b/Tests/test_image_filter.py index 8a38b2979..3636a73f7 100644 --- a/Tests/test_image_filter.py +++ b/Tests/test_image_filter.py @@ -1,7 +1,6 @@ from helper import unittest, PillowTestCase, hopper -from PIL import Image -from PIL import ImageFilter +from PIL import Image, ImageFilter class TestImageFilter(PillowTestCase): @@ -9,10 +8,11 @@ class TestImageFilter(PillowTestCase): def test_sanity(self): def filter(filter): - im = hopper("L") - out = im.filter(filter) - self.assertEqual(out.mode, im.mode) - self.assertEqual(out.size, im.size) + for mode in ["L", "RGB", "CMYK"]: + im = hopper(mode) + out = im.filter(filter) + self.assertEqual(out.mode, im.mode) + self.assertEqual(out.size, im.size) filter(ImageFilter.BLUR) filter(ImageFilter.CONTOUR) @@ -28,9 +28,9 @@ class TestImageFilter(PillowTestCase): filter(ImageFilter.MedianFilter) filter(ImageFilter.MinFilter) filter(ImageFilter.ModeFilter) - filter(ImageFilter.Kernel((3, 3), list(range(9)))) filter(ImageFilter.GaussianBlur) filter(ImageFilter.GaussianBlur(5)) + filter(ImageFilter.BoxBlur(5)) filter(ImageFilter.UnsharpMask) filter(ImageFilter.UnsharpMask(10)) diff --git a/Tests/test_image_transpose.py b/Tests/test_image_transpose.py index e13fc8605..a6b1191db 100644 --- a/Tests/test_image_transpose.py +++ b/Tests/test_image_transpose.py @@ -2,7 +2,7 @@ import helper from helper import unittest, PillowTestCase from PIL.Image import (FLIP_LEFT_RIGHT, FLIP_TOP_BOTTOM, ROTATE_90, ROTATE_180, - ROTATE_270, TRANSPOSE) + ROTATE_270, TRANSPOSE, TRANSVERSE) class TestImageTranspose(PillowTestCase): @@ -108,6 +108,22 @@ class TestImageTranspose(PillowTestCase): for mode in ("L", "RGB"): transpose(mode) + def test_tranverse(self): + def transpose(mode): + im = self.hopper[mode] + out = im.transpose(TRANSVERSE) + self.assertEqual(out.mode, mode) + self.assertEqual(out.size, im.size[::-1]) + + x, y = im.size + self.assertEqual(im.getpixel((1, 1)), out.getpixel((y-2, x-2))) + self.assertEqual(im.getpixel((x-2, 1)), out.getpixel((y-2, 1))) + self.assertEqual(im.getpixel((1, y-2)), out.getpixel((1, x-2))) + self.assertEqual(im.getpixel((x-2, y-2)), out.getpixel((1, 1))) + + for mode in ("L", "RGB"): + transpose(mode) + def test_roundtrip(self): im = self.hopper['L'] @@ -124,6 +140,12 @@ class TestImageTranspose(PillowTestCase): im.transpose(TRANSPOSE), transpose(ROTATE_90, FLIP_TOP_BOTTOM)) self.assert_image_equal( im.transpose(TRANSPOSE), transpose(ROTATE_270, FLIP_LEFT_RIGHT)) + self.assert_image_equal( + im.transpose(TRANSVERSE), transpose(ROTATE_90, FLIP_LEFT_RIGHT)) + self.assert_image_equal( + im.transpose(TRANSVERSE), transpose(ROTATE_270, FLIP_TOP_BOTTOM)) + self.assert_image_equal( + im.transpose(TRANSVERSE), transpose(ROTATE_180, TRANSPOSE)) if __name__ == '__main__': diff --git a/Tests/test_imageops_usm.py b/Tests/test_imageops_usm.py index eed8ff754..cd7dcae5f 100644 --- a/Tests/test_imageops_usm.py +++ b/Tests/test_imageops_usm.py @@ -12,15 +12,25 @@ class TestImageOpsUsm(PillowTestCase): def test_ops_api(self): - i = ImageOps.gaussian_blur(im, 2.0) + i = self.assert_warning(DeprecationWarning, + ImageOps.gaussian_blur, im, 2.0) self.assertEqual(i.mode, "RGB") self.assertEqual(i.size, (128, 128)) - # i.save("blur.bmp") - i = ImageOps.unsharp_mask(im, 2.0, 125, 8) + i = self.assert_warning(DeprecationWarning, + ImageOps.gblur, im, 2.0) + self.assertEqual(i.mode, "RGB") + self.assertEqual(i.size, (128, 128)) + + i = self.assert_warning(DeprecationWarning, + ImageOps.unsharp_mask, im, 2.0, 125, 8) + self.assertEqual(i.mode, "RGB") + self.assertEqual(i.size, (128, 128)) + + i = self.assert_warning(DeprecationWarning, + ImageOps.usm, im, 2.0, 125, 8) self.assertEqual(i.mode, "RGB") self.assertEqual(i.size, (128, 128)) - # i.save("usm.bmp") def test_filter_api(self): @@ -36,38 +46,38 @@ class TestImageOpsUsm(PillowTestCase): def test_usm_formats(self): - usm = ImageOps.unsharp_mask - self.assertRaises(ValueError, usm, im.convert("1")) - usm(im.convert("L")) - self.assertRaises(ValueError, usm, im.convert("I")) - self.assertRaises(ValueError, usm, im.convert("F")) - usm(im.convert("RGB")) - usm(im.convert("RGBA")) - usm(im.convert("CMYK")) - self.assertRaises(ValueError, usm, im.convert("YCbCr")) + usm = ImageFilter.UnsharpMask + self.assertRaises(ValueError, im.convert("1").filter, usm) + im.convert("L").filter(usm) + self.assertRaises(ValueError, im.convert("I").filter, usm) + self.assertRaises(ValueError, im.convert("F").filter, usm) + im.convert("RGB").filter(usm) + im.convert("RGBA").filter(usm) + im.convert("CMYK").filter(usm) + self.assertRaises(ValueError, im.convert("YCbCr").filter, usm) def test_blur_formats(self): - blur = ImageOps.gaussian_blur - self.assertRaises(ValueError, blur, im.convert("1")) + blur = ImageFilter.GaussianBlur + self.assertRaises(ValueError, im.convert("1").filter, blur) blur(im.convert("L")) - self.assertRaises(ValueError, blur, im.convert("I")) - self.assertRaises(ValueError, blur, im.convert("F")) - blur(im.convert("RGB")) - blur(im.convert("RGBA")) - blur(im.convert("CMYK")) - self.assertRaises(ValueError, blur, im.convert("YCbCr")) + self.assertRaises(ValueError, im.convert("I").filter, blur) + self.assertRaises(ValueError, im.convert("F").filter, blur) + im.convert("RGB").filter(blur) + im.convert("RGBA").filter(blur) + im.convert("CMYK").filter(blur) + self.assertRaises(ValueError, im.convert("YCbCr").filter, blur) def test_usm_accuracy(self): src = snakes.convert('RGB') - i = src._new(ImageOps.unsharp_mask(src, 5, 1024, 0)) + i = src.filter(ImageFilter.UnsharpMask(5, 1024, 0)) # Image should not be changed because it have only 0 and 255 levels. self.assertEqual(i.tobytes(), src.tobytes()) def test_blur_accuracy(self): - i = snakes._new(ImageOps.gaussian_blur(snakes, .4)) + i = snakes.filter(ImageFilter.GaussianBlur(.4)) # These pixels surrounded with pixels with 255 intensity. # They must be very close to 255. for x, y, c in [(1, 0, 1), (2, 0, 1), (7, 8, 1), (8, 8, 1), (2, 9, 1), diff --git a/_imaging.c b/_imaging.c index acba91bfd..be612c1b7 100644 --- a/_imaging.c +++ b/_imaging.c @@ -1669,6 +1669,7 @@ _transpose(ImagingObject* self, PyObject* args) case 2: /* rotate 90 */ case 4: /* rotate 270 */ case 5: /* transpose */ + case 6: /* transverse */ imOut = ImagingNewDirty(imIn->mode, imIn->ysize, imIn->xsize); break; default: @@ -1696,6 +1697,9 @@ _transpose(ImagingObject* self, PyObject* args) case 5: (void) ImagingTranspose(imOut, imIn); break; + case 6: + (void) ImagingTransverse(imOut, imIn); + break; } return PyImagingNew(imOut); @@ -3129,21 +3133,10 @@ _getattr_ptr(ImagingObject* self, void* closure) static PyObject* _getattr_unsafe_ptrs(ImagingObject* self, void* closure) { - return Py_BuildValue("(ss)(si)(si)(si)(si)(si)(sn)(sn)(sn)(sn)(sn)(si)(si)(sn)", - "mode", self->image->mode, - "type", self->image->type, - "depth", self->image->depth, - "bands", self->image->bands, - "xsize", self->image->xsize, - "ysize", self->image->ysize, - "palette", self->image->palette, + return Py_BuildValue("(sn)(sn)(sn)", "image8", self->image->image8, "image32", self->image->image32, - "image", self->image->image, - "block", self->image->block, - "pixelsize", self->image->pixelsize, - "linesize", self->image->linesize, - "destroy", self->image->destroy + "image", self->image->image ); }; diff --git a/docs/reference/ImageFilter.rst b/docs/reference/ImageFilter.rst index e89fafbcf..bc1868667 100644 --- a/docs/reference/ImageFilter.rst +++ b/docs/reference/ImageFilter.rst @@ -38,6 +38,7 @@ image enhancement filters: * **SHARPEN** .. autoclass:: PIL.ImageFilter.GaussianBlur +.. autoclass:: PIL.ImageFilter.BoxBlur .. autoclass:: PIL.ImageFilter.UnsharpMask .. autoclass:: PIL.ImageFilter.Kernel .. autoclass:: PIL.ImageFilter.RankFilter diff --git a/docs/releasenotes/4.3.0.rst b/docs/releasenotes/4.3.0.rst index b893a623a..606e83918 100644 --- a/docs/releasenotes/4.3.0.rst +++ b/docs/releasenotes/4.3.0.rst @@ -4,13 +4,19 @@ Get One Channel From Image ========================== -New method :py:meth:`PIL.Image.Image.getchannel` added. +New method :py:meth:`PIL.Image.Image.getchannel` is added. It returns single channel by index or name. For example, ``image.getchannel("A")`` will return alpha channel as separate image. ``getchannel`` should work up to 6 times faster than ``image.split()[0]`` in previous Pillow versions. +Box Blur +======== + +New filter :py:class:`PIL.ImageFilter.BoxBlur` is added. + + Partial Resampling ================== @@ -40,6 +46,22 @@ This release contains several performance improvements: using a recent version of libjpeg-turbo. +TIFF Metadata Changes +===================== + +* TIFF tags with unknown type/quantity now default to being bare + values if they are 1 element, where previously they would be a + single element tuple. This is only with the new api, not the legacy + api. This normalizes the handling of fields, so that the metadata + with inferred or image specified counts are handled the same as + metadata with count specified in the TIFF spec. +* The ``PhotoshopInfo``, ``XMP``, and ``JPEGTables`` tags now have a + defined type (bytes) and a count of 1. +* The ``ImageJMetaDataByteCounts`` tag now has an arbitrary number of + items, as there can be multiple items, one for UTF-8, and one for + UTF-16. + + Core Image API Changes ====================== diff --git a/libImaging/AlphaComposite.c b/libImaging/AlphaComposite.c index 00d28f956..81b621123 100644 --- a/libImaging/AlphaComposite.c +++ b/libImaging/AlphaComposite.c @@ -11,6 +11,7 @@ #include "Imaging.h" +#define PRECISION_BITS 7 typedef struct { @@ -49,13 +50,11 @@ ImagingAlphaComposite(Imaging imDst, Imaging imSrc) ImagingCopyInfo(imOut, imDst); for (y = 0; y < imDst->ysize; y++) { - rgba8* dst = (rgba8*) imDst->image[y]; rgba8* src = (rgba8*) imSrc->image[y]; rgba8* out = (rgba8*) imOut->image[y]; for (x = 0; x < imDst->xsize; x ++) { - if (src->a == 0) { // Copy 4 bytes at once. *out = *dst; @@ -64,25 +63,20 @@ ImagingAlphaComposite(Imaging imDst, Imaging imSrc) // Each variable has extra meaningful bits. // Divisions are rounded. - // This code uses trick from Paste.c: - // (a + (2 << (n-1)) - 1) / ((2 << n)-1) - // almost equivalent to: - // tmp = a + (2 << (n-1)), ((tmp >> n) + tmp) >> n - UINT32 tmpr, tmpg, tmpb; - UINT16 blend = dst->a * (255 - src->a); - UINT16 outa255 = src->a * 255 + blend; + UINT32 blend = dst->a * (255 - src->a); + UINT32 outa255 = src->a * 255 + blend; // There we use 7 bits for precision. // We could use more, but we go beyond 32 bits. - UINT16 coef1 = src->a * 255 * 255 * 128 / outa255; - UINT16 coef2 = 255 * 128 - coef1; + UINT32 coef1 = src->a * 255 * 255 * (1<r * coef1 + dst->r * coef2 + (0x80 << 7); - out->r = SHIFTFORDIV255(tmpr) >> 7; - tmpg = src->g * coef1 + dst->g * coef2 + (0x80 << 7); - out->g = SHIFTFORDIV255(tmpg) >> 7; - tmpb = src->b * coef1 + dst->b * coef2 + (0x80 << 7); - out->b = SHIFTFORDIV255(tmpb) >> 7; + tmpr = src->r * coef1 + dst->r * coef2; + tmpg = src->g * coef1 + dst->g * coef2; + tmpb = src->b * coef1 + dst->b * coef2; + out->r = SHIFTFORDIV255(tmpr + (0x80<> PRECISION_BITS; + out->g = SHIFTFORDIV255(tmpg + (0x80<> PRECISION_BITS; + out->b = SHIFTFORDIV255(tmpb + (0x80<> PRECISION_BITS; out->a = SHIFTFORDIV255(outa255 + 0x80); } diff --git a/libImaging/Geometry.c b/libImaging/Geometry.c index 2b3b1d5ef..759056ae0 100644 --- a/libImaging/Geometry.c +++ b/libImaging/Geometry.c @@ -5,7 +5,8 @@ Rotating in chunks that fit in the cache can speed up rotation 8x on a modern CPU. A chunk size of 128 requires only 65k and is large enough that the overhead from the extra loops are not apparent. */ -#define ROTATE_CHUNK 128 +#define ROTATE_CHUNK 512 +#define ROTATE_SMALL_CHUNK 8 #define COORD(v) ((v) < 0.0 ? -1 : ((int)(v))) #define FLOOR(v) ((v) < 0.0 ? ((int)floor(v)) : ((int)(v))) @@ -26,30 +27,27 @@ ImagingFlipLeftRight(Imaging imOut, Imaging imIn) ImagingCopyInfo(imOut, imIn); +#define FLIP_LEFT_RIGHT(INT, image) \ + for (y = 0; y < imIn->ysize; y++) { \ + INT* in = imIn->image[y]; \ + INT* out = imOut->image[y]; \ + xr = imIn->xsize-1; \ + for (x = 0; x < imIn->xsize; x++, xr--) \ + out[xr] = in[x]; \ + } + ImagingSectionEnter(&cookie); if (imIn->image8) { - for (y = 0; y < imIn->ysize; y++) { - UINT8* in = (UINT8*) imIn->image8[y]; - UINT8* out = (UINT8*) imOut->image8[y]; - x = 0; - xr = imIn->xsize-1; - for (; x < imIn->xsize; x++, xr--) - out[xr] = in[x]; - } + FLIP_LEFT_RIGHT(UINT8, image8) } else { - for (y = 0; y < imIn->ysize; y++) { - UINT32* in = (UINT32*) imIn->image32[y]; - UINT32* out = (UINT32*) imOut->image32[y]; - x = 0; - xr = imIn->xsize-1; - for (; x < imIn->xsize; x++, xr--) - out[xr] = in[x]; - } + FLIP_LEFT_RIGHT(INT32, image32) } ImagingSectionLeave(&cookie); +#undef FLIP_LEFT_RIGHT + return imOut; } @@ -84,6 +82,7 @@ ImagingRotate90(Imaging imOut, Imaging imIn) { ImagingSectionCookie cookie; int x, y, xx, yy, xr, xxsize, yysize; + int xxx, yyy, xxxsize, yyysize; if (!imOut || !imIn || strcmp(imIn->mode, imOut->mode) != 0) return (Imaging) ImagingError_ModeError(); @@ -92,15 +91,22 @@ ImagingRotate90(Imaging imOut, Imaging imIn) ImagingCopyInfo(imOut, imIn); -#define ROTATE_90(image) \ +#define ROTATE_90(INT, image) \ for (y = 0; y < imIn->ysize; y += ROTATE_CHUNK) { \ for (x = 0; x < imIn->xsize; x += ROTATE_CHUNK) { \ yysize = y + ROTATE_CHUNK < imIn->ysize ? y + ROTATE_CHUNK : imIn->ysize; \ xxsize = x + ROTATE_CHUNK < imIn->xsize ? x + ROTATE_CHUNK : imIn->xsize; \ - for (yy = y; yy < yysize; yy++) { \ - xr = imIn->xsize - 1 - x; \ - for (xx = x; xx < xxsize; xx++, xr--) { \ - imOut->image[xr][yy] = imIn->image[yy][xx]; \ + for (yy = y; yy < yysize; yy += ROTATE_SMALL_CHUNK) { \ + for (xx = x; xx < xxsize; xx += ROTATE_SMALL_CHUNK) { \ + yyysize = yy + ROTATE_SMALL_CHUNK < imIn->ysize ? yy + ROTATE_SMALL_CHUNK : imIn->ysize; \ + xxxsize = xx + ROTATE_SMALL_CHUNK < imIn->xsize ? xx + ROTATE_SMALL_CHUNK : imIn->xsize; \ + for (yyy = yy; yyy < yyysize; yyy++) { \ + INT* in = imIn->image[yyy]; \ + xr = imIn->xsize - 1 - xx; \ + for (xxx = xx; xxx < xxxsize; xxx++, xr--) { \ + imOut->image[xr][yyy] = in[xxx]; \ + } \ + } \ } \ } \ } \ @@ -109,9 +115,9 @@ ImagingRotate90(Imaging imOut, Imaging imIn) ImagingSectionEnter(&cookie); if (imIn->image8) - ROTATE_90(image8) + ROTATE_90(UINT8, image8) else - ROTATE_90(image32) + ROTATE_90(INT32, image32) ImagingSectionLeave(&cookie); @@ -126,6 +132,7 @@ ImagingTranspose(Imaging imOut, Imaging imIn) { ImagingSectionCookie cookie; int x, y, xx, yy, xxsize, yysize; + int xxx, yyy, xxxsize, yyysize; if (!imOut || !imIn || strcmp(imIn->mode, imOut->mode) != 0) return (Imaging) ImagingError_ModeError(); @@ -134,14 +141,21 @@ ImagingTranspose(Imaging imOut, Imaging imIn) ImagingCopyInfo(imOut, imIn); -#define TRANSPOSE(image) \ +#define TRANSPOSE(INT, image) \ for (y = 0; y < imIn->ysize; y += ROTATE_CHUNK) { \ for (x = 0; x < imIn->xsize; x += ROTATE_CHUNK) { \ yysize = y + ROTATE_CHUNK < imIn->ysize ? y + ROTATE_CHUNK : imIn->ysize; \ xxsize = x + ROTATE_CHUNK < imIn->xsize ? x + ROTATE_CHUNK : imIn->xsize; \ - for (yy = y; yy < yysize; yy++) { \ - for (xx = x; xx < xxsize; xx++) { \ - imOut->image[xx][yy] = imIn->image[yy][xx]; \ + for (yy = y; yy < yysize; yy += ROTATE_SMALL_CHUNK) { \ + for (xx = x; xx < xxsize; xx += ROTATE_SMALL_CHUNK) { \ + yyysize = yy + ROTATE_SMALL_CHUNK < imIn->ysize ? yy + ROTATE_SMALL_CHUNK : imIn->ysize; \ + xxxsize = xx + ROTATE_SMALL_CHUNK < imIn->xsize ? xx + ROTATE_SMALL_CHUNK : imIn->xsize; \ + for (yyy = yy; yyy < yyysize; yyy++) { \ + INT* in = imIn->image[yyy]; \ + for (xxx = xx; xxx < xxxsize; xxx++) { \ + imOut->image[xxx][yyy] = in[xxx]; \ + } \ + } \ } \ } \ } \ @@ -150,9 +164,9 @@ ImagingTranspose(Imaging imOut, Imaging imIn) ImagingSectionEnter(&cookie); if (imIn->image8) - TRANSPOSE(image8) + TRANSPOSE(UINT8, image8) else - TRANSPOSE(image32) + TRANSPOSE(INT32, image32) ImagingSectionLeave(&cookie); @@ -162,6 +176,57 @@ ImagingTranspose(Imaging imOut, Imaging imIn) } +Imaging +ImagingTransverse(Imaging imOut, Imaging imIn) +{ + ImagingSectionCookie cookie; + int x, y, xr, yr, xx, yy, xxsize, yysize; + int xxx, yyy, xxxsize, yyysize; + + if (!imOut || !imIn || strcmp(imIn->mode, imOut->mode) != 0) + return (Imaging) ImagingError_ModeError(); + if (imIn->xsize != imOut->ysize || imIn->ysize != imOut->xsize) + return (Imaging) ImagingError_Mismatch(); + + ImagingCopyInfo(imOut, imIn); + +#define TRANSVERSE(INT, image) \ + for (y = 0; y < imIn->ysize; y += ROTATE_CHUNK) { \ + for (x = 0; x < imIn->xsize; x += ROTATE_CHUNK) { \ + yysize = y + ROTATE_CHUNK < imIn->ysize ? y + ROTATE_CHUNK : imIn->ysize; \ + xxsize = x + ROTATE_CHUNK < imIn->xsize ? x + ROTATE_CHUNK : imIn->xsize; \ + for (yy = y; yy < yysize; yy += ROTATE_SMALL_CHUNK) { \ + for (xx = x; xx < xxsize; xx += ROTATE_SMALL_CHUNK) { \ + yyysize = yy + ROTATE_SMALL_CHUNK < imIn->ysize ? yy + ROTATE_SMALL_CHUNK : imIn->ysize; \ + xxxsize = xx + ROTATE_SMALL_CHUNK < imIn->xsize ? xx + ROTATE_SMALL_CHUNK : imIn->xsize; \ + yr = imIn->ysize - 1 - yy; \ + for (yyy = yy; yyy < yyysize; yyy++, yr--) { \ + INT* in = imIn->image[yyy]; \ + xr = imIn->xsize - 1 - xx; \ + for (xxx = xx; xxx < xxxsize; xxx++, xr--) { \ + imOut->image[xr][yr] = in[xxx]; \ + } \ + } \ + } \ + } \ + } \ + } + + ImagingSectionEnter(&cookie); + + if (imIn->image8) + TRANSVERSE(UINT8, image8) + else + TRANSVERSE(INT32, image32) + + ImagingSectionLeave(&cookie); + +#undef TRANSVERSE + + return imOut; +} + + Imaging ImagingRotate180(Imaging imOut, Imaging imIn) { @@ -175,20 +240,23 @@ ImagingRotate180(Imaging imOut, Imaging imIn) ImagingCopyInfo(imOut, imIn); -#define ROTATE_180(image)\ - for (y = 0; y < imIn->ysize; y++, yr--) {\ - xr = imIn->xsize-1;\ - for (x = 0; x < imIn->xsize; x++, xr--)\ - imOut->image[y][x] = imIn->image[yr][xr];\ +#define ROTATE_180(INT, image) \ + for (y = 0; y < imIn->ysize; y++, yr--) { \ + INT* in = imIn->image[y]; \ + INT* out = imOut->image[yr]; \ + xr = imIn->xsize-1; \ + for (x = 0; x < imIn->xsize; x++, xr--) \ + out[xr] = in[x]; \ } ImagingSectionEnter(&cookie); yr = imIn->ysize-1; - if (imIn->image8) - ROTATE_180(image8) - else - ROTATE_180(image32) + if (imIn->image8) { + ROTATE_180(UINT8, image8) + } else { + ROTATE_180(INT32, image32) + } ImagingSectionLeave(&cookie); @@ -203,6 +271,7 @@ ImagingRotate270(Imaging imOut, Imaging imIn) { ImagingSectionCookie cookie; int x, y, xx, yy, yr, xxsize, yysize; + int xxx, yyy, xxxsize, yyysize; if (!imOut || !imIn || strcmp(imIn->mode, imOut->mode) != 0) return (Imaging) ImagingError_ModeError(); @@ -211,15 +280,22 @@ ImagingRotate270(Imaging imOut, Imaging imIn) ImagingCopyInfo(imOut, imIn); -#define ROTATE_270(image) \ +#define ROTATE_270(INT, image) \ for (y = 0; y < imIn->ysize; y += ROTATE_CHUNK) { \ for (x = 0; x < imIn->xsize; x += ROTATE_CHUNK) { \ yysize = y + ROTATE_CHUNK < imIn->ysize ? y + ROTATE_CHUNK : imIn->ysize; \ xxsize = x + ROTATE_CHUNK < imIn->xsize ? x + ROTATE_CHUNK : imIn->xsize; \ - yr = imIn->ysize - 1 - y; \ - for (yy = y; yy < yysize; yy++, yr--) { \ - for (xx = x; xx < xxsize; xx++) { \ - imOut->image[xx][yr] = imIn->image[yy][xx]; \ + for (yy = y; yy < yysize; yy += ROTATE_SMALL_CHUNK) { \ + for (xx = x; xx < xxsize; xx += ROTATE_SMALL_CHUNK) { \ + yyysize = yy + ROTATE_SMALL_CHUNK < imIn->ysize ? yy + ROTATE_SMALL_CHUNK : imIn->ysize; \ + xxxsize = xx + ROTATE_SMALL_CHUNK < imIn->xsize ? xx + ROTATE_SMALL_CHUNK : imIn->xsize; \ + yr = imIn->ysize - 1 - yy; \ + for (yyy = yy; yyy < yyysize; yyy++, yr--) { \ + INT* in = imIn->image[yyy]; \ + for (xxx = xx; xxx < xxxsize; xxx++) { \ + imOut->image[xxx][yr] = in[xxx]; \ + } \ + } \ } \ } \ } \ @@ -228,9 +304,9 @@ ImagingRotate270(Imaging imOut, Imaging imIn) ImagingSectionEnter(&cookie); if (imIn->image8) - ROTATE_270(image8) + ROTATE_270(UINT8, image8) else - ROTATE_270(image32) + ROTATE_270(INT32, image32) ImagingSectionLeave(&cookie); diff --git a/libImaging/Imaging.h b/libImaging/Imaging.h index 556f3965e..32d7c001b 100644 --- a/libImaging/Imaging.h +++ b/libImaging/Imaging.h @@ -310,8 +310,9 @@ extern Imaging ImagingRankFilter(Imaging im, int size, int rank); extern Imaging ImagingRotate90(Imaging imOut, Imaging imIn); extern Imaging ImagingRotate180(Imaging imOut, Imaging imIn); extern Imaging ImagingRotate270(Imaging imOut, Imaging imIn); -extern Imaging ImagingResample(Imaging imIn, int xsize, int ysize, int filter, float box[4]); extern Imaging ImagingTranspose(Imaging imOut, Imaging imIn); +extern Imaging ImagingTransverse(Imaging imOut, Imaging imIn); +extern Imaging ImagingResample(Imaging imIn, int xsize, int ysize, int filter, float box[4]); extern Imaging ImagingTransform( Imaging imOut, Imaging imIn, int method, int x0, int y0, int x1, int y1, double *a, int filter, int fill); diff --git a/winbuild/config.py b/winbuild/config.py index 87d485568..310a58eb8 100644 --- a/winbuild/config.py +++ b/winbuild/config.py @@ -34,9 +34,9 @@ libs = { 'dir': 'tiff-4.0.8', }, 'freetype': { - 'url': 'https://download.savannah.gnu.org/releases/freetype/freetype-2.8.tar.gz', - 'filename': PILLOW_DEPENDS_DIR + 'freetype-2.8.tar.gz', - 'dir': 'freetype-2.8', + 'url': 'https://download.savannah.gnu.org/releases/freetype/freetype-2.8.1.tar.gz', + 'filename': PILLOW_DEPENDS_DIR + 'freetype-2.8.1.tar.gz', + 'dir': 'freetype-2.8.1', }, 'lcms': { 'url': SF_MIRROR+'/project/lcms/lcms/2.7/lcms2-2.7.zip',