This commit is contained in:
Riley Lahd 2019-04-09 08:24:52 -06:00
commit 5fb36d2de4
119 changed files with 2054 additions and 954 deletions

View File

@ -52,7 +52,7 @@ install:
} }
else 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 c:\pillow\winbuild\build_deps.cmd
$host.SetShouldExit(0) $host.SetShouldExit(0)
} }

View File

@ -9,7 +9,7 @@ Please send a pull request to the master branch. Please include [documentation](
- Fork the Pillow repository. - Fork the Pillow repository.
- Create a branch from master. - Create a branch from master.
- Develop bug fixes, features, tests, etc. - 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. - Create a pull request to pull the changes from your branch to the Pillow master.
### Guidelines ### Guidelines

View File

@ -18,41 +18,27 @@ matrix:
env: LINT="true" env: LINT="true"
- python: "pypy2.7-6.0" - python: "pypy2.7-6.0"
name: "PyPy2 Xenial" name: "PyPy2 Xenial"
dist: xenial
- python: "pypy3.5-6.0" - python: "pypy3.5-6.0"
name: "PyPy3 Xenial" name: "PyPy3 Xenial"
dist: xenial
- python: '3.7' - python: '3.7'
name: "3.7 Xenial" name: "3.7 Xenial"
- python: '2.7' - python: '2.7'
name: "2.7 Xenial" name: "2.7 Xenial"
- python: '2.7'
name: "2.7 Trusty"
dist: trusty
- python: "2.7_with_system_site_packages" # For PyQt4 - python: "2.7_with_system_site_packages" # For PyQt4
name: "2.7_with_system_site_packages Xenial" name: "2.7_with_system_site_packages Xenial"
services: xvfb services: xvfb
- python: "2.7_with_system_site_packages" # For PyQt4
name: "2.7_with_system_site_packages Trusty"
dist: trusty
- python: '3.6' - python: '3.6'
name: "3.6 Xenial" name: "3.6 Xenial PYTHONOPTIMIZE=1"
- python: '3.6'
name: "3.6 Trusty PYTHONOPTIMIZE=1"
dist: trusty
env: PYTHONOPTIMIZE=1 env: PYTHONOPTIMIZE=1
- python: '3.5' - python: '3.5'
name: "3.5 Xenial" name: "3.5 Xenial PYTHONOPTIMIZE=2"
- python: '3.5'
name: "3.5 Trusty PYTHONOPTIMIZE=2"
dist: trusty
env: PYTHONOPTIMIZE=2 env: PYTHONOPTIMIZE=2
- python: "3.8-dev" - python: "3.8-dev"
name: "3.8-dev Xenial" name: "3.8-dev Xenial"
- env: DOCKER="alpine" DOCKER_TAG="master" - env: DOCKER="alpine" DOCKER_TAG="master"
- env: DOCKER="arch" DOCKER_TAG="master" # contains PyQt5 - env: DOCKER="arch" DOCKER_TAG="master" # contains PyQt5
- env: DOCKER="ubuntu-trusty-x86" DOCKER_TAG="master" - env: DOCKER="ubuntu-16.04-xenial-amd64" DOCKER_TAG="master"
- env: DOCKER="ubuntu-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="debian-stretch-x86" DOCKER_TAG="master"
- env: DOCKER="centos-6-amd64" DOCKER_TAG="master" - env: DOCKER="centos-6-amd64" DOCKER_TAG="master"
- env: DOCKER="centos-7-amd64" DOCKER_TAG="master" - env: DOCKER="centos-7-amd64" DOCKER_TAG="master"
@ -75,14 +61,6 @@ install:
.travis/install.sh; .travis/install.sh;
fi 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: script:
- | - |
if [ "$LINT" == "true" ]; then if [ "$LINT" == "true" ]; then

View File

@ -2,12 +2,102 @@
Changelog (Pillow) 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 - Python 2.7 support will be removed in Pillow 7.0.0 #3682
[hugovk] [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 - Add EXIF support for PNG #3674
[radarhere] [radarhere]
@ -38,7 +128,7 @@ Changelog (Pillow)
- Make ContainerIO.isatty() return a bool, not int #3568 - Make ContainerIO.isatty() return a bool, not int #3568
[jdufresne] [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] [radarhere]
- Deprecate support for PyQt4 and PySide #3655 - Deprecate support for PyQt4 and PySide #3655

View File

@ -20,6 +20,30 @@ Pillow is the friendly PIL fork by `Alex Clark and Contributors <https://github.
* - social * - social
- |gitter| |twitter| - |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 .. |docs| image:: https://readthedocs.org/projects/pillow/badge/?version=latest
:target: https://pillow.readthedocs.io/?badge=latest :target: https://pillow.readthedocs.io/?badge=latest
:alt: Documentation Status :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 :target: https://ci.appveyor.com/project/python-pillow/Pillow
:alt: AppVeyor CI build status (Windows) :alt: AppVeyor CI build status (Windows)
.. |coverage| image:: https://coveralls.io/repos/python-pillow/Pillow/badge.svg?branch=master&service=github .. |coverage| image:: https://codecov.io/gh/python-pillow/Pillow/branch/master/graph/badge.svg
:target: https://coveralls.io/github/python-pillow/Pillow?branch=master :target: https://codecov.io/gh/python-pillow/Pillow
:alt: Code coverage :alt: Code coverage
.. |zenodo| image:: https://zenodo.org/badge/17549/python-pillow/Pillow.svg .. |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 .. |twitter| image:: https://img.shields.io/badge/tweet-on%20Twitter-00aced.svg
:target: https://twitter.com/PythonPillow :target: https://twitter.com/PythonPillow
:alt: Follow on 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>`_

View File

@ -102,4 +102,4 @@ Released as needed privately to individual vendors for critical security-related
## Documentation ## 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 612 B

Binary file not shown.

BIN
Tests/images/fujifilm.mpo Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 MiB

BIN
Tests/images/hopper.pnm Normal file

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
Tests/images/i_trns.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 117 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 777 B

View File

@ -71,6 +71,27 @@ class TestFileBmp(PillowTestCase):
self.assertEqual(im.size, reloaded.size) self.assertEqual(im.size, reloaded.size)
self.assertEqual(reloaded.format, "JPEG") 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): def test_load_dib(self):
# test for #1293, Imagegrab returning Unsupported Bitfields Format # test for #1293, Imagegrab returning Unsupported Bitfields Format
im = Image.open('Tests/images/clipboard.dib') im = Image.open('Tests/images/clipboard.dib')
@ -90,3 +111,15 @@ class TestFileBmp(PillowTestCase):
self.assertEqual(reloaded.format, "DIB") self.assertEqual(reloaded.format, "DIB")
self.assertEqual(reloaded.get_format_mimetype(), "image/bmp") self.assertEqual(reloaded.get_format_mimetype(), "image/bmp")
self.assert_image_equal(im, reloaded) 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)

View File

@ -230,6 +230,15 @@ class TestFileGif(PillowTestCase):
self.assertEqual(im.info, info) 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): def test_n_frames(self):
for path, n_frames in [ for path, n_frames in [
[TEST_GIF, 1], [TEST_GIF, 1],

View File

@ -14,6 +14,7 @@ class TestFileIco(PillowTestCase):
self.assertEqual(im.mode, "RGBA") self.assertEqual(im.mode, "RGBA")
self.assertEqual(im.size, (16, 16)) self.assertEqual(im.size, (16, 16))
self.assertEqual(im.format, "ICO") self.assertEqual(im.format, "ICO")
self.assertEqual(im.get_format_mimetype(), "image/x-icon")
def test_invalid_file(self): def test_invalid_file(self):
with open("Tests/images/flower.jpg", "rb") as fp: with open("Tests/images/flower.jpg", "rb") as fp:

View File

@ -524,6 +524,27 @@ class TestFileJpeg(PillowTestCase):
reloaded.load() reloaded.load()
self.assertEqual(im.info['dpi'], reloaded.info['dpi']) 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): def test_dpi_tuple_from_exif(self):
# Arrange # Arrange
# This Photoshop CC 2017 image has DPI in EXIF not metadata # This Photoshop CC 2017 image has DPI in EXIF not metadata
@ -590,6 +611,15 @@ class TestFileJpeg(PillowTestCase):
# Act / Assert # Act / Assert
self.assertEqual(im._getexif()[306], '2017:03:13 23:03:09') 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") @unittest.skipUnless(sys.platform.startswith('win32'), "Windows only")
class TestFileCloseW32(PillowTestCase): class TestFileCloseW32(PillowTestCase):

View File

@ -716,3 +716,10 @@ class TestFileLibTiff(LibTiffTestCase):
im = Image.open(infile) im = Image.open(infile)
self.assert_image_similar_tofile(im, "Tests/images/flower.jpg", 0.5) 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")

View File

@ -55,6 +55,27 @@ class TestFileMpo(PillowTestCase):
self.assertEqual(info[296], 2) self.assertEqual(info[296], 2)
self.assertEqual(info[34665], 188) 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): def test_mp(self):
for test_file in test_files: for test_file in test_files:
im = Image.open(test_file) im = Image.open(test_file)

View File

@ -13,6 +13,7 @@ class TestFilePcx(PillowTestCase):
self.assertEqual(im2.mode, im.mode) self.assertEqual(im2.mode, im.mode)
self.assertEqual(im2.size, im.size) self.assertEqual(im2.size, im.size)
self.assertEqual(im2.format, "PCX") self.assertEqual(im2.format, "PCX")
self.assertEqual(im2.get_format_mimetype(), "image/x-pcx")
self.assert_image_equal(im2, im) self.assert_image_equal(im2, im)
def test_sanity(self): def test_sanity(self):

View File

