Merge branch 'master' of https://github.com/python-pillow/Pillow
|  | @ -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) | ||||
|       } | ||||
|  |  | |||
							
								
								
									
										2
									
								
								.github/CONTRIBUTING.md
									
									
									
									
										vendored
									
									
								
							
							
						
						|  | @ -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 | ||||
|  |  | |||
							
								
								
									
										30
									
								
								.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 | ||||
|  |  | |||
							
								
								
									
										94
									
								
								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 | ||||
|  |  | |||
							
								
								
									
										49
									
								
								README.rst
									
									
									
									
									
								
							
							
						
						|  | @ -20,6 +20,30 @@ Pillow is the friendly PIL fork by `Alex Clark and Contributors <https://github. | |||
|     * - social | ||||
|       - |gitter| |twitter| | ||||
| 
 | ||||
| .. end-badges | ||||
| 
 | ||||
| More Information | ||||
| ---------------- | ||||
| 
 | ||||
| - `Documentation <https://pillow.readthedocs.io/>`_ | ||||
| 
 | ||||
|   - `Installation <https://pillow.readthedocs.io/en/latest/installation.html>`_ | ||||
|   - `Handbook <https://pillow.readthedocs.io/en/latest/handbook/index.html>`_ | ||||
| 
 | ||||
| - `Contribute <https://github.com/python-pillow/Pillow/blob/master/.github/CONTRIBUTING.md>`_ | ||||
| 
 | ||||
|   - `Issues <https://github.com/python-pillow/Pillow/issues>`_ | ||||
|   - `Pull requests <https://github.com/python-pillow/Pillow/pulls>`_ | ||||
| 
 | ||||
| - `Changelog <https://github.com/python-pillow/Pillow/blob/master/CHANGES.rst>`_ | ||||
| 
 | ||||
|   - `Pre-fork <https://github.com/python-pillow/Pillow/blob/master/CHANGES.rst#pre-fork>`_ | ||||
| 
 | ||||
| Report a Vulnerability | ||||
| ---------------------- | ||||
| 
 | ||||
| To report a security vulnerability, please follow the procedure described in the `Tidelift security policy <https://tidelift.com/docs/security>`_. | ||||
| 
 | ||||
| .. |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 <https://github. | |||
|    :target: https://ci.appveyor.com/project/python-pillow/Pillow | ||||
|    :alt: AppVeyor CI build status (Windows) | ||||
| 
 | ||||
| .. |coverage| image:: https://coveralls.io/repos/python-pillow/Pillow/badge.svg?branch=master&service=github | ||||
|    :target: https://coveralls.io/github/python-pillow/Pillow?branch=master | ||||
| .. |coverage| image:: https://codecov.io/gh/python-pillow/Pillow/branch/master/graph/badge.svg | ||||
|    :target: https://codecov.io/gh/python-pillow/Pillow | ||||
|    :alt: Code coverage | ||||
| 
 | ||||
| .. |zenodo| image:: https://zenodo.org/badge/17549/python-pillow/Pillow.svg | ||||
|  | @ -61,24 +85,3 @@ Pillow is the friendly PIL fork by `Alex Clark and Contributors <https://github. | |||
| .. |twitter| image:: https://img.shields.io/badge/tweet-on%20Twitter-00aced.svg | ||||
|    :target: https://twitter.com/PythonPillow | ||||
|    :alt: Follow on https://twitter.com/PythonPillow | ||||
| 
 | ||||
| .. end-badges | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
| More Information | ||||
| ---------------- | ||||
| 
 | ||||
| - `Documentation <https://pillow.readthedocs.io/>`_ | ||||
| 
 | ||||
|   - `Installation <https://pillow.readthedocs.io/en/latest/installation.html>`_ | ||||
|   - `Handbook <https://pillow.readthedocs.io/en/latest/handbook/index.html>`_ | ||||
| 
 | ||||
| - `Contribute <https://github.com/python-pillow/Pillow/blob/master/.github/CONTRIBUTING.md>`_ | ||||
| 
 | ||||
|   - `Issues <https://github.com/python-pillow/Pillow/issues>`_ | ||||
|   - `Pull requests <https://github.com/python-pillow/Pillow/pulls>`_ | ||||
| 
 | ||||
