Merge pull request #4486 from radarhere/pillowtestcase

Removed PillowTestCase helper class
This commit is contained in:
Hugo van Kemenade 2020-03-29 19:14:04 +03:00 committed by GitHub
commit dcadfc6412
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 721 additions and 745 deletions

View File

@ -1,9 +1,8 @@
import time import time
import unittest
from PIL import PyAccess from PIL import PyAccess
from .helper import PillowTestCase, hopper from .helper import hopper
# Not running this test by default. No DOS against Travis CI. # Not running this test by default. No DOS against Travis CI.
@ -41,22 +40,17 @@ def timer(func, label, *args):
) )
class BenchCffiAccess(PillowTestCase): def test_direct():
def test_direct(self): im = hopper()
im = hopper() im.load()
im.load() # im = Image.new( "RGB", (2000, 2000), (1, 3, 2))
# im = Image.new( "RGB", (2000, 2000), (1, 3, 2)) caccess = im.im.pixel_access(False)
caccess = im.im.pixel_access(False) access = PyAccess.new(im, False)
access = PyAccess.new(im, False)
assert caccess[(0, 0)] == access[(0, 0)] assert caccess[(0, 0)] == access[(0, 0)]
print("Size: %sx%s" % im.size) print("Size: %sx%s" % im.size)
timer(iterate_get, "PyAccess - get", im.size, access) timer(iterate_get, "PyAccess - get", im.size, access)
timer(iterate_set, "PyAccess - set", im.size, access) timer(iterate_set, "PyAccess - set", im.size, access)
timer(iterate_get, "C-api - get", im.size, caccess) timer(iterate_get, "C-api - get", im.size, caccess)
timer(iterate_set, "C-api - set", im.size, caccess) timer(iterate_set, "C-api - set", im.size, caccess)
if __name__ == "__main__":
unittest.main()

View File

@ -1,19 +1,10 @@
import unittest
from PIL import Image from PIL import Image
from .helper import PillowTestCase
TEST_FILE = "Tests/images/fli_overflow.fli" TEST_FILE = "Tests/images/fli_overflow.fli"
class TestFliOverflow(PillowTestCase): def test_fli_overflow():
def test_fli_overflow(self):
# this should not crash with a malloc error or access violation # this should not crash with a malloc error or access violation
with Image.open(TEST_FILE) as im: with Image.open(TEST_FILE) as im:
im.load() im.load()
if __name__ == "__main__":
unittest.main()

View File

@ -1,46 +1,44 @@
#!/usr/bin/env python #!/usr/bin/env python
import unittest import pytest
from PIL import Image from PIL import Image
from .helper import PillowTestCase, is_win32 from .helper import is_win32
min_iterations = 100 min_iterations = 100
max_iterations = 10000 max_iterations = 10000
pytestmark = pytest.mark.skipif(is_win32(), reason="requires Unix or macOS")
@unittest.skipIf(is_win32(), "requires Unix or macOS")
class TestImagingLeaks(PillowTestCase):
def _get_mem_usage(self):
from resource import getpagesize, getrusage, RUSAGE_SELF
mem = getrusage(RUSAGE_SELF).ru_maxrss
return mem * getpagesize() / 1024 / 1024
def _test_leak(self, min_iterations, max_iterations, fn, *args, **kwargs):
mem_limit = None
for i in range(max_iterations):
fn(*args, **kwargs)
mem = self._get_mem_usage()
if i < min_iterations:
mem_limit = mem + 1
continue
msg = "memory usage limit exceeded after %d iterations" % (i + 1)
assert mem <= mem_limit, msg
def test_leak_putdata(self):
im = Image.new("RGB", (25, 25))
self._test_leak(min_iterations, max_iterations, im.putdata, im.getdata())
def test_leak_getlist(self):
im = Image.new("P", (25, 25))
self._test_leak(
min_iterations,
max_iterations,
# Pass a new list at each iteration.
lambda: im.point(range(256)),
)
if __name__ == "__main__": def _get_mem_usage():
unittest.main() from resource import getpagesize, getrusage, RUSAGE_SELF
mem = getrusage(RUSAGE_SELF).ru_maxrss
return mem * getpagesize() / 1024 / 1024
def _test_leak(min_iterations, max_iterations, fn, *args, **kwargs):
mem_limit = None
for i in range(max_iterations):
fn(*args, **kwargs)
mem = _get_mem_usage()
if i < min_iterations:
mem_limit = mem + 1
continue
msg = "memory usage limit exceeded after %d iterations" % (i + 1)
assert mem <= mem_limit, msg
def test_leak_putdata():
im = Image.new("RGB", (25, 25))
_test_leak(min_iterations, max_iterations, im.putdata, im.getdata())
def test_leak_getlist():
im = Image.new("P", (25, 25))
_test_leak(
min_iterations,
max_iterations,
# Pass a new list at each iteration.
lambda: im.point(range(256)),
)

View File

@ -1,9 +1,9 @@
import unittest
from io import BytesIO from io import BytesIO
import pytest
from PIL import Image from PIL import Image
from .helper import PillowTestCase, is_win32, skip_unless_feature from .helper import is_win32, skip_unless_feature
# Limits for testing the leak # Limits for testing the leak
mem_limit = 1024 * 1048576 mem_limit = 1024 * 1048576
@ -11,32 +11,31 @@ stack_size = 8 * 1048576
iterations = int((mem_limit / stack_size) * 2) iterations = int((mem_limit / stack_size) * 2)
test_file = "Tests/images/rgb_trns_ycbc.jp2" test_file = "Tests/images/rgb_trns_ycbc.jp2"
pytestmark = [
@unittest.skipIf(is_win32(), "requires Unix or macOS") pytest.mark.skipif(is_win32(), reason="requires Unix or macOS"),
@skip_unless_feature("jpg_2000") skip_unless_feature("jpg_2000"),
class TestJpegLeaks(PillowTestCase): ]
def test_leak_load(self):
from resource import setrlimit, RLIMIT_AS, RLIMIT_STACK
setrlimit(RLIMIT_STACK, (stack_size, stack_size))
setrlimit(RLIMIT_AS, (mem_limit, mem_limit))
for _ in range(iterations):
with Image.open(test_file) as im:
im.load()
def test_leak_save(self):
from resource import setrlimit, RLIMIT_AS, RLIMIT_STACK
setrlimit(RLIMIT_STACK, (stack_size, stack_size))
setrlimit(RLIMIT_AS, (mem_limit, mem_limit))
for _ in range(iterations):
with Image.open(test_file) as im:
im.load()
test_output = BytesIO()
im.save(test_output, "JPEG2000")
test_output.seek(0)
test_output.read()
if __name__ == "__main__": def test_leak_load():
unittest.main() from resource import setrlimit, RLIMIT_AS, RLIMIT_STACK
setrlimit(RLIMIT_STACK, (stack_size, stack_size))
setrlimit(RLIMIT_AS, (mem_limit, mem_limit))
for _ in range(iterations):
with Image.open(test_file) as im:
im.load()
def test_leak_save():
from resource import setrlimit, RLIMIT_AS, RLIMIT_STACK
setrlimit(RLIMIT_STACK, (stack_size, stack_size))
setrlimit(RLIMIT_AS, (mem_limit, mem_limit))
for _ in range(iterations):
with Image.open(test_file) as im:
im.load()
test_output = BytesIO()
im.save(test_output, "JPEG2000")
test_output.seek(0)
test_output.read()

View File

@ -1,18 +1,9 @@
import unittest import pytest
from PIL import Image from PIL import Image
from .helper import PillowTestCase
def test_j2k_overflow(tmp_path):
class TestJ2kEncodeOverflow(PillowTestCase): im = Image.new("RGBA", (1024, 131584))
def test_j2k_overflow(self): target = str(tmp_path / "temp.jpc")
with pytest.raises(IOError):
im = Image.new("RGBA", (1024, 131584)) im.save(target)
target = self.tempfile("temp.jpc")
with self.assertRaises(IOError):
im.save(target)
if __name__ == "__main__":
unittest.main()

View File