@ -88,20 +88,13 @@ class TestFilePng(PillowTestCase):
self.assertEqual(im.format, "PNG") self.assertEqual(im.format, "PNG")
self.assertEqual(im.get_format_mimetype(), 'image/png') self.assertEqual(im.get_format_mimetype(), 'image/png')
hopper("1").save(test_file) for mode in ["1", "L", "P", "RGB", "I", "I;16"]:
Image.open(test_file) im = hopper(mode)
im.save(test_file)
hopper("L").save(test_file) reloaded = Image.open(test_file)
Image.open(test_file) if mode == "I;16":
reloaded = reloaded.convert(mode)
hopper("P").save(test_file) self.assert_image_equal(reloaded, im)
Image.open(test_file)
hopper("RGB").save(test_file)
Image.open(test_file)
hopper("I").save(test_file)
Image.open(test_file)
def test_invalid_file(self): def test_invalid_file(self):
invalid_file = "Tests/images/flower.jpg" invalid_file = "Tests/images/flower.jpg"
@ -298,13 +291,15 @@ class TestFilePng(PillowTestCase):
self.assert_image(im, "RGBA", (10, 10)) self.assert_image(im, "RGBA", (10, 10))
self.assertEqual(im.getcolors(), [(100, (0, 0, 0, 0))]) self.assertEqual(im.getcolors(), [(100, (0, 0, 0, 0))])
def test_save_l_transparency(self): def test_save_greyscale_transparency(self):
# There are 559 transparent pixels in l_trns.png. for mode, num_transparent in {
num_transparent = 559 "1": 1994,
"L": 559,
in_file = "Tests/images/l_trns.png" "I": 559,
}.items():
in_file = "Tests/images/"+mode.lower()+"_trns.png"
im = Image.open(in_file) im = Image.open(in_file)
self.assertEqual(im.mode, "L") self.assertEqual(im.mode, mode)
self.assertEqual(im.info["transparency"], 255) self.assertEqual(im.info["transparency"], 255)
im_rgba = im.convert('RGBA') im_rgba = im.convert('RGBA')
@ -315,7 +310,7 @@ class TestFilePng(PillowTestCase):
im.save(test_file) im.save(test_file)
test_im = Image.open(test_file) test_im = Image.open(test_file)
self.assertEqual(test_im.mode, "L") self.assertEqual(test_im.mode, mode)
self.assertEqual(test_im.info["transparency"], 255) self.assertEqual(test_im.info["transparency"], 255)
self.assert_image_equal(im, test_im) self.assert_image_equal(im, test_im)
@ -394,6 +389,24 @@ class TestFilePng(PillowTestCase):
im = roundtrip(im, dpi=(100, 100)) im = roundtrip(im, dpi=(100, 100))
self.assertEqual(im.info["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): def test_roundtrip_text(self):
# Check text roundtripping # Check text roundtripping

View File

@ -1,4 +1,4 @@
from .helper import PillowTestCase from .helper import PillowTestCase, hopper
from PIL import Image from PIL import Image
@ -36,6 +36,16 @@ class TestFilePpm(PillowTestCase):
reloaded = Image.open(f) reloaded = Image.open(f)
self.assert_image_equal(im, reloaded) 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): def test_truncated_file(self):
path = self.tempfile('temp.pgm') path = self.tempfile('temp.pgm')
with open(path, 'w') as f: with open(path, 'w') as f:

View File

@ -37,6 +37,8 @@ class TestFileTga(PillowTestCase):
path_no_ext, origin, "rle" if rle else "raw") path_no_ext, origin, "rle" if rle else "raw")
original_im = Image.open(tga_path) original_im = Image.open(tga_path)
self.assertEqual(original_im.format, "TGA")
self.assertEqual(original_im.get_format_mimetype(), "image/x-tga")
if rle: if rle:
self.assertEqual( self.assertEqual(
original_im.info["compression"], "tga_rle") original_im.info["compression"], "tga_rle")

View File

@ -126,6 +126,30 @@ class TestFileTiff(PillowTestCase):
im._setup() im._setup()
self.assertEqual(im.info['dpi'], (71., 71.)) 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): def test_save_setting_missing_resolution(self):
b = BytesIO() b = BytesIO()
Image.open("Tests/images/10ct_32bit_128.tiff").save( 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-lastframe.tif', 1],
['Tests/images/multipage.tiff', 3] ['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) im = Image.open(path)
self.assertEqual(im.n_frames, n_frames) self.assertEqual(im.n_frames, n_frames)
self.assertEqual(im.is_animated, n_frames != 1) self.assertEqual(im.is_animated, n_frames != 1)
@ -263,6 +282,11 @@ class TestFileTiff(PillowTestCase):
self.assertEqual(im.size, (10, 10)) self.assertEqual(im.size, (10, 10))
self.assertEqual(im.convert('RGB').getpixel((0, 0)), (255, 0, 0)) 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.seek(2)
im.load() im.load()
self.assertEqual(im.size, (20, 20)) self.assertEqual(im.size, (20, 20))

View File

@ -45,6 +45,15 @@ class TestFileWmf(PillowTestCase):
# Restore the state before this test # Restore the state before this test
WmfImagePlugin.register_handler(None) 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): def test_save(self):
im = hopper() im = hopper()

View File

@ -3,6 +3,8 @@ from .helper import unittest, PillowTestCase, hopper
from PIL import Image from PIL import Image
from PIL._util import py3 from PIL._util import py3
import os import os
import sys
import shutil
class TestImage(PillowTestCase): class TestImage(PillowTestCase):
@ -121,6 +123,16 @@ class TestImage(PillowTestCase):
im.paste(0, (0, 0, 100, 100)) im.paste(0, (0, 0, 100, 100))
self.assertFalse(im.readonly) 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): def test_dump(self):
im = Image.new("L", (10, 10)) im = Image.new("L", (10, 10))
im._dump(self.tempfile("temp_L.ppm")) 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_p, ImagePalette.ImagePalette())
_make_new(im, blank_pa, 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): def test_no_resource_warning_on_save(self):
# https://github.com/python-pillow/Pillow/issues/835 # https://github.com/python-pillow/Pillow/issues/835
# Arrange # Arrange
@ -532,6 +554,18 @@ class TestImage(PillowTestCase):
with Image.open(test_file) as im: with Image.open(test_file) as im:
self.assert_warning(None, im.save, temp_file) 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): class MockEncoder(object):
pass pass

View File

@ -12,7 +12,8 @@ class TestImageConvert(PillowTestCase):
self.assertEqual(out.mode, mode) self.assertEqual(out.mode, mode)
self.assertEqual(out.size, im.size) 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: for mode in modes:
im = hopper(mode) im = hopper(mode)

View File

@ -28,3 +28,10 @@ class TestImageLoad(PillowTestCase):
os.fstat(fn) os.fstat(fn)
self.assertRaises(OSError, 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)

View File

@ -42,7 +42,7 @@ class TestImageMode(PillowTestCase):
self.assertEqual(signature, result) self.assertEqual(signature, result)
check("1", "L", "L", 1, ("1",)) check("1", "L", "L", 1, ("1",))
check("L", "L", "L", 1, ("L",)) 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("I", "L", "I", 1, ("I",))
check("F", "L", "F", 1, ("F",)) check("F", "L", "F", 1, ("F",))
check("RGB", "RGB", "L", 3, ("R", "G", "B")) check("RGB", "RGB", "L", 3, ("R", "G", "B"))

View File

@ -28,6 +28,13 @@ class TestImagePutAlpha(PillowTestCase):
self.assertEqual(im.mode, 'LA') self.assertEqual(im.mode, 'LA')
self.assertEqual(im.getpixel((0, 0)), (1, 2)) 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)) im = Image.new("RGB", (1, 1), (1, 2, 3))
self.assertEqual(im.getpixel((0, 0)), (1, 2, 3)) self.assertEqual(im.getpixel((0, 0)), (1, 2, 3))

View File

@ -14,8 +14,10 @@ class TestImagePutPalette(PillowTestCase):
return im.mode, p[:10] return im.mode, p[:10]
return im.mode return im.mode
self.assertRaises(ValueError, palette, "1") self.assertRaises(ValueError, palette, "1")
self.assertEqual(palette("L"), ("P", [0, 1, 2, 3, 4, 5, 6, 7, 8, 9])) for mode in ["L", "LA", "P", "PA"]:
self.assertEqual(palette("P"), ("P", [0, 1, 2, 3, 4, 5, 6, 7, 8, 9])) 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, "I")
self.assertRaises(ValueError, palette, "F") self.assertRaises(ValueError, palette, "F")
self.assertRaises(ValueError, palette, "RGB") self.assertRaises(ValueError, palette, "RGB")

View File

@ -38,9 +38,10 @@ class TestImageQuantize(PillowTestCase):
def test_rgba_quantize(self): def test_rgba_quantize(self):
image = hopper('RGBA') image = hopper('RGBA')
image.quantize()
self.assertRaises(ValueError, image.quantize, method=0) self.assertRaises(ValueError, image.quantize, method=0)
self.assertEqual(image.quantize().convert().mode, "RGBA")
def test_quantize(self): def test_quantize(self):
image = Image.open('Tests/images/caption_6_33_22.png').convert('RGB') image = Image.open('Tests/images/caption_6_33_22.png').convert('RGB')
converted = image.quantize() converted = image.quantize()

View File

@ -24,7 +24,7 @@ class TestImageTranspose(PillowTestCase):
self.assertEqual(im.getpixel((1, y-2)), out.getpixel((x-2, y-2))) 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))) 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) transpose(mode)
def test_flip_top_bottom(self): 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((1, y-2)), out.getpixel((1, 1)))
self.assertEqual(im.getpixel((x-2, y-2)), out.getpixel((x-2, 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) transpose(mode)
def test_rotate_90(self): 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((1, y-2)), out.getpixel((y-2, x-2)))
self.assertEqual(im.getpixel((x-2, y-2)), out.getpixel((y-2, 1))) 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) transpose(mode)
def test_rotate_180(self): 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((1, y-2)), out.getpixel((x-2, 1)))
self.assertEqual(im.getpixel((x-2, y-2)), out.getpixel((1, 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) transpose(mode)
def test_rotate_270(self): 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((1, y-2)), out.getpixel((1, 1)))
self.assertEqual(im.getpixel((x-2, y-2)), out.getpixel((1, x-2))) 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) transpose(mode)
def test_transpose(self): 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((1, y-2)), out.getpixel((y-2, 1)))
self.assertEqual(im.getpixel((x-2, y-2)), out.getpixel((y-2, x-2))) 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) transpose(mode)
def test_tranverse(self): def test_tranverse(self):
@ -120,11 +120,12 @@ class TestImageTranspose(PillowTestCase):
self.assertEqual(im.getpixel((1, y-2)), out.getpixel((1, x-2))) self.assertEqual(im.getpixel((1, y-2)), out.getpixel((1, x-2)))
self.assertEqual(im.getpixel((x-2, y-2)), out.getpixel((1, 1))) self.assertEqual(im.getpixel((x-2, y-2)), out.getpixel((1, 1)))
for mode in ("L", "RGB"): for mode in self.hopper:
transpose(mode) transpose(mode)
def test_roundtrip(self): def test_roundtrip(self):
im = self.hopper['L'] for mode in self.hopper:
im = self.hopper[mode]
def transpose(first, second): def transpose(first, second):
return im.transpose(first).transpose(second) return im.transpose(first).transpose(second)

View File

@ -309,7 +309,7 @@ class TestImageCms(PillowTestCase):
2: (False, False, True), 2: (False, False, True),
3: (False, False, True) 3: (False, False, True)
}) })
self.assertEqual(p.color_space, 'RGB')
self.assertIsNone(p.colorant_table) self.assertIsNone(p.colorant_table)
self.assertIsNone(p.colorant_table_out) self.assertIsNone(p.colorant_table_out)
self.assertIsNone(p.colorimetric_intent) self.assertIsNone(p.colorimetric_intent)
@ -361,16 +361,9 @@ class TestImageCms(PillowTestCase):
(5000.722328847392,)) (5000.722328847392,))
self.assertEqual(p.model, self.assertEqual(p.model,
'IEC 61966-2-1 Default RGB Colour Space - sRGB') 'IEC 61966-2-1 Default RGB Colour Space - sRGB')
self.assertEqual(p.pcs, 'XYZ')
self.assertIsNone(p.perceptual_rendering_intent_gamut) 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( self.assertEqual(
p.profile_description, 'sRGB IEC61966-2-1 black scaled') p.profile_description, 'sRGB IEC61966-2-1 black scaled')
self.assertEqual( self.assertEqual(
@ -393,6 +386,40 @@ class TestImageCms(PillowTestCase):
'Reference Viewing Condition in IEC 61966-2-1') 'Reference Viewing Condition in IEC 61966-2-1')
self.assertEqual(p.xcolor_space, 'RGB ') 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): def test_profile_typesafety(self):
""" Profile init type safety """ Profile init type safety

