diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 6c6e9b612..eef2a30bd 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -4,7 +4,7 @@ Bug fixes, feature additions, tests, documentation and more can be contributed v ## Bug fixes, feature additions, etc. -Please send a pull request to the master branch. Please include [documentation](https://pillow.readthedocs.io) and [tests](../Tests/README.rst) for new features. Tests or documentation without bug fixes or feature additions are welcome too. Feel free to ask questions [via issues](https://github.com/python-pillow/Pillow/issues/new) or irc://irc.freenode.net#pil +Please send a pull request to the master branch. Please include [documentation](https://pillow.readthedocs.io) and [tests](../Tests/README.rst) for new features. Tests or documentation without bug fixes or feature additions are welcome too. Feel free to ask questions [via issues](https://github.com/python-pillow/Pillow/issues/new), [Gitter](https://gitter.im/python-pillow/Pillow) or irc://irc.freenode.net#pil - Fork the Pillow repository. - Create a branch from master. @@ -21,7 +21,9 @@ Please send a pull request to the master branch. Please include [documentation]( ## Reporting Issues -When reporting issues, please include code that reproduces the issue and whenever possible, an image that demonstrates the issue. The best reproductions are self-contained scripts with minimal dependencies. +When reporting issues, please include code that reproduces the issue and whenever possible, an image that demonstrates the issue. Please upload images to GitHub, not to third-party file hosting sites. If necessary, add the image to a zip or tar archive. + +The best reproductions are self-contained scripts with minimal dependencies. If you are using a framework such as plone, Django, or buildout, try to replicate the issue just using Pillow. ### Provide details diff --git a/.travis.yml b/.travis.yml index cc06084a5..23225dbbb 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,3 +1,5 @@ +dist: xenial +sudo: required language: python cache: pip @@ -12,13 +14,24 @@ matrix: fast_finish: true include: - python: "pypy" + dist: trusty - python: "pypy3" - - python: '3.6' + dist: trusty + - python: '3.7' - python: '2.7' + - python: '2.7' + dist: trusty - python: "2.7_with_system_site_packages" # For PyQt4 + - python: "2.7_with_system_site_packages" # For PyQt4 + dist: trusty + - python: '3.6' + - python: '3.6' + dist: trusty - python: '3.5' + - python: '3.5' + dist: trusty - python: '3.4' - - python: '3.7-dev' + dist: trusty - env: DOCKER="alpine" DOCKER_TAG="pytest" - env: DOCKER="arch" DOCKER_TAG="pytest" # contains PyQt5 - env: DOCKER="ubuntu-trusty-x86" DOCKER_TAG="pytest" @@ -31,10 +44,6 @@ matrix: - env: DOCKER="fedora-26-amd64" DOCKER_TAG="pytest" - env: DOCKER="fedora-27-amd64" DOCKER_TAG="pytest" -dist: trusty - -sudo: required - services: - docker @@ -42,7 +51,7 @@ install: - if [ "$DOCKER" == "" ]; then .travis/install.sh; fi before_install: - - if [ "$DOCKER" ]; then docker pull pythonpillow/$DOCKER:$DOCKER_TAG; fi + - if [ "$DOCKER" ]; then travis_retry docker pull pythonpillow/$DOCKER:$DOCKER_TAG; fi before_script: # Qt needs a display for some of the tests, and it's only run on the system site packages install @@ -61,29 +70,3 @@ script: after_success: - .travis/after_success.sh - -after_failure: - - | - if [ "$TRAVIS_REPO_SLUG" = "python-pillow/Pillow" ] && [ "$TRAVIS_BRANCH" = "master" ] && [ "$TRAVIS_PULL_REQUEST" = "false" ]; then - curl -Lo travis_after_all.py https://raw.github.com/dmakhno/travis_after_all/master/travis_after_all.py - python travis_after_all.py - export $(cat .to_export_back) - if [ "$BUILD_LEADER" = "YES" ]; then - if [ "$BUILD_AGGREGATE_STATUS" = "others_failed" ]; then - echo "All jobs failed" - else - echo "Some jobs failed" - fi - fi - fi - -after_script: - - | - if [ "$TRAVIS_REPO_SLUG" = "python-pillow/Pillow" ] && [ "$TRAVIS_BRANCH" = "master" ] && [ "$TRAVIS_PULL_REQUEST" = "false" ]; then - echo leader=$BUILD_LEADER status=$BUILD_AGGREGATE_STATUS - fi - -env: - global: - # travis encrypt AUTH_TOKEN= - secure: "Vzm7aG1Qv0SDQcqiPzZMedNLn5ZmpL7IzF0DYnqcD+/l+zmKU22SnJBcX0uVXumo+r7eZfpsShpqfcdsZvMlvmQnwz+Y6AGKQru9tCKZbTMnuRjWKKXekC+tr8Xt9CKvRVtte5PyXW31paxUI3/e+fQGBwoFjEEC+6EpEOjeRfE=" diff --git a/.travis/after_success.sh b/.travis/after_success.sh index a18c095c9..c215f4219 100755 --- a/.travis/after_success.sh +++ b/.travis/after_success.sh @@ -30,20 +30,3 @@ if [ "$TRAVIS_PYTHON_VERSION" == "2.7" ] && [ "$DOCKER" == "" ]; then depends/diffcover-install.sh depends/diffcover-run.sh fi - -# after_all - -if [ "$TRAVIS_REPO_SLUG" = "python-pillow/Pillow" ] && [ "$TRAVIS_BRANCH" = "master" ] && [ "$TRAVIS_PULL_REQUEST" = "false" ]; then - curl -Lo travis_after_all.py https://raw.github.com/dmakhno/travis_after_all/master/travis_after_all.py - python travis_after_all.py - export $(cat .to_export_back) - if [ "$BUILD_LEADER" = "YES" ]; then - if [ "$BUILD_AGGREGATE_STATUS" = "others_succeeded" ]; then - echo "All jobs succeeded! Triggering macOS build..." - # Trigger a macOS build at the pillow-wheels repo - ./build_children.sh - else - echo "Some jobs failed" - fi - fi -fi diff --git a/.travis/install.sh b/.travis/install.sh index cad0e3c32..f18aff079 100755 --- a/.travis/install.sh +++ b/.travis/install.sh @@ -15,6 +15,7 @@ pip install -U pytest pip install -U pytest-cov pip install pyroma pip install test-image-results +pip install numpy # docs only on Python 2.7 if [ "$TRAVIS_PYTHON_VERSION" == "2.7" ]; then pip install -r requirements.txt ; fi diff --git a/CHANGES.rst b/CHANGES.rst index b7d5486b3..972092010 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -2,9 +2,159 @@ Changelog (Pillow) ================== -5.1.0 (unreleased) +5.3.0 (unreleased) ------------------ +- Depends: Update libimagequant to 2.12.1 #3281 + [radarhere] + +- Add three-color support to ImageOps.colorize #3242 + [tsennott] + +- Tests: Add LA to TGA test modes #3222 + [danpla] + +- Skip outline if the draw operation fills with the same colour #2922 + [radarhere] + +- Flake8 fixes #3173 + [radarhere] + +- Avoid deprecated 'U' mode when opening files #2187 + [jdufresne] + +5.2.0 (2018-07-01) +------------------ + +- Fixed saving a multiframe image as a single frame PDF #3137 + [radarhere] + +- If a Qt version is already imported, attempt to use it first #3143 + [radarhere] + +- Fix transform fill color for alpha images #3147 + [fozcode] + +- TGA: Add support for writing RLE data #3186 + [danpla] + +- TGA: Read and write LA data #3178 + [danpla] + +- QuantOctree.c: Remove erroneous attempt to average over an empty range #3196 + [tkoeppe] + +- Changed ICNS format tests to pass on OS X 10.11 #3202 + [radarhere] + +- Fixed bug in ImageDraw.multiline_textsize() #3114 + [tianyu139] + +- Added getsize_multiline support for PIL.ImageFont #3113 + [tianyu139] + +- Added ImageFile get_format_mimetype method #3190 + [radarhere] + +- Changed mmap file pointer to use context manager #3216 + [radarhere] + +- Changed ellipse point calculations to be more evenly distributed #3142 + [radarhere] + +- Only extract first Exif segment #2946 + [hugovk] + +- Tests: Test ImageDraw2, WalImageFile #3135, #2989 + [hugovk] + +- Remove unnecessary '#if 0' code #3075 + [hugovk] + +- Tests: Added GD tests #1817 + [radarhere] + +- Fix collections ABCs DeprecationWarning in Python 3.7 #3123 + [hugovk] + +- unpack_from is faster than unpack of slice #3201 + [landfillbaby] + +- Docs: Add coordinate system links and file handling links in documentation #3204, #3214 + [radarhere] + +- Tests: TestFilePng: Fix test_save_l_transparency() #3182 + [danpla] + +- Docs: Correct argument name #3171 + [radarhere] + +- Docs: Update CMake download URL #3166 + [radarhere] + +- Docs: Improve Image.transform documentation #3164 + [radarhere] + +- Fix transform fillcolor argument when image mode is RGBA or LA #3163 + [radarhere] + +- Tests: More specific Exception testing #3158 + [radarhere] + +- Add getrgb HSB/HSV color strings #3148 + [radarhere] + +- Allow float values in getrgb HSL color string #3146 + [radarhere] + +- AppVeyor: Upgrade to Python 2.7.15 and 3.4.4 #3140 + [radarhere] + +- AppVeyor: Upgrade to PyPy 6.0.0 #3133 + [hugovk] + +- Deprecate PILLOW_VERSION and VERSION #3090 + [hugovk] + +- Support Python 3.7 #3076 + [hugovk] + +- Depends: Update freetype to 2.9.1, libjpeg to 9c, libwebp to 1.0.0 #3121, #3136, #3108 + [radarhere] + +- Build macOS wheels with Xcode 6.4, supporting older macOS versions #3068 + [wiredfool] + +- Fix _i2f compilation on some GCC versions #3067 + [homm] + +- Changed encoderinfo to have priority over info when saving GIF images #3086 + [radarhere] + +- Rename PIL.version to PIL._version and remove it from module #3083 + [homm] + +- Enable background colour parameter on rotate #3057 + [storesource] + +- Remove unnecessary `#if 1` directive #3072 + [jdufresne] + +- Remove unused Python class, Path #3070 + [jdufresne] + +- Fix dereferencing type-punned pointer will break strict-aliasing #3069 + [jdufresne] + +5.1.0 (2018-04-02) +------------------ + +- Close fp before return in ImagingSavePPM #3061 + [kathryndavies] + +- Added documentation for ICNS append_images #3051 + [radarhere] + - Docs: Move intro text below its header #3021 [hugovk] @@ -13,12 +163,12 @@ Changelog (Pillow) - Fix TypeError for JPEG2000 parser feed #3042 [hugovk] - + - Certain corrupted jpegs can result in no data read #3023 [kkopachev] - + - Add support for BLP file format #3007 - [jleclanche] + [jleclanche] - Simplify version checks #2998 [hugovk] @@ -83,10 +233,6 @@ Changelog (Pillow) - Docs: Changed documentation references to 2.x to 2.7 #2921 [radarhere] - -5.0.1 (unreleased) ------------------- - - Fix memory leak when opening webp files #2974 [wiredfool] @@ -177,7 +323,7 @@ Changelog (Pillow) - Add eog support for Ubuntu Image Viewer #2864 [NafisFaysal] -- Test: Test on 3.7-dev on Travis.ci #2870 +- Test: Test on 3.7-dev on Travis CI #2870 [hugovk] - Dependencies: Update libtiff to 4.0.9 #2871 @@ -306,7 +452,7 @@ Changelog (Pillow) - Fixed doc syntax in ImageDraw #2752 [radarhere] -- Fixed support for building on Windows/msys2. Added Appveyor CI coverage for python3 on msys2 #2476 +- Fixed support for building on Windows/msys2. Added Appveyor CI coverage for python3 on msys2 #2746 [wiredfool] - Fix ValueError in Exif/Tiff IFD #2719 @@ -378,7 +524,7 @@ Changelog (Pillow) - Use RGBX rawmode for RGB JPEG images where possible #1989 [homm] -- Remove palettes from non-palette modes in _new #2702 +- Remove palettes from non-palette modes in _new #2704 [wiredfool] - Delete transparency info when convert'ing RGB/L to RGBA #2633 @@ -498,7 +644,7 @@ Changelog (Pillow) - Doc: Clarified Image.save:append_images documentation #2604 [radarhere] -- CI: Amazon Linux and Centos6 docker images added to TravisCI #2585 +- CI: Amazon Linux and Centos6 docker images added to Travis CI #2585 [wiredfool] - Image.alpha_composite added #2595 @@ -567,7 +713,7 @@ Changelog (Pillow) - Update Feature Detection #2520 [wiredfool] -- CI: Update pypy on TravisCI #2573 +- CI: Update pypy on Travis CI #2573 [hugovk] - ImageMorph: Fix wrong expected size of MRLs read from disk #2561 @@ -801,10 +947,10 @@ Changelog (Pillow) - Add center and translate option to Image.rotate. #2328 [lambdafu] -- Test: Relax WMF test condition, fixes #2323 +- Test: Relax WMF test condition, fixes #2323. #2327 [wiredfool] -- Allow 0 size images, Fixes #2259, Reverts to pre-3.4 behavior. +- Allow 0 size images, Fixes #2259, Reverts to pre-3.4 behavior. #2262 [wiredfool] - SGI: Save uncompressed SGI/BW/RGB/RGBA files #2325 @@ -1135,10 +1281,10 @@ Changelog (Pillow) 3.3.2 (2016-10-03) ------------------ -- Fix negative image sizes in Storage.c #2105 +- Fix negative image sizes in Storage.c #2146 [wiredfool] -- Fix integer overflow in map.c #2105 +- Fix integer overflow in map.c #2146 [wiredfool] 3.3.1 (2016-08-18) @@ -1276,7 +1422,7 @@ Changelog (Pillow) - Skip tests that require libtiff if it is not installed #1893 (fixes #1866) [wiredfool] -- Skip test when icc profile is not available, fixes #1887 +- Skip test when icc profile is not available, fixes #1887. #1892 [doko42] - Make deprecated functions raise NotImplementedError instead of Exception. #1862, #1890 @@ -1881,7 +2027,7 @@ Changelog (Pillow) 2.8.1 (2015-04-02) ------------------ -- Bug fix: Catch struct.error on invalid JPEG, fixes #1163 +- Bug fix: Catch struct.error on invalid JPEG, fixes #1163. #1165 [wiredfool, hugovk] 2.8.0 (2015-04-01) @@ -2016,7 +2162,7 @@ Changelog (Pillow) - Updated manifest #957 [wiredfool] -- Fix PyPy 2.4 regression #952 +- Fix PyPy 2.4 regression #958 [wiredfool] - Webp Metadata Skip Test comments #954 @@ -2058,7 +2204,7 @@ Changelog (Pillow) - Use redistributable ICC profiles for testing, skip if not available #923 [wiredfool] -- Additional documentation for JPEG info and save options #890 +- Additional documentation for JPEG info and save options #922 [wiredfool] - Fix JPEG Encoding memory leak when exif or qtables were specified #921 diff --git a/MANIFEST.in b/MANIFEST.in index cd65f46fc..40b2ef5d7 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -24,7 +24,6 @@ exclude .editorconfig exclude .landscape.yaml exclude .travis exclude .travis/* -exclude build_children.sh exclude tox.ini global-exclude .git* global-exclude *.pyc diff --git a/Makefile b/Makefile index 1e888ee35..1803e617d 100644 --- a/Makefile +++ b/Makefile @@ -91,7 +91,7 @@ sdist: test: pytest -qq -# https://docs.python.org/2/distutils/packageindex.html#the-pypirc-file +# https://docs.python.org/3/distutils/packageindex.html#the-pypirc-file upload-test: # [test] # username: diff --git a/README.rst b/README.rst index d48e88bf7..b88a103b0 100644 --- a/README.rst +++ b/README.rst @@ -14,7 +14,7 @@ Pillow is the friendly PIL fork by `Alex Clark and Contributors 2.9.0" Pillow + $ git commit -m "Pillow -> 5.2.0" Pillow $ git push ``` * [ ] Download distributions from the [Pillow Wheel Builder container](http://a365fff413fe338398b6-1c8a9b3114517dc5fe17b7c3f8c63a43.r19.cf2.rackcdn.com/). @@ -99,8 +102,8 @@ Released as needed privately to individual vendors for critical security-related ## Publicize Release -* [ ] Announce release availability via [Twitter](https://twitter.com/pythonpillow) e.g. https://twitter.com/aclark4life/status/583366798302691328. +* [ ] Announce release availability via [Twitter](https://twitter.com/pythonpillow) e.g. https://twitter.com/PythonPillow/status/1013789184354603010 ## Documentation -* [ ] Make sure the default version for Read the Docs is the latest release version, e.g. ``3.1.x`` rather than ``latest``: https://readthedocs.org/projects/pillow/versions/ +* [ ] Make sure the default version for Read the Docs is the latest release version, i.e. ``5.2.0`` rather than ``latest`` e.g. https://pillow.readthedocs.io/en/5.2.x/ diff --git a/Tests/check_icns_dos.py b/Tests/check_icns_dos.py index e56709bbb..d4c3cf7cb 100644 --- a/Tests/check_icns_dos.py +++ b/Tests/check_icns_dos.py @@ -2,10 +2,11 @@ # Run from anywhere that PIL is importable. from PIL import Image +from PIL._util import py3 from io import BytesIO -if bytes is str: - Image.open(BytesIO(bytes('icns\x00\x00\x00\x10hang\x00\x00\x00\x00'))) -else: +if py3: Image.open(BytesIO(bytes('icns\x00\x00\x00\x10hang\x00\x00\x00\x00', 'latin-1'))) +else: + Image.open(BytesIO(bytes('icns\x00\x00\x00\x10hang\x00\x00\x00\x00'))) diff --git a/Tests/check_j2k_dos.py b/Tests/check_j2k_dos.py index 9f06888a3..4ea31cec2 100644 --- a/Tests/check_j2k_dos.py +++ b/Tests/check_j2k_dos.py @@ -2,12 +2,14 @@ # Run from anywhere that PIL is importable. from PIL import Image +from PIL._util import py3 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: +if py3: Image.open(BytesIO(bytes( '\x00\x00\x00\x0cjP\x20\x20\x0d\x0a\x87\x0a\x00\x00\x00\x00hang', 'latin-1'))) + +else: + Image.open(BytesIO(bytes( + '\x00\x00\x00\x0cjP\x20\x20\x0d\x0a\x87\x0a\x00\x00\x00\x00hang'))) diff --git a/Tests/helper.py b/Tests/helper.py index f04dffbc5..b6ef6dc13 100644 --- a/Tests/helper.py +++ b/Tests/helper.py @@ -8,6 +8,7 @@ import os import unittest from PIL import Image, ImageMath +from PIL._util import py3 import logging logger = logging.getLogger(__name__) @@ -152,7 +153,8 @@ class PillowTestCase(unittest.TestCase): pass raise e - def assert_image_similar_tofile(self, a, filename, epsilon, msg=None, mode=None): + def assert_image_similar_tofile(self, a, filename, epsilon, msg=None, + mode=None): with Image.open(filename) as img: if mode: img = img.convert(mode) @@ -190,6 +192,16 @@ class PillowTestCase(unittest.TestCase): def assert_not_all_same(self, items, msg=None): self.assertFalse(items.count(items[0]) == len(items), msg) + def assert_tuple_approx_equal(self, actuals, targets, threshold, msg): + """Tests if actuals has values within threshold from targets""" + + value = True + for i, target in enumerate(targets): + value *= (target - threshold <= actuals[i] <= target + threshold) + + self.assertTrue(value, + msg + ': ' + repr(actuals) + ' != ' + repr(targets)) + def skipKnownBadTest(self, msg=None, platform=None, travis=None, interpreter=None): # Skip if platform/travis matches, and @@ -229,23 +241,24 @@ class PillowTestCase(unittest.TestCase): @unittest.skipIf(sys.platform.startswith('win32'), "requires Unix or macOS") class PillowLeakTestCase(PillowTestCase): - # requires unix/osx + # requires unix/macOS iterations = 100 # count mem_limit = 512 # k def _get_mem_usage(self): """ Gets the RUSAGE memory usage, returns in K. Encapsulates the difference - between OSX and Linux rss reporting + between macOS and Linux rss reporting - :returns; memory usage in kilobytes + :returns: memory usage in kilobytes """ from resource import getrusage, RUSAGE_SELF mem = getrusage(RUSAGE_SELF).ru_maxrss if sys.platform == 'darwin': # man 2 getrusage: - # ru_maxrss the maximum resident set size utilized (in bytes). + # ru_maxrss + # This is the maximum resident set size utilized (in bytes). return mem / 1024 # Kb else: # linux @@ -265,7 +278,10 @@ class PillowLeakTestCase(PillowTestCase): # helpers -py3 = sys.version_info.major >= 3 +if not py3: + # Remove DeprecationWarning in Python 3 + PillowTestCase.assertRaisesRegex = PillowTestCase.assertRaisesRegexp + PillowTestCase.assertRegex = PillowTestCase.assertRegexpMatches def fromstring(data): diff --git a/Tests/images/bw_gradient.png b/Tests/images/bw_gradient.png new file mode 100644 index 000000000..79c921486 Binary files /dev/null and b/Tests/images/bw_gradient.png differ diff --git a/Tests/images/hopper.gd b/Tests/images/hopper.gd new file mode 100644 index 000000000..82d2408f9 Binary files /dev/null and b/Tests/images/hopper.gd differ diff --git a/Tests/images/hopper.wal b/Tests/images/hopper.wal new file mode 100644 index 000000000..f6260c6b3 Binary files /dev/null and b/Tests/images/hopper.wal differ diff --git a/Tests/images/imagedraw2_text.png b/Tests/images/imagedraw2_text.png new file mode 100644 index 000000000..b22e6545b Binary files /dev/null and b/Tests/images/imagedraw2_text.png differ diff --git a/Tests/images/imagedraw_outline_chord_L.png b/Tests/images/imagedraw_outline_chord_L.png new file mode 100644 index 000000000..9c20ad217 Binary files /dev/null and b/Tests/images/imagedraw_outline_chord_L.png differ diff --git a/Tests/images/imagedraw_outline_chord_RGB.png b/Tests/images/imagedraw_outline_chord_RGB.png new file mode 100644 index 000000000..9e9cb5af0 Binary files /dev/null and b/Tests/images/imagedraw_outline_chord_RGB.png differ diff --git a/Tests/images/imagedraw_outline_ellipse_L.png b/Tests/images/imagedraw_outline_ellipse_L.png new file mode 100644 index 000000000..53b76b62b Binary files /dev/null and b/Tests/images/imagedraw_outline_ellipse_L.png differ diff --git a/Tests/images/imagedraw_outline_ellipse_RGB.png b/Tests/images/imagedraw_outline_ellipse_RGB.png new file mode 100644 index 000000000..37a519327 Binary files /dev/null and b/Tests/images/imagedraw_outline_ellipse_RGB.png differ diff --git a/Tests/images/imagedraw_outline_pieslice_L.png b/Tests/images/imagedraw_outline_pieslice_L.png new file mode 100644 index 000000000..92972d54c Binary files /dev/null and b/Tests/images/imagedraw_outline_pieslice_L.png differ diff --git a/Tests/images/imagedraw_outline_pieslice_RGB.png b/Tests/images/imagedraw_outline_pieslice_RGB.png new file mode 100644 index 000000000..4be4fc4af Binary files /dev/null and b/Tests/images/imagedraw_outline_pieslice_RGB.png differ diff --git a/Tests/images/imagedraw_outline_polygon_L.png b/Tests/images/imagedraw_outline_polygon_L.png new file mode 100644 index 000000000..57ed9d43b Binary files /dev/null and b/Tests/images/imagedraw_outline_polygon_L.png differ diff --git a/Tests/images/imagedraw_outline_polygon_RGB.png b/Tests/images/imagedraw_outline_polygon_RGB.png new file mode 100644 index 000000000..286b71c23 Binary files /dev/null and b/Tests/images/imagedraw_outline_polygon_RGB.png differ diff --git a/Tests/images/imagedraw_outline_rectangle_L.png b/Tests/images/imagedraw_outline_rectangle_L.png new file mode 100644 index 000000000..b9c47018f Binary files /dev/null and b/Tests/images/imagedraw_outline_rectangle_L.png differ diff --git a/Tests/images/imagedraw_outline_rectangle_RGB.png b/Tests/images/imagedraw_outline_rectangle_RGB.png new file mode 100644 index 000000000..41b92fb75 Binary files /dev/null and b/Tests/images/imagedraw_outline_rectangle_RGB.png differ diff --git a/Tests/images/imagedraw_outline_shape_L.png b/Tests/images/imagedraw_outline_shape_L.png new file mode 100644 index 000000000..20ebef157 Binary files /dev/null and b/Tests/images/imagedraw_outline_shape_L.png differ diff --git a/Tests/images/imagedraw_outline_shape_RGB.png b/Tests/images/imagedraw_outline_shape_RGB.png new file mode 100644 index 000000000..6fb6f623e Binary files /dev/null and b/Tests/images/imagedraw_outline_shape_RGB.png differ diff --git a/Tests/images/la.tga b/Tests/images/la.tga new file mode 100644 index 000000000..8c83104ed Binary files /dev/null and b/Tests/images/la.tga differ diff --git a/Tests/images/rotate_45_no_fill.png b/Tests/images/rotate_45_no_fill.png new file mode 100644 index 000000000..3c9d03e6c Binary files /dev/null and b/Tests/images/rotate_45_no_fill.png differ diff --git a/Tests/images/rotate_45_with_fill.png b/Tests/images/rotate_45_with_fill.png new file mode 100644 index 000000000..05b2d34d5 Binary files /dev/null and b/Tests/images/rotate_45_with_fill.png differ diff --git a/Tests/images/tga/common/1x1_l.png b/Tests/images/tga/common/1x1_l.png new file mode 100644 index 000000000..d1a2cb813 Binary files /dev/null and b/Tests/images/tga/common/1x1_l.png differ diff --git a/Tests/images/tga/common/1x1_l_bl_raw.tga b/Tests/images/tga/common/1x1_l_bl_raw.tga new file mode 100644 index 000000000..c79e125ea Binary files /dev/null and b/Tests/images/tga/common/1x1_l_bl_raw.tga differ diff --git a/Tests/images/tga/common/1x1_l_bl_rle.tga b/Tests/images/tga/common/1x1_l_bl_rle.tga new file mode 100644 index 000000000..ee1a7d2d8 Binary files /dev/null and b/Tests/images/tga/common/1x1_l_bl_rle.tga differ diff --git a/Tests/images/tga/common/1x1_l_tl_raw.tga b/Tests/images/tga/common/1x1_l_tl_raw.tga new file mode 100644 index 000000000..6c9968758 Binary files /dev/null and b/Tests/images/tga/common/1x1_l_tl_raw.tga differ diff --git a/Tests/images/tga/common/1x1_l_tl_rle.tga b/Tests/images/tga/common/1x1_l_tl_rle.tga new file mode 100644 index 000000000..efd4e3af4 Binary files /dev/null and b/Tests/images/tga/common/1x1_l_tl_rle.tga differ diff --git a/Tests/images/tga/common/200x32_l.png b/Tests/images/tga/common/200x32_l.png new file mode 100644 index 000000000..ff37cbe30 Binary files /dev/null and b/Tests/images/tga/common/200x32_l.png differ diff --git a/Tests/images/tga/common/200x32_l_bl_raw.tga b/Tests/images/tga/common/200x32_l_bl_raw.tga new file mode 100644 index 000000000..e629910a0 Binary files /dev/null and b/Tests/images/tga/common/200x32_l_bl_raw.tga differ diff --git a/Tests/images/tga/common/200x32_l_bl_rle.tga b/Tests/images/tga/common/200x32_l_bl_rle.tga new file mode 100644 index 000000000..2e6f9377b Binary files /dev/null and b/Tests/images/tga/common/200x32_l_bl_rle.tga differ diff --git a/Tests/images/tga/common/200x32_l_tl_raw.tga b/Tests/images/tga/common/200x32_l_tl_raw.tga new file mode 100644 index 000000000..f9ed8b9c2 Binary files /dev/null and b/Tests/images/tga/common/200x32_l_tl_raw.tga differ diff --git a/Tests/images/tga/common/200x32_l_tl_rle.tga b/Tests/images/tga/common/200x32_l_tl_rle.tga new file mode 100644 index 000000000..03c797e53 Binary files /dev/null and b/Tests/images/tga/common/200x32_l_tl_rle.tga differ diff --git a/Tests/images/tga/common/200x32_la.png b/Tests/images/tga/common/200x32_la.png new file mode 100644 index 000000000..a8c4f274f Binary files /dev/null and b/Tests/images/tga/common/200x32_la.png differ diff --git a/Tests/images/tga/common/200x32_la_bl_raw.tga b/Tests/images/tga/common/200x32_la_bl_raw.tga new file mode 100644 index 000000000..afdc97151 Binary files /dev/null and b/Tests/images/tga/common/200x32_la_bl_raw.tga differ diff --git a/Tests/images/tga/common/200x32_la_bl_rle.tga b/Tests/images/tga/common/200x32_la_bl_rle.tga new file mode 100644 index 000000000..9fb8b06ab Binary files /dev/null and b/Tests/images/tga/common/200x32_la_bl_rle.tga differ diff --git a/Tests/images/tga/common/200x32_la_tl_raw.tga b/Tests/images/tga/common/200x32_la_tl_raw.tga new file mode 100644 index 000000000..6af1fa053 Binary files /dev/null and b/Tests/images/tga/common/200x32_la_tl_raw.tga differ diff --git a/Tests/images/tga/common/200x32_la_tl_rle.tga b/Tests/images/tga/common/200x32_la_tl_rle.tga new file mode 100644 index 000000000..fce83e3cf Binary files /dev/null and b/Tests/images/tga/common/200x32_la_tl_rle.tga differ diff --git a/Tests/images/tga/common/200x32_p.png b/Tests/images/tga/common/200x32_p.png new file mode 100644 index 000000000..a57a8a22a Binary files /dev/null and b/Tests/images/tga/common/200x32_p.png differ diff --git a/Tests/images/tga/common/200x32_p_bl_raw.tga b/Tests/images/tga/common/200x32_p_bl_raw.tga new file mode 100644 index 000000000..89145aa81 Binary files /dev/null and b/Tests/images/tga/common/200x32_p_bl_raw.tga differ diff --git a/Tests/images/tga/common/200x32_p_bl_rle.tga b/Tests/images/tga/common/200x32_p_bl_rle.tga new file mode 100644 index 000000000..bc53f2f93 Binary files /dev/null and b/Tests/images/tga/common/200x32_p_bl_rle.tga differ diff --git a/Tests/images/tga/common/200x32_p_tl_raw.tga b/Tests/images/tga/common/200x32_p_tl_raw.tga new file mode 100644 index 000000000..247db20a2 Binary files /dev/null and b/Tests/images/tga/common/200x32_p_tl_raw.tga differ diff --git a/Tests/images/tga/common/200x32_p_tl_rle.tga b/Tests/images/tga/common/200x32_p_tl_rle.tga new file mode 100644 index 000000000..3092ff923 Binary files /dev/null and b/Tests/images/tga/common/200x32_p_tl_rle.tga differ diff --git a/Tests/images/tga/common/200x32_rgb.png b/Tests/images/tga/common/200x32_rgb.png new file mode 100644 index 000000000..6614141a5 Binary files /dev/null and b/Tests/images/tga/common/200x32_rgb.png differ diff --git a/Tests/images/tga/common/200x32_rgb_bl_raw.tga b/Tests/images/tga/common/200x32_rgb_bl_raw.tga new file mode 100644 index 000000000..ebcea6b03 Binary files /dev/null and b/Tests/images/tga/common/200x32_rgb_bl_raw.tga differ diff --git a/Tests/images/tga/common/200x32_rgb_bl_rle.tga b/Tests/images/tga/common/200x32_rgb_bl_rle.tga new file mode 100644 index 000000000..87eb71c75 Binary files /dev/null and b/Tests/images/tga/common/200x32_rgb_bl_rle.tga differ diff --git a/Tests/images/tga/common/200x32_rgb_tl_raw.tga b/Tests/images/tga/common/200x32_rgb_tl_raw.tga new file mode 100644 index 000000000..2122ffa10 Binary files /dev/null and b/Tests/images/tga/common/200x32_rgb_tl_raw.tga differ diff --git a/Tests/images/tga/common/200x32_rgb_tl_rle.tga b/Tests/images/tga/common/200x32_rgb_tl_rle.tga new file mode 100644 index 000000000..2122ffa10 Binary files /dev/null and b/Tests/images/tga/common/200x32_rgb_tl_rle.tga differ diff --git a/Tests/images/tga/common/200x32_rgba.png b/Tests/images/tga/common/200x32_rgba.png new file mode 100644 index 000000000..74def0b7c Binary files /dev/null and b/Tests/images/tga/common/200x32_rgba.png differ diff --git a/Tests/images/tga/common/200x32_rgba_bl_raw.tga b/Tests/images/tga/common/200x32_rgba_bl_raw.tga new file mode 100644 index 000000000..148cc206a Binary files /dev/null and b/Tests/images/tga/common/200x32_rgba_bl_raw.tga differ diff --git a/Tests/images/tga/common/200x32_rgba_bl_rle.tga b/Tests/images/tga/common/200x32_rgba_bl_rle.tga new file mode 100644 index 000000000..1727fe338 Binary files /dev/null and b/Tests/images/tga/common/200x32_rgba_bl_rle.tga differ diff --git a/Tests/images/tga/common/200x32_rgba_tl_raw.tga b/Tests/images/tga/common/200x32_rgba_tl_raw.tga new file mode 100644 index 000000000..92ab8940d Binary files /dev/null and b/Tests/images/tga/common/200x32_rgba_tl_raw.tga differ diff --git a/Tests/images/tga/common/200x32_rgba_tl_rle.tga b/Tests/images/tga/common/200x32_rgba_tl_rle.tga new file mode 100644 index 000000000..2b593aee2 Binary files /dev/null and b/Tests/images/tga/common/200x32_rgba_tl_rle.tga differ diff --git a/Tests/images/tga/common/readme.txt b/Tests/images/tga/common/readme.txt new file mode 100644 index 000000000..4535d7fe6 --- /dev/null +++ b/Tests/images/tga/common/readme.txt @@ -0,0 +1,12 @@ +Images in this directory were created with GIMP. + +TGAs have names in the following format: + + {width}x{height}_{mode}_{origin}_{compression}.tga + +Where: + mode is PIL mode in lower case (L, P, RGB, etc.) + origin: + "bl" - bottom left + "tl" - top left + compression is either "raw" or "rle" diff --git a/Tests/test_box_blur.py b/Tests/test_box_blur.py index 622b842d0..2787dfc0d 100644 --- a/Tests/test_box_blur.py +++ b/Tests/test_box_blur.py @@ -1,6 +1,6 @@ from helper import unittest, PillowTestCase -from PIL import Image, ImageOps +from PIL import Image, ImageFilter sample = Image.new("L", (7, 5)) @@ -16,7 +16,7 @@ sample.putdata(sum([ class TestBoxBlurApi(PillowTestCase): def test_imageops_box_blur(self): - i = ImageOps.box_blur(sample, 1) + i = sample.filter(ImageFilter.BoxBlur(1)) self.assertEqual(i.mode, sample.mode) self.assertEqual(i.size, sample.size) self.assertIsInstance(i, Image.Image) diff --git a/Tests/test_color_lut.py b/Tests/test_color_lut.py new file mode 100644 index 000000000..6d0b76fed --- /dev/null +++ b/Tests/test_color_lut.py @@ -0,0 +1,523 @@ +from __future__ import division + +from array import array + +from PIL import Image, ImageFilter +from helper import unittest, PillowTestCase + +try: + import numpy +except ImportError: + numpy = None + + +class TestColorLut3DCoreAPI(PillowTestCase): + def generate_identity_table(self, channels, size): + if isinstance(size, tuple): + size1D, size2D, size3D = size + else: + size1D, size2D, size3D = (size, size, size) + + table = [ + [ + r / float(size1D-1) if size1D != 1 else 0, + g / float(size2D-1) if size2D != 1 else 0, + b / float(size3D-1) if size3D != 1 else 0, + r / float(size1D-1) if size1D != 1 else 0, + g / float(size2D-1) if size2D != 1 else 0, + ][:channels] + for b in range(size3D) + for g in range(size2D) + for r in range(size1D) + ] + return ( + channels, size1D, size2D, size3D, + [item for sublist in table for item in sublist]) + + def test_wrong_args(self): + im = Image.new('RGB', (10, 10), 0) + + with self.assertRaisesRegex(ValueError, "filter"): + im.im.color_lut_3d('RGB', + Image.CUBIC, + *self.generate_identity_table(3, 3)) + + with self.assertRaisesRegex(ValueError, "image mode"): + im.im.color_lut_3d('wrong', + Image.LINEAR, + *self.generate_identity_table(3, 3)) + + with self.assertRaisesRegex(ValueError, "table_channels"): + im.im.color_lut_3d('RGB', + Image.LINEAR, + *self.generate_identity_table(5, 3)) + + with self.assertRaisesRegex(ValueError, "table_channels"): + im.im.color_lut_3d('RGB', + Image.LINEAR, + *self.generate_identity_table(1, 3)) + + with self.assertRaisesRegex(ValueError, "table_channels"): + im.im.color_lut_3d('RGB', + Image.LINEAR, + *self.generate_identity_table(2, 3)) + + with self.assertRaisesRegex(ValueError, "Table size"): + im.im.color_lut_3d('RGB', + Image.LINEAR, + *self.generate_identity_table(3, (1, 3, 3))) + + with self.assertRaisesRegex(ValueError, "Table size"): + im.im.color_lut_3d('RGB', + Image.LINEAR, + *self.generate_identity_table(3, (66, 3, 3))) + + with self.assertRaisesRegex(ValueError, r"size1D \* size2D \* size3D"): + im.im.color_lut_3d('RGB', + Image.LINEAR, + 3, 2, 2, 2, [0, 0, 0] * 7) + + with self.assertRaisesRegex(ValueError, r"size1D \* size2D \* size3D"): + im.im.color_lut_3d('RGB', + Image.LINEAR, + 3, 2, 2, 2, [0, 0, 0] * 9) + + with self.assertRaises(TypeError): + im.im.color_lut_3d('RGB', + Image.LINEAR, + 3, 2, 2, 2, [0, 0, "0"] * 8) + + with self.assertRaises(TypeError): + im.im.color_lut_3d('RGB', + Image.LINEAR, + 3, 2, 2, 2, 16) + + def test_correct_args(self): + im = Image.new('RGB', (10, 10), 0) + + im.im.color_lut_3d('RGB', Image.LINEAR, + *self.generate_identity_table(3, 3)) + + im.im.color_lut_3d('CMYK', Image.LINEAR, + *self.generate_identity_table(4, 3)) + + im.im.color_lut_3d('RGB', Image.LINEAR, + *self.generate_identity_table(3, (2, 3, 3))) + + im.im.color_lut_3d('RGB', Image.LINEAR, + *self.generate_identity_table(3, (65, 3, 3))) + + im.im.color_lut_3d('RGB', Image.LINEAR, + *self.generate_identity_table(3, (3, 65, 3))) + + im.im.color_lut_3d('RGB', Image.LINEAR, + *self.generate_identity_table(3, (3, 3, 65))) + + def test_wrong_mode(self): + with self.assertRaisesRegex(ValueError, "wrong mode"): + im = Image.new('L', (10, 10), 0) + im.im.color_lut_3d('RGB', Image.LINEAR, + *self.generate_identity_table(3, 3)) + + with self.assertRaisesRegex(ValueError, "wrong mode"): + im = Image.new('RGB', (10, 10), 0) + im.im.color_lut_3d('L', Image.LINEAR, + *self.generate_identity_table(3, 3)) + + with self.assertRaisesRegex(ValueError, "wrong mode"): + im = Image.new('L', (10, 10), 0) + im.im.color_lut_3d('L', Image.LINEAR, + *self.generate_identity_table(3, 3)) + + with self.assertRaisesRegex(ValueError, "wrong mode"): + im = Image.new('RGB', (10, 10), 0) + im.im.color_lut_3d('RGBA', Image.LINEAR, + *self.generate_identity_table(3, 3)) + + with self.assertRaisesRegex(ValueError, "wrong mode"): + im = Image.new('RGB', (10, 10), 0) + im.im.color_lut_3d('RGB', Image.LINEAR, + *self.generate_identity_table(4, 3)) + + def test_correct_mode(self): + im = Image.new('RGBA', (10, 10), 0) + im.im.color_lut_3d('RGBA', Image.LINEAR, + *self.generate_identity_table(3, 3)) + + im = Image.new('RGBA', (10, 10), 0) + im.im.color_lut_3d('RGBA', Image.LINEAR, + *self.generate_identity_table(4, 3)) + + im = Image.new('RGB', (10, 10), 0) + im.im.color_lut_3d('HSV', Image.LINEAR, + *self.generate_identity_table(3, 3)) + + im = Image.new('RGB', (10, 10), 0) + im.im.color_lut_3d('RGBA', Image.LINEAR, + *self.generate_identity_table(4, 3)) + + def test_identities(self): + g = Image.linear_gradient('L') + im = Image.merge('RGB', [g, g.transpose(Image.ROTATE_90), + g.transpose(Image.ROTATE_180)]) + + # Fast test with small cubes + for size in [2, 3, 5, 7, 11, 16, 17]: + self.assert_image_equal(im, im._new( + im.im.color_lut_3d('RGB', Image.LINEAR, + *self.generate_identity_table(3, size)))) + + # Not so fast + self.assert_image_equal(im, im._new( + im.im.color_lut_3d('RGB', Image.LINEAR, + *self.generate_identity_table(3, (2, 2, 65))))) + + def test_identities_4_channels(self): + g = Image.linear_gradient('L') + im = Image.merge('RGB', [g, g.transpose(Image.ROTATE_90), + g.transpose(Image.ROTATE_180)]) + + # Red channel copied to alpha + self.assert_image_equal( + Image.merge('RGBA', (im.split()*2)[:4]), + im._new(im.im.color_lut_3d('RGBA', Image.LINEAR, + *self.generate_identity_table(4, 17)))) + + def test_copy_alpha_channel(self): + g = Image.linear_gradient('L') + im = Image.merge('RGBA', [g, g.transpose(Image.ROTATE_90), + g.transpose(Image.ROTATE_180), + g.transpose(Image.ROTATE_270)]) + + self.assert_image_equal(im, im._new( + im.im.color_lut_3d('RGBA', Image.LINEAR, + *self.generate_identity_table(3, 17)))) + + def test_channels_order(self): + g = Image.linear_gradient('L') + im = Image.merge('RGB', [g, g.transpose(Image.ROTATE_90), + g.transpose(Image.ROTATE_180)]) + + # Reverse channels by splitting and using table + self.assert_image_equal( + Image.merge('RGB', im.split()[::-1]), + im._new(im.im.color_lut_3d('RGB', Image.LINEAR, + 3, 2, 2, 2, [ + 0, 0, 0, 0, 0, 1, + 0, 1, 0, 0, 1, 1, + + 1, 0, 0, 1, 0, 1, + 1, 1, 0, 1, 1, 1, + ]))) + + def test_overflow(self): + g = Image.linear_gradient('L') + im = Image.merge('RGB', [g, g.transpose(Image.ROTATE_90), + g.transpose(Image.ROTATE_180)]) + + transformed = im._new(im.im.color_lut_3d('RGB', Image.LINEAR, + 3, 2, 2, 2, + [ + -1, -1, -1, 2, -1, -1, + -1, 2, -1, 2, 2, -1, + + -1, -1, 2, 2, -1, 2, + -1, 2, 2, 2, 2, 2, + ])).load() + self.assertEqual(transformed[0, 0], (0, 0, 255)) + self.assertEqual(transformed[50, 50], (0, 0, 255)) + self.assertEqual(transformed[255, 0], (0, 255, 255)) + self.assertEqual(transformed[205, 50], (0, 255, 255)) + self.assertEqual(transformed[0, 255], (255, 0, 0)) + self.assertEqual(transformed[50, 205], (255, 0, 0)) + self.assertEqual(transformed[255, 255], (255, 255, 0)) + self.assertEqual(transformed[205, 205], (255, 255, 0)) + + transformed = im._new(im.im.color_lut_3d('RGB', Image.LINEAR, + 3, 2, 2, 2, + [ + -3, -3, -3, 5, -3, -3, + -3, 5, -3, 5, 5, -3, + + -3, -3, 5, 5, -3, 5, + -3, 5, 5, 5, 5, 5, + ])).load() + self.assertEqual(transformed[0, 0], (0, 0, 255)) + self.assertEqual(transformed[50, 50], (0, 0, 255)) + self.assertEqual(transformed[255, 0], (0, 255, 255)) + self.assertEqual(transformed[205, 50], (0, 255, 255)) + self.assertEqual(transformed[0, 255], (255, 0, 0)) + self.assertEqual(transformed[50, 205], (255, 0, 0)) + self.assertEqual(transformed[255, 255], (255, 255, 0)) + self.assertEqual(transformed[205, 205], (255, 255, 0)) + + +class TestColorLut3DFilter(PillowTestCase): + def test_wrong_args(self): + with self.assertRaisesRegex(ValueError, "should be either an integer"): + ImageFilter.Color3DLUT("small", [1]) + + with self.assertRaisesRegex(ValueError, "should be either an integer"): + ImageFilter.Color3DLUT((11, 11), [1]) + + with self.assertRaisesRegex(ValueError, r"in \[2, 65\] range"): + ImageFilter.Color3DLUT((11, 11, 1), [1]) + + with self.assertRaisesRegex(ValueError, r"in \[2, 65\] range"): + ImageFilter.Color3DLUT((11, 11, 66), [1]) + + with self.assertRaisesRegex(ValueError, "table should have .+ items"): + ImageFilter.Color3DLUT((3, 3, 3), [1, 1, 1]) + + with self.assertRaisesRegex(ValueError, "table should have .+ items"): + ImageFilter.Color3DLUT((3, 3, 3), [[1, 1, 1]] * 2) + + with self.assertRaisesRegex(ValueError, "should have a length of 4"): + ImageFilter.Color3DLUT((3, 3, 3), [[1, 1, 1]] * 27, channels=4) + + with self.assertRaisesRegex(ValueError, "should have a length of 3"): + ImageFilter.Color3DLUT((2, 2, 2), [[1, 1]] * 8) + + with self.assertRaisesRegex(ValueError, "Only 3 or 4 output"): + ImageFilter.Color3DLUT((2, 2, 2), [[1, 1]] * 8, channels=2) + + def test_convert_table(self): + lut = ImageFilter.Color3DLUT(2, [0, 1, 2] * 8) + self.assertEqual(tuple(lut.size), (2, 2, 2)) + self.assertEqual(lut.name, "Color 3D LUT") + + lut = ImageFilter.Color3DLUT((2, 2, 2), [ + (0, 1, 2), (3, 4, 5), (6, 7, 8), (9, 10, 11), + (12, 13, 14), (15, 16, 17), (18, 19, 20), (21, 22, 23)]) + self.assertEqual(tuple(lut.size), (2, 2, 2)) + self.assertEqual(lut.table, list(range(24))) + + lut = ImageFilter.Color3DLUT((2, 2, 2), [(0, 1, 2, 3)] * 8, + channels=4) + + @unittest.skipIf(numpy is None, "Numpy is not installed") + def test_numpy_sources(self): + table = numpy.ones((5, 6, 7, 3), dtype=numpy.float16) + with self.assertRaisesRegex(ValueError, "should have either channels"): + lut = ImageFilter.Color3DLUT((5, 6, 7), table) + + table = numpy.ones((7, 6, 5, 3), dtype=numpy.float16) + lut = ImageFilter.Color3DLUT((5, 6, 7), table) + self.assertIsInstance(lut.table, numpy.ndarray) + self.assertEqual(lut.table.dtype, table.dtype) + self.assertEqual(lut.table.shape, (table.size,)) + + table = numpy.ones((7 * 6 * 5, 3), dtype=numpy.float16) + lut = ImageFilter.Color3DLUT((5, 6, 7), table) + self.assertEqual(lut.table.shape, (table.size,)) + + table = numpy.ones((7 * 6 * 5 * 3), dtype=numpy.float16) + lut = ImageFilter.Color3DLUT((5, 6, 7), table) + self.assertEqual(lut.table.shape, (table.size,)) + + # Check application + Image.new('RGB', (10, 10), 0).filter(lut) + + # Check copy + table[0] = 33 + self.assertEqual(lut.table[0], 1) + + # Check not copy + table = numpy.ones((7 * 6 * 5 * 3), dtype=numpy.float16) + lut = ImageFilter.Color3DLUT((5, 6, 7), table, _copy_table=False) + table[0] = 33 + self.assertEqual(lut.table[0], 33) + + @unittest.skipIf(numpy is None, "Numpy is not installed") + def test_numpy_formats(self): + g = Image.linear_gradient('L') + im = Image.merge('RGB', [g, g.transpose(Image.ROTATE_90), + g.transpose(Image.ROTATE_180)]) + + lut = ImageFilter.Color3DLUT.generate((7, 9, 11), + lambda r, g, b: (r, g, b)) + lut.table = numpy.array(lut.table, dtype=numpy.float32)[:-1] + with self.assertRaisesRegex(ValueError, "should have table_channels"): + im.filter(lut) + + lut = ImageFilter.Color3DLUT.generate((7, 9, 11), + lambda r, g, b: (r, g, b)) + lut.table = (numpy.array(lut.table, dtype=numpy.float32) + .reshape((7 * 9 * 11), 3)) + with self.assertRaisesRegex(ValueError, "should have table_channels"): + im.filter(lut) + + lut = ImageFilter.Color3DLUT.generate((7, 9, 11), + lambda r, g, b: (r, g, b)) + lut.table = numpy.array(lut.table, dtype=numpy.float16) + self.assert_image_equal(im, im.filter(lut)) + + lut = ImageFilter.Color3DLUT.generate((7, 9, 11), + lambda r, g, b: (r, g, b)) + lut.table = numpy.array(lut.table, dtype=numpy.float32) + self.assert_image_equal(im, im.filter(lut)) + + lut = ImageFilter.Color3DLUT.generate((7, 9, 11), + lambda r, g, b: (r, g, b)) + lut.table = numpy.array(lut.table, dtype=numpy.float64) + self.assert_image_equal(im, im.filter(lut)) + + lut = ImageFilter.Color3DLUT.generate((7, 9, 11), + lambda r, g, b: (r, g, b)) + lut.table = numpy.array(lut.table, dtype=numpy.int32) + im.filter(lut) + lut.table = numpy.array(lut.table, dtype=numpy.int8) + im.filter(lut) + + def test_repr(self): + lut = ImageFilter.Color3DLUT(2, [0, 1, 2] * 8) + self.assertEqual(repr(lut), + "") + + lut = ImageFilter.Color3DLUT( + (3, 4, 5), array('f', [0, 0, 0, 0] * (3 * 4 * 5)), + channels=4, target_mode='YCbCr', _copy_table=False) + self.assertEqual( + repr(lut), + "") + + +class TestGenerateColorLut3D(PillowTestCase): + def test_wrong_channels_count(self): + with self.assertRaisesRegex(ValueError, "3 or 4 output channels"): + ImageFilter.Color3DLUT.generate( + 5, channels=2, callback=lambda r, g, b: (r, g, b)) + + with self.assertRaisesRegex(ValueError, "should have either channels"): + ImageFilter.Color3DLUT.generate(5, lambda r, g, b: (r, g, b, r)) + + with self.assertRaisesRegex(ValueError, "should have either channels"): + ImageFilter.Color3DLUT.generate( + 5, channels=4, callback=lambda r, g, b: (r, g, b)) + + def test_3_channels(self): + lut = ImageFilter.Color3DLUT.generate(5, lambda r, g, b: (r, g, b)) + self.assertEqual(tuple(lut.size), (5, 5, 5)) + self.assertEqual(lut.name, "Color 3D LUT") + self.assertEqual(lut.table[:24], [ + 0.0, 0.0, 0.0, 0.25, 0.0, 0.0, 0.5, 0.0, 0.0, 0.75, 0.0, 0.0, + 1.0, 0.0, 0.0, 0.0, 0.25, 0.0, 0.25, 0.25, 0.0, 0.5, 0.25, 0.0]) + + def test_4_channels(self): + lut = ImageFilter.Color3DLUT.generate( + 5, channels=4, callback=lambda r, g, b: (b, r, g, (r+g+b) / 2)) + self.assertEqual(tuple(lut.size), (5, 5, 5)) + self.assertEqual(lut.name, "Color 3D LUT") + self.assertEqual(lut.table[:24], [ + 0.0, 0.0, 0.0, 0.0, 0.0, 0.25, 0.0, 0.125, 0.0, 0.5, 0.0, 0.25, + 0.0, 0.75, 0.0, 0.375, 0.0, 1.0, 0.0, 0.5, 0.0, 0.0, 0.25, 0.125 + ]) + + def test_apply(self): + lut = ImageFilter.Color3DLUT.generate(5, lambda r, g, b: (r, g, b)) + + g = Image.linear_gradient('L') + im = Image.merge('RGB', [g, g.transpose(Image.ROTATE_90), + g.transpose(Image.ROTATE_180)]) + self.assertEqual(im, im.filter(lut)) + + +class TestTransformColorLut3D(PillowTestCase): + def test_wrong_args(self): + source = ImageFilter.Color3DLUT.generate(5, lambda r, g, b: (r, g, b)) + + with self.assertRaisesRegex(ValueError, "Only 3 or 4 output"): + source.transform(lambda r, g, b: (r, g, b), channels=8) + + with self.assertRaisesRegex(ValueError, "should have either channels"): + source.transform(lambda r, g, b: (r, g, b), channels=4) + + with self.assertRaisesRegex(ValueError, "should have either channels"): + source.transform(lambda r, g, b: (r, g, b, 1)) + + with self.assertRaises(TypeError): + source.transform(lambda r, g, b, a: (r, g, b)) + + def test_target_mode(self): + source = ImageFilter.Color3DLUT.generate( + 2, lambda r, g, b: (r, g, b), target_mode='HSV') + + lut = source.transform(lambda r, g, b: (r, g, b)) + self.assertEqual(lut.mode, 'HSV') + + lut = source.transform(lambda r, g, b: (r, g, b), target_mode='RGB') + self.assertEqual(lut.mode, 'RGB') + + def test_3_to_3_channels(self): + source = ImageFilter.Color3DLUT.generate( + (3, 4, 5), lambda r, g, b: (r, g, b)) + lut = source.transform(lambda r, g, b: (r*r, g*g, b*b)) + self.assertEqual(tuple(lut.size), tuple(source.size)) + self.assertEqual(len(lut.table), len(source.table)) + self.assertNotEqual(lut.table, source.table) + self.assertEqual(lut.table[0:10], [ + 0.0, 0.0, 0.0, 0.25, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0]) + + def test_3_to_4_channels(self): + source = ImageFilter.Color3DLUT.generate( + (6, 5, 4), lambda r, g, b: (r, g, b)) + lut = source.transform(lambda r, g, b: (r*r, g*g, b*b, 1), channels=4) + self.assertEqual(tuple(lut.size), tuple(source.size)) + self.assertNotEqual(len(lut.table), len(source.table)) + self.assertNotEqual(lut.table, source.table) + self.assertEqual(lut.table[0:16], [ + 0.0, 0.0, 0.0, 1, 0.2**2, 0.0, 0.0, 1, + 0.4**2, 0.0, 0.0, 1, 0.6**2, 0.0, 0.0, 1]) + + def test_4_to_3_channels(self): + source = ImageFilter.Color3DLUT.generate( + (3, 6, 5), lambda r, g, b: (r, g, b, 1), channels=4) + lut = source.transform(lambda r, g, b, a: (a - r*r, a - g*g, a - b*b), + channels=3) + self.assertEqual(tuple(lut.size), tuple(source.size)) + self.assertNotEqual(len(lut.table), len(source.table)) + self.assertNotEqual(lut.table, source.table) + self.assertEqual(lut.table[0:18], [ + 1.0, 1.0, 1.0, 0.75, 1.0, 1.0, 0.0, 1.0, 1.0, + 1.0, 0.96, 1.0, 0.75, 0.96, 1.0, 0.0, 0.96, 1.0]) + + def test_4_to_4_channels(self): + source = ImageFilter.Color3DLUT.generate( + (6, 5, 4), lambda r, g, b: (r, g, b, 1), channels=4) + lut = source.transform(lambda r, g, b, a: (r*r, g*g, b*b, a - 0.5)) + self.assertEqual(tuple(lut.size), tuple(source.size)) + self.assertEqual(len(lut.table), len(source.table)) + self.assertNotEqual(lut.table, source.table) + self.assertEqual(lut.table[0:16], [ + 0.0, 0.0, 0.0, 0.5, 0.2**2, 0.0, 0.0, 0.5, + 0.4**2, 0.0, 0.0, 0.5, 0.6**2, 0.0, 0.0, 0.5]) + + def test_with_normals_3_channels(self): + source = ImageFilter.Color3DLUT.generate( + (6, 5, 4), lambda r, g, b: (r*r, g*g, b*b)) + lut = source.transform( + lambda nr, ng, nb, r, g, b: (nr - r, ng - g, nb - b), + with_normals=True) + self.assertEqual(tuple(lut.size), tuple(source.size)) + self.assertEqual(len(lut.table), len(source.table)) + self.assertNotEqual(lut.table, source.table) + self.assertEqual(lut.table[0:18], [ + 0.0, 0.0, 0.0, 0.16, 0.0, 0.0, 0.24, 0.0, 0.0, + 0.24, 0.0, 0.0, 0.8 - (0.8**2), 0, 0, 0, 0, 0]) + + def test_with_normals_4_channels(self): + source = ImageFilter.Color3DLUT.generate( + (3, 6, 5), lambda r, g, b: (r*r, g*g, b*b, 1), channels=4) + lut = source.transform( + lambda nr, ng, nb, r, g, b, a: (nr - r, ng - g, nb - b, a-0.5), + with_normals=True) + self.assertEqual(tuple(lut.size), tuple(source.size)) + self.assertEqual(len(lut.table), len(source.table)) + self.assertNotEqual(lut.table, source.table) + self.assertEqual(lut.table[0:16], [ + 0.0, 0.0, 0.0, 0.5, 0.25, 0.0, 0.0, 0.5, + 0.0, 0.0, 0.0, 0.5, 0.0, 0.16, 0.0, 0.5]) + + +if __name__ == '__main__': + unittest.main() diff --git a/Tests/test_file_eps.py b/Tests/test_file_eps.py index 2313b292c..66fedee90 100644 --- a/Tests/test_file_eps.py +++ b/Tests/test_file_eps.py @@ -76,7 +76,7 @@ class TestFileEps(PillowTestCase): plot_image = Image.open("Tests/images/reqd_showpage.eps") target = Image.open("Tests/images/reqd_showpage.png") - #should not crash/hang + # should not crash/hang plot_image.load() # fonts could be slightly different self.assert_image_similar(plot_image, target, 6) @@ -195,41 +195,15 @@ class TestFileEps(PillowTestCase): self.assertEqual(t.readline().strip('\r\n'), 'baz', ending) self.assertEqual(t.readline().strip('\r\n'), 'bif', ending) - def _test_readline_stringio(self, test_string, ending): - # check all the freaking line endings possible - try: - import StringIO - except ImportError: - # don't skip, it skips everything in the parent test - return - t = StringIO.StringIO(test_string) + def _test_readline_io_psfile(self, test_string, ending): + f = io.BytesIO(test_string.encode('latin-1')) + t = EpsImagePlugin.PSFile(f) self._test_readline(t, ending) - def _test_readline_io(self, test_string, ending): - if str is bytes: - t = io.StringIO(unicode(test_string)) - else: - t = io.StringIO(test_string) - self._test_readline(t, ending) - - def _test_readline_file_universal(self, test_string, ending): - f = self.tempfile('temp.txt') - with open(f, 'wb') as w: - if str is bytes: - w.write(test_string) - else: - w.write(test_string.encode('UTF-8')) - - with open(f, 'rU') as t: - self._test_readline(t, ending) - def _test_readline_file_psfile(self, test_string, ending): f = self.tempfile('temp.txt') with open(f, 'wb') as w: - if str is bytes: - w.write(test_string) - else: - w.write(test_string.encode('UTF-8')) + w.write(test_string.encode('latin-1')) with open(f, 'rb') as r: t = EpsImagePlugin.PSFile(r) @@ -238,29 +212,12 @@ class TestFileEps(PillowTestCase): def test_readline(self): # check all the freaking line endings possible from the spec # test_string = u'something\r\nelse\n\rbaz\rbif\n' - line_endings = ['\r\n', '\n'] - not_working_endings = ['\n\r', '\r'] + line_endings = ['\r\n', '\n', '\n\r', '\r'] strings = ['something', 'else', 'baz', 'bif'] for ending in line_endings: s = ending.join(strings) - # Native Python versions will pass these endings. - # self._test_readline_stringio(s, ending) - # self._test_readline_io(s, ending) - # self._test_readline_file_universal(s, ending) - - self._test_readline_file_psfile(s, ending) - - for ending in not_working_endings: - # these only work with the PSFile, while they're in spec, - # they're not likely to be used - s = ending.join(strings) - - # Native Python versions may fail on these endings. - # self._test_readline_stringio(s, ending) - # self._test_readline_io(s, ending) - # self._test_readline_file_universal(s, ending) - + self._test_readline_io_psfile(s, ending) self._test_readline_file_psfile(s, ending) def test_open_eps(self): diff --git a/Tests/test_file_gd.py b/Tests/test_file_gd.py new file mode 100644 index 000000000..326b0c4b2 --- /dev/null +++ b/Tests/test_file_gd.py @@ -0,0 +1,28 @@ +from helper import unittest, PillowTestCase + +from PIL import GdImageFile + +import io + +TEST_GD_FILE = "Tests/images/hopper.gd" + + +class TestFileGd(PillowTestCase): + + def test_sanity(self): + im = GdImageFile.open(TEST_GD_FILE) + self.assertEqual(im.size, (128, 128)) + self.assertEqual(im.format, "GD") + + def test_bad_mode(self): + self.assertRaises(ValueError, + GdImageFile.open, TEST_GD_FILE, 'bad mode') + + def test_invalid_file(self): + invalid_file = "Tests/images/flower.jpg" + + self.assertRaises(IOError, GdImageFile.open, invalid_file) + + +if __name__ == '__main__': + unittest.main() diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index c8f3836fc..086a0f5d0 100644 --- a/Tests/test_file_gif.py +++ b/Tests/test_file_gif.py @@ -305,9 +305,12 @@ class TestFileGif(PillowTestCase): out = self.tempfile('temp.gif') im = Image.new('L', (100, 100), '#000') - im.save(out, duration=duration) - reread = Image.open(out) + # Check that the argument has priority over the info settings + im.info['duration'] = 100 + im.save(out, duration=duration) + + reread = Image.open(out) self.assertEqual(reread.info['duration'], duration) def test_multiple_duration(self): @@ -399,7 +402,8 @@ class TestFileGif(PillowTestCase): def test_comment(self): im = Image.open(TEST_GIF) - self.assertEqual(im.info['comment'], b"File written by Adobe Photoshop\xa8 4.0") + self.assertEqual(im.info['comment'], + b"File written by Adobe Photoshop\xa8 4.0") out = self.tempfile('temp.gif') im = Image.new('L', (100, 100), '#000') diff --git a/Tests/test_file_icns.py b/Tests/test_file_icns.py index d8d6a128e..06a7e39bb 100644 --- a/Tests/test_file_icns.py +++ b/Tests/test_file_icns.py @@ -40,11 +40,11 @@ class TestFileIcns(PillowTestCase): im = Image.open(TEST_FILE) temp_file = self.tempfile("temp.icns") - provided_im = Image.new('RGBA', (32, 32), (255, 0, 0, 0)) + provided_im = Image.new('RGBA', (32, 32), (255, 0, 0, 128)) im.save(temp_file, append_images=[provided_im]) reread = Image.open(temp_file) - self.assert_image_equal(reread, im) + self.assert_image_similar(reread, im, 1) reread = Image.open(temp_file) reread.size = (16, 16, 2) diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index 205a6ffdf..c42d67885 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -41,13 +41,14 @@ class TestFileJpeg(PillowTestCase): def test_sanity(self): # internal version number - self.assertRegexpMatches(Image.core.jpeglib_version, r"\d+\.\d+$") + self.assertRegex(Image.core.jpeglib_version, r"\d+\.\d+$") im = Image.open(TEST_FILE) im.load() self.assertEqual(im.mode, "RGB") self.assertEqual(im.size, (128, 128)) self.assertEqual(im.format, "JPEG") + self.assertEqual(im.get_format_mimetype(), "image/jpeg") def test_app(self): # Test APP/COM reader (@PIL135) @@ -363,7 +364,6 @@ class TestFileJpeg(PillowTestCase): with self.assertRaises(IOError): im.load() - def _n_qtables_helper(self, n, test_file): im = Image.open(test_file) f = self.tempfile('temp.jpg') @@ -439,17 +439,17 @@ class TestFileJpeg(PillowTestCase): self._n_qtables_helper(4, "Tests/images/pil_sample_cmyk.jpg") # not a sequence - self.assertRaises(Exception, self.roundtrip, im, qtables='a') + self.assertRaises(ValueError, self.roundtrip, im, qtables='a') # sequence wrong length - self.assertRaises(Exception, self.roundtrip, im, qtables=[]) + self.assertRaises(ValueError, self.roundtrip, im, qtables=[]) # sequence wrong length - self.assertRaises(Exception, + self.assertRaises(ValueError, self.roundtrip, im, qtables=[1, 2, 3, 4, 5]) # qtable entry not a sequence - self.assertRaises(Exception, self.roundtrip, im, qtables=[1]) + self.assertRaises(ValueError, self.roundtrip, im, qtables=[1]) # qtable entry has wrong number of items - self.assertRaises(Exception, + self.assertRaises(ValueError, self.roundtrip, im, qtables=[[1, 2, 3, 4]]) @unittest.skipUnless(djpeg_available(), "djpeg not available") @@ -599,7 +599,7 @@ class TestFileCloseW32(PillowTestCase): im = Image.open(tmpfile) fp = im.fp self.assertFalse(fp.closed) - self.assertRaises(Exception, os.remove, tmpfile) + self.assertRaises(WindowsError, os.remove, tmpfile) im.load() self.assertTrue(fp.closed) # this should not fail, as load should have closed the file. diff --git a/Tests/test_file_jpeg2k.py b/Tests/test_file_jpeg2k.py index 810e21a9d..be49c99bf 100644 --- a/Tests/test_file_jpeg2k.py +++ b/Tests/test_file_jpeg2k.py @@ -31,7 +31,7 @@ class TestFileJpeg2k(PillowTestCase): def test_sanity(self): # Internal version number - self.assertRegexpMatches(Image.core.jp2klib_version, r'\d+\.\d+\.\d+$') + self.assertRegex(Image.core.jp2klib_version, r'\d+\.\d+\.\d+$') im = Image.open('Tests/images/test-card-lossless.jp2') px = im.load() @@ -146,13 +146,13 @@ class TestFileJpeg2k(PillowTestCase): self.assertEqual(j2k.mode, 'I;16') self.assertEqual(jp2.mode, 'I;16') - def test_16bit_monchrome_jp2_like_tiff(self): + def test_16bit_monochrome_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): + def test_16bit_monochrome_j2k_like_tiff(self): tiff_16bit = Image.open('Tests/images/16bit.cropped.tif') j2k = Image.open('Tests/images/16bit.cropped.j2k') diff --git a/Tests/test_file_libtiff.py b/Tests/test_file_libtiff.py index dc3b10845..77caa0b9d 100644 --- a/Tests/test_file_libtiff.py +++ b/Tests/test_file_libtiff.py @@ -1,6 +1,7 @@ from __future__ import print_function -from helper import unittest, PillowTestCase, hopper, py3 +from helper import unittest, PillowTestCase, hopper from PIL import features +from PIL._util import py3 from ctypes import c_float import io @@ -30,7 +31,7 @@ class LibTiffTestCase(PillowTestCase): try: self.assertEqual(im._compression, 'group4') - except: + except AttributeError: print("No _compression") print(dir(im)) @@ -125,7 +126,8 @@ class TestFileLibTiff(LibTiffTestCase): im.tile[0][:3], ('tiff_adobe_deflate', (0, 0, 278, 374), 0)) im.load() - self.assert_image_equal_tofile(im, 'Tests/images/tiff_adobe_deflate.png') + self.assert_image_equal_tofile(im, + 'Tests/images/tiff_adobe_deflate.png') def test_write_metadata(self): """ Test metadata writing through libtiff """ @@ -193,7 +195,7 @@ class TestFileLibTiff(LibTiffTestCase): for tag in im.tag_v2: try: del(core_items[tag]) - except: + except KeyError: pass # Type codes: @@ -216,7 +218,8 @@ class TestFileLibTiff(LibTiffTestCase): if info.length == 0: new_ifd[tag] = tuple(values[info.type] for _ in range(3)) else: - new_ifd[tag] = tuple(values[info.type] for _ in range(info.length)) + new_ifd[tag] = tuple(values[info.type] + for _ in range(info.length)) # Extra samples really doesn't make sense in this application. del(new_ifd[338]) @@ -524,7 +527,7 @@ class TestFileLibTiff(LibTiffTestCase): f.write(src.read()) im = Image.open(tmpfile) - count = im.n_frames + im.n_frames im.close() try: os.remove(tmpfile) # Windows PermissionError here! @@ -577,10 +580,14 @@ class TestFileLibTiff(LibTiffTestCase): self.assertEqual(im.mode, "RGBA") self.assertEqual(im.size, (100, 40)) - self.assertEqual(im.tile, [('tiff_lzw', (0, 0, 100, 40), 0, ('RGBa;16N', 'tiff_lzw', False))]) + self.assertEqual( + im.tile, + [('tiff_lzw', (0, 0, 100, 40), 0, ('RGBa;16N', 'tiff_lzw', False))] + ) im.load() - self.assert_image_equal_tofile(im, "Tests/images/tiff_16bit_RGBa_target.png") + self.assert_image_equal_tofile( + im, "Tests/images/tiff_16bit_RGBa_target.png") def test_gimp_tiff(self): # Read TIFF JPEG images from GIMP [@PIL168] @@ -606,7 +613,8 @@ class TestFileLibTiff(LibTiffTestCase): im = Image.open("Tests/images/copyleft.tiff") self.assertEqual(im.mode, 'RGB') - self.assert_image_equal_tofile(im, "Tests/images/copyleft.png", mode='RGB') + self.assert_image_equal_tofile(im, "Tests/images/copyleft.png", + mode='RGB') def test_lzw(self): im = Image.open("Tests/images/hopper_lzw.tif") diff --git a/Tests/test_file_pdf.py b/Tests/test_file_pdf.py index f17da8d74..f012fb9d8 100644 --- a/Tests/test_file_pdf.py +++ b/Tests/test_file_pdf.py @@ -20,7 +20,8 @@ class TestFilePdf(PillowTestCase): self.assertTrue(os.path.isfile(outfile)) self.assertGreater(os.path.getsize(outfile), 0) with PdfParser.PdfParser(outfile) as pdf: - if kwargs.get("append_images", False) or kwargs.get("append", False): + if kwargs.get("append_images", False) or \ + kwargs.get("append", False): self.assertGreater(len(pdf.pages), 1) else: self.assertGreater(len(pdf.pages), 0) @@ -104,9 +105,21 @@ class TestFilePdf(PillowTestCase): self.assertTrue(os.path.isfile(outfile)) self.assertGreater(os.path.getsize(outfile), 0) + def test_multiframe_normal_save(self): + # Test saving a multiframe image without save_all + im = Image.open("Tests/images/dispose_bgnd.gif") + + outfile = self.tempfile('temp.pdf') + im.save(outfile) + + self.assertTrue(os.path.isfile(outfile)) + self.assertGreater(os.path.getsize(outfile), 0) + def test_pdf_open(self): # fail on a buffer full of null bytes - self.assertRaises(PdfParser.PdfFormatError, PdfParser.PdfParser, buf=bytearray(65536)) + self.assertRaises( + PdfParser.PdfFormatError, + PdfParser.PdfParser, buf=bytearray(65536)) # make an empty PDF object with PdfParser.PdfParser() as empty_pdf: @@ -143,7 +156,10 @@ class TestFilePdf(PillowTestCase): im = hopper("RGB") temp_dir = tempfile.mkdtemp() try: - self.assertRaises(IOError, im.save, os.path.join(temp_dir, "nonexistent.pdf"), append=True) + self.assertRaises(IOError, + im.save, + os.path.join(temp_dir, "nonexistent.pdf"), + append=True) finally: os.rmdir(temp_dir) @@ -194,7 +210,8 @@ class TestFilePdf(PillowTestCase): # append two images mode_CMYK = hopper("CMYK") mode_P = hopper("P") - mode_CMYK.save(pdf_filename, append=True, save_all=True, append_images=[mode_P]) + mode_CMYK.save(pdf_filename, + append=True, save_all=True, append_images=[mode_P]) # open the PDF again, check pages and info again with PdfParser.PdfParser(pdf_filename) as pdf: @@ -209,7 +226,9 @@ class TestFilePdf(PillowTestCase): def test_pdf_info(self): # make a PDF file - pdf_filename = self.helper_save_as_pdf("RGB", title="title", author="author", subject="subject", keywords="keywords", creator="creator", producer="producer") + pdf_filename = self.helper_save_as_pdf( + "RGB", title="title", author="author", subject="subject", + keywords="keywords", creator="creator", producer="producer") # open it, check pages and info with PdfParser.PdfParser(pdf_filename) as pdf: diff --git a/Tests/test_file_pixar.py b/Tests/test_file_pixar.py index ae8c7d5f5..3b00c710e 100644 --- a/Tests/test_file_pixar.py +++ b/Tests/test_file_pixar.py @@ -13,6 +13,7 @@ class TestFilePixar(PillowTestCase): self.assertEqual(im.mode, "RGB") self.assertEqual(im.size, (128, 128)) self.assertEqual(im.format, "PIXAR") + self.assertIsNone(im.get_format_mimetype()) im2 = hopper() self.assert_image_similar(im, im2, 4.8) diff --git a/Tests/test_file_png.py b/Tests/test_file_png.py index 8cf109e70..e9dcd5203 100644 --- a/Tests/test_file_png.py +++ b/Tests/test_file_png.py @@ -1,5 +1,6 @@ from helper import unittest, PillowTestCase, PillowLeakTestCase, hopper from PIL import Image, ImageFile, PngImagePlugin +from PIL._util import py3 from io import BytesIO import zlib @@ -68,8 +69,7 @@ class TestFilePng(PillowTestCase): def test_sanity(self): # internal version number - self.assertRegexpMatches( - Image.core.zlib_version, r"\d+\.\d+\.\d+(\.\d+)?$") + self.assertRegex(Image.core.zlib_version, r"\d+\.\d+\.\d+(\.\d+)?$") test_file = self.tempfile("temp.png") @@ -292,15 +292,29 @@ class TestFilePng(PillowTestCase): self.assertEqual(im.getcolors(), [(100, (0, 0, 0, 0))]) def test_save_l_transparency(self): + # There are 559 transparent pixels in l_trns.png. + num_transparent = 559 + in_file = "Tests/images/l_trns.png" im = Image.open(in_file) + self.assertEqual(im.mode, "L") + self.assertEqual(im.info["transparency"], 255) + + im_rgba = im.convert('RGBA') + self.assertEqual( + im_rgba.getchannel("A").getcolors()[0][0], num_transparent) test_file = self.tempfile("temp.png") im.save(test_file) - # There are 559 transparent pixels. - im = im.convert('RGBA') - self.assertEqual(im.getchannel('A').getcolors()[0][0], 559) + test_im = Image.open(test_file) + self.assertEqual(test_im.mode, "L") + self.assertEqual(test_im.info["transparency"], 255) + self.assert_image_equal(im, test_im) + + test_im_rgba = test_im.convert('RGBA') + self.assertEqual( + test_im_rgba.getchannel('A').getcolors()[0][0], num_transparent) def test_save_rgb_single_transparency(self): in_file = "Tests/images/caption_6_33_22.png" @@ -341,7 +355,8 @@ class TestFilePng(PillowTestCase): broken_crc_chunk_data = chunk_data[:-1] + b'q' # break CRC image_data = HEAD + broken_crc_chunk_data + TAIL - self.assertRaises(SyntaxError, PngImagePlugin.PngImageFile, BytesIO(image_data)) + self.assertRaises(SyntaxError, PngImagePlugin.PngImageFile, + BytesIO(image_data)) ImageFile.LOAD_TRUNCATED_IMAGES = True try: @@ -357,7 +372,8 @@ class TestFilePng(PillowTestCase): ImageFile.LOAD_TRUNCATED_IMAGES = True try: - self.assertRaises(SyntaxError, PngImagePlugin.PngImageFile, BytesIO(image_data)) + self.assertRaises(SyntaxError, PngImagePlugin.PngImageFile, + BytesIO(image_data)) finally: ImageFile.LOAD_TRUNCATED_IMAGES = False @@ -420,7 +436,7 @@ class TestFilePng(PillowTestCase): im = roundtrip(im, pnginfo=info) self.assertEqual(im.info, {"Text": value}) - if str is not bytes: + if py3: 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 diff --git a/Tests/test_file_tar.py b/Tests/test_file_tar.py index 3dd075042..9e02ab1a5 100644 --- a/Tests/test_file_tar.py +++ b/Tests/test_file_tar.py @@ -12,7 +12,7 @@ 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") + self.skipTest("neither jpeg nor zip support available") def test_sanity(self): if "zip_decoder" in codecs: diff --git a/Tests/test_file_tga.py b/Tests/test_file_tga.py index ef3acfe65..226b899dc 100644 --- a/Tests/test_file_tga.py +++ b/Tests/test_file_tga.py @@ -1,10 +1,76 @@ +import os +from glob import glob +from itertools import product + from helper import unittest, PillowTestCase from PIL import Image +_TGA_DIR = os.path.join("Tests", "images", "tga") +_TGA_DIR_COMMON = os.path.join(_TGA_DIR, "common") + + class TestFileTga(PillowTestCase): + _MODES = ("L", "LA", "P", "RGB", "RGBA") + _ORIGINS = ("tl", "bl") + + _ORIGIN_TO_ORIENTATION = { + "tl": 1, + "bl": -1 + } + + def test_sanity(self): + for mode in self._MODES: + png_paths = glob( + os.path.join( + _TGA_DIR_COMMON, "*x*_{}.png".format(mode.lower()))) + + for png_path in png_paths: + reference_im = Image.open(png_path) + self.assertEqual(reference_im.mode, mode) + + path_no_ext = os.path.splitext(png_path)[0] + for origin, rle in product(self._ORIGINS, (True, False)): + tga_path = "{}_{}_{}.tga".format( + path_no_ext, origin, "rle" if rle else "raw") + + original_im = Image.open(tga_path) + if rle: + self.assertEqual( + original_im.info["compression"], "tga_rle") + self.assertEqual( + original_im.info["orientation"], + self._ORIGIN_TO_ORIENTATION[origin]) + if mode == "P": + self.assertEqual( + original_im.getpalette(), + reference_im.getpalette()) + + self.assert_image_equal(original_im, reference_im) + + # Generate a new test name every time so the + # test will not fail with permission error + # on Windows. + test_file = self.tempfile("temp.tga") + + original_im.save(test_file, rle=rle) + saved_im = Image.open(test_file) + if rle: + self.assertEqual( + saved_im.info["compression"], + original_im.info["compression"]) + self.assertEqual( + saved_im.info["orientation"], + original_im.info["orientation"]) + if mode == "P": + self.assertEqual( + saved_im.getpalette(), + original_im.getpalette()) + + self.assert_image_equal(saved_im, original_im) + def test_id_field(self): # tga file with id field test_file = "Tests/images/tga_id_field.tga" @@ -41,9 +107,6 @@ class TestFileTga(PillowTestCase): test_im = Image.open(test_file) self.assertEqual(test_im.size, (100, 100)) - # Unsupported mode save - self.assertRaises(IOError, lambda: im.convert("LA").save(test_file)) - def test_save_rle(self): test_file = "Tests/images/rgb32rle.tga" im = Image.open(test_file) @@ -60,8 +123,25 @@ class TestFileTga(PillowTestCase): test_im = Image.open(test_file) self.assertEqual(test_im.size, (199, 199)) - # Unsupported mode save - self.assertRaises(IOError, lambda: im.convert("LA").save(test_file)) + def test_save_l_transparency(self): + # There are 559 transparent pixels in la.tga. + num_transparent = 559 + + in_file = "Tests/images/la.tga" + im = Image.open(in_file) + self.assertEqual(im.mode, "LA") + self.assertEqual( + im.getchannel("A").getcolors()[0][0], num_transparent) + + test_file = self.tempfile("temp.tga") + im.save(test_file) + + test_im = Image.open(test_file) + self.assertEqual(test_im.mode, "LA") + self.assertEqual( + test_im.getchannel("A").getcolors()[0][0], num_transparent) + + self.assert_image_equal(im, test_im) if __name__ == '__main__': diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py index 334f5c96b..79630c773 100644 --- a/Tests/test_file_tiff.py +++ b/Tests/test_file_tiff.py @@ -3,9 +3,10 @@ from io import BytesIO import struct import sys -from helper import unittest, PillowTestCase, hopper, py3 +from helper import unittest, PillowTestCase, hopper from PIL import Image, TiffImagePlugin +from PIL._util import py3 from PIL.TiffImagePlugin import X_RESOLUTION, Y_RESOLUTION, RESOLUTION_UNIT logger = logging.getLogger(__name__) @@ -62,8 +63,11 @@ class TestFileTiff(PillowTestCase): im.load() def test_set_legacy_api(self): - with self.assertRaises(Exception): - ImageFileDirectory_v2.legacy_api = None + ifd = TiffImagePlugin.ImageFileDirectory_v2() + with self.assertRaises(Exception) as e: + ifd.legacy_api = None + self.assertEqual(str(e.exception), + "Not allowing setting of legacy api") def test_xyres_tiff(self): filename = "Tests/images/pil168.tif" @@ -440,7 +444,8 @@ class TestFileTiff(PillowTestCase): for im in ims: yield im mp = io.BytesIO() - im.save(mp, format="TIFF", save_all=True, append_images=imGenerator(ims)) + im.save(mp, format="TIFF", save_all=True, + append_images=imGenerator(ims)) mp.seek(0, os.SEEK_SET) reread = Image.open(mp) @@ -501,7 +506,7 @@ class TestFileTiffW32(PillowTestCase): im = Image.open(tmpfile) fp = im.fp self.assertFalse(fp.closed) - self.assertRaises(Exception, os.remove, tmpfile) + self.assertRaises(WindowsError, os.remove, tmpfile) im.load() self.assertTrue(fp.closed) diff --git a/Tests/test_file_wal.py b/Tests/test_file_wal.py new file mode 100644 index 000000000..13b1e3a2f --- /dev/null +++ b/Tests/test_file_wal.py @@ -0,0 +1,23 @@ +from helper import unittest, PillowTestCase + +from PIL import WalImageFile + + +class TestFileWal(PillowTestCase): + + def test_open(self): + # Arrange + TEST_FILE = "Tests/images/hopper.wal" + + # Act + im = WalImageFile.open(TEST_FILE) + + # Assert + self.assertEqual(im.format, "WAL") + self.assertEqual(im.format_description, "Quake2 Texture") + self.assertEqual(im.mode, "P") + self.assertEqual(im.size, (128, 128)) + + +if __name__ == '__main__': + unittest.main() diff --git a/Tests/test_file_webp_animated.py b/Tests/test_file_webp_animated.py index f98cde764..6b3dc1622 100644 --- a/Tests/test_file_webp_animated.py +++ b/Tests/test_file_webp_animated.py @@ -83,7 +83,8 @@ class TestFileWebpAnimation(PillowTestCase): temp_file1 = self.tempfile("temp.webp") frame1.copy().save(temp_file1, - save_all=True, append_images=[frame2], lossless=True) + save_all=True, append_images=[frame2], + lossless=True) check(temp_file1) # Tests appending using a generator @@ -92,7 +93,8 @@ class TestFileWebpAnimation(PillowTestCase): yield im temp_file2 = self.tempfile("temp_generator.webp") frame1.copy().save(temp_file2, - save_all=True, append_images=imGenerator([frame2]), lossless=True) + save_all=True, append_images=imGenerator([frame2]), + lossless=True) check(temp_file2) def test_timestamp_and_duration(self): diff --git a/Tests/test_font_leaks.py b/Tests/test_font_leaks.py index 4de28a95a..f1ce44e6d 100644 --- a/Tests/test_font_leaks.py +++ b/Tests/test_font_leaks.py @@ -16,7 +16,8 @@ class TestTTypeFontLeak(PillowLeakTestCase): self._test_leak(lambda: draw.text((0, 0), "some text "*1024, # ~10k font=font, fill="black")) - @unittest.skipIf(not features.check('freetype2'), "Test requires freetype2") + @unittest.skipIf(not features.check('freetype2'), + "Test requires freetype2") def test_leak(self): ttype = ImageFont.truetype('Tests/fonts/FreeMono.ttf', 20) self._test_font(ttype) diff --git a/Tests/test_font_pcf.py b/Tests/test_font_pcf.py index 2b344c6c2..20869505f 100644 --- a/Tests/test_font_pcf.py +++ b/Tests/test_font_pcf.py @@ -2,6 +2,7 @@ from helper import unittest, PillowTestCase from PIL import Image, FontFile, PcfFontFile from PIL import ImageFont, ImageDraw +from PIL._util import py3 codecs = dir(Image.core) @@ -20,7 +21,7 @@ class TestFontPcf(PillowTestCase): with open(fontname, "rb") as test_file: font = PcfFontFile.PcfFontFile(test_file) self.assertIsInstance(font, FontFile.FontFile) - #check the number of characters in the font + # check the number of characters in the font self.assertEqual(len([_f for _f in font.glyph if _f]), 223) tempname = self.tempfile("temp.pil") @@ -66,7 +67,7 @@ class TestFontPcf(PillowTestCase): def _test_high_characters(self, message): tempname = self.save_font() font = ImageFont.load(tempname) - im = Image.new("L", (750, 30) , "white") + im = Image.new("L", (750, 30), "white") draw = ImageDraw.Draw(im) draw.text((0, 0), message, "black", font=font) with Image.open('Tests/images/high_ascii_chars.png') as target: @@ -76,7 +77,7 @@ class TestFontPcf(PillowTestCase): 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: + if py3: self._test_high_characters(message.encode('latin1')) diff --git a/Tests/test_format_hsv.py b/Tests/test_format_hsv.py index 2cc54c910..31309da60 100644 --- a/Tests/test_format_hsv.py +++ b/Tests/test_format_hsv.py @@ -1,6 +1,7 @@ from helper import unittest, PillowTestCase, hopper from PIL import Image +from PIL._util import py3 import colorsys import itertools @@ -57,10 +58,10 @@ class TestFormatHSV(PillowTestCase): (r, g, b) = im.split() - if bytes is str: - conv_func = self.str_to_float - else: + if py3: conv_func = self.int_to_float + else: + conv_func = self.str_to_float if hasattr(itertools, 'izip'): iter_helper = itertools.izip @@ -72,11 +73,11 @@ class TestFormatHSV(PillowTestCase): 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 ( + if py3: + new_bytes = b''.join(bytes(chr(h)+chr(s)+chr(v), 'latin-1') for ( h, s, v) in converted) else: - new_bytes = b''.join(bytes(chr(h)+chr(s)+chr(v), 'latin-1') for ( + new_bytes = b''.join(chr(h)+chr(s)+chr(v) for ( h, s, v) in converted) hsv = Image.frombytes(mode, r.size, new_bytes) diff --git a/Tests/test_image.py b/Tests/test_image.py index 6f6d1983e..7222e389b 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -1,6 +1,7 @@ from helper import unittest, PillowTestCase, hopper from PIL import Image +from PIL._util import py3 import os @@ -60,12 +61,12 @@ class TestImage(PillowTestCase): self.assertEqual(im.height, 4) def test_invalid_image(self): - if str is bytes: - import StringIO - im = StringIO.StringIO('') - else: + if py3: import io im = io.BytesIO(b'') + else: + import StringIO + im = StringIO.StringIO('') self.assertRaises(IOError, Image.open, im) def test_bad_mode(self): @@ -112,7 +113,6 @@ class TestImage(PillowTestCase): self.assertRaises(ValueError, im.save, temp_file) def test_internals(self): - im = Image.new("L", (100, 100)) im.readonly = 1 im._copy() @@ -122,8 +122,15 @@ class TestImage(PillowTestCase): im.paste(0, (0, 0, 100, 100)) self.assertFalse(im.readonly) - test_file = self.tempfile("temp.ppm") - im._dump(test_file) + def test_dump(self): + im = Image.new("L", (10, 10)) + im._dump(self.tempfile("temp_L.ppm")) + + im = Image.new("RGB", (10, 10)) + im._dump(self.tempfile("temp_RGB.ppm")) + + im = Image.new("HSV", (10, 10)) + self.assertRaises(ValueError, im._dump, self.tempfile("temp_HSV.ppm")) def test_comparison_with_other_type(self): # Arrange diff --git a/Tests/test_image_access.py b/Tests/test_image_access.py index 155fa5c2e..7a9378bbd 100644 --- a/Tests/test_image_access.py +++ b/Tests/test_image_access.py @@ -294,8 +294,9 @@ int main(int argc, char* argv[]) compiler = ccompiler.new_compiler() compiler.add_include_dir(sysconfig.get_python_inc()) - libdir = sysconfig.get_config_var('LIBDIR') or sysconfig.get_python_inc().replace('include', 'libs') - print (libdir) + libdir = (sysconfig.get_config_var('LIBDIR') or + sysconfig.get_python_inc().replace('include', 'libs')) + print(libdir) compiler.add_library_dir(libdir) objects = compiler.compile(['embed_pil.c']) compiler.link_executable(objects, 'embed_pil') diff --git a/Tests/test_image_array.py b/Tests/test_image_array.py index 11c2648bb..7a86a3e54 100644 --- a/Tests/test_image_array.py +++ b/Tests/test_image_array.py @@ -15,9 +15,11 @@ class TestImageArray(PillowTestCase): self.assertEqual(test("L"), (3, (100, 128), '|u1', 12800)) # FIXME: wrong? - self.assertEqual(test("I"), (3, (100, 128), Image._ENDIAN + 'i4', 51200)) + self.assertEqual(test("I"), (3, (100, 128), + Image._ENDIAN + 'i4', 51200)) # FIXME: wrong? - self.assertEqual(test("F"), (3, (100, 128), Image._ENDIAN + 'f4', 51200)) + self.assertEqual(test("F"), (3, (100, 128), + Image._ENDIAN + 'f4', 51200)) self.assertEqual(test("LA"), (3, (100, 128, 2), '|u1', 25600)) self.assertEqual(test("RGB"), (3, (100, 128, 3), '|u1', 38400)) diff --git a/Tests/test_image_convert.py b/Tests/test_image_convert.py index c7c381382..1e208d80c 100644 --- a/Tests/test_image_convert.py +++ b/Tests/test_image_convert.py @@ -67,13 +67,13 @@ class TestImageConvert(PillowTestCase): f = self.tempfile('temp.png') - l = im.convert('L') - self.assertEqual(l.info['transparency'], 0) # undone - l.save(f) + im_l = im.convert('L') + self.assertEqual(im_l.info['transparency'], 0) # undone + im_l.save(f) - rgb = im.convert('RGB') - self.assertEqual(rgb.info['transparency'], (0, 0, 0)) # undone - rgb.save(f) + im_rgb = im.convert('RGB') + self.assertEqual(im_rgb.info['transparency'], (0, 0, 0)) # undone + im_rgb.save(f) # ref https://github.com/python-pillow/Pillow/issues/664 @@ -83,12 +83,12 @@ class TestImageConvert(PillowTestCase): im.info['transparency'] = 128 # Act - rgba = im.convert('RGBA') + im_rgba = im.convert('RGBA') # Assert - self.assertNotIn('transparency', rgba.info) + self.assertNotIn('transparency', im_rgba.info) # https://github.com/python-pillow/Pillow/issues/2702 - self.assertEqual(rgba.palette, None) + self.assertEqual(im_rgba.palette, None) def test_trns_l(self): im = hopper('L') @@ -96,19 +96,20 @@ class TestImageConvert(PillowTestCase): f = self.tempfile('temp.png') - rgb = im.convert('RGB') - self.assertEqual(rgb.info['transparency'], (128, 128, 128)) # undone - rgb.save(f) + im_rgb = im.convert('RGB') + self.assertEqual(im_rgb.info['transparency'], + (128, 128, 128)) # undone + im_rgb.save(f) - p = im.convert('P') - self.assertIn('transparency', p.info) - p.save(f) + im_p = im.convert('P') + self.assertIn('transparency', im_p.info) + im_p.save(f) - p = self.assert_warning( + im_p = self.assert_warning( UserWarning, im.convert, 'P', palette=Image.ADAPTIVE) - self.assertNotIn('transparency', p.info) - p.save(f) + self.assertNotIn('transparency', im_p.info) + im_p.save(f) def test_trns_RGB(self): im = hopper('RGB') @@ -116,23 +117,35 @@ class TestImageConvert(PillowTestCase): f = self.tempfile('temp.png') - l = im.convert('L') - self.assertEqual(l.info['transparency'], l.getpixel((0, 0))) # undone - l.save(f) + im_l = im.convert('L') + self.assertEqual(im_l.info['transparency'], + im_l.getpixel((0, 0))) # undone + im_l.save(f) - p = im.convert('P') - self.assertIn('transparency', p.info) - p.save(f) + im_p = im.convert('P') + self.assertIn('transparency', im_p.info) + im_p.save(f) - p = im.convert('RGBA') - self.assertNotIn('transparency', p.info) - p.save(f) + im_rgba = im.convert('RGBA') + self.assertNotIn('transparency', im_rgba.info) + im_rgba.save(f) - p = self.assert_warning( + im_p = self.assert_warning( UserWarning, im.convert, 'P', palette=Image.ADAPTIVE) - self.assertNotIn('transparency', p.info) - p.save(f) + self.assertNotIn('transparency', im_p.info) + im_p.save(f) + + def test_gif_with_rgba_palette_to_p(self): + # See https://github.com/python-pillow/Pillow/issues/2433 + im = Image.open('Tests/images/hopper.gif') + im.info['transparency'] = 255 + im.load() + self.assertEqual(im.palette.mode, 'RGBA') + im_p = im.convert('P') + + # Should not raise ValueError: unrecognized raw mode + im_p.load() def test_p_la(self): im = hopper('RGBA') @@ -191,7 +204,8 @@ class TestImageConvert(PillowTestCase): if converted_im.mode == 'RGB': self.assert_image_similar(converted_im, target, 3) else: - self.assert_image_similar(converted_im, target.getchannel(0), 1) + self.assert_image_similar(converted_im, + target.getchannel(0), 1) matrix_convert('RGB') matrix_convert('L') diff --git a/Tests/test_image_getim.py b/Tests/test_image_getim.py index bc562de5a..1452e584e 100644 --- a/Tests/test_image_getim.py +++ b/Tests/test_image_getim.py @@ -1,4 +1,5 @@ -from helper import unittest, PillowTestCase, hopper, py3 +from helper import unittest, PillowTestCase, hopper +from PIL._util import py3 class TestImageGetIm(PillowTestCase): diff --git a/Tests/test_image_quantize.py b/Tests/test_image_quantize.py index 6a9ff1187..5b10d2de3 100644 --- a/Tests/test_image_quantize.py +++ b/Tests/test_image_quantize.py @@ -39,7 +39,7 @@ class TestImageQuantize(PillowTestCase): def test_rgba_quantize(self): image = hopper('RGBA') image.quantize() - self.assertRaises(Exception, image.quantize, method=0) + self.assertRaises(ValueError, image.quantize, method=0) def test_quantize(self): image = Image.open('Tests/images/caption_6_33_22.png').convert('RGB') diff --git a/Tests/test_image_resample.py b/Tests/test_image_resample.py index 4223db530..3687a1a2c 100644 --- a/Tests/test_image_resample.py +++ b/Tests/test_image_resample.py @@ -245,8 +245,8 @@ class CoreResampleAlphaCorrectTest(PillowTestCase): for y in range(i.size[1]): used_colors = {px[x, y][0] for x in range(i.size[0])} self.assertEqual(256, len(used_colors), - 'All colors should present in resized image. ' - 'Only {} on {} line.'.format(len(used_colors), y)) + 'All colors should present in resized image. ' + 'Only {} on {} line.'.format(len(used_colors), y)) @unittest.skip("current implementation isn't precise enough") def test_levels_rgba(self): @@ -288,10 +288,14 @@ class CoreResampleAlphaCorrectTest(PillowTestCase): def test_dirty_pixels_rgba(self): case = self.make_dirty_case('RGBA', (255, 255, 0, 128), (0, 0, 255, 0)) self.run_dirty_case(case.resize((20, 20), Image.BOX), (255, 255, 0)) - self.run_dirty_case(case.resize((20, 20), Image.BILINEAR), (255, 255, 0)) - self.run_dirty_case(case.resize((20, 20), Image.HAMMING), (255, 255, 0)) - self.run_dirty_case(case.resize((20, 20), Image.BICUBIC), (255, 255, 0)) - self.run_dirty_case(case.resize((20, 20), Image.LANCZOS), (255, 255, 0)) + self.run_dirty_case(case.resize((20, 20), Image.BILINEAR), + (255, 255, 0)) + self.run_dirty_case(case.resize((20, 20), Image.HAMMING), + (255, 255, 0)) + self.run_dirty_case(case.resize((20, 20), Image.BICUBIC), + (255, 255, 0)) + self.run_dirty_case(case.resize((20, 20), Image.LANCZOS), + (255, 255, 0)) def test_dirty_pixels_la(self): case = self.make_dirty_case('LA', (255, 128), (0, 0)) @@ -367,40 +371,45 @@ class CoreResampleCoefficientsTest(PillowTestCase): im = Image.new('RGBA', (1280, 1280), (0x20, 0x40, 0x60, 0xff)) histogram = im.resize((256, 256), Image.BICUBIC).histogram() - self.assertEqual(histogram[0x100 * 0 + 0x20], 0x10000) # first channel - self.assertEqual(histogram[0x100 * 1 + 0x40], 0x10000) # second channel - self.assertEqual(histogram[0x100 * 2 + 0x60], 0x10000) # third channel - self.assertEqual(histogram[0x100 * 3 + 0xff], 0x10000) # fourth channel + # first channel + self.assertEqual(histogram[0x100 * 0 + 0x20], 0x10000) + # second channel + self.assertEqual(histogram[0x100 * 1 + 0x40], 0x10000) + # third channel + self.assertEqual(histogram[0x100 * 2 + 0x60], 0x10000) + # fourth channel + self.assertEqual(histogram[0x100 * 3 + 0xff], 0x10000) class CoreResampleBoxTest(PillowTestCase): def test_wrong_arguments(self): im = hopper() - for resample in (Image.NEAREST, Image.BOX, Image.BILINEAR, Image.HAMMING, - Image.BICUBIC, Image.LANCZOS): + for resample in (Image.NEAREST, Image.BOX, Image.BILINEAR, + Image.HAMMING, Image.BICUBIC, Image.LANCZOS): im.resize((32, 32), resample, (0, 0, im.width, im.height)) im.resize((32, 32), resample, (20, 20, im.width, im.height)) im.resize((32, 32), resample, (20, 20, 20, 100)) im.resize((32, 32), resample, (20, 20, 100, 20)) - with self.assertRaisesRegexp(TypeError, "must be sequence of length 4"): + with self.assertRaisesRegex(TypeError, + "must be sequence of length 4"): im.resize((32, 32), resample, (im.width, im.height)) - with self.assertRaisesRegexp(ValueError, "can't be negative"): + with self.assertRaisesRegex(ValueError, "can't be negative"): im.resize((32, 32), resample, (-20, 20, 100, 100)) - with self.assertRaisesRegexp(ValueError, "can't be negative"): + with self.assertRaisesRegex(ValueError, "can't be negative"): im.resize((32, 32), resample, (20, -20, 100, 100)) - with self.assertRaisesRegexp(ValueError, "can't be empty"): + with self.assertRaisesRegex(ValueError, "can't be empty"): im.resize((32, 32), resample, (20.1, 20, 20, 100)) - with self.assertRaisesRegexp(ValueError, "can't be empty"): + with self.assertRaisesRegex(ValueError, "can't be empty"): im.resize((32, 32), resample, (20, 20.1, 100, 20)) - with self.assertRaisesRegexp(ValueError, "can't be empty"): + with self.assertRaisesRegex(ValueError, "can't be empty"): im.resize((32, 32), resample, (20.1, 20.1, 20, 20)) - with self.assertRaisesRegexp(ValueError, "can't exceed"): + with self.assertRaisesRegex(ValueError, "can't exceed"): im.resize((32, 32), resample, (0, 0, im.width + 1, im.height)) - with self.assertRaisesRegexp(ValueError, "can't exceed"): + with self.assertRaisesRegex(ValueError, "can't exceed"): im.resize((32, 32), resample, (0, 0, im.width, im.height + 1)) def resize_tiled(self, im, dst_size, xtiles, ytiles): @@ -447,7 +456,7 @@ class CoreResampleBoxTest(PillowTestCase): # error with box should be much smaller than without self.assert_image_similar(reference, with_box, 6) - with self.assertRaisesRegexp(AssertionError, "difference 29\."): + with self.assertRaisesRegex(AssertionError, "difference 29\."): self.assert_image_similar(reference, without_box, 5) def test_formats(self): @@ -490,7 +499,7 @@ class CoreResampleBoxTest(PillowTestCase): try: res = im.resize(size, Image.LANCZOS, box) self.assertEqual(res.size, size) - with self.assertRaisesRegexp(AssertionError, "difference \d"): + with self.assertRaisesRegex(AssertionError, "difference \d"): # check that the difference at least that much self.assert_image_similar(res, im.crop(box), 20) except AssertionError: diff --git a/Tests/test_image_resize.py b/Tests/test_image_resize.py index 8d14b9008..535f1d77e 100644 --- a/Tests/test_image_resize.py +++ b/Tests/test_image_resize.py @@ -39,15 +39,15 @@ class TestImagingCoreResize(PillowTestCase): self.assertEqual(r.im.bands, im.im.bands) def test_reduce_filters(self): - for f in [Image.NEAREST, Image.BOX, Image.BILINEAR, Image.HAMMING, - Image.BICUBIC, Image.LANCZOS]: + for f in [Image.NEAREST, Image.BOX, Image.BILINEAR, + Image.HAMMING, Image.BICUBIC, Image.LANCZOS]: r = self.resize(hopper("RGB"), (15, 12), f) self.assertEqual(r.mode, "RGB") self.assertEqual(r.size, (15, 12)) def test_enlarge_filters(self): - for f in [Image.NEAREST, Image.BOX, Image.BILINEAR, Image.HAMMING, - Image.BICUBIC, Image.LANCZOS]: + for f in [Image.NEAREST, Image.BOX, Image.BILINEAR, + Image.HAMMING, Image.BICUBIC, Image.LANCZOS]: r = self.resize(hopper("RGB"), (212, 195), f) self.assertEqual(r.mode, "RGB") self.assertEqual(r.size, (212, 195)) @@ -66,8 +66,8 @@ class TestImagingCoreResize(PillowTestCase): } samples['dirty'].putpixel((1, 1), 128) - for f in [Image.NEAREST, Image.BOX, Image.BILINEAR, Image.HAMMING, - Image.BICUBIC, Image.LANCZOS]: + for f in [Image.NEAREST, Image.BOX, Image.BILINEAR, + Image.HAMMING, Image.BICUBIC, Image.LANCZOS]: # samples resized with current filter references = { name: self.resize(ch, (4, 4), f) @@ -90,8 +90,8 @@ class TestImagingCoreResize(PillowTestCase): self.assert_image_equal(ch, references[channels[i]]) def test_enlarge_zero(self): - for f in [Image.NEAREST, Image.BOX, Image.BILINEAR, Image.HAMMING, - Image.BICUBIC, Image.LANCZOS]: + for f in [Image.NEAREST, Image.BOX, Image.BILINEAR, + Image.HAMMING, Image.BICUBIC, Image.LANCZOS]: r = self.resize(Image.new('RGB', (0, 0), "white"), (212, 195), f) self.assertEqual(r.mode, "RGB") self.assertEqual(r.size, (212, 195)) diff --git a/Tests/test_image_rotate.py b/Tests/test_image_rotate.py index fbcf9008d..17d394a02 100644 --- a/Tests/test_image_rotate.py +++ b/Tests/test_image_rotate.py @@ -95,8 +95,34 @@ class TestImageRotate(PillowTestCase): im = hopper() self.rotate(im, im.mode, 45, center=(0, 0)) self.rotate(im, im.mode, 45, translate=(im.size[0]/2, 0)) - self.rotate(im, im.mode, 45, center=(0, 0), translate=(im.size[0]/2, 0)) + self.rotate(im, im.mode, 45, center=(0, 0), + translate=(im.size[0]/2, 0)) + def test_rotate_no_fill(self): + im = Image.new('RGB', (100, 100), 'green') + target = Image.open('Tests/images/rotate_45_no_fill.png') + im = im.rotate(45) + self.assert_image_equal(im, target) + + def test_rotate_with_fill(self): + im = Image.new('RGB', (100, 100), 'green') + target = Image.open('Tests/images/rotate_45_with_fill.png') + im = im.rotate(45, fillcolor='white') + self.assert_image_equal(im, target) + + def test_alpha_rotate_no_fill(self): + # Alpha images are handled differently internally + im = Image.new('RGBA', (10, 10), 'green') + im = im.rotate(45, expand=1) + corner = im.getpixel((0,0)) + self.assertEqual(corner, (0, 0, 0, 0)) + + def test_alpha_rotate_with_fill(self): + # Alpha images are handled differently internally + im = Image.new('RGBA', (10, 10), 'green') + im = im.rotate(45, expand=1, fillcolor=(255, 0, 0, 255)) + corner = im.getpixel((0,0)) + self.assertEqual(corner, (255, 0, 0, 255)) if __name__ == '__main__': unittest.main() diff --git a/Tests/test_image_transform.py b/Tests/test_image_transform.py index a3ab7805f..add8cc5e7 100644 --- a/Tests/test_image_transform.py +++ b/Tests/test_image_transform.py @@ -53,15 +53,20 @@ class TestImageTransform(PillowTestCase): self.assert_image_equal(transformed, scaled) def test_fill(self): - im = hopper('RGB') - (w, h) = im.size - transformed = im.transform(im.size, Image.EXTENT, - (0, 0, - w*2, h*2), - Image.BILINEAR, - fillcolor = 'red') + for mode, pixel in [ + ['RGB', (255, 0, 0)], + ['RGBA', (255, 0, 0, 255)], + ['LA', (76, 0)] + ]: + im = hopper(mode) + (w, h) = im.size + transformed = im.transform(im.size, Image.EXTENT, + (0, 0, + w*2, h*2), + Image.BILINEAR, + fillcolor='red') - self.assertEqual(transformed.getpixel((w-1, h-1)), (255, 0, 0)) + self.assertEqual(transformed.getpixel((w-1, h-1)), pixel) def test_mesh(self): # this should be a checkerboard of halfsized hoppers in ul, lr diff --git a/Tests/test_imagecms.py b/Tests/test_imagecms.py index 31417564f..1fdfe916d 100644 --- a/Tests/test_imagecms.py +++ b/Tests/test_imagecms.py @@ -43,7 +43,7 @@ class TestImageCms(PillowTestCase): self.assertEqual(list(map(type, v)), [str, str, str, str]) # internal version number - self.assertRegexpMatches(ImageCms.core.littlecms_version, r"\d+\.\d+$") + self.assertRegex(ImageCms.core.littlecms_version, r"\d+\.\d+$") self.skip_missing() i = ImageCms.profileToProfile(hopper(), SRGB, SRGB) @@ -196,13 +196,13 @@ class TestImageCms(PillowTestCase): # 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) + l_data = i_lab.getdata(0) + a_data = i_lab.getdata(1) + b_data = i_lab.getdata(2) - self.assertEqual(list(l), [137] * 100) - self.assertEqual(list(a), [128] * 100) - self.assertEqual(list(b), [128] * 100) + self.assertEqual(list(l_data), [137] * 100) + self.assertEqual(list(a_data), [128] * 100) + self.assertEqual(list(b_data), [128] * 100) def test_lab_color(self): psRGB = ImageCms.createProfile("sRGB") @@ -286,53 +286,104 @@ class TestImageCms(PillowTestCase): self.assertEqual(truncate_tuple(tup1), truncate_tuple(tup2)) self.assertEqual(p.attributes, 4294967296) - assert_truncated_tuple_equal(p.blue_colorant, ((0.14306640625, 0.06060791015625, 0.7140960693359375), (0.1558847490315394, 0.06603820639433387, 0.06060791015625))) - assert_truncated_tuple_equal(p.blue_primary, ((0.14306641366715667, 0.06060790921083026, 0.7140960805782015), (0.15588475410450106, 0.06603820408959558, 0.06060790921083026))) + assert_truncated_tuple_equal( + p.blue_colorant, + ((0.14306640625, 0.06060791015625, 0.7140960693359375), + (0.1558847490315394, 0.06603820639433387, 0.06060791015625))) + assert_truncated_tuple_equal( + p.blue_primary, + ((0.14306641366715667, 0.06060790921083026, 0.7140960805782015), + (0.15588475410450106, 0.06603820408959558, 0.06060790921083026))) assert_truncated_tuple_equal(p.chromatic_adaptation, (((1.04791259765625, 0.0229339599609375, -0.050201416015625), (0.02960205078125, 0.9904632568359375, -0.0170745849609375), (-0.009246826171875, 0.0150604248046875, 0.7517852783203125)), ((1.0267159024652783, 0.022470062342089134, 0.0229339599609375), (0.02951378324103937, 0.9875098886387147, 0.9904632568359375), (-0.012205438066465256, 0.01987915407854985, 0.0150604248046875)))) self.assertIsNone(p.chromaticity) - self.assertEqual(p.clut, {0: (False, False, True), 1: (False, False, True), 2: (False, False, True), 3: (False, False, True)}) + self.assertEqual(p.clut, { + 0: (False, False, True), + 1: (False, False, True), + 2: (False, False, True), + 3: (False, False, True) + }) self.assertEqual(p.color_space, 'RGB') self.assertIsNone(p.colorant_table) self.assertIsNone(p.colorant_table_out) self.assertIsNone(p.colorimetric_intent) self.assertEqual(p.connection_space, 'XYZ ') - self.assertEqual(p.copyright, 'Copyright International Color Consortium, 2009') - self.assertEqual(p.creation_date, datetime.datetime(2009, 2, 27, 21, 36, 31)) + self.assertEqual(p.copyright, + 'Copyright International Color Consortium, 2009') + self.assertEqual(p.creation_date, + datetime.datetime(2009, 2, 27, 21, 36, 31)) self.assertEqual(p.device_class, 'mntr') - assert_truncated_tuple_equal(p.green_colorant, ((0.3851470947265625, 0.7168731689453125, 0.097076416015625), (0.32119769927720654, 0.5978443449048152, 0.7168731689453125))) - assert_truncated_tuple_equal(p.green_primary, ((0.3851470888162112, 0.7168731974161346, 0.09707641738998518), (0.32119768793686687, 0.5978443567149709, 0.7168731974161346))) + assert_truncated_tuple_equal( + p.green_colorant, + ((0.3851470947265625, 0.7168731689453125, 0.097076416015625), + (0.32119769927720654, 0.5978443449048152, 0.7168731689453125))) + assert_truncated_tuple_equal( + p.green_primary, + ((0.3851470888162112, 0.7168731974161346, 0.09707641738998518), + (0.32119768793686687, 0.5978443567149709, 0.7168731974161346))) self.assertEqual(p.header_flags, 0) self.assertEqual(p.header_manufacturer, '\x00\x00\x00\x00') self.assertEqual(p.header_model, '\x00\x00\x00\x00') - self.assertEqual(p.icc_measurement_condition, {'backing': (0.0, 0.0, 0.0), 'flare': 0.0, 'geo': 'unknown', 'observer': 1, 'illuminant_type': 'D65'}) + self.assertEqual(p.icc_measurement_condition, { + 'backing': (0.0, 0.0, 0.0), + 'flare': 0.0, + 'geo': 'unknown', + 'observer': 1, + 'illuminant_type': 'D65' + }) self.assertEqual(p.icc_version, 33554432) self.assertIsNone(p.icc_viewing_condition) - self.assertEqual(p.intent_supported, {0: (True, True, True), 1: (True, True, True), 2: (True, True, True), 3: (True, True, True)}) + self.assertEqual(p.intent_supported, { + 0: (True, True, True), + 1: (True, True, True), + 2: (True, True, True), + 3: (True, True, True) + }) self.assertTrue(p.is_matrix_shaper) self.assertEqual(p.luminance, ((0.0, 80.0, 0.0), (0.0, 1.0, 80.0))) self.assertIsNone(p.manufacturer) - assert_truncated_tuple_equal(p.media_black_point, ((0.012054443359375, 0.0124969482421875, 0.01031494140625), (0.34573304157549234, 0.35842450765864337, 0.0124969482421875))) - assert_truncated_tuple_equal(p.media_white_point, ((0.964202880859375, 1.0, 0.8249053955078125), (0.3457029219802284, 0.3585375327567059, 1.0))) - assert_truncated_tuple_equal((p.media_white_point_temperature,), (5000.722328847392,)) - self.assertEqual(p.model, 'IEC 61966-2-1 Default RGB Colour Space - sRGB') + assert_truncated_tuple_equal( + p.media_black_point, + ((0.012054443359375, 0.0124969482421875, 0.01031494140625), + (0.34573304157549234, 0.35842450765864337, 0.0124969482421875))) + assert_truncated_tuple_equal( + p.media_white_point, + ((0.964202880859375, 1.0, 0.8249053955078125), + (0.3457029219802284, 0.3585375327567059, 1.0))) + assert_truncated_tuple_equal( + (p.media_white_point_temperature,), + (5000.722328847392,)) + self.assertEqual(p.model, + 'IEC 61966-2-1 Default RGB Colour Space - sRGB') self.assertEqual(p.pcs, 'XYZ') self.assertIsNone(p.perceptual_rendering_intent_gamut) - self.assertEqual(p.product_copyright, 'Copyright International Color Consortium, 2009') + self.assertEqual(p.product_copyright, + 'Copyright International Color Consortium, 2009') self.assertEqual(p.product_desc, 'sRGB IEC61966-2-1 black scaled') - self.assertEqual(p.product_description, 'sRGB IEC61966-2-1 black scaled') + self.assertEqual(p.product_description, + 'sRGB IEC61966-2-1 black scaled') self.assertEqual(p.product_manufacturer, '') - self.assertEqual(p.product_model, 'IEC 61966-2-1 Default RGB Colour Space - sRGB') - self.assertEqual(p.profile_description, 'sRGB IEC61966-2-1 black scaled') - self.assertEqual(p.profile_id, b')\xf8=\xde\xaf\xf2U\xaexB\xfa\xe4\xca\x839\r') - assert_truncated_tuple_equal(p.red_colorant, ((0.436065673828125, 0.2224884033203125, 0.013916015625), (0.6484536316398539, 0.3308524880306778, 0.2224884033203125))) - assert_truncated_tuple_equal(p.red_primary, ((0.43606566581047446, 0.22248840582960838, 0.013916015621759925), (0.6484536250319214, 0.3308524944738204, 0.22248840582960838))) + self.assertEqual( + p.product_model, 'IEC 61966-2-1 Default RGB Colour Space - sRGB') + self.assertEqual( + p.profile_description, 'sRGB IEC61966-2-1 black scaled') + self.assertEqual( + p.profile_id, b')\xf8=\xde\xaf\xf2U\xaexB\xfa\xe4\xca\x839\r') + assert_truncated_tuple_equal( + p.red_colorant, + ((0.436065673828125, 0.2224884033203125, 0.013916015625), + (0.6484536316398539, 0.3308524880306778, 0.2224884033203125))) + assert_truncated_tuple_equal( + p.red_primary, + ((0.43606566581047446, 0.22248840582960838, 0.013916015621759925), + (0.6484536250319214, 0.3308524944738204, 0.22248840582960838))) self.assertEqual(p.rendering_intent, 0) self.assertIsNone(p.saturation_rendering_intent_gamut) self.assertIsNone(p.screening_description) self.assertIsNone(p.target) self.assertEqual(p.technology, 'CRT ') self.assertEqual(p.version, 2.0) - self.assertEqual(p.viewing_condition, 'Reference Viewing Condition in IEC 61966-2-1') + self.assertEqual(p.viewing_condition, + 'Reference Viewing Condition in IEC 61966-2-1') self.assertEqual(p.xcolor_space, 'RGB ') def test_profile_typesafety(self): @@ -346,7 +397,8 @@ class TestImageCms(PillowTestCase): with self.assertRaises(TypeError): ImageCms.ImageCmsProfile(1).tobytes() - def assert_aux_channel_preserved(self, mode, transform_in_place, preserved_channel): + def assert_aux_channel_preserved(self, mode, + transform_in_place, preserved_channel): def create_test_image(): # set up test image with something interesting in the tested aux # channel. @@ -379,29 +431,35 @@ class TestImageCms(PillowTestCase): # create some transform, it doesn't matter which one source_profile = ImageCms.createProfile("sRGB") destination_profile = ImageCms.createProfile("sRGB") - t = ImageCms.buildTransform(source_profile, destination_profile, inMode=mode, outMode=mode) + t = ImageCms.buildTransform( + source_profile, destination_profile, inMode=mode, outMode=mode) # apply transform if transform_in_place: ImageCms.applyTransform(source_image, t, inPlace=True) result_image = source_image else: - result_image = ImageCms.applyTransform(source_image, t, inPlace=False) + result_image = ImageCms.applyTransform( + source_image, t, inPlace=False) result_image_aux = result_image.getchannel(preserved_channel) self.assert_image_equal(source_image_aux, result_image_aux) def test_preserve_auxiliary_channels_rgba(self): - self.assert_aux_channel_preserved(mode='RGBA', transform_in_place=False, preserved_channel='A') + self.assert_aux_channel_preserved(mode='RGBA', + transform_in_place=False, preserved_channel='A') def test_preserve_auxiliary_channels_rgba_in_place(self): - self.assert_aux_channel_preserved(mode='RGBA', transform_in_place=True, preserved_channel='A') + self.assert_aux_channel_preserved(mode='RGBA', + transform_in_place=True, preserved_channel='A') def test_preserve_auxiliary_channels_rgbx(self): - self.assert_aux_channel_preserved(mode='RGBX', transform_in_place=False, preserved_channel='X') + self.assert_aux_channel_preserved(mode='RGBX', + transform_in_place=False, preserved_channel='X') def test_preserve_auxiliary_channels_rgbx_in_place(self): - self.assert_aux_channel_preserved(mode='RGBX', transform_in_place=True, preserved_channel='X') + self.assert_aux_channel_preserved(mode='RGBX', + transform_in_place=True, preserved_channel='X') def test_auxiliary_channels_isolated(self): # test data in aux channels does not affect non-aux channels @@ -422,20 +480,29 @@ class TestImageCms(PillowTestCase): source_profile = ImageCms.createProfile(src_format[1]) destination_profile = ImageCms.createProfile(dst_format[1]) source_image = src_format[3] - test_transform = ImageCms.buildTransform(source_profile, destination_profile, inMode=src_format[0], outMode=dst_format[0]) + test_transform = ImageCms.buildTransform( + source_profile, destination_profile, + inMode=src_format[0], outMode=dst_format[0]) # test conversion from aux-ful source if transform_in_place: test_image = source_image.copy() - ImageCms.applyTransform(test_image, test_transform, inPlace=True) + ImageCms.applyTransform( + test_image, test_transform, inPlace=True) else: - test_image = ImageCms.applyTransform(source_image, test_transform, inPlace=False) + test_image = ImageCms.applyTransform( + source_image, test_transform, inPlace=False) # reference conversion from aux-less source - reference_transform = ImageCms.buildTransform(source_profile, destination_profile, inMode=src_format[2], outMode=dst_format[2]) - reference_image = ImageCms.applyTransform(source_image.convert(src_format[2]), reference_transform) + reference_transform = ImageCms.buildTransform( + source_profile, destination_profile, + inMode=src_format[2], outMode=dst_format[2]) + reference_image = ImageCms.applyTransform( + source_image.convert(src_format[2]), + reference_transform) - self.assert_image_equal(test_image.convert(dst_format[2]), reference_image) + self.assert_image_equal(test_image.convert(dst_format[2]), + reference_image) if __name__ == '__main__': diff --git a/Tests/test_imagecolor.py b/Tests/test_imagecolor.py index 64e88cf9c..1ea37544b 100644 --- a/Tests/test_imagecolor.py +++ b/Tests/test_imagecolor.py @@ -31,7 +31,8 @@ class TestImageColor(PillowTestCase): # case insensitivity self.assertEqual(ImageColor.getrgb("#DEF"), ImageColor.getrgb("#def")) - self.assertEqual(ImageColor.getrgb("#CDEF"), ImageColor.getrgb("#cdef")) + self.assertEqual(ImageColor.getrgb("#CDEF"), + ImageColor.getrgb("#cdef")) self.assertEqual(ImageColor.getrgb("#DEFDEF"), ImageColor.getrgb("#defdef")) self.assertEqual(ImageColor.getrgb("#CDEFCDEF"), @@ -78,6 +79,26 @@ class TestImageColor(PillowTestCase): self.assertEqual((255, 0, 0), ImageColor.getrgb("hsl(360,100%,50%)")) self.assertEqual((0, 255, 255), ImageColor.getrgb("hsl(180,100%,50%)")) + self.assertEqual((255, 0, 0), ImageColor.getrgb("hsv(0,100%,100%)")) + self.assertEqual((255, 0, 0), ImageColor.getrgb("hsv(360,100%,100%)")) + self.assertEqual((0, 255, 255), + ImageColor.getrgb("hsv(180,100%,100%)")) + + # alternate format + self.assertEqual(ImageColor.getrgb("hsb(0,100%,50%)"), + ImageColor.getrgb("hsv(0,100%,50%)")) + + # floats + self.assertEqual((254, 3, 3), + ImageColor.getrgb("hsl(0.1,99.2%,50.3%)")) + self.assertEqual((255, 0, 0), + ImageColor.getrgb("hsl(360.,100.0%,50%)")) + + self.assertEqual((253, 2, 2), + ImageColor.getrgb("hsv(0.1,99.2%,99.3%)")) + self.assertEqual((255, 0, 0), + ImageColor.getrgb("hsv(360.,100.0%,100%)")) + # case insensitivity self.assertEqual(ImageColor.getrgb("RGB(255,0,0)"), ImageColor.getrgb("rgb(255,0,0)")) @@ -87,6 +108,10 @@ class TestImageColor(PillowTestCase): ImageColor.getrgb("rgba(255,0,0,0)")) self.assertEqual(ImageColor.getrgb("HSL(0,100%,50%)"), ImageColor.getrgb("hsl(0,100%,50%)")) + self.assertEqual(ImageColor.getrgb("HSV(0,100%,50%)"), + ImageColor.getrgb("hsv(0,100%,50%)")) + self.assertEqual(ImageColor.getrgb("HSB(0,100%,50%)"), + ImageColor.getrgb("hsb(0,100%,50%)")) # space agnosticism self.assertEqual((255, 0, 0), @@ -97,6 +122,8 @@ class TestImageColor(PillowTestCase): ImageColor.getrgb("rgba( 255 , 0 , 0 , 0 )")) self.assertEqual((255, 0, 0), ImageColor.getrgb("hsl( 0 , 100% , 50% )")) + self.assertEqual((255, 0, 0), + ImageColor.getrgb("hsv( 0 , 100% , 100% )")) # wrong number of components self.assertRaises(ValueError, ImageColor.getrgb, "rgb(255,0)") @@ -116,6 +143,12 @@ class TestImageColor(PillowTestCase): self.assertRaises(ValueError, ImageColor.getrgb, "hsl(0,100,50%)") self.assertRaises(ValueError, ImageColor.getrgb, "hsl(0,100%,50)") + self.assertRaises(ValueError, ImageColor.getrgb, "hsv(0,100%)") + self.assertRaises(ValueError, ImageColor.getrgb, "hsv(0,100%,0%,0%)") + self.assertRaises(ValueError, ImageColor.getrgb, "hsv(0%,100%,50%)") + self.assertRaises(ValueError, ImageColor.getrgb, "hsv(0,100,50%)") + self.assertRaises(ValueError, ImageColor.getrgb, "hsv(0,100%,50)") + # look for rounding errors (based on code by Tim Hatch) def test_rounding_errors(self): diff --git a/Tests/test_imagedraw.py b/Tests/test_imagedraw.py index a79a75ca0..33e7f8477 100644 --- a/Tests/test_imagedraw.py +++ b/Tests/test_imagedraw.py @@ -1,11 +1,7 @@ -from helper import unittest, PillowTestCase, hopper - -from PIL import Image -from PIL import ImageColor -from PIL import ImageDraw import os.path -import sys +from helper import PillowTestCase, hopper, unittest +from PIL import Image, ImageColor, ImageDraw BLACK = (0, 0, 0) WHITE = (255, 255, 255) @@ -173,6 +169,16 @@ class TestImageDraw(PillowTestCase): self.assert_image_similar( im, Image.open("Tests/images/imagedraw_ellipse_edge.png"), 1) + def test_ellipse_symmetric(self): + for bbox in [ + (25, 25, 76, 76), + (25, 25, 75, 75) + ]: + im = Image.new("RGB", (101, 101)) + draw = ImageDraw.Draw(im) + draw.ellipse(bbox, fill="green", outline="blue") + self.assert_image_equal(im, im.transpose(Image.FLIP_LEFT_RIGHT)) + def helper_line(self, points): # Arrange im = Image.new("RGB", (W, H)) @@ -360,8 +366,6 @@ class TestImageDraw(PillowTestCase): ImageDraw.floodfill(im, (W, H), red) self.assert_image_equal(im, im_floodfill) - @unittest.skipIf(hasattr(sys, 'pypy_version_info'), - "Causes fatal RPython error on PyPy") def test_floodfill_border(self): # floodfill() is experimental @@ -572,6 +576,47 @@ class TestImageDraw(PillowTestCase): draw.textsize("\n") draw.textsize("test\n") + def test_same_color_outline(self): + # Prepare shape + x0, y0 = 5, 5 + x1, y1 = 5, 50 + x2, y2 = 95, 50 + x3, y3 = 95, 5 + + s = ImageDraw.Outline() + s.move(x0, y0) + s.curve(x1, y1, x2, y2, x3, y3) + s.line(x0, y0) + + # Begin + for mode in ["RGB", "L"]: + for fill, outline in [ + ["red", None], + ["red", "red"], + ["red", "#f00"] + ]: + for operation, args in { + 'chord':[BBOX1, 0, 180], + 'ellipse':[BBOX1], + 'shape':[s], + 'pieslice':[BBOX1, -90, 45], + 'polygon':[[(18, 30), (85, 30), (60, 72)]], + 'rectangle':[BBOX1] + }.items(): + # Arrange + im = Image.new(mode, (W, H)) + draw = ImageDraw.Draw(im) + + # Act + draw_method = getattr(draw, operation) + args += [fill, outline] + draw_method(*args) + + # Assert + expected = ("Tests/images/imagedraw_outline" + "_{}_{}.png".format(operation, mode)) + self.assert_image_similar(im, Image.open(expected), 1) + if __name__ == '__main__': unittest.main() diff --git a/Tests/test_imagedraw2.py b/Tests/test_imagedraw2.py new file mode 100644 index 000000000..c5faeb616 --- /dev/null +++ b/Tests/test_imagedraw2.py @@ -0,0 +1,229 @@ +import os.path + +from helper import PillowTestCase, hopper, unittest +from PIL import Image, ImageDraw2, features + +BLACK = (0, 0, 0) +WHITE = (255, 255, 255) +GRAY = (190, 190, 190) +DEFAULT_MODE = "RGB" +IMAGES_PATH = os.path.join("Tests", "images", "imagedraw") + +# Image size +W, H = 100, 100 + +# Bounding box points +X0 = int(W / 4) +X1 = int(X0 * 3) +Y0 = int(H / 4) +Y1 = int(X0 * 3) + +# Two kinds of bounding box +BBOX1 = [(X0, Y0), (X1, Y1)] +BBOX2 = [X0, Y0, X1, Y1] + +# Two kinds of coordinate sequences +POINTS1 = [(10, 10), (20, 40), (30, 30)] +POINTS2 = [10, 10, 20, 40, 30, 30] + +KITE_POINTS = [(10, 50), (70, 10), (90, 50), (70, 90), (10, 50)] + +HAS_FREETYPE = features.check("freetype2") +FONT_PATH = "Tests/fonts/FreeMono.ttf" + + +class TestImageDraw(PillowTestCase): + + def test_sanity(self): + im = hopper("RGB").copy() + + draw = ImageDraw2.Draw(im) + pen = ImageDraw2.Pen("blue", width=7) + draw.line(list(range(10)), pen) + + from PIL import ImageDraw + + draw, handler = ImageDraw.getdraw(im) + pen = ImageDraw2.Pen("blue", width=7) + draw.line(list(range(10)), pen) + + def helper_ellipse(self, mode, bbox): + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw2.Draw(im) + pen = ImageDraw2.Pen("blue", width=2) + brush = ImageDraw2.Brush("green") + expected = "Tests/images/imagedraw_ellipse_{}.png".format(mode) + + # Act + draw.ellipse(bbox, pen, brush) + + # Assert + self.assert_image_similar(im, Image.open(expected), 1) + + def test_ellipse1(self): + self.helper_ellipse("RGB", BBOX1) + + def test_ellipse2(self): + self.helper_ellipse("RGB", BBOX2) + + def test_ellipse_edge(self): + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw2.Draw(im) + brush = ImageDraw2.Brush("white") + + # Act + draw.ellipse(((0, 0), (W-1, H)), brush) + + # Assert + self.assert_image_similar( + im, Image.open("Tests/images/imagedraw_ellipse_edge.png"), 1) + + def helper_line(self, points): + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw2.Draw(im) + pen = ImageDraw2.Pen("yellow", width=2) + + # Act + draw.line(points, pen) + + # Assert + self.assert_image_equal( + im, Image.open("Tests/images/imagedraw_line.png")) + + def test_line1_pen(self): + self.helper_line(POINTS1) + + def test_line2_pen(self): + self.helper_line(POINTS2) + + def test_line_pen_as_brush(self): + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw2.Draw(im) + pen = None + brush = ImageDraw2.Pen("yellow", width=2) + + # Act + # Pass in the pen as the brush parameter + draw.line(POINTS1, pen, brush) + + # Assert + self.assert_image_equal( + im, Image.open("Tests/images/imagedraw_line.png")) + + def helper_polygon(self, points): + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw2.Draw(im) + pen = ImageDraw2.Pen("blue", width=2) + brush = ImageDraw2.Brush("red") + + # Act + draw.polygon(points, pen, brush) + + # 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 = ImageDraw2.Draw(im) + pen = ImageDraw2.Pen("green", width=2) + brush = ImageDraw2.Brush("black") + + # Act + draw.rectangle(bbox, pen, brush) + + # 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_big_rectangle(self): + # Test drawing a rectangle bigger than the image + # Arrange + im = Image.new("RGB", (W, H)) + bbox = [(-1, -1), (W + 1, H + 1)] + brush = ImageDraw2.Brush("orange") + draw = ImageDraw2.Draw(im) + expected = "Tests/images/imagedraw_big_rectangle.png" + + # Act + draw.rectangle(bbox, brush) + + # Assert + self.assert_image_similar(im, Image.open(expected), 1) + + @unittest.skipUnless(HAS_FREETYPE, "ImageFont not available") + def test_text(self): + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw2.Draw(im) + font = ImageDraw2.Font("white", FONT_PATH) + expected = "Tests/images/imagedraw2_text.png" + + # Act + draw.text((5, 5), "ImageDraw2", font) + + # Assert + self.assert_image_similar(im, Image.open(expected), 13) + + @unittest.skipUnless(HAS_FREETYPE, "ImageFont not available") + def test_textsize(self): + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw2.Draw(im) + font = ImageDraw2.Font("white", FONT_PATH) + + # Act + size = draw.textsize("ImageDraw2", font) + + # Assert + self.assertEqual(size[1], 12) + + @unittest.skipUnless(HAS_FREETYPE, "ImageFont not available") + def test_textsize_empty_string(self): + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw2.Draw(im) + font = ImageDraw2.Font("white", FONT_PATH) + + # Act + # Should not cause 'SystemError: returned NULL without setting an error' + draw.textsize("", font) + draw.textsize("\n", font) + draw.textsize("test\n", font) + + @unittest.skipUnless(HAS_FREETYPE, "ImageFont not available") + def test_flush(self): + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw2.Draw(im) + font = ImageDraw2.Font("white", FONT_PATH) + + # Act + draw.text((5, 5), "ImageDraw2", font) + im2 = draw.flush() + + # Assert + self.assert_image_equal(im, im2) + + +if __name__ == "__main__": + unittest.main() diff --git a/Tests/test_imageenhance.py b/Tests/test_imageenhance.py index e9727613b..54288f4db 100644 --- a/Tests/test_imageenhance.py +++ b/Tests/test_imageenhance.py @@ -45,8 +45,9 @@ class TestImageEnhance(PillowTestCase): for op in ['Color', 'Brightness', 'Contrast', 'Sharpness']: for amount in [0, 0.5, 1.0]: - self._check_alpha(getattr(ImageEnhance, op)(original).enhance(amount), - original, op, amount) + self._check_alpha( + getattr(ImageEnhance, op)(original).enhance(amount), + original, op, amount) if __name__ == '__main__': diff --git a/Tests/test_imagefile.py b/Tests/test_imagefile.py index 32e44a0e1..837e81d30 100644 --- a/Tests/test_imagefile.py +++ b/Tests/test_imagefile.py @@ -215,14 +215,25 @@ class TestPyDecoder(PillowTestCase): buf = BytesIO(b'\x00'*255) im = MockImageFile(buf) - im.tile = [("MOCK", (xoff, yoff, xoff+xsize + 100, yoff+ysize), 32, None)] + im.tile = [ + ("MOCK", (xoff, yoff, xoff+xsize + 100, yoff+ysize), 32, None) + ] d = self.get_decoder() self.assertRaises(ValueError, im.load) - im.tile = [("MOCK", (xoff, yoff, xoff+xsize, yoff+ysize + 100), 32, None)] + im.tile = [ + ("MOCK", (xoff, yoff, xoff+xsize, yoff+ysize + 100), 32, None) + ] self.assertRaises(ValueError, im.load) + def test_no_format(self): + buf = BytesIO(b'\x00'*255) + + im = MockImageFile(buf) + self.assertIsNone(im.format) + self.assertIsNone(im.get_format_mimetype()) + if __name__ == '__main__': unittest.main() diff --git a/Tests/test_imagefont.py b/Tests/test_imagefont.py index 0ec49384e..f2116bdc4 100644 --- a/Tests/test_imagefont.py +++ b/Tests/test_imagefont.py @@ -42,7 +42,7 @@ class SimplePatcher(object): delattr(self._parent_obj, self._attr_name) -@unittest.skipUnless(HAS_FREETYPE, "ImageFont not Available") +@unittest.skipUnless(HAS_FREETYPE, "ImageFont not available") class TestImageFont(PillowTestCase): LAYOUT_ENGINE = ImageFont.LAYOUT_BASIC @@ -67,16 +67,18 @@ class TestImageFont(PillowTestCase): } def setUp(self): - freetype_version = tuple(ImageFont.core.freetype2_version.split('.'))[:2] - self.metrics = self.METRICS.get(freetype_version, self.METRICS['Default']) + freetype_version = tuple( + ImageFont.core.freetype2_version.split('.') + )[:2] + self.metrics = self.METRICS.get(freetype_version, + self.METRICS['Default']) def get_font(self): return ImageFont.truetype(FONT_PATH, FONT_SIZE, layout_engine=self.LAYOUT_ENGINE) def test_sanity(self): - self.assertRegexpMatches( - ImageFont.core.freetype2_version, r"\d+\.\d+\.\d+$") + self.assertRegex(ImageFont.core.freetype2_version, r"\d+\.\d+\.\d+$") def test_font_properties(self): ttf = self.get_font() @@ -203,7 +205,8 @@ class TestImageFont(PillowTestCase): target_img = Image.open(target) # Epsilon ~.5 fails with FreeType 2.7 - self.assert_image_similar(im, target_img, self.metrics['multiline']) + self.assert_image_similar(im, target_img, + self.metrics['multiline']) def test_unknown_align(self): im = Image.new(mode='RGB', size=(300, 100)) @@ -211,10 +214,9 @@ class TestImageFont(PillowTestCase): ttf = self.get_font() # Act/Assert - self.assertRaises(AssertionError, - draw.multiline_text, (0, 0), TEST_TEXT, - font=ttf, - align="unknown") + self.assertRaises( + AssertionError, + draw.multiline_text, (0, 0), TEST_TEXT, font=ttf, align="unknown") def test_draw_align(self): im = Image.new('RGB', (300, 100), 'white') @@ -232,6 +234,11 @@ class TestImageFont(PillowTestCase): self.assertEqual(draw.textsize(TEST_TEXT, font=ttf), draw.multiline_textsize(TEST_TEXT, font=ttf)) + # Test that multiline_textsize corresponds to ImageFont.textsize() + # for single line text + self.assertEqual(ttf.getsize('A'), + draw.multiline_textsize('A', font=ttf)) + # Test that textsize() can pass on additional arguments # to multiline_textsize() draw.textsize(TEST_TEXT, font=ttf, spacing=4) @@ -409,7 +416,7 @@ class TestImageFont(PillowTestCase): im = Image.new(mode='RGB', size=(300, 100)) target = im.copy() draw = ImageDraw.Draw(im) - #should not crash here. + # should not crash here. draw.text((10, 10), '', font=font) self.assert_image_equal(im, target) @@ -424,7 +431,8 @@ class TestImageFont(PillowTestCase): # Make a copy of FreeTypeFont so we can patch the original free_type_font = copy.deepcopy(ImageFont.FreeTypeFont) with SimplePatcher(ImageFont, '_FreeTypeFont', free_type_font): - def loadable_font(filepath, size, index, encoding, *args, **kwargs): + def loadable_font(filepath, size, index, encoding, + *args, **kwargs): if filepath == path_to_fake: return ImageFont._FreeTypeFont(FONT_PATH, size, index, encoding, *args, **kwargs) @@ -510,6 +518,12 @@ class TestImageFont(PillowTestCase): self.assertEqual(t.getsize('M'), self.metrics['getters']) self.assertEqual(t.getsize('y'), (12, 20)) self.assertEqual(t.getsize('a'), (12, 16)) + self.assertEqual(t.getsize_multiline('A'), (12, 16)) + self.assertEqual(t.getsize_multiline('AB'), (24, 16)) + self.assertEqual(t.getsize_multiline('a'), (12, 16)) + self.assertEqual(t.getsize_multiline('ABC\n'), (36, 36)) + self.assertEqual(t.getsize_multiline('ABC\nA'), (36, 36)) + self.assertEqual(t.getsize_multiline('ABC\nAaaa'), (48, 36)) @unittest.skipUnless(HAS_RAQM, "Raqm not Available") diff --git a/Tests/test_imagefont_bitmap.py b/Tests/test_imagefont_bitmap.py index bee415fd7..8376728a2 100644 --- a/Tests/test_imagefont_bitmap.py +++ b/Tests/test_imagefont_bitmap.py @@ -19,7 +19,8 @@ class TestImageFontBitmap(PillowTestCase): font='Tests/fonts/DejaVuSans-bitmap.ttf', size=24) size_outline = font_outline.getsize(text) size_bitmap = font_bitmap.getsize(text) - size_final = max(size_outline[0], size_bitmap[0]), max(size_outline[1], size_bitmap[1]) + size_final = (max(size_outline[0], size_bitmap[0]), + max(size_outline[1], size_bitmap[1])) im_bitmap = Image.new('RGB', size_final, (255, 255, 255)) im_outline = im_bitmap.copy() draw_bitmap = ImageDraw.Draw(im_bitmap) diff --git a/Tests/test_imagefontctl.py b/Tests/test_imagefontctl.py index 89f9563f3..04432b14f 100644 --- a/Tests/test_imagefontctl.py +++ b/Tests/test_imagefontctl.py @@ -11,7 +11,7 @@ FONT_PATH = "Tests/fonts/DejaVuSans.ttf" class TestImagecomplextext(PillowTestCase): def test_english(self): - #smoke test, this should not fail + # smoke test, this should not fail ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) im = Image.new(mode='RGB', size=(300, 100)) draw = ImageDraw.Draw(im) @@ -30,7 +30,8 @@ class TestImagecomplextext(PillowTestCase): self.assert_image_similar(im, target_img, .5) def test_y_offset(self): - ttf = ImageFont.truetype("Tests/fonts/NotoNastaliqUrdu-Regular.ttf", FONT_SIZE) + ttf = ImageFont.truetype("Tests/fonts/NotoNastaliqUrdu-Regular.ttf", + FONT_SIZE) im = Image.new(mode='RGB', size=(300, 100)) draw = ImageDraw.Draw(im) @@ -70,7 +71,8 @@ class TestImagecomplextext(PillowTestCase): im = Image.new(mode='RGB', size=(300, 100)) draw = ImageDraw.Draw(im) - draw.text((0, 0), 'سلطنة عمان Oman', font=ttf, fill=500, direction='ltr') + draw.text((0, 0), 'سلطنة عمان Oman', + font=ttf, fill=500, direction='ltr') target = 'Tests/images/test_direction_ltr.png' target_img = Image.open(target) @@ -82,7 +84,8 @@ class TestImagecomplextext(PillowTestCase): im = Image.new(mode='RGB', size=(300, 100)) draw = ImageDraw.Draw(im) - draw.text((0, 0), 'Oman سلطنة عمان', font=ttf, fill=500, direction='rtl') + draw.text((0, 0), 'Oman سلطنة عمان', + font=ttf, fill=500, direction='rtl') target = 'Tests/images/test_direction_ltr.png' target_img = Image.open(target) @@ -120,7 +123,8 @@ class TestImagecomplextext(PillowTestCase): im = Image.new(mode='RGB', size=(300, 100)) draw = ImageDraw.Draw(im) - draw.text((0, 0), 'اللغة العربية', font=ttf, fill=500, features=['-fina', '-init', '-medi']) + draw.text((0, 0), 'اللغة العربية', font=ttf, fill=500, + features=['-fina', '-init', '-medi']) target = 'Tests/images/test_arabictext_features.png' target_img = Image.open(target) diff --git a/Tests/test_imagegrab.py b/Tests/test_imagegrab.py index b2edffa57..87a6956f6 100644 --- a/Tests/test_imagegrab.py +++ b/Tests/test_imagegrab.py @@ -12,11 +12,6 @@ try: im = ImageGrab.grab() self.assert_image(im, im.mode, im.size) - @unittest.skipIf(on_appveyor(), "Test fails on appveyor") - def test_grab2(self): - im = ImageGrab.grab() - self.assert_image(im, im.mode, im.size) - except ImportError: class TestImageGrab(PillowTestCase): def test_skip(self): diff --git a/Tests/test_imagemorph.py b/Tests/test_imagemorph.py index b51b212e0..dceadebf4 100644 --- a/Tests/test_imagemorph.py +++ b/Tests/test_imagemorph.py @@ -1,8 +1,7 @@ # Test the ImageMorphology functionality from helper import unittest, PillowTestCase, hopper -from PIL import Image -from PIL import ImageMorph +from PIL import Image, ImageMorph, _imagingmorph class MorphTests(PillowTestCase): @@ -81,9 +80,15 @@ class MorphTests(PillowTestCase): def test_no_operator_loaded(self): mop = ImageMorph.MorphOp() - self.assertRaises(Exception, mop.apply, None) - self.assertRaises(Exception, mop.match, None) - self.assertRaises(Exception, mop.save_lut, None) + with self.assertRaises(Exception) as e: + mop.apply(None) + self.assertEqual(str(e.exception), 'No operator loaded') + with self.assertRaises(Exception) as e: + mop.match(None) + self.assertEqual(str(e.exception), 'No operator loaded') + with self.assertRaises(Exception) as e: + mop.save_lut(None) + self.assertEqual(str(e.exception), 'No operator loaded') # Test the named patterns def test_erosion8(self): @@ -214,9 +219,18 @@ class MorphTests(PillowTestCase): im = hopper('RGB') mop = ImageMorph.MorphOp(op_name="erosion8") - self.assertRaises(Exception, mop.apply, im) - self.assertRaises(Exception, mop.match, im) - self.assertRaises(Exception, mop.get_on_pixels, im) + with self.assertRaises(Exception) as e: + mop.apply(im) + self.assertEqual(str(e.exception), + 'Image must be binary, meaning it must use mode L') + with self.assertRaises(Exception) as e: + mop.match(im) + self.assertEqual(str(e.exception), + 'Image must be binary, meaning it must use mode L') + with self.assertRaises(Exception) as e: + mop.get_on_pixels(im) + self.assertEqual(str(e.exception), + 'Image must be binary, meaning it must use mode L') def test_add_patterns(self): # Arrange @@ -249,7 +263,11 @@ class MorphTests(PillowTestCase): lb.add_patterns(new_patterns) # Act / Assert - self.assertRaises(Exception, lb.build_lut) + with self.assertRaises(Exception) as e: + lb.build_lut() + self.assertEqual( + str(e.exception), + 'Syntax error in pattern "a pattern with a syntax error"') def test_load_invalid_mrl(self): # Arrange @@ -257,7 +275,10 @@ class MorphTests(PillowTestCase): mop = ImageMorph.MorphOp() # Act / Assert - self.assertRaises(Exception, mop.load_lut, invalid_mrl) + with self.assertRaises(Exception) as e: + mop.load_lut(invalid_mrl) + self.assertEqual(str(e.exception), + 'Wrong size operator file!') def test_roundtrip_mrl(self): # Arrange @@ -284,6 +305,23 @@ class MorphTests(PillowTestCase): # Assert self.assertEqual(mop.lut, lut) + def test_wrong_mode(self): + lut = ImageMorph.LutBuilder(op_name='corner').build_lut() + imrgb = Image.new('RGB', (10, 10)) + iml = Image.new('L', (10, 10)) + + with self.assertRaises(RuntimeError): + _imagingmorph.apply(bytes(lut), imrgb.im.id, iml.im.id) + + with self.assertRaises(RuntimeError): + _imagingmorph.apply(bytes(lut), iml.im.id, imrgb.im.id) + + with self.assertRaises(RuntimeError): + _imagingmorph.match(bytes(lut), imrgb.im.id) + + # Should not raise + _imagingmorph.match(bytes(lut), iml.im.id) + if __name__ == '__main__': unittest.main() diff --git a/Tests/test_imageops.py b/Tests/test_imageops.py index 11cf3619d..9c4da2463 100644 --- a/Tests/test_imageops.py +++ b/Tests/test_imageops.py @@ -1,6 +1,7 @@ from helper import unittest, PillowTestCase, hopper from PIL import ImageOps +from PIL import Image class TestImageOps(PillowTestCase): @@ -94,6 +95,107 @@ class TestImageOps(PillowTestCase): newimg = ImageOps.scale(i, 0.5) self.assertEqual(newimg.size, (25, 25)) + def test_colorize_2color(self): + # Test the colorizing function with 2-color functionality + + # Open test image (256px by 10px, black to white) + im = Image.open("Tests/images/bw_gradient.png") + im = im.convert("L") + + # Create image with original 2-color functionality + im_test = ImageOps.colorize(im, 'red', 'green') + + # Test output image (2-color) + left = (0, 1) + middle = (127, 1) + right = (255, 1) + self.assert_tuple_approx_equal(im_test.getpixel(left), + (255, 0, 0), + threshold=1, + msg='black test pixel incorrect') + self.assert_tuple_approx_equal(im_test.getpixel(middle), + (127, 63, 0), + threshold=1, + msg='mid test pixel incorrect') + self.assert_tuple_approx_equal(im_test.getpixel(right), + (0, 127, 0), + threshold=1, + msg='white test pixel incorrect') + + def test_colorize_2color_offset(self): + # Test the colorizing function with 2-color functionality and offset + + # Open test image (256px by 10px, black to white) + im = Image.open("Tests/images/bw_gradient.png") + im = im.convert("L") + + # Create image with original 2-color functionality with offsets + im_test = ImageOps.colorize(im, + black='red', + white='green', + blackpoint=50, + whitepoint=100) + + # Test output image (2-color) with offsets + left = (25, 1) + middle = (75, 1) + right = (125, 1) + self.assert_tuple_approx_equal(im_test.getpixel(left), + (255, 0, 0), + threshold=1, + msg='black test pixel incorrect') + self.assert_tuple_approx_equal(im_test.getpixel(middle), + (127, 63, 0), + threshold=1, + msg='mid test pixel incorrect') + self.assert_tuple_approx_equal(im_test.getpixel(right), + (0, 127, 0), + threshold=1, + msg='white test pixel incorrect') + + def test_colorize_3color_offset(self): + # Test the colorizing function with 3-color functionality and offset + + # Open test image (256px by 10px, black to white) + im = Image.open("Tests/images/bw_gradient.png") + im = im.convert("L") + + # Create image with new three color functionality with offsets + im_test = ImageOps.colorize(im, + black='red', + white='green', + mid='blue', + blackpoint=50, + whitepoint=200, + midpoint=100) + + # Test output image (3-color) with offsets + left = (25, 1) + left_middle = (75, 1) + middle = (100, 1) + right_middle = (150, 1) + right = (225, 1) + self.assert_tuple_approx_equal(im_test.getpixel(left), + (255, 0, 0), + threshold=1, + msg='black test pixel incorrect') + self.assert_tuple_approx_equal(im_test.getpixel(left_middle), + (127, 0, 127), + threshold=1, + msg='low-mid test pixel incorrect') + self.assert_tuple_approx_equal(im_test.getpixel(middle), + (0, 0, 255), + threshold=1, + msg='mid incorrect') + self.assert_tuple_approx_equal(im_test.getpixel(right_middle), + (0, 63, 127), + threshold=1, + msg='high-mid test pixel incorrect') + self.assert_tuple_approx_equal(im_test.getpixel(right), + (0, 127, 0), + threshold=1, + msg='white test pixel incorrect') + if __name__ == '__main__': unittest.main() diff --git a/Tests/test_imageops_usm.py b/Tests/test_imageops_usm.py index 3fb021fe7..20758e9f8 100644 --- a/Tests/test_imageops_usm.py +++ b/Tests/test_imageops_usm.py @@ -13,22 +13,25 @@ class TestImageOpsUsm(PillowTestCase): def test_ops_api(self): i = self.assert_warning(DeprecationWarning, - ImageOps.gaussian_blur, im, 2.0) + ImageOps.gaussian_blur, im, 2.0) + self.assertEqual(i.mode, "RGB") + self.assertEqual(i.size, (128, 128)) + + i = self.assert_warning(DeprecationWarning, ImageOps.box_blur, im, 1) + self.assertEqual(i.mode, "RGB") + self.assertEqual(i.size, (128, 128)) + + i = self.assert_warning(DeprecationWarning, ImageOps.gblur, im, 2.0) self.assertEqual(i.mode, "RGB") self.assertEqual(i.size, (128, 128)) i = self.assert_warning(DeprecationWarning, - ImageOps.gblur, im, 2.0) + ImageOps.unsharp_mask, im, 2.0, 125, 8) self.assertEqual(i.mode, "RGB") self.assertEqual(i.size, (128, 128)) i = self.assert_warning(DeprecationWarning, - ImageOps.unsharp_mask, im, 2.0, 125, 8) - self.assertEqual(i.mode, "RGB") - self.assertEqual(i.size, (128, 128)) - - i = self.assert_warning(DeprecationWarning, - ImageOps.usm, im, 2.0, 125, 8) + ImageOps.usm, im, 2.0, 125, 8) self.assertEqual(i.mode, "RGB") self.assertEqual(i.size, (128, 128)) diff --git a/Tests/test_imagepalette.py b/Tests/test_imagepalette.py index 889f022ae..3b7087d7a 100644 --- a/Tests/test_imagepalette.py +++ b/Tests/test_imagepalette.py @@ -9,7 +9,7 @@ class TestImagePalette(PillowTestCase): ImagePalette.ImagePalette("RGB", list(range(256))*3) self.assertRaises(ValueError, - ImagePalette.ImagePalette, "RGB", list(range(256))*2) + ImagePalette.ImagePalette, "RGB", list(range(256))*2) def test_getcolor(self): @@ -66,7 +66,7 @@ class TestImagePalette(PillowTestCase): # Act self.assertRaises(NotImplementedError, - ImagePalette.make_linear_lut, black, white) + ImagePalette.make_linear_lut, black, white) def test_make_gamma_lut(self): # Arrange @@ -133,7 +133,7 @@ class TestImagePalette(PillowTestCase): def test_invalid_palette(self): self.assertRaises(IOError, - ImagePalette.load, "Tests/images/hopper.jpg") + ImagePalette.load, "Tests/images/hopper.jpg") if __name__ == '__main__': diff --git a/Tests/test_imagepath.py b/Tests/test_imagepath.py index 14cc4d14b..8df71121d 100644 --- a/Tests/test_imagepath.py +++ b/Tests/test_imagepath.py @@ -1,6 +1,7 @@ from helper import unittest, PillowTestCase from PIL import ImagePath, Image +from PIL._util import py3 import array import struct @@ -17,6 +18,11 @@ class TestImagePath(PillowTestCase): 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)]) + with self.assertRaises(TypeError) as cm: + p['foo'] + self.assertEqual( + str(cm.exception), + "Path indices must be integers, not str") self.assertEqual( list(p), [(0.0, 1.0), (2.0, 3.0), (4.0, 5.0), (6.0, 7.0), (8.0, 9.0)]) @@ -72,10 +78,10 @@ class TestImagePath(PillowTestCase): # This fails due to the invalid malloc above, # and segfaults for i in range(200000): - if str is bytes: - x[i] = "0"*16 - else: + if py3: x[i] = b'0'*16 + else: + x[i] = "0"*16 class evil: diff --git a/Tests/test_imageqt.py b/Tests/test_imageqt.py index d3de5875b..cf9712ace 100644 --- a/Tests/test_imageqt.py +++ b/Tests/test_imageqt.py @@ -46,7 +46,7 @@ class PillowQPixmapTestCase(PillowQtTestCase): class TestImageQt(PillowQtTestCase, PillowTestCase): def test_rgb(self): - # from https://doc.qt.io/qt-4.8/qcolor.html + # from https://doc.qt.io/archives/qt-4.8/qcolor.html # typedef QRgb # An ARGB quadruplet on the format #AARRGGBB, # equivalent to an unsigned int. diff --git a/Tests/test_imagetk.py b/Tests/test_imagetk.py index fbf48a1b6..2df35c584 100644 --- a/Tests/test_imagetk.py +++ b/Tests/test_imagetk.py @@ -1,10 +1,14 @@ from helper import unittest, PillowTestCase, hopper from PIL import Image +from PIL._util import py3 try: from PIL import ImageTk - import Tkinter as tk + if py3: + import tkinter as tk + else: + import Tkinter as tk dir(ImageTk) HAS_TK = True except (OSError, ImportError) as v: diff --git a/Tests/test_lib_pack.py b/Tests/test_lib_pack.py index 002db2e19..e0fcf264a 100644 --- a/Tests/test_lib_pack.py +++ b/Tests/test_lib_pack.py @@ -34,7 +34,8 @@ class TestLibPack(PillowTestCase): self.assert_pack("1", "1;R", b'\xaa', 0, X, 0, X, 0, X, 0, X) self.assert_pack("1", "1;IR", b'\xaa', X, 0, X, 0, X, 0, X, 0) - self.assert_pack("1", "L", b'\xff\x00\x00\xff\x00\x00', X, 0, 0, X, 0, 0) + self.assert_pack( + "1", "L", b'\xff\x00\x00\xff\x00\x00', X, 0, 0, X, 0, 0) def test_L(self): self.assert_pack("L", "L", 1, 1, 2, 3, 4) @@ -57,14 +58,18 @@ class TestLibPack(PillowTestCase): def test_RGB(self): self.assert_pack("RGB", "RGB", 3, (1, 2, 3), (4, 5, 6), (7, 8, 9)) - self.assert_pack("RGB", "RGBX", + self.assert_pack( + "RGB", "RGBX", b'\x01\x02\x03\xff\x05\x06\x07\xff', (1, 2, 3), (5, 6, 7)) - self.assert_pack("RGB", "XRGB", + self.assert_pack( + "RGB", "XRGB", b'\x00\x02\x03\x04\x00\x06\x07\x08', (2, 3, 4), (6, 7, 8)) self.assert_pack("RGB", "BGR", 3, (3, 2, 1), (6, 5, 4), (9, 8, 7)) - self.assert_pack("RGB", "BGRX", + self.assert_pack( + "RGB", "BGRX", b'\x01\x02\x03\x00\x05\x06\x07\x00', (3, 2, 1), (7, 6, 5)) - self.assert_pack("RGB", "XBGR", + self.assert_pack( + "RGB", "XBGR", b'\x00\x02\x03\x04\x00\x06\x07\x08', (4, 3, 2), (8, 7, 6)) self.assert_pack("RGB", "RGB;L", 3, (1, 4, 7), (2, 5, 8), (3, 6, 9)) self.assert_pack("RGB", "R", 1, (1, 9, 9), (2, 9, 9), (3, 9, 9)) @@ -72,71 +77,98 @@ class TestLibPack(PillowTestCase): self.assert_pack("RGB", "B", 1, (9, 9, 1), (9, 9, 2), (9, 9, 3)) def test_RGBA(self): - self.assert_pack("RGBA", "RGBA", 4, - (1, 2, 3, 4), (5, 6, 7, 8), (9, 10, 11, 12)) - self.assert_pack("RGBA", "RGBA;L", 4, - (1, 4, 7, 10), (2, 5, 8, 11), (3, 6, 9, 12)) - self.assert_pack("RGBA", "RGB", 3, (1, 2, 3, 14), (4, 5, 6, 15), (7, 8, 9, 16)) - self.assert_pack("RGBA", "BGR", 3, (3, 2, 1, 14), (6, 5, 4, 15), (9, 8, 7, 16)) - self.assert_pack("RGBA", "BGRA", 4, + self.assert_pack( + "RGBA", "RGBA", 4, (1, 2, 3, 4), (5, 6, 7, 8), (9, 10, 11, 12)) + self.assert_pack( + "RGBA", "RGBA;L", 4, (1, 4, 7, 10), (2, 5, 8, 11), (3, 6, 9, 12)) + self.assert_pack( + "RGBA", "RGB", 3, (1, 2, 3, 14), (4, 5, 6, 15), (7, 8, 9, 16)) + self.assert_pack( + "RGBA", "BGR", 3, (3, 2, 1, 14), (6, 5, 4, 15), (9, 8, 7, 16)) + self.assert_pack( + "RGBA", "BGRA", 4, (3, 2, 1, 4), (7, 6, 5, 8), (11, 10, 9, 12)) - self.assert_pack("RGBA", "ABGR", 4, - (4, 3, 2, 1), (8, 7, 6, 5), (12, 11, 10, 9)) - self.assert_pack("RGBA", "BGRa", 4, + self.assert_pack( + "RGBA", "ABGR", 4, (4, 3, 2, 1), (8, 7, 6, 5), (12, 11, 10, 9)) + self.assert_pack( + "RGBA", "BGRa", 4, (191, 127, 63, 4), (223, 191, 159, 8), (233, 212, 191, 12)) - self.assert_pack("RGBA", "R", 1, (1, 0, 8, 9), (2, 0, 8, 9), (3, 0, 8, 0)) - self.assert_pack("RGBA", "G", 1, (6, 1, 8, 9), (6, 2, 8, 9), (6, 3, 8, 9)) - self.assert_pack("RGBA", "B", 1, (6, 7, 1, 9), (6, 7, 2, 0), (6, 7, 3, 9)) - self.assert_pack("RGBA", "A", 1, (6, 7, 0, 1), (6, 7, 0, 2), (0, 7, 0, 3)) + self.assert_pack( + "RGBA", "R", 1, (1, 0, 8, 9), (2, 0, 8, 9), (3, 0, 8, 0)) + self.assert_pack( + "RGBA", "G", 1, (6, 1, 8, 9), (6, 2, 8, 9), (6, 3, 8, 9)) + self.assert_pack( + "RGBA", "B", 1, (6, 7, 1, 9), (6, 7, 2, 0), (6, 7, 3, 9)) + self.assert_pack( + "RGBA", "A", 1, (6, 7, 0, 1), (6, 7, 0, 2), (0, 7, 0, 3)) def test_RGBa(self): - self.assert_pack("RGBa", "RGBa", 4, - (1, 2, 3, 4), (5, 6, 7, 8), (9, 10, 11, 12)) - self.assert_pack("RGBa", "BGRa", 4, - (3, 2, 1, 4), (7, 6, 5, 8), (11, 10, 9, 12)) - self.assert_pack("RGBa", "aBGR", 4, - (4, 3, 2, 1), (8, 7, 6, 5), (12, 11, 10, 9)) + self.assert_pack( + "RGBa", "RGBa", 4, (1, 2, 3, 4), (5, 6, 7, 8), (9, 10, 11, 12)) + self.assert_pack( + "RGBa", "BGRa", 4, (3, 2, 1, 4), (7, 6, 5, 8), (11, 10, 9, 12)) + self.assert_pack( + "RGBa", "aBGR", 4, (4, 3, 2, 1), (8, 7, 6, 5), (12, 11, 10, 9)) def test_RGBX(self): - self.assert_pack("RGBX", "RGBX", 4, (1, 2, 3, 4), (5, 6, 7, 8), (9, 10, 11, 12)) - self.assert_pack("RGBX", "RGBX;L", 4, (1, 4, 7, 10), (2, 5, 8, 11), (3, 6, 9, 12)) - self.assert_pack("RGBX", "RGB", 3, (1, 2, 3, X), (4, 5, 6, X), (7, 8, 9, X)) - self.assert_pack("RGBX", "BGR", 3, (3, 2, 1, X), (6, 5, 4, X), (9, 8, 7, X)) - self.assert_pack("RGBX", "BGRX", + self.assert_pack( + "RGBX", "RGBX", 4, (1, 2, 3, 4), (5, 6, 7, 8), (9, 10, 11, 12)) + self.assert_pack( + "RGBX", "RGBX;L", 4, (1, 4, 7, 10), (2, 5, 8, 11), (3, 6, 9, 12)) + self.assert_pack( + "RGBX", "RGB", 3, (1, 2, 3, X), (4, 5, 6, X), (7, 8, 9, X)) + self.assert_pack( + "RGBX", "BGR", 3, (3, 2, 1, X), (6, 5, 4, X), (9, 8, 7, X)) + self.assert_pack( + "RGBX", "BGRX", b'\x01\x02\x03\x00\x05\x06\x07\x00\t\n\x0b\x00', (3, 2, 1, X), (7, 6, 5, X), (11, 10, 9, X)) - self.assert_pack("RGBX", "XBGR", + self.assert_pack( + "RGBX", "XBGR", b'\x00\x02\x03\x04\x00\x06\x07\x08\x00\n\x0b\x0c', (4, 3, 2, X), (8, 7, 6, X), (12, 11, 10, X)) - self.assert_pack("RGBX", "R", 1, (1, 0, 8, 9), (2, 0, 8, 9), (3, 0, 8, 0)) - self.assert_pack("RGBX", "G", 1, (6, 1, 8, 9), (6, 2, 8, 9), (6, 3, 8, 9)) - self.assert_pack("RGBX", "B", 1, (6, 7, 1, 9), (6, 7, 2, 0), (6, 7, 3, 9)) - self.assert_pack("RGBX", "X", 1, (6, 7, 0, 1), (6, 7, 0, 2), (0, 7, 0, 3)) + self.assert_pack("RGBX", "R", 1, + (1, 0, 8, 9), (2, 0, 8, 9), (3, 0, 8, 0)) + self.assert_pack("RGBX", "G", 1, + (6, 1, 8, 9), (6, 2, 8, 9), (6, 3, 8, 9)) + self.assert_pack("RGBX", "B", 1, + (6, 7, 1, 9), (6, 7, 2, 0), (6, 7, 3, 9)) + self.assert_pack("RGBX", "X", 1, + (6, 7, 0, 1), (6, 7, 0, 2), (0, 7, 0, 3)) def test_CMYK(self): - self.assert_pack("CMYK", "CMYK", 4, (1, 2, 3, 4), (5, 6, 7, 8), (9, 10, 11, 12)) - self.assert_pack("CMYK", "CMYK;I", 4, + self.assert_pack("CMYK", "CMYK", 4, + (1, 2, 3, 4), (5, 6, 7, 8), (9, 10, 11, 12)) + self.assert_pack( + "CMYK", "CMYK;I", 4, (254, 253, 252, 251), (250, 249, 248, 247), (246, 245, 244, 243)) - self.assert_pack("CMYK", "CMYK;L", 4, - (1, 4, 7, 10), (2, 5, 8, 11), (3, 6, 9, 12)) - self.assert_pack("CMYK", "K", 1, (6, 7, 0, 1), (6, 7, 0, 2), (0, 7, 0, 3)) + self.assert_pack( + "CMYK", "CMYK;L", 4, (1, 4, 7, 10), (2, 5, 8, 11), (3, 6, 9, 12)) + self.assert_pack("CMYK", "K", 1, + (6, 7, 0, 1), (6, 7, 0, 2), (0, 7, 0, 3)) def test_YCbCr(self): self.assert_pack("YCbCr", "YCbCr", 3, (1, 2, 3), (4, 5, 6), (7, 8, 9)) - self.assert_pack("YCbCr", "YCbCr;L", 3, (1, 4, 7), (2, 5, 8), (3, 6, 9)) - self.assert_pack("YCbCr", "YCbCrX", + self.assert_pack("YCbCr", "YCbCr;L", 3, + (1, 4, 7), (2, 5, 8), (3, 6, 9)) + self.assert_pack( + "YCbCr", "YCbCrX", b'\x01\x02\x03\xff\x05\x06\x07\xff\t\n\x0b\xff', (1, 2, 3), (5, 6, 7), (9, 10, 11)) - self.assert_pack("YCbCr", "YCbCrK", + self.assert_pack( + "YCbCr", "YCbCrK", b'\x01\x02\x03\xff\x05\x06\x07\xff\t\n\x0b\xff', (1, 2, 3), (5, 6, 7), (9, 10, 11)) - self.assert_pack("YCbCr", "Y", 1, (1, 0, 8, 9), (2, 0, 8, 9), (3, 0, 8, 0)) - self.assert_pack("YCbCr", "Cb", 1, (6, 1, 8, 9), (6, 2, 8, 9), (6, 3, 8, 9)) - self.assert_pack("YCbCr", "Cr", 1, (6, 7, 1, 9), (6, 7, 2, 0), (6, 7, 3, 9)) + self.assert_pack("YCbCr", "Y", 1, + (1, 0, 8, 9), (2, 0, 8, 9), (3, 0, 8, 0)) + self.assert_pack("YCbCr", "Cb", 1, + (6, 1, 8, 9), (6, 2, 8, 9), (6, 3, 8, 9)) + self.assert_pack("YCbCr", "Cr", 1, + (6, 7, 1, 9), (6, 7, 2, 0), (6, 7, 3, 9)) def test_LAB(self): - self.assert_pack("LAB", "LAB", 3, - (1, 130, 131), (4, 133, 134), (7, 136, 137)) + self.assert_pack( + "LAB", "LAB", 3, (1, 130, 131), (4, 133, 134), (7, 136, 137)) self.assert_pack("LAB", "L", 1, (1, 9, 9), (2, 9, 9), (3, 9, 9)) self.assert_pack("LAB", "A", 1, (9, 1, 9), (9, 2, 9), (9, 3, 9)) self.assert_pack("LAB", "B", 1, (9, 9, 1), (9, 9, 2), (9, 9, 3)) @@ -149,35 +181,35 @@ class TestLibPack(PillowTestCase): def test_I(self): self.assert_pack("I", "I;16B", 2, 0x0102, 0x0304) - self.assert_pack("I", "I;32S", - b'\x83\x00\x00\x01\x01\x00\x00\x83', - 0x01000083, -2097151999) + self.assert_pack( + "I", "I;32S", + b'\x83\x00\x00\x01\x01\x00\x00\x83', 0x01000083, -2097151999) if sys.byteorder == 'little': self.assert_pack("I", "I", 4, 0x04030201, 0x08070605) - self.assert_pack("I", "I;32NS", - b'\x83\x00\x00\x01\x01\x00\x00\x83', - 0x01000083, -2097151999) + self.assert_pack( + "I", "I;32NS", + b'\x83\x00\x00\x01\x01\x00\x00\x83', 0x01000083, -2097151999) else: self.assert_pack("I", "I", 4, 0x01020304, 0x05060708) - self.assert_pack("I", "I;32NS", - b'\x83\x00\x00\x01\x01\x00\x00\x83', - -2097151999, 0x01000083) + self.assert_pack( + "I", "I;32NS", + b'\x83\x00\x00\x01\x01\x00\x00\x83', -2097151999, 0x01000083) def test_F_float(self): - self.assert_pack("F", "F;32F", 4, - 1.539989614439558e-36, 4.063216068939723e-34) + self.assert_pack( + "F", "F;32F", 4, 1.539989614439558e-36, 4.063216068939723e-34) if sys.byteorder == 'little': - self.assert_pack("F", "F", 4, - 1.539989614439558e-36, 4.063216068939723e-34) - self.assert_pack("F", "F;32NF", 4, - 1.539989614439558e-36, 4.063216068939723e-34) + self.assert_pack( + "F", "F", 4, 1.539989614439558e-36, 4.063216068939723e-34) + self.assert_pack( + "F", "F;32NF", 4, 1.539989614439558e-36, 4.063216068939723e-34) else: - self.assert_pack("F", "F", 4, - 2.387939260590663e-38, 6.301941157072183e-36) - self.assert_pack("F", "F;32NF", 4, - 2.387939260590663e-38, 6.301941157072183e-36) + self.assert_pack( + "F", "F", 4, 2.387939260590663e-38, 6.301941157072183e-36) + self.assert_pack( + "F", "F;32NF", 4, 2.387939260590663e-38, 6.301941157072183e-36) class TestLibUnpack(PillowTestCase): @@ -258,9 +290,12 @@ class TestLibUnpack(PillowTestCase): self.assert_unpack("RGB", "RGBX", 4, (1, 2, 3), (5, 6, 7), (9, 10, 11)) self.assert_unpack("RGB", "RGBX;L", 4, (1, 4, 7), (2, 5, 8), (3, 6, 9)) self.assert_unpack("RGB", "BGRX", 4, (3, 2, 1), (7, 6, 5), (11, 10, 9)) - self.assert_unpack("RGB", "XRGB", 4, (2, 3, 4), (6, 7, 8), (10, 11, 12)) - self.assert_unpack("RGB", "XBGR", 4, (4, 3, 2), (8, 7, 6), (12, 11, 10)) - self.assert_unpack("RGB", "YCC;P", + self.assert_unpack( + "RGB", "XRGB", 4, (2, 3, 4), (6, 7, 8), (10, 11, 12)) + self.assert_unpack( + "RGB", "XBGR", 4, (4, 3, 2), (8, 7, 6), (12, 11, 10)) + self.assert_unpack( + "RGB", "YCC;P", b'D]\x9c\x82\x1a\x91\xfaOC\xe7J\x12', # random data (127, 102, 0), (192, 227, 0), (213, 255, 170), (98, 255, 133)) self.assert_unpack("RGB", "R", 1, (1, 0, 0), (2, 0, 0), (3, 0, 0)) @@ -268,130 +303,172 @@ class TestLibUnpack(PillowTestCase): self.assert_unpack("RGB", "B", 1, (0, 0, 1), (0, 0, 2), (0, 0, 3)) def test_RGBA(self): - self.assert_unpack("RGBA", "LA", 2, (1, 1, 1, 2), (3, 3, 3, 4), (5, 5, 5, 6)) - self.assert_unpack("RGBA", "LA;16B", 4, - (1, 1, 1, 3), (5, 5, 5, 7), (9, 9, 9, 11)) - self.assert_unpack("RGBA", "RGBA", 4, - (1, 2, 3, 4), (5, 6, 7, 8), (9, 10, 11, 12)) - self.assert_unpack("RGBA", "RGBa", 4, + self.assert_unpack( + "RGBA", "LA", 2, (1, 1, 1, 2), (3, 3, 3, 4), (5, 5, 5, 6)) + self.assert_unpack( + "RGBA", "LA;16B", 4, (1, 1, 1, 3), (5, 5, 5, 7), (9, 9, 9, 11)) + self.assert_unpack( + "RGBA", "RGBA", 4, (1, 2, 3, 4), (5, 6, 7, 8), (9, 10, 11, 12)) + self.assert_unpack( + "RGBA", "RGBa", 4, (63, 127, 191, 4), (159, 191, 223, 8), (191, 212, 233, 12)) - self.assert_unpack("RGBA", "RGBa", + self.assert_unpack( + "RGBA", "RGBa", b'\x01\x02\x03\x00\x10\x20\x30\xff', (0, 0, 0, 0), (16, 32, 48, 255)) - self.assert_unpack("RGBA", "RGBa;16L", 8, + self.assert_unpack( + "RGBA", "RGBa;16L", 8, (63, 127, 191, 8), (159, 191, 223, 16), (191, 212, 233, 24)) - self.assert_unpack("RGBA", "RGBa;16L", + self.assert_unpack( + "RGBA", "RGBa;16L", b'\x88\x01\x88\x02\x88\x03\x88\x00' b'\x88\x10\x88\x20\x88\x30\x88\xff', (0, 0, 0, 0), (16, 32, 48, 255)) - self.assert_unpack("RGBA", "RGBa;16B", 8, + self.assert_unpack( + "RGBA", "RGBa;16B", 8, (36, 109, 182, 7), (153, 187, 221, 15), (188, 210, 232, 23)) - self.assert_unpack("RGBA", "RGBa;16B", + self.assert_unpack( + "RGBA", "RGBa;16B", b'\x01\x88\x02\x88\x03\x88\x00\x88' b'\x10\x88\x20\x88\x30\x88\xff\x88', (0, 0, 0, 0), (16, 32, 48, 255)) - self.assert_unpack("RGBA", "BGRa", 4, + self.assert_unpack( + "RGBA", "BGRa", 4, (191, 127, 63, 4), (223, 191, 159, 8), (233, 212, 191, 12)) - self.assert_unpack("RGBA", "BGRa", + self.assert_unpack( + "RGBA", "BGRa", b'\x01\x02\x03\x00\x10\x20\x30\xff', (0, 0, 0, 0), (48, 32, 16, 255)) - self.assert_unpack("RGBA", "RGBA;I", 4, + self.assert_unpack( + "RGBA", "RGBA;I", 4, (254, 253, 252, 4), (250, 249, 248, 8), (246, 245, 244, 12)) - self.assert_unpack("RGBA", "RGBA;L", 4, - (1, 4, 7, 10), (2, 5, 8, 11), (3, 6, 9, 12)) + self.assert_unpack( + "RGBA", "RGBA;L", 4, (1, 4, 7, 10), (2, 5, 8, 11), (3, 6, 9, 12)) self.assert_unpack("RGBA", "RGBA;15", 2, (8, 131, 0, 0), (24, 0, 8, 0)) self.assert_unpack("RGBA", "BGRA;15", 2, (0, 131, 8, 0), (8, 0, 24, 0)) - self.assert_unpack("RGBA", "RGBA;4B", 2, (17, 0, 34, 0), (51, 0, 68, 0)) - self.assert_unpack("RGBA", "RGBA;16L", 8, (2, 4, 6, 8), (10, 12, 14, 16)) - self.assert_unpack("RGBA", "RGBA;16B", 8, (1, 3, 5, 7), (9, 11, 13, 15)) - self.assert_unpack("RGBA", "BGRA", 4, - (3, 2, 1, 4), (7, 6, 5, 8), (11, 10, 9, 12)) - self.assert_unpack("RGBA", "ARGB", 4, - (2, 3, 4, 1), (6, 7, 8, 5), (10, 11, 12, 9)) - self.assert_unpack("RGBA", "ABGR", 4, - (4, 3, 2, 1), (8, 7, 6, 5), (12, 11, 10, 9)) - self.assert_unpack("RGBA", "YCCA;P", + self.assert_unpack( + "RGBA", "RGBA;4B", 2, (17, 0, 34, 0), (51, 0, 68, 0)) + self.assert_unpack( + "RGBA", "RGBA;16L", 8, (2, 4, 6, 8), (10, 12, 14, 16)) + self.assert_unpack( + "RGBA", "RGBA;16B", 8, (1, 3, 5, 7), (9, 11, 13, 15)) + self.assert_unpack( + "RGBA", "BGRA", 4, (3, 2, 1, 4), (7, 6, 5, 8), (11, 10, 9, 12)) + self.assert_unpack( + "RGBA", "ARGB", 4, (2, 3, 4, 1), (6, 7, 8, 5), (10, 11, 12, 9)) + self.assert_unpack( + "RGBA", "ABGR", 4, (4, 3, 2, 1), (8, 7, 6, 5), (12, 11, 10, 9)) + self.assert_unpack( + "RGBA", "YCCA;P", b']bE\x04\xdd\xbej\xed57T\xce\xac\xce:\x11', # random data (0, 161, 0, 4), (255, 255, 255, 237), (27, 158, 0, 206), (0, 118, 0, 17)) - self.assert_unpack("RGBA", "R", 1, (1, 0, 0, 0), (2, 0, 0, 0), (3, 0, 0, 0)) - self.assert_unpack("RGBA", "G", 1, (0, 1, 0, 0), (0, 2, 0, 0), (0, 3, 0, 0)) - self.assert_unpack("RGBA", "B", 1, (0, 0, 1, 0), (0, 0, 2, 0), (0, 0, 3, 0)) - self.assert_unpack("RGBA", "A", 1, (0, 0, 0, 1), (0, 0, 0, 2), (0, 0, 0, 3)) + self.assert_unpack( + "RGBA", "R", 1, (1, 0, 0, 0), (2, 0, 0, 0), (3, 0, 0, 0)) + self.assert_unpack( + "RGBA", "G", 1, (0, 1, 0, 0), (0, 2, 0, 0), (0, 3, 0, 0)) + self.assert_unpack( + "RGBA", "B", 1, (0, 0, 1, 0), (0, 0, 2, 0), (0, 0, 3, 0)) + self.assert_unpack( + "RGBA", "A", 1, (0, 0, 0, 1), (0, 0, 0, 2), (0, 0, 0, 3)) def test_RGBa(self): - self.assert_unpack("RGBa", "RGBa", 4, - (1, 2, 3, 4), (5, 6, 7, 8), (9, 10, 11, 12)) - self.assert_unpack("RGBa", "BGRa", 4, - (3, 2, 1, 4), (7, 6, 5, 8), (11, 10, 9, 12)) - self.assert_unpack("RGBa", "aRGB", 4, - (2, 3, 4, 1), (6, 7, 8, 5), (10, 11, 12, 9)) - self.assert_unpack("RGBa", "aBGR", 4, - (4, 3, 2, 1), (8, 7, 6, 5), (12, 11, 10, 9)) + self.assert_unpack( + "RGBa", "RGBa", 4, (1, 2, 3, 4), (5, 6, 7, 8), (9, 10, 11, 12)) + self.assert_unpack( + "RGBa", "BGRa", 4, (3, 2, 1, 4), (7, 6, 5, 8), (11, 10, 9, 12)) + self.assert_unpack( + "RGBa", "aRGB", 4, (2, 3, 4, 1), (6, 7, 8, 5), (10, 11, 12, 9)) + self.assert_unpack( + "RGBa", "aBGR", 4, (4, 3, 2, 1), (8, 7, 6, 5), (12, 11, 10, 9)) def test_RGBX(self): - self.assert_unpack("RGBX", "RGB", 3, (1, 2, 3, X), (4, 5, 6, X), (7, 8, 9, X)) - self.assert_unpack("RGBX", "RGB;L", 3, (1, 4, 7, X), (2, 5, 8, X), (3, 6, 9, X)) + self.assert_unpack("RGBX", "RGB", 3, + (1, 2, 3, X), (4, 5, 6, X), (7, 8, 9, X)) + self.assert_unpack("RGBX", "RGB;L", 3, + (1, 4, 7, X), (2, 5, 8, X), (3, 6, 9, X)) self.assert_unpack("RGBX", "RGB;16B", 6, (1, 3, 5, X), (7, 9, 11, X)) - self.assert_unpack("RGBX", "BGR", 3, (3, 2, 1, X), (6, 5, 4, X), (9, 8, 7, X)) + self.assert_unpack("RGBX", "BGR", 3, + (3, 2, 1, X), (6, 5, 4, X), (9, 8, 7, X)) self.assert_unpack("RGBX", "RGB;15", 2, (8, 131, 0, X), (24, 0, 8, X)) self.assert_unpack("RGBX", "BGR;15", 2, (0, 131, 8, X), (8, 0, 24, X)) self.assert_unpack("RGBX", "RGB;4B", 2, (17, 0, 34, X), (51, 0, 68, X)) - self.assert_unpack("RGBX", "RGBX", 4, - (1, 2, 3, 4), (5, 6, 7, 8), (9, 10, 11, 12)) - self.assert_unpack("RGBX", "RGBXX", 5, - (1, 2, 3, 4), (6, 7, 8, 9), (11, 12, 13, 14)) - self.assert_unpack("RGBX", "RGBXXX", 6, - (1, 2, 3, 4), (7, 8, 9, 10), (13, 14, 15, 16)) - self.assert_unpack("RGBX", "RGBX;L", 4, - (1, 4, 7, 10), (2, 5, 8, 11), (3, 6, 9, 12)) - self.assert_unpack("RGBX", "RGBX;16L", 8, (2, 4, 6, 8), (10, 12, 14, 16)) - self.assert_unpack("RGBX", "RGBX;16B", 8, (1, 3, 5, 7), (9, 11, 13, 15)) - self.assert_unpack("RGBX", "BGRX", 4, (3, 2, 1, X), (7, 6, 5, X), (11, 10, 9, X)) - self.assert_unpack("RGBX", "XRGB", 4, (2, 3, 4, X), (6, 7, 8, X), (10, 11, 12, X)) - self.assert_unpack("RGBX", "XBGR", 4, (4, 3, 2, X), (8, 7, 6, X), (12, 11, 10, X)) - self.assert_unpack("RGBX", "YCC;P", + self.assert_unpack( + "RGBX", "RGBX", 4, (1, 2, 3, 4), (5, 6, 7, 8), (9, 10, 11, 12)) + self.assert_unpack( + "RGBX", "RGBXX", 5, (1, 2, 3, 4), (6, 7, 8, 9), (11, 12, 13, 14)) + self.assert_unpack( + "RGBX", "RGBXXX", 6, (1, 2, 3, 4), (7, 8, 9, 10), (13, 14, 15, 16)) + self.assert_unpack( + "RGBX", "RGBX;L", 4, (1, 4, 7, 10), (2, 5, 8, 11), (3, 6, 9, 12)) + self.assert_unpack("RGBX", "RGBX;16L", 8, + (2, 4, 6, 8), (10, 12, 14, 16)) + self.assert_unpack("RGBX", "RGBX;16B", 8, + (1, 3, 5, 7), (9, 11, 13, 15)) + self.assert_unpack("RGBX", "BGRX", 4, + (3, 2, 1, X), (7, 6, 5, X), (11, 10, 9, X)) + self.assert_unpack("RGBX", "XRGB", 4, + (2, 3, 4, X), (6, 7, 8, X), (10, 11, 12, X)) + self.assert_unpack("RGBX", "XBGR", 4, + (4, 3, 2, X), (8, 7, 6, X), (12, 11, 10, X)) + self.assert_unpack( + "RGBX", "YCC;P", b'D]\x9c\x82\x1a\x91\xfaOC\xe7J\x12', # random data (127, 102, 0, X), (192, 227, 0, X), (213, 255, 170, X), (98, 255, 133, X)) - self.assert_unpack("RGBX", "R", 1, (1, 0, 0, 0), (2, 0, 0, 0), (3, 0, 0, 0)) - self.assert_unpack("RGBX", "G", 1, (0, 1, 0, 0), (0, 2, 0, 0), (0, 3, 0, 0)) - self.assert_unpack("RGBX", "B", 1, (0, 0, 1, 0), (0, 0, 2, 0), (0, 0, 3, 0)) - self.assert_unpack("RGBX", "X", 1, (0, 0, 0, 1), (0, 0, 0, 2), (0, 0, 0, 3)) + self.assert_unpack("RGBX", "R", 1, + (1, 0, 0, 0), (2, 0, 0, 0), (3, 0, 0, 0)) + self.assert_unpack("RGBX", "G", 1, + (0, 1, 0, 0), (0, 2, 0, 0), (0, 3, 0, 0)) + self.assert_unpack("RGBX", "B", 1, + (0, 0, 1, 0), (0, 0, 2, 0), (0, 0, 3, 0)) + self.assert_unpack("RGBX", "X", 1, + (0, 0, 0, 1), (0, 0, 0, 2), (0, 0, 0, 3)) def test_CMYK(self): - self.assert_unpack("CMYK", "CMYK", 4, - (1, 2, 3, 4), (5, 6, 7, 8), (9, 10, 11, 12)) - self.assert_unpack("CMYK", "CMYKX", 5, - (1, 2, 3, 4), (6, 7, 8, 9), (11, 12, 13, 14)) - self.assert_unpack("CMYK", "CMYKXX", 6, - (1, 2, 3, 4), (7, 8, 9, 10), (13, 14, 15, 16)) - self.assert_unpack("CMYK", "CMYK;I", 4, + self.assert_unpack( + "CMYK", "CMYK", 4, (1, 2, 3, 4), (5, 6, 7, 8), (9, 10, 11, 12)) + self.assert_unpack( + "CMYK", "CMYKX", 5, (1, 2, 3, 4), (6, 7, 8, 9), (11, 12, 13, 14)) + self.assert_unpack( + "CMYK", "CMYKXX", 6, (1, 2, 3, 4), (7, 8, 9, 10), (13, 14, 15, 16)) + self.assert_unpack( + "CMYK", "CMYK;I", 4, (254, 253, 252, 251), (250, 249, 248, 247), (246, 245, 244, 243)) - self.assert_unpack("CMYK", "CMYK;L", 4, - (1, 4, 7, 10), (2, 5, 8, 11), (3, 6, 9, 12)) - self.assert_unpack("CMYK", "C", 1, (1, 0, 0, 0), (2, 0, 0, 0), (3, 0, 0, 0)) - self.assert_unpack("CMYK", "M", 1, (0, 1, 0, 0), (0, 2, 0, 0), (0, 3, 0, 0)) - self.assert_unpack("CMYK", "Y", 1, (0, 0, 1, 0), (0, 0, 2, 0), (0, 0, 3, 0)) - self.assert_unpack("CMYK", "K", 1, (0, 0, 0, 1), (0, 0, 0, 2), (0, 0, 0, 3)) - self.assert_unpack("CMYK", "C;I", 1, - (254, 0, 0, 0), (253, 0, 0, 0), (252, 0, 0, 0)) - self.assert_unpack("CMYK", "M;I", 1, - (0, 254, 0, 0), (0, 253, 0, 0), (0, 252, 0, 0)) - self.assert_unpack("CMYK", "Y;I", 1, - (0, 0, 254, 0), (0, 0, 253, 0), (0, 0, 252, 0)) - self.assert_unpack("CMYK", "K;I", 1, - (0, 0, 0, 254), (0, 0, 0, 253), (0, 0, 0, 252)) + self.assert_unpack( + "CMYK", "CMYK;L", 4, (1, 4, 7, 10), (2, 5, 8, 11), (3, 6, 9, 12)) + self.assert_unpack("CMYK", "C", 1, + (1, 0, 0, 0), (2, 0, 0, 0), (3, 0, 0, 0)) + self.assert_unpack("CMYK", "M", 1, + (0, 1, 0, 0), (0, 2, 0, 0), (0, 3, 0, 0)) + self.assert_unpack("CMYK", "Y", 1, + (0, 0, 1, 0), (0, 0, 2, 0), (0, 0, 3, 0)) + self.assert_unpack("CMYK", "K", 1, + (0, 0, 0, 1), (0, 0, 0, 2), (0, 0, 0, 3)) + self.assert_unpack( + "CMYK", "C;I", 1, (254, 0, 0, 0), (253, 0, 0, 0), (252, 0, 0, 0)) + self.assert_unpack( + "CMYK", "M;I", 1, (0, 254, 0, 0), (0, 253, 0, 0), (0, 252, 0, 0)) + self.assert_unpack( + "CMYK", "Y;I", 1, (0, 0, 254, 0), (0, 0, 253, 0), (0, 0, 252, 0)) + self.assert_unpack( + "CMYK", "K;I", 1, (0, 0, 0, 254), (0, 0, 0, 253), (0, 0, 0, 252)) def test_YCbCr(self): - self.assert_unpack("YCbCr", "YCbCr", 3, (1, 2, 3), (4, 5, 6), (7, 8, 9)) - self.assert_unpack("YCbCr", "YCbCr;L", 3, (1, 4, 7), (2, 5, 8), (3, 6, 9)) - self.assert_unpack("YCbCr", "YCbCrK", 4, (1, 2, 3), (5, 6, 7), (9, 10, 11)) - self.assert_unpack("YCbCr", "YCbCrX", 4, (1, 2, 3), (5, 6, 7), (9, 10, 11)) - self.assert_unpack("YCbCr", "YCbCrXX", 5, (1, 2, 3), (6, 7, 8), (11, 12, 13)) - self.assert_unpack("YCbCr", "YCbCrXXX", 6, (1, 2, 3), (7, 8, 9), (13, 14, 15)) + self.assert_unpack( + "YCbCr", "YCbCr", 3, (1, 2, 3), (4, 5, 6), (7, 8, 9)) + self.assert_unpack( + "YCbCr", "YCbCr;L", 3, (1, 4, 7), (2, 5, 8), (3, 6, 9)) + self.assert_unpack( + "YCbCr", "YCbCrK", 4, (1, 2, 3), (5, 6, 7), (9, 10, 11)) + self.assert_unpack( + "YCbCr", "YCbCrX", 4, (1, 2, 3), (5, 6, 7), (9, 10, 11)) + self.assert_unpack( + "YCbCr", "YCbCrXX", 5, (1, 2, 3), (6, 7, 8), (11, 12, 13)) + self.assert_unpack( + "YCbCr", "YCbCrXXX", 6, (1, 2, 3), (7, 8, 9), (13, 14, 15)) def test_LAB(self): - self.assert_unpack("LAB", "LAB", 3, - (1, 130, 131), (4, 133, 134), (7, 136, 137)) + self.assert_unpack( + "LAB", "LAB", 3, (1, 130, 131), (4, 133, 134), (7, 136, 137)) self.assert_unpack("LAB", "L", 1, (1, 0, 0), (2, 0, 0), (3, 0, 0)) self.assert_unpack("LAB", "A", 1, (0, 1, 0), (0, 2, 0), (0, 3, 0)) self.assert_unpack("LAB", "B", 1, (0, 0, 1), (0, 0, 2), (0, 0, 3)) @@ -410,30 +487,32 @@ class TestLibUnpack(PillowTestCase): self.assert_unpack("I", "I;16B", 2, 0x0102, 0x0304) self.assert_unpack("I", "I;16BS", b'\x83\x01\x01\x83', -31999, 0x0183) self.assert_unpack("I", "I;32", 4, 0x04030201, 0x08070605) - self.assert_unpack("I", "I;32S", - b'\x83\x00\x00\x01\x01\x00\x00\x83', - 0x01000083, -2097151999) + self.assert_unpack( + "I", "I;32S", + b'\x83\x00\x00\x01\x01\x00\x00\x83', 0x01000083, -2097151999) self.assert_unpack("I", "I;32B", 4, 0x01020304, 0x05060708) - self.assert_unpack("I", "I;32BS", - b'\x83\x00\x00\x01\x01\x00\x00\x83', - -2097151999, 0x01000083) + self.assert_unpack( + "I", "I;32BS", + b'\x83\x00\x00\x01\x01\x00\x00\x83', -2097151999, 0x01000083) if sys.byteorder == 'little': self.assert_unpack("I", "I", 4, 0x04030201, 0x08070605) self.assert_unpack("I", "I;16N", 2, 0x0201, 0x0403) - self.assert_unpack("I", "I;16NS", b'\x83\x01\x01\x83', 0x0183, -31999) + self.assert_unpack("I", "I;16NS", + b'\x83\x01\x01\x83', 0x0183, -31999) self.assert_unpack("I", "I;32N", 4, 0x04030201, 0x08070605) - self.assert_unpack("I", "I;32NS", - b'\x83\x00\x00\x01\x01\x00\x00\x83', - 0x01000083, -2097151999) + self.assert_unpack( + "I", "I;32NS", + b'\x83\x00\x00\x01\x01\x00\x00\x83', 0x01000083, -2097151999) else: self.assert_unpack("I", "I", 4, 0x01020304, 0x05060708) self.assert_unpack("I", "I;16N", 2, 0x0102, 0x0304) - self.assert_unpack("I", "I;16NS", b'\x83\x01\x01\x83', -31999, 0x0183) + self.assert_unpack("I", "I;16NS", + b'\x83\x01\x01\x83', -31999, 0x0183) self.assert_unpack("I", "I;32N", 4, 0x01020304, 0x05060708) - self.assert_unpack("I", "I;32NS", - b'\x83\x00\x00\x01\x01\x00\x00\x83', - -2097151999, 0x01000083) + self.assert_unpack( + "I", "I;32NS", + b'\x83\x00\x00\x01\x01\x00\x00\x83', -2097151999, 0x01000083) def test_F_int(self): self.assert_unpack("F", "F;8", 1, 0x01, 0x02, 0x03, 0x04) @@ -443,55 +522,70 @@ class TestLibUnpack(PillowTestCase): self.assert_unpack("F", "F;16B", 2, 0x0102, 0x0304) self.assert_unpack("F", "F;16BS", b'\x83\x01\x01\x83', -31999, 0x0183) self.assert_unpack("F", "F;32", 4, 67305984, 134678016) - self.assert_unpack("F", "F;32S", - b'\x83\x00\x00\x01\x01\x00\x00\x83', - 16777348, -2097152000) + self.assert_unpack( + "F", "F;32S", + b'\x83\x00\x00\x01\x01\x00\x00\x83', 16777348, -2097152000) self.assert_unpack("F", "F;32B", 4, 0x01020304, 0x05060708) - self.assert_unpack("F", "F;32BS", - b'\x83\x00\x00\x01\x01\x00\x00\x83', - -2097152000, 16777348) + self.assert_unpack( + "F", "F;32BS", + b'\x83\x00\x00\x01\x01\x00\x00\x83', -2097152000, 16777348) if sys.byteorder == 'little': self.assert_unpack("F", "F;16N", 2, 0x0201, 0x0403) - self.assert_unpack("F", "F;16NS", b'\x83\x01\x01\x83', 0x0183, -31999) + self.assert_unpack( + "F", "F;16NS", + b'\x83\x01\x01\x83', 0x0183, -31999) self.assert_unpack("F", "F;32N", 4, 67305984, 134678016) - self.assert_unpack("F", "F;32NS", - b'\x83\x00\x00\x01\x01\x00\x00\x83', - 16777348, -2097152000) + self.assert_unpack( + "F", "F;32NS", + b'\x83\x00\x00\x01\x01\x00\x00\x83', 16777348, -2097152000) else: self.assert_unpack("F", "F;16N", 2, 0x0102, 0x0304) - self.assert_unpack("F", "F;16NS", b'\x83\x01\x01\x83', -31999, 0x0183) + self.assert_unpack( + "F", "F;16NS", + b'\x83\x01\x01\x83', -31999, 0x0183) self.assert_unpack("F", "F;32N", 4, 0x01020304, 0x05060708) - self.assert_unpack("F", "F;32NS", + self.assert_unpack( + "F", "F;32NS", b'\x83\x00\x00\x01\x01\x00\x00\x83', -2097152000, 16777348) def test_F_float(self): - self.assert_unpack("F", "F;32F", 4, + self.assert_unpack( + "F", "F;32F", 4, 1.539989614439558e-36, 4.063216068939723e-34) - self.assert_unpack("F", "F;32BF", 4, + self.assert_unpack( + "F", "F;32BF", 4, 2.387939260590663e-38, 6.301941157072183e-36) - self.assert_unpack("F", "F;64F", + self.assert_unpack( + "F", "F;64F", b'333333\xc3?\x00\x00\x00\x00\x00J\x93\xc0', # by struct.pack 0.15000000596046448, -1234.5) - self.assert_unpack("F", "F;64BF", + self.assert_unpack( + "F", "F;64BF", b'?\xc3333333\xc0\x93J\x00\x00\x00\x00\x00', # by struct.pack 0.15000000596046448, -1234.5) if sys.byteorder == 'little': - self.assert_unpack("F", "F", 4, + self.assert_unpack( + "F", "F", 4, 1.539989614439558e-36, 4.063216068939723e-34) - self.assert_unpack("F", "F;32NF", 4, + self.assert_unpack( + "F", "F;32NF", 4, 1.539989614439558e-36, 4.063216068939723e-34) - self.assert_unpack("F", "F;64NF", + self.assert_unpack( + "F", "F;64NF", b'333333\xc3?\x00\x00\x00\x00\x00J\x93\xc0', 0.15000000596046448, -1234.5) else: - self.assert_unpack("F", "F", 4, + self.assert_unpack( + "F", "F", 4, 2.387939260590663e-38, 6.301941157072183e-36) - self.assert_unpack("F", "F;32NF", 4, + self.assert_unpack( + "F", "F;32NF", 4, 2.387939260590663e-38, 6.301941157072183e-36) - self.assert_unpack("F", "F;64NF", + self.assert_unpack( + "F", "F;64NF", b'?\xc3333333\xc0\x93J\x00\x00\x00\x00\x00', 0.15000000596046448, -1234.5) diff --git a/Tests/test_locale.py b/Tests/test_locale.py index 142753791..5aef8427b 100644 --- a/Tests/test_locale.py +++ b/Tests/test_locale.py @@ -29,7 +29,7 @@ class TestLocale(PillowTestCase): Image.open(path) try: locale.setlocale(locale.LC_ALL, "polish") - except: + except locale.Error: unittest.skip('Polish locale not available') Image.open(path) diff --git a/Tests/test_map.py b/Tests/test_map.py index 14bd835a2..8e3916d27 100644 --- a/Tests/test_map.py +++ b/Tests/test_map.py @@ -4,7 +4,8 @@ import sys from PIL import Image -@unittest.skipIf(sys.platform.startswith('win32'), "Win32 does not call map_buffer") +@unittest.skipIf(sys.platform.startswith('win32'), + "Win32 does not call map_buffer") class TestMap(PillowTestCase): def test_overflow(self): # There is the potential to overflow comparisons in map.c diff --git a/Tests/test_numpy.py b/Tests/test_numpy.py index 3f9586513..4efcd2c51 100644 --- a/Tests/test_numpy.py +++ b/Tests/test_numpy.py @@ -1,39 +1,19 @@ from __future__ import print_function -import sys -from helper import unittest, PillowTestCase, hopper +from helper import PillowTestCase, hopper, unittest from PIL import Image try: - import site import numpy - assert site # silence warning - assert numpy # silence warning except ImportError: - # Skip via setUp() - pass + numpy = None + TEST_IMAGE_SIZE = (10, 10) -# Numpy on pypy as of pypy 5.3.1 is corrupting the numpy.array(Image) -# call such that it's returning a object of type numpy.ndarray, but -# the repr is that of a PIL.Image. Size and shape are 1 and (), not the -# size and shape of the array. This causes failures in several tests. -SKIP_NUMPY_ON_PYPY = hasattr(sys, 'pypy_version_info') and ( - sys.pypy_version_info <= (5, 3, 1, 'final', 0)) - +@unittest.skipIf(numpy is None, "Numpy is not installed") class TestNumpy(PillowTestCase): - - def setUp(self): - try: - import site - import numpy - assert site # silence warning - assert numpy # silence warning - except ImportError: - self.skipTest("ImportError") - def test_numpy_to_image(self): def to_image(dtype, bands=1, boolean=0): @@ -121,7 +101,6 @@ class TestNumpy(PillowTestCase): for y in range(0, img.size[1], int(img.size[1]/10)): self.assert_deep_equal(px[x, y], np[y, x]) - @unittest.skipIf(SKIP_NUMPY_ON_PYPY, "numpy.array(Image) is flaky on PyPy") def test_16bit(self): img = Image.open('Tests/images/16bit.cropped.tif') np_img = numpy.array(img) @@ -145,14 +124,15 @@ class TestNumpy(PillowTestCase): def test_save_tiff_uint16(self): # Tests that we're getting the pixel value in the right byte order. pixel_value = 0x1234 - a = numpy.array([pixel_value] * TEST_IMAGE_SIZE[0] * TEST_IMAGE_SIZE[1], dtype=numpy.uint16) + a = numpy.array( + [pixel_value] * TEST_IMAGE_SIZE[0] * TEST_IMAGE_SIZE[1], + dtype=numpy.uint16) a.shape = TEST_IMAGE_SIZE img = Image.fromarray(a) img_px = img.load() self.assertEqual(img_px[0, 0], pixel_value) - @unittest.skipIf(SKIP_NUMPY_ON_PYPY, "numpy.array(Image) is flaky on PyPy") def test_to_array(self): def _to_array(mode, dtype): diff --git a/Tests/test_pdfparser.py b/Tests/test_pdfparser.py index db97c97dd..42c813520 100644 --- a/Tests/test_pdfparser.py +++ b/Tests/test_pdfparser.py @@ -1,6 +1,8 @@ from helper import unittest, PillowTestCase -from PIL.PdfParser import IndirectObjectDef, IndirectReference, PdfBinary, PdfDict, PdfFormatError, PdfName, PdfParser, PdfStream, decode_text, encode_text, pdf_repr +from PIL.PdfParser import IndirectObjectDef, IndirectReference, PdfBinary, \ + PdfDict, PdfFormatError, PdfName, PdfParser, \ + PdfStream, decode_text, encode_text, pdf_repr class TestPdfParser(PillowTestCase): @@ -22,23 +24,35 @@ class TestPdfParser(PillowTestCase): self.assertNotEqual(IndirectObjectDef(1, 2), (1, 2)) def test_parsing(self): - self.assertEqual(PdfParser.interpret_name(b"Name#23Hash"), b"Name#Hash") + self.assertEqual(PdfParser.interpret_name(b"Name#23Hash"), + b"Name#Hash") self.assertEqual(PdfParser.interpret_name(b"Name#23Hash", as_text=True), "Name#Hash") - self.assertEqual(PdfParser.get_value(b"1 2 R ", 0), (IndirectReference(1, 2), 5)) + self.assertEqual(PdfParser.get_value(b"1 2 R ", 0), + (IndirectReference(1, 2), 5)) self.assertEqual(PdfParser.get_value(b"true[", 0), (True, 4)) self.assertEqual(PdfParser.get_value(b"false%", 0), (False, 5)) self.assertEqual(PdfParser.get_value(b"null<", 0), (None, 4)) - self.assertEqual(PdfParser.get_value(b"%cmt\n %cmt\n 123\n", 0), (123, 15)) - self.assertEqual(PdfParser.get_value(b"<901FA3>", 0), (b"\x90\x1F\xA3", 8)) - self.assertEqual(PdfParser.get_value(b"asd < 9 0 1 f A > qwe", 3), (b"\x90\x1F\xA0", 17)) + self.assertEqual(PdfParser.get_value(b"%cmt\n %cmt\n 123\n", 0), + (123, 15)) + self.assertEqual(PdfParser.get_value(b"<901FA3>", 0), + (b"\x90\x1F\xA3", 8)) + self.assertEqual(PdfParser.get_value(b"asd < 9 0 1 f A > qwe", 3), + (b"\x90\x1F\xA0", 17)) self.assertEqual(PdfParser.get_value(b"(asd)", 0), (b"asd", 5)) - self.assertEqual(PdfParser.get_value(b"(asd(qwe)zxc)zzz(aaa)", 0), (b"asd(qwe)zxc", 13)) - self.assertEqual(PdfParser.get_value(b"(Two \\\nwords.)", 0), (b"Two words.", 14)) - self.assertEqual(PdfParser.get_value(b"(Two\nlines.)", 0), (b"Two\nlines.", 12)) - self.assertEqual(PdfParser.get_value(b"(Two\r\nlines.)", 0), (b"Two\nlines.", 13)) - self.assertEqual(PdfParser.get_value(b"(Two\\nlines.)", 0), (b"Two\nlines.", 13)) - self.assertEqual(PdfParser.get_value(b"(One\\(paren).", 0), (b"One(paren", 12)) - self.assertEqual(PdfParser.get_value(b"(One\\)paren).", 0), (b"One)paren", 12)) + self.assertEqual(PdfParser.get_value(b"(asd(qwe)zxc)zzz(aaa)", 0), + (b"asd(qwe)zxc", 13)) + self.assertEqual(PdfParser.get_value(b"(Two \\\nwords.)", 0), + (b"Two words.", 14)) + self.assertEqual(PdfParser.get_value(b"(Two\nlines.)", 0), + (b"Two\nlines.", 12)) + self.assertEqual(PdfParser.get_value(b"(Two\r\nlines.)", 0), + (b"Two\nlines.", 13)) + self.assertEqual(PdfParser.get_value(b"(Two\\nlines.)", 0), + (b"Two\nlines.", 13)) + self.assertEqual(PdfParser.get_value(b"(One\\(paren).", 0), + (b"One(paren", 12)) + self.assertEqual(PdfParser.get_value(b"(One\\)paren).", 0), + (b"One)paren", 12)) self.assertEqual(PdfParser.get_value(b"(\\0053)", 0), (b"\x053", 7)) self.assertEqual(PdfParser.get_value(b"(\\053)", 0), (b"\x2B", 6)) self.assertEqual(PdfParser.get_value(b"(\\53)", 0), (b"\x2B", 5)) @@ -65,23 +79,30 @@ class TestPdfParser(PillowTestCase): def test_pdf_repr(self): self.assertEqual(bytes(IndirectReference(1, 2)), b"1 2 R") - self.assertEqual(bytes(IndirectObjectDef(*IndirectReference(1, 2))), b"1 2 obj") + self.assertEqual(bytes(IndirectObjectDef(*IndirectReference(1, 2))), + b"1 2 obj") self.assertEqual(bytes(PdfName(b"Name#Hash")), b"/Name#23Hash") self.assertEqual(bytes(PdfName("Name#Hash")), b"/Name#23Hash") - self.assertEqual(bytes(PdfDict({b"Name": IndirectReference(1, 2)})), b"<<\n/Name 1 2 R\n>>") - self.assertEqual(bytes(PdfDict({"Name": IndirectReference(1, 2)})), b"<<\n/Name 1 2 R\n>>") + self.assertEqual(bytes(PdfDict({b"Name": IndirectReference(1, 2)})), + b"<<\n/Name 1 2 R\n>>") + self.assertEqual(bytes(PdfDict({"Name": IndirectReference(1, 2)})), + b"<<\n/Name 1 2 R\n>>") self.assertEqual(pdf_repr(IndirectReference(1, 2)), b"1 2 R") - self.assertEqual(pdf_repr(IndirectObjectDef(*IndirectReference(1, 2))), b"1 2 obj") + self.assertEqual(pdf_repr(IndirectObjectDef(*IndirectReference(1, 2))), + b"1 2 obj") self.assertEqual(pdf_repr(PdfName(b"Name#Hash")), b"/Name#23Hash") self.assertEqual(pdf_repr(PdfName("Name#Hash")), b"/Name#23Hash") - self.assertEqual(pdf_repr(PdfDict({b"Name": IndirectReference(1, 2)})), b"<<\n/Name 1 2 R\n>>") - self.assertEqual(pdf_repr(PdfDict({"Name": IndirectReference(1, 2)})), b"<<\n/Name 1 2 R\n>>") + self.assertEqual(pdf_repr(PdfDict({b"Name": IndirectReference(1, 2)})), + b"<<\n/Name 1 2 R\n>>") + self.assertEqual(pdf_repr(PdfDict({"Name": IndirectReference(1, 2)})), + b"<<\n/Name 1 2 R\n>>") self.assertEqual(pdf_repr(123), b"123") self.assertEqual(pdf_repr(True), b"true") self.assertEqual(pdf_repr(False), b"false") self.assertEqual(pdf_repr(None), b"null") self.assertEqual(pdf_repr(b"a)/b\\(c"), br"(a\)/b\\\(c)") - self.assertEqual(pdf_repr([123, True, {"a": PdfName(b"b")}]), b"[ 123 true <<\n/a /b\n>> ]") + self.assertEqual(pdf_repr([123, True, {"a": PdfName(b"b")}]), + b"[ 123 true <<\n/a /b\n>> ]") self.assertEqual(pdf_repr(PdfBinary(b"\x90\x1F\xA0")), b"<901FA0>") diff --git a/Tests/test_pyroma.py b/Tests/test_pyroma.py index 962535f03..cf5fc361f 100644 --- a/Tests/test_pyroma.py +++ b/Tests/test_pyroma.py @@ -1,6 +1,6 @@ from helper import unittest, PillowTestCase -from PIL import PILLOW_VERSION +from PIL import __version__ try: import pyroma @@ -26,7 +26,7 @@ class TestPyroma(PillowTestCase): rating = pyroma.ratings.rate(data) # Assert - if 'rc' in PILLOW_VERSION: + if 'rc' in __version__: # Pyroma needs to chill about RC versions # and not kill all our tests. self.assertEqual(rating, (9, [ diff --git a/Tests/test_image_fromqpixmap.py b/Tests/test_qt_image_fromqpixmap.py similarity index 100% rename from Tests/test_image_fromqpixmap.py rename to Tests/test_qt_image_fromqpixmap.py diff --git a/Tests/test_image_toqimage.py b/Tests/test_qt_image_toqimage.py similarity index 93% rename from Tests/test_image_toqimage.py rename to Tests/test_qt_image_toqimage.py index 6d7715c80..c9971cf73 100644 --- a/Tests/test_image_toqimage.py +++ b/Tests/test_qt_image_toqimage.py @@ -43,8 +43,9 @@ class TestToQImage(PillowQtTestCase, PillowTestCase): if mode == '1': # BW appears to not save correctly on QT4 and QT5 # kicks out errors on console: - # libpng warning: Invalid color type/bit depth combination in IHDR - # libpng error: Invalid IHDR data + # libpng warning: Invalid color type/bit depth combination + # in IHDR + # libpng error: Invalid IHDR data continue # Test saving the file diff --git a/Tests/test_image_toqpixmap.py b/Tests/test_qt_image_toqpixmap.py similarity index 100% rename from Tests/test_image_toqpixmap.py rename to Tests/test_qt_image_toqpixmap.py diff --git a/Tests/test_scipy.py b/Tests/test_scipy.py deleted file mode 100644 index 18c4403a0..000000000 --- a/Tests/test_scipy.py +++ /dev/null @@ -1,53 +0,0 @@ -from helper import unittest, PillowTestCase -from distutils.version import LooseVersion -try: - import numpy as np - from numpy.testing import assert_equal - - from scipy import misc - import scipy - HAS_SCIPY = True -except ImportError: - HAS_SCIPY = False - - -class Test_scipy_resize(PillowTestCase): - """ Tests for scipy regression in Pillow 2.6.0 - - Tests from https://github.com/scipy/scipy/blob/master/scipy/misc/pilutil.py - """ - - def setUp(self): - if not HAS_SCIPY: - self.skipTest("Scipy Required") - - def test_imresize(self): - im = np.random.random((10, 20)) - for T in np.sctypes['float'] + [float]: - # 1.1 rounds to below 1.1 for float16, 1.101 works - im1 = misc.imresize(im, T(1.101)) - self.assertEqual(im1.shape, (11, 22)) - - # this test fails prior to scipy 0.14.0b1 - # https://github.com/scipy/scipy/commit/855ff1fff805fb91840cf36b7082d18565fc8352 - @unittest.skipIf(HAS_SCIPY and - (LooseVersion(scipy.__version__) < LooseVersion('0.14.0')), - "Test fails on scipy < 0.14.0") - def test_imresize4(self): - im = np.array([[1, 2], - [3, 4]]) - res = np.array([[1., 1.25, 1.75, 2.], - [1.5, 1.75, 2.25, 2.5], - [2.5, 2.75, 3.25, 3.5], - [3., 3.25, 3.75, 4.]], dtype=np.float32) - # Check that resizing by target size, float and int are the same - im2 = misc.imresize(im, (4, 4), mode='F') # output size - im3 = misc.imresize(im, 2., mode='F') # fraction - im4 = misc.imresize(im, 200, mode='F') # percentage - assert_equal(im2, res) - assert_equal(im3, res) - assert_equal(im4, res) - - -if __name__ == '__main__': - unittest.main() diff --git a/build_children.sh b/build_children.sh deleted file mode 100755 index c4ed4ebfa..000000000 --- a/build_children.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/bin/bash - -# Get last child project build number from branch named "latest" -BUILD_NUM=$(curl -s 'https://api.travis-ci.org/repos/python-pillow/pillow-wheels/branches/latest' | grep -o '^{"branch":{"id":[0-9]*,' | grep -o '[0-9]' | tr -d '\n') - -# Restart last child project build -curl -X POST https://api.travis-ci.org/builds/$BUILD_NUM/restart --header "Authorization: token "$AUTH_TOKEN diff --git a/depends/install_imagequant.sh b/depends/install_imagequant.sh index ce5f66d8d..56dfabf8f 100755 --- a/depends/install_imagequant.sh +++ b/depends/install_imagequant.sh @@ -1,7 +1,7 @@ #!/bin/bash # install libimagequant -archive=libimagequant-2.11.10 +archive=libimagequant-2.12.1 ./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/master/$archive.tar.gz diff --git a/depends/install_webp.sh b/depends/install_webp.sh index 37a772436..e27fedc51 100755 --- a/depends/install_webp.sh +++ b/depends/install_webp.sh @@ -1,7 +1,7 @@ #!/bin/bash # install webp -archive=libwebp-0.6.1 +archive=libwebp-1.0.0 ./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/master/$archive.tar.gz diff --git a/docs/PIL.rst b/docs/PIL.rst index 67edb9901..fe69fed62 100644 --- a/docs/PIL.rst +++ b/docs/PIL.rst @@ -62,8 +62,6 @@ can be found here. :undoc-members: :show-inheritance: -.. intentionally skipped documenting this because it's deprecated - :mod:`ImageShow` Module ----------------------- diff --git a/docs/_templates/sidebarhelp.html b/docs/_templates/sidebarhelp.html index da3882e8d..8a33f18dc 100644 --- a/docs/_templates/sidebarhelp.html +++ b/docs/_templates/sidebarhelp.html @@ -1,4 +1,4 @@

Need help?

- You can get help via IRC at irc://irc.freenode.net#pil or Stack Overflow here and here. Please report issues on GitHub. + You can get help via IRC at irc://irc.freenode.net#pil, Gitter or Stack Overflow. Please report issues on GitHub.

diff --git a/docs/about.rst b/docs/about.rst index dd6ca9a98..323593a36 100644 --- a/docs/about.rst +++ b/docs/about.rst @@ -13,7 +13,7 @@ The fork author's goal is to foster and support active development of PIL throug .. _Travis CI: https://travis-ci.org/python-pillow/Pillow .. _AppVeyor: https://ci.appveyor.com/project/Python-pillow/pillow .. _GitHub: https://github.com/python-pillow/Pillow -.. _Python Package Index: https://pypi.python.org/pypi/Pillow +.. _Python Package Index: https://pypi.org/project/Pillow/ License ------- @@ -35,7 +35,7 @@ What about PIL? Prior to Pillow 2.0.0, very few image code changes were made. Pillow 2.0.0 added Python 3 support and includes many bug fixes from many contributors. -As more time passes since the last PIL release, the likelihood of a new PIL release decreases. However, we've yet to hear an official "PIL is dead" announcement. So if you still want to support PIL, please `report issues here first`_, then `open corresponding Pillow tickets here`_. +As more time passes since the last PIL release (1.1.7 in 2009), the likelihood of a new PIL release decreases. However, we've yet to hear an official "PIL is dead" announcement. So if you still want to support PIL, please `report issues here first`_, then `open corresponding Pillow tickets here`_. .. _report issues here first: https://bitbucket.org/effbot/pil-2009-raclette/issues diff --git a/docs/conf.py b/docs/conf.py index 4053e24e6..ba0a552b3 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -53,9 +53,9 @@ author = u'Fredrik Lundh, Alex Clark and Contributors' # # The short X.Y version. import PIL -version = PIL.PILLOW_VERSION +version = PIL.__version__ # The full version, including alpha/beta/rc tags. -release = PIL.PILLOW_VERSION +release = PIL.__version__ # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/docs/handbook/concepts.rst b/docs/handbook/concepts.rst index fd410afe0..b2611c11b 100644 --- a/docs/handbook/concepts.rst +++ b/docs/handbook/concepts.rst @@ -58,6 +58,8 @@ You can read the image size through the :py:attr:`~PIL.Image.Image.size` attribute. This is a 2-tuple, containing the horizontal and vertical size in pixels. +.. _coordinate-system: + Coordinate System ----------------- diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index d265561de..eb50ff23d 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -84,7 +84,14 @@ The :py:meth:`~PIL.Image.Image.open` method sets the following of the GIF, in milliseconds. **loop** - May not be present. The number of times the GIF should loop. + May not be present. The number of times the GIF should loop. 0 means that + it will loop forever. + +**comment** + May not be present. A comment about the image. + +**extension** + May not be present. Contains application specific information. Reading sequences ~~~~~~~~~~~~~~~~~ @@ -110,27 +117,17 @@ are available:: **append_images** A list of images to append as additional frames. Each of the images in the list can be single or multiframe images. - This is currently only supported for GIF, PDF, TIFF, and WebP. + This is currently supported for GIF, PDF, TIFF, and WebP. -**duration** - The display duration of each frame of the multiframe gif, in - milliseconds. Pass a single integer for a constant duration, or a - list or tuple to set the duration for each frame separately. + It is also supported for ICNS. If images are passed in of relevant sizes, + they will be used instead of scaling down the main image. -**loop** - Integer number of times the GIF should loop. +**include_color_table** + Whether or not to include local color table. -**optimize** - If present and true, attempt to compress the palette by - eliminating unused colors. This is only useful if the palette can - be compressed to the next smaller power of 2 elements. - -**palette** - Use the specified palette for the saved image. The palette should - be a bytes or bytearray object containing the palette entries in - RGBRGB... form. It should be no more than 768 bytes. Alternately, - the palette can be passed in as an - :py:class:`PIL.ImagePalette.ImagePalette` object. +**interlace** + Whether or not the image is interlaced. By default, it is, unless the image + is less than 16 pixels in width or height. **disposal** Indicates the way in which the graphic is to be treated after being displayed. @@ -143,6 +140,38 @@ are available:: Pass a single integer for a constant disposal, or a list or tuple to set the disposal for each frame separately. +**palette** + Use the specified palette for the saved image. The palette should + be a bytes or bytearray object containing the palette entries in + RGBRGB... form. It should be no more than 768 bytes. Alternately, + the palette can be passed in as an + :py:class:`PIL.ImagePalette.ImagePalette` object. + +**optimize** + If present and true, attempt to compress the palette by + eliminating unused colors. This is only useful if the palette can + be compressed to the next smaller power of 2 elements. + +Note that if the image you are saving comes from an existing GIF, it may have +the following properties in its :py:attr:`~PIL.Image.Image.info` dictionary. +For these options, if you do not pass them in, they will default to +their :py:attr:`~PIL.Image.Image.info` values. + +**transparency** + Transparency color index. + +**duration** + The display duration of each frame of the multiframe gif, in + milliseconds. Pass a single integer for a constant duration, or a + list or tuple to set the duration for each frame separately. + +**loop** + Integer number of times the GIF should loop. 0 means that it will loop + forever. By default, the image will not loop. + +**comment** + A comment about the image. + Reading local images ~~~~~~~~~~~~~~~~~~~~ @@ -179,6 +208,15 @@ sets the following :py:attr:`~PIL.Image.Image.info` property: ask for ``(512, 512, 2)``, the final value of :py:attr:`~PIL.Image.Image.size` will be ``(1024, 1024)``). +The :py:meth:`~PIL.Image.Image.save` method can take the following keyword arguments: + +**append_images** + A list of images to replace the scaled down versions of the image. + The order of the images does not matter, as their use is determined by + the size of each image. + + .. versionadded:: 5.1.0 + ICO ^^^ @@ -491,8 +529,8 @@ The :py:meth:`~PIL.Image.Image.save` method supports the following options: .. note:: To enable PNG support, you need to build and install the ZLIB compression - library before building the Python Imaging Library. See the installation - documentation for details. + library before building the Python Imaging Library. See the `installation + documentation <../installation.html>`_ for details. PPM ^^^ @@ -546,6 +584,13 @@ For more information about the SPIDER image processing package, see the .. _SPIDER homepage: https://spider.wadsworth.org/spider_doc/spider/docs/spider.html .. _Wadsworth Center: https://www.wadsworth.org/ +TGA +^^^ + +PIL reads and writes TGA images containing ``L``, ``LA``, ``P``, +``RGB``, and ``RGBA`` data. PIL can read and write both uncompressed and +run-length encoded TGAs. + TIFF ^^^^ @@ -767,6 +812,13 @@ PIL reads and writes X bitmap files (mode ``1``). Read-only formats ----------------- +BLP +^^^ + +BLP is the Blizzard Mipmap Format, a texture format used in World of +Warcraft. Pillow supports reading ``JPEG`` Compressed or raw ``BLP1`` +images, and all types of ``BLP2`` images. + CUR ^^^ @@ -844,9 +896,8 @@ The :py:meth:`~PIL.Image.Image.open` method sets the following GD ^^ -PIL reads uncompressed GD files. Note that this file format cannot be -automatically identified, so you must use :py:func:`PIL.GdImageFile.open` to -read such a file. +PIL reads uncompressed GD2 files. Note that you must use +:py:func:`PIL.GdImageFile.open` to read such a file. The :py:meth:`~PIL.Image.Image.open` method sets the following :py:attr:`~PIL.Image.Image.info` properties: @@ -908,11 +959,6 @@ PSD PIL identifies and reads PSD files written by Adobe Photoshop 2.5 and 3.0. -TGA -^^^ - -PIL reads 24- and 32-bit uncompressed and run-length encoded TGA files. - WAL ^^^ diff --git a/docs/index.rst b/docs/index.rst index 8ee910bbd..9106efe8d 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -23,7 +23,7 @@ Pillow is the friendly PIL fork by `Alex Clark and Contributors = 2.0.0 < 4.0.0 supports Python versions 2.6, 2.7, 3.2, 3.3, 3.4, 3.5 - -.. note:: Pillow >= 4.0.0 < 5.0.0 supports Python versions 2.7, 3.3, 3.4, 3.5, 3.6 - -.. note:: Pillow >= 5.0.0 supports Python versions 2.7, 3.4, 3.5, 3.6 ++--------------------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+ +|**Python** |**2.4**|**2.5**|**2.6**|**2.7**|**3.2**|**3.3**|**3.4**|**3.5**|**3.6**|**3.7**| ++--------------------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+ +|Pillow < 2.0.0 | Yes | Yes | Yes | Yes | | | | | | | ++--------------------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+ +|Pillow 2.x - 3.x | | | Yes | Yes | Yes | Yes | Yes | Yes | | | ++--------------------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+ +|Pillow 4.x | | | | Yes | | Yes | Yes | Yes | Yes | | ++--------------------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+ +|Pillow 5.0.x - 5.1.x| | | | Yes | | | Yes | Yes | Yes | | ++--------------------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+ +|Pillow >= 5.2.0 | | | | Yes | | | Yes | Yes | Yes | Yes | ++--------------------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+ Basic Installation ------------------ @@ -96,7 +104,7 @@ Building From Source Download and extract the `compressed archive from PyPI`_. -.. _compressed archive from PyPI: https://pypi.python.org/pypi/Pillow +.. _compressed archive from PyPI: https://pypi.org/project/Pillow/ .. _external-libraries: @@ -120,8 +128,8 @@ Many of Pillow's features require external libraries: * **libjpeg** provides JPEG functionality. - * Pillow has been tested with libjpeg versions **6b**, **8**, **9**, **9a**, - and **9b** and libjpeg-turbo version **8**. + * Pillow has been tested with libjpeg versions **6b**, **8**, **9-9c** and + libjpeg-turbo version **8**. * Starting with Pillow 3.0.0, libjpeg is required by default, but may be disabled with the ``--disable-jpeg`` flag. @@ -157,7 +165,7 @@ Many of Pillow's features require external libraries: * **libimagequant** provides improved color quantization - * Pillow has been tested with libimagequant **2.6-2.11** + * Pillow has been tested with libimagequant **2.6-2.12.1** * Libimagequant is licensed GPLv3, which is more restrictive than the Pillow license, therefore we will not be distributing binaries with libimagequant support enabled. @@ -170,12 +178,12 @@ Many of Pillow's features require external libraries: shaping (using HarfBuzz), and proper script itemization. As a result, Raqm can support most writing systems covered by Unicode. * libraqm depends on the following libraries: FreeType, HarfBuzz, - FriBiDi, make sure that you install them before install libraqm + FriBiDi, make sure that you install them before installing libraqm if not available as package in your system. * setting text direction or font features is not supported without libraqm. * libraqm is dynamically loaded in Pillow 5.0.0 and above, so support - is available if all the libraries are installed. + is available if all the libraries are installed. * Windows support: Raqm support is currently unsupported on Windows. Once you have installed the prerequisites, run:: @@ -207,17 +215,15 @@ Build Options parallel building. * Build flags: ``--disable-zlib``, ``--disable-jpeg``, - ``--disable-tiff``, ``--disable-freetype``, ``--disable-tcl``, - ``--disable-tk``, ``--disable-lcms``, ``--disable-webp``, - ``--disable-webpmux``, ``--disable-jpeg2000``, + ``--disable-tiff``, ``--disable-freetype``, ``--disable-lcms``, + ``--disable-webp``, ``--disable-webpmux``, ``--disable-jpeg2000``, ``--disable-imagequant``. Disable building the corresponding feature even if the development libraries are present on the building machine. * Build flags: ``--enable-zlib``, ``--enable-jpeg``, - ``--enable-tiff``, ``--enable-freetype``, ``--enable-tcl``, - ``--enable-tk``, ``--enable-lcms``, ``--enable-webp``, - ``--enable-webpmux``, ``--enable-jpeg2000``, + ``--enable-tiff``, ``--enable-freetype``, ``--enable-lcms``, + ``--enable-webp``, ``--enable-webpmux``, ``--enable-jpeg2000``, ``--enable-imagequant``. Require that the corresponding feature is built. The build will raise an exception if the libraries are not found. Webpmux (WebP metadata) @@ -388,11 +394,11 @@ These platforms are built and tested for every change. +----------------------------------+-------------------------------+-----------------------+ | Fedora 26 | 2.7 |x86-64 | +----------------------------------+-------------------------------+-----------------------+ -| Mac OS X 10.10 Yosemite* | 2.7, 3.4, 3.5, 3.6 |x86-64 | +| Mac OS X 10.10 Yosemite* | 2.7, 3.4, 3.5, 3.6, 3.7 |x86-64 | +----------------------------------+-------------------------------+-----------------------+ | Ubuntu Linux 16.04 LTS | 2.7 |x86-64 | +----------------------------------+-------------------------------+-----------------------+ -| Ubuntu Linux 14.04 LTS | 2.7, 3.4, 3.5, 3.6, |x86-64 | +| Ubuntu Linux 14.04 LTS | 2.7, 3.4, 3.5, 3.6, 3.7, |x86-64 | | | pypy, pypy3 | | | +-------------------------------+-----------------------+ | | 2.7 |x86 | @@ -444,7 +450,9 @@ These platforms have been reported to work at the versions mentioned. +----------------------------------+------------------------------+--------------------------------+-----------------------+ | Debian 8.2 Jessie | 2.7, 3.4 | 3.1.0 |x86-64 | +----------------------------------+------------------------------+--------------------------------+-----------------------+ -| Raspian Jessie | 2.7, 3.4 | 3.1.0 |arm | +| Raspbian Jessie | 2.7, 3.4 | 3.1.0 |arm | ++----------------------------------+------------------------------+--------------------------------+-----------------------+ +| Raspbian Stretch | 2.7, 3.5 | 4.0.0 |arm | +----------------------------------+------------------------------+--------------------------------+-----------------------+ | Gentoo Linux | 2.7, 3.2 | 2.1.0 |x86-64 | +----------------------------------+------------------------------+--------------------------------+-----------------------+ @@ -467,7 +475,7 @@ Old Versions ------------ You can download old distributions from `PyPI -`_. Only the latest major +`_. Only the latest major releases for Python 2.x and 3.x are visible, but all releases are available by direct URL access -e.g. https://pypi.python.org/pypi/Pillow/1.0. +e.g. https://pypi.org/project/Pillow/1.0/. diff --git a/docs/reference/Image.rst b/docs/reference/Image.rst index 9a9bf57d7..388116a10 100644 --- a/docs/reference/Image.rst +++ b/docs/reference/Image.rst @@ -59,7 +59,7 @@ Functions 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 + .. _the logging documentation: https://docs.python.org/3/library/logging.html#integration-with-the-warnings-module Image processing ^^^^^^^^^^^^^^^^ diff --git a/docs/reference/ImageColor.rst b/docs/reference/ImageColor.rst index f3e6a6f90..187306f1b 100644 --- a/docs/reference/ImageColor.rst +++ b/docs/reference/ImageColor.rst @@ -31,6 +31,13 @@ The ImageColor module supports the following string formats: (black=0%, normal=50%, white=100%). For example, ``hsl(0,100%,50%)`` is pure red. +* Hue-Saturation-Value (HSV) functions, given as ``hsv(hue, saturation%, + value%)`` where hue and saturation are the same as HSL, and value is between + 0% and 100% (black=0%, normal=100%). For example, ``hsv(0,100%,100%)`` is + pure red. This format is also known as Hue-Saturation-Brightness (HSB), and + can be given as ``hsb(hue, saturation%, brightness%)``, where each of the + values are used as they are in HSV. + * Common HTML color names. The :py:mod:`~PIL.ImageColor` module provides some 140 standard color names, based on the colors supported by the X Window system and most web browsers. color names are case insensitive. For example, diff --git a/docs/reference/ImageDraw.rst b/docs/reference/ImageDraw.rst index de26a4d85..6057712e2 100644 --- a/docs/reference/ImageDraw.rst +++ b/docs/reference/ImageDraw.rst @@ -25,7 +25,6 @@ Example: Draw a gray cross over an image draw = ImageDraw.Draw(im) draw.line((0, 0) + im.size, fill=128) draw.line((0, im.size[1], im.size[0], 0), fill=128) - del draw # write to stdout im.save(sys.stdout, "PNG") @@ -133,7 +132,8 @@ Methods angles, inside the given bounding box. :param xy: Two points to define the bounding box. Sequence of - ``[(x0, y0), (x1, y1)]`` or ``[x0, y0, x1, y1]``. + ``[(x0, y0), (x1, y1)]`` or ``[x0, y0, x1, y1]``, + where ``x1 >= x0`` and ``y1 >= y0``. :param start: Starting angle, in degrees. Angles are measured from 3 o'clock, increasing clockwise. :param end: Ending angle, in degrees. @@ -156,7 +156,8 @@ Methods with a straight line. :param xy: Two points to define the bounding box. Sequence of - ``[(x0, y0), (x1, y1)]`` or ``[x0, y0, x1, y1]``. + ``[(x0, y0), (x1, y1)]`` or ``[x0, y0, x1, y1]``, + where ``x1 >= x0`` and ``y1 >= y0``. :param outline: Color to use for the outline. :param fill: Color to use for the fill. @@ -165,7 +166,8 @@ Methods Draws an ellipse inside the given bounding box. :param xy: Two points to define the bounding box. Sequence of either - ``[(x0, y0), (x1, y1)]`` or ``[x0, y0, x1, y1]``. + ``[(x0, y0), (x1, y1)]`` or ``[x0, y0, x1, y1]``, + where ``x1 >= x0`` and ``y1 >= y0``. :param outline: Color to use for the outline. :param fill: Color to use for the fill. @@ -189,7 +191,8 @@ Methods center of the bounding box. :param xy: Two points to define the bounding box. Sequence of - ``[(x0, y0), (x1, y1)]`` or ``[x0, y0, x1, y1]``. + ``[(x0, y0), (x1, y1)]`` or ``[x0, y0, x1, y1]``, + where ``x1 >= x0`` and ``y1 >= y0``. :param start: Starting angle, in degrees. Angles are measured from 3 o'clock, increasing clockwise. :param end: Ending angle, in degrees. @@ -261,7 +264,7 @@ Methods example '-liga' to disable ligatures or '-kern' to disable kerning. To get all supported features, see - https://www.microsoft.com/typography/otspec/featurelist.htm + https://docs.microsoft.com/en-us/typography/opentype/spec/featurelist Requires libraqm. .. versionadded:: 4.2.0 @@ -291,7 +294,7 @@ Methods example '-liga' to disable ligatures or '-kern' to disable kerning. To get all supported features, see - https://www.microsoft.com/typography/otspec/featurelist.htm + https://docs.microsoft.com/en-us/typography/opentype/spec/featurelist Requires libraqm. .. versionadded:: 4.2.0 @@ -320,7 +323,7 @@ Methods example '-liga' to disable ligatures or '-kern' to disable kerning. To get all supported features, see - https://www.microsoft.com/typography/otspec/featurelist.htm + https://docs.microsoft.com/en-us/typography/opentype/spec/featurelist Requires libraqm. .. versionadded:: 4.2.0 @@ -347,7 +350,7 @@ Methods example '-liga' to disable ligatures or '-kern' to disable kerning. To get all supported features, see - https://www.microsoft.com/typography/otspec/featurelist.htm + https://docs.microsoft.com/en-us/typography/opentype/spec/featurelist Requires libraqm. .. versionadded:: 4.2.0 diff --git a/docs/reference/ImageFilter.rst b/docs/reference/ImageFilter.rst index 5275329ab..3368f799f 100644 --- a/docs/reference/ImageFilter.rst +++ b/docs/reference/ImageFilter.rst @@ -37,8 +37,9 @@ image enhancement filters: * **SMOOTH** * **SMOOTH_MORE** -.. autoclass:: PIL.ImageFilter.GaussianBlur +.. autoclass:: PIL.ImageFilter.Color3DLUT .. autoclass:: PIL.ImageFilter.BoxBlur +.. autoclass:: PIL.ImageFilter.GaussianBlur .. autoclass:: PIL.ImageFilter.UnsharpMask .. autoclass:: PIL.ImageFilter.Kernel .. autoclass:: PIL.ImageFilter.RankFilter diff --git a/docs/reference/ImageFont.rst b/docs/reference/ImageFont.rst index 76fde44ff..080e52137 100644 --- a/docs/reference/ImageFont.rst +++ b/docs/reference/ImageFont.rst @@ -81,7 +81,7 @@ Methods example '-liga' to disable ligatures or '-kern' to disable kerning. To get all supported features, see - https://www.microsoft.com/typography/otspec/featurelist.htm + https://docs.microsoft.com/en-us/typography/opentype/spec/featurelist Requires libraqm. .. versionadded:: 4.2.0 diff --git a/docs/reference/open_files.rst b/docs/reference/open_files.rst index d60f63a09..a31a1f37e 100644 --- a/docs/reference/open_files.rst +++ b/docs/reference/open_files.rst @@ -1,3 +1,5 @@ +.. _file-handling: + File Handling in Pillow ======================= diff --git a/docs/releasenotes/4.2.0.rst b/docs/releasenotes/4.2.0.rst index 1b41580a7..e07fd9071 100644 --- a/docs/releasenotes/4.2.0.rst +++ b/docs/releasenotes/4.2.0.rst @@ -6,9 +6,10 @@ Added Complex Text Rendering Pillow now supports complex text rendering for scripts requiring glyph composition and bidirectional flow. This optional feature adds three -dependencies: harfbuzz, fribidi, and raqm. See the install -documentation for further details. This feature is tested and works on -Unix and Mac, but has not yet been built on Windows platforms. +dependencies: harfbuzz, fribidi, and raqm. See the `install +documentation <../installation.html>`_ for further details. This feature is +tested and works on Unix and Mac, but has not yet been built on Windows +platforms. New Optional Parameters ======================= diff --git a/docs/releasenotes/5.1.0.rst b/docs/releasenotes/5.1.0.rst new file mode 100644 index 000000000..2a4c64ac5 --- /dev/null +++ b/docs/releasenotes/5.1.0.rst @@ -0,0 +1,36 @@ +5.1.0 +----- + +New File Format +=============== + +BLP File Format +^^^^^^^^^^^^^^^ + +Pillow now supports reading the BLP "Blizzard Mipmap" file format used +for tiles in Blizzard's engine. + +API Changes +=========== + +Optional channels for TIFF files +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Pillow can now open TIFF files with base modes of ``RGB``, ``YCbCr``, +and ``CMYK`` with up to 6 8-bit channels, discarding any extra +channels if the content is tagged as UNSPECIFIED. Pillow still does +not store more than 4 8-bit channels of image data. + +Append to PDF Files +^^^^^^^^^^^^^^^^^^^ + +Images can now be appended to PDF files in place by passing in +``append=True`` when saving the image. + +Other Changes +============= + +WebP memory leak +^^^^^^^^^^^^^^^^ + +A memory leak when opening ``WebP`` files has been fixed. diff --git a/docs/releasenotes/5.2.0.rst b/docs/releasenotes/5.2.0.rst new file mode 100644 index 000000000..75e8da655 --- /dev/null +++ b/docs/releasenotes/5.2.0.rst @@ -0,0 +1,113 @@ +5.2.0 +----- + +API Changes +=========== + +Deprecations +^^^^^^^^^^^^ + +These version constants have been deprecated. ``VERSION`` will be removed in +Pillow 6.0.0, and ``PILLOW_VERSION`` will be removed after that. + +* ``PIL.VERSION`` (old PIL version 1.1.7) +* ``PIL.PILLOW_VERSION`` +* ``PIL.Image.VERSION`` +* ``PIL.Image.PILLOW_VERSION`` + +Use ``PIL.__version__`` instead. + +API Additions +============= + +3D color lookup tables +^^^^^^^^^^^^^^^^^^^^^^ + +Support for 3D color lookup table transformations has been added. + +* https://en.wikipedia.org/wiki/3D_lookup_table + +``Color3DLUT.generate`` transforms 3-channel pixels using the values of the +channels as coordinates in the 3D lookup table and interpolating the nearest +elements. + +It allows you to apply almost any color transformation in constant time by +using pre-calculated decimated tables. + +``Color3DLUT.transform()`` allows altering table values with a callback. + +If NumPy is installed, the performance of argument conversion is dramatically +improved when a source table supports buffer interface (NumPy && arrays in +Python >= 3). + +ImageColor.getrgb +^^^^^^^^^^^^^^^^^ + +Previously ``Image.rotate`` only supported HSL color strings. Now HSB and HSV +strings are also supported, as well as float values. For example, +``ImageColor.getrgb("hsv(180,100%,99.5%)")``. + +ImageFile.get_format_mimetype +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +``ImageFile.get_format_mimetype`` has been added to return the MIME type of an +image file, where available. For example, +``Image.open("hopper.jpg").get_format_mimetype()`` returns ``"image/jpeg"``. + +ImageFont.getsize_multiline +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +A new method to return the size of multiline text, for example +``font.getsize_multiline("ABC\nAaaa")`` + +Image.rotate +^^^^^^^^^^^^ + +A new named parameter, ``fillcolor``, has been added to ``Image.rotate``. This +color specifies the background color to use in the area outside the rotated +image. This parameter takes the same color specifications as used in +``Image.new``. + + +TGA file format +^^^^^^^^^^^^^^^ + +Pillow can now read and write LA data (in addition to L, P, RGB and RGBA), and +write RLE data (in addition to uncompressed). + +Other Changes +============= + +Support added for Python 3.7 +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Pillow 5.2 supports Python 3.7. + +Build macOS wheels with Xcode 6.4, supporting older macOS versions +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The macOS wheels for Pillow 5.1.0 were built with Xcode 9.2, meaning 10.12 +Sierra was the lowest supported version. + +Prior to Pillow 5.1.0, Xcode 8 was used, supporting El Capitan 10.11. + +Instead, Pillow 5.2.0 is built with the oldest available Xcode 6.4 to support +at least 10.10 Yosemite. + +Fix _i2f compilation with some GCC versions +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +For example, this allows compilation with GCC 4.8 on NetBSD. + +Resolve confusion getting PIL / Pillow version string +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Re: "version constants deprecated" listed above, as user gnbl notes in #3082: + +- it's confusing that PIL.VERSION returns the version string of the former PIL instead of Pillow's +- there does not seem to be documentation on this version number (why this, will it ever change, ..) e.g. at https://pillow.readthedocs.io/en/5.1.x/about.html#why-a-fork +- it's confusing that PIL.version is a module and does not return the version information directly or hints on how to get it +- the package information header is essentially useless (placeholder, does not even mention Pillow, nor the version) +- PIL._version module documentation comment could explain how to access the version information + +We have attempted to resolve these issues in #3083, #3090 and #3218. diff --git a/docs/releasenotes/5.3.0.rst b/docs/releasenotes/5.3.0.rst new file mode 100644 index 000000000..a5591b98d --- /dev/null +++ b/docs/releasenotes/5.3.0.rst @@ -0,0 +1,23 @@ +5.3.0 +----- + +API Additions +============= + +ImageOps.colorize +^^^^^^^^^^^^^^^^^ + +Previously ``ImageOps.colorize`` only supported two-color mapping with +``black`` and ``white`` arguments being mapped to 0 and 255 respectively. +Now it supports three-color mapping with the optional ``mid`` parameter, and +the positions for all three color arguments can each be optionally specified +(``blackpoint``, ``whitepoint`` and ``midpoint``). +For example, with all optional arguments:: + ImageOps.colorize(im, black=(32, 37, 79), white='white', mid=(59, 101, 175), + blackpoint=15, whitepoint=240, midpoint=100) + + + +Other Changes +============= + diff --git a/docs/releasenotes/index.rst b/docs/releasenotes/index.rst index 0ee853fca..fc8d686eb 100644 --- a/docs/releasenotes/index.rst +++ b/docs/releasenotes/index.rst @@ -6,6 +6,9 @@ Release Notes .. toctree:: :maxdepth: 2 + 5.3.0 + 5.2.0 + 5.1.0 5.0.0 4.3.0 4.2.1 diff --git a/selftest.py b/selftest.py index 3f358c583..ed2ba815f 100755 --- a/selftest.py +++ b/selftest.py @@ -161,7 +161,7 @@ if __name__ == "__main__": exit_status = 0 print("-"*68) - print("Pillow", Image.PILLOW_VERSION, "TEST SUMMARY ") + print("Pillow", Image.__version__, "TEST SUMMARY ") print("-"*68) print("Python modules loaded from", os.path.dirname(Image.__file__)) print("Binary modules loaded from", os.path.dirname(Image.core.__file__)) diff --git a/setup.py b/setup.py index 4a0ad86c5..9529787f9 100755 --- a/setup.py +++ b/setup.py @@ -26,7 +26,7 @@ from setuptools import Extension, setup import mp_compile -if sys.platform == "win32" and sys.version_info >= (3, 7): +if sys.platform == "win32" and sys.version_info >= (3, 8): warnings.warn( "Pillow does not yet support Python {}.{} and does not yet provide " "prebuilt Windows binaries. We do not recommend building from " @@ -39,17 +39,17 @@ _IMAGING = ("decode", "encode", "map", "display", "outline", "path") _LIB_IMAGING = ( "Access", "AlphaComposite", "Resample", "Bands", "BcnDecode", "BitDecode", - "Blend", "Chops", "Convert", "ConvertYCbCr", "Copy", "Crop", + "Blend", "Chops", "ColorLUT", "Convert", "ConvertYCbCr", "Copy", "Crop", "Dib", "Draw", "Effects", "EpsEncode", "File", "Fill", "Filter", "FliDecode", "Geometry", "GetBBox", "GifDecode", "GifEncode", "HexDecode", "Histo", "JpegDecode", "JpegEncode", "Matrix", "ModeFilter", "Negative", "Offset", "Pack", "PackDecode", "Palette", "Paste", "Quant", "QuantOctree", "QuantHash", "QuantHeap", "PcdDecode", "PcxDecode", "PcxEncode", "Point", "RankFilter", "RawDecode", "RawEncode", "Storage", - "SgiRleDecode", "SunRleDecode", "TgaRleDecode", "Unpack", "UnpackYCC", - "UnsharpMask", "XbmDecode", "XbmEncode", "ZipDecode", "ZipEncode", - "TiffDecode", "Jpeg2KDecode", "Jpeg2KEncode", "BoxBlur", "QuantPngQuant", - "codec_fd") + "SgiRleDecode", "SunRleDecode", "TgaRleDecode", "TgaRleEncode", "Unpack", + "UnpackYCC", "UnsharpMask", "XbmDecode", "XbmEncode", "ZipDecode", + "ZipEncode", "TiffDecode", "Jpeg2KDecode", "Jpeg2KEncode", "BoxBlur", + "QuantPngQuant", "codec_fd") DEBUG = False @@ -122,7 +122,7 @@ def _read(file): def get_version(): - version_file = 'src/PIL/version.py' + version_file = 'src/PIL/_version.py' with open(version_file, 'r') as f: exec(compile(f.read(), version_file, 'exec')) return locals()['__version__'] @@ -424,7 +424,8 @@ class pil_build_ext(build_ext): best_path = None for name in os.listdir(program_files): if name.startswith('OpenJPEG '): - version = tuple(int(x) for x in name[9:].strip().split('.')) + version = tuple(int(x) for x in + name[9:].strip().split('.')) if version > best_version: best_version = version best_path = os.path.join(program_files, name) @@ -501,7 +502,8 @@ class pil_build_ext(build_ext): # possible. _add_directory(self.compiler.include_dirs, best_path, 0) feature.jpeg2000 = 'openjp2' - feature.openjpeg_version = '.'.join(str(x) for x in best_version) + feature.openjpeg_version = '.'.join(str(x) for x in + best_version) if feature.want('imagequant'): _dbg('Looking for imagequant') @@ -516,7 +518,8 @@ class pil_build_ext(build_ext): if _find_include_file(self, 'tiff.h'): if _find_library_file(self, "tiff"): feature.tiff = "tiff" - if sys.platform == "win32" and _find_library_file(self, "libtiff"): + if (sys.platform == "win32" and + _find_library_file(self, "libtiff")): feature.tiff = "libtiff" if (sys.platform == "darwin" and _find_library_file(self, "libtiff")): @@ -528,14 +531,16 @@ class pil_build_ext(build_ext): # look for freetype2 include files freetype_version = 0 for subdir in self.compiler.include_dirs: - _dbg('Checking for include file %s in %s', ("ft2build.h", subdir)) + _dbg('Checking for include file %s in %s', + ("ft2build.h", subdir)) if os.path.isfile(os.path.join(subdir, "ft2build.h")): _dbg('Found %s in %s', ("ft2build.h", subdir)) freetype_version = 21 subdir = os.path.join(subdir, "freetype2") break subdir = os.path.join(subdir, "freetype2") - _dbg('Checking for include file %s in %s', ("ft2build.h", subdir)) + _dbg('Checking for include file %s in %s', + ("ft2build.h", subdir)) if os.path.isfile(os.path.join(subdir, "ft2build.h")): _dbg('Found %s in %s', ("ft2build.h", subdir)) freetype_version = 21 @@ -763,7 +768,7 @@ try: long_description=_read('README.rst').decode('utf-8'), author='Alex Clark (Fork Author)', author_email='aclark@aclark.net', - url='https://python-pillow.org', + url='http://python-pillow.org', classifiers=[ "Development Status :: 6 - Mature", "Topic :: Multimedia :: Graphics", @@ -778,6 +783,7 @@ try: "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", + "Programming Language :: Python :: 3.7", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", ], diff --git a/src/PIL/BlpImagePlugin.py b/src/PIL/BlpImagePlugin.py index ec358db3b..9b1a99ae1 100644 --- a/src/PIL/BlpImagePlugin.py +++ b/src/PIL/BlpImagePlugin.py @@ -65,7 +65,7 @@ def decode_dxt1(data, alpha=False): for block in range(blocks): # Decode next 8-byte block. idx = block * 8 - color0, color1, bits = struct.unpack("= 52: - for idx, mask in enumerate(['r_mask', 'g_mask', 'b_mask', 'a_mask']): + for idx, mask in enumerate(['r_mask', + 'g_mask', + 'b_mask', + 'a_mask']): file_info[mask] = i32(header_data[36+idx*4:40+idx*4]) else: - # 40 byte headers only have the three components in the bitfields masks, + # 40 byte headers only have the three components in the + # bitfields masks, # ref: https://msdn.microsoft.com/en-us/library/windows/desktop/dd183376(v=vs.85).aspx # See also https://github.com/python-pillow/Pillow/issues/1293 - # There is a 4th component in the RGBQuad, in the alpha location, but it - # is listed as a reserved component, and it is not generally an alpha channel + # There is a 4th component in the RGBQuad, in the alpha + # location, but it is listed as a reserved component, + # and it is not generally an alpha channel file_info['a_mask'] = 0x0 for mask in ['r_mask', 'g_mask', 'b_mask']: file_info[mask] = i32(read(4)) - file_info['rgb_mask'] = (file_info['r_mask'], file_info['g_mask'], file_info['b_mask']) - file_info['rgba_mask'] = (file_info['r_mask'], file_info['g_mask'], file_info['b_mask'], file_info['a_mask']) + file_info['rgb_mask'] = (file_info['r_mask'], + file_info['g_mask'], + file_info['b_mask']) + file_info['rgba_mask'] = (file_info['r_mask'], + file_info['g_mask'], + file_info['b_mask'], + file_info['a_mask']) else: - raise IOError("Unsupported BMP header type (%d)" % file_info['header_size']) + raise IOError("Unsupported BMP header type (%d)" % + file_info['header_size']) # ------------------ Special case : header is reported 40, which # ---------------------- is shorter than real size for bpp >= 16 self.size = file_info['width'], file_info['height'] @@ -127,11 +152,15 @@ class BmpImageFile(ImageFile.ImageFile): # ----------------------- Check bit depth for unusual unsupported values self.mode, raw_mode = BIT2MODE.get(file_info['bits'], (None, None)) if self.mode is None: - raise IOError("Unsupported BMP pixel depth (%d)" % file_info['bits']) + raise IOError("Unsupported BMP pixel depth (%d)" + % file_info['bits']) # ----------------- Process BMP with Bitfields compression (not palette) if file_info['compression'] == self.BITFIELDS: SUPPORTED = { - 32: [(0xff0000, 0xff00, 0xff, 0x0), (0xff0000, 0xff00, 0xff, 0xff000000), (0x0, 0x0, 0x0, 0x0), (0xff000000, 0xff0000, 0xff00, 0x0)], + 32: [(0xff0000, 0xff00, 0xff, 0x0), + (0xff0000, 0xff00, 0xff, 0xff000000), + (0x0, 0x0, 0x0, 0x0), + (0xff000000, 0xff0000, 0xff00, 0x0)], 24: [(0xff0000, 0xff00, 0xff)], 16: [(0xf800, 0x7e0, 0x1f), (0x7c00, 0x3e0, 0x1f)] } @@ -145,11 +174,15 @@ class BmpImageFile(ImageFile.ImageFile): (16, (0x7c00, 0x3e0, 0x1f)): "BGR;15" } if file_info['bits'] in SUPPORTED: - if file_info['bits'] == 32 and file_info['rgba_mask'] in SUPPORTED[file_info['bits']]: + if file_info['bits'] == 32 and \ + file_info['rgba_mask'] in SUPPORTED[file_info['bits']]: raw_mode = MASK_MODES[(file_info['bits'], file_info['rgba_mask'])] self.mode = "RGBA" if raw_mode in ("BGRA",) else self.mode - elif file_info['bits'] in (24, 16) and file_info['rgb_mask'] in SUPPORTED[file_info['bits']]: - raw_mode = MASK_MODES[(file_info['bits'], file_info['rgb_mask'])] + elif (file_info['bits'] in (24, 16) and + file_info['rgb_mask'] in SUPPORTED[file_info['bits']]): + raw_mode = MASK_MODES[ + (file_info['bits'], file_info['rgb_mask']) + ] else: raise IOError("Unsupported BMP bitfields layout") else: @@ -158,17 +191,20 @@ class BmpImageFile(ImageFile.ImageFile): if file_info['bits'] == 32 and header == 22: # 32-bit .cur offset raw_mode, self.mode = "BGRA", "RGBA" else: - raise IOError("Unsupported BMP compression (%d)" % file_info['compression']) + raise IOError("Unsupported BMP compression (%d)" % + file_info['compression']) # ---------------- Once the header is processed, process the palette/LUT if self.mode == "P": # Paletted for 1, 4 and 8 bit images # ----------------------------------------------------- 1-bit images if not (0 < file_info['colors'] <= 65536): - raise IOError("Unsupported BMP Palette size (%d)" % file_info['colors']) + raise IOError("Unsupported BMP Palette size (%d)" % + file_info['colors']) else: padding = file_info['palette_padding'] palette = read(padding * file_info['colors']) greyscale = True - indices = (0, 255) if file_info['colors'] == 2 else list(range(file_info['colors'])) + indices = (0, 255) if file_info['colors'] == 2 else \ + list(range(file_info['colors'])) # ------------------ Check if greyscale and ignore palette if so for ind, val in enumerate(indices): rgb = palette[ind*padding:ind*padding + 3] @@ -180,13 +216,19 @@ class BmpImageFile(ImageFile.ImageFile): raw_mode = self.mode else: self.mode = "P" - self.palette = ImagePalette.raw("BGRX" if padding == 4 else "BGR", palette) + self.palette = ImagePalette.raw( + "BGRX" if padding == 4 else "BGR", palette) # ----------------------------- Finally set the tile data for the plugin self.info['compression'] = file_info['compression'] - self.tile = [('raw', (0, 0, file_info['width'], file_info['height']), offset or self.fp.tell(), - (raw_mode, ((file_info['width'] * file_info['bits'] + 31) >> 3) & (~3), file_info['direction']) - )] + self.tile = [ + ('raw', + (0, 0, file_info['width'], file_info['height']), + offset or self.fp.tell(), + (raw_mode, + ((file_info['width'] * file_info['bits'] + 31) >> 3) & (~3), + file_info['direction'])) + ] def _open(self): """ Open file, check magic number and read header """ diff --git a/src/PIL/ContainerIO.py b/src/PIL/ContainerIO.py index 496ed6826..682ad9031 100644 --- a/src/PIL/ContainerIO.py +++ b/src/PIL/ContainerIO.py @@ -107,10 +107,10 @@ class ContainerIO(object): :returns: A list of 8-bit strings. """ - l = [] + lines = [] while True: s = self.readline() if not s: break - l.append(s) - return l + lines.append(s) + return lines diff --git a/src/PIL/DdsImagePlugin.py b/src/PIL/DdsImagePlugin.py index 9508e61c8..3bd65c93d 100644 --- a/src/PIL/DdsImagePlugin.py +++ b/src/PIL/DdsImagePlugin.py @@ -3,7 +3,7 @@ A Pillow loader for .dds files (S3TC-compressed aka DXTC) Jerome Leclanche Documentation: - http://oss.sgi.com/projects/ogl-sample/registry/EXT/texture_compression_s3tc.txt + https://web.archive.org/web/20170802060935/http://oss.sgi.com/projects/ogl-sample/registry/EXT/texture_compression_s3tc.txt The contents of this file are hereby released in the public domain (CC0) Full text of the CC0 license: @@ -142,7 +142,8 @@ class DdsImageFile(ImageFile.ImageFile): # ignoring flags which pertain to volume textures and cubemaps dxt10 = BytesIO(self.fp.read(20)) dxgi_format, dimension = struct.unpack("= 8 and i32(prefix[:4]) >= 20 and i32(prefix[4:8]) in (1, 2) + return len(prefix) >= 8 and \ + i32(prefix[:4]) >= 20 and i32(prefix[4:8]) in (1, 2) ## @@ -54,7 +55,8 @@ class GbrImageFile(ImageFile.ImageFile): if width <= 0 or height <= 0: raise SyntaxError("not a GIMP brush") if color_depth not in (1, 4): - raise SyntaxError("Unsupported GIMP brush color depth: %s" % color_depth) + raise SyntaxError( + "Unsupported GIMP brush color depth: %s" % color_depth) if version == 1: comment_length = header_size-20 diff --git a/src/PIL/GdImageFile.py b/src/PIL/GdImageFile.py index 09ab5ec69..2ca1e8218 100644 --- a/src/PIL/GdImageFile.py +++ b/src/PIL/GdImageFile.py @@ -24,17 +24,10 @@ from . import ImageFile, ImagePalette -from ._binary import i16be as i16 -from ._util import isPath +from ._binary import i8, i16be as i16, i32be as i32 __version__ = "0.1" -try: - import builtins -except ImportError: - import __builtin__ - builtins = __builtin__ - ## # Image plugin for the GD uncompressed format. Note that this format @@ -50,19 +43,25 @@ class GdImageFile(ImageFile.ImageFile): def _open(self): # Header - s = self.fp.read(775) + s = self.fp.read(1037) + + if not i16(s[:2]) in [65534, 65535]: + raise SyntaxError("Not a valid GD 2.x .gd file") self.mode = "L" # FIXME: "P" - self.size = i16(s[0:2]), i16(s[2:4]) + self.size = i16(s[2:4]), i16(s[4:6]) + + trueColor = i8(s[6]) + trueColorOffset = 2 if trueColor else 0 # transparency index - tindex = i16(s[5:7]) + tindex = i32(s[7+trueColorOffset:7+trueColorOffset+4]) if tindex < 256: - self.info["transparent"] = tindex + self.info["transparency"] = tindex - self.palette = ImagePalette.raw("RGB", s[7:]) + self.palette = ImagePalette.raw("XBGR", s[7+trueColorOffset+4:7+trueColorOffset+4+256*4]) - self.tile = [("raw", (0, 0)+self.size, 775, ("L", 0, -1))] + self.tile = [("raw", (0, 0)+self.size, 7+trueColorOffset+4+256*4, ("L", 0, 1))] def open(fp, mode="r"): @@ -78,13 +77,7 @@ def open(fp, mode="r"): if mode != "r": raise ValueError("bad mode") - if isPath(fp): - filename = fp - fp = builtins.open(fp, "rb") - else: - filename = "" - try: - return GdImageFile(fp, filename) + return GdImageFile(fp) except SyntaxError: raise IOError("cannot identify this image file") diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py index c01adff88..fec2f7663 100644 --- a/src/PIL/GifImagePlugin.py +++ b/src/PIL/GifImagePlugin.py @@ -397,7 +397,8 @@ def _write_multiple_frames(im, fp, palette): im_frames = [] frame_count = 0 - for imSequence in itertools.chain([im], im.encoderinfo.get("append_images", [])): + for imSequence in itertools.chain([im], + im.encoderinfo.get("append_images", [])): for im_frame in ImageSequence.Iterator(imSequence): # a copy is required here since seek can still mutate the image im_frame = _normalize_mode(im_frame.copy()) @@ -413,17 +414,19 @@ def _write_multiple_frames(im, fp, palette): if im_frames: # delta frame previous = im_frames[-1] - if _get_palette_bytes(im_frame) == _get_palette_bytes(previous['im']): + if _get_palette_bytes(im_frame) == \ + _get_palette_bytes(previous['im']): delta = ImageChops.subtract_modulo(im_frame, previous['im']) else: - delta = ImageChops.subtract_modulo(im_frame.convert('RGB'), - previous['im'].convert('RGB')) + delta = ImageChops.subtract_modulo( + im_frame.convert('RGB'), previous['im'].convert('RGB')) bbox = delta.getbbox() if not bbox: # This frame is identical to the previous frame if duration: - previous['encoderinfo']['duration'] += encoderinfo['duration'] + previous['encoderinfo']['duration'] += \ + encoderinfo['duration'] continue else: bbox = None @@ -457,7 +460,8 @@ def _save_all(im, fp, filename): def _save(im, fp, filename, save_all=False): - im.encoderinfo.update(im.info) + for k, v in im.info.items(): + im.encoderinfo.setdefault(k, v) # header try: palette = im.encoderinfo["palette"] @@ -524,7 +528,8 @@ def _write_local_header(fp, im, offset, flags): o8(transparency) + # transparency index o8(0)) - if "comment" in im.encoderinfo and 1 <= len(im.encoderinfo["comment"]) <= 255: + if "comment" in im.encoderinfo and \ + 1 <= len(im.encoderinfo["comment"]) <= 255: fp.write(b"!" + o8(254) + # extension intro o8(len(im.encoderinfo["comment"])) + @@ -542,7 +547,6 @@ def _write_local_header(fp, im, offset, flags): o8(0)) include_color_table = im.encoderinfo.get('include_color_table') if include_color_table: - palette = im.encoderinfo.get("palette", None) palette_bytes = _get_palette_bytes(im) color_table_size = _get_color_table_size(palette_bytes) if color_table_size: @@ -691,7 +695,8 @@ def _get_global_header(im, info): for extensionKey in ["transparency", "duration", "loop", "comment"]: if info and extensionKey in info: if ((extensionKey == "duration" and info[extensionKey] == 0) or - (extensionKey == "comment" and not (1 <= len(info[extensionKey]) <= 255))): + (extensionKey == "comment" and + not (1 <= len(info[extensionKey]) <= 255))): continue version = b"89a" break diff --git a/src/PIL/IcnsImagePlugin.py b/src/PIL/IcnsImagePlugin.py index b382a73e1..dc93f6a74 100644 --- a/src/PIL/IcnsImagePlugin.py +++ b/src/PIL/IcnsImagePlugin.py @@ -310,8 +310,8 @@ def _save(im, fp, filename): # create the temporary set of pngs iconset = tempfile.mkdtemp('.iconset') - provided_images = {im.width:im for im in - im.encoderinfo.get("append_images", [])} + provided_images = {im.width: im + for im in im.encoderinfo.get("append_images", [])} last_w = None for w in [16, 32, 128, 256, 512]: prefix = 'icon_{}x{}'.format(w, w) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 379a1d8a6..f13a98276 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -24,7 +24,11 @@ # See the README file for information on usage and redistribution. # -from . import VERSION, PILLOW_VERSION, _plugins +# VERSION is deprecated and will be removed in Pillow 6.0.0. +# PILLOW_VERSION is deprecated and will be removed after that. +# Use __version__ instead. +from . import VERSION, PILLOW_VERSION, __version__, _plugins +from ._util import py3 import logging import warnings @@ -58,13 +62,13 @@ try: # Also note that Image.core is not a publicly documented interface, # and should be considered private and subject to change. from . import _imaging as core - if PILLOW_VERSION != getattr(core, 'PILLOW_VERSION', None): + if __version__ != getattr(core, 'PILLOW_VERSION', None): raise ImportError("The _imaging extension was built for another " "version of Pillow or PIL:\n" "Core version: %s\n" "Pillow version: %s" % (getattr(core, 'PILLOW_VERSION', None), - PILLOW_VERSION)) + __version__)) except ImportError as v: core = _imaging_not_installed() @@ -117,8 +121,14 @@ import struct import atexit # type stuff -import collections import numbers +try: + # Python 3 + from collections.abc import Callable +except ImportError: + # Python 2.7 + from collections import Callable + # works everywhere, win for pypy, not cpython USE_CFFI_ACCESS = hasattr(sys, 'pypy_version_info') @@ -358,7 +368,7 @@ _initialized = 0 def preinit(): - "Explicitly load standard file format drivers." + """Explicitly load standard file format drivers.""" global _initialized if _initialized >= 1: @@ -568,7 +578,8 @@ class Image(object): This function is only required to close images that have not had their file read and closed by the - :py:meth:`~PIL.Image.Image.load` method. + :py:meth:`~PIL.Image.Image.load` method. See + :ref:`file-handling` for more information. """ try: self.fp.close() @@ -798,8 +809,10 @@ class Image(object): Allocates storage for the image and loads the pixel data. In normal cases, you don't need to call this method, since the Image class automatically loads an opened image when it is - accessed for the first time. This method will close the file - associated with the image. + accessed for the first time. + + This method will close the file associated with the image. See + :ref:`file-handling` for more information. :returns: An image access object. :rtype: :ref:`PixelAccess` or :py:class:`PIL.PyAccess` @@ -1062,7 +1075,7 @@ class Image(object): """ Returns a rectangular region from this image. The box is a 4-tuple defining the left, upper, right, and lower pixel - coordinate. + coordinate. See :ref:`coordinate-system`. Note: Prior to Pillow 3.4.0, this was a lazy operation. @@ -1138,7 +1151,7 @@ class Image(object): self.load() - if isinstance(filter, collections.Callable): + if isinstance(filter, Callable): filter = filter() if not hasattr(filter, "filter"): raise TypeError("filter argument should be ImageFilter.Filter " + @@ -1169,8 +1182,9 @@ class Image(object): image. :returns: The bounding box is returned as a 4-tuple defining the - left, upper, right, and lower pixel coordinate. If the image - is completely empty, this method returns None. + left, upper, right, and lower pixel coordinate. See + :ref:`coordinate-system`. If the image is completely empty, this + method returns None. """ @@ -1260,10 +1274,10 @@ class Image(object): self.load() try: - if bytes is str: - return [i8(c) for c in self.im.getpalette()] - else: + if py3: return list(self.im.getpalette()) + else: + return [i8(c) for c in self.im.getpalette()] except ValueError: return None # no palette @@ -1271,7 +1285,8 @@ class Image(object): """ Returns the pixel value at a given position. - :param xy: The coordinate, given as (x, y). + :param xy: The coordinate, given as (x, y). See + :ref:`coordinate-system`. :returns: The pixel value. If the image is a multi-layer image, this method returns a tuple. """ @@ -1331,8 +1346,8 @@ class Image(object): Pastes another image into this image. The box argument is either a 2-tuple giving the upper left corner, a 4-tuple defining the left, upper, right, and lower pixel coordinate, or None (same as - (0, 0)). If a 4-tuple is given, the size of the pasted image - must match the size of the region. + (0, 0)). See :ref:`coordinate-system`. If a 4-tuple is given, the size + of the pasted image must match the size of the region. If the modes don't match, the pasted image is converted to the mode of this image (see the :py:meth:`~PIL.Image.Image.convert` method for @@ -1586,10 +1601,10 @@ class Image(object): palette = ImagePalette.raw(data.rawmode, data.palette) else: if not isinstance(data, bytes): - if bytes is str: - data = "".join(chr(x) for x in data) - else: + if py3: data = bytes(data) + else: + data = "".join(chr(x) for x in data) palette = ImagePalette.raw(rawmode, data) self.mode = "P" self.palette = palette @@ -1612,7 +1627,8 @@ class Image(object): * :py:meth:`~PIL.Image.Image.putdata` * :py:mod:`~PIL.ImageDraw` - :param xy: The pixel coordinate, given as (x, y). + :param xy: The pixel coordinate, given as (x, y). See + :ref:`coordinate-system`. :param value: The pixel value. """ @@ -1749,7 +1765,7 @@ class Image(object): return self._new(self.im.resize(size, resample, box)) def rotate(self, angle, resample=NEAREST, expand=0, center=None, - translate=None): + translate=None, fillcolor=None): """ Returns a rotated copy of this image. This method returns a copy of this image, rotated the given number of degrees counter @@ -1771,6 +1787,7 @@ class Image(object): :param center: Optional center of rotation (a 2-tuple). Origin is the upper left corner. Default is the center of the image. :param translate: An optional post-rotate translation (a 2-tuple). + :param fillcolor: An optional color for area outside the rotated image. :returns: An :py:class:`~PIL.Image.Image` object. """ @@ -1851,7 +1868,7 @@ class Image(object): matrix) w, h = nw, nh - return self.transform((w, h), AFFINE, matrix, resample) + return self.transform((w, h), AFFINE, matrix, resample, fillcolor=fillcolor) def save(self, fp, format=None, **params): """ @@ -1875,7 +1892,7 @@ class Image(object): format to use is determined from the filename extension. If a file object was used instead of a filename, this parameter should always be used. - :param options: Extra parameters to the image writer. + :param params: Extra parameters to the image writer. :returns: None :exception KeyError: If the output format could not be determined from the file name. Use the format option to solve this. @@ -1898,9 +1915,7 @@ class Image(object): # may mutate self! self.load() - save_all = False - if 'save_all' in params: - save_all = params.pop('save_all') + save_all = params.pop('save_all', False) self.encoderinfo = params self.encoderconfig = () @@ -2096,6 +2111,20 @@ class Image(object): :py:attr:`PIL.Image.QUAD` (map a quadrilateral to a rectangle), or :py:attr:`PIL.Image.MESH` (map a number of source quadrilaterals in one operation). + + It may also be an :py:class:`~PIL.Image.ImageTransformHandler` + object:: + class Example(Image.ImageTransformHandler): + def transform(size, method, data, resample, fill=1): + # Return result + + It may also be an object with a :py:meth:`~method.getdata` method + that returns a tuple supplying new **method** and **data** values:: + class Example(object): + def getdata(self): + method = Image.EXTENT + data = (0, 0, 100, 100) + return method, data :param data: Extra data to the transformation method. :param resample: Optional resampling filter. It can be one of :py:attr:`PIL.Image.NEAREST` (use nearest neighbour), @@ -2103,6 +2132,9 @@ class Image(object): environment), or :py:attr:`PIL.Image.BICUBIC` (cubic spline interpolation in a 4x4 environment). If omitted, or if the image has mode "1" or "P", it is set to :py:attr:`PIL.Image.NEAREST`. + :param fill: If **method** is an + :py:class:`~PIL.Image.ImageTransformHandler` object, this is one of + the arguments passed to it. Otherwise, it is unused. :param fillcolor: Optional fill color for the area outside the transform in the output image. :returns: An :py:class:`~PIL.Image.Image` object. @@ -2110,11 +2142,11 @@ class Image(object): if self.mode == 'LA': return self.convert('La').transform( - size, method, data, resample, fill).convert('LA') + size, method, data, resample, fill, fillcolor).convert('LA') if self.mode == 'RGBA': return self.convert('RGBa').transform( - size, method, data, resample, fill).convert('RGBA') + size, method, data, resample, fill, fillcolor).convert('RGBA') if isinstance(method, ImageTransformHandler): return method.transform(size, self, resample=resample, fill=fill) @@ -2245,7 +2277,7 @@ class ImageTransformHandler(object): # Debugging def _wedge(): - "Create greyscale wedge (for debugging only)" + """Create greyscale wedge (for debugging only)""" return Image()._new(core.wedge("L")) @@ -2408,9 +2440,20 @@ def fromarray(obj, mode=None): Creates an image memory from an object exporting the array interface (using the buffer protocol). - If obj is not contiguous, then the tobytes method is called + If **obj** is not contiguous, then the tobytes method is called and :py:func:`~PIL.Image.frombuffer` is used. + If you have an image in NumPy:: + + from PIL import Image + import numpy as np + im = Image.open('hopper.jpg') + a = numpy.asarray(im) + + Then this can be used to convert it to a Pillow image:: + + im = Image.fromarray(a) + :param obj: Object with array interface :param mode: Mode to use (will be determined from type if None) See: :ref:`concept-modes`. @@ -2522,7 +2565,7 @@ def open(fp, mode="r"): the file remains open and the actual image data is not read from the file until you try to process the data (or call the :py:meth:`~PIL.Image.Image.load` method). See - :py:func:`~PIL.Image.new`. + :py:func:`~PIL.Image.new`. See :ref:`file-handling`. :param fp: A filename (string), pathlib.Path object or a file object. The file object must implement :py:meth:`~file.read`, diff --git a/src/PIL/ImageCms.py b/src/PIL/ImageCms.py index d82e30efc..4b6281f13 100644 --- a/src/PIL/ImageCms.py +++ b/src/PIL/ImageCms.py @@ -305,10 +305,10 @@ def profileToProfile( :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) + ImageCms.INTENT_PERCEPTUAL = 0 (DEFAULT) + ImageCms.INTENT_RELATIVE_COLORIMETRIC = 1 + ImageCms.INTENT_SATURATION = 2 + ImageCms.INTENT_ABSOLUTE_COLORIMETRIC = 3 see the pyCMS documentation for details on rendering intents and what they do. @@ -424,10 +424,10 @@ def buildTransform( :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) + ImageCms.INTENT_PERCEPTUAL = 0 (DEFAULT) + ImageCms.INTENT_RELATIVE_COLORIMETRIC = 1 + ImageCms.INTENT_SATURATION = 2 + ImageCms.INTENT_ABSOLUTE_COLORIMETRIC = 3 see the pyCMS documentation for details on rendering intents and what they do. @@ -512,20 +512,20 @@ def buildProofTransform( :param renderingIntent: Integer (0-3) specifying the rendering intent you wish to use for the input->proof (simulated) 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) + ImageCms.INTENT_PERCEPTUAL = 0 (DEFAULT) + ImageCms.INTENT_RELATIVE_COLORIMETRIC = 1 + ImageCms.INTENT_SATURATION = 2 + ImageCms.INTENT_ABSOLUTE_COLORIMETRIC = 3 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 + :param proofRenderingIntent: Integer (0-3) specifying the rendering intent + you wish to use for proof->output 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) + ImageCms.INTENT_PERCEPTUAL = 0 (DEFAULT) + ImageCms.INTENT_RELATIVE_COLORIMETRIC = 1 + ImageCms.INTENT_SATURATION = 2 + ImageCms.INTENT_ABSOLUTE_COLORIMETRIC = 3 see the pyCMS documentation for details on rendering intents and what they do. @@ -875,10 +875,10 @@ def getDefaultIntent(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) + ImageCms.INTENT_PERCEPTUAL = 0 (DEFAULT) + ImageCms.INTENT_RELATIVE_COLORIMETRIC = 1 + ImageCms.INTENT_SATURATION = 2 + ImageCms.INTENT_ABSOLUTE_COLORIMETRIC = 3 see the pyCMS documentation for details on rendering intents and what they do. @@ -913,15 +913,15 @@ def isIntentSupported(profile, intent, direction): :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) + ImageCms.INTENT_PERCEPTUAL = 0 (DEFAULT) + ImageCms.INTENT_RELATIVE_COLORIMETRIC = 1 + ImageCms.INTENT_SATURATION = 2 + ImageCms.INTENT_ABSOLUTE_COLORIMETRIC = 3 see the pyCMS documentation for details on rendering intents and what they do. - :param direction: Integer specifying if the profile is to be used for input, - output, or proof + :param direction: Integer specifying if the profile is to be used for + input, output, or proof INPUT = 0 (or use ImageCms.DIRECTION_INPUT) OUTPUT = 1 (or use ImageCms.DIRECTION_OUTPUT) diff --git a/src/PIL/ImageColor.py b/src/PIL/ImageColor.py index 4a1c90b27..08c00fd54 100644 --- a/src/PIL/ImageColor.py +++ b/src/PIL/ImageColor.py @@ -87,7 +87,7 @@ def getrgb(color): int((int(m.group(3)) * 255) / 100.0 + 0.5) ) - m = re.match(r"hsl\(\s*(\d+)\s*,\s*(\d+)%\s*,\s*(\d+)%\s*\)$", color) + m = re.match(r"hsl\(\s*(\d+\.?\d*)\s*,\s*(\d+\.?\d*)%\s*,\s*(\d+\.?\d*)%\s*\)$", color) if m: from colorsys import hls_to_rgb rgb = hls_to_rgb( @@ -101,6 +101,20 @@ def getrgb(color): int(rgb[2] * 255 + 0.5) ) + m = re.match(r"hs[bv]\(\s*(\d+\.?\d*)\s*,\s*(\d+\.?\d*)%\s*,\s*(\d+\.?\d*)%\s*\)$", color) + if m: + from colorsys import hsv_to_rgb + rgb = hsv_to_rgb( + float(m.group(1)) / 360.0, + float(m.group(2)) / 100.0, + float(m.group(3)) / 100.0, + ) + return ( + int(rgb[0] * 255 + 0.5), + int(rgb[1] * 255 + 0.5), + int(rgb[2] * 255 + 0.5) + ) + m = re.match(r"rgba\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)$", color) if m: diff --git a/src/PIL/ImageDraw.py b/src/PIL/ImageDraw.py index 89df27338..ca8c1d17b 100644 --- a/src/PIL/ImageDraw.py +++ b/src/PIL/ImageDraw.py @@ -138,7 +138,7 @@ class ImageDraw(object): ink, fill = self._getink(outline, fill) if fill is not None: self.draw.draw_chord(xy, start, end, fill, 1) - if ink is not None: + if ink is not None and ink != fill: self.draw.draw_chord(xy, start, end, ink, 0) def ellipse(self, xy, fill=None, outline=None): @@ -146,7 +146,7 @@ class ImageDraw(object): ink, fill = self._getink(outline, fill) if fill is not None: self.draw.draw_ellipse(xy, fill, 1) - if ink is not None: + if ink is not None and ink != fill: self.draw.draw_ellipse(xy, ink, 0) def line(self, xy, fill=None, width=0): @@ -161,7 +161,7 @@ class ImageDraw(object): ink, fill = self._getink(outline, fill) if fill is not None: self.draw.draw_outline(shape, fill, 1) - if ink is not None: + if ink is not None and ink != fill: self.draw.draw_outline(shape, ink, 0) def pieslice(self, xy, start, end, fill=None, outline=None): @@ -169,7 +169,7 @@ class ImageDraw(object): ink, fill = self._getink(outline, fill) if fill is not None: self.draw.draw_pieslice(xy, start, end, fill, 1) - if ink is not None: + if ink is not None and ink != fill: self.draw.draw_pieslice(xy, start, end, ink, 0) def point(self, xy, fill=None): @@ -183,7 +183,7 @@ class ImageDraw(object): ink, fill = self._getink(outline, fill) if fill is not None: self.draw.draw_polygon(xy, fill, 1) - if ink is not None: + if ink is not None and ink != fill: self.draw.draw_polygon(xy, ink, 0) def rectangle(self, xy, fill=None, outline=None): @@ -191,7 +191,7 @@ class ImageDraw(object): ink, fill = self._getink(outline, fill) if fill is not None: self.draw.draw_rectangle(xy, fill, 1) - if ink is not None: + if ink is not None and ink != fill: self.draw.draw_rectangle(xy, ink, 0) def _multiline_check(self, text): @@ -217,7 +217,8 @@ class ImageDraw(object): ink = fill if ink is not None: try: - mask, offset = font.getmask2(text, self.fontmode, *args, **kwargs) + mask, offset = font.getmask2(text, self.fontmode, + *args, **kwargs) xy = xy[0] + offset[0], xy[1] + offset[1] except AttributeError: try: @@ -271,7 +272,7 @@ class ImageDraw(object): line_width, line_height = self.textsize(line, font, spacing, direction, features) max_width = max(max_width, line_width) - return max_width, len(lines)*line_spacing + return max_width, len(lines)*line_spacing - spacing def Draw(im, mode=None): @@ -327,7 +328,8 @@ def floodfill(image, xy, value, border=None, thresh=0): (experimental) Fills a bounded region with a given color. :param image: Target image. - :param xy: Seed position (a 2-item coordinate tuple). + :param xy: Seed position (a 2-item coordinate tuple). See + :ref:`coordinate-system`. :param value: Fill color. :param border: Optional border value. If given, the region consists of pixels with a color different from the border color. If not given, @@ -383,4 +385,4 @@ def _color_diff(rgb1, rgb2): """ Uses 1-norm distance to calculate difference between two rgb values. """ - return abs(rgb1[0]-rgb2[0]) + abs(rgb1[1]-rgb2[1]) + abs(rgb1[2]-rgb2[2]) + return abs(rgb1[0]-rgb2[0]) + abs(rgb1[1]-rgb2[1]) + abs(rgb1[2]-rgb2[2]) diff --git a/src/PIL/ImageDraw2.py b/src/PIL/ImageDraw2.py index a1763350d..f7902b031 100644 --- a/src/PIL/ImageDraw2.py +++ b/src/PIL/ImageDraw2.py @@ -98,9 +98,6 @@ class Draw(object): def rectangle(self, xy, *options): self.render("rectangle", xy, *options) - def symbol(self, xy, symbol, *options): - raise NotImplementedError("not in this version") - def text(self, xy, text, font): if self.transform: xy = ImagePath.Path(xy) diff --git a/src/PIL/ImageEnhance.py b/src/PIL/ImageEnhance.py index 11c9c3a06..1b78bfd9b 100644 --- a/src/PIL/ImageEnhance.py +++ b/src/PIL/ImageEnhance.py @@ -51,7 +51,8 @@ class Color(_Enhance): if 'A' in image.getbands(): self.intermediate_mode = 'LA' - self.degenerate = image.convert(self.intermediate_mode).convert(image.mode) + self.degenerate = image.convert( + self.intermediate_mode).convert(image.mode) class Contrast(_Enhance): diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py index 1a3c4aa94..bdcc43d1f 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -118,6 +118,11 @@ class ImageFile(Image.Image): pass + def get_format_mimetype(self): + if self.format is None: + return + return Image.MIME.get(self.format.upper()) + def verify(self): "Check file integrity" @@ -161,8 +166,9 @@ class ImageFile(Image.Image): if use_mmap: # try memory mapping decoder_name, extents, offset, args = self.tile[0] - if decoder_name == "raw" and len(args) >= 3 and args[0] == self.mode \ - and args[0] in Image._MAPMODES: + if decoder_name == "raw" and len(args) >= 3 and \ + args[0] == self.mode and \ + args[0] in Image._MAPMODES: try: if hasattr(Image.core, "map"): # use built-in mapper WIN32 only @@ -174,14 +180,15 @@ class ImageFile(Image.Image): else: # use mmap, if possible import mmap - fp = open(self.filename, "r") - size = os.path.getsize(self.filename) - self.map = mmap.mmap(fp.fileno(), size, access=mmap.ACCESS_READ) + with open(self.filename, "r") as fp: + self.map = mmap.mmap(fp.fileno(), 0, + access=mmap.ACCESS_READ) self.im = Image.core.map_buffer( - self.map, self.size, decoder_name, extents, offset, args - ) + self.map, self.size, decoder_name, extents, + offset, args) readonly = 1 - # After trashing self.im, we might need to reload the palette data. + # After trashing self.im, + # we might need to reload the palette data. if self.palette: self.palette.dirty = 1 except (AttributeError, EnvironmentError, ImportError): @@ -213,7 +220,8 @@ class ImageFile(Image.Image): while True: try: s = read(self.decodermaxblock) - except (IndexError, struct.error): # truncated png/gif + except (IndexError, struct.error): + # truncated png/gif if LOAD_TRUNCATED_IMAGES: break else: @@ -225,7 +233,8 @@ class ImageFile(Image.Image): else: self.tile = [] raise IOError("image file is truncated " - "(%d bytes not processed)" % len(b)) + "(%d bytes not processed)" % + len(b)) b = b + s n, err_code = decoder.decode(b) @@ -584,10 +593,12 @@ class PyDecoder(object): """ Override to perform the decoding process. - :param buffer: A bytes object with the data to be decoded. If `handles_eof` - is set, then `buffer` will be empty and `self.fd` will be set. - :returns: A tuple of (bytes consumed, errcode). If finished with decoding - return <0 for the bytes consumed. Err codes are from `ERRORS` + :param buffer: A bytes object with the data to be decoded. + If `handles_eof` is set, then `buffer` will be empty and `self.fd` + will be set. + :returns: A tuple of (bytes consumed, errcode). + If finished with decoding return <0 for the bytes consumed. + Err codes are from `ERRORS` """ raise NotImplementedError() @@ -646,8 +657,8 @@ class PyDecoder(object): Convenience method to set the internal image from a stream of raw data :param data: Bytes to be set - :param rawmode: The rawmode to be used for the decoder. If not specified, - it will default to the mode of the image + :param rawmode: The rawmode to be used for the decoder. + If not specified, it will default to the mode of the image :returns: None """ diff --git a/src/PIL/ImageFilter.py b/src/PIL/ImageFilter.py index 735a00831..e77349df0 100644 --- a/src/PIL/ImageFilter.py +++ b/src/PIL/ImageFilter.py @@ -15,8 +15,15 @@ # See the README file for information on usage and redistribution. # +from __future__ import division + import functools +try: + import numpy +except ImportError: # pragma: no cover + numpy = None + class Filter(object): pass @@ -43,6 +50,7 @@ class Kernel(MultibandFilter): :param offset: Offset. If given, this value is added to the result, after it has been divided by the scale factor. """ + name = "Kernel" def __init__(self, size, kernel, scale=None, offset=0): if scale is None: @@ -130,7 +138,6 @@ class MaxFilter(RankFilter): class ModeFilter(Filter): """ - Create a mode filter. Picks the most frequent pixel value in a box with the given size. Pixel values that occur only once or twice are ignored; if no pixel value occurs more than twice, the original pixel value is preserved. @@ -297,3 +304,183 @@ class SMOOTH_MORE(BuiltinFilter): 1, 5, 5, 5, 1, 1, 1, 1, 1, 1 ) + + +class Color3DLUT(MultibandFilter): + """Three-dimensional color lookup table. + + Transforms 3-channel pixels using the values of the channels as coordinates + in the 3D lookup table and interpolating the nearest elements. + + This method allows you to apply almost any color transformation + in constant time by using pre-calculated decimated tables. + + .. versionadded:: 5.2.0 + + :param size: Size of the table. One int or tuple of (int, int, int). + Minimal size in any dimension is 2, maximum is 65. + :param table: Flat lookup table. A list of ``channels * size**3`` + float elements or a list of ``size**3`` channels-sized + tuples with floats. Channels are changed first, + then first dimension, then second, then third. + Value 0.0 corresponds lowest value of output, 1.0 highest. + :param channels: Number of channels in the table. Could be 3 or 4. + Default is 3. + :param target_mode: A mode for the result image. Should have not less + than ``channels`` channels. Default is ``None``, + which means that mode wouldn't be changed. + """ + name = "Color 3D LUT" + + def __init__(self, size, table, channels=3, target_mode=None, **kwargs): + if channels not in (3, 4): + raise ValueError("Only 3 or 4 output channels are supported") + self.size = size = self._check_size(size) + self.channels = channels + self.mode = target_mode + + # Hidden flag `_copy_table=False` could be used to avoid extra copying + # of the table if the table is specially made for the constructor. + copy_table = kwargs.get('_copy_table', True) + items = size[0] * size[1] * size[2] + wrong_size = False + + if numpy and isinstance(table, numpy.ndarray): + if copy_table: + table = table.copy() + + if table.shape in [(items * channels,), (items, channels), + (size[2], size[1], size[0], channels)]: + table = table.reshape(items * channels) + else: + wrong_size = True + + else: + if copy_table: + table = list(table) + + # Convert to a flat list + if table and isinstance(table[0], (list, tuple)): + table, raw_table = [], table + for pixel in raw_table: + if len(pixel) != channels: + raise ValueError( + "The elements of the table should " + "have a length of {}.".format(channels)) + table.extend(pixel) + + if wrong_size or len(table) != items * channels: + raise ValueError( + "The table should have either channels * size**3 float items " + "or size**3 items of channels-sized tuples with floats. " + "Table should be: {}x{}x{}x{}. Actual length: {}".format( + channels, size[0], size[1], size[2], len(table))) + self.table = table + + @staticmethod + def _check_size(size): + try: + _, _, _ = size + except ValueError: + raise ValueError("Size should be either an integer or " + "a tuple of three integers.") + except TypeError: + size = (size, size, size) + size = [int(x) for x in size] + for size1D in size: + if not 2 <= size1D <= 65: + raise ValueError("Size should be in [2, 65] range.") + return size + + @classmethod + def generate(cls, size, callback, channels=3, target_mode=None): + """Generates new LUT using provided callback. + + :param size: Size of the table. Passed to the constructor. + :param callback: Function with three parameters which correspond + three color channels. Will be called ``size**3`` + times with values from 0.0 to 1.0 and should return + a tuple with ``channels`` elements. + :param channels: The number of channels which should return callback. + :param target_mode: Passed to the constructor of the resulting + lookup table. + """ + size1D, size2D, size3D = cls._check_size(size) + if channels not in (3, 4): + raise ValueError("Only 3 or 4 output channels are supported") + + table = [0] * (size1D * size2D * size3D * channels) + idx_out = 0 + for b in range(size3D): + for g in range(size2D): + for r in range(size1D): + table[idx_out:idx_out + channels] = callback( + r / (size1D-1), g / (size2D-1), b / (size3D-1)) + idx_out += channels + + return cls((size1D, size2D, size3D), table, channels=channels, + target_mode=target_mode, _copy_table=False) + + def transform(self, callback, with_normals=False, channels=None, + target_mode=None): + """Transforms the table values using provided callback and returns + a new LUT with altered values. + + :param callback: A function which takes old lookup table values + and returns a new set of values. The number + of arguments which function should take is + ``self.channels`` or ``3 + self.channels`` + if ``with_normals`` flag is set. + Should return a tuple of ``self.channels`` or + ``channels`` elements if it is set. + :param with_normals: If true, ``callback`` will be called with + coordinates in the color cube as the first + three arguments. Otherwise, ``callback`` + will be called only with actual color values. + :param channels: The number of channels in the resulting lookup table. + :param target_mode: Passed to the constructor of the resulting + lookup table. + """ + if channels not in (None, 3, 4): + raise ValueError("Only 3 or 4 output channels are supported") + ch_in = self.channels + ch_out = channels or ch_in + size1D, size2D, size3D = self.size + + table = [0] * (size1D * size2D * size3D * ch_out) + idx_in = 0 + idx_out = 0 + for b in range(size3D): + for g in range(size2D): + for r in range(size1D): + values = self.table[idx_in:idx_in + ch_in] + if with_normals: + values = callback(r / (size1D-1), g / (size2D-1), + b / (size3D-1), *values) + else: + values = callback(*values) + table[idx_out:idx_out + ch_out] = values + idx_in += ch_in + idx_out += ch_out + + return type(self)(self.size, table, channels=ch_out, + target_mode=target_mode or self.mode, + _copy_table=False) + + def __repr__(self): + r = [ + "{} from {}".format(self.__class__.__name__, + self.table.__class__.__name__), + "size={:d}x{:d}x{:d}".format(*self.size), + "channels={:d}".format(self.channels), + ] + if self.mode: + r.append("target_mode={}".format(self.mode)) + return "<{}>".format(" ".join(r)) + + def filter(self, image): + from . import Image + + return image.color_lut_3d( + self.mode or image.mode, Image.LINEAR, self.channels, + self.size[0], self.size[1], self.size[2], self.table) diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index f3b55e0c4..099ccc4ff 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -26,7 +26,7 @@ # from . import Image -from ._util import isDirectory, isPath +from ._util import isDirectory, isPath, py3 import os import sys @@ -141,12 +141,17 @@ class FreeTypeFont(object): self.layout_engine = layout_engine if isPath(font): - self.font = core.getfont(font, size, index, encoding, layout_engine=layout_engine) + self.font = core.getfont(font, size, index, encoding, + layout_engine=layout_engine) else: self.font_bytes = font.read() self.font = core.getfont( "", size, index, encoding, self.font_bytes, layout_engine) + def _multiline_split(self, text): + split_character = "\n" if isinstance(text, str) else b"\n" + return text.split(split_character) + def getname(self): return self.font.family, self.font.style @@ -157,13 +162,25 @@ class FreeTypeFont(object): size, offset = self.font.getsize(text, direction, features) return (size[0] + offset[0], size[1] + offset[1]) + def getsize_multiline(self, text, direction=None, spacing=4, features=None): + max_width = 0 + lines = self._multiline_split(text) + line_spacing = self.getsize('A')[1] + spacing + for line in lines: + line_width, line_height = self.getsize(line, direction, features) + max_width = max(max_width, line_width) + + return max_width, len(lines)*line_spacing - spacing + def getoffset(self, text): return self.font.getsize(text)[1] def getmask(self, text, mode="", direction=None, features=None): - return self.getmask2(text, mode, direction=direction, features=features)[0] + return self.getmask2(text, mode, direction=direction, + features=features)[0] - def getmask2(self, text, mode="", fill=Image.core.fill, direction=None, features=None, *args, **kwargs): + def getmask2(self, text, mode="", fill=Image.core.fill, direction=None, + features=None, *args, **kwargs): size, offset = self.font.getsize(text, direction, features) im = fill("L", size, 0) self.font.render(text, im.id, mode == "1", direction, features) @@ -180,12 +197,13 @@ class FreeTypeFont(object): :return: A FreeTypeFont object. """ - return FreeTypeFont(font=self.path if font is None else font, - size=self.size if size is None else size, - index=self.index if index is None else index, - encoding=self.encoding if encoding is None else encoding, - layout_engine=self.layout_engine if layout_engine is None else layout_engine - ) + return FreeTypeFont( + font=self.path if font is None else font, + size=self.size if size is None else size, + index=self.index if index is None else index, + encoding=self.encoding if encoding is None else encoding, + layout_engine=self.layout_engine if layout_engine is None else layout_engine + ) class TransposedFont(object): @@ -289,12 +307,16 @@ def truetype(font=None, size=10, index=0, encoding="", for walkfilename in walkfilenames: if ext and walkfilename == ttf_filename: fontpath = os.path.join(walkroot, walkfilename) - return FreeTypeFont(fontpath, size, index, encoding, layout_engine) - elif not ext and os.path.splitext(walkfilename)[0] == ttf_filename: + return FreeTypeFont(fontpath, size, index, + encoding, layout_engine) + elif (not ext and + os.path.splitext(walkfilename)[0] == ttf_filename): fontpath = os.path.join(walkroot, walkfilename) if os.path.splitext(fontpath)[1] == '.ttf': - return FreeTypeFont(fontpath, size, index, encoding, layout_engine) - if not ext and first_font_with_a_different_extension is None: + return FreeTypeFont(fontpath, size, index, + encoding, layout_engine) + if not ext \ + and first_font_with_a_different_extension is None: first_font_with_a_different_extension = fontpath if first_font_with_a_different_extension: return FreeTypeFont(first_font_with_a_different_extension, size, @@ -314,10 +336,10 @@ def load_path(filename): for directory in sys.path: if isDirectory(directory): if not isinstance(filename, str): - if bytes is str: - filename = filename.encode("utf-8") - else: + if py3: filename = filename.decode("utf-8") + else: + filename = filename.encode("utf-8") try: return load(os.path.join(directory, filename)) except IOError: diff --git a/src/PIL/ImageMath.py b/src/PIL/ImageMath.py index c5bea708a..d985877a6 100644 --- a/src/PIL/ImageMath.py +++ b/src/PIL/ImageMath.py @@ -16,6 +16,7 @@ # from . import Image, _imagingmath +from ._util import py3 try: import builtins @@ -100,7 +101,7 @@ class _Operand(object): # an image is "true" if it contains at least one non-zero pixel return self.im.getbbox() is not None - if bytes is str: + if not py3: # Provide __nonzero__ for pre-Py3k __nonzero__ = __bool__ del __bool__ @@ -151,7 +152,7 @@ class _Operand(object): def __rpow__(self, other): return self.apply("pow", other, self) - if bytes is str: + if not py3: # Provide __div__ and __rdiv__ for pre-Py3k __div__ = __truediv__ __rdiv__ = __rtruediv__ diff --git a/src/PIL/ImageMode.py b/src/PIL/ImageMode.py index b227f2127..2b3377a14 100644 --- a/src/PIL/ImageMode.py +++ b/src/PIL/ImageMode.py @@ -42,7 +42,8 @@ def getmode(mode): for m, (basemode, basetype, bands) in Image._MODEINFO.items(): modes[m] = ModeDescriptor(m, bands, basemode, basetype) # extra experimental modes - modes["RGBa"] = ModeDescriptor("RGBa", ("R", "G", "B", "a"), "RGB", "L") + modes["RGBa"] = ModeDescriptor("RGBa", + ("R", "G", "B", "a"), "RGB", "L") modes["LA"] = ModeDescriptor("LA", ("L", "A"), "L", "L") modes["La"] = ModeDescriptor("La", ("L", "a"), "L", "L") modes["PA"] = ModeDescriptor("PA", ("P", "A"), "RGB", "L") diff --git a/src/PIL/ImageMorph.py b/src/PIL/ImageMorph.py index d2367737f..579ee4e1a 100644 --- a/src/PIL/ImageMorph.py +++ b/src/PIL/ImageMorph.py @@ -211,7 +211,7 @@ class MorphOp(object): an image. Returns a list of tuples of (x,y) coordinates - of all matching pixels.""" + of all matching pixels. See :ref:`coordinate-system`.""" if self.lut is None: raise Exception('No operator loaded') @@ -223,7 +223,7 @@ class MorphOp(object): """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.""" + of all matching pixels. See :ref:`coordinate-system`.""" if image.mode != 'L': raise Exception('Image must be binary, meaning it must use mode L') diff --git a/src/PIL/ImageOps.py b/src/PIL/ImageOps.py index 25d491aff..9b470062a 100644 --- a/src/PIL/ImageOps.py +++ b/src/PIL/ImageOps.py @@ -136,28 +136,87 @@ def autocontrast(image, cutoff=0, ignore=None): return _lut(image, lut) -def colorize(image, black, white): +def colorize(image, black, white, mid=None, blackpoint=0, + whitepoint=255, midpoint=127): """ - Colorize grayscale image. The **black** and **white** - arguments should be RGB tuples; this function calculates a color - wedge mapping all black pixels in the source image to the first - color, and all white pixels to the second color. + Colorize grayscale image. + This function calculates a color wedge which maps all black pixels in + the source image to the first color and all white pixels to the + second color. If **mid** is specified, it uses three-color mapping. + The **black** and **white** arguments should be RGB tuples or color names; + optionally you can use three-color mapping by also specifying **mid**. + Mapping positions for any of the colors can be specified + (e.g. **blackpoint**), where these parameters are the integer + value corresponding to where the corresponding color should be mapped. + These parameters must have logical order, such that + **blackpoint** <= **midpoint** <= **whitepoint** (if **mid** is specified). :param image: The image to colorize. :param black: The color to use for black input pixels. :param white: The color to use for white input pixels. + :param mid: The color to use for midtone input pixels. + :param blackpoint: an int value [0, 255] for the black mapping. + :param whitepoint: an int value [0, 255] for the white mapping. + :param midpoint: an int value [0, 255] for the midtone mapping. :return: An image. """ + + # Initial asserts assert image.mode == "L" + if mid is None: + assert 0 <= blackpoint <= whitepoint <= 255 + else: + assert 0 <= blackpoint <= midpoint <= whitepoint <= 255 + + # Define colors from arguments black = _color(black, "RGB") white = _color(white, "RGB") + if mid is not None: + mid = _color(mid, "RGB") + + # Empty lists for the mapping red = [] green = [] blue = [] - for i in range(256): - red.append(black[0]+i*(white[0]-black[0])//255) - green.append(black[1]+i*(white[1]-black[1])//255) - blue.append(black[2]+i*(white[2]-black[2])//255) + + # Create the low-end values + for i in range(0, blackpoint): + red.append(black[0]) + green.append(black[1]) + blue.append(black[2]) + + # Create the mapping (2-color) + if mid is None: + + range_map = range(0, whitepoint - blackpoint) + + for i in range_map: + red.append(black[0] + i * (white[0] - black[0]) // len(range_map)) + green.append(black[1] + i * (white[1] - black[1]) // len(range_map)) + blue.append(black[2] + i * (white[2] - black[2]) // len(range_map)) + + # Create the mapping (3-color) + else: + + range_map1 = range(0, midpoint - blackpoint) + range_map2 = range(0, whitepoint - midpoint) + + for i in range_map1: + red.append(black[0] + i * (mid[0] - black[0]) // len(range_map1)) + green.append(black[1] + i * (mid[1] - black[1]) // len(range_map1)) + blue.append(black[2] + i * (mid[2] - black[2]) // len(range_map1)) + for i in range_map2: + red.append(mid[0] + i * (white[0] - mid[0]) // len(range_map2)) + green.append(mid[1] + i * (white[1] - mid[1]) // len(range_map2)) + blue.append(mid[2] + i * (white[2] - mid[2]) // len(range_map2)) + + # Create the high-end values + for i in range(0, 256 - whitepoint): + red.append(white[0]) + green.append(white[1]) + blue.append(white[2]) + + # Return converted image image = image.convert("RGB") return _lut(image, red + green + blue) diff --git a/src/PIL/ImagePalette.py b/src/PIL/ImagePalette.py index cecc64583..81e99abbf 100644 --- a/src/PIL/ImagePalette.py +++ b/src/PIL/ImagePalette.py @@ -59,7 +59,7 @@ class ImagePalette(object): def getdata(self): """ - Get palette contents in format suitable # for the low-level + Get palette contents in format suitable for the low-level ``im.putpalette`` primitive. .. warning:: This method is experimental. diff --git a/src/PIL/ImagePath.py b/src/PIL/ImagePath.py index 1543508e4..8cbfec0d3 100644 --- a/src/PIL/ImagePath.py +++ b/src/PIL/ImagePath.py @@ -17,44 +17,4 @@ from . import Image -# the Python class below is overridden by the C implementation. - - -class Path(object): - - def __init__(self, xy): - pass - - def compact(self, distance=2): - """ - Compacts the path, by removing points that are close to each other. - This method modifies the path in place. - """ - pass - - def getbbox(self): - """Gets the bounding box.""" - pass - - def map(self, function): - """Maps the path through a function.""" - pass - - def tolist(self, flat=0): - """ - Converts the path to Python list. - # - @param flat By default, this function returns a list of 2-tuples - [(x, y), ...]. If this argument is true, it returns a flat list - [x, y, ...] instead. - @return A list of coordinates. - """ - pass - - def transform(self, matrix): - """Transforms the path.""" - pass - - -# override with C implementation Path = Image.core.path diff --git a/src/PIL/ImageQt.py b/src/PIL/ImageQt.py index 280cbc6fc..2930c1d9c 100644 --- a/src/PIL/ImageQt.py +++ b/src/PIL/ImageQt.py @@ -17,27 +17,35 @@ # from . import Image -from ._util import isPath +from ._util import isPath, py3 from io import BytesIO +import sys -qt_is_installed = True -qt_version = None -try: - from PyQt5.QtGui import QImage, qRgba, QPixmap - from PyQt5.QtCore import QBuffer, QIODevice - qt_version = '5' -except (ImportError, RuntimeError): +qt_versions = [ + ['5', 'PyQt5'], + ['4', 'PyQt4'], + ['side', 'PySide'] +] +# If a version has already been imported, attempt it first +qt_versions.sort(key=lambda qt_version: qt_version[1] in sys.modules, reverse=True) +for qt_version, qt_module in qt_versions: try: - from PyQt4.QtGui import QImage, qRgba, QPixmap - from PyQt4.QtCore import QBuffer, QIODevice - qt_version = '4' - except (ImportError, RuntimeError): - try: + if qt_module == 'PyQt5': + from PyQt5.QtGui import QImage, qRgba, QPixmap + from PyQt5.QtCore import QBuffer, QIODevice + elif qt_module == 'PyQt4': + from PyQt4.QtGui import QImage, qRgba, QPixmap + from PyQt4.QtCore import QBuffer, QIODevice + elif qt_module == 'PySide': from PySide.QtGui import QImage, qRgba, QPixmap from PySide.QtCore import QBuffer, QIODevice - qt_version = 'side' - except ImportError: - qt_is_installed = False + except (ImportError, RuntimeError): + continue + qt_is_installed = True + break +else: + qt_is_installed = False + qt_version = None def rgb(r, g, b, a=255): @@ -111,7 +119,8 @@ def align8to32(bytes, width, mode): new_data = [] for i in range(len(bytes) // bytes_per_line): - new_data.append(bytes[i*bytes_per_line:(i+1)*bytes_per_line] + b'\x00' * extra_padding) + new_data.append(bytes[i*bytes_per_line:(i+1)*bytes_per_line] + + b'\x00' * extra_padding) return b''.join(new_data) @@ -123,10 +132,10 @@ def _toqclass_helper(im): # handle filename, if given instead of image name if hasattr(im, "toUtf8"): # FIXME - is this really the best way to do this? - if str is bytes: - im = unicode(im.toUtf8(), "utf-8") - else: + if py3: im = str(im.toUtf8(), "utf-8") + else: + im = unicode(im.toUtf8(), "utf-8") if isPath(im): im = Image.open(im) diff --git a/src/PIL/ImageStat.py b/src/PIL/ImageStat.py index cd58fc8ff..d4b38d856 100644 --- a/src/PIL/ImageStat.py +++ b/src/PIL/ImageStat.py @@ -110,11 +110,11 @@ class Stat(object): v = [] for i in self.bands: s = 0 - l = self.count[i]//2 + half = self.count[i]//2 b = i * 256 for j in range(256): s = s + self.h[b+j] - if s > l: + if s > half: break v.append(j) return v diff --git a/src/PIL/ImageTk.py b/src/PIL/ImageTk.py index faf52d2b9..17bf32f62 100644 --- a/src/PIL/ImageTk.py +++ b/src/PIL/ImageTk.py @@ -169,8 +169,8 @@ class PhotoImage(object): mode does not match, the image is converted to the mode of the bitmap image. :param box: A 4-tuple defining the left, upper, right, and lower pixel - coordinate. If None is given instead of a tuple, all of - the image is assumed. + coordinate. See :ref:`coordinate-system`. If None is given + instead of a tuple, all of the image is assumed. """ # convert to blittable @@ -195,7 +195,8 @@ class PhotoImage(object): # Pypy is using a ffi cdata element # (Pdb) self.tk.interp # - _imagingtk.tkinit(int(ffi.cast("uintptr_t", tk.interp)), 1) + _imagingtk.tkinit( + int(ffi.cast("uintptr_t", tk.interp)), 1) else: _imagingtk.tkinit(tk.interpaddr(), 1) except AttributeError: diff --git a/src/PIL/ImageTransform.py b/src/PIL/ImageTransform.py index 4207ffb83..c3f6af8b5 100644 --- a/src/PIL/ImageTransform.py +++ b/src/PIL/ImageTransform.py @@ -65,7 +65,7 @@ class ExtentTransform(Transform): See :py:meth:`~PIL.Image.Image.transform` :param bbox: A 4-tuple (x0, y0, x1, y1) which specifies two points in the - input image's coordinate system. + input image's coordinate system. See :ref:`coordinate-system`. """ method = Image.EXTENT diff --git a/src/PIL/ImageWin.py b/src/PIL/ImageWin.py index d8398e92b..9b86270bc 100644 --- a/src/PIL/ImageWin.py +++ b/src/PIL/ImageWin.py @@ -154,8 +154,9 @@ class Dib(object): If the mode does not match, the image is converted to the mode of the bitmap image. :param box: A 4-tuple defining the left, upper, right, and - lower pixel coordinate. If None is given instead of a - tuple, all of the image is assumed. + lower pixel coordinate. See :ref:`coordinate-system`. If + None is given instead of a tuple, all of the image is + assumed. """ im.load() if self.mode != im.mode: diff --git a/src/PIL/Jpeg2KImagePlugin.py b/src/PIL/Jpeg2KImagePlugin.py index 7ab183a4b..7b170fe16 100644 --- a/src/PIL/Jpeg2KImagePlugin.py +++ b/src/PIL/Jpeg2KImagePlugin.py @@ -29,13 +29,13 @@ def _parse_codestream(fp): siz = hdr + fp.read(lsiz - 2) lsiz, rsiz, xsiz, ysiz, xosiz, yosiz, xtsiz, ytsiz, \ xtosiz, ytosiz, csiz \ - = struct.unpack('>HHIIIIIIIIH', siz[:38]) + = struct.unpack_from('>HHIIIIIIIIH', siz) ssiz = [None]*csiz xrsiz = [None]*csiz yrsiz = [None]*csiz for i in range(csiz): ssiz[i], xrsiz[i], yrsiz[i] \ - = struct.unpack('>BBB', siz[36 + 3 * i:39 + 3 * i]) + = struct.unpack_from('>BBB', siz, 36 + 3 * i) size = (xsiz - xosiz, ysiz - yosiz) if csiz == 1: @@ -114,9 +114,9 @@ def _parse_jp2_header(fp): mode = 'RGBA' break elif tbox == b'colr': - meth, prec, approx = struct.unpack('>BBB', content[:3]) + meth, prec, approx = struct.unpack_from('>BBB', content) if meth == 1: - cs = struct.unpack('>I', content[3:7])[0] + cs = struct.unpack_from('>I', content, 3)[0] if cs == 16: # sRGB if nc == 1 and (bpc & 0x7f) > 8: mode = 'I;16' @@ -270,7 +270,8 @@ def _save(im, fp, filename): Image.register_open(Jpeg2KImageFile.format, Jpeg2KImageFile, _accept) Image.register_save(Jpeg2KImageFile.format, _save) -Image.register_extensions(Jpeg2KImageFile.format, [".jp2", ".j2k", ".jpc", ".jpf", ".jpx", ".j2c"]) +Image.register_extensions(Jpeg2KImageFile.format, + [".jp2", ".j2k", ".jpc", ".jpf", ".jpx", ".j2c"]) Image.register_mime(Jpeg2KImageFile.format, 'image/jp2') Image.register_mime(Jpeg2KImageFile.format, 'image/jpx') diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py index 6a18fff19..a75e3d428 100644 --- a/src/PIL/JpegImagePlugin.py +++ b/src/PIL/JpegImagePlugin.py @@ -83,8 +83,9 @@ def APP(self, marker): self.info["jfif_unit"] = jfif_unit self.info["jfif_density"] = jfif_density elif marker == 0xFFE1 and s[:5] == b"Exif\0": - # extract Exif information (incomplete) - self.info["exif"] = s # FIXME: value will change + if "exif" not in self.info: + # extract Exif information (incomplete) + self.info["exif"] = s # FIXME: value will change elif marker == 0xFFE2 and s[:5] == b"FPXR\0": # extract FlashPix information (incomplete) self.info["flashpix"] = s # FIXME: value will change @@ -683,7 +684,7 @@ def _save(im, fp, filename): for idx, table in enumerate(qtables): try: if len(table) != 64: - raise + raise TypeError table = array.array('B', table) except TypeError: raise ValueError("Invalid quantization table") @@ -792,12 +793,13 @@ def jpeg_factory(fp=None, filename=None): return im -# -------------------------------------------------------------------q- +# --------------------------------------------------------------------- # Registry stuff Image.register_open(JpegImageFile.format, jpeg_factory, _accept) Image.register_save(JpegImageFile.format, _save) -Image.register_extensions(JpegImageFile.format, [".jfif", ".jpe", ".jpg", ".jpeg"]) +Image.register_extensions(JpegImageFile.format, + [".jfif", ".jpe", ".jpg", ".jpeg"]) Image.register_mime(JpegImageFile.format, "image/jpeg") diff --git a/src/PIL/MpoImagePlugin.py b/src/PIL/MpoImagePlugin.py index 460ccec27..a1a8d655a 100644 --- a/src/PIL/MpoImagePlugin.py +++ b/src/PIL/MpoImagePlugin.py @@ -85,7 +85,7 @@ class MpoImageFile(JpegImagePlugin.JpegImageFile): return self.__frame -# -------------------------------------------------------------------q- +# --------------------------------------------------------------------- # Registry stuff # Note that since MPO shares a factory with JPEG, we do not need to do a diff --git a/src/PIL/MspImagePlugin.py b/src/PIL/MspImagePlugin.py index 5ea3c1c49..b2c7a3d79 100644 --- a/src/PIL/MspImagePlugin.py +++ b/src/PIL/MspImagePlugin.py @@ -126,14 +126,15 @@ class MspDecoder(ImageFile.PyDecoder): continue row = self.fd.read(rowlen) if len(row) != rowlen: - raise IOError("Truncated MSP file, expected %d bytes on row %s", - (rowlen, x)) + raise IOError( + "Truncated MSP file, expected %d bytes on row %s", + (rowlen, x)) idx = 0 while idx < rowlen: runtype = i8(row[idx]) idx += 1 if runtype == 0: - (runcount, runval) = struct.unpack("Bc", row[idx:idx+2]) + (runcount, runval) = struct.unpack_from("Bc", row, idx) img.write(runval * runcount) idx += 2 else: diff --git a/src/PIL/PSDraw.py b/src/PIL/PSDraw.py index de34713ea..d2ded6fea 100644 --- a/src/PIL/PSDraw.py +++ b/src/PIL/PSDraw.py @@ -16,6 +16,7 @@ # from . import EpsImagePlugin +from ._util import py3 import sys ## @@ -24,7 +25,7 @@ import sys class PSDraw(object): """ - Sets up printing to the given file. If **file** is omitted, + Sets up printing to the given file. If **fp** is omitted, :py:attr:`sys.stdout` is assumed. """ @@ -34,7 +35,7 @@ class PSDraw(object): self.fp = fp def _fp_write(self, to_write): - if bytes is str or self.fp == sys.stdout: + if not py3 or self.fp == sys.stdout: self.fp.write(to_write) else: self.fp.write(bytes(to_write, 'UTF-8')) diff --git a/src/PIL/PdfImagePlugin.py b/src/PIL/PdfImagePlugin.py index e8e0c4f3b..d411bfc41 100644 --- a/src/PIL/PdfImagePlugin.py +++ b/src/PIL/PdfImagePlugin.py @@ -98,7 +98,8 @@ def _save(im, fp, filename, save_all=False): try: im_numberOfPages = im.n_frames except AttributeError: - # Image format does not have n_frames. It is a single frame image + # Image format does not have n_frames. + # It is a single frame image pass numberOfPages += im_numberOfPages for i in range(im_numberOfPages): @@ -113,10 +114,11 @@ def _save(im, fp, filename, save_all=False): pageNumber = 0 for imSequence in ims: - for im in ImageSequence.Iterator(imSequence): - # FIXME: Should replace ASCIIHexDecode with RunLengthDecode (packbits) - # or LZWDecode (tiff/lzw compression). Note that PDF 1.2 also supports - # Flatedecode (zip compression). + im_pages = ImageSequence.Iterator(imSequence) if save_all else [imSequence] + for im in im_pages: + # FIXME: Should replace ASCIIHexDecode with RunLengthDecode + # (packbits) or LZWDecode (tiff/lzw compression). Note that + # PDF 1.2 also supports Flatedecode (zip compression). bits = 8 params = None @@ -134,7 +136,12 @@ def _save(im, fp, filename, save_all=False): elif im.mode == "P": filter = "ASCIIHexDecode" palette = im.im.getpalette("RGB") - colorspace = [PdfParser.PdfName("Indexed"), PdfParser.PdfName("DeviceRGB"), 255, PdfParser.PdfBinary(palette)] + colorspace = [ + PdfParser.PdfName("Indexed"), + PdfParser.PdfName("DeviceRGB"), + 255, + PdfParser.PdfBinary(palette) + ] procset = "ImageI" # indexed color elif im.mode == "RGB": filter = "DCTDecode" @@ -165,7 +172,8 @@ def _save(im, fp, filename, save_all=False): elif filter == "FlateDecode": ImageFile._save(im, op, [("zip", (0, 0)+im.size, 0, im.mode)]) elif filter == "RunLengthDecode": - ImageFile._save(im, op, [("packbits", (0, 0)+im.size, 0, im.mode)]) + ImageFile._save(im, op, + [("packbits", (0, 0)+im.size, 0, im.mode)]) else: raise ValueError("unsupported PDF filter (%s)" % filter) @@ -174,26 +182,37 @@ def _save(im, fp, filename, save_all=False): width, height = im.size - existing_pdf.write_obj(image_refs[pageNumber], stream=op.getvalue(), - Type=PdfParser.PdfName("XObject"), - Subtype=PdfParser.PdfName("Image"), - Width=width, # * 72.0 / resolution, - Height=height, # * 72.0 / resolution, - Filter=PdfParser.PdfName(filter), - BitsPerComponent=bits, - DecodeParams=params, - ColorSpace=colorspace) + existing_pdf.write_obj(image_refs[pageNumber], + stream=op.getvalue(), + Type=PdfParser.PdfName("XObject"), + Subtype=PdfParser.PdfName("Image"), + Width=width, # * 72.0 / resolution, + Height=height, # * 72.0 / resolution, + Filter=PdfParser.PdfName(filter), + BitsPerComponent=bits, + DecodeParams=params, + ColorSpace=colorspace) # # page existing_pdf.write_page(page_refs[pageNumber], - Resources=PdfParser.PdfDict( - ProcSet=[PdfParser.PdfName("PDF"), PdfParser.PdfName(procset)], - XObject=PdfParser.PdfDict(image=image_refs[pageNumber])), - MediaBox=[0, 0, int(width * 72.0 / resolution), int(height * 72.0 / resolution)], - Contents=contents_refs[pageNumber] - ) + Resources=PdfParser.PdfDict( + ProcSet=[ + PdfParser.PdfName("PDF"), + PdfParser.PdfName(procset) + ], + XObject=PdfParser.PdfDict( + image=image_refs[pageNumber] + ) + ), + MediaBox=[ + 0, + 0, + int(width * 72.0 / resolution), + int(height * 72.0 / resolution) + ], + Contents=contents_refs[pageNumber]) # # page contents @@ -203,7 +222,8 @@ def _save(im, fp, filename, save_all=False): int(width * 72.0 / resolution), int(height * 72.0 / resolution))) - existing_pdf.write_obj(contents_refs[pageNumber], stream=page_contents) + existing_pdf.write_obj(contents_refs[pageNumber], + stream=page_contents) pageNumber += 1 diff --git a/src/PIL/PdfParser.py b/src/PIL/PdfParser.py index b6938fdb7..971f44514 100644 --- a/src/PIL/PdfParser.py +++ b/src/PIL/PdfParser.py @@ -4,6 +4,7 @@ import mmap import os import re import zlib +from ._util import py3 try: from UserDict import UserDict # Python 2.x @@ -11,15 +12,16 @@ except ImportError: UserDict = collections.UserDict # Python 3.x -if str == bytes: # Python 2.x - def make_bytes(s): # pragma: no cover - return s # pragma: no cover -else: # Python 3.x +if py3: # Python 3.x def make_bytes(s): return s.encode("us-ascii") +else: # Python 2.x + def make_bytes(s): # pragma: no cover + return s # pragma: no cover -# see 7.9.2.2 Text String Type on page 86 and D.3 PDFDocEncoding Character Set on page 656 +# see 7.9.2.2 Text String Type on page 86 and D.3 PDFDocEncoding Character Set +# on page 656 def encode_text(s): return codecs.BOM_UTF16_BE + s.encode("utf_16_be") @@ -66,20 +68,21 @@ PDFDocEncoding = { 0x9D: u"\u0161", 0x9E: u"\u017E", 0xA0: u"\u20AC", - } +} def decode_text(b): if b[:len(codecs.BOM_UTF16_BE)] == codecs.BOM_UTF16_BE: return b[len(codecs.BOM_UTF16_BE):].decode("utf_16_be") - elif str == bytes: # Python 2.x - return u"".join(PDFDocEncoding.get(ord(byte), byte) for byte in b) - else: + elif py3: # Python 3.x return "".join(PDFDocEncoding.get(byte, chr(byte)) for byte in b) + else: # Python 2.x + return u"".join(PDFDocEncoding.get(ord(byte), byte) for byte in b) class PdfFormatError(RuntimeError): - """An error that probably indicates a syntactic or semantic error in the PDF file structure""" + """An error that probably indicates a syntactic or semantic error in the + PDF file structure""" pass @@ -88,7 +91,8 @@ def check_format_condition(condition, error_message): raise PdfFormatError(error_message) -class IndirectReference(collections.namedtuple("IndirectReferenceTuple", ["object_id", "generation"])): +class IndirectReference(collections.namedtuple("IndirectReferenceTuple", + ["object_id", "generation"])): def __str__(self): return "%s %s R" % self @@ -96,7 +100,9 @@ class IndirectReference(collections.namedtuple("IndirectReferenceTuple", ["objec return self.__str__().encode("us-ascii") def __eq__(self, other): - return other.__class__ is self.__class__ and other.object_id == self.object_id and other.generation == self.generation + return other.__class__ is self.__class__ and \ + other.object_id == self.object_id and \ + other.generation == self.generation def __ne__(self, other): return not (self == other) @@ -142,19 +148,26 @@ class XrefTable: elif key in self.deleted_entries: generation = self.deleted_entries[key] else: - raise IndexError("object ID " + str(key) + " cannot be deleted because it doesn't exist") + raise IndexError("object ID " + str(key) + + " cannot be deleted because it doesn't exist") def __contains__(self, key): return key in self.existing_entries or key in self.new_entries def __len__(self): - return len(set(self.existing_entries.keys()) | set(self.new_entries.keys()) | set(self.deleted_entries.keys())) + return len(set(self.existing_entries.keys()) | + set(self.new_entries.keys()) | + set(self.deleted_entries.keys())) def keys(self): - return (set(self.existing_entries.keys()) - set(self.deleted_entries.keys())) | set(self.new_entries.keys()) + return ( + set(self.existing_entries.keys()) - + set(self.deleted_entries.keys()) + ) | set(self.new_entries.keys()) def write(self, f): - keys = sorted(set(self.new_entries.keys()) | set(self.deleted_entries.keys())) + keys = sorted(set(self.new_entries.keys()) | + set(self.deleted_entries.keys())) deleted_keys = sorted(set(self.deleted_entries.keys())) startxref = f.tell() f.write(b"xref\n") @@ -171,18 +184,25 @@ class XrefTable: else: contiguous_keys = keys keys = None - f.write(make_bytes("%d %d\n" % (contiguous_keys[0], len(contiguous_keys)))) + f.write(make_bytes("%d %d\n" % + (contiguous_keys[0], len(contiguous_keys)))) for object_id in contiguous_keys: if object_id in self.new_entries: - f.write(make_bytes("%010d %05d n \n" % self.new_entries[object_id])) + f.write(make_bytes("%010d %05d n \n" % + self.new_entries[object_id])) else: this_deleted_object_id = deleted_keys.pop(0) - check_format_condition(object_id == this_deleted_object_id, "expected the next deleted object ID to be %s, instead found %s" % (object_id, this_deleted_object_id)) + check_format_condition(object_id == this_deleted_object_id, + "expected the next deleted object " + "ID to be %s, instead found %s" % + (object_id, this_deleted_object_id)) try: next_in_linked_list = deleted_keys[0] except IndexError: next_in_linked_list = 0 - f.write(make_bytes("%010d %05d f \n" % (next_in_linked_list, self.deleted_entries[object_id]))) + f.write(make_bytes("%010d %05d f \n" % + (next_in_linked_list, + self.deleted_entries[object_id]))) return startxref @@ -199,7 +219,8 @@ class PdfName: return self.name.decode("us-ascii") def __eq__(self, other): - return (isinstance(other, PdfName) and other.name == self.name) or other == self.name + return (isinstance(other, PdfName) and other.name == self.name) or \ + other == self.name def __hash__(self): return hash(self.name) @@ -208,26 +229,24 @@ class PdfName: return "PdfName(%s)" % repr(self.name) @classmethod - def from_pdf_stream(klass, data): - return klass(PdfParser.interpret_name(data)) + def from_pdf_stream(cls, data): + return cls(PdfParser.interpret_name(data)) - allowed_chars = set(range(33,127)) - set(ord(c) for c in "#%/()<>[]{}") + allowed_chars = set(range(33, 127)) - set(ord(c) for c in "#%/()<>[]{}") def __bytes__(self): - if str == bytes: # Python 2.x - result = bytearray(b"/") - for b in self.name: - if ord(b) in self.allowed_chars: - result.append(b) - else: - result.extend(b"#%02X" % ord(b)) - else: # Python 3.x - result = bytearray(b"/") - for b in self.name: + result = bytearray(b"/") + for b in self.name: + if py3: # Python 3.x if b in self.allowed_chars: result.append(b) else: result.extend(make_bytes("#%02X" % b)) + else: # Python 2.x + if ord(b) in self.allowed_chars: + result.append(b) + else: + result.extend(b"#%02X" % ord(b)) return bytes(result) __str__ = __bytes__ @@ -241,9 +260,6 @@ class PdfArray(list): class PdfDict(UserDict): - def __init__(self, *args, **kwargs): - UserDict.__init__(self, *args, **kwargs) - def __setattr__(self, key, value): if key == "data": if hasattr(UserDict, "__setattr__"): @@ -281,7 +297,7 @@ class PdfDict(UserDict): out.extend(b"\n>>") return bytes(out) - if str == bytes: + if not py3: __str__ = __bytes__ @@ -289,13 +305,12 @@ class PdfBinary: def __init__(self, data): self.data = data - if str == bytes: # Python 2.x - def __str__(self): - return "<%s>" % "".join("%02X" % ord(b) for b in self.data) - - else: # Python 3.x + if py3: # Python 3.x def __bytes__(self): return make_bytes("<%s>" % "".join("%02X" % b for b in self.data)) + else: # Python 2.x + def __str__(self): + return "<%s>" % "".join("%02X" % ord(b) for b in self.data) class PdfStream: @@ -315,7 +330,9 @@ class PdfStream: expected_length = self.dictionary.Length return zlib.decompress(self.buf, bufsize=int(expected_length)) else: - raise NotImplementedError("stream filter %s unknown/unsupported" % repr(self.dictionary.Filter)) + raise NotImplementedError( + "stream filter %s unknown/unsupported" % + repr(self.dictionary.Filter)) def pdf_repr(x): @@ -325,7 +342,8 @@ def pdf_repr(x): return b"false" elif x is None: return b"null" - elif isinstance(x, PdfName) or isinstance(x, PdfDict) or isinstance(x, PdfArray) or isinstance(x, PdfBinary): + elif (isinstance(x, PdfName) or isinstance(x, PdfDict) or + isinstance(x, PdfArray) or isinstance(x, PdfBinary)): return bytes(x) elif isinstance(x, int): return str(x).encode("us-ascii") @@ -333,23 +351,31 @@ def pdf_repr(x): return bytes(PdfDict(x)) elif isinstance(x, list): return bytes(PdfArray(x)) - elif (str == bytes and isinstance(x, unicode)) or (str != bytes and isinstance(x, str)): + elif ((py3 and isinstance(x, str)) or + (not py3 and isinstance(x, unicode))): return pdf_repr(encode_text(x)) elif isinstance(x, bytes): - return b"(" + x.replace(b"\\", b"\\\\").replace(b"(", b"\\(").replace(b")", b"\\)") + b")" # XXX escape more chars? handle binary garbage + # XXX escape more chars? handle binary garbage + x = x.replace(b"\\", b"\\\\") + x = x.replace(b"(", b"\\(") + x = x.replace(b")", b"\\)") + return b"(" + x + b")" else: return bytes(x) class PdfParser: - """Based on http://www.adobe.com/content/dam/acom/en/devnet/acrobat/pdfs/PDF32000_2008.pdf + """Based on https://www.adobe.com/content/dam/acom/en/devnet/acrobat/pdfs/PDF32000_2008.pdf Supports PDF up to 1.4 """ - def __init__(self, filename=None, f=None, buf=None, start_offset=0, mode="rb"): - # type: (PdfParser, str, file, Union[bytes, bytearray], int, str) -> None + def __init__(self, filename=None, f=None, + buf=None, start_offset=0, mode="rb"): + # type: (PdfParser, str, file, Union[bytes, bytearray], int, str) + # -> None if buf and f: - raise RuntimeError("specify buf or f or filename, but not both buf and f") + raise RuntimeError( + "specify buf or f or filename, but not both buf and f") self.filename = filename self.buf = buf self.f = f @@ -475,7 +501,8 @@ class PdfParser: if self.info: trailer_dict[b"Info"] = self.info_ref self.last_xref_section_offset = start_xref - self.f.write(b"trailer\n" + bytes(PdfDict(trailer_dict)) + make_bytes("\nstartxref\n%d\n%%%%EOF" % start_xref)) + self.f.write(b"trailer\n" + bytes(PdfDict(trailer_dict)) + + make_bytes("\nstartxref\n%d\n%%%%EOF" % start_xref)) def write_page(self, ref, *objs, **dict_obj): if isinstance(ref, int): @@ -537,13 +564,18 @@ class PdfParser: else: self.info = PdfDict(self.read_indirect(self.info_ref)) check_format_condition(b"Type" in self.root, "/Type missing in Root") - check_format_condition(self.root[b"Type"] == b"Catalog", "/Type in Root is not /Catalog") + check_format_condition(self.root[b"Type"] == b"Catalog", + "/Type in Root is not /Catalog") check_format_condition(b"Pages" in self.root, "/Pages missing in Root") - check_format_condition(isinstance(self.root[b"Pages"], IndirectReference), "/Pages in Root is not an indirect reference") + check_format_condition(isinstance(self.root[b"Pages"], + IndirectReference), + "/Pages in Root is not an indirect reference") self.pages_ref = self.root[b"Pages"] self.page_tree_root = self.read_indirect(self.pages_ref) self.pages = self.linearize_page_tree(self.page_tree_root) - # save the original list of page references in case the user modifies, adds or deletes some pages and we need to rewrite the pages and their list + # save the original list of page references + # in case the user modifies, adds or deletes some pages + # and we need to rewrite the pages and their list self.orig_pages = self.pages[:] def next_object_id(self, offset=None): @@ -564,10 +596,14 @@ class PdfParser: whitespace_mandatory = whitespace + b"+" newline_only = br"[\r\n]+" newline = whitespace_optional + newline_only + whitespace_optional - re_trailer_end = re.compile(whitespace_mandatory + br"trailer" + whitespace_optional + br"\<\<(.*\>\>)" + newline - + br"startxref" + newline + br"([0-9]+)" + newline + br"%%EOF" + whitespace_optional + br"$", re.DOTALL) - re_trailer_prev = re.compile(whitespace_optional + br"trailer" + whitespace_optional + br"\<\<(.*?\>\>)" + newline - + br"startxref" + newline + br"([0-9]+)" + newline + br"%%EOF" + whitespace_optional, re.DOTALL) + re_trailer_end = re.compile( + whitespace_mandatory + br"trailer" + whitespace_optional + + br"\<\<(.*\>\>)" + newline + br"startxref" + newline + br"([0-9]+)" + + newline + br"%%EOF" + whitespace_optional + br"$", re.DOTALL) + re_trailer_prev = re.compile( + whitespace_optional + br"trailer" + whitespace_optional + + br"\<\<(.*?\>\>)" + newline + br"startxref" + newline + br"([0-9]+)" + + newline + br"%%EOF" + whitespace_optional, re.DOTALL) def read_trailer(self): search_start_offset = len(self.buf) - 16384 @@ -591,45 +627,61 @@ class PdfParser: self.read_prev_trailer(self.trailer_dict[b"Prev"]) def read_prev_trailer(self, xref_section_offset): - trailer_offset = self.read_xref_table(xref_section_offset=xref_section_offset) - m = self.re_trailer_prev.search(self.buf[trailer_offset:trailer_offset+16384]) + trailer_offset = self.read_xref_table( + xref_section_offset=xref_section_offset) + m = self.re_trailer_prev.search( + self.buf[trailer_offset:trailer_offset+16384]) check_format_condition(m, "previous trailer not found") trailer_data = m.group(1) - check_format_condition(int(m.group(2)) == xref_section_offset, "xref section offset in previous trailer doesn't match what was expected") + check_format_condition(int(m.group(2)) == xref_section_offset, + "xref section offset in previous trailer " + "doesn't match what was expected") trailer_dict = self.interpret_trailer(trailer_data) if b"Prev" in trailer_dict: self.read_prev_trailer(trailer_dict[b"Prev"]) re_whitespace_optional = re.compile(whitespace_optional) - re_name = re.compile(whitespace_optional + br"/([!-$&'*-.0-;=?-Z\\^-z|~]+)(?=" + delimiter_or_ws + br")") + re_name = re.compile( + whitespace_optional + br"/([!-$&'*-.0-;=?-Z\\^-z|~]+)(?=" + + delimiter_or_ws + br")") re_dict_start = re.compile(whitespace_optional + br"\<\<") - re_dict_end = re.compile(whitespace_optional + br"\>\>" + whitespace_optional) + re_dict_end = re.compile( + whitespace_optional + br"\>\>" + whitespace_optional) @classmethod - def interpret_trailer(klass, trailer_data): + def interpret_trailer(cls, trailer_data): trailer = {} offset = 0 while True: - m = klass.re_name.match(trailer_data, offset) + m = cls.re_name.match(trailer_data, offset) if not m: - m = klass.re_dict_end.match(trailer_data, offset) - check_format_condition(m and m.end() == len(trailer_data), "name not found in trailer, remaining data: " + repr(trailer_data[offset:])) + m = cls.re_dict_end.match(trailer_data, offset) + check_format_condition( + m and m.end() == len(trailer_data), + "name not found in trailer, remaining data: " + + repr(trailer_data[offset:])) break - key = klass.interpret_name(m.group(1)) - value, offset = klass.get_value(trailer_data, m.end()) + key = cls.interpret_name(m.group(1)) + value, offset = cls.get_value(trailer_data, m.end()) trailer[key] = value - check_format_condition(b"Size" in trailer and isinstance(trailer[b"Size"], int), "/Size not in trailer or not an integer") - check_format_condition(b"Root" in trailer and isinstance(trailer[b"Root"], IndirectReference), "/Root not in trailer or not an indirect reference") + check_format_condition( + b"Size" in trailer and isinstance(trailer[b"Size"], int), + "/Size not in trailer or not an integer") + check_format_condition( + b"Root" in trailer and + isinstance(trailer[b"Root"], IndirectReference), + "/Root not in trailer or not an indirect reference") return trailer re_hashes_in_name = re.compile(br"([^#]*)(#([0-9a-fA-F]{2}))?") @classmethod - def interpret_name(klass, raw, as_text=False): + def interpret_name(cls, raw, as_text=False): name = b"" - for m in klass.re_hashes_in_name.finditer(raw): + for m in cls.re_hashes_in_name.finditer(raw): if m.group(3): - name += m.group(1) + bytearray.fromhex(m.group(3).decode("us-ascii")) + name += m.group(1) + \ + bytearray.fromhex(m.group(3).decode("us-ascii")) else: name += m.group(1) if as_text: @@ -637,117 +689,158 @@ class PdfParser: else: return bytes(name) - re_null = re.compile(whitespace_optional + br"null(?=" + delimiter_or_ws + br")") - re_true = re.compile(whitespace_optional + br"true(?=" + delimiter_or_ws + br")") - re_false = re.compile(whitespace_optional + br"false(?=" + delimiter_or_ws + br")") - re_int = re.compile(whitespace_optional + br"([-+]?[0-9]+)(?=" + delimiter_or_ws + br")") - re_real = re.compile(whitespace_optional + br"([-+]?([0-9]+\.[0-9]*|[0-9]*\.[0-9]+))(?=" + delimiter_or_ws + br")") + re_null = re.compile( + whitespace_optional + br"null(?=" + delimiter_or_ws + br")") + re_true = re.compile( + whitespace_optional + br"true(?=" + delimiter_or_ws + br")") + re_false = re.compile( + whitespace_optional + br"false(?=" + delimiter_or_ws + br")") + re_int = re.compile( + whitespace_optional + br"([-+]?[0-9]+)(?=" + delimiter_or_ws + br")") + re_real = re.compile( + whitespace_optional + br"([-+]?([0-9]+\.[0-9]*|[0-9]*\.[0-9]+))(?=" + + delimiter_or_ws + br")") re_array_start = re.compile(whitespace_optional + br"\[") re_array_end = re.compile(whitespace_optional + br"]") - re_string_hex = re.compile(whitespace_optional + br"\<(" + whitespace_or_hex + br"*)\>") + re_string_hex = re.compile( + whitespace_optional + br"\<(" + whitespace_or_hex + br"*)\>") re_string_lit = re.compile(whitespace_optional + br"\(") - re_indirect_reference = re.compile(whitespace_optional + br"([-+]?[0-9]+)" + whitespace_mandatory + br"([-+]?[0-9]+)" + whitespace_mandatory + br"R(?=" + delimiter_or_ws + br")") - re_indirect_def_start = re.compile(whitespace_optional + br"([-+]?[0-9]+)" + whitespace_mandatory + br"([-+]?[0-9]+)" + whitespace_mandatory + br"obj(?=" + delimiter_or_ws + br")") - re_indirect_def_end = re.compile(whitespace_optional + br"endobj(?=" + delimiter_or_ws + br")") - re_comment = re.compile(br"(" + whitespace_optional + br"%[^\r\n]*" + newline + br")*") + re_indirect_reference = re.compile( + whitespace_optional + br"([-+]?[0-9]+)" + whitespace_mandatory + + br"([-+]?[0-9]+)" + whitespace_mandatory + br"R(?=" + delimiter_or_ws + + br")") + re_indirect_def_start = re.compile( + whitespace_optional + br"([-+]?[0-9]+)" + whitespace_mandatory + + br"([-+]?[0-9]+)" + whitespace_mandatory + br"obj(?=" + + delimiter_or_ws + br")") + re_indirect_def_end = re.compile( + whitespace_optional + br"endobj(?=" + delimiter_or_ws + br")") + re_comment = re.compile( + br"(" + whitespace_optional + br"%[^\r\n]*" + newline + br")*") re_stream_start = re.compile(whitespace_optional + br"stream\r?\n") - re_stream_end = re.compile(whitespace_optional + br"endstream(?=" + delimiter_or_ws + br")") + re_stream_end = re.compile( + whitespace_optional + br"endstream(?=" + delimiter_or_ws + br")") @classmethod - def get_value(klass, data, offset, expect_indirect=None, max_nesting=-1): + def get_value(cls, data, offset, expect_indirect=None, max_nesting=-1): if max_nesting == 0: return None, None - m = klass.re_comment.match(data, offset) + m = cls.re_comment.match(data, offset) if m: offset = m.end() - m = klass.re_indirect_def_start.match(data, offset) + m = cls.re_indirect_def_start.match(data, offset) if m: - check_format_condition(int(m.group(1)) > 0, "indirect object definition: object ID must be greater than 0") - check_format_condition(int(m.group(2)) >= 0, "indirect object definition: generation must be non-negative") - check_format_condition(expect_indirect is None or expect_indirect == IndirectReference(int(m.group(1)), int(m.group(2))), + check_format_condition( + int(m.group(1)) > 0, + "indirect object definition: object ID must be greater than 0") + check_format_condition( + int(m.group(2)) >= 0, + "indirect object definition: generation must be non-negative") + check_format_condition( + expect_indirect is None or expect_indirect == + IndirectReference(int(m.group(1)), int(m.group(2))), "indirect object definition different than expected") - object, offset = klass.get_value(data, m.end(), max_nesting=max_nesting-1) + object, offset = cls.get_value( + data, m.end(), max_nesting=max_nesting-1) if offset is None: return object, None - m = klass.re_indirect_def_end.match(data, offset) - check_format_condition(m, "indirect object definition end not found") + m = cls.re_indirect_def_end.match(data, offset) + check_format_condition( + m, "indirect object definition end not found") return object, m.end() - check_format_condition(not expect_indirect, "indirect object definition not found") - m = klass.re_indirect_reference.match(data, offset) + check_format_condition( + not expect_indirect, "indirect object definition not found") + m = cls.re_indirect_reference.match(data, offset) if m: - check_format_condition(int(m.group(1)) > 0, "indirect object reference: object ID must be greater than 0") - check_format_condition(int(m.group(2)) >= 0, "indirect object reference: generation must be non-negative") + check_format_condition( + int(m.group(1)) > 0, + "indirect object reference: object ID must be greater than 0") + check_format_condition( + int(m.group(2)) >= 0, + "indirect object reference: generation must be non-negative") return IndirectReference(int(m.group(1)), int(m.group(2))), m.end() - m = klass.re_dict_start.match(data, offset) + m = cls.re_dict_start.match(data, offset) if m: offset = m.end() result = {} - m = klass.re_dict_end.match(data, offset) + m = cls.re_dict_end.match(data, offset) while not m: - key, offset = klass.get_value(data, offset, max_nesting=max_nesting-1) + key, offset = cls.get_value( + data, offset, max_nesting=max_nesting-1) if offset is None: return result, None - value, offset = klass.get_value(data, offset, max_nesting=max_nesting-1) + value, offset = cls.get_value( + data, offset, max_nesting=max_nesting-1) result[key] = value if offset is None: return result, None - m = klass.re_dict_end.match(data, offset) + m = cls.re_dict_end.match(data, offset) offset = m.end() - m = klass.re_stream_start.match(data, offset) + m = cls.re_stream_start.match(data, offset) if m: try: stream_len = int(result[b"Length"]) except (TypeError, KeyError, ValueError): - raise PdfFormatError("bad or missing Length in stream dict (%r)" % result.get(b"Length", None)) + raise PdfFormatError( + "bad or missing Length in stream dict (%r)" % + result.get(b"Length", None)) stream_data = data[m.end():m.end() + stream_len] - m = klass.re_stream_end.match(data, m.end() + stream_len) + m = cls.re_stream_end.match(data, m.end() + stream_len) check_format_condition(m, "stream end not found") offset = m.end() result = PdfStream(PdfDict(result), stream_data) else: result = PdfDict(result) return result, offset - m = klass.re_array_start.match(data, offset) + m = cls.re_array_start.match(data, offset) if m: offset = m.end() result = [] - m = klass.re_array_end.match(data, offset) + m = cls.re_array_end.match(data, offset) while not m: - value, offset = klass.get_value(data, offset, max_nesting=max_nesting-1) + value, offset = cls.get_value( + data, offset, max_nesting=max_nesting-1) result.append(value) if offset is None: return result, None - m = klass.re_array_end.match(data, offset) + m = cls.re_array_end.match(data, offset) return result, m.end() - m = klass.re_null.match(data, offset) + m = cls.re_null.match(data, offset) if m: return None, m.end() - m = klass.re_true.match(data, offset) + m = cls.re_true.match(data, offset) if m: return True, m.end() - m = klass.re_false.match(data, offset) + m = cls.re_false.match(data, offset) if m: return False, m.end() - m = klass.re_name.match(data, offset) + m = cls.re_name.match(data, offset) if m: - return PdfName(klass.interpret_name(m.group(1))), m.end() - m = klass.re_int.match(data, offset) + return PdfName(cls.interpret_name(m.group(1))), m.end() + m = cls.re_int.match(data, offset) if m: return int(m.group(1)), m.end() - m = klass.re_real.match(data, offset) + m = cls.re_real.match(data, offset) if m: - return float(m.group(1)), m.end() # XXX Decimal instead of float??? - m = klass.re_string_hex.match(data, offset) + # XXX Decimal instead of float??? + return float(m.group(1)), m.end() + m = cls.re_string_hex.match(data, offset) if m: - hex_string = bytearray([b for b in m.group(1) if b in b"0123456789abcdefABCDEF"]) # filter out whitespace + # filter out whitespace + hex_string = bytearray([ + b for b in m.group(1) + if b in b"0123456789abcdefABCDEF" + ]) if len(hex_string) % 2 == 1: - hex_string.append(ord(b"0")) # append a 0 if the length is not even - yes, at the end + # append a 0 if the length is not even - yes, at the end + hex_string.append(ord(b"0")) return bytearray.fromhex(hex_string.decode("us-ascii")), m.end() - m = klass.re_string_lit.match(data, offset) + m = cls.re_string_lit.match(data, offset) if m: - return klass.get_literal_string(data, m.end()) - #return None, offset # fallback (only for debugging) - raise PdfFormatError("unrecognized object: " + repr(data[offset:offset+32])) + return cls.get_literal_string(data, m.end()) + # return None, offset # fallback (only for debugging) + raise PdfFormatError( + "unrecognized object: " + repr(data[offset:offset+32])) re_lit_str_token = re.compile(br"(\\[nrtbf()\\])|(\\[0-9]{1,3})|(\\(\r\n|\r|\n))|(\r\n|\r|\n)|(\()|(\))") escaped_chars = { @@ -770,13 +863,13 @@ class PdfParser: } @classmethod - def get_literal_string(klass, data, offset): + def get_literal_string(cls, data, offset): nesting_depth = 0 result = bytearray() - for m in klass.re_lit_str_token.finditer(data, offset): + for m in cls.re_lit_str_token.finditer(data, offset): result.extend(data[offset:m.start()]) if m.group(1): - result.extend(klass.escaped_chars[m.group(1)[1]]) + result.extend(cls.escaped_chars[m.group(1)[1]]) elif m.group(2): result.append(int(m.group(2)[1:], 8)) elif m.group(3): @@ -794,19 +887,24 @@ class PdfParser: offset = m.end() raise PdfFormatError("unfinished literal string") - re_xref_section_start = re.compile(whitespace_optional + br"xref" + newline) - re_xref_subsection_start = re.compile(whitespace_optional + br"([0-9]+)" + whitespace_mandatory + br"([0-9]+)" + whitespace_optional + newline_only) + re_xref_section_start = re.compile( + whitespace_optional + br"xref" + newline) + re_xref_subsection_start = re.compile( + whitespace_optional + br"([0-9]+)" + whitespace_mandatory + + br"([0-9]+)" + whitespace_optional + newline_only) re_xref_entry = re.compile(br"([0-9]{10}) ([0-9]{5}) ([fn])( \r| \n|\r\n)") def read_xref_table(self, xref_section_offset): subsection_found = False - m = self.re_xref_section_start.match(self.buf, xref_section_offset + self.start_offset) + m = self.re_xref_section_start.match( + self.buf, xref_section_offset + self.start_offset) check_format_condition(m, "xref section start not found") offset = m.end() while True: m = self.re_xref_subsection_start.match(self.buf, offset) if not m: - check_format_condition(subsection_found, "xref subsection start not found") + check_format_condition( + subsection_found, "xref subsection start not found") break subsection_found = True offset = m.end() @@ -820,22 +918,31 @@ class PdfParser: generation = int(m.group(2)) if not is_free: new_entry = (int(m.group(1)), generation) - check_format_condition(i not in self.xref_table or self.xref_table[i] == new_entry, "xref entry duplicated (and not identical)") + check_format_condition( + i not in self.xref_table or + self.xref_table[i] == new_entry, + "xref entry duplicated (and not identical)") self.xref_table[i] = new_entry return offset def read_indirect(self, ref, max_nesting=-1): offset, generation = self.xref_table[ref[0]] - check_format_condition(generation == ref[1], "expected to find generation %s for object ID %s in xref table, instead found generation %s at offset %s" \ + check_format_condition( + generation == ref[1], + "expected to find generation %s for object ID %s in xref table, " + "instead found generation %s at offset %s" % (ref[1], ref[0], generation, offset)) - value = self.get_value(self.buf, offset + self.start_offset, expect_indirect=IndirectReference(*ref), max_nesting=max_nesting)[0] + value = self.get_value(self.buf, offset + self.start_offset, + expect_indirect=IndirectReference(*ref), + max_nesting=max_nesting)[0] self.cached_objects[ref] = value return value def linearize_page_tree(self, node=None): if node is None: node = self.page_tree_root - check_format_condition(node[b"Type"] == b"Pages", "/Type of page tree node is not /Pages") + check_format_condition( + node[b"Type"] == b"Pages", "/Type of page tree node is not /Pages") pages = [] for kid in node[b"Kids"]: kid_object = self.read_indirect(kid) diff --git a/src/PIL/PngImagePlugin.py b/src/PIL/PngImagePlugin.py index 621e19b9a..4f1f0f9cf 100644 --- a/src/PIL/PngImagePlugin.py +++ b/src/PIL/PngImagePlugin.py @@ -38,6 +38,7 @@ import struct from . import Image, ImageFile, ImagePalette from ._binary import i8, i16be as i16, i32be as i32, o16be as o16, o32be as o32 +from ._util import py3 __version__ = "0.9" @@ -141,7 +142,8 @@ class ChunkStream(object): def crc(self, cid, data): "Read and verify checksum" - # Skip CRC checks for ancillary chunks if allowed to load truncated images + # Skip CRC checks for ancillary chunks if allowed to load truncated + # images # 5th byte of first char is 1 [specs, section 5.4] if ImageFile.LOAD_TRUNCATED_IMAGES and (i8(cid[0]) >> 5 & 1): self.crc_skip(cid, data) @@ -300,8 +302,8 @@ class PngStream(ChunkStream): def check_text_memory(self, chunklen): self.text_memory += chunklen if self.text_memory > MAX_TEXT_MEMORY: - raise ValueError("Too much memory used in text chunks: %s>MAX_TEXT_MEMORY" % - self.text_memory) + raise ValueError("Too much memory used in text chunks: " + "%s>MAX_TEXT_MEMORY" % self.text_memory) def chunk_iCCP(self, pos, length): @@ -437,7 +439,7 @@ class PngStream(ChunkStream): k = s v = b"" if k: - if bytes is not str: + if py3: k = k.decode('latin-1', 'strict') v = v.decode('latin-1', 'replace') @@ -473,7 +475,7 @@ class PngStream(ChunkStream): v = b"" if k: - if bytes is not str: + if py3: k = k.decode('latin-1', 'strict') v = v.decode('latin-1', 'replace') @@ -510,7 +512,7 @@ class PngStream(ChunkStream): return s else: return s - if bytes is not str: + if py3: try: k = k.decode("latin-1", "strict") lang = lang.decode("utf-8", "strict") @@ -663,7 +665,7 @@ _OUTMODES = { def putchunk(fp, cid, *data): - "Write a PNG chunk (including CRC field)" + """Write a PNG chunk (including CRC field)""" data = b"".join(data) diff --git a/src/PIL/PpmImagePlugin.py b/src/PIL/PpmImagePlugin.py index c599ba8d5..9866c9040 100644 --- a/src/PIL/PpmImagePlugin.py +++ b/src/PIL/PpmImagePlugin.py @@ -83,7 +83,8 @@ class PpmImageFile(ImageFile.ImageFile): if s not in b_whitespace: break if s == b"": - raise ValueError("File does not extend beyond magic number") + raise ValueError( + "File does not extend beyond magic number") if s != b"#": break s = self.fp.readline() diff --git a/src/PIL/PyAccess.py b/src/PIL/PyAccess.py index 7eec1b160..cce2de2b8 100644 --- a/src/PIL/PyAccess.py +++ b/src/PIL/PyAccess.py @@ -68,7 +68,8 @@ class PyAccess(object): numerical value for single band images, and a tuple for multi-band images - :param xy: The pixel coordinate, given as (x, y). + :param xy: The pixel coordinate, given as (x, y). See + :ref:`coordinate-system`. :param color: The pixel value. """ if self.readonly: @@ -82,7 +83,8 @@ class PyAccess(object): value for single band images or a tuple for multiple band images - :param xy: The pixel coordinate, given as (x, y). + :param xy: The pixel coordinate, given as (x, y). See + :ref:`coordinate-system`. :returns: a pixel value for single band images, a tuple of pixel values for multiband images. """ diff --git a/src/PIL/SgiImagePlugin.py b/src/PIL/SgiImagePlugin.py index 8b34561e5..113d44fac 100644 --- a/src/PIL/SgiImagePlugin.py +++ b/src/PIL/SgiImagePlugin.py @@ -24,6 +24,7 @@ from . import Image, ImageFile from ._binary import i8, o8, i16be as i16 +from ._util import py3 import struct import os @@ -165,7 +166,7 @@ def _save(im, fp, filename): pinmax = 255 # Image name (79 characters max, truncated below in write) imgName = os.path.splitext(os.path.basename(filename))[0] - if str is not bytes: + if py3: imgName = imgName.encode('ascii', 'ignore') # Standard representation of pixel in the file colormap = 0 @@ -221,6 +222,7 @@ Image.register_save(SgiImageFile.format, _save) Image.register_mime(SgiImageFile.format, "image/sgi") Image.register_mime(SgiImageFile.format, "image/rgb") -Image.register_extensions(SgiImageFile.format, [".bw", ".rgb", ".rgba", ".sgi"]) +Image.register_extensions(SgiImageFile.format, + [".bw", ".rgb", ".rgba", ".sgi"]) # End of file diff --git a/src/PIL/SpiderImagePlugin.py b/src/PIL/SpiderImagePlugin.py index d89a3e1b3..d502779e2 100644 --- a/src/PIL/SpiderImagePlugin.py +++ b/src/PIL/SpiderImagePlugin.py @@ -200,7 +200,7 @@ class SpiderImageFile(ImageFile.ImageFile): # given a list of filenames, return a list of images def loadImageSeries(filelist=None): - " create a list of Image.images for use in montage " + """create a list of Image.images for use in montage""" if filelist is None or len(filelist) < 1: return diff --git a/src/PIL/SunImagePlugin.py b/src/PIL/SunImagePlugin.py index fd5e82724..3126bd9d6 100644 --- a/src/PIL/SunImagePlugin.py +++ b/src/PIL/SunImagePlugin.py @@ -62,7 +62,7 @@ class SunImageFile(ImageFile.ImageFile): self.size = i32(s[4:8]), i32(s[8:12]) depth = i32(s[12:16]) - data_length = i32(s[16:20]) # unreliable, ignore. + # data_length = i32(s[16:20]) # unreliable, ignore. file_type = i32(s[20:24]) palette_type = i32(s[24:28]) # 0: None, 1: RGB, 2: Raw/arbitrary palette_length = i32(s[28:32]) @@ -94,7 +94,8 @@ class SunImageFile(ImageFile.ImageFile): raise SyntaxError("Unsupported Palette Type") offset = offset + palette_length - self.palette = ImagePalette.raw("RGB;L", self.fp.read(palette_length)) + self.palette = ImagePalette.raw("RGB;L", + self.fp.read(palette_length)) if self.mode == "L": self.mode = "P" rawmode = rawmode.replace('L', 'P') diff --git a/src/PIL/TgaImagePlugin.py b/src/PIL/TgaImagePlugin.py index 76d9ba8de..57b6ae2c8 100644 --- a/src/PIL/TgaImagePlugin.py +++ b/src/PIL/TgaImagePlugin.py @@ -33,6 +33,7 @@ MODES = { (1, 8): "P", (3, 1): "1", (3, 8): "L", + (3, 16): "LA", (2, 16): "BGR;5", (2, 24): "BGR", (2, 32): "BGRA", @@ -74,6 +75,8 @@ class TgaImageFile(ImageFile.ImageFile): self.mode = "L" if depth == 1: self.mode = "1" # ??? + elif depth == 16: + self.mode = "LA" elif imagetype in (1, 9): self.mode = "P" elif imagetype in (2, 10): @@ -134,6 +137,7 @@ class TgaImageFile(ImageFile.ImageFile): SAVE = { "1": ("1", 1, 0, 3), "L": ("L", 8, 0, 3), + "LA": ("LA", 16, 0, 3), "P": ("P", 8, 1, 1), "RGB": ("BGR", 24, 0, 2), "RGBA": ("BGRA", 32, 0, 2), @@ -147,12 +151,17 @@ def _save(im, fp, filename): except KeyError: raise IOError("cannot write mode %s as TGA" % im.mode) + rle = im.encoderinfo.get("rle", False) + + if rle: + imagetype += 8 + if colormaptype: colormapfirst, colormaplength, colormapentry = 0, 256, 24 else: colormapfirst, colormaplength, colormapentry = 0, 0, 0 - if im.mode == "RGBA": + if im.mode in ("LA", "RGBA"): flags = 8 else: flags = 0 @@ -177,8 +186,14 @@ def _save(im, fp, filename): if colormaptype: fp.write(im.im.getpalette("RGB", "BGR")) - ImageFile._save( - im, fp, [("raw", (0, 0) + im.size, 0, (rawmode, 0, orientation))]) + if rle: + ImageFile._save( + im, + fp, + [("tga_rle", (0, 0) + im.size, 0, (rawmode, orientation))]) + else: + ImageFile._save( + im, fp, [("raw", (0, 0) + im.size, 0, (rawmode, 0, orientation))]) # write targa version 2 footer fp.write(b"\000" * 8 + b"TRUEVISION-XFILE." + b"\000") diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index f9039183e..66b211cbf 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -43,8 +43,8 @@ from __future__ import division, print_function from . import Image, ImageFile, ImagePalette, TiffTags from ._binary import i8, o8 +from ._util import py3 -import collections from fractions import Fraction from numbers import Number, Rational @@ -57,6 +57,13 @@ import warnings from .TiffTags import TYPES +try: + # Python 3 + from collections.abc import MutableMapping +except ImportError: + # Python 2.7 + from collections import MutableMapping + __version__ = "1.3.5" DEBUG = False # Needs to be merged with the new logging approach. @@ -397,7 +404,7 @@ class IFDRational(Rational): __round__ = _delegate('__round__') -class ImageFileDirectory_v2(collections.MutableMapping): +class ImageFileDirectory_v2(MutableMapping): """This class represents a TIFF tag directory. To speed things up, we don't decode tags unless they're asked for. @@ -423,7 +430,8 @@ class ImageFileDirectory_v2(collections.MutableMapping): * self.tagtype = {} * Key: numerical tiff tag number - * Value: integer corresponding to the data type from `~PIL.TiffTags.TYPES` + * Value: integer corresponding to the data type from + ~PIL.TiffTags.TYPES` .. versionadded:: 3.0.0 """ @@ -519,7 +527,7 @@ class ImageFileDirectory_v2(collections.MutableMapping): def __contains__(self, tag): return tag in self._tags_v2 or tag in self._tagdata - if bytes is str: + if not py3: def has_key(self, tag): return tag in self @@ -528,7 +536,7 @@ class ImageFileDirectory_v2(collections.MutableMapping): def _setitem(self, tag, value, legacy_api): basetypes = (Number, bytes, str) - if bytes is str: + if not py3: basetypes += unicode, info = TiffTags.lookup(tag) @@ -549,14 +557,14 @@ class ImageFileDirectory_v2(collections.MutableMapping): elif all(isinstance(v, float) for v in values): self.tagtype[tag] = 12 else: - if bytes is str: - # Never treat data as binary by default on Python 2. - self.tagtype[tag] = 2 - else: + if py3: if all(isinstance(v, str) for v in values): self.tagtype[tag] = 2 + else: + # Never treat data as binary by default on Python 2. + self.tagtype[tag] = 2 - if self.tagtype[tag] == 7 and bytes is not str: + if self.tagtype[tag] == 7 and py3: values = [value.encode("ascii", 'replace') if isinstance( value, str) else value] @@ -569,8 +577,8 @@ class ImageFileDirectory_v2(collections.MutableMapping): # Spec'd length == 1, Actual > 1, Warn and truncate. Formerly barfed. # No Spec, Actual length 1, Formerly (<4.2) returned a 1 element tuple. # Don't mess with the legacy api, since it's frozen. - if ((info.length == 1) or - (info.length is None and len(values) == 1 and not legacy_api)): + if (info.length == 1) or \ + (info.length is None and len(values) == 1 and not legacy_api): # Don't mess with the legacy api, since it's frozen. if legacy_api and self.tagtype[tag] in [5, 10]: # rationals values = values, @@ -1237,7 +1245,7 @@ class TiffImageFile(ImageFile.ImageFile): self.info["resolution"] = xres, yres # build tile descriptors - x = y = l = 0 + x = y = layer = 0 self.tile = [] self.use_load_libtiff = False if STRIPOFFSETS in self.tag_v2: @@ -1297,7 +1305,7 @@ class TiffImageFile(ImageFile.ImageFile): else: for i, offset in enumerate(offsets): - a = self._decoder(rawmode, l, i) + a = self._decoder(rawmode, layer, i) self.tile.append( (self._compression, (0, min(y, ysize), w, min(y+h, ysize)), @@ -1307,7 +1315,7 @@ class TiffImageFile(ImageFile.ImageFile): y = y + h if y >= self.size[1]: x = y = 0 - l += 1 + layer += 1 a = None elif TILEOFFSETS in self.tag_v2: # tiled image @@ -1316,7 +1324,7 @@ class TiffImageFile(ImageFile.ImageFile): a = None for o in self.tag_v2[TILEOFFSETS]: if not a: - a = self._decoder(rawmode, l) + a = self._decoder(rawmode, layer) # FIXME: this doesn't work if the image size # is not a multiple of the tile size... self.tile.append( @@ -1328,7 +1336,7 @@ class TiffImageFile(ImageFile.ImageFile): x, y = 0, y + h if y >= self.size[1]: x = y = 0 - l += 1 + layer += 1 a = None else: if DEBUG: @@ -1503,7 +1511,7 @@ def _save(im, fp, filename): if tag not in TiffTags.LIBTIFF_CORE: continue if tag not in atts and tag not in blocklist: - if isinstance(value, unicode if bytes is str else str): + if isinstance(value, str if py3 else unicode): atts[tag] = value.encode('ascii', 'replace') + b"\0" elif isinstance(value, IFDRational): atts[tag] = float(value) diff --git a/src/PIL/TiffTags.py b/src/PIL/TiffTags.py index 427f3a489..ef19ee66e 100644 --- a/src/PIL/TiffTags.py +++ b/src/PIL/TiffTags.py @@ -23,7 +23,8 @@ from collections import namedtuple class TagInfo(namedtuple("_TagInfo", "value name type length enum")): __slots__ = [] - def __new__(cls, value=None, name="unknown", type=None, length=None, enum=None): + def __new__(cls, value=None, name="unknown", + type=None, length=None, enum=None): return super(TagInfo, cls).__new__( cls, value, name, type, length, enum or {}) @@ -72,8 +73,8 @@ TAGS_V2 = { 257: ("ImageLength", LONG, 1), 258: ("BitsPerSample", SHORT, 0), 259: ("Compression", SHORT, 1, - {"Uncompressed": 1, "CCITT 1d": 2, "Group 3 Fax": 3, "Group 4 Fax": 4, - "LZW": 5, "JPEG": 6, "PackBits": 32773}), + {"Uncompressed": 1, "CCITT 1d": 2, "Group 3 Fax": 3, + "Group 4 Fax": 4, "LZW": 5, "JPEG": 6, "PackBits": 32773}), 262: ("PhotometricInterpretation", SHORT, 1, {"WhiteIsZero": 0, "BlackIsZero": 1, "RGB": 2, "RGB Palette": 3, diff --git a/src/PIL/WmfImagePlugin.py b/src/PIL/WmfImagePlugin.py index 173ddb25b..aeb19374d 100644 --- a/src/PIL/WmfImagePlugin.py +++ b/src/PIL/WmfImagePlugin.py @@ -22,14 +22,16 @@ from __future__ import print_function from . import Image, ImageFile -from ._binary import i16le as word, si16le as short, i32le as dword, si32le as _long +from ._binary import i16le as word, si16le as short, \ + i32le as dword, si32le as _long +from ._util import py3 __version__ = "0.2" _handler = None -if str != bytes: +if py3: long = int diff --git a/src/PIL/__init__.py b/src/PIL/__init__.py index 76188a0a3..a07280e31 100644 --- a/src/PIL/__init__.py +++ b/src/PIL/__init__.py @@ -1,22 +1,31 @@ -# -# The Python Imaging Library. -# $Id$ -# -# package placeholder -# -# Copyright (c) 1999 by Secret Labs AB. -# -# See the README file for information on usage and redistribution. -# +"""Pillow {} (Fork of the Python Imaging Library) -# ;-) +Pillow is the friendly PIL fork by Alex Clark and Contributors. + https://github.com/python-pillow/Pillow/ -from . import version +Pillow is forked from PIL 1.1.7. +PIL is the Python Imaging Library by Fredrik Lundh and Contributors. +Copyright (c) 1999 by Secret Labs AB. + +Use PIL.__version__ for this Pillow version. +PIL.VERSION is the old PIL version and will be removed in the future. + +;-) +""" + +from . import _version + +# VERSION is deprecated and will be removed in Pillow 6.0.0. +# PILLOW_VERSION is deprecated and will be removed after that. +# Use __version__ instead. VERSION = '1.1.7' # PIL Version -PILLOW_VERSION = version.__version__ +PILLOW_VERSION = __version__ = _version.__version__ + +del _version + +__doc__ = __doc__.format(__version__) # include version in docstring -__version__ = PILLOW_VERSION _plugins = ['BlpImagePlugin', 'BmpImagePlugin', diff --git a/src/PIL/_binary.py b/src/PIL/_binary.py index b15f796c0..767c13b9d 100644 --- a/src/PIL/_binary.py +++ b/src/PIL/_binary.py @@ -11,20 +11,21 @@ # See the README file for information on usage and redistribution. # -from struct import unpack, pack +from struct import unpack_from, pack +from ._util import py3 -if bytes is str: - def i8(c): - return ord(c) - - def o8(i): - return chr(i & 255) -else: +if py3: def i8(c): return c if c.__class__ is int else c[0] def o8(i): return bytes((i & 255,)) +else: + def i8(c): + return ord(c) + + def o8(i): + return chr(i & 255) # Input, le = little endian, be = big endian @@ -35,7 +36,7 @@ def i16le(c, o=0): c: string containing bytes to convert o: offset of bytes to convert in string """ - return unpack("H", c[o:o+2])[0] + return unpack_from(">H", c, o)[0] def i32be(c, o=0): - return unpack(">I", c[o:o+4])[0] + return unpack_from(">I", c, o)[0] # Output, le = little endian, be = big endian diff --git a/src/PIL/_util.py b/src/PIL/_util.py index 51c6f6887..e6989d69d 100644 --- a/src/PIL/_util.py +++ b/src/PIL/_util.py @@ -1,17 +1,20 @@ import os +import sys -if bytes is str: - def isStringType(t): - return isinstance(t, basestring) +py3 = sys.version_info.major >= 3 - def isPath(f): - return isinstance(f, basestring) -else: +if py3: def isStringType(t): return isinstance(t, str) def isPath(f): return isinstance(f, (bytes, str)) +else: + def isStringType(t): + return isinstance(t, basestring) + + def isPath(f): + return isinstance(f, basestring) # Checks if an object is a string, and that it points to a directory. diff --git a/src/PIL/version.py b/src/PIL/_version.py similarity index 50% rename from src/PIL/version.py rename to src/PIL/_version.py index fbf87ab58..f4bf34e00 100644 --- a/src/PIL/version.py +++ b/src/PIL/_version.py @@ -1,2 +1,2 @@ # Master version for Pillow -__version__ = '5.1.0.dev0' +__version__ = '5.3.0.dev0' diff --git a/src/PIL/features.py b/src/PIL/features.py index d96bf385f..9926445ec 100644 --- a/src/PIL/features.py +++ b/src/PIL/features.py @@ -73,8 +73,8 @@ def get_supported_features(): def check(feature): - return (feature in modules and check_module(feature) or \ - feature in codecs and check_codec(feature) or \ + return (feature in modules and check_module(feature) or + feature in codecs and check_codec(feature) or feature in features and check_feature(feature)) diff --git a/src/Tk/tkImaging.c b/src/Tk/tkImaging.c index f448be166..66e093fae 100644 --- a/src/Tk/tkImaging.c +++ b/src/Tk/tkImaging.c @@ -16,7 +16,7 @@ * following lines to your Tcl_AppInit function (in tkappinit.c) * instead. Put them after the calls to Tcl_Init and Tk_Init: * - * { + * { * extern void TkImaging_Init(Tcl_Interp* interp); * TkImaging_Init(interp); * } @@ -69,7 +69,7 @@ ImagingFind(const char* name) id = atol(name); #endif if (!id) - return NULL; + return NULL; return (Imaging) id; } @@ -109,46 +109,29 @@ PyImagingPhotoPut(ClientData clientdata, Tcl_Interp* interp, return TCL_ERROR; } - /* Active region */ -#if 0 - if (src_xoffset + xsize > im->xsize) - xsize = im->xsize - src_xoffset; - if (src_yoffset + ysize > im->ysize) - ysize = im->ysize - src_yoffset; - if (xsize < 0 || ysize < 0 - || src_xoffset >= im->xsize - || src_yoffset >= im->ysize) - return TCL_OK; -#endif - /* Mode */ if (strcmp(im->mode, "1") == 0 || strcmp(im->mode, "L") == 0) { - block.pixelSize = 1; - block.offset[0] = block.offset[1] = block.offset[2] = 0; + block.pixelSize = 1; + block.offset[0] = block.offset[1] = block.offset[2] = 0; } else if (strncmp(im->mode, "RGB", 3) == 0) { - block.pixelSize = 4; - block.offset[0] = 0; - block.offset[1] = 1; - block.offset[2] = 2; + block.pixelSize = 4; + block.offset[0] = 0; + block.offset[1] = 1; + block.offset[2] = 2; if (strcmp(im->mode, "RGBA") == 0) block.offset[3] = 3; /* alpha (or reserved, under 8.2) */ else block.offset[3] = 0; /* no alpha */ } else { TCL_APPEND_RESULT(interp, "Bad mode", (char*) NULL); - return TCL_ERROR; + return TCL_ERROR; } block.width = im->xsize; block.height = im->ysize; block.pitch = im->linesize; block.pixelPtr = (unsigned char*) im->block; -#if 0 - block.pixelPtr = (unsigned char*) im->block + - src_yoffset * im->linesize + - src_xoffset * im->pixelsize; -#endif if (TK_LT_85) { /* Tk 8.4 */ TK_PHOTO_PUT_BLOCK_84(photo, &block, 0, 0, block.width, block.height, diff --git a/src/_imaging.c b/src/_imaging.c index 11f5f6ea4..0cb2f56b9 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -96,8 +96,6 @@ #undef VERBOSE -#define CLIP(x) ((x) <= 0 ? 0 : (x) < 256 ? (x) : 255) - #define B16(p, i) ((((int)p[(i)]) << 8) + p[(i)+1]) #define L16(p, i) ((((int)p[(i)+1]) << 8) + p[(i)]) #define S16(v) ((v) < 32768 ? (v) : ((v) - 65536)) @@ -356,6 +354,7 @@ getbands(const char* mode) #define TYPE_UINT8 (0x100|sizeof(UINT8)) #define TYPE_INT32 (0x200|sizeof(INT32)) +#define TYPE_FLOAT16 (0x500|sizeof(FLOAT16)) #define TYPE_FLOAT32 (0x300|sizeof(FLOAT32)) #define TYPE_DOUBLE (0x400|sizeof(double)) @@ -379,12 +378,12 @@ getlist(PyObject* arg, Py_ssize_t* length, const char* wrong_length, int type) PyObject* seq; PyObject* op; - if (!PySequence_Check(arg)) { + if ( ! PySequence_Check(arg)) { PyErr_SetString(PyExc_TypeError, must_be_sequence); return NULL; } - n = PyObject_Length(arg); + n = PySequence_Size(arg); if (length && wrong_length && n != *length) { PyErr_SetString(PyExc_ValueError, wrong_length); return NULL; @@ -393,13 +392,12 @@ getlist(PyObject* arg, Py_ssize_t* length, const char* wrong_length, int type) /* malloc check ok, type & ff is just a sizeof(something) calloc checks for overflow */ list = calloc(n, type & 0xff); - if (!list) + if ( ! list) return PyErr_NoMemory(); seq = PySequence_Fast(arg, must_be_sequence); - if (!seq) { + if ( ! seq) { free(list); - PyErr_SetString(PyExc_TypeError, must_be_sequence); return NULL; } @@ -410,7 +408,7 @@ getlist(PyObject* arg, Py_ssize_t* length, const char* wrong_length, int type) switch (type) { case TYPE_UINT8: itemp = PyInt_AsLong(op); - ((UINT8*)list)[i] = CLIP(itemp); + ((UINT8*)list)[i] = CLIP8(itemp); break; case TYPE_INT32: itemp = PyInt_AsLong(op); @@ -427,15 +425,43 @@ getlist(PyObject* arg, Py_ssize_t* length, const char* wrong_length, int type) } } + Py_DECREF(seq); + + if (PyErr_Occurred()) { + free(list); + return NULL; + } + if (length) *length = n; - PyErr_Clear(); - Py_DECREF(seq); - return list; } +FLOAT32 +float16tofloat32(const FLOAT16 in) { + UINT32 t1; + UINT32 t2; + UINT32 t3; + FLOAT32 out[1] = {0}; + + t1 = in & 0x7fff; // Non-sign bits + t2 = in & 0x8000; // Sign bit + t3 = in & 0x7c00; // Exponent + + t1 <<= 13; // Align mantissa on MSB + t2 <<= 16; // Shift sign bit into position + + t1 += 0x38000000; // Adjust bias + + t1 = (t3 == 0 ? 0 : t1); // Denormals-as-zero + + t1 |= t2; // Re-insert sign bit + + memcpy(out, &t1, 4); + return out[0]; +} + static inline PyObject* getpixel(Imaging im, ImagingAccess access, int x, int y) { @@ -526,7 +552,7 @@ getink(PyObject* color, Imaging im, char* ink) return NULL; } } - ink[0] = CLIP(r); + ink[0] = CLIP8(r); ink[1] = ink[2] = ink[3] = 0; } else { a = 255; @@ -546,10 +572,10 @@ getink(PyObject* color, Imaging im, char* ink) return NULL; } } - ink[0] = CLIP(r); - ink[1] = CLIP(g); - ink[2] = CLIP(b); - ink[3] = CLIP(a); + ink[0] = CLIP8(r); + ink[1] = CLIP8(g); + ink[2] = CLIP8(b); + ink[3] = CLIP8(a); } return ink; case IMAGING_TYPE_INT32: @@ -696,9 +722,169 @@ _blend(ImagingObject* self, PyObject* args) } /* -------------------------------------------------------------------- */ -/* METHODS */ +/* METHODS */ /* -------------------------------------------------------------------- */ +static INT16* +_prepare_lut_table(PyObject* table, Py_ssize_t table_size) +{ + int i; + Py_buffer buffer_info; + INT32 data_type = TYPE_FLOAT32; + float item = 0; + void* table_data = NULL; + int free_table_data = 0; + INT16* prepared; + + /* NOTE: This value should be the same as in ColorLUT.c */ + #define PRECISION_BITS (16 - 8 - 2) + + const char* wrong_size = ("The table should have table_channels * " + "size1D * size2D * size3D float items."); + + if (PyObject_CheckBuffer(table)) { + if ( ! PyObject_GetBuffer(table, &buffer_info, + PyBUF_CONTIG_RO | PyBUF_FORMAT)) { + if (buffer_info.ndim == 1 && buffer_info.shape[0] == table_size) { + if (strlen(buffer_info.format) == 1) { + switch (buffer_info.format[0]) { + case 'e': + data_type = TYPE_FLOAT16; + table_data = buffer_info.buf; + break; + case 'f': + data_type = TYPE_FLOAT32; + table_data = buffer_info.buf; + break; + case 'd': + data_type = TYPE_DOUBLE; + table_data = buffer_info.buf; + break; + } + } + } + PyBuffer_Release(&buffer_info); + } + } + + if ( ! table_data) { + free_table_data = 1; + table_data = getlist(table, &table_size, wrong_size, TYPE_FLOAT32); + if ( ! table_data) { + return NULL; + } + } + + /* malloc check ok, max is 2 * 4 * 65**3 = 2197000 */ + prepared = (INT16*) malloc(sizeof(INT16) * table_size); + if ( ! prepared) { + if (free_table_data) + free(table_data); + return (INT16*) PyErr_NoMemory(); + } + + for (i = 0; i < table_size; i++) { + switch (data_type) { + case TYPE_FLOAT16: + item = float16tofloat32(((FLOAT16*) table_data)[i]); + break; + case TYPE_FLOAT32: + item = ((FLOAT32*) table_data)[i]; + break; + case TYPE_DOUBLE: + item = ((double*) table_data)[i]; + break; + } + /* Max value for INT16 */ + if (item >= (0x7fff - 0.5) / (255 << PRECISION_BITS)) { + prepared[i] = 0x7fff; + continue; + } + /* Min value for INT16 */ + if (item <= (-0x8000 + 0.5) / (255 << PRECISION_BITS)) { + prepared[i] = -0x8000; + continue; + } + if (item < 0) { + prepared[i] = item * (255 << PRECISION_BITS) - 0.5; + } else { + prepared[i] = item * (255 << PRECISION_BITS) + 0.5; + } + } + + #undef PRECISION_BITS + if (free_table_data) { + free(table_data); + } + return prepared; +} + + +static PyObject* +_color_lut_3d(ImagingObject* self, PyObject* args) +{ + char* mode; + int filter; + int table_channels; + int size1D, size2D, size3D; + PyObject* table; + + INT16* prepared_table; + Imaging imOut; + + if ( ! PyArg_ParseTuple(args, "siiiiiO:color_lut_3d", &mode, &filter, + &table_channels, &size1D, &size2D, &size3D, + &table)) { + return NULL; + } + + /* actually, it is trilinear */ + if (filter != IMAGING_TRANSFORM_BILINEAR) { + PyErr_SetString(PyExc_ValueError, + "Only LINEAR filter is supported."); + return NULL; + } + + if (1 > table_channels || table_channels > 4) { + PyErr_SetString(PyExc_ValueError, + "table_channels should be from 1 to 4"); + return NULL; + } + + if (2 > size1D || size1D > 65 || + 2 > size2D || size2D > 65 || + 2 > size3D || size3D > 65 + ) { + PyErr_SetString(PyExc_ValueError, + "Table size in any dimension should be from 2 to 65"); + return NULL; + } + + prepared_table = _prepare_lut_table( + table, table_channels * size1D * size2D * size3D); + if ( ! prepared_table) { + return NULL; + } + + imOut = ImagingNewDirty(mode, self->image->xsize, self->image->ysize); + if ( ! imOut) { + free(prepared_table); + return NULL; + } + + if ( ! ImagingColorLUT3D_linear(imOut, self->image, + table_channels, size1D, size2D, size3D, + prepared_table)) { + free(prepared_table); + ImagingDelete(imOut); + return NULL; + } + + free(prepared_table); + + return PyImagingNew(imOut); +} + static PyObject* _convert(ImagingObject* self, PyObject* args) { @@ -1167,17 +1353,17 @@ _point(ImagingObject* self, PyObject* args) im = ImagingPoint(self->image, mode, (void*) data); else if (mode && bands > 1) { for (i = 0; i < 256; i++) { - lut[i*4] = CLIP(data[i]); - lut[i*4+1] = CLIP(data[i+256]); - lut[i*4+2] = CLIP(data[i+512]); + lut[i*4] = CLIP8(data[i]); + lut[i*4+1] = CLIP8(data[i+256]); + lut[i*4+2] = CLIP8(data[i+512]); if (n > 768) - lut[i*4+3] = CLIP(data[i+768]); + lut[i*4+3] = CLIP8(data[i+768]); } im = ImagingPoint(self->image, mode, (void*) lut); } else { /* map individual bands */ for (i = 0; i < n; i++) - lut[i] = CLIP(data[i]); + lut[i] = CLIP8(data[i]); im = ImagingPoint(self->image, mode, (void*) lut); } free(data); @@ -1241,7 +1427,7 @@ _putdata(ImagingObject* self, PyObject* args) else /* Scaled and clipped string data */ for (i = x = y = 0; i < n; i++) { - image->image8[y][x] = CLIP((int) (p[i] * scale + offset)); + image->image8[y][x] = CLIP8((int) (p[i] * scale + offset)); if (++x >= (int) image->xsize) x = 0, y++; } @@ -1255,7 +1441,7 @@ _putdata(ImagingObject* self, PyObject* args) /* Clipped data */ for (i = x = y = 0; i < n; i++) { op = PySequence_Fast_GET_ITEM(seq, i); - image->image8[y][x] = (UINT8) CLIP(PyInt_AsLong(op)); + image->image8[y][x] = (UINT8) CLIP8(PyInt_AsLong(op)); if (++x >= (int) image->xsize){ x = 0, y++; } @@ -1265,7 +1451,7 @@ _putdata(ImagingObject* self, PyObject* args) /* Scaled and clipped data */ for (i = x = y = 0; i < n; i++) { PyObject *op = PySequence_Fast_GET_ITEM(seq, i); - image->image8[y][x] = CLIP( + image->image8[y][x] = CLIP8( (int) (PyFloat_AsDouble(op) * scale + offset)); if (++x >= (int) image->xsize){ x = 0, y++; @@ -2982,6 +3168,7 @@ static struct PyMethodDef methods[] = { {"pixel_access", (PyCFunction)pixel_access_new, 1}, /* Standard processing methods (Image) */ + {"color_lut_3d", (PyCFunction)_color_lut_3d, 1}, {"convert", (PyCFunction)_convert, 1}, {"convert2", (PyCFunction)_convert2, 1}, {"convert_matrix", (PyCFunction)_convert_matrix, 1}, @@ -3478,6 +3665,7 @@ extern PyObject* PyImaging_JpegEncoderNew(PyObject* self, PyObject* args); extern PyObject* PyImaging_Jpeg2KEncoderNew(PyObject* self, PyObject* args); extern PyObject* PyImaging_PcxEncoderNew(PyObject* self, PyObject* args); extern PyObject* PyImaging_RawEncoderNew(PyObject* self, PyObject* args); +extern PyObject* PyImaging_TgaRleEncoderNew(PyObject* self, PyObject* args); extern PyObject* PyImaging_XbmEncoderNew(PyObject* self, PyObject* args); extern PyObject* PyImaging_ZipEncoderNew(PyObject* self, PyObject* args); extern PyObject* PyImaging_LibTiffEncoderNew(PyObject* self, PyObject* args); @@ -3545,6 +3733,7 @@ static PyMethodDef functions[] = { {"sgi_rle_decoder", (PyCFunction)PyImaging_SgiRleDecoderNew, 1}, {"sun_rle_decoder", (PyCFunction)PyImaging_SunRleDecoderNew, 1}, {"tga_rle_decoder", (PyCFunction)PyImaging_TgaRleDecoderNew, 1}, + {"tga_rle_encoder", (PyCFunction)PyImaging_TgaRleEncoderNew, 1}, {"xbm_decoder", (PyCFunction)PyImaging_XbmDecoderNew, 1}, {"xbm_encoder", (PyCFunction)PyImaging_XbmEncoderNew, 1}, #ifdef HAVE_LIBZ diff --git a/src/_imagingmorph.c b/src/_imagingmorph.c index 73979f635..fc8f246cc 100644 --- a/src/_imagingmorph.c +++ b/src/_imagingmorph.c @@ -64,12 +64,12 @@ apply(PyObject *self, PyObject* args) width = imgin->xsize; height = imgin->ysize; - if (imgin->type != IMAGING_TYPE_UINT8 && + if (imgin->type != IMAGING_TYPE_UINT8 || imgin->bands != 1) { PyErr_SetString(PyExc_RuntimeError, "Unsupported image type"); return NULL; } - if (imgout->type != IMAGING_TYPE_UINT8 && + if (imgout->type != IMAGING_TYPE_UINT8 || imgout->bands != 1) { PyErr_SetString(PyExc_RuntimeError, "Unsupported image type"); return NULL; @@ -167,7 +167,7 @@ match(PyObject *self, PyObject* args) lut = PyBytes_AsString(py_lut); imgin = (Imaging) i0; - if (imgin->type != IMAGING_TYPE_UINT8 && + if (imgin->type != IMAGING_TYPE_UINT8 || imgin->bands != 1) { PyErr_SetString(PyExc_RuntimeError, "Unsupported image type"); return NULL; @@ -219,7 +219,7 @@ match(PyObject *self, PyObject* args) } /* Return a list of the coordinates of all turned on pixels in an image. - May be used to extract features after a sequence of MorphOp's were applied. + May be used to extract features after a sequence of MorphOps were applied. This is faster than match as only 1x1 lookup is made. */ static PyObject* diff --git a/src/encode.c b/src/encode.c index ae4277c04..c60048c41 100644 --- a/src/encode.c +++ b/src/encode.c @@ -492,6 +492,38 @@ PyImaging_RawEncoderNew(PyObject* self, PyObject* args) } +/* -------------------------------------------------------------------- */ +/* TGA */ +/* -------------------------------------------------------------------- */ + +PyObject* +PyImaging_TgaRleEncoderNew(PyObject* self, PyObject* args) +{ + ImagingEncoderObject* encoder; + + char *mode; + char *rawmode; + int ystep = 1; + + if (!PyArg_ParseTuple(args, "ss|i", &mode, &rawmode, &ystep)) + return NULL; + + encoder = PyImaging_EncoderNew(0); + if (encoder == NULL) + return NULL; + + if (get_packer(encoder, mode, rawmode) < 0) + return NULL; + + encoder->encode = ImagingTgaRleEncode; + + encoder->state.ystep = ystep; + + return (PyObject*) encoder; +} + + + /* -------------------------------------------------------------------- */ /* XBM */ /* -------------------------------------------------------------------- */ diff --git a/src/libImaging/Bands.c b/src/libImaging/Bands.c index 7f86d4973..e38e22819 100644 --- a/src/libImaging/Bands.c +++ b/src/libImaging/Bands.c @@ -19,9 +19,6 @@ #include "Imaging.h" -#define CLIP(x) ((x) <= 0 ? 0 : (x) < 256 ? (x) : 255) - - Imaging ImagingGetBand(Imaging imIn, int band) { @@ -214,7 +211,7 @@ ImagingFillBand(Imaging imOut, int band, int color) if (imOut->bands == 2 && band == 1) band = 3; - color = CLIP(color); + color = CLIP8(color); /* Insert color into image */ for (y = 0; y < imOut->ysize; y++) { diff --git a/src/libImaging/BcnDecode.c b/src/libImaging/BcnDecode.c index 58d3ecc10..1472b0296 100644 --- a/src/libImaging/BcnDecode.c +++ b/src/libImaging/BcnDecode.c @@ -4,7 +4,7 @@ * decoder for DXTn-compressed data * * Format documentation: - * http://oss.sgi.com/projects/ogl-sample/registry/EXT/texture_compression_s3tc.txt + * https://web.archive.org/web/20170802060935/http://oss.sgi.com/projects/ogl-sample/registry/EXT/texture_compression_s3tc.txt * * The contents of this file are in the public domain (CC0) * Full text of the CC0 license: @@ -178,7 +178,7 @@ static void decode_bc5_block(rgba *col, const UINT8* src) { } /* BC6 and BC7 are described here: - https://www.opengl.org/registry/specs/ARB/texture_compression_bptc.txt */ + https://www.khronos.org/registry/OpenGL/extensions/ARB/ARB_texture_compression_bptc.txt */ static UINT8 get_bit(const UINT8* src, int bit) { int by = bit >> 3; diff --git a/src/libImaging/ColorLUT.c b/src/libImaging/ColorLUT.c new file mode 100644 index 000000000..46b00142b --- /dev/null +++ b/src/libImaging/ColorLUT.c @@ -0,0 +1,164 @@ +#include "Imaging.h" +#include + + +/* 8 bits for result. Table can overflow [0, 1.0] range, + so we need extra bits for overflow and negative values. + NOTE: This value should be the same as in _imaging/_prepare_lut_table() */ +#define PRECISION_BITS (16 - 8 - 2) +#define PRECISION_ROUNDING (1<<(PRECISION_BITS-1)) + +/* 8 — scales are multiplied on byte. + 6 — max index in the table + (max size is 65, but index 64 is not reachable) */ +#define SCALE_BITS (32 - 8 - 6) +#define SCALE_MASK ((1<> PRECISION_BITS]; +} + +static inline void +interpolate3(INT16 out[3], const INT16 a[3], const INT16 b[3], INT16 shift) +{ + out[0] = (a[0] * ((1<> SHIFT_BITS; + out[1] = (a[1] * ((1<> SHIFT_BITS; + out[2] = (a[2] * ((1<> SHIFT_BITS; +} + +static inline void +interpolate4(INT16 out[4], const INT16 a[4], const INT16 b[4], INT16 shift) +{ + out[0] = (a[0] * ((1<> SHIFT_BITS; + out[1] = (a[1] * ((1<> SHIFT_BITS; + out[2] = (a[2] * ((1<> SHIFT_BITS; + out[3] = (a[3] * ((1<> SHIFT_BITS; +} + +static inline int +table_index3D(int index1D, int index2D, int index3D, + int size1D, int size1D_2D) +{ + return index1D + index2D * size1D + index3D * size1D_2D; +} + + +/* + Transforms colors of imIn using provided 3D lookup table + and puts the result in imOut. Returns imOut on success or 0 on error. + + imOut, imIn — images, should be the same size and may be the same image. + Should have 3 or 4 channels. + table_channels — number of channels in the lookup table, 3 or 4. + Should be less or equal than number of channels in imOut image; + size1D, size_2D and size3D — dimensions of provided table; + table — flat table, + array with table_channels × size1D × size2D × size3D elements, + where channels are changed first, then 1D, then​ 2D, then 3D. + Each element is signed 16-bit int where 0 is lowest output value + and 255 << PRECISION_BITS (16320) is highest value. +*/ +Imaging +ImagingColorLUT3D_linear(Imaging imOut, Imaging imIn, int table_channels, + int size1D, int size2D, int size3D, + INT16* table) +{ + /* This float to int conversion doesn't have rounding + error compensation (+0.5) for two reasons: + 1. As we don't hit the highest value, + we can use one extra bit for precision. + 2. For every pixel, we interpolate 8 elements from the table: + current and +1 for every dimension and their combinations. + If we hit the upper cells from the table, + +1 cells will be outside of the table. + With this compensation we never hit the upper cells + but this also doesn't introduce any noticeable difference. */ + UINT32 scale1D = (size1D - 1) / 255.0 * (1< 4) { + PyErr_SetString(PyExc_ValueError, "table_channels could be 3 or 4"); + return NULL; + } + + if (imIn->type != IMAGING_TYPE_UINT8 || + imOut->type != IMAGING_TYPE_UINT8 || + imIn->bands < 3 || + imOut->bands < table_channels + ) { + return (Imaging) ImagingError_ModeError(); + } + + /* In case we have one extra band in imOut and don't have in imIn.*/ + if (imOut->bands > table_channels && imOut->bands > imIn->bands) { + return (Imaging) ImagingError_ModeError(); + } + + ImagingSectionEnter(&cookie); + for (y = 0; y < imOut->ysize; y++) { + UINT8* rowIn = (UINT8 *)imIn->image[y]; + UINT32* rowOut = (UINT32 *)imOut->image[y]; + for (x = 0; x < imOut->xsize; x++) { + UINT32 index1D = rowIn[x*4 + 0] * scale1D; + UINT32 index2D = rowIn[x*4 + 1] * scale2D; + UINT32 index3D = rowIn[x*4 + 2] * scale3D; + INT16 shift1D = (SCALE_MASK & index1D) >> (SCALE_BITS - SHIFT_BITS); + INT16 shift2D = (SCALE_MASK & index2D) >> (SCALE_BITS - SHIFT_BITS); + INT16 shift3D = (SCALE_MASK & index3D) >> (SCALE_BITS - SHIFT_BITS); + int idx = table_channels * table_index3D( + index1D >> SCALE_BITS, index2D >> SCALE_BITS, + index3D >> SCALE_BITS, size1D, size1D_2D); + INT16 result[4], left[4], right[4]; + INT16 leftleft[4], leftright[4], rightleft[4], rightright[4]; + + if (table_channels == 3) { + interpolate3(leftleft, &table[idx + 0], &table[idx + 3], shift1D); + interpolate3(leftright, &table[idx + size1D*3], + &table[idx + size1D*3 + 3], shift1D); + interpolate3(left, leftleft, leftright, shift2D); + + interpolate3(rightleft, &table[idx + size1D_2D*3], + &table[idx + size1D_2D*3 + 3], shift1D); + interpolate3(rightright, &table[idx + size1D_2D*3 + size1D*3], + &table[idx + size1D_2D*3 + size1D*3 + 3], shift1D); + interpolate3(right, rightleft, rightright, shift2D); + + interpolate3(result, left, right, shift3D); + + rowOut[x] = MAKE_UINT32( + clip8(result[0]), clip8(result[1]), + clip8(result[2]), rowIn[x*4 + 3]); + } + + if (table_channels == 4) { + interpolate4(leftleft, &table[idx + 0], &table[idx + 4], shift1D); + interpolate4(leftright, &table[idx + size1D*4], + &table[idx + size1D*4 + 4], shift1D); + interpolate4(left, leftleft, leftright, shift2D); + + interpolate4(rightleft, &table[idx + size1D_2D*4], + &table[idx + size1D_2D*4 + 4], shift1D); + interpolate4(rightright, &table[idx + size1D_2D*4 + size1D*4], + &table[idx + size1D_2D*4 + size1D*4 + 4], shift1D); + interpolate4(right, rightleft, rightright, shift2D); + + interpolate4(result, left, right, shift3D); + + rowOut[x] = MAKE_UINT32( + clip8(result[0]), clip8(result[1]), + clip8(result[2]), clip8(result[3])); + } + } + } + ImagingSectionLeave(&cookie); + + return imOut; +} diff --git a/src/libImaging/Convert.c b/src/libImaging/Convert.c index b3e48e52b..39ddf8721 100644 --- a/src/libImaging/Convert.c +++ b/src/libImaging/Convert.c @@ -38,7 +38,6 @@ #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)) /* ITU-R Recommendation 601-2 (assuming nonlinear RGB) */ @@ -141,7 +140,7 @@ la2lA(UINT8* out, const UINT8* in, int xsize) if (alpha == 255 || alpha == 0) { pixel = in[0]; } else { - pixel = CLIP((255 * in[0]) / alpha); + pixel = CLIP8((255 * in[0]) / alpha); } *out++ = (UINT8) pixel; *out++ = (UINT8) pixel; @@ -316,8 +315,8 @@ rgb2hsv(UINT8* out, const UINT8* in, int xsize) // 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)); + uh = (UINT8)CLIP8((int)(h*255.0)); + us = (UINT8)CLIP8((int)(s*255.0)); *out++ = uh; *out++ = us; @@ -355,9 +354,9 @@ hsv2rgb(UINT8* out, const UINT8* in, int xsize) 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); + up = (UINT8)CLIP8(p); + uq = (UINT8)CLIP8(q); + ut = (UINT8)CLIP8(t); switch (i%6) { case 0: @@ -466,9 +465,9 @@ rgba2rgbA(UINT8* out, const UINT8* in, int xsize) *out++ = in[1]; *out++ = in[2]; } else { - *out++ = CLIP((255 * in[0]) / alpha); - *out++ = CLIP((255 * in[1]) / alpha); - *out++ = CLIP((255 * in[2]) / alpha); + *out++ = CLIP8((255 * in[0]) / alpha); + *out++ = CLIP8((255 * in[1]) / alpha); + *out++ = CLIP8((255 * in[2]) / alpha); } *out++ = in[3]; } @@ -537,9 +536,9 @@ cmyk2rgb(UINT8* out, const UINT8* in, int xsize) int x, nk, tmp; for (x = 0; x < xsize; x++) { nk = 255 - in[3]; - out[0] = CLIP(nk - MULDIV255(in[0], nk, tmp)); - out[1] = CLIP(nk - MULDIV255(in[1], nk, tmp)); - out[2] = CLIP(nk - MULDIV255(in[2], nk, tmp)); + out[0] = CLIP8(nk - MULDIV255(in[0], nk, tmp)); + out[1] = CLIP8(nk - MULDIV255(in[1], nk, tmp)); + out[2] = CLIP8(nk - MULDIV255(in[2], nk, tmp)); out[3] = 255; out += 4; in += 4; @@ -1132,9 +1131,9 @@ topalette(Imaging imOut, Imaging imIn, ImagingPalette inpalette, int dither) int d2; INT16* cache; - r = CLIP(in[0] + (r + e[3+0])/16); - g = CLIP(in[1] + (g + e[3+1])/16); - b = CLIP(in[2] + (b + e[3+2])/16); + r = CLIP8(in[0] + (r + e[3+0])/16); + g = CLIP8(in[1] + (g + e[3+1])/16); + b = CLIP8(in[2] + (b + e[3+2])/16); /* get closest colour */ cache = &ImagingPaletteCache(palette, r, g, b); @@ -1236,7 +1235,7 @@ tobilevel(Imaging imOut, Imaging imIn, int dither) for (x = 0; x < imIn->xsize; x++) { /* pick closest colour */ - l = CLIP(in[x] + (l + errors[x+1])/16); + l = CLIP8(in[x] + (l + errors[x+1])/16); out[x] = (l > 128) ? 255 : 0; /* propagate errors */ @@ -1264,7 +1263,7 @@ tobilevel(Imaging imOut, Imaging imIn, int dither) for (x = 0; x < imIn->xsize; x++, in += 4) { /* pick closest colour */ - l = CLIP(L(in)/1000 + (l + errors[x+1])/16); + l = CLIP8(L(in)/1000 + (l + errors[x+1])/16); out[x] = (l > 128) ? 255 : 0; /* propagate errors */ diff --git a/src/libImaging/Draw.c b/src/libImaging/Draw.c index 3fae3d931..a3bb92309 100644 --- a/src/libImaging/Draw.c +++ b/src/libImaging/Draw.c @@ -434,7 +434,7 @@ polygon_generic(Imaging im, int n, Edge *e, int ink, int eofill, } for (i = 0; i < n; i++) { - /* This causes that the pixels of horizontal edges are drawn twice :( + /* This causes the pixels of horizontal edges to be 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); @@ -732,6 +732,29 @@ ImagingDrawBitmap(Imaging im, int x0, int y0, Imaging bitmap, const void* ink, #define CHORD 1 #define PIESLICE 2 +static void +ellipsePoint(int cx, int cy, int w, int h, + float i, int *x, int *y) +{ + float i_cos, i_sin; + float x_f, y_f; + double modf_int; + i_cos = cos(i*M_PI/180); + i_sin = sin(i*M_PI/180); + x_f = (i_cos * w/2) + cx; + y_f = (i_sin * h/2) + cy; + if (modf(x_f, &modf_int) == 0.5) { + *x = i_cos > 0 ? FLOOR(x_f) : CEIL(x_f); + } else { + *x = FLOOR(x_f + 0.5); + } + if (modf(y_f, &modf_int) == 0.5) { + *y = i_sin > 0 ? FLOOR(y_f) : CEIL(y_f); + } else { + *y = FLOOR(y_f + 0.5); + } +} + static int ellipse(Imaging im, int x0, int y0, int x1, int y1, float start, float end, const void* ink_, int fill, @@ -781,8 +804,7 @@ ellipse(Imaging im, int x0, int y0, int x1, int y1, if (i > end) { i = end; } - x = FLOOR((cos(i*M_PI/180) * w/2) + cx + 0.5); - y = FLOOR((sin(i*M_PI/180) * h/2) + cy + 0.5); + ellipsePoint(cx, cy, w, h, i, &x, &y); if (i != start) add_edge(&e[n++], lx, ly, x, y); else @@ -812,8 +834,7 @@ ellipse(Imaging im, int x0, int y0, int x1, int y1, if (i > end) { i = end; } - x = FLOOR((cos(i*M_PI/180) * w/2) + cx + 0.5); - y = FLOOR((sin(i*M_PI/180) * h/2) + cy + 0.5); + ellipsePoint(cx, cy, w, h, i, &x, &y); if (i != start) draw->line(im, lx, ly, x, y, ink); else @@ -872,8 +893,6 @@ ImagingDrawPieslice(Imaging im, int x0, int y0, int x1, int y1, semantics are ok, except that "curve" flattens the bezier curves by itself */ -#if 1 /* ARROW_GRAPHICS */ - struct ImagingOutlineInstance { float x0, y0; @@ -1102,5 +1121,3 @@ ImagingDrawOutline(Imaging im, ImagingOutline outline, const void* ink_, return 0; } - -#endif diff --git a/src/libImaging/Effects.c b/src/libImaging/Effects.c index 26b063a11..7b4ff0b43 100644 --- a/src/libImaging/Effects.c +++ b/src/libImaging/Effects.c @@ -106,7 +106,7 @@ ImagingEffectNoise(int xsize, int ysize, float sigma) this = factor * v1; next = factor * v2; } - out[x] = (unsigned char) (128 + sigma * this); + out[x] = CLIP8(128 + sigma * this); } } diff --git a/src/libImaging/File.c b/src/libImaging/File.c index d67bcabde..6f014c1f8 100644 --- a/src/libImaging/File.c +++ b/src/libImaging/File.c @@ -70,6 +70,7 @@ ImagingSavePPM(Imaging im, const char* outfile) /* Write "PPM" */ fprintf(fp, "P6\n%d %d\n255\n", im->xsize, im->ysize); } else { + fclose(fp); (void) ImagingError_ModeError(); return 0; } diff --git a/src/libImaging/ImPlatform.h b/src/libImaging/ImPlatform.h index 7b42510d4..b2d4db785 100644 --- a/src/libImaging/ImPlatform.h +++ b/src/libImaging/ImPlatform.h @@ -71,6 +71,7 @@ #endif /* assume IEEE; tweak if necessary (patches are welcome) */ +#define FLOAT16 UINT16 #define FLOAT32 float #define FLOAT64 double diff --git a/src/libImaging/Imaging.h b/src/libImaging/Imaging.h index aa59fe18c..6b553c928 100644 --- a/src/libImaging/Imaging.h +++ b/src/libImaging/Imaging.h @@ -319,6 +319,8 @@ extern Imaging ImagingTransform( extern Imaging ImagingUnsharpMask( Imaging imOut, Imaging im, float radius, int percent, int threshold); extern Imaging ImagingBoxBlur(Imaging imOut, Imaging imIn, float radius, int n); +extern Imaging ImagingColorLUT3D_linear(Imaging imOut, Imaging imIn, + int table_channels, int size1D, int size2D, int size3D, INT16* table); extern Imaging ImagingCopy2(Imaging imOut, Imaging imIn); extern Imaging ImagingConvert2(Imaging imOut, Imaging imIn); @@ -471,6 +473,8 @@ extern int ImagingSunRleDecode(Imaging im, ImagingCodecState state, UINT8* buffer, int bytes); extern int ImagingTgaRleDecode(Imaging im, ImagingCodecState state, UINT8* buffer, int bytes); +extern int ImagingTgaRleEncode(Imaging im, ImagingCodecState state, + UINT8* buffer, int bytes); extern int ImagingXbmDecode(Imaging im, ImagingCodecState state, UINT8* buffer, int bytes); extern int ImagingXbmEncode(Imaging im, ImagingCodecState state, @@ -534,6 +538,8 @@ extern Py_ssize_t _imaging_tell_pyFd(PyObject *fd); #include "ImagingUtils.h" +extern UINT8 *clip8_lookups; + #if defined(__cplusplus) } diff --git a/src/libImaging/ImagingUtils.h b/src/libImaging/ImagingUtils.h index b040fc303..d25da80ae 100644 --- a/src/libImaging/ImagingUtils.h +++ b/src/libImaging/ImagingUtils.h @@ -30,6 +30,8 @@ (MULDIV255(in1, (255 - mask), tmp1) + in2) +#define CLIP8(v) ((v) <= 0 ? 0 : (v) < 256 ? (v) : 255) + /* This is to work around a bug in GCC prior 4.9 in 64 bit mode. GCC generates code with partial dependency which is 3 times slower. See: http://stackoverflow.com/a/26588074/253146 */ @@ -37,7 +39,7 @@ ! defined(__clang__) && defined(GCC_VERSION) && (GCC_VERSION < 40900) static float __attribute__((always_inline)) inline _i2f(int v) { float x; - __asm__("xorps %0, %0; cvtsi2ss %1, %0" : "=X"(x) : "r"(v) ); + __asm__("xorps %0, %0; cvtsi2ss %1, %0" : "=x"(x) : "r"(v) ); return x; } #else diff --git a/src/libImaging/QuantOctree.c b/src/libImaging/QuantOctree.c index e18ab3c65..018dfe1d9 100644 --- a/src/libImaging/QuantOctree.c +++ b/src/libImaging/QuantOctree.c @@ -259,6 +259,10 @@ subtract_color_buckets(ColorCube cube, ColorBucket buckets, long nBuckets) { Pixel p; for (i=0; icount == 0) continue; + avg_color_from_color_bucket(subtrahend, &p); minuend = color_bucket_from_cube(cube, &p); minuend->count -= subtrahend->count; diff --git a/src/libImaging/Resample.c b/src/libImaging/Resample.c index 7cefdb2af..6fae2081d 100644 --- a/src/libImaging/Resample.c +++ b/src/libImaging/Resample.c @@ -83,7 +83,40 @@ static struct filter LANCZOS = { lanczos_filter, 3.0 }; #define PRECISION_BITS (32 - 8 - 2) -UINT8 _lookups[512] = { +/* Handles values form -640 to 639. */ +UINT8 _clip8_lookups[1280] = { + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, @@ -115,15 +148,30 @@ UINT8 _lookups[512] = { 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, - 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255 + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, }; -UINT8 *lookups = &_lookups[128]; - +UINT8 *clip8_lookups = &_clip8_lookups[640]; static inline UINT8 clip8(int in) { - return lookups[in >> PRECISION_BITS]; + return clip8_lookups[in >> PRECISION_BITS]; } diff --git a/src/libImaging/TgaRleEncode.c b/src/libImaging/TgaRleEncode.c new file mode 100644 index 000000000..2fb831e6b --- /dev/null +++ b/src/libImaging/TgaRleEncode.c @@ -0,0 +1,153 @@ + +#include "Imaging.h" + +#include +#include + + +static int comparePixels(const UINT8* buf, int x, int bytesPerPixel) +{ + buf += x * bytesPerPixel; + return memcmp(buf, buf + bytesPerPixel, bytesPerPixel) == 0; +} + + +int +ImagingTgaRleEncode(Imaging im, ImagingCodecState state, UINT8* buf, int bytes) +{ + UINT8* dst; + int bytesPerPixel; + + if (state->state == 0) { + if (state->ystep < 0) { + state->ystep = -1; + state->y = state->ysize - 1; + } else + state->ystep = 1; + + state->state = 1; + } + + dst = buf; + bytesPerPixel = (state->bits + 7) / 8; + + while (1) { + int flushCount; + + /* + * state->count is the numbers of bytes in the packet, + * excluding the 1-byte descriptor. + */ + if (state->count == 0) { + UINT8* row; + UINT8 descriptor; + int startX; + + assert(state->x <= state->xsize); + + /* Make sure we have space for the descriptor. */ + if (bytes < 1) + break; + + if (state->x == state->xsize) { + state->x = 0; + + state->y += state->ystep; + if (state->y < 0 || state->y >= state->ysize) { + state->errcode = IMAGING_CODEC_END; + break; + } + } + + if (state->x == 0) + state->shuffle( + state->buffer, + (UINT8*)im->image[state->y + state->yoff] + + state->xoff * im->pixelsize, + state->xsize); + + row = state->buffer; + + /* Start with a raw packet for 1 px. */ + descriptor = 0; + startX = state->x; + state->count = bytesPerPixel; + + if (state->x + 1 < state->xsize) { + int maxLookup; + int isRaw; + + isRaw = !comparePixels(row, state->x, bytesPerPixel); + ++state->x; + + /* + * A packet can contain up to 128 pixels; + * 2 are already behind (state->x points to + * the second one). + */ + maxLookup = state->x + 126; + /* A packet must not span multiple rows. */ + if (maxLookup > state->xsize - 1) + maxLookup = state->xsize - 1; + + if (isRaw) { + while (state->x < maxLookup) + if (!comparePixels(row, state->x, bytesPerPixel)) + ++state->x; + else { + /* Two identical pixels will go to RLE packet. */ + --state->x; + break; + } + + state->count += (state->x - startX) * bytesPerPixel; + } else { + descriptor |= 0x80; + + while (state->x < maxLookup) + if (comparePixels(row, state->x, bytesPerPixel)) + ++state->x; + else + break; + } + } + + /* + * state->x currently points to the last pixel to be + * included in the packet. The pixel count in the + * descriptor is 1 less than actual number of pixels in + * the packet, that is, state->x == startX if we encode + * only 1 pixel. + */ + descriptor += state->x - startX; + *dst++ = descriptor; + --bytes; + + /* Advance to past-the-last encoded pixel. */ + ++state->x; + } + + assert(bytes >= 0); + assert(state->count > 0); + assert(state->x > 0); + assert(state->count <= state->x * bytesPerPixel); + + if (bytes == 0) + break; + + flushCount = state->count; + if (flushCount > bytes) + flushCount = bytes; + + memcpy( + dst, + state->buffer + (state->x * bytesPerPixel - state->count), + flushCount); + dst += flushCount; + bytes -= flushCount; + + state->count -= flushCount; + } + + return dst - buf; +} diff --git a/src/libImaging/Unpack.c b/src/libImaging/Unpack.c index 0e7462c6e..05b4299b0 100644 --- a/src/libImaging/Unpack.c +++ b/src/libImaging/Unpack.c @@ -45,8 +45,6 @@ #define Y 2 #define K 3 -#define CLIP(x) ((x) <= 0 ? 0 : (x) < 256 ? (x) : 255) - /* byte-swapping macros */ #define C16N\ @@ -749,9 +747,9 @@ unpackRGBa16L(UINT8* _out, const UINT8* in, int pixels) } else if (a == 255) { out[i] = MAKE_UINT32(in[1], in[3], in[5], a); } else { - out[i] = MAKE_UINT32(CLIP(in[1] * 255 / a), - CLIP(in[3] * 255 / a), - CLIP(in[5] * 255 / a), a); + out[i] = MAKE_UINT32(CLIP8(in[1] * 255 / a), + CLIP8(in[3] * 255 / a), + CLIP8(in[5] * 255 / a), a); } in += 8; } @@ -770,9 +768,9 @@ unpackRGBa16B(UINT8* _out, const UINT8* in, int pixels) } else if (a == 255) { out[i] = MAKE_UINT32(in[0], in[2], in[4], a); } else { - out[i] = MAKE_UINT32(CLIP(in[0] * 255 / a), - CLIP(in[2] * 255 / a), - CLIP(in[4] * 255 / a), a); + out[i] = MAKE_UINT32(CLIP8(in[0] * 255 / a), + CLIP8(in[2] * 255 / a), + CLIP8(in[4] * 255 / a), a); } in += 8; } @@ -791,9 +789,9 @@ unpackRGBa(UINT8* _out, const UINT8* in, int pixels) } else if (a == 255) { out[i] = MAKE_UINT32(in[0], in[1], in[2], a); } else { - out[i] = MAKE_UINT32(CLIP(in[0] * 255 / a), - CLIP(in[1] * 255 / a), - CLIP(in[2] * 255 / a), a); + out[i] = MAKE_UINT32(CLIP8(in[0] * 255 / a), + CLIP8(in[1] * 255 / a), + CLIP8(in[2] * 255 / a), a); } in += 4; } @@ -812,9 +810,9 @@ unpackBGRa(UINT8* _out, const UINT8* in, int pixels) } else if (a == 255) { out[i] = MAKE_UINT32(in[2], in[1], in[0], a); } else { - out[i] = MAKE_UINT32(CLIP(in[2] * 255 / a), - CLIP(in[1] * 255 / a), - CLIP(in[0] * 255 / a), a); + out[i] = MAKE_UINT32(CLIP8(in[2] * 255 / a), + CLIP8(in[1] * 255 / a), + CLIP8(in[0] * 255 / a), a); } in += 4; } diff --git a/src/path.c b/src/path.c index b56ea838e..d1c18c8ed 100644 --- a/src/path.c +++ b/src/path.c @@ -571,7 +571,7 @@ path_subscript(PyPathObject* self, PyObject* item) { else { PyErr_Format(PyExc_TypeError, "Path indices must be integers, not %.200s", - Py_TYPE(&item)->tp_name); + Py_TYPE(item)->tp_name); return NULL; } } diff --git a/tox.ini b/tox.ini index fdfc5ea56..1226619db 100644 --- a/tox.ini +++ b/tox.ini @@ -4,7 +4,7 @@ # and then run "tox" from this directory. [tox] -envlist = py27, py34, py35, py36 +envlist = py27, py34, py35, py36, py37 [testenv] commands = diff --git a/winbuild/.gitignore b/winbuild/.gitignore deleted file mode 100644 index adc679fa8..000000000 --- a/winbuild/.gitignore +++ /dev/null @@ -1,6 +0,0 @@ -*.zip -*.tar.gz -*.msi -*.asc -__pycache__ -depends/ \ No newline at end of file diff --git a/winbuild/appveyor_install_pypy.cmd b/winbuild/appveyor_install_pypy.cmd index 1fe39f1a1..f68e75daa 100644 --- a/winbuild/appveyor_install_pypy.cmd +++ b/winbuild/appveyor_install_pypy.cmd @@ -1,3 +1,3 @@ -curl -fsSL -o pypy2.zip https://bitbucket.org/pypy/pypy/downloads/pypy2-v5.10.0-win32.zip -7z x pypy2.zip -oc:\ -c:\Python34\Scripts\virtualenv.exe -p c:\pypy2-v5.10.0-win32\pypy.exe c:\vp\pypy2 +curl -fsSL -o pypy2.zip https://bitbucket.org/pypy/pypy/downloads/pypy2-v6.0.0-win32.zip +7z x pypy2.zip -oc:\ +c:\Python34\Scripts\virtualenv.exe -p c:\pypy2-v6.0.0-win32\pypy.exe c:\vp\pypy2 diff --git a/winbuild/build.rst b/winbuild/build.rst index 847a6e1d2..a56f43d1a 100644 --- a/winbuild/build.rst +++ b/winbuild/build.rst @@ -52,13 +52,11 @@ Compilers Download and install: -* `Microsoft Windows SDK for Windows 7 and .NET Framework 3.5 - SP1 `_ - * `Microsoft Windows SDK for Windows 7 and .NET Framework 4 `_ -* `CMake-2.8.10.2-win32-x86.exe `_ +* `CMake-2.8.10.2-win32-x86.exe + `_ The samples and the .NET SDK portions aren't required, just the compilers and other tools. UNDONE -- check exact wording. diff --git a/winbuild/build_dep.py b/winbuild/build_dep.py index a059b1ee8..6ac3baa47 100644 --- a/winbuild/build_dep.py +++ b/winbuild/build_dep.py @@ -2,6 +2,7 @@ from unzip import unzip from untar import untar import os +from fetch import fetch from config import compilers, compiler_from_env, libs @@ -44,6 +45,8 @@ def extract(src, dest): def extract_libs(): for name, lib in libs.items(): filename = lib['filename'] + if not os.path.exists(filename): + filename = fetch(lib['url']) if name == 'openjpeg': for compiler in compilers.values(): if not os.path.exists(os.path.join( diff --git a/winbuild/config.py b/winbuild/config.py index 1a8ef3a0b..404ce16a7 100644 --- a/winbuild/config.py +++ b/winbuild/config.py @@ -24,9 +24,9 @@ libs = { 'dir': 'zlib-1.2.11', }, 'jpeg': { - 'url': 'http://www.ijg.org/files/jpegsr9b.zip', - 'filename': PILLOW_DEPENDS_DIR + 'jpegsr9b.zip', - 'dir': 'jpeg-9b', + 'url': 'http://www.ijg.org/files/jpegsr9c.zip', + 'filename': PILLOW_DEPENDS_DIR + 'jpegsr9c.zip', + 'dir': 'jpeg-9c', }, 'tiff': { 'url': 'ftp://download.osgeo.org/libtiff/tiff-4.0.9.zip', @@ -34,9 +34,9 @@ libs = { 'dir': 'tiff-4.0.9', }, 'freetype': { - 'url': 'https://download.savannah.gnu.org/releases/freetype/freetype-2.9.tar.gz', - 'filename': PILLOW_DEPENDS_DIR + 'freetype-2.9.tar.gz', - 'dir': 'freetype-2.9', + 'url': 'https://download.savannah.gnu.org/releases/freetype/freetype-2.9.1.tar.gz', + 'filename': PILLOW_DEPENDS_DIR + 'freetype-2.9.1.tar.gz', + 'dir': 'freetype-2.9.1', }, 'lcms': { 'url': SF_MIRROR+'/project/lcms/lcms/2.7/lcms2-2.7.zip', @@ -66,9 +66,9 @@ libs = { 'version': '8.6.8', }, 'webp': { - 'url': 'http://downloads.webmproject.org/releases/webp/libwebp-0.6.1.tar.gz', - 'filename': PILLOW_DEPENDS_DIR + 'libwebp-0.6.1.tar.gz', - 'dir': 'libwebp-0.6.1', + 'url': 'http://downloads.webmproject.org/releases/webp/libwebp-1.0.0.tar.gz', + 'filename': PILLOW_DEPENDS_DIR + 'libwebp-1.0.0.tar.gz', + 'dir': 'libwebp-1.0.0', }, 'openjpeg': { 'url': SF_MIRROR+'/project/openjpeg/openjpeg/2.3.0/openjpeg-2.3.0.tar.gz', diff --git a/winbuild/get_pythons.py b/winbuild/get_pythons.py index 448450afb..376f056b7 100644 --- a/winbuild/get_pythons.py +++ b/winbuild/get_pythons.py @@ -2,7 +2,7 @@ from fetch import fetch import os if __name__ == '__main__': - for version in ['2.7.10', '3.4.3']: + for version in ['2.7.15', '3.4.4']: for platform in ['', '.amd64']: for extension in ['', '.asc']: fetch('https://www.python.org/ftp/python/%s/python-%s%s.msi%s' diff --git a/winbuild/nmake.opt b/winbuild/nmake.opt index b155daacc..28aab3479 100644 --- a/winbuild/nmake.opt +++ b/winbuild/nmake.opt @@ -55,7 +55,7 @@ LOGLUV_SUPPORT = 1 # Uncomment and edit following lines to enable JPEG support. # JPEG_SUPPORT = 1 -JPEGDIR = $(BUILD)\jpeg-9b +JPEGDIR = $(BUILD)\jpeg-9c JPEG_INCLUDE = -I$(JPEGDIR) JPEG_LIB = $(JPEGDIR)/libjpeg.lib