Merge branch 'master' of https://github.com/python-pillow/Pillow
|
@ -52,7 +52,7 @@ install:
|
|||
}
|
||||
else
|
||||
{
|
||||
c:\python34\python.exe c:\pillow\winbuild\build_dep.py
|
||||
c:\python37\python.exe c:\pillow\winbuild\build_dep.py
|
||||
c:\pillow\winbuild\build_deps.cmd
|
||||
$host.SetShouldExit(0)
|
||||
}
|
||||
|
|
2
.github/CONTRIBUTING.md
vendored
|
@ -9,7 +9,7 @@ Please send a pull request to the master branch. Please include [documentation](
|
|||
- Fork the Pillow repository.
|
||||
- Create a branch from master.
|
||||
- Develop bug fixes, features, tests, etc.
|
||||
- Run the test suite on Python 2.7 and 3.x. You can enable [Travis CI](https://travis-ci.org/profile/) and [AppVeyor](https://ci.appveyor.com/projects/new) on your repo to catch test failures prior to the pull request, and [Coveralls](https://coveralls.io/repos/new) to see if the changed code is covered by tests.
|
||||
- Run the test suite on Python 2.7 and 3.x. You can enable [Travis CI](https://travis-ci.org/profile/) and [AppVeyor](https://ci.appveyor.com/projects/new) on your repo to catch test failures prior to the pull request, and [Codecov](https://codecov.io/gh) to see if the changed code is covered by tests.
|
||||
- Create a pull request to pull the changes from your branch to the Pillow master.
|
||||
|
||||
### Guidelines
|
||||
|
|
30
.travis.yml
|
@ -18,41 +18,27 @@ matrix:
|
|||
env: LINT="true"
|
||||
- python: "pypy2.7-6.0"
|
||||
name: "PyPy2 Xenial"
|
||||
dist: xenial
|
||||
- python: "pypy3.5-6.0"
|
||||
name: "PyPy3 Xenial"
|
||||
dist: xenial
|
||||
- python: '3.7'
|
||||
name: "3.7 Xenial"
|
||||
- python: '2.7'
|
||||
name: "2.7 Xenial"
|
||||
- python: '2.7'
|
||||
name: "2.7 Trusty"
|
||||
dist: trusty
|
||||
- python: "2.7_with_system_site_packages" # For PyQt4
|
||||
name: "2.7_with_system_site_packages Xenial"
|
||||
services: xvfb
|
||||
- python: "2.7_with_system_site_packages" # For PyQt4
|
||||
name: "2.7_with_system_site_packages Trusty"
|
||||
dist: trusty
|
||||
- python: '3.6'
|
||||
name: "3.6 Xenial"
|
||||
- python: '3.6'
|
||||
name: "3.6 Trusty PYTHONOPTIMIZE=1"
|
||||
dist: trusty
|
||||
name: "3.6 Xenial PYTHONOPTIMIZE=1"
|
||||
env: PYTHONOPTIMIZE=1
|
||||
- python: '3.5'
|
||||
name: "3.5 Xenial"
|
||||
- python: '3.5'
|
||||
name: "3.5 Trusty PYTHONOPTIMIZE=2"
|
||||
dist: trusty
|
||||
name: "3.5 Xenial PYTHONOPTIMIZE=2"
|
||||
env: PYTHONOPTIMIZE=2
|
||||
- python: "3.8-dev"
|
||||
name: "3.8-dev Xenial"
|
||||
- env: DOCKER="alpine" DOCKER_TAG="master"
|
||||
- env: DOCKER="arch" DOCKER_TAG="master" # contains PyQt5
|
||||
- env: DOCKER="ubuntu-trusty-x86" DOCKER_TAG="master"
|
||||
- env: DOCKER="ubuntu-xenial-amd64" DOCKER_TAG="master"
|
||||
- env: DOCKER="ubuntu-16.04-xenial-amd64" DOCKER_TAG="master"
|
||||
- env: DOCKER="ubuntu-18.04-bionic-amd64" DOCKER_TAG="master"
|
||||
- env: DOCKER="debian-stretch-x86" DOCKER_TAG="master"
|
||||
- env: DOCKER="centos-6-amd64" DOCKER_TAG="master"
|
||||
- env: DOCKER="centos-7-amd64" DOCKER_TAG="master"
|
||||
|
@ -75,14 +61,6 @@ install:
|
|||
.travis/install.sh;
|
||||
fi
|
||||
|
||||
before_script:
|
||||
# Qt needs a display for some of the tests, and it's only run on the system site packages install
|
||||
- |
|
||||
if [ "$TRAVIS_JOB_NAME" == "2.7_with_system_site_packages Trusty" ]; then
|
||||
export DISPLAY=:99.0
|
||||
sh -e /etc/init.d/xvfb start
|
||||
fi
|
||||
|
||||
script:
|
||||
- |
|
||||
if [ "$LINT" == "true" ]; then
|
||||
|
|
94
CHANGES.rst
|
@ -2,12 +2,102 @@
|
|||
Changelog (Pillow)
|
||||
==================
|
||||
|
||||
6.0.0 (unreleased)
|
||||
6.0.0 (2019-04-01)
|
||||
------------------
|
||||
|
||||
- Python 2.7 support will be removed in Pillow 7.0.0 #3682
|
||||
[hugovk]
|
||||
|
||||
- Add EXIF class #3625
|
||||
[radarhere]
|
||||
|
||||
- Add ImageOps exif_transpose method #3687
|
||||
[radarhere]
|
||||
|
||||
- Added warnings to deprecated CMSProfile attributes #3615
|
||||
[hugovk]
|
||||
|
||||
- Documented reading TIFF multiframe images #3720
|
||||
[akuchling]
|
||||
|
||||
- Improved speed of opening an MPO file #3658
|
||||
[Glandos]
|
||||
|
||||
- Update palette in quantize #3721
|
||||
[radarhere]
|
||||
|
||||
- Improvements to TIFF is_animated and n_frames #3714
|
||||
[radarhere]
|
||||
|
||||
- Fixed incompatible pointer type warnings #3754
|
||||
[radarhere]
|
||||
|
||||
- Improvements to PA and LA conversion and palette operations #3728
|
||||
[radarhere]
|
||||
|
||||
- Consistent DPI rounding #3709
|
||||
[radarhere]
|
||||
|
||||
- Change size of MPO image to match frame #3588
|
||||
[radarhere]
|
||||
|
||||
- Read Photoshop resolution data #3701
|
||||
[radarhere]
|
||||
|
||||
- Ensure image is mutable before saving #3724
|
||||
[radarhere]
|
||||
|
||||
- Correct remap_palette documentation #3740
|
||||
[radarhere]
|
||||
|
||||
- Promote P images to PA in putalpha #3726
|
||||
[radarhere]
|
||||
|
||||
- Allow RGB and RGBA values for new P images #3719
|
||||
[radarhere]
|
||||
|
||||
- Fixed TIFF bug when seeking backwards and then forwards #3713
|
||||
[radarhere]
|
||||
|
||||
- Cache EXIF information #3498
|
||||
[Glandos]
|
||||
|
||||
- Added transparency for all PNG greyscale modes #3744
|
||||
[radarhere]
|
||||
|
||||
- Fix deprecation warnings in Python 3.8 #3749
|
||||
[radarhere]
|
||||
|
||||
- Fixed GIF bug when rewinding to a non-zero frame #3716
|
||||
[radarhere]
|
||||
|
||||
- Only close original fp in __del__ and __exit__ if original fp is exclusive #3683
|
||||
[radarhere]
|
||||
|
||||
- Fix BytesWarning in Tests/test_numpy.py #3725
|
||||
[jdufresne]
|
||||
|
||||
- Add missing MIME types and extensions #3520
|
||||
[pirate486743186]
|
||||
|
||||
- Add I;16 PNG save #3566
|
||||
[radarhere]
|
||||
|
||||
- Add support for BMP RGBA bitfield compression #3705
|
||||
[radarhere]
|
||||
|
||||
- Added ability to set language for text rendering #3693
|
||||
[iwsfutcmd]
|
||||
|
||||
- Only close exclusive fp on Image __exit__ #3698
|
||||
[radarhere]
|
||||
|
||||
- Changed EPS subprocess stdout from devnull to None #3635
|
||||
[radarhere]
|
||||
|
||||
- Add reading old-JPEG compressed TIFFs #3489
|
||||
[kkopachev]
|
||||
|
||||
- Add EXIF support for PNG #3674
|
||||
[radarhere]
|
||||
|
||||
|
@ -38,7 +128,7 @@ Changelog (Pillow)
|
|||
- Make ContainerIO.isatty() return a bool, not int #3568
|
||||
[jdufresne]
|
||||
|
||||
- Add support for I;16 modes for more transpose operations #3563
|
||||
- Add support to all transpose operations for I;16 modes #3563, #3741
|
||||
[radarhere]
|
||||
|
||||
- Deprecate support for PyQt4 and PySide #3655
|
||||
|
|
49
README.rst
|
@ -20,6 +20,30 @@ Pillow is the friendly PIL fork by `Alex Clark and Contributors <https://github.
|
|||
* - social
|
||||
- |gitter| |twitter|
|
||||
|
||||
.. end-badges
|
||||
|
||||
More Information
|
||||
----------------
|
||||
|
||||
- `Documentation <https://pillow.readthedocs.io/>`_
|
||||
|
||||
- `Installation <https://pillow.readthedocs.io/en/latest/installation.html>`_
|
||||
- `Handbook <https://pillow.readthedocs.io/en/latest/handbook/index.html>`_
|
||||
|
||||
- `Contribute <https://github.com/python-pillow/Pillow/blob/master/.github/CONTRIBUTING.md>`_
|
||||
|
||||
- `Issues <https://github.com/python-pillow/Pillow/issues>`_
|
||||
- `Pull requests <https://github.com/python-pillow/Pillow/pulls>`_
|
||||
|
||||
- `Changelog <https://github.com/python-pillow/Pillow/blob/master/CHANGES.rst>`_
|
||||
|
||||
- `Pre-fork <https://github.com/python-pillow/Pillow/blob/master/CHANGES.rst#pre-fork>`_
|
||||
|
||||
Report a Vulnerability
|
||||
----------------------
|
||||
|
||||
To report a security vulnerability, please follow the procedure described in the `Tidelift security policy <https://tidelift.com/docs/security>`_.
|
||||
|
||||
.. |docs| image:: https://readthedocs.org/projects/pillow/badge/?version=latest
|
||||
:target: https://pillow.readthedocs.io/?badge=latest
|
||||
:alt: Documentation Status
|
||||
|
@ -36,8 +60,8 @@ Pillow is the friendly PIL fork by `Alex Clark and Contributors <https://github.
|
|||
:target: https://ci.appveyor.com/project/python-pillow/Pillow
|
||||
:alt: AppVeyor CI build status (Windows)
|
||||
|
||||
.. |coverage| image:: https://coveralls.io/repos/python-pillow/Pillow/badge.svg?branch=master&service=github
|
||||
:target: https://coveralls.io/github/python-pillow/Pillow?branch=master
|
||||
.. |coverage| image:: https://codecov.io/gh/python-pillow/Pillow/branch/master/graph/badge.svg
|
||||
:target: https://codecov.io/gh/python-pillow/Pillow
|
||||
:alt: Code coverage
|
||||
|
||||
.. |zenodo| image:: https://zenodo.org/badge/17549/python-pillow/Pillow.svg
|
||||
|
@ -61,24 +85,3 @@ Pillow is the friendly PIL fork by `Alex Clark and Contributors <https://github.
|
|||
.. |twitter| image:: https://img.shields.io/badge/tweet-on%20Twitter-00aced.svg
|
||||
:target: https://twitter.com/PythonPillow
|
||||
:alt: Follow on https://twitter.com/PythonPillow
|
||||
|
||||
.. end-badges
|
||||
|
||||
|
||||
|
||||
More Information
|
||||
----------------
|
||||
|
||||
- `Documentation <https://pillow.readthedocs.io/>`_
|
||||
|
||||
- `Installation <https://pillow.readthedocs.io/en/latest/installation.html>`_
|
||||
- `Handbook <https://pillow.readthedocs.io/en/latest/handbook/index.html>`_
|
||||
|
||||
- `Contribute <https://github.com/python-pillow/Pillow/blob/master/.github/CONTRIBUTING.md>`_
|
||||
|
||||
- `Issues <https://github.com/python-pillow/Pillow/issues>`_
|
||||
- `Pull requests <https://github.com/python-pillow/Pillow/pulls>`_
|
||||
|
||||
- `Changelog <https://github.com/python-pillow/Pillow/blob/master/CHANGES.rst>`_
|
||||
|
||||
- `Pre-fork <https://github.com/python-pillow/Pillow/blob/master/CHANGES.rst#pre-fork>`_
|
||||
|
|
|
@ -102,4 +102,4 @@ Released as needed privately to individual vendors for critical security-related
|
|||
|
||||
## Documentation
|
||||
|
||||
* [ ] Make sure the default version for Read the Docs is the latest tagged release e.g. `d2d43879` (5.4.0)
|
||||
* [ ] Make sure the [default version for Read the Docs](https://pillow.readthedocs.io/en/stable/) is up-to-date with the release changes
|
||||
|
|
BIN
Tests/images/1_trns.png
Normal file
After Width: | Height: | Size: 612 B |
BIN
Tests/images/drawing_roundDown.emf
Normal file
BIN
Tests/images/fujifilm.mpo
Normal file
After Width: | Height: | Size: 9.5 MiB |
BIN
Tests/images/hopper.pnm
Normal file
BIN
Tests/images/hopper_orientation_2.jpg
Normal file
After Width: | Height: | Size: 4.6 KiB |
BIN
Tests/images/hopper_orientation_2.webp
Normal file
After Width: | Height: | Size: 3.6 KiB |
BIN
Tests/images/hopper_orientation_3.jpg
Normal file
After Width: | Height: | Size: 4.6 KiB |
BIN
Tests/images/hopper_orientation_3.webp
Normal file
After Width: | Height: | Size: 3.7 KiB |
BIN
Tests/images/hopper_orientation_4.jpg
Normal file
After Width: | Height: | Size: 4.6 KiB |
BIN
Tests/images/hopper_orientation_4.webp
Normal file
After Width: | Height: | Size: 3.6 KiB |
BIN
Tests/images/hopper_orientation_5.jpg
Normal file
After Width: | Height: | Size: 4.8 KiB |
BIN
Tests/images/hopper_orientation_5.webp
Normal file
After Width: | Height: | Size: 3.5 KiB |
BIN
Tests/images/hopper_orientation_6.jpg
Normal file
After Width: | Height: | Size: 4.8 KiB |
BIN
Tests/images/hopper_orientation_6.webp
Normal file
After Width: | Height: | Size: 3.6 KiB |
BIN
Tests/images/hopper_orientation_7.jpg
Normal file
After Width: | Height: | Size: 4.8 KiB |
BIN
Tests/images/hopper_orientation_7.webp
Normal file
After Width: | Height: | Size: 3.6 KiB |
BIN
Tests/images/hopper_orientation_8.jpg
Normal file
After Width: | Height: | Size: 4.8 KiB |
BIN
Tests/images/hopper_orientation_8.webp
Normal file
After Width: | Height: | Size: 3.6 KiB |
BIN
Tests/images/hopper_roundDown.bmp
Normal file
After Width: | Height: | Size: 48 KiB |
BIN
Tests/images/hopper_roundDown_2.tif
Normal file
BIN
Tests/images/hopper_roundDown_3.tif
Normal file
BIN
Tests/images/hopper_roundDown_None.tif
Normal file
BIN
Tests/images/hopper_roundUp_2.tif
Normal file
BIN
Tests/images/hopper_roundUp_3.tif
Normal file
BIN
Tests/images/hopper_roundUp_None.tif
Normal file
BIN
Tests/images/i_trns.png
Normal file
After Width: | Height: | Size: 1.4 KiB |
BIN
Tests/images/iptc_roundDown.jpg
Normal file
After Width: | Height: | Size: 20 KiB |
BIN
Tests/images/iptc_roundUp.jpg
Normal file
After Width: | Height: | Size: 20 KiB |
BIN
Tests/images/old-style-jpeg-compression.png
Normal file
After Width: | Height: | Size: 1.4 MiB |
BIN
Tests/images/old-style-jpeg-compression.tif
Normal file
BIN
Tests/images/rgb32bf-rgba.bmp
Normal file
After Width: | Height: | Size: 32 KiB |
BIN
Tests/images/sugarshack_frame_size.mpo
Normal file
After Width: | Height: | Size: 117 KiB |
BIN
Tests/images/test_language.png
Normal file
After Width: | Height: | Size: 777 B |
|
@ -71,6 +71,27 @@ class TestFileBmp(PillowTestCase):
|
|||
self.assertEqual(im.size, reloaded.size)
|
||||
self.assertEqual(reloaded.format, "JPEG")
|
||||
|
||||
def test_load_dpi_rounding(self):
|
||||
# Round up
|
||||
im = Image.open('Tests/images/hopper.bmp')
|
||||
self.assertEqual(im.info["dpi"], (96, 96))
|
||||
|
||||
# Round down
|
||||
im = Image.open('Tests/images/hopper_roundDown.bmp')
|
||||
self.assertEqual(im.info["dpi"], (72, 72))
|
||||
|
||||
def test_save_dpi_rounding(self):
|
||||
outfile = self.tempfile("temp.bmp")
|
||||
im = Image.open('Tests/images/hopper.bmp')
|
||||
|
||||
im.save(outfile, dpi=(72.2, 72.2))
|
||||
reloaded = Image.open(outfile)
|
||||
self.assertEqual(reloaded.info["dpi"], (72, 72))
|
||||
|
||||
im.save(outfile, dpi=(72.8, 72.8))
|
||||
reloaded = Image.open(outfile)
|
||||
self.assertEqual(reloaded.info["dpi"], (73, 73))
|
||||
|
||||
def test_load_dib(self):
|
||||
# test for #1293, Imagegrab returning Unsupported Bitfields Format
|
||||
im = Image.open('Tests/images/clipboard.dib')
|
||||
|
@ -90,3 +111,15 @@ class TestFileBmp(PillowTestCase):
|
|||
self.assertEqual(reloaded.format, "DIB")
|
||||
self.assertEqual(reloaded.get_format_mimetype(), "image/bmp")
|
||||
self.assert_image_equal(im, reloaded)
|
||||
|
||||
def test_rgba_bitfields(self):
|
||||
# This test image has been manually hexedited
|
||||
# to change the bitfield compression in the header from XBGR to RGBA
|
||||
im = Image.open("Tests/images/rgb32bf-rgba.bmp")
|
||||
|
||||
# So before the comparing the image, swap the channels
|
||||
b, g, r = im.split()[1:]
|
||||
im = Image.merge("RGB", (r, g, b))
|
||||
|
||||
target = Image.open("Tests/images/bmp/q/rgb32bf-xbgr.bmp")
|
||||
self.assert_image_equal(im, target)
|
||||
|
|
|
@ -230,6 +230,15 @@ class TestFileGif(PillowTestCase):
|
|||
|
||||
self.assertEqual(im.info, info)
|
||||
|
||||
def test_seek_rewind(self):
|
||||
im = Image.open("Tests/images/iss634.gif")
|
||||
im.seek(2)
|
||||
im.seek(1)
|
||||
|
||||
expected = Image.open("Tests/images/iss634.gif")
|
||||
expected.seek(1)
|
||||
self.assert_image_equal(im, expected)
|
||||
|
||||
def test_n_frames(self):
|
||||
for path, n_frames in [
|
||||
[TEST_GIF, 1],
|
||||
|
|
|
@ -14,6 +14,7 @@ class TestFileIco(PillowTestCase):
|
|||
self.assertEqual(im.mode, "RGBA")
|
||||
self.assertEqual(im.size, (16, 16))
|
||||
self.assertEqual(im.format, "ICO")
|
||||
self.assertEqual(im.get_format_mimetype(), "image/x-icon")
|
||||
|
||||
def test_invalid_file(self):
|
||||
with open("Tests/images/flower.jpg", "rb") as fp:
|
||||
|
|
|
@ -524,6 +524,27 @@ class TestFileJpeg(PillowTestCase):
|
|||
reloaded.load()
|
||||
self.assertEqual(im.info['dpi'], reloaded.info['dpi'])
|
||||
|
||||
def test_load_dpi_rounding(self):
|
||||
# Round up
|
||||
im = Image.open('Tests/images/iptc_roundUp.jpg')
|
||||
self.assertEqual(im.info["dpi"], (44, 44))
|
||||
|
||||
# Round down
|
||||
im = Image.open('Tests/images/iptc_roundDown.jpg')
|
||||
self.assertEqual(im.info["dpi"], (2, 2))
|
||||
|
||||
def test_save_dpi_rounding(self):
|
||||
outfile = self.tempfile("temp.jpg")
|
||||
im = Image.open('Tests/images/hopper.jpg')
|
||||
|
||||
im.save(outfile, dpi=(72.2, 72.2))
|
||||
reloaded = Image.open(outfile)
|
||||
self.assertEqual(reloaded.info["dpi"], (72, 72))
|
||||
|
||||
im.save(outfile, dpi=(72.8, 72.8))
|
||||
reloaded = Image.open(outfile)
|
||||
self.assertEqual(reloaded.info["dpi"], (73, 73))
|
||||
|
||||
def test_dpi_tuple_from_exif(self):
|
||||
# Arrange
|
||||
# This Photoshop CC 2017 image has DPI in EXIF not metadata
|
||||
|
@ -590,6 +611,15 @@ class TestFileJpeg(PillowTestCase):
|
|||
# Act / Assert
|
||||
self.assertEqual(im._getexif()[306], '2017:03:13 23:03:09')
|
||||
|
||||
def test_photoshop(self):
|
||||
im = Image.open("Tests/images/photoshop-200dpi.jpg")
|
||||
self.assertEqual(im.info["photoshop"][0x03ed], {
|
||||
'XResolution': 200.0,
|
||||
'DisplayedUnitsX': 1,
|
||||
'YResolution': 200.0,
|
||||
'DisplayedUnitsY': 1,
|
||||
})
|
||||
|
||||
|
||||
@unittest.skipUnless(sys.platform.startswith('win32'), "Windows only")
|
||||
class TestFileCloseW32(PillowTestCase):
|
||||
|
|
|
@ -716,3 +716,10 @@ class TestFileLibTiff(LibTiffTestCase):
|
|||
im = Image.open(infile)
|
||||
|
||||
self.assert_image_similar_tofile(im, "Tests/images/flower.jpg", 0.5)
|
||||
|
||||
def test_old_style_jpeg(self):
|
||||
infile = "Tests/images/old-style-jpeg-compression.tif"
|
||||
im = Image.open(infile)
|
||||
|
||||
self.assert_image_equal_tofile(im,
|
||||
"Tests/images/old-style-jpeg-compression.png")
|
||||
|
|
|
@ -55,6 +55,27 @@ class TestFileMpo(PillowTestCase):
|
|||
self.assertEqual(info[296], 2)
|
||||
self.assertEqual(info[34665], 188)
|
||||
|
||||
def test_frame_size(self):
|
||||
# This image has been hexedited to contain a different size
|
||||
# in the EXIF data of the second frame
|
||||
im = Image.open("Tests/images/sugarshack_frame_size.mpo")
|
||||
self.assertEqual(im.size, (640, 480))
|
||||
|
||||
im.seek(1)
|
||||
self.assertEqual(im.size, (680, 480))
|
||||
|
||||
def test_parallax(self):
|
||||
# Nintendo
|
||||
im = Image.open("Tests/images/sugarshack.mpo")
|
||||
exif = im.getexif()
|
||||
self.assertEqual(exif.get_ifd(0x927c)[0x1101]["Parallax"], -44.798187255859375)
|
||||
|
||||
# Fujifilm
|
||||
im = Image.open("Tests/images/fujifilm.mpo")
|
||||
im.seek(1)
|
||||
exif = im.getexif()
|
||||
self.assertEqual(exif.get_ifd(0x927c)[0xb211], -3.125)
|
||||
|
||||
def test_mp(self):
|
||||
for test_file in test_files:
|
||||
im = Image.open(test_file)
|
||||
|
|
|
@ -13,6 +13,7 @@ class TestFilePcx(PillowTestCase):
|
|||
self.assertEqual(im2.mode, im.mode)
|
||||
self.assertEqual(im2.size, im.size)
|
||||
self.assertEqual(im2.format, "PCX")
|
||||
self.assertEqual(im2.get_format_mimetype(), "image/x-pcx")
|
||||
self.assert_image_equal(im2, im)
|
||||
|
||||
def test_sanity(self):
|
||||
|
|
|
@ -88,20 +88,13 @@ class TestFilePng(PillowTestCase):
|
|||
self.assertEqual(im.format, "PNG")
|
||||
self.assertEqual(im.get_format_mimetype(), 'image/png')
|
||||
|
||||
hopper("1").save(test_file)
|
||||
Image.open(test_file)
|
||||
|
||||
hopper("L").save(test_file)
|
||||
Image.open(test_file)
|
||||
|
||||
hopper("P").save(test_file)
|
||||
Image.open(test_file)
|
||||
|
||||
hopper("RGB").save(test_file)
|
||||
Image.open(test_file)
|
||||
|
||||
hopper("I").save(test_file)
|
||||
Image.open(test_file)
|
||||
for mode in ["1", "L", "P", "RGB", "I", "I;16"]:
|
||||
im = hopper(mode)
|
||||
im.save(test_file)
|
||||
reloaded = Image.open(test_file)
|
||||
if mode == "I;16":
|
||||
reloaded = reloaded.convert(mode)
|
||||
self.assert_image_equal(reloaded, im)
|
||||
|
||||
def test_invalid_file(self):
|
||||
invalid_file = "Tests/images/flower.jpg"
|
||||
|
@ -298,30 +291,32 @@ class TestFilePng(PillowTestCase):
|
|||
self.assert_image(im, "RGBA", (10, 10))
|
||||
self.assertEqual(im.getcolors(), [(100, (0, 0, 0, 0))])
|
||||
|
||||
def test_save_l_transparency(self):
|
||||
# There are 559 transparent pixels in l_trns.png.
|
||||
num_transparent = 559
|
||||
def test_save_greyscale_transparency(self):
|
||||
for mode, num_transparent in {
|
||||
"1": 1994,
|
||||
"L": 559,
|
||||
"I": 559,
|
||||
}.items():
|
||||
in_file = "Tests/images/"+mode.lower()+"_trns.png"
|
||||
im = Image.open(in_file)
|
||||
self.assertEqual(im.mode, mode)
|
||||
self.assertEqual(im.info["transparency"], 255)
|
||||
|
||||
in_file = "Tests/images/l_trns.png"
|
||||
im = Image.open(in_file)
|
||||
self.assertEqual(im.mode, "L")
|
||||
self.assertEqual(im.info["transparency"], 255)
|
||||
im_rgba = im.convert('RGBA')
|
||||
self.assertEqual(
|
||||
im_rgba.getchannel("A").getcolors()[0][0], num_transparent)
|
||||
|
||||
im_rgba = im.convert('RGBA')
|
||||
self.assertEqual(
|
||||
im_rgba.getchannel("A").getcolors()[0][0], num_transparent)
|
||||
test_file = self.tempfile("temp.png")
|
||||
im.save(test_file)
|
||||
|
||||
test_file = self.tempfile("temp.png")
|
||||
im.save(test_file)
|
||||
test_im = Image.open(test_file)
|
||||
self.assertEqual(test_im.mode, mode)
|
||||
self.assertEqual(test_im.info["transparency"], 255)
|
||||
self.assert_image_equal(im, test_im)
|
||||
|
||||
test_im = Image.open(test_file)
|
||||
self.assertEqual(test_im.mode, "L")
|
||||
self.assertEqual(test_im.info["transparency"], 255)
|
||||
self.assert_image_equal(im, test_im)
|
||||
|
||||
test_im_rgba = test_im.convert('RGBA')
|
||||
self.assertEqual(
|
||||
test_im_rgba.getchannel('A').getcolors()[0][0], num_transparent)
|
||||
test_im_rgba = test_im.convert('RGBA')
|
||||
self.assertEqual(
|
||||
test_im_rgba.getchannel('A').getcolors()[0][0], num_transparent)
|
||||
|
||||
def test_save_rgb_single_transparency(self):
|
||||
in_file = "Tests/images/caption_6_33_22.png"
|
||||
|
@ -394,6 +389,24 @@ class TestFilePng(PillowTestCase):
|
|||
im = roundtrip(im, dpi=(100, 100))
|
||||
self.assertEqual(im.info["dpi"], (100, 100))
|
||||
|
||||
def test_load_dpi_rounding(self):
|
||||
# Round up
|
||||
im = Image.open(TEST_PNG_FILE)
|
||||
self.assertEqual(im.info["dpi"], (96, 96))
|
||||
|
||||
# Round down
|
||||
im = Image.open("Tests/images/icc_profile_none.png")
|
||||
self.assertEqual(im.info["dpi"], (72, 72))
|
||||
|
||||
def test_save_dpi_rounding(self):
|
||||
im = Image.open(TEST_PNG_FILE)
|
||||
|
||||
im = roundtrip(im, dpi=(72.2, 72.2))
|
||||
self.assertEqual(im.info["dpi"], (72, 72))
|
||||
|
||||
im = roundtrip(im, dpi=(72.8, 72.8))
|
||||
self.assertEqual(im.info["dpi"], (73, 73))
|
||||
|
||||
def test_roundtrip_text(self):
|
||||
# Check text roundtripping
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
from .helper import PillowTestCase
|
||||
from .helper import PillowTestCase, hopper
|
||||
|
||||
from PIL import Image
|
||||
|
||||
|
@ -36,6 +36,16 @@ class TestFilePpm(PillowTestCase):
|
|||
reloaded = Image.open(f)
|
||||
self.assert_image_equal(im, reloaded)
|
||||
|
||||
def test_pnm(self):
|
||||
im = Image.open('Tests/images/hopper.pnm')
|
||||
self.assert_image_similar(im, hopper(), 0.0001)
|
||||
|
||||
f = self.tempfile('temp.pnm')
|
||||
im.save(f)
|
||||
|
||||
reloaded = Image.open(f)
|
||||
self.assert_image_equal(im, reloaded)
|
||||
|
||||
def test_truncated_file(self):
|
||||
path = self.tempfile('temp.pgm')
|
||||
with open(path, 'w') as f:
|
||||
|
|
|
@ -37,6 +37,8 @@ class TestFileTga(PillowTestCase):
|
|||
path_no_ext, origin, "rle" if rle else "raw")
|
||||
|
||||
original_im = Image.open(tga_path)
|
||||
self.assertEqual(original_im.format, "TGA")
|
||||
self.assertEqual(original_im.get_format_mimetype(), "image/x-tga")
|
||||
if rle:
|
||||
self.assertEqual(
|
||||
original_im.info["compression"], "tga_rle")
|
||||
|
|
|
@ -126,6 +126,30 @@ class TestFileTiff(PillowTestCase):
|
|||
im._setup()
|
||||
self.assertEqual(im.info['dpi'], (71., 71.))
|
||||
|
||||
def test_load_dpi_rounding(self):
|
||||
for resolutionUnit, dpi in ((None, (72, 73)),
|
||||
(2, (72, 73)),
|
||||
(3, (183, 185))):
|
||||
im = Image.open(
|
||||
"Tests/images/hopper_roundDown_"+str(resolutionUnit)+".tif")
|
||||
self.assertEqual(im.tag_v2.get(RESOLUTION_UNIT), resolutionUnit)
|
||||
self.assertEqual(im.info['dpi'], (dpi[0], dpi[0]))
|
||||
|
||||
im = Image.open("Tests/images/hopper_roundUp_"+str(resolutionUnit)+".tif")
|
||||
self.assertEqual(im.tag_v2.get(RESOLUTION_UNIT), resolutionUnit)
|
||||
self.assertEqual(im.info['dpi'], (dpi[1], dpi[1]))
|
||||
|
||||
def test_save_dpi_rounding(self):
|
||||
outfile = self.tempfile("temp.tif")
|
||||
im = Image.open("Tests/images/hopper.tif")
|
||||
|
||||
for dpi in (72.2, 72.8):
|
||||
im.save(outfile, dpi=(dpi, dpi))
|
||||
|
||||
reloaded = Image.open(outfile)
|
||||
reloaded.load()
|
||||
self.assertEqual((round(dpi), round(dpi)), reloaded.info['dpi'])
|
||||
|
||||
def test_save_setting_missing_resolution(self):
|
||||
b = BytesIO()
|
||||
Image.open("Tests/images/10ct_32bit_128.tiff").save(
|
||||
|
@ -229,11 +253,6 @@ class TestFileTiff(PillowTestCase):
|
|||
['Tests/images/multipage-lastframe.tif', 1],
|
||||
['Tests/images/multipage.tiff', 3]
|
||||
]:
|
||||
# Test is_animated before n_frames
|
||||
im = Image.open(path)
|
||||
self.assertEqual(im.is_animated, n_frames != 1)
|
||||
|
||||
# Test is_animated after n_frames
|
||||
im = Image.open(path)
|
||||
self.assertEqual(im.n_frames, n_frames)
|
||||
self.assertEqual(im.is_animated, n_frames != 1)
|
||||
|
@ -263,6 +282,11 @@ class TestFileTiff(PillowTestCase):
|
|||
self.assertEqual(im.size, (10, 10))
|
||||
self.assertEqual(im.convert('RGB').getpixel((0, 0)), (255, 0, 0))
|
||||
|
||||
im.seek(0)
|
||||
im.load()
|
||||
self.assertEqual(im.size, (10, 10))
|
||||
self.assertEqual(im.convert('RGB').getpixel((0, 0)), (0, 128, 0))
|
||||
|
||||
im.seek(2)
|
||||
im.load()
|
||||
self.assertEqual(im.size, (20, 20))
|
||||
|
|
|
@ -45,6 +45,15 @@ class TestFileWmf(PillowTestCase):
|
|||
# Restore the state before this test
|
||||
WmfImagePlugin.register_handler(None)
|
||||
|
||||
def test_load_dpi_rounding(self):
|
||||
# Round up
|
||||
im = Image.open('Tests/images/drawing.emf')
|
||||
self.assertEqual(im.info["dpi"], 1424)
|
||||
|
||||
# Round down
|
||||
im = Image.open('Tests/images/drawing_roundDown.emf')
|
||||
self.assertEqual(im.info["dpi"], 1426)
|
||||
|
||||
def test_save(self):
|
||||
im = hopper()
|
||||
|
||||
|
|
|
@ -3,6 +3,8 @@ from .helper import unittest, PillowTestCase, hopper
|
|||
from PIL import Image
|
||||
from PIL._util import py3
|
||||
import os
|
||||
import sys
|
||||
import shutil
|
||||
|
||||
|
||||
class TestImage(PillowTestCase):
|
||||
|
@ -121,6 +123,16 @@ class TestImage(PillowTestCase):
|
|||
im.paste(0, (0, 0, 100, 100))
|
||||
self.assertFalse(im.readonly)
|
||||
|
||||
@unittest.skipIf(sys.platform.startswith('win32'),
|
||||
"Test requires opening tempfile twice")
|
||||
def test_readonly_save(self):
|
||||
temp_file = self.tempfile("temp.bmp")
|
||||
shutil.copy("Tests/images/rgb32bf-rgba.bmp", temp_file)
|
||||
|
||||
im = Image.open(temp_file)
|
||||
self.assertTrue(im.readonly)
|
||||
im.save(temp_file)
|
||||
|
||||
def test_dump(self):
|
||||
im = Image.new("L", (10, 10))
|
||||
im._dump(self.tempfile("temp_L.ppm"))
|
||||
|
@ -522,6 +534,16 @@ class TestImage(PillowTestCase):
|
|||
_make_new(im, blank_p, ImagePalette.ImagePalette())
|
||||
_make_new(im, blank_pa, ImagePalette.ImagePalette())
|
||||
|
||||
def test_p_from_rgb_rgba(self):
|
||||
for mode, color in [
|
||||
("RGB", '#DDEEFF'),
|
||||
("RGB", (221, 238, 255)),
|
||||
("RGBA", (221, 238, 255, 255))
|
||||
]:
|
||||
im = Image.new("P", (100, 100), color)
|
||||
expected = Image.new(mode, (100, 100), color)
|
||||
self.assert_image_equal(im.convert(mode), expected)
|
||||
|
||||
def test_no_resource_warning_on_save(self):
|
||||
# https://github.com/python-pillow/Pillow/issues/835
|
||||
# Arrange
|
||||
|
@ -532,6 +554,18 @@ class TestImage(PillowTestCase):
|
|||
with Image.open(test_file) as im:
|
||||
self.assert_warning(None, im.save, temp_file)
|
||||
|
||||
def test_load_on_nonexclusive_multiframe(self):
|
||||
with open("Tests/images/frozenpond.mpo", "rb") as fp:
|
||||
def act(fp):
|
||||
im = Image.open(fp)
|
||||
im.load()
|
||||
act(fp)
|
||||
|
||||
with Image.open(fp) as im:
|
||||
im.load()
|
||||
|
||||
self.assertFalse(fp.closed)
|
||||
|
||||
|
||||
class MockEncoder(object):
|
||||
pass
|
||||
|
|
|
@ -12,7 +12,8 @@ class TestImageConvert(PillowTestCase):
|
|||
self.assertEqual(out.mode, mode)
|
||||
self.assertEqual(out.size, im.size)
|
||||
|
||||
modes = "1", "L", "I", "F", "RGB", "RGBA", "RGBX", "CMYK", "YCbCr"
|
||||
modes = ("1", "L", "LA", "P", "PA", "I", "F",
|
||||
"RGB", "RGBA", "RGBX", "CMYK", "YCbCr")
|
||||
|
||||
for mode in modes:
|
||||
im = hopper(mode)
|
||||
|
|
|
@ -28,3 +28,10 @@ class TestImageLoad(PillowTestCase):
|
|||
os.fstat(fn)
|
||||
|
||||
self.assertRaises(OSError, os.fstat, fn)
|
||||
|
||||
def test_contextmanager_non_exclusive_fp(self):
|
||||
with open("Tests/images/hopper.gif", "rb") as fp:
|
||||
with Image.open(fp):
|
||||
pass
|
||||
|
||||
self.assertFalse(fp.closed)
|
||||
|
|
|
@ -42,7 +42,7 @@ class TestImageMode(PillowTestCase):
|
|||
self.assertEqual(signature, result)
|
||||
check("1", "L", "L", 1, ("1",))
|
||||
check("L", "L", "L", 1, ("L",))
|
||||
check("P", "RGB", "L", 1, ("P",))
|
||||
check("P", "P", "L", 1, ("P",))
|
||||
check("I", "L", "I", 1, ("I",))
|
||||
check("F", "L", "F", 1, ("F",))
|
||||
check("RGB", "RGB", "L", 3, ("R", "G", "B"))
|
||||
|
|
|
@ -28,6 +28,13 @@ class TestImagePutAlpha(PillowTestCase):
|
|||
self.assertEqual(im.mode, 'LA')
|
||||
self.assertEqual(im.getpixel((0, 0)), (1, 2))
|
||||
|
||||
im = Image.new("P", (1, 1), 1)
|
||||
self.assertEqual(im.getpixel((0, 0)), 1)
|
||||
|
||||
im.putalpha(2)
|
||||
self.assertEqual(im.mode, 'PA')
|
||||
self.assertEqual(im.getpixel((0, 0)), (1, 2))
|
||||
|
||||
im = Image.new("RGB", (1, 1), (1, 2, 3))
|
||||
self.assertEqual(im.getpixel((0, 0)), (1, 2, 3))
|
||||
|
||||
|
|
|
@ -14,8 +14,10 @@ class TestImagePutPalette(PillowTestCase):
|
|||
return im.mode, p[:10]
|
||||
return im.mode
|
||||
self.assertRaises(ValueError, palette, "1")
|
||||
self.assertEqual(palette("L"), ("P", [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]))
|
||||
self.assertEqual(palette("P"), ("P", [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]))
|
||||
for mode in ["L", "LA", "P", "PA"]:
|
||||
self.assertEqual(palette(mode),
|
||||
("PA" if "A" in mode else "P",
|
||||
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]))
|
||||
self.assertRaises(ValueError, palette, "I")
|
||||
self.assertRaises(ValueError, palette, "F")
|
||||
self.assertRaises(ValueError, palette, "RGB")
|
||||
|
|
|
@ -38,9 +38,10 @@ class TestImageQuantize(PillowTestCase):
|
|||
|
||||
def test_rgba_quantize(self):
|
||||
image = hopper('RGBA')
|
||||
image.quantize()
|
||||
self.assertRaises(ValueError, image.quantize, method=0)
|
||||
|
||||
self.assertEqual(image.quantize().convert().mode, "RGBA")
|
||||
|
||||
def test_quantize(self):
|
||||
image = Image.open('Tests/images/caption_6_33_22.png').convert('RGB')
|
||||
converted = image.quantize()
|
||||
|
|
|
@ -24,7 +24,7 @@ class TestImageTranspose(PillowTestCase):
|
|||
self.assertEqual(im.getpixel((1, y-2)), out.getpixel((x-2, y-2)))
|
||||
self.assertEqual(im.getpixel((x-2, y-2)), out.getpixel((1, y-2)))
|
||||
|
||||
for mode in ("L", "RGB", "I;16", "I;16L", "I;16B"):
|
||||
for mode in self.hopper:
|
||||
transpose(mode)
|
||||
|
||||
def test_flip_top_bottom(self):
|
||||
|
@ -40,7 +40,7 @@ class TestImageTranspose(PillowTestCase):
|
|||
self.assertEqual(im.getpixel((1, y-2)), out.getpixel((1, 1)))
|
||||
self.assertEqual(im.getpixel((x-2, y-2)), out.getpixel((x-2, 1)))
|
||||
|
||||
for mode in ("L", "RGB", "I;16", "I;16L", "I;16B"):
|
||||
for mode in self.hopper:
|
||||
transpose(mode)
|
||||
|
||||
def test_rotate_90(self):
|
||||
|
@ -56,7 +56,7 @@ class TestImageTranspose(PillowTestCase):
|
|||
self.assertEqual(im.getpixel((1, y-2)), out.getpixel((y-2, x-2)))
|
||||
self.assertEqual(im.getpixel((x-2, y-2)), out.getpixel((y-2, 1)))
|
||||
|
||||
for mode in ("L", "RGB"):
|
||||
for mode in self.hopper:
|
||||
transpose(mode)
|
||||
|
||||
def test_rotate_180(self):
|
||||
|
@ -72,7 +72,7 @@ class TestImageTranspose(PillowTestCase):
|
|||
self.assertEqual(im.getpixel((1, y-2)), out.getpixel((x-2, 1)))
|
||||
self.assertEqual(im.getpixel((x-2, y-2)), out.getpixel((1, 1)))
|
||||
|
||||
for mode in ("L", "RGB", "I;16", "I;16L", "I;16B"):
|
||||
for mode in self.hopper:
|
||||
transpose(mode)
|
||||
|
||||
def test_rotate_270(self):
|
||||
|
@ -88,7 +88,7 @@ class TestImageTranspose(PillowTestCase):
|
|||
self.assertEqual(im.getpixel((1, y-2)), out.getpixel((1, 1)))
|
||||
self.assertEqual(im.getpixel((x-2, y-2)), out.getpixel((1, x-2)))
|
||||
|
||||
for mode in ("L", "RGB"):
|
||||
for mode in self.hopper:
|
||||
transpose(mode)
|
||||
|
||||
def test_transpose(self):
|
||||
|
@ -104,7 +104,7 @@ class TestImageTranspose(PillowTestCase):
|
|||
self.assertEqual(im.getpixel((1, y-2)), out.getpixel((y-2, 1)))
|
||||
self.assertEqual(im.getpixel((x-2, y-2)), out.getpixel((y-2, x-2)))
|
||||
|
||||
for mode in ("L", "RGB"):
|
||||
for mode in self.hopper:
|
||||
transpose(mode)
|
||||
|
||||
def test_tranverse(self):
|
||||
|
@ -120,28 +120,29 @@ class TestImageTranspose(PillowTestCase):
|
|||
self.assertEqual(im.getpixel((1, y-2)), out.getpixel((1, x-2)))
|
||||
self.assertEqual(im.getpixel((x-2, y-2)), out.getpixel((1, 1)))
|
||||
|
||||
for mode in ("L", "RGB"):
|
||||
for mode in self.hopper:
|
||||
transpose(mode)
|
||||
|
||||
def test_roundtrip(self):
|
||||
im = self.hopper['L']
|
||||
for mode in self.hopper:
|
||||
im = self.hopper[mode]
|
||||
|
||||
def transpose(first, second):
|
||||
return im.transpose(first).transpose(second)
|
||||
def transpose(first, second):
|
||||
return im.transpose(first).transpose(second)
|
||||
|
||||
self.assert_image_equal(
|
||||
im, transpose(FLIP_LEFT_RIGHT, FLIP_LEFT_RIGHT))
|
||||
self.assert_image_equal(
|
||||
im, transpose(FLIP_TOP_BOTTOM, FLIP_TOP_BOTTOM))
|
||||
self.assert_image_equal(im, transpose(ROTATE_90, ROTATE_270))
|
||||
self.assert_image_equal(im, transpose(ROTATE_180, ROTATE_180))
|
||||
self.assert_image_equal(
|
||||
im.transpose(TRANSPOSE), transpose(ROTATE_90, FLIP_TOP_BOTTOM))
|
||||
self.assert_image_equal(
|
||||
im.transpose(TRANSPOSE), transpose(ROTATE_270, FLIP_LEFT_RIGHT))
|
||||
self.assert_image_equal(
|
||||
im.transpose(TRANSVERSE), transpose(ROTATE_90, FLIP_LEFT_RIGHT))
|
||||
self.assert_image_equal(
|
||||
im.transpose(TRANSVERSE), transpose(ROTATE_270, FLIP_TOP_BOTTOM))
|
||||
self.assert_image_equal(
|
||||
im.transpose(TRANSVERSE), transpose(ROTATE_180, TRANSPOSE))
|
||||
self.assert_image_equal(
|
||||
im, transpose(FLIP_LEFT_RIGHT, FLIP_LEFT_RIGHT))
|
||||
self.assert_image_equal(
|
||||
im, transpose(FLIP_TOP_BOTTOM, FLIP_TOP_BOTTOM))
|
||||
self.assert_image_equal(im, transpose(ROTATE_90, ROTATE_270))
|
||||
self.assert_image_equal(im, transpose(ROTATE_180, ROTATE_180))
|
||||
self.assert_image_equal(
|
||||
im.transpose(TRANSPOSE), transpose(ROTATE_90, FLIP_TOP_BOTTOM))
|
||||
self.assert_image_equal(
|
||||
im.transpose(TRANSPOSE), transpose(ROTATE_270, FLIP_LEFT_RIGHT))
|
||||
self.assert_image_equal(
|
||||
im.transpose(TRANSVERSE), transpose(ROTATE_90, FLIP_LEFT_RIGHT))
|
||||
self.assert_image_equal(
|
||||
im.transpose(TRANSVERSE), transpose(ROTATE_270, FLIP_TOP_BOTTOM))
|
||||
self.assert_image_equal(
|
||||
im.transpose(TRANSVERSE), transpose(ROTATE_180, TRANSPOSE))
|
||||
|
|
|
@ -309,7 +309,7 @@ class TestImageCms(PillowTestCase):
|
|||
2: (False, False, True),
|
||||
3: (False, False, True)
|
||||
})
|
||||
self.assertEqual(p.color_space, 'RGB')
|
||||
|
||||
self.assertIsNone(p.colorant_table)
|
||||
self.assertIsNone(p.colorant_table_out)
|
||||
self.assertIsNone(p.colorimetric_intent)
|
||||
|
@ -361,16 +361,9 @@ class TestImageCms(PillowTestCase):
|
|||
(5000.722328847392,))
|
||||
self.assertEqual(p.model,
|
||||
'IEC 61966-2-1 Default RGB Colour Space - sRGB')
|
||||
self.assertEqual(p.pcs, 'XYZ')
|
||||
|
||||
self.assertIsNone(p.perceptual_rendering_intent_gamut)
|
||||
self.assertEqual(p.product_copyright,
|
||||
'Copyright International Color Consortium, 2009')
|
||||
self.assertEqual(p.product_desc, 'sRGB IEC61966-2-1 black scaled')
|
||||
self.assertEqual(p.product_description,
|
||||
'sRGB IEC61966-2-1 black scaled')
|
||||
self.assertEqual(p.product_manufacturer, '')
|
||||
self.assertEqual(
|
||||
p.product_model, 'IEC 61966-2-1 Default RGB Colour Space - sRGB')
|
||||
|
||||
self.assertEqual(
|
||||
p.profile_description, 'sRGB IEC61966-2-1 black scaled')
|
||||
self.assertEqual(
|
||||
|
@ -393,6 +386,40 @@ class TestImageCms(PillowTestCase):
|
|||
'Reference Viewing Condition in IEC 61966-2-1')
|
||||
self.assertEqual(p.xcolor_space, 'RGB ')
|
||||
|
||||
def test_deprecations(self):
|
||||
self.skip_missing()
|
||||
o = ImageCms.getOpenProfile(SRGB)
|
||||
p = o.profile
|
||||
|
||||
def helper_deprecated(attr, expected):
|
||||
result = self.assert_warning(DeprecationWarning, getattr, p, attr)
|
||||
self.assertEqual(result, expected)
|
||||
|
||||
# p.color_space
|
||||
helper_deprecated("color_space", "RGB")
|
||||
|
||||
# p.pcs
|
||||
helper_deprecated("pcs", "XYZ")
|
||||
|
||||
# p.product_copyright
|
||||
helper_deprecated(
|
||||
"product_copyright", "Copyright International Color Consortium, 2009"
|
||||
)
|
||||
|
||||
# p.product_desc
|
||||
helper_deprecated("product_desc", "sRGB IEC61966-2-1 black scaled")
|
||||
|
||||
# p.product_description
|
||||
helper_deprecated("product_description", "sRGB IEC61966-2-1 black scaled")
|
||||
|
||||
# p.product_manufacturer
|
||||
helper_deprecated("product_manufacturer", "")
|
||||
|
||||
# p.product_model
|
||||
helper_deprecated(
|
||||
"product_model", "IEC 61966-2-1 Default RGB Colour Space - sRGB"
|
||||
)
|
||||
|
||||
def test_profile_typesafety(self):
|
||||
""" Profile init type safety
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
from .helper import PillowTestCase, hopper, fromstring, tostring
|
||||
from .helper import unittest, PillowTestCase, hopper, fromstring, tostring
|
||||
|
||||
from io import BytesIO
|
||||
|
||||
|
@ -6,6 +6,12 @@ from PIL import Image
|
|||
from PIL import ImageFile
|
||||
from PIL import EpsImagePlugin
|
||||
|
||||
try:
|
||||
from PIL import _webp
|
||||
HAVE_WEBP = True
|
||||
except ImportError:
|
||||
HAVE_WEBP = False
|
||||
|
||||
|
||||
codecs = dir(Image.core)
|
||||
|
||||
|
@ -233,3 +239,97 @@ class TestPyDecoder(PillowTestCase):
|
|||
im = MockImageFile(buf)
|
||||
self.assertIsNone(im.format)
|
||||
self.assertIsNone(im.get_format_mimetype())
|
||||
|
||||
def test_exif_jpeg(self):
|
||||
im = Image.open("Tests/images/exif-72dpi-int.jpg") # Little endian
|
||||
exif = im.getexif()
|
||||
self.assertNotIn(258, exif)
|
||||
self.assertIn(40960, exif)
|
||||
self.assertEqual(exif[40963], 450)
|
||||
self.assertEqual(exif[11], "gThumb 3.0.1")
|
||||
|
||||
out = self.tempfile('temp.jpg')
|
||||
exif[258] = 8
|
||||
del exif[40960]
|
||||
exif[40963] = 455
|
||||
exif[11] = "Pillow test"
|
||||
im.save(out, exif=exif)
|
||||
reloaded = Image.open(out)
|
||||
reloaded_exif = reloaded.getexif()
|
||||
self.assertEqual(reloaded_exif[258], 8)
|
||||
self.assertNotIn(40960, exif)
|
||||
self.assertEqual(reloaded_exif[40963], 455)
|
||||
self.assertEqual(exif[11], "Pillow test")
|
||||
|
||||
im = Image.open("Tests/images/no-dpi-in-exif.jpg") # Big endian
|
||||
exif = im.getexif()
|
||||
self.assertNotIn(258, exif)
|
||||
self.assertIn(40962, exif)
|
||||
self.assertEqual(exif[40963], 200)
|
||||
self.assertEqual(exif[305], "Adobe Photoshop CC 2017 (Macintosh)")
|
||||
|
||||
out = self.tempfile('temp.jpg')
|
||||
exif[258] = 8
|
||||
del exif[34665]
|
||||
exif[40963] = 455
|
||||
exif[305] = "Pillow test"
|
||||
im.save(out, exif=exif)
|
||||
reloaded = Image.open(out)
|
||||
reloaded_exif = reloaded.getexif()
|
||||
self.assertEqual(reloaded_exif[258], 8)
|
||||
self.assertNotIn(40960, exif)
|
||||
self.assertEqual(reloaded_exif[40963], 455)
|
||||
self.assertEqual(exif[305], "Pillow test")
|
||||
|
||||
@unittest.skipIf(not HAVE_WEBP or not _webp.HAVE_WEBPANIM,
|
||||
"WebP support not installed with animation")
|
||||
def test_exif_webp(self):
|
||||
im = Image.open("Tests/images/hopper.webp")
|
||||
exif = im.getexif()
|
||||
self.assertEqual(exif, {})
|
||||
|
||||
out = self.tempfile('temp.webp')
|
||||
exif[258] = 8
|
||||
exif[40963] = 455
|
||||
exif[305] = "Pillow test"
|
||||
|
||||
def check_exif():
|
||||
reloaded = Image.open(out)
|
||||
reloaded_exif = reloaded.getexif()
|
||||
self.assertEqual(reloaded_exif[258], 8)
|
||||
self.assertEqual(reloaded_exif[40963], 455)
|
||||
self.assertEqual(exif[305], "Pillow test")
|
||||
im.save(out, exif=exif)
|
||||
check_exif()
|
||||
im.save(out, exif=exif, save_all=True)
|
||||
check_exif()
|
||||
|
||||
def test_exif_png(self):
|
||||
im = Image.open("Tests/images/exif.png")
|
||||
exif = im.getexif()
|
||||
self.assertEqual(exif, {274: 1})
|
||||
|
||||
out = self.tempfile('temp.png')
|
||||
exif[258] = 8
|
||||
del exif[274]
|
||||
exif[40963] = 455
|
||||
exif[305] = "Pillow test"
|
||||
im.save(out, exif=exif)
|
||||
|
||||
reloaded = Image.open(out)
|
||||
reloaded_exif = reloaded.getexif()
|
||||
self.assertEqual(reloaded_exif, {
|
||||
258: 8,
|
||||
40963: 455,
|
||||
305: 'Pillow test',
|
||||
})
|
||||
|
||||
def test_exif_interop(self):
|
||||
im = Image.open("Tests/images/flower.jpg")
|
||||
exif = im.getexif()
|
||||
self.assertEqual(exif.get_ifd(0xa005), {
|
||||
1: 'R98',
|
||||
2: b'0100',
|
||||
4097: 2272,
|
||||
4098: 1704,
|
||||
})
|
||||
|
|
|
@ -6,6 +6,8 @@ from io import BytesIO
|
|||
import os
|
||||
import sys
|
||||
import copy
|
||||
import re
|
||||
import distutils.version
|
||||
|
||||
FONT_PATH = "Tests/fonts/FreeMono.ttf"
|
||||
FONT_SIZE = 20
|
||||
|
@ -49,29 +51,40 @@ class TestImageFont(PillowTestCase):
|
|||
# Freetype has different metrics depending on the version.
|
||||
# (and, other things, but first things first)
|
||||
METRICS = {
|
||||
('2', '3'): {'multiline': 30,
|
||||
'textsize': 12,
|
||||
'getters': (13, 16)},
|
||||
('2', '7'): {'multiline': 6.2,
|
||||
'textsize': 2.5,
|
||||
'getters': (12, 16)},
|
||||
('2', '8'): {'multiline': 6.2,
|
||||
'textsize': 2.5,
|
||||
'getters': (12, 16)},
|
||||
('2', '9'): {'multiline': 6.2,
|
||||
'textsize': 2.5,
|
||||
'getters': (12, 16)},
|
||||
'Default': {'multiline': 0.5,
|
||||
'textsize': 0.5,
|
||||
'getters': (12, 16)},
|
||||
('>=2.3', '<2.4'): {
|
||||
'multiline': 30,
|
||||
'textsize': 12,
|
||||
'getters': (13, 16)},
|
||||
('>=2.7',): {
|
||||
'multiline': 6.2,
|
||||
'textsize': 2.5,
|
||||
'getters': (12, 16)},
|
||||
'Default': {
|
||||
'multiline': 0.5,
|
||||
'textsize': 0.5,
|
||||
'getters': (12, 16)},
|
||||
}
|
||||
|
||||
def setUp(self):
|
||||
freetype_version = tuple(
|
||||
ImageFont.core.freetype2_version.split('.')
|
||||
)[:2]
|
||||
self.metrics = self.METRICS.get(freetype_version,
|
||||
self.METRICS['Default'])
|
||||
freetype = distutils.version.StrictVersion(ImageFont.core.freetype2_version)
|
||||
|
||||
self.metrics = self.METRICS['Default']
|
||||
for conditions, metrics in self.METRICS.items():
|
||||
if not isinstance(conditions, tuple):
|
||||
continue
|
||||
|
||||
for condition in conditions:
|
||||
version = re.sub('[<=>]', '', condition)
|
||||
if (condition.startswith('>=') and freetype >= version) or \
|
||||
(condition.startswith('<') and freetype < version):
|
||||
# Condition was met
|
||||
continue
|
||||
|
||||
# Condition failed
|
||||
break
|
||||
else:
|
||||
# All conditions were met
|
||||
self.metrics = metrics
|
||||
|
||||
def get_font(self):
|
||||
return ImageFont.truetype(FONT_PATH, FONT_SIZE,
|
||||
|
@ -525,6 +538,15 @@ class TestImageFont(PillowTestCase):
|
|||
self.assertEqual(t.getsize_multiline('ABC\nA'), (36, 36))
|
||||
self.assertEqual(t.getsize_multiline('ABC\nAaaa'), (48, 36))
|
||||
|
||||
def test_complex_font_settings(self):
|
||||
# Arrange
|
||||
t = self.get_font()
|
||||
# Act / Assert
|
||||
if t.layout_engine == ImageFont.LAYOUT_BASIC:
|
||||
self.assertRaises(KeyError, t.getmask, 'абвг', direction='rtl')
|
||||
self.assertRaises(KeyError, t.getmask, 'абвг', features=['-kern'])
|
||||
self.assertRaises(KeyError, t.getmask, 'абвг', language='sr')
|
||||
|
||||
|
||||
@unittest.skipUnless(HAS_RAQM, "Raqm not Available")
|
||||
class TestImageFont_RaqmLayout(TestImageFont):
|
||||
|
|
|
@ -130,3 +130,16 @@ class TestImagecomplextext(PillowTestCase):
|
|||
target_img = Image.open(target)
|
||||
|
||||
self.assert_image_similar(im, target_img, .5)
|
||||
|
||||
def test_language(self):
|
||||
ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE)
|
||||
|
||||
im = Image.new(mode='RGB', size=(300, 100))
|
||||
draw = ImageDraw.Draw(im)
|
||||
draw.text((0, 0), 'абвг', font=ttf, fill=500,
|
||||
language='sr')
|
||||
|
||||
target = 'Tests/images/test_language.png'
|
||||
target_img = Image.open(target)
|
||||
|
||||
self.assert_image_similar(im, target_img, .5)
|
||||
|
|
|
@ -1,7 +1,13 @@
|
|||
from .helper import PillowTestCase, hopper
|
||||
|
||||
from PIL import ImageOps
|
||||
from PIL import Image
|
||||
from PIL import ImageOps
|
||||
|
||||
try:
|
||||
from PIL import _webp
|
||||
HAVE_WEBP = True
|
||||
except ImportError:
|
||||
HAVE_WEBP = False
|
||||
|
||||
|
||||
class TestImageOps(PillowTestCase):
|
||||
|
@ -62,6 +68,9 @@ class TestImageOps(PillowTestCase):
|
|||
ImageOps.solarize(hopper("L"))
|
||||
ImageOps.solarize(hopper("RGB"))
|
||||
|
||||
ImageOps.exif_transpose(hopper("L"))
|
||||
ImageOps.exif_transpose(hopper("RGB"))
|
||||
|
||||
def test_1pxfit(self):
|
||||
# Division by zero in equalize if image is 1 pixel high
|
||||
newimg = ImageOps.fit(hopper("RGB").resize((1, 1)), (35, 35))
|
||||
|
@ -218,3 +227,36 @@ class TestImageOps(PillowTestCase):
|
|||
(0, 127, 0),
|
||||
threshold=1,
|
||||
msg='white test pixel incorrect')
|
||||
|
||||
def test_exif_transpose(self):
|
||||
exts = [".jpg"]
|
||||
if HAVE_WEBP and _webp.HAVE_WEBPANIM:
|
||||
exts.append(".webp")
|
||||
for ext in exts:
|
||||
base_im = Image.open("Tests/images/hopper"+ext)
|
||||
|
||||
orientations = [base_im]
|
||||
for i in range(2, 9):
|
||||
im = Image.open("Tests/images/hopper_orientation_"+str(i)+ext)
|
||||
orientations.append(im)
|
||||
for i, orientation_im in enumerate(orientations):
|
||||
for im in [
|
||||
orientation_im, # ImageFile
|
||||
orientation_im.copy() # Image
|
||||
]:
|
||||
if i == 0:
|
||||
self.assertNotIn("exif", im.info)
|
||||
else:
|
||||
original_exif = im.info["exif"]
|
||||
transposed_im = ImageOps.exif_transpose(im)
|
||||
self.assert_image_similar(base_im, transposed_im, 17)
|
||||
if i == 0:
|
||||
self.assertNotIn("exif", im.info)
|
||||
else:
|
||||
self.assertNotEqual(transposed_im.info["exif"], original_exif)
|
||||
|
||||
self.assertNotIn(0x0112, transposed_im.getexif())
|
||||
|
||||
# Repeat the operation, to test that it does not keep transposing
|
||||
transposed_im2 = ImageOps.exif_transpose(transposed_im)
|
||||
self.assert_image_equal(transposed_im2, transposed_im)
|
||||
|
|
|
@ -113,12 +113,7 @@ class TestNumpy(PillowTestCase):
|
|||
img = Image.fromarray(arr * 255).convert('1')
|
||||
self.assertEqual(img.mode, '1')
|
||||
arr_back = numpy.array(img)
|
||||
# numpy 1.8 and earlier return this as a boolean. (trusty/precise)
|
||||
if arr_back.dtype == numpy.bool:
|
||||
arr_bool = numpy.array([[1, 0, 0, 1, 0], [0, 1, 0, 0, 0]], 'bool')
|
||||
numpy.testing.assert_array_equal(arr_bool, arr_back)
|
||||
else:
|
||||
numpy.testing.assert_array_equal(arr, arr_back)
|
||||
numpy.testing.assert_array_equal(arr, arr_back)
|
||||
|
||||
def test_save_tiff_uint16(self):
|
||||
# Tests that we're getting the pixel value in the right byte order.
|
||||
|
@ -143,21 +138,21 @@ class TestNumpy(PillowTestCase):
|
|||
|
||||
np_img = numpy.array(img)
|
||||
self._test_img_equals_nparray(img, np_img)
|
||||
self.assertEqual(np_img.dtype, numpy.dtype(dtype))
|
||||
self.assertEqual(np_img.dtype, dtype)
|
||||
|
||||
modes = [("L", 'uint8'),
|
||||
("I", 'int32'),
|
||||
("F", 'float32'),
|
||||
("LA", 'uint8'),
|
||||
("RGB", 'uint8'),
|
||||
("RGBA", 'uint8'),
|
||||
("RGBX", 'uint8'),
|
||||
("CMYK", 'uint8'),
|
||||
("YCbCr", 'uint8'),
|
||||
modes = [("L", numpy.uint8),
|
||||
("I", numpy.int32),
|
||||
("F", numpy.float32),
|
||||
("LA", numpy.uint8),
|
||||
("RGB", numpy.uint8),
|
||||
("RGBA", numpy.uint8),
|
||||
("RGBX", numpy.uint8),
|
||||
("CMYK", numpy.uint8),
|
||||
("YCbCr", numpy.uint8),
|
||||
("I;16", '<u2'),
|
||||
("I;16B", '>u2'),
|
||||
("I;16L", '<u2'),
|
||||
("HSV", 'uint8'),
|
||||
("HSV", numpy.uint8),
|
||||
]
|
||||
|
||||
for mode in modes:
|
||||
|
@ -167,7 +162,7 @@ class TestNumpy(PillowTestCase):
|
|||
# see https://github.com/python-pillow/Pillow/issues/439
|
||||
|
||||
data = list(range(256))*3
|
||||
lut = numpy.array(data, dtype='uint8')
|
||||
lut = numpy.array(data, dtype=numpy.uint8)
|
||||
|
||||
im = hopper()
|
||||
|
||||
|
|
|
@ -10,7 +10,6 @@ except ImportError:
|
|||
|
||||
@unittest.skipIf(pyroma is None, "Pyroma is not installed")
|
||||
class TestPyroma(PillowTestCase):
|
||||
|
||||
def test_pyroma(self):
|
||||
# Arrange
|
||||
data = pyroma.projectdata.get_data(".")
|
||||
|
@ -19,12 +18,13 @@ class TestPyroma(PillowTestCase):
|
|||
rating = pyroma.ratings.rate(data)
|
||||
|
||||
# Assert
|
||||
if 'rc' in __version__:
|
||||
# Pyroma needs to chill about RC versions
|
||||
# and not kill all our tests.
|
||||
self.assertEqual(rating, (9, [
|
||||
"The package's version number does not comply with PEP-386."]))
|
||||
if "rc" in __version__:
|
||||
# Pyroma needs to chill about RC versions and not kill all our tests.
|
||||
self.assertEqual(
|
||||
rating,
|
||||
(9, ["The package's version number does not comply with PEP-386."]),
|
||||
)
|
||||
|
||||
else:
|
||||
# Should have a perfect score
|
||||
self.assertEqual(rating, (10, []))
|
||||
# Should have a near-perfect score
|
||||
self.assertEqual(rating, (9, ["Your package does not have license data."]))
|
||||
|
|
|
@ -11,17 +11,6 @@ class TestFromQPixmap(PillowQPixmapTestCase, PillowTestCase):
|
|||
# Qt saves all pixmaps as rgb
|
||||
self.assert_image_equal(result, expected.convert('RGB'))
|
||||
|
||||
def test_sanity_1(self):
|
||||
self.roundtrip(hopper('1'))
|
||||
|
||||
def test_sanity_rgb(self):
|
||||
self.roundtrip(hopper('RGB'))
|
||||
|
||||
def test_sanity_rgba(self):
|
||||
self.roundtrip(hopper('RGBA'))
|
||||
|
||||
def test_sanity_l(self):
|
||||
self.roundtrip(hopper('L'))
|
||||
|
||||
def test_sanity_p(self):
|
||||
self.roundtrip(hopper('P'))
|
||||
def test_sanity(self):
|
||||
for mode in ('1', 'RGB', 'RGBA', 'L', 'P'):
|
||||
self.roundtrip(hopper(mode))
|
||||
|
|
|
@ -23,13 +23,13 @@ jobs:
|
|||
|
||||
- template: .azure-pipelines/jobs/test-docker.yml
|
||||
parameters:
|
||||
docker: 'ubuntu-trusty-x86'
|
||||
name: 'ubuntu_trusty_x86'
|
||||
docker: 'ubuntu-16.04-xenial-amd64'
|
||||
name: 'ubuntu_16_04_xenial_amd64'
|
||||
|
||||
- template: .azure-pipelines/jobs/test-docker.yml
|
||||
parameters:
|
||||
docker: 'ubuntu-xenial-amd64'
|
||||
name: 'ubuntu_xenial_amd64'
|
||||
docker: 'ubuntu-18.04-bionic-amd64'
|
||||
name: 'ubuntu_18_04_bionic_amd64'
|
||||
|
||||
- template: .azure-pipelines/jobs/test-docker.yml
|
||||
parameters:
|
||||
|
|
|
@ -79,6 +79,26 @@ PILLOW_VERSION constant
|
|||
``PILLOW_VERSION`` has been deprecated and will be removed in the next
|
||||
major release. Use ``__version__`` instead.
|
||||
|
||||
ImageCms.CmsProfile attributes
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. deprecated:: 3.2.0
|
||||
|
||||
Some attributes in ``ImageCms.CmsProfile`` are deprecated. From 6.0.0, they issue a
|
||||
``DeprecationWarning``:
|
||||
|
||||
======================== ===============================
|
||||
Deprecated Use instead
|
||||
======================== ===============================
|
||||
``color_space`` Padded ``xcolor_space``
|
||||
``pcs`` Padded ``connection_space``
|
||||
``product_copyright`` Unicode ``copyright``
|
||||
``product_desc`` Unicode ``profile_description``
|
||||
``product_description`` Unicode ``profile_description``
|
||||
``product_manufacturer`` Unicode ``manufacturer``
|
||||
``product_model`` Unicode ``model``
|
||||
======================== ===============================
|
||||
|
||||
Removed features
|
||||
----------------
|
||||
|
||||
|
|
|
@ -104,11 +104,11 @@ The :py:meth:`~PIL.Image.Image.open` method sets the following
|
|||
Reading sequences
|
||||
~~~~~~~~~~~~~~~~~
|
||||
|
||||
The GIF loader supports the :py:meth:`~file.seek` and :py:meth:`~file.tell`
|
||||
methods. You can seek to the next frame (``im.seek(im.tell() + 1)``), or rewind
|
||||
the file by seeking to the first frame. Random access is not supported.
|
||||
The GIF loader supports the :py:meth:`~PIL.Image.Image.seek` and
|
||||
:py:meth:`~PIL.Image.Image.tell` methods. You can combine these methods
|
||||
to seek to the next frame (``im.seek(im.tell() + 1)``).
|
||||
|
||||
``im.seek()`` raises an ``EOFError`` if you try to seek after the last frame.
|
||||
``im.seek()`` raises an :py:exc:`EOFError` if you try to seek after the last frame.
|
||||
|
||||
Saving
|
||||
~~~~~~
|
||||
|
@ -459,12 +459,14 @@ Pillow reads and writes PCX files containing ``1``, ``L``, ``P``, or ``RGB`` dat
|
|||
PNG
|
||||
^^^
|
||||
|
||||
Pillow identifies, reads, and writes PNG files containing ``1``, ``L``, ``P``,
|
||||
``RGB``, or ``RGBA`` data. Interlaced files are supported as of v1.1.7.
|
||||
Pillow identifies, reads, and writes PNG files containing ``1``, ``L``, ``LA``,
|
||||
``I``, ``P``, ``RGB`` or ``RGBA`` data. Interlaced files are supported as of
|
||||
v1.1.7.
|
||||
|
||||
As of Pillow 6.0, EXIF data can be read from PNG images. However, unlike other
|
||||
image formats, EXIF data is not guaranteed to have been read until
|
||||
:py:meth:`~PIL.Image.Image.load` has been called.
|
||||
image formats, EXIF data is not guaranteed to be present in
|
||||
:py:attr:`~PIL.Image.Image.info` until :py:meth:`~PIL.Image.Image.load` has been
|
||||
called.
|
||||
|
||||
The :py:meth:`~PIL.Image.Image.open` method sets the following
|
||||
:py:attr:`~PIL.Image.Image.info` properties, when appropriate:
|
||||
|
@ -489,12 +491,12 @@ The :py:meth:`~PIL.Image.Image.open` method sets the following
|
|||
For ``P`` images: Either the palette index for full transparent pixels,
|
||||
or a byte string with alpha values for each palette entry.
|
||||
|
||||
For ``L`` and ``RGB`` images, the color that represents full transparent
|
||||
pixels in this image.
|
||||
For ``1``, ``L``, ``I`` and ``RGB`` images, the color that represents
|
||||
full transparent pixels in this image.
|
||||
|
||||
This key is omitted if the image is not a transparent palette image.
|
||||
|
||||
``Open`` also sets ``Image.text`` to a dictionary of the values of the
|
||||
``open`` also sets ``Image.text`` to a dictionary of the values of the
|
||||
``tEXt``, ``zTXt``, and ``iTXt`` chunks of the PNG image. Individual
|
||||
compressed chunks are limited to a decompressed size of
|
||||
``PngImagePlugin.MAX_TEXT_CHUNK``, by default 1MB, to prevent
|
||||
|
@ -510,8 +512,8 @@ The :py:meth:`~PIL.Image.Image.save` method supports the following options:
|
|||
encoder settings.
|
||||
|
||||
**transparency**
|
||||
For ``P``, ``L``, and ``RGB`` images, this option controls what
|
||||
color image to mark as transparent.
|
||||
For ``P``, ``1``, ``L``, ``I``, and ``RGB`` images, this option controls
|
||||
what color from the image to mark as transparent.
|
||||
|
||||
For ``P`` images, this can be a either the palette index,
|
||||
or a byte string with alpha values for each palette entry.
|
||||
|
@ -552,7 +554,7 @@ The :py:meth:`~PIL.Image.Image.save` method supports the following options:
|
|||
PPM
|
||||
^^^
|
||||
|
||||
Pillow reads and writes PBM, PGM and PPM files containing ``1``, ``L`` or
|
||||
Pillow reads and writes PBM, PGM, PPM and PNM files containing ``1``, ``L`` or
|
||||
``RGB`` data.
|
||||
|
||||
SGI
|
||||
|
@ -568,7 +570,7 @@ Pillow reads and writes SPIDER image files of 32-bit floating point data
|
|||
("F;32F").
|
||||
|
||||
Pillow also reads SPIDER stack files containing sequences of SPIDER images. The
|
||||
:py:meth:`~file.seek` and :py:meth:`~file.tell` methods are supported, and
|
||||
:py:meth:`~PIL.Image.Image.seek` and :py:meth:`~PIL.Image.Image.tell` methods are supported, and
|
||||
random access is allowed.
|
||||
|
||||
The :py:meth:`~PIL.Image.Image.open` method sets the following attributes:
|
||||
|
@ -579,7 +581,7 @@ The :py:meth:`~PIL.Image.Image.open` method sets the following attributes:
|
|||
**istack**
|
||||
Set to 1 if the file is an image stack, else 0.
|
||||
|
||||
**nimages**
|
||||
**n_frames**
|
||||
Set to the number of images in the stack.
|
||||
|
||||
A convenience method, :py:meth:`~PIL.Image.Image.convert2byte`, is provided for
|
||||
|
@ -663,6 +665,17 @@ numbers are returned as a tuple of ``(numerator, denominator)``.
|
|||
|
||||
.. deprecated:: 3.0.0
|
||||
|
||||
Reading Multi-frame TIFF Images
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
The TIFF loader supports the :py:meth:`~PIL.Image.Image.seek` and
|
||||
:py:meth:`~PIL.Image.Image.tell` methods, taking and returning frame numbers
|
||||
within the image file. You can combine these methods to seek to the next frame
|
||||
(``im.seek(im.tell() + 1)``). Frames are numbered from 0 to ``im.num_frames - 1``,
|
||||
and can be accessed in any order.
|
||||
|
||||
``im.seek()`` raises an :py:exc:`EOFError` if you try to seek after the
|
||||
last frame.
|
||||
|
||||
Saving Tiff Images
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
|
@ -850,7 +863,7 @@ is commonly used in fax applications. The DCX decoder can read files containing
|
|||
``1``, ``L``, ``P``, or ``RGB`` data.
|
||||
|
||||
When the file is opened, only the first image is read. You can use
|
||||
:py:meth:`~file.seek` or :py:mod:`~PIL.ImageSequence` to read other images.
|
||||
:py:meth:`~PIL.Image.Image.seek` or :py:mod:`~PIL.ImageSequence` to read other images.
|
||||
|
||||
|
||||
DDS
|
||||
|
@ -942,8 +955,8 @@ MIC
|
|||
^^^
|
||||
|
||||
Pillow identifies and reads Microsoft Image Composer (MIC) files. When opened,
|
||||
the first sprite in the file is loaded. You can use :py:meth:`~file.seek` and
|
||||
:py:meth:`~file.tell` to read other sprites from the file.
|
||||
the first sprite in the file is loaded. You can use :py:meth:`~PIL.Image.Image.seek` and
|
||||
:py:meth:`~PIL.Image.Image.tell` to read other sprites from the file.
|
||||
|
||||
Note that there may be an embedded gamma of 2.2 in MIC files.
|
||||
|
||||
|
@ -951,7 +964,7 @@ MPO
|
|||
^^^
|
||||
|
||||
Pillow identifies and reads Multi Picture Object (MPO) files, loading the primary
|
||||
image when first opened. The :py:meth:`~file.seek` and :py:meth:`~file.tell`
|
||||
image when first opened. The :py:meth:`~PIL.Image.Image.seek` and :py:meth:`~PIL.Image.Image.tell`
|
||||
methods may be used to read other pictures from the file. The pictures are
|
||||
zero-indexed and random access is supported.
|
||||
|
||||
|
|
|
@ -396,10 +396,6 @@ Reading sequences
|
|||
As seen in this example, you’ll get an :py:exc:`EOFError` exception when the
|
||||
sequence ends.
|
||||
|
||||
Note that most drivers in the current version of the library only allow you to
|
||||
seek to the next frame (as in the above example). To rewind the file, you may
|
||||
have to reopen it.
|
||||
|
||||
The following class lets you use the for-statement to loop over the sequence:
|
||||
|
||||
Using the ImageSequence Iterator class
|
||||
|
|
|
@ -30,8 +30,8 @@ Pillow is the friendly PIL fork by `Alex Clark and Contributors <https://github.
|
|||
:target: https://pypi.org/project/Pillow/
|
||||
:alt: Number of PyPI downloads
|
||||
|
||||
.. image:: https://coveralls.io/repos/python-pillow/Pillow/badge.svg?branch=master&service=github
|
||||
:target: https://coveralls.io/github/python-pillow/Pillow?branch=master
|
||||
.. image:: https://codecov.io/gh/python-pillow/Pillow/branch/master/graph/badge.svg
|
||||
:target: https://codecov.io/gh/python-pillow/Pillow
|
||||
:alt: Code coverage
|
||||
|
||||
.. toctree::
|
||||
|
|
|
@ -151,7 +151,7 @@ Many of Pillow's features require external libraries:
|
|||
* **littlecms** provides color management
|
||||
|
||||
* Pillow version 2.2.1 and below uses liblcms1, Pillow 2.3.0 and
|
||||
above uses liblcms2. Tested with **1.19** and **2.7**.
|
||||
above uses liblcms2. Tested with **1.19** and **2.7-2.9**.
|
||||
|
||||
* **libwebp** provides the WebP format.
|
||||
|
||||
|
@ -165,7 +165,7 @@ Many of Pillow's features require external libraries:
|
|||
|
||||
* Pillow has been tested with openjpeg **2.0.0** and **2.1.0**.
|
||||
* Pillow does **not** support the earlier **1.5** series which ships
|
||||
with Ubuntu <= 14.04 and Debian Jessie.
|
||||
with Debian Jessie.
|
||||
|
||||
* **libimagequant** provides improved color quantization
|
||||
|
||||
|
@ -403,10 +403,6 @@ These platforms are built and tested for every change.
|
|||
| Ubuntu Linux 16.04 LTS | 2.7, 3.5, 3.6, 3.7, |x86-64 |
|
||||
| | PyPy, PyPy3 | |
|
||||
+----------------------------------+-------------------------------+-----------------------+
|
||||
| Ubuntu Linux 14.04 LTS | 2.7, 3.5, 3.6 |x86-64 |
|
||||
| +-------------------------------+-----------------------+
|
||||
| | 2.7 |x86 |
|
||||
+----------------------------------+-------------------------------+-----------------------+
|
||||
| Windows Server 2012 R2 | 2.7, 3.5, 3.6, 3.7 |x86, x86-64 |
|
||||
| +-------------------------------+-----------------------+
|
||||
| | PyPy, 3.7/MinGW |x86 |
|
||||
|
|
|
@ -132,21 +132,21 @@ can be easily displayed in a chromaticity diagram, for example).
|
|||
|
||||
.. py:attribute:: manufacturer
|
||||
|
||||
The (english) display string for the device manufacturer (see
|
||||
The (English) display string for the device manufacturer (see
|
||||
9.2.22 of ICC.1:2010).
|
||||
|
||||
:type: :py:class:`unicode` or ``None``
|
||||
|
||||
.. py:attribute:: model
|
||||
|
||||
The (english) display string for the device model of the device
|
||||
The (English) display string for the device model of the device
|
||||
for which this profile is created (see 9.2.23 of ICC.1:2010).
|
||||
|
||||
:type: :py:class:`unicode` or ``None``
|
||||
|
||||
.. py:attribute:: profile_description
|
||||
|
||||
The (english) display string for the profile description (see
|
||||
The (English) display string for the profile description (see
|
||||
9.2.41 of ICC.1:2010).
|
||||
|
||||
:type: :py:class:`unicode` or ``None``
|
||||
|
@ -269,14 +269,14 @@ can be easily displayed in a chromaticity diagram, for example).
|
|||
|
||||
.. py:attribute:: viewing_condition
|
||||
|
||||
The (english) display string for the viewing conditions (see
|
||||
The (English) display string for the viewing conditions (see
|
||||
9.2.48 of ICC.1:2010).
|
||||
|
||||
:type: :py:class:`unicode` or ``None``
|
||||
|
||||
.. py:attribute:: screening_description
|
||||
|
||||
The (english) display string for the screening conditions.
|
||||
The (English) display string for the screening conditions.
|
||||
|
||||
This tag was available in ICC 3.2, but it is removed from
|
||||
version 4.
|
||||
|
|
|
@ -255,7 +255,7 @@ Methods
|
|||
|
||||
Draw a shape.
|
||||
|
||||
.. py:method:: PIL.ImageDraw.ImageDraw.text(xy, text, fill=None, font=None, anchor=None, spacing=0, align="left", direction=None, features=None)
|
||||
.. py:method:: PIL.ImageDraw.ImageDraw.text(xy, text, fill=None, font=None, anchor=None, spacing=0, align="left", direction=None, features=None, language=None)
|
||||
|
||||
Draws the string at the given position.
|
||||
|
||||
|
@ -287,7 +287,17 @@ Methods
|
|||
|
||||
.. versionadded:: 4.2.0
|
||||
|
||||
.. py:method:: PIL.ImageDraw.ImageDraw.multiline_text(xy, text, fill=None, font=None, anchor=None, spacing=0, align="left", direction=None, features=None)
|
||||
:param language: Language of the text. Different languages may use
|
||||
different glyph shapes or ligatures. This parameter tells
|
||||
the font which language the text is in, and to apply the
|
||||
correct substitutions as appropriate, if available.
|
||||
It should be a `BCP47 language code
|
||||
<https://www.w3.org/International/articles/language-tags/>`
|
||||
Requires libraqm.
|
||||
|
||||
.. versionadded:: 6.0.0
|
||||
|
||||
.. py:method:: PIL.ImageDraw.ImageDraw.multiline_text(xy, text, fill=None, font=None, anchor=None, spacing=0, align="left", direction=None, features=None, language=None)
|
||||
|
||||
Draws the string at the given position.
|
||||
|
||||
|
@ -316,7 +326,17 @@ Methods
|
|||
|
||||
.. versionadded:: 4.2.0
|
||||
|
||||
.. py:method:: PIL.ImageDraw.ImageDraw.textsize(text, font=None, spacing=4, direction=None, features=None)
|
||||
:param language: Language of the text. Different languages may use
|
||||
different glyph shapes or ligatures. This parameter tells
|
||||
the font which language the text is in, and to apply the
|
||||
correct substitutions as appropriate, if available.
|
||||
It should be a `BCP47 language code
|
||||
<https://www.w3.org/International/articles/language-tags/>`
|
||||
Requires libraqm.
|
||||
|
||||
.. versionadded:: 6.0.0
|
||||
|
||||
.. py:method:: PIL.ImageDraw.ImageDraw.textsize(text, font=None, spacing=4, direction=None, features=None, language=None)
|
||||
|
||||
Return the size of the given string, in pixels.
|
||||
|
||||
|
@ -330,7 +350,6 @@ Methods
|
|||
Requires libraqm.
|
||||
|
||||
.. versionadded:: 4.2.0
|
||||
|
||||
:param features: A list of OpenType font features to be used during text
|
||||
layout. This is usually used to turn on optional
|
||||
font features that are not enabled by default,
|
||||
|
@ -343,8 +362,17 @@ Methods
|
|||
Requires libraqm.
|
||||
|
||||
.. versionadded:: 4.2.0
|
||||
:param language: Language of the text. Different languages may use
|
||||
different glyph shapes or ligatures. This parameter tells
|
||||
the font which language the text is in, and to apply the
|
||||
correct substitutions as appropriate, if available.
|
||||
It should be a `BCP47 language code
|
||||
<https://www.w3.org/International/articles/language-tags/>`
|
||||
Requires libraqm.
|
||||
|
||||
.. py:method:: PIL.ImageDraw.ImageDraw.multiline_textsize(text, font=None, spacing=4, direction=None, features=None)
|
||||
.. versionadded:: 6.0.0
|
||||
|
||||
.. py:method:: PIL.ImageDraw.ImageDraw.multiline_textsize(text, font=None, spacing=4, direction=None, features=None, language=None)
|
||||
|
||||
Return the size of the given string, in pixels.
|
||||
|
||||
|
@ -370,6 +398,16 @@ Methods
|
|||
|
||||
.. versionadded:: 4.2.0
|
||||
|
||||
:param language: Language of the text. Different languages may use
|
||||
different glyph shapes or ligatures. This parameter tells
|
||||
the font which language the text is in, and to apply the
|
||||
correct substitutions as appropriate, if available.
|
||||
It should be a `BCP47 language code
|
||||
<https://www.w3.org/International/articles/language-tags/>`
|
||||
Requires libraqm.
|
||||
|
||||
.. versionadded:: 6.0.0
|
||||
|
||||
.. py:method:: PIL.ImageDraw.getdraw(im=None, hints=None)
|
||||
|
||||
.. warning:: This method is experimental.
|
||||
|
|
|
@ -47,11 +47,45 @@ Functions
|
|||
Methods
|
||||
-------
|
||||
|
||||
.. py:method:: PIL.ImageFont.ImageFont.getsize(text)
|
||||
.. py:method:: PIL.ImageFont.ImageFont.getsize(text, direction=None, features=[], language=None)
|
||||
|
||||
Returns width and height (in pixels) of given text if rendered in font with
|
||||
provided direction, features, and language.
|
||||
|
||||
:param text: Text to measure.
|
||||
|
||||
:param direction: Direction of the text. It can be 'rtl' (right to
|
||||
left), 'ltr' (left to right) or 'ttb' (top to bottom).
|
||||
Requires libraqm.
|
||||
|
||||
.. versionadded:: 4.2.0
|
||||
|
||||
:param features: A list of OpenType font features to be used during text
|
||||
layout. This is usually used to turn on optional
|
||||
font features that are not enabled by default,
|
||||
for example 'dlig' or 'ss01', but can be also
|
||||
used to turn off default font features for
|
||||
example '-liga' to disable ligatures or '-kern'
|
||||
to disable kerning. To get all supported
|
||||
features, see
|
||||
https://docs.microsoft.com/en-us/typography/opentype/spec/featurelist
|
||||
Requires libraqm.
|
||||
|
||||
.. versionadded:: 4.2.0
|
||||
|
||||
:param language: Language of the text. Different languages may use
|
||||
different glyph shapes or ligatures. This parameter tells
|
||||
the font which language the text is in, and to apply the
|
||||
correct substitutions as appropriate, if available.
|
||||
It should be a `BCP47 language code
|
||||
<https://www.w3.org/International/articles/language-tags/>`
|
||||
Requires libraqm.
|
||||
|
||||
.. versionadded:: 6.0.0
|
||||
|
||||
:return: (width, height)
|
||||
|
||||
.. py:method:: PIL.ImageFont.ImageFont.getmask(text, mode='', direction=None, features=[])
|
||||
.. py:method:: PIL.ImageFont.ImageFont.getmask(text, mode='', direction=None, features=[], language=None)
|
||||
|
||||
Create a bitmap for the text.
|
||||
|
||||
|
@ -85,5 +119,15 @@ Methods
|
|||
|
||||
.. versionadded:: 4.2.0
|
||||
|
||||
:param language: Language of the text. Different languages may use
|
||||
different glyph shapes or ligatures. This parameter tells
|
||||
the font which language the text is in, and to apply the
|
||||
correct substitutions as appropriate, if available.
|
||||
It should be a `BCP47 language code
|
||||
<https://www.w3.org/International/articles/language-tags/>`
|
||||
Requires libraqm.
|
||||
|
||||
.. versionadded:: 6.0.0
|
||||
|
||||
:return: An internal PIL storage memory instance as defined by the
|
||||
:py:mod:`PIL.Image.core` interface module.
|
||||
|
|
|
@ -3,10 +3,10 @@
|
|||
File Handling in Pillow
|
||||
=======================
|
||||
|
||||
When opening a file as an image, Pillow requires a filename,
|
||||
pathlib.Path object, or a file-like object. Pillow uses the filename
|
||||
or Path to open a file, so for the rest of this article, they will all
|
||||
be treated as a file-like object.
|
||||
When opening a file as an image, Pillow requires a filename, ``pathlib.Path``
|
||||
object, or a file-like object. Pillow uses the filename or ``Path`` to open a
|
||||
file, so for the rest of this article, they will all be treated as a file-like
|
||||
object.
|
||||
|
||||
The first four of these items are equivalent, the last is dangerous
|
||||
and may fail::
|
||||
|
@ -48,24 +48,23 @@ Issues
|
|||
Image Lifecycle
|
||||
---------------
|
||||
|
||||
* ``Image.open()`` Path-like objects are opened as a file. Metadata is read
|
||||
from the open file. The file is left open for further usage.
|
||||
* ``Image.open()`` Filenames and ``Path`` objects are opened as a file.
|
||||
Metadata is read from the open file. The file is left open for further usage.
|
||||
|
||||
* ``Image.Image.load()`` When the pixel data from the image is
|
||||
required, ``load()`` is called. The current frame is read into
|
||||
memory. The image can now be used independently of the underlying
|
||||
image file.
|
||||
|
||||
If a filename or a path-like object was passed to ``Image.open()``, then
|
||||
the file object was opened by Pillow and is considered to be used exclusively
|
||||
by Pillow. So if the image is a single-frame image, the file will
|
||||
be closed in this method after the frame is read. If the image is a
|
||||
multi-frame image, (e.g. multipage TIFF and animated GIF) the image file is
|
||||
left open so that ``Image.Image.seek()`` can load the appropriate frame.
|
||||
If a filename or a ``Path`` object was passed to ``Image.open()``, then the
|
||||
file object was opened by Pillow and is considered to be used exclusively by
|
||||
Pillow. So if the image is a single-frame image, the file will be closed in
|
||||
this method after the frame is read. If the image is a multi-frame image,
|
||||
(e.g. multipage TIFF and animated GIF) the image file is left open so that
|
||||
``Image.Image.seek()`` can load the appropriate frame.
|
||||
|
||||
* ``Image.Image.close()`` Closes the file pointer and destroys the
|
||||
core image object. This is used in the Pillow context manager
|
||||
support. e.g.::
|
||||
* ``Image.Image.close()`` Closes the file and destroys the core image object.
|
||||
This is used in the Pillow context manager support. e.g.::
|
||||
|
||||
with Image.open('test.jpg') as img:
|
||||
... # image operations here.
|
||||
|
@ -84,17 +83,9 @@ data until the caller has explicitly closed the image.
|
|||
Complications
|
||||
-------------
|
||||
|
||||
* TiffImagePlugin has some code to pass the underlying file descriptor
|
||||
into libtiff (if working on an actual file). Since libtiff closes
|
||||
the file descriptor internally, it is duplicated prior to passing it
|
||||
into libtiff.
|
||||
|
||||
* ``decoder.handles_eof`` This slightly misnamed flag indicates that
|
||||
the decoder wants to be called with a 0 length buffer when reads are
|
||||
done. Despite the comments in ``ImageFile.load()``, the only decoder
|
||||
that actually uses this flag is the Jpeg2K decoder. The use of this
|
||||
flag in Jpeg2K predated the change to the decoder that added the
|
||||
pulls_fd flag, and is therefore not used.
|
||||
* ``TiffImagePlugin`` has some code to pass the underlying file descriptor into
|
||||
libtiff (if working on an actual file). Since libtiff closes the file
|
||||
descriptor internally, it is duplicated prior to passing it into libtiff.
|
||||
|
||||
* I don't think that there's any way to make this safe without
|
||||
changing the lazy loading::
|
||||
|
|
|
@ -99,30 +99,114 @@ version.
|
|||
|
||||
Use ``PIL.__version__`` instead.
|
||||
|
||||
ImageCms.CmsProfile attributes
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Some attributes in ``ImageCms.CmsProfile`` have been deprecated since Pillow 3.2.0. From
|
||||
6.0.0, they issue a ``DeprecationWarning``:
|
||||
|
||||
======================== ===============================
|
||||
Deprecated Use instead
|
||||
======================== ===============================
|
||||
``color_space`` Padded ``xcolor_space``
|
||||
``pcs`` Padded ``connection_space``
|
||||
``product_copyright`` Unicode ``copyright``
|
||||
``product_desc`` Unicode ``profile_description``
|
||||
``product_description`` Unicode ``profile_description``
|
||||
``product_manufacturer`` Unicode ``manufacturer``
|
||||
``product_model`` Unicode ``model``
|
||||
======================== ===============================
|
||||
|
||||
MIME type improvements
|
||||
^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
Previously, all JPEG2000 images had the MIME type "image/jpx". This has now been
|
||||
corrected. After the file format drivers have been loaded, ``Image.MIME["JPEG2000"]``
|
||||
will return "image/jp2". ``ImageFile.get_format_mimetype`` will return "image/jpx" if
|
||||
a JPX profile is present, or "image/jp2" otherwise.
|
||||
|
||||
Previously, all SGI images had the MIME type "image/rgb". This has now been
|
||||
corrected. After the file format drivers have been loaded, ``Image.MIME["SGI"]``
|
||||
will return "image/sgi". ``ImageFile.get_format_mimetype`` will return "image/rgb" if
|
||||
RGB image data is present, or "image/sgi" otherwise.
|
||||
|
||||
MIME types have been added to the PPM format. After the file format drivers have been
|
||||
loaded, ``Image.MIME["PPM"]`` will now return the generic "image/x-portable-anymap".
|
||||
``ImageFile.get_format_mimetype`` will return a MIME type specific to the color type.
|
||||
|
||||
The TGA, PCX and ICO formats also now have MIME types: "image/x-tga", "image/x-pcx" and
|
||||
"image/x-icon" respectively.
|
||||
|
||||
API Additions
|
||||
=============
|
||||
|
||||
DIB File Format
|
||||
DIB file format
|
||||
^^^^^^^^^^^^^^^
|
||||
|
||||
Pillow now supports reading and writing the DIB "Device Independent Bitmap" file format.
|
||||
Pillow now supports reading and writing the Device Independent Bitmap file format.
|
||||
|
||||
Image.quantize
|
||||
^^^^^^^^^^^^^^
|
||||
|
||||
The `dither` option is now a customisable parameter (was previously hardcoded to `1`). This parameter takes the same values used in `Image.convert`
|
||||
The ``dither`` option is now a customisable parameter (was previously hardcoded to ``1``).
|
||||
This parameter takes the same values used in :py:meth:`~PIL.Image.Image.convert`.
|
||||
|
||||
PNG EXIF Data
|
||||
New language parameter
|
||||
^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
These text-rendering functions now accept a ``language`` parameter to request
|
||||
language-specific glyphs and ligatures from the font:
|
||||
|
||||
* ``ImageDraw.ImageDraw.multiline_text()``
|
||||
* ``ImageDraw.ImageDraw.multiline_textsize()``
|
||||
* ``ImageDraw.ImageDraw.text()``
|
||||
* ``ImageDraw.ImageDraw.textsize()``
|
||||
* ``ImageFont.ImageFont.getmask()``
|
||||
* ``ImageFont.ImageFont.getsize_multiline()``
|
||||
* ``ImageFont.ImageFont.getsize()``
|
||||
|
||||
Added EXIF class
|
||||
^^^^^^^^^^^^^^^^
|
||||
|
||||
:py:meth:`~PIL.Image.Image.getexif` has been added, which returns an
|
||||
:py:class:`~PIL.Image.Exif` instance. Values can be retrieved and set like a
|
||||
dictionary. When saving JPEG, PNG or WEBP, the instance can be passed as an
|
||||
``exif`` argument to include any changes in the output image.
|
||||
|
||||
Added ImageOps.exif_transpose
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
:py:meth:`~PIL.ImageOps.exif_transpose` returns a copy of an image, transposed
|
||||
according to its EXIF Orientation tag.
|
||||
|
||||
PNG EXIF data
|
||||
^^^^^^^^^^^^^
|
||||
|
||||
EXIF data can now be read from and saved to PNG images. However, unlike other image
|
||||
formats, EXIF data is not guaranteed to have been read until
|
||||
:py:meth:`~PIL.Image.Image.load` has been called.
|
||||
formats, EXIF data is not guaranteed to be present in :py:attr:`~PIL.Image.Image.info`
|
||||
until :py:meth:`~PIL.Image.Image.load` has been called.
|
||||
|
||||
Other Changes
|
||||
=============
|
||||
|
||||
Reading new DDS image format
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
Pillow can now read uncompressed RGB data from DDS images.
|
||||
|
||||
Reading TIFF with old-style JPEG compression
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
Added support reading TIFF files with old-style JPEG compression through LibTIFF. All
|
||||
YCbCr TIFF images are now always read as RGB.
|
||||
|
||||
TIFF compression codecs
|
||||
^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
Support has been added for the LZMA, Zstd and WebP TIFF compression codecs.
|
||||
|
||||
Improved support for transposing I;16 images
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
I;16, I;16L and I;16B are now supported image modes for all
|
||||
:py:meth:`~PIL.Image.Image.transpose` operations.
|
||||
|
|
13
setup.py
|
@ -765,12 +765,7 @@ try:
|
|||
url='http://python-pillow.org',
|
||||
classifiers=[
|
||||
"Development Status :: 6 - Mature",
|
||||
"Topic :: Multimedia :: Graphics",
|
||||
"Topic :: Multimedia :: Graphics :: Capture :: Digital Camera",
|
||||
"Topic :: Multimedia :: Graphics :: Capture :: Screen Capture",
|
||||
"Topic :: Multimedia :: Graphics :: Graphics Conversion",
|
||||
"Topic :: Multimedia :: Graphics :: Viewers",
|
||||
"License :: Other/Proprietary License",
|
||||
"License :: OSI Approved :: Historical Permission Notice and Disclaimer (HPND)", # noqa: E501
|
||||
"Programming Language :: Python :: 2",
|
||||
"Programming Language :: Python :: 2.7",
|
||||
"Programming Language :: Python :: 3",
|
||||
|
@ -779,6 +774,11 @@ try:
|
|||
"Programming Language :: Python :: 3.7",
|
||||
"Programming Language :: Python :: Implementation :: CPython",
|
||||
"Programming Language :: Python :: Implementation :: PyPy",
|
||||
"Topic :: Multimedia :: Graphics",
|
||||
"Topic :: Multimedia :: Graphics :: Capture :: Digital Camera",
|
||||
"Topic :: Multimedia :: Graphics :: Capture :: Screen Capture",
|
||||
"Topic :: Multimedia :: Graphics :: Graphics Conversion",
|
||||
"Topic :: Multimedia :: Graphics :: Viewers",
|
||||
],
|
||||
python_requires=">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*",
|
||||
cmdclass={"build_ext": pil_build_ext},
|
||||
|
@ -789,7 +789,6 @@ try:
|
|||
packages=["PIL"],
|
||||
package_dir={'': 'src'},
|
||||
keywords=["Imaging", ],
|
||||
license='Standard PIL License',
|
||||
zip_safe=not (debug_build() or PLATFORM_MINGW), )
|
||||
except RequiredDependencyException as err:
|
||||
msg = """
|
||||
|
|
|
@ -27,7 +27,6 @@
|
|||
from . import Image, ImageFile, ImagePalette
|
||||
from ._binary import i8, i16le as i16, i32le as i32, \
|
||||
o8, o16le as o16, o32le as o32
|
||||
import math
|
||||
|
||||
# __version__ is deprecated and will be removed in a future version. Use
|
||||
# PIL.__version__ instead.
|
||||
|
@ -105,53 +104,51 @@ class BmpImageFile(ImageFile.ImageFile):
|
|||
# --------------------------------------------- Windows Bitmap v2 to v5
|
||||
# v3, OS/2 v2, v4, v5
|
||||
elif file_info['header_size'] in (40, 64, 108, 124):
|
||||
if file_info['header_size'] >= 40: # v3 and OS/2
|
||||
file_info['y_flip'] = i8(header_data[7]) == 0xff
|
||||
file_info['direction'] = 1 if file_info['y_flip'] else -1
|
||||
file_info['width'] = i32(header_data[0:4])
|
||||
file_info['height'] = (i32(header_data[4:8])
|
||||
if not file_info['y_flip']
|
||||
else 2**32 - i32(header_data[4:8]))
|
||||
file_info['planes'] = i16(header_data[8:10])
|
||||
file_info['bits'] = i16(header_data[10:12])
|
||||
file_info['compression'] = i32(header_data[12:16])
|
||||
# byte size of pixel data
|
||||
file_info['data_size'] = i32(header_data[16:20])
|
||||
file_info['pixels_per_meter'] = (i32(header_data[20:24]),
|
||||
i32(header_data[24:28]))
|
||||
file_info['colors'] = i32(header_data[28:32])
|
||||
file_info['palette_padding'] = 4
|
||||
self.info["dpi"] = tuple(
|
||||
map(lambda x: int(math.ceil(x / 39.3701)),
|
||||
file_info['pixels_per_meter']))
|
||||
if file_info['compression'] == self.BITFIELDS:
|
||||
if len(header_data) >= 52:
|
||||
for idx, mask in enumerate(['r_mask',
|
||||
'g_mask',
|
||||
'b_mask',
|
||||
'a_mask']):
|
||||
file_info[mask] = i32(
|
||||
header_data[36 + idx * 4:40 + idx * 4]
|
||||
)
|
||||
else:
|
||||
# 40 byte headers only have the three components in the
|
||||
# bitfields masks, ref:
|
||||
# https://msdn.microsoft.com/en-us/library/windows/desktop/dd183376(v=vs.85).aspx
|
||||
# See also
|
||||
# https://github.com/python-pillow/Pillow/issues/1293
|
||||
# There is a 4th component in the RGBQuad, in the alpha
|
||||
# location, but it is listed as a reserved component,
|
||||
# and it is not generally an alpha channel
|
||||
file_info['a_mask'] = 0x0
|
||||
for mask in ['r_mask', 'g_mask', 'b_mask']:
|
||||
file_info[mask] = i32(read(4))
|
||||
file_info['rgb_mask'] = (file_info['r_mask'],
|
||||
file_info['g_mask'],
|
||||
file_info['b_mask'])
|
||||
file_info['rgba_mask'] = (file_info['r_mask'],
|
||||
file_info['g_mask'],
|
||||
file_info['b_mask'],
|
||||
file_info['a_mask'])
|
||||
file_info['y_flip'] = i8(header_data[7]) == 0xff
|
||||
file_info['direction'] = 1 if file_info['y_flip'] else -1
|
||||
file_info['width'] = i32(header_data[0:4])
|
||||
file_info['height'] = (i32(header_data[4:8])
|
||||
if not file_info['y_flip']
|
||||
else 2**32 - i32(header_data[4:8]))
|
||||
file_info['planes'] = i16(header_data[8:10])
|
||||
file_info['bits'] = i16(header_data[10:12])
|
||||
file_info['compression'] = i32(header_data[12:16])
|
||||
# byte size of pixel data
|
||||
file_info['data_size'] = i32(header_data[16:20])
|
||||
file_info['pixels_per_meter'] = (i32(header_data[20:24]),
|
||||
i32(header_data[24:28]))
|
||||
file_info['colors'] = i32(header_data[28:32])
|
||||
file_info['palette_padding'] = 4
|
||||
self.info["dpi"] = tuple(
|
||||
int(x / 39.3701 + 0.5) for x in file_info['pixels_per_meter'])
|
||||
if file_info['compression'] == self.BITFIELDS:
|
||||
if len(header_data) >= 52:
|
||||
for idx, mask in enumerate(['r_mask',
|
||||
'g_mask',
|
||||
'b_mask',
|
||||
'a_mask']):
|
||||
file_info[mask] = i32(
|
||||
header_data[36 + idx * 4:40 + idx * 4]
|
||||
)
|
||||
else:
|
||||
# 40 byte headers only have the three components in the
|
||||
# bitfields masks, ref:
|
||||
# https://msdn.microsoft.com/en-us/library/windows/desktop/dd183376(v=vs.85).aspx
|
||||
# See also
|
||||
# https://github.com/python-pillow/Pillow/issues/1293
|
||||
# There is a 4th component in the RGBQuad, in the alpha
|
||||
# location, but it is listed as a reserved component,
|
||||
# and it is not generally an alpha channel
|
||||
file_info['a_mask'] = 0x0
|
||||
for mask in ['r_mask', 'g_mask', 'b_mask']:
|
||||
file_info[mask] = i32(read(4))
|
||||
file_info['rgb_mask'] = (file_info['r_mask'],
|
||||
file_info['g_mask'],
|
||||
file_info['b_mask'])
|
||||
file_info['rgba_mask'] = (file_info['r_mask'],
|
||||
file_info['g_mask'],
|
||||
file_info['b_mask'],
|
||||
file_info['a_mask'])
|
||||
else:
|
||||
raise IOError("Unsupported BMP header type (%d)" %
|
||||
file_info['header_size'])
|
||||
|
@ -180,6 +177,7 @@ class BmpImageFile(ImageFile.ImageFile):
|
|||
SUPPORTED = {
|
||||
32: [(0xff0000, 0xff00, 0xff, 0x0),
|
||||
(0xff0000, 0xff00, 0xff, 0xff000000),
|
||||
(0xff, 0xff00, 0xff0000, 0xff000000),
|
||||
(0x0, 0x0, 0x0, 0x0),
|
||||
(0xff000000, 0xff0000, 0xff00, 0x0)],
|
||||
24: [(0xff0000, 0xff00, 0xff)],
|
||||
|
@ -188,6 +186,7 @@ class BmpImageFile(ImageFile.ImageFile):
|
|||
MASK_MODES = {
|
||||
(32, (0xff0000, 0xff00, 0xff, 0x0)): "BGRX",
|
||||
(32, (0xff000000, 0xff0000, 0xff00, 0x0)): "XBGR",
|
||||
(32, (0xff, 0xff00, 0xff0000, 0xff000000)): "RGBA",
|
||||
(32, (0xff0000, 0xff00, 0xff, 0xff000000)): "BGRA",
|
||||
(32, (0x0, 0x0, 0x0, 0x0)): "BGRA",
|
||||
(24, (0xff0000, 0xff00, 0xff)): "BGR",
|
||||
|
@ -200,7 +199,7 @@ class BmpImageFile(ImageFile.ImageFile):
|
|||
raw_mode = MASK_MODES[
|
||||
(file_info["bits"], file_info["rgba_mask"])
|
||||
]
|
||||
self.mode = "RGBA" if raw_mode in ("BGRA",) else self.mode
|
||||
self.mode = "RGBA" if "A" in raw_mode else self.mode
|
||||
elif (file_info['bits'] in (24, 16) and
|
||||
file_info['rgb_mask'] in SUPPORTED[file_info['bits']]):
|
||||
raw_mode = MASK_MODES[
|
||||
|
@ -310,7 +309,7 @@ def _save(im, fp, filename, bitmap_header=True):
|
|||
dpi = info.get("dpi", (96, 96))
|
||||
|
||||
# 1 meter == 39.3701 inches
|
||||
ppm = tuple(map(lambda x: int(x * 39.3701), dpi))
|
||||
ppm = tuple(map(lambda x: int(x * 39.3701 + 0.5), dpi))
|
||||
|
||||
stride = ((im.size[0]*bits+7)//8+3) & (~3)
|
||||
header = 40 # or 64 for OS/2 version 2
|
||||
|
|
|
@ -141,13 +141,11 @@ def Ghostscript(tile, size, fp, scale=1):
|
|||
|
||||
# push data through Ghostscript
|
||||
try:
|
||||
with open(os.devnull, 'w+b') as devnull:
|
||||
startupinfo = None
|
||||
if sys.platform.startswith('win'):
|
||||
startupinfo = subprocess.STARTUPINFO()
|
||||
startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
|
||||
subprocess.check_call(command, stdout=devnull,
|
||||
startupinfo=startupinfo)
|
||||
startupinfo = None
|
||||
if sys.platform.startswith('win'):
|
||||
startupinfo = subprocess.STARTUPINFO()
|
||||
startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
|
||||
subprocess.check_call(command, startupinfo=startupinfo)
|
||||
im = Image.open(outfile)
|
||||
im.load()
|
||||
finally:
|
||||
|
|
|
@ -122,6 +122,8 @@ class GifImageFile(ImageFile.ImageFile):
|
|||
if not self._seek_check(frame):
|
||||
return
|
||||
if frame < self.__frame:
|
||||
if frame != 0:
|
||||
self.im = None
|
||||
self._seek(0)
|
||||
|
||||
last_frame = self.__frame
|
||||
|
|
|
@ -32,14 +32,12 @@ class GimpPaletteFile(object):
|
|||
if fp.readline()[:12] != b"GIMP Palette":
|
||||
raise SyntaxError("not a GIMP palette file")
|
||||
|
||||
i = 0
|
||||
|
||||
while i <= 255:
|
||||
for i in range(256):
|
||||
|
||||
s = fp.readline()
|
||||
|
||||
if not s:
|
||||
break
|
||||
|
||||
# skip fields and comment lines
|
||||
if re.match(br"\w+:|#", s):
|
||||
continue
|
||||
|
@ -50,10 +48,7 @@ class GimpPaletteFile(object):
|
|||
if len(v) != 3:
|
||||
raise ValueError("bad palette entry")
|
||||
|
||||
if 0 <= i <= 255:
|
||||
self.palette[i] = o8(v[0]) + o8(v[1]) + o8(v[2])
|
||||
|
||||
i += 1
|
||||
self.palette[i] = o8(v[0]) + o8(v[1]) + o8(v[2])
|
||||
|
||||
self.palette = b"".join(self.palette)
|
||||
|
||||
|
|
|
@ -295,3 +295,5 @@ class IcoImageFile(ImageFile.ImageFile):
|
|||
Image.register_open(IcoImageFile.format, IcoImageFile, _accept)
|
||||
Image.register_save(IcoImageFile.format, _save)
|
||||
Image.register_extension(IcoImageFile.format, ".ico")
|
||||
|
||||
Image.register_mime(IcoImageFile.format, "image/x-icon")
|
||||
|
|
253
src/PIL/Image.py
|
@ -40,8 +40,8 @@ except ImportError:
|
|||
import __builtin__
|
||||
builtins = __builtin__
|
||||
|
||||
from . import ImageMode
|
||||
from ._binary import i8
|
||||
from . import ImageMode, TiffTags
|
||||
from ._binary import i8, i32le
|
||||
from ._util import isPath, isStringType, deferred_error
|
||||
|
||||
import os
|
||||
|
@ -54,10 +54,10 @@ import atexit
|
|||
import numbers
|
||||
try:
|
||||
# Python 3
|
||||
from collections.abc import Callable
|
||||
from collections.abc import Callable, MutableMapping
|
||||
except ImportError:
|
||||
# Python 2.7
|
||||
from collections import Callable
|
||||
from collections import Callable, MutableMapping
|
||||
|
||||
|
||||
# Silence warning
|
||||
|
@ -247,7 +247,7 @@ _MODEINFO = {
|
|||
"L": ("L", "L", ("L",)),
|
||||
"I": ("L", "I", ("I",)),
|
||||
"F": ("L", "F", ("F",)),
|
||||
"P": ("RGB", "L", ("P",)),
|
||||
"P": ("P", "L", ("P",)),
|
||||
"RGB": ("RGB", "L", ("R", "G", "B")),
|
||||
"RGBX": ("RGB", "L", ("R", "G", "B", "X")),
|
||||
"RGBA": ("RGB", "L", ("R", "G", "B", "A")),
|
||||
|
@ -578,7 +578,12 @@ class Image(object):
|
|||
return self
|
||||
|
||||
def __exit__(self, *args):
|
||||
self.close()
|
||||
if hasattr(self, 'fp') and getattr(self, '_exclusive_fp', False):
|
||||
if hasattr(self, "_close__fp"):
|
||||
self._close__fp()
|
||||
if self.fp:
|
||||
self.fp.close()
|
||||
self.fp = None
|
||||
|
||||
def close(self):
|
||||
"""
|
||||
|
@ -610,12 +615,7 @@ class Image(object):
|
|||
|
||||
if sys.version_info.major >= 3:
|
||||
def __del__(self):
|
||||
if hasattr(self, "_close__fp"):
|
||||
self._close__fp()
|
||||
if (hasattr(self, 'fp') and hasattr(self, '_exclusive_fp')
|
||||
and self.fp and self._exclusive_fp):
|
||||
self.fp.close()
|
||||
self.fp = None
|
||||
self.__exit__()
|
||||
|
||||
def _copy(self):
|
||||
self.load()
|
||||
|
@ -950,7 +950,7 @@ class Image(object):
|
|||
delete_trns = False
|
||||
# transparency handling
|
||||
if has_transparency:
|
||||
if self.mode in ('L', 'RGB') and mode == 'RGBA':
|
||||
if self.mode in ('1', 'L', 'I', 'RGB') and mode == 'RGBA':
|
||||
# Use transparent conversion to promote from transparent
|
||||
# color to an alpha channel.
|
||||
new_im = self._new(self.im.convert_transparent(
|
||||
|
@ -1096,7 +1096,13 @@ class Image(object):
|
|||
im = self.im.convert("P", dither, palette.im)
|
||||
return self._new(im)
|
||||
|
||||
return self._new(self.im.quantize(colors, method, kmeans))
|
||||
im = self._new(self.im.quantize(colors, method, kmeans))
|
||||
|
||||
from . import ImagePalette
|
||||
mode = im.im.getpalettemode()
|
||||
im.palette = ImagePalette.ImagePalette(mode, im.im.getpalette(mode, mode))
|
||||
|
||||
return im
|
||||
|
||||
def copy(self):
|
||||
"""
|
||||
|
@ -1291,6 +1297,12 @@ class Image(object):
|
|||
return tuple(extrema)
|
||||
return self.im.getextrema()
|
||||
|
||||
def getexif(self):
|
||||
exif = Exif()
|
||||
if "exif" in self.info:
|
||||
exif.load(self.info["exif"])
|
||||
return exif
|
||||
|
||||
def getim(self):
|
||||
"""
|
||||
Returns a capsule that points to the internal image memory.
|
||||
|
@ -1559,7 +1571,7 @@ class Image(object):
|
|||
|
||||
self._ensure_mutable()
|
||||
|
||||
if self.mode not in ("LA", "RGBA"):
|
||||
if self.mode not in ("LA", "PA", "RGBA"):
|
||||
# attempt to promote self to a matching alpha mode
|
||||
try:
|
||||
mode = getmodebase(self.mode) + "A"
|
||||
|
@ -1568,7 +1580,7 @@ class Image(object):
|
|||
except (AttributeError, ValueError):
|
||||
# do things the hard way
|
||||
im = self.im.convert(mode)
|
||||
if im.mode not in ("LA", "RGBA"):
|
||||
if im.mode not in ("LA", "PA", "RGBA"):
|
||||
raise ValueError # sanity check
|
||||
self.im = im
|
||||
self.pyaccess = None
|
||||
|
@ -1576,7 +1588,7 @@ class Image(object):
|
|||
except (KeyError, ValueError):
|
||||
raise ValueError("illegal image mode")
|
||||
|
||||
if self.mode == "LA":
|
||||
if self.mode in ("LA", "PA"):
|
||||
band = 1
|
||||
else:
|
||||
band = 3
|
||||
|
@ -1619,10 +1631,10 @@ class Image(object):
|
|||
|
||||
def putpalette(self, data, rawmode="RGB"):
|
||||
"""
|
||||
Attaches a palette to this image. The image must be a "P" or
|
||||
"L" image, and the palette sequence must contain 768 integer
|
||||
values, where each group of three values represent the red,
|
||||
green, and blue values for the corresponding pixel
|
||||
Attaches a palette to this image. The image must be a "P",
|
||||
"PA", "L" or "LA" image, and the palette sequence must contain
|
||||
768 integer values, where each group of three values represent
|
||||
the red, green, and blue values for the corresponding pixel
|
||||
index. Instead of an integer sequence, you can use an 8-bit
|
||||
string.
|
||||
|
||||
|
@ -1631,7 +1643,7 @@ class Image(object):
|
|||
"""
|
||||
from . import ImagePalette
|
||||
|
||||
if self.mode not in ("L", "P"):
|
||||
if self.mode not in ("L", "LA", "P", "PA"):
|
||||
raise ValueError("illegal image mode")
|
||||
self.load()
|
||||
if isinstance(data, ImagePalette.ImagePalette):
|
||||
|
@ -1643,7 +1655,7 @@ class Image(object):
|
|||
else:
|
||||
data = "".join(chr(x) for x in data)
|
||||
palette = ImagePalette.raw(rawmode, data)
|
||||
self.mode = "P"
|
||||
self.mode = "PA" if "A" in self.mode else "P"
|
||||
self.palette = palette
|
||||
self.palette.mode = "RGB"
|
||||
self.load() # install new palette
|
||||
|
@ -1688,7 +1700,7 @@ class Image(object):
|
|||
Rewrites the image to reorder the palette.
|
||||
|
||||
:param dest_map: A list of indexes into the original palette.
|
||||
e.g. [1,0] would swap a two item palette, and list(range(255))
|
||||
e.g. [1,0] would swap a two item palette, and list(range(256))
|
||||
is the identity transform.
|
||||
:param source_palette: Bytes or None.
|
||||
:returns: An :py:class:`~PIL.Image.Image` object.
|
||||
|
@ -1958,7 +1970,7 @@ class Image(object):
|
|||
filename = fp.name
|
||||
|
||||
# may mutate self!
|
||||
self.load()
|
||||
self._ensure_mutable()
|
||||
|
||||
save_all = params.pop('save_all', False)
|
||||
self.encoderinfo = params
|
||||
|
@ -2005,9 +2017,6 @@ class Image(object):
|
|||
**EOFError** exception. When a sequence file is opened, the
|
||||
library automatically seeks to frame 0.
|
||||
|
||||
Note that in the current version of the library, most sequence
|
||||
formats only allow you to seek to the next frame.
|
||||
|
||||
See :py:meth:`~PIL.Image.Image.tell`.
|
||||
|
||||
:param frame: Frame number, starting at 0.
|
||||
|
@ -2374,7 +2383,14 @@ def new(mode, size, color=0):
|
|||
from . import ImageColor
|
||||
color = ImageColor.getcolor(color, mode)
|
||||
|
||||
return Image()._new(core.fill(mode, size, color))
|
||||
im = Image()
|
||||
if mode == "P" and \
|
||||
isinstance(color, (list, tuple)) and len(color) in [3, 4]:
|
||||
# RGB or RGBA value for a P image
|
||||
from . import ImagePalette
|
||||
im.palette = ImagePalette.ImagePalette()
|
||||
color = im.palette.getcolor(color)
|
||||
return im._new(core.fill(mode, size, color))
|
||||
|
||||
|
||||
def frombytes(mode, size, data, decoder_name="raw", *args):
|
||||
|
@ -2995,3 +3011,182 @@ def _apply_env_variables(env=None):
|
|||
|
||||
_apply_env_variables()
|
||||
atexit.register(core.clear_cache)
|
||||
|
||||
|
||||
class Exif(MutableMapping):
|
||||
endian = "<"
|
||||
|
||||
def __init__(self):
|
||||
self._data = {}
|
||||
self._ifds = {}
|
||||
|
||||
def _fixup_dict(self, src_dict):
|
||||
# Helper function for _getexif()
|
||||
# returns a dict with any single item tuples/lists as individual values
|
||||
def _fixup(value):
|
||||
try:
|
||||
if len(value) == 1 and not isinstance(value, dict):
|
||||
return value[0]
|
||||
except Exception:
|
||||
pass
|
||||
return value
|
||||
|
||||
return {k: _fixup(v) for k, v in src_dict.items()}
|
||||
|
||||
def _get_ifd_dict(self, tag):
|
||||
try:
|
||||
# an offset pointer to the location of the nested embedded IFD.
|
||||
# It should be a long, but may be corrupted.
|
||||
self.fp.seek(self._data[tag])
|
||||
except (KeyError, TypeError):
|
||||
pass
|
||||
else:
|
||||
from . import TiffImagePlugin
|
||||
info = TiffImagePlugin.ImageFileDirectory_v1(self.head)
|
||||
info.load(self.fp)
|
||||
return self._fixup_dict(info)
|
||||
|
||||
def load(self, data):
|
||||
# Extract EXIF information. This is highly experimental,
|
||||
# and is likely to be replaced with something better in a future
|
||||
# version.
|
||||
|
||||
# The EXIF record consists of a TIFF file embedded in a JPEG
|
||||
# application marker (!).
|
||||
self.fp = io.BytesIO(data[6:])
|
||||
self.head = self.fp.read(8)
|
||||
# process dictionary
|
||||
from . import TiffImagePlugin
|
||||
info = TiffImagePlugin.ImageFileDirectory_v1(self.head)
|
||||
self.endian = info._endian
|
||||
self.fp.seek(info.next)
|
||||
info.load(self.fp)
|
||||
self._data = dict(self._fixup_dict(info))
|
||||
|
||||
# get EXIF extension
|
||||
ifd = self._get_ifd_dict(0x8769)
|
||||
if ifd:
|
||||
self._data.update(ifd)
|
||||
self._ifds[0x8769] = ifd
|
||||
|
||||
# get gpsinfo extension
|
||||
ifd = self._get_ifd_dict(0x8825)
|
||||
if ifd:
|
||||
self._data[0x8825] = ifd
|
||||
self._ifds[0x8825] = ifd
|
||||
|
||||
def tobytes(self, offset=0):
|
||||
from . import TiffImagePlugin
|
||||
if self.endian == "<":
|
||||
head = b"II\x2A\x00\x08\x00\x00\x00"
|
||||
else:
|
||||
head = b"MM\x00\x2A\x00\x00\x00\x08"
|
||||
ifd = TiffImagePlugin.ImageFileDirectory_v2(ifh=head)
|
||||
for tag, value in self._data.items():
|
||||
ifd[tag] = value
|
||||
return b"Exif\x00\x00"+head+ifd.tobytes(offset)
|
||||
|
||||
def get_ifd(self, tag):
|
||||
if tag not in self._ifds and tag in self._data:
|
||||
if tag == 0xa005: # interop
|
||||
self._ifds[tag] = self._get_ifd_dict(tag)
|
||||
elif tag == 0x927c: # makernote
|
||||
from . import TiffImagePlugin
|
||||
if self._data[0x927c][:8] == b"FUJIFILM":
|
||||
exif_data = self._data[0x927c]
|
||||
ifd_offset = i32le(exif_data[8:12])
|
||||
ifd_data = exif_data[ifd_offset:]
|
||||
|
||||
makernote = {}
|
||||
for i in range(0, struct.unpack("<H", ifd_data[:2])[0]):
|
||||
ifd_tag, typ, count, data = struct.unpack(
|
||||
"<HHL4s", ifd_data[i*12 + 2:(i+1)*12 + 2])
|
||||
try:
|
||||
unit_size, handler =\
|
||||
TiffImagePlugin.ImageFileDirectory_v2._load_dispatch[
|
||||
typ
|
||||
]
|
||||
except KeyError:
|
||||
continue
|
||||
size = count * unit_size
|
||||
if size > 4:
|
||||
offset, = struct.unpack("<L", data)
|
||||
data = ifd_data[offset-12:offset+size-12]
|
||||
else:
|
||||
data = data[:size]
|
||||
|
||||
if len(data) != size:
|
||||
warnings.warn("Possibly corrupt EXIF MakerNote data. "
|
||||
"Expecting to read %d bytes but only got %d."
|
||||
" Skipping tag %s"
|
||||
% (size, len(data), ifd_tag))
|
||||
continue
|
||||
|
||||
if not data:
|
||||
continue
|
||||
|
||||
makernote[ifd_tag] = handler(
|
||||
TiffImagePlugin.ImageFileDirectory_v2(), data, False)
|
||||
self._ifds[0x927c] = dict(self._fixup_dict(makernote))
|
||||
elif self._data.get(0x010f) == "Nintendo":
|
||||
ifd_data = self._data[0x927c]
|
||||
|
||||
makernote = {}
|
||||
for i in range(0, struct.unpack(">H", ifd_data[:2])[0]):
|
||||
ifd_tag, typ, count, data = struct.unpack(
|
||||
">HHL4s", ifd_data[i*12 + 2:(i+1)*12 + 2])
|
||||
if ifd_tag == 0x1101:
|
||||
# CameraInfo
|
||||
offset, = struct.unpack(">L", data)
|
||||
self.fp.seek(offset)
|
||||
|
||||
camerainfo = {'ModelID': self.fp.read(4)}
|
||||
|
||||
self.fp.read(4)
|
||||
# Seconds since 2000
|
||||
camerainfo['TimeStamp'] = i32le(self.fp.read(12))
|
||||
|
||||
self.fp.read(4)
|
||||
camerainfo['InternalSerialNumber'] = self.fp.read(4)
|
||||
|
||||
self.fp.read(12)
|
||||
parallax = self.fp.read(4)
|
||||
handler =\
|
||||
TiffImagePlugin.ImageFileDirectory_v2._load_dispatch[
|
||||
TiffTags.FLOAT
|
||||
][1]
|
||||
camerainfo['Parallax'] = handler(
|
||||
TiffImagePlugin.ImageFileDirectory_v2(),
|
||||
parallax, False)
|
||||
|
||||
self.fp.read(4)
|
||||
camerainfo['Category'] = self.fp.read(2)
|
||||
|
||||
makernote = {0x1101: dict(self._fixup_dict(camerainfo))}
|
||||
self._ifds[0x927c] = makernote
|
||||
return self._ifds.get(tag, {})
|
||||
|
||||
def __str__(self):
|
||||
return str(self._data)
|
||||
|
||||
def __len__(self):
|
||||
return len(self._data)
|
||||
|
||||
def __getitem__(self, tag):
|
||||
return self._data[tag]
|
||||
|
||||
def __contains__(self, tag):
|
||||
return tag in self._data
|
||||
|
||||
if not py3:
|
||||
def has_key(self, tag):
|
||||
return tag in self
|
||||
|
||||
def __setitem__(self, tag, value):
|
||||
self._data[tag] = value
|
||||
|
||||
def __delitem__(self, tag):
|
||||
del self._data[tag]
|
||||
|
||||
def __iter__(self):
|
||||
return iter(set(self._data))
|
||||
|
|
|
@ -686,11 +686,11 @@ def getProfileName(profile):
|
|||
# // name was "%s - %s" (model, manufacturer) || Description ,
|
||||
# // but if the Model and Manufacturer were the same or the model
|
||||
# // was long, Just the model, in 1.x
|
||||
model = profile.profile.product_model
|
||||
manufacturer = profile.profile.product_manufacturer
|
||||
model = profile.profile.model
|
||||
manufacturer = profile.profile.manufacturer
|
||||
|
||||
if not (model or manufacturer):
|
||||
return profile.profile.product_description + "\n"
|
||||
return (profile.profile.profile_description or "") + "\n"
|
||||
if not manufacturer or len(model) > 30:
|
||||
return model + "\n"
|
||||
return "%s - %s\n" % (model, manufacturer)
|
||||
|
@ -727,8 +727,8 @@ def getProfileInfo(profile):
|
|||
# Python, not C. the white point bits weren't working well,
|
||||
# so skipping.
|
||||
# info was description \r\n\r\n copyright \r\n\r\n K007 tag \r\n\r\n whitepoint
|
||||
description = profile.profile.product_description
|
||||
cpright = profile.profile.product_copyright
|
||||
description = profile.profile.profile_description
|
||||
cpright = profile.profile.copyright
|
||||
arr = []
|
||||
for elt in (description, cpright):
|
||||
if elt:
|
||||
|
@ -762,7 +762,7 @@ def getProfileCopyright(profile):
|
|||
# add an extra newline to preserve pyCMS compatibility
|
||||
if not isinstance(profile, ImageCmsProfile):
|
||||
profile = ImageCmsProfile(profile)
|
||||
return profile.profile.product_copyright + "\n"
|
||||
return (profile.profile.copyright or "") + "\n"
|
||||
except (AttributeError, IOError, TypeError, ValueError) as v:
|
||||
raise PyCMSError(v)
|
||||
|
||||
|
@ -790,7 +790,7 @@ def getProfileManufacturer(profile):
|
|||
# add an extra newline to preserve pyCMS compatibility
|
||||
if not isinstance(profile, ImageCmsProfile):
|
||||
profile = ImageCmsProfile(profile)
|
||||
return profile.profile.product_manufacturer + "\n"
|
||||
return (profile.profile.manufacturer or "") + "\n"
|
||||
except (AttributeError, IOError, TypeError, ValueError) as v:
|
||||
raise PyCMSError(v)
|
||||
|
||||
|
@ -819,7 +819,7 @@ def getProfileModel(profile):
|
|||
# add an extra newline to preserve pyCMS compatibility
|
||||
if not isinstance(profile, ImageCmsProfile):
|
||||
profile = ImageCmsProfile(profile)
|
||||
return profile.profile.product_model + "\n"
|
||||
return (profile.profile.model or "") + "\n"
|
||||
except (AttributeError, IOError, TypeError, ValueError) as v:
|
||||
raise PyCMSError(v)
|
||||
|
||||
|
@ -848,7 +848,7 @@ def getProfileDescription(profile):
|
|||
# add an extra newline to preserve pyCMS compatibility
|
||||
if not isinstance(profile, ImageCmsProfile):
|
||||
profile = ImageCmsProfile(profile)
|
||||
return profile.profile.product_description + "\n"
|
||||
return (profile.profile.profile_description or "") + "\n"
|
||||
except (AttributeError, IOError, TypeError, ValueError) as v:
|
||||
raise PyCMSError(v)
|
||||
|
||||
|
|
|
@ -282,13 +282,17 @@ class ImageDraw(object):
|
|||
self.draw.draw_bitmap(xy, mask, ink)
|
||||
|
||||
def multiline_text(self, xy, text, fill=None, font=None, anchor=None,
|
||||
spacing=4, align="left", direction=None, features=None):
|
||||
spacing=4, align="left", direction=None, features=None,
|
||||
language=None):
|
||||
widths = []
|
||||
max_width = 0
|
||||
lines = self._multiline_split(text)
|
||||
line_spacing = self.textsize('A', font=font)[1] + spacing
|
||||
for line in lines:
|
||||
line_width, line_height = self.textsize(line, font)
|
||||
line_width, line_height = self.textsize(line, font,
|
||||
direction=direction,
|
||||
features=features,
|
||||
language=language)
|
||||
widths.append(line_width)
|
||||
max_width = max(max_width, line_width)
|
||||
left, top = xy
|
||||
|
@ -302,29 +306,30 @@ class ImageDraw(object):
|
|||
else:
|
||||
raise ValueError('align must be "left", "center" or "right"')
|
||||
self.text((left, top), line, fill, font, anchor,
|
||||
direction=direction, features=features)
|
||||
direction=direction, features=features, language=language)
|
||||
top += line_spacing
|
||||
left = xy[0]
|
||||
|
||||
def textsize(self, text, font=None, spacing=4, direction=None,
|
||||
features=None):
|
||||
features=None, language=None):
|
||||
"""Get the size of a given string, in pixels."""
|
||||
if self._multiline_check(text):
|
||||
return self.multiline_textsize(text, font, spacing,
|
||||
direction, features)
|
||||
direction, features, language)
|
||||
|
||||
if font is None:
|
||||
font = self.getfont()
|
||||
return font.getsize(text, direction, features)
|
||||
return font.getsize(text, direction, features, language)
|
||||
|
||||
def multiline_textsize(self, text, font=None, spacing=4, direction=None,
|
||||
features=None):
|
||||
features=None, language=None):
|
||||
max_width = 0
|
||||
lines = self._multiline_split(text)
|
||||
line_spacing = self.textsize('A', font=font)[1] + spacing
|
||||
for line in lines:
|
||||
line_width, line_height = self.textsize(line, font, spacing,
|
||||
direction, features)
|
||||
direction, features,
|
||||
language)
|
||||
max_width = max(max_width, line_width)
|
||||
return max_width, len(lines)*line_spacing - spacing
|
||||
|
||||
|
|
|
@ -596,8 +596,6 @@ class PyDecoder(object):
|
|||
Override to perform the decoding process.
|
||||
|
||||
:param buffer: A bytes object with the data to be decoded.
|
||||
If `handles_eof` is set, then `buffer` will be empty and `self.fd`
|
||||
will be set.
|
||||
:returns: A tuple of (bytes consumed, errcode).
|
||||
If finished with decoding return <0 for the bytes consumed.
|
||||
Err codes are from `ERRORS`
|
||||
|
|
|
@ -158,17 +158,17 @@ class FreeTypeFont(object):
|
|||
def getmetrics(self):
|
||||
return self.font.ascent, self.font.descent
|
||||
|
||||
def getsize(self, text, direction=None, features=None):
|
||||
size, offset = self.font.getsize(text, direction, features)
|
||||
def getsize(self, text, direction=None, features=None, language=None):
|
||||
size, offset = self.font.getsize(text, direction, features, language)
|
||||
return (size[0] + offset[0], size[1] + offset[1])
|
||||
|
||||
def getsize_multiline(self, text, direction=None,
|
||||
spacing=4, features=None):
|
||||
def getsize_multiline(self, text, direction=None, spacing=4,
|
||||
features=None, language=None):
|
||||
max_width = 0
|
||||
lines = self._multiline_split(text)
|
||||
line_spacing = self.getsize('A')[1] + spacing
|
||||
for line in lines:
|
||||
line_width, line_height = self.getsize(line, direction, features)
|
||||
line_width, line_height = self.getsize(line, direction, features, language)
|
||||
max_width = max(max_width, line_width)
|
||||
|
||||
return max_width, len(lines)*line_spacing - spacing
|
||||
|
@ -176,15 +176,15 @@ class FreeTypeFont(object):
|
|||
def getoffset(self, text):
|
||||
return self.font.getsize(text)[1]
|
||||
|
||||
def getmask(self, text, mode="", direction=None, features=None):
|
||||
return self.getmask2(text, mode, direction=direction,
|
||||
features=features)[0]
|
||||
def getmask(self, text, mode="", direction=None, features=None, language=None):
|
||||
return self.getmask2(text, mode, direction=direction, features=features,
|
||||
language=language)[0]
|
||||
|
||||
def getmask2(self, text, mode="", fill=Image.core.fill, direction=None,
|
||||
features=None, *args, **kwargs):
|
||||
size, offset = self.font.getsize(text, direction, features)
|
||||
features=None, language=None, *args, **kwargs):
|
||||
size, offset = self.font.getsize(text, direction, features, language)
|
||||
im = fill("L", size, 0)
|
||||
self.font.render(text, im.id, mode == "1", direction, features)
|
||||
self.font.render(text, im.id, mode == "1", direction, features, language)
|
||||
return im, offset
|
||||
|
||||
def font_variant(self, font=None, size=None, index=None, encoding=None,
|
||||
|
|
|
@ -522,3 +522,30 @@ def solarize(image, threshold=128):
|
|||
else:
|
||||
lut.append(255-i)
|
||||
return _lut(image, lut)
|
||||
|
||||
|
||||
def exif_transpose(image):
|
||||
"""
|
||||
If an image has an EXIF Orientation tag, return a new image that is
|
||||
transposed accordingly. Otherwise, return a copy of the image.
|
||||
|
||||
:param image: The image to transpose.
|
||||
:return: An image.
|
||||
"""
|
||||
exif = image.getexif()
|
||||
orientation = exif.get(0x0112)
|
||||
method = {
|
||||
2: Image.FLIP_LEFT_RIGHT,
|
||||
3: Image.ROTATE_180,
|
||||
4: Image.FLIP_TOP_BOTTOM,
|
||||
5: Image.TRANSPOSE,
|
||||
6: Image.ROTATE_270,
|
||||
7: Image.TRANSVERSE,
|
||||
8: Image.ROTATE_90
|
||||
}.get(orientation)
|
||||
if method is not None:
|
||||
transposed_image = image.transpose(method)
|
||||
del exif[0x0112]
|
||||
transposed_image.info["exif"] = exif.tobytes()
|
||||
return transposed_image
|
||||
return image.copy()
|
||||
|
|
|
@ -198,35 +198,9 @@ def getiptcinfo(im):
|
|||
|
||||
elif isinstance(im, JpegImagePlugin.JpegImageFile):
|
||||
# extract the IPTC/NAA resource
|
||||
try:
|
||||
app = im.app["APP13"]
|
||||
if app[:14] == b"Photoshop 3.0\x00":
|
||||
app = app[14:]
|
||||
# parse the image resource block
|
||||
offset = 0
|
||||
while app[offset:offset+4] == b"8BIM":
|
||||
offset += 4
|
||||
# resource code
|
||||
code = i16(app, offset)
|
||||
offset += 2
|
||||
# resource name (usually empty)
|
||||
name_len = i8(app[offset])
|
||||
# name = app[offset+1:offset+1+name_len]
|
||||
offset = 1 + offset + name_len
|
||||
if offset & 1:
|
||||
offset += 1
|
||||
# resource data block
|
||||
size = i32(app, offset)
|
||||
offset += 4
|
||||
if code == 0x0404:
|
||||
# 0x0404 contains IPTC/NAA data
|
||||
data = app[offset:offset+size]
|
||||
break
|
||||
offset = offset + size
|
||||
if offset & 1:
|
||||
offset += 1
|
||||
except (AttributeError, KeyError):
|
||||
pass
|
||||
photoshop = im.info.get("photoshop")
|
||||
if photoshop:
|
||||
data = photoshop.get(0x0404)
|
||||
|
||||
elif isinstance(im, TiffImagePlugin.TiffImageFile):
|
||||
# get raw data from the IPTC/NAA tag (PhotoShop tags the data
|
||||
|
|
|
@ -39,7 +39,7 @@ import struct
|
|||
import io
|
||||
import warnings
|
||||
from . import Image, ImageFile, TiffImagePlugin
|
||||
from ._binary import i8, o8, i16be as i16
|
||||
from ._binary import i8, o8, i16be as i16, i32be as i32
|
||||
from .JpegPresets import presets
|
||||
from ._util import isStringType
|
||||
|
||||
|
@ -86,7 +86,7 @@ def APP(self, marker):
|
|||
self.info["jfif_density"] = jfif_density
|
||||
elif marker == 0xFFE1 and s[:5] == b"Exif\0":
|
||||
if "exif" not in self.info:
|
||||
# extract Exif information (incomplete)
|
||||
# extract EXIF information (incomplete)
|
||||
self.info["exif"] = s # FIXME: value will change
|
||||
elif marker == 0xFFE2 and s[:5] == b"FPXR\0":
|
||||
# extract FlashPix information (incomplete)
|
||||
|
@ -104,6 +104,39 @@ def APP(self, marker):
|
|||
# reassemble the profile, rather than assuming that the APP2
|
||||
# markers appear in the correct sequence.
|
||||
self.icclist.append(s)
|
||||
elif marker == 0xFFED:
|
||||
if s[:14] == b"Photoshop 3.0\x00":
|
||||
blocks = s[14:]
|
||||
# parse the image resource block
|
||||
offset = 0
|
||||
photoshop = {}
|
||||
while blocks[offset:offset+4] == b"8BIM":
|
||||
offset += 4
|
||||
# resource code
|
||||
code = i16(blocks, offset)
|
||||
offset += 2
|
||||
# resource name (usually empty)
|
||||
name_len = i8(blocks[offset])
|
||||
# name = blocks[offset+1:offset+1+name_len]
|
||||
offset = 1 + offset + name_len
|
||||
if offset & 1:
|
||||
offset += 1
|
||||
# resource data block
|
||||
size = i32(blocks, offset)
|
||||
offset += 4
|
||||
data = blocks[offset:offset+size]
|
||||
if code == 0x03ED: # ResolutionInfo
|
||||
data = {
|
||||
'XResolution': i32(data[:4]) / 65536,
|
||||
'DisplayedUnitsX': i16(data[4:8]),
|
||||
'YResolution': i32(data[8:12]) / 65536,
|
||||
'DisplayedUnitsY': i16(data[12:]),
|
||||
}
|
||||
photoshop[code] = data
|
||||
offset = offset + size
|
||||
if offset & 1:
|
||||
offset += 1
|
||||
self.info["photoshop"] = photoshop
|
||||
elif marker == 0xFFEE and s[:5] == b"Adobe":
|
||||
self.info["adobe"] = i16(s, 5)
|
||||
# extract Adobe custom properties
|
||||
|
@ -127,15 +160,15 @@ def APP(self, marker):
|
|||
resolution_unit = exif[0x0128]
|
||||
x_resolution = exif[0x011A]
|
||||
try:
|
||||
dpi = x_resolution[0] / x_resolution[1]
|
||||
dpi = float(x_resolution[0]) / x_resolution[1]
|
||||
except TypeError:
|
||||
dpi = x_resolution
|
||||
if resolution_unit == 3: # cm
|
||||
# 1 dpcm = 2.54 dpi
|
||||
dpi *= 2.54
|
||||
self.info["dpi"] = dpi, dpi
|
||||
self.info["dpi"] = int(dpi + 0.5), int(dpi + 0.5)
|
||||
except (KeyError, SyntaxError, ZeroDivisionError):
|
||||
# SyntaxError for invalid/unreadable exif
|
||||
# SyntaxError for invalid/unreadable EXIF
|
||||
# KeyError for dpi not included
|
||||
# ZeroDivisionError for invalid dpi rational value
|
||||
self.info["dpi"] = 72, 72
|
||||
|
@ -439,60 +472,23 @@ class JpegImageFile(ImageFile.ImageFile):
|
|||
def _fixup_dict(src_dict):
|
||||
# Helper function for _getexif()
|
||||
# returns a dict with any single item tuples/lists as individual values
|
||||
def _fixup(value):
|
||||
try:
|
||||
if len(value) == 1 and not isinstance(value, dict):
|
||||
return value[0]
|
||||
except Exception:
|
||||
pass
|
||||
return value
|
||||
|
||||
return {k: _fixup(v) for k, v in src_dict.items()}
|
||||
exif = Image.Exif()
|
||||
return exif._fixup_dict(src_dict)
|
||||
|
||||
|
||||
def _getexif(self):
|
||||
# Extract EXIF information. This method is highly experimental,
|
||||
# and is likely to be replaced with something better in a future
|
||||
# version.
|
||||
|
||||
# The EXIF record consists of a TIFF file embedded in a JPEG
|
||||
# application marker (!).
|
||||
# Use the cached version if possible
|
||||
try:
|
||||
data = self.info["exif"]
|
||||
return self.info["parsed_exif"]
|
||||
except KeyError:
|
||||
return None
|
||||
fp = io.BytesIO(data[6:])
|
||||
head = fp.read(8)
|
||||
# process dictionary
|
||||
info = TiffImagePlugin.ImageFileDirectory_v1(head)
|
||||
fp.seek(info.next)
|
||||
info.load(fp)
|
||||
exif = dict(_fixup_dict(info))
|
||||
# get exif extension
|
||||
try:
|
||||
# exif field 0x8769 is an offset pointer to the location
|
||||
# of the nested embedded exif ifd.
|
||||
# It should be a long, but may be corrupted.
|
||||
fp.seek(exif[0x8769])
|
||||
except (KeyError, TypeError):
|
||||
pass
|
||||
else:
|
||||
info = TiffImagePlugin.ImageFileDirectory_v1(head)
|
||||
info.load(fp)
|
||||
exif.update(_fixup_dict(info))
|
||||
# get gpsinfo extension
|
||||
try:
|
||||
# exif field 0x8825 is an offset pointer to the location
|
||||
# of the nested embedded gps exif ifd.
|
||||
# It should be a long, but may be corrupted.
|
||||
fp.seek(exif[0x8825])
|
||||
except (KeyError, TypeError):
|
||||
pass
|
||||
else:
|
||||
info = TiffImagePlugin.ImageFileDirectory_v1(head)
|
||||
info.load(fp)
|
||||
exif[0x8825] = _fixup_dict(info)
|
||||
|
||||
if "exif" not in self.info:
|
||||
return None
|
||||
exif = dict(self.getexif())
|
||||
|
||||
# Cache the result for future use
|
||||
self.info["parsed_exif"] = exif
|
||||
return exif
|
||||
|
||||
|
||||
|
@ -728,6 +724,10 @@ def _save(im, fp, filename):
|
|||
|
||||
optimize = info.get("optimize", False)
|
||||
|
||||
exif = info.get("exif", b"")
|
||||
if isinstance(exif, Image.Exif):
|
||||
exif = exif.tobytes()
|
||||
|
||||
# get keyword arguments
|
||||
im.encoderconfig = (
|
||||
quality,
|
||||
|
@ -739,7 +739,7 @@ def _save(im, fp, filename):
|
|||
subsampling,
|
||||
qtables,
|
||||
extra,
|
||||
info.get("exif", b"")
|
||||
exif
|
||||
)
|
||||
|
||||
# if we optimize, libjpeg needs a buffer big enough to hold the whole image
|
||||
|
@ -757,9 +757,9 @@ def _save(im, fp, filename):
|
|||
else:
|
||||
bufsize = im.size[0] * im.size[1]
|
||||
|
||||
# The exif info needs to be written as one block, + APP1, + one spare byte.
|
||||
# The EXIF info needs to be written as one block, + APP1, + one spare byte.
|
||||
# Ensure that our buffer is big enough. Same with the icc_profile block.
|
||||
bufsize = max(ImageFile.MAXBLOCK, bufsize, len(info.get("exif", b"")) + 5,
|
||||
bufsize = max(ImageFile.MAXBLOCK, bufsize, len(exif) + 5,
|
||||
len(extra) + 1)
|
||||
|
||||
ImageFile._save(im, fp, [("jpeg", (0, 0)+im.size, 0, rawmode)], bufsize)
|
||||
|
@ -786,7 +786,8 @@ def jpeg_factory(fp=None, filename=None):
|
|||
if mpheader[45057] > 1:
|
||||
# It's actually an MPO
|
||||
from .MpoImagePlugin import MpoImageFile
|
||||
im = MpoImageFile(fp, filename)
|
||||
# Don't reload everything, just convert it.
|
||||
im = MpoImageFile.adopt(im, mpheader)
|
||||
except (TypeError, IndexError):
|
||||
# It is really a JPEG
|
||||
pass
|
||||
|
|
|
@ -18,7 +18,8 @@
|
|||
# See the README file for information on usage and redistribution.
|
||||
#
|
||||
|
||||
from . import Image, JpegImagePlugin
|
||||
from . import Image, ImageFile, JpegImagePlugin
|
||||
from ._binary import i16be as i16
|
||||
|
||||
# __version__ is deprecated and will be removed in a future version. Use
|
||||
# PIL.__version__ instead.
|
||||
|
@ -46,7 +47,10 @@ class MpoImageFile(JpegImagePlugin.JpegImageFile):
|
|||
def _open(self):
|
||||
self.fp.seek(0) # prep the fp in order to pass the JPEG test
|
||||
JpegImagePlugin.JpegImageFile._open(self)
|
||||
self.mpinfo = self._getmp()
|
||||
self._after_jpeg_open()
|
||||
|
||||
def _after_jpeg_open(self, mpheader=None):
|
||||
self.mpinfo = mpheader if mpheader is not None else self._getmp()
|
||||
self.__framecount = self.mpinfo[0xB001]
|
||||
self.__mpoffsets = [mpent['DataOffset'] + self.info['mpoffset']
|
||||
for mpent in self.mpinfo[0xB002]]
|
||||
|
@ -78,6 +82,20 @@ class MpoImageFile(JpegImagePlugin.JpegImageFile):
|
|||
return
|
||||
self.fp = self.__fp
|
||||
self.offset = self.__mpoffsets[frame]
|
||||
|
||||
self.fp.seek(self.offset + 2) # skip SOI marker
|
||||
if "parsed_exif" in self.info:
|
||||
del self.info["parsed_exif"]
|
||||
if i16(self.fp.read(2)) == 0xFFE1: # APP1
|
||||
n = i16(self.fp.read(2))-2
|
||||
self.info["exif"] = ImageFile._safe_read(self.fp, n)
|
||||
|
||||
exif = self._getexif()
|
||||
if 40962 in exif and 40963 in exif:
|
||||
self._size = (exif[40962], exif[40963])
|
||||
elif "exif" in self.info:
|
||||
del self.info["exif"]
|
||||
|
||||
self.tile = [
|
||||
("jpeg", (0, 0) + self.size, self.offset, (self.mode, ""))
|
||||
]
|
||||
|
@ -95,6 +113,22 @@ class MpoImageFile(JpegImagePlugin.JpegImageFile):
|
|||
finally:
|
||||
self.__fp = None
|
||||
|
||||
@staticmethod
|
||||
def adopt(jpeg_instance, mpheader=None):
|
||||
"""
|
||||
Transform the instance of JpegImageFile into
|
||||
an instance of MpoImageFile.
|
||||
After the call, the JpegImageFile is extended
|
||||
to be an MpoImageFile.
|
||||
|
||||
This is essentially useful when opening a JPEG
|
||||
file that reveals itself as an MPO, to avoid
|
||||
double call to _open.
|
||||
"""
|
||||
jpeg_instance.__class__ = MpoImageFile
|
||||
jpeg_instance._after_jpeg_open(mpheader)
|
||||
return jpeg_instance
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------
|
||||
# Registry stuff
|
||||
|
|
|
@ -180,3 +180,5 @@ Image.register_open(PcxImageFile.format, PcxImageFile, _accept)
|
|||
Image.register_save(PcxImageFile.format, _save)
|
||||
|
||||
Image.register_extension(PcxImageFile.format, ".pcx")
|
||||
|
||||
Image.register_mime(PcxImageFile.format, "image/x-pcx")
|
||||
|
|
|
@ -54,19 +54,24 @@ _MAGIC = b"\211PNG\r\n\032\n"
|
|||
|
||||
_MODES = {
|
||||
# supported bits/color combinations, and corresponding modes/rawmodes
|
||||
# Greyscale
|
||||
(1, 0): ("1", "1"),
|
||||
(2, 0): ("L", "L;2"),
|
||||
(4, 0): ("L", "L;4"),
|
||||
(8, 0): ("L", "L"),
|
||||
(16, 0): ("I", "I;16B"),
|
||||
# Truecolour
|
||||
(8, 2): ("RGB", "RGB"),
|
||||
(16, 2): ("RGB", "RGB;16B"),
|
||||
# Indexed-colour
|
||||
(1, 3): ("P", "P;1"),
|
||||
(2, 3): ("P", "P;2"),
|
||||
(4, 3): ("P", "P;4"),
|
||||
(8, 3): ("P", "P"),
|
||||
# Greyscale with alpha
|
||||
(8, 4): ("LA", "LA"),
|
||||
(16, 4): ("RGBA", "LA;16B"), # LA;16B->LA not yet available
|
||||
# Truecolour with alpha
|
||||
(8, 6): ("RGBA", "RGBA"),
|
||||
(16, 6): ("RGBA", "RGBA;16B"),
|
||||
}
|
||||
|
@ -386,7 +391,7 @@ class PngStream(ChunkStream):
|
|||
# otherwise, we have a byte string with one alpha value
|
||||
# for each palette entry
|
||||
self.im_info["transparency"] = s
|
||||
elif self.im_mode == "L":
|
||||
elif self.im_mode in ("1", "L", "I"):
|
||||
self.im_info["transparency"] = i16(s)
|
||||
elif self.im_mode == "RGB":
|
||||
self.im_info["transparency"] = i16(s), i16(s[2:]), i16(s[4:])
|
||||
|
@ -691,8 +696,14 @@ class PngImageFile(ImageFile.ImageFile):
|
|||
def _getexif(self):
|
||||
if "exif" not in self.info:
|
||||
self.load()
|
||||
from .JpegImagePlugin import _getexif
|
||||
return _getexif(self)
|
||||
if "exif" not in self.info:
|
||||
return None
|
||||
return dict(self.getexif())
|
||||
|
||||
def getexif(self):
|
||||
if "exif" not in self.info:
|
||||
self.load()
|
||||
return ImageFile.ImageFile.getexif(self)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------
|
||||
|
@ -707,6 +718,7 @@ _OUTMODES = {
|
|||
"L": ("L", b'\x08\x00'),
|
||||
"LA": ("LA", b'\x08\x04'),
|
||||
"I": ("I;16B", b'\x10\x00'),
|
||||
"I;16": ("I;16B", b'\x10\x00'),
|
||||
"P;1": ("P;1", b'\x01\x03'),
|
||||
"P;2": ("P;2", b'\x02\x03'),
|
||||
"P;4": ("P;4", b'\x04\x03'),
|
||||
|
@ -840,7 +852,7 @@ def _save(im, fp, filename, chunk=putchunk):
|
|||
transparency = max(0, min(255, transparency))
|
||||
alpha = b'\xFF' * transparency + b'\0'
|
||||
chunk(fp, b"tRNS", alpha[:alpha_bytes])
|
||||
elif im.mode == "L":
|
||||
elif im.mode in ("1", "L", "I"):
|
||||
transparency = max(0, min(65535, transparency))
|
||||
chunk(fp, b"tRNS", o16(transparency))
|
||||
elif im.mode == "RGB":
|
||||
|
@ -874,6 +886,8 @@ def _save(im, fp, filename, chunk=putchunk):
|
|||
|
||||
exif = im.encoderinfo.get("exif", im.info.get("exif"))
|
||||
if exif:
|
||||
if isinstance(exif, Image.Exif):
|
||||
exif = exif.tobytes(8)
|
||||
if exif.startswith(b"Exif\x00\x00"):
|
||||
exif = exif[6:]
|
||||
chunk(fp, b"eXIf", exif)
|
||||
|
|
|
@ -164,6 +164,6 @@ def _save(im, fp, filename):
|
|||
Image.register_open(PpmImageFile.format, PpmImageFile, _accept)
|
||||
Image.register_save(PpmImageFile.format, _save)
|
||||
|
||||
Image.register_extensions(PpmImageFile.format, [".pbm", ".pgm", ".ppm"])
|
||||
Image.register_extensions(PpmImageFile.format, [".pbm", ".pgm", ".ppm", ".pnm"])
|
||||
|
||||
Image.register_mime(PpmImageFile.format, "image/x-portable-anymap")
|
||||
|
|
|
@ -208,7 +208,7 @@ class SpiderImageFile(ImageFile.ImageFile):
|
|||
|
||||
# given a list of filenames, return a list of images
|
||||
def loadImageSeries(filelist=None):
|
||||
"""create a list of Image.images for use in montage"""
|
||||
"""create a list of :py:class:`~PIL.Image.Image` objects for use in a montage"""
|
||||
if filelist is None or len(filelist) < 1:
|
||||
return
|
||||
|
||||
|
|
|
@ -226,4 +226,6 @@ def _save(im, fp, filename):
|
|||
Image.register_open(TgaImageFile.format, TgaImageFile)
|
||||
Image.register_save(TgaImageFile.format, _save)
|
||||
|
||||
Image.register_extension(TgaImageFile.format, ".tga")
|
||||
Image.register_extensions(TgaImageFile.format, [".tga", ".icb", ".vda", ".vst"])
|
||||
|
||||
Image.register_mime(TgaImageFile.format, "image/x-tga")
|
||||
|
|
|
@ -263,10 +263,10 @@ OPEN_INFO = {
|
|||
(II, 5, (1,), 1, (8, 8, 8, 8, 8, 8), (0, 0)): ("CMYK", "CMYKXX"),
|
||||
(MM, 5, (1,), 1, (8, 8, 8, 8, 8, 8), (0, 0)): ("CMYK", "CMYKXX"),
|
||||
|
||||
# JPEG compressed images handled by LibTiff and auto-converted to RGB
|
||||
# JPEG compressed images handled by LibTiff and auto-converted to RGBX
|
||||
# Minimal Baseline TIFF requires YCbCr images to have 3 SamplesPerPixel
|
||||
(II, 6, (1,), 1, (8, 8, 8), ()): ("RGB", "RGB"),
|
||||
(MM, 6, (1,), 1, (8, 8, 8), ()): ("RGB", "RGB"),
|
||||
(II, 6, (1,), 1, (8, 8, 8), ()): ("RGB", "RGBX"),
|
||||
(MM, 6, (1,), 1, (8, 8, 8), ()): ("RGB", "RGBX"),
|
||||
|
||||
(II, 8, (1,), 1, (8, 8, 8), ()): ("LAB", "LAB"),
|
||||
(MM, 8, (1,), 1, (8, 8, 8), ()): ("LAB", "LAB"),
|
||||
|
@ -785,17 +785,12 @@ class ImageFileDirectory_v2(MutableMapping):
|
|||
warnings.warn(str(msg))
|
||||
return
|
||||
|
||||
def save(self, fp):
|
||||
|
||||
if fp.tell() == 0: # skip TIFF header on subsequent pages
|
||||
# tiff header -- PIL always starts the first IFD at offset 8
|
||||
fp.write(self._prefix + self._pack("HL", 42, 8))
|
||||
|
||||
def tobytes(self, offset=0):
|
||||
# FIXME What about tagdata?
|
||||
fp.write(self._pack("H", len(self._tags_v2)))
|
||||
result = self._pack("H", len(self._tags_v2))
|
||||
|
||||
entries = []
|
||||
offset = fp.tell() + len(self._tags_v2) * 12 + 4
|
||||
offset = offset + len(result) + len(self._tags_v2) * 12 + 4
|
||||
stripoffsets = None
|
||||
|
||||
# pass 1: convert tags to binary format
|
||||
|
@ -844,18 +839,29 @@ class ImageFileDirectory_v2(MutableMapping):
|
|||
for tag, typ, count, value, data in entries:
|
||||
if DEBUG > 1:
|
||||
print(tag, typ, count, repr(value), repr(data))
|
||||
fp.write(self._pack("HHL4s", tag, typ, count, value))
|
||||
result += self._pack("HHL4s", tag, typ, count, value)
|
||||
|
||||
# -- overwrite here for multi-page --
|
||||
fp.write(b"\0\0\0\0") # end of entries
|
||||
result += b"\0\0\0\0" # end of entries
|
||||
|
||||
# pass 3: write auxiliary data to file
|
||||
for tag, typ, count, value, data in entries:
|
||||
fp.write(data)
|
||||
result += data
|
||||
if len(data) & 1:
|
||||
fp.write(b"\0")
|
||||
result += b"\0"
|
||||
|
||||
return offset
|
||||
return result
|
||||
|
||||
def save(self, fp):
|
||||
|
||||
if fp.tell() == 0: # skip TIFF header on subsequent pages
|
||||
# tiff header -- PIL always starts the first IFD at offset 8
|
||||
fp.write(self._prefix + self._pack("HL", 42, 8))
|
||||
|
||||
offset = fp.tell()
|
||||
result = self.tobytes(offset)
|
||||
fp.write(result)
|
||||
return offset + len(result)
|
||||
|
||||
|
||||
ImageFileDirectory_v2._load_dispatch = _load_dispatch
|
||||
|
@ -985,7 +991,6 @@ class TiffImageFile(ImageFile.ImageFile):
|
|||
self.__fp = self.fp
|
||||
self._frame_pos = []
|
||||
self._n_frames = None
|
||||
self._is_animated = None
|
||||
|
||||
if DEBUG:
|
||||
print("*** TiffImageFile._open ***")
|
||||
|
@ -999,29 +1004,14 @@ class TiffImageFile(ImageFile.ImageFile):
|
|||
def n_frames(self):
|
||||
if self._n_frames is None:
|
||||
current = self.tell()
|
||||
try:
|
||||
while True:
|
||||
self._seek(self.tell() + 1)
|
||||
except EOFError:
|
||||
self._n_frames = self.tell() + 1
|
||||
self._seek(len(self._frame_pos))
|
||||
while self._n_frames is None:
|
||||
self._seek(self.tell() + 1)
|
||||
self.seek(current)
|
||||
return self._n_frames
|
||||
|
||||
@property
|
||||
def is_animated(self):
|
||||
if self._is_animated is None:
|
||||
if self._n_frames is not None:
|
||||
self._is_animated = self._n_frames != 1
|
||||
else:
|
||||
current = self.tell()
|
||||
|
||||
try:
|
||||
self.seek(1)
|
||||
self._is_animated = True
|
||||
except EOFError:
|
||||
self._is_animated = False
|
||||
|
||||
self.seek(current)
|
||||
return self._is_animated
|
||||
|
||||
def seek(self, frame):
|
||||
|
@ -1053,10 +1043,13 @@ class TiffImageFile(ImageFile.ImageFile):
|
|||
print("Loading tags, location: %s" % self.fp.tell())
|
||||
self.tag_v2.load(self.fp)
|
||||
self.__next = self.tag_v2.next
|
||||
if self.__next == 0:
|
||||
self._n_frames = frame + 1
|
||||
if len(self._frame_pos) == 1:
|
||||
self._is_animated = self.__next != 0
|
||||
self.__frame += 1
|
||||
self.fp.seek(self._frame_pos[frame])
|
||||
self.tag_v2.load(self.fp)
|
||||
self.__next = self.tag_v2.next
|
||||
# fill the legacy tag/ifd entries
|
||||
self.tag = self.ifd = ImageFileDirectory_v1.from_v2(self.tag_v2)
|
||||
self.__frame = frame
|
||||
|
@ -1087,7 +1080,7 @@ class TiffImageFile(ImageFile.ImageFile):
|
|||
def load_end(self):
|
||||
# allow closing if we're on the first frame, there's no next
|
||||
# This is the ImageFile.load path only, libtiff specific below.
|
||||
if self.__frame == 0 and not self.__next:
|
||||
if not self._is_animated:
|
||||
self._close_exclusive_fp_after_loading = True
|
||||
|
||||
def _load_libtiff(self):
|
||||
|
@ -1167,10 +1160,9 @@ class TiffImageFile(ImageFile.ImageFile):
|
|||
self.tile = []
|
||||
self.readonly = 0
|
||||
# libtiff closed the fp in a, we need to close self.fp, if possible
|
||||
if self._exclusive_fp:
|
||||
if self.__frame == 0 and not self.__next:
|
||||
self.fp.close()
|
||||
self.fp = None # might be shared
|
||||
if self._exclusive_fp and not self._is_animated:
|
||||
self.fp.close()
|
||||
self.fp = None # might be shared
|
||||
|
||||
if err < 0:
|
||||
raise IOError(err)
|
||||
|
@ -1191,6 +1183,10 @@ class TiffImageFile(ImageFile.ImageFile):
|
|||
# the specification
|
||||
photo = self.tag_v2.get(PHOTOMETRIC_INTERPRETATION, 0)
|
||||
|
||||
# old style jpeg compression images most certainly are YCbCr
|
||||
if self._compression == "tiff_jpeg":
|
||||
photo = 6
|
||||
|
||||
fillorder = self.tag_v2.get(FILLORDER, 1)
|
||||
|
||||
if DEBUG:
|
||||
|
@ -1257,11 +1253,11 @@ class TiffImageFile(ImageFile.ImageFile):
|
|||
if xres and yres:
|
||||
resunit = self.tag_v2.get(RESOLUTION_UNIT)
|
||||
if resunit == 2: # dots per inch
|
||||
self.info["dpi"] = xres, yres
|
||||
self.info["dpi"] = int(xres + 0.5), int(yres + 0.5)
|
||||
elif resunit == 3: # dots per centimeter. convert to dpi
|
||||
self.info["dpi"] = xres * 2.54, yres * 2.54
|
||||
self.info["dpi"] = int(xres * 2.54 + 0.5), int(yres * 2.54 + 0.5)
|
||||
elif resunit is None: # used to default to 1, but now 2)
|
||||
self.info["dpi"] = xres, yres
|
||||
self.info["dpi"] = int(xres + 0.5), int(yres + 0.5)
|
||||
# For backward compatibility,
|
||||
# we also preserve the old behavior
|
||||
self.info["resolution"] = xres, yres
|
||||
|
@ -1472,8 +1468,8 @@ def _save(im, fp, filename):
|
|||
dpi = im.encoderinfo.get("dpi")
|
||||
if dpi:
|
||||
ifd[RESOLUTION_UNIT] = 2
|
||||
ifd[X_RESOLUTION] = dpi[0]
|
||||
ifd[Y_RESOLUTION] = dpi[1]
|
||||
ifd[X_RESOLUTION] = int(dpi[0] + 0.5)
|
||||
ifd[Y_RESOLUTION] = int(dpi[1] + 0.5)
|
||||
|
||||
if bits != (1,):
|
||||
ifd[BITSPERSAMPLE] = bits
|
||||
|
|
|
@ -93,8 +93,9 @@ class WebPImageFile(ImageFile.ImageFile):
|
|||
self.seek(0)
|
||||
|
||||
def _getexif(self):
|
||||
from .JpegImagePlugin import _getexif
|
||||
return _getexif(self)
|
||||
if "exif" not in self.info:
|
||||
return None
|
||||
return dict(self.getexif())
|
||||
|
||||
@property
|
||||
def n_frames(self):
|
||||
|
@ -186,7 +187,7 @@ def _save_all(im, fp, filename):
|
|||
# will preserve non-alpha modes
|
||||
total = 0
|
||||
for ims in [im]+append_images:
|
||||
total += 1 if not hasattr(ims, "n_frames") else ims.n_frames
|
||||
total += getattr(ims, "n_frames", 1)
|
||||
if total == 1:
|
||||
_save(im, fp, filename)
|
||||
return
|
||||
|
@ -216,6 +217,8 @@ def _save_all(im, fp, filename):
|
|||
method = im.encoderinfo.get("method", 0)
|
||||
icc_profile = im.encoderinfo.get("icc_profile", "")
|
||||
exif = im.encoderinfo.get("exif", "")
|
||||
if isinstance(exif, Image.Exif):
|
||||
exif = exif.tobytes()
|
||||
xmp = im.encoderinfo.get("xmp", "")
|
||||
if allow_mixed:
|
||||
lossless = False
|
||||
|
@ -254,10 +257,7 @@ def _save_all(im, fp, filename):
|
|||
try:
|
||||
for ims in [im]+append_images:
|
||||
# Get # of frames in this image
|
||||
if not hasattr(ims, "n_frames"):
|
||||
nfr = 1
|
||||
else:
|
||||
nfr = ims.n_frames
|
||||
nfr = getattr(ims, "n_frames", 1)
|
||||
|
||||
for idx in range(nfr):
|
||||
ims.seek(idx)
|
||||
|
@ -318,6 +318,8 @@ def _save(im, fp, filename):
|
|||
quality = im.encoderinfo.get("quality", 80)
|
||||
icc_profile = im.encoderinfo.get("icc_profile", "")
|
||||
exif = im.encoderinfo.get("exif", "")
|
||||
if isinstance(exif, Image.Exif):
|
||||
exif = exif.tobytes()
|
||||
xmp = im.encoderinfo.get("xmp", "")
|
||||
|
||||
if im.mode not in _VALID_WEBP_LEGACY_MODES:
|
||||
|
|