diff --git a/.appveyor.yml b/.appveyor.yml index 2a4e67dc9..f299794b6 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -30,6 +30,10 @@ environment: PIP_DIR: bin TEST_OPTIONS: --processes=0 DEPLOY: NO + - PYTHON: C:/vp/pypy3 + EXECUTABLE: bin/pypy.exe + PIP_DIR: bin + VENV: YES install: @@ -43,7 +47,12 @@ install: - ps: | if ($env:PYTHON -eq "c:/vp/pypy2") { - c:\pillow\winbuild\appveyor_install_pypy.cmd + c:\pillow\winbuild\appveyor_install_pypy2.cmd + } +- ps: | + if ($env:PYTHON -eq "c:/vp/pypy3") + { + c:\pillow\winbuild\appveyor_install_pypy3.cmd } - ps: | if ($env:PYTHON -eq "c:/msys64/mingw32") diff --git a/.travis.yml b/.travis.yml index 545cb0b50..286f828f2 100644 --- a/.travis.yml +++ b/.travis.yml @@ -22,19 +22,20 @@ matrix: name: "PyPy3 Xenial" - python: '3.7' name: "3.7 Xenial" + services: xvfb - python: '2.7' name: "2.7 Xenial" - - python: "2.7_with_system_site_packages" # For PyQt4 - name: "2.7_with_system_site_packages Xenial" - services: xvfb - python: '3.6' name: "3.6 Xenial PYTHONOPTIMIZE=1" env: PYTHONOPTIMIZE=1 + services: xvfb - python: '3.5' name: "3.5 Xenial PYTHONOPTIMIZE=2" env: PYTHONOPTIMIZE=2 + services: xvfb - python: "3.8-dev" name: "3.8-dev Xenial" + services: xvfb - env: DOCKER="alpine" DOCKER_TAG="master" - env: DOCKER="arch" DOCKER_TAG="master" # contains PyQt5 - env: DOCKER="ubuntu-16.04-xenial-amd64" DOCKER_TAG="master" diff --git a/.travis/install.sh b/.travis/install.sh index 725880934..6ad89402e 100755 --- a/.travis/install.sh +++ b/.travis/install.sh @@ -3,7 +3,7 @@ set -e sudo apt-get update -sudo apt-get -qq install libfreetype6-dev liblcms2-dev python-tk python-qt4\ +sudo apt-get -qq install libfreetype6-dev liblcms2-dev python-tk\ ghostscript libffi-dev libjpeg-turbo-progs libopenjp2-7-dev\ cmake imagemagick libharfbuzz-dev libfribidi-dev @@ -15,6 +15,10 @@ pip install -U pytest-cov pip install pyroma pip install test-image-results pip install numpy +if [[ $TRAVIS_PYTHON_VERSION == 3.* ]]; then + sudo apt-get -qq install pyqt5-dev-tools + pip install pyqt5 +fi # docs only on Python 2.7 if [ "$TRAVIS_PYTHON_VERSION" == "2.7" ]; then pip install -r requirements.txt ; fi diff --git a/CHANGES.rst b/CHANGES.rst index 2419837bf..3d532f5e8 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -2,11 +2,50 @@ Changelog (Pillow) ================== -6.2.0 (unreleased) +7.0.0 (unreleased) +------------------ + +- Drop support for EOL PyQt4 and PySide #4108 + [hugovk, radarhere] + +- Removed deprecated setting of TIFF image sizes #4114 + [radarhere] + +- Removed deprecated PILLOW_VERSION #4107 + [hugovk] + +- Changed default frombuffer raw decoder args #1730 + [radarhere] + +6.2.0 (2019-10-01) ------------------ - This is the last Pillow release to support Python 2.7 #3642 +- Catch buffer overruns #4104 + [radarhere] + +- Initialize rows_per_strip when RowsPerStrip tag is missing #4034 + [cgohlke, radarhere] + +- Raise error if TIFF dimension is a string #4103 + [radarhere] + +- Added decompression bomb checks #4102 + [radarhere] + +- Fix ImageGrab.grab DPI scaling on Windows 10 version 1607+ #4000 + [nulano, radarhere] + +- Corrected negative seeks #4101 + [radarhere] + +- Added argument to capture all screens on Windows #3950 + [nulano, radarhere] + +- Updated warning to specify when Image.frombuffer defaults will change #4086 + [radarhere] + - Changed WindowsViewer format to PNG #4080 [radarhere] diff --git a/RELEASING.md b/RELEASING.md index e1f57883c..feeb7c0d7 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -92,7 +92,7 @@ Released as needed privately to individual vendors for critical security-related ``` * [ ] Download distributions from the [Pillow Wheel Builder container](http://a365fff413fe338398b6-1c8a9b3114517dc5fe17b7c3f8c63a43.r19.cf2.rackcdn.com/). ```bash - wget -m -A 'Pillow-*' \ + wget -m -A 'Pillow--*' \ http://a365fff413fe338398b6-1c8a9b3114517dc5fe17b7c3f8c63a43.r19.cf2.rackcdn.com ``` diff --git a/Tests/check_icns_dos.py b/Tests/check_icns_dos.py index 03eda2e3f..3f4fb6518 100644 --- a/Tests/check_icns_dos.py +++ b/Tests/check_icns_dos.py @@ -4,9 +4,5 @@ from io import BytesIO from PIL import Image -from PIL._util import py3 -if py3: - Image.open(BytesIO(bytes("icns\x00\x00\x00\x10hang\x00\x00\x00\x00", "latin-1"))) -else: - Image.open(BytesIO(bytes("icns\x00\x00\x00\x10hang\x00\x00\x00\x00"))) +Image.open(BytesIO(b"icns\x00\x00\x00\x10hang\x00\x00\x00\x00")) diff --git a/Tests/check_j2k_dos.py b/Tests/check_j2k_dos.py index 7d0e95a60..273c18585 100644 --- a/Tests/check_j2k_dos.py +++ b/Tests/check_j2k_dos.py @@ -4,19 +4,5 @@ from io import BytesIO from PIL import Image -from PIL._util import py3 -if py3: - Image.open( - BytesIO( - bytes( - "\x00\x00\x00\x0cjP\x20\x20\x0d\x0a\x87\x0a\x00\x00\x00\x00hang", - "latin-1", - ) - ) - ) - -else: - Image.open( - BytesIO(bytes("\x00\x00\x00\x0cjP\x20\x20\x0d\x0a\x87\x0a\x00\x00\x00\x00hang")) - ) +Image.open(BytesIO(b"\x00\x00\x00\x0cjP\x20\x20\x0d\x0a\x87\x0a\x00\x00\x00\x00hang")) diff --git a/Tests/helper.py b/Tests/helper.py index 312c6aa4e..466d04bda 100644 --- a/Tests/helper.py +++ b/Tests/helper.py @@ -5,9 +5,11 @@ from __future__ import print_function import logging import os +import subprocess import sys import tempfile import unittest +from io import BytesIO from PIL import Image, ImageMath from PIL._util import py3 @@ -284,14 +286,10 @@ if not py3: def fromstring(data): - from io import BytesIO - return Image.open(BytesIO(data)) def tostring(im, string_format, **options): - from io import BytesIO - out = BytesIO() im.save(out, string_format, **options) return out.getvalue() @@ -323,8 +321,6 @@ def command_succeeds(cmd): Runs the command, which must be a list of strings. Returns True if the command succeeds, or False if an OSError was raised by subprocess.Popen. """ - import subprocess - with open(os.devnull, "wb") as f: try: subprocess.call(cmd, stdout=f, stderr=subprocess.STDOUT) diff --git a/Tests/images/combined_larger_than_size.psd b/Tests/images/combined_larger_than_size.psd new file mode 100644 index 000000000..2e6caef39 Binary files /dev/null and b/Tests/images/combined_larger_than_size.psd differ diff --git a/Tests/images/decompression_bomb.gif b/Tests/images/decompression_bomb.gif new file mode 100644 index 000000000..3ca21b60a Binary files /dev/null and b/Tests/images/decompression_bomb.gif differ diff --git a/Tests/images/decompression_bomb.ico b/Tests/images/decompression_bomb.ico new file mode 100644 index 000000000..0efc9eaf7 Binary files /dev/null and b/Tests/images/decompression_bomb.ico differ diff --git a/Tests/images/fli_overrun.bin b/Tests/images/fli_overrun.bin new file mode 100644 index 000000000..e1e8c5901 Binary files /dev/null and b/Tests/images/fli_overrun.bin differ diff --git a/Tests/images/no_rows_per_strip.tif b/Tests/images/no_rows_per_strip.tif new file mode 100644 index 000000000..67942aec4 Binary files /dev/null and b/Tests/images/no_rows_per_strip.tif differ diff --git a/Tests/images/pcx_overrun.bin b/Tests/images/pcx_overrun.bin new file mode 100644 index 000000000..ea46d2c11 Binary files /dev/null and b/Tests/images/pcx_overrun.bin differ diff --git a/Tests/images/raw_negative_stride.bin b/Tests/images/raw_negative_stride.bin new file mode 100644 index 000000000..312e82a5f Binary files /dev/null and b/Tests/images/raw_negative_stride.bin differ diff --git a/Tests/images/sgi_overrun.bin b/Tests/images/sgi_overrun.bin new file mode 100644 index 000000000..9a45d065a Binary files /dev/null and b/Tests/images/sgi_overrun.bin differ diff --git a/Tests/images/string_dimension.tiff b/Tests/images/string_dimension.tiff new file mode 100644 index 000000000..d0b558301 Binary files /dev/null and b/Tests/images/string_dimension.tiff differ diff --git a/Tests/test_decompression_bomb.py b/Tests/test_decompression_bomb.py index b3a36fe78..3afad674f 100644 --- a/Tests/test_decompression_bomb.py +++ b/Tests/test_decompression_bomb.py @@ -41,6 +41,14 @@ class TestDecompressionBomb(PillowTestCase): self.assertRaises(Image.DecompressionBombError, lambda: Image.open(TEST_FILE)) + def test_exception_ico(self): + with self.assertRaises(Image.DecompressionBombError): + Image.open("Tests/images/decompression_bomb.ico") + + def test_exception_gif(self): + with self.assertRaises(Image.DecompressionBombError): + Image.open("Tests/images/decompression_bomb.gif") + class TestDecompressionCrop(PillowTestCase): def setUp(self): diff --git a/Tests/test_file_libtiff.py b/Tests/test_file_libtiff.py index 41d5dae7e..3372a68f0 100644 --- a/Tests/test_file_libtiff.py +++ b/Tests/test_file_libtiff.py @@ -1,5 +1,6 @@ from __future__ import print_function +import base64 import distutils.version import io import itertools @@ -832,6 +833,13 @@ class TestFileLibTiff(LibTiffTestCase): im, "Tests/images/old-style-jpeg-compression.png" ) + def test_no_rows_per_strip(self): + # This image does not have a RowsPerStrip TIFF tag + infile = "Tests/images/no_rows_per_strip.tif" + im = Image.open(infile) + im.load() + self.assertEqual(im.size, (950, 975)) + def test_orientation(self): base_im = Image.open("Tests/images/g4_orientation_1.tif") @@ -840,3 +848,24 @@ class TestFileLibTiff(LibTiffTestCase): im.load() self.assert_image_similar(base_im, im, 0.7) + + def test_sampleformat_not_corrupted(self): + # Assert that a TIFF image with SampleFormat=UINT tag is not corrupted + # when saving to a new file. + # Pillow 6.0 fails with "OSError: cannot identify image file". + tiff = io.BytesIO( + base64.b64decode( + b"SUkqAAgAAAAPAP4ABAABAAAAAAAAAAABBAABAAAAAQAAAAEBBAABAAAAAQAA" + b"AAIBAwADAAAAwgAAAAMBAwABAAAACAAAAAYBAwABAAAAAgAAABEBBAABAAAA" + b"4AAAABUBAwABAAAAAwAAABYBBAABAAAAAQAAABcBBAABAAAACwAAABoBBQAB" + b"AAAAyAAAABsBBQABAAAA0AAAABwBAwABAAAAAQAAACgBAwABAAAAAQAAAFMB" + b"AwADAAAA2AAAAAAAAAAIAAgACAABAAAAAQAAAAEAAAABAAAAAQABAAEAAAB4" + b"nGNgYAAAAAMAAQ==" + ) + ) + out = io.BytesIO() + with Image.open(tiff) as im: + im.save(out, format="tiff") + out.seek(0) + with Image.open(out) as im: + im.load() diff --git a/Tests/test_file_libtiff_small.py b/Tests/test_file_libtiff_small.py index 0db37c7ea..2eabc60fd 100644 --- a/Tests/test_file_libtiff_small.py +++ b/Tests/test_file_libtiff_small.py @@ -1,3 +1,5 @@ +from io import BytesIO + from PIL import Image from .test_file_libtiff import LibTiffTestCase @@ -25,8 +27,6 @@ class TestFileLibTiffSmall(LibTiffTestCase): def test_g4_hopper_bytesio(self): """Testing the bytesio loading code path""" - from io import BytesIO - test_file = "Tests/images/hopper_g4.tif" s = BytesIO() with open(test_file, "rb") as f: diff --git a/Tests/test_file_psd.py b/Tests/test_file_psd.py index f55ee1dcb..8381ceaef 100644 --- a/Tests/test_file_psd.py +++ b/Tests/test_file_psd.py @@ -87,3 +87,12 @@ class TestImagePsd(PillowTestCase): im = Image.open("Tests/images/hopper_merged.psd") self.assertNotIn("icc_profile", im.info) + + def test_combined_larger_than_size(self): + # The 'combined' sizes of the individual parts is larger than the + # declared 'size' of the extra data field, resulting in a backwards seek. + + # If we instead take the 'size' of the extra data field as the source of truth, + # then the seek can't be negative + with self.assertRaises(IOError): + Image.open("Tests/images/combined_larger_than_size.psd") diff --git a/Tests/test_file_spider.py b/Tests/test_file_spider.py index 8a1eb6637..340208486 100644 --- a/Tests/test_file_spider.py +++ b/Tests/test_file_spider.py @@ -1,4 +1,5 @@ import tempfile +from io import BytesIO from PIL import Image, ImageSequence, SpiderImagePlugin @@ -117,3 +118,14 @@ class TestImageSpider(PillowTestCase): for i, frame in enumerate(ImageSequence.Iterator(im)): if i > 1: self.fail("Non-stack DOS file test failed") + + # for issue #4093 + def test_odd_size(self): + data = BytesIO() + width = 100 + im = Image.new("F", (width, 64)) + im.save(data, format="SPIDER") + + data.seek(0) + im2 = Image.open(data) + self.assert_image_equal(im, im2) diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py index c8a4c2df3..a335e5e7a 100644 --- a/Tests/test_file_tiff.py +++ b/Tests/test_file_tiff.py @@ -1,7 +1,8 @@ import logging +import os from io import BytesIO -from PIL import Image, TiffImagePlugin, features +from PIL import Image, TiffImagePlugin from PIL._util import py3 from PIL.TiffImagePlugin import RESOLUTION_UNIT, X_RESOLUTION, Y_RESOLUTION @@ -72,15 +73,6 @@ class TestFileTiff(PillowTestCase): ifd.legacy_api = None self.assertEqual(str(e.exception), "Not allowing setting of legacy api") - def test_size(self): - filename = "Tests/images/pil168.tif" - im = Image.open(filename) - - def set_size(): - im.size = (256, 256) - - self.assert_warning(DeprecationWarning, set_size) - def test_xyres_tiff(self): filename = "Tests/images/pil168.tif" im = Image.open(filename) @@ -512,10 +504,7 @@ class TestFileTiff(PillowTestCase): self.assert_image_equal(im.convert("RGB"), reloaded.convert("RGB")) def test_tiff_save_all(self): - import io - import os - - mp = io.BytesIO() + mp = BytesIO() with Image.open("Tests/images/multipage.tiff") as im: im.save(mp, format="tiff", save_all=True) @@ -524,7 +513,7 @@ class TestFileTiff(PillowTestCase): self.assertEqual(im.n_frames, 3) # Test appending images - mp = io.BytesIO() + mp = BytesIO() im = Image.new("RGB", (100, 100), "#f00") ims = [Image.new("RGB", (100, 100), color) for color in ["#0f0", "#00f"]] im.copy().save(mp, format="TIFF", save_all=True, append_images=ims) @@ -538,7 +527,7 @@ class TestFileTiff(PillowTestCase): for im in ims: yield im - mp = io.BytesIO() + mp = BytesIO() im.save(mp, format="TIFF", save_all=True, append_images=imGenerator(ims)) mp.seek(0, os.SEEK_SET) @@ -586,36 +575,16 @@ class TestFileTiff(PillowTestCase): im.load() self.assertFalse(fp.closed) - @unittest.skipUnless(features.check("libtiff"), "libtiff not installed") - def test_sampleformat_not_corrupted(self): - # Assert that a TIFF image with SampleFormat=UINT tag is not corrupted - # when saving to a new file. - # Pillow 6.0 fails with "OSError: cannot identify image file". - import base64 - - tiff = BytesIO( - base64.b64decode( - b"SUkqAAgAAAAPAP4ABAABAAAAAAAAAAABBAABAAAAAQAAAAEBBAABAAAAAQAA" - b"AAIBAwADAAAAwgAAAAMBAwABAAAACAAAAAYBAwABAAAAAgAAABEBBAABAAAA" - b"4AAAABUBAwABAAAAAwAAABYBBAABAAAAAQAAABcBBAABAAAACwAAABoBBQAB" - b"AAAAyAAAABsBBQABAAAA0AAAABwBAwABAAAAAQAAACgBAwABAAAAAQAAAFMB" - b"AwADAAAA2AAAAAAAAAAIAAgACAABAAAAAQAAAAEAAAABAAAAAQABAAEAAAB4" - b"nGNgYAAAAAMAAQ==" - ) - ) - out = BytesIO() - with Image.open(tiff) as im: - im.save(out, format="tiff") - out.seek(0) - with Image.open(out) as im: - im.load() + def test_string_dimension(self): + # Assert that an error is raised if one of the dimensions is a string + with self.assertRaises(ValueError): + Image.open("Tests/images/string_dimension.tiff") @unittest.skipUnless(is_win32(), "Windows only") class TestFileTiffW32(PillowTestCase): def test_fd_leak(self): tmpfile = self.tempfile("temp.tif") - import os # this is an mmaped file. with Image.open("Tests/images/uint16_1_4660.tif") as im: diff --git a/Tests/test_file_webp_metadata.py b/Tests/test_file_webp_metadata.py index ae528e3bf..6351dc1e1 100644 --- a/Tests/test_file_webp_metadata.py +++ b/Tests/test_file_webp_metadata.py @@ -1,3 +1,5 @@ +from io import BytesIO + from PIL import Image from .helper import PillowTestCase @@ -39,8 +41,6 @@ class TestFileWebpMetadata(PillowTestCase): self.assertEqual(exif_data, expected_exif) def test_write_exif_metadata(self): - from io import BytesIO - file_path = "Tests/images/flower.jpg" image = Image.open(file_path) expected_exif = image.info["exif"] @@ -73,8 +73,6 @@ class TestFileWebpMetadata(PillowTestCase): self.assertEqual(icc, expected_icc) def test_write_icc_metadata(self): - from io import BytesIO - file_path = "Tests/images/flower2.jpg" image = Image.open(file_path) expected_icc_profile = image.info["icc_profile"] @@ -95,8 +93,6 @@ class TestFileWebpMetadata(PillowTestCase): ) def test_read_no_exif(self): - from io import BytesIO - file_path = "Tests/images/flower.jpg" image = Image.open(file_path) self.assertIn("exif", image.info) diff --git a/Tests/test_file_xbm.py b/Tests/test_file_xbm.py index 9693ba05a..3d2259a21 100644 --- a/Tests/test_file_xbm.py +++ b/Tests/test_file_xbm.py @@ -1,3 +1,5 @@ +from io import BytesIO + from PIL import Image from .helper import PillowTestCase @@ -28,8 +30,6 @@ static char basic_bits[] = { class TestFileXbm(PillowTestCase): def test_pil151(self): - from io import BytesIO - im = Image.open(BytesIO(PIL151)) im.load() diff --git a/Tests/test_image.py b/Tests/test_image.py index cbf52b3b4..54a7680c6 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -1,5 +1,6 @@ import os import shutil +import tempfile from PIL import Image from PIL._util import py3 @@ -125,8 +126,6 @@ class TestImage(PillowTestCase): def test_tempfile(self): # see #1460, pathlib support breaks tempfile.TemporaryFile on py27 # Will error out on save on 3.0.0 - import tempfile - im = hopper() with tempfile.TemporaryFile() as fp: im.save(fp, "JPEG") @@ -586,6 +585,15 @@ class TestImage(PillowTestCase): self.assertFalse(fp.closed) + def test_overrun(self): + for file in ["fli_overrun.bin", "sgi_overrun.bin", "pcx_overrun.bin"]: + im = Image.open(os.path.join("Tests/images", file)) + try: + im.load() + self.assertFail() + except IOError as e: + self.assertEqual(str(e), "buffer overrun when reading image file") + class MockEncoder(object): pass diff --git a/Tests/test_image_access.py b/Tests/test_image_access.py index 577df3dff..d7bd828e9 100644 --- a/Tests/test_image_access.py +++ b/Tests/test_image_access.py @@ -1,5 +1,8 @@ +import ctypes import os +import subprocess import sys +from distutils import ccompiler, sysconfig from PIL import Image @@ -337,10 +340,6 @@ class TestEmbeddable(unittest.TestCase): "Failing on AppVeyor / GitHub Actions when run from subprocess, not from shell", ) def test_embeddable(self): - import subprocess - import ctypes - from distutils import ccompiler, sysconfig - with open("embed_pil.c", "w") as fh: fh.write( """ diff --git a/Tests/test_imagefile.py b/Tests/test_imagefile.py index ceccd2285..a367f62df 100644 --- a/Tests/test_imagefile.py +++ b/Tests/test_imagefile.py @@ -103,6 +103,14 @@ class TestImageFile(PillowTestCase): parser = ImageFile.Parser() parser.feed(1) + def test_negative_stride(self): + with open("Tests/images/raw_negative_stride.bin", "rb") as f: + input = f.read() + p = ImageFile.Parser() + p.feed(input) + with self.assertRaises(IOError): + p.close() + def test_truncated_with_errors(self): if "zip_encoder" not in codecs: self.skipTest("PNG (zlib) encoder not available") diff --git a/Tests/test_imagegrab.py b/Tests/test_imagegrab.py index a29bc019e..bea7f68b3 100644 --- a/Tests/test_imagegrab.py +++ b/Tests/test_imagegrab.py @@ -8,7 +8,11 @@ try: class TestImageGrab(PillowTestCase): def test_grab(self): - for im in [ImageGrab.grab(), ImageGrab.grab(include_layered_windows=True)]: + for im in [ + ImageGrab.grab(), + ImageGrab.grab(include_layered_windows=True), + ImageGrab.grab(all_screens=True), + ]: self.assert_image(im, im.mode, im.size) def test_grabclipboard(self): diff --git a/Tests/test_imageops.py b/Tests/test_imageops.py index 85529a708..2cdbbe02f 100644 --- a/Tests/test_imageops.py +++ b/Tests/test_imageops.py @@ -81,6 +81,16 @@ class TestImageOps(PillowTestCase): newimg = ImageOps.fit(hopper("RGB").resize((100, 1)), (35, 35)) self.assertEqual(newimg.size, (35, 35)) + def test_fit_same_ratio(self): + # The ratio for this image is 1000.0 / 755 = 1.3245033112582782 + # If the ratios are not acknowledged to be the same, + # and Pillow attempts to adjust the width to + # 1.3245033112582782 * 755 = 1000.0000000000001 + # then centering this greater width causes a negative x offset when cropping + with Image.new("RGB", (1000, 755)) as im: + new_im = ImageOps.fit(im, (1000, 755)) + self.assertEqual(new_im.size, (1000, 755)) + def test_pad(self): # Same ratio im = hopper() diff --git a/Tests/test_imageqt.py b/Tests/test_imageqt.py index dc43e6bc7..9248291c3 100644 --- a/Tests/test_imageqt.py +++ b/Tests/test_imageqt.py @@ -1,13 +1,7 @@ -import sys -import warnings - from PIL import ImageQt from .helper import PillowTestCase, hopper -if sys.version_info.major >= 3: - from importlib import reload - if ImageQt.qt_is_installed: from PIL.ImageQt import qRgba @@ -35,10 +29,6 @@ class PillowQPixmapTestCase(PillowQtTestCase): try: if ImageQt.qt_version == "5": from PyQt5.QtGui import QGuiApplication - elif ImageQt.qt_version == "4": - from PyQt4.QtGui import QGuiApplication - elif ImageQt.qt_version == "side": - from PySide.QtGui import QGuiApplication elif ImageQt.qt_version == "side2": from PySide2.QtGui import QGuiApplication except ImportError: @@ -59,10 +49,6 @@ class TestImageQt(PillowQtTestCase, PillowTestCase): # equivalent to an unsigned int. if ImageQt.qt_version == "5": from PyQt5.QtGui import qRgb - elif ImageQt.qt_version == "4": - from PyQt4.QtGui import qRgb - elif ImageQt.qt_version == "side": - from PySide.QtGui import qRgb elif ImageQt.qt_version == "side2": from PySide2.QtGui import qRgb @@ -83,13 +69,3 @@ class TestImageQt(PillowQtTestCase, PillowTestCase): def test_image(self): for mode in ("1", "RGB", "RGBA", "L", "P"): ImageQt.ImageQt(hopper(mode)) - - def test_deprecated(self): - with warnings.catch_warnings(record=True) as w: - reload(ImageQt) - if ImageQt.qt_version in ["4", "side"]: - self.assertEqual(len(w), 1) - self.assertTrue(issubclass(w[-1].category, DeprecationWarning)) - else: - # No warning. - self.assertEqual(w, []) diff --git a/Tests/test_locale.py b/Tests/test_locale.py index 86229dbcb..cbec8b965 100644 --- a/Tests/test_locale.py +++ b/Tests/test_locale.py @@ -31,4 +31,8 @@ class TestLocale(PillowTestCase): locale.setlocale(locale.LC_ALL, "polish") except locale.Error: unittest.skip("Polish locale not available") - Image.open(path) + + try: + Image.open(path) + finally: + locale.setlocale(locale.LC_ALL, (None, None)) diff --git a/Tests/test_qt_image_toqimage.py b/Tests/test_qt_image_toqimage.py index d0c223b1a..5115c1862 100644 --- a/Tests/test_qt_image_toqimage.py +++ b/Tests/test_qt_image_toqimage.py @@ -9,25 +9,9 @@ if ImageQt.qt_is_installed: try: from PyQt5 import QtGui from PyQt5.QtWidgets import QWidget, QHBoxLayout, QLabel, QApplication - - QT_VERSION = 5 except (ImportError, RuntimeError): - try: - from PySide2 import QtGui - from PySide2.QtWidgets import QWidget, QHBoxLayout, QLabel, QApplication - - QT_VERSION = 5 - except (ImportError, RuntimeError): - try: - from PyQt4 import QtGui - from PyQt4.QtGui import QWidget, QHBoxLayout, QLabel, QApplication - - QT_VERSION = 4 - except (ImportError, RuntimeError): - from PySide import QtGui - from PySide.QtGui import QWidget, QHBoxLayout, QLabel, QApplication - - QT_VERSION = 4 + from PySide2 import QtGui + from PySide2.QtWidgets import QWidget, QHBoxLayout, QLabel, QApplication class TestToQImage(PillowQtTestCase, PillowTestCase): @@ -60,10 +44,6 @@ class TestToQImage(PillowQtTestCase, PillowTestCase): # Check that it actually worked. reloaded = Image.open(tempfile) - # Gray images appear to come back in palette mode. - # They're roughly equivalent - if QT_VERSION == 4 and mode == "L": - src = src.convert("P") self.assert_image_equal(reloaded, src) def test_segfault(self): diff --git a/docs/deprecations.rst b/docs/deprecations.rst index f00f3e31f..396300f1f 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -45,17 +45,6 @@ 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 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -81,27 +70,6 @@ Deprecated Deprecated Deprecated ``IptcImagePlugin.__version__`` ``PixarImagePlugin.__version__`` =============================== ================================= ================================== -Setting the size of TIFF images -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. deprecated:: 5.3.0 - -Setting the image size of a TIFF image (eg. ``im.size = (256, 256)``) issues -a ``DeprecationWarning``: - -.. code-block:: none - - 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 constant -~~~~~~~~~~~~~~~~~~~~~~~ - -.. deprecated:: 5.2.0 - -``PILLOW_VERSION`` has been deprecated and will be removed in 7.0.0. Use ``__version__`` -instead. - ImageCms.CmsProfile attributes ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -128,6 +96,32 @@ Removed features Deprecated features are only removed in major releases after an appropriate period of deprecation has passed. +PILLOW_VERSION constant +~~~~~~~~~~~~~~~~~~~~~~~ + +*Removed in version 7.0.0.* + +``PILLOW_VERSION`` has been removed. Use ``__version__`` instead. + +PyQt4 and PySide +~~~~~~~~~~~~~~~~ + +*Removed in version 7.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 removed from ``ImageQt``. Please upgrade to PyQt5 +or PySide2. + +Setting the size of TIFF images +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +*Removed in version 7.0.0.* + +Setting the size of a TIFF image directly (eg. ``im.size = (256, 256)``) throws +an error. Use ``Image.resize`` instead. + VERSION constant ~~~~~~~~~~~~~~~~ diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index 3ce4ccb2b..088fd5a03 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -113,8 +113,8 @@ to seek to the next frame (``im.seek(im.tell() + 1)``). Saving ~~~~~~ -When calling :py:meth:`~PIL.Image.Image.save`, the following options -are available:: +When calling :py:meth:`~PIL.Image.Image.save` to write a GIF file, the +following options are available:: im.save(out, save_all=True, append_images=[im1, im2, ...]) @@ -813,8 +813,9 @@ Saving sequences library is v0.5.0 or later. You can check webp animation support at runtime by calling ``features.check("webp_anim")``. -When calling :py:meth:`~PIL.Image.Image.save`, the following options -are available when the ``save_all`` argument is present and true. +When calling :py:meth:`~PIL.Image.Image.save` to write a WebP file, the +following options are available when the ``save_all`` argument is present and +true. **append_images** A list of images to append as additional frames. Each of the diff --git a/docs/reference/ImageGrab.rst b/docs/reference/ImageGrab.rst index ed7482e99..e94e21cb9 100644 --- a/docs/reference/ImageGrab.rst +++ b/docs/reference/ImageGrab.rst @@ -11,7 +11,7 @@ or the clipboard to a PIL image memory. .. versionadded:: 1.1.3 -.. py:function:: PIL.ImageGrab.grab(bbox=None, include_layered_windows=False) +.. py:function:: PIL.ImageGrab.grab(bbox=None, include_layered_windows=False, all_screens=False) Take a snapshot of the screen. The pixels inside the bounding box are returned as an "RGB" image on Windows or "RGBA" on macOS. @@ -20,7 +20,13 @@ or the clipboard to a PIL image memory. .. versionadded:: 1.1.3 (Windows), 3.0.0 (macOS) :param bbox: What region to copy. Default is the entire screen. + Note that on Windows OS, the top-left point may be negative if ``all_screens=True`` is used. :param include_layered_windows: Includes layered windows. Windows OS only. + + .. versionadded:: 6.1.0 + :param all_screens: Capture all monitors. Windows OS only. + + .. versionadded:: 6.2.0 :return: An image .. py:function:: PIL.ImageGrab.grabclipboard() diff --git a/docs/reference/ImageOps.rst b/docs/reference/ImageOps.rst index 50cea90ca..1c86d168f 100644 --- a/docs/reference/ImageOps.rst +++ b/docs/reference/ImageOps.rst @@ -8,13 +8,13 @@ The :py:mod:`ImageOps` module contains a number of ‘ready-made’ image processing operations. This module is somewhat experimental, and most operators only work on L and RGB images. -Only bug fixes have been added since the Pillow fork. - .. versionadded:: 1.1.3 .. autofunction:: autocontrast .. autofunction:: colorize +.. autofunction:: pad .. autofunction:: crop +.. autofunction:: scale .. autofunction:: deform .. autofunction:: equalize .. autofunction:: expand @@ -25,3 +25,4 @@ Only bug fixes have been added since the Pillow fork. .. autofunction:: mirror .. autofunction:: posterize .. autofunction:: solarize +.. autofunction:: exif_transpose diff --git a/docs/reference/ImageQt.rst b/docs/reference/ImageQt.rst index 5128f28fb..7dd7084db 100644 --- a/docs/reference/ImageQt.rst +++ b/docs/reference/ImageQt.rst @@ -4,14 +4,8 @@ :py:mod:`ImageQt` Module ======================== -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. +The :py:mod:`ImageQt` module contains support for creating PyQt5 or PySide2 QImage +objects from PIL images. .. versionadded:: 1.1.6 @@ -20,7 +14,7 @@ future version. Please upgrade to PyQt5 or PySide2. Creates an :py:class:`~PIL.ImageQt.ImageQt` object from a PIL :py:class:`~PIL.Image.Image` object. This class is a subclass of QtGui.QImage, which means that you can pass the resulting objects directly - to PyQt4/PyQt5/PySide API functions and methods. + to PyQt5/PySide2 API functions and methods. This operation is currently supported for mode 1, L, P, RGB, and RGBA images. To handle other modes, you need to convert the image first. diff --git a/docs/releasenotes/6.2.0.rst b/docs/releasenotes/6.2.0.rst index 68d209f35..9576e6be8 100644 --- a/docs/releasenotes/6.2.0.rst +++ b/docs/releasenotes/6.2.0.rst @@ -44,6 +44,12 @@ creates the following image: .. image:: ../../Tests/images/imagedraw_stroke_different.png +ImageGrab on multi-monitor Windows +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +An ``all_screens`` argument has been added to ``ImageGrab.grab``. If ``True``, +all monitors will be included in the created image. + API Changes =========== @@ -64,6 +70,13 @@ 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.2.x the last release series to support Python 2. +Image.frombuffer +~~~~~~~~~~~~~~~~ + +There has been a longstanding warning that the defaults of ``Image.frombuffer`` +may change in the future for the "raw" decoder. The change will now take place +in Pillow 7.0. + Other Changes ============= diff --git a/src/PIL/EpsImagePlugin.py b/src/PIL/EpsImagePlugin.py index 219f2dbb8..721ad4c21 100644 --- a/src/PIL/EpsImagePlugin.py +++ b/src/PIL/EpsImagePlugin.py @@ -23,7 +23,9 @@ import io import os import re +import subprocess import sys +import tempfile from . import Image, ImageFile from ._binary import i32le as i32 @@ -61,8 +63,6 @@ def has_ghostscript(): if gs_windows_binary: return True if not sys.platform.startswith("win"): - import subprocess - try: with open(os.devnull, "wb") as devnull: subprocess.check_call(["gs", "--version"], stdout=devnull) @@ -91,9 +91,6 @@ def Ghostscript(tile, size, fp, scale=1): float((72.0 * size[1]) / (bbox[3] - bbox[1])), ) - import subprocess - import tempfile - out_fd, outfile = tempfile.mkstemp() os.close(out_fd) diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py index 07f5ab683..a92c142f0 100644 --- a/src/PIL/GifImagePlugin.py +++ b/src/PIL/GifImagePlugin.py @@ -25,6 +25,8 @@ # import itertools +import os +import subprocess from . import Image, ImageChops, ImageFile, ImagePalette, ImageSequence from ._binary import i8, i16le as i16, o8, o16le as o16 @@ -265,6 +267,7 @@ class GifImageFile(ImageFile.ImageFile): self.dispose = None elif self.disposal_method == 2: # replace with background colour + Image._decompression_bomb_check(self.size) self.dispose = Image.core.fill("P", self.size, self.info["background"]) else: # replace with previous contents @@ -616,24 +619,22 @@ def _save_netpbm(im, fp, filename): # If you need real GIF compression and/or RGB quantization, you # can use the external NETPBM/PBMPLUS utilities. See comments # below for information on how to enable this. - - import os - from subprocess import Popen, check_call, PIPE, CalledProcessError - tempfile = im._dump() with open(filename, "wb") as f: if im.mode != "RGB": with open(os.devnull, "wb") as devnull: - check_call(["ppmtogif", tempfile], stdout=f, stderr=devnull) + subprocess.check_call(["ppmtogif", tempfile], stdout=f, stderr=devnull) else: # Pipe ppmquant output into ppmtogif # "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) - togif_proc = Popen( + quant_proc = subprocess.Popen( + quant_cmd, stdout=subprocess.PIPE, stderr=devnull + ) + togif_proc = subprocess.Popen( togif_cmd, stdin=quant_proc.stdout, stdout=f, stderr=devnull ) @@ -642,11 +643,11 @@ def _save_netpbm(im, fp, filename): retcode = quant_proc.wait() if retcode: - raise CalledProcessError(retcode, quant_cmd) + raise subprocess.CalledProcessError(retcode, quant_cmd) retcode = togif_proc.wait() if retcode: - raise CalledProcessError(retcode, togif_cmd) + raise subprocess.CalledProcessError(retcode, togif_cmd) try: os.unlink(tempfile) diff --git a/src/PIL/IcnsImagePlugin.py b/src/PIL/IcnsImagePlugin.py index 75ea18b6b..9cf7a90cd 100644 --- a/src/PIL/IcnsImagePlugin.py +++ b/src/PIL/IcnsImagePlugin.py @@ -19,6 +19,7 @@ import io import os import shutil import struct +import subprocess import sys import tempfile @@ -333,11 +334,12 @@ def _save(im, fp, filename): last_w = w * 2 # iconutil -c icns -o {} {} - from subprocess import Popen, PIPE, CalledProcessError convert_cmd = ["iconutil", "-c", "icns", "-o", filename, iconset] with open(os.devnull, "wb") as devnull: - convert_proc = Popen(convert_cmd, stdout=PIPE, stderr=devnull) + convert_proc = subprocess.Popen( + convert_cmd, stdout=subprocess.PIPE, stderr=devnull + ) convert_proc.stdout.close() @@ -347,7 +349,7 @@ def _save(im, fp, filename): shutil.rmtree(iconset) if retcode: - raise CalledProcessError(retcode, convert_cmd) + raise subprocess.CalledProcessError(retcode, convert_cmd) Image.register_open(IcnsImageFile.format, IcnsImageFile, lambda x: x[:4] == b"icns") diff --git a/src/PIL/IcoImagePlugin.py b/src/PIL/IcoImagePlugin.py index fc728d6fb..148e604f8 100644 --- a/src/PIL/IcoImagePlugin.py +++ b/src/PIL/IcoImagePlugin.py @@ -180,6 +180,7 @@ class IcoFile(object): else: # XOR + AND mask bmp frame im = BmpImagePlugin.DibImageFile(self.buf) + Image._decompression_bomb_check(im.size) # change tile dimension to only encompass XOR image im._size = (im.size[0], int(im.size[1] / 2)) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 4576d6a17..27d61da6d 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -32,12 +32,13 @@ import numbers import os import struct import sys +import tempfile import warnings # VERSION was removed in Pillow 6.0.0. -# PILLOW_VERSION is deprecated and will be removed in Pillow 7.0.0. +# PILLOW_VERSION was removed in Pillow 7.0.0. # Use __version__ instead. -from . import PILLOW_VERSION, ImageMode, TiffTags, __version__, _plugins +from . import ImageMode, TiffTags, __version__, _plugins from ._binary import i8, i32le from ._util import deferred_error, isPath, isStringType, py3 @@ -57,9 +58,6 @@ except ImportError: from collections import Callable, MutableMapping -# Silence warning -assert PILLOW_VERSION - logger = logging.getLogger(__name__) @@ -642,8 +640,6 @@ class Image(object): self.load() def _dump(self, file=None, format=None, **options): - import tempfile - suffix = "" if format: suffix = "." + format @@ -2592,14 +2588,7 @@ def frombuffer(mode, size, data, decoder_name="raw", *args): if decoder_name == "raw": if args == (): - warnings.warn( - "the frombuffer defaults may change in a future release; " - "for portability, change the call to read:\n" - " frombuffer(mode, size, data, 'raw', mode, 0, 1)", - RuntimeWarning, - stacklevel=2, - ) - args = mode, 0, -1 # may change to (mode, 0, 1) post-1.1.6 + args = mode, 0, 1 if args[0] in _MAPMODES: im = new(mode, (1, 1)) im = im._new(core.map_buffer(data, size, decoder_name, None, 0, args)) diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index 5cce9af8e..2546d2aa8 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -25,8 +25,10 @@ # See the README file for information on usage and redistribution. # +import base64 import os import sys +from io import BytesIO from . import Image from ._util import isDirectory, isPath, py3 @@ -713,9 +715,6 @@ def load_default(): :return: A font object. """ - from io import BytesIO - import base64 - f = ImageFont() f._load_pilfont_data( # courB08 diff --git a/src/PIL/ImageGrab.py b/src/PIL/ImageGrab.py index 8f359e229..e587d942d 100644 --- a/src/PIL/ImageGrab.py +++ b/src/PIL/ImageGrab.py @@ -15,21 +15,18 @@ # See the README file for information on usage and redistribution. # +import os +import subprocess import sys +import tempfile from . import Image -if sys.platform == "win32": - grabber = Image.core.grabscreen -elif sys.platform == "darwin": - import os - import tempfile - import subprocess -else: +if sys.platform not in ["win32", "darwin"]: raise ImportError("ImageGrab is macOS and Windows only") -def grab(bbox=None, include_layered_windows=False): +def grab(bbox=None, include_layered_windows=False, all_screens=False): if sys.platform == "darwin": fh, filepath = tempfile.mkstemp(".png") os.close(fh) @@ -37,8 +34,10 @@ def grab(bbox=None, include_layered_windows=False): im = Image.open(filepath) im.load() os.unlink(filepath) + if bbox: + im = im.crop(bbox) else: - size, data = grabber(include_layered_windows) + offset, size, data = Image.core.grabscreen(include_layered_windows, all_screens) im = Image.frombytes( "RGB", size, @@ -49,8 +48,10 @@ def grab(bbox=None, include_layered_windows=False): (size[0] * 3 + 3) & -4, -1, ) - if bbox: - im = im.crop(bbox) + if bbox: + x0, y0 = offset + left, top, right, bottom = bbox + im = im.crop((left - x0, top - y0, right - x0, bottom - y0)) return im diff --git a/src/PIL/ImageOps.py b/src/PIL/ImageOps.py index cfdd5c02a..5052cb74d 100644 --- a/src/PIL/ImageOps.py +++ b/src/PIL/ImageOps.py @@ -426,7 +426,11 @@ def fit(image, size, method=Image.NEAREST, bleed=0.0, centering=(0.5, 0.5)): output_ratio = float(size[0]) / size[1] # figure out if the sides or top/bottom will be cropped off - if live_size_ratio >= output_ratio: + if live_size_ratio == output_ratio: + # live_size is already the needed ratio + crop_width = live_size[0] + crop_height = live_size[1] + elif live_size_ratio >= output_ratio: # live_size is wider than what's needed, crop the sides crop_width = output_ratio * live_size[1] crop_height = live_size[1] diff --git a/src/PIL/ImageQt.py b/src/PIL/ImageQt.py index 2edb0a12b..da60cacd0 100644 --- a/src/PIL/ImageQt.py +++ b/src/PIL/ImageQt.py @@ -17,18 +17,12 @@ # import sys -import warnings from io import BytesIO from . import Image from ._util import isPath, py3 -qt_versions = [["5", "PyQt5"], ["side2", "PySide2"], ["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." -) +qt_versions = [["5", "PyQt5"], ["side2", "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) @@ -40,16 +34,6 @@ for qt_version, qt_module in qt_versions: elif qt_module == "PySide2": from PySide2.QtGui import QImage, qRgba, QPixmap from PySide2.QtCore import QBuffer, QIODevice - 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 diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py index 020b95219..da0759129 100644 --- a/src/PIL/JpegImagePlugin.py +++ b/src/PIL/JpegImagePlugin.py @@ -36,7 +36,10 @@ from __future__ import print_function import array import io +import os import struct +import subprocess +import tempfile import warnings from . import Image, ImageFile, TiffImagePlugin @@ -444,10 +447,6 @@ class JpegImageFile(ImageFile.ImageFile): # ALTERNATIVE: handle JPEGs via the IJG command line utilities - import subprocess - import tempfile - import os - f, path = tempfile.mkstemp() os.close(f) if os.path.exists(self.filename): @@ -772,9 +771,6 @@ def _save(im, fp, filename): def _save_cjpeg(im, fp, filename): # ALTERNATIVE: handle JPEGs via the IJG command line utilities. - import os - import subprocess - tempfile = im._dump() subprocess.check_call(["cjpeg", "-outfile", filename, tempfile]) try: diff --git a/src/PIL/PsdImagePlugin.py b/src/PIL/PsdImagePlugin.py index 9eb0c0966..f72ad5f44 100644 --- a/src/PIL/PsdImagePlugin.py +++ b/src/PIL/PsdImagePlugin.py @@ -224,9 +224,11 @@ def _layerinfo(file): # skip over blend flags and extra information read(12) # filler name = "" - size = i32(read(4)) + size = i32(read(4)) # length of the extra data field combined = 0 if size: + data_end = file.tell() + size + length = i32(read(4)) if length: file.seek(length - 16, io.SEEK_CUR) @@ -244,7 +246,7 @@ def _layerinfo(file): name = read(length).decode("latin-1", "replace") combined += length + 1 - file.seek(size - combined, io.SEEK_CUR) + file.seek(data_end) layers.append((name, mode, (x0, y0, x1, y1))) # get tiles diff --git a/src/PIL/SpiderImagePlugin.py b/src/PIL/SpiderImagePlugin.py index 5663a5b53..f1cae4d9f 100644 --- a/src/PIL/SpiderImagePlugin.py +++ b/src/PIL/SpiderImagePlugin.py @@ -236,7 +236,7 @@ def loadImageSeries(filelist=None): def makeSpiderHeader(im): nsam, nrow = im.size lenbyt = nsam * 4 # There are labrec records in the header - labrec = 1024 / lenbyt + labrec = int(1024 / lenbyt) if 1024 % lenbyt != 0: labrec += 1 labbyt = labrec * lenbyt diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index 9fcffb742..b0d465fe2 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -854,7 +854,7 @@ class ImageFileDirectory_v2(MutableMapping): # pass 2: write entries to file for tag, typ, count, value, data in entries: - if DEBUG > 1: + if DEBUG: print(tag, typ, count, repr(value), repr(data)) result += self._pack("HHL4s", tag, typ, count, value) @@ -1079,19 +1079,6 @@ class TiffImageFile(ImageFile.ImageFile): """Return the current frame number""" return self.__frame - @property - def size(self): - return self._size - - @size.setter - def size(self, value): - warnings.warn( - "Setting the size of a TIFF image directly is deprecated, and will" - " be removed in a future version. Use the resize method instead.", - DeprecationWarning, - ) - self._size = value - def load(self): if self.use_load_libtiff: return self._load_libtiff() @@ -1239,8 +1226,8 @@ class TiffImageFile(ImageFile.ImageFile): print("- YCbCr subsampling:", self.tag.get(530)) # size - xsize = self.tag_v2.get(IMAGEWIDTH) - ysize = self.tag_v2.get(IMAGELENGTH) + xsize = int(self.tag_v2.get(IMAGEWIDTH)) + ysize = int(self.tag_v2.get(IMAGELENGTH)) self._size = xsize, ysize if DEBUG: diff --git a/src/PIL/TiffTags.py b/src/PIL/TiffTags.py index 82719db0e..c047f42b6 100644 --- a/src/PIL/TiffTags.py +++ b/src/PIL/TiffTags.py @@ -120,7 +120,7 @@ TAGS_V2 = { 277: ("SamplesPerPixel", SHORT, 1), 278: ("RowsPerStrip", LONG, 1), 279: ("StripByteCounts", LONG, 0), - 280: ("MinSampleValue", LONG, 0), + 280: ("MinSampleValue", SHORT, 0), 281: ("MaxSampleValue", SHORT, 0), 282: ("XResolution", RATIONAL, 1), 283: ("YResolution", RATIONAL, 1), @@ -182,7 +182,7 @@ TAGS_V2 = { # FIXME add more tags here 34665: ("ExifIFD", LONG, 1), 34675: ("ICCProfile", UNDEFINED, 1), - 34853: ("GPSInfoIFD", BYTE, 1), + 34853: ("GPSInfoIFD", LONG, 1), # MPInfo 45056: ("MPFVersion", UNDEFINED, 1), 45057: ("NumberOfImages", LONG, 1), diff --git a/src/PIL/__init__.py b/src/PIL/__init__.py index 59eccc9b5..fd02b40d9 100644 --- a/src/PIL/__init__.py +++ b/src/PIL/__init__.py @@ -9,7 +9,6 @@ PIL is the Python Imaging Library by Fredrik Lundh and Contributors. Copyright (c) 1999 by Secret Labs AB. Use PIL.__version__ for this Pillow version. -PIL.VERSION is the old PIL version and will be removed in the future. ;-) """ @@ -17,9 +16,9 @@ PIL.VERSION is the old PIL version and will be removed in the future. from . import _version # VERSION was removed in Pillow 6.0.0. -# PILLOW_VERSION is deprecated and will be removed in Pillow 7.0.0. +# PILLOW_VERSION was removed in Pillow 7.0.0. # Use __version__ instead. -PILLOW_VERSION = __version__ = _version.__version__ +__version__ = _version.__version__ del _version diff --git a/src/PIL/_version.py b/src/PIL/_version.py index d9eaea530..eddf15683 100644 --- a/src/PIL/_version.py +++ b/src/PIL/_version.py @@ -1,2 +1,2 @@ # Master version for Pillow -__version__ = "6.2.0.dev0" +__version__ = "7.0.0.dev0" diff --git a/src/display.c b/src/display.c index ab005d4b4..efabf1c86 100644 --- a/src/display.c +++ b/src/display.c @@ -319,18 +319,23 @@ PyImaging_DisplayModeWin32(PyObject* self, PyObject* args) /* -------------------------------------------------------------------- */ /* Windows screen grabber */ +typedef HANDLE(__stdcall* Func_SetThreadDpiAwarenessContext)(HANDLE); + PyObject* PyImaging_GrabScreenWin32(PyObject* self, PyObject* args) { - int width, height; - int includeLayeredWindows = 0; + int x = 0, y = 0, width, height; + int includeLayeredWindows = 0, all_screens = 0; HBITMAP bitmap; BITMAPCOREHEADER core; HDC screen, screen_copy; DWORD rop; PyObject* buffer; + HANDLE dpiAwareness; + HMODULE user32; + Func_SetThreadDpiAwarenessContext SetThreadDpiAwarenessContext_function; - if (!PyArg_ParseTuple(args, "|i", &includeLayeredWindows)) + if (!PyArg_ParseTuple(args, "|ii", &includeLayeredWindows, &all_screens)) return NULL; /* step 1: create a memory DC large enough to hold the @@ -339,8 +344,32 @@ PyImaging_GrabScreenWin32(PyObject* self, PyObject* args) screen = CreateDC("DISPLAY", NULL, NULL, NULL); screen_copy = CreateCompatibleDC(screen); - width = GetDeviceCaps(screen, HORZRES); - height = GetDeviceCaps(screen, VERTRES); + // added in Windows 10 (1607) + // loaded dynamically to avoid link errors + user32 = LoadLibraryA("User32.dll"); + SetThreadDpiAwarenessContext_function = + (Func_SetThreadDpiAwarenessContext) + GetProcAddress(user32, "SetThreadDpiAwarenessContext"); + if (SetThreadDpiAwarenessContext_function != NULL) { + // DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE = ((DPI_CONTEXT_HANDLE)-3) + dpiAwareness = SetThreadDpiAwarenessContext_function((HANDLE) -3); + } + + if (all_screens) { + x = GetSystemMetrics(SM_XVIRTUALSCREEN); + y = GetSystemMetrics(SM_YVIRTUALSCREEN); + width = GetSystemMetrics(SM_CXVIRTUALSCREEN); + height = GetSystemMetrics(SM_CYVIRTUALSCREEN); + } else { + width = GetDeviceCaps(screen, HORZRES); + height = GetDeviceCaps(screen, VERTRES); + } + + if (SetThreadDpiAwarenessContext_function != NULL) { + SetThreadDpiAwarenessContext_function(dpiAwareness); + } + + FreeLibrary(user32); bitmap = CreateCompatibleBitmap(screen, width, height); if (!bitmap) @@ -354,7 +383,7 @@ PyImaging_GrabScreenWin32(PyObject* self, PyObject* args) rop = SRCCOPY; if (includeLayeredWindows) rop |= CAPTUREBLT; - if (!BitBlt(screen_copy, 0, 0, width, height, screen, 0, 0, rop)) + if (!BitBlt(screen_copy, 0, 0, width, height, screen, x, y, rop)) goto error; /* step 3: extract bits from bitmap */ @@ -376,7 +405,7 @@ PyImaging_GrabScreenWin32(PyObject* self, PyObject* args) DeleteDC(screen_copy); DeleteDC(screen); - return Py_BuildValue("(ii)N", width, height, buffer); + return Py_BuildValue("(ii)(ii)N", x, y, width, height, buffer); error: PyErr_SetString(PyExc_IOError, "screen grab failed"); diff --git a/src/libImaging/FliDecode.c b/src/libImaging/FliDecode.c index e21259900..5f4485f89 100644 --- a/src/libImaging/FliDecode.c +++ b/src/libImaging/FliDecode.c @@ -30,7 +30,7 @@ ImagingFliDecode(Imaging im, ImagingCodecState state, UINT8* buf, Py_ssize_t byt { UINT8* ptr; int framesize; - int c, chunks; + int c, chunks, advance; int l, lines; int i, j, x = 0, y, ymax; @@ -59,10 +59,16 @@ ImagingFliDecode(Imaging im, ImagingCodecState state, UINT8* buf, Py_ssize_t byt chunks = I16(ptr+6); ptr += 16; + bytes -= 16; /* Process subchunks */ for (c = 0; c < chunks; c++) { - UINT8 *data = ptr + 6; + UINT8* data; + if (bytes < 10) { + state->errcode = IMAGING_CODEC_OVERRUN; + return -1; + } + data = ptr + 6; switch (I16(ptr+4)) { case 4: case 11: /* FLI COLOR chunk */ @@ -198,7 +204,9 @@ ImagingFliDecode(Imaging im, ImagingCodecState state, UINT8* buf, Py_ssize_t byt state->errcode = IMAGING_CODEC_UNKNOWN; return -1; } - ptr += I32(ptr); + advance = I32(ptr); + ptr += advance; + bytes -= advance; } return -1; /* end of frame */ diff --git a/src/libImaging/PcxDecode.c b/src/libImaging/PcxDecode.c index 4a5931bee..67dcc1e08 100644 --- a/src/libImaging/PcxDecode.c +++ b/src/libImaging/PcxDecode.c @@ -22,6 +22,11 @@ ImagingPcxDecode(Imaging im, ImagingCodecState state, UINT8* buf, Py_ssize_t byt UINT8 n; UINT8* ptr; + if (strcmp(im->mode, "1") == 0 && state->xsize > state->bytes * 8) { + state->errcode = IMAGING_CODEC_OVERRUN; + return -1; + } + ptr = buf; for (;;) { diff --git a/src/libImaging/RawDecode.c b/src/libImaging/RawDecode.c index 774d4245b..c069bdb88 100644 --- a/src/libImaging/RawDecode.c +++ b/src/libImaging/RawDecode.c @@ -33,8 +33,15 @@ ImagingRawDecode(Imaging im, ImagingCodecState state, UINT8* buf, Py_ssize_t byt /* get size of image data and padding */ state->bytes = (state->xsize * state->bits + 7) / 8; - rawstate->skip = (rawstate->stride) ? - rawstate->stride - state->bytes : 0; + if (rawstate->stride) { + rawstate->skip = rawstate->stride - state->bytes; + if (rawstate->skip < 0) { + state->errcode = IMAGING_CODEC_CONFIG; + return -1; + } + } else { + rawstate->skip = 0; + } /* check image orientation */ if (state->ystep < 0) { diff --git a/src/libImaging/SgiRleDecode.c b/src/libImaging/SgiRleDecode.c index d8341f3e5..8a81ba8e6 100644 --- a/src/libImaging/SgiRleDecode.c +++ b/src/libImaging/SgiRleDecode.c @@ -157,6 +157,11 @@ ImagingSgiRleDecode(Imaging im, ImagingCodecState state, c->rlelength = c->lengthtab[c->rowno + c->channo * im->ysize]; c->rleoffset -= SGI_HEADER_SIZE; + if (c->rleoffset + c->rlelength > c->bufsize) { + state->errcode = IMAGING_CODEC_OVERRUN; + return -1; + } + /* row decompression */ if (c->bpc ==1) { if(expandrow(&state->buffer[c->channo], &ptr[c->rleoffset], c->rlelength, im->bands)) diff --git a/src/libImaging/TiffDecode.c b/src/libImaging/TiffDecode.c index e72dae0c8..7592f7f39 100644 --- a/src/libImaging/TiffDecode.c +++ b/src/libImaging/TiffDecode.c @@ -405,8 +405,12 @@ int ImagingLibTiffDecode(Imaging im, ImagingCodecState state, UINT8* buffer, Py_ UINT32 strip_row, row_byte_size; UINT8 *new_data; UINT32 rows_per_strip; + int ret; - TIFFGetField(tiff, TIFFTAG_ROWSPERSTRIP, &rows_per_strip); + ret = TIFFGetField(tiff, TIFFTAG_ROWSPERSTRIP, &rows_per_strip); + if (ret != 1) { + rows_per_strip = state->ysize; + } TRACE(("RowsPerStrip: %u \n", rows_per_strip)); // We could use TIFFStripSize, but for YCbCr data it returns subsampled data size diff --git a/winbuild/appveyor_install_pypy.cmd b/winbuild/appveyor_install_pypy2.cmd similarity index 100% rename from winbuild/appveyor_install_pypy.cmd rename to winbuild/appveyor_install_pypy2.cmd diff --git a/winbuild/appveyor_install_pypy3.cmd b/winbuild/appveyor_install_pypy3.cmd new file mode 100644 index 000000000..63659b165 --- /dev/null +++ b/winbuild/appveyor_install_pypy3.cmd @@ -0,0 +1,3 @@ +curl -fsSL -o pypy3.zip http://buildbot.pypy.org/nightly/py3.6/pypy-c-jit-97588-7392d01b93d0-win32.zip +7z x pypy3.zip -oc:\ +c:\Python37\Scripts\virtualenv.exe -p c:\pypy-c-jit-97588-7392d01b93d0-win32\pypy3.exe c:\vp\pypy3 diff --git a/winbuild/config.py b/winbuild/config.py index 0eccdc1f0..38532a62e 100644 --- a/winbuild/config.py +++ b/winbuild/config.py @@ -9,6 +9,7 @@ pythons = { "pypy2": {"compiler": 7, "vc": 2010}, "35": {"compiler": 7.1, "vc": 2015}, "36": {"compiler": 7.1, "vc": 2015}, + "pypy3": {"compiler": 7.1, "vc": 2015}, "37": {"compiler": 7.1, "vc": 2015}, # for GitHub Actions "3.5": {"compiler": 7.1, "vc": 2015},