| - `Changelog <https://github.com/python-pillow/Pillow/blob/master/CHANGES.rst>`_ | ||||
| 
 | ||||
|   - `Pre-fork <https://github.com/python-pillow/Pillow/blob/master/CHANGES.rst#pre-fork>`_ | ||||
|  |  | |||
|  | @ -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 | ||||
|  |  | |||
							
								
								
									
										
											BIN
										
									
								
								Tests/images/1_trns.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 612 B | 
							
								
								
									
										
											BIN
										
									
								
								Tests/images/drawing_roundDown.emf
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								Tests/images/fujifilm.mpo
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 9.5 MiB | 
							
								
								
									
										
											BIN
										
									
								
								Tests/images/hopper.pnm
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								Tests/images/hopper_orientation_2.jpg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 4.6 KiB | 
							
								
								
									
										
											BIN
										
									
								
								Tests/images/hopper_orientation_2.webp
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 3.6 KiB | 
							
								
								
									
										
											BIN
										
									
								
								Tests/images/hopper_orientation_3.jpg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 4.6 KiB | 
							
								
								
									
										
											BIN
										
									
								
								Tests/images/hopper_orientation_3.webp
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 3.7 KiB | 
							
								
								
									
										
											BIN
										
									
								
								Tests/images/hopper_orientation_4.jpg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 4.6 KiB | 
							
								
								
									
										
											BIN
										
									
								
								Tests/images/hopper_orientation_4.webp
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 3.6 KiB | 
							
								
								
									
										
											BIN
										
									
								
								Tests/images/hopper_orientation_5.jpg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 4.8 KiB | 
							
								
								
									
										
											BIN
										
									
								
								Tests/images/hopper_orientation_5.webp
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 3.5 KiB | 
							
								
								
									
										
											BIN
										
									
								
								Tests/images/hopper_orientation_6.jpg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 4.8 KiB | 
							
								
								
									
										
											BIN
										
									
								
								Tests/images/hopper_orientation_6.webp
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 3.6 KiB | 
							
								
								
									
										
											BIN
										
									
								
								Tests/images/hopper_orientation_7.jpg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 4.8 KiB | 
							
								
								
									
										
											BIN
										
									
								
								Tests/images/hopper_orientation_7.webp
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 3.6 KiB | 
							
								
								
									
										
											BIN
										
									
								
								Tests/images/hopper_orientation_8.jpg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 4.8 KiB | 
							
								
								
									
										
											BIN
										
									
								
								Tests/images/hopper_orientation_8.webp
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 3.6 KiB | 
							
								
								
									
										
											BIN
										
									
								
								Tests/images/hopper_roundDown.bmp
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 48 KiB | 
							
								
								
									
										
											BIN
										
									
								
								Tests/images/hopper_roundDown_2.tif
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								Tests/images/hopper_roundDown_3.tif
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								Tests/images/hopper_roundDown_None.tif
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								Tests/images/hopper_roundUp_2.tif
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								Tests/images/hopper_roundUp_3.tif
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								Tests/images/hopper_roundUp_None.tif
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								Tests/images/i_trns.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 1.4 KiB | 
							
								
								
									
										
											BIN
										
									
								
								Tests/images/iptc_roundDown.jpg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 20 KiB | 
							
								
								
									
										
											BIN
										
									
								
								Tests/images/iptc_roundUp.jpg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 20 KiB | 
							
								
								
									
										
											BIN
										
									
								
								Tests/images/old-style-jpeg-compression.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 1.4 MiB | 
							
								
								
									
										
											BIN
										
									
								
								Tests/images/old-style-jpeg-compression.tif
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								Tests/images/rgb32bf-rgba.bmp
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 32 KiB | 
							
								
								
									
										
											BIN
										
									
								
								Tests/images/sugarshack_frame_size.mpo
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 117 KiB | 
							
								
								
									
										
											BIN
										
									
								
								Tests/images/test_language.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 777 B | 