View File

@ -1,4 +1,4 @@
from .helper import PillowTestCase, hopper, fromstring, tostring from .helper import unittest, PillowTestCase, hopper, fromstring, tostring
from io import BytesIO from io import BytesIO
@ -6,6 +6,12 @@ from PIL import Image
from PIL import ImageFile from PIL import ImageFile
from PIL import EpsImagePlugin from PIL import EpsImagePlugin
try:
from PIL import _webp
HAVE_WEBP = True
except ImportError:
HAVE_WEBP = False
codecs = dir(Image.core) codecs = dir(Image.core)
@ -233,3 +239,97 @@ class TestPyDecoder(PillowTestCase):
im = MockImageFile(buf) im = MockImageFile(buf)
self.assertIsNone(im.format) self.assertIsNone(im.format)
self.assertIsNone(im.get_format_mimetype()) 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,
})

View File

@ -6,6 +6,8 @@ from io import BytesIO
import os import os
import sys import sys
import copy import copy
import re
import distutils.version
FONT_PATH = "Tests/fonts/FreeMono.ttf" FONT_PATH = "Tests/fonts/FreeMono.ttf"
FONT_SIZE = 20 FONT_SIZE = 20
@ -49,29 +51,40 @@ class TestImageFont(PillowTestCase):
# Freetype has different metrics depending on the version. # Freetype has different metrics depending on the version.
# (and, other things, but first things first) # (and, other things, but first things first)
METRICS = { METRICS = {
('2', '3'): {'multiline': 30, ('>=2.3', '<2.4'): {
'multiline': 30,
'textsize': 12, 'textsize': 12,
'getters': (13, 16)}, 'getters': (13, 16)},
('2', '7'): {'multiline': 6.2, ('>=2.7',): {
'multiline': 6.2,
'textsize': 2.5, 'textsize': 2.5,
'getters': (12, 16)}, 'getters': (12, 16)},
('2', '8'): {'multiline': 6.2, 'Default': {
'textsize': 2.5, 'multiline': 0.5,
'getters': (12, 16)},
('2', '9'): {'multiline': 6.2,
'textsize': 2.5,
'getters': (12, 16)},
'Default': {'multiline': 0.5,
'textsize': 0.5, 'textsize': 0.5,
'getters': (12, 16)}, 'getters': (12, 16)},
} }
def setUp(self): def setUp(self):
freetype_version = tuple( freetype = distutils.version.StrictVersion(ImageFont.core.freetype2_version)
ImageFont.core.freetype2_version.split('.')
)[:2] self.metrics = self.METRICS['Default']
self.metrics = self.METRICS.get(freetype_version, for conditions, metrics in self.METRICS.items():
self.METRICS['Default']) 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): def get_font(self):
return ImageFont.truetype(FONT_PATH, FONT_SIZE, 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\nA'), (36, 36))
self.assertEqual(t.getsize_multiline('ABC\nAaaa'), (48, 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") @unittest.skipUnless(HAS_RAQM, "Raqm not Available")
class TestImageFont_RaqmLayout(TestImageFont): class TestImageFont_RaqmLayout(TestImageFont):

View File

@ -130,3 +130,16 @@ class TestImagecomplextext(PillowTestCase):
target_img = Image.open(target) target_img = Image.open(target)
self.assert_image_similar(im, target_img, .5) 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)

View File

@ -1,7 +1,13 @@
from .helper import PillowTestCase, hopper from .helper import PillowTestCase, hopper
from PIL import ImageOps
from PIL import Image from PIL import Image
from PIL import ImageOps
try:
from PIL import _webp
HAVE_WEBP = True
except ImportError:
HAVE_WEBP = False
class TestImageOps(PillowTestCase): class TestImageOps(PillowTestCase):
@ -62,6 +68,9 @@ class TestImageOps(PillowTestCase):
ImageOps.solarize(hopper("L")) ImageOps.solarize(hopper("L"))
ImageOps.solarize(hopper("RGB")) ImageOps.solarize(hopper("RGB"))
ImageOps.exif_transpose(hopper("L"))
ImageOps.exif_transpose(hopper("RGB"))
def test_1pxfit(self): def test_1pxfit(self):
# Division by zero in equalize if image is 1 pixel high # Division by zero in equalize if image is 1 pixel high
newimg = ImageOps.fit(hopper("RGB").resize((1, 1)), (35, 35)) newimg = ImageOps.fit(hopper("RGB").resize((1, 1)), (35, 35))
@ -218,3 +227,36 @@ class TestImageOps(PillowTestCase):
(0, 127, 0), (0, 127, 0),
threshold=1, threshold=1,
msg='white test pixel incorrect') 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)

View File

@ -113,11 +113,6 @@ class TestNumpy(PillowTestCase):
img = Image.fromarray(arr * 255).convert('1') img = Image.fromarray(arr * 255).convert('1')
self.assertEqual(img.mode, '1') self.assertEqual(img.mode, '1')
arr_back = numpy.array(img) 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): def test_save_tiff_uint16(self):
@ -143,21 +138,21 @@ class TestNumpy(PillowTestCase):
np_img = numpy.array(img) np_img = numpy.array(img)
self._test_img_equals_nparray(img, np_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'), modes = [("L", numpy.uint8),
("I", 'int32'), ("I", numpy.int32),
("F", 'float32'), ("F", numpy.float32),
("LA", 'uint8'), ("LA", numpy.uint8),
("RGB", 'uint8'), ("RGB", numpy.uint8),
("RGBA", 'uint8'), ("RGBA", numpy.uint8),
("RGBX", 'uint8'), ("RGBX", numpy.uint8),
("CMYK", 'uint8'), ("CMYK", numpy.uint8),
("YCbCr", 'uint8'), ("YCbCr", numpy.uint8),
("I;16", '<u2'), ("I;16", '<u2'),
("I;16B", '>u2'), ("I;16B", '>u2'),
("I;16L", '<u2'), ("I;16L", '<u2'),
("HSV", 'uint8'), ("HSV", numpy.uint8),
] ]
for mode in modes: for mode in modes:
@ -167,7 +162,7 @@ class TestNumpy(PillowTestCase):
# see https://github.com/python-pillow/Pillow/issues/439 # see https://github.com/python-pillow/Pillow/issues/439
data = list(range(256))*3 data = list(range(256))*3
lut = numpy.array(data, dtype='uint8') lut = numpy.array(data, dtype=numpy.uint8)
im = hopper() im = hopper()

View File

@ -10,7 +10,6 @@ except ImportError:
@unittest.skipIf(pyroma is None, "Pyroma is not installed") @unittest.skipIf(pyroma is None, "Pyroma is not installed")
class TestPyroma(PillowTestCase): class TestPyroma(PillowTestCase):
def test_pyroma(self): def test_pyroma(self):
# Arrange # Arrange
data = pyroma.projectdata.get_data(".") data = pyroma.projectdata.get_data(".")
@ -19,12 +18,13 @@ class TestPyroma(PillowTestCase):
rating = pyroma.ratings.rate(data) rating = pyroma.ratings.rate(data)
# Assert # Assert
if 'rc' in __version__: if "rc" in __version__:
# Pyroma needs to chill about RC versions # Pyroma needs to chill about RC versions and not kill all our tests.
# and not kill all our tests. self.assertEqual(
self.assertEqual(rating, (9, [ rating,
"The package's version number does not comply with PEP-386."])) (9, ["The package's version number does not comply with PEP-386."]),
)
else: else:
# Should have a perfect score # Should have a near-perfect score
self.assertEqual(rating, (10, [])) self.assertEqual(rating, (9, ["Your package does not have license data."]))

View File

@ -11,17 +11,6 @@ class TestFromQPixmap(PillowQPixmapTestCase, PillowTestCase):
# Qt saves all pixmaps as rgb # Qt saves all pixmaps as rgb
self.assert_image_equal(result, expected.convert('RGB')) self.assert_image_equal(result, expected.convert('RGB'))
def test_sanity_1(self): def test_sanity(self):
self.roundtrip(hopper('1')) for mode in ('1', 'RGB', 'RGBA', 'L', 'P'):
self.roundtrip(hopper(mode))
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'))

View File

@ -23,13 +23,13 @@ jobs:
- template: .azure-pipelines/jobs/test-docker.yml - template: .azure-pipelines/jobs/test-docker.yml
parameters: parameters:
docker: 'ubuntu-trusty-x86' docker: 'ubuntu-16.04-xenial-amd64'
name: 'ubuntu_trusty_x86' name: 'ubuntu_16_04_xenial_amd64'
- template: .azure-pipelines/jobs/test-docker.yml - template: .azure-pipelines/jobs/test-docker.yml
parameters: parameters:
docker: 'ubuntu-xenial-amd64' docker: 'ubuntu-18.04-bionic-amd64'
name: 'ubuntu_xenial_amd64' name: 'ubuntu_18_04_bionic_amd64'
- template: .azure-pipelines/jobs/test-docker.yml - template: .azure-pipelines/jobs/test-docker.yml
parameters: parameters:

View File

@ -79,6 +79,26 @@ PILLOW_VERSION constant
``PILLOW_VERSION`` has been deprecated and will be removed in the next ``PILLOW_VERSION`` has been deprecated and will be removed in the next
major release. Use ``__version__`` instead. 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 Removed features
---------------- ----------------

View File

