diff --git a/.travis.yml b/.travis.yml index 34f37b611..20d1ef5bd 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,22 +9,21 @@ notifications: matrix: fast_finish: true - allow_failures: - - python: nightly include: - python: "pypy" - python: "pypy3" - python: '3.6' - python: '2.7' - env: DOCKER="alpine" + - env: DOCKER="arch" # contains PyQt5 - env: DOCKER="ubuntu-trusty-x86" - env: DOCKER="ubuntu-xenial-amd64" - - env: DOCKER="ubuntu-precise-amd64" + - env: DOCKER="ubuntu-precise-amd64" + - env: DOCKER="debian-stretch-x86" - python: "2.7_with_system_site_packages" # For PyQt4 - python: '3.5' - python: '3.4' - python: '3.3' - - python: 'nightly' dist: trusty diff --git a/CHANGES.rst b/CHANGES.rst index d72c5d79d..ab0ce5dc0 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -4,6 +4,33 @@ Changelog (Pillow) 4.1.0 (unreleased) ------------------ +- Removed use of spaces in TIFF kwargs names, deprecated in 2.7 #1390 + [radarhere] + +- Removed deprecated ImageDraw setink, setfill, setfont methods #2220 + [jdufresne] + +- Send unwanted subprocess output to /dev/null #2253 + [jdufresne] + +- Fix division by zero when creating 0x0 image from numpy array #2419 + [hugovk] + +- Test: Added matrix convert tests #2381 + [hugovk] + +- Replaced broken URL to partners.adobe.com #2413 + [radarhere] + +- Removed unused private functions in setup.py and build_dep.py #2414 + [radarhere] + +- Test: Fixed Qt tests for QT5, Arch, and saving 1 bit PNG #2394 + [wiredfool] + +- Test: docker builds for Arch and Debian Stretch #2394 + [wiredfool] + - Updated libwebp to 0.6.0 on appveyor #2395 [radarhere] diff --git a/PIL/EpsImagePlugin.py b/PIL/EpsImagePlugin.py index 8dd3e6857..56b115f30 100644 --- a/PIL/EpsImagePlugin.py +++ b/PIL/EpsImagePlugin.py @@ -22,6 +22,7 @@ import re import io +import os import sys from . import Image, ImageFile from ._binary import i32le as i32, o32le as o32 @@ -57,8 +58,8 @@ def has_ghostscript(): if not sys.platform.startswith('win'): import subprocess try: - gs = subprocess.Popen(['gs', '--version'], stdout=subprocess.PIPE) - gs.stdout.read() + with open(os.devnull, 'wb') as devnull: + subprocess.check_call(['gs', '--version'], stdout=devnull) return True except OSError: # no ghostscript @@ -137,12 +138,8 @@ def Ghostscript(tile, size, fp, scale=1): # push data through ghostscript try: - gs = subprocess.Popen(command, stdin=subprocess.PIPE, - stdout=subprocess.PIPE) - gs.stdin.close() - status = gs.wait() - if status: - raise IOError("gs failed (status %d)" % status) + with open(os.devnull, 'w+b') as devnull: + subprocess.check_call(command, stdin=devnull, stdout=devnull) im = Image.open(outfile) im.load() finally: @@ -321,7 +318,7 @@ class EpsImageFile(ImageFile.ImageFile): # EPS can contain binary data # or start directly with latin coding # more info see: - # http://partners.adobe.com/public/developer/en/ps/5002.EPSF_Spec.pdf + # https://web.archive.org/web/20160528181353/http://partners.adobe.com/public/developer/en/ps/5002.EPSF_Spec.pdf offset = i32(s[4:8]) length = i32(s[8:12]) else: diff --git a/PIL/GifImagePlugin.py b/PIL/GifImagePlugin.py index 2e519c7ac..bdda5c185 100644 --- a/PIL/GifImagePlugin.py +++ b/PIL/GifImagePlugin.py @@ -519,18 +519,17 @@ def _save_netpbm(im, fp, filename): with open(filename, 'wb') as f: if im.mode != "RGB": - with tempfile.TemporaryFile() as stderr: - check_call(["ppmtogif", file], stdout=f, stderr=stderr) + with open(os.devnull, 'wb') as devnull: + check_call(["ppmtogif", file], stdout=f, stderr=devnull) else: # Pipe ppmquant output into ppmtogif # "ppmquant 256 %s | ppmtogif > %s" % (file, filename) quant_cmd = ["ppmquant", "256", file] togif_cmd = ["ppmtogif"] - with tempfile.TemporaryFile() as stderr: - quant_proc = Popen(quant_cmd, stdout=PIPE, stderr=stderr) - with tempfile.TemporaryFile() as stderr: + with open(os.devnull, 'wb') as devnull: + quant_proc = Popen(quant_cmd, stdout=PIPE, stderr=devnull) togif_proc = Popen(togif_cmd, stdin=quant_proc.stdout, - stdout=f, stderr=stderr) + stdout=f, stderr=devnull) # Allow ppmquant to receive SIGPIPE if ppmtogif exits quant_proc.stdout.close() diff --git a/PIL/IcnsImagePlugin.py b/PIL/IcnsImagePlugin.py index cb215fe3e..5c5bd7cf9 100644 --- a/PIL/IcnsImagePlugin.py +++ b/PIL/IcnsImagePlugin.py @@ -329,8 +329,8 @@ def _save(im, fp, filename): from subprocess import Popen, PIPE, CalledProcessError convert_cmd = ["iconutil", "-c", "icns", "-o", filename, iconset] - with tempfile.TemporaryFile() as stderr: - convert_proc = Popen(convert_cmd, stdout=PIPE, stderr=stderr) + with open(os.devnull, 'wb') as devnull: + convert_proc = Popen(convert_cmd, stdout=PIPE, stderr=devnull) convert_proc.stdout.close() diff --git a/PIL/Image.py b/PIL/Image.py index c643e24d9..77f2836c0 100644 --- a/PIL/Image.py +++ b/PIL/Image.py @@ -2036,7 +2036,7 @@ def _check_size(size): if len(size) != 2: raise ValueError("Size must be a tuple of length 2") if size[0] < 0 or size[1] < 0: - raise ValueError("Width and Height must be => 0") + raise ValueError("Width and height must be >= 0") return True diff --git a/PIL/ImageDraw.py b/PIL/ImageDraw.py index 6b72fcab7..838983652 100644 --- a/PIL/ImageDraw.py +++ b/PIL/ImageDraw.py @@ -31,7 +31,6 @@ # import numbers -import warnings from . import Image, ImageColor from ._util import isStringType @@ -87,20 +86,6 @@ class ImageDraw(object): self.fill = 0 self.font = None - def setink(self, ink): - raise NotImplementedError("setink() has been removed. " + - "Please use keyword arguments instead.") - - def setfill(self, onoff): - raise NotImplementedError("setfill() has been removed. " + - "Please use keyword arguments instead.") - - def setfont(self, font): - warnings.warn("setfont() is deprecated. " + - "Please set the attribute directly instead.") - # compatibility - self.font = font - def getfont(self): """Get the current default font.""" if not self.font: diff --git a/PIL/JpegPresets.py b/PIL/JpegPresets.py index ece33bbbe..6fda20aec 100644 --- a/PIL/JpegPresets.py +++ b/PIL/JpegPresets.py @@ -62,7 +62,7 @@ The tables format between im.quantization and quantization in presets differ in You can convert the dict format to the preset format with the `JpegImagePlugin.convert_dict_qtables(dict_qtables)` function. -Libjpeg ref.: http://web.archive.org/web/20120328125543/http://www.jpegcameras.com/libjpeg/libjpeg-3.html +Libjpeg ref.: https://web.archive.org/web/20120328125543/http://www.jpegcameras.com/libjpeg/libjpeg-3.html """ diff --git a/PIL/TiffImagePlugin.py b/PIL/TiffImagePlugin.py index 505025bb8..9dbd2d379 100644 --- a/PIL/TiffImagePlugin.py +++ b/PIL/TiffImagePlugin.py @@ -1377,11 +1377,6 @@ def _save(im, fp, filename): (DATE_TIME, "date_time"), (ARTIST, "artist"), (COPYRIGHT, "copyright")]: - name_with_spaces = name.replace("_", " ") - if "_" in name and name_with_spaces in im.encoderinfo: - warnings.warn("%r is deprecated; use %r instead" % - (name_with_spaces, name), DeprecationWarning) - ifd[key] = im.encoderinfo[name.replace("_", " ")] if name in im.encoderinfo: ifd[key] = im.encoderinfo[name] diff --git a/Tests/helper.py b/Tests/helper.py index f8eed3e2d..cbfad6f03 100644 --- a/Tests/helper.py +++ b/Tests/helper.py @@ -218,25 +218,25 @@ def command_succeeds(cmd): command succeeds, or False if an OSError was raised by subprocess.Popen. """ import subprocess - with open(os.devnull, 'w') as f: + with open(os.devnull, 'wb') as f: try: - subprocess.Popen(cmd, stdout=f, stderr=subprocess.STDOUT).wait() + subprocess.call(cmd, stdout=f, stderr=subprocess.STDOUT) except OSError: return False return True def djpeg_available(): - return command_succeeds(['djpeg', '--help']) + return command_succeeds(['djpeg', '-version']) def cjpeg_available(): - return command_succeeds(['cjpeg', '--help']) + return command_succeeds(['cjpeg', '-version']) def netpbm_available(): - return (command_succeeds(["ppmquant", "--help"]) and - command_succeeds(["ppmtogif", "--help"])) + return (command_succeeds(["ppmquant", "--version"]) and + command_succeeds(["ppmtogif", "--version"])) def imagemagick_available(): @@ -253,6 +253,12 @@ if sys.platform == 'win32': else: IMCONVERT = 'convert' +def distro(): + if os.path.exists('/etc/os-release'): + with open('/etc/os-release', 'r') as f: + for line in f: + if 'ID=' in line: + return line.strip().split('=')[1] class cached_property(object): def __init__(self, func): diff --git a/Tests/images/hopper-XYZ.png b/Tests/images/hopper-XYZ.png new file mode 100644 index 000000000..194d24540 Binary files /dev/null and b/Tests/images/hopper-XYZ.png differ diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py index 1fe3ad45e..cc37c9504 100644 --- a/Tests/test_file_tiff.py +++ b/Tests/test_file_tiff.py @@ -417,25 +417,6 @@ class TestFileTiff(PillowTestCase): self.assertEqual(im.tag_v2[X_RESOLUTION], 72) self.assertEqual(im.tag_v2[Y_RESOLUTION], 36) - def test_deprecation_warning_with_spaces(self): - kwargs = {'resolution unit': 'inch', - 'x resolution': 36, - 'y resolution': 72} - filename = self.tempfile("temp.tif") - self.assert_warning(DeprecationWarning, - lambda: hopper("RGB").save(filename, **kwargs)) - from PIL.TiffImagePlugin import X_RESOLUTION, Y_RESOLUTION - - im = Image.open(filename) - - # legacy interface - self.assertEqual(im.tag[X_RESOLUTION][0][0], 36) - self.assertEqual(im.tag[Y_RESOLUTION][0][0], 72) - - # v2 interface - self.assertEqual(im.tag_v2[X_RESOLUTION], 36) - self.assertEqual(im.tag_v2[Y_RESOLUTION], 72) - def test_lzw(self): # Act im = Image.open("Tests/images/hopper_lzw.tif") @@ -480,12 +461,12 @@ class TestFileTiff(PillowTestCase): # however does. im = Image.new('RGB', (1, 1)) im.info['icc_profile'] = 'Dummy value' - + # Try save-load round trip to make sure both handle icc_profile. tmpfile = self.tempfile('temp.tif') im.save(tmpfile, 'TIFF', compression='raw') reloaded = Image.open(tmpfile) - + self.assertEqual(b'Dummy value', reloaded.info['icc_profile']) diff --git a/Tests/test_image_convert.py b/Tests/test_image_convert.py index 54ffde10b..e641a99f2 100644 --- a/Tests/test_image_convert.py +++ b/Tests/test_image_convert.py @@ -20,10 +20,10 @@ class TestImageConvert(PillowTestCase): convert(im, mode) # Check 0 - im = Image.new(mode, (0,0)) + im = Image.new(mode, (0, 0)) for mode in modes: convert(im, mode) - + def test_default(self): im = hopper("P") @@ -137,6 +137,77 @@ class TestImageConvert(PillowTestCase): self.assert_image_similar(alpha, comparable, 5) + def test_matrix_illegal_conversion(self): + # Arrange + im = hopper('CMYK') + matrix = ( + 0.412453, 0.357580, 0.180423, 0, + 0.212671, 0.715160, 0.072169, 0, + 0.019334, 0.119193, 0.950227, 0) + self.assertNotEqual(im.mode, 'RGB') + + # Act / Assert + self.assertRaises(ValueError, + lambda: im.convert(mode='CMYK', matrix=matrix)) + + def test_matrix_wrong_mode(self): + # Arrange + im = hopper('L') + matrix = ( + 0.412453, 0.357580, 0.180423, 0, + 0.212671, 0.715160, 0.072169, 0, + 0.019334, 0.119193, 0.950227, 0) + self.assertEqual(im.mode, 'L') + + # Act / Assert + self.assertRaises(ValueError, + lambda: im.convert(mode='L', matrix=matrix)) + + def test_matrix_xyz(self): + + def matrix_convert(mode): + # Arrange + im = hopper('RGB') + matrix = ( + 0.412453, 0.357580, 0.180423, 0, + 0.212671, 0.715160, 0.072169, 0, + 0.019334, 0.119193, 0.950227, 0) + self.assertEqual(im.mode, 'RGB') + + # Act + # Convert an RGB image to the CIE XYZ colour space + converted_im = im.convert(mode=mode, matrix=matrix) + + # Assert + self.assertEqual(converted_im.mode, mode) + self.assertEqual(converted_im.size, im.size) + target = Image.open('Tests/images/hopper-XYZ.png') + if converted_im.mode == 'RGB': + self.assert_image_similar(converted_im, target, 3) + else: + self.assert_image_similar(converted_im, target.split()[0], 1) + + + matrix_convert('RGB') + matrix_convert('L') + + def test_matrix_identity(self): + # Arrange + im = hopper('RGB') + identity_matrix = ( + 1, 0, 0, 0, + 0, 1, 0, 0, + 0, 0, 1, 0) + self.assertEqual(im.mode, 'RGB') + + # Act + # Convert with an identity matrix + converted_im = im.convert(mode='RGB', matrix=identity_matrix) + + # Assert + # No change + self.assert_image_equal(converted_im, im) + if __name__ == '__main__': unittest.main() diff --git a/Tests/test_image_fromqpixmap.py b/Tests/test_image_fromqpixmap.py index 543b74bbf..cf76cffca 100644 --- a/Tests/test_image_fromqpixmap.py +++ b/Tests/test_image_fromqpixmap.py @@ -1,9 +1,10 @@ -from helper import unittest, PillowTestCase, hopper +from helper import unittest, PillowTestCase, hopper, distro from test_imageqt import PillowQtTestCase, PillowQPixmapTestCase from PIL import ImageQt - +@unittest.skipIf(ImageQt.qt_version == '5' and distro() == 'arch', + "Topixmap fails on Arch + QT5") class TestFromQPixmap(PillowQPixmapTestCase, PillowTestCase): def roundtrip(self, expected): diff --git a/Tests/test_image_toqimage.py b/Tests/test_image_toqimage.py index 39e68ebbb..f6d13824f 100644 --- a/Tests/test_image_toqimage.py +++ b/Tests/test_image_toqimage.py @@ -1,7 +1,7 @@ from helper import unittest, PillowTestCase, hopper from test_imageqt import PillowQtTestCase -from PIL import ImageQt +from PIL import ImageQt, Image if ImageQt.qt_is_installed: @@ -9,38 +9,66 @@ 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 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 class TestToQImage(PillowQtTestCase, PillowTestCase): def test_sanity(self): PillowQtTestCase.setUp(self) - for mode in ('1', 'RGB', 'RGBA', 'L', 'P'): - data = ImageQt.toqimage(hopper(mode)) + for mode in ('RGB', 'RGBA', 'L', 'P', '1'): + src = hopper(mode) + data = ImageQt.toqimage(src) self.assertIsInstance(data, QImage) self.assertFalse(data.isNull()) + # reload directly from the qimage + rt = ImageQt.fromqimage(data) + if mode in ('L', 'P', '1'): + self.assert_image_equal(rt, src.convert('RGB')) + else: + self.assert_image_equal(rt, src) + + if mode == '1': + # BW appears to not save correctly on QT4 and QT5 + # kicks out errors on console: + # libpng warning: Invalid color type/bit depth combination in IHDR + # libpng error: Invalid IHDR data + continue + # Test saving the file tempfile = self.tempfile('temp_{}.png'.format(mode)) data.save(tempfile) + # 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): PillowQtTestCase.setUp(self) - app = QtGui.QApplication([]) + app = QApplication([]) ex = Example() assert(app) # Silence warning assert(ex) # Silence warning if ImageQt.qt_is_installed: - class Example(QtGui.QWidget): + class Example(QWidget): def __init__(self): super(Example, self).__init__() @@ -51,9 +79,9 @@ if ImageQt.qt_is_installed: pixmap1 = QtGui.QPixmap.fromImage(qimage) - hbox = QtGui.QHBoxLayout(self) + hbox = QHBoxLayout(self) - lbl = QtGui.QLabel(self) + lbl = QLabel(self) # Segfault in the problem lbl.setPixmap(pixmap1.copy()) diff --git a/Tests/test_image_toqpixmap.py b/Tests/test_image_toqpixmap.py index c6555d7ff..a48e278ad 100644 --- a/Tests/test_image_toqpixmap.py +++ b/Tests/test_image_toqpixmap.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase, hopper +from helper import unittest, PillowTestCase, hopper, distro from test_imageqt import PillowQtTestCase, PillowQPixmapTestCase from PIL import ImageQt @@ -9,6 +9,8 @@ if ImageQt.qt_is_installed: class TestToQPixmap(PillowQPixmapTestCase, PillowTestCase): + @unittest.skipIf(ImageQt.qt_version == '5' and distro() == 'arch', + "Topixmap fails on Arch + QT5") def test_sanity(self): PillowQtTestCase.setUp(self) diff --git a/Tests/test_imagedraw.py b/Tests/test_imagedraw.py index cd6519709..187a1fdcb 100644 --- a/Tests/test_imagedraw.py +++ b/Tests/test_imagedraw.py @@ -44,14 +44,6 @@ class TestImageDraw(PillowTestCase): draw.polygon(list(range(100))) draw.rectangle(list(range(4))) - def test_removed_methods(self): - im = hopper() - - draw = ImageDraw.Draw(im) - - self.assertRaises(Exception, lambda: draw.setink(0)) - self.assertRaises(Exception, lambda: draw.setfill(0)) - def test_valueerror(self): im = Image.open("Tests/images/chi.gif") diff --git a/Tests/test_numpy.py b/Tests/test_numpy.py index 01b02c9e5..8b9208cd6 100644 --- a/Tests/test_numpy.py +++ b/Tests/test_numpy.py @@ -204,6 +204,14 @@ class TestNumpy(PillowTestCase): self.assertEqual(len(im.getdata()), len(arr)) + def test_zero_size(self): + # Shouldn't cause floating point exception + # See https://github.com/python-pillow/Pillow/issues/2259 + + im = Image.fromarray(numpy.empty((0, 0), dtype=numpy.uint8)) + + self.assertEqual(im.size, (0, 0)) + if __name__ == '__main__': unittest.main() diff --git a/map.c b/map.c index 75f463440..9d4751e31 100644 --- a/map.c +++ b/map.c @@ -342,7 +342,7 @@ PyImaging_MapBuffer(PyObject* self, PyObject* args) stride = xsize * 4; } - if (ysize > INT_MAX / stride) { + if (stride > 0 && ysize > INT_MAX / stride) { PyErr_SetString(PyExc_MemoryError, "Integer overflow in ysize"); return NULL; } @@ -352,7 +352,7 @@ PyImaging_MapBuffer(PyObject* self, PyObject* args) if (offset > PY_SSIZE_T_MAX - size) { PyErr_SetString(PyExc_MemoryError, "Integer overflow in offset"); return NULL; - } + } /* check buffer size */ if (PyImaging_GetBuffer(target, &view) < 0) diff --git a/setup.py b/setup.py index b9ba1301c..1dc146d26 100755 --- a/setup.py +++ b/setup.py @@ -84,10 +84,6 @@ def _find_library_file(self, library): return ret -def _lib_include(root): - # map root to (root/lib, root/include) - return os.path.join(root, "lib"), os.path.join(root, "include") - def _cmd_exists(cmd): return any( os.access(os.path.join(path, cmd), os.X_OK) diff --git a/winbuild/build_dep.py b/winbuild/build_dep.py index fb4d55d8c..c932a2fcb 100644 --- a/winbuild/build_dep.py +++ b/winbuild/build_dep.py @@ -10,10 +10,6 @@ def _relpath(*args): return os.path.join(os.getcwd(), *args) -def _relbuild(*args): - return _relpath('build', *args) - - build_dir = _relpath('build') inc_dir = _relpath('depends')