|  | @ -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) | ||||
|  |  | |||
|  | @ -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], | ||||
|  |  | |||
|  | @ -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: | ||||
|  |  | |||
|  | @ -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): | ||||
|  |  | |||
|  | @ -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") | ||||
|  |  | |||
|  | @ -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) | ||||
|  |  | |||
|  | @ -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): | ||||
|  |  | |||
|  | @ -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 | ||||
| 
 | ||||
|  |  | |||
|  | @ -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: | ||||
|  |  | |||
|  | @ -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") | ||||
|  |  | |||
|  | @ -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)) | ||||
|  |  | |||
|  | @ -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() | ||||
| 
 | ||||
|  |  | |||
|  | @ -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 | ||||
|  |  | |||
|  | @ -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) | ||||
|  |  | |||
|  | @ -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) | ||||
|  |  | |||
|  | @ -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")) | ||||
|  |  | |||
|  | @ -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)) | ||||
| 
 | ||||
|  |  | |||
|  | @ -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") | ||||
|  |  | |||
|  | @ -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() | ||||
|  |  | |||
|  | @ -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)) | ||||
|  |  | |||
|  | @ -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 | ||||
| 
 | ||||
|  |  | |||
|  | @ -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, | ||||
|         }) | ||||
|  |  | |||
|  | @ -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): | ||||
|  |  | |||
|  | @ -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) | ||||
|  |  | |||
|  | @ -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) | ||||
|  |  | |||
|  | @ -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;16B", '>u2'), | ||||
|                  ("I;16L", '<u2'), | ||||
|                  ("HSV", 'uint8'), | ||||
|                  ("HSV", numpy.uint8), | ||||
|                  ] | ||||
| 
 | ||||
|         for mode in modes: | ||||
|  | @ -167,7 +162,7 @@ class TestNumpy(PillowTestCase): | |||
|         # see https://github.com/python-pillow/Pillow/issues/439 | ||||
| 
 | ||||
|         data = list(range(256))*3 | ||||
|         lut = numpy.array(data, dtype='uint8') | ||||
|         lut = numpy.array(data, dtype=numpy.uint8) | ||||
| 
 | ||||
|         im = hopper() | ||||
| 
 | ||||
|  |  | |||
|  | @ -10,7 +10,6 @@ except ImportError: | |||
| 
 | ||||
| @unittest.skipIf(pyroma is None, "Pyroma is not installed") | ||||
| class TestPyroma(PillowTestCase): | ||||
| 
 | ||||
|     def test_pyroma(self): | ||||
|         # Arrange | ||||
|         data = pyroma.projectdata.get_data(".") | ||||
|  | @ -19,12 +18,13 @@ class TestPyroma(PillowTestCase): | |||
|         rating = pyroma.ratings.rate(data) | ||||
| 
 | ||||
|         # Assert | ||||
|         if 'rc' in __version__: | ||||
|             # Pyroma needs to chill about RC versions | ||||
|             # and not kill all our tests. | ||||
|             self.assertEqual(rating, (9, [ | ||||
|                 "The package's version number does not comply with PEP-386."])) | ||||
|         if "rc" in __version__: | ||||
|             # Pyroma needs to chill about RC versions and not kill all our tests. | ||||
|             self.assertEqual( | ||||
|                 rating, | ||||
|                 (9, ["The package's version number does not comply with PEP-386."]), | ||||
|             ) | ||||
| 
 | ||||
|         else: | ||||
|             # Should have a perfect score | ||||
|             self.assertEqual(rating, (10, [])) | ||||
|             # Should have a near-perfect score | ||||
|             self.assertEqual(rating, (9, ["Your package does not have license data."])) | ||||
|  |  | |||
|  | @ -11,17 +11,6 @@ class TestFromQPixmap(PillowQPixmapTestCase, PillowTestCase): | |||
|         # Qt saves all pixmaps as rgb | ||||
|         self.assert_image_equal(result, expected.convert('RGB')) | ||||
| 
 | ||||
|     def test_sanity_1(self): | ||||
|         self.roundtrip(hopper('1')) | ||||
| 
 | ||||
|     def test_sanity_rgb(self): | ||||
|         self.roundtrip(hopper('RGB')) | ||||
| 
 | ||||
|     def test_sanity_rgba(self): | ||||
|         self.roundtrip(hopper('RGBA')) | ||||
| 
 | ||||