@ -104,11 +104,11 @@ The :py:meth:`~PIL.Image.Image.open` method sets the following
Reading sequences Reading sequences
~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~
The GIF loader supports the :py:meth:`~file.seek` and :py:meth:`~file.tell` The GIF loader supports the :py:meth:`~PIL.Image.Image.seek` and
methods. You can seek to the next frame (``im.seek(im.tell() + 1)``), or rewind :py:meth:`~PIL.Image.Image.tell` methods. You can combine these methods
the file by seeking to the first frame. Random access is not supported. 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 Saving
~~~~~~ ~~~~~~
@ -459,12 +459,14 @@ Pillow reads and writes PCX files containing ``1``, ``L``, ``P``, or ``RGB`` dat
PNG PNG
^^^ ^^^
Pillow identifies, reads, and writes PNG files containing ``1``, ``L``, ``P``, Pillow identifies, reads, and writes PNG files containing ``1``, ``L``, ``LA``,
``RGB``, or ``RGBA`` data. Interlaced files are supported as of v1.1.7. ``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 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 image formats, EXIF data is not guaranteed to be present in
:py:meth:`~PIL.Image.Image.load` has been called. :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 The :py:meth:`~PIL.Image.Image.open` method sets the following
:py:attr:`~PIL.Image.Image.info` properties, when appropriate: :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, For ``P`` images: Either the palette index for full transparent pixels,
or a byte string with alpha values for each palette entry. or a byte string with alpha values for each palette entry.
For ``L`` and ``RGB`` images, the color that represents full transparent For ``1``, ``L``, ``I`` and ``RGB`` images, the color that represents
pixels in this image. full transparent pixels in this image.
This key is omitted if the image is not a transparent palette 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 ``tEXt``, ``zTXt``, and ``iTXt`` chunks of the PNG image. Individual
compressed chunks are limited to a decompressed size of compressed chunks are limited to a decompressed size of
``PngImagePlugin.MAX_TEXT_CHUNK``, by default 1MB, to prevent ``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. encoder settings.
**transparency** **transparency**
For ``P``, ``L``, and ``RGB`` images, this option controls what For ``P``, ``1``, ``L``, ``I``, and ``RGB`` images, this option controls
color image to mark as transparent. what color from the image to mark as transparent.
For ``P`` images, this can be a either the palette index, For ``P`` images, this can be a either the palette index,
or a byte string with alpha values for each palette entry. 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 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. ``RGB`` data.
SGI SGI
@ -568,7 +570,7 @@ Pillow reads and writes SPIDER image files of 32-bit floating point data
("F;32F"). ("F;32F").
Pillow also reads SPIDER stack files containing sequences of SPIDER images. The 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. random access is allowed.
The :py:meth:`~PIL.Image.Image.open` method sets the following attributes: 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** **istack**
Set to 1 if the file is an image stack, else 0. Set to 1 if the file is an image stack, else 0.
**nimages** **n_frames**
Set to the number of images in the stack. Set to the number of images in the stack.
A convenience method, :py:meth:`~PIL.Image.Image.convert2byte`, is provided for 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 .. 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 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. ``1``, ``L``, ``P``, or ``RGB`` data.
When the file is opened, only the first image is read. You can use 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 DDS
@ -942,8 +955,8 @@ MIC
^^^ ^^^
Pillow identifies and reads Microsoft Image Composer (MIC) files. When opened, 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 the first sprite in the file is loaded. You can use :py:meth:`~PIL.Image.Image.seek` and
:py:meth:`~file.tell` to read other sprites from the file. :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. 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 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 methods may be used to read other pictures from the file. The pictures are
zero-indexed and random access is supported. zero-indexed and random access is supported.

View File

@ -396,10 +396,6 @@ Reading sequences
As seen in this example, youll get an :py:exc:`EOFError` exception when the As seen in this example, youll get an :py:exc:`EOFError` exception when the
sequence ends. 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: The following class lets you use the for-statement to loop over the sequence:
Using the ImageSequence Iterator class Using the ImageSequence Iterator class

View File