@ -1,7 +1,8 @@
import unittest
from io import BytesIO from io import BytesIO
from .helper import PillowTestCase, hopper, is_win32 import pytest
from .helper import hopper, is_win32
iterations = 5000 iterations = 5000
@ -15,10 +16,9 @@ valgrind --tool=massif python test-installed.py -s -v Tests/check_jpeg_leaks.py
""" """
@unittest.skipIf(is_win32(), "requires Unix or macOS") pytestmark = pytest.mark.skipif(is_win32(), reason="requires Unix or macOS")
class TestJpegLeaks(PillowTestCase):
""" """
pre patch: pre patch:
MB MB
@ -74,49 +74,51 @@ post-patch:
""" """
def test_qtables_leak(self):
im = hopper("RGB")
standard_l_qtable = [ def test_qtables_leak():
int(s) im = hopper("RGB")
for s in """
16 11 10 16 24 40 51 61
12 12 14 19 26 58 60 55
14 13 16 24 40 57 69 56
14 17 22 29 51 87 80 62
18 22 37 56 68 109 103 77
24 35 55 64 81 104 113 92
49 64 78 87 103 121 120 101
72 92 95 98 112 100 103 99
""".split(
None
)
]
standard_chrominance_qtable = [ standard_l_qtable = [
int(s) int(s)
for s in """ for s in """
17 18 24 47 99 99 99 99 16 11 10 16 24 40 51 61
18 21 26 66 99 99 99 99 12 12 14 19 26 58 60 55
24 26 56 99 99 99 99 99 14 13 16 24 40 57 69 56
47 66 99 99 99 99 99 99 14 17 22 29 51 87 80 62
99 99 99 99 99 99 99 99 18 22 37 56 68 109 103 77
99 99 99 99 99 99 99 99 24 35 55 64 81 104 113 92
99 99 99 99 99 99 99 99 49 64 78 87 103 121 120 101
99 99 99 99 99 99 99 99 72 92 95 98 112 100 103 99
""".split( """.split(
None None
) )
] ]
qtables = [standard_l_qtable, standard_chrominance_qtable] standard_chrominance_qtable = [
int(s)
for s in """
17 18 24 47 99 99 99 99
18 21 26 66 99 99 99 99
24 26 56 99 99 99 99 99
47 66 99 99 99 99 99 99
99 99 99 99 99 99 99 99
99 99 99 99 99 99 99 99
99 99 99 99 99 99 99 99
99 99 99 99 99 99 99 99
""".split(
None
)
]
for _ in range(iterations): qtables = [standard_l_qtable, standard_chrominance_qtable]
test_output = BytesIO()
im.save(test_output, "JPEG", qtables=qtables)
def test_exif_leak(self): for _ in range(iterations):
""" test_output = BytesIO()
im.save(test_output, "JPEG", qtables=qtables)
def test_exif_leak():
"""
pre patch: pre patch:
MB MB
@ -171,15 +173,16 @@ post patch:
0 11.33 0 11.33
""" """
im = hopper("RGB") im = hopper("RGB")
exif = b"12345678" * 4096 exif = b"12345678" * 4096
for _ in range(iterations): for _ in range(iterations):
test_output = BytesIO() test_output = BytesIO()
im.save(test_output, "JPEG", exif=exif) im.save(test_output, "JPEG", exif=exif)
def test_base_save(self):
""" def test_base_save():
"""
base case: base case:
MB MB
20.99^ ::::: :::::::::::::::::::::::::::::::::::::::::::@::: 20.99^ ::::: :::::::::::::::::::::::::::::::::::::::::::@:::
@ -205,12 +208,8 @@ base case:
0 +----------------------------------------------------------------------->Gi 0 +----------------------------------------------------------------------->Gi
0 7.882 0 7.882
""" """
im = hopper("RGB") im = hopper("RGB")
for _ in range(iterations): for _ in range(iterations):
test_output = BytesIO() test_output = BytesIO()
im.save(test_output, "JPEG") im.save(test_output, "JPEG")
if __name__ == "__main__":
unittest.main()

View File

@ -1,10 +1,8 @@
import sys import sys
import unittest
import pytest
from PIL import Image from PIL import Image
from .helper import PillowTestCase
# This test is not run automatically. # This test is not run automatically.
# #
# It requires > 2gb memory for the >2 gigapixel image generated in the # It requires > 2gb memory for the >2 gigapixel image generated in the
@ -24,26 +22,26 @@ YDIM = 32769
XDIM = 48000 XDIM = 48000
@unittest.skipIf(sys.maxsize <= 2 ** 32, "requires 64-bit system") pytestmark = pytest.mark.skipif(sys.maxsize <= 2 ** 32, reason="requires 64-bit system")
class LargeMemoryTest(PillowTestCase):
def _write_png(self, xdim, ydim):
f = self.tempfile("temp.png")
im = Image.new("L", (xdim, ydim), 0)
im.save(f)
def test_large(self):
""" succeeded prepatch"""
self._write_png(XDIM, YDIM)
def test_2gpx(self):
"""failed prepatch"""
self._write_png(XDIM, XDIM)
@unittest.skipIf(numpy is None, "Numpy is not installed")
def test_size_greater_than_int(self):
arr = numpy.ndarray(shape=(16394, 16394))
Image.fromarray(arr)
if __name__ == "__main__": def _write_png(tmp_path, xdim, ydim):
unittest.main() f = str(tmp_path / "temp.png")
im = Image.new("L", (xdim, ydim), 0)
im.save(f)
def test_large(tmp_path):
""" succeeded prepatch"""
_write_png(tmp_path, XDIM, YDIM)
def test_2gpx(tmp_path):
"""failed prepatch"""
_write_png(tmp_path, XDIM, XDIM)
@pytest.mark.skipif(numpy is None, reason="Numpy is not installed")
def test_size_greater_than_int():
arr = numpy.ndarray(shape=(16394, 16394))
Image.fromarray(arr)

View File

@ -1,10 +1,8 @@
import sys import sys
import unittest
import pytest
from PIL import Image from PIL import Image
from .helper import PillowTestCase
# This test is not run automatically. # This test is not run automatically.
# #
# It requires > 2gb memory for the >2 gigapixel image generated in the # It requires > 2gb memory for the >2 gigapixel image generated in the
@ -14,32 +12,28 @@ from .helper import PillowTestCase
# Raspberry Pis). # Raspberry Pis).
try: np = pytest.importorskip("numpy", reason="NumPy not installed")
import numpy as np
except ImportError:
raise unittest.SkipTest("numpy not installed")
YDIM = 32769 YDIM = 32769
XDIM = 48000 XDIM = 48000
@unittest.skipIf(sys.maxsize <= 2 ** 32, "requires 64-bit system") pytestmark = pytest.mark.skipif(sys.maxsize <= 2 ** 32, reason="requires 64-bit system")
class LargeMemoryNumpyTest(PillowTestCase):
def _write_png(self, xdim, ydim):
dtype = np.uint8
a = np.zeros((xdim, ydim), dtype=dtype)
f = self.tempfile("temp.png")
im = Image.fromarray(a, "L")
im.save(f)
def test_large(self):
""" succeeded prepatch"""
self._write_png(XDIM, YDIM)
def test_2gpx(self):
"""failed prepatch"""
self._write_png(XDIM, XDIM)
if __name__ == "__main__": def _write_png(tmp_path, xdim, ydim):
unittest.main() dtype = np.uint8
a = np.zeros((xdim, ydim), dtype=dtype)
f = str(tmp_path / "temp.png")
im = Image.fromarray(a, "L")
im.save(f)
def test_large(tmp_path):
""" succeeded prepatch"""
_write_png(tmp_path, XDIM, YDIM)
def test_2gpx(tmp_path):
"""failed prepatch"""
_write_png(tmp_path, XDIM, XDIM)

View File

@ -1,23 +1,14 @@
import unittest
import pytest import pytest
from PIL import Image from PIL import Image
from .helper import PillowTestCase
TEST_FILE = "Tests/images/libtiff_segfault.tif" TEST_FILE = "Tests/images/libtiff_segfault.tif"
class TestLibtiffSegfault(PillowTestCase): def test_libtiff_segfault():
def test_segfault(self): """ This test should not segfault. It will on Pillow <= 3.1.0 and
""" This test should not segfault. It will on Pillow <= 3.1.0 and libtiff >= 4.0.0
libtiff >= 4.0.0 """
"""
with pytest.raises(IOError): with pytest.raises(IOError):
with Image.open(TEST_FILE) as im: with Image.open(TEST_FILE) as im:
im.load() im.load()
if __name__ == "__main__":
unittest.main()