|     def test_sanity_l(self): | ||||
|         self.roundtrip(hopper('L')) | ||||
| 
 | ||||
|     def test_sanity_p(self): | ||||
|         self.roundtrip(hopper('P')) | ||||
|     def test_sanity(self): | ||||
|         for mode in ('1', 'RGB', 'RGBA', 'L', 'P'): | ||||
|             self.roundtrip(hopper(mode)) | ||||
|  |  | |||
|  | @ -23,13 +23,13 @@ jobs: | |||
| 
 | ||||
| - template: .azure-pipelines/jobs/test-docker.yml | ||||
|   parameters: | ||||
|     docker: 'ubuntu-trusty-x86' | ||||
|     name:   'ubuntu_trusty_x86' | ||||
|     docker: 'ubuntu-16.04-xenial-amd64' | ||||
|     name:   'ubuntu_16_04_xenial_amd64' | ||||
| 
 | ||||
| - template: .azure-pipelines/jobs/test-docker.yml | ||||
|   parameters: | ||||
|     docker: 'ubuntu-xenial-amd64' | ||||
|     name:   'ubuntu_xenial_amd64' | ||||
|     docker: 'ubuntu-18.04-bionic-amd64' | ||||
|     name:   'ubuntu_18_04_bionic_amd64' | ||||
| 
 | ||||
| - template: .azure-pipelines/jobs/test-docker.yml | ||||
|   parameters: | ||||
|  |  | |||
|  | @ -79,6 +79,26 @@ PILLOW_VERSION constant | |||
| ``PILLOW_VERSION`` has been deprecated and will be removed in the next | ||||
| major release. Use ``__version__`` instead. | ||||
| 
 | ||||
| ImageCms.CmsProfile attributes | ||||
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | ||||
| 
 | ||||
| .. deprecated:: 3.2.0 | ||||
| 
 | ||||
| Some attributes in ``ImageCms.CmsProfile`` are deprecated. 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`` | ||||
| ========================  =============================== | ||||
| 
 | ||||
| Removed features | ||||
| ---------------- | ||||
| 
 | ||||
|  |  | |||
|  | @ -104,11 +104,11 @@ The :py:meth:`~PIL.Image.Image.open` method sets the following | |||
| Reading sequences | ||||
| ~~~~~~~~~~~~~~~~~ | ||||
| 
 | ||||
| The GIF loader supports the :py:meth:`~file.seek` and :py:meth:`~file.tell` | ||||
| methods. You can seek to the next frame (``im.seek(im.tell() + 1)``), or rewind | ||||
| the file by seeking to the first frame. Random access is not supported. | ||||
| The GIF loader supports the :py:meth:`~PIL.Image.Image.seek` and | ||||
| :py:meth:`~PIL.Image.Image.tell` methods. You can combine these methods | ||||
| to seek to the next frame (``im.seek(im.tell() + 1)``). | ||||
| 
 | ||||
| ``im.seek()`` raises an ``EOFError`` if you try to seek after the last frame. | ||||
| ``im.seek()`` raises an :py:exc:`EOFError` if you try to seek after the last frame. | ||||
| 
 | ||||
| Saving | ||||
| ~~~~~~ | ||||
|  | @ -459,12 +459,14 @@ Pillow reads and writes PCX files containing ``1``, ``L``, ``P``, or ``RGB`` dat | |||
| PNG | ||||
| ^^^ | ||||
| 
 | ||||
| Pillow identifies, reads, and writes PNG files containing ``1``, ``L``, ``P``, | ||||
| ``RGB``, or ``RGBA`` data. Interlaced files are supported as of v1.1.7. | ||||
| Pillow identifies, reads, and writes PNG files containing ``1``, ``L``, ``LA``, | ||||
| ``I``, ``P``, ``RGB`` or ``RGBA`` data. Interlaced files are supported as of | ||||
| v1.1.7. | ||||
| 
 | ||||
| As of Pillow 6.0, EXIF data can be read from 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. | ||||
| image 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. | ||||
| 
 | ||||