@ -30,8 +30,8 @@ Pillow is the friendly PIL fork by `Alex Clark and Contributors <https://github.
:target: https://pypi.org/project/Pillow/ :target: https://pypi.org/project/Pillow/
:alt: Number of PyPI downloads :alt: Number of PyPI downloads
.. image:: https://coveralls.io/repos/python-pillow/Pillow/badge.svg?branch=master&service=github .. image:: https://codecov.io/gh/python-pillow/Pillow/branch/master/graph/badge.svg
:target: https://coveralls.io/github/python-pillow/Pillow?branch=master :target: https://codecov.io/gh/python-pillow/Pillow
:alt: Code coverage :alt: Code coverage
.. toctree:: .. toctree::

View File

@ -151,7 +151,7 @@ Many of Pillow's features require external libraries:
* **littlecms** provides color management * **littlecms** provides color management
* Pillow version 2.2.1 and below uses liblcms1, Pillow 2.3.0 and * 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. * **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 has been tested with openjpeg **2.0.0** and **2.1.0**.
* Pillow does **not** support the earlier **1.5** series which ships * 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 * **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 | | Ubuntu Linux 16.04 LTS | 2.7, 3.5, 3.6, 3.7, |x86-64 |
| | PyPy, PyPy3 | | | | 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 | | Windows Server 2012 R2 | 2.7, 3.5, 3.6, 3.7 |x86, x86-64 |
| +-------------------------------+-----------------------+ | +-------------------------------+-----------------------+
| | PyPy, 3.7/MinGW |x86 | | | PyPy, 3.7/MinGW |x86 |

View File

@ -132,21 +132,21 @@ can be easily displayed in a chromaticity diagram, for example).
.. py:attribute:: manufacturer .. 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). 9.2.22 of ICC.1:2010).
:type: :py:class:`unicode` or ``None`` :type: :py:class:`unicode` or ``None``
.. py:attribute:: model .. 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). for which this profile is created (see 9.2.23 of ICC.1:2010).
:type: :py:class:`unicode` or ``None`` :type: :py:class:`unicode` or ``None``
.. py:attribute:: profile_description .. 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). 9.2.41 of ICC.1:2010).
:type: :py:class:`unicode` or ``None`` :type: :py:class:`unicode` or ``None``
@ -269,14 +269,14 @@ can be easily displayed in a chromaticity diagram, for example).
.. py:attribute:: viewing_condition .. 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). 9.2.48 of ICC.1:2010).
:type: :py:class:`unicode` or ``None`` :type: :py:class:`unicode` or ``None``
.. py:attribute:: screening_description .. 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 This tag was available in ICC 3.2, but it is removed from
version 4. version 4.

View File

@ -255,7 +255,7 @@ Methods
Draw a shape. 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. Draws the string at the given position.
@ -287,7 +287,17 @@ Methods
.. versionadded:: 4.2.0 .. 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. Draws the string at the given position.
@ -316,7 +326,17 @@ Methods
.. versionadded:: 4.2.0 .. 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. Return the size of the given string, in pixels.
@ -330,7 +350,6 @@ Methods
Requires libraqm. Requires libraqm.
.. versionadded:: 4.2.0 .. versionadded:: 4.2.0
:param features: A list of OpenType font features to be used during text :param features: A list of OpenType font features to be used during text
layout. This is usually used to turn on optional layout. This is usually used to turn on optional
font features that are not enabled by default, font features that are not enabled by default,
@ -343,8 +362,17 @@ Methods
Requires libraqm. Requires libraqm.
.. versionadded:: 4.2.0 .. 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. Return the size of the given string, in pixels.
@ -370,6 +398,16 @@ Methods
.. versionadded:: 4.2.0 .. 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) .. py:method:: PIL.ImageDraw.getdraw(im=None, hints=None)
.. warning:: This method is experimental. .. warning:: This method is experimental.

View File

@ -47,11 +47,45 @@ Functions
Methods 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) :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. Create a bitmap for the text.
@ -85,5 +119,15 @@ Methods
.. versionadded:: 4.2.0 .. 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 :return: An internal PIL storage memory instance as defined by the
:py:mod:`PIL.Image.core` interface module. :py:mod:`PIL.Image.core` interface module.

View File

@ -3,10 +3,10 @@
File Handling in Pillow File Handling in Pillow
======================= =======================
When opening a file as an image, Pillow requires a filename, When opening a file as an image, Pillow requires a filename, ``pathlib.Path``
pathlib.Path object, or a file-like object. Pillow uses the filename object, or a file-like object. Pillow uses the filename or ``Path`` to open a
or Path to open a file, so for the rest of this article, they will all file, so for the rest of this article, they will all be treated as a file-like
be treated as a file-like object. object.
The first four of these items are equivalent, the last is dangerous The first four of these items are equivalent, the last is dangerous
and may fail:: and may fail::
@ -48,24 +48,23 @@ Issues
Image Lifecycle Image Lifecycle
--------------- ---------------
* ``Image.open()`` Path-like objects are opened as a file. Metadata is read * ``Image.open()`` Filenames and ``Path`` objects are opened as a file.
from the open file. The file is left open for further usage. 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 * ``Image.Image.load()`` When the pixel data from the image is
required, ``load()`` is called. The current frame is read into required, ``load()`` is called. The current frame is read into
memory. The image can now be used independently of the underlying memory. The image can now be used independently of the underlying
image file. image file.
If a filename or a path-like object was passed to ``Image.open()``, then If a filename or a ``Path`` object was passed to ``Image.open()``, then the
the file object was opened by Pillow and is considered to be used exclusively file object was opened by Pillow and is considered to be used exclusively by
by Pillow. So if the image is a single-frame image, the file will Pillow. So if the image is a single-frame image, the file will be closed in
be closed in this method after the frame is read. If the image is a this method after the frame is read. If the image is a multi-frame image,
multi-frame image, (e.g. multipage TIFF and animated GIF) the image file is (e.g. multipage TIFF and animated GIF) the image file is left open so that
left open so that ``Image.Image.seek()`` can load the appropriate frame. ``Image.Image.seek()`` can load the appropriate frame.
* ``Image.Image.close()`` Closes the file pointer and destroys the * ``Image.Image.close()`` Closes the file and destroys the core image object.
core image object. This is used in the Pillow context manager This is used in the Pillow context manager support. e.g.::
support. e.g.::
with Image.open('test.jpg') as img: with Image.open('test.jpg') as img:
... # image operations here. ... # image operations here.
@ -84,17 +83,9 @@ data until the caller has explicitly closed the image.
Complications Complications
------------- -------------
* TiffImagePlugin has some code to pass the underlying file descriptor * ``TiffImagePlugin`` has some code to pass the underlying file descriptor into
into libtiff (if working on an actual file). Since libtiff closes libtiff (if working on an actual file). Since libtiff closes the file
the file descriptor internally, it is duplicated prior to passing it descriptor internally, it is duplicated prior to passing it into libtiff.
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.
* I don't think that there's any way to make this safe without * I don't think that there's any way to make this safe without
changing the lazy loading:: changing the lazy loading::

View File

@ -99,30 +99,114 @@ version.
Use ``PIL.__version__`` instead. 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 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 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 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 formats, EXIF data is not guaranteed to be present in :py:attr:`~PIL.Image.Image.info`
:py:meth:`~PIL.Image.Image.load` has been called. until :py:meth:`~PIL.Image.Image.load` has been called.
Other Changes Other Changes
============= =============
Reading new DDS image format Reading new DDS image format
~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Pillow can now read uncompressed RGB data from DDS images. 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.

View File

@ -765,12 +765,7 @@ try:
url='http://python-pillow.org', url='http://python-pillow.org',
classifiers=[ classifiers=[
"Development Status :: 6 - Mature", "Development Status :: 6 - Mature",
"Topic :: Multimedia :: Graphics", "License :: OSI Approved :: Historical Permission Notice and Disclaimer (HPND)", # noqa: E501
"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",
"Programming Language :: Python :: 2", "Programming Language :: Python :: 2",
"Programming Language :: Python :: 2.7", "Programming Language :: Python :: 2.7",
"Programming Language :: Python :: 3", "Programming Language :: Python :: 3",
@ -779,6 +774,11 @@ try:
"Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.7",
"Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: CPython",
"Programming Language :: Python :: Implementation :: PyPy", "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.*", python_requires=">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*",
cmdclass={"build_ext": pil_build_ext}, cmdclass={"build_ext": pil_build_ext},
@ -789,7 +789,6 @@ try:
packages=["PIL"], packages=["PIL"],
package_dir={'': 'src'}, package_dir={'': 'src'},
keywords=["Imaging", ], keywords=["Imaging", ],
license='Standard PIL License',
zip_safe=not (debug_build() or PLATFORM_MINGW), ) zip_safe=not (debug_build() or PLATFORM_MINGW), )
except RequiredDependencyException as err: except RequiredDependencyException as err:
msg = """ msg = """

View File

@ -27,7 +27,6 @@
from . import Image, ImageFile, ImagePalette from . import Image, ImageFile, ImagePalette
from ._binary import i8, i16le as i16, i32le as i32, \ from ._binary import i8, i16le as i16, i32le as i32, \
o8, o16le as o16, o32le as o32 o8, o16le as o16, o32le as o32
import math
# __version__ is deprecated and will be removed in a future version. Use # __version__ is deprecated and will be removed in a future version. Use
# PIL.__version__ instead. # PIL.__version__ instead.
@ -105,7 +104,6 @@ class BmpImageFile(ImageFile.ImageFile):
# --------------------------------------------- Windows Bitmap v2 to v5 # --------------------------------------------- Windows Bitmap v2 to v5
# v3, OS/2 v2, v4, v5 # v3, OS/2 v2, v4, v5
elif file_info['header_size'] in (40, 64, 108, 124): 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['y_flip'] = i8(header_data[7]) == 0xff
file_info['direction'] = 1 if file_info['y_flip'] else -1 file_info['direction'] = 1 if file_info['y_flip'] else -1
file_info['width'] = i32(header_data[0:4]) file_info['width'] = i32(header_data[0:4])
@ -122,8 +120,7 @@ class BmpImageFile(ImageFile.ImageFile):
file_info['colors'] = i32(header_data[28:32]) file_info['colors'] = i32(header_data[28:32])
file_info['palette_padding'] = 4 file_info['palette_padding'] = 4
self.info["dpi"] = tuple( self.info["dpi"] = tuple(
map(lambda x: int(math.ceil(x / 39.3701)), int(x / 39.3701 + 0.5) for x in file_info['pixels_per_meter'])
file_info['pixels_per_meter']))
if file_info['compression'] == self.BITFIELDS: if file_info['compression'] == self.BITFIELDS:
if len(header_data) >= 52: if len(header_data) >= 52:
for idx, mask in enumerate(['r_mask', for idx, mask in enumerate(['r_mask',
@ -180,6 +177,7 @@ class BmpImageFile(ImageFile.ImageFile):
SUPPORTED = { SUPPORTED = {
32: [(0xff0000, 0xff00, 0xff, 0x0), 32: [(0xff0000, 0xff00, 0xff, 0x0),
(0xff0000, 0xff00, 0xff, 0xff000000), (0xff0000, 0xff00, 0xff, 0xff000000),
(0xff, 0xff00, 0xff0000, 0xff000000),
(0x0, 0x0, 0x0, 0x0), (0x0, 0x0, 0x0, 0x0),
(0xff000000, 0xff0000, 0xff00, 0x0)], (0xff000000, 0xff0000, 0xff00, 0x0)],
24: [(0xff0000, 0xff00, 0xff)], 24: [(0xff0000, 0xff00, 0xff)],
@ -188,6 +186,7 @@ class BmpImageFile(ImageFile.ImageFile):
MASK_MODES = { MASK_MODES = {
(32, (0xff0000, 0xff00, 0xff, 0x0)): "BGRX", (32, (0xff0000, 0xff00, 0xff, 0x0)): "BGRX",
(32, (0xff000000, 0xff0000, 0xff00, 0x0)): "XBGR", (32, (0xff000000, 0xff0000, 0xff00, 0x0)): "XBGR",
(32, (0xff, 0xff00, 0xff0000, 0xff000000)): "RGBA",
(32, (0xff0000, 0xff00, 0xff, 0xff000000)): "BGRA", (32, (0xff0000, 0xff00, 0xff, 0xff000000)): "BGRA",
(32, (0x0, 0x0, 0x0, 0x0)): "BGRA", (32, (0x0, 0x0, 0x0, 0x0)): "BGRA",
(24, (0xff0000, 0xff00, 0xff)): "BGR", (24, (0xff0000, 0xff00, 0xff)): "BGR",
@ -200,7 +199,7 @@ class BmpImageFile(ImageFile.ImageFile):
raw_mode = MASK_MODES[ raw_mode = MASK_MODES[
(file_info["bits"], file_info["rgba_mask"]) (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 elif (file_info['bits'] in (24, 16) and
file_info['rgb_mask'] in SUPPORTED[file_info['bits']]): file_info['rgb_mask'] in SUPPORTED[file_info['bits']]):
raw_mode = MASK_MODES[ raw_mode = MASK_MODES[
@ -310,7 +309,7 @@ def _save(im, fp, filename, bitmap_header=True):
dpi = info.get("dpi", (96, 96)) dpi = info.get("dpi", (96, 96))
# 1 meter == 39.3701 inches # 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) stride = ((im.size[0]*bits+7)//8+3) & (~3)
header = 40 # or 64 for OS/2 version 2 header = 40 # or 64 for OS/2 version 2

View File

@ -141,13 +141,11 @@ def Ghostscript(tile, size, fp, scale=1):
# push data through Ghostscript # push data through Ghostscript
try: try:
with open(os.devnull, 'w+b') as devnull:
startupinfo = None startupinfo = None
if sys.platform.startswith('win'): if sys.platform.startswith('win'):
startupinfo = subprocess.STARTUPINFO() startupinfo = subprocess.STARTUPINFO()
startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
subprocess.check_call(command, stdout=devnull, subprocess.check_call(command, startupinfo=startupinfo)
startupinfo=startupinfo)
im = Image.open(outfile) im = Image.open(outfile)
im.load() im.load()
finally: finally:

View File

@ -122,6 +122,8 @@ class GifImageFile(ImageFile.ImageFile):
if not self._seek_check(frame): if not self._seek_check(frame):
return return
if frame < self.__frame: if frame < self.__frame:
if frame != 0:
self.im = None
self._seek(0) self._seek(0)
last_frame = self.__frame last_frame = self.__frame

View File

@ -32,14 +32,12 @@ class GimpPaletteFile(object):
if fp.readline()[:12] != b"GIMP Palette": if fp.readline()[:12] != b"GIMP Palette":
raise SyntaxError("not a GIMP palette file") raise SyntaxError("not a GIMP palette file")
i = 0 for i in range(256):
while i <= 255:
s = fp.readline() s = fp.readline()
if not s: if not s:
break break
# skip fields and comment lines # skip fields and comment lines
if re.match(br"\w+:|#", s): if re.match(br"\w+:|#", s):
continue continue
@ -50,11 +48,8 @@ class GimpPaletteFile(object):
if len(v) != 3: if len(v) != 3:
raise ValueError("bad palette entry") raise ValueError("bad palette entry")
if 0 <= i <= 255:
self.palette[i] = o8(v[0]) + o8(v[1]) + o8(v[2]) self.palette[i] = o8(v[0]) + o8(v[1]) + o8(v[2])
i += 1
self.palette = b"".join(self.palette) self.palette = b"".join(self.palette)
def getpalette(self): def getpalette(self):

View File

@ -295,3 +295,5 @@ class IcoImageFile(ImageFile.ImageFile):
Image.register_open(IcoImageFile.format, IcoImageFile, _accept) Image.register_open(IcoImageFile.format, IcoImageFile, _accept)
Image.register_save(IcoImageFile.format, _save) Image.register_save(IcoImageFile.format, _save)
Image.register_extension(IcoImageFile.format, ".ico") Image.register_extension(IcoImageFile.format, ".ico")
Image.register_mime(IcoImageFile.format, "image/x-icon")

View File

@ -40,8 +40,8 @@ except ImportError:
import __builtin__ import __builtin__
builtins = __builtin__ builtins = __builtin__
from . import ImageMode from . import ImageMode, TiffTags
from ._binary import i8 from ._binary import i8, i32le
from ._util import isPath, isStringType, deferred_error from ._util import isPath, isStringType, deferred_error
import os import os
@ -54,10 +54,10 @@ import atexit
import numbers import numbers
try: try:
# Python 3 # Python 3
from collections.abc import Callable from collections.abc import Callable, MutableMapping
except ImportError: except ImportError:
# Python 2.7 # Python 2.7
from collections import Callable from collections import Callable, MutableMapping
# Silence warning # Silence warning
@ -247,7 +247,7 @@ _MODEINFO = {
"L": ("L", "L", ("L",)), "L": ("L", "L", ("L",)),
"I": ("L", "I", ("I",)), "I": ("L", "I", ("I",)),
"F": ("L", "F", ("F",)), "F": ("L", "F", ("F",)),
"P": ("RGB", "L", ("P",)), "P": ("P", "L", ("P",)),
"RGB": ("RGB", "L", ("R", "G", "B")), "RGB": ("RGB", "L", ("R", "G", "B")),
"RGBX": ("RGB", "L", ("R", "G", "B", "X")), "RGBX": ("RGB", "L", ("R", "G", "B", "X")),
"RGBA": ("RGB", "L", ("R", "G", "B", "A")), "RGBA": ("RGB", "L", ("R", "G", "B", "A")),
@ -578,7 +578,12 @@ class Image(object):
return self return self
def __exit__(self, *args): 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): def close(self):
""" """
@ -610,12 +615,7 @@ class Image(object):
if sys.version_info.major >= 3: if sys.version_info.major >= 3:
def __del__(self): def __del__(self):
if hasattr(self, "_close__fp"): self.__exit__()
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
def _copy(self): def _copy(self):
self.load() self.load()
@ -950,7 +950,7 @@ class Image(object):
delete_trns = False delete_trns = False
# transparency handling # transparency handling
if has_transparency: 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 # Use transparent conversion to promote from transparent
# color to an alpha channel. # color to an alpha channel.
new_im = self._new(self.im.convert_transparent( new_im = self._new(self.im.convert_transparent(
@ -1096,7 +1096,13 @@ class Image(object):
im = self.im.convert("P", dither, palette.im) im = self.im.convert("P", dither, palette.im)
return self._new(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): def copy(self):
""" """
@ -1291,6 +1297,12 @@ class Image(object):
return tuple(extrema) return tuple(extrema)
return self.im.getextrema() return self.im.getextrema()
def getexif(self):
exif = Exif()
if "exif" in self.info:
exif.load(self.info["exif"])
return exif
def getim(self): def getim(self):
""" """
Returns a capsule that points to the internal image memory. Returns a capsule that points to the internal image memory.
@ -1559,7 +1571,7 @@ class Image(object):
self._ensure_mutable() 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 # attempt to promote self to a matching alpha mode
try: try:
mode = getmodebase(self.mode) + "A" mode = getmodebase(self.mode) + "A"
@ -1568,7 +1580,7 @@ class Image(object):
except (AttributeError, ValueError): except (AttributeError, ValueError):
# do things the hard way # do things the hard way
im = self.im.convert(mode) im = self.im.convert(mode)
if im.mode not in ("LA", "RGBA"): if im.mode not in ("LA", "PA", "RGBA"):
raise ValueError # sanity check raise ValueError # sanity check
self.im = im self.im = im
self.pyaccess = None self.pyaccess = None
@ -1576,7 +1588,7 @@ class Image(object):
except (KeyError, ValueError): except (KeyError, ValueError):
raise ValueError("illegal image mode") raise ValueError("illegal image mode")
if self.mode == "LA": if self.mode in ("LA", "PA"):
band = 1 band = 1
else: else:
band = 3 band = 3
@ -1619,10 +1631,10 @@ class Image(object):
def putpalette(self, data, rawmode="RGB"): def putpalette(self, data, rawmode="RGB"):
""" """
Attaches a palette to this image. The image must be a "P" or Attaches a palette to this image. The image must be a "P",
"L" image, and the palette sequence must contain 768 integer "PA", "L" or "LA" image, and the palette sequence must contain
values, where each group of three values represent the red, 768 integer values, where each group of three values represent
green, and blue values for the corresponding pixel the red, green, and blue values for the corresponding pixel
index. Instead of an integer sequence, you can use an 8-bit index. Instead of an integer sequence, you can use an 8-bit
string. string.
@ -1631,7 +1643,7 @@ class Image(object):
""" """
from . import ImagePalette from . import ImagePalette
if self.mode not in ("L", "P"): if self.mode not in ("L", "LA", "P", "PA"):
raise ValueError("illegal image mode") raise ValueError("illegal image mode")
self.load() self.load()
if isinstance(data, ImagePalette.ImagePalette): if isinstance(data, ImagePalette.ImagePalette):
@ -1643,7 +1655,7 @@ class Image(object):
else: else:
data = "".join(chr(x) for x in data) data = "".join(chr(x) for x in data)
palette = ImagePalette.raw(rawmode, data) palette = ImagePalette.raw(rawmode, data)
self.mode = "P" self.mode = "PA" if "A" in self.mode else "P"
self.palette = palette self.palette = palette
self.palette.mode = "RGB" self.palette.mode = "RGB"
self.load() # install new palette self.load() # install new palette
@ -1688,7 +1700,7 @@ class Image(object):
Rewrites the image to reorder the palette. Rewrites the image to reorder the palette.
:param dest_map: A list of indexes into the original 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. is the identity transform.
:param source_palette: Bytes or None. :param source_palette: Bytes or None.
:returns: An :py:class:`~PIL.Image.Image` object. :returns: An :py:class:`~PIL.Image.Image` object.
@ -1958,7 +1970,7 @@ class Image(object):
filename = fp.name filename = fp.name
# may mutate self! # may mutate self!
self.load() self._ensure_mutable()
save_all = params.pop('save_all', False) save_all = params.pop('save_all', False)
self.encoderinfo = params self.encoderinfo = params
@ -2005,9 +2017,6 @@ class Image(object):
**EOFError** exception. When a sequence file is opened, the **EOFError** exception. When a sequence file is opened, the
library automatically seeks to frame 0. 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`. See :py:meth:`~PIL.Image.Image.tell`.
:param frame: Frame number, starting at 0. :param frame: Frame number, starting at 0.
@ -2374,7 +2383,14 @@ def new(mode, size, color=0):
from . import ImageColor from . import ImageColor
color = ImageColor.getcolor(color, mode) 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): def frombytes(mode, size, data, decoder_name="raw", *args):
@ -2995,3 +3011,182 @@ def _apply_env_variables(env=None):
_apply_env_variables() _apply_env_variables()
atexit.register(core.clear_cache) 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))