View File

@ -1,67 +1,61 @@
import unittest
import zlib import zlib
from io import BytesIO from io import BytesIO
from PIL import Image, ImageFile, PngImagePlugin from PIL import Image, ImageFile, PngImagePlugin
from .helper import PillowTestCase
TEST_FILE = "Tests/images/png_decompression_dos.png" TEST_FILE = "Tests/images/png_decompression_dos.png"
class TestPngDos(PillowTestCase): def test_ignore_dos_text():
def test_ignore_dos_text(self): ImageFile.LOAD_TRUNCATED_IMAGES = True
ImageFile.LOAD_TRUNCATED_IMAGES = True
try: try:
im = Image.open(TEST_FILE) im = Image.open(TEST_FILE)
im.load() im.load()
finally: finally:
ImageFile.LOAD_TRUNCATED_IMAGES = False ImageFile.LOAD_TRUNCATED_IMAGES = False
for s in im.text.values(): for s in im.text.values():
assert len(s) < 1024 * 1024, "Text chunk larger than 1M" assert len(s) < 1024 * 1024, "Text chunk larger than 1M"
for s in im.info.values(): for s in im.info.values():
assert len(s) < 1024 * 1024, "Text chunk larger than 1M" assert len(s) < 1024 * 1024, "Text chunk larger than 1M"
def test_dos_text(self):
try:
im = Image.open(TEST_FILE)
im.load()
except ValueError as msg:
assert msg, "Decompressed Data Too Large"
return
for s in im.text.values():
assert len(s) < 1024 * 1024, "Text chunk larger than 1M"
def test_dos_total_memory(self):
im = Image.new("L", (1, 1))
compressed_data = zlib.compress(b"a" * 1024 * 1023)
info = PngImagePlugin.PngInfo()
for x in range(64):
info.add_text("t%s" % x, compressed_data, zip=True)
info.add_itxt("i%s" % x, compressed_data, zip=True)
b = BytesIO()
im.save(b, "PNG", pnginfo=info)
b.seek(0)
try:
im2 = Image.open(b)
except ValueError as msg:
assert "Too much memory" in msg
return
total_len = 0
for txt in im2.text.values():
total_len += len(txt)
assert total_len < 64 * 1024 * 1024, "Total text chunks greater than 64M"
if __name__ == "__main__": def test_dos_text():
unittest.main()
try:
im = Image.open(TEST_FILE)
im.load()
except ValueError as msg:
assert msg, "Decompressed Data Too Large"
return
for s in im.text.values():
assert len(s) < 1024 * 1024, "Text chunk larger than 1M"
def test_dos_total_memory():
im = Image.new("L", (1, 1))
compressed_data = zlib.compress(b"a" * 1024 * 1023)
info = PngImagePlugin.PngInfo()
for x in range(64):
info.add_text("t%s" % x, compressed_data, zip=True)
info.add_itxt("i%s" % x, compressed_data, zip=True)
b = BytesIO()
im.save(b, "PNG", pnginfo=info)
b.seek(0)
try:
im2 = Image.open(b)
except ValueError as msg:
assert "Too much memory" in msg
return
total_len = 0
for txt in im2.text.values():
total_len += len(txt)
assert total_len < 64 * 1024 * 1024, "Total text chunks greater than 64M"

View File

@ -7,7 +7,6 @@ import os
import shutil import shutil
import sys import sys
import tempfile import tempfile
import unittest
from io import BytesIO from io import BytesIO
import pytest import pytest
@ -176,22 +175,6 @@ def skip_unless_feature(feature):
return pytest.mark.skipif(not features.check(feature), reason=reason) return pytest.mark.skipif(not features.check(feature), reason=reason)
class PillowTestCase(unittest.TestCase):
def delete_tempfile(self, path):
try:
os.remove(path)
except OSError:
pass # report?
def tempfile(self, template):
assert template[:5] in ("temp.", "temp_")
fd, path = tempfile.mkstemp(template[4:], template[:4])
os.close(fd)
self.addCleanup(self.delete_tempfile, path)
return path
@pytest.mark.skipif(sys.platform.startswith("win32"), reason="Requires Unix or macOS") @pytest.mark.skipif(sys.platform.startswith("win32"), reason="Requires Unix or macOS")
class PillowLeakTestCase: class PillowLeakTestCase:
# requires unix/macOS # requires unix/macOS

View File

@ -1,15 +1,16 @@
import pytest import pytest
from PIL import Image from PIL import Image
from .helper import PillowTestCase, hopper from .helper import hopper
TEST_FILE = "Tests/images/hopper.ppm" TEST_FILE = "Tests/images/hopper.ppm"
ORIGINAL_LIMIT = Image.MAX_IMAGE_PIXELS ORIGINAL_LIMIT = Image.MAX_IMAGE_PIXELS
class TestDecompressionBomb(PillowTestCase): class TestDecompressionBomb:
def tearDown(self): @classmethod
def teardown_class(self):
Image.MAX_IMAGE_PIXELS = ORIGINAL_LIMIT Image.MAX_IMAGE_PIXELS = ORIGINAL_LIMIT
def test_no_warning_small_file(self): def test_no_warning_small_file(self):
@ -59,20 +60,22 @@ class TestDecompressionBomb(PillowTestCase):
Image.open("Tests/images/decompression_bomb.gif") Image.open("Tests/images/decompression_bomb.gif")
class TestDecompressionCrop(PillowTestCase): class TestDecompressionCrop:
def setUp(self): @classmethod
self.src = hopper() def setup_class(self):
self.addCleanup(self.src.close) width, height = 128, 128
Image.MAX_IMAGE_PIXELS = self.src.height * self.src.width * 4 - 1 Image.MAX_IMAGE_PIXELS = height * width * 4 - 1
def tearDown(self): @classmethod
def teardown_class(self):
Image.MAX_IMAGE_PIXELS = ORIGINAL_LIMIT Image.MAX_IMAGE_PIXELS = ORIGINAL_LIMIT
def testEnlargeCrop(self): def testEnlargeCrop(self):
# Crops can extend the extents, therefore we should have the # Crops can extend the extents, therefore we should have the
# same decompression bomb warnings on them. # same decompression bomb warnings on them.
box = (0, 0, self.src.width * 2, self.src.height * 2) with hopper() as src:
pytest.warns(Image.DecompressionBombWarning, self.src.crop, box) box = (0, 0, src.width * 2, src.height * 2)
pytest.warns(Image.DecompressionBombWarning, src.crop, box)
def test_crop_decompression_checks(self): def test_crop_decompression_checks(self):

View File