| The :py:meth:`~PIL.Image.Image.open` method sets the following | ||||
| :py:attr:`~PIL.Image.Image.info` properties, when appropriate: | ||||
|  | @ -489,12 +491,12 @@ The :py:meth:`~PIL.Image.Image.open` method sets the following | |||
|     For ``P`` images: Either the palette index for full transparent pixels, | ||||
|     or a byte string with alpha values for each palette entry. | ||||
| 
 | ||||
|     For ``L`` and ``RGB`` images, the color that represents full transparent | ||||
|     pixels in this image. | ||||
|     For ``1``, ``L``, ``I`` and ``RGB`` images, the color that represents | ||||
|     full transparent pixels in this image. | ||||
| 
 | ||||
|     This key is omitted if the image is not a transparent palette image. | ||||
| 
 | ||||
| ``Open`` also sets ``Image.text`` to a dictionary of the values of the | ||||
| ``open`` also sets ``Image.text`` to a dictionary of the values of the | ||||
| ``tEXt``, ``zTXt``, and ``iTXt`` chunks of the PNG image. Individual | ||||
| compressed chunks are limited to a decompressed size of | ||||
| ``PngImagePlugin.MAX_TEXT_CHUNK``, by default 1MB, to prevent | ||||
|  | @ -510,8 +512,8 @@ The :py:meth:`~PIL.Image.Image.save` method supports the following options: | |||
|     encoder settings. | ||||
| 
 | ||||
| **transparency** | ||||
|     For ``P``, ``L``, and ``RGB`` images, this option controls what | ||||
|     color image to mark as transparent. | ||||
|     For ``P``, ``1``, ``L``, ``I``, and ``RGB`` images, this option controls | ||||
|     what color from the image to mark as transparent. | ||||
| 
 | ||||
|     For ``P`` images, this can be a either the palette index, | ||||
|     or a byte string with alpha values for each palette entry. | ||||
|  | @ -552,7 +554,7 @@ The :py:meth:`~PIL.Image.Image.save` method supports the following options: | |||
| PPM | ||||
| ^^^ | ||||
| 
 | ||||
| Pillow reads and writes PBM, PGM and PPM files containing ``1``, ``L`` or | ||||
| Pillow reads and writes PBM, PGM, PPM and PNM files containing ``1``, ``L`` or | ||||
| ``RGB`` data. | ||||
| 
 | ||||
| SGI | ||||
|  | @ -568,7 +570,7 @@ Pillow reads and writes SPIDER image files of 32-bit floating point data | |||
| ("F;32F"). | ||||
| 
 | ||||
| Pillow also reads SPIDER stack files containing sequences of SPIDER images. The | ||||
| :py:meth:`~file.seek` and :py:meth:`~file.tell` methods are supported, and | ||||
| :py:meth:`~PIL.Image.Image.seek` and :py:meth:`~PIL.Image.Image.tell` methods are supported, and | ||||
| random access is allowed. | ||||
| 
 | ||||
| The :py:meth:`~PIL.Image.Image.open` method sets the following attributes: | ||||
|  | @ -579,7 +581,7 @@ The :py:meth:`~PIL.Image.Image.open` method sets the following attributes: | |||
| **istack** | ||||
|     Set to 1 if the file is an image stack, else 0. | ||||
| 
 | ||||
| **nimages** | ||||
| **n_frames** | ||||
|     Set to the number of images in the stack. | ||||
| 
 | ||||
| A convenience method, :py:meth:`~PIL.Image.Image.convert2byte`, is provided for | ||||
|  | @ -663,6 +665,17 @@ numbers are returned as a tuple of ``(numerator, denominator)``. | |||
| 
 | ||||
|     .. deprecated:: 3.0.0 | ||||
| 
 | ||||
| Reading Multi-frame TIFF Images | ||||
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | ||||
| 
 | ||||
| The TIFF loader supports the :py:meth:`~PIL.Image.Image.seek` and | ||||
| :py:meth:`~PIL.Image.Image.tell` methods, taking and returning frame numbers | ||||
| within the image file. You can combine these methods to seek to the next frame | ||||
| (``im.seek(im.tell() + 1)``). Frames are numbered from 0 to ``im.num_frames - 1``, | ||||
| and can be accessed in any order. | ||||
| 
 | ||||
