diff --git a/.appveyor.yml b/.appveyor.yml index 0cf10597a..2a4e67dc9 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -52,7 +52,7 @@ install: } else { - c:\python34\python.exe c:\pillow\winbuild\build_dep.py + c:\python37\python.exe c:\pillow\winbuild\build_dep.py c:\pillow\winbuild\build_deps.cmd $host.SetShouldExit(0) } diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index e3c7379ba..676f4374b 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -9,7 +9,7 @@ Please send a pull request to the master branch. Please include [documentation]( - Fork the Pillow repository. - Create a branch from master. - Develop bug fixes, features, tests, etc. -- Run the test suite on Python 2.7 and 3.x. You can enable [Travis CI](https://travis-ci.org/profile/) and [AppVeyor](https://ci.appveyor.com/projects/new) on your repo to catch test failures prior to the pull request, and [Coveralls](https://coveralls.io/repos/new) to see if the changed code is covered by tests. +- Run the test suite on Python 2.7 and 3.x. You can enable [Travis CI](https://travis-ci.org/profile/) and [AppVeyor](https://ci.appveyor.com/projects/new) on your repo to catch test failures prior to the pull request, and [Codecov](https://codecov.io/gh) to see if the changed code is covered by tests. - Create a pull request to pull the changes from your branch to the Pillow master. ### Guidelines diff --git a/.travis.yml b/.travis.yml index 9550eb8f2..68d1d840c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -18,41 +18,27 @@ matrix: env: LINT="true" - python: "pypy2.7-6.0" name: "PyPy2 Xenial" - dist: xenial - python: "pypy3.5-6.0" name: "PyPy3 Xenial" - dist: xenial - python: '3.7' name: "3.7 Xenial" - python: '2.7' name: "2.7 Xenial" - - python: '2.7' - name: "2.7 Trusty" - dist: trusty - python: "2.7_with_system_site_packages" # For PyQt4 name: "2.7_with_system_site_packages Xenial" services: xvfb - - python: "2.7_with_system_site_packages" # For PyQt4 - name: "2.7_with_system_site_packages Trusty" - dist: trusty - python: '3.6' - name: "3.6 Xenial" - - python: '3.6' - name: "3.6 Trusty PYTHONOPTIMIZE=1" - dist: trusty + name: "3.6 Xenial PYTHONOPTIMIZE=1" env: PYTHONOPTIMIZE=1 - python: '3.5' - name: "3.5 Xenial" - - python: '3.5' - name: "3.5 Trusty PYTHONOPTIMIZE=2" - dist: trusty + name: "3.5 Xenial PYTHONOPTIMIZE=2" env: PYTHONOPTIMIZE=2 - python: "3.8-dev" name: "3.8-dev Xenial" - env: DOCKER="alpine" DOCKER_TAG="master" - env: DOCKER="arch" DOCKER_TAG="master" # contains PyQt5 - - env: DOCKER="ubuntu-trusty-x86" DOCKER_TAG="master" - - env: DOCKER="ubuntu-xenial-amd64" DOCKER_TAG="master" + - env: DOCKER="ubuntu-16.04-xenial-amd64" DOCKER_TAG="master" + - env: DOCKER="ubuntu-18.04-bionic-amd64" DOCKER_TAG="master" - env: DOCKER="debian-stretch-x86" DOCKER_TAG="master" - env: DOCKER="centos-6-amd64" DOCKER_TAG="master" - env: DOCKER="centos-7-amd64" DOCKER_TAG="master" @@ -75,14 +61,6 @@ install: .travis/install.sh; fi -before_script: -# Qt needs a display for some of the tests, and it's only run on the system site packages install -- | - if [ "$TRAVIS_JOB_NAME" == "2.7_with_system_site_packages Trusty" ]; then - export DISPLAY=:99.0 - sh -e /etc/init.d/xvfb start - fi - script: - | if [ "$LINT" == "true" ]; then diff --git a/CHANGES.rst b/CHANGES.rst index 2b1945012..cb276b0bf 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -2,12 +2,102 @@ Changelog (Pillow) ================== -6.0.0 (unreleased) +6.0.0 (2019-04-01) ------------------ - Python 2.7 support will be removed in Pillow 7.0.0 #3682 [hugovk] +- Add EXIF class #3625 + [radarhere] + +- Add ImageOps exif_transpose method #3687 + [radarhere] + +- Added warnings to deprecated CMSProfile attributes #3615 + [hugovk] + +- Documented reading TIFF multiframe images #3720 + [akuchling] + +- Improved speed of opening an MPO file #3658 + [Glandos] + +- Update palette in quantize #3721 + [radarhere] + +- Improvements to TIFF is_animated and n_frames #3714 + [radarhere] + +- Fixed incompatible pointer type warnings #3754 + [radarhere] + +- Improvements to PA and LA conversion and palette operations #3728 + [radarhere] + +- Consistent DPI rounding #3709 + [radarhere] + +- Change size of MPO image to match frame #3588 + [radarhere] + +- Read Photoshop resolution data #3701 + [radarhere] + +- Ensure image is mutable before saving #3724 + [radarhere] + +- Correct remap_palette documentation #3740 + [radarhere] + +- Promote P images to PA in putalpha #3726 + [radarhere] + +- Allow RGB and RGBA values for new P images #3719 + [radarhere] + +- Fixed TIFF bug when seeking backwards and then forwards #3713 + [radarhere] + +- Cache EXIF information #3498 + [Glandos] + +- Added transparency for all PNG greyscale modes #3744 + [radarhere] + +- Fix deprecation warnings in Python 3.8 #3749 + [radarhere] + +- Fixed GIF bug when rewinding to a non-zero frame #3716 + [radarhere] + +- Only close original fp in __del__ and __exit__ if original fp is exclusive #3683 + [radarhere] + +- Fix BytesWarning in Tests/test_numpy.py #3725 + [jdufresne] + +- Add missing MIME types and extensions #3520 + [pirate486743186] + +- Add I;16 PNG save #3566 + [radarhere] + +- Add support for BMP RGBA bitfield compression #3705 + [radarhere] + +- Added ability to set language for text rendering #3693 + [iwsfutcmd] + +- Only close exclusive fp on Image __exit__ #3698 + [radarhere] + +- Changed EPS subprocess stdout from devnull to None #3635 + [radarhere] + +- Add reading old-JPEG compressed TIFFs #3489 + [kkopachev] + - Add EXIF support for PNG #3674 [radarhere] @@ -38,7 +128,7 @@ Changelog (Pillow) - Make ContainerIO.isatty() return a bool, not int #3568 [jdufresne] -- Add support for I;16 modes for more transpose operations #3563 +- Add support to all transpose operations for I;16 modes #3563, #3741 [radarhere] - Deprecate support for PyQt4 and PySide #3655 diff --git a/README.rst b/README.rst index 381859c18..a5de420e7 100644 --- a/README.rst +++ b/README.rst @@ -20,6 +20,30 @@ Pillow is the friendly PIL fork by `Alex Clark and Contributors `_ + + - `Installation `_ + - `Handbook `_ + +- `Contribute `_ + + - `Issues `_ + - `Pull requests `_ + +- `Changelog `_ + + - `Pre-fork `_ + +Report a Vulnerability +---------------------- + +To report a security vulnerability, please follow the procedure described in the `Tidelift security policy `_. + .. |docs| image:: https://readthedocs.org/projects/pillow/badge/?version=latest :target: https://pillow.readthedocs.io/?badge=latest :alt: Documentation Status @@ -36,8 +60,8 @@ Pillow is the friendly PIL fork by `Alex Clark and Contributors `_ - - - `Installation `_ - - `Handbook `_ - -- `Contribute `_ - - - `Issues `_ - - `Pull requests `_ - -- `Changelog `_ - - - `Pre-fork `_ diff --git a/RELEASING.md b/RELEASING.md index f28e5f134..e1f57883c 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -102,4 +102,4 @@ Released as needed privately to individual vendors for critical security-related ## Documentation -* [ ] Make sure the default version for Read the Docs is the latest tagged release e.g. `d2d43879` (5.4.0) +* [ ] Make sure the [default version for Read the Docs](https://pillow.readthedocs.io/en/stable/) is up-to-date with the release changes diff --git a/Tests/images/1_trns.png b/Tests/images/1_trns.png new file mode 100644 index 000000000..c9a271b40 Binary files /dev/null and b/Tests/images/1_trns.png differ diff --git a/Tests/images/drawing_roundDown.emf b/Tests/images/drawing_roundDown.emf new file mode 100644 index 000000000..6c3e20248 Binary files /dev/null and b/Tests/images/drawing_roundDown.emf differ diff --git a/Tests/images/fujifilm.mpo b/Tests/images/fujifilm.mpo new file mode 100644 index 000000000..ff0deb8a6 Binary files /dev/null and b/Tests/images/fujifilm.mpo differ diff --git a/Tests/images/hopper.pnm b/Tests/images/hopper.pnm new file mode 100644 index 000000000..52368b2e2 Binary files /dev/null and b/Tests/images/hopper.pnm differ diff --git a/Tests/images/hopper_orientation_2.jpg b/Tests/images/hopper_orientation_2.jpg new file mode 100644 index 000000000..02b4f392e Binary files /dev/null and b/Tests/images/hopper_orientation_2.jpg differ diff --git a/Tests/images/hopper_orientation_2.webp b/Tests/images/hopper_orientation_2.webp new file mode 100644 index 000000000..43381d2ba Binary files /dev/null and b/Tests/images/hopper_orientation_2.webp differ diff --git a/Tests/images/hopper_orientation_3.jpg b/Tests/images/hopper_orientation_3.jpg new file mode 100644 index 000000000..01717d980 Binary files /dev/null and b/Tests/images/hopper_orientation_3.jpg differ diff --git a/Tests/images/hopper_orientation_3.webp b/Tests/images/hopper_orientation_3.webp new file mode 100644 index 000000000..9537ff68e Binary files /dev/null and b/Tests/images/hopper_orientation_3.webp differ diff --git a/Tests/images/hopper_orientation_4.jpg b/Tests/images/hopper_orientation_4.jpg new file mode 100644 index 000000000..3e0bb4e1a Binary files /dev/null and b/Tests/images/hopper_orientation_4.jpg differ diff --git a/Tests/images/hopper_orientation_4.webp b/Tests/images/hopper_orientation_4.webp new file mode 100644 index 000000000..ca7b8cd30 Binary files /dev/null and b/Tests/images/hopper_orientation_4.webp differ diff --git a/Tests/images/hopper_orientation_5.jpg b/Tests/images/hopper_orientation_5.jpg new file mode 100644 index 000000000..fd32afc27 Binary files /dev/null and b/Tests/images/hopper_orientation_5.jpg differ diff --git a/Tests/images/hopper_orientation_5.webp b/Tests/images/hopper_orientation_5.webp new file mode 100644 index 000000000..a3164a90d Binary files /dev/null and b/Tests/images/hopper_orientation_5.webp differ diff --git a/Tests/images/hopper_orientation_6.jpg b/Tests/images/hopper_orientation_6.jpg new file mode 100644 index 000000000..22a096198 Binary files /dev/null and b/Tests/images/hopper_orientation_6.jpg differ diff --git a/Tests/images/hopper_orientation_6.webp b/Tests/images/hopper_orientation_6.webp new file mode 100644 index 000000000..3e24c5bcb Binary files /dev/null and b/Tests/images/hopper_orientation_6.webp differ diff --git a/Tests/images/hopper_orientation_7.jpg b/Tests/images/hopper_orientation_7.jpg new file mode 100644 index 000000000..a7c45146a Binary files /dev/null and b/Tests/images/hopper_orientation_7.jpg differ diff --git a/Tests/images/hopper_orientation_7.webp b/Tests/images/hopper_orientation_7.webp new file mode 100644 index 000000000..f78163aed Binary files /dev/null and b/Tests/images/hopper_orientation_7.webp differ diff --git a/Tests/images/hopper_orientation_8.jpg b/Tests/images/hopper_orientation_8.jpg new file mode 100644 index 000000000..e6b8c2c1c Binary files /dev/null and b/Tests/images/hopper_orientation_8.jpg differ diff --git a/Tests/images/hopper_orientation_8.webp b/Tests/images/hopper_orientation_8.webp new file mode 100644 index 000000000..3cce80a47 Binary files /dev/null and b/Tests/images/hopper_orientation_8.webp differ diff --git a/Tests/images/hopper_roundDown.bmp b/Tests/images/hopper_roundDown.bmp new file mode 100644 index 000000000..62aada050 Binary files /dev/null and b/Tests/images/hopper_roundDown.bmp differ diff --git a/Tests/images/hopper_roundDown_2.tif b/Tests/images/hopper_roundDown_2.tif new file mode 100644 index 000000000..ac8cd057d Binary files /dev/null and b/Tests/images/hopper_roundDown_2.tif differ diff --git a/Tests/images/hopper_roundDown_3.tif b/Tests/images/hopper_roundDown_3.tif new file mode 100644 index 000000000..0542fab9a Binary files /dev/null and b/Tests/images/hopper_roundDown_3.tif differ diff --git a/Tests/images/hopper_roundDown_None.tif b/Tests/images/hopper_roundDown_None.tif new file mode 100644 index 000000000..21c40e8fe Binary files /dev/null and b/Tests/images/hopper_roundDown_None.tif differ diff --git a/Tests/images/hopper_roundUp_2.tif b/Tests/images/hopper_roundUp_2.tif new file mode 100644 index 000000000..e38541c5d Binary files /dev/null and b/Tests/images/hopper_roundUp_2.tif differ diff --git a/Tests/images/hopper_roundUp_3.tif b/Tests/images/hopper_roundUp_3.tif new file mode 100644 index 000000000..af6c96bd4 Binary files /dev/null and b/Tests/images/hopper_roundUp_3.tif differ diff --git a/Tests/images/hopper_roundUp_None.tif b/Tests/images/hopper_roundUp_None.tif new file mode 100644 index 000000000..b98635108 Binary files /dev/null and b/Tests/images/hopper_roundUp_None.tif differ diff --git a/Tests/images/i_trns.png b/Tests/images/i_trns.png new file mode 100644 index 000000000..ef63d33b0 Binary files /dev/null and b/Tests/images/i_trns.png differ diff --git a/Tests/images/iptc_roundDown.jpg b/Tests/images/iptc_roundDown.jpg new file mode 100644 index 000000000..f98206f18 Binary files /dev/null and b/Tests/images/iptc_roundDown.jpg differ diff --git a/Tests/images/iptc_roundUp.jpg b/Tests/images/iptc_roundUp.jpg new file mode 100644 index 000000000..68ac20b71 Binary files /dev/null and b/Tests/images/iptc_roundUp.jpg differ diff --git a/Tests/images/old-style-jpeg-compression.png b/Tests/images/old-style-jpeg-compression.png new file mode 100644 index 000000000..c035542ea Binary files /dev/null and b/Tests/images/old-style-jpeg-compression.png differ diff --git a/Tests/images/old-style-jpeg-compression.tif b/Tests/images/old-style-jpeg-compression.tif new file mode 100644 index 000000000..8d726c404 Binary files /dev/null and b/Tests/images/old-style-jpeg-compression.tif differ diff --git a/Tests/images/rgb32bf-rgba.bmp b/Tests/images/rgb32bf-rgba.bmp new file mode 100644 index 000000000..467c2570b Binary files /dev/null and b/Tests/images/rgb32bf-rgba.bmp differ diff --git a/Tests/images/sugarshack_frame_size.mpo b/Tests/images/sugarshack_frame_size.mpo new file mode 100644 index 000000000..81d58e64b Binary files /dev/null and b/Tests/images/sugarshack_frame_size.mpo differ diff --git a/Tests/images/test_language.png b/Tests/images/test_language.png new file mode 100644 index 000000000..8daf007b0 Binary files /dev/null and b/Tests/images/test_language.png differ diff --git a/Tests/test_file_bmp.py b/Tests/test_file_bmp.py index 5fc6d9f36..b97a84227 100644 --- a/Tests/test_file_bmp.py +++ b/Tests/test_file_bmp.py @@ -71,6 +71,27 @@ class TestFileBmp(PillowTestCase): self.assertEqual(im.size, reloaded.size) self.assertEqual(reloaded.format, "JPEG") + def test_load_dpi_rounding(self): + # Round up + im = Image.open('Tests/images/hopper.bmp') + self.assertEqual(im.info["dpi"], (96, 96)) + + # Round down + im = Image.open('Tests/images/hopper_roundDown.bmp') + self.assertEqual(im.info["dpi"], (72, 72)) + + def test_save_dpi_rounding(self): + outfile = self.tempfile("temp.bmp") + im = Image.open('Tests/images/hopper.bmp') + + im.save(outfile, dpi=(72.2, 72.2)) + reloaded = Image.open(outfile) + self.assertEqual(reloaded.info["dpi"], (72, 72)) + + im.save(outfile, dpi=(72.8, 72.8)) + reloaded = Image.open(outfile) + self.assertEqual(reloaded.info["dpi"], (73, 73)) + def test_load_dib(self): # test for #1293, Imagegrab returning Unsupported Bitfields Format im = Image.open('Tests/images/clipboard.dib') @@ -90,3 +111,15 @@ class TestFileBmp(PillowTestCase): self.assertEqual(reloaded.format, "DIB") self.assertEqual(reloaded.get_format_mimetype(), "image/bmp") self.assert_image_equal(im, reloaded) + + def test_rgba_bitfields(self): + # This test image has been manually hexedited + # to change the bitfield compression in the header from XBGR to RGBA + im = Image.open("Tests/images/rgb32bf-rgba.bmp") + + # So before the comparing the image, swap the channels + b, g, r = im.split()[1:] + im = Image.merge("RGB", (r, g, b)) + + target = Image.open("Tests/images/bmp/q/rgb32bf-xbgr.bmp") + self.assert_image_equal(im, target) diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index 36a2b2648..c2188a0a2 100644 --- a/Tests/test_file_gif.py +++ b/Tests/test_file_gif.py @@ -230,6 +230,15 @@ class TestFileGif(PillowTestCase): self.assertEqual(im.info, info) + def test_seek_rewind(self): + im = Image.open("Tests/images/iss634.gif") + im.seek(2) + im.seek(1) + + expected = Image.open("Tests/images/iss634.gif") + expected.seek(1) + self.assert_image_equal(im, expected) + def test_n_frames(self): for path, n_frames in [ [TEST_GIF, 1], diff --git a/Tests/test_file_ico.py b/Tests/test_file_ico.py index b0d33e33f..f6244e086 100644 --- a/Tests/test_file_ico.py +++ b/Tests/test_file_ico.py @@ -14,6 +14,7 @@ class TestFileIco(PillowTestCase): self.assertEqual(im.mode, "RGBA") self.assertEqual(im.size, (16, 16)) self.assertEqual(im.format, "ICO") + self.assertEqual(im.get_format_mimetype(), "image/x-icon") def test_invalid_file(self): with open("Tests/images/flower.jpg", "rb") as fp: diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index cbe894f34..25c3f754a 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -524,6 +524,27 @@ class TestFileJpeg(PillowTestCase): reloaded.load() self.assertEqual(im.info['dpi'], reloaded.info['dpi']) + def test_load_dpi_rounding(self): + # Round up + im = Image.open('Tests/images/iptc_roundUp.jpg') + self.assertEqual(im.info["dpi"], (44, 44)) + + # Round down + im = Image.open('Tests/images/iptc_roundDown.jpg') + self.assertEqual(im.info["dpi"], (2, 2)) + + def test_save_dpi_rounding(self): + outfile = self.tempfile("temp.jpg") + im = Image.open('Tests/images/hopper.jpg') + + im.save(outfile, dpi=(72.2, 72.2)) + reloaded = Image.open(outfile) + self.assertEqual(reloaded.info["dpi"], (72, 72)) + + im.save(outfile, dpi=(72.8, 72.8)) + reloaded = Image.open(outfile) + self.assertEqual(reloaded.info["dpi"], (73, 73)) + def test_dpi_tuple_from_exif(self): # Arrange # This Photoshop CC 2017 image has DPI in EXIF not metadata @@ -590,6 +611,15 @@ class TestFileJpeg(PillowTestCase): # Act / Assert self.assertEqual(im._getexif()[306], '2017:03:13 23:03:09') + def test_photoshop(self): + im = Image.open("Tests/images/photoshop-200dpi.jpg") + self.assertEqual(im.info["photoshop"][0x03ed], { + 'XResolution': 200.0, + 'DisplayedUnitsX': 1, + 'YResolution': 200.0, + 'DisplayedUnitsY': 1, + }) + @unittest.skipUnless(sys.platform.startswith('win32'), "Windows only") class TestFileCloseW32(PillowTestCase): diff --git a/Tests/test_file_libtiff.py b/Tests/test_file_libtiff.py index e395708c3..0292b7733 100644 --- a/Tests/test_file_libtiff.py +++ b/Tests/test_file_libtiff.py @@ -716,3 +716,10 @@ class TestFileLibTiff(LibTiffTestCase): im = Image.open(infile) self.assert_image_similar_tofile(im, "Tests/images/flower.jpg", 0.5) + + def test_old_style_jpeg(self): + infile = "Tests/images/old-style-jpeg-compression.tif" + im = Image.open(infile) + + self.assert_image_equal_tofile(im, + "Tests/images/old-style-jpeg-compression.png") diff --git a/Tests/test_file_mpo.py b/Tests/test_file_mpo.py index 45172472a..ce1ca96b6 100644 --- a/Tests/test_file_mpo.py +++ b/Tests/test_file_mpo.py @@ -55,6 +55,27 @@ class TestFileMpo(PillowTestCase): self.assertEqual(info[296], 2) self.assertEqual(info[34665], 188) + def test_frame_size(self): + # This image has been hexedited to contain a different size + # in the EXIF data of the second frame + im = Image.open("Tests/images/sugarshack_frame_size.mpo") + self.assertEqual(im.size, (640, 480)) + + im.seek(1) + self.assertEqual(im.size, (680, 480)) + + def test_parallax(self): + # Nintendo + im = Image.open("Tests/images/sugarshack.mpo") + exif = im.getexif() + self.assertEqual(exif.get_ifd(0x927c)[0x1101]["Parallax"], -44.798187255859375) + + # Fujifilm + im = Image.open("Tests/images/fujifilm.mpo") + im.seek(1) + exif = im.getexif() + self.assertEqual(exif.get_ifd(0x927c)[0xb211], -3.125) + def test_mp(self): for test_file in test_files: im = Image.open(test_file) diff --git a/Tests/test_file_pcx.py b/Tests/test_file_pcx.py index 9e42a86f7..7608db47c 100644 --- a/Tests/test_file_pcx.py +++ b/Tests/test_file_pcx.py @@ -13,6 +13,7 @@ class TestFilePcx(PillowTestCase): self.assertEqual(im2.mode, im.mode) self.assertEqual(im2.size, im.size) self.assertEqual(im2.format, "PCX") + self.assertEqual(im2.get_format_mimetype(), "image/x-pcx") self.assert_image_equal(im2, im) def test_sanity(self): diff --git a/Tests/test_file_png.py b/Tests/test_file_png.py index c2864d223..6a061fe6a 100644 --- a/Tests/test_file_png.py +++ b/Tests/test_file_png.py @@ -88,20 +88,13 @@ class TestFilePng(PillowTestCase): self.assertEqual(im.format, "PNG") self.assertEqual(im.get_format_mimetype(), 'image/png') - hopper("1").save(test_file) - Image.open(test_file) - - hopper("L").save(test_file) - Image.open(test_file) - - hopper("P").save(test_file) - Image.open(test_file) - - hopper("RGB").save(test_file) - Image.open(test_file) - - hopper("I").save(test_file) - Image.open(test_file) + for mode in ["1", "L", "P", "RGB", "I", "I;16"]: + im = hopper(mode) + im.save(test_file) + reloaded = Image.open(test_file) + if mode == "I;16": + reloaded = reloaded.convert(mode) + self.assert_image_equal(reloaded, im) def test_invalid_file(self): invalid_file = "Tests/images/flower.jpg" @@ -298,30 +291,32 @@ class TestFilePng(PillowTestCase): self.assert_image(im, "RGBA", (10, 10)) self.assertEqual(im.getcolors(), [(100, (0, 0, 0, 0))]) - def test_save_l_transparency(self): - # There are 559 transparent pixels in l_trns.png. - num_transparent = 559 + def test_save_greyscale_transparency(self): + for mode, num_transparent in { + "1": 1994, + "L": 559, + "I": 559, + }.items(): + in_file = "Tests/images/"+mode.lower()+"_trns.png" + im = Image.open(in_file) + self.assertEqual(im.mode, mode) + self.assertEqual(im.info["transparency"], 255) - in_file = "Tests/images/l_trns.png" - im = Image.open(in_file) - self.assertEqual(im.mode, "L") - self.assertEqual(im.info["transparency"], 255) + im_rgba = im.convert('RGBA') + self.assertEqual( + im_rgba.getchannel("A").getcolors()[0][0], num_transparent) - im_rgba = im.convert('RGBA') - self.assertEqual( - im_rgba.getchannel("A").getcolors()[0][0], num_transparent) + test_file = self.tempfile("temp.png") + im.save(test_file) - test_file = self.tempfile("temp.png") - im.save(test_file) + test_im = Image.open(test_file) + self.assertEqual(test_im.mode, mode) + self.assertEqual(test_im.info["transparency"], 255) + self.assert_image_equal(im, test_im) - test_im = Image.open(test_file) - self.assertEqual(test_im.mode, "L") - self.assertEqual(test_im.info["transparency"], 255) - self.assert_image_equal(im, test_im) - - test_im_rgba = test_im.convert('RGBA') - self.assertEqual( - test_im_rgba.getchannel('A').getcolors()[0][0], num_transparent) + test_im_rgba = test_im.convert('RGBA') + self.assertEqual( + test_im_rgba.getchannel('A').getcolors()[0][0], num_transparent) def test_save_rgb_single_transparency(self): in_file = "Tests/images/caption_6_33_22.png" @@ -394,6 +389,24 @@ class TestFilePng(PillowTestCase): im = roundtrip(im, dpi=(100, 100)) self.assertEqual(im.info["dpi"], (100, 100)) + def test_load_dpi_rounding(self): + # Round up + im = Image.open(TEST_PNG_FILE) + self.assertEqual(im.info["dpi"], (96, 96)) + + # Round down + im = Image.open("Tests/images/icc_profile_none.png") + self.assertEqual(im.info["dpi"], (72, 72)) + + def test_save_dpi_rounding(self): + im = Image.open(TEST_PNG_FILE) + + im = roundtrip(im, dpi=(72.2, 72.2)) + self.assertEqual(im.info["dpi"], (72, 72)) + + im = roundtrip(im, dpi=(72.8, 72.8)) + self.assertEqual(im.info["dpi"], (73, 73)) + def test_roundtrip_text(self): # Check text roundtripping diff --git a/Tests/test_file_ppm.py b/Tests/test_file_ppm.py index dc239cc4c..fa024f2cd 100644 --- a/Tests/test_file_ppm.py +++ b/Tests/test_file_ppm.py @@ -1,4 +1,4 @@ -from .helper import PillowTestCase +from .helper import PillowTestCase, hopper from PIL import Image @@ -36,6 +36,16 @@ class TestFilePpm(PillowTestCase): reloaded = Image.open(f) self.assert_image_equal(im, reloaded) + def test_pnm(self): + im = Image.open('Tests/images/hopper.pnm') + self.assert_image_similar(im, hopper(), 0.0001) + + f = self.tempfile('temp.pnm') + im.save(f) + + reloaded = Image.open(f) + self.assert_image_equal(im, reloaded) + def test_truncated_file(self): path = self.tempfile('temp.pgm') with open(path, 'w') as f: diff --git a/Tests/test_file_tga.py b/Tests/test_file_tga.py index 0dbfb309c..a8ab00d7f 100644 --- a/Tests/test_file_tga.py +++ b/Tests/test_file_tga.py @@ -37,6 +37,8 @@ class TestFileTga(PillowTestCase): path_no_ext, origin, "rle" if rle else "raw") original_im = Image.open(tga_path) + self.assertEqual(original_im.format, "TGA") + self.assertEqual(original_im.get_format_mimetype(), "image/x-tga") if rle: self.assertEqual( original_im.info["compression"], "tga_rle") diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py index 9a4104b78..9f2a04bb9 100644 --- a/Tests/test_file_tiff.py +++ b/Tests/test_file_tiff.py @@ -126,6 +126,30 @@ class TestFileTiff(PillowTestCase): im._setup() self.assertEqual(im.info['dpi'], (71., 71.)) + def test_load_dpi_rounding(self): + for resolutionUnit, dpi in ((None, (72, 73)), + (2, (72, 73)), + (3, (183, 185))): + im = Image.open( + "Tests/images/hopper_roundDown_"+str(resolutionUnit)+".tif") + self.assertEqual(im.tag_v2.get(RESOLUTION_UNIT), resolutionUnit) + self.assertEqual(im.info['dpi'], (dpi[0], dpi[0])) + + im = Image.open("Tests/images/hopper_roundUp_"+str(resolutionUnit)+".tif") + self.assertEqual(im.tag_v2.get(RESOLUTION_UNIT), resolutionUnit) + self.assertEqual(im.info['dpi'], (dpi[1], dpi[1])) + + def test_save_dpi_rounding(self): + outfile = self.tempfile("temp.tif") + im = Image.open("Tests/images/hopper.tif") + + for dpi in (72.2, 72.8): + im.save(outfile, dpi=(dpi, dpi)) + + reloaded = Image.open(outfile) + reloaded.load() + self.assertEqual((round(dpi), round(dpi)), reloaded.info['dpi']) + def test_save_setting_missing_resolution(self): b = BytesIO() Image.open("Tests/images/10ct_32bit_128.tiff").save( @@ -229,11 +253,6 @@ class TestFileTiff(PillowTestCase): ['Tests/images/multipage-lastframe.tif', 1], ['Tests/images/multipage.tiff', 3] ]: - # Test is_animated before n_frames - im = Image.open(path) - self.assertEqual(im.is_animated, n_frames != 1) - - # Test is_animated after n_frames im = Image.open(path) self.assertEqual(im.n_frames, n_frames) self.assertEqual(im.is_animated, n_frames != 1) @@ -263,6 +282,11 @@ class TestFileTiff(PillowTestCase): self.assertEqual(im.size, (10, 10)) self.assertEqual(im.convert('RGB').getpixel((0, 0)), (255, 0, 0)) + im.seek(0) + im.load() + self.assertEqual(im.size, (10, 10)) + self.assertEqual(im.convert('RGB').getpixel((0, 0)), (0, 128, 0)) + im.seek(2) im.load() self.assertEqual(im.size, (20, 20)) diff --git a/Tests/test_file_wmf.py b/Tests/test_file_wmf.py index 146888491..e3c6063f2 100644 --- a/Tests/test_file_wmf.py +++ b/Tests/test_file_wmf.py @@ -45,6 +45,15 @@ class TestFileWmf(PillowTestCase): # Restore the state before this test WmfImagePlugin.register_handler(None) + def test_load_dpi_rounding(self): + # Round up + im = Image.open('Tests/images/drawing.emf') + self.assertEqual(im.info["dpi"], 1424) + + # Round down + im = Image.open('Tests/images/drawing_roundDown.emf') + self.assertEqual(im.info["dpi"], 1426) + def test_save(self): im = hopper() diff --git a/Tests/test_image.py b/Tests/test_image.py index 330048057..5bfcde368 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -3,6 +3,8 @@ from .helper import unittest, PillowTestCase, hopper from PIL import Image from PIL._util import py3 import os +import sys +import shutil class TestImage(PillowTestCase): @@ -121,6 +123,16 @@ class TestImage(PillowTestCase): im.paste(0, (0, 0, 100, 100)) self.assertFalse(im.readonly) + @unittest.skipIf(sys.platform.startswith('win32'), + "Test requires opening tempfile twice") + def test_readonly_save(self): + temp_file = self.tempfile("temp.bmp") + shutil.copy("Tests/images/rgb32bf-rgba.bmp", temp_file) + + im = Image.open(temp_file) + self.assertTrue(im.readonly) + im.save(temp_file) + def test_dump(self): im = Image.new("L", (10, 10)) im._dump(self.tempfile("temp_L.ppm")) @@ -522,6 +534,16 @@ class TestImage(PillowTestCase): _make_new(im, blank_p, ImagePalette.ImagePalette()) _make_new(im, blank_pa, ImagePalette.ImagePalette()) + def test_p_from_rgb_rgba(self): + for mode, color in [ + ("RGB", '#DDEEFF'), + ("RGB", (221, 238, 255)), + ("RGBA", (221, 238, 255, 255)) + ]: + im = Image.new("P", (100, 100), color) + expected = Image.new(mode, (100, 100), color) + self.assert_image_equal(im.convert(mode), expected) + def test_no_resource_warning_on_save(self): # https://github.com/python-pillow/Pillow/issues/835 # Arrange @@ -532,6 +554,18 @@ class TestImage(PillowTestCase): with Image.open(test_file) as im: self.assert_warning(None, im.save, temp_file) + def test_load_on_nonexclusive_multiframe(self): + with open("Tests/images/frozenpond.mpo", "rb") as fp: + def act(fp): + im = Image.open(fp) + im.load() + act(fp) + + with Image.open(fp) as im: + im.load() + + self.assertFalse(fp.closed) + class MockEncoder(object): pass diff --git a/Tests/test_image_convert.py b/Tests/test_image_convert.py index 1ba1794eb..970dc8ce1 100644 --- a/Tests/test_image_convert.py +++ b/Tests/test_image_convert.py @@ -12,7 +12,8 @@ class TestImageConvert(PillowTestCase): self.assertEqual(out.mode, mode) self.assertEqual(out.size, im.size) - modes = "1", "L", "I", "F", "RGB", "RGBA", "RGBX", "CMYK", "YCbCr" + modes = ("1", "L", "LA", "P", "PA", "I", "F", + "RGB", "RGBA", "RGBX", "CMYK", "YCbCr") for mode in modes: im = hopper(mode) diff --git a/Tests/test_image_load.py b/Tests/test_image_load.py index d8f323eb8..9937218cd 100644 --- a/Tests/test_image_load.py +++ b/Tests/test_image_load.py @@ -28,3 +28,10 @@ class TestImageLoad(PillowTestCase): os.fstat(fn) self.assertRaises(OSError, os.fstat, fn) + + def test_contextmanager_non_exclusive_fp(self): + with open("Tests/images/hopper.gif", "rb") as fp: + with Image.open(fp): + pass + + self.assertFalse(fp.closed) diff --git a/Tests/test_image_mode.py b/Tests/test_image_mode.py index 26266611d..345c0dacb 100644 --- a/Tests/test_image_mode.py +++ b/Tests/test_image_mode.py @@ -42,7 +42,7 @@ class TestImageMode(PillowTestCase): self.assertEqual(signature, result) check("1", "L", "L", 1, ("1",)) check("L", "L", "L", 1, ("L",)) - check("P", "RGB", "L", 1, ("P",)) + check("P", "P", "L", 1, ("P",)) check("I", "L", "I", 1, ("I",)) check("F", "L", "F", 1, ("F",)) check("RGB", "RGB", "L", 3, ("R", "G", "B")) diff --git a/Tests/test_image_putalpha.py b/Tests/test_image_putalpha.py index 7b66b8833..702afb995 100644 --- a/Tests/test_image_putalpha.py +++ b/Tests/test_image_putalpha.py @@ -28,6 +28,13 @@ class TestImagePutAlpha(PillowTestCase): self.assertEqual(im.mode, 'LA') self.assertEqual(im.getpixel((0, 0)), (1, 2)) + im = Image.new("P", (1, 1), 1) + self.assertEqual(im.getpixel((0, 0)), 1) + + im.putalpha(2) + self.assertEqual(im.mode, 'PA') + self.assertEqual(im.getpixel((0, 0)), (1, 2)) + im = Image.new("RGB", (1, 1), (1, 2, 3)) self.assertEqual(im.getpixel((0, 0)), (1, 2, 3)) diff --git a/Tests/test_image_putpalette.py b/Tests/test_image_putpalette.py index 34b585f55..4bac448fd 100644 --- a/Tests/test_image_putpalette.py +++ b/Tests/test_image_putpalette.py @@ -14,8 +14,10 @@ class TestImagePutPalette(PillowTestCase): return im.mode, p[:10] return im.mode self.assertRaises(ValueError, palette, "1") - self.assertEqual(palette("L"), ("P", [0, 1, 2, 3, 4, 5, 6, 7, 8, 9])) - self.assertEqual(palette("P"), ("P", [0, 1, 2, 3, 4, 5, 6, 7, 8, 9])) + for mode in ["L", "LA", "P", "PA"]: + self.assertEqual(palette(mode), + ("PA" if "A" in mode else "P", + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9])) self.assertRaises(ValueError, palette, "I") self.assertRaises(ValueError, palette, "F") self.assertRaises(ValueError, palette, "RGB") diff --git a/Tests/test_image_quantize.py b/Tests/test_image_quantize.py index 762b4bcab..5f2318574 100644 --- a/Tests/test_image_quantize.py +++ b/Tests/test_image_quantize.py @@ -38,9 +38,10 @@ class TestImageQuantize(PillowTestCase): def test_rgba_quantize(self): image = hopper('RGBA') - image.quantize() self.assertRaises(ValueError, image.quantize, method=0) + self.assertEqual(image.quantize().convert().mode, "RGBA") + def test_quantize(self): image = Image.open('Tests/images/caption_6_33_22.png').convert('RGB') converted = image.quantize() diff --git a/Tests/test_image_transpose.py b/Tests/test_image_transpose.py index ad8c77126..1cce5e986 100644 --- a/Tests/test_image_transpose.py +++ b/Tests/test_image_transpose.py @@ -24,7 +24,7 @@ class TestImageTranspose(PillowTestCase): self.assertEqual(im.getpixel((1, y-2)), out.getpixel((x-2, y-2))) self.assertEqual(im.getpixel((x-2, y-2)), out.getpixel((1, y-2))) - for mode in ("L", "RGB", "I;16", "I;16L", "I;16B"): + for mode in self.hopper: transpose(mode) def test_flip_top_bottom(self): @@ -40,7 +40,7 @@ class TestImageTranspose(PillowTestCase): self.assertEqual(im.getpixel((1, y-2)), out.getpixel((1, 1))) self.assertEqual(im.getpixel((x-2, y-2)), out.getpixel((x-2, 1))) - for mode in ("L", "RGB", "I;16", "I;16L", "I;16B"): + for mode in self.hopper: transpose(mode) def test_rotate_90(self): @@ -56,7 +56,7 @@ class TestImageTranspose(PillowTestCase): self.assertEqual(im.getpixel((1, y-2)), out.getpixel((y-2, x-2))) self.assertEqual(im.getpixel((x-2, y-2)), out.getpixel((y-2, 1))) - for mode in ("L", "RGB"): + for mode in self.hopper: transpose(mode) def test_rotate_180(self): @@ -72,7 +72,7 @@ class TestImageTranspose(PillowTestCase): self.assertEqual(im.getpixel((1, y-2)), out.getpixel((x-2, 1))) self.assertEqual(im.getpixel((x-2, y-2)), out.getpixel((1, 1))) - for mode in ("L", "RGB", "I;16", "I;16L", "I;16B"): + for mode in self.hopper: transpose(mode) def test_rotate_270(self): @@ -88,7 +88,7 @@ class TestImageTranspose(PillowTestCase): self.assertEqual(im.getpixel((1, y-2)), out.getpixel((1, 1))) self.assertEqual(im.getpixel((x-2, y-2)), out.getpixel((1, x-2))) - for mode in ("L", "RGB"): + for mode in self.hopper: transpose(mode) def test_transpose(self): @@ -104,7 +104,7 @@ class TestImageTranspose(PillowTestCase): self.assertEqual(im.getpixel((1, y-2)), out.getpixel((y-2, 1))) self.assertEqual(im.getpixel((x-2, y-2)), out.getpixel((y-2, x-2))) - for mode in ("L", "RGB"): + for mode in self.hopper: transpose(mode) def test_tranverse(self): @@ -120,28 +120,29 @@ class TestImageTranspose(PillowTestCase): self.assertEqual(im.getpixel((1, y-2)), out.getpixel((1, x-2))) self.assertEqual(im.getpixel((x-2, y-2)), out.getpixel((1, 1))) - for mode in ("L", "RGB"): + for mode in self.hopper: transpose(mode) def test_roundtrip(self): - im = self.hopper['L'] + for mode in self.hopper: + im = self.hopper[mode] - def transpose(first, second): - return im.transpose(first).transpose(second) + def transpose(first, second): + return im.transpose(first).transpose(second) - self.assert_image_equal( - im, transpose(FLIP_LEFT_RIGHT, FLIP_LEFT_RIGHT)) - self.assert_image_equal( - im, transpose(FLIP_TOP_BOTTOM, FLIP_TOP_BOTTOM)) - self.assert_image_equal(im, transpose(ROTATE_90, ROTATE_270)) - self.assert_image_equal(im, transpose(ROTATE_180, ROTATE_180)) - self.assert_image_equal( - im.transpose(TRANSPOSE), transpose(ROTATE_90, FLIP_TOP_BOTTOM)) - self.assert_image_equal( - im.transpose(TRANSPOSE), transpose(ROTATE_270, FLIP_LEFT_RIGHT)) - self.assert_image_equal( - im.transpose(TRANSVERSE), transpose(ROTATE_90, FLIP_LEFT_RIGHT)) - self.assert_image_equal( - im.transpose(TRANSVERSE), transpose(ROTATE_270, FLIP_TOP_BOTTOM)) - self.assert_image_equal( - im.transpose(TRANSVERSE), transpose(ROTATE_180, TRANSPOSE)) + self.assert_image_equal( + im, transpose(FLIP_LEFT_RIGHT, FLIP_LEFT_RIGHT)) + self.assert_image_equal( + im, transpose(FLIP_TOP_BOTTOM, FLIP_TOP_BOTTOM)) + self.assert_image_equal(im, transpose(ROTATE_90, ROTATE_270)) + self.assert_image_equal(im, transpose(ROTATE_180, ROTATE_180)) + self.assert_image_equal( + im.transpose(TRANSPOSE), transpose(ROTATE_90, FLIP_TOP_BOTTOM)) + self.assert_image_equal( + im.transpose(TRANSPOSE), transpose(ROTATE_270, FLIP_LEFT_RIGHT)) + self.assert_image_equal( + im.transpose(TRANSVERSE), transpose(ROTATE_90, FLIP_LEFT_RIGHT)) + self.assert_image_equal( + im.transpose(TRANSVERSE), transpose(ROTATE_270, FLIP_TOP_BOTTOM)) + self.assert_image_equal( + im.transpose(TRANSVERSE), transpose(ROTATE_180, TRANSPOSE)) diff --git a/Tests/test_imagecms.py b/Tests/test_imagecms.py index cdd6f00c3..de140710f 100644 --- a/Tests/test_imagecms.py +++ b/Tests/test_imagecms.py @@ -309,7 +309,7 @@ class TestImageCms(PillowTestCase): 2: (False, False, True), 3: (False, False, True) }) - self.assertEqual(p.color_space, 'RGB') + self.assertIsNone(p.colorant_table) self.assertIsNone(p.colorant_table_out) self.assertIsNone(p.colorimetric_intent) @@ -361,16 +361,9 @@ class TestImageCms(PillowTestCase): (5000.722328847392,)) self.assertEqual(p.model, 'IEC 61966-2-1 Default RGB Colour Space - sRGB') - self.assertEqual(p.pcs, 'XYZ') + self.assertIsNone(p.perceptual_rendering_intent_gamut) - self.assertEqual(p.product_copyright, - 'Copyright International Color Consortium, 2009') - self.assertEqual(p.product_desc, 'sRGB IEC61966-2-1 black scaled') - self.assertEqual(p.product_description, - 'sRGB IEC61966-2-1 black scaled') - self.assertEqual(p.product_manufacturer, '') - self.assertEqual( - p.product_model, 'IEC 61966-2-1 Default RGB Colour Space - sRGB') + self.assertEqual( p.profile_description, 'sRGB IEC61966-2-1 black scaled') self.assertEqual( @@ -393,6 +386,40 @@ class TestImageCms(PillowTestCase): 'Reference Viewing Condition in IEC 61966-2-1') self.assertEqual(p.xcolor_space, 'RGB ') + def test_deprecations(self): + self.skip_missing() + o = ImageCms.getOpenProfile(SRGB) + p = o.profile + + def helper_deprecated(attr, expected): + result = self.assert_warning(DeprecationWarning, getattr, p, attr) + self.assertEqual(result, expected) + + # p.color_space + helper_deprecated("color_space", "RGB") + + # p.pcs + helper_deprecated("pcs", "XYZ") + + # p.product_copyright + helper_deprecated( + "product_copyright", "Copyright International Color Consortium, 2009" + ) + + # p.product_desc + helper_deprecated("product_desc", "sRGB IEC61966-2-1 black scaled") + + # p.product_description + helper_deprecated("product_description", "sRGB IEC61966-2-1 black scaled") + + # p.product_manufacturer + helper_deprecated("product_manufacturer", "") + + # p.product_model + helper_deprecated( + "product_model", "IEC 61966-2-1 Default RGB Colour Space - sRGB" + ) + def test_profile_typesafety(self): """ Profile init type safety diff --git a/Tests/test_imagefile.py b/Tests/test_imagefile.py index 5853fb28f..e2339d76d 100644 --- a/Tests/test_imagefile.py +++ b/Tests/test_imagefile.py @@ -1,4 +1,4 @@ -from .helper import PillowTestCase, hopper, fromstring, tostring +from .helper import unittest, PillowTestCase, hopper, fromstring, tostring from io import BytesIO @@ -6,6 +6,12 @@ from PIL import Image from PIL import ImageFile from PIL import EpsImagePlugin +try: + from PIL import _webp + HAVE_WEBP = True +except ImportError: + HAVE_WEBP = False + codecs = dir(Image.core) @@ -233,3 +239,97 @@ class TestPyDecoder(PillowTestCase): im = MockImageFile(buf) self.assertIsNone(im.format) self.assertIsNone(im.get_format_mimetype()) + + def test_exif_jpeg(self): + im = Image.open("Tests/images/exif-72dpi-int.jpg") # Little endian + exif = im.getexif() + self.assertNotIn(258, exif) + self.assertIn(40960, exif) + self.assertEqual(exif[40963], 450) + self.assertEqual(exif[11], "gThumb 3.0.1") + + out = self.tempfile('temp.jpg') + exif[258] = 8 + del exif[40960] + exif[40963] = 455 + exif[11] = "Pillow test" + im.save(out, exif=exif) + reloaded = Image.open(out) + reloaded_exif = reloaded.getexif() + self.assertEqual(reloaded_exif[258], 8) + self.assertNotIn(40960, exif) + self.assertEqual(reloaded_exif[40963], 455) + self.assertEqual(exif[11], "Pillow test") + + im = Image.open("Tests/images/no-dpi-in-exif.jpg") # Big endian + exif = im.getexif() + self.assertNotIn(258, exif) + self.assertIn(40962, exif) + self.assertEqual(exif[40963], 200) + self.assertEqual(exif[305], "Adobe Photoshop CC 2017 (Macintosh)") + + out = self.tempfile('temp.jpg') + exif[258] = 8 + del exif[34665] + exif[40963] = 455 + exif[305] = "Pillow test" + im.save(out, exif=exif) + reloaded = Image.open(out) + reloaded_exif = reloaded.getexif() + self.assertEqual(reloaded_exif[258], 8) + self.assertNotIn(40960, exif) + self.assertEqual(reloaded_exif[40963], 455) + self.assertEqual(exif[305], "Pillow test") + + @unittest.skipIf(not HAVE_WEBP or not _webp.HAVE_WEBPANIM, + "WebP support not installed with animation") + def test_exif_webp(self): + im = Image.open("Tests/images/hopper.webp") + exif = im.getexif() + self.assertEqual(exif, {}) + + out = self.tempfile('temp.webp') + exif[258] = 8 + exif[40963] = 455 + exif[305] = "Pillow test" + + def check_exif(): + reloaded = Image.open(out) + reloaded_exif = reloaded.getexif() + self.assertEqual(reloaded_exif[258], 8) + self.assertEqual(reloaded_exif[40963], 455) + self.assertEqual(exif[305], "Pillow test") + im.save(out, exif=exif) + check_exif() + im.save(out, exif=exif, save_all=True) + check_exif() + + def test_exif_png(self): + im = Image.open("Tests/images/exif.png") + exif = im.getexif() + self.assertEqual(exif, {274: 1}) + + out = self.tempfile('temp.png') + exif[258] = 8 + del exif[274] + exif[40963] = 455 + exif[305] = "Pillow test" + im.save(out, exif=exif) + + reloaded = Image.open(out) + reloaded_exif = reloaded.getexif() + self.assertEqual(reloaded_exif, { + 258: 8, + 40963: 455, + 305: 'Pillow test', + }) + + def test_exif_interop(self): + im = Image.open("Tests/images/flower.jpg") + exif = im.getexif() + self.assertEqual(exif.get_ifd(0xa005), { + 1: 'R98', + 2: b'0100', + 4097: 2272, + 4098: 1704, + }) diff --git a/Tests/test_imagefont.py b/Tests/test_imagefont.py index be8667211..33f37916c 100644 --- a/Tests/test_imagefont.py +++ b/Tests/test_imagefont.py @@ -6,6 +6,8 @@ from io import BytesIO import os import sys import copy +import re +import distutils.version FONT_PATH = "Tests/fonts/FreeMono.ttf" FONT_SIZE = 20 @@ -49,29 +51,40 @@ class TestImageFont(PillowTestCase): # Freetype has different metrics depending on the version. # (and, other things, but first things first) METRICS = { - ('2', '3'): {'multiline': 30, - 'textsize': 12, - 'getters': (13, 16)}, - ('2', '7'): {'multiline': 6.2, - 'textsize': 2.5, - 'getters': (12, 16)}, - ('2', '8'): {'multiline': 6.2, - 'textsize': 2.5, - 'getters': (12, 16)}, - ('2', '9'): {'multiline': 6.2, - 'textsize': 2.5, - 'getters': (12, 16)}, - 'Default': {'multiline': 0.5, - 'textsize': 0.5, - 'getters': (12, 16)}, + ('>=2.3', '<2.4'): { + 'multiline': 30, + 'textsize': 12, + 'getters': (13, 16)}, + ('>=2.7',): { + 'multiline': 6.2, + 'textsize': 2.5, + 'getters': (12, 16)}, + 'Default': { + 'multiline': 0.5, + 'textsize': 0.5, + 'getters': (12, 16)}, } def setUp(self): - freetype_version = tuple( - ImageFont.core.freetype2_version.split('.') - )[:2] - self.metrics = self.METRICS.get(freetype_version, - self.METRICS['Default']) + freetype = distutils.version.StrictVersion(ImageFont.core.freetype2_version) + + self.metrics = self.METRICS['Default'] + for conditions, metrics in self.METRICS.items(): + if not isinstance(conditions, tuple): + continue + + for condition in conditions: + version = re.sub('[<=>]', '', condition) + if (condition.startswith('>=') and freetype >= version) or \ + (condition.startswith('<') and freetype < version): + # Condition was met + continue + + # Condition failed + break + else: + # All conditions were met + self.metrics = metrics def get_font(self): return ImageFont.truetype(FONT_PATH, FONT_SIZE, @@ -525,6 +538,15 @@ class TestImageFont(PillowTestCase): self.assertEqual(t.getsize_multiline('ABC\nA'), (36, 36)) self.assertEqual(t.getsize_multiline('ABC\nAaaa'), (48, 36)) + def test_complex_font_settings(self): + # Arrange + t = self.get_font() + # Act / Assert + if t.layout_engine == ImageFont.LAYOUT_BASIC: + self.assertRaises(KeyError, t.getmask, 'абвг', direction='rtl') + self.assertRaises(KeyError, t.getmask, 'абвг', features=['-kern']) + self.assertRaises(KeyError, t.getmask, 'абвг', language='sr') + @unittest.skipUnless(HAS_RAQM, "Raqm not Available") class TestImageFont_RaqmLayout(TestImageFont): diff --git a/Tests/test_imagefontctl.py b/Tests/test_imagefontctl.py index d23f6d86f..a00971058 100644 --- a/Tests/test_imagefontctl.py +++ b/Tests/test_imagefontctl.py @@ -130,3 +130,16 @@ class TestImagecomplextext(PillowTestCase): target_img = Image.open(target) self.assert_image_similar(im, target_img, .5) + + def test_language(self): + ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) + + im = Image.new(mode='RGB', size=(300, 100)) + draw = ImageDraw.Draw(im) + draw.text((0, 0), 'абвг', font=ttf, fill=500, + language='sr') + + target = 'Tests/images/test_language.png' + target_img = Image.open(target) + + self.assert_image_similar(im, target_img, .5) diff --git a/Tests/test_imageops.py b/Tests/test_imageops.py index c5e48c431..eaa8fde3c 100644 --- a/Tests/test_imageops.py +++ b/Tests/test_imageops.py @@ -1,7 +1,13 @@ from .helper import PillowTestCase, hopper -from PIL import ImageOps from PIL import Image +from PIL import ImageOps + +try: + from PIL import _webp + HAVE_WEBP = True +except ImportError: + HAVE_WEBP = False class TestImageOps(PillowTestCase): @@ -62,6 +68,9 @@ class TestImageOps(PillowTestCase): ImageOps.solarize(hopper("L")) ImageOps.solarize(hopper("RGB")) + ImageOps.exif_transpose(hopper("L")) + ImageOps.exif_transpose(hopper("RGB")) + def test_1pxfit(self): # Division by zero in equalize if image is 1 pixel high newimg = ImageOps.fit(hopper("RGB").resize((1, 1)), (35, 35)) @@ -218,3 +227,36 @@ class TestImageOps(PillowTestCase): (0, 127, 0), threshold=1, msg='white test pixel incorrect') + + def test_exif_transpose(self): + exts = [".jpg"] + if HAVE_WEBP and _webp.HAVE_WEBPANIM: + exts.append(".webp") + for ext in exts: + base_im = Image.open("Tests/images/hopper"+ext) + + orientations = [base_im] + for i in range(2, 9): + im = Image.open("Tests/images/hopper_orientation_"+str(i)+ext) + orientations.append(im) + for i, orientation_im in enumerate(orientations): + for im in [ + orientation_im, # ImageFile + orientation_im.copy() # Image + ]: + if i == 0: + self.assertNotIn("exif", im.info) + else: + original_exif = im.info["exif"] + transposed_im = ImageOps.exif_transpose(im) + self.assert_image_similar(base_im, transposed_im, 17) + if i == 0: + self.assertNotIn("exif", im.info) + else: + self.assertNotEqual(transposed_im.info["exif"], original_exif) + + self.assertNotIn(0x0112, transposed_im.getexif()) + + # Repeat the operation, to test that it does not keep transposing + transposed_im2 = ImageOps.exif_transpose(transposed_im) + self.assert_image_equal(transposed_im2, transposed_im) diff --git a/Tests/test_numpy.py b/Tests/test_numpy.py index c9c3b0f1e..bdb0ed874 100644 --- a/Tests/test_numpy.py +++ b/Tests/test_numpy.py @@ -113,12 +113,7 @@ class TestNumpy(PillowTestCase): img = Image.fromarray(arr * 255).convert('1') self.assertEqual(img.mode, '1') arr_back = numpy.array(img) - # numpy 1.8 and earlier return this as a boolean. (trusty/precise) - if arr_back.dtype == numpy.bool: - arr_bool = numpy.array([[1, 0, 0, 1, 0], [0, 1, 0, 0, 0]], 'bool') - numpy.testing.assert_array_equal(arr_bool, arr_back) - else: - numpy.testing.assert_array_equal(arr, arr_back) + numpy.testing.assert_array_equal(arr, arr_back) def test_save_tiff_uint16(self): # Tests that we're getting the pixel value in the right byte order. @@ -143,21 +138,21 @@ class TestNumpy(PillowTestCase): np_img = numpy.array(img) self._test_img_equals_nparray(img, np_img) - self.assertEqual(np_img.dtype, numpy.dtype(dtype)) + self.assertEqual(np_img.dtype, dtype) - modes = [("L", 'uint8'), - ("I", 'int32'), - ("F", 'float32'), - ("LA", 'uint8'), - ("RGB", 'uint8'), - ("RGBA", 'uint8'), - ("RGBX", 'uint8'), - ("CMYK", 'uint8'), - ("YCbCr", 'uint8'), + modes = [("L", numpy.uint8), + ("I", numpy.int32), + ("F", numpy.float32), + ("LA", numpy.uint8), + ("RGB", numpy.uint8), + ("RGBA", numpy.uint8), + ("RGBX", numpy.uint8), + ("CMYK", numpy.uint8), + ("YCbCr", numpy.uint8), ("I;16", 'u2'), ("I;16L", '` + Requires libraqm. + + .. versionadded:: 6.0.0 + +.. py:method:: PIL.ImageDraw.ImageDraw.multiline_text(xy, text, fill=None, font=None, anchor=None, spacing=0, align="left", direction=None, features=None, language=None) Draws the string at the given position. @@ -316,7 +326,17 @@ Methods .. versionadded:: 4.2.0 -.. py:method:: PIL.ImageDraw.ImageDraw.textsize(text, font=None, spacing=4, direction=None, features=None) + :param language: Language of the text. Different languages may use + different glyph shapes or ligatures. This parameter tells + the font which language the text is in, and to apply the + correct substitutions as appropriate, if available. + It should be a `BCP47 language code + ` + Requires libraqm. + + .. versionadded:: 6.0.0 + +.. py:method:: PIL.ImageDraw.ImageDraw.textsize(text, font=None, spacing=4, direction=None, features=None, language=None) Return the size of the given string, in pixels. @@ -330,7 +350,6 @@ Methods Requires libraqm. .. versionadded:: 4.2.0 - :param features: A list of OpenType font features to be used during text layout. This is usually used to turn on optional font features that are not enabled by default, @@ -343,8 +362,17 @@ Methods Requires libraqm. .. versionadded:: 4.2.0 + :param language: Language of the text. Different languages may use + different glyph shapes or ligatures. This parameter tells + the font which language the text is in, and to apply the + correct substitutions as appropriate, if available. + It should be a `BCP47 language code + ` + Requires libraqm. -.. py:method:: PIL.ImageDraw.ImageDraw.multiline_textsize(text, font=None, spacing=4, direction=None, features=None) + .. versionadded:: 6.0.0 + +.. py:method:: PIL.ImageDraw.ImageDraw.multiline_textsize(text, font=None, spacing=4, direction=None, features=None, language=None) Return the size of the given string, in pixels. @@ -370,6 +398,16 @@ Methods .. versionadded:: 4.2.0 + :param language: Language of the text. Different languages may use + different glyph shapes or ligatures. This parameter tells + the font which language the text is in, and to apply the + correct substitutions as appropriate, if available. + It should be a `BCP47 language code + ` + Requires libraqm. + + .. versionadded:: 6.0.0 + .. py:method:: PIL.ImageDraw.getdraw(im=None, hints=None) .. warning:: This method is experimental. diff --git a/docs/reference/ImageFont.rst b/docs/reference/ImageFont.rst index 55ce3d382..b30bdac03 100644 --- a/docs/reference/ImageFont.rst +++ b/docs/reference/ImageFont.rst @@ -47,11 +47,45 @@ Functions Methods ------- -.. py:method:: PIL.ImageFont.ImageFont.getsize(text) +.. py:method:: PIL.ImageFont.ImageFont.getsize(text, direction=None, features=[], language=None) + + Returns width and height (in pixels) of given text if rendered in font with + provided direction, features, and language. + + :param text: Text to measure. + + :param direction: Direction of the text. It can be 'rtl' (right to + left), 'ltr' (left to right) or 'ttb' (top to bottom). + Requires libraqm. + + .. versionadded:: 4.2.0 + + :param features: A list of OpenType font features to be used during text + layout. This is usually used to turn on optional + font features that are not enabled by default, + for example 'dlig' or 'ss01', but can be also + used to turn off default font features for + example '-liga' to disable ligatures or '-kern' + to disable kerning. To get all supported + features, see + https://docs.microsoft.com/en-us/typography/opentype/spec/featurelist + Requires libraqm. + + .. versionadded:: 4.2.0 + + :param language: Language of the text. Different languages may use + different glyph shapes or ligatures. This parameter tells + the font which language the text is in, and to apply the + correct substitutions as appropriate, if available. + It should be a `BCP47 language code + ` + Requires libraqm. + + .. versionadded:: 6.0.0 :return: (width, height) -.. py:method:: PIL.ImageFont.ImageFont.getmask(text, mode='', direction=None, features=[]) +.. py:method:: PIL.ImageFont.ImageFont.getmask(text, mode='', direction=None, features=[], language=None) Create a bitmap for the text. @@ -85,5 +119,15 @@ Methods .. versionadded:: 4.2.0 + :param language: Language of the text. Different languages may use + different glyph shapes or ligatures. This parameter tells + the font which language the text is in, and to apply the + correct substitutions as appropriate, if available. + It should be a `BCP47 language code + ` + Requires libraqm. + + .. versionadded:: 6.0.0 + :return: An internal PIL storage memory instance as defined by the :py:mod:`PIL.Image.core` interface module. diff --git a/docs/reference/open_files.rst b/docs/reference/open_files.rst index 511eb97bf..e26d9e639 100644 --- a/docs/reference/open_files.rst +++ b/docs/reference/open_files.rst @@ -3,10 +3,10 @@ File Handling in Pillow ======================= -When opening a file as an image, Pillow requires a filename, -pathlib.Path object, or a file-like object. Pillow uses the filename -or Path to open a file, so for the rest of this article, they will all -be treated as a file-like object. +When opening a file as an image, Pillow requires a filename, ``pathlib.Path`` +object, or a file-like object. Pillow uses the filename or ``Path`` to open a +file, so for the rest of this article, they will all be treated as a file-like +object. The first four of these items are equivalent, the last is dangerous and may fail:: @@ -48,24 +48,23 @@ Issues Image Lifecycle --------------- -* ``Image.open()`` Path-like objects are opened as a file. Metadata is read - from the open file. The file is left open for further usage. +* ``Image.open()`` Filenames and ``Path`` objects are opened as a file. + Metadata is read from the open file. The file is left open for further usage. * ``Image.Image.load()`` When the pixel data from the image is required, ``load()`` is called. The current frame is read into memory. The image can now be used independently of the underlying image file. - If a filename or a path-like object was passed to ``Image.open()``, then - the file object was opened by Pillow and is considered to be used exclusively - by Pillow. So if the image is a single-frame image, the file will - be closed in this method after the frame is read. If the image is a - multi-frame image, (e.g. multipage TIFF and animated GIF) the image file is - left open so that ``Image.Image.seek()`` can load the appropriate frame. + If a filename or a ``Path`` object was passed to ``Image.open()``, then the + file object was opened by Pillow and is considered to be used exclusively by + Pillow. So if the image is a single-frame image, the file will be closed in + this method after the frame is read. If the image is a multi-frame image, + (e.g. multipage TIFF and animated GIF) the image file is left open so that + ``Image.Image.seek()`` can load the appropriate frame. -* ``Image.Image.close()`` Closes the file pointer and destroys the - core image object. This is used in the Pillow context manager - support. e.g.:: +* ``Image.Image.close()`` Closes the file and destroys the core image object. + This is used in the Pillow context manager support. e.g.:: with Image.open('test.jpg') as img: ... # image operations here. @@ -84,17 +83,9 @@ data until the caller has explicitly closed the image. Complications ------------- -* TiffImagePlugin has some code to pass the underlying file descriptor - into libtiff (if working on an actual file). Since libtiff closes - the file descriptor internally, it is duplicated prior to passing it - into libtiff. - -* ``decoder.handles_eof`` This slightly misnamed flag indicates that - the decoder wants to be called with a 0 length buffer when reads are - done. Despite the comments in ``ImageFile.load()``, the only decoder - that actually uses this flag is the Jpeg2K decoder. The use of this - flag in Jpeg2K predated the change to the decoder that added the - pulls_fd flag, and is therefore not used. +* ``TiffImagePlugin`` has some code to pass the underlying file descriptor into + libtiff (if working on an actual file). Since libtiff closes the file + descriptor internally, it is duplicated prior to passing it into libtiff. * I don't think that there's any way to make this safe without changing the lazy loading:: diff --git a/docs/releasenotes/6.0.0.rst b/docs/releasenotes/6.0.0.rst index 58586f3bb..0145347f2 100644 --- a/docs/releasenotes/6.0.0.rst +++ b/docs/releasenotes/6.0.0.rst @@ -99,30 +99,114 @@ version. Use ``PIL.__version__`` instead. +ImageCms.CmsProfile attributes +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Some attributes in ``ImageCms.CmsProfile`` have been deprecated since Pillow 3.2.0. From +6.0.0, they issue a ``DeprecationWarning``: + +======================== =============================== +Deprecated Use instead +======================== =============================== +``color_space`` Padded ``xcolor_space`` +``pcs`` Padded ``connection_space`` +``product_copyright`` Unicode ``copyright`` +``product_desc`` Unicode ``profile_description`` +``product_description`` Unicode ``profile_description`` +``product_manufacturer`` Unicode ``manufacturer`` +``product_model`` Unicode ``model`` +======================== =============================== + +MIME type improvements +^^^^^^^^^^^^^^^^^^^^^^ + +Previously, all JPEG2000 images had the MIME type "image/jpx". This has now been +corrected. After the file format drivers have been loaded, ``Image.MIME["JPEG2000"]`` +will return "image/jp2". ``ImageFile.get_format_mimetype`` will return "image/jpx" if +a JPX profile is present, or "image/jp2" otherwise. + +Previously, all SGI images had the MIME type "image/rgb". This has now been +corrected. After the file format drivers have been loaded, ``Image.MIME["SGI"]`` +will return "image/sgi". ``ImageFile.get_format_mimetype`` will return "image/rgb" if +RGB image data is present, or "image/sgi" otherwise. + +MIME types have been added to the PPM format. After the file format drivers have been +loaded, ``Image.MIME["PPM"]`` will now return the generic "image/x-portable-anymap". +``ImageFile.get_format_mimetype`` will return a MIME type specific to the color type. + +The TGA, PCX and ICO formats also now have MIME types: "image/x-tga", "image/x-pcx" and +"image/x-icon" respectively. + API Additions ============= -DIB File Format +DIB file format ^^^^^^^^^^^^^^^ -Pillow now supports reading and writing the DIB "Device Independent Bitmap" file format. +Pillow now supports reading and writing the Device Independent Bitmap file format. Image.quantize ^^^^^^^^^^^^^^ -The `dither` option is now a customisable parameter (was previously hardcoded to `1`). This parameter takes the same values used in `Image.convert` +The ``dither`` option is now a customisable parameter (was previously hardcoded to ``1``). +This parameter takes the same values used in :py:meth:`~PIL.Image.Image.convert`. -PNG EXIF Data +New language parameter +^^^^^^^^^^^^^^^^^^^^^^ + +These text-rendering functions now accept a ``language`` parameter to request +language-specific glyphs and ligatures from the font: + +* ``ImageDraw.ImageDraw.multiline_text()`` +* ``ImageDraw.ImageDraw.multiline_textsize()`` +* ``ImageDraw.ImageDraw.text()`` +* ``ImageDraw.ImageDraw.textsize()`` +* ``ImageFont.ImageFont.getmask()`` +* ``ImageFont.ImageFont.getsize_multiline()`` +* ``ImageFont.ImageFont.getsize()`` + +Added EXIF class +^^^^^^^^^^^^^^^^ + +:py:meth:`~PIL.Image.Image.getexif` has been added, which returns an +:py:class:`~PIL.Image.Exif` instance. Values can be retrieved and set like a +dictionary. When saving JPEG, PNG or WEBP, the instance can be passed as an +``exif`` argument to include any changes in the output image. + +Added ImageOps.exif_transpose +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +:py:meth:`~PIL.ImageOps.exif_transpose` returns a copy of an image, transposed +according to its EXIF Orientation tag. + +PNG EXIF data ^^^^^^^^^^^^^ EXIF data can now be read from and saved to PNG images. However, unlike other image -formats, EXIF data is not guaranteed to have been read until -:py:meth:`~PIL.Image.Image.load` has been called. +formats, EXIF data is not guaranteed to be present in :py:attr:`~PIL.Image.Image.info` +until :py:meth:`~PIL.Image.Image.load` has been called. Other Changes ============= Reading new DDS image format -~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Pillow can now read uncompressed RGB data from DDS images. + +Reading TIFF with old-style JPEG compression +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Added support reading TIFF files with old-style JPEG compression through LibTIFF. All +YCbCr TIFF images are now always read as RGB. + +TIFF compression codecs +^^^^^^^^^^^^^^^^^^^^^^^ + +Support has been added for the LZMA, Zstd and WebP TIFF compression codecs. + +Improved support for transposing I;16 images +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +I;16, I;16L and I;16B are now supported image modes for all +:py:meth:`~PIL.Image.Image.transpose` operations. diff --git a/setup.py b/setup.py index 5d8159a12..8eb0b3c6b 100755 --- a/setup.py +++ b/setup.py @@ -765,12 +765,7 @@ try: url='http://python-pillow.org', classifiers=[ "Development Status :: 6 - Mature", - "Topic :: Multimedia :: Graphics", - "Topic :: Multimedia :: Graphics :: Capture :: Digital Camera", - "Topic :: Multimedia :: Graphics :: Capture :: Screen Capture", - "Topic :: Multimedia :: Graphics :: Graphics Conversion", - "Topic :: Multimedia :: Graphics :: Viewers", - "License :: Other/Proprietary License", + "License :: OSI Approved :: Historical Permission Notice and Disclaimer (HPND)", # noqa: E501 "Programming Language :: Python :: 2", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", @@ -779,6 +774,11 @@ try: "Programming Language :: Python :: 3.7", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", + "Topic :: Multimedia :: Graphics", + "Topic :: Multimedia :: Graphics :: Capture :: Digital Camera", + "Topic :: Multimedia :: Graphics :: Capture :: Screen Capture", + "Topic :: Multimedia :: Graphics :: Graphics Conversion", + "Topic :: Multimedia :: Graphics :: Viewers", ], python_requires=">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*", cmdclass={"build_ext": pil_build_ext}, @@ -789,7 +789,6 @@ try: packages=["PIL"], package_dir={'': 'src'}, keywords=["Imaging", ], - license='Standard PIL License', zip_safe=not (debug_build() or PLATFORM_MINGW), ) except RequiredDependencyException as err: msg = """ diff --git a/src/PIL/BmpImagePlugin.py b/src/PIL/BmpImagePlugin.py index 19aa6db29..3ca293a94 100644 --- a/src/PIL/BmpImagePlugin.py +++ b/src/PIL/BmpImagePlugin.py @@ -27,7 +27,6 @@ from . import Image, ImageFile, ImagePalette from ._binary import i8, i16le as i16, i32le as i32, \ o8, o16le as o16, o32le as o32 -import math # __version__ is deprecated and will be removed in a future version. Use # PIL.__version__ instead. @@ -105,53 +104,51 @@ class BmpImageFile(ImageFile.ImageFile): # --------------------------------------------- Windows Bitmap v2 to v5 # v3, OS/2 v2, v4, v5 elif file_info['header_size'] in (40, 64, 108, 124): - if file_info['header_size'] >= 40: # v3 and OS/2 - file_info['y_flip'] = i8(header_data[7]) == 0xff - file_info['direction'] = 1 if file_info['y_flip'] else -1 - file_info['width'] = i32(header_data[0:4]) - file_info['height'] = (i32(header_data[4:8]) - if not file_info['y_flip'] - else 2**32 - i32(header_data[4:8])) - file_info['planes'] = i16(header_data[8:10]) - file_info['bits'] = i16(header_data[10:12]) - file_info['compression'] = i32(header_data[12:16]) - # byte size of pixel data - file_info['data_size'] = i32(header_data[16:20]) - file_info['pixels_per_meter'] = (i32(header_data[20:24]), - i32(header_data[24:28])) - file_info['colors'] = i32(header_data[28:32]) - file_info['palette_padding'] = 4 - self.info["dpi"] = tuple( - map(lambda x: int(math.ceil(x / 39.3701)), - file_info['pixels_per_meter'])) - if file_info['compression'] == self.BITFIELDS: - if len(header_data) >= 52: - for idx, mask in enumerate(['r_mask', - 'g_mask', - 'b_mask', - 'a_mask']): - file_info[mask] = i32( - header_data[36 + idx * 4:40 + idx * 4] - ) - else: - # 40 byte headers only have the three components in the - # bitfields masks, ref: - # https://msdn.microsoft.com/en-us/library/windows/desktop/dd183376(v=vs.85).aspx - # See also - # https://github.com/python-pillow/Pillow/issues/1293 - # There is a 4th component in the RGBQuad, in the alpha - # location, but it is listed as a reserved component, - # and it is not generally an alpha channel - file_info['a_mask'] = 0x0 - for mask in ['r_mask', 'g_mask', 'b_mask']: - file_info[mask] = i32(read(4)) - file_info['rgb_mask'] = (file_info['r_mask'], - file_info['g_mask'], - file_info['b_mask']) - file_info['rgba_mask'] = (file_info['r_mask'], - file_info['g_mask'], - file_info['b_mask'], - file_info['a_mask']) + file_info['y_flip'] = i8(header_data[7]) == 0xff + file_info['direction'] = 1 if file_info['y_flip'] else -1 + file_info['width'] = i32(header_data[0:4]) + file_info['height'] = (i32(header_data[4:8]) + if not file_info['y_flip'] + else 2**32 - i32(header_data[4:8])) + file_info['planes'] = i16(header_data[8:10]) + file_info['bits'] = i16(header_data[10:12]) + file_info['compression'] = i32(header_data[12:16]) + # byte size of pixel data + file_info['data_size'] = i32(header_data[16:20]) + file_info['pixels_per_meter'] = (i32(header_data[20:24]), + i32(header_data[24:28])) + file_info['colors'] = i32(header_data[28:32]) + file_info['palette_padding'] = 4 + self.info["dpi"] = tuple( + int(x / 39.3701 + 0.5) for x in file_info['pixels_per_meter']) + if file_info['compression'] == self.BITFIELDS: + if len(header_data) >= 52: + for idx, mask in enumerate(['r_mask', + 'g_mask', + 'b_mask', + 'a_mask']): + file_info[mask] = i32( + header_data[36 + idx * 4:40 + idx * 4] + ) + else: + # 40 byte headers only have the three components in the + # bitfields masks, ref: + # https://msdn.microsoft.com/en-us/library/windows/desktop/dd183376(v=vs.85).aspx + # See also + # https://github.com/python-pillow/Pillow/issues/1293 + # There is a 4th component in the RGBQuad, in the alpha + # location, but it is listed as a reserved component, + # and it is not generally an alpha channel + file_info['a_mask'] = 0x0 + for mask in ['r_mask', 'g_mask', 'b_mask']: + file_info[mask] = i32(read(4)) + file_info['rgb_mask'] = (file_info['r_mask'], + file_info['g_mask'], + file_info['b_mask']) + file_info['rgba_mask'] = (file_info['r_mask'], + file_info['g_mask'], + file_info['b_mask'], + file_info['a_mask']) else: raise IOError("Unsupported BMP header type (%d)" % file_info['header_size']) @@ -180,6 +177,7 @@ class BmpImageFile(ImageFile.ImageFile): SUPPORTED = { 32: [(0xff0000, 0xff00, 0xff, 0x0), (0xff0000, 0xff00, 0xff, 0xff000000), + (0xff, 0xff00, 0xff0000, 0xff000000), (0x0, 0x0, 0x0, 0x0), (0xff000000, 0xff0000, 0xff00, 0x0)], 24: [(0xff0000, 0xff00, 0xff)], @@ -188,6 +186,7 @@ class BmpImageFile(ImageFile.ImageFile): MASK_MODES = { (32, (0xff0000, 0xff00, 0xff, 0x0)): "BGRX", (32, (0xff000000, 0xff0000, 0xff00, 0x0)): "XBGR", + (32, (0xff, 0xff00, 0xff0000, 0xff000000)): "RGBA", (32, (0xff0000, 0xff00, 0xff, 0xff000000)): "BGRA", (32, (0x0, 0x0, 0x0, 0x0)): "BGRA", (24, (0xff0000, 0xff00, 0xff)): "BGR", @@ -200,7 +199,7 @@ class BmpImageFile(ImageFile.ImageFile): raw_mode = MASK_MODES[ (file_info["bits"], file_info["rgba_mask"]) ] - self.mode = "RGBA" if raw_mode in ("BGRA",) else self.mode + self.mode = "RGBA" if "A" in raw_mode else self.mode elif (file_info['bits'] in (24, 16) and file_info['rgb_mask'] in SUPPORTED[file_info['bits']]): raw_mode = MASK_MODES[ @@ -310,7 +309,7 @@ def _save(im, fp, filename, bitmap_header=True): dpi = info.get("dpi", (96, 96)) # 1 meter == 39.3701 inches - ppm = tuple(map(lambda x: int(x * 39.3701), dpi)) + ppm = tuple(map(lambda x: int(x * 39.3701 + 0.5), dpi)) stride = ((im.size[0]*bits+7)//8+3) & (~3) header = 40 # or 64 for OS/2 version 2 diff --git a/src/PIL/EpsImagePlugin.py b/src/PIL/EpsImagePlugin.py index 7421189f8..9ec8ebba5 100644 --- a/src/PIL/EpsImagePlugin.py +++ b/src/PIL/EpsImagePlugin.py @@ -141,13 +141,11 @@ def Ghostscript(tile, size, fp, scale=1): # push data through Ghostscript try: - with open(os.devnull, 'w+b') as devnull: - startupinfo = None - if sys.platform.startswith('win'): - startupinfo = subprocess.STARTUPINFO() - startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW - subprocess.check_call(command, stdout=devnull, - startupinfo=startupinfo) + startupinfo = None + if sys.platform.startswith('win'): + startupinfo = subprocess.STARTUPINFO() + startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW + subprocess.check_call(command, startupinfo=startupinfo) im = Image.open(outfile) im.load() finally: diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py index 4f4e62d6d..b328982aa 100644 --- a/src/PIL/GifImagePlugin.py +++ b/src/PIL/GifImagePlugin.py @@ -122,6 +122,8 @@ class GifImageFile(ImageFile.ImageFile): if not self._seek_check(frame): return if frame < self.__frame: + if frame != 0: + self.im = None self._seek(0) last_frame = self.__frame diff --git a/src/PIL/GimpPaletteFile.py b/src/PIL/GimpPaletteFile.py index 6eef6a2dd..0ee899988 100644 --- a/src/PIL/GimpPaletteFile.py +++ b/src/PIL/GimpPaletteFile.py @@ -32,14 +32,12 @@ class GimpPaletteFile(object): if fp.readline()[:12] != b"GIMP Palette": raise SyntaxError("not a GIMP palette file") - i = 0 - - while i <= 255: + for i in range(256): s = fp.readline() - if not s: break + # skip fields and comment lines if re.match(br"\w+:|#", s): continue @@ -50,10 +48,7 @@ class GimpPaletteFile(object): if len(v) != 3: raise ValueError("bad palette entry") - if 0 <= i <= 255: - self.palette[i] = o8(v[0]) + o8(v[1]) + o8(v[2]) - - i += 1 + self.palette[i] = o8(v[0]) + o8(v[1]) + o8(v[2]) self.palette = b"".join(self.palette) diff --git a/src/PIL/IcoImagePlugin.py b/src/PIL/IcoImagePlugin.py index 16683fb8f..c1c0775da 100644 --- a/src/PIL/IcoImagePlugin.py +++ b/src/PIL/IcoImagePlugin.py @@ -295,3 +295,5 @@ class IcoImageFile(ImageFile.ImageFile): Image.register_open(IcoImageFile.format, IcoImageFile, _accept) Image.register_save(IcoImageFile.format, _save) Image.register_extension(IcoImageFile.format, ".ico") + +Image.register_mime(IcoImageFile.format, "image/x-icon") diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 24e0d7fab..18121c6fe 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -40,8 +40,8 @@ except ImportError: import __builtin__ builtins = __builtin__ -from . import ImageMode -from ._binary import i8 +from . import ImageMode, TiffTags +from ._binary import i8, i32le from ._util import isPath, isStringType, deferred_error import os @@ -54,10 +54,10 @@ import atexit import numbers try: # Python 3 - from collections.abc import Callable + from collections.abc import Callable, MutableMapping except ImportError: # Python 2.7 - from collections import Callable + from collections import Callable, MutableMapping # Silence warning @@ -247,7 +247,7 @@ _MODEINFO = { "L": ("L", "L", ("L",)), "I": ("L", "I", ("I",)), "F": ("L", "F", ("F",)), - "P": ("RGB", "L", ("P",)), + "P": ("P", "L", ("P",)), "RGB": ("RGB", "L", ("R", "G", "B")), "RGBX": ("RGB", "L", ("R", "G", "B", "X")), "RGBA": ("RGB", "L", ("R", "G", "B", "A")), @@ -578,7 +578,12 @@ class Image(object): return self def __exit__(self, *args): - self.close() + if hasattr(self, 'fp') and getattr(self, '_exclusive_fp', False): + if hasattr(self, "_close__fp"): + self._close__fp() + if self.fp: + self.fp.close() + self.fp = None def close(self): """ @@ -610,12 +615,7 @@ class Image(object): if sys.version_info.major >= 3: def __del__(self): - if hasattr(self, "_close__fp"): - self._close__fp() - if (hasattr(self, 'fp') and hasattr(self, '_exclusive_fp') - and self.fp and self._exclusive_fp): - self.fp.close() - self.fp = None + self.__exit__() def _copy(self): self.load() @@ -950,7 +950,7 @@ class Image(object): delete_trns = False # transparency handling if has_transparency: - if self.mode in ('L', 'RGB') and mode == 'RGBA': + if self.mode in ('1', 'L', 'I', 'RGB') and mode == 'RGBA': # Use transparent conversion to promote from transparent # color to an alpha channel. new_im = self._new(self.im.convert_transparent( @@ -1096,7 +1096,13 @@ class Image(object): im = self.im.convert("P", dither, palette.im) return self._new(im) - return self._new(self.im.quantize(colors, method, kmeans)) + im = self._new(self.im.quantize(colors, method, kmeans)) + + from . import ImagePalette + mode = im.im.getpalettemode() + im.palette = ImagePalette.ImagePalette(mode, im.im.getpalette(mode, mode)) + + return im def copy(self): """ @@ -1291,6 +1297,12 @@ class Image(object): return tuple(extrema) return self.im.getextrema() + def getexif(self): + exif = Exif() + if "exif" in self.info: + exif.load(self.info["exif"]) + return exif + def getim(self): """ Returns a capsule that points to the internal image memory. @@ -1559,7 +1571,7 @@ class Image(object): self._ensure_mutable() - if self.mode not in ("LA", "RGBA"): + if self.mode not in ("LA", "PA", "RGBA"): # attempt to promote self to a matching alpha mode try: mode = getmodebase(self.mode) + "A" @@ -1568,7 +1580,7 @@ class Image(object): except (AttributeError, ValueError): # do things the hard way im = self.im.convert(mode) - if im.mode not in ("LA", "RGBA"): + if im.mode not in ("LA", "PA", "RGBA"): raise ValueError # sanity check self.im = im self.pyaccess = None @@ -1576,7 +1588,7 @@ class Image(object): except (KeyError, ValueError): raise ValueError("illegal image mode") - if self.mode == "LA": + if self.mode in ("LA", "PA"): band = 1 else: band = 3 @@ -1619,10 +1631,10 @@ class Image(object): def putpalette(self, data, rawmode="RGB"): """ - Attaches a palette to this image. The image must be a "P" or - "L" image, and the palette sequence must contain 768 integer - values, where each group of three values represent the red, - green, and blue values for the corresponding pixel + Attaches a palette to this image. The image must be a "P", + "PA", "L" or "LA" image, and the palette sequence must contain + 768 integer values, where each group of three values represent + the red, green, and blue values for the corresponding pixel index. Instead of an integer sequence, you can use an 8-bit string. @@ -1631,7 +1643,7 @@ class Image(object): """ from . import ImagePalette - if self.mode not in ("L", "P"): + if self.mode not in ("L", "LA", "P", "PA"): raise ValueError("illegal image mode") self.load() if isinstance(data, ImagePalette.ImagePalette): @@ -1643,7 +1655,7 @@ class Image(object): else: data = "".join(chr(x) for x in data) palette = ImagePalette.raw(rawmode, data) - self.mode = "P" + self.mode = "PA" if "A" in self.mode else "P" self.palette = palette self.palette.mode = "RGB" self.load() # install new palette @@ -1688,7 +1700,7 @@ class Image(object): Rewrites the image to reorder the palette. :param dest_map: A list of indexes into the original palette. - e.g. [1,0] would swap a two item palette, and list(range(255)) + e.g. [1,0] would swap a two item palette, and list(range(256)) is the identity transform. :param source_palette: Bytes or None. :returns: An :py:class:`~PIL.Image.Image` object. @@ -1958,7 +1970,7 @@ class Image(object): filename = fp.name # may mutate self! - self.load() + self._ensure_mutable() save_all = params.pop('save_all', False) self.encoderinfo = params @@ -2005,9 +2017,6 @@ class Image(object): **EOFError** exception. When a sequence file is opened, the library automatically seeks to frame 0. - Note that in the current version of the library, most sequence - formats only allow you to seek to the next frame. - See :py:meth:`~PIL.Image.Image.tell`. :param frame: Frame number, starting at 0. @@ -2374,7 +2383,14 @@ def new(mode, size, color=0): from . import ImageColor color = ImageColor.getcolor(color, mode) - return Image()._new(core.fill(mode, size, color)) + im = Image() + if mode == "P" and \ + isinstance(color, (list, tuple)) and len(color) in [3, 4]: + # RGB or RGBA value for a P image + from . import ImagePalette + im.palette = ImagePalette.ImagePalette() + color = im.palette.getcolor(color) + return im._new(core.fill(mode, size, color)) def frombytes(mode, size, data, decoder_name="raw", *args): @@ -2995,3 +3011,182 @@ def _apply_env_variables(env=None): _apply_env_variables() atexit.register(core.clear_cache) + + +class Exif(MutableMapping): + endian = "<" + + def __init__(self): + self._data = {} + self._ifds = {} + + def _fixup_dict(self, src_dict): + # Helper function for _getexif() + # returns a dict with any single item tuples/lists as individual values + def _fixup(value): + try: + if len(value) == 1 and not isinstance(value, dict): + return value[0] + except Exception: + pass + return value + + return {k: _fixup(v) for k, v in src_dict.items()} + + def _get_ifd_dict(self, tag): + try: + # an offset pointer to the location of the nested embedded IFD. + # It should be a long, but may be corrupted. + self.fp.seek(self._data[tag]) + except (KeyError, TypeError): + pass + else: + from . import TiffImagePlugin + info = TiffImagePlugin.ImageFileDirectory_v1(self.head) + info.load(self.fp) + return self._fixup_dict(info) + + def load(self, data): + # Extract EXIF information. This is highly experimental, + # and is likely to be replaced with something better in a future + # version. + + # The EXIF record consists of a TIFF file embedded in a JPEG + # application marker (!). + self.fp = io.BytesIO(data[6:]) + self.head = self.fp.read(8) + # process dictionary + from . import TiffImagePlugin + info = TiffImagePlugin.ImageFileDirectory_v1(self.head) + self.endian = info._endian + self.fp.seek(info.next) + info.load(self.fp) + self._data = dict(self._fixup_dict(info)) + + # get EXIF extension + ifd = self._get_ifd_dict(0x8769) + if ifd: + self._data.update(ifd) + self._ifds[0x8769] = ifd + + # get gpsinfo extension + ifd = self._get_ifd_dict(0x8825) + if ifd: + self._data[0x8825] = ifd + self._ifds[0x8825] = ifd + + def tobytes(self, offset=0): + from . import TiffImagePlugin + if self.endian == "<": + head = b"II\x2A\x00\x08\x00\x00\x00" + else: + head = b"MM\x00\x2A\x00\x00\x00\x08" + ifd = TiffImagePlugin.ImageFileDirectory_v2(ifh=head) + for tag, value in self._data.items(): + ifd[tag] = value + return b"Exif\x00\x00"+head+ifd.tobytes(offset) + + def get_ifd(self, tag): + if tag not in self._ifds and tag in self._data: + if tag == 0xa005: # interop + self._ifds[tag] = self._get_ifd_dict(tag) + elif tag == 0x927c: # makernote + from . import TiffImagePlugin + if self._data[0x927c][:8] == b"FUJIFILM": + exif_data = self._data[0x927c] + ifd_offset = i32le(exif_data[8:12]) + ifd_data = exif_data[ifd_offset:] + + makernote = {} + for i in range(0, struct.unpack(" 4: + offset, = struct.unpack("H", ifd_data[:2])[0]): + ifd_tag, typ, count, data = struct.unpack( + ">HHL4s", ifd_data[i*12 + 2:(i+1)*12 + 2]) + if ifd_tag == 0x1101: + # CameraInfo + offset, = struct.unpack(">L", data) + self.fp.seek(offset) + + camerainfo = {'ModelID': self.fp.read(4)} + + self.fp.read(4) + # Seconds since 2000 + camerainfo['TimeStamp'] = i32le(self.fp.read(12)) + + self.fp.read(4) + camerainfo['InternalSerialNumber'] = self.fp.read(4) + + self.fp.read(12) + parallax = self.fp.read(4) + handler =\ + TiffImagePlugin.ImageFileDirectory_v2._load_dispatch[ + TiffTags.FLOAT + ][1] + camerainfo['Parallax'] = handler( + TiffImagePlugin.ImageFileDirectory_v2(), + parallax, False) + + self.fp.read(4) + camerainfo['Category'] = self.fp.read(2) + + makernote = {0x1101: dict(self._fixup_dict(camerainfo))} + self._ifds[0x927c] = makernote + return self._ifds.get(tag, {}) + + def __str__(self): + return str(self._data) + + def __len__(self): + return len(self._data) + + def __getitem__(self, tag): + return self._data[tag] + + def __contains__(self, tag): + return tag in self._data + + if not py3: + def has_key(self, tag): + return tag in self + + def __setitem__(self, tag, value): + self._data[tag] = value + + def __delitem__(self, tag): + del self._data[tag] + + def __iter__(self): + return iter(set(self._data)) diff --git a/src/PIL/ImageCms.py b/src/PIL/ImageCms.py index 2cd6f33b5..c75ad6a66 100644 --- a/src/PIL/ImageCms.py +++ b/src/PIL/ImageCms.py @@ -686,11 +686,11 @@ def getProfileName(profile): # // name was "%s - %s" (model, manufacturer) || Description , # // but if the Model and Manufacturer were the same or the model # // was long, Just the model, in 1.x - model = profile.profile.product_model - manufacturer = profile.profile.product_manufacturer + model = profile.profile.model + manufacturer = profile.profile.manufacturer if not (model or manufacturer): - return profile.profile.product_description + "\n" + return (profile.profile.profile_description or "") + "\n" if not manufacturer or len(model) > 30: return model + "\n" return "%s - %s\n" % (model, manufacturer) @@ -727,8 +727,8 @@ def getProfileInfo(profile): # Python, not C. the white point bits weren't working well, # so skipping. # info was description \r\n\r\n copyright \r\n\r\n K007 tag \r\n\r\n whitepoint - description = profile.profile.product_description - cpright = profile.profile.product_copyright + description = profile.profile.profile_description + cpright = profile.profile.copyright arr = [] for elt in (description, cpright): if elt: @@ -762,7 +762,7 @@ def getProfileCopyright(profile): # add an extra newline to preserve pyCMS compatibility if not isinstance(profile, ImageCmsProfile): profile = ImageCmsProfile(profile) - return profile.profile.product_copyright + "\n" + return (profile.profile.copyright or "") + "\n" except (AttributeError, IOError, TypeError, ValueError) as v: raise PyCMSError(v) @@ -790,7 +790,7 @@ def getProfileManufacturer(profile): # add an extra newline to preserve pyCMS compatibility if not isinstance(profile, ImageCmsProfile): profile = ImageCmsProfile(profile) - return profile.profile.product_manufacturer + "\n" + return (profile.profile.manufacturer or "") + "\n" except (AttributeError, IOError, TypeError, ValueError) as v: raise PyCMSError(v) @@ -819,7 +819,7 @@ def getProfileModel(profile): # add an extra newline to preserve pyCMS compatibility if not isinstance(profile, ImageCmsProfile): profile = ImageCmsProfile(profile) - return profile.profile.product_model + "\n" + return (profile.profile.model or "") + "\n" except (AttributeError, IOError, TypeError, ValueError) as v: raise PyCMSError(v) @@ -848,7 +848,7 @@ def getProfileDescription(profile): # add an extra newline to preserve pyCMS compatibility if not isinstance(profile, ImageCmsProfile): profile = ImageCmsProfile(profile) - return profile.profile.product_description + "\n" + return (profile.profile.profile_description or "") + "\n" except (AttributeError, IOError, TypeError, ValueError) as v: raise PyCMSError(v) diff --git a/src/PIL/ImageDraw.py b/src/PIL/ImageDraw.py index ac549790a..86512bb82 100644 --- a/src/PIL/ImageDraw.py +++ b/src/PIL/ImageDraw.py @@ -282,13 +282,17 @@ class ImageDraw(object): self.draw.draw_bitmap(xy, mask, ink) def multiline_text(self, xy, text, fill=None, font=None, anchor=None, - spacing=4, align="left", direction=None, features=None): + spacing=4, align="left", direction=None, features=None, + language=None): widths = [] max_width = 0 lines = self._multiline_split(text) line_spacing = self.textsize('A', font=font)[1] + spacing for line in lines: - line_width, line_height = self.textsize(line, font) + line_width, line_height = self.textsize(line, font, + direction=direction, + features=features, + language=language) widths.append(line_width) max_width = max(max_width, line_width) left, top = xy @@ -302,29 +306,30 @@ class ImageDraw(object): else: raise ValueError('align must be "left", "center" or "right"') self.text((left, top), line, fill, font, anchor, - direction=direction, features=features) + direction=direction, features=features, language=language) top += line_spacing left = xy[0] def textsize(self, text, font=None, spacing=4, direction=None, - features=None): + features=None, language=None): """Get the size of a given string, in pixels.""" if self._multiline_check(text): return self.multiline_textsize(text, font, spacing, - direction, features) + direction, features, language) if font is None: font = self.getfont() - return font.getsize(text, direction, features) + return font.getsize(text, direction, features, language) def multiline_textsize(self, text, font=None, spacing=4, direction=None, - features=None): + features=None, language=None): max_width = 0 lines = self._multiline_split(text) line_spacing = self.textsize('A', font=font)[1] + spacing for line in lines: line_width, line_height = self.textsize(line, font, spacing, - direction, features) + direction, features, + language) max_width = max(max_width, line_width) return max_width, len(lines)*line_spacing - spacing diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py index 2e1f28003..06e7b76ba 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -596,8 +596,6 @@ class PyDecoder(object): Override to perform the decoding process. :param buffer: A bytes object with the data to be decoded. - If `handles_eof` is set, then `buffer` will be empty and `self.fd` - will be set. :returns: A tuple of (bytes consumed, errcode). If finished with decoding return <0 for the bytes consumed. Err codes are from `ERRORS` diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index 7454b4413..580aa8744 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -158,17 +158,17 @@ class FreeTypeFont(object): def getmetrics(self): return self.font.ascent, self.font.descent - def getsize(self, text, direction=None, features=None): - size, offset = self.font.getsize(text, direction, features) + def getsize(self, text, direction=None, features=None, language=None): + size, offset = self.font.getsize(text, direction, features, language) 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, language=None): max_width = 0 lines = self._multiline_split(text) line_spacing = self.getsize('A')[1] + spacing for line in lines: - line_width, line_height = self.getsize(line, direction, features) + line_width, line_height = self.getsize(line, direction, features, language) max_width = max(max_width, line_width) return max_width, len(lines)*line_spacing - spacing @@ -176,15 +176,15 @@ class FreeTypeFont(object): def getoffset(self, text): return self.font.getsize(text)[1] - def getmask(self, text, mode="", direction=None, features=None): - return self.getmask2(text, mode, direction=direction, - features=features)[0] + def getmask(self, text, mode="", direction=None, features=None, language=None): + return self.getmask2(text, mode, direction=direction, features=features, + language=language)[0] def getmask2(self, text, mode="", fill=Image.core.fill, direction=None, - features=None, *args, **kwargs): - size, offset = self.font.getsize(text, direction, features) + features=None, language=None, *args, **kwargs): + size, offset = self.font.getsize(text, direction, features, language) im = fill("L", size, 0) - self.font.render(text, im.id, mode == "1", direction, features) + self.font.render(text, im.id, mode == "1", direction, features, language) return im, offset def font_variant(self, font=None, size=None, index=None, encoding=None, diff --git a/src/PIL/ImageOps.py b/src/PIL/ImageOps.py index b93f6f35d..ab6a3d2c4 100644 --- a/src/PIL/ImageOps.py +++ b/src/PIL/ImageOps.py @@ -522,3 +522,30 @@ def solarize(image, threshold=128): else: lut.append(255-i) return _lut(image, lut) + + +def exif_transpose(image): + """ + If an image has an EXIF Orientation tag, return a new image that is + transposed accordingly. Otherwise, return a copy of the image. + + :param image: The image to transpose. + :return: An image. + """ + exif = image.getexif() + orientation = exif.get(0x0112) + method = { + 2: Image.FLIP_LEFT_RIGHT, + 3: Image.ROTATE_180, + 4: Image.FLIP_TOP_BOTTOM, + 5: Image.TRANSPOSE, + 6: Image.ROTATE_270, + 7: Image.TRANSVERSE, + 8: Image.ROTATE_90 + }.get(orientation) + if method is not None: + transposed_image = image.transpose(method) + del exif[0x0112] + transposed_image.info["exif"] = exif.tobytes() + return transposed_image + return image.copy() diff --git a/src/PIL/IptcImagePlugin.py b/src/PIL/IptcImagePlugin.py index 5055d2a00..371bb3acf 100644 --- a/src/PIL/IptcImagePlugin.py +++ b/src/PIL/IptcImagePlugin.py @@ -198,35 +198,9 @@ def getiptcinfo(im): elif isinstance(im, JpegImagePlugin.JpegImageFile): # extract the IPTC/NAA resource - try: - app = im.app["APP13"] - if app[:14] == b"Photoshop 3.0\x00": - app = app[14:] - # parse the image resource block - offset = 0 - while app[offset:offset+4] == b"8BIM": - offset += 4 - # resource code - code = i16(app, offset) - offset += 2 - # resource name (usually empty) - name_len = i8(app[offset]) - # name = app[offset+1:offset+1+name_len] - offset = 1 + offset + name_len - if offset & 1: - offset += 1 - # resource data block - size = i32(app, offset) - offset += 4 - if code == 0x0404: - # 0x0404 contains IPTC/NAA data - data = app[offset:offset+size] - break - offset = offset + size - if offset & 1: - offset += 1 - except (AttributeError, KeyError): - pass + photoshop = im.info.get("photoshop") + if photoshop: + data = photoshop.get(0x0404) elif isinstance(im, TiffImagePlugin.TiffImageFile): # get raw data from the IPTC/NAA tag (PhotoShop tags the data diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py index 2f76e9675..3caedbd92 100644 --- a/src/PIL/JpegImagePlugin.py +++ b/src/PIL/JpegImagePlugin.py @@ -39,7 +39,7 @@ import struct import io import warnings from . import Image, ImageFile, TiffImagePlugin -from ._binary import i8, o8, i16be as i16 +from ._binary import i8, o8, i16be as i16, i32be as i32 from .JpegPresets import presets from ._util import isStringType @@ -86,7 +86,7 @@ def APP(self, marker): self.info["jfif_density"] = jfif_density elif marker == 0xFFE1 and s[:5] == b"Exif\0": if "exif" not in self.info: - # extract Exif information (incomplete) + # extract EXIF information (incomplete) self.info["exif"] = s # FIXME: value will change elif marker == 0xFFE2 and s[:5] == b"FPXR\0": # extract FlashPix information (incomplete) @@ -104,6 +104,39 @@ def APP(self, marker): # reassemble the profile, rather than assuming that the APP2 # markers appear in the correct sequence. self.icclist.append(s) + elif marker == 0xFFED: + if s[:14] == b"Photoshop 3.0\x00": + blocks = s[14:] + # parse the image resource block + offset = 0 + photoshop = {} + while blocks[offset:offset+4] == b"8BIM": + offset += 4 + # resource code + code = i16(blocks, offset) + offset += 2 + # resource name (usually empty) + name_len = i8(blocks[offset]) + # name = blocks[offset+1:offset+1+name_len] + offset = 1 + offset + name_len + if offset & 1: + offset += 1 + # resource data block + size = i32(blocks, offset) + offset += 4 + data = blocks[offset:offset+size] + if code == 0x03ED: # ResolutionInfo + data = { + 'XResolution': i32(data[:4]) / 65536, + 'DisplayedUnitsX': i16(data[4:8]), + 'YResolution': i32(data[8:12]) / 65536, + 'DisplayedUnitsY': i16(data[12:]), + } + photoshop[code] = data + offset = offset + size + if offset & 1: + offset += 1 + self.info["photoshop"] = photoshop elif marker == 0xFFEE and s[:5] == b"Adobe": self.info["adobe"] = i16(s, 5) # extract Adobe custom properties @@ -127,15 +160,15 @@ def APP(self, marker): resolution_unit = exif[0x0128] x_resolution = exif[0x011A] try: - dpi = x_resolution[0] / x_resolution[1] + dpi = float(x_resolution[0]) / x_resolution[1] except TypeError: dpi = x_resolution if resolution_unit == 3: # cm # 1 dpcm = 2.54 dpi dpi *= 2.54 - self.info["dpi"] = dpi, dpi + self.info["dpi"] = int(dpi + 0.5), int(dpi + 0.5) except (KeyError, SyntaxError, ZeroDivisionError): - # SyntaxError for invalid/unreadable exif + # SyntaxError for invalid/unreadable EXIF # KeyError for dpi not included # ZeroDivisionError for invalid dpi rational value self.info["dpi"] = 72, 72 @@ -439,60 +472,23 @@ class JpegImageFile(ImageFile.ImageFile): def _fixup_dict(src_dict): # Helper function for _getexif() # returns a dict with any single item tuples/lists as individual values - def _fixup(value): - try: - if len(value) == 1 and not isinstance(value, dict): - return value[0] - except Exception: - pass - return value - - return {k: _fixup(v) for k, v in src_dict.items()} + exif = Image.Exif() + return exif._fixup_dict(src_dict) def _getexif(self): - # Extract EXIF information. This method is highly experimental, - # and is likely to be replaced with something better in a future - # version. - - # The EXIF record consists of a TIFF file embedded in a JPEG - # application marker (!). + # Use the cached version if possible try: - data = self.info["exif"] + return self.info["parsed_exif"] except KeyError: - return None - fp = io.BytesIO(data[6:]) - head = fp.read(8) - # process dictionary - info = TiffImagePlugin.ImageFileDirectory_v1(head) - fp.seek(info.next) - info.load(fp) - exif = dict(_fixup_dict(info)) - # get exif extension - try: - # exif field 0x8769 is an offset pointer to the location - # of the nested embedded exif ifd. - # It should be a long, but may be corrupted. - fp.seek(exif[0x8769]) - except (KeyError, TypeError): pass - else: - info = TiffImagePlugin.ImageFileDirectory_v1(head) - info.load(fp) - exif.update(_fixup_dict(info)) - # get gpsinfo extension - try: - # exif field 0x8825 is an offset pointer to the location - # of the nested embedded gps exif ifd. - # It should be a long, but may be corrupted. - fp.seek(exif[0x8825]) - except (KeyError, TypeError): - pass - else: - info = TiffImagePlugin.ImageFileDirectory_v1(head) - info.load(fp) - exif[0x8825] = _fixup_dict(info) + if "exif" not in self.info: + return None + exif = dict(self.getexif()) + + # Cache the result for future use + self.info["parsed_exif"] = exif return exif @@ -728,6 +724,10 @@ def _save(im, fp, filename): optimize = info.get("optimize", False) + exif = info.get("exif", b"") + if isinstance(exif, Image.Exif): + exif = exif.tobytes() + # get keyword arguments im.encoderconfig = ( quality, @@ -739,7 +739,7 @@ def _save(im, fp, filename): subsampling, qtables, extra, - info.get("exif", b"") + exif ) # if we optimize, libjpeg needs a buffer big enough to hold the whole image @@ -757,9 +757,9 @@ def _save(im, fp, filename): else: bufsize = im.size[0] * im.size[1] - # The exif info needs to be written as one block, + APP1, + one spare byte. + # The EXIF info needs to be written as one block, + APP1, + one spare byte. # Ensure that our buffer is big enough. Same with the icc_profile block. - bufsize = max(ImageFile.MAXBLOCK, bufsize, len(info.get("exif", b"")) + 5, + bufsize = max(ImageFile.MAXBLOCK, bufsize, len(exif) + 5, len(extra) + 1) ImageFile._save(im, fp, [("jpeg", (0, 0)+im.size, 0, rawmode)], bufsize) @@ -786,7 +786,8 @@ def jpeg_factory(fp=None, filename=None): if mpheader[45057] > 1: # It's actually an MPO from .MpoImagePlugin import MpoImageFile - im = MpoImageFile(fp, filename) + # Don't reload everything, just convert it. + im = MpoImageFile.adopt(im, mpheader) except (TypeError, IndexError): # It is really a JPEG pass diff --git a/src/PIL/MpoImagePlugin.py b/src/PIL/MpoImagePlugin.py index 64c0f57a4..ead2f1fff 100644 --- a/src/PIL/MpoImagePlugin.py +++ b/src/PIL/MpoImagePlugin.py @@ -18,7 +18,8 @@ # See the README file for information on usage and redistribution. # -from . import Image, JpegImagePlugin +from . import Image, ImageFile, JpegImagePlugin +from ._binary import i16be as i16 # __version__ is deprecated and will be removed in a future version. Use # PIL.__version__ instead. @@ -46,7 +47,10 @@ class MpoImageFile(JpegImagePlugin.JpegImageFile): def _open(self): self.fp.seek(0) # prep the fp in order to pass the JPEG test JpegImagePlugin.JpegImageFile._open(self) - self.mpinfo = self._getmp() + self._after_jpeg_open() + + def _after_jpeg_open(self, mpheader=None): + self.mpinfo = mpheader if mpheader is not None else self._getmp() self.__framecount = self.mpinfo[0xB001] self.__mpoffsets = [mpent['DataOffset'] + self.info['mpoffset'] for mpent in self.mpinfo[0xB002]] @@ -78,6 +82,20 @@ class MpoImageFile(JpegImagePlugin.JpegImageFile): return self.fp = self.__fp self.offset = self.__mpoffsets[frame] + + self.fp.seek(self.offset + 2) # skip SOI marker + if "parsed_exif" in self.info: + del self.info["parsed_exif"] + if i16(self.fp.read(2)) == 0xFFE1: # APP1 + n = i16(self.fp.read(2))-2 + self.info["exif"] = ImageFile._safe_read(self.fp, n) + + exif = self._getexif() + if 40962 in exif and 40963 in exif: + self._size = (exif[40962], exif[40963]) + elif "exif" in self.info: + del self.info["exif"] + self.tile = [ ("jpeg", (0, 0) + self.size, self.offset, (self.mode, "")) ] @@ -95,6 +113,22 @@ class MpoImageFile(JpegImagePlugin.JpegImageFile): finally: self.__fp = None + @staticmethod + def adopt(jpeg_instance, mpheader=None): + """ + Transform the instance of JpegImageFile into + an instance of MpoImageFile. + After the call, the JpegImageFile is extended + to be an MpoImageFile. + + This is essentially useful when opening a JPEG + file that reveals itself as an MPO, to avoid + double call to _open. + """ + jpeg_instance.__class__ = MpoImageFile + jpeg_instance._after_jpeg_open(mpheader) + return jpeg_instance + # --------------------------------------------------------------------- # Registry stuff diff --git a/src/PIL/PcxImagePlugin.py b/src/PIL/PcxImagePlugin.py index 8094a2866..02dbe26e0 100644 --- a/src/PIL/PcxImagePlugin.py +++ b/src/PIL/PcxImagePlugin.py @@ -180,3 +180,5 @@ Image.register_open(PcxImageFile.format, PcxImageFile, _accept) Image.register_save(PcxImageFile.format, _save) Image.register_extension(PcxImageFile.format, ".pcx") + +Image.register_mime(PcxImageFile.format, "image/x-pcx") diff --git a/src/PIL/PngImagePlugin.py b/src/PIL/PngImagePlugin.py index df632af1d..4e192ecd6 100644 --- a/src/PIL/PngImagePlugin.py +++ b/src/PIL/PngImagePlugin.py @@ -54,19 +54,24 @@ _MAGIC = b"\211PNG\r\n\032\n" _MODES = { # supported bits/color combinations, and corresponding modes/rawmodes + # Greyscale (1, 0): ("1", "1"), (2, 0): ("L", "L;2"), (4, 0): ("L", "L;4"), (8, 0): ("L", "L"), (16, 0): ("I", "I;16B"), + # Truecolour (8, 2): ("RGB", "RGB"), (16, 2): ("RGB", "RGB;16B"), + # Indexed-colour (1, 3): ("P", "P;1"), (2, 3): ("P", "P;2"), (4, 3): ("P", "P;4"), (8, 3): ("P", "P"), + # Greyscale with alpha (8, 4): ("LA", "LA"), (16, 4): ("RGBA", "LA;16B"), # LA;16B->LA not yet available + # Truecolour with alpha (8, 6): ("RGBA", "RGBA"), (16, 6): ("RGBA", "RGBA;16B"), } @@ -386,7 +391,7 @@ class PngStream(ChunkStream): # otherwise, we have a byte string with one alpha value # for each palette entry self.im_info["transparency"] = s - elif self.im_mode == "L": + elif self.im_mode in ("1", "L", "I"): self.im_info["transparency"] = i16(s) elif self.im_mode == "RGB": self.im_info["transparency"] = i16(s), i16(s[2:]), i16(s[4:]) @@ -691,8 +696,14 @@ class PngImageFile(ImageFile.ImageFile): def _getexif(self): if "exif" not in self.info: self.load() - from .JpegImagePlugin import _getexif - return _getexif(self) + if "exif" not in self.info: + return None + return dict(self.getexif()) + + def getexif(self): + if "exif" not in self.info: + self.load() + return ImageFile.ImageFile.getexif(self) # -------------------------------------------------------------------- @@ -707,6 +718,7 @@ _OUTMODES = { "L": ("L", b'\x08\x00'), "LA": ("LA", b'\x08\x04'), "I": ("I;16B", b'\x10\x00'), + "I;16": ("I;16B", b'\x10\x00'), "P;1": ("P;1", b'\x01\x03'), "P;2": ("P;2", b'\x02\x03'), "P;4": ("P;4", b'\x04\x03'), @@ -840,7 +852,7 @@ def _save(im, fp, filename, chunk=putchunk): transparency = max(0, min(255, transparency)) alpha = b'\xFF' * transparency + b'\0' chunk(fp, b"tRNS", alpha[:alpha_bytes]) - elif im.mode == "L": + elif im.mode in ("1", "L", "I"): transparency = max(0, min(65535, transparency)) chunk(fp, b"tRNS", o16(transparency)) elif im.mode == "RGB": @@ -874,6 +886,8 @@ def _save(im, fp, filename, chunk=putchunk): exif = im.encoderinfo.get("exif", im.info.get("exif")) if exif: + if isinstance(exif, Image.Exif): + exif = exif.tobytes(8) if exif.startswith(b"Exif\x00\x00"): exif = exif[6:] chunk(fp, b"eXIf", exif) diff --git a/src/PIL/PpmImagePlugin.py b/src/PIL/PpmImagePlugin.py index e3e411cad..a1ac8d69e 100644 --- a/src/PIL/PpmImagePlugin.py +++ b/src/PIL/PpmImagePlugin.py @@ -164,6 +164,6 @@ def _save(im, fp, filename): Image.register_open(PpmImageFile.format, PpmImageFile, _accept) Image.register_save(PpmImageFile.format, _save) -Image.register_extensions(PpmImageFile.format, [".pbm", ".pgm", ".ppm"]) +Image.register_extensions(PpmImageFile.format, [".pbm", ".pgm", ".ppm", ".pnm"]) Image.register_mime(PpmImageFile.format, "image/x-portable-anymap") diff --git a/src/PIL/SpiderImagePlugin.py b/src/PIL/SpiderImagePlugin.py index 4d5aa3f0e..b601847cb 100644 --- a/src/PIL/SpiderImagePlugin.py +++ b/src/PIL/SpiderImagePlugin.py @@ -208,7 +208,7 @@ class SpiderImageFile(ImageFile.ImageFile): # given a list of filenames, return a list of images def loadImageSeries(filelist=None): - """create a list of Image.images for use in montage""" + """create a list of :py:class:`~PIL.Image.Image` objects for use in a montage""" if filelist is None or len(filelist) < 1: return diff --git a/src/PIL/TgaImagePlugin.py b/src/PIL/TgaImagePlugin.py index 26b1e9c60..ae9697b29 100644 --- a/src/PIL/TgaImagePlugin.py +++ b/src/PIL/TgaImagePlugin.py @@ -226,4 +226,6 @@ def _save(im, fp, filename): Image.register_open(TgaImageFile.format, TgaImageFile) Image.register_save(TgaImageFile.format, _save) -Image.register_extension(TgaImageFile.format, ".tga") +Image.register_extensions(TgaImageFile.format, [".tga", ".icb", ".vda", ".vst"]) + +Image.register_mime(TgaImageFile.format, "image/x-tga") diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index 2e05c9a0b..44dde1995 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -263,10 +263,10 @@ OPEN_INFO = { (II, 5, (1,), 1, (8, 8, 8, 8, 8, 8), (0, 0)): ("CMYK", "CMYKXX"), (MM, 5, (1,), 1, (8, 8, 8, 8, 8, 8), (0, 0)): ("CMYK", "CMYKXX"), - # JPEG compressed images handled by LibTiff and auto-converted to RGB + # JPEG compressed images handled by LibTiff and auto-converted to RGBX # Minimal Baseline TIFF requires YCbCr images to have 3 SamplesPerPixel - (II, 6, (1,), 1, (8, 8, 8), ()): ("RGB", "RGB"), - (MM, 6, (1,), 1, (8, 8, 8), ()): ("RGB", "RGB"), + (II, 6, (1,), 1, (8, 8, 8), ()): ("RGB", "RGBX"), + (MM, 6, (1,), 1, (8, 8, 8), ()): ("RGB", "RGBX"), (II, 8, (1,), 1, (8, 8, 8), ()): ("LAB", "LAB"), (MM, 8, (1,), 1, (8, 8, 8), ()): ("LAB", "LAB"), @@ -785,17 +785,12 @@ class ImageFileDirectory_v2(MutableMapping): warnings.warn(str(msg)) return - def save(self, fp): - - if fp.tell() == 0: # skip TIFF header on subsequent pages - # tiff header -- PIL always starts the first IFD at offset 8 - fp.write(self._prefix + self._pack("HL", 42, 8)) - + def tobytes(self, offset=0): # FIXME What about tagdata? - fp.write(self._pack("H", len(self._tags_v2))) + result = self._pack("H", len(self._tags_v2)) entries = [] - offset = fp.tell() + len(self._tags_v2) * 12 + 4 + offset = offset + len(result) + len(self._tags_v2) * 12 + 4 stripoffsets = None # pass 1: convert tags to binary format @@ -844,18 +839,29 @@ class ImageFileDirectory_v2(MutableMapping): for tag, typ, count, value, data in entries: if DEBUG > 1: print(tag, typ, count, repr(value), repr(data)) - fp.write(self._pack("HHL4s", tag, typ, count, value)) + result += self._pack("HHL4s", tag, typ, count, value) # -- overwrite here for multi-page -- - fp.write(b"\0\0\0\0") # end of entries + result += b"\0\0\0\0" # end of entries # pass 3: write auxiliary data to file for tag, typ, count, value, data in entries: - fp.write(data) + result += data if len(data) & 1: - fp.write(b"\0") + result += b"\0" - return offset + return result + + def save(self, fp): + + if fp.tell() == 0: # skip TIFF header on subsequent pages + # tiff header -- PIL always starts the first IFD at offset 8 + fp.write(self._prefix + self._pack("HL", 42, 8)) + + offset = fp.tell() + result = self.tobytes(offset) + fp.write(result) + return offset + len(result) ImageFileDirectory_v2._load_dispatch = _load_dispatch @@ -985,7 +991,6 @@ class TiffImageFile(ImageFile.ImageFile): self.__fp = self.fp self._frame_pos = [] self._n_frames = None - self._is_animated = None if DEBUG: print("*** TiffImageFile._open ***") @@ -999,29 +1004,14 @@ class TiffImageFile(ImageFile.ImageFile): def n_frames(self): if self._n_frames is None: current = self.tell() - try: - while True: - self._seek(self.tell() + 1) - except EOFError: - self._n_frames = self.tell() + 1 + self._seek(len(self._frame_pos)) + while self._n_frames is None: + self._seek(self.tell() + 1) self.seek(current) return self._n_frames @property def is_animated(self): - if self._is_animated is None: - if self._n_frames is not None: - self._is_animated = self._n_frames != 1 - else: - current = self.tell() - - try: - self.seek(1) - self._is_animated = True - except EOFError: - self._is_animated = False - - self.seek(current) return self._is_animated def seek(self, frame): @@ -1053,10 +1043,13 @@ class TiffImageFile(ImageFile.ImageFile): print("Loading tags, location: %s" % self.fp.tell()) self.tag_v2.load(self.fp) self.__next = self.tag_v2.next + if self.__next == 0: + self._n_frames = frame + 1 + if len(self._frame_pos) == 1: + self._is_animated = self.__next != 0 self.__frame += 1 self.fp.seek(self._frame_pos[frame]) self.tag_v2.load(self.fp) - self.__next = self.tag_v2.next # fill the legacy tag/ifd entries self.tag = self.ifd = ImageFileDirectory_v1.from_v2(self.tag_v2) self.__frame = frame @@ -1087,7 +1080,7 @@ class TiffImageFile(ImageFile.ImageFile): def load_end(self): # allow closing if we're on the first frame, there's no next # This is the ImageFile.load path only, libtiff specific below. - if self.__frame == 0 and not self.__next: + if not self._is_animated: self._close_exclusive_fp_after_loading = True def _load_libtiff(self): @@ -1167,10 +1160,9 @@ class TiffImageFile(ImageFile.ImageFile): self.tile = [] self.readonly = 0 # libtiff closed the fp in a, we need to close self.fp, if possible - if self._exclusive_fp: - if self.__frame == 0 and not self.__next: - self.fp.close() - self.fp = None # might be shared + if self._exclusive_fp and not self._is_animated: + self.fp.close() + self.fp = None # might be shared if err < 0: raise IOError(err) @@ -1191,6 +1183,10 @@ class TiffImageFile(ImageFile.ImageFile): # the specification photo = self.tag_v2.get(PHOTOMETRIC_INTERPRETATION, 0) + # old style jpeg compression images most certainly are YCbCr + if self._compression == "tiff_jpeg": + photo = 6 + fillorder = self.tag_v2.get(FILLORDER, 1) if DEBUG: @@ -1257,11 +1253,11 @@ class TiffImageFile(ImageFile.ImageFile): if xres and yres: resunit = self.tag_v2.get(RESOLUTION_UNIT) if resunit == 2: # dots per inch - self.info["dpi"] = xres, yres + self.info["dpi"] = int(xres + 0.5), int(yres + 0.5) elif resunit == 3: # dots per centimeter. convert to dpi - self.info["dpi"] = xres * 2.54, yres * 2.54 + self.info["dpi"] = int(xres * 2.54 + 0.5), int(yres * 2.54 + 0.5) elif resunit is None: # used to default to 1, but now 2) - self.info["dpi"] = xres, yres + self.info["dpi"] = int(xres + 0.5), int(yres + 0.5) # For backward compatibility, # we also preserve the old behavior self.info["resolution"] = xres, yres @@ -1472,8 +1468,8 @@ def _save(im, fp, filename): dpi = im.encoderinfo.get("dpi") if dpi: ifd[RESOLUTION_UNIT] = 2 - ifd[X_RESOLUTION] = dpi[0] - ifd[Y_RESOLUTION] = dpi[1] + ifd[X_RESOLUTION] = int(dpi[0] + 0.5) + ifd[Y_RESOLUTION] = int(dpi[1] + 0.5) if bits != (1,): ifd[BITSPERSAMPLE] = bits diff --git a/src/PIL/WebPImagePlugin.py b/src/PIL/WebPImagePlugin.py index 212e6b4c7..f2a99bb9d 100644 --- a/src/PIL/WebPImagePlugin.py +++ b/src/PIL/WebPImagePlugin.py @@ -93,8 +93,9 @@ class WebPImageFile(ImageFile.ImageFile): self.seek(0) def _getexif(self): - from .JpegImagePlugin import _getexif - return _getexif(self) + if "exif" not in self.info: + return None + return dict(self.getexif()) @property def n_frames(self): @@ -186,7 +187,7 @@ def _save_all(im, fp, filename): # will preserve non-alpha modes total = 0 for ims in [im]+append_images: - total += 1 if not hasattr(ims, "n_frames") else ims.n_frames + total += getattr(ims, "n_frames", 1) if total == 1: _save(im, fp, filename) return @@ -216,6 +217,8 @@ def _save_all(im, fp, filename): method = im.encoderinfo.get("method", 0) icc_profile = im.encoderinfo.get("icc_profile", "") exif = im.encoderinfo.get("exif", "") + if isinstance(exif, Image.Exif): + exif = exif.tobytes() xmp = im.encoderinfo.get("xmp", "") if allow_mixed: lossless = False @@ -254,10 +257,7 @@ def _save_all(im, fp, filename): try: for ims in [im]+append_images: # Get # of frames in this image - if not hasattr(ims, "n_frames"): - nfr = 1 - else: - nfr = ims.n_frames + nfr = getattr(ims, "n_frames", 1) for idx in range(nfr): ims.seek(idx) @@ -318,6 +318,8 @@ def _save(im, fp, filename): quality = im.encoderinfo.get("quality", 80) icc_profile = im.encoderinfo.get("icc_profile", "") exif = im.encoderinfo.get("exif", "") + if isinstance(exif, Image.Exif): + exif = exif.tobytes() xmp = im.encoderinfo.get("xmp", "") if im.mode not in _VALID_WEBP_LEGACY_MODES: diff --git a/src/PIL/WmfImagePlugin.py b/src/PIL/WmfImagePlugin.py index 06eb851c9..413bd1847 100644 --- a/src/PIL/WmfImagePlugin.py +++ b/src/PIL/WmfImagePlugin.py @@ -131,8 +131,8 @@ class WmfStubImageFile(ImageFile.StubImageFile): size = x1 - x0, y1 - y0 # calculate dots per inch from bbox and frame - xdpi = 2540 * (x1 - y0) // (frame[2] - frame[0]) - ydpi = 2540 * (y1 - y0) // (frame[3] - frame[1]) + xdpi = int(2540.0 * (x1 - y0) / (frame[2] - frame[0]) + 0.5) + ydpi = int(2540.0 * (y1 - y0) / (frame[3] - frame[1]) + 0.5) self.info["wmf_bbox"] = x0, y0, x1, y1 diff --git a/src/PIL/_version.py b/src/PIL/_version.py index 0b0d90e80..7db210d63 100644 --- a/src/PIL/_version.py +++ b/src/PIL/_version.py @@ -1,2 +1,2 @@ # Master version for Pillow -__version__ = '6.0.0.dev0' +__version__ = '6.1.0.dev0' diff --git a/src/_imaging.c b/src/_imaging.c index 7715a7995..037348baa 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -71,6 +71,7 @@ * See the README file for information on usage and redistribution. */ +#define PY_SSIZE_T_CLEAN #include "Python.h" #ifdef HAVE_LIBJPEG @@ -1564,11 +1565,12 @@ _putpalette(ImagingObject* self, PyObject* args) char* rawmode; UINT8* palette; - int palettesize; + Py_ssize_t palettesize; if (!PyArg_ParseTuple(args, "s"PY_ARG_BYTES_LENGTH, &rawmode, &palette, &palettesize)) return NULL; - if (strcmp(self->image->mode, "L") != 0 && strcmp(self->image->mode, "P")) { + if (strcmp(self->image->mode, "L") && strcmp(self->image->mode, "LA") && + strcmp(self->image->mode, "P") && strcmp(self->image->mode, "PA")) { PyErr_SetString(PyExc_ValueError, wrong_mode); return NULL; } @@ -1626,7 +1628,7 @@ _putpalettealphas(ImagingObject* self, PyObject* args) { int i; UINT8 *values; - int length; + Py_ssize_t length; if (!PyArg_ParseTuple(args, PY_ARG_BYTES_LENGTH, &values, &length)) return NULL; @@ -1770,7 +1772,7 @@ im_setmode(ImagingObject* self, PyObject* args) Imaging im; char* mode; - int modelen; + Py_ssize_t modelen; if (!PyArg_ParseTuple(args, "s#:setmode", &mode, &modelen)) return NULL; @@ -2066,8 +2068,8 @@ _getprojection(ImagingObject* self, PyObject* args) ImagingGetProjection(self->image, (unsigned char *)xprofile, (unsigned char *)yprofile); result = Py_BuildValue(PY_ARG_BYTES_LENGTH PY_ARG_BYTES_LENGTH, - xprofile, self->image->xsize, - yprofile, self->image->ysize); + xprofile, (Py_ssize_t)self->image->xsize, + yprofile, (Py_ssize_t)self->image->ysize); free(xprofile); free(yprofile); @@ -2342,7 +2344,7 @@ _font_new(PyObject* self_, PyObject* args) ImagingObject* imagep; unsigned char* glyphdata; - int glyphdata_length; + Py_ssize_t glyphdata_length; if (!PyArg_ParseTuple(args, "O!"PY_ARG_BYTES_LENGTH, &Imaging_Type, &imagep, &glyphdata, &glyphdata_length)) diff --git a/src/_imagingcms.c b/src/_imagingcms.c index 5e4196cb7..2c9f3aa68 100644 --- a/src/_imagingcms.c +++ b/src/_imagingcms.c @@ -25,6 +25,7 @@ kevin@cazabon.com\n\ http://www.cazabon.com\n\ " +#define PY_SSIZE_T_CLEAN #include "Python.h" // Include before wchar.h so _GNU_SOURCE is set #include "wchar.h" #include "datetime.h" @@ -120,7 +121,7 @@ cms_profile_fromstring(PyObject* self, PyObject* args) cmsHPROFILE hProfile; char* pProfile; - int nProfile; + Py_ssize_t nProfile; #if PY_VERSION_HEX >= 0x03000000 if (!PyArg_ParseTuple(args, "y#:profile_frombytes", &pProfile, &nProfile)) return NULL; @@ -735,12 +736,12 @@ _xyz3_py(cmsCIEXYZ* XYZ) cmsXYZ2xyY(&xyY[2], &XYZ[2]); return Py_BuildValue("(((d,d,d),(d,d,d),(d,d,d)),((d,d,d),(d,d,d),(d,d,d)))", - XYZ[0].X, XYZ[0].Y, XYZ[0].Z, - XYZ[1].X, XYZ[1].Y, XYZ[1].Z, - XYZ[2].X, XYZ[2].Y, XYZ[2].Z, - xyY[0].x, xyY[0].y, xyY[0].Y, - xyY[1].x, xyY[1].y, xyY[1].Y, - xyY[2].x, xyY[2].y, xyY[2].Y); + XYZ[0].X, XYZ[0].Y, XYZ[0].Z, + XYZ[1].X, XYZ[1].Y, XYZ[1].Z, + XYZ[2].X, XYZ[2].Y, XYZ[2].Z, + xyY[0].x, xyY[0].y, xyY[0].Y, + xyY[1].x, xyY[1].y, xyY[1].Y, + xyY[2].x, xyY[2].y, xyY[2].Y); } static PyObject* @@ -783,9 +784,9 @@ _profile_read_ciexyy_triple(CmsProfileObject* self, cmsTagSignature info) /* Note: lcms does all the heavy lifting and error checking (nr of channels == 3). */ return Py_BuildValue("((d,d,d),(d,d,d),(d,d,d)),", - triple->Red.x, triple->Red.y, triple->Red.Y, - triple->Green.x, triple->Green.y, triple->Green.Y, - triple->Blue.x, triple->Blue.y, triple->Blue.Y); + triple->Red.x, triple->Red.y, triple->Red.Y, + triple->Green.x, triple->Green.y, triple->Green.Y, + triple->Blue.x, triple->Blue.y, triple->Blue.Y); } static PyObject* @@ -817,12 +818,12 @@ _profile_read_named_color_list(CmsProfileObject* self, cmsTagSignature info) for (i = 0; i < n; i++) { PyObject* str; cmsNamedColorInfo(ncl, i, name, NULL, NULL, NULL, NULL); - str = PyUnicode_FromString(name); - if (str == NULL) { - Py_DECREF(result); - Py_INCREF(Py_None); - return Py_None; - } + str = PyUnicode_FromString(name); + if (str == NULL) { + Py_DECREF(result); + Py_INCREF(Py_None); + return Py_None; + } PyList_SET_ITEM(result, i, str); } @@ -844,9 +845,9 @@ static cmsBool _calculate_rgb_primaries(CmsProfileObject* self, cmsCIEXYZTRIPLE* // transform from our profile to XYZ using doubles for highest precision hTransform = cmsCreateTransform(self->profile, TYPE_RGB_DBL, - hXYZ, TYPE_XYZ_DBL, - INTENT_RELATIVE_COLORIMETRIC, - cmsFLAGS_NOCACHE | cmsFLAGS_NOOPTIMIZE); + hXYZ, TYPE_XYZ_DBL, + INTENT_RELATIVE_COLORIMETRIC, + cmsFLAGS_NOCACHE | cmsFLAGS_NOOPTIMIZE); cmsCloseProfile(hXYZ); if (hTransform == NULL) return 0; @@ -885,31 +886,31 @@ _is_intent_supported(CmsProfileObject* self, int clut) n = cmsGetSupportedIntents(INTENTS, - intent_ids, - intent_descs); + intent_ids, + intent_descs); for (i = 0; i < n; i++) { int intent = (int) intent_ids[i]; PyObject* id; - PyObject* entry; + PyObject* entry; - /* Only valid for ICC Intents (otherwise we read invalid memory in lcms cmsio1.c). */ - if (!(intent == INTENT_PERCEPTUAL || intent == INTENT_RELATIVE_COLORIMETRIC - || intent == INTENT_SATURATION || intent == INTENT_ABSOLUTE_COLORIMETRIC)) - continue; + /* Only valid for ICC Intents (otherwise we read invalid memory in lcms cmsio1.c). */ + if (!(intent == INTENT_PERCEPTUAL || intent == INTENT_RELATIVE_COLORIMETRIC + || intent == INTENT_SATURATION || intent == INTENT_ABSOLUTE_COLORIMETRIC)) + continue; - id = PyInt_FromLong((long) intent); - entry = Py_BuildValue("(OOO)", - _check_intent(clut, self->profile, intent, LCMS_USED_AS_INPUT) ? Py_True : Py_False, - _check_intent(clut, self->profile, intent, LCMS_USED_AS_OUTPUT) ? Py_True : Py_False, - _check_intent(clut, self->profile, intent, LCMS_USED_AS_PROOF) ? Py_True : Py_False); - if (id == NULL || entry == NULL) { - Py_XDECREF(id); - Py_XDECREF(entry); - Py_XDECREF(result); - Py_INCREF(Py_None); - return Py_None; - } - PyDict_SetItem(result, id, entry); + id = PyInt_FromLong((long) intent); + entry = Py_BuildValue("(OOO)", + _check_intent(clut, self->profile, intent, LCMS_USED_AS_INPUT) ? Py_True : Py_False, + _check_intent(clut, self->profile, intent, LCMS_USED_AS_OUTPUT) ? Py_True : Py_False, + _check_intent(clut, self->profile, intent, LCMS_USED_AS_PROOF) ? Py_True : Py_False); + if (id == NULL || entry == NULL) { + Py_XDECREF(id); + Py_XDECREF(entry); + Py_XDECREF(result); + Py_INCREF(Py_None); + return Py_None; + } + PyDict_SetItem(result, id, entry); } return result; } @@ -966,6 +967,8 @@ _profile_getattr(CmsProfileObject* self, cmsInfoType field) static PyObject* cms_profile_getattr_product_desc(CmsProfileObject* self, void* closure) { + PyErr_WarnEx(PyExc_DeprecationWarning, + "product_desc is deprecated. Use Unicode profile_description instead.", 1); // description was Description != 'Copyright' || or "%s - %s" (manufacturer, model) in 1.x return _profile_getattr(self, cmsInfoDescription); } @@ -975,24 +978,32 @@ cms_profile_getattr_product_desc(CmsProfileObject* self, void* closure) static PyObject* cms_profile_getattr_product_description(CmsProfileObject* self, void* closure) { + PyErr_WarnEx(PyExc_DeprecationWarning, + "product_description is deprecated. Use Unicode profile_description instead.", 1); return _profile_getattr(self, cmsInfoDescription); } static PyObject* cms_profile_getattr_product_model(CmsProfileObject* self, void* closure) { + PyErr_WarnEx(PyExc_DeprecationWarning, + "product_model is deprecated. Use Unicode model instead.", 1); return _profile_getattr(self, cmsInfoModel); } static PyObject* cms_profile_getattr_product_manufacturer(CmsProfileObject* self, void* closure) { + PyErr_WarnEx(PyExc_DeprecationWarning, + "product_manufacturer is deprecated. Use Unicode manufacturer instead.", 1); return _profile_getattr(self, cmsInfoManufacturer); } static PyObject* cms_profile_getattr_product_copyright(CmsProfileObject* self, void* closure) { + PyErr_WarnEx(PyExc_DeprecationWarning, + "product_copyright is deprecated. Use Unicode copyright instead.", 1); return _profile_getattr(self, cmsInfoCopyright); } @@ -1005,12 +1016,16 @@ cms_profile_getattr_rendering_intent(CmsProfileObject* self, void* closure) static PyObject* cms_profile_getattr_pcs(CmsProfileObject* self, void* closure) { + PyErr_WarnEx(PyExc_DeprecationWarning, + "pcs is deprecated. Use padded connection_space instead.", 1); return PyUnicode_DecodeFSDefault(findICmode(cmsGetPCS(self->profile))); } static PyObject* cms_profile_getattr_color_space(CmsProfileObject* self, void* closure) { + PyErr_WarnEx(PyExc_DeprecationWarning, + "color_space is deprecated. Use padded xcolor_space instead.", 1); return PyUnicode_DecodeFSDefault(findICmode(cmsGetColorSpace(self->profile))); } @@ -1070,7 +1085,7 @@ cms_profile_getattr_creation_date(CmsProfileObject* self, void* closure) } return PyDateTime_FromDateAndTime(1900 + ct.tm_year, ct.tm_mon, ct.tm_mday, - ct.tm_hour, ct.tm_min, ct.tm_sec, 0); + ct.tm_hour, ct.tm_min, ct.tm_sec, 0); } static PyObject* @@ -1295,7 +1310,7 @@ cms_profile_getattr_green_primary(CmsProfileObject* self, void* closure) result = _calculate_rgb_primaries(self, &primaries); if (! result) { Py_INCREF(Py_None); - return Py_None; + return Py_None; } return _xyz_py(&primaries.Green); @@ -1311,7 +1326,7 @@ cms_profile_getattr_blue_primary(CmsProfileObject* self, void* closure) result = _calculate_rgb_primaries(self, &primaries); if (! result) { Py_INCREF(Py_None); - return Py_None; + return Py_None; } return _xyz_py(&primaries.Blue); @@ -1394,11 +1409,11 @@ cms_profile_getattr_icc_measurement_condition (CmsProfileObject* self, void* clo geo = "unknown"; return Py_BuildValue("{s:i,s:(ddd),s:s,s:d,s:s}", - "observer", mc->Observer, - "backing", mc->Backing.X, mc->Backing.Y, mc->Backing.Z, - "geo", geo, - "flare", mc->Flare, - "illuminant_type", _illu_map(mc->IlluminantType)); + "observer", mc->Observer, + "backing", mc->Backing.X, mc->Backing.Y, mc->Backing.Z, + "geo", geo, + "flare", mc->Flare, + "illuminant_type", _illu_map(mc->IlluminantType)); } static PyObject* @@ -1419,9 +1434,9 @@ cms_profile_getattr_icc_viewing_condition (CmsProfileObject* self, void* closure } return Py_BuildValue("{s:(ddd),s:(ddd),s:s}", - "illuminant", vc->IlluminantXYZ.X, vc->IlluminantXYZ.Y, vc->IlluminantXYZ.Z, - "surround", vc->SurroundXYZ.X, vc->SurroundXYZ.Y, vc->SurroundXYZ.Z, - "illuminant_type", _illu_map(vc->IlluminantType)); + "illuminant", vc->IlluminantXYZ.X, vc->IlluminantXYZ.Y, vc->IlluminantXYZ.Z, + "surround", vc->SurroundXYZ.X, vc->SurroundXYZ.Y, vc->SurroundXYZ.Z, + "illuminant_type", _illu_map(vc->IlluminantType)); } diff --git a/src/_imagingft.c b/src/_imagingft.c index f94e55803..6b65e2c33 100644 --- a/src/_imagingft.c +++ b/src/_imagingft.c @@ -18,6 +18,7 @@ * Copyright (c) 1998-2007 by Secret Labs AB */ +#define PY_SSIZE_T_CLEAN #include "Python.h" #include "Imaging.h" @@ -87,6 +88,10 @@ typedef bool (*t_raqm_set_text_utf8) (raqm_t *rq, size_t len); typedef bool (*t_raqm_set_par_direction) (raqm_t *rq, raqm_direction_t dir); +typedef bool (*t_raqm_set_language) (raqm_t *rq, + const char *lang, + size_t start, + size_t len); typedef bool (*t_raqm_add_font_feature) (raqm_t *rq, const char *feature, int len); @@ -106,6 +111,7 @@ typedef struct { t_raqm_set_text set_text; t_raqm_set_text_utf8 set_text_utf8; t_raqm_set_par_direction set_par_direction; + t_raqm_set_language set_language; t_raqm_add_font_feature add_font_feature; t_raqm_set_freetype_face set_freetype_face; t_raqm_layout layout; @@ -160,6 +166,7 @@ setraqm(void) p_raqm.set_text = (t_raqm_set_text)dlsym(p_raqm.raqm, "raqm_set_text"); p_raqm.set_text_utf8 = (t_raqm_set_text_utf8)dlsym(p_raqm.raqm, "raqm_set_text_utf8"); p_raqm.set_par_direction = (t_raqm_set_par_direction)dlsym(p_raqm.raqm, "raqm_set_par_direction"); + p_raqm.set_language = (t_raqm_set_language)dlsym(p_raqm.raqm, "raqm_set_language"); p_raqm.add_font_feature = (t_raqm_add_font_feature)dlsym(p_raqm.raqm, "raqm_add_font_feature"); p_raqm.set_freetype_face = (t_raqm_set_freetype_face)dlsym(p_raqm.raqm, "raqm_set_freetype_face"); p_raqm.layout = (t_raqm_layout)dlsym(p_raqm.raqm, "raqm_layout"); @@ -176,6 +183,7 @@ setraqm(void) p_raqm.set_text && p_raqm.set_text_utf8 && p_raqm.set_par_direction && + p_raqm.set_language && p_raqm.add_font_feature && p_raqm.set_freetype_face && p_raqm.layout && @@ -190,6 +198,7 @@ setraqm(void) p_raqm.set_text = (t_raqm_set_text)GetProcAddress(p_raqm.raqm, "raqm_set_text"); p_raqm.set_text_utf8 = (t_raqm_set_text_utf8)GetProcAddress(p_raqm.raqm, "raqm_set_text_utf8"); p_raqm.set_par_direction = (t_raqm_set_par_direction)GetProcAddress(p_raqm.raqm, "raqm_set_par_direction"); + p_raqm.set_language = (t_raqm_set_language)GetProcAddress(p_raqm.raqm, "raqm_set_language"); p_raqm.add_font_feature = (t_raqm_add_font_feature)GetProcAddress(p_raqm.raqm, "raqm_add_font_feature"); p_raqm.set_freetype_face = (t_raqm_set_freetype_face)GetProcAddress(p_raqm.raqm, "raqm_set_freetype_face"); p_raqm.layout = (t_raqm_layout)GetProcAddress(p_raqm.raqm, "raqm_layout"); @@ -205,6 +214,7 @@ setraqm(void) p_raqm.set_text && p_raqm.set_text_utf8 && p_raqm.set_par_direction && + p_raqm.set_language && p_raqm.add_font_feature && p_raqm.set_freetype_face && p_raqm.layout && @@ -228,12 +238,12 @@ getfont(PyObject* self_, PyObject* args, PyObject* kw) int error = 0; char* filename = NULL; - int size; - int index = 0; - int layout_engine = 0; + Py_ssize_t size; + Py_ssize_t index = 0; + Py_ssize_t layout_engine = 0; unsigned char* encoding; unsigned char* font_bytes; - int font_bytes_size = 0; + Py_ssize_t font_bytes_size = 0; static char* kwlist[] = { "filename", "size", "index", "encoding", "font_bytes", "layout_engine", NULL @@ -332,8 +342,8 @@ font_getchar(PyObject* string, int index, FT_ULong* char_out) } static size_t -text_layout_raqm(PyObject* string, FontObject* self, const char* dir, - PyObject *features ,GlyphInfo **glyph_info, int mask) +text_layout_raqm(PyObject* string, FontObject* self, const char* dir, PyObject *features, + const char* lang, GlyphInfo **glyph_info, int mask) { int i = 0; raqm_t *rq; @@ -341,6 +351,7 @@ text_layout_raqm(PyObject* string, FontObject* self, const char* dir, raqm_glyph_t *glyphs = NULL; raqm_glyph_t_01 *glyphs_01 = NULL; raqm_direction_t direction; + size_t start = 0; rq = (*p_raqm.create)(); if (rq == NULL) { @@ -360,6 +371,13 @@ text_layout_raqm(PyObject* string, FontObject* self, const char* dir, PyErr_SetString(PyExc_ValueError, "raqm_set_text() failed"); goto failed; } + if (lang) { + if (!(*p_raqm.set_language)(rq, lang, start, size)) { + PyErr_SetString(PyExc_ValueError, "raqm_set_language() failed"); + goto failed; + } + } + } #if PY_VERSION_HEX < 0x03000000 else if (PyString_Check(string)) { @@ -372,6 +390,12 @@ text_layout_raqm(PyObject* string, FontObject* self, const char* dir, PyErr_SetString(PyExc_ValueError, "raqm_set_text_utf8() failed"); goto failed; } + if (lang) { + if (!(*p_raqm.set_language)(rq, lang, start, size)) { + PyErr_SetString(PyExc_ValueError, "raqm_set_language() failed"); + goto failed; + } + } } #endif else { @@ -498,8 +522,8 @@ failed: } static size_t -text_layout_fallback(PyObject* string, FontObject* self, const char* dir, - PyObject *features ,GlyphInfo **glyph_info, int mask) +text_layout_fallback(PyObject* string, FontObject* self, const char* dir, PyObject *features, + const char* lang, GlyphInfo **glyph_info, int mask) { int error, load_flags; FT_ULong ch; @@ -509,8 +533,8 @@ text_layout_fallback(PyObject* string, FontObject* self, const char* dir, FT_UInt last_index = 0; int i; - if (features != Py_None || dir != NULL) { - PyErr_SetString(PyExc_KeyError, "setting text direction or font features is not supported without libraqm"); + if (features != Py_None || dir != NULL || lang != NULL) { + PyErr_SetString(PyExc_KeyError, "setting text direction, language or font features is not supported without libraqm"); } #if PY_VERSION_HEX >= 0x03000000 if (!PyUnicode_Check(string)) { @@ -564,15 +588,15 @@ text_layout_fallback(PyObject* string, FontObject* self, const char* dir, } static size_t -text_layout(PyObject* string, FontObject* self, const char* dir, - PyObject *features, GlyphInfo **glyph_info, int mask) +text_layout(PyObject* string, FontObject* self, const char* dir, PyObject *features, + const char* lang, GlyphInfo **glyph_info, int mask) { size_t count; if (p_raqm.raqm && self->layout_engine == LAYOUT_RAQM) { - count = text_layout_raqm(string, self, dir, features, glyph_info, mask); + count = text_layout_raqm(string, self, dir, features, lang, glyph_info, mask); } else { - count = text_layout_fallback(string, self, dir, features, glyph_info, mask); + count = text_layout_fallback(string, self, dir, features, lang, glyph_info, mask); } return count; } @@ -584,6 +608,7 @@ font_getsize(FontObject* self, PyObject* args) FT_Face face; int xoffset, yoffset; const char *dir = NULL; + const char *lang = NULL; size_t count; GlyphInfo *glyph_info = NULL; PyObject *features = Py_None; @@ -591,14 +616,14 @@ font_getsize(FontObject* self, PyObject* args) /* calculate size and bearing for a given string */ PyObject* string; - if (!PyArg_ParseTuple(args, "O|zO:getsize", &string, &dir, &features)) + if (!PyArg_ParseTuple(args, "O|zOz:getsize", &string, &dir, &features, &lang)) return NULL; face = NULL; xoffset = yoffset = 0; y_max = y_min = 0; - count = text_layout(string, self, dir, features, &glyph_info, 0); + count = text_layout(string, self, dir, features, lang, &glyph_info, 0); if (PyErr_Occurred()) { return NULL; } @@ -691,16 +716,17 @@ font_render(FontObject* self, PyObject* args) int temp; int xx, x0, x1; const char *dir = NULL; + const char *lang = NULL; size_t count; GlyphInfo *glyph_info; PyObject *features = NULL; - if (!PyArg_ParseTuple(args, "On|izO:render", &string, &id, &mask, &dir, &features)) { + if (!PyArg_ParseTuple(args, "On|izOz:render", &string, &id, &mask, &dir, &features, &lang)) { return NULL; } glyph_info = NULL; - count = text_layout(string, self, dir, features, &glyph_info, mask); + count = text_layout(string, self, dir, features, lang, &glyph_info, mask); if (PyErr_Occurred()) { return NULL; } diff --git a/src/decode.c b/src/decode.c index be1b503e6..562bc7436 100644 --- a/src/decode.c +++ b/src/decode.c @@ -29,6 +29,7 @@ /* FIXME: make these pluggable! */ +#define PY_SSIZE_T_CLEAN #include "Python.h" #include "Imaging.h" @@ -117,7 +118,8 @@ static PyObject* _decode(ImagingDecoderObject* decoder, PyObject* args) { UINT8* buffer; - int bufsize, status; + Py_ssize_t bufsize; + int status; ImagingSectionCookie cookie; if (!PyArg_ParseTuple(args, PY_ARG_BYTES_LENGTH, &buffer, &bufsize)) diff --git a/src/encode.c b/src/encode.c index 13da1b999..6832f90c8 100644 --- a/src/encode.c +++ b/src/encode.c @@ -22,6 +22,7 @@ /* FIXME: make these pluggable! */ +#define PY_SSIZE_T_CLEAN #include "Python.h" #include "Imaging.h" @@ -123,7 +124,7 @@ _encode(ImagingEncoderObject* encoder, PyObject* args) /* Encode to a Python string (allocated by this method) */ - int bufsize = 16384; + Py_ssize_t bufsize = 16384; if (!PyArg_ParseTuple(args, "|i", &bufsize)) return NULL; @@ -176,8 +177,8 @@ _encode_to_file(ImagingEncoderObject* encoder, PyObject* args) /* Encode to a file handle */ - int fh; - int bufsize = 16384; + Py_ssize_t fh; + Py_ssize_t bufsize = 16384; if (!PyArg_ParseTuple(args, "i|i", &fh, &bufsize)) return NULL; @@ -221,7 +222,7 @@ _setimage(ImagingEncoderObject* encoder, PyObject* args) PyObject* op; Imaging im; ImagingCodecState state; - int x0, y0, x1, y1; + Py_ssize_t x0, y0, x1, y1; /* Define where image data should be stored */ @@ -406,8 +407,8 @@ PyImaging_GifEncoderNew(PyObject* self, PyObject* args) char *mode; char *rawmode; - int bits = 8; - int interlace = 0; + Py_ssize_t bits = 8; + Py_ssize_t interlace = 0; if (!PyArg_ParseTuple(args, "ss|ii", &mode, &rawmode, &bits, &interlace)) return NULL; @@ -438,7 +439,7 @@ PyImaging_PcxEncoderNew(PyObject* self, PyObject* args) char *mode; char *rawmode; - int bits = 8; + Py_ssize_t bits = 8; if (!PyArg_ParseTuple(args, "ss|ii", &mode, &rawmode, &bits)) { return NULL; @@ -470,8 +471,8 @@ PyImaging_RawEncoderNew(PyObject* self, PyObject* args) char *mode; char *rawmode; - int stride = 0; - int ystep = 1; + Py_ssize_t stride = 0; + Py_ssize_t ystep = 1; if (!PyArg_ParseTuple(args, "ss|ii", &mode, &rawmode, &stride, &ystep)) return NULL; @@ -503,7 +504,7 @@ PyImaging_TgaRleEncoderNew(PyObject* self, PyObject* args) char *mode; char *rawmode; - int ystep = 1; + Py_ssize_t ystep = 1; if (!PyArg_ParseTuple(args, "ss|i", &mode, &rawmode, &ystep)) return NULL; @@ -561,11 +562,11 @@ PyImaging_ZipEncoderNew(PyObject* self, PyObject* args) char* mode; char* rawmode; - int optimize = 0; - int compress_level = -1; - int compress_type = -1; + Py_ssize_t optimize = 0; + Py_ssize_t compress_level = -1; + Py_ssize_t compress_type = -1; char* dictionary = NULL; - int dictionary_size = 0; + Py_ssize_t dictionary_size = 0; if (!PyArg_ParseTuple(args, "ss|iii"PY_ARG_BYTES_LENGTH, &mode, &rawmode, &optimize, &compress_level, &compress_type, @@ -701,20 +702,20 @@ PyImaging_JpegEncoderNew(PyObject* self, PyObject* args) char *mode; char *rawmode; - int quality = 0; - int progressive = 0; - int smooth = 0; - int optimize = 0; - int streamtype = 0; /* 0=interchange, 1=tables only, 2=image only */ - int xdpi = 0, ydpi = 0; - int subsampling = -1; /* -1=default, 0=none, 1=medium, 2=high */ + Py_ssize_t quality = 0; + Py_ssize_t progressive = 0; + Py_ssize_t smooth = 0; + Py_ssize_t optimize = 0; + Py_ssize_t streamtype = 0; /* 0=interchange, 1=tables only, 2=image only */ + Py_ssize_t xdpi = 0, ydpi = 0; + Py_ssize_t subsampling = -1; /* -1=default, 0=none, 1=medium, 2=high */ PyObject* qtables=NULL; unsigned int *qarrays = NULL; int qtablesLen = 0; char* extra = NULL; - int extra_size; + Py_ssize_t extra_size; char* rawExif = NULL; - int rawExifLen = 0; + Py_ssize_t rawExifLen = 0; if (!PyArg_ParseTuple(args, "ss|iiiiiiiiO"PY_ARG_BYTES_LENGTH""PY_ARG_BYTES_LENGTH, &mode, &rawmode, &quality, @@ -805,7 +806,7 @@ PyImaging_LibTiffEncoderNew(PyObject* self, PyObject* args) char* rawmode; char* compname; char* filename; - int fp; + Py_ssize_t fp; PyObject *dir; PyObject *key, *value; @@ -985,14 +986,14 @@ PyImaging_Jpeg2KEncoderNew(PyObject *self, PyObject *args) PyObject *offset = NULL, *tile_offset = NULL, *tile_size = NULL; char *quality_mode = "rates"; PyObject *quality_layers = NULL; - int num_resolutions = 0; + Py_ssize_t num_resolutions = 0; PyObject *cblk_size = NULL, *precinct_size = NULL; PyObject *irreversible = NULL; char *progression = "LRCP"; OPJ_PROG_ORDER prog_order; char *cinema_mode = "no"; OPJ_CINEMA_MODE cine_mode; - int fd = -1; + Py_ssize_t fd = -1; if (!PyArg_ParseTuple(args, "ss|OOOsOIOOOssi", &mode, &format, &offset, &tile_offset, &tile_size, diff --git a/src/libImaging/Convert.c b/src/libImaging/Convert.c index 39ddf8721..a6fefca32 100644 --- a/src/libImaging/Convert.c +++ b/src/libImaging/Convert.c @@ -517,6 +517,18 @@ l2cmyk(UINT8* out, const UINT8* in, int xsize) } } +static void +la2cmyk(UINT8* out, const UINT8* in, int xsize) +{ + int x; + for (x = 0; x < xsize; x++, in += 4) { + *out++ = 0; + *out++ = 0; + *out++ = 0; + *out++ = ~(in[0]); + } +} + static void rgb2cmyk(UINT8* out, const UINT8* in, int xsize) { @@ -592,6 +604,22 @@ i2f(UINT8* out_, const UINT8* in_, int xsize) *out++ = (FLOAT32) *in++; } +static void +i2rgb(UINT8* out, const UINT8* in_, int xsize) +{ + int x; + INT32* in = (INT32*) in_; + for (x = 0; x < xsize; x++, in++, out+=4) { + if (*in <= 0) + out[0] = out[1] = out[2] = 0; + else if (*in >= 255) + out[0] = out[1] = out[2] = 255; + else + out[0] = out[1] = out[2] = (UINT8) *in; + out[3] = 255; + } +} + /* ------------- */ /* F conversions */ /* ------------- */ @@ -657,6 +685,18 @@ l2ycbcr(UINT8* out, const UINT8* in, int xsize) } } +static void +la2ycbcr(UINT8* out, const UINT8* in, int xsize) +{ + int x; + for (x = 0; x < xsize; x++, in += 4) { + *out++ = in[0]; + *out++ = 128; + *out++ = 128; + *out++ = 255; + } +} + static void ycbcr2l(UINT8* out, const UINT8* in, int xsize) { @@ -665,6 +705,16 @@ ycbcr2l(UINT8* out, const UINT8* in, int xsize) *out++ = in[0]; } +static void +ycbcr2la(UINT8* out, const UINT8* in, int xsize) +{ + int x; + for (x = 0; x < xsize; x++, in += 4, out += 4) { + out[0] = out[1] = out[2] = in[0]; + out[3] = 255; + } +} + /* ------------------------- */ /* I;16 (16-bit) conversions */ /* ------------------------- */ @@ -802,16 +852,21 @@ static struct { { "LA", "L", la2l }, { "LA", "La", lA2la }, { "LA", "RGB", la2rgb }, - { "LA", "RGBX", la2rgb }, { "LA", "RGBA", la2rgb }, + { "LA", "RGBX", la2rgb }, + { "LA", "CMYK", la2cmyk }, + { "LA", "YCbCr", la2ycbcr }, { "La", "LA", la2lA }, - { "I", "L", i2l }, - { "I", "F", i2f }, + { "I", "L", i2l }, + { "I", "F", i2f }, + { "I", "RGB", i2rgb }, + { "I", "RGBA", i2rgb }, + { "I", "RGBX", i2rgb }, - { "F", "L", f2l }, - { "F", "I", f2i }, + { "F", "L", f2l }, + { "F", "I", f2i }, { "RGB", "1", rgb2bit }, { "RGB", "L", rgb2l }, @@ -842,8 +897,9 @@ static struct { { "RGBX", "1", rgb2bit }, { "RGBX", "L", rgb2l }, - { "RGBA", "I", rgb2i }, - { "RGBA", "F", rgb2f }, + { "RGBX", "LA", rgb2la }, + { "RGBX", "I", rgb2i }, + { "RGBX", "F", rgb2f }, { "RGBX", "RGB", rgba2rgb }, { "RGBX", "CMYK", rgb2cmyk }, { "RGBX", "YCbCr", ImagingConvertRGB2YCbCr }, @@ -853,6 +909,7 @@ static struct { { "CMYK", "RGBX", cmyk2rgb }, { "YCbCr", "L", ycbcr2l }, + { "YCbCr", "LA", ycbcr2la }, { "YCbCr", "RGB", ImagingConvertYCbCr2RGB }, { "HSV", "RGB", hsv2rgb }, @@ -894,6 +951,15 @@ p2bit(UINT8* out, const UINT8* in, int xsize, const UINT8* palette) *out++ = (L(&palette[in[x]*4]) >= 128000) ? 255 : 0; } +static void +pa2bit(UINT8* out, const UINT8* in, int xsize, const UINT8* palette) +{ + int x; + /* FIXME: precalculate greyscale palette? */ + for (x = 0; x < xsize; x++, in += 4) + *out++ = (L(&palette[in[0]*4]) >= 128000) ? 255 : 0; +} + static void p2l(UINT8* out, const UINT8* in, int xsize, const UINT8* palette) { @@ -903,6 +969,28 @@ p2l(UINT8* out, const UINT8* in, int xsize, const UINT8* palette) *out++ = L(&palette[in[x]*4]) / 1000; } +static void +pa2l(UINT8* out, const UINT8* in, int xsize, const UINT8* palette) +{ + int x; + /* FIXME: precalculate greyscale palette? */ + for (x = 0; x < xsize; x++, in += 4) + *out++ = L(&palette[in[0]*4]) / 1000; +} + +static void +p2pa(UINT8* out, const UINT8* in, int xsize, const UINT8* palette) +{ + int x; + for (x = 0; x < xsize; x++, in++) { + const UINT8* rgba = &palette[in[0]]; + *out++ = in[0]; + *out++ = in[0]; + *out++ = in[0]; + *out++ = rgba[3]; + } +} + static void p2la(UINT8* out, const UINT8* in, int xsize, const UINT8* palette) { @@ -920,9 +1008,9 @@ pa2la(UINT8* out, const UINT8* in, int xsize, const UINT8* palette) { int x; /* FIXME: precalculate greyscale palette? */ - for (x = 0; x < xsize; x++, in += 2) { - *out++ = L(&palette[in[0]*4]) / 1000; - *out++ = in[1]; + for (x = 0; x < xsize; x++, in += 4, out += 4) { + out[0] = out[1] = out[2] = L(&palette[in[0]*4]) / 1000; + out[3] = in[3]; } } @@ -935,6 +1023,15 @@ p2i(UINT8* out_, const UINT8* in, int xsize, const UINT8* palette) *out++ = L(&palette[in[x]*4]) / 1000; } +static void +pa2i(UINT8* out_, const UINT8* in, int xsize, const UINT8* palette) +{ + int x; + INT32* out = (INT32*) out_; + for (x = 0; x < xsize; x++, in += 4) + *out++ = L(&palette[in[0]*4]) / 1000; +} + static void p2f(UINT8* out_, const UINT8* in, int xsize, const UINT8* palette) { @@ -944,6 +1041,15 @@ p2f(UINT8* out_, const UINT8* in, int xsize, const UINT8* palette) *out++ = (float) L(&palette[in[x]*4]) / 1000.0F; } +static void +pa2f(UINT8* out_, const UINT8* in, int xsize, const UINT8* palette) +{ + int x; + FLOAT32* out = (FLOAT32*) out_; + for (x = 0; x < xsize; x++, in += 4) + *out++ = (float) L(&palette[in[0]*4]) / 1000.0F; +} + static void p2rgb(UINT8* out, const UINT8* in, int xsize, const UINT8* palette) { @@ -957,6 +1063,19 @@ p2rgb(UINT8* out, const UINT8* in, int xsize, const UINT8* palette) } } +static void +pa2rgb(UINT8* out, const UINT8* in, int xsize, const UINT8* palette) +{ + int x; + for (x = 0; x < xsize; x++, in += 4) { + const UINT8* rgb = &palette[in[0] * 4]; + *out++ = rgb[0]; + *out++ = rgb[1]; + *out++ = rgb[2]; + *out++ = 255; + } +} + static void p2rgba(UINT8* out, const UINT8* in, int xsize, const UINT8* palette) { @@ -990,6 +1109,13 @@ p2cmyk(UINT8* out, const UINT8* in, int xsize, const UINT8* palette) rgb2cmyk(out, out, xsize); } +static void +pa2cmyk(UINT8* out, const UINT8* in, int xsize, const UINT8* palette) +{ + pa2rgb(out, in, xsize, palette); + rgb2cmyk(out, out, xsize); +} + static void p2ycbcr(UINT8* out, const UINT8* in, int xsize, const UINT8* palette) { @@ -997,6 +1123,13 @@ p2ycbcr(UINT8* out, const UINT8* in, int xsize, const UINT8* palette) ImagingConvertRGB2YCbCr(out, out, xsize); } +static void +pa2ycbcr(UINT8* out, const UINT8* in, int xsize, const UINT8* palette) +{ + pa2rgb(out, in, xsize, palette); + ImagingConvertRGB2YCbCr(out, out, xsize); +} + static Imaging frompalette(Imaging imOut, Imaging imIn, const char *mode) { @@ -1013,25 +1146,27 @@ frompalette(Imaging imOut, Imaging imIn, const char *mode) alpha = !strcmp(imIn->mode, "PA"); if (strcmp(mode, "1") == 0) - convert = p2bit; + convert = alpha ? pa2bit : p2bit; else if (strcmp(mode, "L") == 0) - convert = p2l; + convert = alpha ? pa2l : p2l; else if (strcmp(mode, "LA") == 0) - convert = (alpha) ? pa2la : p2la; + convert = alpha ? pa2la : p2la; + else if (strcmp(mode, "PA") == 0) + convert = p2pa; else if (strcmp(mode, "I") == 0) - convert = p2i; + convert = alpha ? pa2i : p2i; else if (strcmp(mode, "F") == 0) - convert = p2f; + convert = alpha ? pa2f : p2f; else if (strcmp(mode, "RGB") == 0) - convert = p2rgb; + convert = alpha ? pa2rgb : p2rgb; else if (strcmp(mode, "RGBA") == 0) - convert = (alpha) ? pa2rgba : p2rgba; + convert = alpha ? pa2rgba : p2rgba; else if (strcmp(mode, "RGBX") == 0) - convert = p2rgba; + convert = alpha ? pa2rgba : p2rgba; else if (strcmp(mode, "CMYK") == 0) - convert = p2cmyk; + convert = alpha ? pa2cmyk : p2cmyk; else if (strcmp(mode, "YCbCr") == 0) - convert = p2ycbcr; + convert = alpha ? pa2ycbcr : p2ycbcr; else return (Imaging) ImagingError_ValueError("conversion not supported"); @@ -1052,9 +1187,10 @@ frompalette(Imaging imOut, Imaging imIn, const char *mode) #pragma optimize("", off) #endif static Imaging -topalette(Imaging imOut, Imaging imIn, ImagingPalette inpalette, int dither) +topalette(Imaging imOut, Imaging imIn, const char *mode, ImagingPalette inpalette, int dither) { ImagingSectionCookie cookie; + int alpha; int x, y; ImagingPalette palette = inpalette;; @@ -1062,6 +1198,8 @@ topalette(Imaging imOut, Imaging imIn, ImagingPalette inpalette, int dither) if (strcmp(imIn->mode, "L") != 0 && strncmp(imIn->mode, "RGB", 3) != 0) return (Imaging) ImagingError_ValueError("conversion not supported"); + alpha = !strcmp(mode, "PA"); + if (palette == NULL) { /* FIXME: make user configurable */ if (imIn->bands == 1) @@ -1073,7 +1211,7 @@ topalette(Imaging imOut, Imaging imIn, ImagingPalette inpalette, int dither) if (!palette) return (Imaging) ImagingError_ValueError("no palette"); - imOut = ImagingNew2Dirty("P", imOut, imIn); + imOut = ImagingNew2Dirty(mode, imOut, imIn); if (!imOut) { if (palette != inpalette) ImagingPaletteDelete(palette); @@ -1088,8 +1226,13 @@ topalette(Imaging imOut, Imaging imIn, ImagingPalette inpalette, int dither) /* Greyscale palette: copy data as is */ ImagingSectionEnter(&cookie); - for (y = 0; y < imIn->ysize; y++) - memcpy(imOut->image[y], imIn->image[y], imIn->linesize); + for (y = 0; y < imIn->ysize; y++) { + if (alpha) { + l2la((UINT8*) imOut->image[y], (UINT8*) imIn->image[y], imIn->xsize); + } else { + memcpy(imOut->image[y], imIn->image[y], imIn->linesize); + } + } ImagingSectionLeave(&cookie); } else { @@ -1120,7 +1263,7 @@ topalette(Imaging imOut, Imaging imIn, ImagingPalette inpalette, int dither) int g, g0, g1, g2; int b, b0, b1, b2; UINT8* in = (UINT8*) imIn->image[y]; - UINT8* out = imOut->image8[y]; + UINT8* out = alpha ? (UINT8*) imOut->image32[y] : imOut->image8[y]; int* e = errors; r = r0 = r1 = 0; @@ -1139,7 +1282,12 @@ topalette(Imaging imOut, Imaging imIn, ImagingPalette inpalette, int dither) cache = &ImagingPaletteCache(palette, r, g, b); if (cache[0] == 0x100) ImagingPaletteCacheUpdate(palette, r, g, b); - out[x] = (UINT8) cache[0]; + if (alpha) { + out[x*4] = out[x*4+1] = out[x*4+2] = (UINT8) cache[0]; + out[x*4+3] = 255; + } else { + out[x] = (UINT8) cache[0]; + } r -= (int) palette->palette[cache[0]*4]; g -= (int) palette->palette[cache[0]*4+1]; @@ -1172,7 +1320,7 @@ topalette(Imaging imOut, Imaging imIn, ImagingPalette inpalette, int dither) for (y = 0; y < imIn->ysize; y++) { int r, g, b; UINT8* in = (UINT8*) imIn->image[y]; - UINT8* out = imOut->image8[y]; + UINT8* out = alpha ? (UINT8*) imOut->image32[y] : imOut->image8[y]; for (x = 0; x < imIn->xsize; x++, in += 4) { INT16* cache; @@ -1183,8 +1331,12 @@ topalette(Imaging imOut, Imaging imIn, ImagingPalette inpalette, int dither) cache = &ImagingPaletteCache(palette, r, g, b); if (cache[0] == 0x100) ImagingPaletteCacheUpdate(palette, r, g, b); - out[x] = (UINT8) cache[0]; - + if (alpha) { + out[x*4] = out[x*4+1] = out[x*4+2] = (UINT8) cache[0]; + out[x*4+3] = 255; + } else { + out[x] = (UINT8) cache[0]; + } } } ImagingSectionLeave(&cookie); @@ -1314,8 +1466,8 @@ convert(Imaging imOut, Imaging imIn, const char *mode, if (strcmp(imIn->mode, "P") == 0 || strcmp(imIn->mode, "PA") == 0) return frompalette(imOut, imIn, mode); - if (strcmp(mode, "P") == 0) - return topalette(imOut, imIn, palette, dither); + if (strcmp(mode, "P") == 0 || strcmp(mode, "PA") == 0) + return topalette(imOut, imIn, mode, palette, dither); if (dither && strcmp(mode, "1") == 0) return tobilevel(imOut, imIn, dither); @@ -1385,6 +1537,8 @@ ImagingConvertTransparent(Imaging imIn, const char *mode, } if (!((strcmp(imIn->mode, "RGB") == 0 || + strcmp(imIn->mode, "1") == 0 || + strcmp(imIn->mode, "I") == 0 || strcmp(imIn->mode, "L") == 0) && strcmp(mode, "RGBA") == 0)) #ifdef notdef @@ -1403,7 +1557,13 @@ ImagingConvertTransparent(Imaging imIn, const char *mode, if (strcmp(imIn->mode, "RGB") == 0) { convert = rgb2rgba; } else { - convert = l2rgb; + if (strcmp(imIn->mode, "1") == 0) { + convert = bit2rgb; + } else if (strcmp(imIn->mode, "I") == 0) { + convert = i2rgb; + } else { + convert = l2rgb; + } g = b = r; } diff --git a/src/libImaging/Geometry.c b/src/libImaging/Geometry.c index 5358f6eeb..23236a971 100644 --- a/src/libImaging/Geometry.c +++ b/src/libImaging/Geometry.c @@ -29,8 +29,8 @@ ImagingFlipLeftRight(Imaging imOut, Imaging imIn) #define FLIP_LEFT_RIGHT(INT, image) \ for (y = 0; y < imIn->ysize; y++) { \ - INT* in = imIn->image[y]; \ - INT* out = imOut->image[y]; \ + INT* in = (INT *)imIn->image[y]; \ + INT* out = (INT *)imOut->image[y]; \ xr = imIn->xsize-1; \ for (x = 0; x < imIn->xsize; x++, xr--) \ out[xr] = in[x]; \ @@ -105,10 +105,11 @@ ImagingRotate90(Imaging imOut, Imaging imIn) yyysize = yy + ROTATE_SMALL_CHUNK < imIn->ysize ? yy + ROTATE_SMALL_CHUNK : imIn->ysize; \ xxxsize = xx + ROTATE_SMALL_CHUNK < imIn->xsize ? xx + ROTATE_SMALL_CHUNK : imIn->xsize; \ for (yyy = yy; yyy < yyysize; yyy++) { \ - INT* in = imIn->image[yyy]; \ + INT* in = (INT *)imIn->image[yyy]; \ xr = imIn->xsize - 1 - xx; \ for (xxx = xx; xxx < xxxsize; xxx++, xr--) { \ - imOut->image[xr][yyy] = in[xxx]; \ + INT* out = (INT *)imOut->image[xr]; \ + out[yyy] = in[xxx]; \ } \ } \ } \ @@ -118,10 +119,15 @@ ImagingRotate90(Imaging imOut, Imaging imIn) ImagingSectionEnter(&cookie); - if (imIn->image8) - ROTATE_90(UINT8, image8) - else - ROTATE_90(INT32, image32) + if (imIn->image8) { + if (strncmp(imIn->mode, "I;16", 4) == 0) { + ROTATE_90(UINT16, image8); + } else { + ROTATE_90(UINT8, image8); + } + } else { + ROTATE_90(INT32, image32); + } ImagingSectionLeave(&cookie); @@ -155,9 +161,10 @@ ImagingTranspose(Imaging imOut, Imaging imIn) yyysize = yy + ROTATE_SMALL_CHUNK < imIn->ysize ? yy + ROTATE_SMALL_CHUNK : imIn->ysize; \ xxxsize = xx + ROTATE_SMALL_CHUNK < imIn->xsize ? xx + ROTATE_SMALL_CHUNK : imIn->xsize; \ for (yyy = yy; yyy < yyysize; yyy++) { \ - INT* in = imIn->image[yyy]; \ + INT* in = (INT *)imIn->image[yyy]; \ for (xxx = xx; xxx < xxxsize; xxx++) { \ - imOut->image[xxx][yyy] = in[xxx]; \ + INT* out = (INT *)imOut->image[xxx]; \ + out[yyy] = in[xxx]; \ } \ } \ } \ @@ -167,10 +174,15 @@ ImagingTranspose(Imaging imOut, Imaging imIn) ImagingSectionEnter(&cookie); - if (imIn->image8) - TRANSPOSE(UINT8, image8) - else - TRANSPOSE(INT32, image32) + if (imIn->image8) { + if (strncmp(imIn->mode, "I;16", 4) == 0) { + TRANSPOSE(UINT16, image8); + } else { + TRANSPOSE(UINT8, image8); + } + } else { + TRANSPOSE(INT32, image32); + } ImagingSectionLeave(&cookie); @@ -205,10 +217,11 @@ ImagingTransverse(Imaging imOut, Imaging imIn) xxxsize = xx + ROTATE_SMALL_CHUNK < imIn->xsize ? xx + ROTATE_SMALL_CHUNK : imIn->xsize; \ yr = imIn->ysize - 1 - yy; \ for (yyy = yy; yyy < yyysize; yyy++, yr--) { \ - INT* in = imIn->image[yyy]; \ + INT* in = (INT *)imIn->image[yyy]; \ xr = imIn->xsize - 1 - xx; \ for (xxx = xx; xxx < xxxsize; xxx++, xr--) { \ - imOut->image[xr][yr] = in[xxx]; \ + INT* out = (INT *)imOut->image[xr]; \ + out[yr] = in[xxx]; \ } \ } \ } \ @@ -218,10 +231,15 @@ ImagingTransverse(Imaging imOut, Imaging imIn) ImagingSectionEnter(&cookie); - if (imIn->image8) - TRANSVERSE(UINT8, image8) - else - TRANSVERSE(INT32, image32) + if (imIn->image8) { + if (strncmp(imIn->mode, "I;16", 4) == 0) { + TRANSVERSE(UINT16, image8); + } else { + TRANSVERSE(UINT8, image8); + } + } else { + TRANSVERSE(INT32, image32); + } ImagingSectionLeave(&cookie); @@ -246,8 +264,8 @@ ImagingRotate180(Imaging imOut, Imaging imIn) #define ROTATE_180(INT, image) \ for (y = 0; y < imIn->ysize; y++, yr--) { \ - INT* in = imIn->image[y]; \ - INT* out = imOut->image[yr]; \ + INT* in = (INT *)imIn->image[y]; \ + INT* out = (INT *)imOut->image[yr]; \ xr = imIn->xsize-1; \ for (x = 0; x < imIn->xsize; x++, xr--) \ out[xr] = in[x]; \ @@ -299,9 +317,10 @@ ImagingRotate270(Imaging imOut, Imaging imIn) xxxsize = xx + ROTATE_SMALL_CHUNK < imIn->xsize ? xx + ROTATE_SMALL_CHUNK : imIn->xsize; \ yr = imIn->ysize - 1 - yy; \ for (yyy = yy; yyy < yyysize; yyy++, yr--) { \ - INT* in = imIn->image[yyy]; \ + INT* in = (INT *)imIn->image[yyy]; \ for (xxx = xx; xxx < xxxsize; xxx++) { \ - imOut->image[xxx][yr] = in[xxx]; \ + INT* out = (INT *)imOut->image[xxx]; \ + out[yr] = in[xxx]; \ } \ } \ } \ @@ -311,10 +330,15 @@ ImagingRotate270(Imaging imOut, Imaging imIn) ImagingSectionEnter(&cookie); - if (imIn->image8) - ROTATE_270(UINT8, image8) - else - ROTATE_270(INT32, image32) + if (imIn->image8) { + if (strncmp(imIn->mode, "I;16", 4) == 0) { + ROTATE_270(UINT16, image8); + } else { + ROTATE_270(UINT8, image8); + } + } else { + ROTATE_270(INT32, image32); + } ImagingSectionLeave(&cookie); diff --git a/src/libImaging/Pack.c b/src/libImaging/Pack.c index 5c298c6c5..56aebf58e 100644 --- a/src/libImaging/Pack.c +++ b/src/libImaging/Pack.c @@ -639,6 +639,7 @@ static struct { /* storage modes */ {"I;16", "I;16", 16, copy2}, + {"I;16", "I;16B", 16, packI16N_I16B}, {"I;16B", "I;16B", 16, copy2}, {"I;16L", "I;16L", 16, copy2}, {"I;16", "I;16N", 16, packI16N_I16}, // LibTiff native->image endian. diff --git a/src/libImaging/QuantHash.c b/src/libImaging/QuantHash.c index 48b7a973e..3fcbf3c02 100644 --- a/src/libImaging/QuantHash.c +++ b/src/libImaging/QuantHash.c @@ -35,8 +35,6 @@ struct _HashTable { uint32_t count; HashFunc hashFunc; HashCmpFunc cmpFunc; - KeyDestroyFunc keyDestroyFunc; - ValDestroyFunc valDestroyFunc; void *userData; }; @@ -51,8 +49,6 @@ HashTable *hashtable_new(HashFunc hf,HashCmpFunc cf) { if (!h) { return NULL; } h->hashFunc=hf; h->cmpFunc=cf; - h->keyDestroyFunc=NULL; - h->valDestroyFunc=NULL; h->length=MIN_LENGTH; h->count=0; h->userData=NULL; @@ -62,15 +58,6 @@ HashTable *hashtable_new(HashFunc hf,HashCmpFunc cf) { return h; } -static void _hashtable_destroy(const HashTable *h,const HashKey_t key,const HashVal_t val,void *u) { - if (h->keyDestroyFunc) { - h->keyDestroyFunc(h,key); - } - if (h->valDestroyFunc) { - h->valDestroyFunc(h,val); - } -} - static uint32_t _findPrime(uint32_t start,int dir) { static int unit[]={0,1,0,1,0,0,0,1,0,1,0,1,0,1,0,0}; uint32_t t; @@ -144,12 +131,6 @@ static int _hashtable_insert_node(HashTable *h,HashNode *node,int resize,int upd free(node); return 1; } else { - if (h->valDestroyFunc) { - h->valDestroyFunc(h,nv->value); - } - if (h->keyDestroyFunc) { - h->keyDestroyFunc(h,nv->key); - } nv->key=node->key; nv->value=node->value; free(node); @@ -180,7 +161,6 @@ static int _hashtable_insert(HashTable *h,HashKey_t key,HashVal_t val,int resize nv=*n; i=h->cmpFunc(h,nv->key,key); if (!i) { - if (h->valDestroyFunc) { h->valDestroyFunc(h,nv->value); } nv->value=val; return 1; } else if (i>0) { @@ -202,34 +182,6 @@ static int _hashtable_insert(HashTable *h,HashKey_t key,HashVal_t val,int resize } } -static int _hashtable_lookup_or_insert(HashTable *h,HashKey_t key,HashVal_t *retVal,HashVal_t newVal,int resize) { - HashNode **n,*nv; - HashNode *t; - int i; - uint32_t hash=h->hashFunc(h,key)%h->length; - - for (n=&(h->table[hash]);*n;n=&((*n)->next)) { - nv=*n; - i=h->cmpFunc(h,nv->key,key); - if (!i) { - *retVal=nv->value; - return 1; - } else if (i>0) { - break; - } - } - t=malloc(sizeof(HashNode)); - if (!t) return 0; - t->next=*n; - *n=t; - t->key=key; - t->value=newVal; - *retVal=newVal; - h->count++; - if (resize) _hashtable_resize(h); - return 1; -} - int hashtable_insert_or_update_computed(HashTable *h, HashKey_t key, ComputeFunc newFunc, @@ -243,14 +195,8 @@ int hashtable_insert_or_update_computed(HashTable *h, nv=*n; i=h->cmpFunc(h,nv->key,key); if (!i) { - HashVal_t old=nv->value; if (existsFunc) { existsFunc(h,nv->key,&(nv->value)); - if (nv->value!=old) { - if (h->valDestroyFunc) { - h->valDestroyFunc(h,old); - } - } } else { return 0; } @@ -275,10 +221,6 @@ int hashtable_insert_or_update_computed(HashTable *h, return 1; } -int hashtable_update(HashTable *h,HashKey_t key,HashVal_t val) { - return _hashtable_insert(h,key,val,1,0); -} - int hashtable_insert(HashTable *h,HashKey_t key,HashVal_t val) { return _hashtable_insert(h,key,val,1,0); } @@ -314,9 +256,6 @@ void hashtable_free(HashTable *h) { uint32_t i; if (h->table) { - if (h->keyDestroyFunc || h->keyDestroyFunc) { - hashtable_foreach(h,_hashtable_destroy,NULL); - } for (i=0;ilength;i++) { for (n=h->table[i];n;n=nn) { nn=n->next; @@ -328,84 +267,10 @@ void hashtable_free(HashTable *h) { free(h); } -ValDestroyFunc hashtable_set_value_destroy_func(HashTable *h,ValDestroyFunc d) { - ValDestroyFunc r=h->valDestroyFunc; - h->valDestroyFunc=d; - return r; -} - -KeyDestroyFunc hashtable_set_key_destroy_func(HashTable *h,KeyDestroyFunc d) { - KeyDestroyFunc r=h->keyDestroyFunc; - h->keyDestroyFunc=d; - return r; -} - -static int _hashtable_remove(HashTable *h, - const HashKey_t key, - HashKey_t *keyRet, - HashVal_t *valRet, - int resize) { - uint32_t hash=h->hashFunc(h,key)%h->length; - HashNode *n,*p; - int i; - - for (p=NULL,n=h->table[hash];n;p=n,n=n->next) { - i=h->cmpFunc(h,n->key,key); - if (!i) { - if (p) p=n->next; else h->table[hash]=n->next; - *keyRet=n->key; - *valRet=n->value; - free(n); - h->count++; - return 1; - } else if (i>0) { - break; - } - } - return 0; -} - -static int _hashtable_delete(HashTable *h,const HashKey_t key,int resize) { - uint32_t hash=h->hashFunc(h,key)%h->length; - HashNode *n,*p; - int i; - - for (p=NULL,n=h->table[hash];n;p=n,n=n->next) { - i=h->cmpFunc(h,n->key,key); - if (!i) { - if (p) p=n->next; else h->table[hash]=n->next; - if (h->valDestroyFunc) { h->valDestroyFunc(h,n->value); } - if (h->keyDestroyFunc) { h->keyDestroyFunc(h,n->key); } - free(n); - h->count++; - return 1; - } else if (i>0) { - break; - } - } - return 0; -} - -int hashtable_remove(HashTable *h,const HashKey_t key,HashKey_t *keyRet,HashVal_t *valRet) { - return _hashtable_remove(h,key,keyRet,valRet,1); -} - -int hashtable_delete(HashTable *h,const HashKey_t key) { - return _hashtable_delete(h,key,1); -} - void hashtable_rehash_compute(HashTable *h,CollisionFunc cf) { _hashtable_rehash(h,cf,h->length); } -void hashtable_rehash(HashTable *h) { - _hashtable_rehash(h,NULL,h->length); -} - -int hashtable_lookup_or_insert(HashTable *h,HashKey_t key,HashVal_t *valp,HashVal_t val) { - return _hashtable_lookup_or_insert(h,key,valp,val,1); -} - int hashtable_lookup(const HashTable *h,const HashKey_t key,HashVal_t *valp) { uint32_t hash=h->hashFunc(h,key)%h->length; HashNode *n; diff --git a/src/libImaging/QuantHash.h b/src/libImaging/QuantHash.h index 028b4af89..9874114e5 100644 --- a/src/libImaging/QuantHash.h +++ b/src/libImaging/QuantHash.h @@ -22,8 +22,6 @@ typedef uint32_t (*HashFunc)(const HashTable *,const HashKey_t); typedef int (*HashCmpFunc)(const HashTable *,const HashKey_t,const HashKey_t); typedef void (*IteratorFunc)(const HashTable *,const HashKey_t,const HashVal_t,void *); typedef void (*IteratorUpdateFunc)(const HashTable *,const HashKey_t,HashVal_t *,void *); -typedef void (*KeyDestroyFunc)(const HashTable *,HashKey_t); -typedef void (*ValDestroyFunc)(const HashTable *,HashVal_t); typedef void (*ComputeFunc)(const HashTable *,const HashKey_t,HashVal_t *); typedef void (*CollisionFunc)(const HashTable *,HashKey_t *,HashVal_t *,HashKey_t,HashVal_t); @@ -32,18 +30,11 @@ void hashtable_free(HashTable *h); void hashtable_foreach(HashTable *h,IteratorFunc i,void *u); void hashtable_foreach_update(HashTable *h,IteratorUpdateFunc i,void *u); int hashtable_insert(HashTable *h,HashKey_t key,HashVal_t val); -int hashtable_update(HashTable *h,HashKey_t key,HashVal_t val); int hashtable_lookup(const HashTable *h,const HashKey_t key,HashVal_t *valp); -int hashtable_lookup_or_insert(HashTable *h,HashKey_t key,HashVal_t *valp,HashVal_t val); int hashtable_insert_or_update_computed(HashTable *h,HashKey_t key,ComputeFunc newFunc,ComputeFunc existsFunc); -int hashtable_delete(HashTable *h,const HashKey_t key); -int hashtable_remove(HashTable *h,const HashKey_t key,HashKey_t *keyRet,HashVal_t *valRet); void *hashtable_set_user_data(HashTable *h,void *data); void *hashtable_get_user_data(const HashTable *h); -KeyDestroyFunc hashtable_set_key_destroy_func(HashTable *,KeyDestroyFunc d); -ValDestroyFunc hashtable_set_value_destroy_func(HashTable *,ValDestroyFunc d); uint32_t hashtable_get_count(const HashTable *h); -void hashtable_rehash(HashTable *h); void hashtable_rehash_compute(HashTable *h,CollisionFunc cf); #endif // __QUANTHASH_H__ diff --git a/src/libImaging/TiffDecode.c b/src/libImaging/TiffDecode.c index 64ac86e6a..381f795e0 100644 --- a/src/libImaging/TiffDecode.c +++ b/src/libImaging/TiffDecode.c @@ -124,6 +124,7 @@ toff_t _tiffSizeProc(thandle_t hdata) { return (toff_t)state->size; } + int _tiffMapProc(thandle_t hdata, tdata_t* pbase, toff_t* psize) { TIFFSTATE *state = (TIFFSTATE *)hdata; @@ -168,13 +169,117 @@ int ImagingLibTiffInit(ImagingCodecState state, int fp, int offset) { return 1; } + +int ReadTile(TIFF* tiff, UINT32 col, UINT32 row, UINT32* buffer) { + uint16 photometric; + + TIFFGetField(tiff, TIFFTAG_PHOTOMETRIC, &photometric); + + // To avoid dealing with YCbCr subsampling, let libtiff handle it + if (photometric == PHOTOMETRIC_YCBCR) { + UINT32 tile_width, tile_height, swap_line_size, i_row; + UINT32* swap_line; + + TIFFGetField(tiff, TIFFTAG_TILEWIDTH, &tile_width); + TIFFGetField(tiff, TIFFTAG_TILELENGTH, &tile_height); + + swap_line_size = tile_width * sizeof(UINT32); + if (tile_width != swap_line_size / sizeof(UINT32)) { + return -1; + } + + /* Read the tile into an RGBA array */ + if (!TIFFReadRGBATile(tiff, col, row, buffer)) { + return -1; + } + + swap_line = (UINT32*)malloc(swap_line_size); + /* + * For some reason the TIFFReadRGBATile() function chooses the + * lower left corner as the origin. Vertically mirror scanlines. + */ + for(i_row = 0; i_row < tile_height / 2; i_row++) { + UINT32 *top_line, *bottom_line; + + top_line = buffer + tile_width * i_row; + bottom_line = buffer + tile_width * (tile_height - i_row - 1); + + memcpy(swap_line, top_line, 4*tile_width); + memcpy(top_line, bottom_line, 4*tile_width); + memcpy(bottom_line, swap_line, 4*tile_width); + } + + free(swap_line); + + return 0; + } + + if (TIFFReadTile(tiff, (tdata_t)buffer, col, row, 0, 0) == -1) { + TRACE(("Decode Error, Tile at %dx%d\n", col, row)); + return -1; + } + + TRACE(("Successfully read tile at %dx%d; \n\n", col, row)); + + return 0; +} + +int ReadStrip(TIFF* tiff, UINT32 row, UINT32* buffer) { + uint16 photometric; + TIFFGetField(tiff, TIFFTAG_PHOTOMETRIC, &photometric); + + // To avoid dealing with YCbCr subsampling, let libtiff handle it + if (photometric == PHOTOMETRIC_YCBCR) { + TIFFRGBAImage img; + char emsg[1024] = ""; + UINT32 rows_per_strip, rows_to_read; + int ok; + + + TIFFGetFieldDefaulted(tiff, TIFFTAG_ROWSPERSTRIP, &rows_per_strip); + if ((row % rows_per_strip) != 0) { + TRACE(("Row passed to ReadStrip() must be first in a strip.")); + return -1; + } + + if (TIFFRGBAImageOK(tiff, emsg) && TIFFRGBAImageBegin(&img, tiff, 0, emsg)) { + TRACE(("Initialized RGBAImage\n")); + + img.req_orientation = ORIENTATION_TOPLEFT; + img.row_offset = row; + img.col_offset = 0; + + rows_to_read = min(rows_per_strip, img.height - row); + + TRACE(("rows to read: %d\n", rows_to_read)); + ok = TIFFRGBAImageGet(&img, buffer, img.width, rows_to_read); + + TIFFRGBAImageEnd(&img); + } else { + ok = 0; + } + + if (ok == 0) { + TRACE(("Decode Error, row %d; msg: %s\n", row, emsg)); + return -1; + } + + return 0; + } + + if (TIFFReadEncodedStrip(tiff, TIFFComputeStrip(tiff, row, 0), (tdata_t)buffer, -1) == -1) { + TRACE(("Decode Error, strip %d\n", TIFFComputeStrip(tiff, row, 0))); + return -1; + } + + return 0; +} + int ImagingLibTiffDecode(Imaging im, ImagingCodecState state, UINT8* buffer, int bytes) { TIFFSTATE *clientstate = (TIFFSTATE *)state->context; char *filename = "tempfile.tif"; char *mode = "r"; TIFF *tiff; - uint16 photometric = 0, compression; - /* buffer is the encoded file, bytes is the length of the encoded file */ /* it all ends up in state->buffer, which is a uint8* from Imaging.h */ @@ -235,19 +340,17 @@ int ImagingLibTiffDecode(Imaging im, ImagingCodecState state, UINT8* buffer, int } } - TIFFGetFieldDefaulted(tiff, TIFFTAG_COMPRESSION, &compression); - TIFFGetField(tiff, TIFFTAG_PHOTOMETRIC, &photometric); - if (compression == COMPRESSION_JPEG && photometric == PHOTOMETRIC_YCBCR) { - /* Set pseudo-tag to force automatic YCbCr->RGB conversion */ - TIFFSetField(tiff, TIFFTAG_JPEGCOLORMODE, JPEGCOLORMODE_RGB); - } - if (TIFFIsTiled(tiff)) { - uint32 x, y, tile_y; - uint32 tileWidth, tileLength; + UINT32 x, y, tile_y, row_byte_size; + UINT32 tile_width, tile_length, current_tile_width; UINT8 *new_data; - state->bytes = TIFFTileSize(tiff); + TIFFGetField(tiff, TIFFTAG_TILEWIDTH, &tile_width); + TIFFGetField(tiff, TIFFTAG_TILELENGTH, &tile_length); + + // We could use TIFFTileSize, but for YCbCr data it returns subsampled data size + row_byte_size = (tile_width * state->bits + 7) / 8; + state->bytes = row_byte_size * tile_length; /* overflow check for malloc */ if (state->bytes > INT_MAX - 1) { @@ -268,12 +371,9 @@ int ImagingLibTiffDecode(Imaging im, ImagingCodecState state, UINT8* buffer, int TRACE(("TIFFTileSize: %d\n", state->bytes)); - TIFFGetField(tiff, TIFFTAG_TILEWIDTH, &tileWidth); - TIFFGetField(tiff, TIFFTAG_TILELENGTH, &tileLength); - - for (y = state->yoff; y < state->ysize; y += tileLength) { - for (x = state->xoff; x < state->xsize; x += tileWidth) { - if (TIFFReadTile(tiff, (tdata_t)state->buffer, x, y, 0, 0) == -1) { + for (y = state->yoff; y < state->ysize; y += tile_length) { + for (x = state->xoff; x < state->xsize; x += tile_width) { + if (ReadTile(tiff, x, y, (UINT32*) state->buffer) == -1) { TRACE(("Decode Error, Tile at %dx%d\n", x, y)); state->errcode = IMAGING_CODEC_BROKEN; TIFFClose(tiff); @@ -282,53 +382,68 @@ int ImagingLibTiffDecode(Imaging im, ImagingCodecState state, UINT8* buffer, int TRACE(("Read tile at %dx%d; \n\n", x, y)); + current_tile_width = min(tile_width, state->xsize - x); + // iterate over each line in the tile and stuff data into image - for (tile_y = 0; tile_y < min(tileLength, state->ysize - y); tile_y++) { + for (tile_y = 0; tile_y < min(tile_length, state->ysize - y); tile_y++) { + TRACE(("Writing tile data at %dx%d using tile_width: %d; \n", tile_y + y, x, current_tile_width)); - TRACE(("Writing tile data at %dx%d using tilwWidth: %d; \n", tile_y + y, x, min(tileWidth, state->xsize - x))); - - // UINT8 * bbb = state->buffer + tile_y * (state->bytes / tileLength); + // UINT8 * bbb = state->buffer + tile_y * row_byte_size; // TRACE(("chars: %x%x%x%x\n", ((UINT8 *)bbb)[0], ((UINT8 *)bbb)[1], ((UINT8 *)bbb)[2], ((UINT8 *)bbb)[3])); state->shuffle((UINT8*) im->image[tile_y + y] + x * im->pixelsize, - state->buffer + tile_y * (state->bytes / tileLength), - min(tileWidth, state->xsize - x) + state->buffer + tile_y * row_byte_size, + current_tile_width ); } } } } else { - tsize_t size; + UINT32 strip_row, row_byte_size; + UINT8 *new_data; + UINT32 rows_per_strip; - size = TIFFScanlineSize(tiff); - TRACE(("ScanlineSize: %lu \n", size)); - if (size > state->bytes) { - TRACE(("Error, scanline size > buffer size\n")); - state->errcode = IMAGING_CODEC_BROKEN; + TIFFGetField(tiff, TIFFTAG_ROWSPERSTRIP, &rows_per_strip); + TRACE(("RowsPerStrip: %u \n", rows_per_strip)); + + // We could use TIFFStripSize, but for YCbCr data it returns subsampled data size + row_byte_size = (state->xsize * state->bits + 7) / 8; + state->bytes = rows_per_strip * row_byte_size; + + TRACE(("StripSize: %d \n", state->bytes)); + + /* realloc to fit whole strip */ + new_data = realloc (state->buffer, state->bytes); + if (!new_data) { + state->errcode = IMAGING_CODEC_MEMORY; TIFFClose(tiff); return -1; } - // Have to do this row by row and shove stuff into the buffer that way, - // with shuffle. (or, just alloc a buffer myself, then figure out how to get it - // back in. Can't use read encoded stripe. + state->buffer = new_data; - // This thing pretty much requires that I have the whole image in one shot. - // Perhaps a stub version would work better??? - while(state->y < state->ysize){ - if (TIFFReadScanline(tiff, (tdata_t)state->buffer, (uint32)state->y, 0) == -1) { - TRACE(("Decode Error, row %d\n", state->y)); + for (; state->y < state->ysize; state->y += rows_per_strip) { + if (ReadStrip(tiff, state->y, (UINT32 *)state->buffer) == -1) { + TRACE(("Decode Error, strip %d\n", TIFFComputeStrip(tiff, state->y, 0))); state->errcode = IMAGING_CODEC_BROKEN; TIFFClose(tiff); return -1; } - /* TRACE(("Decoded row %d \n", state->y)); */ - state->shuffle((UINT8*) im->image[state->y + state->yoff] + - state->xoff * im->pixelsize, - state->buffer, - state->xsize); - state->y++; + TRACE(("Decoded strip for row %d \n", state->y)); + + // iterate over each row in the strip and stuff data into image + for (strip_row = 0; strip_row < min(rows_per_strip, state->ysize - state->y); strip_row++) { + TRACE(("Writing data into line %d ; \n", state->y + strip_row)); + + // UINT8 * bbb = state->buffer + strip_row * (state->bytes / rows_per_strip); + // TRACE(("chars: %x %x %x %x\n", ((UINT8 *)bbb)[0], ((UINT8 *)bbb)[1], ((UINT8 *)bbb)[2], ((UINT8 *)bbb)[3])); + + state->shuffle((UINT8*) im->image[state->y + state->yoff + strip_row] + + state->xoff * im->pixelsize, + state->buffer + strip_row * row_byte_size, + state->xsize); + } } } diff --git a/src/libImaging/Unpack.c b/src/libImaging/Unpack.c index e9921d2ca..ccfeabaf9 100644 --- a/src/libImaging/Unpack.c +++ b/src/libImaging/Unpack.c @@ -1331,6 +1331,7 @@ static struct { {"RGB", "BGR;5", 16, ImagingUnpackBGR15}, /* compat */ {"RGB", "RGBX", 32, copy4}, {"RGB", "RGBX;L", 32, unpackRGBAL}, + {"RGB", "RGBA;L", 32, unpackRGBAL}, {"RGB", "BGRX", 32, ImagingUnpackBGRX}, {"RGB", "XRGB", 24, ImagingUnpackXRGB}, {"RGB", "XBGR", 32, ImagingUnpackXBGR}, diff --git a/winbuild/appveyor_install_pypy.cmd b/winbuild/appveyor_install_pypy.cmd index 3092fc617..8c36268f0 100644 --- a/winbuild/appveyor_install_pypy.cmd +++ b/winbuild/appveyor_install_pypy.cmd @@ -1,3 +1,3 @@ -curl -fsSL -o pypy2.zip https://bitbucket.org/pypy/pypy/downloads/pypy2.7-v7.0.0-win32.zip +curl -fsSL -o pypy2.zip https://bitbucket.org/pypy/pypy/downloads/pypy2.7-v7.1.0-win32.zip 7z x pypy2.zip -oc:\ -c:\Python37\Scripts\virtualenv.exe -p c:\pypy2.7-v7.0.0-win32\pypy.exe c:\vp\pypy2 +c:\Python37\Scripts\virtualenv.exe -p c:\pypy2.7-v7.1.0-win32\pypy.exe c:\vp\pypy2 diff --git a/winbuild/build.py b/winbuild/build.py index 313a14ff4..01c847bae 100755 --- a/winbuild/build.py +++ b/winbuild/build.py @@ -109,6 +109,7 @@ set INCLUDE=%%INCLUDE%%;%%INCLIB%%\%(inc_dir)s;%%INCLIB%%\tcl%(tcl_ver)s\include setlocal set LIB=%%LIB%%;C:\Python%(py_ver)s\tcl%(vc_setup)s call %(python_path)s\%(executable)s setup.py %(imaging_libs)s %%BLDOPT%% +call %(python_path)s\%(executable)s -c "from PIL import _webp;import os, shutil;shutil.copy('%%INCLIB%%\\freetype.dll', os.path.dirname(_webp.__file__));" endlocal endlocal diff --git a/winbuild/build_dep.py b/winbuild/build_dep.py index 13296dc5b..1bf8b51a8 100644 --- a/winbuild/build_dep.py +++ b/winbuild/build_dep.py @@ -187,37 +187,30 @@ endlocal return script % compiler -def msbuild_freetype(compiler): - if compiler['env_version'] == 'v7.1': - return msbuild_freetype_71(compiler) - return msbuild_freetype_70(compiler) - - -def msbuild_freetype_71(compiler): - return r""" +def msbuild_freetype(compiler, bit): + script = r""" rem Build freetype setlocal rd /S /Q %%FREETYPE%%\objs -%%MSBUILD%% %%FREETYPE%%\builds\windows\vc%(vc_version)s\freetype.sln /t:Clean;Build /p:Configuration="Release" /p:Platform=%(platform)s /m +set DefaultPlatformToolset=v100 +""" + properties = r"""/p:Configuration="Release" /p:Platform=%(platform)s""" + if bit == 64: + script += r'copy /Y /B ' +\ + r'"C:\Program Files (x86)\Microsoft SDKs\Windows\v7.1A\Lib\x64\*.Lib" ' +\ + r'%%FREETYPE%%\builds\windows\vc2010' + properties += r" /p:_IsNativeEnvironment=false" + script += r""" +%%MSBUILD%% %%FREETYPE%%\builds\windows\vc2010\freetype.sln /t:Clean;Build """+properties+r""" /m xcopy /Y /E /Q %%FREETYPE%%\include %%INCLIB%% -copy /Y /B %%FREETYPE%%\objs\vc%(vc_version)s\%(platform)s\*.lib %%INCLIB%%\freetype.lib +""" + freetypeReleaseDir = r"%%FREETYPE%%\objs\%(platform)s\Release" + script += r""" +copy /Y /B """+freetypeReleaseDir+r"""\freetype.lib %%INCLIB%%\freetype.lib +copy /Y /B """+freetypeReleaseDir+r"""\freetype.dll %%INCLIB%%\..\freetype.dll endlocal -""" % compiler # noqa: E501 - - -def msbuild_freetype_70(compiler): - return r""" -rem Build freetype -setlocal -py -3 %%~dp0\fixproj.py %%FREETYPE%%\builds\windows\vc%(vc_version)s\freetype.sln %(platform)s -py -3 %%~dp0\fixproj.py %%FREETYPE%%\builds\windows\vc%(vc_version)s\freetype.vcproj %(platform)s -rd /S /Q %%FREETYPE%%\objs -%%MSBUILD%% %%FREETYPE%%\builds\windows\vc%(vc_version)s\freetype.sln /t:Clean;Build /p:Configuration="LIB Release";Platform=%(platform)s /m -xcopy /Y /E /Q %%FREETYPE%%\include %%INCLIB%% -xcopy /Y /E /Q %%FREETYPE%%\objs\win32\vc%(vc_version)s %%INCLIB%% -copy /Y /B %%FREETYPE%%\objs\win32\vc%(vc_version)s\*.lib %%INCLIB%%\freetype.lib -endlocal -""" % compiler # noqa: E501 +""" + return script % compiler # noqa: E501 def build_lcms2(compiler): @@ -238,9 +231,9 @@ setlocal rd /S /Q %%LCMS%%\Lib rd /S /Q %%LCMS%%\Projects\VC%(vc_version)s\Release %%MSBUILD%% %%LCMS%%\Projects\VC%(vc_version)s\lcms2.sln /t:Clean /p:Configuration="Release" /p:Platform=Win32 /m -%%MSBUILD%% %%LCMS%%\Projects\VC%(vc_version)s\lcms2.sln /t:lcms2_static /p:Configuration="Release" /p:Platform=Win32 /m +%%MSBUILD%% %%LCMS%%\Projects\VC%(vc_version)s\lcms2.sln /t:lcms2_static /p:Configuration="Release" /p:Platform=Win32 /p:PlatformToolset=v90 /m xcopy /Y /E /Q %%LCMS%%\include %%INCLIB%% -copy /Y /B %%LCMS%%\Projects\VC%(vc_version)s\Release\*.lib %%INCLIB%% +copy /Y /B %%LCMS%%\Lib\MS\*.lib %%INCLIB%% endlocal """ % compiler # noqa: E501 @@ -265,7 +258,7 @@ rem Build gs setlocal """ + vc_setup(compiler, bit) + r""" set MSVC_VERSION=""" + { - "2008": "9", + "2010": "90", "2015": "14" }[compiler['vc_version']] + r""" set RCOMP="C:\Program Files (x86)\Microsoft SDKs\Windows\v7.1A\Bin\RC.Exe" @@ -289,7 +282,7 @@ def add_compiler(compiler, bit): # script.append(extract_openjpeg(compiler)) - script.append(msbuild_freetype(compiler)) + script.append(msbuild_freetype(compiler, bit)) script.append(build_lcms2(compiler)) # script.append(nmake_openjpeg(compiler)) script.append(build_ghostscript(compiler, bit)) @@ -308,7 +301,7 @@ if 'PYTHON' in os.environ: else: # for compiler in all_compilers(): # add_compiler(compiler) - add_compiler(compilers[7.0][2008][32], 32) + add_compiler(compilers[7.0][2010][32], 32) with open('build_deps.cmd', 'w') as f: f.write("\n".join(script)) diff --git a/winbuild/config.py b/winbuild/config.py index da6f7855b..3b13951f1 100644 --- a/winbuild/config.py +++ b/winbuild/config.py @@ -3,8 +3,8 @@ import os SF_MIRROR = 'http://iweb.dl.sourceforge.net' PILLOW_DEPENDS_DIR = 'C:\\pillow-depends\\' -pythons = {'27': {'compiler': 7, 'vc': 2008}, - 'pypy2': {'compiler': 7, 'vc': 2008}, +pythons = {'27': {'compiler': 7, 'vc': 2010}, + 'pypy2': {'compiler': 7, 'vc': 2010}, '35': {'compiler': 7.1, 'vc': 2015}, '36': {'compiler': 7.1, 'vc': 2015}, '37': {'compiler': 7.1, 'vc': 2015}} @@ -33,9 +33,9 @@ libs = { 'dir': 'tiff-4.0.10', }, 'freetype': { - 'url': 'https://download.savannah.gnu.org/releases/freetype/freetype-2.9.1.tar.gz', # noqa: E501 - 'filename': PILLOW_DEPENDS_DIR + 'freetype-2.9.1.tar.gz', - 'dir': 'freetype-2.9.1', + 'url': 'https://download.savannah.gnu.org/releases/freetype/freetype-2.10.0.tar.gz', # noqa: E501 + 'filename': PILLOW_DEPENDS_DIR + 'freetype-2.10.0.tar.gz', + 'dir': 'freetype-2.10.0', }, 'lcms': { 'url': SF_MIRROR+'/project/lcms/lcms/2.7/lcms2-2.7.zip', @@ -43,9 +43,9 @@ libs = { 'dir': 'lcms2-2.7', }, 'ghostscript': { - 'url': 'https://github.com/ArtifexSoftware/ghostpdl-downloads/releases/download/gs926/ghostscript-9.26.tar.gz', # noqa: E501 - 'filename': PILLOW_DEPENDS_DIR + 'ghostscript-9.26.tar.gz', - 'dir': 'ghostscript-9.26', + 'url': 'https://github.com/ArtifexSoftware/ghostpdl-downloads/releases/download/gs927/ghostscript-9.27.tar.gz', # noqa: E501 + 'filename': PILLOW_DEPENDS_DIR + 'ghostscript-9.27.tar.gz', + 'dir': 'ghostscript-9.27', }, 'tcl-8.5': { 'url': SF_MIRROR+'/project/tcl/Tcl/8.5.19/tcl8519-src.zip', @@ -83,10 +83,10 @@ libs = { compilers = { 7: { - 2008: { + 2010: { 64: { 'env_version': 'v7.0', - 'vc_version': '2008', + 'vc_version': '2010', 'env_flags': '/x64 /xp', 'inc_dir': 'msvcr90-x64', 'platform': 'x64', @@ -94,7 +94,7 @@ compilers = { }, 32: { 'env_version': 'v7.0', - 'vc_version': '2008', + 'vc_version': '2010', 'env_flags': '/x86 /xp', 'inc_dir': 'msvcr90-x32', 'platform': 'Win32', diff --git a/winbuild/fetch.py b/winbuild/fetch.py index b7acb63ac..830a64ee5 100644 --- a/winbuild/fetch.py +++ b/winbuild/fetch.py @@ -9,7 +9,11 @@ def fetch(url): if not os.path.exists(name): print("Fetching", url) - content = urllib.request.urlopen(url).read() + try: + r = urllib.request.urlopen(url) + except urllib.error.URLError: + r = urllib.request.urlopen(url) + content = r.read() with open(name, 'wb') as fd: fd.write(content) return name