@ -1,74 +1,78 @@
import pytest import pytest
from PIL import Image, WmfImagePlugin from PIL import Image, WmfImagePlugin
from .helper import PillowTestCase, assert_image_similar, hopper from .helper import assert_image_similar, hopper
class TestFileWmf(PillowTestCase): def test_load_raw():
def test_load_raw(self):
# Test basic EMF open and rendering # Test basic EMF open and rendering
with Image.open("Tests/images/drawing.emf") as im: with Image.open("Tests/images/drawing.emf") as im:
if hasattr(Image.core, "drawwmf"): if hasattr(Image.core, "drawwmf"):
# Currently, support for WMF/EMF is Windows-only # Currently, support for WMF/EMF is Windows-only
im.load() im.load()
# Compare to reference rendering # Compare to reference rendering
with Image.open("Tests/images/drawing_emf_ref.png") as imref: with Image.open("Tests/images/drawing_emf_ref.png") as imref:
imref.load() imref.load()
assert_image_similar(im, imref, 0) assert_image_similar(im, imref, 0)
# Test basic WMF open and rendering # Test basic WMF open and rendering
with Image.open("Tests/images/drawing.wmf") as im: with Image.open("Tests/images/drawing.wmf") as im:
if hasattr(Image.core, "drawwmf"): if hasattr(Image.core, "drawwmf"):
# Currently, support for WMF/EMF is Windows-only # Currently, support for WMF/EMF is Windows-only
im.load() im.load()
# Compare to reference rendering # Compare to reference rendering
with Image.open("Tests/images/drawing_wmf_ref.png") as imref: with Image.open("Tests/images/drawing_wmf_ref.png") as imref:
imref.load() imref.load()
assert_image_similar(im, imref, 2.0) assert_image_similar(im, imref, 2.0)
def test_register_handler(self):
class TestHandler:
methodCalled = False
def save(self, im, fp, filename): def test_register_handler(tmp_path):
self.methodCalled = True class TestHandler:
methodCalled = False
handler = TestHandler() def save(self, im, fp, filename):
WmfImagePlugin.register_handler(handler) self.methodCalled = True
im = hopper() handler = TestHandler()
tmpfile = self.tempfile("temp.wmf") original_handler = WmfImagePlugin._handler
im.save(tmpfile) WmfImagePlugin.register_handler(handler)
assert handler.methodCalled
# Restore the state before this test im = hopper()
WmfImagePlugin.register_handler(None) tmpfile = str(tmp_path / "temp.wmf")
im.save(tmpfile)
assert handler.methodCalled
def test_load_dpi_rounding(self): # Restore the state before this test
# Round up WmfImagePlugin.register_handler(original_handler)
with Image.open("Tests/images/drawing.emf") as im:
assert im.info["dpi"] == 1424
# Round down
with Image.open("Tests/images/drawing_roundDown.emf") as im:
assert im.info["dpi"] == 1426
def test_load_set_dpi(self): def test_load_dpi_rounding():
with Image.open("Tests/images/drawing.wmf") as im: # Round up
assert im.size == (82, 82) with Image.open("Tests/images/drawing.emf") as im:
assert im.info["dpi"] == 1424
if hasattr(Image.core, "drawwmf"): # Round down
im.load(144) with Image.open("Tests/images/drawing_roundDown.emf") as im:
assert im.size == (164, 164) assert im.info["dpi"] == 1426
with Image.open("Tests/images/drawing_wmf_ref_144.png") as expected:
assert_image_similar(im, expected, 2.0)
def test_save(self): def test_load_set_dpi():
im = hopper() with Image.open("Tests/images/drawing.wmf") as im:
assert im.size == (82, 82)
for ext in [".wmf", ".emf"]: if hasattr(Image.core, "drawwmf"):
tmpfile = self.tempfile("temp" + ext) im.load(144)
with pytest.raises(IOError): assert im.size == (164, 164)
im.save(tmpfile)
with Image.open("Tests/images/drawing_wmf_ref_144.png") as expected:
assert_image_similar(im, expected, 2.0)
def test_save(tmp_path):
im = hopper()
for ext in [".wmf", ".emf"]:
tmpfile = str(tmp_path / ("temp" + ext))
with pytest.raises(IOError):
im.save(tmpfile)

View File

@ -1,79 +1,90 @@
import os
import pytest import pytest
from PIL import FontFile, Image, ImageDraw, ImageFont, PcfFontFile from PIL import FontFile, Image, ImageDraw, ImageFont, PcfFontFile
from .helper import ( from .helper import assert_image_equal, assert_image_similar, skip_unless_feature
PillowTestCase,
assert_image_equal,
assert_image_similar,
skip_unless_feature,
)
fontname = "Tests/fonts/10x20-ISO8859-1.pcf" fontname = "Tests/fonts/10x20-ISO8859-1.pcf"
message = "hello, world" message = "hello, world"
@skip_unless_feature("zlib") pytestmark = skip_unless_feature("zlib")
class TestFontPcf(PillowTestCase):
def save_font(self):
with open(fontname, "rb") as test_file:
font = PcfFontFile.PcfFontFile(test_file)
assert isinstance(font, FontFile.FontFile)
# check the number of characters in the font
assert len([_f for _f in font.glyph if _f]) == 223
tempname = self.tempfile("temp.pil")
self.addCleanup(self.delete_tempfile, tempname[:-4] + ".pbm")
font.save(tempname)
with Image.open(tempname.replace(".pil", ".pbm")) as loaded: def save_font(request, tmp_path):
with Image.open("Tests/fonts/10x20.pbm") as target: with open(fontname, "rb") as test_file:
assert_image_equal(loaded, target) font = PcfFontFile.PcfFontFile(test_file)
assert isinstance(font, FontFile.FontFile)
# check the number of characters in the font
assert len([_f for _f in font.glyph if _f]) == 223
with open(tempname, "rb") as f_loaded: tempname = str(tmp_path / "temp.pil")
with open("Tests/fonts/10x20.pil", "rb") as f_target:
assert f_loaded.read() == f_target.read()
return tempname
def test_sanity(self): def delete_tempfile():
self.save_font() try:
os.remove(tempname[:-4] + ".pbm")
except OSError:
pass # report?
def test_invalid_file(self): request.addfinalizer(delete_tempfile)
with open("Tests/images/flower.jpg", "rb") as fp: font.save(tempname)
with pytest.raises(SyntaxError):
PcfFontFile.PcfFontFile(fp)
def test_draw(self): with Image.open(tempname.replace(".pil", ".pbm")) as loaded:
tempname = self.save_font() with Image.open("Tests/fonts/10x20.pbm") as target:
font = ImageFont.load(tempname) assert_image_equal(loaded, target)
im = Image.new("L", (130, 30), "white")
draw = ImageDraw.Draw(im)
draw.text((0, 0), message, "black", font=font)
with Image.open("Tests/images/test_draw_pbm_target.png") as target:
assert_image_similar(im, target, 0)
def test_textsize(self): with open(tempname, "rb") as f_loaded:
tempname = self.save_font() with open("Tests/fonts/10x20.pil", "rb") as f_target:
font = ImageFont.load(tempname) assert f_loaded.read() == f_target.read()
for i in range(255): return tempname
(dx, dy) = font.getsize(chr(i))
assert dy == 20
assert dx in (0, 10)
for l in range(len(message)):
msg = message[: l + 1]
assert font.getsize(msg) == (len(msg) * 10, 20)
def _test_high_characters(self, message):
tempname = self.save_font()
font = ImageFont.load(tempname)
im = Image.new("L", (750, 30), "white")
draw = ImageDraw.Draw(im)
draw.text((0, 0), message, "black", font=font)
with Image.open("Tests/images/high_ascii_chars.png") as target:
assert_image_similar(im, target, 0)
def test_high_characters(self): def test_sanity(request, tmp_path):
message = "".join(chr(i + 1) for i in range(140, 232)) save_font(request, tmp_path)
self._test_high_characters(message)
# accept bytes instances.
self._test_high_characters(message.encode("latin1")) def test_invalid_file():
with open("Tests/images/flower.jpg", "rb") as fp:
with pytest.raises(SyntaxError):
PcfFontFile.PcfFontFile(fp)
def test_draw(request, tmp_path):
tempname = save_font(request, tmp_path)
font = ImageFont.load(tempname)
im = Image.new("L", (130, 30), "white")
draw = ImageDraw.Draw(im)
draw.text((0, 0), message, "black", font=font)
with Image.open("Tests/images/test_draw_pbm_target.png") as target:
assert_image_similar(im, target, 0)
def test_textsize(request, tmp_path):
tempname = save_font(request, tmp_path)
font = ImageFont.load(tempname)
for i in range(255):
(dx, dy) = font.getsize(chr(i))
assert dy == 20
assert dx in (0, 10)
for l in range(len(message)):
msg = message[: l + 1]
assert font.getsize(msg) == (len(msg) * 10, 20)
def _test_high_characters(request, tmp_path, message):
tempname = save_font(request, tmp_path)
font = ImageFont.load(tempname)
im = Image.new("L", (750, 30), "white")
draw = ImageDraw.Draw(im)
draw.text((0, 0), message, "black", font=font)
with Image.open("Tests/images/high_ascii_chars.png") as target:
assert_image_similar(im, target, 0)
def test_high_characters(request, tmp_path):
message = "".join(chr(i + 1) for i in range(140, 232))
_test_high_characters(request, tmp_path, message)
# accept bytes instances.
_test_high_characters(request, tmp_path, message.encode("latin1"))

View File

