diff --git a/.landscape.yaml b/.landscape.yaml new file mode 100644 index 000000000..c869da5b4 --- /dev/null +++ b/.landscape.yaml @@ -0,0 +1,2 @@ +strictness: medium +test-warnings: yes diff --git a/.travis.yml b/.travis.yml index 7635334a0..a75c80873 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,25 +3,26 @@ language: python notifications: irc: "chat.freenode.net#pil" -env: MAX_CONCURRENCY=4 - +# Run slow PyPy* first, to give them a headstart and reduce waiting time. +# Run latest 3.x and 2.x next, to get quick compatibility results. +# Then run the remainder. python: - "pypy" - "pypy3" - - 2.6 + - 3.4 - 2.7 + - 2.6 - "2.7_with_system_site_packages" # For PyQt4 - 3.2 - 3.3 - - 3.4 install: - - "sudo apt-get -qq install libfreetype6-dev liblcms2-dev python-qt4 ghostscript libffi-dev libjpeg-turbo-progs cmake imagemagick lcov" - - "pip install cffi" - - "pip install coveralls nose coveralls-merge" - - "gem install coveralls-lcov" - - travis_retry pip install pyroma - - if [ "$TRAVIS_PYTHON_VERSION" == "2.6" ]; then pip install unittest2; fi + - "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" + + - if [ "$TRAVIS_PYTHON_VERSION" == "2.6" ]; then travis_retry pip install unittest2; fi # webp - pushd depends && ./install_webp.sh && popd @@ -39,24 +40,72 @@ script: after_success: # gather the coverage data + - travis_retry sudo apt-get -qq install lcov - lcov --capture --directory . -b . --output-file coverage.info # filter to remove system headers - - lcov --remove coverage.info '/usr/*' -o coverage.filtered.info + - lcov --remove coverage.info '/usr/*' -o coverage.filtered.info # convert to json + - travis_retry gem install coveralls-lcov - coveralls-lcov -v -n coverage.filtered.info > coverage.c.json - coverage report + - travis_retry pip install coveralls-merge - coveralls-merge coverage.c.json - - - pip install pep8 pyflakes + - travis_retry pip install pep8 pyflakes - pep8 --statistics --count PIL/*.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 + + # after_all + - | + if [ "$TRAVIS_REPO_SLUG" = "python-pillow/Pillow" ] && [ "$TRAVIS_BRANCH" = "master" ] && [ "$TRAVIS_PULL_REQUEST" = "false" ]; then + curl -Lo travis_after_all.py https://raw.github.com/dmakhno/travis_after_all/master/travis_after_all.py + python travis_after_all.py + export $(cat .to_export_back) + if [ "$BUILD_LEADER" = "YES" ]; then + if [ "$BUILD_AGGREGATE_STATUS" = "others_succeeded" ]; then + echo "All jobs succeded! Triggering OS X build..." + # Trigger an OS X build at the pillow-wheels repo + ./build_children.sh + else + echo "Some jobs failed" + fi + fi + fi + +after_failure: + - | + if [ "$TRAVIS_REPO_SLUG" = "python-pillow/Pillow" ] && [ "$TRAVIS_BRANCH" = "master" ] && [ "$TRAVIS_PULL_REQUEST" = "false" ]; then + curl -Lo travis_after_all.py https://raw.github.com/dmakhno/travis_after_all/master/travis_after_all.py + python travis_after_all.py + export $(cat .to_export_back) + if [ "$BUILD_LEADER" = "YES" ]; then + if [ "$BUILD_AGGREGATE_STATUS" = "others_failed" ]; then + echo "All jobs failed" + else + echo "Some jobs failed" + fi + fi + fi + +after_script: + - | + if [ "$TRAVIS_REPO_SLUG" = "python-pillow/Pillow" ] && [ "$TRAVIS_BRANCH" = "master" ] && [ "$TRAVIS_PULL_REQUEST" = "false" ]; then + echo leader=$BUILD_LEADER status=$BUILD_AGGREGATE_STATUS + fi + +matrix: + fast_finish: true + +env: + global: + # travis encrypt AUTH_TOKEN= + secure: "Vzm7aG1Qv0SDQcqiPzZMedNLn5ZmpL7IzF0DYnqcD+/l+zmKU22SnJBcX0uVXumo+r7eZfpsShpqfcdsZvMlvmQnwz+Y6AGKQru9tCKZbTMnuRjWKKXekC+tr8Xt9CKvRVtte5PyXW31paxUI3/e+fQGBwoFjEEC+6EpEOjeRfE=" diff --git a/CHANGES.rst b/CHANGES.rst index 47df8c176..0b92b1c24 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,10 +1,283 @@ Changelog (Pillow) ================== -2.6.0 (unreleased) +2.9.0 (Unreleased) ------------------ -- Fixed CVE-2014-3598, a DOS in the Jpeg2KImagePlugin (backport) +- Provide n_frames attribute to multi-frame formats #1261 + [anntzer, radarhere] + +- Add duration and loop set to GifImagePlugin #1172, #1269 + [radarhere] + +- Ico files are little endian #1232 + [wiredfool] + +- Upgrade olefile from 0.30 to 0.42b #1226 + [radarhere, decalage2] + +- Setting transparency value to 0 when the tRNS contains only null byte(s) #1239 + [juztin] + +- Separated out feature checking from selftest #1233 + [radarhere] + +- Style/health fixes + [radarhere] + +- Update WebP from 0.4.1 to 0.4.3 #1235 + [radarhere] + +- Release GIL during image load (decode) #1224 + [lkesteloot] + +- Added icns save #1185 + [radarhere] + +- Fix putdata memory leak #1196 + [benoit-pierre] + +- Keep user-specified ordering of icon sizes #1193 + [karimbahgat] + +- Tiff: allow writing floating point tag values #1113 + [bpedersen2] + +2.8.2 (2015-06-06) +------------------ + +- Bug fix: Fixed Tiff handling of bad EXIF data + [radarhere] + +2.8.1 (2015-04-02) +------------------ + +- Bug fix: Catch struct.error on invalid JPEG, fixes #1163 + [wiredfool, hugovk] + +2.8.0 (2015-04-01) +------------------ + +- Fix 32-bit BMP loading (RGBA or RGBX) + [artscoop] + +- Fix UnboundLocalError in ImageFile #1131 + [davarisg] + +- Re-enable test image caching + [hugovk, homm] + +- Fix: Cannot identify EPS images, fixes #1104 + [hugovk] + +- Configure setuptools to run nosetests, fixes #729 + [aclark4life] + +- Style/health fixes + [radarhere, hugovk] + +- Add support for HTTP response objects to Image.open() + [mfitzp] + +- Improve reference docs for PIL.ImageDraw.Draw.pieslice() #1145 + [audreyr] + +- Added copy method font_variant() and accessible properties to truetype() #1123 + [radarhere] + +- Fix ImagingEffectNoise #1128 + [hugovk] + +- Remove unreachable code + [hugovk] + +- Let Python do the endian stuff + tests #1121 + [amoibos, radarhere] + +- Fix webp decode memory leak #1114 + [benoit-pierre] + +- Fast path for opaque pixels in RGBa unpacker #1088 + [bgilbert] + +- Enable basic support for 'RGBa' raw encoding/decoding #1096 + [immerrr] + +- Fix pickling L mode images with no palette, #1095 + [hugovk] + +- iPython display hook #1091 + [wiredfool] + +- Adjust buffer size when quality=keep, fixes #148 (again) + [wiredfool] + +- Fix for corrupted bitmaps embedded in truetype fonts. #1072 + [jackyyf, wiredfool] + +2.7.0 (2015-01-01) +------------------ + +- Split Sane into a separate repo: https://github.com/python-pillow/Sane + [hugovk] + +- Look for OSX and Linux fonts in common places. #1054 + [charleslaw] + +- Fix CVE-2014-9601, potential PNG decompression DOS #1060 + [wiredfool] + +- Use underscores, not spaces, in TIFF tag kwargs. #1044, #1058 + [anntzer, hugovk] + +- Update PSDraw for Python3, add tests. #1055 + [hugovk] + +- Use Bicubic filtering by default for thumbnails. Don't use Jpeg Draft mode for thumbnails. #1029 + [homm] + +- Fix MSVC compiler error: Use Py_ssize_t instead of ssize_t #1051 + [cgohlke] + +- Fix compiler error: MSVC needs variables defined at the start of the block #1048 + [cgohlke] + +- The GIF Palette optimization algorithm is only applicable to mode='P' or 'L' #993 + [moriyoshi] + +- Use PySide as an alternative to PyQt4/5. + [holg] + +- Replace affine-based im.resize implementation with convolution-based im.stretch #997 + [homm] + +- Replace Gaussian Blur implementation with iterated fast box blur. #961 Note: Radius parameter is interpreted differently than before. + [homm] + +- Better docs explaining import _imaging failure #1016, build #1017, mode #1018, PyAccess, PixelAccess objects #1019 Image.quantize #1020 and Image.save #1021 + [wiredfool] + +- Fix for saving TIFF image into an io.BytesIO buffer #1011 + [mfergie] + +- Fix antialias compilation on debug versions of Python #1010 + [wiredfool] + +- Fix for Image.putdata segfault #1009 + [wiredfool] + +- Ico save, additional tests #1007 + [exherb] + +- Use PyQt4 if it has already been imported, otherwise prefer PyQt5. #1003 + [AurelienBallier] + +- Speedup resample implementation up to 2.5 times. #977 + [homm] + +- Speed up rotation by using cache aware loops, added transpose to rotations. #994 + [homm] + +- Fix Bicubic interpolation #970 + [homm] + +- Support for 4-bit greyscale TIFF images #980 + [hugovk] + +- Updated manifest #957 + [wiredfool] + +- Fix PyPy 2.4 regression #952 + [wiredfool] + +- Webp Metadata Skip Test comments #954 + [wiredfool] + +- Fixes for things rpmlint complains about #942 + [manisandro] + +2.6.2 (2015-01-01) +------------------ + +- Fix CVE-2014-9601, potential PNG decompression DOS #1060 + [wiredfool] + +- Fix Regression in PyPy 2.4 in streamio #958 + [wiredfool] + +2.6.1 (2014-10-11) +------------------ + +- Fix SciPy regression in Image.resize #945 + [wiredfool] + +- Fix manifest to include all test files. + [aclark4life] + +2.6.0 (2014-10-01) +------------------ + +- Relax precision of ImageDraw tests for x86, GimpGradient for PPC + [wiredfool] + +2.6.0-rc1 (2014-09-29) +---------------------- + +- Use redistributable image for testing #884 + [hugovk] + +- Use redistributable ICC profiles for testing, skip if not available #923 + [wiredfool] + +- Additional documentation for JPEG info and save options #890 + [wiredfool] + +- Fix JPEG Encoding memory leak when exif or qtables were specified + [wiredfool] + +- Image.tobytes() and Image.tostring() documentation update #916 #917 + [mgedmin] + +- On Windows, do not execute convert.exe without specifying path #912 + [cgohlke] + +- Fix msvc build error #911 + [cgohlke] + +- Fix for handling P + transparency -> RGBA conversions #904 + [wiredfool] + +- Retain alpha in ImageEnhance operations #909 + [wiredfool] + +- Jpeg2k Decode/encode memory leak fix #898 + [joshware, wiredfool] + +- EpsFilePlugin Speed improvements #886 + [wiredfool, karstenw] + +- Don't resize if already the right size #892 + [radarhere] + +- Fix for reading multipage TIFFs #885 + [kostrom, wiredfool] + +- Correctly handle saving gray and CMYK JPEGs with quality=keep #857 + [etienned] + +- Correct duplicate Tiff Metadata and Exif tag values + [hugovk] + +- Windows fixes #871 + [wiredfool] + +- Fix TGA files with image ID field #856 + [megabuz] + +- Fixed wrong P-mode of small, unoptimized L-mode GIF #843 + [uvNikita] + +- Fixed CVE-2014-3598, a DOS in the Jpeg2KImagePlugin [Andrew Drake] - Fixed CVE-2014-3589, a DOS in the IcnsImagePlugin @@ -49,7 +322,7 @@ Changelog (Pillow) - Added docs for ExifTags [Wintermute3] -- More tests for CurImagePlugin, DcxImagePlugin, ImageFont, ImageMath, ImagePalette, IptcImagePlugin, SpiderImagePlugin, SgiImagePlugin, XpmImagePlugin and _util +- More tests for CurImagePlugin, DcxImagePlugin, Effects.c, GimpGradientFile, ImageFont, ImageMath, ImagePalette, IptcImagePlugin, SpiderImagePlugin, SgiImagePlugin, XpmImagePlugin and _util [hugovk] - Fix return value of FreeTypeFont.textsize() does not include font offsets @@ -109,7 +382,7 @@ Changelog (Pillow) [wirefool] - Top level flake8 fixes #741 - [aclark] + [aclark4life] - Remove obsolete Animated Raster Graphics (ARG) support [hugovk] @@ -238,7 +511,7 @@ Changelog (Pillow) [larsmans] - Avoid conflicting _expand functions in PIL & MINGW, fixes #538 - [aclark] + [aclark4life] - Merge from Philippe Lagadec’s OleFileIO_PL fork [vadmium] @@ -653,13 +926,13 @@ Changelog (Pillow) [blueyed] - Package cleanup and additional documentation - [aclark] + [aclark4life] 1.7.4 (2011-07-21) ------------------ - Fix brown bag release - [aclark] + [aclark4life] 1.7.3 (2011-07-20) ------------------ @@ -671,19 +944,19 @@ Changelog (Pillow) ------------------ - Bug fix: Python 2.4 compat - [aclark] + [aclark4life] 1.7.1 (2011-05-31) ------------------ - More multi-arch support - [SteveM, regebro, barry, aclark] + [SteveM, regebro, barry, aclark4life] 1.7.0 (2011-05-27) ------------------ - Add support for multi-arch library directory /usr/lib/x86_64-linux-gnu - [aclark] + [aclark4life] 1.6 (12/01/2010) ---------------- @@ -692,28 +965,28 @@ Changelog (Pillow) [elro] - Doc fixes - [aclark] + [aclark4life] 1.5 (11/28/2010) ---------------- - Module and package fixes - [aclark] + [aclark4life] 1.4 (11/28/2010) ---------------- - Doc fixes - [aclark] + [aclark4life] 1.3 (11/28/2010) ---------------- - Add support for /lib64 and /usr/lib64 library directories on Linux - [aclark] + [aclark4life] - Doc fixes - [aclark] + [aclark4life] 1.2 (08/02/2010) ---------------- @@ -722,26 +995,29 @@ Changelog (Pillow) [jezdez] - Doc fixes - [aclark] + [aclark4life] 1.1 (07/31/2010) ---------------- - Removed setuptools_hg requirement - [aclark] + [aclark4life] - Doc fixes - [aclark] + [aclark4life] 1.0 (07/30/2010) ---------------- - Remove support for ``import Image``, etc. from the standard namespace. ``from PIL import Image`` etc. now required. - Forked PIL based on `Hanno Schlichting's re-packaging `_ - [aclark] + [aclark4life] .. Note:: What follows is the original PIL 1.1.7 CHANGES +0.2b5 - 1.1.7 (1995-2010) +------------------------- + :: -*- coding: utf-8 -*- @@ -1601,7 +1877,7 @@ Changelog (Pillow) (1.1.2c1 and 1.1.2 final released) + Adapted to Python 2.1. Among other things, all uses of the - "regex" module has been repleased with "re". + "regex" module have been replaced with "re". + Fixed attribute error when reading large PNG files (this bug was introduced in maintenance code released after the 1.1.1 @@ -2225,7 +2501,7 @@ Changelog (Pillow) the default value is 75. JPEG smooth smooth dithered images. value - is strengh (1-100). default is + is strength (1-100). default is off (0). PNG optimize minimize output file at the diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a31c9ee09..5a49ab2d7 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,25 +1,29 @@ -# Contributing +# Contributing to Pillow -## Fixes, Features and Changes +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. -Send a pull request. We'll generally want documentation and [tests](Tests/README.rst) for new features. Tests or documentation on their own are also welcomed. Feel free to ask questions as an [issue](https://github.com/python-pillow/Pillow/issues/new) or on IRC (irc.freenode.net, #pil) +## Bug fixes, feature additions, etc. -- Fork the repo -- Make a branch -- Add your changes + Tests -- Run the test suite. Try to run on both Python 2.x and 3.x, or you'll get tripped up. You can enable [Travis CI on your repo](https://travis-ci.org/profile/) to catch test failures prior to the pull request. -- Push to your fork, and make a pull request. +Please send a pull request to the master branch. Please include [documentation](http://pillow.readthedocs.org) and [tests](Tests/README.rst) for new features. Tests or documentation without bug fixes or feature additions are welcome too. Feel free to ask questions [via issues](https://github.com/python-pillow/Pillow/issues/new) or irc://irc.freenode.net#pil -A few guidelines: -- Try to keep any code commits clean and separate from reformatting commits. -- All new code is going to need tests. -- Try to follow PEP8. +- 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. +- Create a pull request to pull the changes from your branch to the Pillow master. -## Bugs +### Guidelines -When reporting bugs, please include example code that reproduces the issue, and if possible a problem image. The best reproductions are self-contained scripts that pull in as few dependencies as possible. An entire Django stack is harder to handle. +- Separate code commits from reformatting commits. +- Provide tests for any newly added code. +- Follow PEP8. + +## Reporting Issues + +When reporting issues, 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. + +### Provide details -Let us know: - What did you do? - What did you expect to happen? - What actually happened? diff --git a/docs/LICENSE b/LICENSE similarity index 100% rename from docs/LICENSE rename to LICENSE diff --git a/MANIFEST.in b/MANIFEST.in index 643e8056c..4fb47c637 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,40 +1,22 @@ - include *.c include *.h include *.md include *.py +include *.sh include *.rst include *.txt +include *.yaml include .coveragerc include .gitattributes include .travis.yml +include LICENSE include Makefile include tox.ini -recursive-include Images *.bdf -recursive-include Images *.fli -recursive-include Images *.gif -recursive-include Images *.icns -recursive-include Images *.ico -recursive-include Images *.jpg -recursive-include Images *.pbm -recursive-include Images *.pil -recursive-include Images *.png -recursive-include Images *.ppm -recursive-include Images *.psd -recursive-include Images *.tar -recursive-include Images *.webp -recursive-include Images *.xpm recursive-include PIL *.md -recursive-include Sane *.c -recursive-include Sane *.py -recursive-include Sane *.rst -recursive-include Sane *.txt -recursive-include Sane CHANGES -recursive-include Sane README recursive-include Scripts *.py recursive-include Scripts *.rst recursive-include Scripts *.sh -recursive-include Scripts README +recursive-include Scripts README.rst recursive-include Tests *.bdf recursive-include Tests *.bin recursive-include Tests *.bmp @@ -44,10 +26,11 @@ 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 *.icm +recursive-include Tests *.icc recursive-include Tests *.icns recursive-include Tests *.ico recursive-include Tests *.j2k @@ -70,15 +53,16 @@ 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 Tk *.txt recursive-include depends *.rst recursive-include depends *.sh recursive-include docs *.bat diff --git a/Makefile b/Makefile index 98e0c647a..4d96c497d 100644 --- a/Makefile +++ b/Makefile @@ -1,28 +1,5 @@ -.PHONY: pre clean install test inplace coverage test-dep help docs livedocs - -help: - @echo "Please use \`make ' where is one of" - @echo " html to make standalone HTML files" - @echo " clean remove build products" - @echo " install make and install" - @echo " test run tests on installed pillow" - @echo " inplace make inplace extension" - @echo " coverage run coverage test (in progress)" - @echo " docs make html docs" - @echo " docserver run an http server on the docs directory" - @echo " test-dep install coveraget and test dependencies" - -pre: - virtualenv . - bin/pip install -r requirements.txt - bin/python setup.py develop - bin/python selftest.py - bin/nosetests Tests/test_*.py - bin/python setup.py install - bin/python test-installed.py - check-manifest - pyroma . - viewdoc +# 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 clean: python setup.py clean @@ -30,32 +7,70 @@ clean: rm -r build || true find . -name __pycache__ | xargs rm -r || true -install: - python setup.py install - python selftest.py --installed - -test: - python test-installed.py - -inplace: clean - python setup.py build_ext --inplace - coverage: -# requires nose-cov coverage erase coverage run --parallel-mode --include=PIL/* selftest.py nosetests --with-cov --cov='PIL/' --cov-report=html Tests/test_*.py -# doesn't combine properly before report, -# writing report instead of displaying invalid report +# Doesn't combine properly before report, writing report instead of displaying invalid report. rm -r htmlcov || true coverage combine coverage report -test-dep: - pip install coveralls nose nose-cov pep8 pyflakes - -docs: +doc: $(MAKE) -C docs html -docserver: - cd docs/_build/html && python -mSimpleHTTPServer 2> /dev/null& \ No newline at end of file +docserve: + cd docs/_build/html && python -mSimpleHTTPServer 2> /dev/null& + +help: + @echo "Welcome to Pillow development. Please use \`make ' where is one of" + @echo " clean remove build products" + @echo " coverage run coverage test (in progress)" + @echo " doc make html docs" + @echo " docserve run an http server on the docs directory" + @echo " html to make standalone HTML files" + @echo " inplace make inplace extension" + @echo " install make and install" + @echo " install-req install documentation and test dependencies" + @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" + @echo " upload-test build and upload sdists to test.pythonpackages.com" + +inplace: clean + python setup.py build_ext --inplace + +install: + python setup.py install + python selftest.py --installed + +install-req: + pip install -r requirements.txt + +release-test: + $(MAKE) install-req + python setup.py develop + python selftest.py + nosetests Tests/test_*.py + python setup.py install + python test-installed.py + check-manifest + pyroma . + viewdoc + +sdist: + python setup.py sdist --format=gztar,zip + +test: + python test-installed.py + +# https://docs.python.org/2/distutils/packageindex.html#the-pypirc-file +upload-test: +# [test] +# username: +# password: +# repository = http://test.pythonpackages.com + python setup.py sdist --format=gztar,zip upload -r test + +upload: + python setup.py sdist --format=gztar,zip upload diff --git a/PIL/BdfFontFile.py b/PIL/BdfFontFile.py index 3a41848d8..0c1614e0f 100644 --- a/PIL/BdfFontFile.py +++ b/PIL/BdfFontFile.py @@ -26,12 +26,12 @@ from PIL import FontFile # -------------------------------------------------------------------- bdf_slant = { - "R": "Roman", - "I": "Italic", - "O": "Oblique", - "RI": "Reverse Italic", - "RO": "Reverse Oblique", - "OT": "Other" + "R": "Roman", + "I": "Italic", + "O": "Oblique", + "RI": "Reverse Italic", + "RO": "Reverse Oblique", + "OT": "Other" } bdf_spacing = { @@ -40,8 +40,8 @@ bdf_spacing = { "C": "Cell" } -def bdf_char(f): +def bdf_char(f): # skip to STARTCHAR while True: s = f.readline() @@ -69,8 +69,8 @@ def bdf_char(f): bitmap.append(s[:-1]) bitmap = b"".join(bitmap) - [x, y, l, d] = [int(s) for s in props["BBX"].split()] - [dx, dy] = [int(s) for s in props["DWIDTH"].split()] + [x, y, l, d] = [int(p) for p in props["BBX"].split()] + [dx, dy] = [int(p) for p in props["DWIDTH"].split()] bbox = (dx, dy), (l, -d-y, x+l, -d), (0, 0, x, y) @@ -82,6 +82,7 @@ def bdf_char(f): return id, int(props["ENCODING"]), bbox, im + ## # Font file plugin for the X11 BDF format. @@ -113,10 +114,10 @@ class BdfFontFile(FontFile.FontFile): font[4] = bdf_slant[font[4].upper()] font[11] = bdf_spacing[font[11].upper()] - ascent = int(props["FONT_ASCENT"]) - descent = int(props["FONT_DESCENT"]) + # ascent = int(props["FONT_ASCENT"]) + # descent = int(props["FONT_DESCENT"]) - fontname = ";".join(font[1:]) + # fontname = ";".join(font[1:]) # print "#", fontname # for i in comments: diff --git a/PIL/BmpImagePlugin.py b/PIL/BmpImagePlugin.py index fae6bd391..30ca10971 100644 --- a/PIL/BmpImagePlugin.py +++ b/PIL/BmpImagePlugin.py @@ -30,6 +30,7 @@ __version__ = "0.7" from PIL import Image, ImageFile, ImagePalette, _binary import math + i8 = _binary.i8 i16 = _binary.i16le i32 = _binary.i32le @@ -48,136 +49,154 @@ BIT2MODE = { 8: ("P", "P"), 16: ("RGB", "BGR;15"), 24: ("RGB", "BGR"), - 32: ("RGB", "BGRX") + 32: ("RGB", "BGRX"), } + def _accept(prefix): return prefix[:2] == b"BM" -## + +# ============================================================================== # Image plugin for the Windows BMP format. - +# ============================================================================== class BmpImageFile(ImageFile.ImageFile): + """ Image plugin for the Windows Bitmap format (BMP) """ - format = "BMP" + # -------------------------------------------------------------- Description format_description = "Windows Bitmap" + format = "BMP" + # --------------------------------------------------- BMP Compression values + COMPRESSIONS = {'RAW': 0, 'RLE8': 1, 'RLE4': 2, 'BITFIELDS': 3, 'JPEG': 4, 'PNG': 5} + RAW, RLE8, RLE4, BITFIELDS, JPEG, PNG = 0, 1, 2, 3, 4, 5 - def _bitmap(self, header = 0, offset = 0): - + def _bitmap(self, header=0, offset=0): + """ Read relevant info about the BMP """ + read, seek = self.fp.read, self.fp.seek if header: - self.fp.seek(header) - - read = self.fp.read - - # CORE/INFO - s = read(4) - s = s + ImageFile._safe_read(self.fp, i32(s)-4) - - if len(s) == 12: - - # OS/2 1.0 CORE - bits = i16(s[10:]) - self.size = i16(s[4:]), i16(s[6:]) - compression = 0 - lutsize = 3 - colors = 0 - direction = -1 - - elif len(s) in [40, 64, 108, 124]: - - # WIN 3.1 or OS/2 2.0 INFO - bits = i16(s[14:]) - self.size = i32(s[4:]), i32(s[8:]) - compression = i32(s[16:]) - pxperm = (i32(s[24:]), i32(s[28:])) # Pixels per meter - lutsize = 4 - colors = i32(s[32:]) - direction = -1 - if i8(s[11]) == 0xff: - # upside-down storage - self.size = self.size[0], 2**32 - self.size[1] - direction = 0 - - self.info["dpi"] = tuple(map(lambda x: math.ceil(x / 39.3701), pxperm)) - + seek(header) + file_info = dict() + file_info['header_size'] = i32(read(4)) # read bmp header size @offset 14 (this is part of the header size) + file_info['direction'] = -1 + # --------------------- If requested, read header at a specific position + header_data = ImageFile._safe_read(self.fp, file_info['header_size'] - 4) # read the rest of the bmp header, without its size + # --------------------------------------------------- IBM OS/2 Bitmap v1 + # ------ This format has different offsets because of width/height types + if file_info['header_size'] == 12: + file_info['width'] = i16(header_data[0:2]) + file_info['height'] = i16(header_data[2:4]) + file_info['planes'] = i16(header_data[4:6]) + file_info['bits'] = i16(header_data[6:8]) + file_info['compression'] = self.RAW + file_info['palette_padding'] = 3 + # ---------------------------------------------- Windows Bitmap v2 to v5 + elif file_info['header_size'] in (40, 64, 108, 124): # v3, OS/2 v2, v4, v5 + if file_info['header_size'] >= 40: # v3 and OS/2 + file_info['y_flip'] = i8(header_data[7]) == 0xff + file_info['direction'] = 1 if file_info['y_flip'] else -1 + file_info['width'] = i32(header_data[0:4]) + file_info['height'] = i32(header_data[4:8]) if not file_info['y_flip'] else 2**32 - i32(header_data[4:8]) + file_info['planes'] = i16(header_data[8:10]) + file_info['bits'] = i16(header_data[10:12]) + file_info['compression'] = i32(header_data[12:16]) + file_info['data_size'] = i32(header_data[16:20]) # byte size of pixel data + 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'])) + if file_info['compression'] == self.BITFIELDS: + if len(header_data) >= 52: + for idx, mask in enumerate(['r_mask', 'g_mask', 'b_mask', 'a_mask']): + file_info[mask] = i32(header_data[36+idx*4:40+idx*4]) + else: + for mask in ['r_mask', 'g_mask', 'b_mask', 'a_mask']: + file_info[mask] = i32(read(4)) + file_info['rgb_mask'] = (file_info['r_mask'], file_info['g_mask'], file_info['b_mask']) + file_info['rgba_mask'] = (file_info['r_mask'], file_info['g_mask'], file_info['b_mask'], file_info['a_mask']) else: - raise IOError("Unsupported BMP header type (%d)" % len(s)) - - if (self.size[0]*self.size[1]) > 2**31: - # Prevent DOS for > 2gb images + raise IOError("Unsupported BMP header type (%d)" % file_info['header_size']) + # ------------------ Special case : header is reported 40, which + # ---------------------- is shorter than real size for bpp >= 16 + self.size = file_info['width'], file_info['height'] + # -------- If color count was not found in the header, compute from bits + file_info['colors'] = file_info['colors'] if file_info.get('colors', 0) else (1 << file_info['bits']) + # -------------------------------- Check abnormal values for DOS attacks + if file_info['width'] * file_info['height'] > 2**31: raise IOError("Unsupported BMP Size: (%dx%d)" % self.size) - - if not colors: - colors = 1 << bits - - # MODE - try: - self.mode, rawmode = BIT2MODE[bits] - except KeyError: - raise IOError("Unsupported BMP pixel depth (%d)" % bits) - - if compression == 3: - # BI_BITFIELDS compression - mask = i32(read(4)), i32(read(4)), i32(read(4)) - if bits == 32 and mask == (0xff0000, 0x00ff00, 0x0000ff): - rawmode = "BGRX" - elif bits == 16 and mask == (0x00f800, 0x0007e0, 0x00001f): - rawmode = "BGR;16" - elif bits == 16 and mask == (0x007c00, 0x0003e0, 0x00001f): - rawmode = "BGR;15" - else: - # print bits, map(hex, mask) - raise IOError("Unsupported BMP bitfields layout") - elif compression != 0: - raise IOError("Unsupported BMP compression (%d)" % compression) - - # LUT - if self.mode == "P": - palette = [] - greyscale = 1 - if colors == 2: - indices = (0, 255) - elif colors > 2**16 or colors <=0: #We're reading a i32. - raise IOError("Unsupported BMP Palette size (%d)" % colors) - else: - indices = list(range(colors)) - for i in indices: - rgb = read(lutsize)[:3] - if rgb != o8(i)*3: - greyscale = 0 - palette.append(rgb) - if greyscale: - if colors == 2: - self.mode = rawmode = "1" + # ----------------------- Check bit depth for unusual unsupported values + self.mode, raw_mode = BIT2MODE.get(file_info['bits'], (None, None)) + if self.mode is None: + raise IOError("Unsupported BMP pixel depth (%d)" % file_info['bits']) + # ----------------- 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)]} + 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"} + 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'])] + self.mode = "RGBA" if raw_mode in ("BGRA",) else self.mode + elif file_info['bits'] in (24, 16) and file_info['rgb_mask'] in SUPPORTED[file_info['bits']]: + raw_mode = MASK_MODES[(file_info['bits'], file_info['rgb_mask'])] else: - self.mode = rawmode = "L" + raise IOError("Unsupported BMP bitfields layout") else: - self.mode = "P" - self.palette = ImagePalette.raw( - "BGR", b"".join(palette) - ) + raise IOError("Unsupported BMP bitfields layout") + elif file_info['compression'] == self.RAW: + if file_info['bits'] == 32 and header == 22: # 32-bit .cur offset + raw_mode, self.mode = "BGRA", "RGBA" + else: + raise IOError("Unsupported BMP compression (%d)" % file_info['compression']) + # ---------------- Once the header is processed, process the palette/LUT + if self.mode == "P": # Paletted for 1, 4 and 8 bit images + # ----------------------------------------------------- 1-bit images + if not (0 < file_info['colors'] <= 65536): + raise IOError("Unsupported BMP Palette size (%d)" % file_info['colors']) + else: + padding = file_info['palette_padding'] + palette = read(padding * file_info['colors']) + greyscale = True + indices = (0, 255) if file_info['colors'] == 2 else list(range(file_info['colors'])) + # ------------------ Check if greyscale and ignore palette if so + for ind, val in enumerate(indices): + rgb = palette[ind*padding:ind*padding + 3] + if rgb != o8(val) * 3: + greyscale = False + # -------- If all colors are grey, white or black, ditch palette + if greyscale: + self.mode = "1" if file_info['colors'] == 2 else "L" + raw_mode = self.mode + else: + self.mode = "P" + self.palette = ImagePalette.raw("BGRX" if padding == 4 else "BGR", palette) - if not offset: - offset = self.fp.tell() - - self.tile = [("raw", - (0, 0) + self.size, - offset, - (rawmode, ((self.size[0]*bits+31)>>3)&(~3), direction))] - - self.info["compression"] = compression + # ----------------------------- Finally set the tile data for the plugin + self.info['compression'] = file_info['compression'] + self.tile = [('raw', (0, 0, file_info['width'], file_info['height']), offset or self.fp.tell(), + (raw_mode, ((file_info['width'] * file_info['bits'] + 31) >> 3) & (~3), file_info['direction']) + )] def _open(self): - - # HEAD - s = self.fp.read(14) - if s[:2] != b"BM": + """ Open file, check magic number and read header """ + # read 14 bytes: magic number, filesize, reserved, header final offset + head_data = self.fp.read(14) + # choke if the file does not have the required magic bytes + if head_data[0:2] != b"BM": raise SyntaxError("Not a BMP file") - offset = i32(s[10:]) - + # read the start position of the BMP image data (u32) + offset = i32(head_data[10:14]) + # load bitmap information (offset=raster info) self._bitmap(offset=offset) +# ============================================================================== +# Image plugin for the DIB format (BMP alias) +# ============================================================================== class DibImageFile(BmpImageFile): format = "DIB" @@ -195,10 +214,11 @@ SAVE = { "L": ("L", 8, 256), "P": ("P", 8, 256), "RGB": ("BGR", 24, 0), + "RGBA": ("BGRA", 32, 0), } -def _save(im, fp, filename, check=0): +def _save(im, fp, filename, check=0): try: rawmode, bits, colors = SAVE[im.mode] except KeyError: @@ -214,10 +234,10 @@ def _save(im, fp, filename, check=0): # 1 meter == 39.3701 inches ppm = tuple(map(lambda x: int(x * 39.3701), dpi)) - stride = ((im.size[0]*bits+7)//8+3)&(~3) - header = 40 # or 64 for OS/2 version 2 + stride = ((im.size[0]*bits+7)//8+3) & (~3) + header = 40 # or 64 for OS/2 version 2 offset = 14 + header + colors * 4 - image = stride * im.size[1] + image = stride * im.size[1] # bitmap header fp.write(b"BM" + # file type (magic) @@ -248,7 +268,8 @@ def _save(im, fp, filename, check=0): elif im.mode == "P": fp.write(im.im.getpalette("RGB", "BGRX")) - ImageFile._save(im, fp, [("raw", (0,0)+im.size, 0, (rawmode, stride, -1))]) + ImageFile._save(im, fp, [("raw", (0, 0)+im.size, 0, + (rawmode, stride, -1))]) # # -------------------------------------------------------------------- diff --git a/PIL/BufrStubImagePlugin.py b/PIL/BufrStubImagePlugin.py index a55ae5613..45ee54776 100644 --- a/PIL/BufrStubImagePlugin.py +++ b/PIL/BufrStubImagePlugin.py @@ -13,6 +13,7 @@ from PIL import Image, ImageFile _handler = None + ## # Install application-specific BUFR image handler. # @@ -22,12 +23,14 @@ def register_handler(handler): global _handler _handler = handler + # -------------------------------------------------------------------- # Image adapter def _accept(prefix): return prefix[:4] == b"BUFR" or prefix[:4] == b"ZCZC" + class BufrStubImageFile(ImageFile.StubImageFile): format = "BUFR" @@ -53,6 +56,7 @@ class BufrStubImageFile(ImageFile.StubImageFile): def _load(self): return _handler + def _save(im, fp, filename): if _handler is None or not hasattr("_handler", "save"): raise IOError("BUFR save handler not installed") diff --git a/PIL/ContainerIO.py b/PIL/ContainerIO.py index f4a15b813..262f2afb9 100644 --- a/PIL/ContainerIO.py +++ b/PIL/ContainerIO.py @@ -18,7 +18,8 @@ # A file object that provides read access to a part of an existing # file (for example a TAR file). -class ContainerIO: + +class ContainerIO(object): ## # Create file object. @@ -48,7 +49,7 @@ class ContainerIO: # for current offset, and 2 for end of region. You cannot move # the pointer outside the defined region. - def seek(self, offset, mode = 0): + def seek(self, offset, mode=0): if mode == 1: self.pos = self.pos + offset elif mode == 2: @@ -75,12 +76,12 @@ class ContainerIO: # read until end of region. # @return An 8-bit string. - def read(self, n = 0): + def read(self, n=0): if n: n = min(n, self.length - self.pos) else: n = self.length - self.pos - if not n: # EOF + if not n: # EOF return "" self.pos = self.pos + n return self.fh.read(n) diff --git a/PIL/DcxImagePlugin.py b/PIL/DcxImagePlugin.py index 0940b3935..978c90e80 100644 --- a/PIL/DcxImagePlugin.py +++ b/PIL/DcxImagePlugin.py @@ -62,6 +62,10 @@ class DcxImageFile(PcxImageFile): self.__fp = self.fp self.seek(0) + @property + def n_frames(self): + return len(self._offset) + def seek(self, frame): if frame >= len(self._offset): raise EOFError("attempt to seek outside DCX directory") diff --git a/PIL/EpsImagePlugin.py b/PIL/EpsImagePlugin.py index 9f963f7e6..7b1f4c1ca 100644 --- a/PIL/EpsImagePlugin.py +++ b/PIL/EpsImagePlugin.py @@ -11,7 +11,8 @@ # 1996-08-23 fl Handle files from Macintosh (0.3) # 2001-02-17 fl Use 're' instead of 'regex' (Python 2.1) (0.4) # 2003-09-07 fl Check gs.close status (from Federico Di Gregorio) (0.5) -# 2014-05-07 e Handling of EPS with binary preview and fixed resolution resizing +# 2014-05-07 e Handling of EPS with binary preview and fixed resolution +# resizing # # Copyright (c) 1997-2003 by Secret Labs AB. # Copyright (c) 1995-2003 by Fredrik Lundh @@ -51,20 +52,21 @@ if sys.platform.startswith('win'): else: gs_windows_binary = False + def has_ghostscript(): if gs_windows_binary: return True if not sys.platform.startswith('win'): import subprocess try: - gs = subprocess.Popen(['gs','--version'], stdout=subprocess.PIPE) + gs = subprocess.Popen(['gs', '--version'], stdout=subprocess.PIPE) gs.stdout.read() return True except OSError: # no ghostscript pass return False - + def Ghostscript(tile, size, fp, scale=1): """Render an image using Ghostscript""" @@ -72,54 +74,64 @@ def Ghostscript(tile, size, fp, scale=1): # Unpack decoder tile decoder, tile, offset, data = tile[0] length, bbox = data - - #Hack to support hi-res rendering - scale = int(scale) or 1 - orig_size = size - orig_bbox = bbox - size = (size[0] * scale, size[1] * scale) - # resolution is dependend on bbox and size - res = ( float((72.0 * size[0]) / (bbox[2]-bbox[0])), float((72.0 * size[1]) / (bbox[3]-bbox[1])) ) - #print("Ghostscript", scale, size, orig_size, bbox, orig_bbox, res) - import tempfile, os, subprocess + # Hack to support hi-res rendering + scale = int(scale) or 1 + # orig_size = size + # orig_bbox = bbox + size = (size[0] * scale, size[1] * scale) + # resolution is dependent on bbox and size + res = (float((72.0 * size[0]) / (bbox[2]-bbox[0])), + float((72.0 * size[1]) / (bbox[3]-bbox[1]))) + # print("Ghostscript", scale, size, orig_size, bbox, orig_bbox, res) + + import os + import subprocess + import tempfile out_fd, outfile = tempfile.mkstemp() os.close(out_fd) - in_fd, infile = tempfile.mkstemp() - os.close(in_fd) - - # ignore length and offset! - # ghostscript can read it - # copy whole file to read in ghostscript - with open(infile, 'wb') as f: - # fetch length of fp - fp.seek(0, 2) - fsize = fp.tell() - # ensure start position - # go back - fp.seek(0) - lengthfile = fsize - while lengthfile > 0: - s = fp.read(min(lengthfile, 100*1024)) - if not s: - break - length -= len(s) - f.write(s) + + infile_temp = None + if hasattr(fp, 'name') and os.path.exists(fp.name): + infile = fp.name + else: + in_fd, infile_temp = tempfile.mkstemp() + os.close(in_fd) + infile = infile_temp + + # ignore length and offset! + # ghostscript can read it + # copy whole file to read in ghostscript + with open(infile_temp, 'wb') as f: + # fetch length of fp + fp.seek(0, 2) + fsize = fp.tell() + # ensure start position + # go back + fp.seek(0) + lengthfile = fsize + while lengthfile > 0: + s = fp.read(min(lengthfile, 100*1024)) + if not s: + break + lengthfile -= len(s) + f.write(s) # Build ghostscript command command = ["gs", - "-q", # quiet mode - "-g%dx%d" % size, # set output geometry (pixels) - "-r%fx%f" % res, # set input DPI (dots per inch) - "-dNOPAUSE -dSAFER", # don't pause between pages, safe mode - "-sDEVICE=ppmraw", # ppm driver - "-sOutputFile=%s" % outfile, # output file + "-q", # quiet mode + "-g%dx%d" % size, # set output geometry (pixels) + "-r%fx%f" % res, # set input DPI (dots per inch) + "-dNOPAUSE -dSAFER", # don't pause between pages, + # safe mode + "-sDEVICE=ppmraw", # ppm driver + "-sOutputFile=%s" % outfile, # output file "-c", "%d %d translate" % (-bbox[0], -bbox[1]), - # adjust for image origin - "-f", infile, # input file - ] - + # adjust for image origin + "-f", infile, # input file + ] + if gs_windows_binary is not None: if not gs_windows_binary: raise WindowsError('Unable to locate Ghostscript on paths') @@ -127,7 +139,8 @@ def Ghostscript(tile, size, fp, scale=1): # push data through ghostscript try: - gs = subprocess.Popen(command, stdin=subprocess.PIPE, stdout=subprocess.PIPE) + gs = subprocess.Popen(command, stdin=subprocess.PIPE, + stdout=subprocess.PIPE) gs.stdin.close() status = gs.wait() if status: @@ -136,48 +149,41 @@ def Ghostscript(tile, size, fp, scale=1): finally: try: os.unlink(outfile) - os.unlink(infile) - except: pass - + if infile_temp: + os.unlink(infile_temp) + except: + pass + return im -class PSFile: - """Wrapper that treats either CR or LF as end of line.""" +class PSFile(object): + """ + Wrapper for bytesio object that treats either CR or LF as end of line. + """ def __init__(self, fp): self.fp = fp self.char = None - def __getattr__(self, id): - v = getattr(self.fp, id) - setattr(self, id, v) - return v + def seek(self, offset, whence=0): self.char = None self.fp.seek(offset, whence) - def read(self, count): - return self.fp.read(count).decode('latin-1') - def readbinary(self, count): - return self.fp.read(count) - def tell(self): - pos = self.fp.tell() - if self.char: - pos -= 1 - return pos + def readline(self): - s = b"" - if self.char: - c = self.char - self.char = None - else: - c = self.fp.read(1) + s = self.char or b"" + self.char = None + + c = self.fp.read(1) while c not in b"\r\n": s = s + c c = self.fp.read(1) - if c == b"\r": - self.char = self.fp.read(1) - if self.char == b"\n": - self.char = None - return s.decode('latin-1') + "\n" + + self.char = self.fp.read(1) + # line endings can be 1 or 2 of \r \n, in either order + if self.char in b"\r\n": + self.char = None + + return s.decode('latin-1') def _accept(prefix): @@ -187,62 +193,48 @@ def _accept(prefix): # Image plugin for Encapsulated Postscript. This plugin supports only # a few variants of this format. + class EpsImageFile(ImageFile.ImageFile): """EPS File Parser for the Python Imaging Library""" format = "EPS" format_description = "Encapsulated Postscript" + mode_map = {1: "L", 2: "LAB", 3: "RGB"} + def _open(self): + (length, offset) = self._find_offset(self.fp) - fp = PSFile(self.fp) + # Rewrap the open file pointer in something that will + # convert line endings and decode to latin-1. + try: + if bytes is str: + # Python2, no encoding conversion necessary + fp = open(self.fp.name, "Ur") + else: + # Python3, can use bare open command. + fp = open(self.fp.name, "Ur", encoding='latin-1') + except: + # Expect this for bytesio/stringio + fp = PSFile(self.fp) - # FIX for: Some EPS file not handled correctly / issue #302 - # EPS can contain binary data - # or start directly with latin coding - # read header in both ways to handle both - # file types - # more info see http://partners.adobe.com/public/developer/en/ps/5002.EPSF_Spec.pdf - - # for HEAD without binary preview - s = fp.read(4) - # for HEAD with binary preview - fp.seek(0) - sb = fp.readbinary(160) - - if s[:4] == "%!PS": - fp.seek(0, 2) - length = fp.tell() - offset = 0 - elif i32(sb[0:4]) == 0xC6D3D0C5: - offset = i32(sb[4:8]) - length = i32(sb[8:12]) - else: - raise SyntaxError("not an EPS file") - - # go to offset - start of "%!PS" + # go to offset - start of "%!PS" fp.seek(offset) - + box = None self.mode = "RGB" - self.size = 1, 1 # FIXME: huh? + self.size = 1, 1 # FIXME: huh? # # Load EPS header - s = fp.readline() - - while s: + s = fp.readline().strip('\r\n') + while s: if len(s) > 255: raise SyntaxError("not an EPS file") - if s[-2:] == '\r\n': - s = s[:-2] - elif s[-1:] == '\n': - s = s[:-1] - try: m = split.match(s) except re.error as v: @@ -256,17 +248,15 @@ class EpsImageFile(ImageFile.ImageFile): # Note: The DSC spec says that BoundingBox # fields should be integers, but some drivers # put floating point values there anyway. - box = [int(float(s)) for s in v.split()] + box = [int(float(i)) for i in v.split()] self.size = box[2] - box[0], box[3] - box[1] - self.tile = [("eps", (0,0) + self.size, offset, + self.tile = [("eps", (0, 0) + self.size, offset, (length, box))] except: pass else: - m = field.match(s) - if m: k = m.group(1) @@ -276,84 +266,69 @@ class EpsImageFile(ImageFile.ImageFile): self.info[k[:8]] = k[9:] else: self.info[k] = "" - elif s[0:1] == '%': + elif s[0] == '%': # handle non-DSC Postscript comments that some # tools mistakenly put in the Comments section pass else: raise IOError("bad EPS header") - s = fp.readline() + s = fp.readline().strip('\r\n') if s[:1] != "%": break - # # Scan for an "ImageData" descriptor - while s[0] == "%": + while s[:1] == "%": if len(s) > 255: raise SyntaxError("not an EPS file") - if s[-2:] == '\r\n': - s = s[:-2] - elif s[-1:] == '\n': - s = s[:-1] - if s[:11] == "%ImageData:": + # Encoded bitmapped image. + x, y, bi, mo = s[11:].split(None, 7)[:4] - [x, y, bi, mo, z3, z4, en, id] =\ - s[11:].split(None, 7) - - x = int(x); y = int(y) - - bi = int(bi) - mo = int(mo) - - en = int(en) - - if en == 1: - decoder = "eps_binary" - elif en == 2: - decoder = "eps_hex" - else: + if int(bi) != 8: break - if bi != 8: - break - if mo == 1: - self.mode = "L" - elif mo == 2: - self.mode = "LAB" - elif mo == 3: - self.mode = "RGB" - else: + try: + self.mode = self.mode_map[int(mo)] + except: break - if id[:1] == id[-1:] == '"': - id = id[1:-1] + self.size = int(x), int(y) + return - # Scan forward to the actual image data - while True: - s = fp.readline() - if not s: - break - if s[:len(id)] == id: - self.size = x, y - self.tile2 = [(decoder, - (0, 0, x, y), - fp.tell(), - 0)] - return - - s = fp.readline() + s = fp.readline().strip('\r\n') if not s: break if not box: raise IOError("cannot determine EPS bounding box") + def _find_offset(self, fp): + + s = fp.read(160) + + if s[:4] == b"%!PS": + # for HEAD without binary preview + fp.seek(0, 2) + length = fp.tell() + offset = 0 + elif i32(s[0:4]) == 0xC6D3D0C5: + # FIX for: Some EPS file not handled correctly / issue #302 + # EPS can contain binary data + # or start directly with latin coding + # more info see: + # http://partners.adobe.com/public/developer/en/ps/5002.EPSF_Spec.pdf + offset = i32(s[4:8]) + length = i32(s[8:12]) + else: + raise SyntaxError("not an EPS file") + + return (length, offset) + def load(self, scale=1): # Load EPS via Ghostscript if not self.tile: @@ -363,11 +338,12 @@ class EpsImageFile(ImageFile.ImageFile): self.size = self.im.size self.tile = [] - def load_seek(self,*args,**kwargs): + def load_seek(self, *args, **kwargs): # we can't incrementally load, so force ImageFile.parser to - # use our custom load method by defining this method. + # use our custom load method by defining this method. pass + # # -------------------------------------------------------------------- @@ -389,11 +365,13 @@ def _save(im, fp, filename, eps=1): else: raise ValueError("image mode is not supported") - class NoCloseStream: + class NoCloseStream(object): def __init__(self, fp): self.fp = fp + def __getattr__(self, name): return getattr(self.fp, name) + def close(self): pass @@ -407,7 +385,7 @@ def _save(im, fp, filename, eps=1): # write EPS header fp.write("%!PS-Adobe-3.0 EPSF-3.0\n") fp.write("%%Creator: PIL 0.1 EpsEncode\n") - #fp.write("%%CreationDate: %s"...) + # fp.write("%%CreationDate: %s"...) fp.write("%%%%BoundingBox: 0 0 %d %d\n" % im.size) fp.write("%%Pages: 1\n") fp.write("%%EndComments\n") @@ -421,13 +399,13 @@ def _save(im, fp, filename, eps=1): fp.write("10 dict begin\n") fp.write("/buf %d string def\n" % (im.size[0] * operator[1])) fp.write("%d %d scale\n" % im.size) - fp.write("%d %d 8\n" % im.size) # <= bits + fp.write("%d %d 8\n" % im.size) # <= bits 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() - ImageFile._save(im, base_fp, [("eps", (0,0)+im.size, 0, None)]) + ImageFile._save(im, base_fp, [("eps", (0, 0)+im.size, 0, None)]) fp.write("\n%%%%EndBinary\n") fp.write("grestore end\n") diff --git a/PIL/ExifTags.py b/PIL/ExifTags.py index 25cd08068..52e145f62 100644 --- a/PIL/ExifTags.py +++ b/PIL/ExifTags.py @@ -67,8 +67,8 @@ TAGS = { 0x0213: "YCbCrPositioning", 0x0214: "ReferenceBlackWhite", 0x1000: "RelatedImageFileFormat", - 0x1001: "RelatedImageLength", # FIXME / Dictionary contains duplicate keys - 0x1001: "RelatedImageWidth", # FIXME \ Dictionary contains duplicate keys + 0x1001: "RelatedImageWidth", + 0x1002: "RelatedImageLength", 0x828d: "CFARepeatPatternDim", 0x828e: "CFAPattern", 0x828f: "BatteryLevel", diff --git a/PIL/FitsStubImagePlugin.py b/PIL/FitsStubImagePlugin.py index 0b851ae59..7aefff212 100644 --- a/PIL/FitsStubImagePlugin.py +++ b/PIL/FitsStubImagePlugin.py @@ -18,6 +18,7 @@ _handler = None # # @param handler Handler object. + def register_handler(handler): global _handler _handler = handler @@ -25,9 +26,11 @@ def register_handler(handler): # -------------------------------------------------------------------- # Image adapter + def _accept(prefix): return prefix[:6] == b"SIMPLE" + class FITSStubImageFile(ImageFile.StubImageFile): format = "FITS" diff --git a/PIL/FliImagePlugin.py b/PIL/FliImagePlugin.py index c9a29051e..0660ddeb6 100644 --- a/PIL/FliImagePlugin.py +++ b/PIL/FliImagePlugin.py @@ -25,12 +25,14 @@ i16 = _binary.i16le i32 = _binary.i32le o8 = _binary.o8 + # # decoder def _accept(prefix): return i16(prefix[4:6]) in [0xAF11, 0xAF12] + ## # Image plugin for the FLI/FLC animation format. Use the seek # method to load individual frames. @@ -47,7 +49,7 @@ class FliImageFile(ImageFile.ImageFile): magic = i16(s[4:6]) if not (magic in [0xAF11, 0xAF12] and i16(s[14:16]) in [0, 3] and # flags - s[20:22] == b"\x00\x00"): # reserved + s[20:22] == b"\x00\x00"): # reserved raise SyntaxError("not an FLI/FLC file") # image characteristics @@ -61,7 +63,7 @@ class FliImageFile(ImageFile.ImageFile): self.info["duration"] = duration # look for palette - palette = [(a,a,a) for a in range(256)] + palette = [(a, a, a) for a in range(256)] s = self.fp.read(16) @@ -80,13 +82,14 @@ class FliImageFile(ImageFile.ImageFile): elif i16(s[4:6]) == 4: self._palette(palette, 0) - palette = [o8(r)+o8(g)+o8(b) for (r,g,b) in palette] + palette = [o8(r)+o8(g)+o8(b) for (r, g, b) in palette] self.palette = ImagePalette.raw("RGB", b"".join(palette)) # set things up to decode first frame - self.frame = -1 + self.__frame = -1 self.__fp = self.fp - + self.__rewind = self.fp.tell() + self._n_frames = None self.seek(0) def _palette(self, palette, shift): @@ -107,11 +110,35 @@ class FliImageFile(ImageFile.ImageFile): palette[i] = (r, g, b) i += 1 - def seek(self, frame): + @property + def n_frames(self): + if self._n_frames is None: + current = self.tell() + try: + while True: + self.seek(self.tell() + 1) + except EOFError: + self._n_frames = self.tell() + 1 + self.seek(current) + return self._n_frames - if frame != self.frame + 1: + def seek(self, frame): + if frame == self.__frame: + return + if frame < self.__frame: + self._seek(0) + for f in range(self.__frame + 1, frame + 1): + self._seek(f) + + def _seek(self, frame): + if frame == 0: + self.__frame = -1 + self.__fp.seek(self.__rewind) + self.__offset = 128 + + if frame != self.__frame + 1: raise ValueError("cannot seek to frame %d" % frame) - self.frame = frame + self.__frame = frame # move to next frame self.fp = self.__fp @@ -124,13 +151,12 @@ class FliImageFile(ImageFile.ImageFile): framesize = i32(s) self.decodermaxblock = framesize - self.tile = [("fli", (0,0)+self.size, self.__offset, None)] + self.tile = [("fli", (0, 0)+self.size, self.__offset, None)] - self.__offset = self.__offset + framesize + self.__offset += framesize def tell(self): - - return self.frame + return self.__frame # # registry diff --git a/PIL/FontFile.py b/PIL/FontFile.py index 26ddff0ac..db8e6bec1 100644 --- a/PIL/FontFile.py +++ b/PIL/FontFile.py @@ -17,11 +17,6 @@ import os from PIL import Image, _binary -try: - import zlib -except ImportError: - zlib = None - WIDTH = 800 @@ -36,7 +31,7 @@ def puti16(fp, values): ## # Base class for raster font file handlers. -class FontFile: +class FontFile(object): bitmap = None @@ -83,7 +78,8 @@ class FontFile: glyph = self[i] if glyph: d, dst, src, im = glyph - xx, yy = src[2] - src[0], src[3] - src[1] + xx = src[2] - src[0] + # yy = src[3] - src[1] x0, y0 = x, y x = x + xx if x > WIDTH: diff --git a/PIL/FpxImagePlugin.py b/PIL/FpxImagePlugin.py index 64c7b1568..9d338d9da 100644 --- a/PIL/FpxImagePlugin.py +++ b/PIL/FpxImagePlugin.py @@ -20,7 +20,7 @@ __version__ = "0.1" from PIL import Image, ImageFile -from PIL.OleFileIO import * +from PIL.OleFileIO import i8, i32, MAGIC, OleFileIO # we map from colour field tuples to (mode, rawmode) descriptors @@ -34,16 +34,18 @@ MODES = { (0x00020000, 0x00020001, 0x00020002): ("RGB", "YCC;P"), (0x00028000, 0x00028001, 0x00028002, 0x00027ffe): ("RGBA", "YCCA;P"), # standard RGB (NIFRGB) - (0x00030000, 0x00030001, 0x00030002): ("RGB","RGB"), - (0x00038000, 0x00038001, 0x00038002, 0x00037ffe): ("RGBA","RGBA"), + (0x00030000, 0x00030001, 0x00030002): ("RGB", "RGB"), + (0x00038000, 0x00038001, 0x00038002, 0x00037ffe): ("RGBA", "RGBA"), } + # # -------------------------------------------------------------------- def _accept(prefix): return prefix[:8] == MAGIC + ## # Image plugin for the FlashPix images. @@ -67,7 +69,7 @@ class FpxImageFile(ImageFile.ImageFile): self._open_index(1) - def _open_index(self, index = 1): + def _open_index(self, index=1): # # get the Image Contents Property Set @@ -95,7 +97,7 @@ class FpxImageFile(ImageFile.ImageFile): id = self.maxid << 16 - s = prop[0x2000002|id] + s = prop[0x2000002 | id] colors = [] for i in range(i32(s, 4)): @@ -107,7 +109,7 @@ class FpxImageFile(ImageFile.ImageFile): # load JPEG tables, if any self.jpeg = {} for i in range(256): - id = 0x3000001|(i << 16) + id = 0x3000001 | (i << 16) if id in prop: self.jpeg[i] = prop[id] @@ -115,7 +117,7 @@ class FpxImageFile(ImageFile.ImageFile): self._open_subimage(1, self.maxid) - def _open_subimage(self, index = 1, subimage = 0): + def _open_subimage(self, index=1, subimage=0): # # setup tile descriptors for a given subimage @@ -128,15 +130,15 @@ class FpxImageFile(ImageFile.ImageFile): fp = self.ole.openstream(stream) # skip prefix - p = fp.read(28) + fp.read(28) # header stream s = fp.read(36) size = i32(s, 4), i32(s, 8) - tilecount = i32(s, 12) + # tilecount = i32(s, 12) tilesize = i32(s, 16), i32(s, 20) - channels = i32(s, 24) + # channels = i32(s, 24) offset = i32(s, 28) length = i32(s, 32) @@ -159,14 +161,14 @@ class FpxImageFile(ImageFile.ImageFile): compression = i32(s, i+8) if compression == 0: - self.tile.append(("raw", (x,y,x+xtile,y+ytile), - i32(s, i) + 28, (self.rawmode))) + self.tile.append(("raw", (x, y, x+xtile, y+ytile), + i32(s, i) + 28, (self.rawmode))) elif compression == 1: # FIXME: the fill decoder is not implemented - self.tile.append(("fill", (x,y,x+xtile,y+ytile), - i32(s, i) + 28, (self.rawmode, s[12:16]))) + self.tile.append(("fill", (x, y, x+xtile, y+ytile), + i32(s, i) + 28, (self.rawmode, s[12:16]))) elif compression == 2: @@ -182,14 +184,14 @@ class FpxImageFile(ImageFile.ImageFile): # this problem : jpegmode, rawmode = "YCbCrK", "CMYK" else: - jpegmode = None # let the decoder decide + jpegmode = None # let the decoder decide else: # The image is stored as defined by rawmode jpegmode = rawmode - self.tile.append(("jpeg", (x,y,x+xtile,y+ytile), - i32(s, i) + 28, (rawmode, jpegmode))) + self.tile.append(("jpeg", (x, y, x+xtile, y+ytile), + i32(s, i) + 28, (rawmode, jpegmode))) # FIXME: jpeg tables are tile dependent; the prefix # data must be placed in the tile descriptor itself! @@ -204,7 +206,7 @@ class FpxImageFile(ImageFile.ImageFile): if x >= xsize: x, y = 0, y + ytile if y >= ysize: - break # isn't really required + break # isn't really required self.stream = stream self.fp = None @@ -212,7 +214,8 @@ class FpxImageFile(ImageFile.ImageFile): def load(self): if not self.fp: - self.fp = self.ole.openstream(self.stream[:2] + ["Subimage 0000 Data"]) + self.fp = self.ole.openstream(self.stream[:2] + + ["Subimage 0000 Data"]) ImageFile.ImageFile.load(self) diff --git a/PIL/GbrImagePlugin.py b/PIL/GbrImagePlugin.py index e2a5d0c9c..e1580e718 100644 --- a/PIL/GbrImagePlugin.py +++ b/PIL/GbrImagePlugin.py @@ -17,9 +17,11 @@ from PIL import Image, ImageFile, _binary i32 = _binary.i32be + def _accept(prefix): return i32(prefix) >= 20 and i32(prefix[4:8]) == 1 + ## # Image plugin for the GIMP brush format. @@ -37,8 +39,8 @@ class GbrImageFile(ImageFile.ImageFile): width = i32(self.fp.read(4)) height = i32(self.fp.read(4)) - bytes = i32(self.fp.read(4)) - if width <= 0 or height <= 0 or bytes != 1: + color_depth = i32(self.fp.read(4)) + if width <= 0 or height <= 0 or color_depth != 1: raise SyntaxError("not a GIMP brush") comment = self.fp.read(header_size - 20)[:-1] diff --git a/PIL/GdImageFile.py b/PIL/GdImageFile.py index f1dbc7c62..080153a9f 100644 --- a/PIL/GdImageFile.py +++ b/PIL/GdImageFile.py @@ -36,6 +36,7 @@ except ImportError: i16 = _binary.i16be + ## # Image plugin for the GD uncompressed format. Note that this format # is not supported by the standard Image.open function. To use @@ -52,7 +53,7 @@ class GdImageFile(ImageFile.ImageFile): # Header s = self.fp.read(775) - self.mode = "L" # FIXME: "P" + self.mode = "L" # FIXME: "P" self.size = i16(s[0:2]), i16(s[2:4]) # transparency index @@ -62,7 +63,8 @@ class GdImageFile(ImageFile.ImageFile): self.palette = ImagePalette.raw("RGB", s[7:]) - self.tile = [("raw", (0,0)+self.size, 775, ("L", 0, -1))] + self.tile = [("raw", (0, 0)+self.size, 775, ("L", 0, -1))] + ## # Load texture from a GD image file. @@ -73,7 +75,7 @@ class GdImageFile(ImageFile.ImageFile): # @return An image instance. # @exception IOError If the image could not be read. -def open(fp, mode = "r"): +def open(fp, mode="r"): if mode != "r": raise ValueError("bad mode") diff --git a/PIL/GifImagePlugin.py b/PIL/GifImagePlugin.py index 640af9efc..18fcb9b21 100644 --- a/PIL/GifImagePlugin.py +++ b/PIL/GifImagePlugin.py @@ -24,13 +24,11 @@ # See the README file for information on usage and redistribution. # +from PIL import Image, ImageFile, ImagePalette, _binary __version__ = "0.9" -from PIL import Image, ImageFile, ImagePalette, _binary - - # -------------------------------------------------------------------- # Helpers @@ -46,6 +44,7 @@ o16 = _binary.o16le def _accept(prefix): return prefix[:6] in [b"GIF87a", b"GIF89a"] + ## # Image plugin for GIF images. This plugin supports both GIF87 and # GIF89 images. @@ -79,24 +78,45 @@ class GifImageFile(ImageFile.ImageFile): # get global palette self.info["background"] = i8(s[11]) # check if palette contains colour indices - p = self.fp.read(3< 0: - paletteBytes += o8(0) * 3 * actualTargetSizeDiff + actual_target_size_diff = (2 << color_table_size) - len(palette_bytes)//3 + if actual_target_size_diff > 0: + palette_bytes += o8(0) * 3 * actual_target_size_diff # Header + Logical Screen Descriptor + Global Color Table - header.append(paletteBytes) - return header, usedPaletteColors + header.append(palette_bytes) + return header, used_palette_colors -def getdata(im, offset = (0, 0), **params): +def getdata(im, offset=(0, 0), **params): """Return a list of strings representing this image. The first string is a local image header, the rest contains encoded image data.""" - class collector: + class Collector(object): data = [] + def write(self, data): self.data.append(data) - im.load() # make sure raster data is available + im.load() # make sure raster data is available - fp = collector() + fp = Collector() try: im.encoderinfo = params # local image header - fp.write(b"," + - o16(offset[0]) + # offset - o16(offset[1]) + - o16(im.size[0]) + # size - o16(im.size[1]) + - o8(0) + # flags - o8(8)) # bits + get_local_header(fp, im, offset, 0) - ImageFile._save(im, fp, [("gif", (0,0)+im.size, 0, RAWMODE[im.mode])]) + ImageFile._save(im, fp, [("gif", (0, 0)+im.size, 0, RAWMODE[im.mode])]) - fp.write(b"\0") # end of image data + fp.write(b"\0") # end of image data finally: del im.encoderinfo diff --git a/PIL/GimpGradientFile.py b/PIL/GimpGradientFile.py index 7c88addae..45af573bb 100644 --- a/PIL/GimpGradientFile.py +++ b/PIL/GimpGradientFile.py @@ -24,6 +24,7 @@ from PIL._binary import o8 EPSILON = 1e-10 + def linear(middle, pos): if pos <= middle: if middle < EPSILON: @@ -38,25 +39,30 @@ def linear(middle, pos): else: return 0.5 + 0.5 * pos / middle + def curved(middle, pos): return pos ** (log(0.5) / log(max(middle, EPSILON))) + def sine(middle, pos): return (sin((-pi / 2.0) + pi * linear(middle, pos)) + 1.0) / 2.0 + def sphere_increasing(middle, pos): return sqrt(1.0 - (linear(middle, pos) - 1.0) ** 2) + def sphere_decreasing(middle, pos): return 1.0 - sqrt(1.0 - linear(middle, pos) ** 2) -SEGMENTS = [ linear, curved, sine, sphere_increasing, sphere_decreasing ] +SEGMENTS = [linear, curved, sine, sphere_increasing, sphere_decreasing] -class GradientFile: + +class GradientFile(object): gradient = None - def getpalette(self, entries = 256): + def getpalette(self, entries=256): palette = [] @@ -89,6 +95,7 @@ class GradientFile: return b"".join(palette), "RGBA" + ## # File handler for GIMP's gradient format. @@ -99,7 +106,13 @@ class GimpGradientFile(GradientFile): if fp.readline()[:13] != b"GIMP Gradient": raise SyntaxError("not a GIMP gradient file") - count = int(fp.readline()) + line = fp.readline() + + # GIMP 1.2 gradient files don't contain a name, but GIMP 1.3 files do + if line.startswith(b"Name: "): + line = fp.readline().strip() + + count = int(line) gradient = [] @@ -108,13 +121,13 @@ class GimpGradientFile(GradientFile): s = fp.readline().split() w = [float(x) for x in s[:11]] - x0, x1 = w[0], w[2] - xm = w[1] - rgb0 = w[3:7] - rgb1 = w[7:11] + x0, x1 = w[0], w[2] + xm = w[1] + rgb0 = w[3:7] + rgb1 = w[7:11] segment = SEGMENTS[int(s[11])] - cspace = int(s[12]) + cspace = int(s[12]) if cspace != 0: raise IOError("cannot handle HSV colour space") diff --git a/PIL/GimpPaletteFile.py b/PIL/GimpPaletteFile.py index 6f71ec678..4bf3ca36a 100644 --- a/PIL/GimpPaletteFile.py +++ b/PIL/GimpPaletteFile.py @@ -17,10 +17,11 @@ import re from PIL._binary import o8 + ## # File handler for GIMP's palette format. -class GimpPaletteFile: +class GimpPaletteFile(object): rawmode = "RGB" @@ -56,7 +57,6 @@ class GimpPaletteFile: self.palette = b"".join(self.palette) - def getpalette(self): return self.palette, self.rawmode diff --git a/PIL/GribStubImagePlugin.py b/PIL/GribStubImagePlugin.py index d76585c99..8ffad8100 100644 --- a/PIL/GribStubImagePlugin.py +++ b/PIL/GribStubImagePlugin.py @@ -13,6 +13,7 @@ from PIL import Image, ImageFile _handler = None + ## # Install application-specific GRIB image handler. # @@ -22,12 +23,14 @@ def register_handler(handler): global _handler _handler = handler + # -------------------------------------------------------------------- # Image adapter def _accept(prefix): return prefix[0:4] == b"GRIB" and prefix[7] == b'\x01' + class GribStubImageFile(ImageFile.StubImageFile): format = "GRIB" @@ -53,6 +56,7 @@ class GribStubImageFile(ImageFile.StubImageFile): def _load(self): return _handler + def _save(im, fp, filename): if _handler is None or not hasattr("_handler", "save"): raise IOError("GRIB save handler not installed") diff --git a/PIL/Hdf5StubImagePlugin.py b/PIL/Hdf5StubImagePlugin.py index eb888d8c3..f7945be7e 100644 --- a/PIL/Hdf5StubImagePlugin.py +++ b/PIL/Hdf5StubImagePlugin.py @@ -13,6 +13,7 @@ from PIL import Image, ImageFile _handler = None + ## # Install application-specific HDF5 image handler. # @@ -22,12 +23,14 @@ def register_handler(handler): global _handler _handler = handler + # -------------------------------------------------------------------- # Image adapter def _accept(prefix): return prefix[:8] == b"\x89HDF\r\n\x1a\n" + class HDF5StubImageFile(ImageFile.StubImageFile): format = "HDF5" diff --git a/PIL/IcnsImagePlugin.py b/PIL/IcnsImagePlugin.py index ca7a14931..e0b130e81 100644 --- a/PIL/IcnsImagePlugin.py +++ b/PIL/IcnsImagePlugin.py @@ -16,7 +16,12 @@ # from PIL import Image, ImageFile, PngImagePlugin, _binary -import struct, io +import io +import os +import shutil +import struct +import sys +import tempfile enable_jpeg2k = hasattr(Image.core, 'jp2klib_version') if enable_jpeg2k: @@ -26,9 +31,11 @@ i8 = _binary.i8 HEADERSIZE = 8 + def nextheader(fobj): return struct.unpack('>4sI', fobj.read(HEADERSIZE)) + def read_32t(fobj, start_length, size): # The 128x128 icon seems to have an extra header for some reason. (start, length) = start_length @@ -38,6 +45,7 @@ def read_32t(fobj, start_length, size): raise SyntaxError('Unknown signature, expecting 0x00000000') return read_32(fobj, (start + 4, length - 4), size) + def read_32(fobj, start_length, size): """ Read a 32bit RGB icon resource. Seems to be either uncompressed or @@ -83,9 +91,10 @@ def read_32(fobj, start_length, size): im.im.putband(band.im, band_ix) return {"RGB": im} + def read_mk(fobj, start_length, size): # Alpha masks seem to be uncompressed - (start, length) = start_length + start = start_length[0] fobj.seek(start) pixel_size = (size[0] * size[2], size[1] * size[2]) sizesq = pixel_size[0] * pixel_size[1] @@ -94,6 +103,7 @@ def read_mk(fobj, start_length, size): ) return {"A": band} + def read_png_or_jpeg2000(fobj, start_length, size): (start, length) = start_length fobj.seek(start) @@ -103,10 +113,11 @@ def read_png_or_jpeg2000(fobj, start_length, size): im = PngImagePlugin.PngImageFile(fobj) return {"RGBA": im} elif sig[:4] == b'\xff\x4f\xff\x51' \ - or sig[:4] == b'\x0d\x0a\x87\x0a' \ - or sig == b'\x00\x00\x00\x0cjP \x0d\x0a\x87\x0a': + or sig[:4] == b'\x0d\x0a\x87\x0a' \ + or sig == b'\x00\x00\x00\x0cjP \x0d\x0a\x87\x0a': if not enable_jpeg2k: - raise ValueError('Unsupported icon subimage format (rebuild PIL with JPEG 2000 support to fix this)') + raise ValueError('Unsupported icon subimage format (rebuild PIL ' + 'with JPEG 2000 support to fix this)') # j2k, jpc or j2c fobj.seek(start) jp2kstream = fobj.read(length) @@ -118,7 +129,8 @@ def read_png_or_jpeg2000(fobj, start_length, size): else: raise ValueError('Unsupported icon subimage format') -class IcnsFile: + +class IcnsFile(object): SIZES = { (512, 512, 2): [ @@ -225,7 +237,7 @@ class IcnsFile: im = channels.get('RGBA', None) if im: return im - + im = channels.get("RGB").copy() try: im.putalpha(channels["A"]) @@ -233,12 +245,13 @@ class IcnsFile: pass return im + ## # Image plugin for Mac OS icons. class IcnsImageFile(ImageFile.ImageFile): """ - PIL read-only image support for Mac OS .icns files. + PIL image support for Mac OS .icns files. Chooses the best resolution, but will possibly load a different size image if you mutate the size attribute before calling 'load'. @@ -275,7 +288,7 @@ class IcnsImageFile(ImageFile.ImageFile): # If this is a PNG or JPEG 2000, it won't be loaded yet im.load() - + self.im = im.im self.mode = im.mode self.size = im.size @@ -284,11 +297,64 @@ class IcnsImageFile(ImageFile.ImageFile): self.tile = () self.load_end() + +def _save(im, fp, filename): + """ + Saves the image as a series of PNG files, + that are then converted to a .icns file + using the OS X command line utility 'iconutil'. + + OS X only. + """ + try: + fp.flush() + except: + pass + + # create the temporary set of pngs + iconset = tempfile.mkdtemp('.iconset') + last_w = None + last_im = None + for w in [16, 32, 128, 256, 512]: + prefix = 'icon_{}x{}'.format(w, w) + + if last_w == w: + im_scaled = last_im + else: + im_scaled = im.resize((w, w), Image.LANCZOS) + im_scaled.save(os.path.join(iconset, prefix+'.png')) + + im_scaled = im.resize((w*2, w*2), Image.LANCZOS) + im_scaled.save(os.path.join(iconset, prefix+'@2x.png')) + last_im = im_scaled + + # iconutil -c icns -o {} {} + from subprocess import Popen, PIPE, CalledProcessError + + convert_cmd = ["iconutil", "-c", "icns", "-o", filename, iconset] + stderr = tempfile.TemporaryFile() + convert_proc = Popen(convert_cmd, stdout=PIPE, stderr=stderr) + + convert_proc.stdout.close() + + retcode = convert_proc.wait() + + # remove the temporary files + shutil.rmtree(iconset) + + if retcode: + raise CalledProcessError(retcode, convert_cmd) + Image.register_open("ICNS", IcnsImageFile, lambda x: x[:4] == b'icns') Image.register_extension("ICNS", '.icns') +if sys.platform == 'darwin': + Image.register_save("ICNS", _save) + + Image.register_mime("ICNS", "image/icns") + + if __name__ == '__main__': - import os, sys imf = IcnsImageFile(open(sys.argv[1], 'rb')) for size in imf.info['sizes']: imf.size = size diff --git a/PIL/IcoImagePlugin.py b/PIL/IcoImagePlugin.py index 268e93d6c..778edaf9a 100644 --- a/PIL/IcoImagePlugin.py +++ b/PIL/IcoImagePlugin.py @@ -13,7 +13,8 @@ # See the README file for information on usage and redistribution. # -# This plugin is a refactored version of Win32IconImagePlugin by Bryan Davis . +# This plugin is a refactored version of Win32IconImagePlugin by Bryan Davis +# . # https://code.google.com/p/casadebender/wiki/Win32IconImagePlugin # # Icon format references: @@ -23,6 +24,9 @@ __version__ = "0.1" +import struct +from io import BytesIO + from PIL import Image, ImageFile, BmpImagePlugin, PngImagePlugin, _binary from math import log, ceil @@ -35,11 +39,47 @@ i32 = _binary.i32le _MAGIC = b"\0\0\1\0" + +def _save(im, fp, filename): + fp.write(_MAGIC) # (2+2) + sizes = im.encoderinfo.get("sizes", + [(16, 16), (24, 24), (32, 32), (48, 48), + (64, 64), (128, 128), (255, 255)]) + width, height = im.size + filter(lambda x: False if (x[0] > width or x[1] > height or + x[0] > 255 or x[1] > 255) else True, sizes) + fp.write(struct.pack("=8bpp) + 'nb_color': i8(s[2]), # No. of colors in image (0 if >=8bpp) 'reserved': i8(s[3]), 'planes': i16(s[4:]), 'bpp': i16(s[6:]), @@ -78,10 +118,14 @@ class IcoFile: # See Wikipedia notes about color depth. # We need this just to differ images with equal sizes - icon_header['color_depth'] = (icon_header['bpp'] or (icon_header['nb_color'] != 0 and ceil(log(icon_header['nb_color'],2))) or 256) + icon_header['color_depth'] = (icon_header['bpp'] or + (icon_header['nb_color'] != 0 and + ceil(log(icon_header['nb_color'], + 2))) or 256) icon_header['dim'] = (icon_header['width'], icon_header['height']) - icon_header['square'] = icon_header['width'] * icon_header['height'] + icon_header['square'] = (icon_header['width'] * + icon_header['height']) self.entry.append(icon_header) @@ -102,7 +146,7 @@ class IcoFile: Get an image from the icon """ for (i, h) in enumerate(self.entry): - if size == h['dim'] and (bpp == False or bpp == h['color_depth']): + if size == h['dim'] and (bpp is False or bpp == h['color_depth']): return self.frame(i) return self.frame(0) @@ -127,7 +171,7 @@ class IcoFile: # change tile dimension to only encompass XOR image im.size = (im.size[0], int(im.size[1] / 2)) d, e, o, a = im.tile[0] - im.tile[0] = d, (0,0) + im.size, o, a + im.tile[0] = d, (0, 0) + im.size, o, a # figure out where AND mask image starts mode = a[0] @@ -139,8 +183,9 @@ class IcoFile: if 32 == bpp: # 32-bit color depth icon image allows semitransparent areas - # PIL's DIB format ignores transparency bits, recover them - # The DIB is packed in BGRX byte order where X is the alpha channel + # PIL's DIB format ignores transparency bits, recover them. + # The DIB is packed in BGRX byte order where X is the alpha + # channel. # Back up to start of bmp data self.buf.seek(o) @@ -162,9 +207,11 @@ class IcoFile: # bitmap row data is aligned to word boundaries w += 32 - (im.size[0] % 32) - # the total mask data is padded row size * height / bits per char + # the total mask data is + # padded row size * height / bits per char - and_mask_offset = o + int(im.size[0] * im.size[1] * (bpp / 8.0)) + and_mask_offset = o + int(im.size[0] * im.size[1] * + (bpp / 8.0)) total_bytes = int((w * im.size[1]) / 8) self.buf.seek(and_mask_offset) @@ -187,6 +234,7 @@ class IcoFile: return im + ## # Image plugin for Windows Icon files. @@ -194,15 +242,16 @@ class IcoImageFile(ImageFile.ImageFile): """ PIL read-only image support for Microsoft Windows .ico files. - By default the largest resolution image in the file will be loaded. This can - be changed by altering the 'size' attribute before calling 'load'. + By default the largest resolution image in the file will be loaded. This + can be changed by altering the 'size' attribute before calling 'load'. The info dictionary has a key 'sizes' that is a list of the sizes available in the icon file. Handles classic, XP and Vista icon formats. - This plugin is a refactored version of Win32IconImagePlugin by Bryan Davis . + This plugin is a refactored version of Win32IconImagePlugin by Bryan Davis + . https://code.google.com/p/casadebender/wiki/Win32IconImagePlugin """ format = "ICO" @@ -222,12 +271,13 @@ class IcoImageFile(ImageFile.ImageFile): self.mode = im.mode self.size = im.size - def load_seek(self): - # Flage the ImageFile.Parser so that it just does all the decode at the end. + # Flag the ImageFile.Parser so that it + # just does all the decode at the end. pass # # -------------------------------------------------------------------- Image.register_open("ICO", IcoImageFile, _accept) +Image.register_save("ICO", _save) Image.register_extension("ICO", ".ico") diff --git a/PIL/ImImagePlugin.py b/PIL/ImImagePlugin.py index a5eeef76a..589928d0e 100644 --- a/PIL/ImImagePlugin.py +++ b/PIL/ImImagePlugin.py @@ -30,7 +30,7 @@ __version__ = "0.7" import re from PIL import Image, ImageFile, ImagePalette -from PIL._binary import i8, o8 +from PIL._binary import i8 # -------------------------------------------------------------------- @@ -46,8 +46,8 @@ SCALE = "Scale (x,y)" SIZE = "Image size (x*y)" MODE = "Image type" -TAGS = { COMMENT:0, DATE:0, EQUIPMENT:0, FRAMES:0, LUT:0, NAME:0, - SCALE:0, SIZE:0, MODE:0 } +TAGS = {COMMENT: 0, DATE: 0, EQUIPMENT: 0, FRAMES: 0, LUT: 0, NAME: 0, + SCALE: 0, SIZE: 0, MODE: 0} OPEN = { # ifunc93/p3cfunc formats @@ -94,12 +94,14 @@ for i in range(2, 33): split = re.compile(br"^([A-Za-z][^:]*):[ \t]*(.*)[ \t]*$") + def number(s): try: return int(s) except ValueError: return float(s) + ## # Image plugin for the IFUNC IM file format. @@ -113,7 +115,7 @@ class ImImageFile(ImageFile.ImageFile): # Quick rejection: if there's not an LF among the first # 100 bytes, this is (probably) not a text header. - if not b"\n" in self.fp.read(100): + if b"\n" not in self.fp.read(100): raise SyntaxError("not an IM file") self.fp.seek(0) @@ -155,10 +157,10 @@ class ImImageFile(ImageFile.ImageFile): if m: - k, v = m.group(1,2) + k, v = m.group(1, 2) - # Don't know if this is the correct encoding, but a decent guess - # (I guess) + # Don't know if this is the correct encoding, + # but a decent guess (I guess) k = k.decode('latin-1', 'replace') v = v.decode('latin-1', 'replace') @@ -186,7 +188,8 @@ class ImImageFile(ImageFile.ImageFile): else: - raise SyntaxError("Syntax error in IM header: " + s.decode('ascii', 'replace')) + raise SyntaxError("Syntax error in IM header: " + + s.decode('ascii', 'replace')) if not n: raise SyntaxError("Not an IM file") @@ -204,8 +207,8 @@ class ImImageFile(ImageFile.ImageFile): if LUT in self.info: # convert lookup table to palette or lut attribute palette = self.fp.read(768) - greyscale = 1 # greyscale palette - linear = 1 # linear greyscale palette + greyscale = 1 # greyscale palette + linear = 1 # linear greyscale palette for i in range(256): if palette[i] == palette[i+256] == palette[i+512]: if i8(palette[i]) != i: @@ -230,7 +233,7 @@ class ImImageFile(ImageFile.ImageFile): self.__offset = offs = self.fp.tell() - self.__fp = self.fp # FIXME: hack + self.__fp = self.fp # FIXME: hack if self.rawmode[:2] == "F;": @@ -239,7 +242,7 @@ class ImImageFile(ImageFile.ImageFile): # use bit decoder (if necessary) bits = int(self.rawmode[2:]) if bits not in [8, 16, 32]: - self.tile = [("bit", (0,0)+self.size, offs, + self.tile = [("bit", (0, 0)+self.size, offs, (bits, 8, 3, 0, -1))] return except ValueError: @@ -249,12 +252,17 @@ class ImImageFile(ImageFile.ImageFile): # Old LabEye/3PC files. Would be very surprised if anyone # ever stumbled upon such a file ;-) size = self.size[0] * self.size[1] - self.tile = [("raw", (0,0)+self.size, offs, ("G", 0, -1)), - ("raw", (0,0)+self.size, offs+size, ("R", 0, -1)), - ("raw", (0,0)+self.size, offs+2*size, ("B", 0, -1))] + self.tile = [("raw", (0, 0)+self.size, offs, ("G", 0, -1)), + ("raw", (0, 0)+self.size, offs+size, ("R", 0, -1)), + ("raw", (0, 0)+self.size, offs+2*size, ("B", 0, -1))] else: # LabEye/IFUNC files - self.tile = [("raw", (0,0)+self.size, offs, (self.rawmode, 0, -1))] + self.tile = [("raw", (0, 0)+self.size, offs, + (self.rawmode, 0, -1))] + + @property + def n_frames(self): + return self.info[FRAMES] def seek(self, frame): @@ -276,7 +284,7 @@ class ImImageFile(ImageFile.ImageFile): self.fp = self.__fp - self.tile = [("raw", (0,0)+self.size, offs, (self.rawmode, 0, -1))] + self.tile = [("raw", (0, 0)+self.size, offs, (self.rawmode, 0, -1))] def tell(self): @@ -305,10 +313,11 @@ SAVE = { "YCbCr": ("YCC", "YCbCr;L") } + def _save(im, fp, filename, check=0): try: - type, rawmode = SAVE[im.mode] + image_type, rawmode = SAVE[im.mode] except KeyError: raise ValueError("Cannot save %s images as IM" % im.mode) @@ -320,7 +329,7 @@ def _save(im, fp, filename, check=0): if check: return check - fp.write(("Image type: %s image\r\n" % type).encode('ascii')) + fp.write(("Image type: %s image\r\n" % image_type).encode('ascii')) if filename: fp.write(("Name: %s\r\n" % filename).encode('ascii')) fp.write(("Image size (x*y): %d*%d\r\n" % im.size).encode('ascii')) @@ -329,8 +338,8 @@ def _save(im, fp, filename, check=0): fp.write(b"Lut: 1\r\n") fp.write(b"\000" * (511-fp.tell()) + b"\032") if im.mode == "P": - fp.write(im.im.getpalette("RGB", "RGB;L")) # 768 bytes - ImageFile._save(im, fp, [("raw", (0,0)+im.size, 0, (rawmode, 0, -1))]) + fp.write(im.im.getpalette("RGB", "RGB;L")) # 768 bytes + ImageFile._save(im, fp, [("raw", (0, 0)+im.size, 0, (rawmode, 0, -1))]) # # -------------------------------------------------------------------- diff --git a/PIL/Image.py b/PIL/Image.py index 480410eff..1c95bfca0 100644 --- a/PIL/Image.py +++ b/PIL/Image.py @@ -34,7 +34,8 @@ import warnings class DecompressionBombWarning(RuntimeWarning): pass -class _imaging_not_installed: + +class _imaging_not_installed(object): # module placeholder def __getattr__(self, id): raise ImportError("The _imaging C module is not installed") @@ -54,10 +55,11 @@ except ImportError: pass try: - # If the _imaging C module is not present, you can still use - # the "open" function to identify files, but you cannot load - # them. Note that other modules should not refer to _imaging - # directly; import Image and use the Image.core variable instead. + # If the _imaging C module is not present, Pillow will not load. + # Note that other modules should not refer to _imaging directly; + # import Image and use the Image.core variable instead. + # Also note that Image.core is not a publicly documented interface, + # and should be considered private and subject to change. from PIL import _imaging as core if PILLOW_VERSION != getattr(core, 'PILLOW_VERSION', None): raise ImportError("The _imaging extension was built for another " @@ -90,6 +92,7 @@ except ImportError as v: RuntimeWarning ) # Fail here anyway. Don't let people run with a mostly broken Pillow. + # see docs/porting-pil-to-pillow.rst raise try: @@ -106,6 +109,8 @@ from PIL._util import deferred_error import os import sys +import io +import struct # type stuff import collections @@ -149,6 +154,7 @@ FLIP_TOP_BOTTOM = 1 ROTATE_90 = 2 ROTATE_180 = 3 ROTATE_270 = 4 +TRANSPOSE = 5 # transforms AFFINE = 0 @@ -158,11 +164,10 @@ QUAD = 3 MESH = 4 # resampling filters -NONE = 0 -NEAREST = 0 -ANTIALIAS = 1 # 3-lobed lanczos -LINEAR = BILINEAR = 2 -CUBIC = BICUBIC = 3 +NEAREST = NONE = 0 +LANCZOS = ANTIALIAS = 1 +BILINEAR = LINEAR = 2 +BICUBIC = CUBIC = 3 # dithers NONE = 0 @@ -382,7 +387,7 @@ def init(): for plugin in _plugins: try: if DEBUG: - print ("Importing %s" % plugin) + print("Importing %s" % plugin) __import__("PIL.%s" % plugin, globals(), locals(), []) except ImportError: if DEBUG: @@ -438,7 +443,7 @@ def coerce_e(value): return value if isinstance(value, _E) else _E(value) -class _E: +class _E(object): def __init__(self, data): self.data = data @@ -473,7 +478,7 @@ def _getscaleoffset(expr): # -------------------------------------------------------------------- # Implementation wrapper -class Image: +class Image(object): """ This class represents an image object. To create :py:class:`~PIL.Image.Image` objects, use the appropriate factory @@ -530,7 +535,7 @@ class Image: """ Closes the file pointer, if possible. - This operation will destroy the image core and release it's memory. + This operation will destroy the image core and release its memory. The image data will be unusable afterward. This function is only required to close images that have not @@ -541,7 +546,7 @@ class Image: self.fp.close() except Exception as msg: if DEBUG: - print ("Error closing: %s" % msg) + print("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 @@ -595,6 +600,16 @@ class Image: id(self) ) + def _repr_png_(self): + """ iPython display hook support + + :returns: png version of the image as bytes + """ + from io import BytesIO + b = BytesIO() + self.save(b, 'PNG') + return b.getvalue() + def __getattr__(self, name): if name == "__array_interface__": # numpy array interface support @@ -622,7 +637,7 @@ class Image: self.mode = mode self.size = size self.im = core.new(mode, size) - if mode in ("L", "P"): + if mode in ("L", "P") and palette: self.putpalette(palette) self.frombytes(data) @@ -664,6 +679,10 @@ class Image: # 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, @@ -737,6 +756,7 @@ class Image: associated with the image. :returns: An image access object. + :rtype: :ref:`PixelAccess` or :py:class:`PIL.PyAccess` """ if self.im and self.palette and self.palette.dirty: # realize palette @@ -796,7 +816,7 @@ class Image: use other thresholds, use the :py:meth:`~PIL.Image.Image.point` method. - :param mode: The requested mode. + :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. :param dither: Dithering method, used when converting from @@ -847,8 +867,9 @@ class Image: t = self.info['transparency'] if isinstance(t, bytes): # Dragons. This can't be represented by a single color - warnings.warn('Palette images with Transparency expressed ' + - ' in bytes should be converted to RGBA images') + warnings.warn('Palette images with Transparency ' + + ' expressed in bytes should be converted ' + + 'to RGBA images') delete_trns = True else: # get the new transparency color. @@ -864,11 +885,20 @@ class Image: # can't just retrieve the palette number, got to do it # after quantization. trns_im = trns_im.convert('RGB') - trns = trns_im.getpixel((0,0)) + trns = trns_im.getpixel((0, 0)) elif self.mode == 'P' and mode == 'RGBA': + t = self.info['transparency'] delete_trns = True + if isinstance(t, bytes): + self.im.putpalettealphas(t) + elif isinstance(t, int): + self.im.putpalettealpha(t, 0) + else: + raise ValueError("Transparency for P mode should" + + " be bytes or int") + if mode == "P" and palette == ADAPTIVE: im = self.im.quantize(colors) new = self._new(im) @@ -920,14 +950,19 @@ class Image: return new_im def quantize(self, colors=256, method=None, kmeans=0, palette=None): + """ + Convert the image to 'P' mode with the specified number + of colors. - # methods: - # 0 = median cut - # 1 = maximum coverage - # 2 = fast octree + :param colors: The desired number of colors, <= 256 + :param method: 0 = median cut + 1 = maximum coverage + 2 = fast octree + :param kmeans: Integer + :param palette: Quantize to the :py:class:`PIL.ImagingPalette` palette. + :returns: A new image - # NOTE: this functionality will be moved to the extended - # quantizer interface in a later version of PIL. + """ self.load() @@ -994,8 +1029,6 @@ class Image: def draft(self, mode, size): """ - NYI - Configures the image file loader so it returns a version of the image that as closely as possible matches the given mode and size. For example, you can use this method to convert a color @@ -1255,11 +1288,11 @@ class Image: images (in the latter case, the alpha band is used as mask). Where the mask is 255, the given image is copied as is. Where the mask is 0, the current value is preserved. Intermediate - values can be used for transparency effects. + values will mix the two images together, including their alpha + channels if they have them. - Note that if you paste an "RGBA" image, the alpha band is - ignored. You can work around this by using the same image as - both source image and mask. + See :py:meth:`~PIL.Image.Image.alpha_composite` if you want to + combine images with respect to their alpha channels. :param im: Source image or pixel value (integer or tuple). :param box: An optional 4-tuple giving the region to paste into. @@ -1500,36 +1533,30 @@ class Image: (width, height). :param resample: An optional resampling filter. This can be one of :py:attr:`PIL.Image.NEAREST` (use nearest neighbour), - :py:attr:`PIL.Image.BILINEAR` (linear interpolation in a 2x2 - environment), :py:attr:`PIL.Image.BICUBIC` (cubic spline - interpolation in a 4x4 environment), or - :py:attr:`PIL.Image.ANTIALIAS` (a high-quality downsampling filter). + :py:attr:`PIL.Image.BILINEAR` (linear interpolation), + :py:attr:`PIL.Image.BICUBIC` (cubic spline interpolation), or + :py:attr:`PIL.Image.LANCZOS` (a high-quality downsampling filter). If omitted, or if the image has mode "1" or "P", it is set :py:attr:`PIL.Image.NEAREST`. :returns: An :py:class:`~PIL.Image.Image` object. """ - if resample not in (NEAREST, BILINEAR, BICUBIC, ANTIALIAS): + if resample not in (NEAREST, BILINEAR, BICUBIC, LANCZOS): raise ValueError("unknown resampling filter") self.load() + size = tuple(size) + if self.size == size: + return self._new(self.im) + if self.mode in ("1", "P"): resample = NEAREST if self.mode == 'RGBA': return self.convert('RGBa').resize(size, resample).convert('RGBA') - if resample == ANTIALIAS: - # requires stretch support (imToolkit & PIL 1.1.3) - try: - im = self.im.stretch(size, resample) - except AttributeError: - raise ValueError("unsupported resampling filter") - else: - im = self.im.resize(size, resample) - - return self._new(im) + return self._new(self.im.resize(size, resample)) def rotate(self, angle, resample=NEAREST, expand=0): """ @@ -1600,15 +1627,16 @@ class Image: Keyword options can be used to provide additional instructions to the writer. If a writer doesn't recognise an option, it is - silently ignored. The available options are described later in - this handbook. + silently ignored. The available options are described in the + :doc:`image format documentation + <../handbook/image-file-formats>` for each writer. You can use a file object instead of a filename. In this case, you must always specify the format. The file object must - implement the **seek**, **tell**, and **write** + implement the ``seek``, ``tell``, and ``write`` methods, and be opened in binary mode. - :param file: File name or file object. + :param fp: File name 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 @@ -1735,7 +1763,7 @@ class Image: """ return 0 - def thumbnail(self, size, resample=ANTIALIAS): + def thumbnail(self, size, resample=BICUBIC): """ Make this image into a thumbnail. This method modifies the image to contain a thumbnail version of itself, no larger than @@ -1744,12 +1772,7 @@ class Image: :py:meth:`~PIL.Image.Image.draft` method to configure the file reader (where applicable), and finally resizes the image. - Note that the bilinear and bicubic filters in the current - version of PIL are not well-suited for thumbnail generation. - You should use :py:attr:`PIL.Image.ANTIALIAS` unless speed is much more - important than quality. - - Also note that this function modifies the :py:class:`~PIL.Image.Image` + Note that this function modifies the :py:class:`~PIL.Image.Image` object in place. If you need to use the full resolution image as well, apply this method to a :py:meth:`~PIL.Image.Image.copy` of the original image. @@ -1757,10 +1780,9 @@ class Image: :param size: Requested size. :param resample: Optional resampling filter. This can be one of :py:attr:`PIL.Image.NEAREST`, :py:attr:`PIL.Image.BILINEAR`, - :py:attr:`PIL.Image.BICUBIC`, or :py:attr:`PIL.Image.ANTIALIAS` - (best quality). If omitted, it defaults to - :py:attr:`PIL.Image.ANTIALIAS`. (was :py:attr:`PIL.Image.NEAREST` - prior to version 2.5.0) + :py:attr:`PIL.Image.BICUBIC`, or :py:attr:`PIL.Image.LANCZOS`. + If omitted, it defaults to :py:attr:`PIL.Image.BICUBIC`. + (was :py:attr:`PIL.Image.NEAREST` prior to version 2.5.0) :returns: None """ @@ -1779,14 +1801,7 @@ class Image: self.draft(None, size) - self.load() - - try: - im = self.resize(size, resample) - except ValueError: - if resample != ANTIALIAS: - raise - im = self.resize(size, NEAREST) # fallback + im = self.resize(size, resample) self.im = im.im self.mode = im.mode @@ -1795,7 +1810,7 @@ class Image: self.readonly = 0 self.pyaccess = None - # FIXME: the different tranform methods need further explanation + # FIXME: the different transform methods need further explanation # instead of bloating the method docs, add a separate chapter. def transform(self, size, method, data=None, resample=NEAREST, fill=1): """ @@ -1902,12 +1917,22 @@ class Image: :param method: One of :py:attr:`PIL.Image.FLIP_LEFT_RIGHT`, :py:attr:`PIL.Image.FLIP_TOP_BOTTOM`, :py:attr:`PIL.Image.ROTATE_90`, - :py:attr:`PIL.Image.ROTATE_180`, or :py:attr:`PIL.Image.ROTATE_270`. + :py:attr:`PIL.Image.ROTATE_180`, :py:attr:`PIL.Image.ROTATE_270` or + :py:attr:`PIL.Image.TRANSPOSE`. :returns: Returns a flipped or rotated copy of this image. """ self.load() - im = self.im.transpose(method) + return self._new(self.im.transpose(method)) + + def effect_spread(self, distance): + """ + Randomly spread pixels in an image. + + :param distance: Distance to spread pixels. + """ + self.load() + im = self.im.effect_spread(distance) return self._new(im) @@ -1950,12 +1975,12 @@ class _ImageCrop(Image): # -------------------------------------------------------------------- # Abstract handlers. -class ImagePointHandler: +class ImagePointHandler(object): # used as a mixin by point transforms (for use with im.point) pass -class ImageTransformHandler: +class ImageTransformHandler(object): # used as a mixin by geometry transforms (for use with im.transform) pass @@ -1976,7 +2001,8 @@ def new(mode, size, color=0): """ Creates a new image with the given mode and size. - :param mode: The mode to use for the new image. + :param mode: The mode to use for the new image. See: + :ref:`concept-modes`. :param size: A 2-tuple, containing (width, height) in pixels. :param color: What color to use for the image. Default is black. If given, this should be a single integer or floating point value @@ -2009,14 +2035,14 @@ def frombytes(mode, size, data, decoder_name="raw", *args): You can also use any pixel decoder supported by PIL. For more information on available decoders, see the section - **Writing Your Own File Decoder**. + :ref:`Writing Your Own File Decoder `. Note that this function decodes pixel data only, not entire images. If you have an entire image in a string, wrap it in a :py:class:`~io.BytesIO` object, and use :py:func:`~PIL.Image.open` to load it. - :param mode: The image mode. + :param mode: The image mode. See: :ref:`concept-modes`. :param size: The image size. :param data: A byte buffer containing raw data for the given mode. :param decoder_name: What decoder to use. @@ -2068,7 +2094,7 @@ def frombuffer(mode, size, data, decoder_name="raw", *args): issues a warning if you do this; to disable the warning, you should provide the full set of parameters. See below for details. - :param mode: The image mode. + :param mode: The image mode. See: :ref:`concept-modes`. :param size: The image size. :param data: A bytes or other buffer object containing raw data for the given mode. @@ -2119,7 +2145,8 @@ def fromarray(obj, mode=None): :param obj: Object with array interface :param mode: Mode to use (will be determined from type if None) - :returns: An image memory. + See: :ref:`concept-modes`. + :returns: An image object. .. versionadded:: 1.1.6 """ @@ -2222,6 +2249,11 @@ def open(fp, mode="r"): else: filename = "" + try: + fp.seek(0) + except (AttributeError, io.UnsupportedOperation): + fp = io.BytesIO(fp.read()) + prefix = fp.read(16) preinit() @@ -2234,7 +2266,7 @@ def open(fp, mode="r"): im = factory(fp, filename) _decompression_bomb_check(im.size) return im - except (SyntaxError, IndexError, TypeError): + except (SyntaxError, IndexError, TypeError, struct.error): # import traceback # traceback.print_exc() pass @@ -2249,7 +2281,7 @@ def open(fp, mode="r"): im = factory(fp, filename) _decompression_bomb_check(im.size) return im - except (SyntaxError, IndexError, TypeError): + except (SyntaxError, IndexError, TypeError, struct.error): # import traceback # traceback.print_exc() pass @@ -2306,7 +2338,7 @@ def composite(image1, image2, mask): :param image1: The first image. :param image2: The second image. Must have the same mode and size as the first image. - :param mask: A mask image. This image can can have mode + :param mask: A mask image. This image can have mode "1", "L", or "RGBA", and must have the same size as the other two images. """ @@ -2336,7 +2368,8 @@ def merge(mode, bands): """ Merge a set of single band images into a new multiband image. - :param mode: The mode to use for the output image. + :param mode: The mode to use for the output image. See: + :ref:`concept-modes`. :param bands: A sequence containing one single-band image for each band in the output image. All bands must have the same size. @@ -2419,3 +2452,32 @@ def _show(image, **options): def _showxv(image, title=None, **options): from PIL import ImageShow ImageShow.show(image, title, **options) + + +# -------------------------------------------------------------------- +# Effects + +def effect_mandelbrot(size, extent, quality): + """ + Generate a Mandelbrot set covering the given extent. + + :param size: The requested size in pixels, as a 2-tuple: + (width, height). + :param extent: The extent to cover, as a 4-tuple: + (x0, y0, x1, y2). + :param quality: Quality. + """ + return Image()._new(core.effect_mandelbrot(size, extent, quality)) + + +def effect_noise(size, sigma): + """ + Generate Gaussian noise centered around 128. + + :param size: The requested size in pixels, as a 2-tuple: + (width, height). + :param sigma: Standard deviation of noise. + """ + return Image()._new(core.effect_noise(size, sigma)) + +# End of file diff --git a/PIL/ImageCms.py b/PIL/ImageCms.py index 04a3f7f61..ebf127df3 100644 --- a/PIL/ImageCms.py +++ b/PIL/ImageCms.py @@ -1,19 +1,19 @@ -## The Python Imaging Library. -## $Id$ +# The Python Imaging Library. +# $Id$ -## Optional color managment support, based on Kevin Cazabon's PyCMS -## library. +# Optional color managment support, based on Kevin Cazabon's PyCMS +# library. -## History: +# History: -## 2009-03-08 fl Added to PIL. +# 2009-03-08 fl Added to PIL. -## Copyright (C) 2002-2003 Kevin Cazabon -## Copyright (c) 2009 by Fredrik Lundh -## Copyright (c) 2013 by Eric Soroos +# Copyright (C) 2002-2003 Kevin Cazabon +# Copyright (c) 2009 by Fredrik Lundh +# Copyright (c) 2013 by Eric Soroos -## See the README file for information on usage and redistribution. See -## below for the original description. +# See the README file for information on usage and redistribution. See +# below for the original description. from __future__ import print_function @@ -64,7 +64,7 @@ pyCMS 0.0.2 alpha Jan 6, 2002 - Added try/except statements arount type() checks of + Added try/except statements around type() checks of potential CObjects... Python won't let you use type() on them, and raises a TypeError (stupid, if you ask me!) @@ -90,8 +90,8 @@ try: except ImportError as ex: # Allow error import for doc purposes, but error out when accessing # anything in core. - from _util import import_err - _imagingcms = import_err(ex) + from _util import deferred_error + _imagingcms = deferred_error(ex) from PIL._util import isStringType core = _imagingcms @@ -123,8 +123,8 @@ FLAGS = { "NOTCACHE": 64, # Inhibit 1-pixel cache "NOTPRECALC": 256, "NULLTRANSFORM": 512, # Don't transform anyway - "HIGHRESPRECALC": 1024, # Use more memory to give better accurancy - "LOWRESPRECALC": 2048, # Use less memory to minimize resouces + "HIGHRESPRECALC": 1024, # Use more memory to give better accuracy + "LOWRESPRECALC": 2048, # Use less memory to minimize resources "WHITEBLACKCOMPENSATION": 8192, "BLACKPOINTCOMPENSATION": 8192, "GAMUTCHECK": 4096, # Out of Gamut alarm @@ -147,16 +147,16 @@ for flag in FLAGS.values(): ## # Profile. -class ImageCmsProfile: +class ImageCmsProfile(object): def __init__(self, profile): """ :param profile: Either a string representing a filename, a file like object containing a profile or a low-level profile object - + """ - + if isStringType(profile): self._set(core.profile_open(profile), profile) elif hasattr(profile, "read"): @@ -181,9 +181,10 @@ class ImageCmsProfile: :returns: a bytes object containing the ICC profile. """ - + return core.profile_tobytes(self.profile) + class ImageCmsTransform(Image.ImagePointHandler): # Transform. This can be used with the procedural API, or with the @@ -191,7 +192,6 @@ class ImageCmsTransform(Image.ImagePointHandler): # # Will return the output profile in the output.info['icc_profile']. - def __init__(self, input, output, input_mode, output_mode, intent=INTENT_PERCEPTUAL, proof=None, proof_intent=INTENT_ABSOLUTE_COLORIMETRIC, flags=0): @@ -573,7 +573,7 @@ def applyTransform(im, transform, inPlace=0): This function applies a pre-calculated transform (from ImageCms.buildTransform() or ImageCms.buildTransformFromOpenProfiles()) to an image. The transform can be used for multiple images, saving - considerable calcuation time if doing the same conversion multiple times. + considerable calculation time if doing the same conversion multiple times. If you want to modify im in-place instead of receiving a new image as the return value, set inPlace to TRUE. This can only be done if @@ -858,7 +858,7 @@ def getDefaultIntent(profile): If an error occurs while trying to obtain the default intent, a PyCMSError is raised. - Use this function to determine the default (and usually best optomized) + Use this function to determine the default (and usually best optimized) rendering intent for this profile. Most profiles support multiple rendering intents, but are intended mostly for one type of conversion. If you wish to use a different intent than returned, use @@ -914,7 +914,7 @@ def isIntentSupported(profile, intent, direction): see the pyCMS documentation for details on rendering intents and what they do. - :param direction: Integer specifing if the profile is to be used for input, + :param direction: Integer specifying if the profile is to be used for input, output, or proof INPUT = 0 (or use ImageCms.DIRECTION_INPUT) diff --git a/PIL/ImageColor.py b/PIL/ImageColor.py index 98a241bb0..fc95e6d19 100644 --- a/PIL/ImageColor.py +++ b/PIL/ImageColor.py @@ -20,6 +20,7 @@ from PIL import Image import re + def getrgb(color): """ Convert a color string to an RGB tuple. If the string cannot be parsed, @@ -86,7 +87,8 @@ def getrgb(color): int(rgb[1] * 255 + 0.5), int(rgb[2] * 255 + 0.5) ) - m = re.match("rgba\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)$", color) + m = re.match("rgba\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)$", + color) if m: return ( int(m.group(1)), @@ -96,6 +98,7 @@ def getrgb(color): ) raise ValueError("unknown color specifier: %r" % color) + def getcolor(color, mode): """ Same as :py:func:`~PIL.ImageColor.getrgb`, but converts the RGB value to a diff --git a/PIL/ImageDraw.py b/PIL/ImageDraw.py index a03d26016..1fc5b4d61 100644 --- a/PIL/ImageDraw.py +++ b/PIL/ImageDraw.py @@ -40,13 +40,14 @@ try: except ImportError: warnings = None + ## # A simple 2D drawing interface for PIL images. #

# Application code should use the Draw factory, instead of # directly. -class ImageDraw: +class ImageDraw(object): ## # Create a drawing instance. @@ -61,7 +62,7 @@ class ImageDraw: def __init__(self, im, mode=None): im.load() if im.readonly: - im._copy() # make it writable + im._copy() # make it writeable blend = 0 if mode is None: mode = im.mode @@ -85,7 +86,7 @@ class ImageDraw: # FIXME: fix Fill2 to properly support matte for I+F images self.fontmode = "1" else: - self.fontmode = "L" # aliasing is okay for other modes + self.fontmode = "L" # aliasing is okay for other modes self.fill = 0 self.font = None @@ -280,6 +281,7 @@ class ImageDraw: font = self.getfont() return font.getsize(text) + ## # A simple 2D drawing interface for PIL images. # @@ -302,6 +304,7 @@ try: except: Outline = None + ## # (Experimental) A more advanced 2D drawing interface for PIL images, # based on the WCK interface. @@ -325,6 +328,7 @@ def getdraw(im=None, hints=None): im = handler.Draw(im) return im, handler + ## # (experimental) Fills a bounded region with a given color. # @@ -344,10 +348,10 @@ def floodfill(image, xy, value, border=None): try: background = pixel[x, y] if background == value: - return # seed point already has fill color + return # seed point already has fill color pixel[x, y] = value except IndexError: - return # seed point outside image + return # seed point outside image edge = [(x, y)] if border is None: while edge: diff --git a/PIL/ImageDraw2.py b/PIL/ImageDraw2.py index 146cc8b16..62ee11630 100644 --- a/PIL/ImageDraw2.py +++ b/PIL/ImageDraw2.py @@ -18,22 +18,26 @@ from PIL import Image, ImageColor, ImageDraw, ImageFont, ImagePath -class Pen: + +class Pen(object): def __init__(self, color, width=1, opacity=255): self.color = ImageColor.getrgb(color) self.width = width -class Brush: + +class Brush(object): def __init__(self, color, opacity=255): self.color = ImageColor.getrgb(color) -class Font: + +class Font(object): def __init__(self, color, file, size=12): # FIXME: add support for bitmap fonts self.color = ImageColor.getrgb(color) self.font = ImageFont.truetype(file, size) -class Draw: + +class Draw(object): def __init__(self, image, size=None, color=None): if not hasattr(image, "im"): @@ -47,7 +51,8 @@ class Draw: def render(self, op, xy, pen, brush=None): # handle color arguments - outline = fill = None; width = 1 + outline = fill = None + width = 1 if isinstance(pen, Pen): outline = pen.color width = pen.width diff --git a/PIL/ImageEnhance.py b/PIL/ImageEnhance.py index f802dc1d3..56b5c0199 100644 --- a/PIL/ImageEnhance.py +++ b/PIL/ImageEnhance.py @@ -21,7 +21,7 @@ from PIL import Image, ImageFilter, ImageStat -class _Enhance: +class _Enhance(object): def enhance(self, factor): """ @@ -47,7 +47,11 @@ class Color(_Enhance): """ def __init__(self, image): self.image = image - self.degenerate = image.convert("L").convert(image.mode) + self.intermediate_mode = 'L' + if 'A' in image.getbands(): + self.intermediate_mode = 'LA' + + self.degenerate = image.convert(self.intermediate_mode).convert(image.mode) class Contrast(_Enhance): @@ -62,11 +66,14 @@ class Contrast(_Enhance): mean = int(ImageStat.Stat(image.convert("L")).mean[0] + 0.5) self.degenerate = Image.new("L", image.size, mean).convert(image.mode) + if 'A' in image.getbands(): + self.degenerate.putalpha(image.split()[-1]) + class Brightness(_Enhance): """Adjust image brightness. - This class can be used to control the brighntess of an image. An + This class can be used to control the brightness of an image. An enhancement factor of 0.0 gives a black image. A factor of 1.0 gives the original image. """ @@ -74,6 +81,9 @@ class Brightness(_Enhance): self.image = image self.degenerate = Image.new(image.mode, image.size, 0) + if 'A' in image.getbands(): + self.degenerate.putalpha(image.split()[-1]) + class Sharpness(_Enhance): """Adjust image sharpness. @@ -85,3 +95,6 @@ class Sharpness(_Enhance): def __init__(self, image): self.image = image self.degenerate = image.filter(ImageFilter.SMOOTH) + + if 'A' in image.getbands(): + self.degenerate.putalpha(image.split()[-1]) diff --git a/PIL/ImageFile.py b/PIL/ImageFile.py index 5e4745d76..b1d261166 100644 --- a/PIL/ImageFile.py +++ b/PIL/ImageFile.py @@ -29,8 +29,10 @@ from PIL import Image from PIL._util import isPath -import traceback, os, sys import io +import os +import sys +import traceback MAXBLOCK = 65536 @@ -46,6 +48,7 @@ ERRORS = { -9: "out of memory error" } + def raise_ioerror(error): try: message = Image.core.getcodecstatus(error) @@ -55,6 +58,7 @@ def raise_ioerror(error): message = "decoder error %d" % error raise IOError(message + " when reading image file") + # # -------------------------------------------------------------------- # Helpers @@ -63,6 +67,7 @@ def _tilesort(t): # sort on offset return t[2] + # # -------------------------------------------------------------------- # ImageFile base class @@ -74,7 +79,7 @@ class ImageFile(Image.Image): Image.Image.__init__(self) self.tile = None - self.readonly = 1 # until we know better + self.readonly = 1 # until we know better self.decoderconfig = () self.decodermaxblock = MAXBLOCK @@ -90,19 +95,19 @@ class ImageFile(Image.Image): try: self._open() - except IndexError as v: # end of data + 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) + except TypeError as v: # end of data (ord) if Image.DEBUG > 1: traceback.print_exc() raise SyntaxError(v) - except KeyError as v: # unsupported mode + 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 + except EOFError as v: # got header but not the first frame if Image.DEBUG > 1: traceback.print_exc() raise SyntaxError(v) @@ -135,8 +140,8 @@ class ImageFile(Image.Image): self.map = None use_mmap = self.filename and len(self.tile) == 1 # As of pypy 2.1.0, memory mapping was failing here. - use_mmap = use_mmap and not hasattr(sys, 'pypy_version_info') - + use_mmap = use_mmap and not hasattr(sys, 'pypy_version_info') + readonly = 0 # look for read/seek overrides @@ -191,6 +196,9 @@ 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) @@ -203,23 +211,25 @@ class ImageFile(Image.Image): while True: try: s = read(self.decodermaxblock) - except IndexError as ie: # truncated png/gif + except IndexError as ie: # truncated png/gif if LOAD_TRUNCATED_IMAGES: break else: raise IndexError(ie) - if not s and not d.handles_eof: # truncated jpeg + if not s and not d.handles_eof: # truncated jpeg self.tile = [] # JpegDecode needs to clean things up here either way - # If we don't destroy the decompressor, we have a memory leak. + # If we don't destroy the decompressor, + # we have a memory leak. d.cleanup() if LOAD_TRUNCATED_IMAGES: break else: - raise IOError("image file is truncated (%d bytes not processed)" % len(b)) + raise IOError("image file is truncated " + "(%d bytes not processed)" % len(b)) b = b + s n, e = d.decode(b) @@ -227,11 +237,13 @@ class ImageFile(Image.Image): break b = b[n:] t = t + n + # Need to cleanup here to prevent leaks in PyPy + d.cleanup() self.tile = [] self.readonly = readonly - self.fp = None # might be shared + self.fp = None # might be shared if not self.map and (not LOAD_TRUNCATED_IMAGES or t == 0) and e < 0: # still raised if decoder fails to return anything @@ -299,7 +311,7 @@ class StubImageFile(ImageFile): ) -class Parser: +class Parser(object): """ Incremental image parser. This class implements the standard feed/close consumer interface. @@ -310,6 +322,7 @@ class Parser: image = None data = None decoder = None + offset = 0 finished = 0 def reset(self): @@ -378,10 +391,10 @@ class Parser: fp = io.BytesIO(self.data) im = Image.open(fp) finally: - fp.close() # explicitly close the virtual file + fp.close() # explicitly close the virtual file except IOError: # traceback.print_exc() - pass # not enough data + pass # not enough data else: flag = hasattr(im, "load_seek") or hasattr(im, "load_read") if flag or len(im.tile) != 1: @@ -431,9 +444,10 @@ class Parser: self.image = Image.open(fp) finally: self.image.load() - fp.close() # explicitly close the virtual file + fp.close() # explicitly close the virtual file return self.image + # -------------------------------------------------------------------- def _save(im, fp, tile, bufsize=0): @@ -450,10 +464,10 @@ def _save(im, fp, tile, bufsize=0): im.encoderconfig = () tile.sort(key=_tilesort) # FIXME: make MAXBLOCK a configuration parameter - # It would be great if we could have the encoder specifiy what it needs + # It would be great if we could have the encoder specify what it needs # 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 + bufsize = max(MAXBLOCK, bufsize, im.size[0] * 4) # see RawEncode.c try: fh = fp.fileno() fp.flush() @@ -471,6 +485,7 @@ def _save(im, fp, tile, bufsize=0): break if s < 0: raise IOError("encoder error %d when writing image file" % s) + e.cleanup() else: # slight speedup: compress to real file object for e, b, o, a in tile: @@ -481,9 +496,11 @@ def _save(im, fp, tile, bufsize=0): s = e.encode_to_file(fh, bufsize) if s < 0: raise IOError("encoder error %d when writing image file" % s) + e.cleanup() try: fp.flush() - except: pass + except: + pass def _safe_read(fp, size): diff --git a/PIL/ImageFilter.py b/PIL/ImageFilter.py index ac8fe9f19..baa168aa7 100644 --- a/PIL/ImageFilter.py +++ b/PIL/ImageFilter.py @@ -15,7 +15,7 @@ # See the README file for information on usage and redistribution. # -from functools import reduce +import functools class Filter(object): @@ -43,7 +43,7 @@ class Kernel(Filter): def __init__(self, size, kernel, scale=None, offset=0): if scale is None: # default scale is sum of kernel - scale = reduce(lambda a,b: a+b, kernel) + scale = functools.reduce(lambda a, b: a+b, kernel) if size[0] * size[1] != len(kernel): raise ValueError("not enough coefficients in kernel") self.filterargs = size, scale, offset, kernel @@ -162,7 +162,13 @@ class UnsharpMask(Filter): See Wikipedia's entry on `digital unsharp masking`_ for an explanation of the parameters. + :param radius: Blur Radius + :param percent: Unsharp strength, in percent + :param threshold: Threshold controls the minimum brightness change that + will be sharpened + .. _digital unsharp masking: https://en.wikipedia.org/wiki/Unsharp_masking#Digital_unsharp_masking + """ name = "UnsharpMask" diff --git a/PIL/ImageFont.py b/PIL/ImageFont.py index bd52ea938..c6e7cb4d9 100644 --- a/PIL/ImageFont.py +++ b/PIL/ImageFont.py @@ -25,8 +25,6 @@ # See the README file for information on usage and redistribution. # -from __future__ import print_function - from PIL import Image from PIL._util import isDirectory, isPath import os @@ -38,7 +36,7 @@ except ImportError: warnings = None -class _imagingft_not_installed: +class _imagingft_not_installed(object): # module placeholder def __getattr__(self, id): raise ImportError("The _imagingft C module is not installed") @@ -64,7 +62,7 @@ except ImportError: # -------------------------------------------------------------------- -class ImageFont: +class ImageFont(object): "PIL font wrapper" def _load_pilfont(self, filename): @@ -120,7 +118,7 @@ class ImageFont: # Wrapper for FreeType fonts. Application code should use the # truetype factory function to create font objects. -class FreeTypeFont: +class FreeTypeFont(object): "FreeType font wrapper (requires _imagingft service)" def __init__(self, font=None, size=10, index=0, encoding="", file=None): @@ -133,6 +131,11 @@ class FreeTypeFont: DeprecationWarning) font = file + self.path = font + self.size = size + self.index = index + self.encoding = encoding + if isPath(font): self.font = core.getfont(font, size, index, encoding) else: @@ -162,6 +165,22 @@ class FreeTypeFont: self.font.render(text, im.id, mode == "1") return im, offset + def font_variant(self, font=None, size=None, index=None, encoding=None): + """ + Create a copy of this 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. + + :return: A FreeTypeFont object. + """ + return FreeTypeFont(font=self.path if font is None else font, + size=self.size if size is None else size, + index=self.index if index is None else index, + encoding=self.encoding if encoding is None else + encoding) + ## # Wrapper that creates a transposed font from any existing font # object. @@ -172,7 +191,7 @@ class FreeTypeFont: # Image.ROTATE_90, Image.ROTATE_180, or Image.ROTATE_270. -class TransposedFont: +class TransposedFont(object): "Wrapper for writing rotated or mirrored text" def __init__(self, font, orientation=None): @@ -214,7 +233,7 @@ def truetype(font=None, size=10, index=0, encoding="", filename=None): This function requires the _imagingft service. - :param filename: A truetype font file. Under Windows, if the file + :param font: A truetype font file. Under Windows, if the file is not found in this filename, the loader also looks in Windows :file:`fonts/` directory. :param size: The requested size, in points. @@ -224,6 +243,7 @@ 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. """ @@ -239,14 +259,44 @@ def truetype(font=None, size=10, index=0, encoding="", filename=None): try: return FreeTypeFont(font, size, index, encoding) except IOError: + ttf_filename = os.path.basename(font) + + dirs = [] if sys.platform == "win32": # check the windows font repository # NOTE: must use uppercase WINDIR, to work around bugs in # 1.5.2's os.environ.get() windir = os.environ.get("WINDIR") if windir: - filename = os.path.join(windir, "fonts", font) - return FreeTypeFont(filename, size, index, encoding) + dirs.append(os.path.join(windir, "fonts")) + elif sys.platform in ('linux', 'linux2'): + lindirs = os.environ.get("XDG_DATA_DIRS", "") + if not lindirs: + # 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(":")] + elif sys.platform == 'darwin': + dirs += ['/Library/Fonts', '/System/Library/Fonts', + os.path.expanduser('~/Library/Fonts')] + + ext = os.path.splitext(ttf_filename)[1] + first_font_with_a_different_extension = None + for directory in dirs: + for walkroot, walkdir, walkfilenames in os.walk(directory): + for walkfilename in walkfilenames: + if ext and walkfilename == ttf_filename: + fontpath = os.path.join(walkroot, walkfilename) + return FreeTypeFont(fontpath, size, index, encoding) + elif not ext and os.path.splitext(walkfilename)[0] == ttf_filename: + fontpath = os.path.join(walkroot, walkfilename) + if os.path.splitext(fontpath)[1] == '.ttf': + return FreeTypeFont(fontpath, size, index, encoding) + if not ext and first_font_with_a_different_extension is None: + first_font_with_a_different_extension = fontpath + if first_font_with_a_different_extension: + return FreeTypeFont(first_font_with_a_different_extension, size, + index, encoding) raise @@ -259,15 +309,15 @@ def load_path(filename): :return: A font object. :exception IOError: If the file could not be read. """ - for dir in sys.path: - if isDirectory(dir): + for directory in sys.path: + if isDirectory(directory): if not isinstance(filename, str): if bytes is str: filename = filename.encode("utf-8") else: filename = filename.decode("utf-8") try: - return load(os.path.join(dir, filename)) + return load(os.path.join(directory, filename)) except IOError: pass raise IOError("cannot find font file") diff --git a/PIL/ImageGrab.py b/PIL/ImageGrab.py index 9bb190934..ef0135334 100644 --- a/PIL/ImageGrab.py +++ b/PIL/ImageGrab.py @@ -17,6 +17,9 @@ from PIL import Image +import sys +if sys.platform != "win32": + raise ImportError("ImageGrab is Windows only") try: # built-in driver (1.1.3 and later) @@ -40,7 +43,7 @@ def grab(bbox=None): def grabclipboard(): - debug = 0 # temporary interface + debug = 0 # temporary interface data = Image.core.grabclipboard(debug) if isinstance(data, bytes): from PIL import BmpImagePlugin diff --git a/PIL/ImageMath.py b/PIL/ImageMath.py index 4dcc5125c..f92d5001f 100644 --- a/PIL/ImageMath.py +++ b/PIL/ImageMath.py @@ -31,7 +31,7 @@ def _isconstant(v): return isinstance(v, int) or isinstance(v, float) -class _Operand: +class _Operand(object): # wraps an image operand, providing standard operators def __init__(self, im): diff --git a/PIL/ImageMode.py b/PIL/ImageMode.py index c3931b525..d8960017b 100644 --- a/PIL/ImageMode.py +++ b/PIL/ImageMode.py @@ -16,10 +16,11 @@ # mode descriptor cache _modes = {} + ## # Wrapper for mode strings. -class ModeDescriptor: +class ModeDescriptor(object): def __init__(self, mode, bands, basemode, basetype): self.mode = mode @@ -30,6 +31,7 @@ class ModeDescriptor: def __str__(self): return self.mode + ## # Gets a mode descriptor for the given mode. diff --git a/PIL/ImageMorph.py b/PIL/ImageMorph.py index 3f15621a6..6f92e9e67 100644 --- a/PIL/ImageMorph.py +++ b/PIL/ImageMorph.py @@ -12,7 +12,7 @@ import re LUT_SIZE = 1 << 9 -class LutBuilder: +class LutBuilder(object): """A class for building a MorphLut from a descriptive language The input patterns is a list of a strings sequences like these:: @@ -35,14 +35,14 @@ class LutBuilder: returned if no other match is found. Operations: - + - 4 - 4 way rotation - N - Negate - 1 - Dummy op for no other operation (an op must always be given) - M - Mirroring Example:: - + lb = LutBuilder(patterns = ["4:(... .1. 111)->1"]) lut = lb.build_lut() @@ -176,7 +176,7 @@ class LutBuilder: return self.lut -class MorphOp: +class MorphOp(object): """A class for binary morphological operators""" def __init__(self, diff --git a/PIL/ImageOps.py b/PIL/ImageOps.py index 9f84eff86..f317645b2 100644 --- a/PIL/ImageOps.py +++ b/PIL/ImageOps.py @@ -20,7 +20,8 @@ from PIL import Image from PIL._util import isStringType import operator -from functools import reduce +import functools + # # helpers @@ -35,12 +36,14 @@ def _border(border): left = top = right = bottom = border return left, top, right, bottom + def _color(color, mode): if isStringType(color): from PIL import ImageColor color = ImageColor.getcolor(color, mode) return color + def _lut(image, lut): if image.mode == "P": # FIXME: apply to lookup table, not image data @@ -147,7 +150,9 @@ def colorize(image, black, white): assert image.mode == "L" black = _color(black, "RGB") white = _color(white, "RGB") - red = []; green = []; blue = [] + red = [] + green = [] + blue = [] for i in range(256): red.append(black[0]+i*(white[0]-black[0])//255) green.append(black[1]+i*(white[1]-black[1])//255) @@ -208,7 +213,7 @@ def equalize(image, mask=None): if len(histo) <= 1: lut.extend(list(range(256))) else: - step = (reduce(operator.add, histo) - histo[-1]) // 255 + step = (functools.reduce(operator.add, histo) - histo[-1]) // 255 if not step: lut.extend(list(range(256))) else: @@ -228,7 +233,6 @@ def expand(image, border=0, fill=0): :param fill: Pixel fill value (a color value). Default is 0 (black). :return: An image. """ - "Add border to image" left, top, right, bottom = _border(border) width = left + image.size[0] + right height = top + image.size[1] + bottom @@ -273,7 +277,7 @@ def fit(image, size, method=Image.NEAREST, bleed=0.0, centering=(0.5, 0.5)): centering = [centering[0], centering[1]] if centering[0] > 1.0 or centering[0] < 0.0: - centering [0] = 0.50 + centering[0] = 0.50 if centering[1] > 1.0 or centering[1] < 0.0: centering[1] = 0.50 @@ -404,6 +408,7 @@ def solarize(image, threshold=128): lut.append(255-i) return _lut(image, lut) + # -------------------------------------------------------------------- # PIL USM components, from Kevin Cazabon. @@ -419,6 +424,7 @@ def gaussian_blur(im, radius=None): gblur = gaussian_blur + def unsharp_mask(im, radius=None, percent=None, threshold=None): """ PIL_usm.usm(im, [radius, percent, threshold])""" @@ -434,3 +440,22 @@ def unsharp_mask(im, radius=None, percent=None, threshold=None): return im.im.unsharp_mask(radius, percent, threshold) usm = unsharp_mask + + +def box_blur(image, radius): + """ + Blur the image by setting each pixel to the average value of the pixels + in a square box extending radius pixels in each direction. + Supports float radius of arbitrary size. Uses an optimized implementation + which runs in linear time relative to the size of the image + for any radius value. + + :param image: The image to blur. + :param radius: Size of the box in one direction. Radius 0 does not blur, + returns an identical image. Radius 1 takes 1 pixel + in each direction, i.e. 9 pixels in total. + :return: An image. + """ + image.load() + + return image._new(image.im.box_blur(radius)) diff --git a/PIL/ImagePalette.py b/PIL/ImagePalette.py index 62f8814f1..b2f51dd06 100644 --- a/PIL/ImagePalette.py +++ b/PIL/ImagePalette.py @@ -21,7 +21,7 @@ import warnings from PIL import ImageColor -class ImagePalette: +class ImagePalette(object): "Color palette for palette mapped images" def __init__(self, mode="RGB", palette=None, size=0): @@ -225,8 +225,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/ImagePath.py b/PIL/ImagePath.py index 656d5ce61..f23d01430 100644 --- a/PIL/ImagePath.py +++ b/PIL/ImagePath.py @@ -20,7 +20,7 @@ from PIL import Image # the Python class below is overridden by the C implementation. -class Path: +class Path(object): def __init__(self, xy): pass diff --git a/PIL/ImageQt.py b/PIL/ImageQt.py index ca8b14b5c..1723e3226 100644 --- a/PIL/ImageQt.py +++ b/PIL/ImageQt.py @@ -18,20 +18,30 @@ from PIL import Image from PIL._util import isPath +import sys -try: - from PyQt5.QtGui import QImage, qRgba -except: +if 'PyQt4.QtGui' not in sys.modules: + try: + from PyQt5.QtGui import QImage, qRgba + except: + 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. + def rgb(r, g, b, a=255): # 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) + ## # An PIL image wrapper for Qt. This is a subclass of PyQt4's QImage # class. diff --git a/PIL/ImageSequence.py b/PIL/ImageSequence.py index 513c9247b..256bcbedb 100644 --- a/PIL/ImageSequence.py +++ b/PIL/ImageSequence.py @@ -15,7 +15,8 @@ ## -class Iterator: + +class Iterator(object): """ This class implements an iterator object that can be used to loop over an image sequence. @@ -38,4 +39,4 @@ class Iterator: self.im.seek(ix) return self.im except EOFError: - raise IndexError # end of sequence + raise IndexError # end of sequence diff --git a/PIL/ImageShow.py b/PIL/ImageShow.py index 40fe629d9..51417c30b 100644 --- a/PIL/ImageShow.py +++ b/PIL/ImageShow.py @@ -15,7 +15,8 @@ from __future__ import print_function from PIL import Image -import os, sys +import os +import sys if sys.version_info >= (3, 3): from shlex import quote @@ -24,17 +25,19 @@ else: _viewers = [] + def register(viewer, order=1): try: if issubclass(viewer, Viewer): viewer = viewer() except TypeError: - pass # raised if viewer wasn't a class + pass # raised if viewer wasn't a class if order > 0: _viewers.append(viewer) elif order < 0: _viewers.insert(0, viewer) + ## # Displays a given image. # @@ -49,10 +52,11 @@ def show(image, title=None, **options): return 1 return 0 + ## # Base class for viewers. -class Viewer: +class Viewer(object): # main api @@ -102,6 +106,7 @@ if sys.platform == "win32": class WindowsViewer(Viewer): format = "BMP" + def get_command(self, file, **options): return ('start "Pillow" /WAIT "%s" ' '&& ping -n 2 127.0.0.1 >NUL ' @@ -113,11 +118,13 @@ elif sys.platform == "darwin": class MacViewer(Viewer): format = "BMP" + def get_command(self, file, **options): # on darwin open returns immediately resulting in the temp # file removal while app is opening command = "open -a /Applications/Preview.app" - command = "(%s %s; sleep 20; rm -f %s)&" % (command, quote(file), quote(file)) + command = "(%s %s; sleep 20; rm -f %s)&" % (command, quote(file), + quote(file)) return command register(MacViewer) @@ -140,7 +147,8 @@ else: class UnixViewer(Viewer): def show_file(self, file, **options): command, executable = self.get_command_ex(file, **options) - command = "(%s %s; rm -f %s)&" % (command, quote(file), quote(file)) + command = "(%s %s; rm -f %s)&" % (command, quote(file), + quote(file)) os.system(command) return 1 diff --git a/PIL/ImageStat.py b/PIL/ImageStat.py index d84e2cbf1..f3c138b3a 100644 --- a/PIL/ImageStat.py +++ b/PIL/ImageStat.py @@ -21,20 +21,21 @@ # See the README file for information on usage and redistribution. # -import operator, math -from functools import reduce +import math +import operator +import functools -class Stat: +class Stat(object): - def __init__(self, image_or_list, mask = None): + def __init__(self, image_or_list, mask=None): try: if mask: self.h = image_or_list.histogram(mask) else: self.h = image_or_list.histogram() except AttributeError: - self.h = image_or_list # assume it to be a histogram list + self.h = image_or_list # assume it to be a histogram list if not isinstance(self.h, list): raise TypeError("first argument must be image or list") self.bands = list(range(len(self.h) // 256)) @@ -58,7 +59,7 @@ class Stat: if histogram[i]: n = min(n, i) x = max(x, i) - return n, x # returns (255, 0) if there's no data in the histogram + return n, x # returns (255, 0) if there's no data in the histogram v = [] for i in range(0, len(self.h), 256): @@ -70,7 +71,7 @@ class Stat: v = [] for i in range(0, len(self.h), 256): - v.append(reduce(operator.add, self.h[i:i+256])) + v.append(functools.reduce(operator.add, self.h[i:i+256])) return v def _getsum(self): @@ -78,10 +79,10 @@ class Stat: v = [] for i in range(0, len(self.h), 256): - sum = 0.0 + layerSum = 0.0 for j in range(256): - sum += j * self.h[i + j] - v.append(sum) + layerSum += j * self.h[i + j] + v.append(layerSum) return v def _getsum2(self): @@ -126,7 +127,6 @@ class Stat: v.append(math.sqrt(self.sum2[i] / self.count[i])) return v - def _getvar(self): "Get variance for each layer" @@ -144,4 +144,4 @@ class Stat: v.append(math.sqrt(self.var[i])) return v -Global = Stat # compatibility +Global = Stat # compatibility diff --git a/PIL/ImageTk.py b/PIL/ImageTk.py index 1e81d240e..68d388e74 100644 --- a/PIL/ImageTk.py +++ b/PIL/ImageTk.py @@ -40,21 +40,23 @@ from PIL import Image _pilbitmap_ok = None + def _pilbitmap_check(): global _pilbitmap_ok if _pilbitmap_ok is None: try: - im = Image.new("1", (1,1)) + im = Image.new("1", (1, 1)) tkinter.BitmapImage(data="PIL:%d" % im.im.id) _pilbitmap_ok = 1 except tkinter.TclError: _pilbitmap_ok = 0 return _pilbitmap_ok + # -------------------------------------------------------------------- # PhotoImage -class PhotoImage: +class PhotoImage(object): """ A Tkinter-compatible photo image. This can be used everywhere Tkinter expects an image object. If the image is an RGBA @@ -95,7 +97,7 @@ class PhotoImage: try: mode = image.palette.mode except AttributeError: - mode = "RGB" # default + mode = "RGB" # default size = image.size kw["width"], kw["height"] = size else: @@ -118,8 +120,7 @@ class PhotoImage: try: self.__photo.tk.call("image", "delete", name) except: - pass # ignore internal errors - + pass # ignore internal errors def __str__(self): """ @@ -131,7 +132,6 @@ class PhotoImage: """ return str(self.__photo) - def width(self): """ Get the width of the image. @@ -140,7 +140,6 @@ class PhotoImage: """ return self.__size[0] - def height(self): """ Get the height of the image. @@ -149,7 +148,6 @@ class PhotoImage: """ return self.__size[1] - def paste(self, im, box=None): """ Paste a PIL image into the photo image. Note that this can @@ -170,13 +168,13 @@ class PhotoImage: block = image else: block = image.new_block(self.__mode, im.size) - image.convert2(block, image) # convert directly between buffers + image.convert2(block, image) # convert directly between buffers tk = self.__photo.tk try: tk.call("PyImagingPhoto", self.__photo, block.id) - except tkinter.TclError as v: + except tkinter.TclError: # activate Tkinter hook try: from PIL import _imagingtk @@ -186,13 +184,13 @@ class PhotoImage: _imagingtk.tkinit(id(tk), 0) tk.call("PyImagingPhoto", self.__photo, block.id) except (ImportError, AttributeError, tkinter.TclError): - raise # configuration problem; cannot attach to Tkinter + raise # configuration problem; cannot attach to Tkinter # -------------------------------------------------------------------- # BitmapImage -class BitmapImage: +class BitmapImage(object): """ A Tkinter-compatible bitmap image. This can be used everywhere Tkinter @@ -226,7 +224,7 @@ class BitmapImage: # fast way (requires the pilbitmap booster patch) image.load() kw["data"] = "PIL:%d" % image.im.id - self.__im = image # must keep a reference + self.__im = image # must keep a reference else: # slow but safe way kw["data"] = image.tobitmap() @@ -238,8 +236,7 @@ class BitmapImage: try: self.__photo.tk.call("image", "delete", name) except: - pass # ignore internal errors - + pass # ignore internal errors def width(self): """ @@ -249,7 +246,6 @@ class BitmapImage: """ return self.__size[0] - def height(self): """ Get the height of the image. @@ -258,7 +254,6 @@ class BitmapImage: """ return self.__size[1] - def __str__(self): """ Get the Tkinter bitmap image identifier. This method is automatically @@ -274,6 +269,7 @@ def getimage(photo): """Copies the contents of a PhotoImage to a PIL image memory.""" photo.tk.call("PyImagingPhotoGet", photo) + # -------------------------------------------------------------------- # Helper for the Image.show method. @@ -286,7 +282,7 @@ def _show(image, title): else: self.image = PhotoImage(im, master=master) tkinter.Label.__init__(self, master, image=self.image, - bg="black", bd=0) + bg="black", bd=0) if not tkinter._default_root: raise IOError("tkinter not initialized") diff --git a/PIL/ImageTransform.py b/PIL/ImageTransform.py index 5a8f9e9ec..81f90502c 100644 --- a/PIL/ImageTransform.py +++ b/PIL/ImageTransform.py @@ -15,16 +15,20 @@ from PIL import Image + class Transform(Image.ImageTransformHandler): def __init__(self, data): self.data = data + def getdata(self): return self.method, self.data + def transform(self, size, image, **options): # can be overridden method, data = self.getdata() return image.transform(size, method, data, **options) + ## # Define an affine image transform. #

@@ -43,9 +47,11 @@ class Transform(Image.ImageTransformHandler): # the first two rows from an affine transform matrix. # @see Image#Image.transform + class AffineTransform(Transform): method = Image.AFFINE + ## # Define a transform to extract a subregion from an image. #

@@ -68,6 +74,7 @@ class AffineTransform(Transform): class ExtentTransform(Transform): method = Image.EXTENT + ## # Define an quad image transform. #

@@ -83,6 +90,7 @@ class ExtentTransform(Transform): class QuadTransform(Transform): method = Image.QUAD + ## # Define an mesh image transform. A mesh transform consists of one # or more individual quad transforms. diff --git a/PIL/ImageWin.py b/PIL/ImageWin.py index aa90b887b..bcb54bc3e 100644 --- a/PIL/ImageWin.py +++ b/PIL/ImageWin.py @@ -21,30 +21,33 @@ import warnings from PIL import Image -class HDC: +class HDC(object): """ - Wraps a HDC integer. The resulting object can be passed to the + Wraps an HDC integer. The resulting object can be passed to the :py:meth:`~PIL.ImageWin.Dib.draw` and :py:meth:`~PIL.ImageWin.Dib.expose` methods. """ def __init__(self, dc): self.dc = dc + def __int__(self): return self.dc -class HWND: + +class HWND(object): """ - Wraps a HWND integer. The resulting object can be passed to the + Wraps an HWND integer. The resulting object can be passed to the :py:meth:`~PIL.ImageWin.Dib.draw` and :py:meth:`~PIL.ImageWin.Dib.expose` methods, instead of a DC. """ def __init__(self, wnd): self.wnd = wnd + def __int__(self): return self.wnd -class Dib: +class Dib(object): """ A Windows bitmap with the given mode and size. The mode can be one of "1", "L", "P", or "RGB". @@ -79,13 +82,12 @@ class Dib: if image: self.paste(image) - def expose(self, handle): """ Copy the bitmap contents to a device context. - :param handle: Device context (HDC), cast to a Python integer, or a HDC - or HWND instance. In PythonWin, you can use the + :param handle: Device context (HDC), cast to a Python integer, or an + HDC or HWND instance. In PythonWin, you can use the :py:meth:`CDC.GetHandleAttrib` to get a suitable handle. """ if isinstance(handle, HWND): @@ -109,7 +111,7 @@ class Dib: necessary. """ if not src: - src = (0,0) + self.size + src = (0, 0) + self.size if isinstance(handle, HWND): dc = self.image.getdc(handle) try: @@ -120,7 +122,6 @@ class Dib: result = self.image.draw(handle, dst, src) return result - def query_palette(self, handle): """ Installs the palette associated with the image in the given device @@ -146,7 +147,6 @@ class Dib: result = self.image.query_palette(handle) return result - def paste(self, im, box=None): """ Paste a PIL image into the bitmap image. @@ -166,7 +166,6 @@ class Dib: else: self.image.paste(im.im) - def frombytes(self, buffer): """ Load display memory contents from byte data. @@ -176,7 +175,6 @@ class Dib: """ return self.image.frombytes(buffer) - def tobytes(self): """ Copy display memory contents to bytes object. @@ -204,10 +202,11 @@ class Dib: ) return self.tobytes() + ## # Create a Window with the given title size. -class Window: +class Window(object): def __init__(self, title="PIL", width=None, height=None): self.hwnd = Image.core.createwindow( @@ -235,6 +234,7 @@ class Window: def mainloop(self): Image.core.eventloop() + ## # Create an image window which displays the given image. diff --git a/PIL/ImtImagePlugin.py b/PIL/ImtImagePlugin.py index e68b00344..f512eb801 100644 --- a/PIL/ImtImagePlugin.py +++ b/PIL/ImtImagePlugin.py @@ -26,6 +26,7 @@ from PIL import Image, ImageFile field = re.compile(br"([a-z]*) ([^ \r\n]*)") + ## # Image plugin for IM Tools images. @@ -39,7 +40,7 @@ class ImtImageFile(ImageFile.ImageFile): # Quick rejection: if there's not a LF among the first # 100 bytes, this is (probably) not a text header. - if not b"\n" in self.fp.read(100): + if b"\n" not in self.fp.read(100): raise SyntaxError("not an IM file") self.fp.seek(0) @@ -54,7 +55,7 @@ class ImtImageFile(ImageFile.ImageFile): if s == b'\x0C': # image data begins - self.tile = [("raw", (0,0)+self.size, + self.tile = [("raw", (0, 0)+self.size, self.fp.tell(), (self.mode, 0, 1))] @@ -68,12 +69,12 @@ class ImtImageFile(ImageFile.ImageFile): if len(s) == 1 or len(s) > 100: break if s[0] == b"*": - continue # comment + continue # comment m = field.match(s) if not m: break - k, v = m.group(1,2) + k, v = m.group(1, 2) if k == "width": xsize = int(v) self.size = xsize, ysize diff --git a/PIL/IptcImagePlugin.py b/PIL/IptcImagePlugin.py index dc8607591..47c7e1936 100644 --- a/PIL/IptcImagePlugin.py +++ b/PIL/IptcImagePlugin.py @@ -222,7 +222,7 @@ def getiptcinfo(im): offset += 2 # resource name (usually empty) name_len = i8(app[offset]) - name = app[offset+1:offset+1+name_len] + # name = app[offset+1:offset+1+name_len] offset = 1 + offset + name_len if offset & 1: offset += 1 @@ -251,7 +251,7 @@ def getiptcinfo(im): return None # no properties # create an IptcImagePlugin object without initializing it - class FakeImage: + class FakeImage(object): pass im = FakeImage() im.__class__ = IptcImageFile diff --git a/PIL/Jpeg2KImagePlugin.py b/PIL/Jpeg2KImagePlugin.py index 53b10ca1a..ed3e1530a 100644 --- a/PIL/Jpeg2KImagePlugin.py +++ b/PIL/Jpeg2KImagePlugin.py @@ -12,14 +12,13 @@ # # See the README file for information on usage and redistribution. # - -__version__ = "0.1" - from PIL import Image, ImageFile import struct import os import io +__version__ = "0.1" + def _parse_codestream(fp): """Parse the JPEG 2000 codestream to extract the size and component @@ -72,7 +71,7 @@ def _parse_jp2_header(fp): if lbox < hlen: raise SyntaxError('Invalid JP2 header length') - + if tbox == b'jp2h': header = fp.read(lbox - hlen) break @@ -208,8 +207,8 @@ class Jpeg2KImageFile(ImageFile.ImageFile): def _accept(prefix): - return (prefix[:4] == b'\xff\x4f\xff\x51' - or prefix[:12] == b'\x00\x00\x00\x0cjP \x0d\x0a\x87\x0a') + return (prefix[:4] == b'\xff\x4f\xff\x51' or + prefix[:12] == b'\x00\x00\x00\x0cjP \x0d\x0a\x87\x0a') # ------------------------------------------------------------ diff --git a/PIL/JpegImagePlugin.py b/PIL/JpegImagePlugin.py index 9cbab6b61..5cae90073 100644 --- a/PIL/JpegImagePlugin.py +++ b/PIL/JpegImagePlugin.py @@ -4,7 +4,7 @@ # # JPEG (JFIF) file handling # -# See "Digital Compression and Coding of Continous-Tone Still Images, +# See "Digital Compression and Coding of Continuous-Tone Still Images, # Part 1, Requirements and Guidelines" (CCITT T.81 / ISO 10918-1) # # History: @@ -115,7 +115,8 @@ def APP(self, marker): elif marker == 0xFFE2 and s[:4] == b"MPF\0": # extract MPO information self.info["mp"] = s[4:] - # offset is current location minus buffer size plus constant header size + # offset is current location minus buffer size + # plus constant header size self.info["mpoffset"] = self.fp.tell() - n + 4 @@ -321,7 +322,8 @@ class JpegImageFile(ImageFile.ImageFile): rawmode = self.mode if self.mode == "CMYK": rawmode = "CMYK;I" # assume adobe conventions - self.tile = [("jpeg", (0, 0) + self.size, 0, (rawmode, ""))] + self.tile = [("jpeg", (0, 0) + self.size, 0, + (rawmode, ""))] # self.__offset = self.fp.tell() break s = self.fp.read(1) @@ -353,7 +355,7 @@ class JpegImageFile(ImageFile.ImageFile): scale = s self.tile = [(d, e, o, a)] - self.decoderconfig = (scale, 1) + self.decoderconfig = (scale, 0) return self @@ -452,13 +454,13 @@ def _getmp(self): data = self.info["mp"] except KeyError: return None - file = io.BytesIO(data) - head = file.read(8) + 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) + info.load(file_contents) for key, value in info.items(): mp[key] = _fixup(value) # it's an error not to have a number of images @@ -472,14 +474,18 @@ def _getmp(self): for entrynum in range(0, quant): rawmpentry = mp[0xB002][entrynum * 16:(entrynum + 1) * 16] unpackedentry = unpack('{0}LLLHH'.format(endianness), rawmpentry) - labels = ('Attribute', 'Size', 'DataOffset', 'EntryNo1', 'EntryNo2') + labels = ('Attribute', 'Size', 'DataOffset', 'EntryNo1', + 'EntryNo2') mpentry = dict(zip(labels, unpackedentry)) mpentryattr = { - 'DependentParentImageFlag': bool(mpentry['Attribute'] & (1<<31)), - 'DependentChildImageFlag': bool(mpentry['Attribute'] & (1<<30)), - 'RepresentativeImageFlag': bool(mpentry['Attribute'] & (1<<29)), - 'Reserved': (mpentry['Attribute'] & (3<<27)) >> 27, - 'ImageDataFormat': (mpentry['Attribute'] & (7<<24)) >> 24, + 'DependentParentImageFlag': bool(mpentry['Attribute'] & + (1 << 31)), + 'DependentChildImageFlag': bool(mpentry['Attribute'] & + (1 << 30)), + 'RepresentativeImageFlag': bool(mpentry['Attribute'] & + (1 << 29)), + 'Reserved': (mpentry['Attribute'] & (3 << 27)) >> 27, + 'ImageDataFormat': (mpentry['Attribute'] & (7 << 24)) >> 24, 'MPType': mpentry['Attribute'] & 0x00FFFFFF } if mpentryattr['ImageDataFormat'] == 0: @@ -496,7 +502,7 @@ def _getmp(self): 0x030000: 'Baseline MP Primary Image' } mpentryattr['MPType'] = mptypemap.get(mpentryattr['MPType'], - 'Unknown') + 'Unknown') mpentry['Attribute'] = mpentryattr mpentries.append(mpentry) mp[0xB002] = mpentries @@ -530,11 +536,10 @@ zigzag_index = ( 0, 1, 5, 6, 14, 15, 27, 28, 21, 34, 37, 47, 50, 56, 59, 61, 35, 36, 48, 49, 57, 58, 62, 63) -samplings = { - (1, 1, 1, 1, 1, 1): 0, +samplings = {(1, 1, 1, 1, 1, 1): 0, (2, 1, 1, 1, 1, 1): 1, (2, 2, 1, 1, 1, 1): 2, - } + } def convert_dict_qtables(qtables): @@ -545,6 +550,15 @@ def convert_dict_qtables(qtables): def get_sampling(im): + # There's no subsampling when image have only 1 layer + # (grayscale images) or when they are CMYK (4 layers), + # so set subsampling to default value. + # + # NOTE: currently Pillow can't encode JPEG to YCCK format. + # If YCCK support is added in the future, subsampling code will have + # to be updated (here and in JpegEncode.c) to deal with 4 layers. + if not hasattr(im, 'layers') or im.layers in (1, 4): + return -1 sampling = im.layer[0][1:3] + im.layer[1][1:3] + im.layer[2][1:3] return samplings.get(sampling, -1) @@ -589,7 +603,8 @@ def _save(im, fp, filename): subsampling = 2 elif subsampling == "keep": if im.format != "JPEG": - raise ValueError("Cannot use 'keep' when original image is not a JPEG") + raise ValueError( + "Cannot use 'keep' when original image is not a JPEG") subsampling = get_sampling(im) def validate_qtables(qtables): @@ -623,7 +638,8 @@ def _save(im, fp, filename): if qtables == "keep": if im.format != "JPEG": - raise ValueError("Cannot use 'keep' when original image is not a JPEG") + raise ValueError( + "Cannot use 'keep' when original image is not a JPEG") qtables = getattr(im, "quantization", None) qtables = validate_qtables(qtables) @@ -641,7 +657,8 @@ def _save(im, fp, filename): i = 1 for marker in markers: size = struct.pack(">H", 2 + ICC_OVERHEAD_LEN + len(marker)) - extra += b"\xFF\xE2" + size + b"ICC_PROFILE\0" + o8(i) + o8(len(markers)) + marker + extra += (b"\xFF\xE2" + size + b"ICC_PROFILE\0" + o8(i) + + o8(len(markers)) + marker) i += 1 # get keyword arguments @@ -667,7 +684,8 @@ def _save(im, fp, filename): # https://github.com/jdriscoll/django-imagekit/issues/50 bufsize = 0 if "optimize" in info or "progressive" in info or "progression" in info: - if quality >= 95: + # keep sets quality to 0, but the actual value may be high. + if quality >= 95 or quality == 0: bufsize = 2 * im.size[0] * im.size[1] else: bufsize = im.size[0] * im.size[1] @@ -686,7 +704,7 @@ def _save_cjpeg(im, fp, filename): tempfile = im._dump() subprocess.check_call(["cjpeg", "-outfile", filename, tempfile]) try: - os.unlink(file) + os.unlink(tempfile) except: pass diff --git a/PIL/JpegPresets.py b/PIL/JpegPresets.py index e7bec148a..67af9ac9a 100644 --- a/PIL/JpegPresets.py +++ b/PIL/JpegPresets.py @@ -48,8 +48,8 @@ You can get the quantization tables of a JPEG with:: im.quantization -This will return a dict with a number of arrays. You can pass this dict directly -as the qtables argument when saving a JPEG. +This will return a dict with a number of arrays. You can pass this dict +directly as the qtables argument when saving a JPEG. The tables format between im.quantization and quantization in presets differ in 3 ways: @@ -67,7 +67,7 @@ Libjpeg ref.: http://www.jpegcameras.com/libjpeg/libjpeg-3.html """ presets = { - 'web_low': {'subsampling': 2, # "4:1:1" + 'web_low': {'subsampling': 2, # "4:1:1" 'quantization': [ [20, 16, 25, 39, 50, 46, 62, 68, 16, 18, 23, 38, 38, 53, 65, 68, @@ -86,7 +86,7 @@ presets = { 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68] ]}, - 'web_medium': {'subsampling': 2, # "4:1:1" + 'web_medium': {'subsampling': 2, # "4:1:1" 'quantization': [ [16, 11, 11, 16, 23, 27, 31, 30, 11, 12, 12, 15, 20, 23, 23, 30, @@ -105,7 +105,7 @@ presets = { 38, 35, 46, 53, 64, 64, 64, 64, 48, 43, 53, 64, 64, 64, 64, 64] ]}, - 'web_high': {'subsampling': 0, # "4:4:4" + 'web_high': {'subsampling': 0, # "4:4:4" 'quantization': [ [ 6, 4, 4, 6, 9, 11, 12, 16, 4, 5, 5, 6, 8, 10, 12, 12, @@ -124,7 +124,7 @@ presets = { 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31] ]}, - 'web_very_high': {'subsampling': 0, # "4:4:4" + 'web_very_high': {'subsampling': 0, # "4:4:4" 'quantization': [ [ 2, 2, 2, 2, 3, 4, 5, 6, 2, 2, 2, 2, 3, 4, 5, 6, @@ -143,7 +143,7 @@ presets = { 15, 12, 12, 12, 12, 12, 12, 12, 15, 12, 12, 12, 12, 12, 12, 12] ]}, - 'web_maximum': {'subsampling': 0, # "4:4:4" + 'web_maximum': {'subsampling': 0, # "4:4:4" 'quantization': [ [ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, @@ -162,7 +162,7 @@ presets = { 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3] ]}, - 'low': {'subsampling': 2, # "4:1:1" + 'low': {'subsampling': 2, # "4:1:1" 'quantization': [ [18, 14, 14, 21, 30, 35, 34, 17, 14, 16, 16, 19, 26, 23, 12, 12, @@ -181,7 +181,7 @@ presets = { 17, 12, 12, 12, 12, 12, 12, 12, 17, 12, 12, 12, 12, 12, 12, 12] ]}, - 'medium': {'subsampling': 2, # "4:1:1" + 'medium': {'subsampling': 2, # "4:1:1" 'quantization': [ [12, 8, 8, 12, 17, 21, 24, 17, 8, 9, 9, 11, 15, 19, 12, 12, @@ -200,7 +200,7 @@ presets = { 17, 12, 12, 12, 12, 12, 12, 12, 17, 12, 12, 12, 12, 12, 12, 12] ]}, - 'high': {'subsampling': 0, # "4:4:4" + 'high': {'subsampling': 0, # "4:4:4" 'quantization': [ [ 6, 4, 4, 6, 9, 11, 12, 16, 4, 5, 5, 6, 8, 10, 12, 12, @@ -219,7 +219,7 @@ presets = { 17, 12, 12, 12, 12, 12, 12, 12, 17, 12, 12, 12, 12, 12, 12, 12] ]}, - 'maximum': {'subsampling': 0, # "4:4:4" + 'maximum': {'subsampling': 0, # "4:4:4" 'quantization': [ [ 2, 2, 2, 2, 3, 4, 5, 6, 2, 2, 2, 2, 3, 4, 5, 6, @@ -238,4 +238,4 @@ presets = { 15, 12, 12, 12, 12, 12, 12, 12, 15, 12, 12, 12, 12, 12, 12, 12] ]}, -} \ No newline at end of file +} diff --git a/PIL/McIdasImagePlugin.py b/PIL/McIdasImagePlugin.py index 3aef10ba8..c3f255fd2 100644 --- a/PIL/McIdasImagePlugin.py +++ b/PIL/McIdasImagePlugin.py @@ -21,9 +21,11 @@ __version__ = "0.2" import struct from PIL import Image, ImageFile + def _accept(s): return s[:8] == b"\x00\x00\x00\x00\x00\x00\x00\x04" + ## # Image plugin for McIdas area images. @@ -47,10 +49,12 @@ class McIdasImageFile(ImageFile.ImageFile): mode = rawmode = "L" elif w[11] == 2: # FIXME: add memory map support - mode = "I"; rawmode = "I;16B" + mode = "I" + rawmode = "I;16B" elif w[11] == 4: # FIXME: add memory map support - mode = "I"; rawmode = "I;32B" + mode = "I" + rawmode = "I;32B" else: raise SyntaxError("unsupported McIdas format") diff --git a/PIL/MicImagePlugin.py b/PIL/MicImagePlugin.py index 84e962860..aa41bf359 100644 --- a/PIL/MicImagePlugin.py +++ b/PIL/MicImagePlugin.py @@ -21,7 +21,7 @@ __version__ = "0.1" from PIL import Image, TiffImagePlugin -from PIL.OleFileIO import * +from PIL.OleFileIO import MAGIC, OleFileIO # @@ -31,6 +31,7 @@ from PIL.OleFileIO import * def _accept(prefix): return prefix[:8] == MAGIC + ## # Image plugin for Microsoft's Image Composer file format. @@ -53,9 +54,9 @@ class MicImageFile(TiffImagePlugin.TiffImageFile): # best way to identify MIC files, but what the... ;-) self.images = [] - for file in self.ole.listdir(): - if file[1:] and file[0][-4:] == ".ACI" and file[1] == "Image": - self.images.append(file) + for path in self.ole.listdir(): + if path[1:] and path[0][-4:] == ".ACI" and path[1] == "Image": + self.images.append(path) # if we didn't find any images, this is probably not # an MIC file. @@ -70,6 +71,10 @@ class MicImageFile(TiffImagePlugin.TiffImageFile): self.seek(0) + @property + def n_frames(self): + return len(self.images) + def seek(self, frame): try: diff --git a/PIL/MpegImagePlugin.py b/PIL/MpegImagePlugin.py index 02e6adc00..ff7c0dce4 100644 --- a/PIL/MpegImagePlugin.py +++ b/PIL/MpegImagePlugin.py @@ -18,10 +18,11 @@ __version__ = "0.1" from PIL import Image, ImageFile from PIL._binary import i8 + # # Bitstream parser -class BitStream: +class BitStream(object): def __init__(self, fp): self.fp = fp @@ -52,6 +53,7 @@ class BitStream: self.bits = self.bits - bits return v + ## # Image plugin for MPEG streams. This plugin can identify a stream, # but it cannot read it. diff --git a/PIL/MpoImagePlugin.py b/PIL/MpoImagePlugin.py index d053d9026..9d21728b9 100644 --- a/PIL/MpoImagePlugin.py +++ b/PIL/MpoImagePlugin.py @@ -22,13 +22,16 @@ __version__ = "0.1" from PIL import Image, JpegImagePlugin + def _accept(prefix): return JpegImagePlugin._accept(prefix) + def _save(im, fp, filename): # Note that we can only save the current frame at present return JpegImagePlugin._save(im, fp, filename) + ## # Image plugin for MPO images. @@ -38,19 +41,19 @@ class MpoImageFile(JpegImagePlugin.JpegImageFile): format_description = "MPO (CIPA DC-007)" def _open(self): - self.fp.seek(0) # prep the fp in order to pass the JPEG test + self.fp.seek(0) # prep the fp in order to pass the JPEG test JpegImagePlugin.JpegImageFile._open(self) self.mpinfo = self._getmp() self.__framecount = self.mpinfo[0xB001] - self.__mpoffsets = [mpent['DataOffset'] + self.info['mpoffset'] \ - for mpent in self.mpinfo[0xB002]] + self.__mpoffsets = [mpent['DataOffset'] + self.info['mpoffset'] + for mpent in self.mpinfo[0xB002]] self.__mpoffsets[0] = 0 # Note that the following assertion will only be invalid if something # gets broken within JpegImagePlugin. assert self.__framecount == len(self.__mpoffsets) - del self.info['mpoffset'] # no longer needed - self.__fp = self.fp # FIXME: hack - self.__fp.seek(self.__mpoffsets[0]) # get ready to read first frame + del self.info['mpoffset'] # no longer needed + self.__fp = self.fp # FIXME: hack + self.__fp.seek(self.__mpoffsets[0]) # get ready to read first frame self.__frame = 0 self.offset = 0 # for now we can only handle reading and individual frame extraction @@ -59,6 +62,10 @@ class MpoImageFile(JpegImagePlugin.JpegImageFile): def load_seek(self, pos): self.__fp.seek(pos) + @property + def n_frames(self): + return self.__framecount + def seek(self, frame): if frame < 0 or frame >= self.__framecount: raise EOFError("no more images in MPO file") @@ -79,7 +86,7 @@ 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_open("MPO", JpegImagePlugin.jpeg_factory, _accept) Image.register_save("MPO", _save) Image.register_extension("MPO", ".mpo") diff --git a/PIL/MspImagePlugin.py b/PIL/MspImagePlugin.py index 743ebe172..1e974d53f 100644 --- a/PIL/MspImagePlugin.py +++ b/PIL/MspImagePlugin.py @@ -27,9 +27,11 @@ from PIL import Image, ImageFile, _binary i16 = _binary.i16le + def _accept(prefix): return prefix[:4] in [b"DanM", b"LinS"] + ## # Image plugin for Windows MSP images. This plugin supports both # uncompressed (Windows 1.0). @@ -47,25 +49,26 @@ class MspImageFile(ImageFile.ImageFile): raise SyntaxError("not an MSP file") # Header checksum - sum = 0 + checksum = 0 for i in range(0, 32, 2): - sum = sum ^ i16(s[i:i+2]) - if sum != 0: + checksum = checksum ^ i16(s[i:i+2]) + if checksum != 0: raise SyntaxError("bad MSP checksum") self.mode = "1" self.size = i16(s[4:]), i16(s[6:]) if s[:4] == b"DanM": - self.tile = [("raw", (0,0)+self.size, 32, ("1", 0, 1))] + self.tile = [("raw", (0, 0)+self.size, 32, ("1", 0, 1))] else: - self.tile = [("msp", (0,0)+self.size, 32+2*self.size[1], None)] + self.tile = [("msp", (0, 0)+self.size, 32+2*self.size[1], None)] # # write MSP files (uncompressed only) o16 = _binary.o16le + def _save(im, fp, filename): if im.mode != "1": @@ -74,23 +77,23 @@ def _save(im, fp, filename): # create MSP header header = [0] * 16 - header[0], header[1] = i16(b"Da"), i16(b"nM") # version 1 + header[0], header[1] = i16(b"Da"), i16(b"nM") # version 1 header[2], header[3] = im.size header[4], header[5] = 1, 1 header[6], header[7] = 1, 1 header[8], header[9] = im.size - sum = 0 + checksum = 0 for h in header: - sum = sum ^ h - header[12] = sum # FIXME: is this the right field? + checksum = checksum ^ h + header[12] = checksum # FIXME: is this the right field? # header for h in header: fp.write(o16(h)) # image body - ImageFile._save(im, fp, [("raw", (0,0)+im.size, 32, ("1", 0, 1))]) + ImageFile._save(im, fp, [("raw", (0, 0)+im.size, 32, ("1", 0, 1))]) # # registry diff --git a/PIL/OleFileIO.py b/PIL/OleFileIO.py old mode 100644 new mode 100755 index e35bfa679..4cf106d97 --- a/PIL/OleFileIO.py +++ b/PIL/OleFileIO.py @@ -1,48 +1,70 @@ -#!/usr/local/bin/python -# -*- coding: latin-1 -*- -## OleFileIO_PL: -## Module to read Microsoft OLE2 files (also called Structured Storage or -## Microsoft Compound Document File Format), such as Microsoft Office -## documents, Image Composer and FlashPix files, Outlook messages, ... -## This version is compatible with Python 2.6+ and 3.x +#!/usr/bin/env python -## version 0.30 2014-02-04 Philippe Lagadec - http://www.decalage.info - -## Project website: http://www.decalage.info/python/olefileio - -## Improved version of the OleFileIO module from PIL library v1.1.6 -## See: http://www.pythonware.com/products/pil/index.htm - -## The Python Imaging Library (PIL) is - -## Copyright (c) 1997-2005 by Secret Labs AB -## Copyright (c) 1995-2005 by Fredrik Lundh - -## OleFileIO_PL changes are Copyright (c) 2005-2014 by Philippe Lagadec - -## See source code and LICENSE.txt for information on usage and redistribution. - -## WARNING: THIS IS (STILL) WORK IN PROGRESS. +# olefile (formerly OleFileIO_PL) version 0.42 2015-01-25 +# +# Module to read/write Microsoft OLE2 files (also called Structured Storage or +# Microsoft Compound Document File Format), such as Microsoft Office 97-2003 +# documents, Image Composer and FlashPix files, Outlook messages, ... +# This version is compatible with Python 2.6+ and 3.x +# +# Project website: http://www.decalage.info/olefile +# +# olefile is copyright (c) 2005-2015 Philippe Lagadec (http://www.decalage.info) +# +# olefile is based on the OleFileIO module from the PIL library v1.1.6 +# See: http://www.pythonware.com/products/pil/index.htm +# +# The Python Imaging Library (PIL) is +# Copyright (c) 1997-2005 by Secret Labs AB +# Copyright (c) 1995-2005 by Fredrik Lundh +# +# See source code and LICENSE.txt for information on usage and redistribution. -# Starting with OleFileIO_PL v0.30, only Python 2.6+ and 3.x is supported +# Since OleFileIO_PL v0.30, only Python 2.6+ and 3.x is supported # This import enables print() as a function rather than a keyword # (main requirement to be compatible with Python 3.x) # The comment on the line below should be printed on Python 2.5 or older: -from __future__ import print_function # This version of OleFileIO_PL requires Python 2.6+ or 3.x. +from __future__ import print_function # This version of olefile requires Python 2.6+ or 3.x. -__author__ = "Philippe Lagadec, Fredrik Lundh (Secret Labs AB)" -__date__ = "2014-02-04" -__version__ = '0.30' +__author__ = "Philippe Lagadec" +__date__ = "2015-01-25" +__version__ = '0.42b' #--- LICENSE ------------------------------------------------------------------ -# OleFileIO_PL is an improved version of the OleFileIO module from the -# Python Imaging Library (PIL). - -# OleFileIO_PL changes are Copyright (c) 2005-2014 by Philippe Lagadec +# olefile (formerly OleFileIO_PL) is copyright (c) 2005-2015 Philippe Lagadec +# (http://www.decalage.info) # +# 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. + +# ---------- +# PIL License: +# +# 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 (c) 1995-2005 by Fredrik Lundh @@ -68,7 +90,7 @@ __version__ = '0.30' # PERFORMANCE OF THIS SOFTWARE. #----------------------------------------------------------------------------- -# CHANGELOG: (only OleFileIO_PL changes compared to PIL 1.1.6) +# CHANGELOG: (only olefile/OleFileIO_PL changes compared to PIL 1.1.6) # 2005-05-11 v0.10 PL: - a few fixes for Python 2.4 compatibility # (all changes flagged with [PL]) # 2006-02-22 v0.11 PL: - a few fixes for some Office 2003 documents which raise @@ -143,10 +165,29 @@ __version__ = '0.30' # 2014-02-04 v0.30 PL: - upgraded code to support Python 3.x by Martin Panter # - several fixes for Python 2.6 (xrange, MAGIC) # - reused i32 from Pillow's _binary +# 2014-07-18 v0.31 - preliminary support for 4K sectors +# 2014-07-27 v0.31 PL: - a few improvements in OleFileIO.open (header parsing) +# - Fixed loadfat for large files with 4K sectors (issue #3) +# 2014-07-30 v0.32 PL: - added write_sect to write sectors to disk +# - added write_mode option to OleFileIO.__init__ and open +# 2014-07-31 PL: - fixed padding in write_sect for Python 3, added checks +# - added write_stream to write a stream to disk +# 2014-09-26 v0.40 PL: - renamed OleFileIO_PL to olefile +# 2014-11-09 NE: - added support for Jython (Niko Ehrenfeuchter) +# 2014-11-13 v0.41 PL: - improved isOleFile and OleFileIO.open to support OLE +# data in a string buffer and file-like objects. +# 2014-11-21 PL: - updated comments according to Pillow's commits +# 2015-01-24 v0.42 PL: - changed the default path name encoding from Latin-1 +# to UTF-8 on Python 2.x (Unicode on Python 3.x) +# - added path_encoding option to override the default +# - fixed a bug in _list when a storage is empty #----------------------------------------------------------------------------- # TODO (for version 1.0): -# + isOleFile should accept file-like objects like open +# + get rid of print statements, to simplify Python 2.x and 3.x support +# + add is_stream and is_storage +# + remove leading and trailing slashes where a path is used +# + add functions path_list2str and path_str2list # + fix how all the methods handle unicode str and/or bytes as arguments # + add path attrib to _OleDirEntry, set it once and for all in init or # append_kids (then listdir/_list can be simplified) @@ -178,30 +219,16 @@ __version__ = '0.30' # - move all debug code (and maybe dump methods) to a separate module, with # a class which inherits OleFileIO ? # - fix docstrings to follow epydoc format -# - add support for 4K sectors ? # - add support for big endian byte order ? # - create a simple OLE explorer with wxPython # FUTURE EVOLUTIONS to add write support: -# 1) add ability to write a stream back on disk from BytesIO (same size, no -# change in FAT/MiniFAT). -# 2) rename a stream/storage if it doesn't change the RB tree -# 3) use rbtree module to update the red-black tree + any rename -# 4) remove a stream/storage: free sectors in FAT/MiniFAT -# 5) allocate new sectors in FAT/MiniFAT -# 6) create new storage/stream -#----------------------------------------------------------------------------- +# see issue #6 on Bitbucket: +# https://bitbucket.org/decalage/olefileio_pl/issue/6/improve-olefileio_pl-to-write-ole-files + +#----------------------------------------------------------------------------- +# NOTES from PIL 1.1.6: -# -# THIS IS WORK IN PROGRESS -# -# The Python Imaging Library -# $Id$ -# -# stuff to deal with OLE2 Structured Storage files. this module is -# used by PIL to read Image Composer and FlashPix files, but can also -# be used to read other files of this type. -# # History: # 1997-01-20 fl Created # 1997-01-22 fl Fixed 64-bit portability quirk @@ -223,22 +250,22 @@ __version__ = '0.30' # "If this document and functionality of the Software conflict, # the actual functionality of the Software represents the correct # functionality" -- Microsoft, in the OLE format specification -# -# Copyright (c) Secret Labs AB 1997. -# Copyright (c) Fredrik Lundh 1997. -# -# See the README file for information on usage and redistribution. -# #------------------------------------------------------------------------------ import io import sys -import struct, array, os.path, datetime +import struct +import array +import os.path +import datetime -#[PL] Define explicitly the public API to avoid private objects in pydoc: -__all__ = ['OleFileIO', 'isOleFile', 'MAGIC'] +#=== COMPATIBILITY WORKAROUNDS ================================================ + +# [PL] Define explicitly the public API to avoid private objects in pydoc: +#TODO: add more +# __all__ = ['OleFileIO', 'isOleFile', 'MAGIC'] # For Python 3.x, need to redefine long as int: if str is not bytes: @@ -252,48 +279,73 @@ except: # no xrange, for Python 3 it was renamed as range: iterrange = range -#[PL] workaround to fix an issue with array item size on 64 bits systems: +# [PL] workaround to fix an issue with array item size on 64 bits systems: if array.array('L').itemsize == 4: # on 32 bits platforms, long integers in an array are 32 bits: UINT32 = 'L' elif array.array('I').itemsize == 4: # on 64 bits platforms, integers in an array are 32 bits: UINT32 = 'I' +elif array.array('i').itemsize == 4: + # On 64 bit Jython, signed integers ('i') are the only way to store our 32 + # bit values in an array in a *somewhat* reasonable way, as the otherwise + # perfectly suited 'H' (unsigned int, 32 bits) results in a completely + # unusable behaviour. This is most likely caused by the fact that Java + # doesn't have unsigned values, and thus Jython's "array" implementation, + # which is based on "jarray", doesn't have them either. + # NOTE: to trick Jython into converting the values it would normally + # interpret as "signed" into "unsigned", a binary-and operation with + # 0xFFFFFFFF can be used. This way it is possible to use the same comparing + # operations on all platforms / implementations. The corresponding code + # lines are flagged with a 'JYTHON-WORKAROUND' tag below. + UINT32 = 'i' else: raise ValueError('Need to fix a bug with 32 bit arrays, please contact author...') -#[PL] These workarounds were inspired from the Path module +# [PL] These workarounds were inspired from the Path module # (see http://www.jorendorff.com/articles/python/path/) -#TODO: test with old Python versions - -# Pre-2.3 workaround for basestring. try: basestring except NameError: - try: - # is Unicode supported (Python >2.0 or >1.6 ?) - basestring = (str, unicode) - except NameError: - basestring = str + basestring = str -#[PL] Experimental setting: if True, OLE filenames will be kept in Unicode +# [PL] Experimental setting: if True, OLE filenames will be kept in Unicode # if False (default PIL behaviour), all filenames are converted to Latin-1. -KEEP_UNICODE_NAMES = False +KEEP_UNICODE_NAMES = True -#[PL] DEBUG display mode: False by default, use set_debug_mode() or "-d" on +if sys.version_info[0] < 3: + # On Python 2.x, the default encoding for path names is UTF-8: + DEFAULT_PATH_ENCODING = 'utf-8' +else: + # On Python 3.x, the default encoding for path names is Unicode (None): + DEFAULT_PATH_ENCODING = None + + +#=== DEBUGGING =============================================================== + +#TODO: replace this by proper logging + +# [PL] DEBUG display mode: False by default, use set_debug_mode() or "-d" on # command line to change it. DEBUG_MODE = False + + def debug_print(msg): print(msg) + + def debug_pass(msg): pass + + debug = debug_pass + def set_debug_mode(debug_mode): """ Set debug mode on or off, to control display of debugging messages. - mode: True or False + :param mode: True or False """ global DEBUG_MODE, debug DEBUG_MODE = debug_mode @@ -302,41 +354,45 @@ def set_debug_mode(debug_mode): else: debug = debug_pass + +#=== CONSTANTS =============================================================== + +# magic bytes that should be at the beginning of every OLE file: MAGIC = b'\xD0\xCF\x11\xE0\xA1\xB1\x1A\xE1' -#[PL]: added constants for Sector IDs (from AAF specifications) -MAXREGSECT = 0xFFFFFFFA; # maximum SECT -DIFSECT = 0xFFFFFFFC; # (-4) denotes a DIFAT sector in a FAT -FATSECT = 0xFFFFFFFD; # (-3) denotes a FAT sector in a FAT -ENDOFCHAIN = 0xFFFFFFFE; # (-2) end of a virtual stream chain -FREESECT = 0xFFFFFFFF; # (-1) unallocated sector +# [PL]: added constants for Sector IDs (from AAF specifications) +MAXREGSECT = 0xFFFFFFFA # (-6) maximum SECT +DIFSECT = 0xFFFFFFFC # (-4) denotes a DIFAT sector in a FAT +FATSECT = 0xFFFFFFFD # (-3) denotes a FAT sector in a FAT +ENDOFCHAIN = 0xFFFFFFFE # (-2) end of a virtual stream chain +FREESECT = 0xFFFFFFFF # (-1) unallocated sector -#[PL]: added constants for Directory Entry IDs (from AAF specifications) -MAXREGSID = 0xFFFFFFFA; # maximum directory entry ID -NOSTREAM = 0xFFFFFFFF; # (-1) unallocated directory entry +# [PL]: added constants for Directory Entry IDs (from AAF specifications) +MAXREGSID = 0xFFFFFFFA # (-6) maximum directory entry ID +NOSTREAM = 0xFFFFFFFF # (-1) unallocated directory entry -#[PL] object types in storage (from AAF specifications) -STGTY_EMPTY = 0 # empty directory entry (according to OpenOffice.org doc) -STGTY_STORAGE = 1 # element is a storage object -STGTY_STREAM = 2 # element is a stream object -STGTY_LOCKBYTES = 3 # element is an ILockBytes object -STGTY_PROPERTY = 4 # element is an IPropertyStorage object -STGTY_ROOT = 5 # element is a root storage +# [PL] object types in storage (from AAF specifications) +STGTY_EMPTY = 0 # empty directory entry (according to OpenOffice.org doc) +STGTY_STORAGE = 1 # element is a storage object +STGTY_STREAM = 2 # element is a stream object +STGTY_LOCKBYTES = 3 # element is an ILockBytes object +STGTY_PROPERTY = 4 # element is an IPropertyStorage object +STGTY_ROOT = 5 # element is a root storage # # -------------------------------------------------------------------- # property types -VT_EMPTY=0; VT_NULL=1; VT_I2=2; VT_I4=3; VT_R4=4; VT_R8=5; VT_CY=6; -VT_DATE=7; VT_BSTR=8; VT_DISPATCH=9; VT_ERROR=10; VT_BOOL=11; -VT_VARIANT=12; VT_UNKNOWN=13; VT_DECIMAL=14; VT_I1=16; VT_UI1=17; -VT_UI2=18; VT_UI4=19; VT_I8=20; VT_UI8=21; VT_INT=22; VT_UINT=23; -VT_VOID=24; VT_HRESULT=25; VT_PTR=26; VT_SAFEARRAY=27; VT_CARRAY=28; -VT_USERDEFINED=29; VT_LPSTR=30; VT_LPWSTR=31; VT_FILETIME=64; -VT_BLOB=65; VT_STREAM=66; VT_STORAGE=67; VT_STREAMED_OBJECT=68; -VT_STORED_OBJECT=69; VT_BLOB_OBJECT=70; VT_CF=71; VT_CLSID=72; -VT_VECTOR=0x1000; +VT_EMPTY = 0; VT_NULL = 1; VT_I2 = 2; VT_I4 = 3; VT_R4 = 4; VT_R8 = 5; VT_CY = 6; +VT_DATE = 7; VT_BSTR = 8; VT_DISPATCH = 9; VT_ERROR = 10; VT_BOOL = 11; +VT_VARIANT = 12; VT_UNKNOWN = 13; VT_DECIMAL = 14; VT_I1 = 16; VT_UI1 = 17; +VT_UI2 = 18; VT_UI4 = 19; VT_I8 = 20; VT_UI8 = 21; VT_INT = 22; VT_UINT = 23; +VT_VOID = 24; VT_HRESULT = 25; VT_PTR = 26; VT_SAFEARRAY = 27; VT_CARRAY = 28; +VT_USERDEFINED = 29; VT_LPSTR = 30; VT_LPWSTR = 31; VT_FILETIME = 64; +VT_BLOB = 65; VT_STREAM = 66; VT_STORAGE = 67; VT_STREAMED_OBJECT = 68; +VT_STORED_OBJECT = 69; VT_BLOB_OBJECT = 70; VT_CF = 71; VT_CLSID = 72; +VT_VECTOR = 0x1000; # map property id to name (for debugging purposes) @@ -352,31 +408,53 @@ for keyword, var in list(vars().items()): WORD_CLSID = "00020900-0000-0000-C000-000000000046" #TODO: check Excel, PPT, ... -#[PL]: Defect levels to classify parsing errors - see OleFileIO._raise_defect() -DEFECT_UNSURE = 10 # a case which looks weird, but not sure it's a defect -DEFECT_POTENTIAL = 20 # a potential defect -DEFECT_INCORRECT = 30 # an error according to specifications, but parsing - # can go on -DEFECT_FATAL = 40 # an error which cannot be ignored, parsing is - # impossible +# [PL]: Defect levels to classify parsing errors - see OleFileIO._raise_defect() +DEFECT_UNSURE = 10 # a case which looks weird, but not sure it's a defect +DEFECT_POTENTIAL = 20 # a potential defect +DEFECT_INCORRECT = 30 # an error according to specifications, but parsing + # can go on +DEFECT_FATAL = 40 # an error which cannot be ignored, parsing is + # impossible -#[PL] add useful constants to __all__: -for key in list(vars().keys()): - if key.startswith('STGTY_') or key.startswith('DEFECT_'): - __all__.append(key) +# Minimal size of an empty OLE file, with 512-bytes sectors = 1536 bytes +# (this is used in isOleFile and OleFile.open) +MINIMAL_OLEFILE_SIZE = 1536 + +# [PL] add useful constants to __all__: +# for key in list(vars().keys()): +# if key.startswith('STGTY_') or key.startswith('DEFECT_'): +# __all__.append(key) -#--- FUNCTIONS ---------------------------------------------------------------- +#=== FUNCTIONS =============================================================== -def isOleFile (filename): +def isOleFile(filename): """ - Test if file is an OLE container (according to its header). - - :param filename: file name or path (str, unicode) + Test if a file is an OLE container (according to the magic bytes in its header). + + :param filename: string-like or file-like object, OLE file to parse + + - if filename is a string smaller than 1536 bytes, it is the path + of the file to open. (bytes or unicode string) + - if filename is a string longer than 1535 bytes, it is parsed + as the content of an OLE file in memory. (bytes type only) + - if filename is a file-like object (with read and seek methods), + it is parsed as-is. + :returns: True if OLE, False otherwise. """ - f = open(filename, 'rb') - header = f.read(len(MAGIC)) + # check if filename is a string-like or file-like object: + if hasattr(filename, 'read'): + # file-like object: use it directly + header = filename.read(len(MAGIC)) + # just in case, seek back to start of file: + filename.seek(0) + elif isinstance(filename, bytes) and len(filename) >= MINIMAL_OLEFILE_SIZE: + # filename is a bytes string containing the OLE file to be parsed: + header = filename[:len(MAGIC)] + else: + # string-like object: filename of file on disk + header = open(filename, 'rb').read(len(MAGIC)) if header == MAGIC: return True else: @@ -399,23 +477,20 @@ def i16(c, o = 0): """ Converts a 2-bytes (16 bits) string to an integer. - :param c: string containing bytes to convert - :param o: offset of bytes to convert in string + c: string containing bytes to convert + o: offset of bytes to convert in string """ - return i8(c[o]) | (i8(c[o+1])<<8) + return struct.unpack("=len(fat): + if sect < 0 or sect >= len(fat): debug('sect=%d (%X) / len(fat)=%d' % (sect, sect, len(fat))) - debug('i=%d / nb_sectors=%d' %(i, nb_sectors)) + debug('i=%d / nb_sectors=%d' % (i, nb_sectors)) ## tmp_data = b"".join(data) ## f = open('test_debug.bin', 'wb') ## f.write(tmp_data) @@ -724,7 +771,7 @@ class _OleStream(io.BytesIO): # Note: if sector is the last of the file, sometimes it is not a # complete sector (of 512 or 4K), so we may read less than # sectorsize. - if len(sector_data)!=sectorsize and sect!=(len(fat)-1): + if len(sector_data) != sectorsize and sect != (len(fat)-1): debug('sect=%d / len(fat)=%d, seek=%d / filesize=%d, len read=%d' % (sect, len(fat), offset+sectorsize*sect, filesize, len(sector_data))) debug('seek+len(read)=%d' % (offset+sectorsize*sect+len(sector_data))) @@ -732,11 +779,11 @@ class _OleStream(io.BytesIO): data.append(sector_data) # jump to next sector in the FAT: try: - sect = fat[sect] + sect = fat[sect] & 0xFFFFFFFF # JYTHON-WORKAROUND except IndexError: # [PL] if pointer is out of the FAT an exception is raised raise IOError('incorrect OLE FAT, sector index out of range') - #[PL] Last sector should be a "end of chain" marker: + # [PL] Last sector should be a "end of chain" marker: if sect != ENDOFCHAIN: raise IOError('incorrect last sector index in OLE stream') data = b"".join(data) @@ -760,12 +807,12 @@ class _OleStream(io.BytesIO): #--- _OleDirectoryEntry ------------------------------------------------------- -class _OleDirectoryEntry: +class _OleDirectoryEntry(object): """ OLE2 Directory Entry """ - #[PL] parsing code moved from OleFileIO.loaddirectory + # [PL] parsing code moved from OleFileIO.loaddirectory # struct to parse directory entries: # <: little-endian byte order, standard sizes @@ -790,7 +837,6 @@ class _OleDirectoryEntry: DIRENTRY_SIZE = 128 assert struct.calcsize(STRUCT_DIRENTRY) == DIRENTRY_SIZE - def __init__(self, entry, sid, olefile): """ Constructor for an _OleDirectoryEntry object. @@ -831,23 +877,26 @@ class _OleDirectoryEntry: sizeHigh ) = struct.unpack(_OleDirectoryEntry.STRUCT_DIRENTRY, entry) if self.entry_type not in [STGTY_ROOT, STGTY_STORAGE, STGTY_STREAM, STGTY_EMPTY]: - olefile._raise_defect(DEFECT_INCORRECT, 'unhandled OLE storage type') + olefile.raise_defect(DEFECT_INCORRECT, 'unhandled OLE storage type') # only first directory entry can (and should) be root: if self.entry_type == STGTY_ROOT and sid != 0: - olefile._raise_defect(DEFECT_INCORRECT, 'duplicate OLE root entry') + olefile.raise_defect(DEFECT_INCORRECT, 'duplicate OLE root entry') if sid == 0 and self.entry_type != STGTY_ROOT: - olefile._raise_defect(DEFECT_INCORRECT, 'incorrect OLE root entry') + olefile.raise_defect(DEFECT_INCORRECT, 'incorrect OLE root entry') #debug (struct.unpack(fmt_entry, entry[:len_entry])) # name should be at most 31 unicode characters + null character, # so 64 bytes in total (31*2 + 2): - if namelength>64: - olefile._raise_defect(DEFECT_INCORRECT, 'incorrect DirEntry name length') + if namelength > 64: + olefile.raise_defect(DEFECT_INCORRECT, 'incorrect DirEntry name length') # if exception not raised, namelength is set to the maximum value: namelength = 64 # only characters without ending null char are kept: name = name[:(namelength-2)] - # name is converted from unicode to Latin-1: - self.name = _unicode(name) + #TODO: check if the name is actually followed by a null unicode character ([MS-CFB] 2.6.1) + #TODO: check if the name does not contain forbidden characters: + # [MS-CFB] 2.6.1: "The following characters are illegal and MUST NOT be part of the name: '/', '\', ':', '!'." + # name is converted from UTF-16LE to the path encoding specified in the OleFileIO: + self.name = olefile._decode_utf16_str(name) debug('DirEntry SID=%d: %s' % (self.sid, repr(self.name))) debug(' - type: %d' % self.entry_type) @@ -862,29 +911,27 @@ class _OleDirectoryEntry: if sizeHigh != 0 and sizeHigh != 0xFFFFFFFF: debug('sectorsize=%d, sizeLow=%d, sizeHigh=%d (%X)' % (olefile.sectorsize, sizeLow, sizeHigh, sizeHigh)) - olefile._raise_defect(DEFECT_UNSURE, 'incorrect OLE stream size') + olefile.raise_defect(DEFECT_UNSURE, 'incorrect OLE stream size') self.size = sizeLow else: - self.size = sizeLow + (long(sizeHigh)<<32) + self.size = sizeLow + (long(sizeHigh) << 32) debug(' - size: %d (sizeLow=%d, sizeHigh=%d)' % (self.size, sizeLow, sizeHigh)) self.clsid = _clsid(clsid) # a storage should have a null size, BUT some implementations such as # Word 8 for Mac seem to allow non-null values => Potential defect: if self.entry_type == STGTY_STORAGE and self.size != 0: - olefile._raise_defect(DEFECT_POTENTIAL, 'OLE storage with size>0') + olefile.raise_defect(DEFECT_POTENTIAL, 'OLE storage with size>0') # check if stream is not already referenced elsewhere: - if self.entry_type in (STGTY_ROOT, STGTY_STREAM) and self.size>0: + if self.entry_type in (STGTY_ROOT, STGTY_STREAM) and self.size > 0: if self.size < olefile.minisectorcutoff \ - and self.entry_type==STGTY_STREAM: # only streams can be in MiniFAT + and self.entry_type == STGTY_STREAM: # only streams can be in MiniFAT # ministream object minifat = True else: minifat = False olefile._check_duplicate_stream(self.isectStart, minifat) - - def build_storage_tree(self): """ Read and build the red-black tree attached to this _OleDirectoryEntry @@ -908,23 +955,22 @@ class _OleDirectoryEntry: # (see rich comparison methods in this class) self.kids.sort() - def append_kids(self, child_sid): """ Walk through red-black tree of children of this directory entry to add all of them to the kids list. (recursive method) - child_sid : index of child directory entry to use, or None when called - first time for the root. (only used during recursion) + :param child_sid : index of child directory entry to use, or None when called + first time for the root. (only used during recursion) """ - #[PL] this method was added to use simple recursion instead of a complex + # [PL] this method was added to use simple recursion instead of a complex # algorithm. # if this is not a storage or a leaf of the tree, nothing to do: if child_sid == NOSTREAM: return # check if child SID is in the proper range: - if child_sid<0 or child_sid>=len(self.olefile.direntries): - self.olefile._raise_defect(DEFECT_FATAL, 'OLE DirEntry index out of range') + if child_sid < 0 or child_sid >= len(self.olefile.direntries): + self.olefile.raise_defect(DEFECT_FATAL, 'OLE DirEntry index out of range') # get child direntry: child = self.olefile._load_direntry(child_sid) #direntries[child_sid] debug('append_kids: child_sid=%d - %s - sid_left=%d, sid_right=%d, sid_child=%d' @@ -936,7 +982,7 @@ class _OleDirectoryEntry: # Check if its name is not already used (case-insensitive): name_lower = child.name.lower() if name_lower in self.kids_dict: - self.olefile._raise_defect(DEFECT_INCORRECT, + self.olefile.raise_defect(DEFECT_INCORRECT, "Duplicate filename in OLE storage") # Then the child_sid _OleDirectoryEntry object is appended to the # kids list and dictionary: @@ -944,7 +990,7 @@ class _OleDirectoryEntry: self.kids_dict[name_lower] = child # Check if kid was not already referenced in a storage: if child.used: - self.olefile._raise_defect(DEFECT_INCORRECT, + self.olefile.raise_defect(DEFECT_INCORRECT, 'OLE Entry referenced more than once') child.used = True # Finally walk through right side of the tree: @@ -952,7 +998,6 @@ class _OleDirectoryEntry: # Afterwards build kid's own tree if it's also a storage: child.build_storage_tree() - def __eq__(self, other): "Compare entries by name" return self.name == other.name @@ -972,7 +1017,6 @@ class _OleDirectoryEntry: #TODO: replace by the same function as MS implementation ? # (order by name length first, then case-insensitive order) - def dump(self, tab = 0): "Dump this entry, and all its subentries (for debug purposes only)" TYPES = ["(invalid)", "(storage)", "(stream)", "(lockbytes)", @@ -987,13 +1031,12 @@ class _OleDirectoryEntry: for kid in self.kids: kid.dump(tab + 2) - def getmtime(self): """ Return modification time of a directory entry. :returns: None if modification time is null, a python datetime object - otherwise (UTC timezone) + otherwise (UTC timezone) new in version 0.26 """ @@ -1001,13 +1044,12 @@ class _OleDirectoryEntry: return None return filetime2datetime(self.modifyTime) - def getctime(self): """ Return creation time of a directory entry. :returns: None if modification time is null, a python datetime object - otherwise (UTC timezone) + otherwise (UTC timezone) new in version 0.26 """ @@ -1018,7 +1060,7 @@ class _OleDirectoryEntry: #--- OleFileIO ---------------------------------------------------------------- -class OleFileIO: +class OleFileIO(object): """ OLE container object @@ -1048,35 +1090,60 @@ class OleFileIO: TIFF files). """ - def __init__(self, filename = None, raise_defects=DEFECT_FATAL): + def __init__(self, filename=None, raise_defects=DEFECT_FATAL, + write_mode=False, debug=False, path_encoding=DEFAULT_PATH_ENCODING): """ - Constructor for OleFileIO class. + Constructor for the OleFileIO class. :param filename: file to open. + + - if filename is a string smaller than 1536 bytes, it is the path + of the file to open. (bytes or unicode string) + - if filename is a string longer than 1535 bytes, it is parsed + as the content of an OLE file in memory. (bytes type only) + - if filename is a file-like object (with read, seek and tell methods), + it is parsed as-is. + :param raise_defects: minimal level for defects to be raised as exceptions. - (use DEFECT_FATAL for a typical application, DEFECT_INCORRECT for a - security-oriented application, see source code for details) + (use DEFECT_FATAL for a typical application, DEFECT_INCORRECT for a + security-oriented application, see source code for details) + + :param write_mode: bool, if True the file is opened in read/write mode instead + of read-only by default. + + :param debug: bool, set debug mode + + :param path_encoding: None or str, name of the codec to use for path + names (streams and storages), or None for Unicode. + Unicode by default on Python 3+, UTF-8 on Python 2.x. + (new in olefile 0.42, was hardcoded to Latin-1 until olefile v0.41) """ + set_debug_mode(debug) # minimal level for defects to be raised as exceptions: self._raise_defects_level = raise_defects # list of defects/issues not raised as exceptions: # tuples of (exception type, message) self.parsing_issues = [] + self.write_mode = write_mode + self.path_encoding = path_encoding + self._filesize = None + self.fp = None if filename: - self.open(filename) + self.open(filename, write_mode=write_mode) - - def _raise_defect(self, defect_level, message, exception_type=IOError): + def raise_defect(self, defect_level, message, exception_type=IOError): """ This method should be called for any defect found during file parsing. It may raise an IOError exception according to the minimal level chosen for the OleFileIO object. :param defect_level: defect level, possible values are: - DEFECT_UNSURE : a case which looks weird, but not sure it's a defect - DEFECT_POTENTIAL : a potential defect - DEFECT_INCORRECT : an error according to specifications, but parsing can go on - DEFECT_FATAL : an error which cannot be ignored, parsing is impossible + + - DEFECT_UNSURE : a case which looks weird, but not sure it's a defect + - DEFECT_POTENTIAL : a potential defect + - DEFECT_INCORRECT : an error according to specifications, but parsing can go on + - DEFECT_FATAL : an error which cannot be ignored, parsing is impossible + :param message: string describing the defect, used with raised exception. :param exception_type: exception class to be raised, IOError by default """ @@ -1087,32 +1154,68 @@ class OleFileIO: # just record the issue, no exception raised: self.parsing_issues.append((exception_type, message)) - - def open(self, filename): + def _decode_utf16_str(self, utf16_str, errors='replace'): """ - Open an OLE2 file. - Reads the header, FAT and directory. + Decode a string encoded in UTF-16 LE format, as found in the OLE + directory or in property streams. Return a string encoded + according to the path_encoding specified for the OleFileIO object. - :param filename: string-like or file-like object + :param utf16_str: bytes string encoded in UTF-16 LE format + :param errors: str, see python documentation for str.decode() + :return: str, encoded according to path_encoding """ - #[PL] check if filename is a string-like or file-like object: + unicode_str = utf16_str.decode('UTF-16LE', errors) + if self.path_encoding: + # an encoding has been specified for path names: + return unicode_str.encode(self.path_encoding, errors) + else: + # path_encoding=None, return the Unicode string as-is: + return unicode_str + + def open(self, filename, write_mode=False): + """ + Open an OLE2 file in read-only or read/write mode. + Read and parse the header, FAT and directory. + + :param filename: string-like or file-like object, OLE file to parse + + - if filename is a string smaller than 1536 bytes, it is the path + of the file to open. (bytes or unicode string) + - if filename is a string longer than 1535 bytes, it is parsed + as the content of an OLE file in memory. (bytes type only) + - if filename is a file-like object (with read, seek and tell methods), + it is parsed as-is. + + :param write_mode: bool, if True the file is opened in read/write mode instead + of read-only by default. (ignored if filename is not a path) + """ + self.write_mode = write_mode + # [PL] check if filename is a string-like or file-like object: # (it is better to check for a read() method) if hasattr(filename, 'read'): - # file-like object + #TODO: also check seek and tell methods? + # file-like object: use it directly self.fp = filename + elif isinstance(filename, bytes) and len(filename) >= MINIMAL_OLEFILE_SIZE: + # filename is a bytes string containing the OLE file to be parsed: + # convert it to BytesIO + self.fp = io.BytesIO(filename) else: # string-like object: filename of file on disk - #TODO: if larger than 1024 bytes, this could be the actual data => BytesIO - self.fp = open(filename, "rb") - # old code fails if filename is not a plain string: - #if isinstance(filename, (bytes, basestring)): - # self.fp = open(filename, "rb") - #else: - # self.fp = filename + if self.write_mode: + # open file in mode 'read with update, binary' + # According to https://docs.python.org/2/library/functions.html#open + # 'w' would truncate the file, 'a' may only append on some Unixes + mode = 'r+b' + else: + # read-only mode by default + mode = 'rb' + self.fp = open(filename, mode) # obtain the filesize by using seek and tell, which should work on most # file-like objects: #TODO: do it above, using getsize with filename when possible? #TODO: fix code to fail with clear exception when filesize cannot be obtained + filesize = 0 self.fp.seek(0, os.SEEK_END) try: filesize = self.fp.tell() @@ -1128,7 +1231,7 @@ class OleFileIO: header = self.fp.read(512) if len(header) != 512 or header[:8] != MAGIC: - self._raise_defect(DEFECT_FATAL, "not an OLE2 structured storage file") + self.raise_defect(DEFECT_FATAL, "not an OLE2 structured storage file") # [PL] header structure according to AAF specifications: ##Header @@ -1169,7 +1272,7 @@ class OleFileIO: # '<' indicates little-endian byte ordering for Intel (cf. struct module help) fmt_header = '<8s16sHHHHHHLLLLLLLLLL' header_size = struct.calcsize(fmt_header) - debug( "fmt_header size = %d, +FAT = %d" % (header_size, header_size + 109*4) ) + debug("fmt_header size = %d, +FAT = %d" % (header_size, header_size + 109*4)) header1 = header[:header_size] ( self.Sig, @@ -1190,61 +1293,78 @@ class OleFileIO: self.sectDifStart, self.csectDif ) = struct.unpack(fmt_header, header1) - debug( struct.unpack(fmt_header, header1)) + debug(struct.unpack(fmt_header, header1)) if self.Sig != MAGIC: # OLE signature should always be present - self._raise_defect(DEFECT_FATAL, "incorrect OLE signature") + self.raise_defect(DEFECT_FATAL, "incorrect OLE signature") if self.clsid != bytearray(16): # according to AAF specs, CLSID should always be zero - self._raise_defect(DEFECT_INCORRECT, "incorrect CLSID in OLE header") - debug( "MinorVersion = %d" % self.MinorVersion ) - debug( "DllVersion = %d" % self.DllVersion ) + self.raise_defect(DEFECT_INCORRECT, "incorrect CLSID in OLE header") + debug("MinorVersion = %d" % self.MinorVersion) + debug("DllVersion = %d" % self.DllVersion) if self.DllVersion not in [3, 4]: # version 3: usual format, 512 bytes per sector # version 4: large format, 4K per sector - self._raise_defect(DEFECT_INCORRECT, "incorrect DllVersion in OLE header") - debug( "ByteOrder = %X" % self.ByteOrder ) + self.raise_defect(DEFECT_INCORRECT, "incorrect DllVersion in OLE header") + debug("ByteOrder = %X" % self.ByteOrder) if self.ByteOrder != 0xFFFE: # For now only common little-endian documents are handled correctly - self._raise_defect(DEFECT_FATAL, "incorrect ByteOrder in OLE header") + self.raise_defect(DEFECT_FATAL, "incorrect ByteOrder in OLE header") # TODO: add big-endian support for documents created on Mac ? + # But according to [MS-CFB] ? v20140502, ByteOrder MUST be 0xFFFE. self.SectorSize = 2**self.SectorShift - debug( "SectorSize = %d" % self.SectorSize ) + debug("SectorSize = %d" % self.SectorSize) if self.SectorSize not in [512, 4096]: - self._raise_defect(DEFECT_INCORRECT, "incorrect SectorSize in OLE header") - if (self.DllVersion==3 and self.SectorSize!=512) \ - or (self.DllVersion==4 and self.SectorSize!=4096): - self._raise_defect(DEFECT_INCORRECT, "SectorSize does not match DllVersion in OLE header") + self.raise_defect(DEFECT_INCORRECT, "incorrect SectorSize in OLE header") + if (self.DllVersion == 3 and self.SectorSize != 512) \ + or (self.DllVersion == 4 and self.SectorSize != 4096): + self.raise_defect(DEFECT_INCORRECT, "SectorSize does not match DllVersion in OLE header") self.MiniSectorSize = 2**self.MiniSectorShift - debug( "MiniSectorSize = %d" % self.MiniSectorSize ) + debug("MiniSectorSize = %d" % self.MiniSectorSize) if self.MiniSectorSize not in [64]: - self._raise_defect(DEFECT_INCORRECT, "incorrect MiniSectorSize in OLE header") + self.raise_defect(DEFECT_INCORRECT, "incorrect MiniSectorSize in OLE header") if self.Reserved != 0 or self.Reserved1 != 0: - self._raise_defect(DEFECT_INCORRECT, "incorrect OLE header (non-null reserved bytes)") - debug( "csectDir = %d" % self.csectDir ) - if self.SectorSize==512 and self.csectDir!=0: - self._raise_defect(DEFECT_INCORRECT, "incorrect csectDir in OLE header") - debug( "csectFat = %d" % self.csectFat ) - debug( "sectDirStart = %X" % self.sectDirStart ) - debug( "signature = %d" % self.signature ) + self.raise_defect(DEFECT_INCORRECT, "incorrect OLE header (non-null reserved bytes)") + debug("csectDir = %d" % self.csectDir) + # Number of directory sectors (only allowed if DllVersion != 3) + if self.SectorSize == 512 and self.csectDir != 0: + self.raise_defect(DEFECT_INCORRECT, "incorrect csectDir in OLE header") + debug("csectFat = %d" % self.csectFat) + # csectFat = number of FAT sectors in the file + debug("sectDirStart = %X" % self.sectDirStart) + # sectDirStart = 1st sector containing the directory + debug("signature = %d" % self.signature) # Signature should be zero, BUT some implementations do not follow this # rule => only a potential defect: + # (according to MS-CFB, may be != 0 for applications supporting file + # transactions) if self.signature != 0: - self._raise_defect(DEFECT_POTENTIAL, "incorrect OLE header (signature>0)") - debug( "MiniSectorCutoff = %d" % self.MiniSectorCutoff ) - debug( "MiniFatStart = %X" % self.MiniFatStart ) - debug( "csectMiniFat = %d" % self.csectMiniFat ) - debug( "sectDifStart = %X" % self.sectDifStart ) - debug( "csectDif = %d" % self.csectDif ) + self.raise_defect(DEFECT_POTENTIAL, "incorrect OLE header (signature>0)") + debug("MiniSectorCutoff = %d" % self.MiniSectorCutoff) + # MS-CFB: This integer field MUST be set to 0x00001000. This field + # specifies the maximum size of a user-defined data stream allocated + # from the mini FAT and mini stream, and that cutoff is 4096 bytes. + # Any user-defined data stream larger than or equal to this cutoff size + # must be allocated as normal sectors from the FAT. + if self.MiniSectorCutoff != 0x1000: + self.raise_defect(DEFECT_INCORRECT, "incorrect MiniSectorCutoff in OLE header") + debug("MiniFatStart = %X" % self.MiniFatStart) + debug("csectMiniFat = %d" % self.csectMiniFat) + debug("sectDifStart = %X" % self.sectDifStart) + debug("csectDif = %d" % self.csectDif) # calculate the number of sectors in the file # (-1 because header doesn't count) - self.nb_sect = ( (filesize + self.SectorSize-1) // self.SectorSize) - 1 - debug( "Number of sectors in the file: %d" % self.nb_sect ) + self.nb_sect = ((filesize + self.SectorSize-1) // self.SectorSize) - 1 + debug("Number of sectors in the file: %d" % self.nb_sect) + #TODO: change this test, because an OLE file MAY contain other data + # after the last sector. - # file clsid (probably never used, so we don't store it) - clsid = _clsid(header[8:24]) + # file clsid + self.clsid = _clsid(header[8:24]) + + #TODO: remove redundant attributes, and fix the code which uses them? self.sectorsize = self.SectorSize #1 << i16(header, 30) self.minisectorsize = self.MiniSectorSize #1 << i16(header, 32) self.minisectorcutoff = self.MiniSectorCutoff # i32(header, 56) @@ -1261,27 +1381,26 @@ class OleFileIO: # Load file allocation tables self.loadfat(header) - # Load direcory. This sets both the direntries list (ordered by sid) + # Load directory. This sets both the direntries list (ordered by sid) # and the root (ordered by hierarchy) members. self.loaddirectory(self.sectDirStart)#i32(header, 48)) self.ministream = None self.minifatsect = self.MiniFatStart #i32(header, 60) - def close(self): """ close the OLE file, to release the file object """ self.fp.close() - def _check_duplicate_stream(self, first_sect, minifat=False): """ Checks if a stream has not been already referenced elsewhere. This method should only be called once for each known stream, and only if stream size is not null. - :param first_sect: index of first sector of the stream in FAT - :param minifat: if True, stream is located in the MiniFAT, else in the FAT + + :param first_sect: int, index of first sector of the stream in FAT + :param minifat: bool, if True, stream is located in the MiniFAT, else in the FAT """ if minifat: debug('_check_duplicate_stream: sect=%d in MiniFAT' % first_sect) @@ -1289,24 +1408,23 @@ class OleFileIO: else: debug('_check_duplicate_stream: sect=%d in FAT' % first_sect) # some values can be safely ignored (not a real stream): - if first_sect in (DIFSECT,FATSECT,ENDOFCHAIN,FREESECT): + if first_sect in (DIFSECT, FATSECT, ENDOFCHAIN, FREESECT): return used_streams = self._used_streams_fat #TODO: would it be more efficient using a dict or hash values, instead # of a list of long ? if first_sect in used_streams: - self._raise_defect(DEFECT_INCORRECT, 'Stream referenced twice') + self.raise_defect(DEFECT_INCORRECT, 'Stream referenced twice') else: used_streams.append(first_sect) - def dumpfat(self, fat, firstindex=0): "Displays a part of FAT in human-readable form for debugging purpose" # [PL] added only for debug if not DEBUG_MODE: return # dictionary to convert special FAT values in human-readable strings - VPL=8 # valeurs par ligne (8+1 * 8+1 = 81) + VPL = 8 # values per line (8+1 * 8+1 = 81) fatnames = { FREESECT: "..free..", ENDOFCHAIN: "[ END. ]", @@ -1323,26 +1441,28 @@ class OleFileIO: index = l*VPL print("%8X:" % (firstindex+index), end=" ") for i in range(index, index+VPL): - if i>=nbsect: + if i >= nbsect: break sect = fat[i] - if sect in fatnames: - nom = fatnames[sect] + aux = sect & 0xFFFFFFFF # JYTHON-WORKAROUND + if aux in fatnames: + name = fatnames[aux] else: if sect == i+1: - nom = " --->" + name = " --->" else: - nom = "%8X" % sect - print(nom, end=" ") + name = "%8X" % sect + print(name, end=" ") print() - def dumpsect(self, sector, firstindex=0): "Displays a sector in a human-readable form, for debugging purpose." if not DEBUG_MODE: return - VPL=8 # number of values per line (8+1 * 8+1 = 81) + VPL = 8 # number of values per line (8+1 * 8+1 = 81) tab = array.array(UINT32, sector) + if sys.byteorder == 'big': + tab.byteswap() nbsect = len(tab) nlines = (nbsect+VPL-1)//VPL print("index", end=" ") @@ -1353,11 +1473,11 @@ class OleFileIO: index = l*VPL print("%8X:" % (firstindex+index), end=" ") for i in range(index, index+VPL): - if i>=nbsect: + if i >= nbsect: break sect = tab[i] - nom = "%8X" % sect - print(nom, end=" ") + name = "%8X" % sect + print(name, end=" ") print() def sect2array(self, sect): @@ -1371,11 +1491,10 @@ class OleFileIO: a.byteswap() return a - def loadfat_sect(self, sect): """ Adds the indexes of the given sector to the FAT - + :param sect: string containing the first FAT sector, or array of long integers :returns: index of last FAT sector. """ @@ -1389,9 +1508,11 @@ class OleFileIO: self.dumpsect(sect) # The FAT is a sector chain starting at the first index of itself. for isect in fat1: - #print("isect = %X" % isect) + isect = isect & 0xFFFFFFFF # JYTHON-WORKAROUND + debug("isect = %X" % isect) if isect == ENDOFCHAIN or isect == FREESECT: # the end of the sector chain has been reached + debug("found end of sector chain") break # read the FAT sector s = self.getsect(isect) @@ -1401,17 +1522,17 @@ class OleFileIO: self.fat = self.fat + nextfat return isect - def loadfat(self, header): """ Load the FAT table. """ - # The header contains a sector numbers - # for the first 109 FAT sectors. Additional sectors are - # described by DIF blocks + # The 1st sector of the file contains sector numbers for the first 109 + # FAT sectors, right after the header which is 76 bytes long. + # (always 109, whatever the sector size: 512 bytes = 76+4*109) + # Additional sectors are described by DIF blocks sect = header[76:512] - debug( "len(sect)=%d, so %d integers" % (len(sect), len(sect)//4) ) + debug("len(sect)=%d, so %d integers" % (len(sect), len(sect)//4)) #fat = [] # [PL] FAT is an array of 32 bits unsigned ints, it's more effective # to use an array than a list in Python. @@ -1421,7 +1542,7 @@ class OleFileIO: #self.dumpfat(self.fat) ## for i in range(0, len(sect), 4): ## ix = i32(sect, i) -## #[PL] if ix == -2 or ix == -1: # ix == 0xFFFFFFFE or ix == 0xFFFFFFFF: +## # [PL] if ix == -2 or ix == -1: # ix == 0xFFFFFFFE or ix == 0xFFFFFFFF: ## if ix == 0xFFFFFFFE or ix == 0xFFFFFFFF: ## break ## s = self.getsect(ix) @@ -1433,28 +1554,31 @@ class OleFileIO: if self.csectFat <= 109: # there must be at least 109 blocks in header and the rest in # DIFAT, so number of sectors must be >109. - self._raise_defect(DEFECT_INCORRECT, 'incorrect DIFAT, not enough sectors') + self.raise_defect(DEFECT_INCORRECT, 'incorrect DIFAT, not enough sectors') if self.sectDifStart >= self.nb_sect: # initial DIFAT block index must be valid - self._raise_defect(DEFECT_FATAL, 'incorrect DIFAT, first index out of range') - debug( "DIFAT analysis..." ) + self.raise_defect(DEFECT_FATAL, 'incorrect DIFAT, first index out of range') + debug("DIFAT analysis...") # We compute the necessary number of DIFAT sectors : - # (each DIFAT sector = 127 pointers + 1 towards next DIFAT sector) - nb_difat = (self.csectFat-109 + 126)//127 - debug( "nb_difat = %d" % nb_difat ) + # Number of pointers per DIFAT sector = (sectorsize/4)-1 + # (-1 because the last pointer is the next DIFAT sector number) + nb_difat_sectors = (self.sectorsize//4)-1 + # (if 512 bytes: each DIFAT sector = 127 pointers + 1 towards next DIFAT sector) + nb_difat = (self.csectFat-109 + nb_difat_sectors-1)//nb_difat_sectors + debug("nb_difat = %d" % nb_difat) if self.csectDif != nb_difat: raise IOError('incorrect DIFAT') isect_difat = self.sectDifStart for i in iterrange(nb_difat): - debug( "DIFAT block %d, sector %X" % (i, isect_difat) ) + debug("DIFAT block %d, sector %X" % (i, isect_difat)) #TODO: check if corresponding FAT SID = DIFSECT sector_difat = self.getsect(isect_difat) difat = self.sect2array(sector_difat) self.dumpsect(sector_difat) - self.loadfat_sect(difat[:127]) + self.loadfat_sect(difat[:nb_difat_sectors]) # last DIFAT pointer is next DIFAT sector: - isect_difat = difat[127] - debug( "next DIFAT sector: %X" % isect_difat ) + isect_difat = difat[nb_difat_sectors] + debug("next DIFAT sector: %X" % isect_difat) # checks: if isect_difat not in [ENDOFCHAIN, FREESECT]: # last DIFAT pointer value must be ENDOFCHAIN or FREESECT @@ -1472,7 +1596,6 @@ class OleFileIO: debug('\nFAT:') self.dumpfat(self.fat) - def loadminifat(self): """ Load the MiniFAT table. @@ -1494,11 +1617,11 @@ class OleFileIO: (self.minifatsect, self.csectMiniFat, used_size, stream_size, nb_minisectors)) if used_size > stream_size: # This is not really a problem, but may indicate a wrong implementation: - self._raise_defect(DEFECT_INCORRECT, 'OLE MiniStream is larger than MiniFAT') + self.raise_defect(DEFECT_INCORRECT, 'OLE MiniStream is larger than MiniFAT') # In any case, first read stream_size: s = self._open(self.minifatsect, stream_size, force_FAT=True).read() - #[PL] Old code replaced by an array: - #self.minifat = [i32(s, i) for i in range(0, len(s), 4)] + # [PL] Old code replaced by an array: + # self.minifat = [i32(s, i) for i in range(0, len(s), 4)] self.minifat = self.sect2array(s) # Then shrink the array to used size, to avoid indexes out of MiniStream: debug('MiniFAT shrunk from %d to %d sectors' % (len(self.minifat), nb_minisectors)) @@ -1510,33 +1633,64 @@ class OleFileIO: def getsect(self, sect): """ Read given sector from file on disk. - - :param sect: sector index + + :param sect: int, sector index :returns: a string containing the sector data. """ - # [PL] this original code was wrong when sectors are 4KB instead of + # From [MS-CFB]: A sector number can be converted into a byte offset + # into the file by using the following formula: + # (sector number + 1) x Sector Size. + # This implies that sector #0 of the file begins at byte offset Sector + # Size, not at 0. + + # [PL] the original code in PIL was wrong when sectors are 4KB instead of # 512 bytes: - #self.fp.seek(512 + self.sectorsize * sect) - #[PL]: added safety checks: - #print("getsect(%X)" % sect) + # self.fp.seek(512 + self.sectorsize * sect) + # [PL]: added safety checks: + # print("getsect(%X)" % sect) try: self.fp.seek(self.sectorsize * (sect+1)) except: debug('getsect(): sect=%X, seek=%d, filesize=%d' % (sect, self.sectorsize*(sect+1), self._filesize)) - self._raise_defect(DEFECT_FATAL, 'OLE sector index out of range') + self.raise_defect(DEFECT_FATAL, 'OLE sector index out of range') sector = self.fp.read(self.sectorsize) if len(sector) != self.sectorsize: debug('getsect(): sect=%X, read=%d, sectorsize=%d' % (sect, len(sector), self.sectorsize)) - self._raise_defect(DEFECT_FATAL, 'incomplete OLE sector') + self.raise_defect(DEFECT_FATAL, 'incomplete OLE sector') return sector + def write_sect(self, sect, data, padding=b'\x00'): + """ + Write given sector to file on disk. + + :param sect: int, sector index + :param data: bytes, sector data + :param padding: single byte, padding character if data < sector size + """ + if not isinstance(data, bytes): + raise TypeError("write_sect: data must be a bytes string") + if not isinstance(padding, bytes) or len(padding) != 1: + raise TypeError("write_sect: padding must be a bytes string of 1 char") + #TODO: we could allow padding=None for no padding at all + try: + self.fp.seek(self.sectorsize * (sect+1)) + except: + debug('write_sect(): sect=%X, seek=%d, filesize=%d' % + (sect, self.sectorsize*(sect+1), self._filesize)) + self.raise_defect(DEFECT_FATAL, 'OLE sector index out of range') + if len(data) < self.sectorsize: + # add padding + data += padding * (self.sectorsize - len(data)) + elif len(data) < self.sectorsize: + raise ValueError("Data is larger than sector size") + self.fp.write(data) def loaddirectory(self, sect): """ Load the directory. - + :param sect: sector index of directory stream. """ # The directory is stored in a standard @@ -1546,14 +1700,14 @@ class OleFileIO: # (stream size is not known in advance) self.directory_fp = self._open(sect) - #[PL] to detect malformed documents and avoid DoS attacks, the maximum + # [PL] to detect malformed documents and avoid DoS attacks, the maximum # number of directory entries can be calculated: max_entries = self.directory_fp.size // 128 debug('loaddirectory: size=%d, max_entries=%d' % (self.directory_fp.size, max_entries)) # Create list of directory entries - #self.direntries = [] + # self.direntries = [] # We start with a list of "None" object self.direntries = [None] * max_entries ## for sid in iterrange(max_entries): @@ -1568,8 +1722,7 @@ class OleFileIO: # read and build all storage trees, starting from the root: self.root.build_storage_tree() - - def _load_direntry (self, sid): + def _load_direntry(self, sid): """ Load a directory entry from the directory. This method should only be called once for each storage/stream when @@ -1577,14 +1730,15 @@ class OleFileIO: :param sid: index of storage/stream in the directory. :returns: a _OleDirectoryEntry object + :exception IOError: if the entry has always been referenced. """ # check if SID is OK: - if sid<0 or sid>=len(self.direntries): - self._raise_defect(DEFECT_FATAL, "OLE directory index out of range") + if sid < 0 or sid >= len(self.direntries): + self.raise_defect(DEFECT_FATAL, "OLE directory index out of range") # check if entry was already referenced: if self.direntries[sid] is not None: - self._raise_defect(DEFECT_INCORRECT, + self.raise_defect(DEFECT_INCORRECT, "double reference for OLE stream/storage") # if exception not raised, return the object return self.direntries[sid] @@ -1593,14 +1747,12 @@ class OleFileIO: self.direntries[sid] = _OleDirectoryEntry(entry, sid, self) return self.direntries[sid] - def dumpdirectory(self): """ Dump directory (for debugging only) """ self.root.dump() - def _open(self, start, size = 0x7FFFFFFF, force_FAT=False): """ Open a stream, either in FAT or MiniFAT according to its size. @@ -1609,7 +1761,7 @@ class OleFileIO: :param start: index of first sector :param size: size of stream (or nothing if size is unknown) :param force_FAT: if False (default), stream will be opened in FAT or MiniFAT - according to size. If True, it will always be opened in FAT. + according to size. If True, it will always be opened in FAT. """ debug('OleFileIO.open(): sect=%d, size=%d, force_FAT=%s' % (start, size, str(force_FAT))) @@ -1626,67 +1778,71 @@ class OleFileIO: (self.root.isectStart, size_ministream)) self.ministream = self._open(self.root.isectStart, size_ministream, force_FAT=True) - return _OleStream(self.ministream, start, size, 0, - self.minisectorsize, self.minifat, - self.ministream.size) + return _OleStream(fp=self.ministream, sect=start, size=size, + offset=0, sectorsize=self.minisectorsize, + fat=self.minifat, filesize=self.ministream.size) else: # standard stream - return _OleStream(self.fp, start, size, 512, - self.sectorsize, self.fat, self._filesize) - + return _OleStream(fp=self.fp, sect=start, size=size, + offset=self.sectorsize, + sectorsize=self.sectorsize, fat=self.fat, + filesize=self._filesize) def _list(self, files, prefix, node, streams=True, storages=False): """ - (listdir helper) + listdir helper + :param files: list of files to fill in :param prefix: current location in storage tree (list of names) :param node: current node (_OleDirectoryEntry object) :param streams: bool, include streams if True (True by default) - new in v0.26 :param storages: bool, include storages if True (False by default) - new in v0.26 - (note: the root storage is never included) + (note: the root storage is never included) """ prefix = prefix + [node.name] for entry in node.kids: - if entry.kids: + if entry.entry_type == STGTY_STORAGE: # this is a storage if storages: # add it to the list files.append(prefix[1:] + [entry.name]) # check its kids self._list(files, prefix, entry, streams, storages) - else: + elif entry.entry_type == STGTY_STREAM: # this is a stream if streams: # add it to the list files.append(prefix[1:] + [entry.name]) - + else: + self.raise_defect(DEFECT_INCORRECT, 'The directory tree contains an entry which is not a stream nor a storage.') def listdir(self, streams=True, storages=False): """ - Return a list of streams stored in this file + Return a list of streams and/or storages stored in this file :param streams: bool, include streams if True (True by default) - new in v0.26 :param storages: bool, include storages if True (False by default) - new in v0.26 (note: the root storage is never included) + :returns: list of stream and/or storage paths """ files = [] self._list(files, [], self.root, streams, storages) return files - def _find(self, filename): """ Returns directory entry of given filename. (openstream helper) Note: this method is case-insensitive. :param filename: path of stream in storage tree (except root entry), either: - + - a string using Unix path syntax, for example: 'storage_1/storage_1.2/stream' - - a list of storage filenames, path to the desired stream/storage. + - or a list of storage filenames, path to the desired stream/storage. Example: ['storage_1', 'storage_1.2', 'stream'] + :returns: sid of requested filename - raise IOError if file not found + :exception IOError: if file not found """ # if filename is a string instead of a list, split it on slashes to @@ -1704,18 +1860,18 @@ class OleFileIO: node = kid return node.sid - def openstream(self, filename): """ Open a stream as a read-only file object (BytesIO). + Note: filename is case-insensitive. :param filename: path of stream in storage tree (except root entry), either: - + - a string using Unix path syntax, for example: 'storage_1/storage_1.2/stream' - - a list of storage filenames, path to the desired stream/storage. + - or a list of storage filenames, path to the desired stream/storage. Example: ['storage_1', 'storage_1.2', 'stream'] - + :returns: file object (read-only) :exception IOError: if filename not found, or if this is not a stream. """ @@ -1725,6 +1881,65 @@ class OleFileIO: raise IOError("this file is not a stream") return self._open(entry.isectStart, entry.size) + def write_stream(self, stream_name, data): + """ + Write a stream to disk. For now, it is only possible to replace an + existing stream by data of the same size. + + :param stream_name: path of stream in storage tree (except root entry), either: + + - a string using Unix path syntax, for example: + 'storage_1/storage_1.2/stream' + - or a list of storage filenames, path to the desired stream/storage. + Example: ['storage_1', 'storage_1.2', 'stream'] + + :param data: bytes, data to be written, must be the same size as the original + stream. + """ + if not isinstance(data, bytes): + raise TypeError("write_stream: data must be a bytes string") + sid = self._find(stream_name) + entry = self.direntries[sid] + if entry.entry_type != STGTY_STREAM: + raise IOError("this is not a stream") + size = entry.size + if size != len(data): + raise ValueError("write_stream: data must be the same size as the existing stream") + if size < self.minisectorcutoff: + raise NotImplementedError("Writing a stream in MiniFAT is not implemented yet") + sect = entry.isectStart + # number of sectors to write + nb_sectors = (size + (self.sectorsize-1)) // self.sectorsize + debug('nb_sectors = %d' % nb_sectors) + for i in range(nb_sectors): + # try: + # self.fp.seek(offset + self.sectorsize * sect) + # except: + # debug('sect=%d, seek=%d' % + # (sect, offset+self.sectorsize*sect)) + # raise IOError('OLE sector index out of range') + # extract one sector from data, the last one being smaller: + if i < (nb_sectors-1): + data_sector = data[i*self.sectorsize:(i+1)*self.sectorsize] + #TODO: comment this if it works + assert(len(data_sector) == self.sectorsize) + else: + data_sector = data[i*self.sectorsize:] + # TODO: comment this if it works + debug('write_stream: size=%d sectorsize=%d data_sector=%d size%%sectorsize=%d' + % (size, self.sectorsize, len(data_sector), size % self.sectorsize)) + assert(len(data_sector) % self.sectorsize == size % self.sectorsize) + self.write_sect(sect, data_sector) +# self.fp.write(data_sector) + # jump to next sector in the FAT: + try: + sect = self.fat[sect] + except IndexError: + # [PL] if pointer is out of the FAT an exception is raised + raise IOError('incorrect OLE FAT, sector index out of range') + # [PL] Last sector should be a "end of chain" marker: + if sect != ENDOFCHAIN: + raise IOError('incorrect last sector index in OLE stream') def get_type(self, filename): """ @@ -1733,7 +1948,7 @@ class OleFileIO: :param filename: path of stream in storage tree. (see openstream for syntax) :returns: False if object does not exist, its entry type (>0) otherwise: - + - STGTY_STREAM: a stream - STGTY_STORAGE: a storage - STGTY_ROOT: the root entry @@ -1745,7 +1960,6 @@ class OleFileIO: except: return False - def getmtime(self, filename): """ Return modification time of a stream/storage. @@ -1761,7 +1975,6 @@ class OleFileIO: entry = self.direntries[sid] return entry.getmtime() - def getctime(self, filename): """ Return creation time of a stream/storage. @@ -1777,11 +1990,11 @@ class OleFileIO: entry = self.direntries[sid] return entry.getctime() - def exists(self, filename): """ Test if given filename exists as a stream or a storage in the OLE container. + Note: filename is case-insensitive. :param filename: path of stream in storage tree. (see openstream for syntax) :returns: True if object exist, else False. @@ -1792,7 +2005,6 @@ class OleFileIO: except: return False - def get_size(self, filename): """ Return size of a stream in the OLE container, in bytes. @@ -1800,7 +2012,7 @@ class OleFileIO: :param filename: path of stream in storage tree (see openstream for syntax) :returns: size in bytes (long integer) :exception IOError: if file not found - :exception TypeError: if this is not a stream + :exception TypeError: if this is not a stream. """ sid = self._find(filename) entry = self.direntries[sid] @@ -1809,7 +2021,6 @@ class OleFileIO: raise TypeError('object is not an OLE stream') return entry.size - def get_rootentry_name(self): """ Return root entry name. Should usually be 'Root Entry' or 'R' in most @@ -1817,7 +2028,6 @@ class OleFileIO: """ return self.root.name - def getproperties(self, filename, convert_time=False, no_conversion=None): """ Return properties described in substream. @@ -1825,11 +2035,13 @@ class OleFileIO: :param filename: path of stream in storage tree (see openstream for syntax) :param convert_time: bool, if True timestamps will be converted to Python datetime :param no_conversion: None or list of int, timestamps not to be converted - (for example total editing time is not a real timestamp) + (for example total editing time is not a real timestamp) + :returns: a dictionary of values indexed by id (integer) """ + # REFERENCE: [MS-OLEPS] https://msdn.microsoft.com/en-us/library/dd942421.aspx # make sure no_conversion is a list, just to simplify code below: - if no_conversion == None: + if no_conversion is None: no_conversion = [] # stream path as a string to report exceptions: streampath = filename @@ -1860,7 +2072,7 @@ class OleFileIO: # a fatal error when parsing the whole file msg = 'Error while parsing properties header in stream %s: %s' % ( repr(streampath), exc) - self._raise_defect(DEFECT_INCORRECT, msg, type(exc)) + self.raise_defect(DEFECT_INCORRECT, msg, type(exc)) return data for i in range(num_props): @@ -1870,7 +2082,7 @@ class OleFileIO: offset = i32(s, 12+i*8) type = i32(s, offset) - debug ('property id=%d: type=%d offset=%X' % (id, type, offset)) + debug('property id=%d: type=%d offset=%X' % (id, type, offset)) # test for common types first (should perhaps use # a dictionary instead?) @@ -1879,14 +2091,14 @@ class OleFileIO: value = i16(s, offset+4) if value >= 32768: value = value - 65536 - elif type == VT_UI2: # 2-byte unsigned integer + elif type == VT_UI2: # 2-byte unsigned integer value = i16(s, offset+4) elif type in (VT_I4, VT_INT, VT_ERROR): # VT_I4: 32-bit signed integer # VT_ERROR: HRESULT, similar to 32-bit signed integer, # see http://msdn.microsoft.com/en-us/library/cc230330.aspx value = i32(s, offset+4) - elif type in (VT_UI4, VT_UINT): # 4-byte unsigned integer + elif type in (VT_UI4, VT_UINT): # 4-byte unsigned integer value = i32(s, offset+4) # FIXME elif type in (VT_BSTR, VT_LPSTR): # CodePageString, see http://msdn.microsoft.com/en-us/library/dd942354.aspx @@ -1908,14 +2120,14 @@ class OleFileIO: # "the string should NOT contain embedded or additional trailing # null characters." count = i32(s, offset+4) - value = _unicode(s[offset+8:offset+8+count*2]) + value = self._decode_utf16_str(s[offset+8:offset+8+count*2]) elif type == VT_FILETIME: - value = long(i32(s, offset+4)) + (long(i32(s, offset+8))<<32) + value = long(i32(s, offset+4)) + (long(i32(s, offset+8)) << 32) # FILETIME is a 64-bit int: "number of 100ns periods # since Jan 1,1601". if convert_time and id not in no_conversion: debug('Converting property #%d to python datetime, value=%d=%fs' - %(id, value, float(value)/10000000)) + % (id, value, float(value) / 10000000)) # convert FILETIME to Python datetime.datetime # inspired from http://code.activestate.com/recipes/511425-filetime-to-datetime/ _FILETIME_null_date = datetime.datetime(1601, 1, 1, 0, 0, 0) @@ -1940,7 +2152,7 @@ class OleFileIO: value = bool(i16(s, offset+4)) else: value = None # everything else yields "None" - debug ('property id=%d: type=%d not implemented in parser yet' % (id, type)) + debug('property id=%d: type=%d not implemented in parser yet' % (id, type)) # missing: VT_EMPTY, VT_NULL, VT_R4, VT_R8, VT_CY, VT_DATE, # VT_DECIMAL, VT_I1, VT_I8, VT_UI8, @@ -1952,8 +2164,8 @@ class OleFileIO: # type of items, e.g. VT_VECTOR|VT_BSTR # see http://msdn.microsoft.com/en-us/library/dd942011.aspx - #print("%08x" % id, repr(value), end=" ") - #print("(%s)" % VT[i32(s, offset) & 0xFFF]) + # print("%08x" % id, repr(value), end=" ") + # print("(%s)" % VT[i32(s, offset) & 0xFFF]) data[id] = value except BaseException as exc: @@ -1961,7 +2173,7 @@ class OleFileIO: # a DEFECT_INCORRECT, because parsing can go on msg = 'Error while parsing property id %d in stream %s: %s' % ( id, repr(streampath), exc) - self._raise_defect(DEFECT_INCORRECT, msg, type(exc)) + self.raise_defect(DEFECT_INCORRECT, msg, type(exc)) return data @@ -1984,107 +2196,110 @@ class OleFileIO: if __name__ == "__main__": - import sys - # [PL] display quick usage info if launched from command-line if len(sys.argv) <= 1: - print(__doc__) - print(""" -Launched from command line, this script parses OLE files and prints info. + print('olefile version %s %s - %s' % (__version__, __date__, __author__)) + print( +""" +Launched from the command line, this script parses OLE files and prints info. -Usage: OleFileIO_PL.py [-d] [-c] [file2 ...] +Usage: olefile.py [-d] [-c] [file2 ...] Options: --d : debug mode (display a lot of debug information, for developers only) +-d : debug mode (displays a lot of debug information, for developers only) -c : check all streams (for debugging purposes) + +For more information, see http://www.decalage.info/olefile """) sys.exit() check_streams = False for filename in sys.argv[1:]: -## try: - # OPTIONS: - if filename == '-d': - # option to switch debug mode on: - set_debug_mode(True) - continue - if filename == '-c': - # option to switch check streams mode on: - check_streams = True - continue + # try: + # OPTIONS: + if filename == '-d': + # option to switch debug mode on: + set_debug_mode(True) + continue + if filename == '-c': + # option to switch check streams mode on: + check_streams = True + continue - ole = OleFileIO(filename)#, raise_defects=DEFECT_INCORRECT) - print("-" * 68) - print(filename) - print("-" * 68) - ole.dumpdirectory() + ole = OleFileIO(filename)#, raise_defects=DEFECT_INCORRECT) + print("-" * 68) + print(filename) + print("-" * 68) + ole.dumpdirectory() + for streamname in ole.listdir(): + if streamname[-1][0] == "\005": + print(streamname, ": properties") + props = ole.getproperties(streamname, convert_time=True) + props = sorted(props.items()) + for k, v in props: + # [PL]: avoid to display too large or binary values: + if isinstance(v, (basestring, bytes)): + if len(v) > 50: + v = v[:50] + if isinstance(v, bytes): + # quick and dirty binary check: + for c in (1, 2, 3, 4, 5, 6, 7, 11, 12, 14, 15, 16, 17, 18, 19, 20, + 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31): + if c in bytearray(v): + v = '(binary data)' + break + print(" ", k, v) + + if check_streams: + # Read all streams to check if there are errors: + print('\nChecking streams...') for streamname in ole.listdir(): - if streamname[-1][0] == "\005": - print(streamname, ": properties") - props = ole.getproperties(streamname, convert_time=True) - props = sorted(props.items()) - for k, v in props: - #[PL]: avoid to display too large or binary values: - if isinstance(v, (basestring, bytes)): - if len(v) > 50: - v = v[:50] - if isinstance(v, bytes): - # quick and dirty binary check: - for c in (1,2,3,4,5,6,7,11,12,14,15,16,17,18,19,20, - 21,22,23,24,25,26,27,28,29,30,31): - if c in bytearray(v): - v = '(binary data)' - break - print(" ", k, v) - - if check_streams: - # Read all streams to check if there are errors: - print('\nChecking streams...') - for streamname in ole.listdir(): - # print name using repr() to convert binary chars to \xNN: - print('-', repr('/'.join(streamname)),'-', end=' ') - st_type = ole.get_type(streamname) - if st_type == STGTY_STREAM: - print('size %d' % ole.get_size(streamname)) - # just try to read stream in memory: - ole.openstream(streamname) - else: - print('NOT a stream : type=%d' % st_type) - print() - -## for streamname in ole.listdir(): -## # print name using repr() to convert binary chars to \xNN: -## print('-', repr('/'.join(streamname)),'-', end=' ') -## print(ole.getmtime(streamname)) -## print() - - print('Modification/Creation times of all directory entries:') - for entry in ole.direntries: - if entry is not None: - print('- %s: mtime=%s ctime=%s' % (entry.name, - entry.getmtime(), entry.getctime())) + # print name using repr() to convert binary chars to \xNN: + print('-', repr('/'.join(streamname)), '-', end=' ') + st_type = ole.get_type(streamname) + if st_type == STGTY_STREAM: + print('size %d' % ole.get_size(streamname)) + # just try to read stream in memory: + ole.openstream(streamname) + else: + print('NOT a stream : type=%d' % st_type) print() - # parse and display metadata: - meta = ole.get_metadata() - meta.dump() - print() - #[PL] Test a few new methods: - root = ole.get_rootentry_name() - print('Root entry name: "%s"' % root) - if ole.exists('worddocument'): - print("This is a Word document.") - print("type of stream 'WordDocument':", ole.get_type('worddocument')) - print("size :", ole.get_size('worddocument')) - if ole.exists('macros/vba'): - print("This document may contain VBA macros.") +# for streamname in ole.listdir(): +# # print name using repr() to convert binary chars to \xNN: +# print('-', repr('/'.join(streamname)),'-', end=' ') +# print(ole.getmtime(streamname)) +# print() - # print parsing issues: - print('\nNon-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') + print('Modification/Creation times of all directory entries:') + for entry in ole.direntries: + if entry is not None: + print('- %s: mtime=%s ctime=%s' % (entry.name, + entry.getmtime(), entry.getctime())) + print() + + # parse and display metadata: + meta = ole.get_metadata() + meta.dump() + print() + # [PL] Test a few new methods: + root = ole.get_rootentry_name() + print('Root entry name: "%s"' % root) + if ole.exists('worddocument'): + print("This is a Word document.") + print("type of stream 'WordDocument':", ole.get_type('worddocument')) + print("size :", ole.get_size('worddocument')) + if ole.exists('macros/vba'): + print("This document may contain VBA macros.") + + # print parsing issues: + print('\nNon-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') ## except IOError as v: ## print("***", "cannot read", file, "-", v) + +# this code was developed while listening to The Wedding Present "Sea Monsters" diff --git a/PIL/PSDraw.py b/PIL/PSDraw.py index 26fdb74ea..a090a8f64 100644 --- a/PIL/PSDraw.py +++ b/PIL/PSDraw.py @@ -15,14 +15,13 @@ # See the README file for information on usage and redistribution. # -from __future__ import print_function - from PIL import EpsImagePlugin + ## # Simple Postscript graphics interface. -class PSDraw: +class PSDraw(object): """ Sets up printing to the given file. If **file** is omitted, :py:attr:`sys.stdout` is assumed. @@ -34,25 +33,31 @@ class PSDraw: fp = sys.stdout self.fp = fp - def begin_document(self, id = None): + def _fp_write(self, to_write): + if bytes is str: + self.fp.write(to_write) + else: + self.fp.write(bytes(to_write, 'UTF-8')) + + def begin_document(self, id=None): """Set up printing of a document. (Write Postscript DSC header.)""" # FIXME: incomplete - self.fp.write("%!PS-Adobe-3.0\n" - "save\n" - "/showpage { } def\n" - "%%EndComments\n" - "%%BeginDocument\n") - #self.fp.write(ERROR_PS) # debugging! - self.fp.write(EDROFF_PS) - self.fp.write(VDI_PS) - self.fp.write("%%EndProlog\n") + self._fp_write("%!PS-Adobe-3.0\n" + "save\n" + "/showpage { } def\n" + "%%EndComments\n" + "%%BeginDocument\n") + # self.fp_write(ERROR_PS) # debugging! + self._fp_write(EDROFF_PS) + self._fp_write(VDI_PS) + self._fp_write("%%EndProlog\n") self.isofont = {} def end_document(self): """Ends printing. (Write Postscript DSC footer.)""" - self.fp.write("%%EndDocument\n" - "restore showpage\n" - "%%End\n") + self._fp_write("%%EndDocument\n" + "restore showpage\n" + "%%End\n") if hasattr(self.fp, "flush"): self.fp.flush() @@ -65,18 +70,11 @@ class PSDraw: """ if font not in self.isofont: # reencode font - self.fp.write("/PSDraw-%s ISOLatin1Encoding /%s E\n" %\ - (font, font)) + self._fp_write("/PSDraw-%s ISOLatin1Encoding /%s E\n" % + (font, font)) self.isofont[font] = 1 # rough - self.fp.write("/F0 %d /PSDraw-%s F\n" % (size, font)) - - def setink(self, ink): - """ - .. warning:: This has been in the PIL API for ages but was never implemented. - - """ - print("*** NOT YET IMPLEMENTED ***") + self._fp_write("/F0 %d /PSDraw-%s F\n" % (size, font)) def line(self, xy0, xy1): """ @@ -85,7 +83,7 @@ class PSDraw: left corner of the page). """ xy = xy0 + xy1 - self.fp.write("%d %d %d %d Vl\n" % xy) + self._fp_write("%d %d %d %d Vl\n" % xy) def rectangle(self, box): """ @@ -100,7 +98,7 @@ class PSDraw: %d %d M %d %d 0 Vr\n """ - self.fp.write("%d %d M %d %d 0 Vr\n" % box) + self._fp_write("%d %d M %d %d 0 Vr\n" % box) def text(self, xy, text): """ @@ -110,16 +108,16 @@ class PSDraw: text = "\\(".join(text.split("(")) text = "\\)".join(text.split(")")) xy = xy + (text,) - self.fp.write("%d %d M (%s) S\n" % xy) + self._fp_write("%d %d M (%s) S\n" % xy) - def image(self, box, im, dpi = None): + def image(self, box, im, dpi=None): """Draw a PIL image, centered in the given box.""" # default resolution depends on mode if not dpi: if im.mode == "1": - dpi = 200 # fax + dpi = 200 # fax else: - dpi = 100 # greyscale + dpi = 100 # greyscale # image size (on paper) x = float(im.size[0] * 72) / dpi y = float(im.size[1] * 72) / dpi @@ -127,19 +125,21 @@ class PSDraw: xmax = float(box[2] - box[0]) ymax = float(box[3] - box[1]) if x > xmax: - y = y * xmax / x; x = xmax + y = y * xmax / x + x = xmax if y > ymax: - x = x * ymax / y; y = ymax + x = x * ymax / y + y = ymax dx = (xmax - x) / 2 + box[0] dy = (ymax - y) / 2 + box[1] - self.fp.write("gsave\n%f %f translate\n" % (dx, dy)) + self._fp_write("gsave\n%f %f translate\n" % (dx, dy)) if (x, y) != im.size: # EpsImagePlugin._save prints the image at (0,0,xsize,ysize) sx = x / im.size[0] sy = y / im.size[1] - self.fp.write("%f %f scale\n" % (sx, sy)) + self._fp_write("%f %f scale\n" % (sx, sy)) EpsImagePlugin._save(im, self.fp, None, 0) - self.fp.write("\ngrestore\n") + self._fp_write("\ngrestore\n") # -------------------------------------------------------------------- # Postscript driver diff --git a/PIL/PaletteFile.py b/PIL/PaletteFile.py index 5627f7b86..ef50feefd 100644 --- a/PIL/PaletteFile.py +++ b/PIL/PaletteFile.py @@ -15,10 +15,11 @@ from PIL._binary import o8 + ## # File handler for Teragon-style palette files. -class PaletteFile: +class PaletteFile(object): rawmode = "RGB" @@ -49,7 +50,6 @@ class PaletteFile: self.palette = b"".join(self.palette) - def getpalette(self): return self.palette, self.rawmode diff --git a/PIL/PcdImagePlugin.py b/PIL/PcdImagePlugin.py index 70066e76b..06db5d628 100644 --- a/PIL/PcdImagePlugin.py +++ b/PIL/PcdImagePlugin.py @@ -22,6 +22,7 @@ from PIL import Image, ImageFile, _binary i8 = _binary.i8 + ## # Image plugin for PhotoCD images. This plugin only reads the 768x512 # image from the file; higher resolutions are encoded in a proprietary @@ -43,32 +44,13 @@ class PcdImageFile(ImageFile.ImageFile): orientation = i8(s[1538]) & 3 if orientation == 1: - self.tile_post_rotate = 90 # hack + self.tile_post_rotate = 90 # hack elif orientation == 3: self.tile_post_rotate = -90 self.mode = "RGB" - self.size = 768, 512 # FIXME: not correct for rotated images! - self.tile = [("pcd", (0,0)+self.size, 96*2048, None)] - - def draft(self, mode, size): - - if len(self.tile) != 1: - return - - d, e, o, a = self.tile[0] - - if size: - scale = max(self.size[0] / size[0], self.size[1] / size[1]) - for s, o in [(4,0*2048), (2,0*2048), (1,96*2048)]: - if scale >= s: - break - # e = e[0], e[1], (e[2]-e[0]+s-1)/s+e[0], (e[3]-e[1]+s-1)/s+e[1] - # self.size = ((self.size[0]+s-1)/s, (self.size[1]+s-1)/s) - - self.tile = [(d, e, o, a)] - - return self + self.size = 768, 512 # FIXME: not correct for rotated images! + self.tile = [("pcd", (0, 0)+self.size, 96*2048, None)] # # registry diff --git a/PIL/PcfFontFile.py b/PIL/PcfFontFile.py index c40d3986d..c2006905e 100644 --- a/PIL/PcfFontFile.py +++ b/PIL/PcfFontFile.py @@ -23,20 +23,20 @@ from PIL import _binary # -------------------------------------------------------------------- # declarations -PCF_MAGIC = 0x70636601 # "\x01fcp" +PCF_MAGIC = 0x70636601 # "\x01fcp" -PCF_PROPERTIES = (1<<0) -PCF_ACCELERATORS = (1<<1) -PCF_METRICS = (1<<2) -PCF_BITMAPS = (1<<3) -PCF_INK_METRICS = (1<<4) -PCF_BDF_ENCODINGS = (1<<5) -PCF_SWIDTHS = (1<<6) -PCF_GLYPH_NAMES = (1<<7) -PCF_BDF_ACCELERATORS = (1<<8) +PCF_PROPERTIES = (1 << 0) +PCF_ACCELERATORS = (1 << 1) +PCF_METRICS = (1 << 2) +PCF_BITMAPS = (1 << 3) +PCF_INK_METRICS = (1 << 4) +PCF_BDF_ENCODINGS = (1 << 5) +PCF_SWIDTHS = (1 << 6) +PCF_GLYPH_NAMES = (1 << 7) +PCF_BDF_ACCELERATORS = (1 << 8) BYTES_PER_ROW = [ - lambda bits: ((bits+7) >> 3), + lambda bits: ((bits+7) >> 3), lambda bits: ((bits+15) >> 3) & ~1, lambda bits: ((bits+31) >> 3) & ~3, lambda bits: ((bits+63) >> 3) & ~7, @@ -48,9 +48,11 @@ l32 = _binary.i32le b16 = _binary.i16be b32 = _binary.i32be + def sz(s, o): return s[o:s.index(b"\0", o)] + ## # Font file plugin for the X11 PCF format. @@ -122,7 +124,7 @@ class PcfFontFile(FontFile.FontFile): for i in range(nprops): p.append((i32(fp.read(4)), i8(fp.read(1)), i32(fp.read(4)))) if nprops & 3: - fp.seek(4 - (nprops & 3), 1) # pad + fp.seek(4 - (nprops & 3), 1) # pad data = fp.read(i32(fp.read(4))) @@ -202,16 +204,16 @@ class PcfFontFile(FontFile.FontFile): for i in range(4): bitmapSizes.append(i32(fp.read(4))) - byteorder = format & 4 # non-zero => MSB - bitorder = format & 8 # non-zero => MSB - padindex = format & 3 + # byteorder = format & 4 # non-zero => MSB + bitorder = format & 8 # non-zero => MSB + padindex = format & 3 bitmapsize = bitmapSizes[padindex] offsets.append(bitmapsize) data = fp.read(bitmapsize) - pad = BYTES_PER_ROW[padindex] + pad = BYTES_PER_ROW[padindex] mode = "1;R" if bitorder: mode = "1" @@ -245,6 +247,6 @@ class PcfFontFile(FontFile.FontFile): try: encoding[i+firstCol] = encodingOffset except IndexError: - break # only load ISO-8859-1 glyphs + break # only load ISO-8859-1 glyphs return encoding diff --git a/PIL/PcxImagePlugin.py b/PIL/PcxImagePlugin.py index 4f6d5a3e5..40fafa0dc 100644 --- a/PIL/PcxImagePlugin.py +++ b/PIL/PcxImagePlugin.py @@ -25,7 +25,7 @@ # See the README file for information on usage and redistribution. # -__version__ = "0.6" +from __future__ import print_function from PIL import Image, ImageFile, ImagePalette, _binary @@ -33,9 +33,13 @@ i8 = _binary.i8 i16 = _binary.i16le o8 = _binary.o8 +__version__ = "0.6" + + def _accept(prefix): return i8(prefix[0]) == 10 and i8(prefix[1]) in [0, 2, 3, 5] + ## # Image plugin for Paintbrush images. @@ -52,23 +56,22 @@ class PcxImageFile(ImageFile.ImageFile): raise SyntaxError("not a PCX file") # image - bbox = i16(s,4), i16(s,6), i16(s,8)+1, i16(s,10)+1 + 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) - + print("BBox: %s %s %s %s" % bbox) # format version = i8(s[1]) bits = i8(s[3]) planes = i8(s[65]) - stride = i16(s,66) + stride = i16(s, 66) if Image.DEBUG: - print ("PCX version %s, bits %s, planes %s, stride %s" % - (version, bits, planes, stride)) + print("PCX version %s, bits %s, planes %s, stride %s" % + (version, bits, planes, stride)) - self.info["dpi"] = i16(s,12), i16(s,14) + self.info["dpi"] = i16(s, 12), i16(s, 14) if bits == 1 and planes == 1: mode = rawmode = "1" @@ -105,8 +108,8 @@ class PcxImageFile(ImageFile.ImageFile): bbox = (0, 0) + self.size if Image.DEBUG: - print ("size: %sx%s" % self.size) - + print("size: %sx%s" % self.size) + self.tile = [("pcx", bbox, self.fp.tell(), (rawmode, planes * stride))] # -------------------------------------------------------------------- @@ -122,6 +125,7 @@ SAVE = { o16 = _binary.o16le + def _save(im, fp, filename, check=0): try: @@ -138,11 +142,10 @@ def _save(im, fp, filename, check=0): stride += stride % 2 # Stride needs to be kept in sync with the PcxEncode.c version. # Ideally it should be passed in in the state, but the bytes value - # gets overwritten. - + # gets overwritten. if Image.DEBUG: - print ("PcxImagePlugin._save: xwidth: %d, bits: %d, stride: %d" % ( + print("PcxImagePlugin._save: xwidth: %d, bits: %d, stride: %d" % ( im.size[0], bits, stride)) # under windows, we could determine the current screen size with @@ -163,13 +166,13 @@ def _save(im, fp, filename, check=0): assert fp.tell() == 128 - ImageFile._save(im, fp, [("pcx", (0,0)+im.size, 0, + ImageFile._save(im, fp, [("pcx", (0, 0)+im.size, 0, (rawmode, bits*planes))]) if im.mode == "P": # colour palette fp.write(o8(12)) - fp.write(im.im.getpalette("RGB", "RGB")) # 768 bytes + fp.write(im.im.getpalette("RGB", "RGB")) # 768 bytes elif im.mode == "L": # greyscale palette fp.write(o8(12)) diff --git a/PIL/PdfImagePlugin.py b/PIL/PdfImagePlugin.py index 5113f099e..1d8c2ff93 100644 --- a/PIL/PdfImagePlugin.py +++ b/PIL/PdfImagePlugin.py @@ -63,7 +63,7 @@ def _save(im, fp, filename): xref = [0]*(5+1) # placeholders - class TextWriter: + class TextWriter(object): def __init__(self, fp): self.fp = fp diff --git a/PIL/PixarImagePlugin.py b/PIL/PixarImagePlugin.py index a4c9032dc..ebf4c8c61 100644 --- a/PIL/PixarImagePlugin.py +++ b/PIL/PixarImagePlugin.py @@ -29,6 +29,7 @@ from PIL import Image, ImageFile, _binary i16 = _binary.i16le i32 = _binary.i32le + ## # Image plugin for PIXAR raster images. @@ -57,7 +58,7 @@ class PixarImageFile(ImageFile.ImageFile): # FIXME: to be continued... # create tile descriptor (assuming "dumped") - self.tile = [("raw", (0,0)+self.size, 1024, (self.mode, 0, 1))] + self.tile = [("raw", (0, 0)+self.size, 1024, (self.mode, 0, 1))] # # -------------------------------------------------------------------- diff --git a/PIL/PngImagePlugin.py b/PIL/PngImagePlugin.py index 4dbedb783..214ff9385 100644 --- a/PIL/PngImagePlugin.py +++ b/PIL/PngImagePlugin.py @@ -52,30 +52,46 @@ _MAGIC = b"\211PNG\r\n\032\n" _MODES = { # supported bits/color combinations, and corresponding modes/rawmodes - (1, 0): ("1", "1"), - (2, 0): ("L", "L;2"), - (4, 0): ("L", "L;4"), - (8, 0): ("L", "L"), - (16,0): ("I", "I;16B"), - (8, 2): ("RGB", "RGB"), - (16,2): ("RGB", "RGB;16B"), - (1, 3): ("P", "P;1"), - (2, 3): ("P", "P;2"), - (4, 3): ("P", "P;4"), - (8, 3): ("P", "P"), - (8, 4): ("LA", "LA"), - (16,4): ("RGBA", "LA;16B"), # LA;16B->LA not yet available - (8, 6): ("RGBA", "RGBA"), - (16,6): ("RGBA", "RGBA;16B"), + (1, 0): ("1", "1"), + (2, 0): ("L", "L;2"), + (4, 0): ("L", "L;4"), + (8, 0): ("L", "L"), + (16, 0): ("I", "I;16B"), + (8, 2): ("RGB", "RGB"), + (16, 2): ("RGB", "RGB;16B"), + (1, 3): ("P", "P;1"), + (2, 3): ("P", "P;2"), + (4, 3): ("P", "P;4"), + (8, 3): ("P", "P"), + (8, 4): ("LA", "LA"), + (16, 4): ("RGBA", "LA;16B"), # LA;16B->LA not yet available + (8, 6): ("RGBA", "RGBA"), + (16, 6): ("RGBA", "RGBA;16B"), } _simple_palette = re.compile(b'^\xff+\x00\xff*$') +_null_palette = re.compile(b'^\x00*$') + +# Maximum decompressed size for a iTXt or zTXt chunk. +# Eliminates decompression bombs where compressed chunks can expand 1000x +MAX_TEXT_CHUNK = ImageFile.SAFEBLOCK +# Set the maximum total text chunk size. +MAX_TEXT_MEMORY = 64 * MAX_TEXT_CHUNK + + +def _safe_zlib_decompress(s): + dobj = zlib.decompressobj() + plaintext = dobj.decompress(s, MAX_TEXT_CHUNK) + if dobj.unconsumed_tail: + raise ValueError("Decompressed Data Too Large") + return plaintext + # -------------------------------------------------------------------- # Support classes. Suitable for PNG and related formats like MNG etc. -class ChunkStream: +class ChunkStream(object): def __init__(self, fp): @@ -123,15 +139,15 @@ class ChunkStream: crc1 = Image.core.crc32(data, Image.core.crc32(cid)) crc2 = i16(self.fp.read(2)), i16(self.fp.read(2)) if crc1 != crc2: - raise SyntaxError("broken PNG file"\ - "(bad header checksum in %s)" % cid) + raise SyntaxError("broken PNG file" + "(bad header checksum in %s)" % cid) def crc_skip(self, cid, data): "Read checksum. Used if the C module is not present" self.fp.read(4) - def verify(self, endchunk = b"IEND"): + def verify(self, endchunk=b"IEND"): # Simple approach; just calculate checksum for all remaining # blocks. Must be called directly after open. @@ -147,30 +163,57 @@ class ChunkStream: return cids -# -------------------------------------------------------------------- -# Subclass of string to allow iTXt chunks to look like strings while -# keeping their extra information class iTXt(str): + """ + Subclass of string to allow iTXt chunks to look like strings while + keeping their extra information + + """ @staticmethod def __new__(cls, text, lang, tkey): + """ + :param value: value for this key + :param lang: language code + :param tkey: UTF-8 version of the key name + """ + self = str.__new__(cls, text) self.lang = lang self.tkey = tkey return self -# -------------------------------------------------------------------- -# PNG chunk container (for use with save(pnginfo=)) -class PngInfo: +class PngInfo(object): + """ + PNG chunk container (for use with save(pnginfo=)) + + """ def __init__(self): self.chunks = [] def add(self, cid, data): + """Appends an arbitrary chunk. Use with caution. + + :param cid: a byte string, 4 bytes long. + :param data: a byte string of the encoded data + + """ + self.chunks.append((cid, data)) def add_itxt(self, key, value, lang="", tkey="", zip=False): + """Appends an iTXt chunk. + + :param key: latin-1 encodable text key name + :param value: value for this key + :param lang: language code + :param tkey: UTF-8 version of the key name + :param zip: compression flag + + """ + if not isinstance(key, bytes): key = key.encode("latin-1", "strict") if not isinstance(value, bytes): @@ -181,12 +224,21 @@ class PngInfo: tkey = tkey.encode("utf-8", "strict") if zip: - import zlib - self.add(b"iTXt", key + b"\0\x01\0" + lang + b"\0" + tkey + b"\0" + zlib.compress(value)) + self.add(b"iTXt", key + b"\0\x01\0" + lang + b"\0" + tkey + b"\0" + + zlib.compress(value)) else: - self.add(b"iTXt", key + b"\0\0\0" + lang + b"\0" + tkey + b"\0" + value) + self.add(b"iTXt", key + b"\0\0\0" + lang + b"\0" + tkey + b"\0" + + value) def add_text(self, key, value, zip=0): + """Appends a text chunk. + + :param key: latin-1 encodable text key name + :param value: value for this key, text or an + :py:class:`PIL.PngImagePlugin.iTXt` instance + :param zip: compression flag + + """ if isinstance(value, iTXt): return self.add_itxt(key, value, value.lang, value.tkey, bool(zip)) @@ -201,11 +253,11 @@ class PngInfo: key = key.encode('latin-1', 'strict') if zip: - import zlib self.add(b"zTXt", key + b"\0\0" + zlib.compress(value)) else: self.add(b"tEXt", key + b"\0" + value) + # -------------------------------------------------------------------- # PNG image stream (IHDR/IEND) @@ -218,11 +270,19 @@ class PngStream(ChunkStream): # local copies of Image attributes self.im_info = {} self.im_text = {} - self.im_size = (0,0) + self.im_size = (0, 0) self.im_mode = None self.im_tile = None self.im_palette = None + self.text_memory = 0 + + def check_text_memory(self, chunklen): + self.text_memory += chunklen + if self.text_memory > MAX_TEXT_MEMORY: + raise ValueError("Too much memory used in text chunks: %s>MAX_TEXT_MEMORY" % + self.text_memory) + def chunk_iCCP(self, pos, length): # ICC profile @@ -238,11 +298,12 @@ class PngStream(ChunkStream): print("Compression method", i8(s[i])) comp_method = i8(s[i]) if comp_method != 0: - raise SyntaxError("Unknown compression method %s in iCCP chunk" % comp_method) + raise SyntaxError("Unknown compression method %s in iCCP chunk" % + comp_method) try: - icc_profile = zlib.decompress(s[i+2:]) + icc_profile = _safe_zlib_decompress(s[i+2:]) except zlib.error: - icc_profile = None # FIXME + icc_profile = None # FIXME self.im_info["icc_profile"] = icc_profile return s @@ -264,7 +325,7 @@ class PngStream(ChunkStream): def chunk_IDAT(self, pos, length): # image data - self.im_tile = [("zip", (0,0)+self.im_size, pos, self.im_rawmode)] + self.im_tile = [("zip", (0, 0)+self.im_size, pos, self.im_rawmode)] self.im_idat = length raise EOFError @@ -290,6 +351,8 @@ class PngStream(ChunkStream): i = s.find(b"\0") if i >= 0: self.im_info["transparency"] = i + elif _null_palette.match(s): + self.im_info["transparency"] = 0 else: self.im_info["transparency"] = s elif self.im_mode == "L": @@ -311,7 +374,7 @@ class PngStream(ChunkStream): s = ImageFile._safe_read(self.fp, length) px, py = i32(s), i32(s[4:]) unit = i8(s[8]) - if unit == 1: # meter + if unit == 1: # meter dpi = int(px * 0.0254 + 0.5), int(py * 0.0254 + 0.5) self.im_info["dpi"] = dpi elif unit == 0: @@ -325,13 +388,17 @@ class PngStream(ChunkStream): try: k, v = s.split(b"\0", 1) except ValueError: - k = s; v = b"" # fallback for broken tEXt tags + # fallback for broken tEXt tags + k = s + v = b"" if k: if bytes is not str: k = k.decode('latin-1', 'strict') v = v.decode('latin-1', 'replace') self.im_info[k] = self.im_text[k] = v + self.check_text_memory(len(v)) + return s def chunk_zTXt(self, pos, length): @@ -341,16 +408,17 @@ class PngStream(ChunkStream): try: k, v = s.split(b"\0", 1) except ValueError: - k = s; v = b"" + k = s + v = b"" if v: comp_method = i8(v[0]) else: comp_method = 0 if comp_method != 0: - raise SyntaxError("Unknown compression method %s in zTXt chunk" % comp_method) - import zlib + raise SyntaxError("Unknown compression method %s in zTXt chunk" % + comp_method) try: - v = zlib.decompress(v[1:]) + v = _safe_zlib_decompress(v[1:]) except zlib.error: v = b"" @@ -360,6 +428,8 @@ class PngStream(ChunkStream): v = v.decode('latin-1', 'replace') self.im_info[k] = self.im_text[k] = v + self.check_text_memory(len(v)) + return s def chunk_iTXt(self, pos, length): @@ -379,9 +449,8 @@ class PngStream(ChunkStream): return s if cf != 0: if cm == 0: - import zlib try: - v = zlib.decompress(v) + v = _safe_zlib_decompress(v) except zlib.error: return s else: @@ -396,15 +465,18 @@ class PngStream(ChunkStream): return s self.im_info[k] = self.im_text[k] = iTXt(v, lang, tk) - + self.check_text_memory(len(v)) + return s + # -------------------------------------------------------------------- # PNG reader def _accept(prefix): return prefix[:8] == _MAGIC + ## # Image plugin for PNG images. @@ -451,7 +523,7 @@ class PngImageFile(ImageFile.ImageFile): self.mode = self.png.im_mode self.size = self.png.im_size self.info = self.png.im_info - self.text = self.png.im_text # experimental + self.text = self.png.im_text # experimental self.tile = self.png.im_tile if self.png.im_palette: @@ -460,7 +532,6 @@ class PngImageFile(ImageFile.ImageFile): self.__idat = length # used by load_read() - def verify(self): "Verify PNG file" @@ -489,7 +560,7 @@ class PngImageFile(ImageFile.ImageFile): while self.__idat == 0: # end of chunk, skip forward to next one - self.fp.read(4) # CRC + self.fp.read(4) # CRC cid, pos, length = self.png.read() @@ -509,7 +580,6 @@ class PngImageFile(ImageFile.ImageFile): return self.fp.read(read_bytes) - def load_end(self): "internal: finished reading image data" @@ -526,21 +596,22 @@ o32 = _binary.o32be _OUTMODES = { # supported PIL modes, and corresponding rawmodes/bits/color combinations - "1": ("1", b'\x01\x00'), - "L;1": ("L;1", b'\x01\x00'), - "L;2": ("L;2", b'\x02\x00'), - "L;4": ("L;4", b'\x04\x00'), - "L": ("L", b'\x08\x00'), - "LA": ("LA", b'\x08\x04'), - "I": ("I;16B", b'\x10\x00'), - "P;1": ("P;1", b'\x01\x03'), - "P;2": ("P;2", b'\x02\x03'), - "P;4": ("P;4", b'\x04\x03'), - "P": ("P", b'\x08\x03'), - "RGB": ("RGB", b'\x08\x02'), - "RGBA":("RGBA", b'\x08\x06'), + "1": ("1", b'\x01\x00'), + "L;1": ("L;1", b'\x01\x00'), + "L;2": ("L;2", b'\x02\x00'), + "L;4": ("L;4", b'\x04\x00'), + "L": ("L", b'\x08\x00'), + "LA": ("LA", b'\x08\x04'), + "I": ("I;16B", b'\x10\x00'), + "P;1": ("P;1", b'\x01\x03'), + "P;2": ("P;2", b'\x02\x03'), + "P;4": ("P;4", b'\x04\x03'), + "P": ("P", b'\x08\x03'), + "RGB": ("RGB", b'\x08\x02'), + "RGBA": ("RGBA", b'\x08\x06'), } + def putchunk(fp, cid, *data): "Write a PNG chunk (including CRC field)" @@ -551,15 +622,18 @@ def putchunk(fp, cid, *data): hi, lo = Image.core.crc32(data, Image.core.crc32(cid)) fp.write(o16(hi) + o16(lo)) -class _idat: + +class _idat(object): # wrap output from the encoder in IDAT chunks def __init__(self, fp, chunk): self.fp = fp self.chunk = chunk + def write(self, data): self.chunk(self.fp, b"IDAT", data) + def _save(im, fp, filename, chunk=putchunk, check=0): # save an image to disk (called by the save method) @@ -597,9 +671,9 @@ def _save(im, fp, filename, chunk=putchunk, check=0): dictionary = b"" im.encoderconfig = ("optimize" in im.encoderinfo, - im.encoderinfo.get("compress_level", -1), - im.encoderinfo.get("compress_type", -1), - dictionary) + im.encoderinfo.get("compress_level", -1), + im.encoderinfo.get("compress_type", -1), + dictionary) # get the corresponding PNG mode try: @@ -616,8 +690,8 @@ def _save(im, fp, filename, chunk=putchunk, check=0): fp.write(_MAGIC) chunk(fp, b"IHDR", - o32(im.size[0]), o32(im.size[1]), # 0: size - mode, # 8: depth/type + o32(im.size[0]), o32(im.size[1]), # 0: size + mode, # 8: depth/type b'\0', # 10: compression b'\0', # 11: filter category b'\0') # 12: interlace flag @@ -629,7 +703,8 @@ def _save(im, fp, filename, chunk=putchunk, check=0): palette_bytes += b'\0' chunk(fp, b"PLTE", palette_bytes) - transparency = im.encoderinfo.get('transparency',im.info.get('transparency', None)) + transparency = im.encoderinfo.get('transparency', + im.info.get('transparency', None)) if transparency or transparency == 0: if im.mode == "P": @@ -658,10 +733,6 @@ def _save(im, fp, filename, chunk=putchunk, check=0): alpha_bytes = 2**bits chunk(fp, b"tRNS", alpha[:alpha_bytes]) - if 0: - # FIXME: to be supported some day - chunk(fp, b"gAMA", o32(int(gamma * 100000.0))) - dpi = im.encoderinfo.get("dpi") if dpi: chunk(fp, b"pHYs", @@ -686,7 +757,8 @@ def _save(im, fp, filename, chunk=putchunk, check=0): data = name + b"\0\0" + zlib.compress(im.info["icc_profile"]) chunk(fp, b"iCCP", data) - ImageFile._save(im, _idat(fp, chunk), [("zip", (0,0)+im.size, 0, rawmode)]) + ImageFile._save(im, _idat(fp, chunk), + [("zip", (0, 0)+im.size, 0, rawmode)]) chunk(fp, b"IEND", b"") @@ -702,10 +774,12 @@ def _save(im, fp, filename, chunk=putchunk, check=0): def getchunks(im, **params): """Return a list of PNG chunks representing this image.""" - class collector: + class collector(object): data = [] + def write(self, data): pass + def append(self, chunk): self.data.append(chunk) diff --git a/PIL/PpmImagePlugin.py b/PIL/PpmImagePlugin.py index 070efd185..954832451 100644 --- a/PIL/PpmImagePlugin.py +++ b/PIL/PpmImagePlugin.py @@ -27,12 +27,13 @@ from PIL import Image, ImageFile b_whitespace = string.whitespace try: import locale - locale_lang,locale_enc = locale.getlocale() + locale_lang, locale_enc = locale.getlocale() if locale_enc is None: - locale_lang,locale_enc = locale.getdefaultlocale() + locale_lang, locale_enc = locale.getdefaultlocale() b_whitespace = b_whitespace.decode(locale_enc) -except: pass -b_whitespace = b_whitespace.encode('ascii','ignore') +except: + pass +b_whitespace = b_whitespace.encode('ascii', 'ignore') MODES = { # standard @@ -47,9 +48,11 @@ MODES = { b"PyCMYK": "CMYK" } + def _accept(prefix): return prefix[0:1] == b"P" and prefix[1] in b"0456y" + ## # Image plugin for PBM, PGM, and PPM images. @@ -58,8 +61,8 @@ class PpmImageFile(ImageFile.ImageFile): format = "PPM" format_description = "Pbmplus image" - def _token(self, s = b""): - while True: # read until next whitespace + def _token(self, s=b""): + while True: # read until next whitespace c = self.fp.read(1) if not c or c in b_whitespace: break @@ -104,14 +107,14 @@ class PpmImageFile(ImageFile.ImageFile): # maxgrey if s > 255: if not mode == 'L': - raise ValueError("Too many colors for band: %s" %s) + raise ValueError("Too many colors for band: %s" % s) if s < 2**16: self.mode = 'I' rawmode = 'I;16B' else: - self.mode = 'I'; + self.mode = 'I' rawmode = 'I;32B' - + self.size = xsize, ysize self.tile = [("raw", (0, 0, xsize, ysize), @@ -123,6 +126,7 @@ class PpmImageFile(ImageFile.ImageFile): # self.mode = self.im.mode # self.size = self.im.size + # # -------------------------------------------------------------------- @@ -152,7 +156,7 @@ def _save(im, fp, filename): fp.write(b"65535\n") elif rawmode == "I;32B": fp.write(b"2147483648\n") - ImageFile._save(im, fp, [("raw", (0,0)+im.size, 0, (rawmode, 0, 1))]) + ImageFile._save(im, fp, [("raw", (0, 0)+im.size, 0, (rawmode, 0, 1))]) # ALTERNATIVE: save via builtin debug function # im._dump(filename) diff --git a/PIL/PsdImagePlugin.py b/PIL/PsdImagePlugin.py index 9e64e7c90..d30695adb 100644 --- a/PIL/PsdImagePlugin.py +++ b/PIL/PsdImagePlugin.py @@ -28,8 +28,8 @@ MODES = { (2, 8): ("P", 1), (3, 8): ("RGB", 3), (4, 8): ("CMYK", 4), - (7, 8): ("L", 1), # FIXME: multilayer - (8, 8): ("L", 1), # duotone + (7, 8): ("L", 1), # FIXME: multilayer + (8, 8): ("L", 1), # duotone (9, 8): ("LAB", 3) } @@ -40,12 +40,14 @@ i8 = _binary.i8 i16 = _binary.i16be i32 = _binary.i32be + # --------------------------------------------------------------------. # read PSD images def _accept(prefix): return prefix[:4] == b"8BPS" + ## # Image plugin for Photoshop images. @@ -100,12 +102,12 @@ class PsdImageFile(ImageFile.ImageFile): id = i16(read(2)) name = read(i8(read(1))) if not (len(name) & 1): - read(1) # padding + read(1) # padding data = read(i32(read(4))) if (len(data) & 1): - read(1) # padding + read(1) # padding self.resources.append((id, name, data)) - if id == 1039: # ICC profile + if id == 1039: # ICC profile self.info["icc_profile"] = data # @@ -130,6 +132,10 @@ class PsdImageFile(ImageFile.ImageFile): self._fp = self.fp self.frame = 0 + @property + def n_frames(self): + return len(self.layers) + def seek(self, layer): # seek to given layer (1..max) if layer == self.frame: @@ -159,6 +165,7 @@ class PsdImageFile(ImageFile.ImageFile): if self.mode == "P": Image.Image.load(self) + def _layerinfo(file): # read layerinfo block layers = [] @@ -166,8 +173,10 @@ def _layerinfo(file): for i in range(abs(i16(read(2)))): # bounding box - y0 = i32(read(4)); x0 = i32(read(4)) - y1 = i32(read(4)); x1 = i32(read(4)) + y0 = i32(read(4)) + x0 = i32(read(4)) + y1 = i32(read(4)) + x1 = i32(read(4)) # image info info = [] @@ -197,7 +206,7 @@ def _layerinfo(file): elif mode == ["A", "B", "G", "R"]: mode = "RGBA" else: - mode = None # unknown + mode = None # unknown # skip over blend flags and extra information filler = read(12) @@ -207,8 +216,10 @@ def _layerinfo(file): if size: length = i32(read(4)) if length: - mask_y = i32(read(4)); mask_x = i32(read(4)) - mask_h = i32(read(4)) - mask_y; mask_w = i32(read(4)) - mask_x + mask_y = i32(read(4)) + mask_x = i32(read(4)) + mask_h = i32(read(4)) - mask_y + mask_w = i32(read(4)) - mask_x file.seek(length - 16, 1) combined += length + 4 @@ -219,7 +230,8 @@ def _layerinfo(file): length = i8(read(1)) if length: - # Don't know the proper encoding, Latin-1 should be a good guess + # Don't know the proper encoding, + # Latin-1 should be a good guess name = read(length).decode('latin-1', 'replace') combined += length + 1 @@ -239,6 +251,7 @@ def _layerinfo(file): return layers + def _maketile(file, mode, bbox, channels): tile = None @@ -283,7 +296,7 @@ def _maketile(file, mode, bbox, channels): file.seek(offset) if offset & 1: - read(1) # padding + read(1) # padding return tile diff --git a/PIL/PyAccess.py b/PIL/PyAccess.py index 7ccc313eb..4924facd5 100644 --- a/PIL/PyAccess.py +++ b/PIL/PyAccess.py @@ -16,7 +16,8 @@ # * Implements the pixel access object following Access. # * Does not implement the line functions, as they don't appear to be used # * Taking only the tuple form, which is used from python. -# * Fill.c uses the integer form, but it's still going to use the old Access.c implementation. +# * Fill.c uses the integer form, but it's still going to use the old +# Access.c implementation. # from __future__ import print_function @@ -25,7 +26,7 @@ from cffi import FFI import sys DEBUG = 0 - + defs = """ struct Pixel_RGBA { unsigned char r,g,b,a; @@ -39,8 +40,8 @@ ffi.cdef(defs) class PyAccess(object): - - def __init__(self, img, readonly = False): + + def __init__(self, img, readonly=False): vals = dict(img.im.unsafe_ptrs) self.readonly = readonly self.image8 = ffi.cast('unsigned char **', vals['image8']) @@ -48,13 +49,14 @@ class PyAccess(object): self.image = ffi.cast('unsigned char **', vals['image']) self.xsize = vals['xsize'] self.ysize = vals['ysize'] - + if DEBUG: - print (vals) + print(vals) self._post_init() - def _post_init(): pass - + def _post_init(self): + pass + def __setitem__(self, xy, color): """ Modifies the pixel at x,y. The color is given as a single @@ -62,11 +64,12 @@ class PyAccess(object): multi-band images :param xy: The pixel coordinate, given as (x, y). - :param value: The pixel value. + :param value: The pixel value. """ - if self.readonly: raise ValueError('Attempt to putpixel a read only image') - (x,y) = self.check_xy(xy) - return self.set_pixel(x,y,color) + if self.readonly: + raise ValueError('Attempt to putpixel a read only image') + (x, y) = self.check_xy(xy) + return self.set_pixel(x, y, color) def __getitem__(self, xy): """ @@ -75,95 +78,101 @@ class PyAccess(object): images :param xy: The pixel coordinate, given as (x, y). + :returns: a pixel value for single band images, a tuple of + pixel values for multiband images. """ - - (x,y) = self.check_xy(xy) - return self.get_pixel(x,y) + + (x, y) = self.check_xy(xy) + return self.get_pixel(x, y) putpixel = __setitem__ getpixel = __getitem__ def check_xy(self, xy): - (x,y) = xy + (x, y) = xy if not (0 <= x < self.xsize and 0 <= y < self.ysize): raise ValueError('pixel location out of range') return xy + class _PyAccess32_2(PyAccess): """ PA, LA, stored in first and last bytes of a 32 bit word """ def _post_init(self, *args, **kwargs): self.pixels = ffi.cast("struct Pixel_RGBA **", self.image32) - - def get_pixel(self, x,y): + + def get_pixel(self, x, y): pixel = self.pixels[y][x] return (pixel.r, pixel.a) - def set_pixel(self, x,y, color): + def set_pixel(self, x, y, color): pixel = self.pixels[y][x] # tuple - pixel.r = min(color[0],255) - pixel.a = min(color[1],255) - + pixel.r = min(color[0], 255) + pixel.a = min(color[1], 255) + + class _PyAccess32_3(PyAccess): """ RGB and friends, stored in the first three bytes of a 32 bit word """ - + def _post_init(self, *args, **kwargs): self.pixels = ffi.cast("struct Pixel_RGBA **", self.image32) - - def get_pixel(self, x,y): + + def get_pixel(self, x, y): pixel = self.pixels[y][x] return (pixel.r, pixel.g, pixel.b) - def set_pixel(self, x,y, color): + def set_pixel(self, x, y, color): pixel = self.pixels[y][x] # tuple - pixel.r = min(color[0],255) - pixel.g = min(color[1],255) - pixel.b = min(color[2],255) + pixel.r = min(color[0], 255) + pixel.g = min(color[1], 255) + pixel.b = min(color[2], 255) + class _PyAccess32_4(PyAccess): """ RGBA etc, all 4 bytes of a 32 bit word """ def _post_init(self, *args, **kwargs): self.pixels = ffi.cast("struct Pixel_RGBA **", self.image32) - - def get_pixel(self, x,y): + + def get_pixel(self, x, y): pixel = self.pixels[y][x] return (pixel.r, pixel.g, pixel.b, pixel.a) - def set_pixel(self, x,y, color): + def set_pixel(self, x, y, color): pixel = self.pixels[y][x] # tuple - pixel.r = min(color[0],255) - pixel.g = min(color[1],255) - pixel.b = min(color[2],255) - pixel.a = min(color[3],255) + pixel.r = min(color[0], 255) + pixel.g = min(color[1], 255) + pixel.b = min(color[2], 255) + pixel.a = min(color[3], 255) + - class _PyAccess8(PyAccess): """ 1, L, P, 8 bit images stored as uint8 """ def _post_init(self, *args, **kwargs): self.pixels = self.image8 - - def get_pixel(self, x,y): + + def get_pixel(self, x, y): return self.pixels[y][x] - def set_pixel(self, x,y, color): + def set_pixel(self, x, y, color): try: # integer - self.pixels[y][x] = min(color,255) + self.pixels[y][x] = min(color, 255) except: # tuple - self.pixels[y][x] = min(color[0],255) + self.pixels[y][x] = min(color[0], 255) + class _PyAccessI16_N(PyAccess): """ I;16 access, native bitendian without conversion """ def _post_init(self, *args, **kwargs): self.pixels = ffi.cast('unsigned short **', self.image) - def get_pixel(self, x,y): + def get_pixel(self, x, y): return self.pixels[y][x] - def set_pixel(self, x,y, color): + def set_pixel(self, x, y, color): try: # integer self.pixels[y][x] = min(color, 65535) @@ -171,35 +180,37 @@ class _PyAccessI16_N(PyAccess): # tuple self.pixels[y][x] = min(color[0], 65535) + class _PyAccessI16_L(PyAccess): """ I;16L access, with conversion """ def _post_init(self, *args, **kwargs): self.pixels = ffi.cast('struct Pixel_I16 **', self.image) - def get_pixel(self, x,y): + def get_pixel(self, x, y): pixel = self.pixels[y][x] return pixel.l + pixel.r * 256 - def set_pixel(self, x,y, color): + def set_pixel(self, x, y, color): pixel = self.pixels[y][x] try: color = min(color, 65535) - except: + except TypeError: color = min(color[0], 65535) pixel.l = color & 0xFF pixel.r = color >> 8 + class _PyAccessI16_B(PyAccess): """ I;16B access, with conversion """ def _post_init(self, *args, **kwargs): self.pixels = ffi.cast('struct Pixel_I16 **', self.image) - def get_pixel(self, x,y): + def get_pixel(self, x, y): pixel = self.pixels[y][x] - return pixel.l *256 + pixel.r + return pixel.l * 256 + pixel.r - def set_pixel(self, x,y, color): + def set_pixel(self, x, y, color): pixel = self.pixels[y][x] try: color = min(color, 65535) @@ -209,17 +220,19 @@ class _PyAccessI16_B(PyAccess): pixel.l = color >> 8 pixel.r = color & 0xFF + class _PyAccessI32_N(PyAccess): """ Signed Int32 access, native endian """ def _post_init(self, *args, **kwargs): self.pixels = self.image32 - def get_pixel(self, x,y): + def get_pixel(self, x, y): return self.pixels[y][x] - def set_pixel(self, x,y, color): + def set_pixel(self, x, y, color): self.pixels[y][x] = color + class _PyAccessI32_Swap(PyAccess): """ I;32L/B access, with byteswapping conversion """ def _post_init(self, *args, **kwargs): @@ -228,24 +241,26 @@ class _PyAccessI32_Swap(PyAccess): def reverse(self, i): orig = ffi.new('int *', i) chars = ffi.cast('unsigned char *', orig) - chars[0],chars[1],chars[2],chars[3] = chars[3], chars[2],chars[1],chars[0] + chars[0], chars[1], chars[2], chars[3] = chars[3], chars[2], \ + chars[1], chars[0] return ffi.cast('int *', chars)[0] - - def get_pixel(self, x,y): + + def get_pixel(self, x, y): return self.reverse(self.pixels[y][x]) - def set_pixel(self, x,y, color): + def set_pixel(self, x, y, color): self.pixels[y][x] = self.reverse(color) + class _PyAccessF(PyAccess): """ 32 bit float access """ def _post_init(self, *args, **kwargs): self.pixels = ffi.cast('float **', self.image32) - def get_pixel(self, x,y): + def get_pixel(self, x, y): return self.pixels[y][x] - def set_pixel(self, x,y, color): + def set_pixel(self, x, y, color): try: # not a tuple self.pixels[y][x] = color @@ -275,7 +290,7 @@ if sys.byteorder == 'little': mode_map['I;16'] = _PyAccessI16_N mode_map['I;16L'] = _PyAccessI16_N mode_map['I;16B'] = _PyAccessI16_B - + mode_map['I;32L'] = _PyAccessI32_N mode_map['I;32B'] = _PyAccessI32_Swap else: @@ -285,14 +300,16 @@ else: mode_map['I;32L'] = _PyAccessI32_Swap mode_map['I;32B'] = _PyAccessI32_N - -def new(img, readonly=False): + +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) + if DEBUG: + print("PyAccess Not Implemented: %s" % img.mode) return None - if DEBUG: print ("New PyAccess: %s" % img.mode) + if DEBUG: + print("New PyAccess: %s" % img.mode) return access_type(img, readonly) - +# End of file diff --git a/PIL/SpiderImagePlugin.py b/PIL/SpiderImagePlugin.py index 306b348bc..7de5156b1 100644 --- a/PIL/SpiderImagePlugin.py +++ b/PIL/SpiderImagePlugin.py @@ -48,7 +48,7 @@ def isInt(f): return 1 else: return 0 - except: + except ValueError: return 0 iforms = [1, 3, -11, -12, -21, -22] @@ -127,12 +127,12 @@ class SpiderImageFile(ImageFile.ImageFile): if self.istack == 0 and self.imgnumber == 0: # stk=0, img=0: a regular 2D image offset = hdrlen - self.nimages = 1 + self._nimages = 1 elif self.istack > 0 and self.imgnumber == 0: # stk>0, img=0: Opening the stack for the first time self.imgbytes = int(h[12]) * int(h[2]) * 4 self.hdrlen = hdrlen - self.nimages = int(h[26]) + self._nimages = int(h[26]) # Point to the first image in the stack offset = hdrlen * 2 self.imgnumber = 1 @@ -154,6 +154,10 @@ class SpiderImageFile(ImageFile.ImageFile): (self.rawmode, 0, 1))] self.__fp = self.fp # FIXME: hack + @property + def n_frames(self): + return self._nimages + # 1st image index is zero (although SPIDER imgnumber starts at 1) def tell(self): if self.imgnumber < 1: @@ -164,7 +168,7 @@ class SpiderImageFile(ImageFile.ImageFile): def seek(self, frame): if self.istack == 0: return - if frame >= self.nimages: + if frame >= self._nimages: raise EOFError("attempt to seek past end of file") self.stkoffset = self.hdrlen + frame * (self.hdrlen + self.imgbytes) self.fp = self.__fp @@ -173,11 +177,11 @@ class SpiderImageFile(ImageFile.ImageFile): # returns a byte image after rescaling to 0..255 def convert2byte(self, depth=255): - (min, max) = self.getextrema() + (minimum, maximum) = self.getextrema() m = 1 - if max != min: - m = depth / (max-min) - b = -m * min + if maximum != minimum: + m = depth / (maximum-minimum) + b = -m * minimum return self.point(lambda i, m=m, b=b: i * m + b).convert("L") # returns a ImageTk.PhotoImage object, after rescaling to 0..255 @@ -271,7 +275,7 @@ def _save(im, fp, filename): def _save_spider(im, fp, filename): # get the filename extension and register it with Image - fn, ext = os.path.splitext(filename) + ext = os.path.splitext(filename)[1] Image.register_extension("SPIDER", ext) _save(im, fp, filename) diff --git a/PIL/TarIO.py b/PIL/TarIO.py index bba493e8f..4e5115b26 100644 --- a/PIL/TarIO.py +++ b/PIL/TarIO.py @@ -16,6 +16,7 @@ from PIL import ContainerIO + ## # A file object that provides read access to a given member of a TAR # file. diff --git a/PIL/TgaImagePlugin.py b/PIL/TgaImagePlugin.py index 55790db08..46eafe8d0 100644 --- a/PIL/TgaImagePlugin.py +++ b/PIL/TgaImagePlugin.py @@ -42,9 +42,6 @@ MODES = { } -def _accept(prefix): - return prefix[0:1] == b"\0" - ## # Image plugin for Targa files. @@ -58,7 +55,7 @@ class TgaImageFile(ImageFile.ImageFile): # process header s = self.fp.read(18) - id = i8(s[0]) + idlen = i8(s[0]) colormaptype = i8(s[1]) imagetype = i8(s[2]) @@ -70,7 +67,7 @@ class TgaImageFile(ImageFile.ImageFile): self.size = i16(s[12:]), i16(s[14:]) # validate header fields - if id != 0 or colormaptype not in (0, 1) or\ + if colormaptype not in (0, 1) or\ self.size[0] <= 0 or self.size[1] <= 0 or\ depth not in (1, 8, 16, 24, 32): raise SyntaxError("not a TGA file") @@ -79,7 +76,7 @@ class TgaImageFile(ImageFile.ImageFile): if imagetype in (3, 11): self.mode = "L" if depth == 1: - self.mode = "1" # ??? + self.mode = "1" # ??? elif imagetype in (1, 9): self.mode = "P" elif imagetype in (2, 10): @@ -103,22 +100,25 @@ class TgaImageFile(ImageFile.ImageFile): if imagetype & 8: self.info["compression"] = "tga_rle" + if idlen: + self.info["id_section"] = self.fp.read(idlen) + if colormaptype: # read palette start, size, mapdepth = i16(s[3:]), i16(s[5:]), i16(s[7:]) if mapdepth == 16: - self.palette = ImagePalette.raw("BGR;16", - b"\0"*2*start + self.fp.read(2*size)) + self.palette = ImagePalette.raw( + "BGR;16", b"\0"*2*start + self.fp.read(2*size)) elif mapdepth == 24: - self.palette = ImagePalette.raw("BGR", - b"\0"*3*start + self.fp.read(3*size)) + self.palette = ImagePalette.raw( + "BGR", b"\0"*3*start + self.fp.read(3*size)) elif mapdepth == 32: - self.palette = ImagePalette.raw("BGRA", - b"\0"*4*start + self.fp.read(4*size)) + self.palette = ImagePalette.raw( + "BGRA", b"\0"*4*start + self.fp.read(4*size)) # setup tile descriptor try: - rawmode = MODES[(imagetype&7, depth)] + rawmode = MODES[(imagetype & 7, depth)] if imagetype & 8: # compressed self.tile = [("tga_rle", (0, 0)+self.size, @@ -127,7 +127,7 @@ class TgaImageFile(ImageFile.ImageFile): self.tile = [("raw", (0, 0)+self.size, self.fp.tell(), (rawmode, 0, orientation))] except KeyError: - pass # cannot decode + pass # cannot decode # # -------------------------------------------------------------------- @@ -145,6 +145,7 @@ SAVE = { "RGBA": ("BGRA", 32, 0, 2), } + def _save(im, fp, filename, check=0): try: @@ -185,13 +186,14 @@ def _save(im, fp, filename, check=0): if colormaptype: fp.write(im.im.getpalette("RGB", "BGR")) - ImageFile._save(im, fp, [("raw", (0,0)+im.size, 0, (rawmode, 0, orientation))]) + ImageFile._save( + im, fp, [("raw", (0, 0) + im.size, 0, (rawmode, 0, orientation))]) # # -------------------------------------------------------------------- # Registry -Image.register_open("TGA", TgaImageFile, _accept) +Image.register_open("TGA", TgaImageFile) Image.register_save("TGA", _save) Image.register_extension("TGA", ".tga") diff --git a/PIL/TiffImagePlugin.py b/PIL/TiffImagePlugin.py index 9bef30ebe..59de84273 100644 --- a/PIL/TiffImagePlugin.py +++ b/PIL/TiffImagePlugin.py @@ -54,6 +54,7 @@ import sys import collections import itertools import os +import io # Set these to true to force use of libtiff for reading or writing. READ_LIBTIFF = False @@ -149,6 +150,7 @@ OPEN_INFO = { (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"), @@ -281,6 +283,7 @@ class ImageFileDirectory(collections.MutableMapping): 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()) @@ -291,7 +294,7 @@ class ImageFileDirectory(collections.MutableMapping): def named(self): """ - Returns the complete tag dictionary, with named tags where posible. + Returns the complete tag dictionary, with named tags where possible. """ from PIL import TiffTags result = {} @@ -415,6 +418,7 @@ class ImageFileDirectory(collections.MutableMapping): # load tag dictionary self.reset() + self.offset = fp.tell() i16 = self.i16 i32 = self.i32 @@ -422,6 +426,11 @@ class ImageFileDirectory(collections.MutableMapping): 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) @@ -446,7 +455,11 @@ class ImageFileDirectory(collections.MutableMapping): # 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: @@ -468,7 +481,14 @@ class ImageFileDirectory(collections.MutableMapping): else: print("- value:", self[tag]) - self.next = i32(fp.read(4)) + 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))) + return + + self.next = i32(ifd) # save primitives @@ -498,7 +518,7 @@ class ImageFileDirectory(collections.MutableMapping): typ = self.tagtype[tag] if Image.DEBUG: - print ("Tag %s, Type: %s, Value: %s" % (tag, typ, value)) + print("Tag %s, Type: %s, Value: %s" % (tag, typ, value)) if typ == 1: # byte data @@ -509,6 +529,15 @@ class ImageFileDirectory(collections.MutableMapping): 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): @@ -619,45 +648,63 @@ class TiffImageFile(ImageFile.ImageFile): self.__first = self.__next = self.ifd.i32(ifh, 4) self.__frame = -1 self.__fp = self.fp + self._frame_pos = [] + self._n_frames = None if Image.DEBUG: - print ("*** TiffImageFile._open ***") - print ("- __first:", self.__first) - print ("- ifh: ", ifh) + print("*** TiffImageFile._open ***") + print("- __first:", self.__first) + print("- ifh: ", ifh) # and load the first frame self._seek(0) + @property + def n_frames(self): + if self._n_frames is None: + current = self.tell() + try: + while True: + self._seek(self.tell() + 1) + except EOFError: + self._n_frames = self.tell() + 1 + self.seek(current) + return self._n_frames + def seek(self, frame): "Select a given frame as current image" - - if frame < 0: - frame = 0 - self._seek(frame) - - def tell(self): - "Return the current frame number" - - return self._tell() + self._seek(max(frame, 0)) # Questionable backwards compatibility. + # Create a new core image object on second and + # subsequent frames in the image. Image may be + # different size/mode. + Image._decompression_bomb_check(self.size) + self.im = Image.core.new(self.mode, self.size) def _seek(self, frame): - self.fp = self.__fp - if frame < self.__frame: - # rewind file - self.__frame = -1 - self.__next = self.__first - while self.__frame < frame: + 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" % + (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: + print("Loading tags, location: %s" % self.fp.tell()) self.tag.load(self.fp) self.__next = self.tag.next self.__frame += 1 + self.fp.seek(self._frame_pos[frame]) + self.tag.load(self.fp) + self.__frame = frame self._setup() - def _tell(self): - + def tell(self): + "Return the current frame number" return self.__frame def _decoder(self, rawmode, layer, tile=None): @@ -705,7 +752,8 @@ class TiffImageFile(ImageFile.ImageFile): # (self._compression, (extents tuple), # 0, (rawmode, self._compression, fp)) - ignored, extents, ignored_2, args = self.tile[0] + extents = self.tile[0][1] + args = self.tile[0][3] + (self.ifd.offset,) decoder = Image._getdecoder(self.mode, 'libtiff', args, self.decoderconfig) try: @@ -722,21 +770,21 @@ 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 - # dea. with here by reordering. + # deal with here by reordering. if Image.DEBUG: - print ("have getvalue. just sending in a string from getvalue") + 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: - print ("have fileno, calling fileno version of the decoder.") + 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: - print ("don't have fileno or getvalue. just reading") + print("don't have fileno or getvalue. just reading") # UNDONE -- so much for that buffer size thing. n, err = decoder.decode(self.fp.read()) @@ -744,7 +792,8 @@ class TiffImageFile(ImageFile.ImageFile): self.readonly = 0 # libtiff closed the fp in a, we need to close self.fp, if possible if hasattr(self.fp, 'close'): - self.fp.close() + if not self.__next: + self.fp.close() self.fp = None # might be shared if err < 0: @@ -866,6 +915,10 @@ class TiffImageFile(ImageFile.ImageFile): try: fp = hasattr(self.fp, "fileno") and \ os.dup(self.fp.fileno()) + # 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() except IOError: # io.BytesIO have a fileno, but returns an IOError if # it doesn't use a file descriptor. @@ -911,7 +964,7 @@ class TiffImageFile(ImageFile.ImageFile): (0, min(y, ysize), w, min(y+h, ysize)), offsets[i], a)) if Image.DEBUG: - print ("tiles: ", self.tile) + print("tiles: ", self.tile) y = y + h if y >= self.size[1]: x = y = 0 @@ -946,14 +999,14 @@ class TiffImageFile(ImageFile.ImageFile): # fixup palette descriptor if self.mode == "P": - palette = [o8(a // 256) for a in self.tag[COLORMAP]] + palette = [o8(b // 256) for b in self.tag[COLORMAP]] self.palette = ImagePalette.raw("RGB;L", b"".join(palette)) # # -------------------------------------------------------------------- # Write TIFF files # little endian is default except for image modes with -# explict big endian byte-order +# explicit big endian byte-order SAVE_INFO = { # mode => rawmode, byteorder, photometrics, @@ -1045,31 +1098,25 @@ def _save(im, fp, filename): if "icc_profile" in im.info: ifd[ICCPROFILE] = im.info["icc_profile"] - if "description" in im.encoderinfo: - ifd[IMAGEDESCRIPTION] = im.encoderinfo["description"] - if "resolution" in im.encoderinfo: - ifd[X_RESOLUTION] = ifd[Y_RESOLUTION] \ - = _cvt_res(im.encoderinfo["resolution"]) - if "x resolution" in im.encoderinfo: - ifd[X_RESOLUTION] = _cvt_res(im.encoderinfo["x resolution"]) - if "y resolution" in im.encoderinfo: - ifd[Y_RESOLUTION] = _cvt_res(im.encoderinfo["y resolution"]) - if "resolution unit" in im.encoderinfo: - unit = im.encoderinfo["resolution unit"] - if unit == "inch": - ifd[RESOLUTION_UNIT] = 2 - elif unit == "cm" or unit == "centimeter": - ifd[RESOLUTION_UNIT] = 3 - else: - ifd[RESOLUTION_UNIT] = 1 - if "software" in im.encoderinfo: - ifd[SOFTWARE] = im.encoderinfo["software"] - if "date time" in im.encoderinfo: - ifd[DATE_TIME] = im.encoderinfo["date time"] - if "artist" in im.encoderinfo: - ifd[ARTIST] = im.encoderinfo["artist"] - if "copyright" in im.encoderinfo: - ifd[COPYRIGHT] = im.encoderinfo["copyright"] + 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)]: + 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("_", " ")]) + if name in im.encoderinfo: + ifd[key] = cvt(im.encoderinfo[name]) dpi = im.encoderinfo.get("dpi") if dpi: @@ -1102,12 +1149,15 @@ def _save(im, fp, filename): if libtiff: if Image.DEBUG: - print ("Saving using libtiff encoder") - print (ifd.items()) + print("Saving using libtiff encoder") + print(ifd.items()) _fp = 0 if hasattr(fp, "fileno"): - fp.seek(0) - _fp = os.dup(fp.fileno()) + try: + fp.seek(0) + _fp = os.dup(fp.fileno()) + except io.UnsupportedOperation: + pass # ICC Profile crashes. blocklist = [STRIPOFFSETS, STRIPBYTECOUNTS, ROWSPERSTRIP, ICCPROFILE] @@ -1132,8 +1182,11 @@ def _save(im, fp, filename): # 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: + 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 @@ -1154,7 +1207,7 @@ def _save(im, fp, filename): atts[k] = v if Image.DEBUG: - print (atts) + print(atts) # libtiff always expects the bytes in native order. # we're storing image byte order. So, if the rawmode diff --git a/PIL/TiffTags.py b/PIL/TiffTags.py index 966779ce9..d15aa7ebe 100644 --- a/PIL/TiffTags.py +++ b/PIL/TiffTags.py @@ -46,8 +46,8 @@ TAGS = { (262, 5): "CMYK", (262, 6): "YCbCr", (262, 8): "CieLAB", - (262, 32803): "CFA", # TIFF/EP, Adobe DNG - (262, 32892): "LinearRaw", # Adobe DNG + (262, 32803): "CFA", # TIFF/EP, Adobe DNG + (262, 32892): "LinearRaw", # Adobe DNG 263: "Thresholding", 264: "CellWidth", @@ -240,7 +240,7 @@ TAGS = { 45579: "YawAngle", 45580: "PitchAngle", 45581: "RollAngle", - + # Adobe DNG 50706: "DNGVersion", 50707: "DNGBackwardVersion", @@ -255,7 +255,6 @@ TAGS = { 50716: "BlackLevelDeltaV", 50717: "WhiteLevel", 50718: "DefaultScale", - 50741: "BestQualityScale", # FIXME! Dictionary contains duplicate keys 50741 50719: "DefaultCropOrigin", 50720: "DefaultCropSize", 50778: "CalibrationIlluminant1", @@ -279,11 +278,12 @@ TAGS = { 50737: "ChromaBlurRadius", 50738: "AntiAliasStrength", 50740: "DNGPrivateData", - 50741: "MakerNoteSafety", # FIXME! Dictionary contains duplicate keys 50741 + 50741: "MakerNoteSafety", + 50780: "BestQualityScale", - #ImageJ - 50838: "ImageJMetaDataByteCounts", # private tag registered with Adobe - 50839: "ImageJMetaData", # private tag registered with Adobe + # ImageJ + 50838: "ImageJMetaDataByteCounts", # private tag registered with Adobe + 50839: "ImageJMetaData", # private tag registered with Adobe } ## diff --git a/PIL/WalImageFile.py b/PIL/WalImageFile.py index d494bfd58..fc2bb30a7 100644 --- a/PIL/WalImageFile.py +++ b/PIL/WalImageFile.py @@ -1,5 +1,3 @@ -# -*- coding: iso-8859-1 -*- -# # The Python Imaging Library. # $Id$ # @@ -33,6 +31,7 @@ except ImportError: i32 = _binary.i32le + ## # Load texture from a Quake2 WAL texture file. #

@@ -75,7 +74,7 @@ def open(filename): quake2palette = ( - # default palette taken from piffo 0.93 by Hans Häggström + # default palette taken from piffo 0.93 by Hans Häggström b"\x01\x01\x01\x0b\x0b\x0b\x12\x12\x12\x17\x17\x17\x1b\x1b\x1b\x1e" b"\x1e\x1e\x22\x22\x22\x26\x26\x26\x29\x29\x29\x2c\x2c\x2c\x2f\x2f" b"\x2f\x32\x32\x32\x35\x35\x35\x37\x37\x37\x3a\x3a\x3a\x3c\x3c\x3c" diff --git a/PIL/WebPImagePlugin.py b/PIL/WebPImagePlugin.py index ab60c8dfa..78a7a5319 100644 --- a/PIL/WebPImagePlugin.py +++ b/PIL/WebPImagePlugin.py @@ -12,7 +12,7 @@ _VALID_WEBP_MODES = { _VP8_MODES_BY_IDENTIFIER = { b"VP8 ": "RGB", b"VP8X": "RGBA", - b"VP8L": "RGBA", # lossless + b"VP8L": "RGBA", # lossless } @@ -30,7 +30,8 @@ class WebPImageFile(ImageFile.ImageFile): format_description = "WebP image" def _open(self): - data, width, height, self.mode, icc_profile, exif = _webp.WebPDecode(self.fp.read()) + data, width, height, self.mode, icc_profile, exif = \ + _webp.WebPDecode(self.fp.read()) if icc_profile: self.info["icc_profile"] = icc_profile diff --git a/PIL/WmfImagePlugin.py b/PIL/WmfImagePlugin.py index 40b2037ab..bdbbc72f0 100644 --- a/PIL/WmfImagePlugin.py +++ b/PIL/WmfImagePlugin.py @@ -24,6 +24,7 @@ _handler = None if str != bytes: long = int + ## # Install application-specific WMF image handler. # @@ -36,14 +37,14 @@ def register_handler(handler): if hasattr(Image.core, "drawwmf"): # install default handler (windows only) - class WmfHandler: + class WmfHandler(object): def open(self, im): im.mode = "RGB" self.bbox = im.info["wmf_bbox"] def load(self, im): - im.fp.seek(0) # rewind + im.fp.seek(0) # rewind return Image.frombytes( "RGB", im.size, Image.core.drawwmf(im.fp.read(), im.size, self.bbox), @@ -56,6 +57,7 @@ if hasattr(Image.core, "drawwmf"): word = _binary.i16le + def short(c, o=0): v = word(c, o) if v >= 32768: @@ -64,6 +66,7 @@ def short(c, o=0): dword = _binary.i32le + # # -------------------------------------------------------------------- # Read WMF file @@ -74,6 +77,7 @@ def _accept(prefix): prefix[:4] == b"\x01\x00\x00\x00" ) + ## # Image plugin for Windows metafiles. @@ -95,8 +99,10 @@ class WmfStubImageFile(ImageFile.StubImageFile): inch = word(s, 14) # get bounding box - x0 = short(s, 6); y0 = short(s, 8) - x1 = short(s, 10); y1 = short(s, 12) + x0 = short(s, 6) + y0 = short(s, 8) + x1 = short(s, 10) + y1 = short(s, 12) # normalize size to 72 dots per inch size = (x1 - x0) * 72 // inch, (y1 - y0) * 72 // inch @@ -115,8 +121,10 @@ class WmfStubImageFile(ImageFile.StubImageFile): # enhanced metafile # get bounding box - x0 = dword(s, 8); y0 = dword(s, 12) - x1 = dword(s, 16); y1 = dword(s, 20) + x0 = dword(s, 8) + y0 = dword(s, 12) + x1 = dword(s, 16) + y1 = dword(s, 20) # get frame (in 0.01 millimeter units) frame = dword(s, 24), dword(s, 28), dword(s, 32), dword(s, 36) diff --git a/PIL/XVThumbImagePlugin.py b/PIL/XVThumbImagePlugin.py index e5bf55acf..5cf1386fd 100644 --- a/PIL/XVThumbImagePlugin.py +++ b/PIL/XVThumbImagePlugin.py @@ -30,6 +30,7 @@ for r in range(8): for b in range(4): PALETTE = PALETTE + (o8((r*255)//7)+o8((g*255)//7)+o8((b*255)//3)) + ## # Image plugin for XV thumbnail images. diff --git a/PIL/XbmImagePlugin.py b/PIL/XbmImagePlugin.py index 799d727a0..604ba15a8 100644 --- a/PIL/XbmImagePlugin.py +++ b/PIL/XbmImagePlugin.py @@ -35,9 +35,11 @@ xbm_head = re.compile( b"[\\000-\\377]*_bits\\[\\]" ) + def _accept(prefix): return prefix.lstrip()[:7] == b"#define" + ## # Image plugin for X11 bitmaps. @@ -81,7 +83,7 @@ def _save(im, fp, filename): fp.write(b"static char im_bits[] = {\n") - ImageFile._save(im, fp, [("xbm", (0,0)+im.size, 0, None)]) + ImageFile._save(im, fp, [("xbm", (0, 0)+im.size, 0, None)]) fp.write(b"};\n") diff --git a/PIL/__init__.py b/PIL/__init__.py index 7b4b8abfa..6d51a5dcb 100644 --- a/PIL/__init__.py +++ b/PIL/__init__.py @@ -12,7 +12,7 @@ # ;-) VERSION = '1.1.7' # PIL version -PILLOW_VERSION = '2.5.3' # Pillow +PILLOW_VERSION = '2.9.0.dev0' # Pillow _plugins = ['BmpImagePlugin', 'BufrStubImagePlugin', diff --git a/PIL/_binary.py b/PIL/_binary.py index 71b2b78c9..2f5e8ffd4 100644 --- a/PIL/_binary.py +++ b/PIL/_binary.py @@ -11,21 +11,24 @@ # See the README file for information on usage and redistribution. # +from struct import unpack, pack + if bytes is str: def i8(c): return ord(c) def o8(i): - return chr(i&255) + return chr(i & 255) else: def i8(c): return c if c.__class__ is int else c[0] def o8(i): - return bytes((i&255,)) + return bytes((i & 255,)) + # Input, le = little endian, be = big endian -#TODO: replace with more readable struct.unpack equivalent +# TODO: replace with more readable struct.unpack equivalent def i16le(c, o=0): """ Converts a 2-bytes (16 bits) string to an integer. @@ -33,7 +36,8 @@ def i16le(c, o=0): c: string containing bytes to convert o: offset of bytes to convert in string """ - return i8(c[o]) | (i8(c[o+1])<<8) + return unpack("H", c[o:o+2])[0] + def i32be(c, o=0): - return (i8(c[o])<<24) | (i8(c[o+1])<<16) | (i8(c[o+2])<<8) | i8(c[o+3]) + return unpack(">I", c[o:o+4])[0] + # Output, le = little endian, be = big endian def o16le(i): - return o8(i) + o8(i>>8) + return pack(">8) + o8(i>>16) + o8(i>>24) + return pack(">8) + o8(i) + return pack(">H", i) + def o32be(i): - return o8(i>>24) + o8(i>>16) + o8(i>>8) + o8(i) + return pack(">I", i) +# End of file diff --git a/PIL/features.py b/PIL/features.py new file mode 100644 index 000000000..fd87f094f --- /dev/null +++ b/PIL/features.py @@ -0,0 +1,67 @@ +from PIL import Image + +modules = { + "pil": "PIL._imaging", + "tkinter": "PIL._imagingtk", + "freetype2": "PIL._imagingft", + "littlecms2": "PIL._imagingcms", + "webp": "PIL._webp", + "transp_webp": ("WEBP", "WebPDecoderBuggyAlpha") +} + + +def check_module(feature): + if feature not in modules: + raise ValueError("Unknown module %s" % feature) + + module = modules[feature] + + method_to_call = None + if type(module) is tuple: + module, method_to_call = module + + try: + imported_module = __import__(module) + except ImportError: + # If a method is being checked, None means that + # rather than the method failing, the module required for the method + # failed to be imported first + return None if method_to_call else False + + if method_to_call: + method = getattr(imported_module, method_to_call) + return method() is True + else: + return True + + +def get_supported_modules(): + supported_modules = [] + for feature in modules: + if check_module(feature): + supported_modules.append(feature) + return supported_modules + +codecs = { + "jpg": "jpeg", + "jpg_2000": "jpeg2k", + "zlib": "zip", + "libtiff": "libtiff" +} + + +def check_codec(feature): + if feature not in codecs: + raise ValueError("Unknown codec %s" % feature) + + codec = codecs[feature] + + return codec + "_encoder" in dir(Image.core) + + +def get_supported_codecs(): + supported_codecs = [] + for feature in codecs: + if check_codec(feature): + supported_codecs.append(feature) + return supported_codecs diff --git a/README.rst b/README.rst index 482a1e2ec..be50c5531 100644 --- a/README.rst +++ b/README.rst @@ -1,22 +1,48 @@ Pillow ====== -*Python Imaging Library (Fork)* +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. For more information, please `read the documentation `_, `check the changelog `_ and `find out how to contribute `_. +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://travis-ci.org/python-pillow/Pillow.svg?branch=master :target: https://travis-ci.org/python-pillow/Pillow - :alt: Travis CI build status + :alt: Travis CI build status (Linux) -.. image:: https://pypip.in/v/Pillow/badge.png +.. + image:: https://pypip.in/v/Pillow/badge.png :target: https://pypi.python.org/pypi/Pillow/ :alt: Latest PyPI version -.. image:: https://pypip.in/d/Pillow/badge.png +.. + image:: https://pypip.in/d/Pillow/badge.png :target: https://pypi.python.org/pypi/Pillow/ :alt: Number of PyPI downloads .. image:: https://coveralls.io/repos/python-pillow/Pillow/badge.png?branch=master :target: https://coveralls.io/r/python-pillow/Pillow?branch=master + :alt: Code coverage +.. image:: https://landscape.io/github/python-pillow/Pillow/master/landscape.png + :target: https://landscape.io/github/python-pillow/Pillow/master + :alt: Code health + +More Information +---------------- + +- `Changelog `_ + + - `Pre-fork `_ + +- `Contribute `_ + + - `Issues `_ + +- `Documentation `_ + + - `About `_ + - `Guides `_ + - `Installation `_ + - `Reference `_ diff --git a/RELEASING.md b/RELEASING.md new file mode 100644 index 000000000..d9c4d761e --- /dev/null +++ b/RELEASING.md @@ -0,0 +1,105 @@ +# Release Checklist + +## Main Release + +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: +``` + PIL/__init__.py setup.py _imaging.c +``` +* [ ] Update `CHANGES.rst`. +* [ ] Run pre-release check via `make pre`. +* [ ] Create branch and tag for release e.g.: +``` + $ git branch 2.9.x + $ git tag 2.9.0 + $ git push --all + $ git push --tags +``` +* [ ] Create and upload source distributions e.g.: +``` + $ make sdistup +``` +* [ ] 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 + +## Point Release + +Released as needed for security, installation or critical bug fixes. + +* [ ] Make necessary changes in ``master`` branch. +* [ ] 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: +``` + PIL/__init__.py setup.py _imaging.c +``` +* [ ] Run pre-release check via `make pre`. +* [ ] Create tag for release e.g.: +``` + $ git tag 2.9.1 + $ git push --tags +``` +* [ ] Create and upload source distributions e.g.: +``` + $ make sdistup +``` +* [ ] Create and upload [binary distributions](#binary-distributions) + +## Embargoed Release + +Released as needed privately to individual vendors for critical security-related bug fixes. + +* [ ] Prepare patch for all versions that will get a fix. Test against local installations. +* [ ] 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` +* [ ] Amend any commits with the CVE # +* [ ] On release date, tag and push to GitHub. +``` + git checkout 2.5.x + git tag 2.5.3 + git push origin 2.5.x + git push origin --tags +``` +* [ ] Create and upload source distributions e.g.: +``` + $ make sdistup +``` +* [ ] Create and upload [binary distributions](#binary-distributions) + +## Binary Distributions + +### Windows +* [ ] Contact @cgohlke for Windows binaries via release ticket e.g. https://github.com/python-pillow/Pillow/issues/1174. +* [ ] Download and extract tarball from @cgohlke and ``twine upload *``. + +### OS X +* [ ] Use the [Pillow OS X Wheel Builder](https://github.com/python-pillow/pillow-wheels): +``` + $ git checkout https://github.com/python-pillow/pillow-wheels + $ cd pillow-wheels + $ git submodule init + $ git submodule update + $ cd Pillow + $ git fetch --all + $ git commit -a -m "Pillow -> 2.9.0" + $ git push +``` +* [ ] Download distributions from the [Pillow OS X Wheel Builder container](http://cdf58691c5cf45771290-6a3b6a0f5f6ab91aadc447b2a897dd9a.r50.cf2.rackcdn.com/) and ``twine upload *``. + +### Linux + +## Publicize Release + +* [ ] Announce release availability via [Twitter](https://twitter.com/pythonpillow) e.g. https://twitter.com/aclark4life/status/583366798302691328. diff --git a/Sane/CHANGES b/Sane/CHANGES deleted file mode 100644 index 47fb96cf1..000000000 --- a/Sane/CHANGES +++ /dev/null @@ -1,34 +0,0 @@ - -from V1.0 to V2.0 - -_sane.c: - - Values for option constraints are correctly translated to floats - if value type is TYPE_FIXED for SANE_CONSTRAINT_RANGE and - SANE_CONSTRAINT_WORD_LIST - - added constants INFO_INEXACT, INFO_RELOAD_OPTIONS, - INFO_RELOAD_PARAMS (possible return values of set_option()) - to module dictionnary. - - removed additional return variable 'i' from SaneDev_get_option(), - because it is only set when SANE_ACTION_SET_VALUE is used. - - scanDev.get_parameters() now returns the scanner mode as 'format', - no more the typical PIL codes. So 'L' became 'gray', 'RGB' is now - 'color', 'R' is 'red', 'G' is 'green', 'B' is 'red'. This matches - the way scanDev.mode is set. - This should be the only incompatibility vs. version 1.0. - -sane.py - - ScanDev got new method __load_option_dict() called from __init__() - and from __setattr__() if backend reported that the frontend should - reload the options. - - Nice human-readable __repr__() method added for class Option - - if __setattr__ (i.e. set_option) reports that all other options - have to be reloaded due to a change in the backend then they are reloaded. - - due to the change in SaneDev_get_option() only the 'value' is - returned from get_option(). - - in __setattr__ integer values are automatically converted to floats - if SANE backend expects SANE_FIXED (i.e. fix-point float) - - The scanner options can now directly be accessed via scanDev[optionName] - instead scanDev.opt[optionName]. (The old way still works). - -V1.0: - A.M. Kuchling's original pysane package. \ No newline at end of file diff --git a/Sane/README.rst b/Sane/README.rst deleted file mode 100644 index 173934040..000000000 --- a/Sane/README.rst +++ /dev/null @@ -1,22 +0,0 @@ -Python SANE module V1.1 (30 Sep. 2004) -================================================================================ - -The SANE module provides an interface to the SANE scanner and frame -grabber interface for Linux. This module was contributed by Andrew -Kuchling and is extended and currently maintained by Ralph Heinkel -(rheinkel-at-email.de). If you write to me please make sure to have the -word 'SANE' or 'sane' in the subject of your mail, otherwise it might -be classified as spam in the future. - - -To build this module, type (in the Sane directory):: - - python setup.py build - -In order to install the module type:: - - python setup.py install - - -For some basic documentation please look at the file sanedoc.txt -The two demo_*.py scripts give basic examples on how to use the software. diff --git a/Sane/_sane.c b/Sane/_sane.c deleted file mode 100644 index 2ebcb1834..000000000 --- a/Sane/_sane.c +++ /dev/null @@ -1,1405 +0,0 @@ -/*********************************************************** -(C) Copyright 2003 A.M. Kuchling. All Rights Reserved -(C) Copyright 2004 A.M. Kuchling, Ralph Heinkel All Rights Reserved - -Permission to use, copy, modify, and distribute this software and its -documentation for any purpose and without fee is hereby granted, -provided that the above copyright notice appear in all copies and that -both that copyright notice and this permission notice appear in -supporting documentation, and that the name of A.M. Kuchling and -Ralph Heinkel not be used in advertising or publicity pertaining to -distribution of the software without specific, written prior permission. - -A.M. KUCHLING, R.H. HEINKEL DISCLAIM ALL WARRANTIES WITH REGARD TO THIS -SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, -IN NO EVENT SHALL THE AUTHORS 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. - -******************************************************************/ - -/* SaneDev objects */ - -#include "Python.h" -#include "Imaging.h" -#include - -#include - -#if PY_MAJOR_VERSION >= 3 - #define PyInt_AsLong PyLong_AsLong - #define PyInt_FromLong PyLong_FromLong - #define PyInt_Check PyLong_Check -#endif - -static PyObject *ErrorObject; - -typedef struct { - PyObject_HEAD - SANE_Handle h; -} SaneDevObject; - -#ifdef WITH_THREAD -PyThreadState *_save; -#endif - -/* Raise a SANE exception */ -PyObject * -PySane_Error(SANE_Status st) -{ - const char *string; - - if (st==SANE_STATUS_GOOD) {Py_INCREF(Py_None); return (Py_None);} - string=sane_strstatus(st); - PyErr_SetString(ErrorObject, string); - return NULL; -} - -static PyTypeObject SaneDev_Type; - -#define SaneDevObject_Check(v) (Py_TYPE(v) == &SaneDev_Type) - -static SaneDevObject * -newSaneDevObject(void) -{ - SaneDevObject *self; - - if (PyType_Ready(&SaneDev_Type) < 0) - return NULL; - - self = PyObject_NEW(SaneDevObject, &SaneDev_Type); - if (self == NULL) - return NULL; - self->h=NULL; - return self; -} - -/* SaneDev methods */ - -static void -SaneDev_dealloc(SaneDevObject *self) -{ - if (self->h) sane_close(self->h); - self->h=NULL; - PyObject_DEL(self); -} - -static PyObject * -SaneDev_close(SaneDevObject *self, PyObject *args) -{ - if (!PyArg_ParseTuple(args, "")) - return NULL; - if (self->h) sane_close(self->h); - self->h=NULL; - Py_INCREF(Py_None); - return (Py_None); -} - -static PyObject * -SaneDev_get_parameters(SaneDevObject *self, PyObject *args) -{ - SANE_Status st; - SANE_Parameters p; - char *format="unknown format"; - - if (!PyArg_ParseTuple(args, "")) - return NULL; - if (self->h==NULL) - { - PyErr_SetString(ErrorObject, "SaneDev object is closed"); - return NULL; - } - Py_BEGIN_ALLOW_THREADS - st=sane_get_parameters(self->h, &p); - Py_END_ALLOW_THREADS - - if (st) return PySane_Error(st); - switch (p.format) - { - case(SANE_FRAME_GRAY): format="gray"; break; - case(SANE_FRAME_RGB): format="color"; break; - case(SANE_FRAME_RED): format="red"; break; - case(SANE_FRAME_GREEN): format="green"; break; - case(SANE_FRAME_BLUE): format="blue"; break; - } - - return Py_BuildValue("si(ii)ii", format, p.last_frame, p.pixels_per_line, - p.lines, p.depth, p.bytes_per_line); -} - - -static PyObject * -SaneDev_fileno(SaneDevObject *self, PyObject *args) -{ - SANE_Status st; - SANE_Int fd; - - if (!PyArg_ParseTuple(args, "")) - return NULL; - if (self->h==NULL) - { - PyErr_SetString(ErrorObject, "SaneDev object is closed"); - return NULL; - } - st=sane_get_select_fd(self->h, &fd); - if (st) return PySane_Error(st); - return PyInt_FromLong(fd); -} - -static PyObject * -SaneDev_start(SaneDevObject *self, PyObject *args) -{ - SANE_Status st; - - if (!PyArg_ParseTuple(args, "")) - return NULL; - if (self->h==NULL) - { - PyErr_SetString(ErrorObject, "SaneDev object is closed"); - return NULL; - } - /* sane_start can take several seconds, if the user initiates - a new scan, while the scan head of a flatbed scanner moves - back to the start position after finishing a previous scan. - Hence it is worth to allow threads here. - */ - Py_BEGIN_ALLOW_THREADS - st=sane_start(self->h); - Py_END_ALLOW_THREADS - if (st) return PySane_Error(st); - Py_INCREF(Py_None); - return Py_None; -} - -static PyObject * -SaneDev_cancel(SaneDevObject *self, PyObject *args) -{ - if (!PyArg_ParseTuple(args, "")) - return NULL; - if (self->h==NULL) - { - PyErr_SetString(ErrorObject, "SaneDev object is closed"); - return NULL; - } - sane_cancel(self->h); - Py_INCREF(Py_None); - return Py_None; -} - -static PyObject * -SaneDev_get_options(SaneDevObject *self, PyObject *args) -{ - const SANE_Option_Descriptor *d; - PyObject *list, *value; - int i=1; - - if (!PyArg_ParseTuple(args, "")) - return NULL; - if (self->h==NULL) - { - PyErr_SetString(ErrorObject, "SaneDev object is closed"); - return NULL; - } - if (!(list = PyList_New(0))) - return NULL; - - do - { - d=sane_get_option_descriptor(self->h, i); - if (d!=NULL) - { - PyObject *constraint=NULL; - int j; - - switch (d->constraint_type) - { - case(SANE_CONSTRAINT_NONE): - Py_INCREF(Py_None); constraint=Py_None; break; - case(SANE_CONSTRAINT_RANGE): - if (d->type == SANE_TYPE_INT) - constraint=Py_BuildValue("iii", d->constraint.range->min, - d->constraint.range->max, - d->constraint.range->quant); - else - constraint=Py_BuildValue("ddd", - SANE_UNFIX(d->constraint.range->min), - SANE_UNFIX(d->constraint.range->max), - SANE_UNFIX(d->constraint.range->quant)); - break; - case(SANE_CONSTRAINT_WORD_LIST): - constraint=PyList_New(d->constraint.word_list[0]); - if (d->type == SANE_TYPE_INT) - for (j=1; j<=d->constraint.word_list[0]; j++) - PyList_SetItem(constraint, j-1, - PyInt_FromLong(d->constraint.word_list[j])); - else - for (j=1; j<=d->constraint.word_list[0]; j++) - PyList_SetItem(constraint, j-1, - PyFloat_FromDouble(SANE_UNFIX(d->constraint.word_list[j]))); - break; - case(SANE_CONSTRAINT_STRING_LIST): - constraint=PyList_New(0); - for(j=0; d->constraint.string_list[j]!=NULL; j++) - PyList_Append(constraint, -#if PY_MAJOR_VERSION >= 3 - PyUnicode_DecodeLatin1(d->constraint.string_list[j], strlen(d->constraint.string_list[j]), NULL)); -#else - PyString_FromString(d->constraint.string_list[j])); -#endif - break; - } - value=Py_BuildValue("isssiiiiO", i, d->name, d->title, d->desc, - d->type, d->unit, d->size, d->cap, constraint); - PyList_Append(list, value); - } - i++; - } while (d!=NULL); - return list; -} - -static PyObject * -SaneDev_get_option(SaneDevObject *self, PyObject *args) -{ - SANE_Status st; - const SANE_Option_Descriptor *d; - PyObject *value=NULL; - int n; - void *v; - - if (!PyArg_ParseTuple(args, "i", &n)) - { - return NULL; - } - if (self->h==NULL) - { - PyErr_SetString(ErrorObject, "SaneDev object is closed"); - return NULL; - } - d=sane_get_option_descriptor(self->h, n); - v=malloc(d->size+1); - st=sane_control_option(self->h, n, SANE_ACTION_GET_VALUE, - v, NULL); - - if (st) - { - free(v); - return PySane_Error(st); - } - - switch(d->type) - { - case(SANE_TYPE_BOOL): - case(SANE_TYPE_INT): - value=Py_BuildValue("i", *( (SANE_Int*)v) ); - break; - case(SANE_TYPE_FIXED): - value=Py_BuildValue("d", SANE_UNFIX((*((SANE_Fixed*)v))) ); - break; - case(SANE_TYPE_STRING): -#if PY_MAJOR_VERSION >= 3 - value=PyUnicode_DecodeLatin1((const char *) v, strlen((const char *) v), NULL); -#else - value=Py_BuildValue("s", v); -#endif - break; - case(SANE_TYPE_BUTTON): - case(SANE_TYPE_GROUP): - value=Py_BuildValue("O", Py_None); - break; - } - - free(v); - return value; -} - -static PyObject * -SaneDev_set_option(SaneDevObject *self, PyObject *args) -{ - SANE_Status st; - const SANE_Option_Descriptor *d; - SANE_Int i; - PyObject *value; - int n; - void *v; - - if (!PyArg_ParseTuple(args, "iO", &n, &value)) - return NULL; - if (self->h==NULL) - { - PyErr_SetString(ErrorObject, "SaneDev object is closed"); - return NULL; - } - d=sane_get_option_descriptor(self->h, n); - v=malloc(d->size+1); - - switch(d->type) - { - case(SANE_TYPE_BOOL): - if (!PyInt_Check(value)) - { - PyErr_SetString(PyExc_TypeError, "SANE_BOOL requires an integer"); - free(v); - return NULL; - } - /* fall through */ - case(SANE_TYPE_INT): - if (!PyInt_Check(value)) - { - PyErr_SetString(PyExc_TypeError, "SANE_INT requires an integer"); - free(v); - return NULL; - } - *( (SANE_Int*)v) = PyInt_AsLong(value); - break; - case(SANE_TYPE_FIXED): - if (!PyFloat_Check(value)) - { - PyErr_SetString(PyExc_TypeError, "SANE_FIXED requires a floating point number"); - free(v); - return NULL; - } - *( (SANE_Fixed*)v) = SANE_FIX(PyFloat_AsDouble(value)); - break; - case(SANE_TYPE_STRING): -#if PY_MAJOR_VERSION >= 3 - if (!PyUnicode_Check(value)) - { - PyErr_SetString(PyExc_TypeError, "SANE_STRING requires a string"); - free(v); - return NULL; - } - { - PyObject *encoded = PyUnicode_AsLatin1String(value); - - if (!encoded) - return NULL; - - strncpy(v, PyBytes_AsString(encoded), d->size-1); - ((char*)v)[d->size-1] = 0; - Py_DECREF(encoded); - } -#else - if (!PyString_Check(value)) - { - PyErr_SetString(PyExc_TypeError, "SANE_STRING requires a string"); - free(v); - return NULL; - } - strncpy(v, PyString_AsString(value), d->size-1); - ((char*)v)[d->size-1] = 0; -#endif - break; - case(SANE_TYPE_BUTTON): - case(SANE_TYPE_GROUP): - break; - } - - st=sane_control_option(self->h, n, SANE_ACTION_SET_VALUE, - v, &i); - if (st) {free(v); return PySane_Error(st);} - - free(v); - return Py_BuildValue("i", i); -} - -static PyObject * -SaneDev_set_auto_option(SaneDevObject *self, PyObject *args) -{ - SANE_Status st; - SANE_Int i; - int n; - - if (!PyArg_ParseTuple(args, "i", &n)) - return NULL; - if (self->h==NULL) - { - PyErr_SetString(ErrorObject, "SaneDev object is closed"); - return NULL; - } - st=sane_control_option(self->h, n, SANE_ACTION_SET_AUTO, - NULL, &i); - if (st) {return PySane_Error(st);} - - return Py_BuildValue("i", i); - } - -#define READSIZE 32768 - -static PyObject * -SaneDev_snap(SaneDevObject *self, PyObject *args) -{ - SANE_Status st; - /* The buffer should be a multiple of 3 in size, so each sane_read - operation will return an integral number of RGB triples. */ - SANE_Byte buffer[READSIZE]; /* XXX how big should the buffer be? */ - SANE_Int len, lastlen; - Imaging im; - SANE_Parameters p; - int px, py, remain, cplen, bufpos, padbytes; - long L; - char errmsg[80]; - union - { char c[2]; - INT16 i16; - } - endian; - PyObject *pyNoCancel = NULL; - int noCancel = 0; - - endian.i16 = 1; - - if (!PyArg_ParseTuple(args, "l|O", &L, &pyNoCancel)) - return NULL; - if (self->h==NULL) - { - PyErr_SetString(ErrorObject, "SaneDev object is closed"); - return NULL; - } - im=(Imaging)L; - - if (pyNoCancel) - noCancel = PyObject_IsTrue(pyNoCancel); - - st=SANE_STATUS_GOOD; px=py=0; - /* xxx not yet implemented - - handscanner support (i.e., unknown image length during start) - - generally: move the functionality from method snap in sane.py - down here -- I don't like this cross-dependency. - we need to call sane_get_parameters here, and we can create - the result Image object here. - */ - - Py_UNBLOCK_THREADS - sane_get_parameters(self->h, &p); - if (p.format == SANE_FRAME_GRAY) - { - switch (p.depth) - { - case 1: - remain = p.bytes_per_line * im->ysize; - padbytes = p.bytes_per_line - (im->xsize+7)/8; - bufpos = 0; - lastlen = len = 0; - while (st!=SANE_STATUS_EOF && py < im->ysize) - { - while (len > 0 && py < im->ysize) - { - int i, j, k; - j = buffer[bufpos++]; - k = 0x80; - for (i = 0; i < 8 && px < im->xsize; i++) - { - im->image8[py][px++] = (k&j) ? 0 : 0xFF; - k = k >> 1; - } - len--; - if (px >= im->xsize) - { - bufpos += padbytes; - len -= padbytes; - py++; - px = 0; - } - } - st=sane_read(self->h, buffer, - remainh); - Py_BLOCK_THREADS - return PySane_Error(st); - } - bufpos -= lastlen; - lastlen = len; - remain -= len; - /* skip possible pad bytes at the start of the buffer */ - len -= bufpos; - } - break; - case 8: - remain = p.bytes_per_line * im->ysize; - padbytes = p.bytes_per_line - im->xsize; - bufpos = 0; - len = 0; - while (st!=SANE_STATUS_EOF && py < im->ysize) - { - while (len > 0 && py < im->ysize) - { - cplen = len; - if (px + cplen >= im->xsize) - cplen = im->xsize - px; - memcpy(&im->image8[py][px], &buffer[bufpos], cplen); - len -= cplen; - bufpos += cplen; - px += cplen; - if (px >= im->xsize) - { - px = 0; - py++; - bufpos += padbytes; - len -= padbytes; - } - } - bufpos = -len; - - st=sane_read(self->h, buffer, - remainh); - Py_BLOCK_THREADS - return PySane_Error(st); - } - remain -= len; - len -= bufpos; - } - break; - case 16: - remain = p.bytes_per_line * im->ysize; - padbytes = p.bytes_per_line - 2 * im->xsize; - bufpos = endian.c[0]; - lastlen = len = 0; - while (st!=SANE_STATUS_EOF && py < im->ysize) - { - while (len > 0 && py < im->ysize) - { - im->image8[py][px++] = buffer[bufpos]; - bufpos += 2; - len -= 2; - if (px >= im->xsize) - { - bufpos += padbytes; - len -= padbytes; - py++; - px = 0; - } - } - st=sane_read(self->h, buffer, - remainh); - Py_BLOCK_THREADS - return PySane_Error(st); - } - remain -= len; - bufpos -= lastlen; - lastlen = len; - len -= bufpos; - } - break; - default: - /* other depths are not formally "illegal" according to the - Sane API, but it's agreed by Sane developers that other - depths than 1, 8, 16 should not be used - */ - sane_cancel(self->h); - Py_BLOCK_THREADS - snprintf(errmsg, 80, "unsupported pixel depth: %i", p.depth); - PyErr_SetString(ErrorObject, errmsg); - return NULL; - } - } - else if (p.format == SANE_FRAME_RGB) - { - int incr, color, pxs, pxmax, bit, val, mask; - switch (p.depth) - { - case 1: - remain = p.bytes_per_line * im->ysize; - padbytes = p.bytes_per_line - ((im->xsize+7)/8) * 3; - bufpos = 0; - len = 0; - lastlen = 0; - pxmax = 4 * im->xsize; - while (st!=SANE_STATUS_EOF && py < im->ysize) - { - pxs = px; - for (color = 0; color < 3; color++) - { - while (len <= 0 && st == SANE_STATUS_GOOD) - { - st=sane_read(self->h, buffer, - remainh); - Py_BLOCK_THREADS - return PySane_Error(st); - } - bufpos -= lastlen; - remain -= len; - lastlen = len; - /* skip possible pad bytes at the start of the buffer */ - len -= bufpos; - } - if (st == SANE_STATUS_EOF) break; - pxs = px; - val = buffer[bufpos++]; - len--; - mask = 0x80; - for (bit = 0; (bit < 8) && (px < pxmax); bit++) - { - ((UINT8**)(im->image32))[py][px] = (val&mask) ? 0xFF : 0; - mask = mask >> 1; - px += 4; - } - pxs++; - px = pxs; - } - if (st == SANE_STATUS_EOF) - break; - for (bit = 0; bit < 8 && px < pxmax; bit++) - { - ((UINT8**)(im->image32))[py][px] = 0; - px += 4; - } - px -= 3; - if (px >= pxmax) - { - bufpos += padbytes; - len -= padbytes; - py++; - px = 0; - } - } - break; - case 8: - case 16: - if (p.depth == 8) - { - padbytes = p.bytes_per_line - 3 * im->xsize; - bufpos = 0; - incr = 1; - } - else - { - padbytes = p.bytes_per_line - 6 * im->xsize; - bufpos = endian.c[0]; - incr = 2; - } - remain = p.bytes_per_line * im->ysize; - len = 0; - lastlen = 0; - pxmax = 4 * im->xsize; - /* probably not very efficient. But we have to deal with these - possible conditions: - - we may have padding bytes at the end of a scan line - - the number of bytes read with sane_read may be smaller - than the number of pad bytes - - the buffer may become empty after setting any of the - red/green/blue pixel values - - */ - while (st != SANE_STATUS_EOF && py < im->ysize) - { - for (color = 0; color < 3; color++) - { - while (len <= 0 && st == SANE_STATUS_GOOD) - { - bufpos -= lastlen; - if (remain == 0) - { - sane_cancel(self->h); - Py_BLOCK_THREADS - PyErr_SetString(ErrorObject, "internal _sane error: premature end of scan"); - return NULL; - } - st = sane_read(self->h, buffer, - remain<(READSIZE) ? remain : (READSIZE), &len); - if (st && (st!=SANE_STATUS_EOF)) - { - sane_cancel(self->h); - Py_BLOCK_THREADS - return PySane_Error(st); - } - lastlen = len; - remain -= len; - len -= bufpos; - } - if (st == SANE_STATUS_EOF) break; - ((UINT8**)(im->image32))[py][px++] = buffer[bufpos]; - bufpos += incr; - len -= incr; - } - if (st == SANE_STATUS_EOF) break; - - ((UINT8**)(im->image32))[py][px++] = 0; - - if (px >= pxmax) - { - px = 0; - py++; - bufpos += padbytes; - len -= padbytes; - } - } - break; - default: - Py_BLOCK_THREADS - sane_cancel(self->h); - snprintf(errmsg, 80, "unsupported pixel depth: %i", p.depth); - PyErr_SetString(ErrorObject, errmsg); - return NULL; - } - - } - else /* should be SANE_FRAME_RED, GREEN or BLUE */ - { - int lastlen, pxa, pxmax, offset, incr, frame_count = 0; - /* at least the Sane test backend behaves a bit weird, if - it returns "premature EOF" for sane_read, i.e., if the - option "return value of sane_read" is set to SANE_STATUS_EOF. - In this case, the test backend does not advance to the next frame, - so p.last_frame will never be set... - So let's count the number of frames we try to acquire - */ - while (!p.last_frame && frame_count < 4) - { - frame_count++; - st = sane_get_parameters(self->h, &p); - if (st) - { - sane_cancel(self->h); - Py_BLOCK_THREADS - return PySane_Error(st); - } - remain = p.bytes_per_line * im->ysize; - bufpos = 0; - len = 0; - lastlen = 0; - py = 0; - switch (p.format) - { - case SANE_FRAME_RED: - offset = 0; - break; - case SANE_FRAME_GREEN: - offset = 1; - break; - case SANE_FRAME_BLUE: - offset = 2; - break; - default: - sane_cancel(self->h); - Py_BLOCK_THREADS - snprintf(errmsg, 80, "unknown/invalid frame format: %i", p.format); - PyErr_SetString(ErrorObject, errmsg); - return NULL; - } - px = offset; - pxa = 3; - pxmax = im->xsize * 4; - switch (p.depth) - { - case 1: - padbytes = p.bytes_per_line - (im->xsize+7)/8; - st = SANE_STATUS_GOOD; - while (st != SANE_STATUS_EOF && py < im->ysize) - { - while (len > 0) - { - int bit, mask, val; - val = buffer[bufpos++]; len--; - mask = 0x80; - for (bit = 0; bit < 8 && px < pxmax; bit++) - { - ((UINT8**)(im->image32))[py][px] - = val&mask ? 0xFF : 0; - ((UINT8**)(im->image32))[py][pxa] = 0; - px += 4; - pxa += 4; - mask = mask >> 1; - } - - if (px >= pxmax) - { - px = offset; - pxa = 3; - py++; - bufpos += padbytes; - len -= padbytes; - } - } - while (len <= 0 && st == SANE_STATUS_GOOD && remain > 0) - { - bufpos -= lastlen; - st = sane_read(self->h, buffer, - remain<(READSIZE) ? remain : (READSIZE), &len); - if (st && (st!=SANE_STATUS_EOF)) - { - sane_cancel(self->h); - Py_BLOCK_THREADS - return PySane_Error(st); - } - remain -= len; - lastlen = len; - len -= bufpos; - } - } - break; - case 8: - case 16: - if (p.depth == 8) - { - padbytes = p.bytes_per_line - im->xsize; - incr = 1; - } - else - { - padbytes = p.bytes_per_line - 2 * im->xsize; - incr = 2; - bufpos = endian.c[0]; - } - st = SANE_STATUS_GOOD; - while (st != SANE_STATUS_EOF && py < im->ysize) - { - while (len <= 0) - { - bufpos -= lastlen; - if (remain == 0) - { - sane_cancel(self->h); - Py_BLOCK_THREADS - PyErr_SetString(ErrorObject, "internal _sane error: premature end of scan"); - return NULL; - } - st = sane_read(self->h, buffer, - remain<(READSIZE) ? remain : (READSIZE), &len); - if (st && (st!=SANE_STATUS_EOF)) - { - sane_cancel(self->h); - Py_BLOCK_THREADS - return PySane_Error(st); - } - if (st == SANE_STATUS_EOF) - break; - lastlen = len; - remain -= len; - if (bufpos >= len) - len = 0; - else - len -= bufpos; - } - if (st == SANE_STATUS_EOF) - break; - ((UINT8**)(im->image32))[py][px] = buffer[bufpos]; - ((UINT8**)(im->image32))[py][pxa] = 0; - bufpos += incr; - len -= incr; - px += 4; - pxa += 4; - - if (px >= pxmax) - { - px = offset; - pxa = 3; - py++; - bufpos += padbytes; - len -= padbytes; - } - } - break; - default: - sane_cancel(self->h); - Py_BLOCK_THREADS - snprintf(errmsg, 80, "unsupported pixel depth: %i", p.depth); - PyErr_SetString(ErrorObject, errmsg); - return NULL; - } - if (!p.last_frame) - { - /* all sane_read calls in the above loop may return - SANE_STATUS_GOOD, but the backend may need another sane_read - call which returns SANE_STATUS_EOF in order to start - a new frame. - */ - if (st != SANE_STATUS_EOF) - { - do { - st = sane_read(self->h, buffer, READSIZE, &len); - } - while (st == SANE_STATUS_GOOD); - } - if (st != SANE_STATUS_EOF) - { - Py_BLOCK_THREADS - sane_cancel(self->h); - return PySane_Error(st); - } - - st = sane_start(self->h); - if (st) - { - Py_BLOCK_THREADS - return PySane_Error(st); - } - } - } - } - /* enforce SANE_STATUS_EOF. Can be necessary for ADF scans for some backends */ - if (st != SANE_STATUS_EOF) - { - do { - st = sane_read(self->h, buffer, READSIZE, &len); - } - while (st == SANE_STATUS_GOOD); - } - if (st != SANE_STATUS_EOF) - { - sane_cancel(self->h); - Py_BLOCK_THREADS - return PySane_Error(st); - } - - if (!noCancel) - sane_cancel(self->h); - Py_BLOCK_THREADS - Py_INCREF(Py_None); - return Py_None; -} - - -#ifdef WITH_NUMARRAY - -#include "numarray/libnumarray.h" - -/* this global variable is set to 1 in 'init_sane()' after successfully - importing the numarray module. */ -int NUMARRAY_IMPORTED = 0; - -static PyObject * -SaneDev_arr_snap(SaneDevObject *self, PyObject *args) -{ - SANE_Status st; - SANE_Byte buffer[READSIZE]; - SANE_Int len; - SANE_Parameters p; - - PyArrayObject *pyArr = NULL; - NumarrayType arrType; - int line, line_index, buffer_index, remain_bytes_line, num_pad_bytes; - int cp_num_bytes, total_remain, bpp, arr_bytes_per_line; - int pixels_per_line = -1; - char errmsg[80]; - - if (!NUMARRAY_IMPORTED) - { - PyErr_SetString(ErrorObject, "numarray package not available"); - return NULL; - } - - if (!PyArg_ParseTuple(args, "|i", &pixels_per_line)) - return NULL; - if (self->h==NULL) - { - PyErr_SetString(ErrorObject, "SaneDev object is closed"); - return NULL; - } - - sane_get_parameters(self->h, &p); - if (p.format != SANE_FRAME_GRAY) - { - sane_cancel(self->h); - snprintf(errmsg, 80, "numarray only supports gray-scale images"); - PyErr_SetString(ErrorObject, errmsg); - return NULL; - } - - if (p.depth == 8) - { - bpp=1; /* bytes-per-pixel */ - arrType = tUInt8; - } - else if (p.depth == 16) - { - bpp=2; /* bytes-per-pixel */ - arrType = tUInt16; - } - else - { - sane_cancel(self->h); - snprintf(errmsg, 80, "arrsnap: unsupported pixel depth: %i", p.depth); - PyErr_SetString(ErrorObject, errmsg); - return NULL; - } - - if (pixels_per_line < 1) - /* The user can choose a smaller result array than the actual scan */ - pixels_per_line = p.pixels_per_line; - else - if (pixels_per_line > p.pixels_per_line) - { - PyErr_SetString(ErrorObject,"given pixels_per_line too big"); - return NULL; - } - /* important: NumArray have indices like (y, x) !! */ - if (!(pyArr = NA_NewArray(NULL, arrType, 2, p.lines, pixels_per_line))) - { - PyErr_SetString(ErrorObject, "failed to create NumArray object"); - return NULL; - } - - arr_bytes_per_line = pixels_per_line * bpp; - st=SANE_STATUS_GOOD; -#ifdef WRITE_PGM - FILE *fp; - fp = fopen("sane_p5.pgm", "w"); - fprintf(fp, "P5\n%d %d\n%d\n", p.pixels_per_line, - p.lines, (int) pow(2.0, (double) p.depth)-1); -#endif - line_index = line = 0; - remain_bytes_line = arr_bytes_per_line; - total_remain = p.bytes_per_line * p.lines; - num_pad_bytes = p.bytes_per_line - arr_bytes_per_line; - - while (st!=SANE_STATUS_EOF) - { - Py_BEGIN_ALLOW_THREADS - st = sane_read(self->h, buffer, - READSIZE < total_remain ? READSIZE : total_remain, &len); - Py_END_ALLOW_THREADS -#ifdef WRITE_PGM - printf("p5_write: read %d of %d\n", len, READSIZE); - fwrite(buffer, 1, len, fp); -#endif - - buffer_index = 0; - total_remain -= len; - - while (len > 0) - { - /* copy at most the number of bytes that fit into (the rest of) - one line: */ - cp_num_bytes = (len > remain_bytes_line ? remain_bytes_line : len); - remain_bytes_line -= cp_num_bytes; - len -= cp_num_bytes; -#ifdef DEBUG - printf("copying %d bytes from b_idx %d to d_idx %d\n", - cp_num_bytes, buffer_index, - line * arr_bytes_per_line + line_index); - printf("len is now %d\n", len); -#endif - memcpy(pyArr->data + line * arr_bytes_per_line + line_index, - buffer + buffer_index, cp_num_bytes); - - buffer_index += cp_num_bytes; - if (remain_bytes_line ==0) - { - /* The line has been completed, so reinitialize remain_bytes_line - increase the line counter, and reset line_index */ -#ifdef DEBUG - printf("line %d full, skipping %d bytes\n",line,num_pad_bytes); -#endif - remain_bytes_line = arr_bytes_per_line; - line++; - line_index = 0; - /* Skip the number of bytes in the input stream which - are not used: */ - len -= num_pad_bytes; - buffer_index += num_pad_bytes; - } - else - line_index += cp_num_bytes; - } - } -#ifdef WRITE_PGM - fclose(fp); - printf("p5_write finished\n"); -#endif - sane_cancel(self->h); - return (PyObject*) pyArr; -} - - - -#endif /* WITH_NUMARRAY */ - -static PyMethodDef SaneDev_methods[] = { - {"get_parameters", (PyCFunction)SaneDev_get_parameters, 1}, - - {"get_options", (PyCFunction)SaneDev_get_options, 1}, - {"get_option", (PyCFunction)SaneDev_get_option, 1}, - {"set_option", (PyCFunction)SaneDev_set_option, 1}, - {"set_auto_option", (PyCFunction)SaneDev_set_auto_option, 1}, - - {"start", (PyCFunction)SaneDev_start, 1}, - {"cancel", (PyCFunction)SaneDev_cancel, 1}, - {"snap", (PyCFunction)SaneDev_snap, 1}, -#ifdef WITH_NUMARRAY - {"arr_snap", (PyCFunction)SaneDev_arr_snap, 1}, -#endif /* WITH_NUMARRAY */ - {"fileno", (PyCFunction)SaneDev_fileno, 1}, - {"close", (PyCFunction)SaneDev_close, 1}, - {NULL, NULL} /* sentinel */ -}; - -static PyTypeObject SaneDev_Type = { - PyVarObject_HEAD_INIT(NULL, 0) - "SaneDev", /*tp_name*/ - sizeof(SaneDevObject), /*tp_basicsize*/ - 0, /*tp_itemsize*/ - /* methods */ - (destructor)SaneDev_dealloc, /*tp_dealloc*/ - 0, /*tp_print*/ - 0, /*tp_getattr*/ - 0, /*tp_setattr*/ - 0, /*tp_compare*/ - 0, /*tp_repr*/ - 0, /*tp_as_number */ - 0, /*tp_as_sequence */ - 0, /*tp_as_mapping */ - 0, /*tp_hash*/ - 0, /*tp_call*/ - 0, /*tp_str*/ - 0, /*tp_getattro*/ - 0, /*tp_setattro*/ - 0, /*tp_as_buffer*/ - Py_TPFLAGS_DEFAULT, /*tp_flags*/ - 0, /*tp_doc*/ - 0, /*tp_traverse*/ - 0, /*tp_clear*/ - 0, /*tp_richcompare*/ - 0, /*tp_weaklistoffset*/ - 0, /*tp_iter*/ - 0, /*tp_iternext*/ - SaneDev_methods, /*tp_methods*/ - 0, /*tp_members*/ - 0, /*tp_getset*/ -}; - -/* --------------------------------------------------------------------- */ - -static PyObject * -PySane_init(PyObject *self, PyObject *args) -{ - SANE_Status st; - SANE_Int version; - - if (!PyArg_ParseTuple(args, "")) - return NULL; - - /* XXX Authorization is not yet supported */ - st=sane_init(&version, NULL); - if (st) return PySane_Error(st); - return Py_BuildValue("iiii", version, SANE_VERSION_MAJOR(version), - SANE_VERSION_MINOR(version), SANE_VERSION_BUILD(version)); -} - -static PyObject * -PySane_exit(PyObject *self, PyObject *args) -{ - if (!PyArg_ParseTuple(args, "")) - return NULL; - - sane_exit(); - Py_INCREF(Py_None); - return Py_None; -} - -static PyObject * -PySane_get_devices(PyObject *self, PyObject *args) -{ - const SANE_Device **devlist; - const SANE_Device *dev; - SANE_Status st; - PyObject *list; - int local_only = 0, i; - - if (!PyArg_ParseTuple(args, "|i", &local_only)) - { - return NULL; - } - - Py_BEGIN_ALLOW_THREADS - st=sane_get_devices(&devlist, local_only); - Py_END_ALLOW_THREADS - if (st) return PySane_Error(st); - if (!(list = PyList_New(0))) - return NULL; - for(i=0; devlist[i]!=NULL; i++) - { - dev=devlist[i]; - PyList_Append(list, Py_BuildValue("ssss", dev->name, dev->vendor, - dev->model, dev->type)); - } - - return list; -} - -/* Function returning new SaneDev object */ - -static PyObject * -PySane_open(PyObject *self, PyObject *args) -{ - SaneDevObject *rv; - SANE_Status st; - char *name; - - if (!PyArg_ParseTuple(args, "s", &name)) - return NULL; - rv = newSaneDevObject(); - if ( rv == NULL ) - return NULL; - Py_BEGIN_ALLOW_THREADS - st = sane_open(name, &(rv->h)); - Py_END_ALLOW_THREADS - if (st) - { - Py_DECREF(rv); - return PySane_Error(st); - } - return (PyObject *)rv; -} - -static PyObject * -PySane_OPTION_IS_ACTIVE(PyObject *self, PyObject *args) -{ - SANE_Int cap; - long lg; - - if (!PyArg_ParseTuple(args, "l", &lg)) - return NULL; - cap=lg; - return PyInt_FromLong( SANE_OPTION_IS_ACTIVE(cap)); -} - -static PyObject * -PySane_OPTION_IS_SETTABLE(PyObject *self, PyObject *args) -{ - SANE_Int cap; - long lg; - - if (!PyArg_ParseTuple(args, "l", &lg)) - return NULL; - cap=lg; - return PyInt_FromLong( SANE_OPTION_IS_SETTABLE(cap)); -} - - -/* List of functions defined in the module */ - -static PyMethodDef PySane_methods[] = { - {"init", PySane_init, 1}, - {"exit", PySane_exit, 1}, - {"get_devices", PySane_get_devices, 1}, - {"_open", PySane_open, 1}, - {"OPTION_IS_ACTIVE", PySane_OPTION_IS_ACTIVE, 1}, - {"OPTION_IS_SETTABLE", PySane_OPTION_IS_SETTABLE, 1}, - {NULL, NULL} /* sentinel */ -}; - - -static void -insint(PyObject *d, char *name, int value) -{ - PyObject *v = PyInt_FromLong((long) value); - if (!v || PyDict_SetItemString(d, name, v)) - Py_FatalError("can't initialize sane module"); - - Py_DECREF(v); -} - -#if PY_MAJOR_VERSION >= 3 -static struct PyModuleDef PySane_moduledef = { - PyModuleDef_HEAD_INIT, - "_sane", - NULL, - 0, - PySane_methods, - NULL, - NULL, - NULL, - NULL -}; - -PyMODINIT_FUNC -PyInit__sane(void) -{ - /* Create the module and add the functions */ - PyObject *m = PyModule_Create(&PySane_moduledef); - if(!m) - return NULL; -#else /* if PY_MAJOR_VERSION < 3 */ - -PyMODINIT_FUNC -init_sane(void) -{ - /* Create the module and add the functions */ - PyObject *m = Py_InitModule("_sane", PySane_methods); - if(!m) - return; -#endif - - /* Add some symbolic constants to the module */ - PyObject *d = PyModule_GetDict(m); - ErrorObject = PyErr_NewException("_sane.error", NULL, NULL); - PyDict_SetItemString(d, "error", ErrorObject); - - insint(d, "INFO_INEXACT", SANE_INFO_INEXACT); - insint(d, "INFO_RELOAD_OPTIONS", SANE_INFO_RELOAD_OPTIONS); - insint(d, "RELOAD_PARAMS", SANE_INFO_RELOAD_PARAMS); - - insint(d, "FRAME_GRAY", SANE_FRAME_GRAY); - insint(d, "FRAME_RGB", SANE_FRAME_RGB); - insint(d, "FRAME_RED", SANE_FRAME_RED); - insint(d, "FRAME_GREEN", SANE_FRAME_GREEN); - insint(d, "FRAME_BLUE", SANE_FRAME_BLUE); - - insint(d, "CONSTRAINT_NONE", SANE_CONSTRAINT_NONE); - insint(d, "CONSTRAINT_RANGE", SANE_CONSTRAINT_RANGE); - insint(d, "CONSTRAINT_WORD_LIST", SANE_CONSTRAINT_WORD_LIST); - insint(d, "CONSTRAINT_STRING_LIST", SANE_CONSTRAINT_STRING_LIST); - - insint(d, "TYPE_BOOL", SANE_TYPE_BOOL); - insint(d, "TYPE_INT", SANE_TYPE_INT); - insint(d, "TYPE_FIXED", SANE_TYPE_FIXED); - insint(d, "TYPE_STRING", SANE_TYPE_STRING); - insint(d, "TYPE_BUTTON", SANE_TYPE_BUTTON); - insint(d, "TYPE_GROUP", SANE_TYPE_GROUP); - - insint(d, "UNIT_NONE", SANE_UNIT_NONE); - insint(d, "UNIT_PIXEL", SANE_UNIT_PIXEL); - insint(d, "UNIT_BIT", SANE_UNIT_BIT); - insint(d, "UNIT_MM", SANE_UNIT_MM); - insint(d, "UNIT_DPI", SANE_UNIT_DPI); - insint(d, "UNIT_PERCENT", SANE_UNIT_PERCENT); - insint(d, "UNIT_MICROSECOND", SANE_UNIT_MICROSECOND); - - insint(d, "CAP_SOFT_SELECT", SANE_CAP_SOFT_SELECT); - insint(d, "CAP_HARD_SELECT", SANE_CAP_HARD_SELECT); - insint(d, "CAP_SOFT_DETECT", SANE_CAP_SOFT_DETECT); - insint(d, "CAP_EMULATED", SANE_CAP_EMULATED); - insint(d, "CAP_AUTOMATIC", SANE_CAP_AUTOMATIC); - insint(d, "CAP_INACTIVE", SANE_CAP_INACTIVE); - insint(d, "CAP_ADVANCED", SANE_CAP_ADVANCED); - - /* handy for checking array lengths: */ - insint(d, "SANE_WORD_SIZE", sizeof(SANE_Word)); - - /* possible return values of set_option() */ - insint(d, "INFO_INEXACT", SANE_INFO_INEXACT); - insint(d, "INFO_RELOAD_OPTIONS", SANE_INFO_RELOAD_OPTIONS); - insint(d, "INFO_RELOAD_PARAMS", SANE_INFO_RELOAD_PARAMS); - - /* Check for errors */ - if (PyErr_Occurred()) - Py_FatalError("can't initialize module _sane"); - -#ifdef WITH_NUMARRAY - import_libnumarray(); - if (PyErr_Occurred()) - PyErr_Clear(); - else - /* this global variable is declared just in front of the - arr_snap() function and should be set to 1 after - successfully importing the numarray module. */ - NUMARRAY_IMPORTED = 1; - -#endif /* WITH_NUMARRAY */ -#if PY_MAJOR_VERSION >= 3 - return m; -#endif -} diff --git a/Sane/demo_numarray.py b/Sane/demo_numarray.py deleted file mode 100644 index 57fcc4407..000000000 --- a/Sane/demo_numarray.py +++ /dev/null @@ -1,41 +0,0 @@ -#!/usr/bin/env python - -# -# Shows how to scan a 16 bit grayscale image into a numarray object -# - -from __future__ import print_function - -# Get the path set up to find PIL modules if not installed yet: -import sys ; sys.path.append('../PIL') - -from numarray import * -import sane -import Image - -def toImage(arr): - if arr.type().bytes == 1: - # need to swap coordinates btw array and image (with [::-1]) - im = Image.frombytes('L', arr.shape[::-1], arr.tostring()) - else: - arr_c = arr - arr.min() - arr_c *= (255./arr_c.max()) - arr = arr_c.astype(UInt8) - # need to swap coordinates btw array and image (with [::-1]) - im = Image.frombytes('L', arr.shape[::-1], arr.tostring()) - return im - -print('SANE version:', sane.init()) -print('Available devices=', sane.get_devices()) - -s = sane.open(sane.get_devices()[0][0]) - -# Set scan parameters -s.mode = 'gray' -s.br_x=320. ; s.br_y=240. - -print('Device parameters:', s.get_parameters()) - -s.depth=16 -arr16 = s.arr_scan() -toImage(arr16).show() diff --git a/Sane/demo_pil.py b/Sane/demo_pil.py deleted file mode 100644 index 490f33158..000000000 --- a/Sane/demo_pil.py +++ /dev/null @@ -1,35 +0,0 @@ -#!/usr/bin/env python - -# -# Shows how to scan a color image into a PIL rgb-image -# - -from __future__ import print_function - -# Get the path set up to find PIL modules if not installed yet: -import sys ; sys.path.append('../PIL') - -import sane -print('SANE version:', sane.init()) -print('Available devices=', sane.get_devices()) - -s = sane.open(sane.get_devices()[0][0]) - -s.mode = 'color' -s.br_x=320. ; s.br_y=240. - -print('Device parameters:', s.get_parameters()) - -# Initiate the scan -s.start() - -# Get an Image object -# (For my B&W QuickCam, this is a grey-scale image. Other scanning devices -# may return a -im=s.snap() - -# Write the image out as a GIF file -#im.save('foo.gif') - -# The show method() simply saves the image to a temporary file and calls "xv". -im.show() diff --git a/Sane/sane.py b/Sane/sane.py deleted file mode 100644 index 331776f95..000000000 --- a/Sane/sane.py +++ /dev/null @@ -1,288 +0,0 @@ -# sane.py -# -# Python wrapper on top of the _sane module, which is in turn a very -# thin wrapper on top of the SANE library. For a complete understanding -# of SANE, consult the documentation at the SANE home page: -# http://www.mostang.com/sane/ . - -__version__ = '2.0' -__author__ = ['Andrew Kuchling', 'Ralph Heinkel'] - -from PIL import Image - -import _sane -from _sane import * - -TYPE_STR = { TYPE_BOOL: "TYPE_BOOL", TYPE_INT: "TYPE_INT", - TYPE_FIXED: "TYPE_FIXED", TYPE_STRING: "TYPE_STRING", - TYPE_BUTTON: "TYPE_BUTTON", TYPE_GROUP: "TYPE_GROUP" } - -UNIT_STR = { UNIT_NONE: "UNIT_NONE", - UNIT_PIXEL: "UNIT_PIXEL", - UNIT_BIT: "UNIT_BIT", - UNIT_MM: "UNIT_MM", - UNIT_DPI: "UNIT_DPI", - UNIT_PERCENT: "UNIT_PERCENT", - UNIT_MICROSECOND: "UNIT_MICROSECOND" } - - -class Option: - """Class representing a SANE option. - Attributes: - index -- number from 0 to n, giving the option number - name -- a string uniquely identifying the option - title -- single-line string containing a title for the option - desc -- a long string describing the option; useful as a help message - type -- type of this option. Possible values: TYPE_BOOL, - TYPE_INT, TYPE_STRING, and so forth. - unit -- units of this option. Possible values: UNIT_NONE, - UNIT_PIXEL, etc. - size -- size of the value in bytes - cap -- capabilities available; CAP_EMULATED, CAP_SOFT_SELECT, etc. - constraint -- constraint on values. Possible values: - None : No constraint - (min,max,step) Integer values, from min to max, stepping by - list of integers or strings: only the listed values are allowed - """ - - def __init__(self, args, scanDev): - self.scanDev = scanDev # needed to get current value of this option - self.index, self.name = args[0], args[1] - self.title, self.desc = args[2], args[3] - self.type, self.unit = args[4], args[5] - self.size, self.cap = args[6], args[7] - self.constraint = args[8] - def f(x): - if x=='-': return '_' - else: return x - if not isinstance(self.name, str): self.py_name=str(self.name) - else: self.py_name=''.join(map(f, self.name)) - - def is_active(self): - return _sane.OPTION_IS_ACTIVE(self.cap) - def is_settable(self): - return _sane.OPTION_IS_SETTABLE(self.cap) - def __repr__(self): - if self.is_settable(): - settable = 'yes' - else: - settable = 'no' - if self.is_active(): - active = 'yes' - curValue = repr(getattr(self.scanDev, self.py_name)) - else: - active = 'no' - curValue = '' - s = """\nName: %s -Cur value: %s -Index: %d -Title: %s -Desc: %s -Type: %s -Unit: %s -Constr: %s -active: %s -settable: %s\n""" % (self.py_name, curValue, - self.index, self.title, self.desc, - TYPE_STR[self.type], UNIT_STR[self.unit], - repr(self.constraint), active, settable) - return s - - -class _SaneIterator: - """ intended for ADF scans. - """ - - def __init__(self, device): - self.device = device - - def __iter__(self): - return self - - def __del__(self): - self.device.cancel() - - def next(self): - try: - self.device.start() - except error as v: - if v == 'Document feeder out of documents': - raise StopIteration - else: - raise - return self.device.snap(1) - - - -class SaneDev: - """Class representing a SANE device. - Methods: - start() -- initiate a scan, using the current settings - snap() -- snap a picture, returning an Image object - arr_snap() -- snap a picture, returning a numarray object - cancel() -- cancel an in-progress scanning operation - fileno() -- return the file descriptor for the scanner (handy for select) - - Also available, but rather low-level: - get_parameters() -- get the current parameter settings of the device - get_options() -- return a list of tuples describing all the options. - - Attributes: - optlist -- list of option names - - You can also access an option name to retrieve its value, and to - set it. For example, if one option has a .name attribute of - imagemode, and scanner is a SaneDev object, you can do: - print scanner.imagemode - scanner.imagemode = 'Full frame' - scanner.['imagemode'] returns the corresponding Option object. - """ - def __init__(self, devname): - d=self.__dict__ - d['sane_signature'] = self._getSaneSignature(devname) - d['scanner_model'] = d['sane_signature'][1:3] - d['dev'] = _sane._open(devname) - self.__load_option_dict() - - def _getSaneSignature(self, devname): - devices = get_devices() - if not devices: - raise RuntimeError('no scanner available') - for dev in devices: - if devname == dev[0]: - return dev - raise RuntimeError('no such scan device "%s"' % devname) - - def __load_option_dict(self): - d=self.__dict__ - d['opt']={} - optlist=d['dev'].get_options() - for t in optlist: - o=Option(t, self) - if o.type!=TYPE_GROUP: - d['opt'][o.py_name]=o - - def __setattr__(self, key, value): - dev=self.__dict__['dev'] - optdict=self.__dict__['opt'] - if key not in optdict: - self.__dict__[key]=value ; return - opt=optdict[key] - if opt.type==TYPE_GROUP: - raise AttributeError("Groups can't be set: "+key) - if not _sane.OPTION_IS_ACTIVE(opt.cap): - raise AttributeError('Inactive option: '+key) - if not _sane.OPTION_IS_SETTABLE(opt.cap): - raise AttributeError("Option can't be set by software: "+key) - if isinstance(value, int) and opt.type == TYPE_FIXED: - # avoid annoying errors of backend if int is given instead float: - value = float(value) - self.last_opt = dev.set_option(opt.index, value) - # do binary AND to find if we have to reload options: - if self.last_opt & INFO_RELOAD_OPTIONS: - self.__load_option_dict() - - def __getattr__(self, key): - dev=self.__dict__['dev'] - optdict=self.__dict__['opt'] - if key=='optlist': - return list(self.opt.keys()) - if key=='area': - return (self.tl_x, self.tl_y),(self.br_x, self.br_y) - if key not in optdict: - raise AttributeError('No such attribute: '+key) - opt=optdict[key] - if opt.type==TYPE_BUTTON: - raise AttributeError("Buttons don't have values: "+key) - if opt.type==TYPE_GROUP: - raise AttributeError("Groups don't have values: "+key) - if not _sane.OPTION_IS_ACTIVE(opt.cap): - raise AttributeError('Inactive option: '+key) - value = dev.get_option(opt.index) - return value - - def __getitem__(self, key): - return self.opt[key] - - def get_parameters(self): - """Return a 5-tuple holding all the current device settings: - (format, last_frame, (pixels_per_line, lines), depth, bytes_per_line) - -- format is one of 'L' (grey), 'RGB', 'R' (red), 'G' (green), 'B' (blue). -- last_frame [bool] indicates if this is the last frame of a multi frame image -- (pixels_per_line, lines) specifies the size of the scanned image (x,y) -- lines denotes the number of scanlines per frame -- depth gives number of pixels per sample -""" - return self.dev.get_parameters() - - def get_options(self): - "Return a list of tuples describing all the available options" - return self.dev.get_options() - - def start(self): - "Initiate a scanning operation" - return self.dev.start() - - def cancel(self): - "Cancel an in-progress scanning operation" - return self.dev.cancel() - - def snap(self, no_cancel=0): - "Snap a picture, returning a PIL image object with the results" - (mode, last_frame, - (xsize, ysize), depth, bytes_per_line) = self.get_parameters() - if mode in ['gray', 'red', 'green', 'blue']: - format = 'L' - elif mode == 'color': - format = 'RGB' - else: - raise ValueError('got unknown "mode" from self.get_parameters()') - im=Image.new(format, (xsize,ysize)) - self.dev.snap( im.im.id, no_cancel ) - return im - - def scan(self): - self.start() - return self.snap() - - def multi_scan(self): - return _SaneIterator(self) - - def arr_snap(self, multipleOf=1): - """Snap a picture, returning a numarray object with the results. - By default the resulting array has the same number of pixels per - line as specified in self.get_parameters()[2][0] - However sometimes it is necessary to obtain arrays where - the number of pixels per line is e.g. a multiple of 4. This can then - be achieved with the option 'multipleOf=4'. So if the scanner - scanned 34 pixels per line, you will obtain an array with 32 pixels - per line. - """ - (mode, last_frame, (xsize, ysize), depth, bpl) = self.get_parameters() - if not mode in ['gray', 'red', 'green', 'blue']: - raise RuntimeError('arr_snap() only works with monochrome images') - if multipleOf < 1: - raise ValueError('option "multipleOf" must be a positive number') - elif multipleOf > 1: - pixels_per_line = xsize - divmod(xsize, 4)[1] - else: - pixels_per_line = xsize - return self.dev.arr_snap(pixels_per_line) - - def arr_scan(self, multipleOf=1): - self.start() - return self.arr_snap(multipleOf=multipleOf) - - def fileno(self): - "Return the file descriptor for the scanning device" - return self.dev.fileno() - - def close(self): - self.dev.close() - - -def open(devname): - "Open a device for scanning" - new=SaneDev(devname) - return new diff --git a/Sane/sanedoc.txt b/Sane/sanedoc.txt deleted file mode 100644 index f23000122..000000000 --- a/Sane/sanedoc.txt +++ /dev/null @@ -1,294 +0,0 @@ -The _sane_ module is an Python interface to the SANE (Scanning is Now -Easy) library, which provides access to various raster scanning -devices such as flatbed scanners and digital cameras. For more -information about SANE, consult the SANE Web site at -http://www.mostang.com/sane/ . Note that this -documentation doesn't duplicate all the information in the SANE -documentation, which you must also consult to get a complete -understanding. - -This module has been originally developed by A.M. Kuchling (amk1@erols.com), -now development has been taken over by Ralph Heinkel (rheinkel-at-email.de). -If you write to me please make sure to have the word 'SANE' or 'sane' in -the subject of your mail, otherwise it might be classified as spam in the -future. - - -The module exports two object types, a bunch of constants, and two -functions. - -get_devices() - Return a list of 4-tuples containing the available scanning - devices. Each tuple contains 4 strings: the device name, suitable for - passing to _open()_; the device's vendor; the model; and the type of - device, such as 'virtual device' or 'video camera'. - - >>> import sane ; sane.get_devices() - [('epson:libusb:001:004', 'Epson', 'GT-8300', 'flatbed scanner')] - -open(devicename) - Open a device, given a string containing its name. SANE - devices have names like 'epson:libusb:001:004'. If the attempt - to open the device fails, a _sane.error_ exception will be raised. If - there are no problems, a SaneDev object will be returned. - As an easy way to open the scanner (if only one is available) just type - >>> sane.open(sane.get_devices()[0][0]) - - -SaneDev objects -=============== - -The basic process of scanning an image consists of getting a SaneDev -object for the device, setting various parameters, starting the scan, -and then reading the image data. Images are composed of one or more -frames; greyscale and one-pass colour scanners return a single frame -containing all the image data, but 3-pass scanners will usually return -3 frames, one for each of the red, green, blue channels. - -Methods: --------- -fileno() - Returns a file descriptor for the scanning device. This - method's existence means that SaneDev objects can be used by the - select module. - -get_parameters() - Return a tuple containing information about the current settings of - the device and the current frame: (format, last_frame, - pixels_per_line, lines, depth, bytes_per_line). - - mode -- 'gray' for greyscale image, 'color' for RGB image, or - one of 'red', 'green', 'blue' if the image is a single - channel of an RGB image (from PIL's point of view, - this is equivalent to 'L'). - last_frame -- A Boolean value, which is true if this is the - last frame of the image, and false otherwise. - pixels_per_line -- Width of the frame. - lines -- Height of the frame. - depth -- Depth of the image, measured in bits. SANE will only - allow using 8, 16, or 24-bit depths. - bytes_per_line -- Bytes required to store a single line of - data, as computed from pixels_per_line and depth. - -start() - Start a scan. This function must be called before the - _snap()_ method can be used. - -cancel() - Cancel a scan already in progress. - -snap(no_cancel=0) - Snap a single frame of data, returning a PIL Image object - containing the data. If no_cancel is false, the Sane library function - sane_cancel is called after the scan. This is reasonable in most cases, - but may cause backends for duplex ADF scanners to drop the backside image, - when snap() is called for the front side image. If no_cancel is true, - cancel() should be called manually, after all scans are finished. - -scan() - This is just a shortcut for s.start(); s.snap() - Returns a PIL image - -multi_scan() - This method returns an iterator. It is intended to be used for - scanning with an automatic document feeder. The next() method of the - iterator tries to start a scan. If this is successful, it returns a - PIL Image object, like scan(); if the document feeder runs out of - paper, it raises StopIteration, thereby signaling that the sequence - is ran out of items. - -arr_snap(multipleOf=1) - same as snap, but the result is a NumArray object. (Not that - num_array must be installed already at compilation time, otherwise - this feature will not be activated). - By default the resulting array has the same number of pixels per - line as specified in self.get_parameters()[2][0] - However sometimes it is necessary to obtain arrays where - the number of pixels per line is e.g. a multiple of 4. This can then - be achieved with the option 'multipleOf=4'. So if the scanner - scanned 34 pixels per line, you will obtain an array with 32 pixels - per line. - Note that this only works with monochrome images (e.g. gray-scales) - -arr_scan(multipleOf=1) - This is just a shortcut for s.start(); s.arr_snap(multipleOf=1) - Returns a NumArray object - -close() - Closes the object. - - -Attributes: ------------ -SaneDev objects have a few fixed attributes which are always -available, and a larger collection of attributes which vary depending -on the device. An Epson 1660 photo scanner has attributes like -'mode', 'depth', etc. -Another (pseudo scanner), the _pnm:0_ device, takes a PNM file and -simulates a scanner using the image data; a SaneDev object -representing the _pnm:0_ device therefore has a _filename_ attribute -which can be changed to specify the filename, _contrast_ and -_brightness_ attributes to modify the returned image, and so forth. - -The values of the scanner options may be an integer, floating-point -value, or string, depending on the nature of the option. - -sane_signature - The tuple for this scandev that is returned by sane.get_devices() - e.g. ('epson:libusb:001:006', 'Epson', 'GT-8300', 'flatbed scanner') - -scanner_model - same as sane_signature[1:3], i.e. ('Epson', 'GT-8300') for the case above. - -optlist - A list containing the all the options supported by this device. - - >>> import sane ; s=sane.open('epson:libusb:001:004') ; s.optlist - ['focus_position', 'color_correction', 'sharpness', ...., 'br_x'] - -A closer look at all options listed in s.optlist can be obtained -through the SaneOption objects. - -SaneOption objects -================== - -SANE's option handling is its most elaborate subsystem, intended to -allow automatically generating dialog boxes and prompts for user -configuration of the scanning device. The SaneOption object can be -used to get a human-readable name and description for an option, the -units to use, and what the legal values are. No information about the -current value of the option is available; for that, read the -corresponding attribute of a SaneDev object. - -This documentation does not explain all the details of SANE's option -handling; consult the SANE documentation for all the details. - -A scandevice option is accessed via __getitem__. For example -s['mode'] returns the option descriptor for the mode-option which -controls whether the scanner works in color, grayscale, or b/w mode. - ->>> s['mode'] -Name: mode -Cur value: Color -Index: 2 -Title: Scan mode -Desc: Selects the scan mode (e.g., lineart, monochrome, or color). -Type: TYPE_STRING -Unit: UNIT_NONE -Constr: ['Binary', 'Gray', 'Color'] -active: yes -settable: yes - -In order to change 'mode' to 'gray', just type: ->>> s.mode = 'gray' - - -With the attributes and methods of sane-option objects it is possible -to access individual option values: - -is_active() - Returns true if the option is active. - -is_settable() - Returns true if the option can be set under software control. - - -Attributes: - -cap - An integer containing various flags about the object's - capabilities; whether it's active, whether it's settable, etc. Also - available as the _capability_ attribute. - -constraint - The constraint placed on the value of this option. If it's - _None_, there are essentially no constraint of the value. It may also - be a list of integers or strings, in which case the value *must* be - one of the possibilities in the list. Numeric values may have a - 3-tuple as the constraint; this 3-tuple contains _(minimum, maximum, - increment)_, and the value must be in the defined range. - -desc - A lengthy description of what the option does; it may be shown - to the user for clarification. - -index - An integer giving the option's index in the option list. - -name - A short name for the option, as it comes from the sane-backend. - -py_name - The option's name, as a legal Python identifier. The name - attribute may contain the '-' character, so it will be converted to - '_' for the py_name attribute. - -size - For a string-valued option, this is the maximum length allowed. - -title - A single-line string that can be used as a title string. - -type - A constant giving the type of this option: will be one of the following - constants found in the SANE module: - TYPE_BOOL - TYPE_INT - TYPE_FIXED - TYPE_STRING - TYPE_BUTTON - TYPE_GROUP - -unit - For numeric-valued options, this is a constant representing - the unit used for this option. It will be one of the following - constants found in the SANE module: - UNIT_NONE - UNIT_PIXEL - UNIT_BIT - UNIT_MM - UNIT_DPI - UNIT_PERCENT - - - -Example us usage: -================= ->>> import sane ->>> print 'SANE version:', sane.init() ->>> print 'Available devices=', sane.get_devices() -SANE version: (16777230, 1, 0, 14) ->>> s = sane.open(sane.get_devices()[0][0]) ->>> print 'Device parameters:', s.get_parameters() -Device parameters: ('L', 1, (424, 585), 1, 53) ->>> print s.resolution -50 - -## In order to scan a color image into a PIL object: ->>> s.mode = 'color' ->>> s.start() ->>> img = s.snap() ->>> img.show() - - -## In order to obtain a 16-bit grayscale image at 100DPI in a numarray object -## with bottom-right coordinates set to (160, 120) [in millimeter] : ->>> s.mode = 'gray' ->>> s.br_x=160. ; s.br_y=120. ->>> s.resolution = 100 ->>> s.depth=16 ->>> s.start() ->>> s.get_parameters()[2] # just check the size -(624, 472) ->>> arr16 = s.arr_snap() ->>> arr16 -array([[63957, 64721, 65067, ..., 65535, 65535, 65535], - [63892, 64342, 64236, ..., 65535, 65535, 65535], - [64286, 64248, 64705, ..., 65535, 65535, 65535], - ..., - [65518, 65249, 65058, ..., 65535, 65535, 65535], - [64435, 65047, 65081, ..., 65535, 65535, 65535], - [65309, 65438, 65535, ..., 65535, 65535, 65535]], type=UInt16) ->>> arr16.shape # inverse order of coordinates, first y, then x! -(472, 624) - diff --git a/Sane/setup.py b/Sane/setup.py deleted file mode 100644 index 3837198ec..000000000 --- a/Sane/setup.py +++ /dev/null @@ -1,24 +0,0 @@ -from distutils.core import setup, Extension - -PIL_BUILD_DIR = '..' -PIL_IMAGING_DIR = PIL_BUILD_DIR+'/libImaging' - -defs = [] -try: - import numarray - defs.append(('WITH_NUMARRAY',None)) -except ImportError: - pass - -sane = Extension('_sane', - include_dirs = [PIL_IMAGING_DIR], - libraries = ['sane'], - library_dirs = [PIL_IMAGING_DIR], - define_macros = defs, - sources = ['_sane.c']) - -setup (name = 'pysane', - version = '2.0', - description = 'This is the pysane package', - py_modules = ['sane'], - ext_modules = [sane]) diff --git a/Scripts/README.rst b/Scripts/README.rst index 1b26ea68c..f86a9a8b6 100644 --- a/Scripts/README.rst +++ b/Scripts/README.rst @@ -63,20 +63,3 @@ explode.py -------------------------------------------------------------------- Split a sequence file into individual frames. - -image2py.py --------------------------------------------------------------------- - -Convert an image to a Python module containing an IMAGE variable. -Note that the module using the module must include JPEG and ZIP -decoders, unless the -u option is used. - -olesummary.py --------------------------------------------------------------------- - -Uses the OleFileIO module to dump the summary information from an OLE -structured storage file. This works with most OLE files, including -Word documents, FlashPix images, etc. - -Note that datetime fields currently show the number of seconds since -January 1st, 1601. diff --git a/Scripts/createfontdatachunk.py b/Scripts/createfontdatachunk.py index 0c860701a..b9514eb6e 100644 --- a/Scripts/createfontdatachunk.py +++ b/Scripts/createfontdatachunk.py @@ -1,3 +1,4 @@ +from __future__ import print_function import base64 import os import sys diff --git a/Scripts/enhancer.py b/Scripts/enhancer.py index fe250c9f8..6393c983f 100644 --- a/Scripts/enhancer.py +++ b/Scripts/enhancer.py @@ -8,9 +8,9 @@ # try: - from tkinter import * + from tkinter import Tk, Toplevel, Frame, Label, Scale, HORIZONTAL except ImportError: - from Tkinter import * + from Tkinter import Tk, Toplevel, Frame, Label, Scale, HORIZONTAL from PIL import Image, ImageTk, ImageEnhance import sys @@ -18,6 +18,7 @@ import sys # # enhancer widget + class Enhance(Frame): def __init__(self, master, image, name, enhancer, lo, hi): Frame.__init__(self, master) @@ -25,7 +26,7 @@ class Enhance(Frame): # set up the image self.tkim = ImageTk.PhotoImage(image.mode, image.size) self.enhancer = enhancer(image) - self.update("1.0") # normalize + self.update("1.0") # normalize # image window Label(self, image=self.tkim).pack() diff --git a/Scripts/explode.py b/Scripts/explode.py index b8680f631..53436100b 100644 --- a/Scripts/explode.py +++ b/Scripts/explode.py @@ -9,11 +9,13 @@ from __future__ import print_function from PIL import Image -import os, sys +import os +import sys -class Interval: - def __init__(self, interval = "0"): +class Interval(object): + + def __init__(self, interval="0"): self.setinterval(interval) diff --git a/Scripts/gifmaker.py b/Scripts/gifmaker.py index 9fa5e71a4..28c6fb25d 100644 --- a/Scripts/gifmaker.py +++ b/Scripts/gifmaker.py @@ -49,20 +49,23 @@ from PIL.GifImagePlugin import getheader, getdata # -------------------------------------------------------------------- # sequence iterator -class image_sequence: + +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 + raise IndexError # end of sequence # -------------------------------------------------------------------- # straightforward delta encoding + def makedelta(fp, sequence): """Convert list of image frames to a GIF animation file""" @@ -72,13 +75,13 @@ def makedelta(fp, sequence): for im in sequence: - # - # FIXME: write graphics control block before each frame + # 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) + getdata(im): + for s in getheader(im)[0] + getdata(im): fp.write(s) else: @@ -91,7 +94,7 @@ def makedelta(fp, sequence): if bbox: # compress difference - for s in getdata(im.crop(bbox), offset = bbox[:2]): + for s in getdata(im.crop(bbox), offset=bbox[:2]): fp.write(s) else: @@ -109,6 +112,7 @@ def makedelta(fp, sequence): # -------------------------------------------------------------------- # main hack + def compress(infile, outfile): # open input image, and force loading of first frame diff --git a/Scripts/painter.py b/Scripts/painter.py index 80c4db6a0..234f06171 100644 --- a/Scripts/painter.py +++ b/Scripts/painter.py @@ -10,9 +10,9 @@ # try: - from tkinter import * + from tkinter import Tk, Canvas, NW except ImportError: - from Tkinter import * + from Tkinter import Tk, Canvas, NW from PIL import Image, ImageTk import sys @@ -20,6 +20,7 @@ import sys # # painter widget + class PaintCanvas(Canvas): def __init__(self, master, image): Canvas.__init__(self, master, width=image.size[0], height=image.size[1]) @@ -33,7 +34,7 @@ class PaintCanvas(Canvas): box = x, y, min(xsize, x+tilesize), min(ysize, y+tilesize) tile = ImageTk.PhotoImage(image.crop(box)) self.create_image(x, y, image=tile, anchor=NW) - self.tile[(x,y)] = box, tile + self.tile[(x, y)] = box, tile self.image = image @@ -59,7 +60,7 @@ class PaintCanvas(Canvas): xy, tile = self.tile[(x, y)] tile.paste(self.image.crop(xy)) except KeyError: - pass # outside the image + pass # outside the image self.update_idletasks() # diff --git a/Scripts/pilconvert.py b/Scripts/pilconvert.py index 934167351..b9ebd52ae 100644 --- a/Scripts/pilconvert.py +++ b/Scripts/pilconvert.py @@ -15,11 +15,13 @@ from __future__ import print_function -import site -import getopt, string, sys +import getopt +import string +import sys from PIL import Image + def usage(): print("PIL Convert 0.5/1998-12-30 -- convert image files") print("Usage: pilconvert [option] infile outfile") @@ -47,10 +49,10 @@ except getopt.error as v: print(v) sys.exit(1) -format = None +output_format = None convert = None -options = { } +options = {} for o, a in opt: @@ -66,7 +68,7 @@ for o, a in opt: sys.exit(1) elif o == "-c": - format = a + output_format = a if o == "-g": convert = "L" @@ -88,8 +90,8 @@ try: if convert and im.mode != convert: im.draft(convert, im.size) im = im.convert(convert) - if format: - im.save(argv[1], format, **options) + if output_format: + im.save(argv[1], output_format, **options) else: im.save(argv[1], **options) except: diff --git a/Scripts/pildriver.py b/Scripts/pildriver.py index e45b05008..32989ccdf 100644 --- a/Scripts/pildriver.py +++ b/Scripts/pildriver.py @@ -52,7 +52,8 @@ from __future__ import print_function from PIL import Image -class PILDriver: + +class PILDriver(object): verbose = 0 @@ -497,10 +498,6 @@ class PILDriver: if __name__ == '__main__': import sys - try: - import readline - except ImportError: - pass # not available on all platforms # If we see command-line arguments, interpret them as a stack state # and execute. Otherwise go interactive. diff --git a/Scripts/pilfile.py b/Scripts/pilfile.py index 1b77b0e78..b954114ac 100644 --- a/Scripts/pilfile.py +++ b/Scripts/pilfile.py @@ -19,8 +19,9 @@ from __future__ import print_function -import site -import getopt, glob, sys +import getopt +import glob +import sys from PIL import Image @@ -59,6 +60,7 @@ for o, a in opt: elif o == "-D": Image.DEBUG += 1 + def globfix(files): # expand wildcards where necessary if sys.platform == "win32": diff --git a/Scripts/pilfont.py b/Scripts/pilfont.py index ec25e7a71..4425c072c 100644 --- a/Scripts/pilfont.py +++ b/Scripts/pilfont.py @@ -14,7 +14,8 @@ from __future__ import print_function VERSION = "0.4" -import glob, sys +import glob +import sys # drivers from PIL import BdfFontFile diff --git a/Scripts/pilprint.py b/Scripts/pilprint.py index be42e0a75..2a5e23061 100644 --- a/Scripts/pilprint.py +++ b/Scripts/pilprint.py @@ -12,24 +12,25 @@ # from __future__ import print_function +import getopt +import os +import sys VERSION = "pilprint 0.3/2003-05-05" from PIL import Image from PIL import PSDraw -letter = ( 1.0*72, 1.0*72, 7.5*72, 10.0*72 ) +letter = (1.0*72, 1.0*72, 7.5*72, 10.0*72) -def description(file, image): - import os - title = os.path.splitext(os.path.split(file)[1])[0] + +def description(filepath, image): + title = os.path.splitext(os.path.split(filepath)[1])[0] format = " (%dx%d " if image.format: format = " (" + image.format + " %dx%d " return title + format % image.size + image.mode + ")" -import getopt, os, sys - if len(sys.argv) == 1: print("PIL Print 0.2a1/96-10-04 -- print image files") print("Usage: pilprint files...") @@ -45,8 +46,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 +printer = None # print to stdout +monochrome = 1 # reduce file size for most common case for o, a in opt: if o == "-d": @@ -64,12 +65,12 @@ for o, a in opt: # printer channel printer = "lpr -P%s" % a -for file in argv: +for filepath in argv: try: - im = Image.open(file) + im = Image.open(filepath) - title = description(file, im) + title = description(filepath, im) if monochrome and im.mode not in ["1", "L"]: im.draft("L", im.size) diff --git a/Scripts/player.py b/Scripts/player.py index 0c90286c5..43877415a 100644 --- a/Scripts/player.py +++ b/Scripts/player.py @@ -56,7 +56,7 @@ class UI(Label): del self.im[0] self.image.paste(im) except IndexError: - return # end of list + return # end of list else: @@ -65,7 +65,7 @@ class UI(Label): im.seek(im.tell() + 1) self.image.paste(im) except EOFError: - return # end of file + return # end of file try: duration = im.info["duration"] diff --git a/Scripts/thresholder.py b/Scripts/thresholder.py index 29d4592d9..bfeebfa9f 100644 --- a/Scripts/thresholder.py +++ b/Scripts/thresholder.py @@ -18,8 +18,9 @@ import sys # # an image viewer + class UI(Frame): - def __init__(self, master, im, value = 128): + def __init__(self, master, im, value=128): Frame.__init__(self, master) self.image = im @@ -31,7 +32,7 @@ class UI(Frame): self.canvas.pack() scale = Scale(self, orient=HORIZONTAL, from_=0, to=255, - resolution=1, command=self.update, length=256) + resolution=1, command=self.update_scale, length=256) scale.set(value) scale.bind("", self.redraw) scale.pack() @@ -40,21 +41,21 @@ class UI(Frame): # be too slow on some platforms) # self.redraw() - def update(self, value): + def update_scale(self, value): self.value = eval(value) self.redraw() - def redraw(self, event = None): + def redraw(self, event=None): # create overlay (note the explicit conversion to mode "1") - im = self.image.point(lambda v,t=self.value: v>=t, "1") + im = self.image.point(lambda v, t=self.value: v >= t, "1") self.overlay = ImageTk.BitmapImage(im, foreground="green") # update canvas self.canvas.delete("overlay") self.canvas.create_image(0, 0, image=self.overlay, anchor=NW, - tags="overlay") + tags="overlay") # -------------------------------------------------------------------- # main diff --git a/Scripts/viewer.py b/Scripts/viewer.py index 86b2526cd..f9bccec4f 100644 --- a/Scripts/viewer.py +++ b/Scripts/viewer.py @@ -7,15 +7,16 @@ from __future__ import print_function try: - from tkinter import * + from tkinter import Tk, Label except ImportError: - from Tkinter import * + from Tkinter import Tk, Label from PIL import Image, ImageTk # # an image viewer + class UI(Label): def __init__(self, master, im): diff --git a/Tests/32bit_segfault_check.py b/Tests/32bit_segfault_check.py index 822d524fd..a601f762e 100644 --- a/Tests/32bit_segfault_check.py +++ b/Tests/32bit_segfault_check.py @@ -6,5 +6,3 @@ import sys if sys.maxsize < 2**32: im = Image.new('L', (999999, 999999), 0) - - diff --git a/Tests/README.rst b/Tests/README.rst index 1b11a45b1..e3f160b09 100644 --- a/Tests/README.rst +++ b/Tests/README.rst @@ -3,13 +3,16 @@ Pillow Tests Test scripts are named ``test_xxx.py`` and use the ``unittest`` module. A base class and helper functions can be found in ``helper.py``. -Depedencies +Dependencies ----------- Install:: pip install coverage nose +If you're using Python 2.6, there's one additional dependency:: + + pip install unittest2 Execution --------- diff --git a/Tests/bench_cffi_access.py b/Tests/bench_cffi_access.py index 8aa322aff..5b64813e4 100644 --- a/Tests/bench_cffi_access.py +++ b/Tests/bench_cffi_access.py @@ -1,4 +1,4 @@ -from helper import * +from helper import unittest, PillowTestCase, hopper # Not running this test by default. No DOS against Travis CI. @@ -39,7 +39,7 @@ def timer(func, label, *args): class BenchCffiAccess(PillowTestCase): def test_direct(self): - im = lena() + im = hopper() im.load() # im = Image.new( "RGB", (2000, 2000), (1, 3, 2)) caccess = im.im.pixel_access(False) @@ -47,7 +47,7 @@ class BenchCffiAccess(PillowTestCase): self.assertEqual(caccess[(0, 0)], access[(0, 0)]) - print ("Size: %sx%s" % im.size) + print("Size: %sx%s" % im.size) timer(iterate_get, 'PyAccess - get', im.size, access) timer(iterate_set, 'PyAccess - set', im.size, access) timer(iterate_get, 'C-api - get', im.size, caccess) diff --git a/Tests/bench_get.py b/Tests/bench_get.py index 8a1331d39..9c51c0258 100644 --- a/Tests/bench_get.py +++ b/Tests/bench_get.py @@ -6,11 +6,11 @@ import timeit def bench(mode): - im = helper.lena(mode) + im = helper.hopper(mode) get = im.im.getpixel xy = 50, 50 # position shouldn't really matter t0 = timeit.default_timer() - for i in range(1000000): + for _ in range(1000000): get(xy) print(mode, timeit.default_timer() - t0, "us") diff --git a/Tests/check_icns_dos.py b/Tests/check_icns_dos.py index ce6338a71..e56709bbb 100644 --- a/Tests/check_icns_dos.py +++ b/Tests/check_icns_dos.py @@ -1,5 +1,5 @@ # Tests potential DOS of IcnsImagePlugin with 0 length block. -# Run from anywhere that PIL is importable. +# Run from anywhere that PIL is importable. from PIL import Image from io import BytesIO @@ -7,4 +7,5 @@ from io import BytesIO if bytes is str: Image.open(BytesIO(bytes('icns\x00\x00\x00\x10hang\x00\x00\x00\x00'))) else: - Image.open(BytesIO(bytes('icns\x00\x00\x00\x10hang\x00\x00\x00\x00', 'latin-1'))) + Image.open(BytesIO(bytes('icns\x00\x00\x00\x10hang\x00\x00\x00\x00', + 'latin-1'))) diff --git a/Tests/check_imaging_leaks.py b/Tests/check_imaging_leaks.py new file mode 100644 index 000000000..c6d99b8d1 --- /dev/null +++ b/Tests/check_imaging_leaks.py @@ -0,0 +1,43 @@ +#!/usr/bin/env python + +from __future__ import division +from helper import unittest, PillowTestCase +import sys +from PIL import Image, ImageFilter + +min_iterations = 100 +max_iterations = 10000 + + +@unittest.skipIf(sys.platform.startswith('win32'), "requires Unix or MacOS") +class TestImagingLeaks(PillowTestCase): + + def _get_mem_usage(self): + from resource import getpagesize, getrusage, RUSAGE_SELF + mem = getrusage(RUSAGE_SELF).ru_maxrss + return mem * getpagesize() / 1024 / 1024 + + def _test_leak(self, min_iterations, max_iterations, fn, *args, **kwargs): + mem_limit = None + for i in range(max_iterations): + fn(*args, **kwargs) + mem = self._get_mem_usage() + if i < min_iterations: + mem_limit = mem + 1 + continue + self.assertLessEqual(mem, mem_limit, + msg='memory usage limit exceeded after %d iterations' + % (i + 1)) + + def test_leak_putdata(self): + im = Image.new('RGB', (25, 25)) + self._test_leak(min_iterations, max_iterations, im.putdata, im.getdata()) + + def test_leak_getlist(self): + im = Image.new('P', (25, 25)) + self._test_leak(min_iterations, max_iterations, + # Pass a new list at each iteration. + lambda: im.point(range(256))) + +if __name__ == '__main__': + unittest.main() diff --git a/Tests/check_j2k_dos.py b/Tests/check_j2k_dos.py index 68f065bbc..9f06888a3 100644 --- a/Tests/check_j2k_dos.py +++ b/Tests/check_j2k_dos.py @@ -1,11 +1,13 @@ # Tests potential DOS of Jpeg2kImagePlugin with 0 length block. -# Run from anywhere that PIL is importable. +# Run from anywhere that PIL is importable. from PIL import Image from io import BytesIO if bytes is str: - Image.open(BytesIO(bytes('\x00\x00\x00\x0cjP\x20\x20\x0d\x0a\x87\x0a\x00\x00\x00\x00hang'))) + Image.open(BytesIO(bytes( + '\x00\x00\x00\x0cjP\x20\x20\x0d\x0a\x87\x0a\x00\x00\x00\x00hang'))) else: - Image.open(BytesIO(bytes('\x00\x00\x00\x0cjP\x20\x20\x0d\x0a\x87\x0a\x00\x00\x00\x00hang', 'latin-1'))) - + Image.open(BytesIO(bytes( + '\x00\x00\x00\x0cjP\x20\x20\x0d\x0a\x87\x0a\x00\x00\x00\x00hang', + 'latin-1'))) diff --git a/Tests/check_j2k_leaks.py b/Tests/check_j2k_leaks.py new file mode 100755 index 000000000..8e9c4ca20 --- /dev/null +++ b/Tests/check_j2k_leaks.py @@ -0,0 +1,42 @@ +from helper import unittest, PillowTestCase +import sys +from PIL import Image +from io import BytesIO + +# Limits for testing the leak +mem_limit = 1024*1048576 +stack_size = 8*1048576 +iterations = int((mem_limit/stack_size)*2) +codecs = dir(Image.core) +test_file = "Tests/images/rgb_trns_ycbc.jp2" + + +@unittest.skipIf(sys.platform.startswith('win32'), "requires Unix or MacOS") +class TestJpegLeaks(PillowTestCase): + def setUp(self): + if "jpeg2k_encoder" not in codecs or "jpeg2k_decoder" not in codecs: + self.skipTest('JPEG 2000 support not available') + + def test_leak_load(self): + from resource import setrlimit, RLIMIT_AS, RLIMIT_STACK + setrlimit(RLIMIT_STACK, (stack_size, stack_size)) + setrlimit(RLIMIT_AS, (mem_limit, mem_limit)) + for _ in range(iterations): + with Image.open(test_file) as im: + im.load() + + def test_leak_save(self): + from resource import setrlimit, RLIMIT_AS, RLIMIT_STACK + setrlimit(RLIMIT_STACK, (stack_size, stack_size)) + setrlimit(RLIMIT_AS, (mem_limit, mem_limit)) + for _ in range(iterations): + with Image.open(test_file) as im: + im.load() + test_output = BytesIO() + im.save(test_output, "JPEG2000") + test_output.seek(0) + test_output.read() + + +if __name__ == '__main__': + unittest.main() diff --git a/Tests/check_jpeg_leaks.py b/Tests/check_jpeg_leaks.py new file mode 100644 index 000000000..4d13978f8 --- /dev/null +++ b/Tests/check_jpeg_leaks.py @@ -0,0 +1,207 @@ +from helper import unittest, PillowTestCase, hopper +from io import BytesIO +import sys + +iterations = 5000 + + +""" +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 + +""" + + +@unittest.skipIf(sys.platform.startswith('win32'), "requires Unix or MacOS") +class TestJpegLeaks(PillowTestCase): + + """ +pre patch: + + MB +31.62^ : + | @:@:@:@#:: + | @:@:@@:@:@:@:@:@#:: + | ::::::::@:@:@@:@:@:@:@:@#:: + | :::::@::::::: ::::@:@:@@:@:@:@:@:@#:: + | @:::::::::::::@: : ::: ::::@:@:@@:@:@:@:@:@#:: + | ::::::@::::@:::::::::::::@: : ::: ::::@:@:@@:@:@:@:@:@#:: + | ::::::: : :@: : @:::::::::::::@: : ::: ::::@:@:@@:@:@:@:@:@#:: + | :::::::: : :@: : @:::::::::::::@: : ::: ::::@:@:@@:@:@:@:@:@#:: + | ::::::::: : :@: : @:::::::::::::@: : ::: ::::@:@:@@:@:@:@:@:@#:: + | ::::::::: : :@: : @:::::::::::::@: : ::: ::::@:@:@@:@:@:@:@:@#:: + | ::::::::::: : :@: : @:::::::::::::@: : ::: ::::@:@:@@:@:@:@:@:@#:: + | : ::::::::: : :@: : @:::::::::::::@: : ::: ::::@:@:@@:@:@:@:@:@#:: + | @: ::::::::: : :@: : @:::::::::::::@: : ::: ::::@:@:@@:@:@:@:@:@#:: + | @@: ::::::::: : :@: : @:::::::::::::@: : ::: ::::@:@:@@:@:@:@:@:@#:: + | :@@: ::::::::: : :@: : @:::::::::::::@: : ::: ::::@:@:@@:@:@:@:@:@#:: + | :@@: ::::::::: : :@: : @:::::::::::::@: : ::: ::::@:@:@@:@:@:@:@:@#:: + | :@:@@: ::::::::: : :@: : @:::::::::::::@: : ::: ::::@:@:@@:@:@:@:@:@#:: + | :@:@@: ::::::::: : :@: : @:::::::::::::@: : ::: ::::@:@:@@:@:@:@:@:@#:: + | :@:@@: ::::::::: : :@: : @:::::::::::::@: : ::: ::::@:@:@@:@:@:@:@:@#:: + 0 +----------------------------------------------------------------------->Gi + 0 8.535 + + +post-patch: + + MB +21.03^ :::@@:::@::::@@:::::::@@::::::::@::::::::::::@:::@:::::::@:::: + | #:::@ :::@::::@ : :: : @ :::::: :@:: ::: :::: @:: @:::::::@:::: + | #:::@ :::@::::@ : :: : @ :::::: :@:: ::: :::: @:: @:::::::@:::: + | :#:::@ :::@::::@ : :: : @ :::::: :@:: ::: :::: @:: @:::::::@:::: + | :#:::@ :::@::::@ : :: : @ :::::: :@:: ::: :::: @:: @:::::::@:::: + | :#:::@ :::@::::@ : :: : @ :::::: :@:: ::: :::: @:: @:::::::@:::: + | :::#:::@ :::@::::@ : :: : @ :::::: :@:: ::: :::: @:: @:::::::@:::: + | : :#:::@ :::@::::@ : :: : @ :::::: :@:: ::: :::: @:: @:::::::@:::: + | : :#:::@ :::@::::@ : :: : @ :::::: :@:: ::: :::: @:: @:::::::@:::: + | @: :#:::@ :::@::::@ : :: : @ :::::: :@:: ::: :::: @:: @:::::::@:::: + | @@: :#:::@ :::@::::@ : :: : @ :::::: :@:: ::: :::: @:: @:::::::@:::: + | @@: :#:::@ :::@::::@ : :: : @ :::::: :@:: ::: :::: @:: @:::::::@:::: + | @@: :#:::@ :::@::::@ : :: : @ :::::: :@:: ::: :::: @:: @:::::::@:::: + | :@@: :#:::@ :::@::::@ : :: : @ :::::: :@:: ::: :::: @:: @:::::::@:::: + | :@@: :#:::@ :::@::::@ : :: : @ :::::: :@:: ::: :::: @:: @:::::::@:::: + | :@:@@: :#:::@ :::@::::@ : :: : @ :::::: :@:: ::: :::: @:: @:::::::@:::: + | :@:@@: :#:::@ :::@::::@ : :: : @ :::::: :@:: ::: :::: @:: @:::::::@:::: + | :@:@@: :#:::@ :::@::::@ : :: : @ :::::: :@:: ::: :::: @:: @:::::::@:::: + | :@:@@: :#:::@ :::@::::@ : :: : @ :::::: :@:: ::: :::: @:: @:::::::@:::: + | :@:@@: :#:::@ :::@::::@ : :: : @ :::::: :@:: ::: :::: @:: @:::::::@:::: + 0 +----------------------------------------------------------------------->Gi + 0 8.421 + +""" + + def test_qtables_leak(self): + im = hopper('RGB') + + standard_l_qtable = [int(s) for s in """ + 16 11 10 16 24 40 51 61 + 12 12 14 19 26 58 60 55 + 14 13 16 24 40 57 69 56 + 14 17 22 29 51 87 80 62 + 18 22 37 56 68 109 103 77 + 24 35 55 64 81 104 113 92 + 49 64 78 87 103 121 120 101 + 72 92 95 98 112 100 103 99 + """.split(None)] + + standard_chrominance_qtable = [int(s) for s in """ + 17 18 24 47 99 99 99 99 + 18 21 26 66 99 99 99 99 + 24 26 56 99 99 99 99 99 + 47 66 99 99 99 99 99 99 + 99 99 99 99 99 99 99 99 + 99 99 99 99 99 99 99 99 + 99 99 99 99 99 99 99 99 + 99 99 99 99 99 99 99 99 + """.split(None)] + + qtables = [standard_l_qtable, + standard_chrominance_qtable] + + for _ in range(iterations): + test_output = BytesIO() + im.save(test_output, "JPEG", qtables=qtables) + + """ +pre patch: + + MB +177.1^ # + | @@@# + | :@@@@@@# + | ::::@@@@@@# + | ::::::::@@@@@@# + | @@::::: ::::@@@@@@# + | @@@@ ::::: ::::@@@@@@# + | @@@@@@@ ::::: ::::@@@@@@# + | @@::@@@@@@@ ::::: ::::@@@@@@# + | @@@@ : @@@@@@@ ::::: ::::@@@@@@# + | @@@@@@ @@ : @@@@@@@ ::::: ::::@@@@@@# + | @@@@ @@ @ @@ : @@@@@@@ ::::: ::::@@@@@@# + | @::@@ @@ @@ @ @@ : @@@@@@@ ::::: ::::@@@@@@# + | ::::@: @@ @@ @@ @ @@ : @@@@@@@ ::::: ::::@@@@@@# + | :@@: : @: @@ @@ @@ @ @@ : @@@@@@@ ::::: ::::@@@@@@# + | ::@@::@@: : @: @@ @@ @@ @ @@ : @@@@@@@ ::::: ::::@@@@@@# + | @@::: @ ::@@: : @: @@ @@ @@ @ @@ : @@@@@@@ ::::: ::::@@@@@@# + | @::@ : : @ ::@@: : @: @@ @@ @@ @ @@ : @@@@@@@ ::::: ::::@@@@@@# + | :::@: @ : : @ ::@@: : @: @@ @@ @@ @ @@ : @@@@@@@ ::::: ::::@@@@@@# + | @@@:: @: @ : : @ ::@@: : @: @@ @@ @@ @ @@ : @@@@@@@ ::::: ::::@@@@@@# + 0 +----------------------------------------------------------------------->Gi + 0 11.37 + + +post patch: + + MB +21.06^ ::::::::::::::::::::::@::::@::::@::::@::::@::::@:::::::::@:::::: + | ##::: ::::: : ::::::::::@::::@::::@::::@::::@::::@:::::::::@:::::: + | # ::: ::::: : ::::::::::@::::@::::@::::@::::@::::@:::::::::@:::::: + | # ::: ::::: : ::::::::::@::::@::::@::::@::::@::::@:::::::::@:::::: + | # ::: ::::: : ::::::::::@::::@::::@::::@::::@::::@:::::::::@:::::: + | @# ::: ::::: : ::::::::::@::::@::::@::::@::::@::::@:::::::::@:::::: + | @# ::: ::::: : ::::::::::@::::@::::@::::@::::@::::@:::::::::@:::::: + | @# ::: ::::: : ::::::::::@::::@::::@::::@::::@::::@:::::::::@:::::: + | @# ::: ::::: : ::::::::::@::::@::::@::::@::::@::::@:::::::::@:::::: + | @@# ::: ::::: : ::::::::::@::::@::::@::::@::::@::::@:::::::::@:::::: + | @@@# ::: ::::: : ::::::::::@::::@::::@::::@::::@::::@:::::::::@:::::: + | @@@# ::: ::::: : ::::::::::@::::@::::@::::@::::@::::@:::::::::@:::::: + | @@@# ::: ::::: : ::::::::::@::::@::::@::::@::::@::::@:::::::::@:::::: + | @@@# ::: ::::: : ::::::::::@::::@::::@::::@::::@::::@:::::::::@:::::: + | @@@# ::: ::::: : ::::::::::@::::@::::@::::@::::@::::@:::::::::@:::::: + | @@@@@# ::: ::::: : ::::::::::@::::@::::@::::@::::@::::@:::::::::@:::::: + | @ @@@# ::: ::::: : ::::::::::@::::@::::@::::@::::@::::@:::::::::@:::::: + | @ @@@# ::: ::::: : ::::::::::@::::@::::@::::@::::@::::@:::::::::@:::::: + | @ @@@# ::: ::::: : ::::::::::@::::@::::@::::@::::@::::@:::::::::@:::::: + | @ @@@# ::: ::::: : ::::::::::@::::@::::@::::@::::@::::@:::::::::@:::::: + 0 +----------------------------------------------------------------------->Gi + 0 11.33 + +""" + + def test_exif_leak(self): + im = hopper('RGB') + exif = b'12345678'*4096 + + for _ in range(iterations): + test_output = BytesIO() + im.save(test_output, "JPEG", exif=exif) + + def test_base_save(self): + """ +base case: + MB +20.99^ ::::: :::::::::::::::::::::::::::::::::::::::::::@::: + | ##: : ::::::@::::::: :::: :::: : : : : : : :::::::::::: :::@::: + | # : : :: :: @:: :::: :::: :::: : : : : : : :::::::::::: :::@::: + | # : : :: :: @:: :::: :::: :::: : : : : : : :::::::::::: :::@::: + | # : : :: :: @:: :::: :::: :::: : : : : : : :::::::::::: :::@::: + | @@# : : :: :: @:: :::: :::: :::: : : : : : : :::::::::::: :::@::: + | @ # : : :: :: @:: :::: :::: :::: : : : : : : :::::::::::: :::@::: + | @ # : : :: :: @:: :::: :::: :::: : : : : : : :::::::::::: :::@::: + | @@@ # : : :: :: @:: :::: :::: :::: : : : : : : :::::::::::: :::@::: + | @ @ # : : :: :: @:: :::: :::: :::: : : : : : : :::::::::::: :::@::: + | @@ @ # : : :: :: @:: :::: :::: :::: : : : : : : :::::::::::: :::@::: + | @@ @ # : : :: :: @:: :::: :::: :::: : : : : : : :::::::::::: :::@::: + | @@ @ # : : :: :: @:: :::: :::: :::: : : : : : : :::::::::::: :::@::: + | @@ @ # : : :: :: @:: :::: :::: :::: : : : : : : :::::::::::: :::@::: + | @@ @ # : : :: :: @:: :::: :::: :::: : : : : : : :::::::::::: :::@::: + | :@@@@ @ # : : :: :: @:: :::: :::: :::: : : : : : : :::::::::::: :::@::: + | :@ @@ @ # : : :: :: @:: :::: :::: :::: : : : : : : :::::::::::: :::@::: + | :@ @@ @ # : : :: :: @:: :::: :::: :::: : : : : : : :::::::::::: :::@::: + | :@ @@ @ # : : :: :: @:: :::: :::: :::: : : : : : : :::::::::::: :::@::: + | :@ @@ @ # : : :: :: @:: :::: :::: :::: : : : : : : :::::::::::: :::@::: + 0 +----------------------------------------------------------------------->Gi + 0 7.882 +""" + im = hopper('RGB') + + for _ in range(iterations): + test_output = BytesIO() + im.save(test_output, "JPEG") + + +if __name__ == '__main__': + unittest.main() diff --git a/Tests/check_png_dos.py b/Tests/check_png_dos.py new file mode 100644 index 000000000..762c9607a --- /dev/null +++ b/Tests/check_png_dos.py @@ -0,0 +1,48 @@ +from helper import unittest, PillowTestCase +from PIL import Image, PngImagePlugin +from io import BytesIO +import zlib + +TEST_FILE = "Tests/images/png_decompression_dos.png" + + +class TestPngDos(PillowTestCase): + def test_dos_text(self): + + try: + im = Image.open(TEST_FILE) + im.load() + except ValueError as msg: + self.assertTrue(msg, "Decompressed Data Too Large") + return + + for s in im.text.values(): + self.assertLess(len(s), 1024*1024, "Text chunk larger than 1M") + + def test_dos_total_memory(self): + im = Image.new('L', (1, 1)) + compressed_data = zlib.compress('a'*1024*1023) + + info = PngImagePlugin.PngInfo() + + for x in range(64): + info.add_text('t%s' % x, compressed_data, 1) + info.add_itxt('i%s' % x, compressed_data, zip=True) + + b = BytesIO() + im.save(b, 'PNG', pnginfo=info) + b.seek(0) + + try: + im2 = Image.open(b) + except ValueError as msg: + self.assertIn("Too much memory", msg) + return + + 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") + +if __name__ == '__main__': + unittest.main() diff --git a/Tests/check_webp_leaks.py b/Tests/check_webp_leaks.py new file mode 100644 index 000000000..0f54f382d --- /dev/null +++ b/Tests/check_webp_leaks.py @@ -0,0 +1,38 @@ +from __future__ import division +from helper import unittest, PillowTestCase +import sys +from PIL import Image +from io import BytesIO + +# Limits for testing the leak +mem_limit = 16 # max increase in MB +iterations = 5000 +test_file = "Tests/images/hopper.webp" + + +@unittest.skipIf(sys.platform.startswith('win32'), "requires Unix or MacOS") +class TestWebPLeaks(PillowTestCase): + + def setUp(self): + try: + from PIL import _webp + except ImportError: + self.skipTest('WebP support not installed') + + def _get_mem_usage(self): + from resource import getpagesize, getrusage, RUSAGE_SELF + mem = getrusage(RUSAGE_SELF).ru_maxrss + return mem * getpagesize() / 1024 / 1024 + + def test_leak_load(self): + with open(test_file, 'rb') as f: + im_data = f.read() + start_mem = self._get_mem_usage() + for _ in range(iterations): + with Image.open(BytesIO(im_data)) as im: + im.load() + mem = (self._get_mem_usage() - start_mem) + self.assertLess(mem, mem_limit, msg='memory usage limit exceeded') + +if __name__ == '__main__': + unittest.main() diff --git a/Tests/crash_ttf_memory_error.py b/Tests/crash_ttf_memory_error.py index 881fc03a6..8af3dbcec 100644 --- a/Tests/crash_ttf_memory_error.py +++ b/Tests/crash_ttf_memory_error.py @@ -9,6 +9,6 @@ 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) +d.text((0, 0), s, font=f, fill=0) i.show() diff --git a/Tests/fonts/DejaVuSans-bitmap.ttf b/Tests/fonts/DejaVuSans-bitmap.ttf new file mode 100644 index 000000000..702cce37d Binary files /dev/null and b/Tests/fonts/DejaVuSans-bitmap.ttf differ diff --git a/Tests/fonts/DejaVuSans.ttf b/Tests/fonts/DejaVuSans.ttf new file mode 100644 index 000000000..683bbbd41 Binary files /dev/null and b/Tests/fonts/DejaVuSans.ttf differ diff --git a/Tests/helper.py b/Tests/helper.py index 3f7913b11..1255b1819 100644 --- a/Tests/helper.py +++ b/Tests/helper.py @@ -100,7 +100,8 @@ class PillowTestCase(unittest.TestCase): ave_diff = float(diff)/(a.size[0]*a.size[1]) self.assertGreaterEqual( epsilon, ave_diff, - (msg or '') + " average pixel value difference %.4f > epsilon %.4f" % ( + (msg or '') + + " average pixel value difference %.4f > epsilon %.4f" % ( ave_diff, epsilon)) def assert_warning(self, warn_class, func): @@ -129,7 +130,7 @@ class PillowTestCase(unittest.TestCase): # Skip if platform/travis matches, and # PILLOW_RUN_KNOWN_BAD is not true in the environment. if bool(os.environ.get('PILLOW_RUN_KNOWN_BAD', False)): - print (os.environ.get('PILLOW_RUN_KNOWN_BAD', False)) + print(os.environ.get('PILLOW_RUN_KNOWN_BAD', False)) return skip = True @@ -138,7 +139,8 @@ class PillowTestCase(unittest.TestCase): if travis is not None: skip = skip and (travis == bool(os.environ.get('TRAVIS', False))) if interpreter is not None: - skip = skip and (interpreter == 'pypy' and hasattr(sys, 'pypy_version_info')) + skip = skip and (interpreter == 'pypy' and + hasattr(sys, 'pypy_version_info')) if skip: self.skipTest(msg or "Known Bad Test") @@ -155,7 +157,7 @@ class PillowTestCase(unittest.TestCase): raise IOError() outfile = self.tempfile("temp.png") - if command_succeeds(['convert', f, outfile]): + if command_succeeds([IMCONVERT, f, outfile]): from PIL import Image return Image.open(outfile) raise IOError() @@ -163,7 +165,6 @@ class PillowTestCase(unittest.TestCase): # helpers -import sys py3 = (sys.version_info >= (3, 0)) @@ -173,31 +174,33 @@ def fromstring(data): return Image.open(BytesIO(data)) -def tostring(im, format, **options): +def tostring(im, string_format, **options): from io import BytesIO out = BytesIO() - im.save(out, format, **options) + im.save(out, string_format, **options) return out.getvalue() -def lena(mode="RGB", cache={}): +def hopper(mode=None, cache={}): from PIL import Image - im = None - # FIXME: Implement caching to reduce reading from disk but so an original - # copy is returned each time and the cached image isn't modified by tests + if mode is None: + # Always return fresh not-yet-loaded version of image. + # Operations on not-yet-loaded images is separate class of errors + # what we should catch. + return Image.open("Tests/images/hopper.ppm") + # Use caching to reduce reading from disk but so an original copy is + # returned each time and the cached image isn't modified by tests # (for fast, isolated, repeatable tests). - # im = cache.get(mode) + im = cache.get(mode) if im is None: - if mode == "RGB": - im = Image.open("Tests/images/lena.ppm") - elif mode == "F": - im = lena("L").convert(mode) + if mode == "F": + im = hopper("L").convert(mode) elif mode[:4] == "I;16": - im = lena("I").convert(mode) + im = hopper("I").convert(mode) else: - im = lena("RGB").convert(mode) - # cache[mode] = im - return im + im = hopper().convert(mode) + cache[mode] = im + return im.copy() def command_succeeds(cmd): @@ -205,7 +208,6 @@ def command_succeeds(cmd): Runs the command, which must be a list of strings. Returns True if the command succeeds, or False if an OSError was raised by subprocess.Popen. """ - import os import subprocess with open(os.devnull, 'w') as f: try: @@ -229,6 +231,14 @@ def netpbm_available(): def imagemagick_available(): - return command_succeeds(['convert', '-version']) + return IMCONVERT and command_succeeds([IMCONVERT, '-version']) + + +if sys.platform == 'win32': + IMCONVERT = os.environ.get('MAGICK_HOME', '') + if IMCONVERT: + IMCONVERT = os.path.join(IMCONVERT, 'convert.exe') +else: + IMCONVERT = 'convert' # End of file diff --git a/Tests/icc/CMY.icm b/Tests/icc/CMY.icm deleted file mode 100644 index acc71ddd9..000000000 Binary files a/Tests/icc/CMY.icm and /dev/null differ diff --git a/Tests/icc/LICENSE.txt b/Tests/icc/LICENSE.txt new file mode 100644 index 000000000..e6ec6a69f --- /dev/null +++ b/Tests/icc/LICENSE.txt @@ -0,0 +1,25 @@ +from http://www.color.org/srgbprofiles.xalter + +Terms of use + +To anyone who acknowledges that the file "sRGB_v4_ICC_preference.icc" +is provided "AS IS" WITH NO EXPRESS OR IMPLIED WARRANTY, permission +to use, copy and distribute this file for any purpose is hereby +granted without fee, provided that the file is not changed including +the ICC copyright notice tag, and that the name of ICC shall not be +used in advertising or publicity pertaining to distribution of the +software without specific, written prior permission. ICC makes no +representations about the suitability of this software for any +purpose. + + +To anyone who acknowledges that the file +"sRGB_IEC61966-2-1_blackscaled.icc" is provided "AS IS" WITH NO +EXPRESS OR IMPLIED WARRANTY, permission to use, copy and distribute +these file for any purpose is hereby granted without fee, provided +that the file is not changed including the ICC copyright notice tag, +and that the name of ICC shall not be used in advertising or publicity +pertaining to distribution of the software without specific, written +prior permission. ICC makes no representations about the suitability +of this software for any purpose. + diff --git a/Tests/icc/YCC709.icm b/Tests/icc/YCC709.icm deleted file mode 100644 index d9f5a5a5d..000000000 Binary files a/Tests/icc/YCC709.icm and /dev/null differ diff --git a/Tests/icc/sRGB.icm b/Tests/icc/sRGB_IEC61966-2-1_black_scaled.icc similarity index 63% rename from Tests/icc/sRGB.icm rename to Tests/icc/sRGB_IEC61966-2-1_black_scaled.icc index 7f9d18d09..71e338302 100644 Binary files a/Tests/icc/sRGB.icm and b/Tests/icc/sRGB_IEC61966-2-1_black_scaled.icc differ diff --git a/Tests/icc/sRGB_v4_ICC_preference.icc b/Tests/icc/sRGB_v4_ICC_preference.icc new file mode 100644 index 000000000..cfbd03e1f Binary files /dev/null and b/Tests/icc/sRGB_v4_ICC_preference.icc differ diff --git a/Tests/images/color_snakes.png b/Tests/images/color_snakes.png new file mode 100644 index 000000000..bf3a35196 Binary files /dev/null and b/Tests/images/color_snakes.png differ diff --git a/Tests/images/effect_mandelbrot.png b/Tests/images/effect_mandelbrot.png new file mode 100644 index 000000000..d8167e721 Binary files /dev/null and b/Tests/images/effect_mandelbrot.png differ diff --git a/Tests/images/effect_spread.png b/Tests/images/effect_spread.png new file mode 100644 index 000000000..01016355a Binary files /dev/null and b/Tests/images/effect_spread.png differ diff --git a/Tests/images/gimp_gradient.ggr b/Tests/images/gimp_gradient.ggr new file mode 100644 index 000000000..4644e5537 --- /dev/null +++ b/Tests/images/gimp_gradient.ggr @@ -0,0 +1,6 @@ +GIMP Gradient +4 +0.000000 0.351923 0.534893 1.000000 1.000000 1.000000 0.910000 0.730303 0.510606 1.000000 0.480000 1 0 +0.501504 0.611002 0.637730 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 1.000000 1 0 +0.931891 0.951264 1.000000 0.531255 0.316078 1.031173 1.000000 0.000000 0.000000 0.000000 1.000000 0 0 +0.810551 0.881217 0.921883 0.717576 0.441331 0.081217 1.000000 0.751576 0.410331 0.081217 1.000000 0 0 \ No newline at end of file diff --git a/Tests/images/gimp_gradient_with_name.ggr b/Tests/images/gimp_gradient_with_name.ggr new file mode 100644 index 000000000..ab1c1d734 --- /dev/null +++ b/Tests/images/gimp_gradient_with_name.ggr @@ -0,0 +1,7 @@ +GIMP Gradient +Name: A GIMP 1.3 gradient file +4 +0.000000 0.351923 0.534893 1.000000 1.000000 1.000000 0.910000 0.730303 0.510606 1.000000 0.480000 1 0 +0.501504 0.611002 0.637730 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 1.000000 1 0 +0.931891 0.951264 1.000000 0.531255 0.316078 1.031173 1.000000 0.000000 0.000000 0.000000 1.000000 0 0 +0.810551 0.881217 0.921883 0.717576 0.441331 0.081217 1.000000 0.751576 0.410331 0.081217 1.000000 0 0 \ No newline at end of file diff --git a/Tests/images/hopper.Lab.tif b/Tests/images/hopper.Lab.tif new file mode 100644 index 000000000..299386697 Binary files /dev/null and b/Tests/images/hopper.Lab.tif differ diff --git a/Tests/images/hopper.bw b/Tests/images/hopper.bw new file mode 100644 index 000000000..c9dabf64a Binary files /dev/null and b/Tests/images/hopper.bw differ diff --git a/Tests/images/hopper.dcx b/Tests/images/hopper.dcx new file mode 100644 index 000000000..ba6bd062e Binary files /dev/null and b/Tests/images/hopper.dcx differ diff --git a/Tests/images/hopper.fli b/Tests/images/hopper.fli new file mode 100644 index 000000000..0de1d3394 Binary files /dev/null and b/Tests/images/hopper.fli differ diff --git a/Tests/images/hopper.gif b/Tests/images/hopper.gif new file mode 100644 index 000000000..2e7f5ade8 Binary files /dev/null and b/Tests/images/hopper.gif differ diff --git a/Tests/images/hopper.ico b/Tests/images/hopper.ico new file mode 100644 index 000000000..cd301823a Binary files /dev/null and b/Tests/images/hopper.ico differ diff --git a/Tests/images/hopper.im b/Tests/images/hopper.im new file mode 100644 index 000000000..ceeb01049 Binary files /dev/null and b/Tests/images/hopper.im differ diff --git a/Tests/images/hopper.jpg b/Tests/images/hopper.jpg new file mode 100644 index 000000000..82ab5b967 Binary files /dev/null and b/Tests/images/hopper.jpg differ diff --git a/Tests/images/hopper.msp b/Tests/images/hopper.msp new file mode 100644 index 000000000..91d9a147f Binary files /dev/null and b/Tests/images/hopper.msp differ diff --git a/Tests/images/hopper.png b/Tests/images/hopper.png new file mode 100644 index 000000000..60ac671a2 Binary files /dev/null and b/Tests/images/hopper.png differ diff --git a/Tests/images/hopper.ppm b/Tests/images/hopper.ppm new file mode 100644 index 000000000..346e42392 Binary files /dev/null and b/Tests/images/hopper.ppm differ diff --git a/Tests/images/hopper.psd b/Tests/images/hopper.psd new file mode 100644 index 000000000..e8b184cf5 Binary files /dev/null and b/Tests/images/hopper.psd differ diff --git a/Tests/images/hopper.ras b/Tests/images/hopper.ras new file mode 100644 index 000000000..70f3c80d0 Binary files /dev/null and b/Tests/images/hopper.ras differ diff --git a/Tests/images/hopper.rgb b/Tests/images/hopper.rgb new file mode 100644 index 000000000..a72fc5b15 Binary files /dev/null and b/Tests/images/hopper.rgb differ diff --git a/Tests/images/hopper.spider b/Tests/images/hopper.spider new file mode 100644 index 000000000..70e31b07c Binary files /dev/null and b/Tests/images/hopper.spider differ diff --git a/Tests/images/hopper.tar b/Tests/images/hopper.tar new file mode 100644 index 000000000..593429c9a Binary files /dev/null and b/Tests/images/hopper.tar differ diff --git a/Tests/images/hopper.tif b/Tests/images/hopper.tif new file mode 100644 index 000000000..cb9ef0490 Binary files /dev/null and b/Tests/images/hopper.tif differ diff --git a/Tests/images/hopper.webp b/Tests/images/hopper.webp new file mode 100644 index 000000000..1169ab02f Binary files /dev/null and b/Tests/images/hopper.webp differ diff --git a/Tests/images/hopper.xpm b/Tests/images/hopper.xpm new file mode 100644 index 000000000..27f51a4fe --- /dev/null +++ b/Tests/images/hopper.xpm @@ -0,0 +1,178 @@ +/* XPM */ +static char *hopper[] = { +/* columns rows colors chars-per-pixel */ +"128 128 44 1 ", +" c #110E13", +". c #2A1110", +"X c #13122C", +"o c #25192A", +"O c #312831", +"+ c #302219", +"@ c #5F241A", +"# c #6F4832", +"$ c #17174A", +"% c #26264F", +"& c #65595D", +"* c #4B3C54", +"= c #8E1F1A", +"- c #914E33", +"; c #A75335", +": c #AC6736", +"> c #B26F4E", +", c #9B6657", +"< c #CA7652", +"1 c #CC7866", +"2 c #C25135", +"3 c #B0896D", +"4 c #D28859", +"5 c #D28E6D", +"6 c #E79371", +"7 c #E5A36E", +"8 c #D58932", +"9 c #3E65AE", +"0 c #4970B5", +"q c #606D95", +"w c #557CC1", +"e c #91778A", +"r c #6A86B9", +"t c #5C83C3", +"y c #6D8FCA", +"u c #7799E3", +"i c #A89A9D", +"p c #CFAA90", +"a c #F4B08F", +"s c #D6A799", +"d c #ECCCAB", +"f c #F9F4F4", +"g c #E3D8D5", +"h c #AAAECA", +/* pixels */ +"$XX$$XXX$%$$$$$$$$$$$$$$$$$$$egip3dr %% @p# Oqgffgfgfgo%q999q9999999999999999999099999099099990999999909909990w999000wwww00w00w0", +"$$X$X$XX$$$$$$$$9$$$$XX$$$$$$ws&7+O&%$Xo@3#XXX&ihffggg%%9q*9*%q99qq9q99999999999999090900909909909099999999999999999000wwwwww0ww", +"X$X$$XXX$$$$$$$$$$$$$$$$$$$$$eq@i#+$$$%X#pOX$%$%*dfffgiX%%OX%XO*%**990909999999009090990900999999099990999w999990990000wwwwwwwww", +"$XX$$$XX$$$$$$$$$$$$XX$$$$$$$*O*7#XX%$%X@p*XX$%Xohfi3hOXOoX*r3*OOO%O99099999q0q990990009090909000999090999999099909999ww0w0000w0", +"$XX$$XXX$$$$$$$$$9$$$$$$$$$$o$.&3++X$$$o#p+X$$%$OhgOX oo+ii*if&+ri&OO%q0990099009090090990909009990999099w999990999090w0000w0ww0", +"X$X$$XXX$$$$$$$$$$$$$$$$$$$$$oX#p#OO%$$o#7O%X$$XOg&O o.&fffgifirfffiOO*&qq990990000000009909099000990990999990999w90099000000000", +"XX$$$XXX$$$$$$$$$$$$$$$$$$$$$oo*3+o$$%$.#7+X$%XOOhii+X,fgifffffffddfgOOrhi0qq000090000090099990909909009w9w9909990999099900000w0", +"X$X$$$XX$$$$$%$$$$$$X$X$$$$$X$X#7@X$$$$o:3+XX$OqgffrXOe&&#*&iggi&&3&*&OOfffgrq900009000099099090000909009999990999900900000000w0", +"X$q$XXXX$$$X%$$$$$$$X$$$$$$$$$X*p+O$%$$o-3oOX%gffff&o++3pgg3fddd3fdg&+OOhffffgt0099090090009090990909990999990999000099090090000", +"X$g&$XXX%$X$%$$$$$$$$$X$$$$$$$o*p+X%$%$X,5+ *gfffff& ++&pddffffffgdd#OOOiffffffhq9qt009w990090900999w9w9900999w90999909009900000", +"X$ggO %$$$X$%$$$$$$$$$$$$$$$Xo*3#$$%%$X,3+&ffffffg#+.+Odddgggggggpd*+OOifffffffgtqt09t909090990090909099990099w0990090909090000", +"X%hfr&h&$$$$%q$X$$$$XXX$$$$$Xoo*p@o$$$$X:3OffffgppdO. ++,ddfffffffd3+ ++&ffffffffgq900909090900999099090w9w990990009009090009900", +"Xohfffh&X$o$$h&$$$$$$$X$$$$$Xo$*7# %%$$ 35dffg73&#++++ +pd3gfffffg3d,++++3ddffffffhq90009099990999909090990909w99990099900900000", +"oegffgiO$$$$$gh$$%&qXXXX$%XX$X.*3# X%X$+#pgfd3&+. + ++3iddgfffddpd&+O++++#pgfffffrq9900900q99090909990099009990009990999090000", +"hffffg&X$%$X$gg*&hg*XXXX%i%$$Xo*5#XX$$$o,dfg&+.. .#&++ +&ddd3gfgdddd#++++&+++#3dfgghq009099090000909090990999090909000099000000w", +"hgffffp*X$X%Ogggfgh$XXX$%g*XXooO5#o$%$$+&df3...&#++3 O#+#pd7di&,ppdd&+++#3+&&..#iffgqq009009009909900990090009990009999009990000", +"o%qffgh&o%o*iffgfg%%X$XX*gi OqO*p#+$%$XO&h3 . +pdpp3pdgdd3#+++++++#ppggpp3ddp+##+ifghqrq09090090090090900909999009009w900009w0w9", +"Xo*fh**%$%igfgfgg&%$$X$X&ggih3o+p# X%X$XX .+33+pdppdddfp,#+O&dfggdddd3pdddd+++dh&%%&q0090909009909090090000900w000w99w0900ww", +"X%&fq%oX$%&hgfffgi%oXXX &fgshOoO3#+X%%XX+ . +#&++7dp3&#O&O&#&33dd3#3&.+#ppiiO#+ X%9909000909099090090900009w9w009ww9ww9w00", +"X$%g*o$XX%Xo*gffffq$X%&egfhp&oXO7# XXX + #++.+++ +..O+ ++O++#ppp+++&3++ o XXq9000900009000090009000009w909w09w00w00w", +"$$*i$$oX$XoX%hfeegg%$%&idggh+X$O4#+XOX + . + ..o + + ++ +&3gg#. X999909000000000000009000000000w09w0www0w", +"$$%%$$$X$$o$$hh*$$q&$o o3ggd,ooo3#+ ++ +++ ++*++ X%0q9q900000ww000000000000ww99w00w9w99ww0", +"$$$$%%X$$$X$%g0$$%%%oX .&fshg*Xo3: X + + X*0000000000w000000000w00000ww0www90wwww0", +"$%$$%$$XX$$X%h$%$%$$XXX qg*XiqXo3:+ %0q0000000w00000w0ww000w0ww990009w0w99ww", +"$$$%%%$$$XX$$%%$%$$$XXXXihXXX%X.,: %t000t90w0w0000090w00w00009w000wwww9www9", +"$$$$$%X$X$$$$%$$$$$$XXXXq*XXXXXo3: . %0q0000t0w0ww00ww00ww00ww0w9w0w0w99ww9ww", +"$$$$$$$XX$X$$$$$$$$$$XXX*$$XXXX :> %q0t0t00w0w0w0w00ww0w0w0w00w0w0www90w09w", +"$$$$$$XX$$XX$$$$%$$$$XXX$$$$XXX.:3. . . .... .. . ....... X%qw000t00www0wwwwwww0ww0www0w00w00w09w9w", +"$$$$%%$$$XX$$$$$$$$$XXXX$$$$XXX -3 . ....+@@@@@@.@@....@@@@#####*@.. %ttt9ttt0t0tw0www0ww0ww00w9ww0w09w9www0w", +"$$$%$%$$X$$$$$$$%%9$XXXX$$$$XXX :3 ...#->1>>1,>>,,--;,--,,,1531>13>#@. X09q9r00t0t0wwwww0w0wwwwwwww0wwwwww0w00w", +"$$$$$$XXX$$$$$$$$$$$XX X$$$$$XX.:: .#11ss65assa56a511155557a775555p5,,. Xqttr0trt0wtwwwwww0ww0wwwwww0wwwww0ww0ww", +"$$$$$$$X$$$$9$9$$$$$XXXXX$$$XX$ >;. ...#,pps55paaaaasadasaaaaaaaaa777a77p53,*... Xt0tq9t0ttwww0wwwwwwww0wwww0wwww0wwwwwww", +"*o$%$%$XX$$$$$$$%$$$$XXXX$$$$XXX,> +#*-,5pp7apasaadddaaaaddadaaadddaaa6pp733,##. Xqrtttttt0t0rt0tt000t0t0t0t00t0t0ttt0ttt", +"go%$$*%$XXX$$$$$$$$$$X XX$$$$XX :> .&,#,5p77p7aaaaadaaadaddaaadaaaasaapp7775534#@ Xr90tq0ttt0tt0ttt0ttttt00tttt0tt0t0tttt0", +"fio%whXXX%$$$$$$$$$$$XXX$$$$X$X.,8. o###-155pppadaasasaaadaaaaaaaaa51>3355455535>>#+ %t0r0r0rttt00tt0ttttttt0tttttt0ttttttttt", +"gg&gf&X *qo$$$$$$$$$$X X$$$$$XX 3: OO##,135565431>113sadaaaddsss611>1>>55s5355333,*+ %rr0r0ttt0tttttrtt0ttqttt0ttttttttt0tttt", +"gffghOXX*g*$$$%$$$$$XXXX$$$$XXX -> +###,>3575p5<><355saaaaaaaap615>;;-;>5a7p335333#+ %rrtttt0t0trtttttttttttttttttttttttttttt", +"fgfg&oo &giO$qi%X$$$XX XX$$$XX .::+ +#,,,3355p55>>;;;,>16s777aaas61>;=--@#@@@@##&##&&+X*rrtttttttt0tttttttttttttttttttttttttttt", +"gggg&o &gg,sfe$X$$$$X XX$$$XXX :: +&#-,,35333>>,-,-,>>15apaadap1>>;@@@#;-##@+,&@##&O+&rttttt0ttttttt0tttttttttttttttttttttttt", +"gffgg*oX&gfggh%X$X$$XX X%%XXX 3:+ ++3:-####@@.@..@++@@->6aaaaaa74>=@,3,11335d&+@+O*&##qrrttttytttttttrtttttttttttttttttttttttt", +"sggggho*dgggg&O$$$$$ *+Oii$X X .::+++&,-##+@+@@:ss1,1i1--=:4a77764::a1,@@@@=,,4d#+##*&,-eqyrwyrwttttttttttttttytytttytytttyttttt", +"id**qi&Ogggdd+X$$$$$o*&eh*X XXX.::.O#,>-#@...+,,,-@@@@-5><<<76748:2<>-@#.@@@@=-@##@@+#,isiryywyyrttrttttyttytytttttytttttttttytu", +",hO%X$Xo&gggg&O$$$$XoO,e&OXXXXX.3:.@33;#+@@@.-,>-@.@..@=::;<4<4<:28<2@#@.@@@@==-,-##-,1,eheywyywyttttyytytyttttttyttttttttytyttt", +"i&o$$X Ogggfg%X$$$$oO,&+ XX X :3+@3,,;##@#@##,@@@..@-;==;426a76<<<;>;--#==;;::4>4>:;:==1ryewywttyrtytttytyytyyttyttuytyttyttyt", +"**%$$$XX+hiO&h&%$$$XX*,,& XXX X ::+#1-#-;,,>,#-==#=#;-;<4444;6ad7<661<<11>2;2<<<445<>:<<@,ryytytyytyyttyyttytyttytyttyttyttyuytt", +"%X$$$$X Og*o%$*X$$$$X*&&i* XXXX :3.#,=;>,-35:-#===:4s51446a6;6aaa6661<66aaa6<664445>:15<-,ewyytyytytyyytyyytytytytyyytyyttuttttu", +"X$$$$XXXOi$$$$$X$$$XXX.o&qXo X .>>.#-@>;-:<6:<;==2<66a6<<6a<-:44:<82<<11<<=54;;<<<<1<;2<<<1aada6:7adadd74aaadaa6446764444;=>s,eitryyyyyyyyyyyyttyyyyyyyytyyttyttutytu", +"XX$$$XXXX$$$$XX$$$$$XXXXXX$X$X .::.@,;6<-;<<1<4::46aadddaa46aadddad66aaaaaa7a76564>4<-;51eryyttyyyttryytyyyyyryrtuyytyyyyyyttytt", +"X$$$$XX XX$$$$X$$$XXXXX XXXXXXX :8+ia>44;;<<<44<;87dadddda>5a221eiiwtyyyyyyyyyyyyyyyyyyytyytytyytttytuyy", +"$X$X$X XX$X$$X$$$$$XX XX$XX$XX #73pda6<>56>2<44<26addaa6>:6adaaadda2;16666a7a6<4>47a1<1ieyyyyytyyytyyyyyyryyyyyyyyytutyyuyyytyy", +"X$$$XX XX$$$XX$$$$XXXX XXXXXX :75ps565577>:<:46<<1aa11;;6aaadddaada<2>2;<6676<<435a1>5eiyytyyyyyyyyyyyyytyyyyyyyyyyyyyyytyyuty", +"XX$$$XX XX$$$$$$$$X$XXXXXXXXX X3773sdp565p742<:<61<2;2;2=<66aaaaadaaa1>6a66a665<5>456>15ewyyyyyyyytyyyyyyyyyyyyytyytyyyyytuyyttu", +"$$$XXX X$$$$$X$$$XXXXXXXXXX &pp73dd357577<<:>481611612;661a1saas6111;21aa664444>56<163iyyytyyyyyyyyyyyyyyyyryyutyyyyyyyyyyyyyy", +"$X$$XX XX$$X$$$$XXXXXXXXXX *ip773ss345775<<<848111aa1;=;-=><1511;;====116166165>4>6615rtyyyyyyyyyyyyyyyyyyyyyyyyyyyyytyyyyytuy", +"X$$X$X XX$$$XX$X$XXXXX X XX*ipp773dd33783::<<<4<<6664;=6<@===-==#;=;1;@21<<646655>>>>6iryyyryyyyyyyyyyyyyyyyyywyryyyyyyyyyyyutu", +"XXX$XX X$$$$$$X$XXXX XXXX&iipp783pp33735771<<46<644<=<66641,-@@:41112==2<4<44545a775piiyyyyyryyyyyyyyyyyyywyyyyyywyyyytyyyutyy", +"XX$*XX XX$$$$$$$$XXXXXXX oriipsp784dp333a7p761446<442=2661666611<6161<1<@=;2<46555aa5ppqryyyyrryryyyyyyyyyyyyyryywyywyywuytyyyyy", +"*oe*XX XXXXX$XXX$XXX Xoiihhisp774ppp837a7dp>4464<<2=<16666666666116661===;<1<445a77p3iryyyyyyyyyyyyyyyyyryyyyyyyyyyyyytyyyyyyy", +"iiiXXXX XXXXX$XX$X X o@iihishpp788dpp#:7p7a,3447842=;166aaa6aaaaaa16666<;=;<<4643ap33iirryyyryyryyyyyyyyyyryyyyyyyyyyyytyyttyyy", +"di&o o %*%XXXX$XXXX Oeiishippp773dd3:#:57>.-47664;@8<61a6a1aaaaaa676a11<=<<6548->53iirrryyyyyyyyyyyyyyyyyyyryyyrryyywyyyyyutuy", +"ppO.oX *q&XXXXXX$XXX..@,eshsipdp783pp3:##:,@.#:4474<:<<11<>1<1661<21<<<6<6;166643##,7irrryyyyyyyyyyyyyryyyyyywyyyryrywtyyyyyyyty", +"di*+X. *&OoXXXXXXX$ .o@&,sshsiip377pd3,##:3,@@4466718<<<2=====22=2===-=;2<6676158##333iryyyyytyyyyyyyyyyyyyyyyyyyyyyyyyyytytyyyy", +"gge. *OoXXXXXXXXXX..@@,3shsipp778dp3,-@@@@+@-46667<22==;22,22=2111<6<<<<8677453##33iriyyyyyyyyyyyyyyyyyyyyyyyryyyyytyyyyyyyyyy", +"esi* &*XXXXXXXXXX .@@=;ppssppp77377:-:@+.+++-446774<<<1116666aa16a61<66<6766558#,3ihrryyyyyyyyyyyyyyyyyyyrryyyyyyyyyytytyyyyyy", +"&.&eo oi& XXX$XXXXXX.@@@=ishhpsp873p3,:28@@.@+=4<4774868<1116aa6a611<88668664453##&3iryyyyyyyyhyyyyyyyyyyyyyyyyyyyryyyyyytyyyyyy", +"oXXo XX&eXXXXX XXXXX..=@@1ssipsp875p3::888:@.@@;44464<68<<2112;;;22<<6664<16443>##3iiryhuyyyyyyyyyyyyryyryyyyrryyyyyyryyyyyyyyyy", +"XXXXX X&OXXXXXXXX .@@===6ssis577377>::88882@@-:<<68<66611<2;;;2=;:<<<<644<<<5-#3&3iyhyyyyyyyyyyyyyyyyyyyyyyyyyryyyyyryyyyyyyyy", +"XXXX X %OoXXXXXXXX..@=@=@1ssis578377:::82822;@;:4<8244<8<1<46a56144<<<656<6<<1#&eerryryyyyyyuyyyyyyyyyyyyyyyyyyyyyyyyyytytytyty", +"XXXXX XXXXXXXXX..@=@====sspp578877:8228282;=;<<><46666666aaaaaa66777a5<4<1,i-&3riryyyyyyyyyuyyyyyyyyyyyyyyyyyyyyyyyryyyyyyyyy", +"X$XXX XXXXXX ..@@==@===1ppp5878p8222828282==<:<<6<<<84667aaaa6aa777744<;113,eiryyyyyyyyyyuyuyhyyyiyyyyyyyyyyyyyyyyyyyytyyyyy", +"XXXXX XXXXXX X .@===@=====1ap387838:8:282822=@>4:>44>44644666a66aaa744>>:::>3,erryyuuyyyyyyyyhyyyyyyuyyyyyyyyyyyyyyytyyyyyyuty", +"XXXX XXXXXXXo@@@===@@===11s>87888:8:828828@-p4;::<:<44<4446455656548>#---33Oirryyuyyyyyyuyyuyyyyyyyyhyyyyhyyyuyyyyuyyytuyyyy", +"XXXXX XXXXXX .@@==========;1s187882;8:28888=+sp1,-=;-:<<444<<><141<<:;=@@=>p,XOrryuuuyyyyyyyyyhyyyyyyyyyyyyyyyyyyyyyyyyyytyyyy", +"XXXX X XXXX .@@======@===@=1118788:;8:8288;.+d151-@@@@;:>2:---;,,-;--#@@@:333 ++qyyyyuyyyyyyyyyyuyyyuyyyyyyyyyuyyyyyyyyyyyutuy", +"XXXXX XXXXX..@@=======@==@==>5<8788:;8:8:8:..&d,35>-@@@@-@@@@@@@@@@@@==@@,55p, Xqyyyyyyyyyhyyyyyyyyyyyyyyyyyyyyyuyyyyyyuttyyy", +"$XXX o...@@=========@====262888:;-82::-.. ip#513:=@@@@@..@......@@---;1>53, Xqhyyhyyyyuyyyyyhyuyhhuuyhyyyyyyyyyyyyyyyyuyy", +"XXXXX oXoO@@@=======@=@=====s;878:=-:-;@.. hp@74>:---##@@......@@#-,--,<51p3 +Oryyuhuyyyyyyyyyyyyyyuyyyyyyyyyyuyyyyuyyyyy", +"XXXX X .o**==========@@@=@===1;878:;-:#.. gd@54>3:-:--@@@.....@@-;--;<>13d& + +O&ryyyyhyuyuyyyuyyyyyyyyyyyuyyyyyuyyyyyyyu", +"XXXXX .&h1@=========@@@=====2;778:@@. hd@343<;;;,--@@@@O@@@#;;-->1>5pg&X X%qyyyyyyyyyuyyyuyyyyyyyyyuyuyyyyyyyyyyy", +"XXXXX Xisi1;===@======@@=====>>88#+ gg#,5>:>:-;---@@@@@@=-;;-;<>53df& X X&ryyyyyyyyuhyuyhyhyyyyhyyyuyyyyyyyyy", +"XXXX &idi31=========@@@=@==@=. + dg+-51<:;;;-#==#@#---->;-:>>5dff& XXXX*rhyyhuyyyyyyuyyyuyyyyyuyyhyyyyyyy", +"X XX +iipi3s2==@======@@@=@@.. df@#55<>:>,---#=#-;=-;;;:>>35gggO X XXX$XOqrhyyyyhuyuyyyyyyyuuyuyyyyyyyuy", +"X X X+pssiiss2======@....... igs@>1<>::>;-----;;;;;>;>>3>dffg . XX XO %qyyyyyyyhhyyuuyyyuyyuyyyyhyyy", +"XXX .iissshss===@@... .. egf,-1<:<:>:----;>;;;>:;;>>5ggfs X XO+OX O*qiyyyyyyyyyyuyuyyuyyyyyyyy", +"XXXX .ppssssdg,...... &gfd@>4::>>>----;;;;;>>;>>,gfggq X+ X XXXOOO9rryyyyhyyyyyyyuyyyyyyy", +"XXX X .iphhshhsO X&gff,#><>:>;;---;>;2;>::--pgfgf* o ++ + X XXX++O++%&ryyyhyyuyyyyyyyyyy", +"XXXX +3pdphi3 *ggfg#-><;:>:-;=;-;;;>;-#3gfgfg X+XX +X XXXXXXXXX%qyhyyyyyyuyyyyyy", +"XXX iphi++ %ggffd-><><3---@----;>:@,gfffgi. +XX X+X X+ XXXoXXXXXX%qyyrhryyuyyyyy", +"XXXX. pdi+ . Ogffffg;>1<>:-;@---=2,-,gffffgO XXXX X +X++X+X+ X+X XXX%%riryyyuyyyyy", +" X X .ih& hgffffd-56>::-=--#->,-dfffffg X XXX X + X+o X+XOXXXX%ryyyyyyyyy", +" o X ii + *ggffffp:553>;-,--,>#gffffffr XX X XX+X+ Xo o+ OXo XXX%yyyyyyyyy", +"O o i& +ggfffff3,555>,3>,1,gfffffffO o+XXXX XXX X o XoXX +XXX$ryuyyuyyy", +",. 3+. qgffffff3>531>1>13gfffffffh . X X++XX +X+XX X X X + + X$*yyyuyyyy", +"iO & o&gfffffff35pddddsiffffffff& X X XXXX X X o X X X X XXrhyyytyy", +"si . O ogfffffffffgfdhpO Ogfffffg ++ + X X+XX+ +X X+ X X XX X Xrryuyyry", +"si# ... Xifffffffe**O+o ++ OgffffqX +X OX +X X X+X XX X+ X X X&yyyyyyy", +"ssi@ &ffffffq+ + + o OgffgOX + X++ X++ OX+ XX+XXXXXXXX XX X X X$yyyyyyy", +"sip&.... &gffff& +o + %gfr + + X X X X+XX+X X +X+ +X X X X$yyyyyyy", +"pisi+. . +hfff&X + + &gO X X+ X+XO+X + X X X XX o XX X X Xqryryuy", +"ppp3#. +eff& o + *+ + + + + XX X OXXX X +X X+ X XX X Xhyyyyu", +"ppi3,. X*fq X + + O + ++ X++XX+X X X+XXXX+ Xo X X X XXrrhyyy", +"p5pi,.. O& + +XX X + X X X O+X X X X X X X XX X*yryyu", +"p5pi3o o + X o+OO X X +X+ X+ + +X X o o X X+ XX X X XXryyuy", +"spp3,. X riggggghy+ + + + X+ X Xo XXo+X X X X+ X X X X Xqyyyu", +"ppi3, . X O+ &gfffffffg&+ + +X X X +X o +X X+ X qiyyu", +"6pp3,. X +hgfffffffgqX o +XX + X X ooX X X X X X X X %yyyu", +"pp5i, X+ +X &gffffffffgO X X + ++ + X oX X X XX Xyhyy", +"ppi3# . +X Ohgfffffffgi+ + ++ X X X +X +X o X XX XX X Xryyu", +"p5ii# Oigffffffffg* X X+X ++XX+ + +XXXoX XX X X X Xryyu", +"5pp1*. X +XX Oiggfffffffhh OO X+ + + X+XX +.X X o+ X XX X X&uyy", +"5pp3@ X +XX ihffffffffgh#oX X+O + X X o o X X XX X X*uuu", +"p3pi. + X+eggffffffgiqO +X++X oo o + . ooo + X X X%uuu", +"p53p. &ii&&&&#O+ + + O*hgfffffgi&O X o O ++ oo .o.o$OO% . X X X $hyu", +"p5i3. 3hfffi3rihhhii3r&& X oihffffgi&&XXoX+X X O+ X ooooo+*@*=#=*=*&, X . XX X$hyu", +"p5i3 .idffg&iiiiphhddhip& Xo &hgffgi&&* o X oX X X X oXX o.Oe,22,,2112s%oXX . X Xryy", +"5ii,. Oihgggiiiihhphhhiihi* X X XXOhdggh&ii XX O XXX+X+X+X+ oo .ooe,=222e22eso.o$... . XXryy", +"p3p# Opgghfiiihgggdgghhhhi* oX ehgheid*XXoXXX X+ XX+X+ X ..&ieeq*1ee,eeee&,e=2=#--&* X Xquh", +"pppO *rpggiqqiiiiihiphpphiio OX o.&gsieii. X+ XX+X + .@&ifseeeheeeeehq,de122=17i*. . X*yu", +"p3p. . O&&q&&iiiiiihihihiiiii X X + hp&&3O X + X+ + + +.@=,sgh1e,heeesqhe,s11222221&% + X+ X%uu", +"ppi. . +oO*&&e&i,iiiiiX XX X &i**& + X + XX X +@-,2212eeieqee,,ee*,22222222@o X X X%hu", +"ii,. .XoO +&OO+ XX ++ +..+@>6a5=2,eih*ihf*&hie=22222==1o + X XXOyy", +"5p,. X +** X +X ..*-375522=eigqeig&,ie,222222=2 c #FF9999", -", c #993333", -"' c #333333", -") c #CCCCCC", -"! c #996699", -"~ c #CC6633", -"{ c #666666", -"] c #999999", -"^ c #666699", -"/ c #330000", -"( c #663300", -"_ c #330033", -": c #999966", -"< c #FFFFCC", -"[ c #333366", -"} c #660033", -"| c #CC99CC", -"1 c #9999CC", -"2 c #660000", -"3 c #FFFFFF", -"4 c #CCCC66", -"5 c #FFCC66", -"6 c #666633", -"7 c #993366", -"8 c #FFFF99", -"9 c #993300", -"0 c #333300", -"a c #FF99CC", -"b c #CCCCFF", -"c c #CC9933", -"d c #CC6699", -"e c #000000", -".....*...*.%*..#&@#&@&#&#&#.&#.#.#.#.#.#..#.*.#...#..#.#.#.#..#..#..#..#..#.#.#@#.>..>...*..*...*....%%%%.&&.@.#.@.#..&#.~.#&#$.", -">%*..*.*..*%.*.&#@&#@#&#&#&#*&.#.#.#.#.*#..#&.~.#.#.#.&.#.#.#.#.#.#&.#..#&.@.@&#&#.>.*.*...*..*..*...%%%%%&&~@#&#.@&##.&#.#.@.>@", -"...>...*.*.>.%*&#,&#&#&@&#.&#.#.#.#.~.#.#.~.>.#.#..~.#*.~.&*.#.*&*.#..#&.#.##.#@&#.$.>.>.*..*..*.....%%;%%@&@.#.@.#..&#..&#.#@+(", -".*...*...%*.*.>&&@@#@&##&#.#&#.~.#.#.#.#.##.#&*.~.#..#.#.#.#.#.#.#.#~.#.#.#&.#&@#&#.>.....*..*..*.*...%%%%%&.&@.#&#.#.#.#$.$&+++", -"..>.*.*.*.%.>.~&#&#&#.@&#&#&.#.#.#..#.~.~.*.~>&>.#.#*.#.#.~.~.#.#.~.#.*&*.#.#.#.#&#..>.>*..>..*.......%%<%%$&#.#&$&#.&#.&>@,++2+", -".*.%...%*.*..*.#&@#@&##&#&#&#&#.&.#*.#.#.#.#*.~*#.~.#~.~*#.#*#.*.#.#.#.#&.#.#.#&@.##.*..%*..*..*>.*.....%%%%&&&.#~.#.#.#.$&+++++", -"..>.*.*.%*.*..*#&,&#@&#&#&#.#.~#.~.&>.~.~#*&#~#.~>#.~$~$&.~..#.#~.~.~.#.#.#.#&$#&#&$.>.>..%*.>....*.....%%%-.@#&.#&#.#.$$,++++++", -"..*.*%.*%.*.>..#&@#&##&#&#&#&.#.#.#.~.#.*.#.*.~.~.#.#.~.##&#.~.#.##.#*&*~.#.#&#.@#&>.>%.*>.%..*.>..*.*..%%%%%&&&.#.&#&#.@++++'=+", -".*...*.*.*.*.*.~#@&#@&#&#&.#~.#&*&*&#.~#.~*#~#.#*#.~.#&>..#.#.#...#.#.#.#.#..#.#&#.#..>...*.*..%*..>...>.%<%-%@.&#.#.$@@++++++=+", -".%.*%.%*.%.*.*.&&,&#&#@&#&#.#&.#&*#.~.*.#..#.*~.#.##..#&..&*&#.##.~.#~.#~.#~#.#.@#@.#..>.>..*.*..>.*.*....%%%%.&&#.#&$,++++=+='+", -"*>.*.*..**..*>.#,@#@#&#&##&#&#*&>&*#.#~.#~.#.#.#*#.&#&.#@#.@..#..@.#..#.#~..#.#&#&#.>.>...*.$..*.*...*.*.>%%%-%&&@&$#@++/++'++=+", -"..%*.%*...*.*.*&,&#&@#&#&&#.#.#.#&.~#.#.#.~.#.#..#$#$.$...$&@.@.#.@.##.#.##.##.#.@#.#..*.>.%*.>...>.#.#....%%<-.&#.@&+++=+=+=+=+", -"*.*.*.*.*%*.*.*&#,@#&##&##&~#&*&*#*&*&*&*@.#.~.#$..$.$.-$%$-%$.@.@.&..#*#.#*&#&##&#.#.>...*....*>.*.*.*.~..%%%%%@@.@++++++=+=++=", -"%..%*..*.*.*..*&&&,@#&##&#.#.#.#.#.#.#.#.#.#.#..@>@.$$$;$%$.$-$%$.#@.#..#.##.##.#@&#$.>.>.>.*>.....*.~.*....%<%<.$+++/+=+=++=/++", -"*.*..~.~.%*.*.*# #&,&#&#&#~.#~.~#.#*&*#..@.#.$.$$.$.$;$;$%$%$-.$.&@#.#~.##.#&##.#.#....*...*.>*.#..#.~....<%%-&+++++'=+[++=++", -".%*.*.&*.*%*.*. @#&#&#.#&#.##.~.#.#..>.#.$&#$@.$$:$$;$;$;$;-;-$.@&#$#.#&###&@.#..#.>..>.>..~..*#*.*.~...%%<-@+++=++=++=++=+", -"*.%*&,&*.*.*.* @#&##&#.~&*&>~.#.>@#&.@#.@$.@$.$.$.$;$;%-;.)%-%$&@.##.#.@.##&#.>.*.#.#.*&*.*.*.#.#..*...-%-+++/+{++_++=+}+", -"*.>.~&~..%*.*. &#&#&#&#.##&.#~#&*&##.@.#&$#.@.$.$;$.%).;-;$;$)%$&@&@#.##,@@.#..>.>..>.>.~.*.#.#>.>.#.*.-$+/+=+=+=++=+=+_+", -".*.&#(#.**.%*. @#&#&##.#&*#.#..##.#&#@.$.$@>$$.$.$%]%;$;;-;)%)%--&@&@&#&#&##.#..*.#.#..>..~.*..#.*...#..++='+++=++=+'+++@", -"%.#&~&~..*c*.* #@&#&#&#.#&*&#*#.&#.#.#@##.&.@.$$.$%$$;$%]%)$;$;-<-%@&@$&##@&#.>.>.#.#&#.*.*.*.>*.~.*..>++++=+=++'=+++=++$", -"*.~&,&#.*%.*.*. &#@#&#.#&#.#.#.#&#&#&@.#..@$#.#.@.$.]%;-;-;$;$;-;)--<$@#&,@&##.*.*.>.#&#&$..*.#.*.#.*$.$&++/+++='=+}+=++@$$", -".#@&,&#.*.*%*.*& @&#&#@&#.#.#.~&*.#&#.##&@@.#.@.#.$.$.$.$%$;$)-3))-;;-;-&@@@#@#&#.>.*.#.@(,$#...*#.**.*..@++++!'+}+=+=+@+@@$.", -".&,&#&&>.*..*.#&#@,&#@&#&#&#&#&#.#.##&#&#.#.#&$.#.$.@.$.$$-$;$$%)%-;-;3;)<-)&@&@&,#.~.*>.>#&,+.@**.*.~>.#.>.+++'+++=+=+=++@@@.$.", -"&#&#&,#..*%*.*.,&,@@#@#&##&#.#&#&#.&~#.##&#&#.#$.#.>.#.$.;.$$;-;-)-);)-)<)%3-++@@&@#.*.*..>@,+@@..#*.*~**.>+++_+='++=+=+=++@.$..", -"&,@,&@&>.*.%.>.#&+&#&@&#&#&#&#.#.#.#&#~.#.#.$&#.#..#.$.$.$$.-;--)%)););););-<-&+,@~@*.**>.#&+2+@>*.*#.*.#>&2+++=++=++_+++@@$$...", -"&#&@,#&>.%**.*.&,@,&@#&@@#&#.#&#.~#.#&#&#&#&#.@.#.$.@.@.$.$;-$-;-;-)-;-);-);-)-$+@&#.*..*.>@++++@.#.**.>*$,+/=+=++'=+'+=+@$$.$..", -"@,#&@&#.*..%.>.#&@@,@##&#&##&#.#.#&#&#&#.##.#.$.$.#..#.@$.--;%)$-)%));)<))<)-<)%@+@.#.**.>.#++++#@.>&#.>.@++++=+'+++=+++@$.@..-.", -"@&@,&,&>..**.*.#&,&#&@&@#&#&#.#&>&#&#.#&#.#.#.#.#.>@.@.$%).-$;-;$-;-%)-)%)-;)%)-$+@&.*..*.#@+++++.@...$.#++}=+=+=+=+++++$$$.$...", -",#@,@&#..*...>&#@+&@,@#&#&#.#&#&%#&.#&##.#.#.#.#...#&$.-.-;$;$-;)-))-;-;-;)-;3;3-@@&$.*.*.>&++++/@.-%%$$+++++++=+'+=+++$.$..$.$.", -"@,&#@,&#%.%*.*.&,@,&#&#&#&#&#.#.>&#&#.#&#&#.#.@.#@@..$%$%$.-;$;-;-;-;--)--;)-;);-$(#..*.>.>@++/++--;-<<)/'+[+=+++=++'+@$&$.$%.$.", -"@,@&,&..*.*%..>&,&@@,@#@#&#&#&#%>&#&#.#.#.#.#.#..&@.$%$%]%).-;$;-;$)%$;-%)-;))<)3-@@...*..#@++=@-)%)%)-<{+[++{+=+='}+@:$.$..$.>.", -"@&#@#,&#.%.*.*.&,@,@&&#&@#&#.#&%>&#&#&#&#.#.#.@>&#.$%$.-.$;$;-;$;$;-)--)-;-)%);)))%@..*.*..@++]-]$<);<)-]'='=++++=@++$.$..$..$.$", -"#&,&#&#.**%.%*#&,&&##&#&#&##&&#%.#&#&#&>&#&.#..#&-*$.$.-$-.)$.-$;$-.;-;.-)%;);-;-)).$...*>&&+$)$))--;)%)$+'+=+=++'}'$$$.$.%$....", -"#&@#,~&*...*..*&@,@&&#&##&#.#&.%*&#&#.#.#.#.&#.&.>%.$.$.;-$%$).-$;-$;--);$);-);)-))-$.>..~@.%;;-;));-;3-{+'+++=+=++@@$$..$...$$.", -"@&#&##&*.*5.%*.#,&@,&#&#&##&#&>%#&#~@#&#@.##.@.>..#.$%$-$.).$;$;%$;-.;-%)%$;-;-;);-;%$....%--$3)-;-));-)++=+[+/++/+@$.$$..$%>..>", -"~@#&,&#*.%*.*.*&@,&#&#&#&#&#@&%.#&#&~.#&#.&#.#..>.$.%$.%$.-;$.$$%$.-;$.$;$;$;-;-)$)-%$.$-%-;%;-;-)-;-3%)'+'++=+=++@$$$.%$..$..$.", -"@&###&#.*.5.%.>&,@@&#&#,&#@~&#%..~#&#&#&#$&#..>..#.>$.$.$%$.-;%.-.$%$%)%$;-)%$);%).]%-%%$.-;-);-)%)%;-3-+'=+_++++@$$.$.$..$.>..$", -"~@&#~#&*..%**.*.,(#,&#&##&#&#&%.>.*&#&#.#.#.#.#..#...$.-.$.$..$.%;$.%$.-;$%)$;$$;$;-;$)%-;-$%)-;-;-)-;-]++'+'=+++@.$$.$.>...$.>.", -"#&#&,&#.*..%.*.#@@&@&#&#&##&#&-%..#.#&#&#.#.$..#.$.>.$.$.-;%$%.-..$;.).$%$-.-$;$;$;$;$;-);-;)-;-)%$%)%-{+_=+=+'+@$$..$.%$.>..$.$", -",@&##,~*.5*%*.*&,@,##&,&#&#@&~%.**.~&#&#&#.#.#..>.$.$%$.$.$.$.$.$%$.$%$%$;$:-$-.$%$;-;-;$)-)%)-;-$:$.)-+'+'=+++@@.$.$.$...$.>.>.", -"&#,&#&#.*..%.*.#,&@&#&#&#&#&#&%..*&*#&#&>$.#$.#..$.#.$.-$.-.-..-..$%$.-$;-$$%]$;-;%)%))););$)%)%-.$@;$$++=++=++@$.$.$..$%.$..$.$", -"@&#&#&#..*.*.*.&@,@#&#&,#&#&&#%.*.*.&#.#.#.#&$.#.>.$.$.$.$$.$$$.$$$%$$%$.$;$$.$$;$%$;)%)-);-;-;-.).$$-'+'+='+'@@.$..-.>.$...$...", -"&#@,#~#*.%5%.%*#+,&@,#&#&##&#.%*.*.*~&#.>@#.>..#&$.#$$$$$.$$@@$$$.$$$$:$$-.$;-;-%;)%)$;);-)-;--$;@.@;-++=+=+++@$.$.$..$..>$..$>.", -",&#&#&&*.*.*.*.~@@,&#&#&#&#&,.%%..*..#.#.#.#.>.#.$#..$&@@@@@$$@$-$$]-$$+.{$%-$;-%$;-;-;-)-;--;-.$@:@-$/++_+=++@$.$..$.$.$..$..$.", -"@&#,##~.*.%.%*>&,@@,&#,&#&#&,.*.*.**.&#.#$#$.#.@*@.$>@$$@@@+$)@!]+@@@++{+$;$:-%]%)%$;-);;--;-%$@&+@%-+'+=++++@$$.$.>.%.$.$..$..$", -"&#&&#&#*..5*..*&,@,@&#&#,#&#&&%%%*.*.#.#&#.#.$.#.$#.@$@@@{!@!@!+=$=@'+++@$$-$%-;)-;).)%-%)%$.@&@&$%)+++'+==++.$.$%$.$$.$.%$.$...", -"#&#@~#..*.*%%*.#@+#@#&#&&#&,&~%%5*.~.~&#.$#$&#..#.$#$@$@!@={=!!@[@[@=e+)$;-;);$%;$-;-);$<$&@6+@&]$-(++++=++++$$.$.$%.$..$....$..", -"&#&#&##*.%..*..~#+&#&,###&#&#(%%%...>&@#.#.#.#.@>.@#$@@+@+=+!{=!!=+!++)@-]%$%)%$;)$;$%)%@]&@&@.$.)+++'++=+=+@$.$%.$.$.-.$.$$..$.", -"@#&##&#.**%.%*.#&,@,@&#&,#&#&#.%5....#@>#$&#.$#.$$@$@=!==+=]!_+=@[+{+$$)%$);$;;-$-;---@@@@.:%.-$-++++=+=+++@$.#.$.%.$.$.$.$.$$.$", -"&&#&#~#...5*%*.&,@,@#@#&#&#&&,&%%%%..@.$.,.#@&$@,$@+}@='=/={+=]!_!/!$]%$;).-$;$;)-$;.@{@@$-$..%%+/'++=+'=++@$$.%..$.%..$%$.$.$.$", -"#&#&#&#**.*..**#@,&@,@#@#&##.#&%%%..#$##&#.&$.$$+@+!@=+!={=+={=@@_]$@-;$%$;$;$;$;-.$(+@${@:%.--+++++=+=++++@..$.$%.$.$%$.$.$.$$.", -"#&###&#.%.%*%...,@,#&#&,#&,&#&#&%%%.&..#$#.>@#@@@@=@=+=!+_'+=+='!!{@)$--;$;$;$%;$)%]&@@@$@$$%.(+++++'+_+=+@$.$%..$%.$..$.%$.$..$", -"#&@&#,&**.%.*%*&,@&@,#@&#&#&#&&9%%.>.#@..@.@.@@@@@@=_=+=@_+=@@!+!]@$).;$.-.$;$;-;-%-.@@@@@:-@++++++=@+'+++@@@.$.%$..$..$.$.$.$.$", -"#&##&###..*%.*.~@,+#@&###~#&#.#&.%%.&.,.#&#$@$@+=+@=+=^@[+++=++!@]@)$%).).)$.).-;$;%.+@@@$@.6+/++++++=+=+@$&$%..$.$.-.$..$..$...", -"&#&#&,&*.%.%*%*#&,@&#,@&@&#&#~&#&-%#.#&#&>.@@@}+=+={!==@!+'+'+@+$1$)$%$.$$.-;-%;-%-%$@++=@]$+/+=+{=+=/+++@$$.$.%.%..$.%$.%$..$.$", -"#&@##&#.*%*.....,@,@@&##,#&#&>&#&%%&#&.@.@>@@+!+='+==+![$!+++@@$]$-$;$;$%$.$;$%-;-%%%$++@+$]+++=+++++'+++$@$.$.$.$$%.$.$.$%.$...", -"&#&#&,&#..%.%*>&,@(##@#&#&#&#.#&#->.&#&>&@++!+=+=+=+[=+=@++/@$$$$]$).$..$;$-%-;%-%;%.$@+}{$$+'+=+={+=+++@@$.$..$...$..%.$.$..$.$", -"&#&##&#*.*%*...#&@,@&#@#&##.#~&#..-&#..#$@@++++^+==+^=@!'=/@$$$-$$$-;-.$.$%$%$%-%--%%#@+}@]$@++=++=+++++$@.$.$..$.$.$.$;..;.$...", -"@&#@#,&...%.%*.#&@,@#&#&#&#~$~#.##%#.@.$@++!_=@={+=+=='=+@{@.$;$)&$%$.$.-$%$%-;%)%%%-%@+++@${+={=++=+++@$$.$..:.$.$:$.$.$$.$.$$.", -"&@&#&,#*.*%*%*.&,@@&@#@##&#&#.&#&$@.@.#@+!++@'+=+'=+=+=@'@$:$.$%-:-$.$.%>%$-.-%%-;-%%>@@+/@$++=+={++'++@@$$.$%$.;..$..$..$.$:..$", -"#&#@#&@....%.%>&#(@#@&#&#&#&##*#@&#.#$@@+@_!_=+{=+={==+/+@@$.$;$$$..$%>.$>%$%-;.-%%)%-$+}+@)@++={+=++++$..$;$...$$..$:@.$:$.$$$.", -"@&#&#~##..*%.*.&@@,&#@#@##~#.&#.>++&$.@+=+=@=+=++++='+^+]@-;-..-%$@&&#.>%>-.$%-;-)%%%$#++++-@+@+=+[+++@$@.$..$;..$.$.$.@.$$.@.$$", -"&#@,@,&*.%..%%>~@(@@&#&#&@&##.*.#+2$@@@+!=^!'={+_+=@!@_+@:-.).;-.$.$.,(#.>$%$%%%$;-%.@@++++%@+=+{+=+++@$$.$....$;.$:.$.$.$.$$@.@", -"@&&#&#@#.*%.*..&#@,@#&#&#~#&#&>.$@(@@@+@++=!+=+'@/=/@+'$@)$%$.-&@$$$.$&,&#.>.$;%%$@@&@#@+++]$++=+=+'++.@.$%$.$..$%.$.$.$:$$$:$@$", -"##&#@9&..*%*%%*.,+#&#&#&#&@&>..>.$@++!=^!!/^+^+=1'+=++@:-.;-$%@@@+++++$.#.>.%%-;-$@+++@++++$@@++=+=++@$@.$.$..$..$$.$.$.$.$.@$:$", -"&#&#&#,>...%.*..&,@,&#&&#&#..$..$+@+=@!@^]=@=+'+_!'+0+$.$;-%-+(+,++@@2+#.#*>.%-;@+++++}+++{$@@+++'+++@..$.$.$..$...$.$.$.$$$$$@$", -"&,@,@(&.**%*.%*&,@,@&#,.,&#-.$.@$++^+^_=!@_'+==/^+_++@:.-.-%+@,++@++--(@~.#.>%%$++@@a+++=++$]@=+==+++.@$.$.$.$.$.>$.$.$$.@.:@@.@", -"#&#&#,,...*%.*..@,&##&#&#@+,$$$(+/@=+=!+^1+[+'+='=+'+@$$.%-&@&,&@+@@$-#@#&#..)%6@+@@$7++++@@$@@++=+++$..$.$.$..$..$.$.$.$.@.$:$.", -"@&#@,&@#.%.%*%.~#,&#&@.,+&@--%+++{!'=!=^+!+==+=+=+'+$..$;-&&$$@@&$.--%.$~#*.>%-@.$#$$,@+++!{$@@+=+'+@:$.$.$.$.$.$..$.$.@$:$.@..;", -"#@#&@,&#.*5.%*..&@#&##.+#$$$@++@@'}'={=!!!'/'=+'=+++@-$%-&@...*$#$@&@$-.#.#.$%;$.$.@,+@}++@@|{++++++@$.$.$..$....$.@.$.@.@.;.%%%", -"#&@,@,&#.*%*%.*.,,&#&#$#$$+@=+'+1$!==^={$1+=++=++/+$]$.-&&*#>.$.$.$.-.#.>.#*$-%-$.##@#@++++@$@+=+=+@.@...$.$.$.$.$.$.@.$.$.$%;.%", -",@#&,+,.>.%*%.*.&#&#.@&$@!+!+==@!{^'==!!!!{+='/='+@@$--#&#...>.>.$.$.$%#*#.*$%-.$.$.#$,@+++@$@@++++@$.-$..$.$.$....$.:@&$%%%%%.%", -"@&+@,&+...%.*%.#,#,@##$#$$|={={^{!+_=]={@$!+++=+++@$$%$&@.#*>.>%$%%$%$..>.#.>%--.*>##@@+}+{+|$+++++@....$..$...$.$.@.@.;%.;%.;5%", -"@,&,@+,#.*%*%.*.&,&#.$@$+=+='=!+!{[='==!^/@!'++/+@]$--(,#.~.*..>.>.%$%>.*#*#$%-%$...>##+++++$]+++++$@.$..$.$$.$.@:$...$.-%.%%.;%", -"@,@+,&+...*%*%.~#&,@@#$+!@^=!'!1'_+==+=+++]@+=+++$$$)++&#&>~>.>%%%$%..$.*.#..---..>.#$#+++++-$++++@.$...$...$.@...@.@.%;%%;%%%%%", -"@+@@,+,@>%..%*..,#&#&@@$!{=!==!{=='+[+{=++$@@'/+$&--@+/@~#*~.>.$>%.%>.>.>*#~$%%-.>.#.#@,++++$$++++@$.$..$.$.$.:$.$.:..%%.%;.;%;%", -",@(+,+&#..*%.%*&,@#@@$$+@!]={^@!{+=='_++++$@$@+/@$-)+/@@#.~**.>..>%$%..*#.~.#$-%...>$#++7+++1$++++@.$.%..$.@.$.@.$&$.;%%4%%%%;%%", -"+,@@+,+&>.%*.*..&@&#$$+!@^===!1]|'+=+}{@+@$$+++]@%$+++/&###~#..>...>.>..#*#..%--.>..>@,+++++$$++++$.$.$...$.$.:$.@...%.%.%4;%%;-", -"@@+,+,+..*.%*%*#,,@@@$@+!{={1{=$=/'+=+@+]@+@/'/$@%@'++@@~*~**>.$*>%..$*.~.#*@.-<..$#.@+++!+/$$@++@$....$...$.$.@.@.$%%454%%%;-%)", -",@+@,++#$%*%.%..,@.#@@@=@!==!=^][+='/=@$@.++++$@:$+/+/@+@#*#*.*$..>.>..##*.#..--...>@,}++@+/$$$+(@@$.$..$.$.$.$.$@..%%%%4%;%;%)%", -"@,@++(,....*%*..#,@@@$+@!{=@^+!!{|+={++=+@+/+@;$-@+'++@,#&*#.#.>*>.%>*#.*#.#.$%-.>.@$+++=++++$$/+.$.$.$..$.$:$...@.%%.4%%%;%%;%;", -",@,@,@+.>.%..%*&&&,@@@@!@=_!='!!]1+={}'+!++++$-$]+/+++@@&#*~*>.>.>.>.$*.#&#&#&>.*..#@+=++++++$$+&$..$.$.$.$.@.$.$..;%%%4;%%;%-%%", -"@,@,+&@...%*%*.>#+#@$@+!+=]=!=]!|!'+!@/++/@@@$6-++=+/+#@~#.#*~>*.#*.*.>.*..#.$%$.>$@+}+=@++++-@+@.$.>.$..@..$:@.@..%%4%%%;%;%;;%", -"@+@@+,@.$*.%.%..&+&#@$+{!@=_{=!!{!!'@^++!@$$@@$+'+++++#@$~#*#.#.>.>>.>.#.>.%%-%$..#@++++!+@+($$@#.$.$..$.$.$..$..$.%%%;%;%%;-;-;", -",@,+@+#..%.*%*.##,@@$@!+!{==={={=1@={@/+]$$$@@@+++=@++@&#.~*~**.#...*..>..>-.%>.>$.++=++@+++/$.@...$.$..$..@.$.@..%%.%5%;%;-%<%;", -"++@+,+@.>.%.*%..&@@#@$+@!+!'==!!^!1+@'+@@$-@@]+++@++++#+##.#.#*.*.>.>.*.>..%$%%$.$@/+=+7@@++($@@$.$...$.@.:$.:.@..%%%;%<%%<;%-;%", -"+,@+@,&$..*%.*.#&,@@@!+!+[=@^@[+^={|++.)$-+@$++{+=+!+++&#.~***#.#*.~.#..*$*>%$.$#@+++++=+@,++@:....$.$..$.$.@.$.@.%%%5;%;;%;%;-<", -"++,++@,.>%.%%%.#&@#@$@=+!'===!{=^!!{@)@-%@@@+/+@}++++++@##.#~.**.#&#.~#.#&.#.,&@>$++++++}@++(@@@.$....$...$..@....%%%%%%%%;-<%;%", -"+++@+@&$..*%*%*.@$,@@@@=+={^'|!{!!{+@$&3$&]$++=+++=+=+,+&#~.#*#.*>*~#.~~>&#.>$.$$}+}'+=+@@+@+&$.$..$..$.@.$:$.]@&%%;%4%;%;%<-;%<", -"@++++@&>..%*.%.#&$+$@@'=+_=@=]=!^!={]$$;@$$++'/=+++@+++@@&#~.~.#.#.*#*#.*>%-*#$>@+++++++}@2@+&$.$..$..$:.$..$..$&%%%%%<%<%;%%;%%", -"++7+,+@..>.%%*..#+@@d+@='='=^@[@^^@!$]$$@-+++++++=++=+@,,@~@~#*~.*>.~.#*#.>.>.>@+_+=+++@+@+@2+$..$..$..$.$:@:$@:@;%;8%;%;%-;%;%;", -"=++++@&#.%.*%>..,@$@@+!+=$[@b{!{=^!]@$$$-+@+}{=+++++@++@,@#&#.#.#.#*#*#.#~##$#$,+=++{++@7@}@++.$..$..$.$.$.$.$.@.%%%%;%<%;%)%%%%", -"+++++#@..*%.%*..@,$@@+!'_=='|^@1!+1@@{$-@++=++++++=+{+@++&#&#&>.#*.>..#.>..>.>@@/=+++++@+@+@++.$...$..$.$.$@:$@:@;%<%%<%%<%%;%;%", -"+=++@@@#..*%.%>.@+@@+@@='@[=$^!]!^+1$+]$+++++=+=++++=+++++,#&##.#.#.>*.>.>$%$.++=+[+{++@+#+#++$.$..$.$.$:$.$$$.@&;<%;%;;%-<%%%%%", -"+++++@..>.%*%..#@,@@=+^+=^+^]='1|]=]$@@+=+=++'+++++=+'+++&+,,&#.#....>.>%$.>$#(++=+++++@@+7@@(-.$.$..@.$.$@:@$@].-%;<%<%;%%)%;%%", -"+=}++@@...%.*%*.@+@+++!+'='_!]={!!]!!{!+'=+=+='@+++++=@+++(+(,#&#.#>..%.>%$.$@+++'={'++@+@+@@+.$..$:$.@$.$.$.@:@:%<%;%%<%;%%%%..", -"+++++#@#.*.%*%.#+@$@=@==+='=]|={!^|{|$1@_+'}{@}@_+'+=+++@+#+,+,(#&&#.#>.%$.$.@++=++++++@!@+#++%$&@.@.$.]$$@$]@$&.;%%4%;%%%-$$+++", -"++++++&$>.>..*..@-+$+'@={=+{!]=={!^!]!{)!'=++=+@+=+++=+=++++&#.#.#.>...$*.$%$.@+'='@{=+@+!+@@($.@.@&$@.@.@.$.@]@%%<%%%%;%;.(++++", -"++++}@,&$%*%*$*.-++===+=+=+_{|]=[!$^@^!+]@'=@++=+++=+'+@++,@#@#$#.#.>.*.$..$%$;-@+'++++!@@@+@($.&@.@.@@@.]@@.@&$;%%4%8%%%.@++++@", -"@@+++++#.>.%.%>.+@]++_{='!{+[@^@|]^!^@^|]!++='+@=+'=++=++++@@#.#.#.#.#..>.$.-%----]/'++++7@+@+.$.$&$&@&@@&@@@@@&%;%%%%%;$&(++@@@", -"@!@@+@+&..**%*..@++='+=@_+^@='|^!!{^@1@^$+1+@+=+++++=++=@++,@#.#.#.>..>...%.-.-;-)--+++@+@@+,@.@.$.@.$@.@@&@&$&@%;%;%;%.&+(@,+@,", -"$]@@@++,.$%..%>.$@++_+|{=]+^+={={==!{!]!{={{=++=+=+'=++++@+@&#$##.#.#.~.>.$.%%$%%)%3;@'+++@++@$.@.$.$.@.@@@&@&@&%4%%%%$(+(@&@,@@", -"@$$$@@2@.*.*%*..$@+=+_+=@_@'!{=$={'!^$!]=$_$+'/@=+=++'=++@+,@.#..#.*.>..*.%%$%.;$%;-%-$+{+@++.$.$.@.@.@$&$@&$.@&%%<;%%@(@@@+&@&@", -"{$]$@@+@$%.%*.>.@)/'={=+='=+=+[{==!{!{1@1+)=+=+{+'+=@+++@@++#.##.#.#..#..>...$%%;%-;%<)@++@++$$.$.:$.$.$@.@@&$&%;%%%;.@(@&#@#@,@", -"+@|$$@(,..>%*%..@@+/=+='=+{='=+={[+=!!]|]!+{$+/=+=++=++@@@#(@#&$*.#.*#.*..%.%.%-%;%;-%--{++++$.$.$@.@$@.$#.@.&$%%%;%%@(@@,@.@&&&", -"'@]$$@++...%.%*.$@+=/!+=+=@=+_{==@^{+^!@|{=|)=++{+=+{+}+@@@(,.#.#.*>.*.>.>..-..%$%-;%<;--+@++.@$..:$..$.@.@.#..%%%%%.&,@&@#@,@#@", -"++]$$$+&.*%*%*.$$+'=+[='_'$'='_@^{!!!{!]|'=@-+/!+=++=+++#@#2@.#.#.#.~>.*..>..%$%;%;%-;-%)++++$..$..@$@.@$.@&..%%%<%%$&@&#@@&@&&@", -"''$]$@++$%.%.%*.$++'=+=+=@!'+=+='=+{!'!1!@={|'+@{=+++++@@@&2&#.#*~.*..#.>.%$..%.%$;%%;-<-)++($.@.$...$@.@.@.$%%%%%%.&&,@&##@#@@@", -"++!$$$(+..*%*...@+_@_+[+[@@_{='=+[={!=@^$^@!{+_+++=++++#&#@(#.#..#*#.*.*.>..%$.-.%-;-%)-;3/0@@...@.$@.$@.@..%%%;%%%$&#&#@&@,@@#&", -"''])$@(+..%.%*..@'+'+[+=@[++=+=+='+=]=!]|]!@^++=++@++++@#.#,~.*#*..#*#.#..>.$%.%$;%-%;-<--@+@.$..$...@.@$&@%<%%%%%%&&&#&#@,@&@@@", -"/+@]$$+($.%*%%*.@@==/='=+]='+'='=+^='!{|=]|$@{++=+++},@#.@(&##..~**..#*.>.>.>.$%.-;%)%--;-)(]..@..@$.$.@.@:%;%4%%%$&#&&#.&#@@@+@", -"''!)$]+,..%..*..$++'+=+[+!@_!+_+='!+=@!1$_'[+=]$@=+++@@#.>,&&*#.#.~*#.*#.>.$.%$.-.-;-;-;-<$+&]..$..@.@:@.@.%%%%<%*.&&#&#@,&@,@@@", -"++{$$$@($.*%*%.>@'+=+'=+'='+'=+'=+^'^^@^@1+=+++@+@@++&#@.#(##.#*.*$*.#.*#*.*$..%.%%%-;-;-;-(@.$..@.$:$$.@...%%8%%.#.&&#&@@#@@@,@", -"_/!]$]@+.>%.%*..$++=++{=++=+=+=+{=+^+^]1+^@'/={++++2+#&#@#+&#.*.#.~.**#*.>.*..$..$;%%-<-%)-:$.@.$..$@.@:@...%.%%%.~#&#&@,@@@+@@@", -"+'+$]$+&&.*%*%.>$@+'+=+'={={+=+=+[+^='!^+^@|'++=++++@&#..#&#.#*.*.#*#.*.#*.>.%.%.%.-;%);-<--:@.@&$.$$].$&.....%%%.&#&@,&@,@#@@+@", -"_+{$$]@+$..%.*..$@}'++='+=+='='+!'_$!'={[@^@=++/++++#@##@2&#.#*#.*.*.~>~.*..>..$..%$%-%-)%-<@&@&@&@.@$@.$....4%%.#&,&@,@&@@+@@@+", -"+/=]$$&&&>%*.%..@@+=+'}=+{=++=+_=@!{!={=={!+'_{+=+++#&$*@(#.#.>.*#.#.*.#.#*...%.%.%.;-;-;-;-.@&+(@@@@@@$...&.%%%.@&@,&@@@@@&@@++", -"+'+]$)@+...*%*..@++'=+'='=+[+='+'_+^$|{!]='=++=+/++@#.##2&#.#.*#*.*.*#.**.*.>*.$.$%$%;-%<--<$6@&@&+({(@+&$..%%%..,+&@,@@,@@@@@++", -"+++$$).(..%*%.%.@=+++=++='={+=+==+=={|!^@=+^/=+/+++#@#&,(#.#.*#.*#.*#*.#.#*...*.%..%$%;-;-%--&@@@@@++++(+%%.%%%.@(@+@&@@@+@@@+++", -"{@{]$]$+&..*%*.&$@_'+'='+=+='=++='=+_{$^!{='=/+=++@#$#&2#.#.>.~*.*.*.#*#*.#.>.>..$%.%-%-;3;-%{&@&@@@@++(,%>.%<.&++,@+@@(@@@@@+@+", -"@@@$$;$&@%%.%%.&$+++=+=+='=@=+[/=+='=!_@!{_++[+++@#@~,(,.#.#.*.~*#.**..*.*.*...*..$%.;-;-%-;-$@&@@&@@++(+.%-.>&(+@+@@@@+@@@@@@++", -"+{@]$]$&&.*.*%%&@=+=+'='+='='=@='!+=+^@^+='=+/+++@@#(2,~#.#.#*#.*.~.#*~.#*#.#*.$*.$%$.%-;)%-<-6@@@@6@@+@&>%%$&+(+@+@@+@+@@@@{+@+", -"@@@$$.]&@.%.%...@++'=+=+=+=++={}+!'!'=@1+=++++}+@(,(#~..~#.*.**~*.**.*>.*.~.*.>..>.%.-;%-;-%--+@&@@@@&@&#.>.&,&+@+@@@@+@@&@@++++", -"+@{@)$$&..*%*%..{++=+{='='=^+!@='!+=]={!{='=++++,(#&&#~#.*##.#*.#.#*.*.~.#*.#.**...$%.$;%)%);-:@@@&@@@&@&..,@(@&+@@@@++@@@@@@+++", -"+'@@$)$&@..%%%.#++_+=+=+!+=@[+=@={!_+={!@++++/+,@&.#*.*~#..~.#****.~.#*.*..*.*#.>>.%$%%$%)%----+6@@@&@@.##&~@#@+@@@+@+@@]@@++{++", -"+++{$-)$..%*.%.@++++'+{='='@={=$=+!'^$_+^+_+++@$&#&~&~*.~#*#*.~.~.*.*.#*.#*#.*.*..>..$;%;-;;-;-+@&@@&$&#&&#&#&@@@@+@+@@.@@@+++++", -"++'@)-.$&$.%%%%&+'=+==+=+=={=@={@[@='={!$@+++,@,.&.#*&~~.*&.#*#*#*~.*.*.*..*>.*>.>.%.%$;%)%--;-@&@&@@.@.#&#.#&@@+@+@@@@@@@++++!@", -"++++-))$...>.%.@++_++{={'=@_+=+={=]+_@[/^@+@+@#@#.~.~*.~*#*#*.~.*.*.**.*.#*.*#.*.>.$..$;$%);--)$6@&@&#.@*&#.#@,@+@@+@@$@@@++++@@"}; diff --git a/Tests/images/lena_bw.png b/Tests/images/lena_bw.png deleted file mode 100644 index f9b64c185..000000000 Binary files a/Tests/images/lena_bw.png and /dev/null differ diff --git a/Tests/images/lena_bw_500.png b/Tests/images/lena_bw_500.png deleted file mode 100644 index 1e1d0bd7d..000000000 Binary files a/Tests/images/lena_bw_500.png and /dev/null differ diff --git a/Tests/images/lena_g4.tif b/Tests/images/lena_g4.tif deleted file mode 100644 index 7ebe72fab..000000000 Binary files a/Tests/images/lena_g4.tif and /dev/null differ diff --git a/Tests/images/lena_g4_500.tif b/Tests/images/lena_g4_500.tif deleted file mode 100644 index 80f5e70f1..000000000 Binary files a/Tests/images/lena_g4_500.tif and /dev/null differ diff --git a/Tests/images/lena_webp_bits.ppm b/Tests/images/lena_webp_bits.ppm deleted file mode 100644 index 62fd9803d..000000000 Binary files a/Tests/images/lena_webp_bits.ppm and /dev/null differ diff --git a/Tests/images/lena_webp_write.ppm b/Tests/images/lena_webp_write.ppm deleted file mode 100644 index 4fa197999..000000000 Binary files a/Tests/images/lena_webp_write.ppm and /dev/null differ diff --git a/Tests/images/multipage-lastframe.tif b/Tests/images/multipage-lastframe.tif new file mode 100644 index 000000000..aeba534e2 Binary files /dev/null and b/Tests/images/multipage-lastframe.tif differ diff --git a/Tests/images/multipage.tiff b/Tests/images/multipage.tiff new file mode 100644 index 000000000..7a7f5f89a Binary files /dev/null and b/Tests/images/multipage.tiff differ diff --git a/Tests/images/png_decompression_dos.png b/Tests/images/png_decompression_dos.png new file mode 100644 index 000000000..986561b2e Binary files /dev/null and b/Tests/images/png_decompression_dos.png differ diff --git a/Tests/images/tRNS_null_1x1.png b/Tests/images/tRNS_null_1x1.png new file mode 100644 index 000000000..976eae939 Binary files /dev/null and b/Tests/images/tRNS_null_1x1.png differ diff --git a/Tests/images/tga_id_field.tga b/Tests/images/tga_id_field.tga new file mode 100644 index 000000000..a3d666848 Binary files /dev/null and b/Tests/images/tga_id_field.tga differ diff --git a/Tests/images/total-pages-zero.tif b/Tests/images/total-pages-zero.tif new file mode 100644 index 000000000..50df07af3 Binary files /dev/null and b/Tests/images/total-pages-zero.tif differ diff --git a/Tests/import_all.py b/Tests/import_all.py index 118bf69a7..88a102b64 100644 --- a/Tests/import_all.py +++ b/Tests/import_all.py @@ -1,7 +1,9 @@ +from __future__ import print_function import sys sys.path.insert(0, ".") -import glob, os +import glob +import os import traceback for file in glob.glob("PIL/*.py"): diff --git a/Tests/large_memory_numpy_test.py b/Tests/large_memory_numpy_test.py index 8a13d0aea..5e5a58441 100644 --- a/Tests/large_memory_numpy_test.py +++ b/Tests/large_memory_numpy_test.py @@ -1,6 +1,6 @@ import sys -from helper import * +from helper import unittest, PillowTestCase # This test is not run automatically. # @@ -14,7 +14,7 @@ from PIL import Image try: import numpy as np except: - sys.exit("Skipping: Numpy not installed") + raise unittest.SkipTest("numpy not installed") YDIM = 32769 XDIM = 48000 diff --git a/Tests/large_memory_test.py b/Tests/large_memory_test.py index a63a42cd5..3ee13091d 100644 --- a/Tests/large_memory_test.py +++ b/Tests/large_memory_test.py @@ -1,6 +1,6 @@ import sys -from helper import * +from helper import unittest, PillowTestCase # This test is not run automatically. # diff --git a/Tests/make_hash.py b/Tests/make_hash.py index 88bb2994b..a92886df9 100644 --- a/Tests/make_hash.py +++ b/Tests/make_hash.py @@ -1,5 +1,7 @@ # brute-force search for access descriptor hash table +from __future__ import print_function + modes = [ "1", "L", "LA", diff --git a/Tests/show_mcidas.py b/Tests/show_mcidas.py index db193b82a..1f1c04aa8 100644 --- a/Tests/show_mcidas.py +++ b/Tests/show_mcidas.py @@ -1,3 +1,4 @@ +from __future__ import print_function import sys sys.path.insert(0, ".") diff --git a/Tests/test_binary.py b/Tests/test_binary.py new file mode 100644 index 000000000..1e44d9641 --- /dev/null +++ b/Tests/test_binary.py @@ -0,0 +1,29 @@ +from helper import unittest, PillowTestCase + +from PIL import _binary + + +class TestBinary(PillowTestCase): + + def test_standard(self): + self.assertEqual(_binary.i8(b'*'), 42) + self.assertEqual(_binary.o8(42), b'*') + + def test_little_endian(self): + self.assertEqual(_binary.i16le(b'\xff\xff\x00\x00'), 65535) + self.assertEqual(_binary.i32le(b'\xff\xff\x00\x00'), 65535) + + self.assertEqual(_binary.o16le(65535), b'\xff\xff') + self.assertEqual(_binary.o32le(65535), b'\xff\xff\x00\x00') + + def test_big_endian(self): + self.assertEqual(_binary.i16be(b'\x00\x00\xff\xff'), 0) + self.assertEqual(_binary.i32be(b'\x00\x00\xff\xff'), 65535) + + self.assertEqual(_binary.o16be(65535), b'\xff\xff') + self.assertEqual(_binary.o32be(65535), b'\x00\x00\xff\xff') + +if __name__ == '__main__': + unittest.main() + +# End of file diff --git a/Tests/test_bmp_reference.py b/Tests/test_bmp_reference.py index b45ea76f6..79ad439ba 100644 --- a/Tests/test_bmp_reference.py +++ b/Tests/test_bmp_reference.py @@ -58,10 +58,10 @@ class TestBmpReference(PillowTestCase): } def get_compare(f): - (head, name) = os.path.split(f) + name = os.path.split(f)[1] if name in file_map: return os.path.join(base, 'html', file_map[name]) - (name, ext) = os.path.splitext(name) + name = os.path.splitext(name)[0] return os.path.join(base, 'html', "%s.png" % name) for f in self.get_files('g'): diff --git a/Tests/test_box_blur.py b/Tests/test_box_blur.py new file mode 100644 index 000000000..8c52054a2 --- /dev/null +++ b/Tests/test_box_blur.py @@ -0,0 +1,235 @@ +from helper import unittest, PillowTestCase + +from PIL import Image, ImageOps + + +sample = Image.new("L", (7, 5)) +sample.putdata(sum([ + [210, 50, 20, 10, 220, 230, 80], + [190, 210, 20, 180, 170, 40, 110], + [120, 210, 250, 60, 220, 0, 220], + [220, 40, 230, 80, 130, 250, 40], + [250, 0, 80, 30, 60, 20, 110], +], [])) + + +class ImageMock(object): + def __init__(self): + self.im = self + + def load(self): + pass + + def _new(self, im): + return im + + def box_blur(self, radius, n): + return radius, n + + +class TestBoxBlurApi(PillowTestCase): + + def test_imageops_box_blur(self): + i = ImageOps.box_blur(sample, 1) + self.assertEqual(i.mode, sample.mode) + self.assertEqual(i.size, sample.size) + self.assertIsInstance(i, Image.Image) + + +class TestBoxBlur(PillowTestCase): + + def box_blur(self, image, radius=1, n=1): + return image._new(image.im.box_blur(radius, n)) + + def assertImage(self, im, data, delta=0): + it = iter(im.getdata()) + for data_row in data: + im_row = [next(it) for _ in range(im.size[0])] + if any( + abs(data_v - im_v) > delta + for data_v, im_v in zip(data_row, im_row) + ): + self.assertEqual(im_row, data_row) + self.assertRaises(StopIteration, next, it) + + def assertBlur(self, im, radius, data, passes=1, delta=0): + # check grayscale image + self.assertImage(self.box_blur(im, radius, passes), data, delta) + rgba = Image.merge('RGBA', (im, im, im, im)) + for band in self.box_blur(rgba, radius, passes).split(): + self.assertImage(band, data, delta) + + def test_color_modes(self): + self.assertRaises(ValueError, self.box_blur, sample.convert("1")) + self.assertRaises(ValueError, self.box_blur, sample.convert("P")) + self.box_blur(sample.convert("L")) + self.box_blur(sample.convert("LA")) + self.assertRaises(ValueError, self.box_blur, sample.convert("I")) + self.assertRaises(ValueError, self.box_blur, sample.convert("F")) + self.box_blur(sample.convert("RGB")) + self.box_blur(sample.convert("RGBA")) + self.box_blur(sample.convert("CMYK")) + self.assertRaises(ValueError, self.box_blur, sample.convert("YCbCr")) + + def test_radius_0(self): + self.assertBlur( + sample, 0, + [ + [210, 50, 20, 10, 220, 230, 80], + [190, 210, 20, 180, 170, 40, 110], + [120, 210, 250, 60, 220, 0, 220], + [220, 40, 230, 80, 130, 250, 40], + [250, 0, 80, 30, 60, 20, 110], + ] + ) + + def test_radius_0_02(self): + self.assertBlur( + sample, 0.02, + [ + [206, 55, 20, 17, 215, 223, 83], + [189, 203, 31, 171, 169, 46, 110], + [125, 206, 241, 69, 210, 13, 210], + [215, 49, 221, 82, 131, 235, 48], + [244, 7, 80, 32, 60, 27, 107], + ], + delta=2, + ) + + def test_radius_0_05(self): + self.assertBlur( + sample, 0.05, + [ + [202, 62, 22, 27, 209, 215, 88], + [188, 194, 44, 161, 168, 56, 111], + [131, 201, 229, 81, 198, 31, 198], + [209, 62, 209, 86, 133, 216, 59], + [237, 17, 80, 36, 60, 35, 103], + ], + delta=2, + ) + + def test_radius_0_1(self): + self.assertBlur( + sample, 0.1, + [ + [196, 72, 24, 40, 200, 203, 93], + [187, 183, 62, 148, 166, 68, 111], + [139, 193, 213, 96, 182, 54, 182], + [201, 78, 193, 91, 133, 191, 73], + [227, 31, 80, 42, 61, 47, 99], + ], + delta=1, + ) + + def test_radius_0_5(self): + self.assertBlur( + sample, 0.5, + [ + [176, 101, 46, 83, 163, 165, 111], + [176, 149, 108, 122, 144, 120, 117], + [164, 171, 159, 141, 134, 119, 129], + [170, 136, 133, 114, 116, 124, 109], + [184, 95, 72, 70, 69, 81, 89], + ], + delta=1, + ) + + def test_radius_1(self): + self.assertBlur( + sample, 1, + [ + [170, 109, 63, 97, 146, 153, 116], + [168, 142, 112, 128, 126, 143, 121], + [169, 166, 142, 149, 126, 131, 114], + [159, 156, 109, 127, 94, 117, 112], + [164, 128, 63, 87, 76, 89, 90], + ], + delta=1, + ) + + def test_radius_1_5(self): + self.assertBlur( + sample, 1.5, + [ + [155, 120, 105, 112, 124, 137, 130], + [160, 136, 124, 125, 127, 134, 130], + [166, 147, 130, 125, 120, 121, 119], + [168, 145, 119, 109, 103, 105, 110], + [168, 134, 96, 85, 85, 89, 97], + ], + delta=1, + ) + + def test_radius_bigger_then_half(self): + self.assertBlur( + sample, 3, + [ + [144, 145, 142, 128, 114, 115, 117], + [148, 145, 137, 122, 109, 111, 112], + [152, 145, 131, 117, 103, 107, 108], + [156, 144, 126, 111, 97, 102, 103], + [160, 144, 121, 106, 92, 98, 99], + ], + delta=1, + ) + + def test_radius_bigger_then_width(self): + self.assertBlur( + sample, 10, + [ + [158, 153, 147, 141, 135, 129, 123], + [159, 153, 147, 141, 136, 130, 124], + [159, 154, 148, 142, 136, 130, 124], + [160, 154, 148, 142, 137, 131, 125], + [160, 155, 149, 143, 137, 131, 125], + ], + delta=0, + ) + + def test_exteme_large_radius(self): + self.assertBlur( + sample, 600, + [ + [162, 162, 162, 162, 162, 162, 162], + [162, 162, 162, 162, 162, 162, 162], + [162, 162, 162, 162, 162, 162, 162], + [162, 162, 162, 162, 162, 162, 162], + [162, 162, 162, 162, 162, 162, 162], + ], + delta=1, + ) + + def test_two_passes(self): + self.assertBlur( + sample, 1, + [ + [153, 123, 102, 109, 132, 135, 129], + [159, 138, 123, 121, 133, 131, 126], + [162, 147, 136, 124, 127, 121, 121], + [159, 140, 125, 108, 111, 106, 108], + [154, 126, 105, 87, 94, 93, 97], + ], + passes=2, + delta=1, + ) + + def test_three_passes(self): + self.assertBlur( + sample, 1, + [ + [146, 131, 116, 118, 126, 131, 130], + [151, 138, 125, 123, 126, 128, 127], + [154, 143, 129, 123, 120, 120, 119], + [152, 139, 122, 113, 108, 108, 108], + [148, 132, 112, 102, 97, 99, 100], + ], + passes=3, + delta=1, + ) + + +if __name__ == '__main__': + unittest.main() + +# End of file diff --git a/Tests/test_cffi.py b/Tests/test_cffi.py index b9f99976d..5d5427685 100644 --- a/Tests/test_cffi.py +++ b/Tests/test_cffi.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase, lena +from helper import unittest, PillowTestCase, hopper try: import cffi @@ -49,10 +49,10 @@ class TestCffi(PillowTestCase): self.skipTest("No cffi") def _test_get_access(self, im): - """ Do we get the same thing as the old pixel access """ + """Do we get the same thing as the old pixel access - """ Using private interfaces, forcing a capi access and - a pyaccess for the same image """ + Using private interfaces, forcing a capi access and + a pyaccess for the same image""" caccess = im.im.pixel_access(False) access = PyAccess.new(im, False) @@ -62,16 +62,16 @@ class TestCffi(PillowTestCase): self.assertEqual(access[(x, y)], caccess[(x, y)]) def test_get_vs_c(self): - rgb = lena('RGB') + rgb = hopper('RGB') rgb.load() self._test_get_access(rgb) - self._test_get_access(lena('RGBA')) - self._test_get_access(lena('L')) - self._test_get_access(lena('LA')) - self._test_get_access(lena('1')) - self._test_get_access(lena('P')) - # self._test_get_access(lena('PA')) # PA -- how do I make a PA image? - self._test_get_access(lena('F')) + self._test_get_access(hopper('RGBA')) + self._test_get_access(hopper('L')) + 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('F')) im = Image.new('I;16', (10, 10), 40000) self._test_get_access(im) @@ -90,10 +90,10 @@ class TestCffi(PillowTestCase): # self._test_get_access(im) def _test_set_access(self, im, color): - """ Are we writing the correct bits into the image? """ + """Are we writing the correct bits into the image? - """ Using private interfaces, forcing a capi access and - a pyaccess for the same image """ + Using private interfaces, forcing a capi access and + a pyaccess for the same image""" caccess = im.im.pixel_access(False) access = PyAccess.new(im, False) @@ -104,16 +104,16 @@ class TestCffi(PillowTestCase): self.assertEqual(color, caccess[(x, y)]) def test_set_vs_c(self): - rgb = lena('RGB') + rgb = hopper('RGB') rgb.load() self._test_set_access(rgb, (255, 128, 0)) - self._test_set_access(lena('RGBA'), (255, 192, 128, 0)) - self._test_set_access(lena('L'), 128) - self._test_set_access(lena('LA'), (128, 128)) - self._test_set_access(lena('1'), 255) - self._test_set_access(lena('P'), 128) + self._test_set_access(hopper('RGBA'), (255, 192, 128, 0)) + self._test_set_access(hopper('L'), 128) + self._test_set_access(hopper('LA'), (128, 128)) + self._test_set_access(hopper('1'), 255) + self._test_set_access(hopper('P'), 128) # self._test_set_access(i, (128, 128)) #PA -- undone how to make - self._test_set_access(lena('F'), 1024.0) + self._test_set_access(hopper('F'), 1024.0) im = Image.new('I;16', (10, 10), 40000) self._test_set_access(im, 45000) diff --git a/Tests/test_decompression_bomb.py b/Tests/test_decompression_bomb.py index 0803732ce..ae9aebfa8 100644 --- a/Tests/test_decompression_bomb.py +++ b/Tests/test_decompression_bomb.py @@ -2,7 +2,7 @@ from helper import unittest, PillowTestCase from PIL import Image -test_file = "Tests/images/lena.ppm" +TEST_FILE = "Tests/images/hopper.ppm" ORIGINAL_LIMIT = Image.MAX_IMAGE_PIXELS @@ -15,7 +15,7 @@ class TestDecompressionBomb(PillowTestCase): def test_no_warning_small_file(self): # Implicit assert: no warning. # A warning would cause a failure. - Image.open(test_file) + Image.open(TEST_FILE) def test_no_warning_no_limit(self): # Arrange @@ -26,7 +26,7 @@ class TestDecompressionBomb(PillowTestCase): # Act / Assert # Implicit assert: no warning. # A warning would cause a failure. - Image.open(test_file) + Image.open(TEST_FILE) def test_warning(self): # Arrange @@ -37,7 +37,7 @@ class TestDecompressionBomb(PillowTestCase): # Act / Assert self.assert_warning( Image.DecompressionBombWarning, - lambda: Image.open(test_file)) + lambda: Image.open(TEST_FILE)) if __name__ == '__main__': unittest.main() diff --git a/Tests/test_features.py b/Tests/test_features.py new file mode 100644 index 000000000..8da22088e --- /dev/null +++ b/Tests/test_features.py @@ -0,0 +1,35 @@ +from helper import unittest, PillowTestCase + +from PIL import features + + +class TestFeatures(PillowTestCase): + + def test_check_features(self): + for feature in features.modules: + self.assertTrue( + features.check_module(feature) in [True, False, None]) + for feature in features.codecs: + self.assertTrue(features.check_codec(feature) in [True, False]) + + def test_supported_features(self): + self.assertTrue(type(features.get_supported_modules()) is list) + self.assertTrue(type(features.get_supported_codecs()) is list) + + def test_unsupported_codec(self): + # Arrange + codec = "unsupported_codec" + # Act / Assert + self.assertRaises(ValueError, lambda: features.check_codec(codec)) + + def test_unsupported_module(self): + # Arrange + module = "unsupported_module" + # Act / Assert + self.assertRaises(ValueError, lambda: features.check_module(module)) + + +if __name__ == '__main__': + unittest.main() + +# End of file diff --git a/Tests/test_file_bmp.py b/Tests/test_file_bmp.py index e04f3642c..69792fe12 100644 --- a/Tests/test_file_bmp.py +++ b/Tests/test_file_bmp.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase, lena +from helper import unittest, PillowTestCase, hopper from PIL import Image import io @@ -18,16 +18,16 @@ class TestFileBmp(PillowTestCase): self.assertEqual(reloaded.format, "BMP") def test_sanity(self): - self.roundtrip(lena()) + self.roundtrip(hopper()) - self.roundtrip(lena("1")) - self.roundtrip(lena("L")) - self.roundtrip(lena("P")) - self.roundtrip(lena("RGB")) + self.roundtrip(hopper("1")) + self.roundtrip(hopper("L")) + self.roundtrip(hopper("P")) + self.roundtrip(hopper("RGB")) def test_save_to_bytes(self): output = io.BytesIO() - im = lena() + im = hopper() im.save(output, "BMP") output.seek(0) @@ -41,7 +41,7 @@ class TestFileBmp(PillowTestCase): dpi = (72, 72) output = io.BytesIO() - im = lena() + im = hopper() im.save(output, "BMP", dpi=dpi) output.seek(0) diff --git a/Tests/test_file_cur.py b/Tests/test_file_cur.py index 54bfe84fe..07bf3a750 100644 --- a/Tests/test_file_cur.py +++ b/Tests/test_file_cur.py @@ -16,9 +16,9 @@ class TestFileCur(PillowTestCase): self.assertEqual(im.size, (32, 32)) self.assertIsInstance(im, CurImagePlugin.CurImageFile) # Check some pixel colors to ensure image is loaded properly - self.assertEqual(im.getpixel((10, 1)), (0, 0, 0)) - self.assertEqual(im.getpixel((11, 1)), (253, 254, 254)) - self.assertEqual(im.getpixel((16, 16)), (84, 87, 86)) + self.assertEqual(im.getpixel((10, 1)), (0, 0, 0, 0)) + self.assertEqual(im.getpixel((11, 1)), (253, 254, 254, 1)) + self.assertEqual(im.getpixel((16, 16)), (84, 87, 86, 255)) if __name__ == '__main__': diff --git a/Tests/test_file_dcx.py b/Tests/test_file_dcx.py index 9a6183651..7d2ae32d8 100644 --- a/Tests/test_file_dcx.py +++ b/Tests/test_file_dcx.py @@ -1,9 +1,9 @@ -from helper import unittest, PillowTestCase, lena +from helper import unittest, PillowTestCase, hopper from PIL import Image, DcxImagePlugin -# Created with ImageMagick: convert lena.ppm lena.dcx -TEST_FILE = "Tests/images/lena.dcx" +# Created with ImageMagick: convert hopper.ppm hopper.dcx +TEST_FILE = "Tests/images/hopper.dcx" class TestFileDcx(PillowTestCase): @@ -17,7 +17,7 @@ class TestFileDcx(PillowTestCase): # Assert self.assertEqual(im.size, (128, 128)) self.assertIsInstance(im, DcxImagePlugin.DcxImageFile) - orig = lena() + orig = hopper() self.assert_image_equal(im, orig) def test_tell(self): @@ -30,6 +30,10 @@ class TestFileDcx(PillowTestCase): # Assert self.assertEqual(frame, 0) + def test_n_frames(self): + im = Image.open(TEST_FILE) + self.assertEqual(im.n_frames, 1) + def test_seek_too_far(self): # Arrange im = Image.open(TEST_FILE) diff --git a/Tests/test_file_eps.py b/Tests/test_file_eps.py index 0ca4249a3..f1fbac922 100644 --- a/Tests/test_file_eps.py +++ b/Tests/test_file_eps.py @@ -63,6 +63,17 @@ class TestFileEps(PillowTestCase): with io.open(self.tempfile('temp_iobase.eps'), 'wb') as fh: image1.save(fh, 'EPS') + def test_bytesio_object(self): + with open(file1, 'rb') as f: + img_bytes = io.BytesIO(f.read()) + + img = Image.open(img_bytes) + img.load() + + image1_scale1_compare = Image.open(file1_compare).convert("RGB") + image1_scale1_compare.load() + self.assert_image_similar(img, image1_scale1_compare, 5) + def test_render_scale1(self): # We need png support for these render test codecs = dir(Image.core) @@ -137,6 +148,97 @@ class TestFileEps(PillowTestCase): # open image with binary preview Image.open(file3) + def _test_readline(self, t, 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) + self.assertEqual(t.readline().strip('\r\n'), 'bif', ending) + + def _test_readline_stringio(self, test_string, ending): + # check all the freaking line endings possible + try: + import StringIO + except: + # don't skip, it skips everything in the parent test + return + t = StringIO.StringIO(test_string) + self._test_readline(t, ending) + + def _test_readline_io(self, test_string, ending): + if str is bytes: + t = io.StringIO(unicode(test_string)) + else: + t = io.StringIO(test_string) + self._test_readline(t, ending) + + def _test_readline_file_universal(self, test_string, ending): + f = self.tempfile('temp.txt') + with open(f, 'wb') as w: + if str is bytes: + w.write(test_string) + else: + w.write(test_string.encode('UTF-8')) + + with open(f, 'rU') as t: + self._test_readline(t, ending) + + def _test_readline_file_psfile(self, test_string, ending): + f = self.tempfile('temp.txt') + with open(f, 'wb') as w: + if str is bytes: + w.write(test_string) + else: + w.write(test_string.encode('UTF-8')) + + with open(f, 'rb') as r: + t = EpsImagePlugin.PSFile(r) + self._test_readline(t, ending) + + def test_readline(self): + # check all the freaking line endings possible from the spec + # test_string = u'something\r\nelse\n\rbaz\rbif\n' + line_endings = ['\r\n', '\n'] + not_working_endings = ['\n\r', '\r'] + strings = ['something', 'else', 'baz', 'bif'] + + for ending in line_endings: + s = ending.join(strings) + # Native Python versions will pass these endings. + # self._test_readline_stringio(s, ending) + # self._test_readline_io(s, ending) + # self._test_readline_file_universal(s, ending) + + self._test_readline_file_psfile(s, ending) + + for ending in not_working_endings: + # these only work with the PSFile, while they're in spec, + # they're not likely to be used + s = ending.join(strings) + + # Native Python versions may fail on these endings. + # self._test_readline_stringio(s, ending) + # self._test_readline_io(s, ending) + # self._test_readline_file_universal(s, ending) + + self._test_readline_file_psfile(s, ending) + + def test_open_eps(self): + # https://github.com/python-pillow/Pillow/issues/1104 + # Arrange + FILES = ["Tests/images/illu10_no_preview.eps", + "Tests/images/illu10_preview.eps", + "Tests/images/illuCS6_no_preview.eps", + "Tests/images/illuCS6_preview.eps"] + + # Act + for filename in FILES: + img = Image.open(filename) + + # Assert + self.assertEqual(img.mode, "RGB") + + if __name__ == '__main__': unittest.main() diff --git a/Tests/test_file_fli.py b/Tests/test_file_fli.py index 0c1d6e36a..04c2006c9 100644 --- a/Tests/test_file_fli.py +++ b/Tests/test_file_fli.py @@ -3,19 +3,25 @@ from helper import unittest, PillowTestCase from PIL import Image # sample ppm stream -file = "Tests/images/lena.fli" -data = open(file, "rb").read() +# created as an export of a palette image from Gimp2.6 +# save as...-> hopper.fli, default options. +test_file = "Tests/images/hopper.fli" +data = open(test_file, "rb").read() class TestFileFli(PillowTestCase): def test_sanity(self): - im = Image.open(file) + im = Image.open(test_file) im.load() self.assertEqual(im.mode, "P") self.assertEqual(im.size, (128, 128)) self.assertEqual(im.format, "FLI") + def test_n_frames(self): + im = Image.open(test_file) + self.assertEqual(im.n_frames, 2) + if __name__ == '__main__': unittest.main() diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index bd4a6e76c..0e9e65a18 100644 --- a/Tests/test_file_gif.py +++ b/Tests/test_file_gif.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase, lena, netpbm_available +from helper import unittest, PillowTestCase, hopper, netpbm_available from PIL import Image from PIL import GifImagePlugin @@ -6,8 +6,9 @@ from PIL import GifImagePlugin codecs = dir(Image.core) # sample gif stream -file = "Tests/images/lena.gif" -with open(file, "rb") as f: +TEST_GIF = "Tests/images/hopper.gif" + +with open(TEST_GIF, "rb") as f: data = f.read() @@ -18,7 +19,7 @@ class TestFileGif(PillowTestCase): self.skipTest("gif support not available") # can this happen? def test_sanity(self): - im = Image.open(file) + im = Image.open(TEST_GIF) im.load() self.assertEqual(im.mode, "P") self.assertEqual(im.size, (128, 128)) @@ -27,25 +28,34 @@ class TestFileGif(PillowTestCase): def test_optimize(self): from io import BytesIO - def test(optimize): + def test_grayscale(optimize): im = Image.new("L", (1, 1), 0) - file = BytesIO() - im.save(file, "GIF", optimize=optimize) - return len(file.getvalue()) - self.assertEqual(test(0), 800) - self.assertEqual(test(1), 38) + filename = BytesIO() + im.save(filename, "GIF", optimize=optimize) + return len(filename.getvalue()) + + def test_bilevel(optimize): + im = Image.new("1", (1, 1), 0) + test_file = BytesIO() + im.save(test_file, "GIF", optimize=optimize) + return len(test_file.getvalue()) + + self.assertEqual(test_grayscale(0), 800) + self.assertEqual(test_grayscale(1), 38) + self.assertEqual(test_bilevel(0), 800) + self.assertEqual(test_bilevel(1), 800) def test_optimize_full_l(self): from io import BytesIO im = Image.frombytes("L", (16, 16), bytes(bytearray(range(256)))) - file = BytesIO() - im.save(file, "GIF", optimize=True) + test_file = BytesIO() + im.save(test_file, "GIF", optimize=True) self.assertEqual(im.mode, "L") def test_roundtrip(self): out = self.tempfile('temp.gif') - im = lena() + im = hopper() im.save(out) reread = Image.open(out) @@ -54,20 +64,20 @@ class TestFileGif(PillowTestCase): def test_roundtrip2(self): # see https://github.com/python-pillow/Pillow/issues/403 out = self.tempfile('temp.gif') - im = Image.open('Tests/images/lena.gif') + im = Image.open(TEST_GIF) im2 = im.copy() im2.save(out) reread = Image.open(out) - self.assert_image_similar(reread.convert('RGB'), lena(), 50) + self.assert_image_similar(reread.convert('RGB'), hopper(), 50) def test_palette_handling(self): # see https://github.com/python-pillow/Pillow/issues/513 - im = Image.open('Tests/images/lena.gif') + im = Image.open(TEST_GIF) im = im.convert('RGB') - im = im.resize((100, 100), Image.ANTIALIAS) + im = im.resize((100, 100), Image.LANCZOS) im2 = im.convert('P', palette=Image.ADAPTIVE, colors=256) f = self.tempfile('temp.gif') @@ -100,7 +110,7 @@ class TestFileGif(PillowTestCase): @unittest.skipUnless(netpbm_available(), "netpbm not available") def test_save_netpbm_bmp_mode(self): - img = Image.open(file).convert("RGB") + img = Image.open(TEST_GIF).convert("RGB") tempfile = self.tempfile("temp.gif") GifImagePlugin._save_netpbm(img, 0, tempfile) @@ -108,7 +118,7 @@ class TestFileGif(PillowTestCase): @unittest.skipUnless(netpbm_available(), "netpbm not available") def test_save_netpbm_l_mode(self): - img = Image.open(file).convert("L") + img = Image.open(TEST_GIF).convert("L") tempfile = self.tempfile("temp.gif") GifImagePlugin._save_netpbm(img, 0, tempfile) @@ -120,15 +130,19 @@ class TestFileGif(PillowTestCase): try: while True: framecount += 1 - img.seek(img.tell() +1) + img.seek(img.tell() + 1) except EOFError: self.assertEqual(framecount, 5) + def test_n_frames(self): + im = Image.open("Tests/images/iss634.gif") + self.assertEqual(im.n_frames, 43) + def test_dispose_none(self): img = Image.open("Tests/images/dispose_none.gif") try: while True: - img.seek(img.tell() +1) + img.seek(img.tell() + 1) self.assertEqual(img.disposal_method, 1) except EOFError: pass @@ -137,7 +151,7 @@ class TestFileGif(PillowTestCase): img = Image.open("Tests/images/dispose_bgnd.gif") try: while True: - img.seek(img.tell() +1) + img.seek(img.tell() + 1) self.assertEqual(img.disposal_method, 2) except EOFError: pass @@ -146,7 +160,7 @@ class TestFileGif(PillowTestCase): img = Image.open("Tests/images/dispose_prev.gif") try: while True: - img.seek(img.tell() +1) + img.seek(img.tell() + 1) self.assertEqual(img.disposal_method, 3) except EOFError: pass @@ -154,10 +168,38 @@ class TestFileGif(PillowTestCase): def test_iss634(self): img = Image.open("Tests/images/iss634.gif") # seek to the second frame - img.seek(img.tell() +1) - # all transparent pixels should be replaced with the color from the first frame + img.seek(img.tell() + 1) + # all transparent pixels should be replaced with the color from the + # first frame self.assertEqual(img.histogram()[img.info['transparency']], 0) + def test_duration(self): + duration = 1000 + + out = self.tempfile('temp.gif') + fp = open(out, "wb") + im = Image.new('L', (100, 100), '#000') + for s in GifImagePlugin.getheader(im)[0] + GifImagePlugin.getdata(im, duration=duration): + fp.write(s) + fp.write(b";") + fp.close() + reread = Image.open(out) + + self.assertEqual(reread.info['duration'], duration) + + def test_number_of_loops(self): + number_of_loops = 2 + + out = self.tempfile('temp.gif') + fp = open(out, "wb") + im = Image.new('L', (100, 100), '#000') + for s in GifImagePlugin.getheader(im)[0] + GifImagePlugin.getdata(im, loop=number_of_loops): + fp.write(s) + fp.write(b";") + fp.close() + reread = Image.open(out) + + self.assertEqual(reread.info['loop'], number_of_loops) if __name__ == '__main__': unittest.main() diff --git a/Tests/test_file_gimpgradient.py b/Tests/test_file_gimpgradient.py new file mode 100644 index 000000000..c54dca7c1 --- /dev/null +++ b/Tests/test_file_gimpgradient.py @@ -0,0 +1,127 @@ +from helper import unittest, PillowTestCase + +from PIL import GimpGradientFile + + +class TestImage(PillowTestCase): + + def test_linear_pos_le_middle(self): + # Arrange + middle = 0.5 + pos = 0.25 + + # Act + ret = GimpGradientFile.linear(middle, pos) + + # Assert + self.assertEqual(ret, 0.25) + + def test_linear_pos_le_small_middle(self): + # Arrange + middle = 1e-11 + pos = 1e-12 + + # Act + ret = GimpGradientFile.linear(middle, pos) + + # Assert + self.assertEqual(ret, 0.0) + + def test_linear_pos_gt_middle(self): + # Arrange + middle = 0.5 + pos = 0.75 + + # Act + ret = GimpGradientFile.linear(middle, pos) + + # Assert + self.assertEqual(ret, 0.75) + + def test_linear_pos_gt_small_middle(self): + # Arrange + middle = 1 - 1e-11 + pos = 1 - 1e-12 + + # Act + ret = GimpGradientFile.linear(middle, pos) + + # Assert + self.assertEqual(ret, 1.0) + + def test_curved(self): + # Arrange + middle = 0.5 + pos = 0.75 + + # Act + ret = GimpGradientFile.curved(middle, pos) + + # Assert + self.assertEqual(ret, 0.75) + + def test_sine(self): + # Arrange + middle = 0.5 + pos = 0.75 + + # Act + ret = GimpGradientFile.sine(middle, pos) + + # Assert + self.assertEqual(ret, 0.8535533905932737) + + def test_sphere_increasing(self): + # Arrange + middle = 0.5 + pos = 0.75 + + # Act + ret = GimpGradientFile.sphere_increasing(middle, pos) + + # Assert + self.assert_almost_equal(ret, 0.9682458365518543) + + def test_sphere_decreasing(self): + # Arrange + middle = 0.5 + pos = 0.75 + + # Act + ret = GimpGradientFile.sphere_decreasing(middle, pos) + + # Assert + self.assertEqual(ret, 0.3385621722338523) + + def test_load_via_imagepalette(self): + # Arrange + from PIL import ImagePalette + test_file = "Tests/images/gimp_gradient.ggr" + + # Act + palette = ImagePalette.load(test_file) + + # Assert + # load returns raw palette information + self.assertEqual(len(palette[0]), 1024) + self.assertEqual(palette[1], "RGBA") + + def test_load_1_3_via_imagepalette(self): + # Arrange + from PIL import ImagePalette + # GIMP 1.3 gradient files contain a name field + test_file = "Tests/images/gimp_gradient_with_name.ggr" + + # Act + palette = ImagePalette.load(test_file) + + # Assert + # load returns raw palette information + self.assertEqual(len(palette[0]), 1024) + self.assertEqual(palette[1], "RGBA") + + +if __name__ == '__main__': + unittest.main() + +# End of file diff --git a/Tests/test_file_icns.py b/Tests/test_file_icns.py index 99f6da9e3..67eb1335f 100644 --- a/Tests/test_file_icns.py +++ b/Tests/test_file_icns.py @@ -2,9 +2,11 @@ from helper import unittest, PillowTestCase from PIL import Image +import sys + # sample icon file -file = "Tests/images/pillow.icns" -data = open(file, "rb").read() +TEST_FILE = "Tests/images/pillow.icns" +data = open(TEST_FILE, "rb").read() enable_jpeg2k = hasattr(Image.core, 'jp2klib_version') @@ -14,20 +16,34 @@ class TestFileIcns(PillowTestCase): def test_sanity(self): # Loading this icon by default should result in the largest size # (512x512@2x) being loaded - im = Image.open(file) + im = Image.open(TEST_FILE) im.load() self.assertEqual(im.mode, "RGBA") self.assertEqual(im.size, (1024, 1024)) self.assertEqual(im.format, "ICNS") + @unittest.skipIf(sys.platform != 'darwin', + "requires MacOS") + def test_save(self): + im = Image.open(TEST_FILE) + + temp_file = self.tempfile("temp.icns") + im.save(temp_file) + + reread = Image.open(temp_file) + + self.assertEqual(reread.mode, "RGBA") + self.assertEqual(reread.size, (1024, 1024)) + self.assertEqual(reread.format, "ICNS") + def test_sizes(self): # Check that we can load all of the sizes, and that the final pixel # dimensions are as expected - im = Image.open(file) + im = Image.open(TEST_FILE) for w, h, r in im.info['sizes']: wr = w * r hr = h * r - im2 = Image.open(file) + im2 = Image.open(TEST_FILE) im2.size = (w, h, r) im2.load() self.assertEqual(im2.mode, 'RGBA') diff --git a/Tests/test_file_ico.py b/Tests/test_file_ico.py index c3bf7a992..f7b52b124 100644 --- a/Tests/test_file_ico.py +++ b/Tests/test_file_ico.py @@ -1,21 +1,47 @@ -from helper import unittest, PillowTestCase +from helper import unittest, PillowTestCase, hopper +import io from PIL import Image # sample ppm stream -file = "Tests/images/lena.ico" -data = open(file, "rb").read() +TEST_ICO_FILE = "Tests/images/hopper.ico" +TEST_DATA = open(TEST_ICO_FILE, "rb").read() class TestFileIco(PillowTestCase): def test_sanity(self): - im = Image.open(file) + im = Image.open(TEST_ICO_FILE) im.load() self.assertEqual(im.mode, "RGBA") self.assertEqual(im.size, (16, 16)) self.assertEqual(im.format, "ICO") + def test_save_to_bytes(self): + output = io.BytesIO() + im = hopper() + im.save(output, "ico", sizes=[(32, 32), (64, 64)]) + + # the default image + output.seek(0) + reloaded = Image.open(output) + self.assertEqual(reloaded.info['sizes'], set([(32, 32), (64, 64)])) + + 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)) + + # the other one + output.seek(0) + reloaded = Image.open(output) + reloaded.size = (32, 32) + + 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)) + if __name__ == '__main__': unittest.main() diff --git a/Tests/test_file_im.py b/Tests/test_file_im.py new file mode 100644 index 000000000..24e00b2f0 --- /dev/null +++ b/Tests/test_file_im.py @@ -0,0 +1,33 @@ +from helper import unittest, PillowTestCase, hopper + +from PIL import Image + +# sample im +TEST_IM = "Tests/images/hopper.im" + + +class TestFileIm(PillowTestCase): + + def test_sanity(self): + im = Image.open(TEST_IM) + im.load() + self.assertEqual(im.mode, "RGB") + self.assertEqual(im.size, (128, 128)) + self.assertEqual(im.format, "IM") + + def test_n_frames(self): + im = Image.open(TEST_IM) + self.assertEqual(im.n_frames, 1) + + def test_roundtrip(self): + out = self.tempfile('temp.im') + im = hopper() + im.save(out) + reread = Image.open(out) + + self.assert_image_equal(reread, im) + +if __name__ == '__main__': + unittest.main() + +# End of file diff --git a/Tests/test_file_iptc.py b/Tests/test_file_iptc.py index bd331e5b2..84fa873be 100644 --- a/Tests/test_file_iptc.py +++ b/Tests/test_file_iptc.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase, lena +from helper import unittest, PillowTestCase, hopper from PIL import Image, IptcImagePlugin @@ -11,7 +11,7 @@ class TestFileIptc(PillowTestCase): def dummy_IptcImagePlugin(self): # Create an IptcImagePlugin object without initializing it - class FakeImage: + class FakeImage(object): pass im = FakeImage() im.__class__ = IptcImagePlugin.IptcImageFile @@ -21,7 +21,7 @@ class TestFileIptc(PillowTestCase): def test_getiptcinfo_jpg_none(self): # Arrange - im = lena() + im = hopper() # Act iptc = IptcImagePlugin.getiptcinfo(im) diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index 3bf757332..d4929dd58 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -1,8 +1,9 @@ -from helper import unittest, PillowTestCase, lena, py3 +from helper import unittest, PillowTestCase, hopper, py3 from helper import djpeg_available, cjpeg_available import random from io import BytesIO +import os from PIL import Image from PIL import ImageFile @@ -10,7 +11,7 @@ from PIL import JpegImagePlugin codecs = dir(Image.core) -test_file = "Tests/images/lena.jpg" +TEST_FILE = "Tests/images/hopper.jpg" class TestFileJpeg(PillowTestCase): @@ -22,10 +23,10 @@ class TestFileJpeg(PillowTestCase): def roundtrip(self, im, **options): out = BytesIO() im.save(out, "JPEG", **options) - bytes = out.tell() + test_bytes = out.tell() out.seek(0) im = Image.open(out) - im.bytes = bytes # for testing only + im.bytes = test_bytes # for testing only return im def test_sanity(self): @@ -33,7 +34,7 @@ class TestFileJpeg(PillowTestCase): # internal version number self.assertRegexpMatches(Image.core.jpeglib_version, "\d+\.\d+$") - im = Image.open(test_file) + im = Image.open(TEST_FILE) im.load() self.assertEqual(im.mode, "RGB") self.assertEqual(im.size, (128, 128)) @@ -41,11 +42,12 @@ class TestFileJpeg(PillowTestCase): def test_app(self): # Test APP/COM reader (@PIL135) - im = Image.open(test_file) + im = Image.open(TEST_FILE) self.assertEqual( im.applist[0], - ("APP0", b"JFIF\x00\x01\x01\x00\x00\x01\x00\x01\x00\x00")) - self.assertEqual(im.applist[1], ("COM", b"Python Imaging Library")) + ("APP0", b"JFIF\x00\x01\x01\x01\x00`\x00`\x00\x00")) + self.assertEqual(im.applist[1], ( + "COM", b"File written by Adobe Photoshop\xa8 4.0\x00")) self.assertEqual(len(im.applist), 2) def test_cmyk(self): @@ -60,7 +62,8 @@ class TestFileJpeg(PillowTestCase): self.assertGreater(y, 0.8) self.assertEqual(k, 0.0) # the opposite corner is black - c, m, y, k = [x / 255.0 for x in im.getpixel((im.size[0]-1, im.size[1]-1))] + c, m, y, k = [x / 255.0 for x in im.getpixel(( + im.size[0]-1, im.size[1]-1))] self.assertGreater(k, 0.9) # roundtrip, and check again im = self.roundtrip(im) @@ -69,12 +72,13 @@ class TestFileJpeg(PillowTestCase): self.assertGreater(m, 0.8) self.assertGreater(y, 0.8) self.assertEqual(k, 0.0) - c, m, y, k = [x / 255.0 for x in im.getpixel((im.size[0]-1, im.size[1]-1))] + c, m, y, k = [x / 255.0 for x in im.getpixel(( + im.size[0]-1, im.size[1]-1))] self.assertGreater(k, 0.9) def test_dpi(self): def test(xdpi, ydpi=None): - im = Image.open(test_file) + im = Image.open(TEST_FILE) im = self.roundtrip(im, dpi=(xdpi, ydpi or xdpi)) return im.info.get("dpi") self.assertEqual(test(72), (72, 72)) @@ -93,8 +97,8 @@ class TestFileJpeg(PillowTestCase): im2 = Image.open(f) self.assertEqual(im2.info.get("icc_profile"), icc_profile) # Roundtrip via memory buffer. - im1 = self.roundtrip(lena()) - im2 = self.roundtrip(lena(), icc_profile=icc_profile) + im1 = self.roundtrip(hopper()) + im2 = self.roundtrip(hopper(), icc_profile=icc_profile) self.assert_image_equal(im1, im2) self.assertFalse(im1.info.get("icc_profile")) self.assertTrue(im2.info.get("icc_profile")) @@ -107,7 +111,7 @@ class TestFileJpeg(PillowTestCase): # order issues. icc_profile = (b"Test"*int(n/4+1))[:n] assert len(icc_profile) == n # sanity - im1 = self.roundtrip(lena(), icc_profile=icc_profile) + im1 = self.roundtrip(hopper(), icc_profile=icc_profile) self.assertEqual(im1.info.get("icc_profile"), icc_profile or None) test(0) test(1) @@ -121,8 +125,8 @@ class TestFileJpeg(PillowTestCase): test(ImageFile.MAXBLOCK*4+3) # large block def test_optimize(self): - im1 = self.roundtrip(lena()) - im2 = self.roundtrip(lena(), optimize=1) + im1 = self.roundtrip(hopper()) + im2 = self.roundtrip(hopper(), optimize=1) self.assert_image_equal(im1, im2) self.assertGreaterEqual(im1.bytes, im2.bytes) @@ -134,8 +138,8 @@ class TestFileJpeg(PillowTestCase): im.save(f, format="JPEG", optimize=True) def test_progressive(self): - im1 = self.roundtrip(lena()) - im2 = self.roundtrip(lena(), progressive=True) + im1 = self.roundtrip(hopper()) + im2 = self.roundtrip(hopper(), progressive=True) self.assert_image_equal(im1, im2) self.assertGreaterEqual(im1.bytes, im2.bytes) @@ -150,7 +154,8 @@ class TestFileJpeg(PillowTestCase): if py3: a = bytes(random.randint(0, 255) for _ in range(256 * 256 * 3)) else: - a = b''.join(chr(random.randint(0, 255)) for _ in range(256 * 256 * 3)) + a = b''.join(chr(random.randint(0, 255)) for _ in range( + 256 * 256 * 3)) im = Image.frombuffer("RGB", (256, 256), a, "raw", "RGB", 0, 1) # this requires more bytes than pixels in the image im.save(f, format="JPEG", progressive=True, quality=100) @@ -158,13 +163,13 @@ class TestFileJpeg(PillowTestCase): def test_large_exif(self): # https://github.com/python-pillow/Pillow/issues/148 f = self.tempfile('temp.jpg') - im = lena() + im = hopper() im.save(f, 'JPEG', quality=90, exif=b"1"*65532) def test_progressive_compat(self): - im1 = self.roundtrip(lena()) - im2 = self.roundtrip(lena(), progressive=1) - im3 = self.roundtrip(lena(), progression=1) # compatibility + im1 = self.roundtrip(hopper()) + im2 = self.roundtrip(hopper(), progressive=1) + im3 = self.roundtrip(hopper(), progression=1) # compatibility self.assert_image_equal(im1, im2) self.assert_image_equal(im1, im3) self.assertFalse(im1.info.get("progressive")) @@ -175,14 +180,14 @@ class TestFileJpeg(PillowTestCase): self.assertTrue(im3.info.get("progression")) def test_quality(self): - im1 = self.roundtrip(lena()) - im2 = self.roundtrip(lena(), quality=50) + im1 = self.roundtrip(hopper()) + im2 = self.roundtrip(hopper(), quality=50) self.assert_image(im1, im2.mode, im2.size) self.assertGreaterEqual(im1.bytes, im2.bytes) def test_smooth(self): - im1 = self.roundtrip(lena()) - im2 = self.roundtrip(lena(), smooth=100) + im1 = self.roundtrip(hopper()) + im2 = self.roundtrip(hopper(), smooth=100) self.assert_image(im1, im2.mode, im2.size) def test_subsampling(self): @@ -190,26 +195,26 @@ class TestFileJpeg(PillowTestCase): layer = im.layer return layer[0][1:3] + layer[1][1:3] + layer[2][1:3] # experimental API - im = self.roundtrip(lena(), subsampling=-1) # default + im = self.roundtrip(hopper(), subsampling=-1) # default self.assertEqual(getsampling(im), (2, 2, 1, 1, 1, 1)) - im = self.roundtrip(lena(), subsampling=0) # 4:4:4 + im = self.roundtrip(hopper(), subsampling=0) # 4:4:4 self.assertEqual(getsampling(im), (1, 1, 1, 1, 1, 1)) - im = self.roundtrip(lena(), subsampling=1) # 4:2:2 + im = self.roundtrip(hopper(), subsampling=1) # 4:2:2 self.assertEqual(getsampling(im), (2, 1, 1, 1, 1, 1)) - im = self.roundtrip(lena(), subsampling=2) # 4:1:1 + im = self.roundtrip(hopper(), subsampling=2) # 4:1:1 self.assertEqual(getsampling(im), (2, 2, 1, 1, 1, 1)) - im = self.roundtrip(lena(), subsampling=3) # default (undefined) + im = self.roundtrip(hopper(), subsampling=3) # default (undefined) self.assertEqual(getsampling(im), (2, 2, 1, 1, 1, 1)) - im = self.roundtrip(lena(), subsampling="4:4:4") + im = self.roundtrip(hopper(), subsampling="4:4:4") self.assertEqual(getsampling(im), (1, 1, 1, 1, 1, 1)) - im = self.roundtrip(lena(), subsampling="4:2:2") + im = self.roundtrip(hopper(), subsampling="4:2:2") self.assertEqual(getsampling(im), (2, 1, 1, 1, 1, 1)) - im = self.roundtrip(lena(), subsampling="4:1:1") + im = self.roundtrip(hopper(), subsampling="4:1:1") self.assertEqual(getsampling(im), (2, 2, 1, 1, 1, 1)) self.assertRaises( - TypeError, lambda: self.roundtrip(lena(), subsampling="1:1:1")) + TypeError, lambda: self.roundtrip(hopper(), subsampling="1:1:1")) def test_exif(self): im = Image.open("Tests/images/pil_sample_rgb.jpg") @@ -221,7 +226,16 @@ class TestFileJpeg(PillowTestCase): self.assertIsNone(im._getmp()) def test_quality_keep(self): - im = Image.open("Tests/images/lena.jpg") + # RGB + im = Image.open("Tests/images/hopper.jpg") + f = self.tempfile('temp.jpg') + im.save(f, quality='keep') + # Grayscale + im = Image.open("Tests/images/hopper_gray.jpg") + f = self.tempfile('temp.jpg') + im.save(f, quality='keep') + # CMYK + im = Image.open("Tests/images/pil_sample_cmyk.jpg") f = self.tempfile('temp.jpg') im.save(f, quality='keep') @@ -231,15 +245,17 @@ class TestFileJpeg(PillowTestCase): Image.open(filename) def test_qtables(self): - im = Image.open("Tests/images/lena.jpg") + im = Image.open("Tests/images/hopper.jpg") qtables = im.quantization reloaded = self.roundtrip(im, qtables=qtables, subsampling=0) self.assertEqual(im.quantization, reloaded.quantization) - self.assert_image_similar(im, self.roundtrip(im, qtables='web_low'), 30) - self.assert_image_similar(im, self.roundtrip(im, qtables='web_high'), 30) + self.assert_image_similar(im, self.roundtrip(im, qtables='web_low'), + 30) + self.assert_image_similar(im, self.roundtrip(im, qtables='web_high'), + 30) self.assert_image_similar(im, self.roundtrip(im, qtables='keep'), 30) - #values from wizard.txt in jpeg9-a src package. + # values from wizard.txt in jpeg9-a src package. standard_l_qtable = [int(s) for s in """ 16 11 10 16 24 40 51 61 12 12 14 19 26 58 60 55 @@ -251,7 +267,7 @@ class TestFileJpeg(PillowTestCase): 72 92 95 98 112 100 103 99 """.split(None)] - standard_chrominance_qtable= [int(s) for s in """ + standard_chrominance_qtable = [int(s) for s in """ 17 18 24 47 99 99 99 99 18 21 26 66 99 99 99 99 24 26 56 99 99 99 99 99 @@ -262,38 +278,77 @@ class TestFileJpeg(PillowTestCase): 99 99 99 99 99 99 99 99 """.split(None)] # list of qtable lists - self.assert_image_similar(im, - self.roundtrip(im, - qtables=[standard_l_qtable, - standard_chrominance_qtable]), - 30) + self.assert_image_similar( + im, self.roundtrip( + im, qtables=[standard_l_qtable, standard_chrominance_qtable]), + 30) + # tuple of qtable lists - self.assert_image_similar(im, - self.roundtrip(im, - qtables=(standard_l_qtable, - standard_chrominance_qtable)), - 30) + self.assert_image_similar( + im, self.roundtrip( + im, qtables=(standard_l_qtable, standard_chrominance_qtable)), + 30) + # dict of qtable lists self.assert_image_similar(im, self.roundtrip(im, - qtables={0:standard_l_qtable, - 1:standard_chrominance_qtable}), + 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])) + + # 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]])) + @unittest.skipUnless(djpeg_available(), "djpeg not available") def test_load_djpeg(self): - img = Image.open(test_file) + img = Image.open(TEST_FILE) img.load_djpeg() - self.assert_image_similar(img, Image.open(test_file), 0) + self.assert_image_similar(img, Image.open(TEST_FILE), 0) @unittest.skipUnless(cjpeg_available(), "cjpeg not available") def test_save_cjpeg(self): - img = Image.open(test_file) + img = Image.open(TEST_FILE) tempfile = self.tempfile("temp.jpg") JpegImagePlugin._save_cjpeg(img, 0, tempfile) # Default save quality is 75%, so a tiny bit of difference is alright - self.assert_image_similar(img, Image.open(tempfile), 1) + self.assert_image_similar(img, Image.open(tempfile), 17) + + def test_no_duplicate_0x1001_tag(self): + # Arrange + from PIL import ExifTags + tag_ids = dict(zip(ExifTags.TAGS.values(), ExifTags.TAGS.keys())) + + # Assert + self.assertEqual(tag_ids['RelatedImageWidth'], 0x1001) + self.assertEqual(tag_ids['RelatedImageLength'], 0x1002) + + def test_MAXBLOCK_scaling(self): + def gen_random_image(size): + """ Generates a very hard to compress file + :param size: tuple + """ + return Image.frombytes('RGB', size, os.urandom(size[0]*size[1] * 3)) + + im = gen_random_image((512, 512)) + f = self.tempfile("temp.jpeg") + im.save(f, quality=100, optimize=True) + + reloaded = Image.open(f) + + # none of these should crash + reloaded.save(f, quality='keep') + reloaded.save(f, quality='keep', progressive=True) + reloaded.save(f, quality='keep', optimize=True) if __name__ == '__main__': diff --git a/Tests/test_file_jpeg2k.py b/Tests/test_file_jpeg2k.py index a0e7dfb53..9768a881d 100644 --- a/Tests/test_file_jpeg2k.py +++ b/Tests/test_file_jpeg2k.py @@ -22,10 +22,10 @@ class TestFileJpeg2k(PillowTestCase): def roundtrip(self, im, **options): out = BytesIO() im.save(out, "JPEG2000", **options) - bytes = out.tell() + test_bytes = out.tell() out.seek(0) im = Image.open(out) - im.bytes = bytes # for testing only + im.bytes = test_bytes # for testing only im.load() return im @@ -52,7 +52,8 @@ class TestFileJpeg2k(PillowTestCase): def test_lossless(self): im = Image.open('Tests/images/test-card-lossless.jp2') im.load() - im.save('/tmp/test-card.png') + outfile = self.tempfile('temp_test-card.png') + im.save(outfile) self.assert_image_similar(im, test_card, 1.0e-3) def test_lossy_tiled(self): diff --git a/Tests/test_file_libtiff.py b/Tests/test_file_libtiff.py index 60eea8b3b..896038b9d 100644 --- a/Tests/test_file_libtiff.py +++ b/Tests/test_file_libtiff.py @@ -1,11 +1,13 @@ -from helper import unittest, PillowTestCase, lena, py3 +from __future__ import print_function +from helper import unittest, PillowTestCase, hopper, py3 import os +import io from PIL import Image, TiffImagePlugin -class TestFileLibTiff(PillowTestCase): +class LibTiffTestCase(PillowTestCase): def setUp(self): codecs = dir(Image.core) @@ -26,31 +28,34 @@ class TestFileLibTiff(PillowTestCase): self.assertEqual(im._compression, 'group4') except: print("No _compression") - print (dir(im)) + print(dir(im)) # can we write it back out, in a different form. out = self.tempfile("temp.png") im.save(out) + +class TestFileLibTiff(LibTiffTestCase): + def test_g4_tiff(self): """Test the ordinary file path load path""" - file = "Tests/images/lena_g4_500.tif" - im = Image.open(file) + test_file = "Tests/images/hopper_g4_500.tif" + im = Image.open(test_file) self.assertEqual(im.size, (500, 500)) self._assert_noerr(im) def test_g4_large(self): - file = "Tests/images/pport_g4.tif" - im = Image.open(file) + test_file = "Tests/images/pport_g4.tif" + im = Image.open(test_file) self._assert_noerr(im) def test_g4_tiff_file(self): """Testing the string load path""" - file = "Tests/images/lena_g4_500.tif" - with open(file, 'rb') as f: + test_file = "Tests/images/hopper_g4_500.tif" + with open(test_file, 'rb') as f: im = Image.open(f) self.assertEqual(im.size, (500, 500)) @@ -58,10 +63,9 @@ class TestFileLibTiff(PillowTestCase): def test_g4_tiff_bytesio(self): """Testing the stringio loading code path""" - from io import BytesIO - file = "Tests/images/lena_g4_500.tif" - s = BytesIO() - with open(file, 'rb') as f: + test_file = "Tests/images/hopper_g4_500.tif" + s = io.BytesIO() + with open(test_file, 'rb') as f: s.write(f.read()) s.seek(0) im = Image.open(s) @@ -71,8 +75,8 @@ class TestFileLibTiff(PillowTestCase): def test_g4_eq_png(self): """ Checking that we're actually getting the data that we expect""" - png = Image.open('Tests/images/lena_bw_500.png') - g4 = Image.open('Tests/images/lena_g4_500.tif') + png = Image.open('Tests/images/hopper_bw_500.png') + g4 = Image.open('Tests/images/hopper_g4_500.tif') self.assert_image_equal(g4, png) @@ -86,8 +90,8 @@ class TestFileLibTiff(PillowTestCase): def test_g4_write(self): """Checking to see that the saved image is the same as what we wrote""" - file = "Tests/images/lena_g4_500.tif" - orig = Image.open(file) + test_file = "Tests/images/hopper_g4_500.tif" + orig = Image.open(test_file) out = self.tempfile("temp.tif") rot = orig.transpose(Image.ROTATE_90) @@ -105,8 +109,8 @@ class TestFileLibTiff(PillowTestCase): self.assertNotEqual(orig.tobytes(), reread.tobytes()) def test_adobe_deflate_tiff(self): - file = "Tests/images/tiff_adobe_deflate.tif" - im = Image.open(file) + test_file = "Tests/images/tiff_adobe_deflate.tif" + im = Image.open(test_file) self.assertEqual(im.mode, "RGB") self.assertEqual(im.size, (278, 374)) @@ -116,7 +120,7 @@ class TestFileLibTiff(PillowTestCase): def test_write_metadata(self): """ Test metadata writing through libtiff """ - img = Image.open('Tests/images/lena_g4.tif') + img = Image.open('Tests/images/hopper_g4.tif') f = self.tempfile('temp.tiff') img.save(f, tiffinfo=img.tag) @@ -155,7 +159,7 @@ class TestFileLibTiff(PillowTestCase): value, reloaded[tag], "%s didn't roundtrip" % tag) def test_g3_compression(self): - i = Image.open('Tests/images/lena_g4_500.tif') + i = Image.open('Tests/images/hopper_g4_500.tif') out = self.tempfile("temp.tif") i.save(out, compression='group3') @@ -212,8 +216,8 @@ class TestFileLibTiff(PillowTestCase): def test_g4_string_info(self): """Tests String data in info directory""" - file = "Tests/images/lena_g4_500.tif" - orig = Image.open(file) + test_file = "Tests/images/hopper_g4_500.tif" + orig = Image.open(test_file) out = self.tempfile("temp.tif") @@ -240,13 +244,13 @@ class TestFileLibTiff(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(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))) + print(im2.getpixel((0, 0))) + print(im2.getpixel((0, 1))) + print(im2.getpixel((0, 2))) self.assert_image_equal(im, im2) @@ -267,7 +271,7 @@ class TestFileLibTiff(PillowTestCase): self.assert_image_equal(im, im2) def test_compressions(self): - im = lena('RGB') + im = hopper('RGB') out = self.tempfile('temp.tif') for compression in ('packbits', 'tiff_lzw'): @@ -280,19 +284,19 @@ class TestFileLibTiff(PillowTestCase): self.assert_image_similar(im, im2, 30) def test_cmyk_save(self): - im = lena('CMYK') + im = hopper('CMYK') out = self.tempfile('temp.tif') im.save(out, compression='tiff_adobe_deflate') im2 = Image.open(out) self.assert_image_equal(im, im2) - def xtest_bw_compression_wRGB(self): + def xtest_bw_compression_w_rgb(self): """ This test passes, but when running all tests causes a failure due to output on stderr from the error thrown by libtiff. We need to capture that but not now""" - im = lena('RGB') + im = hopper('RGB') out = self.tempfile('temp.tif') self.assertRaises( @@ -301,7 +305,7 @@ class TestFileLibTiff(PillowTestCase): self.assertRaises(IOError, lambda: im.save(out, compression='group4')) def test_fp_leak(self): - im = Image.open("Tests/images/lena_g4_500.tif") + im = Image.open("Tests/images/hopper_g4_500.tif") fn = im.fp.fileno() os.fstat(fn) @@ -311,6 +315,78 @@ class TestFileLibTiff(PillowTestCase): self.assertRaises(OSError, lambda: os.fstat(fn)) self.assertRaises(OSError, lambda: os.close(fn)) + def test_multipage(self): + # issue #862 + TiffImagePlugin.READ_LIBTIFF = True + im = Image.open('Tests/images/multipage.tiff') + # file is a multipage tiff, 10x10 green, 10x10 red, 20x20 blue + + im.seek(0) + self.assertEqual(im.size, (10, 10)) + self.assertEqual(im.convert('RGB').getpixel((0, 0)), (0, 128, 0)) + self.assertTrue(im.tag.next) + + im.seek(1) + self.assertEqual(im.size, (10, 10)) + self.assertEqual(im.convert('RGB').getpixel((0, 0)), (255, 0, 0)) + self.assertTrue(im.tag.next) + + im.seek(2) + self.assertFalse(im.tag.next) + self.assertEqual(im.size, (20, 20)) + self.assertEqual(im.convert('RGB').getpixel((0, 0)), (0, 0, 255)) + + TiffImagePlugin.READ_LIBTIFF = False + + def test__next(self): + TiffImagePlugin.READ_LIBTIFF = True + im = Image.open('Tests/images/hopper.tif') + self.assertFalse(im.tag.next) + im.load() + self.assertFalse(im.tag.next) + + def test_4bit(self): + # Arrange + test_file = "Tests/images/hopper_gray_4bpp.tif" + original = hopper("L") + + # Act + TiffImagePlugin.READ_LIBTIFF = True + im = Image.open(test_file) + TiffImagePlugin.READ_LIBTIFF = False + + # Assert + self.assertEqual(im.size, (128, 128)) + self.assertEqual(im.mode, "L") + self.assert_image_similar(im, original, 7.3) + + def test_save_bytesio(self): + # PR 1011 + # Test TIFF saving to io.BytesIO() object. + + TiffImagePlugin.WRITE_LIBTIFF = True + TiffImagePlugin.READ_LIBTIFF = True + + # Generate test image + pilim = hopper() + + def save_bytesio(compression=None): + + buffer_io = io.BytesIO() + pilim.save(buffer_io, format="tiff", compression=compression) + buffer_io.seek(0) + + pilim_load = Image.open(buffer_io) + self.assert_image_similar(pilim, pilim_load, 0) + + # save_bytesio() + save_bytesio('raw') + save_bytesio("packbits") + save_bytesio("tiff_lzw") + + TiffImagePlugin.WRITE_LIBTIFF = False + TiffImagePlugin.READ_LIBTIFF = False + if __name__ == '__main__': unittest.main() diff --git a/Tests/test_file_libtiff_small.py b/Tests/test_file_libtiff_small.py index 043ecaf3f..c6a639ae9 100644 --- a/Tests/test_file_libtiff_small.py +++ b/Tests/test_file_libtiff_small.py @@ -2,12 +2,10 @@ from helper import unittest from PIL import Image -from test_file_libtiff import TestFileLibTiff +from test_file_libtiff import LibTiffTestCase -class TestFileLibTiffSmall(TestFileLibTiff): - - # Inherits TestFileLibTiff's setUp() and self._assert_noerr() +class TestFileLibTiffSmall(LibTiffTestCase): """ The small lena image was failing on open in the libtiff decoder because the file pointer was set to the wrong place @@ -17,22 +15,22 @@ class TestFileLibTiffSmall(TestFileLibTiff): file just before reading in libtiff. These tests remain to ensure that it stays fixed. """ - def test_g4_lena_file(self): + def test_g4_hopper_file(self): """Testing the open file load path""" - file = "Tests/images/lena_g4.tif" - with open(file, 'rb') as f: + test_file = "Tests/images/hopper_g4.tif" + with open(test_file, 'rb') as f: im = Image.open(f) self.assertEqual(im.size, (128, 128)) self._assert_noerr(im) - def test_g4_lena_bytesio(self): + def test_g4_hopper_bytesio(self): """Testing the bytesio loading code path""" from io import BytesIO - file = "Tests/images/lena_g4.tif" + test_file = "Tests/images/hopper_g4.tif" s = BytesIO() - with open(file, 'rb') as f: + with open(test_file, 'rb') as f: s.write(f.read()) s.seek(0) im = Image.open(s) @@ -40,11 +38,11 @@ class TestFileLibTiffSmall(TestFileLibTiff): self.assertEqual(im.size, (128, 128)) self._assert_noerr(im) - def test_g4_lena(self): - """The 128x128 lena image fails for some reason. Investigating""" + def test_g4_hopper(self): + """The 128x128 lena image failed for some reason.""" - file = "Tests/images/lena_g4.tif" - im = Image.open(file) + test_file = "Tests/images/hopper_g4.tif" + im = Image.open(test_file) self.assertEqual(im.size, (128, 128)) self._assert_noerr(im) diff --git a/Tests/test_file_mpo.py b/Tests/test_file_mpo.py index a221eec15..1a0ebc453 100644 --- a/Tests/test_file_mpo.py +++ b/Tests/test_file_mpo.py @@ -17,10 +17,10 @@ class TestFileMpo(PillowTestCase): # Note that for now, there is no MPO saving functionality out = BytesIO() im.save(out, "MPO", **options) - bytes = out.tell() + test_bytes = out.tell() out.seek(0) im = Image.open(out) - im.bytes = bytes # for testing only + im.bytes = test_bytes # for testing only return im def test_sanity(self): @@ -38,7 +38,7 @@ class TestFileMpo(PillowTestCase): self.assertEqual(im.applist[0][0], 'APP1') self.assertEqual(im.applist[1][0], 'APP2') self.assertEqual(im.applist[1][1][:16], - b'MPF\x00MM\x00*\x00\x00\x00\x08\x00\x03\xb0\x00') + b'MPF\x00MM\x00*\x00\x00\x00\x08\x00\x03\xb0\x00') self.assertEqual(len(im.applist), 2) def test_exif(self): @@ -55,7 +55,7 @@ class TestFileMpo(PillowTestCase): mpinfo = im._getmp() self.assertEqual(mpinfo[45056], b'0100') self.assertEqual(mpinfo[45057], 2) - + def test_mp_attribute(self): for test_file in test_files: im = Image.open(test_file) @@ -71,7 +71,7 @@ class TestFileMpo(PillowTestCase): self.assertFalse(mpattr['DependentChildImageFlag']) self.assertEqual(mpattr['ImageDataFormat'], 'JPEG') self.assertEqual(mpattr['MPType'], - 'Multi-Frame Image: (Disparity)') + 'Multi-Frame Image: (Disparity)') self.assertEqual(mpattr['Reserved'], 0) frameNumber += 1 @@ -82,7 +82,8 @@ class TestFileMpo(PillowTestCase): # prior to first image raises an error, both blatant and borderline self.assertRaises(EOFError, im.seek, -1) self.assertRaises(EOFError, im.seek, -523) - # after the final image raises an error, both blatant and borderline + # after the final image raises an error, + # both blatant and borderline self.assertRaises(EOFError, im.seek, 2) self.assertRaises(EOFError, im.seek, 523) # bad calls shouldn't change the frame @@ -93,7 +94,11 @@ class TestFileMpo(PillowTestCase): # and this one, too im.seek(0) self.assertEqual(im.tell(), 0) - + + def test_n_frames(self): + im = Image.open("Tests/images/sugarshack.mpo") + self.assertEqual(im.n_frames, 2) + def test_image_grab(self): for test_file in test_files: im = Image.open(test_file) diff --git a/Tests/test_file_msp.py b/Tests/test_file_msp.py index a64faad10..f4b1af75e 100644 --- a/Tests/test_file_msp.py +++ b/Tests/test_file_msp.py @@ -1,15 +1,16 @@ -from helper import unittest, PillowTestCase, lena +from helper import unittest, PillowTestCase, hopper from PIL import Image +TEST_FILE = "Tests/images/hopper.msp" + class TestFileMsp(PillowTestCase): def test_sanity(self): - file = self.tempfile("temp.msp") - lena("1").save(file) + hopper("1").save(file) im = Image.open(file) im.load() @@ -17,6 +18,23 @@ class TestFileMsp(PillowTestCase): self.assertEqual(im.size, (128, 128)) self.assertEqual(im.format, "MSP") + def test_open(self): + # Arrange + # Act + im = Image.open(TEST_FILE) + + # Assert + self.assertEqual(im.size, (128, 128)) + self.assert_image_similar(im, hopper("1"), 4) + + def test_cannot_save_wrong_mode(self): + # Arrange + im = hopper() + filename = self.tempfile("temp.msp") + + # Act/Assert + self.assertRaises(IOError, lambda: im.save(filename)) + if __name__ == '__main__': unittest.main() diff --git a/Tests/test_file_palm.py b/Tests/test_file_palm.py index 9598b8cbc..99847e04d 100644 --- a/Tests/test_file_palm.py +++ b/Tests/test_file_palm.py @@ -1,14 +1,14 @@ -from helper import unittest, PillowTestCase, lena, imagemagick_available +from helper import unittest, PillowTestCase, hopper, imagemagick_available import os.path class TestFilePalm(PillowTestCase): _roundtrip = imagemagick_available() - + def helper_save_as_palm(self, mode): # Arrange - im = lena(mode) + im = hopper(mode) outfile = self.tempfile("temp_" + mode + ".palm") # Act @@ -21,17 +21,13 @@ class TestFilePalm(PillowTestCase): def roundtrip(self, mode): if not self._roundtrip: return - - im = lena(mode) + + im = hopper(mode) outfile = self.tempfile("temp.palm") im.save(outfile) - try: - converted = self.open_withImagemagick(outfile) - self.assert_image_equal(converted, im) - except IOError: - pass - + converted = self.open_withImagemagick(outfile) + self.assert_image_equal(converted, im) def test_monochrome(self): # Arrange @@ -49,7 +45,7 @@ class TestFilePalm(PillowTestCase): self.helper_save_as_palm(mode) self.skipKnownBadTest("Palm P image is wrong") self.roundtrip(mode) - + def test_rgb_ioerror(self): # Arrange mode = "RGB" diff --git a/Tests/test_file_pcx.py b/Tests/test_file_pcx.py index f278bd91d..10d17d349 100644 --- a/Tests/test_file_pcx.py +++ b/Tests/test_file_pcx.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase, lena +from helper import unittest, PillowTestCase, hopper from PIL import Image @@ -17,7 +17,7 @@ class TestFilePcx(PillowTestCase): def test_sanity(self): for mode in ('1', 'L', 'P', 'RGB'): - self._roundtrip(lena(mode)) + self._roundtrip(hopper(mode)) def test_odd(self): # see issue #523, odd sized images should have a stride that's even. @@ -26,13 +26,13 @@ class TestFilePcx(PillowTestCase): for mode in ('1', 'L', 'P', 'RGB'): # larger, odd sized images are better here to ensure that # we handle interrupted scan lines properly. - self._roundtrip(lena(mode).resize((511, 511))) + self._roundtrip(hopper(mode).resize((511, 511))) def test_pil184(self): # Check reading of files where xmin/xmax is not zero. - file = "Tests/images/pil184.pcx" - im = Image.open(file) + test_file = "Tests/images/pil184.pcx" + im = Image.open(test_file) self.assertEqual(im.size, (447, 144)) self.assertEqual(im.tile[0][1], (0, 0, 447, 144)) diff --git a/Tests/test_file_pdf.py b/Tests/test_file_pdf.py index 689302bb5..9424bc09d 100644 --- a/Tests/test_file_pdf.py +++ b/Tests/test_file_pdf.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase, lena +from helper import unittest, PillowTestCase, hopper import os.path @@ -7,7 +7,7 @@ class TestFilePdf(PillowTestCase): def helper_save_as_pdf(self, mode): # Arrange - im = lena(mode) + im = hopper(mode) outfile = self.tempfile("temp_" + mode + ".pdf") # Act diff --git a/Tests/test_file_png.py b/Tests/test_file_png.py index 8ef166347..f438e24cc 100644 --- a/Tests/test_file_png.py +++ b/Tests/test_file_png.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase, lena +from helper import unittest, PillowTestCase, hopper from io import BytesIO @@ -10,8 +10,8 @@ codecs = dir(Image.core) # sample png stream -file = "Tests/images/lena.png" -data = open(file, "rb").read() +TEST_PNG_FILE = "Tests/images/hopper.png" +TEST_DATA = open(TEST_PNG_FILE, "rb").read() # stuff to create inline PNG images @@ -19,9 +19,9 @@ MAGIC = PngImagePlugin._MAGIC def chunk(cid, *data): - file = BytesIO() - PngImagePlugin.putchunk(*(file, cid) + data) - return file.getvalue() + test_file = BytesIO() + PngImagePlugin.putchunk(*(test_file, cid) + data) + return test_file.getvalue() o32 = PngImagePlugin.o32 @@ -56,37 +56,37 @@ class TestFilePng(PillowTestCase): self.assertRegexpMatches( Image.core.zlib_version, "\d+\.\d+\.\d+(\.\d+)?$") - file = self.tempfile("temp.png") + test_file = self.tempfile("temp.png") - lena("RGB").save(file) + hopper("RGB").save(test_file) - im = Image.open(file) + im = Image.open(test_file) im.load() self.assertEqual(im.mode, "RGB") self.assertEqual(im.size, (128, 128)) self.assertEqual(im.format, "PNG") - lena("1").save(file) - im = Image.open(file) + hopper("1").save(test_file) + im = Image.open(test_file) - lena("L").save(file) - im = Image.open(file) + hopper("L").save(test_file) + im = Image.open(test_file) - lena("P").save(file) - im = Image.open(file) + hopper("P").save(test_file) + im = Image.open(test_file) - lena("RGB").save(file) - im = Image.open(file) + hopper("RGB").save(test_file) + im = Image.open(test_file) - lena("I").save(file) - im = Image.open(file) + hopper("I").save(test_file) + im = Image.open(test_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. - file = "Tests/images/broken.png" - self.assertRaises(IOError, lambda: Image.open(file)) + test_file = "Tests/images/broken.png" + self.assertRaises(IOError, lambda: Image.open(test_file)) def test_bad_text(self): # Make sure PIL can read malformed tEXt chunks (@PIL152) @@ -151,29 +151,32 @@ class TestFilePng(PillowTestCase): self.assertEqual(im.info["spam"].lang, "en") self.assertEqual(im.info["spam"].tkey, "Spam") - im = load(HEAD + chunk(b'iTXt', b'spam\0\1\0en\0Spam\0' + zlib.compress(b"egg")[:1]) + TAIL) + im = load(HEAD + chunk(b'iTXt', b'spam\0\1\0en\0Spam\0' + + zlib.compress(b"egg")[:1]) + TAIL) + self.assertEqual(im.info, {'spam': ''}) + + im = load(HEAD + chunk(b'iTXt', b'spam\0\1\1en\0Spam\0' + + zlib.compress(b"egg")) + TAIL) self.assertEqual(im.info, {}) - im = load(HEAD + chunk(b'iTXt', b'spam\0\1\1en\0Spam\0' + zlib.compress(b"egg")) + TAIL) - self.assertEqual(im.info, {}) - - im = load(HEAD + chunk(b'iTXt', b'spam\0\1\0en\0Spam\0' + zlib.compress(b"egg")) + TAIL) + im = load(HEAD + chunk(b'iTXt', b'spam\0\1\0en\0Spam\0' + + zlib.compress(b"egg")) + TAIL) self.assertEqual(im.info, {"spam": "egg"}) self.assertEqual(im.info["spam"].lang, "en") self.assertEqual(im.info["spam"].tkey, "Spam") def test_interlace(self): - file = "Tests/images/pil123p.png" - im = Image.open(file) + test_file = "Tests/images/pil123p.png" + im = Image.open(test_file) self.assert_image(im, "P", (162, 150)) self.assertTrue(im.info.get("interlace")) im.load() - file = "Tests/images/pil123rgba.png" - im = Image.open(file) + test_file = "Tests/images/pil123rgba.png" + im = Image.open(test_file) self.assert_image(im, "RGBA", (162, 150)) self.assertTrue(im.info.get("interlace")) @@ -181,8 +184,8 @@ class TestFilePng(PillowTestCase): im.load() def test_load_transparent_p(self): - file = "Tests/images/pil123p.png" - im = Image.open(file) + test_file = "Tests/images/pil123p.png" + im = Image.open(test_file) self.assert_image(im, "P", (162, 150)) im = im.convert("RGBA") @@ -192,8 +195,8 @@ class TestFilePng(PillowTestCase): self.assertEqual(len(im.split()[3].getcolors()), 124) def test_load_transparent_rgb(self): - file = "Tests/images/rgb_trns.png" - im = Image.open(file) + test_file = "Tests/images/rgb_trns.png" + im = Image.open(test_file) self.assert_image(im, "RGB", (64, 64)) im = im.convert("RGBA") @@ -206,22 +209,22 @@ class TestFilePng(PillowTestCase): in_file = "Tests/images/pil123p.png" im = Image.open(in_file) - file = self.tempfile("temp.png") - im.save(file) + test_file = self.tempfile("temp.png") + im.save(test_file) def test_save_p_single_transparency(self): in_file = "Tests/images/p_trns_single.png" im = Image.open(in_file) - file = self.tempfile("temp.png") - im.save(file) + test_file = self.tempfile("temp.png") + im.save(test_file) def test_save_l_transparency(self): in_file = "Tests/images/l_trns.png" im = Image.open(in_file) - file = self.tempfile("temp.png") - im.save(file) + test_file = self.tempfile("temp.png") + im.save(test_file) # There are 559 transparent pixels. im = im.convert('RGBA') @@ -231,23 +234,23 @@ class TestFilePng(PillowTestCase): in_file = "Tests/images/caption_6_33_22.png" im = Image.open(in_file) - file = self.tempfile("temp.png") - im.save(file) + test_file = self.tempfile("temp.png") + im.save(test_file) def test_load_verify(self): # Check open/load/verify exception (@PIL150) - im = Image.open("Tests/images/lena.png") + im = Image.open(TEST_PNG_FILE) im.verify() - im = Image.open("Tests/images/lena.png") + im = Image.open(TEST_PNG_FILE) im.load() - self.assertRaises(RuntimeError, lambda: im.verify()) + self.assertRaises(RuntimeError, im.verify) def test_roundtrip_dpi(self): # Check dpi roundtripping - im = Image.open(file) + im = Image.open(TEST_PNG_FILE) im = roundtrip(im, dpi=(100, 100)) self.assertEqual(im.info["dpi"], (100, 100)) @@ -255,7 +258,7 @@ class TestFilePng(PillowTestCase): def test_roundtrip_text(self): # Check text roundtripping - im = Image.open(file) + im = Image.open(TEST_PNG_FILE) info = PngImagePlugin.PngInfo() info.add_text("TXT", "VALUE") @@ -271,7 +274,8 @@ class TestFilePng(PillowTestCase): im = Image.new("RGB", (32, 32)) info = PngImagePlugin.PngInfo() info.add_itxt("spam", "Eggs", "en", "Spam") - info.add_text("eggs", PngImagePlugin.iTXt("Spam", "en", "Eggs"), zip=True) + info.add_text("eggs", PngImagePlugin.iTXt("Spam", "en", "Eggs"), + zip=True) im = roundtrip(im, pnginfo=info) self.assertEqual(im.info, {"spam": "Eggs", "eggs": "Spam"}) @@ -303,11 +307,11 @@ class TestFilePng(PillowTestCase): self.assertEqual(im.info, {"Text": value}) if str is not bytes: - rt_text(" Aa" + chr(0xa0) + chr(0xc4) + chr(0xff)) # Latin1 - rt_text(chr(0x400) + chr(0x472) + chr(0x4ff)) # Cyrillic - rt_text(chr(0x4e00) + chr(0x66f0) + # CJK + rt_text(" Aa" + chr(0xa0) + chr(0xc4) + chr(0xff)) # Latin1 + rt_text(chr(0x400) + chr(0x472) + chr(0x4ff)) # Cyrillic + rt_text(chr(0x4e00) + chr(0x66f0) + # CJK chr(0x9fba) + chr(0x3042) + chr(0xac00)) - rt_text("A" + chr(0xc4) + chr(0x472) + chr(0x3042)) # Combined + rt_text("A" + chr(0xc4) + chr(0x472) + chr(0x3042)) # Combined def test_scary(self): # Check reading of evil PNG file. For information, see: @@ -325,8 +329,8 @@ class TestFilePng(PillowTestCase): # Check writing and reading of tRNS chunks for RGB images. # Independent file sample provided by Sebastian Spaeth. - file = "Tests/images/caption_6_33_22.png" - im = Image.open(file) + test_file = "Tests/images/caption_6_33_22.png" + im = Image.open(test_file) self.assertEqual(im.info["transparency"], (248, 248, 248)) # check saving transparency by default @@ -338,7 +342,7 @@ class TestFilePng(PillowTestCase): def test_trns_p(self): # Check writing a transparency of 0, issue #528 - im = lena('P') + im = hopper('P') im.info['transparency'] = 0 f = self.tempfile("temp.png") @@ -347,7 +351,15 @@ class TestFilePng(PillowTestCase): im2 = Image.open(f) self.assertIn('transparency', im2.info) - self.assert_image_equal(im2.convert('RGBA'), im.convert('RGBA')) + self.assert_image_equal(im2.convert('RGBA'), + im.convert('RGBA')) + + def test_trns_null(self): + # Check reading images with null tRNS value, issue #1239 + test_file = "Tests/images/tRNS_null_1x1.png" + im = Image.open(test_file) + + self.assertEqual(im.info["transparency"], 0) def test_save_icc_profile_none(self): # check saving files with an ICC profile set to None (omit profile) @@ -360,7 +372,7 @@ class TestFilePng(PillowTestCase): def test_roundtrip_icc_profile(self): # check that we can roundtrip the icc profile - im = lena('RGB') + im = hopper('RGB') jpeg_image = Image.open('Tests/images/flower2.jpg') expected_icc = jpeg_image.info['icc_profile'] @@ -369,6 +381,13 @@ class TestFilePng(PillowTestCase): im = roundtrip(im) self.assertEqual(im.info['icc_profile'], expected_icc) + def test_repr_png(self): + im = hopper() + + repr_png = Image.open(BytesIO(im._repr_png_())) + self.assertEqual(repr_png.format, 'PNG') + self.assert_image_equal(im, repr_png) + if __name__ == '__main__': unittest.main() diff --git a/Tests/test_file_ppm.py b/Tests/test_file_ppm.py index e1f1537d2..80c2e60da 100644 --- a/Tests/test_file_ppm.py +++ b/Tests/test_file_ppm.py @@ -3,14 +3,14 @@ from helper import unittest, PillowTestCase from PIL import Image # sample ppm stream -file = "Tests/images/lena.ppm" -data = open(file, "rb").read() +test_file = "Tests/images/hopper.ppm" +data = open(test_file, "rb").read() class TestFilePpm(PillowTestCase): def test_sanity(self): - im = Image.open(file) + im = Image.open(test_file) im.load() self.assertEqual(im.mode, "RGB") self.assertEqual(im.size, (128, 128)) diff --git a/Tests/test_file_psd.py b/Tests/test_file_psd.py index ee903ce5c..dca3601b2 100644 --- a/Tests/test_file_psd.py +++ b/Tests/test_file_psd.py @@ -3,19 +3,26 @@ from helper import unittest, PillowTestCase from PIL import Image # sample ppm stream -file = "Tests/images/lena.psd" -data = open(file, "rb").read() +test_file = "Tests/images/hopper.psd" +data = open(test_file, "rb").read() class TestImagePsd(PillowTestCase): def test_sanity(self): - im = Image.open(file) + im = Image.open(test_file) im.load() self.assertEqual(im.mode, "RGB") self.assertEqual(im.size, (128, 128)) self.assertEqual(im.format, "PSD") + def test_n_frames(self): + im = Image.open("Tests/images/hopper_merged.psd") + self.assertEqual(im.n_frames, 1) + + im = Image.open(test_file) + self.assertEqual(im.n_frames, 2) + if __name__ == '__main__': unittest.main() diff --git a/Tests/test_file_sgi.py b/Tests/test_file_sgi.py index 84b184b63..d49086c51 100644 --- a/Tests/test_file_sgi.py +++ b/Tests/test_file_sgi.py @@ -8,8 +8,8 @@ class TestFileSgi(PillowTestCase): def test_rgb(self): # Arrange # Created with ImageMagick then renamed: - # convert lena.ppm lena.sgi - test_file = "Tests/images/lena.rgb" + # convert hopper.ppm hopper.sgi + test_file = "Tests/images/hopper.rgb" # Act / Assert self.assertRaises(ValueError, lambda: Image.open(test_file)) @@ -17,8 +17,8 @@ class TestFileSgi(PillowTestCase): def test_l(self): # Arrange # Created with ImageMagick then renamed: - # convert lena.ppm -monochrome lena.sgi - test_file = "Tests/images/lena.bw" + # convert hopper.ppm -monochrome hopper.sgi + test_file = "Tests/images/hopper.bw" # Act / Assert self.assertRaises(ValueError, lambda: Image.open(test_file)) diff --git a/Tests/test_file_spider.py b/Tests/test_file_spider.py index 65e4d2782..7d24b2fe5 100644 --- a/Tests/test_file_spider.py +++ b/Tests/test_file_spider.py @@ -1,9 +1,9 @@ -from helper import unittest, PillowTestCase, lena +from helper import unittest, PillowTestCase, hopper from PIL import Image from PIL import SpiderImagePlugin -TEST_FILE = "Tests/images/lena.spider" +TEST_FILE = "Tests/images/hopper.spider" class TestImageSpider(PillowTestCase): @@ -18,7 +18,7 @@ class TestImageSpider(PillowTestCase): def test_save(self): # Arrange temp = self.tempfile('temp.spider') - im = lena() + im = hopper() # Act im.save(temp, "SPIDER") @@ -42,9 +42,13 @@ class TestImageSpider(PillowTestCase): # Assert self.assertEqual(index, 0) + def test_n_frames(self): + im = Image.open(TEST_FILE) + self.assertEqual(im.n_frames, 1) + def test_loadImageSeries(self): # Arrange - not_spider_file = "Tests/images/lena.ppm" + not_spider_file = "Tests/images/hopper.ppm" file_list = [TEST_FILE, not_spider_file, "path/not_found.ext"] # Act diff --git a/Tests/test_file_sun.py b/Tests/test_file_sun.py index c52564f91..332104062 100644 --- a/Tests/test_file_sun.py +++ b/Tests/test_file_sun.py @@ -7,8 +7,8 @@ class TestFileSun(PillowTestCase): def test_sanity(self): # Arrange - # Created with ImageMagick: convert lena.ppm lena.ras - test_file = "Tests/images/lena.ras" + # Created with ImageMagick: convert hopper.jpg hopper.ras + test_file = "Tests/images/hopper.ras" # Act im = Image.open(test_file) diff --git a/Tests/test_file_tar.py b/Tests/test_file_tar.py index 7010973ce..958b2f4df 100644 --- a/Tests/test_file_tar.py +++ b/Tests/test_file_tar.py @@ -4,8 +4,8 @@ from PIL import Image, TarIO codecs = dir(Image.core) -# sample ppm stream -tarfile = "Tests/images/lena.tar" +# Sample tar archive +TEST_TAR_FILE = "Tests/images/hopper.tar" class TestFileTar(PillowTestCase): @@ -16,7 +16,7 @@ class TestFileTar(PillowTestCase): def test_sanity(self): if "zip_decoder" in codecs: - tar = TarIO.TarIO(tarfile, 'lena.png') + tar = TarIO.TarIO(TEST_TAR_FILE, 'hopper.png') im = Image.open(tar) im.load() self.assertEqual(im.mode, "RGB") @@ -24,7 +24,7 @@ class TestFileTar(PillowTestCase): self.assertEqual(im.format, "PNG") if "jpeg_decoder" in codecs: - tar = TarIO.TarIO(tarfile, 'lena.jpg') + tar = TarIO.TarIO(TEST_TAR_FILE, 'hopper.jpg') im = Image.open(tar) im.load() self.assertEqual(im.mode, "RGB") diff --git a/Tests/test_file_tga.py b/Tests/test_file_tga.py new file mode 100644 index 000000000..ea94dee64 --- /dev/null +++ b/Tests/test_file_tga.py @@ -0,0 +1,20 @@ +from helper import unittest, PillowTestCase + +from PIL import Image + + +class TestFileTga(PillowTestCase): + + def test_id_field(self): + # tga file with id field + test_file = "Tests/images/tga_id_field.tga" + + # Act + im = Image.open(test_file) + + # Assert + self.assertEqual(im.size, (100, 100)) + + +if __name__ == '__main__': + unittest.main() diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py index 9c832c206..02a63586c 100644 --- a/Tests/test_file_tiff.py +++ b/Tests/test_file_tiff.py @@ -1,42 +1,45 @@ -from helper import unittest, PillowTestCase, lena, py3 +from __future__ import print_function +from helper import unittest, PillowTestCase, hopper, py3 -from PIL import Image +from PIL import Image, TiffImagePlugin + +import struct class TestFileTiff(PillowTestCase): def test_sanity(self): - file = self.tempfile("temp.tif") + filename = self.tempfile("temp.tif") - lena("RGB").save(file) + hopper("RGB").save(filename) - im = Image.open(file) + im = Image.open(filename) im.load() self.assertEqual(im.mode, "RGB") self.assertEqual(im.size, (128, 128)) self.assertEqual(im.format, "TIFF") - lena("1").save(file) - im = Image.open(file) + hopper("1").save(filename) + im = Image.open(filename) - lena("L").save(file) - im = Image.open(file) + hopper("L").save(filename) + im = Image.open(filename) - lena("P").save(file) - im = Image.open(file) + hopper("P").save(filename) + im = Image.open(filename) - lena("RGB").save(file) - im = Image.open(file) + hopper("RGB").save(filename) + im = Image.open(filename) - lena("I").save(file) - im = Image.open(file) + hopper("I").save(filename) + im = Image.open(filename) def test_mac_tiff(self): # Read RGBa images from Mac OS X [@PIL136] - file = "Tests/images/pil136.tiff" - im = Image.open(file) + filename = "Tests/images/pil136.tiff" + im = Image.open(filename) self.assertEqual(im.mode, "RGBA") self.assertEqual(im.size, (55, 43)) @@ -50,8 +53,8 @@ class TestFileTiff(PillowTestCase): if "jpeg_decoder" not in codecs: self.skipTest("jpeg support not available") - file = "Tests/images/pil168.tif" - im = Image.open(file) + filename = "Tests/images/pil168.tif" + im = Image.open(filename) self.assertEqual(im.mode, "RGB") self.assertEqual(im.size, (256, 256)) @@ -66,8 +69,8 @@ class TestFileTiff(PillowTestCase): def test_xyres_tiff(self): from PIL.TiffImagePlugin import X_RESOLUTION, Y_RESOLUTION - file = "Tests/images/pil168.tif" - im = Image.open(file) + 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 @@ -76,6 +79,12 @@ class TestFileTiff(PillowTestCase): im._setup() self.assertEqual(im.info['dpi'], (72., 72.)) + def test_bad_exif(self): + try: + Image.open('Tests/images/hopper_bad_exif.jpg')._getexif() + except struct.error: + self.fail("Bad EXIF data should not pass incorrect values to _binary unpack") + def test_little_endian(self): im = Image.open('Tests/images/16bit.cropped.tif') self.assertEqual(im.getpixel((0, 0)), 480) @@ -121,13 +130,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(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))) + print(im2.getpixel((0, 0))) + print(im2.getpixel((0, 1))) + print(im2.getpixel((0, 2))) self.assert_image_equal(im, im2) @@ -141,27 +150,70 @@ class TestFileTiff(PillowTestCase): self.assertEqual( im.getextrema(), (-3.140936851501465, 3.140684127807617)) + def test_n_frames(self): + im = Image.open('Tests/images/multipage-lastframe.tif') + self.assertEqual(im.n_frames, 1) + + im = Image.open('Tests/images/multipage.tiff') + self.assertEqual(im.n_frames, 3) + + def test_multipage(self): + # issue #862 + im = Image.open('Tests/images/multipage.tiff') + # file is a multipage tiff: 10x10 green, 10x10 red, 20x20 blue + + im.seek(0) + self.assertEqual(im.size, (10, 10)) + self.assertEqual(im.convert('RGB').getpixel((0, 0)), (0, 128, 0)) + + im.seek(1) + im.load() + self.assertEqual(im.size, (10, 10)) + self.assertEqual(im.convert('RGB').getpixel((0, 0)), (255, 0, 0)) + + im.seek(2) + im.load() + self.assertEqual(im.size, (20, 20)) + self.assertEqual(im.convert('RGB').getpixel((0, 0)), (0, 0, 255)) + + def test_multipage_last_frame(self): + im = Image.open('Tests/images/multipage-lastframe.tif') + im.load() + self.assertEqual(im.size, (20, 20)) + self.assertEqual(im.convert('RGB').getpixel((0, 0)), (0, 0, 255)) + def test___str__(self): # Arrange - file = "Tests/images/pil136.tiff" - im = Image.open(file) + filename = "Tests/images/pil136.tiff" + im = Image.open(filename) # Act ret = str(im.ifd) # Assert self.assertIsInstance(ret, str) + + def test_as_dict(self): + # Arrange + filename = "Tests/images/pil136.tiff" + im = Image.open(filename) + + # Act + ret = im.ifd.as_dict() + + # Assert + self.assertIsInstance(ret, dict) + 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,)}') + 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,)}) def test__delitem__(self): # Arrange - file = "Tests/images/pil136.tiff" - im = Image.open(file) + filename = "Tests/images/pil136.tiff" + im = Image.open(filename) len_before = len(im.ifd.as_dict()) # Act @@ -173,7 +225,6 @@ class TestFileTiff(PillowTestCase): def test_load_byte(self): # Arrange - from PIL import TiffImagePlugin ifd = TiffImagePlugin.ImageFileDirectory() data = b"abc" @@ -185,7 +236,6 @@ class TestFileTiff(PillowTestCase): def test_load_string(self): # Arrange - from PIL import TiffImagePlugin ifd = TiffImagePlugin.ImageFileDirectory() data = b"abc\0" @@ -197,7 +247,6 @@ class TestFileTiff(PillowTestCase): def test_load_float(self): # Arrange - from PIL import TiffImagePlugin ifd = TiffImagePlugin.ImageFileDirectory() data = b"abcdabcd" @@ -209,7 +258,6 @@ class TestFileTiff(PillowTestCase): def test_load_double(self): # Arrange - from PIL import TiffImagePlugin ifd = TiffImagePlugin.ImageFileDirectory() data = b"abcdefghabcdefgh" @@ -221,8 +269,8 @@ class TestFileTiff(PillowTestCase): def test_seek(self): # Arrange - file = "Tests/images/pil136.tiff" - im = Image.open(file) + filename = "Tests/images/pil136.tiff" + im = Image.open(filename) # Act im.seek(-1) @@ -232,8 +280,8 @@ class TestFileTiff(PillowTestCase): def test_seek_eof(self): # Arrange - file = "Tests/images/pil136.tiff" - im = Image.open(file) + filename = "Tests/images/pil136.tiff" + im = Image.open(filename) self.assertEqual(im.tell(), 0) # Act / Assert @@ -272,6 +320,71 @@ class TestFileTiff(PillowTestCase): # Assert self.assertEqual(ret, [0, 1]) + 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_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) + + 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) + if __name__ == '__main__': unittest.main() diff --git a/Tests/test_file_tiff_metadata.py b/Tests/test_file_tiff_metadata.py index e0805b525..dfc16682b 100644 --- a/Tests/test_file_tiff_metadata.py +++ b/Tests/test_file_tiff_metadata.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase, lena +from helper import unittest, PillowTestCase, hopper from PIL import Image, TiffImagePlugin, TiffTags @@ -8,18 +8,26 @@ tag_ids = dict(zip(TiffTags.TAGS.values(), TiffTags.TAGS.keys())) class TestFileTiffMetadata(PillowTestCase): def test_rt_metadata(self): - """ Test writing arbitray metadata into the tiff image directory + """ Test writing arbitrary metadata into the tiff image directory Use case is ImageJ private tags, one numeric, one arbitrary data. https://github.com/python-pillow/Pillow/issues/291 """ - img = lena() + img = hopper() textdata = "This is some arbitrary metadata for a text field" + floatdata = 12.345 + doubledata = 67.89 + info = TiffImagePlugin.ImageFileDirectory() info[tag_ids['ImageJMetaDataByteCounts']] = len(textdata) info[tag_ids['ImageJMetaData']] = textdata + info[tag_ids['RollAngle']] = floatdata + info.tagtype[tag_ids['RollAngle']] = 11 + + info[tag_ids['YawAngle']] = doubledata + info.tagtype[tag_ids['YawAngle']] = 12 f = self.tempfile("temp.tif") @@ -29,28 +37,30 @@ class TestFileTiffMetadata(PillowTestCase): 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) def test_read_metadata(self): - img = Image.open('Tests/images/lena_g4.tif') + img = Image.open('Tests/images/hopper_g4.tif') - known = {'YResolution': ((1207959552, 16777216),), + known = {'YResolution': ((4294967295, 113653537),), 'PlanarConfiguration': (1,), 'BitsPerSample': (1,), 'ImageLength': (128,), 'Compression': (4,), 'FillOrder': (1,), - 'DocumentName': 'lena.g4.tif', 'RowsPerStrip': (128,), - 'ResolutionUnit': (1,), + 'ResolutionUnit': (3,), 'PhotometricInterpretation': (0,), 'PageNumber': (0, 1), - 'XResolution': ((1207959552, 16777216),), + 'XResolution': ((4294967295, 113653537),), 'ImageWidth': (128,), 'Orientation': (1,), - 'StripByteCounts': (1796,), + 'StripByteCounts': (1968,), 'SamplesPerPixel': (1,), 'StripOffsets': (8,), - 'Software': 'ImageMagick 6.5.7-8 2012-08-17 Q16 http://www.imagemagick.org'} + } # self.assertEqual is equivalent, # but less helpful in telling what's wrong. @@ -63,7 +73,7 @@ class TestFileTiffMetadata(PillowTestCase): def test_write_metadata(self): """ Test metadata writing through the python code """ - img = Image.open('Tests/images/lena.tif') + img = Image.open('Tests/images/hopper.tif') f = self.tempfile('temp.tiff') img.save(f, tiffinfo=img.tag) @@ -86,6 +96,10 @@ class TestFileTiffMetadata(PillowTestCase): self.assertEqual( value, reloaded[tag], "%s didn't roundtrip" % tag) + def test_no_duplicate_50741_tag(self): + self.assertEqual(tag_ids['MakerNoteSafety'], 50741) + self.assertEqual(tag_ids['BestQualityScale'], 50780) + if __name__ == '__main__': unittest.main() diff --git a/Tests/test_file_webp.py b/Tests/test_file_webp.py index ffaf7c673..8c8313dd9 100644 --- a/Tests/test_file_webp.py +++ b/Tests/test_file_webp.py @@ -1,10 +1,10 @@ -from helper import unittest, PillowTestCase, lena +from helper import unittest, PillowTestCase, hopper from PIL import Image try: from PIL import _webp -except: +except ImportError: # Skip in setUp() pass @@ -14,7 +14,7 @@ class TestFileWebp(PillowTestCase): def setUp(self): try: from PIL import _webp - except: + except ImportError: self.skipTest('WebP support not installed') def test_version(self): @@ -23,7 +23,7 @@ class TestFileWebp(PillowTestCase): def test_read_rgb(self): - file_path = "Tests/images/lena.webp" + file_path = "Tests/images/hopper.webp" image = Image.open(file_path) self.assertEqual(image.mode, "RGB") @@ -33,8 +33,8 @@ class TestFileWebp(PillowTestCase): image.getdata() # generated with: - # dwebp -ppm ../../Tests/images/lena.webp -o lena_webp_bits.ppm - target = Image.open('Tests/images/lena_webp_bits.ppm') + # dwebp -ppm ../../Tests/images/hopper.webp -o hopper_webp_bits.ppm + target = Image.open('Tests/images/hopper_webp_bits.ppm') self.assert_image_similar(image, target, 20.0) def test_write_rgb(self): @@ -45,7 +45,7 @@ class TestFileWebp(PillowTestCase): temp_file = self.tempfile("temp.webp") - lena("RGB").save(temp_file) + hopper("RGB").save(temp_file) image = Image.open(temp_file) image.load() @@ -60,17 +60,17 @@ class TestFileWebp(PillowTestCase): # but it doesn't if the WebP is generated on Ubuntu and tested on # Fedora. - # generated with: dwebp -ppm temp.webp -o lena_webp_write.ppm - # target = Image.open('Tests/images/lena_webp_write.ppm') + # generated with: dwebp -ppm temp.webp -o hopper_webp_write.ppm + # target = Image.open('Tests/images/hopper_webp_write.ppm') # self.assert_image_equal(image, target) # This test asserts that the images are similar. If the average pixel # difference between the two images is less than the epsilon value, # then we're going to accept that it's a reasonable lossy version of - # the image. The included lena images for WebP are showing ~16 on + # the image. The old lena images for WebP are showing ~16 on # Ubuntu, the jpegs are showing ~18. - target = lena("RGB") - self.assert_image_similar(image, target, 20.0) + target = hopper("RGB") + self.assert_image_similar(image, target, 12) if __name__ == '__main__': diff --git a/Tests/test_file_webp_alpha.py b/Tests/test_file_webp_alpha.py index 5f8f653cf..f316b71e1 100644 --- a/Tests/test_file_webp_alpha.py +++ b/Tests/test_file_webp_alpha.py @@ -1,10 +1,10 @@ -from helper import unittest, PillowTestCase, lena +from helper import unittest, PillowTestCase, hopper from PIL import Image try: from PIL import _webp -except: +except ImportError: pass # Skip in setUp() @@ -14,11 +14,12 @@ class TestFileWebpAlpha(PillowTestCase): def setUp(self): try: from PIL import _webp - except: + except ImportError: self.skipTest('WebP support not installed') if _webp.WebPDecoderBuggyAlpha(self): - self.skipTest("Buggy early version of WebP installed, not testing transparency") + self.skipTest("Buggy early version of WebP installed, " + "not testing transparency") def test_read_rgba(self): # Generated with `cwebp transparent.png -o transparent.webp` @@ -40,7 +41,7 @@ class TestFileWebpAlpha(PillowTestCase): temp_file = self.tempfile("temp.webp") # temp_file = "temp.webp" - pil_image = lena('RGBA') + pil_image = hopper('RGBA') mask = Image.new("RGBA", (64, 64), (128, 128, 128, 128)) # Add some partially transparent bits: @@ -79,10 +80,14 @@ class TestFileWebpAlpha(PillowTestCase): self.assertEqual(image.mode, "RGBA") self.assertEqual(image.size, (10, 10)) self.assertEqual(image.format, "WEBP") - image.load - image.getdata + image.load() + image.getdata() - self.assert_image_similar(image, pil_image, 1.0) + # 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: + self.assert_image_similar(image, pil_image, 1.0) if __name__ == '__main__': diff --git a/Tests/test_file_webp_lossless.py b/Tests/test_file_webp_lossless.py index 662ad1117..52a461a74 100644 --- a/Tests/test_file_webp_lossless.py +++ b/Tests/test_file_webp_lossless.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase, lena +from helper import unittest, PillowTestCase, hopper from PIL import Image @@ -23,7 +23,7 @@ class TestFileWebpLossless(PillowTestCase): def test_write_lossless_rgb(self): temp_file = self.tempfile("temp.webp") - lena("RGB").save(temp_file, lossless=True) + hopper("RGB").save(temp_file, lossless=True) image = Image.open(temp_file) image.load() @@ -34,7 +34,7 @@ class TestFileWebpLossless(PillowTestCase): image.load() image.getdata() - self.assert_image_equal(image, lena("RGB")) + self.assert_image_equal(image, hopper("RGB")) if __name__ == '__main__': diff --git a/Tests/test_file_webp_metadata.py b/Tests/test_file_webp_metadata.py index 6aadf9c7e..8b1254d61 100644 --- a/Tests/test_file_webp_metadata.py +++ b/Tests/test_file_webp_metadata.py @@ -8,10 +8,12 @@ class TestFileWebpMetadata(PillowTestCase): def setUp(self): try: from PIL import _webp - if not _webp.HAVE_WEBPMUX: - self.skipTest('webpmux support not installed') except: self.skipTest('WebP support not installed') + return + + if not _webp.HAVE_WEBPMUX: + self.skipTest('WebPMux support not installed') def test_read_exif_metadata(self): @@ -39,12 +41,12 @@ class TestFileWebpMetadata(PillowTestCase): image = Image.open(file_path) expected_exif = image.info['exif'] - buffer = BytesIO() + test_buffer = BytesIO() - image.save(buffer, "webp", exif=expected_exif) + image.save(test_buffer, "webp", exif=expected_exif) - buffer.seek(0) - webp_image = Image.open(buffer) + test_buffer.seek(0) + webp_image = Image.open(test_buffer) webp_exif = webp_image.info.get('exif', None) self.assertTrue(webp_exif) @@ -74,12 +76,12 @@ class TestFileWebpMetadata(PillowTestCase): image = Image.open(file_path) expected_icc_profile = image.info['icc_profile'] - buffer = BytesIO() + test_buffer = BytesIO() - image.save(buffer, "webp", icc_profile=expected_icc_profile) + image.save(test_buffer, "webp", icc_profile=expected_icc_profile) - buffer.seek(0) - webp_image = Image.open(buffer) + test_buffer.seek(0) + webp_image = Image.open(test_buffer) webp_icc_profile = webp_image.info.get('icc_profile', None) @@ -94,14 +96,14 @@ class TestFileWebpMetadata(PillowTestCase): file_path = "Tests/images/flower.jpg" image = Image.open(file_path) - image.info['exif'] + self.assertTrue('exif' in image.info) - buffer = BytesIO() + test_buffer = BytesIO() - image.save(buffer, "webp") + image.save(test_buffer, "webp") - buffer.seek(0) - webp_image = Image.open(buffer) + test_buffer.seek(0) + webp_image = Image.open(test_buffer) self.assertFalse(webp_image._getexif()) diff --git a/Tests/test_file_xpm.py b/Tests/test_file_xpm.py index 4fc393808..6a6817048 100644 --- a/Tests/test_file_xpm.py +++ b/Tests/test_file_xpm.py @@ -1,9 +1,9 @@ -from helper import unittest, PillowTestCase, lena +from helper import unittest, PillowTestCase, hopper from PIL import Image # sample ppm stream -TEST_FILE = "Tests/images/lena.xpm" +TEST_FILE = "Tests/images/hopper.xpm" class TestFileXpm(PillowTestCase): @@ -15,8 +15,8 @@ class TestFileXpm(PillowTestCase): self.assertEqual(im.size, (128, 128)) self.assertEqual(im.format, "XPM") - #large error due to quantization->44 colors. - self.assert_image_similar(im.convert('RGB'), lena('RGB'), 60) + # large error due to quantization->44 colors. + self.assert_image_similar(im.convert('RGB'), hopper('RGB'), 60) def test_load_read(self): # Arrange diff --git a/Tests/test_font_bdf.py b/Tests/test_font_bdf.py index 0df8e866b..b844f1228 100644 --- a/Tests/test_font_bdf.py +++ b/Tests/test_font_bdf.py @@ -9,8 +9,8 @@ class TestFontBdf(PillowTestCase): def test_sanity(self): - file = open(filename, "rb") - font = BdfFontFile.BdfFontFile(file) + test_file = open(filename, "rb") + font = BdfFontFile.BdfFontFile(test_file) self.assertIsInstance(font, FontFile.FontFile) self.assertEqual(len([_f for _f in font.glyph if _f]), 190) diff --git a/Tests/test_font_pcf.py b/Tests/test_font_pcf.py index 5e9e02c8c..3cc6afa64 100644 --- a/Tests/test_font_pcf.py +++ b/Tests/test_font_pcf.py @@ -17,8 +17,8 @@ class TestFontPcf(PillowTestCase): self.skipTest("zlib support not available") def save_font(self): - file = open(fontname, "rb") - font = PcfFontFile.PcfFontFile(file) + test_file = open(fontname, "rb") + font = PcfFontFile.PcfFontFile(test_file) self.assertIsInstance(font, FontFile.FontFile) self.assertEqual(len([_f for _f in font.glyph if _f]), 192) diff --git a/Tests/test_format_hsv.py b/Tests/test_format_hsv.py index 03603aa9b..dd4413f41 100644 --- a/Tests/test_format_hsv.py +++ b/Tests/test_format_hsv.py @@ -1,56 +1,63 @@ -from helper import unittest, PillowTestCase, lena +from __future__ import print_function +from helper import unittest, PillowTestCase, hopper from PIL import Image -import colorsys, itertools +import colorsys +import itertools + class TestFormatHSV(PillowTestCase): def int_to_float(self, i): return float(i)/255.0 + def str_to_float(self, i): + return float(ord(i))/255.0 + def to_int(self, f): return int(f*255.0) + def tuple_to_ints(self, tp): - x,y,z = tp + x, y, z = tp return (int(x*255.0), int(y*255.0), int(z*255.0)) - + def test_sanity(self): - im = Image.new('HSV', (100,100)) + Image.new('HSV', (100, 100)) def wedge(self): - w =Image._wedge() + w = Image._wedge() w90 = w.rotate(90) (px, h) = w.size - - r = Image.new('L', (px*3,h)) + + r = Image.new('L', (px*3, h)) g = r.copy() b = r.copy() - r.paste(w, (0,0)) - r.paste(w90, (px,0)) + r.paste(w, (0, 0)) + r.paste(w90, (px, 0)) - g.paste(w90, (0,0)) - g.paste(w, (2*px,0)) + g.paste(w90, (0, 0)) + g.paste(w, (2*px, 0)) - b.paste(w, (px,0)) - b.paste(w90, (2*px,0)) + b.paste(w, (px, 0)) + b.paste(w90, (2*px, 0)) - img = Image.merge('RGB',(r,g,b)) + img = Image.merge('RGB', (r, g, b)) - #print (("%d, %d -> "% (int(1.75*px),int(.25*px))) + \ - # "(%s, %s, %s)"%img.getpixel((1.75*px, .25*px))) - #print (("%d, %d -> "% (int(.75*px),int(.25*px))) + \ - # "(%s, %s, %s)"%img.getpixel((.75*px, .25*px))) + # print (("%d, %d -> "% (int(1.75*px),int(.25*px))) + \ + # "(%s, %s, %s)"%img.getpixel((1.75*px, .25*px))) + # print (("%d, %d -> "% (int(.75*px),int(.25*px))) + \ + # "(%s, %s, %s)"%img.getpixel((.75*px, .25*px))) return img - + def to_xxx_colorsys(self, im, func, mode): # convert the hard way using the library colorsys routines. - - (r,g,b) = im.split() - + + (r, g, b) = im.split() + if bytes is str: conv_func = self.str_to_float else: @@ -61,17 +68,20 @@ class TestFormatHSV(PillowTestCase): else: iter_helper = itertools.zip_longest - - converted = [self.tuple_to_ints(func(conv_func(_r), conv_func(_g), conv_func(_b))) - for (_r, _g, _b) in iter_helper(r.tobytes(), g.tobytes(), b.tobytes())] + converted = [self.tuple_to_ints(func(conv_func(_r), conv_func(_g), + conv_func(_b))) + for (_r, _g, _b) in iter_helper(r.tobytes(), g.tobytes(), + b.tobytes())] if str is bytes: - new_bytes = b''.join(chr(h)+chr(s)+chr(v) for (h,s,v) in converted) + new_bytes = b''.join(chr(h)+chr(s)+chr(v) for ( + h, s, v) in converted) else: - new_bytes = b''.join(bytes(chr(h)+chr(s)+chr(v), 'latin-1') for (h,s,v) in converted) - - hsv = Image.frombytes(mode,r.size, new_bytes) - + new_bytes = b''.join(bytes(chr(h)+chr(s)+chr(v), 'latin-1') for ( + h, s, v) in converted) + + hsv = Image.frombytes(mode, r.size, new_bytes) + return hsv def to_hsv_colorsys(self, im): @@ -81,18 +91,18 @@ class TestFormatHSV(PillowTestCase): return self.to_xxx_colorsys(im, colorsys.hsv_to_rgb, 'RGB') def test_wedge(self): - src = self.wedge().resize((3*32,32),Image.BILINEAR) + src = self.wedge().resize((3*32, 32), Image.BILINEAR) im = src.convert('HSV') comparable = self.to_hsv_colorsys(src) - #print (im.getpixel((448, 64))) - #print (comparable.getpixel((448, 64))) - - #print(im.split()[0].histogram()) - #print(comparable.split()[0].histogram()) + # print (im.getpixel((448, 64))) + # print (comparable.getpixel((448, 64))) - #im.split()[0].show() - #comparable.split()[0].show() + # print(im.split()[0].histogram()) + # print(comparable.split()[0].histogram()) + + # im.split()[0].show() + # comparable.split()[0].show() self.assert_image_similar(im.split()[0], comparable.split()[0], 1, "Hue conversion is wrong") @@ -101,34 +111,33 @@ class TestFormatHSV(PillowTestCase): self.assert_image_similar(im.split()[2], comparable.split()[2], 1, "Value conversion is wrong") - #print (im.getpixel((192, 64))) + # print (im.getpixel((192, 64))) comparable = src im = im.convert('RGB') - #im.split()[0].show() - #comparable.split()[0].show() - #print (im.getpixel((192, 64))) - #print (comparable.getpixel((192, 64))) - + # im.split()[0].show() + # comparable.split()[0].show() + # print (im.getpixel((192, 64))) + # print (comparable.getpixel((192, 64))) + self.assert_image_similar(im.split()[0], comparable.split()[0], 3, "R conversion is wrong") self.assert_image_similar(im.split()[1], comparable.split()[1], 3, "G conversion is wrong") self.assert_image_similar(im.split()[2], comparable.split()[2], 3, "B conversion is wrong") - - + def test_convert(self): - im = lena('RGB').convert('HSV') - comparable = self.to_hsv_colorsys(lena('RGB')) + im = hopper('RGB').convert('HSV') + comparable = self.to_hsv_colorsys(hopper('RGB')) # print ([ord(x) for x in im.split()[0].tobytes()[:80]]) # print ([ord(x) for x in comparable.split()[0].tobytes()[:80]]) # print(im.split()[0].histogram()) # print(comparable.split()[0].histogram()) - + self.assert_image_similar(im.split()[0], comparable.split()[0], 1, "Hue conversion is wrong") self.assert_image_similar(im.split()[1], comparable.split()[1], @@ -136,18 +145,16 @@ class TestFormatHSV(PillowTestCase): self.assert_image_similar(im.split()[2], comparable.split()[2], 1, "Value conversion is wrong") - def test_hsv_to_rgb(self): - comparable = self.to_hsv_colorsys(lena('RGB')) + comparable = self.to_hsv_colorsys(hopper('RGB')) converted = comparable.convert('RGB') comparable = self.to_rgb_colorsys(comparable) - - # print(converted.split()[1].histogram()) - # print(target.split()[1].histogram()) - # print ([ord(x) for x in target.split()[1].tobytes()[:80]]) - # print ([ord(x) for x in converted.split()[1].tobytes()[:80]]) + # print(converted.split()[1].histogram()) + # print(target.split()[1].histogram()) + # print ([ord(x) for x in target.split()[1].tobytes()[:80]]) + # print ([ord(x) for x in converted.split()[1].tobytes()[:80]]) self.assert_image_similar(converted.split()[0], comparable.split()[0], 3, "R conversion is wrong") @@ -156,12 +163,6 @@ class TestFormatHSV(PillowTestCase): self.assert_image_similar(converted.split()[2], comparable.split()[2], 3, "B conversion is wrong") - - - - - - if __name__ == '__main__': unittest.main() diff --git a/Tests/test_image.py b/Tests/test_image.py index cd46c9713..caee70fec 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase, lena +from helper import unittest, PillowTestCase, hopper from PIL import Image @@ -41,8 +41,8 @@ class TestImage(PillowTestCase): im.paste(0, (0, 0, 100, 100)) self.assertFalse(im.readonly) - file = self.tempfile("temp.ppm") - im._dump(file) + test_file = self.tempfile("temp.ppm") + im._dump(test_file) def test_comparison_with_other_type(self): # Arrange @@ -57,7 +57,7 @@ class TestImage(PillowTestCase): def test_expand_x(self): # Arrange - im = lena() + im = hopper() orig_size = im.size xmargin = 5 @@ -70,7 +70,7 @@ class TestImage(PillowTestCase): def test_expand_xy(self): # Arrange - im = lena() + im = hopper() orig_size = im.size xmargin = 5 ymargin = 3 @@ -84,7 +84,7 @@ class TestImage(PillowTestCase): def test_getbands(self): # Arrange - im = lena() + im = hopper() # Act bands = im.getbands() @@ -94,7 +94,7 @@ class TestImage(PillowTestCase): def test_getbbox(self): # Arrange - im = lena() + im = hopper() # Act bbox = im.getbbox() @@ -140,6 +140,58 @@ class TestImage(PillowTestCase): img_colors = sorted(img.getcolors()) self.assertEqual(img_colors, expected_colors) + def test_effect_mandelbrot(self): + # Arrange + size = (512, 512) + extent = (-3, -2.5, 2, 2.5) + quality = 100 + + # Act + im = Image.effect_mandelbrot(size, extent, quality) + + # Assert + self.assertEqual(im.size, (512, 512)) + im2 = Image.open('Tests/images/effect_mandelbrot.png') + self.assert_image_equal(im, im2) + + def test_effect_mandelbrot_bad_arguments(self): + # Arrange + size = (512, 512) + # Get coordinates the wrong way round: + extent = (+3, +2.5, -2, -2.5) + # Quality < 2: + quality = 1 + + # Act/Assert + self.assertRaises( + ValueError, + lambda: Image.effect_mandelbrot(size, extent, quality)) + + def test_effect_noise(self): + # Arrange + size = (100, 100) + sigma = 128 + + # Act + im = Image.effect_noise(size, sigma) + + # Assert + self.assertEqual(im.size, (100, 100)) + self.assertEqual(im.mode, "L") + self.assertNotEqual(im.getpixel((0, 0)), im.getpixel((0, 1))) + + def test_effect_spread(self): + # Arrange + im = hopper() + distance = 10 + + # Act + im2 = im.effect_spread(distance) + + # Assert + self.assertEqual(im.size, (128, 128)) + im3 = Image.open('Tests/images/effect_spread.png') + self.assert_image_similar(im2, im3, 110) if __name__ == '__main__': unittest.main() diff --git a/Tests/test_image_array.py b/Tests/test_image_array.py index c5e49fc39..0899afd02 100644 --- a/Tests/test_image_array.py +++ b/Tests/test_image_array.py @@ -1,8 +1,8 @@ -from helper import unittest, PillowTestCase, lena +from helper import unittest, PillowTestCase, hopper from PIL import Image -im = lena().resize((128, 100)) +im = hopper().resize((128, 100)) class TestImageArray(PillowTestCase): diff --git a/Tests/test_image_convert.py b/Tests/test_image_convert.py index 01a80732b..8197a66e0 100644 --- a/Tests/test_image_convert.py +++ b/Tests/test_image_convert.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase, lena +from helper import unittest, PillowTestCase, hopper from PIL import Image @@ -15,13 +15,13 @@ class TestImageConvert(PillowTestCase): modes = "1", "L", "I", "F", "RGB", "RGBA", "RGBX", "CMYK", "YCbCr" for mode in modes: - im = lena(mode) + im = hopper(mode) for mode in modes: convert(im, mode) def test_default(self): - im = lena("P") + im = hopper("P") self.assert_image(im, "P", im.size) im = im.convert() self.assert_image(im, "RGB", im.size) @@ -36,7 +36,7 @@ class TestImageConvert(PillowTestCase): self.assertEqual(orig, converted) def test_8bit(self): - im = Image.open('Tests/images/lena.jpg') + im = Image.open('Tests/images/hopper.jpg') self._test_float_conversion(im.convert('L')) def test_16bit(self): @@ -48,8 +48,8 @@ class TestImageConvert(PillowTestCase): self._test_float_conversion(im.convert('I')) def test_rgba_p(self): - im = lena('RGBA') - im.putalpha(lena('L')) + im = hopper('RGBA') + im.putalpha(hopper('L')) converted = im.convert('P') comparable = converted.convert('RGBA') @@ -57,7 +57,7 @@ class TestImageConvert(PillowTestCase): self.assert_image_similar(im, comparable, 20) def test_trns_p(self): - im = lena('P') + im = hopper('P') im.info['transparency'] = 0 f = self.tempfile('temp.png') @@ -74,7 +74,7 @@ class TestImageConvert(PillowTestCase): def test_trns_p_rgba(self): # Arrange - im = lena('P') + im = hopper('P') im.info['transparency'] = 128 # Act @@ -84,7 +84,7 @@ class TestImageConvert(PillowTestCase): self.assertNotIn('transparency', rgba.info) def test_trns_l(self): - im = lena('L') + im = hopper('L') im.info['transparency'] = 128 f = self.tempfile('temp.png') @@ -104,7 +104,7 @@ class TestImageConvert(PillowTestCase): p.save(f) def test_trns_RGB(self): - im = lena('RGB') + im = hopper('RGB') im.info['transparency'] = im.getpixel((0, 0)) f = self.tempfile('temp.png') diff --git a/Tests/test_image_copy.py b/Tests/test_image_copy.py index a7882db94..d7725e672 100644 --- a/Tests/test_image_copy.py +++ b/Tests/test_image_copy.py @@ -1,13 +1,11 @@ -from helper import unittest, PillowTestCase, lena - -from PIL import Image +from helper import unittest, PillowTestCase, hopper class TestImageCopy(PillowTestCase): def test_copy(self): def copy(mode): - im = lena(mode) + im = hopper(mode) out = im.copy() self.assertEqual(out.mode, mode) self.assertEqual(out.size, im.size) diff --git a/Tests/test_image_crop.py b/Tests/test_image_crop.py index da93fe7c8..29ff2bc2a 100644 --- a/Tests/test_image_crop.py +++ b/Tests/test_image_crop.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase, lena +from helper import unittest, PillowTestCase, hopper from PIL import Image @@ -7,7 +7,7 @@ class TestImageCrop(PillowTestCase): def test_crop(self): def crop(mode): - out = lena(mode).crop((50, 50, 100, 100)) + out = hopper(mode).crop((50, 50, 100, 100)) self.assertEqual(out.mode, mode) self.assertEqual(out.size, (50, 50)) for mode in "1", "P", "L", "RGB", "I", "F": diff --git a/Tests/test_image_draft.py b/Tests/test_image_draft.py index a76b8d266..68676687c 100644 --- a/Tests/test_image_draft.py +++ b/Tests/test_image_draft.py @@ -2,13 +2,13 @@ from helper import unittest, PillowTestCase, fromstring, tostring from PIL import Image -codecs = dir(Image.core) -filename = "Tests/images/lena.jpg" -data = tostring(Image.open(filename).resize((512, 512)), "JPEG") +CODECS = dir(Image.core) +FILENAME = "Tests/images/hopper.jpg" +DATA = tostring(Image.open(FILENAME).resize((512, 512)), "JPEG") def draft(mode, size): - im = fromstring(data) + im = fromstring(DATA) im.draft(mode, size) return im @@ -16,7 +16,7 @@ def draft(mode, size): class TestImageDraft(PillowTestCase): def setUp(self): - if "jpeg_encoder" not in codecs or "jpeg_decoder" not in codecs: + if "jpeg_encoder" not in CODECS or "jpeg_decoder" not in CODECS: self.skipTest("jpeg support not available") def test_size(self): diff --git a/Tests/test_image_filter.py b/Tests/test_image_filter.py index 2452479cc..6a694b3ca 100644 --- a/Tests/test_image_filter.py +++ b/Tests/test_image_filter.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase, lena +from helper import unittest, PillowTestCase, hopper from PIL import Image from PIL import ImageFilter @@ -9,7 +9,7 @@ class TestImageFilter(PillowTestCase): def test_sanity(self): def filter(filter): - im = lena("L") + im = hopper("L") out = im.filter(filter) self.assertEqual(out.mode, im.mode) self.assertEqual(out.size, im.size) @@ -76,10 +76,10 @@ class TestImageFilter(PillowTestCase): # 0 1 2 # 3 4 5 # 6 7 8 - min = im.filter(ImageFilter.MinFilter).getpixel((1, 1)) + minimum = im.filter(ImageFilter.MinFilter).getpixel((1, 1)) med = im.filter(ImageFilter.MedianFilter).getpixel((1, 1)) - max = im.filter(ImageFilter.MaxFilter).getpixel((1, 1)) - return min, med, max + maximum = im.filter(ImageFilter.MaxFilter).getpixel((1, 1)) + return minimum, med, maximum self.assertEqual(rankfilter("1"), (0, 4, 8)) self.assertEqual(rankfilter("L"), (0, 4, 8)) diff --git a/Tests/test_image_frombytes.py b/Tests/test_image_frombytes.py index aad8046a1..3f9a2237f 100644 --- a/Tests/test_image_frombytes.py +++ b/Tests/test_image_frombytes.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase, lena +from helper import unittest, PillowTestCase, hopper from PIL import Image @@ -6,7 +6,7 @@ from PIL import Image class TestImageFromBytes(PillowTestCase): def test_sanity(self): - im1 = lena() + im1 = hopper() im2 = Image.frombytes(im1.mode, im1.size, im1.tobytes()) self.assert_image_equal(im1, im2) diff --git a/Tests/test_image_getbbox.py b/Tests/test_image_getbbox.py index 8d78195bd..3dc341aeb 100644 --- a/Tests/test_image_getbbox.py +++ b/Tests/test_image_getbbox.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase, lena +from helper import unittest, PillowTestCase, hopper from PIL import Image @@ -7,7 +7,7 @@ class TestImageGetBbox(PillowTestCase): def test_sanity(self): - bbox = lena().getbbox() + bbox = hopper().getbbox() self.assertIsInstance(bbox, tuple) def test_bbox(self): diff --git a/Tests/test_image_getcolors.py b/Tests/test_image_getcolors.py index d3e5a4989..8886762c7 100644 --- a/Tests/test_image_getcolors.py +++ b/Tests/test_image_getcolors.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase, lena +from helper import unittest, PillowTestCase, hopper class TestImageGetColors(PillowTestCase): @@ -6,7 +6,7 @@ class TestImageGetColors(PillowTestCase): def test_getcolors(self): def getcolors(mode, limit=None): - im = lena(mode) + im = hopper(mode) if limit: colors = im.getcolors(limit) else: @@ -16,37 +16,36 @@ class TestImageGetColors(PillowTestCase): return None self.assertEqual(getcolors("1"), 2) - self.assertEqual(getcolors("L"), 193) - self.assertEqual(getcolors("I"), 193) - self.assertEqual(getcolors("F"), 193) - self.assertEqual(getcolors("P"), 54) # fixed palette + self.assertEqual(getcolors("L"), 255) + self.assertEqual(getcolors("I"), 255) + self.assertEqual(getcolors("F"), 255) + self.assertEqual(getcolors("P"), 90) # fixed palette self.assertEqual(getcolors("RGB"), None) self.assertEqual(getcolors("RGBA"), None) self.assertEqual(getcolors("CMYK"), None) self.assertEqual(getcolors("YCbCr"), None) self.assertEqual(getcolors("L", 128), None) - self.assertEqual(getcolors("L", 1024), 193) + self.assertEqual(getcolors("L", 1024), 255) self.assertEqual(getcolors("RGB", 8192), None) - self.assertEqual(getcolors("RGB", 16384), 14836) - self.assertEqual(getcolors("RGB", 100000), 14836) + self.assertEqual(getcolors("RGB", 16384), 10100) + self.assertEqual(getcolors("RGB", 100000), 10100) - self.assertEqual(getcolors("RGBA", 16384), 14836) - self.assertEqual(getcolors("CMYK", 16384), 14836) - self.assertEqual(getcolors("YCbCr", 16384), 11995) + self.assertEqual(getcolors("RGBA", 16384), 10100) + self.assertEqual(getcolors("CMYK", 16384), 10100) + self.assertEqual(getcolors("YCbCr", 16384), 9329) # -------------------------------------------------------------------- def test_pack(self): # Pack problems for small tables (@PIL209) - im = lena().quantize(3).convert("RGB") + im = hopper().quantize(3).convert("RGB") - expected = [ - (3236, (227, 183, 147)), - (6297, (143, 84, 81)), - (6851, (208, 143, 112))] + expected = [(4039, (172, 166, 181)), + (4385, (124, 113, 134)), + (7960, (31, 20, 33))] A = im.getcolors(maxcolors=2) self.assertEqual(A, None) diff --git a/Tests/test_image_getdata.py b/Tests/test_image_getdata.py index ff6659595..89bfda895 100644 --- a/Tests/test_image_getdata.py +++ b/Tests/test_image_getdata.py @@ -1,32 +1,32 @@ -from helper import unittest, PillowTestCase, lena +from helper import unittest, PillowTestCase, hopper class TestImageGetData(PillowTestCase): def test_sanity(self): - data = lena().getdata() + data = hopper().getdata() len(data) list(data) - self.assertEqual(data[0], (223, 162, 133)) + self.assertEqual(data[0], (20, 20, 70)) def test_roundtrip(self): def getdata(mode): - im = lena(mode).resize((32, 30)) + im = hopper(mode).resize((32, 30)) data = im.getdata() return data[0], len(data), len(list(data)) - self.assertEqual(getdata("1"), (255, 960, 960)) - self.assertEqual(getdata("L"), (176, 960, 960)) - self.assertEqual(getdata("I"), (176, 960, 960)) - self.assertEqual(getdata("F"), (176.0, 960, 960)) - self.assertEqual(getdata("RGB"), ((223, 162, 133), 960, 960)) - self.assertEqual(getdata("RGBA"), ((223, 162, 133, 255), 960, 960)) - self.assertEqual(getdata("CMYK"), ((32, 93, 122, 0), 960, 960)) - self.assertEqual(getdata("YCbCr"), ((176, 103, 160), 960, 960)) + self.assertEqual(getdata("1"), (0, 960, 960)) + self.assertEqual(getdata("L"), (25, 960, 960)) + self.assertEqual(getdata("I"), (25, 960, 960)) + self.assertEqual(getdata("F"), (25.0, 960, 960)) + self.assertEqual(getdata("RGB"), (((20, 20, 70), 960, 960))) + self.assertEqual(getdata("RGBA"), ((20, 20, 70, 255), 960, 960)) + self.assertEqual(getdata("CMYK"), ((235, 235, 185, 0), 960, 960)) + self.assertEqual(getdata("YCbCr"), ((25, 153, 123), 960, 960)) if __name__ == '__main__': diff --git a/Tests/test_image_getextrema.py b/Tests/test_image_getextrema.py index af7f7698a..84d932a98 100644 --- a/Tests/test_image_getextrema.py +++ b/Tests/test_image_getextrema.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase, lena +from helper import unittest, PillowTestCase, hopper class TestImageGetExtrema(PillowTestCase): @@ -6,19 +6,19 @@ class TestImageGetExtrema(PillowTestCase): def test_extrema(self): def extrema(mode): - return lena(mode).getextrema() + return hopper(mode).getextrema() self.assertEqual(extrema("1"), (0, 255)) - self.assertEqual(extrema("L"), (40, 235)) - self.assertEqual(extrema("I"), (40, 235)) - self.assertEqual(extrema("F"), (40.0, 235.0)) - self.assertEqual(extrema("P"), (11, 218)) # fixed palette + self.assertEqual(extrema("L"), (0, 255)) + self.assertEqual(extrema("I"), (0, 255)) + self.assertEqual(extrema("F"), (0, 255)) + self.assertEqual(extrema("P"), (0, 225)) # fixed palette self.assertEqual( - extrema("RGB"), ((61, 255), (26, 234), (44, 223))) + extrema("RGB"), ((0, 255), (0, 255), (0, 255))) self.assertEqual( - extrema("RGBA"), ((61, 255), (26, 234), (44, 223), (255, 255))) + extrema("RGBA"), ((0, 255), (0, 255), (0, 255), (255, 255))) self.assertEqual( - extrema("CMYK"), ((0, 194), (21, 229), (32, 211), (0, 0))) + extrema("CMYK"), (((0, 255), (0, 255), (0, 255), (0, 0)))) if __name__ == '__main__': diff --git a/Tests/test_image_getim.py b/Tests/test_image_getim.py index d498d3923..e83439789 100644 --- a/Tests/test_image_getim.py +++ b/Tests/test_image_getim.py @@ -1,10 +1,10 @@ -from helper import unittest, PillowTestCase, lena, py3 +from helper import unittest, PillowTestCase, hopper, py3 class TestImageGetIm(PillowTestCase): def test_sanity(self): - im = lena() + im = hopper() type_repr = repr(type(im.getim())) if py3: diff --git a/Tests/test_image_getpalette.py b/Tests/test_image_getpalette.py index 0c399c432..1a673c73d 100644 --- a/Tests/test_image_getpalette.py +++ b/Tests/test_image_getpalette.py @@ -1,11 +1,11 @@ -from helper import unittest, PillowTestCase, lena +from helper import unittest, PillowTestCase, hopper class TestImageGetPalette(PillowTestCase): def test_palette(self): def palette(mode): - p = lena(mode).getpalette() + p = hopper(mode).getpalette() if p: return p[:10] return None diff --git a/Tests/test_image_getprojection.py b/Tests/test_image_getprojection.py index 262a21d4b..108c75b90 100644 --- a/Tests/test_image_getprojection.py +++ b/Tests/test_image_getprojection.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase, lena +from helper import unittest, PillowTestCase, hopper from PIL import Image @@ -7,7 +7,7 @@ class TestImageGetProjection(PillowTestCase): def test_sanity(self): - im = lena() + im = hopper() projection = im.getprojection() diff --git a/Tests/test_image_histogram.py b/Tests/test_image_histogram.py index 70f78a1fb..aa849a138 100644 --- a/Tests/test_image_histogram.py +++ b/Tests/test_image_histogram.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase, lena +from helper import unittest, PillowTestCase, hopper class TestImageHistogram(PillowTestCase): @@ -6,18 +6,18 @@ class TestImageHistogram(PillowTestCase): def test_histogram(self): def histogram(mode): - h = lena(mode).histogram() + h = hopper(mode).histogram() return len(h), min(h), max(h) - self.assertEqual(histogram("1"), (256, 0, 8872)) - self.assertEqual(histogram("L"), (256, 0, 199)) - self.assertEqual(histogram("I"), (256, 0, 199)) - self.assertEqual(histogram("F"), (256, 0, 199)) - self.assertEqual(histogram("P"), (256, 0, 2912)) - self.assertEqual(histogram("RGB"), (768, 0, 285)) + self.assertEqual(histogram("1"), (256, 0, 10994)) + self.assertEqual(histogram("L"), (256, 0, 638)) + self.assertEqual(histogram("I"), (256, 0, 638)) + self.assertEqual(histogram("F"), (256, 0, 638)) + self.assertEqual(histogram("P"), (256, 0, 1871)) + self.assertEqual(histogram("RGB"), (768, 4, 675)) self.assertEqual(histogram("RGBA"), (1024, 0, 16384)) self.assertEqual(histogram("CMYK"), (1024, 0, 16384)) - self.assertEqual(histogram("YCbCr"), (768, 0, 741)) + self.assertEqual(histogram("YCbCr"), (768, 0, 1908)) if __name__ == '__main__': diff --git a/Tests/test_image_load.py b/Tests/test_image_load.py index 786cd6ad8..f9d92e13c 100644 --- a/Tests/test_image_load.py +++ b/Tests/test_image_load.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase, lena +from helper import unittest, PillowTestCase, hopper from PIL import Image @@ -9,21 +9,21 @@ class TestImageLoad(PillowTestCase): def test_sanity(self): - im = lena() + im = hopper() pix = im.load() - self.assertEqual(pix[0, 0], (223, 162, 133)) + self.assertEqual(pix[0, 0], (20, 20, 70)) def test_close(self): - im = Image.open("Tests/images/lena.gif") + im = Image.open("Tests/images/hopper.gif") im.close() - self.assertRaises(ValueError, lambda: im.load()) + self.assertRaises(ValueError, im.load) self.assertRaises(ValueError, lambda: im.getpixel((0, 0))) def test_contextmanager(self): fn = None - with Image.open("Tests/images/lena.gif") as im: + with Image.open("Tests/images/hopper.gif") as im: fn = im.fp.fileno() os.fstat(fn) diff --git a/Tests/test_image_mode.py b/Tests/test_image_mode.py index 74ed9b7aa..6441c7d1b 100644 --- a/Tests/test_image_mode.py +++ b/Tests/test_image_mode.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase, lena +from helper import unittest, PillowTestCase, hopper from PIL import Image @@ -7,7 +7,7 @@ class TestImageMode(PillowTestCase): def test_sanity(self): - im = lena() + im = hopper() im.mode from PIL import ImageMode diff --git a/Tests/test_image_offset.py b/Tests/test_image_offset.py index 09f12266f..e5fe0bf47 100644 --- a/Tests/test_image_offset.py +++ b/Tests/test_image_offset.py @@ -1,11 +1,11 @@ -from helper import unittest, PillowTestCase, lena +from helper import unittest, PillowTestCase, hopper class TestImageOffset(PillowTestCase): def test_offset(self): - im1 = lena() + im1 = hopper() im2 = self.assert_warning(DeprecationWarning, lambda: im1.offset(10)) self.assertEqual(im1.getpixel((0, 0)), im2.getpixel((10, 10))) diff --git a/Tests/test_image_point.py b/Tests/test_image_point.py index 04054fa84..9e001dfb2 100644 --- a/Tests/test_image_point.py +++ b/Tests/test_image_point.py @@ -1,12 +1,10 @@ -from helper import unittest, PillowTestCase, lena - -import sys +from helper import unittest, PillowTestCase, hopper class TestImagePoint(PillowTestCase): def test_sanity(self): - im = lena() + im = hopper() self.assertRaises(ValueError, lambda: im.point(list(range(256)))) im.point(list(range(256))*3) @@ -26,21 +24,21 @@ class TestImagePoint(PillowTestCase): """ # 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') + # self.skipKnownBadTest(msg="Too Slow on pypy", interpreter='pypy') - im = lena("I") + im = hopper("I") im.point(list(range(256))*256, 'L') def test_f_lut(self): """ Tests for floating point lut of 8bit gray image """ - im = lena('L') + im = hopper('L') lut = [0.5 * float(x) for x in range(256)] out = im.point(lut, 'F') int_lut = [x//2 for x in range(256)] self.assert_image_equal(out.convert('L'), im.point(int_lut, 'L')) - + if __name__ == '__main__': unittest.main() diff --git a/Tests/test_image_putdata.py b/Tests/test_image_putdata.py index acea0d62a..b46456eba 100644 --- a/Tests/test_image_putdata.py +++ b/Tests/test_image_putdata.py @@ -1,4 +1,5 @@ -from helper import unittest, PillowTestCase, lena +from helper import unittest, PillowTestCase, hopper +from array import array import sys @@ -9,7 +10,7 @@ class TestImagePutData(PillowTestCase): def test_sanity(self): - im1 = lena() + im1 = hopper() data = list(im1.getdata()) @@ -41,30 +42,47 @@ class TestImagePutData(PillowTestCase): else: self.assertEqual(put(sys.maxsize), (255, 255, 255, 127)) - def test_pypy_performance(self): - im = Image.new('L', (256,256)) + im = Image.new('L', (256, 256)) im.putdata(list(range(256))*256) def test_mode_i(self): - src = lena('L') + src = hopper('L') data = list(src.getdata()) im = Image.new('I', src.size, 0) im.putdata(data, 2, 256) - target = [2* elt + 256 for elt in data] + target = [2 * elt + 256 for elt in data] self.assertEqual(list(im.getdata()), target) def test_mode_F(self): - src = lena('L') + src = hopper('L') data = list(src.getdata()) im = Image.new('F', src.size, 0) im.putdata(data, 2.0, 256.0) - target = [2.0* float(elt) + 256.0 for elt in data] + target = [2.0 * float(elt) + 256.0 for elt in data] self.assertEqual(list(im.getdata()), target) - + def test_array_B(self): + # shouldn't segfault + # see https://github.com/python-pillow/Pillow/issues/1008 + + arr = array('B', [0])*15000 + im = Image.new('L', (150, 100)) + im.putdata(arr) + + self.assertEqual(len(im.getdata()), len(arr)) + + def test_array_F(self): + # shouldn't segfault + # see https://github.com/python-pillow/Pillow/issues/1008 + + im = Image.new('F', (150, 100)) + arr = array('f', [0.0])*15000 + im.putdata(arr) + + self.assertEqual(len(im.getdata()), len(arr)) if __name__ == '__main__': unittest.main() diff --git a/Tests/test_image_putpalette.py b/Tests/test_image_putpalette.py index a77c1e565..a1f00c361 100644 --- a/Tests/test_image_putpalette.py +++ b/Tests/test_image_putpalette.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase, lena +from helper import unittest, PillowTestCase, hopper from PIL import ImagePalette @@ -7,7 +7,7 @@ class TestImagePutPalette(PillowTestCase): def test_putpalette(self): def palette(mode): - im = lena(mode).copy() + im = hopper(mode).copy() im.putpalette(list(range(256))*3) p = im.getpalette() if p: @@ -23,7 +23,7 @@ class TestImagePutPalette(PillowTestCase): self.assertRaises(ValueError, lambda: palette("YCbCr")) def test_imagepalette(self): - im = lena("P") + im = hopper("P") im.putpalette(ImagePalette.negative()) im.putpalette(ImagePalette.random()) im.putpalette(ImagePalette.sepia()) diff --git a/Tests/test_image_putpixel.py b/Tests/test_image_putpixel.py index a7f5dc2bb..418e9703c 100644 --- a/Tests/test_image_putpixel.py +++ b/Tests/test_image_putpixel.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase, lena +from helper import unittest, PillowTestCase, hopper from PIL import Image @@ -9,7 +9,7 @@ class TestImagePutPixel(PillowTestCase): def test_sanity(self): - im1 = lena() + im1 = hopper() im2 = Image.new(im1.mode, im1.size, 0) for y in range(im1.size[1]): diff --git a/Tests/test_image_quantize.py b/Tests/test_image_quantize.py index 2cbdac225..e8183eb48 100644 --- a/Tests/test_image_quantize.py +++ b/Tests/test_image_quantize.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase, lena +from helper import unittest, PillowTestCase, hopper from PIL import Image @@ -6,17 +6,17 @@ from PIL import Image class TestImageQuantize(PillowTestCase): def test_sanity(self): - im = lena() + im = hopper() im = im.quantize() self.assert_image(im, "P", im.size) - im = lena() - im = im.quantize(palette=lena("P")) + im = hopper() + im = im.quantize(palette=hopper("P")) self.assert_image(im, "P", im.size) def test_octree_quantize(self): - im = lena() + im = hopper() im = im.quantize(100, Image.FASTOCTREE) self.assert_image(im, "P", im.size) @@ -24,7 +24,7 @@ class TestImageQuantize(PillowTestCase): assert len(im.getcolors()) == 100 def test_rgba_quantize(self): - im = lena('RGBA') + im = hopper('RGBA') im.quantize() self.assertRaises(Exception, lambda: im.quantize(method=0)) diff --git a/Tests/test_image_resize.py b/Tests/test_image_resize.py index 6c9932e45..d59f8ba14 100644 --- a/Tests/test_image_resize.py +++ b/Tests/test_image_resize.py @@ -1,16 +1,102 @@ -from helper import unittest, PillowTestCase, lena +""" +Tests for resize functionality. +""" +from itertools import permutations + +from helper import unittest, PillowTestCase, hopper + +from PIL import Image + + +class TestImagingCoreResize(PillowTestCase): + + def resize(self, im, size, f): + # Image class independent version of resize. + im.load() + return im._new(im.im.resize(size, f)) + + def test_nearest_mode(self): + for mode in ["1", "P", "L", "I", "F", "RGB", "RGBA", "CMYK", "YCbCr", + "I;16"]: # exotic mode + im = hopper(mode) + r = self.resize(im, (15, 12), Image.NEAREST) + self.assertEqual(r.mode, mode) + self.assertEqual(r.size, (15, 12)) + self.assertEqual(r.im.bands, im.im.bands) + + def test_convolution_modes(self): + self.assertRaises(ValueError, self.resize, hopper("1"), + (15, 12), Image.BILINEAR) + self.assertRaises(ValueError, self.resize, hopper("P"), + (15, 12), Image.BILINEAR) + self.assertRaises(ValueError, self.resize, hopper("I;16"), + (15, 12), Image.BILINEAR) + for mode in ["L", "I", "F", "RGB", "RGBA", "CMYK", "YCbCr"]: + im = hopper(mode) + r = self.resize(im, (15, 12), Image.BILINEAR) + self.assertEqual(r.mode, mode) + self.assertEqual(r.size, (15, 12)) + self.assertEqual(r.im.bands, im.im.bands) + + def test_reduce_filters(self): + for f in [Image.LINEAR, Image.BILINEAR, Image.BICUBIC, Image.LANCZOS]: + r = self.resize(hopper("RGB"), (15, 12), f) + self.assertEqual(r.mode, "RGB") + self.assertEqual(r.size, (15, 12)) + + def test_enlarge_filters(self): + for f in [Image.LINEAR, Image.BILINEAR, Image.BICUBIC, Image.LANCZOS]: + r = self.resize(hopper("RGB"), (212, 195), f) + self.assertEqual(r.mode, "RGB") + self.assertEqual(r.size, (212, 195)) + + def test_endianness(self): + # Make an image with one colored pixel, in one channel. + # When resized, that channel should be the same as a GS image. + # Other channels should be unaffected. + # The R and A channels should not swap, which is indicative of + # an endianness issues. + + samples = { + 'blank': Image.new('L', (2, 2), 0), + 'filled': Image.new('L', (2, 2), 255), + 'dirty': Image.new('L', (2, 2), 0), + } + samples['dirty'].putpixel((1, 1), 128) + + for f in [Image.LINEAR, Image.BILINEAR, Image.BICUBIC, Image.LANCZOS]: + # samples resized with current filter + references = dict( + (name, self.resize(ch, (4, 4), f)) + for name, ch in samples.items() + ) + + for mode, channels_set in [ + ('RGB', ('blank', 'filled', 'dirty')), + ('RGBA', ('blank', 'blank', 'filled', 'dirty')), + ('LA', ('filled', 'dirty')), + ]: + for channels in set(permutations(channels_set)): + # compile image from different channels permutations + im = Image.merge(mode, [samples[ch] for ch in channels]) + resized = self.resize(im, (4, 4), f) + + for i, ch in enumerate(resized.split()): + # check what resized channel in image is the same + # as separately resized channel + self.assert_image_equal(ch, references[channels[i]]) class TestImageResize(PillowTestCase): def test_resize(self): def resize(mode, size): - out = lena(mode).resize(size) + out = hopper(mode).resize(size) self.assertEqual(out.mode, mode) self.assertEqual(out.size, size) for mode in "1", "P", "L", "RGB", "I", "F": - resize(mode, (100, 100)) - resize(mode, (200, 200)) + resize(mode, (112, 103)) + resize(mode, (188, 214)) if __name__ == '__main__': diff --git a/Tests/test_image_rotate.py b/Tests/test_image_rotate.py index 531fdd63f..38391adae 100644 --- a/Tests/test_image_rotate.py +++ b/Tests/test_image_rotate.py @@ -1,11 +1,11 @@ -from helper import unittest, PillowTestCase, lena +from helper import unittest, PillowTestCase, hopper class TestImageRotate(PillowTestCase): def test_rotate(self): def rotate(mode): - im = lena(mode) + im = hopper(mode) out = im.rotate(45) self.assertEqual(out.mode, mode) self.assertEqual(out.size, im.size) # default rotate clips output diff --git a/Tests/test_image_split.py b/Tests/test_image_split.py index 343f4bf8e..beab7b546 100644 --- a/Tests/test_image_split.py +++ b/Tests/test_image_split.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase, lena +from helper import unittest, PillowTestCase, hopper from PIL import Image @@ -7,7 +7,7 @@ class TestImageSplit(PillowTestCase): def test_split(self): def split(mode): - layers = lena(mode).split() + layers = hopper(mode).split() return [(i.mode, i.size[0], i.size[1]) for i in layers] self.assertEqual(split("1"), [('1', 128, 128)]) self.assertEqual(split("L"), [('L', 128, 128)]) @@ -30,28 +30,28 @@ class TestImageSplit(PillowTestCase): def test_split_merge(self): def split_merge(mode): - return Image.merge(mode, lena(mode).split()) - self.assert_image_equal(lena("1"), split_merge("1")) - self.assert_image_equal(lena("L"), split_merge("L")) - self.assert_image_equal(lena("I"), split_merge("I")) - self.assert_image_equal(lena("F"), split_merge("F")) - self.assert_image_equal(lena("P"), split_merge("P")) - self.assert_image_equal(lena("RGB"), split_merge("RGB")) - self.assert_image_equal(lena("RGBA"), split_merge("RGBA")) - self.assert_image_equal(lena("CMYK"), split_merge("CMYK")) - self.assert_image_equal(lena("YCbCr"), split_merge("YCbCr")) + return Image.merge(mode, hopper(mode).split()) + self.assert_image_equal(hopper("1"), split_merge("1")) + self.assert_image_equal(hopper("L"), split_merge("L")) + self.assert_image_equal(hopper("I"), split_merge("I")) + self.assert_image_equal(hopper("F"), split_merge("F")) + self.assert_image_equal(hopper("P"), split_merge("P")) + self.assert_image_equal(hopper("RGB"), split_merge("RGB")) + self.assert_image_equal(hopper("RGBA"), split_merge("RGBA")) + self.assert_image_equal(hopper("CMYK"), split_merge("CMYK")) + self.assert_image_equal(hopper("YCbCr"), split_merge("YCbCr")) def test_split_open(self): codecs = dir(Image.core) if 'zip_encoder' in codecs: - file = self.tempfile("temp.png") + test_file = self.tempfile("temp.png") else: - file = self.tempfile("temp.pcx") + test_file = self.tempfile("temp.pcx") def split_open(mode): - lena(mode).save(file) - im = Image.open(file) + hopper(mode).save(test_file) + im = Image.open(test_file) return len(im.split()) self.assertEqual(split_open("1"), 1) self.assertEqual(split_open("L"), 1) diff --git a/Tests/test_image_thumbnail.py b/Tests/test_image_thumbnail.py index ee49be43e..d4c676bbd 100644 --- a/Tests/test_image_thumbnail.py +++ b/Tests/test_image_thumbnail.py @@ -1,38 +1,38 @@ -from helper import unittest, PillowTestCase, lena +from helper import unittest, PillowTestCase, hopper class TestImageThumbnail(PillowTestCase): def test_sanity(self): - im = lena() + im = hopper() im.thumbnail((100, 100)) self.assert_image(im, im.mode, (100, 100)) def test_aspect(self): - im = lena() + im = hopper() im.thumbnail((100, 100)) self.assert_image(im, im.mode, (100, 100)) - im = lena().resize((128, 256)) + im = hopper().resize((128, 256)) im.thumbnail((100, 100)) self.assert_image(im, im.mode, (50, 100)) - im = lena().resize((128, 256)) + im = hopper().resize((128, 256)) im.thumbnail((50, 100)) self.assert_image(im, im.mode, (50, 100)) - im = lena().resize((256, 128)) + im = hopper().resize((256, 128)) im.thumbnail((100, 100)) self.assert_image(im, im.mode, (100, 50)) - im = lena().resize((256, 128)) + im = hopper().resize((256, 128)) im.thumbnail((100, 50)) self.assert_image(im, im.mode, (100, 50)) - im = lena().resize((128, 128)) + im = hopper().resize((128, 128)) im.thumbnail((100, 100)) self.assert_image(im, im.mode, (100, 100)) diff --git a/Tests/test_image_tobitmap.py b/Tests/test_image_tobitmap.py index 56b5ef001..e26a225c2 100644 --- a/Tests/test_image_tobitmap.py +++ b/Tests/test_image_tobitmap.py @@ -1,14 +1,14 @@ -from helper import unittest, PillowTestCase, lena, fromstring +from helper import unittest, PillowTestCase, hopper, fromstring class TestImageToBitmap(PillowTestCase): def test_sanity(self): - self.assertRaises(ValueError, lambda: lena().tobitmap()) - lena().convert("1").tobitmap() + self.assertRaises(ValueError, lambda: hopper().tobitmap()) + hopper().convert("1").tobitmap() - im1 = lena().convert("1") + im1 = hopper().convert("1") bitmap = im1.tobitmap() diff --git a/Tests/test_image_tobytes.py b/Tests/test_image_tobytes.py index 6dbf7d6f2..31a7e1722 100644 --- a/Tests/test_image_tobytes.py +++ b/Tests/test_image_tobytes.py @@ -1,10 +1,10 @@ -from helper import unittest, PillowTestCase, lena +from helper import unittest, PillowTestCase, hopper class TestImageToBytes(PillowTestCase): def test_sanity(self): - data = lena().tobytes() + data = hopper().tobytes() self.assertTrue(isinstance(data, bytes)) if __name__ == '__main__': diff --git a/Tests/test_image_transform.py b/Tests/test_image_transform.py index 42a3d78f3..26ff8822c 100644 --- a/Tests/test_image_transform.py +++ b/Tests/test_image_transform.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase, lena +from helper import unittest, PillowTestCase, hopper from PIL import Image @@ -22,7 +22,7 @@ class TestImageTransform(PillowTestCase): im.transform((100, 100), transform) def test_extent(self): - im = lena('RGB') + im = hopper('RGB') (w, h) = im.size transformed = im.transform(im.size, Image.EXTENT, (0, 0, @@ -32,11 +32,11 @@ class TestImageTransform(PillowTestCase): scaled = im.resize((w*2, h*2), Image.BILINEAR).crop((0, 0, w, h)) # undone -- precision? - self.assert_image_similar(transformed, scaled, 10) + self.assert_image_similar(transformed, scaled, 23) def test_quad(self): # one simple quad transform, equivalent to scale & crop upper left quad - im = lena('RGB') + im = hopper('RGB') (w, h) = im.size transformed = im.transform(im.size, Image.QUAD, (0, 0, 0, h//2, @@ -44,13 +44,15 @@ class TestImageTransform(PillowTestCase): w//2, h//2, w//2, 0), Image.BILINEAR) - scaled = im.resize((w*2, h*2), Image.BILINEAR).crop((0, 0, w, h)) + scaled = im.transform((w, h), Image.AFFINE, + (.5, 0, 0, 0, .5, 0), + Image.BILINEAR) self.assert_image_equal(transformed, scaled) def test_mesh(self): - # this should be a checkerboard of halfsized lenas in ul, lr - im = lena('RGBA') + # this should be a checkerboard of halfsized hoppers in ul, lr + im = hopper('RGBA') (w, h) = im.size transformed = im.transform(im.size, Image.MESH, [((0, 0, w//2, h//2), # box @@ -61,9 +63,9 @@ class TestImageTransform(PillowTestCase): w, h, w, 0))], # ul -> ccw around quad Image.BILINEAR) - # transformed.save('transformed.png') - - scaled = im.resize((w//2, h//2), Image.BILINEAR) + scaled = im.transform((w//2, h//2), Image.AFFINE, + (2, 0, 0, 0, 2, 0), + Image.BILINEAR) checker = Image.new('RGBA', im.size) checker.paste(scaled, (0, 0)) @@ -126,12 +128,13 @@ class TestImageTransform(PillowTestCase): # Running by default, but I'd totally understand not doing it in # the future - foo = [ + pattern = [ Image.new('RGBA', (1024, 1024), (a, a, a, a)) - for a in range(1, 65)] + for a in range(1, 65) + ] # Yeah. Watch some JIT optimize this out. - foo = None + pattern = None self.test_mesh() diff --git a/Tests/test_image_transpose.py b/Tests/test_image_transpose.py index f13e54ee7..e13fc8605 100644 --- a/Tests/test_image_transpose.py +++ b/Tests/test_image_transpose.py @@ -1,30 +1,115 @@ -from helper import unittest, PillowTestCase, lena +import helper +from helper import unittest, PillowTestCase -from PIL import Image - -FLIP_LEFT_RIGHT = Image.FLIP_LEFT_RIGHT -FLIP_TOP_BOTTOM = Image.FLIP_TOP_BOTTOM -ROTATE_90 = Image.ROTATE_90 -ROTATE_180 = Image.ROTATE_180 -ROTATE_270 = Image.ROTATE_270 +from PIL.Image import (FLIP_LEFT_RIGHT, FLIP_TOP_BOTTOM, ROTATE_90, ROTATE_180, + ROTATE_270, TRANSPOSE) class TestImageTranspose(PillowTestCase): - def test_sanity(self): + hopper = { + 'L': helper.hopper('L').crop((0, 0, 121, 127)).copy(), + 'RGB': helper.hopper('RGB').crop((0, 0, 121, 127)).copy(), + } - im = lena() + def test_flip_left_right(self): + def transpose(mode): + im = self.hopper[mode] + out = im.transpose(FLIP_LEFT_RIGHT) + self.assertEqual(out.mode, mode) + self.assertEqual(out.size, im.size) - im.transpose(FLIP_LEFT_RIGHT) - im.transpose(FLIP_TOP_BOTTOM) + x, y = im.size + self.assertEqual(im.getpixel((1, 1)), out.getpixel((x-2, 1))) + self.assertEqual(im.getpixel((x-2, 1)), out.getpixel((1, 1))) + self.assertEqual(im.getpixel((1, y-2)), out.getpixel((x-2, y-2))) + self.assertEqual(im.getpixel((x-2, y-2)), out.getpixel((1, y-2))) - im.transpose(ROTATE_90) - im.transpose(ROTATE_180) - im.transpose(ROTATE_270) + for mode in ("L", "RGB"): + transpose(mode) + + def test_flip_top_bottom(self): + def transpose(mode): + im = self.hopper[mode] + out = im.transpose(FLIP_TOP_BOTTOM) + self.assertEqual(out.mode, mode) + self.assertEqual(out.size, im.size) + + x, y = im.size + self.assertEqual(im.getpixel((1, 1)), out.getpixel((1, y-2))) + self.assertEqual(im.getpixel((x-2, 1)), out.getpixel((x-2, y-2))) + self.assertEqual(im.getpixel((1, y-2)), out.getpixel((1, 1))) + self.assertEqual(im.getpixel((x-2, y-2)), out.getpixel((x-2, 1))) + + for mode in ("L", "RGB"): + transpose(mode) + + def test_rotate_90(self): + def transpose(mode): + im = self.hopper[mode] + out = im.transpose(ROTATE_90) + self.assertEqual(out.mode, mode) + self.assertEqual(out.size, im.size[::-1]) + + x, y = im.size + self.assertEqual(im.getpixel((1, 1)), out.getpixel((1, x-2))) + self.assertEqual(im.getpixel((x-2, 1)), out.getpixel((1, 1))) + self.assertEqual(im.getpixel((1, y-2)), out.getpixel((y-2, x-2))) + self.assertEqual(im.getpixel((x-2, y-2)), out.getpixel((y-2, 1))) + + for mode in ("L", "RGB"): + transpose(mode) + + def test_rotate_180(self): + def transpose(mode): + im = self.hopper[mode] + out = im.transpose(ROTATE_180) + self.assertEqual(out.mode, mode) + self.assertEqual(out.size, im.size) + + x, y = im.size + self.assertEqual(im.getpixel((1, 1)), out.getpixel((x-2, y-2))) + self.assertEqual(im.getpixel((x-2, 1)), out.getpixel((1, y-2))) + self.assertEqual(im.getpixel((1, y-2)), out.getpixel((x-2, 1))) + self.assertEqual(im.getpixel((x-2, y-2)), out.getpixel((1, 1))) + + for mode in ("L", "RGB"): + transpose(mode) + + def test_rotate_270(self): + def transpose(mode): + im = self.hopper[mode] + out = im.transpose(ROTATE_270) + self.assertEqual(out.mode, mode) + self.assertEqual(out.size, im.size[::-1]) + + x, y = im.size + self.assertEqual(im.getpixel((1, 1)), out.getpixel((y-2, 1))) + self.assertEqual(im.getpixel((x-2, 1)), out.getpixel((y-2, x-2))) + self.assertEqual(im.getpixel((1, y-2)), out.getpixel((1, 1))) + self.assertEqual(im.getpixel((x-2, y-2)), out.getpixel((1, x-2))) + + for mode in ("L", "RGB"): + transpose(mode) + + def test_transpose(self): + def transpose(mode): + im = self.hopper[mode] + out = im.transpose(TRANSPOSE) + self.assertEqual(out.mode, mode) + self.assertEqual(out.size, im.size[::-1]) + + x, y = im.size + self.assertEqual(im.getpixel((1, 1)), out.getpixel((1, 1))) + self.assertEqual(im.getpixel((x-2, 1)), out.getpixel((1, x-2))) + self.assertEqual(im.getpixel((1, y-2)), out.getpixel((y-2, 1))) + self.assertEqual(im.getpixel((x-2, y-2)), out.getpixel((y-2, x-2))) + + for mode in ("L", "RGB"): + transpose(mode) def test_roundtrip(self): - - im = lena() + im = self.hopper['L'] def transpose(first, second): return im.transpose(first).transpose(second) @@ -33,12 +118,13 @@ class TestImageTranspose(PillowTestCase): im, transpose(FLIP_LEFT_RIGHT, FLIP_LEFT_RIGHT)) self.assert_image_equal( im, transpose(FLIP_TOP_BOTTOM, FLIP_TOP_BOTTOM)) - self.assert_image_equal(im, transpose(ROTATE_90, ROTATE_270)) self.assert_image_equal(im, transpose(ROTATE_180, ROTATE_180)) + self.assert_image_equal( + im.transpose(TRANSPOSE), transpose(ROTATE_90, FLIP_TOP_BOTTOM)) + self.assert_image_equal( + im.transpose(TRANSPOSE), transpose(ROTATE_270, FLIP_LEFT_RIGHT)) if __name__ == '__main__': unittest.main() - -# End of file diff --git a/Tests/test_imagechops.py b/Tests/test_imagechops.py index 552314fd1..a3b7fa606 100644 --- a/Tests/test_imagechops.py +++ b/Tests/test_imagechops.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase, lena +from helper import unittest, PillowTestCase, hopper from PIL import Image from PIL import ImageChops @@ -8,7 +8,7 @@ class TestImageChops(PillowTestCase): def test_sanity(self): - im = lena("L") + im = hopper("L") ImageChops.constant(im, 128) ImageChops.duplicate(im) diff --git a/Tests/test_imagecms.py b/Tests/test_imagecms.py index e731c8945..7a2a57151 100644 --- a/Tests/test_imagecms.py +++ b/Tests/test_imagecms.py @@ -1,9 +1,10 @@ -from helper import unittest, PillowTestCase, lena +from helper import unittest, PillowTestCase, hopper from PIL import Image from io import BytesIO - +import os + try: from PIL import ImageCms from PIL.ImageCms import ImageCmsProfile @@ -13,7 +14,8 @@ except ImportError as v: pass -SRGB = "Tests/icc/sRGB.icm" +SRGB = "Tests/icc/sRGB_IEC61966-2-1_black_scaled.icc" +HAVE_PROFILE = os.path.exists(SRGB) class TestImageCms(PillowTestCase): @@ -26,6 +28,10 @@ class TestImageCms(PillowTestCase): except ImportError as v: self.skipTest(v) + def skip_missing(self): + if not HAVE_PROFILE: + self.skipTest("SRGB profile not available") + def test_sanity(self): # basic smoke test. @@ -38,70 +44,78 @@ class TestImageCms(PillowTestCase): # internal version number self.assertRegexpMatches(ImageCms.core.littlecms_version, "\d+\.\d+$") - i = ImageCms.profileToProfile(lena(), SRGB, SRGB) + self.skip_missing() + i = ImageCms.profileToProfile(hopper(), SRGB, SRGB) self.assert_image(i, "RGB", (128, 128)) - i = lena() + i = hopper() ImageCms.profileToProfile(i, SRGB, SRGB, inPlace=True) self.assert_image(i, "RGB", (128, 128)) t = ImageCms.buildTransform(SRGB, SRGB, "RGB", "RGB") - i = ImageCms.applyTransform(lena(), t) + i = ImageCms.applyTransform(hopper(), t) self.assert_image(i, "RGB", (128, 128)) - i = lena() + i = hopper() t = ImageCms.buildTransform(SRGB, SRGB, "RGB", "RGB") - ImageCms.applyTransform(lena(), t, inPlace=True) + ImageCms.applyTransform(hopper(), t, inPlace=True) self.assert_image(i, "RGB", (128, 128)) p = ImageCms.createProfile("sRGB") o = ImageCms.getOpenProfile(SRGB) t = ImageCms.buildTransformFromOpenProfiles(p, o, "RGB", "RGB") - i = ImageCms.applyTransform(lena(), t) + i = ImageCms.applyTransform(hopper(), t) self.assert_image(i, "RGB", (128, 128)) t = ImageCms.buildProofTransform(SRGB, SRGB, SRGB, "RGB", "RGB") self.assertEqual(t.inputMode, "RGB") self.assertEqual(t.outputMode, "RGB") - i = ImageCms.applyTransform(lena(), t) + i = ImageCms.applyTransform(hopper(), t) self.assert_image(i, "RGB", (128, 128)) # test PointTransform convenience API - lena().point(t) + hopper().point(t) def test_name(self): + self.skip_missing() # get profile information for file self.assertEqual( ImageCms.getProfileName(SRGB).strip(), - 'IEC 61966-2.1 Default RGB colour space - sRGB') + 'IEC 61966-2-1 Default RGB Colour Space - sRGB') def test_info(self): + self.skip_missing() self.assertEqual( ImageCms.getProfileInfo(SRGB).splitlines(), [ - 'sRGB IEC61966-2.1', '', - 'Copyright (c) 1998 Hewlett-Packard Company', '']) + 'sRGB IEC61966-2-1 black scaled', '', + 'Copyright International Color Consortium, 2009', '']) def test_copyright(self): + self.skip_missing() self.assertEqual( ImageCms.getProfileCopyright(SRGB).strip(), - 'Copyright (c) 1998 Hewlett-Packard Company') + 'Copyright International Color Consortium, 2009') def test_manufacturer(self): + self.skip_missing() self.assertEqual( ImageCms.getProfileManufacturer(SRGB).strip(), - 'IEC http://www.iec.ch') + '') def test_model(self): + self.skip_missing() self.assertEqual( ImageCms.getProfileModel(SRGB).strip(), - 'IEC 61966-2.1 Default RGB colour space - sRGB') + 'IEC 61966-2-1 Default RGB Colour Space - sRGB') def test_description(self): + self.skip_missing() self.assertEqual( ImageCms.getProfileDescription(SRGB).strip(), - 'sRGB IEC61966-2.1') + 'sRGB IEC61966-2-1 black scaled') def test_intent(self): + self.skip_missing() self.assertEqual(ImageCms.getDefaultIntent(SRGB), 0) self.assertEqual(ImageCms.isIntentSupported( SRGB, ImageCms.INTENT_ABSOLUTE_COLORIMETRIC, @@ -132,13 +146,14 @@ class TestImageCms(PillowTestCase): # the procedural pyCMS API uses PyCMSError for all sorts of errors self.assertRaises( ImageCms.PyCMSError, - lambda: ImageCms.profileToProfile(lena(), "foo", "bar")) + lambda: ImageCms.profileToProfile(hopper(), "foo", "bar")) self.assertRaises( ImageCms.PyCMSError, lambda: ImageCms.buildTransform("foo", "bar", "RGB", "RGB")) self.assertRaises( ImageCms.PyCMSError, lambda: ImageCms.getProfileName(None)) + self.skip_missing() self.assertRaises( ImageCms.PyCMSError, lambda: ImageCms.isIntentSupported(SRGB, None, None)) @@ -154,8 +169,9 @@ class TestImageCms(PillowTestCase): def test_simple_lab(self): i = Image.new('RGB', (10, 10), (128, 128, 128)) + psRGB = ImageCms.createProfile("sRGB") pLab = ImageCms.createProfile("LAB") - t = ImageCms.buildTransform(SRGB, pLab, "RGB", "LAB") + t = ImageCms.buildTransform(psRGB, pLab, "RGB", "LAB") i_lab = ImageCms.applyTransform(i, t) @@ -165,65 +181,66 @@ class TestImageCms(PillowTestCase): # not a linear luminance map. so L != 128: self.assertEqual(k, (137, 128, 128)) - L = i_lab.getdata(0) + l = i_lab.getdata(0) a = i_lab.getdata(1) b = i_lab.getdata(2) - self.assertEqual(list(L), [137] * 100) + self.assertEqual(list(l), [137] * 100) self.assertEqual(list(a), [128] * 100) self.assertEqual(list(b), [128] * 100) def test_lab_color(self): + psRGB = ImageCms.createProfile("sRGB") pLab = ImageCms.createProfile("LAB") - t = ImageCms.buildTransform(SRGB, pLab, "RGB", "LAB") + t = ImageCms.buildTransform(psRGB, pLab, "RGB", "LAB") + # Need to add a type mapping for some PIL type to TYPE_Lab_8 in # findLCMSType, and have that mapping work back to a PIL mode # (likely RGB). - i = ImageCms.applyTransform(lena(), t) + i = ImageCms.applyTransform(hopper(), t) self.assert_image(i, "LAB", (128, 128)) # i.save('temp.lab.tif') # visually verified vs PS. - target = Image.open('Tests/images/lena.Lab.tif') + target = Image.open('Tests/images/hopper.Lab.tif') self.assert_image_similar(i, target, 30) def test_lab_srgb(self): + psRGB = ImageCms.createProfile("sRGB") pLab = ImageCms.createProfile("LAB") - t = ImageCms.buildTransform(pLab, SRGB, "LAB", "RGB") + t = ImageCms.buildTransform(pLab, psRGB, "LAB", "RGB") - img = Image.open('Tests/images/lena.Lab.tif') + img = Image.open('Tests/images/hopper.Lab.tif') img_srgb = ImageCms.applyTransform(img, t) # img_srgb.save('temp.srgb.tif') # visually verified vs ps. - self.assert_image_similar(lena(), img_srgb, 30) + self.assert_image_similar(hopper(), img_srgb, 30) self.assertTrue(img_srgb.info['icc_profile']) profile = ImageCmsProfile(BytesIO(img_srgb.info['icc_profile'])) - self.assertTrue('sRGB' in ImageCms.getProfileDescription(profile)) - + self.assertTrue('sRGB' in ImageCms.getProfileDescription(profile)) def test_lab_roundtrip(self): # check to see if we're at least internally consistent. + psRGB = ImageCms.createProfile("sRGB") pLab = ImageCms.createProfile("LAB") - t = ImageCms.buildTransform(SRGB, pLab, "RGB", "LAB") + t = ImageCms.buildTransform(psRGB, pLab, "RGB", "LAB") - t2 = ImageCms.buildTransform(pLab, SRGB, "LAB", "RGB") + t2 = ImageCms.buildTransform(pLab, psRGB, "LAB", "RGB") - i = ImageCms.applyTransform(lena(), t) + i = ImageCms.applyTransform(hopper(), t) self.assertEqual(i.info['icc_profile'], ImageCmsProfile(pLab).tobytes()) - + out = ImageCms.applyTransform(i, t2) - self.assert_image_similar(lena(), out, 2) - + self.assert_image_similar(hopper(), out, 2) def test_profile_tobytes(self): - from io import BytesIO i = Image.open("Tests/images/rgb.jpg") p = ImageCms.getOpenProfile(BytesIO(i.info["icc_profile"])) @@ -231,14 +248,12 @@ class TestImageCms(PillowTestCase): # not the same bytes as the original icc_profile, # but it does roundtrip - self.assertEqual(p.tobytes(),p2.tobytes()) + self.assertEqual(p.tobytes(), p2.tobytes()) self.assertEqual(ImageCms.getProfileName(p), ImageCms.getProfileName(p2)) self.assertEqual(ImageCms.getProfileDescription(p), ImageCms.getProfileDescription(p2)) - - if __name__ == '__main__': unittest.main() diff --git a/Tests/test_imagedraw.py b/Tests/test_imagedraw.py index b632da73b..a1ed20a3a 100644 --- a/Tests/test_imagedraw.py +++ b/Tests/test_imagedraw.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase, lena +from helper import unittest, PillowTestCase, hopper from PIL import Image from PIL import ImageColor @@ -34,7 +34,7 @@ POINTS2 = [10, 10, 20, 40, 30, 30] class TestImageDraw(PillowTestCase): def test_sanity(self): - im = lena("RGB").copy() + im = hopper("RGB").copy() draw = ImageDraw.ImageDraw(im) draw = ImageDraw.Draw(im) @@ -45,7 +45,7 @@ class TestImageDraw(PillowTestCase): draw.rectangle(list(range(4))) def test_deprecated(self): - im = lena().copy() + im = hopper().copy() draw = ImageDraw.Draw(im) @@ -63,8 +63,8 @@ class TestImageDraw(PillowTestCase): del draw # Assert - self.assert_image_equal( - im, Image.open("Tests/images/imagedraw_arc.png")) + self.assert_image_similar( + im, Image.open("Tests/images/imagedraw_arc.png"), 1) def test_arc1(self): self.helper_arc(BBOX1) @@ -96,8 +96,8 @@ class TestImageDraw(PillowTestCase): del draw # Assert - self.assert_image_equal( - im, Image.open("Tests/images/imagedraw_chord.png")) + self.assert_image_similar( + im, Image.open("Tests/images/imagedraw_chord.png"), 1) def test_chord1(self): self.helper_chord(BBOX1) @@ -115,8 +115,8 @@ class TestImageDraw(PillowTestCase): del draw # Assert - self.assert_image_equal( - im, Image.open("Tests/images/imagedraw_ellipse.png")) + self.assert_image_similar( + im, Image.open("Tests/images/imagedraw_ellipse.png"), 1) def test_ellipse1(self): self.helper_ellipse(BBOX1) @@ -153,8 +153,8 @@ class TestImageDraw(PillowTestCase): del draw # Assert - self.assert_image_equal( - im, Image.open("Tests/images/imagedraw_pieslice.png")) + self.assert_image_similar( + im, Image.open("Tests/images/imagedraw_pieslice.png"), 1) def test_pieslice1(self): self.helper_pieslice(BBOX1) @@ -255,7 +255,6 @@ class TestImageDraw(PillowTestCase): self.assert_image_equal( im, Image.open("Tests/images/imagedraw_floodfill2.png")) - def create_base_image_draw(self, size, mode=DEFAULT_MODE, background1=WHITE, @@ -267,23 +266,25 @@ class TestImageDraw(PillowTestCase): img.putpixel((x, y), background2) return (img, ImageDraw.Draw(img)) - def test_square(self): expected = Image.open(os.path.join(IMAGES_PATH, 'square.png')) expected.load() img, draw = self.create_base_image_draw((10, 10)) draw.polygon([(2, 2), (2, 7), (7, 7), (7, 2)], BLACK) - self.assert_image_equal(img, expected, 'square as normal polygon failed') + self.assert_image_equal(img, expected, + 'square as normal polygon failed') img, draw = self.create_base_image_draw((10, 10)) draw.polygon([(7, 7), (7, 2), (2, 2), (2, 7)], BLACK) - self.assert_image_equal(img, expected, 'square as inverted polygon failed') + self.assert_image_equal(img, expected, + 'square as inverted polygon failed') img, draw = self.create_base_image_draw((10, 10)) draw.rectangle((2, 2, 7, 7), BLACK) - self.assert_image_equal(img, expected, 'square as normal rectangle failed') + self.assert_image_equal(img, expected, + 'square as normal rectangle failed') img, draw = self.create_base_image_draw((10, 10)) draw.rectangle((7, 7, 2, 2), BLACK) - self.assert_image_equal(img, expected, 'square as inverted rectangle failed') - + self.assert_image_equal( + img, expected, 'square as inverted rectangle failed') def test_triangle_right(self): expected = Image.open(os.path.join(IMAGES_PATH, 'triangle_right.png')) @@ -292,93 +293,117 @@ class TestImageDraw(PillowTestCase): draw.polygon([(3, 5), (17, 5), (10, 12)], BLACK) self.assert_image_equal(img, expected, 'triangle right failed') - def test_line_horizontal(self): - expected = Image.open(os.path.join(IMAGES_PATH, 'line_horizontal_w2px_normal.png')) + expected = Image.open(os.path.join(IMAGES_PATH, + 'line_horizontal_w2px_normal.png')) expected.load() img, draw = self.create_base_image_draw((20, 20)) draw.line((5, 5, 14, 5), BLACK, 2) - self.assert_image_equal(img, expected, 'line straigth horizontal normal 2px wide failed') - expected = Image.open(os.path.join(IMAGES_PATH, 'line_horizontal_w2px_inverted.png')) + self.assert_image_equal( + img, expected, 'line straight horizontal normal 2px wide failed') + expected = Image.open(os.path.join(IMAGES_PATH, + 'line_horizontal_w2px_inverted.png')) expected.load() img, draw = self.create_base_image_draw((20, 20)) draw.line((14, 5, 5, 5), BLACK, 2) - self.assert_image_equal(img, expected, 'line straigth horizontal inverted 2px wide failed') - expected = Image.open(os.path.join(IMAGES_PATH, 'line_horizontal_w3px.png')) + self.assert_image_equal( + img, expected, 'line straight horizontal inverted 2px wide failed') + expected = Image.open(os.path.join(IMAGES_PATH, + 'line_horizontal_w3px.png')) expected.load() img, draw = self.create_base_image_draw((20, 20)) draw.line((5, 5, 14, 5), BLACK, 3) - self.assert_image_equal(img, expected, 'line straigth horizontal normal 3px wide failed') + self.assert_image_equal( + img, expected, 'line straight horizontal normal 3px wide failed') img, draw = self.create_base_image_draw((20, 20)) draw.line((14, 5, 5, 5), BLACK, 3) - self.assert_image_equal(img, expected, 'line straigth horizontal inverted 3px wide failed') - expected = Image.open(os.path.join(IMAGES_PATH, 'line_horizontal_w101px.png')) + self.assert_image_equal( + img, expected, 'line straight horizontal inverted 3px wide failed') + expected = Image.open(os.path.join(IMAGES_PATH, + 'line_horizontal_w101px.png')) expected.load() img, draw = self.create_base_image_draw((200, 110)) draw.line((5, 55, 195, 55), BLACK, 101) - self.assert_image_equal(img, expected, 'line straigth horizontal 101px wide failed') + self.assert_image_equal( + img, expected, 'line straight horizontal 101px wide failed') def test_line_h_s1_w2(self): self.skipTest('failing') - expected = Image.open(os.path.join(IMAGES_PATH, 'line_horizontal_slope1px_w2px.png')) + expected = Image.open(os.path.join(IMAGES_PATH, + 'line_horizontal_slope1px_w2px.png')) expected.load() img, draw = self.create_base_image_draw((20, 20)) draw.line((5, 5, 14, 6), BLACK, 2) - self.assert_image_equal(img, expected, 'line horizontal 1px slope 2px wide failed') - + self.assert_image_equal( + img, expected, 'line horizontal 1px slope 2px wide failed') def test_line_vertical(self): - expected = Image.open(os.path.join(IMAGES_PATH, 'line_vertical_w2px_normal.png')) + expected = Image.open(os.path.join(IMAGES_PATH, + 'line_vertical_w2px_normal.png')) expected.load() img, draw = self.create_base_image_draw((20, 20)) draw.line((5, 5, 5, 14), BLACK, 2) - self.assert_image_equal(img, expected, 'line straigth vertical normal 2px wide failed') - expected = Image.open(os.path.join(IMAGES_PATH, 'line_vertical_w2px_inverted.png')) + self.assert_image_equal( + img, expected, 'line straight vertical normal 2px wide failed') + expected = Image.open(os.path.join(IMAGES_PATH, + 'line_vertical_w2px_inverted.png')) expected.load() img, draw = self.create_base_image_draw((20, 20)) draw.line((5, 14, 5, 5), BLACK, 2) - self.assert_image_equal(img, expected, 'line straigth vertical inverted 2px wide failed') - expected = Image.open(os.path.join(IMAGES_PATH, 'line_vertical_w3px.png')) + self.assert_image_equal( + img, expected, 'line straight vertical inverted 2px wide failed') + expected = Image.open(os.path.join(IMAGES_PATH, + 'line_vertical_w3px.png')) expected.load() img, draw = self.create_base_image_draw((20, 20)) draw.line((5, 5, 5, 14), BLACK, 3) - self.assert_image_equal(img, expected, 'line straigth vertical normal 3px wide failed') + self.assert_image_equal( + img, expected, 'line straight vertical normal 3px wide failed') img, draw = self.create_base_image_draw((20, 20)) draw.line((5, 14, 5, 5), BLACK, 3) - self.assert_image_equal(img, expected, 'line straigth vertical inverted 3px wide failed') - expected = Image.open(os.path.join(IMAGES_PATH, 'line_vertical_w101px.png')) + self.assert_image_equal( + img, expected, 'line straight vertical inverted 3px wide failed') + expected = Image.open(os.path.join(IMAGES_PATH, + 'line_vertical_w101px.png')) expected.load() img, draw = self.create_base_image_draw((110, 200)) draw.line((55, 5, 55, 195), BLACK, 101) - self.assert_image_equal(img, expected, 'line straigth vertical 101px wide failed') - expected = Image.open(os.path.join(IMAGES_PATH, 'line_vertical_slope1px_w2px.png')) + self.assert_image_equal(img, expected, + 'line straight vertical 101px wide failed') + expected = Image.open(os.path.join(IMAGES_PATH, + 'line_vertical_slope1px_w2px.png')) expected.load() img, draw = self.create_base_image_draw((20, 20)) draw.line((5, 5, 6, 14), BLACK, 2) - self.assert_image_equal(img, expected, 'line vertical 1px slope 2px wide failed') - + self.assert_image_equal(img, expected, + 'line vertical 1px slope 2px wide failed') def test_line_oblique_45(self): - expected = Image.open(os.path.join(IMAGES_PATH, 'line_oblique_45_w3px_a.png')) + expected = Image.open(os.path.join(IMAGES_PATH, + 'line_oblique_45_w3px_a.png')) expected.load() img, draw = self.create_base_image_draw((20, 20)) draw.line((5, 5, 14, 14), BLACK, 3) - self.assert_image_equal(img, expected, 'line oblique 45 normal 3px wide A failed') + self.assert_image_equal(img, expected, + 'line oblique 45 normal 3px wide A failed') img, draw = self.create_base_image_draw((20, 20)) draw.line((14, 14, 5, 5), BLACK, 3) - self.assert_image_equal(img, expected, 'line oblique 45 inverted 3px wide A failed') - expected = Image.open(os.path.join(IMAGES_PATH, 'line_oblique_45_w3px_b.png')) + self.assert_image_equal(img, expected, + 'line oblique 45 inverted 3px wide A failed') + expected = Image.open(os.path.join(IMAGES_PATH, + 'line_oblique_45_w3px_b.png')) expected.load() img, draw = self.create_base_image_draw((20, 20)) draw.line((14, 5, 5, 14), BLACK, 3) - self.assert_image_equal(img, expected, 'line oblique 45 normal 3px wide B failed') + self.assert_image_equal(img, expected, + 'line oblique 45 normal 3px wide B failed') img, draw = self.create_base_image_draw((20, 20)) draw.line((5, 14, 14, 5), BLACK, 3) - self.assert_image_equal(img, expected, 'line oblique 45 inverted 3px wide B failed') + self.assert_image_equal(img, expected, + 'line oblique 45 inverted 3px wide B failed') if __name__ == '__main__': unittest.main() # End of file - diff --git a/Tests/test_imageenhance.py b/Tests/test_imageenhance.py index 433c49cf6..82ba2e5db 100644 --- a/Tests/test_imageenhance.py +++ b/Tests/test_imageenhance.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase, lena +from helper import unittest, PillowTestCase, hopper from PIL import Image from PIL import ImageEnhance @@ -10,10 +10,10 @@ class TestImageEnhance(PillowTestCase): # FIXME: assert_image # Implicit asserts no exception: - ImageEnhance.Color(lena()).enhance(0.5) - ImageEnhance.Contrast(lena()).enhance(0.5) - ImageEnhance.Brightness(lena()).enhance(0.5) - ImageEnhance.Sharpness(lena()).enhance(0.5) + ImageEnhance.Color(hopper()).enhance(0.5) + ImageEnhance.Contrast(hopper()).enhance(0.5) + ImageEnhance.Brightness(hopper()).enhance(0.5) + ImageEnhance.Sharpness(hopper()).enhance(0.5) def test_crash(self): @@ -21,6 +21,33 @@ class TestImageEnhance(PillowTestCase): im = Image.new("RGB", (1, 1)) ImageEnhance.Sharpness(im).enhance(0.5) + def _half_transparent_image(self): + # returns an image, half transparent, half solid + im = hopper('RGB') + + transparent = Image.new('L', im.size, 0) + solid = Image.new('L', (im.size[0]//2, im.size[1]), 255) + transparent.paste(solid, (0, 0)) + im.putalpha(transparent) + + return im + + def _check_alpha(self, im, original, op, amount): + self.assertEqual(im.getbands(), original.getbands()) + self.assert_image_equal(im.split()[-1], original.split()[-1], + "Diff on %s: %s" % (op, amount)) + + def test_alpha(self): + # Issue https://github.com/python-pillow/Pillow/issues/899 + # Is alpha preserved through image enhancement? + + original = self._half_transparent_image() + + for op in ['Color', 'Brightness', 'Contrast', 'Sharpness']: + for amount in [0, 0.5, 1.0]: + self._check_alpha(getattr(ImageEnhance, op)(original).enhance(amount), + original, op, amount) + if __name__ == '__main__': unittest.main() diff --git a/Tests/test_imagefile.py b/Tests/test_imagefile.py index 78fb3427f..5311b899f 100644 --- a/Tests/test_imagefile.py +++ b/Tests/test_imagefile.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase, lena, fromstring, tostring +from helper import unittest, PillowTestCase, hopper, fromstring, tostring from io import BytesIO @@ -20,15 +20,15 @@ class TestImageFile(PillowTestCase): def roundtrip(format): - im = lena("L").resize((1000, 1000)) + im = hopper("L").resize((1000, 1000)) if format in ("MSP", "XBM"): im = im.convert("1") - file = BytesIO() + test_file = BytesIO() - im.save(file, format) + im.save(test_file, format) - data = file.getvalue() + data = test_file.getvalue() parser = ImageFile.Parser() parser.feed(data) @@ -55,6 +55,12 @@ class TestImageFile(PillowTestCase): if EpsImagePlugin.has_ghostscript(): im1, im2 = roundtrip("EPS") + # This test fails on Ubuntu 12.04, PPC (Bigendian) It + # appears to be a ghostscript 9.05 bug, since the + # ghostscript rendering is wonky and the file is identical + # to that written on ubuntu 12.04 x64 + # md5sum: ba974835ff2d6f3f2fd0053a23521d4a + # EPS comes back in RGB: self.assert_image_similar(im1, im2.convert('L'), 20) @@ -73,7 +79,7 @@ class TestImageFile(PillowTestCase): def test_safeblock(self): - im1 = lena() + im1 = hopper() if "zip_encoder" not in codecs: self.skipTest("PNG (zlib) encoder not available") diff --git a/Tests/test_imagefileio.py b/Tests/test_imagefileio.py index 32ee0bc5e..b06178437 100644 --- a/Tests/test_imagefileio.py +++ b/Tests/test_imagefileio.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase, lena, tostring +from helper import unittest, PillowTestCase, hopper, tostring from PIL import Image from PIL import ImageFileIO @@ -8,7 +8,7 @@ class TestImageFileIo(PillowTestCase): def test_fileio(self): - class DumbFile: + class DumbFile(object): def __init__(self, data): self.data = data @@ -19,7 +19,7 @@ class TestImageFileIo(PillowTestCase): def close(self): pass - im1 = lena() + im1 = hopper() io = ImageFileIO.ImageFileIO(DumbFile(tostring(im1, "PPM"))) diff --git a/Tests/test_imagefont.py b/Tests/test_imagefont.py index ed2439e7c..88858c717 100644 --- a/Tests/test_imagefont.py +++ b/Tests/test_imagefont.py @@ -4,6 +4,8 @@ from PIL import Image from PIL import ImageDraw from io import BytesIO import os +import sys +import copy FONT_PATH = "Tests/fonts/FreeMono.ttf" FONT_SIZE = 20 @@ -13,12 +15,53 @@ try: from PIL import ImageFont ImageFont.core.getfont # check if freetype is available + class SimplePatcher(object): + def __init__(self, parent_obj, attr_name, value): + self._parent_obj = parent_obj + self._attr_name = attr_name + self._saved = None + self._is_saved = False + self._value = value + + def __enter__(self): + # Patch the attr on the object + if hasattr(self._parent_obj, self._attr_name): + self._saved = getattr(self._parent_obj, self._attr_name) + setattr(self._parent_obj, self._attr_name, self._value) + self._is_saved = True + else: + setattr(self._parent_obj, self._attr_name, self._value) + self._is_saved = False + + def __exit__(self, type, value, traceback): + # Restore the original value + if self._is_saved: + setattr(self._parent_obj, self._attr_name, self._saved) + else: + delattr(self._parent_obj, self._attr_name) + class TestImageFont(PillowTestCase): def test_sanity(self): self.assertRegexpMatches( ImageFont.core.freetype2_version, "\d+\.\d+\.\d+$") + def test_font_properties(self): + ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) + self.assertEqual(ttf.path, FONT_PATH) + self.assertEqual(ttf.size, FONT_SIZE) + + ttf_copy = ttf.font_variant() + self.assertEqual(ttf_copy.path, FONT_PATH) + self.assertEqual(ttf_copy.size, FONT_SIZE) + + ttf_copy = ttf.font_variant(size=FONT_SIZE+1) + self.assertEqual(ttf_copy.size, FONT_SIZE+1) + + second_font_path = "Tests/fonts/DejaVuSans.ttf" + ttf_copy = ttf.font_variant(font=second_font_path) + self.assertEqual(ttf_copy.path, second_font_path) + def test_font_with_name(self): ImageFont.truetype(FONT_PATH, FONT_SIZE) self._render(FONT_PATH) @@ -51,7 +94,8 @@ try: def _render(self, font): txt = "Hello World!" ttf = ImageFont.truetype(font, FONT_SIZE) - w, h = ttf.getsize(txt) + ttf.getsize(txt) + img = Image.new("RGB", (256, 64), "white") d = ImageDraw.Draw(img) d.text((10, 10), txt, font=ttf, fill='black') @@ -83,7 +127,7 @@ try: target = 'Tests/images/rectangle_surrounding_text.png' target_img = Image.open(target) - self.assert_image_equal(im, target_img) + self.assert_image_similar(im, target_img, .5) def test_render_multiline(self): im = Image.new(mode='RGB', size=(300, 100)) @@ -192,6 +236,79 @@ try: # Assert self.assert_image_equal(im, target_img) + def _test_fake_loading_font(self, path_to_fake, fontname): + # Make a copy of FreeTypeFont so we can patch the original + free_type_font = copy.deepcopy(ImageFont.FreeTypeFont) + with SimplePatcher(ImageFont, '_FreeTypeFont', free_type_font): + def loadable_font(filepath, size, index, encoding): + if filepath == path_to_fake: + return ImageFont._FreeTypeFont(FONT_PATH, size, index, + encoding) + return ImageFont._FreeTypeFont(filepath, size, index, + encoding) + with SimplePatcher(ImageFont, 'FreeTypeFont', loadable_font): + font = ImageFont.truetype(fontname) + # Make sure it's loaded + name = font.getname() + self.assertEqual(('FreeMono', 'Regular'), name) + + @unittest.skipIf(sys.platform.startswith('win32'), + "requires Unix or MacOS") + def test_find_linux_font(self): + # A lot of mocking here - this is more for hitting code and + # catching syntax like errors + font_directory = '/usr/local/share/fonts' + with SimplePatcher(sys, 'platform', 'linux'): + patched_env = copy.deepcopy(os.environ) + patched_env['XDG_DATA_DIRS'] = '/usr/share/:/usr/local/share/' + with SimplePatcher(os, 'environ', patched_env): + def fake_walker(path): + if path == font_directory: + return [(path, [], [ + 'Arial.ttf', 'Single.otf', 'Duplicate.otf', + 'Duplicate.ttf'], )] + return [(path, [], ['some_random_font.ttf'], )] + with SimplePatcher(os, 'walk', fake_walker): + # Test that the font loads both with and without the + # extension + self._test_fake_loading_font( + font_directory+'/Arial.ttf', 'Arial.ttf') + self._test_fake_loading_font( + font_directory+'/Arial.ttf', 'Arial') + + # Test that non-ttf fonts can be found without the + # extension + self._test_fake_loading_font( + font_directory+'/Single.otf', 'Single') + + # Test that ttf fonts are preferred if the extension is + # not specified + self._test_fake_loading_font( + font_directory+'/Duplicate.ttf', 'Duplicate') + + @unittest.skipIf(sys.platform.startswith('win32'), + "requires Unix or MacOS") + def test_find_osx_font(self): + # Like the linux test, more cover hitting code rather than testing + # correctness. + font_directory = '/System/Library/Fonts' + with SimplePatcher(sys, 'platform', 'darwin'): + def fake_walker(path): + if path == font_directory: + return [(path, [], + ['Arial.ttf', 'Single.otf', + 'Duplicate.otf', 'Duplicate.ttf'], )] + return [(path, [], ['some_random_font.ttf'], )] + with SimplePatcher(os, 'walk', fake_walker): + self._test_fake_loading_font( + font_directory+'/Arial.ttf', 'Arial.ttf') + self._test_fake_loading_font( + font_directory+'/Arial.ttf', 'Arial') + self._test_fake_loading_font( + font_directory+'/Single.otf', 'Single') + self._test_fake_loading_font( + font_directory+'/Duplicate.ttf', 'Duplicate') + except ImportError: class TestImageFont(PillowTestCase): diff --git a/Tests/test_imagefont_bitmap.py b/Tests/test_imagefont_bitmap.py new file mode 100644 index 000000000..27141f4b3 --- /dev/null +++ b/Tests/test_imagefont_bitmap.py @@ -0,0 +1,25 @@ +from helper import unittest, PillowTestCase +from PIL import Image, ImageFont, ImageDraw + + +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) + 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) + + # 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]), + text, fill=(0, 0, 0), font=font_outline) + self.assert_image_similar(im_bitmap, im_outline, 20) + +if __name__ == '__main__': + unittest.main() diff --git a/Tests/test_imagegrab.py b/Tests/test_imagegrab.py index 13affe6b9..ea6b499b2 100644 --- a/Tests/test_imagegrab.py +++ b/Tests/test_imagegrab.py @@ -1,5 +1,7 @@ from helper import unittest, PillowTestCase +import sys + try: from PIL import ImageGrab @@ -19,6 +21,27 @@ except ImportError: self.skipTest("ImportError") +class TestImageGrabImport(PillowTestCase): + + def test_import(self): + # Arrange + exception = None + + # Act + try: + from PIL import ImageGrab + ImageGrab.__name__ # dummy to prevent Pyflakes warning + except Exception as e: + exception = e + + # Assert + if sys.platform == 'win32': + self.assertIsNone(exception, None) + else: + self.assertIsInstance(exception, ImportError) + self.assertEqual(str(exception), "ImageGrab is Windows only") + + if __name__ == '__main__': unittest.main() diff --git a/Tests/test_imagemath.py b/Tests/test_imagemath.py index 5d87c0229..4ca4e2c6c 100644 --- a/Tests/test_imagemath.py +++ b/Tests/test_imagemath.py @@ -1,3 +1,4 @@ +from __future__ import print_function from helper import unittest, PillowTestCase from PIL import Image diff --git a/Tests/test_imagemorph.py b/Tests/test_imagemorph.py index a52111431..bbb3ae190 100644 --- a/Tests/test_imagemorph.py +++ b/Tests/test_imagemorph.py @@ -1,5 +1,5 @@ # Test the ImageMorphology functionality -from helper import * +from helper import unittest, PillowTestCase from PIL import Image from PIL import ImageMorph @@ -63,7 +63,7 @@ class MorphTests(PillowTestCase): 'corner', 'dilation4', 'dilation8', 'erosion4', 'erosion8', 'edge'): lb = ImageMorph.LutBuilder(op_name=op) - lut = lb.build_lut(self) + lut = lb.build_lut() with open('Tests/images/%s.lut' % op, 'wb') as f: f.write(lut) diff --git a/Tests/test_imageops.py b/Tests/test_imageops.py index a4a94ca4d..396f0da6c 100644 --- a/Tests/test_imageops.py +++ b/Tests/test_imageops.py @@ -1,11 +1,11 @@ -from helper import unittest, PillowTestCase, lena +from helper import unittest, PillowTestCase, hopper from PIL import ImageOps class TestImageOps(PillowTestCase): - class Deformer: + class Deformer(object): def getmesh(self, im): x, y = im.size return [((0, 0, x, y), (0, 0, x, 0, x, y, y, 0))] @@ -14,65 +14,65 @@ class TestImageOps(PillowTestCase): def test_sanity(self): - ImageOps.autocontrast(lena("L")) - ImageOps.autocontrast(lena("RGB")) + ImageOps.autocontrast(hopper("L")) + ImageOps.autocontrast(hopper("RGB")) - ImageOps.autocontrast(lena("L"), cutoff=10) - ImageOps.autocontrast(lena("L"), ignore=[0, 255]) + ImageOps.autocontrast(hopper("L"), cutoff=10) + ImageOps.autocontrast(hopper("L"), ignore=[0, 255]) - ImageOps.colorize(lena("L"), (0, 0, 0), (255, 255, 255)) - ImageOps.colorize(lena("L"), "black", "white") + ImageOps.colorize(hopper("L"), (0, 0, 0), (255, 255, 255)) + ImageOps.colorize(hopper("L"), "black", "white") - ImageOps.crop(lena("L"), 1) - ImageOps.crop(lena("RGB"), 1) + ImageOps.crop(hopper("L"), 1) + ImageOps.crop(hopper("RGB"), 1) - ImageOps.deform(lena("L"), self.deformer) - ImageOps.deform(lena("RGB"), self.deformer) + ImageOps.deform(hopper("L"), self.deformer) + ImageOps.deform(hopper("RGB"), self.deformer) - ImageOps.equalize(lena("L")) - ImageOps.equalize(lena("RGB")) + ImageOps.equalize(hopper("L")) + ImageOps.equalize(hopper("RGB")) - ImageOps.expand(lena("L"), 1) - ImageOps.expand(lena("RGB"), 1) - ImageOps.expand(lena("L"), 2, "blue") - ImageOps.expand(lena("RGB"), 2, "blue") + ImageOps.expand(hopper("L"), 1) + ImageOps.expand(hopper("RGB"), 1) + ImageOps.expand(hopper("L"), 2, "blue") + ImageOps.expand(hopper("RGB"), 2, "blue") - ImageOps.fit(lena("L"), (128, 128)) - ImageOps.fit(lena("RGB"), (128, 128)) + ImageOps.fit(hopper("L"), (128, 128)) + ImageOps.fit(hopper("RGB"), (128, 128)) - ImageOps.flip(lena("L")) - ImageOps.flip(lena("RGB")) + ImageOps.flip(hopper("L")) + ImageOps.flip(hopper("RGB")) - ImageOps.grayscale(lena("L")) - ImageOps.grayscale(lena("RGB")) + ImageOps.grayscale(hopper("L")) + ImageOps.grayscale(hopper("RGB")) - ImageOps.invert(lena("L")) - ImageOps.invert(lena("RGB")) + ImageOps.invert(hopper("L")) + ImageOps.invert(hopper("RGB")) - ImageOps.mirror(lena("L")) - ImageOps.mirror(lena("RGB")) + ImageOps.mirror(hopper("L")) + ImageOps.mirror(hopper("RGB")) - ImageOps.posterize(lena("L"), 4) - ImageOps.posterize(lena("RGB"), 4) + ImageOps.posterize(hopper("L"), 4) + ImageOps.posterize(hopper("RGB"), 4) - ImageOps.solarize(lena("L")) - ImageOps.solarize(lena("RGB")) + ImageOps.solarize(hopper("L")) + ImageOps.solarize(hopper("RGB")) def test_1pxfit(self): # Division by zero in equalize if image is 1 pixel high - newimg = ImageOps.fit(lena("RGB").resize((1, 1)), (35, 35)) + newimg = ImageOps.fit(hopper("RGB").resize((1, 1)), (35, 35)) self.assertEqual(newimg.size, (35, 35)) - newimg = ImageOps.fit(lena("RGB").resize((1, 100)), (35, 35)) + newimg = ImageOps.fit(hopper("RGB").resize((1, 100)), (35, 35)) self.assertEqual(newimg.size, (35, 35)) - newimg = ImageOps.fit(lena("RGB").resize((100, 1)), (35, 35)) + newimg = ImageOps.fit(hopper("RGB").resize((100, 1)), (35, 35)) self.assertEqual(newimg.size, (35, 35)) def test_pil163(self): # Division by zero in equalize if < 255 pixels in image (@PIL163) - i = lena("RGB").resize((15, 16)) + i = hopper("RGB").resize((15, 16)) ImageOps.equalize(i.convert("L")) ImageOps.equalize(i.convert("P")) diff --git a/Tests/test_imageops_usm.py b/Tests/test_imageops_usm.py index be7a669da..f6eae640b 100644 --- a/Tests/test_imageops_usm.py +++ b/Tests/test_imageops_usm.py @@ -4,7 +4,8 @@ from PIL import Image from PIL import ImageOps from PIL import ImageFilter -im = Image.open("Tests/images/lena.ppm") +im = Image.open("Tests/images/hopper.ppm") +snakes = Image.open("Tests/images/color_snakes.png") class TestImageOpsUsm(PillowTestCase): @@ -16,7 +17,7 @@ class TestImageOpsUsm(PillowTestCase): self.assertEqual(i.size, (128, 128)) # i.save("blur.bmp") - i = ImageOps.usm(im, 2.0, 125, 8) + i = ImageOps.unsharp_mask(im, 2.0, 125, 8) self.assertEqual(i.mode, "RGB") self.assertEqual(i.size, (128, 128)) # i.save("usm.bmp") @@ -33,7 +34,7 @@ class TestImageOpsUsm(PillowTestCase): self.assertEqual(i.mode, "RGB") self.assertEqual(i.size, (128, 128)) - def test_usm(self): + def test_usm_formats(self): usm = ImageOps.unsharp_mask self.assertRaises(ValueError, lambda: usm(im.convert("1"))) @@ -45,7 +46,7 @@ class TestImageOpsUsm(PillowTestCase): usm(im.convert("CMYK")) self.assertRaises(ValueError, lambda: usm(im.convert("YCbCr"))) - def test_blur(self): + def test_blur_formats(self): blur = ImageOps.gaussian_blur self.assertRaises(ValueError, lambda: blur(im.convert("1"))) @@ -57,6 +58,32 @@ class TestImageOpsUsm(PillowTestCase): blur(im.convert("CMYK")) self.assertRaises(ValueError, lambda: blur(im.convert("YCbCr"))) + def test_usm_accuracy(self): + + src = snakes.convert('RGB') + i = src._new(ImageOps.unsharp_mask(src, 5, 1024, 0)) + # Image should not be changed because it have only 0 and 255 levels. + self.assertEqual(i.tobytes(), src.tobytes()) + + def test_blur_accuracy(self): + + i = snakes._new(ImageOps.gaussian_blur(snakes, .4)) + # These pixels surrounded with pixels with 255 intensity. + # They must be very close to 255. + for x, y, c in [(1, 0, 1), (2, 0, 1), (7, 8, 1), (8, 8, 1), (2, 9, 1), + (7, 3, 0), (8, 3, 0), (5, 8, 0), (5, 9, 0), (1, 3, 0), + (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)) + self.assertTrue(236 <= gp(7, 4)[0] <= 239) + self.assertTrue(236 <= gp(7, 5)[2] <= 239) + self.assertTrue(236 <= gp(7, 6)[2] <= 239) + self.assertTrue(236 <= gp(7, 7)[1] <= 239) + self.assertTrue(236 <= gp(8, 4)[0] <= 239) + self.assertTrue(236 <= gp(8, 5)[2] <= 239) + self.assertTrue(236 <= gp(8, 6)[2] <= 239) + self.assertTrue(236 <= gp(8, 7)[1] <= 239) if __name__ == '__main__': unittest.main() diff --git a/Tests/test_imagepalette.py b/Tests/test_imagepalette.py index e56c61390..707ab4080 100644 --- a/Tests/test_imagepalette.py +++ b/Tests/test_imagepalette.py @@ -17,11 +17,11 @@ class TestImagePalette(PillowTestCase): palette = ImagePalette() - map = {} + test_map = {} for i in range(256): - map[palette.getcolor((i, i, i))] = i + test_map[palette.getcolor((i, i, i))] = i - self.assertEqual(len(map), 256) + self.assertEqual(len(test_map), 256) self.assertRaises(ValueError, lambda: palette.getcolor((1, 2, 3))) def test_file(self): @@ -117,7 +117,7 @@ class TestImagePalette(PillowTestCase): palette = raw("RGB", list(range(256))*3) # Act / Assert - self.assertRaises(ValueError, lambda: palette.tobytes()) + self.assertRaises(ValueError, palette.tobytes) self.assertRaises(ValueError, lambda: palette.getcolor((1, 2, 3))) f = self.tempfile("temp.lut") self.assertRaises(ValueError, lambda: palette.save(f)) diff --git a/Tests/test_imageqt.py b/Tests/test_imageqt.py index fd50bf320..7d57ed1d2 100644 --- a/Tests/test_imageqt.py +++ b/Tests/test_imageqt.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase, lena +from helper import unittest, PillowTestCase, hopper try: from PIL import ImageQt @@ -7,8 +7,11 @@ except: try: from PyQt4.QtGui import QImage, qRgb, qRgba except: - # Will be skipped in setUp - pass + try: + from PySide.QtGui import QImage, qRgb, qRgba + except: + # Will be skipped in setUp + pass class TestImageQt(PillowTestCase): @@ -20,7 +23,10 @@ class TestImageQt(PillowTestCase): try: from PyQt4.QtGui import QImage, qRgb, qRgba except: - self.skipTest('PyQt4 or 5 not installed') + try: + from PySide.QtGui import QImage, qRgb, qRgba + except: + self.skipTest('PyQt4 or 5 or PySide not installed') def test_rgb(self): # from https://qt-project.org/doc/qt-4.8/qcolor.html @@ -44,7 +50,7 @@ class TestImageQt(PillowTestCase): def test_image(self): for mode in ('1', 'RGB', 'RGBA', 'L', 'P'): - ImageQt.ImageQt(lena(mode)) + ImageQt.ImageQt(hopper(mode)) if __name__ == '__main__': diff --git a/Tests/test_imagesequence.py b/Tests/test_imagesequence.py index fd10e5989..1b4bb3c02 100644 --- a/Tests/test_imagesequence.py +++ b/Tests/test_imagesequence.py @@ -1,16 +1,16 @@ -from helper import unittest, PillowTestCase, lena +from helper import unittest, PillowTestCase, hopper -from PIL import ImageSequence +from PIL import Image, ImageSequence, TiffImagePlugin class TestImageSequence(PillowTestCase): def test_sanity(self): - file = self.tempfile("temp.im") + test_file = self.tempfile("temp.im") - im = lena("RGB") - im.save(file) + im = hopper("RGB") + im.save(test_file) seq = ImageSequence.Iterator(im) @@ -22,6 +22,30 @@ 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 + 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) + + def test_libtiff(self): + codecs = dir(Image.core) + + if "libtiff_encoder" not in codecs or "libtiff_decoder" not in codecs: + self.skipTest("tiff support not available") + + TiffImagePlugin.READ_LIBTIFF = True + # self._test_multipage_tiff(True) + self._test_multipage_tiff(False) + TiffImagePlugin.READ_LIBTIFF = False if __name__ == '__main__': unittest.main() diff --git a/Tests/test_imageshow.py b/Tests/test_imageshow.py index e94ae2d0a..236d6e224 100644 --- a/Tests/test_imageshow.py +++ b/Tests/test_imageshow.py @@ -9,7 +9,6 @@ class TestImageShow(PillowTestCase): def test_sanity(self): dir(Image) dir(ImageShow) - pass if __name__ == '__main__': diff --git a/Tests/test_imagestat.py b/Tests/test_imagestat.py index 4d30ff023..4d5e42b3f 100644 --- a/Tests/test_imagestat.py +++ b/Tests/test_imagestat.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase, lena +from helper import unittest, PillowTestCase, hopper from PIL import Image from PIL import ImageStat @@ -8,7 +8,7 @@ class TestImageStat(PillowTestCase): def test_sanity(self): - im = lena() + im = hopper() st = ImageStat.Stat(im) st = ImageStat.Stat(im.histogram()) @@ -28,18 +28,18 @@ class TestImageStat(PillowTestCase): self.assertRaises(TypeError, lambda: ImageStat.Stat(1)) - def test_lena(self): + def test_hopper(self): - im = lena() + im = hopper() st = ImageStat.Stat(im) # verify a few values - self.assertEqual(st.extrema[0], (61, 255)) - self.assertEqual(st.median[0], 197) - self.assertEqual(st.sum[0], 2954416) - self.assertEqual(st.sum[1], 2027250) - self.assertEqual(st.sum[2], 1727331) + self.assertEqual(st.extrema[0], (0, 255)) + self.assertEqual(st.median[0], 72) + self.assertEqual(st.sum[0], 1470218) + self.assertEqual(st.sum[1], 1311896) + self.assertEqual(st.sum[2], 1563008) def test_constant(self): diff --git a/Tests/test_imagewin.py b/Tests/test_imagewin.py index 69dbdbe82..7ceea86ee 100644 --- a/Tests/test_imagewin.py +++ b/Tests/test_imagewin.py @@ -1,15 +1,121 @@ -from helper import unittest, PillowTestCase, lena +from helper import unittest, PillowTestCase, hopper -from PIL import Image from PIL import ImageWin +import sys class TestImageWin(PillowTestCase): def test_sanity(self): - dir(Image) dir(ImageWin) - pass + + def test_hdc(self): + # Arrange + dc = 50 + + # Act + hdc = ImageWin.HDC(dc) + dc2 = int(hdc) + + # Assert + self.assertEqual(dc2, 50) + + def test_hwnd(self): + # Arrange + wnd = 50 + + # Act + hwnd = ImageWin.HWND(wnd) + wnd2 = int(hwnd) + + # Assert + self.assertEqual(wnd2, 50) + + +@unittest.skipUnless(sys.platform.startswith('win32'), "Windows only") +class TestImageWinDib(PillowTestCase): + + def test_dib_image(self): + # Arrange + im = hopper() + + # Act + dib = ImageWin.Dib(im) + + # Assert + self.assertEqual(dib.size, im.size) + + def test_dib_mode_string(self): + # Arrange + mode = "RGBA" + size = (128, 128) + + # Act + dib = ImageWin.Dib(mode, size) + + # Assert + self.assertEqual(dib.size, (128, 128)) + + def test_dib_paste(self): + # Arrange + im = hopper() + + mode = "RGBA" + size = (128, 128) + dib = ImageWin.Dib(mode, size) + + # Act + dib.paste(im) + + # Assert + self.assertEqual(dib.size, (128, 128)) + + def test_dib_paste_bbox(self): + # Arrange + im = hopper() + bbox = (0, 0, 10, 10) + + mode = "RGBA" + size = (128, 128) + dib = ImageWin.Dib(mode, size) + + # Act + dib.paste(im, bbox) + + # Assert + self.assertEqual(dib.size, (128, 128)) + + def test_dib_frombytes_tobytes_roundtrip(self): + # Arrange + # Make two different DIB images + im = hopper() + dib1 = ImageWin.Dib(im) + + mode = "RGB" + size = (128, 128) + dib2 = ImageWin.Dib(mode, size) + + # Confirm they're different + self.assertNotEqual(dib1.tobytes(), dib2.tobytes()) + + # Act + # Make one the same as the using tobytes()/frombytes() + test_buffer = dib1.tobytes() + dib2.frombytes(test_buffer) + + # Assert + # Confirm they're the same + self.assertEqual(dib1.tobytes(), dib2.tobytes()) + + def test_dib_fromstring_tostring_deprecated(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)) if __name__ == '__main__': diff --git a/Tests/test_lib_pack.py b/Tests/test_lib_pack.py index 102835b58..4d40b4100 100644 --- a/Tests/test_lib_pack.py +++ b/Tests/test_lib_pack.py @@ -48,6 +48,10 @@ class TestLibPack(PillowTestCase): self.assertEqual(pack("RGBA", "RGBA"), [1, 2, 3, 4]) + self.assertEqual(pack("RGBa", "RGBa"), [1, 2, 3, 4]) + self.assertEqual(pack("RGBa", "BGRa"), [3, 2, 1, 4]) + self.assertEqual(pack("RGBa", "aBGR"), [4, 3, 2, 1]) + self.assertEqual(pack("CMYK", "CMYK"), [1, 2, 3, 4]) self.assertEqual(pack("YCbCr", "YCbCr"), [1, 2, 3]) @@ -125,6 +129,11 @@ class TestLibPack(PillowTestCase): self.assertEqual(unpack("RGBA", "BGRA;15", 2), (0, 131, 8, 0)) self.assertEqual(unpack("RGBA", "RGBA;4B", 2), (17, 0, 34, 0)) + self.assertEqual(unpack("RGBa", "RGBa", 4), (1, 2, 3, 4)) + self.assertEqual(unpack("RGBa", "BGRa", 4), (3, 2, 1, 4)) + self.assertEqual(unpack("RGBa", "aRGB", 4), (2, 3, 4, 1)) + self.assertEqual(unpack("RGBa", "aBGR", 4), (4, 3, 2, 1)) + self.assertEqual(unpack("RGBX", "RGBX", 4), (1, 2, 3, 4)) # 4->255? self.assertEqual(unpack("RGBX", "BGRX", 4), (3, 2, 1, 255)) self.assertEqual(unpack("RGBX", "XRGB", 4), (2, 3, 4, 255)) diff --git a/Tests/test_locale.py b/Tests/test_locale.py index 9ef136bf9..80e236b8d 100644 --- a/Tests/test_locale.py +++ b/Tests/test_locale.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase, lena +from helper import unittest, PillowTestCase from PIL import Image @@ -19,7 +19,7 @@ import locale # one of string.whitespace is not freely convertable into ascii. -path = "Tests/images/lena.jpg" +path = "Tests/images/hopper.jpg" class TestLocale(PillowTestCase): diff --git a/Tests/test_mode_i16.py b/Tests/test_mode_i16.py index 0cd5dba0f..0827e218b 100644 --- a/Tests/test_mode_i16.py +++ b/Tests/test_mode_i16.py @@ -1,11 +1,11 @@ -from helper import unittest, PillowTestCase, lena +from helper import unittest, PillowTestCase, hopper from PIL import Image class TestModeI16(PillowTestCase): - original = lena().resize((32,32)).convert('I') + original = hopper().resize((32, 32)).convert('I') def verify(self, im1): im2 = self.original.copy() @@ -64,15 +64,15 @@ class TestModeI16(PillowTestCase): self.assertEqual(imIn.getpixel((0, 0)), 2) if mode == "L": - max = 255 + maximum = 255 else: - max = 32767 + maximum = 32767 imIn = Image.new(mode, (1, 1), 256) - self.assertEqual(imIn.getpixel((0, 0)), min(256, max)) + self.assertEqual(imIn.getpixel((0, 0)), min(256, maximum)) imIn.putpixel((0, 0), 512) - self.assertEqual(imIn.getpixel((0, 0)), min(512, max)) + self.assertEqual(imIn.getpixel((0, 0)), min(512, maximum)) basic("L") diff --git a/Tests/test_numpy.py b/Tests/test_numpy.py index 07c3e0c21..19b9a2014 100644 --- a/Tests/test_numpy.py +++ b/Tests/test_numpy.py @@ -1,4 +1,5 @@ -from helper import unittest, PillowTestCase, lena +from __future__ import print_function +from helper import unittest, PillowTestCase, hopper from PIL import Image @@ -90,7 +91,7 @@ class TestNumpy(PillowTestCase): def test_to_array(self): def _to_array(mode, dtype): - img = lena(mode) + img = hopper(mode) np_img = numpy.array(img) self._test_img_equals_nparray(img, np_img) self.assertEqual(np_img.dtype, numpy.dtype(dtype)) @@ -117,10 +118,20 @@ class TestNumpy(PillowTestCase): data = list(range(256))*3 lut = numpy.array(data, dtype='uint8') - im = lena() + im = hopper() im.point(lut) + def test_putdata(self): + # shouldn't segfault + # see https://github.com/python-pillow/Pillow/issues/1008 + + im = Image.new('F', (150, 100)) + arr = numpy.zeros((15000,), numpy.float32) + im.putdata(arr) + + self.assertEqual(len(im.getdata()), len(arr)) + if __name__ == '__main__': unittest.main() diff --git a/Tests/test_pickle.py b/Tests/test_pickle.py index eae5eb671..52d64acee 100644 --- a/Tests/test_pickle.py +++ b/Tests/test_pickle.py @@ -5,9 +5,12 @@ from PIL import Image class TestPickle(PillowTestCase): - def helper_pickle_file(self, pickle, protocol=0): - im = Image.open('Tests/images/lena.jpg') + def helper_pickle_file(self, pickle, protocol=0, mode=None): + # Arrange + im = Image.open('Tests/images/hopper.jpg') filename = self.tempfile('temp.pkl') + if mode: + im = im.convert(mode) # Act with open(filename, 'wb') as f: @@ -18,9 +21,11 @@ class TestPickle(PillowTestCase): # Assert self.assertEqual(im, loaded_im) - def helper_pickle_string( - self, pickle, protocol=0, file='Tests/images/lena.jpg'): + def helper_pickle_string(self, pickle, protocol=0, + file='Tests/images/hopper.jpg', mode=None): im = Image.open(file) + if mode: + im = im.convert(mode) # Act dumped_string = pickle.dumps(im, protocol) @@ -55,7 +60,7 @@ class TestPickle(PillowTestCase): import pickle # Act / Assert - for file in [ + for test_file in [ "Tests/images/test-card.png", "Tests/images/zero_bb.png", "Tests/images/zero_bb_scale2.png", @@ -64,8 +69,28 @@ class TestPickle(PillowTestCase): "Tests/images/p_trns_single.png", "Tests/images/pil123p.png" ]: - self.helper_pickle_string(pickle, file=file) + self.helper_pickle_string(pickle, file=test_file) + def test_pickle_l_mode(self): + # Arrange + import pickle + + # Act / Assert + for protocol in range(0, pickle.HIGHEST_PROTOCOL + 1): + self.helper_pickle_string(pickle, protocol, mode="L") + self.helper_pickle_file(pickle, protocol, mode="L") + + def test_cpickle_l_mode(self): + # Arrange + try: + import cPickle + except ImportError: + return + + # Act / Assert + for protocol in range(0, cPickle.HIGHEST_PROTOCOL + 1): + self.helper_pickle_string(cPickle, protocol, mode="L") + self.helper_pickle_file(cPickle, protocol, mode="L") if __name__ == '__main__': unittest.main() diff --git a/Tests/test_psdraw.py b/Tests/test_psdraw.py new file mode 100644 index 000000000..9606a4392 --- /dev/null +++ b/Tests/test_psdraw.py @@ -0,0 +1,50 @@ +from helper import unittest, PillowTestCase + + +class TestPsDraw(PillowTestCase): + + def test_draw_postscript(self): + + # Based on Pillow tutorial, but there is no textsize: + # http://pillow.readthedocs.org/en/latest/handbook/tutorial.html + + # Arrange + from PIL import Image + from PIL import PSDraw + tempfile = self.tempfile('temp.ps') + fp = open(tempfile, "wb") + + im = Image.open("Tests/images/hopper.ppm") + title = "hopper" + box = (1*72, 2*72, 7*72, 10*72) # in points + + # Act + ps = PSDraw.PSDraw(fp) + ps.begin_document(title) + + # draw diagonal lines in a cross + ps.line((1*72, 2*72), (7*72, 10*72)) + ps.line((7*72, 2*72), (1*72, 10*72)) + + # draw the image (75 dpi) + ps.image(box, im, 75) + ps.rectangle(box) + + # draw title + ps.setfont("Courier", 36) + ps.text((3*72, 4*72), title) + + ps.end_document() + fp.close() + + # Assert + # Check non-zero file was created + import os + self.assertTrue(os.path.isfile(tempfile)) + self.assertGreater(os.path.getsize(tempfile), 0) + + +if __name__ == '__main__': + unittest.main() + +# End of file diff --git a/Tests/test_pyroma.py b/Tests/test_pyroma.py index 295ef1057..0875cc972 100644 --- a/Tests/test_pyroma.py +++ b/Tests/test_pyroma.py @@ -1,4 +1,6 @@ -from helper import * +from helper import unittest, PillowTestCase + +from PIL import PILLOW_VERSION try: import pyroma @@ -12,6 +14,7 @@ class TestPyroma(PillowTestCase): def setUp(self): try: import pyroma + assert pyroma # Ignore warning except ImportError: self.skipTest("ImportError") @@ -23,8 +26,15 @@ class TestPyroma(PillowTestCase): rating = pyroma.ratings.rate(data) # Assert - # Should have a perfect score - self.assertEqual(rating, (10, [])) + if 'rc' in PILLOW_VERSION: + # Pyroma needs to chill about RC versions + # and not kill all our tests. + self.assertEqual(rating, (9, [ + 'The packages version number does not comply with PEP-386.'])) + + else: + # Should have a perfect score + self.assertEqual(rating, (10, [])) if __name__ == '__main__': diff --git a/Tests/test_scipy.py b/Tests/test_scipy.py new file mode 100644 index 000000000..60f1a1b1e --- /dev/null +++ b/Tests/test_scipy.py @@ -0,0 +1,43 @@ +from helper import PillowTestCase + +try: + import numpy as np + from numpy.testing import assert_equal + + from scipy import misc + HAS_SCIPY = True +except: + HAS_SCIPY = False + + +class Test_scipy_resize(PillowTestCase): + """ Tests for scipy regression in 2.6.0 + + Tests from https://github.com/scipy/scipy/blob/master/scipy/misc/pilutil.py + """ + + def setUp(self): + if not HAS_SCIPY: + self.skipTest("Scipy Required") + + def test_imresize(self): + im = np.random.random((10, 20)) + for T in np.sctypes['float'] + [float]: + # 1.1 rounds to below 1.1 for float16, 1.101 works + im1 = misc.imresize(im, T(1.101)) + self.assertEqual(im1.shape, (11, 22)) + + def test_imresize4(self): + im = np.array([[1, 2], + [3, 4]]) + res = np.array([[1. , 1.25, 1.75, 2. ], + [1.5, 1.75, 2.25, 2.5], + [2.5, 2.75, 3.25, 3.5], + [3. , 3.25, 3.75, 4. ]], dtype=np.float32) + # Check that resizing by target size, float and int are the same + im2 = misc.imresize(im, (4, 4), mode='F') # output size + im3 = misc.imresize(im, 2., mode='F') # fraction + im4 = misc.imresize(im, 200, mode='F') # percentage + assert_equal(im2, res) + assert_equal(im3, res) + assert_equal(im4, res) diff --git a/Tests/test_shell_injection.py b/Tests/test_shell_injection.py index ef80bfc98..3430855ed 100644 --- a/Tests/test_shell_injection.py +++ b/Tests/test_shell_injection.py @@ -6,8 +6,8 @@ import shutil from PIL import Image, JpegImagePlugin, GifImagePlugin -test_jpg = "Tests/images/lena.jpg" -test_gif = "Tests/images/lena.gif" +TEST_JPG = "Tests/images/hopper.jpg" +TEST_GIF = "Tests/images/hopper.gif" test_filenames = ( "temp_';", @@ -17,6 +17,7 @@ test_filenames = ( "temp_'\"&&", ) + @unittest.skipIf(sys.platform.startswith('win32'), "requires Unix or MacOS") class TestShellInjection(PillowTestCase): @@ -31,24 +32,24 @@ class TestShellInjection(PillowTestCase): def test_load_djpeg_filename(self): for filename in test_filenames: src_file = self.tempfile(filename) - shutil.copy(test_jpg, src_file) + shutil.copy(TEST_JPG, src_file) im = Image.open(src_file) im.load_djpeg() @unittest.skipUnless(cjpeg_available(), "cjpeg not available") def test_save_cjpeg_filename(self): - im = Image.open(test_jpg) + im = Image.open(TEST_JPG) self.assert_save_filename_check(im, JpegImagePlugin._save_cjpeg) @unittest.skipUnless(netpbm_available(), "netpbm not available") def test_save_netpbm_filename_bmp_mode(self): - im = Image.open(test_gif).convert("RGB") + im = Image.open(TEST_GIF).convert("RGB") self.assert_save_filename_check(im, GifImagePlugin._save_netpbm) @unittest.skipUnless(netpbm_available(), "netpbm not available") def test_save_netpbm_filename_l_mode(self): - im = Image.open(test_gif).convert("L") + im = Image.open(TEST_GIF).convert("L") self.assert_save_filename_check(im, GifImagePlugin._save_netpbm) diff --git a/Tests/threaded_save.py b/Tests/threaded_save.py index 12fcff2cf..ba8b17dbc 100644 --- a/Tests/threaded_save.py +++ b/Tests/threaded_save.py @@ -1,21 +1,22 @@ +from __future__ import print_function from PIL import Image -import sys, time import io -import threading, queue +import queue +import sys +import threading +import time -try: - format = sys.argv[1] -except: - format = "PNG" +test_format = sys.argv[1] if len(sys.argv) > 1 else "PNG" -im = Image.open("Tests/images/lena.ppm") +im = Image.open("Tests/images/hopper.ppm") im.load() queue = queue.Queue() result = [] + class Worker(threading.Thread): def run(self): while True: @@ -25,7 +26,7 @@ class Worker(threading.Thread): sys.stdout.write("x") break f = io.BytesIO() - im.save(f, format, optimize=1) + im.save(f, test_format, optimize=1) data = f.getvalue() result.append(len(data)) im = Image.open(io.BytesIO(data)) diff --git a/Tests/versions.py b/Tests/versions.py index a4e4a0bc2..89be1d7c8 100644 --- a/Tests/versions.py +++ b/Tests/versions.py @@ -1,5 +1,7 @@ +from __future__ import print_function from PIL import Image + def version(module, version): v = getattr(module.core, version + "_version", None) if v: diff --git a/_imaging.c b/_imaging.c index ec8205dd4..09345c0dd 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.5.3" +#define PILLOW_VERSION "2.9.0.dev0" #include "Python.h" @@ -421,6 +421,7 @@ getlist(PyObject* arg, int* length, const char* wrong_length, int type) *length = n; PyErr_Clear(); + Py_DECREF(seq); return list; } @@ -863,7 +864,8 @@ _gaussian_blur(ImagingObject* self, PyObject* args) Imaging imOut; float radius = 0; - if (!PyArg_ParseTuple(args, "f", &radius)) + int passes = 3; + if (!PyArg_ParseTuple(args, "f|i", &radius, &passes)) return NULL; imIn = self->image; @@ -871,7 +873,7 @@ _gaussian_blur(ImagingObject* self, PyObject* args) if (!imOut) return NULL; - if (!ImagingGaussianBlur(imIn, imOut, radius)) + if (!ImagingGaussianBlur(imOut, imIn, radius, passes)) return NULL; return PyImagingNew(imOut); @@ -1220,7 +1222,7 @@ _putdata(ImagingObject* self, PyObject* args) Py_ssize_t n, i, x, y; PyObject* data; - PyObject* seq; + PyObject* seq = NULL; PyObject* op; double scale = 1.0; double offset = 0.0; @@ -1269,7 +1271,7 @@ _putdata(ImagingObject* self, PyObject* args) if (scale == 1.0 && offset == 0.0) { /* Clipped data */ for (i = x = y = 0; i < n; i++) { - op = PySequence_Fast_GET_ITEM(data, i); + op = PySequence_Fast_GET_ITEM(seq, i); image->image8[y][x] = (UINT8) CLIP(PyInt_AsLong(op)); if (++x >= (int) image->xsize){ x = 0, y++; @@ -1279,7 +1281,7 @@ _putdata(ImagingObject* self, PyObject* args) } else { /* Scaled and clipped data */ for (i = x = y = 0; i < n; i++) { - PyObject *op = PySequence_Fast_GET_ITEM(data, i); + PyObject *op = PySequence_Fast_GET_ITEM(seq, i); image->image8[y][x] = CLIP( (int) (PyFloat_AsDouble(op) * scale + offset)); if (++x >= (int) image->xsize){ @@ -1299,7 +1301,7 @@ _putdata(ImagingObject* self, PyObject* args) switch (image->type) { case IMAGING_TYPE_INT32: for (i = x = y = 0; i < n; i++) { - op = PySequence_Fast_GET_ITEM(data, i); + op = PySequence_Fast_GET_ITEM(seq, i); IMAGING_PIXEL_INT32(image, x, y) = (INT32) (PyFloat_AsDouble(op) * scale + offset); if (++x >= (int) image->xsize){ @@ -1310,7 +1312,7 @@ _putdata(ImagingObject* self, PyObject* args) break; case IMAGING_TYPE_FLOAT32: for (i = x = y = 0; i < n; i++) { - op = PySequence_Fast_GET_ITEM(data, i); + op = PySequence_Fast_GET_ITEM(seq, i); IMAGING_PIXEL_FLOAT32(image, x, y) = (FLOAT32) (PyFloat_AsDouble(op) * scale + offset); if (++x >= (int) image->xsize){ @@ -1326,8 +1328,9 @@ _putdata(ImagingObject* self, PyObject* args) INT32 inkint; } u; - op = PySequence_Fast_GET_ITEM(data, i); + op = PySequence_Fast_GET_ITEM(seq, i); if (!op || !getink(op, image, u.ink)) { + Py_DECREF(seq); return NULL; } /* FIXME: what about scale and offset? */ @@ -1341,6 +1344,8 @@ _putdata(ImagingObject* self, PyObject* args) } } + Py_XDECREF(seq); + Py_INCREF(Py_None); return Py_None; } @@ -1513,9 +1518,26 @@ _resize(ImagingObject* self, PyObject* args) imIn = self->image; - imOut = ImagingNew(imIn->mode, xsize, ysize); - if (imOut) - (void) ImagingResize(imOut, imIn, filter); + if (imIn->xsize == xsize && imIn->ysize == ysize) { + imOut = ImagingCopy(imIn); + } + else if ( ! filter) { + double a[6]; + + memset(a, 0, sizeof a); + a[1] = (double) imIn->xsize / xsize; + a[5] = (double) imIn->ysize / ysize; + + imOut = ImagingNew(imIn->mode, xsize, ysize); + + imOut = ImagingTransformAffine( + imOut, imIn, + 0, 0, xsize, ysize, + a, filter, 1); + } + else { + imOut = ImagingResample(imIn, xsize, ysize, filter); + } return PyImagingNew(imOut); } @@ -1609,51 +1631,6 @@ im_setmode(ImagingObject* self, PyObject* args) return Py_None; } -static PyObject* -_stretch(ImagingObject* self, PyObject* args) -{ - Imaging imIn; - Imaging imTemp; - Imaging imOut; - - int xsize, ysize; - int filter = IMAGING_TRANSFORM_NEAREST; - if (!PyArg_ParseTuple(args, "(ii)|i", &xsize, &ysize, &filter)) - return NULL; - - imIn = self->image; - - /* two-pass resize: minimize size of intermediate image */ - if ((Py_ssize_t) imIn->xsize * ysize < (Py_ssize_t) xsize * imIn->ysize) - imTemp = ImagingNew(imIn->mode, imIn->xsize, ysize); - else - imTemp = ImagingNew(imIn->mode, xsize, imIn->ysize); - if (!imTemp) - return NULL; - - /* first pass */ - if (!ImagingStretch(imTemp, imIn, filter)) { - ImagingDelete(imTemp); - return NULL; - } - - imOut = ImagingNew(imIn->mode, xsize, ysize); - if (!imOut) { - ImagingDelete(imTemp); - return NULL; - } - - /* second pass */ - if (!ImagingStretch(imOut, imTemp, filter)) { - ImagingDelete(imOut); - ImagingDelete(imTemp); - return NULL; - } - - ImagingDelete(imTemp); - - return PyImagingNew(imOut); -} static PyObject* _transform2(ImagingObject* self, PyObject* args) @@ -1750,6 +1727,7 @@ _transpose(ImagingObject* self, PyObject* args) break; case 2: /* rotate 90 */ case 4: /* rotate 270 */ + case 5: /* transpose */ imOut = ImagingNew(imIn->mode, imIn->ysize, imIn->xsize); break; default: @@ -1774,6 +1752,9 @@ _transpose(ImagingObject* self, PyObject* args) case 4: (void) ImagingRotate270(imOut, imIn); break; + case 5: + (void) ImagingTranspose(imOut, imIn); + break; } return PyImagingNew(imOut); @@ -1791,18 +1772,39 @@ _unsharp_mask(ImagingObject* self, PyObject* args) if (!PyArg_ParseTuple(args, "fii", &radius, &percent, &threshold)) return NULL; + imIn = self->image; + imOut = ImagingNew(imIn->mode, imIn->xsize, imIn->ysize); + if (!imOut) + return NULL; + + if (!ImagingUnsharpMask(imOut, imIn, radius, percent, threshold)) + return NULL; + + return PyImagingNew(imOut); +} +#endif + +static PyObject* +_box_blur(ImagingObject* self, PyObject* args) +{ + Imaging imIn; + Imaging imOut; + + float radius; + int n = 1; + if (!PyArg_ParseTuple(args, "f|i", &radius, &n)) + return NULL; imIn = self->image; imOut = ImagingNew(imIn->mode, imIn->xsize, imIn->ysize); if (!imOut) return NULL; - if (!ImagingUnsharpMask(imIn, imOut, radius, percent, threshold)) + if (!ImagingBoxBlur(imOut, imIn, radius, n)) return NULL; return PyImagingNew(imOut); } -#endif /* -------------------------------------------------------------------- */ @@ -3031,8 +3033,10 @@ static struct PyMethodDef methods[] = { {"rankfilter", (PyCFunction)_rankfilter, 1}, #endif {"resize", (PyCFunction)_resize, 1}, + // There were two methods for image resize before. + // Starting from Pillow 2.7.0 stretch is depreciated. + {"stretch", (PyCFunction)_resize, 1}, {"rotate", (PyCFunction)_rotate, 1}, - {"stretch", (PyCFunction)_stretch, 1}, {"transpose", (PyCFunction)_transpose, 1}, {"transform2", (PyCFunction)_transform2, 1}, @@ -3078,6 +3082,8 @@ static struct PyMethodDef methods[] = { {"unsharp_mask", (PyCFunction)_unsharp_mask, 1}, #endif + {"box_blur", (PyCFunction)_box_blur, 1}, + #ifdef WITH_EFFECTS /* Special effects */ {"effect_spread", (PyCFunction)_effect_spread, 1}, diff --git a/_imagingcms.c b/_imagingcms.c index 3b822006a..cda7c5f1f 100644 --- a/_imagingcms.c +++ b/_imagingcms.c @@ -278,7 +278,7 @@ findLCMStype(char* PILmode) return TYPE_YCbCr_8; } else if (strcmp(PILmode, "LAB") == 0) { - // LabX equvalent like ALab, but not reversed -- no #define in lcms2 + // LabX equivalent like ALab, but not reversed -- no #define in lcms2 return (COLORSPACE_SH(PT_LabV2)|CHANNELS_SH(3)|BYTES_SH(1)|EXTRA_SH(1)); } diff --git a/_imagingft.c b/_imagingft.c index eb6313704..d8f6d6338 100644 --- a/_imagingft.c +++ b/_imagingft.c @@ -243,7 +243,11 @@ font_getsize(FontObject* self, PyObject* args) &delta); x += delta.x; } - error = FT_Load_Glyph(face, index, FT_LOAD_DEFAULT); + + /* 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) return geterror(error); if (i == 0) @@ -316,7 +320,8 @@ font_getabc(FontObject* self, PyObject* args) int index, error; face = self->face; index = FT_Get_Char_Index(face, ch); - error = FT_Load_Glyph(face, index, FT_LOAD_DEFAULT); + /* Note: bitmap fonts within ttf fonts do not work, see #891/pr#960 */ + error = FT_Load_Glyph(face, index, FT_LOAD_DEFAULT|FT_LOAD_NO_BITMAP); if (error) return geterror(error); a = face->glyph->metrics.horiBearingX / 64.0; @@ -363,8 +368,8 @@ font_render(FontObject* self, PyObject* args) } im = (Imaging) id; - - load_flags = FT_LOAD_RENDER; + /* Note: bitmap fonts within ttf fonts do not work, see #891/pr#960 */ + load_flags = FT_LOAD_RENDER|FT_LOAD_NO_BITMAP; if (mask) load_flags |= FT_LOAD_TARGET_MONO; @@ -567,7 +572,7 @@ setup_module(PyObject* m) { PyType_Ready(&Font_Type); if (FT_Init_FreeType(&library)) - return 0; /* leave it uninitalized */ + return 0; /* leave it uninitialized */ FT_Library_Version(library, &major, &minor, &patch); diff --git a/_webp.c b/_webp.c index c201813d7..a8c6d40af 100644 --- a/_webp.c +++ b/_webp.c @@ -136,9 +136,9 @@ PyObject* WebPEncode_wrapper(PyObject* self, PyObject* args) PyObject* WebPDecode_wrapper(PyObject* self, PyObject* args) { PyBytesObject *webp_string; - uint8_t *webp; + const uint8_t *webp; Py_ssize_t size; - PyObject *ret, *bytes, *pymode, *icc_profile = Py_None, *exif = Py_None; + PyObject *ret = Py_None, *bytes = NULL, *pymode = NULL, *icc_profile = NULL, *exif = NULL; WebPDecoderConfig config; VP8StatusCode vp8_status_code = VP8_STATUS_OK; char* mode = "RGB"; @@ -173,31 +173,34 @@ PyObject* WebPDecode_wrapper(PyObject* self, PyObject* args) WebPData exif_data = {0}; WebPMux* mux = WebPMuxCreate(&data, copy_data); - WebPMuxGetFrame(mux, 1, &image); - webp = (uint8_t*)image.bitstream.bytes; + if (NULL == mux) + goto end; + + if (WEBP_MUX_OK != WebPMuxGetFrame(mux, 1, &image)) + { + WebPMuxDelete(mux); + goto end; + } + + webp = image.bitstream.bytes; size = image.bitstream.size; vp8_status_code = WebPDecode(webp, size, &config); - WebPMuxGetChunk(mux, "ICCP", &icc_profile_data); - if (icc_profile_data.size > 0) { + if (WEBP_MUX_OK == WebPMuxGetChunk(mux, "ICCP", &icc_profile_data)) icc_profile = PyBytes_FromStringAndSize((const char*)icc_profile_data.bytes, icc_profile_data.size); - } - WebPMuxGetChunk(mux, "EXIF", &exif_data); - if (exif_data.size > 0) { + if (WEBP_MUX_OK == WebPMuxGetChunk(mux, "EXIF", &exif_data)) exif = PyBytes_FromStringAndSize((const char*)exif_data.bytes, exif_data.size); - } + WebPDataClear(&image.bitstream); WebPMuxDelete(mux); } #endif } - if (vp8_status_code != VP8_STATUS_OK) { - WebPFreeDecBuffer(&config.output); - Py_RETURN_NONE; - } + if (vp8_status_code != VP8_STATUS_OK) + goto end; if (config.output.colorspace < MODE_YUV) { bytes = PyBytes_FromStringAndSize((char *)config.output.u.RGBA.rgba, @@ -215,8 +218,21 @@ PyObject* WebPDecode_wrapper(PyObject* self, PyObject* args) pymode = PyString_FromString(mode); #endif ret = Py_BuildValue("SiiSSS", bytes, config.output.width, - config.output.height, pymode, icc_profile, exif); + config.output.height, pymode, + NULL == icc_profile ? Py_None : icc_profile, + NULL == exif ? Py_None : exif); + +end: WebPFreeDecBuffer(&config.output); + + Py_XDECREF(bytes); + Py_XDECREF(pymode); + Py_XDECREF(icc_profile); + Py_XDECREF(exif); + + if (Py_None == ret) + Py_RETURN_NONE; + return ret; } diff --git a/build_children.sh b/build_children.sh new file mode 100755 index 000000000..c4ed4ebfa --- /dev/null +++ b/build_children.sh @@ -0,0 +1,7 @@ +#!/bin/bash + +# Get last child project build number from branch named "latest" +BUILD_NUM=$(curl -s 'https://api.travis-ci.org/repos/python-pillow/pillow-wheels/branches/latest' | grep -o '^{"branch":{"id":[0-9]*,' | grep -o '[0-9]' | tr -d '\n') + +# Restart last child project build +curl -X POST https://api.travis-ci.org/builds/$BUILD_NUM/restart --header "Authorization: token "$AUTH_TOKEN diff --git a/decode.c b/decode.c index d5e329384..6299d9124 100644 --- a/decode.c +++ b/decode.c @@ -103,6 +103,8 @@ PyImaging_DecoderNew(int contextsize) static void _dealloc(ImagingDecoderObject* decoder) { + if (decoder->cleanup) + decoder->cleanup(&decoder->state); free(decoder->state.buffer); free(decoder->state.context); Py_XDECREF(decoder->lock); @@ -114,12 +116,17 @@ _decode(ImagingDecoderObject* decoder, PyObject* args) { UINT8* buffer; int bufsize, status; + ImagingSectionCookie cookie; if (!PyArg_ParseTuple(args, PY_ARG_BYTES_LENGTH, &buffer, &bufsize)) return NULL; + ImagingSectionEnter(&cookie); + status = decoder->decode(decoder->im, &decoder->state, buffer, bufsize); + ImagingSectionLeave(&cookie); + return Py_BuildValue("ii", status, decoder->state.errcode); } @@ -442,8 +449,9 @@ PyImaging_LibTiffDecoderNew(PyObject* self, PyObject* args) char* rawmode; char* compname; int fp; + int ifdoffset; - if (! PyArg_ParseTuple(args, "sssi", &mode, &rawmode, &compname, &fp)) + if (! PyArg_ParseTuple(args, "sssii", &mode, &rawmode, &compname, &fp, &ifdoffset)) return NULL; TRACE(("new tiff decoder %s\n", compname)); @@ -455,7 +463,7 @@ PyImaging_LibTiffDecoderNew(PyObject* self, PyObject* args) if (get_unpacker(decoder, mode, rawmode) < 0) return NULL; - if (! ImagingLibTiffInit(&decoder->state, fp)) { + if (! ImagingLibTiffInit(&decoder->state, fp, ifdoffset)) { Py_DECREF(decoder); PyErr_SetString(PyExc_RuntimeError, "tiff codec initialization failed"); return NULL; diff --git a/depends/install_openjpeg.sh b/depends/install_openjpeg.sh index b3a6ccc4d..83620f4b8 100755 --- a/depends/install_openjpeg.sh +++ b/depends/install_openjpeg.sh @@ -11,9 +11,9 @@ rm -r openjpeg-2.1.0 tar -xvzf openjpeg-2.1.0.tar.gz -pushd openjpeg-2.1.0 +pushd openjpeg-2.1.0 -cmake -DCMAKE_INSTALL_PREFIX=/usr . && make && sudo make install +cmake -DCMAKE_INSTALL_PREFIX=/usr . && make -j4 && sudo make -j4 install popd diff --git a/depends/install_webp.sh b/depends/install_webp.sh index 5f5963712..97edf2bcf 100755 --- a/depends/install_webp.sh +++ b/depends/install_webp.sh @@ -1,18 +1,15 @@ #!/bin/bash # install webp - -if [ ! -f libwebp-0.4.0.tar.gz ]; then - wget 'https://webp.googlecode.com/files/libwebp-0.4.0.tar.gz' +if [ ! -f libwebp-0.4.3.tar.gz ]; then + wget 'http://downloads.webmproject.org/releases/webp/libwebp-0.4.3.tar.gz' fi -rm -r libwebp-0.4.0 -tar -xvzf libwebp-0.4.0.tar.gz +rm -r libwebp-0.4.3 +tar -xvzf libwebp-0.4.3.tar.gz +pushd libwebp-0.4.3 -pushd libwebp-0.4.0 - -./configure --prefix=/usr --enable-libwebpmux --enable-libwebpdemux && make && sudo make install +./configure --prefix=/usr --enable-libwebpmux --enable-libwebpdemux && make -j4 && sudo make -j4 install popd - diff --git a/docs/PIL.rst b/docs/PIL.rst index 8bf89c685..53a61872b 100644 --- a/docs/PIL.rst +++ b/docs/PIL.rst @@ -113,6 +113,29 @@ can be found here. :undoc-members: :show-inheritance: +:class:`PngImagePlugin.iTXt` Class +---------------------------------- + +.. autoclass:: PIL.PngImagePlugin.iTXt + :members: + :undoc-members: + :show-inheritance: + + .. method:: __new__(cls, text, lang, tkey) + + :param value: value for this key + :param lang: language code + :param tkey: UTF-8 version of the key name + +:class:`PngImagePlugin.PngInfo` Class +------------------------------------- + +.. autoclass:: PIL.PngImagePlugin.PngInfo + :members: + :undoc-members: + :show-inheritance: + + :mod:`TarIO` Module ------------------- diff --git a/docs/_build/.gitignore b/docs/_build/.gitignore deleted file mode 100644 index b1f9a2ade..000000000 --- a/docs/_build/.gitignore +++ /dev/null @@ -1 +0,0 @@ -# Empty file, to make the directory available in the repository diff --git a/docs/about.rst b/docs/about.rst index 919b2918c..67bf20481 100644 --- a/docs/about.rst +++ b/docs/about.rst @@ -4,7 +4,7 @@ About Pillow Goals ----- -The fork authors' goal is to foster active development of PIL through: +The fork author's goal is to foster and support active development of PIL through: - Continuous integration testing via `Travis CI`_ - Publicized development activity on `GitHub`_ @@ -17,7 +17,7 @@ The fork authors' goal is to foster active development of PIL through: License ------- -like PIL itself, Pillow is licensed under the MIT-like `PIL Software License `:: +Like PIL, Pillow is licensed under the MIT-like open source `PIL Software License `_:: Software License @@ -35,10 +35,7 @@ like PIL itself, Pillow is licensed under the MIT-like `PIL Software License v documentation". +#html_title = None + +# A shorter title for the navigation bar. Default is the same as html_title. +#html_short_title = None + +# The name of an image file (relative to this directory) to place at the top +# of the sidebar. +#html_logo = None + +# The name of an image file (within the static path) to use as favicon of the +# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 +# pixels large. +#html_favicon = None + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ['_static'] -html_theme_options = {} +# Add any extra paths that contain custom files (such as robots.txt or +# .htaccess) here, relative to this directory. These files are copied +# directly to the root of the documentation. +#html_extra_path = [] -html_sidebars = { - '**': ['localtoc.html', 'sourcelink.html', 'sidebarhelp.html', - 'searchbox.html'], - 'index': ['globaltoc.html', 'sidebarhelp.html', 'searchbox.html'], -} +# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, +# using the given strftime format. +#html_last_updated_fmt = '%b %d, %Y' + +# If true, SmartyPants will be used to convert quotes and dashes to +# typographically correct entities. +#html_use_smartypants = True + +# Custom sidebar templates, maps document names to template names. +#html_sidebars = {} + +# Additional templates that should be rendered to pages, maps page names to +# template names. +#html_additional_pages = {} + +# If false, no module index is generated. +#html_domain_indices = True + +# If false, no index is generated. +#html_use_index = True + +# If true, the index is split into individual pages for each letter. +#html_split_index = False + +# If true, links to the reST sources are added to the pages. +#html_show_sourcelink = True + +# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. +#html_show_sphinx = True + +# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. +#html_show_copyright = True + +# If true, an OpenSearch description file will be output, and all pages will +# contain a tag referring to it. The value of this option must be the +# base URL from which the finished HTML is served. +#html_use_opensearch = '' + +# This is the file name suffix for HTML files (e.g. ".xhtml"). +#html_file_suffix = None + +# Language to be used for generating the HTML full-text search index. +# Sphinx supports the following languages: +# 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja' +# 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr' +#html_search_language = 'en' + +# A dictionary with options for the search language support, empty by default. +# Now only 'ja' uses this config value +#html_search_options = {'type': 'default'} + +# The name of a javascript file (relative to the configuration directory) that +# implements a search results scorer. If empty, the default will be used. +#html_search_scorer = 'scorer.js' # Output file base name for HTML help builder. -htmlhelp_basename = 'Pillowdoc' +htmlhelp_basename = 'PillowPILForkdoc' +# -- Options for LaTeX output --------------------------------------------- -### LaTeX output (RtD PDF output as well) ### +latex_elements = { +# The paper size ('letterpaper' or 'a4paper'). +#'papersize': 'letterpaper', -latex_elements = {} +# The font size ('10pt', '11pt' or '12pt'). +#'pointsize': '10pt', +# Additional stuff for the LaTeX preamble. +#'preamble': '', + +# Latex figure (float) alignment +#'figure_align': 'htbp', +} + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, +# author, documentclass [howto, manual, or own class]). latex_documents = [ - ('index', 'Pillow.tex', u'Pillow (PIL fork) Documentation', u'Author', - 'manual'), + (master_doc, 'PillowPILFork.tex', u'Pillow (PIL Fork) Documentation', + u'Alex Clark', 'manual'), ] +# The name of an image file (relative to this directory) to place at the top of +# the title page. +#latex_logo = None -# skip_api_docs setting will skip PIL.rst if True. Used for working on the -# guides; makes livereload basically instantaneous. -def setup(app): - app.add_config_value('skip_api_docs', False, True) +# For "manual" documents, if this is true, then toplevel headings are parts, +# not chapters. +#latex_use_parts = False -skip_api_docs = False +# If true, show page references after internal links. +#latex_show_pagerefs = False -if skip_api_docs: - exclude_patterns += ['PIL.rst'] +# If true, show URL addresses after external links. +#latex_show_urls = False + +# Documents to append as an appendix to all manuals. +#latex_appendices = [] + +# If false, no module index is generated. +#latex_domain_indices = True + + +# -- Options for manual page output --------------------------------------- + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [ + (master_doc, 'pillowpilfork', u'Pillow (PIL Fork) Documentation', + [author], 1) +] + +# If true, show URL addresses after external links. +#man_show_urls = False + + +# -- Options for Texinfo output ------------------------------------------- + +# Grouping the document tree into Texinfo files. List of tuples +# (source start file, target name, title, author, +# dir menu entry, description, category) +texinfo_documents = [ + (master_doc, 'PillowPILFork', u'Pillow (PIL Fork) Documentation', + author, 'PillowPILFork', 'One line description of project.', + 'Miscellaneous'), +] + +# Documents to append as an appendix to all manuals. +#texinfo_appendices = [] + +# If false, no module index is generated. +#texinfo_domain_indices = True + +# How to display URL addresses: 'footnote', 'no', or 'inline'. +#texinfo_show_urls = 'footnote' + +# If true, do not generate a @detailmenu in the "Top" node's menu. +#texinfo_no_detailmenu = False diff --git a/docs/handbook/appendices.rst b/docs/handbook/appendices.rst index f5482ffcd..e6c415cc6 100644 --- a/docs/handbook/appendices.rst +++ b/docs/handbook/appendices.rst @@ -1,6 +1,8 @@ Appendices ========== +.. note:: Contributors please include appendices as needed or appropriate with your bug fixes, feature additions and tests. + .. toctree:: :maxdepth: 2 diff --git a/docs/handbook/concepts.rst b/docs/handbook/concepts.rst index b5e5e44c1..fb97fe098 100644 --- a/docs/handbook/concepts.rst +++ b/docs/handbook/concepts.rst @@ -14,8 +14,10 @@ same dimensions and depth. To get the number and names of bands in an image, use the :py:meth:`~PIL.Image.Image.getbands` method. -Mode ----- +.. _concept-modes: + +Modes +----- The :term:`mode` of an image defines the type and depth of a pixel in the image. The current release supports the following standard modes: @@ -87,25 +89,21 @@ pixel, the Python Imaging Library provides four different resampling *filters*. Pick the nearest pixel from the input image. Ignore all other input pixels. ``BILINEAR`` - Use linear interpolation over a 2x2 environment in the input image. Note - that in the current version of PIL, this filter uses a fixed input - environment when downsampling. + For resize calculate the output pixel value using linear interpolation + on all pixels that may contribute to the output value. + For other transformations linear interpolation over a 2x2 environment + in the input image is used. ``BICUBIC`` - Use cubic interpolation over a 4x4 environment in the input image. Note - that in the current version of PIL, this filter uses a fixed input - environment when downsampling. + For resize calculate the output pixel value using cubic interpolation + on all pixels that may contribute to the output value. + For other transformations cubic interpolation over a 4x4 environment + in the input image is used. -``ANTIALIAS`` - Calculate the output pixel value using a high-quality resampling filter (a +``LANCZOS`` + Calculate the output pixel value using a high-quality Lanczos filter (a truncated sinc) on all pixels that may contribute to the output value. In the current version of PIL, this filter can only be used with the resize and thumbnail methods. .. versionadded:: 1.1.3 - -Note that in the current version of PIL, the ``ANTIALIAS`` filter is the only -filter that behaves properly when downsampling (that is, when converting a -large image to a small one). The ``BILINEAR`` and ``BICUBIC`` filters use a -fixed input environment, and are best used for scale-preserving geometric -transforms and upsamping. diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index 97caea722..a85a917b8 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -117,20 +117,48 @@ 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. -The :py:meth:`~PIL.Image.Image.open` method sets the following -:py:attr:`~PIL.Image.Image.info` properties: +The :py:meth:`~PIL.Image.Image.open` method may set the following +:py:attr:`~PIL.Image.Image.info` properties if available: **jfif** JFIF application marker found. If the file is not a JFIF file, this key is not present. +**jfif_version** + A tuple representing the jfif version, (major version, minor version). + +**jfif_density** + A tuple representing the pixel density of the image, in units specified + by jfif_unit. + +**jfif_unit** + Units for the jfif_density: + + * 0 - No Units + * 1 - Pixels per Inch + * 2 - Pixels per Centimeter + +**dpi** + A tuple representing the reported pixel density in pixels per inch, if + the file is a jfif file and the units are in inches. + **adobe** Adobe application marker found. If the file is not an Adobe JPEG file, this key is not present. +**adobe_transform** + Vendor Specific Tag. + **progression** Indicates that this is a progressive JPEG file. +**icc-profile** + The ICC color profile for the image. + +**exif** + Raw EXIF data from the image. + + The :py:meth:`~PIL.Image.Image.save` method supports the following options: **quality** @@ -147,9 +175,22 @@ The :py:meth:`~PIL.Image.Image.save` method supports the following options: If present, indicates that this image should be stored as a progressive JPEG file. +**dpi** + A tuple of integers representing the pixel density, ``(x,y)``. + +**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:: + + im.save(filename, 'jpeg', icc_profile=im.info.get('icc_profile')) + +**exif** + If present, the image will be stored with the provided raw EXIF data. + **subsampling** - If present, sets the subsampling for the encoder. - + If present, sets the subsampling for the encoder. + * ``keep``: Only valid for JPEG files, will retain the original image setting. * ``4:4:4``, ``4:2:2``, ``4:1:1``: Specific sampling values * ``-1``: equivalent to ``keep`` @@ -165,7 +206,7 @@ The :py:meth:`~PIL.Image.Image.save` method supports the following options: * a string, naming a preset, e.g. ``keep``, ``web_low``, or ``web_high`` * a list, tuple, or dictionary (with integer keys = range(len(keys))) of lists of 64 integers. There must be - between 2 and 4 tables. + between 2 and 4 tables. .. versionadded:: 2.5.0 @@ -291,6 +332,14 @@ The :py:meth:`~PIL.Image.Image.open` method sets the following Transparency color index. This key is omitted if the image is not a transparent palette image. +``Open`` also sets ``Image.text`` to a list of the values of the +``tEXt``, ``zTXt``, and ``iTXt`` chunks of the PNG image. Individual +compressed chunks are limited to a decompressed size of +``PngImagePlugin.MAX_TEXT_CHUNK``, by default 1MB, to prevent +decompression bombs. Additionally, the total size of all of the text +chunks is limited to ``PngImagePlugin.MAX_TEXT_MEMORY``, defaulting to +64MB. + The :py:meth:`~PIL.Image.Image.save` method supports the following options: **optimize** @@ -298,10 +347,22 @@ The :py:meth:`~PIL.Image.Image.save` method supports the following options: possible. This includes extra processing in order to find optimal encoder settings. -**transparency** +**transparency** For ``P``, ``L``, and ``RGB`` images, this option controls what color image to mark as transparent. +**dpi** + A tuple of two numbers corresponding to the desired dpi in each direction. + +**pnginfo** + A :py:class:`PIL.PngImagePlugin.PngInfo` instance containing text tags. + +**compress_level** + ZLIB compression level, a number between 0 and 9: 1 gives best speed, + 9 gives best compression, 0 gives no compression at all. Default is 6. + When ``optimize`` option is True ``compress_level`` has no effect + (it is set to 9 regardless of a value passed). + **bits (experimental)** For ``P`` images, this option controls how many bits to store. If omitted, the PNG writer uses 8 bits (256 colors). @@ -356,9 +417,9 @@ the output format must be specified explicitly:: im.save('newimage.spi', format='SPIDER') For more information about the SPIDER image processing package, see the -`SPIDER home page`_ at `Wadsworth Center`_. +`SPIDER homepage`_ at `Wadsworth Center`_. -.. _SPIDER home page: http://www.wadsworth.org/spider_doc/spider/docs/master.html +.. _SPIDER homepage: http://spider.wadsworth.org/spider_doc/spider/docs/spider.html .. _Wadsworth Center: http://www.wadsworth.org/ TIFF @@ -395,7 +456,7 @@ Saving Tiff Images The :py:meth:`~PIL.Image.Image.save` method can take the following keyword arguments: -**tiffinfo** +**tiffinfo** A :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory` object or dict object containing tiff tags and values. The TIFF field type is autodetected for Numeric and string values, any other types @@ -404,7 +465,7 @@ The :py:meth:`~PIL.Image.Image.save` method can take the following keyword argum :py:attr:`~PIL.TiffImagePlugin.ImageFileDirectory.tagtype` with the appropriate numerical value from ``TiffTags.TYPES``. - + .. versionadded:: 2.3.0 **compression** @@ -416,25 +477,25 @@ The :py:meth:`~PIL.Image.Image.save` method can take the following keyword argum These arguments to set the tiff header fields are an alternative to using the general tags available through tiffinfo. -**description** +**description** **software** -**date time** +**date_time** **artist** **copyright** Strings -**resolution unit** - A string of "inch", "centimeter" or "cm" +**resolution_unit** + A string of "inch", "centimeter" or "cm" **resolution** -**x resolution** +**x_resolution** -**y resolution** +**y_resolution** **dpi** Either a Float, Integer, or 2 tuple of (numerator, @@ -548,6 +609,14 @@ ICO ICO is used to store icons on Windows. The largest available icon is read. +The :py:meth:`~PIL.Image.Image.save` method supports the following options: + +**sizes** + A list of sizes including in this ico file; these are a 2-tuple, + ``(width, height)``; Default to ``[(16, 16), (24, 24), (32, 32), (48, 48), + (64, 64), (128, 128), (255, 255)]``. Any size is bigger then the original + size or 255 will be ignored. + ICNS ^^^^ @@ -670,6 +739,7 @@ files, using either JPEG or HEX encoding depending on the image mode (and whether JPEG support is available or not). PIXAR (read only) +^^^^^^^^^^^^^^^^^ PIL provides limited support for PIXAR raster files. The library can identify and read “dumped†RGB files. diff --git a/docs/handbook/overview.rst b/docs/handbook/overview.rst index f1c26e616..b52939b89 100644 --- a/docs/handbook/overview.rst +++ b/docs/handbook/overview.rst @@ -16,7 +16,7 @@ Let’s look at a few possible uses of this library. Image Archives -------------- -The Python Imaging Library is ideal for for image archival and batch processing +The Python Imaging Library is ideal for image archival and batch processing applications. You can use the library to create thumbnails, convert between file formats, print images, etc. diff --git a/docs/handbook/tutorial.rst b/docs/handbook/tutorial.rst index 05d619f40..365c8e7a8 100644 --- a/docs/handbook/tutorial.rst +++ b/docs/handbook/tutorial.rst @@ -296,7 +296,7 @@ Point Operations The :py:meth:`~PIL.Image.Image.point` method can be used to translate the pixel values of an image (e.g. image contrast manipulation). In most cases, a -function object expecting one argument can be passed to the this method. Each +function object expecting one argument can be passed to this method. Each pixel is processed according to that function: Applying point transforms @@ -397,7 +397,7 @@ Note that most drivers in the current version of the library only allow you to seek to the next frame (as in the above example). To rewind the file, you may have to reopen it. -The following iterator class lets you to use the for-statement to loop over the +The following iterator class lets you use the for-statement to loop over the sequence: A sequence iterator class @@ -445,10 +445,9 @@ Drawing Postscript ps.image(box, im, 75) ps.rectangle(box) - # draw centered title + # draw title ps.setfont("HelveticaNarrow-Bold", 36) - w, h, b = ps.textsize(title) - ps.text((4*72-w/2, 1*72-h), title) + ps.text((3*72, 4*72), title) ps.end_document() diff --git a/docs/handbook/writing-your-own-file-decoder.rst b/docs/handbook/writing-your-own-file-decoder.rst index 10833a53e..0af4007be 100644 --- a/docs/handbook/writing-your-own-file-decoder.rst +++ b/docs/handbook/writing-your-own-file-decoder.rst @@ -1,4 +1,6 @@ -Writing your own file decoder +.. _file-decoders: + +Writing Your Own File Decoder ============================= The Python Imaging Library uses a plug-in model which allows you to @@ -7,7 +9,7 @@ library itself. Such plug-ins usually have names like :file:`XxxImagePlugin.py`, where ``Xxx`` is a unique format name (usually an abbreviation). -.. warning:: Pillow >= 2.1.0 no longer automatically imports any file in the Python path with a name ending in :file:`ImagePlugin.py`. You will need to import your decoder manually. +.. warning:: Pillow >= 2.1.0 no longer automatically imports any file in the Python path with a name ending in :file:`ImagePlugin.py`. You will need to import your decoder manually. A decoder plug-in should contain a decoder class, based on the :py:class:`PIL.ImageFile.ImageFile` base class. This class should provide an @@ -68,7 +70,7 @@ true color. ] Image.register_open("SPAM", SpamImageFile) - + Image.register_extension("SPAM", ".spam") Image.register_extension("SPAM", ".spa") # dos version @@ -116,7 +118,8 @@ The fields are used as follows: Note that the :py:attr:`tile` attribute contains a list of tile descriptors, not just a single descriptor. -The ``raw`` decoder +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 @@ -124,7 +127,7 @@ TIFF, and many others. To use the raw decoder with the :py:func:`PIL.Image.fromstring` function, use the following syntax:: image = Image.fromstring( - mode, size, data, "raw", + mode, size, data, "raw", raw mode, stride, orientation ) diff --git a/docs/index.rst b/docs/index.rst index 16e450856..0c7365c64 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,11 +1,11 @@ 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://travis-ci.org/python-pillow/Pillow.svg?branch=master :target: https://travis-ci.org/python-pillow/Pillow - :alt: Travis CI build status + :alt: Travis CI build status (Linux) .. image:: https://pypip.in/v/Pillow/badge.png :target: https://pypi.python.org/pypi/Pillow/ @@ -16,20 +16,23 @@ Pillow is the 'friendly' PIL fork by Alex Clark and Contributors. PIL is the Pyt :alt: Number of PyPI downloads .. image:: https://coveralls.io/repos/python-pillow/Pillow/badge.png?branch=master - :target: https://coveralls.io/r/python-pillow/Pillow?branch=master - :alt: Test coverage + :target: https://coveralls.io/r/python-pillow/Pillow?branch=master + :alt: Code coverage -To install Pillow, please follow the :doc:`installation instructions `. To download source and/or contribute to development of Pillow please see: https://github.com/python-pillow/Pillow. +.. image:: https://landscape.io/github/python-pillow/Pillow/master/landscape.png + :target: https://landscape.io/github/python-pillow/Pillow/master + :alt: Code health .. toctree:: :maxdepth: 2 installation - about guides reference/index.rst handbook/appendices - original-readme + releasenotes/index.rst + about + pre-fork-readme Indices and tables ================== diff --git a/docs/installation.rst b/docs/installation.rst index a61213e15..e94afa892 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -1,39 +1,38 @@ Installation ============ -.. warning:: Pillow >= 2.1.0 no longer supports "import _imaging". Please use "from PIL.Image import core as _imaging" instead. +Warnings +-------- + +.. warning:: Pillow and PIL cannot co-exist in the same environment. Before installing Pillow, please uninstall PIL. .. warning:: Pillow >= 1.0 no longer supports "import Image". Please use "from PIL import Image" instead. -.. warning:: PIL and Pillow currently cannot co-exist in the same environment. - If you want to use Pillow, please remove PIL first. +.. warning:: Pillow >= 2.1.0 no longer supports "import _imaging". Please use "from PIL.Image import core as _imaging" instead. -.. note:: Pillow >= 2.0.0 supports Python versions 2.6, 2.7, 3.2, 3.3, 3.4 +Notes +----- .. note:: Pillow < 2.0.0 supports Python versions 2.4, 2.5, 2.6, 2.7. -Simple installation -------------------- +.. note:: Pillow >= 2.0.0 supports Python versions 2.6, 2.7, 3.2, 3.3, 3.4 + +Basic Installation +------------------ .. note:: - The following instructions will install Pillow with support for most formats. - See :ref:`external-libraries` for the features you would gain by installing - the external libraries first. This page probably also include specific - instructions for your platform. + 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. -You can install Pillow with :command:`pip`:: +Install Pillow with :command:`pip`:: $ pip install Pillow -Or :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 the `compressed archive from PyPI`_, extract it, and inside it -run:: +Or download and extract the `compressed archive from PyPI`_ and inside it run:: $ python setup.py install @@ -41,22 +40,22 @@ run:: .. _external-libraries: -External libraries +External Libraries ------------------ .. note:: - You *do not* need to install all of the external libraries to use Pillow's basic features. + You **do not need to install all external libraries supported** to use Pillow's basic features. Many of Pillow's features require external libraries: * **libjpeg** provides JPEG functionality. - * Pillow has been tested with libjpeg versions **6b**, **8**, and **9** + * Pillow has been tested with libjpeg versions **6b**, **8**, and **9** and libjpeg-turbo version **8**. * **zlib** provides access to compressed PNGs -* **libtiff** provides group4 tiff functionality +* **libtiff** provides compressed TIFF functionality * Pillow has been tested with libtiff versions **3.x** and **4.0** @@ -67,10 +66,10 @@ Many of Pillow's features require external libraries: * Pillow version 2.2.1 and below uses liblcms1, Pillow 2.3.0 and above uses liblcms2. Tested with **1.19** and **2.2**. -* **libwebp** provides the Webp format. +* **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 **0.4.0** support transparency. * **tcl/tk** provides support for tkinter bitmap and photo images. @@ -93,50 +92,37 @@ line:: $ CFLAGS="-I/usr/pkg/include" pip install pillow +Build Options +------------- -Linux installation ------------------- +* Environment Variable: ``MAX_CONCURRENCY=n``. By default, Pillow will + use multiprocessing to build the extension on all available CPUs, + but not more than 4. Setting ``MAX_CONCURRENCY`` to 1 will disable + parallel building. -.. note:: +* Build flags: ``--disable-zlib``, ``--disable-jpeg``, + ``--disable-tiff``, ``--disable-freetype``, ``--disable-tcl``, + ``--disable-tk``, ``--disable-lcms``, ``--disable-webp``, + ``--disable-webpmux``, ``--disable-jpeg2000``. Disable building the + corresponding feature even if the development libraries are present + on the building machine. - Fedora, Debian/Ubuntu, and ArchLinux include Pillow (instead of PIL) with - their distributions. Consider using those instead of installing manually. +* Build flags: ``--enable-zlib``, ``--enable-jpeg``, + ``--enable-tiff``, ``--enable-freetype``, ``--enable-tcl``, + ``--enable-tk``, ``--enable-lcms``, ``--enable-webp``, + ``--enable-webpmux``, ``--enable-jpeg2000``. Require that the + corresponding feature is built. The build will raise an exception if + the libraries are not found. Webpmux (WebP metadata) relies on WebP + support. Tcl and Tk also must be used together. -**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:: +Sample Usage:: - $ sudo apt-get install python-dev python-setuptools + $ MAX_CONCURRENCY=1 python setup.py build_ext --enable-[feature] install -Or for Python 3:: +OS X Installation +----------------- - $ sudo apt-get install python3-dev python3-setuptools - -In Fedora, the command is:: - - $ sudo yum install python-devel - -Prerequisites are installed on **Ubuntu 10.04 LTS** with:: - - $ sudo apt-get install libtiff4-dev libjpeg62-dev zlib1g-dev \ - libfreetype6-dev tcl8.5-dev tk8.5-dev python-tk - -Prerequisites are installed with on **Ubuntu 12.04 LTS** or **Raspian Wheezy -7.0** with:: - - $ sudo apt-get install libtiff4-dev libjpeg8-dev zlib1g-dev \ - libfreetype6-dev liblcms2-dev libwebp-dev tcl8.5-dev tk8.5-dev python-tk - -Prerequisites are installed on **Fedora 20** with:: - - $ sudo yum install libtiff-devel libjpeg-devel libzip-devel freetype-devel \ - lcms2-devel libwebp-devel tcl-devel tk-devel - - -Mac OS X installation ---------------------- - -We provide binaries for OS X in the form of `Python Wheels `_. Alternatively you can compile Pillow with with XCode. +We provide binaries for OS X in the form of `Python Wheels `_. Alternatively you can compile Pillow from soure with XCode. The easiest way to install external libraries is via `Homebrew `_. After you install Homebrew, run:: @@ -146,7 +132,7 @@ Install Pillow with:: $ pip install Pillow -Windows installation +Windows Installation -------------------- We provide binaries for Windows in the form of Python Eggs and `Python Wheels @@ -167,11 +153,11 @@ Python Eggs Python Wheels ^^^^^^^^^^^^^ -.. Note:: Experimental. Requires setuptools >=0.8 and pip >=1.4.1 +.. Note:: Requires setuptools >=0.8 and pip >=1.4.1. Some older versions of pip required the ``--use-wheel`` flag. :: - $ pip install --use-wheel Pillow + $ 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 @@ -179,14 +165,13 @@ to a specific version: :: - $ pip install --use-wheel Pillow==2.3.0 + $ pip install Pillow==2.6.1 -FreeBSD installation ---------------------- +FreeBSD Installation +-------------------- .. Note:: Only FreeBSD 10 tested - Make sure you have Python's development libraries installed.:: $ sudo pkg install python2 @@ -199,9 +184,48 @@ Prerequisites are installed on **FreeBSD 10** with:: $ sudo pkg install jpeg tiff webp lcms2 freetype2 +Linux Installation +------------------ + +.. note:: + + 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:: + + $ sudo apt-get install python-dev python-setuptools + +Or for Python 3:: + + $ sudo apt-get install python3-dev python3-setuptools + +In Fedora, the command is:: + + $ sudo yum install python-devel + +Prerequisites are installed on **Ubuntu 12.04 LTS** or **Raspian Wheezy +7.0** with:: + + $ sudo apt-get install libtiff4-dev libjpeg8-dev zlib1g-dev \ + libfreetype6-dev liblcms2-dev libwebp-dev tcl8.5-dev tk8.5-dev python-tk + +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:: + + $ sudo yum install libtiff-devel libjpeg-devel libzip-devel freetype-devel \ + lcms2-devel libwebp-devel tcl-devel tk-devel -Platform support + + + +Platform Support ---------------- Current platform support for Pillow. Binary distributions are contributed for @@ -211,15 +235,16 @@ current versions of Linux, OS X, and Windows. .. note:: - Contributors please test on your platform, edit 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.8 Mountain Lion |Yes | 2.6,2.7,3.2,3.3 | |x86-64 | +| Mac OS X 10.10 Yosemite |Yes | 2.7,3.3,3.4 | 2.8.1 |x86-64 | +----------------------------------+-------------+------------------------------+------------------------------+-----------------------+ -| Mac OS X 10.7 Lion |Yes | 2.6,2.7,3.2,3.3 | 2.2.0 |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 | +----------------------------------+-------------+------------------------------+------------------------------+-----------------------+ @@ -229,11 +254,12 @@ current versions of Linux, OS X, and Windows. +----------------------------------+-------------+------------------------------+------------------------------+-----------------------+ | 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.1 | 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.3.0 |ppc | +| | | 2.7,3.2 | 2.6.1 |ppc | +----------------------------------+-------------+------------------------------+------------------------------+-----------------------+ -| Ubuntu Linux 13.10 |Yes | 2.7,3.2,3.3 | 2.3.0 |x86 | +| 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 | +----------------------------------+-------------+------------------------------+------------------------------+-----------------------+ diff --git a/docs/porting-pil-to-pillow.rst b/docs/porting-pil-to-pillow.rst index 88a6768e9..7ad781186 100644 --- a/docs/porting-pil-to-pillow.rst +++ b/docs/porting-pil-to-pillow.rst @@ -21,3 +21,7 @@ automatically imports any file in the Python path with a name ending in :file:`ImagePlugin.py`. You will need to import your image plugin manually. +Pillow will raise an exception if the core extension can't be loaded +for any reason, including a version mismatch between the Python and +extension code. Previously PIL allowed Python only code to run if the +core extension was not available. diff --git a/docs/original-readme.rst b/docs/pre-fork-readme.rst similarity index 99% rename from docs/original-readme.rst rename to docs/pre-fork-readme.rst index 73b941f37..f50e2ef85 100644 --- a/docs/original-readme.rst +++ b/docs/pre-fork-readme.rst @@ -1,7 +1,7 @@ -Original PIL README -=================== +Pre-fork README +=============== -What follows is the original PIL 1.1.7 README file contents. +What follows is the untouched, original pre-fork PIL 1.1.7 README file contents. :: diff --git a/docs/reference/Image.rst b/docs/reference/Image.rst index 11666dd0b..974d84a6e 100644 --- a/docs/reference/Image.rst +++ b/docs/reference/Image.rst @@ -41,7 +41,7 @@ Create thumbnails for infile in glob.glob("*.jpg"): file, ext = os.path.splitext(infile) im = Image.open(infile) - im.thumbnail(size, Image.ANTIALIAS) + im.thumbnail(size) im.save(file + ".thumbnail", "JPEG") Functions @@ -49,7 +49,14 @@ Functions .. autofunction:: open - .. warning:: To protect against potential DOS attacks caused by "`decompression bombs`_" (i.e. malicious files which decompress into a huge amount of data and are designed to crash or cause disruption by using up a lot of memory), Pillow will issue a `DecompressionBombWarning` if the image is over a certain limit. If desired, the warning can be turned into an error with `warnings.simplefilter('error', Image.DecompressionBombWarning)` or suppressed entirely with `warnings.simplefilter('ignore', Image.DecompressionBombWarning)`. See also `the logging documentation`_ to have warnings output to the logging facility instead of stderr. + .. warning:: + To protect against potential DOS attacks caused by "`decompression bombs`_" (i.e. malicious files + which decompress into a huge amount of data and are designed to crash or cause disruption by using up + a lot of memory), Pillow will issue a `DecompressionBombWarning` if the image is over a certain + limit. If desired, the warning can be turned into an error with + ``warnings.simplefilter('error', Image.DecompressionBombWarning)`` or suppressed entirely with + ``warnings.simplefilter('ignore', Image.DecompressionBombWarning)``. See also `the logging + documentation`_ to have warnings output to the logging facility instead of stderr. .. _decompression bombs: https://en.wikipedia.org/wiki/Zip_bomb .. _the logging documentation: https://docs.python.org/2/library/logging.html?highlight=logging#integration-with-the-warnings-module @@ -116,6 +123,7 @@ ITU-R 709, using the D65 luminant) to the CIE XYZ color space: .. automethod:: PIL.Image.Image.getcolors .. automethod:: PIL.Image.Image.getdata .. automethod:: PIL.Image.Image.getextrema +.. automethod:: PIL.Image.Image.getpalette .. automethod:: PIL.Image.Image.getpixel .. automethod:: PIL.Image.Image.histogram .. automethod:: PIL.Image.Image.offset @@ -135,6 +143,7 @@ ITU-R 709, using the D65 luminant) to the CIE XYZ color space: .. automethod:: PIL.Image.Image.tell .. automethod:: PIL.Image.Image.thumbnail .. automethod:: PIL.Image.Image.tobitmap +.. automethod:: PIL.Image.Image.tobytes .. automethod:: PIL.Image.Image.tostring .. automethod:: PIL.Image.Image.transform .. automethod:: PIL.Image.Image.transpose @@ -162,7 +171,7 @@ Instances of the :py:class:`Image` class have the following attributes: Image mode. This is a string specifying the pixel format used by the image. Typical values are “1â€, “Lâ€, “RGBâ€, or “CMYK.†See - :doc:`../handbook/concepts` for a full list. + :ref:`concept-modes` for a full list. :type: :py:class:`string` @@ -191,4 +200,6 @@ Instances of the :py:class:`Image` class have the following attributes: operation affects the dictionary. If you need the information later on, keep a reference to the info dictionary returned from the open method. + Unless noted elsewhere, this dictionary does not affect saving files. + :type: :py:class:`dict` diff --git a/docs/reference/ImageDraw.rst b/docs/reference/ImageDraw.rst index c24a9dd99..e6d5c36ee 100644 --- a/docs/reference/ImageDraw.rst +++ b/docs/reference/ImageDraw.rst @@ -184,8 +184,11 @@ Methods :param xy: Four points to define the bounding box. Sequence of ``[(x0, y0), (x1, y1)]`` or ``[x0, y0, x1, y1]``. - :param outline: Color to use for the outline. + :param start: Starting angle, in degrees. Angles are measured from + 3 o'clock, increasing clockwise. + :param end: Ending angle, in degrees. :param fill: Color to use for the fill. + :param outline: Color to use for the outline. .. py:method:: PIL.ImageDraw.Draw.point(xy, fill=None) diff --git a/docs/reference/ImageMath.rst b/docs/reference/ImageMath.rst index e3f9ed8d6..00249a601 100644 --- a/docs/reference/ImageMath.rst +++ b/docs/reference/ImageMath.rst @@ -13,7 +13,7 @@ Example: Using the :py:mod:`~PIL.ImageMath` module .. code-block:: python - import Image, ImageMath + from PIL import Image, ImageMath im1 = Image.open("image1.jpg") im2 = Image.open("image2.jpg") diff --git a/docs/reference/ImagePath.rst b/docs/reference/ImagePath.rst index 700464144..03aa39811 100644 --- a/docs/reference/ImagePath.rst +++ b/docs/reference/ImagePath.rst @@ -53,7 +53,7 @@ vector data. Path objects can be passed to the methods on the Converts the path to a Python list [(x, y), …]. :param flat: By default, this function returns a list of 2-tuples - [(x, y), ...]. If this argument is :keyword:`True`, it + [(x, y), ...]. If this argument is `True`, it returns a flat list [x, y, ...] instead. :return: A list of coordinates. See **flat**. diff --git a/docs/reference/PixelAccess.rst b/docs/reference/PixelAccess.rst new file mode 100644 index 000000000..55ab8c578 --- /dev/null +++ b/docs/reference/PixelAccess.rst @@ -0,0 +1,74 @@ +.. _PixelAccess: + +:py:class:`PixelAccess` Class +============================= + +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. + +Example +------- + +The following script loads an image, accesses one pixel from it, then +changes it. + +.. code-block:: python + + from PIL import Image + im = Image.open('hopper.jpg') + px = im.load() + print (px[4,4]) + px[4,4] = (0,0,0) + print (px[4,4]) + +Results in the following:: + + (23, 24, 68) + (0, 0, 0) + + + +:py:class:`PixelAccess` Class +----------------------------------- + +.. class:: PixelAccess + + .. method:: __setitem__(self, xy, color): + + Modifies the pixel at x,y. The color is given as a single + numerical value for single band images, and a tuple for + multi-band images + + :param xy: The pixel coordinate, given as (x, y). + :param value: The pixel value. + + .. method:: __getitem__(self, xy): + + Returns the pixel at x,y. The pixel is returned as a single + value for single band images or a tuple for multiple band + images + + :param xy: The pixel coordinate, given as (x, y). + :returns: a pixel value for single band images, a tuple of + pixel values for multiband images. + + .. method:: putpixel(self, xy, color): + + Modifies the pixel at x,y. The color is given as a single + numerical value for single band images, and a tuple for + multi-band images + + :param xy: The pixel coordinate, given as (x, y). + :param value: The pixel value. + + .. method:: getpixel(self, xy): + + Returns the pixel at x,y. The pixel is returned as a single + value for single band images or a tuple for multiple band + images + + :param xy: The pixel coordinate, given as (x, y). + :returns: a pixel value for single band images, a tuple of + pixel values for multiband images. diff --git a/docs/reference/PyAccess.rst b/docs/reference/PyAccess.rst new file mode 100644 index 000000000..cb853f89e --- /dev/null +++ b/docs/reference/PyAccess.rst @@ -0,0 +1,38 @@ +.. py:module:: PIL.PyAccess +.. py:currentmodule:: PIL.PyAccess + +:py:mod:`PyAccess` Module +========================= + +The :py:mod:`PyAccess` module provides a CFFI/Python implementation of the :ref:`PixelAccess`. This implementation is far faster on PyPy than the PixelAccess version. + +.. 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 +------- + +The following script loads an image, accesses one pixel from it, then changes it. + +.. code-block:: python + + from PIL import Image + im = Image.open('hopper.jpg') + px = im.load() + print (px[4,4]) + px[4,4] = (0,0,0) + print (px[4,4]) + +Results in the following:: + + (23, 24, 68) + (0, 0, 0) + + + +:py:class:`PyAccess` Class +-------------------------- + +.. autoclass:: PIL.PyAccess.PyAccess() + :members: diff --git a/docs/reference/index.rst b/docs/reference/index.rst index 2f10b861d..73a3ecfed 100644 --- a/docs/reference/index.rst +++ b/docs/reference/index.rst @@ -28,4 +28,6 @@ Reference ExifTags OleFileIO PSDraw + PixelAccess + PyAccess ../PIL diff --git a/docs/releasenotes/2.7.0.rst b/docs/releasenotes/2.7.0.rst new file mode 100644 index 000000000..65a8f2d11 --- /dev/null +++ b/docs/releasenotes/2.7.0.rst @@ -0,0 +1,175 @@ +2.7.0 +===== + +Sane Plugin +----------- + +The Sane plugin has now been split into its own repo: +https://github.com/python-pillow/Sane . + + +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. + +Individual text chunks are limited to +:py:attr:`PIL.PngImagePlugin.MAX_TEXT_CHUNK`, set to 1MB by +default. The total decompressed size of all text chunks is limited to +:py:attr:`PIL.PngImagePlugin.MAX_TEXT_MEMORY`, which defaults to +64MB. These values can be changed prior to opening PNG images if you +know that there are large text blocks that are desired. + +Image resizing filters +---------------------- + +Image resizing methods :py:meth:`~PIL.Image.Image.resize` and +:py:meth:`~PIL.Image.Image.thumbnail` take a `resample` argument, which tells +which filter should be used for resampling. Possible values are: +:py:attr:`PIL.Image.NEAREST`, :py:attr:`PIL.Image.BILINEAR`, +:py:attr:`PIL.Image.BICUBIC` and :py:attr:`PIL.Image.ANTIALIAS`. +Almost all of them were changed in this version. + +Bicubic and bilinear downscaling +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +From the beginning :py:attr:`~PIL.Image.BILINEAR` and +:py:attr:`~PIL.Image.BICUBIC` filters were based on affine transformations +and used a fixed number of pixels from the source image for every destination +pixel (2x2 pixels for :py:attr:`~PIL.Image.BILINEAR` and 4x4 for +:py:attr:`~PIL.Image.BICUBIC`). This gave an unsatisfactory result for +downscaling. At the same time, a high quality convolutions-based algorithm with +flexible kernel was used for :py:attr:`~PIL.Image.ANTIALIAS` filter. + +Starting from Pillow 2.7.0, a high quality convolutions-based algorithm is used +for all of these three filters. + +If you have previously used any tricks to maintain quality when downscaling with +:py:attr:`~PIL.Image.BILINEAR` and :py:attr:`~PIL.Image.BICUBIC` filters +(for example, reducing within several steps), they are unnecessary now. + +Antialias renamed to Lanczos +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +A new :py:attr:`PIL.Image.LANCZOS` constant was added instead of +:py:attr:`~PIL.Image.ANTIALIAS`. + +When :py:attr:`~PIL.Image.ANTIALIAS` was initially added, it was the only +high-quality filter based on convolutions. It's name was supposed to reflect +this. Starting from Pillow 2.7.0 all resize method are based on convolutions. +All of them are antialias from now on. And the real name of the +:py:attr:`~PIL.Image.ANTIALIAS` filter is Lanczos filter. + +The :py:attr:`~PIL.Image.ANTIALIAS` constant is left for backward compatibility +and is an alias for :py:attr:`~PIL.Image.LANCZOS`. + +Lanczos upscaling quality +^^^^^^^^^^^^^^^^^^^^^^^^^ + +The image upscaling quality with :py:attr:`~PIL.Image.LANCZOS` filter was +almost the same as :py:attr:`~PIL.Image.BILINEAR` due to bug. This has been fixed. + +Bicubic upscaling quality +^^^^^^^^^^^^^^^^^^^^^^^^^ + +The :py:attr:`~PIL.Image.BICUBIC` filter for affine transformations produced +sharp, slightly pixelated image for upscaling. Bicubic for convolutions is +more soft. + +Resize performance +^^^^^^^^^^^^^^^^^^ + +In most cases, convolution is more a expensive algorithm for downscaling +because it takes into account all the pixels of source image. Therefore +:py:attr:`~PIL.Image.BILINEAR` and :py:attr:`~PIL.Image.BICUBIC` filters' +performance can be lower than before. On the other hand the quality of +:py:attr:`~PIL.Image.BILINEAR` and :py:attr:`~PIL.Image.BICUBIC` was close to +:py:attr:`~PIL.Image.NEAREST`. So if such quality is suitable for your tasks +you can switch to :py:attr:`~PIL.Image.NEAREST` filter for downscaling, +which will give a huge improvement in performance. + +At the same time performance of convolution resampling for downscaling has been +improved by around a factor of two compared to the previous version. +The upscaling performance of the :py:attr:`~PIL.Image.LANCZOS` filter has +remained the same. For :py:attr:`~PIL.Image.BILINEAR` filter it has improved by +1.5 times and for :py:attr:`~PIL.Image.BICUBIC` by four times. + +Default filter for thumbnails +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +In Pillow 2.5 the default filter for :py:meth:`~PIL.Image.Image.thumbnail` was +changed from :py:attr:`~PIL.Image.NEAREST` to :py:attr:`~PIL.Image.ANTIALIAS`. +Antialias was chosen because all the other filters gave poor quality for +reduction. Starting from Pillow 2.7.0, :py:attr:`~PIL.Image.ANTIALIAS` has been +replaced with :py:attr:`~PIL.Image.BICUBIC`, because it's faster and +:py:attr:`~PIL.Image.ANTIALIAS` doesn't give any advantages after +downscaling with libjpeg, which uses supersampling internally, not convolutions. + +Image transposition +------------------- + +A new method :py:attr:`PIL.Image.TRANSPOSE` has been added for the +:py:meth:`~PIL.Image.Image.transpose` operation in addition to +:py:attr:`~PIL.Image.FLIP_LEFT_RIGHT`, :py:attr:`~PIL.Image.FLIP_TOP_BOTTOM`, +:py:attr:`~PIL.Image.ROTATE_90`, :py:attr:`~PIL.Image.ROTATE_180`, +:py:attr:`~PIL.Image.ROTATE_270`. :py:attr:`~PIL.Image.TRANSPOSE` is an algebra +transpose, with an image reflected across its main diagonal. + +The speed of :py:attr:`~PIL.Image.ROTATE_90`, :py:attr:`~PIL.Image.ROTATE_270` +and :py:attr:`~PIL.Image.TRANSPOSE` has been significantly improved for large +images which don't fit in the processor cache. + +Gaussian blur and unsharp mask +------------------------------ + +The :py:meth:`~PIL.ImageFilter.GaussianBlur` implementation has been replaced +with a sequential application of box filters. The new implementation is based on +"Theoretical foundations of Gaussian convolution by extended box filtering" from +the Mathematical Image Analysis Group. As :py:meth:`~PIL.ImageFilter.UnsharpMask` +implementations use Gaussian blur internally, all changes from this chapter +are also applicable to it. + +Blur radius +^^^^^^^^^^^ + +There was an error in the previous version of Pillow, where blur radius (the +standard deviation of Gaussian) actually meant blur diameter. For example, to +blur an image with actual radius 5 you were forced to use value 10. This has +been fixed. Now the meaning of the radius is the same as in other software. + +If you used a Gaussian blur with some radius value, you need to divide this +value by two. + +Blur performance +^^^^^^^^^^^^^^^^ + +Box filter computation time is constant relative to the radius and depends +on source image size only. Because the new Gaussian blur implementation +is based on box filter, its computation time also doesn't depends on the blur +radius. + +For example, previously, if the execution time for a given test image was 1 +second for radius 1, 3.6 seconds for radius 10 and 17 seconds for 50, now blur +with any radius on same image is executed for 0.2 seconds. + +Blur quality +^^^^^^^^^^^^ + +The previous implementation takes into account only source pixels within +2 * standard deviation radius for every destination pixel. This was not enough, +so the quality was worse compared to other Gaussian blur software. + +The new implementation does not have this drawback. + +TIFF Parameter Changes +---------------------- + +Several kwarg parameters for saving TIFF images were previously +specified as strings with included spaces (e.g. 'x resolution'). This +was difficult to use as kwargs without constructing and passing a +dictionary. These parameters now use the underscore character instead +of space. (e.g. 'x_resolution') + diff --git a/docs/releasenotes/2.8.0.rst b/docs/releasenotes/2.8.0.rst new file mode 100644 index 000000000..85235d72a --- /dev/null +++ b/docs/releasenotes/2.8.0.rst @@ -0,0 +1,22 @@ +2.8.0 +===== + +Open HTTP response objects with Image.open +------------------------------------------ + +HTTP response objects returned from `urllib2.urlopen(url)` or `requests.get(url, stream=True).raw` are 'file-like' but do not support `.seek()` operations. As a result PIL was unable to open them as images, requiring a wrap in `cStringIO` or `BytesIO`. + +Now new functionality has been added to `Image.open()` by way of an `.seek(0)` check and catch on exception `AttributeError` or `io.UnsupportedOperation`. If this is caught we attempt to wrap the object using `io.BytesIO` (which will only work on buffer-file-like objects). + +This allows opening of files using both `urllib2` and `requests`, e.g.:: + + Image.open(urllib2.urlopen(url)) + Image.open(requests.get(url, stream=True).raw) + +If the response uses content-encoding (compression, either gzip or deflate) then this will fail as both the urllib2 and requests raw file object will produce compressed data in that case. Using Content-Encoding on images is rather non-sensical as most images are already compressed, but it can still happen. + +For requests the work-around is to set the decode_content attribute on the raw object to True:: + + response = requests.get(url, stream=True) + response.raw.decode_content = True + image = Image.open(response.raw) diff --git a/docs/releasenotes/index.rst b/docs/releasenotes/index.rst new file mode 100644 index 000000000..d70edc54d --- /dev/null +++ b/docs/releasenotes/index.rst @@ -0,0 +1,10 @@ +Release Notes +============= + +.. note:: Contributors please include release notes as needed or appropriate with your bug fixes, feature additions and tests. + +.. toctree:: + :maxdepth: 2 + + 2.7.0 + 2.8.0 diff --git a/docs/requirements.txt b/docs/requirements.txt index 7611ea463..6b104fb01 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,17 +1,2 @@ -# requirements for working on docs - -# install pillow from master if you're into that, but RtD needs this -pillow>=2.4.0 - -Jinja2==2.7.1 -MarkupSafe==0.18 -Pygments==1.6 -Sphinx==1.1.3 -docopt==0.6.1 -docutils==0.11 -wsgiref==0.1.2 -sphinx-better-theme==0.1.5 - -# livereload not strictly necessary but really useful (make livehtml) -tornado==3.1.1 -livereload==1.0.1 +Sphinx +sphinx-rtd-theme diff --git a/encode.c b/encode.c index b14942376..6bdb8c71a 100644 --- a/encode.c +++ b/encode.c @@ -99,6 +99,18 @@ _dealloc(ImagingEncoderObject* encoder) PyObject_Del(encoder); } +static PyObject* +_encode_cleanup(ImagingEncoderObject* encoder, PyObject* args) +{ + int status = 0; + + if (encoder->cleanup){ + status = encoder->cleanup(&encoder->state); + } + + return Py_BuildValue("i", status); +} + static PyObject* _encode(ImagingEncoderObject* encoder, PyObject* args) { @@ -239,6 +251,7 @@ _setimage(ImagingEncoderObject* encoder, PyObject* args) static struct PyMethodDef methods[] = { {"encode", (PyCFunction)_encode, 1}, + {"cleanup", (PyCFunction)_encode_cleanup, 1}, {"encode_to_file", (PyCFunction)_encode_to_file, 1}, {"setimage", (PyCFunction)_setimage, 1}, {NULL, NULL} /* sentinel */ @@ -522,12 +535,12 @@ PyImaging_ZipEncoderNew(PyObject* self, PyObject* args) #include "Jpeg.h" -static unsigned int** get_qtables_arrays(PyObject* qtables) { +static unsigned int* get_qtables_arrays(PyObject* qtables, int* qtablesLen) { PyObject* tables; PyObject* table; PyObject* table_data; int i, j, num_tables; - unsigned int **qarrays; + unsigned int *qarrays; if ((qtables == NULL) || (qtables == Py_None)) { return NULL; @@ -540,11 +553,13 @@ static unsigned int** get_qtables_arrays(PyObject* qtables) { tables = PySequence_Fast(qtables, "expected a sequence"); num_tables = PySequence_Size(qtables); - if (num_tables < 2 || num_tables > NUM_QUANT_TBLS) { - PyErr_SetString(PyExc_ValueError, "Not a valid numbers of quantization tables. Should be between 2 and 4."); + if (num_tables < 1 || num_tables > NUM_QUANT_TBLS) { + PyErr_SetString(PyExc_ValueError, + "Not a valid number of quantization tables. Should be between 1 and 4."); + Py_DECREF(tables); return NULL; } - qarrays = (unsigned int**) PyMem_Malloc(num_tables * sizeof(unsigned int*)); + qarrays = (unsigned int*) malloc(num_tables * DCTSIZE2 * sizeof(unsigned int)); if (!qarrays) { Py_DECREF(tables); PyErr_NoMemory(); @@ -553,38 +568,33 @@ static unsigned int** get_qtables_arrays(PyObject* qtables) { for (i = 0; i < num_tables; i++) { table = PySequence_Fast_GET_ITEM(tables, i); if (!PySequence_Check(table)) { - Py_DECREF(tables); PyErr_SetString(PyExc_ValueError, "Invalid quantization tables"); - return NULL; + goto JPEG_QTABLES_ERR; } if (PySequence_Size(table) != DCTSIZE2) { - Py_DECREF(tables); - PyErr_SetString(PyExc_ValueError, "Invalid quantization tables"); - return NULL; + PyErr_SetString(PyExc_ValueError, "Invalid quantization table size"); + goto JPEG_QTABLES_ERR; } table_data = PySequence_Fast(table, "expected a sequence"); - qarrays[i] = (unsigned int*) PyMem_Malloc(DCTSIZE2 * sizeof(unsigned int)); - if (!qarrays[i]) { - Py_DECREF(tables); - PyErr_NoMemory(); - return NULL; - } for (j = 0; j < DCTSIZE2; j++) { - qarrays[i][j] = PyInt_AS_LONG(PySequence_Fast_GET_ITEM(table_data, j)); + qarrays[i * DCTSIZE2 + j] = PyInt_AS_LONG(PySequence_Fast_GET_ITEM(table_data, j)); } + Py_DECREF(table_data); } - Py_DECREF(tables); + *qtablesLen = num_tables; +JPEG_QTABLES_ERR: + Py_DECREF(tables); // Run on both error and not error if (PyErr_Occurred()) { - PyMem_Free(qarrays); + free(qarrays); qarrays = NULL; + return NULL; } return qarrays; } - PyObject* PyImaging_JpegEncoderNew(PyObject* self, PyObject* args) { @@ -600,7 +610,8 @@ PyImaging_JpegEncoderNew(PyObject* self, PyObject* args) int xdpi = 0, ydpi = 0; int subsampling = -1; /* -1=default, 0=none, 1=medium, 2=high */ PyObject* qtables=NULL; - unsigned int **qarrays = NULL; + unsigned int *qarrays = NULL; + int qtablesLen = 0; char* extra = NULL; int extra_size; char* rawExif = NULL; @@ -620,10 +631,10 @@ PyImaging_JpegEncoderNew(PyObject* self, PyObject* args) if (get_packer(encoder, mode, rawmode) < 0) return NULL; - qarrays = get_qtables_arrays(qtables); + qarrays = get_qtables_arrays(qtables, &qtablesLen); if (extra && extra_size > 0) { - char* p = malloc(extra_size); + char* p = malloc(extra_size); // Freed in JpegEncode, Case 5 if (!p) return PyErr_NoMemory(); memcpy(p, extra, extra_size); @@ -632,7 +643,7 @@ PyImaging_JpegEncoderNew(PyObject* self, PyObject* args) extra = NULL; if (rawExif && rawExifLen > 0) { - char* pp = malloc(rawExifLen); + char* pp = malloc(rawExifLen); // Freed in JpegEncode, Case 5 if (!pp) return PyErr_NoMemory(); memcpy(pp, rawExif, rawExifLen); @@ -644,6 +655,7 @@ PyImaging_JpegEncoderNew(PyObject* self, PyObject* args) ((JPEGENCODERSTATE*)encoder->state.context)->quality = quality; ((JPEGENCODERSTATE*)encoder->state.context)->qtables = qarrays; + ((JPEGENCODERSTATE*)encoder->state.context)->qtablesLen = qtablesLen; ((JPEGENCODERSTATE*)encoder->state.context)->subsampling = subsampling; ((JPEGENCODERSTATE*)encoder->state.context)->progressive = progressive; ((JPEGENCODERSTATE*)encoder->state.context)->smooth = smooth; @@ -725,7 +737,7 @@ PyImaging_LibTiffEncoderNew(PyObject* self, PyObject* args) return NULL; } - // While failes on 64 bit machines, complains that pos is an int instead of a Py_ssize_t + // 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;postile_offset_x, &context->tile_offset_y); - j2k_decode_coord_tuple(tile_size, + j2k_decode_coord_tuple(tile_size, &context->tile_size_x, &context->tile_size_y); @@ -918,7 +930,7 @@ PyImaging_Jpeg2KEncoderNew(PyObject *self, PyObject *args) if (context->tile_offset_x > context->offset_x || context->tile_offset_y > context->offset_y) { - PyErr_SetString(PyExc_ValueError, + PyErr_SetString(PyExc_ValueError, "JPEG 2000 tile offset too large to cover image area"); Py_DECREF(encoder); return NULL; diff --git a/libImaging/Antialias.c b/libImaging/Antialias.c deleted file mode 100644 index d413fbb6a..000000000 --- a/libImaging/Antialias.c +++ /dev/null @@ -1,307 +0,0 @@ -/* - * The Python Imaging Library - * $Id$ - * - * pilopen antialiasing support - * - * history: - * 2002-03-09 fl Created (for PIL 1.1.3) - * 2002-03-10 fl Added support for mode "F" - * - * Copyright (c) 1997-2002 by Secret Labs AB - * - * See the README file for information on usage and redistribution. - */ - -#include "Imaging.h" - -#include - -/* resampling filters (from antialias.py) */ - -struct filter { - float (*filter)(float x); - float support; -}; - -static inline float sinc_filter(float x) -{ - if (x == 0.0) - return 1.0; - x = x * M_PI; - return sin(x) / x; -} - -static inline float antialias_filter(float x) -{ - /* lanczos (truncated sinc) */ - if (-3.0 <= x && x < 3.0) - return sinc_filter(x) * sinc_filter(x/3); - return 0.0; -} - -static struct filter ANTIALIAS = { antialias_filter, 3.0 }; - -static inline float nearest_filter(float x) -{ - if (-0.5 <= x && x < 0.5) - return 1.0; - return 0.0; -} - -static struct filter NEAREST = { nearest_filter, 0.5 }; - -static inline float bilinear_filter(float x) -{ - if (x < 0.0) - x = -x; - if (x < 1.0) - return 1.0-x; - return 0.0; -} - -static struct filter BILINEAR = { bilinear_filter, 1.0 }; - -static inline float bicubic_filter(float x) -{ - /* FIXME: double-check this algorithm */ - /* FIXME: for best results, "a" should be -0.5 to -1.0, but we'll - set it to zero for now, to match the 1.1 magnifying filter */ -#define a 0.0 - if (x < 0.0) - x = -x; - if (x < 1.0) - return (((a + 2.0) * x) - (a + 3.0)) * x*x + 1; - if (x < 2.0) - return (((a * x) - 5*a) * x + 8) * x - 4*a; - return 0.0; -#undef a -} - -static struct filter BICUBIC = { bicubic_filter, 2.0 }; - -Imaging -ImagingStretch(Imaging imOut, Imaging imIn, int filter) -{ - /* FIXME: this is a quick and straightforward translation from a - python prototype. might need some further C-ification... */ - - ImagingSectionCookie cookie; - struct filter *filterp; - float support, scale, filterscale; - float center, ww, ss, ymin, ymax, xmin, xmax; - int xx, yy, x, y, b; - float *k; - - /* check modes */ - if (!imOut || !imIn || strcmp(imIn->mode, imOut->mode) != 0) - return (Imaging) ImagingError_ModeError(); - - /* check filter */ - switch (filter) { - case IMAGING_TRANSFORM_NEAREST: - filterp = &NEAREST; - break; - case IMAGING_TRANSFORM_ANTIALIAS: - filterp = &ANTIALIAS; - break; - case IMAGING_TRANSFORM_BILINEAR: - filterp = &BILINEAR; - break; - case IMAGING_TRANSFORM_BICUBIC: - filterp = &BICUBIC; - break; - default: - return (Imaging) ImagingError_ValueError( - "unsupported resampling filter" - ); - } - - if (imIn->ysize == imOut->ysize) { - /* prepare for horizontal stretch */ - filterscale = scale = (float) imIn->xsize / imOut->xsize; - } else if (imIn->xsize == imOut->xsize) { - /* prepare for vertical stretch */ - filterscale = scale = (float) imIn->ysize / imOut->ysize; - } else - return (Imaging) ImagingError_Mismatch(); - - /* determine support size (length of resampling filter) */ - support = filterp->support; - - if (filterscale < 1.0) { - filterscale = 1.0; - support = 0.5; - } - - support = support * filterscale; - - /* coefficient buffer (with rounding safety margin) */ - k = malloc(((int) support * 2 + 10) * sizeof(float)); - if (!k) - return (Imaging) ImagingError_MemoryError(); - - ImagingSectionEnter(&cookie); - if (imIn->xsize == imOut->xsize) { - /* vertical stretch */ - for (yy = 0; yy < imOut->ysize; yy++) { - center = (yy + 0.5) * scale; - ww = 0.0; - ss = 1.0 / filterscale; - /* calculate filter weights */ - ymin = floor(center - support); - if (ymin < 0.0) - ymin = 0.0; - ymax = ceil(center + support); - if (ymax > (float) imIn->ysize) - ymax = (float) imIn->ysize; - for (y = (int) ymin; y < (int) ymax; y++) { - float w = filterp->filter((y - center + 0.5) * ss) * ss; - k[y - (int) ymin] = w; - ww = ww + w; - } - if (ww == 0.0) - ww = 1.0; - else - ww = 1.0 / ww; - if (imIn->image8) { - /* 8-bit grayscale */ - for (xx = 0; xx < imOut->xsize; xx++) { - ss = 0.0; - for (y = (int) ymin; y < (int) ymax; y++) - ss = ss + imIn->image8[y][xx] * k[y - (int) ymin]; - ss = ss * ww + 0.5; - if (ss < 0.5) - imOut->image8[yy][xx] = 0; - else if (ss >= 255.0) - imOut->image8[yy][xx] = 255; - else - imOut->image8[yy][xx] = (UINT8) ss; - } - } else - switch(imIn->type) { - case IMAGING_TYPE_UINT8: - /* n-bit grayscale */ - for (xx = 0; xx < imOut->xsize*4; xx++) { - /* FIXME: skip over unused pixels */ - ss = 0.0; - for (y = (int) ymin; y < (int) ymax; y++) - ss = ss + (UINT8) imIn->image[y][xx] * k[y-(int) ymin]; - ss = ss * ww + 0.5; - if (ss < 0.5) - imOut->image[yy][xx] = (UINT8) 0; - else if (ss >= 255.0) - imOut->image[yy][xx] = (UINT8) 255; - else - imOut->image[yy][xx] = (UINT8) ss; - } - break; - case IMAGING_TYPE_INT32: - /* 32-bit integer */ - for (xx = 0; xx < imOut->xsize; xx++) { - ss = 0.0; - for (y = (int) ymin; y < (int) ymax; y++) - ss = ss + IMAGING_PIXEL_I(imIn, xx, y) * k[y - (int) ymin]; - IMAGING_PIXEL_I(imOut, xx, yy) = (int) ss * ww; - } - break; - case IMAGING_TYPE_FLOAT32: - /* 32-bit float */ - for (xx = 0; xx < imOut->xsize; xx++) { - ss = 0.0; - for (y = (int) ymin; y < (int) ymax; y++) - ss = ss + IMAGING_PIXEL_F(imIn, xx, y) * k[y - (int) ymin]; - IMAGING_PIXEL_F(imOut, xx, yy) = ss * ww; - } - break; - default: - ImagingSectionLeave(&cookie); - return (Imaging) ImagingError_ModeError(); - } - } - } else { - /* horizontal stretch */ - for (xx = 0; xx < imOut->xsize; xx++) { - center = (xx + 0.5) * scale; - ww = 0.0; - ss = 1.0 / filterscale; - xmin = floor(center - support); - if (xmin < 0.0) - xmin = 0.0; - xmax = ceil(center + support); - if (xmax > (float) imIn->xsize) - xmax = (float) imIn->xsize; - for (x = (int) xmin; x < (int) xmax; x++) { - float w = filterp->filter((x - center + 0.5) * ss) * ss; - k[x - (int) xmin] = w; - ww = ww + w; - } - if (ww == 0.0) - ww = 1.0; - else - ww = 1.0 / ww; - if (imIn->image8) { - /* 8-bit grayscale */ - for (yy = 0; yy < imOut->ysize; yy++) { - ss = 0.0; - for (x = (int) xmin; x < (int) xmax; x++) - ss = ss + imIn->image8[yy][x] * k[x - (int) xmin]; - ss = ss * ww + 0.5; - if (ss < 0.5) - imOut->image8[yy][xx] = (UINT8) 0; - else if (ss >= 255.0) - imOut->image8[yy][xx] = (UINT8) 255; - else - imOut->image8[yy][xx] = (UINT8) ss; - } - } else - switch(imIn->type) { - case IMAGING_TYPE_UINT8: - /* n-bit grayscale */ - for (yy = 0; yy < imOut->ysize; yy++) { - for (b = 0; b < imIn->bands; b++) { - if (imIn->bands == 2 && b) - b = 3; /* hack to deal with LA images */ - ss = 0.0; - for (x = (int) xmin; x < (int) xmax; x++) - ss = ss + (UINT8) imIn->image[yy][x*4+b] * k[x - (int) xmin]; - ss = ss * ww + 0.5; - if (ss < 0.5) - imOut->image[yy][xx*4+b] = (UINT8) 0; - else if (ss >= 255.0) - imOut->image[yy][xx*4+b] = (UINT8) 255; - else - imOut->image[yy][xx*4+b] = (UINT8) ss; - } - } - break; - case IMAGING_TYPE_INT32: - /* 32-bit integer */ - for (yy = 0; yy < imOut->ysize; yy++) { - ss = 0.0; - for (x = (int) xmin; x < (int) xmax; x++) - ss = ss + IMAGING_PIXEL_I(imIn, x, yy) * k[x - (int) xmin]; - IMAGING_PIXEL_I(imOut, xx, yy) = (int) ss * ww; - } - break; - case IMAGING_TYPE_FLOAT32: - /* 32-bit float */ - for (yy = 0; yy < imOut->ysize; yy++) { - ss = 0.0; - for (x = (int) xmin; x < (int) xmax; x++) - ss = ss + IMAGING_PIXEL_F(imIn, x, yy) * k[x - (int) xmin]; - IMAGING_PIXEL_F(imOut, xx, yy) = ss * ww; - } - break; - default: - ImagingSectionLeave(&cookie); - return (Imaging) ImagingError_ModeError(); - } - } - } - ImagingSectionLeave(&cookie); - - free(k); - - return imOut; -} diff --git a/libImaging/BoxBlur.c b/libImaging/BoxBlur.c new file mode 100644 index 000000000..0fe3b7c35 --- /dev/null +++ b/libImaging/BoxBlur.c @@ -0,0 +1,309 @@ +#include "Python.h" +#include "Imaging.h" + + +#define MAX(x, y) (((x) > (y)) ? (x) : (y)) +#define MIN(x, y) (((x) < (y)) ? (x) : (y)) + + +typedef UINT8 pixel[4]; + +void static inline +ImagingLineBoxBlur32(pixel *lineOut, pixel *lineIn, int lastx, int radius, int edgeA, + int edgeB, UINT32 ww, UINT32 fw) +{ + int x; + UINT32 acc[4]; + UINT32 bulk[4]; + + #define MOVE_ACC(acc, subtract, add) \ + acc[0] += lineIn[add][0] - lineIn[subtract][0]; \ + acc[1] += lineIn[add][1] - lineIn[subtract][1]; \ + acc[2] += lineIn[add][2] - lineIn[subtract][2]; \ + acc[3] += lineIn[add][3] - lineIn[subtract][3]; + + #define ADD_FAR(bulk, acc, left, right) \ + bulk[0] = (acc[0] * ww) + (lineIn[left][0] + lineIn[right][0]) * fw; \ + bulk[1] = (acc[1] * ww) + (lineIn[left][1] + lineIn[right][1]) * fw; \ + bulk[2] = (acc[2] * ww) + (lineIn[left][2] + lineIn[right][2]) * fw; \ + bulk[3] = (acc[3] * ww) + (lineIn[left][3] + lineIn[right][3]) * fw; + + #define SAVE(x, bulk) \ + lineOut[x][0] = (UINT8)((bulk[0] + (1 << 23)) >> 24); \ + lineOut[x][1] = (UINT8)((bulk[1] + (1 << 23)) >> 24); \ + lineOut[x][2] = (UINT8)((bulk[2] + (1 << 23)) >> 24); \ + lineOut[x][3] = (UINT8)((bulk[3] + (1 << 23)) >> 24); + + /* Compute acc for -1 pixel (outside of image): + From "-radius-1" to "-1" get first pixel, + then from "0" to "radius-1". */ + acc[0] = lineIn[0][0] * (radius + 1); + acc[1] = lineIn[0][1] * (radius + 1); + acc[2] = lineIn[0][2] * (radius + 1); + acc[3] = lineIn[0][3] * (radius + 1); + /* As radius can be bigger than xsize, iterate to edgeA -1. */ + for (x = 0; x < edgeA - 1; x++) { + acc[0] += lineIn[x][0]; + acc[1] += lineIn[x][1]; + acc[2] += lineIn[x][2]; + acc[3] += lineIn[x][3]; + } + /* Then multiply remainder to last x. */ + acc[0] += lineIn[lastx][0] * (radius - edgeA + 1); + acc[1] += lineIn[lastx][1] * (radius - edgeA + 1); + acc[2] += lineIn[lastx][2] * (radius - edgeA + 1); + acc[3] += lineIn[lastx][3] * (radius - edgeA + 1); + + if (edgeA <= edgeB) + { + /* Subtract pixel from left ("0"). + Add pixels from radius. */ + for (x = 0; x < edgeA; x++) { + MOVE_ACC(acc, 0, x + radius); + ADD_FAR(bulk, acc, 0, x + radius + 1); + SAVE(x, bulk); + } + /* Subtract previous pixel from "-radius". + Add pixels from radius. */ + for (x = edgeA; x < edgeB; x++) { + MOVE_ACC(acc, x - radius - 1, x + radius); + ADD_FAR(bulk, acc, x - radius - 1, x + radius + 1); + SAVE(x, bulk); + } + /* Subtract previous pixel from "-radius". + Add last pixel. */ + for (x = edgeB; x <= lastx; x++) { + MOVE_ACC(acc, x - radius - 1, lastx); + ADD_FAR(bulk, acc, x - radius - 1, lastx); + SAVE(x, bulk); + } + } + else + { + for (x = 0; x < edgeB; x++) { + MOVE_ACC(acc, 0, x + radius); + ADD_FAR(bulk, acc, 0, x + radius + 1); + SAVE(x, bulk); + } + for (x = edgeB; x < edgeA; x++) { + MOVE_ACC(acc, 0, lastx); + ADD_FAR(bulk, acc, 0, lastx); + SAVE(x, bulk); + } + for (x = edgeA; x <= lastx; x++) { + MOVE_ACC(acc, x - radius - 1, lastx); + ADD_FAR(bulk, acc, x - radius - 1, lastx); + SAVE(x, bulk); + } + } + + #undef MOVE_ACC + #undef ADD_FAR + #undef SAVE +} + + +void static inline +ImagingLineBoxBlur8(UINT8 *lineOut, UINT8 *lineIn, int lastx, int radius, int edgeA, + int edgeB, UINT32 ww, UINT32 fw) +{ + int x; + UINT32 acc; + UINT32 bulk; + + #define MOVE_ACC(acc, subtract, add) \ + acc += lineIn[add] - lineIn[subtract]; + + #define ADD_FAR(bulk, acc, left, right) \ + bulk = (acc * ww) + (lineIn[left] + lineIn[right]) * fw; + + #define SAVE(x, bulk) \ + lineOut[x] = (UINT8)((bulk + (1 << 23)) >> 24) + + acc = lineIn[0] * (radius + 1); + for (x = 0; x < edgeA - 1; x++) { + acc += lineIn[x]; + } + acc += lineIn[lastx] * (radius - edgeA + 1); + + if (edgeA <= edgeB) + { + for (x = 0; x < edgeA; x++) { + MOVE_ACC(acc, 0, x + radius); + ADD_FAR(bulk, acc, 0, x + radius + 1); + SAVE(x, bulk); + } + for (x = edgeA; x < edgeB; x++) { + MOVE_ACC(acc, x - radius - 1, x + radius); + ADD_FAR(bulk, acc, x - radius - 1, x + radius + 1); + SAVE(x, bulk); + } + for (x = edgeB; x <= lastx; x++) { + MOVE_ACC(acc, x - radius - 1, lastx); + ADD_FAR(bulk, acc, x - radius - 1, lastx); + SAVE(x, bulk); + } + } + else + { + for (x = 0; x < edgeB; x++) { + MOVE_ACC(acc, 0, x + radius); + ADD_FAR(bulk, acc, 0, x + radius + 1); + SAVE(x, bulk); + } + for (x = edgeB; x < edgeA; x++) { + MOVE_ACC(acc, 0, lastx); + ADD_FAR(bulk, acc, 0, lastx); + SAVE(x, bulk); + } + for (x = edgeA; x <= lastx; x++) { + MOVE_ACC(acc, x - radius - 1, lastx); + ADD_FAR(bulk, acc, x - radius - 1, lastx); + SAVE(x, bulk); + } + } + + #undef MOVE_ACC + #undef ADD_FAR + #undef SAVE +} + + + +Imaging +ImagingHorizontalBoxBlur(Imaging imOut, Imaging imIn, float floatRadius) +{ + ImagingSectionCookie cookie; + + int y; + + int radius = (int) floatRadius; + UINT32 ww = (UINT32) (1 << 24) / (floatRadius * 2 + 1); + UINT32 fw = ((1 << 24) - (radius * 2 + 1) * ww) / 2; + + int edgeA = MIN(radius + 1, imIn->xsize); + int edgeB = MAX(imIn->xsize - radius - 1, 0); + + UINT32 *lineOut = calloc(imIn->xsize, sizeof(UINT32)); + if (lineOut == NULL) + return ImagingError_MemoryError(); + + // printf(">>> %d %d %d\n", radius, ww, fw); + + ImagingSectionEnter(&cookie); + + if (imIn->image8) + { + for (y = 0; y < imIn->ysize; y++) { + ImagingLineBoxBlur8( + (imIn == imOut ? (UINT8 *) lineOut : imOut->image8[y]), + imIn->image8[y], + imIn->xsize - 1, + radius, edgeA, edgeB, + ww, fw + ); + if (imIn == imOut) { + // Commit. + memcpy(imOut->image8[y], lineOut, imIn->xsize); + } + } + } + else + { + for (y = 0; y < imIn->ysize; y++) { + ImagingLineBoxBlur32( + imIn == imOut ? (pixel *) lineOut : (pixel *) imOut->image32[y], + (pixel *) imIn->image32[y], + imIn->xsize - 1, + radius, edgeA, edgeB, + ww, fw + ); + if (imIn == imOut) { + // Commit. + memcpy(imOut->image32[y], lineOut, imIn->xsize * 4); + } + } + } + + ImagingSectionLeave(&cookie); + + free(lineOut); + + return imOut; +} + + +Imaging +ImagingBoxBlur(Imaging imOut, Imaging imIn, float radius, int n) +{ + int i; + Imaging imTransposed; + + if (n < 1) { + return ImagingError_ValueError( + "number of passes must be greater than zero" + ); + } + + if (strcmp(imIn->mode, imOut->mode) || + imIn->type != imOut->type || + imIn->bands != imOut->bands || + imIn->xsize != imOut->xsize || + imIn->ysize != imOut->ysize) + return ImagingError_Mismatch(); + + if (imIn->type != IMAGING_TYPE_UINT8) + return ImagingError_ModeError(); + + if (!(strcmp(imIn->mode, "RGB") == 0 || + strcmp(imIn->mode, "RGBA") == 0 || + strcmp(imIn->mode, "RGBX") == 0 || + strcmp(imIn->mode, "CMYK") == 0 || + strcmp(imIn->mode, "L") == 0 || + strcmp(imIn->mode, "LA") == 0)) + return ImagingError_ModeError(); + + imTransposed = ImagingNew(imIn->mode, imIn->ysize, imIn->xsize); + if (!imTransposed) + return NULL; + + /* Apply blur in one dimension. + Use imOut as a destination at first pass, + then use imOut as a source too. */ + ImagingHorizontalBoxBlur(imOut, imIn, radius); + for (i = 1; i < n; i ++) { + ImagingHorizontalBoxBlur(imOut, imOut, radius); + } + /* Transpose result for blur in another direction. */ + ImagingTranspose(imTransposed, imOut); + + /* Reuse imTransposed as a source and destination there. */ + for (i = 0; i < n; i ++) { + ImagingHorizontalBoxBlur(imTransposed, imTransposed, radius); + } + /* Restore original orientation. */ + ImagingTranspose(imOut, imTransposed); + + ImagingDelete(imTransposed); + + return imOut; +} + + +Imaging ImagingGaussianBlur(Imaging imOut, Imaging imIn, float radius, + int passes) +{ + float sigma2, L, l, a; + + sigma2 = radius * radius / passes; + // from http://www.mia.uni-saarland.de/Publications/gwosdek-ssvm11.pdf + // [7] Box length. + L = sqrt(12.0 * sigma2 + 1.0); + // [11] Integer part of box radius. + l = floor((L - 1.0) / 2.0); + // [14], [Fig. 2] Fractional part of box radius. + a = (2 * l + 1) * (l * (l + 1) - 3 * sigma2); + a /= 6 * (sigma2 - (l + 1) * (l + 1)); + + return ImagingBoxBlur(imOut, imIn, l + a, passes); +} diff --git a/libImaging/Dib.c b/libImaging/Dib.c index 8e138bd6b..55de7d9a5 100644 --- a/libImaging/Dib.c +++ b/libImaging/Dib.c @@ -204,19 +204,6 @@ ImagingNewDIB(const char *mode, int xsize, int ysize) #endif -#if 0 - { - /* DEBUG: dump palette to file */ - FILE *err = fopen("dib.pal", "w"); - for (i = 0; i < 256; i++) - fprintf(err, "%d: %d/%d/%d\n", i, - pal->palPalEntry[i].peRed, - pal->palPalEntry[i].peGreen, - pal->palPalEntry[i].peBlue); - fclose(err); - } -#endif - dib->palette = CreatePalette(pal); } diff --git a/libImaging/Draw.c b/libImaging/Draw.c index 2ff03e049..c21f7cd83 100644 --- a/libImaging/Draw.c +++ b/libImaging/Draw.c @@ -600,7 +600,6 @@ ImagingDrawWideLine(Imaging im, int x0, int y0, int x1, int y1, double big_hypotenuse, small_hypotenuse, ratio_max, ratio_min; int dxmin, dxmax, dymin, dymax; Edge e[4]; - int vertices[4][2]; DRAWINIT(); diff --git a/libImaging/Effects.c b/libImaging/Effects.c index db6e72989..a3f5e7cd9 100644 --- a/libImaging/Effects.c +++ b/libImaging/Effects.c @@ -48,25 +48,25 @@ ImagingEffectMandelbrot(int xsize, int ysize, double extent[4], int quality) for (y = 0; y < ysize; y++) { UINT8* buf = im->image8[y]; - for (x = 0; x < xsize; x++) { - x1 = y1 = xi2 = yi2 = 0.0; - cr = x*dr + extent[0]; - ci = y*di + extent[1]; - for (k = 1;; k++) { - y1 = 2*x1*y1 + ci; - x1 = xi2 - yi2 + cr; - xi2 = x1*x1; - yi2 = y1*y1; - if ((xi2 + yi2) > radius) { - buf[x] = k*255/quality; - break; - } - if (k > quality) { - buf[x] = 0; - break; - } - } - } + for (x = 0; x < xsize; x++) { + x1 = y1 = xi2 = yi2 = 0.0; + cr = x*dr + extent[0]; + ci = y*di + extent[1]; + for (k = 1;; k++) { + y1 = 2*x1*y1 + ci; + x1 = xi2 - yi2 + cr; + xi2 = x1*x1; + yi2 = y1*y1; + if ((xi2 + yi2) > radius) { + buf[x] = k*255/quality; + break; + } + if (k > quality) { + buf[x] = 0; + break; + } + } + } } return im; } @@ -74,7 +74,7 @@ ImagingEffectMandelbrot(int xsize, int ysize, double extent[4], int quality) Imaging ImagingEffectNoise(int xsize, int ysize, float sigma) { - /* Generate gaussian noise centered around 128 */ + /* Generate Gaussian noise centered around 128 */ Imaging imOut; int x, y; @@ -83,23 +83,23 @@ ImagingEffectNoise(int xsize, int ysize, float sigma) imOut = ImagingNew("L", xsize, ysize); if (!imOut) - return NULL; + return NULL; next = 0.0; nextok = 0; for (y = 0; y < imOut->ysize; y++) { UINT8* out = imOut->image8[y]; - for (x = 0; x < imOut->xsize; x++) { + for (x = 0; x < imOut->xsize; x++) { if (nextok) { this = next; nextok = 0; } else { - /* after numerical recepies */ + /* after numerical recipes */ double v1, v2, radius, factor; do { - v1 = rand()*(2.0/32767.0) - 1.0; - v2 = rand()*(2.0/32767.0) - 1.0; + v1 = rand()*(2.0/RAND_MAX) - 1.0; + v2 = rand()*(2.0/RAND_MAX) - 1.0; radius= v1*v1 + v2*v2; } while (radius >= 1.0); factor = sqrt(-2.0*log(radius)/radius); @@ -113,14 +113,6 @@ ImagingEffectNoise(int xsize, int ysize, float sigma) return imOut; } -Imaging -ImagingEffectPerlinTurbulence(int xsize, int ysize) -{ - /* Perlin turbulence (In progress) */ - - return NULL; -} - Imaging ImagingEffectSpread(Imaging imIn, int distance) { @@ -132,11 +124,11 @@ ImagingEffectSpread(Imaging imIn, int distance) imOut = ImagingNew(imIn->mode, imIn->xsize, imIn->ysize); if (!imOut) - return NULL; + return NULL; -#define SPREAD(type, image)\ +#define SPREAD(type, image)\ for (y = 0; y < imIn->ysize; y++)\ - for (x = 0; x < imIn->xsize; x++) {\ + for (x = 0; x < imIn->xsize; x++) {\ int xx = x + (rand() % distance) - distance/2;\ int yy = y + (rand() % distance) - distance/2;\ if (xx >= 0 && xx < imIn->xsize && yy >= 0 && yy < imIn->ysize) {\ @@ -147,9 +139,9 @@ ImagingEffectSpread(Imaging imIn, int distance) } if (imIn->image8) { - SPREAD(UINT8, image8); + SPREAD(UINT8, image8); } else { - SPREAD(INT32, image32); + SPREAD(INT32, image32); } ImagingCopyInfo(imOut, imIn); @@ -157,217 +149,4 @@ ImagingEffectSpread(Imaging imIn, int distance) return imOut; } -/* -------------------------------------------------------------------- */ -/* Taken from the "C" code in the W3C SVG specification. Translated - to C89 by Fredrik Lundh */ - -#if 0 - -/* Produces results in the range [1, 2**31 - 2]. -Algorithm is: r = (a * r) mod m -where a = 16807 and m = 2**31 - 1 = 2147483647 -See [Park & Miller], CACM vol. 31 no. 10 p. 1195, Oct. 1988 -To test: the algorithm should produce the result 1043618065 -as the 10,000th generated number if the original seed is 1. -*/ -#define RAND_m 2147483647 /* 2**31 - 1 */ -#define RAND_a 16807 /* 7**5; primitive root of m */ -#define RAND_q 127773 /* m / a */ -#define RAND_r 2836 /* m % a */ - -static long -perlin_setup_seed(long lSeed) -{ - if (lSeed <= 0) lSeed = -(lSeed % (RAND_m - 1)) + 1; - if (lSeed > RAND_m - 1) lSeed = RAND_m - 1; - return lSeed; -} - -static long -perlin_random(long lSeed) -{ - long result; - result = RAND_a * (lSeed % RAND_q) - RAND_r * (lSeed / RAND_q); - if (result <= 0) result += RAND_m; - return result; -} - -#define BSize 0x100 -#define BM 0xff -#define PerlinN 0x1000 -#define NP 12 /* 2^PerlinN */ -#define NM 0xfff -static int perlin_uLatticeSelector[BSize + BSize + 2]; -static double perlin_fGradient[4][BSize + BSize + 2][2]; -typedef struct -{ - int nWidth; /* How much to subtract to wrap for stitching. */ - int nHeight; - int nWrapX; /* Minimum value to wrap. */ - int nWrapY; -} StitchInfo; - -static void -perlin_init(long lSeed) -{ - double s; - int i, j, k; - lSeed = perlin_setup_seed(lSeed); - for(k = 0; k < 4; k++) - { - for(i = 0; i < BSize; i++) - { - perlin_uLatticeSelector[i] = i; - for (j = 0; j < 2; j++) - perlin_fGradient[k][i][j] = (double)(((lSeed = perlin_random(lSeed)) % (BSize + BSize)) - BSize) / BSize; - s = (double) (sqrt(perlin_fGradient[k][i][0] * perlin_fGradient[k][i][0] + perlin_fGradient[k][i][1] * perlin_fGradient[k][i][1])); - perlin_fGradient[k][i][0] /= s; - perlin_fGradient[k][i][1] /= s; - } - } - while(--i) - { - k = perlin_uLatticeSelector[i]; - perlin_uLatticeSelector[i] = perlin_uLatticeSelector[j = (lSeed = perlin_random(lSeed)) % BSize]; - perlin_uLatticeSelector[j] = k; - } - for(i = 0; i < BSize + 2; i++) - { - perlin_uLatticeSelector[BSize + i] = perlin_uLatticeSelector[i]; - for(k = 0; k < 4; k++) - for(j = 0; j < 2; j++) - perlin_fGradient[k][BSize + i][j] = perlin_fGradient[k][i][j]; - } -} - -#define s_curve(t) ( t * t * (3. - 2. * t) ) -#define lerp(t, a, b) ( a + t * (b - a) ) -static double -perlin_noise2(int nColorChannel, double vec[2], StitchInfo *pStitchInfo) -{ - int bx0, bx1, by0, by1, b00, b10, b01, b11; - double rx0, rx1, ry0, ry1, *q, sx, sy, a, b, t, u, v; - register int i, j; - - t = vec[0] + (double) PerlinN; - bx0 = (int)t; - bx1 = bx0+1; - rx0 = t - (int)t; - rx1 = rx0 - 1.0f; - t = vec[1] + (double) PerlinN; - by0 = (int)t; - by1 = by0+1; - ry0 = t - (int)t; - ry1 = ry0 - 1.0f; - - /* If stitching, adjust lattice points accordingly. */ - if(pStitchInfo != NULL) - { - if(bx0 >= pStitchInfo->nWrapX) - bx0 -= pStitchInfo->nWidth; - if(bx1 >= pStitchInfo->nWrapX) - bx1 -= pStitchInfo->nWidth; - if(by0 >= pStitchInfo->nWrapY) - by0 -= pStitchInfo->nHeight; - if(by1 >= pStitchInfo->nWrapY) - by1 -= pStitchInfo->nHeight; - } - - bx0 &= BM; - bx1 &= BM; - by0 &= BM; - by1 &= BM; - - i = perlin_uLatticeSelector[bx0]; - j = perlin_uLatticeSelector[bx1]; - b00 = perlin_uLatticeSelector[i + by0]; - b10 = perlin_uLatticeSelector[j + by0]; - b01 = perlin_uLatticeSelector[i + by1]; - b11 = perlin_uLatticeSelector[j + by1]; - sx = (double) (s_curve(rx0)); - sy = (double) (s_curve(ry0)); - q = perlin_fGradient[nColorChannel][b00]; u = rx0 * q[0] + ry0 * q[1]; - q = perlin_fGradient[nColorChannel][b10]; v = rx1 * q[0] + ry0 * q[1]; - a = lerp(sx, u, v); - q = perlin_fGradient[nColorChannel][b01]; u = rx0 * q[0] + ry1 * q[1]; - q = perlin_fGradient[nColorChannel][b11]; v = rx1 * q[0] + ry1 * q[1]; - b = lerp(sx, u, v); - return lerp(sy, a, b); -} - -double -perlin_turbulence( - int nColorChannel, double *point, double fBaseFreqX, double fBaseFreqY, - int nNumOctaves, int bFractalSum, int bDoStitching, - double fTileX, double fTileY, double fTileWidth, double fTileHeight) -{ - StitchInfo stitch; - StitchInfo *pStitchInfo = NULL; /* Not stitching when NULL. */ - - double fSum = 0.0f; - double vec[2]; - double ratio = 1; - - int nOctave; - - vec[0] = point[0] * fBaseFreqX; - vec[1] = point[1] * fBaseFreqY; - - /* Adjust the base frequencies if necessary for stitching. */ - if(bDoStitching) - { - /* When stitching tiled turbulence, the frequencies must be adjusted */ - /* so that the tile borders will be continuous. */ - if(fBaseFreqX != 0.0) - { - double fLoFreq = (double) (floor(fTileWidth * fBaseFreqX)) / fTileWidth; - double fHiFreq = (double) (ceil(fTileWidth * fBaseFreqX)) / fTileWidth; - if(fBaseFreqX / fLoFreq < fHiFreq / fBaseFreqX) - fBaseFreqX = fLoFreq; - else - fBaseFreqX = fHiFreq; - } - - if(fBaseFreqY != 0.0) - { - double fLoFreq = (double) (floor(fTileHeight * fBaseFreqY)) / fTileHeight; - double fHiFreq = (double) (ceil(fTileHeight * fBaseFreqY)) / fTileHeight; - if(fBaseFreqY / fLoFreq < fHiFreq / fBaseFreqY) - fBaseFreqY = fLoFreq; - else - fBaseFreqY = fHiFreq; - } - - /* Set up initial stitch values. */ - pStitchInfo = &stitch; - stitch.nWidth = (int) (fTileWidth * fBaseFreqX + 0.5f); - stitch.nWrapX = (int) (fTileX * fBaseFreqX + PerlinN + stitch.nWidth); - stitch.nHeight = (int) (fTileHeight * fBaseFreqY + 0.5f); - stitch.nWrapY = (int) (fTileY * fBaseFreqY + PerlinN + stitch.nHeight); - } - - for(nOctave = 0; nOctave < nNumOctaves; nOctave++) - { - if(bFractalSum) - fSum += (double) (perlin_noise2(nColorChannel, vec, pStitchInfo) / ratio); - else - fSum += (double) (fabs(perlin_noise2(nColorChannel, vec, pStitchInfo)) / ratio); - - vec[0] *= 2; - vec[1] *= 2; - ratio *= 2; - - if(pStitchInfo != NULL) - { - /* Update stitch values. Subtracting PerlinN before the multiplication and */ - /* adding it afterward simplifies to subtracting it once. */ - stitch.nWidth *= 2; - stitch.nWrapX = 2 * stitch.nWrapX - PerlinN; - stitch.nHeight *= 2; - stitch.nWrapY = 2 * stitch.nWrapY - PerlinN; - } - } - return fSum; -} - -#endif +// End of file diff --git a/libImaging/Geometry.c b/libImaging/Geometry.c index 0f59ee0d5..97cbcf2ed 100644 --- a/libImaging/Geometry.c +++ b/libImaging/Geometry.c @@ -30,11 +30,18 @@ /* Undef if you don't need resampling filters */ #define WITH_FILTERS +/* For large images rotation is an inefficient operation in terms of CPU cache. + One row in the source image affects each column in destination. + Rotating in chunks that fit in the cache can speed up rotation + 8x on a modern CPU. A chunk size of 128 requires only 65k and is large enough + that the overhead from the extra loops are not apparent. */ +#define ROTATE_CHUNK 128 + #define COORD(v) ((v) < 0.0 ? -1 : ((int)(v))) #define FLOOR(v) ((v) < 0.0 ? ((int)floor(v)) : ((int)(v))) /* -------------------------------------------------------------------- */ -/* Transpose operations */ +/* Transpose operations */ Imaging ImagingFlipLeftRight(Imaging imOut, Imaging imIn) @@ -43,25 +50,25 @@ ImagingFlipLeftRight(Imaging imOut, Imaging imIn) int x, y, xr; if (!imOut || !imIn || strcmp(imIn->mode, imOut->mode) != 0) - return (Imaging) ImagingError_ModeError(); + return (Imaging) ImagingError_ModeError(); if (imIn->xsize != imOut->xsize || imIn->ysize != imOut->ysize) - return (Imaging) ImagingError_Mismatch(); + return (Imaging) ImagingError_Mismatch(); ImagingCopyInfo(imOut, imIn); -#define FLIP_HORIZ(image)\ +#define FLIP_HORIZ(image)\ for (y = 0; y < imIn->ysize; y++) {\ - xr = imIn->xsize-1;\ - for (x = 0; x < imIn->xsize; x++, xr--)\ - imOut->image[y][x] = imIn->image[y][xr];\ + xr = imIn->xsize-1;\ + for (x = 0; x < imIn->xsize; x++, xr--)\ + imOut->image[y][x] = imIn->image[y][xr];\ } ImagingSectionEnter(&cookie); if (imIn->image8) - FLIP_HORIZ(image8) + FLIP_HORIZ(image8) else - FLIP_HORIZ(image32) + FLIP_HORIZ(image32) ImagingSectionLeave(&cookie); @@ -76,9 +83,9 @@ ImagingFlipTopBottom(Imaging imOut, Imaging imIn) int y, yr; if (!imOut || !imIn || strcmp(imIn->mode, imOut->mode) != 0) - return (Imaging) ImagingError_ModeError(); + return (Imaging) ImagingError_ModeError(); if (imIn->xsize != imOut->xsize || imIn->ysize != imOut->ysize) - return (Imaging) ImagingError_Mismatch(); + return (Imaging) ImagingError_Mismatch(); ImagingCopyInfo(imOut, imIn); @@ -86,7 +93,7 @@ ImagingFlipTopBottom(Imaging imOut, Imaging imIn) yr = imIn->ysize-1; for (y = 0; y < imIn->ysize; y++, yr--) - memcpy(imOut->image[yr], imIn->image[y], imIn->linesize); + memcpy(imOut->image[yr], imIn->image[y], imIn->linesize); ImagingSectionLeave(&cookie); @@ -98,28 +105,35 @@ Imaging ImagingRotate90(Imaging imOut, Imaging imIn) { ImagingSectionCookie cookie; - int x, y, xr; + int x, y, xx, yy, xr, xxsize, yysize; if (!imOut || !imIn || strcmp(imIn->mode, imOut->mode) != 0) - return (Imaging) ImagingError_ModeError(); + return (Imaging) ImagingError_ModeError(); if (imIn->xsize != imOut->ysize || imIn->ysize != imOut->xsize) - return (Imaging) ImagingError_Mismatch(); + return (Imaging) ImagingError_Mismatch(); ImagingCopyInfo(imOut, imIn); -#define ROTATE_90(image)\ - for (y = 0; y < imIn->ysize; y++) {\ - xr = imIn->xsize-1;\ - for (x = 0; x < imIn->xsize; x++, xr--)\ - imOut->image[xr][y] = imIn->image[y][x];\ +#define ROTATE_90(image) \ + for (y = 0; y < imIn->ysize; y += ROTATE_CHUNK) { \ + for (x = 0; x < imIn->xsize; x += ROTATE_CHUNK) { \ + yysize = y + ROTATE_CHUNK < imIn->ysize ? y + ROTATE_CHUNK : imIn->ysize; \ + xxsize = x + ROTATE_CHUNK < imIn->xsize ? x + ROTATE_CHUNK : imIn->xsize; \ + for (yy = y; yy < yysize; yy++) { \ + xr = imIn->xsize - 1 - x; \ + for (xx = x; xx < xxsize; xx++, xr--) { \ + imOut->image[xr][yy] = imIn->image[yy][xx]; \ + } \ + } \ + } \ } ImagingSectionEnter(&cookie); if (imIn->image8) - ROTATE_90(image8) + ROTATE_90(image8) else - ROTATE_90(image32) + ROTATE_90(image32) ImagingSectionLeave(&cookie); @@ -127,6 +141,58 @@ ImagingRotate90(Imaging imOut, Imaging imIn) } +Imaging +ImagingTranspose(Imaging imOut, Imaging imIn) +{ + ImagingSectionCookie cookie; + int x, y, xx, yy, xxsize, yysize; + + if (!imOut || !imIn || strcmp(imIn->mode, imOut->mode) != 0) + return (Imaging) ImagingError_ModeError(); + if (imIn->xsize != imOut->ysize || imIn->ysize != imOut->xsize) + return (Imaging) ImagingError_Mismatch(); + +#define TRANSPOSE(image) \ + for (y = 0; y < imIn->ysize; y += ROTATE_CHUNK) { \ + for (x = 0; x < imIn->xsize; x += ROTATE_CHUNK) { \ + yysize = y + ROTATE_CHUNK < imIn->ysize ? y + ROTATE_CHUNK : imIn->ysize; \ + xxsize = x + ROTATE_CHUNK < imIn->xsize ? x + ROTATE_CHUNK : imIn->xsize; \ + for (yy = y; yy < yysize; yy++) { \ + for (xx = x; xx < xxsize; xx++) { \ + imOut->image[xx][yy] = imIn->image[yy][xx]; \ + } \ + } \ + } \ + } + + ImagingCopyInfo(imOut, imIn); + + ImagingSectionEnter(&cookie); + if (imIn->image8) + TRANSPOSE(image8) + else + TRANSPOSE(image32) + ImagingSectionLeave(&cookie); + + return imOut; +} + + +Imaging +ImagingTransposeToNew(Imaging imIn) +{ + Imaging imTemp = ImagingNew(imIn->mode, imIn->ysize, imIn->xsize); + if ( ! imTemp) + return NULL; + + if ( ! ImagingTranspose(imTemp, imIn)) { + ImagingDelete(imTemp); + return NULL; + } + return imTemp; +} + + Imaging ImagingRotate180(Imaging imOut, Imaging imIn) { @@ -134,27 +200,27 @@ ImagingRotate180(Imaging imOut, Imaging imIn) int x, y, xr, yr; if (!imOut || !imIn || strcmp(imIn->mode, imOut->mode) != 0) - return (Imaging) ImagingError_ModeError(); + return (Imaging) ImagingError_ModeError(); if (imIn->xsize != imOut->xsize || imIn->ysize != imOut->ysize) - return (Imaging) ImagingError_Mismatch(); + return (Imaging) ImagingError_Mismatch(); ImagingCopyInfo(imOut, imIn); yr = imIn->ysize-1; -#define ROTATE_180(image)\ +#define ROTATE_180(image)\ for (y = 0; y < imIn->ysize; y++, yr--) {\ - xr = imIn->xsize-1;\ - for (x = 0; x < imIn->xsize; x++, xr--)\ - imOut->image[y][x] = imIn->image[yr][xr];\ + xr = imIn->xsize-1;\ + for (x = 0; x < imIn->xsize; x++, xr--)\ + imOut->image[y][x] = imIn->image[yr][xr];\ } ImagingSectionEnter(&cookie); if (imIn->image8) - ROTATE_180(image8) + ROTATE_180(image8) else - ROTATE_180(image32) + ROTATE_180(image32) ImagingSectionLeave(&cookie); @@ -166,28 +232,35 @@ Imaging ImagingRotate270(Imaging imOut, Imaging imIn) { ImagingSectionCookie cookie; - int x, y, yr; + int x, y, xx, yy, yr, xxsize, yysize; if (!imOut || !imIn || strcmp(imIn->mode, imOut->mode) != 0) - return (Imaging) ImagingError_ModeError(); + return (Imaging) ImagingError_ModeError(); if (imIn->xsize != imOut->ysize || imIn->ysize != imOut->xsize) - return (Imaging) ImagingError_Mismatch(); + return (Imaging) ImagingError_Mismatch(); ImagingCopyInfo(imOut, imIn); - yr = imIn->ysize - 1; - -#define ROTATE_270(image)\ - for (y = 0; y < imIn->ysize; y++, yr--)\ - for (x = 0; x < imIn->xsize; x++)\ - imOut->image[x][y] = imIn->image[yr][x]; +#define ROTATE_270(image) \ + for (y = 0; y < imIn->ysize; y += ROTATE_CHUNK) { \ + for (x = 0; x < imIn->xsize; x += ROTATE_CHUNK) { \ + yysize = y + ROTATE_CHUNK < imIn->ysize ? y + ROTATE_CHUNK : imIn->ysize; \ + xxsize = x + ROTATE_CHUNK < imIn->xsize ? x + ROTATE_CHUNK : imIn->xsize; \ + yr = imIn->ysize - 1 - y; \ + for (yy = y; yy < yysize; yy++, yr--) { \ + for (xx = x; xx < xxsize; xx++) { \ + imOut->image[xx][yr] = imIn->image[yy][xx]; \ + } \ + } \ + } \ + } ImagingSectionEnter(&cookie); if (imIn->image8) - ROTATE_270(image8) + ROTATE_270(image8) else - ROTATE_270(image32) + ROTATE_270(image32) ImagingSectionLeave(&cookie); @@ -196,7 +269,7 @@ ImagingRotate270(Imaging imOut, Imaging imIn) /* -------------------------------------------------------------------- */ -/* Transforms */ +/* Transforms */ /* transform primitives (ImagingTransformMap) */ @@ -231,23 +304,6 @@ perspective_transform(double* xin, double* yin, int x, int y, void* data) return 1; } -#if 0 -static int -quadratic_transform(double* xin, double* yin, int x, int y, void* data) -{ - double* a = (double*) data; - - double a0 = a[0]; double a1 = a[1]; double a2 = a[2]; double a3 = a[3]; - double a4 = a[4]; double a5 = a[5]; double a6 = a[6]; double a7 = a[7]; - double a8 = a[8]; double a9 = a[9]; double a10 = a[10]; double a11 = a[11]; - - xin[0] = a0 + a1*x + a2*y + a3*x*x + a4*x*y + a5*y*y; - yin[0] = a6 + a7*x + a8*y + a9*x*x + a10*x*y + a11*y*y; - - return 1; -} -#endif - static int quad_transform(double* xin, double* yin, int x, int y, void* data) { @@ -597,7 +653,7 @@ ImagingTransform( double xx, yy; if (!imOut || !imIn || strcmp(imIn->mode, imOut->mode) != 0) - return (Imaging) ImagingError_ModeError(); + return (Imaging) ImagingError_ModeError(); ImagingCopyInfo(imOut, imIn); @@ -613,15 +669,15 @@ ImagingTransform( y1 = imOut->ysize; for (y = y0; y < y1; y++) { - out = imOut->image[y] + x0*imOut->pixelsize; - for (x = x0; x < x1; x++) { - if (!transform(&xx, &yy, x-x0, y-y0, transform_data) || + out = imOut->image[y] + x0*imOut->pixelsize; + for (x = x0; x < x1; x++) { + if (!transform(&xx, &yy, x-x0, y-y0, transform_data) || !filter(out, imIn, xx, yy, filter_data)) { if (fill) memset(out, 0, imOut->pixelsize); } out += imOut->pixelsize; - } + } } ImagingSectionLeave(&cookie); @@ -644,7 +700,7 @@ ImagingScaleAffine(Imaging imOut, Imaging imIn, int *xintab; if (!imOut || !imIn || strcmp(imIn->mode, imOut->mode) != 0) - return (Imaging) ImagingError_ModeError(); + return (Imaging) ImagingError_ModeError(); ImagingCopyInfo(imOut, imIn); @@ -659,8 +715,8 @@ ImagingScaleAffine(Imaging imOut, Imaging imIn, xintab = (int*) malloc(imOut->xsize * sizeof(int)); if (!xintab) { - ImagingDelete(imOut); - return (Imaging) ImagingError_MemoryError(); + ImagingDelete(imOut); + return (Imaging) ImagingError_MemoryError(); } xo = a[0]; @@ -671,29 +727,29 @@ ImagingScaleAffine(Imaging imOut, Imaging imIn, /* Pretabulate horizontal pixel positions */ for (x = x0; x < x1; x++) { - xin = COORD(xo); - if (xin >= 0 && xin < (int) imIn->xsize) { - xmax = x+1; - if (x < xmin) - xmin = x; - xintab[x] = xin; - } - xo += a[1]; + xin = COORD(xo); + if (xin >= 0 && xin < (int) imIn->xsize) { + xmax = x+1; + if (x < xmin) + xmin = x; + xintab[x] = xin; + } + xo += a[1]; } -#define AFFINE_SCALE(pixel, image)\ +#define AFFINE_SCALE(pixel, image)\ for (y = y0; y < y1; y++) {\ - int yi = COORD(yo);\ - pixel *in, *out;\ - out = imOut->image[y];\ + int yi = COORD(yo);\ + pixel *in, *out;\ + out = imOut->image[y];\ if (fill && x1 > x0)\ memset(out+x0, 0, (x1-x0)*sizeof(pixel));\ - if (yi >= 0 && yi < imIn->ysize) {\ - in = imIn->image[yi];\ - for (x = xmin; x < xmax; x++)\ - out[x] = in[xintab[x]];\ - }\ - yo += a[5];\ + if (yi >= 0 && yi < imIn->ysize) {\ + in = imIn->image[yi];\ + for (x = xmin; x < xmax; x++)\ + out[x] = in[xintab[x]];\ + }\ + yo += a[5];\ } ImagingSectionEnter(&cookie); @@ -743,32 +799,32 @@ affine_fixed(Imaging imOut, Imaging imIn, a0 = FIX(a[0]); a1 = FIX(a[1]); a2 = FIX(a[2]); a3 = FIX(a[3]); a4 = FIX(a[4]); a5 = FIX(a[5]); -#define AFFINE_TRANSFORM_FIXED(pixel, image)\ +#define AFFINE_TRANSFORM_FIXED(pixel, image)\ for (y = y0; y < y1; y++) {\ - pixel *out;\ - xx = a0;\ - yy = a3;\ - out = imOut->image[y];\ + pixel *out;\ + xx = a0;\ + yy = a3;\ + out = imOut->image[y];\ if (fill && x1 > x0)\ memset(out+x0, 0, (x1-x0)*sizeof(pixel));\ for (x = x0; x < x1; x++, out++) {\ - xin = xx >> 16;\ - if (xin >= 0 && xin < xsize) {\ - yin = yy >> 16;\ - if (yin >= 0 && yin < ysize)\ + xin = xx >> 16;\ + if (xin >= 0 && xin < xsize) {\ + yin = yy >> 16;\ + if (yin >= 0 && yin < ysize)\ *out = imIn->image[yin][xin];\ }\ - xx += a1;\ - yy += a4;\ - }\ - a0 += a2;\ - a3 += a5;\ + xx += a1;\ + yy += a4;\ + }\ + a0 += a2;\ + a3 += a5;\ } if (imIn->image8) - AFFINE_TRANSFORM_FIXED(UINT8, image8) + AFFINE_TRANSFORM_FIXED(UINT8, image8) else - AFFINE_TRANSFORM_FIXED(INT32, image32) + AFFINE_TRANSFORM_FIXED(INT32, image32) return imOut; } @@ -801,11 +857,11 @@ ImagingTransformAffine(Imaging imOut, Imaging imIn, } if (a[2] == 0 && a[4] == 0) - /* Scaling */ - return ImagingScaleAffine(imOut, imIn, x0, y0, x1, y1, a, fill); + /* Scaling */ + return ImagingScaleAffine(imOut, imIn, x0, y0, x1, y1, a, fill); if (!imOut || !imIn || strcmp(imIn->mode, imOut->mode) != 0) - return (Imaging) ImagingError_ModeError(); + return (Imaging) ImagingError_ModeError(); if (x0 < 0) x0 = 0; @@ -835,34 +891,34 @@ ImagingTransformAffine(Imaging imOut, Imaging imIn, xo = a[0]; yo = a[3]; -#define AFFINE_TRANSFORM(pixel, image)\ +#define AFFINE_TRANSFORM(pixel, image)\ for (y = y0; y < y1; y++) {\ - pixel *out;\ - xx = xo;\ - yy = yo;\ - out = imOut->image[y];\ + pixel *out;\ + xx = xo;\ + yy = yo;\ + out = imOut->image[y];\ if (fill && x1 > x0)\ memset(out+x0, 0, (x1-x0)*sizeof(pixel));\ for (x = x0; x < x1; x++, out++) {\ - xin = COORD(xx);\ - if (xin >= 0 && xin < xsize) {\ - yin = COORD(yy);\ - if (yin >= 0 && yin < ysize)\ + xin = COORD(xx);\ + if (xin >= 0 && xin < xsize) {\ + yin = COORD(yy);\ + if (yin >= 0 && yin < ysize)\ *out = imIn->image[yin][xin];\ }\ - xx += a[1];\ - yy += a[4];\ - }\ - xo += a[2];\ - yo += a[5];\ + xx += a[1];\ + yy += a[4];\ + }\ + xo += a[2];\ + yo += a[5];\ } ImagingSectionEnter(&cookie); if (imIn->image8) - AFFINE_TRANSFORM(UINT8, image8) + AFFINE_TRANSFORM(UINT8, image8) else - AFFINE_TRANSFORM(INT32, image32) + AFFINE_TRANSFORM(INT32, image32) ImagingSectionLeave(&cookie); @@ -906,30 +962,6 @@ ImagingTransformQuad(Imaging imOut, Imaging imIn, /* -------------------------------------------------------------------- */ /* Convenience functions */ -Imaging -ImagingResize(Imaging imOut, Imaging imIn, int filterid) -{ - double a[6]; - - if (imOut->xsize == imIn->xsize && imOut->ysize == imIn->ysize) - return ImagingCopy2(imOut, imIn); - - memset(a, 0, sizeof a); - a[1] = (double) imIn->xsize / imOut->xsize; - a[5] = (double) imIn->ysize / imOut->ysize; - - if (!filterid && imIn->type != IMAGING_TYPE_SPECIAL) - return ImagingScaleAffine( - imOut, imIn, - 0, 0, imOut->xsize, imOut->ysize, - a, 1); - - return ImagingTransformAffine( - imOut, imIn, - 0, 0, imOut->xsize, imOut->ysize, - a, filterid, 1); -} - Imaging ImagingRotate(Imaging imOut, Imaging imIn, double theta, int filterid) { diff --git a/libImaging/ImPlatform.h b/libImaging/ImPlatform.h index 70ee63119..43c17cc85 100644 --- a/libImaging/ImPlatform.h +++ b/libImaging/ImPlatform.h @@ -72,3 +72,9 @@ #ifdef _MSC_VER typedef signed __int64 int64_t; #endif + +#ifdef __GNUC__ + #define GCC_VERSION (__GNUC__ * 10000 \ + + __GNUC_MINOR__ * 100 \ + + __GNUC_PATCHLEVEL__) +#endif diff --git a/libImaging/Imaging.h b/libImaging/Imaging.h index d958387c9..c341ac84e 100644 --- a/libImaging/Imaging.h +++ b/libImaging/Imaging.h @@ -229,7 +229,7 @@ extern void ImagingError_Clear(void); /* standard filters */ #define IMAGING_TRANSFORM_NEAREST 0 -#define IMAGING_TRANSFORM_ANTIALIAS 1 +#define IMAGING_TRANSFORM_LANCZOS 1 #define IMAGING_TRANSFORM_BILINEAR 2 #define IMAGING_TRANSFORM_BICUBIC 3 @@ -263,7 +263,8 @@ extern Imaging ImagingFilter( FLOAT32 offset, FLOAT32 divisor); extern Imaging ImagingFlipLeftRight(Imaging imOut, Imaging imIn); extern Imaging ImagingFlipTopBottom(Imaging imOut, Imaging imIn); -extern Imaging ImagingGaussianBlur(Imaging im, Imaging imOut, float radius); +extern Imaging ImagingGaussianBlur(Imaging imOut, Imaging imIn, float radius, + int passes); extern Imaging ImagingGetBand(Imaging im, int band); extern int ImagingGetBBox(Imaging im, int bbox[4]); typedef struct { int x, y; INT32 count; INT32 pixel; } ImagingColorItem; @@ -285,13 +286,14 @@ extern Imaging ImagingPointTransform( Imaging imIn, double scale, double offset); extern Imaging ImagingPutBand(Imaging im, Imaging imIn, int band); extern Imaging ImagingRankFilter(Imaging im, int size, int rank); -extern Imaging ImagingResize(Imaging imOut, Imaging imIn, int filter); extern Imaging ImagingRotate( Imaging imOut, Imaging imIn, double theta, int filter); extern Imaging ImagingRotate90(Imaging imOut, Imaging imIn); extern Imaging ImagingRotate180(Imaging imOut, Imaging imIn); extern Imaging ImagingRotate270(Imaging imOut, Imaging imIn); -extern Imaging ImagingStretch(Imaging imOut, Imaging imIn, int filter); +extern Imaging ImagingResample(Imaging imIn, int xsize, int ysize, int filter); +extern Imaging ImagingTranspose(Imaging imOut, Imaging imIn); +extern Imaging ImagingTransposeToNew(Imaging imIn); extern Imaging ImagingTransformPerspective( Imaging imOut, Imaging imIn, int x0, int y0, int x1, int y1, double a[8], int filter, int fill); @@ -307,7 +309,8 @@ extern Imaging ImagingTransform( ImagingTransformFilter filter, void* filter_data, int fill); extern Imaging ImagingUnsharpMask( - Imaging im, Imaging imOut, float radius, int percent, int threshold); + Imaging imOut, Imaging im, float radius, int percent, int threshold); +extern Imaging ImagingBoxBlur(Imaging imOut, Imaging imIn, float radius, int n); extern Imaging ImagingCopy2(Imaging imOut, Imaging imIn); extern Imaging ImagingConvert2(Imaging imOut, Imaging imIn); @@ -525,9 +528,9 @@ enum { extern ImagingIncrementalCodec ImagingIncrementalCodecCreate(ImagingIncrementalCodecEntry codec_entry, Imaging im, ImagingCodecState state, int read_or_write, int seekable, int fd); extern void ImagingIncrementalCodecDestroy(ImagingIncrementalCodec codec); extern int ImagingIncrementalCodecPushBuffer(ImagingIncrementalCodec codec, UINT8 *buf, int bytes); -extern ssize_t ImagingIncrementalCodecRead(ImagingIncrementalCodec codec, void *buffer, size_t bytes); +extern Py_ssize_t ImagingIncrementalCodecRead(ImagingIncrementalCodec codec, void *buffer, size_t bytes); extern off_t ImagingIncrementalCodecSkip(ImagingIncrementalCodec codec, off_t bytes); -extern ssize_t ImagingIncrementalCodecWrite(ImagingIncrementalCodec codec, const void *buffer, size_t bytes); +extern Py_ssize_t ImagingIncrementalCodecWrite(ImagingIncrementalCodec codec, const void *buffer, size_t bytes); extern off_t ImagingIncrementalCodecSeek(ImagingIncrementalCodec codec, off_t bytes); extern size_t ImagingIncrementalCodecBytesInBuffer(ImagingIncrementalCodec codec); diff --git a/libImaging/Incremental.c b/libImaging/Incremental.c index 206c8130b..84e20acf6 100644 --- a/libImaging/Incremental.c +++ b/libImaging/Incremental.c @@ -413,7 +413,7 @@ ImagingIncrementalCodecBytesInBuffer(ImagingIncrementalCodec codec) return codec->stream.ptr - codec->stream.buffer; } -ssize_t +Py_ssize_t ImagingIncrementalCodecRead(ImagingIncrementalCodec codec, void *buffer, size_t bytes) { @@ -428,7 +428,7 @@ ImagingIncrementalCodecRead(ImagingIncrementalCodec codec, DEBUG("reading (want %llu bytes)\n", (unsigned long long)bytes); if (codec->stream.fd >= 0) { - ssize_t ret = read(codec->stream.fd, buffer, bytes); + Py_ssize_t ret = read(codec->stream.fd, buffer, bytes); DEBUG("read %lld bytes from fd\n", (long long)ret); return ret; } @@ -497,7 +497,7 @@ ImagingIncrementalCodecSkip(ImagingIncrementalCodec codec, off_t done = 0; while (bytes) { size_t todo = (size_t)(bytes > 256 ? 256 : bytes); - ssize_t written = ImagingIncrementalCodecWrite(codec, zeroes, todo); + Py_ssize_t written = ImagingIncrementalCodecWrite(codec, zeroes, todo); if (written <= 0) break; done += written; @@ -554,7 +554,7 @@ ImagingIncrementalCodecSkip(ImagingIncrementalCodec codec, return done; } -ssize_t +Py_ssize_t ImagingIncrementalCodecWrite(ImagingIncrementalCodec codec, const void *buffer, size_t bytes) { diff --git a/libImaging/Jpeg.h b/libImaging/Jpeg.h index 0b8c5cf9a..9536cad79 100644 --- a/libImaging/Jpeg.h +++ b/libImaging/Jpeg.h @@ -89,7 +89,10 @@ typedef struct { int subsampling; /* Custom quantization tables () */ - unsigned int **qtables; + unsigned int *qtables; + + /* in factors of DCTSIZE2 */ + int qtablesLen; /* Extra data (to be injected after header) */ char* extra; int extra_size; diff --git a/libImaging/Jpeg2KDecode.c b/libImaging/Jpeg2KDecode.c index 97ec81003..abf8cebbe 100644 --- a/libImaging/Jpeg2KDecode.c +++ b/libImaging/Jpeg2KDecode.c @@ -800,6 +800,11 @@ ImagingJpeg2KDecodeCleanup(ImagingCodecState state) { if (context->decoder) ImagingIncrementalCodecDestroy(context->decoder); + context->error_msg = NULL; + + /* Prevent multiple calls to ImagingIncrementalCodecDestroy */ + context->decoder = NULL; + return -1; } diff --git a/libImaging/Jpeg2KEncode.c b/libImaging/Jpeg2KEncode.c index e8eef08c1..868cfdb41 100644 --- a/libImaging/Jpeg2KEncode.c +++ b/libImaging/Jpeg2KEncode.c @@ -257,7 +257,7 @@ j2k_encode_entry(Imaging im, ImagingCodecState state, opj_image_cmptparm_t image_params[4]; unsigned xsiz, ysiz; unsigned tile_width, tile_height; - unsigned tiles_x, tiles_y, num_tiles; + unsigned tiles_x, tiles_y; unsigned x, y, tile_ndx; unsigned n; j2k_pack_tile_t pack; @@ -471,8 +471,6 @@ j2k_encode_entry(Imaging im, ImagingCodecState state, tiles_y = (im->ysize + (params.image_offset_y0 - params.cp_ty0) + tile_height - 1) / tile_height; - num_tiles = tiles_x * tiles_y; - state->buffer = malloc (tile_width * tile_height * components * prec / 8); tile_ndx = 0; @@ -576,15 +574,20 @@ int ImagingJpeg2KEncodeCleanup(ImagingCodecState state) { JPEG2KENCODESTATE *context = (JPEG2KENCODESTATE *)state->context; - if (context->quality_layers) + if (context->quality_layers && context->encoder) Py_DECREF(context->quality_layers); if (context->error_msg) free ((void *)context->error_msg); + context->error_msg = NULL; + if (context->encoder) ImagingIncrementalCodecDestroy(context->encoder); + /* Prevent multiple calls to ImagingIncrementalCodecDestroy */ + context->encoder = NULL; + return -1; } diff --git a/libImaging/JpegEncode.c b/libImaging/JpegEncode.c index 711b201e0..3fe5d753f 100644 --- a/libImaging/JpegEncode.c +++ b/libImaging/JpegEncode.c @@ -151,9 +151,9 @@ ImagingJpegEncode(Imaging im, ImagingCodecState state, UINT8* buf, int bytes) if (context->quality > 0) { quality = context->quality; } - for (i = 0; i < sizeof(context->qtables)/sizeof(unsigned int); i++) { + for (i = 0; i < context->qtablesLen; i++) { // TODO: Should add support for none baseline - jpeg_add_quant_table(&context->cinfo, i, context->qtables[i], + jpeg_add_quant_table(&context->cinfo, i, &context->qtables[i * DCTSIZE2], quality, TRUE); } } else if (context->quality > 0) { @@ -289,8 +289,19 @@ ImagingJpegEncode(Imaging im, ImagingCodecState state, UINT8* buf, int bytes) jpeg_finish_compress(&context->cinfo); /* Clean up */ - if (context->extra) + if (context->extra) { free(context->extra); + context->extra = NULL; + } + if (context->rawExif) { + free(context->rawExif); + context->rawExif = NULL; + } + if (context->qtables) { + free(context->qtables); + context->qtables = NULL; + } + jpeg_destroy_compress(&context->cinfo); /* if (jerr.pub.num_warnings) return BROKEN; */ state->errcode = IMAGING_CODEC_END; diff --git a/libImaging/Pack.c b/libImaging/Pack.c index fecafbde4..d174d7d0d 100644 --- a/libImaging/Pack.c +++ b/libImaging/Pack.c @@ -530,6 +530,11 @@ static struct { {"RGBA", "B", 8, band2}, {"RGBA", "A", 8, band3}, + /* true colour w. alpha premultiplied */ + {"RGBa", "RGBa", 32, copy4}, + {"RGBa", "BGRa", 32, ImagingPackBGRA}, + {"RGBa", "aBGR", 32, ImagingPackABGR}, + /* true colour w. padding */ {"RGBX", "RGBX", 32, copy4}, {"RGBX", "RGBX;L", 32, packRGBXL}, diff --git a/libImaging/QuantHash.c b/libImaging/QuantHash.c index a1f99ed3a..48b7a973e 100644 --- a/libImaging/QuantHash.c +++ b/libImaging/QuantHash.c @@ -44,9 +44,6 @@ struct _HashTable { #define RESIZE_FACTOR 3 static int _hashtable_insert_node(HashTable *,HashNode *,int,int,CollisionFunc); -#if 0 -static int _hashtable_test(HashTable *); -#endif HashTable *hashtable_new(HashFunc hf,HashCmpFunc cf) { HashTable *h; @@ -132,22 +129,6 @@ static void _hashtable_resize(HashTable *h) { } } -#if 0 -static int _hashtable_test(HashTable *h) { - uint32_t i; - int j; - HashNode *n; - for (i=0;ilength;i++) { - for (n=h->table[i];n&&n->next;n=n->next) { - j=h->cmpFunc(h,n->key,n->next->key); - printf ("%c",j?(j<0?'-':'+'):'='); - } - printf ("\n"); - } - return 0; -} -#endif - static int _hashtable_insert_node(HashTable *h,HashNode *node,int resize,int update,CollisionFunc cf) { uint32_t hash=h->hashFunc(h,node->key)%h->length; HashNode **n,*nv; diff --git a/libImaging/QuantOctree.c b/libImaging/QuantOctree.c index b233ccd2e..cd0d7fbd1 100644 --- a/libImaging/QuantOctree.c +++ b/libImaging/QuantOctree.c @@ -345,7 +345,7 @@ int quantize_octree(Pixel *pixelData, /* Create two color cubes, one fine grained with 8x16x8=1024 colors buckets and a coarse with 4x4x4=64 color buckets. - The coarse one guarantes that there are color buckets available for + The coarse one guarantees that there are color buckets available for the whole color range (assuming nQuantPixels > 64). For a quantization to 256 colors all 64 coarse colors will be used @@ -421,7 +421,7 @@ int quantize_octree(Pixel *pixelData, /* add fine colors to the lookup cube */ add_lookup_buckets(lookupCube, paletteBuckets, nFineColors, nCoarseColors); - /* create result pixles and map palatte indices */ + /* create result pixels and map palette indices */ qp = malloc(sizeof(Pixel)*nPixels); if (!qp) goto error; map_image_pixels(pixelData, nPixels, lookupCube, qp); diff --git a/libImaging/Resample.c b/libImaging/Resample.c new file mode 100644 index 000000000..597fca3e9 --- /dev/null +++ b/libImaging/Resample.c @@ -0,0 +1,318 @@ +/* + * The Python Imaging Library + * $Id$ + * + * Pillow image resampling support + * + * history: + * 2002-03-09 fl Created (for PIL 1.1.3) + * 2002-03-10 fl Added support for mode "F" + * + * Copyright (c) 1997-2002 by Secret Labs AB + * + * See the README file for information on usage and redistribution. + */ + +#include "Imaging.h" + +#include + +struct filter { + float (*filter)(float x); + float support; +}; + +static inline float sinc_filter(float x) +{ + if (x == 0.0) + return 1.0; + x = x * M_PI; + return sin(x) / x; +} + +static inline float lanczos_filter(float x) +{ + /* truncated sinc */ + if (-3.0 <= x && x < 3.0) + return sinc_filter(x) * sinc_filter(x/3); + return 0.0; +} + +static struct filter LANCZOS = { lanczos_filter, 3.0 }; + +static inline float bilinear_filter(float x) +{ + if (x < 0.0) + x = -x; + if (x < 1.0) + return 1.0-x; + return 0.0; +} + +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 */ +#define a -0.5 + if (x < 0.0) + x = -x; + if (x < 1.0) + return ((a + 2.0) * x - (a + 3.0)) * x*x + 1; + if (x < 2.0) + return (((x - 5) * x + 8) * x - 4) * a; + return 0.0; +#undef a +} + +static struct filter BICUBIC = { bicubic_filter, 2.0 }; + + +static inline UINT8 clip8(float in) +{ + int out = (int) in; + if (out >= 255) + return 255; + if (out <= 0) + return 0; + return (UINT8) out; +} + + +/* 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__) && \ + ! defined(__clang__) && defined(GCC_VERSION) && (GCC_VERSION < 40900) +static float __attribute__((always_inline)) i2f(int v) { + float x; + __asm__("xorps %0, %0; cvtsi2ss %1, %0" : "=X"(x) : "r"(v) ); + return x; +} +#else +static float inline i2f(int v) { return (float) v; } +#endif + + +Imaging +ImagingResampleHorizontal(Imaging imIn, int xsize, int filter) +{ + ImagingSectionCookie cookie; + Imaging imOut; + struct filter *filterp; + float support, scale, filterscale; + float center, ww, ss, ss0, ss1, ss2, ss3; + int xx, yy, x, kmax, xmin, xmax; + int *xbounds; + float *k, *kk; + + /* check filter */ + switch (filter) { + case IMAGING_TRANSFORM_LANCZOS: + filterp = &LANCZOS; + break; + case IMAGING_TRANSFORM_BILINEAR: + filterp = &BILINEAR; + break; + case IMAGING_TRANSFORM_BICUBIC: + filterp = &BICUBIC; + break; + default: + return (Imaging) ImagingError_ValueError( + "unsupported resampling filter" + ); + } + + /* prepare for horizontal stretch */ + filterscale = scale = (float) imIn->xsize / xsize; + + /* determine support size (length of resampling filter) */ + support = filterp->support; + + if (filterscale < 1.0) { + filterscale = 1.0; + } + + support = support * filterscale; + + /* maximum number of coofs */ + kmax = (int) ceil(support) * 2 + 1; + + /* coefficient buffer */ + kk = malloc(xsize * kmax * sizeof(float)); + if ( ! kk) + return (Imaging) ImagingError_MemoryError(); + + xbounds = malloc(xsize * 2 * sizeof(int)); + if ( ! xbounds) { + free(kk); + return (Imaging) ImagingError_MemoryError(); + } + + for (xx = 0; xx < xsize; xx++) { + k = &kk[xx * kmax]; + center = (xx + 0.5) * scale; + ww = 0.0; + ss = 1.0 / filterscale; + xmin = (int) floor(center - support); + if (xmin < 0) + xmin = 0; + xmax = (int) ceil(center + support); + if (xmax > imIn->xsize) + xmax = imIn->xsize; + for (x = xmin; x < xmax; x++) { + float w = filterp->filter((x - center + 0.5) * ss) * ss; + k[x - xmin] = w; + ww += w; + } + for (x = 0; x < xmax - xmin; x++) { + if (ww != 0.0) + k[x] /= ww; + } + xbounds[xx * 2 + 0] = xmin; + xbounds[xx * 2 + 1] = xmax; + } + + imOut = ImagingNew(imIn->mode, xsize, imIn->ysize); + if ( ! imOut) { + free(kk); + free(xbounds); + return NULL; + } + + ImagingSectionEnter(&cookie); + /* horizontal stretch */ + for (yy = 0; yy < imOut->ysize; yy++) { + if (imIn->image8) { + /* 8-bit grayscale */ + for (xx = 0; xx < xsize; xx++) { + xmin = xbounds[xx * 2 + 0]; + xmax = xbounds[xx * 2 + 1]; + k = &kk[xx * kmax]; + ss = 0.5; + for (x = xmin; x < xmax; x++) + ss += i2f(imIn->image8[yy][x]) * k[x - xmin]; + imOut->image8[yy][xx] = clip8(ss); + } + } else { + switch(imIn->type) { + case IMAGING_TYPE_UINT8: + /* n-bit grayscale */ + if (imIn->bands == 2) { + for (xx = 0; xx < xsize; xx++) { + xmin = xbounds[xx * 2 + 0]; + xmax = xbounds[xx * 2 + 1]; + k = &kk[xx * kmax]; + ss0 = ss1 = 0.5; + for (x = xmin; x < xmax; x++) { + ss0 += i2f((UINT8) imIn->image[yy][x*4 + 0]) * k[x - xmin]; + ss1 += i2f((UINT8) imIn->image[yy][x*4 + 3]) * k[x - xmin]; + } + imOut->image[yy][xx*4 + 0] = clip8(ss0); + imOut->image[yy][xx*4 + 3] = clip8(ss1); + } + } else if (imIn->bands == 3) { + for (xx = 0; xx < xsize; xx++) { + xmin = xbounds[xx * 2 + 0]; + xmax = xbounds[xx * 2 + 1]; + k = &kk[xx * kmax]; + ss0 = ss1 = ss2 = 0.5; + for (x = xmin; x < xmax; x++) { + ss0 += i2f((UINT8) imIn->image[yy][x*4 + 0]) * k[x - xmin]; + ss1 += i2f((UINT8) imIn->image[yy][x*4 + 1]) * k[x - xmin]; + ss2 += i2f((UINT8) imIn->image[yy][x*4 + 2]) * k[x - xmin]; + } + imOut->image[yy][xx*4 + 0] = clip8(ss0); + imOut->image[yy][xx*4 + 1] = clip8(ss1); + imOut->image[yy][xx*4 + 2] = clip8(ss2); + } + } else { + for (xx = 0; xx < xsize; xx++) { + xmin = xbounds[xx * 2 + 0]; + xmax = xbounds[xx * 2 + 1]; + k = &kk[xx * kmax]; + ss0 = ss1 = ss2 = ss3 = 0.5; + for (x = xmin; x < xmax; x++) { + ss0 += i2f((UINT8) imIn->image[yy][x*4 + 0]) * k[x - xmin]; + ss1 += i2f((UINT8) imIn->image[yy][x*4 + 1]) * k[x - xmin]; + ss2 += i2f((UINT8) imIn->image[yy][x*4 + 2]) * k[x - xmin]; + ss3 += i2f((UINT8) imIn->image[yy][x*4 + 3]) * k[x - xmin]; + } + imOut->image[yy][xx*4 + 0] = clip8(ss0); + imOut->image[yy][xx*4 + 1] = clip8(ss1); + imOut->image[yy][xx*4 + 2] = clip8(ss2); + imOut->image[yy][xx*4 + 3] = clip8(ss3); + } + } + break; + case IMAGING_TYPE_INT32: + /* 32-bit integer */ + for (xx = 0; xx < xsize; xx++) { + xmin = xbounds[xx * 2 + 0]; + xmax = xbounds[xx * 2 + 1]; + k = &kk[xx * kmax]; + ss = 0.0; + for (x = xmin; x < xmax; x++) + ss += i2f(IMAGING_PIXEL_I(imIn, x, yy)) * k[x - xmin]; + IMAGING_PIXEL_I(imOut, xx, yy) = (int) ss; + } + break; + case IMAGING_TYPE_FLOAT32: + /* 32-bit float */ + for (xx = 0; xx < xsize; xx++) { + xmin = xbounds[xx * 2 + 0]; + xmax = xbounds[xx * 2 + 1]; + k = &kk[xx * kmax]; + ss = 0.0; + for (x = xmin; x < xmax; x++) + ss += IMAGING_PIXEL_F(imIn, x, yy) * k[x - xmin]; + IMAGING_PIXEL_F(imOut, xx, yy) = ss; + } + break; + } + } + } + ImagingSectionLeave(&cookie); + free(kk); + free(xbounds); + return imOut; +} + + +Imaging +ImagingResample(Imaging imIn, int xsize, int ysize, int filter) +{ + Imaging imTemp1, imTemp2, imTemp3; + Imaging imOut; + + if (strcmp(imIn->mode, "P") == 0 || strcmp(imIn->mode, "1") == 0) + return (Imaging) ImagingError_ModeError(); + + if (imIn->type == IMAGING_TYPE_SPECIAL) + return (Imaging) ImagingError_ModeError(); + + /* two-pass resize, first pass */ + imTemp1 = ImagingResampleHorizontal(imIn, xsize, filter); + if ( ! imTemp1) + return NULL; + + /* transpose image once */ + imTemp2 = ImagingTransposeToNew(imTemp1); + ImagingDelete(imTemp1); + if ( ! imTemp2) + return NULL; + + /* second pass */ + imTemp3 = ImagingResampleHorizontal(imTemp2, ysize, filter); + ImagingDelete(imTemp2); + if ( ! imTemp3) + return NULL; + + /* transpose result */ + imOut = ImagingTransposeToNew(imTemp3); + ImagingDelete(imTemp3); + if ( ! imOut) + return NULL; + + return imOut; +} diff --git a/libImaging/TiffDecode.c b/libImaging/TiffDecode.c index 787cd4506..25336e7fa 100644 --- a/libImaging/TiffDecode.c +++ b/libImaging/TiffDecode.c @@ -21,8 +21,8 @@ #include "TiffDecode.h" void dump_state(const TIFFSTATE *state){ - TRACE(("State: Location %u size %d eof %d data: %p \n", (uint)state->loc, - (int)state->size, (uint)state->eof, state->data)); + TRACE(("State: Location %u size %d eof %d data: %p ifd: %d\n", (uint)state->loc, + (int)state->size, (uint)state->eof, state->data, state->ifd)); } /* @@ -142,7 +142,7 @@ void _tiffUnmapProc(thandle_t hdata, tdata_t base, toff_t size) { (void) hdata; (void) base; (void) size; } -int ImagingLibTiffInit(ImagingCodecState state, int fp) { +int ImagingLibTiffInit(ImagingCodecState state, int fp, int offset) { TIFFSTATE *clientstate = (TIFFSTATE *)state->context; TRACE(("initing libtiff\n")); @@ -158,6 +158,7 @@ int ImagingLibTiffInit(ImagingCodecState state, int fp) { clientstate->size = 0; clientstate->data = 0; clientstate->fp = fp; + clientstate->ifd = offset; clientstate->eof = 0; return 1; @@ -195,7 +196,6 @@ int ImagingLibTiffDecode(Imaging im, ImagingCodecState state, UINT8* buffer, int clientstate->loc = 0; clientstate->data = (tdata_t)buffer; clientstate->flrealloc = 0; - dump_state(clientstate); TIFFSetWarningHandler(NULL); @@ -220,6 +220,17 @@ int ImagingLibTiffDecode(Imaging im, ImagingCodecState state, UINT8* buffer, int return -1; } + if (clientstate->ifd){ + int rv; + unsigned int ifdoffset = clientstate->ifd; + TRACE(("reading tiff ifd %d\n", ifdoffset)); + rv = TIFFSetSubDirectory(tiff, ifdoffset); + if (!rv){ + TRACE(("error in TIFFSetSubDirectory")); + return -1; + } + } + size = TIFFScanlineSize(tiff); TRACE(("ScanlineSize: %d \n", size)); if (size > state->bytes) { @@ -234,7 +245,7 @@ int ImagingLibTiffDecode(Imaging im, ImagingCodecState state, UINT8* buffer, int // back in. Can't use read encoded stripe. // This thing pretty much requires that I have the whole image in one shot. - // Prehaps a stub version would work better??? + // Perhaps a stub version would work better??? while(state->y < state->ysize){ if (TIFFReadScanline(tiff, (tdata_t)state->buffer, (uint32)state->y, 0) == -1) { TRACE(("Decode Error, row %d\n", state->y)); diff --git a/libImaging/TiffDecode.h b/libImaging/TiffDecode.h index 8f41e7cb4..0f9038528 100644 --- a/libImaging/TiffDecode.h +++ b/libImaging/TiffDecode.h @@ -32,6 +32,7 @@ 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 */ toff_t eof; int flrealloc; /* may we realloc */ @@ -39,7 +40,7 @@ typedef struct { -extern int ImagingLibTiffInit(ImagingCodecState state, int fp); +extern int ImagingLibTiffInit(ImagingCodecState state, int fp, int offset); extern int ImagingLibTiffEncodeInit(ImagingCodecState state, char *filename, int fp); extern int ImagingLibTiffSetField(ImagingCodecState state, ttag_t tag, ...); @@ -56,5 +57,4 @@ extern int ImagingLibTiffSetField(ImagingCodecState state, ttag_t tag, ...); #define TRACE(args) - #endif diff --git a/libImaging/Unpack.c b/libImaging/Unpack.c index 7c453dbfd..522e9b04c 100644 --- a/libImaging/Unpack.c +++ b/libImaging/Unpack.c @@ -638,7 +638,12 @@ unpackRGBa(UINT8* out, const UINT8* in, int pixels) int a = in[3]; if (!a) out[R] = out[G] = out[B] = out[A] = 0; - else { + else if (a == 255) { + out[R] = in[0]; + out[G] = in[1]; + out[B] = in[2]; + out[A] = a; + } else { out[R] = CLIP(in[0] * 255 / a); out[G] = CLIP(in[1] * 255 / a); out[B] = CLIP(in[2] * 255 / a); @@ -803,7 +808,7 @@ unpackI12_I16(UINT8* out, const UINT8* in, int pixels){ 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 ambigous situations. + or 2D compression, to avoid potentially ambiguous situations. Yeah. I thought so. We'll see how well people read the spec. We've got several fillorder=2 modes in TiffImagePlugin.py @@ -1114,6 +1119,12 @@ static struct { {"RGBA", "B", 8, band2}, {"RGBA", "A", 8, band3}, + /* true colour w. alpha premultiplied */ + {"RGBa", "RGBa", 32, copy4}, + {"RGBa", "BGRa", 32, unpackBGRA}, + {"RGBa", "aRGB", 32, unpackARGB}, + {"RGBa", "aBGR", 32, unpackABGR}, + /* true colour w. padding */ {"RGBX", "RGB", 24, ImagingUnpackRGB}, {"RGBX", "RGB;L", 24, unpackRGBL}, diff --git a/libImaging/UnsharpMask.c b/libImaging/UnsharpMask.c index 231826245..ec3bb23cc 100644 --- a/libImaging/UnsharpMask.c +++ b/libImaging/UnsharpMask.c @@ -9,387 +9,85 @@ #include "Python.h" #include "Imaging.h" -#define PILUSMVERSION "0.6.1" -/* version history +typedef UINT8 pixel[4]; -0.6.1 converted to C and added to PIL 1.1.7 -0.6.0 fixed/improved float radius support (oops!) - now that radius can be a float (properly), changed radius value to - be an actual radius (instead of diameter). So, you should get - similar results from PIL_usm as from other paint programs when - using the SAME values (no doubling of radius required any more). - Be careful, this may "break" software if you had it set for 2x - or 5x the radius as was recommended with earlier versions. - made PILusm thread-friendly (release GIL before lengthly operations, - and re-acquire it before returning to Python). This makes a huge - difference with multi-threaded applications on dual-processor - or "Hyperthreading"-enabled systems (Pentium4, Xeon, etc.) - -0.5.0 added support for float radius values! - -0.4.0 tweaked gaussian curve calculation to be closer to consistent shape - across a wide range of radius values - -0.3.0 changed deviation calculation in gausian algorithm to be dynamic - _gblur now adds 1 to the user-supplied radius before using it so - that a value of "0" returns the original image instead of a - black one. - fixed handling of alpha channel in RGBX, RGBA images - improved speed of gblur by reducing unnecessary checks and assignments - -0.2.0 fixed L-mode image support - -0.1.0 initial release - -*/ - -static inline UINT8 clip(double in) +static inline UINT8 clip8(int in) { - if (in >= 255.0) - return (UINT8) 255; - if (in <= 0.0) - return (UINT8) 0; + if (in >= 255) + return 255; + if (in <= 0) + return 0; return (UINT8) in; } -static Imaging -gblur(Imaging im, Imaging imOut, float floatRadius, int channels, int padding) -{ - ImagingSectionCookie cookie; - - float *maskData = NULL; - int y = 0; - int x = 0; - float z = 0; - float sum = 0.0; - float dev = 0.0; - - float *buffer = NULL; - - int *line = NULL; - UINT8 *line8 = NULL; - - int pix = 0; - float newPixel[4]; - int channel = 0; - int offset = 0; - INT32 newPixelFinals; - - int radius = 0; - float remainder = 0.0; - - int i; - - /* Do the gaussian blur */ - - /* For a symmetrical gaussian blur, instead of doing a radius*radius - matrix lookup, you get the EXACT same results by doing a radius*1 - transform, followed by a 1*radius transform. This reduces the - number of lookups exponentially (10 lookups per pixel for a - radius of 5 instead of 25 lookups). So, we blur the lines first, - then we blur the resulting columns. */ - - /* first, round radius off to the next higher integer and hold the - remainder this is used so we can support float radius values - properly. */ - - remainder = floatRadius - ((int) floatRadius); - floatRadius = ceil(floatRadius); - - /* Next, double the radius and offset by 2.0... that way "0" returns - the original image instead of a black one. We multiply it by 2.0 - so that it is a true "radius", not a diameter (the results match - other paint programs closer that way too). */ - radius = (int) ((floatRadius * 2.0) + 2.0); - - /* create the maskData for the gaussian curve */ - maskData = malloc(radius * sizeof(float)); - /* FIXME: error checking */ - for (x = 0; x < radius; x++) { - z = ((float) (x + 2) / ((float) radius)); - dev = 0.5 + (((float) (radius * radius)) * 0.001); - /* you can adjust this factor to change the shape/center-weighting - of the gaussian */ - maskData[x] = (float) pow((1.0 / sqrt(2.0 * 3.14159265359 * dev)), - ((-(z - 1.0) * -(x - 1.0)) / - (2.0 * dev))); - } - - /* if there's any remainder, multiply the first/last values in - MaskData it. this allows us to support float radius values. */ - if (remainder > 0.0) { - maskData[0] *= remainder; - maskData[radius - 1] *= remainder; - } - - for (x = 0; x < radius; x++) { - /* this is done separately now due to the correction for float - radius values above */ - sum += maskData[x]; - } - - for (i = 0; i < radius; i++) { - maskData[i] *= (1.0 / sum); - /* printf("%f\n", maskData[i]); */ - } - - /* create a temporary memory buffer for the data for the first pass - memset the buffer to 0 so we can use it directly with += */ - - /* don't bother about alpha/padding */ - buffer = calloc((size_t) (im->xsize * im->ysize * channels), - sizeof(float)); - if (buffer == NULL) - return ImagingError_MemoryError(); - - /* be nice to other threads while you go off to lala land */ - ImagingSectionEnter(&cookie); - - /* memset(buffer, 0, sizeof(buffer)); */ - - newPixel[0] = newPixel[1] = newPixel[2] = newPixel[3] = 0; - - /* perform a blur on each line, and place in the temporary storage buffer */ - for (y = 0; y < im->ysize; y++) { - if (channels == 1 && im->image8 != NULL) { - line8 = (UINT8 *) im->image8[y]; - } else { - line = im->image32[y]; - } - for (x = 0; x < im->xsize; x++) { - newPixel[0] = newPixel[1] = newPixel[2] = newPixel[3] = 0; - /* for each neighbor pixel, factor in its value/weighting to the - current pixel */ - for (pix = 0; pix < radius; pix++) { - /* figure the offset of this neighbor pixel */ - offset = - (int) ((-((float) radius / 2.0) + (float) pix) + 0.5); - if (x + offset < 0) - offset = -x; - else if (x + offset >= im->xsize) - offset = im->xsize - x - 1; - - /* add (neighbor pixel value * maskData[pix]) to the current - pixel value */ - if (channels == 1) { - buffer[(y * im->xsize) + x] += - ((float) ((UINT8 *) & line8[x + offset])[0]) * - (maskData[pix]); - } else { - for (channel = 0; channel < channels; channel++) { - buffer[(y * im->xsize * channels) + - (x * channels) + channel] += - ((float) ((UINT8 *) & line[x + offset]) - [channel]) * (maskData[pix]); - } - } - } - } - } - - /* perform a blur on each column in the buffer, and place in the - output image */ - for (x = 0; x < im->xsize; x++) { - for (y = 0; y < im->ysize; y++) { - newPixel[0] = newPixel[1] = newPixel[2] = newPixel[3] = 0; - /* for each neighbor pixel, factor in its value/weighting to the - current pixel */ - for (pix = 0; pix < radius; pix++) { - /* figure the offset of this neighbor pixel */ - offset = - (int) (-((float) radius / 2.0) + (float) pix + 0.5); - if (y + offset < 0) - offset = -y; - else if (y + offset >= im->ysize) - offset = im->ysize - y - 1; - /* add (neighbor pixel value * maskData[pix]) to the current - pixel value */ - for (channel = 0; channel < channels; channel++) { - newPixel[channel] += - (buffer - [((y + offset) * im->xsize * channels) + - (x * channels) + channel]) * (maskData[pix]); - } - } - /* if the image is RGBX or RGBA, copy the 4th channel data to - newPixel, so it gets put in imOut */ - if (strcmp(im->mode, "RGBX") == 0 - || strcmp(im->mode, "RGBA") == 0) { - newPixel[3] = (float) ((UINT8 *) & line[x + offset])[3]; - } - - /* pack the channels into an INT32 so we can put them back in - the PIL image */ - newPixelFinals = 0; - if (channels == 1) { - newPixelFinals = clip(newPixel[0]); - } else { - /* for RGB, the fourth channel isn't used anyways, so just - pack a 0 in there, this saves checking the mode for each - pixel. */ - /* this doesn't work on little-endian machines... fix it! */ - newPixelFinals = - clip(newPixel[0]) | clip(newPixel[1]) << 8 | - clip(newPixel[2]) << 16 | clip(newPixel[3]) << 24; - } - /* set the resulting pixel in imOut */ - if (channels == 1) { - imOut->image8[y][x] = (UINT8) newPixelFinals; - } else { - imOut->image32[y][x] = newPixelFinals; - } - } - } - - /* free the buffer */ - free(buffer); - - /* get the GIL back so Python knows who you are */ - ImagingSectionLeave(&cookie); - - return imOut; -} - -Imaging ImagingGaussianBlur(Imaging im, Imaging imOut, float radius) -{ - int channels = 0; - int padding = 0; - - if (strcmp(im->mode, "RGB") == 0) { - channels = 3; - padding = 1; - } else if (strcmp(im->mode, "RGBA") == 0) { - channels = 3; - padding = 1; - } else if (strcmp(im->mode, "RGBX") == 0) { - channels = 3; - padding = 1; - } else if (strcmp(im->mode, "CMYK") == 0) { - channels = 4; - padding = 0; - } else if (strcmp(im->mode, "L") == 0) { - channels = 1; - padding = 0; - } else - return ImagingError_ModeError(); - - return gblur(im, imOut, radius, channels, padding); -} Imaging -ImagingUnsharpMask(Imaging im, Imaging imOut, float radius, int percent, - int threshold) +ImagingUnsharpMask(Imaging imOut, Imaging imIn, float radius, int percent, + int threshold) { ImagingSectionCookie cookie; - Imaging result; - int channel = 0; - int channels = 0; - int padding = 0; - int x = 0; - int y = 0; + int x, y, diff; - int *lineIn = NULL; - int *lineOut = NULL; + pixel *lineIn = NULL; + pixel *lineOut = NULL; UINT8 *lineIn8 = NULL; UINT8 *lineOut8 = NULL; - int diff = 0; - - INT32 newPixel = 0; - - if (strcmp(im->mode, "RGB") == 0) { - channels = 3; - padding = 1; - } else if (strcmp(im->mode, "RGBA") == 0) { - channels = 3; - padding = 1; - } else if (strcmp(im->mode, "RGBX") == 0) { - channels = 3; - padding = 1; - } else if (strcmp(im->mode, "CMYK") == 0) { - channels = 4; - padding = 0; - } else if (strcmp(im->mode, "L") == 0) { - channels = 1; - padding = 0; - } else - return ImagingError_ModeError(); - - /* first, do a gaussian blur on the image, putting results in imOut - temporarily */ - result = gblur(im, imOut, radius, channels, padding); + /* First, do a gaussian blur on the image, putting results in imOut + temporarily. All format checks are in gaussian blur. */ + result = ImagingGaussianBlur(imOut, imIn, radius, 3); if (!result) - return NULL; + return NULL; - /* now, go through each pixel, compare "normal" pixel to blurred - pixel. if the difference is more than threshold values, apply + /* Now, go through each pixel, compare "normal" pixel to blurred + pixel. If the difference is more than threshold values, apply the OPPOSITE correction to the amount of blur, multiplied by percent. */ ImagingSectionEnter(&cookie); - for (y = 0; y < im->ysize; y++) { - if (channels == 1) { - lineIn8 = im->image8[y]; - lineOut8 = imOut->image8[y]; - } else { - lineIn = im->image32[y]; - lineOut = imOut->image32[y]; - } - for (x = 0; x < im->xsize; x++) { - newPixel = 0; - /* compare in/out pixels, apply sharpening */ - if (channels == 1) { - diff = - ((UINT8 *) & lineIn8[x])[0] - - ((UINT8 *) & lineOut8[x])[0]; - if (abs(diff) > threshold) { - /* add the diff*percent to the original pixel */ - imOut->image8[y][x] = - clip((((UINT8 *) & lineIn8[x])[0]) + - (diff * ((float) percent) / 100.0)); - } else { - /* newPixel is the same as imIn */ - imOut->image8[y][x] = ((UINT8 *) & lineIn8[x])[0]; - } - } + for (y = 0; y < imIn->ysize; y++) { + if (imIn->image8) + { + lineIn8 = imIn->image8[y]; + lineOut8 = imOut->image8[y]; + for (x = 0; x < imIn->xsize; x++) { + /* compare in/out pixels, apply sharpening */ + diff = lineIn8[x] - lineOut8[x]; + if (abs(diff) > threshold) { + /* add the diff*percent to the original pixel */ + lineOut8[x] = clip8(lineIn8[x] + diff * percent / 100); + } else { + /* new pixel is the same as imIn */ + lineOut8[x] = lineIn8[x]; + } + } + } else { + lineIn = (pixel *)imIn->image32[y]; + lineOut = (pixel *)imOut->image32[y]; + for (x = 0; x < imIn->xsize; x++) { + /* compare in/out pixels, apply sharpening */ + diff = lineIn[x][0] - lineOut[x][0]; + lineOut[x][0] = abs(diff) > threshold ? + clip8(lineIn[x][0] + diff * percent / 100) : lineIn[x][0]; - else { - for (channel = 0; channel < channels; channel++) { - diff = (int) ((((UINT8 *) & lineIn[x])[channel]) - - (((UINT8 *) & lineOut[x])[channel])); - if (abs(diff) > threshold) { - /* add the diff*percent to the original pixel - this may not work for little-endian systems, fix it! */ - newPixel = - newPixel | - clip((float) (((UINT8 *) & lineIn[x])[channel]) - + - (diff * - (((float) percent / - 100.0)))) << (channel * 8); - } else { - /* newPixel is the same as imIn - this may not work for little-endian systems, fix it! */ - newPixel = - newPixel | ((UINT8 *) & lineIn[x])[channel] << - (channel * 8); - } - } - if (strcmp(im->mode, "RGBX") == 0 - || strcmp(im->mode, "RGBA") == 0) { - /* preserve the alpha channel - this may not work for little-endian systems, fix it! */ - newPixel = - newPixel | ((UINT8 *) & lineIn[x])[channel] << 24; - } - imOut->image32[y][x] = newPixel; - } - } + diff = lineIn[x][1] - lineOut[x][1]; + lineOut[x][1] = abs(diff) > threshold ? + clip8(lineIn[x][1] + diff * percent / 100) : lineIn[x][1]; + + diff = lineIn[x][2] - lineOut[x][2]; + lineOut[x][2] = abs(diff) > threshold ? + clip8(lineIn[x][2] + diff * percent / 100) : lineIn[x][2]; + + diff = lineIn[x][3] - lineOut[x][3]; + lineOut[x][3] = abs(diff) > threshold ? + clip8(lineIn[x][3] + diff * percent / 100) : lineIn[x][3]; + } + } } ImagingSectionLeave(&cookie); diff --git a/mp_compile.py b/mp_compile.py index adbac7c19..a930f4245 100644 --- a/mp_compile.py +++ b/mp_compile.py @@ -1,15 +1,17 @@ # A monkey patch of the base distutils.ccompiler to use parallel builds # Tested on 2.7, looks to be identical to 3.3. +from __future__ import print_function from multiprocessing import Pool, cpu_count from distutils.ccompiler import CCompiler -import os, sys +import os +import sys try: - MAX_PROCS = int(os.environ.get('MAX_CONCURRENCY', cpu_count())) -except: + MAX_PROCS = int(os.environ.get('MAX_CONCURRENCY', min(4, cpu_count()))) +except NotImplementedError: MAX_PROCS = None - + # hideous monkeypatching. but. but. but. def _mp_compile_one(tp): @@ -38,7 +40,7 @@ def _mp_compile(self, sources, output_dir=None, macros=None, pool = Pool(MAX_PROCS) try: - print ("Building using %d processes" % pool._processes) + print("Building using %d processes" % pool._processes) except: pass arr = [(self, obj, build, cc_args, extra_postargs, pp_opts) @@ -57,6 +59,7 @@ if MAX_PROCS != 1 and not sys.platform.startswith('win'): pool = Pool(2) CCompiler.compile = _mp_compile except Exception as msg: - print("Exception installing mp_compile, proceeding without: %s" %msg) + print("Exception installing mp_compile, proceeding without: %s" % msg) else: - print("Single threaded build, not installing mp_compile: %s processes" %MAX_PROCS) + print("Single threaded build, not installing mp_compile: %s processes" % + MAX_PROCS) diff --git a/profile-installed.py b/profile-installed.py index be9a960d2..c319c2899 100755 --- a/profile-installed.py +++ b/profile-installed.py @@ -21,6 +21,6 @@ if len(sys.argv) == 1: # Make sure that nose doesn't muck with our paths. if ('--no-path-adjustment' not in sys.argv) and ('-P' not in sys.argv): sys.argv.insert(1, '--no-path-adjustment') - + if __name__ == '__main__': profile.run("nose.main()", sort=2) diff --git a/requirements.txt b/requirements.txt index 86ddbd771..bd37d7ba9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,8 @@ -# Testing reqs +# Testing and documentation requirements -e . +-r docs/requirements.txt +coveralls nose +nose-cov +pep8 +pyflakes diff --git a/selftest.py b/selftest.py index 29af34ad2..4ffae3a16 100644 --- a/selftest.py +++ b/selftest.py @@ -9,6 +9,7 @@ if "--installed" in sys.argv: del sys.path[0] from PIL import Image, ImageDraw, ImageFilter, ImageMath +from PIL import features if "--installed" in sys.argv: sys.path.insert(0, sys_path_0) @@ -49,13 +50,13 @@ def testimage(): Or open existing files: - >>> im = Image.open(os.path.join(ROOT, "Tests/images/lena.gif")) + >>> im = Image.open(os.path.join(ROOT, "Tests/images/hopper.gif")) >>> _info(im) ('GIF', 'P', (128, 128)) - >>> _info(Image.open(os.path.join(ROOT, "Tests/images/lena.ppm"))) + >>> _info(Image.open(os.path.join(ROOT, "Tests/images/hopper.ppm"))) ('PPM', 'RGB', (128, 128)) >>> try: - ... _info(Image.open(os.path.join(ROOT, "Tests/images/lena.jpg"))) + ... _info(Image.open(os.path.join(ROOT, "Tests/images/hopper.jpg"))) ... except IOError as v: ... print(v) ('JPEG', 'RGB', (128, 128)) @@ -63,7 +64,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/lena.ppm")) + >>> im = Image.open(os.path.join(ROOT, "Tests/images/hopper.ppm")) >>> print(im.im) # internal image attribute None >>> a = im.load() @@ -73,7 +74,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/lena.ppm")) + >>> im = Image.open(os.path.join(ROOT, "Tests/images/hopper.ppm")) >>> _info(im.convert("L")) (None, 'L', (128, 128)) >>> _info(im.copy()) @@ -89,9 +90,9 @@ def testimage(): >>> len(im.getdata()) 16384 >>> im.getextrema() - ((61, 255), (26, 234), (44, 223)) + ((0, 255), (0, 255), (0, 255)) >>> im.getpixel((0, 0)) - (223, 162, 133) + (20, 20, 70) >>> len(im.getprojection()) 2 >>> len(im.histogram()) @@ -162,22 +163,6 @@ def testimage(): """ -def check_module(feature, module): - try: - __import__(module) - except ImportError: - print("***", feature, "support not installed") - else: - print("---", feature, "support ok") - - -def check_codec(feature, codec): - if codec + "_encoder" not in dir(Image.core): - print("***", feature, "support not installed") - else: - print("---", feature, "support ok") - - if __name__ == "__main__": # check build sanity @@ -189,23 +174,34 @@ if __name__ == "__main__": print("Python modules loaded from", os.path.dirname(Image.__file__)) print("Binary modules loaded from", os.path.dirname(Image.core.__file__)) print("-"*68) - check_module("PIL CORE", "PIL._imaging") - check_module("TKINTER", "PIL._imagingtk") - check_codec("JPEG", "jpeg") - check_codec("JPEG 2000", "jpeg2k") - check_codec("ZLIB (PNG/ZIP)", "zip") - check_codec("LIBTIFF", "libtiff") - check_module("FREETYPE2", "PIL._imagingft") - check_module("LITTLECMS2", "PIL._imagingcms") - check_module("WEBP", "PIL._webp") - try: - from PIL import _webp - if _webp.WebPDecoderBuggyAlpha(): - print("***", "Transparent WEBP", "support not installed") + for name, feature in [ + ("pil", "PIL CORE"), + ("tkinter", "TKINTER"), + ("freetype2", "FREETYPE2"), + ("littlecms2", "LITTLECMS2"), + ("webp", "WEBP"), + ("transp_webp", "Transparent WEBP") + ]: + supported = features.check_module(name) + + if supported is None: + # A method was being tested, but the module required + # for the method could not be correctly imported + pass + elif supported: + print("---", feature, "support ok") else: - print("---", "Transparent WEBP", "support ok") - except Exception: - pass + print("***", feature, "support not installed") + for name, feature in [ + ("jpg", "JPEG"), + ("jpg_2000", "OPENJPEG (JPEG2000)"), + ("zlib", "ZLIB (PNG/ZIP)"), + ("libtiff", "LIBTIFF") + ]: + if features.check_codec(name): + print("---", feature, "support ok") + else: + print("***", feature, "support not installed") print("-"*68) # use doctest to make sure the test program behaves as documented! diff --git a/setup.py b/setup.py index 00715ee47..d5c085552 100644 --- a/setup.py +++ b/setup.py @@ -19,14 +19,14 @@ from distutils import sysconfig from setuptools import Extension, setup, find_packages # monkey patch import hook. Even though flake8 says it's not used, it is. -# comment this out to disable multi threaded builds. +# comment this out to disable multi threaded builds. import mp_compile _IMAGING = ( "decode", "encode", "map", "display", "outline", "path") _LIB_IMAGING = ( - "Access", "AlphaComposite", "Antialias", "Bands", "BitDecode", "Blend", + "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", @@ -37,7 +37,7 @@ _LIB_IMAGING = ( "RankFilter", "RawDecode", "RawEncode", "Storage", "SunRleDecode", "TgaRleDecode", "Unpack", "UnpackYCC", "UnsharpMask", "XbmDecode", "XbmEncode", "ZipDecode", "ZipEncode", "TiffDecode", "Incremental", - "Jpeg2KDecode", "Jpeg2KEncode") + "Jpeg2KDecode", "Jpeg2KEncode", "BoxBlur") def _add_directory(path, dir, where=None): @@ -90,7 +90,7 @@ except (ImportError, OSError): NAME = 'Pillow' -PILLOW_VERSION = '2.5.3' +PILLOW_VERSION = '2.9.0.dev0' TCL_ROOT = None JPEG_ROOT = None JPEG2K_ROOT = None @@ -410,7 +410,7 @@ class pil_build_ext(build_ext): for directory in self.compiler.include_dirs: try: listdir = os.listdir(directory) - except Exception: + except Exception: # WindowsError, FileNotFoundError continue for name in listdir: @@ -573,7 +573,7 @@ class pil_build_ext(build_ext): if feature.webpmux: defs.append(("HAVE_WEBPMUX", None)) libs.append(feature.webpmux) - libs.append(feature.webpmux.replace('pmux','pdemux')) + libs.append(feature.webpmux.replace('pmux', 'pdemux')) exts.append(Extension( "PIL._webp", ["_webp.c"], libraries=libs, define_macros=defs)) @@ -725,12 +725,15 @@ class pil_build_ext(build_ext): os.unlink(tmpfile) +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='Alex Clark (Fork Author)', author_email='aclark@aclark.net', url='http://python-pillow.github.io/', classifiers=[ @@ -754,8 +757,10 @@ setup( include_package_data=True, packages=find_packages(), scripts=glob.glob("Scripts/pil*.py"), - test_suite='PIL.tests', + test_suite='nose.collector', keywords=["Imaging", ], license='Standard PIL License', - zip_safe=False, - ) + zip_safe=not debug_build(), +) +# End of file + diff --git a/test-installed.py b/test-installed.py index 0fed377b8..9beef27aa 100755 --- a/test-installed.py +++ b/test-installed.py @@ -24,9 +24,9 @@ if 'NOSE_PROCESSES' not in os.environ: for arg in sys.argv: if '--processes' in arg: break - else: # for - sys.argv.insert(1, '--processes=-1') # -1 == number of cores - sys.argv.insert(1, '--process-timeout=30') - + else: # for + sys.argv.insert(1, '--processes=-1') # -1 == number of cores + sys.argv.insert(1, '--process-timeout=30') + if __name__ == '__main__': nose.main() diff --git a/tox.ini b/tox.ini index 8d6ea7067..80f7edef4 100644 --- a/tox.ini +++ b/tox.ini @@ -4,7 +4,7 @@ # and then run "tox" from this directory. [tox] -envlist = py26, py27, py32, py33 +envlist = py26, py27, py32, py33, py34 [testenv] commands =