diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index 6c91b6427..6cea87df2 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -4,7 +4,11 @@ ### What actually happened? -### What versions of Pillow and Python are you using? +### What are your OS, Python and Pillow versions? + +* OS: +* Python: +* Pillow: Please include **code** that reproduces the issue and whenever possible, an **image** that demonstrates the issue. Please upload images to GitHub, not to third-party file hosting sites. If necessary, add the image to a zip or tar archive. diff --git a/.travis.yml b/.travis.yml index 23225dbbb..36a956e95 100644 --- a/.travis.yml +++ b/.travis.yml @@ -27,9 +27,11 @@ matrix: - python: '3.6' - python: '3.6' dist: trusty + env: PYTHONOPTIMIZE=1 - python: '3.5' - python: '3.5' dist: trusty + env: PYTHONOPTIMIZE=2 - python: '3.4' dist: trusty - env: DOCKER="alpine" DOCKER_TAG="pytest" diff --git a/.travis/install.sh b/.travis/install.sh index f18aff079..b123fd302 100755 --- a/.travis/install.sh +++ b/.travis/install.sh @@ -7,7 +7,7 @@ sudo apt-get -qq install libfreetype6-dev liblcms2-dev python-tk\ python-qt4 ghostscript libffi-dev libjpeg-turbo-progs cmake imagemagick\ libharfbuzz-dev libfribidi-dev -pip install cffi +PYTHONOPTIMIZE=0 pip install cffi pip install check-manifest pip install coverage pip install olefile diff --git a/CHANGES.rst b/CHANGES.rst index 972092010..4cb7320ea 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,84 @@ Changelog (Pillow) 5.3.0 (unreleased) ------------------ +- Fixed decompression bomb check in _crop #3313 + [dinkolubina, hugovk] + +- Added support to ImageDraw.floodfill for non-RGB colors #3377 + [radarhere] + +- Tests: Avoid catching unexpected exceptions in tests #2203 + [jdufresne] + +- Use TextIOWrapper.detach() instead of NoCloseStream #2214 + [jdufresne] + +- Added transparency to matrix conversion #3205 + [radarhere] + +- Added ImageOps pad method #3364 + [radarhere] + +- Give correct extrema for I;16 format images #3359 + [bz2] + +- Added PySide2 #3279 + [radarhere] + +- Corrected TIFF tags #3369 + [radarhere] + +- CI: Install CFFI and pycparser without any PYTHONOPTIMIZE #3374 + [hugovk] + +- Read/Save RGB webp as RGB (instead of RGBX) #3298 + [kkopachev] + +- ImageDraw: Add line joints #3250 + [radarhere] + +- Improved performance of ImageDraw floodfill method #3294 + [yo1995] + +- Fix builds with --parallel #3272 + [hsoft] + +- Add more raw Tiff modes (RGBaX, RGBaXX, RGBAX, RGBAXX) #3335 + [homm] + +- Close existing WebP fp before setting new fp #3341 + [radarhere] + +- Add orientation, compression and id_section as TGA save keyword arguments #3327 + [radarhere] + +- Convert int values of RATIONAL TIFF tags to floats #3338 + [radarhere, wiredfool] + +- Fix code for PYTHONOPTIMIZE #3233 + [hugovk] + +- Changed ImageFilter.Kernel to subclass ImageFilter.BuiltinFilter, instead of the other way around #3273 + [radarhere] + +- Remove unused draw.draw_line, draw.draw_point and font.getabc methods #3232 + [hugovk] + +- Tests: Added ImageFilter tests #3295 + [radarhere] + +- Tests: Added ImageChops tests #3230 + [hugovk, radarhere] + +- AppVeyor: Download lib if not present in pillow-depends #3316 + [radarhere] + +- Travis CI: Add Python 3.7 and Xenial #3234 + [hugovk] + +- Docs: Added documentation for NumPy conversion #3301 + [radarhere] + - Depends: Update libimagequant to 2.12.1 #3281 [radarhere] @@ -17,7 +95,7 @@ Changelog (Pillow) - Skip outline if the draw operation fills with the same colour #2922 [radarhere] -- Flake8 fixes #3173 +- Flake8 fixes #3173, #3380 [radarhere] - Avoid deprecated 'U' mode when opening files #2187 diff --git a/RELEASING.md b/RELEASING.md index 7794612fa..b5e548e06 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -89,7 +89,7 @@ Released as needed privately to individual vendors for critical security-related $ git clone https://github.com/python-pillow/pillow-wheels $ cd pillow-wheels $ git submodule init - $ git submodule update + $ git submodule update Pillow $ cd Pillow $ git fetch --all $ git checkout [[release tag]] diff --git a/Tests/check_imaging_leaks.py b/Tests/check_imaging_leaks.py index eb9426eee..0c41b6da1 100755 --- a/Tests/check_imaging_leaks.py +++ b/Tests/check_imaging_leaks.py @@ -25,9 +25,8 @@ class TestImagingLeaks(PillowTestCase): if i < min_iterations: mem_limit = mem + 1 continue - self.assertLessEqual(mem, mem_limit, - msg='memory usage limit exceeded after %d iterations' - % (i + 1)) + msg = 'memory usage limit exceeded after %d iterations' % (i + 1) + self.assertLessEqual(mem, mem_limit, msg) def test_leak_putdata(self): im = Image.new('RGB', (25, 25)) diff --git a/Tests/check_jpeg_leaks.py b/Tests/check_jpeg_leaks.py index 065e9d817..c85f6f030 100644 --- a/Tests/check_jpeg_leaks.py +++ b/Tests/check_jpeg_leaks.py @@ -9,8 +9,7 @@ iterations = 5000 When run on a system without the jpeg leak fixes, the valgrind runs look like this. -NOSE_PROCESSES=0 NOSE_TIMEOUT=600 valgrind --tool=massif \ - python test-installed.py -s -v Tests/check_jpeg_leaks.py +valgrind --tool=massif python test-installed.py -s -v Tests/check_jpeg_leaks.py """ diff --git a/Tests/helper.py b/Tests/helper.py index b6ef6dc13..8fb34848b 100644 --- a/Tests/helper.py +++ b/Tests/helper.py @@ -272,8 +272,8 @@ class PillowLeakTestCase(PillowTestCase): for cycle in range(self.iterations): core() mem = (self._get_mem_usage() - start_mem) - self.assertLess(mem, self.mem_limit, - msg='memory usage limit exceeded in iteration %d' % cycle) + msg = 'memory usage limit exceeded in iteration %d' % cycle + self.assertLess(mem, self.mem_limit, msg) # helpers diff --git a/Tests/images/16_bit_noise.tif b/Tests/images/16_bit_noise.tif new file mode 100644 index 000000000..19180638e Binary files /dev/null and b/Tests/images/16_bit_noise.tif differ diff --git a/Tests/images/hopper_webp_bits.ppm b/Tests/images/hopper_webp_bits.ppm index 6dce2da2e..f431bc7b1 100644 Binary files a/Tests/images/hopper_webp_bits.ppm and b/Tests/images/hopper_webp_bits.ppm differ diff --git a/Tests/images/imagedraw_floodfill_L.png b/Tests/images/imagedraw_floodfill_L.png new file mode 100644 index 000000000..4139e66d8 Binary files /dev/null and b/Tests/images/imagedraw_floodfill_L.png differ diff --git a/Tests/images/imagedraw_floodfill.png b/Tests/images/imagedraw_floodfill_RGB.png similarity index 100% rename from Tests/images/imagedraw_floodfill.png rename to Tests/images/imagedraw_floodfill_RGB.png diff --git a/Tests/images/imagedraw_floodfill_RGBA.png b/Tests/images/imagedraw_floodfill_RGBA.png new file mode 100644 index 000000000..5e02064d4 Binary files /dev/null and b/Tests/images/imagedraw_floodfill_RGBA.png differ diff --git a/Tests/images/imagedraw_line_joint_curve.png b/Tests/images/imagedraw_line_joint_curve.png new file mode 100644 index 000000000..ad729f528 Binary files /dev/null and b/Tests/images/imagedraw_line_joint_curve.png differ diff --git a/Tests/images/imageops_pad_h_0.jpg b/Tests/images/imageops_pad_h_0.jpg new file mode 100644 index 000000000..f9fcb1cdb Binary files /dev/null and b/Tests/images/imageops_pad_h_0.jpg differ diff --git a/Tests/images/imageops_pad_h_1.jpg b/Tests/images/imageops_pad_h_1.jpg new file mode 100644 index 000000000..4b9b9ebc4 Binary files /dev/null and b/Tests/images/imageops_pad_h_1.jpg differ diff --git a/Tests/images/imageops_pad_h_2.jpg b/Tests/images/imageops_pad_h_2.jpg new file mode 100644 index 000000000..2c8224892 Binary files /dev/null and b/Tests/images/imageops_pad_h_2.jpg differ diff --git a/Tests/images/imageops_pad_v_0.jpg b/Tests/images/imageops_pad_v_0.jpg new file mode 100644 index 000000000..caf435796 Binary files /dev/null and b/Tests/images/imageops_pad_v_0.jpg differ diff --git a/Tests/images/imageops_pad_v_1.jpg b/Tests/images/imageops_pad_v_1.jpg new file mode 100644 index 000000000..4a6698e91 Binary files /dev/null and b/Tests/images/imageops_pad_v_1.jpg differ diff --git a/Tests/images/imageops_pad_v_2.jpg b/Tests/images/imageops_pad_v_2.jpg new file mode 100644 index 000000000..792952bcd Binary files /dev/null and b/Tests/images/imageops_pad_v_2.jpg differ diff --git a/Tests/make_hash.py b/Tests/make_hash.py index c5e32d606..c52e009ec 100644 --- a/Tests/make_hash.py +++ b/Tests/make_hash.py @@ -52,10 +52,5 @@ for i0 in range(65556): print() -# print(check(min_size, min_start)) - print("#define ACCESS_TABLE_SIZE", min_size) print("#define ACCESS_TABLE_HASH", min_start) - -# for m in modes: -# print(m, "=>", hash(m, min_start) % min_size) diff --git a/Tests/test_bmp_reference.py b/Tests/test_bmp_reference.py index 8e84cc8f1..af25f7162 100644 --- a/Tests/test_bmp_reference.py +++ b/Tests/test_bmp_reference.py @@ -22,7 +22,6 @@ class TestBmpReference(PillowTestCase): im.load() except Exception: # as msg: pass - # print("Bad Image %s: %s" %(f,msg)) def test_questionable(self): """ These shouldn't crash/dos, but it's not well defined that these @@ -43,11 +42,11 @@ class TestBmpReference(PillowTestCase): im = Image.open(f) im.load() if os.path.basename(f) not in supported: - print("Please add %s to the partially supported bmp specs." % f) + print("Please add %s to the partially supported" + " bmp specs." % f) except Exception: # as msg: if os.path.basename(f) in supported: raise - # print("Bad Image %s: %s" %(f,msg)) def test_good(self): """ These should all work. There's a set of target files in the diff --git a/Tests/test_color_lut.py b/Tests/test_color_lut.py index 6d0b76fed..ec370f70a 100644 --- a/Tests/test_color_lut.py +++ b/Tests/test_color_lut.py @@ -335,35 +335,35 @@ class TestColorLut3DFilter(PillowTestCase): g.transpose(Image.ROTATE_180)]) lut = ImageFilter.Color3DLUT.generate((7, 9, 11), - lambda r, g, b: (r, g, b)) + lambda r, g, b: (r, g, b)) lut.table = numpy.array(lut.table, dtype=numpy.float32)[:-1] with self.assertRaisesRegex(ValueError, "should have table_channels"): im.filter(lut) lut = ImageFilter.Color3DLUT.generate((7, 9, 11), - lambda r, g, b: (r, g, b)) + lambda r, g, b: (r, g, b)) lut.table = (numpy.array(lut.table, dtype=numpy.float32) .reshape((7 * 9 * 11), 3)) with self.assertRaisesRegex(ValueError, "should have table_channels"): im.filter(lut) lut = ImageFilter.Color3DLUT.generate((7, 9, 11), - lambda r, g, b: (r, g, b)) + lambda r, g, b: (r, g, b)) lut.table = numpy.array(lut.table, dtype=numpy.float16) self.assert_image_equal(im, im.filter(lut)) lut = ImageFilter.Color3DLUT.generate((7, 9, 11), - lambda r, g, b: (r, g, b)) + lambda r, g, b: (r, g, b)) lut.table = numpy.array(lut.table, dtype=numpy.float32) self.assert_image_equal(im, im.filter(lut)) lut = ImageFilter.Color3DLUT.generate((7, 9, 11), - lambda r, g, b: (r, g, b)) + lambda r, g, b: (r, g, b)) lut.table = numpy.array(lut.table, dtype=numpy.float64) self.assert_image_equal(im, im.filter(lut)) lut = ImageFilter.Color3DLUT.generate((7, 9, 11), - lambda r, g, b: (r, g, b)) + lambda r, g, b: (r, g, b)) lut.table = numpy.array(lut.table, dtype=numpy.int32) im.filter(lut) lut.table = numpy.array(lut.table, dtype=numpy.int8) diff --git a/Tests/test_decompression_bomb.py b/Tests/test_decompression_bomb.py index 4e3c26377..0a30e25f1 100644 --- a/Tests/test_decompression_bomb.py +++ b/Tests/test_decompression_bomb.py @@ -61,6 +61,29 @@ class TestDecompressionCrop(PillowTestCase): self.assert_warning(Image.DecompressionBombWarning, self.src.crop, box) + def test_crop_decompression_checks(self): + + im = Image.new("RGB", (100, 100)) + + good_values = ((-9999, -9999, -9990, -9990), + (-999, -999, -990, -990)) + + warning_values = ((-160, -160, 99, 99), + (160, 160, -99, -99)) + + error_values = ((-99909, -99990, 99999, 99999), + (99909, 99990, -99999, -99999)) + + for value in good_values: + self.assertEqual(im.crop(value).size, (9, 9)) + + for value in warning_values: + self.assert_warning(Image.DecompressionBombWarning, im.crop, value) + + for value in error_values: + with self.assertRaises(Image.DecompressionBombError): + im.crop(value) + if __name__ == '__main__': unittest.main() diff --git a/Tests/test_file_gd.py b/Tests/test_file_gd.py index 326b0c4b2..b303369b4 100644 --- a/Tests/test_file_gd.py +++ b/Tests/test_file_gd.py @@ -2,8 +2,6 @@ from helper import unittest, PillowTestCase from PIL import GdImageFile -import io - TEST_GD_FILE = "Tests/images/hopper.gd" diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index c42d67885..1485651c7 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -141,11 +141,9 @@ class TestFileJpeg(PillowTestCase): im = Image.open('Tests/images/icc_profile_big.jpg') f = self.tempfile("temp.jpg") icc_profile = im.info["icc_profile"] - try: - im.save(f, format='JPEG', progressive=True, quality=95, - icc_profile=icc_profile, optimize=True) - except IOError: - self.fail("Failed saving image with icc larger than image size") + # Should not raise IOError for image with icc larger than image size. + im.save(f, format='JPEG', progressive=True, quality=95, + icc_profile=icc_profile, optimize=True) def test_optimize(self): im1 = self.roundtrip(hopper()) diff --git a/Tests/test_file_libtiff.py b/Tests/test_file_libtiff.py index 77caa0b9d..b5e103e39 100644 --- a/Tests/test_file_libtiff.py +++ b/Tests/test_file_libtiff.py @@ -231,6 +231,16 @@ class TestFileLibTiff(LibTiffTestCase): TiffImagePlugin.WRITE_LIBTIFF = False + def test_int_dpi(self): + # issue #1765 + im = hopper('RGB') + out = self.tempfile('temp.tif') + TiffImagePlugin.WRITE_LIBTIFF = True + im.save(out, dpi=(72, 72)) + TiffImagePlugin.WRITE_LIBTIFF = False + reloaded = Image.open(out) + self.assertEqual(reloaded.info['dpi'], (72.0, 72.0)) + def test_g3_compression(self): i = Image.open('Tests/images/hopper_g4_500.tif') out = self.tempfile("temp.tif") @@ -529,10 +539,8 @@ class TestFileLibTiff(LibTiffTestCase): im = Image.open(tmpfile) im.n_frames im.close() - try: - os.remove(tmpfile) # Windows PermissionError here! - except: - self.fail("Should not get permission error here") + # Should not raise PermissionError. + os.remove(tmpfile) def test_read_icc(self): with Image.open("Tests/images/hopper.iccprofile.tif") as img: diff --git a/Tests/test_file_tga.py b/Tests/test_file_tga.py index 226b899dc..77695f2d1 100644 --- a/Tests/test_file_tga.py +++ b/Tests/test_file_tga.py @@ -53,10 +53,10 @@ class TestFileTga(PillowTestCase): # Generate a new test name every time so the # test will not fail with permission error # on Windows. - test_file = self.tempfile("temp.tga") + out = self.tempfile("temp.tga") - original_im.save(test_file, rle=rle) - saved_im = Image.open(test_file) + original_im.save(out, rle=rle) + saved_im = Image.open(out) if rle: self.assertEqual( saved_im.info["compression"], @@ -95,34 +95,93 @@ class TestFileTga(PillowTestCase): test_file = "Tests/images/tga_id_field.tga" im = Image.open(test_file) - test_file = self.tempfile("temp.tga") + out = self.tempfile("temp.tga") # Save - im.save(test_file) - test_im = Image.open(test_file) + im.save(out) + test_im = Image.open(out) self.assertEqual(test_im.size, (100, 100)) + self.assertEqual(test_im.info["id_section"], im.info["id_section"]) # RGBA save - im.convert("RGBA").save(test_file) - test_im = Image.open(test_file) + im.convert("RGBA").save(out) + test_im = Image.open(out) self.assertEqual(test_im.size, (100, 100)) + def test_save_id_section(self): + test_file = "Tests/images/rgb32rle.tga" + im = Image.open(test_file) + + out = self.tempfile("temp.tga") + + # Check there is no id section + im.save(out) + test_im = Image.open(out) + self.assertNotIn("id_section", test_im.info) + + # Save with custom id section + im.save(out, id_section=b"Test content") + test_im = Image.open(out) + self.assertEqual(test_im.info["id_section"], b"Test content") + + # Save with custom id section greater than 255 characters + id_section = b"Test content" * 25 + self.assert_warning(UserWarning, + lambda: im.save(out, id_section=id_section)) + test_im = Image.open(out) + self.assertEqual(test_im.info["id_section"], id_section[:255]) + + test_file = "Tests/images/tga_id_field.tga" + im = Image.open(test_file) + + # Save with no id section + im.save(out, id_section="") + test_im = Image.open(out) + self.assertNotIn("id_section", test_im.info) + + def test_save_orientation(self): + test_file = "Tests/images/rgb32rle.tga" + im = Image.open(test_file) + self.assertEqual(im.info["orientation"], -1) + + out = self.tempfile("temp.tga") + + im.save(out, orientation=1) + test_im = Image.open(out) + self.assertEqual(test_im.info["orientation"], 1) + def test_save_rle(self): test_file = "Tests/images/rgb32rle.tga" im = Image.open(test_file) + self.assertEqual(im.info["compression"], "tga_rle") - test_file = self.tempfile("temp.tga") + out = self.tempfile("temp.tga") # Save - im.save(test_file) - test_im = Image.open(test_file) + im.save(out) + test_im = Image.open(out) self.assertEqual(test_im.size, (199, 199)) + self.assertEqual(test_im.info["compression"], "tga_rle") + + # Save without compression + im.save(out, compression=None) + test_im = Image.open(out) + self.assertNotIn("compression", test_im.info) # RGBA save - im.convert("RGBA").save(test_file) - test_im = Image.open(test_file) + im.convert("RGBA").save(out) + test_im = Image.open(out) self.assertEqual(test_im.size, (199, 199)) + test_file = "Tests/images/tga_id_field.tga" + im = Image.open(test_file) + self.assertNotIn("compression", im.info) + + # Save with compression + im.save(out, compression="tga_rle") + test_im = Image.open(out) + self.assertEqual(test_im.info["compression"], "tga_rle") + def test_save_l_transparency(self): # There are 559 transparent pixels in la.tga. num_transparent = 559 @@ -133,10 +192,10 @@ class TestFileTga(PillowTestCase): self.assertEqual( im.getchannel("A").getcolors()[0][0], num_transparent) - test_file = self.tempfile("temp.tga") - im.save(test_file) + out = self.tempfile("temp.tga") + im.save(out) - test_im = Image.open(test_file) + test_im = Image.open(out) self.assertEqual(test_im.mode, "LA") self.assertEqual( test_im.getchannel("A").getcolors()[0][0], num_transparent) diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py index 79630c773..cbfc63d74 100644 --- a/Tests/test_file_tiff.py +++ b/Tests/test_file_tiff.py @@ -1,6 +1,5 @@ import logging from io import BytesIO -import struct import sys from helper import unittest, PillowTestCase, hopper @@ -59,7 +58,8 @@ class TestFileTiff(PillowTestCase): self.assertEqual(im.mode, "RGBA") self.assertEqual(im.size, (52, 53)) - self.assertEqual(im.tile, [('raw', (0, 0, 52, 53), 160, ('RGBA', 0, 1))]) + self.assertEqual(im.tile, + [('raw', (0, 0, 52, 53), 160, ('RGBA', 0, 1))]) im.load() def test_set_legacy_api(self): @@ -133,11 +133,8 @@ class TestFileTiff(PillowTestCase): def test_bad_exif(self): i = Image.open('Tests/images/hopper_bad_exif.jpg') - try: - self.assert_warning(UserWarning, i._getexif) - except struct.error: - self.fail( - "Bad EXIF data passed incorrect values to _binary unpack") + # Should not raise struct.error. + self.assert_warning(UserWarning, i._getexif) def test_save_rgba(self): im = hopper("RGBA") diff --git a/Tests/test_file_tiff_metadata.py b/Tests/test_file_tiff_metadata.py index aabedd6c8..d1e2b577e 100644 --- a/Tests/test_file_tiff_metadata.py +++ b/Tests/test_file_tiff_metadata.py @@ -56,7 +56,8 @@ class TestFileTiffMetadata(PillowTestCase): loaded = Image.open(f) self.assertEqual(loaded.tag[ImageJMetaDataByteCounts], (len(bindata),)) - self.assertEqual(loaded.tag_v2[ImageJMetaDataByteCounts], (len(bindata),)) + self.assertEqual(loaded.tag_v2[ImageJMetaDataByteCounts], + (len(bindata),)) self.assertEqual(loaded.tag[ImageJMetaData], bindata) self.assertEqual(loaded.tag_v2[ImageJMetaData], bindata) @@ -75,8 +76,10 @@ class TestFileTiffMetadata(PillowTestCase): img.save(f, tiffinfo=info) loaded = Image.open(f) - self.assertEqual(loaded.tag[ImageJMetaDataByteCounts], (8, len(bindata) - 8)) - self.assertEqual(loaded.tag_v2[ImageJMetaDataByteCounts], (8, len(bindata) - 8)) + self.assertEqual(loaded.tag[ImageJMetaDataByteCounts], + (8, len(bindata) - 8)) + self.assertEqual(loaded.tag_v2[ImageJMetaDataByteCounts], + (8, len(bindata) - 8)) def test_read_metadata(self): img = Image.open('Tests/images/hopper_g4.tif') @@ -133,8 +136,8 @@ class TestFileTiffMetadata(PillowTestCase): if isinstance(v, IFDRational): original[k] = IFDRational(*_limit_rational(v, 2**31)) if isinstance(v, tuple) and isinstance(v[0], IFDRational): - original[k] = tuple([IFDRational( - *_limit_rational(elt, 2**31)) for elt in v]) + original[k] = tuple([IFDRational(*_limit_rational(elt, 2**31)) + for elt in v]) ignored = ['StripByteCounts', 'RowsPerStrip', 'PageNumber', 'StripOffsets'] @@ -169,10 +172,8 @@ class TestFileTiffMetadata(PillowTestCase): f = io.BytesIO(b'II*\x00\x08\x00\x00\x00') head = f.read(8) info = TiffImagePlugin.ImageFileDirectory(head) - try: - self.assert_warning(UserWarning, info.load, f) - except struct.error: - self.fail("Should not be struct errors there.") + # Should not raise struct.error. + self.assert_warning(UserWarning, info.load, f) def test_iccprofile(self): # https://github.com/python-pillow/Pillow/issues/1462 @@ -186,7 +187,8 @@ class TestFileTiffMetadata(PillowTestCase): def test_iccprofile_binary(self): # https://github.com/python-pillow/Pillow/issues/1526 - # We should be able to load this, but probably won't be able to save it. + # We should be able to load this, + # but probably won't be able to save it. im = Image.open('Tests/images/hopper.iccprofile_binary.tif') self.assertEqual(im.tag_v2.tagtype[34675], 1) @@ -223,10 +225,8 @@ class TestFileTiffMetadata(PillowTestCase): head = data.read(8) info = TiffImagePlugin.ImageFileDirectory_v2(head) info.load(data) - try: - info = dict(info) - except ValueError: - self.fail("Should not be struct value error there.") + # Should not raise ValueError. + info = dict(info) self.assertIn(33432, info) def test_PhotoshopInfo(self): @@ -245,10 +245,8 @@ class TestFileTiffMetadata(PillowTestCase): ifd._tagdata[277] = struct.pack('hh', 4, 4) ifd.tagtype[277] = TiffTags.SHORT - try: - self.assert_warning(UserWarning, lambda: ifd[277]) - except ValueError: - self.fail("Invalid Metadata count should not cause a Value Error.") + # Should not raise ValueError. + self.assert_warning(UserWarning, lambda: ifd[277]) if __name__ == '__main__': diff --git a/Tests/test_file_webp.py b/Tests/test_file_webp.py index 06e274d0a..fa01cf93e 100644 --- a/Tests/test_file_webp.py +++ b/Tests/test_file_webp.py @@ -16,8 +16,7 @@ class TestFileWebp(PillowTestCase): self.skipTest('WebP support not installed') return - # WebPAnimDecoder only returns RGBA or RGBX, never RGB - self.rgb_mode = "RGBX" if _webp.HAVE_WEBPANIM else "RGB" + self.rgb_mode = "RGB" def test_version(self): _webp.WebPDecoderVersion() @@ -29,8 +28,7 @@ class TestFileWebp(PillowTestCase): Does it have the bits we expect? """ - file_path = "Tests/images/hopper.webp" - image = Image.open(file_path) + image = Image.open("Tests/images/hopper.webp") self.assertEqual(image.mode, self.rgb_mode) self.assertEqual(image.size, (128, 128)) @@ -40,9 +38,8 @@ class TestFileWebp(PillowTestCase): # generated with: # dwebp -ppm ../../Tests/images/hopper.webp -o hopper_webp_bits.ppm - target = Image.open('Tests/images/hopper_webp_bits.ppm') - target = target.convert(self.rgb_mode) - self.assert_image_similar(image, target, 20.0) + self.assert_image_similar_tofile( + image, 'Tests/images/hopper_webp_bits.ppm', 1.0) def test_write_rgb(self): """ @@ -61,13 +58,9 @@ class TestFileWebp(PillowTestCase): image.load() image.getdata() - # If we're using the exact same version of WebP, this test should pass. - # but it doesn't if the WebP is generated on Ubuntu and tested on - # Fedora. - # generated with: dwebp -ppm temp.webp -o hopper_webp_write.ppm - # target = Image.open('Tests/images/hopper_webp_write.ppm') - # self.assert_image_equal(image, target) + self.assert_image_similar_tofile( + image, 'Tests/images/hopper_webp_write.ppm', 12.0) # This test asserts that the images are similar. If the average pixel # difference between the two images is less than the epsilon value, @@ -135,6 +128,13 @@ class TestFileWebp(PillowTestCase): self.assertRaises(TypeError, _webp.WebPAnimDecoder) self.assertRaises(TypeError, _webp.WebPDecode) + def test_no_resource_warning(self): + file_path = "Tests/images/hopper.webp" + image = Image.open(file_path) + + temp_file = self.tempfile("temp.webp") + self.assert_warning(None, image.save, temp_file) + if __name__ == '__main__': unittest.main() diff --git a/Tests/test_file_webp_lossless.py b/Tests/test_file_webp_lossless.py index 10354c55f..4c35dad73 100644 --- a/Tests/test_file_webp_lossless.py +++ b/Tests/test_file_webp_lossless.py @@ -19,8 +19,7 @@ class TestFileWebpLossless(PillowTestCase): if (_webp.WebPDecoderVersion() < 0x0200): self.skipTest('lossless not included') - # WebPAnimDecoder only returns RGBA or RGBX, never RGB - self.rgb_mode = "RGBX" if _webp.HAVE_WEBPANIM else "RGB" + self.rgb_mode = "RGB" def test_write_lossless_rgb(self): temp_file = self.tempfile("temp.webp") diff --git a/Tests/test_format_hsv.py b/Tests/test_format_hsv.py index 31309da60..d7820400e 100644 --- a/Tests/test_format_hsv.py +++ b/Tests/test_format_hsv.py @@ -47,10 +47,6 @@ class TestFormatHSV(PillowTestCase): img = Image.merge('RGB', (r, g, b)) - # print(("%d, %d -> "% (int(1.75*px),int(.25*px))) + \ - # "(%s, %s, %s)"%img.getpixel((1.75*px, .25*px))) - # print(("%d, %d -> "% (int(.75*px),int(.25*px))) + \ - # "(%s, %s, %s)"%img.getpixel((.75*px, .25*px))) return img def to_xxx_colorsys(self, im, func, mode): @@ -95,15 +91,6 @@ class TestFormatHSV(PillowTestCase): im = src.convert('HSV') comparable = self.to_hsv_colorsys(src) - # print(im.getpixel((448, 64))) - # print(comparable.getpixel((448, 64))) - - # print(im.split()[0].histogram()) - # print(comparable.split()[0].histogram()) - - # im.split()[0].show() - # comparable.split()[0].show() - self.assert_image_similar(im.getchannel(0), comparable.getchannel(0), 1, "Hue conversion is wrong") self.assert_image_similar(im.getchannel(1), comparable.getchannel(1), @@ -111,16 +98,9 @@ class TestFormatHSV(PillowTestCase): self.assert_image_similar(im.getchannel(2), comparable.getchannel(2), 1, "Value conversion is wrong") - # print(im.getpixel((192, 64))) - comparable = src im = im.convert('RGB') - # im.split()[0].show() - # comparable.split()[0].show() - # print(im.getpixel((192, 64))) - # print(comparable.getpixel((192, 64))) - self.assert_image_similar(im.getchannel(0), comparable.getchannel(0), 3, "R conversion is wrong") self.assert_image_similar(im.getchannel(1), comparable.getchannel(1), @@ -132,12 +112,6 @@ class TestFormatHSV(PillowTestCase): im = hopper('RGB').convert('HSV') comparable = self.to_hsv_colorsys(hopper('RGB')) -# print([ord(x) for x in im.split()[0].tobytes()[:80]]) -# print([ord(x) for x in comparable.split()[0].tobytes()[:80]]) - -# print(im.split()[0].histogram()) -# print(comparable.split()[0].histogram()) - self.assert_image_similar(im.getchannel(0), comparable.getchannel(0), 1, "Hue conversion is wrong") self.assert_image_similar(im.getchannel(1), comparable.getchannel(1), @@ -150,12 +124,6 @@ class TestFormatHSV(PillowTestCase): converted = comparable.convert('RGB') comparable = self.to_rgb_colorsys(comparable) - # print(converted.split()[1].histogram()) - # print(target.split()[1].histogram()) - - # print([ord(x) for x in target.split()[1].tobytes()[:80]]) - # print([ord(x) for x in converted.split()[1].tobytes()[:80]]) - self.assert_image_similar(converted.getchannel(0), comparable.getchannel(0), 3, "R conversion is wrong") diff --git a/Tests/test_image_access.py b/Tests/test_image_access.py index 7a9378bbd..a7e39a499 100644 --- a/Tests/test_image_access.py +++ b/Tests/test_image_access.py @@ -1,15 +1,20 @@ from helper import unittest, PillowTestCase, hopper, on_appveyor -try: - from PIL import PyAccess -except ImportError: - # Skip in setUp() - pass - from PIL import Image import sys import os +# CFFI imports pycparser which doesn't support PYTHONOPTIMIZE=2 +# https://github.com/eliben/pycparser/pull/198#issuecomment-317001670 +if os.environ.get("PYTHONOPTIMIZE") == "2": + cffi = None +else: + try: + from PIL import PyAccess + import cffi + except ImportError: + cffi = None + class AccessTest(PillowTestCase): # initial value @@ -113,38 +118,20 @@ class TestImageGetPixel(AccessTest): self.check(mode, 2**16-1) +@unittest.skipIf(cffi is None, "No cffi") class TestCffiPutPixel(TestImagePutPixel): _need_cffi_access = True - def setUp(self): - try: - import cffi - assert cffi # silence warning - except ImportError: - self.skipTest("No cffi") - +@unittest.skipIf(cffi is None, "No cffi") class TestCffiGetPixel(TestImageGetPixel): _need_cffi_access = True - def setUp(self): - try: - import cffi - assert cffi # silence warning - except ImportError: - self.skipTest("No cffi") - +@unittest.skipIf(cffi is None, "No cffi") class TestCffi(AccessTest): _need_cffi_access = True - def setUp(self): - try: - import cffi - assert cffi # silence warning - except ImportError: - self.skipTest("No cffi") - def _test_get_access(self, im): """Do we get the same thing as the old pixel access diff --git a/Tests/test_image_convert.py b/Tests/test_image_convert.py index 1e208d80c..1b3815d80 100644 --- a/Tests/test_image_convert.py +++ b/Tests/test_image_convert.py @@ -187,6 +187,7 @@ class TestImageConvert(PillowTestCase): def matrix_convert(mode): # Arrange im = hopper('RGB') + im.info['transparency'] = (255, 0, 0) matrix = ( 0.412453, 0.357580, 0.180423, 0, 0.212671, 0.715160, 0.072169, 0, @@ -203,9 +204,12 @@ class TestImageConvert(PillowTestCase): target = Image.open('Tests/images/hopper-XYZ.png') if converted_im.mode == 'RGB': self.assert_image_similar(converted_im, target, 3) + self.assertEqual(converted_im.info['transparency'], + (105, 54, 4)) else: self.assert_image_similar(converted_im, target.getchannel(0), 1) + self.assertEqual(converted_im.info['transparency'], 105) matrix_convert('RGB') matrix_convert('L') diff --git a/Tests/test_image_filter.py b/Tests/test_image_filter.py index 3636a73f7..6936a84f0 100644 --- a/Tests/test_image_filter.py +++ b/Tests/test_image_filter.py @@ -94,6 +94,15 @@ class TestImageFilter(PillowTestCase): self.assertEqual(rankfilter.size, 1) self.assertEqual(rankfilter.rank, 2) + def test_builtinfilter_p(self): + builtinFilter = ImageFilter.BuiltinFilter() + + self.assertRaises(ValueError, builtinFilter.filter, hopper("P")) + + def test_kernel_not_enough_coefficients(self): + self.assertRaises(ValueError, + lambda: ImageFilter.Kernel((3, 3), (0, 0))) + def test_consistency_3x3(self): source = Image.open("Tests/images/hopper.bmp") reference = Image.open("Tests/images/hopper_emboss.bmp") diff --git a/Tests/test_image_getextrema.py b/Tests/test_image_getextrema.py index 0b0c31b86..9783141a3 100644 --- a/Tests/test_image_getextrema.py +++ b/Tests/test_image_getextrema.py @@ -1,3 +1,4 @@ +from PIL import Image from helper import unittest, PillowTestCase, hopper @@ -19,6 +20,13 @@ class TestImageGetExtrema(PillowTestCase): extrema("RGBA"), ((0, 255), (0, 255), (0, 255), (255, 255))) self.assertEqual( extrema("CMYK"), (((0, 255), (0, 255), (0, 255), (0, 0)))) + self.assertEqual(extrema("I;16"), (0, 255)) + + def test_true_16(self): + im = Image.open("Tests/images/16_bit_noise.tif") + self.assertEqual(im.mode, 'I;16') + extrema = im.getextrema() + self.assertEqual(extrema, (106, 285)) if __name__ == '__main__': diff --git a/Tests/test_image_resample.py b/Tests/test_image_resample.py index 3687a1a2c..1b7081d55 100644 --- a/Tests/test_image_resample.py +++ b/Tests/test_image_resample.py @@ -352,10 +352,8 @@ class CoreResamplePassesTest(PillowTestCase): class CoreResampleCoefficientsTest(PillowTestCase): def test_reduce(self): test_color = 254 - # print() for size in range(400000, 400010, 2): - # print(size) i = Image.new('L', (size, 1), 0) draw = ImageDraw.Draw(i) draw.rectangle((0, 0, i.size[0] // 2 - 1, 0), test_color) @@ -363,7 +361,6 @@ class CoreResampleCoefficientsTest(PillowTestCase): px = i.resize((5, i.size[1]), Image.BICUBIC).load() if px[2, 0] != test_color // 2: self.assertEqual(test_color // 2, px[2, 0]) - # print('>', size, test_color // 2, px[2, 0]) def test_nonzero_coefficients(self): # regression test for the wrong coefficients calculation diff --git a/Tests/test_image_rotate.py b/Tests/test_image_rotate.py index 17d394a02..e788e722f 100644 --- a/Tests/test_image_rotate.py +++ b/Tests/test_image_rotate.py @@ -114,15 +114,16 @@ class TestImageRotate(PillowTestCase): # Alpha images are handled differently internally im = Image.new('RGBA', (10, 10), 'green') im = im.rotate(45, expand=1) - corner = im.getpixel((0,0)) + corner = im.getpixel((0, 0)) self.assertEqual(corner, (0, 0, 0, 0)) def test_alpha_rotate_with_fill(self): # Alpha images are handled differently internally im = Image.new('RGBA', (10, 10), 'green') im = im.rotate(45, expand=1, fillcolor=(255, 0, 0, 255)) - corner = im.getpixel((0,0)) + corner = im.getpixel((0, 0)) self.assertEqual(corner, (255, 0, 0, 255)) + if __name__ == '__main__': unittest.main() diff --git a/Tests/test_imagechops.py b/Tests/test_imagechops.py index 4e30dc175..06febc6d2 100644 --- a/Tests/test_imagechops.py +++ b/Tests/test_imagechops.py @@ -3,6 +3,16 @@ from helper import unittest, PillowTestCase, hopper from PIL import Image from PIL import ImageChops +BLACK = (0, 0, 0) +BROWN = (127, 64, 0) +CYAN = (0, 255, 255) +DARK_GREEN = (0, 128, 0) +GREEN = (0, 255, 0) +ORANGE = (255, 128, 0) +WHITE = (255, 255, 255) + +GREY = 128 + class TestImageChops(PillowTestCase): @@ -35,6 +45,303 @@ class TestImageChops(PillowTestCase): ImageChops.offset(im, 10) ImageChops.offset(im, 10, 20) + def test_add(self): + # Arrange + im1 = Image.open("Tests/images/imagedraw_ellipse_RGB.png") + im2 = Image.open("Tests/images/imagedraw_floodfill_RGB.png") + + # Act + new = ImageChops.add(im1, im2) + + # Assert + self.assertEqual(new.getbbox(), (25, 25, 76, 76)) + self.assertEqual(new.getpixel((50, 50)), ORANGE) + + def test_add_scale_offset(self): + # Arrange + im1 = Image.open("Tests/images/imagedraw_ellipse_RGB.png") + im2 = Image.open("Tests/images/imagedraw_floodfill_RGB.png") + + # Act + new = ImageChops.add(im1, im2, scale=2.5, offset=100) + + # Assert + self.assertEqual(new.getbbox(), (0, 0, 100, 100)) + self.assertEqual(new.getpixel((50, 50)), (202, 151, 100)) + + def test_add_clip(self): + # Arrange + im = hopper() + + # Act + new = ImageChops.add(im, im) + + # Assert + self.assertEqual(new.getpixel((50, 50)), (255, 255, 254)) + + def test_add_modulo(self): + # Arrange + im1 = Image.open("Tests/images/imagedraw_ellipse_RGB.png") + im2 = Image.open("Tests/images/imagedraw_floodfill_RGB.png") + + # Act + new = ImageChops.add_modulo(im1, im2) + + # Assert + self.assertEqual(new.getbbox(), (25, 25, 76, 76)) + self.assertEqual(new.getpixel((50, 50)), ORANGE) + + def test_add_modulo_no_clip(self): + # Arrange + im = hopper() + + # Act + new = ImageChops.add_modulo(im, im) + + # Assert + self.assertEqual(new.getpixel((50, 50)), (224, 76, 254)) + + def test_blend(self): + # Arrange + im1 = Image.open("Tests/images/imagedraw_ellipse_RGB.png") + im2 = Image.open("Tests/images/imagedraw_floodfill_RGB.png") + + # Act + new = ImageChops.blend(im1, im2, 0.5) + + # Assert + self.assertEqual(new.getbbox(), (25, 25, 76, 76)) + self.assertEqual(new.getpixel((50, 50)), BROWN) + + def test_constant(self): + # Arrange + im = Image.new("RGB", (20, 10)) + + # Act + new = ImageChops.constant(im, GREY) + + # Assert + self.assertEqual(new.size, im.size) + self.assertEqual(new.getpixel((0, 0)), GREY) + self.assertEqual(new.getpixel((19, 9)), GREY) + + def test_darker_image(self): + # Arrange + im1 = Image.open("Tests/images/imagedraw_chord_RGB.png") + im2 = Image.open("Tests/images/imagedraw_outline_chord_RGB.png") + + # Act + new = ImageChops.darker(im1, im2) + + # Assert + self.assert_image_equal(new, im2) + + def test_darker_pixel(self): + # Arrange + im1 = hopper() + im2 = Image.open("Tests/images/imagedraw_chord_RGB.png") + + # Act + new = ImageChops.darker(im1, im2) + + # Assert + self.assertEqual(new.getpixel((50, 50)), (240, 166, 0)) + + def test_difference(self): + # Arrange + im1 = Image.open("Tests/images/imagedraw_arc_end_le_start.png") + im2 = Image.open("Tests/images/imagedraw_arc_no_loops.png") + + # Act + new = ImageChops.difference(im1, im2) + + # Assert + self.assertEqual(new.getbbox(), (25, 25, 76, 76)) + + def test_difference_pixel(self): + # Arrange + im1 = hopper() + im2 = Image.open("Tests/images/imagedraw_polygon_kite_RGB.png") + + # Act + new = ImageChops.difference(im1, im2) + + # Assert + self.assertEqual(new.getpixel((50, 50)), (240, 166, 128)) + + def test_duplicate(self): + # Arrange + im = hopper() + + # Act + new = ImageChops.duplicate(im) + + # Assert + self.assert_image_equal(new, im) + + def test_invert(self): + # Arrange + im = Image.open("Tests/images/imagedraw_floodfill_RGB.png") + + # Act + new = ImageChops.invert(im) + + # Assert + self.assertEqual(new.getbbox(), (0, 0, 100, 100)) + self.assertEqual(new.getpixel((0, 0)), WHITE) + self.assertEqual(new.getpixel((50, 50)), CYAN) + + def test_lighter_image(self): + # Arrange + im1 = Image.open("Tests/images/imagedraw_chord_RGB.png") + im2 = Image.open("Tests/images/imagedraw_outline_chord_RGB.png") + + # Act + new = ImageChops.lighter(im1, im2) + + # Assert + self.assert_image_equal(new, im1) + + def test_lighter_pixel(self): + # Arrange + im1 = hopper() + im2 = Image.open("Tests/images/imagedraw_chord_RGB.png") + + # Act + new = ImageChops.lighter(im1, im2) + + # Assert + self.assertEqual(new.getpixel((50, 50)), (255, 255, 127)) + + def test_multiply_black(self): + """If you multiply an image with a solid black image, + the result is black.""" + # Arrange + im1 = hopper() + black = Image.new("RGB", im1.size, "black") + + # Act + new = ImageChops.multiply(im1, black) + + # Assert + self.assert_image_equal(new, black) + + def test_multiply_green(self): + # Arrange + im = Image.open("Tests/images/imagedraw_floodfill_RGB.png") + green = Image.new("RGB", im.size, "green") + + # Act + new = ImageChops.multiply(im, green) + + # Assert + self.assertEqual(new.getbbox(), (25, 25, 76, 76)) + self.assertEqual(new.getpixel((25, 25)), DARK_GREEN) + self.assertEqual(new.getpixel((50, 50)), BLACK) + + def test_multiply_white(self): + """If you multiply with a solid white image, + the image is unaffected.""" + # Arrange + im1 = hopper() + white = Image.new("RGB", im1.size, "white") + + # Act + new = ImageChops.multiply(im1, white) + + # Assert + self.assert_image_equal(new, im1) + + def test_offset(self): + # Arrange + im = Image.open("Tests/images/imagedraw_ellipse_RGB.png") + xoffset = 45 + yoffset = 20 + + # Act + new = ImageChops.offset(im, xoffset, yoffset) + + # Assert + self.assertEqual(new.getbbox(), (0, 45, 100, 96)) + self.assertEqual(new.getpixel((50, 50)), BLACK) + self.assertEqual(new.getpixel((50+xoffset, 50+yoffset)), DARK_GREEN) + + # Test no yoffset + self.assertEqual(ImageChops.offset(im, xoffset), + ImageChops.offset(im, xoffset, xoffset)) + + def test_screen(self): + # Arrange + im1 = Image.open("Tests/images/imagedraw_ellipse_RGB.png") + im2 = Image.open("Tests/images/imagedraw_floodfill_RGB.png") + + # Act + new = ImageChops.screen(im1, im2) + + # Assert + self.assertEqual(new.getbbox(), (25, 25, 76, 76)) + self.assertEqual(new.getpixel((50, 50)), ORANGE) + + def test_subtract(self): + # Arrange + im1 = Image.open("Tests/images/imagedraw_chord_RGB.png") + im2 = Image.open("Tests/images/imagedraw_outline_chord_RGB.png") + + # Act + new = ImageChops.subtract(im1, im2) + + # Assert + self.assertEqual(new.getbbox(), (25, 50, 76, 76)) + self.assertEqual(new.getpixel((50, 50)), GREEN) + self.assertEqual(new.getpixel((50, 51)), BLACK) + + def test_subtract_scale_offset(self): + # Arrange + im1 = Image.open("Tests/images/imagedraw_chord_RGB.png") + im2 = Image.open("Tests/images/imagedraw_outline_chord_RGB.png") + + # Act + new = ImageChops.subtract(im1, im2, scale=2.5, offset=100) + + # Assert + self.assertEqual(new.getbbox(), (0, 0, 100, 100)) + self.assertEqual(new.getpixel((50, 50)), (100, 202, 100)) + + def test_subtract_clip(self): + # Arrange + im1 = hopper() + im2 = Image.open("Tests/images/imagedraw_chord_RGB.png") + + # Act + new = ImageChops.subtract(im1, im2) + + # Assert + self.assertEqual(new.getpixel((50, 50)), (0, 0, 127)) + + def test_subtract_modulo(self): + # Arrange + im1 = Image.open("Tests/images/imagedraw_chord_RGB.png") + im2 = Image.open("Tests/images/imagedraw_outline_chord_RGB.png") + + # Act + new = ImageChops.subtract_modulo(im1, im2) + + # Assert + self.assertEqual(new.getbbox(), (25, 50, 76, 76)) + self.assertEqual(new.getpixel((50, 50)), GREEN) + self.assertEqual(new.getpixel((50, 51)), BLACK) + + def test_subtract_modulo_no_clip(self): + # Arrange + im1 = hopper() + im2 = Image.open("Tests/images/imagedraw_chord_RGB.png") + + # Act + new = ImageChops.subtract_modulo(im1, im2) + + # Assert + self.assertEqual(new.getpixel((50, 50)), (241, 167, 127)) + def test_logical(self): def table(op, a, b): diff --git a/Tests/test_imagecms.py b/Tests/test_imagecms.py index 1fdfe916d..dc6c85f05 100644 --- a/Tests/test_imagecms.py +++ b/Tests/test_imagecms.py @@ -175,11 +175,11 @@ class TestImageCms(PillowTestCase): def test_unsupported_color_space(self): self.assertRaises(ImageCms.PyCMSError, - ImageCms.createProfile, "unsupported") + ImageCms.createProfile, "unsupported") def test_invalid_color_temperature(self): self.assertRaises(ImageCms.PyCMSError, - ImageCms.createProfile, "LAB", "invalid") + ImageCms.createProfile, "LAB", "invalid") def test_simple_lab(self): i = Image.new('RGB', (10, 10), (128, 128, 128)) @@ -446,20 +446,20 @@ class TestImageCms(PillowTestCase): self.assert_image_equal(source_image_aux, result_image_aux) def test_preserve_auxiliary_channels_rgba(self): - self.assert_aux_channel_preserved(mode='RGBA', - transform_in_place=False, preserved_channel='A') + self.assert_aux_channel_preserved( + mode='RGBA', transform_in_place=False, preserved_channel='A') def test_preserve_auxiliary_channels_rgba_in_place(self): - self.assert_aux_channel_preserved(mode='RGBA', - transform_in_place=True, preserved_channel='A') + self.assert_aux_channel_preserved( + mode='RGBA', transform_in_place=True, preserved_channel='A') def test_preserve_auxiliary_channels_rgbx(self): - self.assert_aux_channel_preserved(mode='RGBX', - transform_in_place=False, preserved_channel='X') + self.assert_aux_channel_preserved( + mode='RGBX', transform_in_place=False, preserved_channel='X') def test_preserve_auxiliary_channels_rgbx_in_place(self): - self.assert_aux_channel_preserved(mode='RGBX', - transform_in_place=True, preserved_channel='X') + self.assert_aux_channel_preserved( + mode='RGBX', transform_in_place=True, preserved_channel='X') def test_auxiliary_channels_isolated(self): # test data in aux channels does not affect non-aux channels diff --git a/Tests/test_imagedraw.py b/Tests/test_imagedraw.py index 33e7f8477..5a945e8b2 100644 --- a/Tests/test_imagedraw.py +++ b/Tests/test_imagedraw.py @@ -344,19 +344,26 @@ class TestImageDraw(PillowTestCase): self.assert_image_similar(im, Image.open(expected), 1) def test_floodfill(self): - # Arrange - im = Image.new("RGB", (W, H)) - draw = ImageDraw.Draw(im) - draw.rectangle(BBOX2, outline="yellow", fill="green") - centre_point = (int(W/2), int(H/2)) red = ImageColor.getrgb("red") - im_floodfill = Image.open("Tests/images/imagedraw_floodfill.png") - # Act - ImageDraw.floodfill(im, centre_point, red) + for mode, value in [ + ("L", 1), + ("RGBA", (255, 0, 0, 0)), + ("RGB", red) + ]: + # Arrange + im = Image.new(mode, (W, H)) + draw = ImageDraw.Draw(im) + draw.rectangle(BBOX2, outline="yellow", fill="green") + centre_point = (int(W/2), int(H/2)) - # Assert - self.assert_image_equal(im, im_floodfill) + # Act + ImageDraw.floodfill(im, centre_point, value) + + # Assert + expected = "Tests/images/imagedraw_floodfill_"+mode+".png" + im_floodfill = Image.open(expected) + self.assert_image_equal(im, im_floodfill) # Test that using the same colour does not change the image ImageDraw.floodfill(im, centre_point, red) @@ -366,6 +373,11 @@ class TestImageDraw(PillowTestCase): ImageDraw.floodfill(im, (W, H), red) self.assert_image_equal(im, im_floodfill) + # Test filling at the edge of an image + im = Image.new("RGB", (1, 1)) + ImageDraw.floodfill(im, (0, 0), red) + self.assert_image_equal(im, Image.new("RGB", (1, 1), red)) + def test_floodfill_border(self): # floodfill() is experimental @@ -563,6 +575,20 @@ class TestImageDraw(PillowTestCase): # Assert self.assert_image_similar(im, Image.open(expected), 1) + def test_line_joint(self): + im = Image.new("RGB", (500, 325)) + draw = ImageDraw.Draw(im) + expected = "Tests/images/imagedraw_line_joint_curve.png" + + # Act + xy = [(400, 280), (380, 280), (450, 280), (440, 120), (350, 200), + (310, 280), (300, 280), (250, 280), (250, 200), (150, 200), + (150, 260), (50, 200), (150, 50), (250, 100)] + draw.line(xy, GRAY, 50, "curve") + + # Assert + self.assert_image_similar(im, Image.open(expected), 3) + def test_textsize_empty_string(self): # https://github.com/python-pillow/Pillow/issues/2783 # Arrange @@ -596,12 +622,12 @@ class TestImageDraw(PillowTestCase): ["red", "#f00"] ]: for operation, args in { - 'chord':[BBOX1, 0, 180], - 'ellipse':[BBOX1], - 'shape':[s], - 'pieslice':[BBOX1, -90, 45], - 'polygon':[[(18, 30), (85, 30), (60, 72)]], - 'rectangle':[BBOX1] + 'chord': [BBOX1, 0, 180], + 'ellipse': [BBOX1], + 'shape': [s], + 'pieslice': [BBOX1, -90, 45], + 'polygon': [[(18, 30), (85, 30), (60, 72)]], + 'rectangle': [BBOX1] }.items(): # Arrange im = Image.new(mode, (W, H)) diff --git a/Tests/test_imagefont.py b/Tests/test_imagefont.py index f2116bdc4..82108638c 100644 --- a/Tests/test_imagefont.py +++ b/Tests/test_imagefont.py @@ -215,7 +215,7 @@ class TestImageFont(PillowTestCase): # Act/Assert self.assertRaises( - AssertionError, + ValueError, draw.multiline_text, (0, 0), TEST_TEXT, font=ttf, align="unknown") def test_draw_align(self): diff --git a/Tests/test_imageops.py b/Tests/test_imageops.py index 9c4da2463..70b1659d9 100644 --- a/Tests/test_imageops.py +++ b/Tests/test_imageops.py @@ -24,6 +24,9 @@ class TestImageOps(PillowTestCase): ImageOps.colorize(hopper("L"), (0, 0, 0), (255, 255, 255)) ImageOps.colorize(hopper("L"), "black", "white") + ImageOps.pad(hopper("L"), (128, 128)) + ImageOps.pad(hopper("RGB"), (128, 128)) + ImageOps.crop(hopper("L"), 1) ImageOps.crop(hopper("RGB"), 1) @@ -70,6 +73,26 @@ class TestImageOps(PillowTestCase): newimg = ImageOps.fit(hopper("RGB").resize((100, 1)), (35, 35)) self.assertEqual(newimg.size, (35, 35)) + def test_pad(self): + # Same ratio + im = hopper() + new_size = (im.width * 2, im.height * 2) + new_im = ImageOps.pad(im, new_size) + self.assertEqual(new_im.size, new_size) + + for label, color, new_size in [ + ("h", None, (im.width * 4, im.height * 2)), + ("v", "#f00", (im.width * 2, im.height * 4)) + ]: + for i, centering in enumerate([(0, 0), (0.5, 0.5), (1, 1)]): + new_im = ImageOps.pad(im, new_size, + color=color, centering=centering) + self.assertEqual(new_im.size, new_size) + + target = Image.open( + "Tests/images/imageops_pad_"+label+"_"+str(i)+".jpg") + self.assert_image_similar(new_im, target, 6) + def test_pil163(self): # Division by zero in equalize if < 255 pixels in image (@PIL163) diff --git a/Tests/test_imageqt.py b/Tests/test_imageqt.py index cf9712ace..cecb1b5ee 100644 --- a/Tests/test_imageqt.py +++ b/Tests/test_imageqt.py @@ -33,6 +33,8 @@ class PillowQPixmapTestCase(PillowQtTestCase): from PyQt4.QtGui import QGuiApplication elif ImageQt.qt_version == 'side': from PySide.QtGui import QGuiApplication + elif ImageQt.qt_version == 'side2': + from PySide2.QtGui import QGuiApplication except ImportError: self.skipTest('QGuiApplication not installed') @@ -56,6 +58,8 @@ class TestImageQt(PillowQtTestCase, PillowTestCase): from PyQt4.QtGui import qRgb elif ImageQt.qt_version == 'side': from PySide.QtGui import qRgb + elif ImageQt.qt_version == 'side2': + from PySide2.QtGui import qRgb self.assertEqual(qRgb(0, 0, 0), qRgba(0, 0, 0, 255)) diff --git a/Tests/test_imagetk.py b/Tests/test_imagetk.py index 2df35c584..14ce74eb1 100644 --- a/Tests/test_imagetk.py +++ b/Tests/test_imagetk.py @@ -25,7 +25,7 @@ class TestImageTk(PillowTestCase): self.skipTest("Tk not installed") try: # setup tk - app = tk.Frame() + tk.Frame() # root = tk.Tk() except (tk.TclError) as v: self.skipTest("TCL Error: %s" % v) diff --git a/Tests/test_imagewin_pointers.py b/Tests/test_imagewin_pointers.py index 7178d8cb8..dc78b655c 100644 --- a/Tests/test_imagewin_pointers.py +++ b/Tests/test_imagewin_pointers.py @@ -96,7 +96,6 @@ if sys.platform.startswith('win32'): hdr.biClrImportant = 0 hdc = CreateCompatibleDC(None) - # print('hdc:',hex(hdc)) pixels = ctypes.c_void_p() dib = CreateDIBSection(hdc, ctypes.byref(hdr), DIB_RGB_COLORS, ctypes.byref(pixels), None, 0) diff --git a/Tests/test_lib_pack.py b/Tests/test_lib_pack.py index e0fcf264a..2fb7da281 100644 --- a/Tests/test_lib_pack.py +++ b/Tests/test_lib_pack.py @@ -221,7 +221,8 @@ class TestLibUnpack(PillowTestCase): data_len = data * len(pixels) data = bytes(bytearray(range(1, data_len + 1))) - im = Image.frombytes(mode, (len(pixels), 1), data, "raw", rawmode, 0, 1) + im = Image.frombytes(mode, (len(pixels), 1), data, + "raw", rawmode, 0, 1) for x, pixel in enumerate(pixels): self.assertEqual(pixel, im.getpixel((x, 0))) @@ -265,9 +266,11 @@ class TestLibUnpack(PillowTestCase): def test_P(self): self.assert_unpack("P", "P;1", b'\xe4', 1, 1, 1, 0, 0, 1, 0, 0) self.assert_unpack("P", "P;2", b'\xe4', 3, 2, 1, 0) - # self.assert_unpack("P", "P;2L", b'\xe4', 1, 1, 1, 0) # erroneous? + # erroneous? + # self.assert_unpack("P", "P;2L", b'\xe4', 1, 1, 1, 0) self.assert_unpack("P", "P;4", b'\x02\xef', 0, 2, 14, 15) - # self.assert_unpack("P", "P;4L", b'\x02\xef', 2, 10, 10, 0) # erroneous? + # erroneous? + # self.assert_unpack("P", "P;4L", b'\x02\xef', 2, 10, 10, 0) self.assert_unpack("P", "P", 1, 1, 2, 3, 4) self.assert_unpack("P", "P;R", 1, 128, 64, 192, 32) @@ -309,13 +312,25 @@ class TestLibUnpack(PillowTestCase): "RGBA", "LA;16B", 4, (1, 1, 1, 3), (5, 5, 5, 7), (9, 9, 9, 11)) self.assert_unpack( "RGBA", "RGBA", 4, (1, 2, 3, 4), (5, 6, 7, 8), (9, 10, 11, 12)) + self.assert_unpack( + "RGBA", "RGBAX", 5, (1, 2, 3, 4), (6, 7, 8, 9), (11, 12, 13, 14)) + self.assert_unpack( + "RGBA", "RGBAXX", 6, (1, 2, 3, 4), (7, 8, 9, 10), (13, 14, 15, 16)) self.assert_unpack( "RGBA", "RGBa", 4, (63, 127, 191, 4), (159, 191, 223, 8), (191, 212, 233, 12)) self.assert_unpack( "RGBA", "RGBa", - b'\x01\x02\x03\x00\x10\x20\x30\xff', - (0, 0, 0, 0), (16, 32, 48, 255)) + b'\x01\x02\x03\x00\x10\x20\x30\x7f\x10\x20\x30\xff', + (0, 0, 0, 0), (32, 64, 96, 127), (16, 32, 48, 255)) + self.assert_unpack( + "RGBA", "RGBaX", + b'\x01\x02\x03\x00-\x10\x20\x30\x7f-\x10\x20\x30\xff-', + (0, 0, 0, 0), (32, 64, 96, 127), (16, 32, 48, 255)) + self.assert_unpack( + "RGBA", "RGBaXX", + b'\x01\x02\x03\x00==\x10\x20\x30\x7f!!\x10\x20\x30\xff??', + (0, 0, 0, 0), (32, 64, 96, 127), (16, 32, 48, 255)) self.assert_unpack( "RGBA", "RGBa;16L", 8, (63, 127, 191, 8), (159, 191, 223, 16), (191, 212, 233, 24)) @@ -361,7 +376,8 @@ class TestLibUnpack(PillowTestCase): self.assert_unpack( "RGBA", "YCCA;P", b']bE\x04\xdd\xbej\xed57T\xce\xac\xce:\x11', # random data - (0, 161, 0, 4), (255, 255, 255, 237), (27, 158, 0, 206), (0, 118, 0, 17)) + (0, 161, 0, 4), (255, 255, 255, 237), + (27, 158, 0, 206), (0, 118, 0, 17)) self.assert_unpack( "RGBA", "R", 1, (1, 0, 0, 0), (2, 0, 0, 0), (3, 0, 0, 0)) self.assert_unpack( @@ -413,7 +429,8 @@ class TestLibUnpack(PillowTestCase): self.assert_unpack( "RGBX", "YCC;P", b'D]\x9c\x82\x1a\x91\xfaOC\xe7J\x12', # random data - (127, 102, 0, X), (192, 227, 0, X), (213, 255, 170, X), (98, 255, 133, X)) + (127, 102, 0, X), (192, 227, 0, X), + (213, 255, 170, X), (98, 255, 133, X)) self.assert_unpack("RGBX", "R", 1, (1, 0, 0, 0), (2, 0, 0, 0), (3, 0, 0, 0)) self.assert_unpack("RGBX", "G", 1, diff --git a/Tests/test_numpy.py b/Tests/test_numpy.py index 4efcd2c51..03643ac1e 100644 --- a/Tests/test_numpy.py +++ b/Tests/test_numpy.py @@ -34,7 +34,6 @@ class TestNumpy(PillowTestCase): i = Image.fromarray(a) if list(i.getchannel(0).getdata()) != list(range(100)): print("data mismatch for", dtype) - # print(dtype, list(i.getdata())) return i # Check supported 1-bit integer formats diff --git a/Tests/test_pdfparser.py b/Tests/test_pdfparser.py index 42c813520..b7373842e 100644 --- a/Tests/test_pdfparser.py +++ b/Tests/test_pdfparser.py @@ -26,7 +26,9 @@ class TestPdfParser(PillowTestCase): def test_parsing(self): self.assertEqual(PdfParser.interpret_name(b"Name#23Hash"), b"Name#Hash") - self.assertEqual(PdfParser.interpret_name(b"Name#23Hash", as_text=True), "Name#Hash") + self.assertEqual(PdfParser.interpret_name( + b"Name#23Hash", as_text=True + ), "Name#Hash") self.assertEqual(PdfParser.get_value(b"1 2 R ", 0), (IndirectReference(1, 2), 5)) self.assertEqual(PdfParser.get_value(b"true[", 0), (True, 4)) @@ -72,7 +74,9 @@ class TestPdfParser(PillowTestCase): self.assertIsInstance(a, list) self.assertEqual(len(a), 4) self.assertEqual(a[0], PdfName("Name")) - s = PdfParser.get_value(b"<>\nstream\nabcde\nendstream<<...", 0)[0] + s = PdfParser.get_value( + b"<>\nstream\nabcde\nendstream<<...", 0 + )[0] self.assertIsInstance(s, PdfStream) self.assertEqual(s.dictionary.Name, "value") self.assertEqual(s.decode(), b"abcde") diff --git a/Tests/test_qt_image_fromqpixmap.py b/Tests/test_qt_image_fromqpixmap.py index 543b74bbf..0127f77e2 100644 --- a/Tests/test_qt_image_fromqpixmap.py +++ b/Tests/test_qt_image_fromqpixmap.py @@ -1,5 +1,5 @@ from helper import unittest, PillowTestCase, hopper -from test_imageqt import PillowQtTestCase, PillowQPixmapTestCase +from test_imageqt import PillowQPixmapTestCase from PIL import ImageQt @@ -7,7 +7,6 @@ from PIL import ImageQt class TestFromQPixmap(PillowQPixmapTestCase, PillowTestCase): def roundtrip(self, expected): - PillowQtTestCase.setUp(self) result = ImageQt.fromqpixmap(ImageQt.toqpixmap(expected)) # Qt saves all pixmaps as rgb self.assert_image_equal(result, expected.convert('RGB')) diff --git a/Tests/test_qt_image_toqimage.py b/Tests/test_qt_image_toqimage.py index c9971cf73..28871d201 100644 --- a/Tests/test_qt_image_toqimage.py +++ b/Tests/test_qt_image_toqimage.py @@ -13,19 +13,26 @@ if ImageQt.qt_is_installed: QT_VERSION = 5 except (ImportError, RuntimeError): try: - from PyQt4 import QtGui - from PyQt4.QtGui import QWidget, QHBoxLayout, QLabel, QApplication - QT_VERSION = 4 + from PySide2 import QtGui + from PySide2.QtWidgets import QWidget, QHBoxLayout, QLabel, \ + QApplication + QT_VERSION = 5 except (ImportError, RuntimeError): - from PySide import QtGui - from PySide.QtGui import QWidget, QHBoxLayout, QLabel, QApplication - QT_VERSION = 4 + 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 ('RGB', 'RGBA', 'L', 'P', '1'): src = hopper(mode) data = ImageQt.toqimage(src) @@ -61,8 +68,6 @@ class TestToQImage(PillowQtTestCase, PillowTestCase): self.assert_image_equal(reloaded, src) def test_segfault(self): - PillowQtTestCase.setUp(self) - app = QApplication([]) ex = Example() assert(app) # Silence warning diff --git a/Tests/test_qt_image_toqpixmap.py b/Tests/test_qt_image_toqpixmap.py index c6555d7ff..5de7810f5 100644 --- a/Tests/test_qt_image_toqpixmap.py +++ b/Tests/test_qt_image_toqpixmap.py @@ -1,5 +1,5 @@ from helper import unittest, PillowTestCase, hopper -from test_imageqt import PillowQtTestCase, PillowQPixmapTestCase +from test_imageqt import PillowQPixmapTestCase from PIL import ImageQt @@ -10,8 +10,6 @@ if ImageQt.qt_is_installed: class TestToQPixmap(PillowQPixmapTestCase, PillowTestCase): def test_sanity(self): - PillowQtTestCase.setUp(self) - for mode in ('1', 'RGB', 'RGBA', 'L', 'P'): data = ImageQt.toqpixmap(hopper(mode)) diff --git a/depends/install_extra_test_images.sh b/depends/install_extra_test_images.sh index 1e83e6308..e3c091754 100755 --- a/depends/install_extra_test_images.sh +++ b/depends/install_extra_test_images.sh @@ -1,10 +1,20 @@ #!/bin/bash # install extra test images -rm -r test_images +rm -rf test_images -# Use SVN to just fetch a single git subdirectory -svn checkout https://github.com/python-pillow/pillow-depends/trunk/test_images +# Use SVN to just fetch a single Git subdirectory +svn_checkout() +{ + if [ ! -z $1 ]; then + echo "" + echo "Retrying svn checkout..." + echo "" + fi + + svn checkout https://github.com/python-pillow/pillow-depends/trunk/test_images +} +svn_checkout || svn_checkout retry || svn_checkout retry || svn_checkout retry cp -r test_images/* ../Tests/images diff --git a/docs/handbook/writing-your-own-file-decoder.rst b/docs/handbook/writing-your-own-file-decoder.rst index aa2463bd1..107e25f36 100644 --- a/docs/handbook/writing-your-own-file-decoder.rst +++ b/docs/handbook/writing-your-own-file-decoder.rst @@ -171,7 +171,6 @@ The fields are used as follows: stride defaults to 0. **orientation** - Whether the first line in the image is the top line on the screen (1), or the bottom line (-1). If omitted, the orientation defaults to 1. @@ -204,7 +203,7 @@ table describes some commonly used **raw modes**: +-----------+-----------------------------------------------------------------+ | ``RGBX`` | 24-bit true colour, stored as (red, green, blue, pad). | +-----------+-----------------------------------------------------------------+ -| ``RGB;L`` | 24-bit true colour, line interleaved (first all red pixels, the | +| ``RGB;L`` | 24-bit true colour, line interleaved (first all red pixels, then| | | all green pixels, finally all blue pixels). | +-----------+-----------------------------------------------------------------+ diff --git a/docs/reference/ImageDraw.rst b/docs/reference/ImageDraw.rst index 6057712e2..7e5192499 100644 --- a/docs/reference/ImageDraw.rst +++ b/docs/reference/ImageDraw.rst @@ -171,19 +171,22 @@ Methods :param outline: Color to use for the outline. :param fill: Color to use for the fill. -.. py:method:: PIL.ImageDraw.ImageDraw.line(xy, fill=None, width=0) +.. py:method:: PIL.ImageDraw.ImageDraw.line(xy, fill=None, width=0, joint=None) Draws a line between the coordinates in the **xy** list. :param xy: Sequence of either 2-tuples like ``[(x, y), (x, y), ...]`` or numeric values like ``[x, y, x, y, ...]``. :param fill: Color to use for the line. - :param width: The line width, in pixels. Note that line - joins are not handled well, so wide polylines will not look good. + :param width: The line width, in pixels. .. versionadded:: 1.1.5 .. note:: This option was broken until version 1.1.6. + :param joint: Joint type between a sequence of lines. It can be "curve", + for rounded edges, or None. + + .. versionadded:: 5.3.0 .. py:method:: PIL.ImageDraw.ImageDraw.pieslice(xy, start, end, fill=None, outline=None) @@ -250,9 +253,8 @@ Methods :param align: If the text is passed on to multiline_text(), "left", "center" or "right". :param direction: Direction of the text. It can be 'rtl' (right to - left), 'ltr' (left to right), 'ttb' (top to - bottom) or 'btt' (bottom to top). Requires - libraqm. + left), 'ltr' (left to right) or 'ttb' (top to bottom). + Requires libraqm. .. versionadded:: 4.2.0 @@ -280,9 +282,8 @@ Methods :param spacing: The number of pixels between lines. :param align: "left", "center" or "right". :param direction: Direction of the text. It can be 'rtl' (right to - left), 'ltr' (left to right), 'ttb' (top to - bottom) or 'btt' (bottom to top). Requires - libraqm. + left), 'ltr' (left to right) or 'ttb' (top to bottom). + Requires libraqm. .. versionadded:: 4.2.0 @@ -309,9 +310,8 @@ Methods :param spacing: If the text is passed on to multiline_textsize(), the number of pixels between lines. :param direction: Direction of the text. It can be 'rtl' (right to - left), 'ltr' (left to right), 'ttb' (top to - bottom) or 'btt' (bottom to top). Requires - libraqm. + left), 'ltr' (left to right) or 'ttb' (top to bottom). + Requires libraqm. .. versionadded:: 4.2.0 @@ -336,9 +336,8 @@ Methods :param font: An :py:class:`~PIL.ImageFont.ImageFont` instance. :param spacing: The number of pixels between lines. :param direction: Direction of the text. It can be 'rtl' (right to - left), 'ltr' (left to right), 'ttb' (top to - bottom) or 'btt' (bottom to top). Requires - libraqm. + left), 'ltr' (left to right) or 'ttb' (top to bottom). + Requires libraqm. .. versionadded:: 4.2.0 diff --git a/docs/reference/ImageFont.rst b/docs/reference/ImageFont.rst index 080e52137..55ce3d382 100644 --- a/docs/reference/ImageFont.rst +++ b/docs/reference/ImageFont.rst @@ -67,9 +67,8 @@ Methods .. versionadded:: 1.1.5 :param direction: Direction of the text. It can be 'rtl' (right to - left), 'ltr' (left to right), 'ttb' (top to - bottom) or 'btt' (bottom to top). Requires - libraqm. + left), 'ltr' (left to right) or 'ttb' (top to bottom). + Requires libraqm. .. versionadded:: 4.2.0 diff --git a/docs/reference/ImageQt.rst b/docs/reference/ImageQt.rst index 7bc426eec..386401075 100644 --- a/docs/reference/ImageQt.rst +++ b/docs/reference/ImageQt.rst @@ -4,8 +4,8 @@ :py:mod:`ImageQt` Module ======================== -The :py:mod:`ImageQt` module contains support for creating PyQt4, PyQt5 or -PySide QImage objects from PIL images. +The :py:mod:`ImageQt` module contains support for creating PyQt4, PyQt5, PySide or +PySide2 QImage objects from PIL images. .. versionadded:: 1.1.6 diff --git a/mp_compile.py b/mp_compile.py index 5fac2399f..9043e24b8 100644 --- a/mp_compile.py +++ b/mp_compile.py @@ -1,5 +1,7 @@ # A monkey patch of the base distutils.ccompiler to use parallel builds # Tested on 2.7, looks to be identical to 3.3. +# Only applied on Python < 3.5 because otherwise, it conflicts with Python's +# own newly-added support for parallel builds. from __future__ import print_function from multiprocessing import Pool, cpu_count @@ -77,4 +79,6 @@ def install(): "%s processes" % MAX_PROCS) -install() +# We monkeypatch only versions earlier than 3.5 +if sys.version_info < (3, 5): + install() diff --git a/setup.py b/setup.py index 9529787f9..15d81e465 100755 --- a/setup.py +++ b/setup.py @@ -205,6 +205,12 @@ class pil_build_ext(build_ext): if self.debug: global DEBUG DEBUG = True + if sys.version_info >= (3, 5) and not self.parallel: + # For Python < 3.5, we monkeypatch distutils to have parallel + # builds. If --parallel (or -j) wasn't specified, we want to + # reproduce the same behavior as before, that is, auto-detect the + # number of jobs. + self.parallel = mp_compile.MAX_PROCS for x in self.feature: if getattr(self, 'disable_%s' % x): setattr(self.feature, x, False) @@ -518,10 +524,7 @@ class pil_build_ext(build_ext): if _find_include_file(self, 'tiff.h'): if _find_library_file(self, "tiff"): feature.tiff = "tiff" - if (sys.platform == "win32" and - _find_library_file(self, "libtiff")): - feature.tiff = "libtiff" - if (sys.platform == "darwin" and + if (sys.platform in ["win32", "darwin"] and _find_library_file(self, "libtiff")): feature.tiff = "libtiff" @@ -547,7 +550,6 @@ class pil_build_ext(build_ext): break if freetype_version: feature.freetype = "freetype" - feature.freetype_version = freetype_version if subdir: _add_directory(self.compiler.include_dirs, subdir, 0) diff --git a/src/PIL/BdfFontFile.py b/src/PIL/BdfFontFile.py index c8bc60461..eac19bde1 100644 --- a/src/PIL/BdfFontFile.py +++ b/src/PIL/BdfFontFile.py @@ -110,20 +110,6 @@ class BdfFontFile(FontFile.FontFile): if s.find(b"LogicalFontDescription") < 0: comments.append(s[i+1:-1].decode('ascii')) - # font = props["FONT"].split("-") - - # font[4] = bdf_slant[font[4].upper()] - # font[11] = bdf_spacing[font[11].upper()] - - # ascent = int(props["FONT_ASCENT"]) - # descent = int(props["FONT_DESCENT"]) - - # fontname = ";".join(font[1:]) - - # print("#", fontname) - # for i in comments: - # print("#", i) - while True: c = bdf_char(fp) if not c: diff --git a/src/PIL/CurImagePlugin.py b/src/PIL/CurImagePlugin.py index e4257cd5a..db9ea9874 100644 --- a/src/PIL/CurImagePlugin.py +++ b/src/PIL/CurImagePlugin.py @@ -56,14 +56,6 @@ class CurImageFile(BmpImagePlugin.BmpImageFile): m = s elif i8(s[0]) > i8(m[0]) and i8(s[1]) > i8(m[1]): m = s - # print("width", i8(s[0])) - # print("height", i8(s[1])) - # print("colors", i8(s[2])) - # print("reserved", i8(s[3])) - # print("hotspot x", i16(s[4:])) - # print("hotspot y", i16(s[6:])) - # print("bytes", i32(s[8:])) - # print("offset", i32(s[12:])) if not m: raise TypeError("No cursors were found") diff --git a/src/PIL/EpsImagePlugin.py b/src/PIL/EpsImagePlugin.py index cb2c00d20..601cc4436 100644 --- a/src/PIL/EpsImagePlugin.py +++ b/src/PIL/EpsImagePlugin.py @@ -82,7 +82,6 @@ def Ghostscript(tile, size, fp, scale=1): # resolution is dependent on bbox and size res = (float((72.0 * size[0]) / (bbox[2]-bbox[0])), float((72.0 * size[1]) / (bbox[3]-bbox[1]))) - # print("Ghostscript", scale, size, orig_size, bbox, orig_bbox, res) import subprocess import tempfile @@ -357,54 +356,49 @@ def _save(im, fp, filename, eps=1): else: raise ValueError("image mode is not supported") - class NoCloseStream(object): - def __init__(self, fp): - self.fp = fp - - def __getattr__(self, name): - return getattr(self.fp, name) - - def close(self): - pass - base_fp = fp + wrapped_fp = False if fp != sys.stdout: - fp = NoCloseStream(fp) if sys.version_info.major > 2: fp = io.TextIOWrapper(fp, encoding='latin-1') + wrapped_fp = True + + try: + if eps: + # + # write EPS header + fp.write("%!PS-Adobe-3.0 EPSF-3.0\n") + fp.write("%%Creator: PIL 0.1 EpsEncode\n") + # fp.write("%%CreationDate: %s"...) + fp.write("%%%%BoundingBox: 0 0 %d %d\n" % im.size) + fp.write("%%Pages: 1\n") + fp.write("%%EndComments\n") + fp.write("%%Page: 1 1\n") + fp.write("%%ImageData: %d %d " % im.size) + fp.write("%d %d 0 1 1 \"%s\"\n" % operator) - if eps: # - # write EPS header - fp.write("%!PS-Adobe-3.0 EPSF-3.0\n") - fp.write("%%Creator: PIL 0.1 EpsEncode\n") - # fp.write("%%CreationDate: %s"...) - fp.write("%%%%BoundingBox: 0 0 %d %d\n" % im.size) - fp.write("%%Pages: 1\n") - fp.write("%%EndComments\n") - fp.write("%%Page: 1 1\n") - fp.write("%%ImageData: %d %d " % im.size) - fp.write("%d %d 0 1 1 \"%s\"\n" % operator) + # image header + fp.write("gsave\n") + fp.write("10 dict begin\n") + fp.write("/buf %d string def\n" % (im.size[0] * operator[1])) + fp.write("%d %d scale\n" % im.size) + fp.write("%d %d 8\n" % im.size) # <= bits + fp.write("[%d 0 0 -%d 0 %d]\n" % (im.size[0], im.size[1], im.size[1])) + fp.write("{ currentfile buf readhexstring pop } bind\n") + fp.write(operator[2] + "\n") + if hasattr(fp, "flush"): + fp.flush() - # - # image header - fp.write("gsave\n") - fp.write("10 dict begin\n") - fp.write("/buf %d string def\n" % (im.size[0] * operator[1])) - fp.write("%d %d scale\n" % im.size) - fp.write("%d %d 8\n" % im.size) # <= bits - fp.write("[%d 0 0 -%d 0 %d]\n" % (im.size[0], im.size[1], im.size[1])) - fp.write("{ currentfile buf readhexstring pop } bind\n") - fp.write(operator[2] + "\n") - if hasattr(fp, "flush"): - fp.flush() + ImageFile._save(im, base_fp, [("eps", (0, 0)+im.size, 0, None)]) - ImageFile._save(im, base_fp, [("eps", (0, 0)+im.size, 0, None)]) - - fp.write("\n%%%%EndBinary\n") - fp.write("grestore end\n") - if hasattr(fp, "flush"): - fp.flush() + fp.write("\n%%%%EndBinary\n") + fp.write("grestore end\n") + if hasattr(fp, "flush"): + fp.flush() + finally: + if wrapped_fp: + fp.detach() # # -------------------------------------------------------------------- diff --git a/src/PIL/FontFile.py b/src/PIL/FontFile.py index 46e49bc4e..305e8afa2 100644 --- a/src/PIL/FontFile.py +++ b/src/PIL/FontFile.py @@ -90,7 +90,6 @@ class FontFile(object): x = xx s = src[0] + x0, src[1] + y0, src[2] + x0, src[3] + y0 self.bitmap.paste(im.crop(src), s) - # print(chr(i), dst, s) self.metrics[i] = d, dst, s def save(self, filename): diff --git a/src/PIL/FpxImagePlugin.py b/src/PIL/FpxImagePlugin.py index d7bba42eb..509fd7d91 100644 --- a/src/PIL/FpxImagePlugin.py +++ b/src/PIL/FpxImagePlugin.py @@ -114,8 +114,6 @@ class FpxImageFile(ImageFile.ImageFile): if id in prop: self.jpeg[i] = prop[id] - # print(len(self.jpeg), "tables loaded") - self._open_subimage(1, self.maxid) def _open_subimage(self, index=1, subimage=0): @@ -143,8 +141,6 @@ class FpxImageFile(ImageFile.ImageFile): offset = i32(s, 28) length = i32(s, 32) - # print(size, self.mode, self.rawmode) - if size != self.size: raise IOError("subimage mismatch") diff --git a/src/PIL/GdImageFile.py b/src/PIL/GdImageFile.py index 2ca1e8218..1a0b2c910 100644 --- a/src/PIL/GdImageFile.py +++ b/src/PIL/GdImageFile.py @@ -61,7 +61,8 @@ class GdImageFile(ImageFile.ImageFile): self.palette = ImagePalette.raw("XBGR", s[7+trueColorOffset+4:7+trueColorOffset+4+256*4]) - self.tile = [("raw", (0, 0)+self.size, 7+trueColorOffset+4+256*4, ("L", 0, 1))] + self.tile = [("raw", (0, 0)+self.size, 7+trueColorOffset+4+256*4, + ("L", 0, 1))] def open(fp, mode="r"): diff --git a/src/PIL/Image.py b/src/PIL/Image.py index f13a98276..e5e78cf27 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -443,7 +443,6 @@ def _getdecoder(mode, decoder_name, args, extra=()): try: # get decoder decoder = getattr(core, decoder_name + "_decoder") - # print(decoder, mode, args + extra) return decoder(mode, *args + extra) except AttributeError: raise IOError("decoder %s not available" % decoder_name) @@ -465,7 +464,6 @@ def _getencoder(mode, encoder_name, args, extra=()): try: # get encoder encoder = getattr(core, encoder_name + "_encoder") - # print(encoder, mode, args + extra) return encoder(mode, *args + extra) except AttributeError: raise IOError("encoder %s not available" % encoder_name) @@ -900,12 +898,28 @@ class Image(object): if not mode or (mode == self.mode and not matrix): return self.copy() + has_transparency = self.info.get('transparency') is not None if matrix: # matrix conversion if mode not in ("L", "RGB"): raise ValueError("illegal conversion") im = self.im.convert_matrix(mode, matrix) - return self._new(im) + new = self._new(im) + if has_transparency and self.im.bands == 3: + transparency = new.info['transparency'] + + def convert_transparency(m, v): + v = m[0]*v[0] + m[1]*v[1] + m[2]*v[2] + m[3]*0.5 + return max(0, min(255, int(v))) + if mode == "L": + transparency = convert_transparency(matrix, transparency) + elif len(mode) == 3: + transparency = tuple([ + convert_transparency(matrix[i*4:i*4+4], transparency) + for i in range(0, len(transparency)) + ]) + new.info['transparency'] = transparency + return new if mode == "P" and self.mode == "RGBA": return self.quantize(colors) @@ -913,8 +927,7 @@ class Image(object): trns = None delete_trns = False # transparency handling - if "transparency" in self.info and \ - self.info['transparency'] is not None: + if has_transparency: if self.mode in ('L', 'RGB') and mode == 'RGBA': # Use transparent conversion to promote from transparent # color to an alpha channel. @@ -1104,12 +1117,9 @@ class Image(object): x0, y0, x1, y1 = map(int, map(round, box)) - if x1 < x0: - x1 = x0 - if y1 < y0: - y1 = y0 + absolute_values = (abs(x1 - x0), abs(y1 - y0)) - _decompression_bomb_check((x1, y1)) + _decompression_bomb_check(absolute_values) return im.crop((x0, y0, x1, y1)) @@ -1894,7 +1904,7 @@ class Image(object): parameter should always be used. :param params: Extra parameters to the image writer. :returns: None - :exception KeyError: If the output format could not be determined + :exception ValueError: If the output format could not be determined from the file name. Use the format option to solve this. :exception IOError: If the file could not be written. The file may have been created, and may contain partial data. @@ -2448,7 +2458,7 @@ def fromarray(obj, mode=None): from PIL import Image import numpy as np im = Image.open('hopper.jpg') - a = numpy.asarray(im) + a = np.asarray(im) Then this can be used to convert it to a Pillow image:: @@ -2470,7 +2480,6 @@ def fromarray(obj, mode=None): typekey = (1, 1) + shape[2:], arr['typestr'] mode, rawmode = _fromarray_typemap[typekey] except KeyError: - # print(typekey) raise TypeError("Cannot handle this data type") else: rawmode = mode diff --git a/src/PIL/ImageDraw.py b/src/PIL/ImageDraw.py index ca8c1d17b..511bae537 100644 --- a/src/PIL/ImageDraw.py +++ b/src/PIL/ImageDraw.py @@ -30,6 +30,7 @@ # See the README file for information on usage and redistribution. # +import math import numbers from . import Image, ImageColor @@ -149,11 +150,64 @@ class ImageDraw(object): if ink is not None and ink != fill: self.draw.draw_ellipse(xy, ink, 0) - def line(self, xy, fill=None, width=0): + def line(self, xy, fill=None, width=0, joint=None): """Draw a line, or a connected sequence of line segments.""" - ink, fill = self._getink(fill) + ink = self._getink(fill)[0] if ink is not None: self.draw.draw_lines(xy, ink, width) + if joint == "curve" and width > 4: + for i in range(1, len(xy)-1): + point = xy[i] + angles = [ + math.degrees(math.atan2( + end[0] - start[0], start[1] - end[1] + )) % 360 + for start, end in ((xy[i-1], point), (point, xy[i+1])) + ] + if angles[0] == angles[1]: + # This is a straight line, so no joint is required + continue + + def coord_at_angle(coord, angle): + x, y = coord + angle -= 90 + distance = width/2 - 1 + return tuple([ + p + + (math.floor(p_d) if p_d > 0 else math.ceil(p_d)) + for p, p_d in + ((x, distance * math.cos(math.radians(angle))), + (y, distance * math.sin(math.radians(angle)))) + ]) + flipped = ((angles[1] > angles[0] and + angles[1] - 180 > angles[0]) or + (angles[1] < angles[0] and + angles[1] + 180 > angles[0])) + coords = [ + (point[0] - width/2 + 1, point[1] - width/2 + 1), + (point[0] + width/2 - 1, point[1] + width/2 - 1) + ] + if flipped: + start, end = (angles[1] + 90, angles[0] + 90) + else: + start, end = (angles[0] - 90, angles[1] - 90) + self.pieslice(coords, start - 90, end - 90, fill) + + if width > 8: + # Cover potential gaps between the line and the joint + if flipped: + gapCoords = [ + coord_at_angle(point, angles[0]+90), + point, + coord_at_angle(point, angles[1]+90) + ] + else: + gapCoords = [ + coord_at_angle(point, angles[0]-90), + point, + coord_at_angle(point, angles[1]-90) + ] + self.line(gapCoords, fill, width=3) def shape(self, shape, fill=None, outline=None): """(Experimental) Draw a shape.""" @@ -246,7 +300,7 @@ class ImageDraw(object): elif align == "right": left += (max_width - widths[idx]) else: - assert False, 'align must be "left", "center" or "right"' + raise ValueError('align must be "left", "center" or "right"') self.text((left, top), line, fill, font, anchor, direction=direction, features=features) top += line_spacing @@ -341,6 +395,7 @@ def floodfill(image, xy, value, border=None, thresh=0): homogeneous, but similar, colors. """ # based on an implementation by Eric S. Raymond + # amended by yo1995 @20180806 pixel = image.load() x, y = xy try: @@ -350,39 +405,36 @@ def floodfill(image, xy, value, border=None, thresh=0): pixel[x, y] = value except (ValueError, IndexError): return # seed point outside image - edge = [(x, y)] - if border is None: - while edge: - newedge = [] - for (x, y) in edge: - for (s, t) in ((x+1, y), (x-1, y), (x, y+1), (x, y-1)): - try: - p = pixel[s, t] - except IndexError: - pass + edge = {(x, y)} + full_edge = set() # use a set to keep record of current and previous edge pixels to reduce memory consumption + while edge: + new_edge = set() + for (x, y) in edge: # 4 adjacent method + for (s, t) in ((x+1, y), (x-1, y), (x, y+1), (x, y-1)): + if (s, t) in full_edge: + continue # if already processed, skip + try: + p = pixel[s, t] + except (ValueError, IndexError): + pass + else: + full_edge.add((s, t)) + if border is None: + fill = _color_diff(p, background) <= thresh else: - if _color_diff(p, background) <= thresh: - pixel[s, t] = value - newedge.append((s, t)) - edge = newedge + fill = p != value and p != border + if fill: + pixel[s, t] = value + new_edge.add((s, t)) + full_edge = edge # discard pixels processed + edge = new_edge + + +def _color_diff(color1, color2): + """ + Uses 1-norm distance to calculate difference between two values. + """ + if isinstance(color2, tuple): + return sum([abs(color1[i]-color2[i]) for i in range(0, len(color2))]) else: - while edge: - newedge = [] - for (x, y) in edge: - for (s, t) in ((x+1, y), (x-1, y), (x, y+1), (x, y-1)): - try: - p = pixel[s, t] - except IndexError: - pass - else: - if p != value and p != border: - pixel[s, t] = value - newedge.append((s, t)) - edge = newedge - - -def _color_diff(rgb1, rgb2): - """ - Uses 1-norm distance to calculate difference between two rgb values. - """ - return abs(rgb1[0]-rgb2[0]) + abs(rgb1[1]-rgb2[1]) + abs(rgb1[2]-rgb2[2]) + return abs(color1-color2) diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py index bdcc43d1f..915557a57 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -30,7 +30,6 @@ from . import Image from ._util import isPath import io -import os import sys import struct diff --git a/src/PIL/ImageFilter.py b/src/PIL/ImageFilter.py index e77349df0..de99e6410 100644 --- a/src/PIL/ImageFilter.py +++ b/src/PIL/ImageFilter.py @@ -33,7 +33,14 @@ class MultibandFilter(Filter): pass -class Kernel(MultibandFilter): +class BuiltinFilter(MultibandFilter): + def filter(self, image): + if image.mode == "P": + raise ValueError("cannot filter palette images") + return image.filter(*self.filterargs) + + +class Kernel(BuiltinFilter): """ Create a convolution kernel. The current version only supports 3x3 and 5x5 integer and floating point kernels. @@ -60,16 +67,6 @@ class Kernel(MultibandFilter): raise ValueError("not enough coefficients in kernel") self.filterargs = size, scale, offset, kernel - def filter(self, image): - if image.mode == "P": - raise ValueError("cannot filter palette images") - return image.filter(*self.filterargs) - - -class BuiltinFilter(Kernel): - def __init__(self): - pass - class RankFilter(Filter): """ diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index 099ccc4ff..5384a725b 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -162,7 +162,8 @@ class FreeTypeFont(object): size, offset = self.font.getsize(text, direction, features) return (size[0] + offset[0], size[1] + offset[1]) - def getsize_multiline(self, text, direction=None, spacing=4, features=None): + def getsize_multiline(self, text, direction=None, + spacing=4, features=None): max_width = 0 lines = self._multiline_split(text) line_spacing = self.getsize('A')[1] + spacing diff --git a/src/PIL/ImageMorph.py b/src/PIL/ImageMorph.py index 579ee4e1a..54ceb7905 100644 --- a/src/PIL/ImageMorph.py +++ b/src/PIL/ImageMorph.py @@ -151,11 +151,6 @@ class LutBuilder(object): patterns += self._pattern_permute(pattern, options, result) -# # Debugging -# for p, r in patterns: -# print(p, r) -# print('--') - # compile the patterns into regular expressions for speed for i, pattern in enumerate(patterns): p = pattern[0].replace('.', 'X').replace('X', '[01]') diff --git a/src/PIL/ImageOps.py b/src/PIL/ImageOps.py index 9b470062a..9f516bac1 100644 --- a/src/PIL/ImageOps.py +++ b/src/PIL/ImageOps.py @@ -221,6 +221,50 @@ def colorize(image, black, white, mid=None, blackpoint=0, return _lut(image, red + green + blue) +def pad(image, size, method=Image.NEAREST, color=None, centering=(0.5, 0.5)): + """ + Returns a sized and padded version of the image, expanded to fill the + requested aspect ratio and size. + + :param image: The image to size and crop. + :param size: The requested output size in pixels, given as a + (width, height) tuple. + :param method: What resampling method to use. Default is + :py:attr:`PIL.Image.NEAREST`. + :param color: The background color of the padded image. + :param centering: Control the position of the original image within the + padded version. + (0.5, 0.5) will keep the image centered + (0, 0) will keep the image aligned to the top left + (1, 1) will keep the image aligned to the bottom + right + :return: An image. + """ + + im_ratio = image.width / image.height + dest_ratio = float(size[0]) / size[1] + + if im_ratio == dest_ratio: + out = image.resize(size, resample=method) + else: + out = Image.new(image.mode, size, color) + if im_ratio > dest_ratio: + new_height = int(image.height / image.width * size[0]) + if new_height != size[1]: + image = image.resize((size[0], new_height), resample=method) + + y = int((size[1] - new_height) * max(0, min(centering[1], 1))) + out.paste(image, (0, y)) + else: + new_width = int(image.width / image.height * size[1]) + if new_width != size[0]: + image = image.resize((new_width, size[1]), resample=method) + + x = int((size[0] - new_width) * max(0, min(centering[0], 1))) + out.paste(image, (x, 0)) + return out + + def crop(image, border=0): """ Remove border from image. The same amount of pixels are removed diff --git a/src/PIL/ImageQt.py b/src/PIL/ImageQt.py index 2930c1d9c..e60261360 100644 --- a/src/PIL/ImageQt.py +++ b/src/PIL/ImageQt.py @@ -23,16 +23,21 @@ import sys qt_versions = [ ['5', 'PyQt5'], + ['side2', 'PySide2'], ['4', 'PyQt4'], ['side', 'PySide'] ] # If a version has already been imported, attempt it first -qt_versions.sort(key=lambda qt_version: qt_version[1] in sys.modules, reverse=True) +qt_versions.sort(key=lambda qt_version: qt_version[1] in sys.modules, + reverse=True) for qt_version, qt_module in qt_versions: try: if qt_module == 'PyQt5': from PyQt5.QtGui import QImage, qRgba, QPixmap from PyQt5.QtCore import QBuffer, QIODevice + elif qt_module == 'PySide2': + from PySide2.QtGui import QImage, qRgba, QPixmap + from PySide2.QtCore import QBuffer, QIODevice elif qt_module == 'PyQt4': from PyQt4.QtGui import QImage, qRgba, QPixmap from PyQt4.QtCore import QBuffer, QIODevice diff --git a/src/PIL/ImageTk.py b/src/PIL/ImageTk.py index 17bf32f62..c56f5560a 100644 --- a/src/PIL/ImageTk.py +++ b/src/PIL/ImageTk.py @@ -32,13 +32,6 @@ if sys.version_info.major > 2: else: import Tkinter as tkinter -# required for pypy, which always has cffi installed -try: - from cffi import FFI - ffi = FFI() -except ImportError: - pass - from . import Image from io import BytesIO @@ -192,7 +185,11 @@ class PhotoImage(object): from . import _imagingtk try: if hasattr(tk, 'interp'): - # Pypy is using a ffi cdata element + # Required for PyPy, which always has CFFI installed + from cffi import FFI + ffi = FFI() + + # PyPy is using an FFI CDATA element # (Pdb) self.tk.interp # _imagingtk.tkinit( diff --git a/src/PIL/IptcImagePlugin.py b/src/PIL/IptcImagePlugin.py index f5a8de17e..83885e284 100644 --- a/src/PIL/IptcImagePlugin.py +++ b/src/PIL/IptcImagePlugin.py @@ -103,8 +103,6 @@ class IptcImageFile(ImageFile.ImageFile): else: self.info[tag] = tagdata - # print(tag, self.info[tag]) - # mode layers = i8(self.info[(3, 60)][0]) component = i8(self.info[(3, 60)][1]) diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py index a75e3d428..e38306041 100644 --- a/src/PIL/JpegImagePlugin.py +++ b/src/PIL/JpegImagePlugin.py @@ -334,7 +334,6 @@ class JpegImageFile(ImageFile.ImageFile): if i in MARKER: name, description, handler = MARKER[i] - # print(hex(i), name, description) if handler is not None: handler(self, i) if i == 0xFFDA: # start of scan diff --git a/src/PIL/PdfParser.py b/src/PIL/PdfParser.py index 971f44514..e99040bce 100644 --- a/src/PIL/PdfParser.py +++ b/src/PIL/PdfParser.py @@ -450,12 +450,12 @@ class PdfParser: self.pages_ref = self.next_object_id(0) self.rewrite_pages() self.write_obj(self.root_ref, - Type=PdfName(b"Catalog"), - Pages=self.pages_ref) + Type=PdfName(b"Catalog"), + Pages=self.pages_ref) self.write_obj(self.pages_ref, - Type=PdfName(b"Pages"), - Count=len(self.pages), - Kids=self.pages) + Type=PdfName(b"Pages"), + Count=len(self.pages), + Kids=self.pages) return self.root_ref def rewrite_pages(self): diff --git a/src/PIL/SpiderImagePlugin.py b/src/PIL/SpiderImagePlugin.py index d502779e2..a0d5f17a4 100644 --- a/src/PIL/SpiderImagePlugin.py +++ b/src/PIL/SpiderImagePlugin.py @@ -74,7 +74,6 @@ def isSpiderHeader(t): labrec = int(h[13]) # no. records in file header labbyt = int(h[22]) # total no. of bytes in header lenbyt = int(h[23]) # record length in bytes - # print("labrec = %d, labbyt = %d, lenbyt = %d" % (labrec,labbyt,lenbyt)) if labbyt != (labrec * lenbyt): return 0 # looks like a valid header diff --git a/src/PIL/TgaImagePlugin.py b/src/PIL/TgaImagePlugin.py index 57b6ae2c8..02893e837 100644 --- a/src/PIL/TgaImagePlugin.py +++ b/src/PIL/TgaImagePlugin.py @@ -20,6 +20,8 @@ from . import Image, ImageFile, ImagePalette from ._binary import i8, i16le as i16, o8, o16le as o16 +import warnings + __version__ = "0.3" @@ -53,7 +55,7 @@ class TgaImageFile(ImageFile.ImageFile): # process header s = self.fp.read(18) - idlen = i8(s[0]) + id_len = i8(s[0]) colormaptype = i8(s[1]) imagetype = i8(s[2]) @@ -100,8 +102,8 @@ class TgaImageFile(ImageFile.ImageFile): if imagetype & 8: self.info["compression"] = "tga_rle" - if idlen: - self.info["id_section"] = self.fp.read(idlen) + if id_len: + self.info["id_section"] = self.fp.read(id_len) if colormaptype: # read palette @@ -151,11 +153,23 @@ def _save(im, fp, filename): except KeyError: raise IOError("cannot write mode %s as TGA" % im.mode) - rle = im.encoderinfo.get("rle", False) - + if "rle" in im.encoderinfo: + rle = im.encoderinfo["rle"] + else: + compression = im.encoderinfo.get("compression", + im.info.get("compression")) + rle = compression == "tga_rle" if rle: imagetype += 8 + id_section = im.encoderinfo.get("id_section", + im.info.get("id_section", "")) + id_len = len(id_section) + if id_len > 255: + id_len = 255 + id_section = id_section[:255] + warnings.warn("id_section has been trimmed to 255 characters") + if colormaptype: colormapfirst, colormaplength, colormapentry = 0, 256, 24 else: @@ -166,11 +180,12 @@ def _save(im, fp, filename): else: flags = 0 - orientation = im.info.get("orientation", -1) + orientation = im.encoderinfo.get("orientation", + im.info.get("orientation", -1)) if orientation > 0: flags = flags | 0x20 - fp.write(b"\000" + + fp.write(o8(id_len) + o8(colormaptype) + o8(imagetype) + o16(colormapfirst) + @@ -183,6 +198,9 @@ def _save(im, fp, filename): o8(bits) + o8(flags)) + if id_section: + fp.write(id_section) + if colormaptype: fp.write(im.im.getpalette("RGB", "BGR")) diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index 66b211cbf..ecfe8d890 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -207,8 +207,16 @@ OPEN_INFO = { (MM, 2, (1,), 1, (8, 8, 8, 8, 8, 8), (0, 0, 0)): ("RGBX", "RGBXXX"), (II, 2, (1,), 1, (8, 8, 8, 8), (1,)): ("RGBA", "RGBa"), (MM, 2, (1,), 1, (8, 8, 8, 8), (1,)): ("RGBA", "RGBa"), + (II, 2, (1,), 1, (8, 8, 8, 8, 8), (1, 0)): ("RGBA", "RGBaX"), + (MM, 2, (1,), 1, (8, 8, 8, 8, 8), (1, 0)): ("RGBA", "RGBaX"), + (II, 2, (1,), 1, (8, 8, 8, 8, 8, 8), (1, 0, 0)): ("RGBA", "RGBaXX"), + (MM, 2, (1,), 1, (8, 8, 8, 8, 8, 8), (1, 0, 0)): ("RGBA", "RGBaXX"), (II, 2, (1,), 1, (8, 8, 8, 8), (2,)): ("RGBA", "RGBA"), (MM, 2, (1,), 1, (8, 8, 8, 8), (2,)): ("RGBA", "RGBA"), + (II, 2, (1,), 1, (8, 8, 8, 8, 8), (2, 0)): ("RGBA", "RGBAX"), + (MM, 2, (1,), 1, (8, 8, 8, 8, 8), (2, 0)): ("RGBA", "RGBAX"), + (II, 2, (1,), 1, (8, 8, 8, 8, 8, 8), (2, 0, 0)): ("RGBA", "RGBAXX"), + (MM, 2, (1,), 1, (8, 8, 8, 8, 8, 8), (2, 0, 0)): ("RGBA", "RGBAXX"), (II, 2, (1,), 1, (8, 8, 8, 8), (999,)): ("RGBA", "RGBA"), # Corel Draw 10 (MM, 2, (1,), 1, (8, 8, 8, 8), (999,)): ("RGBA", "RGBA"), # Corel Draw 10 @@ -253,8 +261,8 @@ OPEN_INFO = { (MM, 6, (1,), 1, (8, 8, 8), ()): ("YCbCr", "YCbCr"), (II, 6, (1,), 1, (8, 8, 8, 8), (0,)): ("YCbCr", "YCbCrX"), (MM, 6, (1,), 1, (8, 8, 8, 8), (0,)): ("YCbCr", "YCbCrX"), - (II, 6, (1,), 1, (8, 8, 8, 8, 8), (0, 0)): ("YCbCr", "YCbCrXXX"), - (MM, 6, (1,), 1, (8, 8, 8, 8, 8), (0, 0)): ("YCbCr", "YCbCrXXX"), + (II, 6, (1,), 1, (8, 8, 8, 8, 8), (0, 0)): ("YCbCr", "YCbCrXX"), + (MM, 6, (1,), 1, (8, 8, 8, 8, 8), (0, 0)): ("YCbCr", "YCbCrXX"), (II, 6, (1,), 1, (8, 8, 8, 8, 8, 8), (0, 0, 0)): ("YCbCr", "YCbCrXXX"), (MM, 6, (1,), 1, (8, 8, 8, 8, 8, 8), (0, 0, 0)): ("YCbCr", "YCbCrXXX"), @@ -567,6 +575,9 @@ class ImageFileDirectory_v2(MutableMapping): if self.tagtype[tag] == 7 and py3: values = [value.encode("ascii", 'replace') if isinstance( value, str) else value] + elif self.tagtype[tag] == 5: + values = [float(v) if isinstance(v, int) else v + for v in values] values = tuple(info.cvt_enum(value) for value in values) @@ -1254,9 +1265,6 @@ class TiffImageFile(ImageFile.ImageFile): h = self.tag_v2.get(ROWSPERSTRIP, ysize) w = self.size[0] if READ_LIBTIFF or self._compression != 'raw': - # if DEBUG: - # print("Activating g4 compression for whole file") - # Decoder expects entire file as one tile. # There's a buffer size limit in load (64k) # so large g4 images will fail if we use that @@ -1529,7 +1537,6 @@ def _save(im, fp, filename): rawmode = 'I;16N' a = (rawmode, compression, _fp, filename, atts) - # print(im.mode, compression, a, im.encoderconfig) e = Image._getencoder(im.mode, 'libtiff', a, im.encoderconfig) e.setimage(im.im, (0, 0)+im.size) while True: diff --git a/src/PIL/TiffTags.py b/src/PIL/TiffTags.py index ef19ee66e..bb15a64ee 100644 --- a/src/PIL/TiffTags.py +++ b/src/PIL/TiffTags.py @@ -122,7 +122,7 @@ TAGS_V2 = { 316: ("HostComputer", ASCII, 1), 317: ("Predictor", SHORT, 1, {"none": 1, "Horizontal Differencing": 2}), 318: ("WhitePoint", RATIONAL, 2), - 319: ("PrimaryChromaticities", SHORT, 6), + 319: ("PrimaryChromaticities", RATIONAL, 6), 320: ("ColorMap", SHORT, 0), 321: ("HalftoneHints", SHORT, 2), @@ -159,7 +159,7 @@ TAGS_V2 = { 529: ("YCbCrCoefficients", RATIONAL, 3), 530: ("YCbCrSubSampling", SHORT, 2), 531: ("YCbCrPositioning", SHORT, 1), - 532: ("ReferenceBlackWhite", LONG, 0), + 532: ("ReferenceBlackWhite", RATIONAL, 6), 700: ('XMP', BYTE, 1), diff --git a/src/PIL/WebPImagePlugin.py b/src/PIL/WebPImagePlugin.py index 39a8f2e35..08c6b26ac 100644 --- a/src/PIL/WebPImagePlugin.py +++ b/src/PIL/WebPImagePlugin.py @@ -5,6 +5,7 @@ from io import BytesIO _VALID_WEBP_MODES = { "RGBX": True, "RGBA": True, + "RGB": True, } _VALID_WEBP_LEGACY_MODES = { @@ -63,7 +64,8 @@ class WebPImageFile(ImageFile.ImageFile): bgcolor & 0xFF self.info["background"] = (bg_r, bg_g, bg_b, bg_a) self._n_frames = frame_count - self.mode = mode + self.mode = 'RGB' if mode == 'RGBX' else mode + self.rawmode = mode self.tile = [] # Attempt to read ICC / EXIF / XMP chunks from file @@ -153,8 +155,10 @@ class WebPImageFile(ImageFile.ImageFile): self.__loaded = self.__logical_frame # Set tile + if self.fp: + self.fp.close() self.fp = BytesIO(data) - self.tile = [("raw", (0, 0) + self.size, 0, self.mode)] + self.tile = [("raw", (0, 0) + self.size, 0, self.rawmode)] return super(WebPImageFile, self).load() @@ -240,16 +244,23 @@ def _save_all(im, fp, filename): # Make sure image mode is supported frame = ims + rawmode = ims.mode if ims.mode not in _VALID_WEBP_MODES: - alpha = ims.mode == 'P' and 'A' in ims.im.getpalettemode() - frame = ims.convert('RGBA' if alpha else 'RGBX') + alpha = 'A' in ims.mode or 'a' in ims.mode \ + or (ims.mode == 'P' and 'A' in ims.im.getpalettemode()) + rawmode = 'RGBA' if alpha else 'RGB' + frame = ims.convert(rawmode) + + if rawmode == 'RGB': + # For faster conversion, use RGBX + rawmode = 'RGBX' # Append the frame to the animation encoder enc.add( - frame.tobytes(), + frame.tobytes('raw', rawmode), timestamp, frame.size[0], frame.size[1], - frame.mode, + rawmode, lossless, quality, method @@ -288,7 +299,8 @@ def _save(im, fp, filename): xmp = im.encoderinfo.get("xmp", "") if im.mode not in _VALID_WEBP_LEGACY_MODES: - alpha = im.mode == 'P' and 'A' in im.im.getpalettemode() + alpha = 'A' in im.mode or 'a' in im.mode \ + or (im.mode == 'P' and 'A' in im.im.getpalettemode()) im = im.convert('RGBA' if alpha else 'RGB') data = _webp.WebPEncode( diff --git a/src/PIL/WmfImagePlugin.py b/src/PIL/WmfImagePlugin.py index aeb19374d..6811ddcec 100644 --- a/src/PIL/WmfImagePlugin.py +++ b/src/PIL/WmfImagePlugin.py @@ -109,8 +109,6 @@ class WmfStubImageFile(ImageFile.StubImageFile): self.info["dpi"] = 72 - # print(self.mode, self.size, self.info) - # sanity check (standard metafile header) if s[22:26] != b"\x01\x00\t\x00": raise SyntaxError("Unsupported WMF file format") diff --git a/src/PIL/__init__.py b/src/PIL/__init__.py index a07280e31..bc8cfed8c 100644 --- a/src/PIL/__init__.py +++ b/src/PIL/__init__.py @@ -1,4 +1,4 @@ -"""Pillow {} (Fork of the Python Imaging Library) +"""Pillow (Fork of the Python Imaging Library) Pillow is the friendly PIL fork by Alex Clark and Contributors. https://github.com/python-pillow/Pillow/ @@ -24,8 +24,6 @@ PILLOW_VERSION = __version__ = _version.__version__ del _version -__doc__ = __doc__.format(__version__) # include version in docstring - _plugins = ['BlpImagePlugin', 'BmpImagePlugin', diff --git a/src/_imaging.c b/src/_imaging.c index 0cb2f56b9..9620a0691 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -1998,6 +1998,7 @@ _getextrema(ImagingObject* self, PyObject* args) UINT8 u[2]; INT32 i[2]; FLOAT32 f[2]; + UINT16 s[2]; } extrema; int status; @@ -2013,6 +2014,10 @@ _getextrema(ImagingObject* self, PyObject* args) return Py_BuildValue("ii", extrema.i[0], extrema.i[1]); case IMAGING_TYPE_FLOAT32: return Py_BuildValue("dd", extrema.f[0], extrema.f[1]); + case IMAGING_TYPE_SPECIAL: + if (strcmp(self->image->mode, "I;16") == 0) { + return Py_BuildValue("HH", extrema.s[0], extrema.s[1]); + } } Py_INCREF(Py_None); @@ -2697,22 +2702,6 @@ _draw_ellipse(ImagingDrawObject* self, PyObject* args) return Py_None; } -static PyObject* -_draw_line(ImagingDrawObject* self, PyObject* args) -{ - int x0, y0, x1, y1; - int ink; - if (!PyArg_ParseTuple(args, "(ii)(ii)i", &x0, &y0, &x1, &y1, &ink)) - return NULL; - - if (ImagingDrawLine(self->image->image, x0, y0, x1, y1, - &ink, self->blend) < 0) - return NULL; - - Py_INCREF(Py_None); - return Py_None; -} - static PyObject* _draw_lines(ImagingDrawObject* self, PyObject* args) { @@ -2766,21 +2755,6 @@ _draw_lines(ImagingDrawObject* self, PyObject* args) return Py_None; } -static PyObject* -_draw_point(ImagingDrawObject* self, PyObject* args) -{ - int x, y; - int ink; - if (!PyArg_ParseTuple(args, "(ii)i", &x, &y, &ink)) - return NULL; - - if (ImagingDrawPoint(self->image->image, x, y, &ink, self->blend) < 0) - return NULL; - - Py_INCREF(Py_None); - return Py_None; -} - static PyObject* _draw_points(ImagingDrawObject* self, PyObject* args) { @@ -2961,14 +2935,12 @@ _draw_rectangle(ImagingDrawObject* self, PyObject* args) static struct PyMethodDef _draw_methods[] = { #ifdef WITH_IMAGEDRAW /* Graphics (ImageDraw) */ - {"draw_line", (PyCFunction)_draw_line, 1}, {"draw_lines", (PyCFunction)_draw_lines, 1}, #ifdef WITH_ARROW {"draw_outline", (PyCFunction)_draw_outline, 1}, #endif {"draw_polygon", (PyCFunction)_draw_polygon, 1}, {"draw_rectangle", (PyCFunction)_draw_rectangle, 1}, - {"draw_point", (PyCFunction)_draw_point, 1}, {"draw_points", (PyCFunction)_draw_points, 1}, {"draw_arc", (PyCFunction)_draw_arc, 1}, {"draw_bitmap", (PyCFunction)_draw_bitmap, 1}, diff --git a/src/_imagingft.c b/src/_imagingft.c index 86e0fe45b..f94e55803 100644 --- a/src/_imagingft.c +++ b/src/_imagingft.c @@ -674,47 +674,6 @@ font_getsize(FontObject* self, PyObject* args) ); } -static PyObject* -font_getabc(FontObject* self, PyObject* args) -{ - FT_ULong ch; - FT_Face face; - double a, b, c; - - /* calculate ABC values for a given string */ - - PyObject* string; - if (!PyArg_ParseTuple(args, "O:getabc", &string)) - return NULL; - -#if PY_VERSION_HEX >= 0x03000000 - if (!PyUnicode_Check(string)) { -#else - if (!PyUnicode_Check(string) && !PyString_Check(string)) { -#endif - PyErr_SetString(PyExc_TypeError, "expected string"); - return NULL; - } - - if (font_getchar(string, 0, &ch)) { - int index, error; - face = self->face; - index = FT_Get_Char_Index(face, ch); - /* Note: bitmap fonts within ttf fonts do not work, see #891/pr#960 */ - error = FT_Load_Glyph(face, index, FT_LOAD_DEFAULT|FT_LOAD_NO_BITMAP); - if (error) - return geterror(error); - a = face->glyph->metrics.horiBearingX / 64.0; - b = face->glyph->metrics.width / 64.0; - c = (face->glyph->metrics.horiAdvance - - face->glyph->metrics.horiBearingX - - face->glyph->metrics.width) / 64.0; - } else - a = b = c = 0.0; - - return Py_BuildValue("ddd", a, b, c); -} - static PyObject* font_render(FontObject* self, PyObject* args) { @@ -854,7 +813,6 @@ font_dealloc(FontObject* self) static PyMethodDef font_methods[] = { {"render", (PyCFunction) font_render, METH_VARARGS}, {"getsize", (PyCFunction) font_getsize, METH_VARARGS}, - {"getabc", (PyCFunction) font_getabc, METH_VARARGS}, {NULL, NULL} }; diff --git a/src/_webp.c b/src/_webp.c index f23d950f7..66b6d3268 100644 --- a/src/_webp.c +++ b/src/_webp.c @@ -216,6 +216,8 @@ PyObject* _anim_encoder_add(PyObject* self, PyObject* args) WebPPictureImportRGBA(frame, rgb, 4 * width); } else if (strcmp(mode, "RGBX")==0) { WebPPictureImportRGBX(frame, rgb, 4 * width); + } else { + WebPPictureImportRGB(frame, rgb, 3 * width); } // Add the frame to the encoder diff --git a/src/libImaging/Unpack.c b/src/libImaging/Unpack.c index 05b4299b0..7bb8064ee 100644 --- a/src/libImaging/Unpack.c +++ b/src/libImaging/Unpack.c @@ -797,6 +797,48 @@ unpackRGBa(UINT8* _out, const UINT8* in, int pixels) } } +static void +unpackRGBaskip1(UINT8* _out, const UINT8* in, int pixels) +{ + int i; + UINT32* out = (UINT32*) _out; + /* premultiplied RGBA */ + for (i = 0; i < pixels; i++) { + int a = in[3]; + if ( ! a) { + out[i] = 0; + } else if (a == 255) { + out[i] = MAKE_UINT32(in[0], in[1], in[2], a); + } else { + out[i] = MAKE_UINT32(CLIP8(in[0] * 255 / a), + CLIP8(in[1] * 255 / a), + CLIP8(in[2] * 255 / a), a); + } + in += 5; + } +} + +static void +unpackRGBaskip2(UINT8* _out, const UINT8* in, int pixels) +{ + int i; + UINT32* out = (UINT32*) _out; + /* premultiplied RGBA */ + for (i = 0; i < pixels; i++) { + int a = in[3]; + if ( ! a) { + out[i] = 0; + } else if (a == 255) { + out[i] = MAKE_UINT32(in[0], in[1], in[2], a); + } else { + out[i] = MAKE_UINT32(CLIP8(in[0] * 255 / a), + CLIP8(in[1] * 255 / a), + CLIP8(in[2] * 255 / a), a); + } + in += 6; + } +} + static void unpackBGRa(UINT8* _out, const UINT8* in, int pixels) { @@ -1301,7 +1343,11 @@ static struct { {"RGBA", "LA", 16, unpackRGBALA}, {"RGBA", "LA;16B", 32, unpackRGBALA16B}, {"RGBA", "RGBA", 32, copy4}, + {"RGBA", "RGBAX", 40, copy4skip1}, + {"RGBA", "RGBAXX", 48, copy4skip2}, {"RGBA", "RGBa", 32, unpackRGBa}, + {"RGBA", "RGBaX", 40, unpackRGBaskip1}, + {"RGBA", "RGBaXX", 48, unpackRGBaskip2}, {"RGBA", "RGBa;16L", 64, unpackRGBa16L}, {"RGBA", "RGBa;16B", 64, unpackRGBa16B}, {"RGBA", "BGRa", 32, unpackBGRa}, diff --git a/winbuild/appveyor_install_msys2_deps.sh b/winbuild/appveyor_install_msys2_deps.sh index fbab280b3..f8cb8c641 100644 --- a/winbuild/appveyor_install_msys2_deps.sh +++ b/winbuild/appveyor_install_msys2_deps.sh @@ -1,5 +1,6 @@ #!/bin/sh +mkdir /var/cache/pacman/pkg pacman -S --noconfirm mingw32/mingw-w64-i686-python3-pip \ mingw32/mingw-w64-i686-python3-setuptools \ mingw32/mingw-w64-i686-python2-pip \