diff --git a/.github/workflows/test-docker.yml b/.github/workflows/test-docker.yml index 2ecc27460..051f7865e 100644 --- a/.github/workflows/test-docker.yml +++ b/.github/workflows/test-docker.yml @@ -21,7 +21,6 @@ jobs: centos-7-amd64, centos-8-amd64, debian-10-buster-x86, - fedora-32-amd64, fedora-33-amd64, ubuntu-18.04-bionic-amd64, ubuntu-20.04-focal-amd64, diff --git a/CHANGES.rst b/CHANGES.rst index 84eb79ec1..ea1eee884 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -2,6 +2,27 @@ Changelog (Pillow) ================== +8.3.0 (unreleased) +------------------ + +- Stop decoding BC1 punchthrough alpha in BC2&3 #4144 + [jansol] + +- Use zero if GIF background color index is missing #5390 + [radarhere] + +- Fixed ensuring that GIF previous frame was loaded #5386 + [radarhere] + +- Valgrind fixes #5397 + [wiredfool] + +- Round down the radius in rounded_rectangle #5382 + [radarhere] + +- Fixed reading uncompressed RGB data from DDS #5383 + [radarhere] + 8.2.0 (2021-04-01) ------------------ diff --git a/Makefile b/Makefile index 53eaa0566..af3059f34 100644 --- a/Makefile +++ b/Makefile @@ -102,6 +102,13 @@ sdist: test: pytest -qq +.PHONY: valgrind +valgrind: + python3 -c "import pytest_valgrind" || pip3 install pytest-valgrind + PYTHONMALLOC=malloc valgrind --suppressions=Tests/oss-fuzz/python.supp --leak-check=no \ + --log-file=/tmp/valgrind-output \ + python3 -m pytest --no-memcheck -vv --valgrind --valgrind-log=/tmp/valgrind-output + .PHONY: readme readme: python3 setup.py --long-description | markdown2 > .long-description.html && open .long-description.html diff --git a/Tests/32bit_segfault_check.py b/Tests/32bit_segfault_check.py index 26a91d5cd..e19cdf7a9 100755 --- a/Tests/32bit_segfault_check.py +++ b/Tests/32bit_segfault_check.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 import sys diff --git a/Tests/check_fli_oob.py b/Tests/check_fli_oob.py index 739ad224e..6b63a6826 100644 --- a/Tests/check_fli_oob.py +++ b/Tests/check_fli_oob.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 from PIL import Image diff --git a/Tests/check_imaging_leaks.py b/Tests/check_imaging_leaks.py index 407f3ea80..d07082aba 100755 --- a/Tests/check_imaging_leaks.py +++ b/Tests/check_imaging_leaks.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 import pytest from PIL import Image diff --git a/Tests/check_jp2_overflow.py b/Tests/check_jp2_overflow.py index a7a343c98..f81a360ce 100755 --- a/Tests/check_jp2_overflow.py +++ b/Tests/check_jp2_overflow.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 # Reproductions/tests for OOB read errors in FliDecode.c diff --git a/Tests/conftest.py b/Tests/conftest.py index dd37e7ce5..66da7593c 100644 --- a/Tests/conftest.py +++ b/Tests/conftest.py @@ -13,6 +13,11 @@ def pytest_report_header(config): def pytest_configure(config): + config.addinivalue_line( + "markers", + "pil_noop_mark: A conditional mark where nothing special happens", + ) + # We're marking some tests to ignore valgrind errors and XFAIL them. # Ensure that the mark is defined # even in cases where pytest-valgrind isn't installed diff --git a/Tests/createfontdatachunk.py b/Tests/createfontdatachunk.py index 011bb0bed..e318eb732 100755 --- a/Tests/createfontdatachunk.py +++ b/Tests/createfontdatachunk.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 import base64 import os diff --git a/Tests/helper.py b/Tests/helper.py index 59ba0dd15..8504993fb 100644 --- a/Tests/helper.py +++ b/Tests/helper.py @@ -173,6 +173,21 @@ def skip_unless_feature_version(feature, version_required, reason=None): return pytest.mark.skipif(version_available < version_required, reason=reason) +def mark_if_feature_version(mark, feature, version_blacklist, reason=None): + if not features.check(feature): + return pytest.mark.pil_noop_mark() + if reason is None: + reason = f"{feature} is {version_blacklist}" + version_required = parse_version(version_blacklist) + version_available = parse_version(features.version(feature)) + if ( + version_available.major == version_required.major + and version_available.minor == version_required.minor + ): + return mark(reason=reason) + return pytest.mark.pil_noop_mark() + + @pytest.mark.skipif(sys.platform.startswith("win32"), reason="Requires Unix or macOS") class PillowLeakTestCase: # requires unix/macOS diff --git a/Tests/images/dxt5-colorblock-alpha-issue-4142.dds b/Tests/images/dxt5-colorblock-alpha-issue-4142.dds new file mode 100644 index 000000000..905527ead Binary files /dev/null and b/Tests/images/dxt5-colorblock-alpha-issue-4142.dds differ diff --git a/Tests/images/hopper.dds b/Tests/images/hopper.dds new file mode 100644 index 000000000..8b9af9ed9 Binary files /dev/null and b/Tests/images/hopper.dds differ diff --git a/Tests/images/imagedraw_rounded_rectangle_non_integer_radius_given.png b/Tests/images/imagedraw_rounded_rectangle_non_integer_radius_given.png new file mode 100644 index 000000000..59e55b2a1 Binary files /dev/null and b/Tests/images/imagedraw_rounded_rectangle_non_integer_radius_given.png differ diff --git a/Tests/images/imagedraw_rounded_rectangle_non_integer_radius_height.png b/Tests/images/imagedraw_rounded_rectangle_non_integer_radius_height.png new file mode 100644 index 000000000..c4e54896b Binary files /dev/null and b/Tests/images/imagedraw_rounded_rectangle_non_integer_radius_height.png differ diff --git a/Tests/images/imagedraw_rounded_rectangle_non_integer_radius_width.png b/Tests/images/imagedraw_rounded_rectangle_non_integer_radius_width.png new file mode 100644 index 000000000..6b0f11fa6 Binary files /dev/null and b/Tests/images/imagedraw_rounded_rectangle_non_integer_radius_width.png differ diff --git a/Tests/images/missing_background.gif b/Tests/images/missing_background.gif new file mode 100644 index 000000000..550d68d81 Binary files /dev/null and b/Tests/images/missing_background.gif differ diff --git a/Tests/images/missing_background_first_frame.gif b/Tests/images/missing_background_first_frame.gif new file mode 100644 index 000000000..be2c95b99 Binary files /dev/null and b/Tests/images/missing_background_first_frame.gif differ diff --git a/Tests/images/uncompressed_rgb.png b/Tests/images/uncompressed_rgb.png index 50bca09ee..f02b50f6f 100644 Binary files a/Tests/images/uncompressed_rgb.png and b/Tests/images/uncompressed_rgb.png differ diff --git a/Tests/oss-fuzz/python.supp b/Tests/oss-fuzz/python.supp new file mode 100644 index 000000000..94cc87db9 --- /dev/null +++ b/Tests/oss-fuzz/python.supp @@ -0,0 +1,16 @@ +{ + + Memcheck:Cond + ... + fun:encode_current_locale +} + + +{ + + Memcheck:Cond + fun:inflate + fun:ZIPDecode + fun:_TIFFReadEncodedTileAndAllocBuffer + ... +} diff --git a/Tests/oss-fuzz/test_fuzzers.py b/Tests/oss-fuzz/test_fuzzers.py index a243c0260..2ac1a0d7f 100644 --- a/Tests/oss-fuzz/test_fuzzers.py +++ b/Tests/oss-fuzz/test_fuzzers.py @@ -2,12 +2,19 @@ import subprocess import sys import fuzzers +import packaging import pytest -from PIL import Image +from PIL import Image, features if sys.platform.startswith("win32"): pytest.skip("Fuzzer is linux only", allow_module_level=True) +if features.check("libjpeg_turbo"): + version = packaging.version.parse(features.version("libjpeg_turbo")) + if version.major == 2 and version.minor == 0: + pytestmark = pytest.mark.valgrind_known_error( + reason="Known failing with libjpeg_turbo 2.0" + ) @pytest.mark.parametrize( diff --git a/Tests/test_file_dds.py b/Tests/test_file_dds.py index 682cd048b..bb9a5a074 100644 --- a/Tests/test_file_dds.py +++ b/Tests/test_file_dds.py @@ -14,7 +14,8 @@ TEST_FILE_DX10_BC7 = "Tests/images/bc7-argb-8bpp_MipMaps-1.dds" TEST_FILE_DX10_BC7_UNORM_SRGB = "Tests/images/DXGI_FORMAT_BC7_UNORM_SRGB.dds" TEST_FILE_DX10_R8G8B8A8 = "Tests/images/argb-32bpp_MipMaps-1.dds" TEST_FILE_DX10_R8G8B8A8_UNORM_SRGB = "Tests/images/DXGI_FORMAT_R8G8B8A8_UNORM_SRGB.dds" -TEST_FILE_UNCOMPRESSED_RGB = "Tests/images/uncompressed_rgb.dds" +TEST_FILE_UNCOMPRESSED_RGB = "Tests/images/hopper.dds" +TEST_FILE_UNCOMPRESSED_RGB_WITH_ALPHA = "Tests/images/uncompressed_rgb.dds" def test_sanity_dxt1(): @@ -124,37 +125,44 @@ def test_unimplemented_dxgi_format(): def test_uncompressed_rgb(): """Check uncompressed RGB images can be opened""" + # convert -format dds -define dds:compression=none hopper.jpg hopper.dds with Image.open(TEST_FILE_UNCOMPRESSED_RGB) as im: - im.load() + assert im.format == "DDS" + assert im.mode == "RGB" + assert im.size == (128, 128) + assert_image_equal_tofile(im, "Tests/images/hopper.png") + + # Test image with alpha + with Image.open(TEST_FILE_UNCOMPRESSED_RGB_WITH_ALPHA) as im: assert im.format == "DDS" assert im.mode == "RGBA" assert im.size == (800, 600) assert_image_equal_tofile( - im, TEST_FILE_UNCOMPRESSED_RGB.replace(".dds", ".png") + im, TEST_FILE_UNCOMPRESSED_RGB_WITH_ALPHA.replace(".dds", ".png") ) -def test__validate_true(): +def test__accept_true(): """Check valid prefix""" # Arrange prefix = b"DDS etc" # Act - output = DdsImagePlugin._validate(prefix) + output = DdsImagePlugin._accept(prefix) # Assert assert output -def test__validate_false(): +def test__accept_false(): """Check invalid prefix""" # Arrange prefix = b"something invalid" # Act - output = DdsImagePlugin._validate(prefix) + output = DdsImagePlugin._accept(prefix) # Assert assert not output @@ -187,6 +195,21 @@ def test_short_file(): short_file() +def test_dxt5_colorblock_alpha_issue_4142(): + """ Check that colorblocks are decoded correctly in DXT5""" + + with Image.open("Tests/images/dxt5-colorblock-alpha-issue-4142.dds") as im: + px = im.getpixel((0, 0)) + assert px[0] != 0 + assert px[1] != 0 + assert px[2] != 0 + + px = im.getpixel((1, 0)) + assert px[0] != 0 + assert px[1] != 0 + assert px[2] != 0 + + def test_unimplemented_pixel_format(): with pytest.raises(NotImplementedError): with Image.open("Tests/images/unimplemented_pixel_format.dds"): diff --git a/Tests/test_file_eps.py b/Tests/test_file_eps.py index 7caac34c3..1994a124c 100644 --- a/Tests/test_file_eps.py +++ b/Tests/test_file_eps.py @@ -8,6 +8,7 @@ from .helper import ( assert_image_similar, assert_image_similar_tofile, hopper, + mark_if_feature_version, skip_unless_feature, ) @@ -64,7 +65,9 @@ def test_invalid_file(): EpsImagePlugin.EpsImageFile(invalid_file) -@pytest.mark.valgrind_known_error(reason="Known Failing") +@mark_if_feature_version( + pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" +) @pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") def test_cmyk(): with Image.open("Tests/images/pil_sample_cmyk.eps") as cmyk_image: diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index 2ce191857..420c498ed 100644 --- a/Tests/test_file_gif.py +++ b/Tests/test_file_gif.py @@ -352,6 +352,17 @@ def test_dispose_previous(): pass +def test_previous_frame_loaded(): + with Image.open("Tests/images/dispose_none.gif") as img: + img.load() + img.seek(1) + img.load() + img.seek(2) + with Image.open("Tests/images/dispose_none.gif") as img_skipped: + img_skipped.seek(2) + assert_image_equal(img_skipped, img) + + def test_save_dispose(tmp_path): out = str(tmp_path / "temp.gif") im_list = [ @@ -864,3 +875,11 @@ def test_extents(): assert im.size == (100, 100) im.seek(1) assert im.size == (150, 150) + + +def test_missing_background(): + # The Global Color Table Flag isn't set, so there is no background color index, + # but the disposal method is "Restore to background color" + with Image.open("Tests/images/missing_background.gif") as im: + im.seek(1) + assert_image_equal_tofile(im, "Tests/images/missing_background_first_frame.gif") diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index 64f509a95..18837156a 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -24,6 +24,7 @@ from .helper import ( djpeg_available, hopper, is_win32, + mark_if_feature_version, skip_unless_feature, ) @@ -116,7 +117,9 @@ class TestFileJpeg: assert test(100, 200) == (100, 200) assert test(0) is None # square pixels - @pytest.mark.valgrind_known_error(reason="Known Failing") + @mark_if_feature_version( + pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" + ) def test_icc(self, tmp_path): # Test ICC support with Image.open("Tests/images/rgb.jpg") as im1: @@ -156,7 +159,9 @@ class TestFileJpeg: test(ImageFile.MAXBLOCK + 1) # full buffer block plus one byte test(ImageFile.MAXBLOCK * 4 + 3) # large block - @pytest.mark.valgrind_known_error(reason="Known Failing") + @mark_if_feature_version( + pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" + ) def test_large_icc_meta(self, tmp_path): # https://github.com/python-pillow/Pillow/issues/148 # Sometimes the meta data on the icc_profile block is bigger than @@ -423,7 +428,9 @@ class TestFileJpeg: with Image.open(filename): pass - @pytest.mark.valgrind_known_error(reason="Known Failing") + @mark_if_feature_version( + pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" + ) def test_truncated_jpeg_should_read_all_the_data(self): filename = "Tests/images/truncated_jpeg.jpg" ImageFile.LOAD_TRUNCATED_IMAGES = True @@ -442,7 +449,9 @@ class TestFileJpeg: with pytest.raises(OSError): im.load() - @pytest.mark.valgrind_known_error(reason="Known Failing") + @mark_if_feature_version( + pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" + ) def test_qtables(self, tmp_path): def _n_qtables_helper(n, test_file): with Image.open(test_file) as im: @@ -726,7 +735,9 @@ class TestFileJpeg: # OSError for unidentified image. assert im.info.get("dpi") == (72, 72) - @pytest.mark.valgrind_known_error(reason="Known Failing") + @mark_if_feature_version( + pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" + ) def test_exif_x_resolution(self, tmp_path): with Image.open("Tests/images/flower.jpg") as im: exif = im.getexif() @@ -757,7 +768,9 @@ class TestFileJpeg: # Act / Assert assert im._getexif()[306] == "2017:03:13 23:03:09" - @pytest.mark.valgrind_known_error(reason="Backtrace in Python Core") + @mark_if_feature_version( + pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" + ) def test_photoshop(self): with Image.open("Tests/images/photoshop-200dpi.jpg") as im: assert im.info["photoshop"][0x03ED] == { diff --git a/Tests/test_file_libtiff.py b/Tests/test_file_libtiff.py index 22b641b5f..c0d09d6ce 100644 --- a/Tests/test_file_libtiff.py +++ b/Tests/test_file_libtiff.py @@ -17,6 +17,7 @@ from .helper import ( assert_image_similar, assert_image_similar_tofile, hopper, + mark_if_feature_version, skip_unless_feature, ) @@ -822,13 +823,17 @@ class TestFileLibTiff(LibTiffTestCase): with Image.open(infile) as im: assert_image_similar_tofile(im, "Tests/images/pil_sample_cmyk.jpg", 0.5) - @pytest.mark.valgrind_known_error(reason="Known Failing") + @mark_if_feature_version( + pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" + ) def test_strip_ycbcr_jpeg_2x2_sampling(self): infile = "Tests/images/tiff_strip_ycbcr_jpeg_2x2_sampling.tif" with Image.open(infile) as im: assert_image_similar_tofile(im, "Tests/images/flower.jpg", 0.5) - @pytest.mark.valgrind_known_error(reason="Known Failing") + @mark_if_feature_version( + pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" + ) def test_strip_ycbcr_jpeg_1x1_sampling(self): infile = "Tests/images/tiff_strip_ycbcr_jpeg_1x1_sampling.tif" with Image.open(infile) as im: @@ -839,13 +844,17 @@ class TestFileLibTiff(LibTiffTestCase): with Image.open(infile) as im: assert_image_similar_tofile(im, "Tests/images/pil_sample_cmyk.jpg", 0.5) - @pytest.mark.valgrind_known_error(reason="Known Failing") + @mark_if_feature_version( + pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" + ) def test_tiled_ycbcr_jpeg_1x1_sampling(self): infile = "Tests/images/tiff_tiled_ycbcr_jpeg_1x1_sampling.tif" with Image.open(infile) as im: assert_image_equal_tofile(im, "Tests/images/flower2.jpg") - @pytest.mark.valgrind_known_error(reason="Known Failing") + @mark_if_feature_version( + pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" + ) def test_tiled_ycbcr_jpeg_2x2_sampling(self): infile = "Tests/images/tiff_tiled_ycbcr_jpeg_2x2_sampling.tif" with Image.open(infile) as im: diff --git a/Tests/test_file_pdf.py b/Tests/test_file_pdf.py index e5bba483a..005acdfdd 100644 --- a/Tests/test_file_pdf.py +++ b/Tests/test_file_pdf.py @@ -8,7 +8,7 @@ import pytest from PIL import Image, PdfParser -from .helper import hopper +from .helper import hopper, mark_if_feature_version def helper_save_as_pdf(tmp_path, mode, **kwargs): @@ -85,7 +85,9 @@ def test_unsupported_mode(tmp_path): im.save(outfile) -@pytest.mark.valgrind_known_error(reason="Known Failing") +@mark_if_feature_version( + pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" +) def test_save_all(tmp_path): # Single frame image helper_save_as_pdf(tmp_path, "RGB", save_all=True) @@ -286,3 +288,13 @@ def test_pdf_append_to_bytesio(): f = io.BytesIO(f.getvalue()) im.save(f, format="PDF", append=True) assert len(f.getvalue()) > initial_size + + +@pytest.mark.timeout(1) +def test_redos(): + malicious = b" trailer<<>>" + b"\n" * 3456 + + # This particular exception isn't relevant here. + # The important thing is it doesn't timeout, cause a ReDoS (CVE-2021-25292). + with pytest.raises(PdfParser.PdfFormatError): + PdfParser.PdfParser(buf=malicious) diff --git a/Tests/test_file_png.py b/Tests/test_file_png.py index bbf5f5772..a1234b46a 100644 --- a/Tests/test_file_png.py +++ b/Tests/test_file_png.py @@ -13,6 +13,7 @@ from .helper import ( hopper, is_big_endian, is_win32, + mark_if_feature_version, skip_unless_feature, ) @@ -679,7 +680,9 @@ class TestFilePng: exif = reloaded._getexif() assert exif[274] == 1 - @pytest.mark.valgrind_known_error(reason="Known Failing") + @mark_if_feature_version( + pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" + ) def test_exif_from_jpg(self, tmp_path): with Image.open("Tests/images/pil_sample_rgb.jpg") as im: test_file = str(tmp_path / "temp.png") diff --git a/Tests/test_file_webp_metadata.py b/Tests/test_file_webp_metadata.py index cb133e2c5..e6d6fc63f 100644 --- a/Tests/test_file_webp_metadata.py +++ b/Tests/test_file_webp_metadata.py @@ -4,7 +4,7 @@ import pytest from PIL import Image -from .helper import skip_unless_feature +from .helper import mark_if_feature_version, skip_unless_feature pytestmark = [ skip_unless_feature("webp"), @@ -41,7 +41,9 @@ def test_read_exif_metadata_without_prefix(): assert exif[305] == "Adobe Photoshop CS6 (Macintosh)" -@pytest.mark.valgrind_known_error(reason="Known Failing") +@mark_if_feature_version( + pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" +) def test_write_exif_metadata(): file_path = "Tests/images/flower.jpg" test_buffer = BytesIO() @@ -74,7 +76,9 @@ def test_read_icc_profile(): assert icc == expected_icc -@pytest.mark.valgrind_known_error(reason="Known Failing") +@mark_if_feature_version( + pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" +) def test_write_icc_metadata(): file_path = "Tests/images/flower2.jpg" test_buffer = BytesIO() @@ -92,7 +96,9 @@ def test_write_icc_metadata(): assert webp_icc_profile == expected_icc_profile, "Webp ICC didn't match" -@pytest.mark.valgrind_known_error(reason="Known Failing") +@mark_if_feature_version( + pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" +) def test_read_no_exif(): file_path = "Tests/images/flower.jpg" test_buffer = BytesIO() diff --git a/Tests/test_image.py b/Tests/test_image.py index 30d093e15..82efefc1e 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -16,6 +16,7 @@ from .helper import ( assert_not_all_same, hopper, is_win32, + mark_if_feature_version, skip_unless_feature, ) @@ -662,7 +663,9 @@ class TestImage: assert not fp.closed - @pytest.mark.valgrind_known_error(reason="Known Failing") + @mark_if_feature_version( + pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" + ) def test_exif_jpeg(self, tmp_path): with Image.open("Tests/images/exif-72dpi-int.jpg") as im: # Little endian exif = im.getexif() diff --git a/Tests/test_image_resample.py b/Tests/test_image_resample.py index 69449198e..8bf2ce916 100644 --- a/Tests/test_image_resample.py +++ b/Tests/test_image_resample.py @@ -4,7 +4,12 @@ import pytest from PIL import Image, ImageDraw -from .helper import assert_image_equal, assert_image_similar, hopper +from .helper import ( + assert_image_equal, + assert_image_similar, + hopper, + mark_if_feature_version, +) class TestImagingResampleVulnerability: @@ -455,7 +460,9 @@ class TestCoreResampleBox: tiled.paste(tile, (x0, y0)) return tiled - @pytest.mark.valgrind_known_error(reason="Known Failing") + @mark_if_feature_version( + pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" + ) def test_tiles(self): with Image.open("Tests/images/flower.jpg") as im: assert im.size == (480, 360) @@ -466,7 +473,9 @@ class TestCoreResampleBox: tiled = self.resize_tiled(im, dst_size, *tiles) assert_image_similar(reference, tiled, 0.01) - @pytest.mark.valgrind_known_error(reason="Known Failing") + @mark_if_feature_version( + pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" + ) def test_subsample(self): # This test shows advantages of the subpixel resizing # after supersampling (e.g. during JPEG decoding). diff --git a/Tests/test_image_thumbnail.py b/Tests/test_image_thumbnail.py index 6911ce460..dd140955d 100644 --- a/Tests/test_image_thumbnail.py +++ b/Tests/test_image_thumbnail.py @@ -88,6 +88,7 @@ def test_no_resize(): assert im.size == (64, 64) +# valgrind test is failing with memory allocated in libjpeg @pytest.mark.valgrind_known_error(reason="Known Failing") def test_DCT_scaling_edges(): # Make an image with red borders and size (N * 8) + 1 to cross DCT grid diff --git a/Tests/test_imagedraw.py b/Tests/test_imagedraw.py index 06c5b2503..7f31e1aaf 100644 --- a/Tests/test_imagedraw.py +++ b/Tests/test_imagedraw.py @@ -722,6 +722,29 @@ def test_rounded_rectangle(xy): assert_image_equal_tofile(im, "Tests/images/imagedraw_rounded_rectangle.png") +@pytest.mark.parametrize( + "xy, radius, type", + [ + ((10, 20, 190, 180), 30.5, "given"), + ((10, 10, 181, 190), 90, "width"), + ((10, 20, 190, 181), 85, "height"), + ], +) +def test_rounded_rectangle_non_integer_radius(xy, radius, type): + # Arrange + im = Image.new("RGB", (200, 200)) + draw = ImageDraw.Draw(im) + + # Act + draw.rounded_rectangle(xy, radius, fill="red", outline="green", width=5) + + # Assert + assert_image_equal_tofile( + im, + "Tests/images/imagedraw_rounded_rectangle_non_integer_radius_" + type + ".png", + ) + + def test_rounded_rectangle_zero_radius(): # Arrange im = Image.new("RGB", (W, H)) diff --git a/Tests/test_sgi_crash.py b/Tests/test_sgi_crash.py index d4ddc12f9..f9eaf9b19 100644 --- a/Tests/test_sgi_crash.py +++ b/Tests/test_sgi_crash.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python import pytest from PIL import Image diff --git a/docs/Guardfile b/docs/Guardfile index 16f731611..b689b079a 100755 --- a/docs/Guardfile +++ b/docs/Guardfile @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 from livereload.compiler import shell from livereload.task import Task diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index c67f8fb8f..eb10a51fa 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -1242,8 +1242,10 @@ The :py:meth:`~PIL.Image.Image.save` method can take the following keyword argum .. versionadded:: 3.0.0 **append_images** - A list of images to append as additional pages. Each of the - images in the list can be single or multiframe images. + A list of :py:class:`PIL.Image.Image` objects to append as additional pages. Each + of the images in the list can be single or multiframe images. The ``save_all`` + parameter must be present and set to ``True`` in conjunction with + ``append_images``. .. versionadded:: 4.2.0 diff --git a/docs/installation.rst b/docs/installation.rst index cc9c51039..be0ed84a6 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -452,8 +452,6 @@ These platforms are built and tested for every change. +----------------------------------+--------------------------+-----------------------+ | Debian 10 Buster | 3.7 |x86 | +----------------------------------+--------------------------+-----------------------+ -| Fedora 32 | 3.8 |x86-64 | -+----------------------------------+--------------------------+-----------------------+ | Fedora 33 | 3.9 |x86-64 | +----------------------------------+--------------------------+-----------------------+ | macOS 10.15 Catalina | 3.6, 3.7, 3.8, 3.9, PyPy3|x86-64 | diff --git a/selftest.py b/selftest.py index 7e08d183b..8d77cc5a9 100755 --- a/selftest.py +++ b/selftest.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 # minimal sanity check import sys diff --git a/setup.py b/setup.py index 52babbc6b..c3b9eee41 100755 --- a/setup.py +++ b/setup.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 # > pyroma . # ------------------------------ # Checking . diff --git a/src/PIL/BlpImagePlugin.py b/src/PIL/BlpImagePlugin.py index e07474621..7b78597b4 100644 --- a/src/PIL/BlpImagePlugin.py +++ b/src/PIL/BlpImagePlugin.py @@ -417,9 +417,11 @@ class BLP2Decoder(_BLPBaseDecoder): self.set_as_raw(bytes(data)) -Image.register_open( - BlpImageFile.format, BlpImageFile, lambda p: p[:4] in (b"BLP1", b"BLP2") -) +def _accept(prefix): + return prefix[:4] in (b"BLP1", b"BLP2") + + +Image.register_open(BlpImageFile.format, BlpImageFile, _accept) Image.register_extension(BlpImageFile.format, ".blp") Image.register_decoder("BLP1", BLP1Decoder) diff --git a/src/PIL/DdsImagePlugin.py b/src/PIL/DdsImagePlugin.py index df2d0060c..1a7fe0035 100644 --- a/src/PIL/DdsImagePlugin.py +++ b/src/PIL/DdsImagePlugin.py @@ -133,9 +133,11 @@ class DdsImageFile(ImageFile.ImageFile): rawmode = "" if bitcount == 32: rawmode += masks[0xFF000000] + else: + self.mode = "RGB" rawmode += masks[0xFF0000] + masks[0xFF00] + masks[0xFF] - self.tile = [("raw", (0, 0) + self.size, 0, (rawmode, 0, 1))] + self.tile = [("raw", (0, 0) + self.size, 0, (rawmode[::-1], 0, 1))] else: data_start = header_size + 4 n = 0 @@ -182,9 +184,9 @@ class DdsImageFile(ImageFile.ImageFile): pass -def _validate(prefix): +def _accept(prefix): return prefix[:4] == b"DDS " -Image.register_open(DdsImageFile.format, DdsImageFile, _validate) +Image.register_open(DdsImageFile.format, DdsImageFile, _accept) Image.register_extension(DdsImageFile.format, ".dds") diff --git a/src/PIL/FtexImagePlugin.py b/src/PIL/FtexImagePlugin.py index 900661238..3b169038c 100644 --- a/src/PIL/FtexImagePlugin.py +++ b/src/PIL/FtexImagePlugin.py @@ -98,9 +98,9 @@ class FtexImageFile(ImageFile.ImageFile): pass -def _validate(prefix): +def _accept(prefix): return prefix[:4] == MAGIC -Image.register_open(FtexImageFile.format, FtexImageFile, _validate) +Image.register_open(FtexImageFile.format, FtexImageFile, _accept) Image.register_extensions(FtexImageFile.format, [".ftc", ".ftu"]) diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py index c19d03939..dbabdc59d 100644 --- a/src/PIL/GifImagePlugin.py +++ b/src/PIL/GifImagePlugin.py @@ -274,14 +274,14 @@ class GifImageFile(ImageFile.ImageFile): if frame_transparency is not None: bg = frame_transparency else: - bg = self.info["background"] + bg = self.info.get("background", 0) self.dispose = Image.core.fill("P", dispose_size, bg) else: # replace with previous contents if self.im: # only dispose the extent in this frame self.dispose = self._crop(self.im, self.dispose_extent) - except (AttributeError, KeyError): + except AttributeError: pass if interlace is not None: diff --git a/src/PIL/IcnsImagePlugin.py b/src/PIL/IcnsImagePlugin.py index ca6a0adad..a38f2fc5c 100644 --- a/src/PIL/IcnsImagePlugin.py +++ b/src/PIL/IcnsImagePlugin.py @@ -359,7 +359,11 @@ def _save(im, fp, filename): fp.write(f.read()) -Image.register_open(IcnsImageFile.format, IcnsImageFile, lambda x: x[:4] == b"icns") +def _accept(prefix): + return prefix[:4] == b"icns" + + +Image.register_open(IcnsImageFile.format, IcnsImageFile, _accept) Image.register_extension(IcnsImageFile.format, ".icns") if sys.platform == "darwin": diff --git a/src/PIL/ImageDraw.py b/src/PIL/ImageDraw.py index 8988e4233..0573eadac 100644 --- a/src/PIL/ImageDraw.py +++ b/src/PIL/ImageDraw.py @@ -282,6 +282,7 @@ class ImageDraw: # If the corners have no curve, that is a rectangle return self.rectangle(xy, fill, outline, width) + r = d // 2 ink, fill = self._getink(outline, fill) def draw_corners(pieslice): @@ -315,36 +316,28 @@ class ImageDraw: draw_corners(True) if full_x: - self.draw.draw_rectangle( - (x0, y0 + d / 2 + 1, x1, y1 - d / 2 - 1), fill, 1 - ) + self.draw.draw_rectangle((x0, y0 + r + 1, x1, y1 - r - 1), fill, 1) else: - self.draw.draw_rectangle( - (x0 + d / 2 + 1, y0, x1 - d / 2 - 1, y1), fill, 1 - ) + self.draw.draw_rectangle((x0 + r + 1, y0, x1 - r - 1, y1), fill, 1) if not full_x and not full_y: - self.draw.draw_rectangle( - (x0, y0 + d / 2 + 1, x0 + d / 2, y1 - d / 2 - 1), fill, 1 - ) - self.draw.draw_rectangle( - (x1 - d / 2, y0 + d / 2 + 1, x1, y1 - d / 2 - 1), fill, 1 - ) + self.draw.draw_rectangle((x0, y0 + r + 1, x0 + r, y1 - r - 1), fill, 1) + self.draw.draw_rectangle((x1 - r, y0 + r + 1, x1, y1 - r - 1), fill, 1) if ink is not None and ink != fill and width != 0: draw_corners(False) if not full_x: self.draw.draw_rectangle( - (x0 + d / 2 + 1, y0, x1 - d / 2 - 1, y0 + width - 1), ink, 1 + (x0 + r + 1, y0, x1 - r - 1, y0 + width - 1), ink, 1 ) self.draw.draw_rectangle( - (x0 + d / 2 + 1, y1 - width + 1, x1 - d / 2 - 1, y1), ink, 1 + (x0 + r + 1, y1 - width + 1, x1 - r - 1, y1), ink, 1 ) if not full_y: self.draw.draw_rectangle( - (x0, y0 + d / 2 + 1, x0 + width - 1, y1 - d / 2 - 1), ink, 1 + (x0, y0 + r + 1, x0 + width - 1, y1 - r - 1), ink, 1 ) self.draw.draw_rectangle( - (x1 - width + 1, y0 + d / 2 + 1, x1, y1 - d / 2 - 1), ink, 1 + (x1 - width + 1, y0 + r + 1, x1, y1 - r - 1), ink, 1 ) def _multiline_check(self, text): diff --git a/src/PIL/TiffTags.py b/src/PIL/TiffTags.py index 9e9e117a4..088272162 100644 --- a/src/PIL/TiffTags.py +++ b/src/PIL/TiffTags.py @@ -485,9 +485,6 @@ LIBTIFF_CORE = { 65537, } -LIBTIFF_CORE.remove(301) # Array of short, crashes -LIBTIFF_CORE.remove(532) # Array of long, crashes - LIBTIFF_CORE.remove(255) # We don't have support for subfiletypes LIBTIFF_CORE.remove(322) # We don't have support for writing tiled images with libtiff LIBTIFF_CORE.remove(323) # Tiled images diff --git a/src/decode.c b/src/decode.c index a29c6a46e..dcd11eb5d 100644 --- a/src/decode.c +++ b/src/decode.c @@ -199,7 +199,7 @@ _setimage(ImagingDecoderObject *decoder, PyObject *args) { state->bytes = (state->bits * state->xsize + 7) / 8; } /* malloc check ok, overflow checked above */ - state->buffer = (UINT8 *)malloc(state->bytes); + state->buffer = (UINT8 *)calloc(1, state->bytes); if (!state->buffer) { return ImagingError_MemoryError(); } diff --git a/src/encode.c b/src/encode.c index f92ba62c2..582bdd651 100644 --- a/src/encode.c +++ b/src/encode.c @@ -264,7 +264,7 @@ _setimage(ImagingEncoderObject *encoder, PyObject *args) { } state->bytes = (state->bits * state->xsize + 7) / 8; /* malloc check ok, overflow checked above */ - state->buffer = (UINT8 *)malloc(state->bytes); + state->buffer = (UINT8 *)calloc(1, state->bytes); if (!state->buffer) { return ImagingError_MemoryError(); } @@ -644,10 +644,10 @@ PyImaging_LibTiffEncoderNew(PyObject *self, PyObject *args) { int key_int, status, is_core_tag, is_var_length, num_core_tags, i; TIFFDataType type = TIFF_NOTYPE; // This list also exists in TiffTags.py - const int core_tags[] = {256, 257, 258, 259, 262, 263, 266, 269, 274, - 277, 278, 280, 281, 340, 341, 282, 283, 284, - 286, 287, 296, 297, 320, 321, 338, 32995, 32998, - 32996, 339, 32997, 330, 531, 530, 65537}; + const int core_tags[] = {256, 257, 258, 259, 262, 263, 266, 269, 274, + 277, 278, 280, 281, 340, 341, 282, 283, 284, + 286, 287, 296, 297, 320, 321, 338, 32995, 32998, + 32996, 339, 32997, 330, 531, 530, 65537, 301, 532}; Py_ssize_t tags_size; PyObject *item; @@ -790,7 +790,7 @@ PyImaging_LibTiffEncoderNew(PyObject *self, PyObject *args) { int stride = 256; if (len != 768) { PyErr_SetString( - PyExc_ValueError, "Requiring 768 items for for Colormap"); + PyExc_ValueError, "Requiring 768 items for Colormap"); return NULL; } UINT16 *av; diff --git a/src/libImaging/BcnDecode.c b/src/libImaging/BcnDecode.c index b6a4cbadc..5e8b456b8 100644 --- a/src/libImaging/BcnDecode.c +++ b/src/libImaging/BcnDecode.c @@ -69,7 +69,7 @@ decode_565(UINT16 x) { } static void -decode_bc1_color(rgba *dst, const UINT8 *src) { +decode_bc1_color(rgba *dst, const UINT8 *src, int separate_alpha) { bc1_color col; rgba p[4]; int n, cw; @@ -84,7 +84,10 @@ decode_bc1_color(rgba *dst, const UINT8 *src) { r1 = p[1].r; g1 = p[1].g; b1 = p[1].b; - if (col.c0 > col.c1) { + + + /* NOTE: BC2 and BC3 reuse BC1 color blocks but always act like c0 > c1 */ + if (col.c0 > col.c1 || separate_alpha) { p[2].r = (2 * r0 + 1 * r1) / 3; p[2].g = (2 * g0 + 1 * g1) / 3; p[2].b = (2 * b0 + 1 * b1) / 3; @@ -150,13 +153,13 @@ decode_bc3_alpha(char *dst, const UINT8 *src, int stride, int o) { static void decode_bc1_block(rgba *col, const UINT8 *src) { - decode_bc1_color(col, src); + decode_bc1_color(col, src, 0); } static void decode_bc2_block(rgba *col, const UINT8 *src) { int n, bitI, byI, av; - decode_bc1_color(col, src + 8); + decode_bc1_color(col, src + 8, 1); for (n = 0; n < 16; n++) { bitI = n * 4; byI = bitI >> 3; @@ -168,7 +171,7 @@ decode_bc2_block(rgba *col, const UINT8 *src) { static void decode_bc3_block(rgba *col, const UINT8 *src) { - decode_bc1_color(col, src + 8); + decode_bc1_color(col, src + 8, 1); decode_bc3_alpha((char *)col, src, sizeof(col[0]), 3); } diff --git a/src/libImaging/Jpeg2KDecode.c b/src/libImaging/Jpeg2KDecode.c index f086848e9..6af0f2eae 100644 --- a/src/libImaging/Jpeg2KDecode.c +++ b/src/libImaging/Jpeg2KDecode.c @@ -861,6 +861,10 @@ j2k_decode_entry(Imaging im, ImagingCodecState state) { state->state = J2K_STATE_FAILED; goto quick_exit; } + /* Undefined behavior, sometimes decode_tile_data doesn't + fill the buffer and we do things with it later, leading + to valgrind errors. */ + memset(new, 0, tile_info.data_size); state->buffer = new; buffer_size = tile_info.data_size; }