| ``im.seek()`` raises an :py:exc:`EOFError` if you try to seek after the | ||||
| last frame. | ||||
| 
 | ||||
| Saving Tiff Images | ||||
| ~~~~~~~~~~~~~~~~~~ | ||||
|  | @ -850,7 +863,7 @@ is commonly used in fax applications. The DCX decoder can read files containing | |||
| ``1``, ``L``, ``P``, or ``RGB`` data. | ||||
| 
 | ||||
| When the file is opened, only the first image is read. You can use | ||||
| :py:meth:`~file.seek` or :py:mod:`~PIL.ImageSequence` to read other images. | ||||
| :py:meth:`~PIL.Image.Image.seek` or :py:mod:`~PIL.ImageSequence` to read other images. | ||||
| 
 | ||||
| 
 | ||||
| DDS | ||||
|  | @ -942,8 +955,8 @@ MIC | |||
| ^^^ | ||||
| 
 | ||||
| Pillow identifies and reads Microsoft Image Composer (MIC) files. When opened, | ||||
| the first sprite in the file is loaded. You can use :py:meth:`~file.seek` and | ||||
| :py:meth:`~file.tell` to read other sprites from the file. | ||||
| the first sprite in the file is loaded. You can use :py:meth:`~PIL.Image.Image.seek` and | ||||
| :py:meth:`~PIL.Image.Image.tell` to read other sprites from the file. | ||||
| 
 | ||||
| Note that there may be an embedded gamma of 2.2 in MIC files. | ||||
| 
 | ||||
|  | @ -951,7 +964,7 @@ MPO | |||
| ^^^ | ||||
| 
 | ||||
| Pillow identifies and reads Multi Picture Object (MPO) files, loading the primary | ||||
| image when first opened. The :py:meth:`~file.seek` and :py:meth:`~file.tell` | ||||
| image when first opened. The :py:meth:`~PIL.Image.Image.seek` and :py:meth:`~PIL.Image.Image.tell` | ||||
| methods may be used to read other pictures from the file. The pictures are | ||||
| zero-indexed and random access is supported. | ||||
| 
 | ||||
|  |  | |||
|  | @ -396,10 +396,6 @@ Reading sequences | |||
| As seen in this example, you’ll get an :py:exc:`EOFError` exception when the | ||||
| sequence ends. | ||||
| 
 | ||||
| Note that most drivers in the current version of the library only allow you to | ||||
| seek to the next frame (as in the above example). To rewind the file, you may | ||||
| have to reopen it. | ||||
| 
 | ||||
| The following class lets you use the for-statement to loop over the sequence: | ||||
| 
 | ||||