@ -1,47 +1,59 @@
import pytest
from PIL import Image, ImageQt from PIL import Image, ImageQt
from .helper import PillowTestCase, assert_image_equal, hopper from .helper import assert_image_equal, hopper
from .test_imageqt import PillowQtTestCase
pytestmark = pytest.mark.skipif(
not ImageQt.qt_is_installed, reason="Qt bindings are not installed"
)
class TestFromQImage(PillowQtTestCase, PillowTestCase): @pytest.fixture
def setUp(self): def test_images():
super().setUp() ims = [
self.files_to_test = [ hopper(),
hopper(), Image.open("Tests/images/transparent.png"),
Image.open("Tests/images/transparent.png"), Image.open("Tests/images/7x13.png"),
Image.open("Tests/images/7x13.png"), ]
] try:
for im in self.files_to_test: yield ims
self.addCleanup(im.close) finally:
for im in ims:
im.close()
def roundtrip(self, expected):
# PIL -> Qt
intermediate = expected.toqimage()
# Qt -> PIL
result = ImageQt.fromqimage(intermediate)
if intermediate.hasAlphaChannel(): def roundtrip(expected):
assert_image_equal(result, expected.convert("RGBA")) # PIL -> Qt
else: intermediate = expected.toqimage()
assert_image_equal(result, expected.convert("RGB")) # Qt -> PIL
result = ImageQt.fromqimage(intermediate)
def test_sanity_1(self): if intermediate.hasAlphaChannel():
for im in self.files_to_test: assert_image_equal(result, expected.convert("RGBA"))
self.roundtrip(im.convert("1")) else:
assert_image_equal(result, expected.convert("RGB"))
def test_sanity_rgb(self):
for im in self.files_to_test:
self.roundtrip(im.convert("RGB"))
def test_sanity_rgba(self): def test_sanity_1(test_images):
for im in self.files_to_test: for im in test_images:
self.roundtrip(im.convert("RGBA")) roundtrip(im.convert("1"))
def test_sanity_l(self):
for im in self.files_to_test:
self.roundtrip(im.convert("L"))
def test_sanity_p(self): def test_sanity_rgb(test_images):
for im in self.files_to_test: for im in test_images:
self.roundtrip(im.convert("P")) roundtrip(im.convert("RGB"))
def test_sanity_rgba(test_images):
for im in test_images:
roundtrip(im.convert("RGBA"))
def test_sanity_l(test_images):
for im in test_images:
roundtrip(im.convert("L"))
def test_sanity_p(test_images):
for im in test_images:
roundtrip(im.convert("P"))

View File

@ -1,9 +1,9 @@
from PIL import Image from PIL import Image
from .helper import PillowTestCase, assert_image_equal, cached_property from .helper import assert_image_equal, cached_property
class TestImagingPaste(PillowTestCase): class TestImagingPaste:
masks = {} masks = {}
size = 128 size = 128

View File

@ -6,10 +6,10 @@ from itertools import permutations
import pytest import pytest
from PIL import Image from PIL import Image
from .helper import PillowTestCase, assert_image_equal, assert_image_similar, hopper from .helper import assert_image_equal, assert_image_similar, hopper
class TestImagingCoreResize(PillowTestCase): class TestImagingCoreResize:
def resize(self, im, size, f): def resize(self, im, size, f):
# Image class independent version of resize. # Image class independent version of resize.
im.load() im.load()
@ -135,31 +135,36 @@ class TestImagingCoreResize(PillowTestCase):
self.resize(hopper(), (10, 10), 9) self.resize(hopper(), (10, 10), 9)
class TestReducingGapResize(PillowTestCase): @pytest.fixture
@classmethod def gradients_image():
def setUpClass(cls): im = Image.open("Tests/images/radial_gradients.png")
cls.gradients_image = Image.open("Tests/images/radial_gradients.png") im.load()
cls.gradients_image.load() try:
yield im
finally:
im.close()
def test_reducing_gap_values(self):
ref = self.gradients_image.resize((52, 34), Image.BICUBIC, reducing_gap=None) class TestReducingGapResize:
im = self.gradients_image.resize((52, 34), Image.BICUBIC) def test_reducing_gap_values(self, gradients_image):
ref = gradients_image.resize((52, 34), Image.BICUBIC, reducing_gap=None)
im = gradients_image.resize((52, 34), Image.BICUBIC)
assert_image_equal(ref, im) assert_image_equal(ref, im)
with pytest.raises(ValueError): with pytest.raises(ValueError):
self.gradients_image.resize((52, 34), Image.BICUBIC, reducing_gap=0) gradients_image.resize((52, 34), Image.BICUBIC, reducing_gap=0)
with pytest.raises(ValueError): with pytest.raises(ValueError):
self.gradients_image.resize((52, 34), Image.BICUBIC, reducing_gap=0.99) gradients_image.resize((52, 34), Image.BICUBIC, reducing_gap=0.99)
def test_reducing_gap_1(self): def test_reducing_gap_1(self, gradients_image):
for box, epsilon in [ for box, epsilon in [
(None, 4), (None, 4),
((1.1, 2.2, 510.8, 510.9), 4), ((1.1, 2.2, 510.8, 510.9), 4),
((3, 10, 410, 256), 10), ((3, 10, 410, 256), 10),
]: ]:
ref = self.gradients_image.resize((52, 34), Image.BICUBIC, box=box) ref = gradients_image.resize((52, 34), Image.BICUBIC, box=box)
im = self.gradients_image.resize( im = gradients_image.resize(
(52, 34), Image.BICUBIC, box=box, reducing_gap=1.0 (52, 34), Image.BICUBIC, box=box, reducing_gap=1.0
) )
@ -168,14 +173,14 @@ class TestReducingGapResize(PillowTestCase):
assert_image_similar(ref, im, epsilon) assert_image_similar(ref, im, epsilon)
def test_reducing_gap_2(self): def test_reducing_gap_2(self, gradients_image):
for box, epsilon in [ for box, epsilon in [
(None, 1.5), (None, 1.5),
((1.1, 2.2, 510.8, 510.9), 1.5), ((1.1, 2.2, 510.8, 510.9), 1.5),
((3, 10, 410, 256), 1), ((3, 10, 410, 256), 1),
]: ]:
ref = self.gradients_image.resize((52, 34), Image.BICUBIC, box=box) ref = gradients_image.resize((52, 34), Image.BICUBIC, box=box)
im = self.gradients_image.resize( im = gradients_image.resize(
(52, 34), Image.BICUBIC, box=box, reducing_gap=2.0 (52, 34), Image.BICUBIC, box=box, reducing_gap=2.0
) )
@ -184,14 +189,14 @@ class TestReducingGapResize(PillowTestCase):
assert_image_similar(ref, im, epsilon) assert_image_similar(ref, im, epsilon)
def test_reducing_gap_3(self): def test_reducing_gap_3(self, gradients_image):
for box, epsilon in [ for box, epsilon in [
(None, 1), (None, 1),
((1.1, 2.2, 510.8, 510.9), 1), ((1.1, 2.2, 510.8, 510.9), 1),
((3, 10, 410, 256), 0.5), ((3, 10, 410, 256), 0.5),
]: ]:
ref = self.gradients_image.resize((52, 34), Image.BICUBIC, box=box) ref = gradients_image.resize((52, 34), Image.BICUBIC, box=box)
im = self.gradients_image.resize( im = gradients_image.resize(
(52, 34), Image.BICUBIC, box=box, reducing_gap=3.0 (52, 34), Image.BICUBIC, box=box, reducing_gap=3.0
) )
@ -200,29 +205,27 @@ class TestReducingGapResize(PillowTestCase):
assert_image_similar(ref, im, epsilon) assert_image_similar(ref, im, epsilon)
def test_reducing_gap_8(self): def test_reducing_gap_8(self, gradients_image):
for box in [None, (1.1, 2.2, 510.8, 510.9), (3, 10, 410, 256)]: for box in [None, (1.1, 2.2, 510.8, 510.9), (3, 10, 410, 256)]:
ref = self.gradients_image.resize((52, 34), Image.BICUBIC, box=box) ref = gradients_image.resize((52, 34), Image.BICUBIC, box=box)
im = self.gradients_image.resize( im = gradients_image.resize(
(52, 34), Image.BICUBIC, box=box, reducing_gap=8.0 (52, 34), Image.BICUBIC, box=box, reducing_gap=8.0
) )
assert_image_equal(ref, im) assert_image_equal(ref, im)
def test_box_filter(self): def test_box_filter(self, gradients_image):
for box, epsilon in [ for box, epsilon in [
((0, 0, 512, 512), 5.5), ((0, 0, 512, 512), 5.5),
((0.9, 1.7, 128, 128), 9.5), ((0.9, 1.7, 128, 128), 9.5),
]: ]:
ref = self.gradients_image.resize((52, 34), Image.BOX, box=box) ref = gradients_image.resize((52, 34), Image.BOX, box=box)
im = self.gradients_image.resize( im = gradients_image.resize((52, 34), Image.BOX, box=box, reducing_gap=1.0)
(52, 34), Image.BOX, box=box, reducing_gap=1.0
)
assert_image_similar(ref, im, epsilon) assert_image_similar(ref, im, epsilon)
class TestImageResize(PillowTestCase): class TestImageResize:
def test_resize(self): def test_resize(self):
def resize(mode, size): def resize(mode, size):
out = hopper(mode).resize(size) out = hopper(mode).resize(size)

