diff --git a/.coveragerc b/.coveragerc index bd93c4749..ea79190ae 100644 --- a/.coveragerc +++ b/.coveragerc @@ -9,3 +9,6 @@ exclude_lines = # Don't complain if non-runnable code isn't run: if 0: if __name__ == .__main__.: + # Don't complain about debug code + if Image.DEBUG: + if DEBUG: diff --git a/.gitignore b/.gitignore index a0ba1b4c1..95ed4bac5 100644 --- a/.gitignore +++ b/.gitignore @@ -60,3 +60,9 @@ docs/_build/ \#*# .#* +#Komodo +*.komodoproject + +#OS +.DS_Store + diff --git a/.travis.yml b/.travis.yml index 36dae5b7a..7635334a0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,18 +3,25 @@ language: python notifications: irc: "chat.freenode.net#pil" +env: MAX_CONCURRENCY=4 + python: - "pypy" + - "pypy3" - 2.6 - 2.7 + - "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 cmake" + - "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" + - "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 # webp - pushd depends && ./install_webp.sh && popd @@ -25,21 +32,31 @@ install: script: - coverage erase - python setup.py clean - - python setup.py build_ext --inplace + - CFLAGS="-coverage" python setup.py build_ext --inplace - # Don't cover PyPy: it fails intermittently and is x5.8 slower (#640) - - if [ "$TRAVIS_PYTHON_VERSION" == "pypy" ]; then python selftest.py; fi - - if [ "$TRAVIS_PYTHON_VERSION" == "pypy" ]; then python Tests/run.py; fi - - # Cover the others - - if [ "$TRAVIS_PYTHON_VERSION" != "pypy" ]; then coverage run --append --include=PIL/* selftest.py; fi - - if [ "$TRAVIS_PYTHON_VERSION" != "pypy" ]; then python Tests/run.py --coverage; fi + - coverage run --append --include=PIL/* selftest.py + - coverage run --append --include=PIL/* -m nose -vx Tests/test_*.py after_success: + # gather the coverage data + - lcov --capture --directory . -b . --output-file coverage.info + # filter to remove system headers + - lcov --remove coverage.info '/usr/*' -o coverage.filtered.info + # convert to json + - coveralls-lcov -v -n coverage.filtered.info > coverage.c.json + - coverage report - - coveralls + - coveralls-merge coverage.c.json + + - pip install pep8 pyflakes - - pep8 PIL/*.py - - pyflakes PIL/*.py - - pep8 Tests/*.py - - pyflakes Tests/*.py + - pep8 --statistics --count PIL/*.py + - pep8 --statistics --count Tests/*.py + - 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 diff --git a/CHANGES.rst b/CHANGES.rst index a8dd7d137..47df8c176 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,38 +1,194 @@ Changelog (Pillow) ================== -2.5.0 (unreleased) +2.6.0 (unreleased) ------------------ + +- Fixed CVE-2014-3598, a DOS in the Jpeg2KImagePlugin (backport) + [Andrew Drake] + +- Fixed CVE-2014-3589, a DOS in the IcnsImagePlugin + [Andrew Drake] + +- setup.py: Close open file handle before deleting #844 + [divergentdave] + +- Return Profile with Transformed Images #837 + [wiredfool] + +- Changed docstring to refer to the correct function #836 + [MatMoore] + +- Adding coverage support for C code tests #833 + [wiredfool] + +- PyPy performance improvements #821 + [wiredfool] + +- Added support for reading MPO files + [Feneric] + +- Added support for encoding and decoding iTXt chunks #818 + [dolda2000] + +- HSV Support #816 + [wiredfool] + +- Removed unusable ImagePalette.new() + [hugovk] + +- Fix Scrambled XPM #808 + [wiredfool] + +- Doc cleanup + [wiredfool] + +- Fix `ImageStat` docs + [akx] + +- Added docs for ExifTags + [Wintermute3] + +- More tests for CurImagePlugin, DcxImagePlugin, ImageFont, ImageMath, ImagePalette, IptcImagePlugin, SpiderImagePlugin, SgiImagePlugin, XpmImagePlugin and _util + [hugovk] + +- Fix return value of FreeTypeFont.textsize() does not include font offsets + [tk0miya] + +- Fix dispose calculations for animated GIFs #765 + [larsjsol] + +- Added class checking to Image __eq__ function #775 + [radarhere, hugovk] + +- Test PalmImagePlugin and method to skip known bad tests #776 + [hugovk, wiredfool] + +2.5.3 (2014-08-18) +------------------ + +- Fixed CVE-2014-3598, a DOS in the Jpeg2KImagePlugin (backport) + [Andrew Drake] + + +2.5.2 (2014-08-13) +------------------ + +- Fixed CVE-2014-3589, a DOS in the IcnsImagePlugin (backport) + [Andrew Drake] + + +2.5.1 (2014-07-10) +------------------ + +- Fixed install issue if Multiprocessing.Pool is not available + [wiredfool] + +- 32bit mult overflow fix #782 + [wiredfool] + +2.5.0 (2014-07-01) +------------------ + +- Imagedraw rewrite + [terseus, wiredfool] + +- Add support for multithreaded test execution + [wiredfool] + +- Prevent shell injection #748 + [mbrown1413, wiredfool] + +- Support for Resolution in BMP files #734 + [gcq] + +- Fix error in setup.py for Python 3 + [matthew-brett] + +- Pyroma fix and add Python 3.4 to setup metadata #742 + [wirefool] + +- Top level flake8 fixes #741 + [aclark] + +- Remove obsolete Animated Raster Graphics (ARG) support + [hugovk] + +- Fix test_imagedraw failures #727 + [cgohlke] + +- Fix AttributeError: class Image has no attribute 'DEBUG' #726 + [cgohlke] + +- Fix msvc warning: 'inline' : macro redefinition #725 + [cgohlke] + +- Cleanup #654 + [dvska, hugovk, wiredfool] + +- 16-bit monochrome support for JPEG2000 + [videan42] + +- Fixed ImagePalette.save + [brightpisces] + +- Support JPEG qtables + [csinchok] + +- Add binary morphology addon + [dov, wiredfool] + +- Decompression bomb protection + [hugovk] + +- Put images in a single directory + [hugovk] + +- Support OpenJpeg 2.1 + [al45tair] + +- Remove unistd.h #include for all platforms + [wiredfool] + +- Use unittest for tests + [hugovk] + +- ImageCms fixes + [hugovk] + +- Added more ImageDraw tests + [hugovk] + - Added tests for Spider files [hugovk] - + - Use libtiff to write any compressed tiff files [wiredfool] - + - Support for pickling Image objects [hugovk] - Fixed resolution handling for EPS thumbnails [eliempje] - + - Fixed rendering of some binary EPS files (Issue #302) - [eliempje] - + [eliempje] + - Rename variables not to use built-in function names [hugovk] - -- Ignore junk JPEG markers + +- Ignore junk JPEG markers [hugovk] - + - Change default interpolation for Image.thumbnail to Image.ANTIALIAS [hugovk] - + - Add tests and fixes for saving PDFs [hugovk] - + - Remove transparency resource after P->RGBA conversion [hugovk] - + - Clean up preprocessor cruft for Windows [CounterPillow] @@ -42,13 +198,13 @@ Changelog (Pillow) - Added Image.close, context manager support. [wiredfool] -- Added support for 16 bit PGM files. +- Added support for 16 bit PGM files. [wiredfool] - Updated OleFileIO to version 0.30 from upstream [hugovk] -- Added support for additional TIFF floating point format +- Added support for additional TIFF floating point format [Hijackal] - Have the tempfile use a suffix with a dot @@ -78,7 +234,7 @@ Changelog (Pillow) - Added support for JPEG 2000 [al45tair] -- Add more detailed error messages to Image.py +- Add more detailed error messages to Image.py [larsmans] - Avoid conflicting _expand functions in PIL & MINGW, fixes #538 @@ -106,7 +262,7 @@ Changelog (Pillow) [wiredfool] - Fixed palette handling when converting from mode P->RGB->P - [d_schmidt] + [d_schmidt] - Fixed saving mode P image as a PNG with transparency = palette color 0 [d-schmidt] @@ -116,7 +272,7 @@ Changelog (Pillow) - Fixed DOS with invalid palette size or invalid image size in BMP file [wiredfool] - + - Added support for BMP version 4 and 5 [eddwardo, wiredfool] @@ -149,7 +305,13 @@ Changelog (Pillow) - Prefer homebrew freetype over X11 freetype (but still allow both) [dmckeone] - + +2.3.2 (2014-08-13) +------------------ + +- Fixed CVE-2014-3589, a DOS in the IcnsImagePlugin (backport) + [Andrew Drake] + 2.3.1 (2014-03-14) ------------------ @@ -274,7 +436,7 @@ Changelog (Pillow) [nikmolnar] - Fix for encoding of b_whitespace, similar to closed issue #272 - [mhogg] + [mhogg] - Fix #273: Add numpy array interface support for 16 and 32 bit integer modes [cgohlke] @@ -431,10 +593,14 @@ Changelog (Pillow) 2.0.0 (2013-03-15) ------------------ +.. Note:: Special thanks to Christoph Gohlke and Eric Soroos for assisting with a pre-PyCon 2013 release! + +- Many other bug fixes and enhancements by many other people. + - Add Python 3 support. (Pillow >= 2.0.0 supports Python 2.6, 2.7, 3.2, 3.3. Pillow < 2.0.0 supports Python 2.4, 2.5, 2.6, 2.7.) [fluggo] -- Add PyPy support (experimental, please see: https://github.com/python-imaging/Pillow/issues/67) +- Add PyPy support (experimental, please see: https://github.com/python-pillow/Pillow/issues/67) - Add WebP support. [lqs] @@ -454,10 +620,6 @@ Changelog (Pillow) - Added support for PNG images with transparency palette. [d-schmidt] -- Many other bug fixes and enhancements by many other people (see commit log and/or docs/CONTRIBUTORS.txt). - -- Special thanks to Christoph Gohlke and Eric Soroos for rallying around the effort to get a release out for PyCon 2013. - 1.7.8 (2012-11-01) ------------------ @@ -530,44 +692,55 @@ Changelog (Pillow) [elro] - Doc fixes + [aclark] 1.5 (11/28/2010) ---------------- - Module and package fixes + [aclark] 1.4 (11/28/2010) ---------------- - Doc fixes + [aclark] 1.3 (11/28/2010) ---------------- - Add support for /lib64 and /usr/lib64 library directories on Linux + [aclark] + - Doc fixes + [aclark] 1.2 (08/02/2010) ---------------- -- On OS X also check for freetype2 in the X11 path [jezdez] -- Doc fixes [aclark] +- On OS X also check for freetype2 in the X11 path + [jezdez] + +- Doc fixes + [aclark] 1.1 (07/31/2010) ---------------- - Removed setuptools_hg requirement + [aclark] + - Doc fixes + [aclark] 1.0 (07/30/2010) ---------------- -- Forked PIL based on Hanno Schlichting's re-packaging - (http://dist.plone.org/thirdparty/PIL-1.1.7.tar.gz) +- 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] -- Remove support for importing from the standard namespace - -.. Note:: What follows is the original PIL 1.1.7 CHANGES file contents +.. Note:: What follows is the original PIL 1.1.7 CHANGES :: diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 000000000..a31c9ee09 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,26 @@ +# Contributing + +## Fixes, Features and Changes + +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) + +- 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. + +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. + +## Bugs + +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. + +Let us know: +- What did you do? +- What did you expect to happen? +- What actually happened? +- What versions of Pillow and Python are you using? diff --git a/MANIFEST.in b/MANIFEST.in index c2358f76f..643e8056c 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,7 +1,11 @@ + include *.c include *.h +include *.md include *.py include *.rst +include *.txt +include .coveragerc include .gitattributes include .travis.yml include Makefile @@ -23,33 +27,59 @@ 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 Tests *.bdf recursive-include Tests *.bin recursive-include Tests *.bmp +recursive-include Tests *.bw +recursive-include Tests *.cur +recursive-include Tests *.dcx +recursive-include Tests *.doc recursive-include Tests *.eps +recursive-include Tests *.fli recursive-include Tests *.gif recursive-include Tests *.gnuplot recursive-include Tests *.html recursive-include Tests *.icm recursive-include Tests *.icns recursive-include Tests *.ico +recursive-include Tests *.j2k recursive-include Tests *.jp2 recursive-include Tests *.jpg +recursive-include Tests *.lut +recursive-include Tests *.mpo +recursive-include Tests *.pbm recursive-include Tests *.pcf recursive-include Tests *.pcx +recursive-include Tests *.pgm +recursive-include Tests *.pil recursive-include Tests *.png recursive-include Tests *.ppm +recursive-include Tests *.psd recursive-include Tests *.py +recursive-include Tests *.ras +recursive-include Tests *.rgb +recursive-include Tests *.rst +recursive-include Tests *.sgi +recursive-include Tests *.spider +recursive-include Tests *.tar recursive-include Tests *.tif recursive-include Tests *.tiff recursive-include Tests *.ttf recursive-include Tests *.txt +recursive-include Tests *.webp +recursive-include Tests *.xpm recursive-include Tk *.c +recursive-include Tk *.rst recursive-include Tk *.txt +recursive-include depends *.rst recursive-include depends *.sh recursive-include docs *.bat recursive-include docs *.gitignore @@ -57,10 +87,10 @@ recursive-include docs *.html recursive-include docs *.py recursive-include docs *.rst recursive-include docs *.txt -recursive-include docs Guardfile -recursive-include docs Makefile recursive-include docs BUILDME recursive-include docs COPYING +recursive-include docs Guardfile recursive-include docs LICENSE +recursive-include docs Makefile recursive-include libImaging *.c recursive-include libImaging *.h diff --git a/Makefile b/Makefile index dca36d4ed..98e0c647a 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,61 @@ +.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/python Tests/run.py + bin/nosetests Tests/test_*.py + bin/python setup.py install + bin/python test-installed.py check-manifest pyroma . viewdoc + +clean: + python setup.py clean + rm PIL/*.so || true + 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 + rm -r htmlcov || true + coverage combine + coverage report + +test-dep: + pip install coveralls nose nose-cov pep8 pyflakes + +docs: + $(MAKE) -C docs html + +docserver: + cd docs/_build/html && python -mSimpleHTTPServer 2> /dev/null& \ No newline at end of file diff --git a/PIL/ArgImagePlugin.py b/PIL/ArgImagePlugin.py deleted file mode 100644 index df5ff2625..000000000 --- a/PIL/ArgImagePlugin.py +++ /dev/null @@ -1,506 +0,0 @@ -# -# THIS IS WORK IN PROGRESS -# -# The Python Imaging Library. -# $Id$ -# -# ARG animation support code -# -# history: -# 1996-12-30 fl Created -# 1996-01-06 fl Added safe scripting environment -# 1996-01-10 fl Added JHDR, UHDR and sYNC support -# 2005-03-02 fl Removed AAPP and ARUN support -# -# Copyright (c) Secret Labs AB 1997. -# Copyright (c) Fredrik Lundh 1996-97. -# -# See the README file for information on usage and redistribution. -# - -from __future__ import print_function - -__version__ = "0.4" - -from PIL import Image, ImageFile, ImagePalette - -from PIL.PngImagePlugin import i8, i16, i32, ChunkStream, _MODES - -MAGIC = b"\212ARG\r\n\032\n" - -# -------------------------------------------------------------------- -# ARG parser - -class ArgStream(ChunkStream): - "Parser callbacks for ARG data" - - def __init__(self, fp): - - ChunkStream.__init__(self, fp) - - self.eof = 0 - - self.im = None - self.palette = None - - self.__reset() - - def __reset(self): - - # reset decoder state (called on init and sync) - - self.count = 0 - self.id = None - self.action = ("NONE",) - - self.images = {} - self.names = {} - - - def chunk_AHDR(self, offset, bytes): - "AHDR -- animation header" - - # assertions - if self.count != 0: - raise SyntaxError("misplaced AHDR chunk") - - s = self.fp.read(bytes) - self.size = i32(s), i32(s[4:]) - try: - self.mode, self.rawmode = _MODES[(i8(s[8]), i8(s[9]))] - except: - raise SyntaxError("unknown ARG mode") - - if Image.DEBUG: - print("AHDR size", self.size) - print("AHDR mode", self.mode, self.rawmode) - - return s - - def chunk_AFRM(self, offset, bytes): - "AFRM -- next frame follows" - - # assertions - if self.count != 0: - raise SyntaxError("misplaced AFRM chunk") - - self.show = 1 - self.id = 0 - self.count = 1 - self.repair = None - - s = self.fp.read(bytes) - if len(s) >= 2: - self.id = i16(s) - if len(s) >= 4: - self.count = i16(s[2:4]) - if len(s) >= 6: - self.repair = i16(s[4:6]) - else: - self.repair = None - - if Image.DEBUG: - print("AFRM", self.id, self.count) - - return s - - def chunk_ADEF(self, offset, bytes): - "ADEF -- store image" - - # assertions - if self.count != 0: - raise SyntaxError("misplaced ADEF chunk") - - self.show = 0 - self.id = 0 - self.count = 1 - self.repair = None - - s = self.fp.read(bytes) - if len(s) >= 2: - self.id = i16(s) - if len(s) >= 4: - self.count = i16(s[2:4]) - - if Image.DEBUG: - print("ADEF", self.id, self.count) - - return s - - def chunk_NAME(self, offset, bytes): - "NAME -- name the current image" - - # assertions - if self.count == 0: - raise SyntaxError("misplaced NAME chunk") - - name = self.fp.read(bytes) - self.names[self.id] = name - - return name - - def chunk_AEND(self, offset, bytes): - "AEND -- end of animation" - - if Image.DEBUG: - print("AEND") - - self.eof = 1 - - raise EOFError("end of ARG file") - - def __getmodesize(self, s, full=1): - - size = i32(s), i32(s[4:]) - - try: - mode, rawmode = _MODES[(i8(s[8]), i8(s[9]))] - except: - raise SyntaxError("unknown image mode") - - if full: - if i8(s[12]): - pass # interlace not yet supported - if i8(s[11]): - raise SyntaxError("unknown filter category") - - return size, mode, rawmode - - def chunk_PAST(self, offset, bytes): - "PAST -- paste one image into another" - - # assertions - if self.count == 0: - raise SyntaxError("misplaced PAST chunk") - - if self.repair is not None: - # we must repair the target image before we - # start pasting - - # brute force; a better solution would be to - # update only the dirty rectangles in images[id]. - # note that if images[id] doesn't exist, it must - # be created - - self.images[self.id] = self.images[self.repair].copy() - self.repair = None - - s = self.fp.read(bytes) - im = self.images[i16(s)] - x, y = i32(s[2:6]), i32(s[6:10]) - bbox = x, y, im.size[0]+x, im.size[1]+y - - if im.mode in ["RGBA"]: - # paste with transparency - # FIXME: should handle P+transparency as well - self.images[self.id].paste(im, bbox, im) - else: - # paste without transparency - self.images[self.id].paste(im, bbox) - - self.action = ("PAST",) - self.__store() - - return s - - def chunk_BLNK(self, offset, bytes): - "BLNK -- create blank image" - - # assertions - if self.count == 0: - raise SyntaxError("misplaced BLNK chunk") - - s = self.fp.read(bytes) - size, mode, rawmode = self.__getmodesize(s, 0) - - # store image (FIXME: handle colour) - self.action = ("BLNK",) - self.im = Image.core.fill(mode, size, 0) - self.__store() - - return s - - def chunk_IHDR(self, offset, bytes): - "IHDR -- full image follows" - - # assertions - if self.count == 0: - raise SyntaxError("misplaced IHDR chunk") - - # image header - s = self.fp.read(bytes) - size, mode, rawmode = self.__getmodesize(s) - - # decode and store image - self.action = ("IHDR",) - self.im = Image.core.new(mode, size) - self.decoder = Image.core.zip_decoder(rawmode) - self.decoder.setimage(self.im, (0,0) + size) - self.data = b"" - - return s - - def chunk_DHDR(self, offset, bytes): - "DHDR -- delta image follows" - - # assertions - if self.count == 0: - raise SyntaxError("misplaced DHDR chunk") - - s = self.fp.read(bytes) - - size, mode, rawmode = self.__getmodesize(s) - - # delta header - diff = i8(s[13]) - offs = i32(s[14:18]), i32(s[18:22]) - - bbox = offs + (offs[0]+size[0], offs[1]+size[1]) - - if Image.DEBUG: - print("DHDR", diff, bbox) - - # FIXME: decode and apply image - self.action = ("DHDR", diff, bbox) - - # setup decoder - self.im = Image.core.new(mode, size) - - self.decoder = Image.core.zip_decoder(rawmode) - self.decoder.setimage(self.im, (0,0) + size) - - self.data = b"" - - return s - - def chunk_JHDR(self, offset, bytes): - "JHDR -- JPEG image follows" - - # assertions - if self.count == 0: - raise SyntaxError("misplaced JHDR chunk") - - # image header - s = self.fp.read(bytes) - size, mode, rawmode = self.__getmodesize(s, 0) - - # decode and store image - self.action = ("JHDR",) - self.im = Image.core.new(mode, size) - self.decoder = Image.core.jpeg_decoder(rawmode) - self.decoder.setimage(self.im, (0,0) + size) - self.data = b"" - - return s - - def chunk_UHDR(self, offset, bytes): - "UHDR -- uncompressed image data follows (EXPERIMENTAL)" - - # assertions - if self.count == 0: - raise SyntaxError("misplaced UHDR chunk") - - # image header - s = self.fp.read(bytes) - size, mode, rawmode = self.__getmodesize(s, 0) - - # decode and store image - self.action = ("UHDR",) - self.im = Image.core.new(mode, size) - self.decoder = Image.core.raw_decoder(rawmode) - self.decoder.setimage(self.im, (0,0) + size) - self.data = b"" - - return s - - def chunk_IDAT(self, offset, bytes): - "IDAT -- image data block" - - # pass compressed chunks through the decoder - s = self.fp.read(bytes) - self.data = self.data + s - n, e = self.decoder.decode(self.data) - if n < 0: - # end of image - if e < 0: - raise IOError("decoder error %d" % e) - else: - self.data = self.data[n:] - - return s - - def chunk_DEND(self, offset, bytes): - return self.chunk_IEND(offset, bytes) - - def chunk_JEND(self, offset, bytes): - return self.chunk_IEND(offset, bytes) - - def chunk_UEND(self, offset, bytes): - return self.chunk_IEND(offset, bytes) - - def chunk_IEND(self, offset, bytes): - "IEND -- end of image" - - # we now have a new image. carry out the operation - # defined by the image header. - - # won't need these anymore - del self.decoder - del self.data - - self.__store() - - return self.fp.read(bytes) - - def __store(self): - - # apply operation - cid = self.action[0] - - if cid in ["BLNK", "IHDR", "JHDR", "UHDR"]: - # store - self.images[self.id] = self.im - - elif cid == "DHDR": - # paste - cid, mode, bbox = self.action - im0 = self.images[self.id] - im1 = self.im - if mode == 0: - im1 = im1.chop_add_modulo(im0.crop(bbox)) - im0.paste(im1, bbox) - - self.count = self.count - 1 - - if self.count == 0 and self.show: - self.im = self.images[self.id] - raise EOFError # end of this frame - - def chunk_PLTE(self, offset, bytes): - "PLTE -- palette data" - - s = self.fp.read(bytes) - if self.mode == "P": - self.palette = ImagePalette.raw("RGB", s) - return s - - def chunk_sYNC(self, offset, bytes): - "SYNC -- reset decoder" - - if self.count != 0: - raise SyntaxError("misplaced sYNC chunk") - - s = self.fp.read(bytes) - self.__reset() - return s - - -# -------------------------------------------------------------------- -# ARG reader - -def _accept(prefix): - return prefix[:8] == MAGIC - -## -# Image plugin for the experimental Animated Raster Graphics format. - -class ArgImageFile(ImageFile.ImageFile): - - format = "ARG" - format_description = "Animated raster graphics" - - def _open(self): - - if Image.warnings: - Image.warnings.warn( - "The ArgImagePlugin driver is obsolete, and will be removed " - "from a future release of PIL. If you rely on this module, " - "please contact the PIL authors.", - RuntimeWarning - ) - - if self.fp.read(8) != MAGIC: - raise SyntaxError("not an ARG file") - - self.arg = ArgStream(self.fp) - - # read and process the first chunk (AHDR) - - cid, offset, bytes = self.arg.read() - - if cid != "AHDR": - raise SyntaxError("expected an AHDR chunk") - - s = self.arg.call(cid, offset, bytes) - - self.arg.crc(cid, s) - - # image characteristics - self.mode = self.arg.mode - self.size = self.arg.size - - def load(self): - - if self.arg.im is None: - self.seek(0) - - # image data - self.im = self.arg.im - self.palette = self.arg.palette - - # set things up for further processing - Image.Image.load(self) - - def seek(self, frame): - - if self.arg.eof: - raise EOFError("end of animation") - - self.fp = self.arg.fp - - while True: - - # - # process chunks - - cid, offset, bytes = self.arg.read() - - if self.arg.eof: - raise EOFError("end of animation") - - try: - s = self.arg.call(cid, offset, bytes) - except EOFError: - break - - except "glurk": # AttributeError - if Image.DEBUG: - print(cid, bytes, "(unknown)") - s = self.fp.read(bytes) - - self.arg.crc(cid, s) - - self.fp.read(4) # ship extra CRC - - def tell(self): - return 0 - - def verify(self): - "Verify ARG file" - - # back up to first chunk - self.fp.seek(8) - - self.arg.verify(self) - self.arg.close() - - self.fp = None - -# -# -------------------------------------------------------------------- - -Image.register_open("ARG", ArgImageFile, _accept) - -Image.register_extension("ARG", ".arg") - -Image.register_mime("ARG", "video/x-arg") diff --git a/PIL/BdfFontFile.py b/PIL/BdfFontFile.py index 3be80d602..3a41848d8 100644 --- a/PIL/BdfFontFile.py +++ b/PIL/BdfFontFile.py @@ -128,5 +128,5 @@ class BdfFontFile(FontFile.FontFile): if not c: break id, ch, (xy, dst, src), im = c - if ch >= 0 and ch < len(self.glyph): + if 0 <= ch < len(self.glyph): self.glyph[ch] = xy, dst, src, im diff --git a/PIL/BmpImagePlugin.py b/PIL/BmpImagePlugin.py index 436ca5dce..fae6bd391 100644 --- a/PIL/BmpImagePlugin.py +++ b/PIL/BmpImagePlugin.py @@ -28,6 +28,7 @@ __version__ = "0.7" from PIL import Image, ImageFile, ImagePalette, _binary +import math i8 = _binary.i8 i16 = _binary.i16le @@ -88,6 +89,7 @@ class BmpImageFile(ImageFile.ImageFile): 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 @@ -95,6 +97,8 @@ class BmpImageFile(ImageFile.ImageFile): # 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)) else: raise IOError("Unsupported BMP header type (%d)" % len(s)) @@ -203,30 +207,37 @@ def _save(im, fp, filename, check=0): if check: return check + info = im.encoderinfo + + dpi = info.get("dpi", (96, 96)) + + # 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 offset = 14 + header + colors * 4 image = stride * im.size[1] # bitmap header - fp.write(b"BM" + # file type (magic) - o32(offset+image) + # file size - o32(0) + # reserved - o32(offset)) # image data offset + fp.write(b"BM" + # file type (magic) + o32(offset+image) + # file size + o32(0) + # reserved + o32(offset)) # image data offset # bitmap info header - fp.write(o32(header) + # info header size - o32(im.size[0]) + # width - o32(im.size[1]) + # height - o16(1) + # planes - o16(bits) + # depth - o32(0) + # compression (0=uncompressed) - o32(image) + # size of bitmap - o32(1) + o32(1) + # resolution - o32(colors) + # colors used - o32(colors)) # colors important + fp.write(o32(header) + # info header size + o32(im.size[0]) + # width + o32(im.size[1]) + # height + o16(1) + # planes + o16(bits) + # depth + o32(0) + # compression (0=uncompressed) + o32(image) + # size of bitmap + o32(ppm[0]) + o32(ppm[1]) + # resolution + o32(colors) + # colors used + o32(colors)) # colors important - fp.write(b"\0" * (header - 40)) # padding (for OS/2 format) + fp.write(b"\0" * (header - 40)) # padding (for OS/2 format) if im.mode == "1": for i in (0, 255): diff --git a/PIL/CurImagePlugin.py b/PIL/CurImagePlugin.py index 4cf2882e2..0178957ee 100644 --- a/PIL/CurImagePlugin.py +++ b/PIL/CurImagePlugin.py @@ -33,6 +33,7 @@ i32 = _binary.i32le def _accept(prefix): return prefix[:4] == b"\0\0\2\0" + ## # Image plugin for Windows Cursor files. @@ -48,7 +49,7 @@ class CurImageFile(BmpImagePlugin.BmpImageFile): # check magic s = self.fp.read(6) if not _accept(s): - raise SyntaxError("not an CUR file") + raise SyntaxError("not a CUR file") # pick the largest cursor in the file m = b"" @@ -58,14 +59,14 @@ class CurImageFile(BmpImagePlugin.BmpImageFile): m = s elif i8(s[0]) > i8(m[0]) and i8(s[1]) > i8(m[1]): m = s - #print "width", i8(s[0]) - #print "height", i8(s[1]) - #print "colors", i8(s[2]) - #print "reserved", i8(s[3]) - #print "hotspot x", i16(s[4:]) - #print "hotspot y", i16(s[6:]) - #print "bytes", i32(s[8:]) - #print "offset", i32(s[12:]) + # print "width", i8(s[0]) + # print "height", i8(s[1]) + # print "colors", i8(s[2]) + # print "reserved", i8(s[3]) + # print "hotspot x", i16(s[4:]) + # print "hotspot y", i16(s[6:]) + # print "bytes", i32(s[8:]) + # print "offset", i32(s[12:]) # load as bitmap self._bitmap(i32(m[12:]) + offset) @@ -73,7 +74,7 @@ class CurImageFile(BmpImagePlugin.BmpImageFile): # patch up the bitmap height self.size = self.size[0], self.size[1]//2 d, e, o, a = self.tile[0] - self.tile[0] = d, (0,0)+self.size, o, a + self.tile[0] = d, (0, 0)+self.size, o, a return diff --git a/PIL/DcxImagePlugin.py b/PIL/DcxImagePlugin.py index 631875e68..0940b3935 100644 --- a/PIL/DcxImagePlugin.py +++ b/PIL/DcxImagePlugin.py @@ -27,13 +27,15 @@ from PIL import Image, _binary from PIL.PcxImagePlugin import PcxImageFile -MAGIC = 0x3ADE68B1 # QUIZ: what's this value, then? +MAGIC = 0x3ADE68B1 # QUIZ: what's this value, then? i32 = _binary.i32le + def _accept(prefix): return i32(prefix) == MAGIC + ## # Image plugin for the Intel DCX format. diff --git a/PIL/EpsImagePlugin.py b/PIL/EpsImagePlugin.py index 1df5b5d61..9f963f7e6 100644 --- a/PIL/EpsImagePlugin.py +++ b/PIL/EpsImagePlugin.py @@ -104,7 +104,7 @@ def Ghostscript(tile, size, fp, scale=1): s = fp.read(min(lengthfile, 100*1024)) if not s: break - lengthfile = lengthfile - len(s) + length -= len(s) f.write(s) # Build ghostscript command @@ -161,7 +161,7 @@ class PSFile: def tell(self): pos = self.fp.tell() if self.char: - pos = pos - 1 + pos -= 1 return pos def readline(self): s = b"" diff --git a/PIL/ExifTags.py b/PIL/ExifTags.py index 16473f930..25cd08068 100644 --- a/PIL/ExifTags.py +++ b/PIL/ExifTags.py @@ -63,15 +63,12 @@ TAGS = { 0x0201: "JpegIFOffset", 0x0202: "JpegIFByteCount", 0x0211: "YCbCrCoefficients", - 0x0211: "YCbCrCoefficients", 0x0212: "YCbCrSubSampling", 0x0213: "YCbCrPositioning", - 0x0213: "YCbCrPositioning", - 0x0214: "ReferenceBlackWhite", 0x0214: "ReferenceBlackWhite", 0x1000: "RelatedImageFileFormat", - 0x1001: "RelatedImageLength", - 0x1001: "RelatedImageWidth", + 0x1001: "RelatedImageLength", # FIXME / Dictionary contains duplicate keys + 0x1001: "RelatedImageWidth", # FIXME \ Dictionary contains duplicate keys 0x828d: "CFARepeatPatternDim", 0x828e: "CFAPattern", 0x828f: "BatteryLevel", diff --git a/PIL/FliImagePlugin.py b/PIL/FliImagePlugin.py index bef9b722e..c9a29051e 100644 --- a/PIL/FliImagePlugin.py +++ b/PIL/FliImagePlugin.py @@ -105,7 +105,7 @@ class FliImageFile(ImageFile.ImageFile): g = i8(s[n+1]) << shift b = i8(s[n+2]) << shift palette[i] = (r, g, b) - i = i + 1 + i += 1 def seek(self, frame): diff --git a/PIL/FontFile.py b/PIL/FontFile.py index 4dce0a292..26ddff0ac 100644 --- a/PIL/FontFile.py +++ b/PIL/FontFile.py @@ -17,8 +17,6 @@ import os from PIL import Image, _binary -import marshal - try: import zlib except ImportError: @@ -26,13 +24,15 @@ except ImportError: WIDTH = 800 + def puti16(fp, values): # write network order (big-endian) 16-bit sequence for v in values: if v < 0: - v = v + 65536 + v += 65536 fp.write(_binary.o16be(v)) + ## # Base class for raster font file handlers. @@ -63,7 +63,7 @@ class FontFile: h = max(h, src[3] - src[1]) w = w + (src[2] - src[0]) if w > WIDTH: - lines = lines + 1 + lines += 1 w = (src[2] - src[0]) maxwidth = max(maxwidth, w) @@ -95,9 +95,8 @@ class FontFile: # print chr(i), dst, s self.metrics[i] = d, dst, s - - def save1(self, filename): - "Save font in version 1 format" + def save(self, filename): + "Save font" self.compile() @@ -107,7 +106,7 @@ class FontFile: # font metrics fp = open(os.path.splitext(filename)[0] + ".pil", "wb") fp.write(b"PILfont\n") - fp.write((";;;;;;%d;\n" % self.ysize).encode('ascii')) # HACK!!! + fp.write((";;;;;;%d;\n" % self.ysize).encode('ascii')) # HACK!!! fp.write(b"DATA\n") for id in range(256): m = self.metrics[id] @@ -117,30 +116,4 @@ class FontFile: puti16(fp, m[0] + m[1] + m[2]) fp.close() - - def save2(self, filename): - "Save font in version 2 format" - - # THIS IS WORK IN PROGRESS - - self.compile() - - data = marshal.dumps((self.metrics, self.info)) - - if zlib: - data = b"z" + zlib.compress(data, 9) - else: - data = b"u" + data - - fp = open(os.path.splitext(filename)[0] + ".pil", "wb") - - fp.write(b"PILfont2\n" + self.name + "\n" + "DATA\n") - - fp.write(data) - - self.bitmap.save(fp, "PNG") - - fp.close() - - - save = save1 # for now +# End of file diff --git a/PIL/FpxImagePlugin.py b/PIL/FpxImagePlugin.py index b712557d7..64c7b1568 100644 --- a/PIL/FpxImagePlugin.py +++ b/PIL/FpxImagePlugin.py @@ -84,7 +84,7 @@ class FpxImageFile(ImageFile.ImageFile): i = 1 while size > 64: size = size / 2 - i = i + 1 + i += 1 self.maxid = i - 1 # mode. instead of using a single field for this, flashpix diff --git a/PIL/GifImagePlugin.py b/PIL/GifImagePlugin.py index c6d449425..640af9efc 100644 --- a/PIL/GifImagePlugin.py +++ b/PIL/GifImagePlugin.py @@ -96,8 +96,15 @@ class GifImageFile(ImageFile.ImageFile): # rewind self.__offset = 0 self.dispose = None + self.dispose_extent = [0, 0, 0, 0] #x0, y0, x1, y1 self.__frame = -1 self.__fp.seek(self.__rewind) + self._prev_im = None + self.disposal_method = 0 + else: + # ensure that the previous frame was loaded + if not self.im: + self.load() if frame != self.__frame + 1: raise ValueError("cannot seek to frame %d" % frame) @@ -114,8 +121,7 @@ class GifImageFile(ImageFile.ImageFile): self.__offset = 0 if self.dispose: - self.im = self.dispose - self.dispose = None + self.im.paste(self.dispose, self.dispose_extent) from copy import copy self.palette = copy(self.global_palette) @@ -140,17 +146,16 @@ class GifImageFile(ImageFile.ImageFile): if flags & 1: self.info["transparency"] = i8(block[3]) self.info["duration"] = i16(block[1:3]) * 10 - try: - # disposal methods - if flags & 8: - # replace with background colour - self.dispose = Image.core.fill("P", self.size, - self.info["background"]) - elif flags & 16: - # replace with previous contents - self.dispose = self.im.copy() - except (AttributeError, KeyError): - pass + + # disposal method - find the value of bits 4 - 6 + dispose_bits = 0b00011100 & flags + dispose_bits = dispose_bits >> 2 + if dispose_bits: + # only set the dispose if it is not + # unspecified. I'm not sure if this is + # correct, but it seems to prevent the last + # frame from looking odd for some animations + self.disposal_method = dispose_bits elif i8(s) == 255: # # application extension @@ -172,6 +177,7 @@ class GifImageFile(ImageFile.ImageFile): # extent x0, y0 = i16(s[0:]), i16(s[2:]) x1, y1 = x0 + i16(s[4:]), y0 + i16(s[6:]) + self.dispose_extent = x0, y0, x1, y1 flags = i8(s[8]) interlace = (flags & 64) != 0 @@ -194,6 +200,26 @@ class GifImageFile(ImageFile.ImageFile): pass # raise IOError, "illegal GIF tag `%x`" % i8(s) + try: + if self.disposal_method < 2: + # do not dispose or none specified + self.dispose = None + elif self.disposal_method == 2: + # replace with background colour + self.dispose = Image.core.fill("P", self.size, + self.info["background"]) + else: + # replace with previous contents + if self.im: + self.dispose = self.im.copy() + + # only dispose the extent in this frame + if self.dispose: + self.dispose = self.dispose.crop(self.dispose_extent) + except (AttributeError, KeyError): + pass + + if not self.tile: # self.__fp = None raise EOFError("no more images in GIF file") @@ -205,6 +231,18 @@ class GifImageFile(ImageFile.ImageFile): def tell(self): return self.__frame + def load_end(self): + ImageFile.ImageFile.load_end(self) + + # if the disposal method is 'do not dispose', transparent + # pixels should show the content of the previous frame + if self._prev_im and self.disposal_method == 1: + # we do this by pasting the updated area onto the previous + # frame which we then use as the current image content + updated = self.im.crop(self.dispose_extent) + self._prev_im.paste(updated, self.dispose_extent, updated.convert('RGBA')) + self.im = self._prev_im + self._prev_im = self.im.copy() # -------------------------------------------------------------------- # Write GIF files @@ -230,10 +268,9 @@ def _save(im, fp, filename): except IOError: pass # write uncompressed file - try: - rawmode = RAWMODE[im.mode] + if im.mode in RAWMODE: imOut = im - except KeyError: + else: # convert on the fly (EXPERIMENTAL -- I'm not sure PIL # should automatically convert images on save...) if Image.getmodebase(im.mode) == "RGB": @@ -241,10 +278,8 @@ def _save(im, fp, filename): if im.palette: palette_size = len(im.palette.getdata()[1]) // 3 imOut = im.convert("P", palette=1, colors=palette_size) - rawmode = "P" else: imOut = im.convert("L") - rawmode = "L" # header try: @@ -252,12 +287,6 @@ def _save(im, fp, filename): except KeyError: palette = None im.encoderinfo["optimize"] = im.encoderinfo.get("optimize", True) - if im.encoderinfo["optimize"]: - # When the mode is L, and we optimize, we end up with - # im.mode == P and rawmode = L, which fails. - # If we're optimizing the palette, we're going to be - # in a rawmode of P anyway. - rawmode = 'P' header, usedPaletteColors = getheader(imOut, palette, im.encoderinfo) for s in header: @@ -314,7 +343,7 @@ def _save(im, fp, filename): o8(8)) # bits imOut.encoderconfig = (8, interlace) - ImageFile._save(imOut, fp, [("gif", (0,0)+im.size, 0, rawmode)]) + ImageFile._save(imOut, fp, [("gif", (0,0)+im.size, 0, RAWMODE[imOut.mode])]) fp.write(b"\0") # end of image data @@ -333,13 +362,41 @@ def _save_netpbm(im, fp, filename): # below for information on how to enable this. import os + from subprocess import Popen, check_call, PIPE, CalledProcessError + import tempfile file = im._dump() + if im.mode != "RGB": - os.system("ppmtogif %s >%s" % (file, filename)) + with open(filename, 'wb') as f: + stderr = tempfile.TemporaryFile() + check_call(["ppmtogif", file], stdout=f, stderr=stderr) else: - os.system("ppmquant 256 %s | ppmtogif >%s" % (file, filename)) - try: os.unlink(file) - except: pass + with open(filename, 'wb') as f: + + # Pipe ppmquant output into ppmtogif + # "ppmquant 256 %s | ppmtogif > %s" % (file, filename) + quant_cmd = ["ppmquant", "256", file] + togif_cmd = ["ppmtogif"] + stderr = tempfile.TemporaryFile() + quant_proc = Popen(quant_cmd, stdout=PIPE, stderr=stderr) + stderr = tempfile.TemporaryFile() + togif_proc = Popen(togif_cmd, stdin=quant_proc.stdout, stdout=f, stderr=stderr) + + # Allow ppmquant to receive SIGPIPE if ppmtogif exits + quant_proc.stdout.close() + + retcode = quant_proc.wait() + if retcode: + raise CalledProcessError(retcode, quant_cmd) + + retcode = togif_proc.wait() + if retcode: + raise CalledProcessError(retcode, togif_cmd) + + try: + os.unlink(file) + except: + pass # -------------------------------------------------------------------- diff --git a/PIL/GimpGradientFile.py b/PIL/GimpGradientFile.py index 4ae727773..7c88addae 100644 --- a/PIL/GimpGradientFile.py +++ b/PIL/GimpGradientFile.py @@ -68,7 +68,7 @@ class GradientFile: x = i / float(entries-1) while x1 < x: - ix = ix + 1 + ix += 1 x0, x1, xm, rgb0, rgb1, segment = self.gradient[ix] w = x1 - x0 diff --git a/PIL/GimpPaletteFile.py b/PIL/GimpPaletteFile.py index c271b964f..6f71ec678 100644 --- a/PIL/GimpPaletteFile.py +++ b/PIL/GimpPaletteFile.py @@ -52,7 +52,7 @@ class GimpPaletteFile: if 0 <= i <= 255: self.palette[i] = o8(v[0]) + o8(v[1]) + o8(v[2]) - i = i + 1 + i += 1 self.palette = b"".join(self.palette) diff --git a/PIL/IcnsImagePlugin.py b/PIL/IcnsImagePlugin.py index 9a0864bad..ca7a14931 100644 --- a/PIL/IcnsImagePlugin.py +++ b/PIL/IcnsImagePlugin.py @@ -70,7 +70,7 @@ def read_32(fobj, start_length, size): else: blocksize = byte + 1 data.append(fobj.read(blocksize)) - bytesleft = bytesleft - blocksize + bytesleft -= blocksize if bytesleft <= 0: break if bytesleft != 0: @@ -179,11 +179,13 @@ class IcnsFile: i = HEADERSIZE while i < filesize: sig, blocksize = nextheader(fobj) - i = i + HEADERSIZE - blocksize = blocksize - HEADERSIZE + if blocksize <= 0: + raise SyntaxError('invalid block header') + i += HEADERSIZE + blocksize -= HEADERSIZE dct[sig] = (i, blocksize) fobj.seek(blocksize, 1) - i = i + blocksize + i += blocksize def itersizes(self): sizes = [] diff --git a/PIL/ImImagePlugin.py b/PIL/ImImagePlugin.py index 1f8f011dc..a5eeef76a 100644 --- a/PIL/ImImagePlugin.py +++ b/PIL/ImImagePlugin.py @@ -182,7 +182,7 @@ class ImImageFile(ImageFile.ImageFile): self.info[k] = v if k in TAGS: - n = n + 1 + n += 1 else: diff --git a/PIL/Image.py b/PIL/Image.py index 887ceabc1..480410eff 100644 --- a/PIL/Image.py +++ b/PIL/Image.py @@ -31,11 +31,18 @@ from PIL import VERSION, PILLOW_VERSION, _plugins import warnings +class DecompressionBombWarning(RuntimeWarning): + pass + class _imaging_not_installed: # module placeholder def __getattr__(self, id): raise ImportError("The _imaging C module is not installed") + +# Limit to around a quarter gigabyte for a 24 bit (3 bpp) image +MAX_IMAGE_PIXELS = int(1024 * 1024 * 1024 / 4 / 3) + try: # give Tk a chance to set up the environment, in case we're # using an _imaging module linked against libtcl/libtk (use @@ -213,6 +220,7 @@ _MODEINFO = { "CMYK": ("RGB", "L", ("C", "M", "Y", "K")), "YCbCr": ("RGB", "L", ("Y", "Cb", "Cr")), "LAB": ("RGB", "L", ("L", "A", "B")), + "HSV": ("RGB", "L", ("H", "S", "V")), # Experimental modes include I;16, I;16L, I;16B, RGBa, BGR;15, and # BGR;24. Use these modes only if you know exactly what you're @@ -448,7 +456,7 @@ def _getscaleoffset(expr): (a, b, c) = data # simplified syntax if (a is stub and b == "__mul__" and isinstance(c, numbers.Number)): return c, 0.0 - if (a is stub and b == "__add__" and isinstance(c, numbers.Number)): + if a is stub and b == "__add__" and isinstance(c, numbers.Number): return 1.0, c except TypeError: pass @@ -532,7 +540,7 @@ class Image: try: self.fp.close() except Exception as msg: - if Image.DEBUG: + if DEBUG: print ("Error closing: %s" % msg) # Instead of simply setting to None, we're setting up a @@ -547,7 +555,6 @@ class Image: self.readonly = 0 def _dump(self, file=None, format=None): - import os import tempfile suffix = '' if format: @@ -566,6 +573,8 @@ class Image: return file def __eq__(self, other): + if self.__class__.__name__ != other.__class__.__name__: + return False a = (self.mode == other.mode) b = (self.size == other.size) c = (self.getpalette() == other.getpalette()) @@ -1529,7 +1538,7 @@ class Image: clockwise around its centre. :param angle: In degrees counter clockwise. - :param filter: An optional resampling filter. This can be + :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), or :py:attr:`PIL.Image.BICUBIC` @@ -1550,7 +1559,6 @@ class Image: math.cos(angle), math.sin(angle), 0.0, -math.sin(angle), math.cos(angle), 0.0 ] - def transform(x, y, matrix=matrix): (a, b, c, d, e, f) = matrix @@ -2075,7 +2083,6 @@ def frombuffer(mode, size, data, decoder_name="raw", *args): .. versionadded:: 1.1.4 """ - "Load image from bytes or buffer" # may pass tuple instead of argument list if len(args) == 1 and isinstance(args[0], tuple): @@ -2173,6 +2180,20 @@ _fromarray_typemap[((1, 1), _ENDIAN + "i4")] = ("I", "I") _fromarray_typemap[((1, 1), _ENDIAN + "f4")] = ("F", "F") +def _decompression_bomb_check(size): + if MAX_IMAGE_PIXELS is None: + return + + pixels = size[0] * size[1] + + if pixels > MAX_IMAGE_PIXELS: + warnings.warn( + "Image size (%d pixels) exceeds limit of %d pixels, " + "could be decompression bomb DOS attack." % + (pixels, MAX_IMAGE_PIXELS), + DecompressionBombWarning) + + def open(fp, mode="r"): """ Opens and identifies the given image file. @@ -2210,7 +2231,9 @@ def open(fp, mode="r"): factory, accept = OPEN[i] if not accept or accept(prefix): fp.seek(0) - return factory(fp, filename) + im = factory(fp, filename) + _decompression_bomb_check(im.size) + return im except (SyntaxError, IndexError, TypeError): # import traceback # traceback.print_exc() @@ -2223,7 +2246,9 @@ def open(fp, mode="r"): factory, accept = OPEN[i] if not accept or accept(prefix): fp.seek(0) - return factory(fp, filename) + im = factory(fp, filename) + _decompression_bomb_check(im.size) + return im except (SyntaxError, IndexError, TypeError): # import traceback # traceback.print_exc() diff --git a/PIL/ImageCms.py b/PIL/ImageCms.py index 363650250..04a3f7f61 100644 --- a/PIL/ImageCms.py +++ b/PIL/ImageCms.py @@ -1,19 +1,19 @@ -# -# The Python Imaging Library. -# $Id$ -# -# optional color managment support, based on Kevin Cazabon's PyCMS -# library. -# -# History: -# 2009-03-08 fl Added to PIL. -# -# Copyright (C) 2002-2003 Kevin Cazabon -# Copyright (c) 2009 by Fredrik Lundh -# -# See the README file for information on usage and redistribution. See -# below for the original description. -# +## The Python Imaging Library. +## $Id$ + +## Optional color managment support, based on Kevin Cazabon's PyCMS +## library. + +## History: + +## 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 + +## See the README file for information on usage and redistribution. See +## below for the original description. from __future__ import print_function @@ -66,7 +66,8 @@ pyCMS Added try/except statements arount type() checks of potential CObjects... Python won't let you use type() - on them, and raises a TypeError (stupid, if you ask me!) + on them, and raises a TypeError (stupid, if you ask + me!) Added buildProofTransformFromOpenProfiles() function. Additional fixes in DLL, see DLL code for details. @@ -88,9 +89,9 @@ try: from PIL import _imagingcms except ImportError as ex: # Allow error import for doc purposes, but error out when accessing - # anything in core. - from _util import deferred_error - _imagingcms = deferred_error(ex) + # anything in core. + from _util import import_err + _imagingcms = import_err(ex) from PIL._util import isStringType core = _imagingcms @@ -113,22 +114,24 @@ DIRECTION_PROOF = 2 FLAGS = { "MATRIXINPUT": 1, "MATRIXOUTPUT": 2, - "MATRIXONLY": (1|2), - "NOWHITEONWHITEFIXUP": 4, # Don't hot fix scum dot - "NOPRELINEARIZATION": 16, # Don't create prelinearization tables on precalculated transforms (internal use) - "GUESSDEVICECLASS": 32, # Guess device class (for transform2devicelink) - "NOTCACHE": 64, # Inhibit 1-pixel cache + "MATRIXONLY": (1 | 2), + "NOWHITEONWHITEFIXUP": 4, # Don't hot fix scum dot + # Don't create prelinearization tables on precalculated transforms + # (internal use): + "NOPRELINEARIZATION": 16, + "GUESSDEVICECLASS": 32, # Guess device class (for transform2devicelink) + "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 + "NULLTRANSFORM": 512, # Don't transform anyway + "HIGHRESPRECALC": 1024, # Use more memory to give better accurancy + "LOWRESPRECALC": 2048, # Use less memory to minimize resouces "WHITEBLACKCOMPENSATION": 8192, "BLACKPOINTCOMPENSATION": 8192, - "GAMUTCHECK": 4096, # Out of Gamut alarm - "SOFTPROOFING": 16384, # Do softproofing - "PRESERVEBLACK": 32768, # Black preservation - "NODEFAULTRESOURCEDEF": 16777216, # CRD special - "GRIDPOINTS": lambda n: ((n) & 0xFF) << 16 # Gridpoints + "GAMUTCHECK": 4096, # Out of Gamut alarm + "SOFTPROOFING": 16384, # Do softproofing + "PRESERVEBLACK": 32768, # Black preservation + "NODEFAULTRESOURCEDEF": 16777216, # CRD special + "GRIDPOINTS": lambda n: ((n) & 0xFF) << 16 # Gridpoints } _MAX_FLAG = 0 @@ -136,6 +139,7 @@ for flag in FLAGS.values(): if isinstance(flag, int): _MAX_FLAG = _MAX_FLAG | flag + # --------------------------------------------------------------------. # Experimental PIL-level API # --------------------------------------------------------------------. @@ -146,51 +150,71 @@ for flag in FLAGS.values(): class ImageCmsProfile: def __init__(self, profile): - # accepts a string (filename), a file-like object, or a low-level - # profile object + """ + :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"): self._set(core.profile_frombytes(profile.read())) else: - self._set(profile) # assume it's already a profile + self._set(profile) # assume it's already a profile def _set(self, profile, filename=None): self.profile = profile self.filename = filename if profile: - self.product_name = None #profile.product_name - self.product_info = None #profile.product_info + self.product_name = None # profile.product_name + self.product_info = None # profile.product_info else: self.product_name = None self.product_info = None + def tobytes(self): + """ + Returns the profile in a format suitable for embedding in + saved images. + + :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 - standard Image.point() method. - """ + + # Transform. This can be used with the procedural API, or with the + # standard Image.point() method. + # + # 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): + intent=INTENT_PERCEPTUAL, proof=None, + proof_intent=INTENT_ABSOLUTE_COLORIMETRIC, flags=0): if proof is None: self.transform = core.buildTransform( input.profile, output.profile, input_mode, output_mode, intent, flags - ) + ) else: self.transform = core.buildProofTransform( input.profile, output.profile, proof.profile, input_mode, output_mode, intent, proof_intent, flags - ) + ) # Note: inputMode and outputMode are for pyCMS compatibility only self.input_mode = self.inputMode = input_mode self.output_mode = self.outputMode = output_mode + self.output_profile = output + def point(self, im): return self.apply(im) @@ -198,21 +222,24 @@ class ImageCmsTransform(Image.ImagePointHandler): im.load() if imOut is None: imOut = Image.new(self.output_mode, im.size, None) - result = self.transform.apply(im.im.id, imOut.im.id) + self.transform.apply(im.im.id, imOut.im.id) + imOut.info['icc_profile'] = self.output_profile.tobytes() return imOut def apply_in_place(self, im): im.load() if im.mode != self.output_mode: - raise ValueError("mode mismatch") # wrong output mode - result = self.transform.apply(im.im.id, im.im.id) + raise ValueError("mode mismatch") # wrong output mode + self.transform.apply(im.im.id, im.im.id) + im.info['icc_profile'] = self.output_profile.tobytes() return im + def get_display_profile(handle=None): """ (experimental) Fetches the profile for the current display device. :returns: None if the profile is not known. """ - + import sys if sys.platform == "win32": from PIL import ImageWin @@ -229,15 +256,21 @@ def get_display_profile(handle=None): profile = get() return ImageCmsProfile(profile) + # --------------------------------------------------------------------. # pyCMS compatible layer # --------------------------------------------------------------------. class PyCMSError(Exception): - """ (pyCMS) Exception class. This is used for all errors in the pyCMS API. """ + + """ (pyCMS) Exception class. + This is used for all errors in the pyCMS API. """ pass -def profileToProfile(im, inputProfile, outputProfile, renderingIntent=INTENT_PERCEPTUAL, outputMode=None, inPlace=0, flags=0): + +def profileToProfile( + im, inputProfile, outputProfile, renderingIntent=INTENT_PERCEPTUAL, + outputMode=None, inPlace=0, flags=0): """ (pyCMS) Applies an ICC transformation to a given image, mapping from inputProfile to outputProfile. @@ -259,40 +292,45 @@ def profileToProfile(im, inputProfile, outputProfile, renderingIntent=INTENT_PER profiles, the input profile must handle RGB data, and the output profile must handle CMYK data. - :param im: An open PIL image object (i.e. Image.new(...) or Image.open(...), etc.) - :param inputProfile: String, as a valid filename path to the ICC input profile - you wish to use for this image, or a profile object + :param im: An open PIL image object (i.e. Image.new(...) or + Image.open(...), etc.) + :param inputProfile: String, as a valid filename path to the ICC input + profile you wish to use for this image, or a profile object :param outputProfile: String, as a valid filename path to the ICC output profile you wish to use for this image, or a profile object - :param renderingIntent: Integer (0-3) specifying the rendering intent you wish - to use for the transform + :param renderingIntent: Integer (0-3) specifying the rendering intent you + wish to use for the transform INTENT_PERCEPTUAL = 0 (DEFAULT) (ImageCms.INTENT_PERCEPTUAL) INTENT_RELATIVE_COLORIMETRIC = 1 (ImageCms.INTENT_RELATIVE_COLORIMETRIC) INTENT_SATURATION = 2 (ImageCms.INTENT_SATURATION) INTENT_ABSOLUTE_COLORIMETRIC = 3 (ImageCms.INTENT_ABSOLUTE_COLORIMETRIC) - see the pyCMS documentation for details on rendering intents and what they do. - :param outputMode: A valid PIL mode for the output image (i.e. "RGB", "CMYK", - etc.). Note: if rendering the image "inPlace", outputMode MUST be the - same mode as the input, or omitted completely. If omitted, the outputMode - will be the same as the mode of the input image (im.mode) - :param inPlace: Boolean (1 = True, None or 0 = False). If True, the original - image is modified in-place, and None is returned. If False (default), a - new Image object is returned with the transform applied. + see the pyCMS documentation for details on rendering intents and what + they do. + :param outputMode: A valid PIL mode for the output image (i.e. "RGB", + "CMYK", etc.). Note: if rendering the image "inPlace", outputMode + MUST be the same mode as the input, or omitted completely. If + omitted, the outputMode will be the same as the mode of the input + image (im.mode) + :param inPlace: Boolean (1 = True, None or 0 = False). If True, the + original image is modified in-place, and None is returned. If False + (default), a new Image object is returned with the transform applied. :param flags: Integer (0-...) specifying additional flags - :returns: Either None or a new PIL image object, depending on value of inPlace + :returns: Either None or a new PIL image object, depending on value of + inPlace :exception PyCMSError: """ if outputMode is None: outputMode = im.mode - if not isinstance(renderingIntent, int) or not (0 <= renderingIntent <=3): + if not isinstance(renderingIntent, int) or not (0 <= renderingIntent <= 3): raise PyCMSError("renderingIntent must be an integer between 0 and 3") if not isinstance(flags, int) or not (0 <= flags <= _MAX_FLAG): - raise PyCMSError("flags must be an integer between 0 and %s" + _MAX_FLAG) + raise PyCMSError( + "flags must be an integer between 0 and %s" + _MAX_FLAG) try: if not isinstance(inputProfile, ImageCmsProfile): @@ -300,8 +338,9 @@ def profileToProfile(im, inputProfile, outputProfile, renderingIntent=INTENT_PER if not isinstance(outputProfile, ImageCmsProfile): outputProfile = ImageCmsProfile(outputProfile) transform = ImageCmsTransform( - inputProfile, outputProfile, im.mode, outputMode, renderingIntent, flags=flags - ) + inputProfile, outputProfile, im.mode, outputMode, + renderingIntent, flags=flags + ) if inPlace: transform.apply_in_place(im) imOut = None @@ -323,8 +362,8 @@ def getOpenProfile(profileFilename): If profileFilename is not a vaild filename for an ICC profile, a PyCMSError will be raised. - :param profileFilename: String, as a valid filename path to the ICC profile you - wish to open, or a file-like object. + :param profileFilename: String, as a valid filename path to the ICC profile + you wish to open, or a file-like object. :returns: A CmsProfile class object. :exception PyCMSError: """ @@ -334,7 +373,10 @@ def getOpenProfile(profileFilename): except (IOError, TypeError, ValueError) as v: raise PyCMSError(v) -def buildTransform(inputProfile, outputProfile, inMode, outMode, renderingIntent=INTENT_PERCEPTUAL, flags=0): + +def buildTransform( + inputProfile, outputProfile, inMode, outMode, + renderingIntent=INTENT_PERCEPTUAL, flags=0): """ (pyCMS) Builds an ICC transform mapping from the inputProfile to the outputProfile. Use applyTransform to apply the transform to a given @@ -367,14 +409,14 @@ def buildTransform(inputProfile, outputProfile, inMode, outMode, renderingIntent manually overridden if you really want to, but I don't know of any time that would be of use, or would even work). - :param inputProfile: String, as a valid filename path to the ICC input profile - you wish to use for this transform, or a profile object + :param inputProfile: String, as a valid filename path to the ICC input + profile you wish to use for this transform, or a profile object :param outputProfile: String, as a valid filename path to the ICC output profile you wish to use for this transform, or a profile object - :param inMode: String, as a valid PIL mode that the appropriate profile also - supports (i.e. "RGB", "RGBA", "CMYK", etc.) - :param outMode: String, as a valid PIL mode that the appropriate profile also - supports (i.e. "RGB", "RGBA", "CMYK", etc.) + :param inMode: String, as a valid PIL mode that the appropriate profile + also supports (i.e. "RGB", "RGBA", "CMYK", etc.) + :param outMode: String, as a valid PIL mode that the appropriate profile + also supports (i.e. "RGB", "RGBA", "CMYK", etc.) :param renderingIntent: Integer (0-3) specifying the rendering intent you wish to use for the transform @@ -383,28 +425,37 @@ def buildTransform(inputProfile, outputProfile, inMode, outMode, renderingIntent INTENT_SATURATION = 2 (ImageCms.INTENT_SATURATION) INTENT_ABSOLUTE_COLORIMETRIC = 3 (ImageCms.INTENT_ABSOLUTE_COLORIMETRIC) - see the pyCMS documentation for details on rendering intents and what they do. + see the pyCMS documentation for details on rendering intents and what + they do. :param flags: Integer (0-...) specifying additional flags :returns: A CmsTransform class object. :exception PyCMSError: """ - if not isinstance(renderingIntent, int) or not (0 <= renderingIntent <=3): + if not isinstance(renderingIntent, int) or not (0 <= renderingIntent <= 3): raise PyCMSError("renderingIntent must be an integer between 0 and 3") if not isinstance(flags, int) or not (0 <= flags <= _MAX_FLAG): - raise PyCMSError("flags must be an integer between 0 and %s" + _MAX_FLAG) + raise PyCMSError( + "flags must be an integer between 0 and %s" + _MAX_FLAG) try: if not isinstance(inputProfile, ImageCmsProfile): inputProfile = ImageCmsProfile(inputProfile) if not isinstance(outputProfile, ImageCmsProfile): outputProfile = ImageCmsProfile(outputProfile) - return ImageCmsTransform(inputProfile, outputProfile, inMode, outMode, renderingIntent, flags=flags) + return ImageCmsTransform( + inputProfile, outputProfile, inMode, outMode, + renderingIntent, flags=flags) except (IOError, TypeError, ValueError) as v: raise PyCMSError(v) -def buildProofTransform(inputProfile, outputProfile, proofProfile, inMode, outMode, renderingIntent=INTENT_PERCEPTUAL, proofRenderingIntent=INTENT_ABSOLUTE_COLORIMETRIC, flags=FLAGS["SOFTPROOFING"]): + +def buildProofTransform( + inputProfile, outputProfile, proofProfile, inMode, outMode, + renderingIntent=INTENT_PERCEPTUAL, + proofRenderingIntent=INTENT_ABSOLUTE_COLORIMETRIC, + flags=FLAGS["SOFTPROOFING"]): """ (pyCMS) Builds an ICC transform mapping from the inputProfile to the outputProfile, but tries to simulate the result that would be @@ -443,17 +494,17 @@ def buildProofTransform(inputProfile, outputProfile, proofProfile, inMode, outMo when the simulated device has a much wider gamut than the output device, you may obtain marginal results. - :param inputProfile: String, as a valid filename path to the ICC input profile - you wish to use for this transform, or a profile object + :param inputProfile: String, as a valid filename path to the ICC input + profile you wish to use for this transform, or a profile object :param outputProfile: String, as a valid filename path to the ICC output (monitor, usually) profile you wish to use for this transform, or a profile object - :param proofProfile: String, as a valid filename path to the ICC proof profile - you wish to use for this transform, or a profile object - :param inMode: String, as a valid PIL mode that the appropriate profile also - supports (i.e. "RGB", "RGBA", "CMYK", etc.) - :param outMode: String, as a valid PIL mode that the appropriate profile also - supports (i.e. "RGB", "RGBA", "CMYK", etc.) + :param proofProfile: String, as a valid filename path to the ICC proof + profile you wish to use for this transform, or a profile object + :param inMode: String, as a valid PIL mode that the appropriate profile + also supports (i.e. "RGB", "RGBA", "CMYK", etc.) + :param outMode: String, as a valid PIL mode that the appropriate profile + also supports (i.e. "RGB", "RGBA", "CMYK", etc.) :param renderingIntent: Integer (0-3) specifying the rendering intent you wish to use for the input->proof (simulated) transform @@ -462,7 +513,8 @@ def buildProofTransform(inputProfile, outputProfile, proofProfile, inMode, outMo INTENT_SATURATION = 2 (ImageCms.INTENT_SATURATION) INTENT_ABSOLUTE_COLORIMETRIC = 3 (ImageCms.INTENT_ABSOLUTE_COLORIMETRIC) - see the pyCMS documentation for details on rendering intents and what they do. + see the pyCMS documentation for details on rendering intents and what + they do. :param proofRenderingIntent: Integer (0-3) specifying the rendering intent you wish to use for proof->output transform @@ -471,17 +523,19 @@ def buildProofTransform(inputProfile, outputProfile, proofProfile, inMode, outMo INTENT_SATURATION = 2 (ImageCms.INTENT_SATURATION) INTENT_ABSOLUTE_COLORIMETRIC = 3 (ImageCms.INTENT_ABSOLUTE_COLORIMETRIC) - see the pyCMS documentation for details on rendering intents and what they do. + see the pyCMS documentation for details on rendering intents and what + they do. :param flags: Integer (0-...) specifying additional flags :returns: A CmsTransform class object. :exception PyCMSError: """ - - if not isinstance(renderingIntent, int) or not (0 <= renderingIntent <=3): + + if not isinstance(renderingIntent, int) or not (0 <= renderingIntent <= 3): raise PyCMSError("renderingIntent must be an integer between 0 and 3") if not isinstance(flags, int) or not (0 <= flags <= _MAX_FLAG): - raise PyCMSError("flags must be an integer between 0 and %s" + _MAX_FLAG) + raise PyCMSError( + "flags must be an integer between 0 and %s" + _MAX_FLAG) try: if not isinstance(inputProfile, ImageCmsProfile): @@ -490,13 +544,16 @@ def buildProofTransform(inputProfile, outputProfile, proofProfile, inMode, outMo outputProfile = ImageCmsProfile(outputProfile) if not isinstance(proofProfile, ImageCmsProfile): proofProfile = ImageCmsProfile(proofProfile) - return ImageCmsTransform(inputProfile, outputProfile, inMode, outMode, renderingIntent, proofProfile, proofRenderingIntent, flags) + return ImageCmsTransform( + inputProfile, outputProfile, inMode, outMode, renderingIntent, + proofProfile, proofRenderingIntent, flags) except (IOError, TypeError, ValueError) as v: raise PyCMSError(v) buildTransformFromOpenProfiles = buildTransform buildProofTransformFromOpenProfiles = buildProofTransform + def applyTransform(im, transform, inPlace=0): """ (pyCMS) Applies a transform to a given image. @@ -514,8 +571,8 @@ def applyTransform(im, transform, inPlace=0): is raised. 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 + 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. If you want to modify im in-place instead of receiving a new image as @@ -528,10 +585,12 @@ def applyTransform(im, transform, inPlace=0): :param im: A PIL Image object, and im.mode must be the same as the inMode supported by the transform. :param transform: A valid CmsTransform class object - :param inPlace: Bool (1 == True, 0 or None == False). If True, im is modified - in place and None is returned, if False, a new Image object with the - transform applied is returned (and im is not changed). The default is False. - :returns: Either None, or a new PIL Image object, depending on the value of inPlace + :param inPlace: Bool (1 == True, 0 or None == False). If True, im is + modified in place and None is returned, if False, a new Image object + with the transform applied is returned (and im is not changed). The + default is False. + :returns: Either None, or a new PIL Image object, depending on the value of + inPlace. The profile will be returned in the image's info['icc_profile']. :exception PyCMSError: """ @@ -546,6 +605,7 @@ def applyTransform(im, transform, inPlace=0): return imOut + def createProfile(colorSpace, colorTemp=-1): """ (pyCMS) Creates a profile. @@ -562,36 +622,42 @@ def createProfile(colorSpace, colorTemp=-1): ImageCms.buildTransformFromOpenProfiles() to create a transform to apply to images. - :param colorSpace: String, the color space of the profile you wish to create. + :param colorSpace: String, the color space of the profile you wish to + create. Currently only "LAB", "XYZ", and "sRGB" are supported. :param colorTemp: Positive integer for the white point for the profile, in degrees Kelvin (i.e. 5000, 6500, 9600, etc.). The default is for D50 - illuminant if omitted (5000k). colorTemp is ONLY applied to LAB profiles, - and is ignored for XYZ and sRGB. + illuminant if omitted (5000k). colorTemp is ONLY applied to LAB + profiles, and is ignored for XYZ and sRGB. :returns: A CmsProfile class object :exception PyCMSError: """ if colorSpace not in ["LAB", "XYZ", "sRGB"]: - raise PyCMSError("Color space not supported for on-the-fly profile creation (%s)" % colorSpace) + raise PyCMSError( + "Color space not supported for on-the-fly profile creation (%s)" + % colorSpace) if colorSpace == "LAB": try: colorTemp = float(colorTemp) except: - raise PyCMSError("Color temperature must be numeric, \"%s\" not valid" % colorTemp) + raise PyCMSError( + "Color temperature must be numeric, \"%s\" not valid" + % colorTemp) try: return core.createProfile(colorSpace, colorTemp) except (TypeError, ValueError) as v: raise PyCMSError(v) + def getProfileName(profile): """ (pyCMS) Gets the internal product name for the given profile. - If profile isn't a valid CmsProfile object or filename to a profile, + If profile isn't a valid CmsProfile object or filename to a profile, a PyCMSError is raised If an error occurs while trying to obtain the name tag, a PyCMSError is raised. @@ -600,10 +666,10 @@ def getProfileName(profile): profile was originally created. Sometimes this tag also contains additional information supplied by the creator. - :param profile: EITHER a valid CmsProfile object, OR a string of the filename - of an ICC profile. - :returns: A string containing the internal name of the profile as stored in an - ICC tag. + :param profile: EITHER a valid CmsProfile object, OR a string of the + filename of an ICC profile. + :returns: A string containing the internal name of the profile as stored + in an ICC tag. :exception PyCMSError: """ @@ -612,14 +678,14 @@ def getProfileName(profile): if not isinstance(profile, ImageCmsProfile): profile = ImageCmsProfile(profile) # do it in python, not c. - # // name was "%s - %s" (model, manufacturer) || Description , - # // but if the Model and Manufacturer were the same or the model + # // name was "%s - %s" (model, manufacturer) || Description , + # // but if the Model and Manufacturer were the same or the model # // was long, Just the model, in 1.x model = profile.profile.product_model manufacturer = profile.profile.product_manufacturer if not (model or manufacturer): - return profile.profile.product_description+"\n" + return profile.profile.product_description + "\n" if not manufacturer or len(model) > 30: return model + "\n" return "%s - %s\n" % (model, manufacturer) @@ -627,6 +693,7 @@ def getProfileName(profile): except (AttributeError, IOError, TypeError, ValueError) as v: raise PyCMSError(v) + def getProfileInfo(profile): """ (pyCMS) Gets the internal product information for the given profile. @@ -641,18 +708,19 @@ def getProfileInfo(profile): info tag. This often contains details about the profile, and how it was created, as supplied by the creator. - :param profile: EITHER a valid CmsProfile object, OR a string of the filename - of an ICC profile. - :returns: A string containing the internal profile information stored in an ICC - tag. + :param profile: EITHER a valid CmsProfile object, OR a string of the + filename of an ICC profile. + :returns: A string containing the internal profile information stored in + an ICC tag. :exception PyCMSError: """ - + try: if not isinstance(profile, ImageCmsProfile): profile = ImageCmsProfile(profile) # add an extra newline to preserve pyCMS compatibility - # Python, not C. the white point bits weren't working well, so skipping. + # Python, not C. the white point bits weren't working well, + # so skipping. # // info was description \r\n\r\n copyright \r\n\r\n K007 tag \r\n\r\n whitepoint description = profile.profile.product_description cpright = profile.profile.product_copyright @@ -660,7 +728,7 @@ def getProfileInfo(profile): for elt in (description, cpright): if elt: arr.append(elt) - return "\r\n\r\n".join(arr)+"\r\n\r\n" + return "\r\n\r\n".join(arr) + "\r\n\r\n" except (AttributeError, IOError, TypeError, ValueError) as v: raise PyCMSError(v) @@ -677,12 +745,12 @@ def getProfileCopyright(profile): is raised Use this function to obtain the information stored in the profile's - copyright tag. + copyright tag. - :param profile: EITHER a valid CmsProfile object, OR a string of the filename - of an ICC profile. - :returns: A string containing the internal profile information stored in an ICC - tag. + :param profile: EITHER a valid CmsProfile object, OR a string of the + filename of an ICC profile. + :returns: A string containing the internal profile information stored in + an ICC tag. :exception PyCMSError: """ try: @@ -693,6 +761,7 @@ def getProfileCopyright(profile): except (AttributeError, IOError, TypeError, ValueError) as v: raise PyCMSError(v) + def getProfileManufacturer(profile): """ (pyCMS) Gets the manufacturer for the given profile. @@ -700,16 +769,16 @@ def getProfileManufacturer(profile): If profile isn't a valid CmsProfile object or filename to a profile, a PyCMSError is raised. - If an error occurs while trying to obtain the manufacturer tag, a PyCMSError - is raised + If an error occurs while trying to obtain the manufacturer tag, a + PyCMSError is raised Use this function to obtain the information stored in the profile's - manufacturer tag. + manufacturer tag. - :param profile: EITHER a valid CmsProfile object, OR a string of the filename - of an ICC profile. - :returns: A string containing the internal profile information stored in an ICC - tag. + :param profile: EITHER a valid CmsProfile object, OR a string of the + filename of an ICC profile. + :returns: A string containing the internal profile information stored in + an ICC tag. :exception PyCMSError: """ try: @@ -720,23 +789,24 @@ def getProfileManufacturer(profile): except (AttributeError, IOError, TypeError, ValueError) as v: raise PyCMSError(v) + def getProfileModel(profile): """ (pyCMS) Gets the model for the given profile. - + If profile isn't a valid CmsProfile object or filename to a profile, a PyCMSError is raised. - + If an error occurs while trying to obtain the model tag, a PyCMSError is raised - + Use this function to obtain the information stored in the profile's - model tag. - - :param profile: EITHER a valid CmsProfile object, OR a string of the filename - of an ICC profile. - :returns: A string containing the internal profile information stored in an ICC - tag. + model tag. + + :param profile: EITHER a valid CmsProfile object, OR a string of the + filename of an ICC profile. + :returns: A string containing the internal profile information stored in + an ICC tag. :exception PyCMSError: """ @@ -748,6 +818,7 @@ def getProfileModel(profile): except (AttributeError, IOError, TypeError, ValueError) as v: raise PyCMSError(v) + def getProfileDescription(profile): """ (pyCMS) Gets the description for the given profile. @@ -759,12 +830,12 @@ def getProfileDescription(profile): is raised Use this function to obtain the information stored in the profile's - description tag. + description tag. - :param profile: EITHER a valid CmsProfile object, OR a string of the filename - of an ICC profile. - :returns: A string containing the internal profile information stored in an ICC - tag. + :param profile: EITHER a valid CmsProfile object, OR a string of the + filename of an ICC profile. + :returns: A string containing the internal profile information stored in an + ICC tag. :exception PyCMSError: """ @@ -793,16 +864,18 @@ def getDefaultIntent(profile): If you wish to use a different intent than returned, use ImageCms.isIntentSupported() to verify it will work first. - :param profile: EITHER a valid CmsProfile object, OR a string of the filename - of an ICC profile. - :returns: Integer 0-3 specifying the default rendering intent for this profile. + :param profile: EITHER a valid CmsProfile object, OR a string of the + filename of an ICC profile. + :returns: Integer 0-3 specifying the default rendering intent for this + profile. INTENT_PERCEPTUAL = 0 (DEFAULT) (ImageCms.INTENT_PERCEPTUAL) INTENT_RELATIVE_COLORIMETRIC = 1 (ImageCms.INTENT_RELATIVE_COLORIMETRIC) INTENT_SATURATION = 2 (ImageCms.INTENT_SATURATION) INTENT_ABSOLUTE_COLORIMETRIC = 3 (ImageCms.INTENT_ABSOLUTE_COLORIMETRIC) - see the pyCMS documentation for details on rendering intents and what they do. + see the pyCMS documentation for details on rendering intents and what + they do. :exception PyCMSError: """ @@ -813,6 +886,7 @@ def getDefaultIntent(profile): except (AttributeError, IOError, TypeError, ValueError) as v: raise PyCMSError(v) + def isIntentSupported(profile, intent, direction): """ (pyCMS) Checks if a given intent is supported. @@ -822,23 +896,24 @@ def isIntentSupported(profile, intent, direction): input/output/proof profile as you desire. Some profiles are created specifically for one "direction", can cannot - be used for others. Some profiles can only be used for certain + be used for others. Some profiles can only be used for certain rendering intents... so it's best to either verify this before trying to create a transform with them (using this function), or catch the potential PyCMSError that will occur if they don't support the modes you select. - :param profile: EITHER a valid CmsProfile object, OR a string of the filename - of an ICC profile. - :param intent: Integer (0-3) specifying the rendering intent you wish to use - with this profile + :param profile: EITHER a valid CmsProfile object, OR a string of the + filename of an ICC profile. + :param intent: Integer (0-3) specifying the rendering intent you wish to + use with this profile INTENT_PERCEPTUAL = 0 (DEFAULT) (ImageCms.INTENT_PERCEPTUAL) INTENT_RELATIVE_COLORIMETRIC = 1 (ImageCms.INTENT_RELATIVE_COLORIMETRIC) INTENT_SATURATION = 2 (ImageCms.INTENT_SATURATION) INTENT_ABSOLUTE_COLORIMETRIC = 3 (ImageCms.INTENT_ABSOLUTE_COLORIMETRIC) - see the pyCMS documentation for details on rendering intents and what they do. + 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, output, or proof @@ -862,15 +937,17 @@ def isIntentSupported(profile, intent, direction): except (AttributeError, IOError, TypeError, ValueError) as v: raise PyCMSError(v) + def versions(): """ (pyCMS) Fetches versions. """ - + import sys return ( - VERSION, core.littlecms_version, sys.version.split()[0], Image.VERSION - ) + VERSION, core.littlecms_version, + sys.version.split()[0], Image.VERSION + ) # -------------------------------------------------------------------- @@ -880,14 +957,16 @@ if __name__ == "__main__": from PIL import ImageCms print(__doc__) - for f in dir(pyCMS): - print("="*80) - print("%s" %f) - + for f in dir(ImageCms): + doc = None try: - exec ("doc = ImageCms.%s.__doc__" %(f)) + exec("doc = %s.__doc__" % (f)) if "pyCMS" in doc: # so we don't get the __doc__ string for imported modules + print("=" * 80) + print("%s" % f) print(doc) - except AttributeError: + except (AttributeError, TypeError): pass + +# End of file diff --git a/PIL/ImageEnhance.py b/PIL/ImageEnhance.py index 10433343e..f802dc1d3 100644 --- a/PIL/ImageEnhance.py +++ b/PIL/ImageEnhance.py @@ -6,7 +6,7 @@ # # For a background, see "Image Processing By Interpolation and # Extrapolation", Paul Haeberli and Douglas Voorhies. Available -# at http://www.sgi.com/grafica/interp/index.html +# at http://www.graficaobscura.com/interp/index.html # # History: # 1996-03-23 fl Created diff --git a/PIL/ImageFile.py b/PIL/ImageFile.py index 501e16b00..5e4745d76 100644 --- a/PIL/ImageFile.py +++ b/PIL/ImageFile.py @@ -133,11 +133,27 @@ class ImageFile(Image.Image): return pixel 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') + readonly = 0 - if self.filename and len(self.tile) == 1 and not hasattr(sys, 'pypy_version_info'): - # As of pypy 2.1.0, memory mapping was failing here. + # look for read/seek overrides + try: + read = self.load_read + # don't use mmap if there are custom read/seek functions + use_mmap = False + except AttributeError: + read = self.fp.read + + try: + seek = self.load_seek + use_mmap = False + except AttributeError: + seek = self.fp.seek + + if use_mmap: # try memory mapping d, e, o, a = self.tile[0] if d == "raw" and a[0] == self.mode and a[0] in Image._MAPMODES: @@ -165,19 +181,7 @@ class ImageFile(Image.Image): self.load_prepare() - # look for read/seek overrides - try: - read = self.load_read - except AttributeError: - read = self.fp.read - - try: - seek = self.load_seek - except AttributeError: - seek = self.fp.seek - if not self.map: - # sort tiles in file order self.tile.sort(key=_tilesort) @@ -502,5 +506,5 @@ def _safe_read(fp, size): if not block: break data.append(block) - size = size - len(block) + size -= len(block) return b"".join(data) diff --git a/PIL/ImageFont.py b/PIL/ImageFont.py index 9366937dd..bd52ea938 100644 --- a/PIL/ImageFont.py +++ b/PIL/ImageFont.py @@ -29,13 +29,15 @@ from __future__ import print_function from PIL import Image from PIL._util import isDirectory, isPath -import os, sys +import os +import sys try: import warnings except ImportError: warnings = None + class _imagingft_not_installed: # module placeholder def __getattr__(self, id): @@ -90,8 +92,8 @@ class ImageFont: # read PILfont header if file.readline() != b"PILfont\n": raise SyntaxError("Not a PILfont file") - d = file.readline().split(b";") - self.info = [] # FIXME: should be a dictionary + file.readline().split(b";") + self.info = [] # FIXME: should be a dictionary while True: s = file.readline() if not s or s == b"DATA\n": @@ -113,6 +115,7 @@ class ImageFont: self.getsize = self.font.getsize self.getmask = self.font.getmask + ## # Wrapper for FreeType fonts. Application code should use the # truetype factory function to create font objects. @@ -124,14 +127,18 @@ class FreeTypeFont: # FIXME: use service provider instead if file: if warnings: - warnings.warn('file parameter deprecated, please use font parameter instead.', DeprecationWarning) + warnings.warn( + 'file parameter deprecated, ' + 'please use font parameter instead.', + DeprecationWarning) font = file if isPath(font): self.font = core.getfont(font, size, index, encoding) else: self.font_bytes = font.read() - self.font = core.getfont("", size, index, encoding, self.font_bytes) + self.font = core.getfont( + "", size, index, encoding, self.font_bytes) def getname(self): return self.font.family, self.font.style @@ -140,7 +147,8 @@ class FreeTypeFont: return self.font.ascent, self.font.descent def getsize(self, text): - return self.font.getsize(text)[0] + size, offset = self.font.getsize(text) + return (size[0] + offset[0], size[1] + offset[1]) def getoffset(self, text): return self.font.getsize(text)[1] @@ -151,7 +159,7 @@ class FreeTypeFont: def getmask2(self, text, mode="", fill=Image.core.fill): size, offset = self.font.getsize(text) im = fill("L", size, 0) - self.font.render(text, im.id, mode=="1") + self.font.render(text, im.id, mode == "1") return im, offset ## @@ -163,12 +171,13 @@ class FreeTypeFont: # be one of Image.FLIP_LEFT_RIGHT, Image.FLIP_TOP_BOTTOM, # Image.ROTATE_90, Image.ROTATE_180, or Image.ROTATE_270. + class TransposedFont: "Wrapper for writing rotated or mirrored text" def __init__(self, font, orientation=None): self.font = font - self.orientation = orientation # any 'transpose' argument, or None + self.orientation = orientation # any 'transpose' argument, or None def getsize(self, text): w, h = self.font.getsize(text) @@ -221,7 +230,10 @@ def truetype(font=None, size=10, index=0, encoding="", filename=None): if filename: if warnings: - warnings.warn('filename parameter deprecated, please use font parameter instead.', DeprecationWarning) + warnings.warn( + 'filename parameter deprecated, ' + 'please use font parameter instead.', + DeprecationWarning) font = filename try: @@ -272,8 +284,8 @@ def load_default(): import base64 f = ImageFont() f._load_pilfont_data( - # courB08 - BytesIO(base64.decodestring(b''' + # courB08 + BytesIO(base64.decodestring(b''' UElMZm9udAo7Ozs7OzsxMDsKREFUQQoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA @@ -392,15 +404,4 @@ w7IkEbzhVQAAAABJRU5ErkJggg== ''')))) return f - -if __name__ == "__main__": - # create font data chunk for embedding - import base64, os, sys - font = "../Images/courB08" - print(" f._load_pilfont_data(") - print(" # %s" % os.path.basename(font)) - print(" BytesIO(base64.decodestring(b'''") - base64.encode(open(font + ".pil", "rb"), sys.stdout) - print("''')), Image.open(BytesIO(base64.decodestring(b'''") - base64.encode(open(font + ".pbm", "rb"), sys.stdout) - print("'''))))") +# End of file diff --git a/PIL/ImageMath.py b/PIL/ImageMath.py index b2355ed1d..4dcc5125c 100644 --- a/PIL/ImageMath.py +++ b/PIL/ImageMath.py @@ -17,7 +17,6 @@ from PIL import Image from PIL import _imagingmath -import sys try: import builtins @@ -27,9 +26,11 @@ except ImportError: VERBOSE = 0 + def _isconstant(v): return isinstance(v, int) or isinstance(v, float) + class _Operand: # wraps an image operand, providing standard operators @@ -69,20 +70,25 @@ class _Operand: im2 = self.__fixup(im2) if im1.mode != im2.mode: # convert both arguments to floating point - if im1.mode != "F": im1 = im1.convert("F") - if im2.mode != "F": im2 = im2.convert("F") + if im1.mode != "F": + im1 = im1.convert("F") + if im2.mode != "F": + im2 = im2.convert("F") if im1.mode != im2.mode: raise ValueError("mode mismatch") if im1.size != im2.size: # crop both arguments to a common size size = (min(im1.size[0], im2.size[0]), min(im1.size[1], im2.size[1])) - if im1.size != size: im1 = im1.crop((0, 0) + size) - if im2.size != size: im2 = im2.crop((0, 0) + size) + if im1.size != size: + im1 = im1.crop((0, 0) + size) + if im2.size != size: + im2 = im2.crop((0, 0) + size) out = Image.new(mode or im1.mode, size, None) else: out = Image.new(mode or im1.mode, im1.size, None) - im1.load(); im2.load() + im1.load() + im2.load() try: op = getattr(_imagingmath, op+"_"+im1.mode) except AttributeError: @@ -102,34 +108,47 @@ class _Operand: def __abs__(self): return self.apply("abs", self) + def __pos__(self): return self + def __neg__(self): return self.apply("neg", self) # binary operators def __add__(self, other): return self.apply("add", self, other) + def __radd__(self, other): return self.apply("add", other, self) + def __sub__(self, other): return self.apply("sub", self, other) + def __rsub__(self, other): return self.apply("sub", other, self) + def __mul__(self, other): return self.apply("mul", self, other) + def __rmul__(self, other): return self.apply("mul", other, self) + def __truediv__(self, other): return self.apply("div", self, other) + def __rtruediv__(self, other): return self.apply("div", other, self) + def __mod__(self, other): return self.apply("mod", self, other) + def __rmod__(self, other): return self.apply("mod", other, self) + def __pow__(self, other): return self.apply("pow", self, other) + def __rpow__(self, other): return self.apply("pow", other, self) @@ -143,54 +162,77 @@ class _Operand: # bitwise def __invert__(self): return self.apply("invert", self) + def __and__(self, other): return self.apply("and", self, other) + def __rand__(self, other): return self.apply("and", other, self) + def __or__(self, other): return self.apply("or", self, other) + def __ror__(self, other): return self.apply("or", other, self) + def __xor__(self, other): return self.apply("xor", self, other) + def __rxor__(self, other): return self.apply("xor", other, self) + def __lshift__(self, other): return self.apply("lshift", self, other) + def __rshift__(self, other): return self.apply("rshift", self, other) # logical def __eq__(self, other): return self.apply("eq", self, other) + def __ne__(self, other): return self.apply("ne", self, other) + def __lt__(self, other): return self.apply("lt", self, other) + def __le__(self, other): return self.apply("le", self, other) + def __gt__(self, other): return self.apply("gt", self, other) + def __ge__(self, other): return self.apply("ge", self, other) + # conversions def imagemath_int(self): return _Operand(self.im.convert("I")) + + def imagemath_float(self): return _Operand(self.im.convert("F")) + # logical def imagemath_equal(self, other): return self.apply("eq", self, other, mode="I") + + def imagemath_notequal(self, other): return self.apply("ne", self, other, mode="I") + def imagemath_min(self, other): return self.apply("min", self, other) + + def imagemath_max(self, other): return self.apply("max", self, other) + def imagemath_convert(self, mode): return _Operand(self.im.convert(mode)) diff --git a/PIL/ImageMorph.py b/PIL/ImageMorph.py new file mode 100644 index 000000000..3f15621a6 --- /dev/null +++ b/PIL/ImageMorph.py @@ -0,0 +1,245 @@ +# A binary morphology add-on for the Python Imaging Library +# +# History: +# 2014-06-04 Initial version. +# +# Copyright (c) 2014 Dov Grobgeld + +from PIL import Image +from PIL import _imagingmorph +import re + +LUT_SIZE = 1 << 9 + + +class LutBuilder: + """A class for building a MorphLut from a descriptive language + + The input patterns is a list of a strings sequences like these:: + + 4:(... + .1. + 111)->1 + + (whitespaces including linebreaks are ignored). The option 4 + describes a series of symmetry operations (in this case a + 4-rotation), the pattern is described by: + + - . or X - Ignore + - 1 - Pixel is on + - 0 - Pixel is off + + The result of the operation is described after "->" string. + + The default is to return the current pixel value, which is + 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() + + """ + def __init__(self, patterns=None, op_name=None): + if patterns is not None: + self.patterns = patterns + else: + self.patterns = [] + self.lut = None + if op_name is not None: + known_patterns = { + 'corner': ['1:(... ... ...)->0', + '4:(00. 01. ...)->1'], + 'dilation4': ['4:(... .0. .1.)->1'], + 'dilation8': ['4:(... .0. .1.)->1', + '4:(... .0. ..1)->1'], + 'erosion4': ['4:(... .1. .0.)->0'], + 'erosion8': ['4:(... .1. .0.)->0', + '4:(... .1. ..0)->0'], + 'edge': ['1:(... ... ...)->0', + '4:(.0. .1. ...)->1', + '4:(01. .1. ...)->1'] + } + if op_name not in known_patterns: + raise Exception('Unknown pattern '+op_name+'!') + + self.patterns = known_patterns[op_name] + + def add_patterns(self, patterns): + self.patterns += patterns + + def build_default_lut(self): + symbols = [0, 1] + m = 1 << 4 # pos of current pixel + self.lut = bytearray([symbols[(i & m) > 0] for i in range(LUT_SIZE)]) + + def get_lut(self): + return self.lut + + def _string_permute(self, pattern, permutation): + """string_permute takes a pattern and a permutation and returns the + string permuted according to the permutation list. + """ + assert(len(permutation) == 9) + return ''.join([pattern[p] for p in permutation]) + + def _pattern_permute(self, basic_pattern, options, basic_result): + """pattern_permute takes a basic pattern and its result and clones + the pattern according to the modifications described in the $options + parameter. It returns a list of all cloned patterns.""" + patterns = [(basic_pattern, basic_result)] + + # rotations + if '4' in options: + res = patterns[-1][1] + for i in range(4): + patterns.append( + (self._string_permute(patterns[-1][0], [6, 3, 0, + 7, 4, 1, + 8, 5, 2]), res)) + # mirror + if 'M' in options: + n = len(patterns) + for pattern, res in patterns[0:n]: + patterns.append( + (self._string_permute(pattern, [2, 1, 0, + 5, 4, 3, + 8, 7, 6]), res)) + + # negate + if 'N' in options: + n = len(patterns) + for pattern, res in patterns[0:n]: + # Swap 0 and 1 + pattern = (pattern + .replace('0', 'Z') + .replace('1', '0') + .replace('Z', '1')) + res = '%d' % (1-int(res)) + patterns.append((pattern, res)) + + return patterns + + def build_lut(self): + """Compile all patterns into a morphology lut. + + TBD :Build based on (file) morphlut:modify_lut + """ + self.build_default_lut() + patterns = [] + + # Parse and create symmetries of the patterns strings + for p in self.patterns: + m = re.search( + r'(\w*):?\s*\((.+?)\)\s*->\s*(\d)', p.replace('\n', '')) + if not m: + raise Exception('Syntax error in pattern "'+p+'"') + options = m.group(1) + pattern = m.group(2) + result = int(m.group(3)) + + # Get rid of spaces + pattern = pattern.replace(' ', '').replace('\n', '') + + patterns += self._pattern_permute(pattern, options, result) + +# # Debugging +# for p,r in patterns: +# print p,r +# print '--' + + # compile the patterns into regular expressions for speed + for i in range(len(patterns)): + p = patterns[i][0].replace('.', 'X').replace('X', '[01]') + p = re.compile(p) + patterns[i] = (p, patterns[i][1]) + + # Step through table and find patterns that match. + # Note that all the patterns are searched. The last one + # caught overrides + for i in range(LUT_SIZE): + # Build the bit pattern + bitpattern = bin(i)[2:] + bitpattern = ('0'*(9-len(bitpattern)) + bitpattern)[::-1] + + for p, r in patterns: + if p.match(bitpattern): + self.lut[i] = [0, 1][r] + + return self.lut + + +class MorphOp: + """A class for binary morphological operators""" + + def __init__(self, + lut=None, + op_name=None, + patterns=None): + """Create a binary morphological operator""" + self.lut = lut + if op_name is not None: + self.lut = LutBuilder(op_name=op_name).build_lut() + elif patterns is not None: + self.lut = LutBuilder(patterns=patterns).build_lut() + + def apply(self, image): + """Run a single morphological operation on an image + + Returns a tuple of the number of changed pixels and the + morphed image""" + if self.lut is None: + raise Exception('No operator loaded') + + outimage = Image.new(image.mode, image.size, None) + count = _imagingmorph.apply( + bytes(self.lut), image.im.id, outimage.im.id) + return count, outimage + + def match(self, image): + """Get a list of coordinates matching the morphological operation on + an image. + + Returns a list of tuples of (x,y) coordinates + of all matching pixels.""" + if self.lut is None: + raise Exception('No operator loaded') + + return _imagingmorph.match(bytes(self.lut), image.im.id) + + def get_on_pixels(self, image): + """Get a list of all turned on pixels in a binary image + + Returns a list of tuples of (x,y) coordinates + of all matching pixels.""" + + return _imagingmorph.get_on_pixels(image.im.id) + + def load_lut(self, filename): + """Load an operator from an mrl file""" + with open(filename, 'rb') as f: + self.lut = bytearray(f.read()) + + if len(self.lut) != 8192: + self.lut = None + raise Exception('Wrong size operator file!') + + def save_lut(self, filename): + """Save an operator to an mrl file""" + if self.lut is None: + raise Exception('No operator loaded') + with open(filename, 'wb') as f: + f.write(self.lut) + + def set_lut(self, lut): + """Set the lut from an external source""" + self.lut = lut + +# End of file diff --git a/PIL/ImageOps.py b/PIL/ImageOps.py index 0d22f8c64..9f84eff86 100644 --- a/PIL/ImageOps.py +++ b/PIL/ImageOps.py @@ -94,7 +94,7 @@ def autocontrast(image, cutoff=0, ignore=None): cut = cut - h[lo] h[lo] = 0 else: - h[lo] = h[lo] - cut + h[lo] -= cut cut = 0 if cut <= 0: break @@ -105,7 +105,7 @@ def autocontrast(image, cutoff=0, ignore=None): cut = cut - h[hi] h[hi] = 0 else: - h[hi] = h[hi] - cut + h[hi] -= cut cut = 0 if cut <= 0: break @@ -392,7 +392,7 @@ def solarize(image, threshold=128): """ Invert all pixel values above a threshold. - :param image: The image to posterize. + :param image: The image to solarize. :param threshold: All pixels above this greyscale level are inverted. :return: An image. """ diff --git a/PIL/ImagePalette.py b/PIL/ImagePalette.py index d5b9d04eb..62f8814f1 100644 --- a/PIL/ImagePalette.py +++ b/PIL/ImagePalette.py @@ -17,19 +17,20 @@ # import array -from PIL import Image, ImageColor +import warnings +from PIL import ImageColor class ImagePalette: "Color palette for palette mapped images" - def __init__(self, mode = "RGB", palette = None, size = 0): + def __init__(self, mode="RGB", palette=None, size=0): self.mode = mode - self.rawmode = None # if set, palette contains raw data + self.rawmode = None # if set, palette contains raw data self.palette = palette or list(range(256))*len(self.mode) self.colors = {} self.dirty = None - if ((size == 0 and len(self.mode)*256 != len(self.palette)) or + if ((size == 0 and len(self.mode)*256 != len(self.palette)) or (size != 0 and size != len(self.palette))): raise ValueError("wrong palette size") @@ -55,7 +56,7 @@ class ImagePalette: return self.palette arr = array.array("B", self.palette) if hasattr(arr, 'tobytes'): - #py3k has a tobytes, tostring is deprecated. + # py3k has a tobytes, tostring is deprecated. return arr.tobytes() return arr.tostring() @@ -101,11 +102,15 @@ class ImagePalette: fp.write("# Mode: %s\n" % self.mode) for i in range(256): fp.write("%d" % i) - for j in range(i, len(self.palette), 256): - fp.write(" %d" % self.palette[j]) + for j in range(i*len(self.mode), (i+1)*len(self.mode)): + try: + fp.write(" %d" % self.palette[j]) + except IndexError: + fp.write(" 0") fp.write("\n") fp.close() + # -------------------------------------------------------------------- # Internal @@ -116,32 +121,53 @@ def raw(rawmode, data): palette.dirty = 1 return palette + # -------------------------------------------------------------------- # Factories def _make_linear_lut(black, white): + warnings.warn( + '_make_linear_lut() is deprecated. ' + 'Please call make_linear_lut() instead.', + DeprecationWarning, + stacklevel=2 + ) + return make_linear_lut(black, white) + + +def _make_gamma_lut(exp): + warnings.warn( + '_make_gamma_lut() is deprecated. ' + 'Please call make_gamma_lut() instead.', + DeprecationWarning, + stacklevel=2 + ) + return make_gamma_lut(exp) + + +def make_linear_lut(black, white): lut = [] if black == 0: for i in range(256): lut.append(white*i//255) else: - raise NotImplementedError # FIXME + raise NotImplementedError # FIXME return lut -def _make_gamma_lut(exp, mode="RGB"): + +def make_gamma_lut(exp): lut = [] for i in range(256): lut.append(int(((i / 255.0) ** exp) * 255.0 + 0.5)) return lut -def new(mode, data): - return Image.core.new_palette(mode, data) def negative(mode="RGB"): palette = list(range(256)) palette.reverse() return ImagePalette(mode, palette * len(mode)) + def random(mode="RGB"): from random import randint palette = [] @@ -149,16 +175,19 @@ def random(mode="RGB"): palette.append(randint(0, 255)) return ImagePalette(mode, palette) + def sepia(white="#fff0c0"): r, g, b = ImageColor.getrgb(white) - r = _make_linear_lut(0, r) - g = _make_linear_lut(0, g) - b = _make_linear_lut(0, b) + r = make_linear_lut(0, r) + g = make_linear_lut(0, g) + b = make_linear_lut(0, b) return ImagePalette("RGB", r + g + b) + def wedge(mode="RGB"): return ImagePalette(mode, list(range(256)) * len(mode)) + def load(filename): # FIXME: supports GIMP gradients only @@ -174,8 +203,8 @@ def load(filename): p = GimpPaletteFile.GimpPaletteFile(fp) lut = p.getpalette() except (SyntaxError, ValueError): - #import traceback - #traceback.print_exc() + # import traceback + # traceback.print_exc() pass if not lut: @@ -185,8 +214,8 @@ def load(filename): p = GimpGradientFile.GimpGradientFile(fp) lut = p.getpalette() except (SyntaxError, ValueError): - #import traceback - #traceback.print_exc() + # import traceback + # traceback.print_exc() pass if not lut: @@ -203,4 +232,4 @@ def load(filename): if not lut: raise IOError("cannot load palette") - return lut # data, rawmode + return lut # data, rawmode diff --git a/PIL/ImageShow.py b/PIL/ImageShow.py index e81866bac..40fe629d9 100644 --- a/PIL/ImageShow.py +++ b/PIL/ImageShow.py @@ -17,7 +17,7 @@ from __future__ import print_function from PIL import Image import os, sys -if(sys.version_info >= (3, 3)): +if sys.version_info >= (3, 3): from shlex import quote else: from pipes import quote @@ -160,7 +160,7 @@ else: # imagemagick's display command instead. command = executable = "xv" if title: - command = command + " -name %s" % quote(title) + command += " -name %s" % quote(title) return command, executable if which("xv"): diff --git a/PIL/ImageStat.py b/PIL/ImageStat.py index ef63b7857..d84e2cbf1 100644 --- a/PIL/ImageStat.py +++ b/PIL/ImageStat.py @@ -21,7 +21,6 @@ # See the README file for information on usage and redistribution. # -from PIL import Image import operator, math from functools import reduce @@ -81,7 +80,7 @@ class Stat: for i in range(0, len(self.h), 256): sum = 0.0 for j in range(256): - sum = sum + j * self.h[i+j] + sum += j * self.h[i + j] v.append(sum) return v @@ -92,7 +91,7 @@ class Stat: for i in range(0, len(self.h), 256): sum2 = 0.0 for j in range(256): - sum2 = sum2 + (j ** 2) * float(self.h[i+j]) + sum2 += (j ** 2) * float(self.h[i + j]) v.append(sum2) return v diff --git a/PIL/IptcImagePlugin.py b/PIL/IptcImagePlugin.py index 104153002..dc8607591 100644 --- a/PIL/IptcImagePlugin.py +++ b/PIL/IptcImagePlugin.py @@ -21,7 +21,8 @@ __version__ = "0.3" from PIL import Image, ImageFile, _binary -import os, tempfile +import os +import tempfile i8 = _binary.i8 i16 = _binary.i16be @@ -35,17 +36,20 @@ COMPRESSION = { PAD = o8(0) * 4 + # # Helpers def i(c): return i32((PAD + c)[-4:]) + def dump(c): for i in c: print("%02x" % i8(i), end=' ') print() + ## # Image plugin for IPTC/NAA datastreams. To read IPTC/NAA fields # from TIFF and JPEG files, use the getiptcinfo function. @@ -84,35 +88,13 @@ class IptcImageFile(ImageFile.ImageFile): return tag, size - def _is_raw(self, offset, size): - # - # check if the file can be mapped - - # DISABLED: the following only slows things down... - return 0 - - self.fp.seek(offset) - t, sz = self.field() - if sz != size[0]: - return 0 - y = 1 - while True: - self.fp.seek(sz, 1) - t, s = self.field() - if t != (8, 10): - break - if s != sz: - return 0 - y = y + 1 - return y == size[1] - def _open(self): # load descriptive fields while True: offset = self.fp.tell() tag, size = self.field() - if not tag or tag == (8,10): + if not tag or tag == (8, 10): break if size: tagdata = self.fp.read(size) @@ -129,10 +111,10 @@ class IptcImageFile(ImageFile.ImageFile): # print tag, self.info[tag] # mode - layers = i8(self.info[(3,60)][0]) - component = i8(self.info[(3,60)][1]) - if (3,65) in self.info: - id = i8(self.info[(3,65)][0])-1 + layers = i8(self.info[(3, 60)][0]) + component = i8(self.info[(3, 60)][1]) + if (3, 65) in self.info: + id = i8(self.info[(3, 65)][0])-1 else: id = 0 if layers == 1 and not component: @@ -143,22 +125,18 @@ class IptcImageFile(ImageFile.ImageFile): self.mode = "CMYK"[id] # size - self.size = self.getint((3,20)), self.getint((3,30)) + self.size = self.getint((3, 20)), self.getint((3, 30)) # compression try: - compression = COMPRESSION[self.getint((3,120))] + compression = COMPRESSION[self.getint((3, 120))] except KeyError: raise IOError("Unknown IPTC image compression") # tile - if tag == (8,10): - if compression == "raw" and self._is_raw(offset, self.size): - self.tile = [(compression, (offset, size + 5, -1), - (0, 0, self.size[0], self.size[1]))] - else: - self.tile = [("iptc", (compression, offset), - (0, 0, self.size[0], self.size[1]))] + if tag == (8, 10): + self.tile = [("iptc", (compression, offset), + (0, 0, self.size[0], self.size[1]))] def load(self): @@ -187,7 +165,7 @@ class IptcImageFile(ImageFile.ImageFile): if not s: break o.write(s) - size = size - len(s) + size -= len(s) o.close() try: @@ -200,14 +178,17 @@ class IptcImageFile(ImageFile.ImageFile): im.load() self.im = im.im finally: - try: os.unlink(outfile) - except: pass + try: + os.unlink(outfile) + except: + pass Image.register_open("IPTC", IptcImageFile) Image.register_extension("IPTC", ".iim") + ## # Get IPTC information from TIFF, JPEG, or IPTC file. # @@ -230,31 +211,31 @@ def getiptcinfo(im): # extract the IPTC/NAA resource try: app = im.app["APP13"] - if app[:14] == "Photoshop 3.0\x00": + if app[:14] == b"Photoshop 3.0\x00": app = app[14:] # parse the image resource block offset = 0 - while app[offset:offset+4] == "8BIM": - offset = offset + 4 + while app[offset:offset+4] == b"8BIM": + offset += 4 # resource code code = JpegImagePlugin.i16(app, offset) - offset = offset + 2 + offset += 2 # resource name (usually empty) name_len = i8(app[offset]) name = app[offset+1:offset+1+name_len] offset = 1 + offset + name_len if offset & 1: - offset = offset + 1 + offset += 1 # resource data block size = JpegImagePlugin.i32(app, offset) - offset = offset + 4 + offset += 4 if code == 0x0404: # 0x0404 contains IPTC/NAA data data = app[offset:offset+size] break offset = offset + size if offset & 1: - offset = offset + 1 + offset += 1 except (AttributeError, KeyError): pass @@ -267,7 +248,7 @@ def getiptcinfo(im): pass if data is None: - return None # no properties + return None # no properties # create an IptcImagePlugin object without initializing it class FakeImage: @@ -282,6 +263,6 @@ def getiptcinfo(im): try: im._open() except (IndexError, KeyError): - pass # expected failure + pass # expected failure return im.info diff --git a/PIL/Jpeg2KImagePlugin.py b/PIL/Jpeg2KImagePlugin.py index f57f4a784..53b10ca1a 100644 --- a/PIL/Jpeg2KImagePlugin.py +++ b/PIL/Jpeg2KImagePlugin.py @@ -15,20 +15,21 @@ __version__ = "0.1" -from PIL import Image, ImageFile, _binary +from PIL import Image, ImageFile import struct import os import io + def _parse_codestream(fp): """Parse the JPEG 2000 codestream to extract the size and component count from the SIZ marker segment, returning a PIL (size, mode) tuple.""" - + hdr = fp.read(2) lsiz = struct.unpack('>H', hdr)[0] siz = hdr + fp.read(lsiz - 2) lsiz, rsiz, xsiz, ysiz, xosiz, yosiz, xtsiz, ytsiz, \ - xtosiz, ytosiz, csiz \ + xtosiz, ytosiz, csiz \ = struct.unpack('>HHIIIIIIIIH', siz[:38]) ssiz = [None]*csiz xrsiz = [None]*csiz @@ -39,22 +40,26 @@ def _parse_codestream(fp): size = (xsiz - xosiz, ysiz - yosiz) if csiz == 1: - mode = 'L' + if (yrsiz[0] & 0x7f) > 8: + mode = 'I;16' + else: + mode = 'L' elif csiz == 2: mode = 'LA' elif csiz == 3: mode = 'RGB' elif csiz == 4: - mode == 'RGBA' + mode = 'RGBA' else: mode = None - + return (size, mode) + def _parse_jp2_header(fp): """Parse the JP2 header box to extract size, component count and color space information, returning a PIL (size, mode) tuple.""" - + # Find the JP2 header box header = None while True: @@ -65,6 +70,9 @@ def _parse_jp2_header(fp): else: hlen = 8 + if lbox < hlen: + raise SyntaxError('Invalid JP2 header length') + if tbox == b'jp2h': header = fp.read(lbox - hlen) break @@ -76,7 +84,8 @@ def _parse_jp2_header(fp): size = None mode = None - + bpc = None + hio = io.BytesIO(header) while True: lbox, tbox = struct.unpack('>I4s', hio.read(8)) @@ -90,10 +99,12 @@ def _parse_jp2_header(fp): if tbox == b'ihdr': height, width, nc, bpc, c, unkc, ipr \ - = struct.unpack('>IIHBBBB', content) + = struct.unpack('>IIHBBBB', content) size = (width, height) if unkc: - if nc == 1: + if nc == 1 and (bpc & 0x7f) > 8: + mode = 'I;16' + elif nc == 1: mode = 'L' elif nc == 2: mode = 'LA' @@ -107,22 +118,28 @@ def _parse_jp2_header(fp): if meth == 1: cs = struct.unpack('>I', content[3:7])[0] if cs == 16: # sRGB - if nc == 3: + if nc == 1 and (bpc & 0x7f) > 8: + mode = 'I;16' + elif nc == 1: + mode = 'L' + elif nc == 3: mode = 'RGB' elif nc == 4: mode = 'RGBA' break - elif cs == 17: # grayscale - if nc == 1: + elif cs == 17: # grayscale + if nc == 1 and (bpc & 0x7f) > 8: + mode = 'I;16' + elif nc == 1: mode = 'L' elif nc == 2: mode = 'LA' break - elif cs == 18: # sYCC + elif cs == 18: # sYCC if nc == 3: mode = 'RGB' elif nc == 4: - mode == 'RGBA' + mode = 'RGBA' break return (size, mode) @@ -130,6 +147,7 @@ def _parse_jp2_header(fp): ## # Image plugin for JPEG2000 images. + class Jpeg2KImageFile(ImageFile.ImageFile): format = "JPEG2000" format_description = "JPEG 2000 (ISO 15444)" @@ -141,29 +159,37 @@ class Jpeg2KImageFile(ImageFile.ImageFile): self.size, self.mode = _parse_codestream(self.fp) else: sig = sig + self.fp.read(8) - + if sig == b'\x00\x00\x00\x0cjP \x0d\x0a\x87\x0a': self.codec = "jp2" self.size, self.mode = _parse_jp2_header(self.fp) else: raise SyntaxError('not a JPEG 2000 file') - + if self.size is None or self.mode is None: raise SyntaxError('unable to determine size/mode') - + self.reduce = 0 self.layers = 0 fd = -1 + length = -1 - if hasattr(self.fp, "fileno"): + try: + fd = self.fp.fileno() + length = os.fstat(fd).st_size + except: + fd = -1 try: - fd = self.fp.fileno() + pos = self.fp.tell() + self.fp.seek(0, 2) + length = self.fp.tell() + self.fp.seek(pos, 0) except: - fd = -1 + length = -1 self.tile = [('jpeg2k', (0, 0) + self.size, 0, - (self.codec, self.reduce, self.layers, fd))] + (self.codec, self.reduce, self.layers, fd, length))] def load(self): if self.reduce: @@ -175,15 +201,17 @@ class Jpeg2KImageFile(ImageFile.ImageFile): if self.tile: # Update the reduce and layers settings t = self.tile[0] - t3 = (t[3][0], self.reduce, self.layers, t[3][3]) + t3 = (t[3][0], self.reduce, self.layers, t[3][3], t[3][4]) self.tile = [(t[0], (0, 0) + self.size, t[2], t3)] - + ImageFile.ImageFile.load(self) - + + def _accept(prefix): return (prefix[:4] == b'\xff\x4f\xff\x51' or prefix[:12] == b'\x00\x00\x00\x0cjP \x0d\x0a\x87\x0a') + # ------------------------------------------------------------ # Save support @@ -214,7 +242,7 @@ def _save(im, fp, filename): fd = fp.fileno() except: fd = -1 - + im.encoderconfig = ( offset, tile_offset, @@ -228,10 +256,10 @@ def _save(im, fp, filename): progression, cinema_mode, fd - ) - + ) + ImageFile._save(im, fp, [('jpeg2k', (0, 0)+im.size, 0, kind)]) - + # ------------------------------------------------------------ # Registry stuff diff --git a/PIL/JpegImagePlugin.py b/PIL/JpegImagePlugin.py index 7b40d5d4f..9cbab6b61 100644 --- a/PIL/JpegImagePlugin.py +++ b/PIL/JpegImagePlugin.py @@ -36,7 +36,9 @@ __version__ = "0.6" import array import struct -from PIL import Image, ImageFile, _binary +import io +from struct import unpack +from PIL import Image, ImageFile, TiffImagePlugin, _binary from PIL.JpegPresets import presets from PIL._util import isStringType @@ -110,6 +112,11 @@ def APP(self, marker): pass else: self.info["adobe_transform"] = adobe_transform + 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 + self.info["mpoffset"] = self.fp.tell() - n + 4 def COM(self, marker): @@ -354,12 +361,13 @@ class JpegImageFile(ImageFile.ImageFile): # ALTERNATIVE: handle JPEGs via the IJG command line utilities + import subprocess import tempfile import os f, path = tempfile.mkstemp() os.close(f) if os.path.exists(self.filename): - os.system("djpeg '%s' >'%s'" % (self.filename, path)) + subprocess.check_call(["djpeg", "-outfile", path, self.filename]) else: raise ValueError("Invalid Filename") @@ -379,18 +387,22 @@ class JpegImageFile(ImageFile.ImageFile): def _getexif(self): return _getexif(self) + def _getmp(self): + return _getmp(self) + + +def _fixup(value): + # Helper function for _getexif() and _getmp() + if len(value) == 1: + return value[0] + return value + def _getexif(self): # Extract EXIF information. This method is highly experimental, # and is likely to be replaced with something better in a future # version. - from PIL import TiffImagePlugin - import io - def fixup(value): - if len(value) == 1: - return value[0] - return value # The EXIF record consists of a TIFF file embedded in a JPEG # application marker (!). try: @@ -404,7 +416,7 @@ def _getexif(self): info = TiffImagePlugin.ImageFileDirectory(head) info.load(file) for key, value in info.items(): - exif[key] = fixup(value) + exif[key] = _fixup(value) # get exif extension try: file.seek(exif[0x8769]) @@ -414,7 +426,7 @@ def _getexif(self): info = TiffImagePlugin.ImageFileDirectory(head) info.load(file) for key, value in info.items(): - exif[key] = fixup(value) + exif[key] = _fixup(value) # get gpsinfo extension try: file.seek(exif[0x8825]) @@ -425,9 +437,77 @@ def _getexif(self): info.load(file) exif[0x8825] = gps = {} for key, value in info.items(): - gps[key] = fixup(value) + gps[key] = _fixup(value) return exif + +def _getmp(self): + # Extract MP information. This method was inspired by the "highly + # experimental" _getexif version that's been in use for years now, + # itself based on the ImageFileDirectory class in the TIFF plug-in. + + # The MP record essentially consists of a TIFF file embedded in a JPEG + # application marker. + try: + data = self.info["mp"] + except KeyError: + return None + file = io.BytesIO(data) + head = file.read(8) + endianness = '>' if head[:4] == b'\x4d\x4d\x00\x2a' else '<' + mp = {} + # process dictionary + info = TiffImagePlugin.ImageFileDirectory(head) + info.load(file) + for key, value in info.items(): + mp[key] = _fixup(value) + # it's an error not to have a number of images + try: + quant = mp[0xB001] + except KeyError: + raise SyntaxError("malformed MP Index (no number of images)") + # get MP entries + try: + mpentries = [] + 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') + 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, + 'MPType': mpentry['Attribute'] & 0x00FFFFFF + } + if mpentryattr['ImageDataFormat'] == 0: + mpentryattr['ImageDataFormat'] = 'JPEG' + else: + raise SyntaxError("unsupported picture format in MPO") + mptypemap = { + 0x000000: 'Undefined', + 0x010001: 'Large Thumbnail (VGA Equivalent)', + 0x010002: 'Large Thumbnail (Full HD Equivalent)', + 0x020001: 'Multi-Frame Image (Panorama)', + 0x020002: 'Multi-Frame Image: (Disparity)', + 0x020003: 'Multi-Frame Image: (Multi-Angle)', + 0x030000: 'Baseline MP Primary Image' + } + mpentryattr['MPType'] = mptypemap.get(mpentryattr['MPType'], + 'Unknown') + mpentry['Attribute'] = mpentryattr + mpentries.append(mpentry) + mp[0xB002] = mpentries + except KeyError: + raise SyntaxError("malformed MP Index (bad MP Entry)") + # Next we should try and parse the individual image unique ID list; + # we don't because I've never seen this actually used in a real MPO + # file and so can't test it. + return mp + + # -------------------------------------------------------------------- # stuff to save JPEG files @@ -498,7 +578,7 @@ def _save(im, fp, filename): else: if subsampling in presets: subsampling = presets[subsampling].get('subsampling', -1) - if qtables in presets: + if isStringType(qtables) and qtables in presets: qtables = presets[qtables].get('quantization') if subsampling == "4:4:4": @@ -561,8 +641,8 @@ def _save(im, fp, filename): i = 1 for marker in markers: size = struct.pack(">H", 2 + ICC_OVERHEAD_LEN + len(marker)) - extra = extra + (b"\xFF\xE2" + size + b"ICC_PROFILE\0" + o8(i) + o8(len(markers)) + marker) - i = i + 1 + extra += b"\xFF\xE2" + size + b"ICC_PROFILE\0" + o8(i) + o8(len(markers)) + marker + i += 1 # get keyword arguments im.encoderconfig = ( @@ -602,17 +682,35 @@ def _save(im, fp, filename): def _save_cjpeg(im, fp, filename): # ALTERNATIVE: handle JPEGs via the IJG command line utilities. import os - file = im._dump() - os.system("cjpeg %s >%s" % (file, filename)) + import subprocess + tempfile = im._dump() + subprocess.check_call(["cjpeg", "-outfile", filename, tempfile]) try: os.unlink(file) except: pass + +## +# Factory for making JPEG and MPO instances +def jpeg_factory(fp=None, filename=None): + im = JpegImageFile(fp, filename) + mpheader = im._getmp() + try: + if mpheader[45057] > 1: + # It's actually an MPO + from .MpoImagePlugin import MpoImageFile + im = MpoImageFile(fp, filename) + except (TypeError, IndexError): + # It is really a JPEG + pass + return im + + # -------------------------------------------------------------------q- # Registry stuff -Image.register_open("JPEG", JpegImageFile, _accept) +Image.register_open("JPEG", jpeg_factory, _accept) Image.register_save("JPEG", _save) Image.register_extension("JPEG", ".jfif") diff --git a/PIL/MpegImagePlugin.py b/PIL/MpegImagePlugin.py index 9d7a0ea7a..02e6adc00 100644 --- a/PIL/MpegImagePlugin.py +++ b/PIL/MpegImagePlugin.py @@ -38,13 +38,13 @@ class BitStream: self.bits = 0 continue self.bitbuffer = (self.bitbuffer << 8) + c - self.bits = self.bits + 8 + self.bits += 8 return self.bitbuffer >> (self.bits - bits) & (1 << bits) - 1 def skip(self, bits): while self.bits < bits: self.bitbuffer = (self.bitbuffer << 8) + i8(self.fp.read(1)) - self.bits = self.bits + 8 + self.bits += 8 self.bits = self.bits - bits def read(self, bits): diff --git a/PIL/MpoImagePlugin.py b/PIL/MpoImagePlugin.py new file mode 100644 index 000000000..d053d9026 --- /dev/null +++ b/PIL/MpoImagePlugin.py @@ -0,0 +1,87 @@ +# +# The Python Imaging Library. +# $Id$ +# +# MPO file handling +# +# See "Multi-Picture Format" (CIPA DC-007-Translation 2009, Standard of the +# Camera & Imaging Products Association) +# +# The multi-picture object combines multiple JPEG images (with a modified EXIF +# data format) into a single file. While it can theoretically be used much like +# a GIF animation, it is commonly used to represent 3D photographs and is (as +# of this writing) the most commonly used format by 3D cameras. +# +# History: +# 2014-03-13 Feneric Created +# +# See the README file for information on usage and redistribution. +# + +__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. + +class MpoImageFile(JpegImagePlugin.JpegImageFile): + + format = "MPO" + format_description = "MPO (CIPA DC-007)" + + def _open(self): + self.fp.seek(0) # prep the fp in order to pass the JPEG test + JpegImagePlugin.JpegImageFile._open(self) + self.mpinfo = self._getmp() + self.__framecount = self.mpinfo[0xB001] + 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 + self.__frame = 0 + self.offset = 0 + # for now we can only handle reading and individual frame extraction + self.readonly = 1 + + def load_seek(self, pos): + self.__fp.seek(pos) + + def seek(self, frame): + if frame < 0 or frame >= self.__framecount: + raise EOFError("no more images in MPO file") + else: + self.fp = self.__fp + self.offset = self.__mpoffsets[frame] + self.tile = [ + ("jpeg", (0, 0) + self.size, self.offset, (self.mode, "")) + ] + self.__frame = frame + + def tell(self): + return self.__frame + + +# -------------------------------------------------------------------q- +# Registry stuff + +# Note that since MPO shares a factory with JPEG, we do not need to do a +# separate registration for it here. +#Image.register_open("MPO", JpegImagePlugin.jpeg_factory, _accept) +Image.register_save("MPO", _save) + +Image.register_extension("MPO", ".mpo") + +Image.register_mime("MPO", "image/mpo") diff --git a/PIL/OleFileIO-README.md b/PIL/OleFileIO-README.md index 4a4fdcbca..11a0e9053 100644 --- a/PIL/OleFileIO-README.md +++ b/PIL/OleFileIO-README.md @@ -7,7 +7,7 @@ This is an improved version of the OleFileIO module from [PIL](http://www.python As far as I know, this module is now the most complete and robust Python implementation to read MS OLE2 files, portable on several operating systems. (please tell me if you know other similar Python modules) -OleFileIO_PL can be used as an independent module or with PIL. The goal is to have it integrated into [Pillow](http://python-imaging.github.io/), the friendly fork of PIL. +OleFileIO_PL can be used as an independent module or with PIL. The goal is to have it integrated into [Pillow](http://python-pillow.github.io/), the friendly fork of PIL. OleFileIO\_PL is mostly meant for developers. If you are looking for tools to analyze OLE files or to extract data, then please also check [python-oletools](http://www.decalage.info/python/oletools), which are built upon OleFileIO_PL. @@ -348,4 +348,4 @@ By obtaining, using, and/or copying this software and/or its associated document Permission to use, copy, modify, and distribute this software and its associated documentation for any purpose and without fee is hereby granted, provided that the above copyright notice appears in all copies, and that both that copyright notice and this permission notice appear in supporting documentation, and that the name of Secret Labs AB or the author not be used in advertising or publicity pertaining to distribution of the software without specific, written prior permission. -SECRET LABS AB AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL SECRET LABS AB OR THE AUTHOR BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. \ No newline at end of file +SECRET LABS AB AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL SECRET LABS AB OR THE AUTHOR BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. diff --git a/PIL/OleFileIO.py b/PIL/OleFileIO.py index 8a3c77be4..e35bfa679 100644 --- a/PIL/OleFileIO.py +++ b/PIL/OleFileIO.py @@ -1,28 +1,29 @@ #!/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 +## 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 -version 0.30 2014-02-04 Philippe Lagadec - http://www.decalage.info +## version 0.30 2014-02-04 Philippe Lagadec - http://www.decalage.info -Project website: http://www.decalage.info/python/olefileio +## 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 +## 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 +## The Python Imaging Library (PIL) is -See source code and LICENSE.txt for information on usage and redistribution. +## 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. -WARNING: THIS IS (STILL) WORK IN PROGRESS. -""" # Starting with OleFileIO_PL v0.30, only Python 2.6+ and 3.x is supported # This import enables print() as a function rather than a keyword @@ -370,8 +371,9 @@ for key in list(vars().keys()): def isOleFile (filename): """ Test if file is an OLE container (according to its header). - filename: file name or path (str, unicode) - return: True if OLE, False otherwise. + + :param filename: file name or path (str, unicode) + :returns: True if OLE, False otherwise. """ f = open(filename, 'rb') header = f.read(len(MAGIC)) @@ -397,8 +399,8 @@ def i16(c, o = 0): """ Converts a 2-bytes (16 bits) string to an integer. - c: string containing bytes to convert - o: offset of bytes to convert in string + :param c: string containing bytes to convert + :param o: offset of bytes to convert in string """ return i8(c[o]) | (i8(c[o+1])<<8) @@ -407,8 +409,8 @@ def i32(c, o = 0): """ Converts a 4-bytes (32 bits) string to an integer. - c: string containing bytes to convert - o: offset of bytes to convert in string + :param c: string containing bytes to convert + :param o: offset of bytes to convert in string """ ## return int(ord(c[o])+(ord(c[o+1])<<8)+(ord(c[o+2])<<16)+(ord(c[o+3])<<24)) ## # [PL]: added int() because "<<" gives long int since Python 2.4 @@ -419,7 +421,8 @@ def i32(c, o = 0): def _clsid(clsid): """ Converts a CLSID to a human-readable string. - clsid: string of length 16. + + :param clsid: string of length 16. """ assert len(clsid) == 16 # if clsid is only made of null bytes, return an empty string: @@ -439,8 +442,8 @@ def _unicode(s, errors='replace'): """ Map unicode string to Latin 1. (Python with Unicode support) - s: UTF-16LE unicode string to convert to Latin-1 - errors: 'replace', 'ignore' or 'strict'. + :param s: UTF-16LE unicode string to convert to Latin-1 + :param errors: 'replace', 'ignore' or 'strict'. """ #TODO: test if it OleFileIO works with Unicode strings, instead of # converting to Latin-1. @@ -650,14 +653,14 @@ class _OleStream(io.BytesIO): """ Constructor for _OleStream class. - fp : file object, the OLE container or the MiniFAT stream - sect : sector index of first sector in the stream - size : total size of the stream - offset : offset in bytes for the first FAT or MiniFAT sector - sectorsize: size of one sector - fat : array/list of sector indexes (FAT or MiniFAT) - filesize : size of OLE file (for debugging) - return : a BytesIO instance containing the OLE stream + :param fp : file object, the OLE container or the MiniFAT stream + :param sect : sector index of first sector in the stream + :param size : total size of the stream + :param offset : offset in bytes for the first FAT or MiniFAT sector + :param sectorsize: size of one sector + :param fat : array/list of sector indexes (FAT or MiniFAT) + :param filesize : size of OLE file (for debugging) + :returns : a BytesIO instance containing the OLE stream """ debug('_OleStream.__init__:') debug(' sect=%d (%X), size=%d, offset=%d, sectorsize=%d, len(fat)=%d, fp=%s' @@ -793,9 +796,9 @@ class _OleDirectoryEntry: Constructor for an _OleDirectoryEntry object. Parses a 128-bytes entry from the OLE Directory stream. - entry : string (must be 128 bytes long) - sid : index of this directory entry in the OLE file directory - olefile: OleFileIO containing this directory entry + :param entry : string (must be 128 bytes long) + :param sid : index of this directory entry in the OLE file directory + :param olefile: OleFileIO containing this directory entry """ self.sid = sid # ref to olefile is stored for future use @@ -989,7 +992,7 @@ class _OleDirectoryEntry: """ Return modification time of a directory entry. - return: None if modification time is null, a python datetime object + :returns: None if modification time is null, a python datetime object otherwise (UTC timezone) new in version 0.26 @@ -1003,7 +1006,7 @@ class _OleDirectoryEntry: """ Return creation time of a directory entry. - return: None if modification time is null, a python datetime object + :returns: None if modification time is null, a python datetime object otherwise (UTC timezone) new in version 0.26 @@ -1020,7 +1023,8 @@ class OleFileIO: OLE container object This class encapsulates the interface to an OLE 2 structured - storage file. Use the {@link listdir} and {@link openstream} methods to + storage file. Use the :py:meth:`~PIL.OleFileIO.OleFileIO.listdir` and + :py:meth:`~PIL.OleFileIO.OleFileIO.openstream` methods to access the contents of this file. Object names are given as a list of strings, one for each subentry @@ -1048,8 +1052,8 @@ class OleFileIO: """ Constructor for OleFileIO class. - filename: file to open. - raise_defects: minimal level for defects to be raised as exceptions. + :param filename: file to open. + :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) """ @@ -1068,13 +1072,13 @@ class OleFileIO: It may raise an IOError exception according to the minimal level chosen for the OleFileIO object. - defect_level: defect level, possible values are: + :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 - message: string describing the defect, used with raised exception. - exception_type: exception class to be raised, IOError by default + :param message: string describing the defect, used with raised exception. + :param exception_type: exception class to be raised, IOError by default """ # added by [PL] if defect_level >= self._raise_defects_level: @@ -1089,7 +1093,7 @@ class OleFileIO: Open an OLE2 file. Reads the header, FAT and directory. - filename: string-like or file-like object + :param filename: string-like or file-like object """ #[PL] check if filename is a string-like or file-like object: # (it is better to check for a read() method) @@ -1276,8 +1280,8 @@ class OleFileIO: 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. - first_sect: index of first sector of the stream in FAT - minifat: if True, stream is located in the MiniFAT, else in the FAT + :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 """ if minifat: debug('_check_duplicate_stream: sect=%d in MiniFAT' % first_sect) @@ -1371,8 +1375,9 @@ class OleFileIO: def loadfat_sect(self, sect): """ Adds the indexes of the given sector to the FAT - sect: string containing the first FAT sector, or array of long integers - return: index of last FAT sector. + + :param sect: string containing the first FAT sector, or array of long integers + :returns: index of last FAT sector. """ # a FAT sector is an array of ulong integers. if isinstance(sect, array.array): @@ -1505,8 +1510,9 @@ class OleFileIO: def getsect(self, sect): """ Read given sector from file on disk. - sect: sector index - returns a string containing the sector data. + + :param sect: sector index + :returns: a string containing the sector data. """ # [PL] this original code was wrong when sectors are 4KB instead of # 512 bytes: @@ -1530,7 +1536,8 @@ class OleFileIO: def loaddirectory(self, sect): """ Load the directory. - sect: sector index of directory stream. + + :param sect: sector index of directory stream. """ # The directory is stored in a standard # substream, independent of its size. @@ -1567,9 +1574,10 @@ class OleFileIO: Load a directory entry from the directory. This method should only be called once for each storage/stream when loading the directory. - sid: index of storage/stream in the directory. - return: a _OleDirectoryEntry object - raise: IOError if the entry has always been referenced. + + :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): @@ -1598,9 +1606,9 @@ class OleFileIO: Open a stream, either in FAT or MiniFAT according to its size. (openstream helper) - start: index of first sector - size: size of stream (or nothing if size is unknown) - force_FAT: if False (default), stream will be opened in FAT or MiniFAT + :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. """ debug('OleFileIO.open(): sect=%d, size=%d, force_FAT=%s' % @@ -1630,11 +1638,11 @@ class OleFileIO: def _list(self, files, prefix, node, streams=True, storages=False): """ (listdir helper) - files: list of files to fill in - prefix: current location in storage tree (list of names) - node: current node (_OleDirectoryEntry object) - streams: bool, include streams if True (True by default) - new in v0.26 - storages: bool, include storages if True (False by default) - new in v0.26 + :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) """ prefix = prefix + [node.name] @@ -1657,9 +1665,9 @@ class OleFileIO: """ Return a list of streams stored in this file - streams: bool, include streams if True (True by default) - new in v0.26 - storages: bool, include storages if True (False by default) - new in v0.26 - (note: the root storage is never included) + :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) """ files = [] self._list(files, [], self.root, streams, storages) @@ -1671,12 +1679,13 @@ class OleFileIO: Returns directory entry of given filename. (openstream helper) Note: this method is case-insensitive. - filename: path of stream in storage tree (except root entry), either: + :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. Example: ['storage_1', 'storage_1.2', 'stream'] - return: sid of requested filename + :returns: sid of requested filename raise IOError if file not found """ @@ -1700,13 +1709,15 @@ class OleFileIO: """ Open a stream as a read-only file object (BytesIO). - filename: path of stream in storage tree (except root entry), either: + :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. Example: ['storage_1', 'storage_1.2', 'stream'] - return: file object (read-only) - raise IOError if filename not found, or if this is not a stream. + + :returns: file object (read-only) + :exception IOError: if filename not found, or if this is not a stream. """ sid = self._find(filename) entry = self.direntries[sid] @@ -1720,8 +1731,9 @@ class OleFileIO: Test if given filename exists as a stream or a storage in the OLE container, and return its type. - filename: path of stream in storage tree. (see openstream for syntax) - return: False if object does not exist, its entry type (>0) otherwise: + :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 @@ -1738,10 +1750,10 @@ class OleFileIO: """ Return modification time of a stream/storage. - filename: path of stream/storage in storage tree. (see openstream for - syntax) - return: None if modification time is null, a python datetime object - otherwise (UTC timezone) + :param filename: path of stream/storage in storage tree. (see openstream for + syntax) + :returns: None if modification time is null, a python datetime object + otherwise (UTC timezone) new in version 0.26 """ @@ -1754,10 +1766,10 @@ class OleFileIO: """ Return creation time of a stream/storage. - filename: path of stream/storage in storage tree. (see openstream for - syntax) - return: None if creation time is null, a python datetime object - otherwise (UTC timezone) + :param filename: path of stream/storage in storage tree. (see openstream for + syntax) + :returns: None if creation time is null, a python datetime object + otherwise (UTC timezone) new in version 0.26 """ @@ -1771,8 +1783,8 @@ class OleFileIO: Test if given filename exists as a stream or a storage in the OLE container. - filename: path of stream in storage tree. (see openstream for syntax) - return: True if object exist, else False. + :param filename: path of stream in storage tree. (see openstream for syntax) + :returns: True if object exist, else False. """ try: sid = self._find(filename) @@ -1785,9 +1797,10 @@ class OleFileIO: """ Return size of a stream in the OLE container, in bytes. - filename: path of stream in storage tree (see openstream for syntax) - return: size in bytes (long integer) - raise: IOError if file not found, TypeError if this is not a stream. + :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 """ sid = self._find(filename) entry = self.direntries[sid] @@ -1809,11 +1822,11 @@ class OleFileIO: """ Return properties described in substream. - filename: path of stream in storage tree (see openstream for syntax) - convert_time: bool, if True timestamps will be converted to Python datetime - no_conversion: None or list of int, timestamps not to be converted - (for example total editing time is not a real timestamp) - return: a dictionary of values indexed by id (integer) + :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) + :returns: a dictionary of values indexed by id (integer) """ # make sure no_conversion is a list, just to simplify code below: if no_conversion == None: diff --git a/PIL/PSDraw.py b/PIL/PSDraw.py index 88593bb9d..26fdb74ea 100644 --- a/PIL/PSDraw.py +++ b/PIL/PSDraw.py @@ -73,9 +73,8 @@ class PSDraw: def setink(self, ink): """ - .. warning:: + .. warning:: This has been in the PIL API for ages but was never implemented. - This has been in the PIL API for ages but was never implemented. """ print("*** NOT YET IMPLEMENTED ***") diff --git a/PIL/PalmImagePlugin.py b/PIL/PalmImagePlugin.py index 89f42bffc..bba1de8bb 100644 --- a/PIL/PalmImagePlugin.py +++ b/PIL/PalmImagePlugin.py @@ -12,74 +12,75 @@ __version__ = "1.0" from PIL import Image, ImageFile, _binary _Palm8BitColormapValues = ( - ( 255, 255, 255 ), ( 255, 204, 255 ), ( 255, 153, 255 ), ( 255, 102, 255 ), - ( 255, 51, 255 ), ( 255, 0, 255 ), ( 255, 255, 204 ), ( 255, 204, 204 ), - ( 255, 153, 204 ), ( 255, 102, 204 ), ( 255, 51, 204 ), ( 255, 0, 204 ), - ( 255, 255, 153 ), ( 255, 204, 153 ), ( 255, 153, 153 ), ( 255, 102, 153 ), - ( 255, 51, 153 ), ( 255, 0, 153 ), ( 204, 255, 255 ), ( 204, 204, 255 ), - ( 204, 153, 255 ), ( 204, 102, 255 ), ( 204, 51, 255 ), ( 204, 0, 255 ), - ( 204, 255, 204 ), ( 204, 204, 204 ), ( 204, 153, 204 ), ( 204, 102, 204 ), - ( 204, 51, 204 ), ( 204, 0, 204 ), ( 204, 255, 153 ), ( 204, 204, 153 ), - ( 204, 153, 153 ), ( 204, 102, 153 ), ( 204, 51, 153 ), ( 204, 0, 153 ), - ( 153, 255, 255 ), ( 153, 204, 255 ), ( 153, 153, 255 ), ( 153, 102, 255 ), - ( 153, 51, 255 ), ( 153, 0, 255 ), ( 153, 255, 204 ), ( 153, 204, 204 ), - ( 153, 153, 204 ), ( 153, 102, 204 ), ( 153, 51, 204 ), ( 153, 0, 204 ), - ( 153, 255, 153 ), ( 153, 204, 153 ), ( 153, 153, 153 ), ( 153, 102, 153 ), - ( 153, 51, 153 ), ( 153, 0, 153 ), ( 102, 255, 255 ), ( 102, 204, 255 ), - ( 102, 153, 255 ), ( 102, 102, 255 ), ( 102, 51, 255 ), ( 102, 0, 255 ), - ( 102, 255, 204 ), ( 102, 204, 204 ), ( 102, 153, 204 ), ( 102, 102, 204 ), - ( 102, 51, 204 ), ( 102, 0, 204 ), ( 102, 255, 153 ), ( 102, 204, 153 ), - ( 102, 153, 153 ), ( 102, 102, 153 ), ( 102, 51, 153 ), ( 102, 0, 153 ), - ( 51, 255, 255 ), ( 51, 204, 255 ), ( 51, 153, 255 ), ( 51, 102, 255 ), - ( 51, 51, 255 ), ( 51, 0, 255 ), ( 51, 255, 204 ), ( 51, 204, 204 ), - ( 51, 153, 204 ), ( 51, 102, 204 ), ( 51, 51, 204 ), ( 51, 0, 204 ), - ( 51, 255, 153 ), ( 51, 204, 153 ), ( 51, 153, 153 ), ( 51, 102, 153 ), - ( 51, 51, 153 ), ( 51, 0, 153 ), ( 0, 255, 255 ), ( 0, 204, 255 ), - ( 0, 153, 255 ), ( 0, 102, 255 ), ( 0, 51, 255 ), ( 0, 0, 255 ), - ( 0, 255, 204 ), ( 0, 204, 204 ), ( 0, 153, 204 ), ( 0, 102, 204 ), - ( 0, 51, 204 ), ( 0, 0, 204 ), ( 0, 255, 153 ), ( 0, 204, 153 ), - ( 0, 153, 153 ), ( 0, 102, 153 ), ( 0, 51, 153 ), ( 0, 0, 153 ), - ( 255, 255, 102 ), ( 255, 204, 102 ), ( 255, 153, 102 ), ( 255, 102, 102 ), - ( 255, 51, 102 ), ( 255, 0, 102 ), ( 255, 255, 51 ), ( 255, 204, 51 ), - ( 255, 153, 51 ), ( 255, 102, 51 ), ( 255, 51, 51 ), ( 255, 0, 51 ), - ( 255, 255, 0 ), ( 255, 204, 0 ), ( 255, 153, 0 ), ( 255, 102, 0 ), - ( 255, 51, 0 ), ( 255, 0, 0 ), ( 204, 255, 102 ), ( 204, 204, 102 ), - ( 204, 153, 102 ), ( 204, 102, 102 ), ( 204, 51, 102 ), ( 204, 0, 102 ), - ( 204, 255, 51 ), ( 204, 204, 51 ), ( 204, 153, 51 ), ( 204, 102, 51 ), - ( 204, 51, 51 ), ( 204, 0, 51 ), ( 204, 255, 0 ), ( 204, 204, 0 ), - ( 204, 153, 0 ), ( 204, 102, 0 ), ( 204, 51, 0 ), ( 204, 0, 0 ), - ( 153, 255, 102 ), ( 153, 204, 102 ), ( 153, 153, 102 ), ( 153, 102, 102 ), - ( 153, 51, 102 ), ( 153, 0, 102 ), ( 153, 255, 51 ), ( 153, 204, 51 ), - ( 153, 153, 51 ), ( 153, 102, 51 ), ( 153, 51, 51 ), ( 153, 0, 51 ), - ( 153, 255, 0 ), ( 153, 204, 0 ), ( 153, 153, 0 ), ( 153, 102, 0 ), - ( 153, 51, 0 ), ( 153, 0, 0 ), ( 102, 255, 102 ), ( 102, 204, 102 ), - ( 102, 153, 102 ), ( 102, 102, 102 ), ( 102, 51, 102 ), ( 102, 0, 102 ), - ( 102, 255, 51 ), ( 102, 204, 51 ), ( 102, 153, 51 ), ( 102, 102, 51 ), - ( 102, 51, 51 ), ( 102, 0, 51 ), ( 102, 255, 0 ), ( 102, 204, 0 ), - ( 102, 153, 0 ), ( 102, 102, 0 ), ( 102, 51, 0 ), ( 102, 0, 0 ), - ( 51, 255, 102 ), ( 51, 204, 102 ), ( 51, 153, 102 ), ( 51, 102, 102 ), - ( 51, 51, 102 ), ( 51, 0, 102 ), ( 51, 255, 51 ), ( 51, 204, 51 ), - ( 51, 153, 51 ), ( 51, 102, 51 ), ( 51, 51, 51 ), ( 51, 0, 51 ), - ( 51, 255, 0 ), ( 51, 204, 0 ), ( 51, 153, 0 ), ( 51, 102, 0 ), - ( 51, 51, 0 ), ( 51, 0, 0 ), ( 0, 255, 102 ), ( 0, 204, 102 ), - ( 0, 153, 102 ), ( 0, 102, 102 ), ( 0, 51, 102 ), ( 0, 0, 102 ), - ( 0, 255, 51 ), ( 0, 204, 51 ), ( 0, 153, 51 ), ( 0, 102, 51 ), - ( 0, 51, 51 ), ( 0, 0, 51 ), ( 0, 255, 0 ), ( 0, 204, 0 ), - ( 0, 153, 0 ), ( 0, 102, 0 ), ( 0, 51, 0 ), ( 17, 17, 17 ), - ( 34, 34, 34 ), ( 68, 68, 68 ), ( 85, 85, 85 ), ( 119, 119, 119 ), - ( 136, 136, 136 ), ( 170, 170, 170 ), ( 187, 187, 187 ), ( 221, 221, 221 ), - ( 238, 238, 238 ), ( 192, 192, 192 ), ( 128, 0, 0 ), ( 128, 0, 128 ), - ( 0, 128, 0 ), ( 0, 128, 128 ), ( 0, 0, 0 ), ( 0, 0, 0 ), - ( 0, 0, 0 ), ( 0, 0, 0 ), ( 0, 0, 0 ), ( 0, 0, 0 ), - ( 0, 0, 0 ), ( 0, 0, 0 ), ( 0, 0, 0 ), ( 0, 0, 0 ), - ( 0, 0, 0 ), ( 0, 0, 0 ), ( 0, 0, 0 ), ( 0, 0, 0 ), - ( 0, 0, 0 ), ( 0, 0, 0 ), ( 0, 0, 0 ), ( 0, 0, 0 ), - ( 0, 0, 0 ), ( 0, 0, 0 ), ( 0, 0, 0 ), ( 0, 0, 0 ), - ( 0, 0, 0 ), ( 0, 0, 0 ), ( 0, 0, 0 ), ( 0, 0, 0 )) + (255, 255, 255), (255, 204, 255), (255, 153, 255), (255, 102, 255), + (255, 51, 255), (255, 0, 255), (255, 255, 204), (255, 204, 204), + (255, 153, 204), (255, 102, 204), (255, 51, 204), (255, 0, 204), + (255, 255, 153), (255, 204, 153), (255, 153, 153), (255, 102, 153), + (255, 51, 153), (255, 0, 153), (204, 255, 255), (204, 204, 255), + (204, 153, 255), (204, 102, 255), (204, 51, 255), (204, 0, 255), + (204, 255, 204), (204, 204, 204), (204, 153, 204), (204, 102, 204), + (204, 51, 204), (204, 0, 204), (204, 255, 153), (204, 204, 153), + (204, 153, 153), (204, 102, 153), (204, 51, 153), (204, 0, 153), + (153, 255, 255), (153, 204, 255), (153, 153, 255), (153, 102, 255), + (153, 51, 255), (153, 0, 255), (153, 255, 204), (153, 204, 204), + (153, 153, 204), (153, 102, 204), (153, 51, 204), (153, 0, 204), + (153, 255, 153), (153, 204, 153), (153, 153, 153), (153, 102, 153), + (153, 51, 153), (153, 0, 153), (102, 255, 255), (102, 204, 255), + (102, 153, 255), (102, 102, 255), (102, 51, 255), (102, 0, 255), + (102, 255, 204), (102, 204, 204), (102, 153, 204), (102, 102, 204), + (102, 51, 204), (102, 0, 204), (102, 255, 153), (102, 204, 153), + (102, 153, 153), (102, 102, 153), (102, 51, 153), (102, 0, 153), + ( 51, 255, 255), ( 51, 204, 255), ( 51, 153, 255), ( 51, 102, 255), + ( 51, 51, 255), ( 51, 0, 255), ( 51, 255, 204), ( 51, 204, 204), + ( 51, 153, 204), ( 51, 102, 204), ( 51, 51, 204), ( 51, 0, 204), + ( 51, 255, 153), ( 51, 204, 153), ( 51, 153, 153), ( 51, 102, 153), + ( 51, 51, 153), ( 51, 0, 153), ( 0, 255, 255), ( 0, 204, 255), + ( 0, 153, 255), ( 0, 102, 255), ( 0, 51, 255), ( 0, 0, 255), + ( 0, 255, 204), ( 0, 204, 204), ( 0, 153, 204), ( 0, 102, 204), + ( 0, 51, 204), ( 0, 0, 204), ( 0, 255, 153), ( 0, 204, 153), + ( 0, 153, 153), ( 0, 102, 153), ( 0, 51, 153), ( 0, 0, 153), + (255, 255, 102), (255, 204, 102), (255, 153, 102), (255, 102, 102), + (255, 51, 102), (255, 0, 102), (255, 255, 51), (255, 204, 51), + (255, 153, 51), (255, 102, 51), (255, 51, 51), (255, 0, 51), + (255, 255, 0), (255, 204, 0), (255, 153, 0), (255, 102, 0), + (255, 51, 0), (255, 0, 0), (204, 255, 102), (204, 204, 102), + (204, 153, 102), (204, 102, 102), (204, 51, 102), (204, 0, 102), + (204, 255, 51), (204, 204, 51), (204, 153, 51), (204, 102, 51), + (204, 51, 51), (204, 0, 51), (204, 255, 0), (204, 204, 0), + (204, 153, 0), (204, 102, 0), (204, 51, 0), (204, 0, 0), + (153, 255, 102), (153, 204, 102), (153, 153, 102), (153, 102, 102), + (153, 51, 102), (153, 0, 102), (153, 255, 51), (153, 204, 51), + (153, 153, 51), (153, 102, 51), (153, 51, 51), (153, 0, 51), + (153, 255, 0), (153, 204, 0), (153, 153, 0), (153, 102, 0), + (153, 51, 0), (153, 0, 0), (102, 255, 102), (102, 204, 102), + (102, 153, 102), (102, 102, 102), (102, 51, 102), (102, 0, 102), + (102, 255, 51), (102, 204, 51), (102, 153, 51), (102, 102, 51), + (102, 51, 51), (102, 0, 51), (102, 255, 0), (102, 204, 0), + (102, 153, 0), (102, 102, 0), (102, 51, 0), (102, 0, 0), + ( 51, 255, 102), ( 51, 204, 102), ( 51, 153, 102), ( 51, 102, 102), + ( 51, 51, 102), ( 51, 0, 102), ( 51, 255, 51), ( 51, 204, 51), + ( 51, 153, 51), ( 51, 102, 51), ( 51, 51, 51), ( 51, 0, 51), + ( 51, 255, 0), ( 51, 204, 0), ( 51, 153, 0), ( 51, 102, 0), + ( 51, 51, 0), ( 51, 0, 0), ( 0, 255, 102), ( 0, 204, 102), + ( 0, 153, 102), ( 0, 102, 102), ( 0, 51, 102), ( 0, 0, 102), + ( 0, 255, 51), ( 0, 204, 51), ( 0, 153, 51), ( 0, 102, 51), + ( 0, 51, 51), ( 0, 0, 51), ( 0, 255, 0), ( 0, 204, 0), + ( 0, 153, 0), ( 0, 102, 0), ( 0, 51, 0), ( 17, 17, 17), + ( 34, 34, 34), ( 68, 68, 68), ( 85, 85, 85), (119, 119, 119), + (136, 136, 136), (170, 170, 170), (187, 187, 187), (221, 221, 221), + (238, 238, 238), (192, 192, 192), (128, 0, 0), (128, 0, 128), + ( 0, 128, 0), ( 0, 128, 128), ( 0, 0, 0), ( 0, 0, 0), + ( 0, 0, 0), ( 0, 0, 0), ( 0, 0, 0), ( 0, 0, 0), + ( 0, 0, 0), ( 0, 0, 0), ( 0, 0, 0), ( 0, 0, 0), + ( 0, 0, 0), ( 0, 0, 0), ( 0, 0, 0), ( 0, 0, 0), + ( 0, 0, 0), ( 0, 0, 0), ( 0, 0, 0), ( 0, 0, 0), + ( 0, 0, 0), ( 0, 0, 0), ( 0, 0, 0), ( 0, 0, 0), + ( 0, 0, 0), ( 0, 0, 0), ( 0, 0, 0), ( 0, 0, 0)) + # so build a prototype image to be used for palette resampling def build_prototype_image(): - image = Image.new("L", (1,len(_Palm8BitColormapValues),)) + image = Image.new("L", (1, len(_Palm8BitColormapValues),)) image.putdata(list(range(len(_Palm8BitColormapValues)))) palettedata = () for i in range(len(_Palm8BitColormapValues)): @@ -91,7 +92,8 @@ def build_prototype_image(): Palm8BitColormapImage = build_prototype_image() -# OK, we now have in Palm8BitColormapImage, a "P"-mode image with the right palette +# OK, we now have in Palm8BitColormapImage, +# a "P"-mode image with the right palette # # -------------------------------------------------------------------- @@ -110,6 +112,7 @@ _COMPRESSION_TYPES = { o8 = _binary.o8 o16b = _binary.o16be + # # -------------------------------------------------------------------- @@ -127,12 +130,16 @@ def _save(im, fp, filename, check=0): bpp = 8 version = 1 - elif im.mode == "L" and "bpp" in im.encoderinfo and im.encoderinfo["bpp"] in (1, 2, 4): + elif (im.mode == "L" and + "bpp" in im.encoderinfo and + im.encoderinfo["bpp"] in (1, 2, 4)): - # this is 8-bit grayscale, so we shift it to get the high-order bits, and invert it because + # this is 8-bit grayscale, so we shift it to get the high-order bits, + # and invert it because # Palm does greyscale from white (0) to black (1) bpp = im.encoderinfo["bpp"] - im = im.point(lambda x, shift=8-bpp, maxval=(1 << bpp)-1: maxval - (x >> shift)) + im = im.point( + lambda x, shift=8-bpp, maxval=(1 << bpp)-1: maxval - (x >> shift)) # we ignore the palette here im.mode = "P" rawmode = "P;" + str(bpp) @@ -140,8 +147,9 @@ def _save(im, fp, filename, check=0): elif im.mode == "L" and "bpp" in im.info and im.info["bpp"] in (1, 2, 4): - # here we assume that even though the inherent mode is 8-bit grayscale, only - # the lower bpp bits are significant. We invert them to match the Palm. + # here we assume that even though the inherent mode is 8-bit grayscale, + # only the lower bpp bits are significant. + # We invert them to match the Palm. bpp = im.info["bpp"] im = im.point(lambda x, maxval=(1 << bpp)-1: maxval - (x & maxval)) # we ignore the palette here @@ -172,21 +180,21 @@ def _save(im, fp, filename, check=0): cols = im.size[0] rows = im.size[1] - rowbytes = ((cols + (16//bpp - 1)) / (16 // bpp)) * 2; + rowbytes = int((cols + (16//bpp - 1)) / (16 // bpp)) * 2 transparent_index = 0 compression_type = _COMPRESSION_TYPES["none"] - flags = 0; + flags = 0 if im.mode == "P" and "custom-colormap" in im.info: flags = flags & _FLAGS["custom-colormap"] - colormapsize = 4 * 256 + 2; + colormapsize = 4 * 256 + 2 colormapmode = im.palette.mode colormap = im.getdata().getpalette() else: colormapsize = 0 if "offset" in im.info: - offset = (rowbytes * rows + 16 + 3 + colormapsize) // 4; + offset = (rowbytes * rows + 16 + 3 + colormapsize) // 4 else: offset = 0 @@ -205,12 +213,19 @@ def _save(im, fp, filename, check=0): for i in range(256): fp.write(o8(i)) if colormapmode == 'RGB': - fp.write(o8(colormap[3 * i]) + o8(colormap[3 * i + 1]) + o8(colormap[3 * i + 2])) + fp.write( + o8(colormap[3 * i]) + + o8(colormap[3 * i + 1]) + + o8(colormap[3 * i + 2])) elif colormapmode == 'RGBA': - fp.write(o8(colormap[4 * i]) + o8(colormap[4 * i + 1]) + o8(colormap[4 * i + 2])) + fp.write( + o8(colormap[4 * i]) + + o8(colormap[4 * i + 1]) + + o8(colormap[4 * i + 2])) # now convert data to raw form - ImageFile._save(im, fp, [("raw", (0,0)+im.size, 0, (rawmode, rowbytes, 1))]) + ImageFile._save( + im, fp, [("raw", (0, 0)+im.size, 0, (rawmode, rowbytes, 1))]) fp.flush() diff --git a/PIL/PcxImagePlugin.py b/PIL/PcxImagePlugin.py index 2496af676..4f6d5a3e5 100644 --- a/PIL/PcxImagePlugin.py +++ b/PIL/PcxImagePlugin.py @@ -135,7 +135,7 @@ def _save(im, fp, filename, check=0): # bytes per plane stride = (im.size[0] * bits + 7) // 8 # stride should be even - stride = stride + (stride % 2) + 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. diff --git a/PIL/PdfImagePlugin.py b/PIL/PdfImagePlugin.py index fcc841438..5113f099e 100644 --- a/PIL/PdfImagePlugin.py +++ b/PIL/PdfImagePlugin.py @@ -108,8 +108,8 @@ def _save(im, fp, filename): r = i8(palette[i*3]) g = i8(palette[i*3+1]) b = i8(palette[i*3+2]) - colorspace = colorspace + "%02x%02x%02x " % (r, g, b) - colorspace = colorspace + "> ]" + colorspace += "%02x%02x%02x " % (r, g, b) + colorspace += "> ]" procset = "/ImageI" # indexed color elif im.mode == "RGB": filter = "/DCTDecode" diff --git a/PIL/PngImagePlugin.py b/PIL/PngImagePlugin.py index e794ef702..4dbedb783 100644 --- a/PIL/PngImagePlugin.py +++ b/PIL/PngImagePlugin.py @@ -147,6 +147,17 @@ class ChunkStream: return cids +# -------------------------------------------------------------------- +# Subclass of string to allow iTXt chunks to look like strings while +# keeping their extra information + +class iTXt(str): + @staticmethod + def __new__(cls, text, lang, tkey): + self = str.__new__(cls, text) + self.lang = lang + self.tkey = tkey + return self # -------------------------------------------------------------------- # PNG chunk container (for use with save(pnginfo=)) @@ -159,14 +170,36 @@ class PngInfo: def add(self, cid, data): self.chunks.append((cid, data)) + def add_itxt(self, key, value, lang="", tkey="", zip=False): + if not isinstance(key, bytes): + key = key.encode("latin-1", "strict") + if not isinstance(value, bytes): + value = value.encode("utf-8", "strict") + if not isinstance(lang, bytes): + lang = lang.encode("utf-8", "strict") + if not isinstance(tkey, bytes): + 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)) + else: + self.add(b"iTXt", key + b"\0\0\0" + lang + b"\0" + tkey + b"\0" + value) + def add_text(self, key, value, zip=0): + if isinstance(value, iTXt): + return self.add_itxt(key, value, value.lang, value.tkey, bool(zip)) + # The tEXt chunk stores latin-1 text + if not isinstance(value, bytes): + try: + value = value.encode('latin-1', 'strict') + except UnicodeError: + return self.add_itxt(key, value, zip=bool(zip)) + if not isinstance(key, bytes): key = key.encode('latin-1', 'strict') - if not isinstance(value, bytes): - value = value.encode('latin-1', 'replace') - if zip: import zlib self.add(b"zTXt", key + b"\0\0" + zlib.compress(value)) @@ -329,6 +362,43 @@ class PngStream(ChunkStream): self.im_info[k] = self.im_text[k] = v return s + def chunk_iTXt(self, pos, length): + + # international text + r = s = ImageFile._safe_read(self.fp, length) + try: + k, r = r.split(b"\0", 1) + except ValueError: + return s + if len(r) < 2: + return s + cf, cm, r = i8(r[0]), i8(r[1]), r[2:] + try: + lang, tk, v = r.split(b"\0", 2) + except ValueError: + return s + if cf != 0: + if cm == 0: + import zlib + try: + v = zlib.decompress(v) + except zlib.error: + return s + else: + return s + if bytes is not str: + try: + k = k.decode("latin-1", "strict") + lang = lang.decode("utf-8", "strict") + tk = tk.decode("utf-8", "strict") + v = v.decode("utf-8", "strict") + except UnicodeError: + return s + + self.im_info[k] = self.im_text[k] = iTXt(v, lang, tk) + + return s + # -------------------------------------------------------------------- # PNG reader diff --git a/PIL/PsdImagePlugin.py b/PIL/PsdImagePlugin.py index f6aefe9c9..9e64e7c90 100644 --- a/PIL/PsdImagePlugin.py +++ b/PIL/PsdImagePlugin.py @@ -235,7 +235,7 @@ def _layerinfo(file): if t: tile.extend(t) layers[i] = name, mode, bbox, tile - i = i + 1 + i += 1 return layers @@ -258,7 +258,7 @@ def _maketile(file, mode, bbox, channels): for channel in range(channels): layer = mode[channel] if mode == "CMYK": - layer = layer + ";I" + layer += ";I" tile.append(("raw", bbox, offset, layer)) offset = offset + xsize*ysize @@ -272,13 +272,13 @@ def _maketile(file, mode, bbox, channels): for channel in range(channels): layer = mode[channel] if mode == "CMYK": - layer = layer + ";I" + layer += ";I" tile.append( ("packbits", bbox, offset, layer) ) for y in range(ysize): offset = offset + i16(bytecount[i:i+2]) - i = i + 2 + i += 2 file.seek(offset) diff --git a/PIL/PyAccess.py b/PIL/PyAccess.py index f76beb820..7ccc313eb 100644 --- a/PIL/PyAccess.py +++ b/PIL/PyAccess.py @@ -261,6 +261,7 @@ mode_map = {'1': _PyAccess8, 'PA': _PyAccess32_2, 'RGB': _PyAccess32_3, 'LAB': _PyAccess32_3, + 'HSV': _PyAccess32_3, 'YCbCr': _PyAccess32_3, 'RGBA': _PyAccess32_4, 'RGBa': _PyAccess32_4, diff --git a/PIL/SgiImagePlugin.py b/PIL/SgiImagePlugin.py index b60df473c..2b8fcd8e4 100644 --- a/PIL/SgiImagePlugin.py +++ b/PIL/SgiImagePlugin.py @@ -31,6 +31,7 @@ i32 = _binary.i32be def _accept(prefix): return i16(prefix) == 474 + ## # Image plugin for SGI images. @@ -44,7 +45,7 @@ class SgiImageFile(ImageFile.ImageFile): # HEAD s = self.fp.read(512) if i16(s) != 474: - raise SyntaxError("not an SGI image file") + raise ValueError("Not an SGI image file") # relevant header entries compression = i8(s[2]) @@ -60,22 +61,22 @@ class SgiImageFile(ImageFile.ImageFile): elif layout == (1, 3, 4): self.mode = "RGBA" else: - raise SyntaxError("unsupported SGI image mode") + raise ValueError("Unsupported SGI image mode") # size self.size = i16(s[6:]), i16(s[8:]) - # decoder info if compression == 0: offset = 512 pagesize = self.size[0]*self.size[1]*layout[0] self.tile = [] for layer in self.mode: - self.tile.append(("raw", (0,0)+self.size, offset, (layer,0,-1))) + self.tile.append( + ("raw", (0, 0)+self.size, offset, (layer, 0, -1))) offset = offset + pagesize elif compression == 1: - self.tile = [("sgi_rle", (0,0)+self.size, 512, (self.mode, 0, -1))] + raise ValueError("SGI RLE encoding not supported") # # registry @@ -85,5 +86,6 @@ Image.register_open("SGI", SgiImageFile, _accept) Image.register_extension("SGI", ".bw") Image.register_extension("SGI", ".rgb") Image.register_extension("SGI", ".rgba") +Image.register_extension("SGI", ".sgi") -Image.register_extension("SGI", ".sgi") # really? +# End of file diff --git a/PIL/SunImagePlugin.py b/PIL/SunImagePlugin.py index 0db02ad25..e0a7aa6ee 100644 --- a/PIL/SunImagePlugin.py +++ b/PIL/SunImagePlugin.py @@ -29,6 +29,7 @@ i32 = _binary.i32be def _accept(prefix): return i32(prefix) == 0x59a66a95 + ## # Image plugin for Sun raster files. @@ -70,9 +71,9 @@ class SunImageFile(ImageFile.ImageFile): stride = (((self.size[0] * depth + 7) // 8) + 3) & (~3) if compression == 1: - self.tile = [("raw", (0,0)+self.size, offset, (rawmode, stride))] + self.tile = [("raw", (0, 0)+self.size, offset, (rawmode, stride))] elif compression == 2: - self.tile = [("sun_rle", (0,0)+self.size, offset, rawmode)] + self.tile = [("sun_rle", (0, 0)+self.size, offset, rawmode)] # # registry diff --git a/PIL/TiffImagePlugin.py b/PIL/TiffImagePlugin.py index 11b92747c..9bef30ebe 100644 --- a/PIL/TiffImagePlugin.py +++ b/PIL/TiffImagePlugin.py @@ -49,17 +49,18 @@ from PIL import _binary from PIL._util import isStringType import warnings -import array, sys +import array +import sys import collections import itertools import os -# Set these to true to force use of libtiff for reading or writing. +# Set these to true to force use of libtiff for reading or writing. READ_LIBTIFF = False -WRITE_LIBTIFF= False +WRITE_LIBTIFF = False -II = b"II" # little-endian (intel-style) -MM = b"MM" # big-endian (motorola-style) +II = b"II" # little-endian (Intel style) +MM = b"MM" # big-endian (Motorola style) i8 = _binary.i8 o8 = _binary.o8 @@ -109,8 +110,8 @@ EXTRASAMPLES = 338 SAMPLEFORMAT = 339 JPEGTABLES = 347 COPYRIGHT = 33432 -IPTC_NAA_CHUNK = 33723 # newsphoto properties -PHOTOSHOP_CHUNK = 34377 # photoshop properties +IPTC_NAA_CHUNK = 33723 # newsphoto properties +PHOTOSHOP_CHUNK = 34377 # photoshop properties ICCPROFILE = 34675 EXIFIFD = 34665 XMP = 700 @@ -126,10 +127,10 @@ COMPRESSION_INFO = { 3: "group3", 4: "group4", 5: "tiff_lzw", - 6: "tiff_jpeg", # obsolete + 6: "tiff_jpeg", # obsolete 7: "jpeg", 8: "tiff_adobe_deflate", - 32771: "tiff_raw_16", # 16-bit padding + 32771: "tiff_raw_16", # 16-bit padding 32773: "packbits", 32809: "tiff_thunderscan", 32946: "tiff_deflate", @@ -137,7 +138,7 @@ COMPRESSION_INFO = { 34677: "tiff_sgilog24", } -COMPRESSION_INFO_REV = dict([(v,k) for (k,v) in COMPRESSION_INFO.items()]) +COMPRESSION_INFO_REV = dict([(v, k) for (k, v) in COMPRESSION_INFO.items()]) OPEN_INFO = { # (ByteOrder, PhotoInterpretation, SampleFormat, FillOrder, BitsPerSample, @@ -150,7 +151,7 @@ OPEN_INFO = { (II, 1, 1, 1, (1,), ()): ("1", "1"), (II, 1, 1, 2, (1,), ()): ("1", "1;R"), (II, 1, 1, 1, (8,), ()): ("L", "L"), - (II, 1, 1, 1, (8,8), (2,)): ("LA", "LA"), + (II, 1, 1, 1, (8, 8), (2,)): ("LA", "LA"), (II, 1, 1, 2, (8,), ()): ("L", "L;R"), (II, 1, 1, 1, (12,), ()): ("I;16", "I;12"), (II, 1, 1, 1, (16,), ()): ("I;16", "I;16"), @@ -158,13 +159,13 @@ OPEN_INFO = { (II, 1, 1, 1, (32,), ()): ("I", "I;32N"), (II, 1, 2, 1, (32,), ()): ("I", "I;32S"), (II, 1, 3, 1, (32,), ()): ("F", "F;32F"), - (II, 2, 1, 1, (8,8,8), ()): ("RGB", "RGB"), - (II, 2, 1, 2, (8,8,8), ()): ("RGB", "RGB;R"), - (II, 2, 1, 1, (8,8,8,8), ()): ("RGBA", "RGBA"), # missing ExtraSamples - (II, 2, 1, 1, (8,8,8,8), (0,)): ("RGBX", "RGBX"), - (II, 2, 1, 1, (8,8,8,8), (1,)): ("RGBA", "RGBa"), - (II, 2, 1, 1, (8,8,8,8), (2,)): ("RGBA", "RGBA"), - (II, 2, 1, 1, (8,8,8,8), (999,)): ("RGBA", "RGBA"), # corel draw 10 + (II, 2, 1, 1, (8, 8, 8), ()): ("RGB", "RGB"), + (II, 2, 1, 2, (8, 8, 8), ()): ("RGB", "RGB;R"), + (II, 2, 1, 1, (8, 8, 8, 8), ()): ("RGBA", "RGBA"), # missing ExtraSamples + (II, 2, 1, 1, (8, 8, 8, 8), (0,)): ("RGBX", "RGBX"), + (II, 2, 1, 1, (8, 8, 8, 8), (1,)): ("RGBA", "RGBa"), + (II, 2, 1, 1, (8, 8, 8, 8), (2,)): ("RGBA", "RGBA"), + (II, 2, 1, 1, (8, 8, 8, 8), (999,)): ("RGBA", "RGBA"), # Corel Draw 10 (II, 3, 1, 1, (1,), ()): ("P", "P;1"), (II, 3, 1, 2, (1,), ()): ("P", "P;1R"), (II, 3, 1, 1, (2,), ()): ("P", "P;2"), @@ -172,11 +173,11 @@ OPEN_INFO = { (II, 3, 1, 1, (4,), ()): ("P", "P;4"), (II, 3, 1, 2, (4,), ()): ("P", "P;4R"), (II, 3, 1, 1, (8,), ()): ("P", "P"), - (II, 3, 1, 1, (8,8), (2,)): ("PA", "PA"), + (II, 3, 1, 1, (8, 8), (2,)): ("PA", "PA"), (II, 3, 1, 2, (8,), ()): ("P", "P;R"), - (II, 5, 1, 1, (8,8,8,8), ()): ("CMYK", "CMYK"), - (II, 6, 1, 1, (8,8,8), ()): ("YCbCr", "YCbCr"), - (II, 8, 1, 1, (8,8,8), ()): ("LAB", "LAB"), + (II, 5, 1, 1, (8, 8, 8, 8), ()): ("CMYK", "CMYK"), + (II, 6, 1, 1, (8, 8, 8), ()): ("YCbCr", "YCbCr"), + (II, 8, 1, 1, (8, 8, 8), ()): ("LAB", "LAB"), (MM, 0, 1, 1, (1,), ()): ("1", "1;I"), (MM, 0, 1, 2, (1,), ()): ("1", "1;IR"), @@ -185,18 +186,18 @@ OPEN_INFO = { (MM, 1, 1, 1, (1,), ()): ("1", "1"), (MM, 1, 1, 2, (1,), ()): ("1", "1;R"), (MM, 1, 1, 1, (8,), ()): ("L", "L"), - (MM, 1, 1, 1, (8,8), (2,)): ("LA", "LA"), + (MM, 1, 1, 1, (8, 8), (2,)): ("LA", "LA"), (MM, 1, 1, 2, (8,), ()): ("L", "L;R"), (MM, 1, 1, 1, (16,), ()): ("I;16B", "I;16B"), (MM, 1, 2, 1, (16,), ()): ("I;16BS", "I;16BS"), (MM, 1, 2, 1, (32,), ()): ("I;32BS", "I;32BS"), (MM, 1, 3, 1, (32,), ()): ("F", "F;32BF"), - (MM, 2, 1, 1, (8,8,8), ()): ("RGB", "RGB"), - (MM, 2, 1, 2, (8,8,8), ()): ("RGB", "RGB;R"), - (MM, 2, 1, 1, (8,8,8,8), (0,)): ("RGBX", "RGBX"), - (MM, 2, 1, 1, (8,8,8,8), (1,)): ("RGBA", "RGBa"), - (MM, 2, 1, 1, (8,8,8,8), (2,)): ("RGBA", "RGBA"), - (MM, 2, 1, 1, (8,8,8,8), (999,)): ("RGBA", "RGBA"), # corel draw 10 + (MM, 2, 1, 1, (8, 8, 8), ()): ("RGB", "RGB"), + (MM, 2, 1, 2, (8, 8, 8), ()): ("RGB", "RGB;R"), + (MM, 2, 1, 1, (8, 8, 8, 8), (0,)): ("RGBX", "RGBX"), + (MM, 2, 1, 1, (8, 8, 8, 8), (1,)): ("RGBA", "RGBa"), + (MM, 2, 1, 1, (8, 8, 8, 8), (2,)): ("RGBA", "RGBA"), + (MM, 2, 1, 1, (8, 8, 8, 8), (999,)): ("RGBA", "RGBA"), # Corel Draw 10 (MM, 3, 1, 1, (1,), ()): ("P", "P;1"), (MM, 3, 1, 2, (1,), ()): ("P", "P;1R"), (MM, 3, 1, 1, (2,), ()): ("P", "P;2"), @@ -204,19 +205,21 @@ OPEN_INFO = { (MM, 3, 1, 1, (4,), ()): ("P", "P;4"), (MM, 3, 1, 2, (4,), ()): ("P", "P;4R"), (MM, 3, 1, 1, (8,), ()): ("P", "P"), - (MM, 3, 1, 1, (8,8), (2,)): ("PA", "PA"), + (MM, 3, 1, 1, (8, 8), (2,)): ("PA", "PA"), (MM, 3, 1, 2, (8,), ()): ("P", "P;R"), - (MM, 5, 1, 1, (8,8,8,8), ()): ("CMYK", "CMYK"), - (MM, 6, 1, 1, (8,8,8), ()): ("YCbCr", "YCbCr"), - (MM, 8, 1, 1, (8,8,8), ()): ("LAB", "LAB"), + (MM, 5, 1, 1, (8, 8, 8, 8), ()): ("CMYK", "CMYK"), + (MM, 6, 1, 1, (8, 8, 8), ()): ("YCbCr", "YCbCr"), + (MM, 8, 1, 1, (8, 8, 8), ()): ("LAB", "LAB"), } PREFIXES = [b"MM\000\052", b"II\052\000", b"II\xBC\000"] + def _accept(prefix): return prefix[:4] in PREFIXES + ## # Wrapper for TIFF IFDs. @@ -238,7 +241,7 @@ class ImageFileDirectory(collections.MutableMapping): Value: integer corresponding to the data type from `TiffTags.TYPES` - 'internal' + 'internal' * self.tags = {} Key: numerical tiff tag number Value: Decoded data, Generally a tuple. * If set from __setval__ -- always a tuple @@ -276,7 +279,7 @@ class ImageFileDirectory(collections.MutableMapping): #: For a complete dictionary, use the as_dict method. self.tags = {} self.tagdata = {} - self.tagtype = {} # added 2008-06-05 by Florian Hoech + self.tagtype = {} # added 2008-06-05 by Florian Hoech self.next = None def __str__(self): @@ -287,7 +290,9 @@ class ImageFileDirectory(collections.MutableMapping): return dict(self.items()) def named(self): - """Returns the complete tag dictionary, with named tags where posible.""" + """ + Returns the complete tag dictionary, with named tags where posible. + """ from PIL import TiffTags result = {} for tag_code, value in self.items(): @@ -295,7 +300,6 @@ class ImageFileDirectory(collections.MutableMapping): result[tag_name] = value return result - # dictionary API def __len__(self): @@ -305,7 +309,7 @@ class ImageFileDirectory(collections.MutableMapping): try: return self.tags[tag] except KeyError: - data = self.tagdata[tag] # unpack on the fly + data = self.tagdata[tag] # unpack on the fly type = self.tagtype[tag] size, handler = self.load_dispatch[type] self.tags[tag] = data = handler(self, data) @@ -319,7 +323,7 @@ class ImageFileDirectory(collections.MutableMapping): if tag == SAMPLEFORMAT: # work around broken (?) matrox library # (from Ted Wright, via Bob Klimek) - raise KeyError # use default + raise KeyError # use default raise ValueError("not a scalar") return value[0] except KeyError: @@ -433,7 +437,7 @@ class ImageFileDirectory(collections.MutableMapping): except KeyError: if Image.DEBUG: print("- unsupported type", typ) - continue # ignore unsupported type + continue # ignore unsupported type size, handler = dispatch @@ -449,14 +453,17 @@ class ImageFileDirectory(collections.MutableMapping): data = ifd[8:8+size] if len(data) != size: - warnings.warn("Possibly corrupt EXIF data. Expecting to read %d bytes but only got %d. Skipping tag %s" % (size, len(data), tag)) + warnings.warn("Possibly corrupt EXIF data. " + "Expecting to read %d bytes but only got %d. " + "Skipping tag %s" % (size, len(data), tag)) continue self.tagdata[tag] = data self.tagtype[tag] = typ if Image.DEBUG: - if tag in (COLORMAP, IPTC_NAA_CHUNK, PHOTOSHOP_CHUNK, ICCPROFILE, XMP): + if tag in (COLORMAP, IPTC_NAA_CHUNK, PHOTOSHOP_CHUNK, + ICCPROFILE, XMP): print("- value: " % size) else: print("- value:", self[tag]) @@ -489,10 +496,10 @@ class ImageFileDirectory(collections.MutableMapping): if tag in self.tagtype: typ = self.tagtype[tag] - + if Image.DEBUG: print ("Tag %s, Type: %s, Value: %s" % (tag, typ, value)) - + if typ == 1: # byte data if isinstance(value, tuple): @@ -512,14 +519,14 @@ class ImageFileDirectory(collections.MutableMapping): # and doesn't match the tiff spec: 8-bit byte that # contains a 7-bit ASCII code; the last byte must be # NUL (binary zero). Also, I don't think this was well - # excersized before. + # excersized before. data = value = b"" + value.encode('ascii', 'replace') + b"\0" else: # integer data if tag == STRIPOFFSETS: stripoffsets = len(directory) - typ = 4 # to avoid catch-22 - elif tag in (X_RESOLUTION, Y_RESOLUTION) or typ==5: + typ = 4 # to avoid catch-22 + elif tag in (X_RESOLUTION, Y_RESOLUTION) or typ == 5: # identify rational data fields typ = 5 if isinstance(value[0], tuple): @@ -541,7 +548,8 @@ class ImageFileDirectory(collections.MutableMapping): typname = TiffTags.TYPES.get(typ, "unknown") print("save: %s (%d)" % (tagname, tag), end=' ') print("- type: %s (%d)" % (typname, typ), end=' ') - if tag in (COLORMAP, IPTC_NAA_CHUNK, PHOTOSHOP_CHUNK, ICCPROFILE, XMP): + if tag in (COLORMAP, IPTC_NAA_CHUNK, PHOTOSHOP_CHUNK, + ICCPROFILE, XMP): size = len(data) print("- value: " % size) else: @@ -558,9 +566,9 @@ class ImageFileDirectory(collections.MutableMapping): count = count // 2 # adjust for rational data field append((tag, typ, count, o32(offset), data)) - offset = offset + len(data) + offset += len(data) if offset & 1: - offset = offset + 1 # word padding + offset += 1 # word padding # update strip offset data to point beyond auxiliary data if stripoffsets is not None: @@ -576,7 +584,7 @@ class ImageFileDirectory(collections.MutableMapping): fp.write(o16(tag) + o16(typ) + o32(count) + value) # -- overwrite here for multi-page -- - fp.write(b"\0\0\0\0") # end of directory + fp.write(b"\0\0\0\0") # end of directory # pass 3: write auxiliary data to file for tag, typ, count, value, data in directory: @@ -586,6 +594,7 @@ class ImageFileDirectory(collections.MutableMapping): return offset + ## # Image plugin for TIFF files. @@ -616,7 +625,7 @@ class TiffImageFile(ImageFile.ImageFile): print ("- __first:", self.__first) print ("- ifh: ", ifh) - # and load the first frame + # and load the first frame self._seek(0) def seek(self, frame): @@ -644,7 +653,7 @@ class TiffImageFile(ImageFile.ImageFile): self.fp.seek(self.__next) self.tag.load(self.fp) self.__next = self.tag.next - self.__frame = self.__frame + 1 + self.__frame += 1 self._setup() def _tell(self): @@ -694,9 +703,11 @@ class TiffImageFile(ImageFile.ImageFile): if not len(self.tile) == 1: raise IOError("Not exactly one tile") - # (self._compression, (extents tuple), 0, (rawmode, self._compression, fp)) + # (self._compression, (extents tuple), + # 0, (rawmode, self._compression, fp)) ignored, extents, ignored_2, args = self.tile[0] - decoder = Image._getdecoder(self.mode, 'libtiff', args, self.decoderconfig) + decoder = Image._getdecoder(self.mode, 'libtiff', args, + self.decoderconfig) try: decoder.setimage(self.im, extents) except ValueError: @@ -706,35 +717,35 @@ class TiffImageFile(ImageFile.ImageFile): # We've got a stringio like thing passed in. Yay for all in memory. # The decoder needs the entire file in one shot, so there's not # a lot we can do here other than give it the entire file. - # unless we could do something like get the address of the underlying - # string for stringio. + # unless we could do something like get the address of the + # underlying string for stringio. # # Rearranging for supporting byteio items, since they have a fileno - # that returns an IOError if there's no underlying fp. Easier to deal - # with here by reordering. + # that returns an IOError if there's no underlying fp. Easier to + # dea. with here by reordering. if Image.DEBUG: print ("have getvalue. just sending in a string from getvalue") - n,err = decoder.decode(self.fp.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.") self.fp.seek(0) - n,err = decoder.decode(b"fpfp") # 4 bytes, otherwise the trace might error out + # 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") # UNDONE -- so much for that buffer size thing. - n,err = decoder.decode(self.fp.read()) - + n, err = decoder.decode(self.fp.read()) self.tile = [] 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() - self.fp = None # might be shared + self.fp = None # might be shared if err < 0: raise IOError(err) @@ -810,11 +821,11 @@ class TiffImageFile(ImageFile.ImageFile): xres = xres[0] / (xres[1] or 1) yres = yres[0] / (yres[1] or 1) resunit = getscalar(RESOLUTION_UNIT, 1) - if resunit == 2: # dots per inch + if resunit == 2: # dots per inch self.info["dpi"] = xres, yres - elif resunit == 3: # dots per centimeter. convert to dpi + elif resunit == 3: # dots per centimeter. convert to dpi self.info["dpi"] = xres * 2.54, yres * 2.54 - else: # No absolute unit of measurement + else: # No absolute unit of measurement self.info["resolution"] = xres, yres # build tile descriptors @@ -825,13 +836,16 @@ class TiffImageFile(ImageFile.ImageFile): offsets = self.tag[STRIPOFFSETS] h = getscalar(ROWSPERSTRIP, ysize) w = self.size[0] - if READ_LIBTIFF or self._compression in ["tiff_ccitt", "group3", "group4", - "tiff_jpeg", "tiff_adobe_deflate", - "tiff_thunderscan", "tiff_deflate", - "tiff_sgilog", "tiff_sgilog24", + if READ_LIBTIFF or self._compression in ["tiff_ccitt", "group3", + "group4", "tiff_jpeg", + "tiff_adobe_deflate", + "tiff_thunderscan", + "tiff_deflate", + "tiff_sgilog", + "tiff_sgilog24", "tiff_raw_16"]: - ## if Image.DEBUG: - ## print "Activating g4 compression for whole file" + # if Image.DEBUG: + # print "Activating g4 compression for whole file" # Decoder expects entire file as one tile. # There's a buffer size limit in load (64k) @@ -850,7 +864,8 @@ class TiffImageFile(ImageFile.ImageFile): # libtiff closes the file descriptor, so pass in a dup. try: - fp = hasattr(self.fp, "fileno") and os.dup(self.fp.fileno()) + fp = hasattr(self.fp, "fileno") and \ + os.dup(self.fp.fileno()) except IOError: # io.BytesIO have a fileno, but returns an IOError if # it doesn't use a file descriptor. @@ -859,7 +874,7 @@ class TiffImageFile(ImageFile.ImageFile): # libtiff handles the fillmode for us, so 1;IR should # actually be 1;I. Including the R double reverses the # bits, so stripes of the image are reversed. See - # https://github.com/python-imaging/Pillow/issues/279 + # https://github.com/python-pillow/Pillow/issues/279 if fillorder == 2: key = ( self.tag.prefix, photo, format, 1, @@ -881,7 +896,7 @@ class TiffImageFile(ImageFile.ImageFile): # Offset in the tile tuple is 0, we go from 0,0 to # w,h, and we only do this once -- eds - a = (rawmode, self._compression, fp ) + a = (rawmode, self._compression, fp) self.tile.append( (self._compression, (0, 0, w, ysize), @@ -893,14 +908,14 @@ class TiffImageFile(ImageFile.ImageFile): a = self._decoder(rawmode, l, i) self.tile.append( (self._compression, - (0, min(y, ysize), w, min(y+h, ysize)), - offsets[i], a)) + (0, min(y, ysize), w, min(y+h, ysize)), + offsets[i], a)) if Image.DEBUG: print ("tiles: ", self.tile) y = y + h if y >= self.size[1]: x = y = 0 - l = l + 1 + l += 1 a = None elif TILEOFFSETS in self.tag: # tiled image @@ -914,14 +929,14 @@ class TiffImageFile(ImageFile.ImageFile): # is not a multiple of the tile size... self.tile.append( (self._compression, - (x, y, x+w, y+h), - o, a)) + (x, y, x+w, y+h), + o, a)) x = x + w if x >= self.size[0]: x, y = 0, y + h if y >= self.size[1]: x = y = 0 - l = l + 1 + l += 1 a = None else: if Image.DEBUG: @@ -937,25 +952,27 @@ class TiffImageFile(ImageFile.ImageFile): # -------------------------------------------------------------------- # Write TIFF files -# little endian is default except for image modes with explict big endian byte-order +# little endian is default except for image modes with +# explict big endian byte-order SAVE_INFO = { - # mode => rawmode, byteorder, photometrics, sampleformat, bitspersample, extra + # mode => rawmode, byteorder, photometrics, + # sampleformat, bitspersample, extra "1": ("1", II, 1, 1, (1,), None), "L": ("L", II, 1, 1, (8,), None), - "LA": ("LA", II, 1, 1, (8,8), 2), + "LA": ("LA", II, 1, 1, (8, 8), 2), "P": ("P", II, 3, 1, (8,), None), - "PA": ("PA", II, 3, 1, (8,8), 2), + "PA": ("PA", II, 3, 1, (8, 8), 2), "I": ("I;32S", II, 1, 2, (32,), None), "I;16": ("I;16", II, 1, 1, (16,), None), "I;16S": ("I;16S", II, 1, 2, (16,), None), "F": ("F;32F", II, 1, 3, (32,), None), - "RGB": ("RGB", II, 2, 1, (8,8,8), None), - "RGBX": ("RGBX", II, 2, 1, (8,8,8,8), 0), - "RGBA": ("RGBA", II, 2, 1, (8,8,8,8), 2), - "CMYK": ("CMYK", II, 5, 1, (8,8,8,8), None), - "YCbCr": ("YCbCr", II, 6, 1, (8,8,8), None), - "LAB": ("LAB", II, 8, 1, (8,8,8), None), + "RGB": ("RGB", II, 2, 1, (8, 8, 8), None), + "RGBX": ("RGBX", II, 2, 1, (8, 8, 8, 8), 0), + "RGBA": ("RGBA", II, 2, 1, (8, 8, 8, 8), 2), + "CMYK": ("CMYK", II, 5, 1, (8, 8, 8, 8), None), + "YCbCr": ("YCbCr", II, 6, 1, (8, 8, 8), None), + "LAB": ("LAB", II, 8, 1, (8, 8, 8), None), "I;32BS": ("I;32BS", MM, 1, 2, (32,), None), "I;16B": ("I;16B", MM, 1, 1, (16,), None), @@ -963,6 +980,7 @@ SAVE_INFO = { "F;32BF": ("F;32BF", MM, 1, 3, (32,), None), } + def _cvt_res(value): # convert value to TIFF rational number -- (numerator, denominator) if isinstance(value, collections.Sequence): @@ -973,6 +991,7 @@ def _cvt_res(value): value = float(value) return (int(value * 65536), 65536) + def _save(im, fp, filename): try: @@ -982,13 +1001,14 @@ def _save(im, fp, filename): ifd = ImageFileDirectory(prefix) - compression = im.encoderinfo.get('compression',im.info.get('compression','raw')) + compression = im.encoderinfo.get('compression', im.info.get('compression', + 'raw')) - libtiff = WRITE_LIBTIFF or compression != 'raw' + libtiff = WRITE_LIBTIFF or compression != 'raw' # required for color libtiff images ifd[PLANAR_CONFIGURATION] = getattr(im, '_planar_configuration', 1) - + # -- multi-page -- skip TIFF header on subsequent pages if not libtiff and fp.tell() == 0: # tiff header (write via IFD to get everything right) @@ -999,17 +1019,16 @@ def _save(im, fp, filename): ifd[IMAGELENGTH] = im.size[1] # write any arbitrary tags passed in as an ImageFileDirectory - info = im.encoderinfo.get("tiffinfo",{}) + info = im.encoderinfo.get("tiffinfo", {}) if Image.DEBUG: - print ("Tiffinfo Keys: %s"% info.keys) + print("Tiffinfo Keys: %s" % info.keys) keys = list(info.keys()) for key in keys: ifd[key] = info.get(key) try: ifd.tagtype[key] = info.tagtype[key] except: - pass # might not be an IFD, Might not have populated type - + pass # might not be an IFD, Might not have populated type # additions written by Greg Couch, gregc@cgl.ucsf.edu # inspired by image-sig posting from Kevin Cazabon, kcazabon@home.com @@ -1025,12 +1044,12 @@ def _save(im, fp, filename): # which support profiles as TIFF) -- 2008-06-06 Florian Hoech 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"]) + = _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: @@ -1077,8 +1096,9 @@ def _save(im, fp, filename): stride = len(bits) * ((im.size[0]*bits[0]+7)//8) ifd[ROWSPERSTRIP] = im.size[1] ifd[STRIPBYTECOUNTS] = stride * im.size[1] - ifd[STRIPOFFSETS] = 0 # this is adjusted by IFD writer - ifd[COMPRESSION] = COMPRESSION_INFO_REV.get(compression,1) # no compression by default + ifd[STRIPOFFSETS] = 0 # this is adjusted by IFD writer + # no compression by default: + ifd[COMPRESSION] = COMPRESSION_INFO_REV.get(compression, 1) if libtiff: if Image.DEBUG: @@ -1089,23 +1109,27 @@ def _save(im, fp, filename): fp.seek(0) _fp = os.dup(fp.fileno()) - blocklist = [STRIPOFFSETS, STRIPBYTECOUNTS, ROWSPERSTRIP, ICCPROFILE] # ICC Profile crashes. - atts={} - # bits per sample is a single short in the tiff directory, not a list. + # ICC Profile crashes. + blocklist = [STRIPOFFSETS, STRIPBYTECOUNTS, ROWSPERSTRIP, ICCPROFILE] + atts = {} + # bits per sample is a single short in the tiff directory, not a list. atts[BITSPERSAMPLE] = bits[0] # Merge the ones that we have with (optional) more bits from # the original file, e.g x,y resolution so that we can # save(load('')) == original file. - for k,v in itertools.chain(ifd.items(), getattr(im, 'ifd', {}).items()): + for k, v in itertools.chain(ifd.items(), + getattr(im, 'ifd', {}).items()): if k not in atts and k not in blocklist: if type(v[0]) == tuple and len(v) > 1: # A tuple of more than one rational tuples - # flatten to floats, following tiffcp.c->cpTag->TIFF_RATIONAL + # flatten to floats, + # following tiffcp.c->cpTag->TIFF_RATIONAL atts[k] = [float(elt[0])/float(elt[1]) for elt in v] continue if type(v[0]) == tuple and len(v) == 1: # A tuple of one rational tuples - # flatten to floats, following tiffcp.c->cpTag->TIFF_RATIONAL + # flatten to floats, + # following tiffcp.c->cpTag->TIFF_RATIONAL atts[k] = float(v[0][0])/float(v[0][1]) continue if type(v) == tuple and len(v) > 2: @@ -1115,7 +1139,8 @@ def _save(im, fp, filename): continue if type(v) == tuple and len(v) == 2: # one rational tuple - # flatten to float, following tiffcp.c->cpTag->TIFF_RATIONAL + # flatten to float, + # following tiffcp.c->cpTag->TIFF_RATIONAL atts[k] = float(v[0])/float(v[1]) continue if type(v) == tuple and len(v) == 1: @@ -1141,9 +1166,10 @@ def _save(im, fp, filename): a = (rawmode, compression, _fp, filename, atts) # print (im.mode, compression, a, im.encoderconfig) e = Image._getencoder(im.mode, 'libtiff', a, im.encoderconfig) - e.setimage(im.im, (0,0)+im.size) + e.setimage(im.im, (0, 0)+im.size) while True: - l, s, d = e.encode(16*1024) # undone, change to self.decodermaxblock + # undone, change to self.decodermaxblock: + l, s, d = e.encode(16*1024) if not _fp: fp.write(d) if s: @@ -1155,13 +1181,12 @@ def _save(im, fp, filename): offset = ifd.save(fp) ImageFile._save(im, fp, [ - ("raw", (0,0)+im.size, offset, (rawmode, stride, 1)) + ("raw", (0, 0)+im.size, offset, (rawmode, stride, 1)) ]) - # -- helper for multi-page save -- if "_debug_multipage" in im.encoderinfo: - #just to access o32 and o16 (using correct byte order) + # just to access o32 and o16 (using correct byte order) im._debug_multipage = ifd # diff --git a/PIL/TiffTags.py b/PIL/TiffTags.py index 9d4530051..966779ce9 100644 --- a/PIL/TiffTags.py +++ b/PIL/TiffTags.py @@ -147,6 +147,100 @@ TAGS = { # ICC Profile 34675: "ICCProfile", + # Additional Exif Info + 33434: "ExposureTime", + 33437: "FNumber", + 34850: "ExposureProgram", + 34852: "SpectralSensitivity", + 34853: "GPSInfoIFD", + 34855: "ISOSpeedRatings", + 34856: "OECF", + 34864: "SensitivityType", + 34865: "StandardOutputSensitivity", + 34866: "RecommendedExposureIndex", + 34867: "ISOSpeed", + 34868: "ISOSpeedLatitudeyyy", + 34869: "ISOSpeedLatitudezzz", + 36864: "ExifVersion", + 36867: "DateTimeOriginal", + 36868: "DateTImeDigitized", + 37121: "ComponentsConfiguration", + 37122: "CompressedBitsPerPixel", + 37377: "ShutterSpeedValue", + 37378: "ApertureValue", + 37379: "BrightnessValue", + 37380: "ExposureBiasValue", + 37381: "MaxApertureValue", + 37382: "SubjectDistance", + 37383: "MeteringMode", + 37384: "LightSource", + 37385: "Flash", + 37386: "FocalLength", + 37396: "SubjectArea", + 37500: "MakerNote", + 37510: "UserComment", + 37520: "SubSec", + 37521: "SubSecTimeOriginal", + 37522: "SubsecTimeDigitized", + 40960: "FlashPixVersion", + 40961: "ColorSpace", + 40962: "PixelXDimension", + 40963: "PixelYDimension", + 40964: "RelatedSoundFile", + 40965: "InteroperabilityIFD", + 41483: "FlashEnergy", + 41484: "SpatialFrequencyResponse", + 41486: "FocalPlaneXResolution", + 41487: "FocalPlaneYResolution", + 41488: "FocalPlaneResolutionUnit", + 41492: "SubjectLocation", + 41493: "ExposureIndex", + 41495: "SensingMethod", + 41728: "FileSource", + 41729: "SceneType", + 41730: "CFAPattern", + 41985: "CustomRendered", + 41986: "ExposureMode", + 41987: "WhiteBalance", + 41988: "DigitalZoomRatio", + 41989: "FocalLengthIn35mmFilm", + 41990: "SceneCaptureType", + 41991: "GainControl", + 41992: "Contrast", + 41993: "Saturation", + 41994: "Sharpness", + 41995: "DeviceSettingDescription", + 41996: "SubjectDistanceRange", + 42016: "ImageUniqueID", + 42032: "CameraOwnerName", + 42033: "BodySerialNumber", + 42034: "LensSpecification", + 42035: "LensMake", + 42036: "LensModel", + 42037: "LensSerialNumber", + 42240: "Gamma", + + # MP Info + 45056: "MPFVersion", + 45057: "NumberOfImages", + 45058: "MPEntry", + 45059: "ImageUIDList", + 45060: "TotalFrames", + 45313: "MPIndividualNum", + 45569: "PanOrientation", + 45570: "PanOverlap_H", + 45571: "PanOverlap_V", + 45572: "BaseViewpointNum", + 45573: "ConvergenceAngle", + 45574: "BaselineLength", + 45575: "VerticalDivergence", + 45576: "AxisDistance_X", + 45577: "AxisDistance_Y", + 45578: "AxisDistance_Z", + 45579: "YawAngle", + 45580: "PitchAngle", + 45581: "RollAngle", + # Adobe DNG 50706: "DNGVersion", 50707: "DNGBackwardVersion", @@ -161,7 +255,7 @@ TAGS = { 50716: "BlackLevelDeltaV", 50717: "WhiteLevel", 50718: "DefaultScale", - 50741: "BestQualityScale", + 50741: "BestQualityScale", # FIXME! Dictionary contains duplicate keys 50741 50719: "DefaultCropOrigin", 50720: "DefaultCropSize", 50778: "CalibrationIlluminant1", @@ -185,7 +279,7 @@ TAGS = { 50737: "ChromaBlurRadius", 50738: "AntiAliasStrength", 50740: "DNGPrivateData", - 50741: "MakerNoteSafety", + 50741: "MakerNoteSafety", # FIXME! Dictionary contains duplicate keys 50741 #ImageJ 50838: "ImageJMetaDataByteCounts", # private tag registered with Adobe diff --git a/PIL/WalImageFile.py b/PIL/WalImageFile.py index a962e8a99..d494bfd58 100644 --- a/PIL/WalImageFile.py +++ b/PIL/WalImageFile.py @@ -14,11 +14,11 @@ # # NOTE: This format cannot be automatically recognized, so the reader -# is not registered for use with Image.open(). To open a WEL file, use +# is not registered for use with Image.open(). To open a WAL file, use # the WalImageFile.open() function instead. # This reader is based on the specification available from: -# http://www.flipcode.com/tutorials/tut_q2levels.shtml +# http://www.flipcode.com/archives/Quake_2_BSP_File_Format.shtml # and has been tested with a few sample files found using google. from __future__ import print_function diff --git a/PIL/WmfImagePlugin.py b/PIL/WmfImagePlugin.py index 9a95a0713..40b2037ab 100644 --- a/PIL/WmfImagePlugin.py +++ b/PIL/WmfImagePlugin.py @@ -59,7 +59,7 @@ word = _binary.i16le def short(c, o=0): v = word(c, o) if v >= 32768: - v = v - 65536 + v -= 65536 return v dword = _binary.i32le diff --git a/PIL/XpmImagePlugin.py b/PIL/XpmImagePlugin.py index 701a23b64..517580895 100644 --- a/PIL/XpmImagePlugin.py +++ b/PIL/XpmImagePlugin.py @@ -29,6 +29,7 @@ xpm_head = re.compile(b"\"([0-9]*) ([0-9]*) ([0-9]*) ([0-9]*)") def _accept(prefix): return prefix[:9] == b"/* XPM */" + ## # Image plugin for X11 pixel maps. @@ -86,9 +87,9 @@ class XpmImageFile(ImageFile.ImageFile): elif rgb[0:1] == b"#": # FIXME: handle colour names (see ImagePalette.py) rgb = int(rgb[1:], 16) - palette[c] = o8((rgb >> 16) & 255) +\ - o8((rgb >> 8) & 255) +\ - o8(rgb & 255) + palette[c] = (o8((rgb >> 16) & 255) + + o8((rgb >> 8) & 255) + + o8(rgb & 255)) else: # unknown colour raise ValueError("cannot read this XPM file") diff --git a/PIL/__init__.py b/PIL/__init__.py index c35f6207e..7b4b8abfa 100644 --- a/PIL/__init__.py +++ b/PIL/__init__.py @@ -12,10 +12,9 @@ # ;-) VERSION = '1.1.7' # PIL version -PILLOW_VERSION = '2.4.0' # Pillow +PILLOW_VERSION = '2.5.3' # Pillow -_plugins = ['ArgImagePlugin', - 'BmpImagePlugin', +_plugins = ['BmpImagePlugin', 'BufrStubImagePlugin', 'CurImagePlugin', 'DcxImagePlugin', @@ -37,6 +36,7 @@ _plugins = ['ArgImagePlugin', 'McIdasImagePlugin', 'MicImagePlugin', 'MpegImagePlugin', + 'MpoImagePlugin', 'MspImagePlugin', 'PalmImagePlugin', 'PcdImagePlugin', diff --git a/PIL/_util.py b/PIL/_util.py index eb5c2c242..51c6f6887 100644 --- a/PIL/_util.py +++ b/PIL/_util.py @@ -3,20 +3,25 @@ import os if bytes is str: def isStringType(t): return isinstance(t, basestring) + def isPath(f): return isinstance(f, basestring) else: def isStringType(t): return isinstance(t, str) + def isPath(f): return isinstance(f, (bytes, str)) + # Checks if an object is a string, and that it points to a directory. def isDirectory(f): return isPath(f) and os.path.isdir(f) + class deferred_error(object): def __init__(self, ex): self.ex = ex + def __getattr__(self, elt): raise self.ex diff --git a/PIL/tests.py b/PIL/tests.py deleted file mode 100644 index eb4a8342d..000000000 --- a/PIL/tests.py +++ /dev/null @@ -1,17 +0,0 @@ -import unittest - - -class PillowTests(unittest.TestCase): - """ - Can we start moving the test suite here? - """ - - def test_suite_should_move_here(self): - """ - Great idea! - """ - assert True is True - - -if __name__ == '__main__': - unittest.main() diff --git a/README.rst b/README.rst index 277c5d01f..482a1e2ec 100644 --- a/README.rst +++ b/README.rst @@ -3,10 +3,10 @@ Pillow *Python Imaging Library (Fork)* -Pillow is the "friendly" PIL fork by Alex Clark and Contributors. PIL is the Python Imaging Library by Fredrik Lundh and Contributors. +Pillow is the "friendly" PIL fork by `Alex Clark and Contributors `_. PIL is the Python Imaging Library by Fredrik Lundh and Contributors. For more information, please `read the documentation `_, `check the changelog `_ and `find out how to contribute `_. -.. image:: https://travis-ci.org/python-imaging/Pillow.svg?branch=master - :target: https://travis-ci.org/python-imaging/Pillow +.. image:: https://travis-ci.org/python-pillow/Pillow.svg?branch=master + :target: https://travis-ci.org/python-pillow/Pillow :alt: Travis CI build status .. image:: https://pypip.in/v/Pillow/badge.png @@ -17,7 +17,6 @@ Pillow is the "friendly" PIL fork by Alex Clark and Contributors. PIL is the Pyt :target: https://pypi.python.org/pypi/Pillow/ :alt: Number of PyPI downloads -.. image:: https://coveralls.io/repos/python-imaging/Pillow/badge.png?branch=master - :target: https://coveralls.io/r/python-imaging/Pillow?branch=master +.. image:: https://coveralls.io/repos/python-pillow/Pillow/badge.png?branch=master + :target: https://coveralls.io/r/python-pillow/Pillow?branch=master -The documentation is hosted at http://pillow.readthedocs.org/. It contains installation instructions, tutorials, reference, compatibility details, and more. diff --git a/Sane/README b/Sane/README.rst similarity index 78% rename from Sane/README rename to Sane/README.rst index fa6c8a05f..173934040 100644 --- a/Sane/README +++ b/Sane/README.rst @@ -1,5 +1,5 @@ - 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 @@ -9,11 +9,11 @@ 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): +To build this module, type (in the Sane directory):: python setup.py build -In order to install the module type: +In order to install the module type:: python setup.py install diff --git a/Scripts/README b/Scripts/README.rst similarity index 99% rename from Scripts/README rename to Scripts/README.rst index befa7e7be..1b26ea68c 100644 --- a/Scripts/README +++ b/Scripts/README.rst @@ -1,6 +1,5 @@ -------- Scripts -------- +======= This directory contains a number of more or less trivial utilities and demo programs. @@ -9,50 +8,50 @@ Comments and contributions are welcome. --------------------------------------------------------------------- pildriver.py (by Eric S. Raymond) +-------------------------------------------------------------------- A class implementing an image-processing calculator for scripts. Parses lists of commnds (or, called interactively, command-line arguments) into image loads, transformations, and saves. --------------------------------------------------------------------- viewer.py +-------------------------------------------------------------------- A simple image viewer. Can display all file formats handled by PIL. Transparent images are properly handled. --------------------------------------------------------------------- thresholder.py +-------------------------------------------------------------------- A simple utility that demonstrates how a transparent 1-bit overlay can be used to show the current thresholding of an 8-bit image. --------------------------------------------------------------------- enhancer.py +-------------------------------------------------------------------- Illustrates the ImageEnhance module. Drag the sliders to modify the images. This might be very slow on some platforms, depending on the Tk version. --------------------------------------------------------------------- painter.py +-------------------------------------------------------------------- Illustrates how a painting program could be based on PIL and Tk. Press the left mouse button and drag over the image to remove the colour. Some clever tricks have been used to get decent performance when updating the screen; see the sources for details. --------------------------------------------------------------------- player.py +-------------------------------------------------------------------- A simple image sequence player. You can use either a sequence format like FLI/FLC, GIF, or ARG, or give a number of images which are interpreted as frames in a sequence. All frames must have the same size. --------------------------------------------------------------------- gifmaker.py +-------------------------------------------------------------------- Convert a sequence file to a GIF animation. @@ -60,20 +59,20 @@ Note that the GIF encoder provided with this release of PIL writes uncompressed GIF files only, so the resulting animations are rather large compared with these created by other tools. --------------------------------------------------------------------- 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 diff --git a/Scripts/createfontdatachunk.py b/Scripts/createfontdatachunk.py new file mode 100644 index 000000000..0c860701a --- /dev/null +++ b/Scripts/createfontdatachunk.py @@ -0,0 +1,16 @@ +import base64 +import os +import sys + +if __name__ == "__main__": + # create font data chunk for embedding + font = "Tests/images/courB08" + print(" f._load_pilfont_data(") + print(" # %s" % os.path.basename(font)) + print(" BytesIO(base64.decodestring(b'''") + base64.encode(open(font + ".pil", "rb"), sys.stdout) + print("''')), Image.open(BytesIO(base64.decodestring(b'''") + base64.encode(open(font + ".pbm", "rb"), sys.stdout) + print("'''))))") + +# End of file diff --git a/Scripts/diffcover-install.sh b/Scripts/diffcover-install.sh new file mode 100755 index 000000000..93e06efe3 --- /dev/null +++ b/Scripts/diffcover-install.sh @@ -0,0 +1,7 @@ +# Fetch the remote master branch before running diff-cover on Travis CI. +# https://github.com/edx/diff-cover#troubleshooting +git fetch origin master:refs/remotes/origin/master + +# CFLAGS=-O0 means build with no optimisation. +# Makes build much quicker for lxml and other dependencies. +time CFLAGS=-O0 pip install --use-wheel diff_cover diff --git a/Scripts/diffcover-run.sh b/Scripts/diffcover-run.sh new file mode 100755 index 000000000..02efab6ae --- /dev/null +++ b/Scripts/diffcover-run.sh @@ -0,0 +1,4 @@ +coverage xml +diff-cover coverage.xml +diff-quality --violation=pyflakes +diff-quality --violation=pep8 diff --git a/Scripts/explode.py b/Scripts/explode.py index 90084a464..b8680f631 100644 --- a/Scripts/explode.py +++ b/Scripts/explode.py @@ -104,7 +104,7 @@ while True: except EOFError: break - ix = ix + 1 + ix += 1 if html: html.write("\n\n") diff --git a/Scripts/gifmaker.py b/Scripts/gifmaker.py index 9964f77b1..9fa5e71a4 100644 --- a/Scripts/gifmaker.py +++ b/Scripts/gifmaker.py @@ -100,7 +100,7 @@ def makedelta(fp, sequence): previous = im.copy() - frames = frames + 1 + frames += 1 fp.write(";") diff --git a/Scripts/pildriver.py b/Scripts/pildriver.py index 98708c897..e45b05008 100644 --- a/Scripts/pildriver.py +++ b/Scripts/pildriver.py @@ -486,7 +486,7 @@ class PILDriver: print("Stack: " + repr(self.stack)) top = self.top() if not isinstance(top, str): - continue; + continue funcname = "do_" + top if not hasattr(self, funcname): continue @@ -513,9 +513,9 @@ if __name__ == '__main__': while True: try: if sys.version_info[0] >= 3: - line = input('pildriver> '); + line = input('pildriver> ') else: - line = raw_input('pildriver> '); + line = raw_input('pildriver> ') except EOFError: print("\nPILDriver says goodbye.") break diff --git a/Scripts/pilfile.py b/Scripts/pilfile.py index 48514e88b..1b77b0e78 100644 --- a/Scripts/pilfile.py +++ b/Scripts/pilfile.py @@ -57,7 +57,7 @@ for o, a in opt: elif o == "-v": verify = 1 elif o == "-D": - Image.DEBUG = Image.DEBUG + 1 + Image.DEBUG += 1 def globfix(files): # expand wildcards where necessary diff --git a/Scripts/player.py b/Scripts/player.py index 84b636668..0c90286c5 100644 --- a/Scripts/player.py +++ b/Scripts/player.py @@ -18,25 +18,6 @@ import sys Image.DEBUG = 0 -# -------------------------------------------------------------------- -# experimental: support ARG animation scripts - -import ArgImagePlugin - -def applet_hook(animation, images): - app = animation(animation_display, images) - app.run() - -ArgImagePlugin.APPLET_HOOK = applet_hook - -class AppletDisplay: - def __init__(self, ui): - self.__ui = ui - def paste(self, im, bbox): - self.__ui.image.paste(im, bbox) - def update(self): - self.__ui.update_idletasks() - # -------------------------------------------------------------------- # an image animation player @@ -56,10 +37,6 @@ class UI(Label): else: self.image = ImageTk.PhotoImage(im) - # APPLET SUPPORT (very crude, and not 100% safe) - global animation_display - animation_display = AppletDisplay(self) - Label.__init__(self, master, image=self.image, bg="black", bd=0) self.update() diff --git a/Tests/32bit_segfault_check.py b/Tests/32bit_segfault_check.py new file mode 100644 index 000000000..822d524fd --- /dev/null +++ b/Tests/32bit_segfault_check.py @@ -0,0 +1,10 @@ +#!/usr/bin/env python + +from PIL import Image +import sys + + +if sys.maxsize < 2**32: + im = Image.new('L', (999999, 999999), 0) + + diff --git a/Tests/README.rst b/Tests/README.rst new file mode 100644 index 000000000..1b11a45b1 --- /dev/null +++ b/Tests/README.rst @@ -0,0 +1,45 @@ +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 +----------- + +Install:: + + pip install coverage nose + + +Execution +--------- + +**If Pillow has been built in-place** + +To run an individual test:: + + python Tests/test_image.py + +Run all the tests from the root of the Pillow source distribution:: + + nosetests -vx Tests/test_*.py + +Or with coverage:: + + coverage run --append --include=PIL/* -m nose -vx Tests/test_*.py + coverage report + coverage html + open htmlcov/index.html + +**If Pillow has been installed** + +To run an individual test:: + + ./test-installed.py Tests/test_image.py + +Run all the tests from the root of the Pillow source distribution:: + + ./test-installed.py + + + diff --git a/Tests/README.txt b/Tests/README.txt deleted file mode 100644 index 169bc4da5..000000000 --- a/Tests/README.txt +++ /dev/null @@ -1,14 +0,0 @@ -Minimalistic PIL test framework. - -Test scripts are named "test_xxx" and are supposed to output "ok". That's it. To run the tests:: - - python setup.py develop - -Run the tests from the root of the Pillow source distribution: - - python selftest.py - python Tests/run.py --installed - -To run an individual test: - - python Tests/test_image.py diff --git a/Tests/bench_cffi_access.py b/Tests/bench_cffi_access.py index 8f8ef937a..8aa322aff 100644 --- a/Tests/bench_cffi_access.py +++ b/Tests/bench_cffi_access.py @@ -1,23 +1,25 @@ -from tester import * +from helper import * -# not running this test by default. No DOS against travis. +# Not running this test by default. No DOS against Travis CI. from PIL import PyAccess -from PIL import Image import time + def iterate_get(size, access): - (w,h) = size + (w, h) = size for x in range(w): for y in range(h): - access[(x,y)] + access[(x, y)] + def iterate_set(size, access): - (w,h) = size + (w, h) = size for x in range(w): for y in range(h): - access[(x,y)] = (x %256,y%256,0) + access[(x, y)] = (x % 256, y % 256, 0) + def timer(func, label, *args): iterations = 5000 @@ -25,27 +27,34 @@ def timer(func, label, *args): for x in range(iterations): func(*args) if time.time()-starttime > 10: - print ("%s: breaking at %s iterations, %.6f per iteration"%(label, x+1, (time.time()-starttime)/(x+1.0))) + print("%s: breaking at %s iterations, %.6f per iteration" % ( + label, x+1, (time.time()-starttime)/(x+1.0))) break if x == iterations-1: endtime = time.time() - print ("%s: %.4f s %.6f per iteration" %(label, endtime-starttime, (endtime-starttime)/(x+1.0))) + print("%s: %.4f s %.6f per iteration" % ( + label, endtime-starttime, (endtime-starttime)/(x+1.0))) -def test_direct(): - im = lena() - im.load() - #im = Image.new( "RGB", (2000,2000), (1,3,2)) - caccess = im.im.pixel_access(False) - access = PyAccess.new(im, False) - assert_equal(caccess[(0,0)], access[(0,0)]) +class BenchCffiAccess(PillowTestCase): - 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) - timer(iterate_set, 'C-api - set', im.size, caccess) - - + def test_direct(self): + im = lena() + im.load() + # im = Image.new( "RGB", (2000, 2000), (1, 3, 2)) + caccess = im.im.pixel_access(False) + access = PyAccess.new(im, False) - + self.assertEqual(caccess[(0, 0)], access[(0, 0)]) + + 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) + timer(iterate_set, 'C-api - set', im.size, caccess) + + +if __name__ == '__main__': + unittest.main() + +# End of file diff --git a/Tests/bench_get.py b/Tests/bench_get.py index eca491600..8a1331d39 100644 --- a/Tests/bench_get.py +++ b/Tests/bench_get.py @@ -1,13 +1,14 @@ import sys sys.path.insert(0, ".") -import tester +import helper import timeit + def bench(mode): - im = tester.lena(mode) + im = helper.lena(mode) get = im.im.getpixel - xy = 50, 50 # position shouldn't really matter + xy = 50, 50 # position shouldn't really matter t0 = timeit.default_timer() for i in range(1000000): get(xy) diff --git a/Tests/check_icns_dos.py b/Tests/check_icns_dos.py new file mode 100644 index 000000000..ce6338a71 --- /dev/null +++ b/Tests/check_icns_dos.py @@ -0,0 +1,10 @@ +# Tests potential DOS of IcnsImagePlugin with 0 length block. +# Run from anywhere that PIL is importable. + +from PIL import Image +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'))) diff --git a/Tests/check_j2k_dos.py b/Tests/check_j2k_dos.py new file mode 100644 index 000000000..68f065bbc --- /dev/null +++ b/Tests/check_j2k_dos.py @@ -0,0 +1,11 @@ +# Tests potential DOS of Jpeg2kImagePlugin with 0 length block. +# 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'))) +else: + Image.open(BytesIO(bytes('\x00\x00\x00\x0cjP\x20\x20\x0d\x0a\x87\x0a\x00\x00\x00\x00hang', 'latin-1'))) + diff --git a/Tests/cms_test.py b/Tests/cms_test.py deleted file mode 100644 index 464c9c7ef..000000000 --- a/Tests/cms_test.py +++ /dev/null @@ -1,222 +0,0 @@ -# PyCMSTests.py -# Examples of how to use pyCMS, as well as tests to verify it works properly -# By Kevin Cazabon (kevin@cazabon.com) - -# Imports -import os -from PIL import Image -from PIL import ImageCms - -# import PyCMSError separately so we can catch it -PyCMSError = ImageCms.PyCMSError - -####################################################################### -# Configuration: -####################################################################### -# set this to the image you want to test with -IMAGE = "c:\\temp\\test.tif" - -# set this to where you want to save the output images -OUTPUTDIR = "c:\\temp\\" - -# set these to two different ICC profiles, one for input, one for output -# set the corresponding mode to the proper PIL mode for that profile -INPUT_PROFILE = "c:\\temp\\profiles\\sRGB.icm" -INMODE = "RGB" - -OUTPUT_PROFILE = "c:\\temp\\profiles\\genericRGB.icm" -OUTMODE = "RGB" - -PROOF_PROFILE = "c:\\temp\\profiles\\monitor.icm" - -# set to True to show() images, False to save them into OUTPUT_DIRECTORY -SHOW = False - -# Tests you can enable/disable -TEST_error_catching = True -TEST_profileToProfile = True -TEST_profileToProfile_inPlace = True -TEST_buildTransform = True -TEST_buildTransformFromOpenProfiles = True -TEST_buildProofTransform = True -TEST_getProfileInfo = True -TEST_misc = False - -####################################################################### -# helper functions -####################################################################### -def outputImage(im, funcName = None): - # save or display the image, depending on value of SHOW_IMAGES - if SHOW: - im.show() - else: - im.save(os.path.join(OUTPUTDIR, "%s.tif" %funcName)) - - -####################################################################### -# The tests themselves -####################################################################### - -if TEST_error_catching: - im = Image.open(IMAGE) - try: - #neither of these proifles exists (unless you make them), so we should - # get an error - imOut = ImageCms.profileToProfile(im, "missingProfile.icm", "cmyk.icm") - - except PyCMSError as reason: - print("We caught a PyCMSError: %s\n\n" %reason) - - print("error catching test completed successfully (if you see the message \ - above that we caught the error).") - -if TEST_profileToProfile: - # open the image file using the standard PIL function Image.open() - im = Image.open(IMAGE) - - # send the image, input/output profiles, and rendering intent to - # ImageCms.profileToProfile() - imOut = ImageCms.profileToProfile(im, INPUT_PROFILE, OUTPUT_PROFILE, \ - outputMode = OUTMODE) - - # now that the image is converted, save or display it - outputImage(imOut, "profileToProfile") - - print("profileToProfile test completed successfully.") - -if TEST_profileToProfile_inPlace: - # we'll do the same test as profileToProfile, but modify im in place - # instead of getting a new image returned to us - im = Image.open(IMAGE) - - # send the image to ImageCms.profileToProfile(), specifying inPlace = True - result = ImageCms.profileToProfile(im, INPUT_PROFILE, OUTPUT_PROFILE, \ - outputMode = OUTMODE, inPlace = True) - - # now that the image is converted, save or display it - if result is None: - # this is the normal result when modifying in-place - outputImage(im, "profileToProfile_inPlace") - else: - # something failed... - print("profileToProfile in-place failed: %s" %result) - - print("profileToProfile in-place test completed successfully.") - -if TEST_buildTransform: - # make a transform using the input and output profile path strings - transform = ImageCms.buildTransform(INPUT_PROFILE, OUTPUT_PROFILE, INMODE, \ - OUTMODE) - - # now, use the trnsform to convert a couple images - im = Image.open(IMAGE) - - # transform im normally - im2 = ImageCms.applyTransform(im, transform) - outputImage(im2, "buildTransform") - - # then transform it again using the same transform, this time in-place. - result = ImageCms.applyTransform(im, transform, inPlace = True) - outputImage(im, "buildTransform_inPlace") - - print("buildTransform test completed successfully.") - - # and, to clean up a bit, delete the transform - # this should call the C destructor for the transform structure. - # Python should also do this automatically when it goes out of scope. - del(transform) - -if TEST_buildTransformFromOpenProfiles: - # we'll actually test a couple profile open/creation functions here too - - # first, get a handle to an input profile, in this case we'll create - # an sRGB profile on the fly: - inputProfile = ImageCms.createProfile("sRGB") - - # then, get a handle to the output profile - outputProfile = ImageCms.getOpenProfile(OUTPUT_PROFILE) - - # make a transform from these - transform = ImageCms.buildTransformFromOpenProfiles(inputProfile, \ - outputProfile, INMODE, OUTMODE) - - # now, use the trnsform to convert a couple images - im = Image.open(IMAGE) - - # transform im normally - im2 = ImageCms.applyTransform(im, transform) - outputImage(im2, "buildTransformFromOpenProfiles") - - # then do it again using the same transform, this time in-place. - result = ImageCms.applyTransform(im, transform, inPlace = True) - outputImage(im, "buildTransformFromOpenProfiles_inPlace") - - print("buildTransformFromOpenProfiles test completed successfully.") - - # and, to clean up a bit, delete the transform - # this should call the C destructor for the each item. - # Python should also do this automatically when it goes out of scope. - del(inputProfile) - del(outputProfile) - del(transform) - -if TEST_buildProofTransform: - # make a transform using the input and output and proof profile path - # strings - # images converted with this transform will simulate the appearance - # of the output device while actually being displayed/proofed on the - # proof device. This usually means a monitor, but can also mean - # other proof-printers like dye-sub, etc. - transform = ImageCms.buildProofTransform(INPUT_PROFILE, OUTPUT_PROFILE, \ - PROOF_PROFILE, INMODE, OUTMODE) - - # now, use the trnsform to convert a couple images - im = Image.open(IMAGE) - - # transform im normally - im2 = ImageCms.applyTransform(im, transform) - outputImage(im2, "buildProofTransform") - - # then transform it again using the same transform, this time in-place. - result = ImageCms.applyTransform(im, transform, inPlace = True) - outputImage(im, "buildProofTransform_inPlace") - - print("buildProofTransform test completed successfully.") - - # and, to clean up a bit, delete the transform - # this should call the C destructor for the transform structure. - # Python should also do this automatically when it goes out of scope. - del(transform) - -if TEST_getProfileInfo: - # get a profile handle - profile = ImageCms.getOpenProfile(INPUT_PROFILE) - - # lets print some info about our input profile: - print("Profile name (retrieved from profile string path name): %s" %ImageCms.getProfileName(INPUT_PROFILE)) - - # or, you could do the same thing using a profile handle as the arg - print("Profile name (retrieved from profile handle): %s" %ImageCms.getProfileName(profile)) - - # now lets get the embedded "info" tag contents - # once again, you can use a path to a profile, or a profile handle - print("Profile info (retrieved from profile handle): %s" %ImageCms.getProfileInfo(profile)) - - # and what's the default intent of this profile? - print("The default intent is (this will be an integer): %d" %(ImageCms.getDefaultIntent(profile))) - - # Hmmmm... but does this profile support INTENT_ABSOLUTE_COLORIMETRIC? - print("Does it support INTENT_ABSOLUTE_COLORIMETRIC?: (1 is yes, -1 is no): %s" \ - %ImageCms.isIntentSupported(profile, ImageCms.INTENT_ABSOLUTE_COLORIMETRIC, \ - ImageCms.DIRECTION_INPUT)) - - print("getProfileInfo test completed successfully.") - -if TEST_misc: - # test the versions, about, and copyright functions - print("Versions: %s" %str(ImageCms.versions())) - print("About:\n\n%s" %ImageCms.about()) - print("Copyright:\n\n%s" %ImageCms.copyright()) - - print("misc test completed successfully.") - diff --git a/Tests/helper.py b/Tests/helper.py new file mode 100644 index 000000000..3f7913b11 --- /dev/null +++ b/Tests/helper.py @@ -0,0 +1,234 @@ +""" +Helper functions. +""" +from __future__ import print_function +import sys +import tempfile +import os + +if sys.version_info[:2] <= (2, 6): + import unittest2 as unittest +else: + import unittest + + +class PillowTestCase(unittest.TestCase): + + def __init__(self, *args, **kwargs): + unittest.TestCase.__init__(self, *args, **kwargs) + # holds last result object passed to run method: + self.currentResult = None + + # Nicer output for --verbose + def __str__(self): + return self.__class__.__name__ + "." + self._testMethodName + + def run(self, result=None): + self.currentResult = result # remember result for use later + unittest.TestCase.run(self, result) # call superclass run method + + def delete_tempfile(self, path): + try: + ok = self.currentResult.wasSuccessful() + except AttributeError: # for nosetests + proxy = self.currentResult + ok = (len(proxy.errors) + len(proxy.failures) == 0) + + if ok: + # only clean out tempfiles if test passed + try: + os.remove(path) + except OSError: + pass # report? + else: + print("=== orphaned temp file: %s" % path) + + def assert_almost_equal(self, a, b, msg=None, eps=1e-6): + self.assertLess( + abs(a-b), eps, + msg or "got %r, expected %r" % (a, b)) + + def assert_deep_equal(self, a, b, msg=None): + try: + self.assertEqual( + len(a), len(b), + msg or "got length %s, expected %s" % (len(a), len(b))) + self.assertTrue( + all([x == y for x, y in zip(a, b)]), + msg or "got %s, expected %s" % (a, b)) + except: + self.assertEqual(a, b, msg) + + def assert_image(self, im, mode, size, msg=None): + if mode is not None: + self.assertEqual( + im.mode, mode, + msg or "got mode %r, expected %r" % (im.mode, mode)) + + if size is not None: + self.assertEqual( + im.size, size, + msg or "got size %r, expected %r" % (im.size, size)) + + def assert_image_equal(self, a, b, msg=None): + self.assertEqual( + a.mode, b.mode, + msg or "got mode %r, expected %r" % (a.mode, b.mode)) + self.assertEqual( + a.size, b.size, + msg or "got size %r, expected %r" % (a.size, b.size)) + if a.tobytes() != b.tobytes(): + self.fail(msg or "got different content") + + def assert_image_similar(self, a, b, epsilon, msg=None): + epsilon = float(epsilon) + self.assertEqual( + a.mode, b.mode, + msg or "got mode %r, expected %r" % (a.mode, b.mode)) + self.assertEqual( + a.size, b.size, + msg or "got size %r, expected %r" % (a.size, b.size)) + + diff = 0 + try: + ord(b'0') + for abyte, bbyte in zip(a.tobytes(), b.tobytes()): + diff += abs(ord(abyte)-ord(bbyte)) + except: + for abyte, bbyte in zip(a.tobytes(), b.tobytes()): + diff += abs(abyte-bbyte) + ave_diff = float(diff)/(a.size[0]*a.size[1]) + self.assertGreaterEqual( + epsilon, ave_diff, + (msg or '') + " average pixel value difference %.4f > epsilon %.4f" % ( + ave_diff, epsilon)) + + def assert_warning(self, warn_class, func): + import warnings + + result = None + with warnings.catch_warnings(record=True) as w: + # Cause all warnings to always be triggered. + warnings.simplefilter("always") + + # Hopefully trigger a warning. + result = func() + + # Verify some things. + self.assertGreaterEqual(len(w), 1) + found = False + for v in w: + if issubclass(v.category, warn_class): + found = True + break + self.assertTrue(found) + return result + + def skipKnownBadTest(self, msg=None, platform=None, + travis=None, interpreter=None): + # 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)) + return + + skip = True + if platform is not None: + skip = sys.platform.startswith(platform) + 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')) + if skip: + self.skipTest(msg or "Known Bad Test") + + def tempfile(self, template): + assert template[:5] in ("temp.", "temp_") + (fd, path) = tempfile.mkstemp(template[4:], template[:4]) + os.close(fd) + + self.addCleanup(self.delete_tempfile, path) + return path + + def open_withImagemagick(self, f): + if not imagemagick_available(): + raise IOError() + + outfile = self.tempfile("temp.png") + if command_succeeds(['convert', f, outfile]): + from PIL import Image + return Image.open(outfile) + raise IOError() + + +# helpers + +import sys +py3 = (sys.version_info >= (3, 0)) + + +def fromstring(data): + from io import BytesIO + from PIL import Image + return Image.open(BytesIO(data)) + + +def tostring(im, format, **options): + from io import BytesIO + out = BytesIO() + im.save(out, format, **options) + return out.getvalue() + + +def lena(mode="RGB", 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 + # (for fast, isolated, repeatable tests). + # 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) + elif mode[:4] == "I;16": + im = lena("I").convert(mode) + else: + im = lena("RGB").convert(mode) + # cache[mode] = im + return im + + +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: + subprocess.Popen(cmd, stdout=f, stderr=subprocess.STDOUT).wait() + except OSError: + return False + return True + + +def djpeg_available(): + return command_succeeds(['djpeg', '--help']) + + +def cjpeg_available(): + return command_succeeds(['cjpeg', '--help']) + + +def netpbm_available(): + return (command_succeeds(["ppmquant", "--help"]) and + command_succeeds(["ppmtogif", "--help"])) + + +def imagemagick_available(): + return command_succeeds(['convert', '-version']) + +# End of file diff --git a/Tests/images/16bit.cropped.j2k b/Tests/images/16bit.cropped.j2k new file mode 100644 index 000000000..c12df0cd7 Binary files /dev/null and b/Tests/images/16bit.cropped.j2k differ diff --git a/Tests/images/16bit.cropped.jp2 b/Tests/images/16bit.cropped.jp2 new file mode 100644 index 000000000..0f6685a46 Binary files /dev/null and b/Tests/images/16bit.cropped.jp2 differ diff --git a/Tests/images/corner.lut b/Tests/images/corner.lut new file mode 100644 index 000000000..7b0386be3 Binary files /dev/null and b/Tests/images/corner.lut differ diff --git a/Images/courB08.bdf b/Tests/images/courB08.bdf similarity index 100% rename from Images/courB08.bdf rename to Tests/images/courB08.bdf diff --git a/Images/courB08.pbm b/Tests/images/courB08.pbm similarity index 100% rename from Images/courB08.pbm rename to Tests/images/courB08.pbm diff --git a/Images/courB08.pil b/Tests/images/courB08.pil similarity index 100% rename from Images/courB08.pil rename to Tests/images/courB08.pil diff --git a/Tests/images/deerstalker.cur b/Tests/images/deerstalker.cur new file mode 100644 index 000000000..16e4bfa52 Binary files /dev/null and b/Tests/images/deerstalker.cur differ diff --git a/Tests/images/default_font.png b/Tests/images/default_font.png new file mode 100644 index 000000000..bb4862b99 Binary files /dev/null and b/Tests/images/default_font.png differ diff --git a/Tests/images/dilation4.lut b/Tests/images/dilation4.lut new file mode 100644 index 000000000..958b801ab Binary files /dev/null and b/Tests/images/dilation4.lut differ diff --git a/Tests/images/dilation8.lut b/Tests/images/dilation8.lut new file mode 100644 index 000000000..c3bca8684 Binary files /dev/null and b/Tests/images/dilation8.lut differ diff --git a/Tests/images/dispose_bgnd.gif b/Tests/images/dispose_bgnd.gif new file mode 100644 index 000000000..d68504010 Binary files /dev/null and b/Tests/images/dispose_bgnd.gif differ diff --git a/Tests/images/dispose_none.gif b/Tests/images/dispose_none.gif new file mode 100644 index 000000000..beeb13fd9 Binary files /dev/null and b/Tests/images/dispose_none.gif differ diff --git a/Tests/images/dispose_prev.gif b/Tests/images/dispose_prev.gif new file mode 100644 index 000000000..0de7760e9 Binary files /dev/null and b/Tests/images/dispose_prev.gif differ diff --git a/Tests/images/edge.lut b/Tests/images/edge.lut new file mode 100644 index 000000000..f7aabf41c Binary files /dev/null and b/Tests/images/edge.lut differ diff --git a/Tests/images/erosion4.lut b/Tests/images/erosion4.lut new file mode 100644 index 000000000..9b6d2933e Binary files /dev/null and b/Tests/images/erosion4.lut differ diff --git a/Tests/images/erosion8.lut b/Tests/images/erosion8.lut new file mode 100644 index 000000000..7ff2c4a99 Binary files /dev/null and b/Tests/images/erosion8.lut differ diff --git a/Images/flower.webp b/Tests/images/flower.webp similarity index 100% rename from Images/flower.webp rename to Tests/images/flower.webp diff --git a/Images/flower2.webp b/Tests/images/flower2.webp similarity index 100% rename from Images/flower2.webp rename to Tests/images/flower2.webp diff --git a/Tests/images/frozenpond.mpo b/Tests/images/frozenpond.mpo new file mode 100644 index 000000000..2dfe44ac1 Binary files /dev/null and b/Tests/images/frozenpond.mpo differ diff --git a/Tests/images/imagedraw/line_horizontal_slope1px_w2px.png b/Tests/images/imagedraw/line_horizontal_slope1px_w2px.png new file mode 100644 index 000000000..aaed76786 Binary files /dev/null and b/Tests/images/imagedraw/line_horizontal_slope1px_w2px.png differ diff --git a/Tests/images/imagedraw/line_horizontal_w101px.png b/Tests/images/imagedraw/line_horizontal_w101px.png new file mode 100644 index 000000000..bbcc8133f Binary files /dev/null and b/Tests/images/imagedraw/line_horizontal_w101px.png differ diff --git a/Tests/images/imagedraw/line_horizontal_w2px_inverted.png b/Tests/images/imagedraw/line_horizontal_w2px_inverted.png new file mode 100644 index 000000000..a2bb3bbab Binary files /dev/null and b/Tests/images/imagedraw/line_horizontal_w2px_inverted.png differ diff --git a/Tests/images/imagedraw/line_horizontal_w2px_normal.png b/Tests/images/imagedraw/line_horizontal_w2px_normal.png new file mode 100644 index 000000000..a6d409ea4 Binary files /dev/null and b/Tests/images/imagedraw/line_horizontal_w2px_normal.png differ diff --git a/Tests/images/imagedraw/line_horizontal_w3px.png b/Tests/images/imagedraw/line_horizontal_w3px.png new file mode 100644 index 000000000..06334d666 Binary files /dev/null and b/Tests/images/imagedraw/line_horizontal_w3px.png differ diff --git a/Tests/images/imagedraw/line_oblique_45_w3px_a.png b/Tests/images/imagedraw/line_oblique_45_w3px_a.png new file mode 100644 index 000000000..55c535e29 Binary files /dev/null and b/Tests/images/imagedraw/line_oblique_45_w3px_a.png differ diff --git a/Tests/images/imagedraw/line_oblique_45_w3px_b.png b/Tests/images/imagedraw/line_oblique_45_w3px_b.png new file mode 100644 index 000000000..8c1036559 Binary files /dev/null and b/Tests/images/imagedraw/line_oblique_45_w3px_b.png differ diff --git a/Tests/images/imagedraw/line_vertical_slope1px_w2px.png b/Tests/images/imagedraw/line_vertical_slope1px_w2px.png new file mode 100644 index 000000000..1d48a57e8 Binary files /dev/null and b/Tests/images/imagedraw/line_vertical_slope1px_w2px.png differ diff --git a/Tests/images/imagedraw/line_vertical_w101px.png b/Tests/images/imagedraw/line_vertical_w101px.png new file mode 100644 index 000000000..558450950 Binary files /dev/null and b/Tests/images/imagedraw/line_vertical_w101px.png differ diff --git a/Tests/images/imagedraw/line_vertical_w2px_inverted.png b/Tests/images/imagedraw/line_vertical_w2px_inverted.png new file mode 100644 index 000000000..74d666b88 Binary files /dev/null and b/Tests/images/imagedraw/line_vertical_w2px_inverted.png differ diff --git a/Tests/images/imagedraw/line_vertical_w2px_normal.png b/Tests/images/imagedraw/line_vertical_w2px_normal.png new file mode 100644 index 000000000..5b18a7c94 Binary files /dev/null and b/Tests/images/imagedraw/line_vertical_w2px_normal.png differ diff --git a/Tests/images/imagedraw/line_vertical_w3px.png b/Tests/images/imagedraw/line_vertical_w3px.png new file mode 100644 index 000000000..4e5234f2a Binary files /dev/null and b/Tests/images/imagedraw/line_vertical_w3px.png differ diff --git a/Tests/images/imagedraw/square.png b/Tests/images/imagedraw/square.png new file mode 100644 index 000000000..842ee4722 Binary files /dev/null and b/Tests/images/imagedraw/square.png differ diff --git a/Tests/images/imagedraw/triangle_right.png b/Tests/images/imagedraw/triangle_right.png new file mode 100644 index 000000000..e91fa5802 Binary files /dev/null and b/Tests/images/imagedraw/triangle_right.png differ diff --git a/Tests/images/imagedraw_arc.png b/Tests/images/imagedraw_arc.png new file mode 100644 index 000000000..b09774389 Binary files /dev/null and b/Tests/images/imagedraw_arc.png differ diff --git a/Tests/images/imagedraw_bitmap.png b/Tests/images/imagedraw_bitmap.png new file mode 100644 index 000000000..05337b693 Binary files /dev/null and b/Tests/images/imagedraw_bitmap.png differ diff --git a/Tests/images/imagedraw_chord.png b/Tests/images/imagedraw_chord.png new file mode 100644 index 000000000..db3b35310 Binary files /dev/null and b/Tests/images/imagedraw_chord.png differ diff --git a/Tests/images/imagedraw_ellipse.png b/Tests/images/imagedraw_ellipse.png new file mode 100644 index 000000000..b52b12802 Binary files /dev/null and b/Tests/images/imagedraw_ellipse.png differ diff --git a/Tests/images/imagedraw_floodfill.png b/Tests/images/imagedraw_floodfill.png new file mode 100644 index 000000000..89376a0f0 Binary files /dev/null and b/Tests/images/imagedraw_floodfill.png differ diff --git a/Tests/images/imagedraw_floodfill2.png b/Tests/images/imagedraw_floodfill2.png new file mode 100644 index 000000000..41b92fb75 Binary files /dev/null and b/Tests/images/imagedraw_floodfill2.png differ diff --git a/Tests/images/imagedraw_line.png b/Tests/images/imagedraw_line.png new file mode 100644 index 000000000..f7303a832 Binary files /dev/null and b/Tests/images/imagedraw_line.png differ diff --git a/Tests/images/imagedraw_pieslice.png b/Tests/images/imagedraw_pieslice.png new file mode 100644 index 000000000..2f8c09191 Binary files /dev/null and b/Tests/images/imagedraw_pieslice.png differ diff --git a/Tests/images/imagedraw_point.png b/Tests/images/imagedraw_point.png new file mode 100644 index 000000000..ab7c1a322 Binary files /dev/null and b/Tests/images/imagedraw_point.png differ diff --git a/Tests/images/imagedraw_polygon.png b/Tests/images/imagedraw_polygon.png new file mode 100644 index 000000000..7199a25dd Binary files /dev/null and b/Tests/images/imagedraw_polygon.png differ diff --git a/Tests/images/imagedraw_rectangle.png b/Tests/images/imagedraw_rectangle.png new file mode 100644 index 000000000..782d84461 Binary files /dev/null and b/Tests/images/imagedraw_rectangle.png differ diff --git a/Tests/images/iptc.jpg b/Tests/images/iptc.jpg new file mode 100644 index 000000000..b4d49caa1 Binary files /dev/null and b/Tests/images/iptc.jpg differ diff --git a/Tests/images/iss634.gif b/Tests/images/iss634.gif new file mode 100644 index 000000000..ba4e4566f Binary files /dev/null and b/Tests/images/iss634.gif differ diff --git a/Tests/images/lena.bw b/Tests/images/lena.bw new file mode 100644 index 000000000..e2981ef5c Binary files /dev/null and b/Tests/images/lena.bw differ diff --git a/Tests/images/lena.dcx b/Tests/images/lena.dcx new file mode 100644 index 000000000..d05370be8 Binary files /dev/null and b/Tests/images/lena.dcx differ diff --git a/Images/lena.fli b/Tests/images/lena.fli similarity index 100% rename from Images/lena.fli rename to Tests/images/lena.fli diff --git a/Images/lena.gif b/Tests/images/lena.gif similarity index 100% rename from Images/lena.gif rename to Tests/images/lena.gif diff --git a/Images/lena.ico b/Tests/images/lena.ico similarity index 100% rename from Images/lena.ico rename to Tests/images/lena.ico diff --git a/Images/lena.jpg b/Tests/images/lena.jpg similarity index 100% rename from Images/lena.jpg rename to Tests/images/lena.jpg diff --git a/Images/lena.png b/Tests/images/lena.png similarity index 100% rename from Images/lena.png rename to Tests/images/lena.png diff --git a/Images/lena.ppm b/Tests/images/lena.ppm similarity index 100% rename from Images/lena.ppm rename to Tests/images/lena.ppm diff --git a/Images/lena.psd b/Tests/images/lena.psd similarity index 100% rename from Images/lena.psd rename to Tests/images/lena.psd diff --git a/Tests/images/lena.ras b/Tests/images/lena.ras new file mode 100644 index 000000000..b5893e758 Binary files /dev/null and b/Tests/images/lena.ras differ diff --git a/Tests/images/lena.rgb b/Tests/images/lena.rgb new file mode 100644 index 000000000..82552c758 Binary files /dev/null and b/Tests/images/lena.rgb differ diff --git a/Images/lena.tar b/Tests/images/lena.tar similarity index 100% rename from Images/lena.tar rename to Tests/images/lena.tar diff --git a/Images/lena.webp b/Tests/images/lena.webp similarity index 100% rename from Images/lena.webp rename to Tests/images/lena.webp diff --git a/Images/lena.xpm b/Tests/images/lena.xpm similarity index 100% rename from Images/lena.xpm rename to Tests/images/lena.xpm diff --git a/Tests/images/morph_a.png b/Tests/images/morph_a.png new file mode 100644 index 000000000..19f6b777f Binary files /dev/null and b/Tests/images/morph_a.png differ diff --git a/Images/pillow.icns b/Tests/images/pillow.icns similarity index 100% rename from Images/pillow.icns rename to Tests/images/pillow.icns diff --git a/Images/pillow.ico b/Tests/images/pillow.ico similarity index 100% rename from Images/pillow.ico rename to Tests/images/pillow.ico diff --git a/Tests/images/rectangle_surrounding_text.png b/Tests/images/rectangle_surrounding_text.png new file mode 100644 index 000000000..2b75a5e9c Binary files /dev/null and b/Tests/images/rectangle_surrounding_text.png differ diff --git a/Tests/images/rgb_trns_ycbc.j2k b/Tests/images/rgb_trns_ycbc.j2k new file mode 100644 index 000000000..462729501 Binary files /dev/null and b/Tests/images/rgb_trns_ycbc.j2k differ diff --git a/Tests/images/rgb_trns_ycbc.jp2 b/Tests/images/rgb_trns_ycbc.jp2 new file mode 100644 index 000000000..dea77c6da Binary files /dev/null and b/Tests/images/rgb_trns_ycbc.jp2 differ diff --git a/Tests/images/sugarshack.mpo b/Tests/images/sugarshack.mpo new file mode 100644 index 000000000..85346fcc9 Binary files /dev/null and b/Tests/images/sugarshack.mpo differ diff --git a/Images/transparent.png b/Tests/images/transparent.png similarity index 100% rename from Images/transparent.png rename to Tests/images/transparent.png diff --git a/Tests/images/transparent.sgi b/Tests/images/transparent.sgi new file mode 100644 index 000000000..482572df5 Binary files /dev/null and b/Tests/images/transparent.sgi differ diff --git a/Images/transparent.webp b/Tests/images/transparent.webp similarity index 100% rename from Images/transparent.webp rename to Tests/images/transparent.webp diff --git a/Tests/large_memory_numpy_test.py b/Tests/large_memory_numpy_test.py index eb9b8aa01..8a13d0aea 100644 --- a/Tests/large_memory_numpy_test.py +++ b/Tests/large_memory_numpy_test.py @@ -1,4 +1,6 @@ -from tester import * +import sys + +from helper import * # This test is not run automatically. # @@ -6,32 +8,38 @@ from tester import * # second test. Running this automatically would amount to a denial of # service on our testing infrastructure. I expect this test to fail # on any 32 bit machine, as well as any smallish things (like -# raspberrypis). +# Raspberry Pis). from PIL import Image try: import numpy as np except: - skip() - -ydim = 32769 -xdim = 48000 -f = tempfile('temp.png') + sys.exit("Skipping: Numpy not installed") -def _write_png(xdim,ydim): - dtype = np.uint8 - a = np.zeros((xdim, ydim), dtype=dtype) - im = Image.fromarray(a, 'L') - im.save(f) - success() - -def test_large(): - """ succeeded prepatch""" - _write_png(xdim,ydim) -def test_2gpx(): - """failed prepatch""" - _write_png(xdim,xdim) +YDIM = 32769 +XDIM = 48000 +@unittest.skipIf(sys.maxsize <= 2**32, "requires 64 bit system") +class LargeMemoryNumpyTest(PillowTestCase): + + def _write_png(self, xdim, ydim): + dtype = np.uint8 + a = np.zeros((xdim, ydim), dtype=dtype) + f = self.tempfile('temp.png') + im = Image.fromarray(a, 'L') + im.save(f) + + def test_large(self): + """ succeeded prepatch""" + self._write_png(XDIM, YDIM) + + def test_2gpx(self): + """failed prepatch""" + self._write_png(XDIM, XDIM) +if __name__ == '__main__': + unittest.main() + +# End of file diff --git a/Tests/large_memory_test.py b/Tests/large_memory_test.py index 148841ec2..a63a42cd5 100644 --- a/Tests/large_memory_test.py +++ b/Tests/large_memory_test.py @@ -1,4 +1,6 @@ -from tester import * +import sys + +from helper import * # This test is not run automatically. # @@ -6,22 +8,32 @@ from tester import * # second test. Running this automatically would amount to a denial of # service on our testing infrastructure. I expect this test to fail # on any 32 bit machine, as well as any smallish things (like -# raspberrypis). It does succeed on a 3gb Ubuntu 12.04x64 VM on python -# 2.7 an 3.2 +# Raspberry Pis). It does succeed on a 3gb Ubuntu 12.04x64 VM on Python +# 2.7 an 3.2. from PIL import Image -ydim = 32769 -xdim = 48000 -f = tempfile('temp.png') +YDIM = 32769 +XDIM = 48000 -def _write_png(xdim,ydim): - im = Image.new('L',(xdim,ydim),(0)) - im.save(f) - success() -def test_large(): - """ succeeded prepatch""" - _write_png(xdim,ydim) -def test_2gpx(): - """failed prepatch""" - _write_png(xdim,xdim) +@unittest.skipIf(sys.maxsize <= 2**32, "requires 64 bit system") +class LargeMemoryTest(PillowTestCase): + + def _write_png(self, xdim, ydim): + f = self.tempfile('temp.png') + im = Image.new('L', (xdim, ydim), (0)) + im.save(f) + + def test_large(self): + """ succeeded prepatch""" + self._write_png(XDIM, YDIM) + + def test_2gpx(self): + """failed prepatch""" + self._write_png(XDIM, XDIM) + + +if __name__ == '__main__': + unittest.main() + +# End of file diff --git a/Tests/make_hash.py b/Tests/make_hash.py index 71e208cff..88bb2994b 100644 --- a/Tests/make_hash.py +++ b/Tests/make_hash.py @@ -1,7 +1,5 @@ # brute-force search for access descriptor hash table -import random - modes = [ "1", "L", "LA", @@ -11,14 +9,17 @@ modes = [ "RGB", "RGBA", "RGBa", "RGBX", "CMYK", "YCbCr", + "LAB", "HSV", ] + def hash(s, i): # djb2 hash: multiply by 33 and xor character for c in s: - i = (((i<<5) + i) ^ ord(c)) & 0xffffffff + i = (((i << 5) + i) ^ ord(c)) & 0xffffffff return i + def check(size, i0): h = [None] * size for m in modes: diff --git a/Tests/run.py b/Tests/run.py deleted file mode 100644 index 758923f0f..000000000 --- a/Tests/run.py +++ /dev/null @@ -1,135 +0,0 @@ -from __future__ import print_function - -# minimal test runner - -import glob -import os -import os.path -import re -import sys -import tempfile - -try: - root = os.path.dirname(__file__) -except NameError: - root = os.path.dirname(sys.argv[0]) - -if not os.path.isfile("PIL/Image.py"): - print("***", "please run this script from the PIL development directory as") - print("***", "$ python Tests/run.py") - sys.exit(1) - -print("-"*68) - -python_options = [] -tester_options = [] - -if "--installed" not in sys.argv: - os.environ["PYTHONPATH"] = "." - -if "--coverage" in sys.argv: - tester_options.append("--coverage") - -if "--log" in sys.argv: - tester_options.append("--log") - -files = glob.glob(os.path.join(root, "test_*.py")) -files.sort() - -success = failure = 0 -include = [x for x in sys.argv[1:] if x[:2] != "--"] -skipped = [] -failed = [] - -python_options = " ".join(python_options) -tester_options = " ".join(tester_options) - -ignore_re = re.compile('^ignore: (.*)$', re.MULTILINE) - -for file in files: - test, ext = os.path.splitext(os.path.basename(file)) - if include and test not in include: - continue - print("running", test, "...") - # 2>&1 works on unix and on modern windowses. we might care about - # very old Python versions, but not ancient microsoft products :-) - out = os.popen("%s %s -u %s %s 2>&1" % ( - sys.executable, python_options, file, tester_options - )) - result = out.read() - - result_lines = result.splitlines() - if len(result_lines): - if result_lines[0] == "ignore_all_except_last_line": - result = result_lines[-1] - - # Extract any ignore patterns - ignore_pats = ignore_re.findall(result) - result = ignore_re.sub('', result) - - try: - def fix_re(p): - if not p.startswith('^'): - p = '^' + p - if not p.endswith('$'): - p = p + '$' - return p - - ignore_res = [re.compile(fix_re(p), re.MULTILINE) for p in ignore_pats] - except: - print('(bad ignore patterns %r)' % ignore_pats) - ignore_res = [] - - for r in ignore_res: - result = r.sub('', result) - - result = result.strip() - - if result == "ok": - result = None - elif result == "skip": - print("---", "skipped") # FIXME: driver should include a reason - skipped.append(test) - continue - elif not result: - result = "(no output)" - status = out.close() - if status or result: - if status: - print("=== error", status) - if result: - if result[-3:] == "\nok": - # if there's an ok at the end, it's not really ok - result = result[:-3] - print(result) - failed.append(test) - else: - success = success + 1 - -print("-"*68) - -temp_root = os.path.join(tempfile.gettempdir(), 'pillow-tests') -tempfiles = glob.glob(os.path.join(temp_root, "temp_*")) -if tempfiles: - print("===", "remaining temporary files") - for file in tempfiles: - print(file) - print("-"*68) - - -def tests(n): - if n == 1: - return "1 test" - else: - return "%d tests" % n - -if skipped: - print("---", tests(len(skipped)), "skipped:") - print(", ".join(skipped)) -if failed: - failure = len(failed) - print("***", tests(failure), "of", (success + failure), "failed:") - print(", ".join(failed)) - sys.exit(1) -else: - print(tests(success), "passed.") diff --git a/Tests/test_000_sanity.py b/Tests/test_000_sanity.py index a30786458..22e582ec3 100644 --- a/Tests/test_000_sanity.py +++ b/Tests/test_000_sanity.py @@ -1,24 +1,32 @@ -from __future__ import print_function -from tester import * +from helper import unittest, PillowTestCase import PIL import PIL.Image -# Make sure we have the binary extension -im = PIL.Image.core.new("L", (100, 100)) -assert PIL.Image.VERSION[:3] == '1.1' +class TestSanity(PillowTestCase): -# Create an image and do stuff with it. -im = PIL.Image.new("1", (100, 100)) -assert (im.mode, im.size) == ('1', (100, 100)) -assert len(im.tobytes()) == 1300 + def test_sanity(self): -# Create images in all remaining major modes. -im = PIL.Image.new("L", (100, 100)) -im = PIL.Image.new("P", (100, 100)) -im = PIL.Image.new("RGB", (100, 100)) -im = PIL.Image.new("I", (100, 100)) -im = PIL.Image.new("F", (100, 100)) + # Make sure we have the binary extension + im = PIL.Image.core.new("L", (100, 100)) -print("ok") + self.assertEqual(PIL.Image.VERSION[:3], '1.1') + + # Create an image and do stuff with it. + im = PIL.Image.new("1", (100, 100)) + self.assertEqual((im.mode, im.size), ('1', (100, 100))) + self.assertEqual(len(im.tobytes()), 1300) + + # Create images in all remaining major modes. + im = PIL.Image.new("L", (100, 100)) + im = PIL.Image.new("P", (100, 100)) + im = PIL.Image.new("RGB", (100, 100)) + im = PIL.Image.new("I", (100, 100)) + im = PIL.Image.new("F", (100, 100)) + + +if __name__ == '__main__': + unittest.main() + +# End of file diff --git a/Tests/test_001_archive.py b/Tests/test_001_archive.py deleted file mode 100644 index a914a6c4c..000000000 --- a/Tests/test_001_archive.py +++ /dev/null @@ -1,23 +0,0 @@ -import PIL -import PIL.Image - -import glob, os - -for file in glob.glob("../pil-archive/*"): - f, e = os.path.splitext(file) - if e in [".txt", ".ttf", ".otf", ".zip"]: - continue - try: - im = PIL.Image.open(file) - im.load() - except IOError as v: - print("-", "failed to open", file, "-", v) - else: - print("+", file, im.mode, im.size, im.format) - if e == ".exif": - try: - info = im._getexif() - except KeyError as v: - print("-", "failed to get exif info from", file, "-", v) - -print("ok") diff --git a/Tests/test_bmp_reference.py b/Tests/test_bmp_reference.py index 99818229f..b45ea76f6 100644 --- a/Tests/test_bmp_reference.py +++ b/Tests/test_bmp_reference.py @@ -1,4 +1,4 @@ -from tester import * +from helper import unittest, PillowTestCase from PIL import Image import os @@ -6,81 +6,89 @@ import os base = os.path.join('Tests', 'images', 'bmp') -def get_files(d, ext='.bmp'): - return [os.path.join(base,d,f) for f - in os.listdir(os.path.join(base, d)) if ext in f] +class TestBmpReference(PillowTestCase): -def test_bad(): - """ These shouldn't crash/dos, but they shouldn't return anything either """ - for f in get_files('b'): - try: - im = Image.open(f) - im.load() - except Exception as msg: - pass - # print ("Bad Image %s: %s" %(f,msg)) + def get_files(self, d, ext='.bmp'): + return [os.path.join(base, d, f) for f + in os.listdir(os.path.join(base, d)) if ext in f] -def test_questionable(): - """ These shouldn't crash/dos, but its not well defined that these are in spec """ - for f in get_files('q'): - try: - im = Image.open(f) - im.load() - except Exception as msg: - pass - # print ("Bad Image %s: %s" %(f,msg)) + def test_bad(self): + """ These shouldn't crash/dos, but they shouldn't return anything + either """ + for f in self.get_files('b'): + try: + im = Image.open(f) + im.load() + except Exception: # as msg: + pass + # print ("Bad Image %s: %s" %(f,msg)) + + def test_questionable(self): + """ These shouldn't crash/dos, but its not well defined that these + are in spec """ + for f in self.get_files('q'): + try: + im = Image.open(f) + im.load() + except Exception: # as msg: + pass + # print ("Bad Image %s: %s" %(f,msg)) + + def test_good(self): + """ These should all work. There's a set of target files in the + html directory that we can compare against. """ + + # Target files, if they're not just replacing the extension + file_map = {'pal1wb.bmp': 'pal1.png', + 'pal4rle.bmp': 'pal4.png', + 'pal8-0.bmp': 'pal8.png', + 'pal8rle.bmp': 'pal8.png', + 'pal8topdown.bmp': 'pal8.png', + 'pal8nonsquare.bmp': 'pal8nonsquare-v.png', + 'pal8os2.bmp': 'pal8.png', + 'pal8os2sp.bmp': 'pal8.png', + 'pal8os2v2.bmp': 'pal8.png', + 'pal8os2v2-16.bmp': 'pal8.png', + 'pal8v4.bmp': 'pal8.png', + 'pal8v5.bmp': 'pal8.png', + 'rgb16-565pal.bmp': 'rgb16-565.png', + 'rgb24pal.bmp': 'rgb24.png', + 'rgb32.bmp': 'rgb24.png', + 'rgb32bf.bmp': 'rgb24.png' + } + + def get_compare(f): + (head, name) = os.path.split(f) + if name in file_map: + return os.path.join(base, 'html', file_map[name]) + (name, ext) = os.path.splitext(name) + return os.path.join(base, 'html', "%s.png" % name) + + for f in self.get_files('g'): + try: + im = Image.open(f) + im.load() + compare = Image.open(get_compare(f)) + compare.load() + if im.mode == 'P': + # assert image similar doesn't really work + # with paletized image, since the palette might + # be differently ordered for an equivalent image. + im = im.convert('RGBA') + compare = im.convert('RGBA') + self.assert_image_similar(im, compare, 5) + + except Exception as msg: + # there are three here that are unsupported: + unsupported = (os.path.join(base, 'g', 'rgb32bf.bmp'), + os.path.join(base, 'g', 'pal8rle.bmp'), + os.path.join(base, 'g', 'pal4rle.bmp')) + if f not in unsupported: + self.assertTrue( + False, "Unsupported Image %s: %s" % (f, msg)) -def test_good(): - """ These should all work. There's a set of target files in the - html directory that we can compare against. """ - - # Target files, if they're not just replacing the extension - file_map = { 'pal1wb.bmp': 'pal1.png', - 'pal4rle.bmp': 'pal4.png', - 'pal8-0.bmp': 'pal8.png', - 'pal8rle.bmp': 'pal8.png', - 'pal8topdown.bmp': 'pal8.png', - 'pal8nonsquare.bmp': 'pal8nonsquare-v.png', - 'pal8os2.bmp': 'pal8.png', - 'pal8os2sp.bmp': 'pal8.png', - 'pal8os2v2.bmp': 'pal8.png', - 'pal8os2v2-16.bmp': 'pal8.png', - 'pal8v4.bmp': 'pal8.png', - 'pal8v5.bmp': 'pal8.png', - 'rgb16-565pal.bmp': 'rgb16-565.png', - 'rgb24pal.bmp': 'rgb24.png', - 'rgb32.bmp': 'rgb24.png', - 'rgb32bf.bmp': 'rgb24.png' - } - - def get_compare(f): - (head, name) = os.path.split(f) - if name in file_map: - return os.path.join(base, 'html', file_map[name]) - (name,ext) = os.path.splitext(name) - return os.path.join(base, 'html', "%s.png"%name) - - for f in get_files('g'): - try: - im = Image.open(f) - im.load() - compare = Image.open(get_compare(f)) - compare.load() - if im.mode == 'P': - # assert image similar doesn't really work - # with paletized image, since the palette might - # be differently ordered for an equivalent image. - im = im.convert('RGBA') - compare = im.convert('RGBA') - assert_image_similar(im, compare,5) - - - except Exception as msg: - # there are three here that are unsupported: - unsupported = (os.path.join(base, 'g', 'rgb32bf.bmp'), - os.path.join(base, 'g', 'pal8rle.bmp'), - os.path.join(base, 'g', 'pal4rle.bmp')) - if f not in unsupported: - assert_true(False, "Unsupported Image %s: %s" %(f,msg)) +if __name__ == '__main__': + unittest.main() +# End of file diff --git a/Tests/test_cffi.py b/Tests/test_cffi.py index 1c0d8d31e..b9f99976d 100644 --- a/Tests/test_cffi.py +++ b/Tests/test_cffi.py @@ -1,99 +1,136 @@ -from tester import * +from helper import unittest, PillowTestCase, lena try: import cffi + from PIL import PyAccess except: - skip() - -from PIL import Image, PyAccess + # Skip in setUp() + pass -import test_image_putpixel as put -import test_image_getpixel as get +from PIL import Image +from test_image_putpixel import TestImagePutPixel +from test_image_getpixel import TestImageGetPixel Image.USE_CFFI_ACCESS = True -def test_put(): - put.test_sanity() -def test_get(): - get.test_basic() - get.test_signedness() +class TestCffiPutPixel(TestImagePutPixel): -def _test_get_access(im): - """ Do we get the same thing as the old pixel access """ + def setUp(self): + try: + import cffi + except: + self.skipTest("No cffi") - """ 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) - - w,h = im.size - for x in range(0,w,10): - for y in range(0,h,10): - assert_equal(access[(x,y)], caccess[(x,y)]) - -def test_get_vs_c(): - _test_get_access(lena('RGB')) - _test_get_access(lena('RGBA')) - _test_get_access(lena('L')) - _test_get_access(lena('LA')) - _test_get_access(lena('1')) - _test_get_access(lena('P')) - #_test_get_access(lena('PA')) # PA -- how do I make a PA image??? - _test_get_access(lena('F')) - - im = Image.new('I;16', (10,10), 40000) - _test_get_access(im) - im = Image.new('I;16L', (10,10), 40000) - _test_get_access(im) - im = Image.new('I;16B', (10,10), 40000) - _test_get_access(im) - - im = Image.new('I', (10,10), 40000) - _test_get_access(im) - # These don't actually appear to be modes that I can actually make, - # as unpack sets them directly into the I mode. - #im = Image.new('I;32L', (10,10), -2**10) - #_test_get_access(im) - #im = Image.new('I;32B', (10,10), 2**10) - #_test_get_access(im) + def test_put(self): + self.test_sanity() +class TestCffiGetPixel(TestImageGetPixel): -def _test_set_access(im, color): - """ Are we writing the correct bits into the image? """ + def setUp(self): + try: + import cffi + except: + self.skipTest("No cffi") - """ 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) + def test_get(self): + self.test_basic() + self.test_signedness() - w,h = im.size - for x in range(0,w,10): - for y in range(0,h,10): - access[(x,y)] = color - assert_equal(color, caccess[(x,y)]) -def test_set_vs_c(): - _test_set_access(lena('RGB'), (255, 128,0) ) - _test_set_access(lena('RGBA'), (255, 192, 128, 0)) - _test_set_access(lena('L'), 128) - _test_set_access(lena('LA'), (128,128)) - _test_set_access(lena('1'), 255) - _test_set_access(lena('P') , 128) - ##_test_set_access(i, (128,128)) #PA -- undone how to make - _test_set_access(lena('F'), 1024.0) - - im = Image.new('I;16', (10,10), 40000) - _test_set_access(im, 45000) - im = Image.new('I;16L', (10,10), 40000) - _test_set_access(im, 45000) - im = Image.new('I;16B', (10,10), 40000) - _test_set_access(im, 45000) - +class TestCffi(PillowTestCase): - im = Image.new('I', (10,10), 40000) - _test_set_access(im, 45000) -# im = Image.new('I;32L', (10,10), -(2**10)) -# _test_set_access(im, -(2**13)+1) - #im = Image.new('I;32B', (10,10), 2**10) - #_test_set_access(im, 2**13-1) + def setUp(self): + try: + import cffi + except: + self.skipTest("No cffi") + + def _test_get_access(self, im): + """ 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 """ + caccess = im.im.pixel_access(False) + access = PyAccess.new(im, False) + + w, h = im.size + for x in range(0, w, 10): + for y in range(0, h, 10): + self.assertEqual(access[(x, y)], caccess[(x, y)]) + + def test_get_vs_c(self): + rgb = lena('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')) + + im = Image.new('I;16', (10, 10), 40000) + self._test_get_access(im) + im = Image.new('I;16L', (10, 10), 40000) + self._test_get_access(im) + im = Image.new('I;16B', (10, 10), 40000) + self._test_get_access(im) + + im = Image.new('I', (10, 10), 40000) + self._test_get_access(im) + # These don't actually appear to be modes that I can actually make, + # as unpack sets them directly into the I mode. + # im = Image.new('I;32L', (10, 10), -2**10) + # self._test_get_access(im) + # im = Image.new('I;32B', (10, 10), 2**10) + # self._test_get_access(im) + + def _test_set_access(self, im, color): + """ Are we writing the correct bits into the 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) + + w, h = im.size + for x in range(0, w, 10): + for y in range(0, h, 10): + access[(x, y)] = color + self.assertEqual(color, caccess[(x, y)]) + + def test_set_vs_c(self): + rgb = lena('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(i, (128, 128)) #PA -- undone how to make + self._test_set_access(lena('F'), 1024.0) + + im = Image.new('I;16', (10, 10), 40000) + self._test_set_access(im, 45000) + im = Image.new('I;16L', (10, 10), 40000) + self._test_set_access(im, 45000) + im = Image.new('I;16B', (10, 10), 40000) + self._test_set_access(im, 45000) + + im = Image.new('I', (10, 10), 40000) + self._test_set_access(im, 45000) + # im = Image.new('I;32L', (10, 10), -(2**10)) + # self._test_set_access(im, -(2**13)+1) + # im = Image.new('I;32B', (10, 10), 2**10) + # self._test_set_access(im, 2**13-1) + + +if __name__ == '__main__': + unittest.main() + +# End of file diff --git a/Tests/test_decompression_bomb.py b/Tests/test_decompression_bomb.py new file mode 100644 index 000000000..0803732ce --- /dev/null +++ b/Tests/test_decompression_bomb.py @@ -0,0 +1,45 @@ +from helper import unittest, PillowTestCase + +from PIL import Image + +test_file = "Tests/images/lena.ppm" + +ORIGINAL_LIMIT = Image.MAX_IMAGE_PIXELS + + +class TestDecompressionBomb(PillowTestCase): + + def tearDown(self): + Image.MAX_IMAGE_PIXELS = ORIGINAL_LIMIT + + def test_no_warning_small_file(self): + # Implicit assert: no warning. + # A warning would cause a failure. + Image.open(test_file) + + def test_no_warning_no_limit(self): + # Arrange + # Turn limit off + Image.MAX_IMAGE_PIXELS = None + self.assertEqual(Image.MAX_IMAGE_PIXELS, None) + + # Act / Assert + # Implicit assert: no warning. + # A warning would cause a failure. + Image.open(test_file) + + def test_warning(self): + # Arrange + # Set limit to a low, easily testable value + Image.MAX_IMAGE_PIXELS = 10 + self.assertEqual(Image.MAX_IMAGE_PIXELS, 10) + + # Act / Assert + self.assert_warning( + Image.DecompressionBombWarning, + lambda: Image.open(test_file)) + +if __name__ == '__main__': + unittest.main() + +# End of file diff --git a/Tests/test_file_bmp.py b/Tests/test_file_bmp.py index dd5f31fd2..e04f3642c 100644 --- a/Tests/test_file_bmp.py +++ b/Tests/test_file_bmp.py @@ -1,38 +1,56 @@ -from tester import * +from helper import unittest, PillowTestCase, lena from PIL import Image import io -def roundtrip(im): - outfile = tempfile("temp.bmp") - im.save(outfile, 'BMP') +class TestFileBmp(PillowTestCase): - reloaded = Image.open(outfile) - reloaded.load() - assert_equal(im.mode, reloaded.mode) - assert_equal(im.size, reloaded.size) - assert_equal(reloaded.format, "BMP") + def roundtrip(self, im): + outfile = self.tempfile("temp.bmp") + + im.save(outfile, 'BMP') + + reloaded = Image.open(outfile) + reloaded.load() + self.assertEqual(im.mode, reloaded.mode) + self.assertEqual(im.size, reloaded.size) + self.assertEqual(reloaded.format, "BMP") + + def test_sanity(self): + self.roundtrip(lena()) + + self.roundtrip(lena("1")) + self.roundtrip(lena("L")) + self.roundtrip(lena("P")) + self.roundtrip(lena("RGB")) + + def test_save_to_bytes(self): + output = io.BytesIO() + im = lena() + im.save(output, "BMP") + + output.seek(0) + reloaded = Image.open(output) + + self.assertEqual(im.mode, reloaded.mode) + self.assertEqual(im.size, reloaded.size) + self.assertEqual(reloaded.format, "BMP") + + def test_dpi(self): + dpi = (72, 72) + + output = io.BytesIO() + im = lena() + im.save(output, "BMP", dpi=dpi) + + output.seek(0) + reloaded = Image.open(output) + + self.assertEqual(reloaded.info["dpi"], dpi) -def test_sanity(): - roundtrip(lena()) - - roundtrip(lena("1")) - roundtrip(lena("L")) - roundtrip(lena("P")) - roundtrip(lena("RGB")) +if __name__ == '__main__': + unittest.main() - -def test_save_to_bytes(): - output = io.BytesIO() - im = lena() - im.save(output, "BMP") - - output.seek(0) - reloaded = Image.open(output) - - assert_equal(im.mode, reloaded.mode) - assert_equal(im.size, reloaded.size) - assert_equal(reloaded.format, "BMP") - +# End of file diff --git a/Tests/test_file_cur.py b/Tests/test_file_cur.py new file mode 100644 index 000000000..54bfe84fe --- /dev/null +++ b/Tests/test_file_cur.py @@ -0,0 +1,27 @@ +from helper import unittest, PillowTestCase + +from PIL import Image, CurImagePlugin + + +class TestFileCur(PillowTestCase): + + def test_sanity(self): + # Arrange + test_file = "Tests/images/deerstalker.cur" + + # Act + im = Image.open(test_file) + + # Assert + self.assertEqual(im.size, (32, 32)) + self.assertIsInstance(im, CurImagePlugin.CurImageFile) + # Check some pixel colors to ensure image is loaded properly + 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)) + + +if __name__ == '__main__': + unittest.main() + +# End of file diff --git a/Tests/test_file_dcx.py b/Tests/test_file_dcx.py new file mode 100644 index 000000000..9a6183651 --- /dev/null +++ b/Tests/test_file_dcx.py @@ -0,0 +1,45 @@ +from helper import unittest, PillowTestCase, lena + +from PIL import Image, DcxImagePlugin + +# Created with ImageMagick: convert lena.ppm lena.dcx +TEST_FILE = "Tests/images/lena.dcx" + + +class TestFileDcx(PillowTestCase): + + def test_sanity(self): + # Arrange + + # Act + im = Image.open(TEST_FILE) + + # Assert + self.assertEqual(im.size, (128, 128)) + self.assertIsInstance(im, DcxImagePlugin.DcxImageFile) + orig = lena() + self.assert_image_equal(im, orig) + + def test_tell(self): + # Arrange + im = Image.open(TEST_FILE) + + # Act + frame = im.tell() + + # Assert + self.assertEqual(frame, 0) + + def test_seek_too_far(self): + # Arrange + im = Image.open(TEST_FILE) + frame = 999 # too big on purpose + + # Act / Assert + self.assertRaises(EOFError, lambda: im.seek(frame)) + + +if __name__ == '__main__': + unittest.main() + +# End of file diff --git a/Tests/test_file_eps.py b/Tests/test_file_eps.py index 34ece8c8b..0ca4249a3 100644 --- a/Tests/test_file_eps.py +++ b/Tests/test_file_eps.py @@ -1,11 +1,8 @@ -from tester import * +from helper import unittest, PillowTestCase from PIL import Image, EpsImagePlugin import io -if not EpsImagePlugin.has_ghostscript(): - skip() - # Our two EPS test files (they are identical except for their bounding boxes) file1 = "Tests/images/zero_bb.eps" file2 = "Tests/images/non_zero_bb.eps" @@ -20,123 +17,127 @@ file2_compare_scale2 = "Tests/images/non_zero_bb_scale2.png" # EPS test files with binary preview file3 = "Tests/images/binary_preview_map.eps" -def test_sanity(): - # Regular scale - image1 = Image.open(file1) - image1.load() - assert_equal(image1.mode, "RGB") - assert_equal(image1.size, (460, 352)) - assert_equal(image1.format, "EPS") - image2 = Image.open(file2) - image2.load() - assert_equal(image2.mode, "RGB") - assert_equal(image2.size, (360, 252)) - assert_equal(image2.format, "EPS") +class TestFileEps(PillowTestCase): - # Double scale - image1_scale2 = Image.open(file1) - image1_scale2.load(scale=2) - assert_equal(image1_scale2.mode, "RGB") - assert_equal(image1_scale2.size, (920, 704)) - assert_equal(image1_scale2.format, "EPS") + def setUp(self): + if not EpsImagePlugin.has_ghostscript(): + self.skipTest("Ghostscript not available") - image2_scale2 = Image.open(file2) - image2_scale2.load(scale=2) - assert_equal(image2_scale2.mode, "RGB") - assert_equal(image2_scale2.size, (720, 504)) - assert_equal(image2_scale2.format, "EPS") + def test_sanity(self): + # Regular scale + image1 = Image.open(file1) + image1.load() + self.assertEqual(image1.mode, "RGB") + self.assertEqual(image1.size, (460, 352)) + self.assertEqual(image1.format, "EPS") + image2 = Image.open(file2) + image2.load() + self.assertEqual(image2.mode, "RGB") + self.assertEqual(image2.size, (360, 252)) + self.assertEqual(image2.format, "EPS") -def test_file_object(): - # issue 479 - image1 = Image.open(file1) - with open(tempfile('temp_file.eps'), 'wb') as fh: - image1.save(fh, 'EPS') + # Double scale + image1_scale2 = Image.open(file1) + image1_scale2.load(scale=2) + self.assertEqual(image1_scale2.mode, "RGB") + self.assertEqual(image1_scale2.size, (920, 704)) + self.assertEqual(image1_scale2.format, "EPS") + image2_scale2 = Image.open(file2) + image2_scale2.load(scale=2) + self.assertEqual(image2_scale2.mode, "RGB") + self.assertEqual(image2_scale2.size, (720, 504)) + self.assertEqual(image2_scale2.format, "EPS") -def test_iobase_object(): - # issue 479 - image1 = Image.open(file1) - with io.open(tempfile('temp_iobase.eps'), 'wb') as fh: - image1.save(fh, 'EPS') + def test_file_object(self): + # issue 479 + image1 = Image.open(file1) + with open(self.tempfile('temp_file.eps'), 'wb') as fh: + image1.save(fh, 'EPS') + def test_iobase_object(self): + # issue 479 + image1 = Image.open(file1) + with io.open(self.tempfile('temp_iobase.eps'), 'wb') as fh: + image1.save(fh, 'EPS') -def test_render_scale1(): - # We need png support for these render test - codecs = dir(Image.core) - if "zip_encoder" not in codecs or "zip_decoder" not in codecs: - skip("zip/deflate support not available") + def test_render_scale1(self): + # We need png support for these render test + codecs = dir(Image.core) + if "zip_encoder" not in codecs or "zip_decoder" not in codecs: + self.skipTest("zip/deflate support not available") - # Zero bounding box - image1_scale1 = Image.open(file1) - image1_scale1.load() - image1_scale1_compare = Image.open(file1_compare).convert("RGB") - image1_scale1_compare.load() - assert_image_similar(image1_scale1, image1_scale1_compare, 5) + # Zero bounding box + image1_scale1 = Image.open(file1) + image1_scale1.load() + image1_scale1_compare = Image.open(file1_compare).convert("RGB") + image1_scale1_compare.load() + self.assert_image_similar(image1_scale1, image1_scale1_compare, 5) - # Non-Zero bounding box - image2_scale1 = Image.open(file2) - image2_scale1.load() - image2_scale1_compare = Image.open(file2_compare).convert("RGB") - image2_scale1_compare.load() - assert_image_similar(image2_scale1, image2_scale1_compare, 10) + # Non-Zero bounding box + image2_scale1 = Image.open(file2) + image2_scale1.load() + image2_scale1_compare = Image.open(file2_compare).convert("RGB") + image2_scale1_compare.load() + self.assert_image_similar(image2_scale1, image2_scale1_compare, 10) + def test_render_scale2(self): + # We need png support for these render test + codecs = dir(Image.core) + if "zip_encoder" not in codecs or "zip_decoder" not in codecs: + self.skipTest("zip/deflate support not available") -def test_render_scale2(): - # We need png support for these render test - codecs = dir(Image.core) - if "zip_encoder" not in codecs or "zip_decoder" not in codecs: - skip("zip/deflate support not available") + # Zero bounding box + image1_scale2 = Image.open(file1) + image1_scale2.load(scale=2) + image1_scale2_compare = Image.open(file1_compare_scale2).convert("RGB") + image1_scale2_compare.load() + self.assert_image_similar(image1_scale2, image1_scale2_compare, 5) - # Zero bounding box - image1_scale2 = Image.open(file1) - image1_scale2.load(scale=2) - image1_scale2_compare = Image.open(file1_compare_scale2).convert("RGB") - image1_scale2_compare.load() - assert_image_similar(image1_scale2, image1_scale2_compare, 5) + # Non-Zero bounding box + image2_scale2 = Image.open(file2) + image2_scale2.load(scale=2) + image2_scale2_compare = Image.open(file2_compare_scale2).convert("RGB") + image2_scale2_compare.load() + self.assert_image_similar(image2_scale2, image2_scale2_compare, 10) - # Non-Zero bounding box - image2_scale2 = Image.open(file2) - image2_scale2.load(scale=2) - image2_scale2_compare = Image.open(file2_compare_scale2).convert("RGB") - image2_scale2_compare.load() - assert_image_similar(image2_scale2, image2_scale2_compare, 10) + def test_resize(self): + # Arrange + image1 = Image.open(file1) + image2 = Image.open(file2) + new_size = (100, 100) + # Act + image1 = image1.resize(new_size) + image2 = image2.resize(new_size) -def test_resize(): - # Arrange - image1 = Image.open(file1) - image2 = Image.open(file2) - new_size = (100, 100) + # Assert + self.assertEqual(image1.size, new_size) + self.assertEqual(image2.size, new_size) - # Act - image1 = image1.resize(new_size) - image2 = image2.resize(new_size) + def test_thumbnail(self): + # Issue #619 + # Arrange + image1 = Image.open(file1) + image2 = Image.open(file2) + new_size = (100, 100) - # Assert - assert_equal(image1.size, new_size) - assert_equal(image2.size, new_size) + # Act + image1.thumbnail(new_size) + image2.thumbnail(new_size) + # Assert + self.assertEqual(max(image1.size), max(new_size)) + self.assertEqual(max(image2.size), max(new_size)) -def test_thumbnail(): - # Issue #619 - # Arrange - image1 = Image.open(file1) - image2 = Image.open(file2) - new_size = (100, 100) + def test_read_binary_preview(self): + # Issue 302 + # open image with binary preview + Image.open(file3) - # Act - image1.thumbnail(new_size) - image2.thumbnail(new_size) - - # Assert - assert_equal(max(image1.size), max(new_size)) - assert_equal(max(image2.size), max(new_size)) - -def test_read_binary_preview(): - # Issue 302 - # open image with binary preview - image1 = Image.open(file3) +if __name__ == '__main__': + unittest.main() # End of file diff --git a/Tests/test_file_fli.py b/Tests/test_file_fli.py index 4e06a732e..0c1d6e36a 100644 --- a/Tests/test_file_fli.py +++ b/Tests/test_file_fli.py @@ -1,14 +1,23 @@ -from tester import * +from helper import unittest, PillowTestCase from PIL import Image # sample ppm stream -file = "Images/lena.fli" +file = "Tests/images/lena.fli" data = open(file, "rb").read() -def test_sanity(): - im = Image.open(file) - im.load() - assert_equal(im.mode, "P") - assert_equal(im.size, (128, 128)) - assert_equal(im.format, "FLI") + +class TestFileFli(PillowTestCase): + + def test_sanity(self): + im = Image.open(file) + im.load() + self.assertEqual(im.mode, "P") + self.assertEqual(im.size, (128, 128)) + self.assertEqual(im.format, "FLI") + + +if __name__ == '__main__': + unittest.main() + +# End of file diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index 4318e178e..bd4a6e76c 100644 --- a/Tests/test_file_gif.py +++ b/Tests/test_file_gif.py @@ -1,87 +1,165 @@ -from tester import * +from helper import unittest, PillowTestCase, lena, netpbm_available from PIL import Image +from PIL import GifImagePlugin codecs = dir(Image.core) -if "gif_encoder" not in codecs or "gif_decoder" not in codecs: - skip("gif support not available") # can this happen? - # sample gif stream -file = "Images/lena.gif" +file = "Tests/images/lena.gif" with open(file, "rb") as f: data = f.read() -def test_sanity(): - im = Image.open(file) - im.load() - assert_equal(im.mode, "P") - assert_equal(im.size, (128, 128)) - assert_equal(im.format, "GIF") -def test_optimize(): - def test(optimize): - im = Image.new("L", (1, 1), 0) +class TestFileGif(PillowTestCase): + + def setUp(self): + if "gif_encoder" not in codecs or "gif_decoder" not in codecs: + self.skipTest("gif support not available") # can this happen? + + def test_sanity(self): + im = Image.open(file) + im.load() + self.assertEqual(im.mode, "P") + self.assertEqual(im.size, (128, 128)) + self.assertEqual(im.format, "GIF") + + def test_optimize(self): + from io import BytesIO + + def test(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) + + 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=optimize) - return len(file.getvalue()) - assert_equal(test(0), 800) - assert_equal(test(1), 38) + im.save(file, "GIF", optimize=True) + self.assertEqual(im.mode, "L") -def test_roundtrip(): - out = tempfile('temp.gif') - im = lena() - im.save(out) - reread = Image.open(out) + def test_roundtrip(self): + out = self.tempfile('temp.gif') + im = lena() + im.save(out) + reread = Image.open(out) - assert_image_similar(reread.convert('RGB'), im, 50) + self.assert_image_similar(reread.convert('RGB'), im, 50) -def test_roundtrip2(): - #see https://github.com/python-imaging/Pillow/issues/403 - out = tempfile('temp.gif') - im = Image.open('Images/lena.gif') - im2 = im.copy() - im2.save(out) - reread = Image.open(out) + 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') + im2 = im.copy() + im2.save(out) + reread = Image.open(out) - assert_image_similar(reread.convert('RGB'), lena(), 50) + self.assert_image_similar(reread.convert('RGB'), lena(), 50) + + def test_palette_handling(self): + # see https://github.com/python-pillow/Pillow/issues/513 + + im = Image.open('Tests/images/lena.gif') + im = im.convert('RGB') + + im = im.resize((100, 100), Image.ANTIALIAS) + im2 = im.convert('P', palette=Image.ADAPTIVE, colors=256) + + f = self.tempfile('temp.gif') + im2.save(f, optimize=True) + + reloaded = Image.open(f) + + self.assert_image_similar(im, reloaded.convert('RGB'), 10) + + def test_palette_434(self): + # see https://github.com/python-pillow/Pillow/issues/434 + + def roundtrip(im, *args, **kwargs): + out = self.tempfile('temp.gif') + im.save(out, *args, **kwargs) + reloaded = Image.open(out) + + return [im, reloaded] + + orig = "Tests/images/test.colors.gif" + im = Image.open(orig) + + self.assert_image_equal(*roundtrip(im)) + self.assert_image_equal(*roundtrip(im, optimize=True)) + + im = im.convert("RGB") + # check automatic P conversion + reloaded = roundtrip(im)[1].convert('RGB') + self.assert_image_equal(im, reloaded) + + @unittest.skipUnless(netpbm_available(), "netpbm not available") + def test_save_netpbm_bmp_mode(self): + img = Image.open(file).convert("RGB") + + tempfile = self.tempfile("temp.gif") + GifImagePlugin._save_netpbm(img, 0, tempfile) + self.assert_image_similar(img, Image.open(tempfile).convert("RGB"), 0) + + @unittest.skipUnless(netpbm_available(), "netpbm not available") + def test_save_netpbm_l_mode(self): + img = Image.open(file).convert("L") + + tempfile = self.tempfile("temp.gif") + GifImagePlugin._save_netpbm(img, 0, tempfile) + self.assert_image_similar(img, Image.open(tempfile).convert("L"), 0) + + def test_seek(self): + img = Image.open("Tests/images/dispose_none.gif") + framecount = 0 + try: + while True: + framecount += 1 + img.seek(img.tell() +1) + except EOFError: + self.assertEqual(framecount, 5) + + def test_dispose_none(self): + img = Image.open("Tests/images/dispose_none.gif") + try: + while True: + img.seek(img.tell() +1) + self.assertEqual(img.disposal_method, 1) + except EOFError: + pass + + def test_dispose_background(self): + img = Image.open("Tests/images/dispose_bgnd.gif") + try: + while True: + img.seek(img.tell() +1) + self.assertEqual(img.disposal_method, 2) + except EOFError: + pass + + def test_dispose_previous(self): + img = Image.open("Tests/images/dispose_prev.gif") + try: + while True: + img.seek(img.tell() +1) + self.assertEqual(img.disposal_method, 3) + except EOFError: + pass + + 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 + self.assertEqual(img.histogram()[img.info['transparency']], 0) -def test_palette_handling(): - # see https://github.com/python-imaging/Pillow/issues/513 +if __name__ == '__main__': + unittest.main() - im = Image.open('Images/lena.gif') - im = im.convert('RGB') - - im = im.resize((100,100), Image.ANTIALIAS) - im2 = im.convert('P', palette=Image.ADAPTIVE, colors=256) - - f = tempfile('temp.gif') - im2.save(f, optimize=True) - - reloaded = Image.open(f) - - assert_image_similar(im, reloaded.convert('RGB'), 10) - -def test_palette_434(): - # see https://github.com/python-imaging/Pillow/issues/434 - - def roundtrip(im, *args, **kwargs): - out = tempfile('temp.gif') - im.save(out, *args, **kwargs) - reloaded = Image.open(out) - - return [im, reloaded] - - orig = "Tests/images/test.colors.gif" - im = Image.open(orig) - - assert_image_equal(*roundtrip(im)) - assert_image_equal(*roundtrip(im, optimize=True)) - - im = im.convert("RGB") - # check automatic P conversion - reloaded = roundtrip(im)[1].convert('RGB') - assert_image_equal(im, reloaded) - - +# End of file diff --git a/Tests/test_file_icns.py b/Tests/test_file_icns.py index 3e31f8879..99f6da9e3 100644 --- a/Tests/test_file_icns.py +++ b/Tests/test_file_icns.py @@ -1,66 +1,74 @@ -from tester import * +from helper import unittest, PillowTestCase from PIL import Image # sample icon file -file = "Images/pillow.icns" +file = "Tests/images/pillow.icns" data = open(file, "rb").read() enable_jpeg2k = hasattr(Image.core, 'jp2klib_version') -def test_sanity(): - # Loading this icon by default should result in the largest size - # (512x512@2x) being loaded - im = Image.open(file) - im.load() - assert_equal(im.mode, "RGBA") - assert_equal(im.size, (1024, 1024)) - assert_equal(im.format, "ICNS") -def test_sizes(): - # Check that we can load all of the sizes, and that the final pixel - # dimensions are as expected - im = Image.open(file) - for w,h,r in im.info['sizes']: - wr = w * r - hr = h * r - im2 = Image.open(file) - im2.size = (w, h, r) - im2.load() - assert_equal(im2.mode, 'RGBA') - assert_equal(im2.size, (wr, hr)) +class TestFileIcns(PillowTestCase): -def test_older_icon(): - # This icon was made with Icon Composer rather than iconutil; it still - # uses PNG rather than JP2, however (since it was made on 10.9). - im = Image.open('Tests/images/pillow2.icns') - for w,h,r in im.info['sizes']: - wr = w * r - hr = h * r - im2 = Image.open('Tests/images/pillow2.icns') - im2.size = (w, h, r) - im2.load() - assert_equal(im2.mode, 'RGBA') - assert_equal(im2.size, (wr, hr)) + def test_sanity(self): + # Loading this icon by default should result in the largest size + # (512x512@2x) being loaded + im = Image.open(file) + im.load() + self.assertEqual(im.mode, "RGBA") + self.assertEqual(im.size, (1024, 1024)) + self.assertEqual(im.format, "ICNS") -def test_jp2_icon(): - # This icon was made by using Uli Kusterer's oldiconutil to replace - # the PNG images with JPEG 2000 ones. The advantage of doing this is - # that OS X 10.5 supports JPEG 2000 but not PNG; some commercial - # software therefore does just this. - - # (oldiconutil is here: https://github.com/uliwitness/oldiconutil) + 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) + for w, h, r in im.info['sizes']: + wr = w * r + hr = h * r + im2 = Image.open(file) + im2.size = (w, h, r) + im2.load() + self.assertEqual(im2.mode, 'RGBA') + self.assertEqual(im2.size, (wr, hr)) - if not enable_jpeg2k: - return - - im = Image.open('Tests/images/pillow3.icns') - for w,h,r in im.info['sizes']: - wr = w * r - hr = h * r - im2 = Image.open('Tests/images/pillow3.icns') - im2.size = (w, h, r) - im2.load() - assert_equal(im2.mode, 'RGBA') - assert_equal(im2.size, (wr, hr)) - + def test_older_icon(self): + # This icon was made with Icon Composer rather than iconutil; it still + # uses PNG rather than JP2, however (since it was made on 10.9). + im = Image.open('Tests/images/pillow2.icns') + for w, h, r in im.info['sizes']: + wr = w * r + hr = h * r + im2 = Image.open('Tests/images/pillow2.icns') + im2.size = (w, h, r) + im2.load() + self.assertEqual(im2.mode, 'RGBA') + self.assertEqual(im2.size, (wr, hr)) + + def test_jp2_icon(self): + # This icon was made by using Uli Kusterer's oldiconutil to replace + # the PNG images with JPEG 2000 ones. The advantage of doing this is + # that OS X 10.5 supports JPEG 2000 but not PNG; some commercial + # software therefore does just this. + + # (oldiconutil is here: https://github.com/uliwitness/oldiconutil) + + if not enable_jpeg2k: + return + + im = Image.open('Tests/images/pillow3.icns') + for w, h, r in im.info['sizes']: + wr = w * r + hr = h * r + im2 = Image.open('Tests/images/pillow3.icns') + im2.size = (w, h, r) + im2.load() + self.assertEqual(im2.mode, 'RGBA') + self.assertEqual(im2.size, (wr, hr)) + + +if __name__ == '__main__': + unittest.main() + +# End of file diff --git a/Tests/test_file_ico.py b/Tests/test_file_ico.py index e0db34acc..c3bf7a992 100644 --- a/Tests/test_file_ico.py +++ b/Tests/test_file_ico.py @@ -1,14 +1,23 @@ -from tester import * +from helper import unittest, PillowTestCase from PIL import Image # sample ppm stream -file = "Images/lena.ico" +file = "Tests/images/lena.ico" data = open(file, "rb").read() -def test_sanity(): - im = Image.open(file) - im.load() - assert_equal(im.mode, "RGBA") - assert_equal(im.size, (16, 16)) - assert_equal(im.format, "ICO") + +class TestFileIco(PillowTestCase): + + def test_sanity(self): + im = Image.open(file) + im.load() + self.assertEqual(im.mode, "RGBA") + self.assertEqual(im.size, (16, 16)) + self.assertEqual(im.format, "ICO") + + +if __name__ == '__main__': + unittest.main() + +# End of file diff --git a/Tests/test_file_iptc.py b/Tests/test_file_iptc.py new file mode 100644 index 000000000..bd331e5b2 --- /dev/null +++ b/Tests/test_file_iptc.py @@ -0,0 +1,79 @@ +from helper import unittest, PillowTestCase, lena + +from PIL import Image, IptcImagePlugin + +TEST_FILE = "Tests/images/iptc.jpg" + + +class TestFileIptc(PillowTestCase): + + # Helpers + + def dummy_IptcImagePlugin(self): + # Create an IptcImagePlugin object without initializing it + class FakeImage: + pass + im = FakeImage() + im.__class__ = IptcImagePlugin.IptcImageFile + return im + + # Tests + + def test_getiptcinfo_jpg_none(self): + # Arrange + im = lena() + + # Act + iptc = IptcImagePlugin.getiptcinfo(im) + + # Assert + self.assertIsNone(iptc) + + def test_getiptcinfo_jpg_found(self): + # Arrange + im = Image.open(TEST_FILE) + + # Act + iptc = IptcImagePlugin.getiptcinfo(im) + + # Assert + self.assertIsInstance(iptc, dict) + self.assertEqual(iptc[(2, 90)], b"Budapest") + self.assertEqual(iptc[(2, 101)], b"Hungary") + + def test_i(self): + # Arrange + c = b"a" + + # Act + ret = IptcImagePlugin.i(c) + + # Assert + self.assertEqual(ret, 97) + + def test_dump(self): + # Arrange + c = b"abc" + # Temporarily redirect stdout + try: + from cStringIO import StringIO + except ImportError: + from io import StringIO + import sys + old_stdout = sys.stdout + sys.stdout = mystdout = StringIO() + + # Act + IptcImagePlugin.dump(c) + + # Reset stdout + sys.stdout = old_stdout + + # Assert + self.assertEqual(mystdout.getvalue(), "61 62 63 \n") + + +if __name__ == '__main__': + unittest.main() + +# End of file diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index 4871c3fbf..3bf757332 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -1,238 +1,302 @@ -from tester import * +from helper import unittest, PillowTestCase, lena, py3 +from helper import djpeg_available, cjpeg_available import random +from io import BytesIO from PIL import Image from PIL import ImageFile +from PIL import JpegImagePlugin codecs = dir(Image.core) -if "jpeg_encoder" not in codecs or "jpeg_decoder" not in codecs: - skip("jpeg support not available") - -test_file = "Images/lena.jpg" +test_file = "Tests/images/lena.jpg" -def roundtrip(im, **options): - out = BytesIO() - im.save(out, "JPEG", **options) - bytes = out.tell() - out.seek(0) - im = Image.open(out) - im.bytes = bytes # for testing only - return im +class TestFileJpeg(PillowTestCase): -# -------------------------------------------------------------------- + def setUp(self): + if "jpeg_encoder" not in codecs or "jpeg_decoder" not in codecs: + self.skipTest("jpeg support not available") + def roundtrip(self, im, **options): + out = BytesIO() + im.save(out, "JPEG", **options) + bytes = out.tell() + out.seek(0) + im = Image.open(out) + im.bytes = bytes # for testing only + return im -def test_sanity(): + def test_sanity(self): - # internal version number - assert_match(Image.core.jpeglib_version, "\d+\.\d+$") + # internal version number + self.assertRegexpMatches(Image.core.jpeglib_version, "\d+\.\d+$") - im = Image.open(test_file) - im.load() - assert_equal(im.mode, "RGB") - assert_equal(im.size, (128, 128)) - assert_equal(im.format, "JPEG") - - -# -------------------------------------------------------------------- - -def test_app(): - # Test APP/COM reader (@PIL135) - im = Image.open(test_file) - assert_equal(im.applist[0], - ("APP0", b"JFIF\x00\x01\x01\x00\x00\x01\x00\x01\x00\x00")) - assert_equal(im.applist[1], ("COM", b"Python Imaging Library")) - assert_equal(len(im.applist), 2) - - -def test_cmyk(): - # Test CMYK handling. Thanks to Tim and Charlie for test data, - # Michael for getting me to look one more time. - f = "Tests/images/pil_sample_cmyk.jpg" - im = Image.open(f) - # the source image has red pixels in the upper left corner. - c, m, y, k = [x / 255.0 for x in im.getpixel((0, 0))] - assert_true(c == 0.0 and m > 0.8 and y > 0.8 and 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))] - assert_true(k > 0.9) - # roundtrip, and check again - im = roundtrip(im) - c, m, y, k = [x / 255.0 for x in im.getpixel((0, 0))] - assert_true(c == 0.0 and m > 0.8 and y > 0.8 and k == 0.0) - c, m, y, k = [x / 255.0 for x in im.getpixel((im.size[0]-1, im.size[1]-1))] - assert_true(k > 0.9) - - -def test_dpi(): - def test(xdpi, ydpi=None): im = Image.open(test_file) - im = roundtrip(im, dpi=(xdpi, ydpi or xdpi)) - return im.info.get("dpi") - assert_equal(test(72), (72, 72)) - assert_equal(test(300), (300, 300)) - assert_equal(test(100, 200), (100, 200)) - assert_equal(test(0), None) # square pixels + im.load() + self.assertEqual(im.mode, "RGB") + self.assertEqual(im.size, (128, 128)) + self.assertEqual(im.format, "JPEG") + + def test_app(self): + # Test APP/COM reader (@PIL135) + 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")) + self.assertEqual(len(im.applist), 2) + + def test_cmyk(self): + # Test CMYK handling. Thanks to Tim and Charlie for test data, + # Michael for getting me to look one more time. + f = "Tests/images/pil_sample_cmyk.jpg" + im = Image.open(f) + # the source image has red pixels in the upper left corner. + c, m, y, k = [x / 255.0 for x in im.getpixel((0, 0))] + self.assertEqual(c, 0.0) + self.assertGreater(m, 0.8) + 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))] + self.assertGreater(k, 0.9) + # roundtrip, and check again + im = self.roundtrip(im) + c, m, y, k = [x / 255.0 for x in im.getpixel((0, 0))] + self.assertEqual(c, 0.0) + 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))] + self.assertGreater(k, 0.9) + + def test_dpi(self): + def test(xdpi, ydpi=None): + 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)) + self.assertEqual(test(300), (300, 300)) + self.assertEqual(test(100, 200), (100, 200)) + self.assertEqual(test(0), None) # square pixels + + def test_icc(self): + # Test ICC support + im1 = Image.open("Tests/images/rgb.jpg") + icc_profile = im1.info["icc_profile"] + self.assertEqual(len(icc_profile), 3144) + # Roundtrip via physical file. + f = self.tempfile("temp.jpg") + im1.save(f, icc_profile=icc_profile) + 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) + self.assert_image_equal(im1, im2) + self.assertFalse(im1.info.get("icc_profile")) + self.assertTrue(im2.info.get("icc_profile")) + + def test_icc_big(self): + # Make sure that the "extra" support handles large blocks + def test(n): + # The ICC APP marker can store 65519 bytes per marker, so + # using a 4-byte test code should allow us to detect out of + # 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) + self.assertEqual(im1.info.get("icc_profile"), icc_profile or None) + test(0) + test(1) + test(3) + test(4) + test(5) + test(65533-14) # full JPEG marker block + test(65533-14+1) # full block plus one byte + test(ImageFile.MAXBLOCK) # full buffer block + test(ImageFile.MAXBLOCK+1) # full buffer block plus one byte + test(ImageFile.MAXBLOCK*4+3) # large block + + def test_optimize(self): + im1 = self.roundtrip(lena()) + im2 = self.roundtrip(lena(), optimize=1) + self.assert_image_equal(im1, im2) + self.assertGreaterEqual(im1.bytes, im2.bytes) + + def test_optimize_large_buffer(self): + # https://github.com/python-pillow/Pillow/issues/148 + f = self.tempfile('temp.jpg') + # this requires ~ 1.5x Image.MAXBLOCK + im = Image.new("RGB", (4096, 4096), 0xff3333) + im.save(f, format="JPEG", optimize=True) + + def test_progressive(self): + im1 = self.roundtrip(lena()) + im2 = self.roundtrip(lena(), progressive=True) + self.assert_image_equal(im1, im2) + self.assertGreaterEqual(im1.bytes, im2.bytes) + + def test_progressive_large_buffer(self): + f = self.tempfile('temp.jpg') + # this requires ~ 1.5x Image.MAXBLOCK + im = Image.new("RGB", (4096, 4096), 0xff3333) + im.save(f, format="JPEG", progressive=True) + + def test_progressive_large_buffer_highest_quality(self): + f = self.tempfile('temp.jpg') + 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)) + 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) + + def test_large_exif(self): + # https://github.com/python-pillow/Pillow/issues/148 + f = self.tempfile('temp.jpg') + im = lena() + 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 + self.assert_image_equal(im1, im2) + self.assert_image_equal(im1, im3) + self.assertFalse(im1.info.get("progressive")) + self.assertFalse(im1.info.get("progression")) + self.assertTrue(im2.info.get("progressive")) + self.assertTrue(im2.info.get("progression")) + self.assertTrue(im3.info.get("progressive")) + self.assertTrue(im3.info.get("progression")) + + def test_quality(self): + im1 = self.roundtrip(lena()) + im2 = self.roundtrip(lena(), 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) + self.assert_image(im1, im2.mode, im2.size) + + def test_subsampling(self): + def getsampling(im): + 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 + self.assertEqual(getsampling(im), (2, 2, 1, 1, 1, 1)) + im = self.roundtrip(lena(), subsampling=0) # 4:4:4 + self.assertEqual(getsampling(im), (1, 1, 1, 1, 1, 1)) + im = self.roundtrip(lena(), subsampling=1) # 4:2:2 + self.assertEqual(getsampling(im), (2, 1, 1, 1, 1, 1)) + im = self.roundtrip(lena(), subsampling=2) # 4:1:1 + self.assertEqual(getsampling(im), (2, 2, 1, 1, 1, 1)) + im = self.roundtrip(lena(), subsampling=3) # default (undefined) + self.assertEqual(getsampling(im), (2, 2, 1, 1, 1, 1)) + + im = self.roundtrip(lena(), subsampling="4:4:4") + self.assertEqual(getsampling(im), (1, 1, 1, 1, 1, 1)) + im = self.roundtrip(lena(), subsampling="4:2:2") + self.assertEqual(getsampling(im), (2, 1, 1, 1, 1, 1)) + im = self.roundtrip(lena(), 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")) + + def test_exif(self): + im = Image.open("Tests/images/pil_sample_rgb.jpg") + info = im._getexif() + self.assertEqual(info[305], 'Adobe Photoshop CS Macintosh') + + def test_mp(self): + im = Image.open("Tests/images/pil_sample_rgb.jpg") + self.assertIsNone(im._getmp()) + + def test_quality_keep(self): + im = Image.open("Tests/images/lena.jpg") + f = self.tempfile('temp.jpg') + im.save(f, quality='keep') + + def test_junk_jpeg_header(self): + # https://github.com/python-pillow/Pillow/issues/630 + filename = "Tests/images/junk_jpeg_header.jpg" + Image.open(filename) + + def test_qtables(self): + im = Image.open("Tests/images/lena.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='keep'), 30) + + #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 + 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)] + # list of qtable lists + 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) + # dict of qtable lists + self.assert_image_similar(im, + self.roundtrip(im, + qtables={0:standard_l_qtable, + 1:standard_chrominance_qtable}), + 30) + + @unittest.skipUnless(djpeg_available(), "djpeg not available") + def test_load_djpeg(self): + img = Image.open(test_file) + img.load_djpeg() + 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) + + 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) -def test_icc(): - # Test ICC support - im1 = Image.open("Tests/images/rgb.jpg") - icc_profile = im1.info["icc_profile"] - assert_equal(len(icc_profile), 3144) - # Roundtrip via physical file. - f = tempfile("temp.jpg") - im1.save(f, icc_profile=icc_profile) - im2 = Image.open(f) - assert_equal(im2.info.get("icc_profile"), icc_profile) - # Roundtrip via memory buffer. - im1 = roundtrip(lena()) - im2 = roundtrip(lena(), icc_profile=icc_profile) - assert_image_equal(im1, im2) - assert_false(im1.info.get("icc_profile")) - assert_true(im2.info.get("icc_profile")) - - -def test_icc_big(): - # Make sure that the "extra" support handles large blocks - def test(n): - # The ICC APP marker can store 65519 bytes per marker, so - # using a 4-byte test code should allow us to detect out of - # order issues. - icc_profile = (b"Test"*int(n/4+1))[:n] - assert len(icc_profile) == n # sanity - im1 = roundtrip(lena(), icc_profile=icc_profile) - assert_equal(im1.info.get("icc_profile"), icc_profile or None) - test(0) - test(1) - test(3) - test(4) - test(5) - test(65533-14) # full JPEG marker block - test(65533-14+1) # full block plus one byte - test(ImageFile.MAXBLOCK) # full buffer block - test(ImageFile.MAXBLOCK+1) # full buffer block plus one byte - test(ImageFile.MAXBLOCK*4+3) # large block - - -def test_optimize(): - im1 = roundtrip(lena()) - im2 = roundtrip(lena(), optimize=1) - assert_image_equal(im1, im2) - assert_true(im1.bytes >= im2.bytes) - - -def test_optimize_large_buffer(): - # https://github.com/python-imaging/Pillow/issues/148 - f = tempfile('temp.jpg') - # this requires ~ 1.5x Image.MAXBLOCK - im = Image.new("RGB", (4096, 4096), 0xff3333) - im.save(f, format="JPEG", optimize=True) - - -def test_progressive(): - im1 = roundtrip(lena()) - im2 = roundtrip(lena(), progressive=True) - assert_image_equal(im1, im2) - assert_true(im1.bytes >= im2.bytes) - - -def test_progressive_large_buffer(): - f = tempfile('temp.jpg') - # this requires ~ 1.5x Image.MAXBLOCK - im = Image.new("RGB", (4096, 4096), 0xff3333) - im.save(f, format="JPEG", progressive=True) - - -def test_progressive_large_buffer_highest_quality(): - f = tempfile('temp.jpg') - 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)) - 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) - - -def test_large_exif(): - # https://github.com/python-imaging/Pillow/issues/148 - f = tempfile('temp.jpg') - im = lena() - im.save(f, 'JPEG', quality=90, exif=b"1"*65532) - - -def test_progressive_compat(): - im1 = roundtrip(lena()) - im2 = roundtrip(lena(), progressive=1) - im3 = roundtrip(lena(), progression=1) # compatibility - assert_image_equal(im1, im2) - assert_image_equal(im1, im3) - assert_false(im1.info.get("progressive")) - assert_false(im1.info.get("progression")) - assert_true(im2.info.get("progressive")) - assert_true(im2.info.get("progression")) - assert_true(im3.info.get("progressive")) - assert_true(im3.info.get("progression")) - - -def test_quality(): - im1 = roundtrip(lena()) - im2 = roundtrip(lena(), quality=50) - assert_image(im1, im2.mode, im2.size) - assert_true(im1.bytes >= im2.bytes) - - -def test_smooth(): - im1 = roundtrip(lena()) - im2 = roundtrip(lena(), smooth=100) - assert_image(im1, im2.mode, im2.size) - - -def test_subsampling(): - def getsampling(im): - layer = im.layer - return layer[0][1:3] + layer[1][1:3] + layer[2][1:3] - # experimental API - im = roundtrip(lena(), subsampling=-1) # default - assert_equal(getsampling(im), (2, 2, 1, 1, 1, 1)) - im = roundtrip(lena(), subsampling=0) # 4:4:4 - assert_equal(getsampling(im), (1, 1, 1, 1, 1, 1)) - im = roundtrip(lena(), subsampling=1) # 4:2:2 - assert_equal(getsampling(im), (2, 1, 1, 1, 1, 1)) - im = roundtrip(lena(), subsampling=2) # 4:1:1 - assert_equal(getsampling(im), (2, 2, 1, 1, 1, 1)) - im = roundtrip(lena(), subsampling=3) # default (undefined) - assert_equal(getsampling(im), (2, 2, 1, 1, 1, 1)) - - im = roundtrip(lena(), subsampling="4:4:4") - assert_equal(getsampling(im), (1, 1, 1, 1, 1, 1)) - im = roundtrip(lena(), subsampling="4:2:2") - assert_equal(getsampling(im), (2, 1, 1, 1, 1, 1)) - im = roundtrip(lena(), subsampling="4:1:1") - assert_equal(getsampling(im), (2, 2, 1, 1, 1, 1)) - - assert_exception(TypeError, lambda: roundtrip(lena(), subsampling="1:1:1")) - - -def test_exif(): - im = Image.open("Tests/images/pil_sample_rgb.jpg") - info = im._getexif() - assert_equal(info[305], 'Adobe Photoshop CS Macintosh') - - -def test_quality_keep(): - im = Image.open("Images/lena.jpg") - f = tempfile('temp.jpg') - assert_no_exception(lambda: im.save(f, quality='keep')) - - -def test_junk_jpeg_header(): - # https://github.com/python-imaging/Pillow/issues/630 - filename = "Tests/images/junk_jpeg_header.jpg" - assert_no_exception(lambda: Image.open(filename)) +if __name__ == '__main__': + unittest.main() # End of file diff --git a/Tests/test_file_jpeg2k.py b/Tests/test_file_jpeg2k.py index b11e5e6ab..a0e7dfb53 100644 --- a/Tests/test_file_jpeg2k.py +++ b/Tests/test_file_jpeg2k.py @@ -1,110 +1,169 @@ -from tester import * +from helper import unittest, PillowTestCase from PIL import Image -from PIL import ImageFile +from io import BytesIO codecs = dir(Image.core) -if "jpeg2k_encoder" not in codecs or "jpeg2k_decoder" not in codecs: - skip('JPEG 2000 support not available') - -# OpenJPEG 2.0.0 outputs this debugging message sometimes; we should -# ignore it---it doesn't represent a test failure. -ignore('Not enough memory to handle tile data') - test_card = Image.open('Tests/images/test-card.png') test_card.load() -def roundtrip(im, **options): - out = BytesIO() - im.save(out, "JPEG2000", **options) - bytes = out.tell() - out.seek(0) - im = Image.open(out) - im.bytes = bytes # for testing only - im.load() - return im +# OpenJPEG 2.0.0 outputs this debugging message sometimes; we should +# ignore it---it doesn't represent a test failure. +# 'Not enough memory to handle tile data' -# ---------------------------------------------------------------------- -def test_sanity(): - # Internal version number - assert_match(Image.core.jp2klib_version, '\d+\.\d+\.\d+$') +class TestFileJpeg2k(PillowTestCase): - im = Image.open('Tests/images/test-card-lossless.jp2') - im.load() - assert_equal(im.mode, 'RGB') - assert_equal(im.size, (640, 480)) - assert_equal(im.format, 'JPEG2000') - -# ---------------------------------------------------------------------- + def setUp(self): + if "jpeg2k_encoder" not in codecs or "jpeg2k_decoder" not in codecs: + self.skipTest('JPEG 2000 support not available') -# These two test pre-written JPEG 2000 files that were not written with -# PIL (they were made using Adobe Photoshop) + def roundtrip(self, im, **options): + out = BytesIO() + im.save(out, "JPEG2000", **options) + bytes = out.tell() + out.seek(0) + im = Image.open(out) + im.bytes = bytes # for testing only + im.load() + return im -def test_lossless(): - im = Image.open('Tests/images/test-card-lossless.jp2') - im.load() - im.save('/tmp/test-card.png') - assert_image_similar(im, test_card, 1.0e-3) + def test_sanity(self): + # Internal version number + self.assertRegexpMatches(Image.core.jp2klib_version, '\d+\.\d+\.\d+$') -def test_lossy_tiled(): - im = Image.open('Tests/images/test-card-lossy-tiled.jp2') - im.load() - assert_image_similar(im, test_card, 2.0) + im = Image.open('Tests/images/test-card-lossless.jp2') + im.load() + self.assertEqual(im.mode, 'RGB') + self.assertEqual(im.size, (640, 480)) + self.assertEqual(im.format, 'JPEG2000') -# ---------------------------------------------------------------------- + def test_bytesio(self): + with open('Tests/images/test-card-lossless.jp2', 'rb') as f: + data = BytesIO(f.read()) + im = Image.open(data) + im.load() + self.assert_image_similar(im, test_card, 1.0e-3) -def test_lossless_rt(): - im = roundtrip(test_card) - assert_image_equal(im, test_card) + # These two test pre-written JPEG 2000 files that were not written with + # PIL (they were made using Adobe Photoshop) -def test_lossy_rt(): - im = roundtrip(test_card, quality_layers=[20]) - assert_image_similar(im, test_card, 2.0) + def test_lossless(self): + im = Image.open('Tests/images/test-card-lossless.jp2') + im.load() + im.save('/tmp/test-card.png') + self.assert_image_similar(im, test_card, 1.0e-3) -def test_tiled_rt(): - im = roundtrip(test_card, tile_size=(128, 128)) - assert_image_equal(im, test_card) + def test_lossy_tiled(self): + im = Image.open('Tests/images/test-card-lossy-tiled.jp2') + im.load() + self.assert_image_similar(im, test_card, 2.0) -def test_tiled_offset_rt(): - im = roundtrip(test_card, tile_size=(128, 128), tile_offset=(0, 0), - offset=(32, 32)) - assert_image_equal(im, test_card) - -def test_irreversible_rt(): - im = roundtrip(test_card, irreversible=True, quality_layers=[20]) - assert_image_similar(im, test_card, 2.0) + def test_lossless_rt(self): + im = self.roundtrip(test_card) + self.assert_image_equal(im, test_card) -def test_prog_qual_rt(): - im = roundtrip(test_card, quality_layers=[60, 40, 20], progression='LRCP') - assert_image_similar(im, test_card, 2.0) + def test_lossy_rt(self): + im = self.roundtrip(test_card, quality_layers=[20]) + self.assert_image_similar(im, test_card, 2.0) -def test_prog_res_rt(): - im = roundtrip(test_card, num_resolutions=8, progression='RLCP') - assert_image_equal(im, test_card) + def test_tiled_rt(self): + im = self.roundtrip(test_card, tile_size=(128, 128)) + self.assert_image_equal(im, test_card) -# ---------------------------------------------------------------------- + def test_tiled_offset_rt(self): + im = self.roundtrip( + test_card, tile_size=(128, 128), + tile_offset=(0, 0), offset=(32, 32)) + self.assert_image_equal(im, test_card) -def test_reduce(): - im = Image.open('Tests/images/test-card-lossless.jp2') - im.reduce = 2 - im.load() - assert_equal(im.size, (160, 120)) + def test_irreversible_rt(self): + im = self.roundtrip(test_card, irreversible=True, quality_layers=[20]) + self.assert_image_similar(im, test_card, 2.0) -def test_layers(): - out = BytesIO() - test_card.save(out, 'JPEG2000', quality_layers=[100, 50, 10], - progression='LRCP') - out.seek(0) - - im = Image.open(out) - im.layers = 1 - im.load() - assert_image_similar(im, test_card, 13) + def test_prog_qual_rt(self): + im = self.roundtrip( + test_card, quality_layers=[60, 40, 20], progression='LRCP') + self.assert_image_similar(im, test_card, 2.0) - out.seek(0) - im = Image.open(out) - im.layers = 3 - im.load() - assert_image_similar(im, test_card, 0.4) + def test_prog_res_rt(self): + im = self.roundtrip(test_card, num_resolutions=8, progression='RLCP') + self.assert_image_equal(im, test_card) + + def test_reduce(self): + im = Image.open('Tests/images/test-card-lossless.jp2') + im.reduce = 2 + im.load() + self.assertEqual(im.size, (160, 120)) + + def test_layers(self): + out = BytesIO() + test_card.save(out, 'JPEG2000', quality_layers=[100, 50, 10], + progression='LRCP') + out.seek(0) + + im = Image.open(out) + im.layers = 1 + im.load() + self.assert_image_similar(im, test_card, 13) + + out.seek(0) + im = Image.open(out) + im.layers = 3 + im.load() + self.assert_image_similar(im, test_card, 0.4) + + def test_rgba(self): + # Arrange + j2k = Image.open('Tests/images/rgb_trns_ycbc.j2k') + jp2 = Image.open('Tests/images/rgb_trns_ycbc.jp2') + + # Act + j2k.load() + jp2.load() + + # Assert + self.assertEqual(j2k.mode, 'RGBA') + self.assertEqual(jp2.mode, 'RGBA') + + def test_16bit_monochrome_has_correct_mode(self): + + j2k = Image.open('Tests/images/16bit.cropped.j2k') + jp2 = Image.open('Tests/images/16bit.cropped.jp2') + + j2k.load() + jp2.load() + + self.assertEqual(j2k.mode, 'I;16') + self.assertEqual(jp2.mode, 'I;16') + + def test_16bit_monchrome_jp2_like_tiff(self): + + tiff_16bit = Image.open('Tests/images/16bit.cropped.tif') + jp2 = Image.open('Tests/images/16bit.cropped.jp2') + self.assert_image_similar(jp2, tiff_16bit, 1e-3) + + def test_16bit_monchrome_j2k_like_tiff(self): + + tiff_16bit = Image.open('Tests/images/16bit.cropped.tif') + j2k = Image.open('Tests/images/16bit.cropped.j2k') + self.assert_image_similar(j2k, tiff_16bit, 1e-3) + + def test_16bit_j2k_roundtrips(self): + + j2k = Image.open('Tests/images/16bit.cropped.j2k') + im = self.roundtrip(j2k) + self.assert_image_equal(im, j2k) + + def test_16bit_jp2_roundtrips(self): + + jp2 = Image.open('Tests/images/16bit.cropped.jp2') + im = self.roundtrip(jp2) + self.assert_image_equal(im, jp2) + + +if __name__ == '__main__': + unittest.main() + +# End of file diff --git a/Tests/test_file_libtiff.py b/Tests/test_file_libtiff.py index 58fa75239..60eea8b3b 100644 --- a/Tests/test_file_libtiff.py +++ b/Tests/test_file_libtiff.py @@ -1,300 +1,318 @@ -from tester import * +from helper import unittest, PillowTestCase, lena, py3 + import os from PIL import Image, TiffImagePlugin -codecs = dir(Image.core) -if "libtiff_encoder" not in codecs or "libtiff_decoder" not in codecs: - skip("tiff support not available") +class TestFileLibTiff(PillowTestCase): -def _assert_noerr(im): - """Helper tests that assert basic sanity about the g4 tiff reading""" - #1 bit - assert_equal(im.mode, "1") + def setUp(self): + codecs = dir(Image.core) - # Does the data actually load - assert_no_exception(lambda: im.load()) - assert_no_exception(lambda: im.getdata()) + if "libtiff_encoder" not in codecs or "libtiff_decoder" not in codecs: + self.skipTest("tiff support not available") - try: - assert_equal(im._compression, 'group4') - except: - print("No _compression") - print (dir(im)) + def _assert_noerr(self, im): + """Helper tests that assert basic sanity about the g4 tiff reading""" + # 1 bit + self.assertEqual(im.mode, "1") - # can we write it back out, in a different form. - out = tempfile("temp.png") - assert_no_exception(lambda: im.save(out)) + # Does the data actually load + im.load() + im.getdata() -def test_g4_tiff(): - """Test the ordinary file path load path""" + try: + self.assertEqual(im._compression, 'group4') + except: + print("No _compression") + print (dir(im)) - file = "Tests/images/lena_g4_500.tif" - im = Image.open(file) + # can we write it back out, in a different form. + out = self.tempfile("temp.png") + im.save(out) - assert_equal(im.size, (500,500)) - _assert_noerr(im) + def test_g4_tiff(self): + """Test the ordinary file path load path""" -def test_g4_large(): - file = "Tests/images/pport_g4.tif" - im = Image.open(file) - _assert_noerr(im) + file = "Tests/images/lena_g4_500.tif" + im = Image.open(file) -def test_g4_tiff_file(): - """Testing the string load path""" + self.assertEqual(im.size, (500, 500)) + self._assert_noerr(im) - file = "Tests/images/lena_g4_500.tif" - with open(file,'rb') as f: - im = Image.open(f) + def test_g4_large(self): + file = "Tests/images/pport_g4.tif" + im = Image.open(file) + self._assert_noerr(im) - assert_equal(im.size, (500,500)) - _assert_noerr(im) + def test_g4_tiff_file(self): + """Testing the string load path""" -def test_g4_tiff_bytesio(): - """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: - s.write(f.read()) - s.seek(0) - im = Image.open(s) + file = "Tests/images/lena_g4_500.tif" + with open(file, 'rb') as f: + im = Image.open(f) - assert_equal(im.size, (500,500)) - _assert_noerr(im) + self.assertEqual(im.size, (500, 500)) + self._assert_noerr(im) -def test_g4_eq_png(): - """ 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') + 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: + s.write(f.read()) + s.seek(0) + im = Image.open(s) - assert_image_equal(g4, png) + self.assertEqual(im.size, (500, 500)) + self._assert_noerr(im) -# see https://github.com/python-imaging/Pillow/issues/279 -def test_g4_fillorder_eq_png(): - """ Checking that we're actually getting the data that we expect""" - png = Image.open('Tests/images/g4-fillorder-test.png') - g4 = Image.open('Tests/images/g4-fillorder-test.tif') + 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') - assert_image_equal(g4, png) + self.assert_image_equal(g4, png) -def test_g4_write(): - """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) + # see https://github.com/python-pillow/Pillow/issues/279 + def test_g4_fillorder_eq_png(self): + """ Checking that we're actually getting the data that we expect""" + png = Image.open('Tests/images/g4-fillorder-test.png') + g4 = Image.open('Tests/images/g4-fillorder-test.tif') - out = tempfile("temp.tif") - rot = orig.transpose(Image.ROTATE_90) - assert_equal(rot.size,(500,500)) - rot.save(out) + self.assert_image_equal(g4, png) - reread = Image.open(out) - assert_equal(reread.size,(500,500)) - _assert_noerr(reread) - assert_image_equal(reread, rot) - assert_equal(reread.info['compression'], 'group4') + 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) - assert_equal(reread.info['compression'], orig.info['compression']) - - assert_false(orig.tobytes() == reread.tobytes()) + out = self.tempfile("temp.tif") + rot = orig.transpose(Image.ROTATE_90) + self.assertEqual(rot.size, (500, 500)) + rot.save(out) -def test_adobe_deflate_tiff(): - file = "Tests/images/tiff_adobe_deflate.tif" - im = Image.open(file) + reread = Image.open(out) + self.assertEqual(reread.size, (500, 500)) + self._assert_noerr(reread) + self.assert_image_equal(reread, rot) + self.assertEqual(reread.info['compression'], 'group4') - assert_equal(im.mode, "RGB") - assert_equal(im.size, (278, 374)) - assert_equal(im.tile[0][:3], ('tiff_adobe_deflate', (0, 0, 278, 374), 0)) - assert_no_exception(lambda: im.load()) + self.assertEqual(reread.info['compression'], orig.info['compression']) -def test_write_metadata(): - """ Test metadata writing through libtiff """ - img = Image.open('Tests/images/lena_g4.tif') - f = tempfile('temp.tiff') + self.assertNotEqual(orig.tobytes(), reread.tobytes()) - img.save(f, tiffinfo = img.tag) + def test_adobe_deflate_tiff(self): + file = "Tests/images/tiff_adobe_deflate.tif" + im = Image.open(file) - loaded = Image.open(f) + self.assertEqual(im.mode, "RGB") + self.assertEqual(im.size, (278, 374)) + self.assertEqual( + im.tile[0][:3], ('tiff_adobe_deflate', (0, 0, 278, 374), 0)) + im.load() - original = img.tag.named() - reloaded = loaded.tag.named() + def test_write_metadata(self): + """ Test metadata writing through libtiff """ + img = Image.open('Tests/images/lena_g4.tif') + f = self.tempfile('temp.tiff') - # PhotometricInterpretation is set from SAVE_INFO, not the original image. - ignored = ['StripByteCounts', 'RowsPerStrip', 'PageNumber', 'PhotometricInterpretation'] + img.save(f, tiffinfo=img.tag) - for tag, value in reloaded.items(): - if tag not in ignored: - if tag.endswith('Resolution'): - val = original[tag] - assert_almost_equal(val[0][0]/val[0][1], value[0][0]/value[0][1], - msg="%s didn't roundtrip" % tag) - else: - assert_equal(original[tag], value, "%s didn't roundtrip" % tag) + loaded = Image.open(f) - for tag, value in original.items(): - if tag not in ignored: - if tag.endswith('Resolution'): - val = reloaded[tag] - assert_almost_equal(val[0][0]/val[0][1], value[0][0]/value[0][1], - msg="%s didn't roundtrip" % tag) - else: - assert_equal(value, reloaded[tag], "%s didn't roundtrip" % tag) + original = img.tag.named() + reloaded = loaded.tag.named() + # PhotometricInterpretation is set from SAVE_INFO, + # not the original image. + ignored = [ + 'StripByteCounts', 'RowsPerStrip', + 'PageNumber', 'PhotometricInterpretation'] -def test_g3_compression(): - i = Image.open('Tests/images/lena_g4_500.tif') - out = tempfile("temp.tif") - i.save(out, compression='group3') + for tag, value in reloaded.items(): + if tag not in ignored: + if tag.endswith('Resolution'): + val = original[tag] + self.assert_almost_equal( + val[0][0]/val[0][1], value[0][0]/value[0][1], + msg="%s didn't roundtrip" % tag) + else: + self.assertEqual( + original[tag], value, "%s didn't roundtrip" % tag) - reread = Image.open(out) - assert_equal(reread.info['compression'], 'group3') - assert_image_equal(reread, i) + for tag, value in original.items(): + if tag not in ignored: + if tag.endswith('Resolution'): + val = reloaded[tag] + self.assert_almost_equal( + val[0][0]/val[0][1], value[0][0]/value[0][1], + msg="%s didn't roundtrip" % tag) + else: + self.assertEqual( + value, reloaded[tag], "%s didn't roundtrip" % tag) -def test_little_endian(): - im = Image.open('Tests/images/16bit.deflate.tif') - assert_equal(im.getpixel((0,0)), 480) - assert_equal(im.mode, 'I;16') + def test_g3_compression(self): + i = Image.open('Tests/images/lena_g4_500.tif') + out = self.tempfile("temp.tif") + i.save(out, compression='group3') - b = im.tobytes() - # Bytes are in image native order (little endian) - if py3: - assert_equal(b[0], ord(b'\xe0')) - assert_equal(b[1], ord(b'\x01')) - else: - assert_equal(b[0], b'\xe0') - assert_equal(b[1], b'\x01') - + reread = Image.open(out) + self.assertEqual(reread.info['compression'], 'group3') + self.assert_image_equal(reread, i) - out = tempfile("temp.tif") - #out = "temp.le.tif" - im.save(out) - reread = Image.open(out) + def test_little_endian(self): + im = Image.open('Tests/images/16bit.deflate.tif') + self.assertEqual(im.getpixel((0, 0)), 480) + self.assertEqual(im.mode, 'I;16') - assert_equal(reread.info['compression'], im.info['compression']) - assert_equal(reread.getpixel((0,0)), 480) - # UNDONE - libtiff defaults to writing in native endian, so - # on big endian, we'll get back mode = 'I;16B' here. - -def test_big_endian(): - im = Image.open('Tests/images/16bit.MM.deflate.tif') + b = im.tobytes() + # Bytes are in image native order (little endian) + if py3: + self.assertEqual(b[0], ord(b'\xe0')) + self.assertEqual(b[1], ord(b'\x01')) + else: + self.assertEqual(b[0], b'\xe0') + self.assertEqual(b[1], b'\x01') - assert_equal(im.getpixel((0,0)), 480) - assert_equal(im.mode, 'I;16B') + out = self.tempfile("temp.tif") + # out = "temp.le.tif" + im.save(out) + reread = Image.open(out) - b = im.tobytes() + self.assertEqual(reread.info['compression'], im.info['compression']) + self.assertEqual(reread.getpixel((0, 0)), 480) + # UNDONE - libtiff defaults to writing in native endian, so + # on big endian, we'll get back mode = 'I;16B' here. - # Bytes are in image native order (big endian) - if py3: - assert_equal(b[0], ord(b'\x01')) - assert_equal(b[1], ord(b'\xe0')) - else: - assert_equal(b[0], b'\x01') - assert_equal(b[1], b'\xe0') - - out = tempfile("temp.tif") - im.save(out) - reread = Image.open(out) + def test_big_endian(self): + im = Image.open('Tests/images/16bit.MM.deflate.tif') - assert_equal(reread.info['compression'], im.info['compression']) - assert_equal(reread.getpixel((0,0)), 480) + self.assertEqual(im.getpixel((0, 0)), 480) + self.assertEqual(im.mode, 'I;16B') -def test_g4_string_info(): - """Tests String data in info directory""" - file = "Tests/images/lena_g4_500.tif" - orig = Image.open(file) - - out = tempfile("temp.tif") + b = im.tobytes() - orig.tag[269] = 'temp.tif' - orig.save(out) - - reread = Image.open(out) - assert_equal('temp.tif', reread.tag[269]) + # Bytes are in image native order (big endian) + if py3: + self.assertEqual(b[0], ord(b'\x01')) + self.assertEqual(b[1], ord(b'\xe0')) + else: + self.assertEqual(b[0], b'\x01') + self.assertEqual(b[1], b'\xe0') -def test_12bit_rawmode(): - """ Are we generating the same interpretation of the image as Imagemagick is? """ - TiffImagePlugin.READ_LIBTIFF = True - #Image.DEBUG = True - im = Image.open('Tests/images/12bit.cropped.tif') - im.load() - TiffImagePlugin.READ_LIBTIFF = False - # to make the target -- - # convert 12bit.cropped.tif -depth 16 tmp.tif - # convert tmp.tif -evaluate RightShift 4 12in16bit2.tif - # imagemagick will auto scale so that a 12bit FFF is 16bit FFF0, - # so we need to unshift so that the integer values are the same. - - im2 = Image.open('Tests/images/12in16bit.tif') + out = self.tempfile("temp.tif") + im.save(out) + reread = Image.open(out) - if Image.DEBUG: - print (im.getpixel((0,0))) - print (im.getpixel((0,1))) - print (im.getpixel((0,2))) + self.assertEqual(reread.info['compression'], im.info['compression']) + self.assertEqual(reread.getpixel((0, 0)), 480) - print (im2.getpixel((0,0))) - print (im2.getpixel((0,1))) - print (im2.getpixel((0,2))) - - assert_image_equal(im, im2) + def test_g4_string_info(self): + """Tests String data in info directory""" + file = "Tests/images/lena_g4_500.tif" + orig = Image.open(file) -def test_blur(): - # test case from irc, how to do blur on b/w image and save to compressed tif. - from PIL import ImageFilter - out = tempfile('temp.tif') - im = Image.open('Tests/images/pport_g4.tif') - im = im.convert('L') + out = self.tempfile("temp.tif") - im=im.filter(ImageFilter.GaussianBlur(4)) - im.save(out, compression='tiff_adobe_deflate') + orig.tag[269] = 'temp.tif' + orig.save(out) - im2 = Image.open(out) - im2.load() + reread = Image.open(out) + self.assertEqual('temp.tif', reread.tag[269]) - assert_image_equal(im, im2) + def test_12bit_rawmode(self): + """ Are we generating the same interpretation + of the image as Imagemagick is? """ + TiffImagePlugin.READ_LIBTIFF = True + # Image.DEBUG = True + im = Image.open('Tests/images/12bit.cropped.tif') + im.load() + TiffImagePlugin.READ_LIBTIFF = False + # to make the target -- + # convert 12bit.cropped.tif -depth 16 tmp.tif + # convert tmp.tif -evaluate RightShift 4 12in16bit2.tif + # imagemagick will auto scale so that a 12bit FFF is 16bit FFF0, + # so we need to unshift so that the integer values are the same. + im2 = Image.open('Tests/images/12in16bit.tif') -def test_compressions(): - im = lena('RGB') - out = tempfile('temp.tif') + if Image.DEBUG: + print (im.getpixel((0, 0))) + print (im.getpixel((0, 1))) + print (im.getpixel((0, 2))) + + print (im2.getpixel((0, 0))) + print (im2.getpixel((0, 1))) + print (im2.getpixel((0, 2))) + + self.assert_image_equal(im, im2) + + def test_blur(self): + # test case from irc, how to do blur on b/w image + # and save to compressed tif. + from PIL import ImageFilter + out = self.tempfile('temp.tif') + im = Image.open('Tests/images/pport_g4.tif') + im = im.convert('L') + + im = im.filter(ImageFilter.GaussianBlur(4)) + im.save(out, compression='tiff_adobe_deflate') - for compression in ('packbits', 'tiff_lzw'): - im.save(out, compression=compression) im2 = Image.open(out) - assert_image_equal(im, im2) + im2.load() - im.save(out, compression='jpeg') - im2 = Image.open(out) - assert_image_similar(im, im2, 30) - + self.assert_image_equal(im, im2) -def test_cmyk_save(): - im = lena('CMYK') - out = tempfile('temp.tif') + def test_compressions(self): + im = lena('RGB') + out = self.tempfile('temp.tif') - im.save(out, compression='tiff_adobe_deflate') - im2 = Image.open(out) - assert_image_equal(im, im2) + for compression in ('packbits', 'tiff_lzw'): + im.save(out, compression=compression) + im2 = Image.open(out) + self.assert_image_equal(im, im2) -def xtest_bw_compression_wRGB(): - """ 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') - out = tempfile('temp.tif') + im.save(out, compression='jpeg') + im2 = Image.open(out) + self.assert_image_similar(im, im2, 30) - assert_exception(IOError, lambda: im.save(out, compression='tiff_ccitt')) - assert_exception(IOError, lambda: im.save(out, compression='group3')) - assert_exception(IOError, lambda: im.save(out, compression='group4')) + def test_cmyk_save(self): + im = lena('CMYK') + out = self.tempfile('temp.tif') -def test_fp_leak(): - im = Image.open("Tests/images/lena_g4_500.tif") - fn = im.fp.fileno() + im.save(out, compression='tiff_adobe_deflate') + im2 = Image.open(out) + self.assert_image_equal(im, im2) - assert_no_exception(lambda: os.fstat(fn)) - im.load() # this should close it. - assert_exception(OSError, lambda: os.fstat(fn)) - im = None # this should force even more closed. - assert_exception(OSError, lambda: os.fstat(fn)) - assert_exception(OSError, lambda: os.close(fn)) + def xtest_bw_compression_wRGB(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') + out = self.tempfile('temp.tif') + + self.assertRaises( + IOError, lambda: im.save(out, compression='tiff_ccitt')) + self.assertRaises(IOError, lambda: im.save(out, compression='group3')) + self.assertRaises(IOError, lambda: im.save(out, compression='group4')) + + def test_fp_leak(self): + im = Image.open("Tests/images/lena_g4_500.tif") + fn = im.fp.fileno() + + os.fstat(fn) + im.load() # this should close it. + self.assertRaises(OSError, lambda: os.fstat(fn)) + im = None # this should force even more closed. + self.assertRaises(OSError, lambda: os.fstat(fn)) + self.assertRaises(OSError, lambda: os.close(fn)) + + +if __name__ == '__main__': + unittest.main() + +# End of file diff --git a/Tests/test_file_libtiff_small.py b/Tests/test_file_libtiff_small.py index 2ad71d6e6..043ecaf3f 100644 --- a/Tests/test_file_libtiff_small.py +++ b/Tests/test_file_libtiff_small.py @@ -1,52 +1,56 @@ -from tester import * +from helper import unittest from PIL import Image -from test_file_libtiff import _assert_noerr - -codecs = dir(Image.core) - -if "libtiff_encoder" not in codecs or "libtiff_decoder" not in codecs: - skip("tiff support not available") - -""" The small lena image was failing on open in the libtiff - decoder because the file pointer was set to the wrong place - by a spurious seek. It wasn't failing with the byteio method. - - It was fixed by forcing an lseek to the beginning of the - file just before reading in libtiff. These tests remain - to ensure that it stays fixed. """ +from test_file_libtiff import TestFileLibTiff -def test_g4_lena_file(): - """Testing the open file load path""" +class TestFileLibTiffSmall(TestFileLibTiff): - file = "Tests/images/lena_g4.tif" - with open(file,'rb') as f: - im = Image.open(f) + # Inherits TestFileLibTiff's setUp() and self._assert_noerr() - assert_equal(im.size, (128,128)) - _assert_noerr(im) + """ The small lena image was failing on open in the libtiff + decoder because the file pointer was set to the wrong place + by a spurious seek. It wasn't failing with the byteio method. -def test_g4_lena_bytesio(): - """Testing the bytesio loading code path""" - from io import BytesIO - file = "Tests/images/lena_g4.tif" - s = BytesIO() - with open(file,'rb') as f: - s.write(f.read()) - s.seek(0) - im = Image.open(s) + It was fixed by forcing an lseek to the beginning of the + file just before reading in libtiff. These tests remain + to ensure that it stays fixed. """ - assert_equal(im.size, (128,128)) - _assert_noerr(im) + def test_g4_lena_file(self): + """Testing the open file load path""" -def test_g4_lena(): - """The 128x128 lena image fails for some reason. Investigating""" + file = "Tests/images/lena_g4.tif" + with open(file, 'rb') as f: + im = Image.open(f) - file = "Tests/images/lena_g4.tif" - im = Image.open(file) + self.assertEqual(im.size, (128, 128)) + self._assert_noerr(im) - assert_equal(im.size, (128,128)) - _assert_noerr(im) + def test_g4_lena_bytesio(self): + """Testing the bytesio loading code path""" + from io import BytesIO + file = "Tests/images/lena_g4.tif" + s = BytesIO() + with open(file, 'rb') as f: + s.write(f.read()) + s.seek(0) + im = Image.open(s) + self.assertEqual(im.size, (128, 128)) + self._assert_noerr(im) + + def test_g4_lena(self): + """The 128x128 lena image fails for some reason. Investigating""" + + file = "Tests/images/lena_g4.tif" + im = Image.open(file) + + self.assertEqual(im.size, (128, 128)) + self._assert_noerr(im) + + +if __name__ == '__main__': + unittest.main() + +# End of file diff --git a/Tests/test_file_mpo.py b/Tests/test_file_mpo.py new file mode 100644 index 000000000..a221eec15 --- /dev/null +++ b/Tests/test_file_mpo.py @@ -0,0 +1,127 @@ +from helper import unittest, PillowTestCase +from io import BytesIO +from PIL import Image + + +test_files = ["Tests/images/sugarshack.mpo", "Tests/images/frozenpond.mpo"] + + +class TestFileMpo(PillowTestCase): + + def setUp(self): + codecs = dir(Image.core) + if "jpeg_encoder" not in codecs or "jpeg_decoder" not in codecs: + self.skipTest("jpeg support not available") + + def frame_roundtrip(self, im, **options): + # Note that for now, there is no MPO saving functionality + out = BytesIO() + im.save(out, "MPO", **options) + bytes = out.tell() + out.seek(0) + im = Image.open(out) + im.bytes = bytes # for testing only + return im + + def test_sanity(self): + for test_file in test_files: + im = Image.open(test_file) + im.load() + self.assertEqual(im.mode, "RGB") + self.assertEqual(im.size, (640, 480)) + self.assertEqual(im.format, "MPO") + + def test_app(self): + for test_file in test_files: + # Test APP/COM reader (@PIL135) + im = Image.open(test_file) + 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') + self.assertEqual(len(im.applist), 2) + + def test_exif(self): + for test_file in test_files: + im = Image.open(test_file) + info = im._getexif() + self.assertEqual(info[272], 'Nintendo 3DS') + self.assertEqual(info[296], 2) + self.assertEqual(info[34665], 188) + + def test_mp(self): + for test_file in test_files: + im = Image.open(test_file) + 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) + mpinfo = im._getmp() + frameNumber = 0 + for mpentry in mpinfo[45058]: + mpattr = mpentry['Attribute'] + if frameNumber: + self.assertFalse(mpattr['RepresentativeImageFlag']) + else: + self.assertTrue(mpattr['RepresentativeImageFlag']) + self.assertFalse(mpattr['DependentParentImageFlag']) + self.assertFalse(mpattr['DependentChildImageFlag']) + self.assertEqual(mpattr['ImageDataFormat'], 'JPEG') + self.assertEqual(mpattr['MPType'], + 'Multi-Frame Image: (Disparity)') + self.assertEqual(mpattr['Reserved'], 0) + frameNumber += 1 + + def test_seek(self): + for test_file in test_files: + im = Image.open(test_file) + self.assertEqual(im.tell(), 0) + # 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 + self.assertRaises(EOFError, im.seek, 2) + self.assertRaises(EOFError, im.seek, 523) + # bad calls shouldn't change the frame + self.assertEqual(im.tell(), 0) + # this one will work + im.seek(1) + self.assertEqual(im.tell(), 1) + # and this one, too + im.seek(0) + self.assertEqual(im.tell(), 0) + + def test_image_grab(self): + for test_file in test_files: + im = Image.open(test_file) + self.assertEqual(im.tell(), 0) + im0 = im.tobytes() + im.seek(1) + self.assertEqual(im.tell(), 1) + im1 = im.tobytes() + im.seek(0) + self.assertEqual(im.tell(), 0) + im02 = im.tobytes() + self.assertEqual(im0, im02) + self.assertNotEqual(im0, im1) + + def test_save(self): + # Note that only individual frames can be saved at present + for test_file in test_files: + im = Image.open(test_file) + self.assertEqual(im.tell(), 0) + jpg0 = self.frame_roundtrip(im) + self.assert_image_similar(im, jpg0, 30) + im.seek(1) + self.assertEqual(im.tell(), 1) + jpg1 = self.frame_roundtrip(im) + self.assert_image_similar(im, jpg1, 30) + + +if __name__ == '__main__': + unittest.main() + +# End of file diff --git a/Tests/test_file_msp.py b/Tests/test_file_msp.py index 7ed200e43..a64faad10 100644 --- a/Tests/test_file_msp.py +++ b/Tests/test_file_msp.py @@ -1,15 +1,24 @@ -from tester import * +from helper import unittest, PillowTestCase, lena from PIL import Image -def test_sanity(): - file = tempfile("temp.msp") +class TestFileMsp(PillowTestCase): - lena("1").save(file) + def test_sanity(self): - im = Image.open(file) - im.load() - assert_equal(im.mode, "1") - assert_equal(im.size, (128, 128)) - assert_equal(im.format, "MSP") + file = self.tempfile("temp.msp") + + lena("1").save(file) + + im = Image.open(file) + im.load() + self.assertEqual(im.mode, "1") + self.assertEqual(im.size, (128, 128)) + self.assertEqual(im.format, "MSP") + + +if __name__ == '__main__': + unittest.main() + +# End of file diff --git a/Tests/test_file_palm.py b/Tests/test_file_palm.py new file mode 100644 index 000000000..388df0237 --- /dev/null +++ b/Tests/test_file_palm.py @@ -0,0 +1,61 @@ +from helper import unittest, PillowTestCase, lena, imagemagick_available + +import os.path + + +class TestFilePalm(PillowTestCase): + _roundtrip = imagemagick_available() + + def helper_save_as_palm(self, mode): + # Arrange + im = lena(mode) + outfile = self.tempfile("temp_" + mode + ".palm") + + # Act + im.save(outfile) + + # Assert + self.assertTrue(os.path.isfile(outfile)) + self.assertGreater(os.path.getsize(outfile), 0) + + def roundtrip(self, mode): + if not self._roundtrip: + return + + im = lena(mode) + outfile = self.tempfile("temp.palm") + + im.save(outfile) + converted = self.open_withImagemagick(outfile) + self.assert_image_equal(converted, im) + + + def test_monochrome(self): + # Arrange + mode = "1" + + # Act / Assert + self.helper_save_as_palm(mode) + self.roundtrip(mode) + + def test_p_mode(self): + # Arrange + mode = "P" + + # Act / Assert + self.helper_save_as_palm(mode) + self.skipKnownBadTest("Palm P image is wrong") + self.roundtrip(mode) + + def test_rgb_ioerror(self): + # Arrange + mode = "RGB" + + # Act / Assert + self.assertRaises(IOError, lambda: self.helper_save_as_palm(mode)) + + +if __name__ == '__main__': + unittest.main() + +# End of file diff --git a/Tests/test_file_pcx.py b/Tests/test_file_pcx.py index cb785ec54..f278bd91d 100644 --- a/Tests/test_file_pcx.py +++ b/Tests/test_file_pcx.py @@ -1,40 +1,47 @@ -from tester import * +from helper import unittest, PillowTestCase, lena from PIL import Image -def _roundtrip(im): - f = tempfile("temp.pcx") - im.save(f) - im2 = Image.open(f) +class TestFilePcx(PillowTestCase): - assert_equal(im2.mode, im.mode) - assert_equal(im2.size, im.size) - assert_equal(im2.format, "PCX") - assert_image_equal(im2, im) - -def test_sanity(): - for mode in ('1', 'L', 'P', 'RGB'): - _roundtrip(lena(mode)) + def _roundtrip(self, im): + f = self.tempfile("temp.pcx") + im.save(f) + im2 = Image.open(f) -def test_odd(): - # see issue #523, odd sized images should have a stride that's even. - # not that imagemagick or gimp write pcx that way. - # we were not handling properly. - for mode in ('1', 'L', 'P', 'RGB'): - # larger, odd sized images are better here to ensure that - # we handle interrupted scan lines properly. - _roundtrip(lena(mode).resize((511,511))) - + self.assertEqual(im2.mode, im.mode) + self.assertEqual(im2.size, im.size) + self.assertEqual(im2.format, "PCX") + self.assert_image_equal(im2, im) -def test_pil184(): - # Check reading of files where xmin/xmax is not zero. + def test_sanity(self): + for mode in ('1', 'L', 'P', 'RGB'): + self._roundtrip(lena(mode)) - file = "Tests/images/pil184.pcx" - im = Image.open(file) + def test_odd(self): + # see issue #523, odd sized images should have a stride that's even. + # not that imagemagick or gimp write pcx that way. + # we were not handling properly. + 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))) - assert_equal(im.size, (447, 144)) - assert_equal(im.tile[0][1], (0, 0, 447, 144)) + def test_pil184(self): + # Check reading of files where xmin/xmax is not zero. - # Make sure all pixels are either 0 or 255. - assert_equal(im.histogram()[0] + im.histogram()[255], 447*144) + file = "Tests/images/pil184.pcx" + im = Image.open(file) + + self.assertEqual(im.size, (447, 144)) + self.assertEqual(im.tile[0][1], (0, 0, 447, 144)) + + # Make sure all pixels are either 0 or 255. + self.assertEqual(im.histogram()[0] + im.histogram()[255], 447*144) + + +if __name__ == '__main__': + unittest.main() + +# End of file diff --git a/Tests/test_file_pdf.py b/Tests/test_file_pdf.py index e99f22db1..689302bb5 100644 --- a/Tests/test_file_pdf.py +++ b/Tests/test_file_pdf.py @@ -1,58 +1,59 @@ -from tester import * +from helper import unittest, PillowTestCase, lena + import os.path -def helper_save_as_pdf(mode): - # Arrange - im = lena(mode) - outfile = tempfile("temp_" + mode + ".pdf") +class TestFilePdf(PillowTestCase): - # Act - im.save(outfile) + def helper_save_as_pdf(self, mode): + # Arrange + im = lena(mode) + outfile = self.tempfile("temp_" + mode + ".pdf") - # Assert - assert_true(os.path.isfile(outfile)) - assert_greater(os.path.getsize(outfile), 0) + # Act + im.save(outfile) + + # Assert + self.assertTrue(os.path.isfile(outfile)) + self.assertGreater(os.path.getsize(outfile), 0) + + def test_monochrome(self): + # Arrange + mode = "1" + + # Act / Assert + self.helper_save_as_pdf(mode) + + def test_greyscale(self): + # Arrange + mode = "L" + + # Act / Assert + self.helper_save_as_pdf(mode) + + def test_rgb(self): + # Arrange + mode = "RGB" + + # Act / Assert + self.helper_save_as_pdf(mode) + + def test_p_mode(self): + # Arrange + mode = "P" + + # Act / Assert + self.helper_save_as_pdf(mode) + + def test_cmyk_mode(self): + # Arrange + mode = "CMYK" + + # Act / Assert + self.helper_save_as_pdf(mode) -def test_monochrome(): - # Arrange - mode = "1" - - # Act / Assert - helper_save_as_pdf(mode) - - -def test_greyscale(): - # Arrange - mode = "L" - - # Act / Assert - helper_save_as_pdf(mode) - - -def test_rgb(): - # Arrange - mode = "RGB" - - # Act / Assert - helper_save_as_pdf(mode) - - -def test_p_mode(): - # Arrange - mode = "P" - - # Act / Assert - helper_save_as_pdf(mode) - - -def test_cmyk_mode(): - # Arrange - mode = "CMYK" - - # Act / Assert - helper_save_as_pdf(mode) - +if __name__ == '__main__': + unittest.main() # End of file diff --git a/Tests/test_file_png.py b/Tests/test_file_png.py index c17829194..8ef166347 100644 --- a/Tests/test_file_png.py +++ b/Tests/test_file_png.py @@ -1,4 +1,6 @@ -from tester import * +from helper import unittest, PillowTestCase, lena + +from io import BytesIO from PIL import Image from PIL import PngImagePlugin @@ -6,18 +8,16 @@ import zlib codecs = dir(Image.core) -if "zip_encoder" not in codecs or "zip_decoder" not in codecs: - skip("zip/deflate support not available") - # sample png stream -file = "Images/lena.png" +file = "Tests/images/lena.png" data = open(file, "rb").read() # stuff to create inline PNG images MAGIC = PngImagePlugin._MAGIC + def chunk(cid, *data): file = BytesIO() PngImagePlugin.putchunk(*(file, cid) + data) @@ -32,256 +32,345 @@ IEND = chunk(b"IEND") HEAD = MAGIC + IHDR TAIL = IDAT + IEND + def load(data): return Image.open(BytesIO(data)) + def roundtrip(im, **options): out = BytesIO() im.save(out, "PNG", **options) out.seek(0) return Image.open(out) -# -------------------------------------------------------------------- -def test_sanity(): +class TestFilePng(PillowTestCase): - # internal version number - assert_match(Image.core.zlib_version, "\d+\.\d+\.\d+(\.\d+)?$") + def setUp(self): + if "zip_encoder" not in codecs or "zip_decoder" not in codecs: + self.skipTest("zip/deflate support not available") - file = tempfile("temp.png") + def test_sanity(self): - lena("RGB").save(file) + # internal version number + self.assertRegexpMatches( + Image.core.zlib_version, "\d+\.\d+\.\d+(\.\d+)?$") - im = Image.open(file) - im.load() - assert_equal(im.mode, "RGB") - assert_equal(im.size, (128, 128)) - assert_equal(im.format, "PNG") + file = self.tempfile("temp.png") - lena("1").save(file) - im = Image.open(file) + lena("RGB").save(file) - lena("L").save(file) - im = Image.open(file) + im = Image.open(file) + im.load() + self.assertEqual(im.mode, "RGB") + self.assertEqual(im.size, (128, 128)) + self.assertEqual(im.format, "PNG") - lena("P").save(file) - im = Image.open(file) + lena("1").save(file) + im = Image.open(file) - lena("RGB").save(file) - im = Image.open(file) + lena("L").save(file) + im = Image.open(file) - lena("I").save(file) - im = Image.open(file) + lena("P").save(file) + im = Image.open(file) -# -------------------------------------------------------------------- + lena("RGB").save(file) + im = Image.open(file) -def test_broken(): - # Check reading of totally broken files. In this case, the test - # file was checked into Subversion as a text file. + lena("I").save(file) + im = Image.open(file) - file = "Tests/images/broken.png" - assert_exception(IOError, lambda: Image.open(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. -def test_bad_text(): - # Make sure PIL can read malformed tEXt chunks (@PIL152) + file = "Tests/images/broken.png" + self.assertRaises(IOError, lambda: Image.open(file)) - im = load(HEAD + chunk(b'tEXt') + TAIL) - assert_equal(im.info, {}) + def test_bad_text(self): + # Make sure PIL can read malformed tEXt chunks (@PIL152) - im = load(HEAD + chunk(b'tEXt', b'spam') + TAIL) - assert_equal(im.info, {'spam': ''}) + im = load(HEAD + chunk(b'tEXt') + TAIL) + self.assertEqual(im.info, {}) - im = load(HEAD + chunk(b'tEXt', b'spam\0') + TAIL) - assert_equal(im.info, {'spam': ''}) + im = load(HEAD + chunk(b'tEXt', b'spam') + TAIL) + self.assertEqual(im.info, {'spam': ''}) - im = load(HEAD + chunk(b'tEXt', b'spam\0egg') + TAIL) - assert_equal(im.info, {'spam': 'egg'}) + im = load(HEAD + chunk(b'tEXt', b'spam\0') + TAIL) + self.assertEqual(im.info, {'spam': ''}) - im = load(HEAD + chunk(b'tEXt', b'spam\0egg\0') + TAIL) - assert_equal(im.info, {'spam': 'egg\x00'}) + im = load(HEAD + chunk(b'tEXt', b'spam\0egg') + TAIL) + self.assertEqual(im.info, {'spam': 'egg'}) -def test_bad_ztxt(): - # Test reading malformed zTXt chunks (python-imaging/Pillow#318) + im = load(HEAD + chunk(b'tEXt', b'spam\0egg\0') + TAIL) + self.assertEqual(im.info, {'spam': 'egg\x00'}) - im = load(HEAD + chunk(b'zTXt') + TAIL) - assert_equal(im.info, {}) + def test_bad_ztxt(self): + # Test reading malformed zTXt chunks (python-pillow/Pillow#318) - im = load(HEAD + chunk(b'zTXt', b'spam') + TAIL) - assert_equal(im.info, {'spam': ''}) + im = load(HEAD + chunk(b'zTXt') + TAIL) + self.assertEqual(im.info, {}) - im = load(HEAD + chunk(b'zTXt', b'spam\0') + TAIL) - assert_equal(im.info, {'spam': ''}) + im = load(HEAD + chunk(b'zTXt', b'spam') + TAIL) + self.assertEqual(im.info, {'spam': ''}) - im = load(HEAD + chunk(b'zTXt', b'spam\0\0') + TAIL) - assert_equal(im.info, {'spam': ''}) + im = load(HEAD + chunk(b'zTXt', b'spam\0') + TAIL) + self.assertEqual(im.info, {'spam': ''}) - im = load(HEAD + chunk(b'zTXt', b'spam\0\0' + zlib.compress(b'egg')[:1]) + TAIL) - assert_equal(im.info, {'spam': ''}) + im = load(HEAD + chunk(b'zTXt', b'spam\0\0') + TAIL) + self.assertEqual(im.info, {'spam': ''}) - im = load(HEAD + chunk(b'zTXt', b'spam\0\0' + zlib.compress(b'egg')) + TAIL) - assert_equal(im.info, {'spam': 'egg'}) + im = load(HEAD + chunk( + b'zTXt', b'spam\0\0' + zlib.compress(b'egg')[:1]) + TAIL) + self.assertEqual(im.info, {'spam': ''}) -def test_interlace(): + im = load( + HEAD + chunk(b'zTXt', b'spam\0\0' + zlib.compress(b'egg')) + TAIL) + self.assertEqual(im.info, {'spam': 'egg'}) - file = "Tests/images/pil123p.png" - im = Image.open(file) + def test_bad_itxt(self): - assert_image(im, "P", (162, 150)) - assert_true(im.info.get("interlace")) + im = load(HEAD + chunk(b'iTXt') + TAIL) + self.assertEqual(im.info, {}) - assert_no_exception(lambda: im.load()) + im = load(HEAD + chunk(b'iTXt', b'spam') + TAIL) + self.assertEqual(im.info, {}) - file = "Tests/images/pil123rgba.png" - im = Image.open(file) + im = load(HEAD + chunk(b'iTXt', b'spam\0') + TAIL) + self.assertEqual(im.info, {}) - assert_image(im, "RGBA", (162, 150)) - assert_true(im.info.get("interlace")) + im = load(HEAD + chunk(b'iTXt', b'spam\0\x02') + TAIL) + self.assertEqual(im.info, {}) - assert_no_exception(lambda: im.load()) + im = load(HEAD + chunk(b'iTXt', b'spam\0\0\0foo\0') + TAIL) + self.assertEqual(im.info, {}) -def test_load_transparent_p(): - file = "Tests/images/pil123p.png" - im = Image.open(file) + im = load(HEAD + chunk(b'iTXt', b'spam\0\0\0en\0Spam\0egg') + TAIL) + self.assertEqual(im.info, {"spam": "egg"}) + self.assertEqual(im.info["spam"].lang, "en") + self.assertEqual(im.info["spam"].tkey, "Spam") - assert_image(im, "P", (162, 150)) - im = im.convert("RGBA") - assert_image(im, "RGBA", (162, 150)) + im = load(HEAD + chunk(b'iTXt', b'spam\0\1\0en\0Spam\0' + zlib.compress(b"egg")[:1]) + TAIL) + self.assertEqual(im.info, {}) - # image has 124 uniqe qlpha values - assert_equal(len(im.split()[3].getcolors()), 124) + im = load(HEAD + chunk(b'iTXt', b'spam\0\1\1en\0Spam\0' + zlib.compress(b"egg")) + TAIL) + self.assertEqual(im.info, {}) -def test_load_transparent_rgb(): - file = "Tests/images/rgb_trns.png" - im = Image.open(file) + 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") - assert_image(im, "RGB", (64, 64)) - im = im.convert("RGBA") - assert_image(im, "RGBA", (64, 64)) + def test_interlace(self): - # image has 876 transparent pixels - assert_equal(im.split()[3].getcolors()[0][0], 876) + file = "Tests/images/pil123p.png" + im = Image.open(file) -def test_save_p_transparent_palette(): - in_file = "Tests/images/pil123p.png" - im = Image.open(in_file) + self.assert_image(im, "P", (162, 150)) + self.assertTrue(im.info.get("interlace")) - file = tempfile("temp.png") - assert_no_exception(lambda: im.save(file)) + im.load() -def test_save_p_single_transparency(): - in_file = "Tests/images/p_trns_single.png" - im = Image.open(in_file) + file = "Tests/images/pil123rgba.png" + im = Image.open(file) - file = tempfile("temp.png") - assert_no_exception(lambda: im.save(file)) + self.assert_image(im, "RGBA", (162, 150)) + self.assertTrue(im.info.get("interlace")) -def test_save_l_transparency(): - in_file = "Tests/images/l_trns.png" - im = Image.open(in_file) + im.load() - file = tempfile("temp.png") - assert_no_exception(lambda: im.save(file)) + def test_load_transparent_p(self): + file = "Tests/images/pil123p.png" + im = Image.open(file) - # There are 559 transparent pixels. - im = im.convert('RGBA') - assert_equal(im.split()[3].getcolors()[0][0], 559) + self.assert_image(im, "P", (162, 150)) + im = im.convert("RGBA") + self.assert_image(im, "RGBA", (162, 150)) -def test_save_rgb_single_transparency(): - in_file = "Tests/images/caption_6_33_22.png" - im = Image.open(in_file) + # image has 124 uniqe qlpha values + self.assertEqual(len(im.split()[3].getcolors()), 124) - file = tempfile("temp.png") - assert_no_exception(lambda: im.save(file)) + def test_load_transparent_rgb(self): + file = "Tests/images/rgb_trns.png" + im = Image.open(file) -def test_load_verify(): - # Check open/load/verify exception (@PIL150) + self.assert_image(im, "RGB", (64, 64)) + im = im.convert("RGBA") + self.assert_image(im, "RGBA", (64, 64)) - im = Image.open("Images/lena.png") - assert_no_exception(lambda: im.verify()) + # image has 876 transparent pixels + self.assertEqual(im.split()[3].getcolors()[0][0], 876) - im = Image.open("Images/lena.png") - im.load() - assert_exception(RuntimeError, lambda: im.verify()) + def test_save_p_transparent_palette(self): + in_file = "Tests/images/pil123p.png" + im = Image.open(in_file) -def test_roundtrip_dpi(): - # Check dpi roundtripping + file = self.tempfile("temp.png") + im.save(file) - im = Image.open(file) + def test_save_p_single_transparency(self): + in_file = "Tests/images/p_trns_single.png" + im = Image.open(in_file) - im = roundtrip(im, dpi=(100, 100)) - assert_equal(im.info["dpi"], (100, 100)) + file = self.tempfile("temp.png") + im.save(file) -def test_roundtrip_text(): - # Check text roundtripping + def test_save_l_transparency(self): + in_file = "Tests/images/l_trns.png" + im = Image.open(in_file) - im = Image.open(file) + file = self.tempfile("temp.png") + im.save(file) - info = PngImagePlugin.PngInfo() - info.add_text("TXT", "VALUE") - info.add_text("ZIP", "VALUE", 1) + # There are 559 transparent pixels. + im = im.convert('RGBA') + self.assertEqual(im.split()[3].getcolors()[0][0], 559) - im = roundtrip(im, pnginfo=info) - assert_equal(im.info, {'TXT': 'VALUE', 'ZIP': 'VALUE'}) - assert_equal(im.text, {'TXT': 'VALUE', 'ZIP': 'VALUE'}) + def test_save_rgb_single_transparency(self): + in_file = "Tests/images/caption_6_33_22.png" + im = Image.open(in_file) -def test_scary(): - # Check reading of evil PNG file. For information, see: - # http://scary.beasts.org/security/CESA-2004-001.txt - # The first byte is removed from pngtest_bad.png - # to avoid classification as malware. + file = self.tempfile("temp.png") + im.save(file) - with open("Tests/images/pngtest_bad.png.bin", 'rb') as fd: - data = b'\x89' + fd.read() + def test_load_verify(self): + # Check open/load/verify exception (@PIL150) - pngfile = BytesIO(data) - assert_exception(IOError, lambda: Image.open(pngfile)) + im = Image.open("Tests/images/lena.png") + im.verify() -def test_trns_rgb(): - # Check writing and reading of tRNS chunks for RGB images. - # Independent file sample provided by Sebastian Spaeth. + im = Image.open("Tests/images/lena.png") + im.load() + self.assertRaises(RuntimeError, lambda: im.verify()) - file = "Tests/images/caption_6_33_22.png" - im = Image.open(file) - assert_equal(im.info["transparency"], (248, 248, 248)) + def test_roundtrip_dpi(self): + # Check dpi roundtripping - # check saving transparency by default - im = roundtrip(im) - assert_equal(im.info["transparency"], (248, 248, 248)) + im = Image.open(file) - im = roundtrip(im, transparency=(0, 1, 2)) - assert_equal(im.info["transparency"], (0, 1, 2)) + im = roundtrip(im, dpi=(100, 100)) + self.assertEqual(im.info["dpi"], (100, 100)) -def test_trns_p(): - # Check writing a transparency of 0, issue #528 - im = lena('P') - im.info['transparency']=0 - - f = tempfile("temp.png") - im.save(f) + def test_roundtrip_text(self): + # Check text roundtripping - im2 = Image.open(f) - assert_true('transparency' in im2.info) + im = Image.open(file) - assert_image_equal(im2.convert('RGBA'), im.convert('RGBA')) - - -def test_save_icc_profile_none(): - # check saving files with an ICC profile set to None (omit profile) - in_file = "Tests/images/icc_profile_none.png" - im = Image.open(in_file) - assert_equal(im.info['icc_profile'], None) + info = PngImagePlugin.PngInfo() + info.add_text("TXT", "VALUE") + info.add_text("ZIP", "VALUE", 1) - im = roundtrip(im) - assert_false('icc_profile' in im.info) + im = roundtrip(im, pnginfo=info) + self.assertEqual(im.info, {'TXT': 'VALUE', 'ZIP': 'VALUE'}) + self.assertEqual(im.text, {'TXT': 'VALUE', 'ZIP': 'VALUE'}) -def test_roundtrip_icc_profile(): - # check that we can roundtrip the icc profile - im = lena('RGB') + def test_roundtrip_itxt(self): + # Check iTXt roundtripping - jpeg_image = Image.open('Tests/images/flower2.jpg') - expected_icc = jpeg_image.info['icc_profile'] + 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) - im.info['icc_profile'] = expected_icc - im = roundtrip(im) - assert_equal(im.info['icc_profile'], expected_icc) + im = roundtrip(im, pnginfo=info) + self.assertEqual(im.info, {"spam": "Eggs", "eggs": "Spam"}) + self.assertEqual(im.text, {"spam": "Eggs", "eggs": "Spam"}) + self.assertEqual(im.text["spam"].lang, "en") + self.assertEqual(im.text["spam"].tkey, "Spam") + self.assertEqual(im.text["eggs"].lang, "en") + self.assertEqual(im.text["eggs"].tkey, "Eggs") + def test_nonunicode_text(self): + # Check so that non-Unicode text is saved as a tEXt rather than iTXt + + im = Image.new("RGB", (32, 32)) + info = PngImagePlugin.PngInfo() + info.add_text("Text", "Ascii") + im = roundtrip(im, pnginfo=info) + self.assertEqual(type(im.info["Text"]), str) + + def test_unicode_text(self): + # Check preservation of non-ASCII characters on Python3 + # This cannot really be meaningfully tested on Python2, + # since it didn't preserve charsets to begin with. + + def rt_text(value): + im = Image.new("RGB", (32, 32)) + info = PngImagePlugin.PngInfo() + info.add_text("Text", value) + im = roundtrip(im, pnginfo=info) + 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 + chr(0x9fba) + chr(0x3042) + chr(0xac00)) + rt_text("A" + chr(0xc4) + chr(0x472) + chr(0x3042)) # Combined + + def test_scary(self): + # Check reading of evil PNG file. For information, see: + # http://scary.beasts.org/security/CESA-2004-001.txt + # The first byte is removed from pngtest_bad.png + # to avoid classification as malware. + + with open("Tests/images/pngtest_bad.png.bin", 'rb') as fd: + data = b'\x89' + fd.read() + + pngfile = BytesIO(data) + self.assertRaises(IOError, lambda: Image.open(pngfile)) + + def test_trns_rgb(self): + # 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) + self.assertEqual(im.info["transparency"], (248, 248, 248)) + + # check saving transparency by default + im = roundtrip(im) + self.assertEqual(im.info["transparency"], (248, 248, 248)) + + im = roundtrip(im, transparency=(0, 1, 2)) + self.assertEqual(im.info["transparency"], (0, 1, 2)) + + def test_trns_p(self): + # Check writing a transparency of 0, issue #528 + im = lena('P') + im.info['transparency'] = 0 + + f = self.tempfile("temp.png") + im.save(f) + + im2 = Image.open(f) + self.assertIn('transparency', im2.info) + + self.assert_image_equal(im2.convert('RGBA'), im.convert('RGBA')) + + def test_save_icc_profile_none(self): + # check saving files with an ICC profile set to None (omit profile) + in_file = "Tests/images/icc_profile_none.png" + im = Image.open(in_file) + self.assertEqual(im.info['icc_profile'], None) + + im = roundtrip(im) + self.assertNotIn('icc_profile', im.info) + + def test_roundtrip_icc_profile(self): + # check that we can roundtrip the icc profile + im = lena('RGB') + + jpeg_image = Image.open('Tests/images/flower2.jpg') + expected_icc = jpeg_image.info['icc_profile'] + + im.info['icc_profile'] = expected_icc + im = roundtrip(im) + self.assertEqual(im.info['icc_profile'], expected_icc) + + +if __name__ == '__main__': + unittest.main() + +# End of file diff --git a/Tests/test_file_ppm.py b/Tests/test_file_ppm.py index 34136a83c..e1f1537d2 100644 --- a/Tests/test_file_ppm.py +++ b/Tests/test_file_ppm.py @@ -1,36 +1,42 @@ -from tester import * +from helper import unittest, PillowTestCase from PIL import Image # sample ppm stream -file = "Images/lena.ppm" +file = "Tests/images/lena.ppm" data = open(file, "rb").read() -def test_sanity(): - im = Image.open(file) - im.load() - assert_equal(im.mode, "RGB") - assert_equal(im.size, (128, 128)) - assert_equal(im.format, "PPM") -def test_16bit_pgm(): - im = Image.open('Tests/images/16_bit_binary.pgm') - im.load() - assert_equal(im.mode, 'I') - assert_equal(im.size, (20,100)) +class TestFilePpm(PillowTestCase): - tgt = Image.open('Tests/images/16_bit_binary_pgm.png') - assert_image_equal(im, tgt) + def test_sanity(self): + im = Image.open(file) + im.load() + self.assertEqual(im.mode, "RGB") + self.assertEqual(im.size, (128, 128)) + self.assertEqual(im.format, "PPM") + + def test_16bit_pgm(self): + im = Image.open('Tests/images/16_bit_binary.pgm') + im.load() + self.assertEqual(im.mode, 'I') + self.assertEqual(im.size, (20, 100)) + + tgt = Image.open('Tests/images/16_bit_binary_pgm.png') + self.assert_image_equal(im, tgt) + + def test_16bit_pgm_write(self): + im = Image.open('Tests/images/16_bit_binary.pgm') + im.load() + + f = self.tempfile('temp.pgm') + im.save(f, 'PPM') + + reloaded = Image.open(f) + self.assert_image_equal(im, reloaded) -def test_16bit_pgm_write(): - im = Image.open('Tests/images/16_bit_binary.pgm') - im.load() - - f = tempfile('temp.pgm') - assert_no_exception(lambda: im.save(f, 'PPM')) - - reloaded = Image.open(f) - assert_image_equal(im, reloaded) - +if __name__ == '__main__': + unittest.main() +# End of file diff --git a/Tests/test_file_psd.py b/Tests/test_file_psd.py index ef2d40594..ee903ce5c 100644 --- a/Tests/test_file_psd.py +++ b/Tests/test_file_psd.py @@ -1,14 +1,23 @@ -from tester import * +from helper import unittest, PillowTestCase from PIL import Image # sample ppm stream -file = "Images/lena.psd" +file = "Tests/images/lena.psd" data = open(file, "rb").read() -def test_sanity(): - im = Image.open(file) - im.load() - assert_equal(im.mode, "RGB") - assert_equal(im.size, (128, 128)) - assert_equal(im.format, "PSD") + +class TestImagePsd(PillowTestCase): + + def test_sanity(self): + im = Image.open(file) + im.load() + self.assertEqual(im.mode, "RGB") + self.assertEqual(im.size, (128, 128)) + self.assertEqual(im.format, "PSD") + + +if __name__ == '__main__': + unittest.main() + +# End of file diff --git a/Tests/test_file_sgi.py b/Tests/test_file_sgi.py new file mode 100644 index 000000000..84b184b63 --- /dev/null +++ b/Tests/test_file_sgi.py @@ -0,0 +1,39 @@ +from helper import unittest, PillowTestCase + +from PIL import Image + + +class TestFileSgi(PillowTestCase): + + def test_rgb(self): + # Arrange + # Created with ImageMagick then renamed: + # convert lena.ppm lena.sgi + test_file = "Tests/images/lena.rgb" + + # Act / Assert + self.assertRaises(ValueError, lambda: Image.open(test_file)) + + def test_l(self): + # Arrange + # Created with ImageMagick then renamed: + # convert lena.ppm -monochrome lena.sgi + test_file = "Tests/images/lena.bw" + + # Act / Assert + self.assertRaises(ValueError, lambda: Image.open(test_file)) + + def test_rgba(self): + # Arrange + # Created with ImageMagick: + # convert transparent.png transparent.sgi + test_file = "Tests/images/transparent.sgi" + + # Act / Assert + self.assertRaises(ValueError, lambda: Image.open(test_file)) + + +if __name__ == '__main__': + unittest.main() + +# End of file diff --git a/Tests/test_file_spider.py b/Tests/test_file_spider.py index d93724492..65e4d2782 100644 --- a/Tests/test_file_spider.py +++ b/Tests/test_file_spider.py @@ -1,36 +1,82 @@ -from tester import * +from helper import unittest, PillowTestCase, lena from PIL import Image from PIL import SpiderImagePlugin -test_file = "Tests/images/lena.spider" +TEST_FILE = "Tests/images/lena.spider" -def test_sanity(): - im = Image.open(test_file) - im.load() - assert_equal(im.mode, "F") - assert_equal(im.size, (128, 128)) - assert_equal(im.format, "SPIDER") +class TestImageSpider(PillowTestCase): + + def test_sanity(self): + im = Image.open(TEST_FILE) + im.load() + self.assertEqual(im.mode, "F") + self.assertEqual(im.size, (128, 128)) + self.assertEqual(im.format, "SPIDER") + + def test_save(self): + # Arrange + temp = self.tempfile('temp.spider') + im = lena() + + # Act + im.save(temp, "SPIDER") + + # Assert + im2 = Image.open(temp) + self.assertEqual(im2.mode, "F") + self.assertEqual(im2.size, (128, 128)) + self.assertEqual(im2.format, "SPIDER") + + def test_isSpiderImage(self): + self.assertTrue(SpiderImagePlugin.isSpiderImage(TEST_FILE)) + + def test_tell(self): + # Arrange + im = Image.open(TEST_FILE) + + # Act + index = im.tell() + + # Assert + self.assertEqual(index, 0) + + def test_loadImageSeries(self): + # Arrange + not_spider_file = "Tests/images/lena.ppm" + file_list = [TEST_FILE, not_spider_file, "path/not_found.ext"] + + # Act + img_list = SpiderImagePlugin.loadImageSeries(file_list) + + # Assert + self.assertEqual(len(img_list), 1) + self.assertIsInstance(img_list[0], Image.Image) + self.assertEqual(img_list[0].size, (128, 128)) + + def test_loadImageSeries_no_input(self): + # Arrange + file_list = None + + # Act + img_list = SpiderImagePlugin.loadImageSeries(file_list) + + # Assert + self.assertEqual(img_list, None) + + def test_isInt_not_a_number(self): + # Arrange + not_a_number = "a" + + # Act + ret = SpiderImagePlugin.isInt(not_a_number) + + # Assert + self.assertEqual(ret, 0) -def test_save(): - # Arrange - temp = tempfile('temp.spider') - im = lena() - - # Act - im.save(temp, "SPIDER") - - # Assert - im2 = Image.open(temp) - assert_equal(im2.mode, "F") - assert_equal(im2.size, (128, 128)) - assert_equal(im2.format, "SPIDER") - - -def test_isSpiderImage(): - assert_true(SpiderImagePlugin.isSpiderImage(test_file)) - +if __name__ == '__main__': + unittest.main() # End of file diff --git a/Tests/test_file_sun.py b/Tests/test_file_sun.py new file mode 100644 index 000000000..c52564f91 --- /dev/null +++ b/Tests/test_file_sun.py @@ -0,0 +1,23 @@ +from helper import unittest, PillowTestCase + +from PIL import Image + + +class TestFileSun(PillowTestCase): + + def test_sanity(self): + # Arrange + # Created with ImageMagick: convert lena.ppm lena.ras + test_file = "Tests/images/lena.ras" + + # Act + im = Image.open(test_file) + + # Assert + self.assertEqual(im.size, (128, 128)) + + +if __name__ == '__main__': + unittest.main() + +# End of file diff --git a/Tests/test_file_tar.py b/Tests/test_file_tar.py index fa33d3802..7010973ce 100644 --- a/Tests/test_file_tar.py +++ b/Tests/test_file_tar.py @@ -1,28 +1,38 @@ -from tester import * +from helper import unittest, PillowTestCase from PIL import Image, TarIO codecs = dir(Image.core) -if "zip_decoder" not in codecs and "jpeg_decoder" not in codecs: - skip("neither jpeg nor zip support not available") # sample ppm stream -tarfile = "Images/lena.tar" +tarfile = "Tests/images/lena.tar" -def test_sanity(): - if "zip_decoder" in codecs: - tar = TarIO.TarIO(tarfile, 'lena.png') - im = Image.open(tar) - im.load() - assert_equal(im.mode, "RGB") - assert_equal(im.size, (128, 128)) - assert_equal(im.format, "PNG") - if "jpeg_decoder" in codecs: - tar = TarIO.TarIO(tarfile, 'lena.jpg') - im = Image.open(tar) - im.load() - assert_equal(im.mode, "RGB") - assert_equal(im.size, (128, 128)) - assert_equal(im.format, "JPEG") +class TestFileTar(PillowTestCase): + def setUp(self): + if "zip_decoder" not in codecs and "jpeg_decoder" not in codecs: + self.skipTest("neither jpeg nor zip support not available") + + def test_sanity(self): + if "zip_decoder" in codecs: + tar = TarIO.TarIO(tarfile, 'lena.png') + im = Image.open(tar) + im.load() + self.assertEqual(im.mode, "RGB") + self.assertEqual(im.size, (128, 128)) + self.assertEqual(im.format, "PNG") + + if "jpeg_decoder" in codecs: + tar = TarIO.TarIO(tarfile, 'lena.jpg') + im = Image.open(tar) + im.load() + self.assertEqual(im.mode, "RGB") + self.assertEqual(im.size, (128, 128)) + self.assertEqual(im.format, "JPEG") + + +if __name__ == '__main__': + unittest.main() + +# End of file diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py index 804ae04e4..9c832c206 100644 --- a/Tests/test_file_tiff.py +++ b/Tests/test_file_tiff.py @@ -1,141 +1,279 @@ -from tester import * +from helper import unittest, PillowTestCase, lena, py3 from PIL import Image -def test_sanity(): - file = tempfile("temp.tif") +class TestFileTiff(PillowTestCase): - lena("RGB").save(file) + def test_sanity(self): - im = Image.open(file) - im.load() - assert_equal(im.mode, "RGB") - assert_equal(im.size, (128, 128)) - assert_equal(im.format, "TIFF") + file = self.tempfile("temp.tif") - lena("1").save(file) - im = Image.open(file) + lena("RGB").save(file) - lena("L").save(file) - im = Image.open(file) + im = Image.open(file) + im.load() + self.assertEqual(im.mode, "RGB") + self.assertEqual(im.size, (128, 128)) + self.assertEqual(im.format, "TIFF") - lena("P").save(file) - im = Image.open(file) + lena("1").save(file) + im = Image.open(file) - lena("RGB").save(file) - im = Image.open(file) + lena("L").save(file) + im = Image.open(file) - lena("I").save(file) - im = Image.open(file) + lena("P").save(file) + im = Image.open(file) -def test_mac_tiff(): - # Read RGBa images from Mac OS X [@PIL136] + lena("RGB").save(file) + im = Image.open(file) - file = "Tests/images/pil136.tiff" - im = Image.open(file) + lena("I").save(file) + im = Image.open(file) - assert_equal(im.mode, "RGBA") - assert_equal(im.size, (55, 43)) - assert_equal(im.tile, [('raw', (0, 0, 55, 43), 8, ('RGBa', 0, 1))]) - assert_no_exception(lambda: im.load()) + def test_mac_tiff(self): + # Read RGBa images from Mac OS X [@PIL136] -def test_gimp_tiff(): - # Read TIFF JPEG images from GIMP [@PIL168] + file = "Tests/images/pil136.tiff" + im = Image.open(file) - codecs = dir(Image.core) - if "jpeg_decoder" not in codecs: - skip("jpeg support not available") + self.assertEqual(im.mode, "RGBA") + self.assertEqual(im.size, (55, 43)) + self.assertEqual(im.tile, [('raw', (0, 0, 55, 43), 8, ('RGBa', 0, 1))]) + im.load() - file = "Tests/images/pil168.tif" - im = Image.open(file) + def test_gimp_tiff(self): + # Read TIFF JPEG images from GIMP [@PIL168] - assert_equal(im.mode, "RGB") - assert_equal(im.size, (256, 256)) - assert_equal(im.tile, [ - ('jpeg', (0, 0, 256, 64), 8, ('RGB', '')), - ('jpeg', (0, 64, 256, 128), 1215, ('RGB', '')), - ('jpeg', (0, 128, 256, 192), 2550, ('RGB', '')), - ('jpeg', (0, 192, 256, 256), 3890, ('RGB', '')), - ]) - assert_no_exception(lambda: im.load()) + codecs = dir(Image.core) + if "jpeg_decoder" not in codecs: + self.skipTest("jpeg support not available") -def test_xyres_tiff(): - from PIL.TiffImagePlugin import X_RESOLUTION, Y_RESOLUTION - file = "Tests/images/pil168.tif" - im = Image.open(file) - assert isinstance(im.tag.tags[X_RESOLUTION][0], tuple) - assert isinstance(im.tag.tags[Y_RESOLUTION][0], tuple) - #Try to read a file where X,Y_RESOLUTION are ints - im.tag.tags[X_RESOLUTION] = (72,) - im.tag.tags[Y_RESOLUTION] = (72,) - im._setup() - assert_equal(im.info['dpi'], (72., 72.)) + file = "Tests/images/pil168.tif" + im = Image.open(file) + + self.assertEqual(im.mode, "RGB") + self.assertEqual(im.size, (256, 256)) + self.assertEqual( + im.tile, [ + ('jpeg', (0, 0, 256, 64), 8, ('RGB', '')), + ('jpeg', (0, 64, 256, 128), 1215, ('RGB', '')), + ('jpeg', (0, 128, 256, 192), 2550, ('RGB', '')), + ('jpeg', (0, 192, 256, 256), 3890, ('RGB', '')), + ]) + im.load() + + def test_xyres_tiff(self): + from PIL.TiffImagePlugin import X_RESOLUTION, Y_RESOLUTION + file = "Tests/images/pil168.tif" + im = Image.open(file) + assert isinstance(im.tag.tags[X_RESOLUTION][0], tuple) + assert isinstance(im.tag.tags[Y_RESOLUTION][0], tuple) + # Try to read a file where X,Y_RESOLUTION are ints + im.tag.tags[X_RESOLUTION] = (72,) + im.tag.tags[Y_RESOLUTION] = (72,) + im._setup() + self.assertEqual(im.info['dpi'], (72., 72.)) + + def test_little_endian(self): + im = Image.open('Tests/images/16bit.cropped.tif') + self.assertEqual(im.getpixel((0, 0)), 480) + self.assertEqual(im.mode, 'I;16') + + b = im.tobytes() + # Bytes are in image native order (little endian) + if py3: + self.assertEqual(b[0], ord(b'\xe0')) + self.assertEqual(b[1], ord(b'\x01')) + else: + self.assertEqual(b[0], b'\xe0') + self.assertEqual(b[1], b'\x01') + + def test_big_endian(self): + im = Image.open('Tests/images/16bit.MM.cropped.tif') + self.assertEqual(im.getpixel((0, 0)), 480) + self.assertEqual(im.mode, 'I;16B') + + b = im.tobytes() + + # Bytes are in image native order (big endian) + if py3: + self.assertEqual(b[0], ord(b'\x01')) + self.assertEqual(b[1], ord(b'\xe0')) + else: + self.assertEqual(b[0], b'\x01') + self.assertEqual(b[1], b'\xe0') + + def test_12bit_rawmode(self): + """ Are we generating the same interpretation + of the image as Imagemagick is? """ + + # Image.DEBUG = True + im = Image.open('Tests/images/12bit.cropped.tif') + + # to make the target -- + # convert 12bit.cropped.tif -depth 16 tmp.tif + # convert tmp.tif -evaluate RightShift 4 12in16bit2.tif + # imagemagick will auto scale so that a 12bit FFF is 16bit FFF0, + # so we need to unshift so that the integer values are the same. + + im2 = Image.open('Tests/images/12in16bit.tif') + + if Image.DEBUG: + print (im.getpixel((0, 0))) + print (im.getpixel((0, 1))) + print (im.getpixel((0, 2))) + + print (im2.getpixel((0, 0))) + print (im2.getpixel((0, 1))) + print (im2.getpixel((0, 2))) + + self.assert_image_equal(im, im2) + + def test_32bit_float(self): + # Issue 614, specific 32 bit float format + path = 'Tests/images/10ct_32bit_128.tiff' + im = Image.open(path) + im.load() + + self.assertEqual(im.getpixel((0, 0)), -0.4526388943195343) + self.assertEqual( + im.getextrema(), (-3.140936851501465, 3.140684127807617)) + + def test___str__(self): + # Arrange + file = "Tests/images/pil136.tiff" + im = Image.open(file) + + # Act + ret = str(im.ifd) + + # Assert + self.assertIsInstance(ret, str) + 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,)}') + + def test__delitem__(self): + # Arrange + file = "Tests/images/pil136.tiff" + im = Image.open(file) + len_before = len(im.ifd.as_dict()) + + # Act + del im.ifd[256] + + # Assert + len_after = len(im.ifd.as_dict()) + self.assertEqual(len_before, len_after + 1) + + def test_load_byte(self): + # Arrange + from PIL import TiffImagePlugin + ifd = TiffImagePlugin.ImageFileDirectory() + data = b"abc" + + # Act + ret = ifd.load_byte(data) + + # Assert + self.assertEqual(ret, b"abc") + + def test_load_string(self): + # Arrange + from PIL import TiffImagePlugin + ifd = TiffImagePlugin.ImageFileDirectory() + data = b"abc\0" + + # Act + ret = ifd.load_string(data) + + # Assert + self.assertEqual(ret, "abc") + + def test_load_float(self): + # Arrange + from PIL import TiffImagePlugin + ifd = TiffImagePlugin.ImageFileDirectory() + data = b"abcdabcd" + + # Act + ret = ifd.load_float(data) + + # Assert + self.assertEqual(ret, (1.6777999408082104e+22, 1.6777999408082104e+22)) + + def test_load_double(self): + # Arrange + from PIL import TiffImagePlugin + ifd = TiffImagePlugin.ImageFileDirectory() + data = b"abcdefghabcdefgh" + + # Act + ret = ifd.load_double(data) + + # Assert + self.assertEqual(ret, (8.540883223036124e+194, 8.540883223036124e+194)) + + def test_seek(self): + # Arrange + file = "Tests/images/pil136.tiff" + im = Image.open(file) + + # Act + im.seek(-1) + + # Assert + self.assertEqual(im.tell(), 0) + + def test_seek_eof(self): + # Arrange + file = "Tests/images/pil136.tiff" + im = Image.open(file) + self.assertEqual(im.tell(), 0) + + # Act / Assert + self.assertRaises(EOFError, lambda: im.seek(1)) + + def test__cvt_res_int(self): + # Arrange + from PIL.TiffImagePlugin import _cvt_res + value = 34 + + # Act + ret = _cvt_res(value) + + # Assert + self.assertEqual(ret, (34, 1)) + + def test__cvt_res_float(self): + # Arrange + from PIL.TiffImagePlugin import _cvt_res + value = 22.3 + + # Act + ret = _cvt_res(value) + + # Assert + self.assertEqual(ret, (1461452, 65536)) + + def test__cvt_res_sequence(self): + # Arrange + from PIL.TiffImagePlugin import _cvt_res + value = [0, 1] + + # Act + ret = _cvt_res(value) + + # Assert + self.assertEqual(ret, [0, 1]) -def test_little_endian(): - im = Image.open('Tests/images/16bit.cropped.tif') - assert_equal(im.getpixel((0,0)), 480) - assert_equal(im.mode, 'I;16') +if __name__ == '__main__': + unittest.main() - b = im.tobytes() - # Bytes are in image native order (little endian) - if py3: - assert_equal(b[0], ord(b'\xe0')) - assert_equal(b[1], ord(b'\x01')) - else: - assert_equal(b[0], b'\xe0') - assert_equal(b[1], b'\x01') - - -def test_big_endian(): - im = Image.open('Tests/images/16bit.MM.cropped.tif') - assert_equal(im.getpixel((0,0)), 480) - assert_equal(im.mode, 'I;16B') - - b = im.tobytes() - - # Bytes are in image native order (big endian) - if py3: - assert_equal(b[0], ord(b'\x01')) - assert_equal(b[1], ord(b'\xe0')) - else: - assert_equal(b[0], b'\x01') - assert_equal(b[1], b'\xe0') - - -def test_12bit_rawmode(): - """ Are we generating the same interpretation of the image as Imagemagick is? """ - - #Image.DEBUG = True - im = Image.open('Tests/images/12bit.cropped.tif') - - # to make the target -- - # convert 12bit.cropped.tif -depth 16 tmp.tif - # convert tmp.tif -evaluate RightShift 4 12in16bit2.tif - # imagemagick will auto scale so that a 12bit FFF is 16bit FFF0, - # so we need to unshift so that the integer values are the same. - - im2 = Image.open('Tests/images/12in16bit.tif') - - if Image.DEBUG: - print (im.getpixel((0,0))) - print (im.getpixel((0,1))) - print (im.getpixel((0,2))) - - print (im2.getpixel((0,0))) - print (im2.getpixel((0,1))) - print (im2.getpixel((0,2))) - - assert_image_equal(im, im2) - -def test_32bit_float(): - # Issue 614, specific 32 bit float format - path = 'Tests/images/10ct_32bit_128.tiff' - im = Image.open(path) - im.load() - - assert_equal(im.getpixel((0,0)), -0.4526388943195343) - assert_equal(im.getextrema(), (-3.140936851501465, 3.140684127807617)) - - +# End of file diff --git a/Tests/test_file_tiff_metadata.py b/Tests/test_file_tiff_metadata.py index 354eb972f..e0805b525 100644 --- a/Tests/test_file_tiff_metadata.py +++ b/Tests/test_file_tiff_metadata.py @@ -1,80 +1,93 @@ -from tester import * +from helper import unittest, PillowTestCase, lena + from PIL import Image, TiffImagePlugin, TiffTags tag_ids = dict(zip(TiffTags.TAGS.values(), TiffTags.TAGS.keys())) -def test_rt_metadata(): - """ Test writing arbitray metadata into the tiff image directory - Use case is ImageJ private tags, one numeric, one arbitrary - data. https://github.com/python-imaging/Pillow/issues/291 - """ - - img = lena() - textdata = "This is some arbitrary metadata for a text field" - info = TiffImagePlugin.ImageFileDirectory() +class TestFileTiffMetadata(PillowTestCase): - info[tag_ids['ImageJMetaDataByteCounts']] = len(textdata) - info[tag_ids['ImageJMetaData']] = textdata + def test_rt_metadata(self): + """ Test writing arbitray 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 + """ - f = tempfile("temp.tif") + img = lena() - img.save(f, tiffinfo=info) - - loaded = Image.open(f) + textdata = "This is some arbitrary metadata for a text field" + info = TiffImagePlugin.ImageFileDirectory() - assert_equal(loaded.tag[50838], (len(textdata),)) - assert_equal(loaded.tag[50839], textdata) - -def test_read_metadata(): - img = Image.open('Tests/images/lena_g4.tif') - - known = {'YResolution': ((1207959552, 16777216),), - 'PlanarConfiguration': (1,), - 'BitsPerSample': (1,), - 'ImageLength': (128,), - 'Compression': (4,), - 'FillOrder': (1,), - 'DocumentName': 'lena.g4.tif', - 'RowsPerStrip': (128,), - 'ResolutionUnit': (1,), - 'PhotometricInterpretation': (0,), - 'PageNumber': (0, 1), - 'XResolution': ((1207959552, 16777216),), - 'ImageWidth': (128,), - 'Orientation': (1,), - 'StripByteCounts': (1796,), - 'SamplesPerPixel': (1,), - 'StripOffsets': (8,), - 'Software': 'ImageMagick 6.5.7-8 2012-08-17 Q16 http://www.imagemagick.org'} + info[tag_ids['ImageJMetaDataByteCounts']] = len(textdata) + info[tag_ids['ImageJMetaData']] = textdata - # assert_equal is equivalent, but less helpful in telling what's wrong. - named = img.tag.named() - for tag, value in named.items(): - assert_equal(known[tag], value) + f = self.tempfile("temp.tif") - for tag, value in known.items(): - assert_equal(value, named[tag]) + img.save(f, tiffinfo=info) + + loaded = Image.open(f) + + self.assertEqual(loaded.tag[50838], (len(textdata),)) + self.assertEqual(loaded.tag[50839], textdata) + + def test_read_metadata(self): + img = Image.open('Tests/images/lena_g4.tif') + + known = {'YResolution': ((1207959552, 16777216),), + 'PlanarConfiguration': (1,), + 'BitsPerSample': (1,), + 'ImageLength': (128,), + 'Compression': (4,), + 'FillOrder': (1,), + 'DocumentName': 'lena.g4.tif', + 'RowsPerStrip': (128,), + 'ResolutionUnit': (1,), + 'PhotometricInterpretation': (0,), + 'PageNumber': (0, 1), + 'XResolution': ((1207959552, 16777216),), + 'ImageWidth': (128,), + 'Orientation': (1,), + 'StripByteCounts': (1796,), + '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. + named = img.tag.named() + for tag, value in named.items(): + self.assertEqual(known[tag], value) + + for tag, value in known.items(): + self.assertEqual(value, named[tag]) + + def test_write_metadata(self): + """ Test metadata writing through the python code """ + img = Image.open('Tests/images/lena.tif') + + f = self.tempfile('temp.tiff') + img.save(f, tiffinfo=img.tag) + + loaded = Image.open(f) + + original = img.tag.named() + reloaded = loaded.tag.named() + + ignored = [ + 'StripByteCounts', 'RowsPerStrip', 'PageNumber', 'StripOffsets'] + + for tag, value in reloaded.items(): + if tag not in ignored: + self.assertEqual( + original[tag], value, "%s didn't roundtrip" % tag) + + for tag, value in original.items(): + if tag not in ignored: + self.assertEqual( + value, reloaded[tag], "%s didn't roundtrip" % tag) -def test_write_metadata(): - """ Test metadata writing through the python code """ - img = Image.open('Tests/images/lena.tif') +if __name__ == '__main__': + unittest.main() - f = tempfile('temp.tiff') - img.save(f, tiffinfo = img.tag) - - loaded = Image.open(f) - - original = img.tag.named() - reloaded = loaded.tag.named() - - ignored = ['StripByteCounts', 'RowsPerStrip', 'PageNumber', 'StripOffsets'] - - for tag, value in reloaded.items(): - if tag not in ignored: - assert_equal(original[tag], value, "%s didn't roundtrip" % tag) - - for tag, value in original.items(): - if tag not in ignored: - assert_equal(value, reloaded[tag], "%s didn't roundtrip" % tag) +# End of file diff --git a/Tests/test_file_webp.py b/Tests/test_file_webp.py index a89dea3c4..ffaf7c673 100644 --- a/Tests/test_file_webp.py +++ b/Tests/test_file_webp.py @@ -1,68 +1,79 @@ -from tester import * +from helper import unittest, PillowTestCase, lena from PIL import Image - try: from PIL import _webp except: - skip('webp support not installed') + # Skip in setUp() + pass -def test_version(): - assert_no_exception(lambda: _webp.WebPDecoderVersion()) - assert_no_exception(lambda: _webp.WebPDecoderBuggyAlpha()) +class TestFileWebp(PillowTestCase): -def test_read_rgb(): + def setUp(self): + try: + from PIL import _webp + except: + self.skipTest('WebP support not installed') - file_path = "Images/lena.webp" - image = Image.open(file_path) + def test_version(self): + _webp.WebPDecoderVersion() + _webp.WebPDecoderBuggyAlpha() - assert_equal(image.mode, "RGB") - assert_equal(image.size, (128, 128)) - assert_equal(image.format, "WEBP") - assert_no_exception(lambda: image.load()) - assert_no_exception(lambda: image.getdata()) - - # generated with: dwebp -ppm ../../Images/lena.webp -o lena_webp_bits.ppm - target = Image.open('Tests/images/lena_webp_bits.ppm') - assert_image_similar(image, target, 20.0) - - -def test_write_rgb(): - """ - Can we write a RGB mode file to webp without error. Does it have the bits we - expect? - - """ - - temp_file = tempfile("temp.webp") - - lena("RGB").save(temp_file) - - image = Image.open(temp_file) - image.load() - - assert_equal(image.mode, "RGB") - assert_equal(image.size, (128, 128)) - assert_equal(image.format, "WEBP") - assert_no_exception(lambda: image.load()) - assert_no_exception(lambda: image.getdata()) - - # If we're using the exact same version of webp, this test should pass. - # 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') - #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 Ubuntu, the jpegs are showing ~18. - target = lena("RGB") - assert_image_similar(image, target, 20.0) + def test_read_rgb(self): + file_path = "Tests/images/lena.webp" + image = Image.open(file_path) + + self.assertEqual(image.mode, "RGB") + self.assertEqual(image.size, (128, 128)) + self.assertEqual(image.format, "WEBP") + image.load() + image.getdata() + + # generated with: + # dwebp -ppm ../../Tests/images/lena.webp -o lena_webp_bits.ppm + target = Image.open('Tests/images/lena_webp_bits.ppm') + self.assert_image_similar(image, target, 20.0) + + def test_write_rgb(self): + """ + Can we write a RGB mode file to webp without error. + Does it have the bits we expect? + """ + + temp_file = self.tempfile("temp.webp") + + lena("RGB").save(temp_file) + + image = Image.open(temp_file) + image.load() + + self.assertEqual(image.mode, "RGB") + self.assertEqual(image.size, (128, 128)) + self.assertEqual(image.format, "WEBP") + image.load() + image.getdata() + + # If we're using the exact same version of WebP, this test should pass. + # 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') + # 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 + # Ubuntu, the jpegs are showing ~18. + target = lena("RGB") + self.assert_image_similar(image, target, 20.0) +if __name__ == '__main__': + unittest.main() +# End of file diff --git a/Tests/test_file_webp_alpha.py b/Tests/test_file_webp_alpha.py index 5ac03b9d4..5f8f653cf 100644 --- a/Tests/test_file_webp_alpha.py +++ b/Tests/test_file_webp_alpha.py @@ -1,82 +1,91 @@ -from tester import * +from helper import unittest, PillowTestCase, lena from PIL import Image try: from PIL import _webp except: - skip('webp support not installed') + pass + # Skip in setUp() -if _webp.WebPDecoderBuggyAlpha(): - skip("Buggy early version of webp installed, not testing transparency") +class TestFileWebpAlpha(PillowTestCase): -def test_read_rgba(): - # Generated with `cwebp transparent.png -o transparent.webp` - file_path = "Images/transparent.webp" - image = Image.open(file_path) + def setUp(self): + try: + from PIL import _webp + except: + self.skipTest('WebP support not installed') - assert_equal(image.mode, "RGBA") - assert_equal(image.size, (200, 150)) - assert_equal(image.format, "WEBP") - assert_no_exception(lambda: image.load()) - assert_no_exception(lambda: image.getdata()) + if _webp.WebPDecoderBuggyAlpha(self): + self.skipTest("Buggy early version of WebP installed, not testing transparency") - orig_bytes = image.tobytes() - - target = Image.open('Images/transparent.png') - assert_image_similar(image, target, 20.0) - - -def test_write_lossless_rgb(): - temp_file = tempfile("temp.webp") - #temp_file = "temp.webp" - - pil_image = lena('RGBA') - - mask = Image.new("RGBA", (64, 64), (128,128,128,128)) - pil_image.paste(mask, (0,0), mask) # add some partially transparent bits. - - pil_image.save(temp_file, lossless=True) - - image = Image.open(temp_file) - image.load() - - assert_equal(image.mode, "RGBA") - assert_equal(image.size, pil_image.size) - assert_equal(image.format, "WEBP") - assert_no_exception(lambda: image.load()) - assert_no_exception(lambda: image.getdata()) - - - assert_image_equal(image, pil_image) - -def test_write_rgba(): - """ - Can we write a RGBA mode file to webp without error. Does it have the bits we - expect? - - """ - - temp_file = tempfile("temp.webp") - - pil_image = Image.new("RGBA", (10, 10), (255, 0, 0, 20)) - pil_image.save(temp_file) - - if _webp.WebPDecoderBuggyAlpha(): - return - - image = Image.open(temp_file) - image.load() - - assert_equal(image.mode, "RGBA") - assert_equal(image.size, (10, 10)) - assert_equal(image.format, "WEBP") - assert_no_exception(image.load) - assert_no_exception(image.getdata) - - assert_image_similar(image, pil_image, 1.0) + def test_read_rgba(self): + # Generated with `cwebp transparent.png -o transparent.webp` + file_path = "Tests/images/transparent.webp" + image = Image.open(file_path) + self.assertEqual(image.mode, "RGBA") + self.assertEqual(image.size, (200, 150)) + self.assertEqual(image.format, "WEBP") + image.load() + image.getdata() + + image.tobytes() + + target = Image.open('Tests/images/transparent.png') + self.assert_image_similar(image, target, 20.0) + + def test_write_lossless_rgb(self): + temp_file = self.tempfile("temp.webp") + # temp_file = "temp.webp" + + pil_image = lena('RGBA') + + mask = Image.new("RGBA", (64, 64), (128, 128, 128, 128)) + # Add some partially transparent bits: + pil_image.paste(mask, (0, 0), mask) + + pil_image.save(temp_file, lossless=True) + + image = Image.open(temp_file) + image.load() + + self.assertEqual(image.mode, "RGBA") + self.assertEqual(image.size, pil_image.size) + self.assertEqual(image.format, "WEBP") + image.load() + image.getdata() + + self.assert_image_equal(image, pil_image) + + def test_write_rgba(self): + """ + Can we write a RGBA mode file to webp without error. + Does it have the bits we expect? + """ + + temp_file = self.tempfile("temp.webp") + + pil_image = Image.new("RGBA", (10, 10), (255, 0, 0, 20)) + pil_image.save(temp_file) + + if _webp.WebPDecoderBuggyAlpha(self): + return + + image = Image.open(temp_file) + image.load() + + self.assertEqual(image.mode, "RGBA") + self.assertEqual(image.size, (10, 10)) + self.assertEqual(image.format, "WEBP") + image.load + image.getdata + + self.assert_image_similar(image, pil_image, 1.0) +if __name__ == '__main__': + unittest.main() +# End of file diff --git a/Tests/test_file_webp_lossless.py b/Tests/test_file_webp_lossless.py index ca2b5af19..662ad1117 100644 --- a/Tests/test_file_webp_lossless.py +++ b/Tests/test_file_webp_lossless.py @@ -1,33 +1,43 @@ -from tester import * +from helper import unittest, PillowTestCase, lena from PIL import Image - try: from PIL import _webp except: - skip('webp support not installed') + pass + # Skip in setUp() -if (_webp.WebPDecoderVersion() < 0x0200): - skip('lossless not included') +class TestFileWebpLossless(PillowTestCase): -def test_write_lossless_rgb(): - temp_file = tempfile("temp.webp") + def setUp(self): + try: + from PIL import _webp + except: + self.skipTest('WebP support not installed') - lena("RGB").save(temp_file, lossless=True) + if (_webp.WebPDecoderVersion() < 0x0200): + self.skipTest('lossless not included') - image = Image.open(temp_file) - image.load() + def test_write_lossless_rgb(self): + temp_file = self.tempfile("temp.webp") - assert_equal(image.mode, "RGB") - assert_equal(image.size, (128, 128)) - assert_equal(image.format, "WEBP") - assert_no_exception(lambda: image.load()) - assert_no_exception(lambda: image.getdata()) - - - assert_image_equal(image, lena("RGB")) + lena("RGB").save(temp_file, lossless=True) + + image = Image.open(temp_file) + image.load() + + self.assertEqual(image.mode, "RGB") + self.assertEqual(image.size, (128, 128)) + self.assertEqual(image.format, "WEBP") + image.load() + image.getdata() + + self.assert_image_equal(image, lena("RGB")) +if __name__ == '__main__': + unittest.main() +# End of file diff --git a/Tests/test_file_webp_metadata.py b/Tests/test_file_webp_metadata.py index b4146c3ee..6aadf9c7e 100644 --- a/Tests/test_file_webp_metadata.py +++ b/Tests/test_file_webp_metadata.py @@ -1,101 +1,112 @@ -from tester import * +from helper import unittest, PillowTestCase from PIL import Image -try: - from PIL import _webp - if not _webp.HAVE_WEBPMUX: - skip('webpmux support not installed') -except: - skip('webp support not installed') + +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') + + def test_read_exif_metadata(self): + + file_path = "Tests/images/flower.webp" + image = Image.open(file_path) + + self.assertEqual(image.format, "WEBP") + exif_data = image.info.get("exif", None) + self.assertTrue(exif_data) + + exif = image._getexif() + + # camera make + self.assertEqual(exif[271], "Canon") + + jpeg_image = Image.open('Tests/images/flower.jpg') + expected_exif = jpeg_image.info['exif'] + + self.assertEqual(exif_data, expected_exif) + + def test_write_exif_metadata(self): + from io import BytesIO + + file_path = "Tests/images/flower.jpg" + image = Image.open(file_path) + expected_exif = image.info['exif'] + + buffer = BytesIO() + + image.save(buffer, "webp", exif=expected_exif) + + buffer.seek(0) + webp_image = Image.open(buffer) + + webp_exif = webp_image.info.get('exif', None) + self.assertTrue(webp_exif) + if webp_exif: + self.assertEqual( + webp_exif, expected_exif, "WebP EXIF didn't match") + + def test_read_icc_profile(self): + + file_path = "Tests/images/flower2.webp" + image = Image.open(file_path) + + self.assertEqual(image.format, "WEBP") + self.assertTrue(image.info.get("icc_profile", None)) + + icc = image.info['icc_profile'] + + jpeg_image = Image.open('Tests/images/flower2.jpg') + expected_icc = jpeg_image.info['icc_profile'] + + self.assertEqual(icc, expected_icc) + + def test_write_icc_metadata(self): + from io import BytesIO + + file_path = "Tests/images/flower2.jpg" + image = Image.open(file_path) + expected_icc_profile = image.info['icc_profile'] + + buffer = BytesIO() + + image.save(buffer, "webp", icc_profile=expected_icc_profile) + + buffer.seek(0) + webp_image = Image.open(buffer) + + webp_icc_profile = webp_image.info.get('icc_profile', None) + + self.assertTrue(webp_icc_profile) + if webp_icc_profile: + self.assertEqual( + webp_icc_profile, expected_icc_profile, + "Webp ICC didn't match") + + def test_read_no_exif(self): + from io import BytesIO + + file_path = "Tests/images/flower.jpg" + image = Image.open(file_path) + image.info['exif'] + + buffer = BytesIO() + + image.save(buffer, "webp") + + buffer.seek(0) + webp_image = Image.open(buffer) + + self.assertFalse(webp_image._getexif()) +if __name__ == '__main__': + unittest.main() -def test_read_exif_metadata(): - - file_path = "Images/flower.webp" - image = Image.open(file_path) - - assert_equal(image.format, "WEBP") - exif_data = image.info.get("exif", None) - assert_true(exif_data) - - exif = image._getexif() - - #camera make - assert_equal(exif[271], "Canon") - - jpeg_image = Image.open('Tests/images/flower.jpg') - expected_exif = jpeg_image.info['exif'] - - assert_equal(exif_data, expected_exif) - - -def test_write_exif_metadata(): - file_path = "Tests/images/flower.jpg" - image = Image.open(file_path) - expected_exif = image.info['exif'] - - buffer = BytesIO() - - image.save(buffer, "webp", exif=expected_exif) - - buffer.seek(0) - webp_image = Image.open(buffer) - - webp_exif = webp_image.info.get('exif', None) - assert_true(webp_exif) - if webp_exif: - assert_equal(webp_exif, expected_exif, "Webp Exif didn't match") - - -def test_read_icc_profile(): - - file_path = "Images/flower2.webp" - image = Image.open(file_path) - - assert_equal(image.format, "WEBP") - assert_true(image.info.get("icc_profile", None)) - - icc = image.info['icc_profile'] - - jpeg_image = Image.open('Tests/images/flower2.jpg') - expected_icc = jpeg_image.info['icc_profile'] - - assert_equal(icc, expected_icc) - - -def test_write_icc_metadata(): - file_path = "Tests/images/flower2.jpg" - image = Image.open(file_path) - expected_icc_profile = image.info['icc_profile'] - - buffer = BytesIO() - - image.save(buffer, "webp", icc_profile=expected_icc_profile) - - buffer.seek(0) - webp_image = Image.open(buffer) - - webp_icc_profile = webp_image.info.get('icc_profile', None) - - assert_true(webp_icc_profile) - if webp_icc_profile: - assert_equal(webp_icc_profile, expected_icc_profile, "Webp ICC didn't match") - - -def test_read_no_exif(): - file_path = "Tests/images/flower.jpg" - image = Image.open(file_path) - expected_exif = image.info['exif'] - - buffer = BytesIO() - - image.save(buffer, "webp") - - buffer.seek(0) - webp_image = Image.open(buffer) - - assert_false(webp_image._getexif()) - - +# End of file diff --git a/Tests/test_file_xbm.py b/Tests/test_file_xbm.py index f27a3a349..02aec70b1 100644 --- a/Tests/test_file_xbm.py +++ b/Tests/test_file_xbm.py @@ -1,4 +1,4 @@ -from tester import * +from helper import unittest, PillowTestCase from PIL import Image @@ -25,10 +25,20 @@ static char basic_bits[] = { }; """ -def test_pil151(): - im = Image.open(BytesIO(PIL151)) +class TestFileXbm(PillowTestCase): - assert_no_exception(lambda: im.load()) - assert_equal(im.mode, '1') - assert_equal(im.size, (32, 32)) + def test_pil151(self): + from io import BytesIO + + im = Image.open(BytesIO(PIL151)) + + im.load() + self.assertEqual(im.mode, '1') + self.assertEqual(im.size, (32, 32)) + + +if __name__ == '__main__': + unittest.main() + +# End of file diff --git a/Tests/test_file_xpm.py b/Tests/test_file_xpm.py index 44135d028..4fc393808 100644 --- a/Tests/test_file_xpm.py +++ b/Tests/test_file_xpm.py @@ -1,14 +1,36 @@ -from tester import * +from helper import unittest, PillowTestCase, lena from PIL import Image # sample ppm stream -file = "Images/lena.xpm" -data = open(file, "rb").read() +TEST_FILE = "Tests/images/lena.xpm" -def test_sanity(): - im = Image.open(file) - im.load() - assert_equal(im.mode, "P") - assert_equal(im.size, (128, 128)) - assert_equal(im.format, "XPM") + +class TestFileXpm(PillowTestCase): + + def test_sanity(self): + im = Image.open(TEST_FILE) + im.load() + self.assertEqual(im.mode, "P") + 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) + + def test_load_read(self): + # Arrange + im = Image.open(TEST_FILE) + dummy_bytes = 1 + + # Act + data = im.load_read(dummy_bytes) + + # Assert + self.assertEqual(len(data), 16384) + + +if __name__ == '__main__': + unittest.main() + +# End of file diff --git a/Tests/test_font_bdf.py b/Tests/test_font_bdf.py index 366bb4468..0df8e866b 100644 --- a/Tests/test_font_bdf.py +++ b/Tests/test_font_bdf.py @@ -1,13 +1,22 @@ -from tester import * +from helper import unittest, PillowTestCase -from PIL import Image, FontFile, BdfFontFile +from PIL import FontFile, BdfFontFile -filename = "Images/courB08.bdf" +filename = "Tests/images/courB08.bdf" -def test_sanity(): - file = open(filename, "rb") - font = BdfFontFile.BdfFontFile(file) +class TestFontBdf(PillowTestCase): - assert_true(isinstance(font, FontFile.FontFile)) - assert_equal(len([_f for _f in font.glyph if _f]), 190) + def test_sanity(self): + + file = open(filename, "rb") + font = BdfFontFile.BdfFontFile(file) + + self.assertIsInstance(font, FontFile.FontFile) + self.assertEqual(len([_f for _f in font.glyph if _f]), 190) + + +if __name__ == '__main__': + unittest.main() + +# End of file diff --git a/Tests/test_font_pcf.py b/Tests/test_font_pcf.py index bae214e35..5e9e02c8c 100644 --- a/Tests/test_font_pcf.py +++ b/Tests/test_font_pcf.py @@ -1,49 +1,64 @@ -from tester import * +from helper import unittest, PillowTestCase from PIL import Image, FontFile, PcfFontFile from PIL import ImageFont, ImageDraw codecs = dir(Image.core) -if "zip_encoder" not in codecs or "zip_decoder" not in codecs: - skip("zlib support not available") - fontname = "Tests/fonts/helvO18.pcf" -tempname = tempfile("temp.pil", "temp.pbm") -message = "hello, world" +message = "hello, world" -def test_sanity(): - file = open(fontname, "rb") - font = PcfFontFile.PcfFontFile(file) - assert_true(isinstance(font, FontFile.FontFile)) - assert_equal(len([_f for _f in font.glyph if _f]), 192) +class TestFontPcf(PillowTestCase): - font.save(tempname) + def setUp(self): + if "zip_encoder" not in codecs or "zip_decoder" not in codecs: + self.skipTest("zlib support not available") -def xtest_draw(): + def save_font(self): + file = open(fontname, "rb") + font = PcfFontFile.PcfFontFile(file) + self.assertIsInstance(font, FontFile.FontFile) + self.assertEqual(len([_f for _f in font.glyph if _f]), 192) - font = ImageFont.load(tempname) - image = Image.new("L", font.getsize(message), "white") - draw = ImageDraw.Draw(image) - draw.text((0, 0), message, font=font) - # assert_signature(image, "7216c60f988dea43a46bb68321e3c1b03ec62aee") + tempname = self.tempfile("temp.pil") + self.addCleanup(self.delete_tempfile, tempname[:-4]+'.pbm') + font.save(tempname) + return tempname -def _test_high_characters(message): + def test_sanity(self): + self.save_font() - font = ImageFont.load(tempname) - image = Image.new("L", font.getsize(message), "white") - draw = ImageDraw.Draw(image) - draw.text((0, 0), message, font=font) + def xtest_draw(self): - compare = Image.open('Tests/images/high_ascii_chars.png') - assert_image_equal(image, compare) + tempname = self.save_font() + font = ImageFont.load(tempname) + image = Image.new("L", font.getsize(message), "white") + draw = ImageDraw.Draw(image) + draw.text((0, 0), message, font=font) + # assert_signature(image, "7216c60f988dea43a46bb68321e3c1b03ec62aee") -def test_high_characters(): - message = "".join([chr(i+1) for i in range(140,232)]) - _test_high_characters(message) - # accept bytes instances in Py3. - if bytes is not str: - _test_high_characters(message.encode('latin1')) + def _test_high_characters(self, message): + tempname = self.save_font() + font = ImageFont.load(tempname) + image = Image.new("L", font.getsize(message), "white") + draw = ImageDraw.Draw(image) + draw.text((0, 0), message, font=font) + + compare = Image.open('Tests/images/high_ascii_chars.png') + self.assert_image_equal(image, compare) + + def test_high_characters(self): + message = "".join([chr(i+1) for i in range(140, 232)]) + self._test_high_characters(message) + # accept bytes instances in Py3. + if bytes is not str: + self._test_high_characters(message.encode('latin1')) + + +if __name__ == '__main__': + unittest.main() + +# End of file diff --git a/Tests/test_format_hsv.py b/Tests/test_format_hsv.py new file mode 100644 index 000000000..03603aa9b --- /dev/null +++ b/Tests/test_format_hsv.py @@ -0,0 +1,169 @@ +from helper import unittest, PillowTestCase, lena + +from PIL import Image + +import colorsys, 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 + return (int(x*255.0), int(y*255.0), int(z*255.0)) + + def test_sanity(self): + im = Image.new('HSV', (100,100)) + + def wedge(self): + w =Image._wedge() + w90 = w.rotate(90) + + (px, h) = w.size + + r = Image.new('L', (px*3,h)) + g = r.copy() + b = r.copy() + + r.paste(w, (0,0)) + r.paste(w90, (px,0)) + + g.paste(w90, (0,0)) + g.paste(w, (2*px,0)) + + b.paste(w, (px,0)) + b.paste(w90, (2*px,0)) + + 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))) + return img + + def to_xxx_colorsys(self, im, func, mode): + # convert the hard way using the library colorsys routines. + + (r,g,b) = im.split() + + if bytes is str: + conv_func = self.str_to_float + else: + conv_func = self.int_to_float + + if hasattr(itertools, 'izip'): + iter_helper = itertools.izip + 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())] + + if str is bytes: + 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) + + return hsv + + def to_hsv_colorsys(self, im): + return self.to_xxx_colorsys(im, colorsys.rgb_to_hsv, 'HSV') + + def to_rgb_colorsys(self, im): + return self.to_xxx_colorsys(im, colorsys.hsv_to_rgb, 'RGB') + + def test_wedge(self): + 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()) + + #im.split()[0].show() + #comparable.split()[0].show() + + 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], + 1, "Saturation conversion is wrong") + self.assert_image_similar(im.split()[2], comparable.split()[2], + 1, "Value conversion is wrong") + + #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))) + + 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')) + +# 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], + 1, "Saturation conversion is wrong") + 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')) + 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]]) + + + self.assert_image_similar(converted.split()[0], comparable.split()[0], + 3, "R conversion is wrong") + self.assert_image_similar(converted.split()[1], comparable.split()[1], + 3, "G conversion is wrong") + self.assert_image_similar(converted.split()[2], comparable.split()[2], + 3, "B conversion is wrong") + + + + + + + + +if __name__ == '__main__': + unittest.main() + +# End of file diff --git a/Tests/test_format_lab.py b/Tests/test_format_lab.py index 371b06a0b..53468db5f 100644 --- a/Tests/test_format_lab.py +++ b/Tests/test_format_lab.py @@ -1,41 +1,48 @@ -from tester import * +from helper import unittest, PillowTestCase from PIL import Image -def test_white(): - i = Image.open('Tests/images/lab.tif') - bits = i.load() - - assert_equal(i.mode, 'LAB') +class TestFormatLab(PillowTestCase): - assert_equal(i.getbands(), ('L','A', 'B')) + def test_white(self): + i = Image.open('Tests/images/lab.tif') - k = i.getpixel((0,0)) - assert_equal(k, (255,128,128)) + i.load() - L = i.getdata(0) - a = i.getdata(1) - b = i.getdata(2) + self.assertEqual(i.mode, 'LAB') - assert_equal(list(L), [255]*100) - assert_equal(list(a), [128]*100) - assert_equal(list(b), [128]*100) - + self.assertEqual(i.getbands(), ('L', 'A', 'B')) -def test_green(): - # l= 50 (/100), a = -100 (-128 .. 128) b=0 in PS - # == RGB: 0, 152, 117 - i = Image.open('Tests/images/lab-green.tif') + k = i.getpixel((0, 0)) + self.assertEqual(k, (255, 128, 128)) - k = i.getpixel((0,0)) - assert_equal(k, (128,28,128)) + L = i.getdata(0) + a = i.getdata(1) + b = i.getdata(2) + + self.assertEqual(list(L), [255]*100) + self.assertEqual(list(a), [128]*100) + self.assertEqual(list(b), [128]*100) + + def test_green(self): + # l= 50 (/100), a = -100 (-128 .. 128) b=0 in PS + # == RGB: 0, 152, 117 + i = Image.open('Tests/images/lab-green.tif') + + k = i.getpixel((0, 0)) + self.assertEqual(k, (128, 28, 128)) + + def test_red(self): + # l= 50 (/100), a = 100 (-128 .. 128) b=0 in PS + # == RGB: 255, 0, 124 + i = Image.open('Tests/images/lab-red.tif') + + k = i.getpixel((0, 0)) + self.assertEqual(k, (128, 228, 128)) -def test_red(): - # l= 50 (/100), a = 100 (-128 .. 128) b=0 in PS - # == RGB: 255, 0, 124 - i = Image.open('Tests/images/lab-red.tif') +if __name__ == '__main__': + unittest.main() - k = i.getpixel((0,0)) - assert_equal(k, (128,228,128)) +# End of file diff --git a/Tests/test_image.py b/Tests/test_image.py index 26c699e66..cd46c9713 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -1,39 +1,147 @@ -from tester import * +from helper import unittest, PillowTestCase, lena from PIL import Image -def test_sanity(): - im = Image.new("L", (100, 100)) - assert_equal(repr(im)[:45], " 8 bit lut for converting I->L images - see https://github.com/python-imaging/Pillow/issues/440 - """ +class TestImagePoint(PillowTestCase): - im = lena("I") - assert_no_exception(lambda: im.point(list(range(256))*256, 'L')) + def test_sanity(self): + im = lena() + + self.assertRaises(ValueError, lambda: im.point(list(range(256)))) + im.point(list(range(256))*3) + im.point(lambda x: x) + + im = im.convert("I") + self.assertRaises(ValueError, lambda: im.point(list(range(256)))) + im.point(lambda x: x*1) + im.point(lambda x: x+1) + im.point(lambda x: x*1+1) + self.assertRaises(TypeError, lambda: im.point(lambda x: x-1)) + self.assertRaises(TypeError, lambda: im.point(lambda x: x/1)) + + def test_16bit_lut(self): + """ Tests for 16 bit -> 8 bit lut for converting I->L images + see https://github.com/python-pillow/Pillow/issues/440 + """ + # This takes _forever_ on PyPy. Open Bug, + # see https://github.com/python-pillow/Pillow/issues/484 + #self.skipKnownBadTest(msg="Too Slow on pypy", interpreter='pypy') + + im = lena("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') + 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() + +# End of file diff --git a/Tests/test_image_putalpha.py b/Tests/test_image_putalpha.py index b23f69834..85c7ac262 100644 --- a/Tests/test_image_putalpha.py +++ b/Tests/test_image_putalpha.py @@ -1,43 +1,52 @@ -from tester import * +from helper import unittest, PillowTestCase from PIL import Image -def test_interface(): - im = Image.new("RGBA", (1, 1), (1, 2, 3, 0)) - assert_equal(im.getpixel((0, 0)), (1, 2, 3, 0)) +class TestImagePutAlpha(PillowTestCase): - im = Image.new("RGBA", (1, 1), (1, 2, 3)) - assert_equal(im.getpixel((0, 0)), (1, 2, 3, 255)) + def test_interface(self): - im.putalpha(Image.new("L", im.size, 4)) - assert_equal(im.getpixel((0, 0)), (1, 2, 3, 4)) + im = Image.new("RGBA", (1, 1), (1, 2, 3, 0)) + self.assertEqual(im.getpixel((0, 0)), (1, 2, 3, 0)) - im.putalpha(5) - assert_equal(im.getpixel((0, 0)), (1, 2, 3, 5)) + im = Image.new("RGBA", (1, 1), (1, 2, 3)) + self.assertEqual(im.getpixel((0, 0)), (1, 2, 3, 255)) -def test_promote(): + im.putalpha(Image.new("L", im.size, 4)) + self.assertEqual(im.getpixel((0, 0)), (1, 2, 3, 4)) - im = Image.new("L", (1, 1), 1) - assert_equal(im.getpixel((0, 0)), 1) + im.putalpha(5) + self.assertEqual(im.getpixel((0, 0)), (1, 2, 3, 5)) - im.putalpha(2) - assert_equal(im.mode, 'LA') - assert_equal(im.getpixel((0, 0)), (1, 2)) + def test_promote(self): - im = Image.new("RGB", (1, 1), (1, 2, 3)) - assert_equal(im.getpixel((0, 0)), (1, 2, 3)) + im = Image.new("L", (1, 1), 1) + self.assertEqual(im.getpixel((0, 0)), 1) - im.putalpha(4) - assert_equal(im.mode, 'RGBA') - assert_equal(im.getpixel((0, 0)), (1, 2, 3, 4)) + im.putalpha(2) + self.assertEqual(im.mode, 'LA') + self.assertEqual(im.getpixel((0, 0)), (1, 2)) -def test_readonly(): + im = Image.new("RGB", (1, 1), (1, 2, 3)) + self.assertEqual(im.getpixel((0, 0)), (1, 2, 3)) - im = Image.new("RGB", (1, 1), (1, 2, 3)) - im.readonly = 1 + im.putalpha(4) + self.assertEqual(im.mode, 'RGBA') + self.assertEqual(im.getpixel((0, 0)), (1, 2, 3, 4)) - im.putalpha(4) - assert_false(im.readonly) - assert_equal(im.mode, 'RGBA') - assert_equal(im.getpixel((0, 0)), (1, 2, 3, 4)) + def test_readonly(self): + + im = Image.new("RGB", (1, 1), (1, 2, 3)) + im.readonly = 1 + + im.putalpha(4) + self.assertFalse(im.readonly) + self.assertEqual(im.mode, 'RGBA') + self.assertEqual(im.getpixel((0, 0)), (1, 2, 3, 4)) + + +if __name__ == '__main__': + unittest.main() + +# End of file diff --git a/Tests/test_image_putdata.py b/Tests/test_image_putdata.py index e25359fdf..acea0d62a 100644 --- a/Tests/test_image_putdata.py +++ b/Tests/test_image_putdata.py @@ -1,40 +1,72 @@ -from tester import * +from helper import unittest, PillowTestCase, lena import sys from PIL import Image -def test_sanity(): - im1 = lena() +class TestImagePutData(PillowTestCase): - data = list(im1.getdata()) + def test_sanity(self): - im2 = Image.new(im1.mode, im1.size, 0) - im2.putdata(data) + im1 = lena() - assert_image_equal(im1, im2) + data = list(im1.getdata()) - # readonly - im2 = Image.new(im1.mode, im2.size, 0) - im2.readonly = 1 - im2.putdata(data) + im2 = Image.new(im1.mode, im1.size, 0) + im2.putdata(data) - assert_false(im2.readonly) - assert_image_equal(im1, im2) + self.assert_image_equal(im1, im2) + + # readonly + im2 = Image.new(im1.mode, im2.size, 0) + im2.readonly = 1 + im2.putdata(data) + + self.assertFalse(im2.readonly) + self.assert_image_equal(im1, im2) + + def test_long_integers(self): + # see bug-200802-systemerror + def put(value): + im = Image.new("RGBA", (1, 1)) + im.putdata([value]) + return im.getpixel((0, 0)) + self.assertEqual(put(0xFFFFFFFF), (255, 255, 255, 255)) + self.assertEqual(put(0xFFFFFFFF), (255, 255, 255, 255)) + self.assertEqual(put(-1), (255, 255, 255, 255)) + self.assertEqual(put(-1), (255, 255, 255, 255)) + if sys.maxsize > 2**32: + self.assertEqual(put(sys.maxsize), (255, 255, 255, 255)) + else: + self.assertEqual(put(sys.maxsize), (255, 255, 255, 127)) -def test_long_integers(): - # see bug-200802-systemerror - def put(value): - im = Image.new("RGBA", (1, 1)) - im.putdata([value]) - return im.getpixel((0, 0)) - assert_equal(put(0xFFFFFFFF), (255, 255, 255, 255)) - assert_equal(put(0xFFFFFFFF), (255, 255, 255, 255)) - assert_equal(put(-1), (255, 255, 255, 255)) - assert_equal(put(-1), (255, 255, 255, 255)) - if sys.maxsize > 2**32: - assert_equal(put(sys.maxsize), (255, 255, 255, 255)) - else: - assert_equal(put(sys.maxsize), (255, 255, 255, 127)) + def test_pypy_performance(self): + im = Image.new('L', (256,256)) + im.putdata(list(range(256))*256) + + def test_mode_i(self): + src = lena('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] + self.assertEqual(list(im.getdata()), target) + + def test_mode_F(self): + src = lena('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] + self.assertEqual(list(im.getdata()), target) + + + +if __name__ == '__main__': + unittest.main() + +# End of file diff --git a/Tests/test_image_putpalette.py b/Tests/test_image_putpalette.py index b7ebb8853..a77c1e565 100644 --- a/Tests/test_image_putpalette.py +++ b/Tests/test_image_putpalette.py @@ -1,28 +1,36 @@ -from tester import * +from helper import unittest, PillowTestCase, lena -from PIL import Image from PIL import ImagePalette -def test_putpalette(): - def palette(mode): - im = lena(mode).copy() - im.putpalette(list(range(256))*3) - p = im.getpalette() - if p: - return im.mode, p[:10] - return im.mode - assert_exception(ValueError, lambda: palette("1")) - assert_equal(palette("L"), ("P", [0, 1, 2, 3, 4, 5, 6, 7, 8, 9])) - assert_equal(palette("P"), ("P", [0, 1, 2, 3, 4, 5, 6, 7, 8, 9])) - assert_exception(ValueError, lambda: palette("I")) - assert_exception(ValueError, lambda: palette("F")) - assert_exception(ValueError, lambda: palette("RGB")) - assert_exception(ValueError, lambda: palette("RGBA")) - assert_exception(ValueError, lambda: palette("YCbCr")) -def test_imagepalette(): - im = lena("P") - assert_no_exception(lambda: im.putpalette(ImagePalette.negative())) - assert_no_exception(lambda: im.putpalette(ImagePalette.random())) - assert_no_exception(lambda: im.putpalette(ImagePalette.sepia())) - assert_no_exception(lambda: im.putpalette(ImagePalette.wedge())) +class TestImagePutPalette(PillowTestCase): + + def test_putpalette(self): + def palette(mode): + im = lena(mode).copy() + im.putpalette(list(range(256))*3) + p = im.getpalette() + if p: + return im.mode, p[:10] + return im.mode + self.assertRaises(ValueError, lambda: palette("1")) + self.assertEqual(palette("L"), ("P", [0, 1, 2, 3, 4, 5, 6, 7, 8, 9])) + self.assertEqual(palette("P"), ("P", [0, 1, 2, 3, 4, 5, 6, 7, 8, 9])) + self.assertRaises(ValueError, lambda: palette("I")) + self.assertRaises(ValueError, lambda: palette("F")) + self.assertRaises(ValueError, lambda: palette("RGB")) + self.assertRaises(ValueError, lambda: palette("RGBA")) + self.assertRaises(ValueError, lambda: palette("YCbCr")) + + def test_imagepalette(self): + im = lena("P") + im.putpalette(ImagePalette.negative()) + im.putpalette(ImagePalette.random()) + im.putpalette(ImagePalette.sepia()) + im.putpalette(ImagePalette.wedge()) + + +if __name__ == '__main__': + unittest.main() + +# End of file diff --git a/Tests/test_image_putpixel.py b/Tests/test_image_putpixel.py index 5f19237cb..a7f5dc2bb 100644 --- a/Tests/test_image_putpixel.py +++ b/Tests/test_image_putpixel.py @@ -1,45 +1,50 @@ -from tester import * +from helper import unittest, PillowTestCase, lena from PIL import Image -Image.USE_CFFI_ACCESS=False - -def test_sanity(): - - im1 = lena() - im2 = Image.new(im1.mode, im1.size, 0) - - for y in range(im1.size[1]): - for x in range(im1.size[0]): - pos = x, y - im2.putpixel(pos, im1.getpixel(pos)) - - assert_image_equal(im1, im2) - - im2 = Image.new(im1.mode, im1.size, 0) - im2.readonly = 1 - - for y in range(im1.size[1]): - for x in range(im1.size[0]): - pos = x, y - im2.putpixel(pos, im1.getpixel(pos)) - - assert_false(im2.readonly) - assert_image_equal(im1, im2) - - im2 = Image.new(im1.mode, im1.size, 0) - - pix1 = im1.load() - pix2 = im2.load() - - for y in range(im1.size[1]): - for x in range(im1.size[0]): - pix2[x, y] = pix1[x, y] - - assert_image_equal(im1, im2) +Image.USE_CFFI_ACCESS = False +class TestImagePutPixel(PillowTestCase): + + def test_sanity(self): + + im1 = lena() + im2 = Image.new(im1.mode, im1.size, 0) + + for y in range(im1.size[1]): + for x in range(im1.size[0]): + pos = x, y + im2.putpixel(pos, im1.getpixel(pos)) + + self.assert_image_equal(im1, im2) + + im2 = Image.new(im1.mode, im1.size, 0) + im2.readonly = 1 + + for y in range(im1.size[1]): + for x in range(im1.size[0]): + pos = x, y + im2.putpixel(pos, im1.getpixel(pos)) + + self.assertFalse(im2.readonly) + self.assert_image_equal(im1, im2) + + im2 = Image.new(im1.mode, im1.size, 0) + + pix1 = im1.load() + pix2 = im2.load() + + for y in range(im1.size[1]): + for x in range(im1.size[0]): + pix2[x, y] = pix1[x, y] + + self.assert_image_equal(im1, im2) + + # see test_image_getpixel for more tests -# see test_image_getpixel for more tests +if __name__ == '__main__': + unittest.main() +# End of file diff --git a/Tests/test_image_quantize.py b/Tests/test_image_quantize.py index dbf68a25e..2cbdac225 100644 --- a/Tests/test_image_quantize.py +++ b/Tests/test_image_quantize.py @@ -1,27 +1,35 @@ -from tester import * +from helper import unittest, PillowTestCase, lena from PIL import Image -def test_sanity(): - im = lena() +class TestImageQuantize(PillowTestCase): - im = im.quantize() - assert_image(im, "P", im.size) + def test_sanity(self): + im = lena() - im = lena() - im = im.quantize(palette=lena("P")) - assert_image(im, "P", im.size) + im = im.quantize() + self.assert_image(im, "P", im.size) -def test_octree_quantize(): - im = lena() + im = lena() + im = im.quantize(palette=lena("P")) + self.assert_image(im, "P", im.size) - im = im.quantize(100, Image.FASTOCTREE) - assert_image(im, "P", im.size) + def test_octree_quantize(self): + im = lena() - assert len(im.getcolors()) == 100 + im = im.quantize(100, Image.FASTOCTREE) + self.assert_image(im, "P", im.size) -def test_rgba_quantize(): - im = lena('RGBA') - assert_no_exception(lambda: im.quantize()) - assert_exception(Exception, lambda: im.quantize(method=0)) + assert len(im.getcolors()) == 100 + + def test_rgba_quantize(self): + im = lena('RGBA') + im.quantize() + self.assertRaises(Exception, lambda: im.quantize(method=0)) + + +if __name__ == '__main__': + unittest.main() + +# End of file diff --git a/Tests/test_image_resize.py b/Tests/test_image_resize.py index 4e228a396..6c9932e45 100644 --- a/Tests/test_image_resize.py +++ b/Tests/test_image_resize.py @@ -1,12 +1,19 @@ -from tester import * +from helper import unittest, PillowTestCase, lena -from PIL import Image -def test_resize(): - def resize(mode, size): - out = lena(mode).resize(size) - assert_equal(out.mode, mode) - assert_equal(out.size, size) - for mode in "1", "P", "L", "RGB", "I", "F": - yield_test(resize, mode, (100, 100)) - yield_test(resize, mode, (200, 200)) +class TestImageResize(PillowTestCase): + + def test_resize(self): + def resize(mode, size): + out = lena(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)) + + +if __name__ == '__main__': + unittest.main() + +# End of file diff --git a/Tests/test_image_rotate.py b/Tests/test_image_rotate.py index 5e4782c87..531fdd63f 100644 --- a/Tests/test_image_rotate.py +++ b/Tests/test_image_rotate.py @@ -1,15 +1,22 @@ -from tester import * +from helper import unittest, PillowTestCase, lena -from PIL import Image -def test_rotate(): - def rotate(mode): - im = lena(mode) - out = im.rotate(45) - assert_equal(out.mode, mode) - assert_equal(out.size, im.size) # default rotate clips output - out = im.rotate(45, expand=1) - assert_equal(out.mode, mode) - assert_true(out.size != im.size) - for mode in "1", "P", "L", "RGB", "I", "F": - yield_test(rotate, mode) +class TestImageRotate(PillowTestCase): + + def test_rotate(self): + def rotate(mode): + im = lena(mode) + out = im.rotate(45) + self.assertEqual(out.mode, mode) + self.assertEqual(out.size, im.size) # default rotate clips output + out = im.rotate(45, expand=1) + self.assertEqual(out.mode, mode) + self.assertNotEqual(out.size, im.size) + for mode in "1", "P", "L", "RGB", "I", "F": + rotate(mode) + + +if __name__ == '__main__': + unittest.main() + +# End of file diff --git a/Tests/test_image_save.py b/Tests/test_image_save.py deleted file mode 100644 index 7d4b6d9b3..000000000 --- a/Tests/test_image_save.py +++ /dev/null @@ -1,5 +0,0 @@ -from tester import * - -from PIL import Image - -success() diff --git a/Tests/test_image_seek.py b/Tests/test_image_seek.py deleted file mode 100644 index 7d4b6d9b3..000000000 --- a/Tests/test_image_seek.py +++ /dev/null @@ -1,5 +0,0 @@ -from tester import * - -from PIL import Image - -success() diff --git a/Tests/test_image_show.py b/Tests/test_image_show.py deleted file mode 100644 index 7d4b6d9b3..000000000 --- a/Tests/test_image_show.py +++ /dev/null @@ -1,5 +0,0 @@ -from tester import * - -from PIL import Image - -success() diff --git a/Tests/test_image_split.py b/Tests/test_image_split.py index 07a779664..343f4bf8e 100644 --- a/Tests/test_image_split.py +++ b/Tests/test_image_split.py @@ -1,49 +1,67 @@ -from tester import * +from helper import unittest, PillowTestCase, lena from PIL import Image -def test_split(): - def split(mode): - layers = lena(mode).split() - return [(i.mode, i.size[0], i.size[1]) for i in layers] - assert_equal(split("1"), [('1', 128, 128)]) - assert_equal(split("L"), [('L', 128, 128)]) - assert_equal(split("I"), [('I', 128, 128)]) - assert_equal(split("F"), [('F', 128, 128)]) - assert_equal(split("P"), [('P', 128, 128)]) - assert_equal(split("RGB"), [('L', 128, 128), ('L', 128, 128), ('L', 128, 128)]) - assert_equal(split("RGBA"), [('L', 128, 128), ('L', 128, 128), ('L', 128, 128), ('L', 128, 128)]) - assert_equal(split("CMYK"), [('L', 128, 128), ('L', 128, 128), ('L', 128, 128), ('L', 128, 128)]) - assert_equal(split("YCbCr"), [('L', 128, 128), ('L', 128, 128), ('L', 128, 128)]) -def test_split_merge(): - def split_merge(mode): - return Image.merge(mode, lena(mode).split()) - assert_image_equal(lena("1"), split_merge("1")) - assert_image_equal(lena("L"), split_merge("L")) - assert_image_equal(lena("I"), split_merge("I")) - assert_image_equal(lena("F"), split_merge("F")) - assert_image_equal(lena("P"), split_merge("P")) - assert_image_equal(lena("RGB"), split_merge("RGB")) - assert_image_equal(lena("RGBA"), split_merge("RGBA")) - assert_image_equal(lena("CMYK"), split_merge("CMYK")) - assert_image_equal(lena("YCbCr"), split_merge("YCbCr")) +class TestImageSplit(PillowTestCase): -def test_split_open(): - codecs = dir(Image.core) + def test_split(self): + def split(mode): + layers = lena(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)]) + self.assertEqual(split("I"), [('I', 128, 128)]) + self.assertEqual(split("F"), [('F', 128, 128)]) + self.assertEqual(split("P"), [('P', 128, 128)]) + self.assertEqual( + split("RGB"), [('L', 128, 128), ('L', 128, 128), ('L', 128, 128)]) + self.assertEqual( + split("RGBA"), + [('L', 128, 128), ('L', 128, 128), + ('L', 128, 128), ('L', 128, 128)]) + self.assertEqual( + split("CMYK"), + [('L', 128, 128), ('L', 128, 128), + ('L', 128, 128), ('L', 128, 128)]) + self.assertEqual( + split("YCbCr"), + [('L', 128, 128), ('L', 128, 128), ('L', 128, 128)]) - if 'zip_encoder' in codecs: - file = tempfile("temp.png") - else: - file = tempfile("temp.pcx") + 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")) - def split_open(mode): - lena(mode).save(file) - im = Image.open(file) - return len(im.split()) - assert_equal(split_open("1"), 1) - assert_equal(split_open("L"), 1) - assert_equal(split_open("P"), 1) - assert_equal(split_open("RGB"), 3) - if 'zip_encoder' in codecs: - assert_equal(split_open("RGBA"), 4) + def test_split_open(self): + codecs = dir(Image.core) + + if 'zip_encoder' in codecs: + file = self.tempfile("temp.png") + else: + file = self.tempfile("temp.pcx") + + def split_open(mode): + lena(mode).save(file) + im = Image.open(file) + return len(im.split()) + self.assertEqual(split_open("1"), 1) + self.assertEqual(split_open("L"), 1) + self.assertEqual(split_open("P"), 1) + self.assertEqual(split_open("RGB"), 3) + if 'zip_encoder' in codecs: + self.assertEqual(split_open("RGBA"), 4) + + +if __name__ == '__main__': + unittest.main() + +# End of file diff --git a/Tests/test_image_tell.py b/Tests/test_image_tell.py deleted file mode 100644 index 7d4b6d9b3..000000000 --- a/Tests/test_image_tell.py +++ /dev/null @@ -1,5 +0,0 @@ -from tester import * - -from PIL import Image - -success() diff --git a/Tests/test_image_thumbnail.py b/Tests/test_image_thumbnail.py index 871dd1f54..ee49be43e 100644 --- a/Tests/test_image_thumbnail.py +++ b/Tests/test_image_thumbnail.py @@ -1,36 +1,43 @@ -from tester import * +from helper import unittest, PillowTestCase, lena -from PIL import Image -def test_sanity(): +class TestImageThumbnail(PillowTestCase): - im = lena() - im.thumbnail((100, 100)) + def test_sanity(self): - assert_image(im, im.mode, (100, 100)) + im = lena() + im.thumbnail((100, 100)) -def test_aspect(): + self.assert_image(im, im.mode, (100, 100)) - im = lena() - im.thumbnail((100, 100)) - assert_image(im, im.mode, (100, 100)) + def test_aspect(self): - im = lena().resize((128, 256)) - im.thumbnail((100, 100)) - assert_image(im, im.mode, (50, 100)) + im = lena() + im.thumbnail((100, 100)) + self.assert_image(im, im.mode, (100, 100)) - im = lena().resize((128, 256)) - im.thumbnail((50, 100)) - assert_image(im, im.mode, (50, 100)) + im = lena().resize((128, 256)) + im.thumbnail((100, 100)) + self.assert_image(im, im.mode, (50, 100)) - im = lena().resize((256, 128)) - im.thumbnail((100, 100)) - assert_image(im, im.mode, (100, 50)) + im = lena().resize((128, 256)) + im.thumbnail((50, 100)) + self.assert_image(im, im.mode, (50, 100)) - im = lena().resize((256, 128)) - im.thumbnail((100, 50)) - assert_image(im, im.mode, (100, 50)) + im = lena().resize((256, 128)) + im.thumbnail((100, 100)) + self.assert_image(im, im.mode, (100, 50)) - im = lena().resize((128, 128)) - im.thumbnail((100, 100)) - assert_image(im, im.mode, (100, 100)) + im = lena().resize((256, 128)) + im.thumbnail((100, 50)) + self.assert_image(im, im.mode, (100, 50)) + + im = lena().resize((128, 128)) + im.thumbnail((100, 100)) + self.assert_image(im, im.mode, (100, 100)) + + +if __name__ == '__main__': + unittest.main() + +# End of file diff --git a/Tests/test_image_tobitmap.py b/Tests/test_image_tobitmap.py index 6fb10dd53..56b5ef001 100644 --- a/Tests/test_image_tobitmap.py +++ b/Tests/test_image_tobitmap.py @@ -1,15 +1,22 @@ -from tester import * +from helper import unittest, PillowTestCase, lena, fromstring -from PIL import Image -def test_sanity(): +class TestImageToBitmap(PillowTestCase): - assert_exception(ValueError, lambda: lena().tobitmap()) - assert_no_exception(lambda: lena().convert("1").tobitmap()) + def test_sanity(self): - im1 = lena().convert("1") + self.assertRaises(ValueError, lambda: lena().tobitmap()) + lena().convert("1").tobitmap() - bitmap = im1.tobitmap() + im1 = lena().convert("1") - assert_true(isinstance(bitmap, bytes)) - assert_image_equal(im1, fromstring(bitmap)) + bitmap = im1.tobitmap() + + self.assertIsInstance(bitmap, bytes) + self.assert_image_equal(im1, fromstring(bitmap)) + + +if __name__ == '__main__': + unittest.main() + +# End of file diff --git a/Tests/test_image_tobytes.py b/Tests/test_image_tobytes.py index d42399993..6dbf7d6f2 100644 --- a/Tests/test_image_tobytes.py +++ b/Tests/test_image_tobytes.py @@ -1,7 +1,13 @@ -from tester import * +from helper import unittest, PillowTestCase, lena -from PIL import Image -def test_sanity(): - data = lena().tobytes() - assert_true(isinstance(data, bytes)) +class TestImageToBytes(PillowTestCase): + + def test_sanity(self): + data = lena().tobytes() + self.assertTrue(isinstance(data, bytes)) + +if __name__ == '__main__': + unittest.main() + +# End of file diff --git a/Tests/test_image_transform.py b/Tests/test_image_transform.py index fdee6072f..42a3d78f3 100644 --- a/Tests/test_image_transform.py +++ b/Tests/test_image_transform.py @@ -1,116 +1,142 @@ -from tester import * +from helper import unittest, PillowTestCase, lena from PIL import Image -def test_extent(): - im = lena('RGB') - (w,h) = im.size - transformed = im.transform(im.size, Image.EXTENT, - (0,0, - w//2,h//2), # ul -> lr - Image.BILINEAR) - - scaled = im.resize((w*2, h*2), Image.BILINEAR).crop((0,0,w,h)) - - assert_image_similar(transformed, scaled, 10) # undone -- precision? +class TestImageTransform(PillowTestCase): -def test_quad(): - # one simple quad transform, equivalent to scale & crop upper left quad - im = lena('RGB') - (w,h) = im.size - transformed = im.transform(im.size, Image.QUAD, - (0,0,0,h//2, - w//2,h//2,w//2,0), # ul -> ccw around quad - Image.BILINEAR) - - scaled = im.resize((w*2, h*2), Image.BILINEAR).crop((0,0,w,h)) - - assert_image_equal(transformed, scaled) + def test_sanity(self): + from PIL import ImageTransform -def test_mesh(): - # this should be a checkerboard of halfsized lenas in ul, lr - im = lena('RGBA') - (w,h) = im.size - transformed = im.transform(im.size, Image.MESH, - [((0,0,w//2,h//2), # box - (0,0,0,h, - w,h,w,0)), # ul -> ccw around quad - ((w//2,h//2,w,h), # box - (0,0,0,h, - w,h,w,0))], # ul -> ccw around quad - Image.BILINEAR) + im = Image.new("L", (100, 100)) - #transformed.save('transformed.png') + seq = tuple(range(10)) - scaled = im.resize((w//2, h//2), Image.BILINEAR) + transform = ImageTransform.AffineTransform(seq[:6]) + im.transform((100, 100), transform) + transform = ImageTransform.ExtentTransform(seq[:4]) + im.transform((100, 100), transform) + transform = ImageTransform.QuadTransform(seq[:8]) + im.transform((100, 100), transform) + transform = ImageTransform.MeshTransform([(seq[:4], seq[:8])]) + im.transform((100, 100), transform) - checker = Image.new('RGBA', im.size) - checker.paste(scaled, (0,0)) - checker.paste(scaled, (w//2,h//2)) - - assert_image_equal(transformed, checker) + def test_extent(self): + im = lena('RGB') + (w, h) = im.size + transformed = im.transform(im.size, Image.EXTENT, + (0, 0, + w//2, h//2), # ul -> lr + Image.BILINEAR) - # now, check to see that the extra area is (0,0,0,0) - blank = Image.new('RGBA', (w//2,h//2), (0,0,0,0)) + scaled = im.resize((w*2, h*2), Image.BILINEAR).crop((0, 0, w, h)) - assert_image_equal(blank, transformed.crop((w//2,0,w,h//2))) - assert_image_equal(blank, transformed.crop((0,h//2,w//2,h))) + # undone -- precision? + self.assert_image_similar(transformed, scaled, 10) -def _test_alpha_premult(op): - # create image with half white, half black, with the black half transparent. - # do op, - # there should be no darkness in the white section. - im = Image.new('RGBA', (10,10), (0,0,0,0)); - im2 = Image.new('RGBA', (5,10), (255,255,255,255)); - im.paste(im2, (0,0)) - - im = op(im, (40,10)) - im_background = Image.new('RGB', (40,10), (255,255,255)) - im_background.paste(im, (0,0), im) - - hist = im_background.histogram() - assert_equal(40*10, hist[-1]) + def test_quad(self): + # one simple quad transform, equivalent to scale & crop upper left quad + im = lena('RGB') + (w, h) = im.size + transformed = im.transform(im.size, Image.QUAD, + (0, 0, 0, h//2, + # ul -> ccw around quad: + w//2, h//2, w//2, 0), + Image.BILINEAR) - -def test_alpha_premult_resize(): + scaled = im.resize((w*2, h*2), Image.BILINEAR).crop((0, 0, w, h)) - def op (im, sz): - return im.resize(sz, Image.LINEAR) - - _test_alpha_premult(op) - -def test_alpha_premult_transform(): - - def op(im, sz): - (w,h) = im.size - return im.transform(sz, Image.EXTENT, - (0,0, - w,h), - Image.BILINEAR) + self.assert_image_equal(transformed, scaled) - _test_alpha_premult(op) + def test_mesh(self): + # this should be a checkerboard of halfsized lenas in ul, lr + im = lena('RGBA') + (w, h) = im.size + transformed = im.transform(im.size, Image.MESH, + [((0, 0, w//2, h//2), # box + (0, 0, 0, h, + w, h, w, 0)), # ul -> ccw around quad + ((w//2, h//2, w, h), # box + (0, 0, 0, h, + w, h, w, 0))], # ul -> ccw around quad + Image.BILINEAR) + + # transformed.save('transformed.png') + + scaled = im.resize((w//2, h//2), Image.BILINEAR) + + checker = Image.new('RGBA', im.size) + checker.paste(scaled, (0, 0)) + checker.paste(scaled, (w//2, h//2)) + + self.assert_image_equal(transformed, checker) + + # now, check to see that the extra area is (0, 0, 0, 0) + blank = Image.new('RGBA', (w//2, h//2), (0, 0, 0, 0)) + + self.assert_image_equal(blank, transformed.crop((w//2, 0, w, h//2))) + self.assert_image_equal(blank, transformed.crop((0, h//2, w//2, h))) + + def _test_alpha_premult(self, op): + # create image with half white, half black, + # with the black half transparent. + # do op, + # there should be no darkness in the white section. + im = Image.new('RGBA', (10, 10), (0, 0, 0, 0)) + im2 = Image.new('RGBA', (5, 10), (255, 255, 255, 255)) + im.paste(im2, (0, 0)) + + im = op(im, (40, 10)) + im_background = Image.new('RGB', (40, 10), (255, 255, 255)) + im_background.paste(im, (0, 0), im) + + hist = im_background.histogram() + self.assertEqual(40*10, hist[-1]) + + def test_alpha_premult_resize(self): + + def op(im, sz): + return im.resize(sz, Image.LINEAR) + + self._test_alpha_premult(op) + + def test_alpha_premult_transform(self): + + def op(im, sz): + (w, h) = im.size + return im.transform(sz, Image.EXTENT, + (0, 0, + w, h), + Image.BILINEAR) + + self._test_alpha_premult(op) + + def test_blank_fill(self): + # attempting to hit + # https://github.com/python-pillow/Pillow/issues/254 reported + # + # issue is that transforms with transparent overflow area + # contained junk from previous images, especially on systems with + # constrained memory. So, attempt to fill up memory with a + # pattern, free it, and then run the mesh test again. Using a 1Mp + # image with 4 bands, for 4 megs of data allocated, x 64. OMM (64 + # bit 12.04 VM with 512 megs available, this fails with Pillow < + # a0eaf06cc5f62a6fb6de556989ac1014ff3348ea + # + # Running by default, but I'd totally understand not doing it in + # the future + + foo = [ + Image.new('RGBA', (1024, 1024), (a, a, a, a)) + for a in range(1, 65)] + + # Yeah. Watch some JIT optimize this out. + foo = None + + self.test_mesh() -def test_blank_fill(): - # attempting to hit - # https://github.com/python-imaging/Pillow/issues/254 reported - # - # issue is that transforms with transparent overflow area - # contained junk from previous images, especially on systems with - # constrained memory. So, attempt to fill up memory with a - # pattern, free it, and then run the mesh test again. Using a 1Mp - # image with 4 bands, for 4 megs of data allocated, x 64. OMM (64 - # bit 12.04 VM with 512 megs available, this fails with Pillow < - # a0eaf06cc5f62a6fb6de556989ac1014ff3348ea - # - # Running by default, but I'd totally understand not doing it in - # the future - - foo = [Image.new('RGBA',(1024,1024), (a,a,a,a)) - for a in range(1,65)] +if __name__ == '__main__': + unittest.main() - # Yeah. Watch some JIT optimize this out. - foo = None - - test_mesh() +# End of file diff --git a/Tests/test_image_transpose.py b/Tests/test_image_transpose.py index 43b3ef9d3..f13e54ee7 100644 --- a/Tests/test_image_transpose.py +++ b/Tests/test_image_transpose.py @@ -1,4 +1,4 @@ -from tester import * +from helper import unittest, PillowTestCase, lena from PIL import Image @@ -8,27 +8,37 @@ ROTATE_90 = Image.ROTATE_90 ROTATE_180 = Image.ROTATE_180 ROTATE_270 = Image.ROTATE_270 -def test_sanity(): - im = lena() +class TestImageTranspose(PillowTestCase): - assert_no_exception(lambda: im.transpose(FLIP_LEFT_RIGHT)) - assert_no_exception(lambda: im.transpose(FLIP_TOP_BOTTOM)) + def test_sanity(self): - assert_no_exception(lambda: im.transpose(ROTATE_90)) - assert_no_exception(lambda: im.transpose(ROTATE_180)) - assert_no_exception(lambda: im.transpose(ROTATE_270)) + im = lena() -def test_roundtrip(): + im.transpose(FLIP_LEFT_RIGHT) + im.transpose(FLIP_TOP_BOTTOM) - im = lena() + im.transpose(ROTATE_90) + im.transpose(ROTATE_180) + im.transpose(ROTATE_270) - def transpose(first, second): - return im.transpose(first).transpose(second) + def test_roundtrip(self): - assert_image_equal(im, transpose(FLIP_LEFT_RIGHT, FLIP_LEFT_RIGHT)) - assert_image_equal(im, transpose(FLIP_TOP_BOTTOM, FLIP_TOP_BOTTOM)) + im = lena() - assert_image_equal(im, transpose(ROTATE_90, ROTATE_270)) - assert_image_equal(im, transpose(ROTATE_180, ROTATE_180)) + def transpose(first, second): + return im.transpose(first).transpose(second) + self.assert_image_equal( + im, transpose(FLIP_LEFT_RIGHT, FLIP_LEFT_RIGHT)) + self.assert_image_equal( + im, transpose(FLIP_TOP_BOTTOM, FLIP_TOP_BOTTOM)) + + self.assert_image_equal(im, transpose(ROTATE_90, ROTATE_270)) + self.assert_image_equal(im, transpose(ROTATE_180, ROTATE_180)) + + +if __name__ == '__main__': + unittest.main() + +# End of file diff --git a/Tests/test_image_verify.py b/Tests/test_image_verify.py deleted file mode 100644 index 7d4b6d9b3..000000000 --- a/Tests/test_image_verify.py +++ /dev/null @@ -1,5 +0,0 @@ -from tester import * - -from PIL import Image - -success() diff --git a/Tests/test_imagechops.py b/Tests/test_imagechops.py index 16eaaf55e..552314fd1 100644 --- a/Tests/test_imagechops.py +++ b/Tests/test_imagechops.py @@ -1,56 +1,74 @@ -from tester import * +from helper import unittest, PillowTestCase, lena from PIL import Image from PIL import ImageChops -def test_sanity(): - im = lena("L") +class TestImageChops(PillowTestCase): - ImageChops.constant(im, 128) - ImageChops.duplicate(im) - ImageChops.invert(im) - ImageChops.lighter(im, im) - ImageChops.darker(im, im) - ImageChops.difference(im, im) - ImageChops.multiply(im, im) - ImageChops.screen(im, im) + def test_sanity(self): - ImageChops.add(im, im) - ImageChops.add(im, im, 2.0) - ImageChops.add(im, im, 2.0, 128) - ImageChops.subtract(im, im) - ImageChops.subtract(im, im, 2.0) - ImageChops.subtract(im, im, 2.0, 128) + im = lena("L") - ImageChops.add_modulo(im, im) - ImageChops.subtract_modulo(im, im) + ImageChops.constant(im, 128) + ImageChops.duplicate(im) + ImageChops.invert(im) + ImageChops.lighter(im, im) + ImageChops.darker(im, im) + ImageChops.difference(im, im) + ImageChops.multiply(im, im) + ImageChops.screen(im, im) - ImageChops.blend(im, im, 0.5) - ImageChops.composite(im, im, im) + ImageChops.add(im, im) + ImageChops.add(im, im, 2.0) + ImageChops.add(im, im, 2.0, 128) + ImageChops.subtract(im, im) + ImageChops.subtract(im, im, 2.0) + ImageChops.subtract(im, im, 2.0, 128) - ImageChops.offset(im, 10) - ImageChops.offset(im, 10, 20) + ImageChops.add_modulo(im, im) + ImageChops.subtract_modulo(im, im) -def test_logical(): + ImageChops.blend(im, im, 0.5) + ImageChops.composite(im, im, im) - def table(op, a, b): - out = [] - for x in (a, b): - imx = Image.new("1", (1, 1), x) - for y in (a, b): - imy = Image.new("1", (1, 1), y) - out.append(op(imx, imy).getpixel((0, 0))) - return tuple(out) + ImageChops.offset(im, 10) + ImageChops.offset(im, 10, 20) - assert_equal(table(ImageChops.logical_and, 0, 1), (0, 0, 0, 255)) - assert_equal(table(ImageChops.logical_or, 0, 1), (0, 255, 255, 255)) - assert_equal(table(ImageChops.logical_xor, 0, 1), (0, 255, 255, 0)) + def test_logical(self): - assert_equal(table(ImageChops.logical_and, 0, 128), (0, 0, 0, 255)) - assert_equal(table(ImageChops.logical_or, 0, 128), (0, 255, 255, 255)) - assert_equal(table(ImageChops.logical_xor, 0, 128), (0, 255, 255, 0)) + def table(op, a, b): + out = [] + for x in (a, b): + imx = Image.new("1", (1, 1), x) + for y in (a, b): + imy = Image.new("1", (1, 1), y) + out.append(op(imx, imy).getpixel((0, 0))) + return tuple(out) - assert_equal(table(ImageChops.logical_and, 0, 255), (0, 0, 0, 255)) - assert_equal(table(ImageChops.logical_or, 0, 255), (0, 255, 255, 255)) - assert_equal(table(ImageChops.logical_xor, 0, 255), (0, 255, 255, 0)) + self.assertEqual( + table(ImageChops.logical_and, 0, 1), (0, 0, 0, 255)) + self.assertEqual( + table(ImageChops.logical_or, 0, 1), (0, 255, 255, 255)) + self.assertEqual( + table(ImageChops.logical_xor, 0, 1), (0, 255, 255, 0)) + + self.assertEqual( + table(ImageChops.logical_and, 0, 128), (0, 0, 0, 255)) + self.assertEqual( + table(ImageChops.logical_or, 0, 128), (0, 255, 255, 255)) + self.assertEqual( + table(ImageChops.logical_xor, 0, 128), (0, 255, 255, 0)) + + self.assertEqual( + table(ImageChops.logical_and, 0, 255), (0, 0, 0, 255)) + self.assertEqual( + table(ImageChops.logical_or, 0, 255), (0, 255, 255, 255)) + self.assertEqual( + table(ImageChops.logical_xor, 0, 255), (0, 255, 255, 0)) + + +if __name__ == '__main__': + unittest.main() + +# End of file diff --git a/Tests/test_imagecms.py b/Tests/test_imagecms.py index dcb445c9f..e731c8945 100644 --- a/Tests/test_imagecms.py +++ b/Tests/test_imagecms.py @@ -1,160 +1,245 @@ -from tester import * +from helper import unittest, PillowTestCase, lena from PIL import Image + +from io import BytesIO + try: from PIL import ImageCms + from PIL.ImageCms import ImageCmsProfile ImageCms.core.profile_open -except ImportError: - skip() +except ImportError as v: + # Skipped via setUp() + pass + SRGB = "Tests/icc/sRGB.icm" -def test_sanity(): - # basic smoke test. - # this mostly follows the cms_test outline. +class TestImageCms(PillowTestCase): - v = ImageCms.versions() # should return four strings - assert_equal(v[0], '1.0.0 pil') - assert_equal(list(map(type, v)), [str, str, str, str]) + def setUp(self): + try: + from PIL import ImageCms + # need to hit getattr to trigger the delayed import error + ImageCms.core.profile_open + except ImportError as v: + self.skipTest(v) - # internal version number - assert_match(ImageCms.core.littlecms_version, "\d+\.\d+$") + def test_sanity(self): - i = ImageCms.profileToProfile(lena(), SRGB, SRGB) - assert_image(i, "RGB", (128, 128)) + # basic smoke test. + # this mostly follows the cms_test outline. - t = ImageCms.buildTransform(SRGB, SRGB, "RGB", "RGB") - i = ImageCms.applyTransform(lena(), t) - assert_image(i, "RGB", (128, 128)) + v = ImageCms.versions() # should return four strings + self.assertEqual(v[0], '1.0.0 pil') + self.assertEqual(list(map(type, v)), [str, str, str, str]) - p = ImageCms.createProfile("sRGB") - o = ImageCms.getOpenProfile(SRGB) - t = ImageCms.buildTransformFromOpenProfiles(p, o, "RGB", "RGB") - i = ImageCms.applyTransform(lena(), t) - assert_image(i, "RGB", (128, 128)) + # internal version number + self.assertRegexpMatches(ImageCms.core.littlecms_version, "\d+\.\d+$") - t = ImageCms.buildProofTransform(SRGB, SRGB, SRGB, "RGB", "RGB") - assert_equal(t.inputMode, "RGB") - assert_equal(t.outputMode, "RGB") - i = ImageCms.applyTransform(lena(), t) - assert_image(i, "RGB", (128, 128)) + i = ImageCms.profileToProfile(lena(), SRGB, SRGB) + self.assert_image(i, "RGB", (128, 128)) - # test PointTransform convenience API - im = lena().point(t) + i = lena() + ImageCms.profileToProfile(i, SRGB, SRGB, inPlace=True) + self.assert_image(i, "RGB", (128, 128)) -def test_name(): - # get profile information for file - assert_equal(ImageCms.getProfileName(SRGB).strip(), - 'IEC 61966-2.1 Default RGB colour space - sRGB') -def x_test_info(): - assert_equal(ImageCms.getProfileInfo(SRGB).splitlines(), - ['sRGB IEC61966-2.1', '', - 'Copyright (c) 1998 Hewlett-Packard Company', '', - 'WhitePoint : D65 (daylight)', '', - 'Tests/icc/sRGB.icm']) + t = ImageCms.buildTransform(SRGB, SRGB, "RGB", "RGB") + i = ImageCms.applyTransform(lena(), t) + self.assert_image(i, "RGB", (128, 128)) -def test_intent(): - assert_equal(ImageCms.getDefaultIntent(SRGB), 0) - assert_equal(ImageCms.isIntentSupported( + i = lena() + t = ImageCms.buildTransform(SRGB, SRGB, "RGB", "RGB") + ImageCms.applyTransform(lena(), 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) + 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) + self.assert_image(i, "RGB", (128, 128)) + + # test PointTransform convenience API + lena().point(t) + + def test_name(self): + # get profile information for file + self.assertEqual( + ImageCms.getProfileName(SRGB).strip(), + 'IEC 61966-2.1 Default RGB colour space - sRGB') + + def test_info(self): + self.assertEqual( + ImageCms.getProfileInfo(SRGB).splitlines(), [ + 'sRGB IEC61966-2.1', '', + 'Copyright (c) 1998 Hewlett-Packard Company', '']) + + def test_copyright(self): + self.assertEqual( + ImageCms.getProfileCopyright(SRGB).strip(), + 'Copyright (c) 1998 Hewlett-Packard Company') + + def test_manufacturer(self): + self.assertEqual( + ImageCms.getProfileManufacturer(SRGB).strip(), + 'IEC http://www.iec.ch') + + def test_model(self): + self.assertEqual( + ImageCms.getProfileModel(SRGB).strip(), + 'IEC 61966-2.1 Default RGB colour space - sRGB') + + def test_description(self): + self.assertEqual( + ImageCms.getProfileDescription(SRGB).strip(), + 'sRGB IEC61966-2.1') + + def test_intent(self): + self.assertEqual(ImageCms.getDefaultIntent(SRGB), 0) + self.assertEqual(ImageCms.isIntentSupported( SRGB, ImageCms.INTENT_ABSOLUTE_COLORIMETRIC, ImageCms.DIRECTION_INPUT), 1) -def test_profile_object(): - # same, using profile object - p = ImageCms.createProfile("sRGB") -# assert_equal(ImageCms.getProfileName(p).strip(), -# 'sRGB built-in - (lcms internal)') -# assert_equal(ImageCms.getProfileInfo(p).splitlines(), -# ['sRGB built-in', '', 'WhitePoint : D65 (daylight)', '', '']) - assert_equal(ImageCms.getDefaultIntent(p), 0) - assert_equal(ImageCms.isIntentSupported( + def test_profile_object(self): + # same, using profile object + p = ImageCms.createProfile("sRGB") + # self.assertEqual(ImageCms.getProfileName(p).strip(), + # 'sRGB built-in - (lcms internal)') + # self.assertEqual(ImageCms.getProfileInfo(p).splitlines(), + # ['sRGB built-in', '', 'WhitePoint : D65 (daylight)', '', '']) + self.assertEqual(ImageCms.getDefaultIntent(p), 0) + self.assertEqual(ImageCms.isIntentSupported( p, ImageCms.INTENT_ABSOLUTE_COLORIMETRIC, ImageCms.DIRECTION_INPUT), 1) -def test_extensions(): - # extensions - i = Image.open("Tests/images/rgb.jpg") - p = ImageCms.getOpenProfile(BytesIO(i.info["icc_profile"])) - assert_equal(ImageCms.getProfileName(p).strip(), - 'IEC 61966-2.1 Default RGB colour space - sRGB') + def test_extensions(self): + # extensions -def test_exceptions(): - # the procedural pyCMS API uses PyCMSError for all sorts of errors - assert_exception(ImageCms.PyCMSError, lambda: ImageCms.profileToProfile(lena(), "foo", "bar")) - assert_exception(ImageCms.PyCMSError, lambda: ImageCms.buildTransform("foo", "bar", "RGB", "RGB")) - assert_exception(ImageCms.PyCMSError, lambda: ImageCms.getProfileName(None)) - assert_exception(ImageCms.PyCMSError, lambda: ImageCms.isIntentSupported(SRGB, None, None)) + i = Image.open("Tests/images/rgb.jpg") + p = ImageCms.getOpenProfile(BytesIO(i.info["icc_profile"])) + self.assertEqual( + ImageCms.getProfileName(p).strip(), + 'IEC 61966-2.1 Default RGB colour space - sRGB') + + def test_exceptions(self): + # the procedural pyCMS API uses PyCMSError for all sorts of errors + self.assertRaises( + ImageCms.PyCMSError, + lambda: ImageCms.profileToProfile(lena(), "foo", "bar")) + self.assertRaises( + ImageCms.PyCMSError, + lambda: ImageCms.buildTransform("foo", "bar", "RGB", "RGB")) + self.assertRaises( + ImageCms.PyCMSError, + lambda: ImageCms.getProfileName(None)) + self.assertRaises( + ImageCms.PyCMSError, + lambda: ImageCms.isIntentSupported(SRGB, None, None)) + + def test_display_profile(self): + # try fetching the profile for the current display device + ImageCms.get_display_profile() + + def test_lab_color_profile(self): + ImageCms.createProfile("LAB", 5000) + ImageCms.createProfile("LAB", 6500) + + def test_simple_lab(self): + i = Image.new('RGB', (10, 10), (128, 128, 128)) + + pLab = ImageCms.createProfile("LAB") + t = ImageCms.buildTransform(SRGB, pLab, "RGB", "LAB") + + i_lab = ImageCms.applyTransform(i, t) + + self.assertEqual(i_lab.mode, 'LAB') + + k = i_lab.getpixel((0, 0)) + # not a linear luminance map. so L != 128: + self.assertEqual(k, (137, 128, 128)) + + L = i_lab.getdata(0) + a = i_lab.getdata(1) + b = i_lab.getdata(2) + + self.assertEqual(list(L), [137] * 100) + self.assertEqual(list(a), [128] * 100) + self.assertEqual(list(b), [128] * 100) + + def test_lab_color(self): + pLab = ImageCms.createProfile("LAB") + t = ImageCms.buildTransform(SRGB, 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) + self.assert_image(i, "LAB", (128, 128)) + + # i.save('temp.lab.tif') # visually verified vs PS. + + target = Image.open('Tests/images/lena.Lab.tif') + + self.assert_image_similar(i, target, 30) + + def test_lab_srgb(self): + pLab = ImageCms.createProfile("LAB") + t = ImageCms.buildTransform(pLab, SRGB, "LAB", "RGB") + + img = Image.open('Tests/images/lena.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.assertTrue(img_srgb.info['icc_profile']) + + profile = ImageCmsProfile(BytesIO(img_srgb.info['icc_profile'])) + self.assertTrue('sRGB' in ImageCms.getProfileDescription(profile)) -def test_display_profile(): - # try fetching the profile for the current display device - assert_no_exception(lambda: ImageCms.get_display_profile()) + def test_lab_roundtrip(self): + # check to see if we're at least internally consistent. + pLab = ImageCms.createProfile("LAB") + t = ImageCms.buildTransform(SRGB, pLab, "RGB", "LAB") + + t2 = ImageCms.buildTransform(pLab, SRGB, "LAB", "RGB") + + i = ImageCms.applyTransform(lena(), t) + + self.assertEqual(i.info['icc_profile'], + ImageCmsProfile(pLab).tobytes()) + + out = ImageCms.applyTransform(i, t2) + + self.assert_image_similar(lena(), out, 2) -def test_lab_color_profile(): - pLab = ImageCms.createProfile("LAB", 5000) - pLab = ImageCms.createProfile("LAB", 6500) + def test_profile_tobytes(self): + from io import BytesIO + i = Image.open("Tests/images/rgb.jpg") + p = ImageCms.getOpenProfile(BytesIO(i.info["icc_profile"])) -def test_simple_lab(): - i = Image.new('RGB', (10,10), (128,128,128)) + p2 = ImageCms.getOpenProfile(BytesIO(p.tobytes())) - pLab = ImageCms.createProfile("LAB") - t = ImageCms.buildTransform(SRGB, pLab, "RGB", "LAB") - - i_lab = ImageCms.applyTransform(i, t) - - - assert_equal(i_lab.mode, 'LAB') - - k = i_lab.getpixel((0,0)) - assert_equal(k, (137,128,128)) # not a linear luminance map. so L != 128 - - L = i_lab.getdata(0) - a = i_lab.getdata(1) - b = i_lab.getdata(2) - - assert_equal(list(L), [137]*100) - assert_equal(list(a), [128]*100) - assert_equal(list(b), [128]*100) - - -def test_lab_color(): - pLab = ImageCms.createProfile("LAB") - t = ImageCms.buildTransform(SRGB, 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) - assert_image(i, "LAB", (128, 128)) - - # i.save('temp.lab.tif') # visually verified vs PS. - - target = Image.open('Tests/images/lena.Lab.tif') - - assert_image_similar(i, target, 30) - -def test_lab_srgb(): - pLab = ImageCms.createProfile("LAB") - t = ImageCms.buildTransform(pLab, SRGB, "LAB", "RGB") - - img = Image.open('Tests/images/lena.Lab.tif') - - img_srgb = ImageCms.applyTransform(img, t) - - # img_srgb.save('temp.srgb.tif') # visually verified vs ps. - - assert_image_similar(lena(), img_srgb, 30) - -def test_lab_roundtrip(): - # check to see if we're at least internally consistent. - pLab = ImageCms.createProfile("LAB") - t = ImageCms.buildTransform(SRGB, pLab, "RGB", "LAB") - - t2 = ImageCms.buildTransform(pLab, SRGB, "LAB", "RGB") - - i = ImageCms.applyTransform(lena(), t) - out = ImageCms.applyTransform(i, t2) - - assert_image_similar(lena(), out, 2) + # not the same bytes as the original icc_profile, + # but it does roundtrip + 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() +# End of file diff --git a/Tests/test_imagecolor.py b/Tests/test_imagecolor.py index c67c20255..5d8944852 100644 --- a/Tests/test_imagecolor.py +++ b/Tests/test_imagecolor.py @@ -1,54 +1,71 @@ -from tester import * +from helper import unittest, PillowTestCase from PIL import Image from PIL import ImageColor -# -------------------------------------------------------------------- -# sanity -assert_equal((255, 0, 0), ImageColor.getrgb("#f00")) -assert_equal((255, 0, 0), ImageColor.getrgb("#ff0000")) -assert_equal((255, 0, 0), ImageColor.getrgb("rgb(255,0,0)")) -assert_equal((255, 0, 0), ImageColor.getrgb("rgb(255, 0, 0)")) -assert_equal((255, 0, 0), ImageColor.getrgb("rgb(100%,0%,0%)")) -assert_equal((255, 0, 0), ImageColor.getrgb("hsl(0, 100%, 50%)")) -assert_equal((255, 0, 0, 0), ImageColor.getrgb("rgba(255,0,0,0)")) -assert_equal((255, 0, 0, 0), ImageColor.getrgb("rgba(255, 0, 0, 0)")) -assert_equal((255, 0, 0), ImageColor.getrgb("red")) +class TestImageColor(PillowTestCase): -# -------------------------------------------------------------------- -# look for rounding errors (based on code by Tim Hatch) + def test_sanity(self): + self.assertEqual((255, 0, 0), ImageColor.getrgb("#f00")) + self.assertEqual((255, 0, 0), ImageColor.getrgb("#ff0000")) + self.assertEqual((255, 0, 0), ImageColor.getrgb("rgb(255,0,0)")) + self.assertEqual((255, 0, 0), ImageColor.getrgb("rgb(255, 0, 0)")) + self.assertEqual((255, 0, 0), ImageColor.getrgb("rgb(100%,0%,0%)")) + self.assertEqual((255, 0, 0), ImageColor.getrgb("hsl(0, 100%, 50%)")) + self.assertEqual((255, 0, 0, 0), ImageColor.getrgb("rgba(255,0,0,0)")) + self.assertEqual( + (255, 0, 0, 0), ImageColor.getrgb("rgba(255, 0, 0, 0)")) + self.assertEqual((255, 0, 0), ImageColor.getrgb("red")) -for color in list(ImageColor.colormap.keys()): - expected = Image.new("RGB", (1, 1), color).convert("L").getpixel((0, 0)) - actual = Image.new("L", (1, 1), color).getpixel((0, 0)) - assert_equal(expected, actual) + # look for rounding errors (based on code by Tim Hatch) + def test_rounding_errors(self): -assert_equal((0, 0, 0), ImageColor.getcolor("black", "RGB")) -assert_equal((255, 255, 255), ImageColor.getcolor("white", "RGB")) -assert_equal((0, 255, 115), ImageColor.getcolor("rgba(0, 255, 115, 33)", "RGB")) -Image.new("RGB", (1, 1), "white") + for color in list(ImageColor.colormap.keys()): + expected = Image.new( + "RGB", (1, 1), color).convert("L").getpixel((0, 0)) + actual = Image.new("L", (1, 1), color).getpixel((0, 0)) + self.assertEqual(expected, actual) -assert_equal((0, 0, 0, 255), ImageColor.getcolor("black", "RGBA")) -assert_equal((255, 255, 255, 255), ImageColor.getcolor("white", "RGBA")) -assert_equal((0, 255, 115, 33), ImageColor.getcolor("rgba(0, 255, 115, 33)", "RGBA")) -Image.new("RGBA", (1, 1), "white") + self.assertEqual((0, 0, 0), ImageColor.getcolor("black", "RGB")) + self.assertEqual((255, 255, 255), ImageColor.getcolor("white", "RGB")) + self.assertEqual( + (0, 255, 115), ImageColor.getcolor("rgba(0, 255, 115, 33)", "RGB")) + Image.new("RGB", (1, 1), "white") -assert_equal(0, ImageColor.getcolor("black", "L")) -assert_equal(255, ImageColor.getcolor("white", "L")) -assert_equal(162, ImageColor.getcolor("rgba(0, 255, 115, 33)", "L")) -Image.new("L", (1, 1), "white") + self.assertEqual((0, 0, 0, 255), ImageColor.getcolor("black", "RGBA")) + self.assertEqual( + (255, 255, 255, 255), ImageColor.getcolor("white", "RGBA")) + self.assertEqual( + (0, 255, 115, 33), + ImageColor.getcolor("rgba(0, 255, 115, 33)", "RGBA")) + Image.new("RGBA", (1, 1), "white") -assert_equal(0, ImageColor.getcolor("black", "1")) -assert_equal(255, ImageColor.getcolor("white", "1")) -# The following test is wrong, but is current behavior -# The correct result should be 255 due to the mode 1 -assert_equal(162, ImageColor.getcolor("rgba(0, 255, 115, 33)", "1")) -# Correct behavior -# assert_equal(255, ImageColor.getcolor("rgba(0, 255, 115, 33)", "1")) -Image.new("1", (1, 1), "white") + self.assertEqual(0, ImageColor.getcolor("black", "L")) + self.assertEqual(255, ImageColor.getcolor("white", "L")) + self.assertEqual( + 162, ImageColor.getcolor("rgba(0, 255, 115, 33)", "L")) + Image.new("L", (1, 1), "white") -assert_equal((0, 255), ImageColor.getcolor("black", "LA")) -assert_equal((255, 255), ImageColor.getcolor("white", "LA")) -assert_equal((162, 33), ImageColor.getcolor("rgba(0, 255, 115, 33)", "LA")) -Image.new("LA", (1, 1), "white") + self.assertEqual(0, ImageColor.getcolor("black", "1")) + self.assertEqual(255, ImageColor.getcolor("white", "1")) + # The following test is wrong, but is current behavior + # The correct result should be 255 due to the mode 1 + self.assertEqual( + 162, ImageColor.getcolor("rgba(0, 255, 115, 33)", "1")) + # Correct behavior + # self.assertEqual( + # 255, ImageColor.getcolor("rgba(0, 255, 115, 33)", "1")) + Image.new("1", (1, 1), "white") + + self.assertEqual((0, 255), ImageColor.getcolor("black", "LA")) + self.assertEqual((255, 255), ImageColor.getcolor("white", "LA")) + self.assertEqual( + (162, 33), ImageColor.getcolor("rgba(0, 255, 115, 33)", "LA")) + Image.new("LA", (1, 1), "white") + + +if __name__ == '__main__': + unittest.main() + +# End of file diff --git a/Tests/test_imagedraw.py b/Tests/test_imagedraw.py index f8b5c3c5c..b632da73b 100644 --- a/Tests/test_imagedraw.py +++ b/Tests/test_imagedraw.py @@ -1,28 +1,384 @@ -from tester import * +from helper import unittest, PillowTestCase, lena from PIL import Image +from PIL import ImageColor from PIL import ImageDraw +import os.path -def test_sanity(): +BLACK = (0, 0, 0) +WHITE = (255, 255, 255) +GRAY = (190, 190, 190) +DEFAULT_MODE = 'RGB' +IMAGES_PATH = os.path.join('Tests', 'images', 'imagedraw') - im = lena("RGB").copy() +import sys - draw = ImageDraw.ImageDraw(im) - draw = ImageDraw.Draw(im) +# Image size +W, H = 100, 100 - draw.ellipse(list(range(4))) - draw.line(list(range(10))) - draw.polygon(list(range(100))) - draw.rectangle(list(range(4))) +# Bounding box points +X0 = int(W / 4) +X1 = int(X0 * 3) +Y0 = int(H / 4) +Y1 = int(X0 * 3) - success() +# Two kinds of bounding box +BBOX1 = [(X0, Y0), (X1, Y1)] +BBOX2 = [X0, Y0, X1, Y1] -def test_deprecated(): +# Two kinds of coordinate sequences +POINTS1 = [(10, 10), (20, 40), (30, 30)] +POINTS2 = [10, 10, 20, 40, 30, 30] - im = lena().copy() - draw = ImageDraw.Draw(im) +class TestImageDraw(PillowTestCase): - assert_warning(DeprecationWarning, lambda: draw.setink(0)) - assert_warning(DeprecationWarning, lambda: draw.setfill(0)) + def test_sanity(self): + im = lena("RGB").copy() + + draw = ImageDraw.ImageDraw(im) + draw = ImageDraw.Draw(im) + + draw.ellipse(list(range(4))) + draw.line(list(range(10))) + draw.polygon(list(range(100))) + draw.rectangle(list(range(4))) + + def test_deprecated(self): + im = lena().copy() + + draw = ImageDraw.Draw(im) + + self.assert_warning(DeprecationWarning, lambda: draw.setink(0)) + self.assert_warning(DeprecationWarning, lambda: draw.setfill(0)) + + def helper_arc(self, bbox): + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw.Draw(im) + + # Act + # FIXME Fill param should be named outline. + draw.arc(bbox, 0, 180) + del draw + + # Assert + self.assert_image_equal( + im, Image.open("Tests/images/imagedraw_arc.png")) + + def test_arc1(self): + self.helper_arc(BBOX1) + + def test_arc2(self): + self.helper_arc(BBOX2) + + def test_bitmap(self): + # Arrange + small = Image.open("Tests/images/pil123rgba.png").resize((50, 50)) + im = Image.new("RGB", (W, H)) + draw = ImageDraw.Draw(im) + + # Act + draw.bitmap((10, 10), small) + del draw + + # Assert + self.assert_image_equal( + im, Image.open("Tests/images/imagedraw_bitmap.png")) + + def helper_chord(self, bbox): + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw.Draw(im) + + # Act + draw.chord(bbox, 0, 180, fill="red", outline="yellow") + del draw + + # Assert + self.assert_image_equal( + im, Image.open("Tests/images/imagedraw_chord.png")) + + def test_chord1(self): + self.helper_chord(BBOX1) + + def test_chord2(self): + self.helper_chord(BBOX2) + + def helper_ellipse(self, bbox): + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw.Draw(im) + + # Act + draw.ellipse(bbox, fill="green", outline="blue") + del draw + + # Assert + self.assert_image_equal( + im, Image.open("Tests/images/imagedraw_ellipse.png")) + + def test_ellipse1(self): + self.helper_ellipse(BBOX1) + + def test_ellipse2(self): + self.helper_ellipse(BBOX2) + + def helper_line(self, points): + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw.Draw(im) + + # Act + draw.line(points, fill="yellow", width=2) + del draw + + # Assert + self.assert_image_equal( + im, Image.open("Tests/images/imagedraw_line.png")) + + def test_line1(self): + self.helper_line(POINTS1) + + def test_line2(self): + self.helper_line(POINTS2) + + def helper_pieslice(self, bbox): + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw.Draw(im) + + # Act + draw.pieslice(bbox, -90, 45, fill="white", outline="blue") + del draw + + # Assert + self.assert_image_equal( + im, Image.open("Tests/images/imagedraw_pieslice.png")) + + def test_pieslice1(self): + self.helper_pieslice(BBOX1) + + def test_pieslice2(self): + self.helper_pieslice(BBOX2) + + def helper_point(self, points): + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw.Draw(im) + + # Act + draw.point(points, fill="yellow") + del draw + + # Assert + self.assert_image_equal( + im, Image.open("Tests/images/imagedraw_point.png")) + + def test_point1(self): + self.helper_point(POINTS1) + + def test_point2(self): + self.helper_point(POINTS2) + + def helper_polygon(self, points): + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw.Draw(im) + + # Act + draw.polygon(points, fill="red", outline="blue") + del draw + + # Assert + self.assert_image_equal( + im, Image.open("Tests/images/imagedraw_polygon.png")) + + def test_polygon1(self): + self.helper_polygon(POINTS1) + + def test_polygon2(self): + self.helper_polygon(POINTS2) + + def helper_rectangle(self, bbox): + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw.Draw(im) + + # Act + draw.rectangle(bbox, fill="black", outline="green") + del draw + + # Assert + self.assert_image_equal( + im, Image.open("Tests/images/imagedraw_rectangle.png")) + + def test_rectangle1(self): + self.helper_rectangle(BBOX1) + + def test_rectangle2(self): + self.helper_rectangle(BBOX2) + + def test_floodfill(self): + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw.Draw(im) + draw.rectangle(BBOX2, outline="yellow", fill="green") + centre_point = (int(W/2), int(H/2)) + + # Act + ImageDraw.floodfill(im, centre_point, ImageColor.getrgb("red")) + del draw + + # Assert + self.assert_image_equal( + im, Image.open("Tests/images/imagedraw_floodfill.png")) + + @unittest.skipIf(hasattr(sys, 'pypy_version_info'), + "Causes fatal RPython error on PyPy") + def test_floodfill_border(self): + # floodfill() is experimental + + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw.Draw(im) + draw.rectangle(BBOX2, outline="yellow", fill="green") + centre_point = (int(W/2), int(H/2)) + + # Act + ImageDraw.floodfill( + im, centre_point, ImageColor.getrgb("red"), + border=ImageColor.getrgb("black")) + del draw + + # Assert + self.assert_image_equal( + im, Image.open("Tests/images/imagedraw_floodfill2.png")) + + + def create_base_image_draw(self, size, + mode=DEFAULT_MODE, + background1=WHITE, + background2=GRAY): + img = Image.new(mode, size, background1) + for x in range(0, size[0]): + for y in range(0, size[1]): + if (x + y) % 2 == 0: + 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') + 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') + 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') + 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') + + + def test_triangle_right(self): + expected = Image.open(os.path.join(IMAGES_PATH, 'triangle_right.png')) + expected.load() + img, draw = self.create_base_image_draw((20, 20)) + 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.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')) + 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')) + 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') + 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')) + 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') + + def test_line_h_s1_w2(self): + self.skipTest('failing') + 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') + + + def test_line_vertical(self): + 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')) + 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')) + 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') + 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')) + 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')) + 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') + + + def test_line_oblique_45(self): + 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') + 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')) + 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') + 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') + + +if __name__ == '__main__': + unittest.main() + +# End of file diff --git a/Tests/test_imageenhance.py b/Tests/test_imageenhance.py index 04f16bfa5..433c49cf6 100644 --- a/Tests/test_imageenhance.py +++ b/Tests/test_imageenhance.py @@ -1,19 +1,28 @@ -from tester import * +from helper import unittest, PillowTestCase, lena from PIL import Image from PIL import ImageEnhance -def test_sanity(): - # FIXME: assert_image - assert_no_exception(lambda: ImageEnhance.Color(lena()).enhance(0.5)) - assert_no_exception(lambda: ImageEnhance.Contrast(lena()).enhance(0.5)) - assert_no_exception(lambda: ImageEnhance.Brightness(lena()).enhance(0.5)) - assert_no_exception(lambda: ImageEnhance.Sharpness(lena()).enhance(0.5)) +class TestImageEnhance(PillowTestCase): -def test_crash(): + def test_sanity(self): - # crashes on small images - im = Image.new("RGB", (1, 1)) - assert_no_exception(lambda: ImageEnhance.Sharpness(im).enhance(0.5)) + # 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) + def test_crash(self): + + # crashes on small images + im = Image.new("RGB", (1, 1)) + ImageEnhance.Sharpness(im).enhance(0.5) + + +if __name__ == '__main__': + unittest.main() + +# End of file diff --git a/Tests/test_imagefile.py b/Tests/test_imagefile.py index adf282b03..78fb3427f 100644 --- a/Tests/test_imagefile.py +++ b/Tests/test_imagefile.py @@ -1,82 +1,93 @@ -from tester import * +from helper import unittest, PillowTestCase, lena, fromstring, tostring + +from io import BytesIO from PIL import Image from PIL import ImageFile from PIL import EpsImagePlugin + codecs = dir(Image.core) # save original block sizes MAXBLOCK = ImageFile.MAXBLOCK SAFEBLOCK = ImageFile.SAFEBLOCK -def test_parser(): - def roundtrip(format): +class TestImageFile(PillowTestCase): - im = lena("L").resize((1000, 1000)) - if format in ("MSP", "XBM"): - im = im.convert("1") + def test_parser(self): - file = BytesIO() + def roundtrip(format): - im.save(file, format) + im = lena("L").resize((1000, 1000)) + if format in ("MSP", "XBM"): + im = im.convert("1") - data = file.getvalue() + file = BytesIO() - parser = ImageFile.Parser() - parser.feed(data) - imOut = parser.close() + im.save(file, format) - return im, imOut + data = file.getvalue() + + parser = ImageFile.Parser() + parser.feed(data) + imOut = parser.close() + + return im, imOut + + self.assert_image_equal(*roundtrip("BMP")) + self.assert_image_equal(*roundtrip("GIF")) + self.assert_image_equal(*roundtrip("IM")) + self.assert_image_equal(*roundtrip("MSP")) + if "zip_encoder" in codecs: + try: + # force multiple blocks in PNG driver + ImageFile.MAXBLOCK = 8192 + self.assert_image_equal(*roundtrip("PNG")) + finally: + ImageFile.MAXBLOCK = MAXBLOCK + self.assert_image_equal(*roundtrip("PPM")) + self.assert_image_equal(*roundtrip("TIFF")) + self.assert_image_equal(*roundtrip("XBM")) + self.assert_image_equal(*roundtrip("TGA")) + self.assert_image_equal(*roundtrip("PCX")) + + if EpsImagePlugin.has_ghostscript(): + im1, im2 = roundtrip("EPS") + # EPS comes back in RGB: + self.assert_image_similar(im1, im2.convert('L'), 20) + + if "jpeg_encoder" in codecs: + im1, im2 = roundtrip("JPEG") # lossy compression + self.assert_image(im1, im2.mode, im2.size) + + self.assertRaises(IOError, lambda: roundtrip("PDF")) + + def test_ico(self): + with open('Tests/images/python.ico', 'rb') as f: + data = f.read() + p = ImageFile.Parser() + p.feed(data) + self.assertEqual((48, 48), p.image.size) + + def test_safeblock(self): + + im1 = lena() + + if "zip_encoder" not in codecs: + self.skipTest("PNG (zlib) encoder not available") - assert_image_equal(*roundtrip("BMP")) - assert_image_equal(*roundtrip("GIF")) - assert_image_equal(*roundtrip("IM")) - assert_image_equal(*roundtrip("MSP")) - if "zip_encoder" in codecs: try: - # force multiple blocks in PNG driver - ImageFile.MAXBLOCK = 8192 - assert_image_equal(*roundtrip("PNG")) + ImageFile.SAFEBLOCK = 1 + im2 = fromstring(tostring(im1, "PNG")) finally: - ImageFile.MAXBLOCK = MAXBLOCK - assert_image_equal(*roundtrip("PPM")) - assert_image_equal(*roundtrip("TIFF")) - assert_image_equal(*roundtrip("XBM")) - assert_image_equal(*roundtrip("TGA")) - assert_image_equal(*roundtrip("PCX")) + ImageFile.SAFEBLOCK = SAFEBLOCK - if EpsImagePlugin.has_ghostscript(): - im1, im2 = roundtrip("EPS") - assert_image_similar(im1, im2.convert('L'),20) # EPS comes back in RGB - - if "jpeg_encoder" in codecs: - im1, im2 = roundtrip("JPEG") # lossy compression - assert_image(im1, im2.mode, im2.size) + self.assert_image_equal(im1, im2) - # XXX Why assert exception and why does it fail? - # https://github.com/python-imaging/Pillow/issues/78 - #assert_exception(IOError, lambda: roundtrip("PDF")) -def test_ico(): - with open('Tests/images/python.ico', 'rb') as f: - data = f.read() - p = ImageFile.Parser() - p.feed(data) - assert_equal((48,48), p.image.size) +if __name__ == '__main__': + unittest.main() -def test_safeblock(): - - im1 = lena() - - if "zip_encoder" not in codecs: - skip("PNG (zlib) encoder not available") - - try: - ImageFile.SAFEBLOCK = 1 - im2 = fromstring(tostring(im1, "PNG")) - finally: - ImageFile.SAFEBLOCK = SAFEBLOCK - - assert_image_equal(im1, im2) +# End of file diff --git a/Tests/test_imagefileio.py b/Tests/test_imagefileio.py index c63f07bb0..32ee0bc5e 100644 --- a/Tests/test_imagefileio.py +++ b/Tests/test_imagefileio.py @@ -1,24 +1,33 @@ -from tester import * +from helper import unittest, PillowTestCase, lena, tostring from PIL import Image from PIL import ImageFileIO -def test_fileio(): - class DumbFile: - def __init__(self, data): - self.data = data - def read(self, bytes=None): - assert_equal(bytes, None) - return self.data - def close(self): - pass +class TestImageFileIo(PillowTestCase): - im1 = lena() + def test_fileio(self): - io = ImageFileIO.ImageFileIO(DumbFile(tostring(im1, "PPM"))) + class DumbFile: + def __init__(self, data): + self.data = data - im2 = Image.open(io) - assert_image_equal(im1, im2) + def read(self, bytes=None): + assert(bytes is None) + return self.data + + def close(self): + pass + + im1 = lena() + + io = ImageFileIO.ImageFileIO(DumbFile(tostring(im1, "PPM"))) + + im2 = Image.open(io) + self.assert_image_equal(im1, im2) +if __name__ == '__main__': + unittest.main() + +# End of file diff --git a/Tests/test_imagefilter.py b/Tests/test_imagefilter.py deleted file mode 100644 index 214f88024..000000000 --- a/Tests/test_imagefilter.py +++ /dev/null @@ -1,31 +0,0 @@ -from tester import * - -from PIL import Image -from PIL import ImageFilter - -def test_sanity(): - # see test_image_filter for more tests - - assert_no_exception(lambda: ImageFilter.MaxFilter) - assert_no_exception(lambda: ImageFilter.MedianFilter) - assert_no_exception(lambda: ImageFilter.MinFilter) - assert_no_exception(lambda: ImageFilter.ModeFilter) - assert_no_exception(lambda: ImageFilter.Kernel((3, 3), list(range(9)))) - assert_no_exception(lambda: ImageFilter.GaussianBlur) - assert_no_exception(lambda: ImageFilter.GaussianBlur(5)) - assert_no_exception(lambda: ImageFilter.UnsharpMask) - assert_no_exception(lambda: ImageFilter.UnsharpMask(10)) - - assert_no_exception(lambda: ImageFilter.BLUR) - assert_no_exception(lambda: ImageFilter.CONTOUR) - assert_no_exception(lambda: ImageFilter.DETAIL) - assert_no_exception(lambda: ImageFilter.EDGE_ENHANCE) - assert_no_exception(lambda: ImageFilter.EDGE_ENHANCE_MORE) - assert_no_exception(lambda: ImageFilter.EMBOSS) - assert_no_exception(lambda: ImageFilter.FIND_EDGES) - assert_no_exception(lambda: ImageFilter.SMOOTH) - assert_no_exception(lambda: ImageFilter.SMOOTH_MORE) - assert_no_exception(lambda: ImageFilter.SHARPEN) - - - diff --git a/Tests/test_imagefont.py b/Tests/test_imagefont.py index 9ac2cdd89..ed2439e7c 100644 --- a/Tests/test_imagefont.py +++ b/Tests/test_imagefont.py @@ -1,135 +1,205 @@ -from tester import * +from helper import unittest, PillowTestCase from PIL import Image +from PIL import ImageDraw from io import BytesIO import os +FONT_PATH = "Tests/fonts/FreeMono.ttf" +FONT_SIZE = 20 + + try: from PIL import ImageFont - ImageFont.core.getfont # check if freetype is available + ImageFont.core.getfont # check if freetype is available + + class TestImageFont(PillowTestCase): + + def test_sanity(self): + self.assertRegexpMatches( + ImageFont.core.freetype2_version, "\d+\.\d+\.\d+$") + + def test_font_with_name(self): + ImageFont.truetype(FONT_PATH, FONT_SIZE) + self._render(FONT_PATH) + self._clean() + + def _font_as_bytes(self): + with open(FONT_PATH, 'rb') as f: + font_bytes = BytesIO(f.read()) + return font_bytes + + def test_font_with_filelike(self): + ImageFont.truetype(self._font_as_bytes(), FONT_SIZE) + self._render(self._font_as_bytes()) + # Usage note: making two fonts from the same buffer fails. + # shared_bytes = self._font_as_bytes() + # self._render(shared_bytes) + # self.assertRaises(Exception, lambda: _render(shared_bytes)) + self._clean() + + def test_font_with_open_file(self): + with open(FONT_PATH, 'rb') as f: + self._render(f) + self._clean() + + def test_font_old_parameters(self): + self.assert_warning( + DeprecationWarning, + lambda: ImageFont.truetype(filename=FONT_PATH, size=FONT_SIZE)) + + def _render(self, font): + txt = "Hello World!" + ttf = ImageFont.truetype(font, FONT_SIZE) + w, h = ttf.getsize(txt) + img = Image.new("RGB", (256, 64), "white") + d = ImageDraw.Draw(img) + d.text((10, 10), txt, font=ttf, fill='black') + + img.save('font.png') + return img + + def _clean(self): + os.unlink('font.png') + + def test_render_equal(self): + img_path = self._render(FONT_PATH) + with open(FONT_PATH, 'rb') as f: + font_filelike = BytesIO(f.read()) + img_filelike = self._render(font_filelike) + + self.assert_image_equal(img_path, img_filelike) + self._clean() + + def test_textsize_equal(self): + im = Image.new(mode='RGB', size=(300, 100)) + draw = ImageDraw.Draw(im) + ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) + + txt = "Hello World!" + size = draw.textsize(txt, ttf) + draw.text((10, 10), txt, font=ttf) + draw.rectangle((10, 10, 10 + size[0], 10 + size[1])) + + target = 'Tests/images/rectangle_surrounding_text.png' + target_img = Image.open(target) + self.assert_image_equal(im, target_img) + + def test_render_multiline(self): + im = Image.new(mode='RGB', size=(300, 100)) + draw = ImageDraw.Draw(im) + ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) + line_spacing = draw.textsize('A', font=ttf)[1] + 4 + lines = ['hey you', 'you are awesome', 'this looks awkward'] + y = 0 + for line in lines: + draw.text((0, y), line, font=ttf) + y += line_spacing + + target = 'Tests/images/multiline_text.png' + target_img = Image.open(target) + + # some versions of freetype have different horizontal spacing. + # setting a tight epsilon, I'm showing the original test failure + # at epsilon = ~38. + self.assert_image_similar(im, target_img, .5) + + def test_rotated_transposed_font(self): + img_grey = Image.new("L", (100, 100)) + draw = ImageDraw.Draw(img_grey) + word = "testing" + font = ImageFont.truetype(FONT_PATH, FONT_SIZE) + + orientation = Image.ROTATE_90 + transposed_font = ImageFont.TransposedFont( + font, orientation=orientation) + + # Original font + draw.setfont(font) + box_size_a = draw.textsize(word) + + # Rotated font + draw.setfont(transposed_font) + box_size_b = draw.textsize(word) + + # Check (w,h) of box a is (h,w) of box b + self.assertEqual(box_size_a[0], box_size_b[1]) + self.assertEqual(box_size_a[1], box_size_b[0]) + + def test_unrotated_transposed_font(self): + img_grey = Image.new("L", (100, 100)) + draw = ImageDraw.Draw(img_grey) + word = "testing" + font = ImageFont.truetype(FONT_PATH, FONT_SIZE) + + orientation = None + transposed_font = ImageFont.TransposedFont( + font, orientation=orientation) + + # Original font + draw.setfont(font) + box_size_a = draw.textsize(word) + + # Rotated font + draw.setfont(transposed_font) + box_size_b = draw.textsize(word) + + # Check boxes a and b are same size + self.assertEqual(box_size_a, box_size_b) + + def test_free_type_font_get_name(self): + # Arrange + font = ImageFont.truetype(FONT_PATH, FONT_SIZE) + + # Act + name = font.getname() + + # Assert + self.assertEqual(('FreeMono', 'Regular'), name) + + def test_free_type_font_get_metrics(self): + # Arrange + font = ImageFont.truetype(FONT_PATH, FONT_SIZE) + + # Act + ascent, descent = font.getmetrics() + + # Assert + self.assertIsInstance(ascent, int) + self.assertIsInstance(descent, int) + self.assertEqual((ascent, descent), (16, 4)) # too exact check? + + def test_load_path_not_found(self): + # Arrange + filename = "somefilenamethatdoesntexist.ttf" + + # Act/Assert + self.assertRaises(IOError, lambda: ImageFont.load_path(filename)) + + def test_default_font(self): + # Arrange + txt = 'This is a "better than nothing" default font.' + im = Image.new(mode='RGB', size=(300, 100)) + draw = ImageDraw.Draw(im) + + target = 'Tests/images/default_font.png' + target_img = Image.open(target) + + # Act + default_font = ImageFont.load_default() + draw.text((10, 10), txt, font=default_font) + + # Assert + self.assert_image_equal(im, target_img) + + except ImportError: - skip() - -from PIL import ImageDraw - -font_path = "Tests/fonts/FreeMono.ttf" -font_size=20 - -def test_sanity(): - assert_match(ImageFont.core.freetype2_version, "\d+\.\d+\.\d+$") - -def test_font_with_name(): - assert_no_exception(lambda: ImageFont.truetype(font_path, font_size)) - assert_no_exception(lambda: _render(font_path)) - _clean() - -def _font_as_bytes(): - with open(font_path, 'rb') as f: - font_bytes = BytesIO(f.read()) - return font_bytes - -def test_font_with_filelike(): - assert_no_exception(lambda: ImageFont.truetype(_font_as_bytes(), font_size)) - assert_no_exception(lambda: _render(_font_as_bytes())) - # Usage note: making two fonts from the same buffer fails. - #shared_bytes = _font_as_bytes() - #assert_no_exception(lambda: _render(shared_bytes)) - #assert_exception(Exception, lambda: _render(shared_bytes)) - _clean() - -def test_font_with_open_file(): - with open(font_path, 'rb') as f: - assert_no_exception(lambda: _render(f)) - _clean() - -def test_font_old_parameters(): - assert_warning(DeprecationWarning, lambda: ImageFont.truetype(filename=font_path, size=font_size)) - -def _render(font): - txt = "Hello World!" - ttf = ImageFont.truetype(font, font_size) - w, h = ttf.getsize(txt) - img = Image.new("RGB", (256, 64), "white") - d = ImageDraw.Draw(img) - d.text((10, 10), txt, font=ttf, fill='black') - - img.save('font.png') - return img - -def _clean(): - os.unlink('font.png') - -def test_render_equal(): - img_path = _render(font_path) - with open(font_path, 'rb') as f: - font_filelike = BytesIO(f.read()) - img_filelike = _render(font_filelike) - - assert_image_equal(img_path, img_filelike) - _clean() + class TestImageFont(PillowTestCase): + def test_skip(self): + self.skipTest("ImportError") -def test_render_multiline(): - im = Image.new(mode='RGB', size=(300,100)) - draw = ImageDraw.Draw(im) - ttf = ImageFont.truetype(font_path, font_size) - line_spacing = draw.textsize('A', font=ttf)[1] + 8 - lines = ['hey you', 'you are awesome', 'this looks awkward'] - y = 0 - for line in lines: - draw.text((0, y), line, font=ttf) - y += line_spacing - - - target = 'Tests/images/multiline_text.png' - target_img = Image.open(target) - - # some versions of freetype have different horizontal spacing. - # setting a tight epsilon, I'm showing the original test failure - # at epsilon = ~38. - assert_image_similar(im, target_img,.5) - - -def test_rotated_transposed_font(): - img_grey = Image.new("L", (100, 100)) - draw = ImageDraw.Draw(img_grey) - word = "testing" - font = ImageFont.truetype(font_path, font_size) - - orientation = Image.ROTATE_90 - transposed_font = ImageFont.TransposedFont(font, orientation=orientation) - - # Original font - draw.setfont(font) - box_size_a = draw.textsize(word) - - # Rotated font - draw.setfont(transposed_font) - box_size_b = draw.textsize(word) - - # Check (w,h) of box a is (h,w) of box b - assert_equal(box_size_a[0], box_size_b[1]) - assert_equal(box_size_a[1], box_size_b[0]) - - -def test_unrotated_transposed_font(): - img_grey = Image.new("L", (100, 100)) - draw = ImageDraw.Draw(img_grey) - word = "testing" - font = ImageFont.truetype(font_path, font_size) - - orientation = None - transposed_font = ImageFont.TransposedFont(font, orientation=orientation) - - # Original font - draw.setfont(font) - box_size_a = draw.textsize(word) - - # Rotated font - draw.setfont(transposed_font) - box_size_b = draw.textsize(word) - - # Check boxes a and b are same size - assert_equal(box_size_a, box_size_b) - +if __name__ == '__main__': + unittest.main() +# End of file diff --git a/Tests/test_imagegrab.py b/Tests/test_imagegrab.py index 67ff71960..13affe6b9 100644 --- a/Tests/test_imagegrab.py +++ b/Tests/test_imagegrab.py @@ -1,13 +1,25 @@ -from tester import * +from helper import unittest, PillowTestCase -from PIL import Image try: from PIL import ImageGrab -except ImportError as v: - skip(v) -def test_grab(): - im = ImageGrab.grab() - assert_image(im, im.mode, im.size) + class TestImageGrab(PillowTestCase): + + def test_grab(self): + im = ImageGrab.grab() + self.assert_image(im, im.mode, im.size) + + def test_grab2(self): + im = ImageGrab.grab() + self.assert_image(im, im.mode, im.size) + +except ImportError: + class TestImageGrab(PillowTestCase): + def test_skip(self): + self.skipTest("ImportError") +if __name__ == '__main__': + unittest.main() + +# End of file diff --git a/Tests/test_imagemath.py b/Tests/test_imagemath.py index eaeb711ba..5d87c0229 100644 --- a/Tests/test_imagemath.py +++ b/Tests/test_imagemath.py @@ -1,62 +1,188 @@ -from tester import * +from helper import unittest, PillowTestCase from PIL import Image from PIL import ImageMath + def pixel(im): if hasattr(im, "im"): return "%s %r" % (im.mode, im.getpixel((0, 0))) else: if isinstance(im, type(0)): - return int(im) # hack to deal with booleans + return int(im) # hack to deal with booleans print(im) A = Image.new("L", (1, 1), 1) B = Image.new("L", (1, 1), 2) +Z = Image.new("L", (1, 1), 0) # Z for zero F = Image.new("F", (1, 1), 3) I = Image.new("I", (1, 1), 4) +A2 = A.resize((2, 2)) +B2 = B.resize((2, 2)) + images = {"A": A, "B": B, "F": F, "I": I} -def test_sanity(): - assert_equal(ImageMath.eval("1"), 1) - assert_equal(ImageMath.eval("1+A", A=2), 3) - assert_equal(pixel(ImageMath.eval("A+B", A=A, B=B)), "I 3") - assert_equal(pixel(ImageMath.eval("A+B", images)), "I 3") - assert_equal(pixel(ImageMath.eval("float(A)+B", images)), "F 3.0") - assert_equal(pixel(ImageMath.eval("int(float(A)+B)", images)), "I 3") -def test_ops(): +class TestImageMath(PillowTestCase): - assert_equal(pixel(ImageMath.eval("-A", images)), "I -1") - assert_equal(pixel(ImageMath.eval("+B", images)), "L 2") + def test_sanity(self): + self.assertEqual(ImageMath.eval("1"), 1) + self.assertEqual(ImageMath.eval("1+A", A=2), 3) + self.assertEqual(pixel(ImageMath.eval("A+B", A=A, B=B)), "I 3") + self.assertEqual(pixel(ImageMath.eval("A+B", images)), "I 3") + self.assertEqual(pixel(ImageMath.eval("float(A)+B", images)), "F 3.0") + self.assertEqual(pixel( + ImageMath.eval("int(float(A)+B)", images)), "I 3") - assert_equal(pixel(ImageMath.eval("A+B", images)), "I 3") - assert_equal(pixel(ImageMath.eval("A-B", images)), "I -1") - assert_equal(pixel(ImageMath.eval("A*B", images)), "I 2") - assert_equal(pixel(ImageMath.eval("A/B", images)), "I 0") - assert_equal(pixel(ImageMath.eval("B**2", images)), "I 4") - assert_equal(pixel(ImageMath.eval("B**33", images)), "I 2147483647") + def test_ops(self): - assert_equal(pixel(ImageMath.eval("float(A)+B", images)), "F 3.0") - assert_equal(pixel(ImageMath.eval("float(A)-B", images)), "F -1.0") - assert_equal(pixel(ImageMath.eval("float(A)*B", images)), "F 2.0") - assert_equal(pixel(ImageMath.eval("float(A)/B", images)), "F 0.5") - assert_equal(pixel(ImageMath.eval("float(B)**2", images)), "F 4.0") - assert_equal(pixel(ImageMath.eval("float(B)**33", images)), "F 8589934592.0") + self.assertEqual(pixel(ImageMath.eval("-A", images)), "I -1") + self.assertEqual(pixel(ImageMath.eval("+B", images)), "L 2") -def test_logical(): - assert_equal(pixel(ImageMath.eval("not A", images)), 0) - assert_equal(pixel(ImageMath.eval("A and B", images)), "L 2") - assert_equal(pixel(ImageMath.eval("A or B", images)), "L 1") + self.assertEqual(pixel(ImageMath.eval("A+B", images)), "I 3") + self.assertEqual(pixel(ImageMath.eval("A-B", images)), "I -1") + self.assertEqual(pixel(ImageMath.eval("A*B", images)), "I 2") + self.assertEqual(pixel(ImageMath.eval("A/B", images)), "I 0") + self.assertEqual(pixel(ImageMath.eval("B**2", images)), "I 4") + self.assertEqual(pixel( + ImageMath.eval("B**33", images)), "I 2147483647") -def test_convert(): - assert_equal(pixel(ImageMath.eval("convert(A+B, 'L')", images)), "L 3") - assert_equal(pixel(ImageMath.eval("convert(A+B, '1')", images)), "1 0") - assert_equal(pixel(ImageMath.eval("convert(A+B, 'RGB')", images)), "RGB (3, 3, 3)") + self.assertEqual(pixel(ImageMath.eval("float(A)+B", images)), "F 3.0") + self.assertEqual(pixel(ImageMath.eval("float(A)-B", images)), "F -1.0") + self.assertEqual(pixel(ImageMath.eval("float(A)*B", images)), "F 2.0") + self.assertEqual(pixel(ImageMath.eval("float(A)/B", images)), "F 0.5") + self.assertEqual(pixel(ImageMath.eval("float(B)**2", images)), "F 4.0") + self.assertEqual(pixel( + ImageMath.eval("float(B)**33", images)), "F 8589934592.0") -def test_compare(): - assert_equal(pixel(ImageMath.eval("min(A, B)", images)), "I 1") - assert_equal(pixel(ImageMath.eval("max(A, B)", images)), "I 2") - assert_equal(pixel(ImageMath.eval("A == 1", images)), "I 1") - assert_equal(pixel(ImageMath.eval("A == 2", images)), "I 0") + def test_logical(self): + self.assertEqual(pixel(ImageMath.eval("not A", images)), 0) + self.assertEqual(pixel(ImageMath.eval("A and B", images)), "L 2") + self.assertEqual(pixel(ImageMath.eval("A or B", images)), "L 1") + + def test_convert(self): + self.assertEqual(pixel( + ImageMath.eval("convert(A+B, 'L')", images)), "L 3") + self.assertEqual(pixel( + ImageMath.eval("convert(A+B, '1')", images)), "1 0") + self.assertEqual(pixel( + ImageMath.eval("convert(A+B, 'RGB')", images)), "RGB (3, 3, 3)") + + def test_compare(self): + self.assertEqual(pixel(ImageMath.eval("min(A, B)", images)), "I 1") + self.assertEqual(pixel(ImageMath.eval("max(A, B)", images)), "I 2") + self.assertEqual(pixel(ImageMath.eval("A == 1", images)), "I 1") + self.assertEqual(pixel(ImageMath.eval("A == 2", images)), "I 0") + + def test_one_image_larger(self): + self.assertEqual(pixel(ImageMath.eval("A+B", A=A2, B=B)), "I 3") + self.assertEqual(pixel(ImageMath.eval("A+B", A=A, B=B2)), "I 3") + + def test_abs(self): + self.assertEqual(pixel(ImageMath.eval("abs(A)", A=A)), "I 1") + self.assertEqual(pixel(ImageMath.eval("abs(B)", B=B)), "I 2") + + def test_binary_mod(self): + self.assertEqual(pixel(ImageMath.eval("A%A", A=A)), "I 0") + self.assertEqual(pixel(ImageMath.eval("B%B", B=B)), "I 0") + self.assertEqual(pixel(ImageMath.eval("A%B", A=A, B=B)), "I 1") + self.assertEqual(pixel(ImageMath.eval("B%A", A=A, B=B)), "I 0") + self.assertEqual(pixel(ImageMath.eval("Z%A", A=A, Z=Z)), "I 0") + self.assertEqual(pixel(ImageMath.eval("Z%B", B=B, Z=Z)), "I 0") + + def test_bitwise_invert(self): + self.assertEqual(pixel(ImageMath.eval("~Z", Z=Z)), "I -1") + self.assertEqual(pixel(ImageMath.eval("~A", A=A)), "I -2") + self.assertEqual(pixel(ImageMath.eval("~B", B=B)), "I -3") + + def test_bitwise_and(self): + self.assertEqual(pixel(ImageMath.eval("Z&Z", A=A, Z=Z)), "I 0") + self.assertEqual(pixel(ImageMath.eval("Z&A", A=A, Z=Z)), "I 0") + self.assertEqual(pixel(ImageMath.eval("A&Z", A=A, Z=Z)), "I 0") + self.assertEqual(pixel(ImageMath.eval("A&A", A=A, Z=Z)), "I 1") + + def test_bitwise_or(self): + self.assertEqual(pixel(ImageMath.eval("Z|Z", A=A, Z=Z)), "I 0") + self.assertEqual(pixel(ImageMath.eval("Z|A", A=A, Z=Z)), "I 1") + self.assertEqual(pixel(ImageMath.eval("A|Z", A=A, Z=Z)), "I 1") + self.assertEqual(pixel(ImageMath.eval("A|A", A=A, Z=Z)), "I 1") + + def test_bitwise_xor(self): + self.assertEqual(pixel(ImageMath.eval("Z^Z", A=A, Z=Z)), "I 0") + self.assertEqual(pixel(ImageMath.eval("Z^A", A=A, Z=Z)), "I 1") + self.assertEqual(pixel(ImageMath.eval("A^Z", A=A, Z=Z)), "I 1") + self.assertEqual(pixel(ImageMath.eval("A^A", A=A, Z=Z)), "I 0") + + def test_bitwise_leftshift(self): + self.assertEqual(pixel(ImageMath.eval("Z<<0", Z=Z)), "I 0") + self.assertEqual(pixel(ImageMath.eval("Z<<1", Z=Z)), "I 0") + self.assertEqual(pixel(ImageMath.eval("A<<0", A=A)), "I 1") + self.assertEqual(pixel(ImageMath.eval("A<<1", A=A)), "I 2") + + def test_bitwise_rightshift(self): + self.assertEqual(pixel(ImageMath.eval("Z>>0", Z=Z)), "I 0") + self.assertEqual(pixel(ImageMath.eval("Z>>1", Z=Z)), "I 0") + self.assertEqual(pixel(ImageMath.eval("A>>0", A=A)), "I 1") + self.assertEqual(pixel(ImageMath.eval("A>>1", A=A)), "I 0") + + def test_logical_eq(self): + self.assertEqual(pixel(ImageMath.eval("A==A", A=A)), "I 1") + self.assertEqual(pixel(ImageMath.eval("B==B", B=B)), "I 1") + self.assertEqual(pixel(ImageMath.eval("A==B", A=A, B=B)), "I 0") + self.assertEqual(pixel(ImageMath.eval("B==A", A=A, B=B)), "I 0") + + def test_logical_ne(self): + self.assertEqual(pixel(ImageMath.eval("A!=A", A=A)), "I 0") + self.assertEqual(pixel(ImageMath.eval("B!=B", B=B)), "I 0") + self.assertEqual(pixel(ImageMath.eval("A!=B", A=A, B=B)), "I 1") + self.assertEqual(pixel(ImageMath.eval("B!=A", A=A, B=B)), "I 1") + + def test_logical_lt(self): + self.assertEqual(pixel(ImageMath.eval("AA", A=A)), "I 0") + self.assertEqual(pixel(ImageMath.eval("B>B", B=B)), "I 0") + self.assertEqual(pixel(ImageMath.eval("A>B", A=A, B=B)), "I 0") + self.assertEqual(pixel(ImageMath.eval("B>A", A=A, B=B)), "I 1") + + def test_logical_ge(self): + self.assertEqual(pixel(ImageMath.eval("A>=A", A=A)), "I 1") + self.assertEqual(pixel(ImageMath.eval("B>=B", B=B)), "I 1") + self.assertEqual(pixel(ImageMath.eval("A>=B", A=A, B=B)), "I 0") + self.assertEqual(pixel(ImageMath.eval("B>=A", A=A, B=B)), "I 1") + + def test_logical_equal(self): + self.assertEqual(pixel(ImageMath.eval("equal(A, A)", A=A)), "I 1") + self.assertEqual(pixel(ImageMath.eval("equal(B, B)", B=B)), "I 1") + self.assertEqual(pixel(ImageMath.eval("equal(Z, Z)", Z=Z)), "I 1") + self.assertEqual(pixel(ImageMath.eval("equal(A, B)", A=A, B=B)), "I 0") + self.assertEqual(pixel(ImageMath.eval("equal(B, A)", A=A, B=B)), "I 0") + self.assertEqual(pixel(ImageMath.eval("equal(A, Z)", A=A, Z=Z)), "I 0") + + def test_logical_not_equal(self): + self.assertEqual(pixel(ImageMath.eval("notequal(A, A)", A=A)), "I 0") + self.assertEqual(pixel(ImageMath.eval("notequal(B, B)", B=B)), "I 0") + self.assertEqual(pixel(ImageMath.eval("notequal(Z, Z)", Z=Z)), "I 0") + self.assertEqual( + pixel(ImageMath.eval("notequal(A, B)", A=A, B=B)), "I 1") + self.assertEqual( + pixel(ImageMath.eval("notequal(B, A)", A=A, B=B)), "I 1") + self.assertEqual( + pixel(ImageMath.eval("notequal(A, Z)", A=A, Z=Z)), "I 1") + + +if __name__ == '__main__': + unittest.main() + +# End of file diff --git a/Tests/test_imagemode.py b/Tests/test_imagemode.py deleted file mode 100644 index 54b04435f..000000000 --- a/Tests/test_imagemode.py +++ /dev/null @@ -1,23 +0,0 @@ -from tester import * - -from PIL import Image -from PIL import ImageMode - -ImageMode.getmode("1") -ImageMode.getmode("L") -ImageMode.getmode("P") -ImageMode.getmode("RGB") -ImageMode.getmode("I") -ImageMode.getmode("F") - -m = ImageMode.getmode("1") -assert_equal(m.mode, "1") -assert_equal(m.bands, ("1",)) -assert_equal(m.basemode, "L") -assert_equal(m.basetype, "L") - -m = ImageMode.getmode("RGB") -assert_equal(m.mode, "RGB") -assert_equal(m.bands, ("R", "G", "B")) -assert_equal(m.basemode, "RGB") -assert_equal(m.basetype, "L") diff --git a/Tests/test_imagemorph.py b/Tests/test_imagemorph.py new file mode 100644 index 000000000..a52111431 --- /dev/null +++ b/Tests/test_imagemorph.py @@ -0,0 +1,175 @@ +# Test the ImageMorphology functionality +from helper import * + +from PIL import Image +from PIL import ImageMorph + + +class MorphTests(PillowTestCase): + + def setUp(self): + self.A = self.string_to_img( + """ + ....... + ....... + ..111.. + ..111.. + ..111.. + ....... + ....... + """ + ) + + def img_to_string(self, im): + """Turn a (small) binary image into a string representation""" + chars = '.1' + width, height = im.size + return '\n'.join( + [''.join([chars[im.getpixel((c, r)) > 0] for c in range(width)]) + for r in range(height)]) + + def string_to_img(self, image_string): + """Turn a string image representation into a binary image""" + rows = [s for s in image_string.replace(' ', '').split('\n') + if len(s)] + height = len(rows) + width = len(rows[0]) + im = Image.new('L', (width, height)) + for i in range(width): + for j in range(height): + c = rows[j][i] + v = c in 'X1' + im.putpixel((i, j), v) + + return im + + def img_string_normalize(self, im): + return self.img_to_string(self.string_to_img(im)) + + def assert_img_equal(self, A, B): + self.assertEqual(self.img_to_string(A), self.img_to_string(B)) + + def assert_img_equal_img_string(self, A, Bstring): + self.assertEqual( + self.img_to_string(A), + self.img_string_normalize(Bstring)) + + def test_str_to_img(self): + im = Image.open('Tests/images/morph_a.png') + self.assert_image_equal(self.A, im) + + def create_lut(self): + for op in ( + 'corner', 'dilation4', 'dilation8', + 'erosion4', 'erosion8', 'edge'): + lb = ImageMorph.LutBuilder(op_name=op) + lut = lb.build_lut(self) + with open('Tests/images/%s.lut' % op, 'wb') as f: + f.write(lut) + + # create_lut() + def test_lut(self): + for op in ( + 'corner', 'dilation4', 'dilation8', + 'erosion4', 'erosion8', 'edge'): + lb = ImageMorph.LutBuilder(op_name=op) + lut = lb.build_lut() + with open('Tests/images/%s.lut' % op, 'rb') as f: + self.assertEqual(lut, bytearray(f.read())) + + # Test the named patterns + def test_erosion8(self): + # erosion8 + mop = ImageMorph.MorphOp(op_name='erosion8') + count, Aout = mop.apply(self.A) + self.assertEqual(count, 8) + self.assert_img_equal_img_string(Aout, + """ + ....... + ....... + ....... + ...1... + ....... + ....... + ....... + """) + + def test_dialation8(self): + # dialation8 + mop = ImageMorph.MorphOp(op_name='dilation8') + count, Aout = mop.apply(self.A) + self.assertEqual(count, 16) + self.assert_img_equal_img_string(Aout, + """ + ....... + .11111. + .11111. + .11111. + .11111. + .11111. + ....... + """) + + def test_erosion4(self): + # erosion4 + mop = ImageMorph.MorphOp(op_name='dilation4') + count, Aout = mop.apply(self.A) + self.assertEqual(count, 12) + self.assert_img_equal_img_string(Aout, + """ + ....... + ..111.. + .11111. + .11111. + .11111. + ..111.. + ....... + """) + + def test_edge(self): + # edge + mop = ImageMorph.MorphOp(op_name='edge') + count, Aout = mop.apply(self.A) + self.assertEqual(count, 1) + self.assert_img_equal_img_string(Aout, + """ + ....... + ....... + ..111.. + ..1.1.. + ..111.. + ....... + ....... + """) + + def test_corner(self): + # Create a corner detector pattern + mop = ImageMorph.MorphOp(patterns=['1:(... ... ...)->0', + '4:(00. 01. ...)->1']) + count, Aout = mop.apply(self.A) + self.assertEqual(count, 5) + self.assert_img_equal_img_string(Aout, + """ + ....... + ....... + ..1.1.. + ....... + ..1.1.. + ....... + ....... + """) + + # Test the coordinate counting with the same operator + coords = mop.match(self.A) + self.assertEqual(len(coords), 4) + self.assertEqual(tuple(coords), ((2, 2), (4, 2), (2, 4), (4, 4))) + + coords = mop.get_on_pixels(Aout) + self.assertEqual(len(coords), 4) + self.assertEqual(tuple(coords), ((2, 2), (4, 2), (2, 4), (4, 4))) + + +if __name__ == '__main__': + unittest.main() + +# End of file diff --git a/Tests/test_imageops.py b/Tests/test_imageops.py index 8ed5ccefa..a4a94ca4d 100644 --- a/Tests/test_imageops.py +++ b/Tests/test_imageops.py @@ -1,81 +1,85 @@ -from tester import * +from helper import unittest, PillowTestCase, lena -from PIL import Image from PIL import ImageOps -class Deformer: - def getmesh(self, im): - x, y = im.size - return [((0, 0, x, y), (0, 0, x, 0, x, y, y, 0))] -deformer = Deformer() +class TestImageOps(PillowTestCase): -def test_sanity(): + class Deformer: + def getmesh(self, im): + x, y = im.size + return [((0, 0, x, y), (0, 0, x, 0, x, y, y, 0))] - ImageOps.autocontrast(lena("L")) - ImageOps.autocontrast(lena("RGB")) + deformer = Deformer() - ImageOps.autocontrast(lena("L"), cutoff=10) - ImageOps.autocontrast(lena("L"), ignore=[0, 255]) + def test_sanity(self): - ImageOps.colorize(lena("L"), (0, 0, 0), (255, 255, 255)) - ImageOps.colorize(lena("L"), "black", "white") + ImageOps.autocontrast(lena("L")) + ImageOps.autocontrast(lena("RGB")) - ImageOps.crop(lena("L"), 1) - ImageOps.crop(lena("RGB"), 1) + ImageOps.autocontrast(lena("L"), cutoff=10) + ImageOps.autocontrast(lena("L"), ignore=[0, 255]) - ImageOps.deform(lena("L"), deformer) - ImageOps.deform(lena("RGB"), deformer) + ImageOps.colorize(lena("L"), (0, 0, 0), (255, 255, 255)) + ImageOps.colorize(lena("L"), "black", "white") - ImageOps.equalize(lena("L")) - ImageOps.equalize(lena("RGB")) + ImageOps.crop(lena("L"), 1) + ImageOps.crop(lena("RGB"), 1) - ImageOps.expand(lena("L"), 1) - ImageOps.expand(lena("RGB"), 1) - ImageOps.expand(lena("L"), 2, "blue") - ImageOps.expand(lena("RGB"), 2, "blue") + ImageOps.deform(lena("L"), self.deformer) + ImageOps.deform(lena("RGB"), self.deformer) - ImageOps.fit(lena("L"), (128, 128)) - ImageOps.fit(lena("RGB"), (128, 128)) + ImageOps.equalize(lena("L")) + ImageOps.equalize(lena("RGB")) - ImageOps.flip(lena("L")) - ImageOps.flip(lena("RGB")) + ImageOps.expand(lena("L"), 1) + ImageOps.expand(lena("RGB"), 1) + ImageOps.expand(lena("L"), 2, "blue") + ImageOps.expand(lena("RGB"), 2, "blue") - ImageOps.grayscale(lena("L")) - ImageOps.grayscale(lena("RGB")) + ImageOps.fit(lena("L"), (128, 128)) + ImageOps.fit(lena("RGB"), (128, 128)) - ImageOps.invert(lena("L")) - ImageOps.invert(lena("RGB")) + ImageOps.flip(lena("L")) + ImageOps.flip(lena("RGB")) - ImageOps.mirror(lena("L")) - ImageOps.mirror(lena("RGB")) + ImageOps.grayscale(lena("L")) + ImageOps.grayscale(lena("RGB")) - ImageOps.posterize(lena("L"), 4) - ImageOps.posterize(lena("RGB"), 4) + ImageOps.invert(lena("L")) + ImageOps.invert(lena("RGB")) - ImageOps.solarize(lena("L")) - ImageOps.solarize(lena("RGB")) + ImageOps.mirror(lena("L")) + ImageOps.mirror(lena("RGB")) - success() + ImageOps.posterize(lena("L"), 4) + ImageOps.posterize(lena("RGB"), 4) -def test_1pxfit(): - # Division by zero in equalize if image is 1 pixel high - newimg = ImageOps.fit(lena("RGB").resize((1,1)), (35,35)) - assert_equal(newimg.size,(35,35)) - - newimg = ImageOps.fit(lena("RGB").resize((1,100)), (35,35)) - assert_equal(newimg.size,(35,35)) + ImageOps.solarize(lena("L")) + ImageOps.solarize(lena("RGB")) - newimg = ImageOps.fit(lena("RGB").resize((100,1)), (35,35)) - assert_equal(newimg.size,(35,35)) + 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)) + self.assertEqual(newimg.size, (35, 35)) -def test_pil163(): - # Division by zero in equalize if < 255 pixels in image (@PIL163) + newimg = ImageOps.fit(lena("RGB").resize((1, 100)), (35, 35)) + self.assertEqual(newimg.size, (35, 35)) - i = lena("RGB").resize((15, 16)) + newimg = ImageOps.fit(lena("RGB").resize((100, 1)), (35, 35)) + self.assertEqual(newimg.size, (35, 35)) - ImageOps.equalize(i.convert("L")) - ImageOps.equalize(i.convert("P")) - ImageOps.equalize(i.convert("RGB")) + def test_pil163(self): + # Division by zero in equalize if < 255 pixels in image (@PIL163) - success() + i = lena("RGB").resize((15, 16)) + + ImageOps.equalize(i.convert("L")) + ImageOps.equalize(i.convert("P")) + ImageOps.equalize(i.convert("RGB")) + + +if __name__ == '__main__': + unittest.main() + +# End of file diff --git a/Tests/test_imageops_usm.py b/Tests/test_imageops_usm.py index 83a93b8e0..be7a669da 100644 --- a/Tests/test_imageops_usm.py +++ b/Tests/test_imageops_usm.py @@ -1,55 +1,64 @@ -from tester import * +from helper import unittest, PillowTestCase from PIL import Image from PIL import ImageOps from PIL import ImageFilter -im = Image.open("Images/lena.ppm") +im = Image.open("Tests/images/lena.ppm") -def test_ops_api(): - i = ImageOps.gaussian_blur(im, 2.0) - assert_equal(i.mode, "RGB") - assert_equal(i.size, (128, 128)) - # i.save("blur.bmp") +class TestImageOpsUsm(PillowTestCase): - i = ImageOps.usm(im, 2.0, 125, 8) - assert_equal(i.mode, "RGB") - assert_equal(i.size, (128, 128)) - # i.save("usm.bmp") + def test_ops_api(self): -def test_filter_api(): + i = ImageOps.gaussian_blur(im, 2.0) + self.assertEqual(i.mode, "RGB") + self.assertEqual(i.size, (128, 128)) + # i.save("blur.bmp") - filter = ImageFilter.GaussianBlur(2.0) - i = im.filter(filter) - assert_equal(i.mode, "RGB") - assert_equal(i.size, (128, 128)) + i = ImageOps.usm(im, 2.0, 125, 8) + self.assertEqual(i.mode, "RGB") + self.assertEqual(i.size, (128, 128)) + # i.save("usm.bmp") - filter = ImageFilter.UnsharpMask(2.0, 125, 8) - i = im.filter(filter) - assert_equal(i.mode, "RGB") - assert_equal(i.size, (128, 128)) + def test_filter_api(self): -def test_usm(): + filter = ImageFilter.GaussianBlur(2.0) + i = im.filter(filter) + self.assertEqual(i.mode, "RGB") + self.assertEqual(i.size, (128, 128)) - usm = ImageOps.unsharp_mask - assert_exception(ValueError, lambda: usm(im.convert("1"))) - assert_no_exception(lambda: usm(im.convert("L"))) - assert_exception(ValueError, lambda: usm(im.convert("I"))) - assert_exception(ValueError, lambda: usm(im.convert("F"))) - assert_no_exception(lambda: usm(im.convert("RGB"))) - assert_no_exception(lambda: usm(im.convert("RGBA"))) - assert_no_exception(lambda: usm(im.convert("CMYK"))) - assert_exception(ValueError, lambda: usm(im.convert("YCbCr"))) + filter = ImageFilter.UnsharpMask(2.0, 125, 8) + i = im.filter(filter) + self.assertEqual(i.mode, "RGB") + self.assertEqual(i.size, (128, 128)) -def test_blur(): + def test_usm(self): - blur = ImageOps.gaussian_blur - assert_exception(ValueError, lambda: blur(im.convert("1"))) - assert_no_exception(lambda: blur(im.convert("L"))) - assert_exception(ValueError, lambda: blur(im.convert("I"))) - assert_exception(ValueError, lambda: blur(im.convert("F"))) - assert_no_exception(lambda: blur(im.convert("RGB"))) - assert_no_exception(lambda: blur(im.convert("RGBA"))) - assert_no_exception(lambda: blur(im.convert("CMYK"))) - assert_exception(ValueError, lambda: blur(im.convert("YCbCr"))) + usm = ImageOps.unsharp_mask + self.assertRaises(ValueError, lambda: usm(im.convert("1"))) + usm(im.convert("L")) + self.assertRaises(ValueError, lambda: usm(im.convert("I"))) + self.assertRaises(ValueError, lambda: usm(im.convert("F"))) + usm(im.convert("RGB")) + usm(im.convert("RGBA")) + usm(im.convert("CMYK")) + self.assertRaises(ValueError, lambda: usm(im.convert("YCbCr"))) + + def test_blur(self): + + blur = ImageOps.gaussian_blur + self.assertRaises(ValueError, lambda: blur(im.convert("1"))) + blur(im.convert("L")) + self.assertRaises(ValueError, lambda: blur(im.convert("I"))) + self.assertRaises(ValueError, lambda: blur(im.convert("F"))) + blur(im.convert("RGB")) + blur(im.convert("RGBA")) + blur(im.convert("CMYK")) + self.assertRaises(ValueError, lambda: blur(im.convert("YCbCr"))) + + +if __name__ == '__main__': + unittest.main() + +# End of file diff --git a/Tests/test_imagepalette.py b/Tests/test_imagepalette.py index a22addda9..e56c61390 100644 --- a/Tests/test_imagepalette.py +++ b/Tests/test_imagepalette.py @@ -1,44 +1,153 @@ -from tester import * +from helper import unittest, PillowTestCase -from PIL import Image from PIL import ImagePalette ImagePalette = ImagePalette.ImagePalette -def test_sanity(): - assert_no_exception(lambda: ImagePalette("RGB", list(range(256))*3)) - assert_exception(ValueError, lambda: ImagePalette("RGB", list(range(256))*2)) +class TestImagePalette(PillowTestCase): -def test_getcolor(): + def test_sanity(self): - palette = ImagePalette() + ImagePalette("RGB", list(range(256))*3) + self.assertRaises( + ValueError, lambda: ImagePalette("RGB", list(range(256))*2)) - map = {} - for i in range(256): - map[palette.getcolor((i, i, i))] = i + def test_getcolor(self): - assert_equal(len(map), 256) - assert_exception(ValueError, lambda: palette.getcolor((1, 2, 3))) + palette = ImagePalette() -def test_file(): + map = {} + for i in range(256): + map[palette.getcolor((i, i, i))] = i - palette = ImagePalette() + self.assertEqual(len(map), 256) + self.assertRaises(ValueError, lambda: palette.getcolor((1, 2, 3))) - file = tempfile("temp.lut") + def test_file(self): - palette.save(file) + palette = ImagePalette("RGB", list(range(256))*3) - from PIL.ImagePalette import load, raw + f = self.tempfile("temp.lut") - p = load(file) + palette.save(f) - # load returns raw palette information - assert_equal(len(p[0]), 768) - assert_equal(p[1], "RGB") + from PIL.ImagePalette import load, raw - p = raw(p[1], p[0]) - assert_true(isinstance(p, ImagePalette)) + p = load(f) + + # load returns raw palette information + self.assertEqual(len(p[0]), 768) + self.assertEqual(p[1], "RGB") + + p = raw(p[1], p[0]) + self.assertIsInstance(p, ImagePalette) + self.assertEqual(p.palette, palette.tobytes()) + + def test_make_linear_lut(self): + # Arrange + from PIL.ImagePalette import make_linear_lut + black = 0 + white = 255 + + # Act + lut = make_linear_lut(black, white) + + # Assert + self.assertIsInstance(lut, list) + self.assertEqual(len(lut), 256) + # Check values + for i in range(0, len(lut)): + self.assertEqual(lut[i], i) + + def test_make_linear_lut_not_yet_implemented(self): + # Update after FIXME + # Arrange + from PIL.ImagePalette import make_linear_lut + black = 1 + white = 255 + + # Act + self.assertRaises( + NotImplementedError, + lambda: make_linear_lut(black, white)) + + def test_make_gamma_lut(self): + # Arrange + from PIL.ImagePalette import make_gamma_lut + exp = 5 + + # Act + lut = make_gamma_lut(exp) + + # Assert + self.assertIsInstance(lut, list) + self.assertEqual(len(lut), 256) + # Check a few values + self.assertEqual(lut[0], 0) + self.assertEqual(lut[63], 0) + self.assertEqual(lut[127], 8) + self.assertEqual(lut[191], 60) + self.assertEqual(lut[255], 255) + + def test_private_make_linear_lut_warning(self): + # Arrange + from PIL.ImagePalette import _make_linear_lut + black = 0 + white = 255 + + # Act / Assert + self.assert_warning( + DeprecationWarning, + lambda: _make_linear_lut(black, white)) + + def test_private_make_gamma_lut_warning(self): + # Arrange + from PIL.ImagePalette import _make_gamma_lut + exp = 5 + + # Act / Assert + self.assert_warning( + DeprecationWarning, + lambda: _make_gamma_lut(exp)) + + def test_rawmode_valueerrors(self): + # Arrange + from PIL.ImagePalette import raw + palette = raw("RGB", list(range(256))*3) + + # Act / Assert + self.assertRaises(ValueError, lambda: palette.tobytes()) + self.assertRaises(ValueError, lambda: palette.getcolor((1, 2, 3))) + f = self.tempfile("temp.lut") + self.assertRaises(ValueError, lambda: palette.save(f)) + + def test_getdata(self): + # Arrange + data_in = list(range(256))*3 + palette = ImagePalette("RGB", data_in) + + # Act + mode, data_out = palette.getdata() + + # Assert + self.assertEqual(mode, "RGB;L") + + def test_rawmode_getdata(self): + # Arrange + from PIL.ImagePalette import raw + data_in = list(range(256))*3 + palette = raw("RGB", data_in) + + # Act + rawmode, data_out = palette.getdata() + + # Assert + self.assertEqual(rawmode, "RGB") + self.assertEqual(data_in, data_out) +if __name__ == '__main__': + unittest.main() +# End of file diff --git a/Tests/test_imagepath.py b/Tests/test_imagepath.py index 9e9e63c3a..cd221b5ca 100644 --- a/Tests/test_imagepath.py +++ b/Tests/test_imagepath.py @@ -1,54 +1,68 @@ -from tester import * +from helper import unittest, PillowTestCase -from PIL import Image from PIL import ImagePath import array -def test_path(): - p = ImagePath.Path(list(range(10))) +class TestImagePath(PillowTestCase): - # sequence interface - assert_equal(len(p), 5) - assert_equal(p[0], (0.0, 1.0)) - assert_equal(p[-1], (8.0, 9.0)) - assert_equal(list(p[:1]), [(0.0, 1.0)]) - assert_equal(list(p), [(0.0, 1.0), (2.0, 3.0), (4.0, 5.0), (6.0, 7.0), (8.0, 9.0)]) + def test_path(self): - # method sanity check - assert_equal(p.tolist(), [(0.0, 1.0), (2.0, 3.0), (4.0, 5.0), (6.0, 7.0), (8.0, 9.0)]) - assert_equal(p.tolist(1), [0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0]) + p = ImagePath.Path(list(range(10))) - assert_equal(p.getbbox(), (0.0, 1.0, 8.0, 9.0)) + # sequence interface + self.assertEqual(len(p), 5) + self.assertEqual(p[0], (0.0, 1.0)) + self.assertEqual(p[-1], (8.0, 9.0)) + self.assertEqual(list(p[:1]), [(0.0, 1.0)]) + self.assertEqual( + list(p), + [(0.0, 1.0), (2.0, 3.0), (4.0, 5.0), (6.0, 7.0), (8.0, 9.0)]) - assert_equal(p.compact(5), 2) - assert_equal(list(p), [(0.0, 1.0), (4.0, 5.0), (8.0, 9.0)]) + # method sanity check + self.assertEqual( + p.tolist(), + [(0.0, 1.0), (2.0, 3.0), (4.0, 5.0), (6.0, 7.0), (8.0, 9.0)]) + self.assertEqual( + p.tolist(1), + [0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0]) - p.transform((1,0,1,0,1,1)) - assert_equal(list(p), [(1.0, 2.0), (5.0, 6.0), (9.0, 10.0)]) + self.assertEqual(p.getbbox(), (0.0, 1.0, 8.0, 9.0)) - # alternative constructors - p = ImagePath.Path([0, 1]) - assert_equal(list(p), [(0.0, 1.0)]) - p = ImagePath.Path([0.0, 1.0]) - assert_equal(list(p), [(0.0, 1.0)]) - p = ImagePath.Path([0, 1]) - assert_equal(list(p), [(0.0, 1.0)]) - p = ImagePath.Path([(0, 1)]) - assert_equal(list(p), [(0.0, 1.0)]) - p = ImagePath.Path(p) - assert_equal(list(p), [(0.0, 1.0)]) - p = ImagePath.Path(p.tolist(0)) - assert_equal(list(p), [(0.0, 1.0)]) - p = ImagePath.Path(p.tolist(1)) - assert_equal(list(p), [(0.0, 1.0)]) - p = ImagePath.Path(array.array("f", [0, 1])) - assert_equal(list(p), [(0.0, 1.0)]) + self.assertEqual(p.compact(5), 2) + self.assertEqual(list(p), [(0.0, 1.0), (4.0, 5.0), (8.0, 9.0)]) - arr = array.array("f", [0, 1]) - if hasattr(arr, 'tobytes'): - p = ImagePath.Path(arr.tobytes()) - else: - p = ImagePath.Path(arr.tostring()) - assert_equal(list(p), [(0.0, 1.0)]) + p.transform((1, 0, 1, 0, 1, 1)) + self.assertEqual(list(p), [(1.0, 2.0), (5.0, 6.0), (9.0, 10.0)]) + + # alternative constructors + p = ImagePath.Path([0, 1]) + self.assertEqual(list(p), [(0.0, 1.0)]) + p = ImagePath.Path([0.0, 1.0]) + self.assertEqual(list(p), [(0.0, 1.0)]) + p = ImagePath.Path([0, 1]) + self.assertEqual(list(p), [(0.0, 1.0)]) + p = ImagePath.Path([(0, 1)]) + self.assertEqual(list(p), [(0.0, 1.0)]) + p = ImagePath.Path(p) + self.assertEqual(list(p), [(0.0, 1.0)]) + p = ImagePath.Path(p.tolist(0)) + self.assertEqual(list(p), [(0.0, 1.0)]) + p = ImagePath.Path(p.tolist(1)) + self.assertEqual(list(p), [(0.0, 1.0)]) + p = ImagePath.Path(array.array("f", [0, 1])) + self.assertEqual(list(p), [(0.0, 1.0)]) + + arr = array.array("f", [0, 1]) + if hasattr(arr, 'tobytes'): + p = ImagePath.Path(arr.tobytes()) + else: + p = ImagePath.Path(arr.tostring()) + self.assertEqual(list(p), [(0.0, 1.0)]) + + +if __name__ == '__main__': + unittest.main() + +# End of file diff --git a/Tests/test_imageqt.py b/Tests/test_imageqt.py index 73d1f4b1c..fd50bf320 100644 --- a/Tests/test_imageqt.py +++ b/Tests/test_imageqt.py @@ -1,37 +1,53 @@ -from tester import * - -from PIL import Image +from helper import unittest, PillowTestCase, lena try: + from PIL import ImageQt from PyQt5.QtGui import QImage, qRgb, qRgba except: try: from PyQt4.QtGui import QImage, qRgb, qRgba except: - skip('PyQT4 or 5 not installed') - -from PIL import ImageQt - -def test_rgb(): - # from https://qt-project.org/doc/qt-4.8/qcolor.html - # typedef QRgb - # An ARGB quadruplet on the format #AARRGGBB, equivalent to an unsigned int. - - assert_equal(qRgb(0,0,0), qRgba(0,0,0,255)) - - def checkrgb(r,g,b): - val = ImageQt.rgb(r,g,b) - val = val % 2**24 # drop the alpha - assert_equal(val >> 16, r) - assert_equal(((val >> 8 ) % 2**8), g) - assert_equal(val % 2**8, b) - - checkrgb(0,0,0) - checkrgb(255,0,0) - checkrgb(0,255,0) - checkrgb(0,0,255) + # Will be skipped in setUp + pass -def test_image(): - for mode in ('1', 'RGB', 'RGBA', 'L', 'P'): - assert_no_exception(lambda: ImageQt.ImageQt(lena(mode))) +class TestImageQt(PillowTestCase): + + def setUp(self): + try: + from PyQt5.QtGui import QImage, qRgb, qRgba + except: + try: + from PyQt4.QtGui import QImage, qRgb, qRgba + except: + self.skipTest('PyQt4 or 5 not installed') + + def test_rgb(self): + # from https://qt-project.org/doc/qt-4.8/qcolor.html + # typedef QRgb + # An ARGB quadruplet on the format #AARRGGBB, + # equivalent to an unsigned int. + + self.assertEqual(qRgb(0, 0, 0), qRgba(0, 0, 0, 255)) + + def checkrgb(r, g, b): + val = ImageQt.rgb(r, g, b) + val = val % 2**24 # drop the alpha + self.assertEqual(val >> 16, r) + self.assertEqual(((val >> 8) % 2**8), g) + self.assertEqual(val % 2**8, b) + + checkrgb(0, 0, 0) + checkrgb(255, 0, 0) + checkrgb(0, 255, 0) + checkrgb(0, 0, 255) + + def test_image(self): + for mode in ('1', 'RGB', 'RGBA', 'L', 'P'): + ImageQt.ImageQt(lena(mode)) + + +if __name__ == '__main__': + unittest.main() + +# End of file diff --git a/Tests/test_imagesequence.py b/Tests/test_imagesequence.py index 3329b1a05..fd10e5989 100644 --- a/Tests/test_imagesequence.py +++ b/Tests/test_imagesequence.py @@ -1,22 +1,29 @@ -from tester import * +from helper import unittest, PillowTestCase, lena -from PIL import Image from PIL import ImageSequence -def test_sanity(): - file = tempfile("temp.im") +class TestImageSequence(PillowTestCase): - im = lena("RGB") - im.save(file) + def test_sanity(self): - seq = ImageSequence.Iterator(im) + file = self.tempfile("temp.im") - index = 0 - for frame in seq: - assert_image_equal(im, frame) - assert_equal(im.tell(), index) - index = index + 1 + im = lena("RGB") + im.save(file) - assert_equal(index, 1) + seq = ImageSequence.Iterator(im) + index = 0 + for frame in seq: + self.assert_image_equal(im, frame) + self.assertEqual(im.tell(), index) + index += 1 + + self.assertEqual(index, 1) + + +if __name__ == '__main__': + unittest.main() + +# End of file diff --git a/Tests/test_imageshow.py b/Tests/test_imageshow.py index 99ec005c8..e94ae2d0a 100644 --- a/Tests/test_imageshow.py +++ b/Tests/test_imageshow.py @@ -1,6 +1,18 @@ -from tester import * +from helper import unittest, PillowTestCase from PIL import Image from PIL import ImageShow -success() + +class TestImageShow(PillowTestCase): + + def test_sanity(self): + dir(Image) + dir(ImageShow) + pass + + +if __name__ == '__main__': + unittest.main() + +# End of file diff --git a/Tests/test_imagestat.py b/Tests/test_imagestat.py index 02a461e22..4d30ff023 100644 --- a/Tests/test_imagestat.py +++ b/Tests/test_imagestat.py @@ -1,52 +1,63 @@ -from tester import * +from helper import unittest, PillowTestCase, lena from PIL import Image from PIL import ImageStat -def test_sanity(): - im = lena() +class TestImageStat(PillowTestCase): - st = ImageStat.Stat(im) - st = ImageStat.Stat(im.histogram()) - st = ImageStat.Stat(im, Image.new("1", im.size, 1)) + def test_sanity(self): - assert_no_exception(lambda: st.extrema) - assert_no_exception(lambda: st.sum) - assert_no_exception(lambda: st.mean) - assert_no_exception(lambda: st.median) - assert_no_exception(lambda: st.rms) - assert_no_exception(lambda: st.sum2) - assert_no_exception(lambda: st.var) - assert_no_exception(lambda: st.stddev) - assert_exception(AttributeError, lambda: st.spam) + im = lena() - assert_exception(TypeError, lambda: ImageStat.Stat(1)) + st = ImageStat.Stat(im) + st = ImageStat.Stat(im.histogram()) + st = ImageStat.Stat(im, Image.new("1", im.size, 1)) -def test_lena(): + # Check these run. Exceptions will cause failures. + st.extrema + st.sum + st.mean + st.median + st.rms + st.sum2 + st.var + st.stddev - im = lena() + self.assertRaises(AttributeError, lambda: st.spam) - st = ImageStat.Stat(im) + self.assertRaises(TypeError, lambda: ImageStat.Stat(1)) - # verify a few values - assert_equal(st.extrema[0], (61, 255)) - assert_equal(st.median[0], 197) - assert_equal(st.sum[0], 2954416) - assert_equal(st.sum[1], 2027250) - assert_equal(st.sum[2], 1727331) + def test_lena(self): -def test_constant(): + im = lena() - im = Image.new("L", (128, 128), 128) + st = ImageStat.Stat(im) - 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) - assert_equal(st.extrema[0], (128, 128)) - assert_equal(st.sum[0], 128**3) - assert_equal(st.sum2[0], 128**4) - assert_equal(st.mean[0], 128) - assert_equal(st.median[0], 128) - assert_equal(st.rms[0], 128) - assert_equal(st.var[0], 0) - assert_equal(st.stddev[0], 0) + def test_constant(self): + + im = Image.new("L", (128, 128), 128) + + st = ImageStat.Stat(im) + + self.assertEqual(st.extrema[0], (128, 128)) + self.assertEqual(st.sum[0], 128**3) + self.assertEqual(st.sum2[0], 128**4) + self.assertEqual(st.mean[0], 128) + self.assertEqual(st.median[0], 128) + self.assertEqual(st.rms[0], 128) + self.assertEqual(st.var[0], 0) + self.assertEqual(st.stddev[0], 0) + + +if __name__ == '__main__': + unittest.main() + +# End of file diff --git a/Tests/test_imagetk.py b/Tests/test_imagetk.py index b30971e8f..87a07e288 100644 --- a/Tests/test_imagetk.py +++ b/Tests/test_imagetk.py @@ -1,9 +1,17 @@ -from tester import * +from helper import unittest, PillowTestCase -from PIL import Image -try: - from PIL import ImageTk -except (OSError, ImportError) as v: - skip(v) -success() +class TestImageTk(PillowTestCase): + + def test_import(self): + try: + from PIL import ImageTk + dir(ImageTk) + except (OSError, ImportError) as v: + self.skipTest(v) + + +if __name__ == '__main__': + unittest.main() + +# End of file diff --git a/Tests/test_imagetransform.py b/Tests/test_imagetransform.py deleted file mode 100644 index 884e6bb1c..000000000 --- a/Tests/test_imagetransform.py +++ /dev/null @@ -1,18 +0,0 @@ -from tester import * - -from PIL import Image -from PIL import ImageTransform - -im = Image.new("L", (100, 100)) - -seq = tuple(range(10)) - -def test_sanity(): - transform = ImageTransform.AffineTransform(seq[:6]) - assert_no_exception(lambda: im.transform((100, 100), transform)) - transform = ImageTransform.ExtentTransform(seq[:4]) - assert_no_exception(lambda: im.transform((100, 100), transform)) - transform = ImageTransform.QuadTransform(seq[:8]) - assert_no_exception(lambda: im.transform((100, 100), transform)) - transform = ImageTransform.MeshTransform([(seq[:4], seq[:8])]) - assert_no_exception(lambda: im.transform((100, 100), transform)) diff --git a/Tests/test_imagewin.py b/Tests/test_imagewin.py index 268a75b6f..69dbdbe82 100644 --- a/Tests/test_imagewin.py +++ b/Tests/test_imagewin.py @@ -1,6 +1,18 @@ -from tester import * +from helper import unittest, PillowTestCase, lena from PIL import Image from PIL import ImageWin -success() + +class TestImageWin(PillowTestCase): + + def test_sanity(self): + dir(Image) + dir(ImageWin) + pass + + +if __name__ == '__main__': + unittest.main() + +# End of file diff --git a/Tests/test_lib_image.py b/Tests/test_lib_image.py index 93aa694d8..1a16deff2 100644 --- a/Tests/test_lib_image.py +++ b/Tests/test_lib_image.py @@ -1,30 +1,39 @@ -from tester import * +from helper import unittest, PillowTestCase from PIL import Image -def test_setmode(): - im = Image.new("L", (1, 1), 255) - im.im.setmode("1") - assert_equal(im.im.getpixel((0, 0)), 255) - im.im.setmode("L") - assert_equal(im.im.getpixel((0, 0)), 255) +class TestLibImage(PillowTestCase): - im = Image.new("1", (1, 1), 1) - im.im.setmode("L") - assert_equal(im.im.getpixel((0, 0)), 255) - im.im.setmode("1") - assert_equal(im.im.getpixel((0, 0)), 255) + def test_setmode(self): - im = Image.new("RGB", (1, 1), (1, 2, 3)) - im.im.setmode("RGB") - assert_equal(im.im.getpixel((0, 0)), (1, 2, 3)) - im.im.setmode("RGBA") - assert_equal(im.im.getpixel((0, 0)), (1, 2, 3, 255)) - im.im.setmode("RGBX") - assert_equal(im.im.getpixel((0, 0)), (1, 2, 3, 255)) - im.im.setmode("RGB") - assert_equal(im.im.getpixel((0, 0)), (1, 2, 3)) + im = Image.new("L", (1, 1), 255) + im.im.setmode("1") + self.assertEqual(im.im.getpixel((0, 0)), 255) + im.im.setmode("L") + self.assertEqual(im.im.getpixel((0, 0)), 255) - assert_exception(ValueError, lambda: im.im.setmode("L")) - assert_exception(ValueError, lambda: im.im.setmode("RGBABCDE")) + im = Image.new("1", (1, 1), 1) + im.im.setmode("L") + self.assertEqual(im.im.getpixel((0, 0)), 255) + im.im.setmode("1") + self.assertEqual(im.im.getpixel((0, 0)), 255) + + im = Image.new("RGB", (1, 1), (1, 2, 3)) + im.im.setmode("RGB") + self.assertEqual(im.im.getpixel((0, 0)), (1, 2, 3)) + im.im.setmode("RGBA") + self.assertEqual(im.im.getpixel((0, 0)), (1, 2, 3, 255)) + im.im.setmode("RGBX") + self.assertEqual(im.im.getpixel((0, 0)), (1, 2, 3, 255)) + im.im.setmode("RGB") + self.assertEqual(im.im.getpixel((0, 0)), (1, 2, 3)) + + self.assertRaises(ValueError, lambda: im.im.setmode("L")) + self.assertRaises(ValueError, lambda: im.im.setmode("RGBABCDE")) + + +if __name__ == '__main__': + unittest.main() + +# End of file diff --git a/Tests/test_lib_pack.py b/Tests/test_lib_pack.py index 7675348b3..102835b58 100644 --- a/Tests/test_lib_pack.py +++ b/Tests/test_lib_pack.py @@ -1,138 +1,147 @@ -from tester import * +from helper import unittest, PillowTestCase, py3 from PIL import Image -def pack(): - pass # not yet -def test_pack(): +class TestLibPack(PillowTestCase): - def pack(mode, rawmode): - if len(mode) == 1: - im = Image.new(mode, (1, 1), 1) - else: - im = Image.new(mode, (1, 1), (1, 2, 3, 4)[:len(mode)]) + def pack(self): + pass # not yet - if py3: - return list(im.tobytes("raw", rawmode)) - else: - return [ord(c) for c in im.tobytes("raw", rawmode)] + def test_pack(self): - order = 1 if Image._ENDIAN == '<' else -1 + def pack(mode, rawmode): + if len(mode) == 1: + im = Image.new(mode, (1, 1), 1) + else: + im = Image.new(mode, (1, 1), (1, 2, 3, 4)[:len(mode)]) - assert_equal(pack("1", "1"), [128]) - assert_equal(pack("1", "1;I"), [0]) - assert_equal(pack("1", "1;R"), [1]) - assert_equal(pack("1", "1;IR"), [0]) + if py3: + return list(im.tobytes("raw", rawmode)) + else: + return [ord(c) for c in im.tobytes("raw", rawmode)] - assert_equal(pack("L", "L"), [1]) + order = 1 if Image._ENDIAN == '<' else -1 - assert_equal(pack("I", "I"), [1, 0, 0, 0][::order]) + self.assertEqual(pack("1", "1"), [128]) + self.assertEqual(pack("1", "1;I"), [0]) + self.assertEqual(pack("1", "1;R"), [1]) + self.assertEqual(pack("1", "1;IR"), [0]) - assert_equal(pack("F", "F"), [0, 0, 128, 63][::order]) + self.assertEqual(pack("L", "L"), [1]) - assert_equal(pack("LA", "LA"), [1, 2]) + self.assertEqual(pack("I", "I"), [1, 0, 0, 0][::order]) - assert_equal(pack("RGB", "RGB"), [1, 2, 3]) - assert_equal(pack("RGB", "RGB;L"), [1, 2, 3]) - assert_equal(pack("RGB", "BGR"), [3, 2, 1]) - assert_equal(pack("RGB", "RGBX"), [1, 2, 3, 255]) # 255? - assert_equal(pack("RGB", "BGRX"), [3, 2, 1, 0]) - assert_equal(pack("RGB", "XRGB"), [0, 1, 2, 3]) - assert_equal(pack("RGB", "XBGR"), [0, 3, 2, 1]) + self.assertEqual(pack("F", "F"), [0, 0, 128, 63][::order]) - assert_equal(pack("RGBX", "RGBX"), [1, 2, 3, 4]) # 4->255? + self.assertEqual(pack("LA", "LA"), [1, 2]) - assert_equal(pack("RGBA", "RGBA"), [1, 2, 3, 4]) + self.assertEqual(pack("RGB", "RGB"), [1, 2, 3]) + self.assertEqual(pack("RGB", "RGB;L"), [1, 2, 3]) + self.assertEqual(pack("RGB", "BGR"), [3, 2, 1]) + self.assertEqual(pack("RGB", "RGBX"), [1, 2, 3, 255]) # 255? + self.assertEqual(pack("RGB", "BGRX"), [3, 2, 1, 0]) + self.assertEqual(pack("RGB", "XRGB"), [0, 1, 2, 3]) + self.assertEqual(pack("RGB", "XBGR"), [0, 3, 2, 1]) - assert_equal(pack("CMYK", "CMYK"), [1, 2, 3, 4]) - assert_equal(pack("YCbCr", "YCbCr"), [1, 2, 3]) + self.assertEqual(pack("RGBX", "RGBX"), [1, 2, 3, 4]) # 4->255? -def test_unpack(): + self.assertEqual(pack("RGBA", "RGBA"), [1, 2, 3, 4]) - def unpack(mode, rawmode, bytes_): - im = None + self.assertEqual(pack("CMYK", "CMYK"), [1, 2, 3, 4]) + self.assertEqual(pack("YCbCr", "YCbCr"), [1, 2, 3]) - if py3: - data = bytes(range(1,bytes_+1)) - else: - data = ''.join(chr(i) for i in range(1,bytes_+1)) + def test_unpack(self): - im = Image.frombytes(mode, (1, 1), data, "raw", rawmode, 0, 1) + def unpack(mode, rawmode, bytes_): + im = None - return im.getpixel((0, 0)) + if py3: + data = bytes(range(1, bytes_+1)) + else: + data = ''.join(chr(i) for i in range(1, bytes_+1)) - def unpack_1(mode, rawmode, value): - assert mode == "1" - im = None + im = Image.frombytes(mode, (1, 1), data, "raw", rawmode, 0, 1) - if py3: - im = Image.frombytes(mode, (8, 1), bytes([value]), "raw", rawmode, 0, 1) - else: - im = Image.frombytes(mode, (8, 1), chr(value), "raw", rawmode, 0, 1) + return im.getpixel((0, 0)) - return tuple(im.getdata()) + def unpack_1(mode, rawmode, value): + assert mode == "1" + im = None - X = 255 + if py3: + im = Image.frombytes( + mode, (8, 1), bytes([value]), "raw", rawmode, 0, 1) + else: + im = Image.frombytes( + mode, (8, 1), chr(value), "raw", rawmode, 0, 1) - assert_equal(unpack_1("1", "1", 1), (0,0,0,0,0,0,0,X)) - assert_equal(unpack_1("1", "1;I", 1), (X,X,X,X,X,X,X,0)) - assert_equal(unpack_1("1", "1;R", 1), (X,0,0,0,0,0,0,0)) - assert_equal(unpack_1("1", "1;IR", 1), (0,X,X,X,X,X,X,X)) + return tuple(im.getdata()) - assert_equal(unpack_1("1", "1", 170), (X,0,X,0,X,0,X,0)) - assert_equal(unpack_1("1", "1;I", 170), (0,X,0,X,0,X,0,X)) - assert_equal(unpack_1("1", "1;R", 170), (0,X,0,X,0,X,0,X)) - assert_equal(unpack_1("1", "1;IR", 170), (X,0,X,0,X,0,X,0)) + X = 255 - assert_equal(unpack("L", "L;2", 1), 0) - assert_equal(unpack("L", "L;4", 1), 0) - assert_equal(unpack("L", "L", 1), 1) - assert_equal(unpack("L", "L;I", 1), 254) - assert_equal(unpack("L", "L;R", 1), 128) - assert_equal(unpack("L", "L;16", 2), 2) # little endian - assert_equal(unpack("L", "L;16B", 2), 1) # big endian + self.assertEqual(unpack_1("1", "1", 1), (0, 0, 0, 0, 0, 0, 0, X)) + self.assertEqual(unpack_1("1", "1;I", 1), (X, X, X, X, X, X, X, 0)) + self.assertEqual(unpack_1("1", "1;R", 1), (X, 0, 0, 0, 0, 0, 0, 0)) + self.assertEqual(unpack_1("1", "1;IR", 1), (0, X, X, X, X, X, X, X)) - assert_equal(unpack("LA", "LA", 2), (1, 2)) - assert_equal(unpack("LA", "LA;L", 2), (1, 2)) + self.assertEqual(unpack_1("1", "1", 170), (X, 0, X, 0, X, 0, X, 0)) + self.assertEqual(unpack_1("1", "1;I", 170), (0, X, 0, X, 0, X, 0, X)) + self.assertEqual(unpack_1("1", "1;R", 170), (0, X, 0, X, 0, X, 0, X)) + self.assertEqual(unpack_1("1", "1;IR", 170), (X, 0, X, 0, X, 0, X, 0)) - assert_equal(unpack("RGB", "RGB", 3), (1, 2, 3)) - assert_equal(unpack("RGB", "RGB;L", 3), (1, 2, 3)) - assert_equal(unpack("RGB", "RGB;R", 3), (128, 64, 192)) - assert_equal(unpack("RGB", "RGB;16B", 6), (1, 3, 5)) # ? - assert_equal(unpack("RGB", "BGR", 3), (3, 2, 1)) - assert_equal(unpack("RGB", "RGB;15", 2), (8, 131, 0)) - assert_equal(unpack("RGB", "BGR;15", 2), (0, 131, 8)) - assert_equal(unpack("RGB", "RGB;16", 2), (8, 64, 0)) - assert_equal(unpack("RGB", "BGR;16", 2), (0, 64, 8)) - assert_equal(unpack("RGB", "RGB;4B", 2), (17, 0, 34)) + self.assertEqual(unpack("L", "L;2", 1), 0) + self.assertEqual(unpack("L", "L;4", 1), 0) + self.assertEqual(unpack("L", "L", 1), 1) + self.assertEqual(unpack("L", "L;I", 1), 254) + self.assertEqual(unpack("L", "L;R", 1), 128) + self.assertEqual(unpack("L", "L;16", 2), 2) # little endian + self.assertEqual(unpack("L", "L;16B", 2), 1) # big endian - assert_equal(unpack("RGB", "RGBX", 4), (1, 2, 3)) - assert_equal(unpack("RGB", "BGRX", 4), (3, 2, 1)) - assert_equal(unpack("RGB", "XRGB", 4), (2, 3, 4)) - assert_equal(unpack("RGB", "XBGR", 4), (4, 3, 2)) + self.assertEqual(unpack("LA", "LA", 2), (1, 2)) + self.assertEqual(unpack("LA", "LA;L", 2), (1, 2)) - assert_equal(unpack("RGBA", "RGBA", 4), (1, 2, 3, 4)) - assert_equal(unpack("RGBA", "BGRA", 4), (3, 2, 1, 4)) - assert_equal(unpack("RGBA", "ARGB", 4), (2, 3, 4, 1)) - assert_equal(unpack("RGBA", "ABGR", 4), (4, 3, 2, 1)) - assert_equal(unpack("RGBA", "RGBA;15", 2), (8, 131, 0, 0)) - assert_equal(unpack("RGBA", "BGRA;15", 2), (0, 131, 8, 0)) - assert_equal(unpack("RGBA", "RGBA;4B", 2), (17, 0, 34, 0)) + self.assertEqual(unpack("RGB", "RGB", 3), (1, 2, 3)) + self.assertEqual(unpack("RGB", "RGB;L", 3), (1, 2, 3)) + self.assertEqual(unpack("RGB", "RGB;R", 3), (128, 64, 192)) + self.assertEqual(unpack("RGB", "RGB;16B", 6), (1, 3, 5)) # ? + self.assertEqual(unpack("RGB", "BGR", 3), (3, 2, 1)) + self.assertEqual(unpack("RGB", "RGB;15", 2), (8, 131, 0)) + self.assertEqual(unpack("RGB", "BGR;15", 2), (0, 131, 8)) + self.assertEqual(unpack("RGB", "RGB;16", 2), (8, 64, 0)) + self.assertEqual(unpack("RGB", "BGR;16", 2), (0, 64, 8)) + self.assertEqual(unpack("RGB", "RGB;4B", 2), (17, 0, 34)) - assert_equal(unpack("RGBX", "RGBX", 4), (1, 2, 3, 4)) # 4->255? - assert_equal(unpack("RGBX", "BGRX", 4), (3, 2, 1, 255)) - assert_equal(unpack("RGBX", "XRGB", 4), (2, 3, 4, 255)) - assert_equal(unpack("RGBX", "XBGR", 4), (4, 3, 2, 255)) - assert_equal(unpack("RGBX", "RGB;15", 2), (8, 131, 0, 255)) - assert_equal(unpack("RGBX", "BGR;15", 2), (0, 131, 8, 255)) - assert_equal(unpack("RGBX", "RGB;4B", 2), (17, 0, 34, 255)) + self.assertEqual(unpack("RGB", "RGBX", 4), (1, 2, 3)) + self.assertEqual(unpack("RGB", "BGRX", 4), (3, 2, 1)) + self.assertEqual(unpack("RGB", "XRGB", 4), (2, 3, 4)) + self.assertEqual(unpack("RGB", "XBGR", 4), (4, 3, 2)) - assert_equal(unpack("CMYK", "CMYK", 4), (1, 2, 3, 4)) - assert_equal(unpack("CMYK", "CMYK;I", 4), (254, 253, 252, 251)) + 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("RGBA", "RGBA;15", 2), (8, 131, 0, 0)) + self.assertEqual(unpack("RGBA", "BGRA;15", 2), (0, 131, 8, 0)) + self.assertEqual(unpack("RGBA", "RGBA;4B", 2), (17, 0, 34, 0)) - assert_exception(ValueError, lambda: unpack("L", "L", 0)) - assert_exception(ValueError, lambda: unpack("RGB", "RGB", 2)) - assert_exception(ValueError, lambda: unpack("CMYK", "CMYK", 2)) + 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)) + self.assertEqual(unpack("RGBX", "XBGR", 4), (4, 3, 2, 255)) + self.assertEqual(unpack("RGBX", "RGB;15", 2), (8, 131, 0, 255)) + self.assertEqual(unpack("RGBX", "BGR;15", 2), (0, 131, 8, 255)) + self.assertEqual(unpack("RGBX", "RGB;4B", 2), (17, 0, 34, 255)) -run() + self.assertEqual(unpack("CMYK", "CMYK", 4), (1, 2, 3, 4)) + self.assertEqual(unpack("CMYK", "CMYK;I", 4), (254, 253, 252, 251)) + + self.assertRaises(ValueError, lambda: unpack("L", "L", 0)) + self.assertRaises(ValueError, lambda: unpack("RGB", "RGB", 2)) + self.assertRaises(ValueError, lambda: unpack("CMYK", "CMYK", 2)) + + +if __name__ == '__main__': + unittest.main() + +# End of file diff --git a/Tests/test_locale.py b/Tests/test_locale.py index 2683c561b..9ef136bf9 100644 --- a/Tests/test_locale.py +++ b/Tests/test_locale.py @@ -1,31 +1,39 @@ -from tester import * +from helper import unittest, PillowTestCase, lena + from PIL import Image import locale -# ref https://github.com/python-imaging/Pillow/issues/272 -## on windows, in polish locale: +# ref https://github.com/python-pillow/Pillow/issues/272 +# on windows, in polish locale: -## import locale -## print locale.setlocale(locale.LC_ALL, 'polish') -## import string -## print len(string.whitespace) -## print ord(string.whitespace[6]) +# import locale +# print locale.setlocale(locale.LC_ALL, 'polish') +# import string +# print len(string.whitespace) +# print ord(string.whitespace[6]) -## Polish_Poland.1250 -## 7 -## 160 +# Polish_Poland.1250 +# 7 +# 160 -# one of string.whitespace is not freely convertable into ascii. +# one of string.whitespace is not freely convertable into ascii. -path = "Images/lena.jpg" +path = "Tests/images/lena.jpg" -def test_sanity(): - assert_no_exception(lambda: Image.open(path)) - try: - locale.setlocale(locale.LC_ALL, "polish") - except: - skip('polish locale not available') - import string - assert_no_exception(lambda: Image.open(path)) +class TestLocale(PillowTestCase): + + def test_sanity(self): + Image.open(path) + try: + locale.setlocale(locale.LC_ALL, "polish") + except: + unittest.skip('Polish locale not available') + Image.open(path) + + +if __name__ == '__main__': + unittest.main() + +# End of file diff --git a/Tests/test_mode_i16.py b/Tests/test_mode_i16.py index 782a26623..0cd5dba0f 100644 --- a/Tests/test_mode_i16.py +++ b/Tests/test_mode_i16.py @@ -1,107 +1,113 @@ -from tester import * +from helper import unittest, PillowTestCase, lena from PIL import Image -def verify(im1): - im2 = lena("I") - assert_equal(im1.size, im2.size) - pix1 = im1.load() - pix2 = im2.load() - for y in range(im1.size[1]): - for x in range(im1.size[0]): - xy = x, y - if pix1[xy] != pix2[xy]: - failure( - "got %r from mode %s at %s, expected %r" % - (pix1[xy], im1.mode, xy, pix2[xy]) - ) - return - success() +class TestModeI16(PillowTestCase): + + original = lena().resize((32,32)).convert('I') + + def verify(self, im1): + im2 = self.original.copy() + self.assertEqual(im1.size, im2.size) + pix1 = im1.load() + pix2 = im2.load() + for y in range(im1.size[1]): + for x in range(im1.size[0]): + xy = x, y + p1 = pix1[xy] + p2 = pix2[xy] + self.assertEqual( + p1, p2, + ("got %r from mode %s at %s, expected %r" % + (p1, im1.mode, xy, p2))) + + def test_basic(self): + # PIL 1.1 has limited support for 16-bit image data. Check that + # create/copy/transform and save works as expected. + + def basic(mode): + + imIn = self.original.convert(mode) + self.verify(imIn) + + w, h = imIn.size + + imOut = imIn.copy() + self.verify(imOut) # copy + + imOut = imIn.transform((w, h), Image.EXTENT, (0, 0, w, h)) + self.verify(imOut) # transform + + filename = self.tempfile("temp.im") + imIn.save(filename) + + imOut = Image.open(filename) + + self.verify(imIn) + self.verify(imOut) + + imOut = imIn.crop((0, 0, w, h)) + self.verify(imOut) + + imOut = Image.new(mode, (w, h), None) + imOut.paste(imIn.crop((0, 0, w//2, h)), (0, 0)) + imOut.paste(imIn.crop((w//2, 0, w, h)), (w//2, 0)) + + self.verify(imIn) + self.verify(imOut) + + imIn = Image.new(mode, (1, 1), 1) + self.assertEqual(imIn.getpixel((0, 0)), 1) + + imIn.putpixel((0, 0), 2) + self.assertEqual(imIn.getpixel((0, 0)), 2) + + if mode == "L": + max = 255 + else: + max = 32767 + + imIn = Image.new(mode, (1, 1), 256) + self.assertEqual(imIn.getpixel((0, 0)), min(256, max)) + + imIn.putpixel((0, 0), 512) + self.assertEqual(imIn.getpixel((0, 0)), min(512, max)) + + basic("L") + + basic("I;16") + basic("I;16B") + basic("I;16L") + + basic("I") + + def test_tobytes(self): + + def tobytes(mode): + return Image.new(mode, (1, 1), 1).tobytes() + + order = 1 if Image._ENDIAN == '<' else -1 + + self.assertEqual(tobytes("L"), b"\x01") + self.assertEqual(tobytes("I;16"), b"\x01\x00") + self.assertEqual(tobytes("I;16B"), b"\x00\x01") + self.assertEqual(tobytes("I"), b"\x01\x00\x00\x00"[::order]) + + def test_convert(self): + + im = self.original.copy() + + self.verify(im.convert("I;16")) + self.verify(im.convert("I;16").convert("L")) + self.verify(im.convert("I;16").convert("I")) + + self.verify(im.convert("I;16B")) + self.verify(im.convert("I;16B").convert("L")) + self.verify(im.convert("I;16B").convert("I")) -def test_basic(): - # PIL 1.1 has limited support for 16-bit image data. Check that - # create/copy/transform and save works as expected. +if __name__ == '__main__': + unittest.main() - def basic(mode): - - imIn = lena("I").convert(mode) - verify(imIn) - - w, h = imIn.size - - imOut = imIn.copy() - verify(imOut) # copy - - imOut = imIn.transform((w, h), Image.EXTENT, (0, 0, w, h)) - verify(imOut) # transform - - filename = tempfile("temp.im") - imIn.save(filename) - - imOut = Image.open(filename) - - verify(imIn) - verify(imOut) - - imOut = imIn.crop((0, 0, w, h)) - verify(imOut) - - imOut = Image.new(mode, (w, h), None) - imOut.paste(imIn.crop((0, 0, w//2, h)), (0, 0)) - imOut.paste(imIn.crop((w//2, 0, w, h)), (w//2, 0)) - - verify(imIn) - verify(imOut) - - imIn = Image.new(mode, (1, 1), 1) - assert_equal(imIn.getpixel((0, 0)), 1) - - imIn.putpixel((0, 0), 2) - assert_equal(imIn.getpixel((0, 0)), 2) - - if mode == "L": - max = 255 - else: - max = 32767 - - imIn = Image.new(mode, (1, 1), 256) - assert_equal(imIn.getpixel((0, 0)), min(256, max)) - - imIn.putpixel((0, 0), 512) - assert_equal(imIn.getpixel((0, 0)), min(512, max)) - - basic("L") - - basic("I;16") - basic("I;16B") - basic("I;16L") - - basic("I") - - -def test_tobytes(): - - def tobytes(mode): - return Image.new(mode, (1, 1), 1).tobytes() - - order = 1 if Image._ENDIAN == '<' else -1 - - assert_equal(tobytes("L"), b"\x01") - assert_equal(tobytes("I;16"), b"\x01\x00") - assert_equal(tobytes("I;16B"), b"\x00\x01") - assert_equal(tobytes("I"), b"\x01\x00\x00\x00"[::order]) - - -def test_convert(): - - im = lena("I") - - verify(im.convert("I;16")) - verify(im.convert("I;16").convert("L")) - verify(im.convert("I;16").convert("I")) - - verify(im.convert("I;16B")) - verify(im.convert("I;16B").convert("L")) - verify(im.convert("I;16B").convert("I")) +# End of file diff --git a/Tests/test_numpy.py b/Tests/test_numpy.py index 4833cabb2..07c3e0c21 100644 --- a/Tests/test_numpy.py +++ b/Tests/test_numpy.py @@ -1,120 +1,128 @@ -from tester import * +from helper import unittest, PillowTestCase, lena from PIL import Image -import struct try: import site import numpy except ImportError: - skip() + # Skip via setUp() + pass -def test_numpy_to_image(): - def to_image(dtype, bands=1, bool=0): - if bands == 1: - if bool: - data = [0, 1] * 50 +class TestNumpy(PillowTestCase): + + def setUp(self): + try: + import site + import numpy + except ImportError: + self.skipTest("ImportError") + + def test_numpy_to_image(self): + + def to_image(dtype, bands=1, bool=0): + if bands == 1: + if bool: + data = [0, 1] * 50 + else: + data = list(range(100)) + a = numpy.array(data, dtype=dtype) + a.shape = 10, 10 + i = Image.fromarray(a) + if list(i.getdata()) != data: + print("data mismatch for", dtype) else: data = list(range(100)) - a = numpy.array(data, dtype=dtype) - a.shape = 10, 10 - i = Image.fromarray(a) - if list(i.getdata()) != data: - print("data mismatch for", dtype) + a = numpy.array([[x]*bands for x in data], dtype=dtype) + a.shape = 10, 10, bands + i = Image.fromarray(a) + if list(i.split()[0].getdata()) != list(range(100)): + print("data mismatch for", dtype) + # print dtype, list(i.getdata()) + return i + + # self.assert_image(to_image(numpy.bool, bool=1), "1", (10, 10)) + # self.assert_image(to_image(numpy.bool8, bool=1), "1", (10, 10)) + + self.assertRaises(TypeError, lambda: to_image(numpy.uint)) + self.assert_image(to_image(numpy.uint8), "L", (10, 10)) + self.assertRaises(TypeError, lambda: to_image(numpy.uint16)) + self.assertRaises(TypeError, lambda: to_image(numpy.uint32)) + self.assertRaises(TypeError, lambda: to_image(numpy.uint64)) + + self.assert_image(to_image(numpy.int8), "I", (10, 10)) + if Image._ENDIAN == '<': # Little endian + self.assert_image(to_image(numpy.int16), "I;16", (10, 10)) else: - data = list(range(100)) - a = numpy.array([[x]*bands for x in data], dtype=dtype) - a.shape = 10, 10, bands - i = Image.fromarray(a) - if list(i.split()[0].getdata()) != list(range(100)): - print("data mismatch for", dtype) - # print dtype, list(i.getdata()) - return i + self.assert_image(to_image(numpy.int16), "I;16B", (10, 10)) + self.assert_image(to_image(numpy.int32), "I", (10, 10)) + self.assertRaises(TypeError, lambda: to_image(numpy.int64)) - # assert_image(to_image(numpy.bool, bool=1), "1", (10, 10)) - # assert_image(to_image(numpy.bool8, bool=1), "1", (10, 10)) + self.assert_image(to_image(numpy.float), "F", (10, 10)) + self.assert_image(to_image(numpy.float32), "F", (10, 10)) + self.assert_image(to_image(numpy.float64), "F", (10, 10)) - assert_exception(TypeError, lambda: to_image(numpy.uint)) - assert_image(to_image(numpy.uint8), "L", (10, 10)) - assert_exception(TypeError, lambda: to_image(numpy.uint16)) - assert_exception(TypeError, lambda: to_image(numpy.uint32)) - assert_exception(TypeError, lambda: to_image(numpy.uint64)) + self.assert_image(to_image(numpy.uint8, 3), "RGB", (10, 10)) + self.assert_image(to_image(numpy.uint8, 4), "RGBA", (10, 10)) - assert_image(to_image(numpy.int8), "I", (10, 10)) - if Image._ENDIAN == '<': # Little endian - assert_image(to_image(numpy.int16), "I;16", (10, 10)) - else: - assert_image(to_image(numpy.int16), "I;16B", (10, 10)) - assert_image(to_image(numpy.int32), "I", (10, 10)) - assert_exception(TypeError, lambda: to_image(numpy.int64)) + # based on an erring example at http://is.gd/6F0esS (which resolves to) + # http://stackoverflow.com/questions/10854903/what-is-causing-dimension-dependent-attributeerror-in-pil-fromarray-function + def test_3d_array(self): + a = numpy.ones((10, 10, 10), dtype=numpy.uint8) + self.assert_image(Image.fromarray(a[1, :, :]), "L", (10, 10)) + self.assert_image(Image.fromarray(a[:, 1, :]), "L", (10, 10)) + self.assert_image(Image.fromarray(a[:, :, 1]), "L", (10, 10)) - assert_image(to_image(numpy.float), "F", (10, 10)) - assert_image(to_image(numpy.float32), "F", (10, 10)) - assert_image(to_image(numpy.float64), "F", (10, 10)) + def _test_img_equals_nparray(self, img, np): + self.assertEqual(img.size, np.shape[0:2]) + px = img.load() + for x in range(0, img.size[0], int(img.size[0]/10)): + for y in range(0, img.size[1], int(img.size[1]/10)): + self.assert_deep_equal(px[x, y], np[y, x]) - assert_image(to_image(numpy.uint8, 3), "RGB", (10, 10)) - assert_image(to_image(numpy.uint8, 4), "RGBA", (10, 10)) - - -# based on an erring example at http://is.gd/6F0esS (which resolves to) -# http://stackoverflow.com/questions/10854903/what-is-causing-dimension-dependent-attributeerror-in-pil-fromarray-function -def test_3d_array(): - a = numpy.ones((10, 10, 10), dtype=numpy.uint8) - assert_image(Image.fromarray(a[1, :, :]), "L", (10, 10)) - assert_image(Image.fromarray(a[:, 1, :]), "L", (10, 10)) - assert_image(Image.fromarray(a[:, :, 1]), "L", (10, 10)) - - -def _test_img_equals_nparray(img, np): - assert_equal(img.size, np.shape[0:2]) - px = img.load() - for x in range(0, img.size[0], int(img.size[0]/10)): - for y in range(0, img.size[1], int(img.size[1]/10)): - assert_deep_equal(px[x,y], np[y,x]) - - -def test_16bit(): - img = Image.open('Tests/images/16bit.cropped.tif') - np_img = numpy.array(img) - _test_img_equals_nparray(img, np_img) - assert_equal(np_img.dtype, numpy.dtype('u2'), - ("I;16L", 'u2'), + ("I;16L", '= (3, 0)) - -# some test helpers - -_target = None -_tempfiles = [] -_logfile = None - - -def success(): - import sys - success.count += 1 - if _logfile: - print(sys.argv[0], success.count, failure.count, file=_logfile) - return True - - -def failure(msg=None, frame=None): - import sys - import linecache - failure.count += 1 - if _target: - if frame is None: - frame = sys._getframe() - while frame.f_globals.get("__name__") != _target.__name__: - frame = frame.f_back - location = (frame.f_code.co_filename, frame.f_lineno) - prefix = "%s:%d: " % location - line = linecache.getline(*location) - print(prefix + line.strip() + " failed:") - if msg: - print("- " + msg) - if _logfile: - print(sys.argv[0], success.count, failure.count, file=_logfile) - return False - -success.count = failure.count = 0 - - -# predicates - -def assert_true(v, msg=None): - if v: - success() - else: - failure(msg or "got %r, expected true value" % v) - - -def assert_false(v, msg=None): - if v: - failure(msg or "got %r, expected false value" % v) - else: - success() - - -def assert_equal(a, b, msg=None): - if a == b: - success() - else: - failure(msg or "got %r, expected %r" % (a, b)) - - -def assert_almost_equal(a, b, msg=None, eps=1e-6): - if abs(a-b) < eps: - success() - else: - failure(msg or "got %r, expected %r" % (a, b)) - - -def assert_deep_equal(a, b, msg=None): - try: - if len(a) == len(b): - if all([x == y for x, y in zip(a, b)]): - success() - else: - failure(msg or "got %s, expected %s" % (a, b)) - else: - failure(msg or "got length %s, expected %s" % (len(a), len(b))) - except: - assert_equal(a, b, msg) - - -def assert_greater(a, b, msg=None): - if a > b: - success() - else: - failure(msg or "%r unexpectedly not greater than %r" % (a, b)) - - -def assert_greater_equal(a, b, msg=None): - if a >= b: - success() - else: - failure( - msg or "%r unexpectedly not greater than or equal to %r" % (a, b)) - - -def assert_less(a, b, msg=None): - if a < b: - success() - else: - failure(msg or "%r unexpectedly not less than %r" % (a, b)) - - -def assert_less_equal(a, b, msg=None): - if a <= b: - success() - else: - failure( - msg or "%r unexpectedly not less than or equal to %r" % (a, b)) - - -def assert_is_instance(a, b, msg=None): - if isinstance(a, b): - success() - else: - failure(msg or "got %r, expected %r" % (type(a), b)) - - -def assert_in(a, b, msg=None): - if a in b: - success() - else: - failure(msg or "%r unexpectedly not in %r" % (a, b)) - - -def assert_match(v, pattern, msg=None): - import re - if re.match(pattern, v): - success() - else: - failure(msg or "got %r, doesn't match pattern %r" % (v, pattern)) - - -def assert_exception(exc_class, func): - import sys - import traceback - try: - func() - except exc_class: - success() - except: - failure("expected %r exception, got %r" % ( - exc_class.__name__, sys.exc_info()[0].__name__)) - traceback.print_exc() - else: - failure("expected %r exception, got no exception" % exc_class.__name__) - - -def assert_no_exception(func): - import sys - import traceback - try: - func() - except: - failure("expected no exception, got %r" % sys.exc_info()[0].__name__) - traceback.print_exc() - else: - success() - - -def assert_warning(warn_class, func): - # note: this assert calls func three times! - import warnings - - def warn_error(message, category=UserWarning, **options): - raise category(message) - - def warn_ignore(message, category=UserWarning, **options): - pass - warn = warnings.warn - result = None - try: - warnings.warn = warn_ignore - assert_no_exception(func) - result = func() - warnings.warn = warn_error - assert_exception(warn_class, func) - finally: - warnings.warn = warn # restore - return result - -# helpers - -from io import BytesIO - - -def fromstring(data): - from PIL import Image - return Image.open(BytesIO(data)) - - -def tostring(im, format, **options): - out = BytesIO() - im.save(out, format, **options) - return out.getvalue() - - -def lena(mode="RGB", cache={}): - from PIL import Image - im = cache.get(mode) - if im is None: - if mode == "RGB": - im = Image.open("Images/lena.ppm") - elif mode == "F": - im = lena("L").convert(mode) - elif mode[:4] == "I;16": - im = lena("I").convert(mode) - else: - im = lena("RGB").convert(mode) - cache[mode] = im - return im - - -def assert_image(im, mode, size, msg=None): - if mode is not None and im.mode != mode: - failure(msg or "got mode %r, expected %r" % (im.mode, mode)) - elif size is not None and im.size != size: - failure(msg or "got size %r, expected %r" % (im.size, size)) - else: - success() - - -def assert_image_equal(a, b, msg=None): - if a.mode != b.mode: - failure(msg or "got mode %r, expected %r" % (a.mode, b.mode)) - elif a.size != b.size: - failure(msg or "got size %r, expected %r" % (a.size, b.size)) - elif a.tobytes() != b.tobytes(): - failure(msg or "got different content") - else: - success() - - -def assert_image_completely_equal(a, b, msg=None): - if a != b: - failure(msg or "images different") - else: - success() - - -def assert_image_similar(a, b, epsilon, msg=None): - epsilon = float(epsilon) - if a.mode != b.mode: - return failure(msg or "got mode %r, expected %r" % (a.mode, b.mode)) - elif a.size != b.size: - return failure(msg or "got size %r, expected %r" % (a.size, b.size)) - diff = 0 - try: - ord(b'0') - for abyte, bbyte in zip(a.tobytes(), b.tobytes()): - diff += abs(ord(abyte)-ord(bbyte)) - except: - for abyte, bbyte in zip(a.tobytes(), b.tobytes()): - diff += abs(abyte-bbyte) - ave_diff = float(diff)/(a.size[0]*a.size[1]) - if epsilon < ave_diff: - return failure( - msg or "average pixel value difference %.4f > epsilon %.4f" % ( - ave_diff, epsilon)) - else: - return success() - - -def tempfile(template, *extra): - import os - import os.path - import sys - import tempfile - files = [] - root = os.path.join(tempfile.gettempdir(), 'pillow-tests') - try: - os.mkdir(root) - except OSError: - pass - for temp in (template,) + extra: - assert temp[:5] in ("temp.", "temp_") - name = os.path.basename(sys.argv[0]) - name = temp[:4] + os.path.splitext(name)[0][4:] - name = name + "_%d" % len(_tempfiles) + temp[4:] - name = os.path.join(root, name) - files.append(name) - _tempfiles.extend(files) - return files[0] - - -# test runner - -def run(): - global _target, _tests, run - import sys - import traceback - _target = sys.modules["__main__"] - run = None # no need to run twice - tests = [] - for name, value in list(vars(_target).items()): - if name[:5] == "test_" and type(value) is type(success): - tests.append((value.__code__.co_firstlineno, name, value)) - tests.sort() # sort by line - for lineno, name, func in tests: - try: - _tests = [] - func() - for func, args in _tests: - func(*args) - except: - t, v, tb = sys.exc_info() - tb = tb.tb_next - if tb: - failure(frame=tb.tb_frame) - traceback.print_exception(t, v, tb) - else: - print("%s:%d: cannot call test function: %s" % ( - sys.argv[0], lineno, v)) - failure.count += 1 - - -def yield_test(function, *args): - # collect delayed/generated tests - _tests.append((function, args)) - - -def skip(msg=None): - import os - print("skip") - os._exit(0) # don't run exit handlers - - -def ignore(pattern): - """Tells the driver to ignore messages matching the pattern, for the - duration of the current test.""" - print('ignore: %s' % pattern) - - -def _setup(): - global _logfile - - import sys - if "--coverage" in sys.argv: - # Temporary: ignore PendingDeprecationWarning from Coverage (Py3.4) - with warnings.catch_warnings(): - warnings.simplefilter("ignore") - import coverage - cov = coverage.coverage(auto_data=True, include="PIL/*") - cov.start() - - def report(): - if run: - run() - if success.count and not failure.count: - print("ok") - # only clean out tempfiles if test passed - import os - import os.path - import tempfile - for file in _tempfiles: - try: - os.remove(file) - except OSError: - pass # report? - temp_root = os.path.join(tempfile.gettempdir(), 'pillow-tests') - try: - os.rmdir(temp_root) - except OSError: - pass - - import atexit - atexit.register(report) - - if "--log" in sys.argv: - _logfile = open("test.log", "a") - - -_setup() diff --git a/Tests/threaded_save.py b/Tests/threaded_save.py index 471e24815..12fcff2cf 100644 --- a/Tests/threaded_save.py +++ b/Tests/threaded_save.py @@ -9,7 +9,7 @@ try: except: format = "PNG" -im = Image.open("Images/lena.ppm") +im = Image.open("Tests/images/lena.ppm") im.load() queue = queue.Queue() diff --git a/Tk/README.rst b/Tk/README.rst new file mode 100644 index 000000000..bb5eb9c3c --- /dev/null +++ b/Tk/README.rst @@ -0,0 +1,292 @@ +Using PIL With Tkinter +==================================================================== + +Starting with 1.0 final (release candidate 2 and later, to be +precise), PIL can attach itself to Tkinter in flight. As a result, +you no longer need to rebuild the Tkinter extension to be able to +use PIL. + +However, if you cannot get the this to work on your platform, you +can do it in the old way: + +Adding Tkinter support +---------------------- + +1. Compile Python's _tkinter.c with the WITH_APPINIT and WITH_PIL + flags set, and link it with tkImaging.c and tkappinit.c. To + do this, copy the former to the Modules directory, and edit + the _tkinter line in Setup (or Setup.in) according to the + instructions in that file. + + NOTE: if you have an old Python version, the tkappinit.c + file is not included by default. If this is the case, you + will have to add the following lines to tkappinit.c, after + the MOREBUTTONS stuff:: + + { + extern void TkImaging_Init(Tcl_Interp* interp); + TkImaging_Init(interp); + } + + This registers a Tcl command called "PyImagingPhoto", which is + use to communicate between PIL and Tk's PhotoImage handler. + + You must also change the _tkinter line in Setup (or Setup.in) + to something like:: + + _tkinter _tkinter.c tkImaging.c tkappinit.c -DWITH_APPINIT + -I/usr/local/include -L/usr/local/lib -ltk8.0 -ltcl8.0 -lX11 + +The Photoimage Booster Patch (for Windows 95/NT) +==================================================================== + +This patch kit boosts performance for 16/24-bit displays. The +first patch is required on Tk 4.2 (where it fixes the problems for +16-bit displays) and later versions, with the exception for Tk 8.0b1 +where Sun added something similar themselves, only to remove it in +8.0b2. By installing both patches, Tk's PhotoImage handling becomes +much faster on both 16-bit and 24-bit displays. The patch has been +tested with Tk 4.2 and 8.0. + +Here's a benchmark, made with a sample program which loads two +512x512 greyscale PGM's, and two 512x512 colour PPM's, and displays +each of them in a separate toplevel windows. Tcl/Tk was compiled +with Visual C 4.0, and run on a P100 under Win95. Image load times +are not included in the timings: + ++----------------------+------------+-------------+----------------+ +| | **8-bit** | **16-bit** | **24-bit** | ++----------------------+------------+-------------+----------------+ +| 1. original 4.2 code | 5.52 s | 8.57 s | 3.79 s | ++----------------------+------------+-------------+----------------+ +| 2. booster patch | 5.49 s | 1.87 s | 1.82 s | ++----------------------+------------+-------------+----------------+ +| speedup | None | 4.6x | 2.1x | ++----------------------+------------+-------------+----------------+ + +Here's the patches: + +1. For portability and speed, the best thing under Windows is to +treat 16-bit displays as if they were 24-bit. The Windows device +drivers take care of the rest. + +.. Note:: + + If you have Tk 4.1 or Tk 8.0b1, you don't have to apply this + patch! It only applies to Tk 4.2, Tk 8.0a[12] and Tk 8.0b2. + +In ``win/tkWinImage.c``, change the following line in ``XCreateImage``:: + + imagePtr->bits_per_pixel = depth; + +to:: + + /* ==================================================================== */ + /* The tk photo image booster patch -- patch section 1 */ + /* ==================================================================== */ + + if (visual->class == TrueColor) + /* true colour is stored as 3 bytes: (blue, green, red) */ + imagePtr->bits_per_pixel = 24; + else + imagePtr->bits_per_pixel = depth; + + /* ==================================================================== */ + + +2. The DitherInstance implementation is not good. It's especially +bad on highend truecolour displays. IMO, it should be rewritten from +scratch (some other day...). + +Anyway, the following band-aid makes the situation a little bit +better under Windows. This hack trades some marginal quality (no +dithering on 16-bit displays) for a dramatic performance boost. +Requires patch 1, unless you're using Tk 4.1 or Tk 8.0b1. + +In generic/tkImgPhoto.c, add the #ifdef section to the DitherInstance +function:: + + /* ==================================================================== */ + + for (; height > 0; height -= nLines) { + if (nLines > height) { + nLines = height; + } + dstLinePtr = (unsigned char *) imagePtr->data; + yEnd = yStart + nLines; + + /* ==================================================================== */ + /* The tk photo image booster patch -- patch section 2 */ + /* ==================================================================== */ + + #ifdef __WIN32__ + if (colorPtr->visualInfo.class == TrueColor + && instancePtr->gamma == 1.0) { + /* Windows hicolor/truecolor booster */ + for (y = yStart; y < yEnd; ++y) { + destBytePtr = dstLinePtr; + srcPtr = srcLinePtr; + for (x = xStart; x < xEnd; ++x) { + destBytePtr[0] = srcPtr[2]; + destBytePtr[1] = srcPtr[1]; + destBytePtr[2] = srcPtr[0]; + destBytePtr += 3; srcPtr += 3; + } + srcLinePtr += lineLength; + dstLinePtr += bytesPerLine; + } + } else + #endif + + /* ==================================================================== */ + + for (y = yStart; y < yEnd; ++y) { + srcPtr = srcLinePtr; + errPtr = errLinePtr; + destBytePtr = dstLinePtr; + +last updated: 97-07-03/fl + + +The PIL Bitmap Booster Patch +==================================================================== + +The pilbitmap booster patch greatly improves performance of the +ImageTk.BitmapImage constructor. Unfortunately, the design of Tk +doesn't allow us to do this from the tkImaging interface module, so +you have to patch the Tk sources. + +Once installed, the ImageTk module will automatically detect this +patch. + +(Note: this patch has been tested with Tk 8.0 on Win32 only, but it +should work just fine on other platforms as well). + +1. To the beginning of TkGetBitmapData (in generic/tkImgBmap.c), add + the following stuff:: + + /* ==================================================================== */ + + int width, height, numBytes, hotX, hotY; + char *p, *end, *expandedFileName; + ParseInfo pi; + char *data = NULL; + Tcl_DString buffer; + + /* ==================================================================== */ + /* The pilbitmap booster patch -- patch section */ + /* ==================================================================== */ + + char *PILGetBitmapData(); + + if (string) { + /* Is this a PIL bitmap reference? */ + data = PILGetBitmapData(string, widthPtr, heightPtr, hotXPtr, hotYPtr); + if (data) + return data; + } + + /* ==================================================================== */ + + pi.string = string; + if (string == NULL) { + if (Tcl_IsSafe(interp)) { + +2. Append the following to the same file (you may wish to include +Imaging.h instead of copying the struct declaration...):: + + /* ==================================================================== */ + /* The pilbitmap booster patch -- code section */ + /* ==================================================================== */ + + /* Imaging declaration boldly copied from Imaging.h (!) */ + + typedef struct ImagingInstance *Imaging; /* a.k.a. ImagingImage :-) */ + + typedef unsigned char UINT8; + typedef int INT32; + + struct ImagingInstance { + + /* Format */ + char mode[4+1]; /* Band names ("1", "L", "P", "RGB", "RGBA", "CMYK") */ + int type; /* Always 0 in this version */ + int depth; /* Always 8 in this version */ + int bands; /* Number of bands (1, 3, or 4) */ + int xsize; /* Image dimension. */ + int ysize; + + /* Colour palette (for "P" images only) */ + void* palette; + + /* Data pointers */ + UINT8 **image8; /* Set for 8-bit image (pixelsize=1). */ + INT32 **image32; /* Set for 32-bit image (pixelsize=4). */ + + /* Internals */ + char **image; /* Actual raster data. */ + char *block; /* Set if data is allocated in a single block. */ + + int pixelsize; /* Size of a pixel, in bytes (1 or 4) */ + int linesize; /* Size of a line, in bytes (xsize * pixelsize) */ + + /* Virtual methods */ + void (*im_delete)(Imaging *); + + }; + + /* The pilbitmap booster patch allows you to pass PIL images to the + Tk bitmap decoder. Passing images this way is much more efficient + than using the "tobitmap" method. */ + + char * + PILGetBitmapData(string, widthPtr, heightPtr, hotXPtr, hotYPtr) + char *string; + int *widthPtr, *heightPtr; + int *hotXPtr, *hotYPtr; + { + char* data; + char* p; + int y; + Imaging im; + + if (strncmp(string, "PIL:", 4) != 0) + return NULL; + + im = (Imaging) atol(string + 4); + + if (strcmp(im->mode, "1") != 0 && strcmp(im->mode, "L") != 0) + return NULL; + + data = p = (char *) ckalloc((unsigned) ((im->xsize+7)/8) * im->ysize); + + for (y = 0; y < im->ysize; y++) { + char* in = im->image8[y]; + int i, m, b; + b = 0; m = 1; + for (i = 0; i < im->xsize; i++) { + if (in[i] != 0) + b |= m; + m <<= 1; + if (m == 256){ + *p++ = b; + b = 0; m = 1; + } + } + if (m != 1) + *p++ = b; + } + + *widthPtr = im->xsize; + *heightPtr = im->ysize; + *hotXPtr = -1; + *hotYPtr = -1; + + return data; + } + + /* ==================================================================== */ + +3. Recompile Tk and relink the _tkinter module (where necessary). + +Last updated: 97-05-17/fl diff --git a/Tk/booster.txt b/Tk/booster.txt deleted file mode 100644 index 2d787983b..000000000 --- a/Tk/booster.txt +++ /dev/null @@ -1,108 +0,0 @@ -==================================================================== -The Photoimage Booster Patch (for Windows 95/NT) -==================================================================== - - This patch kit boosts performance for 16/24-bit displays. The -first patch is required on Tk 4.2 (where it fixes the problems for -16-bit displays) and later versions, with the exception for Tk 8.0b1 -where Sun added something similar themselves, only to remove it in -8.0b2. By installing both patches, Tk's PhotoImage handling becomes -much faster on both 16-bit and 24-bit displays. The patch has been -tested with Tk 4.2 and 8.0. - - Here's a benchmark, made with a sample program which loads two -512x512 greyscale PGM's, and two 512x512 colour PPM's, and displays -each of them in a separate toplevel windows. Tcl/Tk was compiled -with Visual C 4.0, and run on a P100 under Win95. Image load times -are not included in the timings: - - 8-bit 16-bit 24-bit --------------------------------------------------------------------- -1. original 4.2 code 5.52 s 8.57 s 3.79 s -2. booster patch 5.49 s 1.87 s 1.82 s - - speedup None 4.6x 2.1x - -==================================================================== - -Here's the patches: - -1. For portability and speed, the best thing under Windows is to -treat 16-bit displays as if they were 24-bit. The Windows device -drivers take care of the rest. - - ---------------------------------------------------------------- - If you have Tk 4.1 or Tk 8.0b1, you don't have to apply this - patch! It only applies to Tk 4.2, Tk 8.0a[12] and Tk 8.0b2. - ---------------------------------------------------------------- - -In win/tkWinImage.c, change the following line in XCreateImage: - - imagePtr->bits_per_pixel = depth; - -to - -/* ==================================================================== */ -/* The tk photo image booster patch -- patch section 1 */ -/* ==================================================================== */ - - if (visual->class == TrueColor) - /* true colour is stored as 3 bytes: (blue, green, red) */ - imagePtr->bits_per_pixel = 24; - else - imagePtr->bits_per_pixel = depth; - -/* ==================================================================== */ - - -2. The DitherInstance implementation is not good. It's especially -bad on highend truecolour displays. IMO, it should be rewritten from -scratch (some other day...). - - Anyway, the following band-aid makes the situation a little bit -better under Windows. This hack trades some marginal quality (no -dithering on 16-bit displays) for a dramatic performance boost. -Requires patch 1, unless you're using Tk 4.1 or Tk 8.0b1. - -In generic/tkImgPhoto.c, add the #ifdef section to the DitherInstance -function: - - for (; height > 0; height -= nLines) { - if (nLines > height) { - nLines = height; - } - dstLinePtr = (unsigned char *) imagePtr->data; - yEnd = yStart + nLines; - -/* ==================================================================== */ -/* The tk photo image booster patch -- patch section 2 */ -/* ==================================================================== */ - -#ifdef __WIN32__ - if (colorPtr->visualInfo.class == TrueColor - && instancePtr->gamma == 1.0) { - /* Windows hicolor/truecolor booster */ - for (y = yStart; y < yEnd; ++y) { - destBytePtr = dstLinePtr; - srcPtr = srcLinePtr; - for (x = xStart; x < xEnd; ++x) { - destBytePtr[0] = srcPtr[2]; - destBytePtr[1] = srcPtr[1]; - destBytePtr[2] = srcPtr[0]; - destBytePtr += 3; srcPtr += 3; - } - srcLinePtr += lineLength; - dstLinePtr += bytesPerLine; - } - } else -#endif - -/* ==================================================================== */ - - for (y = yStart; y < yEnd; ++y) { - srcPtr = srcLinePtr; - errPtr = errLinePtr; - destBytePtr = dstLinePtr; - -==================================================================== -last updated: 97-07-03/fl diff --git a/Tk/install.txt b/Tk/install.txt deleted file mode 100644 index 0e2ade06d..000000000 --- a/Tk/install.txt +++ /dev/null @@ -1,41 +0,0 @@ -==================================================================== -Using PIL With Tkinter -==================================================================== - -Starting with 1.0 final (release candidate 2 and later, to be -precise), PIL can attach itself to Tkinter in flight. As a result, -you no longer need to rebuild the Tkinter extension to be able to -use PIL. - -However, if you cannot get the this to work on your platform, you -can do it in the old way: - -* Adding Tkinter support - -1. Compile Python's _tkinter.c with the WITH_APPINIT and WITH_PIL - flags set, and link it with tkImaging.c and tkappinit.c. To - do this, copy the former to the Modules directory, and edit - the _tkinter line in Setup (or Setup.in) according to the - instructions in that file. - - NOTE: if you have an old Python version, the tkappinit.c - file is not included by default. If this is the case, you - will have to add the following lines to tkappinit.c, after - the MOREBUTTONS stuff: - - { - extern void TkImaging_Init(Tcl_Interp* interp); - TkImaging_Init(interp); - } - - This registers a Tcl command called "PyImagingPhoto", which is - use to communicate between PIL and Tk's PhotoImage handler. - - You must also change the _tkinter line in Setup (or Setup.in) - to something like: - - _tkinter _tkinter.c tkImaging.c tkappinit.c -DWITH_APPINIT - -I/usr/local/include -L/usr/local/lib -ltk8.0 -ltcl8.0 -lX11 - - - diff --git a/Tk/pilbitmap.txt b/Tk/pilbitmap.txt deleted file mode 100644 index e7c46c65f..000000000 --- a/Tk/pilbitmap.txt +++ /dev/null @@ -1,149 +0,0 @@ -==================================================================== -The PIL Bitmap Booster Patch -==================================================================== - -The pilbitmap booster patch greatly improves performance of the -ImageTk.BitmapImage constructor. Unfortunately, the design of Tk -doesn't allow us to do this from the tkImaging interface module, so -you have to patch the Tk sources. - -Once installed, the ImageTk module will automatically detect this -patch. - -(Note: this patch has been tested with Tk 8.0 on Win32 only, but it -should work just fine on other platforms as well). - -1. To the beginning of TkGetBitmapData (in generic/tkImgBmap.c), add - the following stuff: - ------------------------------------------------------------------------- - int width, height, numBytes, hotX, hotY; - char *p, *end, *expandedFileName; - ParseInfo pi; - char *data = NULL; - Tcl_DString buffer; - -/* ==================================================================== */ -/* The pilbitmap booster patch -- patch section */ -/* ==================================================================== */ - - char *PILGetBitmapData(); - - if (string) { - /* Is this a PIL bitmap reference? */ - data = PILGetBitmapData(string, widthPtr, heightPtr, hotXPtr, hotYPtr); - if (data) - return data; - } - -/* ==================================================================== */ - - pi.string = string; - if (string == NULL) { - if (Tcl_IsSafe(interp)) { ------------------------------------------------------------------------- - - -2. Append the following to the same file (you may wish to include -Imaging.h instead of copying the struct declaration...) - ------------------------------------------------------------------------- - -/* ==================================================================== */ -/* The pilbitmap booster patch -- code section */ -/* ==================================================================== */ - -/* Imaging declaration boldly copied from Imaging.h (!) */ - -typedef struct ImagingInstance *Imaging; /* a.k.a. ImagingImage :-) */ - -typedef unsigned char UINT8; -typedef int INT32; - -struct ImagingInstance { - - /* Format */ - char mode[4+1]; /* Band names ("1", "L", "P", "RGB", "RGBA", "CMYK") */ - int type; /* Always 0 in this version */ - int depth; /* Always 8 in this version */ - int bands; /* Number of bands (1, 3, or 4) */ - int xsize; /* Image dimension. */ - int ysize; - - /* Colour palette (for "P" images only) */ - void* palette; - - /* Data pointers */ - UINT8 **image8; /* Set for 8-bit image (pixelsize=1). */ - INT32 **image32; /* Set for 32-bit image (pixelsize=4). */ - - /* Internals */ - char **image; /* Actual raster data. */ - char *block; /* Set if data is allocated in a single block. */ - - int pixelsize; /* Size of a pixel, in bytes (1 or 4) */ - int linesize; /* Size of a line, in bytes (xsize * pixelsize) */ - - /* Virtual methods */ - void (*im_delete)(Imaging *); - -}; - -/* The pilbitmap booster patch allows you to pass PIL images to the - Tk bitmap decoder. Passing images this way is much more efficient - than using the "tobitmap" method. */ - -char * -PILGetBitmapData(string, widthPtr, heightPtr, hotXPtr, hotYPtr) - char *string; - int *widthPtr, *heightPtr; - int *hotXPtr, *hotYPtr; -{ - char* data; - char* p; - int y; - Imaging im; - - if (strncmp(string, "PIL:", 4) != 0) - return NULL; - - im = (Imaging) atol(string + 4); - - if (strcmp(im->mode, "1") != 0 && strcmp(im->mode, "L") != 0) - return NULL; - - data = p = (char *) ckalloc((unsigned) ((im->xsize+7)/8) * im->ysize); - - for (y = 0; y < im->ysize; y++) { - char* in = im->image8[y]; - int i, m, b; - b = 0; m = 1; - for (i = 0; i < im->xsize; i++) { - if (in[i] != 0) - b |= m; - m <<= 1; - if (m == 256){ - *p++ = b; - b = 0; m = 1; - } - } - if (m != 1) - *p++ = b; - } - - *widthPtr = im->xsize; - *heightPtr = im->ysize; - *hotXPtr = -1; - *hotYPtr = -1; - - return data; -} - -/* ==================================================================== */ - ------------------------------------------------------------------------- - -3. Recompile Tk and relink the _tkinter module (where necessary). - -==================================================================== -Last updated: 97-05-17/fl diff --git a/_imaging.c b/_imaging.c index 4f14b7cbd..ec8205dd4 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.4.0" +#define PILLOW_VERSION "2.5.3" #include "Python.h" @@ -365,9 +365,12 @@ getbands(const char* mode) static void* getlist(PyObject* arg, int* length, const char* wrong_length, int type) { - int i, n; + int i, n, itemp; + double dtemp; void* list; - + PyObject* seq; + PyObject* op; + if (!PySequence_Check(arg)) { PyErr_SetString(PyExc_TypeError, must_be_sequence); return NULL; @@ -383,71 +386,35 @@ getlist(PyObject* arg, int* length, const char* wrong_length, int type) if (!list) return PyErr_NoMemory(); - switch (type) { - case TYPE_UINT8: - if (PyList_Check(arg)) { - for (i = 0; i < n; i++) { - PyObject *op = PyList_GET_ITEM(arg, i); - int temp = PyInt_AsLong(op); - ((UINT8*)list)[i] = CLIP(temp); - } - } else { - for (i = 0; i < n; i++) { - PyObject *op = PySequence_GetItem(arg, i); - int temp = PyInt_AsLong(op); - Py_XDECREF(op); - ((UINT8*)list)[i] = CLIP(temp); - } + seq = PySequence_Fast(arg, must_be_sequence); + if (!seq) { + free(list); + PyErr_SetString(PyExc_TypeError, must_be_sequence); + return NULL; + } + + for (i = 0; i < n; i++) { + op = PySequence_Fast_GET_ITEM(seq, i); + // DRY, branch prediction is going to work _really_ well + // on this switch. And 3 fewer loops to copy/paste. + switch (type) { + case TYPE_UINT8: + itemp = PyInt_AsLong(op); + ((UINT8*)list)[i] = CLIP(itemp); + break; + case TYPE_INT32: + itemp = PyInt_AsLong(op); + ((INT32*)list)[i] = itemp; + break; + case TYPE_FLOAT32: + dtemp = PyFloat_AsDouble(op); + ((FLOAT32*)list)[i] = (FLOAT32) dtemp; + break; + case TYPE_DOUBLE: + dtemp = PyFloat_AsDouble(op); + ((double*)list)[i] = (double) dtemp; + break; } - break; - case TYPE_INT32: - if (PyList_Check(arg)) { - for (i = 0; i < n; i++) { - PyObject *op = PyList_GET_ITEM(arg, i); - int temp = PyInt_AsLong(op); - ((INT32*)list)[i] = temp; - } - } else { - for (i = 0; i < n; i++) { - PyObject *op = PySequence_GetItem(arg, i); - int temp = PyInt_AsLong(op); - Py_XDECREF(op); - ((INT32*)list)[i] = temp; - } - } - break; - case TYPE_FLOAT32: - if (PyList_Check(arg)) { - for (i = 0; i < n; i++) { - PyObject *op = PyList_GET_ITEM(arg, i); - double temp = PyFloat_AsDouble(op); - ((FLOAT32*)list)[i] = (FLOAT32) temp; - } - } else { - for (i = 0; i < n; i++) { - PyObject *op = PySequence_GetItem(arg, i); - double temp = PyFloat_AsDouble(op); - Py_XDECREF(op); - ((FLOAT32*)list)[i] = (FLOAT32) temp; - } - } - break; - case TYPE_DOUBLE: - if (PyList_Check(arg)) { - for (i = 0; i < n; i++) { - PyObject *op = PyList_GET_ITEM(arg, i); - double temp = PyFloat_AsDouble(op); - ((double*)list)[i] = temp; - } - } else { - for (i = 0; i < n; i++) { - PyObject *op = PySequence_GetItem(arg, i); - double temp = PyFloat_AsDouble(op); - Py_XDECREF(op); - ((double*)list)[i] = temp; - } - } - break; } if (length) @@ -1253,6 +1220,8 @@ _putdata(ImagingObject* self, PyObject* args) Py_ssize_t n, i, x, y; PyObject* data; + PyObject* seq; + PyObject* op; double scale = 1.0; double offset = 0.0; @@ -1292,69 +1261,61 @@ _putdata(ImagingObject* self, PyObject* args) x = 0, y++; } } else { - if (scale == 1.0 && offset == 0.0) { - /* Clipped data */ - if (PyList_Check(data)) { - for (i = x = y = 0; i < n; i++) { - PyObject *op = PyList_GET_ITEM(data, i); - image->image8[y][x] = (UINT8) CLIP(PyInt_AsLong(op)); - if (++x >= (int) image->xsize) - x = 0, y++; - } - } else { - for (i = x = y = 0; i < n; i++) { - PyObject *op = PySequence_GetItem(data, i); - image->image8[y][x] = (UINT8) CLIP(PyInt_AsLong(op)); - Py_XDECREF(op); - if (++x >= (int) image->xsize) - x = 0, y++; - } - } + seq = PySequence_Fast(data, must_be_sequence); + if (!seq) { + PyErr_SetString(PyExc_TypeError, must_be_sequence); + return NULL; + } + if (scale == 1.0 && offset == 0.0) { + /* Clipped data */ + for (i = x = y = 0; i < n; i++) { + op = PySequence_Fast_GET_ITEM(data, i); + image->image8[y][x] = (UINT8) CLIP(PyInt_AsLong(op)); + if (++x >= (int) image->xsize){ + x = 0, y++; + } + } + } else { - if (PyList_Check(data)) { - /* Scaled and clipped data */ - for (i = x = y = 0; i < n; i++) { - PyObject *op = PyList_GET_ITEM(data, i); - image->image8[y][x] = CLIP( - (int) (PyFloat_AsDouble(op) * scale + offset)); - if (++x >= (int) image->xsize) - x = 0, y++; - } - } else { - for (i = x = y = 0; i < n; i++) { - PyObject *op = PySequence_GetItem(data, i); - image->image8[y][x] = CLIP( - (int) (PyFloat_AsDouble(op) * scale + offset)); - Py_XDECREF(op); - if (++x >= (int) image->xsize) - x = 0, y++; - } - } - } - PyErr_Clear(); /* Avoid weird exceptions */ + /* Scaled and clipped data */ + for (i = x = y = 0; i < n; i++) { + PyObject *op = PySequence_Fast_GET_ITEM(data, i); + image->image8[y][x] = CLIP( + (int) (PyFloat_AsDouble(op) * scale + offset)); + if (++x >= (int) image->xsize){ + x = 0, y++; + } + } + } + PyErr_Clear(); /* Avoid weird exceptions */ } } else { /* 32-bit images */ + seq = PySequence_Fast(data, must_be_sequence); + if (!seq) { + PyErr_SetString(PyExc_TypeError, must_be_sequence); + return NULL; + } switch (image->type) { case IMAGING_TYPE_INT32: for (i = x = y = 0; i < n; i++) { - PyObject *op = PySequence_GetItem(data, i); + op = PySequence_Fast_GET_ITEM(data, i); IMAGING_PIXEL_INT32(image, x, y) = (INT32) (PyFloat_AsDouble(op) * scale + offset); - Py_XDECREF(op); - if (++x >= (int) image->xsize) + if (++x >= (int) image->xsize){ x = 0, y++; + } } PyErr_Clear(); /* Avoid weird exceptions */ break; case IMAGING_TYPE_FLOAT32: for (i = x = y = 0; i < n; i++) { - PyObject *op = PySequence_GetItem(data, i); + op = PySequence_Fast_GET_ITEM(data, i); IMAGING_PIXEL_FLOAT32(image, x, y) = (FLOAT32) (PyFloat_AsDouble(op) * scale + offset); - Py_XDECREF(op); - if (++x >= (int) image->xsize) + if (++x >= (int) image->xsize){ x = 0, y++; + } } PyErr_Clear(); /* Avoid weird exceptions */ break; @@ -1365,16 +1326,15 @@ _putdata(ImagingObject* self, PyObject* args) INT32 inkint; } u; - PyObject *op = PySequence_GetItem(data, i); + op = PySequence_Fast_GET_ITEM(data, i); if (!op || !getink(op, image, u.ink)) { - Py_DECREF(op); return NULL; } /* FIXME: what about scale and offset? */ image->image32[y][x] = u.inkint; - Py_XDECREF(op); - if (++x >= (int) image->xsize) + if (++x >= (int) image->xsize){ x = 0, y++; + } } PyErr_Clear(); /* Avoid weird exceptions */ break; @@ -2252,17 +2212,17 @@ void _font_text_asBytes(PyObject* encoded_string, unsigned char** text){ if (bytes) { *text = (unsigned char*)PyBytes_AsString(bytes); return; - } + } #if PY_VERSION_HEX < 0x03000000 /* likely case here is py2.x with an ordinary string. but this isn't defined in Py3.x */ if (PyString_Check(encoded_string)) { *text = (unsigned char *)PyString_AsString(encoded_string); - } + } #endif } - + static PyObject* _font_getmask(ImagingFontObject* self, PyObject* args) @@ -2336,7 +2296,7 @@ _font_getsize(ImagingFontObject* self, PyObject* args) return NULL; } - return Py_BuildValue("ii", textwidth(self, text), self->ysize); + return Py_BuildValue("ii", textwidth(self, text), self->ysize); } static struct PyMethodDef _font_methods[] = { @@ -2399,17 +2359,35 @@ _draw_ink(ImagingDrawObject* self, PyObject* args) static PyObject* _draw_arc(ImagingDrawObject* self, PyObject* args) { - int x0, y0, x1, y1; + double* xy; + int n; + + PyObject* data; int ink; int start, end; int op = 0; - if (!PyArg_ParseTuple(args, "(iiii)iii|i", - &x0, &y0, &x1, &y1, - &start, &end, &ink)) + if (!PyArg_ParseTuple(args, "Oiii|i", &data, &start, &end, &ink)) return NULL; - if (ImagingDrawArc(self->image->image, x0, y0, x1, y1, start, end, - &ink, op) < 0) + n = PyPath_Flatten(data, &xy); + if (n < 0) + return NULL; + if (n != 2) { + PyErr_SetString(PyExc_TypeError, + "coordinate list must contain exactly 2 coordinates" + ); + return NULL; + } + + n = ImagingDrawArc(self->image->image, + (int) xy[0], (int) xy[1], + (int) xy[2], (int) xy[3], + start, end, &ink, op + ); + + free(xy); + + if (n < 0) return NULL; Py_INCREF(Py_None); @@ -2455,15 +2433,35 @@ _draw_bitmap(ImagingDrawObject* self, PyObject* args) static PyObject* _draw_chord(ImagingDrawObject* self, PyObject* args) { - int x0, y0, x1, y1; + double* xy; + int n; + + PyObject* data; int ink, fill; int start, end; - if (!PyArg_ParseTuple(args, "(iiii)iiii", - &x0, &y0, &x1, &y1, &start, &end, &ink, &fill)) + if (!PyArg_ParseTuple(args, "Oiiii", + &data, &start, &end, &ink, &fill)) return NULL; - if (ImagingDrawChord(self->image->image, x0, y0, x1, y1, - start, end, &ink, fill, self->blend) < 0) + n = PyPath_Flatten(data, &xy); + if (n < 0) + return NULL; + if (n != 2) { + PyErr_SetString(PyExc_TypeError, + "coordinate list must contain exactly 2 coordinates" + ); + return NULL; + } + + n = ImagingDrawChord(self->image->image, + (int) xy[0], (int) xy[1], + (int) xy[2], (int) xy[3], + start, end, &ink, fill, self->blend + ); + + free(xy); + + if (n < 0) return NULL; Py_INCREF(Py_None); @@ -2492,8 +2490,8 @@ _draw_ellipse(ImagingDrawObject* self, PyObject* args) return NULL; } - n = ImagingDrawEllipse(self->image->image, - (int) xy[0], (int) xy[1], + n = ImagingDrawEllipse(self->image->image, + (int) xy[0], (int) xy[1], (int) xy[2], (int) xy[3], &ink, fill, self->blend ); @@ -2656,15 +2654,34 @@ _draw_outline(ImagingDrawObject* self, PyObject* args) static PyObject* _draw_pieslice(ImagingDrawObject* self, PyObject* args) { - int x0, y0, x1, y1; + double* xy; + int n; + + PyObject* data; int ink, fill; int start, end; - if (!PyArg_ParseTuple(args, "(iiii)iiii", - &x0, &y0, &x1, &y1, &start, &end, &ink, &fill)) + if (!PyArg_ParseTuple(args, "Oiiii", &data, &start, &end, &ink, &fill)) return NULL; - if (ImagingDrawPieslice(self->image->image, x0, y0, x1, y1, - start, end, &ink, fill, self->blend) < 0) + n = PyPath_Flatten(data, &xy); + if (n < 0) + return NULL; + if (n != 2) { + PyErr_SetString(PyExc_TypeError, + "coordinate list must contain exactly 2 coordinates" + ); + return NULL; + } + + n = ImagingDrawPieslice(self->image->image, + (int) xy[0], (int) xy[1], + (int) xy[2], (int) xy[3], + start, end, &ink, fill, self->blend + ); + + free(xy); + + if (n < 0) return NULL; Py_INCREF(Py_None); @@ -2738,9 +2755,9 @@ _draw_rectangle(ImagingDrawObject* self, PyObject* args) return NULL; } - n = ImagingDrawRectangle(self->image->image, + n = ImagingDrawRectangle(self->image->image, (int) xy[0], (int) xy[1], - (int) xy[2], (int) xy[3], + (int) xy[2], (int) xy[3], &ink, fill, self->blend ); @@ -3117,8 +3134,8 @@ _getattr_ptr(ImagingObject* self, void* closure) static PyObject* _getattr_unsafe_ptrs(ImagingObject* self, void* closure) { - return Py_BuildValue("(ss)(si)(si)(si)(si)(si)(sn)(sn)(sn)(sn)(sn)(si)(si)(sn)", - "mode", self->image->mode, + return Py_BuildValue("(ss)(si)(si)(si)(si)(si)(sn)(sn)(sn)(sn)(sn)(si)(si)(sn)", + "mode", self->image->mode, "type", self->image->type, "depth", self->image->depth, "bands", self->image->bands, diff --git a/_imagingcms.c b/_imagingcms.c index df26e1a2d..3b822006a 100644 --- a/_imagingcms.c +++ b/_imagingcms.c @@ -6,6 +6,8 @@ * http://www.cazabon.com * Adapted/reworked for PIL by Fredrik Lundh * Copyright (c) 2009 Fredrik Lundh + * Updated to LCMS2 + * Copyright (c) 2013 Eric Soroos * * pyCMS home page: http://www.cazabon.com/pyCMS * littleCMS home page: http://www.littlecms.com @@ -47,7 +49,8 @@ http://www.cazabon.com\n\ /* known to-do list with current version: - Verify that PILmode->littleCMStype conversion in findLCMStype is correct for all PIL modes (it probably isn't for the more obscure ones) + Verify that PILmode->littleCMStype conversion in findLCMStype is correct for all + PIL modes (it probably isn't for the more obscure ones) Add support for creating custom RGB profiles on the fly Add support for checking presence of a specific tag in a profile @@ -132,6 +135,49 @@ cms_profile_fromstring(PyObject* self, PyObject* args) return cms_profile_new(hProfile); } +static PyObject* +cms_profile_tobytes(PyObject* self, PyObject* args) +{ + char *pProfile =NULL; + cmsUInt32Number nProfile; + PyObject* CmsProfile; + + cmsHPROFILE *profile; + + PyObject* ret; + if (!PyArg_ParseTuple(args, "O", &CmsProfile)){ + return NULL; + } + + profile = ((CmsProfileObject*)CmsProfile)->profile; + + if (!cmsSaveProfileToMem(profile, pProfile, &nProfile)) { + PyErr_SetString(PyExc_IOError, "Could not determine profile size"); + return NULL; + } + + pProfile = (char*)malloc(nProfile); + if (!pProfile) { + PyErr_SetString(PyExc_IOError, "Out of Memory"); + return NULL; + } + + if (!cmsSaveProfileToMem(profile, pProfile, &nProfile)) { + PyErr_SetString(PyExc_IOError, "Could not get profile"); + free(pProfile); + return NULL; + } + +#if PY_VERSION_HEX >= 0x03000000 + ret = PyBytes_FromStringAndSize(pProfile, (Py_ssize_t)nProfile); +#else + ret = PyString_FromStringAndSize(pProfile, (Py_ssize_t)nProfile); +#endif + + free(pProfile); + return ret; +} + static void cms_profile_dealloc(CmsProfileObject* self) { @@ -483,6 +529,7 @@ static PyMethodDef pyCMSdll_methods[] = { {"profile_open", cms_profile_open, 1}, {"profile_frombytes", cms_profile_fromstring, 1}, {"profile_fromstring", cms_profile_fromstring, 1}, + {"profile_tobytes", cms_profile_tobytes, 1}, /* profile and transform functions */ {"buildTransform", buildTransform, 1}, diff --git a/_imagingmorph.c b/_imagingmorph.c new file mode 100644 index 000000000..1dee7eb64 --- /dev/null +++ b/_imagingmorph.c @@ -0,0 +1,304 @@ +/* + * The Python Imaging Library + * + * A binary morphology add-on for the Python Imaging Library + * + * History: + * 2014-06-04 Initial version. + * + * Copyright (c) 2014 Dov Grobgeld + * + * See the README file for information on usage and redistribution. + */ + +#include "Python.h" +#include "Imaging.h" +#include "py3.h" + +#define LUT_SIZE (1<<9) + +/* Apply a morphologic LUT to a binary image. Outputs a + a new binary image. + + Expected parameters: + + 1. a LUT - a 512 byte size lookup table. + 2. an input Imaging image id. + 3. an output Imaging image id + + Returns number of changed pixels. +*/ +static PyObject* +apply(PyObject *self, PyObject* args) +{ + const char *lut; + PyObject *py_lut; + Py_ssize_t lut_len, i0, i1; + Imaging imgin, imgout; + int width, height; + int row_idx, col_idx; + UINT8 **inrows, **outrows; + int num_changed_pixels = 0; + + if (!PyArg_ParseTuple(args, "Onn", &py_lut, &i0, &i1)) { + PyErr_SetString(PyExc_RuntimeError, "Argument parsing problem"); + return NULL; + } + + if (!PyBytes_Check(py_lut)) { + PyErr_SetString(PyExc_RuntimeError, "The morphology LUT is not a bytes object"); + return NULL; + } + + lut_len = PyBytes_Size(py_lut); + + if (lut_len < LUT_SIZE) { + PyErr_SetString(PyExc_RuntimeError, "The morphology LUT has the wrong size"); + return NULL; + } + + lut = PyBytes_AsString(py_lut); + + imgin = (Imaging) i0; + imgout = (Imaging) i1; + width = imgin->xsize; + height = imgin->ysize; + + if (imgin->type != IMAGING_TYPE_UINT8 && + imgin->bands != 1) { + PyErr_SetString(PyExc_RuntimeError, "Unsupported image type"); + return NULL; + } + if (imgout->type != IMAGING_TYPE_UINT8 && + imgout->bands != 1) { + PyErr_SetString(PyExc_RuntimeError, "Unsupported image type"); + return NULL; + } + + inrows = imgin->image8; + outrows = imgout->image8; + + for (row_idx=0; row_idx < height; row_idx++) { + UINT8 *outrow = outrows[row_idx]; + UINT8 *inrow = inrows[row_idx]; + UINT8 *prow, *nrow; /* Previous and next row */ + + /* zero boundary conditions. TBD support other modes */ + outrow[0] = outrow[width-1] = 0; + if (row_idx==0 || row_idx == height-1) { + for(col_idx=0; col_idxtype != IMAGING_TYPE_UINT8 && + imgin->bands != 1) { + PyErr_SetString(PyExc_RuntimeError, "Unsupported image type"); + return NULL; + } + + inrows = imgin->image8; + width = imgin->xsize; + height = imgin->ysize; + + for (row_idx=1; row_idx < height-1; row_idx++) { + UINT8 *inrow = inrows[row_idx]; + UINT8 *prow, *nrow; + + prow = inrows[row_idx-1]; + nrow = inrows[row_idx+1]; + + for (col_idx=1; col_idximage8; + width = img->xsize; + height = img->ysize; + + for (row_idx=0; row_idx < height; row_idx++) { + UINT8 *row = rows[row_idx]; + for (col_idx=0; col_idx= 0x03000000 +PyMODINIT_FUNC +PyInit__imagingmorph(void) { + PyObject* m; + + static PyModuleDef module_def = { + PyModuleDef_HEAD_INIT, + "_imagingmorph", /* m_name */ + "A module for doing image morphology", /* m_doc */ + -1, /* m_size */ + functions, /* m_methods */ + }; + + m = PyModule_Create(&module_def); + + if (setup_module(m) < 0) + return NULL; + + return m; +} +#else +PyMODINIT_FUNC +init_imagingmorph(void) +{ + PyObject* m = Py_InitModule("_imagingmorph", functions); + setup_module(m); +} +#endif + diff --git a/decode.c b/decode.c index 33367dfe3..d5e329384 100644 --- a/decode.c +++ b/decode.c @@ -797,8 +797,9 @@ PyImaging_Jpeg2KDecoderNew(PyObject* self, PyObject* args) int reduce = 0; int layers = 0; int fd = -1; - if (!PyArg_ParseTuple(args, "ss|iii", &mode, &format, - &reduce, &layers, &fd)) + PY_LONG_LONG length = -1; + if (!PyArg_ParseTuple(args, "ss|iiiL", &mode, &format, + &reduce, &layers, &fd, &length)) return NULL; if (strcmp(format, "j2k") == 0) @@ -821,6 +822,7 @@ PyImaging_Jpeg2KDecoderNew(PyObject* self, PyObject* args) context = (JPEG2KDECODESTATE *)decoder->state.context; context->fd = fd; + context->length = (off_t)length; context->format = codec_format; context->reduce = reduce; context->layers = layers; diff --git a/depends/README.rst b/depends/README.rst new file mode 100644 index 000000000..62c101ecf --- /dev/null +++ b/depends/README.rst @@ -0,0 +1,4 @@ +Depends +======= + +Scripts in this directory can be used to download, build & install non-packaged dependencies; useful for testing with Travis CI. diff --git a/depends/install_openjpeg.sh b/depends/install_openjpeg.sh index bd6b83e3b..b3a6ccc4d 100755 --- a/depends/install_openjpeg.sh +++ b/depends/install_openjpeg.sh @@ -2,15 +2,16 @@ # install openjpeg -if [ ! -f openjpeg-2.0.0.tar.gz ]; then - wget 'https://openjpeg.googlecode.com/files/openjpeg-2.0.0.tar.gz' +if [ ! -f openjpeg-2.1.0.tar.gz ]; then + wget 'http://iweb.dl.sourceforge.net/project/openjpeg.mirror/2.1.0/openjpeg-2.1.0.tar.gz' + fi -rm -r openjpeg-2.0.0 -tar -xvzf openjpeg-2.0.0.tar.gz +rm -r openjpeg-2.1.0 +tar -xvzf openjpeg-2.1.0.tar.gz -pushd openjpeg-2.0.0 +pushd openjpeg-2.1.0 cmake -DCMAKE_INSTALL_PREFIX=/usr . && make && sudo make install diff --git a/docs/PIL.rst b/docs/PIL.rst index 6726f661f..8bf89c685 100644 --- a/docs/PIL.rst +++ b/docs/PIL.rst @@ -20,14 +20,6 @@ can be found here. :undoc-members: :show-inheritance: -:mod:`ExifTags` Module ----------------------- - -.. automodule:: PIL.ExifTags - :members: - :undoc-members: - :show-inheritance: - :mod:`FontFile` Module ---------------------- @@ -60,15 +52,8 @@ can be found here. :undoc-members: :show-inheritance: -:mod:`ImageCms` Module ----------------------- - -.. automodule:: PIL.ImageCms - :members: - :undoc-members: - :show-inheritance: - .. intentionally skipped documenting this because it's not documented anywhere + :mod:`ImageDraw2` Module ------------------------ @@ -78,6 +63,7 @@ can be found here. :show-inheritance: .. intentionally skipped documenting this because it's deprecated + :mod:`ImageFileIO` Module ------------------------- @@ -86,6 +72,7 @@ can be found here. :undoc-members: :show-inheritance: + :mod:`ImageShow` Module ----------------------- @@ -110,14 +97,6 @@ can be found here. :undoc-members: :show-inheritance: -:mod:`OleFileIO` Module ------------------------ - -.. automodule:: PIL.OleFileIO - :members: - :undoc-members: - :show-inheritance: - :mod:`PaletteFile` Module ------------------------- diff --git a/docs/_templates/sidebarhelp.html b/docs/_templates/sidebarhelp.html index 330b3de45..e07180a99 100644 --- a/docs/_templates/sidebarhelp.html +++ b/docs/_templates/sidebarhelp.html @@ -1,18 +1,4 @@

Need help?

-

- You can seek realtime assistance via IRC at - irc://irc.freenode.net#pil. You can - also post to the - - Image-SIG mailing list. And, of course, there's - - Stack Overflow. + You can get help via IRC at irc://irc.freenode.net#pil or Stack Overflow here and here. Please report issues on GitHub.

- -

- If you've discovered a bug, you can - open an issue - on Github. -

- diff --git a/docs/about.rst b/docs/about.rst index e8c9356dc..919b2918c 100644 --- a/docs/about.rst +++ b/docs/about.rst @@ -9,12 +9,10 @@ The fork authors' goal is to foster active development of PIL through: - Continuous integration testing via `Travis CI`_ - Publicized development activity on `GitHub`_ - Regular releases to the `Python Package Index`_ -- Solicitation for community contributions and involvement on `Image-SIG`_ -.. _Travis CI: https://travis-ci.org/python-imaging/Pillow -.. _GitHub: https://github.com/python-imaging/Pillow +.. _Travis CI: https://travis-ci.org/python-pillow/Pillow +.. _GitHub: https://github.com/python-pillow/Pillow .. _Python Package Index: https://pypi.python.org/pypi/Pillow -.. _Image-SIG: http://mail.python.org/mailman/listinfo/image-sig License ------- @@ -60,6 +58,6 @@ announcement. So if you still want to support PIL, please .. _report issues here first: https://bitbucket.org/effbot/pil-2009-raclette/issues -.. _open the corresponding Pillow tickets here: https://github.com/python-imaging/Pillow/issues +.. _open the corresponding Pillow tickets here: https://github.com/python-pillow/Pillow/issues Please provide a link to the PIL ticket so we can track the issue(s) upstream. diff --git a/docs/developer.rst b/docs/developer.rst new file mode 100644 index 000000000..ea3b0f05e --- /dev/null +++ b/docs/developer.rst @@ -0,0 +1,17 @@ +Developer +========= + +.. Note:: When committing only trivial changes, please include [ci skip] in the commit message to avoid running tests on Travis-CI. Thank you! + + +Release +------- + +Details about making a Pillow release. + +Version number +~~~~~~~~~~~~~~ + +The version number is currently stored in 3 places:: + + PIL/__init__.py _imaging.c setup.py diff --git a/docs/guides.rst b/docs/guides.rst index 926d3cfcc..87ce75bd4 100644 --- a/docs/guides.rst +++ b/docs/guides.rst @@ -8,3 +8,4 @@ Guides handbook/tutorial handbook/concepts porting-pil-to-pillow + developer diff --git a/docs/handbook/concepts.rst b/docs/handbook/concepts.rst index 93f964e41..b5e5e44c1 100644 --- a/docs/handbook/concepts.rst +++ b/docs/handbook/concepts.rst @@ -27,6 +27,8 @@ image. The current release supports the following standard modes: * ``RGBA`` (4x8-bit pixels, true color with transparency mask) * ``CMYK`` (4x8-bit pixels, color separation) * ``YCbCr`` (3x8-bit pixels, color video format) + * ``LAB`` (3x8-bit pixels, the L*a*b color space) + * ``HSV`` (3x8-bit pixels, Hue, Saturation, Value color space) * ``I`` (32-bit signed integer pixels) * ``F`` (32-bit floating point pixels) @@ -34,7 +36,7 @@ PIL also provides limited support for a few special modes, including ``LA`` (L with alpha), ``RGBX`` (true color with padding) and ``RGBa`` (true color with premultiplied alpha). However, PIL doesn’t support user-defined modes; if you to handle band combinations that are not listed above, use a sequence of Image -objects. +objects. You can read the mode of an image through the :py:attr:`~PIL.Image.Image.mode` attribute. This is a string containing one of the above values. diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index fddc134e6..97caea722 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -136,7 +136,7 @@ The :py:meth:`~PIL.Image.Image.save` method supports the following options: **quality** The image quality, on a scale from 1 (worst) to 95 (best). The default is 75. Values above 95 should be avoided; 100 disables portions of the JPEG - compression algorithm, and results in large files with hardly any gain in = + compression algorithm, and results in large files with hardly any gain in image quality. **optimize** @@ -147,6 +147,29 @@ 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. +**subsampling** + 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`` + * ``0``: equivalent to ``4:4:4`` + * ``1``: equivalent to ``4:2:2`` + * ``2``: equivalent to ``4:1:1`` + +**qtables** + If present, sets the qtables for the encoder. This is listed as an + advanced option for wizards in the JPEG documentation. Use with + caution. ``qtables`` can be one of several types of values: + + * 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. + + .. versionadded:: 2.5.0 + + .. note:: To enable JPEG support, you need to build and install the IJG JPEG library @@ -565,6 +588,20 @@ PIL identifies and reads Microsoft Image Composer (MIC) files. When opened, the first sprite in the file is loaded. You can use :py:meth:`~file.seek` and :py:meth:`~file.tell` to read other sprites from the file. +MPO +^^^ + +Pillow identifies and reads Multi Picture Object (MPO) files, loading the primary +image when first opened. The :py:meth:`~file.seek` and :py:meth:`~file.tell` +methods may be used to read other pictures from the file. The pictures are +zero-indexed and random access is supported. + +MIC (read only) + +Pillow identifies and reads Microsoft Image Composer (MIC) files. When opened, the +first sprite in the file is loaded. You can use :py:meth:`~file.seek` and +:py:meth:`~file.tell` to read other sprites from the file. + PCD ^^^ diff --git a/docs/index.rst b/docs/index.rst index 25e9f6b73..16e450856 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,11 +1,10 @@ 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-imaging/Pillow.svg?branch=master - :target: https://travis-ci.org/python-imaging/Pillow +.. image:: https://travis-ci.org/python-pillow/Pillow.svg?branch=master + :target: https://travis-ci.org/python-pillow/Pillow :alt: Travis CI build status .. image:: https://pypip.in/v/Pillow/badge.png @@ -16,16 +15,11 @@ Python Imaging Library by Fredrik Lundh and Contributors. :target: https://pypi.python.org/pypi/Pillow/ :alt: Number of PyPI downloads -.. image:: https://coveralls.io/repos/python-imaging/Pillow/badge.png?branch=master - :target: https://coveralls.io/r/python-imaging/Pillow?branch=master +.. 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 -To start using Pillow, please read the :doc:`installation -instructions `. - -You can get the source and contribute at -https://github.com/python-imaging/Pillow. You can download archives -and old versions from `PyPI `_. +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. .. toctree:: :maxdepth: 2 @@ -37,30 +31,6 @@ and old versions from `PyPI `_. handbook/appendices original-readme -Support Pillow! -=============== - -PIL needs you! Please help us maintain the Python Imaging Library here: - -- `GitHub `_ -- `Freenode `_ -- `Image-SIG `_ - -Financial ---------- - -Pillow is a volunteer effort led by Alex Clark. If you can't help with -development please consider helping us financially. Your assistance would -be very much appreciated! - -.. note:: Contributors please add your name and donation preference here. - -======================================= ======================================= -**Developer** **Preference** -======================================= ======================================= -Alex Clark (fork author) http://gittip.com/aclark4life -======================================= ======================================= - Indices and tables ================== diff --git a/docs/installation.rst b/docs/installation.rst index 94054df82..a61213e15 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -44,6 +44,10 @@ run:: External libraries ------------------ +.. note:: + + You *do not* need to install all of the external libraries to use Pillow's basic features. + Many of Pillow's features require external libraries: * **libjpeg** provides JPEG functionality. @@ -67,23 +71,29 @@ Many of Pillow's features require external libraries: * 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 - transparency. + transparency. -* **tcl/tk** provides support for tkinter bitmap and photo images. +* **tcl/tk** provides support for tkinter bitmap and photo images. -* **openjpeg** provides JPEG 2000 functionality. +* **openjpeg** provides JPEG 2000 functionality. - * Pillow has been tested with openjpeg **2.0.0**. + * Pillow has been tested with openjpeg **2.0.0** and **2.1.0**. -If the prerequisites are installed in the standard library locations for your -machine (e.g. :file:`/usr` or :file:`/usr/local`), no additional configuration -should be required. If they are installed in a non-standard location, you may -need to configure setuptools to use those locations (i.e. by editing -:file:`setup.py` and/or :file:`setup.cfg`). Once you have installed the -prerequisites, run:: +Once you have installed the prerequisites,run:: $ pip install Pillow +If the prerequisites are installed in the standard library locations +for your machine (e.g. :file:`/usr` or :file:`/usr/local`), no +additional configuration should be required. If they are installed in +a non-standard location, you may need to configure setuptools to use +those locations by editing :file:`setup.py` or +:file:`setup.cfg`, or by adding environment variables on the command +line:: + + $ CFLAGS="-I/usr/pkg/include" pip install pillow + + Linux installation ------------------ @@ -92,11 +102,6 @@ Linux installation Fedora, Debian/Ubuntu, and ArchLinux include Pillow (instead of PIL) with their distributions. Consider using those instead of installing manually. -.. note:: - - You *do not* need to install all of the external libraries to get Pillow's - basics to work. - **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:: @@ -108,7 +113,7 @@ 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 10.04 LTS** with:: @@ -131,22 +136,13 @@ Prerequisites are installed on **Fedora 20** with:: Mac OS X installation --------------------- -.. note:: +We provide binaries for OS X in the form of `Python Wheels `_. Alternatively you can compile Pillow with with XCode. - You *do not* need to install all of the external libraries to get Pillow's - basics to work. - -**We do not provide binaries for OS X**, so you'll need XCode to install -Pillow. (XCode 4.2 on 10.6 will work with the Official Python binary -distribution. Otherwise, use whatever XCode you used to compile Python.) - -The easiest way to install the prerequisites is via `Homebrew -`_. After you install Homebrew, run:: +The easiest way to install external libraries is via `Homebrew `_. After you install Homebrew, run:: $ brew install libtiff libjpeg webp little-cms2 -If you've built your own Python, then you should be able to install Pillow -using:: +Install Pillow with:: $ pip install Pillow @@ -185,6 +181,25 @@ to a specific version: $ pip install --use-wheel Pillow==2.3.0 +FreeBSD installation +--------------------- + +.. Note:: Only FreeBSD 10 tested + + +Make sure you have Python's development libraries installed.:: + + $ sudo pkg install python2 + +Or for Python 3:: + + $ sudo pkg install python3 + +Prerequisites are installed on **FreeBSD 10** with:: + + $ sudo pkg install jpeg tiff webp lcms2 freetype2 + + Platform support ---------------- @@ -199,7 +214,7 @@ current versions of Linux, OS X, and Windows. Contributors please test on your platform, edit 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 | @@ -224,6 +239,8 @@ current versions of Linux, OS X, and Windows. +----------------------------------+-------------+------------------------------+------------------------------+-----------------------+ | Gentoo Linux |Yes | 2.7,3.2 | 2.1.0 |x86-64 | +----------------------------------+-------------+------------------------------+------------------------------+-----------------------+ +| FreeBSD 10 |Yes | 2.7,3.4 | 2.4,2.3.1 |x86-64 | ++----------------------------------+-------------+------------------------------+------------------------------+-----------------------+ | Windows 7 Pro |Yes | 2.7,3.2,3.3 | 2.2.1 |x86-64 | +----------------------------------+-------------+------------------------------+------------------------------+-----------------------+ | Windows Server 2008 R2 Enterprise|Yes | 3.3 | |x86-64 | @@ -233,3 +250,7 @@ current versions of Linux, OS X, and Windows. | Windows 8.1 Pro |Yes | 2.6,2.7,3.2,3.3,3.4 | 2.3.0, 2.4.0 |x86,x86-64 | +----------------------------------+-------------+------------------------------+------------------------------+-----------------------+ +Old Versions +------------ + +You can download old distributions from `PyPI `_. Only the latest 1.x and 2.x releases are visible, but all releases are available by direct URL access e.g. https://pypi.python.org/pypi/Pillow/1.0. diff --git a/docs/plugins.rst b/docs/plugins.rst index 001cee949..a069f80df 100644 --- a/docs/plugins.rst +++ b/docs/plugins.rst @@ -1,14 +1,6 @@ Plugin reference ================ -:mod:`ArgImagePlugin` Module ----------------------------- - -.. automodule:: PIL.ArgImagePlugin - :members: - :undoc-members: - :show-inheritance: - :mod:`BmpImagePlugin` Module ---------------------------- diff --git a/docs/reference/ExifTags.rst b/docs/reference/ExifTags.rst new file mode 100644 index 000000000..9fc7cd13b --- /dev/null +++ b/docs/reference/ExifTags.rst @@ -0,0 +1,26 @@ +.. py:module:: PIL.ExifTags +.. py:currentmodule:: PIL.ExifTags + +:py:mod:`ExifTags` Module +========================== + +The :py:mod:`ExifTags` module exposes two dictionaries which +provide constants and clear-text names for various well-known EXIF tags. + +.. py:class:: PIL.ExifTags.TAGS + + The TAG dictionary maps 16-bit integer EXIF tag enumerations to + descriptive string names. For instance: + + >>> from PIL.ExifTags import TAGS + >>> TAGS[0x010e] + 'ImageDescription' + +.. py:class:: PIL.ExifTags.GPSTAGS + + The GPSTAGS dictionary maps 8-bit integer EXIF gps enumerations to + descriptive string names. For instance: + + >>> from PIL.ExifTags import GPSTAGS + >>> GPSTAGS[20] + 'GPSDestLatitude' diff --git a/docs/reference/Image.rst b/docs/reference/Image.rst index 7125fcad4..11666dd0b 100644 --- a/docs/reference/Image.rst +++ b/docs/reference/Image.rst @@ -49,6 +49,11 @@ 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. + + .. _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 + Image processing ^^^^^^^^^^^^^^^^ diff --git a/docs/reference/ImageCms.rst b/docs/reference/ImageCms.rst new file mode 100644 index 000000000..2d5bb1388 --- /dev/null +++ b/docs/reference/ImageCms.rst @@ -0,0 +1,13 @@ +.. py:module:: PIL.ImageCms +.. py:currentmodule:: PIL.ImageCms + +:py:mod:`ImageCms` Module +========================= + +The :py:mod:`ImageCms` module provides color profile management +support using the LittleCMS2 color management engine, based on Kevin +Cazabon's PyCMS library. + +.. automodule:: PIL.ImageCms + :members: + :noindex: diff --git a/docs/reference/ImageDraw.rst b/docs/reference/ImageDraw.rst index 68855eb5b..c24a9dd99 100644 --- a/docs/reference/ImageDraw.rst +++ b/docs/reference/ImageDraw.rst @@ -74,6 +74,34 @@ To load a OpenType/TrueType font, use the truetype function in the :py:mod:`~PIL.ImageFont` module. Note that this function depends on third-party libraries, and may not available in all PIL builds. +Example: Draw Partial Opacity Text +---------------------------------- + +.. code-block:: python + + from PIL import Image, ImageDraw, ImageFont + # get an image + base = Image.open('Pillow/Tests/images/lena.png').convert('RGBA') + + # make a blank image for the text, initialized to transparent text color + txt = Image.new('RGBA', base.size, (255,255,255,0)) + + # get a font + fnt = ImageFont.truetype('Pillow/Tests/fonts/FreeMono.ttf', 40) + # get a drawing context + d = ImageDraw.Draw(txt) + + # draw text, half opacity + d.text((10,10), "Hello", font=fnt, fill=(255,255,255,128)) + # draw text, full opacity + d.text((10,60), "World", font=fnt, fill=(255,255,255,255)) + + out = Image.alpha_composite(base, txt) + + out.show() + + + Functions --------- @@ -83,6 +111,13 @@ Functions Note that the image will be modified in place. + :param im: The image to draw in. + :param mode: Optional mode to use for color values. For RGB + images, this argument can be RGB or RGBA (to blend the + drawing into the image). For all other modes, this argument + must be the same as the image mode. If omitted, the mode + defaults to the mode of the image. + Methods ------- @@ -91,9 +126,12 @@ Methods Draws an arc (a portion of a circle outline) between the start and end angles, inside the given bounding box. - :param xy: Four points to define the bounding box. Sequence of either + :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 arc. .. py:method:: PIL.ImageDraw.Draw.bitmap(xy, bitmap, fill=None) @@ -111,7 +149,7 @@ Methods Same as :py:meth:`~PIL.ImageDraw.Draw.arc`, but connects the end points with a straight line. - :param xy: Four points to define the bounding box. Sequence of either + :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 fill: Color to use for the fill. @@ -144,7 +182,7 @@ Methods Same as arc, but also draws straight lines between the end points and the center of the bounding box. - :param xy: Four points to define the bounding box. Sequence of either + :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 fill: Color to use for the fill. diff --git a/docs/reference/ImageMorph.rst b/docs/reference/ImageMorph.rst new file mode 100644 index 000000000..eaf3b1c5e --- /dev/null +++ b/docs/reference/ImageMorph.rst @@ -0,0 +1,13 @@ +.. py:module:: PIL.ImageMorph +.. py:currentmodule:: PIL.ImageMorph + +:py:mod:`ImageMorph` Module +=========================== + +The :py:mod:`ImageMorph` module provides morphology operations on images. + +.. automodule:: PIL.ImageMorph + :members: + :undoc-members: + :show-inheritance: + :noindex: diff --git a/docs/reference/ImageStat.rst b/docs/reference/ImageStat.rst index c8dfe3062..b8925bf8c 100644 --- a/docs/reference/ImageStat.rst +++ b/docs/reference/ImageStat.rst @@ -22,32 +22,32 @@ for a region of an image. .. py:attribute:: count - Total number of pixels. + Total number of pixels for each band in the image. .. py:attribute:: sum - Sum of all pixels. + Sum of all pixels for each band in the image. .. py:attribute:: sum2 - Squared sum of all pixels. + Squared sum of all pixels for each band in the image. - .. py:attribute:: pixel + .. py:attribute:: mean - Average pixel level. + Average (arithmetic mean) pixel level for each band in the image. .. py:attribute:: median - Median pixel level. + Median pixel level for each band in the image. .. py:attribute:: rms - RMS (root-mean-square). + RMS (root-mean-square) for each band in the image. .. py:attribute:: var - Variance. + Variance for each band in the image. .. py:attribute:: stddev - Standard deviation. + Standard deviation for each band in the image. diff --git a/docs/reference/OleFileIO.rst b/docs/reference/OleFileIO.rst new file mode 100644 index 000000000..74c4b7b36 --- /dev/null +++ b/docs/reference/OleFileIO.rst @@ -0,0 +1,364 @@ +.. py:module:: PIL.OleFileIO +.. py:currentmodule:: PIL.OleFileIO + +:py:mod:`OleFileIO` Module +=========================== + +The :py:mod:`OleFileIO` module reads Microsoft OLE2 files (also called +Structured Storage or Microsoft Compound Document File Format), such +as Microsoft Office documents, Image Composer and FlashPix files, and +Outlook messages. + +This module is the `OleFileIO\_PL`_ project by Philippe Lagadec, v0.30, +merged back into Pillow. + +.. _OleFileIO\_PL: http://www.decalage.info/python/olefileio + +How to use this module +---------------------- + +For more information, see also the file **PIL/OleFileIO.py**, sample +code at the end of the module itself, and docstrings within the code. + +About the structure of OLE files +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +An OLE file can be seen as a mini file system or a Zip archive: It +contains **streams** of data that look like files embedded within the +OLE file. Each stream has a name. For example, the main stream of a MS +Word document containing its text is named "WordDocument". + +An OLE file can also contain **storages**. A storage is a folder that +contains streams or other storages. For example, a MS Word document with +VBA macros has a storage called "Macros". + +Special streams can contain **properties**. A property is a specific +value that can be used to store information such as the metadata of a +document (title, author, creation date, etc). Property stream names +usually start with the character '05'. + +For example, a typical MS Word document may look like this: + +:: + + \x05DocumentSummaryInformation (stream) + \x05SummaryInformation (stream) + WordDocument (stream) + Macros (storage) + PROJECT (stream) + PROJECTwm (stream) + VBA (storage) + Module1 (stream) + ThisDocument (stream) + _VBA_PROJECT (stream) + dir (stream) + ObjectPool (storage) + +Test if a file is an OLE container +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Use isOleFile to check if the first bytes of the file contain the Magic +for OLE files, before opening it. isOleFile returns True if it is an OLE +file, False otherwise. + +.. code-block:: python + + assert OleFileIO.isOleFile('myfile.doc') + +Open an OLE file from disk +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Create an OleFileIO object with the file path as parameter: + +.. code-block:: python + + ole = OleFileIO.OleFileIO('myfile.doc') + +Open an OLE file from a file-like object +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +This is useful if the file is not on disk, e.g. already stored in a +string or as a file-like object. + +.. code-block:: python + + ole = OleFileIO.OleFileIO(f) + +For example the code below reads a file into a string, then uses BytesIO +to turn it into a file-like object. + +.. code-block:: python + + data = open('myfile.doc', 'rb').read() + f = io.BytesIO(data) # or StringIO.StringIO for Python 2.x + ole = OleFileIO.OleFileIO(f) + +How to handle malformed OLE files +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +By default, the parser is configured to be as robust and permissive as +possible, allowing to parse most malformed OLE files. Only fatal errors +will raise an exception. It is possible to tell the parser to be more +strict in order to raise exceptions for files that do not fully conform +to the OLE specifications, using the raise\_defect option: + +.. code-block:: python + + ole = OleFileIO.OleFileIO('myfile.doc', raise_defects=DEFECT_INCORRECT) + +When the parsing is done, the list of non-fatal issues detected is +available as a list in the parsing\_issues attribute of the OleFileIO +object: + +.. code-block:: python + + print('Non-fatal issues raised during parsing:') + if ole.parsing_issues: + for exctype, msg in ole.parsing_issues: + print('- %s: %s' % (exctype.__name__, msg)) + else: + print('None') + +Syntax for stream and storage path +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Two different syntaxes are allowed for methods that need or return the +path of streams and storages: + +1) Either a **list of strings** including all the storages from the root + up to the stream/storage name. For example a stream called + "WordDocument" at the root will have ['WordDocument'] as full path. A + stream called "ThisDocument" located in the storage "Macros/VBA" will + be ['Macros', 'VBA', 'ThisDocument']. This is the original syntax + from PIL. While hard to read and not very convenient, this syntax + works in all cases. + +2) Or a **single string with slashes** to separate storage and stream + names (similar to the Unix path syntax). The previous examples would + be 'WordDocument' and 'Macros/VBA/ThisDocument'. This syntax is + easier, but may fail if a stream or storage name contains a slash. + +Both are case-insensitive. + +Switching between the two is easy: + +.. code-block:: python + + slash_path = '/'.join(list_path) + list_path = slash_path.split('/') + +Get the list of streams +~~~~~~~~~~~~~~~~~~~~~~~ + +listdir() returns a list of all the streams contained in the OLE file, +including those stored in storages. Each stream is listed itself as a +list, as described above. + +.. code-block:: python + + print(ole.listdir()) + +Sample result: + +.. code-block:: python + + [['\x01CompObj'], ['\x05DocumentSummaryInformation'], ['\x05SummaryInformation'] + , ['1Table'], ['Macros', 'PROJECT'], ['Macros', 'PROJECTwm'], ['Macros', 'VBA', + 'Module1'], ['Macros', 'VBA', 'ThisDocument'], ['Macros', 'VBA', '_VBA_PROJECT'] + , ['Macros', 'VBA', 'dir'], ['ObjectPool'], ['WordDocument']] + +As an option it is possible to choose if storages should also be listed, +with or without streams: + +.. code-block:: python + + ole.listdir (streams=False, storages=True) + +Test if known streams/storages exist: +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +exists(path) checks if a given stream or storage exists in the OLE file. + +.. code-block:: python + + if ole.exists('worddocument'): + print("This is a Word document.") + if ole.exists('macros/vba'): + print("This document seems to contain VBA macros.") + +Read data from a stream +~~~~~~~~~~~~~~~~~~~~~~~ + +openstream(path) opens a stream as a file-like object. + +The following example extracts the "Pictures" stream from a PPT file: + +.. code-block:: python + + pics = ole.openstream('Pictures') + data = pics.read() + + +Get information about a stream/storage +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Several methods can provide the size, type and timestamps of a given +stream/storage: + +get\_size(path) returns the size of a stream in bytes: + +.. code-block:: python + + s = ole.get_size('WordDocument') + +get\_type(path) returns the type of a stream/storage, as one of the +following constants: STGTY\_STREAM for a stream, STGTY\_STORAGE for a +storage, STGTY\_ROOT for the root entry, and False for a non existing +path. + +.. code-block:: python + + t = ole.get_type('WordDocument') + +get\_ctime(path) and get\_mtime(path) return the creation and +modification timestamps of a stream/storage, as a Python datetime object +with UTC timezone. Please note that these timestamps are only present if +the application that created the OLE file explicitly stored them, which +is rarely the case. When not present, these methods return None. + +.. code-block:: python + + c = ole.get_ctime('WordDocument') + m = ole.get_mtime('WordDocument') + +The root storage is a special case: You can get its creation and +modification timestamps using the OleFileIO.root attribute: + +.. code-block:: python + + c = ole.root.getctime() + m = ole.root.getmtime() + +Extract metadata +~~~~~~~~~~~~~~~~ + +get\_metadata() will check if standard property streams exist, parse all +the properties they contain, and return an OleMetadata object with the +found properties as attributes. + +.. code-block:: python + + meta = ole.get_metadata() + print('Author:', meta.author) + print('Title:', meta.title) + print('Creation date:', meta.create_time) + # print all metadata: + meta.dump() + +Available attributes include: + +:: + + codepage, title, subject, author, keywords, comments, template, + last_saved_by, revision_number, total_edit_time, last_printed, create_time, + last_saved_time, num_pages, num_words, num_chars, thumbnail, + creating_application, security, codepage_doc, category, presentation_target, + bytes, lines, paragraphs, slides, notes, hidden_slides, mm_clips, + scale_crop, heading_pairs, titles_of_parts, manager, company, links_dirty, + chars_with_spaces, unused, shared_doc, link_base, hlinks, hlinks_changed, + version, dig_sig, content_type, content_status, language, doc_version + +See the source code of the OleMetadata class for more information. + +Parse a property stream +~~~~~~~~~~~~~~~~~~~~~~~ + +get\_properties(path) can be used to parse any property stream that is +not handled by get\_metadata. It returns a dictionary indexed by +integers. Each integer is the index of the property, pointing to its +value. For example in the standard property stream +'05SummaryInformation', the document title is property #2, and the +subject is #3. + +.. code-block:: python + + p = ole.getproperties('specialprops') + +By default as in the original PIL version, timestamp properties are +converted into a number of seconds since Jan 1,1601. With the option +convert\_time, you can obtain more convenient Python datetime objects +(UTC timezone). If some time properties should not be converted (such as +total editing time in '05SummaryInformation'), the list of indexes can +be passed as no\_conversion: + +.. code-block:: python + + p = ole.getproperties('specialprops', convert_time=True, no_conversion=[10]) + +Close the OLE file +~~~~~~~~~~~~~~~~~~ + +Unless your application is a simple script that terminates after +processing an OLE file, do not forget to close each OleFileIO object +after parsing to close the file on disk. + +.. code-block:: python + + ole.close() + +Use OleFileIO as a script +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +OleFileIO can also be used as a script from the command-line to +display the structure of an OLE file and its metadata, for example: + +:: + + PIL/OleFileIO.py myfile.doc + +You can use the option -c to check that all streams can be read fully, +and -d to generate very verbose debugging information. + +How to contribute +----------------- + +The code is available in `a Mercurial repository on +bitbucket `_. You may use +it to submit enhancements or to report any issue. + +If you would like to help us improve this module, or simply provide +feedback, please `contact me `_. You can +help in many ways: + +- test this module on different platforms / Python versions +- find and report bugs +- improve documentation, code samples, docstrings +- write unittest test cases +- provide tricky malformed files + +How to report bugs +------------------ + +To report a bug, for example a normal file which is not parsed +correctly, please use the `issue reporting +page `_, +or if you prefer to do it privately, use this `contact +form `_. Please provide all the +information about the context and how to reproduce the bug. + +If possible please join the debugging output of OleFileIO. For this, +launch the following command : + +:: + + PIL/OleFileIO.py -d -c file >debug.txt + + +Classes and Methods +------------------- + +.. automodule:: PIL.OleFileIO + :members: + :undoc-members: + :show-inheritance: + :noindex: diff --git a/docs/reference/index.rst b/docs/reference/index.rst index 2d57e37be..2f10b861d 100644 --- a/docs/reference/index.rst +++ b/docs/reference/index.rst @@ -4,9 +4,11 @@ Reference .. toctree:: :maxdepth: 2 + Image ImageChops ImageColor + ImageCms ImageDraw ImageEnhance ImageFile @@ -14,6 +16,7 @@ Reference ImageFont ImageGrab ImageMath + ImageMorph ImageOps ImagePalette ImagePath @@ -22,5 +25,7 @@ Reference ImageStat ImageTk ImageWin + ExifTags + OleFileIO PSDraw ../PIL diff --git a/libImaging/Access.c b/libImaging/Access.c index 62c97f3a3..97474a0b8 100644 --- a/libImaging/Access.c +++ b/libImaging/Access.c @@ -13,8 +13,8 @@ #include "Imaging.h" /* use Tests/make_hash.py to calculate these values */ -#define ACCESS_TABLE_SIZE 21 -#define ACCESS_TABLE_HASH 30197 +#define ACCESS_TABLE_SIZE 27 +#define ACCESS_TABLE_HASH 3078 static struct ImagingAccessInstance access_table[ACCESS_TABLE_SIZE]; @@ -238,6 +238,7 @@ ImagingAccessInit() ADD("CMYK", line_32, get_pixel_32, put_pixel_32); ADD("YCbCr", line_32, get_pixel_32, put_pixel_32); ADD("LAB", line_32, get_pixel_32, put_pixel_32); + ADD("HSV", line_32, get_pixel_32, put_pixel_32); } ImagingAccess diff --git a/libImaging/Convert.c b/libImaging/Convert.c index 631263b31..46a6cfb72 100644 --- a/libImaging/Convert.c +++ b/libImaging/Convert.c @@ -35,6 +35,9 @@ #include "Imaging.h" +#define MAX(a, b) (a)>(b) ? (a) : (b) +#define MIN(a, b) (a)<(b) ? (a) : (b) + #define CLIP(v) ((v) <= 0 ? 0 : (v) >= 255 ? 255 : (v)) #define CLIP16(v) ((v) <= -32768 ? -32768 : (v) >= 32767 ? 32767 : (v)) @@ -236,6 +239,126 @@ rgb2bgr24(UINT8* out, const UINT8* in, int xsize) } } +static void +rgb2hsv(UINT8* out, const UINT8* in, int xsize) +{ // following colorsys.py + float h,s,rc,gc,bc,cr; + UINT8 maxc,minc; + UINT8 r, g, b; + UINT8 uh,us,uv; + int x; + + for (x = 0; x < xsize; x++, in += 4) { + r = in[0]; + g = in[1]; + b = in[2]; + + maxc = MAX(r,MAX(g,b)); + minc = MIN(r,MIN(g,b)); + uv = maxc; + if (minc == maxc){ + *out++ = 0; + *out++ = 0; + *out++ = uv; + } else { + cr = (float)(maxc-minc); + s = cr/(float)maxc; + rc = ((float)(maxc-r))/cr; + gc = ((float)(maxc-g))/cr; + bc = ((float)(maxc-b))/cr; + if (r == maxc) { + h = bc-gc; + } else if (g == maxc) { + h = 2.0 + rc-bc; + } else { + h = 4.0 + gc-rc; + } + // incorrect hue happens if h/6 is negative. + h = fmod((h/6.0 + 1.0), 1.0); + + uh = (UINT8)CLIP((int)(h*255.0)); + us = (UINT8)CLIP((int)(s*255.0)); + + *out++ = uh; + *out++ = us; + *out++ = uv; + + } + *out++ = in[3]; + } +} + +static void +hsv2rgb(UINT8* out, const UINT8* in, int xsize) +{ // following colorsys.py + + int p,q,t; + UINT8 up,uq,ut; + int i, x; + float f, fs; + UINT8 h,s,v; + + for (x = 0; x < xsize; x++, in += 4) { + h = in[0]; + s = in[1]; + v = in[2]; + + if (s==0){ + *out++ = v; + *out++ = v; + *out++ = v; + } else { + i = floor((float)h * 6.0 / 255.0); // 0 - 6 + f = (float)h * 6.0 / 255.0 - (float)i; // 0-1 : remainder. + fs = ((float)s)/255.0; + + p = round((float)v * (1.0-fs)); + q = round((float)v * (1.0-fs*f)); + t = round((float)v * (1.0-fs*(1.0-f))); + up = (UINT8)CLIP(p); + uq = (UINT8)CLIP(q); + ut = (UINT8)CLIP(t); + + switch (i%6) { + case 0: + *out++ = v; + *out++ = ut; + *out++ = up; + break; + case 1: + *out++ = uq; + *out++ = v; + *out++ = up; + break; + case 2: + *out++ = up; + *out++ = v; + *out++ = ut; + break; + case 3: + *out++ = up; + *out++ = uq; + *out++ = v; + break; + case 4: + *out++ = ut; + *out++ = up; + *out++ = v; + break; + case 5: + *out++ = v; + *out++ = up; + *out++ = uq; + break; + + } + } + *out++ = in[3]; + } +} + + + /* ---------------- */ /* RGBA conversions */ /* ---------------- */ @@ -658,6 +781,7 @@ static struct { { "RGB", "RGBX", rgb2rgba }, { "RGB", "CMYK", rgb2cmyk }, { "RGB", "YCbCr", ImagingConvertRGB2YCbCr }, + { "RGB", "HSV", rgb2hsv }, { "RGBA", "1", rgb2bit }, { "RGBA", "L", rgb2l }, @@ -687,6 +811,8 @@ static struct { { "YCbCr", "L", ycbcr2l }, { "YCbCr", "RGB", ImagingConvertYCbCr2RGB }, + { "HSV", "RGB", hsv2rgb }, + { "I", "I;16", I_I16L }, { "I;16", "I", I16L_I }, { "L", "I;16", L_I16L }, diff --git a/libImaging/Draw.c b/libImaging/Draw.c index f13ba4df0..2ff03e049 100644 --- a/libImaging/Draw.c +++ b/libImaging/Draw.c @@ -49,6 +49,13 @@ #define BLEND(mask, in1, in2, tmp1, tmp2)\ (MULDIV255(in1, 255 - mask, tmp1) + MULDIV255(in2, mask, tmp2)) +/* + * Rounds around zero (up=away from zero, down=torwards zero) + * This guarantees that ROUND_UP|DOWN(f) == -ROUND_UP|DOWN(-f) + */ +#define ROUND_UP(f) ((int) ((f) >= 0.0 ? floor((f) + 0.5F) : -floor(fabs(f) + 0.5F))) +#define ROUND_DOWN(f) ((int) ((f) >= 0.0 ? ceil((f) - 0.5F) : -ceil(fabs(f) - 0.5F))) + /* -------------------------------------------------------------------- */ /* Primitives */ /* -------------------------------------------------------------------- */ @@ -61,6 +68,9 @@ typedef struct { float dx; } Edge; +/* Type used in "polygon*" functions */ +typedef void (*hline_handler)(Imaging, int, int, int, int); + static inline void point8(Imaging im, int x, int y, int ink) { @@ -403,177 +413,100 @@ x_cmp(const void *x0, const void *x1) return 0; } + +/* + * Filled polygon draw function using scan line algorithm. + */ static inline int -polygon8(Imaging im, int n, Edge *e, int ink, int eofill) +polygon_generic(Imaging im, int n, Edge *e, int ink, int eofill, + hline_handler hline) { - int i, j; - float *xx; - int ymin, ymax; - float y; - if (n <= 0) + Edge** edge_table; + float* xx; + int edge_count = 0; + int ymin = im->ysize - 1; + int ymax = 0; + int i; + + if (n <= 0) { return 0; - - /* Find upper and lower polygon boundary (within image) */ - - ymin = e[0].ymin; - ymax = e[0].ymax; - for (i = 1; i < n; i++) { - if (e[i].ymin < ymin) ymin = e[i].ymin; - if (e[i].ymax > ymax) ymax = e[i].ymax; } - if (ymin < 0) - ymin = 0; - if (ymax >= im->ysize) - ymax = im->ysize-1; - - /* Process polygon edges */ - - xx = malloc(n * sizeof(float)); - if (!xx) + /* Initialize the edge table and find polygon boundaries */ + edge_table = malloc(sizeof(Edge*) * n); + if (!edge_table) { return -1; + } - for (;ymin <= ymax; ymin++) { - y = ymin+0.5F; - for (i = j = 0; i < n; i++) - if (y >= e[i].ymin && y <= e[i].ymax) { - if (e[i].d == 0) - hline8(im, e[i].xmin, ymin, e[i].xmax, ink); - else - xx[j++] = (y-e[i].y0) * e[i].dx + e[i].x0; + for (i = 0; i < n; i++) { + /* This causes that the pixels of horizontal edges are drawn twice :( + * but without it there are inconsistencies in ellipses */ + if (e[i].ymin == e[i].ymax) { + (*hline)(im, e[i].xmin, e[i].ymin, e[i].xmax, ink); + continue; + } + if (ymin > e[i].ymin) { + ymin = e[i].ymin; + } + if (ymax < e[i].ymax) { + ymax = e[i].ymax; + } + edge_table[edge_count++] = (e + i); + } + if (ymin < 0) { + ymin = 0; + } + if (ymax >= im->ysize) { + ymax = im->ysize - 1; + } + + /* Process the edge table with a scan line searching for intersections */ + xx = malloc(sizeof(float) * edge_count * 2); + if (!xx) { + free(edge_table); + return -1; + } + for (; ymin <= ymax; ymin++) { + int j = 0; + for (i = 0; i < edge_count; i++) { + Edge* current = edge_table[i]; + if (ymin >= current->ymin && ymin <= current->ymax) { + xx[j++] = (ymin - current->y0) * current->dx + current->x0; } - if (j == 2) { - if (xx[0] < xx[1]) - hline8(im, CEIL(xx[0]-0.5), ymin, FLOOR(xx[1]+0.5), ink); - else - hline8(im, CEIL(xx[1]-0.5), ymin, FLOOR(xx[0]+0.5), ink); - } else { - qsort(xx, j, sizeof(float), x_cmp); - for (i = 0; i < j-1 ; i += 2) - hline8(im, CEIL(xx[i]-0.5), ymin, FLOOR(xx[i+1]+0.5), ink); + /* Needed to draw consistent polygons */ + if (ymin == current->ymax && ymin < ymax) { + xx[j] = xx[j - 1]; + j++; + } + } + qsort(xx, j, sizeof(float), x_cmp); + for (i = 1; i < j; i += 2) { + (*hline)(im, ROUND_UP(xx[i - 1]), ymin, ROUND_DOWN(xx[i]), ink); } } free(xx); - + free(edge_table); return 0; } +static inline int +polygon8(Imaging im, int n, Edge *e, int ink, int eofill) +{ + return polygon_generic(im, n, e, ink, eofill, hline8); +} + static inline int polygon32(Imaging im, int n, Edge *e, int ink, int eofill) { - int i, j; - float *xx; - int ymin, ymax; - float y; - - if (n <= 0) - return 0; - - /* Find upper and lower polygon boundary (within image) */ - - ymin = e[0].ymin; - ymax = e[0].ymax; - for (i = 1; i < n; i++) { - if (e[i].ymin < ymin) ymin = e[i].ymin; - if (e[i].ymax > ymax) ymax = e[i].ymax; - } - - if (ymin < 0) - ymin = 0; - if (ymax >= im->ysize) - ymax = im->ysize-1; - - /* Process polygon edges */ - - xx = malloc(n * sizeof(float)); - if (!xx) - return -1; - - for (;ymin <= ymax; ymin++) { - y = ymin+0.5F; - for (i = j = 0; i < n; i++) { - if (y >= e[i].ymin && y <= e[i].ymax) { - if (e[i].d == 0) - hline32(im, e[i].xmin, ymin, e[i].xmax, ink); - else - xx[j++] = (y-e[i].y0) * e[i].dx + e[i].x0; - } - } - if (j == 2) { - if (xx[0] < xx[1]) - hline32(im, CEIL(xx[0]-0.5), ymin, FLOOR(xx[1]+0.5), ink); - else - hline32(im, CEIL(xx[1]-0.5), ymin, FLOOR(xx[0]+0.5), ink); - } else { - qsort(xx, j, sizeof(float), x_cmp); - for (i = 0; i < j-1 ; i += 2) - hline32(im, CEIL(xx[i]-0.5), ymin, FLOOR(xx[i+1]+0.5), ink); - } - } - - free(xx); - - return 0; + return polygon_generic(im, n, e, ink, eofill, hline32); } static inline int polygon32rgba(Imaging im, int n, Edge *e, int ink, int eofill) { - int i, j; - float *xx; - int ymin, ymax; - float y; - - if (n <= 0) - return 0; - - /* Find upper and lower polygon boundary (within image) */ - - ymin = e[0].ymin; - ymax = e[0].ymax; - for (i = 1; i < n; i++) { - if (e[i].ymin < ymin) ymin = e[i].ymin; - if (e[i].ymax > ymax) ymax = e[i].ymax; - } - - if (ymin < 0) - ymin = 0; - if (ymax >= im->ysize) - ymax = im->ysize-1; - - /* Process polygon edges */ - - xx = malloc(n * sizeof(float)); - if (!xx) - return -1; - - for (;ymin <= ymax; ymin++) { - y = ymin+0.5F; - for (i = j = 0; i < n; i++) { - if (y >= e[i].ymin && y <= e[i].ymax) { - if (e[i].d == 0) - hline32rgba(im, e[i].xmin, ymin, e[i].xmax, ink); - else - xx[j++] = (y-e[i].y0) * e[i].dx + e[i].x0; - } - } - if (j == 2) { - if (xx[0] < xx[1]) - hline32rgba(im, CEIL(xx[0]-0.5), ymin, FLOOR(xx[1]+0.5), ink); - else - hline32rgba(im, CEIL(xx[1]-0.5), ymin, FLOOR(xx[0]+0.5), ink); - } else { - qsort(xx, j, sizeof(float), x_cmp); - for (i = 0; i < j-1 ; i += 2) - hline32rgba(im, CEIL(xx[i]-0.5), ymin, FLOOR(xx[i+1]+0.5), ink); - } - } - - free(xx); - - return 0; + return polygon_generic(im, n, e, ink, eofill, hline32rgba); } static inline void @@ -663,11 +596,11 @@ ImagingDrawWideLine(Imaging im, int x0, int y0, int x1, int y1, { DRAW* draw; INT32 ink; - - Edge e[4]; - int dx, dy; - double d; + double big_hypotenuse, small_hypotenuse, ratio_max, ratio_min; + int dxmin, dxmax, dymin, dymax; + Edge e[4]; + int vertices[4][2]; DRAWINIT(); @@ -678,24 +611,35 @@ ImagingDrawWideLine(Imaging im, int x0, int y0, int x1, int y1, dx = x1-x0; dy = y1-y0; - if (dx == 0 && dy == 0) { draw->point(im, x0, y0, ink); return 0; } - d = width / sqrt((float) (dx*dx + dy*dy)) / 2.0; + big_hypotenuse = sqrt((double) (dx*dx + dy*dy)); + small_hypotenuse = (width - 1) / 2.0; + ratio_max = ROUND_UP(small_hypotenuse) / big_hypotenuse; + ratio_min = ROUND_DOWN(small_hypotenuse) / big_hypotenuse; - dx = (int) floor(d * (y1-y0) + 0.5); - dy = (int) floor(d * (x1-x0) + 0.5); + dxmin = ROUND_DOWN(ratio_min * dy); + dxmax = ROUND_DOWN(ratio_max * dy); + dymin = ROUND_DOWN(ratio_min * dx); + dymax = ROUND_DOWN(ratio_max * dx); + { + int vertices[4][2] = { + {x0 - dxmin, y0 + dymax}, + {x1 - dxmin, y1 + dymax}, + {x1 + dxmax, y1 - dymin}, + {x0 + dxmax, y0 - dymin} + }; - add_edge(e+0, x0 - dx, y0 + dy, x1 - dx, y1 + dy); - add_edge(e+1, x1 - dx, y1 + dy, x1 + dx, y1 - dy); - add_edge(e+2, x1 + dx, y1 - dy, x0 + dx, y0 - dy); - add_edge(e+3, x0 + dx, y0 - dy, x0 - dx, y0 + dy); - - draw->polygon(im, 4, e, ink, 0); + add_edge(e+0, vertices[0][0], vertices[0][1], vertices[1][0], vertices[1][1]); + add_edge(e+1, vertices[1][0], vertices[1][1], vertices[2][0], vertices[2][1]); + add_edge(e+2, vertices[2][0], vertices[2][1], vertices[3][0], vertices[3][1]); + add_edge(e+3, vertices[3][0], vertices[3][1], vertices[0][0], vertices[0][1]); + draw->polygon(im, 4, e, ink, 0); + } return 0; } diff --git a/libImaging/ImPlatform.h b/libImaging/ImPlatform.h index 92c126a10..70ee63119 100644 --- a/libImaging/ImPlatform.h +++ b/libImaging/ImPlatform.h @@ -17,12 +17,12 @@ #error Sorry, this library requires ANSI header files. #endif +#if defined(PIL_NO_INLINE) +#define inline +#else #if defined(_MSC_VER) && !defined(__GNUC__) #define inline __inline #endif - -#if !defined(PIL_USE_INLINE) -#define inline #endif #ifdef _WIN32 @@ -69,4 +69,6 @@ #define FLOAT32 float #define FLOAT64 double - +#ifdef _MSC_VER +typedef signed __int64 int64_t; +#endif diff --git a/libImaging/Imaging.h b/libImaging/Imaging.h index 26207d121..d958387c9 100644 --- a/libImaging/Imaging.h +++ b/libImaging/Imaging.h @@ -20,7 +20,7 @@ extern "C" { #ifndef M_PI -#define M_PI 3.14159265359 +#define M_PI 3.1415926535897932384626433832795 #endif diff --git a/libImaging/Jpeg2K.h b/libImaging/Jpeg2K.h index be6e0770b..fd53b59df 100644 --- a/libImaging/Jpeg2K.h +++ b/libImaging/Jpeg2K.h @@ -8,7 +8,7 @@ * Copyright (c) 2014 by Alastair Houghton */ -#include +#include /* -------------------------------------------------------------------- */ /* Decoder */ @@ -20,6 +20,9 @@ typedef struct { /* File descriptor, if available; otherwise, -1 */ int fd; + /* Length of data, if available; otherwise, -1 */ + off_t length; + /* Specify the desired format */ OPJ_CODEC_FORMAT format; diff --git a/libImaging/Jpeg2KDecode.c b/libImaging/Jpeg2KDecode.c index 6b6176c78..97ec81003 100644 --- a/libImaging/Jpeg2KDecode.c +++ b/libImaging/Jpeg2KDecode.c @@ -135,6 +135,56 @@ j2ku_gray_l(opj_image_t *in, const JPEG2KTILEINFO *tileinfo, } } + +static void +j2ku_gray_i(opj_image_t *in, const JPEG2KTILEINFO *tileinfo, + const UINT8 *tiledata, Imaging im) +{ + unsigned x0 = tileinfo->x0 - in->x0, y0 = tileinfo->y0 - in->y0; + unsigned w = tileinfo->x1 - tileinfo->x0; + unsigned h = tileinfo->y1 - tileinfo->y0; + + int shift = 16 - in->comps[0].prec; + int offset = in->comps[0].sgnd ? 1 << (in->comps[0].prec - 1) : 0; + int csiz = (in->comps[0].prec + 7) >> 3; + + unsigned x, y; + + if (csiz == 3) + csiz = 4; + + if (shift < 0) + offset += 1 << (-shift - 1); + + switch (csiz) { + case 1: + for (y = 0; y < h; ++y) { + const UINT8 *data = &tiledata[y * w]; + UINT16 *row = (UINT16 *)im->image[y0 + y] + x0; + for (x = 0; x < w; ++x) + *row++ = j2ku_shift(offset + *data++, shift); + } + break; + case 2: + for (y = 0; y < h; ++y) { + const UINT16 *data = (const UINT16 *)&tiledata[2 * y * w]; + UINT16 *row = (UINT16 *)im->image[y0 + y] + x0; + for (x = 0; x < w; ++x) + *row++ = j2ku_shift(offset + *data++, shift); + } + break; + case 4: + for (y = 0; y < h; ++y) { + const UINT32 *data = (const UINT32 *)&tiledata[4 * y * w]; + UINT16 *row = (UINT16 *)im->image[y0 + y] + x0; + for (x = 0; x < w; ++x) + *row++ = j2ku_shift(offset + *data++, shift); + } + break; + } +} + + static void j2ku_gray_rgb(opj_image_t *in, const JPEG2KTILEINFO *tileinfo, const UINT8 *tiledata, Imaging im) @@ -466,6 +516,8 @@ j2ku_sycca_rgba(opj_image_t *in, const JPEG2KTILEINFO *tileinfo, static const struct j2k_decode_unpacker j2k_unpackers[] = { { "L", OPJ_CLRSPC_GRAY, 1, j2ku_gray_l }, + { "I;16", OPJ_CLRSPC_GRAY, 1, j2ku_gray_i }, + { "I;16B", OPJ_CLRSPC_GRAY, 1, j2ku_gray_i }, { "LA", OPJ_CLRSPC_GRAY, 2, j2ku_graya_la }, { "RGB", OPJ_CLRSPC_GRAY, 1, j2ku_gray_rgb }, { "RGB", OPJ_CLRSPC_GRAY, 2, j2ku_gray_rgb }, @@ -517,7 +569,21 @@ j2k_decode_entry(Imaging im, ImagingCodecState state, opj_stream_set_read_function(stream, j2k_read); opj_stream_set_skip_function(stream, j2k_skip); + /* OpenJPEG 2.0 doesn't have OPJ_VERSION_MAJOR */ +#ifndef OPJ_VERSION_MAJOR opj_stream_set_user_data(stream, decoder); +#else + opj_stream_set_user_data(stream, decoder, NULL); + + /* Hack: if we don't know the length, the largest file we can + possibly support is 4GB. We can't go larger than this, because + OpenJPEG truncates this value for the final box in the file, and + the box lengths in OpenJPEG are currently 32 bit. */ + if (context->length < 0) + opj_stream_set_user_data_length(stream, 0xffffffff); + else + opj_stream_set_user_data_length(stream, context->length); +#endif /* Setup decompression context */ context->error_msg = NULL; diff --git a/libImaging/Jpeg2KEncode.c b/libImaging/Jpeg2KEncode.c index c1e16e97f..e8eef08c1 100644 --- a/libImaging/Jpeg2KEncode.c +++ b/libImaging/Jpeg2KEncode.c @@ -88,6 +88,22 @@ j2k_pack_l(Imaging im, UINT8 *buf, } } +static void +j2k_pack_i16(Imaging im, UINT8 *buf, + unsigned x0, unsigned y0, unsigned w, unsigned h) +{ + UINT8 *ptr = buf; + unsigned x,y; + for (y = 0; y < h; ++y) { + UINT8 *data = (UINT8 *)(im->image[y + y0] + x0); + for (x = 0; x < w; ++x) { + *ptr++ = *data++; + *ptr++ = *data++; + } + } +} + + static void j2k_pack_la(Imaging im, UINT8 *buf, unsigned x0, unsigned y0, unsigned w, unsigned h) @@ -247,6 +263,9 @@ j2k_encode_entry(Imaging im, ImagingCodecState state, j2k_pack_tile_t pack; int ret = -1; + unsigned prec = 8; + unsigned bpp = 8; + stream = opj_stream_default_create(OPJ_FALSE); if (!stream) { @@ -259,13 +278,30 @@ j2k_encode_entry(Imaging im, ImagingCodecState state, opj_stream_set_skip_function(stream, j2k_skip); opj_stream_set_seek_function(stream, j2k_seek); + /* OpenJPEG 2.0 doesn't have OPJ_VERSION_MAJOR */ +#ifndef OPJ_VERSION_MAJOR opj_stream_set_user_data(stream, encoder); +#else + opj_stream_set_user_data(stream, encoder, NULL); +#endif /* Setup an opj_image */ if (strcmp (im->mode, "L") == 0) { components = 1; color_space = OPJ_CLRSPC_GRAY; pack = j2k_pack_l; + } else if (strcmp (im->mode, "I;16") == 0){ + components = 1; + color_space = OPJ_CLRSPC_GRAY; + pack = j2k_pack_i16; + prec = 16; + bpp = 12; + } else if (strcmp (im->mode, "I;16B") == 0){ + components = 1; + color_space = OPJ_CLRSPC_GRAY; + pack = j2k_pack_i16; + prec = 16; + bpp = 12; } else if (strcmp (im->mode, "LA") == 0) { components = 2; color_space = OPJ_CLRSPC_GRAY; @@ -293,8 +329,8 @@ j2k_encode_entry(Imaging im, ImagingCodecState state, image_params[n].w = im->xsize; image_params[n].h = im->ysize; image_params[n].x0 = image_params[n].y0 = 0; - image_params[n].prec = 8; - image_params[n].bpp = 8; + image_params[n].prec = prec; + image_params[n].bpp = bpp; image_params[n].sgnd = 0; } @@ -437,7 +473,7 @@ j2k_encode_entry(Imaging im, ImagingCodecState state, num_tiles = tiles_x * tiles_y; - state->buffer = malloc (tile_width * tile_height * components); + state->buffer = malloc (tile_width * tile_height * components * prec / 8); tile_ndx = 0; for (y = 0; y < tiles_y; ++y) { @@ -469,7 +505,7 @@ j2k_encode_entry(Imaging im, ImagingCodecState state, pack(im, state->buffer, pixx, pixy, pixw, pixh); - data_size = pixw * pixh * components; + data_size = pixw * pixh * components * prec / 8; if (!opj_write_tile(codec, tile_ndx++, state->buffer, data_size, stream)) { diff --git a/libImaging/Pack.c b/libImaging/Pack.c index 1cc1f3a94..fecafbde4 100644 --- a/libImaging/Pack.c +++ b/libImaging/Pack.c @@ -566,6 +566,12 @@ static struct { {"LAB", "A", 8, band1}, {"LAB", "B", 8, band2}, + /* HSV */ + {"HSV", "HSV", 24, ImagingPackRGB}, + {"HSV", "H", 8, band0}, + {"HSV", "S", 8, band1}, + {"HSV", "V", 8, band2}, + /* integer */ {"I", "I", 32, copy4}, {"I", "I;16B", 16, packI16B}, diff --git a/libImaging/Storage.c b/libImaging/Storage.c index d31db5cb2..d65de1c0a 100644 --- a/libImaging/Storage.c +++ b/libImaging/Storage.c @@ -186,6 +186,13 @@ ImagingNewPrologueSubtype(const char *mode, unsigned xsize, unsigned ysize, im->pixelsize = 4; im->linesize = xsize * 4; + } else if (strcmp(mode, "HSV") == 0) { + /* 24-bit color, luminance, + 2 color channels */ + /* L is uint8, a,b are int8 */ + im->bands = 3; + im->pixelsize = 4; + im->linesize = xsize * 4; + } else { free(im); return (Imaging) ImagingError_ValueError("unrecognized mode"); @@ -379,7 +386,7 @@ ImagingNew(const char* mode, int xsize, int ysize) } else bytes = strlen(mode); /* close enough */ - if ((Py_ssize_t) xsize * ysize * bytes <= THRESHOLD) { + if ((int64_t) xsize * (int64_t) ysize * bytes <= THRESHOLD) { im = ImagingNewBlock(mode, xsize, ysize); if (im) return im; diff --git a/libImaging/TiffDecode.h b/libImaging/TiffDecode.h index 2dd557d41..8f41e7cb4 100644 --- a/libImaging/TiffDecode.h +++ b/libImaging/TiffDecode.h @@ -44,11 +44,10 @@ extern int ImagingLibTiffEncodeInit(ImagingCodecState state, char *filename, int extern int ImagingLibTiffSetField(ImagingCodecState state, ttag_t tag, ...); -#if defined(_MSC_VER) && (_MSC_VER == 1310) -/* VS2003/py2.4 can't use varargs. Skipping trace for now.*/ -#define TRACE(args) -#else - +/* + Trace debugging + legacy, don't enable for python 3.x, unicode issues. +*/ /* #define VA_ARGS(...) __VA_ARGS__ @@ -57,8 +56,5 @@ extern int ImagingLibTiffSetField(ImagingCodecState state, ttag_t tag, ...); #define TRACE(args) -#endif /* _MSC_VER */ - - #endif diff --git a/libImaging/Unpack.c b/libImaging/Unpack.c index 70b11b1b0..7c453dbfd 100644 --- a/libImaging/Unpack.c +++ b/libImaging/Unpack.c @@ -863,13 +863,6 @@ copy2(UINT8* out, const UINT8* in, int pixels) memcpy(out, in, pixels*2); } -static void -copy3(UINT8* out, const UINT8* in, int pixels) -{ - /* LAB triples, 24bit */ - memcpy(out, in, 3 * pixels); -} - static void copy4(UINT8* out, const UINT8* in, int pixels) { @@ -1166,6 +1159,12 @@ static struct { {"LAB", "A", 8, band1}, {"LAB", "B", 8, band2}, + /* HSV Color */ + {"HSV", "HSV", 24, ImagingUnpackRGB}, + {"HSV", "H", 8, band0}, + {"HSV", "S", 8, band1}, + {"HSV", "V", 8, band2}, + /* integer variations */ {"I", "I", 32, copy4}, {"I", "I;8", 8, unpackI8}, diff --git a/mp_compile.py b/mp_compile.py new file mode 100644 index 000000000..b7c3e7ce4 --- /dev/null +++ b/mp_compile.py @@ -0,0 +1,62 @@ +# A monkey patch of the base distutils.ccompiler to use parallel builds +# Tested on 2.7, looks to be identical to 3.3. + +from multiprocessing import Pool, cpu_count +from distutils.ccompiler import CCompiler +import os + +try: + MAX_PROCS = int(os.environ.get('MAX_CONCURRENCY', cpu_count())) +except: + MAX_PROCS = None + + +# hideous monkeypatching. but. but. but. +def _mp_compile_one(tp): + (self, obj, build, cc_args, extra_postargs, pp_opts) = tp + try: + src, ext = build[obj] + except KeyError: + return + self._compile(obj, src, ext, cc_args, extra_postargs, pp_opts) + return + + +def _mp_compile(self, sources, output_dir=None, macros=None, + include_dirs=None, debug=0, extra_preargs=None, + extra_postargs=None, depends=None): + """Compile one or more source files. + + see distutils.ccompiler.CCompiler.compile for comments. + """ + # A concrete compiler class can either override this method + # entirely or implement _compile(). + + macros, objects, extra_postargs, pp_opts, build = self._setup_compile( + output_dir, macros, include_dirs, sources, depends, extra_postargs) + cc_args = self._get_cc_args(pp_opts, debug, extra_preargs) + + pool = Pool(MAX_PROCS) + try: + print ("Building using %d processes" % pool._processes) + except: + pass + arr = [(self, obj, build, cc_args, extra_postargs, pp_opts) + for obj in objects] + pool.map_async(_mp_compile_one, arr) + pool.close() + pool.join() + # Return *all* object filenames, not just the ones we just built. + return objects + +# explicitly don't enable if environment says 1 processor +if MAX_PROCS != 1: + try: + # bug, only enable if we can make a Pool. see issue #790 and + # http://stackoverflow.com/questions/6033599/oserror-38-errno-38-with-multiprocessing + pool = Pool(2) + CCompiler.compile = _mp_compile + except Exception as msg: + print("Exception installing mp_compile, proceeding without: %s" %msg) +else: + print("Single threaded build, not installing mp_compile: %s processes" %MAX_PROCS) diff --git a/profile-installed.py b/profile-installed.py new file mode 100755 index 000000000..be9a960d2 --- /dev/null +++ b/profile-installed.py @@ -0,0 +1,26 @@ +#!/usr/bin/env python +import nose +import os +import sys +import glob + +import profile + +# monkey with the path, removing the local directory but adding the Tests/ +# directory for helper.py and the other local imports there. + +del(sys.path[0]) +sys.path.insert(0, os.path.abspath('./Tests')) + +# if there's no test selected (mostly) choose a working default. +# Something is required, because if we import the tests from the local +# directory, once again, we've got the non-installed PIL in the way +if len(sys.argv) == 1: + sys.argv.extend(glob.glob('Tests/test*.py')) + +# 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 new file mode 100644 index 000000000..86ddbd771 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +# Testing reqs +-e . +nose diff --git a/selftest.py b/selftest.py index 248cb3937..29af34ad2 100644 --- a/selftest.py +++ b/selftest.py @@ -49,13 +49,13 @@ def testimage(): Or open existing files: - >>> im = Image.open(os.path.join(ROOT, "Images/lena.gif")) + >>> im = Image.open(os.path.join(ROOT, "Tests/images/lena.gif")) >>> _info(im) ('GIF', 'P', (128, 128)) - >>> _info(Image.open(os.path.join(ROOT, "Images/lena.ppm"))) + >>> _info(Image.open(os.path.join(ROOT, "Tests/images/lena.ppm"))) ('PPM', 'RGB', (128, 128)) >>> try: - ... _info(Image.open(os.path.join(ROOT, "Images/lena.jpg"))) + ... _info(Image.open(os.path.join(ROOT, "Tests/images/lena.jpg"))) ... except IOError as v: ... print(v) ('JPEG', 'RGB', (128, 128)) @@ -63,7 +63,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, "Images/lena.ppm")) + >>> im = Image.open(os.path.join(ROOT, "Tests/images/lena.ppm")) >>> print(im.im) # internal image attribute None >>> a = im.load() @@ -73,7 +73,7 @@ def testimage(): You can apply many different operations on images. Most operations return a new image: - >>> im = Image.open(os.path.join(ROOT, "Images/lena.ppm")) + >>> im = Image.open(os.path.join(ROOT, "Tests/images/lena.ppm")) >>> _info(im.convert("L")) (None, 'L', (128, 128)) >>> _info(im.copy()) diff --git a/setup.py b/setup.py index 50ca985e3..5cf0e5e65 100644 --- a/setup.py +++ b/setup.py @@ -18,6 +18,9 @@ from distutils.command.build_ext import build_ext 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. +import mp_compile _IMAGING = ( "decode", "encode", "map", "display", "outline", "path") @@ -87,7 +90,7 @@ except (ImportError, OSError): NAME = 'Pillow' -VERSION = '2.4.0' +PILLOW_VERSION = '2.5.3' TCL_ROOT = None JPEG_ROOT = None JPEG2K_ROOT = None @@ -106,6 +109,7 @@ class pil_build_ext(build_ext): def require(self, feat): return feat in self.required + def want(self, feat): return getattr(self, feat) is None @@ -137,8 +141,8 @@ class pil_build_ext(build_ext): setattr(self.feature, x, False) if getattr(self, 'enable_%s' % x): raise ValueError( - 'Conflicting options: --enable-%s and --disable-%s' - % (x, x)) + 'Conflicting options: --enable-%s and --disable-%s' + % (x, x)) if getattr(self, 'enable_%s' % x): self.feature.required.append(x) @@ -209,7 +213,9 @@ class pil_build_ext(build_ext): # if Homebrew is installed, use its lib and include directories import subprocess try: - prefix = subprocess.check_output(['brew', '--prefix']).strip() + prefix = subprocess.check_output( + ['brew', '--prefix'] + ).strip().decode('latin1') except: # Homebrew not installed prefix = None @@ -225,16 +231,18 @@ class pil_build_ext(build_ext): if ft_prefix and os.path.isdir(ft_prefix): # freetype might not be linked into Homebrew's prefix _add_directory(library_dirs, os.path.join(ft_prefix, 'lib')) - _add_directory(include_dirs, os.path.join(ft_prefix, 'include')) + _add_directory( + include_dirs, os.path.join(ft_prefix, 'include')) else: - # fall back to freetype from XQuartz if Homebrew's freetype is missing + # fall back to freetype from XQuartz if + # Homebrew's freetype is missing _add_directory(library_dirs, "/usr/X11/lib") _add_directory(include_dirs, "/usr/X11/include") elif sys.platform.startswith("linux"): arch_tp = (plat.processor(), plat.architecture()[0]) - if arch_tp == ("x86_64","32bit"): - # 32 bit build on 64 bit machine. + if arch_tp == ("x86_64", "32bit"): + # 32 bit build on 64 bit machine. _add_directory(library_dirs, "/usr/lib/i386-linux-gnu") else: for platform_ in arch_tp: @@ -245,30 +253,38 @@ class pil_build_ext(build_ext): if platform_ in ["x86_64", "64bit"]: _add_directory(library_dirs, "/lib64") _add_directory(library_dirs, "/usr/lib64") - _add_directory(library_dirs, "/usr/lib/x86_64-linux-gnu") + _add_directory( + library_dirs, "/usr/lib/x86_64-linux-gnu") break elif platform_ in ["i386", "i686", "32bit"]: - _add_directory(library_dirs, "/usr/lib/i386-linux-gnu") + _add_directory( + library_dirs, "/usr/lib/i386-linux-gnu") break elif platform_ in ["aarch64"]: _add_directory(library_dirs, "/usr/lib64") - _add_directory(library_dirs, "/usr/lib/aarch64-linux-gnu") + _add_directory( + library_dirs, "/usr/lib/aarch64-linux-gnu") break elif platform_ in ["arm", "armv7l"]: - _add_directory(library_dirs, "/usr/lib/arm-linux-gnueabi") + _add_directory( + library_dirs, "/usr/lib/arm-linux-gnueabi") break elif platform_ in ["ppc64"]: _add_directory(library_dirs, "/usr/lib64") - _add_directory(library_dirs, "/usr/lib/ppc64-linux-gnu") - _add_directory(library_dirs, "/usr/lib/powerpc64-linux-gnu") + _add_directory( + library_dirs, "/usr/lib/ppc64-linux-gnu") + _add_directory( + library_dirs, "/usr/lib/powerpc64-linux-gnu") break elif platform_ in ["ppc"]: _add_directory(library_dirs, "/usr/lib/ppc-linux-gnu") - _add_directory(library_dirs, "/usr/lib/powerpc-linux-gnu") + _add_directory( + library_dirs, "/usr/lib/powerpc-linux-gnu") break elif platform_ in ["s390x"]: _add_directory(library_dirs, "/usr/lib64") - _add_directory(library_dirs, "/usr/lib/s390x-linux-gnu") + _add_directory( + library_dirs, "/usr/lib/s390x-linux-gnu") break elif platform_ in ["s390"]: _add_directory(library_dirs, "/usr/lib/s390-linux-gnu") @@ -337,14 +353,24 @@ class pil_build_ext(build_ext): _add_directory(include_dirs, "/usr/include") # on Windows, look for the OpenJPEG libraries in the location that - # the official installed puts them + # the official installer puts them if sys.platform == "win32": - _add_directory(library_dirs, - os.path.join(os.environ.get("ProgramFiles", ""), - "OpenJPEG 2.0", "lib")) - _add_directory(include_dirs, - os.path.join(os.environ.get("ProgramFiles", ""), - "OpenJPEG 2.0", "include")) + program_files = os.environ.get('ProgramFiles', '') + best_version = (0, 0) + best_path = None + for name in os.listdir(program_files): + if name.startswith('OpenJPEG '): + version = tuple( + [int(x) for x in name[9:].strip().split('.')]) + if version > best_version: + best_version = version + best_path = os.path.join(program_files, name) + + if best_path: + _add_directory(library_dirs, + os.path.join(best_path, 'lib')) + _add_directory(include_dirs, + os.path.join(best_path, 'include')) # # insert new dirs *before* default libs, to avoid conflicts @@ -362,7 +388,8 @@ class pil_build_ext(build_ext): if _find_include_file(self, "zlib.h"): if _find_library_file(self, "z"): feature.zlib = "z" - elif sys.platform == "win32" and _find_library_file(self, "zlib"): + elif (sys.platform == "win32" and + _find_library_file(self, "zlib")): feature.zlib = "zlib" # alternative name if feature.want('jpeg'): @@ -374,17 +401,43 @@ class pil_build_ext(build_ext): _find_library_file(self, "libjpeg")): feature.jpeg = "libjpeg" # alternative name + feature.openjpeg_version = None if feature.want('jpeg2000'): - if _find_include_file(self, "openjpeg-2.0/openjpeg.h"): - if _find_library_file(self, "openjp2"): - feature.jpeg2000 = "openjp2" - + best_version = None + best_path = None + + # Find the best version + for directory in self.compiler.include_dirs: + try: + listdir = os.listdir(directory) + except Exception: + # WindowsError, FileNotFoundError + continue + for name in listdir: + if name.startswith('openjpeg-') and \ + os.path.isfile(os.path.join(directory, name, + 'openjpeg.h')): + version = tuple([int(x) for x in name[9:].split('.')]) + if best_version is None or version > best_version: + best_version = version + best_path = os.path.join(directory, name) + + if best_version and _find_library_file(self, 'openjp2'): + # Add the directory to the include path so we can include + # rather than having to cope with the versioned + # include path + _add_directory(self.compiler.include_dirs, best_path, 0) + feature.jpeg2000 = 'openjp2' + feature.openjpeg_version = '.'.join( + [str(x) for x in best_version]) + if feature.want('tiff'): if _find_library_file(self, "tiff"): feature.tiff = "tiff" if sys.platform == "win32" and _find_library_file(self, "libtiff"): feature.tiff = "libtiff" - if sys.platform == "darwin" and _find_library_file(self, "libtiff"): + if (sys.platform == "darwin" and + _find_library_file(self, "libtiff")): feature.tiff = "libtiff" if feature.want('freetype'): @@ -431,20 +484,22 @@ class pil_build_ext(build_ext): if feature.want('webp'): if (_find_include_file(self, "webp/encode.h") and _find_include_file(self, "webp/decode.h")): - if _find_library_file(self, "webp"): # in googles precompiled zip it is call "libwebp" + # In Google's precompiled zip it is call "libwebp": + if _find_library_file(self, "webp"): feature.webp = "webp" if feature.want('webpmux'): if (_find_include_file(self, "webp/mux.h") and _find_include_file(self, "webp/demux.h")): - if _find_library_file(self, "webpmux") and _find_library_file(self, "webpdemux"): + if (_find_library_file(self, "webpmux") and + _find_library_file(self, "webpdemux")): feature.webpmux = "webpmux" for f in feature: if not getattr(feature, f) and feature.require(f): raise ValueError( - '--enable-%s requested but %s not found, aborting.' - % (f, f)) + '--enable-%s requested but %s not found, aborting.' + % (f, f)) # # core library @@ -499,7 +554,9 @@ class pil_build_ext(build_ext): if sys.platform == "win32": extra.extend(["user32", "gdi32"]) exts.append(Extension( - "PIL._imagingcms", ["_imagingcms.c"], libraries=["lcms2"] + extra)) + "PIL._imagingcms", + ["_imagingcms.c"], + libraries=["lcms2"] + extra)) if os.path.isfile("_webp.c") and feature.webp: libs = ["webp"] @@ -543,6 +600,9 @@ class pil_build_ext(build_ext): if os.path.isfile("_imagingmath.c"): exts.append(Extension("PIL._imagingmath", ["_imagingmath.c"])) + if os.path.isfile("_imagingmorph.c"): + exts.append(Extension("PIL._imagingmorph", ["_imagingmorph.c"])) + self.extensions[:] = exts build_ext.build_extensions(self) @@ -562,7 +622,7 @@ class pil_build_ext(build_ext): print("-" * 68) print("PIL SETUP SUMMARY") print("-" * 68) - print("version Pillow %s" % VERSION) + print("version Pillow %s" % PILLOW_VERSION) v = sys.version.split("[") print("platform %s %s" % (sys.platform, v[0].strip())) for v in v[1:]: @@ -572,7 +632,8 @@ class pil_build_ext(build_ext): options = [ (feature.tcl and feature.tk, "TKINTER"), (feature.jpeg, "JPEG"), - (feature.jpeg2000, "OPENJPEG (JPEG2000)"), + (feature.jpeg2000, "OPENJPEG (JPEG2000)", + feature.openjpeg_version), (feature.zlib, "ZLIB (PNG/ZIP)"), (feature.tiff, "LIBTIFF"), (feature.freetype, "FREETYPE2"), @@ -583,7 +644,10 @@ class pil_build_ext(build_ext): all = 1 for option in options: if option[0]: - print("--- %s support available" % option[1]) + version = '' + if len(option) >= 3 and option[2]: + version = ' (%s)' % option[2] + print("--- %s support available%s" % (option[1], version)) else: print("*** %s support not available" % option[1]) if option[1] == "TKINTER" and _tkinter: @@ -642,6 +706,7 @@ class pil_build_ext(build_ext): if ret >> 8 == 0: fp = open(tmpfile, 'r') multiarch_path_component = fp.readline().strip() + fp.close() _add_directory( self.compiler.library_dirs, '/usr/lib/' + multiarch_path_component) @@ -651,16 +716,15 @@ class pil_build_ext(build_ext): finally: os.unlink(tmpfile) + setup( name=NAME, - version=VERSION, + version=PILLOW_VERSION, description='Python Imaging Library (Fork)', - long_description=( - _read('README.rst') + b'\n' + - _read('CHANGES.rst')).decode('utf-8'), + long_description=_read('README.rst').decode('utf-8'), author='Alex Clark (fork author)', author_email='aclark@aclark.net', - url='http://python-imaging.github.io/', + url='http://python-pillow.github.io/', classifiers=[ "Development Status :: 6 - Mature", "Topic :: Multimedia :: Graphics", @@ -674,15 +738,17 @@ setup( "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.2", - "Programming Language :: Python :: 3.3", ], + "Programming Language :: Python :: 3.3", + "Programming Language :: Python :: 3.4", + ], cmdclass={"build_ext": pil_build_ext}, ext_modules=[Extension("PIL._imaging", ["_imaging.c"])], include_package_data=True, packages=find_packages(), scripts=glob.glob("Scripts/pil*.py"), test_suite='PIL.tests', - keywords=["Imaging",], + keywords=["Imaging", ], license='Standard PIL License', zip_safe=True, - ) - +) +# End of file diff --git a/test-installed.py b/test-installed.py new file mode 100755 index 000000000..0fed377b8 --- /dev/null +++ b/test-installed.py @@ -0,0 +1,32 @@ +#!/usr/bin/env python +import nose +import os +import sys +import glob + +# monkey with the path, removing the local directory but adding the Tests/ +# directory for helper.py and the other local imports there. + +del(sys.path[0]) +sys.path.insert(0, os.path.abspath('./Tests')) + +# if there's no test selected (mostly) choose a working default. +# Something is required, because if we import the tests from the local +# directory, once again, we've got the non-installed PIL in the way +if len(sys.argv) == 1: + sys.argv.extend(glob.glob('Tests/test*.py')) + +# 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 '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') + +if __name__ == '__main__': + nose.main()