diff --git a/.travis.yml b/.travis.yml index ac51aa4e8..34f37b611 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,104 +6,55 @@ notifications: # Run slow PyPy* first, to give them a headstart and reduce waiting time. # Run latest 3.x and 2.x next, to get quick compatibility results. # Then run the remainder. -python: - - "pypy" - - "pypy3" - - 3.5 - - 2.7 - - 2.6 - - "2.7_with_system_site_packages" # For PyQt4 - - 3.2 - - 3.3 - - 3.4 - - nightly + +matrix: + fast_finish: true + allow_failures: + - python: nightly + include: + - python: "pypy" + - python: "pypy3" + - python: '3.6' + - python: '2.7' + - env: DOCKER="alpine" + - env: DOCKER="ubuntu-trusty-x86" + - env: DOCKER="ubuntu-xenial-amd64" + - env: DOCKER="ubuntu-precise-amd64" + - python: "2.7_with_system_site_packages" # For PyQt4 + - python: '3.5' + - python: '3.4' + - python: '3.3' + - python: 'nightly' + +dist: trusty + +sudo: required + +services: + - docker install: - - "travis_retry pip install pycparser!=2.14" # TEMPORARY, WORKAROUND FOR https://github.com/eliben/pycparser/issues/147 - - "travis_retry sudo apt-get update" - - "travis_retry sudo apt-get -qq install libfreetype6-dev liblcms2-dev python-qt4 ghostscript libffi-dev libjpeg-turbo-progs cmake imagemagick" - - "travis_retry pip install cffi" - - "travis_retry pip install nose" - - "travis_retry pip install check-manifest" - # Pyroma tests sometimes hang on PyPy and Python 2.6; skip for those - - if [ $TRAVIS_PYTHON_VERSION != "pypy" && $TRAVIS_PYTHON_VERSION != "2.6" ]; then travis_retry pip install pyroma; fi + - if [ "$DOCKER" == "" ]; then .travis/install.sh; fi - - if [ "$TRAVIS_PYTHON_VERSION" == "2.6" ]; then travis_retry pip install unittest2; fi +before_install: + - if [ "$DOCKER" ]; then docker pull pythonpillow/$DOCKER; fi - # Coverage 4.0 doesn't support Python 3.2 - - if [ "$TRAVIS_PYTHON_VERSION" == "3.2" ]; then travis_retry pip install coverage==3.7.1; fi - - if [ "$TRAVIS_PYTHON_VERSION" != "3.2" ]; then travis_retry pip install coverage; fi - - # docs only on python 2.7 - - if [ "$TRAVIS_PYTHON_VERSION" == "2.7" ]; then travis_retry pip install -r requirements.txt ; fi - - # clean checkout for manifest - - mkdir /tmp/check-manifest && cp -a . /tmp/check-manifest - - # webp - - pushd depends && ./install_webp.sh && popd - - # openjpeg - - pushd depends && ./install_openjpeg.sh && popd - - # libimagequant - - pushd depends && ./install_imagequant.sh && popd +before_script: +# Qt needs a display for some of the tests, and it's only run on the system site packages install + - "export DISPLAY=:99.0" + - "sh -e /etc/init.d/xvfb start" script: - - if [ "$TRAVIS_PYTHON_VERSION" != "nightly" ]; then coverage erase; fi - - python setup.py clean - - CFLAGS="-coverage" python setup.py build_ext --inplace - - - if [ "$TRAVIS_PYTHON_VERSION" != "nightly" ]; then coverage run --append --include=PIL/* selftest.py; fi - - if [ "$TRAVIS_PYTHON_VERSION" != "nightly" ]; then coverage run --append --include=PIL/* -m nose -vx Tests/test_*.py; fi - - pushd /tmp/check-manifest && check-manifest --ignore ".coveragerc,.editorconfig,*.yml,*.yaml,tox.ini" && popd - - # Docs - - if [ "$TRAVIS_PYTHON_VERSION" == "2.7" ]; then make install && make doccheck; fi + - | + if [ "$DOCKER" == "" ]; then + .travis/script.sh + else + docker run -v $TRAVIS_BUILD_DIR:/Pillow pythonpillow/$DOCKER + fi after_success: - # gather the coverage data - - travis_retry sudo apt-get -qq install lcov - - lcov --capture --directory . -b . --output-file coverage.info - # filter to remove system headers - - lcov --remove coverage.info '/usr/*' -o coverage.filtered.info - # convert to json - - travis_retry gem install coveralls-lcov - - coveralls-lcov -v -n coverage.filtered.info > coverage.c.json - - - coverage report - - travis_retry pip install coveralls-merge - - coveralls-merge coverage.c.json - - - travis_retry pip install pep8 pyflakes - - pep8 --statistics --count PIL/*.py - - pep8 --statistics --count Tests/*.py - - pyflakes *.py | tee >(wc -l) - - pyflakes PIL/*.py | tee >(wc -l) - - pyflakes Tests/*.py | tee >(wc -l) - - # Coverage and quality reports on just the latest diff. - # (Installation is very slow on Py3, so just do it for Py2.) - - if [ ${TRAVIS_PYTHON_VERSION:0:1} == "2" ]; then depends/diffcover-install.sh; fi - - if [ ${TRAVIS_PYTHON_VERSION:0:1} == "2" ]; then 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 succeded! Triggering macOS build..." - # Trigger a macOS build at the pillow-wheels repo - ./build_children.sh - else - echo "Some jobs failed" - fi - fi - fi - + - .travis/after_success.sh + after_failure: - | if [ "$TRAVIS_REPO_SLUG" = "python-pillow/Pillow" ] && [ "$TRAVIS_BRANCH" = "master" ] && [ "$TRAVIS_PULL_REQUEST" = "false" ]; then @@ -125,11 +76,6 @@ after_script: echo leader=$BUILD_LEADER status=$BUILD_AGGREGATE_STATUS fi -matrix: - fast_finish: true - allow_failures: - - python: nightly - env: global: # travis encrypt AUTH_TOKEN= diff --git a/.travis/after_success.sh b/.travis/after_success.sh new file mode 100755 index 000000000..136dbdc8e --- /dev/null +++ b/.travis/after_success.sh @@ -0,0 +1,46 @@ +#!/bin/bash + +# gather the coverage data +sudo apt-get -qq install lcov +lcov --capture --directory . -b . --output-file coverage.info +# filter to remove system headers +lcov --remove coverage.info '/usr/*' -o coverage.filtered.info +# convert to json +gem install coveralls-lcov +coveralls-lcov -v -n coverage.filtered.info > coverage.c.json + +coverage report +pip install coveralls-merge +coveralls-merge coverage.c.json + +if [ "$DOCKER" == "" ]; then + pip install pep8 pyflakes + pep8 --statistics --count PIL/*.py + pep8 --statistics --count Tests/*.py + pyflakes *.py | tee >(wc -l) + pyflakes PIL/*.py | tee >(wc -l) + pyflakes Tests/*.py | tee >(wc -l) +fi + +if [ "$TRAVIS_PYTHON_VERSION" == "2.7" ] && [ "$DOCKER" == "" ]; then + # Coverage and quality reports on just the latest diff. + # (Installation is very slow on Py3, so just do it for Py2.) + 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 succeded! Triggering macOS build..." + # Trigger a macOS build at the pillow-wheels repo + ./build_children.sh + else + echo "Some jobs failed" + fi + fi diff --git a/.travis/install.sh b/.travis/install.sh new file mode 100755 index 000000000..4b7503bed --- /dev/null +++ b/.travis/install.sh @@ -0,0 +1,34 @@ +#!/bin/bash + +set -e + +sudo apt-get update +sudo apt-get -qq install libfreetype6-dev liblcms2-dev\ + python-qt4 ghostscript libffi-dev libjpeg-turbo-progs cmake imagemagick +pip install cffi +pip install nose +pip install check-manifest +pip install olefile +# Pyroma tests sometimes hang on PyPy; skip +if [ "$TRAVIS_PYTHON_VERSION" != "pypy" ]; then pip install pyroma; fi + +pip install coverage + +# docs only on python 2.7 +if [ "$TRAVIS_PYTHON_VERSION" == "2.7" ]; then pip install -r requirements.txt ; fi + +# clean checkout for manifest +mkdir /tmp/check-manifest && cp -a . /tmp/check-manifest + +# webp +pushd depends && ./install_webp.sh && popd + +# openjpeg +pushd depends && ./install_openjpeg.sh && popd + +# libimagequant +pushd depends && ./install_imagequant.sh && popd + +# extra test images +pushd depends && ./install_extra_test_images.sh && popd + diff --git a/.travis/script.sh b/.travis/script.sh new file mode 100755 index 000000000..e1d522122 --- /dev/null +++ b/.travis/script.sh @@ -0,0 +1,14 @@ +#!/bin/bash + +set -e + +coverage erase +python setup.py clean +CFLAGS="-coverage" python setup.py build_ext --inplace + +coverage run --append --include=PIL/* selftest.py +coverage run --append --include=PIL/* -m nose -vx Tests/test_*.py +pushd /tmp/check-manifest && check-manifest --ignore ".coveragerc,.editorconfig,*.yml,*.yaml,tox.ini" && popd + +# Docs +if [ "$TRAVIS_PYTHON_VERSION" == "2.7" ]; then make install && make doccheck; fi diff --git a/CHANGES.rst b/CHANGES.rst index 8cd9f2d1f..8c5025758 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,9 +1,268 @@ Changelog (Pillow) ================== -3.4.0 (unreleased) +4.1.0 (unreleased) ------------------ +- PNG: Moved iCCP chunk before PLTE chunk when saving as PNG, restricted chunks known value/ordering #2347 + [radarhere] + +- Default to inch-interpretation for missing ResolutionUnit in TiffImagePlugin #2365 + [lambdafu] + +- Bug: Fixed segfault when using ImagingTk on pypy Issue #2376, #2359. + [wiredfool] + +- Bug: Fixed Integer overflow using ImagingTk on 32 bit platforms #2359 + [wiredfool, QuLogic] + +- Tests: Added docker images for testing alternate platforms. See also https://github.com/python-pillow/docker-images. #2368 + [wiredfool] + +- Removed PIL 1.0 era TK readme that concerns Windows 95/NT #2360 + [wiredfool] + +- Prevent `nose -v` printing docstrings #2369 + [hugovk] + +- Replaced absolute PIL imports with relative imports #2349 + [radarhere] + +- Added context managers for file handling #2307 + [radarhere] + +- Expose registered file extensions in Image #2343 + [iggomez, radarhere] + +- Make mode descriptor cache initialization thread-safe. #2351 + [gunjambi] + +- Updated Windows test dependencies: Freetype 2.7.1, zlib 1.2.11 #2331, #2332, #2357 + [radarhere] + +- Followed upstream pngquant packaging reorg to libimagquant #2354 + [radarhere] + +- Fix invalid string escapes #2352 + [hugovk] + +- Add test for crop operation with no argument #2333 + [radarhere] + +4.0.0 (2017-01-01) +------------------ + +- Refactor out postprocessing hack to load_end in PcdImageFile + [wiredfool] + +- Add center and translate option to Image.rotate. #2328 + [lambdafu] + +- Test: Relax WMF test condition, fixes #2323 + [wiredfool] + +- Allow 0 size images, Fixes #2259, Reverts to pre-3.4 behavior. + [wiredfool] + +- SGI: Save uncompressed SGI/BW/RGB/RGBA files #2325 + [jbltx] + +- Depends: Updated pngquant to 2.8.2 #2319 + [radarhere] + +- Test: Added correctness tests for opening SGI images #2324 + [wiredfool] + +- Allow passing a list or tuple of individual frame durations when saving a GIF #2298 + [Xdynix] + +- Unified different GIF optimize conditions #2196 + [radarhere] + +- Build: Refactor dependency installation #2305 + [hugovk] + +- Test: Add python 3.6 to travis, tox #2304 + [hugovk] + +- Test: Fix coveralls coverage for Python+C #2300 + [hugovk] + +- Remove executable bit and shebang from OleFileIO.py #2308 + [jwilk, radarhere] + +- PyPy: Buffer interface workaround #2294 + [wiredfool] + +- Test: Switch to Ubuntu Trusty 14.04 on Travis CI #2294 + +- Remove vendored version of olefile Python package in favor of upstream #2199 + [jdufresne] + +- Updated comments to use print as a function #2234 + [radarhere] + +- Set executable flag on selftest.py, setup.py and added shebang line #2282, #2277 + [radarhere, homm] + +- Test: Increase epsilon for FreeType 2.7 as rendering is slightly different. #2286 + [hugovk] + +- Test: Faster assert_image_similar #2279 + [homm] + +- Removed depreciated internal "stretch" method #2276 + [homm] + +- Removed the handles_eof flag in decode.c #2223 + [wiredfool] + +- Tiff: Fix for writing Tiff to BytesIO using libtiff #2263 + [wiredfool] + +- Doc: Design docs #2269 + [wiredfool] + +- Test: Move tests requiring libtiff to test_file_libtiff #2273 + [wiredfool] + +- Update Maxblock heuristic #2275 + [wiredfool] + +- Fix for 2-bit palette corruption #2274 + [pdknsk, wiredfool] + +- Tiff: Update info.icc_profile when using libtiff reader. #2193 + [lambdafu] + +- Test: Fix bug in test_ifd_rational_save when libtiff is not available #2270 + [ChristopherHogan] + +- ICO: Only save relevant sizes #2267 + [hugovk] + +- ICO: Allow saving .ico files of 256x256 instead of 255x255 #2265 + [hugovk] + +- Fix TIFFImagePlugin ICC color profile saving. #2087 + [cskau] + +- Doc: Improved description of ImageOps.deform resample parameter #2256 + [radarhere] + +- EMF: support negative bounding box coordinates #2249 + [glexey] + +- Close file if opened in WalImageFile #2216 + [radarhere] + +- Use Image._new() instead of _makeself() #2248 + [homm] + +- SunImagePlugin fixes #2241 + [wiredfool] + +- Use minimal scale for jpeg drafts #2240 + [homm] + +- Updated dependency scripts to use FreeType 2.7, OpenJpeg 2.1.2, WebP 0.5.2 and Tcl/Tk 8.6.6 #2235, #2236, #2237, #2290, #2302 + [radarhere] + +- Fix "invalid escape sequence" bytestring warnings in Python 3.6 #2186 + [timgraham] + +- Removed support for Python 2.6 and Python 3.2 #2192 + [jdufresne] + +- Setup: Raise custom exceptions when required/requested dependencies are not found #2213 + [wiredfool] + +- Use a context manager in FontFile.save() to ensure file is always closed #2226 + [jdufresne] + +- Fixed bug in saving to fp-objects in Python >= 3.4 #2227 + [radarhere] + +- Use a context manager in ImageFont._load_pilfont() to ensure file is always closed #2232 + [jdufresne] + +- Use generator expressions instead of list comprehension #2225 + [jdufresne] + +- Close file after reading in ImagePalette.load() #2215 + [jdufresne] + +- Changed behaviour of default box argument for paste method to match docs #2211 + [radarhere] + +- Add support for another BMP bitfield #2221 + [jmerdich] + +- Added missing top-level test __main__ #2222 + [radarhere] + +- Replaced range(len()) #2197 + [radarhere] + +- Fix for ImageQt Segfault, fixes #1370 #2182 + [wiredfool] + +- Setup: Close file in setup.py after finished reading #2208 + [jdufresne] + +- Setup: optionally use pkg-config (when present) to detect dependencies #2074 + [garbas] + +- Search for tkinter first in builtins #2210 + [matthew-brett] + +- Tests: Replace try/except/fail pattern with TestCase.assertRaises() #2200 + [jdufresne] + +- Tests: Remove unused, open files at top level of tests #2188 + [jdufresne] + +- Replace type() equality checks with isinstance #2184 + [jdufresne] + +- Doc: Move ICO out of the list of read-only file formats #2180 + [alexwlchan] + +- Doc: Fix formatting, too-short title underlines and malformed table #2175 + [hugovk] + +- Fix BytesWarnings #2172 + [jdufresne] + +- Use Integer division to eliminate deprecation warning. #2168 + [mastermatt] + +- Doc: Update compatibility matrix + [daavve, wiredfool] + + +3.4.2 (2016-10-18) +------------------ + +- Fix Resample coefficient calculation #2161 + [homm] + + +3.4.1 (2016-10-04) +------------------ + +- Allow lists as arguments for Image.new() #2149 + [homm] + +- Fix fix for map.c overflow #2151 (also in 3.3.3) + [wiredfool] + +3.4.0 (2016-10-03) +------------------ + +- Removed Image.core.open_ppm, added negative image size checks in Image.py. #2146 + [wiredfool] + - Windows build: fetch dependencies from pillow-depends #2095 [hugovk] @@ -27,10 +286,10 @@ Changelog (Pillow) - Force reloading palette when using mmap in ImageFile. #2139 [lambdafu] - + - Fix "invalid escape sequence" warning in Python 3.6 #2136 [timgraham] - + - Update documentation about drafts #2137 [radarhere] @@ -39,10 +298,10 @@ Changelog (Pillow) - Fixed typos #2128 #2142 [radarhere] - + - Renamed references to OS X to macOS #2125 2130 [radarhere] - + - Use truth value when checking for progressive and optimize option on save #2115, #2129 [radarhere] @@ -52,7 +311,7 @@ Changelog (Pillow) - Added append_images parameter to GIF saving #2103 [radarhere] -- Speedup paste with masks up to 80% #2015 +- Speedup paste with masks up to 80% #2015 [homm] - Rewrite DDS decoders in C, add DXT3 and BC7 decoders #2068 @@ -124,6 +383,21 @@ Changelog (Pillow) - Retain a reference to core image object in PyAccess #2009 [homm] +3.3.3 (2016-10-04) +------------------ + +- Fix fix for map.c overflow #2151 + [wiredfool] + +3.3.2 (2016-10-03) +------------------ + +- Fix negative image sizes in Storage.c #2105 + [wiredfool] + +- Fix integer overflow in map.c #2105 + [wiredfool] + 3.3.1 (2016-08-18) ------------------ @@ -256,7 +530,7 @@ Changelog (Pillow) - Fix typos in TIFF tags #1918 [radarhere] -- Skip tests that require libtiff if it is not installed, fixes #1866 +- Skip tests that require libtiff if it is not installed #1893 (fixes #1866) [wiredfool] - Skip test when icc profile is not available, fixes #1887 @@ -283,7 +557,7 @@ Changelog (Pillow) - Combined duplicate code in ImageTk #1856 [radarhere] -- Added --disable-platform-guessing option to setup.py build extension, #1861 +- Added --disable-platform-guessing option to setup.py build extension #1861 [angeloc] - Fixed loading Transparent PNGs with a transparent black color #1840 @@ -349,7 +623,7 @@ Changelog (Pillow) - SpiderImagePlugin: raise an error when seeking in a non-stack file #1794 [radarhere, jmichalon] -- Added Support for 2/4 bpp Tiff Grayscale Images #1789 +- Added support for 2/4 bpp Tiff grayscale images #1789 [zwhfly] - Removed unused variable from selftest #1788 @@ -376,7 +650,7 @@ Changelog (Pillow) - Added __copy__ method to Image #1772 [radarhere] -- Updated dates in PIL license in OleFileIO README #1787 +- Updated dates in PIL license in OleFileIO README #1787 [radarhere] - Corrected Tiff tag names #1786 @@ -400,16 +674,16 @@ Changelog (Pillow) - Documentation changes, URL update, transpose, release checklist [radarhere] -- Fixed saving to nonexistant files specified by pathlib.Path objects, fixes #1747 +- Fixed saving to nonexistant files specified by pathlib.Path objects #1748 (fixes #1747) [radarhere] -- Round Image.crop arguments to the nearest integer, fixes #1744 +- Round Image.crop arguments to the nearest integer #1745 (fixes #1744) [hugovk] -- Fix uninitialized variable warning in _imaging.c:getink, fixes #486 +- Fix uninitialized variable warning in _imaging.c:getink #1663 (fixes #486) [wiredfool] -- Disable multiprocessing install on cygwin, fixes #1690 +- Disable multiprocessing install on cygwin #1700 (fixes #1690) [wiredfool] - Fix the error reported when libz is not found #1764 @@ -424,7 +698,7 @@ Changelog (Pillow) - Fix EXIF tag name typos #1736 [zarlant, radarhere] -- Updated freetype to 2.6.3, Tk/Tcl to 8.6.5 and 8.5.19 +- Updated freetype to 2.6.3, Tk/Tcl to 8.6.5 and 8.5.19 #1725, #1752 [radarhere] - Add a loader for the FTEX format from Independence War 2: Edge of Chaos #1688 @@ -442,7 +716,7 @@ Changelog (Pillow) - ImageSequence Iterator is now an iterator #1649 [radarhere] -- Updated windows test builds to jpeg9b +- Updated windows test builds to jpeg9b #1673 [radarhere] - Fixed support for .gbr version 1 images, added support for version 2 in GbrImagePlugin #1653 @@ -516,7 +790,7 @@ Changelog (Pillow) - Let EditorConfig take care of some basic formatting #1489 [hugovk] -- Restore gpsexif data to the v1 form +- Restore gpsexif data to the v1 form #1619 [wiredfool] - Add /usr/local include and library directories for freebsd #1613 @@ -615,16 +889,16 @@ Changelog (Pillow) - Added some requirements for make release-test #1451 [wiredfool] -- Flatten tiff metadata value SAMPLEFORMAT to initial value, fixes #1466 +- Flatten tiff metadata value SAMPLEFORMAT to initial value #1467 (fixes #1466) [wiredfool] -- Fix handling of pathlib in Image.save. Fixes #1460 +- Fix handling of pathlib in Image.save #1464 (fixes #1460) [wiredfool] - Make tests more robust #1469 [hugovk] -- Use correctly sized pointers for windows handle types. #1458 +- Use correctly sized pointers for windows handle types #1458 [nu744] 3.0.0 (2015-10-01) @@ -681,7 +955,7 @@ Changelog (Pillow) - Fix loading of truncated images with LOAD_TRUNCATED_IMAGES enabled #1366 [homm] -- Documentation update for concepts: bands +- Documentation update for concepts: bands #1406 [merriam] - Add Solaris/SmartOS include and library directories #1356 @@ -690,7 +964,7 @@ Changelog (Pillow) - Improved handling of getink color #1387 [radarhere] -- Disable compiler optimizations for topalette and tobilevel functions for all msvc versions, fixes #1357 +- Disable compiler optimizations for topalette and tobilevel functions for all msvc versions #1402 (fixes #1357) [cgohlke] - Skip ImageFont_bitmap test if _imagingft C module is not installed #1409 @@ -870,16 +1144,16 @@ Changelog (Pillow) 2.8.0 (2015-04-01) ------------------ -- Fix 32-bit BMP loading (RGBA or RGBX) +- Fix 32-bit BMP loading (RGBA or RGBX) #1125 [artscoop] - Fix UnboundLocalError in ImageFile #1131 [davarisg] -- Re-enable test image caching +- Re-enable test image caching #982 [hugovk, homm] -- Fix: Cannot identify EPS images, fixes #1104 +- Fix: Cannot identify EPS images #1152 (fixes #1104) [hugovk] - Configure setuptools to run nosetests, fixes #729 @@ -888,7 +1162,7 @@ Changelog (Pillow) - Style/health fixes [radarhere, hugovk] -- Add support for HTTP response objects to Image.open() +- Add support for HTTP response objects to Image.open() #1151 [mfitzp] - Improve reference docs for PIL.ImageDraw.Draw.pieslice() #1145 @@ -900,7 +1174,7 @@ Changelog (Pillow) - Fix ImagingEffectNoise #1128 [hugovk] -- Remove unreachable code +- Remove unreachable code #1126 [hugovk] - Let Python do the endian stuff + tests #1121 @@ -921,10 +1195,10 @@ Changelog (Pillow) - iPython display hook #1091 [wiredfool] -- Adjust buffer size when quality=keep, fixes #148 (again) +- Adjust buffer size when quality=keep #1079 (fixes #148 again) [wiredfool] -- Fix for corrupted bitmaps embedded in truetype fonts. #1072 +- Fix for corrupted bitmaps embedded in truetype fonts #1072 [jackyyf, wiredfool] 2.7.0 (2015-01-01) @@ -933,19 +1207,19 @@ Changelog (Pillow) - Split Sane into a separate repo: https://github.com/python-pillow/Sane [hugovk] -- Look for OS X and Linux fonts in common places. #1054 +- Look for OS X and Linux fonts in common places #1054 [charleslaw] - Fix CVE-2014-9601, potential PNG decompression DOS #1060 [wiredfool] -- Use underscores, not spaces, in TIFF tag kwargs. #1044, #1058 +- Use underscores, not spaces, in TIFF tag kwargs #1044, #1058 [anntzer, hugovk] -- Update PSDraw for Python3, add tests. #1055 +- Update PSDraw for Python3, add tests #1055 [hugovk] -- Use Bicubic filtering by default for thumbnails. Don't use Jpeg Draft mode for thumbnails. #1029 +- Use Bicubic filtering by default for thumbnails. Don't use Jpeg Draft mode for thumbnails #1029 [homm] - Fix MSVC compiler error: Use Py_ssize_t instead of ssize_t #1051 @@ -957,7 +1231,7 @@ Changelog (Pillow) - The GIF Palette optimization algorithm is only applicable to mode='P' or 'L' #993 [moriyoshi] -- Use PySide as an alternative to PyQt4/5. +- Use PySide as an alternative to PyQt4/5 #1024 [holg] - Replace affine-based im.resize implementation with convolution-based im.stretch #997 @@ -981,13 +1255,13 @@ Changelog (Pillow) - Ico save, additional tests #1007 [exherb] -- Use PyQt4 if it has already been imported, otherwise prefer PyQt5. #1003 +- Use PyQt4 if it has already been imported, otherwise prefer PyQt5 #1003 [AurelienBallier] -- Speedup resample implementation up to 2.5 times. #977 +- Speedup resample implementation up to 2.5 times #977 [homm] -- Speed up rotation by using cache aware loops, added transpose to rotations. #994 +- Speed up rotation by using cache aware loops, added transpose to rotations #994 [homm] - Fix Bicubic interpolation #970 @@ -1029,7 +1303,7 @@ Changelog (Pillow) 2.6.0 (2014-10-01) ------------------ -- Relax precision of ImageDraw tests for x86, GimpGradient for PPC +- Relax precision of ImageDraw tests for x86, GimpGradient for PPC #930 [wiredfool] 2.6.0-rc1 (2014-09-29) @@ -1044,7 +1318,7 @@ Changelog (Pillow) - Additional documentation for JPEG info and save options #890 [wiredfool] -- Fix JPEG Encoding memory leak when exif or qtables were specified +- Fix JPEG Encoding memory leak when exif or qtables were specified #921 [wiredfool] - Image.tobytes() and Image.tostring() documentation update #916 #917 @@ -1110,7 +1384,7 @@ Changelog (Pillow) - PyPy performance improvements #821 [wiredfool] -- Added support for reading MPO files +- Added support for reading MPO files #822 [Feneric] - Added support for encoding and decoding iTXt chunks #818 @@ -1128,16 +1402,16 @@ Changelog (Pillow) - Doc cleanup [wiredfool] -- Fix `ImageStat` docs +- Fix `ImageStat` docs #796 [akx] -- Added docs for ExifTags +- Added docs for ExifTags #794 [Wintermute3] - More tests for CurImagePlugin, DcxImagePlugin, Effects.c, GimpGradientFile, ImageFont, ImageMath, ImagePalette, IptcImagePlugin, SpiderImagePlugin, SgiImagePlugin, XpmImagePlugin and _util [hugovk] -- Fix return value of FreeTypeFont.textsize() does not include font offsets +- Fix return value of FreeTypeFont.textsize() does not include font offsets #784 [tk0miya] - Fix dispose calculations for animated GIFs #765 @@ -1162,7 +1436,6 @@ Changelog (Pillow) - Fixed CVE-2014-3589, a DOS in the IcnsImagePlugin (backport) [Andrew Drake] - 2.5.1 (2014-07-10) ------------------ @@ -1175,10 +1448,10 @@ Changelog (Pillow) 2.5.0 (2014-07-01) ------------------ -- Imagedraw rewrite +- Imagedraw rewrite #737 [terseus, wiredfool] -- Add support for multithreaded test execution +- Add support for multithreaded test execution #755 [wiredfool] - Prevent shell injection #748 @@ -1187,7 +1460,7 @@ Changelog (Pillow) - Support for Resolution in BMP files #734 [gcq] -- Fix error in setup.py for Python 3 +- Fix error in setup.py for Python 3 #744 [matthew-brett] - Pyroma fix and add Python 3.4 to setup metadata #742 @@ -1196,7 +1469,7 @@ Changelog (Pillow) - Top level flake8 fixes #741 [aclark4life] -- Remove obsolete Animated Raster Graphics (ARG) support +- Remove obsolete Animated Raster Graphics (ARG) support #736 [hugovk] - Fix test_imagedraw failures #727 @@ -1211,28 +1484,28 @@ Changelog (Pillow) - Cleanup #654 [dvska, hugovk, wiredfool] -- 16-bit monochrome support for JPEG2000 +- 16-bit monochrome support for JPEG2000 #730 [videan42] - Fixed ImagePalette.save [brightpisces] -- Support JPEG qtables +- Support JPEG qtables #677 [csinchok] - Add binary morphology addon [dov, wiredfool] -- Decompression bomb protection +- Decompression bomb protection #674 [hugovk] -- Put images in a single directory +- Put images in a single directory #708 [hugovk] -- Support OpenJpeg 2.1 - [al45tair] +- Support OpenJpeg 2.1 #681 + [al45tair, wiredfool] -- Remove unistd.h #include for all platforms +- Remove unistd.h #include for all platforms #704 [wiredfool] - Use unittest for tests @@ -1247,19 +1520,19 @@ Changelog (Pillow) - Added tests for Spider files [hugovk] -- Use libtiff to write any compressed tiff files +- Use libtiff to write any compressed tiff files #669 [wiredfool] - Support for pickling Image objects [hugovk] -- Fixed resolution handling for EPS thumbnails +- Fixed resolution handling for EPS thumbnails #619 [eliempje] - Fixed rendering of some binary EPS files (Issue #302) [eliempje] -- Rename variables not to use built-in function names +- Rename variables not to use built-in function names #670 [hugovk] - Ignore junk JPEG markers @@ -1274,19 +1547,19 @@ Changelog (Pillow) - Remove transparency resource after P->RGBA conversion [hugovk] -- Clean up preprocessor cruft for Windows +- Clean up preprocessor cruft for Windows #652 [CounterPillow] -- Adjust Homebrew freetype detection logic +- Adjust Homebrew freetype detection logic #656 [jacknagel] -- Added Image.close, context manager support. +- Added Image.close, context manager support [wiredfool] -- Added support for 16 bit PGM files. +- Added support for 16 bit PGM files [wiredfool] -- Updated OleFileIO to version 0.30 from upstream +- Updated OleFileIO to version 0.30 from upstream #618 [hugovk] - Added support for additional TIFF floating point format @@ -1295,64 +1568,64 @@ Changelog (Pillow) - Have the tempfile use a suffix with a dot [wiredfool] -- Fix variable name used for transparency manipulations +- Fix variable name used for transparency manipulations #604 [nijel] 2.4.0 (2014-04-01) ------------------ -- Indexed Transparency handled for conversions between L, RGB, and P modes. Fixes #510 +- Indexed Transparency handled for conversions between L, RGB, and P modes #574 (fixes #510) [wiredfool] -- Conversions enabled from RGBA->P, Fixes #544 +- Conversions enabled from RGBA->P #574 (fixes #544) [wiredfool] -- Improved icns support +- Improved icns support #565 [al45tair] -- Fix libtiff leaking open files, fixes #580 +- Fix libtiff leaking open files #580 (fixes #526) [wiredfool] -- Fixes for Jpeg encoding in Python 3, fixes #577 +- Fixes for Jpeg encoding in Python 3 #578 (fixes #577) [wiredfool] -- Added support for JPEG 2000 +- Added support for JPEG 2000 #547 [al45tair] -- Add more detailed error messages to Image.py +- Add more detailed error messages to Image.py #566 [larsmans] - Avoid conflicting _expand functions in PIL & MINGW, fixes #538 [aclark4life] -- Merge from Philippe Lagadec’s OleFileIO_PL fork +- Merge from Philippe Lagadec’s OleFileIO_PL fork #512 [vadmium] -- Fix ImageColor.getcolor +- Fix ImageColor.getcolor #534 [homm] -- Make ICO files work with the ImageFile.Parser interface, fixes #522 +- Make ICO files work with the ImageFile.Parser interface #525 (fixes #522) [wiredfool] -- Handle 32bit compiled python on 64bit architecture +- Handle 32bit compiled python on 64bit architecture #521 [choppsv1] -- Fix support for characters >128 using .pcf or .pil fonts in Py3k. Fixes #505 +- Fix support for characters >128 using .pcf or .pil fonts in Py3k #517 (fixes #505) [wiredfool] -- Skip CFFI test earlier if it's not installed +- Skip CFFI test earlier if it's not installed #516 [wiredfool] -- Fixed opening and saving odd sized .pcx files, fixes #523 +- Fixed opening and saving odd sized .pcx files #535 (fixes #523) [wiredfool] - Fixed palette handling when converting from mode P->RGB->P - [d_schmidt] + [d-schmidt] - Fixed saving mode P image as a PNG with transparency = palette color 0 [d-schmidt] -- Improve heuristic used when saving progressive and optimized JPEGs with high quality values +- Improve heuristic used when saving progressive and optimized JPEGs with high quality values #504 [e98cuenc] - Fixed DOS with invalid palette size or invalid image size in BMP file @@ -1364,7 +1637,7 @@ Changelog (Pillow) - Fix segfault in getfont when passed a memory resident font [wiredfool] -- Fix crash on Saving a PNG when icc-profile is None +- Fix crash on Saving a PNG when icc-profile is None #496 [brutasse] - Cffi+Python implementation of the PixelAccess object @@ -1373,13 +1646,13 @@ Changelog (Pillow) - PixelAccess returns unsigned ints for I16 mode [wiredfool] -- Minor patch on booleans + Travis +- Minor patch on booleans + Travis #474 [sciunto] -- Look in multiarch paths in GNU platforms +- Look in multiarch paths in GNU platforms #511 [pinotree] -- Add arch support for pcc64, s390, s390x, armv7l, aarch64 +- Add arch support for pcc64, s390, s390x, armv7l, aarch64 #475 [manisandro] - Add arch support for ppc @@ -1388,7 +1661,7 @@ Changelog (Pillow) - Correctly quote file names for WindowsViewer command [cgohlke] -- Prefer homebrew freetype over X11 freetype (but still allow both) +- Prefer homebrew freetype over X11 freetype (but still allow both) #466 [dmckeone] 2.3.2 (2014-08-13) @@ -1406,76 +1679,76 @@ Changelog (Pillow) 2.3.0 (2014-01-01) ------------------ -- Stop leaking filename parameter passed to getfont +- Stop leaking filename parameter passed to getfont #459 [jpharvey] - Report availability of LIBTIFF during setup and selftest [cgohlke] -- Fix msvc build error C1189: "No Target Architecture" +- Fix msvc build error C1189: "No Target Architecture" #460 [cgohlke] - Fix memory leak in font_getsize [wiredfool] -- Correctly prioritize include and library paths +- Correctly prioritize include and library paths #442 [ohanar] -- Image.point fixes for numpy.array and docs +- Image.point fixes for numpy.array and docs #441 [wiredfool] -- Save the transparency header by default for PNGs +- Save the transparency header by default for PNGs #424 [wiredfool] -- Support for PNG tRNS header when converting from RGB->RGBA +- Support for PNG tRNS header when converting from RGB->RGBA #423 [wiredfool] -- PyQT5 Support +- PyQT5 Support #418 [wiredfool] -- Updates for saving color tiffs w/compression using libtiff +- Updates for saving color tiffs w/compression using libtiff #417 [wiredfool] - 2gigapix image fixes and redux [wiredfool] -- Save arbitrary tags in Tiff image files +- Save arbitrary tags in Tiff image files #369 [wiredfool] -- Quote filenames and title before using on command line +- Quote filenames and title before using on command line #398 [tmccombs] -- Fixed Viewer.show to return properly +- Fixed Viewer.show to return properly #399 [tmccombs] - Documentation fixes [wiredfool] -- Fixed memory leak saving images as webp when webpmux is available +- Fixed memory leak saving images as webp when webpmux is available #429 [cezarsa] -- Fix compiling with FreeType 2.5.1 +- Fix compiling with FreeType 2.5.1 #427 [stromnov] -- Adds directories for NetBSD. +- Adds directories for NetBSD #411 [deepy] -- Support RGBA TIFF with missing ExtraSamples tag +- Support RGBA TIFF with missing ExtraSamples tag #393 [cgohlke] -- Lossless WEBP Support +- Lossless WEBP Support #390 [wiredfool] -- Take compression as an option in the save call for tiffs +- Take compression as an option in the save call for tiffs #389 [wiredfool] -- Add support for saving lossless WebP. Just pass 'lossless=True' to save() +- Add support for saving lossless WebP. Just pass 'lossless=True' to save() #386 [liftoff] -- LCMS support upgraded from version 1 to version 2, fixes #343 +- LCMS support upgraded from version 1 to version 2 #380 (fixes #343) [wiredfool] -- Added more raw decoder 16 bit pixel formats +- Added more raw decoder 16 bit pixel formats #379 [svanheulen] - Document remaining Image* modules listed in PIL handbook @@ -1496,34 +1769,34 @@ Changelog (Pillow) - Port PIL Handbook tutorial and appendices [irksep] -- Alpha Premultiplication support for transform and resize +- Alpha Premultiplication support for transform and resize #364 [wiredfool] -- Fixes to make Pypy 2.1.0 work on Ubuntu 12.04/64 +- Fixes to make Pypy 2.1.0 work on Ubuntu 12.04/64 #359 [wiredfool] 2.2.2 (2013-12-11) ------------------ -- Fix #427: compiling with FreeType 2.5.1 +- Fix compiling with FreeType 2.5.1 #427 [stromnov] 2.2.1 (2013-10-02) ------------------ -- Fix #356: Error installing Pillow 2.2.0 on Mac OS X (due to hard dep on brew) +- Error installing Pillow 2.2.0 on Mac OS X (due to hard dep on brew) #357 (fixes #356) [wiredfool] 2.2.0 (2013-10-02) ------------------ -- Fix #254: Bug in image transformations resulting from uninitialized memory +- Bug in image transformations resulting from uninitialized memory #348 (fixes #254) [nikmolnar] -- Fix for encoding of b_whitespace, similar to closed issue #272 +- Fix for encoding of b_whitespace #346 (similar to closed issue #272) [mhogg] -- Fix #273: Add numpy array interface support for 16 and 32 bit integer modes +- Add numpy array interface support for 16 and 32 bit integer modes #347 (fixes #273) [cgohlke] - Partial fix for #290: Add preliminary support for TIFF tags. @@ -1532,91 +1805,93 @@ Changelog (Pillow) - Fix #251 and #326: circumvent classification of pngtest_bad.png as malware [cgohlke] -- Add typedef uint64_t for MSVC. +- Add typedef uint64_t for MSVC #339 [cgohlke] -- Fix #329: setup.py: better support for C_INCLUDE_PATH, LD_RUN_PATH, etc. +- setup.py: better support for C_INCLUDE_PATH, LD_RUN_PATH, etc. #336 (fixes #329) [nu774] -- Fix #328: _imagingcms.c: include windef.h to fix build issue on MSVC +- _imagingcms.c: include windef.h to fix build issue on MSVC #335 (fixes #328) [nu774] -- Automatically discover homebrew include/ and lib/ paths on OS X +- Automatically discover homebrew include/ and lib/ paths on OS X #330 [donspaulding] -- Fix bytes which should be bytearray +- Fix bytes which should be bytearray #325 [manisandro] - Add respective paths for C_INCLUDE_PATH, LD_RUN_PATH (rpath) to build - if specified as environment variables. + if specified as environment variables #324 [seanupton] - Fix #312 + gif optimize improvement [d-schmidt] -- Be more tolerant of tag read failures +- Be more tolerant of tag read failures #320 [ericbuehl] -- Fix #318: Catch truncated zTXt errors. +- Catch truncated zTXt errors #321 (fixes #318) [vytisb] -- Fix IOError when saving progressive JPEGs. +- Fix IOError when saving progressive JPEGs #313 [e98cuenc] -- Add RGBA support to ImageColor +- Add RGBA support to ImageColor #309 [yoavweiss] -- Fix #304: test for `str`, not `"utf-8"`. +- Test for `str`, not `"utf-8"` #306 (fixes #304) [mjpieters] -- Fix missing import os in _util.py. +- Fix missing import os in _util.py #303 [mnowotka] -- Added missing exif tags. +- Added missing exif tags #300 [freyes] -- Fail on all import errors, fixes #298. +- Fail on all import errors #298, #299 (fixes #297) [macfreek, wiredfool] -- Fixed Windows fallback (wasn't using correct file in Windows fonts). +- Fixed Windows fallback (wasn't using correct file in Windows fonts) #295 [lmollea] -- Moved ImageFile and ImageFileIO comments to docstrings. +- Moved ImageFile and ImageFileIO comments to docstrings #293 [freyes] -- Restore compatibility with ISO C. +- Restore compatibility with ISO C #289 [cgohlke] -- Use correct format character for C int type. +- Use correct format character for C int type #288 [cgohlke] -- Allocate enough memory to hold pointers in encode.c. +- Allocate enough memory to hold pointers in encode.c #287 [cgohlke] -- Fix #279, fillorder double shuffling bug when FillOrder ==2 and decoding using libtiff. +- Fillorder double shuffling bug when FillOrder ==2 and decoding using libtiff #284 (fixes #279) [wiredfool] - Moved Image module comments to docstrings. [freyes] -- Add 16-bit TIFF support, fixes #274. +- Add 16-bit TIFF support #277 (fixes #274) [wiredfool] -- Ignore high ascii characters in string.whitespace, fixes #272. +- Ignore high ascii characters in string.whitespace #276 (fixes #272) [wiredfool] -- Added clean/build to tox to make it behave like travis. +- Added clean/build to tox to make it behave like Travis #275 [freyes] -- Adding support for metadata in webp images. +- Adding support for metadata in webp images #271 [heynemann] 2.1.0 (2013-07-02) ------------------ -- Add /usr/bin/env python shebangs to all scripts in /Scripts. +- Add /usr/bin/env python shebangs to all scripts in /Scripts #197 + [mgorny] -- Add several TIFF decoders and encoders. +- Add several TIFF decoders and encoders #268 + [megabuz] - Added support for alpha transparent webp images. @@ -1624,15 +1899,17 @@ Changelog (Pillow) - Adding Python3 basestring compatibility without changing basestring. -- Fix webp encode errors on win-amd64. +- Fix webp encode errors on win-amd64 #259 + [cgohlke] -- Better fix for ZeroDivisionError in ImageOps.fit for image.size height is 1. +- Better fix for ZeroDivisionError in ImageOps.fit for image.size height is 1 #267 + [chrispbailey] - Better support for ICO images. -- Changed PY_VERSION_HEX, fixes #166. +- Changed PY_VERSION_HEX #190 (fixes #166) -- Changes to put everything under the PIL namespace. +- Changes to put everything under the PIL namespace #191 [wiredfool] - Changing StringIO to BytesIO. @@ -1643,35 +1920,44 @@ Changelog (Pillow) - Don't skip 'import site' on initialization when running tests for inplace builds. [cgohlke] -- Enable warnings for test suite. +- Enable warnings for test suite #227 + [wiredfool] -- Fix for ZeroDivisionError in ImageOps.fit for image.size == (1,1) +- Fix for ZeroDivisionError in ImageOps.fit for image.size == (1,1) #255 + [pterk] - Fix for if isinstance(filter, collections.Callable) crash. Python bug #7624 on <2.6.6 -- Fix #193: remove double typedef declaration. +- Remove double typedef declaration #194 (fixes #193) + [evertrol] - Fix msvc compile errors (#230). -- Fix rendered characters have been chipped for some TrueType fonts. +- Fix rendered characters have been chipped for some TrueType fonts + [tk0miya] -- Fix usage of pilfont.py script. +- Fix usage of pilfont.py script #184 + [fabiomcosta] - Fresh start for docs, generated by sphinx-apidoc. - Introduce --enable-x and fail if it is given and x is not available. -- Partial work to add a wrapper for WebPGetFeatures to correctly support #204. +- Partial work to add a wrapper for WebPGetFeatures to correctly support #220 (fixes #204) -- Significant performance improvement of `alpha_composite` function. +- Significant performance improvement of `alpha_composite` function #156 + [homm] -- Support explicitly disabling features via --disable-* options. +- Support explicitly disabling features via --disable-* options #240 + [mgorny] -- Support selftest.py --installed, fixes #263. +- Support selftest.py --installed, fixes #263 -- Transparent WebP Support, #204 +- Transparent WebP Support #220 (fixes #204) + [euangoddard, wiredfool] -- Use PyCapsule for py3.1, fixes #237. +- Use PyCapsule for py3.1 #238 (fixes #237) + [wiredfool] - Workaround for: http://bugs.python.org/issue16754 in 3.2.x < 3.2.4 and 3.3.0. @@ -1685,15 +1971,15 @@ Changelog (Pillow) - Add Python 3 support. (Pillow >= 2.0.0 supports Python 2.6, 2.7, 3.2, 3.3. Pillow < 2.0.0 supports Python 2.4, 2.5, 2.6, 2.7.) [fluggo] -- Add PyPy support (experimental, please see: https://github.com/python-pillow/Pillow/issues/67) +- Add PyPy support (experimental, please see #67) -- Add WebP support. +- Add WebP support #96 [lqs] - Add Tiff G3/G4 support (experimental) [wiredfool] -- Backport PIL's PNG/Zip improvements. +- Backport PIL's PNG/Zip improvements #95, #97 [olt] - Various 64-bit and Windows fixes. diff --git a/LICENSE b/LICENSE index e0e216dea..f8346793e 100644 --- a/LICENSE +++ b/LICENSE @@ -5,7 +5,7 @@ The Python Imaging Library (PIL) is Pillow is the friendly PIL fork. It is - Copyright © 2012-2016 by Alex Clark and contributors + Copyright © 2010-2017 by Alex Clark and contributors Like PIL, Pillow is licensed under the open source PIL Software License: diff --git a/Makefile b/Makefile index 493364cd8..60f4aa421 100644 --- a/Makefile +++ b/Makefile @@ -58,6 +58,13 @@ install: python setup.py install python selftest.py --installed +debug: +# make a debug version if we don't have a -dbg python. Leaves in symbols +# for our stuff, kills optimization, and redirects to dev null so we +# see any build failures. + make clean > /dev/null + CFLAGS='-g -O0' python setup.py build_ext install > /dev/null + install-req: pip install -r requirements.txt diff --git a/PIL/BdfFontFile.py b/PIL/BdfFontFile.py index e6cc22f91..c8bc60461 100644 --- a/PIL/BdfFontFile.py +++ b/PIL/BdfFontFile.py @@ -17,8 +17,9 @@ # See the README file for information on usage and redistribution. # -from PIL import Image -from PIL import FontFile +from __future__ import print_function + +from . import Image, FontFile # -------------------------------------------------------------------- @@ -119,9 +120,9 @@ class BdfFontFile(FontFile.FontFile): # fontname = ";".join(font[1:]) - # print "#", fontname + # print("#", fontname) # for i in comments: - # print "#", i + # print("#", i) while True: c = bdf_char(fp) diff --git a/PIL/BmpImagePlugin.py b/PIL/BmpImagePlugin.py index eccd29923..1afe303ab 100644 --- a/PIL/BmpImagePlugin.py +++ b/PIL/BmpImagePlugin.py @@ -24,18 +24,13 @@ # -from PIL import Image, ImageFile, ImagePalette, _binary +from . import Image, ImageFile, ImagePalette +from ._binary import i8, i16le as i16, i32le as i32, \ + o8, o16le as o16, o32le as o32 import math __version__ = "0.7" -i8 = _binary.i8 -i16 = _binary.i16le -i32 = _binary.i32le -o8 = _binary.o8 -o16 = _binary.o16le -o32 = _binary.o32le - # # -------------------------------------------------------------------- # Read BMP file @@ -73,7 +68,7 @@ class BmpImageFile(ImageFile.ImageFile): read, seek = self.fp.read, self.fp.seek if header: seek(header) - file_info = dict() + file_info = {} file_info['header_size'] = i32(read(4)) # read bmp header size @offset 14 (this is part of the header size) file_info['direction'] = -1 # --------------------- If requested, read header at a specific position @@ -136,12 +131,13 @@ class BmpImageFile(ImageFile.ImageFile): # ----------------- Process BMP with Bitfields compression (not palette) if file_info['compression'] == self.BITFIELDS: SUPPORTED = { - 32: [(0xff0000, 0xff00, 0xff, 0x0), (0xff0000, 0xff00, 0xff, 0xff000000), (0x0, 0x0, 0x0, 0x0)], + 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)] } MASK_MODES = { (32, (0xff0000, 0xff00, 0xff, 0x0)): "BGRX", + (32, (0xff000000, 0xff0000, 0xff00, 0x0)): "XBGR", (32, (0xff0000, 0xff00, 0xff, 0xff000000)): "BGRA", (32, (0x0, 0x0, 0x0, 0x0)): "BGRA", (24, (0xff0000, 0xff00, 0xff)): "BGR", diff --git a/PIL/BufrStubImagePlugin.py b/PIL/BufrStubImagePlugin.py index 5184546e4..4c5da942f 100644 --- a/PIL/BufrStubImagePlugin.py +++ b/PIL/BufrStubImagePlugin.py @@ -9,7 +9,7 @@ # See the README file for information on usage and redistribution. # -from PIL import Image, ImageFile +from . import Image, ImageFile _handler = None diff --git a/PIL/CurImagePlugin.py b/PIL/CurImagePlugin.py index 4db4c4073..e4257cd5a 100644 --- a/PIL/CurImagePlugin.py +++ b/PIL/CurImagePlugin.py @@ -16,18 +16,16 @@ # See the README file for information on usage and redistribution. # +from __future__ import print_function -from PIL import Image, BmpImagePlugin, _binary +from . import Image, BmpImagePlugin +from ._binary import i8, i16le as i16, i32le as i32 __version__ = "0.1" # # -------------------------------------------------------------------- -i8 = _binary.i8 -i16 = _binary.i16le -i32 = _binary.i32le - def _accept(prefix): return prefix[:4] == b"\0\0\2\0" @@ -58,14 +56,14 @@ class CurImageFile(BmpImagePlugin.BmpImageFile): m = s elif i8(s[0]) > i8(m[0]) and i8(s[1]) > i8(m[1]): m = s - # print "width", i8(s[0]) - # print "height", i8(s[1]) - # print "colors", i8(s[2]) - # print "reserved", i8(s[3]) - # print "hotspot x", i16(s[4:]) - # print "hotspot y", i16(s[6:]) - # print "bytes", i32(s[8:]) - # print "offset", i32(s[12:]) + # print("width", i8(s[0])) + # print("height", i8(s[1])) + # print("colors", i8(s[2])) + # print("reserved", i8(s[3])) + # print("hotspot x", i16(s[4:])) + # print("hotspot y", i16(s[6:])) + # print("bytes", i32(s[8:])) + # print("offset", i32(s[12:])) if not m: raise TypeError("No cursors were found") diff --git a/PIL/DcxImagePlugin.py b/PIL/DcxImagePlugin.py index f9034d15c..5663dff5f 100644 --- a/PIL/DcxImagePlugin.py +++ b/PIL/DcxImagePlugin.py @@ -21,15 +21,14 @@ # See the README file for information on usage and redistribution. # -from PIL import Image, _binary -from PIL.PcxImagePlugin import PcxImageFile +from . import Image +from ._binary import i32le as i32 +from .PcxImagePlugin import PcxImageFile __version__ = "0.2" MAGIC = 0x3ADE68B1 # QUIZ: what's this value, then? -i32 = _binary.i32le - def _accept(prefix): return len(prefix) >= 4 and i32(prefix) == MAGIC diff --git a/PIL/DdsImagePlugin.py b/PIL/DdsImagePlugin.py index b6228c2ad..9508e61c8 100644 --- a/PIL/DdsImagePlugin.py +++ b/PIL/DdsImagePlugin.py @@ -12,7 +12,7 @@ Full text of the CC0 license: import struct from io import BytesIO -from PIL import Image, ImageFile +from . import Image, ImageFile # Magic ("DDS ") diff --git a/PIL/EpsImagePlugin.py b/PIL/EpsImagePlugin.py index 9a668125a..8dd3e6857 100644 --- a/PIL/EpsImagePlugin.py +++ b/PIL/EpsImagePlugin.py @@ -23,16 +23,14 @@ import re import io import sys -from PIL import Image, ImageFile, _binary +from . import Image, ImageFile +from ._binary import i32le as i32, o32le as o32 __version__ = "0.5" # # -------------------------------------------------------------------- -i32 = _binary.i32le -o32 = _binary.o32le - split = re.compile(r"^%%([^:]*):[ \t]*(.*)[ \t]*$") field = re.compile(r"^%[%!\w]([^:]*)[ \t]*$") @@ -145,7 +143,8 @@ def Ghostscript(tile, size, fp, scale=1): status = gs.wait() if status: raise IOError("gs failed (status %d)" % status) - im = Image.core.open_ppm(outfile) + im = Image.open(outfile) + im.load() finally: try: os.unlink(outfile) @@ -154,7 +153,7 @@ def Ghostscript(tile, size, fp, scale=1): except OSError: pass - return im + return im.im.copy() class PSFile(object): diff --git a/PIL/FitsStubImagePlugin.py b/PIL/FitsStubImagePlugin.py index b6ea0e37d..e3a7eb4a6 100644 --- a/PIL/FitsStubImagePlugin.py +++ b/PIL/FitsStubImagePlugin.py @@ -9,7 +9,7 @@ # See the README file for information on usage and redistribution. # -from PIL import Image, ImageFile +from . import Image, ImageFile _handler = None diff --git a/PIL/FliImagePlugin.py b/PIL/FliImagePlugin.py index a07dc29b0..429b5e26f 100644 --- a/PIL/FliImagePlugin.py +++ b/PIL/FliImagePlugin.py @@ -16,15 +16,11 @@ # -from PIL import Image, ImageFile, ImagePalette, _binary +from . import Image, ImageFile, ImagePalette +from ._binary import i8, i16le as i16, i32le as i32, o8 __version__ = "0.2" -i8 = _binary.i8 -i16 = _binary.i16le -i32 = _binary.i32le -o8 = _binary.o8 - # # decoder diff --git a/PIL/FontFile.py b/PIL/FontFile.py index 1ccfaa3c3..46e49bc4e 100644 --- a/PIL/FontFile.py +++ b/PIL/FontFile.py @@ -14,8 +14,10 @@ # See the README file for information on usage and redistribution. # +from __future__ import print_function + import os -from PIL import Image, _binary +from . import Image, _binary WIDTH = 800 @@ -88,7 +90,7 @@ class FontFile(object): x = xx s = src[0] + x0, src[1] + y0, src[2] + x0, src[3] + y0 self.bitmap.paste(im.crop(src), s) - # print chr(i), dst, s + # print(chr(i), dst, s) self.metrics[i] = d, dst, s def save(self, filename): @@ -100,14 +102,13 @@ class FontFile(object): self.bitmap.save(os.path.splitext(filename)[0] + ".pbm", "PNG") # font metrics - fp = open(os.path.splitext(filename)[0] + ".pil", "wb") - fp.write(b"PILfont\n") - fp.write((";;;;;;%d;\n" % self.ysize).encode('ascii')) # HACK!!! - fp.write(b"DATA\n") - for id in range(256): - m = self.metrics[id] - if not m: - puti16(fp, [0] * 10) - else: - puti16(fp, m[0] + m[1] + m[2]) - fp.close() + with open(os.path.splitext(filename)[0] + ".pil", "wb") as fp: + fp.write(b"PILfont\n") + fp.write((";;;;;;%d;\n" % self.ysize).encode('ascii')) # HACK!!! + fp.write(b"DATA\n") + for id in range(256): + m = self.metrics[id] + if not m: + puti16(fp, [0] * 10) + else: + puti16(fp, m[0] + m[1] + m[2]) diff --git a/PIL/FpxImagePlugin.py b/PIL/FpxImagePlugin.py index a4a9098a7..23f15f459 100644 --- a/PIL/FpxImagePlugin.py +++ b/PIL/FpxImagePlugin.py @@ -15,13 +15,15 @@ # See the README file for information on usage and redistribution. # +from __future__ import print_function -from PIL import Image, ImageFile -from PIL.OleFileIO import i8, i32, MAGIC, OleFileIO +from . import Image, ImageFile +from ._binary import i32le as i32, i8 + +import olefile __version__ = "0.1" - # we map from colour field tuples to (mode, rawmode) descriptors MODES = { # opacity @@ -42,7 +44,7 @@ MODES = { # -------------------------------------------------------------------- def _accept(prefix): - return prefix[:8] == MAGIC + return prefix[:8] == olefile.MAGIC ## @@ -59,7 +61,7 @@ class FpxImageFile(ImageFile.ImageFile): # to be a FlashPix file try: - self.ole = OleFileIO(self.fp) + self.ole = olefile.OleFileIO(self.fp) except IOError: raise SyntaxError("not an FPX file; invalid OLE file") @@ -112,7 +114,7 @@ class FpxImageFile(ImageFile.ImageFile): if id in prop: self.jpeg[i] = prop[id] - # print len(self.jpeg), "tables loaded" + # print(len(self.jpeg), "tables loaded") self._open_subimage(1, self.maxid) @@ -141,7 +143,7 @@ class FpxImageFile(ImageFile.ImageFile): offset = i32(s, 28) length = i32(s, 32) - # print size, self.mode, self.rawmode + # print(size, self.mode, self.rawmode) if size != self.size: raise IOError("subimage mismatch") diff --git a/PIL/FtexImagePlugin.py b/PIL/FtexImagePlugin.py index 9dab83621..0d08f4cc4 100644 --- a/PIL/FtexImagePlugin.py +++ b/PIL/FtexImagePlugin.py @@ -13,7 +13,7 @@ packed custom format called FTEX. This file format uses file extensions FTC and * FTC files are compressed textures (using standard texture compression). * FTU files are not compressed. Texture File Format -The FTC and FTU texture files both use the same format, called. This +The FTC and FTU texture files both use the same format. This has the following structure: {header} {format_directory} @@ -42,7 +42,7 @@ Note: All data is stored in little-Endian (Intel) byte order. import struct from io import BytesIO -from PIL import Image, ImageFile +from . import Image, ImageFile MAGIC = b"FTEX" diff --git a/PIL/GbrImagePlugin.py b/PIL/GbrImagePlugin.py index d62981c28..b8b9f1a3c 100644 --- a/PIL/GbrImagePlugin.py +++ b/PIL/GbrImagePlugin.py @@ -24,9 +24,8 @@ # Version 3 files have a format specifier of 18 for 16bit floats in # the color depth field. This is currently unsupported by Pillow. -from PIL import Image, ImageFile, _binary - -i32 = _binary.i32be +from . import Image, ImageFile +from ._binary import i32be as i32 def _accept(prefix): diff --git a/PIL/GdImageFile.py b/PIL/GdImageFile.py index 5a07ee230..09ab5ec69 100644 --- a/PIL/GdImageFile.py +++ b/PIL/GdImageFile.py @@ -23,8 +23,9 @@ # purposes only. -from PIL import ImageFile, ImagePalette, _binary -from PIL._util import isPath +from . import ImageFile, ImagePalette +from ._binary import i16be as i16 +from ._util import isPath __version__ = "0.1" @@ -34,8 +35,6 @@ except ImportError: import __builtin__ builtins = __builtin__ -i16 = _binary.i16be - ## # Image plugin for the GD uncompressed format. Note that this format diff --git a/PIL/GifImagePlugin.py b/PIL/GifImagePlugin.py index df17f830b..2e519c7ac 100644 --- a/PIL/GifImagePlugin.py +++ b/PIL/GifImagePlugin.py @@ -24,8 +24,9 @@ # See the README file for information on usage and redistribution. # -from PIL import Image, ImageFile, ImagePalette, \ - ImageChops, ImageSequence, _binary +from . import Image, ImageFile, ImagePalette, \ + ImageChops, ImageSequence +from ._binary import i8, i16le as i16, o8, o16le as o16 __version__ = "0.9" @@ -33,11 +34,6 @@ __version__ = "0.9" # -------------------------------------------------------------------- # Helpers -i8 = _binary.i8 -i16 = _binary.i16le -o8 = _binary.o8 -o16 = _binary.o16le - # -------------------------------------------------------------------- # Identify/read GIF files @@ -352,10 +348,18 @@ def _save(im, fp, filename, save_all=False): first_frame = None append_images = im.encoderinfo.get("append_images", []) + if "duration" in im.encoderinfo: + duration = im.encoderinfo["duration"] + else: + duration = None + frame_count = 0 for imSequence in [im]+append_images: for im_frame in ImageSequence.Iterator(imSequence): encoderinfo = im.encoderinfo.copy() im_frame = _convert_mode(im_frame) + if isinstance(duration, (list, tuple)): + encoderinfo["duration"] = duration[frame_count] + frame_count += 1 # To specify duration, add the time in milliseconds to getdata(), # e.g. getdata(im_frame, duration=1000) @@ -434,18 +438,16 @@ def _get_local_header(fp, im, offset, flags): # optimize the block away if transparent color is not used transparent_color_exists = True - if _get_optimize(im, im.encoderinfo): - used_palette_colors = _get_used_palette_colors(im) - + used_palette_colors = _get_optimize(im, im.encoderinfo) + if used_palette_colors is not None: # adjust the transparency index after optimize - if len(used_palette_colors) < 256: - for i in range(len(used_palette_colors)): - if used_palette_colors[i] == transparency: - transparency = i - transparent_color_exists = True - break - else: - transparent_color_exists = False + for i, palette_color in enumerate(used_palette_colors): + if palette_color == transparency: + transparency = i + transparent_color_exists = True + break + else: + transparent_color_exists = False if "duration" in im.encoderinfo: duration = int(im.encoderinfo["duration"] / 10) @@ -515,22 +517,20 @@ def _save_netpbm(im, fp, filename): import tempfile file = im._dump() - if im.mode != "RGB": - with open(filename, 'wb') as f: - stderr = tempfile.TemporaryFile() - check_call(["ppmtogif", file], stdout=f, stderr=stderr) - else: - with open(filename, 'wb') as f: - + with open(filename, 'wb') as f: + if im.mode != "RGB": + with tempfile.TemporaryFile() as stderr: + check_call(["ppmtogif", file], stdout=f, stderr=stderr) + else: # Pipe ppmquant output into ppmtogif # "ppmquant 256 %s | ppmtogif > %s" % (file, filename) quant_cmd = ["ppmquant", "256", file] togif_cmd = ["ppmtogif"] - stderr = tempfile.TemporaryFile() - quant_proc = Popen(quant_cmd, stdout=PIPE, stderr=stderr) - stderr = tempfile.TemporaryFile() - togif_proc = Popen(togif_cmd, stdin=quant_proc.stdout, stdout=f, - stderr=stderr) + with tempfile.TemporaryFile() as stderr: + quant_proc = Popen(quant_cmd, stdout=PIPE, stderr=stderr) + with tempfile.TemporaryFile() as stderr: + togif_proc = Popen(togif_cmd, stdin=quant_proc.stdout, + stdout=f, stderr=stderr) # Allow ppmquant to receive SIGPIPE if ppmtogif exits quant_proc.stdout.close() @@ -552,9 +552,28 @@ def _save_netpbm(im, fp, filename): # -------------------------------------------------------------------- # GIF utilities -def _get_optimize(im, info): - return im.mode in ("P", "L") and info and info.get("optimize", 0) +# Force optimization so that we can test performance against +# cases where it took lots of memory and time previously. +_FORCE_OPTIMIZE = False +def _get_optimize(im, info): + if im.mode in ("P", "L") and info and info.get("optimize", 0): + # Potentially expensive operation. + + # The palette saves 3 bytes per color not used, but palette + # lengths are restricted to 3*(2**N) bytes. Max saving would + # be 768 -> 6 bytes if we went all the way down to 2 colors. + # * If we're over 128 colors, we can't save any space. + # * If there aren't any holes, it's not worth collapsing. + # * If we have a 'large' image, the palette is in the noise. + + # create the new palette if not every color is used + used_palette_colors = _get_used_palette_colors(im) + if _FORCE_OPTIMIZE or im.mode == 'L' or \ + (len(used_palette_colors) <= 128 and + max(used_palette_colors) > len(used_palette_colors) and + im.width * im.height < 512 * 512): + return used_palette_colors def _get_used_palette_colors(im): used_palette_colors = [] @@ -586,10 +605,6 @@ def _get_header_palette(palette_bytes): palette_bytes += o8(0) * 3 * actual_target_size_diff return palette_bytes -# Force optimization so that we can test performance against -# cases where it took lots of memory and time previously. -_FORCE_OPTIMIZE = False - def _get_palette_bytes(im, palette, info): if im.mode == "P": if palette and isinstance(palette, bytes): @@ -600,81 +615,66 @@ def _get_palette_bytes(im, palette, info): if palette and isinstance(palette, bytes): source_palette = palette[:768] else: - source_palette = bytearray([i//3 for i in range(768)]) + source_palette = bytearray(i//3 for i in range(768)) - used_palette_colors = palette_bytes = None + palette_bytes = None - if _get_optimize(im, info): - used_palette_colors = _get_used_palette_colors(im) + used_palette_colors = _get_optimize(im, info) + if used_palette_colors is not None: + palette_bytes = b"" + new_positions = [0]*256 - # Potentially expensive operation. + # pick only the used colors from the palette + for i, oldPosition in enumerate(used_palette_colors): + palette_bytes += source_palette[oldPosition*3:oldPosition*3+3] + new_positions[oldPosition] = i - # The palette saves 3 bytes per color not used, but palette - # lengths are restricted to 3*(2**N) bytes. Max saving would - # be 768 -> 6 bytes if we went all the way down to 2 colors. - # * If we're over 128 colors, we can't save any space. - # * If there aren't any holes, it's not worth collapsing. - # * If we have a 'large' image, the palette is in the noise. + # replace the palette color id of all pixel with the new id - # create the new palette if not every color is used - if _FORCE_OPTIMIZE or im.mode == 'L' or \ - (len(used_palette_colors) <= 128 and - max(used_palette_colors) > len(used_palette_colors) and - im.width * im.height < 512 * 512): - palette_bytes = b"" - new_positions = [0]*256 + # Palette images are [0..255], mapped through a 1 or 3 + # byte/color map. We need to remap the whole image + # from palette 1 to palette 2. New_positions is + # an array of indexes into palette 1. Palette 2 is + # palette 1 with any holes removed. - # pick only the used colors from the palette - for i, oldPosition in enumerate(used_palette_colors): - palette_bytes += source_palette[oldPosition*3:oldPosition*3+3] - new_positions[oldPosition] = i + # We're going to leverage the convert mechanism to use the + # C code to remap the image from palette 1 to palette 2, + # by forcing the source image into 'L' mode and adding a + # mapping 'L' mode palette, then converting back to 'L' + # sans palette thus converting the image bytes, then + # assigning the optimized RGB palette. - # replace the palette color id of all pixel with the new id + # perf reference, 9500x4000 gif, w/~135 colors + # 14 sec prepatch, 1 sec postpatch with optimization forced. - # Palette images are [0..255], mapped through a 1 or 3 - # byte/color map. We need to remap the whole image - # from palette 1 to palette 2. New_positions is - # an array of indexes into palette 1. Palette 2 is - # palette 1 with any holes removed. + mapping_palette = bytearray(new_positions) - # We're going to leverage the convert mechanism to use the - # C code to remap the image from palette 1 to palette 2, - # by forcing the source image into 'L' mode and adding a - # mapping 'L' mode palette, then converting back to 'L' - # sans palette thus converting the image bytes, then - # assigning the optimized RGB palette. + m_im = im.copy() + m_im.mode = 'P' - # perf reference, 9500x4000 gif, w/~135 colors - # 14 sec prepatch, 1 sec postpatch with optimization forced. + m_im.palette = ImagePalette.ImagePalette("RGB", + palette=mapping_palette*3, + size=768) + #possibly set palette dirty, then + #m_im.putpalette(mapping_palette, 'L') # converts to 'P' + # or just force it. + # UNDONE -- this is part of the general issue with palettes + m_im.im.putpalette(*m_im.palette.getdata()) - mapping_palette = bytearray(new_positions) + m_im = m_im.convert('L') - m_im = im.copy() - m_im.mode = 'P' + # Internally, we require 768 bytes for a palette. + new_palette_bytes = (palette_bytes + + (768 - len(palette_bytes)) * b'\x00') + m_im.putpalette(new_palette_bytes) + m_im.palette = ImagePalette.ImagePalette("RGB", + palette=palette_bytes, + size=len(palette_bytes)) - m_im.palette = ImagePalette.ImagePalette("RGB", - palette=mapping_palette*3, - size=768) - #possibly set palette dirty, then - #m_im.putpalette(mapping_palette, 'L') # converts to 'P' - # or just force it. - # UNDONE -- this is part of the general issue with palettes - m_im.im.putpalette(*m_im.palette.getdata()) - - m_im = m_im.convert('L') - - # Internally, we require 768 bytes for a palette. - new_palette_bytes = (palette_bytes + - (768 - len(palette_bytes)) * b'\x00') - m_im.putpalette(new_palette_bytes) - m_im.palette = ImagePalette.ImagePalette("RGB", - palette=palette_bytes, - size=len(palette_bytes)) + # oh gawd, this is modifying the image in place so I can pass by ref. + # REFACTOR SOONEST + im.frombytes(m_im.tobytes()) - # oh gawd, this is modifying the image in place so I can pass by ref. - # REFACTOR SOONEST - im.frombytes(m_im.tobytes()) - if not palette_bytes: palette_bytes = source_palette @@ -696,7 +696,7 @@ def getheader(im, palette=None, info=None): version = b"89a" break else: - if im.info.get("version") == "89a": + if im.info.get("version") == b"89a": version = b"89a" header = [ diff --git a/PIL/GimpGradientFile.py b/PIL/GimpGradientFile.py index 45af573bb..43cd72649 100644 --- a/PIL/GimpGradientFile.py +++ b/PIL/GimpGradientFile.py @@ -14,7 +14,7 @@ # from math import pi, log, sin, sqrt -from PIL._binary import o8 +from ._binary import o8 # -------------------------------------------------------------------- # Stuff to translate curve segments to palette values (derived from diff --git a/PIL/GimpPaletteFile.py b/PIL/GimpPaletteFile.py index 4bf3ca36a..6eef6a2dd 100644 --- a/PIL/GimpPaletteFile.py +++ b/PIL/GimpPaletteFile.py @@ -15,7 +15,7 @@ # import re -from PIL._binary import o8 +from ._binary import o8 ## @@ -41,7 +41,7 @@ class GimpPaletteFile(object): if not s: break # skip fields and comment lines - if re.match(b"\w+:|#", s): + if re.match(br"\w+:|#", s): continue if len(s) > 100: raise SyntaxError("bad palette file") diff --git a/PIL/GribStubImagePlugin.py b/PIL/GribStubImagePlugin.py index e880e5281..1fbfe61dc 100644 --- a/PIL/GribStubImagePlugin.py +++ b/PIL/GribStubImagePlugin.py @@ -9,7 +9,7 @@ # See the README file for information on usage and redistribution. # -from PIL import Image, ImageFile +from . import Image, ImageFile _handler = None diff --git a/PIL/Hdf5StubImagePlugin.py b/PIL/Hdf5StubImagePlugin.py index dc85084d8..a5d6b1bc1 100644 --- a/PIL/Hdf5StubImagePlugin.py +++ b/PIL/Hdf5StubImagePlugin.py @@ -9,7 +9,7 @@ # See the README file for information on usage and redistribution. # -from PIL import Image, ImageFile +from . import Image, ImageFile _handler = None diff --git a/PIL/IcnsImagePlugin.py b/PIL/IcnsImagePlugin.py index d93e0de04..cb215fe3e 100644 --- a/PIL/IcnsImagePlugin.py +++ b/PIL/IcnsImagePlugin.py @@ -15,7 +15,8 @@ # See the README file for information on usage and redistribution. # -from PIL import Image, ImageFile, PngImagePlugin, _binary +from PIL import Image, ImageFile, PngImagePlugin +from PIL._binary import i8 import io import os import shutil @@ -27,8 +28,6 @@ enable_jpeg2k = hasattr(Image.core, 'jp2klib_version') if enable_jpeg2k: from PIL import Jpeg2KImagePlugin -i8 = _binary.i8 - HEADERSIZE = 8 @@ -330,8 +329,8 @@ def _save(im, fp, filename): from subprocess import Popen, PIPE, CalledProcessError convert_cmd = ["iconutil", "-c", "icns", "-o", filename, iconset] - stderr = tempfile.TemporaryFile() - convert_proc = Popen(convert_cmd, stdout=PIPE, stderr=stderr) + with tempfile.TemporaryFile() as stderr: + convert_proc = Popen(convert_cmd, stdout=PIPE, stderr=stderr) convert_proc.stdout.close() diff --git a/PIL/IcoImagePlugin.py b/PIL/IcoImagePlugin.py index a01aed376..e4db4e766 100644 --- a/PIL/IcoImagePlugin.py +++ b/PIL/IcoImagePlugin.py @@ -25,7 +25,8 @@ import struct from io import BytesIO -from PIL import Image, ImageFile, BmpImagePlugin, PngImagePlugin, _binary +from . import Image, ImageFile, BmpImagePlugin, PngImagePlugin +from ._binary import i8, i16le as i16, i32le as i32 from math import log, ceil __version__ = "0.1" @@ -33,10 +34,6 @@ __version__ = "0.1" # # -------------------------------------------------------------------- -i8 = _binary.i8 -i16 = _binary.i16le -i32 = _binary.i32le - _MAGIC = b"\0\0\1\0" @@ -44,16 +41,19 @@ def _save(im, fp, filename): fp.write(_MAGIC) # (2+2) sizes = im.encoderinfo.get("sizes", [(16, 16), (24, 24), (32, 32), (48, 48), - (64, 64), (128, 128), (255, 255)]) + (64, 64), (128, 128), (256, 256)]) width, height = im.size - filter(lambda x: False if (x[0] > width or x[1] > height or - x[0] > 255 or x[1] > 255) else True, sizes) + sizes = filter(lambda x: False if (x[0] > width or x[1] > height or + x[0] > 256 or x[1] > 256) else True, + sizes) + sizes = list(sizes) fp.write(struct.pack(" 0") + + return True def new(mode, size, color=0): """ @@ -2002,6 +2046,8 @@ def new(mode, size, color=0): :returns: An :py:class:`~PIL.Image.Image` object. """ + _check_size(size) + if color is None: # don't initialize return Image()._new(core.new(mode, size)) @@ -2009,7 +2055,7 @@ def new(mode, size, color=0): if isStringType(color): # css3-style specifier - from PIL import ImageColor + from . import ImageColor color = ImageColor.getcolor(color, mode) return Image()._new(core.fill(mode, size, color)) @@ -2039,6 +2085,8 @@ def frombytes(mode, size, data, decoder_name="raw", *args): :returns: An :py:class:`~PIL.Image.Image` object. """ + _check_size(size) + # may pass tuple instead of argument list if len(args) == 1 and isinstance(args[0], tuple): args = args[0] @@ -2091,6 +2139,8 @@ def frombuffer(mode, size, data, decoder_name="raw", *args): .. versionadded:: 1.1.4 """ + _check_size(size) + # may pass tuple instead of argument list if len(args) == 1 and isinstance(args[0], tuple): args = args[0] @@ -2142,7 +2192,7 @@ def fromarray(obj, mode=None): typekey = (1, 1) + shape[2:], arr['typestr'] mode, rawmode = _fromarray_typemap[typekey] except KeyError: - # print typekey + # print(typekey) raise TypeError("Cannot handle this data type") else: rawmode = mode @@ -2167,7 +2217,7 @@ def fromarray(obj, mode=None): def fromqimage(im): """Creates an image instance from a QImage image""" - from PIL import ImageQt + from . import ImageQt if not ImageQt.qt_is_installed: raise ImportError("Qt bindings are not installed") return ImageQt.fromqimage(im) @@ -2175,7 +2225,7 @@ def fromqimage(im): def fromqpixmap(im): """Creates an image instance from a QPixmap image""" - from PIL import ImageQt + from . import ImageQt if not ImageQt.qt_is_installed: raise ImportError("Qt bindings are not installed") return ImageQt.fromqpixmap(im) @@ -2460,6 +2510,16 @@ def register_extension(id, extension): EXTENSION[extension.lower()] = id.upper() +def registered_extensions(): + """ + Returns a dictionary containing all file extensions belonging + to registered plugins + """ + if not bool(EXTENSION): + init() + return EXTENSION + + # -------------------------------------------------------------------- # Simple display support. User code may override this. @@ -2469,7 +2529,7 @@ def _show(image, **options): def _showxv(image, title=None, **options): - from PIL import ImageShow + from . import ImageShow ImageShow.show(image, title, **options) diff --git a/PIL/ImageChops.py b/PIL/ImageChops.py index ba5350e02..89016730e 100644 --- a/PIL/ImageChops.py +++ b/PIL/ImageChops.py @@ -15,7 +15,7 @@ # See the README file for information on usage and redistribution. # -from PIL import Image +from . import Image def constant(image, value): diff --git a/PIL/ImageColor.py b/PIL/ImageColor.py index 64eebfe9d..1c7bc31d5 100644 --- a/PIL/ImageColor.py +++ b/PIL/ImageColor.py @@ -17,7 +17,7 @@ # See the README file for information on usage and redistribution. # -from PIL import Image +from . import Image import re diff --git a/PIL/ImageDraw.py b/PIL/ImageDraw.py index 5dfb5929d..6b72fcab7 100644 --- a/PIL/ImageDraw.py +++ b/PIL/ImageDraw.py @@ -33,8 +33,8 @@ import numbers import warnings -from PIL import Image, ImageColor -from PIL._util import isStringType +from . import Image, ImageColor +from ._util import isStringType """ A simple 2D drawing interface for PIL images. @@ -105,7 +105,7 @@ class ImageDraw(object): """Get the current default font.""" if not self.font: # FIXME: should add a font repository - from PIL import ImageFont + from . import ImageFont self.font = ImageFont.load_default() return self.font @@ -208,12 +208,12 @@ class ImageDraw(object): def _multiline_check(self, text): """Draw text.""" - split_character = "\n" if isinstance(text, type("")) else b"\n" + split_character = "\n" if isinstance(text, str) else b"\n" return split_character in text def _multiline_split(self, text): - split_character = "\n" if isinstance(text, type("")) else b"\n" + split_character = "\n" if isinstance(text, str) else b"\n" return text.split(split_character) @@ -319,11 +319,11 @@ def getdraw(im=None, hints=None): handler = None if not hints or "nicest" in hints: try: - from PIL import _imagingagg as handler + from . import _imagingagg as handler except ImportError: pass if handler is None: - from PIL import ImageDraw2 as handler + from . import ImageDraw2 as handler if im: im = handler.Draw(im) return im, handler diff --git a/PIL/ImageDraw2.py b/PIL/ImageDraw2.py index 62ee11630..a1763350d 100644 --- a/PIL/ImageDraw2.py +++ b/PIL/ImageDraw2.py @@ -16,7 +16,7 @@ # See the README file for information on usage and redistribution. # -from PIL import Image, ImageColor, ImageDraw, ImageFont, ImagePath +from . import Image, ImageColor, ImageDraw, ImageFont, ImagePath class Pen(object): diff --git a/PIL/ImageEnhance.py b/PIL/ImageEnhance.py index 56b5c0199..b38f406a3 100644 --- a/PIL/ImageEnhance.py +++ b/PIL/ImageEnhance.py @@ -18,7 +18,7 @@ # See the README file for information on usage and redistribution. # -from PIL import Image, ImageFilter, ImageStat +from . import Image, ImageFilter, ImageStat class _Enhance(object): diff --git a/PIL/ImageFile.py b/PIL/ImageFile.py index a66452478..8f3ee524c 100644 --- a/PIL/ImageFile.py +++ b/PIL/ImageFile.py @@ -27,8 +27,8 @@ # See the README file for information on usage and redistribution. # -from PIL import Image -from PIL._util import isPath +from . import Image +from ._util import isPath import io import os import sys @@ -150,15 +150,16 @@ class ImageFile(Image.Image): if use_mmap: # try memory mapping - d, e, o, a = self.tile[0] - if d == "raw" and a[0] == self.mode and a[0] in Image._MAPMODES: + 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: try: if hasattr(Image.core, "map"): - # use built-in mapper + # use built-in mapper WIN32 only self.map = Image.core.map(self.filename) - self.map.seek(o) + self.map.seek(offset) self.im = self.map.readimage( - self.mode, self.size, a[1], a[2] + self.mode, self.size, args[1], args[2] ) else: # use mmap, if possible @@ -167,7 +168,7 @@ class ImageFile(Image.Image): size = os.path.getsize(self.filename) self.map = mmap.mmap(fp.fileno(), size, access=mmap.ACCESS_READ) self.im = Image.core.map_buffer( - self.map, self.size, d, e, o, a + self.map, self.size, decoder_name, extents, offset, args ) readonly = 1 # After trashing self.im, we might need to reload the palette data. @@ -210,7 +211,7 @@ class ImageFile(Image.Image): else: raise IOError("image file is truncated") - if not s and not decoder.handles_eof: # truncated jpeg + if not s: # truncated jpeg self.tile = [] # JpegDecode needs to clean things up here either way @@ -242,12 +243,6 @@ class ImageFile(Image.Image): # still raised if decoder fails to return anything raise_ioerror(err_code) - # post processing - if hasattr(self, "tile_post_rotate"): - # FIXME: This is a hack to handle rotated PCD's - self.im = self.im.rotate(self.tile_post_rotate) - self.size = self.im.size - self.load_end() return Image.Image.load(self) diff --git a/PIL/ImageFont.py b/PIL/ImageFont.py index 6bd48dd6e..c74d00138 100644 --- a/PIL/ImageFont.py +++ b/PIL/ImageFont.py @@ -25,8 +25,8 @@ # See the README file for information on usage and redistribution. # -from PIL import Image -from PIL._util import isDirectory, isPath +from . import Image +from ._util import isDirectory, isPath import os import sys @@ -37,7 +37,7 @@ class _imagingft_not_installed(object): raise ImportError("The _imagingft C module is not installed") try: - from PIL import _imagingft as core + from . import _imagingft as core except ImportError: core = _imagingft_not_installed() @@ -62,23 +62,22 @@ class ImageFont(object): def _load_pilfont(self, filename): - fp = open(filename, "rb") - - for ext in (".png", ".gif", ".pbm"): - try: - fullname = os.path.splitext(filename)[0] + ext - image = Image.open(fullname) - except: - pass + with open(filename, "rb") as fp: + for ext in (".png", ".gif", ".pbm"): + try: + fullname = os.path.splitext(filename)[0] + ext + image = Image.open(fullname) + except: + pass + else: + if image and image.mode in ("1", "L"): + break else: - if image and image.mode in ("1", "L"): - break - else: - raise IOError("cannot find glyph data file") + raise IOError("cannot find glyph data file") - self.file = fullname + self.file = fullname - return self._load_pilfont_data(fp, image) + return self._load_pilfont_data(fp, image) def _load_pilfont_data(self, file, image): diff --git a/PIL/ImageGrab.py b/PIL/ImageGrab.py index 55fb014c4..938d0e994 100644 --- a/PIL/ImageGrab.py +++ b/PIL/ImageGrab.py @@ -15,7 +15,7 @@ # See the README file for information on usage and redistribution. # -from PIL import Image +from . import Image import sys if sys.platform not in ["win32", "darwin"]: @@ -41,7 +41,7 @@ def grab(bbox=None): size, data = grabber() im = Image.frombytes( "RGB", size, data, - # RGB, 32-bit line padding, origo in lower left corner + # RGB, 32-bit line padding, origin lower left corner "raw", "BGR", (size[0]*3 + 3) & -4, -1 ) if bbox: @@ -75,7 +75,7 @@ def grabclipboard(): debug = 0 # temporary interface data = Image.core.grabclipboard(debug) if isinstance(data, bytes): - from PIL import BmpImagePlugin + from . import BmpImagePlugin import io return BmpImagePlugin.DibImageFile(io.BytesIO(data)) return data diff --git a/PIL/ImageMath.py b/PIL/ImageMath.py index 897f0aeb1..2ccd1891b 100644 --- a/PIL/ImageMath.py +++ b/PIL/ImageMath.py @@ -15,8 +15,7 @@ # See the README file for information on usage and redistribution. # -from PIL import Image -from PIL import _imagingmath +from . import Image, _imagingmath try: import builtins diff --git a/PIL/ImageMode.py b/PIL/ImageMode.py index f78a8df90..b227f2127 100644 --- a/PIL/ImageMode.py +++ b/PIL/ImageMode.py @@ -14,7 +14,7 @@ # # mode descriptor cache -_modes = {} +_modes = None class ModeDescriptor(object): @@ -32,19 +32,24 @@ class ModeDescriptor(object): def getmode(mode): """Gets a mode descriptor for the given mode.""" + global _modes if not _modes: # initialize mode cache - from PIL import Image + + from . import Image + modes = {} # core modes for m, (basemode, basetype, bands) in Image._MODEINFO.items(): - _modes[m] = ModeDescriptor(m, bands, basemode, basetype) + modes[m] = ModeDescriptor(m, bands, basemode, basetype) # extra experimental modes - _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") + 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") # mapping modes - _modes["I;16"] = ModeDescriptor("I;16", "I", "L", "L") - _modes["I;16L"] = ModeDescriptor("I;16L", "I", "L", "L") - _modes["I;16B"] = ModeDescriptor("I;16B", "I", "L", "L") + modes["I;16"] = ModeDescriptor("I;16", "I", "L", "L") + modes["I;16L"] = ModeDescriptor("I;16L", "I", "L", "L") + modes["I;16B"] = ModeDescriptor("I;16B", "I", "L", "L") + # set global mode cache atomically + _modes = modes return _modes[mode] diff --git a/PIL/ImageMorph.py b/PIL/ImageMorph.py index 4847594f6..8382f7ce0 100644 --- a/PIL/ImageMorph.py +++ b/PIL/ImageMorph.py @@ -5,8 +5,9 @@ # # Copyright (c) 2014 Dov Grobgeld -from PIL import Image -from PIL import _imagingmorph +from __future__ import print_function + +from . import Image, _imagingmorph import re LUT_SIZE = 1 << 9 @@ -78,7 +79,7 @@ class LutBuilder(object): def build_default_lut(self): symbols = [0, 1] m = 1 << 4 # pos of current pixel - self.lut = bytearray([symbols[(i & m) > 0] for i in range(LUT_SIZE)]) + self.lut = bytearray(symbols[(i & m) > 0] for i in range(LUT_SIZE)) def get_lut(self): return self.lut @@ -88,7 +89,7 @@ class LutBuilder(object): string permuted according to the permutation list. """ assert(len(permutation) == 9) - return ''.join([pattern[p] for p in permutation]) + return ''.join(pattern[p] for p in permutation) def _pattern_permute(self, basic_pattern, options, basic_result): """pattern_permute takes a basic pattern and its result and clones @@ -152,14 +153,14 @@ class LutBuilder(object): # # Debugging # for p,r in patterns: -# print p,r -# print '--' +# print(p,r) +# print('--') # compile the patterns into regular expressions for speed - for i in range(len(patterns)): - p = patterns[i][0].replace('.', 'X').replace('X', '[01]') + for i, pattern in enumerate(patterns): + p = pattern[0].replace('.', 'X').replace('X', '[01]') p = re.compile(p) - patterns[i] = (p, patterns[i][1]) + patterns[i] = (p, pattern[1]) # Step through table and find patterns that match. # Note that all the patterns are searched. The last one diff --git a/PIL/ImageOps.py b/PIL/ImageOps.py index 8580ec5fb..3681109c1 100644 --- a/PIL/ImageOps.py +++ b/PIL/ImageOps.py @@ -17,8 +17,8 @@ # See the README file for information on usage and redistribution. # -from PIL import Image -from PIL._util import isStringType +from . import Image +from ._util import isStringType import operator import functools @@ -39,7 +39,7 @@ def _border(border): def _color(color, mode): if isStringType(color): - from PIL import ImageColor + from . import ImageColor color = ImageColor.getcolor(color, mode) return color @@ -206,7 +206,8 @@ def deform(image, deformer, resample=Image.BILINEAR): :param image: The image to deform. :param deformer: A deformer object. Any object that implements a **getmesh** method can be used. - :param resample: What resampling filter to use. + :param resample: An optional resampling filter. Same values possible as + in the PIL.Image.transform function. :return: An image. """ return image.transform( diff --git a/PIL/ImagePalette.py b/PIL/ImagePalette.py index 3b60068bc..cecc64583 100644 --- a/PIL/ImagePalette.py +++ b/PIL/ImagePalette.py @@ -17,10 +17,7 @@ # import array -from PIL import ImageColor -from PIL import GimpPaletteFile -from PIL import GimpGradientFile -from PIL import PaletteFile +from . import ImageColor, GimpPaletteFile, GimpGradientFile, PaletteFile class ImagePalette(object): @@ -197,23 +194,23 @@ def load(filename): # FIXME: supports GIMP gradients only - fp = open(filename, "rb") + with open(filename, "rb") as fp: - for paletteHandler in [ - GimpPaletteFile.GimpPaletteFile, - GimpGradientFile.GimpGradientFile, - PaletteFile.PaletteFile - ]: - try: - fp.seek(0) - lut = paletteHandler(fp).getpalette() - if lut: - break - except (SyntaxError, ValueError): - # import traceback - # traceback.print_exc() - pass - else: - raise IOError("cannot load palette") + for paletteHandler in [ + GimpPaletteFile.GimpPaletteFile, + GimpGradientFile.GimpGradientFile, + PaletteFile.PaletteFile + ]: + try: + fp.seek(0) + lut = paletteHandler(fp).getpalette() + if lut: + break + except (SyntaxError, ValueError): + # import traceback + # traceback.print_exc() + pass + else: + raise IOError("cannot load palette") return lut # data, rawmode diff --git a/PIL/ImagePath.py b/PIL/ImagePath.py index 3abfba031..1543508e4 100644 --- a/PIL/ImagePath.py +++ b/PIL/ImagePath.py @@ -14,7 +14,7 @@ # See the README file for information on usage and redistribution. # -from PIL import Image +from . import Image # the Python class below is overridden by the C implementation. diff --git a/PIL/ImageQt.py b/PIL/ImageQt.py index 3f7ae2518..36b4e1ebc 100644 --- a/PIL/ImageQt.py +++ b/PIL/ImageQt.py @@ -16,8 +16,8 @@ # See the README file for information on usage and redistribution. # -from PIL import Image -from PIL._util import isPath +from . import Image +from ._util import isPath from io import BytesIO qt_is_installed = True @@ -157,7 +157,6 @@ def _toqclass_helper(im): else: raise ValueError("unsupported image mode %r" % im.mode) - # must keep a reference, or Qt will crash! __data = data or align8to32(im.tobytes(), im.size[0], im.mode) return { 'data': __data, 'im': im, 'format': format, 'colortable': colortable @@ -175,8 +174,13 @@ if qt_is_installed: string or a PyQt string object). """ im_data = _toqclass_helper(im) + # must keep a reference, or Qt will crash! + # All QImage constructors that take data operate on an existing + # buffer, so this buffer has to hang on for the life of the image. + # Fixes https://github.com/python-pillow/Pillow/issues/1370 + self.__data = im_data['data'] QImage.__init__(self, - im_data['data'], im_data['im'].size[0], + self.__data, im_data['im'].size[0], im_data['im'].size[1], im_data['format']) if im_data['colortable']: self.setColorTable(im_data['colortable']) diff --git a/PIL/ImageTk.py b/PIL/ImageTk.py index d3957b6d1..8c6d1a9c2 100644 --- a/PIL/ImageTk.py +++ b/PIL/ImageTk.py @@ -32,7 +32,14 @@ except ImportError: tkinter = Tkinter del Tkinter -from PIL import Image +# required for pypy, which always has cffi installed +try: + from cffi import FFI + ffi = FFI() +except ImportError: + pass + +from . import Image from io import BytesIO @@ -182,9 +189,15 @@ class PhotoImage(object): except tkinter.TclError: # activate Tkinter hook try: - from PIL import _imagingtk + from . import _imagingtk try: - _imagingtk.tkinit(tk.interpaddr(), 1) + if hasattr(tk, 'interp'): + # Pypy is using a ffi cdata element + # (Pdb) self.tk.interp + # + _imagingtk.tkinit(int(ffi.cast("uintptr_t", tk.interp)), 1) + else: + _imagingtk.tkinit(tk.interpaddr(), 1) except AttributeError: _imagingtk.tkinit(id(tk), 0) tk.call("PyImagingPhoto", self.__photo, block.id) @@ -264,6 +277,8 @@ class BitmapImage(object): def getimage(photo): + """ This function is unimplemented """ + """Copies the contents of a PhotoImage to a PIL image memory.""" photo.tk.call("PyImagingPhotoGet", photo) diff --git a/PIL/ImageTransform.py b/PIL/ImageTransform.py index 92cc2c7e0..dcfcdfae8 100644 --- a/PIL/ImageTransform.py +++ b/PIL/ImageTransform.py @@ -13,7 +13,7 @@ # See the README file for information on usage and redistribution. # -from PIL import Image +from . import Image class Transform(Image.ImageTransformHandler): diff --git a/PIL/ImageWin.py b/PIL/ImageWin.py index 514fc4ff8..cc4dced97 100644 --- a/PIL/ImageWin.py +++ b/PIL/ImageWin.py @@ -17,7 +17,7 @@ # See the README file for information on usage and redistribution. # -from PIL import Image +from . import Image class HDC(object): diff --git a/PIL/ImtImagePlugin.py b/PIL/ImtImagePlugin.py index 63e892483..05e8cd31a 100644 --- a/PIL/ImtImagePlugin.py +++ b/PIL/ImtImagePlugin.py @@ -17,7 +17,7 @@ import re -from PIL import Image, ImageFile +from . import Image, ImageFile __version__ = "0.2" @@ -69,7 +69,7 @@ class ImtImageFile(ImageFile.ImageFile): s = s + self.fp.readline() if len(s) == 1 or len(s) > 100: break - if s[0] == b"*": + if s[0] == ord(b"*"): continue # comment m = field.match(s) diff --git a/PIL/IptcImagePlugin.py b/PIL/IptcImagePlugin.py index ed3bf81b1..8941643bb 100644 --- a/PIL/IptcImagePlugin.py +++ b/PIL/IptcImagePlugin.py @@ -17,17 +17,13 @@ from __future__ import print_function -from PIL import Image, ImageFile, _binary +from . import Image, ImageFile +from ._binary import i8, i16be as i16, i32be as i32, o8 import os import tempfile __version__ = "0.3" -i8 = _binary.i8 -i16 = _binary.i16be -i32 = _binary.i32be -o8 = _binary.o8 - COMPRESSION = { 1: "raw", 5: "jpeg" @@ -107,7 +103,7 @@ class IptcImageFile(ImageFile.ImageFile): else: self.info[tag] = tagdata - # print tag, self.info[tag] + # print(tag, self.info[tag]) # mode layers = i8(self.info[(3, 60)][0]) @@ -168,14 +164,9 @@ class IptcImageFile(ImageFile.ImageFile): o.close() try: - try: - # fast - self.im = Image.core.open_ppm(outfile) - except: - # slightly slower - im = Image.open(outfile) - im.load() - self.im = im.im + _im = Image.open(outfile) + _im.load() + self.im = _im.im finally: try: os.unlink(outfile) @@ -196,7 +187,7 @@ def getiptcinfo(im): :returns: A dictionary containing IPTC information, or None if no IPTC information block was found. """ - from PIL import TiffImagePlugin, JpegImagePlugin + from . import TiffImagePlugin, JpegImagePlugin import io data = None diff --git a/PIL/Jpeg2KImagePlugin.py b/PIL/Jpeg2KImagePlugin.py index 66de34bfa..101f55f76 100644 --- a/PIL/Jpeg2KImagePlugin.py +++ b/PIL/Jpeg2KImagePlugin.py @@ -12,7 +12,7 @@ # # See the README file for information on usage and redistribution. # -from PIL import Image, ImageFile +from . import Image, ImageFile import struct import os import io diff --git a/PIL/JpegImagePlugin.py b/PIL/JpegImagePlugin.py index e216fa06d..f01885b60 100644 --- a/PIL/JpegImagePlugin.py +++ b/PIL/JpegImagePlugin.py @@ -32,19 +32,16 @@ # See the README file for information on usage and redistribution. # +from __future__ import print_function + import array import struct import io import warnings -from struct import unpack_from -from PIL import Image, ImageFile, TiffImagePlugin, _binary -from PIL.JpegPresets import presets -from PIL._util import isStringType - -i8 = _binary.i8 -o8 = _binary.o8 -i16 = _binary.i16be -i32 = _binary.i32be +from . import Image, ImageFile, TiffImagePlugin +from ._binary import i8, o8, i16be as i16, i32be as i32 +from .JpegPresets import presets +from ._util import isStringType __version__ = "0.6" @@ -316,7 +313,7 @@ class JpegImageFile(ImageFile.ImageFile): if i in MARKER: name, description, handler = MARKER[i] - # print hex(i), name, description + # print(hex(i), name, description) if handler is not None: handler(self, i) if i == 0xFFDA: # start of scan @@ -341,6 +338,10 @@ class JpegImageFile(ImageFile.ImageFile): if len(self.tile) != 1: return + # Protect from second call + if self.decoderconfig: + return + d, e, o, a = self.tile[0] scale = 0 @@ -349,7 +350,7 @@ class JpegImageFile(ImageFile.ImageFile): a = mode, "" if size: - scale = max(self.size[0] // size[0], self.size[1] // size[1]) + scale = min(self.size[0] // size[0], self.size[1] // size[1]) for s in [8, 4, 2, 1]: if scale >= s: break @@ -377,7 +378,9 @@ class JpegImageFile(ImageFile.ImageFile): raise ValueError("Invalid Filename") try: - self.im = Image.core.open_ppm(path) + _im = Image.open(path) + _im.load() + self.im = _im.im finally: try: os.unlink(path) @@ -406,7 +409,7 @@ def _fixup_dict(src_dict): except: pass return value - return dict([(k, _fixup(v)) for k, v in src_dict.items()]) + return {k: _fixup(v) for k, v in src_dict.items()} def _getexif(self): @@ -485,8 +488,8 @@ def _getmp(self): try: rawmpentries = mp[0xB002] for entrynum in range(0, quant): - unpackedentry = unpack_from( - '{0}LLLHH'.format(endianness), rawmpentries, entrynum * 16) + unpackedentry = struct.unpack_from( + '{}LLLHH'.format(endianness), rawmpentries, entrynum * 16) labels = ('Attribute', 'Size', 'DataOffset', 'EntryNo1', 'EntryNo2') mpentry = dict(zip(labels, unpackedentry)) @@ -710,8 +713,11 @@ def _save(im, fp, filename): # https://github.com/matthewwithanm/django-imagekit/issues/50 bufsize = 0 if optimize or progressive: + # CMYK can be bigger + if im.mode == 'CMYK': + bufsize = 4 * im.size[0] * im.size[1] # keep sets quality to 0, but the actual value may be high. - if quality >= 95 or quality == 0: + elif quality >= 95 or quality == 0: bufsize = 2 * im.size[0] * im.size[1] else: bufsize = im.size[0] * im.size[1] diff --git a/PIL/McIdasImagePlugin.py b/PIL/McIdasImagePlugin.py index b75360353..08eeec39f 100644 --- a/PIL/McIdasImagePlugin.py +++ b/PIL/McIdasImagePlugin.py @@ -17,7 +17,7 @@ # import struct -from PIL import Image, ImageFile +from . import Image, ImageFile __version__ = "0.2" diff --git a/PIL/MicImagePlugin.py b/PIL/MicImagePlugin.py index 3c912442b..a70838b07 100644 --- a/PIL/MicImagePlugin.py +++ b/PIL/MicImagePlugin.py @@ -17,8 +17,9 @@ # -from PIL import Image, TiffImagePlugin -from PIL.OleFileIO import MAGIC, OleFileIO +from . import Image, TiffImagePlugin + +import olefile __version__ = "0.1" @@ -28,7 +29,7 @@ __version__ = "0.1" def _accept(prefix): - return prefix[:8] == MAGIC + return prefix[:8] == olefile.MAGIC ## @@ -45,7 +46,7 @@ class MicImageFile(TiffImagePlugin.TiffImageFile): # to be a Microsoft Image Composer file try: - self.ole = OleFileIO(self.fp) + self.ole = olefile.OleFileIO(self.fp) except IOError: raise SyntaxError("not an MIC file; invalid OLE file") diff --git a/PIL/MpegImagePlugin.py b/PIL/MpegImagePlugin.py index 6671b8691..bdc5e3689 100644 --- a/PIL/MpegImagePlugin.py +++ b/PIL/MpegImagePlugin.py @@ -14,8 +14,8 @@ # -from PIL import Image, ImageFile -from PIL._binary import i8 +from . import Image, ImageFile +from ._binary import i8 __version__ = "0.1" diff --git a/PIL/MpoImagePlugin.py b/PIL/MpoImagePlugin.py index 1d26021d8..9341c530c 100644 --- a/PIL/MpoImagePlugin.py +++ b/PIL/MpoImagePlugin.py @@ -18,7 +18,7 @@ # See the README file for information on usage and redistribution. # -from PIL import Image, JpegImagePlugin +from . import Image, JpegImagePlugin __version__ = "0.1" diff --git a/PIL/MspImagePlugin.py b/PIL/MspImagePlugin.py index 85f8e764b..b60a21d69 100644 --- a/PIL/MspImagePlugin.py +++ b/PIL/MspImagePlugin.py @@ -17,7 +17,8 @@ # -from PIL import Image, ImageFile, _binary +from . import Image, ImageFile +from ._binary import i16le as i16, o16le as o16 __version__ = "0.1" @@ -25,8 +26,6 @@ __version__ = "0.1" # # read MSP files -i16 = _binary.i16le - def _accept(prefix): return prefix[:4] in [b"DanM", b"LinS"] @@ -66,8 +65,6 @@ class MspImageFile(ImageFile.ImageFile): # # write MSP files (uncompressed only) -o16 = _binary.o16le - def _save(im, fp, filename): diff --git a/PIL/OleFileIO-README.md b/PIL/OleFileIO-README.md deleted file mode 100644 index 35e028265..000000000 --- a/PIL/OleFileIO-README.md +++ /dev/null @@ -1,180 +0,0 @@ -olefile (formerly OleFileIO_PL) -=============================== - -[olefile](http://www.decalage.info/olefile) is a Python package to parse, read and write -[Microsoft OLE2 files](http://en.wikipedia.org/wiki/Compound_File_Binary_Format) -(also called Structured Storage, Compound File Binary Format or Compound Document File Format), -such as Microsoft Office 97-2003 documents, vbaProject.bin in MS Office 2007+ files, Image Composer -and FlashPix files, Outlook messages, StickyNotes, several Microscopy file formats, McAfee antivirus quarantine files, -etc. - - -**Quick links:** [Home page](http://www.decalage.info/olefile) - -[Download/Install](https://bitbucket.org/decalage/olefileio_pl/wiki/Install) - -[Documentation](https://bitbucket.org/decalage/olefileio_pl/wiki) - -[Report Issues/Suggestions/Questions](https://bitbucket.org/decalage/olefileio_pl/issues?status=new&status=open) - -[Contact the author](http://decalage.info/contact) - -[Repository](https://bitbucket.org/decalage/olefileio_pl) - -[Updates on Twitter](https://twitter.com/decalage2) - - -News ----- - -Follow all updates and news on Twitter: - -- **2015-01-25 v0.42**: improved handling of special characters in stream/storage names on Python 2.x (using UTF-8 - instead of Latin-1), fixed bug in listdir with empty storages. -- 2014-11-25 v0.41: OleFileIO.open and isOleFile now support OLE files stored in byte strings, fixed installer for - python 3, added support for Jython (Niko Ehrenfeuchter) -- 2014-10-01 v0.40: renamed OleFileIO_PL to olefile, added initial write support for streams >4K, updated doc and - license, improved the setup script. -- 2014-07-27 v0.31: fixed support for large files with 4K sectors, thanks to Niko Ehrenfeuchter, Martijn Berger and - Dave Jones. Added test scripts from Pillow (by hugovk). Fixed setup for Python 3 (Martin Panter) -- 2014-02-04 v0.30: now compatible with Python 3.x, thanks to Martin Panter who did most of the hard work. -- 2013-07-24 v0.26: added methods to parse stream/storage timestamps, improved listdir to include storages, fixed - parsing of direntry timestamps -- 2013-05-27 v0.25: improved metadata extraction, properties parsing and exception handling, fixed - [issue #12](https://bitbucket.org/decalage/olefileio_pl/issue/12/error-when-converting-timestamps-in-ole) -- 2013-05-07 v0.24: new features to extract metadata (get\_metadata method and OleMetadata class), improved - getproperties to convert timestamps to Python datetime -- 2012-10-09: published [python-oletools](http://www.decalage.info/python/oletools), a package of analysis tools based - on OleFileIO_PL -- 2012-09-11 v0.23: added support for file-like objects, fixed [issue #8](https://bitbucket.org/decalage/olefileio_pl/issue/8/bug-with-file-object) -- 2012-02-17 v0.22: fixed issues #7 (bug in getproperties) and #2 (added close method) -- 2011-10-20: code hosted on bitbucket to ease contributions and bug tracking -- 2010-01-24 v0.21: fixed support for big-endian CPUs, such as PowerPC Macs. -- 2009-12-11 v0.20: small bugfix in OleFileIO.open when filename is not plain str. -- 2009-12-10 v0.19: fixed support for 64 bits platforms (thanks to Ben G. and Martijn for reporting the bug) -- see changelog in source code for more info. - -Download/Install ----------------- - -If you have pip or setuptools installed (pip is included in Python 2.7.9+), you may simply run **pip install olefile** -or **easy_install olefile** for the first installation. - -To update olefile, run **pip install -U olefile**. - -Otherwise, see https://bitbucket.org/decalage/olefileio_pl/wiki/Install - -Features --------- - -- Parse, read and write any OLE file such as Microsoft Office 97-2003 legacy document formats (Word .doc, Excel .xls, - PowerPoint .ppt, Visio .vsd, Project .mpp), Image Composer and FlashPix files, Outlook messages, StickyNotes, - Zeiss AxioVision ZVI files, Olympus FluoView OIB files, etc -- List all the streams and storages contained in an OLE file -- Open streams as files -- Parse and read property streams, containing metadata of the file -- Portable, pure Python module, no dependency - -olefile can be used as an independent package or with PIL/Pillow. - -olefile is mostly meant for developers. If you are looking for tools to analyze OLE files or to extract data (especially -for security purposes such as malware analysis and forensics), then please also check my -[python-oletools](http://www.decalage.info/python/oletools), which are built upon olefile and provide a higher-level interface. - - -History -------- - -olefile is based on the OleFileIO module from [PIL](http://www.pythonware.com/products/pil/index.htm), the excellent -Python Imaging Library, created and maintained by Fredrik Lundh. The olefile API is still compatible with PIL, but -since 2005 I have improved the internal implementation significantly, with new features, bugfixes and a more robust -design. From 2005 to 2014 the project was called OleFileIO_PL, and in 2014 I changed its name to olefile to celebrate -its 9 years and its new write features. - -As far as I know, olefile is the most complete and robust Python implementation to read MS OLE2 files, portable on -several operating systems. (please tell me if you know other similar Python modules) - -Since 2014 olefile/OleFileIO_PL has been integrated into [Pillow](http://python-pillow.org), the friendly fork -of PIL. olefile will continue to be improved as a separate project, and new versions will be merged into Pillow -regularly. - - -Main improvements over the original version of OleFileIO in PIL: ----------------------------------------------------------------- - -- Compatible with Python 3.x and 2.6+ -- Many bug fixes -- Support for files larger than 6.8MB -- Support for 64 bits platforms and big-endian CPUs -- Robust: many checks to detect malformed files -- Runtime option to choose if malformed files should be parsed or raise exceptions -- Improved API -- Metadata extraction, stream/storage timestamps (e.g. for document forensics) -- Can open file-like objects -- Added setup.py and install.bat to ease installation -- More convenient slash-based syntax for stream paths -- Write features - -Documentation -------------- - -Please see the [online documentation](https://bitbucket.org/decalage/olefileio_pl/wiki) for more information, -especially the [OLE overview](https://bitbucket.org/decalage/olefileio_pl/wiki/OLE_Overview) and the -[API page](https://bitbucket.org/decalage/olefileio_pl/wiki/API) which describe how to use olefile in Python applications. -A copy of the same documentation is also provided in the doc subfolder of the olefile package. - - -## Real-life examples ## - -A real-life example: [using OleFileIO_PL for malware analysis and forensics](http://blog.gregback.net/2011/03/using-remnux-for-forensic-puzzle-6/). - -See also [this paper](https://computer-forensics.sans.org/community/papers/gcfa/grow-forensic-tools-taxonomy-python-libraries-helpful-forensic-analysis_6879) about python tools for forensics, which features olefile. - - -License -------- - -olefile (formerly OleFileIO_PL) is copyright (c) 2005-2015 Philippe Lagadec -([http://www.decalage.info](http://www.decalage.info)) - -All rights reserved. - -Redistribution and use in source and binary forms, with or without modification, -are permitted provided that the following conditions are met: - - * Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE -FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - - ----------- - -olefile is based on source code from the OleFileIO module of the Python Imaging Library (PIL) published by Fredrik -Lundh under the following license: - -The Python Imaging Library (PIL) is - - Copyright © 1997-2011 by Secret Labs AB - Copyright © 1995-2011 by Fredrik Lundh - -By obtaining, using, and/or copying this software and/or its associated documentation, you agree that you have read, -understood, and will comply with the following terms and conditions: - -Permission to use, copy, modify, and distribute this software and its associated documentation for any purpose and -without fee is hereby granted, provided that the above copyright notice appears in all copies, and that both that -copyright notice and this permission notice appear in supporting documentation, and that the name of Secret Labs AB or -the author not be used in advertising or publicity pertaining to distribution of the software without specific, written -prior permission. - -SECRET LABS AB AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES -OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL SECRET LABS AB OR THE AUTHOR BE LIABLE FOR ANY SPECIAL, INDIRECT OR -CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF -CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS -SOFTWARE. diff --git a/PIL/OleFileIO.py b/PIL/OleFileIO.py old mode 100755 new mode 100644 index 1998e3c10..2d6aeb85c --- a/PIL/OleFileIO.py +++ b/PIL/OleFileIO.py @@ -1,2305 +1,12 @@ -#!/usr/bin/env python +import warnings -# olefile (formerly OleFileIO_PL) version 0.42 2015-01-25 -# -# Module to read/write Microsoft OLE2 files (also called Structured Storage or -# Microsoft Compound Document File Format), such as Microsoft Office 97-2003 -# documents, Image Composer and FlashPix files, Outlook messages, ... -# This version is compatible with Python 2.6+ and 3.x -# -# Project website: http://www.decalage.info/olefile -# -# olefile is copyright (c) 2005-2015 Philippe Lagadec (http://www.decalage.info) -# -# olefile is based on the OleFileIO module from the PIL library v1.1.6 -# See: http://www.pythonware.com/products/pil/index.htm -# -# The Python Imaging Library (PIL) is -# Copyright (c) 1997-2005 by Secret Labs AB -# Copyright (c) 1995-2005 by Fredrik Lundh -# -# See source code and LICENSE.txt for information on usage and redistribution. +warnings.warn( + 'PIL.OleFileIO is deprecated. Use the olefile Python package ' + 'instead. This module will be removed in a future version.', + DeprecationWarning +) - -# Since OleFileIO_PL v0.30, only Python 2.6+ and 3.x is supported -# This import enables print() as a function rather than a keyword -# (main requirement to be compatible with Python 3.x) -# The comment on the line below should be printed on Python 2.5 or older: -from __future__ import print_function # This version of olefile requires Python 2.6+ or 3.x. - - -__author__ = "Philippe Lagadec" -__date__ = "2015-01-25" -__version__ = '0.42b' - -#--- LICENSE ------------------------------------------------------------------ - -# olefile (formerly OleFileIO_PL) is copyright (c) 2005-2015 Philippe Lagadec -# (http://www.decalage.info) -# -# All rights reserved. -# -# Redistribution and use in source and binary forms, with or without modification, -# are permitted provided that the following conditions are met: -# -# * Redistributions of source code must retain the above copyright notice, this -# list of conditions and the following disclaimer. -# * Redistributions in binary form must reproduce the above copyright notice, -# this list of conditions and the following disclaimer in the documentation -# and/or other materials provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE -# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -# ---------- -# PIL License: -# -# olefile is based on source code from the OleFileIO module of the Python -# Imaging Library (PIL) published by Fredrik Lundh under the following license: - -# The Python Imaging Library (PIL) is -# Copyright (c) 1997-2005 by Secret Labs AB -# Copyright (c) 1995-2005 by Fredrik Lundh -# -# By obtaining, using, and/or copying this software and/or its associated -# documentation, you agree that you have read, understood, and will comply with -# the following terms and conditions: -# -# Permission to use, copy, modify, and distribute this software and its -# associated documentation for any purpose and without fee is hereby granted, -# provided that the above copyright notice appears in all copies, and that both -# that copyright notice and this permission notice appear in supporting -# documentation, and that the name of Secret Labs AB or the author(s) not be used -# in advertising or publicity pertaining to distribution of the software -# without specific, written prior permission. -# -# SECRET LABS AB AND THE AUTHORS DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS -# SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. -# IN NO EVENT SHALL SECRET LABS AB OR THE AUTHORS BE LIABLE FOR ANY SPECIAL, -# INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM -# LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR -# OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR -# PERFORMANCE OF THIS SOFTWARE. - -#----------------------------------------------------------------------------- -# CHANGELOG: (only olefile/OleFileIO_PL changes compared to PIL 1.1.6) -# 2005-05-11 v0.10 PL: - a few fixes for Python 2.4 compatibility -# (all changes flagged with [PL]) -# 2006-02-22 v0.11 PL: - a few fixes for some Office 2003 documents which raise -# exceptions in _OleStream.__init__() -# 2006-06-09 v0.12 PL: - fixes for files above 6.8MB (DIFAT in loadfat) -# - added some constants -# - added header values checks -# - added some docstrings -# - getsect: bugfix in case sectors >512 bytes -# - getsect: added conformity checks -# - DEBUG_MODE constant to activate debug display -# 2007-09-04 v0.13 PL: - improved/translated (lots of) comments -# - updated license -# - converted tabs to 4 spaces -# 2007-11-19 v0.14 PL: - added OleFileIO._raise_defect() to adapt sensitivity -# - improved _unicode() to use Python 2.x unicode support -# - fixed bug in _OleDirectoryEntry -# 2007-11-25 v0.15 PL: - added safety checks to detect FAT loops -# - fixed _OleStream which didn't check stream size -# - added/improved many docstrings and comments -# - moved helper functions _unicode and _clsid out of -# OleFileIO class -# - improved OleFileIO._find() to add Unix path syntax -# - OleFileIO._find() is now case-insensitive -# - added get_type() and get_rootentry_name() -# - rewritten loaddirectory and _OleDirectoryEntry -# 2007-11-27 v0.16 PL: - added _OleDirectoryEntry.kids_dict -# - added detection of duplicate filenames in storages -# - added detection of duplicate references to streams -# - added get_size() and exists() to _OleDirectoryEntry -# - added isOleFile to check header before parsing -# - added __all__ list to control public keywords in pydoc -# 2007-12-04 v0.17 PL: - added _load_direntry to fix a bug in loaddirectory -# - improved _unicode(), added workarounds for Python <2.3 -# - added set_debug_mode and -d option to set debug mode -# - fixed bugs in OleFileIO.open and _OleDirectoryEntry -# - added safety check in main for large or binary -# properties -# - allow size>0 for storages for some implementations -# 2007-12-05 v0.18 PL: - fixed several bugs in handling of FAT, MiniFAT and -# streams -# - added option '-c' in main to check all streams -# 2009-12-10 v0.19 PL: - bugfix for 32 bit arrays on 64 bits platforms -# (thanks to Ben G. and Martijn for reporting the bug) -# 2009-12-11 v0.20 PL: - bugfix in OleFileIO.open when filename is not plain str -# 2010-01-22 v0.21 PL: - added support for big-endian CPUs such as PowerPC Macs -# 2012-02-16 v0.22 PL: - fixed bug in getproperties, patch by chuckleberryfinn -# (https://bitbucket.org/decalage/olefileio_pl/issue/7) -# - added close method to OleFileIO (fixed issue #2) -# 2012-07-25 v0.23 PL: - added support for file-like objects (patch by mete0r_kr) -# 2013-05-05 v0.24 PL: - getproperties: added conversion from filetime to python -# datetime -# - main: displays properties with date format -# - new class OleMetadata to parse standard properties -# - added get_metadata method -# 2013-05-07 v0.24 PL: - a few improvements in OleMetadata -# 2013-05-24 v0.25 PL: - getproperties: option to not convert some timestamps -# - OleMetaData: total_edit_time is now a number of seconds, -# not a timestamp -# - getproperties: added support for VT_BOOL, VT_INT, V_UINT -# - getproperties: filter out null chars from strings -# - getproperties: raise non-fatal defects instead of -# exceptions when properties cannot be parsed properly -# 2013-05-27 PL: - getproperties: improved exception handling -# - _raise_defect: added option to set exception type -# - all non-fatal issues are now recorded, and displayed -# when run as a script -# 2013-07-11 v0.26 PL: - added methods to get modification and creation times -# of a directory entry or a storage/stream -# - fixed parsing of direntry timestamps -# 2013-07-24 PL: - new options in listdir to list storages and/or streams -# 2014-02-04 v0.30 PL: - upgraded code to support Python 3.x by Martin Panter -# - several fixes for Python 2.6 (xrange, MAGIC) -# - reused i32 from Pillow's _binary -# 2014-07-18 v0.31 - preliminary support for 4K sectors -# 2014-07-27 v0.31 PL: - a few improvements in OleFileIO.open (header parsing) -# - Fixed loadfat for large files with 4K sectors (issue #3) -# 2014-07-30 v0.32 PL: - added write_sect to write sectors to disk -# - added write_mode option to OleFileIO.__init__ and open -# 2014-07-31 PL: - fixed padding in write_sect for Python 3, added checks -# - added write_stream to write a stream to disk -# 2014-09-26 v0.40 PL: - renamed OleFileIO_PL to olefile -# 2014-11-09 NE: - added support for Jython (Niko Ehrenfeuchter) -# 2014-11-13 v0.41 PL: - improved isOleFile and OleFileIO.open to support OLE -# data in a string buffer and file-like objects. -# 2014-11-21 PL: - updated comments according to Pillow's commits -# 2015-01-24 v0.42 PL: - changed the default path name encoding from Latin-1 -# to UTF-8 on Python 2.x (Unicode on Python 3.x) -# - added path_encoding option to override the default -# - fixed a bug in _list when a storage is empty - -#----------------------------------------------------------------------------- -# TODO (for version 1.0): -# + get rid of print statements, to simplify Python 2.x and 3.x support -# + add is_stream and is_storage -# + remove leading and trailing slashes where a path is used -# + add functions path_list2str and path_str2list -# + fix how all the methods handle unicode str and/or bytes as arguments -# + add path attrib to _OleDirEntry, set it once and for all in init or -# append_kids (then listdir/_list can be simplified) -# - TESTS with Linux, MacOSX, Python 1.5.2, various files, PIL, ... -# - add underscore to each private method, to avoid their display in -# pydoc/epydoc documentation - Remove it for classes to be documented -# - replace all raised exceptions with _raise_defect (at least in OleFileIO) -# - merge code from _OleStream and OleFileIO.getsect to read sectors -# (maybe add a class for FAT and MiniFAT ?) -# - add method to check all streams (follow sectors chains without storing all -# stream in memory, and report anomalies) -# - use _OleDirectoryEntry.kids_dict to improve _find and _list ? -# - fix Unicode names handling (find some way to stay compatible with Py1.5.2) -# => if possible avoid converting names to Latin-1 -# - review DIFAT code: fix handling of DIFSECT blocks in FAT (not stop) -# - rewrite OleFileIO.getproperties -# - improve docstrings to show more sample uses -# - see also original notes and FIXME below -# - remove all obsolete FIXMEs -# - OleMetadata: fix version attrib according to -# http://msdn.microsoft.com/en-us/library/dd945671%28v=office.12%29.aspx - -# IDEAS: -# - in OleFileIO._open and _OleStream, use size=None instead of 0x7FFFFFFF for -# streams with unknown size -# - use arrays of int instead of long integers for FAT/MiniFAT, to improve -# performance and reduce memory usage ? (possible issue with values >2^31) -# - provide tests with unittest (may need write support to create samples) -# - move all debug code (and maybe dump methods) to a separate module, with -# a class which inherits OleFileIO ? -# - fix docstrings to follow epydoc format -# - add support for big endian byte order ? -# - create a simple OLE explorer with wxPython - -# FUTURE EVOLUTIONS to add write support: -# see issue #6 on Bitbucket: -# https://bitbucket.org/decalage/olefileio_pl/issue/6/improve-olefileio_pl-to-write-ole-files - -#----------------------------------------------------------------------------- -# NOTES from PIL 1.1.6: - -# History: -# 1997-01-20 fl Created -# 1997-01-22 fl Fixed 64-bit portability quirk -# 2003-09-09 fl Fixed typo in OleFileIO.loadfat (noted by Daniel Haertle) -# 2004-02-29 fl Changed long hex constants to signed integers -# -# Notes: -# FIXME: sort out sign problem (eliminate long hex constants) -# FIXME: change filename to use "a/b/c" instead of ["a", "b", "c"] -# FIXME: provide a glob mechanism function (using fnmatchcase) -# -# Literature: -# -# "FlashPix Format Specification, Appendix A", Kodak and Microsoft, -# September 1996. -# -# Quotes: -# -# "If this document and functionality of the Software conflict, -# the actual functionality of the Software represents the correct -# functionality" -- Microsoft, in the OLE format specification - -#------------------------------------------------------------------------------ - - -import io +import olefile import sys -import struct -import array -import os.path -import datetime -#=== COMPATIBILITY WORKAROUNDS ================================================ - -# [PL] Define explicitly the public API to avoid private objects in pydoc: -#TODO: add more -# __all__ = ['OleFileIO', 'isOleFile', 'MAGIC'] - -# For Python 3.x, need to redefine long as int: -if str is not bytes: - long = int - -# Need to make sure we use xrange both on Python 2 and 3.x: -try: - # on Python 2 we need xrange: - iterrange = xrange -except: - # no xrange, for Python 3 it was renamed as range: - iterrange = range - -# [PL] workaround to fix an issue with array item size on 64 bits systems: -if array.array('L').itemsize == 4: - # on 32 bits platforms, long integers in an array are 32 bits: - UINT32 = 'L' -elif array.array('I').itemsize == 4: - # on 64 bits platforms, integers in an array are 32 bits: - UINT32 = 'I' -elif array.array('i').itemsize == 4: - # On 64 bit Jython, signed integers ('i') are the only way to store our 32 - # bit values in an array in a *somewhat* reasonable way, as the otherwise - # perfectly suited 'H' (unsigned int, 32 bits) results in a completely - # unusable behaviour. This is most likely caused by the fact that Java - # doesn't have unsigned values, and thus Jython's "array" implementation, - # which is based on "jarray", doesn't have them either. - # NOTE: to trick Jython into converting the values it would normally - # interpret as "signed" into "unsigned", a binary-and operation with - # 0xFFFFFFFF can be used. This way it is possible to use the same comparing - # operations on all platforms / implementations. The corresponding code - # lines are flagged with a 'JYTHON-WORKAROUND' tag below. - UINT32 = 'i' -else: - raise ValueError('Need to fix a bug with 32 bit arrays, please contact author...') - - -# [PL] These workarounds were inspired from the Path module -# (see http://www.jorendorff.com/articles/python/path/) -try: - basestring -except NameError: - basestring = str - -# [PL] Experimental setting: if True, OLE filenames will be kept in Unicode -# if False (default PIL behaviour), all filenames are converted to Latin-1. -KEEP_UNICODE_NAMES = True - -if sys.version_info[0] < 3: - # On Python 2.x, the default encoding for path names is UTF-8: - DEFAULT_PATH_ENCODING = 'utf-8' -else: - # On Python 3.x, the default encoding for path names is Unicode (None): - DEFAULT_PATH_ENCODING = None - - -#=== DEBUGGING =============================================================== - -#TODO: replace this by proper logging - -# [PL] DEBUG display mode: False by default, use set_debug_mode() or "-d" on -# command line to change it. -DEBUG_MODE = False - - -def debug_print(msg): - print(msg) - - -def debug_pass(msg): - pass - - -debug = debug_pass - - -def set_debug_mode(debug_mode): - """ - Set debug mode on or off, to control display of debugging messages. - :param mode: True or False - """ - global DEBUG_MODE, debug - DEBUG_MODE = debug_mode - if debug_mode: - debug = debug_print - else: - debug = debug_pass - - -#=== CONSTANTS =============================================================== - -# magic bytes that should be at the beginning of every OLE file: -MAGIC = b'\xD0\xCF\x11\xE0\xA1\xB1\x1A\xE1' - -# [PL]: added constants for Sector IDs (from AAF specifications) -MAXREGSECT = 0xFFFFFFFA # (-6) maximum SECT -DIFSECT = 0xFFFFFFFC # (-4) denotes a DIFAT sector in a FAT -FATSECT = 0xFFFFFFFD # (-3) denotes a FAT sector in a FAT -ENDOFCHAIN = 0xFFFFFFFE # (-2) end of a virtual stream chain -FREESECT = 0xFFFFFFFF # (-1) unallocated sector - -# [PL]: added constants for Directory Entry IDs (from AAF specifications) -MAXREGSID = 0xFFFFFFFA # (-6) maximum directory entry ID -NOSTREAM = 0xFFFFFFFF # (-1) unallocated directory entry - -# [PL] object types in storage (from AAF specifications) -STGTY_EMPTY = 0 # empty directory entry (according to OpenOffice.org doc) -STGTY_STORAGE = 1 # element is a storage object -STGTY_STREAM = 2 # element is a stream object -STGTY_LOCKBYTES = 3 # element is an ILockBytes object -STGTY_PROPERTY = 4 # element is an IPropertyStorage object -STGTY_ROOT = 5 # element is a root storage - - -# -# -------------------------------------------------------------------- -# property types - -VT_EMPTY = 0; VT_NULL = 1; VT_I2 = 2; VT_I4 = 3; VT_R4 = 4; VT_R8 = 5; VT_CY = 6; -VT_DATE = 7; VT_BSTR = 8; VT_DISPATCH = 9; VT_ERROR = 10; VT_BOOL = 11; -VT_VARIANT = 12; VT_UNKNOWN = 13; VT_DECIMAL = 14; VT_I1 = 16; VT_UI1 = 17; -VT_UI2 = 18; VT_UI4 = 19; VT_I8 = 20; VT_UI8 = 21; VT_INT = 22; VT_UINT = 23; -VT_VOID = 24; VT_HRESULT = 25; VT_PTR = 26; VT_SAFEARRAY = 27; VT_CARRAY = 28; -VT_USERDEFINED = 29; VT_LPSTR = 30; VT_LPWSTR = 31; VT_FILETIME = 64; -VT_BLOB = 65; VT_STREAM = 66; VT_STORAGE = 67; VT_STREAMED_OBJECT = 68; -VT_STORED_OBJECT = 69; VT_BLOB_OBJECT = 70; VT_CF = 71; VT_CLSID = 72; -VT_VECTOR = 0x1000; - -# map property id to name (for debugging purposes) - -VT = {} -for keyword, var in list(vars().items()): - if keyword[:3] == "VT_": - VT[var] = keyword - -# -# -------------------------------------------------------------------- -# Some common document types (root.clsid fields) - -WORD_CLSID = "00020900-0000-0000-C000-000000000046" -#TODO: check Excel, PPT, ... - -# [PL]: Defect levels to classify parsing errors - see OleFileIO._raise_defect() -DEFECT_UNSURE = 10 # a case which looks weird, but not sure it's a defect -DEFECT_POTENTIAL = 20 # a potential defect -DEFECT_INCORRECT = 30 # an error according to specifications, but parsing - # can go on -DEFECT_FATAL = 40 # an error which cannot be ignored, parsing is - # impossible - -# Minimal size of an empty OLE file, with 512-bytes sectors = 1536 bytes -# (this is used in isOleFile and OleFile.open) -MINIMAL_OLEFILE_SIZE = 1536 - -# [PL] add useful constants to __all__: -# for key in list(vars().keys()): -# if key.startswith('STGTY_') or key.startswith('DEFECT_'): -# __all__.append(key) - - -#=== FUNCTIONS =============================================================== - -def isOleFile(filename): - """ - Test if a file is an OLE container (according to the magic bytes in its header). - - :param filename: string-like or file-like object, OLE file to parse - - - if filename is a string smaller than 1536 bytes, it is the path - of the file to open. (bytes or unicode string) - - if filename is a string longer than 1535 bytes, it is parsed - as the content of an OLE file in memory. (bytes type only) - - if filename is a file-like object (with read and seek methods), - it is parsed as-is. - - :returns: True if OLE, False otherwise. - """ - # check if filename is a string-like or file-like object: - if hasattr(filename, 'read'): - # file-like object: use it directly - header = filename.read(len(MAGIC)) - # just in case, seek back to start of file: - filename.seek(0) - elif isinstance(filename, bytes) and len(filename) >= MINIMAL_OLEFILE_SIZE: - # filename is a bytes string containing the OLE file to be parsed: - header = filename[:len(MAGIC)] - else: - # string-like object: filename of file on disk - header = open(filename, 'rb').read(len(MAGIC)) - if header == MAGIC: - return True - else: - return False - - -if bytes is str: - # version for Python 2.x - def i8(c): - return ord(c) -else: - # version for Python 3.x - def i8(c): - return c if c.__class__ is int else c[0] - - -#TODO: replace i16 and i32 with more readable struct.unpack equivalent? - -def i16(c, o = 0): - """ - Converts a 2-bytes (16 bits) string to an integer. - - c: string containing bytes to convert - o: offset of bytes to convert in string - """ - return struct.unpack(" len(fat): - raise IOError('malformed OLE document, stream too large') - # optimization(?): data is first a list of strings, and join() is called - # at the end to concatenate all in one string. - # (this may not be really useful with recent Python versions) - data = [] - # if size is zero, then first sector index should be ENDOFCHAIN: - if size == 0 and sect != ENDOFCHAIN: - debug('size == 0 and sect != ENDOFCHAIN:') - raise IOError('incorrect OLE sector index for empty stream') - # [PL] A fixed-length for loop is used instead of an undefined while - # loop to avoid DoS attacks: - for i in range(nb_sectors): - # Sector index may be ENDOFCHAIN, but only if size was unknown - if sect == ENDOFCHAIN: - if unknown_size: - break - else: - # else this means that the stream is smaller than declared: - debug('sect=ENDOFCHAIN before expected size') - raise IOError('incomplete OLE stream') - # sector index should be within FAT: - if sect < 0 or sect >= len(fat): - debug('sect=%d (%X) / len(fat)=%d' % (sect, sect, len(fat))) - debug('i=%d / nb_sectors=%d' % (i, nb_sectors)) -## tmp_data = b"".join(data) -## f = open('test_debug.bin', 'wb') -## f.write(tmp_data) -## f.close() -## debug('data read so far: %d bytes' % len(tmp_data)) - raise IOError('incorrect OLE FAT, sector index out of range') - #TODO: merge this code with OleFileIO.getsect() ? - #TODO: check if this works with 4K sectors: - try: - fp.seek(offset + sectorsize * sect) - except: - debug('sect=%d, seek=%d, filesize=%d' % - (sect, offset+sectorsize*sect, filesize)) - raise IOError('OLE sector index out of range') - sector_data = fp.read(sectorsize) - # [PL] check if there was enough data: - # Note: if sector is the last of the file, sometimes it is not a - # complete sector (of 512 or 4K), so we may read less than - # sectorsize. - if len(sector_data) != sectorsize and sect != (len(fat)-1): - debug('sect=%d / len(fat)=%d, seek=%d / filesize=%d, len read=%d' % - (sect, len(fat), offset+sectorsize*sect, filesize, len(sector_data))) - debug('seek+len(read)=%d' % (offset+sectorsize*sect+len(sector_data))) - raise IOError('incomplete OLE sector') - data.append(sector_data) - # jump to next sector in the FAT: - try: - sect = fat[sect] & 0xFFFFFFFF # JYTHON-WORKAROUND - except IndexError: - # [PL] if pointer is out of the FAT an exception is raised - raise IOError('incorrect OLE FAT, sector index out of range') - # [PL] Last sector should be a "end of chain" marker: - if sect != ENDOFCHAIN: - raise IOError('incorrect last sector index in OLE stream') - data = b"".join(data) - # Data is truncated to the actual stream size: - if len(data) >= size: - data = data[:size] - # actual stream size is stored for future use: - self.size = size - elif unknown_size: - # actual stream size was not known, now we know the size of read - # data: - self.size = len(data) - else: - # read data is less than expected: - debug('len(data)=%d, size=%d' % (len(data), size)) - raise IOError('OLE stream size is less than declared') - # when all data is read in memory, BytesIO constructor is called - io.BytesIO.__init__(self, data) - # Then the _OleStream object can be used as a read-only file object. - - -#--- _OleDirectoryEntry ------------------------------------------------------- - -class _OleDirectoryEntry(object): - - """ - OLE2 Directory Entry - """ - # [PL] parsing code moved from OleFileIO.loaddirectory - - # struct to parse directory entries: - # <: little-endian byte order, standard sizes - # (note: this should guarantee that Q returns a 64 bits int) - # 64s: string containing entry name in unicode (max 31 chars) + null char - # H: uint16, number of bytes used in name buffer, including null = (len+1)*2 - # B: uint8, dir entry type (between 0 and 5) - # B: uint8, color: 0=black, 1=red - # I: uint32, index of left child node in the red-black tree, NOSTREAM if none - # I: uint32, index of right child node in the red-black tree, NOSTREAM if none - # I: uint32, index of child root node if it is a storage, else NOSTREAM - # 16s: CLSID, unique identifier (only used if it is a storage) - # I: uint32, user flags - # Q (was 8s): uint64, creation timestamp or zero - # Q (was 8s): uint64, modification timestamp or zero - # I: uint32, SID of first sector if stream or ministream, SID of 1st sector - # of stream containing ministreams if root entry, 0 otherwise - # I: uint32, total stream size in bytes if stream (low 32 bits), 0 otherwise - # I: uint32, total stream size in bytes if stream (high 32 bits), 0 otherwise - STRUCT_DIRENTRY = '<64sHBBIII16sIQQIII' - # size of a directory entry: 128 bytes - DIRENTRY_SIZE = 128 - assert struct.calcsize(STRUCT_DIRENTRY) == DIRENTRY_SIZE - - def __init__(self, entry, sid, olefile): - """ - Constructor for an _OleDirectoryEntry object. - Parses a 128-bytes entry from the OLE Directory stream. - - :param entry : string (must be 128 bytes long) - :param sid : index of this directory entry in the OLE file directory - :param olefile: OleFileIO containing this directory entry - """ - self.sid = sid - # ref to olefile is stored for future use - self.olefile = olefile - # kids is a list of children entries, if this entry is a storage: - # (list of _OleDirectoryEntry objects) - self.kids = [] - # kids_dict is a dictionary of children entries, indexed by their - # name in lowercase: used to quickly find an entry, and to detect - # duplicates - self.kids_dict = {} - # flag used to detect if the entry is referenced more than once in - # directory: - self.used = False - # decode DirEntry - ( - name, - namelength, - self.entry_type, - self.color, - self.sid_left, - self.sid_right, - self.sid_child, - clsid, - self.dwUserFlags, - self.createTime, - self.modifyTime, - self.isectStart, - sizeLow, - sizeHigh - ) = struct.unpack(_OleDirectoryEntry.STRUCT_DIRENTRY, entry) - if self.entry_type not in [STGTY_ROOT, STGTY_STORAGE, STGTY_STREAM, STGTY_EMPTY]: - olefile.raise_defect(DEFECT_INCORRECT, 'unhandled OLE storage type') - # only first directory entry can (and should) be root: - if self.entry_type == STGTY_ROOT and sid != 0: - olefile.raise_defect(DEFECT_INCORRECT, 'duplicate OLE root entry') - if sid == 0 and self.entry_type != STGTY_ROOT: - olefile.raise_defect(DEFECT_INCORRECT, 'incorrect OLE root entry') - #debug (struct.unpack(fmt_entry, entry[:len_entry])) - # name should be at most 31 unicode characters + null character, - # so 64 bytes in total (31*2 + 2): - if namelength > 64: - olefile.raise_defect(DEFECT_INCORRECT, 'incorrect DirEntry name length') - # if exception not raised, namelength is set to the maximum value: - namelength = 64 - # only characters without ending null char are kept: - name = name[:(namelength-2)] - #TODO: check if the name is actually followed by a null unicode character ([MS-CFB] 2.6.1) - #TODO: check if the name does not contain forbidden characters: - # [MS-CFB] 2.6.1: "The following characters are illegal and MUST NOT be part of the name: '/', '\', ':', '!'." - # name is converted from UTF-16LE to the path encoding specified in the OleFileIO: - self.name = olefile._decode_utf16_str(name) - - debug('DirEntry SID=%d: %s' % (self.sid, repr(self.name))) - debug(' - type: %d' % self.entry_type) - debug(' - sect: %d' % self.isectStart) - debug(' - SID left: %d, right: %d, child: %d' % (self.sid_left, - self.sid_right, self.sid_child)) - - # sizeHigh is only used for 4K sectors, it should be zero for 512 bytes - # sectors, BUT apparently some implementations set it as 0xFFFFFFFF, 1 - # or some other value so it cannot be raised as a defect in general: - if olefile.sectorsize == 512: - if sizeHigh != 0 and sizeHigh != 0xFFFFFFFF: - debug('sectorsize=%d, sizeLow=%d, sizeHigh=%d (%X)' % - (olefile.sectorsize, sizeLow, sizeHigh, sizeHigh)) - olefile.raise_defect(DEFECT_UNSURE, 'incorrect OLE stream size') - self.size = sizeLow - else: - self.size = sizeLow + (long(sizeHigh) << 32) - debug(' - size: %d (sizeLow=%d, sizeHigh=%d)' % (self.size, sizeLow, sizeHigh)) - - self.clsid = _clsid(clsid) - # a storage should have a null size, BUT some implementations such as - # Word 8 for Mac seem to allow non-null values => Potential defect: - if self.entry_type == STGTY_STORAGE and self.size != 0: - olefile.raise_defect(DEFECT_POTENTIAL, 'OLE storage with size>0') - # check if stream is not already referenced elsewhere: - if self.entry_type in (STGTY_ROOT, STGTY_STREAM) and self.size > 0: - if self.size < olefile.minisectorcutoff \ - and self.entry_type == STGTY_STREAM: # only streams can be in MiniFAT - # ministream object - minifat = True - else: - minifat = False - olefile._check_duplicate_stream(self.isectStart, minifat) - - def build_storage_tree(self): - """ - Read and build the red-black tree attached to this _OleDirectoryEntry - object, if it is a storage. - Note that this method builds a tree of all subentries, so it should - only be called for the root object once. - """ - debug('build_storage_tree: SID=%d - %s - sid_child=%d' - % (self.sid, repr(self.name), self.sid_child)) - if self.sid_child != NOSTREAM: - # if child SID is not NOSTREAM, then this entry is a storage. - # Let's walk through the tree of children to fill the kids list: - self.append_kids(self.sid_child) - - # Note from OpenOffice documentation: the safest way is to - # recreate the tree because some implementations may store broken - # red-black trees... - - # in the OLE file, entries are sorted on (length, name). - # for convenience, we sort them on name instead: - # (see rich comparison methods in this class) - self.kids.sort() - - def append_kids(self, child_sid): - """ - Walk through red-black tree of children of this directory entry to add - all of them to the kids list. (recursive method) - - :param child_sid : index of child directory entry to use, or None when called - first time for the root. (only used during recursion) - """ - # [PL] this method was added to use simple recursion instead of a complex - # algorithm. - # if this is not a storage or a leaf of the tree, nothing to do: - if child_sid == NOSTREAM: - return - # check if child SID is in the proper range: - if child_sid < 0 or child_sid >= len(self.olefile.direntries): - self.olefile.raise_defect(DEFECT_FATAL, 'OLE DirEntry index out of range') - # get child direntry: - child = self.olefile._load_direntry(child_sid) #direntries[child_sid] - debug('append_kids: child_sid=%d - %s - sid_left=%d, sid_right=%d, sid_child=%d' - % (child.sid, repr(child.name), child.sid_left, child.sid_right, child.sid_child)) - # the directory entries are organized as a red-black tree. - # (cf. Wikipedia for details) - # First walk through left side of the tree: - self.append_kids(child.sid_left) - # Check if its name is not already used (case-insensitive): - name_lower = child.name.lower() - if name_lower in self.kids_dict: - self.olefile.raise_defect(DEFECT_INCORRECT, - "Duplicate filename in OLE storage") - # Then the child_sid _OleDirectoryEntry object is appended to the - # kids list and dictionary: - self.kids.append(child) - self.kids_dict[name_lower] = child - # Check if kid was not already referenced in a storage: - if child.used: - self.olefile.raise_defect(DEFECT_INCORRECT, - 'OLE Entry referenced more than once') - child.used = True - # Finally walk through right side of the tree: - self.append_kids(child.sid_right) - # Afterwards build kid's own tree if it's also a storage: - child.build_storage_tree() - - def __eq__(self, other): - "Compare entries by name" - return self.name == other.name - - def __lt__(self, other): - "Compare entries by name" - return self.name < other.name - - def __ne__(self, other): - return not self.__eq__(other) - - def __le__(self, other): - return self.__eq__(other) or self.__lt__(other) - - # Reflected __lt__() and __le__() will be used for __gt__() and __ge__() - - #TODO: replace by the same function as MS implementation ? - # (order by name length first, then case-insensitive order) - - def dump(self, tab = 0): - "Dump this entry, and all its subentries (for debug purposes only)" - TYPES = ["(invalid)", "(storage)", "(stream)", "(lockbytes)", - "(property)", "(root)"] - print(" "*tab + repr(self.name), TYPES[self.entry_type], end=' ') - if self.entry_type in (STGTY_STREAM, STGTY_ROOT): - print(self.size, "bytes", end=' ') - print() - if self.entry_type in (STGTY_STORAGE, STGTY_ROOT) and self.clsid: - print(" "*tab + "{%s}" % self.clsid) - - for kid in self.kids: - kid.dump(tab + 2) - - def getmtime(self): - """ - Return modification time of a directory entry. - - :returns: None if modification time is null, a python datetime object - otherwise (UTC timezone) - - new in version 0.26 - """ - if self.modifyTime == 0: - return None - return filetime2datetime(self.modifyTime) - - def getctime(self): - """ - Return creation time of a directory entry. - - :returns: None if modification time is null, a python datetime object - otherwise (UTC timezone) - - new in version 0.26 - """ - if self.createTime == 0: - return None - return filetime2datetime(self.createTime) - - -#--- OleFileIO ---------------------------------------------------------------- - -class OleFileIO(object): - """ - OLE container object - - This class encapsulates the interface to an OLE 2 structured - storage file. Use the :py:meth:`~PIL.OleFileIO.OleFileIO.listdir` and - :py:meth:`~PIL.OleFileIO.OleFileIO.openstream` methods to - access the contents of this file. - - Object names are given as a list of strings, one for each subentry - level. The root entry should be omitted. For example, the following - code extracts all image streams from a Microsoft Image Composer file:: - - ole = OleFileIO("fan.mic") - - for entry in ole.listdir(): - if entry[1:2] == "Image": - fin = ole.openstream(entry) - fout = open(entry[0:1], "wb") - while True: - s = fin.read(8192) - if not s: - break - fout.write(s) - - You can use the viewer application provided with the Python Imaging - Library to view the resulting files (which happens to be standard - TIFF files). - """ - - def __init__(self, filename=None, raise_defects=DEFECT_FATAL, - write_mode=False, debug=False, path_encoding=DEFAULT_PATH_ENCODING): - """ - Constructor for the OleFileIO class. - - :param filename: file to open. - - - if filename is a string smaller than 1536 bytes, it is the path - of the file to open. (bytes or unicode string) - - if filename is a string longer than 1535 bytes, it is parsed - as the content of an OLE file in memory. (bytes type only) - - if filename is a file-like object (with read, seek and tell methods), - it is parsed as-is. - - :param raise_defects: minimal level for defects to be raised as exceptions. - (use DEFECT_FATAL for a typical application, DEFECT_INCORRECT for a - security-oriented application, see source code for details) - - :param write_mode: bool, if True the file is opened in read/write mode instead - of read-only by default. - - :param debug: bool, set debug mode - - :param path_encoding: None or str, name of the codec to use for path - names (streams and storages), or None for Unicode. - Unicode by default on Python 3+, UTF-8 on Python 2.x. - (new in olefile 0.42, was hardcoded to Latin-1 until olefile v0.41) - """ - set_debug_mode(debug) - # minimal level for defects to be raised as exceptions: - self._raise_defects_level = raise_defects - # list of defects/issues not raised as exceptions: - # tuples of (exception type, message) - self.parsing_issues = [] - self.write_mode = write_mode - self.path_encoding = path_encoding - self._filesize = None - self.fp = None - if filename: - self.open(filename, write_mode=write_mode) - - def raise_defect(self, defect_level, message, exception_type=IOError): - """ - This method should be called for any defect found during file parsing. - It may raise an IOError exception according to the minimal level chosen - for the OleFileIO object. - - :param defect_level: defect level, possible values are: - - - DEFECT_UNSURE : a case which looks weird, but not sure it's a defect - - DEFECT_POTENTIAL : a potential defect - - DEFECT_INCORRECT : an error according to specifications, but parsing can go on - - DEFECT_FATAL : an error which cannot be ignored, parsing is impossible - - :param message: string describing the defect, used with raised exception. - :param exception_type: exception class to be raised, IOError by default - """ - # added by [PL] - if defect_level >= self._raise_defects_level: - raise exception_type(message) - else: - # just record the issue, no exception raised: - self.parsing_issues.append((exception_type, message)) - - def _decode_utf16_str(self, utf16_str, errors='replace'): - """ - Decode a string encoded in UTF-16 LE format, as found in the OLE - directory or in property streams. Return a string encoded - according to the path_encoding specified for the OleFileIO object. - - :param utf16_str: bytes string encoded in UTF-16 LE format - :param errors: str, see python documentation for str.decode() - :return: str, encoded according to path_encoding - """ - unicode_str = utf16_str.decode('UTF-16LE', errors) - if self.path_encoding: - # an encoding has been specified for path names: - return unicode_str.encode(self.path_encoding, errors) - else: - # path_encoding=None, return the Unicode string as-is: - return unicode_str - - def open(self, filename, write_mode=False): - """ - Open an OLE2 file in read-only or read/write mode. - Read and parse the header, FAT and directory. - - :param filename: string-like or file-like object, OLE file to parse - - - if filename is a string smaller than 1536 bytes, it is the path - of the file to open. (bytes or unicode string) - - if filename is a string longer than 1535 bytes, it is parsed - as the content of an OLE file in memory. (bytes type only) - - if filename is a file-like object (with read, seek and tell methods), - it is parsed as-is. - - :param write_mode: bool, if True the file is opened in read/write mode instead - of read-only by default. (ignored if filename is not a path) - """ - self.write_mode = write_mode - # [PL] check if filename is a string-like or file-like object: - # (it is better to check for a read() method) - if hasattr(filename, 'read'): - #TODO: also check seek and tell methods? - # file-like object: use it directly - self.fp = filename - elif isinstance(filename, bytes) and len(filename) >= MINIMAL_OLEFILE_SIZE: - # filename is a bytes string containing the OLE file to be parsed: - # convert it to BytesIO - self.fp = io.BytesIO(filename) - else: - # string-like object: filename of file on disk - if self.write_mode: - # open file in mode 'read with update, binary' - # According to https://docs.python.org/2/library/functions.html#open - # 'w' would truncate the file, 'a' may only append on some Unixes - mode = 'r+b' - else: - # read-only mode by default - mode = 'rb' - self.fp = open(filename, mode) - # obtain the filesize by using seek and tell, which should work on most - # file-like objects: - #TODO: do it above, using getsize with filename when possible? - #TODO: fix code to fail with clear exception when filesize cannot be obtained - filesize = 0 - self.fp.seek(0, os.SEEK_END) - try: - filesize = self.fp.tell() - finally: - self.fp.seek(0) - self._filesize = filesize - - # lists of streams in FAT and MiniFAT, to detect duplicate references - # (list of indexes of first sectors of each stream) - self._used_streams_fat = [] - self._used_streams_minifat = [] - - header = self.fp.read(512) - - if len(header) != 512 or header[:8] != MAGIC: - self.raise_defect(DEFECT_FATAL, "not an OLE2 structured storage file") - - # [PL] header structure according to AAF specifications: - ##Header - ##struct StructuredStorageHeader { // [offset from start (bytes), length (bytes)] - ##BYTE _abSig[8]; // [00H,08] {0xd0, 0xcf, 0x11, 0xe0, 0xa1, 0xb1, - ## // 0x1a, 0xe1} for current version - ##CLSID _clsid; // [08H,16] reserved must be zero (WriteClassStg/ - ## // GetClassFile uses root directory class id) - ##USHORT _uMinorVersion; // [18H,02] minor version of the format: 33 is - ## // written by reference implementation - ##USHORT _uDllVersion; // [1AH,02] major version of the dll/format: 3 for - ## // 512-byte sectors, 4 for 4 KB sectors - ##USHORT _uByteOrder; // [1CH,02] 0xFFFE: indicates Intel byte-ordering - ##USHORT _uSectorShift; // [1EH,02] size of sectors in power-of-two; - ## // typically 9 indicating 512-byte sectors - ##USHORT _uMiniSectorShift; // [20H,02] size of mini-sectors in power-of-two; - ## // typically 6 indicating 64-byte mini-sectors - ##USHORT _usReserved; // [22H,02] reserved, must be zero - ##ULONG _ulReserved1; // [24H,04] reserved, must be zero - ##FSINDEX _csectDir; // [28H,04] must be zero for 512-byte sectors, - ## // number of SECTs in directory chain for 4 KB - ## // sectors - ##FSINDEX _csectFat; // [2CH,04] number of SECTs in the FAT chain - ##SECT _sectDirStart; // [30H,04] first SECT in the directory chain - ##DFSIGNATURE _signature; // [34H,04] signature used for transactions; must - ## // be zero. The reference implementation - ## // does not support transactions - ##ULONG _ulMiniSectorCutoff; // [38H,04] maximum size for a mini stream; - ## // typically 4096 bytes - ##SECT _sectMiniFatStart; // [3CH,04] first SECT in the MiniFAT chain - ##FSINDEX _csectMiniFat; // [40H,04] number of SECTs in the MiniFAT chain - ##SECT _sectDifStart; // [44H,04] first SECT in the DIFAT chain - ##FSINDEX _csectDif; // [48H,04] number of SECTs in the DIFAT chain - ##SECT _sectFat[109]; // [4CH,436] the SECTs of first 109 FAT sectors - ##}; - - # [PL] header decoding: - # '<' indicates little-endian byte ordering for Intel (cf. struct module help) - fmt_header = '<8s16sHHHHHHLLLLLLLLLL' - header_size = struct.calcsize(fmt_header) - debug("fmt_header size = %d, +FAT = %d" % (header_size, header_size + 109*4)) - header1 = header[:header_size] - ( - self.Sig, - self.clsid, - self.MinorVersion, - self.DllVersion, - self.ByteOrder, - self.SectorShift, - self.MiniSectorShift, - self.Reserved, self.Reserved1, - self.csectDir, - self.csectFat, - self.sectDirStart, - self.signature, - self.MiniSectorCutoff, - self.MiniFatStart, - self.csectMiniFat, - self.sectDifStart, - self.csectDif - ) = struct.unpack(fmt_header, header1) - debug(struct.unpack(fmt_header, header1)) - - if self.Sig != MAGIC: - # OLE signature should always be present - self.raise_defect(DEFECT_FATAL, "incorrect OLE signature") - if self.clsid != bytearray(16): - # according to AAF specs, CLSID should always be zero - self.raise_defect(DEFECT_INCORRECT, "incorrect CLSID in OLE header") - debug("MinorVersion = %d" % self.MinorVersion) - debug("DllVersion = %d" % self.DllVersion) - if self.DllVersion not in [3, 4]: - # version 3: usual format, 512 bytes per sector - # version 4: large format, 4K per sector - self.raise_defect(DEFECT_INCORRECT, "incorrect DllVersion in OLE header") - debug("ByteOrder = %X" % self.ByteOrder) - if self.ByteOrder != 0xFFFE: - # For now only common little-endian documents are handled correctly - self.raise_defect(DEFECT_FATAL, "incorrect ByteOrder in OLE header") - # TODO: add big-endian support for documents created on Mac ? - # But according to [MS-CFB] ? v20140502, ByteOrder MUST be 0xFFFE. - self.SectorSize = 2**self.SectorShift - debug("SectorSize = %d" % self.SectorSize) - if self.SectorSize not in [512, 4096]: - self.raise_defect(DEFECT_INCORRECT, "incorrect SectorSize in OLE header") - if (self.DllVersion == 3 and self.SectorSize != 512) \ - or (self.DllVersion == 4 and self.SectorSize != 4096): - self.raise_defect(DEFECT_INCORRECT, "SectorSize does not match DllVersion in OLE header") - self.MiniSectorSize = 2**self.MiniSectorShift - debug("MiniSectorSize = %d" % self.MiniSectorSize) - if self.MiniSectorSize not in [64]: - self.raise_defect(DEFECT_INCORRECT, "incorrect MiniSectorSize in OLE header") - if self.Reserved != 0 or self.Reserved1 != 0: - self.raise_defect(DEFECT_INCORRECT, "incorrect OLE header (non-null reserved bytes)") - debug("csectDir = %d" % self.csectDir) - # Number of directory sectors (only allowed if DllVersion != 3) - if self.SectorSize == 512 and self.csectDir != 0: - self.raise_defect(DEFECT_INCORRECT, "incorrect csectDir in OLE header") - debug("csectFat = %d" % self.csectFat) - # csectFat = number of FAT sectors in the file - debug("sectDirStart = %X" % self.sectDirStart) - # sectDirStart = 1st sector containing the directory - debug("signature = %d" % self.signature) - # Signature should be zero, BUT some implementations do not follow this - # rule => only a potential defect: - # (according to MS-CFB, may be != 0 for applications supporting file - # transactions) - if self.signature != 0: - self.raise_defect(DEFECT_POTENTIAL, "incorrect OLE header (signature>0)") - debug("MiniSectorCutoff = %d" % self.MiniSectorCutoff) - # MS-CFB: This integer field MUST be set to 0x00001000. This field - # specifies the maximum size of a user-defined data stream allocated - # from the mini FAT and mini stream, and that cutoff is 4096 bytes. - # Any user-defined data stream larger than or equal to this cutoff size - # must be allocated as normal sectors from the FAT. - if self.MiniSectorCutoff != 0x1000: - self.raise_defect(DEFECT_INCORRECT, "incorrect MiniSectorCutoff in OLE header") - debug("MiniFatStart = %X" % self.MiniFatStart) - debug("csectMiniFat = %d" % self.csectMiniFat) - debug("sectDifStart = %X" % self.sectDifStart) - debug("csectDif = %d" % self.csectDif) - - # calculate the number of sectors in the file - # (-1 because header doesn't count) - self.nb_sect = ((filesize + self.SectorSize-1) // self.SectorSize) - 1 - debug("Number of sectors in the file: %d" % self.nb_sect) - #TODO: change this test, because an OLE file MAY contain other data - # after the last sector. - - # file clsid - self.clsid = _clsid(header[8:24]) - - #TODO: remove redundant attributes, and fix the code which uses them? - self.sectorsize = self.SectorSize #1 << i16(header, 30) - self.minisectorsize = self.MiniSectorSize #1 << i16(header, 32) - self.minisectorcutoff = self.MiniSectorCutoff # i32(header, 56) - - # check known streams for duplicate references (these are always in FAT, - # never in MiniFAT): - self._check_duplicate_stream(self.sectDirStart) - # check MiniFAT only if it is not empty: - if self.csectMiniFat: - self._check_duplicate_stream(self.MiniFatStart) - # check DIFAT only if it is not empty: - if self.csectDif: - self._check_duplicate_stream(self.sectDifStart) - - # Load file allocation tables - self.loadfat(header) - # Load directory. This sets both the direntries list (ordered by sid) - # and the root (ordered by hierarchy) members. - self.loaddirectory(self.sectDirStart)#i32(header, 48)) - self.ministream = None - self.minifatsect = self.MiniFatStart #i32(header, 60) - - def close(self): - """ - close the OLE file, to release the file object - """ - self.fp.close() - - def _check_duplicate_stream(self, first_sect, minifat=False): - """ - Checks if a stream has not been already referenced elsewhere. - This method should only be called once for each known stream, and only - if stream size is not null. - - :param first_sect: int, index of first sector of the stream in FAT - :param minifat: bool, if True, stream is located in the MiniFAT, else in the FAT - """ - if minifat: - debug('_check_duplicate_stream: sect=%d in MiniFAT' % first_sect) - used_streams = self._used_streams_minifat - else: - debug('_check_duplicate_stream: sect=%d in FAT' % first_sect) - # some values can be safely ignored (not a real stream): - if first_sect in (DIFSECT, FATSECT, ENDOFCHAIN, FREESECT): - return - used_streams = self._used_streams_fat - #TODO: would it be more efficient using a dict or hash values, instead - # of a list of long ? - if first_sect in used_streams: - self.raise_defect(DEFECT_INCORRECT, 'Stream referenced twice') - else: - used_streams.append(first_sect) - - def dumpfat(self, fat, firstindex=0): - "Displays a part of FAT in human-readable form for debugging purpose" - # [PL] added only for debug - if not DEBUG_MODE: - return - # dictionary to convert special FAT values in human-readable strings - VPL = 8 # values per line (8+1 * 8+1 = 81) - fatnames = { - FREESECT: "..free..", - ENDOFCHAIN: "[ END. ]", - FATSECT: "FATSECT ", - DIFSECT: "DIFSECT " - } - nbsect = len(fat) - nlines = (nbsect+VPL-1)//VPL - print("index", end=" ") - for i in range(VPL): - print("%8X" % i, end=" ") - print() - for l in range(nlines): - index = l*VPL - print("%8X:" % (firstindex+index), end=" ") - for i in range(index, index+VPL): - if i >= nbsect: - break - sect = fat[i] - aux = sect & 0xFFFFFFFF # JYTHON-WORKAROUND - if aux in fatnames: - name = fatnames[aux] - else: - if sect == i+1: - name = " --->" - else: - name = "%8X" % sect - print(name, end=" ") - print() - - def dumpsect(self, sector, firstindex=0): - "Displays a sector in a human-readable form, for debugging purpose." - if not DEBUG_MODE: - return - VPL = 8 # number of values per line (8+1 * 8+1 = 81) - tab = array.array(UINT32, sector) - if sys.byteorder == 'big': - tab.byteswap() - nbsect = len(tab) - nlines = (nbsect+VPL-1)//VPL - print("index", end=" ") - for i in range(VPL): - print("%8X" % i, end=" ") - print() - for l in range(nlines): - index = l*VPL - print("%8X:" % (firstindex+index), end=" ") - for i in range(index, index+VPL): - if i >= nbsect: - break - sect = tab[i] - name = "%8X" % sect - print(name, end=" ") - print() - - def sect2array(self, sect): - """ - convert a sector to an array of 32 bits unsigned integers, - swapping bytes on big endian CPUs such as PowerPC (old Macs) - """ - a = array.array(UINT32, sect) - # if CPU is big endian, swap bytes: - if sys.byteorder == 'big': - a.byteswap() - return a - - def loadfat_sect(self, sect): - """ - Adds the indexes of the given sector to the FAT - - :param sect: string containing the first FAT sector, or array of long integers - :returns: index of last FAT sector. - """ - # a FAT sector is an array of ulong integers. - if isinstance(sect, array.array): - # if sect is already an array it is directly used - fat1 = sect - else: - # if it's a raw sector, it is parsed in an array - fat1 = self.sect2array(sect) - self.dumpsect(sect) - # The FAT is a sector chain starting at the first index of itself. - for isect in fat1: - isect = isect & 0xFFFFFFFF # JYTHON-WORKAROUND - debug("isect = %X" % isect) - if isect == ENDOFCHAIN or isect == FREESECT: - # the end of the sector chain has been reached - debug("found end of sector chain") - break - # read the FAT sector - s = self.getsect(isect) - # parse it as an array of 32 bits integers, and add it to the - # global FAT array - nextfat = self.sect2array(s) - self.fat = self.fat + nextfat - return isect - - def loadfat(self, header): - """ - Load the FAT table. - """ - # The 1st sector of the file contains sector numbers for the first 109 - # FAT sectors, right after the header which is 76 bytes long. - # (always 109, whatever the sector size: 512 bytes = 76+4*109) - # Additional sectors are described by DIF blocks - - sect = header[76:512] - debug("len(sect)=%d, so %d integers" % (len(sect), len(sect)//4)) - #fat = [] - # [PL] FAT is an array of 32 bits unsigned ints, it's more effective - # to use an array than a list in Python. - # It's initialized as empty first: - self.fat = array.array(UINT32) - self.loadfat_sect(sect) - #self.dumpfat(self.fat) -## for i in range(0, len(sect), 4): -## ix = i32(sect, i) -## # [PL] if ix == -2 or ix == -1: # ix == 0xFFFFFFFE or ix == 0xFFFFFFFF: -## if ix == 0xFFFFFFFE or ix == 0xFFFFFFFF: -## break -## s = self.getsect(ix) -## #fat = fat + [i32(s, i) for i in range(0, len(s), 4)] -## fat = fat + array.array(UINT32, s) - if self.csectDif != 0: - # [PL] There's a DIFAT because file is larger than 6.8MB - # some checks just in case: - if self.csectFat <= 109: - # there must be at least 109 blocks in header and the rest in - # DIFAT, so number of sectors must be >109. - self.raise_defect(DEFECT_INCORRECT, 'incorrect DIFAT, not enough sectors') - if self.sectDifStart >= self.nb_sect: - # initial DIFAT block index must be valid - self.raise_defect(DEFECT_FATAL, 'incorrect DIFAT, first index out of range') - debug("DIFAT analysis...") - # We compute the necessary number of DIFAT sectors : - # Number of pointers per DIFAT sector = (sectorsize/4)-1 - # (-1 because the last pointer is the next DIFAT sector number) - nb_difat_sectors = (self.sectorsize//4)-1 - # (if 512 bytes: each DIFAT sector = 127 pointers + 1 towards next DIFAT sector) - nb_difat = (self.csectFat-109 + nb_difat_sectors-1)//nb_difat_sectors - debug("nb_difat = %d" % nb_difat) - if self.csectDif != nb_difat: - raise IOError('incorrect DIFAT') - isect_difat = self.sectDifStart - for i in iterrange(nb_difat): - debug("DIFAT block %d, sector %X" % (i, isect_difat)) - #TODO: check if corresponding FAT SID = DIFSECT - sector_difat = self.getsect(isect_difat) - difat = self.sect2array(sector_difat) - self.dumpsect(sector_difat) - self.loadfat_sect(difat[:nb_difat_sectors]) - # last DIFAT pointer is next DIFAT sector: - isect_difat = difat[nb_difat_sectors] - debug("next DIFAT sector: %X" % isect_difat) - # checks: - if isect_difat not in [ENDOFCHAIN, FREESECT]: - # last DIFAT pointer value must be ENDOFCHAIN or FREESECT - raise IOError('incorrect end of DIFAT') -## if len(self.fat) != self.csectFat: -## # FAT should contain csectFat blocks -## print("FAT length: %d instead of %d" % (len(self.fat), self.csectFat)) -## raise IOError('incorrect DIFAT') - # since FAT is read from fixed-size sectors, it may contain more values - # than the actual number of sectors in the file. - # Keep only the relevant sector indexes: - if len(self.fat) > self.nb_sect: - debug('len(fat)=%d, shrunk to nb_sect=%d' % (len(self.fat), self.nb_sect)) - self.fat = self.fat[:self.nb_sect] - debug('\nFAT:') - self.dumpfat(self.fat) - - def loadminifat(self): - """ - Load the MiniFAT table. - """ - # MiniFAT is stored in a standard sub-stream, pointed to by a header - # field. - # NOTE: there are two sizes to take into account for this stream: - # 1) Stream size is calculated according to the number of sectors - # declared in the OLE header. This allocated stream may be more than - # needed to store the actual sector indexes. - # (self.csectMiniFat is the number of sectors of size self.SectorSize) - stream_size = self.csectMiniFat * self.SectorSize - # 2) Actually used size is calculated by dividing the MiniStream size - # (given by root entry size) by the size of mini sectors, *4 for - # 32 bits indexes: - nb_minisectors = (self.root.size + self.MiniSectorSize-1) // self.MiniSectorSize - used_size = nb_minisectors * 4 - debug('loadminifat(): minifatsect=%d, nb FAT sectors=%d, used_size=%d, stream_size=%d, nb MiniSectors=%d' % - (self.minifatsect, self.csectMiniFat, used_size, stream_size, nb_minisectors)) - if used_size > stream_size: - # This is not really a problem, but may indicate a wrong implementation: - self.raise_defect(DEFECT_INCORRECT, 'OLE MiniStream is larger than MiniFAT') - # In any case, first read stream_size: - s = self._open(self.minifatsect, stream_size, force_FAT=True).read() - # [PL] Old code replaced by an array: - # self.minifat = [i32(s, i) for i in range(0, len(s), 4)] - self.minifat = self.sect2array(s) - # Then shrink the array to used size, to avoid indexes out of MiniStream: - debug('MiniFAT shrunk from %d to %d sectors' % (len(self.minifat), nb_minisectors)) - self.minifat = self.minifat[:nb_minisectors] - debug('loadminifat(): len=%d' % len(self.minifat)) - debug('\nMiniFAT:') - self.dumpfat(self.minifat) - - def getsect(self, sect): - """ - Read given sector from file on disk. - - :param sect: int, sector index - :returns: a string containing the sector data. - """ - # From [MS-CFB]: A sector number can be converted into a byte offset - # into the file by using the following formula: - # (sector number + 1) x Sector Size. - # This implies that sector #0 of the file begins at byte offset Sector - # Size, not at 0. - - # [PL] the original code in PIL was wrong when sectors are 4KB instead of - # 512 bytes: - # self.fp.seek(512 + self.sectorsize * sect) - # [PL]: added safety checks: - # print("getsect(%X)" % sect) - try: - self.fp.seek(self.sectorsize * (sect+1)) - except: - debug('getsect(): sect=%X, seek=%d, filesize=%d' % - (sect, self.sectorsize*(sect+1), self._filesize)) - self.raise_defect(DEFECT_FATAL, 'OLE sector index out of range') - sector = self.fp.read(self.sectorsize) - if len(sector) != self.sectorsize: - debug('getsect(): sect=%X, read=%d, sectorsize=%d' % - (sect, len(sector), self.sectorsize)) - self.raise_defect(DEFECT_FATAL, 'incomplete OLE sector') - return sector - - def write_sect(self, sect, data, padding=b'\x00'): - """ - Write given sector to file on disk. - - :param sect: int, sector index - :param data: bytes, sector data - :param padding: single byte, padding character if data < sector size - """ - if not isinstance(data, bytes): - raise TypeError("write_sect: data must be a bytes string") - if not isinstance(padding, bytes) or len(padding) != 1: - raise TypeError("write_sect: padding must be a bytes string of 1 char") - #TODO: we could allow padding=None for no padding at all - try: - self.fp.seek(self.sectorsize * (sect+1)) - except: - debug('write_sect(): sect=%X, seek=%d, filesize=%d' % - (sect, self.sectorsize*(sect+1), self._filesize)) - self.raise_defect(DEFECT_FATAL, 'OLE sector index out of range') - if len(data) < self.sectorsize: - # add padding - data += padding * (self.sectorsize - len(data)) - elif len(data) < self.sectorsize: - raise ValueError("Data is larger than sector size") - self.fp.write(data) - - def loaddirectory(self, sect): - """ - Load the directory. - - :param sect: sector index of directory stream. - """ - # The directory is stored in a standard - # substream, independent of its size. - - # open directory stream as a read-only file: - # (stream size is not known in advance) - self.directory_fp = self._open(sect) - - # [PL] to detect malformed documents and avoid DoS attacks, the maximum - # number of directory entries can be calculated: - max_entries = self.directory_fp.size // 128 - debug('loaddirectory: size=%d, max_entries=%d' % - (self.directory_fp.size, max_entries)) - - # Create list of directory entries - # self.direntries = [] - # We start with a list of "None" object - self.direntries = [None] * max_entries -## for sid in iterrange(max_entries): -## entry = fp.read(128) -## if not entry: -## break -## self.direntries.append(_OleDirectoryEntry(entry, sid, self)) - # load root entry: - root_entry = self._load_direntry(0) - # Root entry is the first entry: - self.root = self.direntries[0] - # read and build all storage trees, starting from the root: - self.root.build_storage_tree() - - def _load_direntry(self, sid): - """ - Load a directory entry from the directory. - This method should only be called once for each storage/stream when - loading the directory. - - :param sid: index of storage/stream in the directory. - :returns: a _OleDirectoryEntry object - - :exception IOError: if the entry has always been referenced. - """ - # check if SID is OK: - if sid < 0 or sid >= len(self.direntries): - self.raise_defect(DEFECT_FATAL, "OLE directory index out of range") - # check if entry was already referenced: - if self.direntries[sid] is not None: - self.raise_defect(DEFECT_INCORRECT, - "double reference for OLE stream/storage") - # if exception not raised, return the object - return self.direntries[sid] - self.directory_fp.seek(sid * 128) - entry = self.directory_fp.read(128) - self.direntries[sid] = _OleDirectoryEntry(entry, sid, self) - return self.direntries[sid] - - def dumpdirectory(self): - """ - Dump directory (for debugging only) - """ - self.root.dump() - - def _open(self, start, size = 0x7FFFFFFF, force_FAT=False): - """ - Open a stream, either in FAT or MiniFAT according to its size. - (openstream helper) - - :param start: index of first sector - :param size: size of stream (or nothing if size is unknown) - :param force_FAT: if False (default), stream will be opened in FAT or MiniFAT - according to size. If True, it will always be opened in FAT. - """ - debug('OleFileIO.open(): sect=%d, size=%d, force_FAT=%s' % - (start, size, str(force_FAT))) - # stream size is compared to the MiniSectorCutoff threshold: - if size < self.minisectorcutoff and not force_FAT: - # ministream object - if not self.ministream: - # load MiniFAT if it wasn't already done: - self.loadminifat() - # The first sector index of the miniFAT stream is stored in the - # root directory entry: - size_ministream = self.root.size - debug('Opening MiniStream: sect=%d, size=%d' % - (self.root.isectStart, size_ministream)) - self.ministream = self._open(self.root.isectStart, - size_ministream, force_FAT=True) - return _OleStream(fp=self.ministream, sect=start, size=size, - offset=0, sectorsize=self.minisectorsize, - fat=self.minifat, filesize=self.ministream.size) - else: - # standard stream - return _OleStream(fp=self.fp, sect=start, size=size, - offset=self.sectorsize, - sectorsize=self.sectorsize, fat=self.fat, - filesize=self._filesize) - - def _list(self, files, prefix, node, streams=True, storages=False): - """ - listdir helper - - :param files: list of files to fill in - :param prefix: current location in storage tree (list of names) - :param node: current node (_OleDirectoryEntry object) - :param streams: bool, include streams if True (True by default) - new in v0.26 - :param storages: bool, include storages if True (False by default) - new in v0.26 - (note: the root storage is never included) - """ - prefix = prefix + [node.name] - for entry in node.kids: - if entry.entry_type == STGTY_STORAGE: - # this is a storage - if storages: - # add it to the list - files.append(prefix[1:] + [entry.name]) - # check its kids - self._list(files, prefix, entry, streams, storages) - elif entry.entry_type == STGTY_STREAM: - # this is a stream - if streams: - # add it to the list - files.append(prefix[1:] + [entry.name]) - else: - self.raise_defect(DEFECT_INCORRECT, 'The directory tree contains an entry which is not a stream nor a storage.') - - def listdir(self, streams=True, storages=False): - """ - Return a list of streams and/or storages stored in this file - - :param streams: bool, include streams if True (True by default) - new in v0.26 - :param storages: bool, include storages if True (False by default) - new in v0.26 - (note: the root storage is never included) - :returns: list of stream and/or storage paths - """ - files = [] - self._list(files, [], self.root, streams, storages) - return files - - def _find(self, filename): - """ - Returns directory entry of given filename. (openstream helper) - Note: this method is case-insensitive. - - :param filename: path of stream in storage tree (except root entry), either: - - - a string using Unix path syntax, for example: - 'storage_1/storage_1.2/stream' - - or a list of storage filenames, path to the desired stream/storage. - Example: ['storage_1', 'storage_1.2', 'stream'] - - :returns: sid of requested filename - :exception IOError: if file not found - """ - - # if filename is a string instead of a list, split it on slashes to - # convert to a list: - if isinstance(filename, basestring): - filename = filename.split('/') - # walk across storage tree, following given path: - node = self.root - for name in filename: - for kid in node.kids: - if kid.name.lower() == name.lower(): - break - else: - raise IOError("file not found") - node = kid - return node.sid - - def openstream(self, filename): - """ - Open a stream as a read-only file object (BytesIO). - Note: filename is case-insensitive. - - :param filename: path of stream in storage tree (except root entry), either: - - - a string using Unix path syntax, for example: - 'storage_1/storage_1.2/stream' - - or a list of storage filenames, path to the desired stream/storage. - Example: ['storage_1', 'storage_1.2', 'stream'] - - :returns: file object (read-only) - :exception IOError: if filename not found, or if this is not a stream. - """ - sid = self._find(filename) - entry = self.direntries[sid] - if entry.entry_type != STGTY_STREAM: - raise IOError("this file is not a stream") - return self._open(entry.isectStart, entry.size) - - def write_stream(self, stream_name, data): - """ - Write a stream to disk. For now, it is only possible to replace an - existing stream by data of the same size. - - :param stream_name: path of stream in storage tree (except root entry), either: - - - a string using Unix path syntax, for example: - 'storage_1/storage_1.2/stream' - - or a list of storage filenames, path to the desired stream/storage. - Example: ['storage_1', 'storage_1.2', 'stream'] - - :param data: bytes, data to be written, must be the same size as the original - stream. - """ - if not isinstance(data, bytes): - raise TypeError("write_stream: data must be a bytes string") - sid = self._find(stream_name) - entry = self.direntries[sid] - if entry.entry_type != STGTY_STREAM: - raise IOError("this is not a stream") - size = entry.size - if size != len(data): - raise ValueError("write_stream: data must be the same size as the existing stream") - if size < self.minisectorcutoff: - raise NotImplementedError("Writing a stream in MiniFAT is not implemented yet") - sect = entry.isectStart - # number of sectors to write - nb_sectors = (size + (self.sectorsize-1)) // self.sectorsize - debug('nb_sectors = %d' % nb_sectors) - for i in range(nb_sectors): - # try: - # self.fp.seek(offset + self.sectorsize * sect) - # except: - # debug('sect=%d, seek=%d' % - # (sect, offset+self.sectorsize*sect)) - # raise IOError('OLE sector index out of range') - # extract one sector from data, the last one being smaller: - if i < (nb_sectors-1): - data_sector = data[i*self.sectorsize:(i+1)*self.sectorsize] - #TODO: comment this if it works - assert(len(data_sector) == self.sectorsize) - else: - data_sector = data[i*self.sectorsize:] - # TODO: comment this if it works - debug('write_stream: size=%d sectorsize=%d data_sector=%d size%%sectorsize=%d' - % (size, self.sectorsize, len(data_sector), size % self.sectorsize)) - assert(len(data_sector) % self.sectorsize == size % self.sectorsize) - self.write_sect(sect, data_sector) -# self.fp.write(data_sector) - # jump to next sector in the FAT: - try: - sect = self.fat[sect] - except IndexError: - # [PL] if pointer is out of the FAT an exception is raised - raise IOError('incorrect OLE FAT, sector index out of range') - # [PL] Last sector should be a "end of chain" marker: - if sect != ENDOFCHAIN: - raise IOError('incorrect last sector index in OLE stream') - - def get_type(self, filename): - """ - Test if given filename exists as a stream or a storage in the OLE - container, and return its type. - - :param filename: path of stream in storage tree. (see openstream for syntax) - :returns: False if object does not exist, its entry type (>0) otherwise: - - - STGTY_STREAM: a stream - - STGTY_STORAGE: a storage - - STGTY_ROOT: the root entry - """ - try: - sid = self._find(filename) - entry = self.direntries[sid] - return entry.entry_type - except: - return False - - def getmtime(self, filename): - """ - Return modification time of a stream/storage. - - :param filename: path of stream/storage in storage tree. (see openstream for - syntax) - :returns: None if modification time is null, a python datetime object - otherwise (UTC timezone) - - new in version 0.26 - """ - sid = self._find(filename) - entry = self.direntries[sid] - return entry.getmtime() - - def getctime(self, filename): - """ - Return creation time of a stream/storage. - - :param filename: path of stream/storage in storage tree. (see openstream for - syntax) - :returns: None if creation time is null, a python datetime object - otherwise (UTC timezone) - - new in version 0.26 - """ - sid = self._find(filename) - entry = self.direntries[sid] - return entry.getctime() - - def exists(self, filename): - """ - Test if given filename exists as a stream or a storage in the OLE - container. - Note: filename is case-insensitive. - - :param filename: path of stream in storage tree. (see openstream for syntax) - :returns: True if object exist, else False. - """ - try: - sid = self._find(filename) - return True - except: - return False - - def get_size(self, filename): - """ - Return size of a stream in the OLE container, in bytes. - - :param filename: path of stream in storage tree (see openstream for syntax) - :returns: size in bytes (long integer) - :exception IOError: if file not found - :exception TypeError: if this is not a stream. - """ - sid = self._find(filename) - entry = self.direntries[sid] - if entry.entry_type != STGTY_STREAM: - #TODO: Should it return zero instead of raising an exception ? - raise TypeError('object is not an OLE stream') - return entry.size - - def get_rootentry_name(self): - """ - Return root entry name. Should usually be 'Root Entry' or 'R' in most - implementations. - """ - return self.root.name - - def getproperties(self, filename, convert_time=False, no_conversion=None): - """ - Return properties described in substream. - - :param filename: path of stream in storage tree (see openstream for syntax) - :param convert_time: bool, if True timestamps will be converted to Python datetime - :param no_conversion: None or list of int, timestamps not to be converted - (for example total editing time is not a real timestamp) - - :returns: a dictionary of values indexed by id (integer) - """ - # REFERENCE: [MS-OLEPS] https://msdn.microsoft.com/en-us/library/dd942421.aspx - # make sure no_conversion is a list, just to simplify code below: - if no_conversion is None: - no_conversion = [] - # stream path as a string to report exceptions: - streampath = filename - if not isinstance(streampath, str): - streampath = '/'.join(streampath) - - fp = self.openstream(filename) - - data = {} - - try: - # header - s = fp.read(28) - clsid = _clsid(s[8:24]) - - # format id - s = fp.read(20) - fmtid = _clsid(s[:16]) - fp.seek(i32(s, 16)) - - # get section - s = b"****" + fp.read(i32(fp.read(4))-4) - # number of properties: - num_props = i32(s, 4) - except BaseException as exc: - # catch exception while parsing property header, and only raise - # a DEFECT_INCORRECT then return an empty dict, because this is not - # a fatal error when parsing the whole file - msg = 'Error while parsing properties header in stream %s: %s' % ( - repr(streampath), exc) - self.raise_defect(DEFECT_INCORRECT, msg, type(exc)) - return data - - for i in range(num_props): - try: - id = 0 # just in case of an exception - id = i32(s, 8+i*8) - offset = i32(s, 12+i*8) - type = i32(s, offset) - - debug('property id=%d: type=%d offset=%X' % (id, type, offset)) - - # test for common types first (should perhaps use - # a dictionary instead?) - - if type == VT_I2: # 16-bit signed integer - value = i16(s, offset+4) - if value >= 32768: - value = value - 65536 - elif type == VT_UI2: # 2-byte unsigned integer - value = i16(s, offset+4) - elif type in (VT_I4, VT_INT, VT_ERROR): - # VT_I4: 32-bit signed integer - # VT_ERROR: HRESULT, similar to 32-bit signed integer, - # see http://msdn.microsoft.com/en-us/library/cc230330.aspx - value = i32(s, offset+4) - elif type in (VT_UI4, VT_UINT): # 4-byte unsigned integer - value = i32(s, offset+4) # FIXME - elif type in (VT_BSTR, VT_LPSTR): - # CodePageString, see http://msdn.microsoft.com/en-us/library/dd942354.aspx - # size is a 32 bits integer, including the null terminator, and - # possibly trailing or embedded null chars - #TODO: if codepage is unicode, the string should be converted as such - count = i32(s, offset+4) - value = s[offset+8:offset+8+count-1] - # remove all null chars: - value = value.replace(b'\x00', b'') - elif type == VT_BLOB: - # binary large object (BLOB) - # see http://msdn.microsoft.com/en-us/library/dd942282.aspx - count = i32(s, offset+4) - value = s[offset+8:offset+8+count] - elif type == VT_LPWSTR: - # UnicodeString - # see http://msdn.microsoft.com/en-us/library/dd942313.aspx - # "the string should NOT contain embedded or additional trailing - # null characters." - count = i32(s, offset+4) - value = self._decode_utf16_str(s[offset+8:offset+8+count*2]) - elif type == VT_FILETIME: - value = long(i32(s, offset+4)) + (long(i32(s, offset+8)) << 32) - # FILETIME is a 64-bit int: "number of 100ns periods - # since Jan 1,1601". - if convert_time and id not in no_conversion: - debug('Converting property #%d to python datetime, value=%d=%fs' - % (id, value, float(value) / 10000000)) - # convert FILETIME to Python datetime.datetime - # inspired from http://code.activestate.com/recipes/511425-filetime-to-datetime/ - _FILETIME_null_date = datetime.datetime(1601, 1, 1, 0, 0, 0) - debug('timedelta days=%d' % (value//(10*1000000*3600*24))) - value = _FILETIME_null_date + datetime.timedelta(microseconds=value//10) - else: - # legacy code kept for backward compatibility: returns a - # number of seconds since Jan 1,1601 - value = value // 10000000 # seconds - elif type == VT_UI1: # 1-byte unsigned integer - value = i8(s[offset+4]) - elif type == VT_CLSID: - value = _clsid(s[offset+4:offset+20]) - elif type == VT_CF: - # PropertyIdentifier or ClipboardData?? - # see http://msdn.microsoft.com/en-us/library/dd941945.aspx - count = i32(s, offset+4) - value = s[offset+8:offset+8+count] - elif type == VT_BOOL: - # VARIANT_BOOL, 16 bits bool, 0x0000=Fals, 0xFFFF=True - # see http://msdn.microsoft.com/en-us/library/cc237864.aspx - value = bool(i16(s, offset+4)) - else: - value = None # everything else yields "None" - debug('property id=%d: type=%d not implemented in parser yet' % (id, type)) - - # missing: VT_EMPTY, VT_NULL, VT_R4, VT_R8, VT_CY, VT_DATE, - # VT_DECIMAL, VT_I1, VT_I8, VT_UI8, - # see http://msdn.microsoft.com/en-us/library/dd942033.aspx - - # FIXME: add support for VT_VECTOR - # VT_VECTOR is a 32 uint giving the number of items, followed by - # the items in sequence. The VT_VECTOR value is combined with the - # type of items, e.g. VT_VECTOR|VT_BSTR - # see http://msdn.microsoft.com/en-us/library/dd942011.aspx - - # print("%08x" % id, repr(value), end=" ") - # print("(%s)" % VT[i32(s, offset) & 0xFFF]) - - data[id] = value - except BaseException as exc: - # catch exception while parsing each property, and only raise - # a DEFECT_INCORRECT, because parsing can go on - msg = 'Error while parsing property id %d in stream %s: %s' % ( - id, repr(streampath), exc) - self.raise_defect(DEFECT_INCORRECT, msg, type(exc)) - - return data - - def get_metadata(self): - """ - Parse standard properties streams, return an OleMetadata object - containing all the available metadata. - (also stored in the metadata attribute of the OleFileIO object) - - new in version 0.25 - """ - self.metadata = OleMetadata() - self.metadata.parse_properties(self) - return self.metadata - -# -# -------------------------------------------------------------------- -# This script can be used to dump the directory of any OLE2 structured -# storage file. - -if __name__ == "__main__": - - # [PL] display quick usage info if launched from command-line - if len(sys.argv) <= 1: - print('olefile version %s %s - %s' % (__version__, __date__, __author__)) - print( -""" -Launched from the command line, this script parses OLE files and prints info. - -Usage: olefile.py [-d] [-c] [file2 ...] - -Options: --d : debug mode (displays a lot of debug information, for developers only) --c : check all streams (for debugging purposes) - -For more information, see http://www.decalage.info/olefile -""") - sys.exit() - - check_streams = False - for filename in sys.argv[1:]: - # try: - # OPTIONS: - if filename == '-d': - # option to switch debug mode on: - set_debug_mode(True) - continue - if filename == '-c': - # option to switch check streams mode on: - check_streams = True - continue - - ole = OleFileIO(filename)#, raise_defects=DEFECT_INCORRECT) - print("-" * 68) - print(filename) - print("-" * 68) - ole.dumpdirectory() - for streamname in ole.listdir(): - if streamname[-1][0] == "\005": - print(streamname, ": properties") - props = ole.getproperties(streamname, convert_time=True) - props = sorted(props.items()) - for k, v in props: - # [PL]: avoid to display too large or binary values: - if isinstance(v, (basestring, bytes)): - if len(v) > 50: - v = v[:50] - if isinstance(v, bytes): - # quick and dirty binary check: - for c in (1, 2, 3, 4, 5, 6, 7, 11, 12, 14, 15, 16, 17, 18, 19, 20, - 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31): - if c in bytearray(v): - v = '(binary data)' - break - print(" ", k, v) - - if check_streams: - # Read all streams to check if there are errors: - print('\nChecking streams...') - for streamname in ole.listdir(): - # print name using repr() to convert binary chars to \xNN: - print('-', repr('/'.join(streamname)), '-', end=' ') - st_type = ole.get_type(streamname) - if st_type == STGTY_STREAM: - print('size %d' % ole.get_size(streamname)) - # just try to read stream in memory: - ole.openstream(streamname) - else: - print('NOT a stream : type=%d' % st_type) - print() - -# for streamname in ole.listdir(): -# # print name using repr() to convert binary chars to \xNN: -# print('-', repr('/'.join(streamname)),'-', end=' ') -# print(ole.getmtime(streamname)) -# print() - - print('Modification/Creation times of all directory entries:') - for entry in ole.direntries: - if entry is not None: - print('- %s: mtime=%s ctime=%s' % (entry.name, - entry.getmtime(), entry.getctime())) - print() - - # parse and display metadata: - meta = ole.get_metadata() - meta.dump() - print() - # [PL] Test a few new methods: - root = ole.get_rootentry_name() - print('Root entry name: "%s"' % root) - if ole.exists('worddocument'): - print("This is a Word document.") - print("type of stream 'WordDocument':", ole.get_type('worddocument')) - print("size :", ole.get_size('worddocument')) - if ole.exists('macros/vba'): - print("This document may contain VBA macros.") - - # print parsing issues: - print('\nNon-fatal issues raised during parsing:') - if ole.parsing_issues: - for exctype, msg in ole.parsing_issues: - print('- %s: %s' % (exctype.__name__, msg)) - else: - print('None') -## except IOError as v: -## print("***", "cannot read", file, "-", v) - -# this code was developed while listening to The Wedding Present "Sea Monsters" +sys.modules[__name__] = olefile diff --git a/PIL/PSDraw.py b/PIL/PSDraw.py index d4e7b18cc..fe0823860 100644 --- a/PIL/PSDraw.py +++ b/PIL/PSDraw.py @@ -15,7 +15,7 @@ # See the README file for information on usage and redistribution. # -from PIL import EpsImagePlugin +from . import EpsImagePlugin import sys ## diff --git a/PIL/PaletteFile.py b/PIL/PaletteFile.py index ef50feefd..9ed69d687 100644 --- a/PIL/PaletteFile.py +++ b/PIL/PaletteFile.py @@ -13,7 +13,7 @@ # See the README file for information on usage and redistribution. # -from PIL._binary import o8 +from ._binary import o8 ## diff --git a/PIL/PalmImagePlugin.py b/PIL/PalmImagePlugin.py index 4f415ff7c..cb4e491c0 100644 --- a/PIL/PalmImagePlugin.py +++ b/PIL/PalmImagePlugin.py @@ -7,7 +7,8 @@ # Image plugin for Palm pixmap images (output only). ## -from PIL import Image, ImageFile, _binary +from . import Image, ImageFile +from ._binary import o8, o16be as o16b __version__ = "1.0" @@ -80,13 +81,12 @@ _Palm8BitColormapValues = ( # so build a prototype image to be used for palette resampling def build_prototype_image(): - image = Image.new("L", (1, len(_Palm8BitColormapValues),)) + image = Image.new("L", (1, len(_Palm8BitColormapValues))) image.putdata(list(range(len(_Palm8BitColormapValues)))) palettedata = () - for i in range(len(_Palm8BitColormapValues)): - palettedata = palettedata + _Palm8BitColormapValues[i] - for i in range(256 - len(_Palm8BitColormapValues)): - palettedata = palettedata + (0, 0, 0) + for colormapValue in _Palm8BitColormapValues: + palettedata += colormapValue + palettedata += (0, 0, 0)*(256 - len(_Palm8BitColormapValues)) image.putpalette(palettedata) return image @@ -109,9 +109,6 @@ _COMPRESSION_TYPES = { "scanline": 0x00, } -o8 = _binary.o8 -o16b = _binary.o16be - # # -------------------------------------------------------------------- diff --git a/PIL/PcdImagePlugin.py b/PIL/PcdImagePlugin.py index b53635a99..fa95b5008 100644 --- a/PIL/PcdImagePlugin.py +++ b/PIL/PcdImagePlugin.py @@ -15,12 +15,11 @@ # -from PIL import Image, ImageFile, _binary +from . import Image, ImageFile +from ._binary import i8 __version__ = "0.1" -i8 = _binary.i8 - ## # Image plugin for PhotoCD images. This plugin only reads the 768x512 @@ -42,8 +41,9 @@ class PcdImageFile(ImageFile.ImageFile): raise SyntaxError("not a PCD file") orientation = i8(s[1538]) & 3 + self.tile_post_rotate = None if orientation == 1: - self.tile_post_rotate = 90 # hack + self.tile_post_rotate = 90 elif orientation == 3: self.tile_post_rotate = -90 @@ -51,6 +51,13 @@ class PcdImageFile(ImageFile.ImageFile): self.size = 768, 512 # FIXME: not correct for rotated images! self.tile = [("pcd", (0, 0)+self.size, 96*2048, None)] + def load_end(self): + if self.tile_post_rotate: + # Handle rotated PCDs + self.im = self.im.rotate(self.tile_post_rotate) + self.size = self.im.size + + # # registry diff --git a/PIL/PcfFontFile.py b/PIL/PcfFontFile.py index c2006905e..eba85feb0 100644 --- a/PIL/PcfFontFile.py +++ b/PIL/PcfFontFile.py @@ -16,9 +16,8 @@ # See the README file for information on usage and redistribution. # -from PIL import Image -from PIL import FontFile -from PIL import _binary +from . import Image, FontFile +from ._binary import i8, i16le as l16, i32le as l32, i16be as b16, i32be as b32 # -------------------------------------------------------------------- # declarations @@ -42,12 +41,6 @@ BYTES_PER_ROW = [ lambda bits: ((bits+63) >> 3) & ~7, ] -i8 = _binary.i8 -l16 = _binary.i16le -l32 = _binary.i32le -b16 = _binary.i16be -b32 = _binary.i32be - def sz(s, o): return s[o:s.index(b"\0", o)] diff --git a/PIL/PcxImagePlugin.py b/PIL/PcxImagePlugin.py index 9440d5362..e3c008f4f 100644 --- a/PIL/PcxImagePlugin.py +++ b/PIL/PcxImagePlugin.py @@ -28,14 +28,11 @@ from __future__ import print_function import logging -from PIL import Image, ImageFile, ImagePalette, _binary +from . import Image, ImageFile, ImagePalette +from ._binary import i8, i16le as i16, o8, o16le as o16 logger = logging.getLogger(__name__) -i8 = _binary.i8 -i16 = _binary.i16le -o8 = _binary.o8 - __version__ = "0.6" @@ -123,8 +120,6 @@ SAVE = { "RGB": (5, 8, 3, "RGB;L"), } -o16 = _binary.o16le - def _save(im, fp, filename, check=0): diff --git a/PIL/PdfImagePlugin.py b/PIL/PdfImagePlugin.py index 7decf0ee5..b615fe1e0 100644 --- a/PIL/PdfImagePlugin.py +++ b/PIL/PdfImagePlugin.py @@ -20,8 +20,8 @@ # Image plugin for PDF images (output only). ## -from PIL import Image, ImageFile -from PIL._binary import i8 +from . import Image, ImageFile +from ._binary import i8 import io __version__ = "0.4" diff --git a/PIL/PixarImagePlugin.py b/PIL/PixarImagePlugin.py index fd002d9cf..732d8c692 100644 --- a/PIL/PixarImagePlugin.py +++ b/PIL/PixarImagePlugin.py @@ -19,16 +19,14 @@ # See the README file for information on usage and redistribution. # -from PIL import Image, ImageFile, _binary +from . import Image, ImageFile +from ._binary import i16le as i16 __version__ = "0.1" # # helpers -i16 = _binary.i16le - - def _accept(prefix): return prefix[:4] == b"\200\350\000\000" diff --git a/PIL/PngImagePlugin.py b/PIL/PngImagePlugin.py index 4975d5598..a633d4e54 100644 --- a/PIL/PngImagePlugin.py +++ b/PIL/PngImagePlugin.py @@ -38,17 +38,14 @@ import re import zlib import struct -from PIL import Image, ImageFile, ImagePalette, _binary +from . import Image, ImageFile, ImagePalette +from ._binary import i8, i16be as i16, i32be as i32, o8, o16be as o16, o32be as o32 __version__ = "0.9" logger = logging.getLogger(__name__) -i8 = _binary.i8 -i16 = _binary.i16be -i32 = _binary.i32be - -is_cid = re.compile(b"\w\w\w\w").match +is_cid = re.compile(br"\w\w\w\w").match _MAGIC = b"\211PNG\r\n\032\n" @@ -132,7 +129,7 @@ class ChunkStream(object): def call(self, cid, pos, length): "Call the appropriate chunk handler" - logger.debug("STREAM %s %s %s", cid, pos, length) + logger.debug("STREAM %r %s %s", cid, pos, length) return getattr(self, "chunk_" + cid.decode('ascii'))(pos, length) def crc(self, cid, data): @@ -148,10 +145,10 @@ class ChunkStream(object): crc1 = Image.core.crc32(data, Image.core.crc32(cid)) crc2 = i16(self.fp.read(2)), i16(self.fp.read(2)) if crc1 != crc2: - raise SyntaxError("broken PNG file (bad header checksum in %s)" + raise SyntaxError("broken PNG file (bad header checksum in %r)" % cid) except struct.error: - raise SyntaxError("broken PNG file (incomplete checksum in %s)" + raise SyntaxError("broken PNG file (incomplete checksum in %r)" % cid) def crc_skip(self, cid, data): @@ -309,7 +306,7 @@ class PngStream(ChunkStream): # Compression method 1 byte (0) # Compressed profile n bytes (zlib with deflate compression) i = s.find(b"\0") - logger.debug("iCCP profile name %s", s[:i]) + logger.debug("iCCP profile name %r", s[:i]) logger.debug("Compression method %s", i8(s[i])) comp_method = i8(s[i]) if comp_method != 0: @@ -539,7 +536,7 @@ class PngImageFile(ImageFile.ImageFile): except EOFError: break except AttributeError: - logger.debug("%s %s %s (unknown)", cid, pos, length) + logger.debug("%r %s %s (unknown)", cid, pos, length) s = ImageFile._safe_read(self.fp, length) self.png.crc(cid, s) @@ -621,10 +618,6 @@ class PngImageFile(ImageFile.ImageFile): # -------------------------------------------------------------------- # PNG writer -o8 = _binary.o8 -o16 = _binary.o16be -o32 = _binary.o32be - _OUTMODES = { # supported PIL modes, and corresponding rawmodes/bits/color combinations "1": ("1", b'\x01\x00'), @@ -722,6 +715,32 @@ def _save(im, fp, filename, chunk=putchunk, check=0): b'\0', # 11: filter category b'\0') # 12: interlace flag + chunks = [b"cHRM", b"gAMA", b"sBIT", b"sRGB", b"tIME"] + + icc = im.encoderinfo.get("icc_profile", im.info.get("icc_profile")) + if icc: + # ICC profile + # according to PNG spec, the iCCP chunk contains: + # Profile name 1-79 bytes (character string) + # Null separator 1 byte (null character) + # Compression method 1 byte (0) + # Compressed profile n bytes (zlib with deflate compression) + name = b"ICC Profile" + data = name + b"\0\0" + zlib.compress(icc) + chunk(fp, b"iCCP", data) + else: + chunks.remove(b"sRGB") + + info = im.encoderinfo.get("pnginfo") + if info: + chunks_multiple_allowed = [b"sPLT", b"iTXt", b"tEXt", b"zTXt"] + for cid, data in info.chunks: + if cid in chunks: + chunks.remove(cid) + chunk(fp, cid, data) + elif cid in chunks_multiple_allowed: + chunk(fp, cid, data) + if im.mode == "P": palette_byte_number = (2 ** bits) * 3 palette_bytes = im.im.getpalette("RGB")[:palette_byte_number] @@ -768,20 +787,11 @@ def _save(im, fp, filename, chunk=putchunk, check=0): info = im.encoderinfo.get("pnginfo") if info: + chunks = [b"bKGD", b"hIST"] for cid, data in info.chunks: - chunk(fp, cid, data) - - icc = im.encoderinfo.get("icc_profile", im.info.get("icc_profile")) - if icc: - # ICC profile - # according to PNG spec, the iCCP chunk contains: - # Profile name 1-79 bytes (character string) - # Null separator 1 byte (null character) - # Compression method 1 byte (0) - # Compressed profile n bytes (zlib with deflate compression) - name = b"ICC Profile" - data = name + b"\0\0" + zlib.compress(icc) - chunk(fp, b"iCCP", data) + if cid in chunks: + chunks.remove(cid) + chunk(fp, cid, data) ImageFile._save(im, _idat(fp, chunk), [("zip", (0, 0)+im.size, 0, rawmode)]) diff --git a/PIL/PpmImagePlugin.py b/PIL/PpmImagePlugin.py index 68073cace..b91f9912b 100644 --- a/PIL/PpmImagePlugin.py +++ b/PIL/PpmImagePlugin.py @@ -17,7 +17,7 @@ import string -from PIL import Image, ImageFile +from . import Image, ImageFile __version__ = "0.2" @@ -123,11 +123,6 @@ class PpmImageFile(ImageFile.ImageFile): self.fp.tell(), (rawmode, 0, 1))] - # ALTERNATIVE: load via builtin debug function - # self.im = Image.core.open_ppm(self.filename) - # self.mode = self.im.mode - # self.size = self.im.size - # # -------------------------------------------------------------------- diff --git a/PIL/PsdImagePlugin.py b/PIL/PsdImagePlugin.py index d06e320b0..1e4051c29 100644 --- a/PIL/PsdImagePlugin.py +++ b/PIL/PsdImagePlugin.py @@ -18,7 +18,8 @@ __version__ = "0.4" -from PIL import Image, ImageFile, ImagePalette, _binary +from . import Image, ImageFile, ImagePalette +from ._binary import i8, i16be as i16, i32be as i32 MODES = { # (photoshop mode, bits) -> (pil mode, required channels) @@ -33,13 +34,6 @@ MODES = { (9, 8): ("LAB", 3) } -# -# helpers - -i8 = _binary.i8 -i16 = _binary.i16be -i32 = _binary.i32be - # --------------------------------------------------------------------. # read PSD images diff --git a/PIL/SgiImagePlugin.py b/PIL/SgiImagePlugin.py index d2efd3e25..973c68567 100644 --- a/PIL/SgiImagePlugin.py +++ b/PIL/SgiImagePlugin.py @@ -7,9 +7,12 @@ # See "The SGI Image File Format (Draft version 0.97)", Paul Haeberli. # # +# # History: +# 2016-16-10 mb Add save method without compression # 1995-09-10 fl Created # +# Copyright (c) 2016 by Mickael Bonfill. # Copyright (c) 2008 by Karsten Hiddemann. # Copyright (c) 1997 by Secret Labs AB. # Copyright (c) 1995 by Fredrik Lundh. @@ -18,12 +21,12 @@ # -from PIL import Image, ImageFile, _binary +from . import Image, ImageFile +from ._binary import i8, o8, i16be as i16 +import struct +import os -__version__ = "0.2" - -i8 = _binary.i8 -i16 = _binary.i16be +__version__ = "0.3" def _accept(prefix): @@ -76,12 +79,79 @@ class SgiImageFile(ImageFile.ImageFile): elif compression == 1: raise ValueError("SGI RLE encoding not supported") + +def _save(im, fp, filename): + if im.mode != "RGB" and im.mode != "RGBA" and im.mode != "L": + raise ValueError("Unsupported SGI image mode") + + # Flip the image, since the origin of SGI file is the bottom-left corner + im = im.transpose(Image.FLIP_TOP_BOTTOM) + # Define the file as SGI File Format + magicNumber = 474 + # Run-Length Encoding Compression - Unsupported at this time + rle = 0 + # Byte-per-pixel precision, 1 = 8bits per pixel + bpc = 1 + # Number of dimensions (x,y,z) + dim = 3 + # X Dimension = width / Y Dimension = height + x, y = im.size + if im.mode == "L" and y == 1: + dim = 1 + elif im.mode == "L": + dim = 2 + # Z Dimension: Number of channels + z = len(im.mode) + if dim == 1 or dim == 2: + z = 1 + # Minimum Byte value + pinmin = 0 + # Maximum Byte value (255 = 8bits per pixel) + 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: + imgName = imgName.encode('ascii', 'ignore') + # Standard representation of pixel in the file + colormap = 0 + fp.write(struct.pack('>h', magicNumber)) + fp.write(o8(rle)) + fp.write(o8(bpc)) + fp.write(struct.pack('>H', dim)) + fp.write(struct.pack('>H', x)) + fp.write(struct.pack('>H', y)) + fp.write(struct.pack('>H', z)) + fp.write(struct.pack('>l', pinmin)) + fp.write(struct.pack('>l', pinmax)) + + fp.write(struct.pack('4s', b'')) # dummy + fp.write(struct.pack('79s', imgName)) # truncates to 79 chars + fp.write(struct.pack('s', b'')) # force null byte after imgname + fp.write(struct.pack('>l', colormap)) + + fp.write(struct.pack('404s', b'')) # dummy + + #assert we've got the right number of bands. + if len(im.getbands()) != z: + raise ValueError("incorrect number of bands in SGI write: %s vs %s" % + (z, len(im.getbands()))) + + for channel in im.split(): + fp.write(channel.tobytes()) + + fp.close() + + # # registry Image.register_open(SgiImageFile.format, SgiImageFile, _accept) - +Image.register_save(SgiImageFile.format, _save) +Image.register_mime(SgiImageFile.format, "image/sgi") +Image.register_mime(SgiImageFile.format, "image/rgb") Image.register_extension(SgiImageFile.format, ".bw") Image.register_extension(SgiImageFile.format, ".rgb") Image.register_extension(SgiImageFile.format, ".rgba") Image.register_extension(SgiImageFile.format, ".sgi") + +# End of file diff --git a/PIL/SpiderImagePlugin.py b/PIL/SpiderImagePlugin.py index 07f623c7b..aa332bf02 100644 --- a/PIL/SpiderImagePlugin.py +++ b/PIL/SpiderImagePlugin.py @@ -58,7 +58,7 @@ iforms = [1, 3, -11, -12, -21, -22] # There is no magic number to identify Spider files, so just check a # series of header locations to see if they have reasonable values. -# Returns no.of bytes in the header, if it is a valid Spider header, +# Returns no. of bytes in the header, if it is a valid Spider header, # otherwise returns 0 def isSpiderHeader(t): @@ -75,7 +75,7 @@ def isSpiderHeader(t): labrec = int(h[13]) # no. records in file header labbyt = int(h[22]) # total no. of bytes in header lenbyt = int(h[23]) # record length in bytes - # print "labrec = %d, labbyt = %d, lenbyt = %d" % (labrec,labbyt,lenbyt) + # print("labrec = %d, labbyt = %d, lenbyt = %d" % (labrec,labbyt,lenbyt)) if labbyt != (labrec * lenbyt): return 0 # looks like a valid header @@ -83,9 +83,8 @@ def isSpiderHeader(t): def isSpiderImage(filename): - fp = open(filename, 'rb') - f = fp.read(92) # read 23 * 4 bytes - fp.close() + with open(filename, 'rb') as fp: + f = fp.read(92) # read 23 * 4 bytes t = struct.unpack('>23f', f) # try big-endian first hdrlen = isSpiderHeader(t) if hdrlen == 0: diff --git a/PIL/SunImagePlugin.py b/PIL/SunImagePlugin.py index af63144f2..876fb73fa 100644 --- a/PIL/SunImagePlugin.py +++ b/PIL/SunImagePlugin.py @@ -17,12 +17,11 @@ # -from PIL import Image, ImageFile, ImagePalette, _binary +from . import Image, ImageFile, ImagePalette +from ._binary import i32be as i32 __version__ = "0.3" -i32 = _binary.i32be - def _accept(prefix): return len(prefix) >= 4 and i32(prefix) == 0x59a66a95 @@ -38,6 +37,21 @@ class SunImageFile(ImageFile.ImageFile): def _open(self): + # The Sun Raster file header is 32 bytes in length and has the following format: + + # typedef struct _SunRaster + # { + # DWORD MagicNumber; /* Magic (identification) number */ + # DWORD Width; /* Width of image in pixels */ + # DWORD Height; /* Height of image in pixels */ + # DWORD Depth; /* Number of bits per pixel */ + # DWORD Length; /* Size of image data in bytes */ + # DWORD Type; /* Type of raster file */ + # DWORD ColorMapType; /* Type of color map */ + # DWORD ColorMapLength; /* Size of the color map in bytes */ + # } SUNRASTER; + + # HEAD s = self.fp.read(32) if i32(s) != 0x59a66a95: @@ -48,30 +62,70 @@ 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. + 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]) + if depth == 1: self.mode, rawmode = "1", "1;I" + elif depth == 4: + self.mode, rawmode = "L", "L;4" elif depth == 8: self.mode = rawmode = "L" elif depth == 24: - self.mode, rawmode = "RGB", "BGR" + if file_type == 3: + self.mode, rawmode = "RGB", "RGB" + else: + self.mode, rawmode = "RGB", "BGR" + elif depth == 32: + if file_type == 3: + self.mode, rawmode = 'RGB', 'RGBX' + else: + self.mode, rawmode = 'RGB', 'BGRX' else: - raise SyntaxError("unsupported mode") + raise SyntaxError("Unsupported Mode/Bit Depth") - compression = i32(s[20:24]) + if palette_length: + if palette_length > 1024: + raise SyntaxError("Unsupported Color Palette Length") - if i32(s[24:28]) != 0: - length = i32(s[28:32]) - offset = offset + length - self.palette = ImagePalette.raw("RGB;L", self.fp.read(length)) + if palette_type != 1: + raise SyntaxError("Unsupported Palette Type") + + offset = offset + palette_length + self.palette = ImagePalette.raw("RGB;L", self.fp.read(palette_length)) if self.mode == "L": - self.mode = rawmode = "P" + self.mode = "P" + rawmode = rawmode.replace('L', 'P') - stride = (((self.size[0] * depth + 7) // 8) + 3) & (~3) + # 16 bit boundaries on stride + stride = ((self.size[0] * depth + 15) // 16) * 2 - if compression == 1: + # file type: Type is the version (or flavor) of the bitmap + # file. The following values are typically found in the Type + # field: + # 0000h Old + # 0001h Standard + # 0002h Byte-encoded + # 0003h RGB format + # 0004h TIFF format + # 0005h IFF format + # FFFFh Experimental + + # Old and standard are the same, except for the length tag. + # byte-encoded is run-length-encoded + # RGB looks similar to standard, but RGB byte order + # TIFF and IFF mean that they were converted from T/IFF + # Experimental means that it's something else. + # (http://www.fileformat.info/format/sunraster/egff.htm) + + if file_type in (0, 1, 3, 4, 5): self.tile = [("raw", (0, 0)+self.size, offset, (rawmode, stride))] - elif compression == 2: + elif file_type == 2: self.tile = [("sun_rle", (0, 0)+self.size, offset, rawmode)] + else: + raise SyntaxError('Unsupported Sun Raster file type') # # registry diff --git a/PIL/TarIO.py b/PIL/TarIO.py index 4f3182848..0e949ff88 100644 --- a/PIL/TarIO.py +++ b/PIL/TarIO.py @@ -14,7 +14,7 @@ # See the README file for information on usage and redistribution. # -from PIL import ContainerIO +from . import ContainerIO ## diff --git a/PIL/TgaImagePlugin.py b/PIL/TgaImagePlugin.py index a75ce2986..de2844339 100644 --- a/PIL/TgaImagePlugin.py +++ b/PIL/TgaImagePlugin.py @@ -17,7 +17,8 @@ # -from PIL import Image, ImageFile, ImagePalette, _binary +from . import Image, ImageFile, ImagePalette +from ._binary import i8, i16le as i16, o8, o16le as o16, o32le as o32 __version__ = "0.3" @@ -26,9 +27,6 @@ __version__ = "0.3" # -------------------------------------------------------------------- # Read RGA file -i8 = _binary.i8 -i16 = _binary.i16le - MODES = { # map imagetype/depth to rawmode @@ -132,10 +130,6 @@ class TgaImageFile(ImageFile.ImageFile): # -------------------------------------------------------------------- # Write TGA file -o8 = _binary.o8 -o16 = _binary.o16le -o32 = _binary.o32le - SAVE = { "1": ("1", 1, 0, 3), "L": ("L", 8, 0, 3), diff --git a/PIL/TiffImagePlugin.py b/PIL/TiffImagePlugin.py index fefed6b30..505025bb8 100644 --- a/PIL/TiffImagePlugin.py +++ b/PIL/TiffImagePlugin.py @@ -41,10 +41,8 @@ from __future__ import division, print_function -from PIL import Image, ImageFile -from PIL import ImagePalette -from PIL import _binary -from PIL import TiffTags +from . import Image, ImageFile, ImagePalette, TiffTags +from ._binary import i8, o8 import collections from fractions import Fraction @@ -71,9 +69,6 @@ IFD_LEGACY_API = True II = b"II" # little-endian (Intel style) MM = b"MM" # big-endian (Motorola style) -i8 = _binary.i8 -o8 = _binary.o8 - # # -------------------------------------------------------------------- # Read TIFF files @@ -132,7 +127,7 @@ COMPRESSION_INFO = { 34677: "tiff_sgilog24", } -COMPRESSION_INFO_REV = dict([(v, k) for (k, v) in COMPRESSION_INFO.items()]) +COMPRESSION_INFO_REV = {v: k for k, v in COMPRESSION_INFO.items()} OPEN_INFO = { # (ByteOrder, PhotoInterpretation, SampleFormat, FillOrder, BitsPerSample, @@ -278,12 +273,12 @@ class IFDRational(Rational): self._numerator = value self._val = float(1) - if type(value) == Fraction: + if isinstance(value, Fraction): self._numerator = value.numerator self._denominator = value.denominator self._val = value - if type(value) == IFDRational: + if isinstance(value, IFDRational): self._denominator = value.denominator self._numerator = value.numerator self._val = value._val @@ -294,11 +289,7 @@ class IFDRational(Rational): return elif denominator == 1: - if sys.hexversion < 0x2070000 and type(value) == float: - # python 2.6 is different. - self._val = Fraction.from_float(value) - else: - self._val = Fraction(value) + self._val = Fraction(value) else: self._val = Fraction(value, denominator) @@ -342,7 +333,7 @@ class IFDRational(Rational): 'rfloordiv','mod','rmod', 'pow','rpow', 'pos', 'neg', 'abs', 'trunc', 'lt', 'gt', 'le', 'ge', 'nonzero', 'ceil', 'floor', 'round'] - print "\n".join("__%s__ = _delegate('__%s__')" % (s,s) for s in a) + print("\n".join("__%s__ = _delegate('__%s__')" % (s,s) for s in a)) """ __add__ = _delegate('__add__') @@ -573,7 +564,7 @@ class ImageFileDirectory_v2(collections.MutableMapping): def _register_loader(idx, size): def decorator(func): - from PIL.TiffTags import TYPES + from .TiffTags import TYPES if func.__name__.startswith("load_"): TYPES[idx] = func.__name__[5:].replace("_", " ") _load_dispatch[idx] = size, func @@ -587,12 +578,12 @@ class ImageFileDirectory_v2(collections.MutableMapping): return decorator def _register_basic(idx_fmt_name): - from PIL.TiffTags import TYPES + from .TiffTags import TYPES idx, fmt, name = idx_fmt_name TYPES[idx] = name size = struct.calcsize("=" + fmt) _load_dispatch[idx] = size, lambda self, data, legacy_api=True: ( - self._unpack("{0}{1}".format(len(data) // size, fmt), data)) + self._unpack("{}{}".format(len(data) // size, fmt), data)) _write_dispatch[idx] = lambda self, *values: ( b"".join(self._pack(fmt, value) for value in values)) @@ -624,7 +615,7 @@ class ImageFileDirectory_v2(collections.MutableMapping): @_register_loader(5, 8) def load_rational(self, data, legacy_api=True): - vals = self._unpack("{0}L".format(len(data) // 4), data) + vals = self._unpack("{}L".format(len(data) // 4), data) combine = lambda a, b: (a, b) if legacy_api else IFDRational(a, b) return tuple(combine(num, denom) for num, denom in zip(vals[::2], vals[1::2])) @@ -644,7 +635,7 @@ class ImageFileDirectory_v2(collections.MutableMapping): @_register_loader(10, 8) def load_signed_rational(self, data, legacy_api=True): - vals = self._unpack("{0}l".format(len(data) // 4), data) + vals = self._unpack("{}l".format(len(data) // 4), data) combine = lambda a, b: (a, b) if legacy_api else IFDRational(a, b) return tuple(combine(num, denom) for num, denom in zip(vals[::2], vals[1::2])) @@ -804,7 +795,7 @@ class ImageFileDirectory_v1(ImageFileDirectory_v2): ifd = ImageFileDirectory_v1() ifd[key] = 'Some Data' ifd.tagtype[key] = 2 - print ifd[key] + print(ifd[key]) ('Some Data',) Also contains a dictionary of tag types as read from the tiff image file, @@ -1010,9 +1001,6 @@ class TiffImageFile(ImageFile.ImageFile): # Section 14: Differencing Predictor self.decoderconfig = (self.tag_v2[PREDICTOR],) - if ICCPROFILE in self.tag_v2: - self.info['icc_profile'] = self.tag_v2[ICCPROFILE] - return args def load(self): @@ -1056,7 +1044,7 @@ class TiffImageFile(ImageFile.ImageFile): # io.BytesIO have a fileno, but returns an IOError if # it doesn't use a file descriptor. fp = False - + if fp: args[2] = fp @@ -1175,11 +1163,15 @@ class TiffImageFile(ImageFile.ImageFile): yres = self.tag_v2.get(Y_RESOLUTION, 1) if xres and yres: - resunit = self.tag_v2.get(RESOLUTION_UNIT, 1) + resunit = self.tag_v2.get(RESOLUTION_UNIT) if resunit == 2: # dots per inch self.info["dpi"] = xres, yres elif resunit == 3: # dots per centimeter. convert to dpi self.info["dpi"] = xres * 2.54, yres * 2.54 + elif resunit == None: # used to default to 1, but now 2) + self.info["dpi"] = xres, yres + # For backward compatibility, we also preserve the old behavior. + self.info["resolution"] = xres, yres else: # No absolute unit of measurement self.info["resolution"] = xres, yres @@ -1201,7 +1193,7 @@ class TiffImageFile(ImageFile.ImageFile): "tiff_sgilog24", "tiff_raw_16"]: # if DEBUG: - # print "Activating g4 compression for whole file" + # print("Activating g4 compression for whole file") # Decoder expects entire file as one tile. # There's a buffer size limit in load (64k) @@ -1246,12 +1238,12 @@ class TiffImageFile(ImageFile.ImageFile): a = None else: - for i in range(len(offsets)): + for i, offset in enumerate(offsets): a = self._decoder(rawmode, l, i) self.tile.append( (self._compression, (0, min(y, ysize), w, min(y+h, ysize)), - offsets[i], a)) + offset, a)) if DEBUG: print("tiles: ", self.tile) y = y + h @@ -1285,6 +1277,10 @@ class TiffImageFile(ImageFile.ImageFile): print("- unsupported data organization") raise SyntaxError("unknown data organization") + # Fix up info. + if ICCPROFILE in self.tag_v2: + self.info['icc_profile'] = self.tag_v2[ICCPROFILE] + # fixup palette descriptor if self.mode == "P": @@ -1366,10 +1362,10 @@ def _save(im, fp, filename): ifd[key] = im.tag_v2[key] ifd.tagtype[key] = im.tag_v2.tagtype[key] - # preserve ICC profile (should also work when saving other formats - # which support profiles as TIFF) -- 2008-06-06 Florian Hoech - if "icc_profile" in im.info: - ifd[ICCPROFILE] = im.info["icc_profile"] + # preserve ICC profile (should also work when saving other formats + # which support profiles as TIFF) -- 2008-06-06 Florian Hoech + if "icc_profile" in im.info: + ifd[ICCPROFILE] = im.info["icc_profile"] for key, name in [(IMAGEDESCRIPTION, "description"), (X_RESOLUTION, "resolution"), @@ -1518,7 +1514,7 @@ class AppendingTiffWriter: # JPEGQTables = 519 # JPEGDCTables = 520 # JPEGACTables = 521 - Tags = set((273, 288, 324, 519, 520, 521)) + Tags = {273, 288, 324, 519, 520, 521} def __init__(self, fn, new=False): if hasattr(fn, 'read'): diff --git a/PIL/TiffTags.py b/PIL/TiffTags.py index edb28b9ec..6644b237e 100644 --- a/PIL/TiffTags.py +++ b/PIL/TiffTags.py @@ -418,13 +418,13 @@ TYPES = {} # some of these are not in our TAGS_V2 dict and were included from tiff.h -LIBTIFF_CORE = set([255, 256, 257, 258, 259, 262, 263, 266, 274, 277, - 278, 280, 281, 340, 341, 282, 283, 284, 286, 287, - 296, 297, 321, 320, 338, 32995, 322, 323, 32998, - 32996, 339, 32997, 330, 531, 530, 301, 532, 333, - # as above - 269 # this has been in our tests forever, and works - ]) +LIBTIFF_CORE = {255, 256, 257, 258, 259, 262, 263, 266, 274, 277, + 278, 280, 281, 340, 341, 282, 283, 284, 286, 287, + 296, 297, 321, 320, 338, 32995, 322, 323, 32998, + 32996, 339, 32997, 330, 531, 530, 301, 532, 333, + # as above + 269 # this has been in our tests forever, and works + } LIBTIFF_CORE.remove(320) # Array of short, crashes LIBTIFF_CORE.remove(301) # Array of short, crashes diff --git a/PIL/WalImageFile.py b/PIL/WalImageFile.py index b0b1e684a..a17238a5a 100644 --- a/PIL/WalImageFile.py +++ b/PIL/WalImageFile.py @@ -23,7 +23,8 @@ from __future__ import print_function -from PIL import Image, _binary +from . import Image +from ._binary import i32le as i32 try: import builtins @@ -31,8 +32,6 @@ except ImportError: import __builtin__ builtins = __builtin__ -i32 = _binary.i32le - def open(filename): """ @@ -47,33 +46,35 @@ def open(filename): # FIXME: modify to return a WalImageFile instance instead of # plain Image object ? + def imopen(fp): + # read header fields + header = fp.read(32+24+32+12) + size = i32(header, 32), i32(header, 36) + offset = i32(header, 40) + + # load pixel data + fp.seek(offset) + + Image._decompression_bomb_check(size) + im = Image.frombytes("P", size, fp.read(size[0] * size[1])) + im.putpalette(quake2palette) + + im.format = "WAL" + im.format_description = "Quake2 Texture" + + # strings are null-terminated + im.info["name"] = header[:32].split(b"\0", 1)[0] + next_name = header[56:56+32].split(b"\0", 1)[0] + if next_name: + im.info["next_name"] = next_name + + return im + if hasattr(filename, "read"): - fp = filename + return imopen(filename) else: - fp = builtins.open(filename, "rb") - - # read header fields - header = fp.read(32+24+32+12) - size = i32(header, 32), i32(header, 36) - offset = i32(header, 40) - - # load pixel data - fp.seek(offset) - - im = Image.frombytes("P", size, fp.read(size[0] * size[1])) - im.putpalette(quake2palette) - - im.format = "WAL" - im.format_description = "Quake2 Texture" - - # strings are null-terminated - im.info["name"] = header[:32].split(b"\0", 1)[0] - next_name = header[56:56+32].split(b"\0", 1)[0] - if next_name: - im.info["next_name"] = next_name - - return im - + with builtins.open(filename, "rb") as fp: + return imopen(fp) quake2palette = ( # default palette taken from piffo 0.93 by Hans Häggström diff --git a/PIL/WebPImagePlugin.py b/PIL/WebPImagePlugin.py index 6837b53be..b93e0d3e7 100644 --- a/PIL/WebPImagePlugin.py +++ b/PIL/WebPImagePlugin.py @@ -1,7 +1,5 @@ -from PIL import Image -from PIL import ImageFile +from . import Image, ImageFile, _webp from io import BytesIO -from PIL import _webp _VALID_WEBP_MODES = { @@ -43,7 +41,7 @@ class WebPImageFile(ImageFile.ImageFile): self.tile = [("raw", (0, 0) + self.size, 0, self.mode)] def _getexif(self): - from PIL.JpegImagePlugin import _getexif + from .JpegImagePlugin import _getexif return _getexif(self) diff --git a/PIL/WmfImagePlugin.py b/PIL/WmfImagePlugin.py index 9416035c0..f7076c0d9 100644 --- a/PIL/WmfImagePlugin.py +++ b/PIL/WmfImagePlugin.py @@ -14,8 +14,17 @@ # # See the README file for information on usage and redistribution. # +# WMF/EMF reference documentation: +# https://winprotocoldoc.blob.core.windows.net/productionwindowsarchives/MS-WMF/[MS-WMF].pdf +# http://wvware.sourceforge.net/caolan/index.html +# http://wvware.sourceforge.net/caolan/ora-wmf.html + +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 PIL import Image, ImageFile, _binary __version__ = "0.2" @@ -53,20 +62,6 @@ if hasattr(Image.core, "drawwmf"): register_handler(WmfHandler()) -# -------------------------------------------------------------------- - -word = _binary.i16le - - -def short(c, o=0): - v = word(c, o) - if v >= 32768: - v -= 65536 - return v - -dword = _binary.i32le - - # # -------------------------------------------------------------------- # Read WMF file @@ -111,7 +106,7 @@ class WmfStubImageFile(ImageFile.StubImageFile): self.info["dpi"] = 72 - # print self.mode, self.size, self.info + # print(self.mode, self.size, self.info) # sanity check (standard metafile header) if s[22:26] != b"\x01\x00\t\x00": @@ -121,13 +116,13 @@ class WmfStubImageFile(ImageFile.StubImageFile): # enhanced metafile # get bounding box - x0 = dword(s, 8) - y0 = dword(s, 12) - x1 = dword(s, 16) - y1 = dword(s, 20) + x0 = _long(s, 8) + y0 = _long(s, 12) + x1 = _long(s, 16) + y1 = _long(s, 20) # get frame (in 0.01 millimeter units) - frame = dword(s, 24), dword(s, 28), dword(s, 32), dword(s, 36) + frame = _long(s, 24), _long(s, 28), _long(s, 32), _long(s, 36) # normalize size to 72 dots per inch size = x1 - x0, y1 - y0 diff --git a/PIL/XVThumbImagePlugin.py b/PIL/XVThumbImagePlugin.py index 0034ff7d0..6929e8b82 100644 --- a/PIL/XVThumbImagePlugin.py +++ b/PIL/XVThumbImagePlugin.py @@ -17,12 +17,11 @@ # FIXME: make save work (this requires quantization support) # -from PIL import Image, ImageFile, ImagePalette, _binary +from . import Image, ImageFile, ImagePalette +from ._binary import o8 __version__ = "0.1" -o8 = _binary.o8 - _MAGIC = b"P7 332" # standard color palette for thumbnails (RGB332) diff --git a/PIL/XbmImagePlugin.py b/PIL/XbmImagePlugin.py index bca882866..b43fbef50 100644 --- a/PIL/XbmImagePlugin.py +++ b/PIL/XbmImagePlugin.py @@ -20,13 +20,13 @@ # import re -from PIL import Image, ImageFile +from . import Image, ImageFile __version__ = "0.6" # XBM header xbm_head = re.compile( - b"\s*#define[ \t]+.*_width[ \t]+(?P[0-9]+)[\r\n]+" + br"\s*#define[ \t]+.*_width[ \t]+(?P[0-9]+)[\r\n]+" b"#define[ \t]+.*_height[ \t]+(?P[0-9]+)[\r\n]+" b"(?P" b"#define[ \t]+[^_]*_x_hot[ \t]+(?P[0-9]+)[\r\n]+" diff --git a/PIL/XpmImagePlugin.py b/PIL/XpmImagePlugin.py index 556adb8f7..87736aff9 100644 --- a/PIL/XpmImagePlugin.py +++ b/PIL/XpmImagePlugin.py @@ -16,8 +16,8 @@ import re -from PIL import Image, ImageFile, ImagePalette -from PIL._binary import i8, o8 +from . import Image, ImageFile, ImagePalette +from ._binary import i8, o8 __version__ = "0.2" diff --git a/PIL/__init__.py b/PIL/__init__.py index 561a13a67..7a32533d9 100644 --- a/PIL/__init__.py +++ b/PIL/__init__.py @@ -12,7 +12,7 @@ # ;-) VERSION = '1.1.7' # PIL version -PILLOW_VERSION = '3.4.0.dev0' # Pillow +PILLOW_VERSION = '4.1.0.dev0' # Pillow __version__ = PILLOW_VERSION diff --git a/PIL/_binary.py b/PIL/_binary.py index 1cbe59dea..17ee67b11 100644 --- a/PIL/_binary.py +++ b/PIL/_binary.py @@ -28,26 +28,43 @@ else: # Input, le = little endian, be = big endian -# TODO: replace with more readable struct.unpack equivalent def i16le(c, o=0): """ - Converts a 2-bytes (16 bits) string to an integer. + Converts a 2-bytes (16 bits) string to an unsigned integer. c: string containing bytes to convert o: offset of bytes to convert in string """ return unpack("H", c[o:o+2])[0] diff --git a/PIL/features.py b/PIL/features.py index fd87f094f..fb8e4371b 100644 --- a/PIL/features.py +++ b/PIL/features.py @@ -1,4 +1,4 @@ -from PIL import Image +from . import Image modules = { "pil": "PIL._imaging", @@ -17,7 +17,7 @@ def check_module(feature): module = modules[feature] method_to_call = None - if type(module) is tuple: + if isinstance(module, tuple): module, method_to_call = module try: diff --git a/Tests/README.rst b/Tests/README.rst index a7212cb3d..0985e0f26 100644 --- a/Tests/README.rst +++ b/Tests/README.rst @@ -10,10 +10,6 @@ Install:: pip install coverage nose -If you're using Python 2.6, there's one additional dependency:: - - pip install unittest2 - Execution --------- diff --git a/Tests/check_j2k_overflow.py b/Tests/check_j2k_overflow.py index 474b49948..bec4ea694 100644 --- a/Tests/check_j2k_overflow.py +++ b/Tests/check_j2k_overflow.py @@ -7,13 +7,8 @@ class TestJ2kEncodeOverflow(PillowTestCase): im = Image.new('RGBA', (1024, 131584)) target = self.tempfile('temp.jpc') - try: + with self.assertRaises(IOError): im.save(target) - self.assertTrue(False, "Expected IOError, save succeeded?") - except IOError as err: - self.assertTrue(True, "IOError is expected") - except Exception as err: - self.assertTrue(False, "Expected IOError, got %s" % type(err)) if __name__ == '__main__': unittest.main() diff --git a/Tests/check_libtiff_segfault.py b/Tests/check_libtiff_segfault.py index c2e01dd55..6611648a5 100644 --- a/Tests/check_libtiff_segfault.py +++ b/Tests/check_libtiff_segfault.py @@ -10,13 +10,9 @@ class TestLibtiffSegfault(PillowTestCase): libtiff >= 4.0.0 """ - try: + with self.assertRaises(IOError): im = Image.open(TEST_FILE) im.load() - except IOError: - self.assertTrue(True, "Got expected IOError") - except Exception: - self.fail("Should have returned IOError") if __name__ == '__main__': diff --git a/Tests/helper.py b/Tests/helper.py index 9f1501249..f8eed3e2d 100644 --- a/Tests/helper.py +++ b/Tests/helper.py @@ -5,11 +5,22 @@ from __future__ import print_function import sys import tempfile import os +import unittest -if sys.version_info[:2] <= (2, 6): - import unittest2 as unittest -else: - import unittest +from PIL import Image, ImageMath + + +def convert_to_comparable(a, b): + new_a, new_b = a, b + if a.mode == 'P': + new_a = Image.new('L', a.size) + new_b = Image.new('L', b.size) + new_a.putdata(a.getdata()) + new_b.putdata(b.getdata()) + elif a.mode == 'I;16': + new_a = a.convert('I') + new_b = b.convert('I') + return new_a, new_b class PillowTestCase(unittest.TestCase): @@ -49,7 +60,7 @@ class PillowTestCase(unittest.TestCase): len(a), len(b), msg or "got length %s, expected %s" % (len(a), len(b))) self.assertTrue( - all([x == y for x, y in zip(a, b)]), + all(x == y for x, y in zip(a, b)), msg or "got %s, expected %s" % (a, b)) except: self.assertEqual(a, b, msg) @@ -84,14 +95,13 @@ class PillowTestCase(unittest.TestCase): a.size, b.size, msg or "got size %r, expected %r" % (a.size, b.size)) + a, b = convert_to_comparable(a, b) + diff = 0 - try: - ord(b'0') - for abyte, bbyte in zip(a.tobytes(), b.tobytes()): - diff += abs(ord(abyte)-ord(bbyte)) - except: - for abyte, bbyte in zip(a.tobytes(), b.tobytes()): - diff += abs(abyte-bbyte) + for ach, bch in zip(a.split(), b.split()): + chdiff = ImageMath.eval("abs(a - b)", a=ach, b=bch).convert('L') + diff += sum(i * num for i, num in enumerate(chdiff.histogram())) + ave_diff = float(diff)/(a.size[0]*a.size[1]) self.assertGreaterEqual( epsilon, ave_diff, @@ -139,9 +149,13 @@ class PillowTestCase(unittest.TestCase): if skip: self.skipTest(msg or "Known Bad Test") + def shortDescription(self): + # Prevents `nose -v` printing docstrings + return None + def tempfile(self, template): assert template[:5] in ("temp.", "temp_") - (fd, path) = tempfile.mkstemp(template[4:], template[:4]) + fd, path = tempfile.mkstemp(template[4:], template[:4]) os.close(fd) self.addCleanup(self.delete_tempfile, path) diff --git a/Tests/images/bmp/q/rgb32bf-xbgr.bmp b/Tests/images/bmp/q/rgb32bf-xbgr.bmp new file mode 100644 index 000000000..c6c05e148 Binary files /dev/null and b/Tests/images/bmp/q/rgb32bf-xbgr.bmp differ diff --git a/Tests/images/drawing.emf b/Tests/images/drawing.emf new file mode 100644 index 000000000..ef751cd51 Binary files /dev/null and b/Tests/images/drawing.emf differ diff --git a/Tests/images/drawing.wmf b/Tests/images/drawing.wmf new file mode 100644 index 000000000..d9cfda453 Binary files /dev/null and b/Tests/images/drawing.wmf differ diff --git a/Tests/images/drawing_emf_ref.png b/Tests/images/drawing_emf_ref.png new file mode 100644 index 000000000..3e66cbd44 Binary files /dev/null and b/Tests/images/drawing_emf_ref.png differ diff --git a/Tests/images/drawing_wmf_ref.png b/Tests/images/drawing_wmf_ref.png new file mode 100644 index 000000000..207160de0 Binary files /dev/null and b/Tests/images/drawing_wmf_ref.png differ diff --git a/Tests/images/hopper.bw b/Tests/images/hopper.bw index c9dabf64a..1503168ab 100644 Binary files a/Tests/images/hopper.bw and b/Tests/images/hopper.bw differ diff --git a/Tests/images/hopper.msp b/Tests/images/hopper.msp index 91d9a147f..18215f1af 100644 Binary files a/Tests/images/hopper.msp and b/Tests/images/hopper.msp differ diff --git a/Tests/images/hopper.rgb b/Tests/images/hopper.rgb index a72fc5b15..7c6d4ce18 100644 Binary files a/Tests/images/hopper.rgb and b/Tests/images/hopper.rgb differ diff --git a/Tests/images/hopper.sgi b/Tests/images/hopper.sgi new file mode 100644 index 000000000..a72fc5b15 Binary files /dev/null and b/Tests/images/hopper.sgi differ diff --git a/Tests/images/hopper_256x256.ico b/Tests/images/hopper_256x256.ico new file mode 100644 index 000000000..2c08b1f3c Binary files /dev/null and b/Tests/images/hopper_256x256.ico differ diff --git a/Tests/images/hopper_45.png b/Tests/images/hopper_45.png new file mode 100644 index 000000000..a6e614283 Binary files /dev/null and b/Tests/images/hopper_45.png differ diff --git a/Tests/images/l2rgb_read.bmp b/Tests/images/l2rgb_read.bmp new file mode 100644 index 000000000..838e3226b Binary files /dev/null and b/Tests/images/l2rgb_read.bmp differ diff --git a/Tests/images/negative_size.ppm b/Tests/images/negative_size.ppm new file mode 100755 index 000000000..257b8c29c Binary files /dev/null and b/Tests/images/negative_size.ppm differ diff --git a/Tests/images/sunraster.im1 b/Tests/images/sunraster.im1 new file mode 100644 index 000000000..82c92bca9 Binary files /dev/null and b/Tests/images/sunraster.im1 differ diff --git a/Tests/images/sunraster.im1.png b/Tests/images/sunraster.im1.png new file mode 100644 index 000000000..4390dd6c3 Binary files /dev/null and b/Tests/images/sunraster.im1.png differ diff --git a/Tests/images/transparent.sgi b/Tests/images/transparent.sgi index 482572df5..0003cf33f 100644 Binary files a/Tests/images/transparent.sgi and b/Tests/images/transparent.sgi differ diff --git a/Tests/make_hash.py b/Tests/make_hash.py index 6d700addf..4412f65be 100644 --- a/Tests/make_hash.py +++ b/Tests/make_hash.py @@ -51,10 +51,10 @@ for i0 in range(65556): print() -# print check(min_size, min_start) +# print(check(min_size, min_start)) print("#define ACCESS_TABLE_SIZE", min_size) print("#define ACCESS_TABLE_HASH", min_start) # for m in modes: -# print m, "=>", hash(m, min_start) % min_size +# print(m, "=>", hash(m, min_start) % min_size) diff --git a/Tests/test_bmp_reference.py b/Tests/test_bmp_reference.py index b6f0b4564..fa4571e60 100644 --- a/Tests/test_bmp_reference.py +++ b/Tests/test_bmp_reference.py @@ -1,3 +1,4 @@ +from __future__ import print_function from helper import unittest, PillowTestCase from PIL import Image @@ -21,18 +22,32 @@ class TestBmpReference(PillowTestCase): im.load() except Exception: # as msg: pass - # print ("Bad Image %s: %s" %(f,msg)) + # print("Bad Image %s: %s" %(f,msg)) def test_questionable(self): """ These shouldn't crash/dos, but it's not well defined that these are in spec """ + supported = [ + "pal8os2v2.bmp", + "rgb24prof.bmp", + "pal1p1.bmp", + "pal8offs.bmp", + "rgb24lprof.bmp", + "rgb32fakealpha.bmp", + "rgb24largepal.bmp", + "pal8os2sp.bmp", + "rgb32bf-xbgr.bmp", + ] for f in self.get_files('q'): try: im = Image.open(f) im.load() + if os.path.basename(f) not in supported: + print("Please add %s to the partially supported bmp specs." % f) except Exception: # as msg: - pass - # print ("Bad Image %s: %s" %(f,msg)) + if os.path.basename(f) in supported: + raise + # print("Bad Image %s: %s" %(f,msg)) def test_good(self): """ These should all work. There's a set of target files in the diff --git a/Tests/test_features.py b/Tests/test_features.py index 7861693b2..b9afe9b1d 100644 --- a/Tests/test_features.py +++ b/Tests/test_features.py @@ -13,8 +13,8 @@ class TestFeatures(PillowTestCase): self.assertTrue(features.check_codec(feature) in [True, False]) def test_supported_features(self): - self.assertTrue(type(features.get_supported_modules()) is list) - self.assertTrue(type(features.get_supported_codecs()) is list) + self.assertIsInstance(features.get_supported_modules(), list) + self.assertIsInstance(features.get_supported_codecs(), list) def test_unsupported_codec(self): # Arrange diff --git a/Tests/test_file_fli.py b/Tests/test_file_fli.py index 5ce4839bc..a49301de1 100644 --- a/Tests/test_file_fli.py +++ b/Tests/test_file_fli.py @@ -6,7 +6,6 @@ from PIL import Image, FliImagePlugin # created as an export of a palette image from Gimp2.6 # save as...-> hopper.fli, default options. test_file = "Tests/images/hopper.fli" -data = open(test_file, "rb").read() class TestFileFli(PillowTestCase): diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index dbe4f34fd..d987f6851 100644 --- a/Tests/test_file_gif.py +++ b/Tests/test_file_gif.py @@ -56,11 +56,11 @@ class TestFileGif(PillowTestCase): # 256 color Palette image, posterize to > 128 and < 128 levels # Size bigger and smaller than 512x512 # Check the palette for number of colors allocated. - # Check for correctness after conversion back to RGB + # Check for correctness after conversion back to RGB def check(colors, size, expected_palette_length): # make an image with empty colors in the start of the palette range im = Image.frombytes('P', (colors,colors), - bytes(bytearray(list(range(256-colors,256))*colors))) + bytes(bytearray(range(256-colors,256))*colors)) im = im.resize((size,size)) outfile = BytesIO() im.save(outfile, 'GIF') @@ -70,7 +70,7 @@ class TestFileGif(PillowTestCase): # check palette length palette_length = max(i+1 for i,v in enumerate(reloaded.histogram()) if v) self.assertEqual(expected_palette_length, palette_length) - + self.assert_image_equal(im.convert('RGB'), reloaded.convert('RGB')) @@ -271,26 +271,68 @@ class TestFileGif(PillowTestCase): duration = 1000 out = self.tempfile('temp.gif') - fp = open(out, "wb") - im = Image.new('L', (100, 100), '#000') - for s in GifImagePlugin.getheader(im)[0] + GifImagePlugin.getdata(im, duration=duration): - fp.write(s) - fp.write(b";") - fp.close() + with open(out, "wb") as fp: + im = Image.new('L', (100, 100), '#000') + for s in GifImagePlugin.getheader(im)[0] + GifImagePlugin.getdata(im, duration=duration): + fp.write(s) + fp.write(b";") reread = Image.open(out) self.assertEqual(reread.info['duration'], duration) + def test_multiple_duration(self): + duration_list = [1000, 2000, 3000] + + out = self.tempfile('temp.gif') + im_list = [ + Image.new('L', (100, 100), '#000'), + Image.new('L', (100, 100), '#111'), + Image.new('L', (100, 100), '#222'), + ] + + #duration as list + im_list[0].save( + out, + save_all=True, + append_images=im_list[1:], + duration=duration_list + ) + reread = Image.open(out) + + for duration in duration_list: + self.assertEqual(reread.info['duration'], duration) + try: + reread.seek(reread.tell() + 1) + except EOFError: + pass + + # duration as tuple + im_list[0].save( + out, + save_all=True, + append_images=im_list[1:], + duration=tuple(duration_list) + ) + reread = Image.open(out) + + for duration in duration_list: + self.assertEqual(reread.info['duration'], duration) + try: + reread.seek(reread.tell() + 1) + except EOFError: + pass + + + def test_number_of_loops(self): number_of_loops = 2 out = self.tempfile('temp.gif') - fp = open(out, "wb") - im = Image.new('L', (100, 100), '#000') - for s in GifImagePlugin.getheader(im)[0] + GifImagePlugin.getdata(im, loop=number_of_loops): - fp.write(s) - fp.write(b";") - fp.close() + with open(out, "wb") as fp: + im = Image.new('L', (100, 100), '#000') + for s in GifImagePlugin.getheader(im)[0] + GifImagePlugin.getdata(im, loop=number_of_loops): + fp.write(s) + fp.write(b";") reread = Image.open(out) self.assertEqual(reread.info['loop'], number_of_loops) @@ -338,7 +380,7 @@ class TestFileGif(PillowTestCase): self.assertEqual(reread.info["version"], b"GIF87a") # Test that a GIF89a image is also saved in that format - im.info["version"] = "GIF89a" + im.info["version"] = b"GIF89a" im.save(out) reread = Image.open(out) self.assertEqual(reread.info["version"], b"GIF87a") @@ -362,5 +404,28 @@ class TestFileGif(PillowTestCase): reread = Image.open(out) self.assertEqual(reread.n_frames, 10) + def test_transparent_optimize(self): + # from issue #2195, if the transparent color is incorrectly + # optimized out, gif loses transparency Need a palette that + # isn't using the 0 color, and one that's > 128 items where + # the transparent color is actually the top palette entry to + # trigger the bug. + + from PIL import ImagePalette + + data = bytes(bytearray(range(1,254))) + palette = ImagePalette.ImagePalette("RGB", list(range(256))*3) + + im = Image.new('L', (253,1)) + im.frombytes(data) + im.putpalette(palette) + + out = self.tempfile('temp.gif') + im.save(out, transparency=253) + reloaded = Image.open(out) + + self.assertEqual(reloaded.info['transparency'], 253) + + if __name__ == '__main__': unittest.main() diff --git a/Tests/test_file_icns.py b/Tests/test_file_icns.py index 7332db964..92fe136f2 100644 --- a/Tests/test_file_icns.py +++ b/Tests/test_file_icns.py @@ -6,7 +6,6 @@ import sys # sample icon file TEST_FILE = "Tests/images/pillow.icns" -data = open(TEST_FILE, "rb").read() enable_jpeg2k = hasattr(Image.core, 'jp2klib_version') diff --git a/Tests/test_file_ico.py b/Tests/test_file_ico.py index 806cff66f..3904340f3 100644 --- a/Tests/test_file_ico.py +++ b/Tests/test_file_ico.py @@ -5,7 +5,6 @@ from PIL import Image, IcoImagePlugin # sample ppm stream TEST_ICO_FILE = "Tests/images/hopper.ico" -TEST_DATA = open(TEST_ICO_FILE, "rb").read() class TestFileIco(PillowTestCase): @@ -49,6 +48,37 @@ class TestFileIco(PillowTestCase): self.assert_image_equal(reloaded, hopper().resize((32, 32), Image.LANCZOS)) + def test_save_256x256(self): + """Issue #2264 https://github.com/python-pillow/Pillow/issues/2264""" + # Arrange + im = Image.open("Tests/images/hopper_256x256.ico") + outfile = self.tempfile("temp_saved_hopper_256x256.ico") + + # Act + im.save(outfile) + im_saved = Image.open(outfile) + + # Assert + self.assertEqual(im_saved.size, (256, 256)) + + def test_only_save_relevant_sizes(self): + """Issue #2266 https://github.com/python-pillow/Pillow/issues/2266 + Should save in 16x16, 24x24, 32x32, 48x48 sizes + and not in 16x16, 24x24, 32x32, 48x48, 48x48, 48x48, 48x48 sizes + """ + # Arrange + im = Image.open("Tests/images/python.ico") # 16x16, 32x32, 48x48 + outfile = self.tempfile("temp_saved_python.ico") + + # Act + im.save(outfile) + im_saved = Image.open(outfile) + + # Assert + self.assertEqual( + im_saved.info['sizes'], + set([(16, 16), (24, 24), (32, 32), (48, 48)])) + if __name__ == '__main__': unittest.main() diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index 1b34b42c6..b703598c5 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -1,7 +1,6 @@ from helper import unittest, PillowTestCase, hopper, py3 from helper import djpeg_available, cjpeg_available -import random from io import BytesIO import os @@ -29,6 +28,15 @@ class TestFileJpeg(PillowTestCase): im.bytes = test_bytes # for testing only return im + def gen_random_image(self, size, mode='RGB'): + """ Generates a very hard to compress file + :param size: tuple + :param mode: optional image mode + + """ + return Image.frombytes(mode, size, + os.urandom(size[0]*size[1]*len(mode))) + def test_sanity(self): # internal version number @@ -159,15 +167,16 @@ class TestFileJpeg(PillowTestCase): def test_progressive_large_buffer_highest_quality(self): f = self.tempfile('temp.jpg') - if py3: - a = bytes(random.randint(0, 255) for _ in range(256 * 256 * 3)) - else: - a = b''.join(chr(random.randint(0, 255)) for _ in range( - 256 * 256 * 3)) - im = Image.frombuffer("RGB", (256, 256), a, "raw", "RGB", 0, 1) + im = self.gen_random_image((255,255)) # this requires more bytes than pixels in the image im.save(f, format="JPEG", progressive=True, quality=100) + def test_progressive_cmyk_buffer(self): + # Issue 2272, quality 90 cmyk image is tripping the large buffer bug. + f = BytesIO() + im = self.gen_random_image((256,256), 'CMYK') + im.save(f, format='JPEG', progressive=True, quality=94) + def test_large_exif(self): # https://github.com/python-pillow/Pillow/issues/148 f = self.tempfile('temp.jpg') @@ -436,14 +445,7 @@ class TestFileJpeg(PillowTestCase): self.assertEqual(tag_ids['RelatedImageLength'], 0x1002) def test_MAXBLOCK_scaling(self): - def gen_random_image(size): - """ Generates a very hard to compress file - :param size: tuple - """ - return Image.frombytes('RGB', - size, os.urandom(size[0]*size[1] * 3)) - - im = gen_random_image((512, 512)) + im = self.gen_random_image((512, 512)) f = self.tempfile("temp.jpeg") im.save(f, quality=100, optimize=True) diff --git a/Tests/test_file_jpeg2k.py b/Tests/test_file_jpeg2k.py index 0f3522a3b..8aeb7ecae 100644 --- a/Tests/test_file_jpeg2k.py +++ b/Tests/test_file_jpeg2k.py @@ -174,15 +174,8 @@ class TestFileJpeg2k(PillowTestCase): def test_unbound_local(self): # prepatch, a malformed jp2 file could cause an UnboundLocalError # exception. - try: - jp2 = Image.open('Tests/images/unbound_variable.jp2') - self.assertTrue(False, 'Expecting an exception') - except SyntaxError as err: - self.assertTrue(True, 'Expecting a syntax error') - except IOError as err: - self.assertTrue(True, 'Expecting an IO error') - except UnboundLocalError as err: - self.assertTrue(False, "Prepatch error") + with self.assertRaises(IOError): + Image.open('Tests/images/unbound_variable.jp2') if __name__ == '__main__': unittest.main() diff --git a/Tests/test_file_libtiff.py b/Tests/test_file_libtiff.py index 6e40d4b37..48b74964f 100644 --- a/Tests/test_file_libtiff.py +++ b/Tests/test_file_libtiff.py @@ -39,6 +39,8 @@ class LibTiffTestCase(PillowTestCase): out = self.tempfile("temp.png") im.save(out) + out_bytes = io.BytesIO() + im.save(out_bytes, format='tiff', compression='group4') class TestFileLibTiff(LibTiffTestCase): @@ -180,9 +182,9 @@ class TestFileLibTiff(LibTiffTestCase): # Get the list of the ones that we should be able to write - core_items = dict((tag, info) for tag, info in [(s, TiffTags.lookup(s)) for s - in TiffTags.LIBTIFF_CORE] - if info.type is not None) + core_items = {tag: info for tag, info in ((s, TiffTags.lookup(s)) for s + in TiffTags.LIBTIFF_CORE) + if info.type is not None} # Exclude ones that have special meaning # that we're already testing them @@ -521,7 +523,60 @@ class TestFileLibTiff(LibTiffTestCase): except: self.fail("Should not get permission error here") - + def test_read_icc(self): + with Image.open("Tests/images/hopper.iccprofile.tif") as img: + icc = img.info.get('icc_profile') + self.assertNotEqual(icc, None) + TiffImagePlugin.READ_LIBTIFF = True + with Image.open("Tests/images/hopper.iccprofile.tif") as img: + icc_libtiff = img.info.get('icc_profile') + self.assertNotEqual(icc_libtiff, None) + TiffImagePlugin.READ_LIBTIFF = False + self.assertEqual(icc, icc_libtiff) + + def test_multipage_compression(self): + im = Image.open('Tests/images/compression.tif') + + im.seek(0) + self.assertEqual(im._compression, 'tiff_ccitt') + self.assertEqual(im.size, (10, 10)) + + im.seek(1) + self.assertEqual(im._compression, 'packbits') + self.assertEqual(im.size, (10, 10)) + im.load() + + im.seek(0) + self.assertEqual(im._compression, 'tiff_ccitt') + self.assertEqual(im.size, (10, 10)) + im.load() + + def test_save_tiff_with_jpegtables(self): + # Arrange + outfile = self.tempfile("temp.tif") + + # Created with ImageMagick: convert hopper.jpg hopper_jpg.tif + # Contains JPEGTables (347) tag + infile = "Tests/images/hopper_jpg.tif" + im = Image.open(infile) + + # Act / Assert + # Should not raise UnicodeDecodeError or anything else + im.save(outfile) + + def test_page_number_x_0(self): + # Issue 973 + # Test TIFF with tag 297 (Page Number) having value of 0 0. + # The first number is the current page number. + # The second is the total number of pages, zero means not available. + outfile = self.tempfile("temp.tif") + # Created by printing a page in Chrome to PDF, then: + # /usr/bin/gs -q -sDEVICE=tiffg3 -sOutputFile=total-pages-zero.tif + # -dNOPAUSE /tmp/test.pdf -c quit + infile = "Tests/images/total-pages-zero.tif" + im = Image.open(infile) + # Should not divide by zero + im.save(outfile) if __name__ == '__main__': diff --git a/Tests/test_file_msp.py b/Tests/test_file_msp.py index f7c518379..e174d0061 100644 --- a/Tests/test_file_msp.py +++ b/Tests/test_file_msp.py @@ -31,7 +31,7 @@ class TestFileMsp(PillowTestCase): # Assert self.assertEqual(im.size, (128, 128)) - self.assert_image_similar(im, hopper("1"), 4) + self.assert_image_equal(im, hopper("1"), 4) def test_cannot_save_wrong_mode(self): # Arrange diff --git a/Tests/test_file_png.py b/Tests/test_file_png.py index a96422fa7..32d6a3acd 100644 --- a/Tests/test_file_png.py +++ b/Tests/test_file_png.py @@ -12,7 +12,6 @@ codecs = dir(Image.core) # sample png stream TEST_PNG_FILE = "Tests/images/hopper.png" -TEST_DATA = open(TEST_PNG_FILE, "rb").read() # stuff to create inline PNG images @@ -392,11 +391,11 @@ class TestFilePng(PillowTestCase): info = PngImagePlugin.PngInfo() info.add_text("Text", "Ascii") im = roundtrip(im, pnginfo=info) - self.assertEqual(type(im.info["Text"]), str) + self.assertIsInstance(im.info["Text"], str) def test_unicode_text(self): - # Check preservation of non-ASCII characters on Python3 - # This cannot really be meaningfully tested on Python2, + # Check preservation of non-ASCII characters on Python 3 + # This cannot really be meaningfully tested on Python 2, # since it didn't preserve charsets to begin with. def rt_text(value): @@ -498,6 +497,38 @@ class TestFilePng(PillowTestCase): self.assertEqual(repr_png.format, 'PNG') self.assert_image_equal(im, repr_png) + def test_chunk_order(self): + im = Image.open("Tests/images/icc_profile.png") + test_file = self.tempfile("temp.png") + im.convert("P").save(test_file, dpi=(100, 100)) + + chunks = [] + fp = open(test_file, "rb") + fp.read(8) + png = PngImagePlugin.PngStream(fp) + while True: + cid, pos, length = png.read() + chunks.append(cid) + try: + s = png.call(cid, pos, length) + except EOFError: + break + png.crc(cid, s) + + # https://www.w3.org/TR/PNG/#5ChunkOrdering + # IHDR - shall be first + self.assertEqual(chunks.index(b"IHDR"), 0) + # PLTE - before first IDAT + self.assertLess(chunks.index(b"PLTE"), chunks.index(b"IDAT")) + # iCCP - before PLTE and IDAT + self.assertLess(chunks.index(b"iCCP"), chunks.index(b"PLTE")) + self.assertLess(chunks.index(b"iCCP"), chunks.index(b"IDAT")) + # tRNS - after PLTE, before IDAT + self.assertGreater(chunks.index(b"tRNS"), chunks.index(b"PLTE")) + self.assertLess(chunks.index(b"tRNS"), chunks.index(b"IDAT")) + # pHYs - before IDAT + self.assertLess(chunks.index(b"pHYs"), chunks.index(b"IDAT")) + if __name__ == '__main__': unittest.main() diff --git a/Tests/test_file_ppm.py b/Tests/test_file_ppm.py index 3adc7a6d1..a798466b7 100644 --- a/Tests/test_file_ppm.py +++ b/Tests/test_file_ppm.py @@ -4,7 +4,6 @@ from PIL import Image # sample ppm stream test_file = "Tests/images/hopper.ppm" -data = open(test_file, "rb").read() class TestFilePpm(PillowTestCase): @@ -37,12 +36,21 @@ class TestFilePpm(PillowTestCase): def test_truncated_file(self): path = self.tempfile('temp.pgm') - f = open(path, 'w') - f.write('P6') - f.close() + with open(path, 'w') as f: + f.write('P6') self.assertRaises(ValueError, lambda: Image.open(path)) + def test_neg_ppm(self): + # Storage.c accepted negative values for xsize, ysize. the + # internal open_ppm function didn't check for sanity but it + # has been removed. The default opener doesn't accept negative + # sizes. + + with self.assertRaises(IOError): + Image.open('Tests/images/negative_size.ppm') + + if __name__ == '__main__': unittest.main() diff --git a/Tests/test_file_psd.py b/Tests/test_file_psd.py index 6bf34cf78..2b4825734 100644 --- a/Tests/test_file_psd.py +++ b/Tests/test_file_psd.py @@ -4,7 +4,6 @@ from PIL import Image, PsdImagePlugin # sample ppm stream test_file = "Tests/images/hopper.psd" -data = open(test_file, "rb").read() class TestImagePsd(PillowTestCase): diff --git a/Tests/test_file_sgi.py b/Tests/test_file_sgi.py index 870e57ed8..65910b5eb 100644 --- a/Tests/test_file_sgi.py +++ b/Tests/test_file_sgi.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase +from helper import unittest, PillowTestCase, hopper from PIL import Image, SgiImagePlugin @@ -6,31 +6,37 @@ from PIL import Image, SgiImagePlugin class TestFileSgi(PillowTestCase): def test_rgb(self): - # Arrange # Created with ImageMagick then renamed: - # convert hopper.ppm hopper.sgi + # convert hopper.ppm -compress None sgi:hopper.rgb test_file = "Tests/images/hopper.rgb" - # Act / Assert - self.assertRaises(ValueError, lambda: Image.open(test_file)) + im = Image.open(test_file) + self.assert_image_equal(im, hopper()) def test_l(self): - # Arrange - # Created with ImageMagick then renamed: - # convert hopper.ppm -monochrome hopper.sgi + # Created with ImageMagick + # convert hopper.ppm -monochrome -compress None sgi:hopper.bw test_file = "Tests/images/hopper.bw" - # Act / Assert - self.assertRaises(ValueError, lambda: Image.open(test_file)) + im = Image.open(test_file) + self.assert_image_similar(im, hopper('L'), 2) def test_rgba(self): - # Arrange # Created with ImageMagick: - # convert transparent.png transparent.sgi + # convert transparent.png -compress None transparent.sgi test_file = "Tests/images/transparent.sgi" + + im = Image.open(test_file) + target = Image.open('Tests/images/transparent.png') + self.assert_image_equal(im, target) - # Act / Assert - self.assertRaises(ValueError, lambda: Image.open(test_file)) + def test_rle(self): + # convert hopper.ppm hopper.sgi + # We don't support RLE compression, this should throw a value error + test_file = "Tests/images/hopper.sgi" + + with self.assertRaises(ValueError): + Image.open(test_file) def test_invalid_file(self): invalid_file = "Tests/images/flower.jpg" @@ -39,6 +45,17 @@ class TestFileSgi(PillowTestCase): lambda: SgiImagePlugin.SgiImageFile(invalid_file)) + def test_write(self): + def roundtrip(img): + out = self.tempfile('temp.sgi') + img.save(out, format='sgi') + reloaded = Image.open(out) + self.assert_image_equal(img, reloaded) + + for mode in ('L', 'RGB', 'RGBA'): + roundtrip(hopper(mode)) + + if __name__ == '__main__': unittest.main() diff --git a/Tests/test_file_sun.py b/Tests/test_file_sun.py index 003740c5b..40eb73898 100644 --- a/Tests/test_file_sun.py +++ b/Tests/test_file_sun.py @@ -1,7 +1,10 @@ -from helper import unittest, PillowTestCase +from helper import unittest, PillowTestCase, hopper from PIL import Image, SunImagePlugin +import os + +EXTRA_DIR = 'Tests/images/sunraster' class TestFileSun(PillowTestCase): @@ -16,10 +19,32 @@ class TestFileSun(PillowTestCase): # Assert self.assertEqual(im.size, (128, 128)) + self.assert_image_similar(im, hopper(), 5) # visually verified + invalid_file = "Tests/images/flower.jpg" self.assertRaises(SyntaxError, lambda: SunImagePlugin.SunImageFile(invalid_file)) + def test_im1(self): + im = Image.open('Tests/images/sunraster.im1') + target = Image.open('Tests/images/sunraster.im1.png') + self.assert_image_equal(im, target) + + @unittest.skipIf(not os.path.exists(EXTRA_DIR), + "Extra image files not installed") + def test_others(self): + files = (os.path.join(EXTRA_DIR, f) for f in + os.listdir(EXTRA_DIR) if os.path.splitext(f)[1] + in ('.sun', '.SUN', '.ras')) + for path in files: + with Image.open(path) as im: + im.load() + self.assertIsInstance(im, SunImagePlugin.SunImageFile) + target_path = "%s.png" % os.path.splitext(path)[0] + #im.save(target_file) + with Image.open(target_path) as target: + self.assert_image_equal(im, target) + if __name__ == '__main__': unittest.main() diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py index ad7fad7c8..1fe3ad45e 100644 --- a/Tests/test_file_tiff.py +++ b/Tests/test_file_tiff.py @@ -93,6 +93,24 @@ class TestFileTiff(PillowTestCase): self.assertEqual(im.info['dpi'], (72., 72.)) + def test_xyres_fallback_tiff(self): + from PIL.TiffImagePlugin import X_RESOLUTION, Y_RESOLUTION, RESOLUTION_UNIT + filename = "Tests/images/compression.tif" + im = Image.open(filename) + + # v2 api + self.assertIsInstance(im.tag_v2[X_RESOLUTION], + TiffImagePlugin.IFDRational) + self.assertIsInstance(im.tag_v2[Y_RESOLUTION], + TiffImagePlugin.IFDRational) + self.assertRaises(KeyError, + lambda: im.tag_v2[RESOLUTION_UNIT]) + + # Legacy. + self.assertEqual(im.info['resolution'], (100., 100.)) + # Fallback "inch". + self.assertEqual(im.info['dpi'], (100., 100.)) + def test_int_resolution(self): from PIL.TiffImagePlugin import X_RESOLUTION, Y_RESOLUTION filename = "Tests/images/pil168.tif" @@ -119,7 +137,7 @@ class TestFileTiff(PillowTestCase): self.assertRaises(SyntaxError, lambda: TiffImagePlugin.TiffImageFile(invalid_file)) - TiffImagePlugin.PREFIXES.append("\xff\xd8\xff\xe0") + TiffImagePlugin.PREFIXES.append(b"\xff\xd8\xff\xe0") self.assertRaises(SyntaxError, lambda: TiffImagePlugin.TiffImageFile(invalid_file)) TiffImagePlugin.PREFIXES.pop() @@ -382,20 +400,6 @@ class TestFileTiff(PillowTestCase): self.assertEqual(im2.mode, "L") self.assert_image_equal(im, im2) - def test_page_number_x_0(self): - # Issue 973 - # Test TIFF with tag 297 (Page Number) having value of 0 0. - # The first number is the current page number. - # The second is the total number of pages, zero means not available. - outfile = self.tempfile("temp.tif") - # Created by printing a page in Chrome to PDF, then: - # /usr/bin/gs -q -sDEVICE=tiffg3 -sOutputFile=total-pages-zero.tif - # -dNOPAUSE /tmp/test.pdf -c quit - infile = "Tests/images/total-pages-zero.tif" - im = Image.open(infile) - # Should not divide by zero - im.save(outfile) - def test_with_underscores(self): kwargs = {'resolution_unit': 'inch', 'x_resolution': 72, @@ -432,36 +436,6 @@ class TestFileTiff(PillowTestCase): self.assertEqual(im.tag_v2[X_RESOLUTION], 36) self.assertEqual(im.tag_v2[Y_RESOLUTION], 72) - def test_multipage_compression(self): - im = Image.open('Tests/images/compression.tif') - - im.seek(0) - self.assertEqual(im._compression, 'tiff_ccitt') - self.assertEqual(im.size, (10, 10)) - - im.seek(1) - self.assertEqual(im._compression, 'packbits') - self.assertEqual(im.size, (10, 10)) - im.load() - - im.seek(0) - self.assertEqual(im._compression, 'tiff_ccitt') - self.assertEqual(im.size, (10, 10)) - im.load() - - def test_save_tiff_with_jpegtables(self): - # Arrange - outfile = self.tempfile("temp.tif") - - # Created with ImageMagick: convert hopper.jpg hopper_jpg.tif - # Contains JPEGTables (347) tag - infile = "Tests/images/hopper_jpg.tif" - im = Image.open(infile) - - # Act / Assert - # Should not raise UnicodeDecodeError or anything else - im.save(outfile) - def test_lzw(self): # Act im = Image.open("Tests/images/hopper_lzw.tif") @@ -499,5 +473,21 @@ class TestFileTiff(PillowTestCase): with Image.open(mp) as im: self.assertEqual(im.n_frames, 3) + def test_saving_icc_profile(self): + # Tests saving TIFF with icc_profile set. + # At the time of writing this will only work for non-compressed tiffs + # as libtiff does not support embedded ICC profiles, ImageFile._save(..) + # however does. + im = Image.new('RGB', (1, 1)) + im.info['icc_profile'] = 'Dummy value' + + # Try save-load round trip to make sure both handle icc_profile. + tmpfile = self.tempfile('temp.tif') + im.save(tmpfile, 'TIFF', compression='raw') + reloaded = Image.open(tmpfile) + + self.assertEqual(b'Dummy value', reloaded.info['icc_profile']) + + if __name__ == '__main__': unittest.main() diff --git a/Tests/test_file_tiff_metadata.py b/Tests/test_file_tiff_metadata.py index f40ec0982..c75487f9b 100644 --- a/Tests/test_file_tiff_metadata.py +++ b/Tests/test_file_tiff_metadata.py @@ -8,7 +8,7 @@ from helper import unittest, PillowTestCase, hopper from PIL import Image, TiffImagePlugin, TiffTags from PIL.TiffImagePlugin import _limit_rational, IFDRational -tag_ids = dict((info.name, info.value) for info in TiffTags.TAGS_V2.values()) +tag_ids = {info.name: info.value for info in TiffTags.TAGS_V2.values()} class TestFileTiffMetadata(PillowTestCase): @@ -123,10 +123,9 @@ class TestFileTiffMetadata(PillowTestCase): reloaded = loaded.tag_v2.named() for k, v in original.items(): - if type(v) == IFDRational: + if isinstance(v, IFDRational): original[k] = IFDRational(*_limit_rational(v, 2**31)) - if type(v) == tuple and \ - type(v[0]) == IFDRational: + if isinstance(v, tuple) and isinstance(v[0], IFDRational): original[k] = tuple([IFDRational( *_limit_rational(elt, 2**31)) for elt in v]) @@ -136,8 +135,8 @@ class TestFileTiffMetadata(PillowTestCase): for tag, value in reloaded.items(): if tag in ignored: continue - if (type(original[tag]) == tuple - and type(original[tag][0]) == IFDRational): + if (isinstance(original[tag], tuple) + and isinstance(original[tag][0], IFDRational)): # Need to compare element by element in the tuple, # not comparing tuples of object references self.assert_deep_equal(original[tag], @@ -175,7 +174,7 @@ class TestFileTiffMetadata(PillowTestCase): im.save(out) reloaded = Image.open(out) - self.assert_(type(im.info['icc_profile']) is not tuple) + self.assertNotIsInstance(im.info['icc_profile'], tuple) self.assertEqual(im.info['icc_profile'], reloaded.info['icc_profile']) def test_iccprofile_binary(self): diff --git a/Tests/test_file_wmf.py b/Tests/test_file_wmf.py new file mode 100644 index 000000000..9f2f893cb --- /dev/null +++ b/Tests/test_file_wmf.py @@ -0,0 +1,31 @@ +from helper import unittest, PillowTestCase +from PIL import Image +from io import BytesIO + +class TestFileWmf(PillowTestCase): + + def test_load_raw(self): + + # Test basic EMF open and rendering + im = Image.open('Tests/images/drawing.emf') + if hasattr(Image.core, "drawwmf"): + # Currently, support for WMF/EMF is Windows-only + im.load() + # Compare to reference rendering + imref = Image.open('Tests/images/drawing_emf_ref.png') + imref.load() + self.assert_image_similar(im, imref, 0) + + # Test basic WMF open and rendering + im = Image.open('Tests/images/drawing.wmf') + if hasattr(Image.core, "drawwmf"): + # Currently, support for WMF/EMF is Windows-only + im.load() + # Compare to reference rendering + imref = Image.open('Tests/images/drawing_wmf_ref.png') + imref.load() + self.assert_image_similar(im, imref, 2.0) + + +if __name__ == '__main__': + unittest.main() diff --git a/Tests/test_font_pcf.py b/Tests/test_font_pcf.py index 6afa9d476..25c5d7001 100644 --- a/Tests/test_font_pcf.py +++ b/Tests/test_font_pcf.py @@ -55,7 +55,7 @@ class TestFontPcf(PillowTestCase): self.assert_image_equal(image, compare) def test_high_characters(self): - message = "".join([chr(i+1) for i in range(140, 232)]) + 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: diff --git a/Tests/test_format_hsv.py b/Tests/test_format_hsv.py index e32f1c047..eea29b3f1 100644 --- a/Tests/test_format_hsv.py +++ b/Tests/test_format_hsv.py @@ -47,9 +47,9 @@ class TestFormatHSV(PillowTestCase): img = Image.merge('RGB', (r, g, b)) - # print (("%d, %d -> "% (int(1.75*px),int(.25*px))) + \ - # "(%s, %s, %s)"%img.getpixel((1.75*px, .25*px))) - # print (("%d, %d -> "% (int(.75*px),int(.25*px))) + \ + # print(("%d, %d -> "% (int(1.75*px),int(.25*px))) + \ + # "(%s, %s, %s)"%img.getpixel((1.75*px, .25*px))) + # print(("%d, %d -> "% (int(.75*px),int(.25*px))) + \ # "(%s, %s, %s)"%img.getpixel((.75*px, .25*px))) return img @@ -95,8 +95,8 @@ class TestFormatHSV(PillowTestCase): im = src.convert('HSV') comparable = self.to_hsv_colorsys(src) - # print (im.getpixel((448, 64))) - # print (comparable.getpixel((448, 64))) + # print(im.getpixel((448, 64))) + # print(comparable.getpixel((448, 64))) # print(im.split()[0].histogram()) # print(comparable.split()[0].histogram()) @@ -111,15 +111,15 @@ class TestFormatHSV(PillowTestCase): self.assert_image_similar(im.split()[2], comparable.split()[2], 1, "Value conversion is wrong") - # print (im.getpixel((192, 64))) + # print(im.getpixel((192, 64))) comparable = src im = im.convert('RGB') # im.split()[0].show() # comparable.split()[0].show() - # print (im.getpixel((192, 64))) - # print (comparable.getpixel((192, 64))) + # print(im.getpixel((192, 64))) + # print(comparable.getpixel((192, 64))) self.assert_image_similar(im.split()[0], comparable.split()[0], 3, "R conversion is wrong") @@ -132,8 +132,8 @@ class TestFormatHSV(PillowTestCase): im = hopper('RGB').convert('HSV') comparable = self.to_hsv_colorsys(hopper('RGB')) -# print ([ord(x) for x in im.split()[0].tobytes()[:80]]) -# print ([ord(x) for x in comparable.split()[0].tobytes()[:80]]) +# print([ord(x) for x in im.split()[0].tobytes()[:80]]) +# print([ord(x) for x in comparable.split()[0].tobytes()[:80]]) # print(im.split()[0].histogram()) # print(comparable.split()[0].histogram()) @@ -153,8 +153,8 @@ class TestFormatHSV(PillowTestCase): # print(converted.split()[1].histogram()) # print(target.split()[1].histogram()) - # print ([ord(x) for x in target.split()[1].tobytes()[:80]]) - # print ([ord(x) for x in converted.split()[1].tobytes()[:80]]) + # print([ord(x) for x in target.split()[1].tobytes()[:80]]) + # print([ord(x) for x in converted.split()[1].tobytes()[:80]]) self.assert_image_similar(converted.split()[0], comparable.split()[0], 3, "R conversion is wrong") diff --git a/Tests/test_image.py b/Tests/test_image.py index b3fbd508d..afeacb0a0 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -63,16 +63,28 @@ class TestImage(PillowTestCase): os.remove(temp_file) im.save(Path(temp_file)) + def test_fp_name(self): + temp_file = self.tempfile("temp.jpg") + + class FP(object): + def write(a, b): + pass + fp = FP() + fp.name = temp_file + + im = hopper() + im.save(fp) + def test_tempfile(self): # see #1460, pathlib support breaks tempfile.TemporaryFile on py27 # Will error out on save on 3.0.0 import tempfile im = hopper() - fp = tempfile.TemporaryFile() - im.save(fp, 'JPEG') - fp.seek(0) - reloaded = Image.open(fp) - self.assert_image_similar(im, reloaded, 20) + with tempfile.TemporaryFile() as fp: + im.save(fp, 'JPEG') + fp.seek(0) + reloaded = Image.open(fp) + self.assert_image_similar(im, reloaded, 20) def test_internals(self): @@ -184,6 +196,35 @@ class TestImage(PillowTestCase): img_colors = sorted(img.getcolors()) self.assertEqual(img_colors, expected_colors) + def test_registered_extensions_uninitialized(self): + # Arrange + Image._initialized = 0 + extension = Image.EXTENSION + Image.EXTENSION = {} + + # Act + Image.registered_extensions() + + # Assert + self.assertEqual(Image._initialized, 2) + + # Restore the original state and assert + Image.EXTENSION = extension + self.assertTrue(Image.EXTENSION) + + def test_registered_extensions(self): + # Arrange + # Open an image to trigger plugin registration + Image.open('Tests/images/rgb.jpg') + + # Act + extensions = Image.registered_extensions() + + # Assert + self.assertTrue(bool(extensions)) + for ext in ['.cur', '.icns', '.tif', '.tiff']: + self.assertIn(ext, extensions) + def test_effect_mandelbrot(self): # Arrange size = (512, 512) @@ -237,5 +278,43 @@ class TestImage(PillowTestCase): im3 = Image.open('Tests/images/effect_spread.png') self.assert_image_similar(im2, im3, 110) + def test_check_size(self): + # Checking that the _check_size function throws value errors when we want it to. + with self.assertRaises(ValueError): + Image.new('RGB', 0) # not a tuple + with self.assertRaises(ValueError): + Image.new('RGB', (0,)) # Tuple too short + with self.assertRaises(ValueError): + Image.new('RGB', (-1,-1)) # w,h < 0 + + # this should pass with 0 sized images, #2259 + im = Image.new('L', (0, 0)) + self.assertEqual(im.size, (0, 0)) + + self.assertTrue(Image.new('RGB', (1,1))) + # Should pass lists too + i = Image.new('RGB', [1,1]) + self.assertIsInstance(i.size, tuple) + + def test_storage_neg(self): + # Storage.c accepted negative values for xsize, ysize. Was + # test_neg_ppm, but the core function for that has been + # removed Calling directly into core to test the error in + # Storage.c, rather than the size check above + + with self.assertRaises(ValueError): + Image.core.fill('RGB', (2,-2), (0,0,0)) + + def test_offset_not_implemented(self): + # Arrange + im = hopper() + + # Act / Assert + self.assertRaises(NotImplementedError, lambda: im.offset(None)) + + def test_fromstring(self): + self.assertRaises(NotImplementedError, Image.fromstring) + + if __name__ == '__main__': unittest.main() diff --git a/Tests/test_image_access.py b/Tests/test_image_access.py index 21295ac82..900f39eb4 100644 --- a/Tests/test_image_access.py +++ b/Tests/test_image_access.py @@ -78,12 +78,25 @@ class TestImageGetPixel(AccessTest): im.getpixel((0, 0)), c, "put/getpixel roundtrip failed for mode %s, color %s" % (mode, c)) + # Check 0 + im = Image.new(mode, (0, 0), None) + with self.assertRaises(IndexError): + im.putpixel((0, 0), c) + with self.assertRaises(IndexError): + im.getpixel((0, 0)) + # check initial color im = Image.new(mode, (1, 1), c) self.assertEqual( im.getpixel((0, 0)), c, "initial color failed for mode %s, color %s " % (mode, c)) + # Check 0 + im = Image.new(mode, (0, 0), c) + with self.assertRaises(IndexError): + im.getpixel((0, 0)) + + def test_basic(self): for mode in ("1", "L", "LA", "I", "I;16", "I;16B", "F", "P", "PA", "RGB", "RGBA", "RGBX", "CMYK", "YCbCr"): @@ -192,11 +205,8 @@ class TestCffi(AccessTest): # Attempt to set the value on a read-only image access = PyAccess.new(im, True) - try: + with self.assertRaises(ValueError): access[(0, 0)] = color - except ValueError: - return - self.fail("Putpixel did not fail on a read-only image") def test_set_vs_c(self): rgb = hopper('RGB') diff --git a/Tests/test_image_convert.py b/Tests/test_image_convert.py index 0c98211e7..54ffde10b 100644 --- a/Tests/test_image_convert.py +++ b/Tests/test_image_convert.py @@ -19,6 +19,11 @@ class TestImageConvert(PillowTestCase): for mode in modes: convert(im, mode) + # Check 0 + im = Image.new(mode, (0,0)) + for mode in modes: + convert(im, mode) + def test_default(self): im = hopper("P") diff --git a/Tests/test_image_copy.py b/Tests/test_image_copy.py index ba53758d5..c50205c9c 100644 --- a/Tests/test_image_copy.py +++ b/Tests/test_image_copy.py @@ -1,5 +1,7 @@ from helper import unittest, PillowTestCase, hopper +from PIL import Image + import copy @@ -33,5 +35,12 @@ class TestImageCopy(PillowTestCase): self.assertEqual(out.mode, im.mode) self.assertEqual(out.size, croppedSize) + def test_copy_zero(self): + im = Image.new('RGB', (0,0)) + out = im.copy() + self.assertEqual(out.mode, im.mode) + self.assertEqual(out.size, im.size) + + if __name__ == '__main__': unittest.main() diff --git a/Tests/test_image_crop.py b/Tests/test_image_crop.py index c12e29be4..be50b46b7 100644 --- a/Tests/test_image_crop.py +++ b/Tests/test_image_crop.py @@ -7,9 +7,12 @@ class TestImageCrop(PillowTestCase): def test_crop(self): def crop(mode): - out = hopper(mode).crop((50, 50, 100, 100)) - self.assertEqual(out.mode, mode) - self.assertEqual(out.size, (50, 50)) + im = hopper(mode) + self.assert_image_equal(im.crop(), im) + + cropped = im.crop((50, 50, 100, 100)) + self.assertEqual(cropped.mode, mode) + self.assertEqual(cropped.size, (50, 50)) for mode in "1", "P", "L", "RGB", "I", "F": crop(mode) @@ -70,19 +73,36 @@ class TestImageCrop(PillowTestCase): #Image.crop crashes prepatch with an access violation #apparently a use after free on windows, see #https://github.com/python-pillow/Pillow/issues/1077 - + test_img = 'Tests/images/bmp/g/pal8-0.bmp' extents = (1,1,10,10) #works prepatch img = Image.open(test_img) img2 = img.crop(extents) img2.load() - + # fail prepatch img = Image.open(test_img) img = img.crop(extents) img.load() + def test_crop_zero(self): + + im = Image.new('RGB', (0, 0), 'white') + + cropped = im.crop((0, 0, 0, 0)) + self.assertEqual(cropped.size, (0, 0)) + + cropped = im.crop((10, 10, 20, 20)) + self.assertEqual(cropped.size, (10, 10)) + self.assertEqual(cropped.getdata()[0], (0, 0, 0)) + + im = Image.new('RGB', (0, 0)) + + cropped = im.crop((10, 10, 20, 20)) + self.assertEqual(cropped.size, (10, 10)) + self.assertEqual(cropped.getdata()[2], (0, 0, 0)) + if __name__ == '__main__': diff --git a/Tests/test_image_draft.py b/Tests/test_image_draft.py index 0a8cc023c..12f5e0e9f 100644 --- a/Tests/test_image_draft.py +++ b/Tests/test_image_draft.py @@ -2,36 +2,73 @@ from helper import unittest, PillowTestCase, fromstring, tostring from PIL import Image -CODECS = dir(Image.core) -FILENAME = "Tests/images/hopper.jpg" -DATA = tostring(Image.open(FILENAME).resize((512, 512)), "JPEG") - - -def draft(mode, size): - im = fromstring(DATA) - im.draft(mode, size) - return im - class TestImageDraft(PillowTestCase): - def setUp(self): - if "jpeg_encoder" not in CODECS or "jpeg_decoder" not in CODECS: + codecs = dir(Image.core) + if "jpeg_encoder" not in codecs or "jpeg_decoder" not in codecs: self.skipTest("jpeg support not available") + def draft_roundtrip(self, in_mode, in_size, req_mode, req_size): + im = Image.new(in_mode, in_size) + data = tostring(im, 'JPEG') + im = fromstring(data) + im.draft(req_mode, req_size) + return im + def test_size(self): - self.assertEqual(draft("RGB", (512, 512)).size, (512, 512)) - self.assertEqual(draft("RGB", (256, 256)).size, (256, 256)) - self.assertEqual(draft("RGB", (128, 128)).size, (128, 128)) - self.assertEqual(draft("RGB", (64, 64)).size, (64, 64)) - self.assertEqual(draft("RGB", (32, 32)).size, (64, 64)) + for in_size, req_size, out_size in [ + ((435, 361), (2048, 2048), (435, 361)), # bigger + ((435, 361), (435, 361), (435, 361)), # same + ((128, 128), (64, 64), (64, 64)), + ((128, 128), (32, 32), (32, 32)), + ((128, 128), (16, 16), (16, 16)), + + # large requested width + ((435, 361), (218, 128), (435, 361)), # almost 2x + ((435, 361), (217, 128), (218, 181)), # more than 2x + ((435, 361), (109, 64), (218, 181)), # almost 4x + ((435, 361), (108, 64), (109, 91)), # more than 4x + ((435, 361), (55, 32), (109, 91)), # almost 8x + ((435, 361), (54, 32), (55, 46)), # more than 8x + ((435, 361), (27, 16), (55, 46)), # more than 16x + + # and vice versa + ((435, 361), (128, 181), (435, 361)), # almost 2x + ((435, 361), (128, 180), (218, 181)), # more than 2x + ((435, 361), (64, 91), (218, 181)), # almost 4x + ((435, 361), (64, 90), (109, 91)), # more than 4x + ((435, 361), (32, 46), (109, 91)), # almost 8x + ((435, 361), (32, 45), (55, 46)), # more than 8x + ((435, 361), (16, 22), (55, 46)), # more than 16x + ]: + im = self.draft_roundtrip('L', in_size, None, req_size) + im.load() + self.assertEqual(im.size, out_size) def test_mode(self): - self.assertEqual(draft("1", (512, 512)).mode, "RGB") - self.assertEqual(draft("L", (512, 512)).mode, "L") - self.assertEqual(draft("RGB", (512, 512)).mode, "RGB") - self.assertEqual(draft("YCbCr", (512, 512)).mode, "YCbCr") + for in_mode, req_mode, out_mode in [ + ("RGB", "1", "RGB"), + ("RGB", "L", "L"), + ("RGB", "RGB", "RGB"), + ("RGB", "YCbCr", "YCbCr"), + ("L", "1", "L"), + ("L", "L", "L"), + ("L", "RGB", "L"), + ("L", "YCbCr", "L"), + ("CMYK", "1", "CMYK"), + ("CMYK", "L", "CMYK"), + ("CMYK", "RGB", "CMYK"), + ("CMYK", "YCbCr", "CMYK"), + ]: + im = self.draft_roundtrip(in_mode, (64, 64), req_mode, None) + im.load() + self.assertEqual(im.mode, out_mode) + def test_several_drafts(self): + im = self.draft_roundtrip('L', (128, 128), None, (64, 64)) + im.draft(None, (64, 64)) + im.load() if __name__ == '__main__': unittest.main() diff --git a/Tests/test_image_getim.py b/Tests/test_image_getim.py index c67118b58..bc562de5a 100644 --- a/Tests/test_image_getim.py +++ b/Tests/test_image_getim.py @@ -1,5 +1,4 @@ from helper import unittest, PillowTestCase, hopper, py3 -import sys class TestImageGetIm(PillowTestCase): @@ -11,13 +10,7 @@ class TestImageGetIm(PillowTestCase): if py3: self.assertIn("PyCapsule", type_repr) - if sys.hexversion < 0x2070000: - # py2.6 x64, windows - target_types = (int, long) - else: - target_types = (int) - - self.assertIsInstance(im.im.id, target_types) + self.assertIsInstance(im.im.id, int) if __name__ == '__main__': diff --git a/Tests/test_image_paste.py b/Tests/test_image_paste.py index f3fe9a2af..1b76c6609 100644 --- a/Tests/test_image_paste.py +++ b/Tests/test_image_paste.py @@ -1,4 +1,4 @@ -from helper import PillowTestCase, cached_property +from helper import unittest, PillowTestCase, cached_property from PIL import Image @@ -28,6 +28,15 @@ class TestImagingPaste(PillowTestCase): ] self.assertEqual(actual, expected) + def assert_9points_paste(self, im, im2, mask, expected): + im3 = im.copy() + im3.paste(im2, (0, 0), mask) + self.assert_9points_image(im3, expected) + + # Abbreviated syntax + im.paste(im2, mask) + self.assert_9points_image(im, expected) + @cached_property def mask_1(self): mask = Image.new('1', (self.size, self.size)) @@ -91,9 +100,7 @@ class TestImagingPaste(PillowTestCase): im = Image.new(mode, (200, 200), 'white') im2 = getattr(self, 'gradient_' + mode) - im.paste(im2, (0, 0), self.mask_1) - - self.assert_9points_image(im, [ + self.assert_9points_paste(im, im2, self.mask_1, [ (255, 255, 255, 255), (255, 255, 255, 255), (127, 254, 127, 0), @@ -110,9 +117,7 @@ class TestImagingPaste(PillowTestCase): im = Image.new(mode, (200, 200), 'white') im2 = getattr(self, 'gradient_' + mode) - im.paste(im2, (0, 0), self.mask_L) - - self.assert_9points_image(im, [ + self.assert_9points_paste(im, im2, self.mask_L, [ (128, 191, 255, 191), (208, 239, 239, 208), (255, 255, 255, 255), @@ -129,9 +134,7 @@ class TestImagingPaste(PillowTestCase): im = Image.new(mode, (200, 200), 'white') im2 = getattr(self, 'gradient_' + mode) - im.paste(im2, (0, 0), self.gradient_RGBA) - - self.assert_9points_image(im, [ + self.assert_9points_paste(im, im2, self.gradient_RGBA, [ (128, 191, 255, 191), (208, 239, 239, 208), (255, 255, 255, 255), @@ -148,9 +151,7 @@ class TestImagingPaste(PillowTestCase): im = Image.new(mode, (200, 200), 'white') im2 = getattr(self, 'gradient_' + mode) - im.paste(im2, (0, 0), self.gradient_RGBa) - - self.assert_9points_image(im, [ + self.assert_9points_paste(im, im2, self.gradient_RGBa, [ (128, 255, 126, 255), (0, 127, 126, 255), (126, 253, 126, 255), @@ -180,9 +181,7 @@ class TestImagingPaste(PillowTestCase): im = Image.new(mode, (200, 200), (50, 60, 70, 80)[:len(mode)]) color = (10, 20, 30, 40)[:len(mode)] - im.paste(color, (0, 0), self.mask_1) - - self.assert_9points_image(im, [ + self.assert_9points_paste(im, color, self.mask_1, [ (50, 60, 70, 80), (50, 60, 70, 80), (10, 20, 30, 40), @@ -199,9 +198,7 @@ class TestImagingPaste(PillowTestCase): im = getattr(self, 'gradient_' + mode).copy() color = 'white' - im.paste(color, (0, 0), self.mask_L) - - self.assert_9points_image(im, [ + self.assert_9points_paste(im, color, self.mask_L, [ (127, 191, 254, 191), (111, 207, 206, 110), (127, 254, 127, 0), @@ -218,9 +215,7 @@ class TestImagingPaste(PillowTestCase): im = getattr(self, 'gradient_' + mode).copy() color = 'white' - im.paste(color, (0, 0), self.gradient_RGBA) - - self.assert_9points_image(im, [ + self.assert_9points_paste(im, color, self.gradient_RGBA, [ (127, 191, 254, 191), (111, 207, 206, 110), (127, 254, 127, 0), @@ -237,9 +232,7 @@ class TestImagingPaste(PillowTestCase): im = getattr(self, 'gradient_' + mode).copy() color = 'white' - im.paste(color, (0, 0), self.gradient_RGBa) - - self.assert_9points_image(im, [ + self.assert_9points_paste(im, color, self.gradient_RGBa, [ (255, 63, 126, 63), (47, 143, 142, 46), (126, 253, 126, 255), @@ -250,3 +243,14 @@ class TestImagingPaste(PillowTestCase): (48, 15, 15, 47), (126, 63, 255, 63) ]) + + def test_different_sizes(self): + im = Image.new('RGB', (100, 100)) + im2 = Image.new('RGB', (50, 50)) + + im.copy().paste(im2) + im.copy().paste(im2, (0,0)) + + +if __name__ == '__main__': + unittest.main() diff --git a/Tests/test_image_point.py b/Tests/test_image_point.py index dd33b3632..a6d20daa4 100644 --- a/Tests/test_image_point.py +++ b/Tests/test_image_point.py @@ -35,6 +35,9 @@ class TestImagePoint(PillowTestCase): int_lut = [x//2 for x in range(256)] self.assert_image_equal(out.convert('L'), im.point(int_lut, 'L')) + def test_f_mode(self): + self.assertRaises(ValueError, lambda: hopper('F').point(None)) + if __name__ == '__main__': unittest.main() diff --git a/Tests/test_image_resample.py b/Tests/test_image_resample.py index 3c8afd8ea..7ae687e2f 100644 --- a/Tests/test_image_resample.py +++ b/Tests/test_image_resample.py @@ -1,3 +1,4 @@ +from __future__ import print_function from helper import unittest, PillowTestCase, hopper from PIL import Image, ImageDraw, ImageMode @@ -8,12 +9,9 @@ class TestImagingResampleVulnerability(PillowTestCase): im = hopper('L') xsize = 0x100000008 // 4 ysize = 1000 # unimportant - try: + with self.assertRaises(MemoryError): # any resampling filter will do here im.im.resize((xsize, ysize), Image.BILINEAR) - self.fail("Resize should raise MemoryError on invalid xsize") - except MemoryError: - self.assertTrue(True, "Should raise MemoryError") def test_invalid_size(self): im = hopper() @@ -21,17 +19,20 @@ class TestImagingResampleVulnerability(PillowTestCase): im.resize((100, 100)) self.assertTrue(True, "Should not Crash") - try: + with self.assertRaises(ValueError): im.resize((-100, 100)) - self.fail("Resize should raise a value error on x negative size") - except ValueError: - self.assertTrue(True, "Should raise ValueError") - try: + with self.assertRaises(ValueError): im.resize((100, -100)) - self.fail("Resize should raise a value error on y negative size") - except ValueError: - self.assertTrue(True, "Should raise ValueError") + + def test_modify_after_resizing(self): + im = hopper('RGB') + # get copy with same size + copy = im.resize(im.size) + # some in-place operation + copy.paste('black', (0, 0, im.width // 2, im.height // 2)) + # image should be different + self.assertNotEqual(im.tobytes(), copy.tobytes()) class TestImagingCoreResampleAccuracy(PillowTestCase): @@ -241,10 +242,10 @@ class CoreResampleAlphaCorrectTest(PillowTestCase): def run_levels_case(self, i): px = i.load() for y in range(i.size[1]): - used_colors = set(px[x, y][0] for x in range(i.size[0])) + 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 {0} on {1} line.'.format(len(used_colors), y)) + 'Only {} on {} line.'.format(len(used_colors), y)) @unittest.skip("current implementation isn't precise enough") def test_levels_rgba(self): @@ -264,7 +265,7 @@ class CoreResampleAlphaCorrectTest(PillowTestCase): self.run_levels_case(case.resize((512, 32), Image.BICUBIC)) self.run_levels_case(case.resize((512, 32), Image.LANCZOS)) - def make_dity_case(self, mode, clean_pixel, dirty_pixel): + def make_dirty_case(self, mode, clean_pixel, dirty_pixel): i = Image.new(mode, (64, 64), dirty_pixel) px = i.load() xdiv4 = i.size[0] // 4 @@ -274,30 +275,30 @@ class CoreResampleAlphaCorrectTest(PillowTestCase): px[x + xdiv4, y + ydiv4] = clean_pixel return i - def run_dity_case(self, i, clean_pixel): + def run_dirty_case(self, i, clean_pixel): px = i.load() for y in range(i.size[1]): for x in range(i.size[0]): if px[x, y][-1] != 0 and px[x, y][:-1] != clean_pixel: - message = 'pixel at ({0}, {1}) is differ:\n{2}\n{3}'\ + message = 'pixel at ({}, {}) is differ:\n{}\n{}'\ .format(x, y, px[x, y], clean_pixel) self.assertEqual(px[x, y][:3], clean_pixel, message) def test_dirty_pixels_rgba(self): - case = self.make_dity_case('RGBA', (255, 255, 0, 128), (0, 0, 255, 0)) - self.run_dity_case(case.resize((20, 20), Image.BOX), (255, 255, 0)) - self.run_dity_case(case.resize((20, 20), Image.BILINEAR), (255, 255, 0)) - self.run_dity_case(case.resize((20, 20), Image.HAMMING), (255, 255, 0)) - self.run_dity_case(case.resize((20, 20), Image.BICUBIC), (255, 255, 0)) - self.run_dity_case(case.resize((20, 20), Image.LANCZOS), (255, 255, 0)) + 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)) def test_dirty_pixels_la(self): - case = self.make_dity_case('LA', (255, 128), (0, 0)) - self.run_dity_case(case.resize((20, 20), Image.BOX), (255,)) - self.run_dity_case(case.resize((20, 20), Image.BILINEAR), (255,)) - self.run_dity_case(case.resize((20, 20), Image.HAMMING), (255,)) - self.run_dity_case(case.resize((20, 20), Image.BICUBIC), (255,)) - self.run_dity_case(case.resize((20, 20), Image.LANCZOS), (255,)) + case = self.make_dirty_case('LA', (255, 128), (0, 0)) + self.run_dirty_case(case.resize((20, 20), Image.BOX), (255,)) + self.run_dirty_case(case.resize((20, 20), Image.BILINEAR), (255,)) + self.run_dirty_case(case.resize((20, 20), Image.HAMMING), (255,)) + self.run_dirty_case(case.resize((20, 20), Image.BICUBIC), (255,)) + self.run_dirty_case(case.resize((20, 20), Image.LANCZOS), (255,)) class CoreResamplePassesTest(PillowTestCase): @@ -323,10 +324,10 @@ class CoreResamplePassesTest(PillowTestCase): class CoreResampleCoefficientsTest(PillowTestCase): def test_reduce(self): test_color = 254 - # print '' + # print() for size in range(400000, 400010, 2): - # print '\r', size, + # print(size) i = Image.new('L', (size, 1), 0) draw = ImageDraw.Draw(i) draw.rectangle((0, 0, i.size[0] // 2 - 1, 0), test_color) @@ -334,7 +335,18 @@ class CoreResampleCoefficientsTest(PillowTestCase): px = i.resize((5, i.size[1]), Image.BICUBIC).load() if px[2, 0] != test_color // 2: self.assertEqual(test_color // 2, px[2, 0]) - # print '\r>', size, test_color // 2, px[2, 0] + # print('>', size, test_color // 2, px[2, 0]) + + def test_nonzero_coefficients(self): + # regression test for the wrong coefficients calculation + # due to bug https://github.com/python-pillow/Pillow/issues/2161 + 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 if __name__ == '__main__': diff --git a/Tests/test_image_resize.py b/Tests/test_image_resize.py index 73f8091ed..de7b6abc3 100644 --- a/Tests/test_image_resize.py +++ b/Tests/test_image_resize.py @@ -39,14 +39,14 @@ class TestImagingCoreResize(PillowTestCase): self.assertEqual(r.im.bands, im.im.bands) def test_reduce_filters(self): - for f in [Image.LINEAR, Image.BOX, Image.BILINEAR, Image.HAMMING, + 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.LINEAR, Image.BOX, Image.BILINEAR, Image.HAMMING, + 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") @@ -66,13 +66,13 @@ class TestImagingCoreResize(PillowTestCase): } samples['dirty'].putpixel((1, 1), 128) - for f in [Image.LINEAR, Image.BOX, Image.BILINEAR, Image.HAMMING, + for f in [Image.NEAREST, Image.BOX, Image.BILINEAR, Image.HAMMING, Image.BICUBIC, Image.LANCZOS]: # samples resized with current filter - references = dict( - (name, self.resize(ch, (4, 4), f)) + references = { + name: self.resize(ch, (4, 4), f) for name, ch in samples.items() - ) + } for mode, channels_set in [ ('RGB', ('blank', 'filled', 'dirty')), @@ -89,6 +89,17 @@ class TestImagingCoreResize(PillowTestCase): # as separately resized channel 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]: + r = self.resize(Image.new('RGB', (0,0), "white"), (212, 195), f) + self.assertEqual(r.mode, "RGB") + self.assertEqual(r.size, (212, 195)) + self.assertEqual(r.getdata()[0], (0,0,0)) + + def test_unknown_filter(self): + self.assertRaises(ValueError, self.resize, hopper(), (10, 10), 9) + class TestImageResize(PillowTestCase): diff --git a/Tests/test_image_rotate.py b/Tests/test_image_rotate.py index e90b9a592..9ee01db44 100644 --- a/Tests/test_image_rotate.py +++ b/Tests/test_image_rotate.py @@ -4,23 +4,100 @@ from PIL import Image class TestImageRotate(PillowTestCase): - def test_rotate(self): - def rotate(im, mode, angle): - out = im.rotate(angle) - self.assertEqual(out.mode, mode) - self.assertEqual(out.size, im.size) # default rotate clips output - out = im.rotate(angle, expand=1) - self.assertEqual(out.mode, mode) - if angle % 180 == 0: - self.assertEqual(out.size, im.size) - else: - self.assertNotEqual(out.size, im.size) - for mode in "1", "P", "L", "RGB", "I", "F": + def rotate(self, im, mode, angle, center=None, translate=None): + out = im.rotate(angle, center=center, translate=translate) + self.assertEqual(out.mode, mode) + self.assertEqual(out.size, im.size) # default rotate clips output + out = im.rotate(angle, center=center, translate=translate, expand=1) + self.assertEqual(out.mode, mode) + if angle % 180 == 0: + self.assertEqual(out.size, im.size) + elif im.size == (0, 0): + self.assertEqual(out.size, im.size) + else: + self.assertNotEqual(out.size, im.size) + + def test_mode(self): + for mode in ("1", "P", "L", "RGB", "I", "F"): im = hopper(mode) - rotate(im, mode, 45) - for angle in 0, 90, 180, 270: + self.rotate(im, mode, 45) + + def test_angle(self): + for angle in (0, 90, 180, 270): im = Image.open('Tests/images/test-card.png') - rotate(im, im.mode, angle) + self.rotate(im, im.mode, angle) + + def test_zero(self): + for angle in (0, 45, 90, 180, 270): + im = Image.new('RGB',(0,0)) + self.rotate(im, im.mode, angle) + + def test_resample(self): + # Target image creation, inspected by eye. + # >>> im = Image.open('Tests/images/hopper.ppm') + # >>> im = im.rotate(45, resample=Image.BICUBIC, expand=True) + # >>> im.save('Tests/images/hopper_45.png') + + target = Image.open('Tests/images/hopper_45.png') + for (resample, epsilon) in ((Image.NEAREST, 10), + (Image.BILINEAR, 5), + (Image.BICUBIC, 0)): + im = hopper() + im = im.rotate(45, resample=resample, expand=True) + self.assert_image_similar(im, target, epsilon) + + def test_center_0(self): + im = hopper() + target = Image.open('Tests/images/hopper_45.png') + target_origin = target.size[1]/2 + target = target.crop((0, target_origin, 128, target_origin + 128)) + + im = im.rotate(45, center=(0,0), resample=Image.BICUBIC) + + self.assert_image_similar(im, target, 15) + + def test_center_14(self): + im = hopper() + target = Image.open('Tests/images/hopper_45.png') + target_origin = target.size[1] / 2 - 14 + target = target.crop((6, target_origin, 128 + 6, target_origin + 128)) + + im = im.rotate(45, center=(14,14), resample=Image.BICUBIC) + + self.assert_image_similar(im, target, 10) + + def test_translate(self): + im = hopper() + target = Image.open('Tests/images/hopper_45.png') + target_origin = (target.size[1] / 2 - 64) - 5 + target = target.crop((target_origin, target_origin, + target_origin + 128, target_origin + 128)) + + im = im.rotate(45, translate=(5,5), resample=Image.BICUBIC) + + self.assert_image_similar(im, target, 1) + + def test_fastpath_center(self): + # if the center is -1,-1 and we rotate by 90<=x<=270 the + # resulting image should be black + for angle in (90, 180, 270): + im = hopper().rotate(angle, center=(-1,-1)) + self.assert_image_equal(im, Image.new('RGB', im.size, 'black')) + + def test_fastpath_translate(self): + # if we post-translate by -128 + # resulting image should be black + for angle in (0, 90, 180, 270): + im = hopper().rotate(angle, translate=(-128,-128)) + self.assert_image_equal(im, Image.new('RGB', im.size, 'black')) + + def test_center(self): + 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)) + + if __name__ == '__main__': diff --git a/Tests/test_image_toqimage.py b/Tests/test_image_toqimage.py index 0dd9751d3..4a318c5c4 100644 --- a/Tests/test_image_toqimage.py +++ b/Tests/test_image_toqimage.py @@ -5,7 +5,17 @@ from PIL import ImageQt if ImageQt.qt_is_installed: - from PIL.ImageQt import QImage + from PIL.ImageQt import QImage, QPixmap + + try: + from PyQt5 import QtGui + except (ImportError, RuntimeError): + try: + from PyQt4 import QtGui + except (ImportError, RuntimeError): + from PySide import QtGui + + class TestToQImage(PillowQtTestCase, PillowTestCase): @@ -19,9 +29,45 @@ class TestToQImage(PillowQtTestCase, PillowTestCase): self.assertFalse(data.isNull()) # Test saving the file - tempfile = self.tempfile('temp_{0}.png'.format(mode)) + tempfile = self.tempfile('temp_{}.png'.format(mode)) data.save(tempfile) + def test_segfault(self): + PillowQtTestCase.setUp(self) + + app = QtGui.QApplication([]) + ex = Example() + + +if ImageQt.qt_is_installed: + class Example(QtGui.QWidget): + + def __init__(self): + super(Example, self).__init__() + + img = hopper().resize((1000,1000)) + + qimage = ImageQt.ImageQt(img) + + pixmap1 = QtGui.QPixmap.fromImage(qimage) + + hbox = QtGui.QHBoxLayout(self) + + lbl = QtGui.QLabel(self) + # Segfault in the problem + lbl.setPixmap(pixmap1.copy()) + + + + +def main(): + app = QtGui.QApplication(sys.argv) + ex = Example() + sys.exit(app.exec_()) + + + + if __name__ == '__main__': unittest.main() diff --git a/Tests/test_image_toqpixmap.py b/Tests/test_image_toqpixmap.py index 0bed9c747..c6555d7ff 100644 --- a/Tests/test_image_toqpixmap.py +++ b/Tests/test_image_toqpixmap.py @@ -19,7 +19,7 @@ class TestToQPixmap(PillowQPixmapTestCase, PillowTestCase): self.assertFalse(data.isNull()) # Test saving the file - tempfile = self.tempfile('temp_{0}.png'.format(mode)) + tempfile = self.tempfile('temp_{}.png'.format(mode)) data.save(tempfile) diff --git a/Tests/test_image_transform.py b/Tests/test_image_transform.py index 16e2e4850..da254eaf6 100644 --- a/Tests/test_image_transform.py +++ b/Tests/test_image_transform.py @@ -140,6 +140,10 @@ class TestImageTransform(PillowTestCase): self.test_mesh() + def test_missing_method_data(self): + self.assertRaises(ValueError, lambda: + hopper().transform((100, 100), None)) + class TestImageTransformAffine(PillowTestCase): transform = Image.AFFINE diff --git a/Tests/test_imagecms.py b/Tests/test_imagecms.py index e1a3e0af5..136590667 100644 --- a/Tests/test_imagecms.py +++ b/Tests/test_imagecms.py @@ -205,7 +205,7 @@ class TestImageCms(PillowTestCase): target = Image.open('Tests/images/hopper.Lab.tif') - self.assert_image_similar(i, target, 30) + self.assert_image_similar(i, target, 3.5) def test_lab_srgb(self): psRGB = ImageCms.createProfile("sRGB") @@ -326,12 +326,12 @@ class TestImageCms(PillowTestCase): prepatch, these would segfault, postpatch they should emit a typeerror """ - + with self.assertRaises(TypeError): ImageCms.ImageCmsProfile(0).tobytes() with self.assertRaises(TypeError): ImageCms.ImageCmsProfile(1).tobytes() - + if __name__ == '__main__': unittest.main() diff --git a/Tests/test_imagefont.py b/Tests/test_imagefont.py index de89ac929..5207dce38 100644 --- a/Tests/test_imagefont.py +++ b/Tests/test_imagefont.py @@ -125,7 +125,9 @@ try: target = 'Tests/images/rectangle_surrounding_text.png' target_img = Image.open(target) - self.assert_image_similar(im, target_img, .5) + + # Epsilon ~.5 fails with FreeType 2.7 + self.assert_image_similar(im, target_img, 2.5) def test_render_multiline(self): im = Image.new(mode='RGB', size=(300, 100)) @@ -144,7 +146,7 @@ try: # some versions of freetype have different horizontal spacing. # setting a tight epsilon, I'm showing the original test failure # at epsilon = ~38. - self.assert_image_similar(im, target_img, .5) + self.assert_image_similar(im, target_img, 6.2) def test_render_multiline_text(self): ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) @@ -158,7 +160,8 @@ try: target = 'Tests/images/multiline_text.png' target_img = Image.open(target) - self.assert_image_similar(im, target_img, .5) + # Epsilon ~.5 fails with FreeType 2.7 + self.assert_image_similar(im, target_img, 6.2) # Test that text() can pass on additional arguments # to multiline_text() @@ -178,7 +181,8 @@ try: target = 'Tests/images/multiline_text'+ext+'.png' target_img = Image.open(target) - self.assert_image_similar(im, target_img, .5) + # Epsilon ~.5 fails with FreeType 2.7 + self.assert_image_similar(im, target_img, 6.2) def test_unknown_align(self): im = Image.new(mode='RGB', size=(300, 100)) @@ -227,7 +231,8 @@ try: target = 'Tests/images/multiline_text_spacing.png' target_img = Image.open(target) - self.assert_image_similar(im, target_img, .5) + # Epsilon ~.5 fails with FreeType 2.7 + self.assert_image_similar(im, target_img, 6.2) def test_rotated_transposed_font(self): img_grey = Image.new("L", (100, 100)) diff --git a/Tests/test_imagemath.py b/Tests/test_imagemath.py index a5295c4f9..4f99eda93 100644 --- a/Tests/test_imagemath.py +++ b/Tests/test_imagemath.py @@ -9,7 +9,7 @@ def pixel(im): if hasattr(im, "im"): return "%s %r" % (im.mode, im.getpixel((0, 0))) else: - if isinstance(im, type(0)): + if isinstance(im, int): return int(im) # hack to deal with booleans print(im) diff --git a/Tests/test_imagemorph.py b/Tests/test_imagemorph.py index 22372b78d..ea54417a2 100644 --- a/Tests/test_imagemorph.py +++ b/Tests/test_imagemorph.py @@ -25,8 +25,8 @@ class MorphTests(PillowTestCase): chars = '.1' width, height = im.size return '\n'.join( - [''.join([chars[im.getpixel((c, r)) > 0] for c in range(width)]) - for r in range(height)]) + ''.join(chars[im.getpixel((c, r)) > 0] for c in range(width)) + for r in range(height)) def string_to_img(self, image_string): """Turn a string image representation into a binary image""" diff --git a/Tests/test_imagepalette.py b/Tests/test_imagepalette.py index e26c242b0..f80fa34c8 100644 --- a/Tests/test_imagepalette.py +++ b/Tests/test_imagepalette.py @@ -1,6 +1,6 @@ from helper import unittest, PillowTestCase -from PIL import ImagePalette +from PIL import ImagePalette, Image ImagePalette = ImagePalette.ImagePalette @@ -125,6 +125,19 @@ class TestImagePalette(PillowTestCase): self.assertEqual(rawmode, "RGB") self.assertEqual(data_in, data_out) + def test_2bit_palette(self): + # issue #2258, 2 bit palettes are corrupted. + outfile = self.tempfile('temp.png') + + rgb = b'\x00' * 2 + b'\x01' * 2 + b'\x02' * 2 + img = Image.frombytes('P', (6, 1), rgb) + img.putpalette(b'\xFF\x00\x00\x00\xFF\x00\x00\x00\xFF') # RGB + img.save(outfile, format='PNG') + + reloaded = Image.open(outfile) + + self.assert_image_equal(img, reloaded) + if __name__ == '__main__': unittest.main() diff --git a/Tests/test_imagepath.py b/Tests/test_imagepath.py index d5ec5dfaa..14cc4d14b 100644 --- a/Tests/test_imagepath.py +++ b/Tests/test_imagepath.py @@ -63,7 +63,9 @@ class TestImagePath(PillowTestCase): self.assertEqual(list(p), [(0.0, 1.0)]) def test_overflow_segfault(self): - try: + # Some Pythons fail getting the argument as an integer, and it falls + # through to the sequence. Seeing this on 32-bit Windows. + with self.assertRaises((TypeError, MemoryError)): # post patch, this fails with a memory error x = evil() @@ -74,14 +76,6 @@ class TestImagePath(PillowTestCase): x[i] = "0"*16 else: x[i] = b'0'*16 - except TypeError as msg: - # Some pythons fail getting the argument as an integer, and - # it falls through to the sequence. Seeing this on 32bit windows. - self.assertTrue(True, "Sequence required") - except MemoryError as msg: - self.assertTrue(msg) - except: - self.assertTrue(False, "Should have received a memory error") class evil: diff --git a/Tests/test_imagetk.py b/Tests/test_imagetk.py index f56333a59..1d5a281a6 100644 --- a/Tests/test_imagetk.py +++ b/Tests/test_imagetk.py @@ -1,22 +1,29 @@ -from helper import unittest, PillowTestCase +from helper import unittest, PillowTestCase, hopper from PIL import Image + try: from PIL import ImageTk + import Tkinter as tk dir(ImageTk) + HAS_TK = True except (OSError, ImportError) as v: # Skipped via setUp() - pass - + HAS_TK = False + +TK_MODES = ('1', 'L', 'P', 'RGB', 'RGBA') class TestImageTk(PillowTestCase): def setUp(self): + if not HAS_TK: + self.skipTest("Tk not installed") try: - from PIL import ImageTk - dir(ImageTk) - except (OSError, ImportError) as v: - self.skipTest(v) + # setup tk + app = tk.Frame() + #root = tk.Tk() + except (tk.TclError) as v: + self.skipTest("TCL Error: %s" % v) def test_kw(self): TEST_JPG = "Tests/images/hopper.jpg" @@ -40,5 +47,47 @@ class TestImageTk(PillowTestCase): self.assertEqual(im, None) + def test_photoimage(self): + for mode in TK_MODES: + # test as image: + im = hopper(mode) + + # this should not crash + im_tk = ImageTk.PhotoImage(im) + + self.assertEqual(im_tk.width(), im.width) + self.assertEqual(im_tk.height(), im.height) + + # _tkinter.TclError: this function is not yet supported + #reloaded = ImageTk.getimage(im_tk) + #self.assert_image_equal(reloaded, im) + + + + def test_photoimage_blank(self): + # test a image using mode/size: + for mode in TK_MODES: + im_tk = ImageTk.PhotoImage(mode, (100,100)) + + self.assertEqual(im_tk.width(), 100) + self.assertEqual(im_tk.height(), 100) + + #reloaded = ImageTk.getimage(im_tk) + #self.assert_image_equal(reloaded, im) + + def test_bitmapimage(self): + im = hopper('1') + + # this should not crash + im_tk = ImageTk.BitmapImage(im) + + self.assertEqual(im_tk.width(), im.width) + self.assertEqual(im_tk.height(), im.height) + + #reloaded = ImageTk.getimage(im_tk) + #self.assert_image_equal(reloaded, im) + + + if __name__ == '__main__': unittest.main() diff --git a/Tests/test_imagewin_pointers.py b/Tests/test_imagewin_pointers.py index f37c67eac..7178d8cb8 100644 --- a/Tests/test_imagewin_pointers.py +++ b/Tests/test_imagewin_pointers.py @@ -75,11 +75,7 @@ if sys.platform.startswith('win32'): memcpy(bp, ctypes.byref(bf), ctypes.sizeof(bf)) memcpy(bp + ctypes.sizeof(bf), ctypes.byref(bi), bi.biSize) memcpy(bp + bf.bfOffBits, pixels, bi.biSizeImage) - try: - return bytearray(buf) - except ValueError: - # py2.6 - return buffer(buf)[:] + return bytearray(buf) class TestImageWinPointers(PillowTestCase): def test_pointer(self): diff --git a/Tests/test_locale.py b/Tests/test_locale.py index 3f6ce0ade..142753791 100644 --- a/Tests/test_locale.py +++ b/Tests/test_locale.py @@ -1,3 +1,4 @@ +from __future__ import print_function from helper import unittest, PillowTestCase from PIL import Image @@ -8,10 +9,10 @@ import locale # on windows, in polish locale: # import locale -# print locale.setlocale(locale.LC_ALL, 'polish') +# print(locale.setlocale(locale.LC_ALL, 'polish')) # import string -# print len(string.whitespace) -# print ord(string.whitespace[6]) +# print(len(string.whitespace)) +# print(ord(string.whitespace[6])) # Polish_Poland.1250 # 7 diff --git a/Tests/test_map.py b/Tests/test_map.py new file mode 100644 index 000000000..14bd835a2 --- /dev/null +++ b/Tests/test_map.py @@ -0,0 +1,28 @@ +from helper import PillowTestCase, unittest +import sys + +from PIL import Image + + +@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 + # if there are > SIZE_MAX bytes in the image or if + # the file encodes an offset that makes + # (offset + size(bytes)) > SIZE_MAX + + # Note that this image triggers the decompression bomb warning: + max_pixels = Image.MAX_IMAGE_PIXELS + Image.MAX_IMAGE_PIXELS = None + + # This image hits the offset test. + im = Image.open('Tests/images/l2rgb_read.bmp') + with self.assertRaises((ValueError, MemoryError, IOError)): + im.load() + + Image.MAX_IMAGE_PIXELS = max_pixels + + +if __name__ == '__main__': + unittest.main() diff --git a/Tests/test_numpy.py b/Tests/test_numpy.py index 370b8c1f9..8a530ce1b 100644 --- a/Tests/test_numpy.py +++ b/Tests/test_numpy.py @@ -54,7 +54,7 @@ class TestNumpy(PillowTestCase): i = Image.fromarray(a) if list(i.split()[0].getdata()) != list(range(100)): print("data mismatch for", dtype) - # print dtype, list(i.getdata()) + # print(dtype, list(i.getdata())) return i # Check supported 1-bit integer formats @@ -135,7 +135,12 @@ class TestNumpy(PillowTestCase): img = Image.fromarray(arr * 255).convert('1') self.assertEqual(img.mode, '1') arr_back = numpy.array(img) - numpy.testing.assert_array_equal(arr, arr_back) + # numpy 1.8 and earlier return this as a boolean. (trusty/precise) + if arr_back.dtype == numpy.bool: + arr_bool = numpy.array([[1, 0, 0, 1, 0], [0, 1, 0, 0, 0]], 'bool') + numpy.testing.assert_array_equal(arr_bool, arr_back) + else: + numpy.testing.assert_array_equal(arr, arr_back) def test_save_tiff_uint16(self): # Tests that we're getting the pixel value in the right byte order. diff --git a/Tests/test_olefileio.py b/Tests/test_olefileio.py deleted file mode 100644 index 28c39afb5..000000000 --- a/Tests/test_olefileio.py +++ /dev/null @@ -1,148 +0,0 @@ -from helper import unittest, PillowTestCase - -import datetime - -import PIL.OleFileIO as OleFileIO - - -class TestOleFileIo(PillowTestCase): - - def test_isOleFile(self): - ole_file = "Tests/images/test-ole-file.doc" - - self.assertTrue(OleFileIO.isOleFile(ole_file)) - with open(ole_file, 'rb') as fp: - self.assertTrue(OleFileIO.isOleFile(fp)) - self.assertTrue(OleFileIO.isOleFile(fp.read())) - - non_ole_file = "Tests/images/flower.jpg" - - self.assertFalse(OleFileIO.isOleFile(non_ole_file)) - with open(non_ole_file, 'rb') as fp: - self.assertFalse(OleFileIO.isOleFile(fp)) - self.assertFalse(OleFileIO.isOleFile(fp.read())) - - def test_exists_worddocument(self): - # Arrange - ole_file = "Tests/images/test-ole-file.doc" - ole = OleFileIO.OleFileIO(ole_file) - - # Act - exists = ole.exists('worddocument') - - # Assert - self.assertTrue(exists) - ole.close() - - def test_exists_no_vba_macros(self): - # Arrange - ole_file = "Tests/images/test-ole-file.doc" - ole = OleFileIO.OleFileIO(ole_file) - - # Act - exists = ole.exists('macros/vba') - - # Assert - self.assertFalse(exists) - ole.close() - - def test_get_type(self): - # Arrange - ole_file = "Tests/images/test-ole-file.doc" - ole = OleFileIO.OleFileIO(ole_file) - - # Act - entry_type = ole.get_type('worddocument') - - # Assert - self.assertEqual(entry_type, OleFileIO.STGTY_STREAM) - ole.close() - - def test_get_size(self): - # Arrange - ole_file = "Tests/images/test-ole-file.doc" - ole = OleFileIO.OleFileIO(ole_file) - - # Act - size = ole.get_size('worddocument') - - # Assert - self.assertGreater(size, 0) - ole.close() - - def test_get_rootentry_name(self): - # Arrange - ole_file = "Tests/images/test-ole-file.doc" - ole = OleFileIO.OleFileIO(ole_file) - - # Act - root = ole.get_rootentry_name() - - # Assert - self.assertEqual(root, "Root Entry") - ole.close() - - def test_meta(self): - # Arrange - ole_file = "Tests/images/test-ole-file.doc" - ole = OleFileIO.OleFileIO(ole_file) - - # Act - meta = ole.get_metadata() - - # Assert - self.assertEqual(meta.author, b"Laurence Ipsum") - self.assertEqual(meta.num_pages, 1) - ole.close() - - def test_gettimes(self): - # Arrange - ole_file = "Tests/images/test-ole-file.doc" - ole = OleFileIO.OleFileIO(ole_file) - root_entry = ole.direntries[0] - - # Act - ctime = root_entry.getctime() - mtime = root_entry.getmtime() - - # Assert - self.assertIsInstance(ctime, type(None)) - self.assertIsInstance(mtime, datetime.datetime) - self.assertEqual(ctime, None) - self.assertEqual(mtime.year, 2014) - ole.close() - - def test_listdir(self): - # Arrange - ole_file = "Tests/images/test-ole-file.doc" - ole = OleFileIO.OleFileIO(ole_file) - - # Act - dirlist = ole.listdir() - - # Assert - self.assertIn(['WordDocument'], dirlist) - ole.close() - - def test_debug(self): - # Arrange - ole_file = "Tests/images/test-ole-file.doc" - ole = OleFileIO.OleFileIO(ole_file) - meta = ole.get_metadata() - - # Act - OleFileIO.set_debug_mode(True) - ole.dumpdirectory() - meta.dump() - - OleFileIO.set_debug_mode(False) - ole.dumpdirectory() - meta.dump() - - # Assert - # No assert, just check they run ok - ole.close() - - -if __name__ == '__main__': - unittest.main() diff --git a/Tests/test_psdraw.py b/Tests/test_psdraw.py index 31a2de33d..50e8763e2 100644 --- a/Tests/test_psdraw.py +++ b/Tests/test_psdraw.py @@ -35,12 +35,10 @@ class TestPsDraw(PillowTestCase): # Arrange tempfile = self.tempfile('temp.ps') - fp = open(tempfile, "wb") - - # Act - ps = PSDraw.PSDraw(fp) - self._create_document(ps) - fp.close() + with open(tempfile, "wb") as fp: + # Act + ps = PSDraw.PSDraw(fp) + self._create_document(ps) # Assert # Check non-zero file was created diff --git a/Tests/test_scipy.py b/Tests/test_scipy.py index dda49e707..1f4f016d1 100644 --- a/Tests/test_scipy.py +++ b/Tests/test_scipy.py @@ -1,10 +1,11 @@ -from helper import PillowTestCase - +from helper import unittest, PillowTestCase +from distutils.version import StrictVersion 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 @@ -27,6 +28,11 @@ class Test_scipy_resize(PillowTestCase): 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 + (StrictVersion(scipy.__version__) < StrictVersion('0.14.0')), + "Test fails on scipy < 0.14.0") def test_imresize4(self): im = np.array([[1, 2], [3, 4]]) @@ -41,3 +47,7 @@ class Test_scipy_resize(PillowTestCase): assert_equal(im2, res) assert_equal(im3, res) assert_equal(im4, res) + + +if __name__ == '__main__': + unittest.main() diff --git a/Tests/test_tiff_ifdrational.py b/Tests/test_tiff_ifdrational.py index dd3ad1b3d..ffd4f9eac 100644 --- a/Tests/test_tiff_ifdrational.py +++ b/Tests/test_tiff_ifdrational.py @@ -1,6 +1,6 @@ from __future__ import print_function -from helper import PillowTestCase, hopper +from helper import unittest, PillowTestCase, hopper from PIL import TiffImagePlugin, Image from PIL.TiffImagePlugin import IFDRational @@ -47,7 +47,7 @@ class Test_IFDRational(PillowTestCase): def test_ifd_rational_save(self): methods = (True, False) if 'libtiff_encoder' not in dir(Image.core): - methods = (False) + methods = (False,) for libtiff in methods: TiffImagePlugin.WRITE_LIBTIFF = libtiff @@ -60,3 +60,6 @@ class Test_IFDRational(PillowTestCase): reloaded = Image.open(out) self.assertEqual(float(IFDRational(301, 1)), float(reloaded.tag_v2[282])) + +if __name__ == '__main__': + unittest.main() diff --git a/Tk/README.rst b/Tk/README.rst deleted file mode 100644 index 61385effb..000000000 --- a/Tk/README.rst +++ /dev/null @@ -1,285 +0,0 @@ -Using PIL With Tkinter -==================================================================== - -Starting with 1.0 final (release candidate 2 and later, to be -precise), PIL can attach itself to Tkinter in flight. As a result, -you no longer need to rebuild the Tkinter extension to be able to -use PIL. - -However, if you cannot get the this to work on your platform, you -can do it in the old way: - -Adding Tkinter support ----------------------- - -1. Compile Python's _tkinter.c with the WITH_APPINIT and WITH_PIL - flags set, and link it with tkImaging.c and tkappinit.c. To - do this, copy the former to the Modules directory, and edit - the _tkinter line in Setup (or Setup.in) according to the - instructions in that file. - - NOTE: if you have an old Python version, the tkappinit.c - file is not included by default. If this is the case, you - will have to add the following lines to tkappinit.c, after - the MOREBUTTONS stuff:: - - { - extern void TkImaging_Init(Tcl_Interp* interp); - TkImaging_Init(interp); - } - - This registers a Tcl command called "PyImagingPhoto", which is - use to communicate between PIL and Tk's PhotoImage handler. - - You must also change the _tkinter line in Setup (or Setup.in) - to something like:: - - _tkinter _tkinter.c tkImaging.c tkappinit.c -DWITH_APPINIT - -I/usr/local/include -L/usr/local/lib -ltk8.0 -ltcl8.0 -lX11 - -The Photoimage Booster Patch (for Windows 95/NT) -==================================================================== - -This patch kit boosts performance for 16/24-bit displays. The -first patch is required on Tk 4.2 (where it fixes the problems for -16-bit displays) and later versions. By installing both patches, -Tk's PhotoImage handling becomes much faster on both 16-bit and -24-bit displays. The patch has been tested with Tk 4.2 and 8.0. - -Here's a benchmark, made with a sample program which loads two -512x512 greyscale PGM's, and two 512x512 colour PPM's, and displays -each of them in a separate toplevel windows. Tcl/Tk was compiled -with Visual C 4.0, and run on a P100 under Win95. Image load times -are not included in the timings: - -+----------------------+------------+-------------+----------------+ -| | **8-bit** | **16-bit** | **24-bit** | -+----------------------+------------+-------------+----------------+ -| 1. original 4.2 code | 5.52 s | 8.57 s | 3.79 s | -+----------------------+------------+-------------+----------------+ -| 2. booster patch | 5.49 s | 1.87 s | 1.82 s | -+----------------------+------------+-------------+----------------+ -| speedup | None | 4.6x | 2.1x | -+----------------------+------------+-------------+----------------+ - -Here's the patches: - -1. For portability and speed, the best thing under Windows is to -treat 16-bit displays as if they were 24-bit. The Windows device -drivers take care of the rest. - -.. Note:: - - If you have Tk 4.1 or Tk 8.0b1, you don't have to apply this - patch! It only applies to Tk 4.2, Tk 8.0a[12] and Tk 8.0b2. - -In ``win/tkWinImage.c``, change the following line in ``XCreateImage``:: - - imagePtr->bits_per_pixel = depth; - -to:: - - /* ==================================================================== */ - /* The tk photo image booster patch -- patch section 1 */ - /* ==================================================================== */ - - if (visual->class == TrueColor) - /* true colour is stored as 3 bytes: (blue, green, red) */ - imagePtr->bits_per_pixel = 24; - else - imagePtr->bits_per_pixel = depth; - - /* ==================================================================== */ - - -2. The DitherInstance implementation is not good. It's especially -bad on highend truecolour displays. IMO, it should be rewritten from -scratch (some other day...). - -Anyway, the following band-aid makes the situation a little bit -better under Windows. This hack trades some marginal quality (no -dithering on 16-bit displays) for a dramatic performance boost. -Requires patch 1, unless you're using Tk 4.1 or Tk 8.0b1. - -In generic/tkImgPhoto.c, add the #ifdef section to the DitherInstance -function:: - - /* ==================================================================== */ - - for (; height > 0; height -= nLines) { - if (nLines > height) { - nLines = height; - } - dstLinePtr = (unsigned char *) imagePtr->data; - yEnd = yStart + nLines; - - /* ==================================================================== */ - /* The tk photo image booster patch -- patch section 2 */ - /* ==================================================================== */ - - #ifdef __WIN32__ - if (colorPtr->visualInfo.class == TrueColor - && instancePtr->gamma == 1.0) { - /* Windows hicolor/truecolor booster */ - for (y = yStart; y < yEnd; ++y) { - destBytePtr = dstLinePtr; - srcPtr = srcLinePtr; - for (x = xStart; x < xEnd; ++x) { - destBytePtr[0] = srcPtr[2]; - destBytePtr[1] = srcPtr[1]; - destBytePtr[2] = srcPtr[0]; - destBytePtr += 3; srcPtr += 3; - } - srcLinePtr += lineLength; - dstLinePtr += bytesPerLine; - } - } else - #endif - - /* ==================================================================== */ - - for (y = yStart; y < yEnd; ++y) { - srcPtr = srcLinePtr; - errPtr = errLinePtr; - destBytePtr = dstLinePtr; - -The PIL Bitmap Booster Patch -==================================================================== - -The pilbitmap booster patch greatly improves performance of the -ImageTk.BitmapImage constructor. Unfortunately, the design of Tk -doesn't allow us to do this from the tkImaging interface module, so -you have to patch the Tk sources. - -Once installed, the ImageTk module will automatically detect this -patch. - -(Note: this patch has been tested with Tk 8.0 on Win32 only, but it -should work just fine on other platforms as well). - -1. To the beginning of TkGetBitmapData (in generic/tkImgBmap.c), add - the following stuff:: - - /* ==================================================================== */ - - int width, height, numBytes, hotX, hotY; - char *p, *end, *expandedFileName; - ParseInfo pi; - char *data = NULL; - Tcl_DString buffer; - - /* ==================================================================== */ - /* The pilbitmap booster patch -- patch section */ - /* ==================================================================== */ - - char *PILGetBitmapData(); - - if (string) { - /* Is this a PIL bitmap reference? */ - data = PILGetBitmapData(string, widthPtr, heightPtr, hotXPtr, hotYPtr); - if (data) - return data; - } - - /* ==================================================================== */ - - pi.string = string; - if (string == NULL) { - if (Tcl_IsSafe(interp)) { - -2. Append the following to the same file (you may wish to include -Imaging.h instead of copying the struct declaration...):: - - /* ==================================================================== */ - /* The pilbitmap booster patch -- code section */ - /* ==================================================================== */ - - /* Imaging declaration boldly copied from Imaging.h (!) */ - - typedef struct ImagingInstance *Imaging; /* a.k.a. ImagingImage :-) */ - - typedef unsigned char UINT8; - typedef int INT32; - - struct ImagingInstance { - - /* Format */ - char mode[4+1]; /* Band names ("1", "L", "P", "RGB", "RGBA", "CMYK") */ - int type; /* Always 0 in this version */ - int depth; /* Always 8 in this version */ - int bands; /* Number of bands (1, 3, or 4) */ - int xsize; /* Image dimension. */ - int ysize; - - /* Colour palette (for "P" images only) */ - void* palette; - - /* Data pointers */ - UINT8 **image8; /* Set for 8-bit image (pixelsize=1). */ - INT32 **image32; /* Set for 32-bit image (pixelsize=4). */ - - /* Internals */ - char **image; /* Actual raster data. */ - char *block; /* Set if data is allocated in a single block. */ - - int pixelsize; /* Size of a pixel, in bytes (1 or 4) */ - int linesize; /* Size of a line, in bytes (xsize * pixelsize) */ - - /* Virtual methods */ - void (*im_delete)(Imaging *); - - }; - - /* The pilbitmap booster patch allows you to pass PIL images to the - Tk bitmap decoder. Passing images this way is much more efficient - than using the "tobitmap" method. */ - - char * - PILGetBitmapData(string, widthPtr, heightPtr, hotXPtr, hotYPtr) - char *string; - int *widthPtr, *heightPtr; - int *hotXPtr, *hotYPtr; - { - char* data; - char* p; - int y; - Imaging im; - - if (strncmp(string, "PIL:", 4) != 0) - return NULL; - - im = (Imaging) atol(string + 4); - - if (strcmp(im->mode, "1") != 0 && strcmp(im->mode, "L") != 0) - return NULL; - - data = p = (char *) ckalloc((unsigned) ((im->xsize+7)/8) * im->ysize); - - for (y = 0; y < im->ysize; y++) { - char* in = im->image8[y]; - int i, m, b; - b = 0; m = 1; - for (i = 0; i < im->xsize; i++) { - if (in[i] != 0) - b |= m; - m <<= 1; - if (m == 256){ - *p++ = b; - b = 0; m = 1; - } - } - if (m != 1) - *p++ = b; - } - - *widthPtr = im->xsize; - *heightPtr = im->ysize; - *hotXPtr = -1; - *hotYPtr = -1; - - return data; - } - - /* ==================================================================== */ - -3. Recompile Tk and relink the _tkinter module (where necessary). \ No newline at end of file diff --git a/Tk/_tkmini.h b/Tk/_tkmini.h index 348425e41..adc470532 100644 --- a/Tk/_tkmini.h +++ b/Tk/_tkmini.h @@ -59,7 +59,7 @@ /* * Unless otherwise noted, these definitions are stable from Tcl / Tk 8.4 - * through Tck / Tk master as of 21 May 2016 + * through Tcl / Tk master as of 21 May 2016 */ #ifdef __cplusplus diff --git a/Tk/tkImaging.c b/Tk/tkImaging.c index 5999a140a..f448be166 100644 --- a/Tk/tkImaging.c +++ b/Tk/tkImaging.c @@ -24,10 +24,7 @@ * This registers a Tcl command called "PyImagingPhoto", which is used * to communicate between PIL and Tk's PhotoImage handler. * - * Compile and link tkImaging.c with tkappinit.c and _tkinter (see the - * Setup file for details on how to use tkappinit.c). Note that - * _tkinter.c must be compiled with WITH_APPINIT. - * + * History: * 1995-09-12 fl Created * 1996-04-08 fl Ready for release @@ -169,7 +166,7 @@ PyImagingPhotoPut(ClientData clientdata, Tcl_Interp* interp, return TCL_OK; } - +/* Warning -- this does not work at all */ static int PyImagingPhotoGet(ClientData clientdata, Tcl_Interp* interp, int argc, const char **argv) @@ -438,10 +435,19 @@ int load_tkinter_funcs(void) */ int ret = -1; - void *tkinter_lib; + void *main_program, *tkinter_lib; char *tkinter_libname; PyObject *pModule = NULL, *pString = NULL; + /* Try loading from the main program namespace first */ + main_program = dlopen(NULL, RTLD_LAZY); + if (_func_loader(main_program) == 0) { + return 0; + } + /* Clear exception triggered when we didn't find symbols above */ + PyErr_Clear(); + + /* Now try finding the tkinter compiled module */ pModule = PyImport_ImportModule(TKINTER_FINDER); if (pModule == NULL) { goto exit; diff --git a/_imaging.c b/_imaging.c index ef5d346ba..aa2e04778 100644 --- a/_imaging.c +++ b/_imaging.c @@ -71,7 +71,7 @@ * See the README file for information on usage and redistribution. */ -#define PILLOW_VERSION "3.4.0.dev0" +#define PILLOW_VERSION "4.1.0.dev0" #include "Python.h" @@ -251,7 +251,9 @@ int PyImaging_GetBuffer(PyObject* buffer, Py_buffer *view) /* Use new buffer protocol if available (mmap doesn't support this in 2.7, go figure) */ if (PyObject_CheckBuffer(buffer)) { - return PyObject_GetBuffer(buffer, view, PyBUF_SIMPLE); + int success = PyObject_GetBuffer(buffer, view, PyBUF_SIMPLE); + if (!success) { return success; } + PyErr_Clear(); } /* Pretend we support the new protocol; PyBuffer_Release happily ignores @@ -686,17 +688,6 @@ _radial_gradient(PyObject* self, PyObject* args) return PyImagingNew(ImagingFillRadialGradient(mode)); } -static PyObject* -_open_ppm(PyObject* self, PyObject* args) -{ - char* filename; - - if (!PyArg_ParseTuple(args, "s", &filename)) - return NULL; - - return PyImagingNew(ImagingOpenPPM(filename)); -} - static PyObject* _alpha_composite(ImagingObject* self, PyObject* args) { @@ -2988,9 +2979,6 @@ static struct PyMethodDef methods[] = { {"rankfilter", (PyCFunction)_rankfilter, 1}, #endif {"resize", (PyCFunction)_resize, 1}, - // There were two methods for image resize before. - // Starting from Pillow 2.7.0 stretch is depreciated. - {"stretch", (PyCFunction)_resize, 1}, {"transpose", (PyCFunction)_transpose, 1}, {"transform2", (PyCFunction)_transform2, 1}, @@ -3084,11 +3072,7 @@ _getattr_id(ImagingObject* self, void* closure) static PyObject* _getattr_ptr(ImagingObject* self, void* closure) { -#if PY_VERSION_HEX >= 0x02070000 return PyCapsule_New(self->image, IMAGING_MAGIC, NULL); -#else - return PyCObject_FromVoidPtrAndDesc(self->image, IMAGING_MAGIC, NULL); -#endif } static PyObject* @@ -3424,9 +3408,6 @@ static PyMethodDef functions[] = { {"crc32", (PyCFunction)_crc32, 1}, {"getcodecstatus", (PyCFunction)_getcodecstatus, 1}, - /* Debugging stuff */ - {"open_ppm", (PyCFunction)_open_ppm, 1}, - /* Special effects (experimental) */ #ifdef WITH_EFFECTS {"effect_mandelbrot", (PyCFunction)_effect_mandelbrot, 1}, diff --git a/_imagingtk.c b/_imagingtk.c index 87de36a04..d0295f317 100644 --- a/_imagingtk.c +++ b/_imagingtk.c @@ -36,18 +36,18 @@ _tkinit(PyObject* self, PyObject* args) { Tcl_Interp* interp; - Py_ssize_t arg; + PyObject* arg; int is_interp; - if (!PyArg_ParseTuple(args, "ni", &arg, &is_interp)) + if (!PyArg_ParseTuple(args, "Oi", &arg, &is_interp)) return NULL; if (is_interp) - interp = (Tcl_Interp*) arg; + interp = (Tcl_Interp*)PyLong_AsVoidPtr(arg); else { TkappObject* app; /* Do it the hard way. This will break if the TkappObject layout changes */ - app = (TkappObject*) arg; + app = (TkappObject*)PyLong_AsVoidPtr(arg); interp = app->interp; } diff --git a/appveyor.yml b/appveyor.yml index 826c4c1a5..deb6d3b1a 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,4 +1,4 @@ -version: 3.4.pre.{build} +version: 4.1.pre.{build} clone_folder: c:\pillow init: - ECHO %PYTHON% @@ -19,6 +19,7 @@ install: - git clone https://github.com/python-pillow/pillow-depends.git c:\pillow-depends - xcopy c:\pillow-depends\*.zip c:\pillow\winbuild\ - xcopy c:\pillow-depends\*.tar.gz c:\pillow\winbuild\ +- xcopy /s c:\pillow-depends\test_images\* c:\pillow\tests\images - cd c:\pillow\winbuild\ - c:\python34\python.exe c:\pillow\winbuild\build_dep.py - c:\pillow\winbuild\build_deps.cmd diff --git a/decode.c b/decode.c index f700747e1..91de1075e 100644 --- a/decode.c +++ b/decode.c @@ -52,7 +52,6 @@ typedef struct { struct ImagingCodecStateInstance state; Imaging im; PyObject* lock; - int handles_eof; int pulls_fd; } ImagingDecoderObject; @@ -95,9 +94,6 @@ PyImaging_DecoderNew(int contextsize) /* Initialize the cleanup function pointer */ decoder->cleanup = NULL; - /* Most decoders don't want to handle EOF themselves */ - decoder->handles_eof = 0; - /* set if the decoder needs to pull data from the fd, instead of having it pushed */ decoder->pulls_fd = 0; @@ -239,12 +235,6 @@ _setfd(ImagingDecoderObject* decoder, PyObject* args) } -static PyObject * -_get_handles_eof(ImagingDecoderObject *decoder) -{ - return PyBool_FromLong(decoder->handles_eof); -} - static PyObject * _get_pulls_fd(ImagingDecoderObject *decoder) { @@ -260,9 +250,6 @@ static struct PyMethodDef methods[] = { }; static struct PyGetSetDef getseters[] = { - {"handles_eof", (getter)_get_handles_eof, NULL, - "True if this decoder expects to handle EOF itself.", - NULL}, {"pulls_fd", (getter)_get_pulls_fd, NULL, "True if this decoder expects to pull from self.fd itself.", NULL}, @@ -918,7 +905,6 @@ PyImaging_Jpeg2KDecoderNew(PyObject* self, PyObject* args) if (decoder == NULL) return NULL; - decoder->handles_eof = 1; decoder->pulls_fd = 1; decoder->decode = ImagingJpeg2KDecode; decoder->cleanup = ImagingJpeg2KDecodeCleanup; diff --git a/depends/download-and-extract.sh b/depends/download-and-extract.sh new file mode 100755 index 000000000..9f82877db --- /dev/null +++ b/depends/download-and-extract.sh @@ -0,0 +1,12 @@ +#!/bin/bash +# Usage: ./download-and-extract.sh something.tar.gz https://example.com/something.tar.gz + +archive=$1 +url=$2 + +if [ ! -f $archive.tar.gz ]; then + wget -O $archive.tar.gz $url +fi + +rm -r $archive +tar -xvzf $archive.tar.gz diff --git a/depends/install_extra_test_images.sh b/depends/install_extra_test_images.sh new file mode 100755 index 000000000..00ed219d6 --- /dev/null +++ b/depends/install_extra_test_images.sh @@ -0,0 +1,11 @@ +#!/bin/bash +# install extra test images + +if [ ! -f test_images.tar.gz ]; then + wget -O 'test_images.tar.gz' 'https://github.com/python-pillow/pillow-depends/blob/master/test_images.tar.gz?raw=true' +fi + +rm -r test_images +tar -xvzf test_images.tar.gz + +cp -r test_images/* ../Tests/images diff --git a/depends/install_imagequant.sh b/depends/install_imagequant.sh index dd497dc3c..4a8c0e6be 100755 --- a/depends/install_imagequant.sh +++ b/depends/install_imagequant.sh @@ -1,12 +1,14 @@ #!/bin/bash # install libimagequant -git clone -b 2.6.0 https://github.com/pornel/pngquant +archive=libimagequant-2.8.2 -pushd pngquant +./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/master/$archive.tar.gz -make -C lib shared -sudo cp lib/libimagequant.so* /usr/lib/ -sudo cp lib/libimagequant.h /usr/include/ +pushd $archive + +make shared +sudo cp libimagequant.so* /usr/lib/ +sudo cp libimagequant.h /usr/include/ popd diff --git a/depends/install_openjpeg.sh b/depends/install_openjpeg.sh index fb7d3e9e4..cac86dbbb 100755 --- a/depends/install_openjpeg.sh +++ b/depends/install_openjpeg.sh @@ -1,19 +1,12 @@ #!/bin/bash # install openjpeg +archive=openjpeg-2.1.2 -if [ ! -f openjpeg-2.1.0.tar.gz ]; then - wget -O 'openjpeg-2.1.0.tar.gz' 'https://github.com/python-pillow/pillow-depends/blob/master/openjpeg-2.1.0.tar.gz?raw=true' +./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/master/$archive.tar.gz -fi - -rm -r openjpeg-2.1.0 -tar -xvzf openjpeg-2.1.0.tar.gz - - -pushd openjpeg-2.1.0 +pushd $archive cmake -DCMAKE_INSTALL_PREFIX=/usr . && make -j4 && sudo make -j4 install popd - diff --git a/depends/install_webp.sh b/depends/install_webp.sh index 08507ede6..8f700901c 100755 --- a/depends/install_webp.sh +++ b/depends/install_webp.sh @@ -1,14 +1,11 @@ #!/bin/bash # install webp -if [ ! -f libwebp-0.5.0.tar.gz ]; then - wget -O 'libwebp-0.5.0.tar.gz' 'https://github.com/python-pillow/pillow-depends/blob/master/libwebp-0.5.0.tar.gz?raw=true' -fi +archive=libwebp-0.5.2 -rm -r libwebp-0.5.0 -tar -xvzf libwebp-0.5.0.tar.gz +./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/master/$archive.tar.gz -pushd libwebp-0.5.0 +pushd $archive ./configure --prefix=/usr --enable-libwebpmux --enable-libwebpdemux && make -j4 && sudo make -j4 install diff --git a/docs/COPYING b/docs/COPYING index 6bdbe5b4e..2d4f92b41 100644 --- a/docs/COPYING +++ b/docs/COPYING @@ -5,7 +5,7 @@ The Python Imaging Library (PIL) is Pillow is the friendly PIL fork. It is - Copyright © 2012-2016 by Alex Clark and contributors + Copyright © 2010-2017 by Alex Clark and contributors Like PIL, Pillow is licensed under the open source PIL Software License: diff --git a/docs/conf.py b/docs/conf.py index f66bea521..33dd6a4f7 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -48,8 +48,8 @@ master_doc = 'index' # General information about the project. project = u'Pillow (PIL Fork)' -copyright = u'1995-2016, Fredrik Lundh and Contributors, Alex Clark and Contributors' -author = u'Fredrik Lundh and Contributors, Alex Clark and Contributors' +copyright = u'1995-2011 Fredrik Lundh, 2010-2017 Alex Clark and Contributors' +author = u'Fredrik Lundh, Alex Clark and Contributors' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the diff --git a/docs/handbook/concepts.rst b/docs/handbook/concepts.rst index 8c5de15d6..3023e1e67 100644 --- a/docs/handbook/concepts.rst +++ b/docs/handbook/concepts.rst @@ -93,7 +93,7 @@ Filters ------- For geometry operations that may map multiple input pixels to a single output -pixel, the Python Imaging Library provides four different resampling *filters*. +pixel, the Python Imaging Library provides different resampling *filters*. ``NEAREST`` Pick one nearest pixel from the input image. Ignore all other input pixels. diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index 61c7ed607..21339e75f 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -72,9 +72,6 @@ The :py:meth:`~PIL.Image.Image.open` method sets the following **background** Default background color (a palette color index). -**duration** - Time between frames in an animation (in milliseconds). - **transparency** Transparency color index. This key is omitted if the image is not transparent. @@ -82,9 +79,9 @@ The :py:meth:`~PIL.Image.Image.open` method sets the following **version** Version (either ``GIF87a`` or ``GIF89a``). -**duration** - May not be present. The time to display each frame of the GIF, in - milliseconds. +**duration** + May not be present. The time to display the current frame + of the GIF, in milliseconds. **loop** May not be present. The number of times the GIF should loop. @@ -98,20 +95,37 @@ the file by seeking to the first frame. Random access is not supported. ``im.seek()`` raises an ``EOFError`` if you try to seek after the last frame. -Saving sequences -~~~~~~~~~~~~~~~~ +Saving +~~~~~~ -When calling :py:meth:`~PIL.Image.Image.save`, if a multiframe image is used, -by default only the first frame will be saved. To save all frames, the -``save_all`` parameter must be present and set to ``True``. To append -additional frames when saving, the ``append_images`` parameter works with -``save_all`` to append a list of images containing the extra frames:: +When calling :py:meth:`~PIL.Image.Image.save`, the following options +are available:: im.save(out, save_all=True, append_images=[im1, im2, ...]) -If present, the ``loop`` parameter can be used to set the number of times -the GIF should loop, and the ``duration`` parameter can set the number of -milliseconds between each frame. +**save_all** + If present and true, all frames of the image will be saved. If + not, then only the first frame of a multiframe image will be saved. + +**append_images** + A list of images to append as additional frames. Each of the + images in the list can be single or multiframe images. + +**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. + +**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. Reading local images ~~~~~~~~~~~~~~~~~~~~ @@ -149,6 +163,19 @@ 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)``). +ICO +^^^ + +ICO is used to store icons on Windows. The largest available icon is read. + +The :py:meth:`~PIL.Image.Image.save` method supports the following options: + +**sizes** + A list of sizes including in this ico file; these are a 2-tuple, + ``(width, height)``; Default to ``[(16, 16), (24, 24), (32, 32), (48, 48), + (64, 64), (128, 128), (255, 255)]``. Any sizes bigger than the original + size or 255 will be ignored. + IM ^^ @@ -444,6 +471,12 @@ PPM PIL reads and writes PBM, PGM and PPM files containing ``1``, ``L`` or ``RGB`` data. +SGI +^^^ + +Pillow reads and writes uncompressed ``L``, ``RGB``, and ``RGBA`` files. + + SPIDER ^^^^^^ @@ -540,6 +573,11 @@ Saving Tiff Images The :py:meth:`~PIL.Image.Image.save` method can take the following keyword arguments: +**save_all** + If true, Pillow will save all frames of the image to a multiframe tiff document. + + .. versionadded:: 3.4.0 + **tiffinfo** A :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v2` object or dict object containing tiff tags and values. The TIFF field type is @@ -659,9 +697,10 @@ DDS DDS is a popular container texture format used in video games and natively supported by DirectX. -Currently, only DXT1 and DXT5 pixel formats are supported and only in ``RGBA`` +Currently, DXT1, DXT3, and DXT5 pixel formats are supported and only in ``RGBA`` mode. +.. versionadded:: 3.4.0 DXT3 FLI, FLC ^^^^^^^^ @@ -724,19 +763,6 @@ The :py:meth:`~PIL.Image.Image.open` method sets the following Transparency color index. This key is omitted if the image is not transparent. -ICO -^^^ - -ICO is used to store icons on Windows. The largest available icon is read. - -The :py:meth:`~PIL.Image.Image.save` method supports the following options: - -**sizes** - A list of sizes including in this ico file; these are a 2-tuple, - ``(width, height)``; Default to ``[(16, 16), (24, 24), (32, 32), (48, 48), - (64, 64), (128, 128), (255, 255)]``. Any size is bigger then the original - size or 255 will be ignored. - IMT ^^^ @@ -787,10 +813,6 @@ PSD PIL identifies and reads PSD files written by Adobe Photoshop 2.5 and 3.0. -SGI -^^^ - -PIL reads uncompressed ``L``, ``RGB``, and ``RGBA`` files. TGA ^^^ diff --git a/docs/installation.rst b/docs/installation.rst index 35b4e54fd..42a1e28fe 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -15,7 +15,9 @@ Notes .. note:: Pillow < 2.0.0 supports Python versions 2.4, 2.5, 2.6, 2.7. -.. note:: Pillow >= 2.0.0 supports Python versions 2.6, 2.7, 3.2, 3.3, 3.4, 3.5 +.. note:: Pillow >= 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 supports Python versions 2.7, 3.3, 3.4, 3.5, 3.6 Basic Installation ------------------ @@ -59,7 +61,7 @@ or:: macOS Installation -^^^^^^^^^^^^^^^^^ +^^^^^^^^^^^^^^^^^^ We provide binaries for macOS for each of the supported Python versions in the wheel format. These include support for all optional libraries @@ -124,8 +126,8 @@ Many of Pillow's features require external libraries: * **libjpeg** provides JPEG functionality. - * Pillow has been tested with libjpeg versions **6b**, **8**, **9**, and - **9a** and libjpeg-turbo version **8**. + * Pillow has been tested with libjpeg versions **6b**, **8**, **9**, **9a**, + and **9b** 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. @@ -191,7 +193,7 @@ build with newly installed external libraries. Build Options ^^^^^^^^^^^^^ -* Environment Variable: ``MAX_CONCURRENCY=n``. By default, Pillow will +* Environment variable: ``MAX_CONCURRENCY=n``. By default, Pillow will use multiprocessing to build the extension on all available CPUs, but not more than 4. Setting ``MAX_CONCURRENCY`` to 1 will disable parallel building. @@ -221,7 +223,7 @@ Build Options stdout. -Sample Usage:: +Sample usage:: $ MAX_CONCURRENCY=1 python setup.py build_ext --enable-[feature] install @@ -231,7 +233,7 @@ or using pip:: Building on macOS -^^^^^^^^^^^^^^^^ +^^^^^^^^^^^^^^^^^ The Xcode command line tools are required to compile portions of Pillow. The tools are installed by running ``xcode-select --install`` @@ -336,7 +338,9 @@ current versions of Linux, macOS, and Windows. +----------------------------------+-------------+------------------------------+--------------------------------+-----------------------+ |**Operating system** |**Supported**|**Tested Python versions** |**Latest tested Pillow version**|**Tested processors** | +----------------------------------+-------------+------------------------------+--------------------------------+-----------------------+ -| Mac OS X 10.11 El Capitan |Yes | 2.7,3.3,3.4,3.5 | 3.3.0 |x86-64 | +| macOS 10.12 Sierra |Yes | 3.4,3.5,3.6 | 4.0.0 |x86-64 | ++----------------------------------+-------------+------------------------------+--------------------------------+-----------------------+ +| Mac OS X 10.11 El Capitan |Yes | 2.7,3.3,3.4,3.5 | 3.4.1 |x86-64 | +----------------------------------+-------------+------------------------------+--------------------------------+-----------------------+ | Mac OS X 10.10 Yosemite |Yes | 2.7,3.3,3.4 | 3.0.0 |x86-64 | +----------------------------------+-------------+------------------------------+--------------------------------+-----------------------+ @@ -352,10 +356,10 @@ current versions of Linux, macOS, and Windows. +----------------------------------+-------------+------------------------------+--------------------------------+-----------------------+ | Ubuntu Linux 10.04 LTS |Yes | 2.6 | 2.3.0 |x86,x86-64 | +----------------------------------+-------------+------------------------------+--------------------------------+-----------------------+ -| Ubuntu Linux 12.04 LTS |Yes | 2.6,2.7,3.2,3.3,3.4,3.5 | 3.1.0 |x86,x86-64 | -| | | PyPy2.4,PyPy3,v2.3 | | | +| Ubuntu Linux 12.04 LTS |Yes | 2.6,2.7,3.2,3.3,3.4,3.5 | 3.4.1 (CI target) |x86,x86-64 | +| | | PyPy5.3.1,PyPy3 v2.4.0 | | | | | | | | | -| | | 2.7,3.2 | 2.6.1 |ppc | +| | | 2.7,3.2 | 3.4.1 |ppc | +----------------------------------+-------------+------------------------------+--------------------------------+-----------------------+ | Ubuntu Linux 14.04 LTS |Yes | 2.7,3.4 | 3.1.0 |x86-64 | +----------------------------------+-------------+------------------------------+--------------------------------+-----------------------+ @@ -365,13 +369,15 @@ current versions of Linux, macOS, and Windows. +----------------------------------+-------------+------------------------------+--------------------------------+-----------------------+ | Gentoo Linux |Yes | 2.7,3.2 | 2.1.0 |x86-64 | +----------------------------------+-------------+------------------------------+--------------------------------+-----------------------+ +| Arch Linux |Yes | 2.7,3.6 | 4.0.0 |x86,x86-64 | ++----------------------------------+-------------+------------------------------+--------------------------------+-----------------------+ | FreeBSD 10.2 |Yes | 2.7,3.4 | 3.1.0 |x86-64 | +----------------------------------+-------------+------------------------------+--------------------------------+-----------------------+ -| Windows 7 Pro |Yes | 2.7,3.2,3.3 | 2.2.1 |x86-64 | +| Windows 7 Pro |Yes | 2.7,3.2,3.3 | 3.4.1 |x86-64 | +----------------------------------+-------------+------------------------------+--------------------------------+-----------------------+ | Windows Server 2008 R2 Enterprise|Yes | 3.3 | |x86-64 | +----------------------------------+-------------+------------------------------+--------------------------------+-----------------------+ -| Windows Server 2012 R2 |Yes | 2.7,3.3,3.4 | 3.0.0 |x86-64 | +| Windows Server 2012 R2 |Yes | 2.7,3.3,3.4 | 3.4.1 (CI target) |x86,x86-64 | +----------------------------------+-------------+------------------------------+--------------------------------+-----------------------+ | Windows 8 Pro |Yes | 2.6,2.7,3.2,3.3,3.4a3 | 2.2.0 |x86,x86-64 | +----------------------------------+-------------+------------------------------+--------------------------------+-----------------------+ diff --git a/docs/reference/ImageGrab.rst b/docs/reference/ImageGrab.rst index 8711c761d..39aaef6bc 100644 --- a/docs/reference/ImageGrab.rst +++ b/docs/reference/ImageGrab.rst @@ -2,7 +2,7 @@ .. py:currentmodule:: PIL.ImageGrab :py:mod:`ImageGrab` Module (macOS and Windows only) -================================================== +=================================================== The :py:mod:`ImageGrab` module can be used to copy the contents of the screen or the clipboard to a PIL image memory. diff --git a/docs/reference/OleFileIO.rst b/docs/reference/OleFileIO.rst deleted file mode 100644 index 791cb5ff3..000000000 --- a/docs/reference/OleFileIO.rst +++ /dev/null @@ -1,364 +0,0 @@ -.. py:module:: PIL.OleFileIO -.. py:currentmodule:: PIL.OleFileIO - -:py:mod:`OleFileIO` Module -=========================== - -The :py:mod:`OleFileIO` module reads Microsoft OLE2 files (also called -Structured Storage or Microsoft Compound Document File Format), such -as Microsoft Office documents, Image Composer and FlashPix files, and -Outlook messages. - -This module is the `OleFileIO\_PL`_ project by Philippe Lagadec, v0.42, -merged back into Pillow. - -.. _OleFileIO\_PL: http://www.decalage.info/python/olefileio - -How to use this module ----------------------- - -For more information, see also the file **PIL/OleFileIO.py**, sample -code at the end of the module itself, and docstrings within the code. - -About the structure of OLE files -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -An OLE file can be seen as a mini file system or a Zip archive: It -contains **streams** of data that look like files embedded within the -OLE file. Each stream has a name. For example, the main stream of a MS -Word document containing its text is named "WordDocument". - -An OLE file can also contain **storages**. A storage is a folder that -contains streams or other storages. For example, a MS Word document with -VBA macros has a storage called "Macros". - -Special streams can contain **properties**. A property is a specific -value that can be used to store information such as the metadata of a -document (title, author, creation date, etc). Property stream names -usually start with the character '05'. - -For example, a typical MS Word document may look like this: - -:: - - \x05DocumentSummaryInformation (stream) - \x05SummaryInformation (stream) - WordDocument (stream) - Macros (storage) - PROJECT (stream) - PROJECTwm (stream) - VBA (storage) - Module1 (stream) - ThisDocument (stream) - _VBA_PROJECT (stream) - dir (stream) - ObjectPool (storage) - -Test if a file is an OLE container -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Use isOleFile to check if the first bytes of the file contain the Magic -for OLE files, before opening it. isOleFile returns True if it is an OLE -file, False otherwise. - -.. code-block:: python - - assert OleFileIO.isOleFile('myfile.doc') - -Open an OLE file from disk -~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Create an OleFileIO object with the file path as parameter: - -.. code-block:: python - - ole = OleFileIO.OleFileIO('myfile.doc') - -Open an OLE file from a file-like object -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -This is useful if the file is not on disk, e.g. already stored in a -string or as a file-like object. - -.. code-block:: python - - ole = OleFileIO.OleFileIO(f) - -For example the code below reads a file into a string, then uses BytesIO -to turn it into a file-like object. - -.. code-block:: python - - data = open('myfile.doc', 'rb').read() - f = io.BytesIO(data) # or StringIO.StringIO for Python 2.x - ole = OleFileIO.OleFileIO(f) - -How to handle malformed OLE files -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -By default, the parser is configured to be as robust and permissive as -possible, allowing to parse most malformed OLE files. Only fatal errors -will raise an exception. It is possible to tell the parser to be more -strict in order to raise exceptions for files that do not fully conform -to the OLE specifications, using the raise\_defect option: - -.. code-block:: python - - ole = OleFileIO.OleFileIO('myfile.doc', raise_defects=DEFECT_INCORRECT) - -When the parsing is done, the list of non-fatal issues detected is -available as a list in the parsing\_issues attribute of the OleFileIO -object: - -.. code-block:: python - - print('Non-fatal issues raised during parsing:') - if ole.parsing_issues: - for exctype, msg in ole.parsing_issues: - print('- %s: %s' % (exctype.__name__, msg)) - else: - print('None') - -Syntax for stream and storage path -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Two different syntaxes are allowed for methods that need or return the -path of streams and storages: - -1) Either a **list of strings** including all the storages from the root - up to the stream/storage name. For example a stream called - "WordDocument" at the root will have ['WordDocument'] as full path. A - stream called "ThisDocument" located in the storage "Macros/VBA" will - be ['Macros', 'VBA', 'ThisDocument']. This is the original syntax - from PIL. While hard to read and not very convenient, this syntax - works in all cases. - -2) Or a **single string with slashes** to separate storage and stream - names (similar to the Unix path syntax). The previous examples would - be 'WordDocument' and 'Macros/VBA/ThisDocument'. This syntax is - easier, but may fail if a stream or storage name contains a slash. - -Both are case-insensitive. - -Switching between the two is easy: - -.. code-block:: python - - slash_path = '/'.join(list_path) - list_path = slash_path.split('/') - -Get the list of streams -~~~~~~~~~~~~~~~~~~~~~~~ - -listdir() returns a list of all the streams contained in the OLE file, -including those stored in storages. Each stream is listed itself as a -list, as described above. - -.. code-block:: python - - print(ole.listdir()) - -Sample result: - -.. code-block:: python - - [['\x01CompObj'], ['\x05DocumentSummaryInformation'], ['\x05SummaryInformation'] - , ['1Table'], ['Macros', 'PROJECT'], ['Macros', 'PROJECTwm'], ['Macros', 'VBA', - 'Module1'], ['Macros', 'VBA', 'ThisDocument'], ['Macros', 'VBA', '_VBA_PROJECT'] - , ['Macros', 'VBA', 'dir'], ['ObjectPool'], ['WordDocument']] - -As an option it is possible to choose if storages should also be listed, -with or without streams: - -.. code-block:: python - - ole.listdir (streams=False, storages=True) - -Test if known streams/storages exist: -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -exists(path) checks if a given stream or storage exists in the OLE file. - -.. code-block:: python - - if ole.exists('worddocument'): - print("This is a Word document.") - if ole.exists('macros/vba'): - print("This document seems to contain VBA macros.") - -Read data from a stream -~~~~~~~~~~~~~~~~~~~~~~~ - -openstream(path) opens a stream as a file-like object. - -The following example extracts the "Pictures" stream from a PPT file: - -.. code-block:: python - - pics = ole.openstream('Pictures') - data = pics.read() - - -Get information about a stream/storage -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Several methods can provide the size, type and timestamps of a given -stream/storage: - -get\_size(path) returns the size of a stream in bytes: - -.. code-block:: python - - s = ole.get_size('WordDocument') - -get\_type(path) returns the type of a stream/storage, as one of the -following constants: STGTY\_STREAM for a stream, STGTY\_STORAGE for a -storage, STGTY\_ROOT for the root entry, and False for a non existing -path. - -.. code-block:: python - - t = ole.get_type('WordDocument') - -get\_ctime(path) and get\_mtime(path) return the creation and -modification timestamps of a stream/storage, as a Python datetime object -with UTC timezone. Please note that these timestamps are only present if -the application that created the OLE file explicitly stored them, which -is rarely the case. When not present, these methods return None. - -.. code-block:: python - - c = ole.get_ctime('WordDocument') - m = ole.get_mtime('WordDocument') - -The root storage is a special case: You can get its creation and -modification timestamps using the OleFileIO.root attribute: - -.. code-block:: python - - c = ole.root.getctime() - m = ole.root.getmtime() - -Extract metadata -~~~~~~~~~~~~~~~~ - -get\_metadata() will check if standard property streams exist, parse all -the properties they contain, and return an OleMetadata object with the -found properties as attributes. - -.. code-block:: python - - meta = ole.get_metadata() - print('Author:', meta.author) - print('Title:', meta.title) - print('Creation date:', meta.create_time) - # print all metadata: - meta.dump() - -Available attributes include: - -:: - - codepage, title, subject, author, keywords, comments, template, - last_saved_by, revision_number, total_edit_time, last_printed, create_time, - last_saved_time, num_pages, num_words, num_chars, thumbnail, - creating_application, security, codepage_doc, category, presentation_target, - bytes, lines, paragraphs, slides, notes, hidden_slides, mm_clips, - scale_crop, heading_pairs, titles_of_parts, manager, company, links_dirty, - chars_with_spaces, unused, shared_doc, link_base, hlinks, hlinks_changed, - version, dig_sig, content_type, content_status, language, doc_version - -See the source code of the OleMetadata class for more information. - -Parse a property stream -~~~~~~~~~~~~~~~~~~~~~~~ - -get\_properties(path) can be used to parse any property stream that is -not handled by get\_metadata. It returns a dictionary indexed by -integers. Each integer is the index of the property, pointing to its -value. For example in the standard property stream -'05SummaryInformation', the document title is property #2, and the -subject is #3. - -.. code-block:: python - - p = ole.getproperties('specialprops') - -By default as in the original PIL version, timestamp properties are -converted into a number of seconds since Jan 1,1601. With the option -convert\_time, you can obtain more convenient Python datetime objects -(UTC timezone). If some time properties should not be converted (such as -total editing time in '05SummaryInformation'), the list of indexes can -be passed as no\_conversion: - -.. code-block:: python - - p = ole.getproperties('specialprops', convert_time=True, no_conversion=[10]) - -Close the OLE file -~~~~~~~~~~~~~~~~~~ - -Unless your application is a simple script that terminates after -processing an OLE file, do not forget to close each OleFileIO object -after parsing to close the file on disk. - -.. code-block:: python - - ole.close() - -Use OleFileIO as a script -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -OleFileIO can also be used as a script from the command-line to -display the structure of an OLE file and its metadata, for example: - -:: - - PIL/OleFileIO.py myfile.doc - -You can use the option -c to check that all streams can be read fully, -and -d to generate very verbose debugging information. - -How to contribute ------------------ - -The code is available in `a Mercurial repository on -bitbucket `_. You may use -it to submit enhancements or to report any issue. - -If you would like to help us improve this module, or simply provide -feedback, please `contact me `_. You can -help in many ways: - -- test this module on different platforms / Python versions -- find and report bugs -- improve documentation, code samples, docstrings -- write unittest test cases -- provide tricky malformed files - -How to report bugs ------------------- - -To report a bug, for example a normal file which is not parsed -correctly, please use the `issue reporting -page `_, -or if you prefer to do it privately, use this `contact -form `_. Please provide all the -information about the context and how to reproduce the bug. - -If possible please join the debugging output of OleFileIO. For this, -launch the following command : - -:: - - PIL/OleFileIO.py -d -c file >debug.txt - - -Classes and Methods -------------------- - -.. automodule:: PIL.OleFileIO - :members: - :undoc-members: - :show-inheritance: - :noindex: diff --git a/docs/reference/TiffTags.rst b/docs/reference/TiffTags.rst index 9518461dd..3b261625a 100644 --- a/docs/reference/TiffTags.rst +++ b/docs/reference/TiffTags.rst @@ -4,7 +4,7 @@ :py:mod:`TiffTags` Module ========================= -The :py:mod:`TiffTags` module exposes many of the stantard TIFF +The :py:mod:`TiffTags` module exposes many of the standard TIFF metadata tag numbers, names, and type information. .. method:: lookup(tag) diff --git a/docs/reference/index.rst b/docs/reference/index.rst index 555bd2a57..8c09e7b67 100644 --- a/docs/reference/index.rst +++ b/docs/reference/index.rst @@ -27,9 +27,9 @@ Reference ImageWin ExifTags TiffTags - OleFileIO PSDraw PixelAccess PyAccess ../PIL plugins + internal_design diff --git a/docs/reference/internal_design.rst b/docs/reference/internal_design.rst new file mode 100644 index 000000000..a8d6e2284 --- /dev/null +++ b/docs/reference/internal_design.rst @@ -0,0 +1,8 @@ +Internal Reference Docs +======================= + +.. toctree:: + :maxdepth: 2 + + open_files + limits diff --git a/docs/reference/limits.rst b/docs/reference/limits.rst new file mode 100644 index 000000000..6f81c9e65 --- /dev/null +++ b/docs/reference/limits.rst @@ -0,0 +1,41 @@ +Limits +------ + +This page is documentation to the various fundamental size limits in +the Pillow implementation. + +Internal Limits +=============== + +* Image sizes cannot be negative. These are checked both in + ``Storage.c`` and ``Image.py`` + +* Image sizes may be 0. (At least, prior to 3.4) + +* Maximum pixel dimensions are limited to INT32, or 2^31 by the sizes + in the image header. + +* Individual allocations are limited to 2GB in ``Storage.c`` + +* The 2GB allocation puts an upper limit to the xsize of the image of + either 2^31 for 'L' or 2^29 for 'RGB' + +* Individual memory mapped segments are limited to 2GB in map.c based + on the overflow checks. This requires that any memory mapped image + is smaller than 2GB, as calculated by ``y*stride`` (so 2Gpx for 'L' + images, and .5Gpx for 'RGB' + +* Any call to internal python size functions for buffers or strings + are currently returned as int32, not py_ssize_t. This limits the + maximum buffer to 2GB for operations like frombytes and frombuffer. + +* This also limits the size of buffers converted using a + decoder. (decode.c:127) + +Format Size Limits +================== + +* ICO: Max size is 256x256 + +* Webp: 16383x16383 (underlying library size limit: + https://developers.google.com/speed/webp/docs/api) diff --git a/docs/reference/open_files.rst b/docs/reference/open_files.rst new file mode 100644 index 000000000..143eb7209 --- /dev/null +++ b/docs/reference/open_files.rst @@ -0,0 +1,125 @@ +File Handling in Pillow +======================= + +When opening a file as an image, Pillow requires a filename, +pathlib.Path object, or a file-like object. Pillow uses the filename +or Path to open a file, so for the rest of this article, they will all +be treated as a file-like object. + +The first four of these items are equivalent, the last is dangerous +and may fail:: + + from PIL import Image + import io + import pathlib + + im = Image.open('test.jpg') + + im2 = Image.open(pathlib.Path('test.jpg')) + + f = open('test.jpg', 'rb') + im3 = Image.open(f) + + with open('test.jpg', 'rb') as f: + im4 = Image.open(io.BytesIO(f.read())) + + # Dangerous FAIL: + with open('test.jpg', 'rb') as f: + im5 = Image.open(f) + im5.load() # FAILS, closed file + +The documentation specifies that the file will be closed after the +``Image.Image.load()`` method is called. This is an aspirational +specification rather than an accurate reflection of the state of the +code. + +Pillow cannot in general close and reopen a file, so any access to +that file needs to be prior to the close. + +Issues +------ + +The current open file handling is inconsistent at best: + +* Most of the image plugins do not close the input file. +* Multi-frame images behave badly when seeking through the file, as + it's legal to seek backward in the file until the last image is + read, and then it's not. +* Using the file context manager to provide a file-like object to + Pillow is dangerous unless the context of the image is limited to + the context of the file. + +Image Lifecycle +--------------- + +* ``Image.open()`` called. Path-like objects are opened as a + file. Metadata is read from the open file. The file is left open for + further usage. + +* ``Image.Image.load()`` when the pixel data from the image is + required, ``load()`` is called. The current frame is read into + memory. The image can now be used independently of the underlying + image file. + +* ``Image.Image.seek()`` in the case of multi-frame images + (e.g. multipage TIFF and animated GIF) the image file left open so + that seek can load the appropriate frame. When the last frame is + read, the image file is closed (at least in some image plugins), and + no more seeks can occur. + +* ``Image.Image.close()`` Closes the file pointer and destroys the + core image object. This is used in the Pillow context manager + support. e.g.:: + + with Image.open('test.jpg') as img: + ... # image operations here. + + +The lifecycle of a single frame image is relatively simple. The file +must remain open until the ``load()`` or ``close()`` function is +called. + +Multi-frame images are more complicated. The ``load()`` method is not +a terminal method, so it should not close the underlying file. The +current behavior of ``seek()`` closing the underlying file on +accessing the last frame is presumably a heuristic for closing the +file after iterating through the entire sequence. In general, Pillow +does not know if there are going to be any requests for additional +data until the caller has explicitly closed the image. + + +Complications +------------- + +* TiffImagePlugin has some code to pass the underlying file descriptor + into libtiff (if working on an actual file). Since libtiff closes + the file descriptor internally, it is duplicated prior to passing it + into libtiff. + +* ``decoder.handles_eof`` This slightly misnamed flag indicates that + the decoder wants to be called with a 0 length buffer when reads are + done. Despite the comments in ``ImageFile.load()``, the only decoder + that actually uses this flag is the Jpeg2K decoder. The use of this + flag in Jpeg2K predated the change to the decoder that added the + pulls_fd flag, and is therefore not used. + +* I don't think that there's any way to make this safe without + changing the lazy loading:: + + # Dangerous FAIL: + with open('test.jpg', 'rb') as f: + im5 = Image.open(f) + im5.load() # FAILS, closed file + + +Proposed File Handling +---------------------- + +* ``Image.Image.load()`` should close the image file, unless there are + multiple frames. + +* ``Image.Image.seek()`` should never close the image file. + +* Users of the library should call ``Image.Image.close()`` on any + multi-frame image to ensure that the underlying file is closed. + diff --git a/docs/releasenotes/3.3.2.rst b/docs/releasenotes/3.3.2.rst new file mode 100644 index 000000000..141413093 --- /dev/null +++ b/docs/releasenotes/3.3.2.rst @@ -0,0 +1,40 @@ + +3.3.2 +===== + +Integer overflow in Map.c +------------------------- + +Pillow prior to 3.3.2 may experience integer overflow errors in map.c +when reading specially crafted image files. This may lead to memory +disclosure or corruption. + +Specifically, when parameters from the image are passed into +``Image.core.map_buffer``, the size of the image was calculated with +``xsize``*``ysize``*``bytes_per_pixel``. This will overflow if the +result is larger than SIZE_MAX. This is possible on a 32-bit system. + +Furthermore this ``size`` value was added to a potentially attacker +provided ``offset`` value and compared to the size of the buffer +without checking for overflow or negative values. + +These values were then used for creating pointers, at which point +Pillow could read the memory and include it in other images. The image +was marked readonly, so Pillow would not ordinarily write to that +memory without duplicating the image first. + +This issue was found by Cris Neckar at Divergent Security. + +Sign Extension in Storage.c +--------------------------- + +Pillow prior to 3.3.2 and PIL 1.1.7 (at least) do not check for +negative image sizes in ``ImagingNew`` in ``Storage.c``. A negative +image size can lead to a smaller allocation than expected, leading to +arbitrary writes. + +This issue was found by Cris Neckar at Divergent Security. + + + + diff --git a/docs/releasenotes/3.4.0.rst b/docs/releasenotes/3.4.0.rst index ef4db3322..a6512fc12 100644 --- a/docs/releasenotes/3.4.0.rst +++ b/docs/releasenotes/3.4.0.rst @@ -14,6 +14,20 @@ two times shorter window than ``BILINEAR``. It can be used for image reduction providing the image downscaling quality comparable to ``BICUBIC``. Both new filters don't show good quality for the image upscaling. +Deprecation Warning when Saving JPEGs +===================================== + +JPEG images cannot contain an alpha channel. Pillow prior to 3.4.0 +silently drops the alpha channel. With this release Pillow will now +issue a ``DeprecationWarning`` when attempting to save a ``RGBA`` mode +image as a JPEG. This will become an error in Pillow 3.7. + +New DDS Decoders +================ + +Pillow can now decode DXT3 images, as well as the previously support +DXT1 and DXT5 formats. All three formats are now decoded in C code for +better performance. Append images to GIF ==================== @@ -26,3 +40,18 @@ Note that the ``append_images`` argument is only used if ``save_all`` is also in effect, e.g.:: im.save(out, save_all=True, append_images=[im1, im2, ...]) + +Save multiple frame TIFF +======================== + +Multiple frames can now be saved in a TIFF file by using the ``save_all`` option. +e.g.:: + + im.save("filename.tiff", format="TIFF", save_all=True) + +Image.core.open_ppm removed +=========================== + +The nominally private/debugging function ``Image.core.open_ppm`` has +been removed. If you were using this function, please use +``Image.open`` instead. diff --git a/docs/releasenotes/4.0.0.rst b/docs/releasenotes/4.0.0.rst new file mode 100644 index 000000000..4d21a2e54 --- /dev/null +++ b/docs/releasenotes/4.0.0.rst @@ -0,0 +1,51 @@ +4.0.0 +----- + +Python 2.6 and 3.2 Dropped +========================== + +Pillow 4.0 no longer supports Python 2.6 and 3.2. We will not be +creating binaries, testing, or retaining compatibility with these +releases. This release removes some workarounds for those Python +releases, so the final working version of Pillow on 2.6 or 3.2 is 3.4.2. + +Support added for Python 3.6 +============================ + +Pillow 4.0 supports Python 3.6. + +OleFileIO.py +============ + +OleFileIO.py has been removed as a vendored file and is now installed +from the upstream olefile pypi package. All internal dependencies are +redirected to the olefile package. Direct accesses to +``PIL.OlefileIO`` raises a deprecation warning, then patches the +upstream olefile into ``sys.modules`` in its place. + +SGI image save +============== + +It is now possible to save images in modes ``L``, ``RGB``, and +``RGBA`` to the uncompressed SGI image format. + +Zero sized images +================= + +Pillow 3.4.0 removed support for creating images with (0,0) size. This +has been reenabled, restoring pre 3.4 behavior. + +Internal handles_eof flag +========================= + +The ``handles_eof flag`` for decoding images has been removed, as there +were no internal users of the flag. Anyone maintaining image decoders +outside of the Pillow source tree should consider using the cleanup +function pointers instead. + +Image.core.stretch removed +========================== + +The stretch function on the core image object has been removed. This +used to be for enlarging the image, but has been aliased to resize +recently. diff --git a/docs/releasenotes/index.rst b/docs/releasenotes/index.rst index 2ffe37980..e32bf7e40 100644 --- a/docs/releasenotes/index.rst +++ b/docs/releasenotes/index.rst @@ -6,7 +6,9 @@ Release Notes .. toctree:: :maxdepth: 2 + 4.0.0 3.4.0 + 3.3.2 3.3.0 3.2.0 3.1.2 diff --git a/libImaging/File.c b/libImaging/File.c index ac9ec3be1..d67bcabde 100644 --- a/libImaging/File.c +++ b/libImaging/File.c @@ -20,116 +20,6 @@ #include -Imaging -ImagingOpenPPM(const char* infile) -{ - FILE* fp; - int i, c, v; - char* mode; - int x, y, max; - Imaging im; - - if (!infile) - return ImagingError_ValueError(NULL); - - fp = fopen(infile, "rb"); - if (!fp) - return ImagingError_IOError(); - - /* PPM magic */ - if (fgetc(fp) != 'P') - goto error; - switch (fgetc(fp)) { - case '4': /* FIXME: 1-bit images are not yet supported */ - goto error; - case '5': - mode = "L"; - break; - case '6': - mode = "RGB"; - break; - default: - goto error; - } - - i = 0; - c = fgetc(fp); - - x = y = max = 0; - - while (i < 3) { - - /* Ignore optional comment fields */ - while (c == '\n') { - c = fgetc(fp); - if (c == '#') { - do { - c = fgetc(fp); - if (c == EOF) - goto error; - } while (c != '\n'); - c = fgetc(fp); - } - } - - /* Skip forward to next value */ - while (isspace(c)) - c = fgetc(fp); - - /* And parse it */ - v = 0; - while (isdigit(c)) { - v = v * 10 + (c - '0'); - c = fgetc(fp); - } - - if (c == EOF) - goto error; - - switch (i++) { - case 0: - x = v; - break; - case 1: - y = v; - break; - case 2: - max = v; - break; - } - } - - im = ImagingNew(mode, x, y); - if (!im) - return NULL; - - /* if (max != 255) ... FIXME: does anyone ever use this feature? */ - - if (strcmp(im->mode, "L") == 0) { - - /* PPM "L" */ - for (y = 0; y < im->ysize; y++) - if (fread(im->image[y], im->xsize, 1, fp) != 1) - goto error; - - } else { - - /* PPM "RGB" or PyPPM mode */ - for (y = 0; y < im->ysize; y++) - for (x = i = 0; x < im->xsize; x++, i += im->pixelsize) - if (fread(im->image[y]+i, im->bands, 1, fp) != 1) - goto error; - } - - fclose(fp); - - return im; - -error: - fclose(fp); - return ImagingError_IOError(); -} - int ImagingSaveRaw(Imaging im, FILE* fp) @@ -140,16 +30,16 @@ ImagingSaveRaw(Imaging im, FILE* fp) /* @PIL227: FIXME: for mode "1", map != 0 to 255 */ - /* PGM "L" */ - for (y = 0; y < im->ysize; y++) - fwrite(im->image[y], 1, im->xsize, fp); + /* PGM "L" */ + for (y = 0; y < im->ysize; y++) + fwrite(im->image[y], 1, im->xsize, fp); } else { - /* PPM "RGB" or other internal format */ - for (y = 0; y < im->ysize; y++) - for (x = i = 0; x < im->xsize; x++, i += im->pixelsize) - fwrite(im->image[y]+i, 1, im->bands, fp); + /* PPM "RGB" or other internal format */ + for (y = 0; y < im->ysize; y++) + for (x = i = 0; x < im->xsize; x++, i += im->pixelsize) + fwrite(im->image[y]+i, 1, im->bands, fp); } @@ -163,24 +53,24 @@ ImagingSavePPM(Imaging im, const char* outfile) FILE* fp; if (!im) { - (void) ImagingError_ValueError(NULL); - return 0; + (void) ImagingError_ValueError(NULL); + return 0; } fp = fopen(outfile, "wb"); if (!fp) { - (void) ImagingError_IOError(); - return 0; + (void) ImagingError_IOError(); + return 0; } if (strcmp(im->mode, "1") == 0 || strcmp(im->mode, "L") == 0) { - /* Write "PGM" */ - fprintf(fp, "P5\n%d %d\n255\n", im->xsize, im->ysize); + /* Write "PGM" */ + fprintf(fp, "P5\n%d %d\n255\n", im->xsize, im->ysize); } else if (strcmp(im->mode, "RGB") == 0) { /* Write "PPM" */ fprintf(fp, "P6\n%d %d\n255\n", im->xsize, im->ysize); } else { - (void) ImagingError_ModeError(); + (void) ImagingError_ModeError(); return 0; } diff --git a/libImaging/Pack.c b/libImaging/Pack.c index 8a0fcc004..621936351 100644 --- a/libImaging/Pack.c +++ b/libImaging/Pack.c @@ -1,4 +1,4 @@ -/* + /* * The Python Imaging Library. * $Id$ * @@ -28,16 +28,16 @@ #include "Imaging.h" -#define R 0 -#define G 1 -#define B 2 -#define X 3 -#define A 3 +#define R 0 +#define G 1 +#define B 2 +#define X 3 +#define A 3 -#define C 0 -#define M 1 -#define Y 2 -#define K 3 +#define C 0 +#define M 1 +#define Y 2 +#define K 3 /* byte swapping macros */ @@ -83,16 +83,16 @@ pack1(UINT8* out, const UINT8* in, int pixels) /* bilevel (black is 0) */ b = 0; m = 128; for (i = 0; i < pixels; i++) { - if (in[i] != 0) - b |= m; - m >>= 1; - if (m == 0) { - *out++ = b; - b = 0; m = 128; - } + if (in[i] != 0) + b |= m; + m >>= 1; + if (m == 0) { + *out++ = b; + b = 0; m = 128; + } } if (m != 128) - *out++ = b; + *out++ = b; } static void @@ -102,16 +102,16 @@ pack1I(UINT8* out, const UINT8* in, int pixels) /* bilevel (black is 1) */ b = 0; m = 128; for (i = 0; i < pixels; i++) { - if (in[i] == 0) - b |= m; - m >>= 1; - if (m == 0) { - *out++ = b; - b = 0; m = 128; - } + if (in[i] == 0) + b |= m; + m >>= 1; + if (m == 0) { + *out++ = b; + b = 0; m = 128; + } } if (m != 128) - *out++ = b; + *out++ = b; } static void @@ -121,16 +121,16 @@ pack1R(UINT8* out, const UINT8* in, int pixels) /* bilevel, lsb first (black is 0) */ b = 0; m = 1; for (i = 0; i < pixels; i++) { - if (in[i] != 0) - b |= m; - m <<= 1; - if (m == 256){ - *out++ = b; - b = 0; m = 1; - } + if (in[i] != 0) + b |= m; + m <<= 1; + if (m == 256){ + *out++ = b; + b = 0; m = 1; + } } if (m != 1) - *out++ = b; + *out++ = b; } static void @@ -140,16 +140,16 @@ pack1IR(UINT8* out, const UINT8* in, int pixels) /* bilevel, lsb first (black is 1) */ b = 0; m = 1; for (i = 0; i < pixels; i++) { - if (in[i] == 0) - b |= m; - m <<= 1; - if (m == 256){ - *out++ = b; - b = 0; m = 1; - } + if (in[i] == 0) + b |= m; + m <<= 1; + if (m == 256){ + *out++ = b; + b = 0; m = 1; + } } if (m != 1) - *out++ = b; + *out++ = b; } static void @@ -158,44 +158,45 @@ pack1L(UINT8* out, const UINT8* in, int pixels) int i; /* bilevel, stored as bytes */ for (i = 0; i < pixels; i++) - out[i] = (in[i] != 0) ? 255 : 0; + out[i] = (in[i] != 0) ? 255 : 0; } static void packP4(UINT8* out, const UINT8* in, int pixels) { while (pixels >= 2) { - *out++ = (in[0] << 4) | - (in[1] & 15); - in += 2; pixels -= 2; + *out++ = (in[0] << 4) | + (in[1] & 15); + in += 2; pixels -= 2; } if (pixels) - out[0] = (in[0] << 4); + out[0] = (in[0] << 4); } static void packP2(UINT8* out, const UINT8* in, int pixels) { while (pixels >= 4) { - *out++ = (in[0] << 6) | - ((in[1] & 3) << 4) | - ((in[2] & 3) << 2) | - (in[3] & 3); - in += 4; pixels -= 4; + *out++ = (in[0] << 6) | + ((in[1] & 3) << 4) | + ((in[2] & 3) << 2) | + (in[3] & 3); + in += 4; pixels -= 4; } switch (pixels) { case 3: - out[0] = (in[0] << 6) | - ((in[1] & 3) << 4) | - ((in[2] & 3) << 2); - break; + out[0] = (in[0] << 6) | + ((in[1] & 3) << 4) | + ((in[2] & 3) << 2); + break; case 2: - out[0] = (in[0] << 6) | - ((in[1] & 3) << 4); + out[0] = (in[0] << 6) | + ((in[1] & 3) << 4); + break; case 1: - out[0] = (in[0] << 6); + out[0] = (in[0] << 6); } } @@ -205,9 +206,9 @@ packLA(UINT8* out, const UINT8* in, int pixels) int i; /* LA, pixel interleaved */ for (i = 0; i < pixels; i++) { - out[0] = in[R]; - out[1] = in[A]; - out += 2; in += 4; + out[0] = in[R]; + out[1] = in[A]; + out += 2; in += 4; } } @@ -217,9 +218,9 @@ packLAL(UINT8* out, const UINT8* in, int pixels) int i; /* LA, line interleaved */ for (i = 0; i < pixels; i++) { - out[i] = in[R]; - out[i+pixels] = in[A]; - in += 4; + out[i] = in[R]; + out[i+pixels] = in[A]; + in += 4; } } @@ -229,10 +230,10 @@ ImagingPackRGB(UINT8* out, const UINT8* in, int pixels) int i; /* RGB triplets */ for (i = 0; i < pixels; i++) { - out[0] = in[R]; - out[1] = in[G]; - out[2] = in[B]; - out += 3; in += 4; + out[0] = in[R]; + out[1] = in[G]; + out[2] = in[B]; + out += 3; in += 4; } } @@ -242,11 +243,11 @@ ImagingPackXRGB(UINT8* out, const UINT8* in, int pixels) int i; /* XRGB, triplets with left padding */ for (i = 0; i < pixels; i++) { - out[0] = 0; - out[1] = in[R]; - out[2] = in[G]; - out[3] = in[B]; - out += 4; in += 4; + out[0] = 0; + out[1] = in[R]; + out[2] = in[G]; + out[3] = in[B]; + out += 4; in += 4; } } @@ -256,10 +257,10 @@ ImagingPackBGR(UINT8* out, const UINT8* in, int pixels) int i; /* RGB, reversed bytes */ for (i = 0; i < pixels; i++) { - out[0] = in[B]; - out[1] = in[G]; - out[2] = in[R]; - out += 3; in += 4; + out[0] = in[B]; + out[1] = in[G]; + out[2] = in[R]; + out += 3; in += 4; } } @@ -269,11 +270,11 @@ ImagingPackBGRX(UINT8* out, const UINT8* in, int pixels) int i; /* BGRX, reversed bytes with right padding */ for (i = 0; i < pixels; i++) { - out[0] = in[B]; - out[1] = in[G]; - out[2] = in[R]; - out[3] = 0; - out += 4; in += 4; + out[0] = in[B]; + out[1] = in[G]; + out[2] = in[R]; + out[3] = 0; + out += 4; in += 4; } } @@ -283,11 +284,11 @@ ImagingPackXBGR(UINT8* out, const UINT8* in, int pixels) int i; /* XBGR, reversed bytes with left padding */ for (i = 0; i < pixels; i++) { - out[0] = 0; - out[1] = in[B]; - out[2] = in[G]; - out[3] = in[R]; - out += 4; in += 4; + out[0] = 0; + out[1] = in[B]; + out[2] = in[G]; + out[3] = in[R]; + out += 4; in += 4; } } @@ -297,11 +298,11 @@ ImagingPackBGRA(UINT8* out, const UINT8* in, int pixels) int i; /* BGRX, reversed bytes with right padding */ for (i = 0; i < pixels; i++) { - out[0] = in[B]; - out[1] = in[G]; - out[2] = in[R]; - out[3] = in[A]; - out += 4; in += 4; + out[0] = in[B]; + out[1] = in[G]; + out[2] = in[R]; + out[3] = in[A]; + out += 4; in += 4; } } @@ -311,11 +312,11 @@ ImagingPackABGR(UINT8* out, const UINT8* in, int pixels) int i; /* XBGR, reversed bytes with left padding */ for (i = 0; i < pixels; i++) { - out[0] = in[A]; - out[1] = in[B]; - out[2] = in[G]; - out[3] = in[R]; - out += 4; in += 4; + out[0] = in[A]; + out[1] = in[B]; + out[2] = in[G]; + out[3] = in[R]; + out += 4; in += 4; } } @@ -340,10 +341,10 @@ packRGBL(UINT8* out, const UINT8* in, int pixels) int i; /* RGB, line interleaved */ for (i = 0; i < pixels; i++) { - out[i] = in[R]; - out[i+pixels] = in[G]; - out[i+pixels+pixels] = in[B]; - in += 4; + out[i] = in[R]; + out[i+pixels] = in[G]; + out[i+pixels+pixels] = in[B]; + in += 4; } } @@ -353,11 +354,11 @@ packRGBXL(UINT8* out, const UINT8* in, int pixels) int i; /* RGBX, line interleaved */ for (i = 0; i < pixels; i++) { - out[i] = in[R]; - out[i+pixels] = in[G]; - out[i+pixels+pixels] = in[B]; - out[i+pixels+pixels+pixels] = in[X]; - in += 4; + out[i] = in[R]; + out[i+pixels] = in[G]; + out[i+pixels+pixels] = in[B]; + out[i+pixels+pixels+pixels] = in[X]; + in += 4; } } @@ -376,7 +377,7 @@ packI16B(UINT8* out, const UINT8* in_, int pixels) else tmp_ = in[0]; C16B; - out += 2; in++; + out += 2; in++; } } @@ -386,7 +387,7 @@ packI16N_I16B(UINT8* out, const UINT8* in, int pixels){ UINT8* tmp = (UINT8*) in; for (i = 0; i < pixels; i++) { C16B; - out += 2; tmp += 2; + out += 2; tmp += 2; } } @@ -396,7 +397,7 @@ packI16N_I16(UINT8* out, const UINT8* in, int pixels){ UINT8* tmp = (UINT8*) in; for (i = 0; i < pixels; i++) { C16L; - out += 2; tmp += 2; + out += 2; tmp += 2; } } @@ -408,7 +409,7 @@ packI32S(UINT8* out, const UINT8* in, int pixels) UINT8* tmp = (UINT8*) in; for (i = 0; i < pixels; i++) { C32L; - out += 4; tmp += 4; + out += 4; tmp += 4; } } @@ -418,10 +419,10 @@ ImagingPackLAB(UINT8* out, const UINT8* in, int pixels) int i; /* LAB triplets */ for (i = 0; i < pixels; i++) { - out[0] = in[0]; - out[1] = in[1] ^ 128; /* signed in outside world */ - out[2] = in[2] ^ 128; - out += 3; in += 4; + out[0] = in[0]; + out[1] = in[1] ^ 128; /* signed in outside world */ + out[2] = in[2] ^ 128; + out += 3; in += 4; } } @@ -459,7 +460,7 @@ copy4I(UINT8* out, const UINT8* in, int pixels) /* RGBA, CMYK quadruples, inverted */ int i; for (i = 0; i < pixels*4; i++) - out[i] = ~in[i]; + out[i] = ~in[i]; } static void @@ -467,7 +468,7 @@ band0(UINT8* out, const UINT8* in, int pixels) { int i; for (i = 0; i < pixels; i++, in += 4) - out[i] = in[0]; + out[i] = in[0]; } static void @@ -475,7 +476,7 @@ band1(UINT8* out, const UINT8* in, int pixels) { int i; for (i = 0; i < pixels; i++, in += 4) - out[i] = in[1]; + out[i] = in[1]; } static void @@ -483,7 +484,7 @@ band2(UINT8* out, const UINT8* in, int pixels) { int i; for (i = 0; i < pixels; i++, in += 4) - out[i] = in[2]; + out[i] = in[2]; } static void @@ -491,7 +492,7 @@ band3(UINT8* out, const UINT8* in, int pixels) { int i; for (i = 0; i < pixels; i++, in += 4) - out[i] = in[3]; + out[i] = in[3]; } static struct { @@ -502,122 +503,122 @@ static struct { } packers[] = { /* bilevel */ - {"1", "1", 1, pack1}, - {"1", "1;I", 1, pack1I}, - {"1", "1;R", 1, pack1R}, - {"1", "1;IR", 1, pack1IR}, - {"1", "L", 8, pack1L}, + {"1", "1", 1, pack1}, + {"1", "1;I", 1, pack1I}, + {"1", "1;R", 1, pack1R}, + {"1", "1;IR", 1, pack1IR}, + {"1", "L", 8, pack1L}, /* greyscale */ - {"L", "L", 8, copy1}, + {"L", "L", 8, copy1}, /* greyscale w. alpha */ - {"LA", "LA", 16, packLA}, - {"LA", "LA;L", 16, packLAL}, + {"LA", "LA", 16, packLA}, + {"LA", "LA;L", 16, packLAL}, /* palette */ - {"P", "P;1", 1, pack1}, - {"P", "P;2", 2, packP2}, - {"P", "P;4", 4, packP4}, - {"P", "P", 8, copy1}, + {"P", "P;1", 1, pack1}, + {"P", "P;2", 2, packP2}, + {"P", "P;4", 4, packP4}, + {"P", "P", 8, copy1}, /* palette w. alpha */ - {"PA", "PA", 16, packLA}, - {"PA", "PA;L", 16, packLAL}, + {"PA", "PA", 16, packLA}, + {"PA", "PA;L", 16, packLAL}, /* true colour */ - {"RGB", "RGB", 24, ImagingPackRGB}, - {"RGB", "RGBX", 32, copy4}, - {"RGB", "XRGB", 32, ImagingPackXRGB}, - {"RGB", "BGR", 24, ImagingPackBGR}, - {"RGB", "BGRX", 32, ImagingPackBGRX}, - {"RGB", "XBGR", 32, ImagingPackXBGR}, - {"RGB", "RGB;L", 24, packRGBL}, - {"RGB", "R", 8, band0}, - {"RGB", "G", 8, band1}, - {"RGB", "B", 8, band2}, + {"RGB", "RGB", 24, ImagingPackRGB}, + {"RGB", "RGBX", 32, copy4}, + {"RGB", "XRGB", 32, ImagingPackXRGB}, + {"RGB", "BGR", 24, ImagingPackBGR}, + {"RGB", "BGRX", 32, ImagingPackBGRX}, + {"RGB", "XBGR", 32, ImagingPackXBGR}, + {"RGB", "RGB;L", 24, packRGBL}, + {"RGB", "R", 8, band0}, + {"RGB", "G", 8, band1}, + {"RGB", "B", 8, band2}, /* true colour w. alpha */ - {"RGBA", "RGBA", 32, copy4}, - {"RGBA", "RGBA;L", 32, packRGBXL}, - {"RGBA", "RGB", 24, ImagingPackRGB}, - {"RGBA", "BGR", 24, ImagingPackBGR}, - {"RGBA", "BGRA", 32, ImagingPackBGRA}, - {"RGBA", "ABGR", 32, ImagingPackABGR}, - {"RGBA", "BGRa", 32, ImagingPackBGRa}, - {"RGBA", "R", 8, band0}, - {"RGBA", "G", 8, band1}, - {"RGBA", "B", 8, band2}, - {"RGBA", "A", 8, band3}, + {"RGBA", "RGBA", 32, copy4}, + {"RGBA", "RGBA;L", 32, packRGBXL}, + {"RGBA", "RGB", 24, ImagingPackRGB}, + {"RGBA", "BGR", 24, ImagingPackBGR}, + {"RGBA", "BGRA", 32, ImagingPackBGRA}, + {"RGBA", "ABGR", 32, ImagingPackABGR}, + {"RGBA", "BGRa", 32, ImagingPackBGRa}, + {"RGBA", "R", 8, band0}, + {"RGBA", "G", 8, band1}, + {"RGBA", "B", 8, band2}, + {"RGBA", "A", 8, band3}, /* true colour w. alpha premultiplied */ - {"RGBa", "RGBa", 32, copy4}, - {"RGBa", "BGRa", 32, ImagingPackBGRA}, - {"RGBa", "aBGR", 32, ImagingPackABGR}, + {"RGBa", "RGBa", 32, copy4}, + {"RGBa", "BGRa", 32, ImagingPackBGRA}, + {"RGBa", "aBGR", 32, ImagingPackABGR}, /* true colour w. padding */ - {"RGBX", "RGBX", 32, copy4}, - {"RGBX", "RGBX;L", 32, packRGBXL}, - {"RGBX", "RGB", 32, ImagingPackRGB}, - {"RGBX", "BGR", 32, ImagingPackBGR}, - {"RGBX", "BGRX", 32, ImagingPackBGRX}, - {"RGBX", "XBGR", 32, ImagingPackXBGR}, - {"RGBX", "R", 8, band0}, - {"RGBX", "G", 8, band1}, - {"RGBX", "B", 8, band2}, - {"RGBX", "X", 8, band3}, + {"RGBX", "RGBX", 32, copy4}, + {"RGBX", "RGBX;L", 32, packRGBXL}, + {"RGBX", "RGB", 32, ImagingPackRGB}, + {"RGBX", "BGR", 32, ImagingPackBGR}, + {"RGBX", "BGRX", 32, ImagingPackBGRX}, + {"RGBX", "XBGR", 32, ImagingPackXBGR}, + {"RGBX", "R", 8, band0}, + {"RGBX", "G", 8, band1}, + {"RGBX", "B", 8, band2}, + {"RGBX", "X", 8, band3}, /* colour separation */ - {"CMYK", "CMYK", 32, copy4}, - {"CMYK", "CMYK;I", 32, copy4I}, - {"CMYK", "CMYK;L", 32, packRGBXL}, - {"CMYK", "C", 8, band0}, - {"CMYK", "M", 8, band1}, - {"CMYK", "Y", 8, band2}, - {"CMYK", "K", 8, band3}, + {"CMYK", "CMYK", 32, copy4}, + {"CMYK", "CMYK;I", 32, copy4I}, + {"CMYK", "CMYK;L", 32, packRGBXL}, + {"CMYK", "C", 8, band0}, + {"CMYK", "M", 8, band1}, + {"CMYK", "Y", 8, band2}, + {"CMYK", "K", 8, band3}, /* video (YCbCr) */ - {"YCbCr", "YCbCr", 24, ImagingPackRGB}, - {"YCbCr", "YCbCr;L", 24, packRGBL}, - {"YCbCr", "YCbCrX", 32, copy4}, - {"YCbCr", "YCbCrK", 32, copy4}, - {"YCbCr", "Y", 8, band0}, - {"YCbCr", "Cb", 8, band1}, - {"YCbCr", "Cr", 8, band2}, + {"YCbCr", "YCbCr", 24, ImagingPackRGB}, + {"YCbCr", "YCbCr;L", 24, packRGBL}, + {"YCbCr", "YCbCrX", 32, copy4}, + {"YCbCr", "YCbCrK", 32, copy4}, + {"YCbCr", "Y", 8, band0}, + {"YCbCr", "Cb", 8, band1}, + {"YCbCr", "Cr", 8, band2}, /* LAB Color */ - {"LAB", "LAB", 24, ImagingPackLAB}, - {"LAB", "L", 8, band0}, - {"LAB", "A", 8, band1}, - {"LAB", "B", 8, band2}, + {"LAB", "LAB", 24, ImagingPackLAB}, + {"LAB", "L", 8, band0}, + {"LAB", "A", 8, band1}, + {"LAB", "B", 8, band2}, /* HSV */ - {"HSV", "HSV", 24, ImagingPackRGB}, - {"HSV", "H", 8, band0}, - {"HSV", "S", 8, band1}, - {"HSV", "V", 8, band2}, + {"HSV", "HSV", 24, ImagingPackRGB}, + {"HSV", "H", 8, band0}, + {"HSV", "S", 8, band1}, + {"HSV", "V", 8, band2}, /* integer */ - {"I", "I", 32, copy4}, - {"I", "I;16B", 16, packI16B}, - {"I", "I;32S", 32, packI32S}, - {"I", "I;32NS", 32, copy4}, + {"I", "I", 32, copy4}, + {"I", "I;16B", 16, packI16B}, + {"I", "I;32S", 32, packI32S}, + {"I", "I;32NS", 32, copy4}, /* floating point */ - {"F", "F", 32, copy4}, - {"F", "F;32F", 32, packI32S}, - {"F", "F;32NF", 32, copy4}, + {"F", "F", 32, copy4}, + {"F", "F;32F", 32, packI32S}, + {"F", "F;32NF", 32, copy4}, /* storage modes */ - {"I;16", "I;16", 16, copy2}, - {"I;16B", "I;16B", 16, copy2}, - {"I;16L", "I;16L", 16, copy2}, - {"I;16", "I;16N", 16, packI16N_I16}, // LibTiff native->image endian. - {"I;16L", "I;16N", 16, packI16N_I16}, - {"I;16B", "I;16N", 16, packI16N_I16B}, - {"BGR;15", "BGR;15", 16, copy2}, - {"BGR;16", "BGR;16", 16, copy2}, - {"BGR;24", "BGR;24", 24, copy3}, + {"I;16", "I;16", 16, copy2}, + {"I;16B", "I;16B", 16, copy2}, + {"I;16L", "I;16L", 16, copy2}, + {"I;16", "I;16N", 16, packI16N_I16}, // LibTiff native->image endian. + {"I;16L", "I;16N", 16, packI16N_I16}, + {"I;16B", "I;16N", 16, packI16N_I16B}, + {"BGR;15", "BGR;15", 16, copy2}, + {"BGR;16", "BGR;16", 16, copy2}, + {"BGR;24", "BGR;24", 24, copy3}, {NULL} /* sentinel */ }; @@ -630,11 +631,11 @@ ImagingFindPacker(const char* mode, const char* rawmode, int* bits_out) /* find a suitable pixel packer */ for (i = 0; packers[i].rawmode; i++) - if (strcmp(packers[i].mode, mode) == 0 && + if (strcmp(packers[i].mode, mode) == 0 && strcmp(packers[i].rawmode, rawmode) == 0) { - if (bits_out) - *bits_out = packers[i].bits; - return packers[i].pack; - } + if (bits_out) + *bits_out = packers[i].bits; + return packers[i].pack; + } return NULL; } diff --git a/libImaging/Resample.c b/libImaging/Resample.c index 5f202bc3f..770f5f611 100644 --- a/libImaging/Resample.c +++ b/libImaging/Resample.c @@ -176,18 +176,23 @@ precompute_coeffs(int inSize, int outSize, struct filter *filterp, k = &kk[xx * kmax]; for (x = 0; x < xmax; x++) { double w = filterp->filter((x + xmin - center + 0.5) * ss); + k[x] = w; + ww += w; + + // We can skip extreme coefficients if they are zeroes. if (w == 0) { + // Skip from the start. if (x == 0) { + // At next loop `x` will be 0. x -= 1; + // But `w` will not be 0, because it based on `xmin`. xmin += 1; xmax -= 1; } else if (x == xmax - 1) { + // Truncate the last coefficient for current `xx`. xmax -= 1; } - continue; } - k[x] = w; - ww += w; } for (x = 0; x < xmax; x++) { if (ww != 0.0) diff --git a/libImaging/Storage.c b/libImaging/Storage.c index f40840671..27661bfdb 100644 --- a/libImaging/Storage.c +++ b/libImaging/Storage.c @@ -406,6 +406,10 @@ ImagingNew(const char* mode, int xsize, int ysize) } else bytes = strlen(mode); /* close enough */ + if (xsize < 0 || ysize < 0) { + return (Imaging) ImagingError_ValueError("bad image size"); + } + if ((int64_t) xsize * (int64_t) ysize <= THRESHOLD / bytes) { im = ImagingNewBlock(mode, xsize, ysize); if (im) diff --git a/libImaging/SunRleDecode.c b/libImaging/SunRleDecode.c index 6c240e400..09375cfa5 100644 --- a/libImaging/SunRleDecode.c +++ b/libImaging/SunRleDecode.c @@ -7,7 +7,7 @@ * decoder for SUN RLE data. * * history: - * 97-01-04 fl Created + * 97-01-04 fl Created * * Copyright (c) Fredrik Lundh 1997. * Copyright (c) Secret Labs AB 1997. @@ -24,88 +24,122 @@ ImagingSunRleDecode(Imaging im, ImagingCodecState state, UINT8* buf, int bytes) { int n; UINT8* ptr; + UINT8 extra_data = 0; + UINT8 extra_bytes = 0; ptr = buf; for (;;) { - if (bytes < 1) - return ptr - buf; + if (bytes < 1) + return ptr - buf; - if (ptr[0] == 0x80) { + if (ptr[0] == 0x80) { - if (bytes < 2) - break; + if (bytes < 2) + break; - n = ptr[1]; + n = ptr[1]; - if (n == 0) { - /* Literal 0x80 (2 bytes) */ - n = 1; + if (n == 0) { - state->buffer[state->x] = 0x80; + /* Literal 0x80 (2 bytes) */ + n = 1; - ptr += 2; - bytes -= 2; + state->buffer[state->x] = 0x80; - } else { + ptr += 2; + bytes -= 2; - /* Run (3 bytes) */ - if (bytes < 3) - break; + } else { - if (state->x + n > state->bytes) { - /* FIXME: is this correct? */ - state->errcode = IMAGING_CODEC_OVERRUN; - return -1; - } + /* Run (3 bytes) */ + if (bytes < 3) + break; - memset(state->buffer + state->x, ptr[2], n); + /* from (http://www.fileformat.info/format/sunraster/egff.htm) - ptr += 3; - bytes -= 3; + For example, a run of 100 pixels with the value of + 0Ah would encode as the values 80h 64h 0Ah. A + single pixel value of 80h would encode as the + values 80h 00h. The four unencoded bytes 12345678h + would be stored in the RLE stream as 12h 34h 56h + 78h. 100 pixels, n=100, not 100 pixels, n=99. - } + But Wait! There's More! + (http://www.fileformat.info/format/sunraster/spec/598a59c4fac64c52897585d390d86360/view.htm) - } else { + If the first byte is 0x80, and the second byte is + not zero, the record is three bytes long. The + second byte is a count and the third byte is a + value. Output (count+1) pixels of that value. - /* Literal (1+n bytes block) */ - n = ptr[0]; + 2 specs, same site, but Imagemagick and GIMP seem + to agree on the second one. + */ + n += 1; - if (bytes < 1 + n) - break; + if (state->x + n > state->bytes) { + extra_bytes = n; /* full value */ + n = state->bytes - state->x; + extra_bytes -= n; + extra_data = ptr[2]; + } - if (state->x + n > state->bytes) { - /* FIXME: is this correct? */ - state->errcode = IMAGING_CODEC_OVERRUN; - return -1; - } + memset(state->buffer + state->x, ptr[2], n); - memcpy(state->buffer + state->x, ptr + 1, n); + ptr += 3; + bytes -= 3; - ptr += 1 + n; - bytes -= 1 + n; + } - } + } else { - state->x += n; + /* Literal byte */ + n = 1; - if (state->x >= state->bytes) { + state->buffer[state->x] = ptr[0]; - /* Got a full line, unpack it */ - state->shuffle((UINT8*) im->image[state->y + state->yoff] + - state->xoff * im->pixelsize, state->buffer, - state->xsize); + ptr += 1; + bytes -= 1; - state->x = 0; + } - if (++state->y >= state->ysize) { - /* End of file (errcode = 0) */ - return -1; - } - } + for (;;) { + state->x += n; + + if (state->x >= state->bytes) { + /* Got a full line, unpack it */ + state->shuffle((UINT8*) im->image[state->y + state->yoff] + + state->xoff * im->pixelsize, state->buffer, + state->xsize); + + state->x = 0; + + if (++state->y >= state->ysize) { + /* End of file (errcode = 0) */ + return -1; + } + } + + if (extra_bytes == 0) { + break; + } + + if (state->x > 0) { + break; // assert + } + + if (extra_bytes >= state->bytes) { + n = state->bytes; + } else { + n = extra_bytes; + } + memset(state->buffer + state->x, extra_data, n); + extra_bytes -= n; + } } return ptr - buf; diff --git a/libImaging/TiffDecode.c b/libImaging/TiffDecode.c index 8793f2b34..f292da388 100644 --- a/libImaging/TiffDecode.c +++ b/libImaging/TiffDecode.c @@ -58,7 +58,7 @@ tsize_t _tiffWriteProc(thandle_t hdata, tdata_t buf, tsize_t size) { tdata_t new; tsize_t newsize=state->size; while (newsize < (size + state->size)) { - if (newsize > (tsize_t)SIZE_MAX - 64*1024){ + if (newsize > INT_MAX - 64*1024){ return 0; } newsize += 64*1024; diff --git a/map.c b/map.c index 7309a7bd7..75f463440 100644 --- a/map.c +++ b/map.c @@ -342,8 +342,18 @@ PyImaging_MapBuffer(PyObject* self, PyObject* args) stride = xsize * 4; } + if (ysize > INT_MAX / stride) { + PyErr_SetString(PyExc_MemoryError, "Integer overflow in ysize"); + return NULL; + } + size = (Py_ssize_t) ysize * stride; + if (offset > PY_SSIZE_T_MAX - size) { + PyErr_SetString(PyExc_MemoryError, "Integer overflow in offset"); + return NULL; + } + /* check buffer size */ if (PyImaging_GetBuffer(target, &view) < 0) return NULL; diff --git a/py3.h b/py3.h index a2a1e264f..310583845 100644 --- a/py3.h +++ b/py3.h @@ -1,5 +1,5 @@ /* - Python3 definition file to consistently map the code to Python 2.6 or + Python3 definition file to consistently map the code to Python 2 or Python 3. PyInt and PyLong were merged into PyLong in Python 3, so all PyInt functions diff --git a/selftest.py b/selftest.py old mode 100644 new mode 100755 index 7829bae5b..067db4d79 --- a/selftest.py +++ b/selftest.py @@ -1,3 +1,4 @@ +#!/usr/bin/env python # minimal sanity check from __future__ import print_function diff --git a/setup.py b/setup.py old mode 100644 new mode 100755 index 024780d3b..8992a1179 --- a/setup.py +++ b/setup.py @@ -1,3 +1,4 @@ +#!/usr/bin/env python # > pyroma . # ------------------------------ # Checking . @@ -40,6 +41,8 @@ _LIB_IMAGING = ( DEBUG = False +class DependencyException(Exception): pass +class RequiredDependencyException(Exception): pass def _dbg(s, tp=None): if DEBUG: @@ -72,20 +75,7 @@ def _find_include_file(self, include): def _find_library_file(self, library): - # Fix for 3.2.x <3.2.4, 3.3.0, shared lib extension is the python shared - # lib extension, not the system shared lib extension: e.g. .cpython-33.so - # vs .so. See Python bug http://bugs.python.org/16754 - if 'cpython' in self.compiler.shared_lib_extension: - _dbg('stripping cpython from shared library extension %s', - self.compiler.shared_lib_extension) - existing = self.compiler.shared_lib_extension - self.compiler.shared_lib_extension = "." + existing.split('.')[-1] - ret = self.compiler.find_library_file(self.compiler.library_dirs, - library) - self.compiler.shared_lib_extension = existing - else: - ret = self.compiler.find_library_file(self.compiler.library_dirs, - library) + ret = self.compiler.find_library_file(self.compiler.library_dirs, library) if ret: _dbg('Found library %s at %s', (library, ret)) else: @@ -98,9 +88,15 @@ def _lib_include(root): # map root to (root/lib, root/include) return os.path.join(root, "lib"), os.path.join(root, "include") +def _cmd_exists(cmd): + return any( + os.access(os.path.join(path, cmd), os.X_OK) + for path in os.environ["PATH"].split(os.pathsep) + ) def _read(file): - return open(file, 'rb').read() + with open(file, 'rb') as fp: + return fp.read() try: @@ -110,7 +106,7 @@ except (ImportError, OSError): _tkinter = None NAME = 'Pillow' -PILLOW_VERSION = '3.4.0.dev0' +PILLOW_VERSION = '4.1.0.dev0' JPEG_ROOT = None JPEG2K_ROOT = None ZLIB_ROOT = None @@ -120,12 +116,26 @@ FREETYPE_ROOT = None LCMS_ROOT = None +def _pkg_config(name): + try: + command = [ + 'pkg-config', + '--libs-only-L', name, + '--cflags-only-I', name, + ] + if not DEBUG: + command.append('--silence-errors') + libs = subprocess.check_output(command).decode('utf8').split(' ') + return libs[1][2:].strip(), libs[0][2:].strip() + except: + pass + class pil_build_ext(build_ext): class feature: features = ['zlib', 'jpeg', 'tiff', 'freetype', 'lcms', 'webp', 'webpmux', 'jpeg2000', 'imagequant'] - required = set(['jpeg', 'zlib']) + required = {'jpeg', 'zlib'} def __init__(self): for f in self.features: @@ -184,15 +194,37 @@ class pil_build_ext(build_ext): _add_directory(include_dirs, "libImaging") + pkg_config = None + if _cmd_exists('pkg-config'): + pkg_config = _pkg_config + # # add configured kits + for root_name, lib_name in dict(JPEG_ROOT="libjpeg", + JPEG2K_ROOT="libopenjp2", + TIFF_ROOT=("libtiff-5", "libtiff-4"), + ZLIB_ROOT="zlib", + FREETYPE_ROOT="freetype2", + LCMS_ROOT="lcms2", + IMAGEQUANT_ROOT="libimagequant" + ).items(): + root = globals()[root_name] + if root is None and pkg_config: + if isinstance(lib_name, tuple): + for lib_name2 in lib_name: + _dbg('Looking for `%s` using pkg-config.' % lib_name2) + root = pkg_config(lib_name2) + if root: + break + else: + _dbg('Looking for `%s` using pkg-config.' % lib_name) + root = pkg_config(lib_name) - for root in (JPEG_ROOT, JPEG2K_ROOT, TIFF_ROOT, ZLIB_ROOT, - FREETYPE_ROOT, LCMS_ROOT, IMAGEQUANT_ROOT): - if isinstance(root, type(())): + if isinstance(root, tuple): lib_root, include_root = root else: lib_root = include_root = root + _add_directory(library_dirs, lib_root) _add_directory(include_dirs, include_root) @@ -363,8 +395,7 @@ 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) @@ -424,7 +455,7 @@ class pil_build_ext(build_ext): os.path.isfile(os.path.join(directory, name, 'openjpeg.h')): _dbg('Found openjpeg.h in %s/%s', (directory, name)) - version = tuple([int(x) for x in name[9:].split('.')]) + version = tuple(int(x) for x in name[9:].split('.')) if best_version is None or version > best_version: best_version = version best_path = os.path.join(directory, name) @@ -437,8 +468,7 @@ class pil_build_ext(build_ext): # include path _add_directory(self.compiler.include_dirs, best_path, 0) feature.jpeg2000 = 'openjp2' - feature.openjpeg_version = '.'.join([str(x) for x in - best_version]) + feature.openjpeg_version = '.'.join(str(x) for x in best_version) if feature.want('imagequant'): _dbg('Looking for imagequant') @@ -516,12 +546,8 @@ class pil_build_ext(build_ext): for f in feature: if not getattr(feature, f) and feature.require(f): if f in ('jpeg', 'zlib'): - raise ValueError( - '%s is required unless explicitly disabled' - ' using --disable-%s, aborting' % (f, f)) - raise ValueError( - '--enable-%s requested but %s not found, aborting.' % - (f, f)) + raise RequiredDependencyException(f) + raise DependencyException(f) # # core library @@ -605,16 +631,11 @@ class pil_build_ext(build_ext): build_ext.build_extensions(self) # - # sanity and security checks + # sanity checks - unsafe_zlib = None + self.summary_report(feature) - if feature.zlib: - unsafe_zlib = self.check_zlib_version(self.compiler.include_dirs) - - self.summary_report(feature, unsafe_zlib) - - def summary_report(self, feature, unsafe_zlib): + def summary_report(self, feature): print("-" * 68) print("PIL SETUP SUMMARY") @@ -650,16 +671,6 @@ class pil_build_ext(build_ext): print("*** %s support not available" % option[1]) all = 0 - if feature.zlib and unsafe_zlib: - print("") - print("*** Warning: zlib", unsafe_zlib) - print("may contain a security vulnerability.") - print("*** Consider upgrading to zlib 1.2.3 or newer.") - print("*** See: http://www.kb.cert.org/vuls/id/238678") - print(" http://www.kb.cert.org/vuls/id/680620") - print(" http://www.gzip.org/zlib/advisory-2002-03-11.txt") - print("") - print("-" * 68) if not all: @@ -671,21 +682,6 @@ class pil_build_ext(build_ext): print("To check the build, run the selftest.py script.") print("") - def check_zlib_version(self, include_dirs): - # look for unsafe versions of zlib - for subdir in include_dirs: - zlibfile = os.path.join(subdir, "zlib.h") - if os.path.isfile(zlibfile): - break - else: - return - for line in open(zlibfile).readlines(): - m = re.match(r'#define\s+ZLIB_VERSION\s+"([^"]*)"', line) - if not m: - continue - if m.group(1) < "1.2.3": - return m.group(1) - # https://hg.python.org/users/barry/rev/7e8deab93d5a def add_multiarch_paths(self): # Debian/Ubuntu multiarch support. @@ -702,9 +698,8 @@ class pil_build_ext(build_ext): return try: if ret >> 8 == 0: - fp = open(tmpfile, 'r') - multiarch_path_component = fp.readline().strip() - fp.close() + with open(tmpfile, 'r') as fp: + multiarch_path_component = fp.readline().strip() _add_directory(self.compiler.library_dirs, '/usr/lib/' + multiarch_path_component) _add_directory(self.compiler.include_dirs, @@ -716,38 +711,60 @@ class pil_build_ext(build_ext): def debug_build(): return hasattr(sys, 'gettotalrefcount') +try: + setup(name=NAME, + version=PILLOW_VERSION, + description='Python Imaging Library (Fork)', + long_description=_read('README.rst').decode('utf-8'), + author='Alex Clark (Fork Author)', + author_email='aclark@aclark.net', + url='http://python-pillow.org', + classifiers=[ + "Development Status :: 6 - Mature", + "Topic :: Multimedia :: Graphics", + "Topic :: Multimedia :: Graphics :: Capture :: Digital Camera", + "Topic :: Multimedia :: Graphics :: Capture :: Screen Capture", + "Topic :: Multimedia :: Graphics :: Graphics Conversion", + "Topic :: Multimedia :: Graphics :: Viewers", + "Programming Language :: Python :: 2", + "Programming Language :: Python :: 2.7", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.3", + "Programming Language :: Python :: 3.4", + "Programming Language :: Python :: 3.5", + "Programming Language :: Python :: 3.6", + 'Programming Language :: Python :: Implementation :: CPython', + 'Programming Language :: Python :: Implementation :: PyPy', + ], + cmdclass={"build_ext": pil_build_ext}, + ext_modules=[Extension("PIL._imaging", ["_imaging.c"])], + include_package_data=True, + packages=find_packages(), + scripts=glob.glob("Scripts/*.py"), + install_requires=['olefile'], + test_suite='nose.collector', + keywords=["Imaging", ], + license='Standard PIL License', + zip_safe=not debug_build(), ) +except RequiredDependencyException as err: + msg = """ + +The headers or library files could not be found for %s, +a required dependency when compiling Pillow from source. + +Please see the install instructions at: + http://pillow.readthedocs.io/en/latest/installation.html + +""" % (str(err)) + sys.stderr.write(msg) + raise RequiredDependencyException(msg) +except DependencyException as err: + msg = """ + +The headers or library files could not be found for %s, +which was requested by the option flag --enable-%s + +""" % (str(err), str(err)) + sys.stderr.write(msg) + raise DependencyException(msg) -setup(name=NAME, - version=PILLOW_VERSION, - description='Python Imaging Library (Fork)', - long_description=_read('README.rst').decode('utf-8'), - author='Alex Clark (Fork Author)', - author_email='aclark@aclark.net', - url='http://python-pillow.org', - classifiers=[ - "Development Status :: 6 - Mature", - "Topic :: Multimedia :: Graphics", - "Topic :: Multimedia :: Graphics :: Capture :: Digital Camera", - "Topic :: Multimedia :: Graphics :: Capture :: Screen Capture", - "Topic :: Multimedia :: Graphics :: Graphics Conversion", - "Topic :: Multimedia :: Graphics :: Viewers", - "Programming Language :: Python :: 2", - "Programming Language :: Python :: 2.6", - "Programming Language :: Python :: 2.7", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.2", - "Programming Language :: Python :: 3.3", - "Programming Language :: Python :: 3.4", - "Programming Language :: Python :: 3.5", - 'Programming Language :: Python :: Implementation :: CPython', - 'Programming Language :: Python :: Implementation :: PyPy', - ], - cmdclass={"build_ext": pil_build_ext}, - ext_modules=[Extension("PIL._imaging", ["_imaging.c"])], - include_package_data=True, - packages=find_packages(), - scripts=glob.glob("Scripts/*.py"), - test_suite='nose.collector', - keywords=["Imaging", ], - license='Standard PIL License', - zip_safe=not debug_build(), ) diff --git a/tox.ini b/tox.ini index 507af757a..a4a21e697 100644 --- a/tox.ini +++ b/tox.ini @@ -4,7 +4,7 @@ # and then run "tox" from this directory. [tox] -envlist = py26, py27, py32, py33, py34, py35 +envlist = py27, py33, py34, py35, py36 [testenv] commands = diff --git a/winbuild/build.py b/winbuild/build.py index aacf13174..1fbfa14a6 100644 --- a/winbuild/build.py +++ b/winbuild/build.py @@ -16,11 +16,8 @@ def setup_vms(): for arch in ('', X64_EXT): ret.append("virtualenv -p c:/Python%s%s/python.exe --clear %s%s%s" % (py, arch, VIRT_BASE, py, arch)) - ret.append("%s%s%s\Scripts\pip.exe install nose" % + ret.append(r"%s%s%s\Scripts\pip.exe install nose" % (VIRT_BASE, py, arch)) - if py == '26': - ret.append("%s%s%s\Scripts\pip.exe install unittest2" % - (VIRT_BASE, py, arch)) return "\n".join(ret) diff --git a/winbuild/build.rst b/winbuild/build.rst index e4c2293a9..86c4cf4c7 100644 --- a/winbuild/build.rst +++ b/winbuild/build.rst @@ -31,7 +31,7 @@ The build routines expect Python to be installed at C:\PythonXX for Download Python 3.4, install it, and add it to the path. This is the Python that we will use to bootstrap the build process. (The download -routines are using 3.2+ features, and installing 3.4 gives us pip and +routines are using 3 features, and installing 3.4 gives us pip and virtualenv as well, reducing the number of packages that we need to install.) diff --git a/winbuild/build_dep.py b/winbuild/build_dep.py index 4c397236b..fb4d55d8c 100644 --- a/winbuild/build_dep.py +++ b/winbuild/build_dep.py @@ -13,6 +13,7 @@ def _relpath(*args): def _relbuild(*args): return _relpath('build', *args) + build_dir = _relpath('build') inc_dir = _relpath('depends') @@ -108,7 +109,7 @@ set MSBUILD=C:\Windows\Microsoft.NET\Framework64\v4.0.30319\MSBuild.exe set CMAKE="cmake.exe" set INCLIB=%~dp0\depends set BUILD=%~dp0\build -""" + "\n".join('set %s=%%BUILD%%\%s' % (k.upper(), v['dir']) +""" + "\n".join(r'set %s=%%BUILD%%\%s' % (k.upper(), v['dir']) for (k, v) in libs.items() if v['dir']) diff --git a/winbuild/config.py b/winbuild/config.py index 1b5d5e380..6e927d2fb 100644 --- a/winbuild/config.py +++ b/winbuild/config.py @@ -19,10 +19,10 @@ libs = { # 'version': '2.0' # }, 'zlib': { - 'url': 'http://zlib.net/zlib128.zip', - 'filename': PILLOW_DEPENDS_DIR + 'zlib128.zip', - 'hash': 'md5:126f8676442ffbd97884eb4d6f32afb4', - 'dir': 'zlib-1.2.8', + 'url': 'http://zlib.net/zlib1211.zip', + 'filename': PILLOW_DEPENDS_DIR + 'zlib1211.zip', + 'hash': 'md5:16b41357b2cd81bca5e1947238e64465', + 'dir': 'zlib-1.2.11', }, 'jpeg': { 'url': 'http://www.ijg.org/files/jpegsr9b.zip', @@ -37,10 +37,10 @@ libs = { 'dir': 'tiff-4.0.6', }, 'freetype': { - 'url': 'http://download.savannah.gnu.org/releases/freetype/freetype-2.6.5.tar.gz', - 'filename': PILLOW_DEPENDS_DIR + 'freetype-2.6.5.tar.gz', - 'hash': 'md5:31b2276515d9ee1c7f37d9c9f4f3145a', - 'dir': 'freetype-2.6.5', + 'url': 'http://download.savannah.gnu.org/releases/freetype/freetype-2.7.1.tar.gz', + 'filename': PILLOW_DEPENDS_DIR + 'freetype-2.7.1.tar.gz', + 'hash': 'md5:78701bee8d249578d83bb9a2f3aa3616', + 'dir': 'freetype-2.7.1', }, 'lcms': { 'url': SF_MIRROR+'/project/lcms/lcms/2.7/lcms2-2.7.zip', @@ -62,29 +62,29 @@ libs = { 'version': '8.5.19', }, 'tcl-8.6': { - 'url': SF_MIRROR+'/project/tcl/Tcl/8.6.5/tcl865-src.zip', - 'filename': PILLOW_DEPENDS_DIR + 'tcl865-src.zip', - 'hash': 'md5:932e360acb40ec760ebeed659bc893de', + 'url': SF_MIRROR+'/project/tcl/Tcl/8.6.6/tcl866-src.zip', + 'filename': PILLOW_DEPENDS_DIR + 'tcl866-src.zip', + 'hash': 'md5:45dae95abc12a5f8c29dca5baf169a13', 'dir': '', }, 'tk-8.6': { - 'url': SF_MIRROR+'/project/tcl/Tcl/8.6.5/tk865-src.zip', - 'filename': PILLOW_DEPENDS_DIR + 'tk865-src.zip', - 'hash': 'md5:f2f5802a5a3b1f70b906e6930db12089', + 'url': SF_MIRROR+'/project/tcl/Tcl/8.6.6/tk866-src.zip', + 'filename': PILLOW_DEPENDS_DIR + 'tk866-src.zip', + 'hash': 'md5:5004cc0ed2ab820406a36a0c0553b917', 'dir': '', - 'version': '8.6.5', + 'version': '8.6.6', }, 'webp': { - 'url': 'http://downloads.webmproject.org/releases/webp/libwebp-0.5.0.tar.gz', - 'filename': PILLOW_DEPENDS_DIR + 'libwebp-0.5.0.tar.gz', - 'hash': 'sha1:d3de815b272fcf88fc4f2dc1ab65d176bcb8df68', - 'dir': 'libwebp-0.5.0', + 'url': 'http://downloads.webmproject.org/releases/webp/libwebp-0.5.2.tar.gz', + 'filename': PILLOW_DEPENDS_DIR + 'libwebp-0.5.2.tar.gz', + 'hash': 'sha1:c3adfa47f96a3909fb05e41636fdcbe3826edfbd', + 'dir': 'libwebp-0.5.2', }, 'openjpeg': { - 'url': SF_MIRROR+'/project/openjpeg/openjpeg/2.1.0/openjpeg-2.1.0.tar.gz', - 'filename': PILLOW_DEPENDS_DIR + 'openjpeg-2.1.0.tar.gz', - 'hash': 'md5:f6419fcc233df84f9a81eb36633c6db6', - 'dir': 'openjpeg-2.1.0', + 'url': SF_MIRROR+'/project/openjpeg/openjpeg/2.1.2/openjpeg-2.1.2.tar.gz', + 'filename': PILLOW_DEPENDS_DIR + 'openjpeg-2.1.2.tar.gz', + 'hash': 'md5:40a7bfdcc66280b3c1402a0eb1a27624', + 'dir': 'openjpeg-2.1.2', }, } diff --git a/winbuild/get_pythons.py b/winbuild/get_pythons.py index 57c866daa..481283df3 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.6.6', '2.7.10', '3.2.5', '3.3.5', '3.4.3']: + for version in ['2.7.10', '3.3.5', '3.4.3']: for platform in ['', '.amd64']: for extension in ['', '.asc']: fetch('https://www.python.org/ftp/python/%s/python-%s%s.msi%s'