View File

@ -3,14 +3,13 @@ import distutils.version
import os import os
import re import re
import shutil import shutil
import sys
from io import BytesIO from io import BytesIO
from unittest import mock
import pytest import pytest
from PIL import Image, ImageDraw, ImageFont from PIL import Image, ImageDraw, ImageFont
from .helper import ( from .helper import (
PillowTestCase,
assert_image_equal, assert_image_equal,
assert_image_similar, assert_image_similar,
assert_image_similar_tofile, assert_image_similar_tofile,
@ -25,8 +24,10 @@ FONT_SIZE = 20
TEST_TEXT = "hey you\nyou are awesome\nthis looks awkward" TEST_TEXT = "hey you\nyou are awesome\nthis looks awkward"
@skip_unless_feature("freetype2") pytestmark = skip_unless_feature("freetype2")
class TestImageFont(PillowTestCase):
class TestImageFont:
LAYOUT_ENGINE = ImageFont.LAYOUT_BASIC LAYOUT_ENGINE = ImageFont.LAYOUT_BASIC
# Freetype has different metrics depending on the version. # Freetype has different metrics depending on the version.
@ -37,7 +38,8 @@ class TestImageFont(PillowTestCase):
"Default": {"multiline": 0.5, "textsize": 0.5, "getters": (12, 16)}, "Default": {"multiline": 0.5, "textsize": 0.5, "getters": (12, 16)},
} }
def setUp(self): @classmethod
def setup_class(self):
freetype = distutils.version.StrictVersion(ImageFont.core.freetype2_version) freetype = distutils.version.StrictVersion(ImageFont.core.freetype2_version)
self.metrics = self.METRICS["Default"] self.metrics = self.METRICS["Default"]
@ -107,12 +109,12 @@ class TestImageFont(PillowTestCase):
with open(FONT_PATH, "rb") as f: with open(FONT_PATH, "rb") as f:
self._render(f) self._render(f)
def test_non_unicode_path(self): def test_non_unicode_path(self, tmp_path):
tempfile = str(tmp_path / ("temp_" + chr(128) + ".ttf"))
try: try:
tempfile = self.tempfile("temp_" + chr(128) + ".ttf") shutil.copy(FONT_PATH, tempfile)
except UnicodeEncodeError: except UnicodeEncodeError:
self.skipTest("Unicode path could not be created") pytest.skip("Unicode path could not be created")
shutil.copy(FONT_PATH, tempfile)
ImageFont.truetype(tempfile, FONT_SIZE) ImageFont.truetype(tempfile, FONT_SIZE)
@ -457,10 +459,11 @@ class TestImageFont(PillowTestCase):
assert_image_similar_tofile(img, target, self.metrics["multiline"]) assert_image_similar_tofile(img, target, self.metrics["multiline"])
def _test_fake_loading_font(self, path_to_fake, fontname): def _test_fake_loading_font(self, monkeypatch, path_to_fake, fontname):
# Make a copy of FreeTypeFont so we can patch the original # Make a copy of FreeTypeFont so we can patch the original
free_type_font = copy.deepcopy(ImageFont.FreeTypeFont) free_type_font = copy.deepcopy(ImageFont.FreeTypeFont)
with mock.patch.object(ImageFont, "_FreeTypeFont", free_type_font, create=True): with monkeypatch.context() as m:
m.setattr(ImageFont, "_FreeTypeFont", free_type_font, raising=False)
def loadable_font(filepath, size, index, encoding, *args, **kwargs): def loadable_font(filepath, size, index, encoding, *args, **kwargs):
if filepath == path_to_fake: if filepath == path_to_fake:
@ -471,87 +474,84 @@ class TestImageFont(PillowTestCase):
filepath, size, index, encoding, *args, **kwargs filepath, size, index, encoding, *args, **kwargs
) )
with mock.patch.object(ImageFont, "FreeTypeFont", loadable_font): m.setattr(ImageFont, "FreeTypeFont", loadable_font)
font = ImageFont.truetype(fontname) font = ImageFont.truetype(fontname)
# Make sure it's loaded # Make sure it's loaded
name = font.getname() name = font.getname()
assert ("FreeMono", "Regular") == name assert ("FreeMono", "Regular") == name
@pytest.mark.skipif(is_win32(), reason="requires Unix or macOS") @pytest.mark.skipif(is_win32(), reason="requires Unix or macOS")
def test_find_linux_font(self): def test_find_linux_font(self, monkeypatch):
# A lot of mocking here - this is more for hitting code and # A lot of mocking here - this is more for hitting code and
# catching syntax like errors # catching syntax like errors
font_directory = "/usr/local/share/fonts" font_directory = "/usr/local/share/fonts"
with mock.patch("sys.platform", "linux"): monkeypatch.setattr(sys, "platform", "linux")
patched_env = {"XDG_DATA_DIRS": "/usr/share/:/usr/local/share/"} monkeypatch.setenv("XDG_DATA_DIRS", "/usr/share/:/usr/local/share/")
with mock.patch.dict(os.environ, patched_env):
def fake_walker(path): def fake_walker(path):
if path == font_directory: if path == font_directory:
return [ return [
( (
path, path,
[], [],
[ ["Arial.ttf", "Single.otf", "Duplicate.otf", "Duplicate.ttf"],
"Arial.ttf",
"Single.otf",
"Duplicate.otf",
"Duplicate.ttf",
],
)
]
return [(path, [], ["some_random_font.ttf"])]
with mock.patch("os.walk", fake_walker):
# Test that the font loads both with and without the
# extension
self._test_fake_loading_font(
font_directory + "/Arial.ttf", "Arial.ttf"
) )
self._test_fake_loading_font(font_directory + "/Arial.ttf", "Arial") ]
return [(path, [], ["some_random_font.ttf"])]
# Test that non-ttf fonts can be found without the monkeypatch.setattr(os, "walk", fake_walker)
# extension # Test that the font loads both with and without the
self._test_fake_loading_font( # extension
font_directory + "/Single.otf", "Single" self._test_fake_loading_font(
) monkeypatch, font_directory + "/Arial.ttf", "Arial.ttf"
)
self._test_fake_loading_font(
monkeypatch, font_directory + "/Arial.ttf", "Arial"
)
# Test that ttf fonts are preferred if the extension is # Test that non-ttf fonts can be found without the
# not specified # extension
self._test_fake_loading_font( self._test_fake_loading_font(
font_directory + "/Duplicate.ttf", "Duplicate" monkeypatch, font_directory + "/Single.otf", "Single"
) )
# Test that ttf fonts are preferred if the extension is
# not specified
self._test_fake_loading_font(
monkeypatch, font_directory + "/Duplicate.ttf", "Duplicate"
)
@pytest.mark.skipif(is_win32(), reason="requires Unix or macOS") @pytest.mark.skipif(is_win32(), reason="requires Unix or macOS")
def test_find_macos_font(self): def test_find_macos_font(self, monkeypatch):
# Like the linux test, more cover hitting code rather than testing # Like the linux test, more cover hitting code rather than testing
# correctness. # correctness.
font_directory = "/System/Library/Fonts" font_directory = "/System/Library/Fonts"
with mock.patch("sys.platform", "darwin"): monkeypatch.setattr(sys, "platform", "darwin")
def fake_walker(path): def fake_walker(path):
if path == font_directory: if path == font_directory:
return [ return [
( (
path, path,
[], [],
[ ["Arial.ttf", "Single.otf", "Duplicate.otf", "Duplicate.ttf"],
"Arial.ttf", )
"Single.otf", ]
"Duplicate.otf", return [(path, [], ["some_random_font.ttf"])]
"Duplicate.ttf",
],
)
]
return [(path, [], ["some_random_font.ttf"])]
with mock.patch("os.walk", fake_walker): monkeypatch.setattr(os, "walk", fake_walker)
self._test_fake_loading_font(font_directory + "/Arial.ttf", "Arial.ttf") self._test_fake_loading_font(
self._test_fake_loading_font(font_directory + "/Arial.ttf", "Arial") monkeypatch, font_directory + "/Arial.ttf", "Arial.ttf"
self._test_fake_loading_font(font_directory + "/Single.otf", "Single") )
self._test_fake_loading_font( self._test_fake_loading_font(
font_directory + "/Duplicate.ttf", "Duplicate" monkeypatch, font_directory + "/Arial.ttf", "Arial"
) )
self._test_fake_loading_font(
monkeypatch, font_directory + "/Single.otf", "Single"
)
self._test_fake_loading_font(
monkeypatch, font_directory + "/Duplicate.ttf", "Duplicate"
)
def test_imagefont_getters(self): def test_imagefont_getters(self):
# Arrange # Arrange

