diff --git a/.azure-pipelines/jobs/lint.yml b/.azure-pipelines/jobs/lint.yml new file mode 100644 index 000000000..d017590f8 --- /dev/null +++ b/.azure-pipelines/jobs/lint.yml @@ -0,0 +1,28 @@ +parameters: + name: '' # defaults for any parameters that aren't specified + vmImage: '' + +jobs: + +- job: ${{ parameters.name }} + pool: + vmImage: ${{ parameters.vmImage }} + + strategy: + matrix: + Python37: + python.version: '3.7' + + steps: + - task: UsePythonVersion@0 + inputs: + versionSpec: '$(python.version)' + architecture: 'x64' + + - script: | + python -m pip install --upgrade tox + displayName: 'Install dependencies' + + - script: | + tox -e lint + displayName: 'Lint' diff --git a/.azure-pipelines/jobs/test-docker.yml b/.azure-pipelines/jobs/test-docker.yml new file mode 100644 index 000000000..41dc2daec --- /dev/null +++ b/.azure-pipelines/jobs/test-docker.yml @@ -0,0 +1,22 @@ +parameters: + docker: '' # defaults for any parameters that aren't specified + dockerTag: 'master' + name: '' + vmImage: 'Ubuntu-16.04' + +jobs: + +- job: ${{ parameters.name }} + pool: + vmImage: ${{ parameters.vmImage }} + + steps: + - script: | + docker pull pythonpillow/${{ parameters.docker }}:${{ parameters.dockerTag }} + displayName: 'Docker pull' + + - script: | + # The Pillow user in the docker container is UID 1000 + sudo chown -R 1000 $(Build.SourcesDirectory) + docker run -v $(Build.SourcesDirectory):/Pillow pythonpillow/${{ parameters.docker }}:${{ parameters.dockerTag }} + displayName: 'Docker build' diff --git a/CHANGES.rst b/CHANGES.rst index e1c08d8bd..01fc87bf7 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,39 @@ Changelog (Pillow) 6.0.0 (unreleased) ------------------ +- Python 2.7 support will be removed in Pillow 7.0.0 #3682 + [hugovk] + +- Removed deprecated VERSION #3624 + [hugovk] + +- Fix 'BytesWarning: Comparison between bytes and string' in PdfDict #3580 + [jdufresne] + +- Do not resize in Image.thumbnail if already the destination size #3632 + [radarhere] + +- Replace .seek() magic numbers with io.SEEK_* constants #3572 + [jdufresne] + +- Make ContainerIO.isatty() return a bool, not int #3568 + [jdufresne] + +- Add support for I;16 modes for more transpose operations #3563 + [radarhere] + +- Deprecate support for PyQt4 and PySide #3655 + [hugovk, radarhere] + +- Add TIFF compression codecs: LZMA, Zstd, WebP #3555 + [cgohlke] + +- Fixed pickling of iTXt class with protocol > 1 #3537 + [radarhere] + +- _util.isPath returns True for pathlib.Path objects #3616 + [wbadart] + - Remove unnecessary unittest.main() boilerplate from test files #3631 [jdufresne] diff --git a/MANIFEST.in b/MANIFEST.in index 809d0d667..f11ee174c 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -23,9 +23,10 @@ exclude .codecov.yml exclude .editorconfig exclude .landscape.yaml exclude .readthedocs.yml -exclude .travis -exclude .travis/* +exclude azure-pipelines.yml exclude tox.ini global-exclude .git* global-exclude *.pyc global-exclude *.so +prune .azure-pipelines +prune .travis diff --git a/RELEASING.md b/RELEASING.md index 40a48c4d3..f28e5f134 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -88,14 +88,7 @@ Released as needed privately to individual vendors for critical security-related ```bash git clone https://github.com/python-pillow/pillow-wheels cd pillow-wheels - git submodule init - git submodule update Pillow - cd Pillow - git fetch --all - git checkout [[release tag]] - cd .. - git commit -m "Pillow -> 5.2.0" Pillow - git push + ./update-pillow-tag.sh [[release tag]] ``` * [ ] Download distributions from the [Pillow Wheel Builder container](http://a365fff413fe338398b6-1c8a9b3114517dc5fe17b7c3f8c63a43.r19.cf2.rackcdn.com/). ```bash diff --git a/Tests/images/itxt_chunks.png b/Tests/images/itxt_chunks.png new file mode 100644 index 000000000..ca098440c Binary files /dev/null and b/Tests/images/itxt_chunks.png differ diff --git a/Tests/test_000_sanity.py b/Tests/test_000_sanity.py index 321a6b3ce..b9dd413f9 100644 --- a/Tests/test_000_sanity.py +++ b/Tests/test_000_sanity.py @@ -11,8 +11,6 @@ class TestSanity(PillowTestCase): # Make sure we have the binary extension PIL.Image.core.new("L", (100, 100)) - self.assertEqual(PIL.Image.VERSION[:3], '1.1') - # Create an image and do stuff with it. im = PIL.Image.new("1", (100, 100)) self.assertEqual((im.mode, im.size), ('1', (100, 100))) diff --git a/Tests/test_file_container.py b/Tests/test_file_container.py index 8ca3310e5..04f055464 100644 --- a/Tests/test_file_container.py +++ b/Tests/test_file_container.py @@ -16,7 +16,7 @@ class TestFileContainer(PillowTestCase): im = hopper() container = ContainerIO.ContainerIO(im, 0, 0) - self.assertEqual(container.isatty(), 0) + self.assertFalse(container.isatty()) def test_seek_mode_0(self): # Arrange diff --git a/Tests/test_file_msp.py b/Tests/test_file_msp.py index 66af65fcd..724fc78b1 100644 --- a/Tests/test_file_msp.py +++ b/Tests/test_file_msp.py @@ -12,11 +12,11 @@ YA_EXTRA_DIR = "Tests/images/msp" class TestFileMsp(PillowTestCase): def test_sanity(self): - file = self.tempfile("temp.msp") + test_file = self.tempfile("temp.msp") - hopper("1").save(file) + hopper("1").save(test_file) - im = Image.open(file) + im = Image.open(test_file) im.load() self.assertEqual(im.mode, "1") self.assertEqual(im.size, (128, 128)) diff --git a/Tests/test_file_ppm.py b/Tests/test_file_ppm.py index 47f8b845e..dc239cc4c 100644 --- a/Tests/test_file_ppm.py +++ b/Tests/test_file_ppm.py @@ -14,12 +14,14 @@ class TestFilePpm(PillowTestCase): self.assertEqual(im.mode, "RGB") self.assertEqual(im.size, (128, 128)) self.assertEqual(im.format, "PPM") + self.assertEqual(im.get_format_mimetype(), "image/x-portable-pixmap") def test_16bit_pgm(self): im = Image.open('Tests/images/16_bit_binary.pgm') im.load() self.assertEqual(im.mode, 'I') self.assertEqual(im.size, (20, 100)) + self.assertEqual(im.get_format_mimetype(), "image/x-portable-graymap") tgt = Image.open('Tests/images/16_bit_binary_pgm.png') self.assert_image_equal(im, tgt) @@ -49,3 +51,16 @@ class TestFilePpm(PillowTestCase): with self.assertRaises(IOError): Image.open('Tests/images/negative_size.ppm') + + def test_mimetypes(self): + path = self.tempfile('temp.pgm') + + with open(path, 'w') as f: + f.write("P4\n128 128\n255") + im = Image.open(path) + self.assertEqual(im.get_format_mimetype(), "image/x-portable-bitmap") + + with open(path, 'w') as f: + f.write("PyCMYK\n128 128\n255") + im = Image.open(path) + self.assertEqual(im.get_format_mimetype(), "image/x-portable-anymap") diff --git a/Tests/test_image_thumbnail.py b/Tests/test_image_thumbnail.py index d03ea4141..fbadf50cf 100644 --- a/Tests/test_image_thumbnail.py +++ b/Tests/test_image_thumbnail.py @@ -1,4 +1,5 @@ from .helper import PillowTestCase, hopper +from PIL import Image class TestImageThumbnail(PillowTestCase): @@ -35,3 +36,14 @@ class TestImageThumbnail(PillowTestCase): im = hopper().resize((128, 128)) im.thumbnail((100, 100)) self.assert_image(im, im.mode, (100, 100)) + + def test_no_resize(self): + # Check that draft() can resize the image to the destination size + im = Image.open("Tests/images/hopper.jpg") + im.draft(None, (64, 64)) + self.assertEqual(im.size, (64, 64)) + + # Test thumbnail(), where only draft() is necessary to resize the image + im = Image.open("Tests/images/hopper.jpg") + im.thumbnail((64, 64)) + self.assert_image(im, im.mode, (64, 64)) diff --git a/Tests/test_image_transpose.py b/Tests/test_image_transpose.py index 8ffb9e9bf..ad8c77126 100644 --- a/Tests/test_image_transpose.py +++ b/Tests/test_image_transpose.py @@ -7,10 +7,9 @@ from PIL.Image import (FLIP_LEFT_RIGHT, FLIP_TOP_BOTTOM, ROTATE_90, ROTATE_180, class TestImageTranspose(PillowTestCase): - hopper = { - 'L': helper.hopper('L').crop((0, 0, 121, 127)).copy(), - 'RGB': helper.hopper('RGB').crop((0, 0, 121, 127)).copy(), - } + hopper = {mode: helper.hopper(mode).crop((0, 0, 121, 127)).copy() for mode in [ + 'L', 'RGB', 'I;16', 'I;16L', 'I;16B' + ]} def test_flip_left_right(self): def transpose(mode): @@ -25,7 +24,7 @@ class TestImageTranspose(PillowTestCase): self.assertEqual(im.getpixel((1, y-2)), out.getpixel((x-2, y-2))) self.assertEqual(im.getpixel((x-2, y-2)), out.getpixel((1, y-2))) - for mode in ("L", "RGB"): + for mode in ("L", "RGB", "I;16", "I;16L", "I;16B"): transpose(mode) def test_flip_top_bottom(self): @@ -41,7 +40,7 @@ class TestImageTranspose(PillowTestCase): self.assertEqual(im.getpixel((1, y-2)), out.getpixel((1, 1))) self.assertEqual(im.getpixel((x-2, y-2)), out.getpixel((x-2, 1))) - for mode in ("L", "RGB"): + for mode in ("L", "RGB", "I;16", "I;16L", "I;16B"): transpose(mode) def test_rotate_90(self): @@ -73,7 +72,7 @@ class TestImageTranspose(PillowTestCase): self.assertEqual(im.getpixel((1, y-2)), out.getpixel((x-2, 1))) self.assertEqual(im.getpixel((x-2, y-2)), out.getpixel((1, 1))) - for mode in ("L", "RGB"): + for mode in ("L", "RGB", "I;16", "I;16L", "I;16B"): transpose(mode) def test_rotate_270(self): diff --git a/Tests/test_imageqt.py b/Tests/test_imageqt.py index 2ded37c09..bd93828ef 100644 --- a/Tests/test_imageqt.py +++ b/Tests/test_imageqt.py @@ -1,7 +1,16 @@ from .helper import PillowTestCase, hopper -from PIL import ImageQt +import warnings +deprecated = False +with warnings.catch_warnings(): + warnings.filterwarnings("error", category=DeprecationWarning) + try: + from PIL import ImageQt + except DeprecationWarning: + deprecated = True + warnings.filterwarnings("ignore", category=DeprecationWarning) + from PIL import ImageQt if ImageQt.qt_is_installed: from PIL.ImageQt import qRgba @@ -78,3 +87,6 @@ class TestImageQt(PillowQtTestCase, PillowTestCase): def test_image(self): for mode in ('1', 'RGB', 'RGBA', 'L', 'P'): ImageQt.ImageQt(hopper(mode)) + + def test_deprecated(self): + self.assertEqual(ImageQt.qt_version in ["4", "side"], deprecated) diff --git a/Tests/test_pickle.py b/Tests/test_pickle.py index 46958c085..45ef0f1db 100644 --- a/Tests/test_pickle.py +++ b/Tests/test_pickle.py @@ -67,9 +67,13 @@ class TestPickle(PillowTestCase): "Tests/images/non_zero_bb.png", "Tests/images/non_zero_bb_scale2.png", "Tests/images/p_trns_single.png", - "Tests/images/pil123p.png" + "Tests/images/pil123p.png", + "Tests/images/itxt_chunks.png" ]: - self.helper_pickle_string(pickle, test_file=test_file) + for protocol in range(0, pickle.HIGHEST_PROTOCOL + 1): + self.helper_pickle_string(pickle, + protocol=protocol, + test_file=test_file) def test_pickle_l_mode(self): # Arrange diff --git a/Tests/test_util.py b/Tests/test_util.py index 4471b75bd..08e9c1665 100644 --- a/Tests/test_util.py +++ b/Tests/test_util.py @@ -1,4 +1,4 @@ -from .helper import PillowTestCase +from .helper import unittest, PillowTestCase from PIL import _util @@ -35,6 +35,18 @@ class TestUtil(PillowTestCase): # Assert self.assertTrue(it_is) + @unittest.skipIf(not _util.py36, 'os.path support for Paths added in 3.6') + def test_path_obj_is_path(self): + # Arrange + from pathlib import Path + test_path = Path('filename.ext') + + # Act + it_is = _util.isPath(test_path) + + # Assert + self.assertTrue(it_is) + def test_is_not_path(self): # Arrange filename = self.tempfile("temp.ext") diff --git a/azure-pipelines.yml b/azure-pipelines.yml new file mode 100644 index 000000000..bef5eeee6 --- /dev/null +++ b/azure-pipelines.yml @@ -0,0 +1,67 @@ +# Python package +# Create and test a Python package on multiple Python versions. +# Add steps that analyze code, save the dist with the build record, +# publish to a PyPI-compatible index, and more: +# https://docs.microsoft.com/azure/devops/pipelines/languages/python + +jobs: + +- template: .azure-pipelines/jobs/lint.yml + parameters: + name: Lint + vmImage: 'Ubuntu-16.04' + +- template: .azure-pipelines/jobs/test-docker.yml + parameters: + docker: 'alpine' + name: 'alpine' + +- template: .azure-pipelines/jobs/test-docker.yml + parameters: + docker: 'arch' + name: 'arch' + +- template: .azure-pipelines/jobs/test-docker.yml + parameters: + docker: 'ubuntu-trusty-x86' + name: 'ubuntu_trusty_x86' + +- template: .azure-pipelines/jobs/test-docker.yml + parameters: + docker: 'ubuntu-xenial-amd64' + name: 'ubuntu_xenial_amd64' + +- template: .azure-pipelines/jobs/test-docker.yml + parameters: + docker: 'debian-stretch-x86' + name: 'debian_stretch_x86' + +- template: .azure-pipelines/jobs/test-docker.yml + parameters: + docker: 'centos-6-amd64' + name: 'centos_6_amd64' + +- template: .azure-pipelines/jobs/test-docker.yml + parameters: + docker: 'centos-7-amd64' + name: 'centos_7_amd64' + +- template: .azure-pipelines/jobs/test-docker.yml + parameters: + docker: 'amazon-1-amd64' + name: 'amazon_1_amd64' + +- template: .azure-pipelines/jobs/test-docker.yml + parameters: + docker: 'amazon-2-amd64' + name: 'amazon_2_amd64' + +- template: .azure-pipelines/jobs/test-docker.yml + parameters: + docker: 'fedora-28-amd64' + name: 'fedora_28_amd64' + +- template: .azure-pipelines/jobs/test-docker.yml + parameters: + docker: 'fedora-29-amd64' + name: 'fedora_29_amd64' diff --git a/docs/deprecations.rst b/docs/deprecations.rst index b8131ac05..fb5acfa8e 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -12,6 +12,27 @@ Deprecated features Below are features which are considered deprecated. Where appropriate, a ``DeprecationWarning`` is issued. +Python 2.7 +~~~~~~~~~~ + +.. deprecated:: 6.0.0 + +Python 2.7 reaches end-of-life on 2020-01-01. + +Pillow 7.0.0 will be released on 2020-01-01 and will drop support for Python 2.7, making +Pillow 6.x the last series to support Python 2. + +PyQt4 and PySide +~~~~~~~~~~~~~~~~ + +.. deprecated:: 6.0.0 + +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 deprecated from ``ImageQt`` and will be removed in +a future version. Please upgrade to PyQt5 or PySide2. + PIL.*ImagePlugin.__version__ attributes ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -50,13 +71,12 @@ a ``DeprecationWarning``: Setting the size of a TIFF image directly is deprecated, and will be removed in a future version. Use the resize method instead. -PILLOW_VERSION and VERSION constants -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +PILLOW_VERSION constant +~~~~~~~~~~~~~~~~~~~~~~~ .. deprecated:: 5.2.0 -Two version constants – ``VERSION`` (the old PIL version, always 1.1.7) and -``PILLOW_VERSION`` – have been deprecated and will be removed in the next +``PILLOW_VERSION`` has been deprecated and will be removed in the next major release. Use ``__version__`` instead. Removed features @@ -65,6 +85,14 @@ Removed features Deprecated features are only removed in major releases after an appropriate period of deprecation has passed. +VERSION constant +~~~~~~~~~~~~~~~~ + +*Removed in version 6.0.0.* + +``VERSION`` (the old PIL version, always 1.1.7) has been removed. Use +``__version__`` instead. + Undocumented ImageOps functions ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/docs/installation.rst b/docs/installation.rst index abccfcf56..44cd9af6f 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -15,21 +15,23 @@ Notes .. note:: Pillow is supported on the following Python versions -+--------------------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+ -|**Python** |**2.4**|**2.5**|**2.6**|**2.7**|**3.2**|**3.3**|**3.4**|**3.5**|**3.6**|**3.7**| -+--------------------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+ -|Pillow < 2.0.0 | Yes | Yes | Yes | Yes | | | | | | | -+--------------------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+ -|Pillow 2.x - 3.x | | | Yes | Yes | Yes | Yes | Yes | Yes | | | -+--------------------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+ -|Pillow 4.x | | | | Yes | | Yes | Yes | Yes | Yes | | -+--------------------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+ -|Pillow 5.0.x - 5.1.x| | | | Yes | | | Yes | Yes | Yes | | -+--------------------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+ -|Pillow 5.2.x - 5.4.x| | | | Yes | | | Yes | Yes | Yes | Yes | -+--------------------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+ -|Pillow >= 6.0.0 | | | | Yes | | | | Yes | Yes | Yes | -+--------------------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+ ++---------------------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+ +|**Python** |**2.4**|**2.5**|**2.6**|**2.7**|**3.2**|**3.3**|**3.4**|**3.5**|**3.6**|**3.7**| ++---------------------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+ +|Pillow < 2.0.0 | Yes | Yes | Yes | Yes | | | | | | | ++---------------------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+ +|Pillow 2.x - 3.x | | | Yes | Yes | Yes | Yes | Yes | Yes | | | ++---------------------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+ +|Pillow 4.x | | | | Yes | | Yes | Yes | Yes | Yes | | ++---------------------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+ +|Pillow 5.0.x - 5.1.x | | | | Yes | | | Yes | Yes | Yes | | ++---------------------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+ +|Pillow 5.2.x - 5.4.x | | | | Yes | | | Yes | Yes | Yes | Yes | ++---------------------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+ +|Pillow 6.x | | | | Yes | | | | Yes | Yes | Yes | ++---------------------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+ +|Pillow >= 7.0.0 | | | | | | | | Yes | Yes | Yes | ++---------------------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+ Basic Installation ------------------ diff --git a/docs/reference/ImageQt.rst b/docs/reference/ImageQt.rst index 386401075..5128f28fb 100644 --- a/docs/reference/ImageQt.rst +++ b/docs/reference/ImageQt.rst @@ -7,6 +7,12 @@ The :py:mod:`ImageQt` module contains support for creating PyQt4, PyQt5, PySide or PySide2 QImage objects from PIL images. +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 is deprecated since Pillow 6.0.0 and will be removed in a +future version. Please upgrade to PyQt5 or PySide2. + .. versionadded:: 1.1.6 .. py:class:: ImageQt.ImageQt(image) diff --git a/docs/releasenotes/6.0.0.rst b/docs/releasenotes/6.0.0.rst index e1fa83cce..c9712ed8a 100644 --- a/docs/releasenotes/6.0.0.rst +++ b/docs/releasenotes/6.0.0.rst @@ -26,12 +26,38 @@ Several undocumented functions in ``ImageOps`` were deprecated in Pillow 4.3.0 ( and have now been removed: ``gaussian_blur``, ``gblur``, ``unsharp_mask``, ``usm`` and ``box_blur``. Use the equivalent operations in ``ImageFilter`` instead. +Removed deprecated VERSION +^^^^^^^^^^^^^^^^^^^^^^^^^^ + +``VERSION`` (the old PIL version, always 1.1.7) has been removed. Use ``__version__`` +instead. + API Changes =========== Deprecations ^^^^^^^^^^^^ +Python 2.7 +~~~~~~~~~~ + +Python 2.7 reaches end-of-life on 2020-01-01. + +Pillow 7.0.0 will be released on 2020-01-01 and will drop support for Python 2.7, making +Pillow 6.x the last series to support Python 2. + +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 deprecated from ``ImageQt`` and will be removed in +a future version. Please upgrade to PyQt5 or PySide2. + +PIL.*ImagePlugin.__version__ attributes +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + These version constants have been deprecated and will be removed in a future version. diff --git a/src/PIL/ContainerIO.py b/src/PIL/ContainerIO.py index 682ad9031..e6c288c83 100644 --- a/src/PIL/ContainerIO.py +++ b/src/PIL/ContainerIO.py @@ -18,6 +18,8 @@ # A file object that provides read access to a part of an existing # file (for example a TAR file). +import io + class ContainerIO(object): @@ -39,9 +41,9 @@ class ContainerIO(object): # Always false. def isatty(self): - return 0 + return False - def seek(self, offset, mode=0): + def seek(self, offset, mode=io.SEEK_SET): """ Move file pointer. diff --git a/src/PIL/EpsImagePlugin.py b/src/PIL/EpsImagePlugin.py index 43dcdfb67..7421189f8 100644 --- a/src/PIL/EpsImagePlugin.py +++ b/src/PIL/EpsImagePlugin.py @@ -104,7 +104,7 @@ def Ghostscript(tile, size, fp, scale=1): # Copy whole file to read in Ghostscript with open(infile_temp, 'wb') as f: # fetch length of fp - fp.seek(0, 2) + fp.seek(0, io.SEEK_END) fsize = fp.tell() # ensure start position # go back @@ -169,7 +169,7 @@ class PSFile(object): self.fp = fp self.char = None - def seek(self, offset, whence=0): + def seek(self, offset, whence=io.SEEK_SET): self.char = None self.fp.seek(offset, whence) @@ -312,7 +312,7 @@ class EpsImageFile(ImageFile.ImageFile): if s[:4] == b"%!PS": # for HEAD without binary preview - fp.seek(0, 2) + fp.seek(0, io.SEEK_END) length = fp.tell() offset = 0 elif i32(s[0:4]) == 0xC6D3D0C5: diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py index 2d13de022..2ebd8b248 100644 --- a/src/PIL/GifImagePlugin.py +++ b/src/PIL/GifImagePlugin.py @@ -604,16 +604,16 @@ def _save_netpbm(im, fp, filename): import os from subprocess import Popen, check_call, PIPE, CalledProcessError - file = im._dump() + tempfile = im._dump() with open(filename, 'wb') as f: if im.mode != "RGB": with open(os.devnull, 'wb') as devnull: - check_call(["ppmtogif", file], stdout=f, stderr=devnull) + check_call(["ppmtogif", tempfile], stdout=f, stderr=devnull) else: # Pipe ppmquant output into ppmtogif - # "ppmquant 256 %s | ppmtogif > %s" % (file, filename) - quant_cmd = ["ppmquant", "256", file] + # "ppmquant 256 %s | ppmtogif > %s" % (tempfile, filename) + quant_cmd = ["ppmquant", "256", tempfile] togif_cmd = ["ppmtogif"] with open(os.devnull, 'wb') as devnull: quant_proc = Popen(quant_cmd, stdout=PIPE, stderr=devnull) @@ -632,7 +632,7 @@ def _save_netpbm(im, fp, filename): raise CalledProcessError(retcode, togif_cmd) try: - os.unlink(file) + os.unlink(tempfile) except OSError: pass diff --git a/src/PIL/IcnsImagePlugin.py b/src/PIL/IcnsImagePlugin.py index 2ea66675f..4a10b24b8 100644 --- a/src/PIL/IcnsImagePlugin.py +++ b/src/PIL/IcnsImagePlugin.py @@ -195,7 +195,7 @@ class IcnsFile(object): i += HEADERSIZE blocksize -= HEADERSIZE dct[sig] = (i, blocksize) - fobj.seek(blocksize, 1) + fobj.seek(blocksize, io.SEEK_CUR) i += blocksize def itersizes(self): diff --git a/src/PIL/Image.py b/src/PIL/Image.py index bdbb7f88d..9cfb2160a 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -40,10 +40,10 @@ if False: from PyQt5.QtGui import QImage, QPixmap -# VERSION is deprecated and will be removed in Pillow 6.0.0. -# PILLOW_VERSION is deprecated and will be removed after that. +# VERSION was removed in Pillow 6.0.0. +# PILLOW_VERSION is deprecated and will be removed in Pillow 7.0.0. # Use __version__ instead. -from . import VERSION, PILLOW_VERSION, __version__, _plugins +from . import PILLOW_VERSION, __version__, _plugins from ._util import py3 import logging @@ -76,8 +76,7 @@ except ImportError: from collections import Callable -# Silence warnings -assert VERSION +# Silence warning assert PILLOW_VERSION logger = logging.getLogger(__name__) @@ -680,8 +679,7 @@ class Image(object): def __eq__(self, other): # type: (object) -> bool - return (isinstance(other, Image) and - self.__class__.__name__ == other.__class__.__name__ and + return (self.__class__ is other.__class__ and self.mode == other.mode and self.size == other.size and self.info == other.info and @@ -709,8 +707,7 @@ class Image(object): :returns: png version of the image as bytes """ - from io import BytesIO - b = BytesIO() + b = io.BytesIO() self.save(b, 'PNG') return b.getvalue() @@ -2138,10 +2135,10 @@ class Image(object): debugging purposes. On Unix platforms, this method saves the image to a temporary - PPM file, and calls either the **xv** utility or the **display** + PPM file, and calls the **display**, **eog** or **xv** utility, depending on which one can be found. - On macOS, this method saves the image to a temporary BMP file, and + On macOS, this method saves the image to a temporary PNG file, and opens it with the native Preview application. On Windows, it saves the image to a temporary BMP file, and uses @@ -2251,11 +2248,12 @@ class Image(object): self.draft(None, size) - im = self.resize(size, resample) + if self.size != size: + im = self.resize(size, resample) - self.im = im.im - self.mode = im.mode - self._size = size + self.im = im.im + self._size = size + self.mode = self.im.mode self.readonly = 0 self.pyaccess = None diff --git a/src/PIL/ImageCms.py b/src/PIL/ImageCms.py index 29959a83a..2cd6f33b5 100644 --- a/src/PIL/ImageCms.py +++ b/src/PIL/ImageCms.py @@ -950,5 +950,5 @@ def versions(): return ( VERSION, core.littlecms_version, - sys.version.split()[0], Image.VERSION + sys.version.split()[0], Image.__version__ ) diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py index 32c5af6e8..a282280dc 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -525,7 +525,7 @@ def _save(im, fp, tile, bufsize=0): for e, b, o, a in tile: e = Image._getencoder(im.mode, e, a, im.encoderconfig) if o > 0: - fp.seek(o, 0) + fp.seek(o) e.setimage(im.im, b) if e.pushes_fd: e.setfd(fp) @@ -544,7 +544,7 @@ def _save(im, fp, tile, bufsize=0): for e, b, o, a in tile: e = Image._getencoder(im.mode, e, a, im.encoderconfig) if o > 0: - fp.seek(o, 0) + fp.seek(o) e.setimage(im.im, b) if e.pushes_fd: e.setfd(fp) diff --git a/src/PIL/ImageMath.py b/src/PIL/ImageMath.py index d985877a6..68247c290 100644 --- a/src/PIL/ImageMath.py +++ b/src/PIL/ImageMath.py @@ -28,7 +28,7 @@ VERBOSE = 0 def _isconstant(v): - return isinstance(v, int) or isinstance(v, float) + return isinstance(v, (int, float)) class _Operand(object): diff --git a/src/PIL/ImageQt.py b/src/PIL/ImageQt.py index b747781c5..02ce6354e 100644 --- a/src/PIL/ImageQt.py +++ b/src/PIL/ImageQt.py @@ -20,6 +20,7 @@ from . import Image from ._util import isPath, py3 from io import BytesIO import sys +import warnings qt_versions = [ ['5', 'PyQt5'], @@ -27,6 +28,12 @@ qt_versions = [ ['4', 'PyQt4'], ['side', 'PySide'] ] + +WARNING_TEXT = ( + "Support for EOL {} is deprecated and will be removed in a future version. " + "Please upgrade to PyQt5 or PySide2." +) + # If a version has already been imported, attempt it first qt_versions.sort(key=lambda qt_version: qt_version[1] in sys.modules, reverse=True) @@ -41,9 +48,13 @@ for qt_version, qt_module in qt_versions: elif qt_module == 'PyQt4': from PyQt4.QtGui import QImage, qRgba, QPixmap from PyQt4.QtCore import QBuffer, QIODevice + + warnings.warn(WARNING_TEXT.format(qt_module), DeprecationWarning) elif qt_module == 'PySide': from PySide.QtGui import QImage, qRgba, QPixmap from PySide.QtCore import QBuffer, QIODevice + + warnings.warn(WARNING_TEXT.format(qt_module), DeprecationWarning) except (ImportError, RuntimeError): continue qt_is_installed = True @@ -67,7 +78,7 @@ def fromqimage(im): """ buffer = QBuffer() buffer.open(QIODevice.ReadWrite) - # preserve alha channel with png + # preserve alpha channel with png # otherwise ppm is more friendly with Image.open if im.hasAlphaChannel(): im.save(buffer, 'png') diff --git a/src/PIL/Jpeg2KImagePlugin.py b/src/PIL/Jpeg2KImagePlugin.py index ab01845c7..9645f8ef0 100644 --- a/src/PIL/Jpeg2KImagePlugin.py +++ b/src/PIL/Jpeg2KImagePlugin.py @@ -193,9 +193,9 @@ class Jpeg2KImageFile(ImageFile.ImageFile): fd = -1 try: pos = self.fp.tell() - self.fp.seek(0, 2) + self.fp.seek(0, io.SEEK_END) length = self.fp.tell() - self.fp.seek(pos, 0) + self.fp.seek(pos) except Exception: length = -1 diff --git a/src/PIL/PcfFontFile.py b/src/PIL/PcfFontFile.py index 471d6647a..b50fe72da 100644 --- a/src/PIL/PcfFontFile.py +++ b/src/PIL/PcfFontFile.py @@ -16,6 +16,7 @@ # See the README file for information on usage and redistribution. # +import io from . import Image, FontFile from ._binary import i8, i16le as l16, i32le as l32, i16be as b16, i32be as b32 @@ -117,7 +118,7 @@ class PcfFontFile(FontFile.FontFile): for i in range(nprops): p.append((i32(fp.read(4)), i8(fp.read(1)), i32(fp.read(4)))) if nprops & 3: - fp.seek(4 - (nprops & 3), 1) # pad + fp.seek(4 - (nprops & 3), io.SEEK_CUR) # pad data = fp.read(i32(fp.read(4))) diff --git a/src/PIL/PcxImagePlugin.py b/src/PIL/PcxImagePlugin.py index 57d8189dd..8094a2866 100644 --- a/src/PIL/PcxImagePlugin.py +++ b/src/PIL/PcxImagePlugin.py @@ -25,6 +25,7 @@ # See the README file for information on usage and redistribution. # +import io import logging from . import Image, ImageFile, ImagePalette from ._binary import i8, i16le as i16, o8, o16le as o16 @@ -82,7 +83,7 @@ class PcxImageFile(ImageFile.ImageFile): elif version == 5 and bits == 8 and planes == 1: mode = rawmode = "L" # FIXME: hey, this doesn't work with the incremental loader !!! - self.fp.seek(-769, 2) + self.fp.seek(-769, io.SEEK_END) s = self.fp.read(769) if len(s) == 769 and i8(s[0]) == 12: # check if the palette is linear greyscale diff --git a/src/PIL/PdfParser.py b/src/PIL/PdfParser.py index a7773ef1e..8f90b668d 100644 --- a/src/PIL/PdfParser.py +++ b/src/PIL/PdfParser.py @@ -269,18 +269,13 @@ class PdfDict(UserDict): else: self.__dict__[key] = value else: - if isinstance(key, str): - key = key.encode("us-ascii") - self[key] = value + self[key.encode("us-ascii")] = value def __getattr__(self, key): try: - value = self[key] + value = self[key.encode("us-ascii")] except KeyError: - try: - value = self[key.encode("us-ascii")] - except KeyError: - raise AttributeError(key) + raise AttributeError(key) if isinstance(value, bytes): value = decode_text(value) if key.endswith("Date"): @@ -361,8 +356,7 @@ def pdf_repr(x): return b"false" elif x is None: return b"null" - elif (isinstance(x, PdfName) or isinstance(x, PdfDict) or - isinstance(x, PdfArray) or isinstance(x, PdfBinary)): + elif isinstance(x, (PdfName, PdfDict, PdfArray, PdfBinary)): return bytes(x) elif isinstance(x, int): return str(x).encode("us-ascii") diff --git a/src/PIL/PngImagePlugin.py b/src/PIL/PngImagePlugin.py index ee080fae9..b383e17f0 100644 --- a/src/PIL/PngImagePlugin.py +++ b/src/PIL/PngImagePlugin.py @@ -193,7 +193,7 @@ class iTXt(str): """ @staticmethod - def __new__(cls, text, lang, tkey): + def __new__(cls, text, lang=None, tkey=None): """ :param cls: the class to use when creating the instance :param text: value for this key diff --git a/src/PIL/PpmImagePlugin.py b/src/PIL/PpmImagePlugin.py index 750454dc5..e3e411cad 100644 --- a/src/PIL/PpmImagePlugin.py +++ b/src/PIL/PpmImagePlugin.py @@ -70,7 +70,14 @@ class PpmImageFile(ImageFile.ImageFile): s = self.fp.read(1) if s != b"P": raise SyntaxError("not a PPM file") - mode = MODES[self._token(s)] + magic_number = self._token(s) + mode = MODES[magic_number] + + self.custom_mimetype = { + b"P4": "image/x-portable-bitmap", + b"P5": "image/x-portable-graymap", + b"P6": "image/x-portable-pixmap", + }.get(magic_number) if mode == "1": self.mode = "1" @@ -158,3 +165,5 @@ Image.register_open(PpmImageFile.format, PpmImageFile, _accept) Image.register_save(PpmImageFile.format, _save) Image.register_extensions(PpmImageFile.format, [".pbm", ".pgm", ".ppm"]) + +Image.register_mime(PpmImageFile.format, "image/x-portable-anymap") diff --git a/src/PIL/PsdImagePlugin.py b/src/PIL/PsdImagePlugin.py index c631f41cb..6623f8f87 100644 --- a/src/PIL/PsdImagePlugin.py +++ b/src/PIL/PsdImagePlugin.py @@ -20,6 +20,7 @@ # PIL.__version__ instead. __version__ = "0.4" +import io from . import Image, ImageFile, ImagePalette from ._binary import i8, i16be as i16, i32be as i32 @@ -216,12 +217,12 @@ def _layerinfo(file): if size: length = i32(read(4)) if length: - file.seek(length - 16, 1) + file.seek(length - 16, io.SEEK_CUR) combined += length + 4 length = i32(read(4)) if length: - file.seek(length, 1) + file.seek(length, io.SEEK_CUR) combined += length + 4 length = i8(read(1)) @@ -231,7 +232,7 @@ def _layerinfo(file): name = read(length).decode('latin-1', 'replace') combined += length + 1 - file.seek(size - combined, 1) + file.seek(size - combined, io.SEEK_CUR) layers.append((name, mode, (x0, y0, x1, y1))) # get tiles diff --git a/src/PIL/TarIO.py b/src/PIL/TarIO.py index 89957fba4..a421b12a5 100644 --- a/src/PIL/TarIO.py +++ b/src/PIL/TarIO.py @@ -14,6 +14,7 @@ # See the README file for information on usage and redistribution. # +import io import sys from . import ContainerIO @@ -51,7 +52,7 @@ class TarIO(ContainerIO.ContainerIO): if file == name: break - self.fh.seek((size + 511) & (~511), 1) + self.fh.seek((size + 511) & (~511), io.SEEK_CUR) # Open region ContainerIO.ContainerIO.__init__(self, self.fh, self.fh.tell(), size) diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index 195c87f91..cea1b0ba4 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -135,6 +135,9 @@ COMPRESSION_INFO = { 32946: "tiff_deflate", 34676: "tiff_sgilog", 34677: "tiff_sgilog24", + 34925: "lzma", + 50000: "zstd", + 50001: "webp", } COMPRESSION_INFO_REV = {v: k for k, v in COMPRESSION_INFO.items()} @@ -423,7 +426,7 @@ class ImageFileDirectory_v2(MutableMapping): ifd = ImageFileDirectory_v2() ifd[key] = 'Some Data' - ifd.tagtype[key] = 2 + ifd.tagtype[key] = TiffTags.ASCII print(ifd[key]) 'Some Data' @@ -557,7 +560,7 @@ class ImageFileDirectory_v2(MutableMapping): if info.type: self.tagtype[tag] = info.type else: - self.tagtype[tag] = 7 + self.tagtype[tag] = TiffTags.UNDEFINED if all(isinstance(v, IFDRational) for v in values): self.tagtype[tag] = TiffTags.RATIONAL elif all(isinstance(v, int) for v in values): @@ -872,7 +875,7 @@ class ImageFileDirectory_v1(ImageFileDirectory_v2): ifd = ImageFileDirectory_v1() ifd[key] = 'Some Data' - ifd.tagtype[key] = 2 + ifd.tagtype[key] = TiffTags.ASCII print(ifd[key]) ('Some Data',) @@ -1436,7 +1439,7 @@ def _save(im, fp, filename): try: ifd.tagtype[key] = info.tagtype[key] except Exception: - pass # might not be an IFD, Might not have populated type + pass # might not be an IFD. Might not have populated type # additions written by Greg Couch, gregc@cgl.ucsf.edu # inspired by image-sig posting from Kevin Cazabon, kcazabon@home.com @@ -1680,7 +1683,7 @@ class AppendingTiffWriter: def tell(self): return self.f.tell() - self.offsetOfNewPage - def seek(self, offset, whence): + def seek(self, offset, whence=io.SEEK_SET): if whence == os.SEEK_SET: offset += self.offsetOfNewPage diff --git a/src/PIL/__init__.py b/src/PIL/__init__.py index bc8cfed8c..ec0611b68 100644 --- a/src/PIL/__init__.py +++ b/src/PIL/__init__.py @@ -16,10 +16,9 @@ PIL.VERSION is the old PIL version and will be removed in the future. from . import _version -# VERSION is deprecated and will be removed in Pillow 6.0.0. -# PILLOW_VERSION is deprecated and will be removed after that. +# VERSION was removed in Pillow 6.0.0. +# PILLOW_VERSION is deprecated and will be removed in Pillow 7.0.0. # Use __version__ instead. -VERSION = '1.1.7' # PIL Version PILLOW_VERSION = __version__ = _version.__version__ del _version diff --git a/src/PIL/_util.py b/src/PIL/_util.py index 5828c2c24..cb307050c 100644 --- a/src/PIL/_util.py +++ b/src/PIL/_util.py @@ -2,13 +2,20 @@ import os import sys py3 = sys.version_info.major >= 3 +py36 = sys.version_info[0:2] >= (3, 6) if py3: def isStringType(t): return isinstance(t, str) - def isPath(f): - return isinstance(f, (bytes, str)) + if py36: + from pathlib import Path + + def isPath(f): + return isinstance(f, (bytes, str, Path)) + else: + def isPath(f): + return isinstance(f, (bytes, str)) else: def isStringType(t): return isinstance(t, basestring) # noqa: F821 diff --git a/src/libImaging/Geometry.c b/src/libImaging/Geometry.c index 1d08728da..5358f6eeb 100644 --- a/src/libImaging/Geometry.c +++ b/src/libImaging/Geometry.c @@ -39,7 +39,11 @@ ImagingFlipLeftRight(Imaging imOut, Imaging imIn) ImagingSectionEnter(&cookie); if (imIn->image8) { - FLIP_LEFT_RIGHT(UINT8, image8) + if (strncmp(imIn->mode, "I;16", 4) == 0) { + FLIP_LEFT_RIGHT(UINT16, image8) + } else { + FLIP_LEFT_RIGHT(UINT8, image8) + } } else { FLIP_LEFT_RIGHT(INT32, image32) } @@ -253,7 +257,11 @@ ImagingRotate180(Imaging imOut, Imaging imIn) yr = imIn->ysize-1; if (imIn->image8) { - ROTATE_180(UINT8, image8) + if (strncmp(imIn->mode, "I;16", 4) == 0) { + ROTATE_180(UINT16, image8) + } else { + ROTATE_180(UINT8, image8) + } } else { ROTATE_180(INT32, image32) }