From 5f4c1e113c91246e546c7f021cb2da6328606666 Mon Sep 17 00:00:00 2001 From: nulano Date: Fri, 27 Sep 2019 00:09:04 +0200 Subject: [PATCH 01/11] add libimagequant to features.py --- selftest.py | 28 +------------------- src/PIL/features.py | 64 +++++++++++++++++++++++++-------------------- src/_imaging.c | 6 +++++ 3 files changed, 42 insertions(+), 56 deletions(-) diff --git a/selftest.py b/selftest.py index dcac54a5a..817b2872a 100755 --- a/selftest.py +++ b/selftest.py @@ -2,7 +2,6 @@ # minimal sanity check from __future__ import print_function -import os import sys from PIL import Image, features @@ -162,32 +161,7 @@ if __name__ == "__main__": exit_status = 0 - print("-" * 68) - print("Pillow", Image.__version__, "TEST SUMMARY ") - print("-" * 68) - print("Python modules loaded from", os.path.dirname(Image.__file__)) - print("Binary modules loaded from", os.path.dirname(Image.core.__file__)) - print("-" * 68) - for name, feature in [ - ("pil", "PIL CORE"), - ("tkinter", "TKINTER"), - ("freetype2", "FREETYPE2"), - ("littlecms2", "LITTLECMS2"), - ("webp", "WEBP"), - ("transp_webp", "WEBP Transparency"), - ("webp_mux", "WEBPMUX"), - ("webp_anim", "WEBP Animation"), - ("jpg", "JPEG"), - ("jpg_2000", "OPENJPEG (JPEG2000)"), - ("zlib", "ZLIB (PNG/ZIP)"), - ("libtiff", "LIBTIFF"), - ("raqm", "RAQM (Bidirectional Text)"), - ]: - if features.check(name): - print("---", feature, "support ok") - else: - print("***", feature, "support not installed") - print("-" * 68) + features.pilinfo(sys.stdout, True) # use doctest to make sure the test program behaves as documented! import doctest diff --git a/src/PIL/features.py b/src/PIL/features.py index 9fd522368..e8ce1e4ff 100644 --- a/src/PIL/features.py +++ b/src/PIL/features.py @@ -56,6 +56,7 @@ features = { "transp_webp": ("PIL._webp", "HAVE_TRANSPARENCY"), "raqm": ("PIL._imagingft", "HAVE_RAQM"), "libjpeg_turbo": ("PIL._imaging", "HAVE_LIBJPEGTURBO"), + "libimagequant": ("PIL._imaging", "HAVE_LIBIMAGEQUANT"), } @@ -94,7 +95,7 @@ def get_supported(): return ret -def pilinfo(out=None): +def pilinfo(out=None, brief=False): if out is None: out = sys.stdout @@ -113,11 +114,12 @@ def pilinfo(out=None): ) print("-" * 68, file=out) - v = sys.version.splitlines() - print("Python {}".format(v[0].strip()), file=out) - for v in v[1:]: - print(" {}".format(v.strip()), file=out) - print("-" * 68, file=out) + if not brief: + v = sys.version.splitlines() + print("Python {}".format(v[0].strip()), file=out) + for v in v[1:]: + print(" {}".format(v.strip()), file=out) + print("-" * 68, file=out) for name, feature in [ ("pil", "PIL CORE"), @@ -133,6 +135,7 @@ def pilinfo(out=None): ("zlib", "ZLIB (PNG/ZIP)"), ("libtiff", "LIBTIFF"), ("raqm", "RAQM (Bidirectional Text)"), + ("libimagequant", "LIBIMAGEQUANT (quantization method)"), ]: if check(name): print("---", feature, "support ok", file=out) @@ -140,30 +143,33 @@ def pilinfo(out=None): print("***", feature, "support not installed", file=out) print("-" * 68, file=out) - extensions = collections.defaultdict(list) - for ext, i in Image.EXTENSION.items(): - extensions[i].append(ext) + if not brief: + extensions = collections.defaultdict(list) + for ext, i in Image.EXTENSION.items(): + extensions[i].append(ext) - for i in sorted(Image.ID): - line = "{}".format(i) - if i in Image.MIME: - line = "{} {}".format(line, Image.MIME[i]) - print(line, file=out) + for i in sorted(Image.ID): + line = "{}".format(i) + if i in Image.MIME: + line = "{} {}".format(line, Image.MIME[i]) + print(line, file=out) - if i in extensions: - print("Extensions: {}".format(", ".join(sorted(extensions[i]))), file=out) + if i in extensions: + print( + "Extensions: {}".format(", ".join(sorted(extensions[i]))), file=out + ) - features = [] - if i in Image.OPEN: - features.append("open") - if i in Image.SAVE: - features.append("save") - if i in Image.SAVE_ALL: - features.append("save_all") - if i in Image.DECODERS: - features.append("decode") - if i in Image.ENCODERS: - features.append("encode") + features = [] + if i in Image.OPEN: + features.append("open") + if i in Image.SAVE: + features.append("save") + if i in Image.SAVE_ALL: + features.append("save_all") + if i in Image.DECODERS: + features.append("decode") + if i in Image.ENCODERS: + features.append("encode") - print("Features: {}".format(", ".join(features)), file=out) - print("-" * 68, file=out) + print("Features: {}".format(", ".join(features)), file=out) + print("-" * 68, file=out) diff --git a/src/_imaging.c b/src/_imaging.c index 04520b1a1..81de38e2f 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -3934,6 +3934,12 @@ setup_module(PyObject* m) { PyModule_AddObject(m, "HAVE_LIBJPEGTURBO", Py_False); #endif +#ifdef HAVE_LIBIMAGEQUANT + PyModule_AddObject(m, "HAVE_LIBIMAGEQUANT", Py_True); +#else + PyModule_AddObject(m, "HAVE_LIBIMAGEQUANT", Py_False); +#endif + #ifdef HAVE_LIBZ /* zip encoding strategies */ PyModule_AddIntConstant(m, "DEFAULT_STRATEGY", Z_DEFAULT_STRATEGY); From accbe58b5eaf86a1a372ebc1882899fe4264c0c9 Mon Sep 17 00:00:00 2001 From: nulano Date: Sat, 12 Oct 2019 15:01:18 +0100 Subject: [PATCH 02/11] add Python version to selftest, rename brief parameter --- Tests/test_features.py | 13 ++++++++----- Tests/test_main.py | 13 ++++++++----- selftest.py | 2 +- src/PIL/features.py | 17 +++++++---------- 4 files changed, 24 insertions(+), 21 deletions(-) diff --git a/Tests/test_features.py b/Tests/test_features.py index 64b0302ca..06da35ee4 100644 --- a/Tests/test_features.py +++ b/Tests/test_features.py @@ -70,11 +70,14 @@ class TestFeatures(PillowTestCase): lines = out.splitlines() self.assertEqual(lines[0], "-" * 68) self.assertTrue(lines[1].startswith("Pillow ")) - self.assertEqual(lines[2], "-" * 68) - self.assertTrue(lines[3].startswith("Python modules loaded from ")) - self.assertTrue(lines[4].startswith("Binary modules loaded from ")) - self.assertEqual(lines[5], "-" * 68) - self.assertTrue(lines[6].startswith("Python ")) + self.assertTrue(lines[2].startswith("Python ")) + lines = lines[3:] + while lines[0].startswith(" "): + lines = lines[1:] + self.assertEqual(lines[0], "-" * 68) + self.assertTrue(lines[1].startswith("Python modules loaded from ")) + self.assertTrue(lines[2].startswith("Binary modules loaded from ")) + self.assertEqual(lines[3], "-" * 68) jpeg = ( "\n" + "-" * 68 diff --git a/Tests/test_main.py b/Tests/test_main.py index 847def834..8258396c1 100644 --- a/Tests/test_main.py +++ b/Tests/test_main.py @@ -12,11 +12,14 @@ class TestMain(TestCase): lines = out.splitlines() self.assertEqual(lines[0], "-" * 68) self.assertTrue(lines[1].startswith("Pillow ")) - self.assertEqual(lines[2], "-" * 68) - self.assertTrue(lines[3].startswith("Python modules loaded from ")) - self.assertTrue(lines[4].startswith("Binary modules loaded from ")) - self.assertEqual(lines[5], "-" * 68) - self.assertTrue(lines[6].startswith("Python ")) + self.assertTrue(lines[2].startswith("Python ")) + lines = lines[3:] + while lines[0].startswith(" "): + lines = lines[1:] + self.assertEqual(lines[0], "-" * 68) + self.assertTrue(lines[1].startswith("Python modules loaded from ")) + self.assertTrue(lines[2].startswith("Binary modules loaded from ")) + self.assertEqual(lines[3], "-" * 68) jpeg = ( os.linesep + "-" * 68 diff --git a/selftest.py b/selftest.py index 817b2872a..aef950b7f 100755 --- a/selftest.py +++ b/selftest.py @@ -161,7 +161,7 @@ if __name__ == "__main__": exit_status = 0 - features.pilinfo(sys.stdout, True) + features.pilinfo(sys.stdout, False) # use doctest to make sure the test program behaves as documented! import doctest diff --git a/src/PIL/features.py b/src/PIL/features.py index e8ce1e4ff..7b007dc56 100644 --- a/src/PIL/features.py +++ b/src/PIL/features.py @@ -95,7 +95,7 @@ def get_supported(): return ret -def pilinfo(out=None, brief=False): +def pilinfo(out=None, supported_formats=True): if out is None: out = sys.stdout @@ -103,6 +103,10 @@ def pilinfo(out=None, brief=False): print("-" * 68, file=out) print("Pillow {}".format(PIL.__version__), file=out) + py_version = sys.version.splitlines() + print("Python {}".format(py_version[0].strip()), file=out) + for py_version in py_version[1:]: + print(" {}".format(py_version.strip()), file=out) print("-" * 68, file=out) print( "Python modules loaded from {}".format(os.path.dirname(Image.__file__)), @@ -114,13 +118,6 @@ def pilinfo(out=None, brief=False): ) print("-" * 68, file=out) - if not brief: - v = sys.version.splitlines() - print("Python {}".format(v[0].strip()), file=out) - for v in v[1:]: - print(" {}".format(v.strip()), file=out) - print("-" * 68, file=out) - for name, feature in [ ("pil", "PIL CORE"), ("tkinter", "TKINTER"), @@ -135,7 +132,7 @@ def pilinfo(out=None, brief=False): ("zlib", "ZLIB (PNG/ZIP)"), ("libtiff", "LIBTIFF"), ("raqm", "RAQM (Bidirectional Text)"), - ("libimagequant", "LIBIMAGEQUANT (quantization method)"), + ("libimagequant", "LIBIMAGEQUANT (Quantization method)"), ]: if check(name): print("---", feature, "support ok", file=out) @@ -143,7 +140,7 @@ def pilinfo(out=None, brief=False): print("***", feature, "support not installed", file=out) print("-" * 68, file=out) - if not brief: + if supported_formats: extensions = collections.defaultdict(list) for ext, i in Image.EXTENSION.items(): extensions[i].append(ext) From 2694564d08fd147b40641cee80fe594c8bba864c Mon Sep 17 00:00:00 2001 From: Christoph Gohlke Date: Mon, 21 Oct 2019 14:47:51 -0700 Subject: [PATCH 03/11] Do not destroy glyph while its bitmap is used --- src/_imagingft.c | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/_imagingft.c b/src/_imagingft.c index 7776e43f1..0e8622844 100644 --- a/src/_imagingft.c +++ b/src/_imagingft.c @@ -791,7 +791,7 @@ font_render(FontObject* self, PyObject* args) int index, error, ascender, horizontal_dir; int load_flags; unsigned char *source; - FT_Glyph glyph; + FT_Glyph glyph = NULL; FT_GlyphSlot glyph_slot; FT_Bitmap bitmap; FT_BitmapGlyph bitmap_glyph; @@ -890,8 +890,6 @@ font_render(FontObject* self, PyObject* args) bitmap = bitmap_glyph->bitmap; left = bitmap_glyph->left; - - FT_Done_Glyph(glyph); } else { bitmap = glyph_slot->bitmap; left = glyph_slot->bitmap_left; @@ -953,6 +951,10 @@ font_render(FontObject* self, PyObject* args) } x += glyph_info[i].x_advance; y -= glyph_info[i].y_advance; + if (glyph != NULL) { + FT_Done_Glyph(glyph); + glyph = NULL; + } } FT_Stroker_Done(stroker); From a2225ae961d16185d7191b5fbb325addc5f4048f Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 1 Nov 2019 21:34:38 +1100 Subject: [PATCH 04/11] Employ same condition used to set glyph --- src/_imagingft.c | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/_imagingft.c b/src/_imagingft.c index 0e8622844..146df095b 100644 --- a/src/_imagingft.c +++ b/src/_imagingft.c @@ -791,7 +791,7 @@ font_render(FontObject* self, PyObject* args) int index, error, ascender, horizontal_dir; int load_flags; unsigned char *source; - FT_Glyph glyph = NULL; + FT_Glyph glyph; FT_GlyphSlot glyph_slot; FT_Bitmap bitmap; FT_BitmapGlyph bitmap_glyph; @@ -951,9 +951,8 @@ font_render(FontObject* self, PyObject* args) } x += glyph_info[i].x_advance; y -= glyph_info[i].y_advance; - if (glyph != NULL) { + if (stroker != NULL) { FT_Done_Glyph(glyph); - glyph = NULL; } } From c9c02c513b841e63c6947528881fb5deab8a1869 Mon Sep 17 00:00:00 2001 From: Hugo Date: Fri, 1 Nov 2019 13:42:12 +0200 Subject: [PATCH 05/11] Update docs for 7.0.0 --- docs/deprecations.rst | 46 ++++++++++++------------ docs/releasenotes/7.0.0.rst | 71 +++++++++++++++++++++++++++++++++++++ docs/releasenotes/index.rst | 1 + 3 files changed, 95 insertions(+), 23 deletions(-) create mode 100644 docs/releasenotes/7.0.0.rst diff --git a/docs/deprecations.rst b/docs/deprecations.rst index 396300f1f..3a48f37ee 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -12,29 +12,6 @@ Deprecated features Below are features which are considered deprecated. Where appropriate, a ``DeprecationWarning`` is issued. -Image.__del__ -~~~~~~~~~~~~~ - -.. deprecated:: 6.1.0 - -Implicitly closing the image's underlying file in ``Image.__del__`` has been deprecated. -Use a context manager or call ``Image.close()`` instead to close the file in a -deterministic way. - -Deprecated: - -.. code-block:: python - - im = Image.open("hopper.png") - im.save("out.jpg") - -Use instead: - -.. code-block:: python - - with Image.open("hopper.png") as im: - im.save("out.jpg") - Python 2.7 ~~~~~~~~~~ @@ -96,6 +73,29 @@ Removed features Deprecated features are only removed in major releases after an appropriate period of deprecation has passed. +Image.__del__ +~~~~~~~~~~~~~ + +*Removed in version 7.0.0.* + +Implicitly closing the image's underlying file in ``Image.__del__`` has been removed. +Use a context manager or call ``Image.close()`` instead to close the file in a +deterministic way. + +Deprecated: + +.. code-block:: python + + im = Image.open("hopper.png") + im.save("out.jpg") + +Use instead: + +.. code-block:: python + + with Image.open("hopper.png") as im: + im.save("out.jpg") + PILLOW_VERSION constant ~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/docs/releasenotes/7.0.0.rst b/docs/releasenotes/7.0.0.rst new file mode 100644 index 000000000..4fb6773c4 --- /dev/null +++ b/docs/releasenotes/7.0.0.rst @@ -0,0 +1,71 @@ +7.0.0 +----- + +Backwards Incompatible Changes +============================== + +PILLOW_VERSION constant +^^^^^^^^^^^^^^^^^^^^^^^ + +``PILLOW_VERSION`` has been removed. Use ``__version__`` instead. + +PyQt4 and PySide +^^^^^^^^^^^^^^^^ + +Qt 4 reached end-of-life on 2015-12-19. Its Python bindings are also EOL: PyQt4 since +2018-08-31 and PySide since 2015-10-14. + +Support for PyQt4 and PySide has been removed from ``ImageQt``. Please upgrade to PyQt5 +or PySide2. + +Setting the size of TIFF images +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Setting the size of a TIFF image directly (eg. ``im.size = (256, 256)``) throws +an error. Use ``Image.resize`` instead. + + +API Changes +=========== + +Deprecations +^^^^^^^^^^^^ + +TODO +~~~~ + +TODO + + +API Additions +============= + +TODO +^^^^ + +TODO + + +Other Changes +============= + +Image.__del__ +^^^^^^^^^^^^^ + +Implicitly closing the image's underlying file in ``Image.__del__`` has been removed. +Use a context manager or call ``Image.close()`` instead to close the file in a +deterministic way. + +Deprecated: + +.. code-block:: python + + im = Image.open("hopper.png") + im.save("out.jpg") + +Use instead: + +.. code-block:: python + + with Image.open("hopper.png") as im: + im.save("out.jpg") diff --git a/docs/releasenotes/index.rst b/docs/releasenotes/index.rst index 381643cf3..6f8ecb95c 100644 --- a/docs/releasenotes/index.rst +++ b/docs/releasenotes/index.rst @@ -6,6 +6,7 @@ Release Notes .. toctree:: :maxdepth: 2 + 7.0.0 6.2.1 6.2.0 6.1.0 From 41ddb1ef5db8a006eb46021cfb973805300dff22 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 1 Nov 2019 23:21:54 +1100 Subject: [PATCH 06/11] Removed unused variable --- winbuild/config.py | 1 - 1 file changed, 1 deletion(-) diff --git a/winbuild/config.py b/winbuild/config.py index 72669f753..1c5ee6c28 100644 --- a/winbuild/config.py +++ b/winbuild/config.py @@ -1,7 +1,6 @@ import os SF_MIRROR = "https://iweb.dl.sourceforge.net" -PILLOW_DEPENDS_DIR = "C:\\pillow-depends\\" pythons = { # for AppVeyor From 2058e00e3e81e6e298a93d3f209d13568970fa7d Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sat, 2 Nov 2019 08:40:29 +0200 Subject: [PATCH 07/11] Update docs/deprecations.rst Co-Authored-By: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- docs/deprecations.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/deprecations.rst b/docs/deprecations.rst index 3a48f37ee..144661820 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -82,7 +82,7 @@ Implicitly closing the image's underlying file in ``Image.__del__`` has been rem Use a context manager or call ``Image.close()`` instead to close the file in a deterministic way. -Deprecated: +Previous method: .. code-block:: python From a3d16dd40ac1ae0487ff5467cac29c4c600d0ace Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sat, 2 Nov 2019 08:40:40 +0200 Subject: [PATCH 08/11] Update docs/releasenotes/7.0.0.rst Co-Authored-By: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- docs/releasenotes/7.0.0.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/releasenotes/7.0.0.rst b/docs/releasenotes/7.0.0.rst index 4fb6773c4..a6d4a6d35 100644 --- a/docs/releasenotes/7.0.0.rst +++ b/docs/releasenotes/7.0.0.rst @@ -56,7 +56,7 @@ Implicitly closing the image's underlying file in ``Image.__del__`` has been rem Use a context manager or call ``Image.close()`` instead to close the file in a deterministic way. -Deprecated: +Previous method: .. code-block:: python From b1ee44a74b0552c1d83fe9988687159ad915580b Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 2 Nov 2019 18:10:55 +1100 Subject: [PATCH 09/11] Ignore UserWarnings --- Tests/test_file_tiff.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py index ccaa91920..146bf1851 100644 --- a/Tests/test_file_tiff.py +++ b/Tests/test_file_tiff.py @@ -2,6 +2,7 @@ import logging import os from io import BytesIO +import pytest from PIL import Image, TiffImagePlugin from PIL._util import py3 from PIL.TiffImagePlugin import RESOLUTION_UNIT, X_RESOLUTION, Y_RESOLUTION @@ -594,6 +595,9 @@ class TestFileTiff(PillowTestCase): im.load() self.assertFalse(fp.closed) + # Ignore this UserWarning which triggers for four tags: + # "Possibly corrupt EXIF data. Expecting to read 50404352 bytes but..." + @pytest.mark.filterwarnings("ignore:Possibly corrupt EXIF data") def test_string_dimension(self): # Assert that an error is raised if one of the dimensions is a string with self.assertRaises(ValueError): From 690bd430b0d1883e2daf960a58cfcf06c12b2f44 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sat, 2 Nov 2019 11:06:58 +0200 Subject: [PATCH 10/11] Update docs/releasenotes/7.0.0.rst Co-Authored-By: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- docs/releasenotes/7.0.0.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/releasenotes/7.0.0.rst b/docs/releasenotes/7.0.0.rst index a6d4a6d35..48660b506 100644 --- a/docs/releasenotes/7.0.0.rst +++ b/docs/releasenotes/7.0.0.rst @@ -15,7 +15,7 @@ PyQt4 and PySide Qt 4 reached end-of-life on 2015-12-19. Its Python bindings are also EOL: PyQt4 since 2018-08-31 and PySide since 2015-10-14. -Support for PyQt4 and PySide has been removed from ``ImageQt``. Please upgrade to PyQt5 +Support for PyQt4 and PySide has been removed from ``ImageQt``. Please upgrade to PyQt5 or PySide2. Setting the size of TIFF images From 4483642e45242b9c5d6da5a5cfbf5d0b8a7fc477 Mon Sep 17 00:00:00 2001 From: Jon Dufresne Date: Sun, 3 Nov 2019 07:35:48 -0800 Subject: [PATCH 11/11] Reuse deferred_error instead of _imaging_not_installed deferred_error is a general implementation of _imaging_not_installed. Can reuse rather than repeating the same logic. --- src/PIL/Image.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index d65810d8b..42487ba93 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -69,12 +69,6 @@ class DecompressionBombError(Exception): pass -class _imaging_not_installed(object): - # module placeholder - def __getattr__(self, id): - raise ImportError("The _imaging C module is not installed") - - # Limit to around a quarter gigabyte for a 24 bit (3 bpp) image MAX_IMAGE_PIXELS = int(1024 * 1024 * 1024 // 4 // 3) @@ -95,7 +89,7 @@ try: ) except ImportError as v: - core = _imaging_not_installed() + core = deferred_error(ImportError("The _imaging C module is not installed.")) # Explanations for ways that we know we might have an import error if str(v).startswith("Module use of python"): # The _imaging C module is present, but not compiled for