View File

@ -1,98 +1,110 @@
import pytest import pytest
from PIL import Image, ImageFilter from PIL import Image, ImageFilter
from .helper import PillowTestCase
@pytest.fixture
def test_images():
ims = {
"im": Image.open("Tests/images/hopper.ppm"),
"snakes": Image.open("Tests/images/color_snakes.png"),
}
try:
yield ims
finally:
for im in ims.values():
im.close()
class TestImageOpsUsm(PillowTestCase): def test_filter_api(test_images):
def setUp(self): im = test_images["im"]
super().setUp()
self.im = Image.open("Tests/images/hopper.ppm")
self.addCleanup(self.im.close)
self.snakes = Image.open("Tests/images/color_snakes.png")
self.addCleanup(self.snakes.close)
def test_filter_api(self): test_filter = ImageFilter.GaussianBlur(2.0)
i = im.filter(test_filter)
assert i.mode == "RGB"
assert i.size == (128, 128)
test_filter = ImageFilter.GaussianBlur(2.0) test_filter = ImageFilter.UnsharpMask(2.0, 125, 8)
i = self.im.filter(test_filter) i = im.filter(test_filter)
assert i.mode == "RGB" assert i.mode == "RGB"
assert i.size == (128, 128) assert i.size == (128, 128)
test_filter = ImageFilter.UnsharpMask(2.0, 125, 8)
i = self.im.filter(test_filter)
assert i.mode == "RGB"
assert i.size == (128, 128)
def test_usm_formats(self): def test_usm_formats(test_images):
im = test_images["im"]
usm = ImageFilter.UnsharpMask usm = ImageFilter.UnsharpMask
with pytest.raises(ValueError): with pytest.raises(ValueError):
self.im.convert("1").filter(usm) im.convert("1").filter(usm)
self.im.convert("L").filter(usm) im.convert("L").filter(usm)
with pytest.raises(ValueError): with pytest.raises(ValueError):
self.im.convert("I").filter(usm) im.convert("I").filter(usm)
with pytest.raises(ValueError): with pytest.raises(ValueError):
self.im.convert("F").filter(usm) im.convert("F").filter(usm)
self.im.convert("RGB").filter(usm) im.convert("RGB").filter(usm)
self.im.convert("RGBA").filter(usm) im.convert("RGBA").filter(usm)
self.im.convert("CMYK").filter(usm) im.convert("CMYK").filter(usm)
with pytest.raises(ValueError): with pytest.raises(ValueError):
self.im.convert("YCbCr").filter(usm) im.convert("YCbCr").filter(usm)
def test_blur_formats(self):
blur = ImageFilter.GaussianBlur def test_blur_formats(test_images):
with pytest.raises(ValueError): im = test_images["im"]
self.im.convert("1").filter(blur)
blur(self.im.convert("L"))
with pytest.raises(ValueError):
self.im.convert("I").filter(blur)
with pytest.raises(ValueError):
self.im.convert("F").filter(blur)
self.im.convert("RGB").filter(blur)
self.im.convert("RGBA").filter(blur)
self.im.convert("CMYK").filter(blur)
with pytest.raises(ValueError):
self.im.convert("YCbCr").filter(blur)
def test_usm_accuracy(self): blur = ImageFilter.GaussianBlur
with pytest.raises(ValueError):
im.convert("1").filter(blur)
blur(im.convert("L"))
with pytest.raises(ValueError):
im.convert("I").filter(blur)
with pytest.raises(ValueError):
im.convert("F").filter(blur)
im.convert("RGB").filter(blur)
im.convert("RGBA").filter(blur)
im.convert("CMYK").filter(blur)
with pytest.raises(ValueError):
im.convert("YCbCr").filter(blur)
src = self.snakes.convert("RGB")
i = src.filter(ImageFilter.UnsharpMask(5, 1024, 0))
# Image should not be changed because it have only 0 and 255 levels.
assert i.tobytes() == src.tobytes()
def test_blur_accuracy(self): def test_usm_accuracy(test_images):
snakes = test_images["snakes"]
i = self.snakes.filter(ImageFilter.GaussianBlur(0.4)) src = snakes.convert("RGB")
# These pixels surrounded with pixels with 255 intensity. i = src.filter(ImageFilter.UnsharpMask(5, 1024, 0))
# They must be very close to 255. # Image should not be changed because it have only 0 and 255 levels.
for x, y, c in [ assert i.tobytes() == src.tobytes()
(1, 0, 1),
(2, 0, 1),
(7, 8, 1),
(8, 8, 1),
(2, 9, 1),
(7, 3, 0),
(8, 3, 0),
(5, 8, 0),
(5, 9, 0),
(1, 3, 0),
(4, 3, 2),
(4, 2, 2),
]:
assert i.im.getpixel((x, y))[c] >= 250
# Fuzzy match.
def gp(x, y):
return i.im.getpixel((x, y))
assert 236 <= gp(7, 4)[0] <= 239 def test_blur_accuracy(test_images):
assert 236 <= gp(7, 5)[2] <= 239 snakes = test_images["snakes"]
assert 236 <= gp(7, 6)[2] <= 239
assert 236 <= gp(7, 7)[1] <= 239 i = snakes.filter(ImageFilter.GaussianBlur(0.4))
assert 236 <= gp(8, 4)[0] <= 239 # These pixels surrounded with pixels with 255 intensity.
assert 236 <= gp(8, 5)[2] <= 239 # They must be very close to 255.
assert 236 <= gp(8, 6)[2] <= 239 for x, y, c in [
assert 236 <= gp(8, 7)[1] <= 239 (1, 0, 1),
(2, 0, 1),
(7, 8, 1),
(8, 8, 1),
(2, 9, 1),
(7, 3, 0),
(8, 3, 0),
(5, 8, 0),
(5, 9, 0),
(1, 3, 0),
(4, 3, 2),
(4, 2, 2),
]:
assert i.im.getpixel((x, y))[c] >= 250
# Fuzzy match.
def gp(x, y):
return i.im.getpixel((x, y))
assert 236 <= gp(7, 4)[0] <= 239
assert 236 <= gp(7, 5)[2] <= 239
assert 236 <= gp(7, 6)[2] <= 239
assert 236 <= gp(7, 7)[1] <= 239
assert 236 <= gp(8, 4)[0] <= 239
assert 236 <= gp(8, 5)[2] <= 239
assert 236 <= gp(8, 6)[2] <= 239
assert 236 <= gp(8, 7)[1] <= 239

View File

