diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 000000000..b1b12b379 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,24 @@ +# Top-most EditorConfig file +root = true + +[*] +# Unix-style newlines with a newline ending every file +end_of_line = lf +insert_final_newline = true +charset = utf-8 + +# Four-space indentation +indent_size = 4 +indent_style = space + +insert_final_newline = true +trim_trailing_whitespace = true + +[*.yml] +# Two-space indentation +indent_size = 2 +indent_style = space + +# Tab indentation (no size specified) +[Makefile] +indent_style = tab diff --git a/CONTRIBUTING.md b/.github/CONTRIBUTING.md similarity index 65% rename from CONTRIBUTING.md rename to .github/CONTRIBUTING.md index 5a49ab2d7..f2f474941 100644 --- a/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -1,6 +1,6 @@ # Contributing to Pillow -Bug fixes, feature additions, tests, documentation and more can be contributed via [issues](https://github.com/python-pillow/Pillow/issues) and/or [pull requests](https://github.com/python-pillow/Pillow/issues). All contributions are welcome. +Bug fixes, feature additions, tests, documentation and more can be contributed via [issues](https://github.com/python-pillow/Pillow/issues) and/or [pull requests](https://github.com/python-pillow/Pillow/pulls). All contributions are welcome. ## Bug fixes, feature additions, etc. @@ -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 both Python 2.x and 3.x. You can enable [Travis CI on your repo](https://travis-ci.org/profile/) 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 both Python 2.x 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. - Create a pull request to pull the changes from your branch to the Pillow master. ### Guidelines @@ -17,6 +17,7 @@ Please send a pull request to the master branch. Please include [documentation]( - Separate code commits from reformatting commits. - Provide tests for any newly added code. - Follow PEP8. +- When committing only documentation changes please include [ci skip] in the commit message to avoid running tests on Travis-CI and AppVeyor. ## Reporting Issues @@ -28,3 +29,9 @@ When reporting issues, please include code that reproduces the issue and wheneve - What did you expect to happen? - What actually happened? - What versions of Pillow and Python are you using? + +## Security vulnerabilities + +To report sensitive vulnerability information, email aclark@aclark.net. + +If your organisation/employer is a distributor of Pillow and would like advance notification of security-related bugs, please let us know your preferred contact method. diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md new file mode 100644 index 000000000..980b85404 --- /dev/null +++ b/.github/ISSUE_TEMPLATE.md @@ -0,0 +1,13 @@ +### What did you do? + +### What did you expect to happen? + +### What actually happened? + +### What versions of Pillow and Python are you using? + +Please include code that reproduces the issue and whenever possible, an image that demonstrates the issue. The best reproductions are self-contained scripts with minimal dependencies. + +```python +code goes here +``` \ No newline at end of file diff --git a/.landscape.yaml b/.landscape.yaml index c869da5b4..ddd9cef32 100644 --- a/.landscape.yaml +++ b/.landscape.yaml @@ -1,2 +1,3 @@ strictness: medium test-warnings: yes +max-line-length: 80 diff --git a/.travis.yml b/.travis.yml index a75c80873..b76f5dbdb 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,21 +9,34 @@ notifications: python: - "pypy" - "pypy3" - - 3.4 + - 3.5 - 2.7 - 2.6 - "2.7_with_system_site_packages" # For PyQt4 - 3.2 - 3.3 + - 3.4 + - nightly install: + - "travis_retry sudo apt-get update" - "travis_retry sudo apt-get -qq install libfreetype6-dev liblcms2-dev python-qt4 ghostscript libffi-dev libjpeg-turbo-progs cmake imagemagick" - "travis_retry pip install cffi" - - "travis_retry pip install coverage nose" - - "travis_retry pip install pyroma" + - "travis_retry pip install nose" + - "travis_retry pip install check-manifest" + - "travis_retry pip install Sphinx" + # Pyroma tests sometimes hang on PyPy and Python 2.6; skip for those + - if [ $TRAVIS_PYTHON_VERSION != "pypy" && $TRAVIS_PYTHON_VERSION != "2.6" ]; then travis_retry pip install pyroma; fi - if [ "$TRAVIS_PYTHON_VERSION" == "2.6" ]; then travis_retry pip install unittest2; fi + # Coverage 4.0 doesn't support Python 3.2 + - if [ "$TRAVIS_PYTHON_VERSION" == "3.2" ]; then travis_retry pip install coverage==3.7.1; fi + - if [ "$TRAVIS_PYTHON_VERSION" != "3.2" ]; then travis_retry pip install coverage; fi + + # clean checkout for manifest + - mkdir /tmp/check-manifest && cp -a . /tmp/check-manifest + # webp - pushd depends && ./install_webp.sh && popd @@ -31,12 +44,16 @@ install: - pushd depends && ./install_openjpeg.sh && popd script: - - coverage erase + - if [ "$TRAVIS_PYTHON_VERSION" != "nightly" ]; then coverage erase; fi - python setup.py clean - CFLAGS="-coverage" python setup.py build_ext --inplace - - coverage run --append --include=PIL/* selftest.py - - coverage run --append --include=PIL/* -m nose -vx Tests/test_*.py + - if [ "$TRAVIS_PYTHON_VERSION" != "nightly" ]; then coverage run --append --include=PIL/* selftest.py; fi + - if [ "$TRAVIS_PYTHON_VERSION" != "nightly" ]; then coverage run --append --include=PIL/* -m nose -vx Tests/test_*.py; fi + - pushd /tmp/check-manifest && check-manifest --ignore ".coveragerc,.editorconfig,*.yml,*.yaml,tox.ini" && popd + # Sphinx + - make install + - pushd docs; make html; make linkcheck; popd after_success: # gather the coverage data @@ -54,15 +71,15 @@ after_success: - travis_retry pip install pep8 pyflakes - pep8 --statistics --count PIL/*.py - - pep8 --statistics --count Tests/*.py + - pep8 --statistics --count Tests/*.py - pyflakes *.py | tee >(wc -l) - pyflakes PIL/*.py | tee >(wc -l) - pyflakes Tests/*.py | tee >(wc -l) # Coverage and quality reports on just the latest diff. # (Installation is very slow on Py3, so just do it for Py2.) - - if [ ${TRAVIS_PYTHON_VERSION:0:1} == "2" ]; then Scripts/diffcover-install.sh; fi - - if [ ${TRAVIS_PYTHON_VERSION:0:1} == "2" ]; then Scripts/diffcover-run.sh; fi + - if [ ${TRAVIS_PYTHON_VERSION:0:1} == "2" ]; then depends/diffcover-install.sh; fi + - if [ ${TRAVIS_PYTHON_VERSION:0:1} == "2" ]; then depends/diffcover-run.sh; fi # after_all - | @@ -104,6 +121,8 @@ after_script: matrix: fast_finish: true + allow_failures: + - python: nightly env: global: diff --git a/CHANGES.rst b/CHANGES.rst index 518a4244f..7dfef9e55 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,13 +1,480 @@ Changelog (Pillow) ================== -2.9.0 (Unreleased) +3.3.0 (unreleased) ------------------ +- Added Support for 2/4 bpp Tiff Grayscale Images #1789 + [zwhfly] + +- Removed unused variable from selftest #1788 + [radarhere] + +- Added warning for as_dict method (deprecated in 3.0.0) #1799 + [radarhere] + +- Removed powf support for older Python versions #1784 + [radarhere] + +- Health fixes #1625 + [radarhere] + +3.2.0 (2016-04-01) +------------------ + +- Added install docs for Fedora 23 and FreeBSD #1729, #1739, #1792 + [koobs, zandermartin, wiredfool] + +- Fixed TIFF multiframe load when the frames have different compression types #1782 + [radarhere, geka000] + +- Added __copy__ method to Image #1772 + [radarhere] + +- Updated dates in PIL license in OleFileIO README #1787 + [radarhere] + +- Corrected Tiff tag names #1786 + [radarhere] + +- Fixed documented name of JPEG property #1783 + [radarhere] + +- Fixed UnboundLocalErrorwhen loading a corrupt jpeg2k file #1780 + [wiredfool] + +- Fixed integer overflow in path.c #1773 + [wiredfool, nedwill] + +- Added debug to command line help text for pilprint #1766 + [radarhere] + +- Expose many more fields in ICC Profiles #1756 + [lambdafu] + +- Documentation changes, URL update, transpose, release checklist + [radarhere] + +- Fixed saving to nonexistant files specified by pathlib.Path objects, fixes #1747 + [radarhere] + +- Round Image.crop arguments to the nearest integer, fixes #1744 + [hugovk] + +- Fix uninitialized variable warning in _imaging.c:getink, fixes #486 + [wiredfool] + +- Disable multiprocessing install on cygwin, fixes #1690 + [wiredfool] + +- Fix the error reported when libz is not found #1764 + [wiredfool] + +- More general error check to avoid Symbol not found: _PyUnicodeUCS2_AsLatin1String on OS X #1761 + [wiredfool] + +- Added py35 to tox envlist #1724 + [radarhere] + +- Fix EXIF tag name typos #1736 + [zarlant, radarhere] + +- Updated freetype to 2.6.3, Tk/Tcl to 8.6.5 and 8.5.19 + [radarhere] + +- Add a loader for the FTEX format from Independence War 2: Edge of Chaos #1688 + [jleclanche] + +- Improved alpha_composite documentation #1698 + [radarhere] + +- Extend ImageDraw.text method to pass on multiline_text method specific arguments #1647 + [radarhere] + +- Allow ImageSequence to seek to zero #1686 + [radarhere] + +- ImageSequence Iterator is now an iterator #1649 + [radarhere] + +- Updated windows test builds to jpeg9b + [radarhere] + +- Fixed support for .gbr version 1 images, added support for version 2 in GbrImagePlugin #1653 + [wiredfool] + +- Clarified which YCbCr format is used #1677 + [radarhere] + +- Added TiffTags documentation, Moved windows build documentation to winbuild/ #1667 + [wiredfool] + +- Add tests for OLE file based formats #1678 + [radarhere] + +- Add TIFF IFD test #1671 + [radarhere] + +- Add a basic DDS image plugin with more tests #1654 + [jleclanche, hugovk, wiredfool] + +- Fix incorrect conditional in encode.c #1638 + [manisandro] + + +3.1.2 (2016-04-01) +------------------ + +- Fixed an integer overflow in Jpeg2KEncode.c causing a buffer overflow. CVE-2016-3076 + [wiredfool] + +3.1.1 (2016-02-04) +------------------ + +- Fixed an integer overflow in Resample.c causing writes in the Python heap. + [nedwill] + +- Fixed a buffer overflow in PcdDecode.c causing a segfault when opening PhotoCD files. CVE-2016-2533 + [wiredfool] + +- Fixed a buffer overflow in FliDecode.c causing a segfault when opening FLI files. CVE-2016-0775 + [wiredfool] + +- Fixed a buffer overflow in TiffDecode.c causing an arbitrary amount of memory to be overwritten when opening a specially crafted invalid TIFF file. CVE-2016-0740 + [wiredfool] + + +3.1.0 (2016-01-04) +------------------ + +- Fixing test failures on Python 2.6/Windows #1633 + [wiredfool] + +- Limit metadata tags when writing using libtiff #1620 + [wiredfool] + +- Rolling back exif support to pre-3.0 format #1627 + [wiredfool] + +- Fix Divide by zero in Exif, add IFDRational class #1531 + [wiredfool] + +- Catch the IFD error near the source #1622 + [wiredfool] + +- Added release notes for 3.1.0 #1623 + [radarhere] + +- Updated spacing to be consistent between multiline methods #1624 + [radarhere] + +- Let EditorConfig take care of some basic formatting #1489 + [hugovk] + +- Restore gpsexif data to the v1 form + [wiredfool] + +- Add /usr/local include and library directories for freebsd #1613 + [leforestier] + +- Updated installation docs for new versions of dependencies #1611 + [radarhere] + +- Removed unrunnable test file #1610 + [radarhere] + +- Changed register calls to use format property #1608 + [radarhere] + +- Added field type constants to TiffTags #1596 + [radarhere] + +- Allow saving RowsPerStrip with libtiff #1594 + [wiredfool] + +- Enabled conversion to numpy array for HSV images #1578 + [cartisan] + +- Changed some urls in the docs to use https #1580 + [hugovk] + +- Removed logger.exception from ImageFile.py #1590 + [radarhere] + +- Removed warnings module check #1587 + [radarhere] + +- Changed arcs, chords and pie slices to use floats #1577 + [radarhere] + +- Update unit test asserts #1584, #1598 + [radarhere] + +- Fix command to invoke ghostscript for eps files #1478 + [baumatron, radarhere] + +- Consistent multiline text spacing #1574 + [wiredfool, hugovk] + +- Removed unused lines in BDFFontFile #1530 + [radarhere] + +- Changed ImageQt import of Image #1560 + [radarhere, ericfrederich] + +- Throw TypeError if no cursors were found in .cur file #1556 + [radarhere] + +- Fix crash in ImageTk.PhotoImage on win-amd64 #1553 + [cgohlke] + +- ExtraSamples tag should be a SHORT, not a BYTE #1555 + [Nexuapex] + +- Docs and code health fixes #1565 #1566 #1581 #1586 #1591 #1621 + [radarhere] + +- Updated freetype to 2.6.2 #1564 + [radarhere] + +- Updated WebP to 0.5.0 for Travis #1515 #1609 + [radarhere] + +- Fix missing 'version' key value in __array_interface__ #1519 + [mattip] + +- Replaced os.popen with subprocess.Popen to pilprint script #1523 + [radarhere] + +- Catch OverflowError in SpiderImagePlugin #1545 + [radarhere, MrShark] + +- Fix the definition of icc_profile in TiffTags #1539 + [wiredfool] + +- Remove old _imagingtiff.c and pilplus stuff #1499 + [hugovk] + +- Fix Exception when requiring jpeg #1501 + [hansmosh] + +- Dependency scripts for Debian and Ubuntu #1486 + [wiredfool] + +- Added Usage message to painter script #1482 + [radarhere] + +- Add tag info for iccprofile, fixes #1462. #1465 + [wiredfool] + +- Added some requirements for make release-test #1451 + [wiredfool] + +- Flatten tiff metadata value SAMPLEFORMAT to initial value, fixes #1466 + [wiredfool] + +- Fix handling of pathlib in Image.save. Fixes #1460 + [wiredfool] + +- Make tests more robust #1469 + [hugovk] + +- Use correctly sized pointers for windows handle types. #1458 + [nu744] + +3.0.0 (2015-10-01) +------------------ + +- Check flush method existence for file-like object #1398 + [mrTable, radarhere] + +- Added PDF multipage saving #1445 + [radarhere] + +- Removed deprecated code, Image.tostring, Image.fromstring, Image.offset, ImageDraw.setink, ImageDraw.setfill, ImageFileIO, ImageFont.FreeTypeFont and ImageFont.truetype `file` kwarg, ImagePalette private _make functions, ImageWin.fromstring and ImageWin.tostring #1343 + [radarhere] + +- Load more broken images #1428 + [homm] + +- Require zlib and libjpeg #1439 + [wiredfool] + +- Preserve alpha when converting from a QImage to a Pillow Image by using png instead of ppm #1429 + [ericfrederich] + +- Qt needs 32 bit aligned image data #1430 + [ericfrederich] + +- Tiff ImageFileDirectory rewrite #1419 + [anntzer, wiredfool, homm] + +- Removed spammy debug logging #1423 + [wiredfool] + +- Save as GiF89a with support for animation parameters #1384 + [radarhere] + +- Correct convert matrix docs #1426 + [wiredfool] + +- Catch TypeError in _getexif #1414 + [radarhere, wiredfool] + +- Fix for UnicodeDecodeError in TiffImagePlugin #1416 + [bogdan199, wiredfool] + +- Dedup code in image.open #1415 + [wiredfool] + +- Skip any number extraneous chars at the end of JPEG chunks #1337 + [homm] + +- Single threaded build for pypy3, refactor #1413 + [wiredfool] + +- Fix loading of truncated images with LOAD_TRUNCATED_IMAGES enabled #1366 + [homm] + +- Documentation update for concepts: bands + [merriam] + +- Add Solaris/SmartOS include and library directories #1356 + [njones11] + +- Improved handling of getink color #1387 + [radarhere] + +- Disable compiler optimizations for topalette and tobilevel functions for all msvc versions, fixes #1357 + [cgohlke] + +- Skip ImageFont_bitmap test if _imagingft C module is not installed #1409 + [homm] + +- Add param documentation to ImagePalette #1381 + [bwrsandman] + +- Corrected scripts path #1407 + [radarhere] + +- Updated libtiff to 4.0.6 #1405, #1421 + [radarhere] + +- Updated Platform Support for Yosemite #1403 + [radarhere] + +- Fixed infinite loop on truncated file #1401 + [radarhere] + +- Check that images are L mode in ImageMorph methods #1400 + [radarhere] + +- In tutorial of pasting images, add to mask text #1389 + [merriam] + +- Style/health fixes #1391, #1397, #1417, #1418 + [radarhere] + +- Test on Python 3.5 dev and 3.6 nightly #1361 + [hugovk] + +- Fix fast rotate operations #1373 + [radarhere] + +- Added support for pathlib Path objects to open and save #1372 + [radarhere] + +- Changed register calls to use format property #1333 + [radarhere] + +- Added support for ImageGrab.grab to OS X #1367, #1443 + [radarhere, hugovk] + +- Fixed PSDraw stdout Python 3 compatibility #1365 + [radarhere] + +- Added Python 3.3 to AppVeyor #1363 + [radarhere] + +- Treat MPO with unknown header as base JPEG file #1350 + [hugovk, radarhere] + +- Added various tests #1330, #1344 + [radarhere] + +- More ImageFont tests #1327 + [hugovk] + +- Use logging instead of print #1207 + [anntzer] + +2.9.0 (2015-07-01) +------------------ + +- Added test for GimpPaletteFile #1324 + [radarhere] + +- Merged gifmaker script to allow saving of multi-frame GIF images #1320 + [radarhere] + +- Added is_animated property to multi-frame formats #1319 + [radarhere] + +- Fixed ValueError in Python 2.6 #1315 #1316 + [cgohlke, radarhere] + +- Fixed tox test script path #1308 + [radarhere] + +- Added width and height properties #1304 + [radarhere] + +- Update tiff and tk tcl 8.5 versions #1303 + [radarhere, wiredfool] + +- Add functions to convert: Image <-> QImage; Image <-> QPixmap #1217 + [radarhere, rominf] + +- Remove duplicate code in gifmaker script #1294 + [radarhere] + +- Multiline text in ImageDraw #1177 + [allo-, radarhere] + +- Automated Windows CI/build support #1278 + [wiredfool] + +- Removed support for Tk versions earlier than 8.4 #1288 + [radarhere] + +- Fixed polygon edge drawing #1255 (fixes #1252) + [radarhere] + +- Check prefix length in _accept methods #1267 + [radarhere] + +- Register MIME type for BMP #1277 + [coldmind] + +- Adjusted ImageQt use of unicode() for 2/3 compatibility #1218 + [radarhere] + +- Identify XBM file created with filename including underscore #1230 (fixes #1229) + [hugovk] + +- Copy image when saving in GifImagePlugin #1231 (fixes #718) + [radarhere] + +- Removed support for FreeType 2.0 #1247 + [radarhere] + +- Added background saving to GifImagePlugin #1273 + [radarhere] + - Provide n_frames attribute to multi-frame formats #1261 [anntzer, radarhere] -- Add duration and loop set to GifImagePlugin #1172 +- Add duration and loop set to GifImagePlugin #1172, #1269 [radarhere] - Ico files are little endian #1232 @@ -112,7 +579,7 @@ Changelog (Pillow) - Adjust buffer size when quality=keep, fixes #148 (again) [wiredfool] -- Fix for corrupted bitmaps embedded in truetype fonts. #1072 +- Fix for corrupted bitmaps embedded in truetype fonts. #1072 [jackyyf, wiredfool] 2.7.0 (2015-01-01) @@ -121,7 +588,7 @@ Changelog (Pillow) - Split Sane into a separate repo: https://github.com/python-pillow/Sane [hugovk] -- Look for OSX and Linux fonts in common places. #1054 +- Look for OS X and Linux fonts in common places. #1054 [charleslaw] - Fix CVE-2014-9601, potential PNG decompression DOS #1060 @@ -199,7 +666,7 @@ Changelog (Pillow) 2.6.2 (2015-01-01) ------------------ -- Fix CVE-2014-9601, potential PNG decompression DOS #1060 +- Fix CVE-2014-9601, potential PNG decompression DOS #1060 [wiredfool] - Fix Regression in PyPy 2.4 in streamio #958 @@ -729,7 +1196,7 @@ Changelog (Pillow) - Fix #328: _imagingcms.c: include windef.h to fix build issue on MSVC [nu774] -- Automatically discover homebrew include/ and lib/ paths on OSX +- Automatically discover homebrew include/ and lib/ paths on OS X [donspaulding] - Fix bytes which should be bytearray @@ -861,14 +1328,14 @@ Changelog (Pillow) - Use PyCapsule for py3.1, fixes #237. -- Workaround for: http://bugs.python.org/16754 in 3.2.x < 3.2.4 and 3.3.0. +- Workaround for: http://bugs.python.org/issue16754 in 3.2.x < 3.2.4 and 3.3.0. 2.0.0 (2013-03-15) ------------------ .. Note:: Special thanks to Christoph Gohlke and Eric Soroos for assisting with a pre-PyCon 2013 release! -- Many other bug fixes and enhancements by many other people. +- Many other bug fixes and enhancements by many other people. - Add Python 3 support. (Pillow >= 2.0.0 supports Python 2.6, 2.7, 3.2, 3.3. Pillow < 2.0.0 supports Python 2.4, 2.5, 2.6, 2.7.) [fluggo] @@ -884,7 +1351,7 @@ Changelog (Pillow) - Backport PIL's PNG/Zip improvements. [olt] -- Various 64 bit and Windows fixes. +- Various 64-bit and Windows fixes. [cgohlke] - Add testing suite. @@ -1013,10 +1480,11 @@ Changelog (Pillow) - Forked PIL based on `Hanno Schlichting's re-packaging `_ [aclark4life] -.. Note:: What follows is the original PIL 1.1.7 CHANGES +Pre-fork +-------- -0.2b5 - 1.1.7 (1995-2010) -------------------------- +0.2b5-1.1.7 ++++++++++++ :: @@ -1055,7 +1523,7 @@ Changelog (Pillow) This section may not be fully complete. For changes since this file was last updated, see the repository revision history: - http://bitbucket.org/effbot/pil-2009-raclette/changesets/ + https://bitbucket.org/effbot/pil-2009-raclette/commits/all (1.1.7 final) @@ -2028,7 +2496,7 @@ Changelog (Pillow) + Added experimental "RGBa" mode support. An "RGBa" image is an RGBA image where the colour components - have have been premultipled with the alpha value. PIL allows + have have been premultiplied with the alpha value. PIL allows you to convert an RGBA image to an RGBa image, and to paste RGBa images on top of RGB images. Since this saves a bunch of multiplications and shifts, it is typically about twice diff --git a/LICENSE b/LICENSE index c0f6cde5b..87743e737 100644 --- a/LICENSE +++ b/LICENSE @@ -3,6 +3,12 @@ The Python Imaging Library (PIL) is Copyright © 1997-2011 by Secret Labs AB Copyright © 1995-2011 by Fredrik Lundh +Pillow is the friendly PIL fork. It is + + Copyright © 2016 by Alex Clark and contributors + +Like PIL, Pillow is licensed under the MIT-like open source PIL Software License: + By obtaining, using, and/or copying this software and/or its associated documentation, you agree that you have read, understood, and will comply with the following terms and conditions: Permission to use, copy, modify, and distribute this software and its associated documentation for any purpose and without fee is hereby granted, provided that the above copyright notice appears in all copies, and that both that copyright notice and this permission notice appear in supporting documentation, and that the name of Secret Labs AB or the author not be used in advertising or publicity pertaining to distribution of the software without specific, written prior permission. diff --git a/MANIFEST.in b/MANIFEST.in index 4fb47c637..f6a1488f0 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,80 +1,31 @@ + include *.c include *.h +include *.in include *.md include *.py -include *.sh include *.rst +include *.sh include *.txt -include *.yaml -include .coveragerc -include .gitattributes -include .travis.yml include LICENSE include Makefile -include tox.ini -recursive-include PIL *.md -recursive-include Scripts *.py -recursive-include Scripts *.rst -recursive-include Scripts *.sh -recursive-include Scripts README.rst -recursive-include Tests *.bdf -recursive-include Tests *.bin -recursive-include Tests *.bmp -recursive-include Tests *.bw -recursive-include Tests *.cur -recursive-include Tests *.dcx -recursive-include Tests *.doc -recursive-include Tests *.eps -recursive-include Tests *.fli -recursive-include Tests *.ggr -recursive-include Tests *.gif -recursive-include Tests *.gnuplot -recursive-include Tests *.html -recursive-include Tests *.icc -recursive-include Tests *.icns -recursive-include Tests *.ico -recursive-include Tests *.j2k -recursive-include Tests *.jp2 -recursive-include Tests *.jpg -recursive-include Tests *.lut -recursive-include Tests *.mpo -recursive-include Tests *.pbm -recursive-include Tests *.pcf -recursive-include Tests *.pcx -recursive-include Tests *.pgm -recursive-include Tests *.pil -recursive-include Tests *.png -recursive-include Tests *.ppm -recursive-include Tests *.psd -recursive-include Tests *.py -recursive-include Tests *.ras -recursive-include Tests *.rgb -recursive-include Tests *.rst -recursive-include Tests *.sgi -recursive-include Tests *.spider -recursive-include Tests *.tar -recursive-include Tests *.tga -recursive-include Tests *.tif -recursive-include Tests *.tiff -recursive-include Tests *.ttf -recursive-include Tests *.txt -recursive-include Tests *.webp -recursive-include Tests *.xpm -recursive-include Tests *.msp -recursive-include Tk *.c -recursive-include Tk *.rst -recursive-include depends *.rst -recursive-include depends *.sh -recursive-include docs *.bat -recursive-include docs *.gitignore -recursive-include docs *.html -recursive-include docs *.py -recursive-include docs *.rst -recursive-include docs *.txt -recursive-include docs BUILDME -recursive-include docs COPYING -recursive-include docs Guardfile -recursive-include docs LICENSE -recursive-include docs Makefile -recursive-include libImaging *.c -recursive-include libImaging *.h +graft Scripts +graft Tests +graft PIL +graft Tk +graft libImaging +graft depends +graft winbuild +graft docs +prune docs/_static + +# build/src control detritus +exclude .coveragerc +exclude .editorconfig +exclude .landscape.yaml +exclude appveyor.yml +exclude build_children.sh +exclude tox.ini +global-exclude .git* +global-exclude *.pyc +global-exclude *.so diff --git a/Makefile b/Makefile index 4d96c497d..148a70070 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,6 @@ # https://www.gnu.org/software/make/manual/html_node/Phony-Targets.html .PHONY: clean coverage doc docserve help inplace install install-req release-test sdist test upload upload-test +.DEFAULT_GOAL := release-test clean: python setup.py clean @@ -7,6 +8,12 @@ clean: rm -r build || true find . -name __pycache__ | xargs rm -r || true +BRANCHES=`git branch -a | grep -v HEAD | grep -v master | grep remote` +co: + -for i in $(BRANCHES) ; do \ + git checkout -t $$i ; \ + done + coverage: coverage erase coverage run --parallel-mode --include=PIL/* selftest.py @@ -32,6 +39,7 @@ help: @echo " inplace make inplace extension" @echo " install make and install" @echo " install-req install documentation and test dependencies" + @echo " install-venv install in virtualenv" @echo " release-test run code and package tests before release" @echo " test run tests on installed pillow" @echo " upload build and upload sdists to PyPI" @@ -47,6 +55,10 @@ install: install-req: pip install -r requirements.txt +install-venv: + virtualenv . + bin/pip install -r requirements.txt + release-test: $(MAKE) install-req python setup.py develop @@ -74,3 +86,6 @@ upload-test: upload: python setup.py sdist --format=gztar,zip upload + +readme: + viewdoc diff --git a/PIL/BdfFontFile.py b/PIL/BdfFontFile.py index 0c1614e0f..e6cc22f91 100644 --- a/PIL/BdfFontFile.py +++ b/PIL/BdfFontFile.py @@ -109,10 +109,10 @@ class BdfFontFile(FontFile.FontFile): if s.find(b"LogicalFontDescription") < 0: comments.append(s[i+1:-1].decode('ascii')) - font = props["FONT"].split("-") + # font = props["FONT"].split("-") - font[4] = bdf_slant[font[4].upper()] - font[11] = bdf_spacing[font[11].upper()] + # font[4] = bdf_slant[font[4].upper()] + # font[11] = bdf_spacing[font[11].upper()] # ascent = int(props["FONT_ASCENT"]) # descent = int(props["FONT_DESCENT"]) @@ -123,7 +123,6 @@ class BdfFontFile(FontFile.FontFile): # for i in comments: # print "#", i - font = [] while True: c = bdf_char(fp) if not c: diff --git a/PIL/BmpImagePlugin.py b/PIL/BmpImagePlugin.py index 30ca10971..e398445fa 100644 --- a/PIL/BmpImagePlugin.py +++ b/PIL/BmpImagePlugin.py @@ -24,12 +24,10 @@ # -__version__ = "0.7" - - from PIL import Image, ImageFile, ImagePalette, _binary import math +__version__ = "0.7" i8 = _binary.i8 i16 = _binary.i16le @@ -103,7 +101,9 @@ class BmpImageFile(ImageFile.ImageFile): 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: math.ceil(x / 39.3701), file_info['pixels_per_meter'])) + 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']): @@ -130,13 +130,18 @@ class BmpImageFile(ImageFile.ImageFile): # ----------------- Process BMP with Bitfields compression (not palette) if file_info['compression'] == self.BITFIELDS: SUPPORTED = { - 32: [(0xff0000, 0xff00, 0xff, 0x0), (0xff0000, 0xff00, 0xff, 0xff000000), (0x0, 0x0, 0x0, 0x0)], - 24: [(0xff0000, 0xff00, 0xff)], - 16: [(0xf800, 0x7e0, 0x1f), (0x7c00, 0x3e0, 0x1f)]} + 32: [(0xff0000, 0xff00, 0xff, 0x0), (0xff0000, 0xff00, 0xff, 0xff000000), (0x0, 0x0, 0x0, 0x0)], + 24: [(0xff0000, 0xff00, 0xff)], + 16: [(0xf800, 0x7e0, 0x1f), (0x7c00, 0x3e0, 0x1f)] + } MASK_MODES = { - (32, (0xff0000, 0xff00, 0xff, 0x0)): "BGRX", (32, (0xff0000, 0xff00, 0xff, 0xff000000)): "BGRA", (32, (0x0, 0x0, 0x0, 0x0)): "BGRA", - (24, (0xff0000, 0xff00, 0xff)): "BGR", - (16, (0xf800, 0x7e0, 0x1f)): "BGR;16", (16, (0x7c00, 0x3e0, 0x1f)): "BGR;15"} + (32, (0xff0000, 0xff00, 0xff, 0x0)): "BGRX", + (32, (0xff0000, 0xff00, 0xff, 0xff000000)): "BGRA", + (32, (0x0, 0x0, 0x0, 0x0)): "BGRA", + (24, (0xff0000, 0xff00, 0xff)): "BGR", + (16, (0xf800, 0x7e0, 0x1f)): "BGR;16", + (16, (0x7c00, 0x3e0, 0x1f)): "BGR;15" + } if file_info['bits'] in SUPPORTED: if file_info['bits'] == 32 and file_info['rgba_mask'] in SUPPORTED[file_info['bits']]: raw_mode = MASK_MODES[(file_info['bits'], file_info['rgba_mask'])] @@ -279,3 +284,5 @@ Image.register_open(BmpImageFile.format, BmpImageFile, _accept) Image.register_save(BmpImageFile.format, _save) Image.register_extension(BmpImageFile.format, ".bmp") + +Image.register_mime(BmpImageFile.format, "image/bmp") diff --git a/PIL/CurImagePlugin.py b/PIL/CurImagePlugin.py index 0178957ee..4db4c4073 100644 --- a/PIL/CurImagePlugin.py +++ b/PIL/CurImagePlugin.py @@ -17,10 +17,9 @@ # -__version__ = "0.1" - from PIL import Image, BmpImagePlugin, _binary +__version__ = "0.1" # # -------------------------------------------------------------------- @@ -67,6 +66,8 @@ class CurImageFile(BmpImagePlugin.BmpImageFile): # print "hotspot y", i16(s[6:]) # print "bytes", i32(s[8:]) # print "offset", i32(s[12:]) + if not m: + raise TypeError("No cursors were found") # load as bitmap self._bitmap(i32(m[12:]) + offset) @@ -82,6 +83,6 @@ class CurImageFile(BmpImagePlugin.BmpImageFile): # # -------------------------------------------------------------------- -Image.register_open("CUR", CurImageFile, _accept) +Image.register_open(CurImageFile.format, CurImageFile, _accept) -Image.register_extension("CUR", ".cur") +Image.register_extension(CurImageFile.format, ".cur") diff --git a/PIL/DcxImagePlugin.py b/PIL/DcxImagePlugin.py index 978c90e80..f9034d15c 100644 --- a/PIL/DcxImagePlugin.py +++ b/PIL/DcxImagePlugin.py @@ -21,19 +21,18 @@ # See the README file for information on usage and redistribution. # -__version__ = "0.2" - from PIL import Image, _binary - from PIL.PcxImagePlugin import PcxImageFile +__version__ = "0.2" + MAGIC = 0x3ADE68B1 # QUIZ: what's this value, then? i32 = _binary.i32le def _accept(prefix): - return i32(prefix) == MAGIC + return len(prefix) >= 4 and i32(prefix) == MAGIC ## @@ -66,6 +65,10 @@ class DcxImageFile(PcxImageFile): def n_frames(self): return len(self._offset) + @property + def is_animated(self): + return len(self._offset) > 1 + def seek(self, frame): if frame >= len(self._offset): raise EOFError("attempt to seek outside DCX directory") @@ -78,6 +81,6 @@ class DcxImageFile(PcxImageFile): return self.frame -Image.register_open("DCX", DcxImageFile, _accept) +Image.register_open(DcxImageFile.format, DcxImageFile, _accept) -Image.register_extension("DCX", ".dcx") +Image.register_extension(DcxImageFile.format, ".dcx") diff --git a/PIL/DdsImagePlugin.py b/PIL/DdsImagePlugin.py new file mode 100644 index 000000000..2ebfdf037 --- /dev/null +++ b/PIL/DdsImagePlugin.py @@ -0,0 +1,268 @@ +""" +A Pillow loader for .dds files (S3TC-compressed aka DXTC) +Jerome Leclanche + +Documentation: + http://oss.sgi.com/projects/ogl-sample/registry/EXT/texture_compression_s3tc.txt + +The contents of this file are hereby released in the public domain (CC0) +Full text of the CC0 license: + https://creativecommons.org/publicdomain/zero/1.0/ +""" + +import struct +from io import BytesIO +from PIL import Image, ImageFile + + +# Magic ("DDS ") +DDS_MAGIC = 0x20534444 + +# DDS flags +DDSD_CAPS = 0x1 +DDSD_HEIGHT = 0x2 +DDSD_WIDTH = 0x4 +DDSD_PITCH = 0x8 +DDSD_PIXELFORMAT = 0x1000 +DDSD_MIPMAPCOUNT = 0x20000 +DDSD_LINEARSIZE = 0x80000 +DDSD_DEPTH = 0x800000 + +# DDS caps +DDSCAPS_COMPLEX = 0x8 +DDSCAPS_TEXTURE = 0x1000 +DDSCAPS_MIPMAP = 0x400000 + +DDSCAPS2_CUBEMAP = 0x200 +DDSCAPS2_CUBEMAP_POSITIVEX = 0x400 +DDSCAPS2_CUBEMAP_NEGATIVEX = 0x800 +DDSCAPS2_CUBEMAP_POSITIVEY = 0x1000 +DDSCAPS2_CUBEMAP_NEGATIVEY = 0x2000 +DDSCAPS2_CUBEMAP_POSITIVEZ = 0x4000 +DDSCAPS2_CUBEMAP_NEGATIVEZ = 0x8000 +DDSCAPS2_VOLUME = 0x200000 + +# Pixel Format +DDPF_ALPHAPIXELS = 0x1 +DDPF_ALPHA = 0x2 +DDPF_FOURCC = 0x4 +DDPF_PALETTEINDEXED8 = 0x20 +DDPF_RGB = 0x40 +DDPF_LUMINANCE = 0x20000 + + +# dds.h + +DDS_FOURCC = DDPF_FOURCC +DDS_RGB = DDPF_RGB +DDS_RGBA = DDPF_RGB | DDPF_ALPHAPIXELS +DDS_LUMINANCE = DDPF_LUMINANCE +DDS_LUMINANCEA = DDPF_LUMINANCE | DDPF_ALPHAPIXELS +DDS_ALPHA = DDPF_ALPHA +DDS_PAL8 = DDPF_PALETTEINDEXED8 + +DDS_HEADER_FLAGS_TEXTURE = (DDSD_CAPS | DDSD_HEIGHT | DDSD_WIDTH | + DDSD_PIXELFORMAT) +DDS_HEADER_FLAGS_MIPMAP = DDSD_MIPMAPCOUNT +DDS_HEADER_FLAGS_VOLUME = DDSD_DEPTH +DDS_HEADER_FLAGS_PITCH = DDSD_PITCH +DDS_HEADER_FLAGS_LINEARSIZE = DDSD_LINEARSIZE + +DDS_HEIGHT = DDSD_HEIGHT +DDS_WIDTH = DDSD_WIDTH + +DDS_SURFACE_FLAGS_TEXTURE = DDSCAPS_TEXTURE +DDS_SURFACE_FLAGS_MIPMAP = DDSCAPS_COMPLEX | DDSCAPS_MIPMAP +DDS_SURFACE_FLAGS_CUBEMAP = DDSCAPS_COMPLEX + +DDS_CUBEMAP_POSITIVEX = DDSCAPS2_CUBEMAP | DDSCAPS2_CUBEMAP_POSITIVEX +DDS_CUBEMAP_NEGATIVEX = DDSCAPS2_CUBEMAP | DDSCAPS2_CUBEMAP_NEGATIVEX +DDS_CUBEMAP_POSITIVEY = DDSCAPS2_CUBEMAP | DDSCAPS2_CUBEMAP_POSITIVEY +DDS_CUBEMAP_NEGATIVEY = DDSCAPS2_CUBEMAP | DDSCAPS2_CUBEMAP_NEGATIVEY +DDS_CUBEMAP_POSITIVEZ = DDSCAPS2_CUBEMAP | DDSCAPS2_CUBEMAP_POSITIVEZ +DDS_CUBEMAP_NEGATIVEZ = DDSCAPS2_CUBEMAP | DDSCAPS2_CUBEMAP_NEGATIVEZ + + +# DXT1 +DXT1_FOURCC = 0x31545844 + +# DXT3 +DXT3_FOURCC = 0x33545844 + +# DXT5 +DXT5_FOURCC = 0x35545844 + + +def _decode565(bits): + a = ((bits >> 11) & 0x1f) << 3 + b = ((bits >> 5) & 0x3f) << 2 + c = (bits & 0x1f) << 3 + return a, b, c + + +def _c2a(a, b): + return (2 * a + b) // 3 + + +def _c2b(a, b): + return (a + b) // 2 + + +def _c3(a, b): + return (2 * b + a) // 3 + + +def _dxt1(data, width, height): + # TODO implement this function as pixel format in decode.c + ret = bytearray(4 * width * height) + + for y in range(0, height, 4): + for x in range(0, width, 4): + color0, color1, bits = struct.unpack("> 2 + if control == 0: + r, g, b = r0, g0, b0 + elif control == 1: + r, g, b = r1, g1, b1 + elif control == 2: + if color0 > color1: + r, g, b = _c2a(r0, r1), _c2a(g0, g1), _c2a(b0, b1) + else: + r, g, b = _c2b(r0, r1), _c2b(g0, g1), _c2b(b0, b1) + elif control == 3: + if color0 > color1: + r, g, b = _c3(r0, r1), _c3(g0, g1), _c3(b0, b1) + else: + r, g, b = 0, 0, 0 + + idx = 4 * ((y + j) * width + (x + i)) + ret[idx:idx+4] = struct.pack('4B', r, g, b, 255) + + return bytes(ret) + + +def _dxtc_alpha(a0, a1, ac0, ac1, ai): + if ai <= 12: + ac = (ac0 >> ai) & 7 + elif ai == 15: + ac = (ac0 >> 15) | ((ac1 << 1) & 6) + else: + ac = (ac1 >> (ai - 16)) & 7 + + if ac == 0: + alpha = a0 + elif ac == 1: + alpha = a1 + elif a0 > a1: + alpha = ((8 - ac) * a0 + (ac - 1) * a1) // 7 + elif ac == 6: + alpha = 0 + elif ac == 7: + alpha = 0xff + else: + alpha = ((6 - ac) * a0 + (ac - 1) * a1) // 5 + + return alpha + + +def _dxt5(data, width, height): + # TODO implement this function as pixel format in decode.c + ret = bytearray(4 * width * height) + + for y in range(0, height, 4): + for x in range(0, width, 4): + a0, a1, ac0, ac1, c0, c1, code = struct.unpack("<2BHI2HI", + data.read(16)) + + r0, g0, b0 = _decode565(c0) + r1, g1, b1 = _decode565(c1) + + for j in range(4): + for i in range(4): + ai = 3 * (4 * j + i) + alpha = _dxtc_alpha(a0, a1, ac0, ac1, ai) + + cc = (code >> 2 * (4 * j + i)) & 3 + if cc == 0: + r, g, b = r0, g0, b0 + elif cc == 1: + r, g, b = r1, g1, b1 + elif cc == 2: + r, g, b = _c2a(r0, r1), _c2a(g0, g1), _c2a(b0, b1) + elif cc == 3: + r, g, b = _c3(r0, r1), _c3(g0, g1), _c3(b0, b1) + + idx = 4 * ((y + j) * width + (x + i)) + ret[idx:idx+4] = struct.pack('4B', r, g, b, alpha) + + return bytes(ret) + + +class DdsImageFile(ImageFile.ImageFile): + format = "DDS" + format_description = "DirectDraw Surface" + + def _open(self): + magic, header_size = struct.unpack("= 4 and i32(prefix) == 0xC6D3D0C5) ## # Image plugin for Encapsulated Postscript. This plugin supports only @@ -294,7 +295,7 @@ class EpsImageFile(ImageFile.ImageFile): break try: self.mode = self.mode_map[int(mo)] - except: + except ValueError: break self.size = int(x), int(y) @@ -376,9 +377,10 @@ def _save(im, fp, filename, eps=1): pass base_fp = fp - fp = NoCloseStream(fp) - if sys.version_info[0] > 2: - fp = io.TextIOWrapper(fp, encoding='latin-1') + if fp != sys.stdout: + fp = NoCloseStream(fp) + if sys.version_info[0] > 2: + fp = io.TextIOWrapper(fp, encoding='latin-1') if eps: # @@ -403,13 +405,15 @@ def _save(im, fp, filename, eps=1): fp.write("[%d 0 0 -%d 0 %d]\n" % (im.size[0], im.size[1], im.size[1])) fp.write("{ currentfile buf readhexstring pop } bind\n") fp.write(operator[2] + "\n") - fp.flush() + if hasattr(fp, "flush"): + fp.flush() ImageFile._save(im, base_fp, [("eps", (0, 0)+im.size, 0, None)]) fp.write("\n%%%%EndBinary\n") fp.write("grestore end\n") - fp.flush() + if hasattr(fp, "flush"): + fp.flush() # # -------------------------------------------------------------------- diff --git a/PIL/ExifTags.py b/PIL/ExifTags.py index 52e145f62..72e629dc5 100644 --- a/PIL/ExifTags.py +++ b/PIL/ExifTags.py @@ -27,9 +27,9 @@ TAGS = { 0x0102: "BitsPerSample", 0x0103: "Compression", 0x0106: "PhotometricInterpretation", - 0x0107: "Threshholding", + 0x0107: "Thresholding", 0x0108: "CellWidth", - 0x0109: "CellLenght", + 0x0109: "CellLength", 0x010a: "FillOrder", 0x010d: "DocumentName", 0x011d: "PageName", @@ -40,7 +40,7 @@ TAGS = { 0x0112: "Orientation", 0x0115: "SamplesPerPixel", 0x0116: "RowsPerStrip", - 0x0117: "StripByteConunts", + 0x0117: "StripByteCounts", 0x0118: "MinSampleValue", 0x0119: "MaxSampleValue", 0x011a: "XResolution", diff --git a/PIL/FliImagePlugin.py b/PIL/FliImagePlugin.py index 0660ddeb6..a07dc29b0 100644 --- a/PIL/FliImagePlugin.py +++ b/PIL/FliImagePlugin.py @@ -16,10 +16,10 @@ # -__version__ = "0.2" - from PIL import Image, ImageFile, ImagePalette, _binary +__version__ = "0.2" + i8 = _binary.i8 i16 = _binary.i16le i32 = _binary.i32le @@ -30,7 +30,7 @@ o8 = _binary.o8 # decoder def _accept(prefix): - return i16(prefix[4:6]) in [0xAF11, 0xAF12] + return len(prefix) >= 6 and i16(prefix[4:6]) in [0xAF11, 0xAF12] ## @@ -90,6 +90,7 @@ class FliImageFile(ImageFile.ImageFile): self.__fp = self.fp self.__rewind = self.fp.tell() self._n_frames = None + self._is_animated = None self.seek(0) def _palette(self, palette, shift): @@ -122,13 +123,33 @@ class FliImageFile(ImageFile.ImageFile): self.seek(current) return self._n_frames + @property + def is_animated(self): + if self._is_animated is None: + 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): if frame == self.__frame: return if frame < self.__frame: self._seek(0) + + last_frame = self.__frame for f in range(self.__frame + 1, frame + 1): - self._seek(f) + try: + self._seek(f) + except EOFError: + self.seek(last_frame) + raise EOFError("no more images in FLI file") def _seek(self, frame): if frame == 0: @@ -161,7 +182,7 @@ class FliImageFile(ImageFile.ImageFile): # # registry -Image.register_open("FLI", FliImageFile, _accept) +Image.register_open(FliImageFile.format, FliImageFile, _accept) -Image.register_extension("FLI", ".fli") -Image.register_extension("FLI", ".flc") +Image.register_extension(FliImageFile.format, ".fli") +Image.register_extension(FliImageFile.format, ".flc") diff --git a/PIL/FpxImagePlugin.py b/PIL/FpxImagePlugin.py index 9d338d9da..aefc57420 100644 --- a/PIL/FpxImagePlugin.py +++ b/PIL/FpxImagePlugin.py @@ -16,12 +16,11 @@ # -__version__ = "0.1" - - from PIL import Image, ImageFile from PIL.OleFileIO import i8, i32, MAGIC, OleFileIO +__version__ = "0.1" + # we map from colour field tuples to (mode, rawmode) descriptors MODES = { @@ -222,6 +221,6 @@ class FpxImageFile(ImageFile.ImageFile): # # -------------------------------------------------------------------- -Image.register_open("FPX", FpxImageFile, _accept) +Image.register_open(FpxImageFile.format, FpxImageFile, _accept) -Image.register_extension("FPX", ".fpx") +Image.register_extension(FpxImageFile.format, ".fpx") diff --git a/PIL/FtexImagePlugin.py b/PIL/FtexImagePlugin.py new file mode 100644 index 000000000..f3a2d7fa7 --- /dev/null +++ b/PIL/FtexImagePlugin.py @@ -0,0 +1,96 @@ +""" +A Pillow loader for .ftc and .ftu files (FTEX) +Jerome Leclanche + +The contents of this file are hereby released in the public domain (CC0) +Full text of the CC0 license: + https://creativecommons.org/publicdomain/zero/1.0/ + +Independence War 2: Edge Of Chaos - Texture File Format - 16 October 2001 + +The textures used for 3D objects in Independence War 2: Edge Of Chaos are in a +packed custom format called FTEX. This file format uses file extensions FTC and FTU. +* FTC files are compressed textures (using standard texture compression). +* FTU files are not compressed. +Texture File Format +The FTC and FTU texture files both use the same format, called. This +has the following structure: +{header} +{format_directory} +{data} +Where: +{header} = { u32:magic, u32:version, u32:width, u32:height, u32:mipmap_count, u32:format_count } + +* The "magic" number is "FTEX". +* "width" and "height" are the dimensions of the texture. +* "mipmap_count" is the number of mipmaps in the texture. +* "format_count" is the number of texture formats (different versions of the same texture) in this file. + +{format_directory} = format_count * { u32:format, u32:where } + +The format value is 0 for DXT1 compressed textures and 1 for 24-bit RGB uncompressed textures. +The texture data for a format starts at the position "where" in the file. + +Each set of texture data in the file has the following structure: +{data} = format_count * { u32:mipmap_size, mipmap_size * { u8 } } +* "mipmap_size" is the number of bytes in that mip level. For compressed textures this is the +size of the texture data compressed with DXT1. For 24 bit uncompressed textures, this is 3 * width * height. +Following this are the image bytes for that mipmap level. + +Note: All data is stored in little-Endian (Intel) byte order. +""" + +import struct +from io import BytesIO +from PIL import Image, ImageFile +from PIL.DdsImagePlugin import _dxt1 + + +MAGIC = b"FTEX" +FORMAT_DXT1 = 0 +FORMAT_UNCOMPRESSED = 1 + + +class FtexImageFile(ImageFile.ImageFile): + format = "FTEX" + format_description = "Texture File Format (IW2:EOC)" + + def _open(self): + magic = struct.unpack("= 20 and i32(prefix[4:8]) == 1 + return len(prefix) >= 8 and i32(prefix[:4]) >= 20 and i32(prefix[4:8]) in (1, 2) ## @@ -31,41 +42,53 @@ class GbrImageFile(ImageFile.ImageFile): format_description = "GIMP brush file" def _open(self): - header_size = i32(self.fp.read(4)) version = i32(self.fp.read(4)) - if header_size < 20 or version != 1: + if header_size < 20: raise SyntaxError("not a GIMP brush") + if version not in (1, 2): + raise SyntaxError("Unsupported GIMP brush version: %s" % version) width = i32(self.fp.read(4)) height = i32(self.fp.read(4)) color_depth = i32(self.fp.read(4)) - if width <= 0 or height <= 0 or color_depth != 1: + if width <= 0 or height <= 0: raise SyntaxError("not a GIMP brush") + if color_depth not in (1, 4): + raise SyntaxError("Unsupported GMP brush color depth: %s" % color_depth) - comment = self.fp.read(header_size - 20)[:-1] + if version == 1: + comment_length = header_size-20 + else: + comment_length = header_size-28 + magic_number = self.fp.read(4) + if magic_number != b'GIMP': + raise SyntaxError("not a GIMP brush, bad magic number") + self.info['spacing'] = i32(self.fp.read(4)) + + comment = self.fp.read(comment_length)[:-1] + + if color_depth == 1: + self.mode = "L" + else: + self.mode = 'RGBA' - self.mode = "L" self.size = width, height self.info["comment"] = comment - # Since the brush is so small, we read the data immediately - self.data = self.fp.read(width * height) + # Image might not be small + Image._decompression_bomb_check(self.size) + + # Data is an uncompressed block of w * h * bytes/pixel + self._data_size = width * height * color_depth def load(self): - - if not self.data: - return - - # create an image out of the brush data block self.im = Image.core.new(self.mode, self.size) - self.im.frombytes(self.data) - self.data = b"" + self.frombytes(self.fp.read(self._data_size)) # # registry -Image.register_open("GBR", GbrImageFile, _accept) - -Image.register_extension("GBR", ".gbr") +Image.register_open(GbrImageFile.format, GbrImageFile, _accept) +Image.register_extension(GbrImageFile.format, ".gbr") diff --git a/PIL/GdImageFile.py b/PIL/GdImageFile.py index 080153a9f..ae3500f0c 100644 --- a/PIL/GdImageFile.py +++ b/PIL/GdImageFile.py @@ -23,11 +23,11 @@ # purposes only. -__version__ = "0.1" - from PIL import ImageFile, ImagePalette, _binary from PIL._util import isPath +__version__ = "0.1" + try: import builtins except ImportError: diff --git a/PIL/GifImagePlugin.py b/PIL/GifImagePlugin.py index 150773b67..b9d258898 100644 --- a/PIL/GifImagePlugin.py +++ b/PIL/GifImagePlugin.py @@ -24,7 +24,8 @@ # See the README file for information on usage and redistribution. # -from PIL import Image, ImageFile, ImagePalette, _binary +from PIL import Image, ImageFile, ImagePalette, \ + ImageChops, ImageSequence, _binary __version__ = "0.9" @@ -88,6 +89,7 @@ class GifImageFile(ImageFile.ImageFile): self.__fp = self.fp # FIXME: hack self.__rewind = self.fp.tell() self._n_frames = None + self._is_animated = None self._seek(0) # get ready to read first frame @property @@ -102,13 +104,33 @@ class GifImageFile(ImageFile.ImageFile): self.seek(current) return self._n_frames + @property + def is_animated(self): + if self._is_animated is None: + 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): if frame == self.__frame: return if frame < self.__frame: self._seek(0) + + last_frame = self.__frame for f in range(self.__frame + 1, frame + 1): - self._seek(f) + try: + self._seek(f) + except EOFError: + self.seek(last_frame) + raise EOFError("no more images in GIF file") def _seek(self, frame): @@ -241,7 +263,7 @@ class GifImageFile(ImageFile.ImageFile): if not self.tile: # self.__fp = None - raise EOFError("no more images in GIF file") + raise EOFError self.mode = "L" if self.palette: @@ -279,8 +301,27 @@ RAWMODE = { } -def _save(im, fp, filename): +def _convert_mode(im, initial_call=False): + # convert on the fly (EXPERIMENTAL -- I'm not sure PIL + # should automatically convert images on save...) + if Image.getmodebase(im.mode) == "RGB": + if initial_call: + palette_size = 256 + if im.palette: + palette_size = len(im.palette.getdata()[1]) // 3 + return im.convert("P", palette=1, colors=palette_size) + else: + return im.convert("P") + return im.convert("L") + +def _save_all(im, fp, filename): + _save(im, fp, filename, save_all=True) + + +def _save(im, fp, filename, save_all=False): + + im.encoderinfo.update(im.info) if _imaging_gif: # call external driver try: @@ -290,17 +331,9 @@ def _save(im, fp, filename): pass # write uncompressed file if im.mode in RAWMODE: - im_out = im + im_out = im.copy() else: - # convert on the fly (EXPERIMENTAL -- I'm not sure PIL - # should automatically convert images on save...) - if Image.getmodebase(im.mode) == "RGB": - palette_size = 256 - if im.palette: - palette_size = len(im.palette.getdata()[1]) // 3 - im_out = im.convert("P", palette=1, colors=palette_size) - else: - im_out = im.convert("L") + im_out = _convert_mode(im, True) # header try: @@ -309,25 +342,63 @@ def _save(im, fp, filename): palette = None im.encoderinfo["optimize"] = im.encoderinfo.get("optimize", True) - header, used_palette_colors = getheader(im_out, palette, im.encoderinfo) - for s in header: - fp.write(s) + if save_all: + previous = None - # local image header - get_local_header(fp, im) + first_frame = None + for im_frame in ImageSequence.Iterator(im): + im_frame = _convert_mode(im_frame) - im_out.encoderconfig = (8, get_interlace(im)) - ImageFile._save(im_out, fp, [("gif", (0, 0)+im.size, 0, - RAWMODE[im_out.mode])]) + # To specify duration, add the time in milliseconds to getdata(), + # e.g. getdata(im_frame, duration=1000) + if not previous: + # global header + first_frame = getheader(im_frame, palette, im.encoderinfo)[0] + first_frame += getdata(im_frame, (0, 0), **im.encoderinfo) + else: + if first_frame: + for s in first_frame: + fp.write(s) + first_frame = None - fp.write(b"\0") # end of image data + # delta frame + delta = ImageChops.subtract_modulo(im_frame, previous.copy()) + bbox = delta.getbbox() + + if bbox: + # compress difference + for s in getdata(im_frame.crop(bbox), + bbox[:2], **im.encoderinfo): + fp.write(s) + else: + # FIXME: what should we do in this case? + pass + previous = im_frame + if first_frame: + save_all = False + if not save_all: + header = getheader(im_out, palette, im.encoderinfo)[0] + for s in header: + fp.write(s) + + flags = 0 + + if get_interlace(im): + flags = flags | 64 + + # local image header + _get_local_header(fp, im, (0, 0), flags) + + im_out.encoderconfig = (8, get_interlace(im)) + ImageFile._save(im_out, fp, [("gif", (0, 0)+im.size, 0, + RAWMODE[im_out.mode])]) + + fp.write(b"\0") # end of image data fp.write(b";") # end of file - try: + if hasattr(fp, "flush"): fp.flush() - except: - pass def get_interlace(im): @@ -343,7 +414,7 @@ def get_interlace(im): return interlace -def get_local_header(fp, im, offset=(0, 0)): +def _get_local_header(fp, im, offset, flags): transparent_color_exists = False try: transparency = im.encoderinfo["transparency"] @@ -394,12 +465,6 @@ def get_local_header(fp, im, offset=(0, 0)): o8(1) + o16(number_of_loops) + # number of loops o8(0)) - - flags = 0 - - if get_interlace(im): - flags = flags | 64 - fp.write(b"," + o16(offset[0]) + # offset o16(offset[1]) + @@ -451,7 +516,7 @@ def _save_netpbm(im, fp, filename): try: os.unlink(file) - except: + except OSError: pass @@ -480,8 +545,19 @@ def getheader(im, palette=None, info=None): # Header Block # http://www.matthewflickinger.com/lab/whatsinagif/bits_and_bytes.asp + + version = b"87a" + for extensionKey in ["transparency", "duration", "loop"]: + if info and extensionKey in info and \ + not (extensionKey == "duration" and info[extensionKey] == 0): + version = b"89a" + break + else: + if im.info.get("version") == "89a": + version = b"89a" + header = [ - b"GIF87a" + # signature + version + b"GIF"+version + # signature + version o16(im.size[0]) + # canvas width o16(im.size[1]) # canvas height ] @@ -538,7 +614,17 @@ def getheader(im, palette=None, info=None): # size of global color table + global color table flag header.append(o8(color_table_size + 128)) # background + reserved/aspect - header.append(o8(0) + o8(0)) + if info and "background" in info: + background = info["background"] + elif "background" in im.info: + # This elif is redundant within GifImagePlugin + # since im.info parameters are bundled into the info dictionary + # However, external scripts may call getheader directly + # So this maintains earlier behaviour + background = im.info["background"] + else: + background = 0 + header.append(o8(background) + o8(0)) # end of Logical Screen Descriptor # add the missing amount of bytes @@ -571,7 +657,7 @@ def getdata(im, offset=(0, 0), **params): im.encoderinfo = params # local image header - get_local_header(fp, im, offset) + _get_local_header(fp, im, offset, 0) ImageFile._save(im, fp, [("gif", (0, 0)+im.size, 0, RAWMODE[im.mode])]) @@ -588,6 +674,7 @@ def getdata(im, offset=(0, 0), **params): Image.register_open(GifImageFile.format, GifImageFile, _accept) Image.register_save(GifImageFile.format, _save) +Image.register_save_all(GifImageFile.format, _save_all) Image.register_extension(GifImageFile.format, ".gif") Image.register_mime(GifImageFile.format, "image/gif") diff --git a/PIL/IcnsImagePlugin.py b/PIL/IcnsImagePlugin.py index e0b130e81..a4366e9e7 100644 --- a/PIL/IcnsImagePlugin.py +++ b/PIL/IcnsImagePlugin.py @@ -306,10 +306,8 @@ def _save(im, fp, filename): OS X only. """ - try: + if hasattr(fp, "flush"): fp.flush() - except: - pass # create the temporary set of pngs iconset = tempfile.mkdtemp('.iconset') @@ -345,13 +343,14 @@ def _save(im, fp, filename): if retcode: raise CalledProcessError(retcode, convert_cmd) -Image.register_open("ICNS", IcnsImageFile, lambda x: x[:4] == b'icns') -Image.register_extension("ICNS", '.icns') +Image.register_open(IcnsImageFile.format, IcnsImageFile, + lambda x: x[:4] == b'icns') +Image.register_extension(IcnsImageFile.format, '.icns') if sys.platform == 'darwin': - Image.register_save("ICNS", _save) + Image.register_save(IcnsImageFile.format, _save) - Image.register_mime("ICNS", "image/icns") + Image.register_mime(IcnsImageFile.format, "image/icns") if __name__ == '__main__': diff --git a/PIL/IcoImagePlugin.py b/PIL/IcoImagePlugin.py index 778edaf9a..a01aed376 100644 --- a/PIL/IcoImagePlugin.py +++ b/PIL/IcoImagePlugin.py @@ -15,21 +15,21 @@ # This plugin is a refactored version of Win32IconImagePlugin by Bryan Davis # . -# https://code.google.com/p/casadebender/wiki/Win32IconImagePlugin +# https://code.google.com/archive/p/casadebender/wikis/Win32IconImagePlugin.wiki # # Icon format references: -# * http://en.wikipedia.org/wiki/ICO_(file_format) -# * http://msdn.microsoft.com/en-us/library/ms997538.aspx +# * https://en.wikipedia.org/wiki/ICO_(file_format) +# * https://msdn.microsoft.com/en-us/library/ms997538.aspx -__version__ = "0.1" - import struct from io import BytesIO from PIL import Image, ImageFile, BmpImagePlugin, PngImagePlugin, _binary from math import log, ceil +__version__ = "0.1" + # # -------------------------------------------------------------------- @@ -252,7 +252,7 @@ class IcoImageFile(ImageFile.ImageFile): This plugin is a refactored version of Win32IconImagePlugin by Bryan Davis . - https://code.google.com/p/casadebender/wiki/Win32IconImagePlugin + https://code.google.com/archive/p/casadebender/wikis/Win32IconImagePlugin.wiki """ format = "ICO" format_description = "Windows Icon" @@ -278,6 +278,6 @@ class IcoImageFile(ImageFile.ImageFile): # # -------------------------------------------------------------------- -Image.register_open("ICO", IcoImageFile, _accept) -Image.register_save("ICO", _save) -Image.register_extension("ICO", ".ico") +Image.register_open(IcoImageFile.format, IcoImageFile, _accept) +Image.register_save(IcoImageFile.format, _save) +Image.register_extension(IcoImageFile.format, ".ico") diff --git a/PIL/ImImagePlugin.py b/PIL/ImImagePlugin.py index 589928d0e..dd4f82900 100644 --- a/PIL/ImImagePlugin.py +++ b/PIL/ImImagePlugin.py @@ -26,12 +26,12 @@ # -__version__ = "0.7" - import re from PIL import Image, ImageFile, ImagePalette from PIL._binary import i8 +__version__ = "0.7" + # -------------------------------------------------------------------- # Standard tags @@ -264,6 +264,10 @@ class ImImageFile(ImageFile.ImageFile): def n_frames(self): return self.info[FRAMES] + @property + def is_animated(self): + return self.info[FRAMES] > 1 + def seek(self, frame): if frame < 0 or frame >= self.info[FRAMES]: @@ -345,7 +349,7 @@ def _save(im, fp, filename, check=0): # -------------------------------------------------------------------- # Registry -Image.register_open("IM", ImImageFile) -Image.register_save("IM", _save) +Image.register_open(ImImageFile.format, ImImageFile) +Image.register_save(ImImageFile.format, _save) -Image.register_extension("IM", ".im") +Image.register_extension(ImImageFile.format, ".im") diff --git a/PIL/Image.py b/PIL/Image.py index 1c95bfca0..c0515d378 100644 --- a/PIL/Image.py +++ b/PIL/Image.py @@ -28,8 +28,11 @@ from __future__ import print_function from PIL import VERSION, PILLOW_VERSION, _plugins +import logging import warnings +logger = logging.getLogger(__name__) + class DecompressionBombWarning(RuntimeWarning): pass @@ -79,20 +82,24 @@ except ImportError as v: ) elif str(v).startswith("The _imaging extension"): warnings.warn(str(v), RuntimeWarning) - elif "Symbol not found: _PyUnicodeUCS2_FromString" in str(v): + elif "Symbol not found: _PyUnicodeUCS2_" in str(v): + # should match _PyUnicodeUCS2_FromString and + # _PyUnicodeUCS2_AsLatin1String warnings.warn( "The _imaging extension was built for Python with UCS2 support; " - "recompile PIL or build Python --without-wide-unicode. ", + "recompile Pillow or build Python --without-wide-unicode. ", RuntimeWarning ) - elif "Symbol not found: _PyUnicodeUCS4_FromString" in str(v): + elif "Symbol not found: _PyUnicodeUCS4_" in str(v): + # should match _PyUnicodeUCS4_FromString and + # _PyUnicodeUCS4_AsLatin1String warnings.warn( "The _imaging extension was built for Python with UCS4 support; " - "recompile PIL or build Python --with-wide-unicode. ", + "recompile Pillow or build Python --with-wide-unicode. ", RuntimeWarning ) # Fail here anyway. Don't let people run with a mostly broken Pillow. - # see docs/porting-pil-to-pillow.rst + # see docs/porting.rst raise try: @@ -121,7 +128,7 @@ USE_CFFI_ACCESS = hasattr(sys, 'pypy_version_info') try: import cffi HAS_CFFI = True -except: +except ImportError: HAS_CFFI = False @@ -138,11 +145,6 @@ def isImageType(t): """ return hasattr(t, "im") -# -# Debug level - -DEBUG = 0 - # # Constants (also defined in _imagingmodule.c!) @@ -204,6 +206,7 @@ ID = [] OPEN = {} MIME = {} SAVE = {} +SAVE_ALL = {} EXTENSION = {} # -------------------------------------------------------------------- @@ -251,6 +254,7 @@ _MODE_CONV = { "CMYK": ('|u1', 4), "YCbCr": ('|u1', 3), "LAB": ('|u1', 3), # UNDONE - unsigned |u1i1i1 + "HSV": ('|u1', 3), # I;16 == I;16L, and I;32 == I;32L "I;16": ('u2', None), @@ -386,13 +390,10 @@ def init(): for plugin in _plugins: try: - if DEBUG: - print("Importing %s" % plugin) + logger.debug("Importing %s", plugin) __import__("PIL.%s" % plugin, globals(), locals(), []) - except ImportError: - if DEBUG: - print("Image: failed to import", end=' ') - print(plugin, ":", sys.exc_info()[1]) + except ImportError as e: + logger.debug("Image: failed to import %s: %s", plugin, e) if OPEN or SAVE: _initialized = 2 @@ -504,12 +505,21 @@ class Image(object): self.readonly = 0 self.pyaccess = None + @property + def width(self): + return self.size[0] + + @property + def height(self): + return self.size[1] + def _new(self, im): new = Image() new.im = im new.mode = im.mode new.size = im.size - new.palette = self.palette + if self.palette: + new.palette = self.palette.copy() if im.mode == "P" and not new.palette: from PIL import ImagePalette new.palette = ImagePalette.ImagePalette() @@ -545,8 +555,7 @@ class Image(object): try: self.fp.close() except Exception as msg: - if DEBUG: - print("Error closing: %s" % msg) + logger.debug("Error closing: %s", msg) # Instead of simply setting to None, we're setting up a # deferred error that will better explain that the core image @@ -618,6 +627,7 @@ class Image(object): new['shape'] = shape new['typestr'] = typestr new['data'] = self.tobytes() + new['version'] = 3 return new raise AttributeError(name) @@ -643,7 +653,14 @@ class Image(object): def tobytes(self, encoder_name="raw", *args): """ - Return image as a bytes object + Return image as a bytes object. + + .. warning:: + + This method returns the raw image data from the internal + storage. For compressed image data (e.g. PNG, JPEG) use + :meth:`~.save`, with a BytesIO parameter for in-memory + data. :param encoder_name: What encoder to use. The default is to use the standard "raw" encoder. @@ -677,18 +694,9 @@ class Image(object): return b"".join(data) - # Declare tostring as alias to tobytes def tostring(self, *args, **kw): - """Deprecated alias to tobytes. - - .. deprecated:: 2.0 - """ - warnings.warn( - 'tostring() is deprecated. Please call tobytes() instead.', - DeprecationWarning, - stacklevel=2, - ) - return self.tobytes(*args, **kw) + raise Exception("tostring() has been removed. " + + "Please call tobytes() instead.") def tobitmap(self, name="image"): """ @@ -738,14 +746,8 @@ class Image(object): raise ValueError("cannot decode image data") def fromstring(self, *args, **kw): - """Deprecated alias to frombytes. - - .. deprecated:: 2.0 - """ - warnings.warn( - 'fromstring() is deprecated. Please call frombytes() instead.', - DeprecationWarning) - return self.frombytes(*args, **kw) + raise Exception("fromstring() has been removed. " + + "Please call frombytes() instead.") def load(self): """ @@ -818,7 +820,7 @@ class Image(object): :param mode: The requested mode. See: :ref:`concept-modes`. :param matrix: An optional conversion matrix. If given, this - should be 4- or 16-tuple containing floating point values. + should be 4- or 12-tuple containing floating point values. :param dither: Dithering method, used when converting from mode "RGB" to "P" or from "RGB" or "L" to "1". Available methods are NONE or FLOYDSTEINBERG (default). @@ -877,6 +879,12 @@ class Image(object): trns_im = Image()._new(core.new(self.mode, (1, 1))) if self.mode == 'P': trns_im.putpalette(self.palette) + if type(t) == tuple: + try: + t = trns_im.palette.getcolor(t) + except: + raise ValueError("Couldn't allocate a palette "+ + "color for transparency") trns_im.putpixel((0, 0), t) if mode in ('L', 'RGB'): @@ -1004,6 +1012,8 @@ class Image(object): im = self.im.copy() return self._new(im) + __copy__ = copy + def crop(self, box=None): """ Returns a rectangular region from this image. The box is a @@ -1244,27 +1254,8 @@ class Image(object): return self.im.histogram() def offset(self, xoffset, yoffset=None): - """ - .. deprecated:: 2.0 - - .. note:: New code should use :py:func:`PIL.ImageChops.offset`. - - Returns a copy of the image where the data has been offset by the given - distances. Data wraps around the edges. If **yoffset** is omitted, it - is assumed to be equal to **xoffset**. - - :param xoffset: The horizontal distance. - :param yoffset: The vertical distance. If omitted, both - distances are set to the same value. - :returns: An :py:class:`~PIL.Image.Image` object. - """ - if warnings: - warnings.warn( - "'offset' is deprecated; use 'ImageChops.offset' instead", - DeprecationWarning, stacklevel=2 - ) - from PIL import ImageChops - return ImageChops.offset(self, xoffset, yoffset) + raise Exception("offset() has been removed. " + + "Please call ImageChops.offset() instead.") def paste(self, im, box=None, mask=None): """ @@ -1316,7 +1307,7 @@ class Image(object): box = (0, 0) + self.size if len(box) == 2: - # lower left corner given; get size from image or mask + # upper left corner given; get size from image or mask if isImageType(im): size = im.size elif isImageType(mask): @@ -1617,7 +1608,7 @@ class Image(object): if self.mode in ("1", "P"): resample = NEAREST - return self._new(self.im.rotate(angle, resample)) + return self._new(self.im.rotate(angle, resample, expand)) def save(self, fp, format=None, **params): """ @@ -1636,7 +1627,7 @@ class Image(object): implement the ``seek``, ``tell``, and ``write`` methods, and be opened in binary mode. - :param fp: File name or file object. + :param fp: A filename (string), pathlib.Path object or file object. :param format: Optional format override. If omitted, the format to use is determined from the filename extension. If a file object was used instead of a filename, this @@ -1649,17 +1640,27 @@ class Image(object): may have been created, and may contain partial data. """ + filename = "" + open_fp = False if isPath(fp): filename = fp - else: - if hasattr(fp, "name") and isPath(fp.name): - filename = fp.name - else: - filename = "" + open_fp = True + elif sys.version_info >= (3, 4): + from pathlib import Path + if isinstance(fp, Path): + filename = str(fp) + open_fp = True + elif hasattr(fp, "name") and isPath(fp.name): + # only set the name for metadata purposes + filename = fp.name # may mutate self! self.load() + save_all = False + if 'save_all' in params: + save_all = params['save_all'] + del params['save_all'] self.encoderinfo = params self.encoderconfig = () @@ -1668,32 +1669,25 @@ class Image(object): ext = os.path.splitext(filename)[1].lower() if not format: - try: - format = EXTENSION[ext] - except KeyError: + if ext not in EXTENSION: init() - try: - format = EXTENSION[ext] - except KeyError: - raise KeyError(ext) # unknown extension + format = EXTENSION[ext] - try: - save_handler = SAVE[format.upper()] - except KeyError: + if format.upper() not in SAVE: init() - save_handler = SAVE[format.upper()] # unknown format - - if isPath(fp): - fp = builtins.open(fp, "wb") - close = 1 + if save_all: + save_handler = SAVE_ALL[format.upper()] else: - close = 0 + save_handler = SAVE[format.upper()] + + if open_fp: + fp = builtins.open(filename, "wb") try: save_handler(self, fp, filename) finally: # do what we can to clean up - if close: + if open_fp: fp.close() def seek(self, frame): @@ -1935,6 +1929,20 @@ class Image(object): im = self.im.effect_spread(distance) return self._new(im) + def toqimage(self): + """Returns a QImage copy of this image""" + from PIL import ImageQt + if not ImageQt.qt_is_installed: + raise ImportError("Qt bindings are not installed") + return ImageQt.toqimage(self) + + def toqpixmap(self): + """Returns a QPixmap copy of this image""" + from PIL import ImageQt + if not ImageQt.qt_is_installed: + raise ImportError("Qt bindings are not installed") + return ImageQt.toqpixmap(self) + # -------------------------------------------------------------------- # Lazy operations @@ -1945,7 +1953,9 @@ class _ImageCrop(Image): Image.__init__(self) - x0, y0, x1, y1 = box + # Round to nearest integer, runs int(round(x)) when unpacking + x0, y0, x1, y1 = map(int, map(round, box)) + if x1 < x0: x1 = x0 if y1 < y0: @@ -2063,16 +2073,8 @@ def frombytes(mode, size, data, decoder_name="raw", *args): def fromstring(*args, **kw): - """Deprecated alias to frombytes. - - .. deprecated:: 2.0 - """ - warnings.warn( - 'fromstring() is deprecated. Please call frombytes() instead.', - DeprecationWarning, - stacklevel=2 - ) - return frombytes(*args, **kw) + raise Exception("fromstring() has been removed. " + + "Please call frombytes() instead.") def frombuffer(mode, size, data, decoder_name="raw", *args): @@ -2089,7 +2091,7 @@ def frombuffer(mode, size, data, decoder_name="raw", *args): **BytesIO** object, and use :py:func:`~PIL.Image.open` to load it. In the current version, the default parameters used for the "raw" decoder - differs from that used for :py:func:`~PIL.Image.fromstring`. This is a + differs from that used for :py:func:`~PIL.Image.frombytes`. This is a bug, and will probably be fixed in a future release. The current release issues a warning if you do this; to disable the warning, you should provide the full set of parameters. See below for details. @@ -2116,13 +2118,12 @@ def frombuffer(mode, size, data, decoder_name="raw", *args): if decoder_name == "raw": if args == (): - if warnings: - warnings.warn( - "the frombuffer defaults may change in a future release; " - "for portability, change the call to read:\n" - " frombuffer(mode, size, data, 'raw', mode, 0, 1)", - RuntimeWarning, stacklevel=2 - ) + warnings.warn( + "the frombuffer defaults may change in a future release; " + "for portability, change the call to read:\n" + " frombuffer(mode, size, data, 'raw', mode, 0, 1)", + RuntimeWarning, stacklevel=2 + ) args = mode, 0, -1 # may change to (mode, 0, 1) post-1.1.6 if args[0] in _MAPMODES: im = new(mode, (1, 1)) @@ -2184,6 +2185,22 @@ def fromarray(obj, mode=None): return frombuffer(mode, size, obj, "raw", rawmode, 0, 1) + +def fromqimage(im): + """Creates an image instance from a QImage image""" + from PIL import ImageQt + if not ImageQt.qt_is_installed: + raise ImportError("Qt bindings are not installed") + return ImageQt.fromqimage(im) + + +def fromqpixmap(im): + """Creates an image instance from a QPixmap image""" + from PIL import ImageQt + if not ImageQt.qt_is_installed: + raise ImportError("Qt bindings are not installed") + return ImageQt.fromqpixmap(im) + _fromarray_typemap = { # (shape, typestr) => mode, rawmode # first two members of shape are set to one @@ -2231,9 +2248,10 @@ def open(fp, mode="r"): :py:meth:`~PIL.Image.Image.load` method). See :py:func:`~PIL.Image.new`. - :param file: A filename (string) or a file object. The file object - must implement :py:meth:`~file.read`, :py:meth:`~file.seek`, and - :py:meth:`~file.tell` methods, and be opened in binary mode. + :param fp: A filename (string), pathlib.Path object or a file object. + The file object must implement :py:meth:`~file.read`, + :py:meth:`~file.seek`, and :py:meth:`~file.tell` methods, + and be opened in binary mode. :param mode: The mode. If given, this argument must be "r". :returns: An :py:class:`~PIL.Image.Image` object. :exception IOError: If the file cannot be found, or the image cannot be @@ -2243,11 +2261,15 @@ def open(fp, mode="r"): if mode != "r": raise ValueError("bad mode %r" % mode) + filename = "" if isPath(fp): filename = fp - fp = builtins.open(fp, "rb") - else: - filename = "" + elif sys.version_info >= (3, 4): + from pathlib import Path + if isinstance(fp, Path): + filename = str(fp.resolve()) + if filename: + fp = builtins.open(filename, "rb") try: fp.seek(0) @@ -2258,21 +2280,7 @@ def open(fp, mode="r"): preinit() - for i in ID: - try: - factory, accept = OPEN[i] - if not accept or accept(prefix): - fp.seek(0) - im = factory(fp, filename) - _decompression_bomb_check(im.size) - return im - except (SyntaxError, IndexError, TypeError, struct.error): - # import traceback - # traceback.print_exc() - pass - - if init(): - + def _open_core(fp, filename, prefix): for i in ID: try: factory, accept = OPEN[i] @@ -2282,23 +2290,34 @@ def open(fp, mode="r"): _decompression_bomb_check(im.size) return im except (SyntaxError, IndexError, TypeError, struct.error): - # import traceback - # traceback.print_exc() - pass + # Leave disabled by default, spams the logs with image + # opening failures that are entirely expected. + # logger.debug("", exc_info=True) + continue + return None + + im = _open_core(fp, filename, prefix) + + if im is None: + if init(): + im = _open_core(fp, filename, prefix) + + if im: + return im raise IOError("cannot identify image file %r" % (filename if filename else fp)) - # # Image processing. + def alpha_composite(im1, im2): """ Alpha composite im2 over im1. - :param im1: The first image. - :param im2: The second image. Must have the same mode and size as + :param im1: The first image. Must have mode RGBA. + :param im2: The second image. Must have mode RGBA, and the same size as the first image. :returns: An :py:class:`~PIL.Image.Image` object. """ @@ -2430,6 +2449,18 @@ def register_save(id, driver): SAVE[id.upper()] = driver +def register_save_all(id, driver): + """ + Registers an image function to save all the frames + of a multiframe format. This function should not be + used in application code. + + :param id: An image format identifier. + :param driver: A function to save images in this format. + """ + SAVE_ALL[id.upper()] = driver + + def register_extension(id, extension): """ Registers an image extension. This function should not be diff --git a/PIL/ImageCms.py b/PIL/ImageCms.py index ebf127df3..ba5504acb 100644 --- a/PIL/ImageCms.py +++ b/PIL/ImageCms.py @@ -16,6 +16,7 @@ # below for the original description. from __future__ import print_function +import sys DESCRIPTION = """ pyCMS @@ -240,7 +241,6 @@ def get_display_profile(handle=None): :returns: None if the profile is not known. """ - import sys if sys.platform == "win32": from PIL import ImageWin if isinstance(handle, ImageWin.HDC): @@ -943,7 +943,6 @@ def versions(): (pyCMS) Fetches versions. """ - import sys return ( VERSION, core.littlecms_version, sys.version.split()[0], Image.VERSION @@ -954,10 +953,9 @@ def versions(): if __name__ == "__main__": # create a cheap manual from the __doc__ strings for the functions above - from PIL import ImageCms print(__doc__) - for f in dir(ImageCms): + for f in dir(sys.modules[__name__]): doc = None try: exec("doc = %s.__doc__" % (f)) diff --git a/PIL/ImageDraw.py b/PIL/ImageDraw.py index 1fc5b4d61..1d5f53746 100644 --- a/PIL/ImageDraw.py +++ b/PIL/ImageDraw.py @@ -31,22 +31,18 @@ # import numbers +import warnings from PIL import Image, ImageColor from PIL._util import isStringType -try: - import warnings -except ImportError: - warnings = None - - ## # A simple 2D drawing interface for PIL images. #

# Application code should use the Draw factory, instead of # directly. + class ImageDraw(object): ## @@ -90,38 +86,17 @@ class ImageDraw(object): self.fill = 0 self.font = None - ## - # Set the default pen color. - def setink(self, ink): - # compatibility - if warnings: - warnings.warn( - "'setink' is deprecated; use keyword arguments instead", - DeprecationWarning, stacklevel=2 - ) - if isStringType(ink): - ink = ImageColor.getcolor(ink, self.mode) - if self.palette and not isinstance(ink, numbers.Number): - ink = self.palette.getcolor(ink) - self.ink = self.draw.draw_ink(ink, self.mode) - - ## - # Set the default background color. + raise Exception("setink() has been removed. " + + "Please use keyword arguments instead.") def setfill(self, onoff): - # compatibility - if warnings: - warnings.warn( - "'setfill' is deprecated; use keyword arguments instead", - DeprecationWarning, stacklevel=2 - ) - self.fill = onoff - - ## - # Set the default font. + raise Exception("setfill() has been removed. " + + "Please use keyword arguments instead.") def setfont(self, font): + warnings.warn("setfont() is deprecated. " + + "Please set the attribute directly instead.") # compatibility self.font = font @@ -256,7 +231,20 @@ class ImageDraw(object): ## # Draw text. - def text(self, xy, text, fill=None, font=None, anchor=None): + def _multiline_check(self, text): + split_character = "\n" if isinstance(text, type("")) else b"\n" + + return split_character in text + + def _multiline_split(self, text): + split_character = "\n" if isinstance(text, type("")) else b"\n" + + return text.split(split_character) + + def text(self, xy, text, fill=None, font=None, anchor=None, *args, **kwargs): + if self._multiline_check(text): + return self.multiline_text(xy, text, fill, font, anchor, *args, **kwargs) + ink, fill = self._getink(fill) if font is None: font = self.getfont() @@ -273,14 +261,50 @@ class ImageDraw(object): mask = font.getmask(text) self.draw.draw_bitmap(xy, mask, ink) + def multiline_text(self, xy, text, fill=None, font=None, anchor=None, + spacing=4, align="left"): + 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) + widths.append(line_width) + max_width = max(max_width, line_width) + left, top = xy + for idx, line in enumerate(lines): + if align == "left": + pass # left = x + elif align == "center": + left += (max_width - widths[idx]) / 2.0 + elif align == "right": + left += (max_width - widths[idx]) + else: + assert False, 'align must be "left", "center" or "right"' + self.text((left, top), line, fill, font, anchor) + top += line_spacing + left = xy[0] + ## # Get the size of a given string, in pixels. - def textsize(self, text, font=None): + def textsize(self, text, font=None, *args, **kwargs): + if self._multiline_check(text): + return self.multiline_textsize(text, font, *args, **kwargs) + if font is None: font = self.getfont() return font.getsize(text) + def multiline_textsize(self, text, font=None, spacing=4): + 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) + max_width = max(max_width, line_width) + return max_width, len(lines)*line_spacing + ## # A simple 2D drawing interface for PIL images. @@ -301,7 +325,7 @@ def Draw(im, mode=None): # experimental access to the outline API try: Outline = Image.core.outline -except: +except AttributeError: Outline = None diff --git a/PIL/ImageFile.py b/PIL/ImageFile.py index b1d261166..9617ffb3b 100644 --- a/PIL/ImageFile.py +++ b/PIL/ImageFile.py @@ -32,7 +32,7 @@ from PIL._util import isPath import io import os import sys -import traceback +import struct MAXBLOCK = 65536 @@ -95,21 +95,11 @@ class ImageFile(Image.Image): try: self._open() - except IndexError as v: # end of data - if Image.DEBUG > 1: - traceback.print_exc() - raise SyntaxError(v) - except TypeError as v: # end of data (ord) - if Image.DEBUG > 1: - traceback.print_exc() - raise SyntaxError(v) - except KeyError as v: # unsupported mode - if Image.DEBUG > 1: - traceback.print_exc() - raise SyntaxError(v) - except EOFError as v: # got header but not the first frame - if Image.DEBUG > 1: - traceback.print_exc() + except (IndexError, # end of data + TypeError, # end of data (ord) + KeyError, # unsupported mode + EOFError, # got header but not the first frame + struct.error) as v: raise SyntaxError(v) if not self.mode or self.size[0] <= 0: @@ -173,10 +163,10 @@ class ImageFile(Image.Image): else: # use mmap, if possible import mmap - file = open(self.filename, "r+") + fp = open(self.filename, "r+") size = os.path.getsize(self.filename) # FIXME: on Unix, use PROT_READ etc - self.map = mmap.mmap(file.fileno(), size) + self.map = mmap.mmap(fp.fileno(), size) self.im = Image.core.map_buffer( self.map, self.size, d, e, o, a ) @@ -196,9 +186,6 @@ class ImageFile(Image.Image): except AttributeError: prefix = b"" - # Buffer length read; assign a default value - t = 0 - for d, e, o, a in self.tile: d = Image._getdecoder(self.mode, d, a, self.decoderconfig) seek(o) @@ -207,15 +194,14 @@ class ImageFile(Image.Image): except ValueError: continue b = prefix - t = len(b) while True: try: s = read(self.decodermaxblock) - except IndexError as ie: # truncated png/gif + except (IndexError, struct.error): # truncated png/gif if LOAD_TRUNCATED_IMAGES: break else: - raise IndexError(ie) + raise IOError("image file is truncated") if not s and not d.handles_eof: # truncated jpeg self.tile = [] @@ -236,7 +222,6 @@ class ImageFile(Image.Image): if n < 0: break b = b[n:] - t = t + n # Need to cleanup here to prevent leaks in PyPy d.cleanup() @@ -245,7 +230,7 @@ class ImageFile(Image.Image): self.fp = None # might be shared - if not self.map and (not LOAD_TRUNCATED_IMAGES or t == 0) and e < 0: + if not self.map and not LOAD_TRUNCATED_IMAGES and e < 0: # still raised if decoder fails to return anything raise_ioerror(e) @@ -468,6 +453,9 @@ def _save(im, fp, tile, bufsize=0): # But, it would need at least the image size in most cases. RawEncode is # a tricky case. bufsize = max(MAXBLOCK, bufsize, im.size[0] * 4) # see RawEncode.c + if fp == sys.stdout: + fp.flush() + return try: fh = fp.fileno() fp.flush() @@ -497,10 +485,8 @@ def _save(im, fp, tile, bufsize=0): if s < 0: raise IOError("encoder error %d when writing image file" % s) e.cleanup() - try: + if hasattr(fp, "flush"): fp.flush() - except: - pass def _safe_read(fp, size): diff --git a/PIL/ImageFileIO.py b/PIL/ImageFileIO.py deleted file mode 100644 index e57d3f43e..000000000 --- a/PIL/ImageFileIO.py +++ /dev/null @@ -1,40 +0,0 @@ -# -# The Python Imaging Library. -# $Id$ -# -# kludge to get basic ImageFileIO functionality -# -# History: -# 1998-08-06 fl Recreated -# -# Copyright (c) Secret Labs AB 1998-2002. -# -# See the README file for information on usage and redistribution. -# -""" -The **ImageFileIO** module can be used to read an image from a -socket, or any other stream device. - -Deprecated. New code should use the :class:`PIL.ImageFile.Parser` -class in the :mod:`PIL.ImageFile` module instead. - -.. seealso:: modules :class:`PIL.ImageFile.Parser` -""" - -from io import BytesIO - - -class ImageFileIO(BytesIO): - def __init__(self, fp): - """ - Adds buffering to a stream file object, in order to - provide **seek** and **tell** methods required - by the :func:`PIL.Image.Image.open` method. The stream object must - implement **read** and **close** methods. - - :param fp: Stream file handle. - - .. seealso:: modules :func:`PIL.Image.open` - """ - data = fp.read() - BytesIO.__init__(self, data) diff --git a/PIL/ImageFont.py b/PIL/ImageFont.py index c6e7cb4d9..af1166dde 100644 --- a/PIL/ImageFont.py +++ b/PIL/ImageFont.py @@ -30,11 +30,6 @@ from PIL._util import isDirectory, isPath import os import sys -try: - import warnings -except ImportError: - warnings = None - class _imagingft_not_installed(object): # module placeholder @@ -67,7 +62,7 @@ class ImageFont(object): def _load_pilfont(self, filename): - file = open(filename, "rb") + fp = open(filename, "rb") for ext in (".png", ".gif", ".pbm"): try: @@ -83,7 +78,7 @@ class ImageFont(object): self.file = fullname - return self._load_pilfont_data(file, image) + return self._load_pilfont_data(fp, image) def _load_pilfont_data(self, file, image): @@ -121,15 +116,8 @@ class ImageFont(object): class FreeTypeFont(object): "FreeType font wrapper (requires _imagingft service)" - def __init__(self, font=None, size=10, index=0, encoding="", file=None): + def __init__(self, font=None, size=10, index=0, encoding=""): # FIXME: use service provider instead - if file: - if warnings: - warnings.warn( - 'file parameter deprecated, ' - 'please use font parameter instead.', - DeprecationWarning) - font = file self.path = font self.size = size @@ -171,7 +159,7 @@ class FreeTypeFont(object): using any specified arguments to override the settings. Parameters are identical to the parameters used to initialize this - object, minus the deprecated 'file' argument. + object. :return: A FreeTypeFont object. """ @@ -225,7 +213,7 @@ def load(filename): return f -def truetype(font=None, size=10, index=0, encoding="", filename=None): +def truetype(font=None, size=10, index=0, encoding=""): """ Load a TrueType or OpenType font file, and create a font object. This function loads a font object from the given file, and creates @@ -243,19 +231,10 @@ def truetype(font=None, size=10, index=0, encoding="", filename=None): Symbol), "ADOB" (Adobe Standard), "ADBE" (Adobe Expert), and "armn" (Apple Roman). See the FreeType documentation for more information. - :param filename: Deprecated. Please use font instead. :return: A font object. :exception IOError: If the file could not be read. """ - if filename: - if warnings: - warnings.warn( - 'filename parameter deprecated, ' - 'please use font parameter instead.', - DeprecationWarning) - font = filename - try: return FreeTypeFont(font, size, index, encoding) except IOError: @@ -275,7 +254,8 @@ def truetype(font=None, size=10, index=0, encoding="", filename=None): # According to the freedesktop spec, XDG_DATA_DIRS should # default to /usr/share lindirs = '/usr/share' - dirs += [os.path.join(lindir, "fonts") for lindir in lindirs.split(":")] + dirs += [os.path.join(lindir, "fonts") + for lindir in lindirs.split(":")] elif sys.platform == 'darwin': dirs += ['/Library/Fonts', '/System/Library/Fonts', os.path.expanduser('~/Library/Fonts')] diff --git a/PIL/ImageGrab.py b/PIL/ImageGrab.py index ef0135334..febdb2310 100644 --- a/PIL/ImageGrab.py +++ b/PIL/ImageGrab.py @@ -2,7 +2,7 @@ # The Python Imaging Library # $Id$ # -# screen grabber (windows only) +# screen grabber (OS X and Windows only) # # History: # 2001-04-26 fl created @@ -18,31 +18,40 @@ from PIL import Image import sys -if sys.platform != "win32": - raise ImportError("ImageGrab is Windows only") +if sys.platform not in ["win32", "darwin"]: + raise ImportError("ImageGrab is OS X and Windows only") -try: - # built-in driver (1.1.3 and later) +if sys.platform == "win32": grabber = Image.core.grabscreen -except AttributeError: - # stand-alone driver (pil plus) - import _grabscreen - grabber = _grabscreen.grab +elif sys.platform == "darwin": + import os + import tempfile + import subprocess def grab(bbox=None): - size, data = grabber() - im = Image.frombytes( - "RGB", size, data, - # RGB, 32-bit line padding, origo in lower left corner - "raw", "BGR", (size[0]*3 + 3) & -4, -1 - ) + if sys.platform == "darwin": + f, file = tempfile.mkstemp('.png') + os.close(f) + subprocess.call(['screencapture', '-x', file]) + im = Image.open(file) + im.load() + os.unlink(file) + else: + size, data = grabber() + im = Image.frombytes( + "RGB", size, data, + # RGB, 32-bit line padding, origo in lower left corner + "raw", "BGR", (size[0]*3 + 3) & -4, -1 + ) if bbox: im = im.crop(bbox) return im def grabclipboard(): + if sys.platform == "darwin": + raise NotImplementedError("Method is not implemented on OS X") debug = 0 # temporary interface data = Image.core.grabclipboard(debug) if isinstance(data, bytes): diff --git a/PIL/ImageMorph.py b/PIL/ImageMorph.py index 6f92e9e67..902ed8db7 100644 --- a/PIL/ImageMorph.py +++ b/PIL/ImageMorph.py @@ -198,6 +198,8 @@ class MorphOp(object): if self.lut is None: raise Exception('No operator loaded') + if image.mode != 'L': + raise Exception('Image must be binary, meaning it must use mode L') outimage = Image.new(image.mode, image.size, None) count = _imagingmorph.apply( bytes(self.lut), image.im.id, outimage.im.id) @@ -212,6 +214,8 @@ class MorphOp(object): if self.lut is None: raise Exception('No operator loaded') + if image.mode != 'L': + raise Exception('Image must be binary, meaning it must use mode L') return _imagingmorph.match(bytes(self.lut), image.im.id) def get_on_pixels(self, image): @@ -220,6 +224,8 @@ class MorphOp(object): Returns a list of tuples of (x,y) coordinates of all matching pixels.""" + if image.mode != 'L': + raise Exception('Image must be binary, meaning it must use mode L') return _imagingmorph.get_on_pixels(image.im.id) def load_lut(self, filename): diff --git a/PIL/ImagePalette.py b/PIL/ImagePalette.py index b2f51dd06..fdc5a46ca 100644 --- a/PIL/ImagePalette.py +++ b/PIL/ImagePalette.py @@ -17,12 +17,23 @@ # import array -import warnings from PIL import ImageColor class ImagePalette(object): - "Color palette for palette mapped images" + """ + Color palette for palette mapped images + + :param mode: The mode to use for the Palette. See: + :ref:`concept-modes`. Defaults to "RGB" + :param palette: An optional palette. If given, it must be a bytearray, + an array or a list of ints between 0-255 and of length ``size`` + times the number of colors in ``mode``. The list must be aligned + by channel (All R values must be contiguous in the list before G + and B values.) Defaults to 0 through 255 per channel. + :param size: An optional palette size. If given, it cannot be equal to + or greater than 256. Defaults to 0. + """ def __init__(self, mode="RGB", palette=None, size=0): self.mode = mode @@ -34,6 +45,18 @@ class ImagePalette(object): (size != 0 and size != len(self.palette))): raise ValueError("wrong palette size") + def copy(self): + new = ImagePalette() + + new.mode = self.mode + new.rawmode = self.rawmode + if self.palette is not None: + new.palette = self.palette[:] + new.colors = self.colors.copy() + new.dirty = self.dirty + + return new + def getdata(self): """ Get palette contents in format suitable # for the low-level @@ -56,7 +79,6 @@ class ImagePalette(object): return self.palette arr = array.array("B", self.palette) if hasattr(arr, 'tobytes'): - # py3k has a tobytes, tostring is deprecated. return arr.tobytes() return arr.tostring() @@ -125,26 +147,6 @@ def raw(rawmode, data): # -------------------------------------------------------------------- # Factories -def _make_linear_lut(black, white): - warnings.warn( - '_make_linear_lut() is deprecated. ' - 'Please call make_linear_lut() instead.', - DeprecationWarning, - stacklevel=2 - ) - return make_linear_lut(black, white) - - -def _make_gamma_lut(exp): - warnings.warn( - '_make_gamma_lut() is deprecated. ' - 'Please call make_gamma_lut() instead.', - DeprecationWarning, - stacklevel=2 - ) - return make_gamma_lut(exp) - - def make_linear_lut(black, white): lut = [] if black == 0: @@ -225,8 +227,8 @@ def load(filename): p = PaletteFile.PaletteFile(fp) lut = p.getpalette() except (SyntaxError, ValueError): - #import traceback - #traceback.print_exc() + # import traceback + # traceback.print_exc() pass if not lut: diff --git a/PIL/ImageQt.py b/PIL/ImageQt.py index 1723e3226..aece9d62a 100644 --- a/PIL/ImageQt.py +++ b/PIL/ImageQt.py @@ -18,82 +18,181 @@ from PIL import Image from PIL._util import isPath -import sys +from io import BytesIO -if 'PyQt4.QtGui' not in sys.modules: +qt_is_installed = True +qt_version = None +try: + from PyQt5.QtGui import QImage, qRgba, QPixmap + from PyQt5.QtCore import QBuffer, QIODevice + qt_version = '5' +except ImportError: try: - from PyQt5.QtGui import QImage, qRgba - except: + from PyQt4.QtGui import QImage, qRgba, QPixmap + from PyQt4.QtCore import QBuffer, QIODevice + qt_version = '4' + except ImportError: try: - from PyQt4.QtGui import QImage, qRgba - except: - from PySide.QtGui import QImage, qRgba - -else: #PyQt4 is used - from PyQt4.QtGui import QImage, qRgba - -## -# (Internal) Turns an RGB color into a Qt compatible color integer. + from PySide.QtGui import QImage, qRgba, QPixmap + from PySide.QtCore import QBuffer, QIODevice + qt_version = 'side' + except ImportError: + qt_is_installed = False def rgb(r, g, b, a=255): + """(Internal) Turns an RGB color into a Qt compatible color integer.""" # use qRgb to pack the colors, and then turn the resulting long # into a negative integer with the same bitpattern. return (qRgba(r, g, b, a) & 0xffffffff) +# :param im A PIL Image object, or a file name +# (given either as Python string or a PyQt string object) + +def fromqimage(im): + buffer = QBuffer() + buffer.open(QIODevice.ReadWrite) + # preserve alha channel with png + # otherwise ppm is more friendly with Image.open + if im.hasAlphaChannel(): + im.save(buffer, 'png') + else: + im.save(buffer, 'ppm') + + b = BytesIO() + try: + b.write(buffer.data()) + except TypeError: + # workaround for Python 2 + b.write(str(buffer.data())) + buffer.close() + b.seek(0) + + return Image.open(b) + + +def fromqpixmap(im): + return fromqimage(im) + # buffer = QBuffer() + # buffer.open(QIODevice.ReadWrite) + # # im.save(buffer) + # # What if png doesn't support some image features like animation? + # im.save(buffer, 'ppm') + # bytes_io = BytesIO() + # bytes_io.write(buffer.data()) + # buffer.close() + # bytes_io.seek(0) + # return Image.open(bytes_io) + + +def align8to32(bytes, width, mode): + """ + converts each scanline of data from 8 bit to 32 bit aligned + """ + + bits_per_pixel = { + '1': 1, + 'L': 8, + 'P': 8, + }[mode] + + # calculate bytes per line and the extra padding if needed + bits_per_line = bits_per_pixel * width + full_bytes_per_line, remaining_bits_per_line = divmod(bits_per_line, 8) + bytes_per_line = full_bytes_per_line + (1 if remaining_bits_per_line else 0) + + extra_padding = -bytes_per_line % 4 + + # already 32 bit aligned by luck + if not extra_padding: + return bytes + + new_data = [] + for i in range(len(bytes) // bytes_per_line): + new_data.append(bytes[i*bytes_per_line:(i+1)*bytes_per_line] + b'\x00' * extra_padding) + + return b''.join(new_data) + + +def _toqclass_helper(im): + data = None + colortable = None + + # handle filename, if given instead of image name + if hasattr(im, "toUtf8"): + # FIXME - is this really the best way to do this? + if str is bytes: + im = unicode(im.toUtf8(), "utf-8") + else: + im = str(im.toUtf8(), "utf-8") + if isPath(im): + im = Image.open(im) + + if im.mode == "1": + format = QImage.Format_Mono + elif im.mode == "L": + format = QImage.Format_Indexed8 + colortable = [] + for i in range(256): + colortable.append(rgb(i, i, i)) + elif im.mode == "P": + format = QImage.Format_Indexed8 + colortable = [] + palette = im.getpalette() + for i in range(0, len(palette), 3): + colortable.append(rgb(*palette[i:i+3])) + elif im.mode == "RGB": + data = im.tobytes("raw", "BGRX") + format = QImage.Format_RGB32 + elif im.mode == "RGBA": + try: + data = im.tobytes("raw", "BGRA") + except SystemError: + # workaround for earlier versions + r, g, b, a = im.split() + im = Image.merge("RGBA", (b, g, r, a)) + format = QImage.Format_ARGB32 + else: + raise ValueError("unsupported image mode %r" % im.mode) + + # must keep a reference, or Qt will crash! + __data = data or align8to32(im.tobytes(), im.size[0], im.mode) + return { + 'data': __data, 'im': im, 'format': format, 'colortable': colortable + } + ## -# An PIL image wrapper for Qt. This is a subclass of PyQt4's QImage +# An PIL image wrapper for Qt. This is a subclass of PyQt's QImage # class. # # @param im A PIL Image object, or a file name (given either as Python # string or a PyQt string object). -class ImageQt(QImage): +if qt_is_installed: + class ImageQt(QImage): - def __init__(self, im): + def __init__(self, im): + im_data = _toqclass_helper(im) + QImage.__init__(self, + im_data['data'], im_data['im'].size[0], + im_data['im'].size[1], im_data['format']) + if im_data['colortable']: + self.setColorTable(im_data['colortable']) - data = None - colortable = None - # handle filename, if given instead of image name - if hasattr(im, "toUtf8"): - # FIXME - is this really the best way to do this? - im = unicode(im.toUtf8(), "utf-8") - if isPath(im): - im = Image.open(im) +def toqimage(im): + return ImageQt(im) - if im.mode == "1": - format = QImage.Format_Mono - elif im.mode == "L": - format = QImage.Format_Indexed8 - colortable = [] - for i in range(256): - colortable.append(rgb(i, i, i)) - elif im.mode == "P": - format = QImage.Format_Indexed8 - colortable = [] - palette = im.getpalette() - for i in range(0, len(palette), 3): - colortable.append(rgb(*palette[i:i+3])) - elif im.mode == "RGB": - data = im.tobytes("raw", "BGRX") - format = QImage.Format_RGB32 - elif im.mode == "RGBA": - try: - data = im.tobytes("raw", "BGRA") - except SystemError: - # workaround for earlier versions - r, g, b, a = im.split() - im = Image.merge("RGBA", (b, g, r, a)) - format = QImage.Format_ARGB32 - else: - raise ValueError("unsupported image mode %r" % im.mode) - # must keep a reference, or Qt will crash! - self.__data = data or im.tobytes() +def toqpixmap(im): + # # This doesn't work. For now using a dumb approach. + # im_data = _toqclass_helper(im) + # result = QPixmap(im_data['im'].size[0], im_data['im'].size[1]) + # result.loadFromData(im_data['data']) + # Fix some strange bug that causes + if im.mode == 'RGB': + im = im.convert('RGBA') - QImage.__init__(self, self.__data, im.size[0], im.size[1], format) - - if colortable: - self.setColorTable(colortable) + qimage = toqimage(im) + return QPixmap.fromImage(qimage) diff --git a/PIL/ImageSequence.py b/PIL/ImageSequence.py index 256bcbedb..1fc6e5de1 100644 --- a/PIL/ImageSequence.py +++ b/PIL/ImageSequence.py @@ -32,11 +32,25 @@ class Iterator(object): if not hasattr(im, "seek"): raise AttributeError("im must have seek method") self.im = im + self.position = 0 def __getitem__(self, ix): try: - if ix: - self.im.seek(ix) + self.im.seek(ix) return self.im except EOFError: raise IndexError # end of sequence + + def __iter__(self): + return self + + def __next__(self): + try: + self.im.seek(self.position) + self.position += 1 + return self.im + except EOFError: + raise StopIteration + + def next(self): + return self.__next__() diff --git a/PIL/ImageTransform.py b/PIL/ImageTransform.py index 81f90502c..de045bff3 100644 --- a/PIL/ImageTransform.py +++ b/PIL/ImageTransform.py @@ -76,7 +76,7 @@ class ExtentTransform(Transform): ## -# Define an quad image transform. +# Define a quad image transform. #

# Maps a quadrilateral (a region defined by four corners) from the # image to a rectangle of the given size. @@ -92,7 +92,7 @@ class QuadTransform(Transform): ## -# Define an mesh image transform. A mesh transform consists of one +# Define a mesh image transform. A mesh transform consists of one # or more individual quad transforms. # # @def MeshTransform(data) diff --git a/PIL/ImageWin.py b/PIL/ImageWin.py index bcb54bc3e..58894d604 100644 --- a/PIL/ImageWin.py +++ b/PIL/ImageWin.py @@ -17,7 +17,6 @@ # See the README file for information on usage and redistribution. # -import warnings from PIL import Image @@ -183,24 +182,13 @@ class Dib(object): """ return self.image.tobytes() - ## - # Deprecated aliases to frombytes & tobytes. - def fromstring(self, *args, **kw): - warnings.warn( - 'fromstring() is deprecated. Please call frombytes() instead.', - DeprecationWarning, - stacklevel=2 - ) - return self.frombytes(*args, **kw) + raise Exception("fromstring() has been removed. " + + "Please use frombytes() instead.") - def tostring(self): - warnings.warn( - 'tostring() is deprecated. Please call tobytes() instead.', - DeprecationWarning, - stacklevel=2 - ) - return self.tobytes() + def tostring(self, *args, **kw): + raise Exception("tostring() has been removed. " + + "Please use tobytes() instead.") ## diff --git a/PIL/ImtImagePlugin.py b/PIL/ImtImagePlugin.py index f512eb801..63e892483 100644 --- a/PIL/ImtImagePlugin.py +++ b/PIL/ImtImagePlugin.py @@ -15,12 +15,13 @@ # -__version__ = "0.2" - import re from PIL import Image, ImageFile +__version__ = "0.2" + + # # -------------------------------------------------------------------- @@ -88,7 +89,7 @@ class ImtImageFile(ImageFile.ImageFile): # # -------------------------------------------------------------------- -Image.register_open("IMT", ImtImageFile) +Image.register_open(ImtImageFile.format, ImtImageFile) # # no extension registered (".im" is simply too common) diff --git a/PIL/IptcImagePlugin.py b/PIL/IptcImagePlugin.py index 47c7e1936..56d1de424 100644 --- a/PIL/IptcImagePlugin.py +++ b/PIL/IptcImagePlugin.py @@ -17,13 +17,12 @@ from __future__ import print_function -__version__ = "0.3" - - from PIL import Image, ImageFile, _binary import os import tempfile +__version__ = "0.3" + i8 = _binary.i8 i16 = _binary.i16be i32 = _binary.i32be @@ -180,13 +179,13 @@ class IptcImageFile(ImageFile.ImageFile): finally: try: os.unlink(outfile) - except: + except OSError: pass -Image.register_open("IPTC", IptcImageFile) +Image.register_open(IptcImageFile.format, IptcImageFile) -Image.register_extension("IPTC", ".iim") +Image.register_extension(IptcImageFile.format, ".iim") ## @@ -218,7 +217,7 @@ def getiptcinfo(im): while app[offset:offset+4] == b"8BIM": offset += 4 # resource code - code = JpegImagePlugin.i16(app, offset) + code = i16(app, offset) offset += 2 # resource name (usually empty) name_len = i8(app[offset]) @@ -227,7 +226,7 @@ def getiptcinfo(im): if offset & 1: offset += 1 # resource data block - size = JpegImagePlugin.i32(app, offset) + size = i32(app, offset) offset += 4 if code == 0x0404: # 0x0404 contains IPTC/NAA data diff --git a/PIL/Jpeg2KImagePlugin.py b/PIL/Jpeg2KImagePlugin.py index ed3e1530a..238500448 100644 --- a/PIL/Jpeg2KImagePlugin.py +++ b/PIL/Jpeg2KImagePlugin.py @@ -84,7 +84,8 @@ def _parse_jp2_header(fp): size = None mode = None bpc = None - + nc = None + hio = io.BytesIO(header) while True: lbox, tbox = struct.unpack('>I4s', hio.read(8)) @@ -141,6 +142,9 @@ def _parse_jp2_header(fp): mode = 'RGBA' break + if size is None or mode is None: + raise SyntaxError("Malformed jp2 header") + return (size, mode) ## @@ -262,15 +266,15 @@ def _save(im, fp, filename): # ------------------------------------------------------------ # Registry stuff -Image.register_open('JPEG2000', Jpeg2KImageFile, _accept) -Image.register_save('JPEG2000', _save) +Image.register_open(Jpeg2KImageFile.format, Jpeg2KImageFile, _accept) +Image.register_save(Jpeg2KImageFile.format, _save) -Image.register_extension('JPEG2000', '.jp2') -Image.register_extension('JPEG2000', '.j2k') -Image.register_extension('JPEG2000', '.jpc') -Image.register_extension('JPEG2000', '.jpf') -Image.register_extension('JPEG2000', '.jpx') -Image.register_extension('JPEG2000', '.j2c') +Image.register_extension(Jpeg2KImageFile.format, '.jp2') +Image.register_extension(Jpeg2KImageFile.format, '.j2k') +Image.register_extension(Jpeg2KImageFile.format, '.jpc') +Image.register_extension(Jpeg2KImageFile.format, '.jpf') +Image.register_extension(Jpeg2KImageFile.format, '.jpx') +Image.register_extension(Jpeg2KImageFile.format, '.j2c') -Image.register_mime('JPEG2000', 'image/jp2') -Image.register_mime('JPEG2000', 'image/jpx') +Image.register_mime(Jpeg2KImageFile.format, 'image/jp2') +Image.register_mime(Jpeg2KImageFile.format, 'image/jpx') diff --git a/PIL/JpegImagePlugin.py b/PIL/JpegImagePlugin.py index 5cae90073..386939ba7 100644 --- a/PIL/JpegImagePlugin.py +++ b/PIL/JpegImagePlugin.py @@ -32,12 +32,11 @@ # See the README file for information on usage and redistribution. # -__version__ = "0.6" - import array import struct import io -from struct import unpack +import warnings +from struct import unpack_from from PIL import Image, ImageFile, TiffImagePlugin, _binary from PIL.JpegPresets import presets from PIL._util import isStringType @@ -47,6 +46,8 @@ o8 = _binary.o8 i16 = _binary.i16be i32 = _binary.i32be +__version__ = "0.6" + # # Parser @@ -287,7 +288,7 @@ class JpegImageFile(ImageFile.ImageFile): s = self.fp.read(1) - if i8(s[0]) != 255: + if i8(s) != 255: raise SyntaxError("not a JPEG file") # Create attributes @@ -310,7 +311,7 @@ class JpegImageFile(ImageFile.ImageFile): i = i16(s) else: # Skip non-0xFF junk - s = b"\xff" + s = self.fp.read(1) continue if i in MARKER: @@ -378,7 +379,7 @@ class JpegImageFile(ImageFile.ImageFile): finally: try: os.unlink(path) - except: + except OSError: pass self.mode = self.im.mode @@ -393,11 +394,17 @@ class JpegImageFile(ImageFile.ImageFile): return _getmp(self) -def _fixup(value): - # Helper function for _getexif() and _getmp() - if len(value) == 1: - return value[0] - return value +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 type(value) != type({}): + return value[0] + except: pass + return value + + return dict([(k, _fixup(v)) for k, v in src_dict.items()]) def _getexif(self): @@ -413,33 +420,35 @@ def _getexif(self): return None file = io.BytesIO(data[6:]) head = file.read(8) - exif = {} # process dictionary - info = TiffImagePlugin.ImageFileDirectory(head) + info = TiffImagePlugin.ImageFileDirectory_v1(head) info.load(file) - for key, value in info.items(): - exif[key] = _fixup(value) + 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. file.seek(exif[0x8769]) - except KeyError: + except (KeyError, TypeError): pass else: - info = TiffImagePlugin.ImageFileDirectory(head) + info = TiffImagePlugin.ImageFileDirectory_v1(head) info.load(file) - for key, value in info.items(): - exif[key] = _fixup(value) + 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. file.seek(exif[0x8825]) - except KeyError: + except (KeyError, TypeError): pass else: - info = TiffImagePlugin.ImageFileDirectory(head) + info = TiffImagePlugin.ImageFileDirectory_v1(head) info.load(file) - exif[0x8825] = gps = {} - for key, value in info.items(): - gps[key] = _fixup(value) + exif[0x8825] = _fixup_dict(info) + return exif @@ -457,23 +466,25 @@ def _getmp(self): file_contents = io.BytesIO(data) head = file_contents.read(8) endianness = '>' if head[:4] == b'\x4d\x4d\x00\x2a' else '<' - mp = {} # process dictionary - info = TiffImagePlugin.ImageFileDirectory(head) - info.load(file_contents) - for key, value in info.items(): - mp[key] = _fixup(value) + try: + info = TiffImagePlugin.ImageFileDirectory_v2(head) + info.load(file_contents) + mp = dict(info) + except: + raise SyntaxError("malformed MP Index (unreadable directory)") # it's an error not to have a number of images try: quant = mp[0xB001] except KeyError: raise SyntaxError("malformed MP Index (no number of images)") # get MP entries + mpentries = [] try: - mpentries = [] + rawmpentries = mp[0xB002] for entrynum in range(0, quant): - rawmpentry = mp[0xB002][entrynum * 16:(entrynum + 1) * 16] - unpackedentry = unpack('{0}LLLHH'.format(endianness), rawmpentry) + unpackedentry = unpack_from( + '{0}LLLHH'.format(endianness), rawmpentries, entrynum * 16) labels = ('Attribute', 'Size', 'DataOffset', 'EntryNo1', 'EntryNo2') mpentry = dict(zip(labels, unpackedentry)) @@ -527,14 +538,14 @@ RAWMODE = { "YCbCr": "YCbCr", } -zigzag_index = ( 0, 1, 5, 6, 14, 15, 27, 28, - 2, 4, 7, 13, 16, 26, 29, 42, - 3, 8, 12, 17, 25, 30, 41, 43, - 9, 11, 18, 24, 31, 40, 44, 53, - 10, 19, 23, 32, 39, 45, 52, 54, - 20, 22, 33, 38, 46, 51, 55, 60, - 21, 34, 37, 47, 50, 56, 59, 61, - 35, 36, 48, 49, 57, 58, 62, 63) +zigzag_index = (0, 1, 5, 6, 14, 15, 27, 28, + 2, 4, 7, 13, 16, 26, 29, 42, + 3, 8, 12, 17, 25, 30, 41, 43, + 9, 11, 18, 24, 31, 40, 44, 53, + 10, 19, 23, 32, 39, 45, 52, 54, + 20, 22, 33, 38, 46, 51, 55, 60, + 21, 34, 37, 47, 50, 56, 59, 61, + 35, 36, 48, 49, 57, 58, 62, 63) samplings = {(1, 1, 1, 1, 1, 1): 0, (2, 1, 1, 1, 1, 1): 1, @@ -681,7 +692,7 @@ def _save(im, fp, filename): # if we optimize, libjpeg needs a buffer big enough to hold the whole image # in a shot. Guessing on the size, at im.size bytes. (raw pizel size is # channels*size, this is a value that's been used in a django patch. - # https://github.com/jdriscoll/django-imagekit/issues/50 + # https://github.com/matthewwithanm/django-imagekit/issues/50 bufsize = 0 if "optimize" in info or "progressive" in info or "progression" in info: # keep sets quality to 0, but the actual value may be high. @@ -705,7 +716,7 @@ def _save_cjpeg(im, fp, filename): subprocess.check_call(["cjpeg", "-outfile", filename, tempfile]) try: os.unlink(tempfile) - except: + except OSError: pass @@ -713,8 +724,8 @@ def _save_cjpeg(im, fp, filename): # Factory for making JPEG and MPO instances def jpeg_factory(fp=None, filename=None): im = JpegImageFile(fp, filename) - mpheader = im._getmp() try: + mpheader = im._getmp() if mpheader[45057] > 1: # It's actually an MPO from .MpoImagePlugin import MpoImageFile @@ -722,18 +733,21 @@ def jpeg_factory(fp=None, filename=None): except (TypeError, IndexError): # It is really a JPEG pass + except SyntaxError: + warnings.warn("Image appears to be a malformed MPO file, it will be " + "interpreted as a base JPEG file") return im # -------------------------------------------------------------------q- # Registry stuff -Image.register_open("JPEG", jpeg_factory, _accept) -Image.register_save("JPEG", _save) +Image.register_open(JpegImageFile.format, jpeg_factory, _accept) +Image.register_save(JpegImageFile.format, _save) -Image.register_extension("JPEG", ".jfif") -Image.register_extension("JPEG", ".jpe") -Image.register_extension("JPEG", ".jpg") -Image.register_extension("JPEG", ".jpeg") +Image.register_extension(JpegImageFile.format, ".jfif") +Image.register_extension(JpegImageFile.format, ".jpe") +Image.register_extension(JpegImageFile.format, ".jpg") +Image.register_extension(JpegImageFile.format, ".jpeg") -Image.register_mime("JPEG", "image/jpeg") +Image.register_mime(JpegImageFile.format, "image/jpeg") diff --git a/PIL/JpegPresets.py b/PIL/JpegPresets.py index 67af9ac9a..ece33bbbe 100644 --- a/PIL/JpegPresets.py +++ b/PIL/JpegPresets.py @@ -27,7 +27,7 @@ Subsampling Subsampling is the practice of encoding images by implementing less resolution for chroma information than for luma information. -(ref.: http://en.wikipedia.org/wiki/Chroma_subsampling) +(ref.: https://en.wikipedia.org/wiki/Chroma_subsampling) Possible subsampling values are 0, 1 and 2 that correspond to 4:4:4, 4:2:2 and 4:1:1 (or 4:2:0?). @@ -41,8 +41,8 @@ Quantization tables They are values use by the DCT (Discrete cosine transform) to remove *unnecessary* information from the image (the lossy part of the compression). -(ref.: http://en.wikipedia.org/wiki/Quantization_matrix#Quantization_matrices, -http://en.wikipedia.org/wiki/JPEG#Quantization) +(ref.: https://en.wikipedia.org/wiki/Quantization_matrix#Quantization_matrices, +https://en.wikipedia.org/wiki/JPEG#Quantization) You can get the quantization tables of a JPEG with:: @@ -62,13 +62,13 @@ The tables format between im.quantization and quantization in presets differ in You can convert the dict format to the preset format with the `JpegImagePlugin.convert_dict_qtables(dict_qtables)` function. -Libjpeg ref.: http://www.jpegcameras.com/libjpeg/libjpeg-3.html +Libjpeg ref.: http://web.archive.org/web/20120328125543/http://www.jpegcameras.com/libjpeg/libjpeg-3.html """ presets = { 'web_low': {'subsampling': 2, # "4:1:1" - 'quantization': [ + 'quantization': [ [20, 16, 25, 39, 50, 46, 62, 68, 16, 18, 23, 38, 38, 53, 65, 68, 25, 23, 31, 38, 53, 65, 68, 68, @@ -85,9 +85,9 @@ presets = { 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68] - ]}, + ]}, 'web_medium': {'subsampling': 2, # "4:1:1" - 'quantization': [ + 'quantization': [ [16, 11, 11, 16, 23, 27, 31, 30, 11, 12, 12, 15, 20, 23, 23, 30, 11, 12, 13, 16, 23, 26, 35, 47, @@ -104,10 +104,10 @@ presets = { 26, 26, 30, 39, 48, 63, 64, 64, 38, 35, 46, 53, 64, 64, 64, 64, 48, 43, 53, 64, 64, 64, 64, 64] - ]}, + ]}, 'web_high': {'subsampling': 0, # "4:4:4" - 'quantization': [ - [ 6, 4, 4, 6, 9, 11, 12, 16, + 'quantization': [ + [6, 4, 4, 6, 9, 11, 12, 16, 4, 5, 5, 6, 8, 10, 12, 12, 4, 5, 5, 6, 10, 12, 14, 19, 6, 6, 6, 11, 12, 15, 19, 28, @@ -115,7 +115,7 @@ presets = { 11, 10, 12, 15, 20, 27, 31, 31, 12, 12, 14, 19, 27, 31, 31, 31, 16, 12, 19, 28, 31, 31, 31, 31], - [ 7, 7, 13, 24, 26, 31, 31, 31, + [7, 7, 13, 24, 26, 31, 31, 31, 7, 12, 16, 21, 31, 31, 31, 31, 13, 16, 17, 31, 31, 31, 31, 31, 24, 21, 31, 31, 31, 31, 31, 31, @@ -123,10 +123,10 @@ presets = { 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31] - ]}, + ]}, 'web_very_high': {'subsampling': 0, # "4:4:4" - 'quantization': [ - [ 2, 2, 2, 2, 3, 4, 5, 6, + 'quantization': [ + [2, 2, 2, 2, 3, 4, 5, 6, 2, 2, 2, 2, 3, 4, 5, 6, 2, 2, 2, 2, 4, 5, 7, 9, 2, 2, 2, 4, 5, 7, 9, 12, @@ -134,7 +134,7 @@ presets = { 4, 4, 5, 7, 10, 12, 12, 12, 5, 5, 7, 9, 12, 12, 12, 12, 6, 6, 9, 12, 12, 12, 12, 12], - [ 3, 3, 5, 9, 13, 15, 15, 15, + [3, 3, 5, 9, 13, 15, 15, 15, 3, 4, 6, 11, 14, 12, 12, 12, 5, 6, 9, 14, 12, 12, 12, 12, 9, 11, 14, 12, 12, 12, 12, 12, @@ -142,10 +142,10 @@ presets = { 15, 12, 12, 12, 12, 12, 12, 12, 15, 12, 12, 12, 12, 12, 12, 12, 15, 12, 12, 12, 12, 12, 12, 12] - ]}, + ]}, 'web_maximum': {'subsampling': 0, # "4:4:4" - 'quantization': [ - [ 1, 1, 1, 1, 1, 1, 1, 1, + 'quantization': [ + [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 2, 2, @@ -153,7 +153,7 @@ presets = { 1, 1, 1, 1, 2, 2, 3, 3, 1, 1, 1, 2, 2, 3, 3, 3, 1, 1, 2, 2, 3, 3, 3, 3], - [ 1, 1, 1, 2, 2, 3, 3, 3, + [1, 1, 1, 2, 2, 3, 3, 3, 1, 1, 1, 2, 3, 3, 3, 3, 1, 1, 1, 3, 3, 3, 3, 3, 2, 2, 3, 3, 3, 3, 3, 3, @@ -161,9 +161,9 @@ presets = { 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3] - ]}, + ]}, 'low': {'subsampling': 2, # "4:1:1" - 'quantization': [ + 'quantization': [ [18, 14, 14, 21, 30, 35, 34, 17, 14, 16, 16, 19, 26, 23, 12, 12, 14, 16, 17, 21, 23, 12, 12, 12, @@ -180,9 +180,9 @@ presets = { 20, 12, 12, 12, 12, 12, 12, 12, 17, 12, 12, 12, 12, 12, 12, 12, 17, 12, 12, 12, 12, 12, 12, 12] - ]}, + ]}, 'medium': {'subsampling': 2, # "4:1:1" - 'quantization': [ + 'quantization': [ [12, 8, 8, 12, 17, 21, 24, 17, 8, 9, 9, 11, 15, 19, 12, 12, 8, 9, 10, 12, 19, 12, 12, 12, @@ -199,10 +199,10 @@ presets = { 20, 12, 12, 12, 12, 12, 12, 12, 17, 12, 12, 12, 12, 12, 12, 12, 17, 12, 12, 12, 12, 12, 12, 12] - ]}, + ]}, 'high': {'subsampling': 0, # "4:4:4" - 'quantization': [ - [ 6, 4, 4, 6, 9, 11, 12, 16, + 'quantization': [ + [6, 4, 4, 6, 9, 11, 12, 16, 4, 5, 5, 6, 8, 10, 12, 12, 4, 5, 5, 6, 10, 12, 12, 12, 6, 6, 6, 11, 12, 12, 12, 12, @@ -210,7 +210,7 @@ presets = { 11, 10, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 16, 12, 12, 12, 12, 12, 12, 12], - [ 7, 7, 13, 24, 20, 20, 17, 17, + [7, 7, 13, 24, 20, 20, 17, 17, 7, 12, 16, 14, 14, 12, 12, 12, 13, 16, 14, 14, 12, 12, 12, 12, 24, 14, 14, 12, 12, 12, 12, 12, @@ -218,10 +218,10 @@ presets = { 20, 12, 12, 12, 12, 12, 12, 12, 17, 12, 12, 12, 12, 12, 12, 12, 17, 12, 12, 12, 12, 12, 12, 12] - ]}, + ]}, 'maximum': {'subsampling': 0, # "4:4:4" - 'quantization': [ - [ 2, 2, 2, 2, 3, 4, 5, 6, + 'quantization': [ + [2, 2, 2, 2, 3, 4, 5, 6, 2, 2, 2, 2, 3, 4, 5, 6, 2, 2, 2, 2, 4, 5, 7, 9, 2, 2, 2, 4, 5, 7, 9, 12, @@ -229,7 +229,7 @@ presets = { 4, 4, 5, 7, 10, 12, 12, 12, 5, 5, 7, 9, 12, 12, 12, 12, 6, 6, 9, 12, 12, 12, 12, 12], - [ 3, 3, 5, 9, 13, 15, 15, 15, + [3, 3, 5, 9, 13, 15, 15, 15, 3, 4, 6, 10, 14, 12, 12, 12, 5, 6, 9, 14, 12, 12, 12, 12, 9, 10, 14, 12, 12, 12, 12, 12, @@ -237,5 +237,5 @@ presets = { 15, 12, 12, 12, 12, 12, 12, 12, 15, 12, 12, 12, 12, 12, 12, 12, 15, 12, 12, 12, 12, 12, 12, 12] - ]}, + ]}, } diff --git a/PIL/McIdasImagePlugin.py b/PIL/McIdasImagePlugin.py index c3f255fd2..b75360353 100644 --- a/PIL/McIdasImagePlugin.py +++ b/PIL/McIdasImagePlugin.py @@ -16,11 +16,11 @@ # See the README file for information on usage and redistribution. # -__version__ = "0.2" - import struct from PIL import Image, ImageFile +__version__ = "0.2" + def _accept(s): return s[:8] == b"\x00\x00\x00\x00\x00\x00\x00\x04" @@ -69,6 +69,6 @@ class McIdasImageFile(ImageFile.ImageFile): # -------------------------------------------------------------------- # registry -Image.register_open("MCIDAS", McIdasImageFile, _accept) +Image.register_open(McIdasImageFile.format, McIdasImageFile, _accept) # no default extension diff --git a/PIL/MicImagePlugin.py b/PIL/MicImagePlugin.py index aa41bf359..3c912442b 100644 --- a/PIL/MicImagePlugin.py +++ b/PIL/MicImagePlugin.py @@ -17,12 +17,11 @@ # -__version__ = "0.1" - - from PIL import Image, TiffImagePlugin from PIL.OleFileIO import MAGIC, OleFileIO +__version__ = "0.1" + # # -------------------------------------------------------------------- @@ -75,6 +74,10 @@ class MicImageFile(TiffImagePlugin.TiffImageFile): def n_frames(self): return len(self.images) + @property + def is_animated(self): + return len(self.images) > 1 + def seek(self, frame): try: @@ -95,6 +98,6 @@ class MicImageFile(TiffImagePlugin.TiffImageFile): # # -------------------------------------------------------------------- -Image.register_open("MIC", MicImageFile, _accept) +Image.register_open(MicImageFile.format, MicImageFile, _accept) -Image.register_extension("MIC", ".mic") +Image.register_extension(MicImageFile.format, ".mic") diff --git a/PIL/MpegImagePlugin.py b/PIL/MpegImagePlugin.py index ff7c0dce4..6671b8691 100644 --- a/PIL/MpegImagePlugin.py +++ b/PIL/MpegImagePlugin.py @@ -13,11 +13,12 @@ # See the README file for information on usage and redistribution. # -__version__ = "0.1" from PIL import Image, ImageFile from PIL._binary import i8 +__version__ = "0.1" + # # Bitstream parser @@ -77,9 +78,9 @@ class MpegImageFile(ImageFile.ImageFile): # -------------------------------------------------------------------- # Registry stuff -Image.register_open("MPEG", MpegImageFile) +Image.register_open(MpegImageFile.format, MpegImageFile) -Image.register_extension("MPEG", ".mpg") -Image.register_extension("MPEG", ".mpeg") +Image.register_extension(MpegImageFile.format, ".mpg") +Image.register_extension(MpegImageFile.format, ".mpeg") -Image.register_mime("MPEG", "video/mpeg") +Image.register_mime(MpegImageFile.format, "video/mpeg") diff --git a/PIL/MpoImagePlugin.py b/PIL/MpoImagePlugin.py index 9d21728b9..1d26021d8 100644 --- a/PIL/MpoImagePlugin.py +++ b/PIL/MpoImagePlugin.py @@ -18,10 +18,10 @@ # See the README file for information on usage and redistribution. # -__version__ = "0.1" - from PIL import Image, JpegImagePlugin +__version__ = "0.1" + def _accept(prefix): return JpegImagePlugin._accept(prefix) @@ -66,6 +66,10 @@ class MpoImageFile(JpegImagePlugin.JpegImageFile): def n_frames(self): return self.__framecount + @property + def is_animated(self): + return self.__framecount > 1 + def seek(self, frame): if frame < 0 or frame >= self.__framecount: raise EOFError("no more images in MPO file") @@ -86,9 +90,10 @@ class MpoImageFile(JpegImagePlugin.JpegImageFile): # Note that since MPO shares a factory with JPEG, we do not need to do a # separate registration for it here. -# Image.register_open("MPO", JpegImagePlugin.jpeg_factory, _accept) -Image.register_save("MPO", _save) +# Image.register_open(MpoImageFile.format, +# JpegImagePlugin.jpeg_factory, _accept) +Image.register_save(MpoImageFile.format, _save) -Image.register_extension("MPO", ".mpo") +Image.register_extension(MpoImageFile.format, ".mpo") -Image.register_mime("MPO", "image/mpo") +Image.register_mime(MpoImageFile.format, "image/mpo") diff --git a/PIL/MspImagePlugin.py b/PIL/MspImagePlugin.py index 1e974d53f..85f8e764b 100644 --- a/PIL/MspImagePlugin.py +++ b/PIL/MspImagePlugin.py @@ -17,10 +17,10 @@ # -__version__ = "0.1" - from PIL import Image, ImageFile, _binary +__version__ = "0.1" + # # read MSP files @@ -98,7 +98,7 @@ def _save(im, fp, filename): # # registry -Image.register_open("MSP", MspImageFile, _accept) -Image.register_save("MSP", _save) +Image.register_open(MspImageFile.format, MspImageFile, _accept) +Image.register_save(MspImageFile.format, _save) -Image.register_extension("MSP", ".msp") +Image.register_extension(MspImageFile.format, ".msp") diff --git a/PIL/OleFileIO-README.md b/PIL/OleFileIO-README.md index 11a0e9053..35e028265 100644 --- a/PIL/OleFileIO-README.md +++ b/PIL/OleFileIO-README.md @@ -1,26 +1,45 @@ -OleFileIO_PL -============ +olefile (formerly OleFileIO_PL) +=============================== -[OleFileIO_PL](http://www.decalage.info/python/olefileio) is a Python module to parse and read [Microsoft OLE2 files (also called Structured Storage, Compound File Binary Format or Compound Document File Format)](http://en.wikipedia.org/wiki/Compound_File_Binary_Format), such as Microsoft Office documents, Image Composer and FlashPix files, Outlook messages, StickyNotes, several Microscopy file formats ... +[olefile](http://www.decalage.info/olefile) is a Python package to parse, read and write +[Microsoft OLE2 files](http://en.wikipedia.org/wiki/Compound_File_Binary_Format) +(also called Structured Storage, Compound File Binary Format or Compound Document File Format), +such as Microsoft Office 97-2003 documents, vbaProject.bin in MS Office 2007+ files, Image Composer +and FlashPix files, Outlook messages, StickyNotes, several Microscopy file formats, McAfee antivirus quarantine files, +etc. + -This is an improved version of the OleFileIO module from [PIL](http://www.pythonware.com/products/pil/index.htm), the excellent Python Imaging Library, created and maintained by Fredrik Lundh. The API is still compatible with PIL, but since 2005 I have improved the internal implementation significantly, with new features, bugfixes and a more robust design. +**Quick links:** [Home page](http://www.decalage.info/olefile) - +[Download/Install](https://bitbucket.org/decalage/olefileio_pl/wiki/Install) - +[Documentation](https://bitbucket.org/decalage/olefileio_pl/wiki) - +[Report Issues/Suggestions/Questions](https://bitbucket.org/decalage/olefileio_pl/issues?status=new&status=open) - +[Contact the author](http://decalage.info/contact) - +[Repository](https://bitbucket.org/decalage/olefileio_pl) - +[Updates on Twitter](https://twitter.com/decalage2) -As far as I know, this module is now the most complete and robust Python implementation to read MS OLE2 files, portable on several operating systems. (please tell me if you know other similar Python modules) - -OleFileIO_PL can be used as an independent module or with PIL. The goal is to have it integrated into [Pillow](http://python-pillow.github.io/), the friendly fork of PIL. - -OleFileIO\_PL is mostly meant for developers. If you are looking for tools to analyze OLE files or to extract data, then please also check [python-oletools](http://www.decalage.info/python/oletools), which are built upon OleFileIO_PL. News ---- Follow all updates and news on Twitter: -- **2014-02-04 v0.30**: now compatible with Python 3.x, thanks to Martin Panter who did most of the hard work. -- 2013-07-24 v0.26: added methods to parse stream/storage timestamps, improved listdir to include storages, fixed parsing of direntry timestamps -- 2013-05-27 v0.25: improved metadata extraction, properties parsing and exception handling, fixed [issue #12](https://bitbucket.org/decalage/olefileio_pl/issue/12/error-when-converting-timestamps-in-ole) -- 2013-05-07 v0.24: new features to extract metadata (get\_metadata method and OleMetadata class), improved getproperties to convert timestamps to Python datetime -- 2012-10-09: published [python-oletools](http://www.decalage.info/python/oletools), a package of analysis tools based on OleFileIO_PL +- **2015-01-25 v0.42**: improved handling of special characters in stream/storage names on Python 2.x (using UTF-8 + instead of Latin-1), fixed bug in listdir with empty storages. +- 2014-11-25 v0.41: OleFileIO.open and isOleFile now support OLE files stored in byte strings, fixed installer for + python 3, added support for Jython (Niko Ehrenfeuchter) +- 2014-10-01 v0.40: renamed OleFileIO_PL to olefile, added initial write support for streams >4K, updated doc and + license, improved the setup script. +- 2014-07-27 v0.31: fixed support for large files with 4K sectors, thanks to Niko Ehrenfeuchter, Martijn Berger and + Dave Jones. Added test scripts from Pillow (by hugovk). Fixed setup for Python 3 (Martin Panter) +- 2014-02-04 v0.30: now compatible with Python 3.x, thanks to Martin Panter who did most of the hard work. +- 2013-07-24 v0.26: added methods to parse stream/storage timestamps, improved listdir to include storages, fixed + parsing of direntry timestamps +- 2013-05-27 v0.25: improved metadata extraction, properties parsing and exception handling, fixed + [issue #12](https://bitbucket.org/decalage/olefileio_pl/issue/12/error-when-converting-timestamps-in-ole) +- 2013-05-07 v0.24: new features to extract metadata (get\_metadata method and OleMetadata class), improved + getproperties to convert timestamps to Python datetime +- 2012-10-09: published [python-oletools](http://www.decalage.info/python/oletools), a package of analysis tools based + on OleFileIO_PL - 2012-09-11 v0.23: added support for file-like objects, fixed [issue #8](https://bitbucket.org/decalage/olefileio_pl/issue/8/bug-with-file-object) - 2012-02-17 v0.22: fixed issues #7 (bug in getproperties) and #2 (added close method) - 2011-10-20: code hosted on bitbucket to ease contributions and bug tracking @@ -29,20 +48,50 @@ Follow all updates and news on Twitter: - 2009-12-10 v0.19: fixed support for 64 bits platforms (thanks to Ben G. and Martijn for reporting the bug) - see changelog in source code for more info. -Download --------- +Download/Install +---------------- -The archive is available on [the project page](https://bitbucket.org/decalage/olefileio_pl/downloads). +If you have pip or setuptools installed (pip is included in Python 2.7.9+), you may simply run **pip install olefile** +or **easy_install olefile** for the first installation. + +To update olefile, run **pip install -U olefile**. + +Otherwise, see https://bitbucket.org/decalage/olefileio_pl/wiki/Install Features -------- -- Parse and read any OLE file such as Microsoft Office 97-2003 legacy document formats (Word .doc, Excel .xls, PowerPoint .ppt, Visio .vsd, Project .mpp), Image Composer and FlashPix files, Outlook messages, StickyNotes, Zeiss AxioVision ZVI files, Olympus FluoView OIB files, ... +- Parse, read and write any OLE file such as Microsoft Office 97-2003 legacy document formats (Word .doc, Excel .xls, + PowerPoint .ppt, Visio .vsd, Project .mpp), Image Composer and FlashPix files, Outlook messages, StickyNotes, + Zeiss AxioVision ZVI files, Olympus FluoView OIB files, etc - List all the streams and storages contained in an OLE file - Open streams as files - Parse and read property streams, containing metadata of the file - Portable, pure Python module, no dependency +olefile can be used as an independent package or with PIL/Pillow. + +olefile is mostly meant for developers. If you are looking for tools to analyze OLE files or to extract data (especially +for security purposes such as malware analysis and forensics), then please also check my +[python-oletools](http://www.decalage.info/python/oletools), which are built upon olefile and provide a higher-level interface. + + +History +------- + +olefile is based on the OleFileIO module from [PIL](http://www.pythonware.com/products/pil/index.htm), the excellent +Python Imaging Library, created and maintained by Fredrik Lundh. The olefile API is still compatible with PIL, but +since 2005 I have improved the internal implementation significantly, with new features, bugfixes and a more robust +design. From 2005 to 2014 the project was called OleFileIO_PL, and in 2014 I changed its name to olefile to celebrate +its 9 years and its new write features. + +As far as I know, olefile is the most complete and robust Python implementation to read MS OLE2 files, portable on +several operating systems. (please tell me if you know other similar Python modules) + +Since 2014 olefile/OleFileIO_PL has been integrated into [Pillow](http://python-pillow.org), the friendly fork +of PIL. olefile will continue to be improved as a separate project, and new versions will be merged into Pillow +regularly. + Main improvements over the original version of OleFileIO in PIL: ---------------------------------------------------------------- @@ -58,294 +107,74 @@ Main improvements over the original version of OleFileIO in PIL: - Can open file-like objects - Added setup.py and install.bat to ease installation - More convenient slash-based syntax for stream paths +- Write features +Documentation +------------- +Please see the [online documentation](https://bitbucket.org/decalage/olefileio_pl/wiki) for more information, +especially the [OLE overview](https://bitbucket.org/decalage/olefileio_pl/wiki/OLE_Overview) and the +[API page](https://bitbucket.org/decalage/olefileio_pl/wiki/API) which describe how to use olefile in Python applications. +A copy of the same documentation is also provided in the doc subfolder of the olefile package. -How to use this module ----------------------- - -OleFileIO_PL can be used as an independent module or with PIL. The main functions and methods are explained below. - -For more information, see also the file **OleFileIO_PL.html**, sample code at the end of the module itself, and docstrings within the code. - -### About the structure of OLE files ### - -An OLE file can be seen as a mini file system or a Zip archive: It contains **streams** of data that look like files embedded within the OLE file. Each stream has a name. For example, the main stream of a MS Word document containing its text is named "WordDocument". - -An OLE file can also contain **storages**. A storage is a folder that contains streams or other storages. For example, a MS Word document with VBA macros has a storage called "Macros". - -Special streams can contain **properties**. A property is a specific value that can be used to store information such as the metadata of a document (title, author, creation date, etc). Property stream names usually start with the character '\x05'. - -For example, a typical MS Word document may look like this: - - \x05DocumentSummaryInformation (stream) - \x05SummaryInformation (stream) - WordDocument (stream) - Macros (storage) - PROJECT (stream) - PROJECTwm (stream) - VBA (storage) - Module1 (stream) - ThisDocument (stream) - _VBA_PROJECT (stream) - dir (stream) - ObjectPool (storage) - - - -### Import OleFileIO_PL ### - - :::python - import OleFileIO_PL - -As of version 0.30, the code has been changed to be compatible with Python 3.x. As a consequence, compatibility with Python 2.5 or older is not provided anymore. However, a copy of v0.26 is available as OleFileIO_PL2.py. If your application needs to be compatible with Python 2.5 or older, you may use the following code to load the old version when needed: - - :::python - try: - import OleFileIO_PL - except: - import OleFileIO_PL2 as OleFileIO_PL - -If you think OleFileIO_PL should stay compatible with Python 2.5 or older, please [contact me](http://decalage.info/contact). - - -### Test if a file is an OLE container ### - -Use isOleFile to check if the first bytes of the file contain the Magic for OLE files, before opening it. isOleFile returns True if it is an OLE file, False otherwise (new in v0.16). - - :::python - assert OleFileIO_PL.isOleFile('myfile.doc') - - -### Open an OLE file from disk ### - -Create an OleFileIO object with the file path as parameter: - - :::python - ole = OleFileIO_PL.OleFileIO('myfile.doc') - -### Open an OLE file from a file-like object ### - -This is useful if the file is not on disk, e.g. already stored in a string or as a file-like object. - - :::python - ole = OleFileIO_PL.OleFileIO(f) - -For example the code below reads a file into a string, then uses BytesIO to turn it into a file-like object. - - :::python - data = open('myfile.doc', 'rb').read() - f = io.BytesIO(data) # or StringIO.StringIO for Python 2.x - ole = OleFileIO_PL.OleFileIO(f) - -### How to handle malformed OLE files ### - -By default, the parser is configured to be as robust and permissive as possible, allowing to parse most malformed OLE files. Only fatal errors will raise an exception. It is possible to tell the parser to be more strict in order to raise exceptions for files that do not fully conform to the OLE specifications, using the raise_defect option (new in v0.14): - - :::python - ole = OleFileIO_PL.OleFileIO('myfile.doc', raise_defects=DEFECT_INCORRECT) - -When the parsing is done, the list of non-fatal issues detected is available as a list in the parsing_issues attribute of the OleFileIO object (new in 0.25): - - :::python - print('Non-fatal issues raised during parsing:') - if ole.parsing_issues: - for exctype, msg in ole.parsing_issues: - print('- %s: %s' % (exctype.__name__, msg)) - else: - print('None') - - -### Syntax for stream and storage path ### - -Two different syntaxes are allowed for methods that need or return the path of streams and storages: - -1) Either a **list of strings** including all the storages from the root up to the stream/storage name. For example a stream called "WordDocument" at the root will have ['WordDocument'] as full path. A stream called "ThisDocument" located in the storage "Macros/VBA" will be ['Macros', 'VBA', 'ThisDocument']. This is the original syntax from PIL. While hard to read and not very convenient, this syntax works in all cases. - -2) Or a **single string with slashes** to separate storage and stream names (similar to the Unix path syntax). The previous examples would be 'WordDocument' and 'Macros/VBA/ThisDocument'. This syntax is easier, but may fail if a stream or storage name contains a slash. (new in v0.15) - -Both are case-insensitive. - -Switching between the two is easy: - - :::python - slash_path = '/'.join(list_path) - list_path = slash_path.split('/') - - -### Get the list of streams ### - -listdir() returns a list of all the streams contained in the OLE file, including those stored in storages. Each stream is listed itself as a list, as described above. - - :::python - print(ole.listdir()) - -Sample result: - - :::python - [['\x01CompObj'], ['\x05DocumentSummaryInformation'], ['\x05SummaryInformation'] - , ['1Table'], ['Macros', 'PROJECT'], ['Macros', 'PROJECTwm'], ['Macros', 'VBA', - 'Module1'], ['Macros', 'VBA', 'ThisDocument'], ['Macros', 'VBA', '_VBA_PROJECT'] - , ['Macros', 'VBA', 'dir'], ['ObjectPool'], ['WordDocument']] - -As an option it is possible to choose if storages should also be listed, with or without streams (new in v0.26): - - :::python - ole.listdir (streams=False, storages=True) - - -### Test if known streams/storages exist: ### - -exists(path) checks if a given stream or storage exists in the OLE file (new in v0.16). - - :::python - if ole.exists('worddocument'): - print("This is a Word document.") - if ole.exists('macros/vba'): - print("This document seems to contain VBA macros.") - - -### Read data from a stream ### - -openstream(path) opens a stream as a file-like object. - -The following example extracts the "Pictures" stream from a PPT file: - - :::python - pics = ole.openstream('Pictures') - data = pics.read() - - -### Get information about a stream/storage ### - -Several methods can provide the size, type and timestamps of a given stream/storage: - -get_size(path) returns the size of a stream in bytes (new in v0.16): - - :::python - s = ole.get_size('WordDocument') - -get_type(path) returns the type of a stream/storage, as one of the following constants: STGTY\_STREAM for a stream, STGTY\_STORAGE for a storage, STGTY\_ROOT for the root entry, and False for a non existing path (new in v0.15). - - :::python - t = ole.get_type('WordDocument') - -get\_ctime(path) and get\_mtime(path) return the creation and modification timestamps of a stream/storage, as a Python datetime object with UTC timezone. Please note that these timestamps are only present if the application that created the OLE file explicitly stored them, which is rarely the case. When not present, these methods return None (new in v0.26). - - :::python - c = ole.get_ctime('WordDocument') - m = ole.get_mtime('WordDocument') - -The root storage is a special case: You can get its creation and modification timestamps using the OleFileIO.root attribute (new in v0.26): - - :::python - c = ole.root.getctime() - m = ole.root.getmtime() - -### Extract metadata ### - -get_metadata() will check if standard property streams exist, parse all the properties they contain, and return an OleMetadata object with the found properties as attributes (new in v0.24). - - :::python - meta = ole.get_metadata() - print('Author:', meta.author) - print('Title:', meta.title) - print('Creation date:', meta.create_time) - # print all metadata: - meta.dump() - -Available attributes include: - - codepage, title, subject, author, keywords, comments, template, - last_saved_by, revision_number, total_edit_time, last_printed, create_time, - last_saved_time, num_pages, num_words, num_chars, thumbnail, - creating_application, security, codepage_doc, category, presentation_target, - bytes, lines, paragraphs, slides, notes, hidden_slides, mm_clips, - scale_crop, heading_pairs, titles_of_parts, manager, company, links_dirty, - chars_with_spaces, unused, shared_doc, link_base, hlinks, hlinks_changed, - version, dig_sig, content_type, content_status, language, doc_version - -See the source code of the OleMetadata class for more information. - - -### Parse a property stream ### - -get\_properties(path) can be used to parse any property stream that is not handled by get\_metadata. It returns a dictionary indexed by integers. Each integer is the index of the property, pointing to its value. For example in the standard property stream '\x05SummaryInformation', the document title is property #2, and the subject is #3. - - :::python - p = ole.getproperties('specialprops') - -By default as in the original PIL version, timestamp properties are converted into a number of seconds since Jan 1,1601. With the option convert\_time, you can obtain more convenient Python datetime objects (UTC timezone). If some time properties should not be converted (such as total editing time in '\x05SummaryInformation'), the list of indexes can be passed as no_conversion (new in v0.25): - - :::python - p = ole.getproperties('specialprops', convert_time=True, no_conversion=[10]) - - -### Close the OLE file ### - -Unless your application is a simple script that terminates after processing an OLE file, do not forget to close each OleFileIO object after parsing to close the file on disk. (new in v0.22) - - :::python - ole.close() - -### Use OleFileIO_PL as a script ### - -OleFileIO_PL can also be used as a script from the command-line to display the structure of an OLE file and its metadata, for example: - - OleFileIO_PL.py myfile.doc - -You can use the option -c to check that all streams can be read fully, and -d to generate very verbose debugging information. ## Real-life examples ## A real-life example: [using OleFileIO_PL for malware analysis and forensics](http://blog.gregback.net/2011/03/using-remnux-for-forensic-puzzle-6/). -See also [this paper](https://computer-forensics.sans.org/community/papers/gcfa/grow-forensic-tools-taxonomy-python-libraries-helpful-forensic-analysis_6879) about python tools for forensics, which features OleFileIO_PL. +See also [this paper](https://computer-forensics.sans.org/community/papers/gcfa/grow-forensic-tools-taxonomy-python-libraries-helpful-forensic-analysis_6879) about python tools for forensics, which features olefile. -About Python 2 and 3 --------------------- - -OleFileIO\_PL used to support only Python 2.x. As of version 0.30, the code has been changed to be compatible with Python 3.x. As a consequence, compatibility with Python 2.5 or older is not provided anymore. However, a copy of v0.26 is available as OleFileIO_PL2.py. See above the "import" section for a workaround. - -If you think OleFileIO_PL should stay compatible with Python 2.5 or older, please [contact me](http://decalage.info/contact). - -How to contribute ------------------ - -The code is available in [a Mercurial repository on bitbucket](https://bitbucket.org/decalage/olefileio_pl). You may use it to submit enhancements or to report any issue. - -If you would like to help us improve this module, or simply provide feedback, please [contact me](http://decalage.info/contact). You can help in many ways: - -- test this module on different platforms / Python versions -- find and report bugs -- improve documentation, code samples, docstrings -- write unittest test cases -- provide tricky malformed files - -How to report bugs ------------------- - -To report a bug, for example a normal file which is not parsed correctly, please use the [issue reporting page](https://bitbucket.org/decalage/olefileio_pl/issues?status=new&status=open), or if you prefer to do it privately, use this [contact form](http://decalage.info/contact). Please provide all the information about the context and how to reproduce the bug. - -If possible please join the debugging output of OleFileIO_PL. For this, launch the following command : - - OleFileIO_PL.py -d -c file >debug.txt License ------- -OleFileIO_PL is open-source. +olefile (formerly OleFileIO_PL) is copyright (c) 2005-2015 Philippe Lagadec +([http://www.decalage.info](http://www.decalage.info)) -OleFileIO_PL changes are Copyright (c) 2005-2014 by Philippe Lagadec. +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +---------- + +olefile is based on source code from the OleFileIO module of the Python Imaging Library (PIL) published by Fredrik +Lundh under the following license: The Python Imaging Library (PIL) is -- Copyright (c) 1997-2005 by Secret Labs AB + Copyright © 1997-2011 by Secret Labs AB + Copyright © 1995-2011 by Fredrik Lundh -- Copyright (c) 1995-2005 by Fredrik Lundh +By obtaining, using, and/or copying this software and/or its associated documentation, you agree that you have read, +understood, and will comply with the following terms and conditions: -By obtaining, using, and/or copying this software and/or its associated documentation, you agree that you have read, understood, and will comply with the following terms and conditions: +Permission to use, copy, modify, and distribute this software and its associated documentation for any purpose and +without fee is hereby granted, provided that the above copyright notice appears in all copies, and that both that +copyright notice and this permission notice appear in supporting documentation, and that the name of Secret Labs AB or +the author not be used in advertising or publicity pertaining to distribution of the software without specific, written +prior permission. -Permission to use, copy, modify, and distribute this software and its associated documentation for any purpose and without fee is hereby granted, provided that the above copyright notice appears in all copies, and that both that copyright notice and this permission notice appear in supporting documentation, and that the name of Secret Labs AB or the author not be used in advertising or publicity pertaining to distribution of the software without specific, written prior permission. - -SECRET LABS AB AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL SECRET LABS AB OR THE AUTHOR BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +SECRET LABS AB AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES +OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL SECRET LABS AB OR THE AUTHOR BE LIABLE FOR ANY SPECIAL, INDIRECT OR +CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF +CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS +SOFTWARE. diff --git a/PIL/PSDraw.py b/PIL/PSDraw.py index a090a8f64..d4e7b18cc 100644 --- a/PIL/PSDraw.py +++ b/PIL/PSDraw.py @@ -16,11 +16,12 @@ # from PIL import EpsImagePlugin - +import sys ## # Simple Postscript graphics interface. + class PSDraw(object): """ Sets up printing to the given file. If **file** is omitted, @@ -29,12 +30,11 @@ class PSDraw(object): def __init__(self, fp=None): if not fp: - import sys fp = sys.stdout self.fp = fp def _fp_write(self, to_write): - if bytes is str: + if bytes is str or self.fp == sys.stdout: self.fp.write(to_write) else: self.fp.write(bytes(to_write, 'UTF-8')) @@ -47,7 +47,7 @@ class PSDraw(object): "/showpage { } def\n" "%%EndComments\n" "%%BeginDocument\n") - # self.fp_write(ERROR_PS) # debugging! + # self._fp_write(ERROR_PS) # debugging! self._fp_write(EDROFF_PS) self._fp_write(VDI_PS) self._fp_write("%%EndProlog\n") diff --git a/PIL/PalmImagePlugin.py b/PIL/PalmImagePlugin.py index bba1de8bb..4f415ff7c 100644 --- a/PIL/PalmImagePlugin.py +++ b/PIL/PalmImagePlugin.py @@ -7,10 +7,10 @@ # Image plugin for Palm pixmap images (output only). ## -__version__ = "1.0" - from PIL import Image, ImageFile, _binary +__version__ = "1.0" + _Palm8BitColormapValues = ( (255, 255, 255), (255, 204, 255), (255, 153, 255), (255, 102, 255), (255, 51, 255), (255, 0, 255), (255, 255, 204), (255, 204, 204), @@ -30,15 +30,15 @@ _Palm8BitColormapValues = ( (102, 255, 204), (102, 204, 204), (102, 153, 204), (102, 102, 204), (102, 51, 204), (102, 0, 204), (102, 255, 153), (102, 204, 153), (102, 153, 153), (102, 102, 153), (102, 51, 153), (102, 0, 153), - ( 51, 255, 255), ( 51, 204, 255), ( 51, 153, 255), ( 51, 102, 255), - ( 51, 51, 255), ( 51, 0, 255), ( 51, 255, 204), ( 51, 204, 204), - ( 51, 153, 204), ( 51, 102, 204), ( 51, 51, 204), ( 51, 0, 204), - ( 51, 255, 153), ( 51, 204, 153), ( 51, 153, 153), ( 51, 102, 153), - ( 51, 51, 153), ( 51, 0, 153), ( 0, 255, 255), ( 0, 204, 255), - ( 0, 153, 255), ( 0, 102, 255), ( 0, 51, 255), ( 0, 0, 255), - ( 0, 255, 204), ( 0, 204, 204), ( 0, 153, 204), ( 0, 102, 204), - ( 0, 51, 204), ( 0, 0, 204), ( 0, 255, 153), ( 0, 204, 153), - ( 0, 153, 153), ( 0, 102, 153), ( 0, 51, 153), ( 0, 0, 153), + (51, 255, 255), (51, 204, 255), (51, 153, 255), (51, 102, 255), + (51, 51, 255), (51, 0, 255), (51, 255, 204), (51, 204, 204), + (51, 153, 204), (51, 102, 204), (51, 51, 204), (51, 0, 204), + (51, 255, 153), (51, 204, 153), (51, 153, 153), (51, 102, 153), + (51, 51, 153), (51, 0, 153), (0, 255, 255), (0, 204, 255), + (0, 153, 255), (0, 102, 255), (0, 51, 255), (0, 0, 255), + (0, 255, 204), (0, 204, 204), (0, 153, 204), (0, 102, 204), + (0, 51, 204), (0, 0, 204), (0, 255, 153), (0, 204, 153), + (0, 153, 153), (0, 102, 153), (0, 51, 153), (0, 0, 153), (255, 255, 102), (255, 204, 102), (255, 153, 102), (255, 102, 102), (255, 51, 102), (255, 0, 102), (255, 255, 51), (255, 204, 51), (255, 153, 51), (255, 102, 51), (255, 51, 51), (255, 0, 51), @@ -57,25 +57,25 @@ _Palm8BitColormapValues = ( (102, 255, 51), (102, 204, 51), (102, 153, 51), (102, 102, 51), (102, 51, 51), (102, 0, 51), (102, 255, 0), (102, 204, 0), (102, 153, 0), (102, 102, 0), (102, 51, 0), (102, 0, 0), - ( 51, 255, 102), ( 51, 204, 102), ( 51, 153, 102), ( 51, 102, 102), - ( 51, 51, 102), ( 51, 0, 102), ( 51, 255, 51), ( 51, 204, 51), - ( 51, 153, 51), ( 51, 102, 51), ( 51, 51, 51), ( 51, 0, 51), - ( 51, 255, 0), ( 51, 204, 0), ( 51, 153, 0), ( 51, 102, 0), - ( 51, 51, 0), ( 51, 0, 0), ( 0, 255, 102), ( 0, 204, 102), - ( 0, 153, 102), ( 0, 102, 102), ( 0, 51, 102), ( 0, 0, 102), - ( 0, 255, 51), ( 0, 204, 51), ( 0, 153, 51), ( 0, 102, 51), - ( 0, 51, 51), ( 0, 0, 51), ( 0, 255, 0), ( 0, 204, 0), - ( 0, 153, 0), ( 0, 102, 0), ( 0, 51, 0), ( 17, 17, 17), - ( 34, 34, 34), ( 68, 68, 68), ( 85, 85, 85), (119, 119, 119), + (51, 255, 102), (51, 204, 102), (51, 153, 102), (51, 102, 102), + (51, 51, 102), (51, 0, 102), (51, 255, 51), (51, 204, 51), + (51, 153, 51), (51, 102, 51), (51, 51, 51), (51, 0, 51), + (51, 255, 0), (51, 204, 0), (51, 153, 0), (51, 102, 0), + (51, 51, 0), (51, 0, 0), (0, 255, 102), (0, 204, 102), + (0, 153, 102), (0, 102, 102), (0, 51, 102), (0, 0, 102), + (0, 255, 51), (0, 204, 51), (0, 153, 51), (0, 102, 51), + (0, 51, 51), (0, 0, 51), (0, 255, 0), (0, 204, 0), + (0, 153, 0), (0, 102, 0), (0, 51, 0), (17, 17, 17), + (34, 34, 34), (68, 68, 68), (85, 85, 85), (119, 119, 119), (136, 136, 136), (170, 170, 170), (187, 187, 187), (221, 221, 221), (238, 238, 238), (192, 192, 192), (128, 0, 0), (128, 0, 128), - ( 0, 128, 0), ( 0, 128, 128), ( 0, 0, 0), ( 0, 0, 0), - ( 0, 0, 0), ( 0, 0, 0), ( 0, 0, 0), ( 0, 0, 0), - ( 0, 0, 0), ( 0, 0, 0), ( 0, 0, 0), ( 0, 0, 0), - ( 0, 0, 0), ( 0, 0, 0), ( 0, 0, 0), ( 0, 0, 0), - ( 0, 0, 0), ( 0, 0, 0), ( 0, 0, 0), ( 0, 0, 0), - ( 0, 0, 0), ( 0, 0, 0), ( 0, 0, 0), ( 0, 0, 0), - ( 0, 0, 0), ( 0, 0, 0), ( 0, 0, 0), ( 0, 0, 0)) + (0, 128, 0), (0, 128, 128), (0, 0, 0), (0, 0, 0), + (0, 0, 0), (0, 0, 0), (0, 0, 0), (0, 0, 0), + (0, 0, 0), (0, 0, 0), (0, 0, 0), (0, 0, 0), + (0, 0, 0), (0, 0, 0), (0, 0, 0), (0, 0, 0), + (0, 0, 0), (0, 0, 0), (0, 0, 0), (0, 0, 0), + (0, 0, 0), (0, 0, 0), (0, 0, 0), (0, 0, 0), + (0, 0, 0), (0, 0, 0), (0, 0, 0), (0, 0, 0)) # so build a prototype image to be used for palette resampling @@ -227,7 +227,8 @@ def _save(im, fp, filename, check=0): ImageFile._save( im, fp, [("raw", (0, 0)+im.size, 0, (rawmode, rowbytes, 1))]) - fp.flush() + if hasattr(fp, "flush"): + fp.flush() # diff --git a/PIL/PcdImagePlugin.py b/PIL/PcdImagePlugin.py index 06db5d628..b53635a99 100644 --- a/PIL/PcdImagePlugin.py +++ b/PIL/PcdImagePlugin.py @@ -15,11 +15,10 @@ # -__version__ = "0.1" - - from PIL import Image, ImageFile, _binary +__version__ = "0.1" + i8 = _binary.i8 @@ -55,6 +54,6 @@ class PcdImageFile(ImageFile.ImageFile): # # registry -Image.register_open("PCD", PcdImageFile) +Image.register_open(PcdImageFile.format, PcdImageFile) -Image.register_extension("PCD", ".pcd") +Image.register_extension(PcdImageFile.format, ".pcd") diff --git a/PIL/PcxImagePlugin.py b/PIL/PcxImagePlugin.py index 40fafa0dc..9440d5362 100644 --- a/PIL/PcxImagePlugin.py +++ b/PIL/PcxImagePlugin.py @@ -27,8 +27,11 @@ from __future__ import print_function +import logging from PIL import Image, ImageFile, ImagePalette, _binary +logger = logging.getLogger(__name__) + i8 = _binary.i8 i16 = _binary.i16le o8 = _binary.o8 @@ -59,17 +62,15 @@ class PcxImageFile(ImageFile.ImageFile): bbox = i16(s, 4), i16(s, 6), i16(s, 8)+1, i16(s, 10)+1 if bbox[2] <= bbox[0] or bbox[3] <= bbox[1]: raise SyntaxError("bad PCX image size") - if Image.DEBUG: - print("BBox: %s %s %s %s" % bbox) + logger.debug("BBox: %s %s %s %s", *bbox) # format version = i8(s[1]) bits = i8(s[3]) planes = i8(s[65]) stride = i16(s, 66) - if Image.DEBUG: - print("PCX version %s, bits %s, planes %s, stride %s" % - (version, bits, planes, stride)) + logger.debug("PCX version %s, bits %s, planes %s, stride %s", + version, bits, planes, stride) self.info["dpi"] = i16(s, 12), i16(s, 14) @@ -107,8 +108,7 @@ class PcxImageFile(ImageFile.ImageFile): self.size = bbox[2]-bbox[0], bbox[3]-bbox[1] bbox = (0, 0) + self.size - if Image.DEBUG: - print("size: %sx%s" % self.size) + logger.debug("size: %sx%s", *self.size) self.tile = [("pcx", bbox, self.fp.tell(), (rawmode, planes * stride))] @@ -144,9 +144,8 @@ def _save(im, fp, filename, check=0): # Ideally it should be passed in in the state, but the bytes value # gets overwritten. - if Image.DEBUG: - print("PcxImagePlugin._save: xwidth: %d, bits: %d, stride: %d" % ( - im.size[0], bits, stride)) + logger.debug("PcxImagePlugin._save: xwidth: %d, bits: %d, stride: %d", + im.size[0], bits, stride) # under windows, we could determine the current screen size with # "Image.core.display_mode()[1]", but I think that's overkill... @@ -182,7 +181,7 @@ def _save(im, fp, filename, check=0): # -------------------------------------------------------------------- # registry -Image.register_open("PCX", PcxImageFile, _accept) -Image.register_save("PCX", _save) +Image.register_open(PcxImageFile.format, PcxImageFile, _accept) +Image.register_save(PcxImageFile.format, _save) -Image.register_extension("PCX", ".pcx") +Image.register_extension(PcxImageFile.format, ".pcx") diff --git a/PIL/PdfImagePlugin.py b/PIL/PdfImagePlugin.py index 1d8c2ff93..7decf0ee5 100644 --- a/PIL/PdfImagePlugin.py +++ b/PIL/PdfImagePlugin.py @@ -20,12 +20,12 @@ # Image plugin for PDF images (output only). ## -__version__ = "0.4" - from PIL import Image, ImageFile from PIL._binary import i8 import io +__version__ = "0.4" + # # -------------------------------------------------------------------- @@ -51,17 +51,21 @@ def _endobj(fp): fp.write("endobj\n") +def _save_all(im, fp, filename): + _save(im, fp, filename, save_all=True) + + ## # (Internal) Image save plugin for the PDF format. -def _save(im, fp, filename): +def _save(im, fp, filename, save_all=False): resolution = im.encoderinfo.get("resolution", 72.0) # # make sure image data is available im.load() - xref = [0]*(5+1) # placeholders + xref = [0] class TextWriter(object): def __init__(self, fp): @@ -78,11 +82,6 @@ def _save(im, fp, filename): fp.write("%PDF-1.2\n") fp.write("% created by PIL PDF driver " + __version__ + "\n") - # - # Get image characteristics - - width, height = im.size - # FIXME: Should replace ASCIIHexDecode with RunLengthDecode (packbits) # or LZWDecode (tiff/lzw compression). Note that PDF 1.2 also supports # Flatedecode (zip compression). @@ -125,7 +124,7 @@ def _save(im, fp, filename): # # catalogue - xref[1] = fp.tell() + xref.append(fp.tell()) _obj( fp, 1, Type="/Catalog", @@ -134,89 +133,108 @@ def _save(im, fp, filename): # # pages + numberOfPages = 1 + if save_all: + try: + numberOfPages = im.n_frames + except AttributeError: + # Image format does not have n_frames. It is a single frame image + pass + pages = [str(pageNumber*3+4)+" 0 R" + for pageNumber in range(0, numberOfPages)] - xref[2] = fp.tell() + xref.append(fp.tell()) _obj( fp, 2, Type="/Pages", - Count=1, - Kids="[4 0 R]") + Count=len(pages), + Kids="["+"\n".join(pages)+"]") _endobj(fp) - # - # image + for pageNumber in range(0, numberOfPages): + im.seek(pageNumber) - op = io.BytesIO() + # + # image - if filter == "/ASCIIHexDecode": - if bits == 1: - # FIXME: the hex encoder doesn't support packed 1-bit - # images; do things the hard way... - data = im.tobytes("raw", "1") - im = Image.new("L", (len(data), 1), None) - im.putdata(data) - ImageFile._save(im, op, [("hex", (0, 0)+im.size, 0, im.mode)]) - elif filter == "/DCTDecode": - Image.SAVE["JPEG"](im, op, filename) - elif filter == "/FlateDecode": - ImageFile._save(im, op, [("zip", (0, 0)+im.size, 0, im.mode)]) - elif filter == "/RunLengthDecode": - ImageFile._save(im, op, [("packbits", (0, 0)+im.size, 0, im.mode)]) - else: - raise ValueError("unsupported PDF filter (%s)" % filter) + op = io.BytesIO() - xref[3] = fp.tell() - _obj( - fp, 3, - Type="/XObject", - Subtype="/Image", - Width=width, # * 72.0 / resolution, - Height=height, # * 72.0 / resolution, - Length=len(op.getvalue()), - Filter=filter, - BitsPerComponent=bits, - DecodeParams=params, - ColorSpace=colorspace) + if filter == "/ASCIIHexDecode": + if bits == 1: + # FIXME: the hex encoder doesn't support packed 1-bit + # images; do things the hard way... + data = im.tobytes("raw", "1") + im = Image.new("L", (len(data), 1), None) + im.putdata(data) + ImageFile._save(im, op, [("hex", (0, 0)+im.size, 0, im.mode)]) + elif filter == "/DCTDecode": + Image.SAVE["JPEG"](im, op, filename) + elif filter == "/FlateDecode": + ImageFile._save(im, op, [("zip", (0, 0)+im.size, 0, im.mode)]) + elif filter == "/RunLengthDecode": + ImageFile._save(im, op, [("packbits", (0, 0)+im.size, 0, im.mode)]) + else: + raise ValueError("unsupported PDF filter (%s)" % filter) - fp.write("stream\n") - fp.fp.write(op.getvalue()) - fp.write("\nendstream\n") + # + # Get image characteristics - _endobj(fp) + width, height = im.size - # - # page + xref.append(fp.tell()) + _obj( + fp, pageNumber*3+3, + Type="/XObject", + Subtype="/Image", + Width=width, # * 72.0 / resolution, + Height=height, # * 72.0 / resolution, + Length=len(op.getvalue()), + Filter=filter, + BitsPerComponent=bits, + DecodeParams=params, + ColorSpace=colorspace) - xref[4] = fp.tell() - _obj(fp, 4) - fp.write( - "<<\n/Type /Page\n/Parent 2 0 R\n" - "/Resources <<\n/ProcSet [ /PDF %s ]\n" - "/XObject << /image 3 0 R >>\n>>\n" - "/MediaBox [ 0 0 %d %d ]\n/Contents 5 0 R\n>>\n" % ( - procset, - int(width * 72.0 / resolution), - int(height * 72.0 / resolution))) - _endobj(fp) + fp.write("stream\n") + fp.fp.write(op.getvalue()) + fp.write("\nendstream\n") - # - # page contents + _endobj(fp) - op = TextWriter(io.BytesIO()) + # + # page - op.write( - "q %d 0 0 %d 0 0 cm /image Do Q\n" % ( - int(width * 72.0 / resolution), - int(height * 72.0 / resolution))) + xref.append(fp.tell()) + _obj(fp, pageNumber*3+4) + fp.write( + "<<\n/Type /Page\n/Parent 2 0 R\n" + "/Resources <<\n/ProcSet [ /PDF %s ]\n" + "/XObject << /image %d 0 R >>\n>>\n" + "/MediaBox [ 0 0 %d %d ]\n/Contents %d 0 R\n>>\n" % ( + procset, + pageNumber*3+3, + int(width * 72.0 / resolution), + int(height * 72.0 / resolution), + pageNumber*3+5)) + _endobj(fp) - xref[5] = fp.tell() - _obj(fp, 5, Length=len(op.fp.getvalue())) + # + # page contents - fp.write("stream\n") - fp.fp.write(op.fp.getvalue()) - fp.write("\nendstream\n") + op = TextWriter(io.BytesIO()) - _endobj(fp) + op.write( + "q %d 0 0 %d 0 0 cm /image Do Q\n" % ( + int(width * 72.0 / resolution), + int(height * 72.0 / resolution))) + + xref.append(fp.tell()) + _obj(fp, pageNumber*3+5, Length=len(op.fp.getvalue())) + + fp.write("stream\n") + fp.fp.write(op.fp.getvalue()) + fp.write("\nendstream\n") + + _endobj(fp) # # trailer @@ -226,12 +244,14 @@ def _save(im, fp, filename): fp.write("%010d 00000 n \n" % x) fp.write("trailer\n<<\n/Size %d\n/Root 1 0 R\n>>\n" % len(xref)) fp.write("startxref\n%d\n%%%%EOF\n" % startxref) - fp.flush() + if hasattr(fp, "flush"): + fp.flush() # # -------------------------------------------------------------------- Image.register_save("PDF", _save) +Image.register_save_all("PDF", _save_all) Image.register_extension("PDF", ".pdf") diff --git a/PIL/PixarImagePlugin.py b/PIL/PixarImagePlugin.py index ebf4c8c61..db2ee55f8 100644 --- a/PIL/PixarImagePlugin.py +++ b/PIL/PixarImagePlugin.py @@ -19,15 +19,14 @@ # See the README file for information on usage and redistribution. # -__version__ = "0.1" - from PIL import Image, ImageFile, _binary +__version__ = "0.1" + # # helpers i16 = _binary.i16le -i32 = _binary.i32le ## @@ -63,7 +62,7 @@ class PixarImageFile(ImageFile.ImageFile): # # -------------------------------------------------------------------- -Image.register_open("PIXAR", PixarImageFile) +Image.register_open(PixarImageFile.format, PixarImageFile) # # FIXME: what's the standard extension? diff --git a/PIL/PngImagePlugin.py b/PIL/PngImagePlugin.py index 214ff9385..d6778821b 100644 --- a/PIL/PngImagePlugin.py +++ b/PIL/PngImagePlugin.py @@ -33,12 +33,15 @@ from __future__ import print_function -__version__ = "0.9" - +import logging import re +import zlib from PIL import Image, ImageFile, ImagePalette, _binary -import zlib + +__version__ = "0.9" + +logger = logging.getLogger(__name__) i8 = _binary.i8 i16 = _binary.i16be @@ -129,8 +132,7 @@ class ChunkStream(object): def call(self, cid, pos, length): "Call the appropriate chunk handler" - if Image.DEBUG: - print("STREAM", cid, pos, length) + logger.debug("STREAM %s %s %s", cid, pos, length) return getattr(self, "chunk_" + cid.decode('ascii'))(pos, length) def crc(self, cid, data): @@ -293,9 +295,8 @@ class PngStream(ChunkStream): # Compression method 1 byte (0) # Compressed profile n bytes (zlib with deflate compression) i = s.find(b"\0") - if Image.DEBUG: - print("iCCP profile name", s[:i]) - print("Compression method", i8(s[i])) + logger.debug("iCCP profile name %s", s[:i]) + logger.debug("Compression method %s", i8(s[i])) comp_method = i8(s[i]) if comp_method != 0: raise SyntaxError("Unknown compression method %s in iCCP chunk" % @@ -507,8 +508,7 @@ class PngImageFile(ImageFile.ImageFile): except EOFError: break except AttributeError: - if Image.DEBUG: - print(cid, pos, length, "(unknown)") + logger.debug("%s %s %s (unknown)", cid, pos, length) s = ImageFile._safe_read(self.fp, length) self.png.crc(cid, s) @@ -762,10 +762,8 @@ def _save(im, fp, filename, chunk=putchunk, check=0): chunk(fp, b"IEND", b"") - try: + if hasattr(fp, "flush"): fp.flush() - except: - pass # -------------------------------------------------------------------- @@ -803,9 +801,9 @@ def getchunks(im, **params): # -------------------------------------------------------------------- # Registry -Image.register_open("PNG", PngImageFile, _accept) -Image.register_save("PNG", _save) +Image.register_open(PngImageFile.format, PngImageFile, _accept) +Image.register_save(PngImageFile.format, _save) -Image.register_extension("PNG", ".png") +Image.register_extension(PngImageFile.format, ".png") -Image.register_mime("PNG", "image/png") +Image.register_mime(PngImageFile.format, "image/png") diff --git a/PIL/PpmImagePlugin.py b/PIL/PpmImagePlugin.py index 954832451..68073cace 100644 --- a/PIL/PpmImagePlugin.py +++ b/PIL/PpmImagePlugin.py @@ -15,12 +15,12 @@ # -__version__ = "0.2" - import string from PIL import Image, ImageFile +__version__ = "0.2" + # # -------------------------------------------------------------------- @@ -93,6 +93,8 @@ class PpmImageFile(ImageFile.ImageFile): s = self.fp.read(1) if s not in b_whitespace: break + if s == b"": + raise ValueError("File does not extend beyond magic number") if s != b"#": break s = self.fp.readline() @@ -164,9 +166,9 @@ def _save(im, fp, filename): # # -------------------------------------------------------------------- -Image.register_open("PPM", PpmImageFile, _accept) -Image.register_save("PPM", _save) +Image.register_open(PpmImageFile.format, PpmImageFile, _accept) +Image.register_save(PpmImageFile.format, _save) -Image.register_extension("PPM", ".pbm") -Image.register_extension("PPM", ".pgm") -Image.register_extension("PPM", ".ppm") +Image.register_extension(PpmImageFile.format, ".pbm") +Image.register_extension(PpmImageFile.format, ".pgm") +Image.register_extension(PpmImageFile.format, ".ppm") diff --git a/PIL/PsdImagePlugin.py b/PIL/PsdImagePlugin.py index d30695adb..d06e320b0 100644 --- a/PIL/PsdImagePlugin.py +++ b/PIL/PsdImagePlugin.py @@ -136,6 +136,10 @@ class PsdImageFile(ImageFile.ImageFile): def n_frames(self): return len(self.layers) + @property + def is_animated(self): + return len(self.layers) > 1 + def seek(self, layer): # seek to given layer (1..max) if layer == self.frame: @@ -303,6 +307,6 @@ def _maketile(file, mode, bbox, channels): # -------------------------------------------------------------------- # registry -Image.register_open("PSD", PsdImageFile, _accept) +Image.register_open(PsdImageFile.format, PsdImageFile, _accept) -Image.register_extension("PSD", ".psd") +Image.register_extension(PsdImageFile.format, ".psd") diff --git a/PIL/PyAccess.py b/PIL/PyAccess.py index 4924facd5..faa868c12 100644 --- a/PIL/PyAccess.py +++ b/PIL/PyAccess.py @@ -22,10 +22,14 @@ from __future__ import print_function -from cffi import FFI +import logging import sys -DEBUG = 0 +from cffi import FFI + + +logger = logging.getLogger(__name__) + defs = """ struct Pixel_RGBA { @@ -50,8 +54,9 @@ class PyAccess(object): self.xsize = vals['xsize'] self.ysize = vals['ysize'] - if DEBUG: - print(vals) + # Debugging is polluting test traces, only useful here + # when hacking on PyAccess + # logger.debug("%s", vals) self._post_init() def _post_init(self): @@ -305,11 +310,8 @@ else: def new(img, readonly=False): access_type = mode_map.get(img.mode, None) if not access_type: - if DEBUG: - print("PyAccess Not Implemented: %s" % img.mode) + logger.debug("PyAccess Not Implemented: %s", img.mode) return None - if DEBUG: - print("New PyAccess: %s" % img.mode) return access_type(img, readonly) # End of file diff --git a/PIL/SgiImagePlugin.py b/PIL/SgiImagePlugin.py index 2b8fcd8e4..f890c7ef6 100644 --- a/PIL/SgiImagePlugin.py +++ b/PIL/SgiImagePlugin.py @@ -18,18 +18,16 @@ # -__version__ = "0.2" - - from PIL import Image, ImageFile, _binary +__version__ = "0.2" + i8 = _binary.i8 i16 = _binary.i16be -i32 = _binary.i32be def _accept(prefix): - return i16(prefix) == 474 + return len(prefix) >= 2 and i16(prefix) == 474 ## @@ -81,11 +79,11 @@ class SgiImageFile(ImageFile.ImageFile): # # registry -Image.register_open("SGI", SgiImageFile, _accept) +Image.register_open(SgiImageFile.format, SgiImageFile, _accept) -Image.register_extension("SGI", ".bw") -Image.register_extension("SGI", ".rgb") -Image.register_extension("SGI", ".rgba") -Image.register_extension("SGI", ".sgi") +Image.register_extension(SgiImageFile.format, ".bw") +Image.register_extension(SgiImageFile.format, ".rgb") +Image.register_extension(SgiImageFile.format, ".rgba") +Image.register_extension(SgiImageFile.format, ".sgi") # End of file diff --git a/PIL/SpiderImagePlugin.py b/PIL/SpiderImagePlugin.py index 7de5156b1..d5457893c 100644 --- a/PIL/SpiderImagePlugin.py +++ b/PIL/SpiderImagePlugin.py @@ -27,10 +27,10 @@ # image data from electron microscopy and tomography. # # Spider home page: -# http://www.wadsworth.org/spider_doc/spider/docs/spider.html +# http://spider.wadsworth.org/spider_doc/spider/docs/spider.html # # Details about the Spider image format: -# http://www.wadsworth.org/spider_doc/spider/docs/image_doc.html +# http://spider.wadsworth.org/spider_doc/spider/docs/image_doc.html # from __future__ import print_function @@ -50,6 +50,8 @@ def isInt(f): return 0 except ValueError: return 0 + except OverflowError: + return 0 iforms = [1, 3, -11, -12, -21, -22] @@ -158,6 +160,10 @@ class SpiderImageFile(ImageFile.ImageFile): def n_frames(self): return self._nimages + @property + def is_animated(self): + return self._nimages > 1 + # 1st image index is zero (although SPIDER imgnumber starts at 1) def tell(self): if self.imgnumber < 1: @@ -281,8 +287,8 @@ def _save_spider(im, fp, filename): # -------------------------------------------------------------------- -Image.register_open("SPIDER", SpiderImageFile) -Image.register_save("SPIDER", _save_spider) +Image.register_open(SpiderImageFile.format, SpiderImageFile) +Image.register_save(SpiderImageFile.format, _save_spider) if __name__ == "__main__": diff --git a/PIL/SunImagePlugin.py b/PIL/SunImagePlugin.py index e0a7aa6ee..af63144f2 100644 --- a/PIL/SunImagePlugin.py +++ b/PIL/SunImagePlugin.py @@ -17,17 +17,15 @@ # -__version__ = "0.3" - - from PIL import Image, ImageFile, ImagePalette, _binary -i16 = _binary.i16be +__version__ = "0.3" + i32 = _binary.i32be def _accept(prefix): - return i32(prefix) == 0x59a66a95 + return len(prefix) >= 4 and i32(prefix) == 0x59a66a95 ## @@ -78,6 +76,6 @@ class SunImageFile(ImageFile.ImageFile): # # registry -Image.register_open("SUN", SunImageFile, _accept) +Image.register_open(SunImageFile.format, SunImageFile, _accept) -Image.register_extension("SUN", ".ras") +Image.register_extension(SunImageFile.format, ".ras") diff --git a/PIL/TgaImagePlugin.py b/PIL/TgaImagePlugin.py index 46eafe8d0..a75ce2986 100644 --- a/PIL/TgaImagePlugin.py +++ b/PIL/TgaImagePlugin.py @@ -17,10 +17,10 @@ # -__version__ = "0.3" - from PIL import Image, ImageFile, ImagePalette, _binary +__version__ = "0.3" + # # -------------------------------------------------------------------- @@ -28,7 +28,6 @@ from PIL import Image, ImageFile, ImagePalette, _binary i8 = _binary.i8 i16 = _binary.i16le -i32 = _binary.i32le MODES = { @@ -193,7 +192,7 @@ def _save(im, fp, filename, check=0): # -------------------------------------------------------------------- # Registry -Image.register_open("TGA", TgaImageFile) -Image.register_save("TGA", _save) +Image.register_open(TgaImageFile.format, TgaImageFile) +Image.register_save(TgaImageFile.format, _save) -Image.register_extension("TGA", ".tga") +Image.register_extension(TgaImageFile.format, ".tga") diff --git a/PIL/TiffImagePlugin.py b/PIL/TiffImagePlugin.py index 59de84273..99beb0f97 100644 --- a/PIL/TiffImagePlugin.py +++ b/PIL/TiffImagePlugin.py @@ -39,26 +39,34 @@ # See the README file for information on usage and redistribution. # -from __future__ import print_function - -__version__ = "1.3.5" +from __future__ import division, print_function from PIL import Image, ImageFile from PIL import ImagePalette from PIL import _binary -from PIL._util import isStringType +from PIL import TiffTags -import warnings -import array -import sys import collections +from fractions import Fraction +from numbers import Number, Rational + +import io import itertools import os -import io +import struct +import sys +import warnings + +from .TiffTags import TYPES + + +__version__ = "1.3.5" +DEBUG = False # Needs to be merged with the new logging approach. # Set these to true to force use of libtiff for reading or writing. READ_LIBTIFF = False WRITE_LIBTIFF = False +IFD_LEGACY_API = True II = b"II" # little-endian (Intel style) MM = b"MM" # big-endian (Motorola style) @@ -66,25 +74,10 @@ MM = b"MM" # big-endian (Motorola style) i8 = _binary.i8 o8 = _binary.o8 -if sys.byteorder == "little": - native_prefix = II -else: - native_prefix = MM - # # -------------------------------------------------------------------- # Read TIFF files -il16 = _binary.i16le -il32 = _binary.i32le -ol16 = _binary.o16le -ol32 = _binary.o32le - -ib16 = _binary.i16be -ib32 = _binary.i32be -ob16 = _binary.o16be -ob32 = _binary.o32be - # a few tag names, just to make the code below a bit more readable IMAGEWIDTH = 256 IMAGELENGTH = 257 @@ -117,7 +110,7 @@ ICCPROFILE = 34675 EXIFIFD = 34665 XMP = 700 -# https://github.com/fiji/ImageJA/blob/master/src/main/java/ij/io/TiffDecoder.java +# https://github.com/imagej/ImageJA/blob/master/src/main/java/ij/io/TiffDecoder.java IMAGEJ_META_DATA_BYTE_COUNTS = 50838 IMAGEJ_META_DATA = 50839 @@ -144,75 +137,102 @@ COMPRESSION_INFO_REV = dict([(v, k) for (k, v) in COMPRESSION_INFO.items()]) OPEN_INFO = { # (ByteOrder, PhotoInterpretation, SampleFormat, FillOrder, BitsPerSample, # ExtraSamples) => mode, rawmode - (II, 0, 1, 1, (1,), ()): ("1", "1;I"), - (II, 0, 1, 2, (1,), ()): ("1", "1;IR"), - (II, 0, 1, 1, (8,), ()): ("L", "L;I"), - (II, 0, 1, 2, (8,), ()): ("L", "L;IR"), - (II, 0, 3, 1, (32,), ()): ("F", "F;32F"), - (II, 1, 1, 1, (1,), ()): ("1", "1"), - (II, 1, 1, 1, (4,), ()): ("L", "L;4"), - (II, 1, 1, 2, (1,), ()): ("1", "1;R"), - (II, 1, 1, 1, (8,), ()): ("L", "L"), - (II, 1, 1, 1, (8, 8), (2,)): ("LA", "LA"), - (II, 1, 1, 2, (8,), ()): ("L", "L;R"), - (II, 1, 1, 1, (12,), ()): ("I;16", "I;12"), - (II, 1, 1, 1, (16,), ()): ("I;16", "I;16"), - (II, 1, 2, 1, (16,), ()): ("I;16S", "I;16S"), - (II, 1, 1, 1, (32,), ()): ("I", "I;32N"), - (II, 1, 2, 1, (32,), ()): ("I", "I;32S"), - (II, 1, 3, 1, (32,), ()): ("F", "F;32F"), - (II, 2, 1, 1, (8, 8, 8), ()): ("RGB", "RGB"), - (II, 2, 1, 2, (8, 8, 8), ()): ("RGB", "RGB;R"), - (II, 2, 1, 1, (8, 8, 8, 8), ()): ("RGBA", "RGBA"), # missing ExtraSamples - (II, 2, 1, 1, (8, 8, 8, 8), (0,)): ("RGBX", "RGBX"), - (II, 2, 1, 1, (8, 8, 8, 8), (1,)): ("RGBA", "RGBa"), - (II, 2, 1, 1, (8, 8, 8, 8), (2,)): ("RGBA", "RGBA"), - (II, 2, 1, 1, (8, 8, 8, 8), (999,)): ("RGBA", "RGBA"), # Corel Draw 10 - (II, 3, 1, 1, (1,), ()): ("P", "P;1"), - (II, 3, 1, 2, (1,), ()): ("P", "P;1R"), - (II, 3, 1, 1, (2,), ()): ("P", "P;2"), - (II, 3, 1, 2, (2,), ()): ("P", "P;2R"), - (II, 3, 1, 1, (4,), ()): ("P", "P;4"), - (II, 3, 1, 2, (4,), ()): ("P", "P;4R"), - (II, 3, 1, 1, (8,), ()): ("P", "P"), - (II, 3, 1, 1, (8, 8), (2,)): ("PA", "PA"), - (II, 3, 1, 2, (8,), ()): ("P", "P;R"), - (II, 5, 1, 1, (8, 8, 8, 8), ()): ("CMYK", "CMYK"), - (II, 6, 1, 1, (8, 8, 8), ()): ("YCbCr", "YCbCr"), - (II, 8, 1, 1, (8, 8, 8), ()): ("LAB", "LAB"), + (II, 0, (1,), 1, (1,), ()): ("1", "1;I"), + (MM, 0, (1,), 1, (1,), ()): ("1", "1;I"), + (II, 0, (1,), 2, (1,), ()): ("1", "1;IR"), + (MM, 0, (1,), 2, (1,), ()): ("1", "1;IR"), + (II, 1, (1,), 1, (1,), ()): ("1", "1"), + (MM, 1, (1,), 1, (1,), ()): ("1", "1"), + (II, 1, (1,), 2, (1,), ()): ("1", "1;R"), + (MM, 1, (1,), 2, (1,), ()): ("1", "1;R"), - (MM, 0, 1, 1, (1,), ()): ("1", "1;I"), - (MM, 0, 1, 2, (1,), ()): ("1", "1;IR"), - (MM, 0, 1, 1, (8,), ()): ("L", "L;I"), - (MM, 0, 1, 2, (8,), ()): ("L", "L;IR"), - (MM, 1, 1, 1, (1,), ()): ("1", "1"), - (MM, 1, 1, 2, (1,), ()): ("1", "1;R"), - (MM, 1, 1, 1, (8,), ()): ("L", "L"), - (MM, 1, 1, 1, (8, 8), (2,)): ("LA", "LA"), - (MM, 1, 1, 2, (8,), ()): ("L", "L;R"), - (MM, 1, 1, 1, (16,), ()): ("I;16B", "I;16B"), - (MM, 1, 2, 1, (16,), ()): ("I;16BS", "I;16BS"), - (MM, 1, 2, 1, (32,), ()): ("I;32BS", "I;32BS"), - (MM, 1, 3, 1, (32,), ()): ("F", "F;32BF"), - (MM, 2, 1, 1, (8, 8, 8), ()): ("RGB", "RGB"), - (MM, 2, 1, 2, (8, 8, 8), ()): ("RGB", "RGB;R"), - (MM, 2, 1, 1, (8, 8, 8, 8), (0,)): ("RGBX", "RGBX"), - (MM, 2, 1, 1, (8, 8, 8, 8), (1,)): ("RGBA", "RGBa"), - (MM, 2, 1, 1, (8, 8, 8, 8), (2,)): ("RGBA", "RGBA"), - (MM, 2, 1, 1, (8, 8, 8, 8), (999,)): ("RGBA", "RGBA"), # Corel Draw 10 - (MM, 3, 1, 1, (1,), ()): ("P", "P;1"), - (MM, 3, 1, 2, (1,), ()): ("P", "P;1R"), - (MM, 3, 1, 1, (2,), ()): ("P", "P;2"), - (MM, 3, 1, 2, (2,), ()): ("P", "P;2R"), - (MM, 3, 1, 1, (4,), ()): ("P", "P;4"), - (MM, 3, 1, 2, (4,), ()): ("P", "P;4R"), - (MM, 3, 1, 1, (8,), ()): ("P", "P"), - (MM, 3, 1, 1, (8, 8), (2,)): ("PA", "PA"), - (MM, 3, 1, 2, (8,), ()): ("P", "P;R"), - (MM, 5, 1, 1, (8, 8, 8, 8), ()): ("CMYK", "CMYK"), - (MM, 6, 1, 1, (8, 8, 8), ()): ("YCbCr", "YCbCr"), - (MM, 8, 1, 1, (8, 8, 8), ()): ("LAB", "LAB"), + (II, 0, (1,), 1, (2,), ()): ("L", "L;2I"), + (MM, 0, (1,), 1, (2,), ()): ("L", "L;2I"), + (II, 0, (1,), 2, (2,), ()): ("L", "L;2IR"), + (MM, 0, (1,), 2, (2,), ()): ("L", "L;2IR"), + (II, 1, (1,), 1, (2,), ()): ("L", "L;2"), + (MM, 1, (1,), 1, (2,), ()): ("L", "L;2"), + (II, 1, (1,), 2, (2,), ()): ("L", "L;2R"), + (MM, 1, (1,), 2, (2,), ()): ("L", "L;2R"), + (II, 0, (1,), 1, (4,), ()): ("L", "L;4I"), + (MM, 0, (1,), 1, (4,), ()): ("L", "L;4I"), + (II, 0, (1,), 2, (4,), ()): ("L", "L;4IR"), + (MM, 0, (1,), 2, (4,), ()): ("L", "L;4IR"), + (II, 1, (1,), 1, (4,), ()): ("L", "L;4"), + (MM, 1, (1,), 1, (4,), ()): ("L", "L;4"), + (II, 1, (1,), 2, (4,), ()): ("L", "L;4R"), + (MM, 1, (1,), 2, (4,), ()): ("L", "L;4R"), + + (II, 0, (1,), 1, (8,), ()): ("L", "L;I"), + (MM, 0, (1,), 1, (8,), ()): ("L", "L;I"), + (II, 0, (1,), 2, (8,), ()): ("L", "L;IR"), + (MM, 0, (1,), 2, (8,), ()): ("L", "L;IR"), + (II, 1, (1,), 1, (8,), ()): ("L", "L"), + (MM, 1, (1,), 1, (8,), ()): ("L", "L"), + (II, 1, (1,), 2, (8,), ()): ("L", "L;R"), + (MM, 1, (1,), 2, (8,), ()): ("L", "L;R"), + + (II, 1, (1,), 1, (12,), ()): ("I;16", "I;12"), + + (II, 1, (1,), 1, (16,), ()): ("I;16", "I;16"), + (MM, 1, (1,), 1, (16,), ()): ("I;16B", "I;16B"), + (II, 1, (2,), 1, (16,), ()): ("I;16S", "I;16S"), + (MM, 1, (2,), 1, (16,), ()): ("I;16BS", "I;16BS"), + + (II, 0, (3,), 1, (32,), ()): ("F", "F;32F"), + (MM, 0, (3,), 1, (32,), ()): ("F", "F;32BF"), + (II, 1, (1,), 1, (32,), ()): ("I", "I;32N"), + (II, 1, (2,), 1, (32,), ()): ("I", "I;32S"), + (MM, 1, (2,), 1, (32,), ()): ("I;32BS", "I;32BS"), + (II, 1, (3,), 1, (32,), ()): ("F", "F;32F"), + (MM, 1, (3,), 1, (32,), ()): ("F", "F;32BF"), + + (II, 1, (1,), 1, (8, 8), (2,)): ("LA", "LA"), + (MM, 1, (1,), 1, (8, 8), (2,)): ("LA", "LA"), + + (II, 2, (1,), 1, (8, 8, 8), ()): ("RGB", "RGB"), + (MM, 2, (1,), 1, (8, 8, 8), ()): ("RGB", "RGB"), + (II, 2, (1,), 2, (8, 8, 8), ()): ("RGB", "RGB;R"), + (MM, 2, (1,), 2, (8, 8, 8), ()): ("RGB", "RGB;R"), + (II, 2, (1,), 1, (8, 8, 8, 8), ()): ("RGBA", "RGBA"), # missing ExtraSamples + (MM, 2, (1,), 1, (8, 8, 8, 8), ()): ("RGBA", "RGBA"), # missing ExtraSamples + (II, 2, (1,), 1, (8, 8, 8, 8), (0,)): ("RGBX", "RGBX"), + (MM, 2, (1,), 1, (8, 8, 8, 8), (0,)): ("RGBX", "RGBX"), + (II, 2, (1,), 1, (8, 8, 8, 8), (1,)): ("RGBA", "RGBa"), + (MM, 2, (1,), 1, (8, 8, 8, 8), (1,)): ("RGBA", "RGBa"), + (II, 2, (1,), 1, (8, 8, 8, 8), (2,)): ("RGBA", "RGBA"), + (MM, 2, (1,), 1, (8, 8, 8, 8), (2,)): ("RGBA", "RGBA"), + (II, 2, (1,), 1, (8, 8, 8, 8), (999,)): ("RGBA", "RGBA"), # Corel Draw 10 + (MM, 2, (1,), 1, (8, 8, 8, 8), (999,)): ("RGBA", "RGBA"), # Corel Draw 10 + + (II, 3, (1,), 1, (1,), ()): ("P", "P;1"), + (MM, 3, (1,), 1, (1,), ()): ("P", "P;1"), + (II, 3, (1,), 2, (1,), ()): ("P", "P;1R"), + (MM, 3, (1,), 2, (1,), ()): ("P", "P;1R"), + (II, 3, (1,), 1, (2,), ()): ("P", "P;2"), + (MM, 3, (1,), 1, (2,), ()): ("P", "P;2"), + (II, 3, (1,), 2, (2,), ()): ("P", "P;2R"), + (MM, 3, (1,), 2, (2,), ()): ("P", "P;2R"), + (II, 3, (1,), 1, (4,), ()): ("P", "P;4"), + (MM, 3, (1,), 1, (4,), ()): ("P", "P;4"), + (II, 3, (1,), 2, (4,), ()): ("P", "P;4R"), + (MM, 3, (1,), 2, (4,), ()): ("P", "P;4R"), + (II, 3, (1,), 1, (8,), ()): ("P", "P"), + (MM, 3, (1,), 1, (8,), ()): ("P", "P"), + (II, 3, (1,), 1, (8, 8), (2,)): ("PA", "PA"), + (MM, 3, (1,), 1, (8, 8), (2,)): ("PA", "PA"), + (II, 3, (1,), 2, (8,), ()): ("P", "P;R"), + (MM, 3, (1,), 2, (8,), ()): ("P", "P;R"), + + (II, 5, (1,), 1, (8, 8, 8, 8), ()): ("CMYK", "CMYK"), + (MM, 5, (1,), 1, (8, 8, 8, 8), ()): ("CMYK", "CMYK"), + + (II, 6, (1,), 1, (8, 8, 8), ()): ("YCbCr", "YCbCr"), + (MM, 6, (1,), 1, (8, 8, 8), ()): ("YCbCr", "YCbCr"), + + (II, 8, (1,), 1, (8, 8, 8), ()): ("LAB", "LAB"), + (MM, 8, (1,), 1, (8, 8, 8), ()): ("LAB", "LAB"), } PREFIXES = [b"MM\000\052", b"II\052\000", b"II\xBC\000"] @@ -222,407 +242,648 @@ def _accept(prefix): return prefix[:4] in PREFIXES +def _limit_rational(val, max_val): + inv = abs(val) > 1 + n_d = IFDRational(1 / val if inv else val).limit_rational(max_val) + return n_d[::-1] if inv else n_d + ## # Wrapper for TIFF IFDs. -class ImageFileDirectory(collections.MutableMapping): - """ This class represents a TIFF tag directory. To speed things - up, we don't decode tags unless they're asked for. - - Exposes a dictionary interface of the tags in the directory - ImageFileDirectory[key] = value - value = ImageFileDirectory[key] - - Also contains a dictionary of tag types as read from the tiff - image file, 'ImageFileDirectory.tagtype' +_load_dispatch = {} +_write_dispatch = {} - Data Structures: - 'public' - * self.tagtype = {} Key: numerical tiff tag number - Value: integer corresponding to the data type from - `TiffTags.TYPES` +class IFDRational(Rational): + """ Implements a rational class where 0/0 is a legal value to match + the in the wild use of exif rationals. - 'internal' - * self.tags = {} Key: numerical tiff tag number - Value: Decoded data, Generally a tuple. - * If set from __setval__ -- always a tuple - * Numeric types -- always a tuple - * String type -- not a tuple, returned as string - * Undefined data -- not a tuple, returned as bytes - * Byte -- not a tuple, returned as byte. - * self.tagdata = {} Key: numerical tiff tag number - Value: undecoded byte string from file + e.g., DigitalZoomRatio - 0.00/0.00 indicates that no digital zoom was used + """ + """ If the denominator is 0, store this as a float('nan'), otherwise store + as a fractions.Fraction(). Delegate as appropriate - Tags will be found in either self.tags or self.tagdata, but - not both. The union of the two should contain all the tags - from the Tiff image file. External classes shouldn't - reference these unless they're really sure what they're doing. + """ + + __slots__ = ('_numerator', '_denominator', '_val') + + def __init__(self, value, denominator=1): + """ + :param value: either an integer numerator, a + float/rational/other number, or an IFDRational + :param denominator: Optional integer denominator + """ + self._denominator = denominator + self._numerator = value + self._val = float(1) + + if type(value) == Fraction: + self._numerator = value.numerator + self._denominator = value.denominator + self._val = value + + if type(value) == IFDRational: + self._denominator = value.denominator + self._numerator = value.numerator + self._val = value._val + return + + if denominator == 0: + self._val = float('nan') + return + + elif denominator == 1: + if sys.hexversion < 0x2070000 and type(value) == float: + # python 2.6 is different. + self._val = Fraction.from_float(value) + else: + self._val = Fraction(value) + else: + self._val = Fraction(value, denominator) + + @property + def numerator(a): + return a._numerator + + @property + def denominator(a): + return a._denominator + + def limit_rational(self, max_denominator): """ - def __init__(self, prefix=II): + :param max_denominator: Integer, the maximum denominator value + :returns: Tuple of (numerator, denominator) """ - :prefix: 'II'|'MM' tiff endianness + + if self.denominator == 0: + return (self.numerator, self.denominator) + + f = self._val.limit_denominator(max_denominator) + return (f.numerator, f.denominator) + + def __repr__(self): + return str(float(self._val)) + + def __hash__(self): + return self._val.__hash__() + + def __eq__(self, other): + return self._val == other + + def _delegate(op): + def delegate(self, *args): + return getattr(self._val, op)(*args) + return delegate + + """ a = ['add','radd', 'sub', 'rsub','div', 'rdiv', 'mul', 'rmul', + 'truediv', 'rtruediv', 'floordiv', + 'rfloordiv','mod','rmod', 'pow','rpow', 'pos', 'neg', + 'abs', 'trunc', 'lt', 'gt', 'le', 'ge', 'nonzero', + 'ceil', 'floor', 'round'] + print "\n".join("__%s__ = _delegate('__%s__')" % (s,s) for s in a) """ - self.prefix = prefix[:2] - if self.prefix == MM: - self.i16, self.i32 = ib16, ib32 - self.o16, self.o32 = ob16, ob32 - elif self.prefix == II: - self.i16, self.i32 = il16, il32 - self.o16, self.o32 = ol16, ol32 + + __add__ = _delegate('__add__') + __radd__ = _delegate('__radd__') + __sub__ = _delegate('__sub__') + __rsub__ = _delegate('__rsub__') + __div__ = _delegate('__div__') + __rdiv__ = _delegate('__rdiv__') + __mul__ = _delegate('__mul__') + __rmul__ = _delegate('__rmul__') + __truediv__ = _delegate('__truediv__') + __rtruediv__ = _delegate('__rtruediv__') + __floordiv__ = _delegate('__floordiv__') + __rfloordiv__ = _delegate('__rfloordiv__') + __mod__ = _delegate('__mod__') + __rmod__ = _delegate('__rmod__') + __pow__ = _delegate('__pow__') + __rpow__ = _delegate('__rpow__') + __pos__ = _delegate('__pos__') + __neg__ = _delegate('__neg__') + __abs__ = _delegate('__abs__') + __trunc__ = _delegate('__trunc__') + __lt__ = _delegate('__lt__') + __gt__ = _delegate('__gt__') + __le__ = _delegate('__le__') + __ge__ = _delegate('__ge__') + __nonzero__ = _delegate('__nonzero__') + __ceil__ = _delegate('__ceil__') + __floor__ = _delegate('__floor__') + __round__ = _delegate('__round__') + + +class ImageFileDirectory_v2(collections.MutableMapping): + """This class represents a TIFF tag directory. To speed things up, we + don't decode tags unless they're asked for. + + Exposes a dictionary interface of the tags in the directory:: + + ifd = ImageFileDirectory_v2() + ifd[key] = 'Some Data' + ifd.tagtype[key] = 2 + print(ifd[key]) + 'Some Data' + + Individual values are returned as the strings or numbers, sequences are + returned as tuples of the values. + + The tiff metadata type of each item is stored in a dictionary of + tag types in + `~PIL.TiffImagePlugin.ImageFileDirectory_v2.tagtype`. The types + are read from a tiff file, guessed from the type added, or added + manually. + + Data Structures: + + * self.tagtype = {} + + * Key: numerical tiff tag number + * Value: integer corresponding to the data type from `~PIL.TiffTags.TYPES` + + .. versionadded:: 3.0.0 + """ + """ + Documentation: + + 'internal' data structures: + * self._tags_v2 = {} Key: numerical tiff tag number + Value: decoded data, as tuple for multiple values + * self._tagdata = {} Key: numerical tiff tag number + Value: undecoded byte string from file + * self._tags_v1 = {} Key: numerical tiff tag number + Value: decoded data in the v1 format + + Tags will be found in the private attributes self._tagdata, and in + self._tags_v2 once decoded. + + Self.legacy_api is a value for internal use, and shouldn't be + changed from outside code. In cooperation with the + ImageFileDirectory_v1 class, if legacy_api is true, then decoded + tags will be populated into both _tags_v1 and _tags_v2. _Tags_v2 + will be used if this IFD is used in the TIFF save routine. Tags + should be read from tags_v1 if legacy_api == true. + + """ + + def __init__(self, ifh=b"II\052\0\0\0\0\0", prefix=None): + """Initialize an ImageFileDirectory. + + To construct an ImageFileDirectory from a real file, pass the 8-byte + magic header to the constructor. To only set the endianness, pass it + as the 'prefix' keyword argument. + + :param ifh: One of the accepted magic headers (cf. PREFIXES); also sets + endianness. + :param prefix: Override the endianness of the file. + """ + if ifh[:4] not in PREFIXES: + raise SyntaxError("not a TIFF file (header %r not valid)" % ifh) + self._prefix = prefix if prefix is not None else ifh[:2] + if self._prefix == MM: + self._endian = ">" + elif self._prefix == II: + self._endian = "<" else: raise SyntaxError("not a TIFF IFD") self.reset() + self.next, = self._unpack("L", ifh[4:]) + self._legacy_api = False + + prefix = property(lambda self: self._prefix) + offset = property(lambda self: self._offset) + legacy_api = property(lambda self: self._legacy_api) + + @legacy_api.setter + def legacy_api(self, value): + raise Exception("Not allowing setting of legacy api") def reset(self): - #: Tags is an incomplete dictionary of the tags of the image. - #: For a complete dictionary, use the as_dict method. - self.tags = {} - self.tagdata = {} - self.tagtype = {} # added 2008-06-05 by Florian Hoech - self.next = None - self.offset = None + self._tags_v1 = {} # will remain empty if legacy_api is false + self._tags_v2 = {} # main tag storage + self._tagdata = {} + self.tagtype = {} # added 2008-06-05 by Florian Hoech + self._next = None + self._offset = None def __str__(self): - return str(self.as_dict()) + return str(dict(self)) def as_dict(self): - """Return a dictionary of the image's tags.""" - return dict(self.items()) + """Return a dictionary of the image's tags. + + .. deprecated:: 3.0.0 + """ + warnings.warn("as_dict() is deprecated. " + + "Please use dict(ifd) instead.", DeprecationWarning) + return dict(self) def named(self): """ + :returns: dict of name|key: value + Returns the complete tag dictionary, with named tags where possible. """ - from PIL import TiffTags - result = {} - for tag_code, value in self.items(): - tag_name = TiffTags.TAGS.get(tag_code, tag_code) - result[tag_name] = value - return result - - # dictionary API + return dict((TiffTags.lookup(code).name, value) + for code, value in self.items()) def __len__(self): - return len(self.tagdata) + len(self.tags) + return len(set(self._tagdata) | set(self._tags_v2)) def __getitem__(self, tag): - try: - return self.tags[tag] - except KeyError: - data = self.tagdata[tag] # unpack on the fly - type = self.tagtype[tag] - size, handler = self.load_dispatch[type] - self.tags[tag] = data = handler(self, data) - del self.tagdata[tag] - return data - - def getscalar(self, tag, default=None): - try: - value = self[tag] - if len(value) != 1: - if tag == SAMPLEFORMAT: - # work around broken (?) matrox library - # (from Ted Wright, via Bob Klimek) - raise KeyError # use default - raise ValueError("not a scalar") - return value[0] - except KeyError: - if default is None: - raise - return default + if tag not in self._tags_v2: # unpack on the fly + data = self._tagdata[tag] + typ = self.tagtype[tag] + size, handler = self._load_dispatch[typ] + self[tag] = handler(self, data, self.legacy_api) # check type + val = self._tags_v2[tag] + if self.legacy_api and not isinstance(val, (tuple, bytes)): + val = val, + return val def __contains__(self, tag): - return tag in self.tags or tag in self.tagdata + return tag in self._tags_v2 or tag in self._tagdata if bytes is str: def has_key(self, tag): return tag in self def __setitem__(self, tag, value): - # tags are tuples for integers - # tags are not tuples for byte, string, and undefined data. - # see load_* - if not isinstance(value, tuple): - value = (value,) - self.tags[tag] = value + self._setitem(tag, value, self.legacy_api) + + def _setitem(self, tag, value, legacy_api): + basetypes = (Number, bytes, str) + if bytes is str: + basetypes += unicode, + + info = TiffTags.lookup(tag) + values = [value] if isinstance(value, basetypes) else value + + if tag not in self.tagtype: + if info.type: + self.tagtype[tag] = info.type + else: + self.tagtype[tag] = 7 + if all(isinstance(v, IFDRational) for v in values): + self.tagtype[tag] = 5 + elif all(isinstance(v, int) for v in values): + if all(v < 2 ** 16 for v in values): + self.tagtype[tag] = 3 + else: + self.tagtype[tag] = 4 + elif all(isinstance(v, float) for v in values): + self.tagtype[tag] = 12 + else: + if bytes is str: + # Never treat data as binary by default on Python 2. + self.tagtype[tag] = 2 + else: + if all(isinstance(v, str) for v in values): + self.tagtype[tag] = 2 + + if self.tagtype[tag] == 7 and bytes is not str: + values = [value.encode("ascii", 'replace') if isinstance(value, str) else value + for value in values] + + values = tuple(info.cvt_enum(value) for value in values) + + dest = self._tags_v1 if legacy_api else self._tags_v2 + + if info.length == 1: + if legacy_api and self.tagtype[tag] in [5, 10]: + values = values, + dest[tag], = values + else: + dest[tag] = values def __delitem__(self, tag): - self.tags.pop(tag, self.tagdata.pop(tag, None)) + self._tags_v2.pop(tag, None) + self._tags_v1.pop(tag, None) + self._tagdata.pop(tag, None) def __iter__(self): - return itertools.chain(self.tags.__iter__(), self.tagdata.__iter__()) + return iter(set(self._tagdata) | set(self._tags_v2)) - def items(self): - keys = list(self.__iter__()) - values = [self[key] for key in keys] - return zip(keys, values) + def _unpack(self, fmt, data): + return struct.unpack(self._endian + fmt, data) - # load primitives + def _pack(self, fmt, *values): + return struct.pack(self._endian + fmt, *values) - load_dispatch = {} + def _register_loader(idx, size): + def decorator(func): + from PIL.TiffTags import TYPES + if func.__name__.startswith("load_"): + TYPES[idx] = func.__name__[5:].replace("_", " ") + _load_dispatch[idx] = size, func + return func + return decorator - def load_byte(self, data): + def _register_writer(idx): + def decorator(func): + _write_dispatch[idx] = func + return func + return decorator + + def _register_basic(idx_fmt_name): + from PIL.TiffTags import TYPES + idx, fmt, name = idx_fmt_name + TYPES[idx] = name + size = struct.calcsize("=" + fmt) + _load_dispatch[idx] = size, lambda self, data, legacy_api=True: ( + self._unpack("{0}{1}".format(len(data) // size, fmt), data)) + _write_dispatch[idx] = lambda self, *values: ( + b"".join(self._pack(fmt, value) for value in values)) + + list(map(_register_basic, + [(3, "H", "short"), (4, "L", "long"), + (6, "b", "signed byte"), (8, "h", "signed short"), + (9, "l", "signed long"), (11, "f", "float"), (12, "d", "double")])) + + @_register_loader(1, 1) # Basic type, except for the legacy API. + def load_byte(self, data, legacy_api=True): + return (data if legacy_api else + tuple(map(ord, data) if bytes is str else data)) + + @_register_writer(1) # Basic type, except for the legacy API. + def write_byte(self, data): return data - load_dispatch[1] = (1, load_byte) - def load_string(self, data): - if data[-1:] == b'\0': + @_register_loader(2, 1) + def load_string(self, data, legacy_api=True): + if data.endswith(b"\0"): data = data[:-1] - return data.decode('latin-1', 'replace') - load_dispatch[2] = (1, load_string) + return data.decode("latin-1", "replace") - def load_short(self, data): - l = [] - for i in range(0, len(data), 2): - l.append(self.i16(data, i)) - return tuple(l) - load_dispatch[3] = (2, load_short) + @_register_writer(2) + def write_string(self, value): + # remerge of https://github.com/python-pillow/Pillow/pull/1416 + if sys.version_info[0] == 2: + value = value.decode('ascii', 'replace') + return b"" + value.encode('ascii', 'replace') + b"\0" - def load_long(self, data): - l = [] - for i in range(0, len(data), 4): - l.append(self.i32(data, i)) - return tuple(l) - load_dispatch[4] = (4, load_long) + @_register_loader(5, 8) + def load_rational(self, data, legacy_api=True): + vals = self._unpack("{0}L".format(len(data) // 4), data) + combine = lambda a, b: (a, b) if legacy_api else IFDRational(a, b) + return tuple(combine(num, denom) + for num, denom in zip(vals[::2], vals[1::2])) - def load_rational(self, data): - l = [] - for i in range(0, len(data), 8): - l.append((self.i32(data, i), self.i32(data, i+4))) - return tuple(l) - load_dispatch[5] = (8, load_rational) + @_register_writer(5) + def write_rational(self, *values): + return b"".join(self._pack("2L", *_limit_rational(frac, 2 ** 31)) + for frac in values) - def load_float(self, data): - a = array.array("f", data) - if self.prefix != native_prefix: - a.byteswap() - return tuple(a) - load_dispatch[11] = (4, load_float) - - def load_double(self, data): - a = array.array("d", data) - if self.prefix != native_prefix: - a.byteswap() - return tuple(a) - load_dispatch[12] = (8, load_double) - - def load_undefined(self, data): - # Untyped data + @_register_loader(7, 1) + def load_undefined(self, data, legacy_api=True): return data - load_dispatch[7] = (1, load_undefined) + + @_register_writer(7) + def write_undefined(self, value): + return value + + @_register_loader(10, 8) + def load_signed_rational(self, data, legacy_api=True): + vals = self._unpack("{0}l".format(len(data) // 4), data) + combine = lambda a, b: (a, b) if legacy_api else IFDRational(a, b) + return tuple(combine(num, denom) + for num, denom in zip(vals[::2], vals[1::2])) + + @_register_writer(10) + def write_signed_rational(self, *values): + return b"".join(self._pack("2L", *_limit_rational(frac, 2 ** 30)) + for frac in values) + + def _ensure_read(self, fp, size): + ret = fp.read(size) + if len(ret) != size: + raise IOError("Corrupt EXIF data. " + + "Expecting to read %d bytes but only got %d. " % + (size, len(ret))) + return ret def load(self, fp): - # load tag dictionary self.reset() - self.offset = fp.tell() + self._offset = fp.tell() - i16 = self.i16 - i32 = self.i32 + try: + for i in range(self._unpack("H", self._ensure_read(fp, 2))[0]): + tag, typ, count, data = self._unpack("HHL4s", self._ensure_read(fp, 12)) + if DEBUG: + tagname = TiffTags.lookup(tag).name + typname = TYPES.get(typ, "unknown") + print("tag: %s (%d) - type: %s (%d)" % + (tagname, tag, typname, typ), end=" ") - for i in range(i16(fp.read(2))): - - ifd = fp.read(12) - if len(ifd) != 12: - warnings.warn("Possibly corrupt EXIF data. " - "Expecting to read 12 bytes but only got %d." - % (len(ifd))) - continue - - tag, typ = i16(ifd), i16(ifd, 2) - - if Image.DEBUG: - from PIL import TiffTags - tagname = TiffTags.TAGS.get(tag, "unknown") - typname = TiffTags.TYPES.get(typ, "unknown") - print("tag: %s (%d)" % (tagname, tag), end=' ') - print("- type: %s (%d)" % (typname, typ), end=' ') - - try: - dispatch = self.load_dispatch[typ] - except KeyError: - if Image.DEBUG: - print("- unsupported type", typ) - continue # ignore unsupported type - - size, handler = dispatch - - size = size * i32(ifd, 4) - - # Get and expand tag value - if size > 4: - here = fp.tell() - if Image.DEBUG: - print("Tag Location: %s" % here) - fp.seek(i32(ifd, 8)) - if Image.DEBUG: - print("Data Location: %s" % fp.tell()) - data = ImageFile._safe_read(fp, size) - fp.seek(here) - else: - data = ifd[8:8+size] - - if len(data) != size: - warnings.warn("Possibly corrupt EXIF data. " - "Expecting to read %d bytes but only got %d. " - "Skipping tag %s" % (size, len(data), tag)) - continue - - self.tagdata[tag] = data - self.tagtype[tag] = typ - - if Image.DEBUG: - if tag in (COLORMAP, IPTC_NAA_CHUNK, PHOTOSHOP_CHUNK, - ICCPROFILE, XMP): - print("- value: " % size) + try: + unit_size, handler = self._load_dispatch[typ] + except KeyError: + if DEBUG: + print("- unsupported type", typ) + continue # ignore unsupported type + size = count * unit_size + if size > 4: + here = fp.tell() + offset, = self._unpack("L", data) + if DEBUG: + print("Tag Location: %s - Data Location: %s" % + (here, offset), end=" ") + fp.seek(offset) + data = ImageFile._safe_read(fp, size) + fp.seek(here) else: - print("- value:", self[tag]) + data = data[:size] - ifd = fp.read(4) - if len(ifd) != 4: - warnings.warn("Possibly corrupt EXIF data. " - "Expecting to read 4 bytes but only got %d." - % (len(ifd))) + if len(data) != size: + warnings.warn("Possibly corrupt EXIF data. " + "Expecting to read %d bytes but only got %d. " + "Skipping tag %s" % (size, len(data), tag)) + continue + + self._tagdata[tag] = data + self.tagtype[tag] = typ + + if DEBUG: + if size > 32: + print("- value: " % size) + else: + print("- value:", self[tag]) + + self.next, = self._unpack("L", self._ensure_read(fp, 4)) + except IOError as msg: + warnings.warn(str(msg)) return - self.next = i32(ifd) - - # save primitives - def save(self, fp): - o16 = self.o16 - o32 = self.o32 + 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)) - fp.write(o16(len(self.tags))) - - # always write in ascending tag order - tags = sorted(self.tags.items()) - - directory = [] - append = directory.append - - offset = fp.tell() + len(self.tags) * 12 + 4 + # FIXME What about tagdata? + fp.write(self._pack("H", len(self._tags_v2))) + entries = [] + offset = fp.tell() + len(self._tags_v2) * 12 + 4 stripoffsets = None # pass 1: convert tags to binary format - for tag, value in tags: - - typ = None - - if tag in self.tagtype: - typ = self.tagtype[tag] - - if Image.DEBUG: + # always write tags in ascending order + for tag, value in sorted(self._tags_v2.items()): + if tag == STRIPOFFSETS: + stripoffsets = len(entries) + typ = self.tagtype.get(tag) + if DEBUG: print("Tag %s, Type: %s, Value: %s" % (tag, typ, value)) - - if typ == 1: - # byte data - if isinstance(value, tuple): - data = value = value[-1] + values = value if isinstance(value, tuple) else (value,) + data = self._write_dispatch[typ](self, *values) + if DEBUG: + tagname = TiffTags.lookup(tag).name + typname = TYPES.get(typ, "unknown") + print("save: %s (%d) - type: %s (%d)" % + (tagname, tag, typname, typ), end=" ") + if len(data) >= 16: + print("- value: " % len(data)) else: - data = value - elif typ == 7: - # untyped data - data = value = b"".join(value) - elif typ in (11, 12): - # float value - tmap = {11: 'f', 12: 'd'} - if not isinstance(value, tuple): - value = (value,) - a = array.array(tmap[typ], value) - if self.prefix != native_prefix: - a.byteswap() - data = a.tostring() - elif isStringType(value[0]): - # string data - if isinstance(value, tuple): - value = value[-1] - typ = 2 - # was b'\0'.join(str), which led to \x00a\x00b sorts - # of strings which I don't see in in the wild tiffs - # and doesn't match the tiff spec: 8-bit byte that - # contains a 7-bit ASCII code; the last byte must be - # NUL (binary zero). Also, I don't think this was well - # excersized before. - data = value = b"" + value.encode('ascii', 'replace') + b"\0" + print("- value:", values) + + # count is sum of lengths for string and arbitrary data + count = len(data) if typ in [2, 7] else len(values) + # figure out if data fits into the entry + if len(data) <= 4: + entries.append((tag, typ, count, data.ljust(4, b"\0"), b"")) else: - # integer data - if tag == STRIPOFFSETS: - stripoffsets = len(directory) - typ = 4 # to avoid catch-22 - elif tag in (X_RESOLUTION, Y_RESOLUTION) or typ == 5: - # identify rational data fields - typ = 5 - if isinstance(value[0], tuple): - # long name for flatten - value = tuple(itertools.chain.from_iterable(value)) - elif not typ: - typ = 3 - for v in value: - if v >= 65536: - typ = 4 - if typ == 3: - data = b"".join(map(o16, value)) - else: - data = b"".join(map(o32, value)) - - if Image.DEBUG: - from PIL import TiffTags - tagname = TiffTags.TAGS.get(tag, "unknown") - typname = TiffTags.TYPES.get(typ, "unknown") - print("save: %s (%d)" % (tagname, tag), end=' ') - print("- type: %s (%d)" % (typname, typ), end=' ') - if tag in (COLORMAP, IPTC_NAA_CHUNK, PHOTOSHOP_CHUNK, - ICCPROFILE, XMP): - size = len(data) - print("- value: " % size) - else: - print("- value:", value) - - # figure out if data fits into the directory - if len(data) == 4: - append((tag, typ, len(value), data, b"")) - elif len(data) < 4: - append((tag, typ, len(value), data + (4-len(data))*b"\0", b"")) - else: - count = len(value) - if typ == 5: - count = count // 2 # adjust for rational data field - - append((tag, typ, count, o32(offset), data)) - offset += len(data) - if offset & 1: - offset += 1 # word padding + entries.append((tag, typ, count, self._pack("L", offset), data)) + offset += (len(data) + 1) // 2 * 2 # pad to word # update strip offset data to point beyond auxiliary data if stripoffsets is not None: - tag, typ, count, value, data = directory[stripoffsets] - assert not data, "multistrip support not yet implemented" - value = o32(self.i32(value) + offset) - directory[stripoffsets] = tag, typ, count, value, data + tag, typ, count, value, data = entries[stripoffsets] + if data: + raise NotImplementedError( + "multistrip support not yet implemented") + value = self._pack("L", self._unpack("L", value)[0] + offset) + entries[stripoffsets] = tag, typ, count, value, data - # pass 2: write directory to file - for tag, typ, count, value, data in directory: - if Image.DEBUG > 1: + # pass 2: write entries to file + for tag, typ, count, value, data in entries: + if DEBUG > 1: print(tag, typ, count, repr(value), repr(data)) - fp.write(o16(tag) + o16(typ) + o32(count) + value) + fp.write(self._pack("HHL4s", tag, typ, count, value)) # -- overwrite here for multi-page -- - fp.write(b"\0\0\0\0") # end of directory + fp.write(b"\0\0\0\0") # end of entries # pass 3: write auxiliary data to file - for tag, typ, count, value, data in directory: + for tag, typ, count, value, data in entries: fp.write(data) if len(data) & 1: fp.write(b"\0") return offset +ImageFileDirectory_v2._load_dispatch = _load_dispatch +ImageFileDirectory_v2._write_dispatch = _write_dispatch +for idx, name in TYPES.items(): + name = name.replace(" ", "_") + setattr(ImageFileDirectory_v2, "load_" + name, _load_dispatch[idx][1]) + setattr(ImageFileDirectory_v2, "write_" + name, _write_dispatch[idx]) +del _load_dispatch, _write_dispatch, idx, name + + +# Legacy ImageFileDirectory support. +class ImageFileDirectory_v1(ImageFileDirectory_v2): + """This class represents the **legacy** interface to a TIFF tag directory. + + Exposes a dictionary interface of the tags in the directory:: + + ifd = ImageFileDirectory_v1() + ifd[key] = 'Some Data' + ifd.tagtype[key] = 2 + print ifd[key] + ('Some Data',) + + Also contains a dictionary of tag types as read from the tiff image file, + `~PIL.TiffImagePlugin.ImageFileDirectory_v1.tagtype`. + + Values are returned as a tuple. + + .. deprecated:: 3.0.0 + """ + def __init__(self, *args, **kwargs): + ImageFileDirectory_v2.__init__(self, *args, **kwargs) + self._legacy_api = True + + tags = property(lambda self: self._tags_v1) + tagdata = property(lambda self: self._tagdata) + + @classmethod + def from_v2(cls, original): + """ Returns an + :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v1` + instance with the same data as is contained in the original + :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v2` + instance. + + :returns: :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v1` + + """ + + ifd = cls(prefix=original.prefix) + ifd._tagdata = original._tagdata + ifd.tagtype = original.tagtype + ifd.next = original.next # an indicator for multipage tiffs + return ifd + + def to_v2(self): + """ Returns an + :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v2` + instance with the same data as is contained in the original + :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v1` + instance. + + :returns: :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v2` + + """ + + ifd = ImageFileDirectory_v2(prefix=self.prefix) + ifd._tagdata = dict(self._tagdata) + ifd.tagtype = dict(self.tagtype) + ifd._tags_v2 = dict(self._tags_v2) + return ifd + + def __contains__(self, tag): + return tag in self._tags_v1 or tag in self._tagdata + + def __len__(self): + return len(set(self._tagdata) | set(self._tags_v1)) + + def __iter__(self): + return iter(set(self._tagdata) | set(self._tags_v1)) + + def __setitem__(self, tag, value): + for legacy_api in (False, True): + self._setitem(tag, value, legacy_api) + + def __getitem__(self, tag): + if tag not in self._tags_v1: # unpack on the fly + data = self._tagdata[tag] + typ = self.tagtype[tag] + size, handler = self._load_dispatch[typ] + for legacy in (False, True): + self._setitem(tag, handler(self, data, legacy), legacy) + val = self._tags_v1[tag] + if not isinstance(val, (tuple, bytes)): + val = val, + return val + + +# undone -- switch this pointer when IFD_LEGACY_API == False +ImageFileDirectory = ImageFileDirectory_v1 + ## # Image plugin for TIFF files. @@ -638,20 +899,21 @@ class TiffImageFile(ImageFile.ImageFile): # Header ifh = self.fp.read(8) - if ifh[:4] not in PREFIXES: - raise SyntaxError("not a TIFF file") - # image file directory (tag dictionary) - self.tag = self.ifd = ImageFileDirectory(ifh[:2]) + self.tag_v2 = ImageFileDirectory_v2(ifh) + + # legacy tag/ifd entries will be filled in later + self.tag = self.ifd = None # setup frame pointers - self.__first = self.__next = self.ifd.i32(ifh, 4) + self.__first = self.__next = self.tag_v2.next self.__frame = -1 self.__fp = self.fp self._frame_pos = [] self._n_frames = None + self._is_animated = None - if Image.DEBUG: + if DEBUG: print("*** TiffImageFile._open ***") print("- __first:", self.__first) print("- ifh: ", ifh) @@ -671,6 +933,20 @@ class TiffImageFile(ImageFile.ImageFile): self.seek(current) return self._n_frames + @property + def is_animated(self): + if self._is_animated is None: + 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): "Select a given frame as current image" self._seek(max(frame, 0)) # Questionable backwards compatibility. @@ -685,21 +961,24 @@ class TiffImageFile(ImageFile.ImageFile): while len(self._frame_pos) <= frame: if not self.__next: raise EOFError("no more images in TIFF file") - if Image.DEBUG: - print("Seeking to frame %s, on frame %s, __next %s, location: %s" % + if DEBUG: + print("Seeking to frame %s, on frame %s, " + "__next %s, location: %s" % (frame, self.__frame, self.__next, self.fp.tell())) # reset python3 buffered io handle in case fp # was passed to libtiff, invalidating the buffer self.fp.tell() self.fp.seek(self.__next) self._frame_pos.append(self.__next) - if Image.DEBUG: + if DEBUG: print("Loading tags, location: %s" % self.fp.tell()) - self.tag.load(self.fp) - self.__next = self.tag.next + self.tag_v2.load(self.fp) + self.__next = self.tag_v2.next self.__frame += 1 self.fp.seek(self._frame_pos[frame]) - self.tag.load(self.fp) + self.tag_v2.load(self.fp) + # fill the legacy tag/ifd entries + self.tag = self.ifd = ImageFileDirectory_v1.from_v2(self.tag_v2) self.__frame = frame self._setup() @@ -718,22 +997,28 @@ class TiffImageFile(ImageFile.ImageFile): args = (rawmode, 0, 1) elif compression == "jpeg": args = rawmode, "" - if JPEGTABLES in self.tag: + if JPEGTABLES in self.tag_v2: # Hack to handle abbreviated JPEG headers - self.tile_prefix = self.tag[JPEGTABLES] + # FIXME This will fail with more than one value + self.tile_prefix, = self.tag_v2[JPEGTABLES] elif compression == "packbits": args = rawmode elif compression == "tiff_lzw": args = rawmode - if 317 in self.tag: + if PREDICTOR in self.tag_v2: # Section 14: Differencing Predictor - self.decoderconfig = (self.tag[PREDICTOR][0],) + self.decoderconfig = (self.tag_v2[PREDICTOR],) - if ICCPROFILE in self.tag: - self.info['icc_profile'] = self.tag[ICCPROFILE] + if ICCPROFILE in self.tag_v2: + self.info['icc_profile'] = self.tag_v2[ICCPROFILE] return args + def load(self): + if self.use_load_libtiff: + return self._load_libtiff() + return super(TiffImageFile, self).load() + def _load_libtiff(self): """ Overload method triggered when we detect a compressed tiff Calls out to libtiff """ @@ -753,7 +1038,7 @@ class TiffImageFile(ImageFile.ImageFile): # (self._compression, (extents tuple), # 0, (rawmode, self._compression, fp)) extents = self.tile[0][1] - args = self.tile[0][3] + (self.ifd.offset,) + args = self.tile[0][3] + (self.tag_v2.offset,) decoder = Image._getdecoder(self.mode, 'libtiff', args, self.decoderconfig) try: @@ -771,19 +1056,19 @@ class TiffImageFile(ImageFile.ImageFile): # Rearranging for supporting byteio items, since they have a fileno # that returns an IOError if there's no underlying fp. Easier to # deal with here by reordering. - if Image.DEBUG: + if DEBUG: print("have getvalue. just sending in a string from getvalue") n, err = decoder.decode(self.fp.getvalue()) elif hasattr(self.fp, "fileno"): # we've got a actual file on disk, pass in the fp. - if Image.DEBUG: + if DEBUG: print("have fileno, calling fileno version of the decoder.") self.fp.seek(0) # 4 bytes, otherwise the trace might error out n, err = decoder.decode(b"fpfp") else: # we have something else. - if Image.DEBUG: + if DEBUG: print("don't have fileno or getvalue. just reading") # UNDONE -- so much for that buffer size thing. n, err = decoder.decode(self.fp.read()) @@ -806,22 +1091,20 @@ class TiffImageFile(ImageFile.ImageFile): def _setup(self): "Setup this image object based on current tags" - if 0xBC01 in self.tag: + if 0xBC01 in self.tag_v2: raise IOError("Windows Media Photo files not yet supported") - getscalar = self.tag.getscalar - # extract relevant tags - self._compression = COMPRESSION_INFO[getscalar(COMPRESSION, 1)] - self._planar_configuration = getscalar(PLANAR_CONFIGURATION, 1) + self._compression = COMPRESSION_INFO[self.tag_v2.get(COMPRESSION, 1)] + self._planar_configuration = self.tag_v2.get(PLANAR_CONFIGURATION, 1) # photometric is a required tag, but not everyone is reading # the specification - photo = getscalar(PHOTOMETRIC_INTERPRETATION, 0) + photo = self.tag_v2.get(PHOTOMETRIC_INTERPRETATION, 0) - fillorder = getscalar(FILLORDER, 1) + fillorder = self.tag_v2.get(FILLORDER, 1) - if Image.DEBUG: + if DEBUG: print("*** Summary ***") print("- compression:", self._compression) print("- photometric_interpretation:", photo) @@ -829,47 +1112,48 @@ class TiffImageFile(ImageFile.ImageFile): print("- fill_order:", fillorder) # size - xsize = getscalar(IMAGEWIDTH) - ysize = getscalar(IMAGELENGTH) + xsize = self.tag_v2.get(IMAGEWIDTH) + ysize = self.tag_v2.get(IMAGELENGTH) self.size = xsize, ysize - if Image.DEBUG: + if DEBUG: print("- size:", self.size) - format = getscalar(SAMPLEFORMAT, 1) + format = self.tag_v2.get(SAMPLEFORMAT, (1,)) + if len(format) > 1 and max(format) == min(format) == 1: + # SAMPLEFORMAT is properly per band, so an RGB image will + # be (1,1,1). But, we don't support per band pixel types, + # and anything more than one band is a uint8. So, just + # take the first element. Revisit this if adding support + # for more exotic images. + format = (1,) # mode: check photometric interpretation and bits per pixel key = ( - self.tag.prefix, photo, format, fillorder, - self.tag.get(BITSPERSAMPLE, (1,)), - self.tag.get(EXTRASAMPLES, ()) + self.tag_v2.prefix, photo, format, fillorder, + self.tag_v2.get(BITSPERSAMPLE, (1,)), + self.tag_v2.get(EXTRASAMPLES, ()) ) - if Image.DEBUG: + if DEBUG: print("format key:", key) try: self.mode, rawmode = OPEN_INFO[key] except KeyError: - if Image.DEBUG: + if DEBUG: print("- unsupported format") raise SyntaxError("unknown pixel mode") - if Image.DEBUG: + if DEBUG: print("- raw mode:", rawmode) print("- pil mode:", self.mode) self.info["compression"] = self._compression - xres = getscalar(X_RESOLUTION, (1, 1)) - yres = getscalar(Y_RESOLUTION, (1, 1)) + xres = self.tag_v2.get(X_RESOLUTION, 1) + yres = self.tag_v2.get(Y_RESOLUTION, 1) - if xres and not isinstance(xres, tuple): - xres = (xres, 1.) - if yres and not isinstance(yres, tuple): - yres = (yres, 1.) if xres and yres: - xres = xres[0] / (xres[1] or 1) - yres = yres[0] / (yres[1] or 1) - resunit = getscalar(RESOLUTION_UNIT, 1) + resunit = self.tag_v2.get(RESOLUTION_UNIT, 1) if resunit == 2: # dots per inch self.info["dpi"] = xres, yres elif resunit == 3: # dots per centimeter. convert to dpi @@ -880,10 +1164,11 @@ class TiffImageFile(ImageFile.ImageFile): # build tile descriptors x = y = l = 0 self.tile = [] - if STRIPOFFSETS in self.tag: + self.use_load_libtiff = False + if STRIPOFFSETS in self.tag_v2: # striped image - offsets = self.tag[STRIPOFFSETS] - h = getscalar(ROWSPERSTRIP, ysize) + offsets = self.tag_v2[STRIPOFFSETS] + h = self.tag_v2.get(ROWSPERSTRIP, ysize) w = self.size[0] if READ_LIBTIFF or self._compression in ["tiff_ccitt", "group3", "group4", "tiff_jpeg", @@ -893,7 +1178,7 @@ class TiffImageFile(ImageFile.ImageFile): "tiff_sgilog", "tiff_sgilog24", "tiff_raw_16"]: - # if Image.DEBUG: + # if DEBUG: # print "Activating g4 compression for whole file" # Decoder expects entire file as one tile. @@ -902,10 +1187,9 @@ class TiffImageFile(ImageFile.ImageFile): # function. # # Setup the one tile for the whole image, then - # replace the existing load function with our - # _load_libtiff function. + # use the _load_libtiff function. - self.load = self._load_libtiff + self.use_load_libtiff = True # To be nice on memory footprint, if there's a # file descriptor, use that instead of reading @@ -918,7 +1202,8 @@ class TiffImageFile(ImageFile.ImageFile): # flush the file descriptor, prevents error on pypy 2.4+ # should also eliminate the need for fp.tell for py3 # in _seek - self.fp.flush() + if hasattr(self.fp, "flush"): + self.fp.flush() except IOError: # io.BytesIO have a fileno, but returns an IOError if # it doesn't use a file descriptor. @@ -930,11 +1215,11 @@ class TiffImageFile(ImageFile.ImageFile): # https://github.com/python-pillow/Pillow/issues/279 if fillorder == 2: key = ( - self.tag.prefix, photo, format, 1, - self.tag.get(BITSPERSAMPLE, (1,)), - self.tag.get(EXTRASAMPLES, ()) + self.tag_v2.prefix, photo, format, 1, + self.tag_v2.get(BITSPERSAMPLE, (1,)), + self.tag_v2.get(EXTRASAMPLES, ()) ) - if Image.DEBUG: + if DEBUG: print("format key:", key) # this should always work, since all the # fillorder==2 modes have a corresponding @@ -963,19 +1248,19 @@ class TiffImageFile(ImageFile.ImageFile): (self._compression, (0, min(y, ysize), w, min(y+h, ysize)), offsets[i], a)) - if Image.DEBUG: + if DEBUG: print("tiles: ", self.tile) y = y + h if y >= self.size[1]: x = y = 0 l += 1 a = None - elif TILEOFFSETS in self.tag: + elif TILEOFFSETS in self.tag_v2: # tiled image - w = getscalar(322) - h = getscalar(323) + w = self.tag_v2.get(322) + h = self.tag_v2.get(323) a = None - for o in self.tag[TILEOFFSETS]: + for o in self.tag_v2[TILEOFFSETS]: if not a: a = self._decoder(rawmode, l) # FIXME: this doesn't work if the image size @@ -992,14 +1277,14 @@ class TiffImageFile(ImageFile.ImageFile): l += 1 a = None else: - if Image.DEBUG: + if DEBUG: print("- unsupported data organization") raise SyntaxError("unknown data organization") # fixup palette descriptor if self.mode == "P": - palette = [o8(b // 256) for b in self.tag[COLORMAP]] + palette = [o8(b // 256) for b in self.tag_v2[COLORMAP]] self.palette = ImagePalette.raw("RGB;L", b"".join(palette)) # # -------------------------------------------------------------------- @@ -1034,17 +1319,6 @@ SAVE_INFO = { } -def _cvt_res(value): - # convert value to TIFF rational number -- (numerator, denominator) - if isinstance(value, collections.Sequence): - assert(len(value) % 2 == 0) - return value - if isinstance(value, int): - return (value, 1) - value = float(value) - return (int(value * 65536), 65536) - - def _save(im, fp, filename): try: @@ -1052,31 +1326,26 @@ def _save(im, fp, filename): except KeyError: raise IOError("cannot write mode %s as TIFF" % im.mode) - ifd = ImageFileDirectory(prefix) + ifd = ImageFileDirectory_v2(prefix=prefix) - compression = im.encoderinfo.get('compression', im.info.get('compression', - 'raw')) + compression = im.encoderinfo.get('compression', + im.info.get('compression', 'raw')) libtiff = WRITE_LIBTIFF or compression != 'raw' # required for color libtiff images ifd[PLANAR_CONFIGURATION] = getattr(im, '_planar_configuration', 1) - # -- multi-page -- skip TIFF header on subsequent pages - if not libtiff and fp.tell() == 0: - # tiff header (write via IFD to get everything right) - # PIL always starts the first IFD at offset 8 - fp.write(ifd.prefix + ifd.o16(42) + ifd.o32(8)) - ifd[IMAGEWIDTH] = im.size[0] ifd[IMAGELENGTH] = im.size[1] # write any arbitrary tags passed in as an ImageFileDirectory info = im.encoderinfo.get("tiffinfo", {}) - if Image.DEBUG: - print("Tiffinfo Keys: %s" % info.keys) - keys = list(info.keys()) - for key in keys: + if DEBUG: + print("Tiffinfo Keys: %s" % list(info)) + if isinstance(info, ImageFileDirectory_v1): + info = info.to_v2() + for key in info: ifd[key] = info.get(key) try: ifd.tagtype[key] = info.tagtype[key] @@ -1085,44 +1354,42 @@ def _save(im, fp, filename): # additions written by Greg Couch, gregc@cgl.ucsf.edu # inspired by image-sig posting from Kevin Cazabon, kcazabon@home.com - if hasattr(im, 'tag'): + if hasattr(im, 'tag_v2'): # preserve tags from original TIFF image file for key in (RESOLUTION_UNIT, X_RESOLUTION, Y_RESOLUTION, IPTC_NAA_CHUNK, PHOTOSHOP_CHUNK, XMP): - if key in im.tag: - ifd[key] = im.tag[key] - ifd.tagtype[key] = im.tag.tagtype.get(key, None) + if key in im.tag_v2: + ifd[key] = im.tag_v2[key] + ifd.tagtype[key] = im.tag_v2.tagtype.get(key, None) # preserve ICC profile (should also work when saving other formats # which support profiles as TIFF) -- 2008-06-06 Florian Hoech if "icc_profile" in im.info: ifd[ICCPROFILE] = im.info["icc_profile"] - for key, name, cvt in [ - (IMAGEDESCRIPTION, "description", lambda x: x), - (X_RESOLUTION, "resolution", _cvt_res), - (Y_RESOLUTION, "resolution", _cvt_res), - (X_RESOLUTION, "x_resolution", _cvt_res), - (Y_RESOLUTION, "y_resolution", _cvt_res), - (RESOLUTION_UNIT, "resolution_unit", - lambda x: {"inch": 2, "cm": 3, "centimeter": 3}.get(x, 1)), - (SOFTWARE, "software", lambda x: x), - (DATE_TIME, "date_time", lambda x: x), - (ARTIST, "artist", lambda x: x), - (COPYRIGHT, "copyright", lambda x: x)]: + for key, name in [(IMAGEDESCRIPTION, "description"), + (X_RESOLUTION, "resolution"), + (Y_RESOLUTION, "resolution"), + (X_RESOLUTION, "x_resolution"), + (Y_RESOLUTION, "y_resolution"), + (RESOLUTION_UNIT, "resolution_unit"), + (SOFTWARE, "software"), + (DATE_TIME, "date_time"), + (ARTIST, "artist"), + (COPYRIGHT, "copyright")]: name_with_spaces = name.replace("_", " ") if "_" in name and name_with_spaces in im.encoderinfo: warnings.warn("%r is deprecated; use %r instead" % (name_with_spaces, name), DeprecationWarning) - ifd[key] = cvt(im.encoderinfo[name.replace("_", " ")]) + ifd[key] = im.encoderinfo[name.replace("_", " ")] if name in im.encoderinfo: - ifd[key] = cvt(im.encoderinfo[name]) + ifd[key] = im.encoderinfo[name] dpi = im.encoderinfo.get("dpi") if dpi: ifd[RESOLUTION_UNIT] = 2 - ifd[X_RESOLUTION] = _cvt_res(dpi[0]) - ifd[Y_RESOLUTION] = _cvt_res(dpi[1]) + ifd[X_RESOLUTION] = dpi[0] + ifd[Y_RESOLUTION] = dpi[1] if bits != (1,): ifd[BITSPERSAMPLE] = bits @@ -1148,9 +1415,9 @@ def _save(im, fp, filename): ifd[COMPRESSION] = COMPRESSION_INFO_REV.get(compression, 1) if libtiff: - if Image.DEBUG: + if DEBUG: print("Saving using libtiff encoder") - print(ifd.items()) + print("Items: %s" % sorted(ifd.items())) _fp = 0 if hasattr(fp, "fileno"): try: @@ -1159,55 +1426,38 @@ def _save(im, fp, filename): except io.UnsupportedOperation: pass - # ICC Profile crashes. - blocklist = [STRIPOFFSETS, STRIPBYTECOUNTS, ROWSPERSTRIP, ICCPROFILE] + # STRIPOFFSETS and STRIPBYTECOUNTS are added by the library + # based on the data in the strip. + blocklist = [STRIPOFFSETS, STRIPBYTECOUNTS] atts = {} # bits per sample is a single short in the tiff directory, not a list. atts[BITSPERSAMPLE] = bits[0] # Merge the ones that we have with (optional) more bits from # the original file, e.g x,y resolution so that we can # save(load('')) == original file. - for k, v in itertools.chain(ifd.items(), - getattr(im, 'ifd', {}).items()): - if k not in atts and k not in blocklist: - if type(v[0]) == tuple and len(v) > 1: - # A tuple of more than one rational tuples - # flatten to floats, - # following tiffcp.c->cpTag->TIFF_RATIONAL - atts[k] = [float(elt[0])/float(elt[1]) for elt in v] - continue - if type(v[0]) == tuple and len(v) == 1: - # A tuple of one rational tuples - # flatten to floats, - # following tiffcp.c->cpTag->TIFF_RATIONAL - atts[k] = float(v[0][0])/float(v[0][1]) - continue - if (type(v) == tuple and - (len(v) > 2 or - (len(v) == 2 and v[1] == 0))): - # List of ints? - # Avoid divide by zero in next if-clause - if type(v[0]) in (int, float): - atts[k] = list(v) - continue - if type(v) == tuple and len(v) == 2: - # one rational tuple - # flatten to float, - # following tiffcp.c->cpTag->TIFF_RATIONAL - atts[k] = float(v[0])/float(v[1]) - continue - if type(v) == tuple and len(v) == 1: - v = v[0] - # drop through - if isStringType(v): - atts[k] = bytes(v.encode('ascii', 'replace')) + b"\0" - continue + legacy_ifd = {} + if hasattr(im, 'tag'): + legacy_ifd = im.tag.to_v2() + for tag, value in itertools.chain(ifd.items(), + getattr(im, 'tag_v2', {}).items(), + legacy_ifd.items()): + # Libtiff can only process certain core items without adding + # them to the custom dictionary. It will segfault if it attempts + # to add a custom tag without the dictionary entry + # + # UNDONE -- add code for the custom dictionary + if tag not in TiffTags.LIBTIFF_CORE: + continue + if tag not in atts and tag not in blocklist: + if isinstance(value, unicode if bytes is str else str): + atts[tag] = value.encode('ascii', 'replace') + b"\0" + elif isinstance(value, IFDRational): + atts[tag] = float(value) else: - # int or similar - atts[k] = v + atts[tag] = value - if Image.DEBUG: - print(atts) + if DEBUG: + print("Converted items: %s" % sorted(atts.items())) # libtiff always expects the bytes in native order. # we're storing image byte order. So, if the rawmode @@ -1217,7 +1467,7 @@ def _save(im, fp, filename): rawmode = 'I;16N' a = (rawmode, compression, _fp, filename, atts) - # print (im.mode, compression, a, im.encoderconfig) + # print(im.mode, compression, a, im.encoderconfig) e = Image._getencoder(im.mode, 'libtiff', a, im.encoderconfig) e.setimage(im.im, (0, 0)+im.size) while True: @@ -1246,10 +1496,10 @@ def _save(im, fp, filename): # -------------------------------------------------------------------- # Register -Image.register_open("TIFF", TiffImageFile, _accept) -Image.register_save("TIFF", _save) +Image.register_open(TiffImageFile.format, TiffImageFile, _accept) +Image.register_save(TiffImageFile.format, _save) -Image.register_extension("TIFF", ".tif") -Image.register_extension("TIFF", ".tiff") +Image.register_extension(TiffImageFile.format, ".tif") +Image.register_extension(TiffImageFile.format, ".tiff") -Image.register_mime("TIFF", "image/tiff") +Image.register_mime(TiffImageFile.format, "image/tiff") diff --git a/PIL/TiffTags.py b/PIL/TiffTags.py index d15aa7ebe..bee921dcd 100644 --- a/PIL/TiffTags.py +++ b/PIL/TiffTags.py @@ -17,291 +17,387 @@ # well-known TIFF tags. ## +from collections import namedtuple + + +class TagInfo(namedtuple("_TagInfo", "value name type length enum")): + __slots__ = [] + + def __new__(cls, value=None, name="unknown", type=None, length=0, enum=None): + return super(TagInfo, cls).__new__( + cls, value, name, type, length, enum or {}) + + def cvt_enum(self, value): + return self.enum.get(value, value) + + +def lookup(tag): + """ + :param tag: Integer tag number + :returns: Taginfo namedtuple, From the TAGS_V2 info if possible, + otherwise just populating the value and name from TAGS. + If the tag is not recognized, "unknown" is returned for the name + + """ + + return TAGS_V2.get(tag, TagInfo(tag, TAGS.get(tag, 'unknown'))) + + ## -# Map tag numbers (or tag number, tag value tuples) to tag names. +# Map tag numbers to tag info. +# +# id: (Name, Type, Length, enum_values) +# -TAGS = { +ASCII = 2 +SHORT = 3 +LONG = 4 +RATIONAL = 5 - 254: "NewSubfileType", - 255: "SubfileType", - 256: "ImageWidth", - 257: "ImageLength", - 258: "BitsPerSample", +TAGS_V2 = { - 259: "Compression", - (259, 1): "Uncompressed", - (259, 2): "CCITT 1d", - (259, 3): "Group 3 Fax", - (259, 4): "Group 4 Fax", - (259, 5): "LZW", - (259, 6): "JPEG", - (259, 32773): "PackBits", + 254: ("NewSubfileType", LONG, 1), + 255: ("SubfileType", SHORT, 1), + 256: ("ImageWidth", LONG, 1), + 257: ("ImageLength", LONG, 1), + 258: ("BitsPerSample", SHORT, 0), + 259: ("Compression", SHORT, 1, + {"Uncompressed": 1, "CCITT 1d": 2, "Group 3 Fax": 3, "Group 4 Fax": 4, + "LZW": 5, "JPEG": 6, "PackBits": 32773}), - 262: "PhotometricInterpretation", - (262, 0): "WhiteIsZero", - (262, 1): "BlackIsZero", - (262, 2): "RGB", - (262, 3): "RGB Palette", - (262, 4): "Transparency Mask", - (262, 5): "CMYK", - (262, 6): "YCbCr", - (262, 8): "CieLAB", - (262, 32803): "CFA", # TIFF/EP, Adobe DNG - (262, 32892): "LinearRaw", # Adobe DNG + 262: ("PhotometricInterpretation", SHORT, 1, + {"WhiteIsZero": 0, "BlackIsZero": 1, "RGB": 2, "RBG Palette": 3, + "Transparency Mask": 4, "CMYK": 5, "YCbCr": 6, "CieLAB": 8, + "CFA": 32803, # TIFF/EP, Adobe DNG + "LinearRaw": 32892}), # Adobe DNG + 263: ("Threshholding", SHORT, 1), + 264: ("CellWidth", SHORT, 1), + 265: ("CellLength", SHORT, 1), + 266: ("FillOrder", SHORT, 1), + 269: ("DocumentName", ASCII, 1), - 263: "Thresholding", - 264: "CellWidth", - 265: "CellHeight", - 266: "FillOrder", - 269: "DocumentName", + 270: ("ImageDescription", ASCII, 1), + 271: ("Make", ASCII, 1), + 272: ("Model", ASCII, 1), + 273: ("StripOffsets", LONG, 0), + 274: ("Orientation", SHORT, 1), + 277: ("SamplesPerPixel", SHORT, 1), + 278: ("RowsPerStrip", LONG, 1), + 279: ("StripByteCounts", LONG, 0), - 270: "ImageDescription", - 271: "Make", - 272: "Model", - 273: "StripOffsets", - 274: "Orientation", - 277: "SamplesPerPixel", - 278: "RowsPerStrip", - 279: "StripByteCounts", + 280: ("MinSampleValue", LONG, 0), + 281: ("MaxSampleValue", SHORT, 0), + 282: ("XResolution", RATIONAL, 1), + 283: ("YResolution", RATIONAL, 1), + 284: ("PlanarConfiguration", SHORT, 1, {"Contigous": 1, "Separate": 2}), + 285: ("PageName", ASCII, 1), + 286: ("XPosition", RATIONAL, 1), + 287: ("YPosition", RATIONAL, 1), + 288: ("FreeOffsets", LONG, 1), + 289: ("FreeByteCounts", LONG, 1), - 280: "MinSampleValue", - 281: "MaxSampleValue", - 282: "XResolution", - 283: "YResolution", - 284: "PlanarConfiguration", - (284, 1): "Contigous", - (284, 2): "Separate", + 290: ("GrayResponseUnit", SHORT, 1), + 291: ("GrayResponseCurve", SHORT, 0), + 292: ("T4Options", LONG, 1), + 293: ("T6Options", LONG, 1), + 296: ("ResolutionUnit", SHORT, 1, {"inch": 1, "cm": 2}), + 297: ("PageNumber", SHORT, 2), - 285: "PageName", - 286: "XPosition", - 287: "YPosition", - 288: "FreeOffsets", - 289: "FreeByteCounts", + 301: ("TransferFunction", SHORT, 0), + 305: ("Software", ASCII, 1), + 306: ("DateTime", ASCII, 1), - 290: "GrayResponseUnit", - 291: "GrayResponseCurve", - 292: "T4Options", - 293: "T6Options", - 296: "ResolutionUnit", - 297: "PageNumber", + 315: ("Artist", ASCII, 1), + 316: ("HostComputer", ASCII, 1), + 317: ("Predictor", SHORT, 1), + 318: ("WhitePoint", RATIONAL, 2), + 319: ("PrimaryChromaticities", SHORT, 6), - 301: "TransferFunction", - 305: "Software", - 306: "DateTime", + 320: ("ColorMap", SHORT, 0), + 321: ("HalftoneHints", SHORT, 2), + 322: ("TileWidth", LONG, 1), + 323: ("TileLength", LONG, 1), + 324: ("TileOffsets", LONG, 0), + 325: ("TileByteCounts", LONG, 0), - 315: "Artist", - 316: "HostComputer", - 317: "Predictor", - 318: "WhitePoint", - 319: "PrimaryChromaticies", + 332: ("InkSet", SHORT, 1), + 333: ("InkNames", ASCII, 1), + 334: ("NumberOfInks", SHORT, 1), + 336: ("DotRange", SHORT, 0), + 337: ("TargetPrinter", ASCII, 1), + 338: ("ExtraSamples", SHORT, 0), + 339: ("SampleFormat", SHORT, 0), - 320: "ColorMap", - 321: "HalftoneHints", - 322: "TileWidth", - 323: "TileLength", - 324: "TileOffsets", - 325: "TileByteCounts", - - 332: "InkSet", - 333: "InkNames", - 334: "NumberOfInks", - 336: "DotRange", - 337: "TargetPrinter", - 338: "ExtraSamples", - 339: "SampleFormat", - - 340: "SMinSampleValue", - 341: "SMaxSampleValue", - 342: "TransferRange", - - 347: "JPEGTables", + 340: ("SMinSampleValue", 12, 0), + 341: ("SMaxSampleValue", 12, 0), + 342: ("TransferRange", SHORT, 6), # obsolete JPEG tags - 512: "JPEGProc", - 513: "JPEGInterchangeFormat", - 514: "JPEGInterchangeFormatLength", - 515: "JPEGRestartInterval", - 517: "JPEGLosslessPredictors", - 518: "JPEGPointTransforms", - 519: "JPEGQTables", - 520: "JPEGDCTables", - 521: "JPEGACTables", + 512: ("JPEGProc", SHORT, 1), + 513: ("JPEGInterchangeFormat", LONG, 1), + 514: ("JPEGInterchangeFormatLength", LONG, 1), + 515: ("JPEGRestartInterval", SHORT, 1), + 517: ("JPEGLosslessPredictors", SHORT, 0), + 518: ("JPEGPointTransforms", SHORT, 0), + 519: ("JPEGQTables", LONG, 0), + 520: ("JPEGDCTables", LONG, 0), + 521: ("JPEGACTables", LONG, 0), - 529: "YCbCrCoefficients", - 530: "YCbCrSubSampling", - 531: "YCbCrPositioning", - 532: "ReferenceBlackWhite", + 529: ("YCbCrCoefficients", RATIONAL, 3), + 530: ("YCbCrSubSampling", SHORT, 2), + 531: ("YCbCrPositioning", SHORT, 1), + 532: ("ReferenceBlackWhite", LONG, 0), - # XMP - 700: "XMP", + 33432: ("Copyright", ASCII, 1), - 33432: "Copyright", + # FIXME add more tags here + 34665: ("ExifIFD", SHORT, 1), + 34675: ('ICCProfile', 7, 0), + 34853: ('GPSInfoIFD', 1, 1), - # various extensions (should check specs for "official" names) - 33723: "IptcNaaInfo", - 34377: "PhotoshopInfo", + # MPInfo + 45056: ("MPFVersion", 7, 1), + 45057: ("NumberOfImages", LONG, 1), + 45058: ("MPEntry", 7, 1), + 45059: ("ImageUIDList", 7, 0), + 45060: ("TotalFrames", LONG, 1), + 45313: ("MPIndividualNum", LONG, 1), + 45569: ("PanOrientation", LONG, 1), + 45570: ("PanOverlap_H", RATIONAL, 1), + 45571: ("PanOverlap_V", RATIONAL, 1), + 45572: ("BaseViewpointNum", LONG, 1), + 45573: ("ConvergenceAngle", 10, 1), + 45574: ("BaselineLength", RATIONAL, 1), + 45575: ("VerticalDivergence", 10, 1), + 45576: ("AxisDistance_X", 10, 1), + 45577: ("AxisDistance_Y", 10, 1), + 45578: ("AxisDistance_Z", 10, 1), + 45579: ("YawAngle", 10, 1), + 45580: ("PitchAngle", 10, 1), + 45581: ("RollAngle", 10, 1), - # Exif IFD - 34665: "ExifIFD", - - # ICC Profile - 34675: "ICCProfile", - - # Additional Exif Info - 33434: "ExposureTime", - 33437: "FNumber", - 34850: "ExposureProgram", - 34852: "SpectralSensitivity", - 34853: "GPSInfoIFD", - 34855: "ISOSpeedRatings", - 34856: "OECF", - 34864: "SensitivityType", - 34865: "StandardOutputSensitivity", - 34866: "RecommendedExposureIndex", - 34867: "ISOSpeed", - 34868: "ISOSpeedLatitudeyyy", - 34869: "ISOSpeedLatitudezzz", - 36864: "ExifVersion", - 36867: "DateTimeOriginal", - 36868: "DateTImeDigitized", - 37121: "ComponentsConfiguration", - 37122: "CompressedBitsPerPixel", - 37377: "ShutterSpeedValue", - 37378: "ApertureValue", - 37379: "BrightnessValue", - 37380: "ExposureBiasValue", - 37381: "MaxApertureValue", - 37382: "SubjectDistance", - 37383: "MeteringMode", - 37384: "LightSource", - 37385: "Flash", - 37386: "FocalLength", - 37396: "SubjectArea", - 37500: "MakerNote", - 37510: "UserComment", - 37520: "SubSec", - 37521: "SubSecTimeOriginal", - 37522: "SubsecTimeDigitized", - 40960: "FlashPixVersion", - 40961: "ColorSpace", - 40962: "PixelXDimension", - 40963: "PixelYDimension", - 40964: "RelatedSoundFile", - 40965: "InteroperabilityIFD", - 41483: "FlashEnergy", - 41484: "SpatialFrequencyResponse", - 41486: "FocalPlaneXResolution", - 41487: "FocalPlaneYResolution", - 41488: "FocalPlaneResolutionUnit", - 41492: "SubjectLocation", - 41493: "ExposureIndex", - 41495: "SensingMethod", - 41728: "FileSource", - 41729: "SceneType", - 41730: "CFAPattern", - 41985: "CustomRendered", - 41986: "ExposureMode", - 41987: "WhiteBalance", - 41988: "DigitalZoomRatio", - 41989: "FocalLengthIn35mmFilm", - 41990: "SceneCaptureType", - 41991: "GainControl", - 41992: "Contrast", - 41993: "Saturation", - 41994: "Sharpness", - 41995: "DeviceSettingDescription", - 41996: "SubjectDistanceRange", - 42016: "ImageUniqueID", - 42032: "CameraOwnerName", - 42033: "BodySerialNumber", - 42034: "LensSpecification", - 42035: "LensMake", - 42036: "LensModel", - 42037: "LensSerialNumber", - 42240: "Gamma", - - # MP Info - 45056: "MPFVersion", - 45057: "NumberOfImages", - 45058: "MPEntry", - 45059: "ImageUIDList", - 45060: "TotalFrames", - 45313: "MPIndividualNum", - 45569: "PanOrientation", - 45570: "PanOverlap_H", - 45571: "PanOverlap_V", - 45572: "BaseViewpointNum", - 45573: "ConvergenceAngle", - 45574: "BaselineLength", - 45575: "VerticalDivergence", - 45576: "AxisDistance_X", - 45577: "AxisDistance_Y", - 45578: "AxisDistance_Z", - 45579: "YawAngle", - 45580: "PitchAngle", - 45581: "RollAngle", - - # Adobe DNG - 50706: "DNGVersion", - 50707: "DNGBackwardVersion", - 50708: "UniqueCameraModel", - 50709: "LocalizedCameraModel", - 50710: "CFAPlaneColor", - 50711: "CFALayout", - 50712: "LinearizationTable", - 50713: "BlackLevelRepeatDim", - 50714: "BlackLevel", - 50715: "BlackLevelDeltaH", - 50716: "BlackLevelDeltaV", - 50717: "WhiteLevel", - 50718: "DefaultScale", - 50719: "DefaultCropOrigin", - 50720: "DefaultCropSize", - 50778: "CalibrationIlluminant1", - 50779: "CalibrationIlluminant2", - 50721: "ColorMatrix1", - 50722: "ColorMatrix2", - 50723: "CameraCalibration1", - 50724: "CameraCalibration2", - 50725: "ReductionMatrix1", - 50726: "ReductionMatrix2", - 50727: "AnalogBalance", - 50728: "AsShotNeutral", - 50729: "AsShotWhiteXY", - 50730: "BaselineExposure", - 50731: "BaselineNoise", - 50732: "BaselineSharpness", - 50733: "BayerGreenSplit", - 50734: "LinearResponseLimit", - 50735: "CameraSerialNumber", - 50736: "LensInfo", - 50737: "ChromaBlurRadius", - 50738: "AntiAliasStrength", - 50740: "DNGPrivateData", - 50741: "MakerNoteSafety", - 50780: "BestQualityScale", - - # ImageJ - 50838: "ImageJMetaDataByteCounts", # private tag registered with Adobe - 50839: "ImageJMetaData", # private tag registered with Adobe + 50741: ("MakerNoteSafety", SHORT, 1, {"Unsafe": 0, "Safe": 1}), + 50780: ("BestQualityScale", RATIONAL, 1), + 50838: ("ImageJMetaDataByteCounts", LONG, 1), + 50839: ("ImageJMetaData", 7, 1) } +# Legacy Tags structure +# these tags aren't included above, but were in the previous versions +TAGS = {347: 'JPEGTables', + 700: 'XMP', + + # Additional Exif Info + 33434: 'ExposureTime', + 33437: 'FNumber', + 33723: 'IptcNaaInfo', + 34377: 'PhotoshopInfo', + 34850: 'ExposureProgram', + 34852: 'SpectralSensitivity', + 34855: 'ISOSpeedRatings', + 34856: 'OECF', + 34864: 'SensitivityType', + 34865: 'StandardOutputSensitivity', + 34866: 'RecommendedExposureIndex', + 34867: 'ISOSpeed', + 34868: 'ISOSpeedLatitudeyyy', + 34869: 'ISOSpeedLatitudezzz', + 36864: 'ExifVersion', + 36867: 'DateTimeOriginal', + 36868: 'DateTImeDigitized', + 37121: 'ComponentsConfiguration', + 37122: 'CompressedBitsPerPixel', + 37377: 'ShutterSpeedValue', + 37378: 'ApertureValue', + 37379: 'BrightnessValue', + 37380: 'ExposureBiasValue', + 37381: 'MaxApertureValue', + 37382: 'SubjectDistance', + 37383: 'MeteringMode', + 37384: 'LightSource', + 37385: 'Flash', + 37386: 'FocalLength', + 37396: 'SubjectArea', + 37500: 'MakerNote', + 37510: 'UserComment', + 37520: 'SubSec', + 37521: 'SubSecTimeOriginal', + 37522: 'SubsecTimeDigitized', + 40960: 'FlashPixVersion', + 40961: 'ColorSpace', + 40962: 'PixelXDimension', + 40963: 'PixelYDimension', + 40964: 'RelatedSoundFile', + 40965: 'InteroperabilityIFD', + 41483: 'FlashEnergy', + 41484: 'SpatialFrequencyResponse', + 41486: 'FocalPlaneXResolution', + 41487: 'FocalPlaneYResolution', + 41488: 'FocalPlaneResolutionUnit', + 41492: 'SubjectLocation', + 41493: 'ExposureIndex', + 41495: 'SensingMethod', + 41728: 'FileSource', + 41729: 'SceneType', + 41730: 'CFAPattern', + 41985: 'CustomRendered', + 41986: 'ExposureMode', + 41987: 'WhiteBalance', + 41988: 'DigitalZoomRatio', + 41989: 'FocalLengthIn35mmFilm', + 41990: 'SceneCaptureType', + 41991: 'GainControl', + 41992: 'Contrast', + 41993: 'Saturation', + 41994: 'Sharpness', + 41995: 'DeviceSettingDescription', + 41996: 'SubjectDistanceRange', + 42016: 'ImageUniqueID', + 42032: 'CameraOwnerName', + 42033: 'BodySerialNumber', + 42034: 'LensSpecification', + 42035: 'LensMake', + 42036: 'LensModel', + 42037: 'LensSerialNumber', + 42240: 'Gamma', + + # Adobe DNG + 50706: 'DNGVersion', + 50707: 'DNGBackwardVersion', + 50708: 'UniqueCameraModel', + 50709: 'LocalizedCameraModel', + 50710: 'CFAPlaneColor', + 50711: 'CFALayout', + 50712: 'LinearizationTable', + 50713: 'BlackLevelRepeatDim', + 50714: 'BlackLevel', + 50715: 'BlackLevelDeltaH', + 50716: 'BlackLevelDeltaV', + 50717: 'WhiteLevel', + 50718: 'DefaultScale', + 50719: 'DefaultCropOrigin', + 50720: 'DefaultCropSize', + 50721: 'ColorMatrix1', + 50722: 'ColorMatrix2', + 50723: 'CameraCalibration1', + 50724: 'CameraCalibration2', + 50725: 'ReductionMatrix1', + 50726: 'ReductionMatrix2', + 50727: 'AnalogBalance', + 50728: 'AsShotNeutral', + 50729: 'AsShotWhiteXY', + 50730: 'BaselineExposure', + 50731: 'BaselineNoise', + 50732: 'BaselineSharpness', + 50733: 'BayerGreenSplit', + 50734: 'LinearResponseLimit', + 50735: 'CameraSerialNumber', + 50736: 'LensInfo', + 50737: 'ChromaBlurRadius', + 50738: 'AntiAliasStrength', + 50740: 'DNGPrivateData', + 50778: 'CalibrationIlluminant1', + 50779: 'CalibrationIlluminant2', + } + + +def _populate(): + for k, v in TAGS_V2.items(): + # Populate legacy structure. + TAGS[k] = v[0] + if len(v) == 4: + for sk, sv in v[3].items(): + TAGS[(k, sv)] = sk + + TAGS_V2[k] = TagInfo(k, *v) + +_populate() ## -# Map type numbers to type names. +# Map type numbers to type names -- defined in ImageFileDirectory. -TYPES = { +TYPES = {} - 1: "byte", - 2: "ascii", - 3: "short", - 4: "long", - 5: "rational", - 6: "signed byte", - 7: "undefined", - 8: "signed short", - 9: "signed long", - 10: "signed rational", - 11: "float", - 12: "double", +# was: +# TYPES = { +# 1: "byte", +# 2: "ascii", +# 3: "short", +# 4: "long", +# 5: "rational", +# 6: "signed byte", +# 7: "undefined", +# 8: "signed short", +# 9: "signed long", +# 10: "signed rational", +# 11: "float", +# 12: "double", +# } -} +# +# These tags are handled by default in libtiff, without +# adding to the custom dictionary. From tif_dir.c, searching for +# case TIFFTAG in the _TIFFVSetField function: +# Line: item. +# 148: case TIFFTAG_SUBFILETYPE: +# 151: case TIFFTAG_IMAGEWIDTH: +# 154: case TIFFTAG_IMAGELENGTH: +# 157: case TIFFTAG_BITSPERSAMPLE: +# 181: case TIFFTAG_COMPRESSION: +# 202: case TIFFTAG_PHOTOMETRIC: +# 205: case TIFFTAG_THRESHHOLDING: +# 208: case TIFFTAG_FILLORDER: +# 214: case TIFFTAG_ORIENTATION: +# 221: case TIFFTAG_SAMPLESPERPIXEL: +# 228: case TIFFTAG_ROWSPERSTRIP: +# 238: case TIFFTAG_MINSAMPLEVALUE: +# 241: case TIFFTAG_MAXSAMPLEVALUE: +# 244: case TIFFTAG_SMINSAMPLEVALUE: +# 247: case TIFFTAG_SMAXSAMPLEVALUE: +# 250: case TIFFTAG_XRESOLUTION: +# 256: case TIFFTAG_YRESOLUTION: +# 262: case TIFFTAG_PLANARCONFIG: +# 268: case TIFFTAG_XPOSITION: +# 271: case TIFFTAG_YPOSITION: +# 274: case TIFFTAG_RESOLUTIONUNIT: +# 280: case TIFFTAG_PAGENUMBER: +# 284: case TIFFTAG_HALFTONEHINTS: +# 288: case TIFFTAG_COLORMAP: +# 294: case TIFFTAG_EXTRASAMPLES: +# 298: case TIFFTAG_MATTEING: +# 305: case TIFFTAG_TILEWIDTH: +# 316: case TIFFTAG_TILELENGTH: +# 327: case TIFFTAG_TILEDEPTH: +# 333: case TIFFTAG_DATATYPE: +# 344: case TIFFTAG_SAMPLEFORMAT: +# 361: case TIFFTAG_IMAGEDEPTH: +# 364: case TIFFTAG_SUBIFD: +# 376: case TIFFTAG_YCBCRPOSITIONING: +# 379: case TIFFTAG_YCBCRSUBSAMPLING: +# 383: case TIFFTAG_TRANSFERFUNCTION: +# 389: case TIFFTAG_REFERENCEBLACKWHITE: +# 393: case TIFFTAG_INKNAMES: + +# some of these are not in our TAGS_V2 dict and were included from tiff.h + +LIBTIFF_CORE = set([255, 256, 257, 258, 259, 262, 263, 266, 274, 277, + 278, 280, 281, 340, 341, 282, 283, 284, 286, 287, + 296, 297, 321, 320, 338, 32995, 322, 323, 32998, + 32996, 339, 32997, 330, 531, 530, 301, 532, 333, + # as above + 269 # this has been in our tests forever, and works + ]) + +LIBTIFF_CORE.remove(320) # Array of short, crashes +LIBTIFF_CORE.remove(301) # Array of short, crashes +LIBTIFF_CORE.remove(532) # Array of long, crashes + +LIBTIFF_CORE.remove(255) # We don't have support for subfiletypes +LIBTIFF_CORE.remove(322) # We don't have support for tiled images in libtiff +LIBTIFF_CORE.remove(323) # Tiled images +LIBTIFF_CORE.remove(333) # Ink Names either + +# Note to advanced users: There may be combinations of these +# parameters and values that when added properly, will work and +# produce valid tiff images that may work in your application. +# It is safe to add and remove tags from this set from Pillow's point +# of view so long as you test against libtiff. diff --git a/PIL/WalImageFile.py b/PIL/WalImageFile.py index fc2bb30a7..0cbd1cae6 100644 --- a/PIL/WalImageFile.py +++ b/PIL/WalImageFile.py @@ -1,3 +1,5 @@ +# encoding: utf-8 +# # The Python Imaging Library. # $Id$ # @@ -124,8 +126,3 @@ quake2palette = ( b"\x10\x0f\x0d\x0d\x0b\x3c\x2e\x2a\x36\x27\x20\x30\x21\x18\x29\x1b" b"\x10\x3c\x39\x37\x37\x32\x2f\x31\x2c\x28\x2b\x26\x21\x30\x22\x20" ) - -if __name__ == "__main__": - im = open("../hacks/sample.wal") - print(im.info, im.mode, im.size) - im.save("../out.png") diff --git a/PIL/WebPImagePlugin.py b/PIL/WebPImagePlugin.py index 78a7a5319..6837b53be 100644 --- a/PIL/WebPImagePlugin.py +++ b/PIL/WebPImagePlugin.py @@ -73,8 +73,8 @@ def _save(im, fp, filename): fp.write(data) -Image.register_open("WEBP", WebPImageFile, _accept) -Image.register_save("WEBP", _save) +Image.register_open(WebPImageFile.format, WebPImageFile, _accept) +Image.register_save(WebPImageFile.format, _save) -Image.register_extension("WEBP", ".webp") -Image.register_mime("WEBP", "image/webp") +Image.register_extension(WebPImageFile.format, ".webp") +Image.register_mime(WebPImageFile.format, "image/webp") diff --git a/PIL/WmfImagePlugin.py b/PIL/WmfImagePlugin.py index bdbbc72f0..3163210a8 100644 --- a/PIL/WmfImagePlugin.py +++ b/PIL/WmfImagePlugin.py @@ -15,10 +15,10 @@ # See the README file for information on usage and redistribution. # -__version__ = "0.2" - from PIL import Image, ImageFile, _binary +__version__ = "0.2" + _handler = None if str != bytes: diff --git a/PIL/XVThumbImagePlugin.py b/PIL/XVThumbImagePlugin.py index 5cf1386fd..311e65dc0 100644 --- a/PIL/XVThumbImagePlugin.py +++ b/PIL/XVThumbImagePlugin.py @@ -17,10 +17,10 @@ # FIXME: make save work (this requires quantization support) # -__version__ = "0.1" - from PIL import Image, ImageFile, ImagePalette, _binary +__version__ = "0.1" + o8 = _binary.o8 # standard color palette for thumbnails (RGB332) @@ -72,4 +72,4 @@ class XVThumbImageFile(ImageFile.ImageFile): # -------------------------------------------------------------------- -Image.register_open("XVThumb", XVThumbImageFile) +Image.register_open(XVThumbImageFile.format, XVThumbImageFile) diff --git a/PIL/XbmImagePlugin.py b/PIL/XbmImagePlugin.py index 604ba15a8..bca882866 100644 --- a/PIL/XbmImagePlugin.py +++ b/PIL/XbmImagePlugin.py @@ -19,15 +19,15 @@ # See the README file for information on usage and redistribution. # -__version__ = "0.6" - import re from PIL import Image, ImageFile +__version__ = "0.6" + # XBM header xbm_head = re.compile( - b"\s*#define[ \t]+[^_]*_width[ \t]+(?P[0-9]+)[\r\n]+" - b"#define[ \t]+[^_]*_height[ \t]+(?P[0-9]+)[\r\n]+" + b"\s*#define[ \t]+.*_width[ \t]+(?P[0-9]+)[\r\n]+" + b"#define[ \t]+.*_height[ \t]+(?P[0-9]+)[\r\n]+" b"(?P" b"#define[ \t]+[^_]*_x_hot[ \t]+(?P[0-9]+)[\r\n]+" b"#define[ \t]+[^_]*_y_hot[ \t]+(?P[0-9]+)[\r\n]+" @@ -88,9 +88,9 @@ def _save(im, fp, filename): fp.write(b"};\n") -Image.register_open("XBM", XbmImageFile, _accept) -Image.register_save("XBM", _save) +Image.register_open(XbmImageFile.format, XbmImageFile, _accept) +Image.register_save(XbmImageFile.format, _save) -Image.register_extension("XBM", ".xbm") +Image.register_extension(XbmImageFile.format, ".xbm") -Image.register_mime("XBM", "image/xbm") +Image.register_mime(XbmImageFile.format, "image/xbm") diff --git a/PIL/XpmImagePlugin.py b/PIL/XpmImagePlugin.py index 517580895..556adb8f7 100644 --- a/PIL/XpmImagePlugin.py +++ b/PIL/XpmImagePlugin.py @@ -15,13 +15,12 @@ # -__version__ = "0.2" - - import re from PIL import Image, ImageFile, ImagePalette from PIL._binary import i8, o8 +__version__ = "0.2" + # XPM header xpm_head = re.compile(b"\"([0-9]*) ([0-9]*) ([0-9]*) ([0-9]*)") @@ -124,8 +123,8 @@ class XpmImageFile(ImageFile.ImageFile): # # Registry -Image.register_open("XPM", XpmImageFile, _accept) +Image.register_open(XpmImageFile.format, XpmImageFile, _accept) -Image.register_extension("XPM", ".xpm") +Image.register_extension(XpmImageFile.format, ".xpm") -Image.register_mime("XPM", "image/xpm") +Image.register_mime(XpmImageFile.format, "image/xpm") diff --git a/PIL/__init__.py b/PIL/__init__.py index 6d51a5dcb..68ceab21b 100644 --- a/PIL/__init__.py +++ b/PIL/__init__.py @@ -12,16 +12,18 @@ # ;-) VERSION = '1.1.7' # PIL version -PILLOW_VERSION = '2.9.0.dev0' # Pillow +PILLOW_VERSION = '3.3.0.dev0' # Pillow _plugins = ['BmpImagePlugin', 'BufrStubImagePlugin', 'CurImagePlugin', 'DcxImagePlugin', + 'DdsImagePlugin', 'EpsImagePlugin', 'FitsStubImagePlugin', 'FliImagePlugin', 'FpxImagePlugin', + 'FtexImagePlugin', 'GbrImagePlugin', 'GifImagePlugin', 'GribStubImagePlugin', diff --git a/README.rst b/README.rst index be50c5531..e996ed4f7 100644 --- a/README.rst +++ b/README.rst @@ -4,45 +4,57 @@ Pillow Python Imaging Library (Fork) ----------------------------- -Pillow is the "friendly PIL fork" by `Alex Clark and Contributors `_. PIL is the Python Imaging Library by Fredrik Lundh and Contributors. +Pillow is the friendly PIL fork by `Alex Clark and Contributors `_. PIL is the Python Imaging Library by Fredrik Lundh and Contributors. -.. - image:: https://travis-ci.org/python-pillow/Pillow.svg?branch=master +.. image:: https://zenodo.org/badge/17549/python-pillow/Pillow.svg + :target: https://zenodo.org/badge/latestdoi/17549/python-pillow/Pillow + +.. image:: https://readthedocs.org/projects/pillow/badge/?version=latest + :target: http://pillow.readthedocs.org/?badge=latest + :alt: Documentation Status + +.. image:: https://img.shields.io/travis/python-pillow/Pillow/master.svg?label=Linux%20build :target: https://travis-ci.org/python-pillow/Pillow :alt: Travis CI build status (Linux) -.. - image:: https://pypip.in/v/Pillow/badge.png - :target: https://pypi.python.org/pypi/Pillow/ - :alt: Latest PyPI version +.. image:: https://img.shields.io/travis/python-pillow/pillow-wheels/latest.svg?label=OS%20X%20build + :target: https://travis-ci.org/python-pillow/pillow-wheels + :alt: Travis CI build status (OS X) -.. - image:: https://pypip.in/d/Pillow/badge.png - :target: https://pypi.python.org/pypi/Pillow/ - :alt: Number of PyPI downloads +.. image:: https://img.shields.io/appveyor/ci/python-pillow/Pillow/master.svg?label=Windows%20build + :target: https://ci.appveyor.com/project/python-pillow/Pillow + :alt: AppVeyor CI build status (Windows) -.. image:: https://coveralls.io/repos/python-pillow/Pillow/badge.png?branch=master - :target: https://coveralls.io/r/python-pillow/Pillow?branch=master +.. image:: https://img.shields.io/pypi/v/pillow.svg + :target: https://pypi.python.org/pypi/Pillow/ + :alt: Latest PyPI version + +.. image:: https://img.shields.io/pypi/dm/pillow.svg + :target: https://pypi.python.org/pypi/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 :alt: Code coverage -.. image:: https://landscape.io/github/python-pillow/Pillow/master/landscape.png +.. image:: https://landscape.io/github/python-pillow/Pillow/master/landscape.svg :target: https://landscape.io/github/python-pillow/Pillow/master :alt: Code health + More Information ---------------- -- `Changelog `_ +- `Documentation `_ - - `Pre-fork `_ + - `Installation `_ + - `Handbook `_ - `Contribute `_ - `Issues `_ + - `Pull requests `_ -- `Documentation `_ +- `Changelog `_ - - `About `_ - - `Guides `_ - - `Installation `_ - - `Reference `_ + - `Pre-fork `_ diff --git a/RELEASING.md b/RELEASING.md index d9c4d761e..acd8033b0 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -6,13 +6,13 @@ Released quarterly on the first day of January, April, July, October. * [ ] Open a release ticket e.g. https://github.com/python-pillow/Pillow/issues/1174 * [ ] Develop and prepare release in ``master`` branch. -* [ ] Check [Travis CI](https://travis-ci.org/python-pillow/Pillow) to confirm passing tests in ``master`` branch. -* [ ] In compliance with https://www.python.org/dev/peps/pep-0440/, update version identifier in: +* [ ] Check [Travis CI](https://travis-ci.org/python-pillow/Pillow) and [AppVeyor CI](https://ci.appveyor.com/project/python-pillow/Pillow) to confirm passing tests in ``master`` branch. +* [ ] In compliance with https://www.python.org/dev/peps/pep-0440/, update version identifier in: ``` - PIL/__init__.py setup.py _imaging.c + PIL/__init__.py setup.py _imaging.c appveyor.yml ``` * [ ] Update `CHANGES.rst`. -* [ ] Run pre-release check via `make pre`. +* [ ] Run pre-release check via `make release-test` in a freshly cloned repo. * [ ] Create branch and tag for release e.g.: ``` $ git branch 2.9.x @@ -22,28 +22,32 @@ Released quarterly on the first day of January, April, July, October. ``` * [ ] Create and upload source distributions e.g.: ``` - $ make sdistup + $ make sdist + $ make upload ``` * [ ] Create and upload [binary distributions](#binary-distributions) -* [ ] Manually hide old versions on PyPI as needed, such that only the latest main release is visible when viewing https://pypi.python.org/pypi/Pillow +* [ ] Manually hide old versions on PyPI such that only the latest major release is visible when viewing https://pypi.python.org/pypi/Pillow (https://pypi.python.org/pypi?:action=pkg_edit&name=Pillow) ## Point Release Released as needed for security, installation or critical bug fixes. * [ ] Make necessary changes in ``master`` branch. -* [ ] Update `CHANGES.rst`. +* [ ] Update `CHANGES.rst`. * [ ] Cherry pick individual commits from ``master`` branch to release branch e.g. ``2.9.x``. * [ ] Check [Travis CI](https://travis-ci.org/python-pillow/Pillow) to confirm passing tests in release branch e.g. ``2.9.x``. * [ ] Checkout release branch e.g.: ``` git checkout -t remotes/origin/2.9.x ``` -* [ ] In compliance with https://www.python.org/dev/peps/pep-0440/, update version identifier in: +* [ ] In compliance with https://www.python.org/dev/peps/pep-0440/, update version identifier in: ``` - PIL/__init__.py setup.py _imaging.c + PIL/__init__.py + setup.py + _imaging.c + appveyor.yml ``` -* [ ] Run pre-release check via `make pre`. +* [ ] Run pre-release check via `make release-test`. * [ ] Create tag for release e.g.: ``` $ git tag 2.9.1 @@ -63,7 +67,7 @@ Released as needed privately to individual vendors for critical security-related * [ ] Commit against master, cherry pick to affected release branches. * [ ] Run local test matrix on each release & Python version. * [ ] Privately send to distros. -* [ ] Run pre-release check via `make pre` +* [ ] Run pre-release check via `make release-test` * [ ] Amend any commits with the CVE # * [ ] On release date, tag and push to GitHub. ``` @@ -103,3 +107,7 @@ Released as needed privately to individual vendors for critical security-related ## Publicize Release * [ ] Announce release availability via [Twitter](https://twitter.com/pythonpillow) e.g. https://twitter.com/aclark4life/status/583366798302691328. + +## Documentation + +* [ ] Make sure the default version for Read the Docs is the latest release version, e.g. ``3.1.x`` rather than ``latest``: https://readthedocs.org/projects/pillow/versions/ diff --git a/Scripts/README.rst b/Scripts/README.rst index f86a9a8b6..c8b06d59c 100644 --- a/Scripts/README.rst +++ b/Scripts/README.rst @@ -12,7 +12,7 @@ pildriver.py (by Eric S. Raymond) -------------------------------------------------------------------- A class implementing an image-processing calculator for scripts. -Parses lists of commnds (or, called interactively, command-line +Parses lists of commands (or, called interactively, command-line arguments) into image loads, transformations, and saves. viewer.py diff --git a/Scripts/createfontdatachunk.py b/Scripts/createfontdatachunk.py index b9514eb6e..b9adedc33 100644 --- a/Scripts/createfontdatachunk.py +++ b/Scripts/createfontdatachunk.py @@ -1,3 +1,4 @@ +#!/usr/bin/env python from __future__ import print_function import base64 import os diff --git a/Scripts/gifmaker.py b/Scripts/gifmaker.py index 28c6fb25d..c0679ca79 100644 --- a/Scripts/gifmaker.py +++ b/Scripts/gifmaker.py @@ -14,120 +14,9 @@ # See the README file for information on usage and redistribution. # -# -# For special purposes, you can import this module and call -# the makedelta or compress functions yourself. For example, -# if you have an application that generates a sequence of -# images, you can convert it to a GIF animation using some- -# thing like the following code: -# -# import Image -# import gifmaker -# -# sequence = [] -# -# # generate sequence -# for i in range(100): -# im = -# sequence.append(im) -# -# # write GIF animation -# fp = open("out.gif", "wb") -# gifmaker.makedelta(fp, sequence) -# fp.close() -# -# Alternatively, use an iterator to generate the sequence, and -# write data directly to a socket. Or something... -# - from __future__ import print_function -from PIL import Image, ImageChops - -from PIL.GifImagePlugin import getheader, getdata - -# -------------------------------------------------------------------- -# sequence iterator - - -class image_sequence(object): - def __init__(self, im): - self.im = im - - def __getitem__(self, ix): - try: - if ix: - self.im.seek(ix) - return self.im - except EOFError: - raise IndexError # end of sequence - -# -------------------------------------------------------------------- -# straightforward delta encoding - - -def makedelta(fp, sequence): - """Convert list of image frames to a GIF animation file""" - - frames = 0 - - previous = None - - for im in sequence: - - # To specify duration, add the time in milliseconds to getdata(), - # e.g. getdata(im, duration=1000) - - if not previous: - - # global header - for s in getheader(im)[0] + getdata(im): - fp.write(s) - - else: - - # delta frame - delta = ImageChops.subtract_modulo(im, previous) - - bbox = delta.getbbox() - - if bbox: - - # compress difference - for s in getdata(im.crop(bbox), offset=bbox[:2]): - fp.write(s) - - else: - # FIXME: what should we do in this case? - pass - - previous = im.copy() - - frames += 1 - - fp.write(";") - - return frames - -# -------------------------------------------------------------------- -# main hack - - -def compress(infile, outfile): - - # open input image, and force loading of first frame - im = Image.open(infile) - im.load() - - # open output file - fp = open(outfile, "wb") - - seq = image_sequence(im) - - makedelta(fp, seq) - - fp.close() - +from PIL import Image if __name__ == "__main__": @@ -138,4 +27,5 @@ if __name__ == "__main__": print("Usage: gifmaker infile outfile") sys.exit(1) - compress(sys.argv[1], sys.argv[2]) + im = Image.open(sys.argv[1]) + im.save(sys.argv[2], save_all=True) diff --git a/Scripts/painter.py b/Scripts/painter.py index 234f06171..e5e4927b7 100644 --- a/Scripts/painter.py +++ b/Scripts/painter.py @@ -5,7 +5,7 @@ # # this demo script illustrates pasting into an already displayed # photoimage. note that the current version of Tk updates the whole -# image everytime we paste, so to get decent performance, we split +# image every time we paste, so to get decent performance, we split # the image into a set of tiles. # @@ -68,6 +68,10 @@ class PaintCanvas(Canvas): root = Tk() +if len(sys.argv) != 2: + print("Usage: painter file") + sys.exit(1) + im = Image.open(sys.argv[1]) if im.mode != "RGB": diff --git a/Scripts/pildriver.py b/Scripts/pildriver.py index 32989ccdf..0ede74db5 100644 --- a/Scripts/pildriver.py +++ b/Scripts/pildriver.py @@ -70,7 +70,7 @@ class PILDriver(object): def push(self, item): "Push an argument onto the evaluation stack." - self.stack = [item] + self.stack + self.stack.insert(0, item) def top(self): "Return the top-of-stack element." @@ -90,9 +90,7 @@ class PILDriver(object): Discard the top element on the stack. """ - top = self.stack[0] - self.stack = self.stack[1:] - return top + return self.stack.pop(0) def do_dup(self): """usage: dup @@ -103,7 +101,7 @@ class PILDriver(object): dup = self.stack[0].copy() else: dup = self.stack[0] - self.stack = [dup] + self.stack + self.push(dup) def do_swap(self): """usage: swap @@ -152,7 +150,8 @@ class PILDriver(object): self.push(Image.composite(image1, image2, mask)) def do_merge(self): - """usage: merge [ [ []]] + """usage: merge + [ [ []]] Merge top-of stack images in a way described by the mode. """ @@ -181,7 +180,8 @@ class PILDriver(object): self.dup() def do_crop(self): - """usage: crop + """usage: crop + Crop and push a rectangular region from the current image. """ @@ -243,7 +243,8 @@ class PILDriver(object): self.push(image.offset(xoff, yoff)) def do_paste(self): - """usage: paste + """usage: paste + Paste figure image into ground with upper left at given offsets. """ diff --git a/Scripts/pilfile.py b/Scripts/pilfile.py index b954114ac..dab240e2f 100644 --- a/Scripts/pilfile.py +++ b/Scripts/pilfile.py @@ -21,6 +21,7 @@ from __future__ import print_function import getopt import glob +import logging import sys from PIL import Image @@ -42,6 +43,7 @@ except getopt.error as v: sys.exit(1) verbose = quiet = verify = 0 +logging_level = "WARNING" for o, a in opt: if o == "-f": @@ -58,7 +60,9 @@ for o, a in opt: elif o == "-v": verify = 1 elif o == "-D": - Image.DEBUG += 1 + logging_level = "DEBUG" + +logging.basicConfig(level=logging_level) def globfix(files): diff --git a/Scripts/pilfont.py b/Scripts/pilfont.py index 4425c072c..aa6a34083 100644 --- a/Scripts/pilfont.py +++ b/Scripts/pilfont.py @@ -12,8 +12,6 @@ from __future__ import print_function -VERSION = "0.4" - import glob import sys @@ -21,6 +19,8 @@ import sys from PIL import BdfFontFile from PIL import PcfFontFile +VERSION = "0.4" + if len(sys.argv) <= 1: print("PILFONT", VERSION, "-- PIL font compiler.") print() diff --git a/Scripts/pilprint.py b/Scripts/pilprint.py old mode 100644 new mode 100755 index 2a5e23061..3d8d01751 --- a/Scripts/pilprint.py +++ b/Scripts/pilprint.py @@ -15,6 +15,7 @@ from __future__ import print_function import getopt import os import sys +import subprocess VERSION = "pilprint 0.3/2003-05-05" @@ -32,10 +33,11 @@ def description(filepath, image): return title + format % image.size + image.mode + ")" if len(sys.argv) == 1: - print("PIL Print 0.2a1/96-10-04 -- print image files") + print("PIL Print 0.3/2003-05-05 -- print image files") print("Usage: pilprint files...") print("Options:") print(" -c colour printer (default is monochrome)") + print(" -d debug (show available drivers)") print(" -p print via lpr (default is stdout)") print(" -P same as -p but use given printer") sys.exit(1) @@ -46,8 +48,8 @@ except getopt.error as v: print(v) sys.exit(1) -printer = None # print to stdout -monochrome = 1 # reduce file size for most common case +printerArgs = [] # print to stdout +monochrome = 1 # reduce file size for most common case for o, a in opt: if o == "-d": @@ -60,10 +62,10 @@ for o, a in opt: monochrome = 0 elif o == "-p": # default printer channel - printer = "lpr" + printerArgs = ["lpr"] elif o == "-P": # printer channel - printer = "lpr -P%s" % a + printerArgs = ["lpr", "-P%s" % a] for filepath in argv: try: @@ -76,8 +78,9 @@ for filepath in argv: im.draft("L", im.size) im = im.convert("L") - if printer: - fp = os.popen(printer, "w") + if printerArgs: + p = subprocess.Popen(printerArgs, stdin=subprocess.PIPE) + fp = p.stdin else: fp = sys.stdout @@ -91,6 +94,9 @@ for filepath in argv: ps.image(letter, im) ps.end_document() + if printerArgs: + fp.close() + except: print("cannot print image", end=' ') print("(%s:%s)" % (sys.exc_info()[0], sys.exc_info()[1])) diff --git a/Scripts/player.py b/Scripts/player.py index 43877415a..ac9eb817f 100644 --- a/Scripts/player.py +++ b/Scripts/player.py @@ -15,9 +15,6 @@ from PIL import Image, ImageTk import sys -Image.DEBUG = 0 - - # -------------------------------------------------------------------- # an image animation player diff --git a/Tests/bench_get.py b/Tests/bench_get.py index 9c51c0258..51f3a6aa2 100644 --- a/Tests/bench_get.py +++ b/Tests/bench_get.py @@ -1,9 +1,9 @@ -import sys -sys.path.insert(0, ".") - import helper import timeit +import sys +sys.path.insert(0, ".") + def bench(mode): im = helper.hopper(mode) diff --git a/Tests/check_fli_overflow.py b/Tests/check_fli_overflow.py new file mode 100644 index 000000000..9b370da3c --- /dev/null +++ b/Tests/check_fli_overflow.py @@ -0,0 +1,16 @@ +from helper import unittest, PillowTestCase +from PIL import Image + +TEST_FILE = "Tests/images/fli_overflow.fli" + + +class TestFliOverflow(PillowTestCase): + def test_fli_overflow(self): + + # this should not crash with a malloc error or access violation + im = Image.open(TEST_FILE) + im.load() + + +if __name__ == '__main__': + unittest.main() diff --git a/Tests/check_imaging_leaks.py b/Tests/check_imaging_leaks.py index c6d99b8d1..a31cd2180 100644 --- a/Tests/check_imaging_leaks.py +++ b/Tests/check_imaging_leaks.py @@ -3,7 +3,7 @@ from __future__ import division from helper import unittest, PillowTestCase import sys -from PIL import Image, ImageFilter +from PIL import Image min_iterations = 100 max_iterations = 10000 @@ -31,7 +31,8 @@ class TestImagingLeaks(PillowTestCase): def test_leak_putdata(self): im = Image.new('RGB', (25, 25)) - self._test_leak(min_iterations, max_iterations, im.putdata, im.getdata()) + self._test_leak(min_iterations, max_iterations, + im.putdata, im.getdata()) def test_leak_getlist(self): im = Image.new('P', (25, 25)) diff --git a/Tests/check_jpeg_leaks.py b/Tests/check_jpeg_leaks.py index 4d13978f8..7df2dfcc4 100644 --- a/Tests/check_jpeg_leaks.py +++ b/Tests/check_jpeg_leaks.py @@ -6,7 +6,8 @@ iterations = 5000 """ -When run on a system without the jpeg leak fixes, the valgrind runs look like this. +When run on a system without the jpeg leak fixes, +the valgrind runs look like this. NOSE_PROCESSES=0 NOSE_TIMEOUT=600 valgrind --tool=massif \ python test-installed.py -s -v Tests/check_jpeg_leaks.py @@ -105,7 +106,8 @@ post-patch: test_output = BytesIO() im.save(test_output, "JPEG", qtables=qtables) - """ + def test_exif_leak(self): + """ pre patch: MB @@ -160,8 +162,6 @@ post patch: 0 11.33 """ - - def test_exif_leak(self): im = hopper('RGB') exif = b'12345678'*4096 diff --git a/Tests/check_libtiff_segfault.py b/Tests/check_libtiff_segfault.py new file mode 100644 index 000000000..c2e01dd55 --- /dev/null +++ b/Tests/check_libtiff_segfault.py @@ -0,0 +1,23 @@ +from helper import unittest, PillowTestCase +from PIL import Image + +TEST_FILE = "Tests/images/libtiff_segfault.tif" + + +class TestLibtiffSegfault(PillowTestCase): + def test_segfault(self): + """ This test should not segfault. It will on Pillow <= 3.1.0 and + libtiff >= 4.0.0 + """ + + try: + im = Image.open(TEST_FILE) + im.load() + except IOError: + self.assertTrue(True, "Got expected IOError") + except Exception: + self.fail("Should have returned IOError") + + +if __name__ == '__main__': + unittest.main() diff --git a/Tests/check_png_dos.py b/Tests/check_png_dos.py index 762c9607a..c24eeb359 100644 --- a/Tests/check_png_dos.py +++ b/Tests/check_png_dos.py @@ -42,7 +42,8 @@ class TestPngDos(PillowTestCase): total_len = 0 for txt in im2.text.values(): total_len += len(txt) - self.assertLess(total_len, 64*1024*1024, "Total text chunks greater than 64M") + self.assertLess(total_len, 64*1024*1024, + "Total text chunks greater than 64M") if __name__ == '__main__': unittest.main() diff --git a/Tests/crash_ttf_memory_error.py b/Tests/crash_ttf_memory_error.py deleted file mode 100644 index 8af3dbcec..000000000 --- a/Tests/crash_ttf_memory_error.py +++ /dev/null @@ -1,14 +0,0 @@ -from PIL import Image, ImageFont, ImageDraw - -font = "../pil-archive/memory-error-2.ttf" - -s = "Test Text" -f = ImageFont.truetype(font, 64, index=0, encoding="unicode") -w, h = f.getsize(s) -i = Image.new("RGB", (500, h), "white") -d = ImageDraw.Draw(i) - -# this line causes a MemoryError -d.text((0, 0), s, font=f, fill=0) - -i.show() diff --git a/Tests/helper.py b/Tests/helper.py index 1255b1819..abb2fbd6d 100644 --- a/Tests/helper.py +++ b/Tests/helper.py @@ -43,11 +43,6 @@ class PillowTestCase(unittest.TestCase): else: print("=== orphaned temp file: %s" % path) - def assert_almost_equal(self, a, b, msg=None, eps=1e-6): - self.assertLess( - abs(a-b), eps, - msg or "got %r, expected %r" % (a, b)) - def assert_deep_equal(self, a, b, msg=None): try: self.assertEqual( @@ -234,6 +229,9 @@ def imagemagick_available(): return IMCONVERT and command_succeeds([IMCONVERT, '-version']) +def on_appveyor(): + return 'APPVEYOR' in os.environ + if sys.platform == 'win32': IMCONVERT = os.environ.get('MAGICK_HOME', '') if IMCONVERT: diff --git a/Tests/images/7x13.png b/Tests/images/7x13.png new file mode 100644 index 000000000..d160c8b38 Binary files /dev/null and b/Tests/images/7x13.png differ diff --git a/Tests/images/bad_palette_entry.gpl b/Tests/images/bad_palette_entry.gpl new file mode 100644 index 000000000..162037184 --- /dev/null +++ b/Tests/images/bad_palette_entry.gpl @@ -0,0 +1,12 @@ +GIMP Palette +Name: badpaletteentry +Columns: 4 +# + 0 0 0 Index 3 + 65 38 +103 62 49 Index 6 + 79 73 72 Index 7 +114 101 97 Index 8 +208 127 100 Index 9 +151 144 142 Index 10 +221 207 199 Index 11 diff --git a/Tests/images/bad_palette_file.gpl b/Tests/images/bad_palette_file.gpl new file mode 100644 index 000000000..c366cc8db --- /dev/null +++ b/Tests/images/bad_palette_file.gpl @@ -0,0 +1,12 @@ +GIMP Palette +Name: badpalettefile +Columns: 4 +# + 0 0 0 Index 3 +01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789 +103 62 49 Index 6 + 79 73 72 Index 7 +114 101 97 Index 8 +208 127 100 Index 9 +151 144 142 Index 10 +221 207 199 Index 11 diff --git a/Tests/images/bmp/html/bmpsuite.html b/Tests/images/bmp/html/bmpsuite.html index 6604102bb..b8e327ed9 100644 --- a/Tests/images/bmp/html/bmpsuite.html +++ b/Tests/images/bmp/html/bmpsuite.html @@ -144,7 +144,7 @@ level of support for it is arguably not important.

q/pal8rletrns.bmp 3 -
+
or

or
diff --git a/Tests/images/broken_data_stream.png b/Tests/images/broken_data_stream.png new file mode 100644 index 000000000..ccf310b2f Binary files /dev/null and b/Tests/images/broken_data_stream.png differ diff --git a/Tests/images/compression.tif b/Tests/images/compression.tif new file mode 100755 index 000000000..2488b7841 Binary files /dev/null and b/Tests/images/compression.tif differ diff --git a/Tests/images/copyleft.tiff b/Tests/images/copyleft.tiff new file mode 100644 index 000000000..d8b7f730c Binary files /dev/null and b/Tests/images/copyleft.tiff differ diff --git a/Tests/images/custom_gimp_palette.gpl b/Tests/images/custom_gimp_palette.gpl new file mode 100644 index 000000000..08ea70028 --- /dev/null +++ b/Tests/images/custom_gimp_palette.gpl @@ -0,0 +1,12 @@ +GIMP Palette +Name: custompalette +Columns: 4 +# + 0 0 0 Index 3 + 65 38 30 Index 4 +103 62 49 Index 6 + 79 73 72 Index 7 +114 101 97 Index 8 +208 127 100 Index 9 +151 144 142 Index 10 +221 207 199 Index 11 diff --git a/Tests/images/dxt1-rgb-4bbp-noalpha_MipMaps-1.dds b/Tests/images/dxt1-rgb-4bbp-noalpha_MipMaps-1.dds new file mode 100755 index 000000000..7e12460c4 Binary files /dev/null and b/Tests/images/dxt1-rgb-4bbp-noalpha_MipMaps-1.dds differ diff --git a/Tests/images/dxt1-rgb-4bbp-noalpha_MipMaps-1.png b/Tests/images/dxt1-rgb-4bbp-noalpha_MipMaps-1.png new file mode 100644 index 000000000..a308f0def Binary files /dev/null and b/Tests/images/dxt1-rgb-4bbp-noalpha_MipMaps-1.png differ diff --git a/Tests/images/dxt3-argb-8bbp-explicitalpha_MipMaps-1.dds b/Tests/images/dxt3-argb-8bbp-explicitalpha_MipMaps-1.dds new file mode 100755 index 000000000..8ec3484fa Binary files /dev/null and b/Tests/images/dxt3-argb-8bbp-explicitalpha_MipMaps-1.dds differ diff --git a/Tests/images/dxt5-argb-8bbp-interpolatedalpha_MipMaps-1.dds b/Tests/images/dxt5-argb-8bbp-interpolatedalpha_MipMaps-1.dds new file mode 100755 index 000000000..b2e56d606 Binary files /dev/null and b/Tests/images/dxt5-argb-8bbp-interpolatedalpha_MipMaps-1.dds differ diff --git a/Tests/images/dxt5-argb-8bbp-interpolatedalpha_MipMaps-1.png b/Tests/images/dxt5-argb-8bbp-interpolatedalpha_MipMaps-1.png new file mode 100644 index 000000000..f1e3520a8 Binary files /dev/null and b/Tests/images/dxt5-argb-8bbp-interpolatedalpha_MipMaps-1.png differ diff --git a/Tests/images/exif_gps.jpg b/Tests/images/exif_gps.jpg new file mode 100644 index 000000000..2b9ae9056 Binary files /dev/null and b/Tests/images/exif_gps.jpg differ diff --git a/Tests/images/exif_gps_typeerror.jpg b/Tests/images/exif_gps_typeerror.jpg new file mode 100644 index 000000000..0b1f06043 Binary files /dev/null and b/Tests/images/exif_gps_typeerror.jpg differ diff --git a/Tests/images/exif_typeerror.jpg b/Tests/images/exif_typeerror.jpg new file mode 100644 index 000000000..550859a0a Binary files /dev/null and b/Tests/images/exif_typeerror.jpg differ diff --git a/Tests/images/fli_overflow.fli b/Tests/images/fli_overflow.fli new file mode 100644 index 000000000..bea12160e Binary files /dev/null and b/Tests/images/fli_overflow.fli differ diff --git a/Tests/images/ftex_dxt1.ftc b/Tests/images/ftex_dxt1.ftc new file mode 100644 index 000000000..145130c8e Binary files /dev/null and b/Tests/images/ftex_dxt1.ftc differ diff --git a/Tests/images/ftex_dxt1.png b/Tests/images/ftex_dxt1.png new file mode 100644 index 000000000..a5521dc98 Binary files /dev/null and b/Tests/images/ftex_dxt1.png differ diff --git a/Tests/images/ftex_uncompressed.ftu b/Tests/images/ftex_uncompressed.ftu new file mode 100644 index 000000000..bf314f75a Binary files /dev/null and b/Tests/images/ftex_uncompressed.ftu differ diff --git a/Tests/images/ftex_uncompressed.png b/Tests/images/ftex_uncompressed.png new file mode 100644 index 000000000..a5521dc98 Binary files /dev/null and b/Tests/images/ftex_uncompressed.png differ diff --git a/Tests/images/gbr.gbr b/Tests/images/gbr.gbr new file mode 100644 index 000000000..054b82df5 Binary files /dev/null and b/Tests/images/gbr.gbr differ diff --git a/Tests/images/gbr.png b/Tests/images/gbr.png new file mode 100644 index 000000000..d27301df2 Binary files /dev/null and b/Tests/images/gbr.png differ diff --git a/Tests/images/hopper.bmp b/Tests/images/hopper.bmp new file mode 100644 index 000000000..785700d22 Binary files /dev/null and b/Tests/images/hopper.bmp differ diff --git a/Tests/images/hopper.iccprofile.tif b/Tests/images/hopper.iccprofile.tif new file mode 100644 index 000000000..f4cfeab3e Binary files /dev/null and b/Tests/images/hopper.iccprofile.tif differ diff --git a/Tests/images/hopper.iccprofile_binary.tif b/Tests/images/hopper.iccprofile_binary.tif new file mode 100644 index 000000000..c10e4f836 Binary files /dev/null and b/Tests/images/hopper.iccprofile_binary.tif differ diff --git a/Tests/images/hopper.pcd b/Tests/images/hopper.pcd new file mode 100644 index 000000000..8ffaf717e Binary files /dev/null and b/Tests/images/hopper.pcd differ diff --git a/Tests/images/hopper.xbm b/Tests/images/hopper.xbm new file mode 100644 index 000000000..2106fbfa2 --- /dev/null +++ b/Tests/images/hopper.xbm @@ -0,0 +1,174 @@ +#define hopper_width 128 +#define hopper_height 128 +static char hopper_bits[] = { + 0xFF, 0xFF, 0xFF, 0x1F, 0xF0, 0x3D, 0x80, 0x96, 0xDA, 0xB6, 0xD6, 0x2A, + 0xA9, 0x6D, 0x25, 0x29, 0xFF, 0xFF, 0xFF, 0xBF, 0xFE, 0x7D, 0xC0, 0xFB, + 0x69, 0x69, 0xA9, 0xD5, 0x96, 0x96, 0x5E, 0x9A, 0xFF, 0xFF, 0xFF, 0x9F, + 0xFE, 0xFD, 0x80, 0xFF, 0x97, 0xB6, 0x5A, 0x6A, 0x5D, 0x6D, 0x5A, 0x62, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0xFD, 0xC1, 0xCF, 0x5F, 0x89, 0xA5, 0xD5, + 0xAA, 0x92, 0xA5, 0x59, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0xFD, 0xF8, 0x00, + 0xB8, 0x59, 0xAA, 0x56, 0xDA, 0x5A, 0x56, 0x49, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFE, 0xFC, 0xF9, 0x00, 0xF0, 0xA6, 0x55, 0xA9, 0x65, 0xA5, 0xA9, 0xA2, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0xFD, 0x71, 0x00, 0x60, 0x70, 0xAA, 0x54, + 0xAA, 0x5A, 0x54, 0x69, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0xFC, 0x38, 0x00, + 0xE0, 0x80, 0x55, 0xAB, 0x55, 0x75, 0xAB, 0x56, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFE, 0x1D, 0x70, 0x00, 0xF0, 0x00, 0xAE, 0xAA, 0xAA, 0xA6, 0x95, 0x55, + 0xFB, 0xFF, 0xFF, 0xFF, 0xFE, 0x1D, 0xF0, 0x00, 0xF0, 0x00, 0x50, 0x55, + 0x55, 0x59, 0x6A, 0xAA, 0xF3, 0xFF, 0xFF, 0xFF, 0xFE, 0x04, 0xF0, 0x00, + 0xF0, 0x00, 0xA4, 0xAA, 0xA9, 0xA5, 0x95, 0x55, 0x21, 0xFF, 0xFF, 0xFF, + 0xFE, 0x01, 0xF8, 0x00, 0xF0, 0x00, 0x50, 0x55, 0x56, 0x5A, 0x6A, 0xAA, + 0x83, 0x9F, 0xFF, 0xFF, 0xFE, 0x00, 0xFC, 0x00, 0xF0, 0x01, 0x50, 0xD5, + 0x95, 0x95, 0xA5, 0x5A, 0x83, 0x9F, 0xFF, 0xFF, 0xFE, 0x81, 0xFF, 0x00, + 0xE0, 0x0B, 0xA0, 0xAA, 0x7A, 0x6A, 0x5A, 0x65, 0x80, 0x1F, 0xF0, 0xFF, + 0xFE, 0xF0, 0xFF, 0x00, 0xF0, 0x3F, 0x50, 0x55, 0xA5, 0xB5, 0xA5, 0xAA, + 0x00, 0x1F, 0xF8, 0xFD, 0xFE, 0xF0, 0xF7, 0x00, 0xF0, 0x6D, 0xA0, 0xAA, + 0x5A, 0x6A, 0x5A, 0x55, 0x83, 0x0F, 0xF8, 0xC9, 0xFE, 0x79, 0x00, 0xF8, + 0x02, 0xC0, 0x81, 0x55, 0xAD, 0xD5, 0xA5, 0xA6, 0xE3, 0x03, 0xFC, 0xC0, + 0xFE, 0xFF, 0x04, 0xF0, 0x00, 0x80, 0x63, 0xAA, 0x52, 0x2A, 0x5A, 0x58, + 0xE7, 0x03, 0xFC, 0xE1, 0xFE, 0xFF, 0x2F, 0xFC, 0x03, 0x0C, 0xFE, 0x55, + 0xA9, 0x55, 0x66, 0x15, 0xF3, 0x0F, 0x7C, 0xF0, 0xFE, 0xFF, 0xFF, 0xFD, + 0xEF, 0x38, 0xFF, 0xAA, 0x56, 0xAA, 0x99, 0xAA, 0xF7, 0x1F, 0x78, 0xF0, + 0xFC, 0xFF, 0xFF, 0xFF, 0xFF, 0xF1, 0xFF, 0x55, 0xAA, 0x5B, 0x66, 0xA4, + 0xFF, 0x9F, 0xFD, 0xF0, 0xFE, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xAB, + 0x65, 0xA4, 0x99, 0x1B, 0xFF, 0xDF, 0xFF, 0xE0, 0xFE, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0x69, 0x51, 0x5A, 0x24, 0x62, 0xFF, 0xDF, 0xFF, 0xEC, + 0xFE, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x95, 0xAA, 0x24, 0x59, 0x55, + 0xFF, 0xFF, 0xFF, 0xFC, 0xFC, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x25, + 0x49, 0xAD, 0x55, 0x2A, 0xFF, 0xFF, 0xFF, 0xFC, 0xFD, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0x9B, 0xA5, 0x52, 0x2A, 0x94, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFD, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x67, 0x64, 0xC1, 0x92, 0x6D, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x95, + 0x49, 0x2C, 0x55, 0x92, 0xFF, 0xFF, 0xFF, 0xFF, 0xFC, 0xFF, 0xFF, 0xFF, + 0x7F, 0xFF, 0xFF, 0x15, 0xB4, 0x62, 0x44, 0x49, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFD, 0xFF, 0x51, 0x30, 0x03, 0xE0, 0xFF, 0x53, 0x03, 0x89, 0x96, 0x92, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFD, 0x7F, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xCD, + 0x68, 0x19, 0x68, 0x99, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0x1F, 0x00, 0x00, + 0x00, 0x00, 0xFF, 0x13, 0x4A, 0x64, 0x23, 0x49, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0x0F, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x47, 0x8A, 0xA4, 0xD8, 0x30, + 0xFE, 0xFF, 0xFF, 0xFF, 0xFC, 0x07, 0x00, 0x00, 0x00, 0x00, 0xF8, 0x39, + 0x29, 0x49, 0x06, 0x86, 0xCC, 0xFF, 0xFF, 0xFF, 0xFC, 0x03, 0x00, 0x00, + 0x80, 0x00, 0xF2, 0x81, 0x50, 0x31, 0xA2, 0x8C, 0xE4, 0xFF, 0xFF, 0xFF, + 0xFF, 0x0A, 0x08, 0x00, 0x00, 0x00, 0xF0, 0xA7, 0x46, 0x8C, 0x28, 0x31, + 0xC0, 0xFC, 0xFF, 0xFF, 0xFC, 0x03, 0x10, 0x00, 0xE0, 0x01, 0xE0, 0x19, + 0x41, 0x61, 0x66, 0x05, 0xE0, 0xB8, 0xFF, 0xFF, 0x7E, 0x0C, 0x44, 0x00, + 0x40, 0xBF, 0xEF, 0x41, 0x5C, 0x04, 0x00, 0xA8, 0xF0, 0xC1, 0xFF, 0xFF, + 0xFE, 0x21, 0x9E, 0x00, 0xE0, 0xFF, 0xED, 0xA5, 0x12, 0x92, 0xDB, 0xA2, + 0xE0, 0xC0, 0xFF, 0xFF, 0x7D, 0xFA, 0xFF, 0x00, 0x70, 0x00, 0xFF, 0x42, + 0x84, 0x0C, 0x00, 0x08, 0xC0, 0xC0, 0xFF, 0xFC, 0x7D, 0xFF, 0x90, 0x07, + 0xC0, 0x1F, 0xDB, 0x0A, 0x24, 0xC8, 0x68, 0x09, 0xCC, 0xC0, 0x7F, 0xFE, + 0x1E, 0x7F, 0x3E, 0x00, 0xD4, 0xFF, 0x3E, 0x60, 0x81, 0x21, 0x02, 0xA0, + 0xFC, 0xE0, 0x7F, 0xFF, 0xCE, 0x9F, 0xFF, 0x05, 0xFA, 0xFF, 0xBF, 0x04, + 0x24, 0x0A, 0x61, 0x1B, 0xFC, 0xC0, 0xBF, 0xFF, 0xAC, 0x7D, 0xFF, 0x03, + 0x40, 0x1F, 0x80, 0x91, 0xC2, 0xA0, 0x05, 0x80, 0xFF, 0x8D, 0x7F, 0xFF, + 0xEF, 0xF5, 0xDF, 0x00, 0x00, 0x94, 0x42, 0x81, 0x01, 0x10, 0x92, 0x85, + 0xFF, 0xFD, 0xFF, 0xFE, 0x2C, 0x62, 0x03, 0x00, 0x20, 0x00, 0x10, 0x2B, + 0x24, 0x82, 0x00, 0x20, 0xFF, 0xFD, 0xFF, 0xFF, 0xBD, 0x55, 0x01, 0x00, + 0x40, 0x00, 0x08, 0x19, 0x81, 0x58, 0x54, 0x14, 0xFF, 0xFF, 0xFF, 0xFF, + 0x3C, 0x82, 0x05, 0x08, 0x00, 0x24, 0x20, 0x81, 0x24, 0x02, 0x02, 0x82, + 0xFF, 0xFF, 0xFF, 0xFF, 0x3D, 0x82, 0x01, 0x00, 0x00, 0x00, 0x69, 0x22, + 0x90, 0xA0, 0x49, 0x06, 0xFF, 0xFF, 0xFF, 0xFF, 0x2D, 0x26, 0x00, 0x00, + 0x00, 0x00, 0x20, 0x20, 0x0A, 0x02, 0x20, 0xA0, 0xFF, 0xFF, 0xFF, 0xFF, + 0x04, 0x81, 0x01, 0x00, 0x10, 0x00, 0xA0, 0x04, 0x02, 0x0A, 0x09, 0x00, + 0xFF, 0xFF, 0xFF, 0xFF, 0x81, 0x19, 0x02, 0x02, 0x00, 0x00, 0x02, 0x42, + 0x50, 0xA0, 0x20, 0x95, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x60, 0x00, 0x01, + 0x80, 0x02, 0x42, 0x52, 0x40, 0x20, 0xA0, 0x20, 0xFF, 0xFF, 0xFF, 0x7F, + 0x00, 0x00, 0x8C, 0x01, 0x40, 0x00, 0x00, 0x00, 0x06, 0x06, 0x09, 0x05, + 0xFF, 0xFF, 0xFF, 0x3F, 0x00, 0x50, 0x00, 0x00, 0x40, 0x00, 0x02, 0x18, + 0xA0, 0x90, 0x84, 0x64, 0xFF, 0xFF, 0xFF, 0x3F, 0x00, 0x10, 0x81, 0x07, + 0xD0, 0x00, 0x48, 0x82, 0x02, 0x02, 0x20, 0x00, 0xFF, 0xFF, 0xFF, 0x0F, + 0x00, 0x04, 0xC0, 0x7E, 0x9F, 0x01, 0x10, 0x04, 0x00, 0x00, 0x24, 0x25, + 0xFF, 0xFF, 0xFF, 0x0F, 0x00, 0x60, 0x48, 0xC0, 0xC1, 0x13, 0x00, 0x41, + 0x9A, 0x99, 0x81, 0x00, 0xFF, 0xFF, 0xFF, 0x03, 0x40, 0x40, 0x60, 0x00, + 0x84, 0x0B, 0x00, 0x50, 0x02, 0x40, 0x24, 0x90, 0xFF, 0xFF, 0xFF, 0x01, + 0x00, 0x00, 0x32, 0x00, 0x00, 0x57, 0x00, 0x04, 0x40, 0x02, 0x01, 0x05, + 0xF8, 0xFF, 0xFF, 0x01, 0x40, 0x00, 0x38, 0x00, 0x00, 0x0E, 0x10, 0x05, + 0x99, 0x0A, 0xA4, 0x0A, 0xFC, 0xFF, 0x7F, 0x00, 0x80, 0x30, 0x10, 0x00, + 0x00, 0x84, 0x04, 0x60, 0x00, 0x50, 0x00, 0x60, 0xFC, 0xFF, 0xFF, 0x00, + 0x00, 0x3A, 0x08, 0x00, 0x00, 0x04, 0x8E, 0x06, 0x00, 0x41, 0x56, 0x00, + 0xF8, 0xFE, 0xFF, 0x01, 0x80, 0x79, 0xA0, 0x7F, 0xFF, 0x01, 0x06, 0x20, + 0x51, 0x08, 0x00, 0x2A, 0xF8, 0xFF, 0xFF, 0x00, 0xE0, 0x3F, 0xE0, 0xAF, + 0x00, 0x00, 0x06, 0x11, 0x91, 0x52, 0x02, 0x92, 0xF8, 0xFF, 0xFF, 0x00, + 0x00, 0x7F, 0x00, 0x00, 0x80, 0x02, 0x02, 0x08, 0x00, 0x00, 0x50, 0x00, + 0x7E, 0xFF, 0xFF, 0x01, 0x40, 0x7F, 0x00, 0x00, 0x10, 0x80, 0x86, 0x00, + 0x08, 0x09, 0x06, 0x08, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFC, 0x20, 0x51, + 0x0D, 0x20, 0x23, 0x94, 0x91, 0x50, 0x20, 0x92, 0xFF, 0xFE, 0xFF, 0x03, + 0x20, 0x70, 0x05, 0x88, 0x01, 0x80, 0x21, 0x06, 0x84, 0x44, 0x80, 0x25, + 0xFF, 0xFF, 0xFF, 0x03, 0x40, 0xEA, 0x90, 0x00, 0x20, 0xA4, 0x05, 0x00, + 0x00, 0x10, 0x26, 0x00, 0xFF, 0xFF, 0xFF, 0x07, 0x40, 0xD8, 0x08, 0x00, + 0x00, 0xA0, 0x81, 0x92, 0x01, 0x01, 0x84, 0x40, 0xFF, 0xFF, 0xFF, 0x0F, + 0x20, 0xC3, 0x42, 0x00, 0x00, 0x0A, 0x24, 0x00, 0x44, 0x48, 0x20, 0x25, + 0xFF, 0xFF, 0xFF, 0x0F, 0x20, 0xD5, 0x05, 0x02, 0x00, 0x46, 0x12, 0x40, + 0x02, 0x22, 0x44, 0x00, 0xFF, 0xFF, 0xFF, 0x1F, 0x90, 0x50, 0x90, 0x00, + 0x00, 0x9A, 0x40, 0x02, 0x40, 0x00, 0x10, 0x64, 0xFF, 0xFF, 0xFF, 0x1F, + 0x98, 0x62, 0x7C, 0x10, 0x11, 0xDF, 0x07, 0x14, 0x28, 0x08, 0x01, 0x05, + 0xFF, 0xFF, 0xFF, 0x1F, 0x20, 0x74, 0xF8, 0xE5, 0xEA, 0x87, 0x23, 0x01, + 0x81, 0x11, 0x48, 0x20, 0xFF, 0xFF, 0xFF, 0x1F, 0x50, 0x38, 0xF2, 0xFF, + 0xFF, 0xC7, 0x8F, 0x80, 0x00, 0x84, 0x40, 0x04, 0xFF, 0xFF, 0xFF, 0xBF, + 0x68, 0x3D, 0xE9, 0xFF, 0x7F, 0xC1, 0x1F, 0x28, 0x14, 0x10, 0x06, 0x50, + 0xFF, 0xFF, 0xFF, 0x3F, 0x90, 0x3F, 0x81, 0xFE, 0x5F, 0x89, 0x3F, 0x82, + 0x00, 0x02, 0xA2, 0x01, 0xFF, 0xFF, 0xFF, 0xBF, 0xB0, 0x3F, 0xB1, 0xFF, + 0xBF, 0xC1, 0xFF, 0x00, 0x82, 0x80, 0x00, 0x24, 0xFF, 0xFB, 0xFF, 0x7F, + 0xF8, 0x3F, 0xA9, 0xFC, 0x5F, 0x81, 0xFF, 0x0F, 0x08, 0x00, 0x04, 0x48, + 0xFF, 0xF0, 0xFF, 0xBF, 0xFC, 0x3F, 0x41, 0xEB, 0xE7, 0xC0, 0xFF, 0x1F, + 0x40, 0x24, 0x40, 0x01, 0xFF, 0xE0, 0xFF, 0xFF, 0xFF, 0x3F, 0xB3, 0x7D, + 0xAD, 0x80, 0xFF, 0xFF, 0x01, 0x00, 0xA0, 0x90, 0x7F, 0xC0, 0xFF, 0xFF, + 0xFF, 0x3F, 0x43, 0xFA, 0x27, 0xC0, 0xFF, 0xFF, 0x93, 0x51, 0x04, 0x04, + 0x7F, 0x80, 0xFF, 0xFF, 0xFF, 0x3F, 0x60, 0x6D, 0x56, 0xC0, 0xFF, 0xFF, + 0x1F, 0x20, 0x0A, 0x40, 0x7F, 0x80, 0xFF, 0xFF, 0xFF, 0x3F, 0x84, 0x6A, + 0x6D, 0xC0, 0xFF, 0xFF, 0x3F, 0x00, 0x00, 0x02, 0x7F, 0x00, 0xFF, 0xFF, + 0xFF, 0x3F, 0x2C, 0xBA, 0x22, 0xC0, 0xFF, 0xFF, 0xFF, 0x87, 0x04, 0x98, + 0x7F, 0x80, 0xFF, 0xFF, 0xFF, 0x3F, 0x48, 0x66, 0xD5, 0xC0, 0xFF, 0xFF, + 0xFF, 0x3F, 0x90, 0x02, 0x7F, 0xC0, 0xFF, 0xFF, 0xFF, 0x7F, 0x80, 0xA6, + 0x63, 0xE0, 0xFF, 0xFF, 0xFF, 0xFF, 0x08, 0x24, 0x7F, 0xF8, 0xFF, 0xFF, + 0xFF, 0x7F, 0x90, 0x78, 0x3A, 0xE0, 0xFF, 0xFF, 0xFF, 0xFF, 0x27, 0x00, + 0x7F, 0xFC, 0xFF, 0xFF, 0xFF, 0x7F, 0x00, 0xF9, 0x06, 0xF0, 0xFF, 0xFF, + 0xFF, 0xFF, 0x8F, 0x61, 0x7F, 0xFC, 0xFF, 0xFF, 0xFF, 0x7F, 0x00, 0xA4, + 0x0B, 0xF0, 0xFF, 0xFF, 0xFF, 0xFF, 0x3F, 0x04, 0x7F, 0xFE, 0xFF, 0xFF, + 0xFF, 0xFF, 0x00, 0xD0, 0x01, 0xF0, 0xFF, 0xFF, 0xFF, 0xFF, 0x7F, 0x08, + 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x01, 0x00, 0xF8, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0x61, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x88, + 0x01, 0xF8, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0xFE, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0x00, 0x00, 0x00, 0xF8, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x48, + 0xFC, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x01, 0x00, 0x07, 0xFC, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0x01, 0xFC, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x01, 0xD0, + 0x0F, 0xFE, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x69, 0xF8, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0x03, 0xFF, 0x0F, 0xFC, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x05, + 0xF0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x03, 0xFF, 0x3F, 0xFE, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0x21, 0xF0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x83, 0xFF, + 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x03, 0xF0, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xC3, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x6B, + 0xF0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF7, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0x23, 0xE0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x07, 0xE0, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xEF, 0xFB, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x27, + 0xF0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x0F, 0xF0, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0x0F, 0xE0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x07, + 0xF0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x0F, 0xF0, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0x03, 0xF0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x8F, + 0xE0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x03, 0xF0, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0x1F, 0xF0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x01, + 0xF0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x8F, 0xF0, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0x00, 0xF8, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x2F, + 0xF0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0xF8, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0x1F, 0xF0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x3F, 0x00, + 0xF8, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x1F, 0xF0, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0x3F, 0x00, 0xFC, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x9F, + 0xF8, 0xFF, 0x07, 0xFF, 0xFF, 0xFF, 0x7F, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0x1F, 0xF0, 0xFF, 0x03, 0x00, 0xF1, 0xFF, 0x7F, 0x80, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x1F, 0xF0, 0xFF, 0x03, 0x00, + 0xF0, 0xFF, 0xFF, 0x00, 0xFF, 0xFF, 0x7F, 0x6F, 0xFD, 0xFF, 0xFF, 0x1F, + 0xF8, 0xFF, 0x03, 0x00, 0xE0, 0xFF, 0xFF, 0x80, 0xFF, 0xFF, 0x7F, 0x7F, + 0xFD, 0xFF, 0xFF, 0x7F, 0xF0, 0xFF, 0x03, 0x00, 0xC0, 0xFF, 0xFF, 0x90, + 0xFF, 0xFF, 0x5F, 0xA2, 0xF4, 0xFF, 0xFF, 0x3F, 0xF8, 0xFF, 0x03, 0x00, + 0x80, 0xFF, 0xFF, 0x80, 0xFF, 0xFF, 0x0F, 0xAA, 0x42, 0xF1, 0xFF, 0x3F, + 0xF8, 0xFF, 0xAF, 0x00, 0x80, 0xFF, 0xFF, 0xD9, 0xFF, 0xFF, 0x0F, 0x02, + 0x64, 0xF1, 0xFF, 0x3F, 0xF8, 0xFF, 0xFF, 0x3F, 0xC2, 0xFF, 0xFF, 0xDB, + 0xFF, 0xFF, 0xCB, 0xD1, 0xDA, 0xF7, 0xFF, 0x7F, 0xF8, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x71, 0x44, 0xFC, 0xF6, 0xFF, 0x3F, + 0xFC, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xB0, 0xC0, + 0xA8, 0xFF, 0xFF, 0x3F, 0xFC, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xBF, 0xFC, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x3F, + 0xFC, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0x7F, 0xFC, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x7F, }; diff --git a/Tests/images/hopper_underscore.xbm b/Tests/images/hopper_underscore.xbm new file mode 100644 index 000000000..50d81e526 --- /dev/null +++ b/Tests/images/hopper_underscore.xbm @@ -0,0 +1,174 @@ +#define hopper_underscore_width 128 +#define hopper_underscore_height 128 +static char hopper_underscore_bits[] = { + 0xFF, 0xFF, 0xFF, 0x1F, 0xF0, 0x3D, 0x80, 0x96, 0xDA, 0xB6, 0xD6, 0x2A, + 0xA9, 0x6D, 0x25, 0x29, 0xFF, 0xFF, 0xFF, 0xBF, 0xFE, 0x7D, 0xC0, 0xFB, + 0x69, 0x69, 0xA9, 0xD5, 0x96, 0x96, 0x5E, 0x9A, 0xFF, 0xFF, 0xFF, 0x9F, + 0xFE, 0xFD, 0x80, 0xFF, 0x97, 0xB6, 0x5A, 0x6A, 0x5D, 0x6D, 0x5A, 0x62, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0xFD, 0xC1, 0xCF, 0x5F, 0x89, 0xA5, 0xD5, + 0xAA, 0x92, 0xA5, 0x59, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0xFD, 0xF8, 0x00, + 0xB8, 0x59, 0xAA, 0x56, 0xDA, 0x5A, 0x56, 0x49, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFE, 0xFC, 0xF9, 0x00, 0xF0, 0xA6, 0x55, 0xA9, 0x65, 0xA5, 0xA9, 0xA2, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0xFD, 0x71, 0x00, 0x60, 0x70, 0xAA, 0x54, + 0xAA, 0x5A, 0x54, 0x69, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0xFC, 0x38, 0x00, + 0xE0, 0x80, 0x55, 0xAB, 0x55, 0x75, 0xAB, 0x56, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFE, 0x1D, 0x70, 0x00, 0xF0, 0x00, 0xAE, 0xAA, 0xAA, 0xA6, 0x95, 0x55, + 0xFB, 0xFF, 0xFF, 0xFF, 0xFE, 0x1D, 0xF0, 0x00, 0xF0, 0x00, 0x50, 0x55, + 0x55, 0x59, 0x6A, 0xAA, 0xF3, 0xFF, 0xFF, 0xFF, 0xFE, 0x04, 0xF0, 0x00, + 0xF0, 0x00, 0xA4, 0xAA, 0xA9, 0xA5, 0x95, 0x55, 0x21, 0xFF, 0xFF, 0xFF, + 0xFE, 0x01, 0xF8, 0x00, 0xF0, 0x00, 0x50, 0x55, 0x56, 0x5A, 0x6A, 0xAA, + 0x83, 0x9F, 0xFF, 0xFF, 0xFE, 0x00, 0xFC, 0x00, 0xF0, 0x01, 0x50, 0xD5, + 0x95, 0x95, 0xA5, 0x5A, 0x83, 0x9F, 0xFF, 0xFF, 0xFE, 0x81, 0xFF, 0x00, + 0xE0, 0x0B, 0xA0, 0xAA, 0x7A, 0x6A, 0x5A, 0x65, 0x80, 0x1F, 0xF0, 0xFF, + 0xFE, 0xF0, 0xFF, 0x00, 0xF0, 0x3F, 0x50, 0x55, 0xA5, 0xB5, 0xA5, 0xAA, + 0x00, 0x1F, 0xF8, 0xFD, 0xFE, 0xF0, 0xF7, 0x00, 0xF0, 0x6D, 0xA0, 0xAA, + 0x5A, 0x6A, 0x5A, 0x55, 0x83, 0x0F, 0xF8, 0xC9, 0xFE, 0x79, 0x00, 0xF8, + 0x02, 0xC0, 0x81, 0x55, 0xAD, 0xD5, 0xA5, 0xA6, 0xE3, 0x03, 0xFC, 0xC0, + 0xFE, 0xFF, 0x04, 0xF0, 0x00, 0x80, 0x63, 0xAA, 0x52, 0x2A, 0x5A, 0x58, + 0xE7, 0x03, 0xFC, 0xE1, 0xFE, 0xFF, 0x2F, 0xFC, 0x03, 0x0C, 0xFE, 0x55, + 0xA9, 0x55, 0x66, 0x15, 0xF3, 0x0F, 0x7C, 0xF0, 0xFE, 0xFF, 0xFF, 0xFD, + 0xEF, 0x38, 0xFF, 0xAA, 0x56, 0xAA, 0x99, 0xAA, 0xF7, 0x1F, 0x78, 0xF0, + 0xFC, 0xFF, 0xFF, 0xFF, 0xFF, 0xF1, 0xFF, 0x55, 0xAA, 0x5B, 0x66, 0xA4, + 0xFF, 0x9F, 0xFD, 0xF0, 0xFE, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xAB, + 0x65, 0xA4, 0x99, 0x1B, 0xFF, 0xDF, 0xFF, 0xE0, 0xFE, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0x69, 0x51, 0x5A, 0x24, 0x62, 0xFF, 0xDF, 0xFF, 0xEC, + 0xFE, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x95, 0xAA, 0x24, 0x59, 0x55, + 0xFF, 0xFF, 0xFF, 0xFC, 0xFC, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x25, + 0x49, 0xAD, 0x55, 0x2A, 0xFF, 0xFF, 0xFF, 0xFC, 0xFD, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0x9B, 0xA5, 0x52, 0x2A, 0x94, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFD, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x67, 0x64, 0xC1, 0x92, 0x6D, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x95, + 0x49, 0x2C, 0x55, 0x92, 0xFF, 0xFF, 0xFF, 0xFF, 0xFC, 0xFF, 0xFF, 0xFF, + 0x7F, 0xFF, 0xFF, 0x15, 0xB4, 0x62, 0x44, 0x49, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFD, 0xFF, 0x51, 0x30, 0x03, 0xE0, 0xFF, 0x53, 0x03, 0x89, 0x96, 0x92, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFD, 0x7F, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xCD, + 0x68, 0x19, 0x68, 0x99, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0x1F, 0x00, 0x00, + 0x00, 0x00, 0xFF, 0x13, 0x4A, 0x64, 0x23, 0x49, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0x0F, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x47, 0x8A, 0xA4, 0xD8, 0x30, + 0xFE, 0xFF, 0xFF, 0xFF, 0xFC, 0x07, 0x00, 0x00, 0x00, 0x00, 0xF8, 0x39, + 0x29, 0x49, 0x06, 0x86, 0xCC, 0xFF, 0xFF, 0xFF, 0xFC, 0x03, 0x00, 0x00, + 0x80, 0x00, 0xF2, 0x81, 0x50, 0x31, 0xA2, 0x8C, 0xE4, 0xFF, 0xFF, 0xFF, + 0xFF, 0x0A, 0x08, 0x00, 0x00, 0x00, 0xF0, 0xA7, 0x46, 0x8C, 0x28, 0x31, + 0xC0, 0xFC, 0xFF, 0xFF, 0xFC, 0x03, 0x10, 0x00, 0xE0, 0x01, 0xE0, 0x19, + 0x41, 0x61, 0x66, 0x05, 0xE0, 0xB8, 0xFF, 0xFF, 0x7E, 0x0C, 0x44, 0x00, + 0x40, 0xBF, 0xEF, 0x41, 0x5C, 0x04, 0x00, 0xA8, 0xF0, 0xC1, 0xFF, 0xFF, + 0xFE, 0x21, 0x9E, 0x00, 0xE0, 0xFF, 0xED, 0xA5, 0x12, 0x92, 0xDB, 0xA2, + 0xE0, 0xC0, 0xFF, 0xFF, 0x7D, 0xFA, 0xFF, 0x00, 0x70, 0x00, 0xFF, 0x42, + 0x84, 0x0C, 0x00, 0x08, 0xC0, 0xC0, 0xFF, 0xFC, 0x7D, 0xFF, 0x90, 0x07, + 0xC0, 0x1F, 0xDB, 0x0A, 0x24, 0xC8, 0x68, 0x09, 0xCC, 0xC0, 0x7F, 0xFE, + 0x1E, 0x7F, 0x3E, 0x00, 0xD4, 0xFF, 0x3E, 0x60, 0x81, 0x21, 0x02, 0xA0, + 0xFC, 0xE0, 0x7F, 0xFF, 0xCE, 0x9F, 0xFF, 0x05, 0xFA, 0xFF, 0xBF, 0x04, + 0x24, 0x0A, 0x61, 0x1B, 0xFC, 0xC0, 0xBF, 0xFF, 0xAC, 0x7D, 0xFF, 0x03, + 0x40, 0x1F, 0x80, 0x91, 0xC2, 0xA0, 0x05, 0x80, 0xFF, 0x8D, 0x7F, 0xFF, + 0xEF, 0xF5, 0xDF, 0x00, 0x00, 0x94, 0x42, 0x81, 0x01, 0x10, 0x92, 0x85, + 0xFF, 0xFD, 0xFF, 0xFE, 0x2C, 0x62, 0x03, 0x00, 0x20, 0x00, 0x10, 0x2B, + 0x24, 0x82, 0x00, 0x20, 0xFF, 0xFD, 0xFF, 0xFF, 0xBD, 0x55, 0x01, 0x00, + 0x40, 0x00, 0x08, 0x19, 0x81, 0x58, 0x54, 0x14, 0xFF, 0xFF, 0xFF, 0xFF, + 0x3C, 0x82, 0x05, 0x08, 0x00, 0x24, 0x20, 0x81, 0x24, 0x02, 0x02, 0x82, + 0xFF, 0xFF, 0xFF, 0xFF, 0x3D, 0x82, 0x01, 0x00, 0x00, 0x00, 0x69, 0x22, + 0x90, 0xA0, 0x49, 0x06, 0xFF, 0xFF, 0xFF, 0xFF, 0x2D, 0x26, 0x00, 0x00, + 0x00, 0x00, 0x20, 0x20, 0x0A, 0x02, 0x20, 0xA0, 0xFF, 0xFF, 0xFF, 0xFF, + 0x04, 0x81, 0x01, 0x00, 0x10, 0x00, 0xA0, 0x04, 0x02, 0x0A, 0x09, 0x00, + 0xFF, 0xFF, 0xFF, 0xFF, 0x81, 0x19, 0x02, 0x02, 0x00, 0x00, 0x02, 0x42, + 0x50, 0xA0, 0x20, 0x95, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x60, 0x00, 0x01, + 0x80, 0x02, 0x42, 0x52, 0x40, 0x20, 0xA0, 0x20, 0xFF, 0xFF, 0xFF, 0x7F, + 0x00, 0x00, 0x8C, 0x01, 0x40, 0x00, 0x00, 0x00, 0x06, 0x06, 0x09, 0x05, + 0xFF, 0xFF, 0xFF, 0x3F, 0x00, 0x50, 0x00, 0x00, 0x40, 0x00, 0x02, 0x18, + 0xA0, 0x90, 0x84, 0x64, 0xFF, 0xFF, 0xFF, 0x3F, 0x00, 0x10, 0x81, 0x07, + 0xD0, 0x00, 0x48, 0x82, 0x02, 0x02, 0x20, 0x00, 0xFF, 0xFF, 0xFF, 0x0F, + 0x00, 0x04, 0xC0, 0x7E, 0x9F, 0x01, 0x10, 0x04, 0x00, 0x00, 0x24, 0x25, + 0xFF, 0xFF, 0xFF, 0x0F, 0x00, 0x60, 0x48, 0xC0, 0xC1, 0x13, 0x00, 0x41, + 0x9A, 0x99, 0x81, 0x00, 0xFF, 0xFF, 0xFF, 0x03, 0x40, 0x40, 0x60, 0x00, + 0x84, 0x0B, 0x00, 0x50, 0x02, 0x40, 0x24, 0x90, 0xFF, 0xFF, 0xFF, 0x01, + 0x00, 0x00, 0x32, 0x00, 0x00, 0x57, 0x00, 0x04, 0x40, 0x02, 0x01, 0x05, + 0xF8, 0xFF, 0xFF, 0x01, 0x40, 0x00, 0x38, 0x00, 0x00, 0x0E, 0x10, 0x05, + 0x99, 0x0A, 0xA4, 0x0A, 0xFC, 0xFF, 0x7F, 0x00, 0x80, 0x30, 0x10, 0x00, + 0x00, 0x84, 0x04, 0x60, 0x00, 0x50, 0x00, 0x60, 0xFC, 0xFF, 0xFF, 0x00, + 0x00, 0x3A, 0x08, 0x00, 0x00, 0x04, 0x8E, 0x06, 0x00, 0x41, 0x56, 0x00, + 0xF8, 0xFE, 0xFF, 0x01, 0x80, 0x79, 0xA0, 0x7F, 0xFF, 0x01, 0x06, 0x20, + 0x51, 0x08, 0x00, 0x2A, 0xF8, 0xFF, 0xFF, 0x00, 0xE0, 0x3F, 0xE0, 0xAF, + 0x00, 0x00, 0x06, 0x11, 0x91, 0x52, 0x02, 0x92, 0xF8, 0xFF, 0xFF, 0x00, + 0x00, 0x7F, 0x00, 0x00, 0x80, 0x02, 0x02, 0x08, 0x00, 0x00, 0x50, 0x00, + 0x7E, 0xFF, 0xFF, 0x01, 0x40, 0x7F, 0x00, 0x00, 0x10, 0x80, 0x86, 0x00, + 0x08, 0x09, 0x06, 0x08, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFC, 0x20, 0x51, + 0x0D, 0x20, 0x23, 0x94, 0x91, 0x50, 0x20, 0x92, 0xFF, 0xFE, 0xFF, 0x03, + 0x20, 0x70, 0x05, 0x88, 0x01, 0x80, 0x21, 0x06, 0x84, 0x44, 0x80, 0x25, + 0xFF, 0xFF, 0xFF, 0x03, 0x40, 0xEA, 0x90, 0x00, 0x20, 0xA4, 0x05, 0x00, + 0x00, 0x10, 0x26, 0x00, 0xFF, 0xFF, 0xFF, 0x07, 0x40, 0xD8, 0x08, 0x00, + 0x00, 0xA0, 0x81, 0x92, 0x01, 0x01, 0x84, 0x40, 0xFF, 0xFF, 0xFF, 0x0F, + 0x20, 0xC3, 0x42, 0x00, 0x00, 0x0A, 0x24, 0x00, 0x44, 0x48, 0x20, 0x25, + 0xFF, 0xFF, 0xFF, 0x0F, 0x20, 0xD5, 0x05, 0x02, 0x00, 0x46, 0x12, 0x40, + 0x02, 0x22, 0x44, 0x00, 0xFF, 0xFF, 0xFF, 0x1F, 0x90, 0x50, 0x90, 0x00, + 0x00, 0x9A, 0x40, 0x02, 0x40, 0x00, 0x10, 0x64, 0xFF, 0xFF, 0xFF, 0x1F, + 0x98, 0x62, 0x7C, 0x10, 0x11, 0xDF, 0x07, 0x14, 0x28, 0x08, 0x01, 0x05, + 0xFF, 0xFF, 0xFF, 0x1F, 0x20, 0x74, 0xF8, 0xE5, 0xEA, 0x87, 0x23, 0x01, + 0x81, 0x11, 0x48, 0x20, 0xFF, 0xFF, 0xFF, 0x1F, 0x50, 0x38, 0xF2, 0xFF, + 0xFF, 0xC7, 0x8F, 0x80, 0x00, 0x84, 0x40, 0x04, 0xFF, 0xFF, 0xFF, 0xBF, + 0x68, 0x3D, 0xE9, 0xFF, 0x7F, 0xC1, 0x1F, 0x28, 0x14, 0x10, 0x06, 0x50, + 0xFF, 0xFF, 0xFF, 0x3F, 0x90, 0x3F, 0x81, 0xFE, 0x5F, 0x89, 0x3F, 0x82, + 0x00, 0x02, 0xA2, 0x01, 0xFF, 0xFF, 0xFF, 0xBF, 0xB0, 0x3F, 0xB1, 0xFF, + 0xBF, 0xC1, 0xFF, 0x00, 0x82, 0x80, 0x00, 0x24, 0xFF, 0xFB, 0xFF, 0x7F, + 0xF8, 0x3F, 0xA9, 0xFC, 0x5F, 0x81, 0xFF, 0x0F, 0x08, 0x00, 0x04, 0x48, + 0xFF, 0xF0, 0xFF, 0xBF, 0xFC, 0x3F, 0x41, 0xEB, 0xE7, 0xC0, 0xFF, 0x1F, + 0x40, 0x24, 0x40, 0x01, 0xFF, 0xE0, 0xFF, 0xFF, 0xFF, 0x3F, 0xB3, 0x7D, + 0xAD, 0x80, 0xFF, 0xFF, 0x01, 0x00, 0xA0, 0x90, 0x7F, 0xC0, 0xFF, 0xFF, + 0xFF, 0x3F, 0x43, 0xFA, 0x27, 0xC0, 0xFF, 0xFF, 0x93, 0x51, 0x04, 0x04, + 0x7F, 0x80, 0xFF, 0xFF, 0xFF, 0x3F, 0x60, 0x6D, 0x56, 0xC0, 0xFF, 0xFF, + 0x1F, 0x20, 0x0A, 0x40, 0x7F, 0x80, 0xFF, 0xFF, 0xFF, 0x3F, 0x84, 0x6A, + 0x6D, 0xC0, 0xFF, 0xFF, 0x3F, 0x00, 0x00, 0x02, 0x7F, 0x00, 0xFF, 0xFF, + 0xFF, 0x3F, 0x2C, 0xBA, 0x22, 0xC0, 0xFF, 0xFF, 0xFF, 0x87, 0x04, 0x98, + 0x7F, 0x80, 0xFF, 0xFF, 0xFF, 0x3F, 0x48, 0x66, 0xD5, 0xC0, 0xFF, 0xFF, + 0xFF, 0x3F, 0x90, 0x02, 0x7F, 0xC0, 0xFF, 0xFF, 0xFF, 0x7F, 0x80, 0xA6, + 0x63, 0xE0, 0xFF, 0xFF, 0xFF, 0xFF, 0x08, 0x24, 0x7F, 0xF8, 0xFF, 0xFF, + 0xFF, 0x7F, 0x90, 0x78, 0x3A, 0xE0, 0xFF, 0xFF, 0xFF, 0xFF, 0x27, 0x00, + 0x7F, 0xFC, 0xFF, 0xFF, 0xFF, 0x7F, 0x00, 0xF9, 0x06, 0xF0, 0xFF, 0xFF, + 0xFF, 0xFF, 0x8F, 0x61, 0x7F, 0xFC, 0xFF, 0xFF, 0xFF, 0x7F, 0x00, 0xA4, + 0x0B, 0xF0, 0xFF, 0xFF, 0xFF, 0xFF, 0x3F, 0x04, 0x7F, 0xFE, 0xFF, 0xFF, + 0xFF, 0xFF, 0x00, 0xD0, 0x01, 0xF0, 0xFF, 0xFF, 0xFF, 0xFF, 0x7F, 0x08, + 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x01, 0x00, 0xF8, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0x61, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x88, + 0x01, 0xF8, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0xFE, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0x00, 0x00, 0x00, 0xF8, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x48, + 0xFC, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x01, 0x00, 0x07, 0xFC, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0x01, 0xFC, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x01, 0xD0, + 0x0F, 0xFE, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x69, 0xF8, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0x03, 0xFF, 0x0F, 0xFC, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x05, + 0xF0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x03, 0xFF, 0x3F, 0xFE, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0x21, 0xF0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x83, 0xFF, + 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x03, 0xF0, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xC3, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x6B, + 0xF0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF7, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0x23, 0xE0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x07, 0xE0, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xEF, 0xFB, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x27, + 0xF0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x0F, 0xF0, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0x0F, 0xE0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x07, + 0xF0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x0F, 0xF0, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0x03, 0xF0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x8F, + 0xE0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x03, 0xF0, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0x1F, 0xF0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x01, + 0xF0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x8F, 0xF0, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0x00, 0xF8, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x2F, + 0xF0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0xF8, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0x1F, 0xF0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x3F, 0x00, + 0xF8, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x1F, 0xF0, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0x3F, 0x00, 0xFC, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x9F, + 0xF8, 0xFF, 0x07, 0xFF, 0xFF, 0xFF, 0x7F, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0x1F, 0xF0, 0xFF, 0x03, 0x00, 0xF1, 0xFF, 0x7F, 0x80, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x1F, 0xF0, 0xFF, 0x03, 0x00, + 0xF0, 0xFF, 0xFF, 0x00, 0xFF, 0xFF, 0x7F, 0x6F, 0xFD, 0xFF, 0xFF, 0x1F, + 0xF8, 0xFF, 0x03, 0x00, 0xE0, 0xFF, 0xFF, 0x80, 0xFF, 0xFF, 0x7F, 0x7F, + 0xFD, 0xFF, 0xFF, 0x7F, 0xF0, 0xFF, 0x03, 0x00, 0xC0, 0xFF, 0xFF, 0x90, + 0xFF, 0xFF, 0x5F, 0xA2, 0xF4, 0xFF, 0xFF, 0x3F, 0xF8, 0xFF, 0x03, 0x00, + 0x80, 0xFF, 0xFF, 0x80, 0xFF, 0xFF, 0x0F, 0xAA, 0x42, 0xF1, 0xFF, 0x3F, + 0xF8, 0xFF, 0xAF, 0x00, 0x80, 0xFF, 0xFF, 0xD9, 0xFF, 0xFF, 0x0F, 0x02, + 0x64, 0xF1, 0xFF, 0x3F, 0xF8, 0xFF, 0xFF, 0x3F, 0xC2, 0xFF, 0xFF, 0xDB, + 0xFF, 0xFF, 0xCB, 0xD1, 0xDA, 0xF7, 0xFF, 0x7F, 0xF8, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x71, 0x44, 0xFC, 0xF6, 0xFF, 0x3F, + 0xFC, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xB0, 0xC0, + 0xA8, 0xFF, 0xFF, 0x3F, 0xFC, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xBF, 0xFC, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x3F, + 0xFC, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0x7F, 0xFC, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x7F, }; diff --git a/Tests/images/imagedraw_ellipse_edge.png b/Tests/images/imagedraw_ellipse_edge.png new file mode 100644 index 000000000..25a95a601 Binary files /dev/null and b/Tests/images/imagedraw_ellipse_edge.png differ diff --git a/Tests/images/invalid.spider b/Tests/images/invalid.spider new file mode 100644 index 000000000..c6b024d6e Binary files /dev/null and b/Tests/images/invalid.spider differ diff --git a/Tests/images/junk_jpeg_header.jpg b/Tests/images/junk_jpeg_header.jpg index 564eb3199..68819f243 100644 Binary files a/Tests/images/junk_jpeg_header.jpg and b/Tests/images/junk_jpeg_header.jpg differ diff --git a/Tests/images/libtiff_segfault.tif b/Tests/images/libtiff_segfault.tif new file mode 100644 index 000000000..8665e5f2b Binary files /dev/null and b/Tests/images/libtiff_segfault.tif differ diff --git a/Tests/images/multiline_text_center.png b/Tests/images/multiline_text_center.png new file mode 100644 index 000000000..f44d0783a Binary files /dev/null and b/Tests/images/multiline_text_center.png differ diff --git a/Tests/images/multiline_text_right.png b/Tests/images/multiline_text_right.png new file mode 100644 index 000000000..1b32d9167 Binary files /dev/null and b/Tests/images/multiline_text_right.png differ diff --git a/Tests/images/multiline_text_spacing.png b/Tests/images/multiline_text_spacing.png new file mode 100644 index 000000000..3c3bc0f26 Binary files /dev/null and b/Tests/images/multiline_text_spacing.png differ diff --git a/Tests/images/no_cursors.cur b/Tests/images/no_cursors.cur new file mode 100644 index 000000000..a98e1035a Binary files /dev/null and b/Tests/images/no_cursors.cur differ diff --git a/Tests/images/rdf.tif b/Tests/images/rdf.tif new file mode 100644 index 000000000..524a9dbc8 Binary files /dev/null and b/Tests/images/rdf.tif differ diff --git a/Tests/images/sugarshack_bad_mpo_header.jpg b/Tests/images/sugarshack_bad_mpo_header.jpg new file mode 100644 index 000000000..6e2d75ab6 Binary files /dev/null and b/Tests/images/sugarshack_bad_mpo_header.jpg differ diff --git a/Tests/images/test.gpl b/Tests/images/test.gpl new file mode 100644 index 000000000..7436a3099 --- /dev/null +++ b/Tests/images/test.gpl @@ -0,0 +1,4 @@ +GIMP Palette +Name: Test +Columns: 0 +# diff --git a/Tests/images/tiff_gray_2_4_bpp/hopper2.tif b/Tests/images/tiff_gray_2_4_bpp/hopper2.tif new file mode 100644 index 000000000..70057219a Binary files /dev/null and b/Tests/images/tiff_gray_2_4_bpp/hopper2.tif differ diff --git a/Tests/images/tiff_gray_2_4_bpp/hopper2I.tif b/Tests/images/tiff_gray_2_4_bpp/hopper2I.tif new file mode 100644 index 000000000..9feaa08f0 Binary files /dev/null and b/Tests/images/tiff_gray_2_4_bpp/hopper2I.tif differ diff --git a/Tests/images/tiff_gray_2_4_bpp/hopper2IR.tif b/Tests/images/tiff_gray_2_4_bpp/hopper2IR.tif new file mode 100644 index 000000000..462416955 Binary files /dev/null and b/Tests/images/tiff_gray_2_4_bpp/hopper2IR.tif differ diff --git a/Tests/images/tiff_gray_2_4_bpp/hopper2R.tif b/Tests/images/tiff_gray_2_4_bpp/hopper2R.tif new file mode 100644 index 000000000..65e0f623e Binary files /dev/null and b/Tests/images/tiff_gray_2_4_bpp/hopper2R.tif differ diff --git a/Tests/images/tiff_gray_2_4_bpp/hopper4.tif b/Tests/images/tiff_gray_2_4_bpp/hopper4.tif new file mode 100644 index 000000000..7a38271ea Binary files /dev/null and b/Tests/images/tiff_gray_2_4_bpp/hopper4.tif differ diff --git a/Tests/images/tiff_gray_2_4_bpp/hopper4I.tif b/Tests/images/tiff_gray_2_4_bpp/hopper4I.tif new file mode 100644 index 000000000..5c3a17f52 Binary files /dev/null and b/Tests/images/tiff_gray_2_4_bpp/hopper4I.tif differ diff --git a/Tests/images/tiff_gray_2_4_bpp/hopper4IR.tif b/Tests/images/tiff_gray_2_4_bpp/hopper4IR.tif new file mode 100644 index 000000000..7652f5e6d Binary files /dev/null and b/Tests/images/tiff_gray_2_4_bpp/hopper4IR.tif differ diff --git a/Tests/images/tiff_gray_2_4_bpp/hopper4R.tif b/Tests/images/tiff_gray_2_4_bpp/hopper4R.tif new file mode 100644 index 000000000..0cdf4d26e Binary files /dev/null and b/Tests/images/tiff_gray_2_4_bpp/hopper4R.tif differ diff --git a/Tests/images/truncated_image.png b/Tests/images/truncated_image.png new file mode 100644 index 000000000..cb32dc986 Binary files /dev/null and b/Tests/images/truncated_image.png differ diff --git a/Tests/images/unbound_variable.jp2 b/Tests/images/unbound_variable.jp2 new file mode 100644 index 000000000..ed6d5943e Binary files /dev/null and b/Tests/images/unbound_variable.jp2 differ diff --git a/Tests/import_all.py b/Tests/import_all.py index 88a102b64..c5960d8bb 100644 --- a/Tests/import_all.py +++ b/Tests/import_all.py @@ -1,11 +1,12 @@ from __future__ import print_function -import sys -sys.path.insert(0, ".") import glob import os import traceback +import sys +sys.path.insert(0, ".") + for file in glob.glob("PIL/*.py"): module = os.path.basename(file)[:-3] try: diff --git a/Tests/large_memory_numpy_test.py b/Tests/large_memory_numpy_test.py index 5e5a58441..5d1b42a22 100644 --- a/Tests/large_memory_numpy_test.py +++ b/Tests/large_memory_numpy_test.py @@ -7,20 +7,20 @@ from helper import unittest, PillowTestCase # It requires > 2gb memory for the >2 gigapixel image generated in the # second test. Running this automatically would amount to a denial of # service on our testing infrastructure. I expect this test to fail -# on any 32 bit machine, as well as any smallish things (like +# on any 32-bit machine, as well as any smallish things (like # Raspberry Pis). from PIL import Image try: import numpy as np -except: +except ImportError: raise unittest.SkipTest("numpy not installed") YDIM = 32769 XDIM = 48000 -@unittest.skipIf(sys.maxsize <= 2**32, "requires 64 bit system") +@unittest.skipIf(sys.maxsize <= 2**32, "requires 64-bit system") class LargeMemoryNumpyTest(PillowTestCase): def _write_png(self, xdim, ydim): diff --git a/Tests/large_memory_test.py b/Tests/large_memory_test.py index 3ee13091d..778aff511 100644 --- a/Tests/large_memory_test.py +++ b/Tests/large_memory_test.py @@ -7,16 +7,16 @@ from helper import unittest, PillowTestCase # It requires > 2gb memory for the >2 gigapixel image generated in the # second test. Running this automatically would amount to a denial of # service on our testing infrastructure. I expect this test to fail -# on any 32 bit machine, as well as any smallish things (like +# on any 32-bit machine, as well as any smallish things (like # Raspberry Pis). It does succeed on a 3gb Ubuntu 12.04x64 VM on Python -# 2.7 an 3.2. +# 2.7 and 3.2. from PIL import Image YDIM = 32769 XDIM = 48000 -@unittest.skipIf(sys.maxsize <= 2**32, "requires 64 bit system") +@unittest.skipIf(sys.maxsize <= 2**32, "requires 64-bit system") class LargeMemoryTest(PillowTestCase): def _write_png(self, xdim, ydim): diff --git a/Tests/show_icc.py b/Tests/show_icc.py deleted file mode 100644 index e062747e7..000000000 --- a/Tests/show_icc.py +++ /dev/null @@ -1,28 +0,0 @@ -import sys -sys.path.insert(0, ".") - -from PIL import Image -from PIL import ImageCms - -try: - filename = sys.argv[1] -except IndexError: - filename = "../pil-archive/cmyk.jpg" - -i = Image.open(filename) - -print(i.format) -print(i.mode) -print(i.size) -print(i.tile) - -p = ImageCms.getMemoryProfile(i.info["icc_profile"]) - -print(repr(p.product_name)) -print(repr(p.product_info)) - -o = ImageCms.createProfile("sRGB") -t = ImageCms.buildTransformFromOpenProfiles(p, o, i.mode, "RGB") -i = ImageCms.applyTransform(i, t) - -i.show() diff --git a/Tests/show_mcidas.py b/Tests/show_mcidas.py deleted file mode 100644 index 1f1c04aa8..000000000 --- a/Tests/show_mcidas.py +++ /dev/null @@ -1,27 +0,0 @@ -from __future__ import print_function -import sys -sys.path.insert(0, ".") - -from PIL import Image -from PIL import ImageMath - -try: - filename = sys.argv[1] -except IndexError: - filename = "../pil-archive/goes12.2005.140.190925.BAND_01.mcidas" - # filename = "../pil-archive/goes12.2005.140.190925.BAND_01.im" - -im = Image.open(filename) - -print(im.format) -print(im.mode) -print(im.size) -print(im.tile) - -lo, hi = im.getextrema() - -print("map", lo, hi, "->", end=' ') -im = ImageMath.eval("convert(im*255/hi, 'L')", im=im, hi=hi) -print(im.getextrema()) - -im.show() diff --git a/Tests/test_cffi.py b/Tests/test_cffi.py index 5d5427685..02d1ff7d3 100644 --- a/Tests/test_cffi.py +++ b/Tests/test_cffi.py @@ -3,7 +3,7 @@ from helper import unittest, PillowTestCase, hopper try: import cffi from PIL import PyAccess -except: +except ImportError: # Skip in setUp() pass @@ -20,7 +20,7 @@ class TestCffiPutPixel(TestImagePutPixel): def setUp(self): try: import cffi - except: + except ImportError: self.skipTest("No cffi") def test_put(self): @@ -32,7 +32,7 @@ class TestCffiGetPixel(TestImageGetPixel): def setUp(self): try: import cffi - except: + except ImportError: self.skipTest("No cffi") def test_get(self): @@ -45,7 +45,7 @@ class TestCffi(PillowTestCase): def setUp(self): try: import cffi - except: + except ImportError: self.skipTest("No cffi") def _test_get_access(self, im): @@ -61,6 +61,10 @@ class TestCffi(PillowTestCase): for y in range(0, h, 10): self.assertEqual(access[(x, y)], caccess[(x, y)]) + # Access an out-of-range pixel + self.assertRaises(ValueError, + lambda: access[(access.xsize+1, access.ysize+1)]) + def test_get_vs_c(self): rgb = hopper('RGB') rgb.load() @@ -70,7 +74,7 @@ class TestCffi(PillowTestCase): self._test_get_access(hopper('LA')) self._test_get_access(hopper('1')) self._test_get_access(hopper('P')) - # self._test_get_access(hopper('PA')) # PA -- how do I make a PA image? + # self._test_get_access(hopper('PA')) # PA -- how do I make a PA image? self._test_get_access(hopper('F')) im = Image.new('I;16', (10, 10), 40000) @@ -103,6 +107,14 @@ class TestCffi(PillowTestCase): access[(x, y)] = color self.assertEqual(color, caccess[(x, y)]) + # Attempt to set the value on a read-only image + access = PyAccess.new(im, True) + try: + access[(0, 0)] = color + except ValueError: + return + self.fail("Putpixel did not fail on a read-only image") + def test_set_vs_c(self): rgb = hopper('RGB') rgb.load() diff --git a/Tests/test_file_bmp.py b/Tests/test_file_bmp.py index 69792fe12..25f70139d 100644 --- a/Tests/test_file_bmp.py +++ b/Tests/test_file_bmp.py @@ -1,6 +1,6 @@ from helper import unittest, PillowTestCase, hopper -from PIL import Image +from PIL import Image, BmpImagePlugin import io @@ -25,6 +25,11 @@ class TestFileBmp(PillowTestCase): self.roundtrip(hopper("P")) self.roundtrip(hopper("RGB")) + def test_invalid_file(self): + with open("Tests/images/flower.jpg", "rb") as fp: + self.assertRaises(SyntaxError, + lambda: BmpImagePlugin.BmpImageFile(fp)) + def test_save_to_bytes(self): output = io.BytesIO() im = hopper() @@ -49,6 +54,22 @@ class TestFileBmp(PillowTestCase): self.assertEqual(reloaded.info["dpi"], dpi) + def test_save_bmp_with_dpi(self): + # Test for #1301 + # Arrange + outfile = self.tempfile("temp.jpg") + im = Image.open("Tests/images/hopper.bmp") + + # Act + im.save(outfile, 'JPEG', dpi=im.info['dpi']) + + # Assert + reloaded = Image.open(outfile) + reloaded.load() + self.assertEqual(im.info['dpi'], reloaded.info['dpi']) + self.assertEqual(im.size, reloaded.size) + self.assertEqual(reloaded.format, "JPEG") + if __name__ == '__main__': unittest.main() diff --git a/Tests/test_file_bufrstub.py b/Tests/test_file_bufrstub.py new file mode 100644 index 000000000..05266e9fd --- /dev/null +++ b/Tests/test_file_bufrstub.py @@ -0,0 +1,19 @@ +from helper import unittest, PillowTestCase + +from PIL import BufrStubImagePlugin + + +class TestFileBufrStub(PillowTestCase): + + def test_invalid_file(self): + invalid_file = "Tests/images/flower.jpg" + + self.assertRaises(SyntaxError, + lambda: + BufrStubImagePlugin.BufrStubImageFile(invalid_file)) + + +if __name__ == '__main__': + unittest.main() + +# End of file diff --git a/Tests/test_file_cur.py b/Tests/test_file_cur.py index 07bf3a750..f1c077945 100644 --- a/Tests/test_file_cur.py +++ b/Tests/test_file_cur.py @@ -2,17 +2,14 @@ from helper import unittest, PillowTestCase from PIL import Image, CurImagePlugin +TEST_FILE = "Tests/images/deerstalker.cur" + class TestFileCur(PillowTestCase): def test_sanity(self): - # Arrange - test_file = "Tests/images/deerstalker.cur" + im = Image.open(TEST_FILE) - # Act - im = Image.open(test_file) - - # Assert self.assertEqual(im.size, (32, 32)) self.assertIsInstance(im, CurImagePlugin.CurImageFile) # Check some pixel colors to ensure image is loaded properly @@ -20,6 +17,18 @@ class TestFileCur(PillowTestCase): self.assertEqual(im.getpixel((11, 1)), (253, 254, 254, 1)) self.assertEqual(im.getpixel((16, 16)), (84, 87, 86, 255)) + def test_invalid_file(self): + invalid_file = "Tests/images/flower.jpg" + + self.assertRaises(SyntaxError, + lambda: CurImagePlugin.CurImageFile(invalid_file)) + + no_cursors_file = "Tests/images/no_cursors.cur" + + cur = CurImagePlugin.CurImageFile(TEST_FILE) + cur.fp = open(no_cursors_file, "rb") + self.assertRaises(TypeError, cur._open) + if __name__ == '__main__': unittest.main() diff --git a/Tests/test_file_dcx.py b/Tests/test_file_dcx.py index 7d2ae32d8..2c0f90c1f 100644 --- a/Tests/test_file_dcx.py +++ b/Tests/test_file_dcx.py @@ -20,6 +20,11 @@ class TestFileDcx(PillowTestCase): orig = hopper() self.assert_image_equal(im, orig) + def test_invalid_file(self): + with open("Tests/images/flower.jpg", "rb") as fp: + self.assertRaises(SyntaxError, + lambda: DcxImagePlugin.DcxImageFile(fp)) + def test_tell(self): # Arrange im = Image.open(TEST_FILE) @@ -33,6 +38,19 @@ class TestFileDcx(PillowTestCase): def test_n_frames(self): im = Image.open(TEST_FILE) self.assertEqual(im.n_frames, 1) + self.assertFalse(im.is_animated) + + def test_eoferror(self): + im = Image.open(TEST_FILE) + + n_frames = im.n_frames + while True: + n_frames -= 1 + try: + im.seek(n_frames) + break + except EOFError: + self.assertTrue(im.tell() < n_frames) def test_seek_too_far(self): # Arrange diff --git a/Tests/test_file_dds.py b/Tests/test_file_dds.py new file mode 100644 index 000000000..a4442a2e8 --- /dev/null +++ b/Tests/test_file_dds.py @@ -0,0 +1,99 @@ +from io import BytesIO + +from helper import unittest, PillowTestCase +from PIL import Image, DdsImagePlugin + +TEST_FILE_DXT1 = "Tests/images/dxt1-rgb-4bbp-noalpha_MipMaps-1.dds" +TEST_FILE_DXT3 = "Tests/images/dxt3-argb-8bbp-explicitalpha_MipMaps-1.dds" +TEST_FILE_DXT5 = "Tests/images/dxt5-argb-8bbp-interpolatedalpha_MipMaps-1.dds" + + +class TestFileDds(PillowTestCase): + """Test DdsImagePlugin""" + + def test_sanity_dxt1(self): + """Check DXT1 images can be opened""" + target = Image.open(TEST_FILE_DXT1.replace('.dds', '.png')) + + im = Image.open(TEST_FILE_DXT1) + im.load() + + self.assertEqual(im.format, "DDS") + self.assertEqual(im.mode, "RGBA") + self.assertEqual(im.size, (256, 256)) + + # This target image is from the test set of images, and is exact. + self.assert_image_equal(target.convert('RGBA'), im) + + def test_sanity_dxt5(self): + """Check DXT5 images can be opened""" + + target = Image.open(TEST_FILE_DXT5.replace('.dds', '.png')) + + im = Image.open(TEST_FILE_DXT5) + im.load() + + self.assertEqual(im.format, "DDS") + self.assertEqual(im.mode, "RGBA") + self.assertEqual(im.size, (256, 256)) + + # Imagemagick, which generated this target image from the .dds + # has a slightly different decoder than is standard. It looks + # a little brighter. The 0,0 pixel is (00,6c,f8,ff) by our code, + # and by the target image for the DXT1, and the imagemagick .png + # is giving (00, 6d, ff, ff). So, assert similar, pretty tight + # I'm currently seeing about a 3 for the epsilon. + self.assert_image_similar(target, im, 5) + + def test_sanity_dxt3(self): + """Check DXT3 images are not supported""" + self.assertRaises(NotImplementedError, + lambda: Image.open(TEST_FILE_DXT3)) + + def test__validate_true(self): + """Check valid prefix""" + # Arrange + prefix = b"DDS etc" + + # Act + output = DdsImagePlugin._validate(prefix) + + # Assert + self.assertTrue(output) + + def test__validate_false(self): + """Check invalid prefix""" + # Arrange + prefix = b"something invalid" + + # Act + output = DdsImagePlugin._validate(prefix) + + # Assert + self.assertFalse(output) + + def test_short_header(self): + """ Check a short header""" + with open(TEST_FILE_DXT5, 'rb') as f: + img_file = f.read() + + def short_header(): + Image.open(BytesIO(img_file[:119])) + + self.assertRaises(IOError, short_header) + + def test_short_file(self): + """ Check that the appropriate error is thrown for a short file""" + + with open(TEST_FILE_DXT5, 'rb') as f: + img_file = f.read() + + def short_file(): + Image.open(BytesIO(img_file[:-100])) + + self.assertRaises(IOError, short_file) + +if __name__ == '__main__': + unittest.main() + +# End of file diff --git a/Tests/test_file_eps.py b/Tests/test_file_eps.py index f1fbac922..7e6207976 100644 --- a/Tests/test_file_eps.py +++ b/Tests/test_file_eps.py @@ -51,6 +51,12 @@ class TestFileEps(PillowTestCase): self.assertEqual(image2_scale2.size, (720, 504)) self.assertEqual(image2_scale2.format, "EPS") + def test_invalid_file(self): + invalid_file = "Tests/images/flower.jpg" + + self.assertRaises(SyntaxError, + lambda: EpsImagePlugin.EpsImageFile(invalid_file)) + def test_file_object(self): # issue 479 image1 = Image.open(file1) @@ -118,15 +124,18 @@ class TestFileEps(PillowTestCase): # Arrange image1 = Image.open(file1) image2 = Image.open(file2) + image3 = Image.open("Tests/images/illu10_preview.eps") new_size = (100, 100) # Act image1 = image1.resize(new_size) image2 = image2.resize(new_size) + image3 = image3.resize(new_size) # Assert self.assertEqual(image1.size, new_size) self.assertEqual(image2.size, new_size) + self.assertEqual(image3.size, new_size) def test_thumbnail(self): # Issue #619 @@ -149,7 +158,9 @@ class TestFileEps(PillowTestCase): Image.open(file3) def _test_readline(self, t, ending): - ending = "Failure with line ending: %s" % ("".join("%s" % ord(s) for s in ending)) + ending = "Failure with line ending: %s" % ("".join( + "%s" % ord(s) + for s in ending)) self.assertEqual(t.readline().strip('\r\n'), 'something', ending) self.assertEqual(t.readline().strip('\r\n'), 'else', ending) self.assertEqual(t.readline().strip('\r\n'), 'baz', ending) @@ -159,7 +170,7 @@ class TestFileEps(PillowTestCase): # check all the freaking line endings possible try: import StringIO - except: + except ImportError: # don't skip, it skips everything in the parent test return t = StringIO.StringIO(test_string) diff --git a/Tests/test_file_fitsstub.py b/Tests/test_file_fitsstub.py new file mode 100644 index 000000000..cc38b4a3a --- /dev/null +++ b/Tests/test_file_fitsstub.py @@ -0,0 +1,19 @@ +from helper import unittest, PillowTestCase + +from PIL import FitsStubImagePlugin + + +class TestFileFitsStub(PillowTestCase): + + def test_invalid_file(self): + invalid_file = "Tests/images/flower.jpg" + + self.assertRaises(SyntaxError, + lambda: + FitsStubImagePlugin.FITSStubImageFile(invalid_file)) + + +if __name__ == '__main__': + unittest.main() + +# End of file diff --git a/Tests/test_file_fli.py b/Tests/test_file_fli.py index 04c2006c9..aa2fa5ae5 100644 --- a/Tests/test_file_fli.py +++ b/Tests/test_file_fli.py @@ -1,6 +1,6 @@ from helper import unittest, PillowTestCase -from PIL import Image +from PIL import Image, FliImagePlugin # sample ppm stream # created as an export of a palette image from Gimp2.6 @@ -18,9 +18,28 @@ class TestFileFli(PillowTestCase): self.assertEqual(im.size, (128, 128)) self.assertEqual(im.format, "FLI") + def test_invalid_file(self): + invalid_file = "Tests/images/flower.jpg" + + self.assertRaises(SyntaxError, + lambda: FliImagePlugin.FliImageFile(invalid_file)) + def test_n_frames(self): im = Image.open(test_file) - self.assertEqual(im.n_frames, 2) + self.assertEqual(im.n_frames, 1) + self.assertFalse(im.is_animated) + + def test_eoferror(self): + im = Image.open(test_file) + + n_frames = im.n_frames + while True: + n_frames -= 1 + try: + im.seek(n_frames) + break + except EOFError: + self.assertTrue(im.tell() < n_frames) if __name__ == '__main__': diff --git a/Tests/test_file_fpx.py b/Tests/test_file_fpx.py new file mode 100644 index 000000000..01b285efe --- /dev/null +++ b/Tests/test_file_fpx.py @@ -0,0 +1,23 @@ +from helper import unittest, PillowTestCase + +from PIL import FpxImagePlugin + + +class TestFileFpx(PillowTestCase): + + def test_invalid_file(self): + # Test an invalid OLE file + invalid_file = "Tests/images/flower.jpg" + self.assertRaises(SyntaxError, + lambda: FpxImagePlugin.FpxImageFile(invalid_file)) + + # Test a valid OLE file, but not an FPX file + ole_file = "Tests/images/test-ole-file.doc" + self.assertRaises(SyntaxError, + lambda: FpxImagePlugin.FpxImageFile(ole_file)) + + +if __name__ == '__main__': + unittest.main() + +# End of file diff --git a/Tests/test_file_ftex.py b/Tests/test_file_ftex.py new file mode 100644 index 000000000..97bf08747 --- /dev/null +++ b/Tests/test_file_ftex.py @@ -0,0 +1,16 @@ +from helper import PillowTestCase +from PIL import Image + + +class TestFileFtex(PillowTestCase): + + def test_load_raw(self): + im = Image.open('Tests/images/ftex_uncompressed.ftu') + target = Image.open('Tests/images/ftex_uncompressed.png') + + self.assert_image_equal(im, target) + + def test_load_dxt1(self): + im = Image.open('Tests/images/ftex_dxt1.ftc') + target = Image.open('Tests/images/ftex_dxt1.png') + self.assert_image_similar(im, target, 15) diff --git a/Tests/test_file_gbr.py b/Tests/test_file_gbr.py new file mode 100644 index 000000000..479f5b83f --- /dev/null +++ b/Tests/test_file_gbr.py @@ -0,0 +1,24 @@ +from helper import unittest, PillowTestCase + +from PIL import Image, GbrImagePlugin + + +class TestFileGbr(PillowTestCase): + + def test_invalid_file(self): + invalid_file = "Tests/images/flower.jpg" + + self.assertRaises(SyntaxError, + lambda: GbrImagePlugin.GbrImageFile(invalid_file)) + + def test_gbr_file(self): + im = Image.open('Tests/images/gbr.gbr') + + target = Image.open('Tests/images/gbr.png') + + self.assert_image_equal(target, im) + +if __name__ == '__main__': + unittest.main() + +# End of file diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index 0e9e65a18..0ac67cd63 100644 --- a/Tests/test_file_gif.py +++ b/Tests/test_file_gif.py @@ -24,6 +24,13 @@ class TestFileGif(PillowTestCase): self.assertEqual(im.mode, "P") self.assertEqual(im.size, (128, 128)) self.assertEqual(im.format, "GIF") + self.assertEqual(im.info["version"], b"GIF89a") + + def test_invalid_file(self): + invalid_file = "Tests/images/flower.jpg" + + self.assertRaises(SyntaxError, + lambda: GifImagePlugin.GifImageFile(invalid_file)) def test_optimize(self): from io import BytesIO @@ -71,6 +78,39 @@ class TestFileGif(PillowTestCase): self.assert_image_similar(reread.convert('RGB'), hopper(), 50) + def test_roundtrip_save_all(self): + # Single frame image + out = self.tempfile('temp.gif') + im = hopper() + im.save(out, save_all=True) + reread = Image.open(out) + + self.assert_image_similar(reread.convert('RGB'), im, 50) + + # Multiframe image + im = Image.open("Tests/images/dispose_bgnd.gif") + + out = self.tempfile('temp.gif') + im.save(out, save_all=True) + reread = Image.open(out) + + self.assertEqual(reread.n_frames, 5) + + def test_headers_saving_for_animated_gifs(self): + important_headers = ['background', 'version', 'duration', 'loop'] + # Multiframe image + im = Image.open("Tests/images/dispose_bgnd.gif") + + out = self.tempfile('temp.gif') + im.save(out, save_all=True) + reread = Image.open(out) + + for header in important_headers: + self.assertEqual( + im.info[header], + reread.info[header] + ) + def test_palette_handling(self): # see https://github.com/python-pillow/Pillow/issues/513 @@ -92,20 +132,20 @@ class TestFileGif(PillowTestCase): def roundtrip(im, *args, **kwargs): out = self.tempfile('temp.gif') - im.save(out, *args, **kwargs) + im.copy().save(out, *args, **kwargs) reloaded = Image.open(out) - return [im, reloaded] + return reloaded orig = "Tests/images/test.colors.gif" im = Image.open(orig) - self.assert_image_equal(*roundtrip(im)) - self.assert_image_equal(*roundtrip(im, optimize=True)) + self.assert_image_similar(im, roundtrip(im), 1) + self.assert_image_similar(im, roundtrip(im, optimize=True), 1) im = im.convert("RGB") # check automatic P conversion - reloaded = roundtrip(im)[1].convert('RGB') + reloaded = roundtrip(im).convert('RGB') self.assert_image_equal(im, reloaded) @unittest.skipUnless(netpbm_available(), "netpbm not available") @@ -135,8 +175,25 @@ class TestFileGif(PillowTestCase): self.assertEqual(framecount, 5) def test_n_frames(self): + im = Image.open(TEST_GIF) + self.assertEqual(im.n_frames, 1) + self.assertFalse(im.is_animated) + im = Image.open("Tests/images/iss634.gif") - self.assertEqual(im.n_frames, 43) + self.assertEqual(im.n_frames, 42) + self.assertTrue(im.is_animated) + + def test_eoferror(self): + im = Image.open(TEST_GIF) + + n_frames = im.n_frames + while True: + n_frames -= 1 + try: + im.seek(n_frames) + break + except EOFError: + self.assertTrue(im.tell() < n_frames) def test_dispose_none(self): img = Image.open("Tests/images/dispose_none.gif") @@ -201,6 +258,43 @@ class TestFileGif(PillowTestCase): self.assertEqual(reread.info['loop'], number_of_loops) + def test_background(self): + out = self.tempfile('temp.gif') + im = Image.new('L', (100, 100), '#000') + im.info['background'] = 1 + im.save(out) + reread = Image.open(out) + + self.assertEqual(reread.info['background'], im.info['background']) + + def test_version(self): + out = self.tempfile('temp.gif') + + # Test that GIF87a is used by default + im = Image.new('L', (100, 100), '#000') + im.save(out) + reread = Image.open(out) + self.assertEqual(reread.info["version"], b"GIF87a") + + # Test that adding a GIF89a feature changes the version + im.info["transparency"] = 1 + im.save(out) + reread = Image.open(out) + self.assertEqual(reread.info["version"], b"GIF89a") + + # Test that a GIF87a image is also saved in that format + im = Image.open(TEST_GIF) + im.save(out) + reread = Image.open(out) + self.assertEqual(reread.info["version"], b"GIF87a") + + # Test that a GIF89a image is also saved in that format + im.info["version"] = "GIF89a" + im.save(out) + reread = Image.open(out) + self.assertEqual(reread.info["version"], b"GIF87a") + + if __name__ == '__main__': unittest.main() diff --git a/Tests/test_file_gimpgradient.py b/Tests/test_file_gimpgradient.py index c54dca7c1..1653fb304 100644 --- a/Tests/test_file_gimpgradient.py +++ b/Tests/test_file_gimpgradient.py @@ -80,7 +80,7 @@ class TestImage(PillowTestCase): ret = GimpGradientFile.sphere_increasing(middle, pos) # Assert - self.assert_almost_equal(ret, 0.9682458365518543) + self.assertAlmostEqual(ret, 0.9682458365518543) def test_sphere_decreasing(self): # Arrange diff --git a/Tests/test_file_gimppalette.py b/Tests/test_file_gimppalette.py new file mode 100644 index 000000000..dfa2845ac --- /dev/null +++ b/Tests/test_file_gimppalette.py @@ -0,0 +1,36 @@ +from helper import unittest, PillowTestCase + +from PIL.GimpPaletteFile import GimpPaletteFile + + +class TestImage(PillowTestCase): + + def test_sanity(self): + with open('Tests/images/test.gpl', 'rb') as fp: + GimpPaletteFile(fp) + + with open('Tests/images/hopper.jpg', 'rb') as fp: + self.assertRaises(SyntaxError, lambda: GimpPaletteFile(fp)) + + with open('Tests/images/bad_palette_file.gpl', 'rb') as fp: + self.assertRaises(SyntaxError, lambda: GimpPaletteFile(fp)) + + with open('Tests/images/bad_palette_entry.gpl', 'rb') as fp: + self.assertRaises(ValueError, lambda: GimpPaletteFile(fp)) + + def test_get_palette(self): + # Arrange + with open('Tests/images/custom_gimp_palette.gpl', 'rb') as fp: + palette_file = GimpPaletteFile(fp) + + # Act + palette, mode = palette_file.getpalette() + + # Assert + self.assertEqual(mode, "RGB") + + +if __name__ == '__main__': + unittest.main() + +# End of file diff --git a/Tests/test_file_gribstub.py b/Tests/test_file_gribstub.py new file mode 100644 index 000000000..dd4ee84ff --- /dev/null +++ b/Tests/test_file_gribstub.py @@ -0,0 +1,19 @@ +from helper import unittest, PillowTestCase + +from PIL import GribStubImagePlugin + + +class TestFileGribStub(PillowTestCase): + + def test_invalid_file(self): + invalid_file = "Tests/images/flower.jpg" + + self.assertRaises(SyntaxError, + lambda: + GribStubImagePlugin.GribStubImageFile(invalid_file)) + + +if __name__ == '__main__': + unittest.main() + +# End of file diff --git a/Tests/test_file_hdf5stub.py b/Tests/test_file_hdf5stub.py new file mode 100644 index 000000000..485931b36 --- /dev/null +++ b/Tests/test_file_hdf5stub.py @@ -0,0 +1,19 @@ +from helper import unittest, PillowTestCase + +from PIL import Hdf5StubImagePlugin + + +class TestFileHdf5Stub(PillowTestCase): + + def test_invalid_file(self): + test_file = "Tests/images/flower.jpg" + + self.assertRaises(SyntaxError, + lambda: + Hdf5StubImagePlugin.HDF5StubImageFile(test_file)) + + +if __name__ == '__main__': + unittest.main() + +# End of file diff --git a/Tests/test_file_ico.py b/Tests/test_file_ico.py index f7b52b124..70e00c083 100644 --- a/Tests/test_file_ico.py +++ b/Tests/test_file_ico.py @@ -1,7 +1,7 @@ from helper import unittest, PillowTestCase, hopper import io -from PIL import Image +from PIL import Image, IcoImagePlugin # sample ppm stream TEST_ICO_FILE = "Tests/images/hopper.ico" @@ -17,6 +17,11 @@ class TestFileIco(PillowTestCase): self.assertEqual(im.size, (16, 16)) self.assertEqual(im.format, "ICO") + def test_invalid_file(self): + with open("Tests/images/flower.jpg", "rb") as fp: + self.assertRaises(SyntaxError, + lambda: IcoImagePlugin.IcoImageFile(fp)) + def test_save_to_bytes(self): output = io.BytesIO() im = hopper() @@ -30,7 +35,8 @@ class TestFileIco(PillowTestCase): self.assertEqual(im.mode, reloaded.mode) self.assertEqual((64, 64), reloaded.size) self.assertEqual(reloaded.format, "ICO") - self.assert_image_equal(reloaded, hopper().resize((64, 64), Image.LANCZOS)) + self.assert_image_equal(reloaded, + hopper().resize((64, 64), Image.LANCZOS)) # the other one output.seek(0) @@ -40,7 +46,8 @@ class TestFileIco(PillowTestCase): self.assertEqual(im.mode, reloaded.mode) self.assertEqual((32, 32), reloaded.size) self.assertEqual(reloaded.format, "ICO") - self.assert_image_equal(reloaded, hopper().resize((32, 32), Image.LANCZOS)) + self.assert_image_equal(reloaded, + hopper().resize((32, 32), Image.LANCZOS)) if __name__ == '__main__': diff --git a/Tests/test_file_im.py b/Tests/test_file_im.py index 24e00b2f0..e3d92d1d5 100644 --- a/Tests/test_file_im.py +++ b/Tests/test_file_im.py @@ -1,6 +1,6 @@ from helper import unittest, PillowTestCase, hopper -from PIL import Image +from PIL import Image, ImImagePlugin # sample im TEST_IM = "Tests/images/hopper.im" @@ -18,6 +18,19 @@ class TestFileIm(PillowTestCase): def test_n_frames(self): im = Image.open(TEST_IM) self.assertEqual(im.n_frames, 1) + self.assertFalse(im.is_animated) + + def test_eoferror(self): + im = Image.open(TEST_IM) + + n_frames = im.n_frames + while True: + n_frames -= 1 + try: + im.seek(n_frames) + break + except EOFError: + self.assertTrue(im.tell() < n_frames) def test_roundtrip(self): out = self.tempfile('temp.im') @@ -27,6 +40,13 @@ class TestFileIm(PillowTestCase): self.assert_image_equal(reread, im) + def test_invalid_file(self): + invalid_file = "Tests/images/flower.jpg" + + self.assertRaises(SyntaxError, + lambda: ImImagePlugin.ImImageFile(invalid_file)) + + if __name__ == '__main__': unittest.main() diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index d4929dd58..7b60f60b1 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -166,6 +166,65 @@ class TestFileJpeg(PillowTestCase): im = hopper() im.save(f, 'JPEG', quality=90, exif=b"1"*65532) + def test_exif_typeerror(self): + im = Image.open('Tests/images/exif_typeerror.jpg') + # Should not raise a TypeError + im._getexif() + + def test_exif_gps(self): + # Arrange + im = Image.open('Tests/images/exif_gps.jpg') + gps_index = 34853 + expected_exif_gps = { + 0: b'\x00\x00\x00\x01', + 2: (4294967295, 1), + 5: b'\x01', + 30: 65535, + 29: '1999:99:99 99:99:99'} + + # Act + exif = im._getexif() + + # Assert + self.assertEqual(exif[gps_index], expected_exif_gps) + + def test_exif_rollback(self): + # rolling back exif support in 3.1 to pre-3.0 formatting. + # expected from 2.9, with b/u qualifiers switched for 3.2 compatibility + # this test passes on 2.9 and 3.1, but not 3.0 + expected_exif = {34867: 4294967295, + 258: (24, 24, 24), + 36867: '2099:09:29 10:10:10', + 34853: {0: b'\x00\x00\x00\x01', + 2: (4294967295, 1), + 5: b'\x01', + 30: 65535, + 29: '1999:99:99 99:99:99'}, + 296: 65535, + 34665: 185, + 41994: 65535, + 514: 4294967295, + 271: 'Make', + 272: 'XXX-XXX', + 305: 'PIL', + 42034: ((1, 1), (1, 1), (1, 1), (1, 1)), + 42035: 'LensMake', + 34856: b'\xaa\xaa\xaa\xaa\xaa\xaa', + 282: (4294967295, 1), + 33434: (4294967295, 1)} + + im = Image.open('Tests/images/exif_gps.jpg') + exif = im._getexif() + + for tag, value in expected_exif.items(): + self.assertEqual(value, exif[tag]) + + def test_exif_gps_typeerror(self): + im = Image.open('Tests/images/exif_gps_typeerror.jpg') + + # Should not raise a TypeError + im._getexif() + def test_progressive_compat(self): im1 = self.roundtrip(hopper()) im2 = self.roundtrip(hopper(), progressive=1) @@ -291,22 +350,24 @@ class TestFileJpeg(PillowTestCase): # dict of qtable lists self.assert_image_similar(im, - self.roundtrip(im, - qtables={0: standard_l_qtable, - 1: standard_chrominance_qtable}), - 30) + self.roundtrip(im, qtables={ + 0: standard_l_qtable, + 1: standard_chrominance_qtable + }), 30) # not a sequence self.assertRaises(Exception, lambda: self.roundtrip(im, qtables='a')) # sequence wrong length self.assertRaises(Exception, lambda: self.roundtrip(im, qtables=[])) # sequence wrong length - self.assertRaises(Exception, lambda: self.roundtrip(im, qtables=[1, 2, 3, 4, 5])) + self.assertRaises(Exception, + lambda: self.roundtrip(im, qtables=[1, 2, 3, 4, 5])) # qtable entry not a sequence self.assertRaises(Exception, lambda: self.roundtrip(im, qtables=[1])) # qtable entry has wrong number of items - self.assertRaises(Exception, lambda: self.roundtrip(im, qtables=[[1, 2, 3, 4]])) + self.assertRaises(Exception, + lambda: self.roundtrip(im, qtables=[[1, 2, 3, 4]])) @unittest.skipUnless(djpeg_available(), "djpeg not available") def test_load_djpeg(self): @@ -337,7 +398,8 @@ class TestFileJpeg(PillowTestCase): """ Generates a very hard to compress file :param size: tuple """ - return Image.frombytes('RGB', size, os.urandom(size[0]*size[1] * 3)) + return Image.frombytes('RGB', + size, os.urandom(size[0]*size[1] * 3)) im = gen_random_image((512, 512)) f = self.tempfile("temp.jpeg") @@ -350,6 +412,18 @@ class TestFileJpeg(PillowTestCase): reloaded.save(f, quality='keep', progressive=True) reloaded.save(f, quality='keep', optimize=True) + def test_bad_mpo_header(self): + """ Treat unknown MPO as JPEG """ + # Arrange + + # Act + # Shouldn't raise error + fn = "Tests/images/sugarshack_bad_mpo_header.jpg" + im = self.assert_warning(UserWarning, lambda: Image.open(fn)) + + # Assert + self.assertEqual(im.format, "JPEG") + if __name__ == '__main__': unittest.main() diff --git a/Tests/test_file_jpeg2k.py b/Tests/test_file_jpeg2k.py index 9768a881d..7e23e091a 100644 --- a/Tests/test_file_jpeg2k.py +++ b/Tests/test_file_jpeg2k.py @@ -1,6 +1,6 @@ from helper import unittest, PillowTestCase -from PIL import Image +from PIL import Image, Jpeg2KImagePlugin from io import BytesIO codecs = dir(Image.core) @@ -39,6 +39,13 @@ class TestFileJpeg2k(PillowTestCase): self.assertEqual(im.size, (640, 480)) self.assertEqual(im.format, 'JPEG2000') + def test_invalid_file(self): + invalid_file = "Tests/images/flower.jpg" + + self.assertRaises(SyntaxError, + lambda: + Jpeg2KImagePlugin.Jpeg2KImageFile(invalid_file)) + def test_bytesio(self): with open('Tests/images/test-card-lossless.jp2', 'rb') as f: data = BytesIO(f.read()) @@ -163,6 +170,18 @@ class TestFileJpeg2k(PillowTestCase): im = self.roundtrip(jp2) self.assert_image_equal(im, jp2) + def test_unbound_local(self): + # prepatch, a malformed jp2 file could cause an UnboundLocalError + # exception. + try: + jp2 = Image.open('Tests/images/unbound_variable.jp2') + self.assertTrue(False, 'Expecting an exception') + except SyntaxError as err: + self.assertTrue(True, 'Expecting a syntax error') + except IOError as err: + self.assertTrue(True, 'Expecting an IO error') + except UnboundLocalError as err: + self.assertTrue(False, "Prepatch error") if __name__ == '__main__': unittest.main() diff --git a/Tests/test_file_libtiff.py b/Tests/test_file_libtiff.py index 896038b9d..83d347239 100644 --- a/Tests/test_file_libtiff.py +++ b/Tests/test_file_libtiff.py @@ -1,10 +1,15 @@ from __future__ import print_function from helper import unittest, PillowTestCase, hopper, py3 -import os +from ctypes import c_float import io +import logging +import itertools +import os -from PIL import Image, TiffImagePlugin +from PIL import Image, TiffImagePlugin, TiffTags + +logger = logging.getLogger(__name__) class LibTiffTestCase(PillowTestCase): @@ -120,43 +125,103 @@ class TestFileLibTiff(LibTiffTestCase): def test_write_metadata(self): """ Test metadata writing through libtiff """ - img = Image.open('Tests/images/hopper_g4.tif') - f = self.tempfile('temp.tiff') + for legacy_api in [False, True]: + img = Image.open('Tests/images/hopper_g4.tif') + f = self.tempfile('temp.tiff') - img.save(f, tiffinfo=img.tag) + img.save(f, tiffinfo=img.tag) - loaded = Image.open(f) + if legacy_api: + original = img.tag.named() + else: + original = img.tag_v2.named() - original = img.tag.named() - reloaded = loaded.tag.named() + # PhotometricInterpretation is set from SAVE_INFO, + # not the original image. + ignored = ['StripByteCounts', 'RowsPerStrip', 'PageNumber', + 'PhotometricInterpretation'] - # PhotometricInterpretation is set from SAVE_INFO, - # not the original image. - ignored = [ - 'StripByteCounts', 'RowsPerStrip', - 'PageNumber', 'PhotometricInterpretation'] + loaded = Image.open(f) + if legacy_api: + reloaded = loaded.tag.named() + else: + reloaded = loaded.tag_v2.named() - for tag, value in reloaded.items(): - if tag not in ignored: - if tag.endswith('Resolution'): + for tag, value in itertools.chain(reloaded.items(), + original.items()): + if tag not in ignored: val = original[tag] - self.assert_almost_equal( - val[0][0]/val[0][1], value[0][0]/value[0][1], - msg="%s didn't roundtrip" % tag) - else: - self.assertEqual( - original[tag], value, "%s didn't roundtrip" % tag) + if tag.endswith('Resolution'): + if legacy_api: + self.assertEqual( + c_float(val[0][0] / val[0][1]).value, + c_float(value[0][0] / value[0][1]).value, + msg="%s didn't roundtrip" % tag) + else: + self.assertEqual( + c_float(val).value, c_float(value).value, + msg="%s didn't roundtrip" % tag) + else: + self.assertEqual( + val, value, msg="%s didn't roundtrip" % tag) - for tag, value in original.items(): - if tag not in ignored: - if tag.endswith('Resolution'): - val = reloaded[tag] - self.assert_almost_equal( - val[0][0]/val[0][1], value[0][0]/value[0][1], - msg="%s didn't roundtrip" % tag) - else: - self.assertEqual( - value, reloaded[tag], "%s didn't roundtrip" % tag) + # https://github.com/python-pillow/Pillow/issues/1561 + requested_fields = ['StripByteCounts', + 'RowsPerStrip', + 'StripOffsets'] + for field in requested_fields: + self.assertTrue(field in reloaded, "%s not in metadata" % field) + + def test_additional_metadata(self): + # these should not crash. Seriously dummy data, most of it doesn't make + # any sense, so we're running up against limits where we're asking + # libtiff to do stupid things. + + # Get the list of the ones that we should be able to write + + core_items = dict((tag, info) for tag, info in [(s, TiffTags.lookup(s)) for s + in TiffTags.LIBTIFF_CORE] + if info.type is not None) + + # Exclude ones that have special meaning that we're already testing them + im = Image.open('Tests/images/hopper_g4.tif') + for tag in im.tag_v2.keys(): + try: + del(core_items[tag]) + except: + pass + + # Type codes: + # 2: "ascii", + # 3: "short", + # 4: "long", + # 5: "rational", + # 12: "double", + # type: dummy value + values = {2: 'test', + 3: 1, + 4: 2**20, + 5: TiffImagePlugin.IFDRational(100, 1), + 12: 1.05} + + new_ifd = TiffImagePlugin.ImageFileDirectory_v2() + for tag, info in core_items.items(): + if info.length == 1: + new_ifd[tag] = values[info.type] + if info.length == 0: + new_ifd[tag] = tuple(values[info.type] for _ in range(3)) + else: + new_ifd[tag] = tuple(values[info.type] for _ in range(info.length)) + + # Extra samples really doesn't make sense in this application. + del(new_ifd[338]) + + out = self.tempfile("temp.tif") + TiffImagePlugin.WRITE_LIBTIFF = True + + im.save(out, tiffinfo=new_ifd) + + TiffImagePlugin.WRITE_LIBTIFF = False def test_g3_compression(self): i = Image.open('Tests/images/hopper_g4_500.tif') @@ -225,13 +290,13 @@ class TestFileLibTiff(LibTiffTestCase): orig.save(out) reread = Image.open(out) - self.assertEqual('temp.tif', reread.tag[269]) + self.assertEqual('temp.tif', reread.tag_v2[269]) + self.assertEqual('temp.tif', reread.tag[269][0]) def test_12bit_rawmode(self): """ Are we generating the same interpretation of the image as Imagemagick is? """ TiffImagePlugin.READ_LIBTIFF = True - # Image.DEBUG = True im = Image.open('Tests/images/12bit.cropped.tif') im.load() TiffImagePlugin.READ_LIBTIFF = False @@ -243,14 +308,8 @@ class TestFileLibTiff(LibTiffTestCase): im2 = Image.open('Tests/images/12in16bit.tif') - if Image.DEBUG: - print(im.getpixel((0, 0))) - print(im.getpixel((0, 1))) - print(im.getpixel((0, 2))) - - print(im2.getpixel((0, 0))) - print(im2.getpixel((0, 1))) - print(im2.getpixel((0, 2))) + logger.debug("%s", [img.getpixel((0, idx)) + for img in [im, im2] for idx in range(3)]) self.assert_image_equal(im, im2) @@ -360,6 +419,39 @@ class TestFileLibTiff(LibTiffTestCase): self.assertEqual(im.mode, "L") self.assert_image_similar(im, original, 7.3) + def test_gray_semibyte_per_pixel(self): + test_files = ( + ( + 24.8,#epsilon + (#group + "Tests/images/tiff_gray_2_4_bpp/hopper2.tif", + "Tests/images/tiff_gray_2_4_bpp/hopper2I.tif", + "Tests/images/tiff_gray_2_4_bpp/hopper2R.tif", + "Tests/images/tiff_gray_2_4_bpp/hopper2IR.tif", + ) + ), + ( + 7.3,#epsilon + (#group + "Tests/images/tiff_gray_2_4_bpp/hopper4.tif", + "Tests/images/tiff_gray_2_4_bpp/hopper4I.tif", + "Tests/images/tiff_gray_2_4_bpp/hopper4R.tif", + "Tests/images/tiff_gray_2_4_bpp/hopper4IR.tif", + ) + ), + ) + original = hopper("L") + for epsilon, group in test_files: + im = Image.open(group[0]) + self.assertEqual(im.size, (128, 128)) + self.assertEqual(im.mode, "L") + self.assert_image_similar(im, original, epsilon) + for file in group[1:]: + im2 = Image.open(file) + self.assertEqual(im2.size, (128, 128)) + self.assertEqual(im2.mode, "L") + self.assert_image_equal(im, im2) + def test_save_bytesio(self): # PR 1011 # Test TIFF saving to io.BytesIO() object. @@ -387,6 +479,16 @@ class TestFileLibTiff(LibTiffTestCase): TiffImagePlugin.WRITE_LIBTIFF = False TiffImagePlugin.READ_LIBTIFF = False + def test_crashing_metadata(self): + # issue 1597 + im = Image.open('Tests/images/rdf.tif') + out = self.tempfile('temp.tif') + + TiffImagePlugin.WRITE_LIBTIFF = True + # this shouldn't crash + im.save(out, format='TIFF') + TiffImagePlugin.WRITE_LIBTIFF = False + if __name__ == '__main__': unittest.main() diff --git a/Tests/test_file_mcidas.py b/Tests/test_file_mcidas.py new file mode 100644 index 000000000..d547011e2 --- /dev/null +++ b/Tests/test_file_mcidas.py @@ -0,0 +1,19 @@ +from helper import unittest, PillowTestCase + +from PIL import McIdasImagePlugin + + +class TestFileMcIdas(PillowTestCase): + + def test_invalid_file(self): + invalid_file = "Tests/images/flower.jpg" + + self.assertRaises(SyntaxError, + lambda: + McIdasImagePlugin.McIdasImageFile(invalid_file)) + + +if __name__ == '__main__': + unittest.main() + +# End of file diff --git a/Tests/test_file_mic.py b/Tests/test_file_mic.py new file mode 100644 index 000000000..f0a2752a9 --- /dev/null +++ b/Tests/test_file_mic.py @@ -0,0 +1,23 @@ +from helper import unittest, PillowTestCase + +from PIL import MicImagePlugin + + +class TestFileMic(PillowTestCase): + + def test_invalid_file(self): + # Test an invalid OLE file + invalid_file = "Tests/images/flower.jpg" + self.assertRaises(SyntaxError, + lambda: MicImagePlugin.MicImageFile(invalid_file)) + + # Test a valid OLE file, but not a MIC file + ole_file = "Tests/images/test-ole-file.doc" + self.assertRaises(SyntaxError, + lambda: MicImagePlugin.MicImageFile(ole_file)) + + +if __name__ == '__main__': + unittest.main() + +# End of file diff --git a/Tests/test_file_mpo.py b/Tests/test_file_mpo.py index 1a0ebc453..b1d3b7413 100644 --- a/Tests/test_file_mpo.py +++ b/Tests/test_file_mpo.py @@ -98,6 +98,19 @@ class TestFileMpo(PillowTestCase): def test_n_frames(self): im = Image.open("Tests/images/sugarshack.mpo") self.assertEqual(im.n_frames, 2) + self.assertTrue(im.is_animated) + + def test_eoferror(self): + im = Image.open("Tests/images/sugarshack.mpo") + + n_frames = im.n_frames + while True: + n_frames -= 1 + try: + im.seek(n_frames) + break + except EOFError: + self.assertTrue(im.tell() < n_frames) def test_image_grab(self): for test_file in test_files: diff --git a/Tests/test_file_msp.py b/Tests/test_file_msp.py index f4b1af75e..3dbca6e60 100644 --- a/Tests/test_file_msp.py +++ b/Tests/test_file_msp.py @@ -1,6 +1,6 @@ from helper import unittest, PillowTestCase, hopper -from PIL import Image +from PIL import Image, MspImagePlugin TEST_FILE = "Tests/images/hopper.msp" @@ -18,6 +18,12 @@ class TestFileMsp(PillowTestCase): self.assertEqual(im.size, (128, 128)) self.assertEqual(im.format, "MSP") + def test_invalid_file(self): + invalid_file = "Tests/images/flower.jpg" + + self.assertRaises(SyntaxError, + lambda: MspImagePlugin.MspImageFile(invalid_file)) + def test_open(self): # Arrange # Act diff --git a/Tests/test_file_pcd.py b/Tests/test_file_pcd.py new file mode 100644 index 000000000..06fd33043 --- /dev/null +++ b/Tests/test_file_pcd.py @@ -0,0 +1,22 @@ +from helper import unittest, PillowTestCase +from PIL import Image + + +class TestFilePcd(PillowTestCase): + + def test_load_raw(self): + im = Image.open('Tests/images/hopper.pcd') + im.load() # should not segfault. + + # Note that this image was created with a resized hopper + # image, which was then converted to pcd with imagemagick + # and the colors are wonky in Pillow. It's unclear if this + # is a pillow or a convert issue, as other images not generated + # from convert look find on pillow and not imagemagick. + + # target = hopper().resize((768,512)) + # self.assert_image_similar(im, target, 10) + + +if __name__ == '__main__': + unittest.main() diff --git a/Tests/test_file_pcx.py b/Tests/test_file_pcx.py index 10d17d349..f6342bed9 100644 --- a/Tests/test_file_pcx.py +++ b/Tests/test_file_pcx.py @@ -1,6 +1,6 @@ from helper import unittest, PillowTestCase, hopper -from PIL import Image +from PIL import Image, PcxImagePlugin class TestFilePcx(PillowTestCase): @@ -19,6 +19,12 @@ class TestFilePcx(PillowTestCase): for mode in ('1', 'L', 'P', 'RGB'): self._roundtrip(hopper(mode)) + def test_invalid_file(self): + invalid_file = "Tests/images/flower.jpg" + + self.assertRaises(SyntaxError, + lambda: PcxImagePlugin.PcxImageFile(invalid_file)) + def test_odd(self): # see issue #523, odd sized images should have a stride that's even. # not that imagemagick or gimp write pcx that way. diff --git a/Tests/test_file_pdf.py b/Tests/test_file_pdf.py index 9424bc09d..e3d41ffed 100644 --- a/Tests/test_file_pdf.py +++ b/Tests/test_file_pdf.py @@ -1,17 +1,20 @@ from helper import unittest, PillowTestCase, hopper - +from PIL import Image import os.path class TestFilePdf(PillowTestCase): - def helper_save_as_pdf(self, mode): + def helper_save_as_pdf(self, mode, save_all=False): # Arrange im = hopper(mode) outfile = self.tempfile("temp_" + mode + ".pdf") # Act - im.save(outfile) + if save_all: + im.save(outfile, save_all=True) + else: + im.save(outfile) # Assert self.assertTrue(os.path.isfile(outfile)) @@ -52,6 +55,25 @@ class TestFilePdf(PillowTestCase): # Act / Assert self.helper_save_as_pdf(mode) + def test_unsupported_mode(self): + im = hopper("LA") + outfile = self.tempfile("temp_LA.pdf") + + self.assertRaises(ValueError, lambda: im.save(outfile)) + + def test_save_all(self): + # Single frame image + self.helper_save_as_pdf("RGB", save_all=True) + + # Multiframe image + im = Image.open("Tests/images/dispose_bgnd.gif") + + outfile = self.tempfile('temp.pdf') + im.save(outfile, save_all=True) + + self.assertTrue(os.path.isfile(outfile)) + self.assertGreater(os.path.getsize(outfile), 0) + if __name__ == '__main__': unittest.main() diff --git a/Tests/test_file_png.py b/Tests/test_file_png.py index f438e24cc..cb72b2d73 100644 --- a/Tests/test_file_png.py +++ b/Tests/test_file_png.py @@ -81,6 +81,12 @@ class TestFilePng(PillowTestCase): hopper("I").save(test_file) im = Image.open(test_file) + def test_invalid_file(self): + invalid_file = "Tests/images/flower.jpg" + + self.assertRaises(SyntaxError, + lambda: PngImagePlugin.PngImageFile(invalid_file)) + def test_broken(self): # Check reading of totally broken files. In this case, the test # file was checked into Subversion as a text file. diff --git a/Tests/test_file_ppm.py b/Tests/test_file_ppm.py index 80c2e60da..cda6ec164 100644 --- a/Tests/test_file_ppm.py +++ b/Tests/test_file_ppm.py @@ -35,6 +35,14 @@ class TestFilePpm(PillowTestCase): reloaded = Image.open(f) self.assert_image_equal(im, reloaded) + def test_truncated_file(self): + path = self.tempfile('temp.pgm') + f = open(path, 'w') + f.write('P6') + f.close() + + self.assertRaises(ValueError, lambda: Image.open(path)) + if __name__ == '__main__': unittest.main() diff --git a/Tests/test_file_psd.py b/Tests/test_file_psd.py index dca3601b2..4890839f1 100644 --- a/Tests/test_file_psd.py +++ b/Tests/test_file_psd.py @@ -1,6 +1,6 @@ from helper import unittest, PillowTestCase -from PIL import Image +from PIL import Image, PsdImagePlugin # sample ppm stream test_file = "Tests/images/hopper.psd" @@ -16,12 +16,33 @@ class TestImagePsd(PillowTestCase): self.assertEqual(im.size, (128, 128)) self.assertEqual(im.format, "PSD") + def test_invalid_file(self): + invalid_file = "Tests/images/flower.jpg" + + self.assertRaises(SyntaxError, + lambda: PsdImagePlugin.PsdImageFile(invalid_file)) + def test_n_frames(self): im = Image.open("Tests/images/hopper_merged.psd") self.assertEqual(im.n_frames, 1) + self.assertFalse(im.is_animated) im = Image.open(test_file) self.assertEqual(im.n_frames, 2) + self.assertTrue(im.is_animated) + + def test_eoferror(self): + im = Image.open(test_file) + + n_frames = im.n_frames + while True: + n_frames -= 1 + try: + # PSD seek index starts at 1 rather than 0 + im.seek(n_frames+1) + break + except EOFError: + self.assertTrue(im.tell() < n_frames) if __name__ == '__main__': diff --git a/Tests/test_file_sgi.py b/Tests/test_file_sgi.py index d49086c51..e78488913 100644 --- a/Tests/test_file_sgi.py +++ b/Tests/test_file_sgi.py @@ -1,6 +1,6 @@ from helper import unittest, PillowTestCase -from PIL import Image +from PIL import Image, SgiImagePlugin class TestFileSgi(PillowTestCase): @@ -32,6 +32,13 @@ class TestFileSgi(PillowTestCase): # Act / Assert self.assertRaises(ValueError, lambda: Image.open(test_file)) + def test_invalid_file(self): + invalid_file = "Tests/images/flower.jpg" + + self.assertRaises(ValueError, + lambda: + SgiImagePlugin.SgiImageFile(invalid_file)) + if __name__ == '__main__': unittest.main() diff --git a/Tests/test_file_spider.py b/Tests/test_file_spider.py index 7d24b2fe5..53cf3915b 100644 --- a/Tests/test_file_spider.py +++ b/Tests/test_file_spider.py @@ -45,6 +45,7 @@ class TestImageSpider(PillowTestCase): def test_n_frames(self): im = Image.open(TEST_FILE) self.assertEqual(im.n_frames, 1) + self.assertFalse(im.is_animated) def test_loadImageSeries(self): # Arrange @@ -79,6 +80,11 @@ class TestImageSpider(PillowTestCase): # Assert self.assertEqual(ret, 0) + def test_invalid_file(self): + invalid_file = "Tests/images/invalid.spider" + + self.assertRaises(IOError, lambda: Image.open(invalid_file)) + if __name__ == '__main__': unittest.main() diff --git a/Tests/test_file_sun.py b/Tests/test_file_sun.py index 332104062..16c43b921 100644 --- a/Tests/test_file_sun.py +++ b/Tests/test_file_sun.py @@ -1,6 +1,6 @@ from helper import unittest, PillowTestCase -from PIL import Image +from PIL import Image, SunImagePlugin class TestFileSun(PillowTestCase): @@ -16,6 +16,10 @@ class TestFileSun(PillowTestCase): # Assert self.assertEqual(im.size, (128, 128)) + invalid_file = "Tests/images/flower.jpg" + self.assertRaises(SyntaxError, + lambda: SunImagePlugin.SunImageFile(invalid_file)) + if __name__ == '__main__': unittest.main() diff --git a/Tests/test_file_tga.py b/Tests/test_file_tga.py index ea94dee64..459e766d5 100644 --- a/Tests/test_file_tga.py +++ b/Tests/test_file_tga.py @@ -15,6 +15,25 @@ class TestFileTga(PillowTestCase): # Assert self.assertEqual(im.size, (100, 100)) + def test_save(self): + test_file = "Tests/images/tga_id_field.tga" + im = Image.open(test_file) + + test_file = self.tempfile("temp.tga") + + # Save + im.save(test_file) + test_im = Image.open(test_file) + self.assertEqual(test_im.size, (100, 100)) + + # RGBA save + im.convert("RGBA").save(test_file) + test_im = Image.open(test_file) + self.assertEqual(test_im.size, (100, 100)) + + # Unsupported mode save + self.assertRaises(IOError, lambda: im.convert("LA").save(test_file)) + if __name__ == '__main__': unittest.main() diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py index cf456d609..84fdd0f49 100644 --- a/Tests/test_file_tiff.py +++ b/Tests/test_file_tiff.py @@ -1,9 +1,12 @@ from __future__ import print_function +import logging +import struct + from helper import unittest, PillowTestCase, hopper, py3 from PIL import Image, TiffImagePlugin -import struct +logger = logging.getLogger(__name__) class TestFileTiff(PillowTestCase): @@ -67,24 +70,65 @@ class TestFileTiff(PillowTestCase): ]) im.load() + def test_sampleformat(self): + # https://github.com/python-pillow/Pillow/issues/1466 + im = Image.open("Tests/images/copyleft.tiff") + self.assertEqual(im.mode, 'RGB') + def test_xyres_tiff(self): from PIL.TiffImagePlugin import X_RESOLUTION, Y_RESOLUTION filename = "Tests/images/pil168.tif" im = Image.open(filename) - assert isinstance(im.tag.tags[X_RESOLUTION][0], tuple) - assert isinstance(im.tag.tags[Y_RESOLUTION][0], tuple) - # Try to read a file where X,Y_RESOLUTION are ints - im.tag.tags[X_RESOLUTION] = (72,) - im.tag.tags[Y_RESOLUTION] = (72,) - im._setup() + + # legacy api + self.assertIsInstance(im.tag[X_RESOLUTION][0], tuple) + self.assertIsInstance(im.tag[Y_RESOLUTION][0], tuple) + + # v2 api + self.assertIsInstance(im.tag_v2[X_RESOLUTION], TiffImagePlugin.IFDRational) + self.assertIsInstance(im.tag_v2[Y_RESOLUTION], TiffImagePlugin.IFDRational) + self.assertEqual(im.info['dpi'], (72., 72.)) + def test_int_resolution(self): + from PIL.TiffImagePlugin import X_RESOLUTION, Y_RESOLUTION + filename = "Tests/images/pil168.tif" + im = Image.open(filename) + + # Try to read a file where X,Y_RESOLUTION are ints + im.tag_v2[X_RESOLUTION] = 71 + im.tag_v2[Y_RESOLUTION] = 71 + im._setup() + self.assertEqual(im.info['dpi'], (71., 71.)) + + def test_invalid_file(self): + invalid_file = "Tests/images/flower.jpg" + + self.assertRaises(SyntaxError, + lambda: TiffImagePlugin.TiffImageFile(invalid_file)) + + TiffImagePlugin.PREFIXES.append("\xff\xd8\xff\xe0") + self.assertRaises(SyntaxError, + lambda: TiffImagePlugin.TiffImageFile(invalid_file)) + TiffImagePlugin.PREFIXES.pop() + def test_bad_exif(self): + i = Image.open('Tests/images/hopper_bad_exif.jpg') try: - Image.open('Tests/images/hopper_bad_exif.jpg')._getexif() + self.assert_warning(UserWarning, i._getexif) except struct.error: - self.fail("Bad EXIF data should not pass incorrect values to " - "_binary unpack") + self.fail( + "Bad EXIF data passed incorrect values to _binary unpack") + + def test_save_rgba(self): + im = hopper("RGBA") + outfile = self.tempfile("temp.tif") + im.save(outfile) + + def test_save_unsupported_mode(self): + im = hopper("HSV") + outfile = self.tempfile("temp.tif") + self.assertRaises(IOError, lambda: im.save(outfile)) def test_little_endian(self): im = Image.open('Tests/images/16bit.cropped.tif') @@ -119,7 +163,6 @@ class TestFileTiff(PillowTestCase): """ Are we generating the same interpretation of the image as Imagemagick is? """ - # Image.DEBUG = True im = Image.open('Tests/images/12bit.cropped.tif') # to make the target -- @@ -130,19 +173,13 @@ class TestFileTiff(PillowTestCase): im2 = Image.open('Tests/images/12in16bit.tif') - if Image.DEBUG: - print(im.getpixel((0, 0))) - print(im.getpixel((0, 1))) - print(im.getpixel((0, 2))) - - print(im2.getpixel((0, 0))) - print(im2.getpixel((0, 1))) - print(im2.getpixel((0, 2))) + logger.debug("%s", [img.getpixel((0, idx)) + for img in [im, im2] for idx in range(3)]) self.assert_image_equal(im, im2) def test_32bit_float(self): - # Issue 614, specific 32 bit float format + # Issue 614, specific 32-bit float format path = 'Tests/images/10ct_32bit_128.tiff' im = Image.open(path) im.load() @@ -154,9 +191,23 @@ class TestFileTiff(PillowTestCase): def test_n_frames(self): im = Image.open('Tests/images/multipage-lastframe.tif') self.assertEqual(im.n_frames, 1) + self.assertFalse(im.is_animated) im = Image.open('Tests/images/multipage.tiff') self.assertEqual(im.n_frames, 3) + self.assertTrue(im.is_animated) + + def test_eoferror(self): + im = Image.open('Tests/images/multipage-lastframe.tif') + + n_frames = im.n_frames + while True: + n_frames -= 1 + try: + im.seek(n_frames) + break + except EOFError: + self.assertTrue(im.tell() < n_frames) def test_multipage(self): # issue #862 @@ -184,7 +235,6 @@ class TestFileTiff(PillowTestCase): self.assertEqual(im.convert('RGB').getpixel((0, 0)), (0, 0, 255)) def test___str__(self): - # Arrange filename = "Tests/images/pil136.tiff" im = Image.open(filename) @@ -194,197 +244,198 @@ class TestFileTiff(PillowTestCase): # Assert self.assertIsInstance(ret, str) - def test_as_dict(self): + def test_as_dict_deprecation(self): # Arrange filename = "Tests/images/pil136.tiff" im = Image.open(filename) - # Act - ret = im.ifd.as_dict() + self.assert_warning(DeprecationWarning, lambda: im.tag_v2.as_dict()) + self.assert_warning(DeprecationWarning, lambda: im.tag.as_dict()) - # Assert - self.assertIsInstance(ret, dict) + def test_dict(self): + # Arrange + filename = "Tests/images/pil136.tiff" + im = Image.open(filename) - self.assertEqual( - ret, {256: (55,), 257: (43,), 258: (8, 8, 8, 8), 259: (1,), - 262: (2,), 296: (2,), 273: (8,), 338: (1,), 277: (4,), - 279: (9460,), 282: ((720000, 10000),), - 283: ((720000, 10000),), 284: (1,)}) + # v2 interface + v2_tags = {256: 55, 257: 43, 258: (8, 8, 8, 8), 259: 1, + 262: 2, 296: 2, 273: (8,), 338: (1,), 277: 4, + 279: (9460,), 282: 72.0, 283: 72.0, 284: 1} + self.assertEqual(dict(im.tag_v2), v2_tags) + self.assertEqual(im.tag_v2.as_dict(), v2_tags) + + # legacy interface + legacy_tags = {256: (55,), 257: (43,), 258: (8, 8, 8, 8), 259: (1,), + 262: (2,), 296: (2,), 273: (8,), 338: (1,), 277: (4,), + 279: (9460,), 282: ((720000, 10000),), + 283: ((720000, 10000),), 284: (1,)} + self.assertEqual(dict(im.tag), legacy_tags) + self.assertEqual(im.tag.as_dict(), legacy_tags) def test__delitem__(self): - # Arrange filename = "Tests/images/pil136.tiff" im = Image.open(filename) - len_before = len(im.ifd.as_dict()) - - # Act + len_before = len(dict(im.ifd)) del im.ifd[256] - - # Assert - len_after = len(im.ifd.as_dict()) + len_after = len(dict(im.ifd)) self.assertEqual(len_before, len_after + 1) def test_load_byte(self): - # Arrange - ifd = TiffImagePlugin.ImageFileDirectory() - data = b"abc" - - # Act - ret = ifd.load_byte(data) - - # Assert - self.assertEqual(ret, b"abc") + for legacy_api in [False, True]: + ifd = TiffImagePlugin.ImageFileDirectory_v2() + data = b"abc" + ret = ifd.load_byte(data, legacy_api) + self.assertEqual(ret, b"abc" if legacy_api else (97, 98, 99)) def test_load_string(self): - # Arrange - ifd = TiffImagePlugin.ImageFileDirectory() + ifd = TiffImagePlugin.ImageFileDirectory_v2() data = b"abc\0" - - # Act - ret = ifd.load_string(data) - - # Assert + ret = ifd.load_string(data, False) self.assertEqual(ret, "abc") def test_load_float(self): - # Arrange - ifd = TiffImagePlugin.ImageFileDirectory() + ifd = TiffImagePlugin.ImageFileDirectory_v2() data = b"abcdabcd" - - # Act - ret = ifd.load_float(data) - - # Assert + ret = ifd.load_float(data, False) self.assertEqual(ret, (1.6777999408082104e+22, 1.6777999408082104e+22)) def test_load_double(self): - # Arrange - ifd = TiffImagePlugin.ImageFileDirectory() + ifd = TiffImagePlugin.ImageFileDirectory_v2() data = b"abcdefghabcdefgh" - - # Act - ret = ifd.load_double(data) - - # Assert + ret = ifd.load_double(data, False) self.assertEqual(ret, (8.540883223036124e+194, 8.540883223036124e+194)) def test_seek(self): - # Arrange filename = "Tests/images/pil136.tiff" im = Image.open(filename) - - # Act im.seek(-1) - - # Assert self.assertEqual(im.tell(), 0) def test_seek_eof(self): - # Arrange filename = "Tests/images/pil136.tiff" im = Image.open(filename) self.assertEqual(im.tell(), 0) - - # Act / Assert self.assertRaises(EOFError, lambda: im.seek(1)) - def test__cvt_res_int(self): - # Arrange - from PIL.TiffImagePlugin import _cvt_res + def test__limit_rational_int(self): + from PIL.TiffImagePlugin import _limit_rational value = 34 - - # Act - ret = _cvt_res(value) - - # Assert + ret = _limit_rational(value, 65536) self.assertEqual(ret, (34, 1)) - def test__cvt_res_float(self): - # Arrange - from PIL.TiffImagePlugin import _cvt_res + def test__limit_rational_float(self): + from PIL.TiffImagePlugin import _limit_rational value = 22.3 - - # Act - ret = _cvt_res(value) - - # Assert - self.assertEqual(ret, (1461452, 65536)) - - def test__cvt_res_sequence(self): - # Arrange - from PIL.TiffImagePlugin import _cvt_res - value = [0, 1] - - # Act - ret = _cvt_res(value) - - # Assert - self.assertEqual(ret, [0, 1]) + ret = _limit_rational(value, 65536) + self.assertEqual(ret, (223, 10)) def test_4bit(self): - # Arrange test_file = "Tests/images/hopper_gray_4bpp.tif" original = hopper("L") - - # Act im = Image.open(test_file) - - # Assert self.assertEqual(im.size, (128, 128)) self.assertEqual(im.mode, "L") self.assert_image_similar(im, original, 7.3) + def test_gray_semibyte_per_pixel(self): + test_files = ( + ( + 24.8,#epsilon + (#group + "Tests/images/tiff_gray_2_4_bpp/hopper2.tif", + "Tests/images/tiff_gray_2_4_bpp/hopper2I.tif", + "Tests/images/tiff_gray_2_4_bpp/hopper2R.tif", + "Tests/images/tiff_gray_2_4_bpp/hopper2IR.tif", + ) + ), + ( + 7.3,#epsilon + (#group + "Tests/images/tiff_gray_2_4_bpp/hopper4.tif", + "Tests/images/tiff_gray_2_4_bpp/hopper4I.tif", + "Tests/images/tiff_gray_2_4_bpp/hopper4R.tif", + "Tests/images/tiff_gray_2_4_bpp/hopper4IR.tif", + ) + ), + ) + original = hopper("L") + for epsilon, group in test_files: + im = Image.open(group[0]) + self.assertEqual(im.size, (128, 128)) + self.assertEqual(im.mode, "L") + self.assert_image_similar(im, original, epsilon) + for file in group[1:]: + im2 = Image.open(file) + self.assertEqual(im2.size, (128, 128)) + self.assertEqual(im2.mode, "L") + self.assert_image_equal(im, im2) + def test_page_number_x_0(self): # Issue 973 # Test TIFF with tag 297 (Page Number) having value of 0 0. # The first number is the current page number. # The second is the total number of pages, zero means not available. - - # Arrange outfile = self.tempfile("temp.tif") - # Created by printing a page in Chrome to PDF, then: # /usr/bin/gs -q -sDEVICE=tiffg3 -sOutputFile=total-pages-zero.tif # -dNOPAUSE /tmp/test.pdf -c quit infile = "Tests/images/total-pages-zero.tif" im = Image.open(infile) - - # Act / Assert # Should not divide by zero im.save(outfile) def test_with_underscores(self): - # Arrange: use underscores kwargs = {'resolution_unit': 'inch', 'x_resolution': 72, 'y_resolution': 36} filename = self.tempfile("temp.tif") - - # Act hopper("RGB").save(filename, **kwargs) - - # Assert from PIL.TiffImagePlugin import X_RESOLUTION, Y_RESOLUTION im = Image.open(filename) - self.assertEqual(im.tag.tags[X_RESOLUTION][0][0], 72) - self.assertEqual(im.tag.tags[Y_RESOLUTION][0][0], 36) + + # legacy interface + self.assertEqual(im.tag[X_RESOLUTION][0][0], 72) + self.assertEqual(im.tag[Y_RESOLUTION][0][0], 36) + + # v2 interface + self.assertEqual(im.tag_v2[X_RESOLUTION], 72) + self.assertEqual(im.tag_v2[Y_RESOLUTION], 36) def test_deprecation_warning_with_spaces(self): - # Arrange: use spaces kwargs = {'resolution unit': 'inch', 'x resolution': 36, 'y resolution': 72} filename = self.tempfile("temp.tif") - - # Act self.assert_warning(DeprecationWarning, lambda: hopper("RGB").save(filename, **kwargs)) - - # Assert from PIL.TiffImagePlugin import X_RESOLUTION, Y_RESOLUTION + im = Image.open(filename) - self.assertEqual(im.tag.tags[X_RESOLUTION][0][0], 36) - self.assertEqual(im.tag.tags[Y_RESOLUTION][0][0], 72) + + # legacy interface + self.assertEqual(im.tag[X_RESOLUTION][0][0], 36) + self.assertEqual(im.tag[Y_RESOLUTION][0][0], 72) + + # v2 interface + self.assertEqual(im.tag_v2[X_RESOLUTION], 36) + self.assertEqual(im.tag_v2[Y_RESOLUTION], 72) + + def test_multipage_compression(self): + im = Image.open('Tests/images/compression.tif') + + im.seek(0) + self.assertEqual(im._compression,'tiff_ccitt') + self.assertEqual(im.size, (10, 10)) + + im.seek(1) + self.assertEqual(im._compression,'packbits') + self.assertEqual(im.size, (10, 10)) + im.load() + + im.seek(0) + self.assertEqual(im._compression,'tiff_ccitt') + self.assertEqual(im.size, (10, 10)) + im.load() def test_save_tiff_with_jpegtables(self): # Arrange @@ -399,7 +450,6 @@ class TestFileTiff(PillowTestCase): # Should not raise UnicodeDecodeError or anything else im.save(outfile) - if __name__ == '__main__': unittest.main() diff --git a/Tests/test_file_tiff_metadata.py b/Tests/test_file_tiff_metadata.py index dfc16682b..fa93b5f08 100644 --- a/Tests/test_file_tiff_metadata.py +++ b/Tests/test_file_tiff_metadata.py @@ -1,8 +1,14 @@ +from __future__ import division + +import io +import struct + from helper import unittest, PillowTestCase, hopper from PIL import Image, TiffImagePlugin, TiffTags +from PIL.TiffImagePlugin import _limit_rational, IFDRational -tag_ids = dict(zip(TiffTags.TAGS.values(), TiffTags.TAGS.keys())) +tag_ids = dict((info.name, info.value) for info in TiffTags.TAGS_V2.values()) class TestFileTiffMetadata(PillowTestCase): @@ -15,61 +21,94 @@ class TestFileTiffMetadata(PillowTestCase): img = hopper() - textdata = "This is some arbitrary metadata for a text field" + # Behaviour change: re #1416 + # Pre ifd rewrite, ImageJMetaData was being written as a string(2), + # Post ifd rewrite, it's defined as arbitrary bytes(7). It should + # roundtrip with the actual bytes, rather than stripped text + # of the premerge tests. + # + # For text items, we still have to decode('ascii','replace') because + # the tiff file format can't take 8 bit bytes in that field. + + basetextdata = "This is some arbitrary metadata for a text field" + bindata = basetextdata.encode('ascii') + b" \xff" + textdata = basetextdata + " " + chr(255) + reloaded_textdata = basetextdata + " ?" floatdata = 12.345 doubledata = 67.89 - info = TiffImagePlugin.ImageFileDirectory() - info[tag_ids['ImageJMetaDataByteCounts']] = len(textdata) - info[tag_ids['ImageJMetaData']] = textdata + ImageJMetaData = tag_ids['ImageJMetaData'] + ImageJMetaDataByteCounts = tag_ids['ImageJMetaDataByteCounts'] + ImageDescription = tag_ids['ImageDescription'] + + info[ImageJMetaDataByteCounts] = len(bindata) + info[ImageJMetaData] = bindata info[tag_ids['RollAngle']] = floatdata info.tagtype[tag_ids['RollAngle']] = 11 - info[tag_ids['YawAngle']] = doubledata info.tagtype[tag_ids['YawAngle']] = 12 + info[ImageDescription] = textdata + f = self.tempfile("temp.tif") img.save(f, tiffinfo=info) loaded = Image.open(f) - self.assertEqual(loaded.tag[50838], (len(textdata),)) - self.assertEqual(loaded.tag[50839], textdata) - self.assertAlmostEqual(loaded.tag[tag_ids['RollAngle']][0], floatdata, - places=5) - self.assertAlmostEqual(loaded.tag[tag_ids['YawAngle']][0], doubledata) + self.assertEqual(loaded.tag[ImageJMetaDataByteCounts], (len(bindata),)) + self.assertEqual(loaded.tag_v2[ImageJMetaDataByteCounts], len(bindata)) + + self.assertEqual(loaded.tag[ImageJMetaData], bindata) + self.assertEqual(loaded.tag_v2[ImageJMetaData], bindata) + + self.assertEqual(loaded.tag[ImageDescription], (reloaded_textdata,)) + self.assertEqual(loaded.tag_v2[ImageDescription], reloaded_textdata) + + loaded_float = loaded.tag[tag_ids['RollAngle']][0] + self.assertAlmostEqual(loaded_float, floatdata, places=5) + loaded_double = loaded.tag[tag_ids['YawAngle']][0] + self.assertAlmostEqual(loaded_double, doubledata) def test_read_metadata(self): img = Image.open('Tests/images/hopper_g4.tif') - known = {'YResolution': ((4294967295, 113653537),), - 'PlanarConfiguration': (1,), - 'BitsPerSample': (1,), - 'ImageLength': (128,), - 'Compression': (4,), - 'FillOrder': (1,), - 'RowsPerStrip': (128,), - 'ResolutionUnit': (3,), - 'PhotometricInterpretation': (0,), - 'PageNumber': (0, 1), - 'XResolution': ((4294967295, 113653537),), - 'ImageWidth': (128,), - 'Orientation': (1,), - 'StripByteCounts': (1968,), - 'SamplesPerPixel': (1,), - 'StripOffsets': (8,), - } + self.assertEqual({'YResolution': IFDRational(4294967295, 113653537), + 'PlanarConfiguration': 1, + 'BitsPerSample': (1,), + 'ImageLength': 128, + 'Compression': 4, + 'FillOrder': 1, + 'RowsPerStrip': 128, + 'ResolutionUnit': 3, + 'PhotometricInterpretation': 0, + 'PageNumber': (0, 1), + 'XResolution': IFDRational(4294967295, 113653537), + 'ImageWidth': 128, + 'Orientation': 1, + 'StripByteCounts': (1968,), + 'SamplesPerPixel': 1, + 'StripOffsets': (8,) + }, img.tag_v2.named()) - # self.assertEqual is equivalent, - # but less helpful in telling what's wrong. - named = img.tag.named() - for tag, value in named.items(): - self.assertEqual(known[tag], value) - - for tag, value in known.items(): - self.assertEqual(value, named[tag]) + self.assertEqual({'YResolution': ((4294967295, 113653537),), + 'PlanarConfiguration': (1,), + 'BitsPerSample': (1,), + 'ImageLength': (128,), + 'Compression': (4,), + 'FillOrder': (1,), + 'RowsPerStrip': (128,), + 'ResolutionUnit': (3,), + 'PhotometricInterpretation': (0,), + 'PageNumber': (0, 1), + 'XResolution': ((4294967295, 113653537),), + 'ImageWidth': (128,), + 'Orientation': (1,), + 'StripByteCounts': (1968,), + 'SamplesPerPixel': (1,), + 'StripOffsets': (8,) + }, img.tag.named()) def test_write_metadata(self): """ Test metadata writing through the python code """ @@ -80,16 +119,36 @@ class TestFileTiffMetadata(PillowTestCase): loaded = Image.open(f) - original = img.tag.named() - reloaded = loaded.tag.named() + original = img.tag_v2.named() + reloaded = loaded.tag_v2.named() - ignored = [ - 'StripByteCounts', 'RowsPerStrip', 'PageNumber', 'StripOffsets'] + for k, v in original.items(): + if type(v) == IFDRational: + original[k] = IFDRational(*_limit_rational(v, 2**31)) + if type(v) == tuple and \ + type(v[0]) == IFDRational: + original[k] = tuple([IFDRational( + *_limit_rational(elt, 2**31)) for elt in v]) + + ignored = ['StripByteCounts', 'RowsPerStrip', + 'PageNumber', 'StripOffsets'] for tag, value in reloaded.items(): - if tag not in ignored: - self.assertEqual( - original[tag], value, "%s didn't roundtrip" % tag) + if tag in ignored: + continue + if (type(original[tag]) == tuple + and type(original[tag][0]) == IFDRational): + # Need to compare element by element in the tuple, + # not comparing tuples of object references + self.assert_deep_equal(original[tag], + value, + "%s didn't roundtrip, %s, %s" % + (tag, original[tag], value)) + else: + self.assertEqual(original[tag], + value, + "%s didn't roundtrip, %s, %s" % + (tag, original[tag], value)) for tag, value in original.items(): if tag not in ignored: @@ -100,6 +159,45 @@ class TestFileTiffMetadata(PillowTestCase): self.assertEqual(tag_ids['MakerNoteSafety'], 50741) self.assertEqual(tag_ids['BestQualityScale'], 50780) + def test_empty_metadata(self): + f = io.BytesIO(b'II*\x00\x08\x00\x00\x00') + head = f.read(8) + info = TiffImagePlugin.ImageFileDirectory(head) + try: + self.assert_warning(UserWarning, lambda: info.load(f)) + except struct.error: + self.fail("Should not be struct errors there.") + + def test_iccprofile(self): + # https://github.com/python-pillow/Pillow/issues/1462 + im = Image.open('Tests/images/hopper.iccprofile.tif') + out = self.tempfile('temp.tiff') + + im.save(out) + reloaded = Image.open(out) + self.assert_(type(im.info['icc_profile']) is not type(tuple)) + self.assertEqual(im.info['icc_profile'], reloaded.info['icc_profile']) + + def test_iccprofile_binary(self): + # https://github.com/python-pillow/Pillow/issues/1526 + # We should be able to load this, but probably won't be able to save it. + + im = Image.open('Tests/images/hopper.iccprofile_binary.tif') + self.assertEqual(im.tag_v2.tagtype[34675], 1) + self.assertTrue(im.info['icc_profile']) + + def test_exif_div_zero(self): + im = hopper() + info = TiffImagePlugin.ImageFileDirectory_v2() + info[41988] = TiffImagePlugin.IFDRational(0, 0) + + out = self.tempfile('temp.tiff') + im.save(out, tiffinfo=info, compression='raw') + + reloaded = Image.open(out) + self.assertEqual(0, reloaded.tag_v2[41988][0].numerator) + self.assertEqual(0, reloaded.tag_v2[41988][0].denominator) + if __name__ == '__main__': unittest.main() diff --git a/Tests/test_file_webp.py b/Tests/test_file_webp.py index 8c8313dd9..d1c8e580e 100644 --- a/Tests/test_file_webp.py +++ b/Tests/test_file_webp.py @@ -72,6 +72,11 @@ class TestFileWebp(PillowTestCase): target = hopper("RGB") self.assert_image_similar(image, target, 12) + def test_write_unsupported_mode(self): + temp_file = self.tempfile("temp.webp") + + self.assertRaises(IOError, lambda: hopper("L").save(temp_file)) + if __name__ == '__main__': unittest.main() diff --git a/Tests/test_file_webp_alpha.py b/Tests/test_file_webp_alpha.py index f316b71e1..e7df62ee6 100644 --- a/Tests/test_file_webp_alpha.py +++ b/Tests/test_file_webp_alpha.py @@ -83,7 +83,8 @@ class TestFileWebpAlpha(PillowTestCase): image.load() image.getdata() - # early versions of webp are known to produce higher deviations: deal with it + # early versions of webp are known to produce higher deviations: + # deal with it if _webp.WebPDecoderVersion(self) <= 0x201: self.assert_image_similar(image, pil_image, 3.0) else: diff --git a/Tests/test_file_webp_lossless.py b/Tests/test_file_webp_lossless.py index 52a461a74..1322e03c5 100644 --- a/Tests/test_file_webp_lossless.py +++ b/Tests/test_file_webp_lossless.py @@ -4,7 +4,7 @@ from PIL import Image try: from PIL import _webp -except: +except ImportError: pass # Skip in setUp() diff --git a/Tests/test_file_webp_metadata.py b/Tests/test_file_webp_metadata.py index 8b1254d61..06df0f82e 100644 --- a/Tests/test_file_webp_metadata.py +++ b/Tests/test_file_webp_metadata.py @@ -8,7 +8,7 @@ class TestFileWebpMetadata(PillowTestCase): def setUp(self): try: from PIL import _webp - except: + except ImportError: self.skipTest('WebP support not installed') return diff --git a/Tests/test_file_xbm.py b/Tests/test_file_xbm.py index 02aec70b1..5812ac934 100644 --- a/Tests/test_file_xbm.py +++ b/Tests/test_file_xbm.py @@ -37,6 +37,30 @@ class TestFileXbm(PillowTestCase): self.assertEqual(im.mode, '1') self.assertEqual(im.size, (32, 32)) + def test_open(self): + # Arrange + # Created with `convert hopper.png hopper.xbm` + filename = "Tests/images/hopper.xbm" + + # Act + im = Image.open(filename) + + # Assert + self.assertEqual(im.mode, '1') + self.assertEqual(im.size, (128, 128)) + + def test_open_filename_with_underscore(self): + # Arrange + # Created with `convert hopper.png hopper_underscore.xbm` + filename = "Tests/images/hopper_underscore.xbm" + + # Act + im = Image.open(filename) + + # Assert + self.assertEqual(im.mode, '1') + self.assertEqual(im.size, (128, 128)) + if __name__ == '__main__': unittest.main() diff --git a/Tests/test_file_xpm.py b/Tests/test_file_xpm.py index 6a6817048..e589a8d26 100644 --- a/Tests/test_file_xpm.py +++ b/Tests/test_file_xpm.py @@ -1,6 +1,6 @@ from helper import unittest, PillowTestCase, hopper -from PIL import Image +from PIL import Image, XpmImagePlugin # sample ppm stream TEST_FILE = "Tests/images/hopper.xpm" @@ -18,6 +18,12 @@ class TestFileXpm(PillowTestCase): # large error due to quantization->44 colors. self.assert_image_similar(im.convert('RGB'), hopper('RGB'), 60) + def test_invalid_file(self): + invalid_file = "Tests/images/flower.jpg" + + self.assertRaises(SyntaxError, + lambda: XpmImagePlugin.XpmImageFile(invalid_file)) + def test_load_read(self): # Arrange im = Image.open(TEST_FILE) diff --git a/Tests/test_font_bdf.py b/Tests/test_font_bdf.py index b844f1228..4d88ae46f 100644 --- a/Tests/test_font_bdf.py +++ b/Tests/test_font_bdf.py @@ -15,6 +15,10 @@ class TestFontBdf(PillowTestCase): self.assertIsInstance(font, FontFile.FontFile) self.assertEqual(len([_f for _f in font.glyph if _f]), 190) + def test_invalid_file(self): + with open("Tests/images/flower.jpg", "rb") as fp: + self.assertRaises(SyntaxError, lambda: BdfFontFile.BdfFontFile(fp)) + if __name__ == '__main__': unittest.main() diff --git a/Tests/test_font_pcf.py b/Tests/test_font_pcf.py index 3cc6afa64..f6dd9265e 100644 --- a/Tests/test_font_pcf.py +++ b/Tests/test_font_pcf.py @@ -30,6 +30,10 @@ class TestFontPcf(PillowTestCase): def test_sanity(self): self.save_font() + def test_invalid_file(self): + with open("Tests/images/flower.jpg", "rb") as fp: + self.assertRaises(SyntaxError, lambda: PcfFontFile.PcfFontFile(fp)) + def xtest_draw(self): tempname = self.save_font() diff --git a/Tests/test_image.py b/Tests/test_image.py index caee70fec..a9c168fa7 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -1,6 +1,8 @@ from helper import unittest, PillowTestCase, hopper from PIL import Image +import os +import sys class TestImage(PillowTestCase): @@ -30,6 +32,48 @@ class TestImage(PillowTestCase): # self.assertRaises( # MemoryError, lambda: Image.new("L", (1000000, 1000000))) + def test_width_height(self): + im = Image.new("RGB", (1, 2)) + self.assertEqual(im.width, 1) + self.assertEqual(im.height, 2) + + im.size = (3, 4) + self.assertEqual(im.width, 3) + self.assertEqual(im.height, 4) + + def test_invalid_image(self): + if str is bytes: + import StringIO + im = StringIO.StringIO('') + else: + import io + im = io.BytesIO(b'') + self.assertRaises(IOError, lambda: Image.open(im)) + + @unittest.skipIf(sys.version_info < (3, 4), + "pathlib only available in Python 3.4 or later") + def test_pathlib(self): + from pathlib import Path + im = Image.open(Path("Tests/images/hopper.jpg")) + self.assertEqual(im.mode, "RGB") + self.assertEqual(im.size, (128, 128)) + + temp_file = self.tempfile("temp.jpg") + if os.path.exists(temp_file): + os.remove(temp_file) + im.save(Path(temp_file)) + + def test_tempfile(self): + # see #1460, pathlib support breaks tempfile.TemporaryFile on py27 + # Will error out on save on 3.0.0 + import tempfile + im = hopper() + fp = tempfile.TemporaryFile() + im.save(fp, 'JPEG') + fp.seek(0) + reloaded = Image.open(fp) + self.assert_image_similar(im, reloaded, 20) + def test_internals(self): im = Image.new("L", (100, 100)) diff --git a/Tests/test_image_array.py b/Tests/test_image_array.py index 0899afd02..ce8cbe78c 100644 --- a/Tests/test_image_array.py +++ b/Tests/test_image_array.py @@ -10,18 +10,18 @@ class TestImageArray(PillowTestCase): def test_toarray(self): def test(mode): ai = im.convert(mode).__array_interface__ - return ai["shape"], ai["typestr"], len(ai["data"]) - # self.assertEqual(test("1"), ((100, 128), '|b1', 1600)) - self.assertEqual(test("L"), ((100, 128), '|u1', 12800)) + return ai['version'], ai["shape"], ai["typestr"], len(ai["data"]) + # self.assertEqual(test("1"), (3, (100, 128), '|b1', 1600)) + self.assertEqual(test("L"), (3, (100, 128), '|u1', 12800)) # FIXME: wrong? - self.assertEqual(test("I"), ((100, 128), Image._ENDIAN + 'i4', 51200)) + self.assertEqual(test("I"), (3, (100, 128), Image._ENDIAN + 'i4', 51200)) # FIXME: wrong? - self.assertEqual(test("F"), ((100, 128), Image._ENDIAN + 'f4', 51200)) + self.assertEqual(test("F"), (3, (100, 128), Image._ENDIAN + 'f4', 51200)) - self.assertEqual(test("RGB"), ((100, 128, 3), '|u1', 38400)) - self.assertEqual(test("RGBA"), ((100, 128, 4), '|u1', 51200)) - self.assertEqual(test("RGBX"), ((100, 128, 4), '|u1', 51200)) + self.assertEqual(test("RGB"), (3, (100, 128, 3), '|u1', 38400)) + self.assertEqual(test("RGBA"), (3, (100, 128, 4), '|u1', 51200)) + self.assertEqual(test("RGBX"), (3, (100, 128, 4), '|u1', 51200)) def test_fromarray(self): def test(mode): diff --git a/Tests/test_image_copy.py b/Tests/test_image_copy.py index d7725e672..8c2165a62 100644 --- a/Tests/test_image_copy.py +++ b/Tests/test_image_copy.py @@ -1,16 +1,37 @@ from helper import unittest, PillowTestCase, hopper +import copy + class TestImageCopy(PillowTestCase): def test_copy(self): - def copy(mode): + croppedCoordinates = (10, 10, 20, 20) + croppedSize = (10, 10) + for mode in "1", "P", "L", "RGB", "I", "F": + # Internal copy method im = hopper(mode) out = im.copy() - self.assertEqual(out.mode, mode) + self.assertEqual(out.mode, im.mode) self.assertEqual(out.size, im.size) - for mode in "1", "P", "L", "RGB", "I", "F": - copy(mode) + + # Python's copy method + im = hopper(mode) + out = copy.copy(im) + self.assertEqual(out.mode, im.mode) + self.assertEqual(out.size, im.size) + + # Internal copy method on a cropped image + im = hopper(mode) + out = im.crop(croppedCoordinates).copy() + self.assertEqual(out.mode, im.mode) + self.assertEqual(out.size, croppedSize) + + # Python's copy method on a cropped image + im = hopper(mode) + out = copy.copy(im.crop(croppedCoordinates)) + self.assertEqual(out.mode, im.mode) + self.assertEqual(out.size, croppedSize) if __name__ == '__main__': unittest.main() diff --git a/Tests/test_image_crop.py b/Tests/test_image_crop.py index 29ff2bc2a..6edc71016 100644 --- a/Tests/test_image_crop.py +++ b/Tests/test_image_crop.py @@ -52,6 +52,20 @@ class TestImageCrop(PillowTestCase): self.assertEqual(len(im.getdata()), 0) self.assertRaises(IndexError, lambda: im.getdata()[0]) + def test_crop_float(self): + # Check cropping floats are rounded to nearest integer + # https://github.com/python-pillow/Pillow/issues/1744 + + # Arrange + im = Image.new("RGB", (10, 10)) + self.assertEqual(im.size, (10, 10)) + + # Act + cropped = im.crop((0.9, 1.1, 4.2, 5.8)) + + # Assert + self.assertEqual(cropped.size, (3, 5)) + if __name__ == '__main__': unittest.main() diff --git a/Tests/test_image_filter.py b/Tests/test_image_filter.py index 6a694b3ca..f75a1891d 100644 --- a/Tests/test_image_filter.py +++ b/Tests/test_image_filter.py @@ -88,6 +88,12 @@ class TestImageFilter(PillowTestCase): self.assertEqual(rankfilter("I"), (0, 4, 8)) self.assertEqual(rankfilter("F"), (0.0, 4.0, 8.0)) + def test_rankfilter_properties(self): + rankfilter = ImageFilter.RankFilter(1, 2) + + self.assertEqual(rankfilter.size, 1) + self.assertEqual(rankfilter.rank, 2) + if __name__ == '__main__': unittest.main() diff --git a/Tests/test_image_fromqimage.py b/Tests/test_image_fromqimage.py new file mode 100644 index 000000000..a4cf76fb9 --- /dev/null +++ b/Tests/test_image_fromqimage.py @@ -0,0 +1,50 @@ +from helper import unittest, PillowTestCase, hopper +from test_imageqt import PillowQtTestCase + +from PIL import ImageQt, Image + + +class TestFromQImage(PillowQtTestCase, PillowTestCase): + + files_to_test = [ + hopper(), + Image.open('Tests/images/transparent.png'), + Image.open('Tests/images/7x13.png'), + ] + + def roundtrip(self, expected): + # PIL -> Qt + intermediate = expected.toqimage() + # Qt -> PIL + result = ImageQt.fromqimage(intermediate) + + if intermediate.hasAlphaChannel(): + self.assert_image_equal(result, expected.convert('RGBA')) + else: + self.assert_image_equal(result, expected.convert('RGB')) + + def test_sanity_1(self): + for im in self.files_to_test: + self.roundtrip(im.convert('1')) + + def test_sanity_rgb(self): + for im in self.files_to_test: + self.roundtrip(im.convert('RGB')) + + def test_sanity_rgba(self): + for im in self.files_to_test: + self.roundtrip(im.convert('RGBA')) + + def test_sanity_l(self): + for im in self.files_to_test: + self.roundtrip(im.convert('L')) + + def test_sanity_p(self): + for im in self.files_to_test: + self.roundtrip(im.convert('P')) + + +if __name__ == '__main__': + unittest.main() + +# End of file diff --git a/Tests/test_image_fromqpixmap.py b/Tests/test_image_fromqpixmap.py new file mode 100644 index 000000000..78e4d6770 --- /dev/null +++ b/Tests/test_image_fromqpixmap.py @@ -0,0 +1,34 @@ +from helper import unittest, PillowTestCase, hopper +from test_imageqt import PillowQtTestCase, PillowQPixmapTestCase + +from PIL import ImageQt + + +class TestFromQPixmap(PillowQPixmapTestCase, PillowTestCase): + + def roundtrip(self, expected): + PillowQtTestCase.setUp(self) + result = ImageQt.fromqpixmap(ImageQt.toqpixmap(expected)) + # 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')) + + +if __name__ == '__main__': + unittest.main() + +# End of file diff --git a/Tests/test_image_getim.py b/Tests/test_image_getim.py index e83439789..a5e1d4e42 100644 --- a/Tests/test_image_getim.py +++ b/Tests/test_image_getim.py @@ -1,4 +1,5 @@ from helper import unittest, PillowTestCase, hopper, py3 +import sys class TestImageGetIm(PillowTestCase): @@ -10,7 +11,13 @@ class TestImageGetIm(PillowTestCase): if py3: self.assertIn("PyCapsule", type_repr) - self.assertIsInstance(im.im.id, int) + if sys.hexversion < 0x2070000: + # py2.6 x64, windows + target_types = (int, long) + else: + target_types = (int) + + self.assertIsInstance(im.im.id, target_types) if __name__ == '__main__': diff --git a/Tests/test_image_mode.py b/Tests/test_image_mode.py index 6441c7d1b..c20db50aa 100644 --- a/Tests/test_image_mode.py +++ b/Tests/test_image_mode.py @@ -21,12 +21,14 @@ class TestImageMode(PillowTestCase): m = ImageMode.getmode("1") self.assertEqual(m.mode, "1") + self.assertEqual(str(m), "1") self.assertEqual(m.bands, ("1",)) self.assertEqual(m.basemode, "L") self.assertEqual(m.basetype, "L") m = ImageMode.getmode("RGB") self.assertEqual(m.mode, "RGB") + self.assertEqual(str(m), "RGB") self.assertEqual(m.bands, ("R", "G", "B")) self.assertEqual(m.basemode, "RGB") self.assertEqual(m.basetype, "L") diff --git a/Tests/test_image_offset.py b/Tests/test_image_offset.py deleted file mode 100644 index e5fe0bf47..000000000 --- a/Tests/test_image_offset.py +++ /dev/null @@ -1,25 +0,0 @@ -from helper import unittest, PillowTestCase, hopper - - -class TestImageOffset(PillowTestCase): - - def test_offset(self): - - im1 = hopper() - - im2 = self.assert_warning(DeprecationWarning, lambda: im1.offset(10)) - self.assertEqual(im1.getpixel((0, 0)), im2.getpixel((10, 10))) - - im2 = self.assert_warning( - DeprecationWarning, lambda: im1.offset(10, 20)) - self.assertEqual(im1.getpixel((0, 0)), im2.getpixel((10, 20))) - - im2 = self.assert_warning( - DeprecationWarning, lambda: im1.offset(20, 20)) - self.assertEqual(im1.getpixel((0, 0)), im2.getpixel((20, 20))) - - -if __name__ == '__main__': - unittest.main() - -# End of file diff --git a/Tests/test_image_point.py b/Tests/test_image_point.py index 9e001dfb2..d60364c08 100644 --- a/Tests/test_image_point.py +++ b/Tests/test_image_point.py @@ -22,10 +22,6 @@ class TestImagePoint(PillowTestCase): """ Tests for 16 bit -> 8 bit lut for converting I->L images see https://github.com/python-pillow/Pillow/issues/440 """ - # This takes _forever_ on PyPy. Open Bug, - # see https://github.com/python-pillow/Pillow/issues/484 - # self.skipKnownBadTest(msg="Too Slow on pypy", interpreter='pypy') - im = hopper("I") im.point(list(range(256))*256, 'L') diff --git a/Tests/test_image_quantize.py b/Tests/test_image_quantize.py index e8183eb48..986a0a6cc 100644 --- a/Tests/test_image_quantize.py +++ b/Tests/test_image_quantize.py @@ -28,6 +28,10 @@ class TestImageQuantize(PillowTestCase): im.quantize() self.assertRaises(Exception, lambda: im.quantize(method=0)) + def test_quantize(self): + im = Image.open('Tests/images/caption_6_33_22.png') + im.convert('RGB').quantize().convert('RGB') + if __name__ == '__main__': unittest.main() diff --git a/Tests/test_image_resample.py b/Tests/test_image_resample.py new file mode 100644 index 000000000..1bba40f8f --- /dev/null +++ b/Tests/test_image_resample.py @@ -0,0 +1,37 @@ +from helper import unittest, PillowTestCase, hopper +from PIL import Image + + +class TestImagingCoreResize(PillowTestCase): + # see https://github.com/python-pillow/Pillow/issues/1710 + def test_overflow(self): + im = hopper('L') + xsize = 0x100000008 // 4 + ysize = 1000 # unimportant + try: + # any resampling filter will do here + im.im.resize((xsize, ysize), Image.LINEAR) + self.fail("Resize should raise MemoryError on invalid xsize") + except MemoryError: + self.assertTrue(True, "Should raise MemoryError") + + def test_invalid_size(self): + im = hopper() + + im.resize((100, 100)) + self.assertTrue(True, "Should not Crash") + + try: + im.resize((-100, 100)) + self.fail("Resize should raise a value error on x negative size") + except ValueError: + self.assertTrue(True, "Should raise ValueError") + + try: + im.resize((100, -100)) + self.fail("Resize should raise a value error on y negative size") + except ValueError: + self.assertTrue(True, "Should raise ValueError") + +if __name__ == '__main__': + unittest.main() diff --git a/Tests/test_image_rotate.py b/Tests/test_image_rotate.py index 38391adae..26c0bd729 100644 --- a/Tests/test_image_rotate.py +++ b/Tests/test_image_rotate.py @@ -1,19 +1,23 @@ from helper import unittest, PillowTestCase, hopper +from PIL import Image class TestImageRotate(PillowTestCase): def test_rotate(self): - def rotate(mode): - im = hopper(mode) - out = im.rotate(45) + def rotate(im, mode, angle): + out = im.rotate(angle) self.assertEqual(out.mode, mode) self.assertEqual(out.size, im.size) # default rotate clips output - out = im.rotate(45, expand=1) + out = im.rotate(angle, expand=1) self.assertEqual(out.mode, mode) self.assertNotEqual(out.size, im.size) for mode in "1", "P", "L", "RGB", "I", "F": - rotate(mode) + im = hopper(mode) + rotate(im, mode, 45) + for angle in 90, 270: + im = Image.open('Tests/images/test-card.png') + rotate(im, im.mode, angle) if __name__ == '__main__': diff --git a/Tests/test_image_tobytes.py b/Tests/test_image_tobytes.py index 31a7e1722..a15bc002d 100644 --- a/Tests/test_image_tobytes.py +++ b/Tests/test_image_tobytes.py @@ -5,7 +5,7 @@ class TestImageToBytes(PillowTestCase): def test_sanity(self): data = hopper().tobytes() - self.assertTrue(isinstance(data, bytes)) + self.assertIsInstance(data, bytes) if __name__ == '__main__': unittest.main() diff --git a/Tests/test_image_toqimage.py b/Tests/test_image_toqimage.py new file mode 100644 index 000000000..76169258c --- /dev/null +++ b/Tests/test_image_toqimage.py @@ -0,0 +1,29 @@ +from helper import unittest, PillowTestCase, hopper +from test_imageqt import PillowQtTestCase + +from PIL import ImageQt + + +if ImageQt.qt_is_installed: + from PIL.ImageQt import QImage + + +class TestToQImage(PillowQtTestCase, PillowTestCase): + + def test_sanity(self): + PillowQtTestCase.setUp(self) + for mode in ('1', 'RGB', 'RGBA', 'L', 'P'): + data = ImageQt.toqimage(hopper(mode)) + + self.assertIsInstance(data, QImage) + self.assertFalse(data.isNull()) + + # Test saving the file + tempfile = self.tempfile('temp_{0}.png'.format(mode)) + data.save(tempfile) + + +if __name__ == '__main__': + unittest.main() + +# End of file diff --git a/Tests/test_image_toqpixmap.py b/Tests/test_image_toqpixmap.py new file mode 100644 index 000000000..eb9f2f7c4 --- /dev/null +++ b/Tests/test_image_toqpixmap.py @@ -0,0 +1,29 @@ +from helper import unittest, PillowTestCase, hopper +from test_imageqt import PillowQtTestCase, PillowQPixmapTestCase + +from PIL import ImageQt + +if ImageQt.qt_is_installed: + from PIL.ImageQt import QPixmap + + +class TestToQPixmap(PillowQPixmapTestCase, PillowTestCase): + + def test_sanity(self): + PillowQtTestCase.setUp(self) + + for mode in ('1', 'RGB', 'RGBA', 'L', 'P'): + data = ImageQt.toqpixmap(hopper(mode)) + + self.assertIsInstance(data, QPixmap) + self.assertFalse(data.isNull()) + + # Test saving the file + tempfile = self.tempfile('temp_{0}.png'.format(mode)) + data.save(tempfile) + + +if __name__ == '__main__': + unittest.main() + +# End of file diff --git a/Tests/test_imagecms.py b/Tests/test_imagecms.py index 7a2a57151..bde01d9d4 100644 --- a/Tests/test_imagecms.py +++ b/Tests/test_imagecms.py @@ -1,4 +1,5 @@ from helper import unittest, PillowTestCase, hopper +import datetime from PIL import Image @@ -254,6 +255,60 @@ class TestImageCms(PillowTestCase): self.assertEqual(ImageCms.getProfileDescription(p), ImageCms.getProfileDescription(p2)) + def test_extended_information(self): + o = ImageCms.getOpenProfile(SRGB) + p = o.profile + + self.assertEqual(p.attributes, 4294967296) + self.assertEqual(p.blue_colorant, ((0.14306640625, 0.06060791015625, 0.7140960693359375), (0.1558847490315394, 0.06603820639433387, 0.06060791015625))) + self.assertEqual(p.blue_primary, ((0.14306641366715667, 0.06060790921083026, 0.7140960805782015), (0.15588475410450106, 0.06603820408959558, 0.06060790921083026))) + self.assertEqual(p.chromatic_adaptation, (((1.04791259765625, 0.0229339599609375, -0.050201416015625), (0.02960205078125, 0.9904632568359375, -0.0170745849609375), (-0.009246826171875, 0.0150604248046875, 0.7517852783203125)), ((1.0267159024652783, 0.022470062342089134, 0.0229339599609375), (0.02951378324103937, 0.9875098886387147, 0.9904632568359375), (-0.012205438066465256, 0.01987915407854985, 0.0150604248046875)))) + self.assertEqual(p.chromaticity, None) + self.assertEqual(p.clut, {0: (False, False, True), 1: (False, False, True), 2: (False, False, True), 3: (False, False, True)}) + self.assertEqual(p.color_space, 'RGB') + self.assertEqual(p.colorant_table, None) + self.assertEqual(p.colorant_table_out, None) + self.assertEqual(p.colorimetric_intent, None) + self.assertEqual(p.connection_space, 'XYZ ') + self.assertEqual(p.copyright, 'Copyright International Color Consortium, 2009') + self.assertEqual(p.creation_date, datetime.datetime(2009, 2, 27, 21, 36, 31)) + self.assertEqual(p.device_class, 'mntr') + self.assertEqual(p.green_colorant, ((0.3851470947265625, 0.7168731689453125, 0.097076416015625), (0.32119769927720654, 0.5978443449048152, 0.7168731689453125))) + self.assertEqual(p.green_primary, ((0.3851470888162112, 0.7168731974161346, 0.09707641738998518), (0.32119768793686687, 0.5978443567149709, 0.7168731974161346))) + self.assertEqual(p.header_flags, 0) + self.assertEqual(p.header_manufacturer, '\x00\x00\x00\x00') + self.assertEqual(p.header_model, '\x00\x00\x00\x00') + self.assertEqual(p.icc_measurement_condition, {'backing': (0.0, 0.0, 0.0), 'flare': 0.0, 'geo': 'unknown', 'observer': 1, 'illuminant_type': 'D65'}) + self.assertEqual(p.icc_version, 33554432) + self.assertEqual(p.icc_viewing_condition, None) + self.assertEqual(p.intent_supported, {0: (True, True, True), 1: (True, True, True), 2: (True, True, True), 3: (True, True, True)}) + self.assertEqual(p.is_matrix_shaper, True) + self.assertEqual(p.luminance, ((0.0, 80.0, 0.0), (0.0, 1.0, 80.0))) + self.assertEqual(p.manufacturer, None) + self.assertEqual(p.media_black_point, ((0.012054443359375, 0.0124969482421875, 0.01031494140625), (0.34573304157549234, 0.35842450765864337, 0.0124969482421875))) + self.assertEqual(p.media_white_point, ((0.964202880859375, 1.0, 0.8249053955078125), (0.3457029219802284, 0.3585375327567059, 1.0))) + self.assertEqual(p.media_white_point_temperature, 5000.722328847392) + self.assertEqual(p.model, 'IEC 61966-2-1 Default RGB Colour Space - sRGB') + self.assertEqual(p.pcs, 'XYZ') + self.assertEqual(p.perceptual_rendering_intent_gamut, None) + 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(p.profile_id, b')\xf8=\xde\xaf\xf2U\xaexB\xfa\xe4\xca\x839\r') + self.assertEqual(p.red_colorant, ((0.436065673828125, 0.2224884033203125, 0.013916015625), (0.6484536316398539, 0.3308524880306778, 0.2224884033203125))) + self.assertEqual(p.red_primary, ((0.43606566581047446, 0.22248840582960838, 0.013916015621759925), (0.6484536250319214, 0.3308524944738204, 0.22248840582960838))) + self.assertEqual(p.rendering_intent, 0) + self.assertEqual(p.saturation_rendering_intent_gamut, None) + self.assertEqual(p.screening_description, None) + self.assertEqual(p.target, None) + self.assertEqual(p.technology, 'CRT ') + self.assertEqual(p.version, 2.0) + self.assertEqual(p.viewing_condition, 'Reference Viewing Condition in IEC 61966-2-1') + self.assertEqual(p.xcolor_space, 'RGB ') + if __name__ == '__main__': unittest.main() diff --git a/Tests/test_imagecolor.py b/Tests/test_imagecolor.py index 5d8944852..a38c8b070 100644 --- a/Tests/test_imagecolor.py +++ b/Tests/test_imagecolor.py @@ -18,6 +18,9 @@ class TestImageColor(PillowTestCase): (255, 0, 0, 0), ImageColor.getrgb("rgba(255, 0, 0, 0)")) self.assertEqual((255, 0, 0), ImageColor.getrgb("red")) + self.assertRaises(ValueError, + lambda: ImageColor.getrgb("invalid color")) + # look for rounding errors (based on code by Tim Hatch) def test_rounding_errors(self): @@ -43,8 +46,8 @@ class TestImageColor(PillowTestCase): self.assertEqual(0, ImageColor.getcolor("black", "L")) self.assertEqual(255, ImageColor.getcolor("white", "L")) - self.assertEqual( - 162, ImageColor.getcolor("rgba(0, 255, 115, 33)", "L")) + self.assertEqual(162, + ImageColor.getcolor("rgba(0, 255, 115, 33)", "L")) Image.new("L", (1, 1), "white") self.assertEqual(0, ImageColor.getcolor("black", "1")) diff --git a/Tests/test_imagedraw.py b/Tests/test_imagedraw.py index a1ed20a3a..947e35971 100644 --- a/Tests/test_imagedraw.py +++ b/Tests/test_imagedraw.py @@ -5,14 +5,14 @@ from PIL import ImageColor from PIL import ImageDraw import os.path +import sys + BLACK = (0, 0, 0) WHITE = (255, 255, 255) GRAY = (190, 190, 190) DEFAULT_MODE = 'RGB' IMAGES_PATH = os.path.join('Tests', 'images', 'imagedraw') -import sys - # Image size W, H = 100, 100 @@ -44,22 +44,27 @@ class TestImageDraw(PillowTestCase): draw.polygon(list(range(100))) draw.rectangle(list(range(4))) - def test_deprecated(self): - im = hopper().copy() + def test_removed_methods(self): + im = hopper() draw = ImageDraw.Draw(im) - self.assert_warning(DeprecationWarning, lambda: draw.setink(0)) - self.assert_warning(DeprecationWarning, lambda: draw.setfill(0)) + self.assertRaises(Exception, lambda: draw.setink(0)) + self.assertRaises(Exception, lambda: draw.setfill(0)) - def helper_arc(self, bbox): + def test_mode_mismatch(self): + im = hopper("RGB").copy() + + self.assertRaises(ValueError, + lambda: ImageDraw.ImageDraw(im, mode="L")) + + def helper_arc(self, bbox, start, end): # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) # Act - # FIXME Fill param should be named outline. - draw.arc(bbox, 0, 180) + draw.arc(bbox, start, end) del draw # Assert @@ -67,10 +72,12 @@ class TestImageDraw(PillowTestCase): im, Image.open("Tests/images/imagedraw_arc.png"), 1) def test_arc1(self): - self.helper_arc(BBOX1) + self.helper_arc(BBOX1, 0, 180) + self.helper_arc(BBOX1, 0.5, 180.4) def test_arc2(self): - self.helper_arc(BBOX2) + self.helper_arc(BBOX2, 0, 180) + self.helper_arc(BBOX2, 0.5, 180.4) def test_bitmap(self): # Arrange @@ -86,13 +93,13 @@ class TestImageDraw(PillowTestCase): self.assert_image_equal( im, Image.open("Tests/images/imagedraw_bitmap.png")) - def helper_chord(self, bbox): + def helper_chord(self, bbox, start, end): # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) # Act - draw.chord(bbox, 0, 180, fill="red", outline="yellow") + draw.chord(bbox, start, end, fill="red", outline="yellow") del draw # Assert @@ -100,10 +107,12 @@ class TestImageDraw(PillowTestCase): im, Image.open("Tests/images/imagedraw_chord.png"), 1) def test_chord1(self): - self.helper_chord(BBOX1) + self.helper_chord(BBOX1, 0, 180) + self.helper_chord(BBOX1, 0.5, 180.4) def test_chord2(self): - self.helper_chord(BBOX2) + self.helper_chord(BBOX2, 0, 180) + self.helper_chord(BBOX2, 0.5, 180.4) def helper_ellipse(self, bbox): # Arrange @@ -124,6 +133,19 @@ class TestImageDraw(PillowTestCase): def test_ellipse2(self): self.helper_ellipse(BBOX2) + def test_ellipse_edge(self): + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw.Draw(im) + + # Act + draw.ellipse(((0, 0), (W-1, H)), fill="white") + del draw + + # Assert + self.assert_image_similar( + im, Image.open("Tests/images/imagedraw_ellipse_edge.png"), 1) + def helper_line(self, points): # Arrange im = Image.new("RGB", (W, H)) @@ -143,13 +165,13 @@ class TestImageDraw(PillowTestCase): def test_line2(self): self.helper_line(POINTS2) - def helper_pieslice(self, bbox): + def helper_pieslice(self, bbox, start, end): # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) # Act - draw.pieslice(bbox, -90, 45, fill="white", outline="blue") + draw.pieslice(bbox, start, end, fill="white", outline="blue") del draw # Assert @@ -157,10 +179,12 @@ class TestImageDraw(PillowTestCase): im, Image.open("Tests/images/imagedraw_pieslice.png"), 1) def test_pieslice1(self): - self.helper_pieslice(BBOX1) + self.helper_pieslice(BBOX1, -90, 45) + self.helper_pieslice(BBOX1, -90.5, 45.4) def test_pieslice2(self): - self.helper_pieslice(BBOX2) + self.helper_pieslice(BBOX2, -90, 45) + self.helper_pieslice(BBOX2, -90.5, 45.4) def helper_point(self, points): # Arrange diff --git a/Tests/test_imagefile.py b/Tests/test_imagefile.py index 5311b899f..5705180ff 100644 --- a/Tests/test_imagefile.py +++ b/Tests/test_imagefile.py @@ -26,7 +26,7 @@ class TestImageFile(PillowTestCase): test_file = BytesIO() - im.save(test_file, format) + im.copy().save(test_file, format) data = test_file.getvalue() @@ -37,7 +37,8 @@ class TestImageFile(PillowTestCase): return im, imOut self.assert_image_equal(*roundtrip("BMP")) - self.assert_image_equal(*roundtrip("GIF")) + im1, im2 = roundtrip("GIF") + self.assert_image_similar(im1.convert('P'), im2, 1) self.assert_image_equal(*roundtrip("IM")) self.assert_image_equal(*roundtrip("MSP")) if "zip_encoder" in codecs: @@ -78,12 +79,11 @@ class TestImageFile(PillowTestCase): self.assertEqual((48, 48), p.image.size) def test_safeblock(self): - - im1 = hopper() - if "zip_encoder" not in codecs: self.skipTest("PNG (zlib) encoder not available") + im1 = hopper() + try: ImageFile.SAFEBLOCK = 1 im2 = fromstring(tostring(im1, "PNG")) @@ -92,6 +92,48 @@ class TestImageFile(PillowTestCase): self.assert_image_equal(im1, im2) + def test_raise_ioerror(self): + self.assertRaises(IOError, lambda: ImageFile.raise_ioerror(1)) + + def test_truncated_with_errors(self): + if "zip_encoder" not in codecs: + self.skipTest("PNG (zlib) encoder not available") + + im = Image.open("Tests/images/truncated_image.png") + with self.assertRaises(IOError): + im.load() + + def test_truncated_without_errors(self): + if "zip_encoder" not in codecs: + self.skipTest("PNG (zlib) encoder not available") + + im = Image.open("Tests/images/truncated_image.png") + + ImageFile.LOAD_TRUNCATED_IMAGES = True + try: + im.load() + finally: + ImageFile.LOAD_TRUNCATED_IMAGES = False + + def test_broken_datastream_with_errors(self): + if "zip_encoder" not in codecs: + self.skipTest("PNG (zlib) encoder not available") + + im = Image.open("Tests/images/broken_data_stream.png") + with self.assertRaises(IOError): + im.load() + + def test_broken_datastream_without_errors(self): + if "zip_encoder" not in codecs: + self.skipTest("PNG (zlib) encoder not available") + + im = Image.open("Tests/images/broken_data_stream.png") + + ImageFile.LOAD_TRUNCATED_IMAGES = True + try: + im.load() + finally: + ImageFile.LOAD_TRUNCATED_IMAGES = False if __name__ == '__main__': unittest.main() diff --git a/Tests/test_imagefileio.py b/Tests/test_imagefileio.py deleted file mode 100644 index b06178437..000000000 --- a/Tests/test_imagefileio.py +++ /dev/null @@ -1,33 +0,0 @@ -from helper import unittest, PillowTestCase, hopper, tostring - -from PIL import Image -from PIL import ImageFileIO - - -class TestImageFileIo(PillowTestCase): - - def test_fileio(self): - - class DumbFile(object): - def __init__(self, data): - self.data = data - - def read(self, bytes=None): - assert(bytes is None) - return self.data - - def close(self): - pass - - im1 = hopper() - - io = ImageFileIO.ImageFileIO(DumbFile(tostring(im1, "PPM"))) - - im2 = Image.open(io) - self.assert_image_equal(im1, im2) - - -if __name__ == '__main__': - unittest.main() - -# End of file diff --git a/Tests/test_imagefont.py b/Tests/test_imagefont.py index 88858c717..cc142d459 100644 --- a/Tests/test_imagefont.py +++ b/Tests/test_imagefont.py @@ -10,6 +10,8 @@ import copy FONT_PATH = "Tests/fonts/FreeMono.ttf" FONT_SIZE = 20 +TEST_TEXT = "hey you\nyou are awesome\nthis looks awkward" + try: from PIL import ImageFont @@ -86,16 +88,11 @@ try: self._render(f) self._clean() - def test_font_old_parameters(self): - self.assert_warning( - DeprecationWarning, - lambda: ImageFont.truetype(filename=FONT_PATH, size=FONT_SIZE)) - def _render(self, font): txt = "Hello World!" ttf = ImageFont.truetype(font, FONT_SIZE) ttf.getsize(txt) - + img = Image.new("RGB", (256, 64), "white") d = ImageDraw.Draw(img) d.text((10, 10), txt, font=ttf, fill='black') @@ -124,6 +121,7 @@ try: size = draw.textsize(txt, ttf) draw.text((10, 10), txt, font=ttf) draw.rectangle((10, 10, 10 + size[0], 10 + size[1])) + del draw target = 'Tests/images/rectangle_surrounding_text.png' target_img = Image.open(target) @@ -134,7 +132,7 @@ try: draw = ImageDraw.Draw(im) ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) line_spacing = draw.textsize('A', font=ttf)[1] + 4 - lines = ['hey you', 'you are awesome', 'this looks awkward'] + lines = TEST_TEXT.split("\n") y = 0 for line in lines: draw.text((0, y), line, font=ttf) @@ -148,6 +146,89 @@ try: # at epsilon = ~38. self.assert_image_similar(im, target_img, .5) + def test_render_multiline_text(self): + ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) + + # Test that text() correctly connects to multiline_text() + # and that align defaults to left + im = Image.new(mode='RGB', size=(300, 100)) + draw = ImageDraw.Draw(im) + draw.text((0, 0), TEST_TEXT, font=ttf) + + target = 'Tests/images/multiline_text.png' + target_img = Image.open(target) + + self.assert_image_similar(im, target_img, .5) + + # Test that text() can pass on additional arguments + # to multiline_text() + draw.text((0, 0), TEST_TEXT, fill=None, font=ttf, anchor=None, + spacing=4, align="left") + draw.text((0, 0), TEST_TEXT, None, ttf, None, 4, "left") + del draw + + # Test align center and right + for align, ext in {"center": "_center", + "right": "_right"}.items(): + im = Image.new(mode='RGB', size=(300, 100)) + draw = ImageDraw.Draw(im) + draw.multiline_text((0, 0), TEST_TEXT, font=ttf, align=align) + del draw + + target = 'Tests/images/multiline_text'+ext+'.png' + target_img = Image.open(target) + + self.assert_image_similar(im, target_img, .5) + + def test_unknown_align(self): + im = Image.new(mode='RGB', size=(300, 100)) + draw = ImageDraw.Draw(im) + ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) + + # Act/Assert + self.assertRaises(AssertionError, + lambda: draw.multiline_text((0, 0), TEST_TEXT, + font=ttf, + align="unknown")) + + def test_multiline_size(self): + ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) + im = Image.new(mode='RGB', size=(300, 100)) + draw = ImageDraw.Draw(im) + + # Test that textsize() correctly connects to multiline_textsize() + self.assertEqual(draw.textsize(TEST_TEXT, font=ttf), + draw.multiline_textsize(TEST_TEXT, font=ttf)) + + # Test that textsize() can pass on additional arguments + # to multiline_textsize() + draw.textsize(TEST_TEXT, font=ttf, spacing=4) + draw.textsize(TEST_TEXT, ttf, 4) + del draw + + def test_multiline_width(self): + ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) + im = Image.new(mode='RGB', size=(300, 100)) + draw = ImageDraw.Draw(im) + + self.assertEqual(draw.textsize("longest line", font=ttf)[0], + draw.multiline_textsize("longest line\nline", + font=ttf)[0]) + del draw + + def test_multiline_spacing(self): + ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) + + im = Image.new(mode='RGB', size=(300, 100)) + draw = ImageDraw.Draw(im) + draw.multiline_text((0, 0), TEST_TEXT, font=ttf, spacing=10) + del draw + + target = 'Tests/images/multiline_text_spacing.png' + target_img = Image.open(target) + + self.assert_image_similar(im, target_img, .5) + def test_rotated_transposed_font(self): img_grey = Image.new("L", (100, 100)) draw = ImageDraw.Draw(img_grey) @@ -159,12 +240,13 @@ try: font, orientation=orientation) # Original font - draw.setfont(font) + draw.font = font box_size_a = draw.textsize(word) # Rotated font - draw.setfont(transposed_font) + draw.font = transposed_font box_size_b = draw.textsize(word) + del draw # Check (w,h) of box a is (h,w) of box b self.assertEqual(box_size_a[0], box_size_b[1]) @@ -181,16 +263,45 @@ try: font, orientation=orientation) # Original font - draw.setfont(font) + draw.font = font box_size_a = draw.textsize(word) # Rotated font - draw.setfont(transposed_font) + draw.font = transposed_font box_size_b = draw.textsize(word) + del draw # Check boxes a and b are same size self.assertEqual(box_size_a, box_size_b) + def test_rotated_transposed_font_get_mask(self): + # Arrange + text = "mask this" + font = ImageFont.truetype(FONT_PATH, FONT_SIZE) + orientation = Image.ROTATE_90 + transposed_font = ImageFont.TransposedFont( + font, orientation=orientation) + + # Act + mask = transposed_font.getmask(text) + + # Assert + self.assertEqual(mask.size, (13, 108)) + + def test_unrotated_transposed_font_get_mask(self): + # Arrange + text = "mask this" + font = ImageFont.truetype(FONT_PATH, FONT_SIZE) + orientation = None + transposed_font = ImageFont.TransposedFont( + font, orientation=orientation) + + # Act + mask = transposed_font.getmask(text) + + # Assert + self.assertEqual(mask.size, (108, 13)) + def test_free_type_font_get_name(self): # Arrange font = ImageFont.truetype(FONT_PATH, FONT_SIZE) @@ -213,6 +324,28 @@ try: self.assertIsInstance(descent, int) self.assertEqual((ascent, descent), (16, 4)) # too exact check? + def test_free_type_font_get_offset(self): + # Arrange + font = ImageFont.truetype(FONT_PATH, FONT_SIZE) + text = "offset this" + + # Act + offset = font.getoffset(text) + + # Assert + self.assertEqual(offset, (0, 3)) + + def test_free_type_font_get_mask(self): + # Arrange + font = ImageFont.truetype(FONT_PATH, FONT_SIZE) + text = "mask this" + + # Act + mask = font.getmask(text) + + # Assert + self.assertEqual(mask.size, (108, 13)) + def test_load_path_not_found(self): # Arrange filename = "somefilenamethatdoesntexist.ttf" @@ -232,6 +365,7 @@ try: # Act default_font = ImageFont.load_default() draw.text((10, 10), txt, font=default_font) + del draw # Assert self.assert_image_equal(im, target_img) @@ -309,6 +443,24 @@ try: self._test_fake_loading_font( font_directory+'/Duplicate.ttf', 'Duplicate') + def test_imagefont_getters(self): + # Arrange + t = ImageFont.truetype(FONT_PATH, FONT_SIZE) + + # Act / Assert + self.assertEqual(t.getmetrics(), (16, 4)) + self.assertEqual(t.font.ascent, 16) + self.assertEqual(t.font.descent, 4) + self.assertEqual(t.font.height, 20) + self.assertEqual(t.font.x_ppem, 20) + self.assertEqual(t.font.y_ppem, 20) + self.assertEqual(t.font.glyphs, 4177) + self.assertEqual(t.getsize('A'), (12, 16)) + self.assertEqual(t.getsize('AB'), (24, 16)) + self.assertEqual(t.getsize('M'), (12, 16)) + self.assertEqual(t.getsize('y'), (12, 20)) + self.assertEqual(t.getsize('a'), (12, 16)) + except ImportError: class TestImageFont(PillowTestCase): diff --git a/Tests/test_imagefont_bitmap.py b/Tests/test_imagefont_bitmap.py index 27141f4b3..7ee5f8a2b 100644 --- a/Tests/test_imagefont_bitmap.py +++ b/Tests/test_imagefont_bitmap.py @@ -2,19 +2,32 @@ from helper import unittest, PillowTestCase from PIL import Image, ImageFont, ImageDraw +image_font_installed = True +try: + ImageFont.core.getfont +except ImportError: + image_font_installed = False + + +@unittest.skipIf(not image_font_installed, "image font not installed") class TestImageFontBitmap(PillowTestCase): def test_similar(self): text = 'EmbeddedBitmap' - font_outline = ImageFont.truetype(font='Tests/fonts/DejaVuSans.ttf', size=24) - font_bitmap = ImageFont.truetype(font='Tests/fonts/DejaVuSans-bitmap.ttf', size=24) - size_outline, size_bitmap = font_outline.getsize(text), font_bitmap.getsize(text) + font_outline = ImageFont.truetype( + font='Tests/fonts/DejaVuSans.ttf', size=24) + font_bitmap = ImageFont.truetype( + font='Tests/fonts/DejaVuSans-bitmap.ttf', size=24) + size_outline = font_outline.getsize(text) + size_bitmap = font_bitmap.getsize(text) size_final = max(size_outline[0], size_bitmap[0]), max(size_outline[1], size_bitmap[1]) im_bitmap = Image.new('RGB', size_final, (255, 255, 255)) im_outline = im_bitmap.copy() - draw_bitmap, draw_outline = ImageDraw.Draw(im_bitmap), ImageDraw.Draw(im_outline) + draw_bitmap = ImageDraw.Draw(im_bitmap) + draw_outline = ImageDraw.Draw(im_outline) - # Metrics are different on the bitmap and ttf fonts, more so on some platforms - # and versions of freetype than others. Mac has a 1px difference, linux doesn't. + # Metrics are different on the bitmap and ttf fonts, + # more so on some platforms and versions of freetype than others. + # Mac has a 1px difference, linux doesn't. draw_bitmap.text((0, size_final[1] - size_bitmap[1]), text, fill=(0, 0, 0), font=font_bitmap) draw_outline.text((0, size_final[1] - size_outline[1]), diff --git a/Tests/test_imagegrab.py b/Tests/test_imagegrab.py index ea6b499b2..bdc9edfad 100644 --- a/Tests/test_imagegrab.py +++ b/Tests/test_imagegrab.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase +from helper import unittest, PillowTestCase, on_appveyor import sys @@ -7,10 +7,12 @@ try: class TestImageGrab(PillowTestCase): + @unittest.skipIf(on_appveyor(), "Test fails on appveyor") def test_grab(self): im = ImageGrab.grab() self.assert_image(im, im.mode, im.size) + @unittest.skipIf(on_appveyor(), "Test fails on appveyor") def test_grab2(self): im = ImageGrab.grab() self.assert_image(im, im.mode, im.size) @@ -35,11 +37,12 @@ class TestImageGrabImport(PillowTestCase): exception = e # Assert - if sys.platform == 'win32': + if sys.platform in ["win32", "darwin"]: self.assertIsNone(exception, None) else: self.assertIsInstance(exception, ImportError) - self.assertEqual(str(exception), "ImageGrab is Windows only") + self.assertEqual(str(exception), + "ImageGrab is OS X and Windows only") if __name__ == '__main__': diff --git a/Tests/test_imagemorph.py b/Tests/test_imagemorph.py index bbb3ae190..a3d1dd8b1 100644 --- a/Tests/test_imagemorph.py +++ b/Tests/test_imagemorph.py @@ -1,5 +1,5 @@ # Test the ImageMorphology functionality -from helper import unittest, PillowTestCase +from helper import unittest, PillowTestCase, hopper from PIL import Image from PIL import ImageMorph @@ -168,6 +168,14 @@ class MorphTests(PillowTestCase): self.assertEqual(len(coords), 4) self.assertEqual(tuple(coords), ((2, 2), (4, 2), (2, 4), (4, 4))) + def test_non_binary_images(self): + im = hopper('RGB') + mop = ImageMorph.MorphOp(op_name="erosion8") + + self.assertRaises(Exception, lambda: mop.apply(im)) + self.assertRaises(Exception, lambda: mop.match(im)) + self.assertRaises(Exception, lambda: mop.get_on_pixels(im)) + if __name__ == '__main__': unittest.main() diff --git a/Tests/test_imageops_usm.py b/Tests/test_imageops_usm.py index f6eae640b..ca567e38e 100644 --- a/Tests/test_imageops_usm.py +++ b/Tests/test_imageops_usm.py @@ -24,13 +24,13 @@ class TestImageOpsUsm(PillowTestCase): def test_filter_api(self): - filter = ImageFilter.GaussianBlur(2.0) - i = im.filter(filter) + test_filter = ImageFilter.GaussianBlur(2.0) + i = im.filter(test_filter) self.assertEqual(i.mode, "RGB") self.assertEqual(i.size, (128, 128)) - filter = ImageFilter.UnsharpMask(2.0, 125, 8) - i = im.filter(filter) + test_filter = ImageFilter.UnsharpMask(2.0, 125, 8) + i = im.filter(test_filter) self.assertEqual(i.mode, "RGB") self.assertEqual(i.size, (128, 128)) @@ -75,7 +75,9 @@ class TestImageOpsUsm(PillowTestCase): (4, 3, 2), (4, 2, 2)]: self.assertGreaterEqual(i.im.getpixel((x, y))[c], 250) # Fuzzy match. - gp = lambda x, y: i.im.getpixel((x, y)) + + def gp(x, y): + return i.im.getpixel((x, y)) self.assertTrue(236 <= gp(7, 4)[0] <= 239) self.assertTrue(236 <= gp(7, 5)[2] <= 239) self.assertTrue(236 <= gp(7, 6)[2] <= 239) diff --git a/Tests/test_imagepalette.py b/Tests/test_imagepalette.py index 707ab4080..a175f7085 100644 --- a/Tests/test_imagepalette.py +++ b/Tests/test_imagepalette.py @@ -90,27 +90,6 @@ class TestImagePalette(PillowTestCase): self.assertEqual(lut[191], 60) self.assertEqual(lut[255], 255) - def test_private_make_linear_lut_warning(self): - # Arrange - from PIL.ImagePalette import _make_linear_lut - black = 0 - white = 255 - - # Act / Assert - self.assert_warning( - DeprecationWarning, - lambda: _make_linear_lut(black, white)) - - def test_private_make_gamma_lut_warning(self): - # Arrange - from PIL.ImagePalette import _make_gamma_lut - exp = 5 - - # Act / Assert - self.assert_warning( - DeprecationWarning, - lambda: _make_gamma_lut(exp)) - def test_rawmode_valueerrors(self): # Arrange from PIL.ImagePalette import raw diff --git a/Tests/test_imagepath.py b/Tests/test_imagepath.py index cd221b5ca..0354b0fd9 100644 --- a/Tests/test_imagepath.py +++ b/Tests/test_imagepath.py @@ -1,9 +1,9 @@ from helper import unittest, PillowTestCase -from PIL import ImagePath +from PIL import ImagePath, Image import array - +import struct class TestImagePath(PillowTestCase): @@ -62,6 +62,40 @@ class TestImagePath(PillowTestCase): self.assertEqual(list(p), [(0.0, 1.0)]) + def test_overflow_segfault(self): + try: + # post patch, this fails with a memory error + x = evil() + + # This fails due to the invalid malloc above, + # and segfaults + for i in range(200000): + if str is bytes: + x[i] = "0"*16 + else: + x[i] = b'0'*16 + except TypeError as msg: + # Some pythons fail getting the argument as an integer, and + # it falls through to the sequence. Seeing this on 32bit windows. + self.assertTrue(True, "Sequence required") + except MemoryError as msg: + self.assertTrue(msg) + except: + self.assertTrue(False, "Should have received a memory error") + + +class evil: + def __init__(self): + self.corrupt = Image.core.path(0x4000000000000000) + + def __getitem__(self, i): + x = self.corrupt[i] + return struct.pack("dd", x[0], x[1]) + + def __setitem__(self, i, x): + self.corrupt[i] = struct.unpack("dd", x) + + if __name__ == '__main__': unittest.main() diff --git a/Tests/test_imageqt.py b/Tests/test_imageqt.py index 7d57ed1d2..b0fad6a45 100644 --- a/Tests/test_imageqt.py +++ b/Tests/test_imageqt.py @@ -1,38 +1,61 @@ from helper import unittest, PillowTestCase, hopper -try: - from PIL import ImageQt - from PyQt5.QtGui import QImage, qRgb, qRgba -except: - try: - from PyQt4.QtGui import QImage, qRgb, qRgba - except: - try: - from PySide.QtGui import QImage, qRgb, qRgba - except: - # Will be skipped in setUp - pass +from PIL import ImageQt -class TestImageQt(PillowTestCase): +if ImageQt.qt_is_installed: + from PIL.ImageQt import qRgba + + def skip_if_qt_is_not_installed(_): + pass +else: + def skip_if_qt_is_not_installed(test_case): + test_case.skipTest('Qt bindings are not installed') + + +class PillowQtTestCase(object): def setUp(self): + skip_if_qt_is_not_installed(self) + + def tearDown(self): + pass + + +class PillowQPixmapTestCase(PillowQtTestCase): + + def setUp(self): + PillowQtTestCase.setUp(self) try: - from PyQt5.QtGui import QImage, qRgb, qRgba - except: - try: - from PyQt4.QtGui import QImage, qRgb, qRgba - except: - try: - from PySide.QtGui import QImage, qRgb, qRgba - except: - self.skipTest('PyQt4 or 5 or PySide not installed') + if ImageQt.qt_version == '5': + from PyQt5.QtGui import QGuiApplication + elif ImageQt.qt_version == '4': + from PyQt4.QtGui import QGuiApplication + elif ImageQt.qt_version == 'side': + from PySide.QtGui import QGuiApplication + except ImportError: + self.skipTest('QGuiApplication not installed') + + self.app = QGuiApplication([]) + + def tearDown(self): + PillowQtTestCase.tearDown(self) + self.app.quit() + + +class TestImageQt(PillowQtTestCase, PillowTestCase): def test_rgb(self): - # from https://qt-project.org/doc/qt-4.8/qcolor.html + # from https://doc.qt.io/qt-4.8/qcolor.html # typedef QRgb # An ARGB quadruplet on the format #AARRGGBB, # equivalent to an unsigned int. + if ImageQt.qt_version == '5': + from PyQt5.QtGui import qRgb + elif ImageQt.qt_version == '4': + from PyQt4.QtGui import qRgb + elif ImageQt.qt_version == 'side': + from PySide.QtGui import qRgb self.assertEqual(qRgb(0, 0, 0), qRgba(0, 0, 0, 255)) diff --git a/Tests/test_imagesequence.py b/Tests/test_imagesequence.py index 1b4bb3c02..1cd98ddf1 100644 --- a/Tests/test_imagesequence.py +++ b/Tests/test_imagesequence.py @@ -22,19 +22,25 @@ class TestImageSequence(PillowTestCase): self.assertEqual(index, 1) - def _test_multipage_tiff(self, dbg=False): - # debug had side effect of calling fp.tell. - Image.DEBUG = dbg + self.assertRaises(AttributeError, lambda: ImageSequence.Iterator(0)) + + def test_iterator(self): + im = Image.open('Tests/images/multipage.tiff') + i = ImageSequence.Iterator(im) + for index in range(0, im.n_frames): + self.assertEqual(i[index], next(i)) + self.assertRaises(IndexError, lambda: i[index+1]) + self.assertRaises(StopIteration, lambda: next(i)) + + def _test_multipage_tiff(self): im = Image.open('Tests/images/multipage.tiff') for index, frame in enumerate(ImageSequence.Iterator(im)): frame.load() self.assertEqual(index, im.tell()) frame.convert('RGB') - Image.DEBUG = False def test_tiff(self): - # self._test_multipage_tiff(True) - self._test_multipage_tiff(False) + self._test_multipage_tiff() def test_libtiff(self): codecs = dir(Image.core) @@ -43,10 +49,20 @@ class TestImageSequence(PillowTestCase): self.skipTest("tiff support not available") TiffImagePlugin.READ_LIBTIFF = True - # self._test_multipage_tiff(True) - self._test_multipage_tiff(False) + self._test_multipage_tiff() TiffImagePlugin.READ_LIBTIFF = False + def test_consecutive(self): + im = Image.open('Tests/images/multipage.tiff') + firstFrame = None + for frame in ImageSequence.Iterator(im): + if firstFrame == None: + firstFrame = frame.copy() + pass + for frame in ImageSequence.Iterator(im): + self.assert_image_equal(frame, firstFrame) + break + if __name__ == '__main__': unittest.main() diff --git a/Tests/test_imagewin.py b/Tests/test_imagewin.py index 7ceea86ee..c4a8b28e4 100644 --- a/Tests/test_imagewin.py +++ b/Tests/test_imagewin.py @@ -107,15 +107,14 @@ class TestImageWinDib(PillowTestCase): # Confirm they're the same self.assertEqual(dib1.tobytes(), dib2.tobytes()) - def test_dib_fromstring_tostring_deprecated(self): + def test_removed_methods(self): # Arrange im = hopper() dib = ImageWin.Dib(im) - test_buffer = dib.tobytes() # Act/Assert - self.assert_warning(DeprecationWarning, dib.tostring) - self.assert_warning(DeprecationWarning, lambda: dib.fromstring(test_buffer)) + self.assertRaises(Exception, dib.tostring) + self.assertRaises(Exception, dib.fromstring) if __name__ == '__main__': diff --git a/Tests/test_imagewin_pointers.py b/Tests/test_imagewin_pointers.py new file mode 100644 index 000000000..692868bdf --- /dev/null +++ b/Tests/test_imagewin_pointers.py @@ -0,0 +1,113 @@ +from helper import unittest, PillowTestCase, hopper +from PIL import Image, ImageWin + +import sys +import ctypes +from io import BytesIO + +# see https://github.com/python-pillow/Pillow/pull/1431#issuecomment-144692652 + +if sys.platform.startswith('win32'): + import ctypes.wintypes + + class BITMAPFILEHEADER(ctypes.Structure): + _pack_ = 2 + _fields_ = [ + ('bfType', ctypes.wintypes.WORD), + ('bfSize', ctypes.wintypes.DWORD), + ('bfReserved1', ctypes.wintypes.WORD), + ('bfReserved2', ctypes.wintypes.WORD), + ('bfOffBits', ctypes.wintypes.DWORD), + ] + + class BITMAPINFOHEADER(ctypes.Structure): + _pack_ = 2 + _fields_ = [ + ('biSize', ctypes.wintypes.DWORD), + ('biWidth', ctypes.wintypes.LONG), + ('biHeight', ctypes.wintypes.LONG), + ('biPlanes', ctypes.wintypes.WORD), + ('biBitCount', ctypes.wintypes.WORD), + ('biCompression', ctypes.wintypes.DWORD), + ('biSizeImage', ctypes.wintypes.DWORD), + ('biXPelsPerMeter', ctypes.wintypes.LONG), + ('biYPelsPerMeter', ctypes.wintypes.LONG), + ('biClrUsed', ctypes.wintypes.DWORD), + ('biClrImportant', ctypes.wintypes.DWORD), + ] + + BI_RGB = 0 + DIB_RGB_COLORS = 0 + + memcpy = ctypes.cdll.msvcrt.memcpy + memcpy.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.c_size_t] + + CreateCompatibleDC = ctypes.windll.gdi32.CreateCompatibleDC + CreateCompatibleDC.argtypes = [ctypes.wintypes.HDC] + CreateCompatibleDC.restype = ctypes.wintypes.HDC + + DeleteDC = ctypes.windll.gdi32.DeleteDC + DeleteDC.argtypes = [ctypes.wintypes.HDC] + + SelectObject = ctypes.windll.gdi32.SelectObject + SelectObject.argtypes = [ctypes.wintypes.HDC, ctypes.wintypes.HGDIOBJ] + SelectObject.restype = ctypes.wintypes.HGDIOBJ + + DeleteObject = ctypes.windll.gdi32.DeleteObject + DeleteObject.argtypes = [ctypes.wintypes.HGDIOBJ] + + CreateDIBSection = ctypes.windll.gdi32.CreateDIBSection + CreateDIBSection.argtypes = [ctypes.wintypes.HDC, ctypes.c_void_p, ctypes.c_uint, ctypes.POINTER(ctypes.c_void_p), ctypes.wintypes.HANDLE, ctypes.wintypes.DWORD] + CreateDIBSection.restype = ctypes.wintypes.HBITMAP + + def serialize_dib(bi, pixels): + bf = BITMAPFILEHEADER() + bf.bfType = 0x4d42 + bf.bfOffBits = ctypes.sizeof(bf) + bi.biSize + bf.bfSize = bf.bfOffBits + bi.biSizeImage + bf.bfReserved1 = bf.bfReserved2 = 0 + + buf = (ctypes.c_byte * bf.bfSize)() + bp = ctypes.addressof(buf) + memcpy(bp, ctypes.byref(bf), ctypes.sizeof(bf)) + memcpy(bp + ctypes.sizeof(bf), ctypes.byref(bi), bi.biSize) + memcpy(bp + bf.bfOffBits, pixels, bi.biSizeImage) + try: + return bytearray(buf) + except ValueError: + # py2.6 + return buffer(buf)[:] + + class TestImageWinPointers(PillowTestCase): + def test_pointer(self): + im = hopper() + (width, height) = im.size + opath = self.tempfile('temp.png') + imdib = ImageWin.Dib(im) + + hdr = BITMAPINFOHEADER() + hdr.biSize = ctypes.sizeof(hdr) + hdr.biWidth = width + hdr.biHeight = height + hdr.biPlanes = 1 + hdr.biBitCount = 32 + hdr.biCompression = BI_RGB + hdr.biSizeImage = width * height * 4 + hdr.biClrUsed = 0 + hdr.biClrImportant = 0 + + hdc = CreateCompatibleDC(None) + # print('hdc:',hex(hdc)) + pixels = ctypes.c_void_p() + dib = CreateDIBSection(hdc, ctypes.byref(hdr), DIB_RGB_COLORS, ctypes.byref(pixels), None, 0) + SelectObject(hdc, dib) + + imdib.expose(hdc) + bitmap = serialize_dib(hdr, pixels) + DeleteObject(dib) + DeleteDC(hdc) + + Image.open(BytesIO(bitmap)).save(opath) + +if __name__ == '__main__': + unittest.main() diff --git a/Tests/test_j2k_overflow.py b/Tests/test_j2k_overflow.py new file mode 100644 index 000000000..de671a53f --- /dev/null +++ b/Tests/test_j2k_overflow.py @@ -0,0 +1,18 @@ +from PIL import Image +from helper import unittest, PillowTestCase + +class TestJ2kEncodeOverflow(PillowTestCase): + def test_j2k_overflow(self): + + im = Image.new('RGBA', (1024, 131584)) + target = self.tempfile('temp.jpc') + try: + im.save(target) + self.assertTrue(False, "Expected IOError, save succeeded?") + except IOError as err: + self.assertTrue(True, "IOError is expected") + except Exception as err: + self.assertTrue(False, "Expected IOError, got %s" %type(err)) + +if __name__ == '__main__': + unittest.main() diff --git a/Tests/test_numpy.py b/Tests/test_numpy.py index 19b9a2014..4f2a5afb1 100644 --- a/Tests/test_numpy.py +++ b/Tests/test_numpy.py @@ -22,9 +22,9 @@ class TestNumpy(PillowTestCase): def test_numpy_to_image(self): - def to_image(dtype, bands=1, bool=0): + def to_image(dtype, bands=1, boolean=0): if bands == 1: - if bool: + if boolean: data = [0, 1] * 50 else: data = list(range(100)) @@ -43,8 +43,8 @@ class TestNumpy(PillowTestCase): # print dtype, list(i.getdata()) return i - # self.assert_image(to_image(numpy.bool, bool=1), "1", (10, 10)) - # self.assert_image(to_image(numpy.bool8, bool=1), "1", (10, 10)) + # self.assert_image(to_image(numpy.bool, boolean=1), "1", (10, 10)) + # self.assert_image(to_image(numpy.bool8, boolean=1), "1", (10, 10)) self.assertRaises(TypeError, lambda: to_image(numpy.uint)) self.assert_image(to_image(numpy.uint8), "L", (10, 10)) @@ -67,7 +67,7 @@ class TestNumpy(PillowTestCase): self.assert_image(to_image(numpy.uint8, 3), "RGB", (10, 10)) self.assert_image(to_image(numpy.uint8, 4), "RGBA", (10, 10)) - # based on an erring example at http://is.gd/6F0esS (which resolves to) + # based on an erring example at # http://stackoverflow.com/questions/10854903/what-is-causing-dimension-dependent-attributeerror-in-pil-fromarray-function def test_3d_array(self): a = numpy.ones((10, 10, 10), dtype=numpy.uint8) @@ -76,7 +76,8 @@ class TestNumpy(PillowTestCase): self.assert_image(Image.fromarray(a[:, :, 1]), "L", (10, 10)) def _test_img_equals_nparray(self, img, np): - self.assertEqual(img.size, np.shape[0:2]) + np_size = np.shape[1], np.shape[0] + self.assertEqual(img.size, np_size) px = img.load() for x in range(0, img.size[0], int(img.size[0]/10)): for y in range(0, img.size[1], int(img.size[1]/10)): @@ -92,6 +93,11 @@ class TestNumpy(PillowTestCase): def _to_array(mode, dtype): img = hopper(mode) + + # Resize to non-square + img = img.crop((3, 0, 124, 127)) + self.assertEqual(img.size, (121, 127)) + np_img = numpy.array(img) self._test_img_equals_nparray(img, np_img) self.assertEqual(np_img.dtype, numpy.dtype(dtype)) @@ -107,6 +113,7 @@ class TestNumpy(PillowTestCase): ("I;16", 'u2'), ("I;16L", 'pixelsize; #endif -#if TK < 84 /* < 8.4.0 */ - if (strcmp(im->mode, "RGBA") == 0) { - /* Copy non-transparent pixels to photo image */ - int x, y; - Tk_PhotoImageBlock run; - - /* Clear current contents */ - Tk_PhotoBlank(photo); - - /* Setup run descriptor */ - run.height = 1; - run.pitch = block.pitch; - run.pixelSize = block.pixelSize; - run.offset[0] = 0; - run.offset[1] = 1; - run.offset[2] = 2; - run.offset[3] = 0; /* no alpha (or reserved, under 8.2) */ - - /* Copy opaque runs to photo image */ - for (y = 0; y < block.height; y++) { - unsigned char* p = block.pixelPtr + y*block.pitch; - unsigned char* s = p; - int w = 0; - for (x = 0; x < block.width; x++) { - if (p[3]) { - /* opaque: add pixel to current run */ - if (w == 0) - s = p; - w = w + 1; - } else if (s) { - /* copy run to photo image */ - if (w > 0) { - run.width = w; - run.pixelPtr = s; - Tk_PhotoPutBlock(photo, &run, x-w, y, run.width, 1); - } - w = 0; - } - p += block.pixelSize; - } - if (w > 0) { - /* copy final run, if any */ - run.width = w; - run.pixelPtr = s; - Tk_PhotoPutBlock(photo, &run, x-w, y, run.width, 1); - } - } - - } else - - /* Copy opaque block to photo image, and leave the rest to TK */ - Tk_PhotoPutBlock(photo, &block, 0, 0, block.width, block.height); - -#else /* Tk 8.4 and newer */ #if TK < 85 /* Tk 8.4 */ Tk_PhotoPutBlock(photo, &block, 0, 0, block.width, block.height, TK_PHOTO_COMPOSITE_SET); @@ -207,7 +157,6 @@ PyImagingPhotoPut(ClientData clientdata, Tcl_Interp* interp, #else /* Tk 8.5 */ Tk_PhotoPutBlock(interp, photo, &block, 0, 0, block.width, block.height, TK_PHOTO_COMPOSITE_SET); -#endif #endif return TCL_OK; diff --git a/_imaging.c b/_imaging.c index 09345c0dd..aaa11d8d9 100644 --- a/_imaging.c +++ b/_imaging.c @@ -71,7 +71,7 @@ * See the README file for information on usage and redistribution. */ -#define PILLOW_VERSION "2.9.0.dev0" +#define PILLOW_VERSION "3.3.0.dev0" #include "Python.h" @@ -98,9 +98,6 @@ #define WITH_DEBUG /* extra debugging interfaces */ -/* PIL Plus extensions */ -#undef WITH_CRACKCODE /* pil plus */ - #undef VERBOSE #define CLIP(x) ((x) <= 0 ? 0 : (x) < 256 ? (x) : 255) @@ -370,7 +367,7 @@ getlist(PyObject* arg, int* length, const char* wrong_length, int type) void* list; PyObject* seq; PyObject* op; - + if (!PySequence_Check(arg)) { PyErr_SetString(PyExc_TypeError, must_be_sequence); return NULL; @@ -392,11 +389,11 @@ getlist(PyObject* arg, int* length, const char* wrong_length, int type) PyErr_SetString(PyExc_TypeError, must_be_sequence); return NULL; } - + for (i = 0; i < n; i++) { op = PySequence_Fast_GET_ITEM(seq, i); - // DRY, branch prediction is going to work _really_ well - // on this switch. And 3 fewer loops to copy/paste. + // DRY, branch prediction is going to work _really_ well + // on this switch. And 3 fewer loops to copy/paste. switch (type) { case TYPE_UINT8: itemp = PyInt_AsLong(op); @@ -474,35 +471,53 @@ getpixel(Imaging im, ImagingAccess access, int x, int y) static char* getink(PyObject* color, Imaging im, char* ink) { - int r, g, b, a; - double f; + int g=0, b=0, a=0; + double f=0; + /* Windows 64 bit longs are 32 bits, and 0xFFFFFFFF (white) is a + python long (not int) that raises an overflow error when trying + to return it into a 32 bit C long + */ + PY_LONG_LONG r = 0; /* fill ink buffer (four bytes) with something that can be cast to either UINT8 or INT32 */ + int rIsInt = 0; + if (im->type == IMAGING_TYPE_UINT8 || + im->type == IMAGING_TYPE_INT32 || + im->type == IMAGING_TYPE_SPECIAL) { +#if PY_VERSION_HEX >= 0x03000000 + if (PyLong_Check(color)) { + r = PyLong_AsLongLong(color); +#else + if (PyInt_Check(color) || PyLong_Check(color)) { + if (PyInt_Check(color)) + r = PyInt_AS_LONG(color); + else + r = PyLong_AsLongLong(color); +#endif + rIsInt = 1; + } + if (r == -1 && PyErr_Occurred()) { + rIsInt = 0; + } + } + switch (im->type) { case IMAGING_TYPE_UINT8: /* unsigned integer */ if (im->bands == 1) { /* unsigned integer, single layer */ - r = PyInt_AsLong(color); - if (r == -1 && PyErr_Occurred()) - return NULL; + if (rIsInt != 1) { + if (!PyArg_ParseTuple(color, "i", &r)) { + return NULL; + } + } ink[0] = CLIP(r); ink[1] = ink[2] = ink[3] = 0; } else { a = 255; -#if PY_VERSION_HEX >= 0x03000000 - if (PyLong_Check(color)) { - r = (int) PyLong_AsLong(color); -#else - if (PyInt_Check(color) || PyLong_Check(color)) { - if (PyInt_Check(color)) - r = PyInt_AS_LONG(color); - else - r = (int) PyLong_AsLong(color); -#endif - + if (rIsInt) { /* compatibility: ABGR */ a = (UINT8) (r >> 24); b = (UINT8) (r >> 16); @@ -526,8 +541,7 @@ getink(PyObject* color, Imaging im, char* ink) return ink; case IMAGING_TYPE_INT32: /* signed integer */ - r = PyInt_AsLong(color); - if (r == -1 && PyErr_Occurred()) + if (rIsInt != 1) return NULL; *(INT32*) ink = r; return ink; @@ -540,8 +554,7 @@ getink(PyObject* color, Imaging im, char* ink) return ink; case IMAGING_TYPE_SPECIAL: if (strncmp(im->mode, "I;16", 4) == 0) { - r = PyInt_AsLong(color); - if (r == -1 && PyErr_Occurred()) + if (rIsInt != 1) return NULL; ink[0] = (UINT8) r; ink[1] = (UINT8) (r >> 8); @@ -577,13 +590,14 @@ _fill(PyObject* self, PyObject* args) if (!im) return NULL; + buffer[0] = buffer[1] = buffer[2] = buffer[3] = 0; if (color) { if (!getink(color, im, buffer)) { ImagingDelete(im); return NULL; } - } else - buffer[0] = buffer[1] = buffer[2] = buffer[3] = 0; + } + (void) ImagingFill(im, buffer); @@ -752,11 +766,12 @@ _convert_matrix(ImagingObject* self, PyObject* args) float m[12]; if (!PyArg_ParseTuple(args, "s(ffff)", &mode, m+0, m+1, m+2, m+3)) { PyErr_Clear(); - if (!PyArg_ParseTuple(args, "s(ffffffffffff)", &mode, - m+0, m+1, m+2, m+3, - m+4, m+5, m+6, m+7, - m+8, m+9, m+10, m+11)) - return NULL; + if (!PyArg_ParseTuple(args, "s(ffffffffffff)", &mode, + m+0, m+1, m+2, m+3, + m+4, m+5, m+6, m+7, + m+8, m+9, m+10, m+11)){ + return NULL; + } } return PyImagingNew(ImagingConvertMatrix(self->image, mode, m)); @@ -1328,6 +1343,8 @@ _putdata(ImagingObject* self, PyObject* args) INT32 inkint; } u; + u.inkint = 0; + op = PySequence_Fast_GET_ITEM(seq, i); if (!op || !getink(op, image, u.ink)) { Py_DECREF(seq); @@ -1518,6 +1535,10 @@ _resize(ImagingObject* self, PyObject* args) imIn = self->image; + if (xsize < 1 || ysize < 1) { + return ImagingError_ValueError("height and width must be > 0"); + } + if (imIn->xsize == xsize && imIn->ysize == ysize) { imOut = ImagingCopy(imIn); } @@ -1550,7 +1571,8 @@ _rotate(ImagingObject* self, PyObject* args) double theta; int filter = IMAGING_TRANSFORM_NEAREST; - if (!PyArg_ParseTuple(args, "d|i", &theta, &filter)) + int expand; + if (!PyArg_ParseTuple(args, "d|i|i", &theta, &filter, &expand)) return NULL; imIn = self->image; @@ -1563,7 +1585,8 @@ _rotate(ImagingObject* self, PyObject* args) /* Rotate with resampling filter */ imOut = ImagingNew(imIn->mode, imIn->xsize, imIn->ysize); (void) ImagingRotate(imOut, imIn, theta, filter); - } else if (theta == 90.0 || theta == 270.0) { + } else if ((theta == 90.0 || theta == 270.0) + && (expand || imIn->xsize == imIn->ysize)) { /* Use fast version */ imOut = ImagingNew(imIn->mode, imIn->ysize, imIn->xsize); if (imOut) { @@ -2366,9 +2389,9 @@ _draw_arc(ImagingDrawObject* self, PyObject* args) PyObject* data; int ink; - int start, end; + float start, end; int op = 0; - if (!PyArg_ParseTuple(args, "Oiii|i", &data, &start, &end, &ink)) + if (!PyArg_ParseTuple(args, "Offi|i", &data, &start, &end, &ink)) return NULL; n = PyPath_Flatten(data, &xy); @@ -2440,8 +2463,8 @@ _draw_chord(ImagingDrawObject* self, PyObject* args) PyObject* data; int ink, fill; - int start, end; - if (!PyArg_ParseTuple(args, "Oiiii", + float start, end; + if (!PyArg_ParseTuple(args, "Offii", &data, &start, &end, &ink, &fill)) return NULL; @@ -2661,8 +2684,8 @@ _draw_pieslice(ImagingDrawObject* self, PyObject* args) PyObject* data; int ink, fill; - int start, end; - if (!PyArg_ParseTuple(args, "Oiiii", &data, &start, &end, &ink, &fill)) + float start, end; + if (!PyArg_ParseTuple(args, "Offii", &data, &start, &end, &ink, &fill)) return NULL; n = PyPath_Flatten(data, &xy); @@ -3011,9 +3034,6 @@ static struct PyMethodDef methods[] = { {"convert_transparent", (PyCFunction)_convert_transparent, 1}, {"copy", (PyCFunction)_copy, 1}, {"copy2", (PyCFunction)_copy2, 1}, -#ifdef WITH_CRACKCODE - {"crackcode", (PyCFunction)_crackcode, 1}, -#endif {"crop", (PyCFunction)_crop, 1}, {"expand", (PyCFunction)_expand_image, 1}, {"filter", (PyCFunction)_filter, 1}, @@ -3082,7 +3102,7 @@ static struct PyMethodDef methods[] = { {"unsharp_mask", (PyCFunction)_unsharp_mask, 1}, #endif - {"box_blur", (PyCFunction)_box_blur, 1}, + {"box_blur", (PyCFunction)_box_blur, 1}, #ifdef WITH_EFFECTS /* Special effects */ diff --git a/_imagingcms.c b/_imagingcms.c index cda7c5f1f..ad5b845e1 100644 --- a/_imagingcms.c +++ b/_imagingcms.c @@ -25,7 +25,11 @@ kevin@cazabon.com\n\ http://www.cazabon.com\n\ " +#include "wchar.h" + #include "Python.h" +#include "datetime.h" + #include "lcms2.h" #include "Imaging.h" #include "py3.h" @@ -49,7 +53,7 @@ http://www.cazabon.com\n\ /* known to-do list with current version: - Verify that PILmode->littleCMStype conversion in findLCMStype is correct for all + Verify that PILmode->littleCMStype conversion in findLCMStype is correct for all PIL modes (it probably isn't for the more obscure ones) Add support for creating custom RGB profiles on the fly @@ -454,7 +458,7 @@ createProfile(PyObject *self, PyObject *args) } else if (strcmp(sColorSpace, "XYZ") == 0) { hProfile = cmsCreateXYZProfile(); - } + } else if (strcmp(sColorSpace, "sRGB") == 0) { hProfile = cmsCreate_sRGBProfile(); } @@ -521,6 +525,288 @@ cms_get_display_profile_win32(PyObject* self, PyObject* args) } #endif +/* -------------------------------------------------------------------- */ +/* Helper functions. */ + +static PyObject* +_profile_read_mlu(CmsProfileObject* self, cmsTagSignature info) +{ + PyObject *uni; + char *lc = "en"; + char *cc = cmsNoCountry; + cmsMLU *mlu; + cmsUInt32Number len; + wchar_t *buf; + + if (!cmsIsTag(self->profile, info)) { + Py_INCREF(Py_None); + return Py_None; + } + + mlu = cmsReadTag(self->profile, info); + if (!mlu) { + Py_INCREF(Py_None); + return Py_None; + } + + len = cmsMLUgetWide(mlu, lc, cc, NULL, 0); + if (len == 0) { + Py_INCREF(Py_None); + return Py_None; + } + + buf = malloc(len); + if (!buf) { + PyErr_SetString(PyExc_IOError, "Out of Memory"); + return NULL; + } + /* Just in case the next call fails. */ + buf[0] = '\0'; + + cmsMLUgetWide(mlu, lc, cc, buf, len); + // buf contains additional junk after \0 + uni = PyUnicode_FromWideChar(buf, wcslen(buf)); + free(buf); + + return uni; +} + + +static PyObject* +_profile_read_int_as_string(cmsUInt32Number nr) +{ + PyObject* ret; + char buf[5]; + buf[0] = (char) ((nr >> 24) & 0xff); + buf[1] = (char) ((nr >> 16) & 0xff); + buf[2] = (char) ((nr >> 8) & 0xff); + buf[3] = (char) (nr & 0xff); + buf[4] = 0; + +#if PY_VERSION_HEX >= 0x03000000 + ret = PyUnicode_DecodeASCII(buf, 4, NULL); +#else + ret = PyString_FromStringAndSize(buf, 4); +#endif + return ret; +} + + +static PyObject* +_profile_read_signature(CmsProfileObject* self, cmsTagSignature info) +{ + unsigned int *sig; + + if (!cmsIsTag(self->profile, info)) { + Py_INCREF(Py_None); + return Py_None; + } + + sig = (unsigned int *) cmsReadTag(self->profile, info); + if (!sig) { + Py_INCREF(Py_None); + return Py_None; + } + + return _profile_read_int_as_string(*sig); +} + +static PyObject* +_xyz_py(cmsCIEXYZ* XYZ) +{ + cmsCIExyY xyY; + cmsXYZ2xyY(&xyY, XYZ); + return Py_BuildValue("((d,d,d),(d,d,d))", XYZ->X, XYZ->Y, XYZ->Z, xyY.x, xyY.y, xyY.Y); +} + +static PyObject* +_xyz3_py(cmsCIEXYZ* XYZ) +{ + cmsCIExyY xyY[3]; + cmsXYZ2xyY(&xyY[0], &XYZ[0]); + cmsXYZ2xyY(&xyY[1], &XYZ[1]); + cmsXYZ2xyY(&xyY[2], &XYZ[2]); + + return Py_BuildValue("(((d,d,d),(d,d,d),(d,d,d)),((d,d,d),(d,d,d),(d,d,d)))", + XYZ[0].X, XYZ[0].Y, XYZ[0].Z, + XYZ[1].X, XYZ[1].Y, XYZ[1].Z, + XYZ[2].X, XYZ[2].Y, XYZ[2].Z, + xyY[0].x, xyY[0].y, xyY[0].Y, + xyY[1].x, xyY[1].y, xyY[1].Y, + xyY[2].x, xyY[2].y, xyY[2].Y); +} + +static PyObject* +_profile_read_ciexyz(CmsProfileObject* self, cmsTagSignature info, int multi) +{ + cmsCIEXYZ* XYZ; + + if (!cmsIsTag(self->profile, info)) { + Py_INCREF(Py_None); + return Py_None; + } + + XYZ = (cmsCIEXYZ*) cmsReadTag(self->profile, info); + if (!XYZ) { + Py_INCREF(Py_None); + return Py_None; + } + if (multi) + return _xyz3_py(XYZ); + else + return _xyz_py(XYZ); +} + +static PyObject* +_profile_read_ciexyy_triple(CmsProfileObject* self, cmsTagSignature info) +{ + cmsCIExyYTRIPLE* triple; + + if (!cmsIsTag(self->profile, info)) { + Py_INCREF(Py_None); + return Py_None; + } + + triple = (cmsCIExyYTRIPLE*) cmsReadTag(self->profile, info); + if (!triple) { + Py_INCREF(Py_None); + return Py_None; + } + + /* Note: lcms does all the heavy lifting and error checking (nr of + channels == 3). */ + return Py_BuildValue("((d,d,d),(d,d,d),(d,d,d)),", + triple->Red.x, triple->Red.y, triple->Red.Y, + triple->Green.x, triple->Green.y, triple->Green.Y, + triple->Blue.x, triple->Blue.y, triple->Blue.Y); +} + +static PyObject* +_profile_read_named_color_list(CmsProfileObject* self, cmsTagSignature info) +{ + cmsNAMEDCOLORLIST* ncl; + int i, n; + char name[cmsMAX_PATH]; + PyObject* result; + + if (!cmsIsTag(self->profile, info)) { + Py_INCREF(Py_None); + return Py_None; + } + + ncl = (cmsNAMEDCOLORLIST*) cmsReadTag(self->profile, info); + if (ncl == NULL) { + Py_INCREF(Py_None); + return Py_None; + } + + n = cmsNamedColorCount(ncl); + result = PyList_New(n); + if (!result) { + Py_INCREF(Py_None); + return Py_None; + } + + for (i = 0; i < n; i++) { + PyObject* str; + cmsNamedColorInfo(ncl, i, name, NULL, NULL, NULL, NULL); + str = PyUnicode_FromString(name); + if (str == NULL) { + Py_DECREF(result); + Py_INCREF(Py_None); + return Py_None; + } + PyList_SET_ITEM(result, i, str); + } + + return result; +} + +static cmsBool _calculate_rgb_primaries(CmsProfileObject* self, cmsCIEXYZTRIPLE* result) +{ + double input[3][3] = { { 1, 0, 0 }, { 0, 1, 0 }, { 0, 0, 1 } }; + cmsHPROFILE hXYZ; + cmsHTRANSFORM hTransform; + + /* http://littlecms2.blogspot.com/2009/07/less-is-more.html */ + + // double array of RGB values with max on each identitiy + hXYZ = cmsCreateXYZProfile(); + if (hXYZ == NULL) + return 0; + + // transform from our profile to XYZ using doubles for highest precision + hTransform = cmsCreateTransform(self->profile, TYPE_RGB_DBL, + hXYZ, TYPE_XYZ_DBL, + INTENT_RELATIVE_COLORIMETRIC, + cmsFLAGS_NOCACHE | cmsFLAGS_NOOPTIMIZE); + cmsCloseProfile(hXYZ); + if (hTransform == NULL) + return 0; + + cmsDoTransform(hTransform, (void*) input, result, 3); + cmsDeleteTransform(hTransform); + return 1; +} + +static cmsBool _check_intent(int clut, cmsHPROFILE hProfile, cmsUInt32Number Intent, cmsUInt32Number UsedDirection) +{ + if (clut) { + return cmsIsCLUT(hProfile, Intent, UsedDirection); + } + else { + return cmsIsIntentSupported(hProfile, Intent, UsedDirection); + } +} + +#define INTENTS 200 + +static PyObject* +_is_intent_supported(CmsProfileObject* self, int clut) +{ + PyObject* result; + int n; + int i; + cmsUInt32Number intent_ids[INTENTS]; + char *intent_descs[INTENTS]; + + result = PyDict_New(); + if (result == NULL) { + Py_INCREF(Py_None); + return Py_None; + } + + + n = cmsGetSupportedIntents(INTENTS, + intent_ids, + intent_descs); + for (i = 0; i < n; i++) { + int intent = (int) intent_ids[i]; + PyObject* id; + PyObject* entry; + + /* Only valid for ICC Intents (otherwise we read invalid memory in lcms cmsio1.c). */ + if (!(intent == INTENT_PERCEPTUAL || intent == INTENT_RELATIVE_COLORIMETRIC + || intent == INTENT_SATURATION || intent == INTENT_ABSOLUTE_COLORIMETRIC)) + continue; + + id = PyInt_FromLong((long) intent); + entry = Py_BuildValue("(OOO)", + _check_intent(clut, self->profile, intent, LCMS_USED_AS_INPUT) ? Py_True : Py_False, + _check_intent(clut, self->profile, intent, LCMS_USED_AS_OUTPUT) ? Py_True : Py_False, + _check_intent(clut, self->profile, intent, LCMS_USED_AS_PROOF) ? Py_True : Py_False); + if (id == NULL || entry == NULL) { + Py_XDECREF(id); + Py_XDECREF(entry); + Py_XDECREF(result); + Py_INCREF(Py_None); + return Py_None; + } + PyDict_SetItem(result, id, entry); + } + return result; +} + /* -------------------------------------------------------------------- */ /* Python interface setup */ @@ -554,10 +840,10 @@ _profile_getattr(CmsProfileObject* self, cmsInfoType field) { // UNDONE -- check that I'm getting the right fields on these. // return PyUnicode_DecodeFSDefault(cmsTakeProductName(self->profile)); - //wchar_t buf[256]; -- UNDONE need wchar_t for unicode version. + //wchar_t buf[256]; -- UNDONE need wchar_t for unicode version. char buf[256]; cmsUInt32Number written; - written = cmsGetProfileInfoASCII(self->profile, + written = cmsGetProfileInfoASCII(self->profile, field, "en", "us", @@ -566,40 +852,40 @@ _profile_getattr(CmsProfileObject* self, cmsInfoType field) if (written) { return PyUnicode_FromString(buf); } - // UNDONE suppressing error here by sending back blank string. + // UNDONE suppressing error here by sending back blank string. return PyUnicode_FromString(""); } static PyObject* cms_profile_getattr_product_desc(CmsProfileObject* self, void* closure) -{ +{ // description was Description != 'Copyright' || or "%s - %s" (manufacturer, model) in 1.x return _profile_getattr(self, cmsInfoDescription); } -/* use these four for the individual fields. +/* use these four for the individual fields. */ static PyObject* cms_profile_getattr_product_description(CmsProfileObject* self, void* closure) -{ +{ return _profile_getattr(self, cmsInfoDescription); } static PyObject* cms_profile_getattr_product_model(CmsProfileObject* self, void* closure) -{ +{ return _profile_getattr(self, cmsInfoModel); } static PyObject* cms_profile_getattr_product_manufacturer(CmsProfileObject* self, void* closure) -{ +{ return _profile_getattr(self, cmsInfoManufacturer); } static PyObject* cms_profile_getattr_product_copyright(CmsProfileObject* self, void* closure) -{ +{ return _profile_getattr(self, cmsInfoCopyright); } @@ -621,19 +907,479 @@ cms_profile_getattr_color_space(CmsProfileObject* self, void* closure) return PyUnicode_DecodeFSDefault(findICmode(cmsGetColorSpace(self->profile))); } -/* FIXME: add more properties (creation_datetime etc) */ +/* New-style unicode interfaces. */ +static PyObject* +cms_profile_getattr_copyright(CmsProfileObject* self, void* closure) +{ + return _profile_read_mlu(self, cmsSigCopyrightTag); +} + +static PyObject* +cms_profile_getattr_target(CmsProfileObject* self, void* closure) +{ + return _profile_read_mlu(self, cmsSigCharTargetTag); +} + +static PyObject* +cms_profile_getattr_manufacturer(CmsProfileObject* self, void* closure) +{ + return _profile_read_mlu(self, cmsSigDeviceMfgDescTag); +} + +static PyObject* +cms_profile_getattr_model(CmsProfileObject* self, void* closure) +{ + return _profile_read_mlu(self, cmsSigDeviceModelDescTag); +} + +static PyObject* +cms_profile_getattr_profile_description(CmsProfileObject* self, void* closure) +{ + return _profile_read_mlu(self, cmsSigProfileDescriptionTag); +} + +static PyObject* +cms_profile_getattr_screening_description(CmsProfileObject* self, void* closure) +{ + return _profile_read_mlu(self, cmsSigScreeningDescTag); +} + +static PyObject* +cms_profile_getattr_viewing_condition(CmsProfileObject* self, void* closure) +{ + return _profile_read_mlu(self, cmsSigViewingCondDescTag); +} + +static PyObject* +cms_profile_getattr_creation_date(CmsProfileObject* self, void* closure) +{ + cmsBool result; + struct tm ct; + + result = cmsGetHeaderCreationDateTime(self->profile, &ct); + if (! result) { + Py_INCREF(Py_None); + return Py_None; + } + + return PyDateTime_FromDateAndTime(1900 + ct.tm_year, ct.tm_mon, ct.tm_mday, + ct.tm_hour, ct.tm_min, ct.tm_sec, 0); +} + +static PyObject* +cms_profile_getattr_version(CmsProfileObject* self, void* closure) +{ + cmsFloat64Number version = cmsGetProfileVersion(self->profile); + return PyFloat_FromDouble(version); +} + +static PyObject* +cms_profile_getattr_icc_version(CmsProfileObject* self, void* closure) +{ + return PyInt_FromLong((long) cmsGetEncodedICCversion(self->profile)); +} + +static PyObject* +cms_profile_getattr_attributes(CmsProfileObject* self, void* closure) +{ + cmsUInt64Number attr; + cmsGetHeaderAttributes(self->profile, &attr); +#ifdef _WIN32 + // Windows is weird this way. + return PyLong_FromLongLong((long long) attr); +#else + return PyInt_FromLong((long) attr); +#endif +} + +static PyObject* +cms_profile_getattr_header_flags(CmsProfileObject* self, void* closure) +{ + cmsUInt32Number flags = cmsGetHeaderFlags(self->profile); + return PyInt_FromLong(flags); +} + +static PyObject* +cms_profile_getattr_header_manufacturer(CmsProfileObject* self, void* closure) +{ + return _profile_read_int_as_string(cmsGetHeaderManufacturer(self->profile)); +} + +static PyObject* +cms_profile_getattr_header_model(CmsProfileObject* self, void* closure) +{ + return _profile_read_int_as_string(cmsGetHeaderModel(self->profile)); +} + +static PyObject* +cms_profile_getattr_device_class(CmsProfileObject* self, void* closure) +{ + return _profile_read_int_as_string(cmsGetDeviceClass(self->profile)); +} + +/* Duplicate of pcs, but uninterpreted. */ +static PyObject* +cms_profile_getattr_connection_space(CmsProfileObject* self, void* closure) +{ + return _profile_read_int_as_string(cmsGetPCS(self->profile)); +} + +/* Duplicate of color_space, but uninterpreted. */ +static PyObject* +cms_profile_getattr_xcolor_space(CmsProfileObject* self, void* closure) +{ + return _profile_read_int_as_string(cmsGetColorSpace(self->profile)); +} + +static PyObject* +cms_profile_getattr_profile_id(CmsProfileObject* self, void* closure) +{ + cmsUInt8Number id[16]; + cmsGetHeaderProfileID(self->profile, id); + return PyBytes_FromStringAndSize((char *) id, 16); +} + +static PyObject* +cms_profile_getattr_is_matrix_shaper(CmsProfileObject* self, void* closure) +{ + return PyBool_FromLong((long) cmsIsMatrixShaper(self->profile)); +} + +static PyObject* +cms_profile_getattr_technology(CmsProfileObject* self, void* closure) +{ + return _profile_read_signature(self, cmsSigTechnologyTag); +} + +static PyObject* +cms_profile_getattr_colorimetric_intent(CmsProfileObject* self, void* closure) +{ + return _profile_read_signature(self, cmsSigColorimetricIntentImageStateTag); +} + +static PyObject* +cms_profile_getattr_perceptual_rendering_intent_gamut(CmsProfileObject* self, void* closure) +{ + return _profile_read_signature(self, cmsSigPerceptualRenderingIntentGamutTag); +} + +static PyObject* +cms_profile_getattr_saturation_rendering_intent_gamut(CmsProfileObject* self, void* closure) +{ + return _profile_read_signature(self, cmsSigSaturationRenderingIntentGamutTag); +} + +static PyObject* +cms_profile_getattr_red_colorant(CmsProfileObject* self, void* closure) +{ + if (!cmsIsMatrixShaper(self->profile)) { + Py_INCREF(Py_None); + return Py_None; + } + return _profile_read_ciexyz(self, cmsSigRedColorantTag, 0); +} + + +static PyObject* +cms_profile_getattr_green_colorant(CmsProfileObject* self, void* closure) +{ + if (!cmsIsMatrixShaper(self->profile)) { + Py_INCREF(Py_None); + return Py_None; + } + return _profile_read_ciexyz(self, cmsSigGreenColorantTag, 0); +} + + +static PyObject* +cms_profile_getattr_blue_colorant(CmsProfileObject* self, void* closure) +{ + if (!cmsIsMatrixShaper(self->profile)) { + Py_INCREF(Py_None); + return Py_None; + } + return _profile_read_ciexyz(self, cmsSigBlueColorantTag, 0); +} + +static PyObject* +cms_profile_getattr_media_white_point_temperature(CmsProfileObject *self, void* closure) +{ + cmsCIEXYZ* XYZ; + cmsCIExyY xyY; + cmsFloat64Number tempK; + cmsTagSignature info = cmsSigMediaWhitePointTag; + cmsBool result; + + if (!cmsIsTag(self->profile, info)) { + Py_INCREF(Py_None); + return Py_None; + } + + XYZ = (cmsCIEXYZ*) cmsReadTag(self->profile, info); + if (!XYZ) { + Py_INCREF(Py_None); + return Py_None; + } + if (XYZ == NULL || XYZ->X == 0) { + Py_INCREF(Py_None); + return Py_None; + } + + cmsXYZ2xyY(&xyY, XYZ); + result = cmsTempFromWhitePoint(&tempK, &xyY); + if (!result) { + Py_INCREF(Py_None); + return Py_None; + } + return PyFloat_FromDouble(tempK); +} + +static PyObject* +cms_profile_getattr_media_white_point(CmsProfileObject* self, void* closure) +{ + return _profile_read_ciexyz(self, cmsSigMediaWhitePointTag, 0); +} + + +static PyObject* +cms_profile_getattr_media_black_point(CmsProfileObject* self, void* closure) +{ + return _profile_read_ciexyz(self, cmsSigMediaBlackPointTag, 0); +} + +static PyObject* +cms_profile_getattr_luminance(CmsProfileObject* self, void* closure) +{ + return _profile_read_ciexyz(self, cmsSigLuminanceTag, 0); +} + +static PyObject* +cms_profile_getattr_chromatic_adaptation(CmsProfileObject* self, void* closure) +{ + return _profile_read_ciexyz(self, cmsSigChromaticAdaptationTag, 1); +} + +static PyObject* +cms_profile_getattr_chromaticity(CmsProfileObject* self, void* closure) +{ + return _profile_read_ciexyy_triple(self, cmsSigChromaticityTag); +} + +static PyObject* +cms_profile_getattr_red_primary(CmsProfileObject* self, void* closure) +{ + cmsBool result = 0; + cmsCIEXYZTRIPLE primaries; + + if (cmsIsMatrixShaper(self->profile)) + result = _calculate_rgb_primaries(self, &primaries); + if (! result) { + Py_INCREF(Py_None); + return Py_None; + } + + return _xyz_py(&primaries.Red); +} + +static PyObject* +cms_profile_getattr_green_primary(CmsProfileObject* self, void* closure) +{ + cmsBool result = 0; + cmsCIEXYZTRIPLE primaries; + + if (cmsIsMatrixShaper(self->profile)) + result = _calculate_rgb_primaries(self, &primaries); + if (! result) { + Py_INCREF(Py_None); + return Py_None; + } + + return _xyz_py(&primaries.Green); +} + +static PyObject* +cms_profile_getattr_blue_primary(CmsProfileObject* self, void* closure) +{ + cmsBool result = 0; + cmsCIEXYZTRIPLE primaries; + + if (cmsIsMatrixShaper(self->profile)) + result = _calculate_rgb_primaries(self, &primaries); + if (! result) { + Py_INCREF(Py_None); + return Py_None; + } + + return _xyz_py(&primaries.Blue); +} + +static PyObject* +cms_profile_getattr_colorant_table(CmsProfileObject* self, void* closure) +{ + return _profile_read_named_color_list(self, cmsSigColorantTableTag); +} + +static PyObject* +cms_profile_getattr_colorant_table_out(CmsProfileObject* self, void* closure) +{ + return _profile_read_named_color_list(self, cmsSigColorantTableOutTag); +} + +static PyObject* +cms_profile_getattr_is_intent_supported (CmsProfileObject* self, void* closure) +{ + return _is_intent_supported(self, 0); +} + +static PyObject* +cms_profile_getattr_is_clut (CmsProfileObject* self, void* closure) +{ + return _is_intent_supported(self, 1); +} + +static const char* +_illu_map(int i) +{ + switch(i) { + case 0: + return "unknown"; + case 1: + return "D50"; + case 2: + return "D65"; + case 3: + return "D93"; + case 4: + return "F2"; + case 5: + return "D55"; + case 6: + return "A"; + case 7: + return "E"; + case 8: + return "F8"; + default: + return NULL; + } +} + +static PyObject* +cms_profile_getattr_icc_measurement_condition (CmsProfileObject* self, void* closure) +{ + cmsICCMeasurementConditions* mc; + cmsTagSignature info = cmsSigMeasurementTag; + const char *geo; + + if (!cmsIsTag(self->profile, info)) { + Py_INCREF(Py_None); + return Py_None; + } + + mc = (cmsICCMeasurementConditions*) cmsReadTag(self->profile, info); + if (!mc) { + Py_INCREF(Py_None); + return Py_None; + } + + if (mc->Geometry == 1) + geo = "45/0, 0/45"; + else if (mc->Geometry == 2) + geo = "0d, d/0"; + else + geo = "unknown"; + + return Py_BuildValue("{s:i,s:(ddd),s:s,s:d,s:s}", + "observer", mc->Observer, + "backing", mc->Backing.X, mc->Backing.Y, mc->Backing.Z, + "geo", geo, + "flare", mc->Flare, + "illuminant_type", _illu_map(mc->IlluminantType)); +} + +static PyObject* +cms_profile_getattr_icc_viewing_condition (CmsProfileObject* self, void* closure) +{ + cmsICCViewingConditions* vc; + cmsTagSignature info = cmsSigViewingConditionsTag; + + if (!cmsIsTag(self->profile, info)) { + Py_INCREF(Py_None); + return Py_None; + } + + vc = (cmsICCViewingConditions*) cmsReadTag(self->profile, info); + if (!vc) { + Py_INCREF(Py_None); + return Py_None; + } + + return Py_BuildValue("{s:(ddd),s:(ddd),s:s}", + "illuminant", vc->IlluminantXYZ.X, vc->IlluminantXYZ.Y, vc->IlluminantXYZ.Z, + "surround", vc->SurroundXYZ.X, vc->SurroundXYZ.Y, vc->SurroundXYZ.Z, + "illuminant_type", _illu_map(vc->IlluminantType)); +} + + static struct PyGetSetDef cms_profile_getsetters[] = { + /* Compatibility interfaces. */ { "product_desc", (getter) cms_profile_getattr_product_desc }, { "product_description", (getter) cms_profile_getattr_product_description }, { "product_manufacturer", (getter) cms_profile_getattr_product_manufacturer }, { "product_model", (getter) cms_profile_getattr_product_model }, { "product_copyright", (getter) cms_profile_getattr_product_copyright }, - { "rendering_intent", (getter) cms_profile_getattr_rendering_intent }, { "pcs", (getter) cms_profile_getattr_pcs }, { "color_space", (getter) cms_profile_getattr_color_space }, + + /* New style interfaces. */ + { "rendering_intent", (getter) cms_profile_getattr_rendering_intent }, + { "creation_date", (getter) cms_profile_getattr_creation_date }, + { "copyright", (getter) cms_profile_getattr_copyright }, + { "target", (getter) cms_profile_getattr_target }, + { "manufacturer", (getter) cms_profile_getattr_manufacturer }, + { "model", (getter) cms_profile_getattr_model }, + { "profile_description", (getter) cms_profile_getattr_profile_description }, + { "screening_description", (getter) cms_profile_getattr_screening_description }, + { "viewing_condition", (getter) cms_profile_getattr_viewing_condition }, + { "version", (getter) cms_profile_getattr_version }, + { "icc_version", (getter) cms_profile_getattr_icc_version }, + { "attributes", (getter) cms_profile_getattr_attributes }, + { "header_flags", (getter) cms_profile_getattr_header_flags }, + { "header_manufacturer", (getter) cms_profile_getattr_header_manufacturer }, + { "header_model", (getter) cms_profile_getattr_header_model }, + { "device_class", (getter) cms_profile_getattr_device_class }, + { "connection_space", (getter) cms_profile_getattr_connection_space }, + /* Similar to color_space, but with full 4-letter signature (including trailing whitespace). */ + { "xcolor_space", (getter) cms_profile_getattr_xcolor_space }, + { "profile_id", (getter) cms_profile_getattr_profile_id }, + { "is_matrix_shaper", (getter) cms_profile_getattr_is_matrix_shaper }, + { "technology", (getter) cms_profile_getattr_technology }, + { "colorimetric_intent", (getter) cms_profile_getattr_colorimetric_intent }, + { "perceptual_rendering_intent_gamut", (getter) cms_profile_getattr_perceptual_rendering_intent_gamut }, + { "saturation_rendering_intent_gamut", (getter) cms_profile_getattr_saturation_rendering_intent_gamut }, + { "red_colorant", (getter) cms_profile_getattr_red_colorant }, + { "green_colorant", (getter) cms_profile_getattr_green_colorant }, + { "blue_colorant", (getter) cms_profile_getattr_blue_colorant }, + { "red_primary", (getter) cms_profile_getattr_red_primary }, + { "green_primary", (getter) cms_profile_getattr_green_primary }, + { "blue_primary", (getter) cms_profile_getattr_blue_primary }, + { "media_white_point_temperature", (getter) cms_profile_getattr_media_white_point_temperature }, + { "media_white_point", (getter) cms_profile_getattr_media_white_point }, + { "media_black_point", (getter) cms_profile_getattr_media_black_point }, + { "luminance", (getter) cms_profile_getattr_luminance }, + { "chromatic_adaptation", (getter) cms_profile_getattr_chromatic_adaptation }, + { "chromaticity", (getter) cms_profile_getattr_chromaticity }, + { "colorant_table", (getter) cms_profile_getattr_colorant_table }, + { "colorant_table_out", (getter) cms_profile_getattr_colorant_table_out }, + { "intent_supported", (getter) cms_profile_getattr_is_intent_supported }, + { "clut", (getter) cms_profile_getattr_is_clut }, + { "icc_measurement_condition", (getter) cms_profile_getattr_icc_measurement_condition }, + { "icc_viewing_condition", (getter) cms_profile_getattr_icc_viewing_condition }, + { NULL } }; + static PyTypeObject CmsProfile_Type = { PyVarObject_HEAD_INIT(NULL, 0) "CmsProfile", sizeof(CmsProfileObject), 0, @@ -758,6 +1504,8 @@ PyInit__imagingcms(void) { if (setup_module(m) < 0) return NULL; + PyDateTime_IMPORT; + return m; } #else @@ -766,6 +1514,6 @@ init_imagingcms(void) { PyObject *m = Py_InitModule("_imagingcms", pyCMSdll_methods); setup_module(m); + PyDateTime_IMPORT; } #endif - diff --git a/_imagingft.c b/_imagingft.c index d8f6d6338..dc1661f3e 100644 --- a/_imagingft.c +++ b/_imagingft.c @@ -21,20 +21,8 @@ #include "Python.h" #include "Imaging.h" -#if !defined(USE_FREETYPE_2_0) -/* undef/comment out to use freetype 2.0 */ -#define USE_FREETYPE_2_1 -#endif - -#if defined(USE_FREETYPE_2_1) -/* freetype 2.1 and newer */ #include #include FT_FREETYPE_H -#else -/* freetype 2.0 */ -#include -#endif - #include FT_GLYPH_H #define KEEP_PY_UNICODE @@ -59,11 +47,7 @@ struct { const char* message; } ft_errors[] = -#if defined(USE_FREETYPE_2_1) #include FT_ERRORS_H -#else -#include -#endif /* -------------------------------------------------------------------- */ /* font objects */ @@ -144,11 +128,11 @@ getfont(PyObject* self_, PyObject* args, PyObject* kw) /* Don't free this before FT_Done_Face */ self->font_bytes = PyMem_Malloc(font_bytes_size); if (!self->font_bytes) { - error = 65; // Out of Memory in Freetype. + error = 65; // Out of Memory in Freetype. } if (!error) { memcpy(self->font_bytes, font_bytes, (size_t)font_bytes_size); - error = FT_New_Memory_Face(library, (FT_Byte*)self->font_bytes, + error = FT_New_Memory_Face(library, (FT_Byte*)self->font_bytes, font_bytes_size, index, &self->face); } } @@ -168,7 +152,7 @@ getfont(PyObject* self_, PyObject* args, PyObject* kw) if (error) { if (self->font_bytes) { PyMem_Free(self->font_bytes); - } + } PyObject_Del(self); return geterror(error); } @@ -244,8 +228,8 @@ font_getsize(FontObject* self, PyObject* args) x += delta.x; } - /* Note: bitmap fonts within ttf fonts do not work, see #891/pr#960 - * Yifu Yu, 2014-10-15 + /* Note: bitmap fonts within ttf fonts do not work, see #891/pr#960 + * Yifu Yu, 2014-10-15 */ error = FT_Load_Glyph(face, index, FT_LOAD_DEFAULT|FT_LOAD_NO_BITMAP); if (error) @@ -459,7 +443,7 @@ font_dealloc(FontObject* self) FT_Done_Face(self->face); if (self->font_bytes) { PyMem_Free(self->font_bytes); - } + } PyObject_Del(self); } @@ -508,6 +492,25 @@ font_getattr_descent(FontObject* self, void* closure) return PyInt_FromLong(-PIXEL(self->face->size->metrics.descender)); } +static PyObject* +font_getattr_height(FontObject* self, void* closure) +{ + return PyInt_FromLong(PIXEL(self->face->size->metrics.height)); +} + +static PyObject* +font_getattr_x_ppem(FontObject* self, void* closure) +{ + return PyInt_FromLong(self->face->size->metrics.x_ppem); +} + +static PyObject* +font_getattr_y_ppem(FontObject* self, void* closure) +{ + return PyInt_FromLong(self->face->size->metrics.y_ppem); +} + + static PyObject* font_getattr_glyphs(FontObject* self, void* closure) { @@ -519,6 +522,9 @@ static struct PyGetSetDef font_getsetters[] = { { "style", (getter) font_getattr_style }, { "ascent", (getter) font_getattr_ascent }, { "descent", (getter) font_getattr_descent }, + { "height", (getter) font_getattr_height }, + { "x_ppem", (getter) font_getattr_x_ppem }, + { "y_ppem", (getter) font_getattr_y_ppem }, { "glyphs", (getter) font_getattr_glyphs }, { NULL } }; diff --git a/_imagingmath.c b/_imagingmath.c index c6334edeb..ea9f103c6 100644 --- a/_imagingmath.c +++ b/_imagingmath.c @@ -24,13 +24,6 @@ #define MAX_INT32 2147483647.0 #define MIN_INT32 -2147483648.0 -#if defined(_MSC_VER) && _MSC_VER < 1500 -/* python 2.1/2.2/2.3 = VC98 = VER 1200 */ -/* python 2.4/2.5 = VS.NET 2003 = VER 1310 */ -/* python 2.6 = VS 9.0 = VER 1500 */ -#define powf(a, b) ((float) pow((double) (a), (double) (b))) -#endif - #define UNOP(name, op, type)\ void name(Imaging out, Imaging im1)\ {\ diff --git a/_imagingmorph.c b/_imagingmorph.c index 1dee7eb64..70779d036 100644 --- a/_imagingmorph.c +++ b/_imagingmorph.c @@ -109,7 +109,7 @@ apply(PyObject *self, PyObject* args) unsigned char b7 = nrow[col_idx]&1; unsigned char b8 = nrow[cip]&1; - int lut_idx = (b0 + int lut_idx = (b0 |(b1 << 1) |(b2 << 2) |(b3 << 3) @@ -199,7 +199,7 @@ match(PyObject *self, PyObject* args) unsigned char b7 = nrow[col_idx]&1; unsigned char b8 = nrow[cip]&1; - int lut_idx = (b0 + int lut_idx = (b0 |(b1 << 1) |(b2 << 2) |(b3 << 3) diff --git a/appveyor.yml b/appveyor.yml new file mode 100644 index 000000000..81f62dfdd --- /dev/null +++ b/appveyor.yml @@ -0,0 +1,30 @@ +version: 3.3.pre.{build} +clone_folder: c:\pillow +init: +- ECHO %PYTHON% +environment: + X64_EXT: -x64 + matrix: + - PYTHON: C:/Python27-x64 + - PYTHON: C:/Python34 + - PYTHON: C:/Python27 + - PYTHON: C:/Python34-x64 + - PYTHON: C:/Python33 + - PYTHON: C:/Python33-x64 +install: +- git clone https://github.com/python-pillow/pillow-depends.git c:\pillow-depends +- xcopy c:\pillow-depends\*.zip c:\pillow\winbuild\ +- xcopy c:\pillow-depends\*.tar.gz c:\pillow\winbuild\ +- cd c:\pillow\winbuild\ +- c:\python34\python.exe c:\pillow\winbuild\build_dep.py +- c:\pillow\winbuild\build_deps.cmd +build_script: +- '%PYTHON%\python.exe c:\pillow\winbuild\build.py' +- cd c:\pillow +- '%PYTHON%\python.exe selftest.py --installed' +test_script: +- cd c:\pillow +- '%PYTHON%\Scripts\pip.exe install nose' +- '%PYTHON%\python.exe test-installed.py' +matrix: + fast_finish: true diff --git a/decode.c b/decode.c index 6299d9124..29c0f9a1b 100644 --- a/decode.c +++ b/decode.c @@ -754,7 +754,7 @@ PyImaging_JpegDecoderNew(PyObject* self, PyObject* args) ImagingDecoderObject* decoder; char* mode; - char* rawmode; /* what we wan't from the decoder */ + char* rawmode; /* what we want from the decoder */ char* jpegmode; /* what's in the file */ int scale = 1; int draft = 0; diff --git a/depends/README.rst b/depends/README.rst index 62c101ecf..dd05b86ca 100644 --- a/depends/README.rst +++ b/depends/README.rst @@ -1,4 +1,27 @@ Depends ======= -Scripts in this directory can be used to download, build & install non-packaged dependencies; useful for testing with Travis CI. +``install_openjpeg.sh`` and ``install_webp.sh`` can be used to +download, build & install non-packaged dependencies; useful for +testing with Travis CI. + +The other scripts can be used to install all of the dependencies for +the listed operating systems/distros. The ``ubuntu_14.04.sh`` and +``debian_8.2.sh`` scripts have been tested on bare AWS images and will +install all required dependencies for the system Python 2.7 and 3.4 +for all of the optional dependencies. Git may also be required prior +to running the script to actually download Pillow. + +e.g.:: + + $ sudo apt-get install git + $ git clone https://github.com/python-pillow/Pillow.git + $ cd Pillow/depends + $ ./debian_8.2.sh + $ cd .. + $ git checkout [branch or tag] + $ virtualenv -p /usr/bin/python2.7 ~/vpy27 + $ source ~/vpy27/bin/activate + $ make install + $ make test + diff --git a/depends/debian_8.2.sh b/depends/debian_8.2.sh new file mode 100755 index 000000000..dd0679a9f --- /dev/null +++ b/depends/debian_8.2.sh @@ -0,0 +1,16 @@ +#!/bin/sh + +# +# Installs all of the dependencies for Pillow for Debian 8.2 +# for both system Pythons 2.7 and 3.4 +# +# Also works for Raspbian Jessie +# + +sudo apt-get -y install python-dev python-setuptools \ + python3-dev python-virtualenv cmake +sudo apt-get -y install libtiff5-dev libjpeg62-turbo-dev zlib1g-dev \ + libfreetype6-dev liblcms2-dev libwebp-dev tcl8.6-dev tk8.6-dev \ + python-tk python3-tk + +./install_openjpeg.sh diff --git a/Scripts/diffcover-install.sh b/depends/diffcover-install.sh similarity index 82% rename from Scripts/diffcover-install.sh rename to depends/diffcover-install.sh index 93e06efe3..850d368f8 100755 --- a/Scripts/diffcover-install.sh +++ b/depends/diffcover-install.sh @@ -1,5 +1,5 @@ # Fetch the remote master branch before running diff-cover on Travis CI. -# https://github.com/edx/diff-cover#troubleshooting +# https://github.com/Bachmann1234/diff-cover#troubleshooting git fetch origin master:refs/remotes/origin/master # CFLAGS=-O0 means build with no optimisation. diff --git a/Scripts/diffcover-run.sh b/depends/diffcover-run.sh similarity index 100% rename from Scripts/diffcover-run.sh rename to depends/diffcover-run.sh diff --git a/depends/fedora_23.sh b/depends/fedora_23.sh new file mode 100755 index 000000000..2825eba56 --- /dev/null +++ b/depends/fedora_23.sh @@ -0,0 +1,18 @@ +#!/bin/sh + +# +# Installs all of the dependencies for Pillow for Fedora 23 +# for both system Pythons 2.7 and 3.4 +# +# note that Fedora does ship packages for Pillow as python-pillow + +# this is a workaround for +# "gcc: error: /usr/lib/rpm/redhat/redhat-hardened-cc1: No such file or directory" +# errors when compiling. +sudo dnf install redhat-rpm-config + +sudo dnf install python-devel python3-devel python-virtualenv make gcc + +sudo dnf install libtiff-devel libjpeg-devel libzip-devel freetype-devel \ + lcms2-devel libwebp-devel openjpeg2-devel tkinter python3-tkinter \ + tcl-devel tk-devel \ No newline at end of file diff --git a/depends/freebsd_10.sh b/depends/freebsd_10.sh new file mode 100755 index 000000000..99b4d6d0f --- /dev/null +++ b/depends/freebsd_10.sh @@ -0,0 +1,11 @@ +#!/bin/sh + +# +# Installs all of the dependencies for Pillow for Freebsd 10.x +# for both system Pythons 2.7 and 3.4 +# +sudo pkg install python2 python3 py27-pip py27-virtualenv py27-setuptools27 + +# Openjpeg fails badly using the openjpeg package. +# I can't find a python3.4 version of tkinter +sudo pkg install jpeg-turbo tiff webp lcms2 freetype2 py27-tkinter diff --git a/depends/install_webp.sh b/depends/install_webp.sh index 97edf2bcf..72a46c8cf 100755 --- a/depends/install_webp.sh +++ b/depends/install_webp.sh @@ -1,14 +1,14 @@ #!/bin/bash # install webp -if [ ! -f libwebp-0.4.3.tar.gz ]; then - wget 'http://downloads.webmproject.org/releases/webp/libwebp-0.4.3.tar.gz' +if [ ! -f libwebp-0.5.0.tar.gz ]; then + wget 'http://downloads.webmproject.org/releases/webp/libwebp-0.5.0.tar.gz' fi -rm -r libwebp-0.4.3 -tar -xvzf libwebp-0.4.3.tar.gz +rm -r libwebp-0.5.0 +tar -xvzf libwebp-0.5.0.tar.gz -pushd libwebp-0.4.3 +pushd libwebp-0.5.0 ./configure --prefix=/usr --enable-libwebpmux --enable-libwebpdemux && make -j4 && sudo make -j4 install diff --git a/depends/ubuntu_12.04.sh b/depends/ubuntu_12.04.sh new file mode 100755 index 000000000..e9b16d2b4 --- /dev/null +++ b/depends/ubuntu_12.04.sh @@ -0,0 +1,16 @@ +#!/bin/sh + +# +# Installs all of the dependencies for Pillow for Ubuntu 12.04 +# for both system Pythons 2.7 and 3.2 +# + +sudo apt-get -y install python-dev python-setuptools \ + python3-dev python-virtualenv cmake +sudo apt-get install libtiff4-dev libjpeg8-dev zlib1g-dev \ + libfreetype6-dev liblcms2-dev tcl8.5-dev \ + tk8.5-dev python-tk python3-tk + + +./install_openjpeg.sh +./install_webp.sh diff --git a/depends/ubuntu_14.04.sh b/depends/ubuntu_14.04.sh new file mode 100755 index 000000000..14b9e7066 --- /dev/null +++ b/depends/ubuntu_14.04.sh @@ -0,0 +1,14 @@ +#!/bin/sh + +# +# Installs all of the dependencies for Pillow for Ubuntu 14.04 +# for both system Pythons 2.7 and 3.4 +# + +sudo apt-get -y install python-dev python-setuptools \ + python3-dev python-virtualenv cmake +sudo apt-get -y install libtiff5-dev libjpeg8-dev zlib1g-dev \ + libfreetype6-dev liblcms2-dev libwebp-dev tcl8.6-dev tk8.6-dev \ + python-tk python3-tk + +./install_openjpeg.sh diff --git a/display.c b/display.c index e10d72c0d..9f493344e 100644 --- a/display.c +++ b/display.c @@ -35,6 +35,12 @@ #include "ImDib.h" +#if SIZEOF_VOID_P == 8 +#define F_HANDLE "K" +#else +#define F_HANDLE "k" +#endif + typedef struct { PyObject_HEAD ImagingDIB dib; @@ -74,8 +80,8 @@ _delete(ImagingDisplayObject* display) static PyObject* _expose(ImagingDisplayObject* display, PyObject* args) { - int hdc; - if (!PyArg_ParseTuple(args, "i", &hdc)) + HDC hdc; + if (!PyArg_ParseTuple(args, F_HANDLE, &hdc)) return NULL; ImagingExposeDIB(display->dib, hdc); @@ -87,10 +93,10 @@ _expose(ImagingDisplayObject* display, PyObject* args) static PyObject* _draw(ImagingDisplayObject* display, PyObject* args) { - int hdc; + HDC hdc; int dst[4]; int src[4]; - if (!PyArg_ParseTuple(args, "i(iiii)(iiii)", &hdc, + if (!PyArg_ParseTuple(args, F_HANDLE "(iiii)(iiii)", &hdc, dst+0, dst+1, dst+2, dst+3, src+0, src+1, src+2, src+3)) return NULL; @@ -131,10 +137,10 @@ _paste(ImagingDisplayObject* display, PyObject* args) static PyObject* _query_palette(ImagingDisplayObject* display, PyObject* args) { - int hdc; + HDC hdc; int status; - if (!PyArg_ParseTuple(args, "i", &hdc)) + if (!PyArg_ParseTuple(args, F_HANDLE, &hdc)) return NULL; status = ImagingQueryPaletteDIB(display->dib, hdc); @@ -145,30 +151,31 @@ _query_palette(ImagingDisplayObject* display, PyObject* args) static PyObject* _getdc(ImagingDisplayObject* display, PyObject* args) { - int window; + HWND window; HDC dc; - if (!PyArg_ParseTuple(args, "i", &window)) + if (!PyArg_ParseTuple(args, F_HANDLE, &window)) return NULL; - dc = GetDC((HWND) window); + dc = GetDC(window); if (!dc) { PyErr_SetString(PyExc_IOError, "cannot create dc"); return NULL; } - return Py_BuildValue("i", (int) dc); + return Py_BuildValue(F_HANDLE, dc); } static PyObject* _releasedc(ImagingDisplayObject* display, PyObject* args) { - int window, dc; + HWND window; + HDC dc; - if (!PyArg_ParseTuple(args, "ii", &window, &dc)) + if (!PyArg_ParseTuple(args, F_HANDLE F_HANDLE, &window, &dc)) return NULL; - ReleaseDC((HWND) window, (HDC) dc); + ReleaseDC(window, dc); Py_INCREF(Py_None); return Py_None; @@ -386,7 +393,7 @@ static BOOL CALLBACK list_windows_callback(HWND hwnd, LPARAM lParam) if (title_size > 0) { title = PyUnicode_FromStringAndSize(NULL, title_size); if (title) - GetWindowText(hwnd, PyUnicode_AS_UNICODE(title), title_size+1); + GetWindowTextW(hwnd, PyUnicode_AS_UNICODE(title), title_size+1); } else title = PyUnicode_FromString(""); if (!title) @@ -397,7 +404,7 @@ static BOOL CALLBACK list_windows_callback(HWND hwnd, LPARAM lParam) GetWindowRect(hwnd, &outer); item = Py_BuildValue( - "nN(iiii)(iiii)", (Py_ssize_t) hwnd, title, + F_HANDLE "N(iiii)(iiii)", hwnd, title, inner.left, inner.top, inner.right, inner.bottom, outer.left, outer.top, outer.right, outer.bottom ); @@ -600,10 +607,10 @@ windowCallback(HWND wnd, UINT message, WPARAM wParam, LPARAM lParam) /* fall through... */ case WM_PAINT: case WM_SIZE: - callback = (PyObject*) GetWindowLong(wnd, 0); + callback = (PyObject*) GetWindowLongPtr(wnd, 0); if (callback) { threadstate = (PyThreadState*) - GetWindowLong(wnd, sizeof(PyObject*)); + GetWindowLongPtr(wnd, sizeof(PyObject*)); current_threadstate = PyThreadState_Swap(NULL); PyEval_RestoreThread(threadstate); } else @@ -631,7 +638,7 @@ windowCallback(HWND wnd, UINT message, WPARAM wParam, LPARAM lParam) callback_error("window damage callback"); result = PyObject_CallFunction( - callback, "siiiii", "clear", (int) dc, + callback, "s" F_HANDLE "iiii", "clear", dc, 0, 0, rect.right-rect.left, rect.bottom-rect.top ); if (result) @@ -640,7 +647,7 @@ windowCallback(HWND wnd, UINT message, WPARAM wParam, LPARAM lParam) callback_error("window clear callback"); result = PyObject_CallFunction( - callback, "siiiii", "repair", (int) dc, + callback, "s" F_HANDLE "iiii", "repair", dc, 0, 0, rect.right-rect.left, rect.bottom-rect.top ); if (result) @@ -741,7 +748,7 @@ PyImaging_CreateWindowWin32(PyObject* self, PyObject* args) SetForegroundWindow(wnd); /* to make sure it's visible */ Py_END_ALLOW_THREADS - return Py_BuildValue("n", (Py_ssize_t) wnd); + return Py_BuildValue(F_HANDLE, wnd); } PyObject* diff --git a/docs/Makefile b/docs/Makefile index f28f802a1..1a912039e 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -154,3 +154,6 @@ doctest: livehtml: html livereload $(BUILDDIR)/html -p 33233 + +serve: + cd $(BUILDDIR)/html; python -m SimpleHTTPServer diff --git a/docs/PIL.rst b/docs/PIL.rst index 53a61872b..67edb9901 100644 --- a/docs/PIL.rst +++ b/docs/PIL.rst @@ -64,15 +64,6 @@ can be found here. .. intentionally skipped documenting this because it's deprecated -:mod:`ImageFileIO` Module -------------------------- - -.. automodule:: PIL.ImageFileIO - :members: - :undoc-members: - :show-inheritance: - - :mod:`ImageShow` Module ----------------------- @@ -144,14 +135,6 @@ can be found here. :undoc-members: :show-inheritance: -:mod:`TiffTags` Module ----------------------- - -.. automodule:: PIL.TiffTags - :members: - :undoc-members: - :show-inheritance: - :mod:`WalImageFile` Module -------------------------- diff --git a/docs/about.rst b/docs/about.rst index 67bf20481..2f97020ca 100644 --- a/docs/about.rst +++ b/docs/about.rst @@ -1,36 +1,24 @@ -About Pillow -============ +About +===== Goals ----- The fork author's goal is to foster and support active development of PIL through: -- Continuous integration testing via `Travis CI`_ +- Continuous integration testing via `Travis CI`_ and `AppVeyor`_ - Publicized development activity on `GitHub`_ - Regular releases to the `Python Package Index`_ .. _Travis CI: https://travis-ci.org/python-pillow/Pillow +.. _AppVeyor: https://ci.appveyor.com/project/Python-pillow/pillow .. _GitHub: https://github.com/python-pillow/Pillow .. _Python Package Index: https://pypi.python.org/pypi/Pillow License ------- -Like PIL, Pillow is licensed under the MIT-like open source `PIL Software License `_:: - - Software License - - The Python Imaging Library (PIL) is - - Copyright © 1997-2011 by Secret Labs AB - Copyright © 1995-2011 by Fredrik Lundh - - By obtaining, using, and/or copying this software and/or its associated documentation, you agree that you have read, understood, and will comply with the following terms and conditions: - - Permission to use, copy, modify, and distribute this software and its associated documentation for any purpose and without fee is hereby granted, provided that the above copyright notice appears in all copies, and that both that copyright notice and this permission notice appear in supporting documentation, and that the name of Secret Labs AB or the author not be used in advertising or publicity pertaining to distribution of the software without specific, written prior permission. - - SECRET LABS AB AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL SECRET LABS AB OR THE AUTHOR BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +Like PIL, Pillow is `licensed under the MIT-like open source PIL Software License `_ Why a fork? ----------- diff --git a/docs/conf.py b/docs/conf.py index 85be12f30..a1d09cd34 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -48,7 +48,7 @@ master_doc = 'index' # General information about the project. project = u'Pillow (PIL Fork)' -copyright = u'1995-2015, Fredrik Lundh and Contributors, Alex Clark and Contributors' +copyright = u'1995-2016, Fredrik Lundh and Contributors, Alex Clark and Contributors' author = u'Fredrik Lundh and Contributors, Alex Clark and Contributors' # The version info for the project you're documenting, acts as replacement for diff --git a/docs/developer.rst b/docs/developer.rst deleted file mode 100644 index ea3b0f05e..000000000 --- a/docs/developer.rst +++ /dev/null @@ -1,17 +0,0 @@ -Developer -========= - -.. Note:: When committing only trivial changes, please include [ci skip] in the commit message to avoid running tests on Travis-CI. Thank you! - - -Release -------- - -Details about making a Pillow release. - -Version number -~~~~~~~~~~~~~~ - -The version number is currently stored in 3 places:: - - PIL/__init__.py _imaging.c setup.py diff --git a/docs/guides.rst b/docs/guides.rst deleted file mode 100644 index 87ce75bd4..000000000 --- a/docs/guides.rst +++ /dev/null @@ -1,11 +0,0 @@ -Guides -====== - -.. toctree:: - :maxdepth: 2 - - handbook/overview - handbook/tutorial - handbook/concepts - porting-pil-to-pillow - developer diff --git a/docs/handbook/concepts.rst b/docs/handbook/concepts.rst index fb97fe098..b5ad9331a 100644 --- a/docs/handbook/concepts.rst +++ b/docs/handbook/concepts.rst @@ -4,12 +4,17 @@ Concepts The Python Imaging Library handles *raster images*; that is, rectangles of pixel data. +.. _concept-bands: + Bands ----- An image can consist of one or more bands of data. The Python Imaging Library allows you to store several bands in a single image, provided they all have the -same dimensions and depth. +same dimensions and depth. For example, a PNG image might have 'R', 'G', 'B', +and 'A' bands for the red, green, blue, and alpha transparency values. Many +operations act on each band separately, e.g., histograms. It is often useful to +think of each pixel as having one value per band. To get the number and names of bands in an image, use the :py:meth:`~PIL.Image.Image.getbands` method. @@ -29,6 +34,9 @@ image. The current release supports the following standard modes: * ``RGBA`` (4x8-bit pixels, true color with transparency mask) * ``CMYK`` (4x8-bit pixels, color separation) * ``YCbCr`` (3x8-bit pixels, color video format) + + * Note that this refers to the JPEG, and not the ITU-R BT.2020, standard + * ``LAB`` (3x8-bit pixels, the L*a*b color space) * ``HSV`` (3x8-bit pixels, Hue, Saturation, Value color space) * ``I`` (32-bit signed integer pixels) @@ -38,7 +46,7 @@ PIL also provides limited support for a few special modes, including ``LA`` (L with alpha), ``RGBX`` (true color with padding) and ``RGBa`` (true color with premultiplied alpha). However, PIL doesn’t support user-defined modes; if you to handle band combinations that are not listed above, use a sequence of Image -objects. +objects. You can read the mode of an image through the :py:attr:`~PIL.Image.Image.mode` attribute. This is a string containing one of the above values. diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index a85a917b8..5fe42fa41 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -4,7 +4,7 @@ Image file formats ================== The Python Imaging Library supports a wide variety of raster file formats. -Nearly 30 different file formats can be identified and read by the library. +Over 30 different file formats can be identified and read by the library. Write support is less extensive, but most common interchange and presentation formats are supported. @@ -54,8 +54,11 @@ GIF ^^^ PIL reads GIF87a and GIF89a versions of the GIF file format. The library writes -run-length encoded GIF87a files. Note that GIF files are always read as -grayscale (``L``) or palette mode (``P``) images. +run-length encoded files in GIF87a by default, unless GIF89a features +are used or GIF89a is already in use. + +Note that GIF files are always read as grayscale (``L``) +or palette mode (``P``) images. The :py:meth:`~PIL.Image.Image.open` method sets the following :py:attr:`~PIL.Image.Image.info` properties: @@ -73,13 +76,33 @@ The :py:meth:`~PIL.Image.Image.open` method sets the following **version** Version (either ``GIF87a`` or ``GIF89a``). +**duration** + May not be present. The time to display each frame of the GIF, in + milliseconds. + +**loop** + May not be present. The number of times the GIF should loop. + 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 +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. +``im.seek()`` raises an ``EOFError`` if you try to seek after the last frame. + +Saving sequences +~~~~~~~~~~~~~~~~ + +When calling :py:meth:`~PIL.Image.Image.save`, if a multiframe image is used, +by default only the first frame will be saved. To save all frames, the +``save_all`` parameter must be present and set to ``True``. + +If present, the ``loop`` parameter can be used to set the number of times +the GIF should loop, and the ``duration`` parameter can set the number of +milliseconds between each frame. + Reading local images ~~~~~~~~~~~~~~~~~~~~ @@ -97,6 +120,25 @@ attributes before loading the file:: im.size = (x1 - x0, y1 - y0) im.tile = [(tag, (0, 0) + im.size, offset, extra)] +ICNS +^^^^ + +PIL reads and (OS X only) writes Mac OS X ``.icns`` files. By default, the +largest available icon is read, though you can override this by setting the +:py:attr:`~PIL.Image.Image.size` property before calling +:py:meth:`~PIL.Image.Image.load`. The :py:meth:`~PIL.Image.Image.open` method +sets the following :py:attr:`~PIL.Image.Image.info` property: + +**sizes** + A list of supported sizes found in this icon file; these are a + 3-tuple, ``(width, height, scale)``, where ``scale`` is 2 for a retina + icon and 1 for a standard icon. You *are* permitted to use this 3-tuple + format for the :py:attr:`~PIL.Image.Image.size` property if you set it + before calling :py:meth:`~PIL.Image.Image.load`; after loading, the size + will be reset to a 2-tuple containing pixel dimensions (so, e.g. if you + ask for ``(512, 512, 2)``, the final value of + :py:attr:`~PIL.Image.Image.size` will be ``(1024, 1024)``). + IM ^^ @@ -114,8 +156,7 @@ PIL reads JPEG, JFIF, and Adobe JPEG files containing ``L``, ``RGB``, or Using the :py:meth:`~PIL.Image.Image.draft` method, you can speed things up by converting ``RGB`` images to ``L``, and resize images to 1/2, 1/4 or 1/8 of -their original size while loading them. The :py:meth:`~PIL.Image.Image.draft` -method also configures the JPEG decoder to trade some quality for speed. +their original size while loading them. The :py:meth:`~PIL.Image.Image.open` method may set the following :py:attr:`~PIL.Image.Image.info` properties if available: @@ -152,7 +193,7 @@ The :py:meth:`~PIL.Image.Image.open` method may set the following **progression** Indicates that this is a progressive JPEG file. -**icc-profile** +**icc_profile** The ICC color profile for the image. **exif** @@ -178,7 +219,7 @@ The :py:meth:`~PIL.Image.Image.save` method supports the following options: **dpi** A tuple of integers representing the pixel density, ``(x,y)``. -**icc-profile** +**icc_profile** If present, the image is stored with the provided ICC profile. If this parameter is not provided, the image will be saved with no profile attached. To preserve the existing profile:: @@ -439,17 +480,39 @@ The :py:meth:`~PIL.Image.Image.open` method sets the following **compression** Compression mode. + .. versionadded:: 2.0.0 + **dpi** - Image resolution as an (xdpi, ydpi) tuple, where applicable. You can use + Image resolution as an ``(xdpi, ydpi)`` tuple, where applicable. You can use the :py:attr:`~PIL.Image.Image.tag` attribute to get more detailed information about the image resolution. .. versionadded:: 1.1.5 -In addition, the :py:attr:`~PIL.Image.Image.tag` attribute contains a -dictionary of decoded TIFF fields. Values are stored as either strings or -tuples. Note that only short, long and ASCII tags are correctly unpacked by -this release. +**resolution** + Image resolution as an ``(xres, yres)`` tuple, where applicable. This is a + measurement in whichever unit is specified by the file. + + .. versionadded:: 1.1.5 + + +The :py:attr:`~PIL.Image.Image.tag_v2` attribute contains a dictionary +of TIFF metadata. The keys are numerical indexes from +:py:attr:`~PIL.TiffTags.TAGS_V2`. Values are strings or numbers for single +items, multiple values are returned in a tuple of values. Rational +numbers are returned as a :py:class:`~PIL.TiffImagePlugin.IFDRational` +object. + + .. versionadded:: 3.0.0 + +For compatibility with legacy code, the +:py:attr:`~PIL.Image.Image.tag` attribute contains a dictionary of +decoded TIFF fields as returned prior to version 3.0.0. Values are +returned as either strings or tuples of numeric values. Rational +numbers are returned as a tuple of ``(numerator, denominator)``. + + .. deprecated:: 3.0.0 + Saving Tiff Images ~~~~~~~~~~~~~~~~~~ @@ -457,25 +520,44 @@ Saving Tiff Images The :py:meth:`~PIL.Image.Image.save` method can take the following keyword arguments: **tiffinfo** - A :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory` object or dict + A :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v2` object or dict object containing tiff tags and values. The TIFF field type is autodetected for Numeric and string values, any other types - require using an :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory` + require using an :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v2` object and setting the type in - :py:attr:`~PIL.TiffImagePlugin.ImageFileDirectory.tagtype` with + :py:attr:`~PIL.TiffImagePlugin.ImageFileDirectory_v2.tagtype` with the appropriate numerical value from ``TiffTags.TYPES``. .. versionadded:: 2.3.0 + Metadata values that are of the rational type should be passed in + using a :py:class:`~PIL.TiffImagePlugin.IFDRational` object. + + .. versionadded:: 3.1.0 + + For compatibility with legacy code, a + :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v1` object may + be passed in this field. However, this is deprecated. + + .. versionadded:: 3.0.0 + + .. note:: + + Only some tags are currently supported when writing using + libtiff. The supported list is found in + :py:attr:`~PIL:TiffTags.LIBTIFF_CORE`. + **compression** A string containing the desired compression method for the - file. (valid only with libtiff installed) Valid compression - methods are: ``[None, "tiff_ccitt", "group3", "group4", - "tiff_jpeg", "tiff_adobe_deflate", "tiff_thunderscan", - "tiff_deflate", "tiff_sgilog", "tiff_sgilog24", "tiff_raw_16"]`` + file. (valid only with libtiff installed) Valid compression + methods are: ``None``, ``"tiff_ccitt"``, ``"group3"``, + ``"group4"``, ``"tiff_jpeg"``, ``"tiff_adobe_deflate"``, + ``"tiff_thunderscan"``, ``"tiff_deflate"``, ``"tiff_sgilog"``, + ``"tiff_sgilog24"``, ``"tiff_raw_16"`` -These arguments to set the tiff header fields are an alternative to using the general tags available through tiffinfo. +These arguments to set the tiff header fields are an alternative to +using the general tags available through tiffinfo. **description** @@ -498,9 +580,10 @@ These arguments to set the tiff header fields are an alternative to using the ge **y_resolution** **dpi** - Either a Float, Integer, or 2 tuple of (numerator, - denominator). Resolution implies an equal x and y resolution, dpi - also implies a unit of inches. + Either a Float, 2 tuple of (numerator, denominator) or a + :py:class:`~PIL.TiffImagePlugin.IFDRational`. Resolution implies + an equal x and y resolution, dpi also implies a unit of inches. + WebP ^^^^ @@ -531,11 +614,6 @@ XBM PIL reads and writes X bitmap files (mode ``1``). -XV Thumbnails -^^^^^^^^^^^^^ - -PIL can read XV thumbnail files. - Read-only formats ----------------- @@ -555,6 +633,16 @@ is commonly used in fax applications. The DCX decoder can read files containing 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. + +DDS +^^^ + +DDS is a popular container texture format used in video games and natively +supported by DirectX. +Currently, only DXT1 and DXT5 pixel formats are supported and only in ``RGBA`` +mode. + + FLI, FLC ^^^^^^^^ @@ -579,17 +667,29 @@ into account. library before building the Python Imaging Library. See the distribution README for details. +FTEX +^^^^ + +.. versionadded:: 3.2.0 + +The FTEX decoder reads textures used for 3D objects in +Independence War 2: Edge Of Chaos. The plugin reads a single texture +per file, in the compressed and uncompressed formats. + GBR ^^^ -The GBR decoder reads GIMP brush files. +The GBR decoder reads GIMP brush files, version 1 and 2. The :py:meth:`~PIL.Image.Image.open` method sets the following :py:attr:`~PIL.Image.Image.info` properties: -**description** +**comment** The brush name. +**spacing** + The spacing between the brushes, in pixels. Version 2 only. + GD ^^ @@ -617,25 +717,6 @@ The :py:meth:`~PIL.Image.Image.save` method supports the following options: (64, 64), (128, 128), (255, 255)]``. Any size is bigger then the original size or 255 will be ignored. -ICNS -^^^^ - -PIL reads Mac OS X ``.icns`` files. By default, the largest available icon is -read, though you can override this by setting the :py:attr:`~PIL.Image.Image.size` -property before calling :py:meth:`~PIL.Image.Image.load`. The -:py:meth:`~PIL.Image.Image.open` method sets the following -:py:attr:`~PIL.Image.Image.info` property: - -**sizes** - A list of supported sizes found in this icon file; these are a - 3-tuple, ``(width, height, scale)``, where ``scale`` is 2 for a retina - icon and 1 for a standard icon. You *are* permitted to use this 3-tuple - format for the :py:attr:`~PIL.Image.Image.size` property if you set it - before calling :py:meth:`~PIL.Image.Image.load`; after loading, the size - will be reset to a 2-tuple containing pixel dimensions (so, e.g. if you - ask for ``(512, 512, 2)``, the final value of - :py:attr:`~PIL.Image.Image.size` will be ``(1024, 1024)``). - IMT ^^^ @@ -651,7 +732,8 @@ MCIDAS PIL identifies and reads 8-bit McIdas area files. -MIC (read only) +MIC +^^^ PIL 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 @@ -665,12 +747,6 @@ image when first opened. The :py:meth:`~file.seek` and :py:meth:`~file.tell` methods may be used to read other pictures from the file. The pictures are zero-indexed and random access is supported. -MIC (read only) - -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. - PCD ^^^ @@ -680,6 +756,14 @@ read the lower resolution versions instead, thus effectively resizing the image to 384x256 or 192x128. Higher resolutions cannot be read by the Python Imaging Library. +PIXAR +^^^^^ + +PIL provides limited support for PIXAR raster files. The library can identify +and read “dumped” RGB files. + +The format code is ``PIXAR``. + PSD ^^^ @@ -738,13 +822,15 @@ PIL can write PDF (Acrobat) images. Such images are written as binary PDF 1.1 files, using either JPEG or HEX encoding depending on the image mode (and whether JPEG support is available or not). -PIXAR (read only) -^^^^^^^^^^^^^^^^^ +When calling :py:meth:`~PIL.Image.Image.save`, if a multiframe image is used, +by default, only the first image will be saved. To save all frames, each frame +to a separate page of the PDF, the ``save_all`` parameter must be present and +set to ``True``. -PIL provides limited support for PIXAR raster files. The library can identify -and read “dumped” RGB files. +XV Thumbnails +^^^^^^^^^^^^^ -The format code is ``PIXAR``. +PIL can read XV thumbnail files. Identify-only formats --------------------- diff --git a/docs/handbook/index.rst b/docs/handbook/index.rst new file mode 100644 index 000000000..acdeff7db --- /dev/null +++ b/docs/handbook/index.rst @@ -0,0 +1,10 @@ +Handbook +======== + +.. toctree:: + :maxdepth: 2 + + overview + tutorial + concepts + appendices diff --git a/docs/handbook/tutorial.rst b/docs/handbook/tutorial.rst index 365c8e7a8..914f7cde8 100644 --- a/docs/handbook/tutorial.rst +++ b/docs/handbook/tutorial.rst @@ -193,7 +193,9 @@ For more advanced tricks, the paste method can also take a transparency mask as an optional argument. In this mask, the value 255 indicates that the pasted image is opaque in that position (that is, the pasted image should be used as is). The value 0 means that the pasted image is completely transparent. Values -in-between indicate different levels of transparency. +in-between indicate different levels of transparency. For example, pasting an +RGBA image and also using it as the mask would paste the opaque portion +of the image but not its transparent background. The Python Imaging Library also allows you to work with the individual bands of an multi-band image, such as an RGB image. The split method creates a set of @@ -245,8 +247,9 @@ Transposing an image out = im.transpose(Image.ROTATE_180) out = im.transpose(Image.ROTATE_270) -There’s no difference in performance or result between ``transpose(ROTATE)`` -and corresponding :py:meth:`~PIL.Image.Image.rotate` operations. +``transpose(ROTATE)`` operations can also be performed identically with +:py:meth:`~PIL.Image.Image.rotate` operations, provided the `expand` flag is +true, to provide for the same changes to the image's size. A more general form of image transformations can be carried out via the :py:meth:`~PIL.Image.Image.transform` method. diff --git a/docs/handbook/writing-your-own-file-decoder.rst b/docs/handbook/writing-your-own-file-decoder.rst index 0af4007be..acc80e4ee 100644 --- a/docs/handbook/writing-your-own-file-decoder.rst +++ b/docs/handbook/writing-your-own-file-decoder.rst @@ -124,9 +124,9 @@ The raw decoder The ``raw`` decoder is used to read uncompressed data from an image file. It can be used with most uncompressed file formats, such as PPM, BMP, uncompressed TIFF, and many others. To use the raw decoder with the -:py:func:`PIL.Image.fromstring` function, use the following syntax:: +:py:func:`PIL.Image.frombytes` function, use the following syntax:: - image = Image.fromstring( + image = Image.frombytes( mode, size, data, "raw", raw mode, stride, orientation ) @@ -233,9 +233,9 @@ If the raw decoder cannot handle your format, PIL also provides a special “bit decoder that can be used to read various packed formats into a floating point image memory. -To use the bit decoder with the fromstring function, use the following syntax:: +To use the bit decoder with the frombytes function, use the following syntax:: - image = fromstring( + image = frombytes( mode, size, data, "bit", bits, pad, fill, sign, orientation ) diff --git a/docs/index.rst b/docs/index.rst index 0c7365c64..d7bd95b9a 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,38 +1,58 @@ Pillow ====== -Pillow is the "friendly PIL fork" by `Alex Clark and Contributors `_. PIL is the Python Imaging Library by Fredrik Lundh and Contributors. +Pillow is the friendly PIL fork by `Alex Clark and Contributors `_. PIL is the Python Imaging Library by Fredrik Lundh and Contributors. + +.. image:: https://zenodo.org/badge/17549/python-pillow/Pillow.svg + :target: https://zenodo.org/badge/latestdoi/17549/python-pillow/Pillow + +.. image:: https://readthedocs.org/projects/pillow/badge/?version=latest + :target: http://pillow.readthedocs.org/?badge=latest + :alt: Documentation Status .. image:: https://travis-ci.org/python-pillow/Pillow.svg?branch=master :target: https://travis-ci.org/python-pillow/Pillow :alt: Travis CI build status (Linux) -.. image:: https://pypip.in/v/Pillow/badge.png - :target: https://pypi.python.org/pypi/Pillow/ - :alt: Latest PyPI version +.. image:: https://travis-ci.org/python-pillow/pillow-wheels.svg?branch=latest + :target: https://travis-ci.org/python-pillow/pillow-wheels + :alt: Travis CI build status (OS X) -.. image:: https://pypip.in/d/Pillow/badge.png - :target: https://pypi.python.org/pypi/Pillow/ - :alt: Number of PyPI downloads +.. image:: https://img.shields.io/appveyor/ci/python-pillow/Pillow/master.svg?label=Windows%20build + :target: https://ci.appveyor.com/project/python-pillow/Pillow + :alt: AppVeyor CI build status (Windows) -.. image:: https://coveralls.io/repos/python-pillow/Pillow/badge.png?branch=master - :target: https://coveralls.io/r/python-pillow/Pillow?branch=master +.. image:: https://img.shields.io/pypi/v/pillow.svg + :target: https://pypi.python.org/pypi/Pillow/ + :alt: Latest PyPI version + +.. image:: https://img.shields.io/pypi/dm/pillow.svg + :target: https://pypi.python.org/pypi/Pillow/ + :alt: Number of PyPI downloads + +.. image:: https://coveralls.io/repos/python-pillow/Pillow/badge.svg?branch=master + :target: https://coveralls.io/github/python-pillow/Pillow?branch=master :alt: Code coverage -.. image:: https://landscape.io/github/python-pillow/Pillow/master/landscape.png +.. image:: https://landscape.io/github/python-pillow/Pillow/master/landscape.svg :target: https://landscape.io/github/python-pillow/Pillow/master :alt: Code health .. toctree:: :maxdepth: 2 - installation - guides + installation.rst + handbook/index.rst reference/index.rst - handbook/appendices + porting.rst + about.rst releasenotes/index.rst - about - pre-fork-readme + +.. raw:: html + + + Support via Gratipay + Indices and tables ================== diff --git a/docs/installation.rst b/docs/installation.rst index e94afa892..53d6b5bb7 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -15,46 +15,125 @@ Notes .. note:: Pillow < 2.0.0 supports Python versions 2.4, 2.5, 2.6, 2.7. -.. note:: Pillow >= 2.0.0 supports Python versions 2.6, 2.7, 3.2, 3.3, 3.4 +.. note:: Pillow >= 2.0.0 supports Python versions 2.6, 2.7, 3.2, 3.3, 3.4, 3.5 Basic Installation ------------------ .. note:: - The following instructions will install Pillow with support for most common image formats. See :ref:`external-libraries` for a full list of external libraries supported. + The following instructions will install Pillow with support for + most common image formats. See :ref:`external-libraries` for a + full list of external libraries supported. + +.. note:: + + The basic installation works on Windows and OS X using the binaries + from PyPI. Other installations require building from source as + detailed below. Install Pillow with :command:`pip`:: $ pip install Pillow -Or use :command:`easy_install` for installing `Python Eggs `_ as :command:`pip` does not support them:: +Or use :command:`easy_install` for installing `Python Eggs +`_ as +:command:`pip` does not support them:: $ easy_install Pillow -Or download and extract the `compressed archive from PyPI`_ and inside it run:: - $ python setup.py install +Windows Installation +^^^^^^^^^^^^^^^^^^^^ + +We provide Pillow binaries for Windows compiled for the matrix of +supported Pythons in both 32 and 64-bit versions in wheel, egg, and +executable installers. These binaries have all of the optional +libraries included:: + + $ pip install Pillow + +or:: + + $ easy_install Pillow + + +OS X Installation +^^^^^^^^^^^^^^^^^ + +We provide binaries for OS X for each of the supported Python versions +in the wheel format. These include support for all optional libraries +except OpenJPEG:: + + $ pip install Pillow + +Linux Installation +^^^^^^^^^^^^^^^^^^ + +We do not provide binaries for Linux. Most major Linux distributions, +including Fedora, Debian/Ubuntu and ArchLinux include Pillow in +packages that previously contained PIL e.g. ``python-imaging``. Please +consider using native operating system packages first to avoid +installation problems and/or missing library support later. + +FreeBSD Installation +^^^^^^^^^^^^^^^^^^^^ + +Pillow can be installed on FreeBSD via the official Ports or Packages systems: + +**Ports**:: + + $ cd /usr/ports/graphics/py-pillow && make install clean + +**Packages**:: + + $ pkg install py27-pillow + +.. note:: + + The `Pillow FreeBSD port + `_ and packages + are tested by the ports team with all supported FreeBSD versions + and against Python 2.x and 3.x. + + +Building From Source +-------------------- + +Download and extract the `compressed archive from PyPI`_. .. _compressed archive from PyPI: https://pypi.python.org/pypi/Pillow .. _external-libraries: External Libraries ------------------- +^^^^^^^^^^^^^^^^^^ .. note:: - You **do not need to install all external libraries supported** to use Pillow's basic features. + You **do not need to install all supported external libraries** to + use Pillow's basic features. **Zlib** and **libjpeg** are required + by default. + +.. note:: + + There are scripts to install the dependencies for some operating + systems included in the ``depends`` directory. Many of Pillow's features require external libraries: * **libjpeg** provides JPEG functionality. - * Pillow has been tested with libjpeg versions **6b**, **8**, and **9** and libjpeg-turbo version **8**. + * Pillow has been tested with libjpeg versions **6b**, **8**, **9**, and + **9a** and libjpeg-turbo version **8**. + * Starting with Pillow 3.0.0, libjpeg is required by default, but + may be disabled with the ``--disable-jpeg`` flag. * **zlib** provides access to compressed PNGs + * Starting with Pillow 3.0.0, zlib is required by default, but may + be disabled with the ``--disable-zlib`` flag. + * **libtiff** provides compressed TIFF functionality * Pillow has been tested with libtiff versions **3.x** and **4.0** @@ -64,12 +143,12 @@ 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.2**. + above uses liblcms2. Tested with **1.19** and **2.7**. * **libwebp** provides the WebP format. * Pillow has been tested with version **0.1.3**, which does not read - transparent WebP files. Versions **0.3.0** and **0.4.0** support + transparent WebP files. Versions **0.3.0** and above support transparency. * **tcl/tk** provides support for tkinter bitmap and photo images. @@ -77,8 +156,10 @@ Many of Pillow's features require external libraries: * **openjpeg** provides JPEG 2000 functionality. * 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 and Debian. -Once you have installed the prerequisites,run:: +Once you have installed the prerequisites, run:: $ pip install Pillow @@ -92,8 +173,14 @@ line:: $ CFLAGS="-I/usr/pkg/include" pip install pillow +If Pillow has been previously built without the required +prerequisites, it may be necessary to manually clear the pip cache or +build without cache using the ``--no-cache-dir`` option to force a +build with newly installed external libraries. + + Build Options -------------- +^^^^^^^^^^^^^ * Environment Variable: ``MAX_CONCURRENCY=n``. By default, Pillow will use multiprocessing to build the extension on all available CPUs, @@ -119,12 +206,21 @@ Sample Usage:: $ MAX_CONCURRENCY=1 python setup.py build_ext --enable-[feature] install -OS X Installation ------------------ +or using pip:: -We provide binaries for OS X in the form of `Python Wheels `_. Alternatively you can compile Pillow from soure with XCode. + $ pip install pillow --global-option="build_ext" --global-option="--enable-[feature]" -The easiest way to install external libraries is via `Homebrew `_. After you install Homebrew, run:: + +Building on OS X +^^^^^^^^^^^^^^^^ + +Xcode is required to compile portions of Pillow. Either install the +full package from the app store, or run ``xcode-select --install`` +from the command line. It may be necessary to run ``sudo xcodebuild +-license`` to accept the license prior to using the tools. + +The easiest way to install external libraries is via `Homebrew +`_. After you install Homebrew, run:: $ brew install libtiff libjpeg webp little-cms2 @@ -132,43 +228,19 @@ Install Pillow with:: $ pip install Pillow -Windows Installation --------------------- +or from within the uncompressed source directory:: -We provide binaries for Windows in the form of Python Eggs and `Python Wheels -`_: + $ python setup.py install -Python Eggs -^^^^^^^^^^^ +Building on Windows +^^^^^^^^^^^^^^^^^^^ -.. note:: +We don't recommend trying to build on Windows. It is a maze of twisty +passages, mostly dead ends. There are build scripts and notes for the +Windows build in the ``winbuild`` directory. - :command:`pip` does not support Python Eggs; use :command:`easy_install` - instead. - -:: - - $ easy_install Pillow - -Python Wheels -^^^^^^^^^^^^^ - -.. Note:: Requires setuptools >=0.8 and pip >=1.4.1. Some older versions of pip required the ``--use-wheel`` flag. - -:: - - $ pip install Pillow - -If the above does not work, it's likely because we haven't uploaded a -wheel for the latest version of Pillow. In that case, try pinning it -to a specific version: - -:: - - $ pip install Pillow==2.6.1 - -FreeBSD Installation --------------------- +Building on FreeBSD +^^^^^^^^^^^^^^^^^^^ .. Note:: Only FreeBSD 10 tested @@ -184,16 +256,12 @@ Prerequisites are installed on **FreeBSD 10** with:: $ sudo pkg install jpeg tiff webp lcms2 freetype2 -Linux Installation ------------------- -.. note:: +Building on Linux +^^^^^^^^^^^^^^^^^ - Most major Linux distributions, including Fedora, Debian/Ubuntu and ArchLinux include Pillow in packages that previously contained PIL e.g. ``python-imaging``. Please consider using native operating system packages first to avoid installation problems and/or missing library support later. - -**We do not provide binaries for Linux.** If you didn't build Python from -source, make sure you have Python's development libraries installed. In Debian -or Ubuntu:: +If you didn't build Python from source, make sure you have Python's +development libraries installed. In Debian or Ubuntu:: $ sudo apt-get install python-dev python-setuptools @@ -203,7 +271,9 @@ Or for Python 3:: In Fedora, the command is:: - $ sudo yum install python-devel + $ sudo dnf install python-devel redhat-rpm-config + +.. Note:: ``redhat-rpm-config`` is required on Fedora 23, but not earlier versions. Prerequisites are installed on **Ubuntu 12.04 LTS** or **Raspian Wheezy 7.0** with:: @@ -216,15 +286,13 @@ Prerequisites are installed on **Ubuntu 14.04 LTS** with:: $ sudo apt-get install libtiff5-dev libjpeg8-dev zlib1g-dev \ libfreetype6-dev liblcms2-dev libwebp-dev tcl8.6-dev tk8.6-dev python-tk -Prerequisites are installed on **Fedora 20** with:: +Prerequisites are installed on **Fedora 23** with:: - $ sudo yum install libtiff-devel libjpeg-devel libzip-devel freetype-devel \ + $ sudo dnf install libtiff-devel libjpeg-devel libzip-devel freetype-devel \ lcms2-devel libwebp-devel tcl-devel tk-devel - - Platform Support ---------------- @@ -235,48 +303,59 @@ current versions of Linux, OS X, and Windows. .. note:: - Contributors please test Pillow on your platform then update this document and send a pull request. + Contributors please test Pillow on your platform then update this + document and send a pull request. -+----------------------------------+-------------+------------------------------+------------------------------+-----------------------+ -|**Operating system** |**Supported**|**Tested Python versions** |**Tested Pillow versions** |**Tested processors** | -+----------------------------------+-------------+------------------------------+------------------------------+-----------------------+ -| Mac OS X 10.10 Yosemite |Yes | 2.7,3.3,3.4 | 2.8.1 |x86-64 | -+----------------------------------+-------------+------------------------------+------------------------------+-----------------------+ -| Mac OS X 10.9 Mavericks |Yes | 2.7,3.4 | 2.6.1 |x86-64 | -+----------------------------------+-------------+------------------------------+------------------------------+-----------------------+ -| Mac OS X 10.8 Mountain Lion |Yes | 2.6,2.7,3.2,3.3 | |x86-64 | -+----------------------------------+-------------+------------------------------+------------------------------+-----------------------+ -| Redhat Linux 6 |Yes | 2.6 | |x86 | -+----------------------------------+-------------+------------------------------+------------------------------+-----------------------+ -| CentOS 6.3 |Yes | 2.7,3.3 | |x86 | -+----------------------------------+-------------+------------------------------+------------------------------+-----------------------+ -| Fedora 20 |Yes | 2.7,3.3 | 2.3.0 |x86-64 | -+----------------------------------+-------------+------------------------------+------------------------------+-----------------------+ -| Ubuntu Linux 10.04 LTS |Yes | 2.6 | 2.3.0 |x86,x86-64 | -+----------------------------------+-------------+------------------------------+------------------------------+-----------------------+ -| Ubuntu Linux 12.04 LTS |Yes | 2.6,2.7,3.2,3.3,PyPy2.4, | 2.6.1 |x86,x86-64 | -| | | PyPy3,v2.3 | | | -| | | | | | -| | | 2.7,3.2 | 2.6.1 |ppc | -+----------------------------------+-------------+------------------------------+------------------------------+-----------------------+ -| Ubuntu Linux 14.04 LTS |Yes | 2.7,3.2,3.3,3.4 | 2.3.0 |x86 | -+----------------------------------+-------------+------------------------------+------------------------------+-----------------------+ -| Raspian Wheezy |Yes | 2.7,3.2 | 2.3.0 |arm | -+----------------------------------+-------------+------------------------------+------------------------------+-----------------------+ -| Gentoo Linux |Yes | 2.7,3.2 | 2.1.0 |x86-64 | -+----------------------------------+-------------+------------------------------+------------------------------+-----------------------+ -| FreeBSD 10 |Yes | 2.7,3.4 | 2.4,2.3.1 |x86-64 | -+----------------------------------+-------------+------------------------------+------------------------------+-----------------------+ -| Windows 7 Pro |Yes | 2.7,3.2,3.3 | 2.2.1 |x86-64 | -+----------------------------------+-------------+------------------------------+------------------------------+-----------------------+ -| Windows Server 2008 R2 Enterprise|Yes | 3.3 | |x86-64 | -+----------------------------------+-------------+------------------------------+------------------------------+-----------------------+ -| Windows 8 Pro |Yes | 2.6,2.7,3.2,3.3,3.4a3 | 2.2.0 |x86,x86-64 | -+----------------------------------+-------------+------------------------------+------------------------------+-----------------------+ -| Windows 8.1 Pro |Yes | 2.6,2.7,3.2,3.3,3.4 | 2.3.0, 2.4.0 |x86,x86-64 | -+----------------------------------+-------------+------------------------------+------------------------------+-----------------------+ ++----------------------------------+-------------+------------------------------+--------------------------------+-----------------------+ +|**Operating system** |**Supported**|**Tested Python versions** |**Latest tested Pillow version**|**Tested processors** | ++----------------------------------+-------------+------------------------------+--------------------------------+-----------------------+ +| Mac OS X 10.11 El Capitan |Yes | 2.7,3.3,3.4,3.5 | 3.2.0 |x86-64 | ++----------------------------------+-------------+------------------------------+--------------------------------+-----------------------+ +| Mac OS X 10.10 Yosemite |Yes | 2.7,3.3,3.4 | 3.0.0 |x86-64 | ++----------------------------------+-------------+------------------------------+--------------------------------+-----------------------+ +| Mac OS X 10.9 Mavericks |Yes | 2.7,3.2,3.3,3.4 | 3.0.0 |x86-64 | ++----------------------------------+-------------+------------------------------+--------------------------------+-----------------------+ +| Mac OS X 10.8 Mountain Lion |Yes | 2.6,2.7,3.2,3.3 | |x86-64 | ++----------------------------------+-------------+------------------------------+--------------------------------+-----------------------+ +| Redhat Linux 6 |Yes | 2.6 | |x86 | ++----------------------------------+-------------+------------------------------+--------------------------------+-----------------------+ +| CentOS 6.3 |Yes | 2.7,3.3 | |x86 | ++----------------------------------+-------------+------------------------------+--------------------------------+-----------------------+ +| Fedora 23 |Yes | 2.7,3.4 | 3.1.0 |x86-64 | ++----------------------------------+-------------+------------------------------+--------------------------------+-----------------------+ +| Ubuntu Linux 10.04 LTS |Yes | 2.6 | 2.3.0 |x86,x86-64 | ++----------------------------------+-------------+------------------------------+--------------------------------+-----------------------+ +| Ubuntu Linux 12.04 LTS |Yes | 2.6,2.7,3.2,3.3,3.4,3.5 | 3.1.0 |x86,x86-64 | +| | | PyPy2.4,PyPy3,v2.3 | | | +| | | | | | +| | | 2.7,3.2 | 2.6.1 |ppc | ++----------------------------------+-------------+------------------------------+--------------------------------+-----------------------+ +| Ubuntu Linux 14.04 LTS |Yes | 2.7,3.4 | 3.1.0 |x86-64 | ++----------------------------------+-------------+------------------------------+--------------------------------+-----------------------+ +| Debian 8.2 Jessie |Yes | 2.7,3.4 | 3.1.0 |x86-64 | ++----------------------------------+-------------+------------------------------+--------------------------------+-----------------------+ +| Raspian Jessie |Yes | 2.7,3.4 | 3.1.0 |arm | ++----------------------------------+-------------+------------------------------+--------------------------------+-----------------------+ +| Gentoo Linux |Yes | 2.7,3.2 | 2.1.0 |x86-64 | ++----------------------------------+-------------+------------------------------+--------------------------------+-----------------------+ +| FreeBSD 10.2 |Yes | 2.7,3.4 | 3.1.0 |x86-64 | ++----------------------------------+-------------+------------------------------+--------------------------------+-----------------------+ +| Windows 7 Pro |Yes | 2.7,3.2,3.3 | 2.2.1 |x86-64 | ++----------------------------------+-------------+------------------------------+--------------------------------+-----------------------+ +| Windows Server 2008 R2 Enterprise|Yes | 3.3 | |x86-64 | ++----------------------------------+-------------+------------------------------+--------------------------------+-----------------------+ +| Windows Server 2012 R2 |Yes | 2.7,3.3,3.4 | 3.0.0 |x86-64 | ++----------------------------------+-------------+------------------------------+--------------------------------+-----------------------+ +| Windows 8 Pro |Yes | 2.6,2.7,3.2,3.3,3.4a3 | 2.2.0 |x86,x86-64 | ++----------------------------------+-------------+------------------------------+--------------------------------+-----------------------+ +| Windows 8.1 Pro |Yes | 2.6,2.7,3.2,3.3,3.4 | 2.4.0 |x86,x86-64 | ++----------------------------------+-------------+------------------------------+--------------------------------+-----------------------+ Old Versions ------------ -You can download old distributions from `PyPI `_. Only the latest 1.x and 2.x releases are visible, but all releases are available by direct URL access e.g. https://pypi.python.org/pypi/Pillow/1.0. +You can download old distributions from `PyPI +`_. Only the latest major +releases for Python 2.x and 3.x are visible, but all releases are +available by direct URL access +e.g. https://pypi.python.org/pypi/Pillow/1.0. diff --git a/docs/porting-pil-to-pillow.rst b/docs/porting.rst similarity index 91% rename from docs/porting-pil-to-pillow.rst rename to docs/porting.rst index 7ad781186..50b713fac 100644 --- a/docs/porting-pil-to-pillow.rst +++ b/docs/porting.rst @@ -1,5 +1,7 @@ -Porting existing PIL-based code to Pillow -========================================= +Porting +======= + +**Porting existing PIL-based code to Pillow** Pillow is a functional drop-in replacement for the Python Imaging Library. To run your existing PIL-compatible code with Pillow, it needs to be modified to diff --git a/docs/pre-fork-readme.rst b/docs/pre-fork-readme.rst deleted file mode 100644 index f50e2ef85..000000000 --- a/docs/pre-fork-readme.rst +++ /dev/null @@ -1,306 +0,0 @@ -Pre-fork README -=============== - -What follows is the untouched, original pre-fork PIL 1.1.7 README file contents. - -:: - - The Python Imaging Library - $Id$ - - Release 1.1.7 (November 15, 2009) - - ==================================================================== - The Python Imaging Library 1.1.7 - ==================================================================== - - Contents - -------- - - + Introduction - + Support Options - - Commercial support - - Free support - + Software License - + Build instructions (all platforms) - - Additional notes for Mac OS X - - Additional notes for Windows - - -------------------------------------------------------------------- - Introduction - -------------------------------------------------------------------- - - The Python Imaging Library (PIL) adds image processing capabilities - to your Python environment. This library provides extensive file - format support, an efficient internal representation, and powerful - image processing capabilities. - - This source kit has been built and tested with Python 2.0 and newer, - on Windows, Mac OS X, and major Unix platforms. Large parts of the - library also work on 1.5.2 and 1.6. - - The main distribution site for this software is: - - http://www.pythonware.com/products/pil/ - - That site also contains information about free and commercial support - options, PIL add-ons, answers to frequently asked questions, and more. - - - Development versions (alphas, betas) are available here: - - http://effbot.org/downloads/ - - - The PIL handbook is not included in this distribution; to get the - latest version, check: - - http://www.pythonware.com/library/ - http://effbot.org/books/imagingbook/ (drafts) - - - For installation and licensing details, see below. - - - -------------------------------------------------------------------- - Support Options - -------------------------------------------------------------------- - - + Commercial Support - - Secret Labs (PythonWare) offers support contracts for companies using - the Python Imaging Library in commercial applications, and in mission- - critical environments. The support contract includes technical support, - bug fixes, extensions to the PIL library, sample applications, and more. - - For the full story, check: - - http://www.pythonware.com/products/pil/support.htm - - - + Free Support - - For support and general questions on the Python Imaging Library, send - e-mail to the Image SIG mailing list: - - image-sig@python.org - - You can join the Image SIG by sending a mail to: - - image-sig-request@python.org - - Put "subscribe" in the message body to automatically subscribe to the - list, or "help" to get additional information. Alternatively, you can - send your questions to the Python mailing list, python-list@python.org, - or post them to the newsgroup comp.lang.python. DO NOT SEND SUPPORT - QUESTIONS TO PYTHONWARE ADDRESSES. - - - -------------------------------------------------------------------- - Software License - -------------------------------------------------------------------- - - The Python Imaging Library is - - Copyright (c) 1997-2009 by Secret Labs AB - Copyright (c) 1995-2009 by Fredrik Lundh - - By obtaining, using, and/or copying this software and/or its - associated documentation, you agree that you have read, understood, - and will comply with the following terms and conditions: - - Permission to use, copy, modify, and distribute this software and its - associated documentation for any purpose and without fee is hereby - granted, provided that the above copyright notice appears in all - copies, and that both that copyright notice and this permission notice - appear in supporting documentation, and that the name of Secret Labs - AB or the author not be used in advertising or publicity pertaining to - distribution of the software without specific, written prior - permission. - - SECRET LABS AB AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO - THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND - FITNESS. IN NO EVENT SHALL SECRET LABS AB OR THE AUTHOR BE LIABLE FOR - ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES - WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN - ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT - OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. - - - -------------------------------------------------------------------- - Build instructions (all platforms) - -------------------------------------------------------------------- - - For a list of changes in this release, see the CHANGES document. - - 0. If you're in a hurry, try this: - - $ tar xvfz Imaging-1.1.7.tar.gz - $ cd Imaging-1.1.7 - $ python setup.py install - - If you prefer to know what you're doing, read on. - - - 1. Prerequisites. - - If you need any of the features described below, make sure you - have the necessary libraries before building PIL. - - feature library - ----------------------------------------------------------------- - JPEG support libjpeg (6a or 6b) - - http://www.ijg.org - http://www.ijg.org/files/jpegsrc.v6b.tar.gz - ftp://ftp.uu.net/graphics/jpeg/ - - PNG support zlib (1.2.3 or later is recommended) - - http://www.gzip.org/zlib/ - - OpenType/TrueType freetype2 (2.3.9 or later is recommended) - support - http://www.freetype.org - http://freetype.sourceforge.net - - CMS support littleCMS (1.1.5 or later is recommended) - support - http://www.littlecms.com/ - - If you have a recent Linux version, the libraries provided with the - operating system usually work just fine. If some library is - missing, installing a prebuilt version (jpeg-devel, zlib-devel, - etc) is usually easier than building from source. For example, for - Ubuntu 9.10 (karmic), you can install the following libraries: - - sudo apt-get install libjpeg62-dev - sudo apt-get install zlib1g-dev - sudo apt-get install libfreetype6-dev - sudo apt-get install liblcms1-dev - - If you're using Mac OS X, you can use the 'fink' tool to install - missing libraries (also see the Mac OS X section below). - - Similar tools are available for many other platforms. - - - 2. To build under Python 1.5.2, you need to install the stand-alone - version of the distutils library: - - http://www.python.org/sigs/distutils-sig/download.html - - You can fetch distutils 1.0.2 from the Python source repository: - - svn export http://svn.python.org/projects/python/tags/Distutils-1_0_2/Lib/distutils/ - - For newer releases, the distutils library is included in the - Python standard library. - - NOTE: Version 1.1.7 is not fully compatible with 1.5.2. Some - more recent additions to the library may not work, but the core - functionality is available. - - - 3. If you didn't build Python from sources, make sure you have - Python's build support files on your machine. If you've down- - loaded a prebuilt package (e.g. a Linux RPM), you probably - need additional developer packages. Look for packages named - "python-dev", "python-devel", or similar. For example, for - Ubuntu 9.10 (karmic), use the following command: - - sudo apt-get install python-dev - - - 4. When you have everything you need, unpack the PIL distribution - (the file Imaging-1.1.7.tar.gz) in a suitable work directory: - - $ cd MyExtensions # example - $ gunzip Imaging-1.1.7.tar.gz - $ tar xvf Imaging-1.1.7.tar - - - 5. Build the library. We recommend that you do an in-place build, - and run the self test before installing. - - $ cd Imaging-1.1.7 - $ python setup.py build_ext -i - $ python selftest.py - - During the build process, the setup.py will display a summary - report that lists what external components it found. The self- - test will display a similar report, with what external components - the tests found in the actual build files: - - ---------------------------------------------------------------- - PIL 1.1.7 SETUP SUMMARY - ---------------------------------------------------------------- - *** TKINTER support not available (Tcl/Tk 8.5 libraries needed) - --- JPEG support available - --- ZLIB (PNG/ZIP) support available - --- FREETYPE support available - ---------------------------------------------------------------- - - Make sure that the optional components you need are included. - - If the build script won't find a given component, you can edit the - setup.py file and set the appropriate ROOT variable. For details, - see instructions in the file. - - If the build script finds the component, but the tests cannot - identify it, try rebuilding *all* modules: - - $ python setup.py clean - $ python setup.py build_ext -i - - - 6. If the setup.py and selftest.py commands finish without any - errors, you're ready to install the library: - - $ python setup.py install - - (depending on how Python has been installed on your machine, - you might have to log in as a superuser to run the 'install' - command, or use the 'sudo' command to run 'install'.) - - - -------------------------------------------------------------------- - Additional notes for Mac OS X - -------------------------------------------------------------------- - - On Mac OS X you will usually install additional software such as - libjpeg or freetype with the "fink" tool, and then it ends up in - "/sw". If you have installed the libraries elsewhere, you may have - to tweak the "setup.py" file before building. - - - -------------------------------------------------------------------- - Additional notes for Windows - -------------------------------------------------------------------- - - On Windows, you need to tweak the ROOT settings in the "setup.py" - file, to make it find the external libraries. See comments in the - file for details. - - Make sure to build PIL and the external libraries with the same - runtime linking options as was used for the Python interpreter - (usually /MD, under Visual Studio). - - - Note that most Python distributions for Windows include libraries - compiled for Microsoft Visual Studio. You can get the free Express - edition of Visual Studio from: - - http://www.microsoft.com/Express/ - - To build extensions using other tool chains, see the "Using - non-Microsoft compilers on Windows" section in the distutils handbook: - - http://www.python.org/doc/current/inst/non-ms-compilers.html - - For additional information on how to build extensions using the - popular MinGW compiler, see: - - http://mingw.org (compiler) - http://sebsauvage.net/python/mingw.html (build instructions) - http://sourceforge.net/projects/gnuwin32 (prebuilt libraries) diff --git a/docs/reference/Image.rst b/docs/reference/Image.rst index 974d84a6e..ac8b6f506 100644 --- a/docs/reference/Image.rst +++ b/docs/reference/Image.rst @@ -181,6 +181,18 @@ Instances of the :py:class:`Image` class have the following attributes: :type: ``(width, height)`` +.. py:attribute:: width + + Image width, in pixels. + + :type: :py:class:`int` + +.. py:attribute:: height + + Image height, in pixels. + + :type: :py:class:`int` + .. py:attribute:: palette Colour palette table, if any. If mode is “P”, this should be an instance of diff --git a/docs/reference/ImageCms.rst b/docs/reference/ImageCms.rst index 2d5bb1388..351d3dc2c 100644 --- a/docs/reference/ImageCms.rst +++ b/docs/reference/ImageCms.rst @@ -11,3 +11,412 @@ Cazabon's PyCMS library. .. automodule:: PIL.ImageCms :members: :noindex: + +CmsProfile +---------- + +The ICC color profiles are wrapped in an instance of the class +:py:class:`CmsProfile`. The specification ICC.1:2010 contains more +information about the meaning of the values in ICC profiles. + +For convenience, all XYZ-values are also given as xyY-values (so they +can be easily displayed in a chromaticity diagram, for example). + +.. py:class:: CmsProfile + + .. py:attribute:: creation_date + + Date and time this profile was first created (see 7.2.1 of ICC.1:2010). + + :type: :py:class:`datetime.datetime` or ``None`` + + .. py:attribute:: version + + The version number of the ICC standard that this profile follows + (e.g. `2.0`). + + :type: :py:class:`float` + + .. py:attribute:: icc_version + + Same as `version`, but in encoded format (see 7.2.4 of ICC.1:2010). + + .. py:attribute:: device_class + + 4-character string identifying the profile class. One of + ``scnr``, ``mntr``, ``prtr``, ``link``, ``spac``, ``abst``, + ``nmcl`` (see 7.2.5 of ICC.1:2010 for details). + + :type: :py:class:`string` + + .. py:attribute:: xcolor_space + + 4-character string (padded with whitespace) identifying the color + space, e.g. ``XYZ␣``, ``RGB␣`` or ``CMYK`` (see 7.2.6 of + ICC.1:2010 for details). + + Note that the deprecated attribute ``color_space`` contains an + interpreted (non-padded) variant of this (but can be empty on + unknown input). + + :type: :py:class:`string` + + .. py:attribute:: connection_space + + 4-character string (padded with whitespace) identifying the color + space on the B-side of the transform (see 7.2.7 of ICC.1:2010 for + details). + + Note that the deprecated attribute ``pcs`` contains an interpreted + (non-padded) variant of this (but can be empty on unknown input). + + :type: :py:class:`string` + + .. py:attribute:: header_flags + + The encoded header flags of the profile (see 7.2.11 of ICC.1:2010 + for details). + + :type: :py:class:`int` + + .. py:attribute:: header_manufacturer + + 4-character string (padded with whitespace) identifying the device + manufacturer, which shall match the signature contained in the + appropriate section of the ICC signature registry found at + www.color.org (see 7.2.12 of ICC.1:2010). + + :type: :py:class:`string` + + .. py:attribute:: header_model + + 4-character string (padded with whitespace) identifying the device + model, which shall match the signature contained in the + appropriate section of the ICC signature registry found at + www.color.org (see 7.2.13 of ICC.1:2010). + + :type: :py:class:`string` + + .. py:attribute:: attributes + + Flags used to identify attributes unique to the particular device + setup for which the profile is applicable (see 7.2.14 of + ICC.1:2010 for details). + + :type: :py:class:`int` + + .. py:attribute:: rendering_intent + + The rendering intent to use when combining this profile with + another profile (usually overridden at run-time, but provided here + for DeviceLink and embedded source profiles, see 7.2.15 of ICC.1:2010). + + One of ``ImageCms.INTENT_ABSOLUTE_COLORIMETRIC``, ``ImageCms.INTENT_PERCEPTUAL``, + ``ImageCms.INTENT_RELATIVE_COLORIMETRIC`` and ``ImageCms.INTENT_SATURATION``. + + :type: :py:class:`int` + + .. py:attribute:: profile_id + + A sequence of 16 bytes identifying the profile (via a specially + constructed MD5 sum), or 16 binary zeroes if the profile ID has + not been calculated (see 7.2.18 of ICC.1:2010). + + :type: :py:class:`bytes` + + .. py:attribute:: copyright + + The text copyright information for the profile (see 9.2.21 of ICC.1:2010). + + :type: :py:class:`unicode` or ``None`` + + .. py:attribute:: manufacturer + + 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 + 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 + 9.2.41 of ICC.1:2010). + + :type: :py:class:`unicode` or ``None`` + + .. py:attribute:: target + + The name of the registered characterization data set, or the + measurement data for a characterization target (see 9.2.14 of + ICC.1:2010). + + :type: :py:class:`unicode` or ``None`` + + .. py:attribute:: red_colorant + + The first column in the matrix used in matrix/TRC transforms (see 9.2.44 of ICC.1:2010). + + :type: ``((X, Y, Z), (x, y, Y))`` or ``None`` + + .. py:attribute:: green_colorant + + The second column in the matrix used in matrix/TRC transforms (see 9.2.30 of ICC.1:2010). + + :type: ``((X, Y, Z), (x, y, Y))`` or ``None`` + + .. py:attribute:: blue_colorant + + The third column in the matrix used in matrix/TRC transforms (see 9.2.4 of ICC.1:2010). + + :type: ``((X, Y, Z), (x, y, Y))`` or ``None`` + + .. py:attribute:: luminance + + The absolute luminance of emissive devices in candelas per square + metre as described by the Y channel (see 9.2.32 of ICC.1:2010). + + :type: ``((X, Y, Z), (x, y, Y))`` or ``None`` + + .. py:attribute:: chromaticity + + The data of the phosphor/colorant chromaticity set used (red, + green and blue channels, see 9.2.16 of ICC.1:2010). + + :type: ``((x, y, Y), (x, y, Y), (x, y, Y))`` or ``None`` + + .. py:attribute:: chromatic_adaption + + The chromatic adaption matrix converts a color measured using the + actual illumination conditions and relative to the actual adopted + white, to an color relative to the PCS adopted white, with + complete adaptation from the actual adopted white chromaticity to + the PCS adopted white chromaticity (see 9.2.15 of ICC.1:2010). + + Two matrices are returned, one in (X, Y, Z) space and one in (x, y, Y) space. + + :type: 2-tuple of 3-tuple, the first with (X, Y, Z) and the second with (x, y, Y) values + + .. py:attribute:: colorant_table + + This tag identifies the colorants used in the profile by a unique + name and set of PCSXYZ or PCSLAB values (see 9.2.19 of + ICC.1:2010). + + :type: list of strings + + .. py:attribute:: colorant_table_out + + This tag identifies the colorants used in the profile by a unique + name and set of PCSLAB values (for DeviceLink profiles only, see + 9.2.19 of ICC.1:2010). + + :type: list of strings + + .. py:attribute:: colorimetric_intent + + 4-character string (padded with whitespace) identifying the image + state of PCS colorimetry produced using the colorimetric intent + transforms (see 9.2.20 of ICC.1:2010 for details). + + :type: :py:class:`string` or ``None`` + + .. py:attribute:: perceptual_rendering_intent_gamut + + 4-character string (padded with whitespace) identifying the (one) + standard reference medium gamut (see 9.2.37 of ICC.1:2010 for + details). + + :type: :py:class:`string` or ``None`` + + .. py:attribute:: saturation_rendering_intent_gamut + + 4-character string (padded with whitespace) identifying the (one) + standard reference medium gamut (see 9.2.37 of ICC.1:2010 for + details). + + :type: :py:class:`string` or ``None`` + + .. py:attribute:: technology + + 4-character string (padded with whitespace) identifying the device + technology (see 9.2.47 of ICC.1:2010 for details). + + :type: :py:class:`string` or ``None`` + + .. py:attribute:: media_black_point + + This tag specifies the media black point and is used for + generating absolute colorimetry. + + This tag was available in ICC 3.2, but it is removed from + version 4. + + :type: ``((X, Y, Z), (x, y, Y))`` or ``None`` + + .. py:attribute:: media_white_point_temperature + + Calculates the white point temperature (see the LCMS documentation + for more information). + + :type: :py:class:`float` or `None` + + .. py:attribute:: viewing_condition + + 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. + + This tag was available in ICC 3.2, but it is removed from + version 4. + + :type: :py:class:`unicode` or ``None`` + + .. py:attribute:: red_primary + + The XYZ-transformed of the RGB primary color red (1, 0, 0). + + :type: ``((X, Y, Z), (x, y, Y))`` or ``None`` + + .. py:attribute:: green_primary + + The XYZ-transformed of the RGB primary color green (0, 1, 0). + + :type: ``((X, Y, Z), (x, y, Y))`` or ``None`` + + .. py:attribute:: blue_primary + + The XYZ-transformed of the RGB primary color blue (0, 0, 1). + + :type: ``((X, Y, Z), (x, y, Y))`` or ``None`` + + .. py:attribute:: is_matrix_shaper + + True if this profile is implemented as a matrix shaper (see + documentation on LCMS). + + :type: :py:class:`bool` + + .. py:attribute:: clut + + Returns a dictionary of all supported intents and directions for + the CLUT model. + + The dictionary is indexed by intents + (``ImageCms.INTENT_ABSOLUTE_COLORIMETRIC``, + ``ImageCms.INTENT_PERCEPTUAL``, + ``ImageCms.INTENT_RELATIVE_COLORIMETRIC`` and + ``ImageCms.INTENT_SATURATION``). + + The values are 3-tuples indexed by directions + (``ImageCms.DIRECTION_INPUT``, ``ImageCms.DIRECTION_OUTPUT``, + ``ImageCms.DIRECTION_PROOF``). + + The elements of the tuple are booleans. If the value is ``True``, + that intent is supported for that direction. + + :type: :py:class:`dict` of boolean 3-tuples + + .. py:attribute:: intent_supported + + Returns a dictionary of all supported intents and directions. + + The dictionary is indexed by intents + (``ImageCms.INTENT_ABSOLUTE_COLORIMETRIC``, + ``ImageCms.INTENT_PERCEPTUAL``, + ``ImageCms.INTENT_RELATIVE_COLORIMETRIC`` and + ``ImageCms.INTENT_SATURATION``). + + The values are 3-tuples indexed by directions + (``ImageCms.DIRECTION_INPUT``, ``ImageCms.DIRECTION_OUTPUT``, + ``ImageCms.DIRECTION_PROOF``). + + The elements of the tuple are booleans. If the value is ``True``, + that intent is supported for that direction. + + :type: :py:class:`dict` of boolean 3-tuples + + .. py:attribute:: color_space + + Deprecated but retained for backwards compatibility. + Interpreted value of :py:attr:`.xcolor_space`. May be the + empty string if value could not be decoded. + + :type: :py:class:`string` + + .. py:attribute:: pcs + + Deprecated but retained for backwards compatibility. + Interpreted value of :py:attr:`.connection_space`. May be + the empty string if value could not be decoded. + + :type: :py:class:`string` + + .. py:attribute:: product_model + + Deprecated but retained for backwards compatibility. + ASCII-encoded value of :py:attr:`.model`. + + :type: :py:class:`string` + + .. py:attribute:: product_manufacturer + + Deprecated but retained for backwards compatibility. + ASCII-encoded value of :py:attr:`.manufacturer`. + + :type: :py:class:`string` + + .. py:attribute:: product_copyright + + Deprecated but retained for backwards compatibility. + ASCII-encoded value of :py:attr:`.copyright`. + + :type: :py:class:`string` + + .. py:attribute:: product_description + + Deprecated but retained for backwards compatibility. + ASCII-encoded value of :py:attr:`.profile_description`. + + :type: :py:class:`string` + + .. py:attribute:: product_desc + + Deprecated but retained for backwards compatibility. + ASCII-encoded value of :py:attr:`.profile_description`. + + This alias of :py:attr:`.product_description` used to + contain a derived informative string about the profile, + depending on the value of the description, copyright, + manufacturer and model fields). + + :type: :py:class:`string` + + There is one function defined on the class: + + .. py:method:: is_intent_supported(intent, direction) + + Returns if the intent is supported for the given direction. + + Note that you can also get this information for all intents and directions + with :py:attr:`.intent_supported`. + + :param intent: One of ``ImageCms.INTENT_ABSOLUTE_COLORIMETRIC``, + ``ImageCms.INTENT_PERCEPTUAL``, + ``ImageCms.INTENT_RELATIVE_COLORIMETRIC`` + and ``ImageCms.INTENT_SATURATION``. + :param direction: One of ``ImageCms.DIRECTION_INPUT``, + ``ImageCms.DIRECTION_OUTPUT`` + and ``ImageCms.DIRECTION_PROOF`` + :return: Boolean if the intent and direction is supported. diff --git a/docs/reference/ImageDraw.rst b/docs/reference/ImageDraw.rst index e6d5c36ee..842407c90 100644 --- a/docs/reference/ImageDraw.rst +++ b/docs/reference/ImageDraw.rst @@ -81,7 +81,7 @@ Example: Draw Partial Opacity Text from PIL import Image, ImageDraw, ImageFont # get an image - base = Image.open('Pillow/Tests/images/lena.png').convert('RGBA') + base = Image.open('Pillow/Tests/images/lena.png').convert('RGBA') # make a blank image for the text, initialized to transparent text color txt = Image.new('RGBA', base.size, (255,255,255,0)) @@ -89,7 +89,7 @@ Example: Draw Partial Opacity Text # get a font fnt = ImageFont.truetype('Pillow/Tests/fonts/FreeMono.ttf', 40) # get a drawing context - d = ImageDraw.Draw(txt) + d = ImageDraw.Draw(txt) # draw text, half opacity d.text((10,10), "Hello", font=fnt, fill=(255,255,255,128)) @@ -227,21 +227,49 @@ Methods Draw a shape. -.. py:method:: PIL.ImageDraw.Draw.text(xy, text, fill=None, font=None, anchor=None) +.. py:method:: PIL.ImageDraw.Draw.text(xy, text, fill=None, font=None, anchor=None, spacing=0, align="left") + + Draws the string at the given position. + + :param xy: Top left corner of the text. + :param text: Text to be drawn. If it contains any newline characters, + the text is passed on to multiline_text() + :param fill: Color to use for the text. + :param font: An :py:class:`~PIL.ImageFont.ImageFont` instance. + :param spacing: If the text is passed on to multiline_text(), + the number of pixels between lines. + :param align: If the text is passed on to multiline_text(), + "left", "center" or "right". + + +.. py:method:: PIL.ImageDraw.Draw.multiline_text(xy, text, fill=None, font=None, anchor=None, spacing=0, align="left") Draws the string at the given position. :param xy: Top left corner of the text. :param text: Text to be drawn. - :param font: An :py:class:`~PIL.ImageFont.ImageFont` instance. :param fill: Color to use for the text. + :param font: An :py:class:`~PIL.ImageFont.ImageFont` instance. + :param spacing: The number of pixels between lines. + :param align: "left", "center" or "right". -.. py:method:: PIL.ImageDraw.Draw.textsize(text, font=None) +.. py:method:: PIL.ImageDraw.Draw.textsize(text, font=None, spacing=0) + + Return the size of the given string, in pixels. + + :param text: Text to be measured. If it contains any newline characters, + the text is passed on to multiline_textsize() + :param font: An :py:class:`~PIL.ImageFont.ImageFont` instance. + :param spacing: If the text is passed on to multiline_textsize(), + the number of pixels between lines. + +.. py:method:: PIL.ImageDraw.Draw.multiline_textsize(text, font=None, spacing=0) Return the size of the given string, in pixels. :param text: Text to be measured. :param font: An :py:class:`~PIL.ImageFont.ImageFont` instance. + :param spacing: The number of pixels between lines. Legacy API ---------- @@ -256,21 +284,6 @@ these methods. Do not mix the old and new calling conventions. :rtype: :py:class:`~PIL.ImageDraw.Draw` -.. py:method:: PIL.ImageDraw.Draw.setink(ink) - - .. deprecated:: 1.1.5 - - Sets the color to use for subsequent draw and fill operations. - -.. py:method:: PIL.ImageDraw.Draw.setfill(fill) - - .. deprecated:: 1.1.5 - - Sets the fill mode. - - If the mode is 0, subsequently drawn shapes (like polygons and rectangles) - are outlined. If the mode is 1, they are filled. - .. py:method:: PIL.ImageDraw.Draw.setfont(font) .. deprecated:: 1.1.5 diff --git a/docs/reference/ImageGrab.rst b/docs/reference/ImageGrab.rst index 117be885b..ec7c42082 100644 --- a/docs/reference/ImageGrab.rst +++ b/docs/reference/ImageGrab.rst @@ -1,21 +1,21 @@ .. py:module:: PIL.ImageGrab .. py:currentmodule:: PIL.ImageGrab -:py:mod:`ImageGrab` Module (Windows-only) -========================================= +:py:mod:`ImageGrab` Module (OS X and Windows only) +================================================== The :py:mod:`ImageGrab` module can be used to copy the contents of the screen or the clipboard to a PIL image memory. -.. note:: The current version works on Windows only. +.. note:: The current version works on OS X and Windows only. OS X support was added in 3.0.0. .. versionadded:: 1.1.3 .. py:function:: PIL.ImageGrab.grab(bbox=None) Take a snapshot of the screen. The pixels inside the bounding box are - returned as an "RGB" image. If the bounding box is omitted, the entire - screen is copied. + returned as an "RGB" image on Windows or "RGBA" on OS X. + If the bounding box is omitted, the entire screen is copied. .. versionadded:: 1.1.3 @@ -28,6 +28,8 @@ or the clipboard to a PIL image memory. .. versionadded:: 1.1.4 - :return: An image, a list of filenames, or None if the clipboard does - not contain image data or filenames. Note that if a list is - returned, the filenames may not represent image files. + :return: On Windows, an image, a list of filenames, + or None if the clipboard does not contain image data or filenames. + Note that if a list is returned, the filenames may not represent image files. + + On Mac, this is not currently supported. diff --git a/docs/reference/ImageMath.rst b/docs/reference/ImageMath.rst index 00249a601..5d7345d79 100644 --- a/docs/reference/ImageMath.rst +++ b/docs/reference/ImageMath.rst @@ -27,7 +27,7 @@ Example: Using the :py:mod:`~PIL.ImageMath` module In the current version, :py:mod:`~PIL.ImageMath` only supports single-layer images. To process multi-band images, use the - :py:meth:`~PIL.Image.Image.split` method or :py:func:`~PIL.Image.merge` + :py:meth:`~PIL.Image.Image.split` method or :py:func:`~PIL.Image.merge` function. :param expression: A string which uses the standard Python expression diff --git a/docs/reference/ImageMorph.rst b/docs/reference/ImageMorph.rst index eaf3b1c5e..be9d59348 100644 --- a/docs/reference/ImageMorph.rst +++ b/docs/reference/ImageMorph.rst @@ -4,7 +4,7 @@ :py:mod:`ImageMorph` Module =========================== -The :py:mod:`ImageMorph` module provides morphology operations on images. +The :py:mod:`ImageMorph` module provides morphology operations on images. .. automodule:: PIL.ImageMorph :members: diff --git a/docs/reference/ImagePath.rst b/docs/reference/ImagePath.rst index 03aa39811..978db4caf 100644 --- a/docs/reference/ImagePath.rst +++ b/docs/reference/ImagePath.rst @@ -36,7 +36,7 @@ vector data. Path objects can be passed to the methods on the **distance** is measured as `Manhattan distance`_ and defaults to two pixels. -.. _Manhattan distance: http://en.wikipedia.org/wiki/Manhattan_distance +.. _Manhattan distance: https://en.wikipedia.org/wiki/Manhattan_distance .. py:method:: PIL.ImagePath.Path.getbbox() diff --git a/docs/reference/OleFileIO.rst b/docs/reference/OleFileIO.rst index 74c4b7b36..791cb5ff3 100644 --- a/docs/reference/OleFileIO.rst +++ b/docs/reference/OleFileIO.rst @@ -7,10 +7,10 @@ The :py:mod:`OleFileIO` module reads Microsoft OLE2 files (also called Structured Storage or Microsoft Compound Document File Format), such as Microsoft Office documents, Image Composer and FlashPix files, and -Outlook messages. +Outlook messages. -This module is the `OleFileIO\_PL`_ project by Philippe Lagadec, v0.30, -merged back into Pillow. +This module is the `OleFileIO\_PL`_ project by Philippe Lagadec, v0.42, +merged back into Pillow. .. _OleFileIO\_PL: http://www.decalage.info/python/olefileio @@ -300,7 +300,7 @@ Close the OLE file Unless your application is a simple script that terminates after processing an OLE file, do not forget to close each OleFileIO object -after parsing to close the file on disk. +after parsing to close the file on disk. .. code-block:: python diff --git a/docs/reference/PixelAccess.rst b/docs/reference/PixelAccess.rst index 55ab8c578..f9d0cf31a 100644 --- a/docs/reference/PixelAccess.rst +++ b/docs/reference/PixelAccess.rst @@ -6,7 +6,7 @@ The PixelAccess class provides read and write access to :py:class:`PIL.Image` data at a pixel level. -.. note:: Accessing individual pixels is fairly slow. If you are looping over all of the pixels in an image, there is likely a faster way using other parts of the Pillow API. +.. note:: Accessing individual pixels is fairly slow. If you are looping over all of the pixels in an image, there is likely a faster way using other parts of the Pillow API. Example ------- diff --git a/docs/reference/TiffTags.rst b/docs/reference/TiffTags.rst new file mode 100644 index 000000000..cf9a97a2b --- /dev/null +++ b/docs/reference/TiffTags.rst @@ -0,0 +1,59 @@ +.. py:module:: PIL.TiffTags +.. py:currentmodule:: PIL.TiffTags + +:py:mod:`TiffTags` Module +========================= + +The :py:mod:`TiffTags` module exposes many of the stantard TIFF +metadata tag numbers, names, and type information. + +.. method:: lookup(tag) + + :param tag: Integer tag number + :returns: Taginfo namedtuple, From the ``TAGS_V2`` info if possible, + otherwise just populating the value and name from ``TAGS``. + If the tag is not recognized, "unknown" is returned for the name + +.. versionadded:: 3.1.0 + +.. class:: TagInfo + + .. method:: __init__(self, value=None, name="unknown", type=None, length=0, enum=None) + + :param value: Integer Tag Number + :param name: Tag Name + :param type: Integer type from :py:attr:`PIL.TiffTags.TYPES` + :param length: Array length: 0 == variable, 1 == single value, n = fixed + :param enum: Dict of name:integer value options for an enumeration + + .. method:: cvt_enum(self, value) + + :param value: The enumerated value name + :returns: The integer corresponding to the name. + +.. versionadded:: 3.0.0 + +.. py:attribute:: PIL.TiffTags.TAGS_V2 + + The ``TAGS_V2`` dictionary maps 16-bit integer tag numbers to + :py:class:`PIL.TagTypes.TagInfo` tuples for metadata fields defined in the TIFF + spec. + +.. versionadded:: 3.0.0 + +.. py:attribute:: PIL.TiffTags.TAGS + + The ``TAGS`` dictionary maps 16-bit integer TIFF tag number to + descriptive string names. For instance: + + >>> from PIL.TiffTags import TAGS + >>> TAGS[0x010e] + 'ImageDescription' + + This dictionary contains a superset of the tags in TAGS_V2, common + EXIF tags, and other well known metadata tags. + +.. py:attribute:: PIL.TiffTags.TYPES + + The ``TYPES`` dictionary maps the TIFF type short integer to a + human readable type name. diff --git a/docs/reference/index.rst b/docs/reference/index.rst index 73a3ecfed..555bd2a57 100644 --- a/docs/reference/index.rst +++ b/docs/reference/index.rst @@ -26,8 +26,10 @@ Reference ImageTk ImageWin ExifTags + TiffTags OleFileIO PSDraw PixelAccess PyAccess ../PIL + plugins diff --git a/docs/plugins.rst b/docs/reference/plugins.rst similarity index 94% rename from docs/plugins.rst rename to docs/reference/plugins.rst index a069f80df..46f657fce 100644 --- a/docs/plugins.rst +++ b/docs/reference/plugins.rst @@ -146,7 +146,7 @@ Plugin reference :show-inheritance: :mod:`Jpeg2KImagePlugin` Module ------------------------------ +------------------------------- .. automodule:: PIL.Jpeg2KImagePlugin :members: @@ -229,9 +229,21 @@ Plugin reference ---------------------------- .. automodule:: PIL.PngImagePlugin + :members: ChunkStream, PngImageFile, PngStream, getchunks, is_cid, putchunk + :show-inheritance: +.. autoclass:: PIL.PngImagePlugin.ChunkStream :members: :undoc-members: :show-inheritance: +.. autoclass:: PIL.PngImagePlugin.PngImageFile + :members: + :undoc-members: + :show-inheritance: +.. autoclass:: PIL.PngImagePlugin.PngStream + :members: + :undoc-members: + :show-inheritance: + :mod:`PpmImagePlugin` Module ---------------------------- diff --git a/docs/releasenotes/2.7.0.rst b/docs/releasenotes/2.7.0.rst index 65a8f2d11..a51ca81b4 100644 --- a/docs/releasenotes/2.7.0.rst +++ b/docs/releasenotes/2.7.0.rst @@ -4,8 +4,8 @@ Sane Plugin ----------- -The Sane plugin has now been split into its own repo: -https://github.com/python-pillow/Sane . +The Sane plugin has now been split into its own repo: +https://github.com/python-pillow/Sane . Png text chunk size limits @@ -14,7 +14,7 @@ Png text chunk size limits To prevent potential denial of service attacks using compressed text chunks, there are now limits to the decompressed size of text chunks decoded from PNG images. If the limits are exceeded when opening a PNG -image a ``ValueError`` will be raised. +image a ``ValueError`` will be raised. Individual text chunks are limited to :py:attr:`PIL.PngImagePlugin.MAX_TEXT_CHUNK`, set to 1MB by diff --git a/docs/releasenotes/3.0.0.rst b/docs/releasenotes/3.0.0.rst new file mode 100644 index 000000000..9cc1de98c --- /dev/null +++ b/docs/releasenotes/3.0.0.rst @@ -0,0 +1,52 @@ + +3.0.0 +===== + +Saving Multipage Images +----------------------- + +There is now support for saving multipage images in the `GIF` and +`PDF` formats. To enable this functionality, pass in `save_all=True` +as a keyword argument to the save:: + + im.save('test.pdf', save_all=True) + +Tiff ImageFileDirectory Rewrite +------------------------------- + +The Tiff ImageFileDirectory metadata code has been rewritten. Where +previously it returned a somewhat arbitrary set of values and tuples, +it now returns bare values where appropriate and tuples when the +metadata item is a sequence or collection. + +The original metadata is still available in the TiffImage.tags, the +new values are available in the TiffImage.tags_v2 member. The old +structures will be deprecated at some point in the future. When +saving Tiff metadata, new code should use the +TiffImagePlugin.ImageFileDirectory_v2 class. + +Deprecated Methods +------------------ + +Several methods that have been marked as deprecated for many releases +have been removed in this release:: + + Image.tostring() + Image.fromstring() + Image.offset() + ImageDraw.setink() + ImageDraw.setfill() + The ImageFileIO module + The ImageFont.FreeTypeFont and ImageFont.truetype `file` keyword arg + The ImagePalette private _make functions + ImageWin.fromstring() + ImageWin.tostring() + +LibJpeg and Zlib are Required by Default +---------------------------------------- + +The external dependencies on libjpeg and zlib are now required by default. +If the headers or libraries are not found, then installation will abort +with an error. This behaviour can be disabled with the ``--disable-libjpeg`` +and ``--disable-zlib`` flags. + diff --git a/docs/releasenotes/3.1.0.rst b/docs/releasenotes/3.1.0.rst new file mode 100644 index 000000000..7f0a7f052 --- /dev/null +++ b/docs/releasenotes/3.1.0.rst @@ -0,0 +1,75 @@ + +3.1.0 +===== + +ImageDraw arc, chord and pieslice can now use floats +---------------------------------------------------- + +There is no longer a need to ensure that the start and end arguments for `arc`, +`chord` and `pieslice` are integers. + +Note that these numbers are not simply rounded internally, but are actually +utilised in the drawing process. + +Consistent multiline text spacing +--------------------------------- + +When using the ``ImageDraw`` multiline methods, the spacing between +lines was inconsistent, based on the combination on ascenders and +descenders. + +This has now been fixed, so that lines are offset by their baselines, +not the absolute height of each line. + +There is also now a default spacing of 4px between lines. + +Exif, Jpeg and Tiff Metadata +---------------------------- + +There were major changes in the TIFF ImageFileDirectory support in +Pillow 3.0 that led to a number of regressions. Some of them have been +fixed in Pillow 3.1, and some of them have been extended to have +different behavior. + +TiffImagePlugin.IFDRational ++++++++++++++++++++++++++++ + +Pillow 3.0 changed rational metadata to use a float. In Pillow 3.1, +this has changed to allow the expression of 0/0 as a valid piece of +rational metadata to reflect usage in the wild. + +Rational metadata is now encapsulated in an ``IFDRational`` +instance. This class extends the Rational class to allow a denominator +of 0. It compares as a float or a number, but does allow access to the +raw numerator and denominator values through attributes. + +When used in a ``ImageFileDirectory_v1``, a 2 item tuple is returned +of the numerator and denominator, as was done previously. + +This class should be used when adding a rational value to an +ImageFileDirectory for saving to image metadata. + +JpegImagePlugin._getexif +++++++++++++++++++++++++ + +In Pillow 3.0, the dictionary returned from the private, experimental, +but generally widely used ``_getexif`` function changed to reflect the +ImageFileDirectory_v2 format, without a fallback to the previous format. + +In Pillow 3.1, ``_getexif`` now returns a dictionary compatible with +Pillow 2.9 and earlier, built with +``ImageFileDirectory_v1`` instances. Additionally, any +single item tuples have been unwrapped and return a bare element. + +The format returned by Pillow 3.0 has been abandoned. A more fully +featured interface for EXIF is anticipated in a future release. + +Out of Spec Metadata +++++++++++++++++++++ + +In Pillow 3.0 and 3.1, images that contain metadata that is internally +consistent but not in agreement with the TIFF spec may cause an +exception when reading the metadata. This can happen when a tag that +is specified to have a single value is stored with an array of values. + +It is anticipated that this behavior will change in future releases. diff --git a/docs/releasenotes/3.1.1.rst b/docs/releasenotes/3.1.1.rst new file mode 100644 index 000000000..969bbb7ca --- /dev/null +++ b/docs/releasenotes/3.1.1.rst @@ -0,0 +1,78 @@ + +3.1.1 +===== + +CVE-2016-0740 -- Buffer overflow in TiffDecode.c +------------------------------------------------ + +Pillow 3.1.0 and earlier when linked against libtiff >= 4.0.0 on x64 +may overflow a buffer when reading a specially crafted tiff file. + +Specifically, libtiff >= 4.0.0 changed the return type of +``TIFFScanlineSize`` from ``int32`` to machine dependent +``int32|64``. If the scanline is sized so that it overflows an +``int32``, it may be interpreted as a negative number, which will then +pass the size check in ``TiffDecode.c`` line 236. To do this, the +logical scanline size has to be > 2gb, and for the test file, the +allocated buffer size is 64k against a roughly 4gb scan line size. Any +image data over 64k is written over the heap, causing a segfault. + +This issue was found by security researcher FourOne. + + +CVE-2016-0775 -- Buffer overflow in FliDecode.c +----------------------------------------------- + +In all versions of Pillow, dating back at least to the last PIL 1.1.7 +release, FliDecode.c has a buffer overflow error. + +Around line 192:: + + case 16: + /* COPY chunk */ + for (y = 0; y < state->ysize; y++) { + UINT8* buf = (UINT8*) im->image[y]; + memcpy(buf+x, data, state->xsize); + data += state->xsize; + } + break; + + +The memcpy has error where ``x`` is added to the target buffer +address. ``X`` is used in several internal temporary variable roles, +but can take a value up to the width of the image. ``Im->image[y]`` +is a set of row pointers to segments of memory that are the size of +the row. At the max ``y``, this will write the contents of the line +off the end of the memory buffer, causing a segfault. + +This issue was found by Alyssa Besseling at Atlassian + +CVE-2016-2533 -- Buffer overflow in PcdDecode.c +---------------------------------------------- + +In all versions of Pillow, dating back at least to the last PIL 1.1.7 +release, ``PcdDecode.c`` has a buffer overflow error. + +The ``state.buffer`` for ``PcdDecode.c`` is allocated based on a 3 +bytes per pixel sizing, where ``PcdDecode.c`` wrote into the buffer +assuming 4 bytes per pixel. This writes 768 bytes beyond the end of +the buffer into other Python object storage. In some cases, this +causes a segfault, in others an internal Python malloc error. + +Integer overflow in Resample.c +------------------------------ + +If a large value was passed into the new size for an image, it is +possible to overflow an int32 value passed into malloc. + + kk = malloc(xsize * kmax * sizeof(float)); + ... + xbounds = malloc(xsize * 2 * sizeof(int)); + +``xsize`` is trusted user input. These multiplications can overflow, +leading the malloc'd buffer to be undersized. These allocations are +followed by a loop that writes out of bounds. This can lead to +corruption on the heap of the Python process with attacker controlled +float data. + +This issue was found by Ned Williamson. diff --git a/docs/releasenotes/3.1.2.rst b/docs/releasenotes/3.1.2.rst new file mode 100644 index 000000000..ddb6a2ada --- /dev/null +++ b/docs/releasenotes/3.1.2.rst @@ -0,0 +1,43 @@ + +3.1.2 +===== + +CVE-2016-3076 -- Buffer overflow in Jpeg2KEncode.c +-------------------------------------------------- + +Pillow between 2.5.0 and 3.1.1 may overflow a buffer when writing +large Jpeg2000 files, allowing for code execution or other memory +corruption. + +This occurs specifically in the function ``j2k_encode_entry``, at the line:: + + state->buffer = malloc (tile_width * tile_height * components * prec / 8); + + +This vulnerability requires a particular value for ``height * width`` +such that ``height * width * components * precision`` overflows, at +which point the malloc will be for a smaller value than expected. The +buffer that is allocated will be ``((height * width * components * +precision) mod (2^31) / 8)``, where components is 1-4 and precision is +either 8 or +16. Common values would be 4 components at precision 8 for a standard +``RGBA`` image. + +The unpackers then split an image that is laid out:: + + RGBARGBARGBA.... + +into:: + + + RRR. + GGG. + BBB. + AAA. + + +If this buffer is smaller than expected, the jpeg2k unpacker functions +will write outside the allocation and onto the heap, corrupting +memory. + +This issue was found by Alyssa Besseling at Atlassian. diff --git a/docs/releasenotes/3.2.0.rst b/docs/releasenotes/3.2.0.rst new file mode 100644 index 000000000..c61774288 --- /dev/null +++ b/docs/releasenotes/3.2.0.rst @@ -0,0 +1,36 @@ + +3.2.0 +----- + +New DDS and FTEX Image Plugins +============================== + +The ``DdsImagePlugin`` reading DXT1 and DXT5 encoded ``.dds`` images was +added. DXT3 images are not currently supported. + +The ``FtexImagePlugin`` reads textures used for 3D objects in +Independence War 2: Edge Of Chaos. The plugin reads a single texture +per file, in the ``.ftc`` (compressed) and ``.ftu`` (uncompressed) +formats. + +Updates to the GbrImagePlugin +============================= + +The ``GbrImagePlugin`` (GIMP brush format) has been updated to fix +support for version 1 files and add support for version 2 files. + +Passthrough Parameters for ImageDraw.text +========================================= + +``ImageDraw.multiline_text`` and ``ImageDraw.multiline_size`` take extra +spacing parameters above what are used in ``ImageDraw.text`` and +``ImageDraw.size``. These parameters can now be passed into +``ImageDraw.text`` and ``ImageDraw.size`` and they will be passed through +to the corresponding multiline functions. + +ImageSequence.Iterator changes +============================== + +``ImageSequence.Iterator`` is now an actual iterator implementing the +Iterator protocol. It is also now possible to seek to the first image +of the file when using direct indexing. diff --git a/docs/releasenotes/index.rst b/docs/releasenotes/index.rst index d70edc54d..ef61e976e 100644 --- a/docs/releasenotes/index.rst +++ b/docs/releasenotes/index.rst @@ -6,5 +6,12 @@ Release Notes .. toctree:: :maxdepth: 2 - 2.7.0 + 3.2.0 + 3.1.2 + 3.1.1 + 3.1.0 + 3.0.0 2.8.0 + 2.7.0 + + diff --git a/docs/requirements.txt b/docs/requirements.txt deleted file mode 100644 index 6b104fb01..000000000 --- a/docs/requirements.txt +++ /dev/null @@ -1,2 +0,0 @@ -Sphinx -sphinx-rtd-theme diff --git a/encode.c b/encode.c index 6bdb8c71a..84e5b96b1 100644 --- a/encode.c +++ b/encode.c @@ -54,7 +54,7 @@ PyImaging_EncoderNew(int contextsize) ImagingEncoderObject *encoder; void *context; - if(!PyType_Ready(&ImagingEncoderType) < 0) + if(PyType_Ready(&ImagingEncoderType) < 0) return NULL; encoder = PyObject_New(ImagingEncoderObject, &ImagingEncoderType); @@ -554,7 +554,7 @@ static unsigned int* get_qtables_arrays(PyObject* qtables, int* qtablesLen) { tables = PySequence_Fast(qtables, "expected a sequence"); num_tables = PySequence_Size(qtables); if (num_tables < 1 || num_tables > NUM_QUANT_TBLS) { - PyErr_SetString(PyExc_ValueError, + PyErr_SetString(PyExc_ValueError, "Not a valid number of quantization tables. Should be between 1 and 4."); Py_DECREF(tables); return NULL; @@ -582,7 +582,7 @@ static unsigned int* get_qtables_arrays(PyObject* qtables, int* qtablesLen) { Py_DECREF(table_data); } - *qtablesLen = num_tables; + *qtablesLen = num_tables; JPEG_QTABLES_ERR: Py_DECREF(tables); // Run on both error and not error @@ -721,7 +721,6 @@ PyImaging_LibTiffEncoderNew(PyObject* self, PyObject* args) pos = 0; } - TRACE(("new tiff encoder %s fp: %d, filename: %s \n", compname, fp, filename)); encoder = PyImaging_EncoderNew(sizeof(TIFFSTATE)); @@ -737,11 +736,9 @@ PyImaging_LibTiffEncoderNew(PyObject* self, PyObject* args) return NULL; } - // While fails on 64 bit machines, complains that pos is an int instead of a Py_ssize_t - // while (PyDict_Next(dir, &pos, &key, &value)) { - for (pos=0;posstate, (ttag_t) PyInt_AsLong(key), PyInt_AsLong(value)); - } else if(PyBytes_Check(value)) { - TRACE(("Setting from Bytes: %d, %s \n", (int)PyInt_AsLong(key),PyBytes_AsString(value))); - status = ImagingLibTiffSetField(&encoder->state, - (ttag_t) PyInt_AsLong(key), - PyBytes_AsString(value)); - } else if(PyList_Check(value)) { - int len,i; - float *floatav; - int *intav; - TRACE(("Setting from List: %d \n", (int)PyInt_AsLong(key))); - len = (int)PyList_Size(value); - if (len) { - if (PyInt_Check(PyList_GetItem(value,0))) { - TRACE((" %d elements, setting as ints \n", len)); - intav = malloc(sizeof(int)*len); - if (intav) { - for (i=0;istate, - (ttag_t) PyInt_AsLong(key), - intav); - free(intav); - } - } else { - TRACE((" %d elements, setting as floats \n", len)); - floatav = malloc(sizeof(float)*len); - if (floatav) { - for (i=0;istate, - (ttag_t) PyInt_AsLong(key), - floatav); - free(floatav); - } - } - } } else if (PyFloat_Check(value)) { TRACE(("Setting from Float: %d, %f \n", (int)PyInt_AsLong(key),PyFloat_AsDouble(value))); status = ImagingLibTiffSetField(&encoder->state, (ttag_t) PyInt_AsLong(key), (float)PyFloat_AsDouble(value)); + } else if (PyBytes_Check(value)) { + TRACE(("Setting from Bytes: %d, %s \n", (int)PyInt_AsLong(key),PyBytes_AsString(value))); + status = ImagingLibTiffSetField(&encoder->state, + (ttag_t) PyInt_AsLong(key), + PyBytes_AsString(value)); + } else if (PyTuple_Check(value)) { + int len,i; + float *floatav; + int *intav; + TRACE(("Setting from Tuple: %d \n", (int)PyInt_AsLong(key))); + len = (int)PyTuple_Size(value); + if (len) { + if (PyInt_Check(PyTuple_GetItem(value,0))) { + TRACE((" %d elements, setting as ints \n", len)); + intav = malloc(sizeof(int)*len); + if (intav) { + for (i=0;istate, + (ttag_t) PyInt_AsLong(key), + len, intav); + free(intav); + } + } else if (PyFloat_Check(PyTuple_GetItem(value,0))) { + TRACE((" %d elements, setting as floats \n", len)); + floatav = malloc(sizeof(float)*len); + if (floatav) { + for (i=0;istate, + (ttag_t) PyInt_AsLong(key), + len, floatav); + free(floatav); + } + } else { + TRACE(("Unhandled type in tuple for key %d : %s \n", + (int)PyInt_AsLong(key), + PyBytes_AsString(PyObject_Str(value)))); + } + } } else { TRACE(("Unhandled type for key %d : %s \n", (int)PyInt_AsLong(key), diff --git a/libImaging/AlphaComposite.c b/libImaging/AlphaComposite.c index 9433fae53..538fd88e3 100644 --- a/libImaging/AlphaComposite.c +++ b/libImaging/AlphaComposite.c @@ -3,7 +3,7 @@ * $Id$ * * Alpha composite imSrc over imDst. - * http://en.wikipedia.org/wiki/Alpha_compositing + * https://en.wikipedia.org/wiki/Alpha_compositing * * See the README file for details on usage and redistribution. */ diff --git a/libImaging/Convert.c b/libImaging/Convert.c index 3d9119c7f..f32e91bad 100644 --- a/libImaging/Convert.c +++ b/libImaging/Convert.c @@ -259,14 +259,14 @@ rgb2hsv(UINT8* out, const UINT8* in, int xsize) g = in[1]; b = in[2]; - maxc = MAX(r,MAX(g,b)); + maxc = MAX(r,MAX(g,b)); minc = MIN(r,MIN(g,b)); uv = maxc; if (minc == maxc){ *out++ = 0; *out++ = 0; *out++ = uv; - } else { + } else { cr = (float)(maxc-minc); s = cr/(float)maxc; rc = ((float)(maxc-r))/cr; @@ -280,7 +280,7 @@ rgb2hsv(UINT8* out, const UINT8* in, int xsize) h = 4.0 + gc-rc; } // incorrect hue happens if h/6 is negative. - h = fmod((h/6.0 + 1.0), 1.0); + h = fmod((h/6.0 + 1.0), 1.0); uh = (UINT8)CLIP((int)(h*255.0)); us = (UINT8)CLIP((int)(s*255.0)); @@ -313,10 +313,10 @@ hsv2rgb(UINT8* out, const UINT8* in, int xsize) *out++ = v; *out++ = v; *out++ = v; - } else { + } else { i = floor((float)h * 6.0 / 255.0); // 0 - 6 - f = (float)h * 6.0 / 255.0 - (float)i; // 0-1 : remainder. - fs = ((float)s)/255.0; + f = (float)h * 6.0 / 255.0 - (float)i; // 0-1 : remainder. + fs = ((float)s)/255.0; p = round((float)v * (1.0-fs)); q = round((float)v * (1.0-fs*f)); @@ -326,32 +326,32 @@ hsv2rgb(UINT8* out, const UINT8* in, int xsize) ut = (UINT8)CLIP(t); switch (i%6) { - case 0: + case 0: *out++ = v; *out++ = ut; *out++ = up; break; - case 1: + case 1: *out++ = uq; *out++ = v; *out++ = up; break; - case 2: + case 2: *out++ = up; *out++ = v; *out++ = ut; break; - case 3: + case 3: *out++ = up; *out++ = uq; *out++ = v; break; - case 4: + case 4: *out++ = ut; *out++ = up; *out++ = v; break; - case 5: + case 5: *out++ = v; *out++ = up; *out++ = uq; @@ -418,7 +418,7 @@ rgba2rgba(UINT8* out, const UINT8* in, int xsize) } } -/* RGBa -> RGBA conversion to remove premultiplication +/* RGBa -> RGBA conversion to remove premultiplication Needed for correct transforms/resizing on RGBA images */ static void rgba2rgbA(UINT8* out, const UINT8* in, int xsize) @@ -442,13 +442,13 @@ rgba2rgbA(UINT8* out, const UINT8* in, int xsize) } /* - * Conversion of RGB + single transparent color to RGBA, - * where any pixel that matches the color will have the + * Conversion of RGB + single transparent color to RGBA, + * where any pixel that matches the color will have the * alpha channel set to 0 */ static void -rgbT2rgba(UINT8* out, int xsize, int r, int g, int b) +rgbT2rgba(UINT8* out, int xsize, int r, int g, int b) { #ifdef WORDS_BIGENDIAN UINT32 trns = ((r & 0xff)<<24) | ((g & 0xff)<<16) | ((b & 0xff)<<8) | 0xff; @@ -998,7 +998,7 @@ frompalette(Imaging imOut, Imaging imIn, const char *mode) return imOut; } -#if defined(_MSC_VER) && (_MSC_VER == 1600) +#if defined(_MSC_VER) #pragma optimize("", off) #endif static Imaging @@ -1233,7 +1233,7 @@ tobilevel(Imaging imOut, Imaging imIn, int dither) return imOut; } -#if defined(_MSC_VER) && (_MSC_VER == 1600) +#if defined(_MSC_VER) #pragma optimize("", on) #endif @@ -1334,8 +1334,8 @@ ImagingConvertTransparent(Imaging imIn, const char *mode, return (Imaging) ImagingError_ModeError(); } - if (!((strcmp(imIn->mode, "RGB") == 0 || - strcmp(imIn->mode, "L") == 0) + if (!((strcmp(imIn->mode, "RGB") == 0 || + strcmp(imIn->mode, "L") == 0) && strcmp(mode, "RGBA") == 0)) #ifdef notdef { @@ -1370,7 +1370,7 @@ ImagingConvertTransparent(Imaging imIn, const char *mode, } ImagingSectionLeave(&cookie); - return imOut; + return imOut; } diff --git a/libImaging/Dib.c b/libImaging/Dib.c index 55de7d9a5..e8ce3e713 100644 --- a/libImaging/Dib.c +++ b/libImaging/Dib.c @@ -228,7 +228,7 @@ ImagingPasteDIB(ImagingDIB dib, Imaging im, int xy[4]) } void -ImagingExposeDIB(ImagingDIB dib, int dc) +ImagingExposeDIB(ImagingDIB dib, void *dc) { /* Copy bitmap to display */ @@ -238,7 +238,7 @@ ImagingExposeDIB(ImagingDIB dib, int dc) } void -ImagingDrawDIB(ImagingDIB dib, int dc, int dst[4], int src[4]) +ImagingDrawDIB(ImagingDIB dib, void *dc, int dst[4], int src[4]) { /* Copy bitmap to printer/display */ @@ -258,7 +258,7 @@ ImagingDrawDIB(ImagingDIB dib, int dc, int dst[4], int src[4]) } int -ImagingQueryPaletteDIB(ImagingDIB dib, int dc) +ImagingQueryPaletteDIB(ImagingDIB dib, void *dc) { /* Install bitmap palette */ diff --git a/libImaging/Draw.c b/libImaging/Draw.c index c21f7cd83..423f1bea2 100644 --- a/libImaging/Draw.c +++ b/libImaging/Draw.c @@ -457,8 +457,8 @@ polygon_generic(Imaging im, int n, Edge *e, int ink, int eofill, if (ymin < 0) { ymin = 0; } - if (ymax >= im->ysize) { - ymax = im->ysize - 1; + if (ymax > im->ysize) { + ymax = im->ysize; } /* Process the edge table with a scan line searching for intersections */ @@ -743,10 +743,11 @@ ImagingDrawBitmap(Imaging im, int x0, int y0, Imaging bitmap, const void* ink, static int ellipse(Imaging im, int x0, int y0, int x1, int y1, - int start, int end, const void* ink_, int fill, + float start, float end, const void* ink_, int fill, int mode, int op) { - int i, n; + float i; + int n; int cx, cy; int w, h; int x = 0, y = 0; @@ -779,7 +780,10 @@ ellipse(Imaging im, int x0, int y0, int x1, int y1, n = 0; - for (i = start; i <= end; i++) { + for (i = start; i < end+1; i++) { + if (i > end) { + i = end; + } x = FLOOR((cos(i*M_PI/180) * w/2) + cx + 0.5); y = FLOOR((sin(i*M_PI/180) * h/2) + cy + 0.5); if (i != start) @@ -807,7 +811,10 @@ ellipse(Imaging im, int x0, int y0, int x1, int y1, } else { - for (i = start; i <= end; i++) { + for (i = start; i < end+1; i++) { + if (i > end) { + i = end; + } x = FLOOR((cos(i*M_PI/180) * w/2) + cx + 0.5); y = FLOOR((sin(i*M_PI/180) * h/2) + cy + 0.5); if (i != start) @@ -835,14 +842,14 @@ ellipse(Imaging im, int x0, int y0, int x1, int y1, int ImagingDrawArc(Imaging im, int x0, int y0, int x1, int y1, - int start, int end, const void* ink, int op) + float start, float end, const void* ink, int op) { return ellipse(im, x0, y0, x1, y1, start, end, ink, 0, ARC, op); } int ImagingDrawChord(Imaging im, int x0, int y0, int x1, int y1, - int start, int end, const void* ink, int fill, int op) + float start, float end, const void* ink, int fill, int op) { return ellipse(im, x0, y0, x1, y1, start, end, ink, fill, CHORD, op); } @@ -856,7 +863,7 @@ ImagingDrawEllipse(Imaging im, int x0, int y0, int x1, int y1, int ImagingDrawPieslice(Imaging im, int x0, int y0, int x1, int y1, - int start, int end, const void* ink, int fill, int op) + float start, float end, const void* ink, int fill, int op) { return ellipse(im, x0, y0, x1, y1, start, end, ink, fill, PIESLICE, op); } diff --git a/libImaging/FliDecode.c b/libImaging/FliDecode.c index 75eebe86c..6d22c6c4e 100644 --- a/libImaging/FliDecode.c +++ b/libImaging/FliDecode.c @@ -185,7 +185,7 @@ ImagingFliDecode(Imaging im, ImagingCodecState state, UINT8* buf, int bytes) /* COPY chunk */ for (y = 0; y < state->ysize; y++) { UINT8* buf = (UINT8*) im->image[y]; - memcpy(buf+x, data, state->xsize); + memcpy(buf, data, state->xsize); data += state->xsize; } break; diff --git a/libImaging/ImDib.h b/libImaging/ImDib.h index 55ecb35aa..e5a2cc0f6 100644 --- a/libImaging/ImDib.h +++ b/libImaging/ImDib.h @@ -43,10 +43,10 @@ extern ImagingDIB ImagingNewDIB(const char *mode, int xsize, int ysize); extern void ImagingDeleteDIB(ImagingDIB im); -extern void ImagingDrawDIB(ImagingDIB dib, int dc, int dst[4], int src[4]); -extern void ImagingExposeDIB(ImagingDIB dib, int dc); +extern void ImagingDrawDIB(ImagingDIB dib, void *dc, int dst[4], int src[4]); +extern void ImagingExposeDIB(ImagingDIB dib, void *dc); -extern int ImagingQueryPaletteDIB(ImagingDIB dib, int dc); +extern int ImagingQueryPaletteDIB(ImagingDIB dib, void *dc); extern void ImagingPasteDIB(ImagingDIB dib, Imaging im, int xy[4]); diff --git a/libImaging/Imaging.h b/libImaging/Imaging.h index c341ac84e..bbef0440d 100644 --- a/libImaging/Imaging.h +++ b/libImaging/Imaging.h @@ -345,11 +345,11 @@ struct ImagingAffineMatrixInstance { typedef struct ImagingAffineMatrixInstance *ImagingAffineMatrix; extern int ImagingDrawArc(Imaging im, int x0, int y0, int x1, int y1, - int start, int end, const void* ink, int op); + float start, float end, const void* ink, int op); extern int ImagingDrawBitmap(Imaging im, int x0, int y0, Imaging bitmap, const void* ink, int op); extern int ImagingDrawChord(Imaging im, int x0, int y0, int x1, int y1, - int start, int end, const void* ink, int fill, + float start, float end, const void* ink, int fill, int op); extern int ImagingDrawEllipse(Imaging im, int x0, int y0, int x1, int y1, const void* ink, int fill, int op); @@ -358,7 +358,7 @@ extern int ImagingDrawLine(Imaging im, int x0, int y0, int x1, int y1, extern int ImagingDrawWideLine(Imaging im, int x0, int y0, int x1, int y1, const void* ink, int width, int op); extern int ImagingDrawPieslice(Imaging im, int x0, int y0, int x1, int y1, - int start, int end, const void* ink, int fill, + float start, float end, const void* ink, int fill, int op); extern int ImagingDrawPoint(Imaging im, int x, int y, const void* ink, int op); extern int ImagingDrawPolygon(Imaging im, int points, int *xy, diff --git a/libImaging/Incremental.c b/libImaging/Incremental.c index 84e20acf6..f34765423 100644 --- a/libImaging/Incremental.c +++ b/libImaging/Incremental.c @@ -175,7 +175,7 @@ ImagingIncrementalCodecCreate(ImagingIncrementalCodecEntry codec_entry, codec->state = state; codec->result = 0; codec->stream.fd = fd; - codec->stream.buffer = codec->stream.ptr = codec->stream.end + codec->stream.buffer = codec->stream.ptr = codec->stream.end = codec->stream.top = NULL; codec->started = 0; codec->seekable = seekable; @@ -294,7 +294,7 @@ ImagingIncrementalCodecDestroy(ImagingIncrementalCodec codec) if (codec->seekable && codec->stream.fd < 0) free (codec->stream.buffer); - codec->stream.buffer = codec->stream.ptr = codec->stream.end + codec->stream.buffer = codec->stream.ptr = codec->stream.end = codec->stream.top = NULL; #ifdef _WIN32 diff --git a/libImaging/Jpeg2KDecode.c b/libImaging/Jpeg2KDecode.c index abf8cebbe..07bee2b5e 100644 --- a/libImaging/Jpeg2KDecode.c +++ b/libImaging/Jpeg2KDecode.c @@ -68,7 +68,7 @@ j2k_skip(OPJ_OFF_T p_nb_bytes, void *p_user_data) typedef void (*j2k_unpacker_t)(opj_image_t *in, const JPEG2KTILEINFO *tileInfo, - const UINT8 *data, + const UINT8 *data, Imaging im); struct j2k_decode_unpacker { @@ -625,7 +625,7 @@ j2k_decode_entry(Imaging im, ImagingCodecState state, } } - /* + /* Colorspace Number of components PIL mode ------------------------------------------------------ sRGB 3 RGB @@ -668,7 +668,7 @@ j2k_decode_entry(Imaging im, ImagingCodecState state, if (!unpack) { state->errcode = IMAGING_CODEC_BROKEN; state->state = J2K_STATE_FAILED; - goto quick_exit; + goto quick_exit; } /* Decode the image tile-by-tile; this means we only need use as much diff --git a/libImaging/Jpeg2KEncode.c b/libImaging/Jpeg2KEncode.c index 868cfdb41..16f44f539 100644 --- a/libImaging/Jpeg2KEncode.c +++ b/libImaging/Jpeg2KEncode.c @@ -265,6 +265,10 @@ j2k_encode_entry(Imaging im, ImagingCodecState state, unsigned prec = 8; unsigned bpp = 8; + unsigned _overflow_scale_factor; + /* SIZE_MAX is not working in the conditionals unless it's a typed + variable */ + unsigned _SIZE__MAX = SIZE_MAX; stream = opj_stream_default_create(OPJ_FALSE); @@ -303,7 +307,7 @@ j2k_encode_entry(Imaging im, ImagingCodecState state, prec = 16; bpp = 12; } else if (strcmp (im->mode, "LA") == 0) { - components = 2; + components = 2; color_space = OPJ_CLRSPC_GRAY; pack = j2k_pack_la; } else if (strcmp (im->mode, "RGB") == 0) { @@ -335,6 +339,11 @@ j2k_encode_entry(Imaging im, ImagingCodecState state, } image = opj_image_create(components, image_params, color_space); + if (!image) { + state->errcode = IMAGING_CODEC_BROKEN; + state->state = J2K_STATE_FAILED; + goto quick_exit; + } /* Setup compression context */ context->error_msg = NULL; @@ -471,7 +480,24 @@ j2k_encode_entry(Imaging im, ImagingCodecState state, tiles_y = (im->ysize + (params.image_offset_y0 - params.cp_ty0) + tile_height - 1) / tile_height; + /* check for integer overflow for the malloc line, checking any expression + that may multiply either tile_width or tile_height */ + _overflow_scale_factor = components * prec; + if (( tile_width > _SIZE__MAX / _overflow_scale_factor ) || + ( tile_height > _SIZE__MAX / _overflow_scale_factor ) || + ( tile_width > _SIZE__MAX / (tile_height * _overflow_scale_factor )) || + ( tile_height > _SIZE__MAX / (tile_width * _overflow_scale_factor ))) { + state->errcode = IMAGING_CODEC_BROKEN; + state->state = J2K_STATE_FAILED; + goto quick_exit; + } + /* malloc check ok, checked for overflow above */ state->buffer = malloc (tile_width * tile_height * components * prec / 8); + if (!state->buffer) { + state->errcode = IMAGING_CODEC_BROKEN; + state->state = J2K_STATE_FAILED; + goto quick_exit; + } tile_ndx = 0; for (y = 0; y < tiles_y; ++y) { @@ -544,8 +570,8 @@ ImagingJpeg2KEncode(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) return -1; if (state->state == J2K_STATE_START) { - int seekable = (context->format != OPJ_CODEC_J2K - ? INCREMENTAL_CODEC_SEEKABLE + int seekable = (context->format != OPJ_CODEC_J2K + ? INCREMENTAL_CODEC_SEEKABLE : INCREMENTAL_CODEC_NOT_SEEKABLE); context->encoder = ImagingIncrementalCodecCreate(j2k_encode_entry, diff --git a/libImaging/JpegEncode.c b/libImaging/JpegEncode.c index 3fe5d753f..5bcba2aa4 100644 --- a/libImaging/JpegEncode.c +++ b/libImaging/JpegEncode.c @@ -300,7 +300,7 @@ ImagingJpegEncode(Imaging im, ImagingCodecState state, UINT8* buf, int bytes) if (context->qtables) { free(context->qtables); context->qtables = NULL; - } + } jpeg_destroy_compress(&context->cinfo); /* if (jerr.pub.num_warnings) return BROKEN; */ diff --git a/libImaging/Pack.c b/libImaging/Pack.c index d174d7d0d..d83cb8284 100644 --- a/libImaging/Pack.c +++ b/libImaging/Pack.c @@ -400,7 +400,7 @@ ImagingPackLAB(UINT8* out, const UINT8* in, int pixels) /* LAB triplets */ for (i = 0; i < pixels; i++) { out[0] = in[0]; - out[1] = in[1] ^ 128; /* signed in outside world */ + out[1] = in[1] ^ 128; /* signed in outside world */ out[2] = in[2] ^ 128; out += 3; in += 4; } diff --git a/libImaging/PcdDecode.c b/libImaging/PcdDecode.c index fb6adc688..f92343890 100644 --- a/libImaging/PcdDecode.c +++ b/libImaging/PcdDecode.c @@ -47,7 +47,7 @@ ImagingPcdDecode(Imaging im, ImagingCodecState state, UINT8* buf, int bytes) out[0] = ptr[x]; out[1] = ptr[(x+4*state->xsize)/2]; out[2] = ptr[(x+5*state->xsize)/2]; - out += 4; + out += 3; } state->shuffle((UINT8*) im->image[state->y], @@ -62,7 +62,7 @@ ImagingPcdDecode(Imaging im, ImagingCodecState state, UINT8* buf, int bytes) out[0] = ptr[x+state->xsize]; out[1] = ptr[(x+4*state->xsize)/2]; out[2] = ptr[(x+5*state->xsize)/2]; - out += 4; + out += 3; } state->shuffle((UINT8*) im->image[state->y], diff --git a/libImaging/PcxDecode.c b/libImaging/PcxDecode.c index af282cfe4..e5417f1bd 100644 --- a/libImaging/PcxDecode.c +++ b/libImaging/PcxDecode.c @@ -62,8 +62,8 @@ ImagingPcxDecode(Imaging im, ImagingCodecState state, UINT8* buf, int bytes) int stride = state->bytes / bands; int i; for (i=1; i< bands; i++) { // note -- skipping first band - memmove(&state->buffer[i*state->xsize], - &state->buffer[i*stride], + memmove(&state->buffer[i*state->xsize], + &state->buffer[i*stride], state->xsize); } } diff --git a/libImaging/PcxEncode.c b/libImaging/PcxEncode.c index c1f64a33d..c73f49a24 100644 --- a/libImaging/PcxEncode.c +++ b/libImaging/PcxEncode.c @@ -94,10 +94,10 @@ ImagingPcxEncode(Imaging im, ImagingCodecState state, UINT8* buf, int bytes) while (state->x < planes * bytes_per_line) { /* If we're encoding an odd width file, and we've got more than one plane, we need to pad each - color row with padding bytes at the end. Since + color row with padding bytes at the end. Since The pixels are stored RRRRRGGGGGBBBBB, so we need - to have the padding be RRRRRPGGGGGPBBBBBP. Hence - the double loop + to have the padding be RRRRRPGGGGGPBBBBBP. Hence + the double loop */ while (state->x % bytes_per_line) { @@ -174,8 +174,8 @@ ImagingPcxEncode(Imaging im, ImagingCodecState state, UINT8* buf, int bytes) } /* reset for the next color plane. */ if (state->x < planes * bytes_per_line) { - state->count = 1; - state->LAST = state->buffer[state->x]; + state->count = 1; + state->LAST = state->buffer[state->x]; state->x++; } } diff --git a/libImaging/Resample.c b/libImaging/Resample.c index 597fca3e9..64e90eed0 100644 --- a/libImaging/Resample.c +++ b/libImaging/Resample.c @@ -53,7 +53,7 @@ static struct filter BILINEAR = { bilinear_filter, 1.0 }; static inline float bicubic_filter(float x) { - /* http://en.wikipedia.org/wiki/Bicubic_interpolation#Bicubic_convolution_algorithm */ + /* https://en.wikipedia.org/wiki/Bicubic_interpolation#Bicubic_convolution_algorithm */ #define a -0.5 if (x < 0.0) x = -x; @@ -79,7 +79,7 @@ static inline UINT8 clip8(float in) } -/* This is work around bug in GCC prior 4.9 in 64 bit mode. +/* This is work around bug in GCC prior 4.9 in 64-bit mode. GCC generates code with partial dependency which 3 times slower. See: http://stackoverflow.com/a/26588074/253146 */ #if defined(__x86_64__) && defined(__SSE__) && ! defined(__NO_INLINE__) && \ @@ -138,11 +138,23 @@ ImagingResampleHorizontal(Imaging imIn, int xsize, int filter) /* maximum number of coofs */ kmax = (int) ceil(support) * 2 + 1; + // check for overflow + if (kmax > 0 && xsize > SIZE_MAX / kmax) + return (Imaging) ImagingError_MemoryError(); + + // sizeof(float) should be greater than 0 + if (xsize * kmax > SIZE_MAX / sizeof(float)) + return (Imaging) ImagingError_MemoryError(); + /* coefficient buffer */ kk = malloc(xsize * kmax * sizeof(float)); if ( ! kk) return (Imaging) ImagingError_MemoryError(); + // sizeof(int) should be greater than 0 as well + if (xsize > SIZE_MAX / (2 * sizeof(int))) + return (Imaging) ImagingError_MemoryError(); + xbounds = malloc(xsize * 2 * sizeof(int)); if ( ! xbounds) { free(kk); diff --git a/libImaging/TiffDecode.c b/libImaging/TiffDecode.c index 25336e7fa..b9b61e48f 100644 --- a/libImaging/TiffDecode.c +++ b/libImaging/TiffDecode.c @@ -169,7 +169,7 @@ int ImagingLibTiffDecode(Imaging im, ImagingCodecState state, UINT8* buffer, int char *filename = "tempfile.tif"; char *mode = "r"; TIFF *tiff; - int size; + tsize_t size; /* buffer is the encoded file, bytes is the length of the encoded file */ @@ -222,8 +222,8 @@ int ImagingLibTiffDecode(Imaging im, ImagingCodecState state, UINT8* buffer, int if (clientstate->ifd){ int rv; - unsigned int ifdoffset = clientstate->ifd; - TRACE(("reading tiff ifd %d\n", ifdoffset)); + uint32 ifdoffset = clientstate->ifd; + TRACE(("reading tiff ifd %u\n", ifdoffset)); rv = TIFFSetSubDirectory(tiff, ifdoffset); if (!rv){ TRACE(("error in TIFFSetSubDirectory")); diff --git a/libImaging/TiffDecode.h b/libImaging/TiffDecode.h index 9875309e3..2c8eeecae 100644 --- a/libImaging/TiffDecode.h +++ b/libImaging/TiffDecode.h @@ -13,6 +13,12 @@ #include #endif +/* UNDONE -- what are we using from this? */ +/*#ifndef _UNISTD_H + # include + # endif +*/ + #ifndef min #define min(x,y) (( x > y ) ? y : x ) #define max(x,y) (( x < y ) ? y : x ) @@ -26,10 +32,13 @@ typedef struct { toff_t loc; /* toff_t == uint32 */ tsize_t size; /* tsize_t == int32 */ int fp; - int ifd; /* offset of the ifd, used for multipage */ - TIFF *tiff; /* Used in write */ + uint32 ifd; /* offset of the ifd, used for multipage + * Should be uint32 for libtiff 3.9.x + * uint64 for libtiff 4.0.x + */ + TIFF *tiff; /* Used in write */ toff_t eof; - int flrealloc; /* may we realloc */ + int flrealloc;/* may we realloc */ } TIFFSTATE; @@ -41,7 +50,7 @@ extern int ImagingLibTiffSetField(ImagingCodecState state, ttag_t tag, ...); /* Trace debugging - legacy, don't enable for python 3.x, unicode issues. + legacy, don't enable for Python 3.x, unicode issues. */ /* diff --git a/libImaging/Unpack.c b/libImaging/Unpack.c index 522e9b04c..702bc9f1f 100644 --- a/libImaging/Unpack.c +++ b/libImaging/Unpack.c @@ -191,14 +191,64 @@ unpack1IR(UINT8* out, const UINT8* in, int pixels) static void unpackL2(UINT8* out, const UINT8* in, int pixels) { - /* nibbles */ + /* nibbles (msb first, white is non-zero) */ while (pixels > 0) { UINT8 byte = *in++; switch (pixels) { - default: *out++ = ((byte >> 6) & 3) * 255 / 3; byte <<= 2; - case 3: *out++ = ((byte >> 6) & 3) * 255 / 3; byte <<= 2; - case 2: *out++ = ((byte >> 6) & 3) * 255 / 3; byte <<= 2; - case 1: *out++ = ((byte >> 6) & 3) * 255 / 3; + default: *out++ = ((byte >> 6) & 0x03U) * 0x55U; byte <<= 2; + case 3: *out++ = ((byte >> 6) & 0x03U) * 0x55U; byte <<= 2; + case 2: *out++ = ((byte >> 6) & 0x03U) * 0x55U; byte <<= 2; + case 1: *out++ = ((byte >> 6) & 0x03U) * 0x55U; + } + pixels -= 4; + } +} + +static void +unpackL2I(UINT8* out, const UINT8* in, int pixels) +{ + /* nibbles (msb first, white is zero) */ + while (pixels > 0) { + UINT8 byte = *in++; + switch (pixels) { + default: *out++ = 0xFFU - (UINT8)(((byte >> 6) & 0x03U) * 0x55U); byte <<= 2; + case 3: *out++ = 0xFFU - (UINT8)(((byte >> 6) & 0x03U) * 0x55U); byte <<= 2; + case 2: *out++ = 0xFFU - (UINT8)(((byte >> 6) & 0x03U) * 0x55U); byte <<= 2; + case 1: *out++ = 0xFFU - (UINT8)(((byte >> 6) & 0x03U) * 0x55U); + } + pixels -= 4; + } +} + +static void +unpackL2R(UINT8* out, const UINT8* in, int pixels) +{ + /* nibbles (bit order reversed, white is non-zero) */ + while (pixels > 0) { + UINT8 byte = *in++; + byte = BITFLIP[byte]; + switch (pixels) { + default: *out++ = ((byte >> 6) & 0x03U) * 0x55U; byte <<= 2; + case 3: *out++ = ((byte >> 6) & 0x03U) * 0x55U; byte <<= 2; + case 2: *out++ = ((byte >> 6) & 0x03U) * 0x55U; byte <<= 2; + case 1: *out++ = ((byte >> 6) & 0x03U) * 0x55U; + } + pixels -= 4; + } +} + +static void +unpackL2IR(UINT8* out, const UINT8* in, int pixels) +{ + /* nibbles (bit order reversed, white is zero) */ + while (pixels > 0) { + UINT8 byte = *in++; + byte = BITFLIP[byte]; + switch (pixels) { + default: *out++ = 0xFFU - (UINT8)(((byte >> 6) & 0x03U) * 0x55U); byte <<= 2; + case 3: *out++ = 0xFFU - (UINT8)(((byte >> 6) & 0x03U) * 0x55U); byte <<= 2; + case 2: *out++ = 0xFFU - (UINT8)(((byte >> 6) & 0x03U) * 0x55U); byte <<= 2; + case 1: *out++ = 0xFFU - (UINT8)(((byte >> 6) & 0x03U) * 0x55U); } pixels -= 4; } @@ -207,12 +257,56 @@ unpackL2(UINT8* out, const UINT8* in, int pixels) static void unpackL4(UINT8* out, const UINT8* in, int pixels) { - /* nibbles */ + /* nibbles (msb first, white is non-zero) */ while (pixels > 0) { UINT8 byte = *in++; switch (pixels) { - default: *out++ = ((byte >> 4) & 15) * 255 / 15; byte <<= 4; - case 1: *out++ = ((byte >> 4) & 15) * 255 / 15; + default: *out++ = ((byte >> 4) & 0x0FU) * 0x11U; byte <<= 4; + case 1: *out++ = ((byte >> 4) & 0x0FU) * 0x11U; + } + pixels -= 2; + } +} + +static void +unpackL4I(UINT8* out, const UINT8* in, int pixels) +{ + /* nibbles (msb first, white is zero) */ + while (pixels > 0) { + UINT8 byte = *in++; + switch (pixels) { + default: *out++ = 0xFFU - (UINT8)(((byte >> 4) & 0x0FU) * 0x11U); byte <<= 4; + case 1: *out++ = 0xFFU - (UINT8)(((byte >> 4) & 0x0FU) * 0x11U); + } + pixels -= 2; + } +} + +static void +unpackL4R(UINT8* out, const UINT8* in, int pixels) +{ + /* nibbles (bit order reversed, white is non-zero) */ + while (pixels > 0) { + UINT8 byte = *in++; + byte = BITFLIP[byte]; + switch (pixels) { + default: *out++ = ((byte >> 4) & 0x0FU) * 0x11U; byte <<= 4; + case 1: *out++ = ((byte >> 4) & 0x0FU) * 0x11U; + } + pixels -= 2; + } +} + +static void +unpackL4IR(UINT8* out, const UINT8* in, int pixels) +{ + /* nibbles (bit order reversed, white is zero) */ + while (pixels > 0) { + UINT8 byte = *in++; + byte = BITFLIP[byte]; + switch (pixels) { + default: *out++ = 0xFFU - (UINT8)(((byte >> 4) & 0x0FU) * 0x11U); byte <<= 4; + case 1: *out++ = 0xFFU - (UINT8)(((byte >> 4) & 0x0FU) * 0x11U); } pixels -= 2; } @@ -758,13 +852,13 @@ unpackCMYKI(UINT8* out, const UINT8* in, int pixels) /* Unpack to "LAB" image */ /* There are two representations of LAB images for whatever precision: L: Uint (in PS, it's 0-100) - A: Int (in ps, -128 .. 128, or elsewhere 0..255, with 128 as middle. - Channels in PS display a 0 value as middle grey, + A: Int (in ps, -128 .. 128, or elsewhere 0..255, with 128 as middle. + Channels in PS display a 0 value as middle grey, LCMS appears to use 128 as the 0 value for these channels) - B: Int (as above) + B: Int (as above) - Since we don't have any signed ints, we're going with the shifted versions - internally, and we'll unshift for saving and whatnot. + Since we don't have any signed ints, we're going with the shifted versions + internally, and we'll unshift for saving and whatnot. */ void ImagingUnpackLAB(UINT8* out, const UINT8* in, int pixels) @@ -802,15 +896,15 @@ unpackI16N_I16(UINT8* out, const UINT8* in, int pixels){ static void unpackI12_I16(UINT8* out, const UINT8* in, int pixels){ - /* Fillorder 1/MSB -> LittleEndian, for 12bit integer greyscale tiffs. + /* Fillorder 1/MSB -> LittleEndian, for 12bit integer greyscale tiffs. - According to the TIFF spec: + According to the TIFF spec: FillOrder = 2 should be used only when BitsPerSample = 1 and the data is either uncompressed or compressed using CCITT 1D or 2D compression, to avoid potentially ambiguous situations. - Yeah. I thought so. We'll see how well people read the spec. + Yeah. I thought so. We'll see how well people read the spec. We've got several fillorder=2 modes in TiffImagePlugin.py There's no spec I can find. It appears that the in storage @@ -1053,7 +1147,15 @@ static struct { /* greyscale */ {"L", "L;2", 2, unpackL2}, + {"L", "L;2I", 2, unpackL2I}, + {"L", "L;2R", 2, unpackL2R}, + {"L", "L;2IR", 2, unpackL2IR}, + {"L", "L;4", 4, unpackL4}, + {"L", "L;4I", 4, unpackL4I}, + {"L", "L;4R", 4, unpackL4R}, + {"L", "L;4IR", 4, unpackL4IR}, + {"L", "L", 8, copy1}, {"L", "L;I", 8, unpackLI}, {"L", "L;R", 8, unpackLR}, diff --git a/mp_compile.py b/mp_compile.py index a930f4245..8f86f0bee 100644 --- a/mp_compile.py +++ b/mp_compile.py @@ -51,15 +51,35 @@ def _mp_compile(self, sources, output_dir=None, macros=None, # Return *all* object filenames, not just the ones we just built. return objects -# explicitly don't enable if environment says 1 processor -if MAX_PROCS != 1 and not sys.platform.startswith('win'): - try: - # bug, only enable if we can make a Pool. see issue #790 and - # http://stackoverflow.com/questions/6033599/oserror-38-errno-38-with-multiprocessing - pool = Pool(2) - CCompiler.compile = _mp_compile - except Exception as msg: - print("Exception installing mp_compile, proceeding without: %s" % msg) -else: - print("Single threaded build, not installing mp_compile: %s processes" % - MAX_PROCS) + +def install(): + + fl_pypy3 = hasattr(sys, 'pypy_version_info') and sys.version_info > (3, 0) + fl_win = sys.platform.startswith('win') + fl_cygwin = sys.platform.startswith('cygwin') + + if fl_pypy3: + # see https://github.com/travis-ci/travis-ci/issues/3587 + print("Single threaded build for pypy3") + return + + if fl_win or fl_cygwin: + #windows barfs on multiprocessing installs + print("Single threaded build for windows") + return + + if MAX_PROCS != 1: + # explicitly don't enable if environment says 1 processor + try: + # bug, only enable if we can make a Pool. see issue #790 and + # http://stackoverflow.com/questions/6033599/oserror-38-errno-38-with-multiprocessing + pool = Pool(2) + CCompiler.compile = _mp_compile + except Exception as msg: + print("Exception installing mp_compile, proceeding without:" + "%s" % msg) + else: + print("Single threaded build, not installing mp_compile:" + "%s processes" % MAX_PROCS) + +install() diff --git a/path.c b/path.c index db4a68e24..dd0ca8b4e 100644 --- a/path.c +++ b/path.c @@ -57,6 +57,10 @@ alloc_array(Py_ssize_t count) PyErr_NoMemory(); return NULL; } + if (count > (SIZE_MAX / (2 * sizeof(double))) - 1 ) { + PyErr_NoMemory(); + return NULL; + } xy = malloc(2 * count * sizeof(double) + 1); if (!xy) PyErr_NoMemory(); diff --git a/requirements.txt b/requirements.txt index bd37d7ba9..6a581002b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,8 +1,25 @@ -# Testing and documentation requirements +# Development, documentation & testing requirements. -e . --r docs/requirements.txt -coveralls -nose -nose-cov -pep8 -pyflakes +Babel==2.2.0 +Jinja2==2.8 +MarkupSafe==0.23 +Pygments==2.1.3 +Sphinx==1.3.6 +alabaster==0.7.7 +check-manifest==0.31 +cov-core==1.15.0 +coverage==4.0.3 +coveralls==1.1 +docopt==0.6.2 +docutils==0.12 +jarn.viewdoc==1.7 +nose-cov==1.6 +nose==1.3.7 +pep8==1.7.0 +pyflakes==1.1.0 +pyroma==2.0.2 +pytz==2016.2 +requests==2.9.1 +six==1.10.0 +snowballstemmer==1.2.1 +sphinx-rtd-theme==0.1.9 diff --git a/selftest.py b/selftest.py index 4ffae3a16..5af1fd762 100644 --- a/selftest.py +++ b/selftest.py @@ -14,8 +14,6 @@ from PIL import features if "--installed" in sys.argv: sys.path.insert(0, sys_path_0) -ROOT = "." - try: Image.core.ping except ImportError as v: @@ -50,13 +48,13 @@ def testimage(): Or open existing files: - >>> im = Image.open(os.path.join(ROOT, "Tests/images/hopper.gif")) + >>> im = Image.open("Tests/images/hopper.gif") >>> _info(im) ('GIF', 'P', (128, 128)) - >>> _info(Image.open(os.path.join(ROOT, "Tests/images/hopper.ppm"))) + >>> _info(Image.open("Tests/images/hopper.ppm")) ('PPM', 'RGB', (128, 128)) >>> try: - ... _info(Image.open(os.path.join(ROOT, "Tests/images/hopper.jpg"))) + ... _info(Image.open("Tests/images/hopper.jpg")) ... except IOError as v: ... print(v) ('JPEG', 'RGB', (128, 128)) @@ -64,7 +62,7 @@ def testimage(): PIL doesn't actually load the image data until it's needed, or you call the "load" method: - >>> im = Image.open(os.path.join(ROOT, "Tests/images/hopper.ppm")) + >>> im = Image.open("Tests/images/hopper.ppm") >>> print(im.im) # internal image attribute None >>> a = im.load() @@ -74,7 +72,7 @@ def testimage(): You can apply many different operations on images. Most operations return a new image: - >>> im = Image.open(os.path.join(ROOT, "Tests/images/hopper.ppm")) + >>> im = Image.open("Tests/images/hopper.ppm") >>> _info(im.convert("L")) (None, 'L', (128, 128)) >>> _info(im.copy()) @@ -206,9 +204,8 @@ if __name__ == "__main__": # use doctest to make sure the test program behaves as documented! import doctest - import selftest print("Running selftest:") - status = doctest.testmod(selftest) + status = doctest.testmod(sys.modules[__name__]) if status[0]: print("*** %s tests of %d failed." % status) exit_status = 1 diff --git a/setup.py b/setup.py index 216322ec2..44c226092 100644 --- a/setup.py +++ b/setup.py @@ -22,18 +22,16 @@ from setuptools import Extension, setup, find_packages # comment this out to disable multi threaded builds. import mp_compile -_IMAGING = ( - "decode", "encode", "map", "display", "outline", "path") +_IMAGING = ("decode", "encode", "map", "display", "outline", "path") _LIB_IMAGING = ( "Access", "AlphaComposite", "Resample", "Bands", "BitDecode", "Blend", "Chops", "Convert", "ConvertYCbCr", "Copy", "Crc32", "Crop", "Dib", "Draw", - "Effects", "EpsEncode", "File", "Fill", "Filter", "FliDecode", - "Geometry", "GetBBox", "GifDecode", "GifEncode", "HexDecode", - "Histo", "JpegDecode", "JpegEncode", "LzwDecode", "Matrix", - "ModeFilter", "MspDecode", "Negative", "Offset", "Pack", - "PackDecode", "Palette", "Paste", "Quant", "QuantOctree", "QuantHash", - "QuantHeap", "PcdDecode", "PcxDecode", "PcxEncode", "Point", + "Effects", "EpsEncode", "File", "Fill", "Filter", "FliDecode", "Geometry", + "GetBBox", "GifDecode", "GifEncode", "HexDecode", "Histo", "JpegDecode", + "JpegEncode", "LzwDecode", "Matrix", "ModeFilter", "MspDecode", "Negative", + "Offset", "Pack", "PackDecode", "Palette", "Paste", "Quant", "QuantOctree", + "QuantHash", "QuantHeap", "PcdDecode", "PcxDecode", "PcxEncode", "Point", "RankFilter", "RawDecode", "RawEncode", "Storage", "SunRleDecode", "TgaRleDecode", "Unpack", "UnpackYCC", "UnsharpMask", "XbmDecode", "XbmEncode", "ZipDecode", "ZipEncode", "TiffDecode", "Incremental", @@ -65,13 +63,13 @@ def _find_library_file(self, library): if 'cpython' in self.compiler.shared_lib_extension: existing = self.compiler.shared_lib_extension self.compiler.shared_lib_extension = "." + existing.split('.')[-1] - ret = self.compiler.find_library_file( - self.compiler.library_dirs, library) + ret = self.compiler.find_library_file(self.compiler.library_dirs, + library) self.compiler.shared_lib_extension = existing return ret else: - return self.compiler.find_library_file( - self.compiler.library_dirs, library) + return self.compiler.find_library_file(self.compiler.library_dirs, + library) def _lib_include(root): @@ -82,15 +80,15 @@ def _lib_include(root): def _read(file): return open(file, 'rb').read() + try: import _tkinter except (ImportError, OSError): # pypy emits an oserror _tkinter = None - NAME = 'Pillow' -PILLOW_VERSION = '2.9.0.dev0' +PILLOW_VERSION = '3.3.0.dev0' TCL_ROOT = None JPEG_ROOT = None JPEG2K_ROOT = None @@ -101,11 +99,10 @@ LCMS_ROOT = None class pil_build_ext(build_ext): - class feature: zlib = jpeg = tiff = freetype = tcl = tk = lcms = webp = webpmux = None jpeg2000 = None - required = [] + required = set(['jpeg', 'zlib']) def require(self, feat): return feat in self.required @@ -121,11 +118,9 @@ class pil_build_ext(build_ext): feature = feature() user_options = build_ext.user_options + [ - ('disable-%s' % x, None, 'Disable support for %s' % x) - for x in feature + ('disable-%s' % x, None, 'Disable support for %s' % x) for x in feature ] + [ - ('enable-%s' % x, None, 'Enable support for %s' % x) - for x in feature + ('enable-%s' % x, None, 'Enable support for %s' % x) for x in feature ] def initialize_options(self): @@ -139,12 +134,13 @@ class pil_build_ext(build_ext): for x in self.feature: if getattr(self, 'disable_%s' % x): setattr(self.feature, x, False) + self.feature.required.discard(x) if getattr(self, 'enable_%s' % x): raise ValueError( - 'Conflicting options: --enable-%s and --disable-%s' - % (x, x)) + 'Conflicting options: --enable-%s and --disable-%s' % + (x, x)) if getattr(self, 'enable_%s' % x): - self.feature.required.append(x) + self.feature.required.add(x) def build_extensions(self): @@ -196,8 +192,9 @@ class pil_build_ext(build_ext): if sys.platform == "cygwin": # pythonX.Y.dll.a is in the /usr/lib/pythonX.Y/config directory - _add_directory(library_dirs, os.path.join( - "/usr/lib", "python%s" % sys.version[:3], "config")) + _add_directory(library_dirs, + os.path.join("/usr/lib", "python%s" % + sys.version[:3], "config")) elif sys.platform == "darwin": # attempt to make sure we pick freetype2 over other versions @@ -213,9 +210,8 @@ class pil_build_ext(build_ext): # if Homebrew is installed, use its lib and include directories import subprocess try: - prefix = subprocess.check_output( - ['brew', '--prefix'] - ).strip().decode('latin1') + prefix = subprocess.check_output(['brew', '--prefix']).strip( + ).decode('latin1') except: # Homebrew not installed prefix = None @@ -231,8 +227,8 @@ class pil_build_ext(build_ext): if ft_prefix and os.path.isdir(ft_prefix): # freetype might not be linked into Homebrew's prefix _add_directory(library_dirs, os.path.join(ft_prefix, 'lib')) - _add_directory( - include_dirs, os.path.join(ft_prefix, 'include')) + _add_directory(include_dirs, + os.path.join(ft_prefix, 'include')) else: # fall back to freetype from XQuartz if # Homebrew's freetype is missing @@ -242,7 +238,7 @@ class pil_build_ext(build_ext): elif sys.platform.startswith("linux"): arch_tp = (plat.processor(), plat.architecture()[0]) if arch_tp == ("x86_64", "32bit"): - # 32 bit build on 64 bit machine. + # 32-bit build on 64-bit machine. _add_directory(library_dirs, "/usr/lib/i386-linux-gnu") else: for platform_ in arch_tp: @@ -253,38 +249,37 @@ class pil_build_ext(build_ext): if platform_ in ["x86_64", "64bit"]: _add_directory(library_dirs, "/lib64") _add_directory(library_dirs, "/usr/lib64") - _add_directory( - library_dirs, "/usr/lib/x86_64-linux-gnu") + _add_directory(library_dirs, + "/usr/lib/x86_64-linux-gnu") break elif platform_ in ["i386", "i686", "32bit"]: - _add_directory( - library_dirs, "/usr/lib/i386-linux-gnu") + _add_directory(library_dirs, "/usr/lib/i386-linux-gnu") break elif platform_ in ["aarch64"]: _add_directory(library_dirs, "/usr/lib64") - _add_directory( - library_dirs, "/usr/lib/aarch64-linux-gnu") + _add_directory(library_dirs, + "/usr/lib/aarch64-linux-gnu") break elif platform_ in ["arm", "armv7l"]: - _add_directory( - library_dirs, "/usr/lib/arm-linux-gnueabi") + _add_directory(library_dirs, + "/usr/lib/arm-linux-gnueabi") break elif platform_ in ["ppc64"]: _add_directory(library_dirs, "/usr/lib64") - _add_directory( - library_dirs, "/usr/lib/ppc64-linux-gnu") - _add_directory( - library_dirs, "/usr/lib/powerpc64-linux-gnu") + _add_directory(library_dirs, + "/usr/lib/ppc64-linux-gnu") + _add_directory(library_dirs, + "/usr/lib/powerpc64-linux-gnu") break elif platform_ in ["ppc"]: _add_directory(library_dirs, "/usr/lib/ppc-linux-gnu") - _add_directory( - library_dirs, "/usr/lib/powerpc-linux-gnu") + _add_directory(library_dirs, + "/usr/lib/powerpc-linux-gnu") break elif platform_ in ["s390x"]: _add_directory(library_dirs, "/usr/lib64") - _add_directory( - library_dirs, "/usr/lib/s390x-linux-gnu") + _add_directory(library_dirs, + "/usr/lib/s390x-linux-gnu") break elif platform_ in ["s390"]: _add_directory(library_dirs, "/usr/lib/s390-linux-gnu") @@ -301,13 +296,20 @@ class pil_build_ext(build_ext): elif sys.platform.startswith("gnu"): self.add_multiarch_paths() + elif sys.platform.startswith("freebsd"): + _add_directory(library_dirs, "/usr/local/lib") + _add_directory(include_dirs, "/usr/local/include") + elif sys.platform.startswith("netbsd"): - _add_directory(library_dirs, "/usr/pkg/lib") - _add_directory(include_dirs, "/usr/pkg/include") + _add_directory(library_dirs, "/usr/pkg/lib") + _add_directory(include_dirs, "/usr/pkg/include") + + elif sys.platform.startswith("sunos5"): + _add_directory(library_dirs, "/opt/local/lib") + _add_directory(include_dirs, "/opt/local/include") # FIXME: check /opt/stuff directories here? - # # locate tkinter libraries if _tkinter: @@ -324,8 +326,12 @@ class pil_build_ext(build_ext): # locations later) os.path.join("/py" + PYVERSION, "Tcl"), os.path.join("/python" + PYVERSION, "Tcl"), - "/Tcl", "/Tcl" + TCLVERSION, "/Tcl" + TCL_VERSION, - os.path.join(os.environ.get("ProgramFiles", ""), "Tcl"), ] + "/Tcl", + "/Tcl" + TCLVERSION, + "/Tcl" + TCL_VERSION, + os.path.join( + os.environ.get("ProgramFiles", ""), "Tcl"), + ] for TCL_ROOT in roots: TCL_ROOT = os.path.abspath(TCL_ROOT) if os.path.isfile(os.path.join(TCL_ROOT, "include", "tk.h")): @@ -360,15 +366,14 @@ class pil_build_ext(build_ext): best_path = None for name in os.listdir(program_files): if name.startswith('OpenJPEG '): - version = tuple( - [int(x) for x in name[9:].strip().split('.')]) + version = tuple([int(x) for x in name[9:].strip().split( + '.')]) if version > best_version: best_version = version best_path = os.path.join(program_files, name) if best_path: - _add_directory(library_dirs, - os.path.join(best_path, 'lib')) + _add_directory(library_dirs, os.path.join(best_path, 'lib')) _add_directory(include_dirs, os.path.join(best_path, 'include')) @@ -389,16 +394,15 @@ class pil_build_ext(build_ext): if _find_library_file(self, "z"): feature.zlib = "z" elif (sys.platform == "win32" and - _find_library_file(self, "zlib")): + _find_library_file(self, "zlib")): feature.zlib = "zlib" # alternative name if feature.want('jpeg'): if _find_include_file(self, "jpeglib.h"): if _find_library_file(self, "jpeg"): feature.jpeg = "jpeg" - elif ( - sys.platform == "win32" and - _find_library_file(self, "libjpeg")): + elif (sys.platform == "win32" and + _find_library_file(self, "libjpeg")): feature.jpeg = "libjpeg" # alternative name feature.openjpeg_version = None @@ -428,8 +432,8 @@ class pil_build_ext(build_ext): # include path _add_directory(self.compiler.include_dirs, best_path, 0) feature.jpeg2000 = 'openjp2' - feature.openjpeg_version = '.'.join( - [str(x) for x in best_version]) + feature.openjpeg_version = '.'.join([str(x) for x in + best_version]) if feature.want('tiff'): if _find_library_file(self, "tiff"): @@ -453,9 +457,6 @@ class pil_build_ext(build_ext): if os.path.isfile(os.path.join(dir, "ft2build.h")): freetype_version = 21 break - if os.path.isdir(os.path.join(dir, "freetype")): - freetype_version = 20 - break if freetype_version: feature.freetype = "freetype" feature.freetype_version = freetype_version @@ -465,10 +466,13 @@ class pil_build_ext(build_ext): if feature.want('lcms'): if _find_include_file(self, "lcms2.h"): if _find_library_file(self, "lcms2"): - feature.lcms = "lcms" + feature.lcms = "lcms2" + elif _find_library_file(self, "lcms2_static"): + # alternate Windows name. + feature.lcms = "lcms2_static" if _tkinter and _find_include_file(self, "tk.h"): - # the library names may vary somewhat (e.g. tcl84 or tcl8.4) + # the library names may vary somewhat (e.g. tcl85 or tcl8.5) version = TCL_VERSION[0] + TCL_VERSION[2] if feature.want('tcl'): if _find_library_file(self, "tcl" + version): @@ -502,9 +506,13 @@ class pil_build_ext(build_ext): for f in feature: if not getattr(feature, f) and feature.require(f): + if f in ('jpeg', 'zlib'): + raise ValueError( + '%s is required unless explicitly disabled' + ' using --disable-%s, aborting' % (f, f)) raise ValueError( - '--enable-%s requested but %s not found, aborting.' - % (f, f)) + '--enable-%s requested but %s not found, aborting.' % + (f, f)) # # core library @@ -536,32 +544,26 @@ class pil_build_ext(build_ext): if struct.unpack("h", "\0\1".encode('ascii'))[0] == 1: defs.append(("WORDS_BIGENDIAN", None)) - exts = [(Extension( - "PIL._imaging", files, libraries=libs, define_macros=defs))] + exts = [(Extension("PIL._imaging", + files, + libraries=libs, + define_macros=defs))] # # additional libraries if feature.freetype: - defs = [] - if feature.freetype_version == 20: - defs.append(("USE_FREETYPE_2_0", None)) - exts.append(Extension( - "PIL._imagingft", ["_imagingft.c"], libraries=["freetype"], - define_macros=defs)) - - if os.path.isfile("_imagingtiff.c") and feature.tiff: - exts.append(Extension( - "PIL._imagingtiff", ["_imagingtiff.c"], libraries=["tiff"])) + exts.append(Extension("PIL._imagingft", + ["_imagingft.c"], + libraries=["freetype"])) if os.path.isfile("_imagingcms.c") and feature.lcms: extra = [] if sys.platform == "win32": extra.extend(["user32", "gdi32"]) - exts.append(Extension( - "PIL._imagingcms", - ["_imagingcms.c"], - libraries=["lcms2"] + extra)) + exts.append(Extension("PIL._imagingcms", + ["_imagingcms.c"], + libraries=[feature.lcms] + extra)) if os.path.isfile("_webp.c") and feature.webp: libs = [feature.webp] @@ -572,35 +574,38 @@ class pil_build_ext(build_ext): libs.append(feature.webpmux) libs.append(feature.webpmux.replace('pmux', 'pdemux')) - exts.append(Extension( - "PIL._webp", ["_webp.c"], libraries=libs, define_macros=defs)) + exts.append(Extension("PIL._webp", + ["_webp.c"], + libraries=libs, + define_macros=defs)) - if sys.platform == "darwin": - # locate Tcl/Tk frameworks - frameworks = [] - framework_roots = [ - "/Library/Frameworks", - "/System/Library/Frameworks"] - for root in framework_roots: - if ( - os.path.exists(os.path.join(root, "Tcl.framework")) and - os.path.exists(os.path.join(root, "Tk.framework"))): - print("--- using frameworks at %s" % root) - frameworks = ["-framework", "Tcl", "-framework", "Tk"] - dir = os.path.join(root, "Tcl.framework", "Headers") - _add_directory(self.compiler.include_dirs, dir, 0) - dir = os.path.join(root, "Tk.framework", "Headers") - _add_directory(self.compiler.include_dirs, dir, 1) - break - if frameworks: - exts.append(Extension( - "PIL._imagingtk", ["_imagingtk.c", "Tk/tkImaging.c"], - extra_compile_args=frameworks, extra_link_args=frameworks)) - feature.tcl = feature.tk = 1 # mark as present - elif feature.tcl and feature.tk: - exts.append(Extension( - "PIL._imagingtk", ["_imagingtk.c", "Tk/tkImaging.c"], - libraries=[feature.tcl, feature.tk])) + if feature.tcl and feature.tk: + if sys.platform == "darwin": + # locate Tcl/Tk frameworks + frameworks = [] + framework_roots = [ + "/Library/Frameworks", "/System/Library/Frameworks" + ] + for root in framework_roots: + root_tcl = os.path.join(root, "Tcl.framework") + root_tk = os.path.join(root, "Tk.framework") + if (os.path.exists(root_tcl) and os.path.exists(root_tk)): + print("--- using frameworks at %s" % root) + frameworks = ["-framework", "Tcl", "-framework", "Tk"] + dir = os.path.join(root_tcl, "Headers") + _add_directory(self.compiler.include_dirs, dir, 0) + dir = os.path.join(root_tk, "Headers") + _add_directory(self.compiler.include_dirs, dir, 1) + break + if frameworks: + exts.append(Extension("PIL._imagingtk", + ["_imagingtk.c", "Tk/tkImaging.c"], + extra_compile_args=frameworks, + extra_link_args=frameworks)) + else: + exts.append(Extension("PIL._imagingtk", + ["_imagingtk.c", "Tk/tkImaging.c"], + libraries=[feature.tcl, feature.tk])) if os.path.isfile("_imagingmath.c"): exts.append(Extension("PIL._imagingmath", ["_imagingmath.c"])) @@ -638,13 +643,14 @@ class pil_build_ext(build_ext): (feature.tcl and feature.tk, "TKINTER"), (feature.jpeg, "JPEG"), (feature.jpeg2000, "OPENJPEG (JPEG2000)", - feature.openjpeg_version), + feature.openjpeg_version), (feature.zlib, "ZLIB (PNG/ZIP)"), (feature.tiff, "LIBTIFF"), (feature.freetype, "FREETYPE2"), (feature.lcms, "LITTLECMS2"), (feature.webp, "WEBP"), - (feature.webpmux, "WEBPMUX"), ] + (feature.webpmux, "WEBPMUX"), + ] all = 1 for option in options: @@ -712,12 +718,10 @@ class pil_build_ext(build_ext): fp = open(tmpfile, 'r') multiarch_path_component = fp.readline().strip() fp.close() - _add_directory( - self.compiler.library_dirs, - '/usr/lib/' + multiarch_path_component) - _add_directory( - self.compiler.include_dirs, - '/usr/include/' + multiarch_path_component) + _add_directory(self.compiler.library_dirs, + '/usr/lib/' + multiarch_path_component) + _add_directory(self.compiler.include_dirs, + '/usr/include/' + multiarch_path_component) finally: os.unlink(tmpfile) @@ -725,38 +729,40 @@ class pil_build_ext(build_ext): def debug_build(): return hasattr(sys, 'gettotalrefcount') -setup( - name=NAME, - version=PILLOW_VERSION, - description='Python Imaging Library (Fork)', - long_description=_read('README.rst').decode('utf-8'), - author='Alex Clark (Fork Author)', - author_email='aclark@aclark.net', - url='http://python-pillow.github.io/', - classifiers=[ - "Development Status :: 6 - Mature", - "Topic :: Multimedia :: Graphics", - "Topic :: Multimedia :: Graphics :: Capture :: Digital Camera", - "Topic :: Multimedia :: Graphics :: Capture :: Scanners", - "Topic :: Multimedia :: Graphics :: Capture :: Screen Capture", - "Topic :: Multimedia :: Graphics :: Graphics Conversion", - "Topic :: Multimedia :: Graphics :: Viewers", - "Programming Language :: Python :: 2", - "Programming Language :: Python :: 2.6", - "Programming Language :: Python :: 2.7", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.2", - "Programming Language :: Python :: 3.3", - "Programming Language :: Python :: 3.4", - ], - cmdclass={"build_ext": pil_build_ext}, - ext_modules=[Extension("PIL._imaging", ["_imaging.c"])], - include_package_data=True, - packages=find_packages(), - scripts=glob.glob("Scripts/pil*.py"), - test_suite='nose.collector', - keywords=["Imaging", ], - license='Standard PIL License', - zip_safe=not debug_build(), -) + +setup(name=NAME, + version=PILLOW_VERSION, + description='Python Imaging Library (Fork)', + long_description=_read('README.rst').decode('utf-8'), + author='Alex Clark (Fork Author)', + author_email='aclark@aclark.net', + url='http://python-pillow.org', + classifiers=[ + "Development Status :: 6 - Mature", + "Topic :: Multimedia :: Graphics", + "Topic :: Multimedia :: Graphics :: Capture :: Digital Camera", + "Topic :: Multimedia :: Graphics :: Capture :: Scanners", + "Topic :: Multimedia :: Graphics :: Capture :: Screen Capture", + "Topic :: Multimedia :: Graphics :: Graphics Conversion", + "Topic :: Multimedia :: Graphics :: Viewers", + "Programming Language :: Python :: 2", + "Programming Language :: Python :: 2.6", + "Programming Language :: Python :: 2.7", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.2", + "Programming Language :: Python :: 3.3", + "Programming Language :: Python :: 3.4", + "Programming Language :: Python :: 3.5", + 'Programming Language :: Python :: Implementation :: CPython', + 'Programming Language :: Python :: Implementation :: PyPy', + ], + cmdclass={"build_ext": pil_build_ext}, + ext_modules=[Extension("PIL._imaging", ["_imaging.c"])], + include_package_data=True, + packages=find_packages(), + scripts=glob.glob("Scripts/*.py"), + test_suite='nose.collector', + keywords=["Imaging", ], + license='Standard PIL License', + zip_safe=not debug_build(), ) # End of file diff --git a/tox.ini b/tox.ini index 80f7edef4..507af757a 100644 --- a/tox.ini +++ b/tox.ini @@ -1,14 +1,14 @@ -# Tox (http://tox.testrun.org/) is a tool for running tests +# Tox (https://testrun.org/tox/latest/) is a tool for running tests # in multiple virtualenvs. This configuration file will run the # test suite on all supported python versions. To use it, "pip install tox" # and then run "tox" from this directory. [tox] -envlist = py26, py27, py32, py33, py34 +envlist = py26, py27, py32, py33, py34, py35 [testenv] commands = {envpython} setup.py clean {envpython} setup.py build_ext --inplace {envpython} selftest.py - {envpython} Tests/run.py --installed + {envpython} test-installed.py --installed diff --git a/winbuild/.gitignore b/winbuild/.gitignore new file mode 100644 index 000000000..adc679fa8 --- /dev/null +++ b/winbuild/.gitignore @@ -0,0 +1,6 @@ +*.zip +*.tar.gz +*.msi +*.asc +__pycache__ +depends/ \ No newline at end of file diff --git a/winbuild/README.md b/winbuild/README.md new file mode 100644 index 000000000..918fb2ba3 --- /dev/null +++ b/winbuild/README.md @@ -0,0 +1,18 @@ +Quick README +------------ + +For more extensive info, see the windows build instructions `docs/build.rst`. + +* See https://github.com/python-pillow/Pillow/issues/553#issuecomment-37877416 and https://github.com/matplotlib/matplotlib/issues/1717#issuecomment-13343859 + +* Works best with Python 3.4, due to virtualenv and pip batteries included. Python3+ required for fetch command. +* Check config.py for virtual env paths, suffix for 64-bit releases. Defaults to `x64`, set `X64_EXT` to change. +* When running in CI with one Python per invocation, set the `PYTHON` env variable to the Python folder. (e.g. `PYTHON`=`c:\Python27\`) This overrides the matrix in config.py and will just build and test for the specific Python. +* `python get_pythons.py` downloads all the Python releases, and their signatures. (Manually) Install in `c:\PythonXX[x64]\`. +* `python build_dep.py` downloads and creates a build script for all the dependencies, in 32 and 64 bit versions, and with both compiler versions. +* (in powershell) `build_deps.cmd` invokes the dependency build. +* `python build.py --clean` makes Pillow for the matrix of Pythons. +* `python test.py` runs the tests on Pillow in all the virtual envs. +* Currently working with zlib, libjpeg, freetype, and libtiff on Python 2.7, 3.3, and 3.4, both 32 and 64 bit, on a local win7 pro machine and appveyor.com +* Webp is built, not detected. +* LCMS and OpenJpeg are not building. diff --git a/winbuild/build.py b/winbuild/build.py new file mode 100644 index 000000000..bdede470a --- /dev/null +++ b/winbuild/build.py @@ -0,0 +1,155 @@ +#/usr/bin/env python3 + +import subprocess +import shutil +import sys +import getopt +import os + +from config import * + + +def setup_vms(): + ret = [] + for py in pythons.keys(): + for arch in ('', X64_EXT): + ret.append("virtualenv -p c:/Python%s%s/python.exe --clear %s%s%s" + % (py, arch, VIRT_BASE, py, arch)) + ret.append("%s%s%s\Scripts\pip.exe install nose" % + (VIRT_BASE, py, arch)) + if py == '26': + ret.append("%s%s%s\Scripts\pip.exe install unittest2" % + (VIRT_BASE, py, arch)) + return "\n".join(ret) + + +def run_script(params): + (version, script) = params + try: + print("Running %s" % version) + filename = 'build_pillow_%s.cmd' % version + with open(filename, 'w') as f: + f.write(script) + + command = ['powershell', "./%s" % filename] + proc = subprocess.Popen(command, + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + (trace, stderr) = proc.communicate() + status = proc.returncode + print(stderr) + print("Done with %s: %s" % (version, status)) + return (version, status, trace, stderr) + except Exception as msg: + print("Error with %s: %s" % (version, str(msg))) + return (version, -1, "", str(msg)) + + +def header(op): + return r""" +setlocal +set MPLSRC=%%~dp0\.. +set INCLIB=%%~dp0\depends +set BLDOPT=%s +cd /D %%MPLSRC%% +""" % (op) + + +def footer(): + return """endlocal +exit +""" + + +def build_one(py_ver, compiler): + # UNDONE virtual envs if we're not running on appveyor + args = {} + args.update(compiler) + if 'PYTHON' in os.environ: + args['python_path'] = "%PYTHON%" + else: + args['python_path'] = "%s%s\\Scripts" % (VIRT_BASE, py_ver) + args['py_ver'] = py_ver + if '34' in py_ver: + args['tcl_ver'] = '86' + else: + args['tcl_ver'] = '85' + + return r""" +setlocal EnableDelayedExpansion +call "%%ProgramFiles%%\Microsoft SDKs\Windows\%(env_version)s\Bin\SetEnv.Cmd" /Release %(env_flags)s +set DISTUTILS_USE_SDK=1 +set LIB=%%LIB%%;%%INCLIB%%\%(inc_dir)s +set INCLUDE=%%INCLUDE%%;%%INCLIB%%\%(inc_dir)s;%%INCLIB%%\tcl%(tcl_ver)s\include + +setlocal +set LIB=%%LIB%%;C:\Python%(py_ver)s\tcl +call %(python_path)s\python.exe setup.py %%BLDOPT%% +endlocal + +endlocal +""" % args + + +def clean(): + try: + shutil.rmtree('../build') + except: + # could already be removed + pass + run_script(('virtualenvs', setup_vms())) + + +def main(op): + scripts = [] + + for py_version, compiler_version in pythons.items(): + scripts.append((py_version, + "\n".join([header(op), + build_one(py_version, + compilers[(compiler_version, + 32)]), + footer()]))) + + scripts.append(("%s%s" % (py_version, X64_EXT), + "\n".join([header(op), + build_one("%sx64" % py_version, + compilers[(compiler_version, + 64)]), + footer()]))) + + results = map(run_script, scripts) + + for (version, status, trace, err) in results: + print("Compiled %s: %s" % (version, status and 'ERR' or 'OK')) + + +def run_one(op): + + compiler = compiler_fromEnv() + py_version = pyversion_fromEnv() + + run_script((py_version, + "\n".join([header(op), + build_one(py_version, compiler), + footer()]) + )) + + +if __name__ == '__main__': + opts, args = getopt.getopt(sys.argv[1:], '', ['clean', 'dist']) + opts = dict(opts) + + if '--clean' in opts: + clean() + + op = 'install' + if '--dist' in opts: + op = "bdist_wininst --user-access-control=auto" + + if 'PYTHON' in os.environ: + run_one(op) + else: + main(op) diff --git a/winbuild/build.rst b/winbuild/build.rst new file mode 100644 index 000000000..e4c2293a9 --- /dev/null +++ b/winbuild/build.rst @@ -0,0 +1,94 @@ +Building Pillow on Windows +========================== + +.. note:: For most people, the :doc:`installation instructions + ` should be sufficient + +This page will describe a build setup to build Pillow against the +supported Python versions in 32 and 64-bit modes, using freely +available Microsoft compilers. This has been developed and tested +against 64-bit Windows 7 Professional and Windows Server 2012 +64-bit version on Amazon EC2. + +Prerequisites +------------- + +Extra Build Helpers +^^^^^^^^^^^^^^^^^^^ + +* Powershell (available by default on Windows Server) +* GitHub client (provides git+bash shell) + +Optional: +* GPG (for checking signatures) (UNDONE -- Python signature checking) + + +Pythons +^^^^^^^ + +The build routines expect Python to be installed at C:\PythonXX for +32-bit versions or C:\PythonXXx64 for the 64-bit versions. + +Download Python 3.4, install it, and add it to the path. This is the +Python that we will use to bootstrap the build process. (The download +routines are using 3.2+ features, and installing 3.4 gives us pip and +virtualenv as well, reducing the number of packages that we need to +install.) + +Download the rest of the Pythons by opening a command window, changing +to the `winbuild` directory, and running `python +get_pythons.py`. + +UNDONE -- gpg verify the signatures (note that we can download from +https) + +Run each installer and set the proper path to the installation. Don't +set any of them as the default Python, or add them to the path. + + +Compilers +^^^^^^^^^ + +Download and install: + +* `Microsoft Windows SDK for Windows 7 and .NET Framework 3.5 + SP1 `_ + +* `Microsoft Windows SDK for Windows 7 and .NET Framework + 4 `_ + +* `CMake-2.8.10.2-win32-x86.exe `_ + +The samples and the .NET SDK portions aren't required, just the +compilers and other tools. UNDONE -- check exact wording. + +Dependencies +------------ + +The script 'build_dep.py' downloads and builds the dependencies. Open +a command window, change directory into `winbuild` and run `python +build_dep.py`. + +This will download libjpeg, libtiff, libz, and freetype. It will then +compile 32 and 64-bit versions of the libraries, with both versions of +the compilers. + +UNDONE -- lcms fails. +UNDONE -- webp, jpeg2k not recognized + +Building Pillow +--------------- + +Once the dependencies are built, run `python build.py --clean` to +build and install Pillow in virtualenvs for each python +build. `build.py --dist` will build Windows installers instead of +installing into virtualenvs. + +UNDONE -- suppressed output, what about failures. + +Testing Pillow +-------------- + +Build and install Pillow, then run `python test.py` from the +`winbuild` directory. + diff --git a/winbuild/build_dep.py b/winbuild/build_dep.py new file mode 100644 index 000000000..02c4edb6a --- /dev/null +++ b/winbuild/build_dep.py @@ -0,0 +1,323 @@ +from fetch import fetch +from unzip import unzip +from untar import untar +import os +import hashlib + +from config import bin_libs, compilers, compiler_fromEnv, libs + + +def _relpath(*args): + return os.path.join(os.getcwd(), *args) + + +def _relbuild(*args): + return _relpath('build', *args) + +build_dir = _relpath('build') +inc_dir = _relpath('depends') + + +def check_hash(filename, checksum): + if not checksum: + return filename + + (algo, value) = checksum.split(':') + h = hashlib.new(algo) + with open(filename, 'rb') as f: + h.update(f.read()) + if not(h.hexdigest().lower() == value): + raise ValueError('Checksum Mismatch for %s' % filename) + return filename + + +def check_sig(filename, signame): + # UNDONE -- need gpg + return filename + + +def mkdirs(): + try: + os.mkdir(build_dir) + except OSError: + pass + try: + os.mkdir(inc_dir) + except OSError: + pass + for compiler in compilers.values(): + try: + os.mkdir(os.path.join(inc_dir, compiler['inc_dir'])) + except OSError: + pass + + +def extract(src, dest): + if '.zip' in src: + return unzip(src, dest) + if '.tar.gz' in src or '.tgz' in src: + return untar(src, dest) + + +def fetch_libs(): + for name, lib in libs.items(): + if name == 'openjpeg': + filename = check_hash(fetch(lib['url']), lib['hash']) + for compiler in compilers.values(): + if not os.path.exists(os.path.join( + build_dir, lib['dir']+compiler['inc_dir'])): + extract(filename, build_dir) + os.rename(os.path.join(build_dir, lib['dir']), + os.path.join( + build_dir, lib['dir']+compiler['inc_dir'])) + else: + extract(check_hash(fetch(lib['url']), lib['hash']), build_dir) + + +def extract_binlib(): + lib = bin_libs['openjpeg'] + extract(lib['filename'], build_dir) + + +def extract_openjpeg(compiler): + return r""" +rem build openjpeg +setlocal +@echo on +cd %%BUILD%% +mkdir %%INCLIB%%\openjpeg-2.0 +copy /Y /B openjpeg-2.0.0-win32-x86\include\openjpeg-2.0 %%INCLIB%%\openjpeg-2.0 +copy /Y /B openjpeg-2.0.0-win32-x86\bin\ %%INCLIB%% +copy /Y /B openjpeg-2.0.0-win32-x86\lib\ %%INCLIB%% +endlocal +""" % compiler + + +def cp_tk(ver_85, ver_86): + versions = {'ver_85': ver_85, 'ver_86': ver_86} + return r""" +mkdir %%INCLIB%%\tcl85\include\X11 +copy /Y /B %%BUILD%%\tcl%(ver_85)s\generic\*.h %%INCLIB%%\tcl85\include\ +copy /Y /B %%BUILD%%\tk%(ver_85)s\generic\*.h %%INCLIB%%\tcl85\include\ +copy /Y /B %%BUILD%%\tk%(ver_85)s\xlib\X11\* %%INCLIB%%\tcl85\include\X11\ + +mkdir %%INCLIB%%\tcl86\include\X11 +copy /Y /B %%BUILD%%\tcl%(ver_86)s\generic\*.h %%INCLIB%%\tcl86\include\ +copy /Y /B %%BUILD%%\tk%(ver_86)s\generic\*.h %%INCLIB%%\tcl86\include\ +copy /Y /B %%BUILD%%\tk%(ver_86)s\xlib\X11\* %%INCLIB%%\tcl86\include\X11\ +""" % versions + + +def header(): + return r"""setlocal +set MSBUILD=C:\Windows\Microsoft.NET\Framework64\v4.0.30319\MSBuild.exe +set CMAKE="cmake.exe" +set INCLIB=%~dp0\depends +set BUILD=%~dp0\build +""" + "\n".join('set %s=%%BUILD%%\%s' % (k.upper(), v['dir']) + for (k, v) in libs.items() if v['dir']) + + +def setup_compiler(compiler): + return r"""setlocal EnableDelayedExpansion +call "%%ProgramFiles%%\Microsoft SDKs\Windows\%(env_version)s\Bin\SetEnv.Cmd" /Release %(env_flags)s +set INCLIB=%%INCLIB%%\%(inc_dir)s +""" % compiler + + +def end_compiler(): + return """ +endlocal +""" + + +def nmake_openjpeg(compiler): + atts = {'op_ver': '2.1'} + atts.update(compiler) + return r""" +rem build openjpeg +setlocal +@echo on +cd /D %%OPENJPEG%%%(inc_dir)s +%%CMAKE%% -DBUILD_THIRDPARTY:BOOL=OFF -G "NMake Makefiles" . +nmake -f Makefile clean +nmake -f Makefile +copy /Y /B bin\* %%INCLIB%% +mkdir %%INCLIB%%\openjpeg-%(op_ver)s +copy /Y /B src\lib\openjp2\*.h %%INCLIB%%\openjpeg-%(op_ver)s +endlocal +""" % atts + + +def msbuild_openjpeg(compiler): + atts = {'op_ver': '2.1'} + atts.update(compiler) + return r""" +rem build openjpeg +setlocal +@echo on +cd /D %%OPENJPEG%%%(inc_dir)s + +%%CMAKE%% -DBUILD_THIRDPARTY:BOOL=OFF -G "NMake Makefiles" . +nmake -f Makefile clean +nmake -f Makefile +copy /Y /B bin\* %%INCLIB%% +mkdir %%INCLIB%%\openjpeg-%(op_ver)s +copy /Y /B src\lib\openjp2\*.h %%INCLIB%%\openjpeg-%(op_ver)s +endlocal +""" % atts + + +def nmake_libs(compiler): + # undone -- pre, makes, headers, libs + return r""" +rem Build libjpeg +setlocal +cd /D %%JPEG%% +nmake -f makefile.vc setup-vc6 +nmake -f makefile.vc clean +nmake -f makefile.vc libjpeg.lib +copy /Y /B *.dll %%INCLIB%% +copy /Y /B *.lib %%INCLIB%% +copy /Y /B j*.h %%INCLIB%% +endlocal + +rem Build zlib +setlocal +cd /D %%ZLIB%% +nmake -f win32\Makefile.msc clean +nmake -f win32\Makefile.msc zlib.lib +copy /Y /B *.dll %%INCLIB%% +copy /Y /B *.lib %%INCLIB%% +copy /Y /B zlib.lib %%INCLIB%%\z.lib +copy /Y /B zlib.h %%INCLIB%% +copy /Y /B zconf.h %%INCLIB%% +endlocal + +rem Build webp +setlocal +cd /D %%WEBP%% +rd /S /Q %%WEBP%%\output\release-static +nmake -f Makefile.vc CFG=release-static RTLIBCFG=static OBJDIR=output all +copy /Y /B output\release-static\%(webp_platform)s\lib\* %%INCLIB%% +mkdir %%INCLIB%%\webp +copy /Y /B src\webp\*.h %%INCLIB%%\\webp +endlocal + +rem Build libtiff +setlocal +rem do after building jpeg and zlib +copy %%~dp0\nmake.opt %%TIFF%% + +cd /D %%TIFF%% +nmake -f makefile.vc clean +nmake -f makefile.vc lib +copy /Y /B libtiff\*.dll %%INCLIB%% +copy /Y /B libtiff\*.lib %%INCLIB%% +copy /Y /B libtiff\tiff*.h %%INCLIB%% +endlocal + + +""" % compiler + + +def msbuild_freetype(compiler): + if compiler['env_version'] == 'v7.1': + return msbuild_freetype_71(compiler) + return msbuild_freetype_70(compiler) + + +def msbuild_freetype_71(compiler): + return r""" +rem Build freetype +setlocal +rd /S /Q %%FREETYPE%%\objs +%%MSBUILD%% %%FREETYPE%%\builds\windows\vc%(vc_version)s\freetype.sln /t:Clean;Build /p:Configuration="Release" /p:Platform=%(platform)s /m +xcopy /Y /E /Q %%FREETYPE%%\include %%INCLIB%% +copy /Y /B %%FREETYPE%%\objs\vc%(vc_version)s\%(platform)s\*.lib %%INCLIB%%\freetype.lib +endlocal +""" % compiler + + +def msbuild_freetype_70(compiler): + return r""" +rem Build freetype +setlocal +py -3 %%~dp0\fixproj.py %%FREETYPE%%\builds\windows\vc%(vc_version)s\freetype.sln %(platform)s +py -3 %%~dp0\fixproj.py %%FREETYPE%%\builds\windows\vc%(vc_version)s\freetype.vcproj %(platform)s +rd /S /Q %%FREETYPE%%\objs +%%MSBUILD%% %%FREETYPE%%\builds\windows\vc%(vc_version)s\freetype.sln /t:Clean;Build /p:Configuration="LIB Release";Platform=%(platform)s /m +xcopy /Y /E /Q %%FREETYPE%%\include %%INCLIB%% +xcopy /Y /E /Q %%FREETYPE%%\objs\win32\vc%(vc_version)s %%INCLIB%% +copy /Y /B %%FREETYPE%%\objs\win32\vc%(vc_version)s\*.lib %%INCLIB%%\freetype.lib +endlocal +""" % compiler + + +def build_lcms2(compiler): + if compiler['env_version'] == 'v7.1': + return build_lcms_71(compiler) + return build_lcms_70(compiler) + + +def build_lcms_70(compiler): + """Link error here on x64""" + if compiler['platform'] == 'x64': + return '' + + """Build LCMS on VC2008. This version is only 32bit/Win32""" + return r""" +rem Build lcms2 +setlocal +rd /S /Q %%LCMS%%\Lib +rd /S /Q %%LCMS%%\Projects\VC%(vc_version)s\Release +%%MSBUILD%% %%LCMS%%\Projects\VC%(vc_version)s\lcms2.sln /t:Clean /p:Configuration="Release" /p:Platform=Win32 /m +%%MSBUILD%% %%LCMS%%\Projects\VC%(vc_version)s\lcms2.sln /t:lcms2_static /p:Configuration="Release" /p:Platform=Win32 /m +xcopy /Y /E /Q %%LCMS%%\include %%INCLIB%% +copy /Y /B %%LCMS%%\Projects\VC%(vc_version)s\Release\*.lib %%INCLIB%% +endlocal +""" % compiler + + +def build_lcms_71(compiler): + return r""" +rem Build lcms2 +setlocal +rd /S /Q %%LCMS%%\Lib +rd /S /Q %%LCMS%%\Projects\VC%(vc_version)s\Release +%%MSBUILD%% %%LCMS%%\Projects\VC%(vc_version)s\lcms2.sln /t:Clean /p:Configuration="Release" /p:Platform=%(platform)s /m +%%MSBUILD%% %%LCMS%%\Projects\VC%(vc_version)s\lcms2.sln /t:lcms2_static /p:Configuration="Release" /p:Platform=%(platform)s /m +xcopy /Y /E /Q %%LCMS%%\include %%INCLIB%% +copy /Y /B %%LCMS%%\Lib\MS\*.lib %%INCLIB%% +endlocal +""" % compiler + + +def add_compiler(compiler): + script.append(setup_compiler(compiler)) + script.append(nmake_libs(compiler)) + + # script.append(extract_openjpeg(compiler)) + + script.append(msbuild_freetype(compiler)) + script.append(build_lcms2(compiler)) + # script.append(nmake_openjpeg(compiler)) + script.append(end_compiler()) + + +mkdirs() +fetch_libs() +# extract_binlib() +script = [header(), cp_tk(libs['tk-8.5']['version'], libs['tk-8.6']['version'])] + + +if 'PYTHON' in os.environ: + add_compiler(compiler_fromEnv()) +else: + # for compiler in compilers.values(): + # add_compiler(compiler) + add_compiler(compilers[(7.0, 32)]) + # add_compiler(compilers[(7.1, 64)]) + +with open('build_deps.cmd', 'w') as f: + f.write("\n".join(script)) diff --git a/winbuild/config.py b/winbuild/config.py new file mode 100644 index 000000000..070986e9a --- /dev/null +++ b/winbuild/config.py @@ -0,0 +1,145 @@ +import os + +SF_MIRROR = 'http://iweb.dl.sourceforge.net' + +pythons = { # '26': 7, + '27': 7, + # '32': 7, + '33': 7.1, + '34': 7.1} + +VIRT_BASE = "c:/vp/" +X64_EXT = os.environ.get('X64_EXT', "x64") + +libs = { + 'zlib': { + 'url': 'http://zlib.net/zlib128.zip', + 'hash': 'md5:126f8676442ffbd97884eb4d6f32afb4', + 'dir': 'zlib-1.2.8', + }, + 'jpeg': { + 'url': 'http://www.ijg.org/files/jpegsr9b.zip', + 'hash': 'md5:a21b8024d78ba05857a75272b4fa95ec', # not found - generated by wiredfool + 'dir': 'jpeg-9b', + }, + 'tiff': { + 'url': 'ftp://ftp.remotesensing.org/pub/libtiff/tiff-4.0.6.zip', + 'hash': 'md5:f5b485d750b2001255ed64224b98b857', + 'dir': 'tiff-4.0.6', + }, + 'freetype': { + 'url': 'http://download.savannah.gnu.org/releases/freetype/freetype-2.6.3.tar.gz', + 'hash': 'md5:8b0c80b64042b7c39b9fa9debe6305c3', + 'dir': 'freetype-2.6.3', + }, + 'lcms': { + 'url': SF_MIRROR+'/project/lcms/lcms/2.7/lcms2-2.7.zip', + 'hash': 'sha1:7ff1a5b721ca719760ba6eb4ec6f38d5e65381cf', + 'dir': 'lcms2-2.7', + }, + 'tcl-8.5': { + 'url': SF_MIRROR+'/project/tcl/Tcl/8.5.19/tcl8519-src.zip', + 'hash': 'sha1:9de57fd34bd688716c16c978db96fa16a5fde924', + 'dir': '', + }, + 'tk-8.5': { + 'url': SF_MIRROR+'/project/tcl/Tcl/8.5.19/tk8519-src.zip', + 'hash': 'sha1:78d0d2c81e024e0b48bfd7b2cc16718f08f46ed9', + 'dir': '', + 'version': '8.5.19', + }, + 'tcl-8.6': { + 'url': SF_MIRROR+'/project/tcl/Tcl/8.6.5/tcl865-src.zip', + 'hash': 'md5:932e360acb40ec760ebeed659bc893de', + 'dir': '', + }, + 'tk-8.6': { + 'url': SF_MIRROR+'/project/tcl/Tcl/8.6.5/tk865-src.zip', + 'hash': 'md5:f2f5802a5a3b1f70b906e6930db12089', + 'dir': '', + 'version': '8.6.5', + }, + 'webp': { + 'url': 'http://downloads.webmproject.org/releases/webp/libwebp-0.5.0.tar.gz', + 'hash': 'sha1:d3de815b272fcf88fc4f2dc1ab65d176bcb8df68', + 'dir': 'libwebp-0.5.0', + }, + 'openjpeg': { + 'url': SF_MIRROR+'/project/openjpeg/openjpeg/2.1.0/openjpeg-2.1.0.tar.gz', + 'hash': 'md5:f6419fcc233df84f9a81eb36633c6db6', + 'dir': 'openjpeg-2.1.0', + }, +} + +bin_libs = { + 'openjpeg': { + 'filename': 'openjpeg-2.0.0-win32-x86.zip', + 'hash': 'sha1:xxx', + 'version': '2.0' + }, +} + +compilers = { + (7, 64): { + 'env_version': 'v7.0', + 'vc_version': '2008', + 'env_flags': '/x64 /xp', + 'inc_dir': 'msvcr90-x64', + 'platform': 'x64', + 'webp_platform': 'x64', + }, + (7, 32): { + 'env_version': 'v7.0', + 'vc_version': '2008', + 'env_flags': '/x86 /xp', + 'inc_dir': 'msvcr90-x32', + 'platform': 'Win32', + 'webp_platform': 'x86', + }, + (7.1, 64): { + 'env_version': 'v7.1', + 'vc_version': '2010', + 'env_flags': '/x64 /vista', + 'inc_dir': 'msvcr10-x64', + 'platform': 'x64', + 'webp_platform': 'x64', + }, + (7.1, 32): { + 'env_version': 'v7.1', + 'vc_version': '2010', + 'env_flags': '/x86 /vista', + 'inc_dir': 'msvcr10-x32', + 'platform': 'Win32', + 'webp_platform': 'x86', + }, +} + + +def pyversion_fromEnv(): + py = os.environ['PYTHON'] + + py_version = '27' + for k in pythons.keys(): + if k in py: + py_version = k + break + + if '64' in py: + py_version = '%s%s' % (py_version, X64_EXT) + + return py_version + + +def compiler_fromEnv(): + py = os.environ['PYTHON'] + + for k, v in pythons.items(): + if k in py: + compiler_version = v + break + + bit = 32 + if '64' in py: + bit = 64 + + return compilers[(compiler_version, bit)] diff --git a/winbuild/fetch.py b/winbuild/fetch.py new file mode 100644 index 000000000..1ab3e7208 --- /dev/null +++ b/winbuild/fetch.py @@ -0,0 +1,18 @@ +import sys +import os +import urllib.parse +import urllib.request + + +def fetch(url): + name = urllib.parse.urlsplit(url)[2].split('/')[-1] + + if not os.path.exists(name): + print("Fetching", url) + content = urllib.request.urlopen(url).read() + with open(name, 'wb') as fd: + fd.write(content) + return name + +if __name__ == '__main__': + fetch(sys.argv[1]) diff --git a/winbuild/fixproj.py b/winbuild/fixproj.py new file mode 100644 index 000000000..9b5203fb8 --- /dev/null +++ b/winbuild/fixproj.py @@ -0,0 +1,8 @@ +import sys + +with open(sys.argv[1], 'r') as fd: + content = '\n'.join(line.strip() for line in fd if line.strip()) +if len(sys.argv) == 3: + content = content.replace('Win32', sys.argv[2]).replace('x64', sys.argv[2]) +with open(sys.argv[1], 'w') as fd: + fd.write(content) diff --git a/winbuild/get_pythons.py b/winbuild/get_pythons.py new file mode 100644 index 000000000..57c866daa --- /dev/null +++ b/winbuild/get_pythons.py @@ -0,0 +1,12 @@ +from fetch import fetch +import os + +if __name__ == '__main__': + for version in ['2.6.6', '2.7.10', '3.2.5', '3.3.5', '3.4.3']: + for platform in ['', '.amd64']: + for extension in ['', '.asc']: + fetch('https://www.python.org/ftp/python/%s/python-%s%s.msi%s' + % (version, version, platform, extension)) + + # find pip, if it's not in the path! + os.system('pip install virtualenv') diff --git a/winbuild/nmake.opt b/winbuild/nmake.opt new file mode 100644 index 000000000..b155daacc --- /dev/null +++ b/winbuild/nmake.opt @@ -0,0 +1,218 @@ +# $Id: nmake.opt,v 1.18 2006/06/07 16:33:45 dron Exp $ +# +# Copyright (C) 2004, Andrey Kiselev +# +# Permission to use, copy, modify, distribute, and sell this software and +# its documentation for any purpose is hereby granted without fee, provided +# that (i) the above copyright notices and this permission notice appear in +# all copies of the software and related documentation, and (ii) the names of +# Sam Leffler and Silicon Graphics may not be used in any advertising or +# publicity relating to the software without the specific, prior written +# permission of Sam Leffler and Silicon Graphics. +# +# THE SOFTWARE IS PROVIDED "AS-IS" AND WITHOUT WARRANTY OF ANY KIND, +# EXPRESS, IMPLIED OR OTHERWISE, INCLUDING WITHOUT LIMITATION, ANY +# WARRANTY OF MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. +# +# IN NO EVENT SHALL SAM LEFFLER OR SILICON GRAPHICS BE LIABLE FOR +# ANY SPECIAL, INCIDENTAL, INDIRECT OR CONSEQUENTIAL DAMAGES OF ANY KIND, +# OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, +# WHETHER OR NOT ADVISED OF THE POSSIBILITY OF DAMAGE, AND ON ANY THEORY OF +# LIABILITY, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE +# OF THIS SOFTWARE. + +# Compile time parameters for MS Visual C++ compiler. +# You may edit this file to specify building options. + +# +###### Edit the following lines to choose a feature set you need. ####### +# + +# +# Select WINMODE_CONSOLE to build a library which reports errors to stderr, or +# WINMODE_WINDOWED to build such that errors are reported via MessageBox(). +# +WINMODE_CONSOLE = 1 +#WINMODE_WINDOWED = 1 + +# +# Comment out the following lines to disable internal codecs. +# +# Support for CCITT Group 3 & 4 algorithms +CCITT_SUPPORT = 1 +# Support for Macintosh PackBits algorithm +PACKBITS_SUPPORT = 1 +# Support for LZW algorithm +LZW_SUPPORT = 1 +# Support for ThunderScan 4-bit RLE algorithm +THUNDER_SUPPORT = 1 +# Support for NeXT 2-bit RLE algorithm +NEXT_SUPPORT = 1 +# Support for LogLuv high dynamic range encoding +LOGLUV_SUPPORT = 1 + +# +# Uncomment and edit following lines to enable JPEG support. +# +JPEG_SUPPORT = 1 +JPEGDIR = $(BUILD)\jpeg-9b +JPEG_INCLUDE = -I$(JPEGDIR) +JPEG_LIB = $(JPEGDIR)/libjpeg.lib + +# +# Uncomment and edit following lines to enable ZIP support +# (required for Deflate compression and Pixar log-format) +# +ZIP_SUPPORT = 1 +ZLIBDIR = $(BUILD)\zlib-1.2.8 +ZLIB_INCLUDE = -I$(ZLIBDIR) +ZLIB_LIB = $(ZLIBDIR)/zlib.lib + +# +# Uncomment and edit following lines to enable ISO JBIG support +# +#JBIG_SUPPORT = 1 +#JBIGDIR = d:/projects/jbigkit +#JBIG_INCLUDE = -I$(JBIGDIR)/libjbig +#JBIG_LIB = $(JBIGDIR)/libjbig/jbig.lib + +# +# Uncomment following line to enable Pixar log-format algorithm +# (Zlib required). +# +#PIXARLOG_SUPPORT = 1 + +# +# Comment out the following lines to disable strip chopping +# (whether or not to convert single-strip uncompressed images to multiple +# strips of specified size to reduce memory usage). Default strip size +# is 8192 bytes, it can be configured via the STRIP_SIZE_DEFAULT parameter +# +STRIPCHOP_SUPPORT = 1 +STRIP_SIZE_DEFAULT = 8192 + +# +# Comment out the following lines to disable treating the fourth sample with +# no EXTRASAMPLE_ value as being ASSOCALPHA. Many packages produce RGBA +# files but don't mark the alpha properly. +# +EXTRASAMPLE_AS_ALPHA_SUPPORT = 1 + +# +# Comment out the following lines to disable picking up YCbCr subsampling +# info from the JPEG data stream to support files lacking the tag. +# See Bug 168 in Bugzilla, and JPEGFixupTestSubsampling() for details. +# +CHECK_JPEG_YCBCR_SUBSAMPLING = 1 + +# +####################### Compiler related options. ####################### +# + +# +# Pick debug or optimized build flags. We default to an optimized build +# with no debugging information. +# NOTE: /EHsc option required if you want to build the C++ stream API +# +OPTFLAGS = /Ox /MD /EHsc /W3 /D_CRT_SECURE_NO_DEPRECATE +#OPTFLAGS = /Zi + +# +# Uncomment following line to enable using Windows Common RunTime Library +# instead of Windows specific system calls. See notes on top of tif_unix.c +# module for details. +# +USE_WIN_CRT_LIB = 1 + +# Compiler specific options. You may probably want to adjust compilation +# parameters in CFLAGS variable. Refer to your compiler documentation +# for the option reference. +# +MAKE = nmake /nologo +CC = cl /nologo +CXX = cl /nologo +AR = lib /nologo +LD = link /nologo + +CFLAGS = $(OPTFLAGS) $(INCL) $(EXTRAFLAGS) +CXXFLAGS = $(OPTFLAGS) $(INCL) $(EXTRAFLAGS) +EXTRAFLAGS = +LIBS = + +# Name of the output shared library +DLLNAME = libtiff.dll + +# +########### There is nothing to edit below this line normally. ########### +# + +# Set the native cpu bit order +EXTRAFLAGS = -DFILLODER_LSB2MSB $(EXTRAFLAGS) + +!IFDEF WINMODE_WINDOWED +EXTRAFLAGS = -DTIF_PLATFORM_WINDOWED $(EXTRAFLAGS) +LIBS = user32.lib $(LIBS) +!ELSE +EXTRAFLAGS = -DTIF_PLATFORM_CONSOLE $(EXTRAFLAGS) +!ENDIF + +# Codec stuff +!IFDEF CCITT_SUPPORT +EXTRAFLAGS = -DCCITT_SUPPORT $(EXTRAFLAGS) +!ENDIF + +!IFDEF PACKBITS_SUPPORT +EXTRAFLAGS = -DPACKBITS_SUPPORT $(EXTRAFLAGS) +!ENDIF + +!IFDEF LZW_SUPPORT +EXTRAFLAGS = -DLZW_SUPPORT $(EXTRAFLAGS) +!ENDIF + +!IFDEF THUNDER_SUPPORT +EXTRAFLAGS = -DTHUNDER_SUPPORT $(EXTRAFLAGS) +!ENDIF + +!IFDEF NEXT_SUPPORT +EXTRAFLAGS = -DNEXT_SUPPORT $(EXTRAFLAGS) +!ENDIF + +!IFDEF LOGLUV_SUPPORT +EXTRAFLAGS = -DLOGLUV_SUPPORT $(EXTRAFLAGS) +!ENDIF + +!IFDEF JPEG_SUPPORT +LIBS = $(LIBS) $(JPEG_LIB) +EXTRAFLAGS = -DJPEG_SUPPORT -DOJPEG_SUPPORT $(EXTRAFLAGS) +!ENDIF + +!IFDEF ZIP_SUPPORT +LIBS = $(LIBS) $(ZLIB_LIB) +EXTRAFLAGS = -DZIP_SUPPORT $(EXTRAFLAGS) +!IFDEF PIXARLOG_SUPPORT +EXTRAFLAGS = -DPIXARLOG_SUPPORT $(EXTRAFLAGS) +!ENDIF +!ENDIF + +!IFDEF JBIG_SUPPORT +LIBS = $(LIBS) $(JBIG_LIB) +EXTRAFLAGS = -DJBIG_SUPPORT $(EXTRAFLAGS) +!ENDIF + +!IFDEF STRIPCHOP_SUPPORT +EXTRAFLAGS = -DSTRIPCHOP_DEFAULT=TIFF_STRIPCHOP -DSTRIP_SIZE_DEFAULT=$(STRIP_SIZE_DEFAULT) $(EXTRAFLAGS) +!ENDIF + +!IFDEF EXTRASAMPLE_AS_ALPHA_SUPPORT +EXTRAFLAGS = -DDEFAULT_EXTRASAMPLE_AS_ALPHA $(EXTRAFLAGS) +!ENDIF + +!IFDEF CHECK_JPEG_YCBCR_SUBSAMPLING +EXTRAFLAGS = -DCHECK_JPEG_YCBCR_SUBSAMPLING $(EXTRAFLAGS) +!ENDIF + +!IFDEF USE_WIN_CRT_LIB +EXTRAFLAGS = -DAVOID_WIN32_FILEIO $(EXTRAFLAGS) +!ELSE +EXTRAFLAGS = -DUSE_WIN32_FILEIO $(EXTRAFLAGS) +!ENDIF diff --git a/winbuild/test.py b/winbuild/test.py new file mode 100644 index 000000000..84e071308 --- /dev/null +++ b/winbuild/test.py @@ -0,0 +1,46 @@ +#!/usr/bin/env python3 + +import subprocess +import os +import glob +import sys + +from config import pythons, VIRT_BASE, X64_EXT + + +def test_one(params): + python, architecture = params + try: + print("Running: %s, %s" % params) + command = [r'%s\%s%s\Scripts\python.exe' % + (VIRT_BASE, python, architecture), + 'test-installed.py', + '--processes=-0', + '--process-timeout=30', + ] + command.extend(glob.glob('Tests/test*.py')) + proc = subprocess.Popen(command, + stdin=subprocess.PIPE, + stdout=subprocess.PIPE) + (trace, stderr) = proc.communicate() + status = proc.returncode + print("Done with %s, %s -- %s" % (python, architecture, status)) + return (python, architecture, status, trace) + except Exception as msg: + print("Error with %s, %s: %s" % (python, architecture, msg)) + return (python, architecture, -1, str(msg)) + + +if __name__ == '__main__': + + os.chdir('..') + matrix = [(python, architecture) for python in pythons + for architecture in ('', X64_EXT)] + + results = map(test_one, matrix) + + for (python, architecture, status, trace) in results: + print("%s%s: %s" % (python, architecture, status and 'ERR' or 'PASS')) + + res = all(status for (python, architecture, status, trace) in results) + sys.exit(res) diff --git a/winbuild/untar.py b/winbuild/untar.py new file mode 100644 index 000000000..95b1c2254 --- /dev/null +++ b/winbuild/untar.py @@ -0,0 +1,10 @@ +import sys +import tarfile + + +def untar(src, dest): + with tarfile.open(src, 'r:gz') as tgz: + tgz.extractall(dest) + +if __name__ == '__main__': + untar(sys.argv[1], sys.argv[2]) diff --git a/winbuild/unzip.py b/winbuild/unzip.py new file mode 100644 index 000000000..92d385fa6 --- /dev/null +++ b/winbuild/unzip.py @@ -0,0 +1,10 @@ +import sys +import zipfile + + +def unzip(src, dest): + with zipfile.ZipFile(src) as zf: + zf.extractall(dest) + +if __name__ == '__main__': + unzip(sys.argv[1], sys.argv[2])