| Using the ImageSequence Iterator class | ||||
|  |  | |||
|  | @ -30,8 +30,8 @@ Pillow is the friendly PIL fork by `Alex Clark and Contributors <https://github. | |||
|    :target: https://pypi.org/project/Pillow/ | ||||
|    :alt: Number of PyPI downloads | ||||
| 
 | ||||
| .. image:: https://coveralls.io/repos/python-pillow/Pillow/badge.svg?branch=master&service=github | ||||
|    :target: https://coveralls.io/github/python-pillow/Pillow?branch=master | ||||
| .. image:: https://codecov.io/gh/python-pillow/Pillow/branch/master/graph/badge.svg | ||||
|    :target: https://codecov.io/gh/python-pillow/Pillow | ||||
|    :alt: Code coverage | ||||
| 
 | ||||
| .. toctree:: | ||||
|  |  | |||
|  | @ -151,7 +151,7 @@ Many of Pillow's features require external libraries: | |||
| * **littlecms** provides color management | ||||
| 
 | ||||
|   * Pillow version 2.2.1 and below uses liblcms1, Pillow 2.3.0 and | ||||
|     above uses liblcms2. Tested with **1.19** and **2.7**. | ||||
|     above uses liblcms2. Tested with **1.19** and **2.7-2.9**. | ||||
| 
 | ||||
| * **libwebp** provides the WebP format. | ||||
| 
 | ||||
|  | @ -165,7 +165,7 @@ Many of Pillow's features require external libraries: | |||
| 
 | ||||
|   * Pillow has been tested with openjpeg **2.0.0** and **2.1.0**. | ||||
|   * Pillow does **not** support the earlier **1.5** series which ships | ||||
|     with Ubuntu <= 14.04 and Debian Jessie. | ||||
|     with Debian Jessie. | ||||
| 
 | ||||
| * **libimagequant** provides improved color quantization | ||||
| 
 | ||||
|  | @ -403,10 +403,6 @@ These platforms are built and tested for every change. | |||
| | Ubuntu Linux 16.04 LTS           | 2.7, 3.5, 3.6, 3.7,           |x86-64                 | | ||||
| |                                  | PyPy, PyPy3                   |                       | | ||||
| +----------------------------------+-------------------------------+-----------------------+ | ||||
| | Ubuntu Linux 14.04 LTS           | 2.7, 3.5, 3.6                 |x86-64                 | | ||||
| |                                  +-------------------------------+-----------------------+ | ||||
| |                                  | 2.7                           |x86                    | | ||||
| +----------------------------------+-------------------------------+-----------------------+ | ||||
| | Windows Server 2012 R2           | 2.7, 3.5, 3.6, 3.7            |x86, x86-64            | | ||||
| |                                  +-------------------------------+-----------------------+ | ||||
| |                                  | PyPy, 3.7/MinGW               |x86                    | | ||||
|  |  | |||
|  | @ -132,21 +132,21 @@ can be easily displayed in a chromaticity diagram, for example). | |||
| 
 | ||||
|     .. py:attribute:: manufacturer | ||||
| 
 | ||||
|         The (english) display string for the device manufacturer (see | ||||
|         The (English) display string for the device manufacturer (see | ||||
|         9.2.22 of ICC.1:2010). | ||||
| 
 | ||||
|         :type: :py:class:`unicode` or ``None`` | ||||
| 
 | ||||
|     .. py:attribute:: model | ||||
| 
 | ||||
|         The (english) display string for the device model of the device | ||||
|         The (English) display string for the device model of the device | ||||
|         for which this profile is created (see 9.2.23 of ICC.1:2010). | ||||
| 
 | ||||
|         :type: :py:class:`unicode` or ``None`` | ||||
| 
 | ||||
|     .. py:attribute:: profile_description | ||||
| 
 | ||||
|         The (english) display string for the profile description (see | ||||
|         The (English) display string for the profile description (see | ||||
|         9.2.41 of ICC.1:2010). | ||||
| 
 | ||||
|         :type: :py:class:`unicode` or ``None`` | ||||
|  | @ -269,14 +269,14 @@ can be easily displayed in a chromaticity diagram, for example). | |||
| 
 | ||||
|     .. py:attribute:: viewing_condition | ||||
| 
 | ||||
|         The (english) display string for the viewing conditions (see | ||||
|         The (English) display string for the viewing conditions (see | ||||
|         9.2.48 of ICC.1:2010). | ||||
| 
 | ||||
|         :type: :py:class:`unicode` or ``None`` | ||||
| 
 | ||||
|     .. py:attribute:: screening_description | ||||
| 
 | ||||
|         The (english) display string for the screening conditions. | ||||
|         The (English) display string for the screening conditions. | ||||
| 
 | ||||
|         This tag was available in ICC 3.2, but it is removed from | ||||
|         version 4. | ||||
|  |  | |||
|  | @ -255,7 +255,7 @@ Methods | |||
| 
 | ||||
|     Draw a shape. | ||||
| 
 | ||||
| .. py:method:: PIL.ImageDraw.ImageDraw.text(xy, text, fill=None, font=None, anchor=None, spacing=0, align="left", direction=None, features=None) | ||||
| .. py:method:: PIL.ImageDraw.ImageDraw.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. | ||||
| 
 | ||||
|  | @ -287,7 +287,17 @@ Methods | |||
| 
 | ||||
|                      .. versionadded:: 4.2.0 | ||||
| 
 | ||||
| .. py:method:: PIL.ImageDraw.ImageDraw.multiline_text(xy, text, fill=None, font=None, anchor=None, spacing=0, align="left", 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 | ||||
|                      <https://www.w3.org/International/articles/language-tags/>` | ||||
|                      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 | ||||
|                      <https://www.w3.org/International/articles/language-tags/>` | ||||
|                      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 | ||||
|                      <https://www.w3.org/International/articles/language-tags/>` | ||||
|                      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 | ||||
|                      <https://www.w3.org/International/articles/language-tags/>` | ||||
|                      Requires libraqm. | ||||
| 
 | ||||