@ -1,71 +1,60 @@
import pytest
from PIL import ImageQt from PIL import ImageQt
from .helper import PillowTestCase, hopper from .helper import hopper
if ImageQt.qt_is_installed: if ImageQt.qt_is_installed:
from PIL.ImageQt import qRgba from PIL.ImageQt import qRgba
def skip_if_qt_is_not_installed(_):
pass
@pytest.mark.skipif(not ImageQt.qt_is_installed, reason="Qt bindings are not installed")
else: class PillowQPixmapTestCase:
@classmethod
def skip_if_qt_is_not_installed(test_case): def setup_class(self):
test_case.skipTest("Qt bindings are not installed")
class PillowQtTestCase:
def setUp(self):
skip_if_qt_is_not_installed(self)
def tearDown(self):
pass
class PillowQPixmapTestCase(PillowQtTestCase):
def setUp(self):
super().setUp()
try: try:
if ImageQt.qt_version == "5": if ImageQt.qt_version == "5":
from PyQt5.QtGui import QGuiApplication from PyQt5.QtGui import QGuiApplication
elif ImageQt.qt_version == "side2": elif ImageQt.qt_version == "side2":
from PySide2.QtGui import QGuiApplication from PySide2.QtGui import QGuiApplication
except ImportError: except ImportError:
self.skipTest("QGuiApplication not installed") pytest.skip("QGuiApplication not installed")
return
self.app = QGuiApplication([]) self.app = QGuiApplication([])
def tearDown(self): @classmethod
super().tearDown() def teardown_class(self):
self.app.quit() self.app.quit()
self.app = None
class TestImageQt(PillowQtTestCase, PillowTestCase): @pytest.mark.skipif(not ImageQt.qt_is_installed, reason="Qt bindings are not installed")
def test_rgb(self): def test_rgb():
# from https://doc.qt.io/archives/qt-4.8/qcolor.html # from https://doc.qt.io/archives/qt-4.8/qcolor.html
# typedef QRgb # typedef QRgb
# An ARGB quadruplet on the format #AARRGGBB, # An ARGB quadruplet on the format #AARRGGBB,
# equivalent to an unsigned int. # equivalent to an unsigned int.
if ImageQt.qt_version == "5": if ImageQt.qt_version == "5":
from PyQt5.QtGui import qRgb from PyQt5.QtGui import qRgb
elif ImageQt.qt_version == "side2": elif ImageQt.qt_version == "side2":
from PySide2.QtGui import qRgb from PySide2.QtGui import qRgb
assert qRgb(0, 0, 0) == qRgba(0, 0, 0, 255) assert qRgb(0, 0, 0) == qRgba(0, 0, 0, 255)
def checkrgb(r, g, b): def checkrgb(r, g, b):
val = ImageQt.rgb(r, g, b) val = ImageQt.rgb(r, g, b)
val = val % 2 ** 24 # drop the alpha val = val % 2 ** 24 # drop the alpha
assert val >> 16 == r assert val >> 16 == r
assert ((val >> 8) % 2 ** 8) == g assert ((val >> 8) % 2 ** 8) == g
assert val % 2 ** 8 == b assert val % 2 ** 8 == b
checkrgb(0, 0, 0) checkrgb(0, 0, 0)
checkrgb(255, 0, 0) checkrgb(255, 0, 0)
checkrgb(0, 255, 0) checkrgb(0, 255, 0)
checkrgb(0, 0, 255) checkrgb(0, 0, 255)
def test_image(self):
for mode in ("1", "RGB", "RGBA", "L", "P"): @pytest.mark.skipif(not ImageQt.qt_is_installed, reason="Qt bindings are not installed")
ImageQt.ImageQt(hopper(mode)) def test_image():
for mode in ("1", "RGB", "RGBA", "L", "P"):
ImageQt.ImageQt(hopper(mode))

View File

@ -1,10 +1,10 @@
from PIL import ImageQt from PIL import ImageQt
from .helper import PillowTestCase, assert_image_equal, hopper from .helper import assert_image_equal, hopper
from .test_imageqt import PillowQPixmapTestCase from .test_imageqt import PillowQPixmapTestCase
class TestFromQPixmap(PillowQPixmapTestCase, PillowTestCase): class TestFromQPixmap(PillowQPixmapTestCase):
def roundtrip(self, expected): def roundtrip(self, expected):
result = ImageQt.fromqpixmap(ImageQt.toqpixmap(expected)) result = ImageQt.fromqpixmap(ImageQt.toqpixmap(expected))
# Qt saves all pixmaps as rgb # Qt saves all pixmaps as rgb

View File

@ -1,7 +1,11 @@
import pytest
from PIL import Image, ImageQt from PIL import Image, ImageQt
from .helper import PillowTestCase, assert_image_equal, hopper from .helper import assert_image_equal, hopper
from .test_imageqt import PillowQtTestCase
pytestmark = pytest.mark.skipif(
not ImageQt.qt_is_installed, reason="Qt bindings are not installed"
)
if ImageQt.qt_is_installed: if ImageQt.qt_is_installed:
from PIL.ImageQt import QImage from PIL.ImageQt import QImage
@ -14,43 +18,43 @@ if ImageQt.qt_is_installed:
from PySide2.QtWidgets import QWidget, QHBoxLayout, QLabel, QApplication from PySide2.QtWidgets import QWidget, QHBoxLayout, QLabel, QApplication
class TestToQImage(PillowQtTestCase, PillowTestCase): def test_sanity(tmp_path):
def test_sanity(self): for mode in ("RGB", "RGBA", "L", "P", "1"):
for mode in ("RGB", "RGBA", "L", "P", "1"): src = hopper(mode)
src = hopper(mode) data = ImageQt.toqimage(src)
data = ImageQt.toqimage(src)
assert isinstance(data, QImage) assert isinstance(data, QImage)
assert not data.isNull() assert not data.isNull()
# reload directly from the qimage # reload directly from the qimage
rt = ImageQt.fromqimage(data) rt = ImageQt.fromqimage(data)
if mode in ("L", "P", "1"): if mode in ("L", "P", "1"):
assert_image_equal(rt, src.convert("RGB")) assert_image_equal(rt, src.convert("RGB"))
else: else:
assert_image_equal(rt, src) assert_image_equal(rt, src)
if mode == "1": if mode == "1":
# BW appears to not save correctly on QT4 and QT5 # BW appears to not save correctly on QT4 and QT5
# kicks out errors on console: # kicks out errors on console:
# libpng warning: Invalid color type/bit depth combination # libpng warning: Invalid color type/bit depth combination
# in IHDR # in IHDR
# libpng error: Invalid IHDR data # libpng error: Invalid IHDR data
continue continue
# Test saving the file # Test saving the file
tempfile = self.tempfile("temp_{}.png".format(mode)) tempfile = str(tmp_path / "temp_{}.png".format(mode))
data.save(tempfile) data.save(tempfile)
# Check that it actually worked. # Check that it actually worked.
with Image.open(tempfile) as reloaded: with Image.open(tempfile) as reloaded:
assert_image_equal(reloaded, src) assert_image_equal(reloaded, src)
def test_segfault(self):
app = QApplication([]) def test_segfault():
ex = Example() app = QApplication([])
assert app # Silence warning ex = Example()
assert ex # Silence warning assert app # Silence warning
assert ex # Silence warning
if ImageQt.qt_is_installed: if ImageQt.qt_is_installed:

View File

@ -1,14 +1,14 @@
from PIL import ImageQt from PIL import ImageQt
from .helper import PillowTestCase, hopper from .helper import hopper
from .test_imageqt import PillowQPixmapTestCase from .test_imageqt import PillowQPixmapTestCase
if ImageQt.qt_is_installed: if ImageQt.qt_is_installed:
from PIL.ImageQt import QPixmap from PIL.ImageQt import QPixmap
class TestToQPixmap(PillowQPixmapTestCase, PillowTestCase): class TestToQPixmap(PillowQPixmapTestCase):
def test_sanity(self): def test_sanity(self, tmp_path):
for mode in ("1", "RGB", "RGBA", "L", "P"): for mode in ("1", "RGB", "RGBA", "L", "P"):
data = ImageQt.toqpixmap(hopper(mode)) data = ImageQt.toqpixmap(hopper(mode))
@ -16,5 +16,5 @@ class TestToQPixmap(PillowQPixmapTestCase, PillowTestCase):
assert not data.isNull() assert not data.isNull()
# Test saving the file # Test saving the file
tempfile = self.tempfile("temp_{}.png".format(mode)) tempfile = str(tmp_path / "temp_{}.png".format(mode))
data.save(tempfile) data.save(tempfile)

View File

@ -8,6 +8,12 @@ codecov:
comment: false comment: false
coverage:
status:
project:
default:
threshold: 0.01%
# Matches 'omit:' in .coveragerc # Matches 'omit:' in .coveragerc
ignore: ignore:
- "Tests/32bit_segfault_check.py" - "Tests/32bit_segfault_check.py"