Browse Source

Fixed #19934 - Use of Pillow is now preferred over PIL.

This starts the deprecation period for PIL (support to end in 1.8).
Daniel Lindsley 12 years ago
parent
commit
33793f7c3e

+ 2 - 6
django/core/files/images.py

@@ -1,7 +1,7 @@
 """
 Utility functions for handling images.
 
-Requires PIL, as you might imagine.
+Requires Pillow (or PIL), as you might imagine.
 """
 import zlib
 
@@ -35,11 +35,7 @@ def get_image_dimensions(file_or_path, close=False):
     'close' to True to close the file at the end if it is initially in an open
     state.
     """
-    # Try to import PIL in either of the two ways it can end up installed.
-    try:
-        from PIL import ImageFile as PILImageFile
-    except ImportError:
-        import ImageFile as PILImageFile
+    from django.utils.image import ImageFile as PILImageFile
 
     p = PILImageFile.Parser()
     if hasattr(file_or_path, 'read'):

+ 2 - 6
django/core/management/validation.py

@@ -105,14 +105,10 @@ def get_validation_errors(outfile, app=None):
             if isinstance(f, models.FileField) and not f.upload_to:
                 e.add(opts, '"%s": FileFields require an "upload_to" attribute.' % f.name)
             if isinstance(f, models.ImageField):
-                # Try to import PIL in either of the two ways it can end up installed.
                 try:
-                    from PIL import Image
+                    from django.utils.image import Image
                 except ImportError:
-                    try:
-                        import Image
-                    except ImportError:
-                        e.add(opts, '"%s": To use ImageFields, you need to install the Python Imaging Library. Get it at http://www.pythonware.com/products/pil/ .' % f.name)
+                    e.add(opts, '"%s": To use ImageFields, you need to install Pillow. Get it at https://pypi.python.org/pypi/Pillow.' % f.name)
             if isinstance(f, models.BooleanField) and getattr(f, 'null', False):
                 e.add(opts, '"%s": BooleanFields do not accept null values. Use a NullBooleanField instead.' % f.name)
             if isinstance(f, models.FilePathField) and not (f.allow_files or f.allow_folders):

+ 4 - 12
django/forms/fields.py

@@ -602,13 +602,9 @@ class ImageField(FileField):
         if f is None:
             return None
 
-        # Try to import PIL in either of the two ways it can end up installed.
-        try:
-            from PIL import Image
-        except ImportError:
-            import Image
+        from django.utils.image import Image
 
-        # We need to get a file object for PIL. We might have a path or we might
+        # We need to get a file object for Pillow. We might have a path or we might
         # have to read the data into memory.
         if hasattr(data, 'temporary_file_path'):
             file = data.temporary_file_path()
@@ -623,12 +619,8 @@ class ImageField(FileField):
             # image in memory, which is a DoS vector. See #3848 and #18520.
             # verify() must be called immediately after the constructor.
             Image.open(file).verify()
-        except ImportError:
-            # Under PyPy, it is possible to import PIL. However, the underlying
-            # _imaging C module isn't available, so an ImportError will be
-            # raised. Catch and re-raise.
-            raise
-        except Exception: # Python Imaging Library doesn't recognize it as an image
+        except Exception:
+            # Pillow (or PIL) doesn't recognize it as an image.
             six.reraise(ValidationError, ValidationError(self.error_messages['invalid_image']), sys.exc_info()[2])
         if hasattr(f, 'seek') and callable(f.seek):
             f.seek(0)

+ 146 - 0
django/utils/image.py

@@ -0,0 +1,146 @@
+# -*- coding: utf-8 -*-
+"""
+To provide a shim layer over Pillow/PIL situation until the PIL support is
+removed.
+
+
+Combinations To Account For
+===========================
+
+* Pillow:
+
+    * never has ``_imaging`` under any Python
+    * has the ``Image.alpha_composite``, which may aid in detection
+
+* PIL
+
+    * CPython 2.x may have _imaging (& work)
+    * CPython 2.x may *NOT* have _imaging (broken & needs a error message)
+    * CPython 3.x doesn't work
+    * PyPy will *NOT* have _imaging (but works?)
+
+Restated, that looks like:
+
+* If we're on Python 2.x, it could be either Pillow or PIL:
+
+    * If ``import _imaging`` results in ``ImportError``, either they have a
+      working Pillow installation or a broken PIL installation, so we need to
+      detect further:
+
+        * To detect, we first ``import Image``.
+        * If ``Image`` has a ``alpha_composite`` attribute present, only Pillow
+          has this, so we assume it's working.
+        * If ``Image`` DOES NOT have a ``alpha_composite``attribute, it must be
+          PIL & is a broken (likely C compiler-less) install, which we need to
+          warn the user about.
+
+    * If ``import _imaging`` works, it must be PIL & is a working install.
+
+* Python 3.x
+
+    * If ``import Image`` works, it must be Pillow, since PIL isn't Python 3.x
+      compatible.
+
+* PyPy
+
+    * If ``import _imaging`` results in ``ImportError``, it could be either
+      Pillow or PIL, both of which work without it on PyPy, so we're fine.
+
+
+Approach
+========
+
+* Attempt to import ``Image``
+
+    * ``ImportError`` - nothing is installed, toss an exception
+    * Either Pillow or the PIL is installed, so continue detecting
+
+* Attempt to ``hasattr(Image, 'alpha_composite')``
+
+    * If it works, it's Pillow & working
+    * If it fails, we've got a PIL install, continue detecting
+
+        * The only option here is that we're on Python 2.x or PyPy, of which
+          we only care about if we're on CPython.
+        * If we're on CPython, attempt to ``import _imaging``
+
+            * ``ImportError`` - Bad install, toss an exception
+
+"""
+import warnings
+
+from django.core.exceptions import ImproperlyConfigured
+from django.utils.translation import ugettext_lazy as _
+
+
+Image = None
+_imaging = None
+ImageFile = None
+
+
+def _detect_image_library():
+    global Image
+    global _imaging
+    global ImageFile
+
+    # Skip re-attempting to import if we've already run detection.
+    if Image is not None:
+        return Image, _imaging, ImageFile
+
+    # Assume it's not there.
+    PIL_imaging = False
+
+    try:
+        # Try from the Pillow (or one variant of PIL) install location first.
+        from PIL import Image as PILImage
+    except ImportError as err:
+        try:
+            # If that failed, try the alternate import syntax for PIL.
+            import Image as PILImage
+        except ImportError as err:
+            # Neither worked, so it's likely not installed.
+            raise ImproperlyConfigured(
+                _(u"Neither Pillow nor PIL could be imported: %s" % err)
+            )
+
+    # ``Image.alpha_composite`` was added to Pillow in SHA: e414c6 & is not
+    # available in any version of the PIL.
+    if hasattr(PILImage, u'alpha_composite'):
+        PIL_imaging = False
+    else:
+        # We're dealing with the PIL. Determine if we're on CPython & if
+        # ``_imaging`` is available.
+        import platform
+
+        # This is the Alex Approved™ way.
+        # See http://mail.python.org/pipermail//pypy-dev/2011-November/008739.html
+        if platform.python_implementation().lower() == u'cpython':
+            # We're on CPython (likely 2.x). Since a C compiler is needed to
+            # produce a fully-working PIL & will create a ``_imaging`` module,
+            # we'll attempt to import it to verify their kit works.
+            try:
+                import _imaging as PIL_imaging
+            except ImportError as err:
+                raise ImproperlyConfigured(
+                    _(u"The '_imaging' module for the PIL could not be " +
+                      u"imported: %s" % err)
+                )
+
+    # Try to import ImageFile as well.
+    try:
+        from PIL import ImageFile as PILImageFile
+    except ImportError:
+        import ImageFile as PILImageFile
+
+    # Finally, warn about deprecation...
+    if PIL_imaging is not False:
+        warnings.warn(
+            "Support for the PIL will be removed in Django 1.8. Please " +
+            "uninstall it & install Pillow instead.",
+            PendingDeprecationWarning
+        )
+
+    return PILImage, PIL_imaging, PILImageFile
+
+
+Image, _imaging, ImageFile = _detect_image_library()

+ 1 - 1
docs/faq/contributing.txt

@@ -27,7 +27,7 @@ to make it dead easy, even for someone who may not be intimately familiar with
 that area of the code, to understand the problem and verify the fix:
 
 * Are there clear instructions on how to reproduce the bug? If this
-  touches a dependency (such as PIL), a contrib module, or a specific
+  touches a dependency (such as Pillow/PIL), a contrib module, or a specific
   database, are those instructions clear enough even for someone not
   familiar with it?
 

+ 6 - 0
docs/internals/deprecation.txt

@@ -365,6 +365,12 @@ these changes.
 * ``django.conf.urls.shortcut`` and ``django.views.defaults.shortcut`` will be
   removed.
 
+* Support for the Python Imaging Library (PIL) module will be removed, as it
+  no longer appears to be actively maintained & does not work on Python 3.
+  You are advised to install `Pillow`_, which should be used instead.
+
+.. _`Pillow`: https://pypi.python.org/pypi/Pillow
+
 * The following private APIs will be removed:
 
   - ``django.db.close_connection()``

+ 7 - 5
docs/ref/forms/fields.txt

@@ -608,19 +608,21 @@ For each field, we describe the default widget used if you don't specify
     * Normalizes to: An ``UploadedFile`` object that wraps the file content
       and file name into a single object.
     * Validates that file data has been bound to the form, and that the
-      file is of an image format understood by PIL.
+      file is of an image format understood by Pillow/PIL.
     * Error message keys: ``required``, ``invalid``, ``missing``, ``empty``,
       ``invalid_image``
 
-    Using an ``ImageField`` requires that the `Python Imaging Library`_ (PIL)
-    is installed and supports the image formats you use. If you encounter a
-    ``corrupt image`` error when you upload an image, it usually means PIL
+    Using an ``ImageField`` requires that either `Pillow`_ (recommended) or the
+    `Python Imaging Library`_ (PIL) are installed and supports the image
+    formats you use. If you encounter a ``corrupt image`` error when you
+    upload an image, it usually means either Pillow or PIL
     doesn't understand its format. To fix this, install the appropriate
-    library and reinstall PIL.
+    library and reinstall Pillow or PIL.
 
     When you use an ``ImageField`` on a form, you must also remember to
     :ref:`bind the file data to the form <binding-uploaded-files>`.
 
+.. _Pillow: http://python-imaging.github.io/Pillow/
 .. _Python Imaging Library: http://www.pythonware.com/products/pil/
 
 ``IntegerField``

+ 7 - 0
docs/releases/1.6.txt

@@ -220,6 +220,13 @@ Minor features
 * Added ``BCryptSHA256PasswordHasher`` to resolve the password truncation issue
   with bcrypt.
 
+* `Pillow`_ is now the preferred image manipulation library to use with Django.
+  `PIL`_ is pending deprecation (support to be removed in Django 1.8).
+  To upgrade, you should **first** uninstall PIL, **then** install Pillow.
+
+.. _`Pillow`: https://pypi.python.org/pypi/Pillow
+.. _`PIL`: https://pypi.python.org/pypi/PIL
+
 Backwards incompatible changes in 1.6
 =====================================
 

+ 7 - 13
tests/file_storage/tests.py

@@ -29,16 +29,10 @@ from django.utils._os import upath
 from django.test.utils import override_settings
 from servers.tests import LiveServerBase
 
-# Try to import PIL in either of the two ways it can end up installed.
-# Checking for the existence of Image is enough for CPython, but
-# for PyPy, you need to check for the underlying modules
 try:
-    from PIL import Image, _imaging
-except ImportError:
-    try:
-        import Image, _imaging
-    except ImportError:
-        Image = None
+    from django.utils.image import Image
+except ImproperlyConfigured:
+    Image = None
 
 
 class GetStorageClassTests(SimpleTestCase):
@@ -494,7 +488,7 @@ class DimensionClosingBug(unittest.TestCase):
     """
     Test that get_image_dimensions() properly closes files (#8817)
     """
-    @unittest.skipUnless(Image, "PIL not installed")
+    @unittest.skipUnless(Image, "Pillow/PIL not installed")
     def test_not_closing_of_files(self):
         """
         Open files passed into get_image_dimensions() should stay opened.
@@ -505,7 +499,7 @@ class DimensionClosingBug(unittest.TestCase):
         finally:
             self.assertTrue(not empty_io.closed)
 
-    @unittest.skipUnless(Image, "PIL not installed")
+    @unittest.skipUnless(Image, "Pillow/PIL not installed")
     def test_closing_of_filenames(self):
         """
         get_image_dimensions() called with a filename should closed the file.
@@ -542,7 +536,7 @@ class InconsistentGetImageDimensionsBug(unittest.TestCase):
     Test that get_image_dimensions() works properly after various calls
     using a file handler (#11158)
     """
-    @unittest.skipUnless(Image, "PIL not installed")
+    @unittest.skipUnless(Image, "Pillow/PIL not installed")
     def test_multiple_calls(self):
         """
         Multiple calls of get_image_dimensions() should return the same size.
@@ -556,7 +550,7 @@ class InconsistentGetImageDimensionsBug(unittest.TestCase):
         self.assertEqual(image_pil.size, size_1)
         self.assertEqual(size_1, size_2)
 
-    @unittest.skipUnless(Image, "PIL not installed")
+    @unittest.skipUnless(Image, "Pillow/PIL not installed")
     def test_bug_19457(self):
         """
         Regression test for #19457

+ 6 - 11
tests/model_fields/models.py

@@ -1,17 +1,12 @@
 import os
 import tempfile
 
-# Try to import PIL in either of the two ways it can end up installed.
-# Checking for the existence of Image is enough for CPython, but for PyPy,
-# you need to check for the underlying modules.
+from django.core.exceptions import ImproperlyConfigured
 
 try:
-    from PIL import Image, _imaging
-except ImportError:
-    try:
-        import Image, _imaging
-    except ImportError:
-        Image = None
+    from django.utils.image import Image
+except ImproperlyConfigured:
+    Image = None
 
 from django.core.files.storage import FileSystemStorage
 from django.db import models
@@ -87,7 +82,7 @@ class VerboseNameField(models.Model):
     field9 = models.FileField("verbose field9", upload_to="unused")
     field10 = models.FilePathField("verbose field10")
     field11 = models.FloatField("verbose field11")
-    # Don't want to depend on PIL in this test
+    # Don't want to depend on Pillow/PIL in this test
     #field_image = models.ImageField("verbose field")
     field12 = models.IntegerField("verbose field12")
     field13 = models.IPAddressField("verbose field13")
@@ -119,7 +114,7 @@ class Document(models.Model):
 ###############################################################################
 # ImageField
 
-# If PIL available, do these tests.
+# If Pillow/PIL available, do these tests.
 if Image:
     class TestImageFieldFile(ImageFieldFile):
         """

+ 6 - 2
tests/model_fields/test_imagefield.py

@@ -3,20 +3,24 @@ from __future__ import absolute_import
 import os
 import shutil
 
+from django.core.exceptions import ImproperlyConfigured
 from django.core.files import File
 from django.core.files.images import ImageFile
 from django.test import TestCase
 from django.utils._os import upath
 from django.utils.unittest import skipIf
 
-from .models import Image
+try:
+    from .models import Image
+except ImproperlyConfigured:
+    Image = None
 
 if Image:
     from .models import (Person, PersonWithHeight, PersonWithHeightAndWidth,
         PersonDimensionsFirst, PersonTwoImages, TestImageFieldFile)
     from .models import temp_storage_dir
 else:
-    # PIL not available, create dummy classes (tests will be skipped anyway)
+    # Pillow not available, create dummy classes (tests will be skipped anyway)
     class Person():
         pass
     PersonWithHeight = PersonWithHeightAndWidth = PersonDimensionsFirst = Person

+ 3 - 9
tests/model_forms/models.py

@@ -11,6 +11,7 @@ from __future__ import unicode_literals
 import os
 import tempfile
 
+from django.core.exceptions import ImproperlyConfigured
 from django.core.files.storage import FileSystemStorage
 from django.db import models
 from django.utils import six
@@ -91,14 +92,7 @@ class TextFile(models.Model):
         return self.description
 
 try:
-    # If PIL is available, try testing ImageFields. Checking for the existence
-    # of Image is enough for CPython, but for PyPy, you need to check for the
-    # underlying modules If PIL is not available, ImageField tests are omitted.
-    # Try to import PIL in either of the two ways it can end up installed.
-    try:
-        from PIL import Image, _imaging
-    except ImportError:
-        import Image, _imaging
+    from django.utils.image import Image
 
     test_images = True
 
@@ -137,7 +131,7 @@ try:
 
         def __str__(self):
             return self.description
-except ImportError:
+except ImproperlyConfigured:
     test_images = False
 
 @python_2_unicode_compatible

+ 1 - 1
tests/serializers_regress/models.py

@@ -2,7 +2,7 @@
 A test spanning all the capabilities of all the serializers.
 
 This class sets up a model for each model field type
-(except for image types, because of the PIL dependency).
+(except for image types, because of the Pillow/PIL dependency).
 """
 
 from django.db import models