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
{
c:\python34\python.exe c:\pillow\winbuild\build_dep.py
c:\python37\python.exe c:\pillow\winbuild\build_dep.py
c:\pillow\winbuild\build_deps.cmd
$host.SetShouldExit(0)
}

View File

@ -9,7 +9,7 @@ Please send a pull request to the master branch. Please include [documentation](
- Fork the Pillow repository.
- Create a branch from master.
- Develop bug fixes, features, tests, etc.
- Run the test suite on Python 2.7 and 3.x. You can enable [Travis CI](https://travis-ci.org/profile/) and [AppVeyor](https://ci.appveyor.com/projects/new) on your repo to catch test failures prior to the pull request, and [Coveralls](https://coveralls.io/repos/new) to see if the changed code is covered by tests.
- Run the test suite on Python 2.7 and 3.x. You can enable [Travis CI](https://travis-ci.org/profile/) and [AppVeyor](https://ci.appveyor.com/projects/new) on your repo to catch test failures prior to the pull request, and [Codecov](https://codecov.io/gh) to see if the changed code is covered by tests.
- Create a pull request to pull the changes from your branch to the Pillow master.
### Guidelines

View File

@ -18,41 +18,27 @@ matrix:
env: LINT="true"
- python: "pypy2.7-6.0"
name: "PyPy2 Xenial"
dist: xenial
- python: "pypy3.5-6.0"
name: "PyPy3 Xenial"
dist: xenial
- python: '3.7'
name: "3.7 Xenial"
- python: '2.7'
name: "2.7 Xenial"
- python: '2.7'
name: "2.7 Trusty"
dist: trusty
- python: "2.7_with_system_site_packages" # For PyQt4
name: "2.7_with_system_site_packages Xenial"
services: xvfb
- python: "2.7_with_system_site_packages" # For PyQt4
name: "2.7_with_system_site_packages Trusty"
dist: trusty
- python: '3.6'
name: "3.6 Xenial"
- python: '3.6'
name: "3.6 Trusty PYTHONOPTIMIZE=1"
dist: trusty
name: "3.6 Xenial PYTHONOPTIMIZE=1"
env: PYTHONOPTIMIZE=1
- python: '3.5'
name: "3.5 Xenial"
- python: '3.5'
name: "3.5 Trusty PYTHONOPTIMIZE=2"
dist: trusty
name: "3.5 Xenial PYTHONOPTIMIZE=2"
env: PYTHONOPTIMIZE=2
- python: "3.8-dev"
name: "3.8-dev Xenial"
- env: DOCKER="alpine" DOCKER_TAG="master"
- env: DOCKER="arch" DOCKER_TAG="master" # contains PyQt5
- env: DOCKER="ubuntu-trusty-x86" DOCKER_TAG="master"
- env: DOCKER="ubuntu-xenial-amd64" DOCKER_TAG="master"
- env: DOCKER="ubuntu-16.04-xenial-amd64" DOCKER_TAG="master"
- env: DOCKER="ubuntu-18.04-bionic-amd64" DOCKER_TAG="master"
- env: DOCKER="debian-stretch-x86" DOCKER_TAG="master"
- env: DOCKER="centos-6-amd64" DOCKER_TAG="master"
- env: DOCKER="centos-7-amd64" DOCKER_TAG="master"
@ -75,14 +61,6 @@ install:
.travis/install.sh;
fi
before_script:
# Qt needs a display for some of the tests, and it's only run on the system site packages install
- |
if [ "$TRAVIS_JOB_NAME" == "2.7_with_system_site_packages Trusty" ]; then
export DISPLAY=:99.0
sh -e /etc/init.d/xvfb start
fi
script:
- |
if [ "$LINT" == "true" ]; then

View File

@ -2,12 +2,102 @@
Changelog (Pillow)
==================
6.0.0 (unreleased)
6.0.0 (2019-04-01)
------------------
- Python 2.7 support will be removed in Pillow 7.0.0 #3682
[hugovk]
- Add EXIF class #3625
[radarhere]
- Add ImageOps exif_transpose method #3687
[radarhere]
- Added warnings to deprecated CMSProfile attributes #3615
[hugovk]
- Documented reading TIFF multiframe images #3720
[akuchling]
- Improved speed of opening an MPO file #3658
[Glandos]
- Update palette in quantize #3721
[radarhere]
- Improvements to TIFF is_animated and n_frames #3714
[radarhere]
- Fixed incompatible pointer type warnings #3754
[radarhere]
- Improvements to PA and LA conversion and palette operations #3728
[radarhere]
- Consistent DPI rounding #3709
[radarhere]
- Change size of MPO image to match frame #3588
[radarhere]
- Read Photoshop resolution data #3701
[radarhere]
- Ensure image is mutable before saving #3724
[radarhere]
- Correct remap_palette documentation #3740
[radarhere]
- Promote P images to PA in putalpha #3726
[radarhere]
- Allow RGB and RGBA values for new P images #3719
[radarhere]
- Fixed TIFF bug when seeking backwards and then forwards #3713
[radarhere]
- Cache EXIF information #3498
[Glandos]
- Added transparency for all PNG greyscale modes #3744
[radarhere]
- Fix deprecation warnings in Python 3.8 #3749
[radarhere]
- Fixed GIF bug when rewinding to a non-zero frame #3716
[radarhere]
- Only close original fp in __del__ and __exit__ if original fp is exclusive #3683
[radarhere]
- Fix BytesWarning in Tests/test_numpy.py #3725
[jdufresne]
- Add missing MIME types and extensions #3520
[pirate486743186]
- Add I;16 PNG save #3566
[radarhere]
- Add support for BMP RGBA bitfield compression #3705
[radarhere]
- Added ability to set language for text rendering #3693
[iwsfutcmd]
- Only close exclusive fp on Image __exit__ #3698
[radarhere]
- Changed EPS subprocess stdout from devnull to None #3635
[radarhere]
- Add reading old-JPEG compressed TIFFs #3489
[kkopachev]
- Add EXIF support for PNG #3674
[radarhere]
@ -38,7 +128,7 @@ Changelog (Pillow)
- Make ContainerIO.isatty() return a bool, not int #3568
[jdufresne]
- Add support for I;16 modes for more transpose operations #3563
- Add support to all transpose operations for I;16 modes #3563, #3741
[radarhere]
- Deprecate support for PyQt4 and PySide #3655

View File

@ -20,6 +20,30 @@ Pillow is the friendly PIL fork by `Alex Clark and Contributors <https://github.
* - social
- |gitter| |twitter|
.. end-badges
More Information
----------------
- `Documentation <https://pillow.readthedocs.io/>`_
- `Installation <https://pillow.readthedocs.io/en/latest/installation.html>`_
- `Handbook <https://pillow.readthedocs.io/en/latest/handbook/index.html>`_
- `Contribute <https://github.com/python-pillow/Pillow/blob/master/.github/CONTRIBUTING.md>`_
- `Issues <https://github.com/python-pillow/Pillow/issues>`_
- `Pull requests <https://github.com/python-pillow/Pillow/pulls>`_
- `Changelog <https://github.com/python-pillow/Pillow/blob/master/CHANGES.rst>`_
- `Pre-fork <https://github.com/python-pillow/Pillow/blob/master/CHANGES.rst#pre-fork>`_
Report a Vulnerability
----------------------
To report a security vulnerability, please follow the procedure described in the `Tidelift security policy <https://tidelift.com/docs/security>`_.
.. |docs| image:: https://readthedocs.org/projects/pillow/badge/?version=latest
:target: https://pillow.readthedocs.io/?badge=latest
:alt: Documentation Status
@ -36,8 +60,8 @@ Pillow is the friendly PIL fork by `Alex Clark and Contributors <https://github.
:target: https://ci.appveyor.com/project/python-pillow/Pillow
:alt: AppVeyor CI build status (Windows)
.. |coverage| image:: https://coveralls.io/repos/python-pillow/Pillow/badge.svg?branch=master&service=github
:target: https://coveralls.io/github/python-pillow/Pillow?branch=master
.. |coverage| image:: https://codecov.io/gh/python-pillow/Pillow/branch/master/graph/badge.svg
:target: https://codecov.io/gh/python-pillow/Pillow
:alt: Code coverage
.. |zenodo| image:: https://zenodo.org/badge/17549/python-pillow/Pillow.svg
@ -61,24 +85,3 @@ Pillow is the friendly PIL fork by `Alex Clark and Contributors <https://github.
.. |twitter| image:: https://img.shields.io/badge/tweet-on%20Twitter-00aced.svg
:target: https://twitter.com/PythonPillow
:alt: Follow on https://twitter.com/PythonPillow
.. end-badges
More Information
----------------
- `Documentation <https://pillow.readthedocs.io/>`_
- `Installation <https://pillow.readthedocs.io/en/latest/installation.html>`_
- `Handbook <https://pillow.readthedocs.io/en/latest/handbook/index.html>`_
- `Contribute <https://github.com/python-pillow/Pillow/blob/master/.github/CONTRIBUTING.md>`_
- `Issues <https://github.com/python-pillow/Pillow/issues>`_
- `Pull requests <https://github.com/python-pillow/Pillow/pulls>`_
- `Changelog <https://github.com/python-pillow/Pillow/blob/master/CHANGES.rst>`_
- `Pre-fork <https://github.com/python-pillow/Pillow/blob/master/CHANGES.rst#pre-fork>`_

View File

@ -102,4 +102,4 @@ Released as needed privately to individual vendors for critical security-related
## Documentation
* [ ] Make sure the default version for Read the Docs is the latest tagged release e.g. `d2d43879` (5.4.0)
* [ ] Make sure the [default version for Read the Docs](https://pillow.readthedocs.io/en/stable/) is up-to-date with the release changes

BIN
Tests/images/1_trns.png Normal file

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(reloaded.format, "JPEG")
def test_load_dpi_rounding(self):
# Round up
im = Image.open('Tests/images/hopper.bmp')
self.assertEqual(im.info["dpi"], (96, 96))
# Round down
im = Image.open('Tests/images/hopper_roundDown.bmp')
self.assertEqual(im.info["dpi"], (72, 72))
def test_save_dpi_rounding(self):
outfile = self.tempfile("temp.bmp")
im = Image.open('Tests/images/hopper.bmp')
im.save(outfile, dpi=(72.2, 72.2))
reloaded = Image.open(outfile)
self.assertEqual(reloaded.info["dpi"], (72, 72))
im.save(outfile, dpi=(72.8, 72.8))
reloaded = Image.open(outfile)
self.assertEqual(reloaded.info["dpi"], (73, 73))
def test_load_dib(self):
# test for #1293, Imagegrab returning Unsupported Bitfields Format
im = Image.open('Tests/images/clipboard.dib')
@ -90,3 +111,15 @@ class TestFileBmp(PillowTestCase):
self.assertEqual(reloaded.format, "DIB")
self.assertEqual(reloaded.get_format_mimetype(), "image/bmp")
self.assert_image_equal(im, reloaded)
def test_rgba_bitfields(self):
# This test image has been manually hexedited
# to change the bitfield compression in the header from XBGR to RGBA
im = Image.open("Tests/images/rgb32bf-rgba.bmp")
# So before the comparing the image, swap the channels
b, g, r = im.split()[1:]
im = Image.merge("RGB", (r, g, b))
target = Image.open("Tests/images/bmp/q/rgb32bf-xbgr.bmp")
self.assert_image_equal(im, target)

View File

@ -230,6 +230,15 @@ class TestFileGif(PillowTestCase):
self.assertEqual(im.info, info)
def test_seek_rewind(self):
im = Image.open("Tests/images/iss634.gif")
im.seek(2)
im.seek(1)
expected = Image.open("Tests/images/iss634.gif")
expected.seek(1)
self.assert_image_equal(im, expected)
def test_n_frames(self):
for path, n_frames in [
[TEST_GIF, 1],

View File

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

View File

@ -524,6 +524,27 @@ class TestFileJpeg(PillowTestCase):
reloaded.load()
self.assertEqual(im.info['dpi'], reloaded.info['dpi'])
def test_load_dpi_rounding(self):
# Round up
im = Image.open('Tests/images/iptc_roundUp.jpg')
self.assertEqual(im.info["dpi"], (44, 44))
# Round down
im = Image.open('Tests/images/iptc_roundDown.jpg')
self.assertEqual(im.info["dpi"], (2, 2))
def test_save_dpi_rounding(self):
outfile = self.tempfile("temp.jpg")
im = Image.open('Tests/images/hopper.jpg')
im.save(outfile, dpi=(72.2, 72.2))
reloaded = Image.open(outfile)
self.assertEqual(reloaded.info["dpi"], (72, 72))
im.save(outfile, dpi=(72.8, 72.8))
reloaded = Image.open(outfile)
self.assertEqual(reloaded.info["dpi"], (73, 73))
def test_dpi_tuple_from_exif(self):
# Arrange
# This Photoshop CC 2017 image has DPI in EXIF not metadata
@ -590,6 +611,15 @@ class TestFileJpeg(PillowTestCase):
# Act / Assert
self.assertEqual(im._getexif()[306], '2017:03:13 23:03:09')
def test_photoshop(self):
im = Image.open("Tests/images/photoshop-200dpi.jpg")
self.assertEqual(im.info["photoshop"][0x03ed], {
'XResolution': 200.0,
'DisplayedUnitsX': 1,
'YResolution': 200.0,
'DisplayedUnitsY': 1,
})
@unittest.skipUnless(sys.platform.startswith('win32'), "Windows only")
class TestFileCloseW32(PillowTestCase):

View File

@ -716,3 +716,10 @@ class TestFileLibTiff(LibTiffTestCase):
im = Image.open(infile)
self.assert_image_similar_tofile(im, "Tests/images/flower.jpg", 0.5)
def test_old_style_jpeg(self):
infile = "Tests/images/old-style-jpeg-compression.tif"
im = Image.open(infile)
self.assert_image_equal_tofile(im,
"Tests/images/old-style-jpeg-compression.png")

View File

@ -55,6 +55,27 @@ class TestFileMpo(PillowTestCase):
self.assertEqual(info[296], 2)
self.assertEqual(info[34665], 188)
def test_frame_size(self):
# This image has been hexedited to contain a different size
# in the EXIF data of the second frame
im = Image.open("Tests/images/sugarshack_frame_size.mpo")
self.assertEqual(im.size, (640, 480))
im.seek(1)
self.assertEqual(im.size, (680, 480))
def test_parallax(self):
# Nintendo
im = Image.open("Tests/images/sugarshack.mpo")
exif = im.getexif()
self.assertEqual(exif.get_ifd(0x927c)[0x1101]["Parallax"], -44.798187255859375)
# Fujifilm
im = Image.open("Tests/images/fujifilm.mpo")
im.seek(1)
exif = im.getexif()
self.assertEqual(exif.get_ifd(0x927c)[0xb211], -3.125)
def test_mp(self):
for test_file in test_files:
im = Image.open(test_file)

View File

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

View File

@ -88,20 +88,13 @@ class TestFilePng(PillowTestCase):
self.assertEqual(im.format, "PNG")
self.assertEqual(im.get_format_mimetype(), 'image/png')
hopper("1").save(test_file)
Image.open(test_file)
hopper("L").save(test_file)
Image.open(test_file)
hopper("P").save(test_file)
Image.open(test_file)
hopper("RGB").save(test_file)
Image.open(test_file)
hopper("I").save(test_file)
Image.open(test_file)
for mode in ["1", "L", "P", "RGB", "I", "I;16"]:
im = hopper(mode)
im.save(test_file)
reloaded = Image.open(test_file)
if mode == "I;16":
reloaded = reloaded.convert(mode)
self.assert_image_equal(reloaded, im)
def test_invalid_file(self):
invalid_file = "Tests/images/flower.jpg"
@ -298,30 +291,32 @@ class TestFilePng(PillowTestCase):
self.assert_image(im, "RGBA", (10, 10))
self.assertEqual(im.getcolors(), [(100, (0, 0, 0, 0))])
def test_save_l_transparency(self):
# There are 559 transparent pixels in l_trns.png.
num_transparent = 559
def test_save_greyscale_transparency(self):
for mode, num_transparent in {
"1": 1994,
"L": 559,
"I": 559,
}.items():
in_file = "Tests/images/"+mode.lower()+"_trns.png"
im = Image.open(in_file)
self.assertEqual(im.mode, mode)
self.assertEqual(im.info["transparency"], 255)
in_file = "Tests/images/l_trns.png"
im = Image.open(in_file)
self.assertEqual(im.mode, "L")
self.assertEqual(im.info["transparency"], 255)
im_rgba = im.convert('RGBA')
self.assertEqual(
im_rgba.getchannel("A").getcolors()[0][0], num_transparent)
im_rgba = im.convert('RGBA')
self.assertEqual(
im_rgba.getchannel("A").getcolors()[0][0], num_transparent)
test_file = self.tempfile("temp.png")
im.save(test_file)
test_file = self.tempfile("temp.png")
im.save(test_file)
test_im = Image.open(test_file)
self.assertEqual(test_im.mode, mode)
self.assertEqual(test_im.info["transparency"], 255)
self.assert_image_equal(im, test_im)
test_im = Image.open(test_file)
self.assertEqual(test_im.mode, "L")
self.assertEqual(test_im.info["transparency"], 255)
self.assert_image_equal(im, test_im)
test_im_rgba = test_im.convert('RGBA')
self.assertEqual(
test_im_rgba.getchannel('A').getcolors()[0][0], num_transparent)
test_im_rgba = test_im.convert('RGBA')
self.assertEqual(
test_im_rgba.getchannel('A').getcolors()[0][0], num_transparent)
def test_save_rgb_single_transparency(self):
in_file = "Tests/images/caption_6_33_22.png"
@ -394,6 +389,24 @@ class TestFilePng(PillowTestCase):
im = roundtrip(im, dpi=(100, 100))
self.assertEqual(im.info["dpi"], (100, 100))
def test_load_dpi_rounding(self):
# Round up
im = Image.open(TEST_PNG_FILE)
self.assertEqual(im.info["dpi"], (96, 96))
# Round down
im = Image.open("Tests/images/icc_profile_none.png")
self.assertEqual(im.info["dpi"], (72, 72))
def test_save_dpi_rounding(self):
im = Image.open(TEST_PNG_FILE)
im = roundtrip(im, dpi=(72.2, 72.2))
self.assertEqual(im.info["dpi"], (72, 72))
im = roundtrip(im, dpi=(72.8, 72.8))
self.assertEqual(im.info["dpi"], (73, 73))
def test_roundtrip_text(self):
# Check text roundtripping

View File

@ -1,4 +1,4 @@
from .helper import PillowTestCase
from .helper import PillowTestCase, hopper
from PIL import Image
@ -36,6 +36,16 @@ class TestFilePpm(PillowTestCase):
reloaded = Image.open(f)
self.assert_image_equal(im, reloaded)
def test_pnm(self):
im = Image.open('Tests/images/hopper.pnm')
self.assert_image_similar(im, hopper(), 0.0001)
f = self.tempfile('temp.pnm')
im.save(f)
reloaded = Image.open(f)
self.assert_image_equal(im, reloaded)
def test_truncated_file(self):
path = self.tempfile('temp.pgm')
with open(path, 'w') as f:

View File

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

View File

@ -126,6 +126,30 @@ class TestFileTiff(PillowTestCase):
im._setup()
self.assertEqual(im.info['dpi'], (71., 71.))
def test_load_dpi_rounding(self):
for resolutionUnit, dpi in ((None, (72, 73)),
(2, (72, 73)),
(3, (183, 185))):
im = Image.open(
"Tests/images/hopper_roundDown_"+str(resolutionUnit)+".tif")
self.assertEqual(im.tag_v2.get(RESOLUTION_UNIT), resolutionUnit)
self.assertEqual(im.info['dpi'], (dpi[0], dpi[0]))
im = Image.open("Tests/images/hopper_roundUp_"+str(resolutionUnit)+".tif")
self.assertEqual(im.tag_v2.get(RESOLUTION_UNIT), resolutionUnit)
self.assertEqual(im.info['dpi'], (dpi[1], dpi[1]))
def test_save_dpi_rounding(self):
outfile = self.tempfile("temp.tif")
im = Image.open("Tests/images/hopper.tif")
for dpi in (72.2, 72.8):
im.save(outfile, dpi=(dpi, dpi))
reloaded = Image.open(outfile)
reloaded.load()
self.assertEqual((round(dpi), round(dpi)), reloaded.info['dpi'])
def test_save_setting_missing_resolution(self):
b = BytesIO()
Image.open("Tests/images/10ct_32bit_128.tiff").save(
@ -229,11 +253,6 @@ class TestFileTiff(PillowTestCase):
['Tests/images/multipage-lastframe.tif', 1],
['Tests/images/multipage.tiff', 3]
]:
# Test is_animated before n_frames
im = Image.open(path)
self.assertEqual(im.is_animated, n_frames != 1)
# Test is_animated after n_frames
im = Image.open(path)
self.assertEqual(im.n_frames, n_frames)
self.assertEqual(im.is_animated, n_frames != 1)
@ -263,6 +282,11 @@ class TestFileTiff(PillowTestCase):
self.assertEqual(im.size, (10, 10))
self.assertEqual(im.convert('RGB').getpixel((0, 0)), (255, 0, 0))
im.seek(0)
im.load()
self.assertEqual(im.size, (10, 10))
self.assertEqual(im.convert('RGB').getpixel((0, 0)), (0, 128, 0))
im.seek(2)
im.load()
self.assertEqual(im.size, (20, 20))

View File

@ -45,6 +45,15 @@ class TestFileWmf(PillowTestCase):
# Restore the state before this test
WmfImagePlugin.register_handler(None)
def test_load_dpi_rounding(self):
# Round up
im = Image.open('Tests/images/drawing.emf')
self.assertEqual(im.info["dpi"], 1424)
# Round down
im = Image.open('Tests/images/drawing_roundDown.emf')
self.assertEqual(im.info["dpi"], 1426)
def test_save(self):
im = hopper()

View File

@ -3,6 +3,8 @@ from .helper import unittest, PillowTestCase, hopper
from PIL import Image
from PIL._util import py3
import os
import sys
import shutil
class TestImage(PillowTestCase):
@ -121,6 +123,16 @@ class TestImage(PillowTestCase):
im.paste(0, (0, 0, 100, 100))
self.assertFalse(im.readonly)
@unittest.skipIf(sys.platform.startswith('win32'),
"Test requires opening tempfile twice")
def test_readonly_save(self):
temp_file = self.tempfile("temp.bmp")
shutil.copy("Tests/images/rgb32bf-rgba.bmp", temp_file)
im = Image.open(temp_file)
self.assertTrue(im.readonly)
im.save(temp_file)
def test_dump(self):
im = Image.new("L", (10, 10))
im._dump(self.tempfile("temp_L.ppm"))
@ -522,6 +534,16 @@ class TestImage(PillowTestCase):
_make_new(im, blank_p, ImagePalette.ImagePalette())
_make_new(im, blank_pa, ImagePalette.ImagePalette())
def test_p_from_rgb_rgba(self):
for mode, color in [
("RGB", '#DDEEFF'),
("RGB", (221, 238, 255)),
("RGBA", (221, 238, 255, 255))
]:
im = Image.new("P", (100, 100), color)
expected = Image.new(mode, (100, 100), color)
self.assert_image_equal(im.convert(mode), expected)
def test_no_resource_warning_on_save(self):
# https://github.com/python-pillow/Pillow/issues/835
# Arrange
@ -532,6 +554,18 @@ class TestImage(PillowTestCase):
with Image.open(test_file) as im:
self.assert_warning(None, im.save, temp_file)
def test_load_on_nonexclusive_multiframe(self):
with open("Tests/images/frozenpond.mpo", "rb") as fp:
def act(fp):
im = Image.open(fp)
im.load()
act(fp)
with Image.open(fp) as im:
im.load()
self.assertFalse(fp.closed)
class MockEncoder(object):
pass

View File

@ -12,7 +12,8 @@ class TestImageConvert(PillowTestCase):
self.assertEqual(out.mode, mode)
self.assertEqual(out.size, im.size)
modes = "1", "L", "I", "F", "RGB", "RGBA", "RGBX", "CMYK", "YCbCr"
modes = ("1", "L", "LA", "P", "PA", "I", "F",
"RGB", "RGBA", "RGBX", "CMYK", "YCbCr")
for mode in modes:
im = hopper(mode)

View File

@ -28,3 +28,10 @@ class TestImageLoad(PillowTestCase):
os.fstat(fn)
self.assertRaises(OSError, os.fstat, fn)
def test_contextmanager_non_exclusive_fp(self):
with open("Tests/images/hopper.gif", "rb") as fp:
with Image.open(fp):
pass
self.assertFalse(fp.closed)

View File

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

View File

@ -28,6 +28,13 @@ class TestImagePutAlpha(PillowTestCase):
self.assertEqual(im.mode, 'LA')
self.assertEqual(im.getpixel((0, 0)), (1, 2))
im = Image.new("P", (1, 1), 1)
self.assertEqual(im.getpixel((0, 0)), 1)
im.putalpha(2)
self.assertEqual(im.mode, 'PA')
self.assertEqual(im.getpixel((0, 0)), (1, 2))
im = Image.new("RGB", (1, 1), (1, 2, 3))
self.assertEqual(im.getpixel((0, 0)), (1, 2, 3))

View File

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

View File

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

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((x-2, y-2)), out.getpixel((1, y-2)))
for mode in ("L", "RGB", "I;16", "I;16L", "I;16B"):
for mode in self.hopper:
transpose(mode)
def test_flip_top_bottom(self):
@ -40,7 +40,7 @@ class TestImageTranspose(PillowTestCase):
self.assertEqual(im.getpixel((1, y-2)), out.getpixel((1, 1)))
self.assertEqual(im.getpixel((x-2, y-2)), out.getpixel((x-2, 1)))
for mode in ("L", "RGB", "I;16", "I;16L", "I;16B"):
for mode in self.hopper:
transpose(mode)
def test_rotate_90(self):
@ -56,7 +56,7 @@ class TestImageTranspose(PillowTestCase):
self.assertEqual(im.getpixel((1, y-2)), out.getpixel((y-2, x-2)))
self.assertEqual(im.getpixel((x-2, y-2)), out.getpixel((y-2, 1)))
for mode in ("L", "RGB"):
for mode in self.hopper:
transpose(mode)
def test_rotate_180(self):
@ -72,7 +72,7 @@ class TestImageTranspose(PillowTestCase):
self.assertEqual(im.getpixel((1, y-2)), out.getpixel((x-2, 1)))
self.assertEqual(im.getpixel((x-2, y-2)), out.getpixel((1, 1)))
for mode in ("L", "RGB", "I;16", "I;16L", "I;16B"):
for mode in self.hopper:
transpose(mode)
def test_rotate_270(self):
@ -88,7 +88,7 @@ class TestImageTranspose(PillowTestCase):
self.assertEqual(im.getpixel((1, y-2)), out.getpixel((1, 1)))
self.assertEqual(im.getpixel((x-2, y-2)), out.getpixel((1, x-2)))
for mode in ("L", "RGB"):
for mode in self.hopper:
transpose(mode)
def test_transpose(self):
@ -104,7 +104,7 @@ class TestImageTranspose(PillowTestCase):
self.assertEqual(im.getpixel((1, y-2)), out.getpixel((y-2, 1)))
self.assertEqual(im.getpixel((x-2, y-2)), out.getpixel((y-2, x-2)))
for mode in ("L", "RGB"):
for mode in self.hopper:
transpose(mode)
def test_tranverse(self):
@ -120,28 +120,29 @@ class TestImageTranspose(PillowTestCase):
self.assertEqual(im.getpixel((1, y-2)), out.getpixel((1, x-2)))
self.assertEqual(im.getpixel((x-2, y-2)), out.getpixel((1, 1)))
for mode in ("L", "RGB"):
for mode in self.hopper:
transpose(mode)
def test_roundtrip(self):
im = self.hopper['L']
for mode in self.hopper:
im = self.hopper[mode]
def transpose(first, second):
return im.transpose(first).transpose(second)
def transpose(first, second):
return im.transpose(first).transpose(second)
self.assert_image_equal(
im, transpose(FLIP_LEFT_RIGHT, FLIP_LEFT_RIGHT))
self.assert_image_equal(
im, transpose(FLIP_TOP_BOTTOM, FLIP_TOP_BOTTOM))
self.assert_image_equal(im, transpose(ROTATE_90, ROTATE_270))
self.assert_image_equal(im, transpose(ROTATE_180, ROTATE_180))
self.assert_image_equal(
im.transpose(TRANSPOSE), transpose(ROTATE_90, FLIP_TOP_BOTTOM))
self.assert_image_equal(
im.transpose(TRANSPOSE), transpose(ROTATE_270, FLIP_LEFT_RIGHT))
self.assert_image_equal(
im.transpose(TRANSVERSE), transpose(ROTATE_90, FLIP_LEFT_RIGHT))
self.assert_image_equal(
im.transpose(TRANSVERSE), transpose(ROTATE_270, FLIP_TOP_BOTTOM))
self.assert_image_equal(
im.transpose(TRANSVERSE), transpose(ROTATE_180, TRANSPOSE))
self.assert_image_equal(
im, transpose(FLIP_LEFT_RIGHT, FLIP_LEFT_RIGHT))
self.assert_image_equal(
im, transpose(FLIP_TOP_BOTTOM, FLIP_TOP_BOTTOM))
self.assert_image_equal(im, transpose(ROTATE_90, ROTATE_270))
self.assert_image_equal(im, transpose(ROTATE_180, ROTATE_180))
self.assert_image_equal(
im.transpose(TRANSPOSE), transpose(ROTATE_90, FLIP_TOP_BOTTOM))
self.assert_image_equal(
im.transpose(TRANSPOSE), transpose(ROTATE_270, FLIP_LEFT_RIGHT))
self.assert_image_equal(
im.transpose(TRANSVERSE), transpose(ROTATE_90, FLIP_LEFT_RIGHT))
self.assert_image_equal(
im.transpose(TRANSVERSE), transpose(ROTATE_270, FLIP_TOP_BOTTOM))
self.assert_image_equal(
im.transpose(TRANSVERSE), transpose(ROTATE_180, TRANSPOSE))

View File

@ -309,7 +309,7 @@ class TestImageCms(PillowTestCase):
2: (False, False, True),
3: (False, False, True)
})
self.assertEqual(p.color_space, 'RGB')
self.assertIsNone(p.colorant_table)
self.assertIsNone(p.colorant_table_out)
self.assertIsNone(p.colorimetric_intent)
@ -361,16 +361,9 @@ class TestImageCms(PillowTestCase):
(5000.722328847392,))
self.assertEqual(p.model,
'IEC 61966-2-1 Default RGB Colour Space - sRGB')
self.assertEqual(p.pcs, 'XYZ')
self.assertIsNone(p.perceptual_rendering_intent_gamut)
self.assertEqual(p.product_copyright,
'Copyright International Color Consortium, 2009')
self.assertEqual(p.product_desc, 'sRGB IEC61966-2-1 black scaled')
self.assertEqual(p.product_description,
'sRGB IEC61966-2-1 black scaled')
self.assertEqual(p.product_manufacturer, '')
self.assertEqual(
p.product_model, 'IEC 61966-2-1 Default RGB Colour Space - sRGB')
self.assertEqual(
p.profile_description, 'sRGB IEC61966-2-1 black scaled')
self.assertEqual(
@ -393,6 +386,40 @@ class TestImageCms(PillowTestCase):
'Reference Viewing Condition in IEC 61966-2-1')
self.assertEqual(p.xcolor_space, 'RGB ')
def test_deprecations(self):
self.skip_missing()
o = ImageCms.getOpenProfile(SRGB)
p = o.profile
def helper_deprecated(attr, expected):
result = self.assert_warning(DeprecationWarning, getattr, p, attr)
self.assertEqual(result, expected)
# p.color_space
helper_deprecated("color_space", "RGB")
# p.pcs
helper_deprecated("pcs", "XYZ")
# p.product_copyright
helper_deprecated(
"product_copyright", "Copyright International Color Consortium, 2009"
)
# p.product_desc
helper_deprecated("product_desc", "sRGB IEC61966-2-1 black scaled")
# p.product_description
helper_deprecated("product_description", "sRGB IEC61966-2-1 black scaled")
# p.product_manufacturer
helper_deprecated("product_manufacturer", "")
# p.product_model
helper_deprecated(
"product_model", "IEC 61966-2-1 Default RGB Colour Space - sRGB"
)
def test_profile_typesafety(self):
""" Profile init type safety

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
@ -6,6 +6,12 @@ from PIL import Image
from PIL import ImageFile
from PIL import EpsImagePlugin
try:
from PIL import _webp
HAVE_WEBP = True
except ImportError:
HAVE_WEBP = False
codecs = dir(Image.core)
@ -233,3 +239,97 @@ class TestPyDecoder(PillowTestCase):
im = MockImageFile(buf)
self.assertIsNone(im.format)
self.assertIsNone(im.get_format_mimetype())
def test_exif_jpeg(self):
im = Image.open("Tests/images/exif-72dpi-int.jpg") # Little endian
exif = im.getexif()
self.assertNotIn(258, exif)
self.assertIn(40960, exif)
self.assertEqual(exif[40963], 450)
self.assertEqual(exif[11], "gThumb 3.0.1")
out = self.tempfile('temp.jpg')
exif[258] = 8
del exif[40960]
exif[40963] = 455
exif[11] = "Pillow test"
im.save(out, exif=exif)
reloaded = Image.open(out)
reloaded_exif = reloaded.getexif()
self.assertEqual(reloaded_exif[258], 8)
self.assertNotIn(40960, exif)
self.assertEqual(reloaded_exif[40963], 455)
self.assertEqual(exif[11], "Pillow test")
im = Image.open("Tests/images/no-dpi-in-exif.jpg") # Big endian
exif = im.getexif()
self.assertNotIn(258, exif)
self.assertIn(40962, exif)
self.assertEqual(exif[40963], 200)
self.assertEqual(exif[305], "Adobe Photoshop CC 2017 (Macintosh)")
out = self.tempfile('temp.jpg')
exif[258] = 8
del exif[34665]
exif[40963] = 455
exif[305] = "Pillow test"
im.save(out, exif=exif)
reloaded = Image.open(out)
reloaded_exif = reloaded.getexif()
self.assertEqual(reloaded_exif[258], 8)
self.assertNotIn(40960, exif)
self.assertEqual(reloaded_exif[40963], 455)
self.assertEqual(exif[305], "Pillow test")
@unittest.skipIf(not HAVE_WEBP or not _webp.HAVE_WEBPANIM,
"WebP support not installed with animation")
def test_exif_webp(self):
im = Image.open("Tests/images/hopper.webp")
exif = im.getexif()
self.assertEqual(exif, {})
out = self.tempfile('temp.webp')
exif[258] = 8
exif[40963] = 455
exif[305] = "Pillow test"
def check_exif():
reloaded = Image.open(out)
reloaded_exif = reloaded.getexif()
self.assertEqual(reloaded_exif[258], 8)
self.assertEqual(reloaded_exif[40963], 455)
self.assertEqual(exif[305], "Pillow test")
im.save(out, exif=exif)
check_exif()
im.save(out, exif=exif, save_all=True)
check_exif()
def test_exif_png(self):
im = Image.open("Tests/images/exif.png")
exif = im.getexif()
self.assertEqual(exif, {274: 1})
out = self.tempfile('temp.png')
exif[258] = 8
del exif[274]
exif[40963] = 455
exif[305] = "Pillow test"
im.save(out, exif=exif)
reloaded = Image.open(out)
reloaded_exif = reloaded.getexif()
self.assertEqual(reloaded_exif, {
258: 8,
40963: 455,
305: 'Pillow test',
})
def test_exif_interop(self):
im = Image.open("Tests/images/flower.jpg")
exif = im.getexif()
self.assertEqual(exif.get_ifd(0xa005), {
1: 'R98',
2: b'0100',
4097: 2272,
4098: 1704,
})

View File

@ -6,6 +6,8 @@ from io import BytesIO
import os
import sys
import copy
import re
import distutils.version
FONT_PATH = "Tests/fonts/FreeMono.ttf"
FONT_SIZE = 20
@ -49,29 +51,40 @@ class TestImageFont(PillowTestCase):
# Freetype has different metrics depending on the version.
# (and, other things, but first things first)
METRICS = {
('2', '3'): {'multiline': 30,
'textsize': 12,
'getters': (13, 16)},
('2', '7'): {'multiline': 6.2,
'textsize': 2.5,
'getters': (12, 16)},
('2', '8'): {'multiline': 6.2,
'textsize': 2.5,
'getters': (12, 16)},
('2', '9'): {'multiline': 6.2,
'textsize': 2.5,
'getters': (12, 16)},
'Default': {'multiline': 0.5,
'textsize': 0.5,
'getters': (12, 16)},
('>=2.3', '<2.4'): {
'multiline': 30,
'textsize': 12,
'getters': (13, 16)},
('>=2.7',): {
'multiline': 6.2,
'textsize': 2.5,
'getters': (12, 16)},
'Default': {
'multiline': 0.5,
'textsize': 0.5,
'getters': (12, 16)},
}
def setUp(self):
freetype_version = tuple(
ImageFont.core.freetype2_version.split('.')
)[:2]
self.metrics = self.METRICS.get(freetype_version,
self.METRICS['Default'])
freetype = distutils.version.StrictVersion(ImageFont.core.freetype2_version)
self.metrics = self.METRICS['Default']
for conditions, metrics in self.METRICS.items():
if not isinstance(conditions, tuple):
continue
for condition in conditions:
version = re.sub('[<=>]', '', condition)
if (condition.startswith('>=') and freetype >= version) or \
(condition.startswith('<') and freetype < version):
# Condition was met
continue
# Condition failed
break
else:
# All conditions were met
self.metrics = metrics
def get_font(self):
return ImageFont.truetype(FONT_PATH, FONT_SIZE,
@ -525,6 +538,15 @@ class TestImageFont(PillowTestCase):
self.assertEqual(t.getsize_multiline('ABC\nA'), (36, 36))
self.assertEqual(t.getsize_multiline('ABC\nAaaa'), (48, 36))
def test_complex_font_settings(self):
# Arrange
t = self.get_font()
# Act / Assert
if t.layout_engine == ImageFont.LAYOUT_BASIC:
self.assertRaises(KeyError, t.getmask, 'абвг', direction='rtl')
self.assertRaises(KeyError, t.getmask, 'абвг', features=['-kern'])
self.assertRaises(KeyError, t.getmask, 'абвг', language='sr')
@unittest.skipUnless(HAS_RAQM, "Raqm not Available")
class TestImageFont_RaqmLayout(TestImageFont):

View File

@ -130,3 +130,16 @@ class TestImagecomplextext(PillowTestCase):
target_img = Image.open(target)
self.assert_image_similar(im, target_img, .5)
def test_language(self):
ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE)
im = Image.new(mode='RGB', size=(300, 100))
draw = ImageDraw.Draw(im)
draw.text((0, 0), 'абвг', font=ttf, fill=500,
language='sr')
target = 'Tests/images/test_language.png'
target_img = Image.open(target)
self.assert_image_similar(im, target_img, .5)

View File

@ -1,7 +1,13 @@
from .helper import PillowTestCase, hopper
from PIL import ImageOps
from PIL import Image
from PIL import ImageOps
try:
from PIL import _webp
HAVE_WEBP = True
except ImportError:
HAVE_WEBP = False
class TestImageOps(PillowTestCase):
@ -62,6 +68,9 @@ class TestImageOps(PillowTestCase):
ImageOps.solarize(hopper("L"))
ImageOps.solarize(hopper("RGB"))
ImageOps.exif_transpose(hopper("L"))
ImageOps.exif_transpose(hopper("RGB"))
def test_1pxfit(self):
# Division by zero in equalize if image is 1 pixel high
newimg = ImageOps.fit(hopper("RGB").resize((1, 1)), (35, 35))
@ -218,3 +227,36 @@ class TestImageOps(PillowTestCase):
(0, 127, 0),
threshold=1,
msg='white test pixel incorrect')
def test_exif_transpose(self):
exts = [".jpg"]
if HAVE_WEBP and _webp.HAVE_WEBPANIM:
exts.append(".webp")
for ext in exts:
base_im = Image.open("Tests/images/hopper"+ext)
orientations = [base_im]
for i in range(2, 9):
im = Image.open("Tests/images/hopper_orientation_"+str(i)+ext)
orientations.append(im)
for i, orientation_im in enumerate(orientations):
for im in [
orientation_im, # ImageFile
orientation_im.copy() # Image
]:
if i == 0:
self.assertNotIn("exif", im.info)
else:
original_exif = im.info["exif"]
transposed_im = ImageOps.exif_transpose(im)
self.assert_image_similar(base_im, transposed_im, 17)
if i == 0:
self.assertNotIn("exif", im.info)
else:
self.assertNotEqual(transposed_im.info["exif"], original_exif)
self.assertNotIn(0x0112, transposed_im.getexif())
# Repeat the operation, to test that it does not keep transposing
transposed_im2 = ImageOps.exif_transpose(transposed_im)
self.assert_image_equal(transposed_im2, transposed_im)

View File

@ -113,12 +113,7 @@ class TestNumpy(PillowTestCase):
img = Image.fromarray(arr * 255).convert('1')
self.assertEqual(img.mode, '1')
arr_back = numpy.array(img)
# numpy 1.8 and earlier return this as a boolean. (trusty/precise)
if arr_back.dtype == numpy.bool:
arr_bool = numpy.array([[1, 0, 0, 1, 0], [0, 1, 0, 0, 0]], 'bool')
numpy.testing.assert_array_equal(arr_bool, arr_back)
else:
numpy.testing.assert_array_equal(arr, arr_back)
numpy.testing.assert_array_equal(arr, arr_back)
def test_save_tiff_uint16(self):
# Tests that we're getting the pixel value in the right byte order.
@ -143,21 +138,21 @@ class TestNumpy(PillowTestCase):
np_img = numpy.array(img)
self._test_img_equals_nparray(img, np_img)
self.assertEqual(np_img.dtype, numpy.dtype(dtype))
self.assertEqual(np_img.dtype, dtype)
modes = [("L", 'uint8'),
("I", 'int32'),
("F", 'float32'),
("LA", 'uint8'),
("RGB", 'uint8'),
("RGBA", 'uint8'),
("RGBX", 'uint8'),
("CMYK", 'uint8'),
("YCbCr", 'uint8'),
modes = [("L", numpy.uint8),
("I", numpy.int32),
("F", numpy.float32),
("LA", numpy.uint8),
("RGB", numpy.uint8),
("RGBA", numpy.uint8),
("RGBX", numpy.uint8),
("CMYK", numpy.uint8),
("YCbCr", numpy.uint8),
("I;16", '<u2'),
("I;16B", '>u2'),
("I;16L", '<u2'),
("HSV", 'uint8'),
("HSV", numpy.uint8),
]
for mode in modes:
@ -167,7 +162,7 @@ class TestNumpy(PillowTestCase):
# see https://github.com/python-pillow/Pillow/issues/439
data = list(range(256))*3
lut = numpy.array(data, dtype='uint8')
lut = numpy.array(data, dtype=numpy.uint8)
im = hopper()

View File

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

View File

@ -11,17 +11,6 @@ class TestFromQPixmap(PillowQPixmapTestCase, PillowTestCase):
# Qt saves all pixmaps as rgb
self.assert_image_equal(result, expected.convert('RGB'))
def test_sanity_1(self):
self.roundtrip(hopper('1'))
def test_sanity_rgb(self):
self.roundtrip(hopper('RGB'))
def test_sanity_rgba(self):
self.roundtrip(hopper('RGBA'))
def test_sanity_l(self):
self.roundtrip(hopper('L'))
def test_sanity_p(self):
self.roundtrip(hopper('P'))
def test_sanity(self):
for mode in ('1', 'RGB', 'RGBA', 'L', 'P'):
self.roundtrip(hopper(mode))

View File

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

View File

@ -79,6 +79,26 @@ PILLOW_VERSION constant
``PILLOW_VERSION`` has been deprecated and will be removed in the next
major release. Use ``__version__`` instead.
ImageCms.CmsProfile attributes
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. deprecated:: 3.2.0
Some attributes in ``ImageCms.CmsProfile`` are deprecated. From 6.0.0, they issue a
``DeprecationWarning``:
======================== ===============================
Deprecated Use instead
======================== ===============================
``color_space`` Padded ``xcolor_space``
``pcs`` Padded ``connection_space``
``product_copyright`` Unicode ``copyright``
``product_desc`` Unicode ``profile_description``
``product_description`` Unicode ``profile_description``
``product_manufacturer`` Unicode ``manufacturer``
``product_model`` Unicode ``model``
======================== ===============================
Removed features
----------------

View File

@ -104,11 +104,11 @@ The :py:meth:`~PIL.Image.Image.open` method sets the following
Reading sequences
~~~~~~~~~~~~~~~~~
The GIF loader supports the :py:meth:`~file.seek` and :py:meth:`~file.tell`
methods. You can seek to the next frame (``im.seek(im.tell() + 1)``), or rewind
the file by seeking to the first frame. Random access is not supported.
The GIF loader supports the :py:meth:`~PIL.Image.Image.seek` and
:py:meth:`~PIL.Image.Image.tell` methods. You can combine these methods
to seek to the next frame (``im.seek(im.tell() + 1)``).
``im.seek()`` raises an ``EOFError`` if you try to seek after the last frame.
``im.seek()`` raises an :py:exc:`EOFError` if you try to seek after the last frame.
Saving
~~~~~~
@ -459,12 +459,14 @@ Pillow reads and writes PCX files containing ``1``, ``L``, ``P``, or ``RGB`` dat
PNG
^^^
Pillow identifies, reads, and writes PNG files containing ``1``, ``L``, ``P``,
``RGB``, or ``RGBA`` data. Interlaced files are supported as of v1.1.7.
Pillow identifies, reads, and writes PNG files containing ``1``, ``L``, ``LA``,
``I``, ``P``, ``RGB`` or ``RGBA`` data. Interlaced files are supported as of
v1.1.7.
As of Pillow 6.0, EXIF data can be read from PNG images. However, unlike other
image formats, EXIF data is not guaranteed to have been read until
:py:meth:`~PIL.Image.Image.load` has been called.
image formats, EXIF data is not guaranteed to be present in
:py:attr:`~PIL.Image.Image.info` until :py:meth:`~PIL.Image.Image.load` has been
called.
The :py:meth:`~PIL.Image.Image.open` method sets the following
:py:attr:`~PIL.Image.Image.info` properties, when appropriate:
@ -489,12 +491,12 @@ The :py:meth:`~PIL.Image.Image.open` method sets the following
For ``P`` images: Either the palette index for full transparent pixels,
or a byte string with alpha values for each palette entry.
For ``L`` and ``RGB`` images, the color that represents full transparent
pixels in this image.
For ``1``, ``L``, ``I`` and ``RGB`` images, the color that represents
full transparent pixels in this image.
This key is omitted if the image is not a transparent palette image.
``Open`` also sets ``Image.text`` to a dictionary of the values of the
``open`` also sets ``Image.text`` to a dictionary of the values of the
``tEXt``, ``zTXt``, and ``iTXt`` chunks of the PNG image. Individual
compressed chunks are limited to a decompressed size of
``PngImagePlugin.MAX_TEXT_CHUNK``, by default 1MB, to prevent
@ -510,8 +512,8 @@ The :py:meth:`~PIL.Image.Image.save` method supports the following options:
encoder settings.
**transparency**
For ``P``, ``L``, and ``RGB`` images, this option controls what
color image to mark as transparent.
For ``P``, ``1``, ``L``, ``I``, and ``RGB`` images, this option controls
what color from the image to mark as transparent.
For ``P`` images, this can be a either the palette index,
or a byte string with alpha values for each palette entry.
@ -552,7 +554,7 @@ The :py:meth:`~PIL.Image.Image.save` method supports the following options:
PPM
^^^
Pillow reads and writes PBM, PGM and PPM files containing ``1``, ``L`` or
Pillow reads and writes PBM, PGM, PPM and PNM files containing ``1``, ``L`` or
``RGB`` data.
SGI
@ -568,7 +570,7 @@ Pillow reads and writes SPIDER image files of 32-bit floating point data
("F;32F").
Pillow also reads SPIDER stack files containing sequences of SPIDER images. The
:py:meth:`~file.seek` and :py:meth:`~file.tell` methods are supported, and
:py:meth:`~PIL.Image.Image.seek` and :py:meth:`~PIL.Image.Image.tell` methods are supported, and
random access is allowed.
The :py:meth:`~PIL.Image.Image.open` method sets the following attributes:
@ -579,7 +581,7 @@ The :py:meth:`~PIL.Image.Image.open` method sets the following attributes:
**istack**
Set to 1 if the file is an image stack, else 0.
**nimages**
**n_frames**
Set to the number of images in the stack.
A convenience method, :py:meth:`~PIL.Image.Image.convert2byte`, is provided for
@ -663,6 +665,17 @@ numbers are returned as a tuple of ``(numerator, denominator)``.
.. deprecated:: 3.0.0
Reading Multi-frame TIFF Images
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The TIFF loader supports the :py:meth:`~PIL.Image.Image.seek` and
:py:meth:`~PIL.Image.Image.tell` methods, taking and returning frame numbers
within the image file. You can combine these methods to seek to the next frame
(``im.seek(im.tell() + 1)``). Frames are numbered from 0 to ``im.num_frames - 1``,
and can be accessed in any order.
``im.seek()`` raises an :py:exc:`EOFError` if you try to seek after the
last frame.
Saving Tiff Images
~~~~~~~~~~~~~~~~~~
@ -850,7 +863,7 @@ is commonly used in fax applications. The DCX decoder can read files containing
``1``, ``L``, ``P``, or ``RGB`` data.
When the file is opened, only the first image is read. You can use
:py:meth:`~file.seek` or :py:mod:`~PIL.ImageSequence` to read other images.
:py:meth:`~PIL.Image.Image.seek` or :py:mod:`~PIL.ImageSequence` to read other images.
DDS
@ -942,8 +955,8 @@ MIC
^^^
Pillow identifies and reads Microsoft Image Composer (MIC) files. When opened,
the first sprite in the file is loaded. You can use :py:meth:`~file.seek` and
:py:meth:`~file.tell` to read other sprites from the file.
the first sprite in the file is loaded. You can use :py:meth:`~PIL.Image.Image.seek` and
:py:meth:`~PIL.Image.Image.tell` to read other sprites from the file.
Note that there may be an embedded gamma of 2.2 in MIC files.
@ -951,7 +964,7 @@ MPO
^^^
Pillow identifies and reads Multi Picture Object (MPO) files, loading the primary
image when first opened. The :py:meth:`~file.seek` and :py:meth:`~file.tell`
image when first opened. The :py:meth:`~PIL.Image.Image.seek` and :py:meth:`~PIL.Image.Image.tell`
methods may be used to read other pictures from the file. The pictures are
zero-indexed and random access is supported.

View File

@ -396,10 +396,6 @@ Reading sequences
As seen in this example, youll get an :py:exc:`EOFError` exception when the
sequence ends.
Note that most drivers in the current version of the library only allow you to
seek to the next frame (as in the above example). To rewind the file, you may
have to reopen it.
The following class lets you use the for-statement to loop over the sequence:
Using the ImageSequence Iterator class

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/
:alt: Number of PyPI downloads
.. image:: https://coveralls.io/repos/python-pillow/Pillow/badge.svg?branch=master&service=github
:target: https://coveralls.io/github/python-pillow/Pillow?branch=master
.. image:: https://codecov.io/gh/python-pillow/Pillow/branch/master/graph/badge.svg
:target: https://codecov.io/gh/python-pillow/Pillow
:alt: Code coverage
.. toctree::

View File

@ -151,7 +151,7 @@ Many of Pillow's features require external libraries:
* **littlecms** provides color management
* Pillow version 2.2.1 and below uses liblcms1, Pillow 2.3.0 and
above uses liblcms2. Tested with **1.19** and **2.7**.
above uses liblcms2. Tested with **1.19** and **2.7-2.9**.
* **libwebp** provides the WebP format.
@ -165,7 +165,7 @@ Many of Pillow's features require external libraries:
* Pillow has been tested with openjpeg **2.0.0** and **2.1.0**.
* Pillow does **not** support the earlier **1.5** series which ships
with Ubuntu <= 14.04 and Debian Jessie.
with Debian Jessie.
* **libimagequant** provides improved color quantization
@ -403,10 +403,6 @@ These platforms are built and tested for every change.
| Ubuntu Linux 16.04 LTS | 2.7, 3.5, 3.6, 3.7, |x86-64 |
| | PyPy, PyPy3 | |
+----------------------------------+-------------------------------+-----------------------+
| Ubuntu Linux 14.04 LTS | 2.7, 3.5, 3.6 |x86-64 |
| +-------------------------------+-----------------------+
| | 2.7 |x86 |
+----------------------------------+-------------------------------+-----------------------+
| Windows Server 2012 R2 | 2.7, 3.5, 3.6, 3.7 |x86, x86-64 |
| +-------------------------------+-----------------------+
| | PyPy, 3.7/MinGW |x86 |

View File

@ -132,21 +132,21 @@ can be easily displayed in a chromaticity diagram, for example).
.. py:attribute:: manufacturer
The (english) display string for the device manufacturer (see
The (English) display string for the device manufacturer (see
9.2.22 of ICC.1:2010).
:type: :py:class:`unicode` or ``None``
.. py:attribute:: model
The (english) display string for the device model of the device
The (English) display string for the device model of the device
for which this profile is created (see 9.2.23 of ICC.1:2010).
:type: :py:class:`unicode` or ``None``
.. py:attribute:: profile_description
The (english) display string for the profile description (see
The (English) display string for the profile description (see
9.2.41 of ICC.1:2010).
:type: :py:class:`unicode` or ``None``
@ -269,14 +269,14 @@ can be easily displayed in a chromaticity diagram, for example).
.. py:attribute:: viewing_condition
The (english) display string for the viewing conditions (see
The (English) display string for the viewing conditions (see
9.2.48 of ICC.1:2010).
:type: :py:class:`unicode` or ``None``
.. py:attribute:: screening_description
The (english) display string for the screening conditions.
The (English) display string for the screening conditions.
This tag was available in ICC 3.2, but it is removed from
version 4.

View File

@ -255,7 +255,7 @@ Methods
Draw a shape.
.. py:method:: PIL.ImageDraw.ImageDraw.text(xy, text, fill=None, font=None, anchor=None, spacing=0, align="left", direction=None, features=None)
.. py:method:: PIL.ImageDraw.ImageDraw.text(xy, text, fill=None, font=None, anchor=None, spacing=0, align="left", direction=None, features=None, language=None)
Draws the string at the given position.
@ -287,7 +287,17 @@ Methods
.. versionadded:: 4.2.0
.. py:method:: PIL.ImageDraw.ImageDraw.multiline_text(xy, text, fill=None, font=None, anchor=None, spacing=0, align="left", direction=None, features=None)
:param language: Language of the text. Different languages may use
different glyph shapes or ligatures. This parameter tells
the font which language the text is in, and to apply the
correct substitutions as appropriate, if available.
It should be a `BCP47 language code
<https://www.w3.org/International/articles/language-tags/>`
Requires libraqm.
.. versionadded:: 6.0.0
.. py:method:: PIL.ImageDraw.ImageDraw.multiline_text(xy, text, fill=None, font=None, anchor=None, spacing=0, align="left", direction=None, features=None, language=None)
Draws the string at the given position.
@ -316,7 +326,17 @@ Methods
.. versionadded:: 4.2.0
.. py:method:: PIL.ImageDraw.ImageDraw.textsize(text, font=None, spacing=4, direction=None, features=None)
:param language: Language of the text. Different languages may use
different glyph shapes or ligatures. This parameter tells
the font which language the text is in, and to apply the
correct substitutions as appropriate, if available.
It should be a `BCP47 language code
<https://www.w3.org/International/articles/language-tags/>`
Requires libraqm.
.. versionadded:: 6.0.0
.. py:method:: PIL.ImageDraw.ImageDraw.textsize(text, font=None, spacing=4, direction=None, features=None, language=None)
Return the size of the given string, in pixels.
@ -330,7 +350,6 @@ Methods
Requires libraqm.
.. versionadded:: 4.2.0
:param features: A list of OpenType font features to be used during text
layout. This is usually used to turn on optional
font features that are not enabled by default,
@ -343,8 +362,17 @@ Methods
Requires libraqm.
.. versionadded:: 4.2.0
:param language: Language of the text. Different languages may use
different glyph shapes or ligatures. This parameter tells
the font which language the text is in, and to apply the
correct substitutions as appropriate, if available.
It should be a `BCP47 language code
<https://www.w3.org/International/articles/language-tags/>`
Requires libraqm.
.. py:method:: PIL.ImageDraw.ImageDraw.multiline_textsize(text, font=None, spacing=4, direction=None, features=None)
.. versionadded:: 6.0.0
.. py:method:: PIL.ImageDraw.ImageDraw.multiline_textsize(text, font=None, spacing=4, direction=None, features=None, language=None)
Return the size of the given string, in pixels.
@ -370,6 +398,16 @@ Methods
.. versionadded:: 4.2.0
:param language: Language of the text. Different languages may use
different glyph shapes or ligatures. This parameter tells
the font which language the text is in, and to apply the
correct substitutions as appropriate, if available.
It should be a `BCP47 language code
<https://www.w3.org/International/articles/language-tags/>`
Requires libraqm.
.. versionadded:: 6.0.0
.. py:method:: PIL.ImageDraw.getdraw(im=None, hints=None)
.. warning:: This method is experimental.

View File

@ -47,11 +47,45 @@ Functions
Methods
-------
.. py:method:: PIL.ImageFont.ImageFont.getsize(text)
.. py:method:: PIL.ImageFont.ImageFont.getsize(text, direction=None, features=[], language=None)
Returns width and height (in pixels) of given text if rendered in font with
provided direction, features, and language.
:param text: Text to measure.
:param direction: Direction of the text. It can be 'rtl' (right to
left), 'ltr' (left to right) or 'ttb' (top to bottom).
Requires libraqm.
.. versionadded:: 4.2.0
:param features: A list of OpenType font features to be used during text
layout. This is usually used to turn on optional
font features that are not enabled by default,
for example 'dlig' or 'ss01', but can be also
used to turn off default font features for
example '-liga' to disable ligatures or '-kern'
to disable kerning. To get all supported
features, see
https://docs.microsoft.com/en-us/typography/opentype/spec/featurelist
Requires libraqm.
.. versionadded:: 4.2.0
:param language: Language of the text. Different languages may use
different glyph shapes or ligatures. This parameter tells
the font which language the text is in, and to apply the
correct substitutions as appropriate, if available.
It should be a `BCP47 language code
<https://www.w3.org/International/articles/language-tags/>`
Requires libraqm.
.. versionadded:: 6.0.0
:return: (width, height)
.. py:method:: PIL.ImageFont.ImageFont.getmask(text, mode='', direction=None, features=[])
.. py:method:: PIL.ImageFont.ImageFont.getmask(text, mode='', direction=None, features=[], language=None)
Create a bitmap for the text.
@ -85,5 +119,15 @@ Methods
.. versionadded:: 4.2.0
:param language: Language of the text. Different languages may use
different glyph shapes or ligatures. This parameter tells
the font which language the text is in, and to apply the
correct substitutions as appropriate, if available.
It should be a `BCP47 language code
<https://www.w3.org/International/articles/language-tags/>`
Requires libraqm.
.. versionadded:: 6.0.0
:return: An internal PIL storage memory instance as defined by the
:py:mod:`PIL.Image.core` interface module.

View File

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

View File

@ -99,30 +99,114 @@ version.
Use ``PIL.__version__`` instead.
ImageCms.CmsProfile attributes
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Some attributes in ``ImageCms.CmsProfile`` have been deprecated since Pillow 3.2.0. From
6.0.0, they issue a ``DeprecationWarning``:
======================== ===============================
Deprecated Use instead
======================== ===============================
``color_space`` Padded ``xcolor_space``
``pcs`` Padded ``connection_space``
``product_copyright`` Unicode ``copyright``
``product_desc`` Unicode ``profile_description``
``product_description`` Unicode ``profile_description``
``product_manufacturer`` Unicode ``manufacturer``
``product_model`` Unicode ``model``
======================== ===============================
MIME type improvements
^^^^^^^^^^^^^^^^^^^^^^
Previously, all JPEG2000 images had the MIME type "image/jpx". This has now been
corrected. After the file format drivers have been loaded, ``Image.MIME["JPEG2000"]``
will return "image/jp2". ``ImageFile.get_format_mimetype`` will return "image/jpx" if
a JPX profile is present, or "image/jp2" otherwise.
Previously, all SGI images had the MIME type "image/rgb". This has now been
corrected. After the file format drivers have been loaded, ``Image.MIME["SGI"]``
will return "image/sgi". ``ImageFile.get_format_mimetype`` will return "image/rgb" if
RGB image data is present, or "image/sgi" otherwise.
MIME types have been added to the PPM format. After the file format drivers have been
loaded, ``Image.MIME["PPM"]`` will now return the generic "image/x-portable-anymap".
``ImageFile.get_format_mimetype`` will return a MIME type specific to the color type.
The TGA, PCX and ICO formats also now have MIME types: "image/x-tga", "image/x-pcx" and
"image/x-icon" respectively.
API Additions
=============
DIB File Format
DIB file format
^^^^^^^^^^^^^^^
Pillow now supports reading and writing the DIB "Device Independent Bitmap" file format.
Pillow now supports reading and writing the Device Independent Bitmap file format.
Image.quantize
^^^^^^^^^^^^^^
The `dither` option is now a customisable parameter (was previously hardcoded to `1`). This parameter takes the same values used in `Image.convert`
The ``dither`` option is now a customisable parameter (was previously hardcoded to ``1``).
This parameter takes the same values used in :py:meth:`~PIL.Image.Image.convert`.
PNG EXIF Data
New language parameter
^^^^^^^^^^^^^^^^^^^^^^
These text-rendering functions now accept a ``language`` parameter to request
language-specific glyphs and ligatures from the font:
* ``ImageDraw.ImageDraw.multiline_text()``
* ``ImageDraw.ImageDraw.multiline_textsize()``
* ``ImageDraw.ImageDraw.text()``
* ``ImageDraw.ImageDraw.textsize()``
* ``ImageFont.ImageFont.getmask()``
* ``ImageFont.ImageFont.getsize_multiline()``
* ``ImageFont.ImageFont.getsize()``
Added EXIF class
^^^^^^^^^^^^^^^^
:py:meth:`~PIL.Image.Image.getexif` has been added, which returns an
:py:class:`~PIL.Image.Exif` instance. Values can be retrieved and set like a
dictionary. When saving JPEG, PNG or WEBP, the instance can be passed as an
``exif`` argument to include any changes in the output image.
Added ImageOps.exif_transpose
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
:py:meth:`~PIL.ImageOps.exif_transpose` returns a copy of an image, transposed
according to its EXIF Orientation tag.
PNG EXIF data
^^^^^^^^^^^^^
EXIF data can now be read from and saved to PNG images. However, unlike other image
formats, EXIF data is not guaranteed to have been read until
:py:meth:`~PIL.Image.Image.load` has been called.
formats, EXIF data is not guaranteed to be present in :py:attr:`~PIL.Image.Image.info`
until :py:meth:`~PIL.Image.Image.load` has been called.
Other Changes
=============
Reading new DDS image format
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Pillow can now read uncompressed RGB data from DDS images.
Reading TIFF with old-style JPEG compression
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Added support reading TIFF files with old-style JPEG compression through LibTIFF. All
YCbCr TIFF images are now always read as RGB.
TIFF compression codecs
^^^^^^^^^^^^^^^^^^^^^^^
Support has been added for the LZMA, Zstd and WebP TIFF compression codecs.
Improved support for transposing I;16 images
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
I;16, I;16L and I;16B are now supported image modes for all
:py:meth:`~PIL.Image.Image.transpose` operations.

View File

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

View File

@ -27,7 +27,6 @@
from . import Image, ImageFile, ImagePalette
from ._binary import i8, i16le as i16, i32le as i32, \
o8, o16le as o16, o32le as o32
import math
# __version__ is deprecated and will be removed in a future version. Use
# PIL.__version__ instead.
@ -105,53 +104,51 @@ class BmpImageFile(ImageFile.ImageFile):
# --------------------------------------------- Windows Bitmap v2 to v5
# v3, OS/2 v2, v4, v5
elif file_info['header_size'] in (40, 64, 108, 124):
if file_info['header_size'] >= 40: # v3 and OS/2
file_info['y_flip'] = i8(header_data[7]) == 0xff
file_info['direction'] = 1 if file_info['y_flip'] else -1
file_info['width'] = i32(header_data[0:4])
file_info['height'] = (i32(header_data[4:8])
if not file_info['y_flip']
else 2**32 - i32(header_data[4:8]))
file_info['planes'] = i16(header_data[8:10])
file_info['bits'] = i16(header_data[10:12])
file_info['compression'] = i32(header_data[12:16])
# byte size of pixel data
file_info['data_size'] = i32(header_data[16:20])
file_info['pixels_per_meter'] = (i32(header_data[20:24]),
i32(header_data[24:28]))
file_info['colors'] = i32(header_data[28:32])
file_info['palette_padding'] = 4
self.info["dpi"] = tuple(
map(lambda x: int(math.ceil(x / 39.3701)),
file_info['pixels_per_meter']))
if file_info['compression'] == self.BITFIELDS:
if len(header_data) >= 52:
for idx, mask in enumerate(['r_mask',
'g_mask',
'b_mask',
'a_mask']):
file_info[mask] = i32(
header_data[36 + idx * 4:40 + idx * 4]
)
else:
# 40 byte headers only have the three components in the
# bitfields masks, ref:
# https://msdn.microsoft.com/en-us/library/windows/desktop/dd183376(v=vs.85).aspx
# See also
# https://github.com/python-pillow/Pillow/issues/1293
# There is a 4th component in the RGBQuad, in the alpha
# location, but it is listed as a reserved component,
# and it is not generally an alpha channel
file_info['a_mask'] = 0x0
for mask in ['r_mask', 'g_mask', 'b_mask']:
file_info[mask] = i32(read(4))
file_info['rgb_mask'] = (file_info['r_mask'],
file_info['g_mask'],
file_info['b_mask'])
file_info['rgba_mask'] = (file_info['r_mask'],
file_info['g_mask'],
file_info['b_mask'],
file_info['a_mask'])
file_info['y_flip'] = i8(header_data[7]) == 0xff
file_info['direction'] = 1 if file_info['y_flip'] else -1
file_info['width'] = i32(header_data[0:4])
file_info['height'] = (i32(header_data[4:8])
if not file_info['y_flip']
else 2**32 - i32(header_data[4:8]))
file_info['planes'] = i16(header_data[8:10])
file_info['bits'] = i16(header_data[10:12])
file_info['compression'] = i32(header_data[12:16])
# byte size of pixel data
file_info['data_size'] = i32(header_data[16:20])
file_info['pixels_per_meter'] = (i32(header_data[20:24]),
i32(header_data[24:28]))
file_info['colors'] = i32(header_data[28:32])
file_info['palette_padding'] = 4
self.info["dpi"] = tuple(
int(x / 39.3701 + 0.5) for x in file_info['pixels_per_meter'])
if file_info['compression'] == self.BITFIELDS:
if len(header_data) >= 52:
for idx, mask in enumerate(['r_mask',
'g_mask',
'b_mask',
'a_mask']):
file_info[mask] = i32(
header_data[36 + idx * 4:40 + idx * 4]
)
else:
# 40 byte headers only have the three components in the
# bitfields masks, ref:
# https://msdn.microsoft.com/en-us/library/windows/desktop/dd183376(v=vs.85).aspx
# See also
# https://github.com/python-pillow/Pillow/issues/1293
# There is a 4th component in the RGBQuad, in the alpha
# location, but it is listed as a reserved component,
# and it is not generally an alpha channel
file_info['a_mask'] = 0x0
for mask in ['r_mask', 'g_mask', 'b_mask']:
file_info[mask] = i32(read(4))
file_info['rgb_mask'] = (file_info['r_mask'],
file_info['g_mask'],
file_info['b_mask'])
file_info['rgba_mask'] = (file_info['r_mask'],
file_info['g_mask'],
file_info['b_mask'],
file_info['a_mask'])
else:
raise IOError("Unsupported BMP header type (%d)" %
file_info['header_size'])
@ -180,6 +177,7 @@ class BmpImageFile(ImageFile.ImageFile):
SUPPORTED = {
32: [(0xff0000, 0xff00, 0xff, 0x0),
(0xff0000, 0xff00, 0xff, 0xff000000),
(0xff, 0xff00, 0xff0000, 0xff000000),
(0x0, 0x0, 0x0, 0x0),
(0xff000000, 0xff0000, 0xff00, 0x0)],
24: [(0xff0000, 0xff00, 0xff)],
@ -188,6 +186,7 @@ class BmpImageFile(ImageFile.ImageFile):
MASK_MODES = {
(32, (0xff0000, 0xff00, 0xff, 0x0)): "BGRX",
(32, (0xff000000, 0xff0000, 0xff00, 0x0)): "XBGR",
(32, (0xff, 0xff00, 0xff0000, 0xff000000)): "RGBA",
(32, (0xff0000, 0xff00, 0xff, 0xff000000)): "BGRA",
(32, (0x0, 0x0, 0x0, 0x0)): "BGRA",
(24, (0xff0000, 0xff00, 0xff)): "BGR",
@ -200,7 +199,7 @@ class BmpImageFile(ImageFile.ImageFile):
raw_mode = MASK_MODES[
(file_info["bits"], file_info["rgba_mask"])
]
self.mode = "RGBA" if raw_mode in ("BGRA",) else self.mode
self.mode = "RGBA" if "A" in raw_mode else self.mode
elif (file_info['bits'] in (24, 16) and
file_info['rgb_mask'] in SUPPORTED[file_info['bits']]):
raw_mode = MASK_MODES[
@ -310,7 +309,7 @@ def _save(im, fp, filename, bitmap_header=True):
dpi = info.get("dpi", (96, 96))
# 1 meter == 39.3701 inches
ppm = tuple(map(lambda x: int(x * 39.3701), dpi))
ppm = tuple(map(lambda x: int(x * 39.3701 + 0.5), dpi))
stride = ((im.size[0]*bits+7)//8+3) & (~3)
header = 40 # or 64 for OS/2 version 2

View File

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

View File

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

View File

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

View File

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

View File

@ -40,8 +40,8 @@ except ImportError:
import __builtin__
builtins = __builtin__
from . import ImageMode
from ._binary import i8
from . import ImageMode, TiffTags
from ._binary import i8, i32le
from ._util import isPath, isStringType, deferred_error
import os
@ -54,10 +54,10 @@ import atexit
import numbers
try:
# Python 3
from collections.abc import Callable
from collections.abc import Callable, MutableMapping
except ImportError:
# Python 2.7
from collections import Callable
from collections import Callable, MutableMapping
# Silence warning
@ -247,7 +247,7 @@ _MODEINFO = {
"L": ("L", "L", ("L",)),
"I": ("L", "I", ("I",)),
"F": ("L", "F", ("F",)),
"P": ("RGB", "L", ("P",)),
"P": ("P", "L", ("P",)),
"RGB": ("RGB", "L", ("R", "G", "B")),
"RGBX": ("RGB", "L", ("R", "G", "B", "X")),
"RGBA": ("RGB", "L", ("R", "G", "B", "A")),
@ -578,7 +578,12 @@ class Image(object):
return self
def __exit__(self, *args):
self.close()
if hasattr(self, 'fp') and getattr(self, '_exclusive_fp', False):
if hasattr(self, "_close__fp"):
self._close__fp()
if self.fp:
self.fp.close()
self.fp = None
def close(self):
"""
@ -610,12 +615,7 @@ class Image(object):
if sys.version_info.major >= 3:
def __del__(self):
if hasattr(self, "_close__fp"):
self._close__fp()
if (hasattr(self, 'fp') and hasattr(self, '_exclusive_fp')
and self.fp and self._exclusive_fp):
self.fp.close()
self.fp = None
self.__exit__()
def _copy(self):
self.load()
@ -950,7 +950,7 @@ class Image(object):
delete_trns = False
# transparency handling
if has_transparency:
if self.mode in ('L', 'RGB') and mode == 'RGBA':
if self.mode in ('1', 'L', 'I', 'RGB') and mode == 'RGBA':
# Use transparent conversion to promote from transparent
# color to an alpha channel.
new_im = self._new(self.im.convert_transparent(
@ -1096,7 +1096,13 @@ class Image(object):
im = self.im.convert("P", dither, palette.im)
return self._new(im)
return self._new(self.im.quantize(colors, method, kmeans))
im = self._new(self.im.quantize(colors, method, kmeans))
from . import ImagePalette
mode = im.im.getpalettemode()
im.palette = ImagePalette.ImagePalette(mode, im.im.getpalette(mode, mode))
return im
def copy(self):
"""
@ -1291,6 +1297,12 @@ class Image(object):
return tuple(extrema)
return self.im.getextrema()
def getexif(self):
exif = Exif()
if "exif" in self.info:
exif.load(self.info["exif"])
return exif
def getim(self):
"""
Returns a capsule that points to the internal image memory.
@ -1559,7 +1571,7 @@ class Image(object):
self._ensure_mutable()
if self.mode not in ("LA", "RGBA"):
if self.mode not in ("LA", "PA", "RGBA"):
# attempt to promote self to a matching alpha mode
try:
mode = getmodebase(self.mode) + "A"
@ -1568,7 +1580,7 @@ class Image(object):
except (AttributeError, ValueError):
# do things the hard way
im = self.im.convert(mode)
if im.mode not in ("LA", "RGBA"):
if im.mode not in ("LA", "PA", "RGBA"):
raise ValueError # sanity check
self.im = im
self.pyaccess = None
@ -1576,7 +1588,7 @@ class Image(object):
except (KeyError, ValueError):
raise ValueError("illegal image mode")
if self.mode == "LA":
if self.mode in ("LA", "PA"):
band = 1
else:
band = 3
@ -1619,10 +1631,10 @@ class Image(object):
def putpalette(self, data, rawmode="RGB"):
"""
Attaches a palette to this image. The image must be a "P" or
"L" image, and the palette sequence must contain 768 integer
values, where each group of three values represent the red,
green, and blue values for the corresponding pixel
Attaches a palette to this image. The image must be a "P",
"PA", "L" or "LA" image, and the palette sequence must contain
768 integer values, where each group of three values represent
the red, green, and blue values for the corresponding pixel
index. Instead of an integer sequence, you can use an 8-bit
string.
@ -1631,7 +1643,7 @@ class Image(object):
"""
from . import ImagePalette
if self.mode not in ("L", "P"):
if self.mode not in ("L", "LA", "P", "PA"):
raise ValueError("illegal image mode")
self.load()
if isinstance(data, ImagePalette.ImagePalette):
@ -1643,7 +1655,7 @@ class Image(object):
else:
data = "".join(chr(x) for x in data)
palette = ImagePalette.raw(rawmode, data)
self.mode = "P"
self.mode = "PA" if "A" in self.mode else "P"
self.palette = palette
self.palette.mode = "RGB"
self.load() # install new palette
@ -1688,7 +1700,7 @@ class Image(object):
Rewrites the image to reorder the palette.
:param dest_map: A list of indexes into the original palette.
e.g. [1,0] would swap a two item palette, and list(range(255))
e.g. [1,0] would swap a two item palette, and list(range(256))
is the identity transform.
:param source_palette: Bytes or None.
:returns: An :py:class:`~PIL.Image.Image` object.
@ -1958,7 +1970,7 @@ class Image(object):
filename = fp.name
# may mutate self!
self.load()
self._ensure_mutable()
save_all = params.pop('save_all', False)
self.encoderinfo = params
@ -2005,9 +2017,6 @@ class Image(object):
**EOFError** exception. When a sequence file is opened, the
library automatically seeks to frame 0.
Note that in the current version of the library, most sequence
formats only allow you to seek to the next frame.
See :py:meth:`~PIL.Image.Image.tell`.
:param frame: Frame number, starting at 0.
@ -2374,7 +2383,14 @@ def new(mode, size, color=0):
from . import ImageColor
color = ImageColor.getcolor(color, mode)
return Image()._new(core.fill(mode, size, color))
im = Image()
if mode == "P" and \
isinstance(color, (list, tuple)) and len(color) in [3, 4]:
# RGB or RGBA value for a P image
from . import ImagePalette
im.palette = ImagePalette.ImagePalette()
color = im.palette.getcolor(color)
return im._new(core.fill(mode, size, color))
def frombytes(mode, size, data, decoder_name="raw", *args):
@ -2995,3 +3011,182 @@ def _apply_env_variables(env=None):
_apply_env_variables()
atexit.register(core.clear_cache)
class Exif(MutableMapping):
endian = "<"
def __init__(self):
self._data = {}
self._ifds = {}
def _fixup_dict(self, src_dict):
# Helper function for _getexif()
# returns a dict with any single item tuples/lists as individual values
def _fixup(value):
try:
if len(value) == 1 and not isinstance(value, dict):
return value[0]
except Exception:
pass
return value
return {k: _fixup(v) for k, v in src_dict.items()}
def _get_ifd_dict(self, tag):
try:
# an offset pointer to the location of the nested embedded IFD.
# It should be a long, but may be corrupted.
self.fp.seek(self._data[tag])
except (KeyError, TypeError):
pass
else:
from . import TiffImagePlugin
info = TiffImagePlugin.ImageFileDirectory_v1(self.head)
info.load(self.fp)
return self._fixup_dict(info)
def load(self, data):
# Extract EXIF information. This is highly experimental,
# and is likely to be replaced with something better in a future
# version.
# The EXIF record consists of a TIFF file embedded in a JPEG
# application marker (!).
self.fp = io.BytesIO(data[6:])
self.head = self.fp.read(8)
# process dictionary
from . import TiffImagePlugin
info = TiffImagePlugin.ImageFileDirectory_v1(self.head)
self.endian = info._endian
self.fp.seek(info.next)
info.load(self.fp)
self._data = dict(self._fixup_dict(info))
# get EXIF extension
ifd = self._get_ifd_dict(0x8769)
if ifd:
self._data.update(ifd)
self._ifds[0x8769] = ifd
# get gpsinfo extension
ifd = self._get_ifd_dict(0x8825)
if ifd:
self._data[0x8825] = ifd
self._ifds[0x8825] = ifd
def tobytes(self, offset=0):
from . import TiffImagePlugin
if self.endian == "<":
head = b"II\x2A\x00\x08\x00\x00\x00"
else:
head = b"MM\x00\x2A\x00\x00\x00\x08"
ifd = TiffImagePlugin.ImageFileDirectory_v2(ifh=head)
for tag, value in self._data.items():
ifd[tag] = value
return b"Exif\x00\x00"+head+ifd.tobytes(offset)
def get_ifd(self, tag):
if tag not in self._ifds and tag in self._data:
if tag == 0xa005: # interop
self._ifds[tag] = self._get_ifd_dict(tag)
elif tag == 0x927c: # makernote
from . import TiffImagePlugin
if self._data[0x927c][:8] == b"FUJIFILM":
exif_data = self._data[0x927c]
ifd_offset = i32le(exif_data[8:12])
ifd_data = exif_data[ifd_offset:]
makernote = {}
for i in range(0, struct.unpack("<H", ifd_data[:2])[0]):
ifd_tag, typ, count, data = struct.unpack(
"<HHL4s", ifd_data[i*12 + 2:(i+1)*12 + 2])
try:
unit_size, handler =\
TiffImagePlugin.ImageFileDirectory_v2._load_dispatch[
typ
]
except KeyError:
continue
size = count * unit_size
if size > 4:
offset, = struct.unpack("<L", data)
data = ifd_data[offset-12:offset+size-12]
else:
data = data[:size]
if len(data) != size:
warnings.warn("Possibly corrupt EXIF MakerNote data. "
"Expecting to read %d bytes but only got %d."
" Skipping tag %s"
% (size, len(data), ifd_tag))
continue
if not data:
continue
makernote[ifd_tag] = handler(
TiffImagePlugin.ImageFileDirectory_v2(), data, False)
self._ifds[0x927c] = dict(self._fixup_dict(makernote))
elif self._data.get(0x010f) == "Nintendo":
ifd_data = self._data[0x927c]
makernote = {}
for i in range(0, struct.unpack(">H", ifd_data[:2])[0]):
ifd_tag, typ, count, data = struct.unpack(
">HHL4s", ifd_data[i*12 + 2:(i+1)*12 + 2])
if ifd_tag == 0x1101:
# CameraInfo
offset, = struct.unpack(">L", data)
self.fp.seek(offset)
camerainfo = {'ModelID': self.fp.read(4)}
self.fp.read(4)
# Seconds since 2000
camerainfo['TimeStamp'] = i32le(self.fp.read(12))
self.fp.read(4)
camerainfo['InternalSerialNumber'] = self.fp.read(4)
self.fp.read(12)
parallax = self.fp.read(4)
handler =\
TiffImagePlugin.ImageFileDirectory_v2._load_dispatch[
TiffTags.FLOAT
][1]
camerainfo['Parallax'] = handler(
TiffImagePlugin.ImageFileDirectory_v2(),
parallax, False)
self.fp.read(4)
camerainfo['Category'] = self.fp.read(2)
makernote = {0x1101: dict(self._fixup_dict(camerainfo))}
self._ifds[0x927c] = makernote
return self._ifds.get(tag, {})
def __str__(self):
return str(self._data)
def __len__(self):
return len(self._data)
def __getitem__(self, tag):
return self._data[tag]
def __contains__(self, tag):
return tag in self._data
if not py3:
def has_key(self, tag):
return tag in self
def __setitem__(self, tag, value):
self._data[tag] = value
def __delitem__(self, tag):
del self._data[tag]
def __iter__(self):
return iter(set(self._data))

View File

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

View File

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

View File

@ -596,8 +596,6 @@ class PyDecoder(object):
Override to perform the decoding process.
:param buffer: A bytes object with the data to be decoded.
If `handles_eof` is set, then `buffer` will be empty and `self.fd`
will be set.
:returns: A tuple of (bytes consumed, errcode).
If finished with decoding return <0 for the bytes consumed.
Err codes are from `ERRORS`

View File

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

View File

@ -522,3 +522,30 @@ def solarize(image, threshold=128):
else:
lut.append(255-i)
return _lut(image, lut)
def exif_transpose(image):
"""
If an image has an EXIF Orientation tag, return a new image that is
transposed accordingly. Otherwise, return a copy of the image.
:param image: The image to transpose.
:return: An image.
"""
exif = image.getexif()
orientation = exif.get(0x0112)
method = {
2: Image.FLIP_LEFT_RIGHT,
3: Image.ROTATE_180,
4: Image.FLIP_TOP_BOTTOM,
5: Image.TRANSPOSE,
6: Image.ROTATE_270,
7: Image.TRANSVERSE,
8: Image.ROTATE_90
}.get(orientation)
if method is not None:
transposed_image = image.transpose(method)
del exif[0x0112]
transposed_image.info["exif"] = exif.tobytes()
return transposed_image
return image.copy()

View File

@ -198,35 +198,9 @@ def getiptcinfo(im):
elif isinstance(im, JpegImagePlugin.JpegImageFile):
# extract the IPTC/NAA resource
try:
app = im.app["APP13"]
if app[:14] == b"Photoshop 3.0\x00":
app = app[14:]
# parse the image resource block
offset = 0
while app[offset:offset+4] == b"8BIM":
offset += 4
# resource code
code = i16(app, offset)
offset += 2
# resource name (usually empty)
name_len = i8(app[offset])
# name = app[offset+1:offset+1+name_len]
offset = 1 + offset + name_len
if offset & 1:
offset += 1
# resource data block
size = i32(app, offset)
offset += 4
if code == 0x0404:
# 0x0404 contains IPTC/NAA data
data = app[offset:offset+size]
break
offset = offset + size
if offset & 1:
offset += 1
except (AttributeError, KeyError):
pass
photoshop = im.info.get("photoshop")
if photoshop:
data = photoshop.get(0x0404)
elif isinstance(im, TiffImagePlugin.TiffImageFile):
# get raw data from the IPTC/NAA tag (PhotoShop tags the data

View File

@ -39,7 +39,7 @@ import struct
import io
import warnings
from . import Image, ImageFile, TiffImagePlugin
from ._binary import i8, o8, i16be as i16
from ._binary import i8, o8, i16be as i16, i32be as i32
from .JpegPresets import presets
from ._util import isStringType
@ -86,7 +86,7 @@ def APP(self, marker):
self.info["jfif_density"] = jfif_density
elif marker == 0xFFE1 and s[:5] == b"Exif\0":
if "exif" not in self.info:
# extract Exif information (incomplete)
# extract EXIF information (incomplete)
self.info["exif"] = s # FIXME: value will change
elif marker == 0xFFE2 and s[:5] == b"FPXR\0":
# extract FlashPix information (incomplete)
@ -104,6 +104,39 @@ def APP(self, marker):
# reassemble the profile, rather than assuming that the APP2
# markers appear in the correct sequence.
self.icclist.append(s)
elif marker == 0xFFED:
if s[:14] == b"Photoshop 3.0\x00":
blocks = s[14:]
# parse the image resource block
offset = 0
photoshop = {}
while blocks[offset:offset+4] == b"8BIM":
offset += 4
# resource code
code = i16(blocks, offset)
offset += 2
# resource name (usually empty)
name_len = i8(blocks[offset])
# name = blocks[offset+1:offset+1+name_len]
offset = 1 + offset + name_len
if offset & 1:
offset += 1
# resource data block
size = i32(blocks, offset)
offset += 4
data = blocks[offset:offset+size]
if code == 0x03ED: # ResolutionInfo
data = {
'XResolution': i32(data[:4]) / 65536,
'DisplayedUnitsX': i16(data[4:8]),
'YResolution': i32(data[8:12]) / 65536,
'DisplayedUnitsY': i16(data[12:]),
}
photoshop[code] = data
offset = offset + size
if offset & 1:
offset += 1
self.info["photoshop"] = photoshop
elif marker == 0xFFEE and s[:5] == b"Adobe":
self.info["adobe"] = i16(s, 5)
# extract Adobe custom properties
@ -127,15 +160,15 @@ def APP(self, marker):
resolution_unit = exif[0x0128]
x_resolution = exif[0x011A]
try:
dpi = x_resolution[0] / x_resolution[1]
dpi = float(x_resolution[0]) / x_resolution[1]
except TypeError:
dpi = x_resolution
if resolution_unit == 3: # cm
# 1 dpcm = 2.54 dpi
dpi *= 2.54
self.info["dpi"] = dpi, dpi
self.info["dpi"] = int(dpi + 0.5), int(dpi + 0.5)
except (KeyError, SyntaxError, ZeroDivisionError):
# SyntaxError for invalid/unreadable exif
# SyntaxError for invalid/unreadable EXIF
# KeyError for dpi not included
# ZeroDivisionError for invalid dpi rational value
self.info["dpi"] = 72, 72
@ -439,60 +472,23 @@ class JpegImageFile(ImageFile.ImageFile):
def _fixup_dict(src_dict):
# Helper function for _getexif()
# returns a dict with any single item tuples/lists as individual values
def _fixup(value):
try:
if len(value) == 1 and not isinstance(value, dict):
return value[0]
except Exception:
pass
return value
return {k: _fixup(v) for k, v in src_dict.items()}
exif = Image.Exif()
return exif._fixup_dict(src_dict)
def _getexif(self):
# Extract EXIF information. This method is highly experimental,
# and is likely to be replaced with something better in a future
# version.
# The EXIF record consists of a TIFF file embedded in a JPEG
# application marker (!).
# Use the cached version if possible
try:
data = self.info["exif"]
return self.info["parsed_exif"]
except KeyError:
return None
fp = io.BytesIO(data[6:])
head = fp.read(8)
# process dictionary
info = TiffImagePlugin.ImageFileDirectory_v1(head)
fp.seek(info.next)
info.load(fp)
exif = dict(_fixup_dict(info))
# get exif extension
try:
# exif field 0x8769 is an offset pointer to the location
# of the nested embedded exif ifd.
# It should be a long, but may be corrupted.
fp.seek(exif[0x8769])
except (KeyError, TypeError):
pass
else:
info = TiffImagePlugin.ImageFileDirectory_v1(head)
info.load(fp)
exif.update(_fixup_dict(info))
# get gpsinfo extension
try:
# exif field 0x8825 is an offset pointer to the location
# of the nested embedded gps exif ifd.
# It should be a long, but may be corrupted.
fp.seek(exif[0x8825])
except (KeyError, TypeError):
pass
else:
info = TiffImagePlugin.ImageFileDirectory_v1(head)
info.load(fp)
exif[0x8825] = _fixup_dict(info)
if "exif" not in self.info:
return None
exif = dict(self.getexif())
# Cache the result for future use
self.info["parsed_exif"] = exif
return exif
@ -728,6 +724,10 @@ def _save(im, fp, filename):
optimize = info.get("optimize", False)
exif = info.get("exif", b"")
if isinstance(exif, Image.Exif):
exif = exif.tobytes()
# get keyword arguments
im.encoderconfig = (
quality,
@ -739,7 +739,7 @@ def _save(im, fp, filename):
subsampling,
qtables,
extra,
info.get("exif", b"")
exif
)
# if we optimize, libjpeg needs a buffer big enough to hold the whole image
@ -757,9 +757,9 @@ def _save(im, fp, filename):
else:
bufsize = im.size[0] * im.size[1]
# The exif info needs to be written as one block, + APP1, + one spare byte.
# The EXIF info needs to be written as one block, + APP1, + one spare byte.
# Ensure that our buffer is big enough. Same with the icc_profile block.
bufsize = max(ImageFile.MAXBLOCK, bufsize, len(info.get("exif", b"")) + 5,
bufsize = max(ImageFile.MAXBLOCK, bufsize, len(exif) + 5,
len(extra) + 1)
ImageFile._save(im, fp, [("jpeg", (0, 0)+im.size, 0, rawmode)], bufsize)
@ -786,7 +786,8 @@ def jpeg_factory(fp=None, filename=None):
if mpheader[45057] > 1:
# It's actually an MPO
from .MpoImagePlugin import MpoImageFile
im = MpoImageFile(fp, filename)
# Don't reload everything, just convert it.
im = MpoImageFile.adopt(im, mpheader)
except (TypeError, IndexError):
# It is really a JPEG
pass

View File

@ -18,7 +18,8 @@
# See the README file for information on usage and redistribution.
#
from . import Image, JpegImagePlugin
from . import Image, ImageFile, JpegImagePlugin
from ._binary import i16be as i16
# __version__ is deprecated and will be removed in a future version. Use
# PIL.__version__ instead.
@ -46,7 +47,10 @@ class MpoImageFile(JpegImagePlugin.JpegImageFile):
def _open(self):
self.fp.seek(0) # prep the fp in order to pass the JPEG test
JpegImagePlugin.JpegImageFile._open(self)
self.mpinfo = self._getmp()
self._after_jpeg_open()
def _after_jpeg_open(self, mpheader=None):
self.mpinfo = mpheader if mpheader is not None else self._getmp()
self.__framecount = self.mpinfo[0xB001]
self.__mpoffsets = [mpent['DataOffset'] + self.info['mpoffset']
for mpent in self.mpinfo[0xB002]]
@ -78,6 +82,20 @@ class MpoImageFile(JpegImagePlugin.JpegImageFile):
return
self.fp = self.__fp
self.offset = self.__mpoffsets[frame]
self.fp.seek(self.offset + 2) # skip SOI marker
if "parsed_exif" in self.info:
del self.info["parsed_exif"]
if i16(self.fp.read(2)) == 0xFFE1: # APP1
n = i16(self.fp.read(2))-2
self.info["exif"] = ImageFile._safe_read(self.fp, n)
exif = self._getexif()
if 40962 in exif and 40963 in exif:
self._size = (exif[40962], exif[40963])
elif "exif" in self.info:
del self.info["exif"]
self.tile = [
("jpeg", (0, 0) + self.size, self.offset, (self.mode, ""))
]
@ -95,6 +113,22 @@ class MpoImageFile(JpegImagePlugin.JpegImageFile):
finally:
self.__fp = None
@staticmethod
def adopt(jpeg_instance, mpheader=None):
"""
Transform the instance of JpegImageFile into
an instance of MpoImageFile.
After the call, the JpegImageFile is extended
to be an MpoImageFile.
This is essentially useful when opening a JPEG
file that reveals itself as an MPO, to avoid
double call to _open.
"""
jpeg_instance.__class__ = MpoImageFile
jpeg_instance._after_jpeg_open(mpheader)
return jpeg_instance
# ---------------------------------------------------------------------
# Registry stuff

View File

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

View File

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

View File

@ -164,6 +164,6 @@ def _save(im, fp, filename):
Image.register_open(PpmImageFile.format, PpmImageFile, _accept)
Image.register_save(PpmImageFile.format, _save)
Image.register_extensions(PpmImageFile.format, [".pbm", ".pgm", ".ppm"])
Image.register_extensions(PpmImageFile.format, [".pbm", ".pgm", ".ppm", ".pnm"])
Image.register_mime(PpmImageFile.format, "image/x-portable-anymap")

View File

@ -208,7 +208,7 @@ class SpiderImageFile(ImageFile.ImageFile):
# given a list of filenames, return a list of images
def loadImageSeries(filelist=None):
"""create a list of Image.images for use in montage"""
"""create a list of :py:class:`~PIL.Image.Image` objects for use in a montage"""
if filelist is None or len(filelist) < 1:
return

View File

@ -226,4 +226,6 @@ def _save(im, fp, filename):
Image.register_open(TgaImageFile.format, TgaImageFile)
Image.register_save(TgaImageFile.format, _save)
Image.register_extension(TgaImageFile.format, ".tga")
Image.register_extensions(TgaImageFile.format, [".tga", ".icb", ".vda", ".vst"])
Image.register_mime(TgaImageFile.format, "image/x-tga")

View File

@ -263,10 +263,10 @@ OPEN_INFO = {
(II, 5, (1,), 1, (8, 8, 8, 8, 8, 8), (0, 0)): ("CMYK", "CMYKXX"),
(MM, 5, (1,), 1, (8, 8, 8, 8, 8, 8), (0, 0)): ("CMYK", "CMYKXX"),
# JPEG compressed images handled by LibTiff and auto-converted to RGB
# JPEG compressed images handled by LibTiff and auto-converted to RGBX
# Minimal Baseline TIFF requires YCbCr images to have 3 SamplesPerPixel
(II, 6, (1,), 1, (8, 8, 8), ()): ("RGB", "RGB"),
(MM, 6, (1,), 1, (8, 8, 8), ()): ("RGB", "RGB"),
(II, 6, (1,), 1, (8, 8, 8), ()): ("RGB", "RGBX"),
(MM, 6, (1,), 1, (8, 8, 8), ()): ("RGB", "RGBX"),
(II, 8, (1,), 1, (8, 8, 8), ()): ("LAB", "LAB"),
(MM, 8, (1,), 1, (8, 8, 8), ()): ("LAB", "LAB"),
@ -785,17 +785,12 @@ class ImageFileDirectory_v2(MutableMapping):
warnings.warn(str(msg))
return
def save(self, fp):
if fp.tell() == 0: # skip TIFF header on subsequent pages
# tiff header -- PIL always starts the first IFD at offset 8
fp.write(self._prefix + self._pack("HL", 42, 8))
def tobytes(self, offset=0):
# FIXME What about tagdata?
fp.write(self._pack("H", len(self._tags_v2)))
result = self._pack("H", len(self._tags_v2))
entries = []
offset = fp.tell() + len(self._tags_v2) * 12 + 4
offset = offset + len(result) + len(self._tags_v2) * 12 + 4
stripoffsets = None
# pass 1: convert tags to binary format
@ -844,18 +839,29 @@ class ImageFileDirectory_v2(MutableMapping):
for tag, typ, count, value, data in entries:
if DEBUG > 1:
print(tag, typ, count, repr(value), repr(data))
fp.write(self._pack("HHL4s", tag, typ, count, value))
result += self._pack("HHL4s", tag, typ, count, value)
# -- overwrite here for multi-page --
fp.write(b"\0\0\0\0") # end of entries
result += b"\0\0\0\0" # end of entries
# pass 3: write auxiliary data to file
for tag, typ, count, value, data in entries:
fp.write(data)
result += data
if len(data) & 1:
fp.write(b"\0")
result += b"\0"
return offset
return result
def save(self, fp):
if fp.tell() == 0: # skip TIFF header on subsequent pages
# tiff header -- PIL always starts the first IFD at offset 8
fp.write(self._prefix + self._pack("HL", 42, 8))
offset = fp.tell()
result = self.tobytes(offset)
fp.write(result)
return offset + len(result)
ImageFileDirectory_v2._load_dispatch = _load_dispatch
@ -985,7 +991,6 @@ class TiffImageFile(ImageFile.ImageFile):
self.__fp = self.fp
self._frame_pos = []
self._n_frames = None
self._is_animated = None
if DEBUG:
print("*** TiffImageFile._open ***")
@ -999,29 +1004,14 @@ class TiffImageFile(ImageFile.ImageFile):
def n_frames(self):
if self._n_frames is None:
current = self.tell()
try:
while True:
self._seek(self.tell() + 1)
except EOFError:
self._n_frames = self.tell() + 1
self._seek(len(self._frame_pos))
while self._n_frames is None:
self._seek(self.tell() + 1)
self.seek(current)
return self._n_frames
@property
def is_animated(self):
if self._is_animated is None:
if self._n_frames is not None:
self._is_animated = self._n_frames != 1
else:
current = self.tell()
try:
self.seek(1)
self._is_animated = True
except EOFError:
self._is_animated = False
self.seek(current)
return self._is_animated
def seek(self, frame):
@ -1053,10 +1043,13 @@ class TiffImageFile(ImageFile.ImageFile):
print("Loading tags, location: %s" % self.fp.tell())
self.tag_v2.load(self.fp)
self.__next = self.tag_v2.next
if self.__next == 0:
self._n_frames = frame + 1
if len(self._frame_pos) == 1:
self._is_animated = self.__next != 0
self.__frame += 1
self.fp.seek(self._frame_pos[frame])
self.tag_v2.load(self.fp)
self.__next = self.tag_v2.next
# fill the legacy tag/ifd entries
self.tag = self.ifd = ImageFileDirectory_v1.from_v2(self.tag_v2)
self.__frame = frame
@ -1087,7 +1080,7 @@ class TiffImageFile(ImageFile.ImageFile):
def load_end(self):
# allow closing if we're on the first frame, there's no next
# This is the ImageFile.load path only, libtiff specific below.
if self.__frame == 0 and not self.__next:
if not self._is_animated:
self._close_exclusive_fp_after_loading = True
def _load_libtiff(self):
@ -1167,10 +1160,9 @@ class TiffImageFile(ImageFile.ImageFile):
self.tile = []
self.readonly = 0
# libtiff closed the fp in a, we need to close self.fp, if possible
if self._exclusive_fp:
if self.__frame == 0 and not self.__next:
self.fp.close()
self.fp = None # might be shared
if self._exclusive_fp and not self._is_animated:
self.fp.close()
self.fp = None # might be shared
if err < 0:
raise IOError(err)
@ -1191,6 +1183,10 @@ class TiffImageFile(ImageFile.ImageFile):
# the specification
photo = self.tag_v2.get(PHOTOMETRIC_INTERPRETATION, 0)
# old style jpeg compression images most certainly are YCbCr
if self._compression == "tiff_jpeg":
photo = 6
fillorder = self.tag_v2.get(FILLORDER, 1)
if DEBUG:
@ -1257,11 +1253,11 @@ class TiffImageFile(ImageFile.ImageFile):
if xres and yres:
resunit = self.tag_v2.get(RESOLUTION_UNIT)
if resunit == 2: # dots per inch
self.info["dpi"] = xres, yres
self.info["dpi"] = int(xres + 0.5), int(yres + 0.5)
elif resunit == 3: # dots per centimeter. convert to dpi
self.info["dpi"] = xres * 2.54, yres * 2.54
self.info["dpi"] = int(xres * 2.54 + 0.5), int(yres * 2.54 + 0.5)
elif resunit is None: # used to default to 1, but now 2)
self.info["dpi"] = xres, yres
self.info["dpi"] = int(xres + 0.5), int(yres + 0.5)
# For backward compatibility,
# we also preserve the old behavior
self.info["resolution"] = xres, yres
@ -1472,8 +1468,8 @@ def _save(im, fp, filename):
dpi = im.encoderinfo.get("dpi")
if dpi:
ifd[RESOLUTION_UNIT] = 2
ifd[X_RESOLUTION] = dpi[0]
ifd[Y_RESOLUTION] = dpi[1]
ifd[X_RESOLUTION] = int(dpi[0] + 0.5)
ifd[Y_RESOLUTION] = int(dpi[1] + 0.5)
if bits != (1,):
ifd[BITSPERSAMPLE] = bits

View File

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

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