|                      .. versionadded:: 6.0.0 | ||||
| 
 | ||||
| .. py:method:: PIL.ImageDraw.getdraw(im=None, hints=None) | ||||
| 
 | ||||
|     .. warning:: This method is experimental. | ||||
|  |  | |||
|  | @ -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 | ||||
|                      <https://www.w3.org/International/articles/language-tags/>` | ||||
|                      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 | ||||
|                      <https://www.w3.org/International/articles/language-tags/>` | ||||
|                      Requires libraqm. | ||||
| 
 | ||||
|                      .. versionadded:: 6.0.0 | ||||
| 
 | ||||
|     :return: An internal PIL storage memory instance as defined by the | ||||
|              :py:mod:`PIL.Image.core` interface module. | ||||
|  |  | |||
|  | @ -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:: | ||||
|  |  | |||
|  | @ -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. | ||||
|  |  | |||
							
								
								
									
										13
									
								
								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 = """ | ||||
|  |  | |||
|  | @ -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 | ||||
|  |  | |||
|  | @ -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: | ||||
|  |  | |||
|  | @ -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 | ||||
|  |  | |||
|  | @ -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) | ||||
| 
 | ||||
|  |  | |||
|  | @ -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") | ||||
|  |  | |||
							
								
								
									
										253
									
								
								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("<H", ifd_data[:2])[0]): | ||||
|                         ifd_tag, typ, count, data = struct.unpack( | ||||
|                             "<HHL4s", ifd_data[i*12 + 2:(i+1)*12 + 2]) | ||||
|                         try: | ||||
|                             unit_size, handler =\ | ||||
|                                 TiffImagePlugin.ImageFileDirectory_v2._load_dispatch[ | ||||
|                                     typ | ||||
|                                 ] | ||||
|                         except KeyError: | ||||
|                             continue | ||||
|                         size = count * unit_size | ||||
|                         if size > 4: | ||||
|                             offset, = struct.unpack("<L", data) | ||||
|                             data = ifd_data[offset-12:offset+size-12] | ||||
|                         else: | ||||
|                             data = data[:size] | ||||
| 
 | ||||
|                         if len(data) != size: | ||||
|                             warnings.warn("Possibly corrupt EXIF MakerNote data.  " | ||||
|                                           "Expecting to read %d bytes but only got %d." | ||||
|                                           " Skipping tag %s" | ||||
|                                           % (size, len(data), ifd_tag)) | ||||
|                             continue | ||||
| 
 | ||||
|                         if not data: | ||||
|                             continue | ||||
| 
 | ||||
|                         makernote[ifd_tag] = handler( | ||||
|                             TiffImagePlugin.ImageFileDirectory_v2(), data, False) | ||||
|                     self._ifds[0x927c] = dict(self._fixup_dict(makernote)) | ||||
|                 elif self._data.get(0x010f) == "Nintendo": | ||||
|                     ifd_data = self._data[0x927c] | ||||
| 
 | ||||
|                     makernote = {} | ||||
|                     for i in range(0, 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)) | ||||
|  |  | |||
|  | @ -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) | ||||
| 
 | ||||
|  |  | |||
|  | @ -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 | ||||
| 
 | ||||
|  |  | |||
|  | @ -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` | ||||
|  |  | |||
|  | @ -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, | ||||
|  |  | |||
|  | @ -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() | ||||
|  |  | |||
|  | @ -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 | ||||
|  |  | |||
|  | @ -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 | ||||
|  |  | |||
|  | @ -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 | ||||
|  |  | |||
|  | @ -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") | ||||
|  |  | |||
|  | @ -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) | ||||
|  |  | |||
|  | @ -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") | ||||
|  |  | |||
|  | @ -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 | ||||
| 
 | ||||
|  |  | |||
|  | @ -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") | ||||
|  |  | |||
|  | @ -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 | ||||
|  |  | |||
|  | @ -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: | ||||
|  |  | |||