View File

@ -686,11 +686,11 @@ def getProfileName(profile):
# // name was "%s - %s" (model, manufacturer) || Description , # // name was "%s - %s" (model, manufacturer) || Description ,
# // but if the Model and Manufacturer were the same or the model # // but if the Model and Manufacturer were the same or the model
# // was long, Just the model, in 1.x # // was long, Just the model, in 1.x
model = profile.profile.product_model model = profile.profile.model
manufacturer = profile.profile.product_manufacturer manufacturer = profile.profile.manufacturer
if not (model or 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: if not manufacturer or len(model) > 30:
return model + "\n" return model + "\n"
return "%s - %s\n" % (model, manufacturer) return "%s - %s\n" % (model, manufacturer)
@ -727,8 +727,8 @@ def getProfileInfo(profile):
# Python, not C. the white point bits weren't working well, # Python, not C. the white point bits weren't working well,
# so skipping. # so skipping.
# info was description \r\n\r\n copyright \r\n\r\n K007 tag \r\n\r\n whitepoint # info was description \r\n\r\n copyright \r\n\r\n K007 tag \r\n\r\n whitepoint
description = profile.profile.product_description description = profile.profile.profile_description
cpright = profile.profile.product_copyright cpright = profile.profile.copyright
arr = [] arr = []
for elt in (description, cpright): for elt in (description, cpright):
if elt: if elt:
@ -762,7 +762,7 @@ def getProfileCopyright(profile):
# add an extra newline to preserve pyCMS compatibility # add an extra newline to preserve pyCMS compatibility
if not isinstance(profile, ImageCmsProfile): if not isinstance(profile, ImageCmsProfile):
profile = ImageCmsProfile(profile) profile = ImageCmsProfile(profile)
return profile.profile.product_copyright + "\n" return (profile.profile.copyright or "") + "\n"
except (AttributeError, IOError, TypeError, ValueError) as v: except (AttributeError, IOError, TypeError, ValueError) as v:
raise PyCMSError(v) raise PyCMSError(v)
@ -790,7 +790,7 @@ def getProfileManufacturer(profile):
# add an extra newline to preserve pyCMS compatibility # add an extra newline to preserve pyCMS compatibility
if not isinstance(profile, ImageCmsProfile): if not isinstance(profile, ImageCmsProfile):
profile = ImageCmsProfile(profile) profile = ImageCmsProfile(profile)
return profile.profile.product_manufacturer + "\n" return (profile.profile.manufacturer or "") + "\n"
except (AttributeError, IOError, TypeError, ValueError) as v: except (AttributeError, IOError, TypeError, ValueError) as v:
raise PyCMSError(v) raise PyCMSError(v)
@ -819,7 +819,7 @@ def getProfileModel(profile):
# add an extra newline to preserve pyCMS compatibility # add an extra newline to preserve pyCMS compatibility
if not isinstance(profile, ImageCmsProfile): if not isinstance(profile, ImageCmsProfile):
profile = ImageCmsProfile(profile) profile = ImageCmsProfile(profile)
return profile.profile.product_model + "\n" return (profile.profile.model or "") + "\n"
except (AttributeError, IOError, TypeError, ValueError) as v: except (AttributeError, IOError, TypeError, ValueError) as v:
raise PyCMSError(v) raise PyCMSError(v)
@ -848,7 +848,7 @@ def getProfileDescription(profile):
# add an extra newline to preserve pyCMS compatibility # add an extra newline to preserve pyCMS compatibility
if not isinstance(profile, ImageCmsProfile): if not isinstance(profile, ImageCmsProfile):
profile = ImageCmsProfile(profile) profile = ImageCmsProfile(profile)
return profile.profile.product_description + "\n" return (profile.profile.profile_description or "") + "\n"
except (AttributeError, IOError, TypeError, ValueError) as v: except (AttributeError, IOError, TypeError, ValueError) as v:
raise PyCMSError(v) raise PyCMSError(v)

View File

@ -282,13 +282,17 @@ class ImageDraw(object):
self.draw.draw_bitmap(xy, mask, ink) self.draw.draw_bitmap(xy, mask, ink)
def multiline_text(self, xy, text, fill=None, font=None, anchor=None, 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 = [] widths = []
max_width = 0 max_width = 0
lines = self._multiline_split(text) lines = self._multiline_split(text)
line_spacing = self.textsize('A', font=font)[1] + spacing line_spacing = self.textsize('A', font=font)[1] + spacing
for line in lines: 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) widths.append(line_width)
max_width = max(max_width, line_width) max_width = max(max_width, line_width)
left, top = xy left, top = xy
@ -302,29 +306,30 @@ class ImageDraw(object):
else: else:
raise ValueError('align must be "left", "center" or "right"') raise ValueError('align must be "left", "center" or "right"')
self.text((left, top), line, fill, font, anchor, self.text((left, top), line, fill, font, anchor,
direction=direction, features=features) direction=direction, features=features, language=language)
top += line_spacing top += line_spacing
left = xy[0] left = xy[0]
def textsize(self, text, font=None, spacing=4, direction=None, 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.""" """Get the size of a given string, in pixels."""
if self._multiline_check(text): if self._multiline_check(text):
return self.multiline_textsize(text, font, spacing, return self.multiline_textsize(text, font, spacing,
direction, features) direction, features, language)
if font is None: if font is None:
font = self.getfont() 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, def multiline_textsize(self, text, font=None, spacing=4, direction=None,
features=None): features=None, language=None):
max_width = 0 max_width = 0
lines = self._multiline_split(text) lines = self._multiline_split(text)
line_spacing = self.textsize('A', font=font)[1] + spacing line_spacing = self.textsize('A', font=font)[1] + spacing
for line in lines: for line in lines:
line_width, line_height = self.textsize(line, font, spacing, line_width, line_height = self.textsize(line, font, spacing,
direction, features) direction, features,
language)
max_width = max(max_width, line_width) max_width = max(max_width, line_width)
return max_width, len(lines)*line_spacing - spacing return max_width, len(lines)*line_spacing - spacing

View File

@ -596,8 +596,6 @@ class PyDecoder(object):
Override to perform the decoding process. Override to perform the decoding process.
:param buffer: A bytes object with the data to be decoded. :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). :returns: A tuple of (bytes consumed, errcode).
If finished with decoding return <0 for the bytes consumed. If finished with decoding return <0 for the bytes consumed.
Err codes are from `ERRORS` Err codes are from `ERRORS`

View File

@ -158,17 +158,17 @@ class FreeTypeFont(object):
def getmetrics(self): def getmetrics(self):
return self.font.ascent, self.font.descent return self.font.ascent, self.font.descent
def getsize(self, text, direction=None, features=None): def getsize(self, text, direction=None, features=None, language=None):
size, offset = self.font.getsize(text, direction, features) size, offset = self.font.getsize(text, direction, features, language)
return (size[0] + offset[0], size[1] + offset[1]) return (size[0] + offset[0], size[1] + offset[1])
def getsize_multiline(self, text, direction=None, def getsize_multiline(self, text, direction=None, spacing=4,
spacing=4, features=None): features=None, language=None):
max_width = 0 max_width = 0
lines = self._multiline_split(text) lines = self._multiline_split(text)
line_spacing = self.getsize('A')[1] + spacing line_spacing = self.getsize('A')[1] + spacing
for line in lines: 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) max_width = max(max_width, line_width)
return max_width, len(lines)*line_spacing - spacing return max_width, len(lines)*line_spacing - spacing
@ -176,15 +176,15 @@ class FreeTypeFont(object):
def getoffset(self, text): def getoffset(self, text):
return self.font.getsize(text)[1] return self.font.getsize(text)[1]
def getmask(self, text, mode="", direction=None, features=None): def getmask(self, text, mode="", direction=None, features=None, language=None):
return self.getmask2(text, mode, direction=direction, return self.getmask2(text, mode, direction=direction, features=features,
features=features)[0] language=language)[0]
def getmask2(self, text, mode="", fill=Image.core.fill, direction=None, def getmask2(self, text, mode="", fill=Image.core.fill, direction=None,
features=None, *args, **kwargs): features=None, language=None, *args, **kwargs):
size, offset = self.font.getsize(text, direction, features) size, offset = self.font.getsize(text, direction, features, language)
im = fill("L", size, 0) 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 return im, offset
def font_variant(self, font=None, size=None, index=None, encoding=None, def font_variant(self, font=None, size=None, index=None, encoding=None,

View File

@ -522,3 +522,30 @@ def solarize(image, threshold=128):
else: else:
lut.append(255-i) lut.append(255-i)
return _lut(image, lut) 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()

View File

@ -198,35 +198,9 @@ def getiptcinfo(im):
elif isinstance(im, JpegImagePlugin.JpegImageFile): elif isinstance(im, JpegImagePlugin.JpegImageFile):
# extract the IPTC/NAA resource # extract the IPTC/NAA resource
try: photoshop = im.info.get("photoshop")
app = im.app["APP13"] if photoshop:
if app[:14] == b"Photoshop 3.0\x00": data = photoshop.get(0x0404)
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
elif isinstance(im, TiffImagePlugin.TiffImageFile): elif isinstance(im, TiffImagePlugin.TiffImageFile):
# get raw data from the IPTC/NAA tag (PhotoShop tags the data # get raw data from the IPTC/NAA tag (PhotoShop tags the data

View File

@ -39,7 +39,7 @@ import struct
import io import io
import warnings import warnings
from . import Image, ImageFile, TiffImagePlugin 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 .JpegPresets import presets
from ._util import isStringType from ._util import isStringType
@ -86,7 +86,7 @@ def APP(self, marker):
self.info["jfif_density"] = jfif_density self.info["jfif_density"] = jfif_density
elif marker == 0xFFE1 and s[:5] == b"Exif\0": elif marker == 0xFFE1 and s[:5] == b"Exif\0":
if "exif" not in self.info: if "exif" not in self.info:
# extract Exif information (incomplete) # extract EXIF information (incomplete)
self.info["exif"] = s # FIXME: value will change self.info["exif"] = s # FIXME: value will change
elif marker == 0xFFE2 and s[:5] == b"FPXR\0": elif marker == 0xFFE2 and s[:5] == b"FPXR\0":
# extract FlashPix information (incomplete) # extract FlashPix information (incomplete)
@ -104,6 +104,39 @@ def APP(self, marker):
# reassemble the profile, rather than assuming that the APP2 # reassemble the profile, rather than assuming that the APP2
# markers appear in the correct sequence. # markers appear in the correct sequence.
self.icclist.append(s) 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": elif marker == 0xFFEE and s[:5] == b"Adobe":
self.info["adobe"] = i16(s, 5) self.info["adobe"] = i16(s, 5)
# extract Adobe custom properties # extract Adobe custom properties
@ -127,15 +160,15 @@ def APP(self, marker):
resolution_unit = exif[0x0128] resolution_unit = exif[0x0128]
x_resolution = exif[0x011A] x_resolution = exif[0x011A]
try: try:
dpi = x_resolution[0] / x_resolution[1] dpi = float(x_resolution[0]) / x_resolution[1]
except TypeError: except TypeError:
dpi = x_resolution dpi = x_resolution
if resolution_unit == 3: # cm if resolution_unit == 3: # cm
# 1 dpcm = 2.54 dpi # 1 dpcm = 2.54 dpi
dpi *= 2.54 dpi *= 2.54
self.info["dpi"] = dpi, dpi self.info["dpi"] = int(dpi + 0.5), int(dpi + 0.5)
except (KeyError, SyntaxError, ZeroDivisionError): except (KeyError, SyntaxError, ZeroDivisionError):
# SyntaxError for invalid/unreadable exif # SyntaxError for invalid/unreadable EXIF
# KeyError for dpi not included # KeyError for dpi not included
# ZeroDivisionError for invalid dpi rational value # ZeroDivisionError for invalid dpi rational value
self.info["dpi"] = 72, 72 self.info["dpi"] = 72, 72
@ -439,60 +472,23 @@ class JpegImageFile(ImageFile.ImageFile):
def _fixup_dict(src_dict): def _fixup_dict(src_dict):
# Helper function for _getexif() # Helper function for _getexif()
# returns a dict with any single item tuples/lists as individual values # returns a dict with any single item tuples/lists as individual values
def _fixup(value): exif = Image.Exif()
try: return exif._fixup_dict(src_dict)
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 _getexif(self): def _getexif(self):
# Extract EXIF information. This method is highly experimental, # Use the cached version if possible
# 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 (!).
try: try:
data = self.info["exif"] return self.info["parsed_exif"]
except KeyError: 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 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 return exif
@ -728,6 +724,10 @@ def _save(im, fp, filename):
optimize = info.get("optimize", False) optimize = info.get("optimize", False)
exif = info.get("exif", b"")
if isinstance(exif, Image.Exif):
exif = exif.tobytes()
# get keyword arguments # get keyword arguments
im.encoderconfig = ( im.encoderconfig = (
quality, quality,
@ -739,7 +739,7 @@ def _save(im, fp, filename):
subsampling, subsampling,
qtables, qtables,
extra, extra,
info.get("exif", b"") exif
) )
# if we optimize, libjpeg needs a buffer big enough to hold the whole image # if we optimize, libjpeg needs a buffer big enough to hold the whole image
@ -757,9 +757,9 @@ def _save(im, fp, filename):
else: else:
bufsize = im.size[0] * im.size[1] 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. # 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) len(extra) + 1)
ImageFile._save(im, fp, [("jpeg", (0, 0)+im.size, 0, rawmode)], bufsize) 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: if mpheader[45057] > 1:
# It's actually an MPO # It's actually an MPO
from .MpoImagePlugin import MpoImageFile from .MpoImagePlugin import MpoImageFile
im = MpoImageFile(fp, filename) # Don't reload everything, just convert it.
im = MpoImageFile.adopt(im, mpheader)
except (TypeError, IndexError): except (TypeError, IndexError):
# It is really a JPEG # It is really a JPEG
pass pass

View File

@ -18,7 +18,8 @@
# See the README file for information on usage and redistribution. # 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 # __version__ is deprecated and will be removed in a future version. Use
# PIL.__version__ instead. # PIL.__version__ instead.
@ -46,7 +47,10 @@ class MpoImageFile(JpegImagePlugin.JpegImageFile):
def _open(self): def _open(self):
self.fp.seek(0) # prep the fp in order to pass the JPEG test self.fp.seek(0) # prep the fp in order to pass the JPEG test
JpegImagePlugin.JpegImageFile._open(self) 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.__framecount = self.mpinfo[0xB001]
self.__mpoffsets = [mpent['DataOffset'] + self.info['mpoffset'] self.__mpoffsets = [mpent['DataOffset'] + self.info['mpoffset']
for mpent in self.mpinfo[0xB002]] for mpent in self.mpinfo[0xB002]]
@ -78,6 +82,20 @@ class MpoImageFile(JpegImagePlugin.JpegImageFile):
return return
self.fp = self.__fp self.fp = self.__fp
self.offset = self.__mpoffsets[frame] 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 = [ self.tile = [
("jpeg", (0, 0) + self.size, self.offset, (self.mode, "")) ("jpeg", (0, 0) + self.size, self.offset, (self.mode, ""))
] ]
@ -95,6 +113,22 @@ class MpoImageFile(JpegImagePlugin.JpegImageFile):
finally: finally:
self.__fp = None 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 # Registry stuff

View File

@ -180,3 +180,5 @@ Image.register_open(PcxImageFile.format, PcxImageFile, _accept)
Image.register_save(PcxImageFile.format, _save) Image.register_save(PcxImageFile.format, _save)
Image.register_extension(PcxImageFile.format, ".pcx") Image.register_extension(PcxImageFile.format, ".pcx")
Image.register_mime(PcxImageFile.format, "image/x-pcx")

View File

@ -54,19 +54,24 @@ _MAGIC = b"\211PNG\r\n\032\n"
_MODES = { _MODES = {
# supported bits/color combinations, and corresponding modes/rawmodes # supported bits/color combinations, and corresponding modes/rawmodes
# Greyscale
(1, 0): ("1", "1"), (1, 0): ("1", "1"),
(2, 0): ("L", "L;2"), (2, 0): ("L", "L;2"),
(4, 0): ("L", "L;4"), (4, 0): ("L", "L;4"),
(8, 0): ("L", "L"), (8, 0): ("L", "L"),
(16, 0): ("I", "I;16B"), (16, 0): ("I", "I;16B"),
# Truecolour
(8, 2): ("RGB", "RGB"), (8, 2): ("RGB", "RGB"),
(16, 2): ("RGB", "RGB;16B"), (16, 2): ("RGB", "RGB;16B"),
# Indexed-colour
(1, 3): ("P", "P;1"), (1, 3): ("P", "P;1"),
(2, 3): ("P", "P;2"), (2, 3): ("P", "P;2"),
(4, 3): ("P", "P;4"), (4, 3): ("P", "P;4"),
(8, 3): ("P", "P"), (8, 3): ("P", "P"),
# Greyscale with alpha
(8, 4): ("LA", "LA"), (8, 4): ("LA", "LA"),
(16, 4): ("RGBA", "LA;16B"), # LA;16B->LA not yet available (16, 4): ("RGBA", "LA;16B"), # LA;16B->LA not yet available
# Truecolour with alpha
(8, 6): ("RGBA", "RGBA"), (8, 6): ("RGBA", "RGBA"),
(16, 6): ("RGBA", "RGBA;16B"), (16, 6): ("RGBA", "RGBA;16B"),
} }
@ -386,7 +391,7 @@ class PngStream(ChunkStream):
# otherwise, we have a byte string with one alpha value # otherwise, we have a byte string with one alpha value
# for each palette entry # for each palette entry
self.im_info["transparency"] = s self.im_info["transparency"] = s
elif self.im_mode == "L": elif self.im_mode in ("1", "L", "I"):
self.im_info["transparency"] = i16(s) self.im_info["transparency"] = i16(s)
elif self.im_mode == "RGB": elif self.im_mode == "RGB":
self.im_info["transparency"] = i16(s), i16(s[2:]), i16(s[4:]) self.im_info["transparency"] = i16(s), i16(s[2:]), i16(s[4:])
@ -691,8 +696,14 @@ class PngImageFile(ImageFile.ImageFile):
def _getexif(self): def _getexif(self):
if "exif" not in self.info: if "exif" not in self.info:
self.load() self.load()
from .JpegImagePlugin import _getexif if "exif" not in self.info:
return _getexif(self) 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'), "L": ("L", b'\x08\x00'),
"LA": ("LA", b'\x08\x04'), "LA": ("LA", b'\x08\x04'),
"I": ("I;16B", b'\x10\x00'), "I": ("I;16B", b'\x10\x00'),
"I;16": ("I;16B", b'\x10\x00'),
"P;1": ("P;1", b'\x01\x03'), "P;1": ("P;1", b'\x01\x03'),
"P;2": ("P;2", b'\x02\x03'), "P;2": ("P;2", b'\x02\x03'),
"P;4": ("P;4", b'\x04\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)) transparency = max(0, min(255, transparency))
alpha = b'\xFF' * transparency + b'\0' alpha = b'\xFF' * transparency + b'\0'
chunk(fp, b"tRNS", alpha[:alpha_bytes]) chunk(fp, b"tRNS", alpha[:alpha_bytes])
elif im.mode == "L": elif im.mode in ("1", "L", "I"):
transparency = max(0, min(65535, transparency)) transparency = max(0, min(65535, transparency))
chunk(fp, b"tRNS", o16(transparency)) chunk(fp, b"tRNS", o16(transparency))
elif im.mode == "RGB": elif im.mode == "RGB":
@ -874,6 +886,8 @@ def _save(im, fp, filename, chunk=putchunk):
exif = im.encoderinfo.get("exif", im.info.get("exif")) exif = im.encoderinfo.get("exif", im.info.get("exif"))
if exif: if exif:
if isinstance(exif, Image.Exif):
exif = exif.tobytes(8)
if exif.startswith(b"Exif\x00\x00"): if exif.startswith(b"Exif\x00\x00"):
exif = exif[6:] exif = exif[6:]
chunk(fp, b"eXIf", exif) chunk(fp, b"eXIf", exif)

View File

@ -164,6 +164,6 @@ def _save(im, fp, filename):
Image.register_open(PpmImageFile.format, PpmImageFile, _accept) Image.register_open(PpmImageFile.format, PpmImageFile, _accept)
Image.register_save(PpmImageFile.format, _save) 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") Image.register_mime(PpmImageFile.format, "image/x-portable-anymap")

View File

@ -208,7 +208,7 @@ class SpiderImageFile(ImageFile.ImageFile):
# given a list of filenames, return a list of images # given a list of filenames, return a list of images
def loadImageSeries(filelist=None): 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: if filelist is None or len(filelist) < 1:
return return

View File

@ -226,4 +226,6 @@ def _save(im, fp, filename):
Image.register_open(TgaImageFile.format, TgaImageFile) Image.register_open(TgaImageFile.format, TgaImageFile)
Image.register_save(TgaImageFile.format, _save) 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")

View File

@ -263,10 +263,10 @@ OPEN_INFO = {
(II, 5, (1,), 1, (8, 8, 8, 8, 8, 8), (0, 0)): ("CMYK", "CMYKXX"), (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"), (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 # Minimal Baseline TIFF requires YCbCr images to have 3 SamplesPerPixel
(II, 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", "RGB"), (MM, 6, (1,), 1, (8, 8, 8), ()): ("RGB", "RGBX"),
(II, 8, (1,), 1, (8, 8, 8), ()): ("LAB", "LAB"), (II, 8, (1,), 1, (8, 8, 8), ()): ("LAB", "LAB"),
(MM, 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)) warnings.warn(str(msg))
return return
def save(self, fp): def tobytes(self, offset=0):
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))
# FIXME What about tagdata? # FIXME What about tagdata?
fp.write(self._pack("H", len(self._tags_v2))) result = self._pack("H", len(self._tags_v2))
entries = [] entries = []
offset = fp.tell() + len(self._tags_v2) * 12 + 4 offset = offset + len(result) + len(self._tags_v2) * 12 + 4
stripoffsets = None stripoffsets = None
# pass 1: convert tags to binary format # pass 1: convert tags to binary format
@ -844,18 +839,29 @@ class ImageFileDirectory_v2(MutableMapping):
for tag, typ, count, value, data in entries: for tag, typ, count, value, data in entries:
if DEBUG > 1: if DEBUG > 1:
print(tag, typ, count, repr(value), repr(data)) 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 -- # -- 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 # pass 3: write auxiliary data to file
for tag, typ, count, value, data in entries: for tag, typ, count, value, data in entries:
fp.write(data) result += data
if len(data) & 1: 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 ImageFileDirectory_v2._load_dispatch = _load_dispatch
@ -985,7 +991,6 @@ class TiffImageFile(ImageFile.ImageFile):
self.__fp = self.fp self.__fp = self.fp
self._frame_pos = [] self._frame_pos = []
self._n_frames = None self._n_frames = None
self._is_animated = None
if DEBUG: if DEBUG:
print("*** TiffImageFile._open ***") print("*** TiffImageFile._open ***")
@ -999,29 +1004,14 @@ class TiffImageFile(ImageFile.ImageFile):
def n_frames(self): def n_frames(self):
if self._n_frames is None: if self._n_frames is None:
current = self.tell() current = self.tell()
try: self._seek(len(self._frame_pos))
while True: while self._n_frames is None:
self._seek(self.tell() + 1) self._seek(self.tell() + 1)
except EOFError:
self._n_frames = self.tell() + 1
self.seek(current) self.seek(current)
return self._n_frames return self._n_frames
@property @property
def is_animated(self): 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 return self._is_animated
def seek(self, frame): def seek(self, frame):
@ -1053,10 +1043,13 @@ class TiffImageFile(ImageFile.ImageFile):
print("Loading tags, location: %s" % self.fp.tell()) print("Loading tags, location: %s" % self.fp.tell())
self.tag_v2.load(self.fp) self.tag_v2.load(self.fp)
self.__next = self.tag_v2.next 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.__frame += 1
self.fp.seek(self._frame_pos[frame]) self.fp.seek(self._frame_pos[frame])
self.tag_v2.load(self.fp) self.tag_v2.load(self.fp)
self.__next = self.tag_v2.next
# fill the legacy tag/ifd entries # fill the legacy tag/ifd entries
self.tag = self.ifd = ImageFileDirectory_v1.from_v2(self.tag_v2) self.tag = self.ifd = ImageFileDirectory_v1.from_v2(self.tag_v2)
self.__frame = frame self.__frame = frame
@ -1087,7 +1080,7 @@ class TiffImageFile(ImageFile.ImageFile):
def load_end(self): def load_end(self):
# allow closing if we're on the first frame, there's no next # allow closing if we're on the first frame, there's no next
# This is the ImageFile.load path only, libtiff specific below. # 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 self._close_exclusive_fp_after_loading = True
def _load_libtiff(self): def _load_libtiff(self):
@ -1167,8 +1160,7 @@ class TiffImageFile(ImageFile.ImageFile):
self.tile = [] self.tile = []
self.readonly = 0 self.readonly = 0
# libtiff closed the fp in a, we need to close self.fp, if possible # libtiff closed the fp in a, we need to close self.fp, if possible
if self._exclusive_fp: if self._exclusive_fp and not self._is_animated:
if self.__frame == 0 and not self.__next:
self.fp.close() self.fp.close()
self.fp = None # might be shared self.fp = None # might be shared
@ -1191,6 +1183,10 @@ class TiffImageFile(ImageFile.ImageFile):
# the specification # the specification
photo = self.tag_v2.get(PHOTOMETRIC_INTERPRETATION, 0) 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) fillorder = self.tag_v2.get(FILLORDER, 1)
if DEBUG: if DEBUG:
@ -1257,11 +1253,11 @@ class TiffImageFile(ImageFile.ImageFile):
if xres and yres: if xres and yres:
resunit = self.tag_v2.get(RESOLUTION_UNIT) resunit = self.tag_v2.get(RESOLUTION_UNIT)
if resunit == 2: # dots per inch 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 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) 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, # For backward compatibility,
# we also preserve the old behavior # we also preserve the old behavior
self.info["resolution"] = xres, yres self.info["resolution"] = xres, yres
@ -1472,8 +1468,8 @@ def _save(im, fp, filename):
dpi = im.encoderinfo.get("dpi") dpi = im.encoderinfo.get("dpi")
if dpi: if dpi:
ifd[RESOLUTION_UNIT] = 2 ifd[RESOLUTION_UNIT] = 2
ifd[X_RESOLUTION] = dpi[0] ifd[X_RESOLUTION] = int(dpi[0] + 0.5)
ifd[Y_RESOLUTION] = dpi[1] ifd[Y_RESOLUTION] = int(dpi[1] + 0.5)
if bits != (1,): if bits != (1,):
ifd[BITSPERSAMPLE] = bits ifd[BITSPERSAMPLE] = bits

View File

@ -93,8 +93,9 @@ class WebPImageFile(ImageFile.ImageFile):
self.seek(0) self.seek(0)
def _getexif(self): def _getexif(self):
from .JpegImagePlugin import _getexif if "exif" not in self.info:
return _getexif(self) return None
return dict(self.getexif())
@property @property
def n_frames(self): def n_frames(self):
@ -186,7 +187,7 @@ def _save_all(im, fp, filename):
# will preserve non-alpha modes # will preserve non-alpha modes
total = 0 total = 0
for ims in [im]+append_images: 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: if total == 1:
_save(im, fp, filename) _save(im, fp, filename)
return return
@ -216,6 +217,8 @@ def _save_all(im, fp, filename):
method = im.encoderinfo.get("method", 0) method = im.encoderinfo.get("method", 0)
icc_profile = im.encoderinfo.get("icc_profile", "") icc_profile = im.encoderinfo.get("icc_profile", "")
exif = im.encoderinfo.get("exif", "") exif = im.encoderinfo.get("exif", "")
if isinstance(exif, Image.Exif):
exif = exif.tobytes()
xmp = im.encoderinfo.get("xmp", "") xmp = im.encoderinfo.get("xmp", "")
if allow_mixed: if allow_mixed:
lossless = False lossless = False
@ -254,10 +257,7 @@ def _save_all(im, fp, filename):
try: try:
for ims in [im]+append_images: for ims in [im]+append_images:
# Get # of frames in this image # Get # of frames in this image
if not hasattr(ims, "n_frames"): nfr = getattr(ims, "n_frames", 1)
nfr = 1
else:
nfr = ims.n_frames
for idx in range(nfr): for idx in range(nfr):
ims.seek(idx) ims.seek(idx)
@ -318,6 +318,8 @@ def _save(im, fp, filename):
quality = im.encoderinfo.get("quality", 80) quality = im.encoderinfo.get("quality", 80)
icc_profile = im.encoderinfo.get("icc_profile", "") icc_profile = im.encoderinfo.get("icc_profile", "")
exif = im.encoderinfo.get("exif", "") exif = im.encoderinfo.get("exif", "")
if isinstance(exif, Image.Exif):
exif = exif.tobytes()
xmp = im.encoderinfo.get("xmp", "") xmp = im.encoderinfo.get("xmp", "")
if im.mode not in _VALID_WEBP_LEGACY_MODES: if im.mode not in _VALID_WEBP_LEGACY_MODES:

Some files were not shown because too many files have changed in this diff Show More