diff --git a/.gitattributes b/.gitattributes index 0d357a22f..983b58729 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1 +1,2 @@ *.ppm binary +*.container binary diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index 2ab0121ae..6c91b6427 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -6,7 +6,9 @@ ### What versions of Pillow and Python are you using? -Please include code that reproduces the issue and whenever possible, an image that demonstrates the issue. The best reproductions are self-contained scripts with minimal dependencies. If you are using a framework such as plone, django, or buildout, try to replicate the issue just using Pillow. +Please include **code** that reproduces the issue and whenever possible, an **image** that demonstrates the issue. Please upload images to GitHub, not to third-party file hosting sites. If necessary, add the image to a zip or tar archive. + +The best reproductions are self-contained scripts with minimal dependencies. If you are using a framework such as plone, Django, or buildout, try to replicate the issue just using Pillow. ```python code goes here diff --git a/.gitignore b/.gitignore index 95ed4bac5..aa45f946f 100644 --- a/.gitignore +++ b/.gitignore @@ -34,6 +34,9 @@ htmlcov/ nosetests.xml coverage.xml +# Test files +test_images + # Translations *.mo diff --git a/.travis.yml b/.travis.yml index 0d0b76146..1f34cd7e8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,46 +5,40 @@ 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.7_with_system_site_packages" # For PyQt4 - - 3.3 - - 3.4 - - nightly +# Then run the remainder, with fastest Docker jobs last. + +matrix: + fast_finish: true + include: + - python: "pypy-5.7.1" + - python: "pypy3.3-5.2-alpha1" + - python: '3.6' + - python: '2.7' + - python: "2.7_with_system_site_packages" # For PyQt4 + - python: '3.5' + - python: '3.4' + - python: '3.3' + - env: DOCKER="alpine" + - env: DOCKER="arch" # contains PyQt5 + - env: DOCKER="ubuntu-trusty-x86" + - env: DOCKER="ubuntu-xenial-amd64" + - env: DOCKER="ubuntu-precise-amd64" + - env: DOCKER="debian-stretch-x86" + - env: DOCKER="centos-6-amd64" + - env: DOCKER="amazon-amd64" + +dist: trusty + +sudo: required + +services: + - docker install: - - "travis_retry sudo apt-get update" - - "travis_retry sudo apt-get -qq install libfreetype6-dev liblcms2-dev python-qt4 ghostscript libffi-dev libjpeg-turbo-progs cmake imagemagick" - - "travis_retry pip install cffi" - - "travis_retry pip install nose" - - "travis_retry pip install check-manifest" - # Pyroma tests sometimes hang on PyPy; skip - - if [ $TRAVIS_PYTHON_VERSION != "pypy" ]; then travis_retry pip install pyroma; fi + - if [ "$DOCKER" == "" ]; then .travis/install.sh; fi - - "travis_retry pip install coverage" - - # 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 - - # extra test images - - pushd depends && ./install_extra_test_images.sh && popd - +before_install: + - if [ "$DOCKER" ]; then docker pull pythonpillow/$DOCKER; fi before_script: # Qt needs a display for some of the tests, and it's only run on the system site packages install @@ -52,59 +46,17 @@ before_script: - "sh -e /etc/init.d/xvfb start" script: - - 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 + - | + if [ "$DOCKER" == "" ]; then + .travis/script.sh + else + # the Pillow user in the docker container is UID 1000 + sudo chown -R 1000 $TRAVIS_BUILD_DIR + 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: - | @@ -127,11 +79,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..31f142f33 --- /dev/null +++ b/.travis/after_success.sh @@ -0,0 +1,49 @@ +#!/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 codecov +pip install coveralls-merge +coveralls-merge coverage.c.json +codecov + +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 succeeded! Triggering macOS build..." + # Trigger a macOS build at the pillow-wheels repo + ./build_children.sh + else + echo "Some jobs failed" + fi + fi +fi diff --git a/.travis/install.sh b/.travis/install.sh new file mode 100755 index 000000000..694a460e9 --- /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-tk\ + python-qt4 ghostscript libffi-dev libjpeg-turbo-progs cmake imagemagick\ + libharfbuzz-dev libfribidi-dev + +pip install cffi +pip install nose +pip install check-manifest +pip install olefile +pip install pyroma +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 3e29eac3f..05b07d53c 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,16 +1,453 @@ Changelog (Pillow) ================== -3.5.0 (unreleased) +4.3.0 (unreleased) ------------------ +- Bug: Fix JPEG DPI when EXIF is invalid #2632 + [wiredfool] + +- Bug: Fix for font getsize on empty string #2624 + [radarhere] + +- Docs: Improved ImageDraw documentation #2626 + [radarhere] + +- Docs: Corrected alpha_composite args documentation #2627 + [radarhere] + +- Docs: added the description of the filename attribute to images.rst #2621 + [dasdachs] + +- Dependencies: Updated libimagequant to 2.10.1 #2616 + [radarhere] + +- PDF: Renamed parameter to not shadow built-in dict #2612 + [kijeong] + +4.2.1 (2017-07-06) +------------------ + +- CI: Fix version specification and test on CI for PyPy/Windows #2608 + [wiredfool] + +4.2.0 (2017-07-01) +------------------ + +- Doc: Clarified Image.save:append_images documentation #2604 + [radarhere] + +- CI: Amazon Linux and Centos6 docker images added to TravisCI #2585 + [wiredfool] + +- Image.alpha_composite added #2595 + [wiredfool] + +- Complex Text Support #2576 + [ShamsaHamed, Fahad-Alsaidi, wiredfool] + +- Added threshold parameter to ImageDraw.floodfill #2599 + [nediamond] + +- Added dBATCH parameter to ghostscript command #2588 + [radarhere] + +- JPEG: Adjust buffer size when icc_profile > MAXBLOCK #2596 + [Darou] + +- Specify Pillow Version in one place #2517 + [wiredfool] + +- CI: Change the owner of the TRAVIS_BUILD_DIR, fixing broken docker runs #2587 + [wiredfool] + +- Fix truncated PNG loading for some images, Fix memory leak on truncated PNG images. #2541, #2598 + [homm] + +- Add decompression bomb check to Image.crop #2410 + [wiredfool] + +- ImageFile: Ensure that the `err_code` variable is initialized in case of exception. #2363 + [alexkiro] + +- Tiff: Support append_images for saving multipage TIFFs #2406 + [blochl] + +- Doc: Clarify that draft is only implemented for JPEG and PCD #2409 + [wiredfool] + +- Test: MicImagePlugin #2447 + [hugovk] + +- Use round() instead of floor() to eliminate zero coefficients in resample #2558 + [homm] + +- Remove deprecated code #2549 + [hugovk] + +- Added append_images to PDF saving #2526 + [radarhere] + +- Remove unused function core image function new_array #2548 + [hugovk] + +- Remove unnecessary calls to dict.keys() #2551 + [jdufresne] + +- Add more ImageDraw.py tests and remove unused Draw.c code #2533 + [hugovk] + +- Test: More tests for ImageMorph #2554 + [hugovk] + +- Test: McIDAS area file #2552 + [radarhere] + +- Update Feature Detection #2520 + [wiredfool] + +- CI: Update pypy on TravisCI #2573 + [hugovk] + +- ImageMorph: Fix wrong expected size of MRLs read from disk #2561 + [dov] + +- Docs: Update install docs for FreeBSD #2546 + [wiredfool] + +- Build: Ignore OpenJpeg 1.5 on FreeBSD #2544 + [melvyn-sopacua] + +- Remove 'not yet implemented' methods from PIL 1.1.4 #2538 + [hugovk] + +- Dependencies: Update FreeType to 2.8, LibTIFF to 4.0.8 and libimagequant to 2.9.1 #2535 #2537 #2540 + [radarhere] + +- Raise TypeError and not also UnboundLocalError in ImageFile.Parser() #2525 + [joshblum] + +- Test: Use Codecov for coverage #2528 + [hugovk] + +- Use PNG for Image.show() #2527 + [HinTak, wiredfool] + +- Remove WITH_DEBUG compilation flag #2522 + [wiredfool] + +- Fix return value on parameter parse error in _webp.c #2521 + [adw1n] + +- Set executable flag on scripts with shebang line #2295 + [radarhere] + +- Flake8 #2460 + [radarhere] + +- Doc: Release Process Changes #2516 + [wiredfool] + +- CI: Added region for s3 deployment on appveyor #2515 + [wiredfool] + +- Doc: Updated references to point to existing files #2507 + [radarhere] + +- Return copy on Image crop if crop dimensions match the image #2471 + [radarhere] + +- Test: Optimize CI speed #2464, #2466 + [hugovk] + +4.1.1 (2017-04-28) +------------------ + +- Undef PySlice_GetIndicesEx, see https://bugs.python.org/issue29943 #2493 + [cgohlke] + +- Fix for file with DPI in EXIF but not metadata, and XResolution is an int rather than tuple #2484 + [hugovk] + +- Docs: Removed broken download counter badge #2487 + [hugovk] + +- Docs: Fixed rst syntax error #2477 + [thebjorn] + +4.1.0 (2017-04-03) +------------------ + +- Close files after loading if possible #2330 + [homm, wiredfool] + +- Fix Image Access to be reloadable when embedding the Python interpreter #2296 + [wiredfool, cgohlke] + +- Fetch DPI from EXIF if not specified in JPEG header #2449, #2472 + [hugovk] + +- Removed winbuild checksum verification #2468 + [radarhere] + +- Git: Set ContainerIO test file as binary #2469 + [cgohlke] + +- Remove superfluous import of FixTk #2455 + [cgohlke) + +- Fix import of tkinter/Tkinter #2456 + [cgohlke) + +- Pure Python Decoders, including Python decoder to fix for MSP images #1938 + [wiredfool, hugovk] + +- Reorganized GifImagePlugin, fixes #2314. #2374 + [radarhere, wiredfool] + +- Doc: Reordered operating systems in Compatibility Matrix #2436 + [radarhere] + +- Test: Additional tests for BurfStub, Eps, Container, GribStub, IPTC, Wmf, XVThumb, ImageDraw, ImageMorph ImageShow #2425 + [radarhere] + +- Health fixes #2437 + [radarhere] + +- Test: Correctness tests ContainerIO, XVThumbImagePlugin, BufrStubImagePlugin, GribStubImagePlugin, FitsStubImagePlugin, Hdf5StubImagePlugin, PixarImageFile, PsdImageFile #2443, #2442, #2441, #2440, #2431, #2430, #2428, #2427 + [hugovk] + +- Remove unused imports #1822 + [radarhere] + +- Replaced KeyError catch with dictionary get method #2424 + [radarhere] + +- Test: Removed unrunnable code in test_image_toqimage #2415 + [hugovk] + +- Removed use of spaces in TIFF kwargs names, deprecated in 2.7 #1390 + [radarhere] + +- Removed deprecated ImageDraw setink, setfill, setfont methods #2220 + [jdufresne] + +- Send unwanted subprocess output to /dev/null #2253 + [jdufresne] + +- Fix division by zero when creating 0x0 image from numpy array #2419 + [hugovk] + +- Test: Added matrix convert tests #2381 + [hugovk] + +- Replaced broken URL to partners.adobe.com #2413 + [radarhere] + +- Removed unused private functions in setup.py and build_dep.py #2414 + [radarhere] + +- Test: Fixed Qt tests for QT5 and saving 1 bit PNG #2394 + [wiredfool] + +- Test: docker builds for Arch and Debian Stretch #2394 + [wiredfool] + +- Updated libwebp to 0.6.0 on appveyor #2395 + [radarhere] + +- More explicit error message when saving to a file with invalid extension #2399 + [ces42] + +- Docs: Update some http urls to https #2403 + [hugovk] + +- Preserve aux/alpha channels when performing Imagecms transforms #2355 + [gunjambi] + +- Test linear and radial gradient effects #2382 + [hugovk] + +- Test ImageDraw.Outline and and ImageDraw.Shape #2389 + [hugovk] + +- Added PySide to ImageQt documentation #2392 + [radarhere] + +- BUG: Empty image mode no longer causes a crash #2380 + [evalapply] + +- Exclude .travis and contents from manifest #2386 + [radarhere] + +- Remove 'MIT-like' from license #2145 + [wiredfool] + +- Tests: Add tests for several Image operations #2379 + [radarhere] + +- 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 Webp 0.5.1, OpenJpeg 2.1.2, and TclTk 8.6.6 #2235, #2236, #2237 +- 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 @@ -30,10 +467,10 @@ Changelog (Pillow) - 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] @@ -59,7 +496,7 @@ Changelog (Pillow) [garbas] - Search for tkinter first in builtins #2210 - [matthew-brett] + [matthew-brett] - Tests: Replace try/except/fail pattern with TestCase.assertRaises() #2200 [jdufresne] @@ -72,7 +509,7 @@ Changelog (Pillow) - 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] @@ -89,7 +526,7 @@ Changelog (Pillow) 3.4.2 (2016-10-18) ------------------ -- Fix Resample coefficient calculation #2161 +- Fix Resample coefficient calculation #2162 [homm] @@ -375,7 +812,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 @@ -402,7 +839,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 @@ -468,7 +905,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 @@ -495,7 +932,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 @@ -519,16 +956,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 @@ -543,7 +980,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 @@ -561,7 +998,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 @@ -635,7 +1072,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 @@ -734,16 +1171,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) @@ -800,7 +1237,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 @@ -809,7 +1246,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 @@ -989,16 +1426,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 @@ -1007,7 +1444,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 @@ -1019,7 +1456,7 @@ Changelog (Pillow) - Fix ImagingEffectNoise #1128 [hugovk] -- Remove unreachable code +- Remove unreachable code #1126 [hugovk] - Let Python do the endian stuff + tests #1121 @@ -1040,10 +1477,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) @@ -1052,19 +1489,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 @@ -1076,7 +1513,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 @@ -1100,13 +1537,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 @@ -1148,7 +1585,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) @@ -1163,7 +1600,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 @@ -1229,7 +1666,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 @@ -1247,16 +1684,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 @@ -1281,7 +1718,6 @@ Changelog (Pillow) - Fixed CVE-2014-3589, a DOS in the IcnsImagePlugin (backport) [Andrew Drake] - 2.5.1 (2014-07-10) ------------------ @@ -1294,10 +1730,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 @@ -1306,7 +1742,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 @@ -1315,7 +1751,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 @@ -1330,28 +1766,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 @@ -1366,19 +1802,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 @@ -1393,19 +1829,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 @@ -1414,64 +1850,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 @@ -1483,7 +1919,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 @@ -1492,13 +1928,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 @@ -1507,7 +1943,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) @@ -1525,76 +1961,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 @@ -1615,34 +2051,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. @@ -1651,91 +2087,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. @@ -1743,15 +2181,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. @@ -1762,37 +2202,46 @@ 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. +- Workaround for: https://bugs.python.org/issue16754 in 3.2.x < 3.2.4 and 3.3.0. 2.0.0 (2013-03-15) ------------------ @@ -1804,15 +2253,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. @@ -1941,7 +2390,7 @@ Changelog (Pillow) ---------------- - Remove support for ``import Image``, etc. from the standard namespace. ``from PIL import Image`` etc. now required. -- Forked PIL based on `Hanno Schlichting's re-packaging `_ +- Forked PIL based on `Hanno Schlichting's re-packaging `_ [aclark4life] Pre-fork @@ -2230,7 +2679,7 @@ Pre-fork import numpy, Image - im = Image.open('lena.jpg') + im = Image.open('hopper.jpg') a = numpy.asarray(im) # a is readonly @@ -3073,7 +3522,7 @@ Pre-fork to any other format, via a lookup table. That table should contain 256 values for each band in the output image. - + Some file drivers (including FLI/FLC, GIF, and IM) accidently + + Some file drivers (including FLI/FLC, GIF, and IM) accidentally overwrote the offset method with an internal attribute. All drivers have been updated to use private attributes where possible. diff --git a/LICENSE b/LICENSE index 87743e737..f8346793e 100644 --- a/LICENSE +++ b/LICENSE @@ -5,9 +5,9 @@ The Python Imaging Library (PIL) is Pillow is the friendly PIL fork. It is - Copyright © 2016 by Alex Clark and contributors + Copyright © 2010-2017 by Alex Clark and contributors -Like PIL, Pillow is licensed under the MIT-like open source PIL Software License: +Like PIL, Pillow is licensed under the open source PIL Software License: By obtaining, using, and/or copying this software and/or its associated documentation, you agree that you have read, understood, and will comply with the following terms and conditions: diff --git a/MANIFEST.in b/MANIFEST.in index f6a1488f0..09f0a133e 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -23,6 +23,8 @@ prune docs/_static exclude .coveragerc exclude .editorconfig exclude .landscape.yaml +exclude .travis +exclude .travis/* exclude appveyor.yml exclude build_children.sh exclude tox.ini diff --git a/Makefile b/Makefile index 493364cd8..6f60606a8 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 @@ -77,7 +84,7 @@ release-test: viewdoc sdist: - python setup.py sdist --format=gztar,zip + python setup.py sdist --format=gztar test: python test-installed.py @@ -88,10 +95,10 @@ upload-test: # username: # password: # repository = http://test.pythonpackages.com - python setup.py sdist --format=gztar,zip upload -r test + python setup.py sdist --format=gztar upload -r test upload: - python setup.py sdist --format=gztar,zip upload + python setup.py sdist --format=gztar upload readme: viewdoc 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 b04981af9..cf18eab66 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 @@ -136,7 +131,7 @@ 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), (0xff000000, 0xff0000, 0xff00, 0x0) ], + 32: [(0xff0000, 0xff00, 0xff, 0x0), (0xff0000, 0xff00, 0xff, 0xff000000), (0x0, 0x0, 0x0, 0x0), (0xff000000, 0xff0000, 0xff00, 0x0)], 24: [(0xff0000, 0xff00, 0xff)], 16: [(0xf800, 0x7e0, 0x1f), (0x7c00, 0x3e0, 0x1f)] } diff --git a/PIL/BufrStubImagePlugin.py b/PIL/BufrStubImagePlugin.py index 5184546e4..16d83c74d 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 @@ -40,7 +40,7 @@ class BufrStubImageFile(ImageFile.StubImageFile): offset = self.fp.tell() - if not _accept(self.fp.read(8)): + if not _accept(self.fp.read(4)): raise SyntaxError("Not a BUFR file") self.fp.seek(offset) 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..797b8a5a8 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 @@ -42,7 +41,8 @@ class DcxImageFile(PcxImageFile): format = "DCX" format_description = "Intel DCX" - + _close_exclusive_fp_after_loading = False + def _open(self): # Header 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 77a7e7e1c..6f22308de 100644 --- a/PIL/EpsImagePlugin.py +++ b/PIL/EpsImagePlugin.py @@ -22,17 +22,16 @@ import re import io +import os import sys -from PIL import Image, ImageFile, _binary +from . import Image, ImageFile +from ._binary import i32le as i32 __version__ = "0.5" # # -------------------------------------------------------------------- -i32 = _binary.i32le -o32 = _binary.o32le - split = re.compile(r"^%%([^:]*):[ \t]*(.*)[ \t]*$") field = re.compile(r"^%[%!\w]([^:]*)[ \t]*$") @@ -59,8 +58,8 @@ def has_ghostscript(): if not sys.platform.startswith('win'): import subprocess try: - gs = subprocess.Popen(['gs', '--version'], stdout=subprocess.PIPE) - gs.stdout.read() + with open(os.devnull, 'wb') as devnull: + subprocess.check_call(['gs', '--version'], stdout=devnull) return True except OSError: # no ghostscript @@ -85,7 +84,6 @@ def Ghostscript(tile, size, fp, scale=1): float((72.0 * size[1]) / (bbox[3]-bbox[1]))) # print("Ghostscript", scale, size, orig_size, bbox, orig_bbox, res) - import os import subprocess import tempfile @@ -123,6 +121,7 @@ def Ghostscript(tile, size, fp, scale=1): "-q", # quiet mode "-g%dx%d" % size, # set output geometry (pixels) "-r%fx%f" % res, # set input DPI (dots per inch) + "-dBATCH", # exit after processing "-dNOPAUSE", # don't pause between pages, "-dSAFER", # safe mode "-sDEVICE=ppmraw", # ppm driver @@ -139,12 +138,8 @@ def Ghostscript(tile, size, fp, scale=1): # push data through ghostscript try: - gs = subprocess.Popen(command, stdin=subprocess.PIPE, - stdout=subprocess.PIPE) - gs.stdin.close() - status = gs.wait() - if status: - raise IOError("gs failed (status %d)" % status) + with open(os.devnull, 'w+b') as devnull: + subprocess.check_call(command, stdin=devnull, stdout=devnull) im = Image.open(outfile) im.load() finally: @@ -323,7 +318,7 @@ class EpsImageFile(ImageFile.ImageFile): # EPS can contain binary data # or start directly with latin coding # more info see: - # http://partners.adobe.com/public/developer/en/ps/5002.EPSF_Spec.pdf + # https://web.archive.org/web/20160528181353/http://partners.adobe.com/public/developer/en/ps/5002.EPSF_Spec.pdf offset = i32(s[4:8]) length = i32(s[8:12]) else: 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..a2d0e0fab 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 @@ -41,7 +37,8 @@ class FliImageFile(ImageFile.ImageFile): format = "FLI" format_description = "Autodesk FLI/FLC Animation" - + _close_exclusive_fp_after_loading = False + def _open(self): # HEAD diff --git a/PIL/FontFile.py b/PIL/FontFile.py index d86f9b3ab..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): 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 4fa462f04..0d08f4cc4 100644 --- a/PIL/FtexImagePlugin.py +++ b/PIL/FtexImagePlugin.py @@ -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 2775a00f1..75158809d 100644 --- a/PIL/GifImagePlugin.py +++ b/PIL/GifImagePlugin.py @@ -24,21 +24,14 @@ # 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 + +import itertools __version__ = "0.9" -# -------------------------------------------------------------------- -# Helpers - -i8 = _binary.i8 -i16 = _binary.i16le -o8 = _binary.o8 -o16 = _binary.o16le - - # -------------------------------------------------------------------- # Identify/read GIF files @@ -54,6 +47,8 @@ class GifImageFile(ImageFile.ImageFile): format = "GIF" format_description = "Compuserve GIF" + _close_exclusive_fp_after_loading = False + global_palette = None def data(self): @@ -262,7 +257,7 @@ class GifImageFile(ImageFile.ImageFile): # only dispose the extent in this frame if self.dispose: - self.dispose = self.dispose.crop(self.dispose_extent) + self.dispose = self._crop(self.dispose, self.dispose_extent) except (AttributeError, KeyError): pass @@ -285,7 +280,7 @@ class GifImageFile(ImageFile.ImageFile): if self._prev_im and self.disposal_method == 1: # we do this by pasting the updated area onto the previous # frame which we then use as the current image content - updated = self.im.crop(self.dispose_extent) + updated = self._crop(self.im, self.dispose_extent) self._prev_im.paste(updated, self.dispose_extent, updated.convert('RGBA')) self.im = self._prev_im @@ -294,52 +289,168 @@ class GifImageFile(ImageFile.ImageFile): # -------------------------------------------------------------------- # Write GIF files -try: - import _imaging_gif -except ImportError: - _imaging_gif = None - RAWMODE = { "1": "L", "L": "L", - "P": "P", + "P": "P" } -def _convert_mode(im, initial_call=False): - # convert on the fly (EXPERIMENTAL -- I'm not sure PIL - # should automatically convert images on save...) +def _normalize_mode(im, initial_call=False): + """ + Takes an image (or frame), returns an image in a mode that is appropriate + for saving in a Gif. + + It may return the original image, or it may return an image converted to + palette or 'L' mode. + + UNDONE: What is the point of mucking with the initial call palette, for + an image that shouldn't have a palette, or it would be a mode 'P' and + get returned in the RAWMODE clause. + + :param im: Image object + :param initial_call: Default false, set to true for a single frame. + :returns: Image object + """ + if im.mode in RAWMODE: + im.load() + return im if Image.getmodebase(im.mode) == "RGB": if initial_call: palette_size = 256 if im.palette: palette_size = len(im.palette.getdata()[1]) // 3 - return im.convert("P", palette=1, colors=palette_size) + return im.convert("P", palette=Image.ADAPTIVE, colors=palette_size) else: return im.convert("P") return im.convert("L") +def _normalize_palette(im, palette, info): + """ + Normalizes the palette for image. + - Sets the palette to the incoming palette, if provided. + - Ensures that there's a palette for L mode images + - Optimizes the palette if necessary/desired. + + :param im: Image object + :param palette: bytes object containing the source palette, or .... + :param info: encoderinfo + :returns: Image object + """ + source_palette = None + if palette: + # a bytes palette + if isinstance(palette, (bytes, bytearray, list)): + source_palette = bytearray(palette[:768]) + if isinstance(palette, ImagePalette.ImagePalette): + source_palette = bytearray(itertools.chain.from_iterable( + zip(palette.palette[:256], + palette.palette[256:512], + palette.palette[512:768]))) + + if im.mode == "P": + if not source_palette: + source_palette = im.im.getpalette("RGB")[:768] + else: # L-mode + if not source_palette: + source_palette = bytearray(i//3 for i in range(768)) + im.palette = ImagePalette.ImagePalette("RGB", + palette=source_palette) + + used_palette_colors = _get_optimize(im, info) + if used_palette_colors is not None: + return im.remap_palette(used_palette_colors, source_palette) + + im.palette.palette = source_palette + return im + + +def _write_single_frame(im, fp, palette): + im_out = _normalize_mode(im, True) + im_out = _normalize_palette(im_out, palette, im.encoderinfo) + + for s in _get_global_header(im_out, im.encoderinfo): + fp.write(s) + + # local image header + flags = 0 + if get_interlace(im): + flags = flags | 64 + _write_local_header(fp, im, (0, 0), flags) + + im_out.encoderconfig = (8, get_interlace(im)) + ImageFile._save(im_out, fp, [("gif", (0, 0)+im.size, 0, + RAWMODE[im_out.mode])]) + + fp.write(b"\0") # end of image data + + +def _write_multiple_frames(im, fp, palette): + + duration = im.encoderinfo.get("duration", None) + + im_frames = [] + frame_count = 0 + for imSequence in [im]+im.encoderinfo.get("append_images", []): + for im_frame in ImageSequence.Iterator(imSequence): + # a copy is required here since seek can still mutate the image + im_frame = _normalize_mode(im_frame.copy()) + im_frame = _normalize_palette(im_frame, palette, im.encoderinfo) + + encoderinfo = im.encoderinfo.copy() + if isinstance(duration, (list, tuple)): + encoderinfo['duration'] = duration[frame_count] + frame_count += 1 + + if im_frames: + # delta frame + previous = im_frames[-1] + if _get_palette_bytes(im_frame) == _get_palette_bytes(previous['im']): + delta = ImageChops.subtract_modulo(im_frame, + previous['im']) + else: + delta = ImageChops.subtract_modulo(im_frame.convert('RGB'), + previous['im'].convert('RGB')) + bbox = delta.getbbox() + if not bbox: + # This frame is identical to the previous frame + if duration: + previous['encoderinfo']['duration'] += encoderinfo['duration'] + continue + else: + bbox = None + im_frames.append({ + 'im': im_frame, + 'bbox': bbox, + 'encoderinfo': encoderinfo + }) + + if len(im_frames) > 1: + for frame_data in im_frames: + im_frame = frame_data['im'] + if not frame_data['bbox']: + # global header + for s in _get_global_header(im_frame, + frame_data['encoderinfo']): + fp.write(s) + offset = (0, 0) + else: + # compress difference + frame_data['encoderinfo']['include_color_table'] = True + + im_frame = im_frame.crop(frame_data['bbox']) + offset = frame_data['bbox'][:2] + _write_frame_data(fp, im_frame, offset, frame_data['encoderinfo']) + return True + + def _save_all(im, fp, filename): _save(im, fp, filename, save_all=True) def _save(im, fp, filename, save_all=False): - im.encoderinfo.update(im.info) - if _imaging_gif: - # call external driver - try: - _imaging_gif.save(im, fp, filename) - return - except IOError: - pass # write uncompressed file - - if im.mode in RAWMODE: - im_out = im.copy() - else: - im_out = _convert_mode(im, True) - # header try: palette = im.encoderinfo["palette"] @@ -347,62 +458,8 @@ def _save(im, fp, filename, save_all=False): palette = None im.encoderinfo["optimize"] = im.encoderinfo.get("optimize", True) - if save_all: - previous = None - - first_frame = None - append_images = im.encoderinfo.get("append_images", []) - for imSequence in [im]+append_images: - for im_frame in ImageSequence.Iterator(imSequence): - encoderinfo = im.encoderinfo.copy() - im_frame = _convert_mode(im_frame) - - # To specify duration, add the time in milliseconds to getdata(), - # e.g. getdata(im_frame, duration=1000) - if not previous: - # global header - first_frame = getheader(im_frame, palette, encoderinfo)[0] - first_frame += getdata(im_frame, (0, 0), **encoderinfo) - else: - if first_frame: - for s in first_frame: - fp.write(s) - first_frame = None - - # delta frame - delta = ImageChops.subtract_modulo(im_frame, previous.copy()) - bbox = delta.getbbox() - - if bbox: - # compress difference - encoderinfo['include_color_table'] = True - for s in getdata(im_frame.crop(bbox), - bbox[:2], **encoderinfo): - fp.write(s) - else: - # FIXME: what should we do in this case? - pass - previous = im_frame - if first_frame: - save_all = False - if not save_all: - header = getheader(im_out, palette, im.encoderinfo)[0] - for s in header: - fp.write(s) - - flags = 0 - - if get_interlace(im): - flags = flags | 64 - - # local image header - _get_local_header(fp, im, (0, 0), flags) - - im_out.encoderconfig = (8, get_interlace(im)) - ImageFile._save(im_out, fp, [("gif", (0, 0)+im.size, 0, - RAWMODE[im_out.mode])]) - - fp.write(b"\0") # end of image data + if not save_all or not _write_multiple_frames(im, fp, palette): + _write_single_frame(im, fp, palette) fp.write(b";") # end of file @@ -411,10 +468,7 @@ def _save(im, fp, filename, save_all=False): def get_interlace(im): - try: - interlace = im.encoderinfo["interlace"] - except KeyError: - interlace = 1 + interlace = im.encoderinfo.get("interlace", 1) # workaround for @PIL153 if min(im.size) < 16: @@ -423,7 +477,7 @@ def get_interlace(im): return interlace -def _get_local_header(fp, im, offset, flags): +def _write_local_header(fp, im, offset, flags): transparent_color_exists = False try: transparency = im.encoderinfo["transparency"] @@ -434,18 +488,13 @@ 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 + try: + transparency = used_palette_colors.index(transparency) + except ValueError: + transparent_color_exists = False if "duration" in im.encoderinfo: duration = int(im.encoderinfo["duration"] / 10) @@ -482,11 +531,8 @@ def _get_local_header(fp, im, offset, flags): o8(0)) include_color_table = im.encoderinfo.get('include_color_table') if include_color_table: - try: - palette = im.encoderinfo["palette"] - except KeyError: - palette = None - palette_bytes = _get_palette_bytes(im, palette, im.encoderinfo)[0] + palette = im.encoderinfo.get("palette", None) + palette_bytes = _get_palette_bytes(im) color_table_size = _get_color_table_size(palette_bytes) if color_table_size: flags = flags | 128 # local color table flag @@ -505,6 +551,8 @@ def _get_local_header(fp, im, offset, flags): def _save_netpbm(im, fp, filename): + # Unused by default. + # To use, uncomment the register_save call at the end of the file. # # If you need real GIF compression and/or RGB quantization, you # can use the external NETPBM/PBMPLUS utilities. See comments @@ -512,25 +560,21 @@ def _save_netpbm(im, fp, filename): import os from subprocess import Popen, check_call, PIPE, CalledProcessError - 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 open(os.devnull, 'wb') as devnull: + check_call(["ppmtogif", file], stdout=f, stderr=devnull) + 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 open(os.devnull, 'wb') as devnull: + quant_proc = Popen(quant_cmd, stdout=PIPE, stderr=devnull) + togif_proc = Popen(togif_cmd, stdin=quant_proc.stdout, + stdout=f, stderr=devnull) # Allow ppmquant to receive SIGPIPE if ppmtogif exits quant_proc.stdout.close() @@ -549,24 +593,45 @@ def _save_netpbm(im, fp, filename): pass -# -------------------------------------------------------------------- -# GIF utilities +# 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): - return im.mode in ("P", "L") and info and info.get("optimize", 0) + """ + Palette optimization is a potentially expensive operation. + This function determines if the palette should be optimized using + some heuristics, then returns the list of palette entries in use. -def _get_used_palette_colors(im): - used_palette_colors = [] + :param im: Image object + :param info: encoderinfo + :returns: list of indexes of palette entries in use, or None + """ + if im.mode in ("P", "L") and info and info.get("optimize", 0): + # Potentially expensive operation. - # check which colors are used - i = 0 - for count in im.histogram(): - if count: - used_palette_colors.append(i) - i += 1 + # 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 + optimise = _FORCE_OPTIMIZE or im.mode == 'L' + if optimise or im.width * im.height < 512 * 512: + # check which colors are used + used_palette_colors = [] + for i, count in enumerate(im.histogram()): + if count: + used_palette_colors.append(i) + + if optimise or (len(used_palette_colors) <= 128 and + max(used_palette_colors) > len(used_palette_colors)): + return used_palette_colors - return used_palette_colors def _get_color_table_size(palette_bytes): # calculate the palette size for the header @@ -576,7 +641,15 @@ def _get_color_table_size(palette_bytes): color_table_size = 0 return color_table_size + def _get_header_palette(palette_bytes): + """ + Returns the palette, null padded to the next power of 2 (*3) bytes + suitable for direct inclusion in the GIF header + + :param palette_bytes: Unpadded palette bytes, in RGBRGB form + :returns: Null padded palette + """ color_table_size = _get_color_table_size(palette_bytes) # add the missing amount of bytes @@ -586,102 +659,18 @@ 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): - source_palette = palette[:768] - else: - source_palette = im.im.getpalette("RGB")[:768] - else: # L-mode - if palette and isinstance(palette, bytes): - source_palette = palette[:768] - else: - source_palette = bytearray(i//3 for i in range(768)) +def _get_palette_bytes(im): + """ + Gets the palette for inclusion in the gif header - used_palette_colors = palette_bytes = None + :param im: Image object + :returns: Bytes, len<=768 suitable for inclusion in gif header + """ + return im.palette.palette - if _get_optimize(im, info): - used_palette_colors = _get_used_palette_colors(im) - # 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 - 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 - - # 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 - - # replace the palette color id of all pixel with the new id - - # 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. - - # 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. - - # perf reference, 9500x4000 gif, w/~135 colors - # 14 sec prepatch, 1 sec postpatch with optimization forced. - - mapping_palette = bytearray(new_positions) - - m_im = im.copy() - m_im.mode = 'P' - - 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()) - - if not palette_bytes: - palette_bytes = source_palette - - # returning palette, _not_ padded to 768 bytes like our internal ones. - return palette_bytes, used_palette_colors - -def getheader(im, palette=None, info=None): +def _get_global_header(im, info): """Return a list of strings representing a GIF header""" # Header Block @@ -691,7 +680,7 @@ def getheader(im, palette=None, info=None): for extensionKey in ["transparency", "duration", "loop", "comment"]: if info and extensionKey in info: if ((extensionKey == "duration" and info[extensionKey] == 0) or - (extensionKey == "comment" and not (1 <= len(info[extensionKey]) <= 255))): + (extensionKey == "comment" and not (1 <= len(info[extensionKey]) <= 255))): continue version = b"89a" break @@ -699,42 +688,89 @@ def getheader(im, palette=None, info=None): if im.info.get("version") == b"89a": version = b"89a" - header = [ - b"GIF"+version + # signature + version - o16(im.size[0]) + # canvas width - o16(im.size[1]) # canvas height + palette_bytes = _get_palette_bytes(im) + color_table_size = _get_color_table_size(palette_bytes) + + background = info["background"] if "background" in info else 0 + + return [ + b"GIF"+version + # signature + version + o16(im.size[0]) + # canvas width + o16(im.size[1]), # canvas height + + # Logical Screen Descriptor + # size of global color table + global color table flag + o8(color_table_size + 128), # packed fields + # background + reserved/aspect + o8(background) + o8(0), + + # Global Color Table + _get_header_palette(palette_bytes) ] - palette_bytes, used_palette_colors = _get_palette_bytes(im, palette, info) - # Logical Screen Descriptor - color_table_size = _get_color_table_size(palette_bytes) - # size of global color table + global color table flag - header.append(o8(color_table_size + 128)) # packed fields - # background + reserved/aspect - if info and "background" in info: - background = info["background"] - elif "background" in im.info: - # This elif is redundant within GifImagePlugin - # since im.info parameters are bundled into the info dictionary - # However, external scripts may call getheader directly - # So this maintains earlier behaviour - background = im.info["background"] - else: - background = 0 - header.append(o8(background) + o8(0)) - # end of Logical Screen Descriptor +def _write_frame_data(fp, im_frame, offset, params): + try: + im_frame.encoderinfo = params + + # local image header + _write_local_header(fp, im_frame, offset, 0) + + ImageFile._save(im_frame, fp, [("gif", (0, 0)+im_frame.size, 0, + RAWMODE[im_frame.mode])]) + + fp.write(b"\0") # end of image data + finally: + del im_frame.encoderinfo + +# -------------------------------------------------------------------- +# Legacy GIF utilities + + +def getheader(im, palette=None, info=None): + """ + Legacy Method to get Gif data from image. + + Warning:: May modify image data. + + :param im: Image object + :param palette: bytes object containing the source palette, or .... + :param info: encoderinfo + :returns: tuple of(list of header items, optimized palette) + + """ + used_palette_colors = _get_optimize(im, info) + + if info is None: + info = {} + + if "background" not in info and "background" in im.info: + info["background"] = im.info["background"] + + im_mod = _normalize_palette(im, palette, info) + im.palette = im_mod.palette + im.im = im_mod.im + header = _get_global_header(im, info) - # Header + Logical Screen Descriptor + Global Color Table - header.append(_get_header_palette(palette_bytes)) return header, used_palette_colors +# To specify duration, add the time in milliseconds to getdata(), +# e.g. getdata(im_frame, duration=1000) def getdata(im, offset=(0, 0), **params): - """Return a list of strings representing this image. - The first string is a local image header, the rest contains - encoded image data.""" + """ + Legacy Method + Return a list of strings representing this image. + The first string is a local image header, the rest contains + encoded image data. + + :param im: Image object + :param offset: Tuple of (x, y) pixels. Defaults to (0,0) + :param **params: E.g. duration or other encoder info parameters + :returns: List of Bytes containing gif encoded frame data + + """ class Collector(object): data = [] @@ -745,18 +781,7 @@ def getdata(im, offset=(0, 0), **params): fp = Collector() - try: - im.encoderinfo = params - - # local image header - _get_local_header(fp, im, offset, 0) - - ImageFile._save(im, fp, [("gif", (0, 0)+im.size, 0, RAWMODE[im.mode])]) - - fp.write(b"\0") # end of image data - - finally: - del im.encoderinfo + _write_frame_data(fp, im, offset, params) return fp.data 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 e4b4641e5..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 ## diff --git a/PIL/GribStubImagePlugin.py b/PIL/GribStubImagePlugin.py index e880e5281..33c8291ea 100644 --- a/PIL/GribStubImagePlugin.py +++ b/PIL/GribStubImagePlugin.py @@ -9,7 +9,8 @@ # See the README file for information on usage and redistribution. # -from PIL import Image, ImageFile +from . import Image, ImageFile +from ._binary import i8 _handler = None @@ -28,7 +29,7 @@ def register_handler(handler): # Image adapter def _accept(prefix): - return prefix[0:4] == b"GRIB" and prefix[7] == b'\x01' + return prefix[0:4] == b"GRIB" and i8(prefix[7]) == 1 class GribStubImageFile(ImageFile.StubImageFile): 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..5c5bd7cf9 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 open(os.devnull, 'wb') as devnull: + convert_proc = Popen(convert_cmd, stdout=PIPE, stderr=devnull) convert_proc.stdout.close() diff --git a/PIL/IcoImagePlugin.py b/PIL/IcoImagePlugin.py index b278a85bf..428fdd41a 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("= (3, 4, 0): + def __del__(self): + if (hasattr(self, 'fp') and hasattr(self, '_exclusive_fp') + and self.fp and self._exclusive_fp): + self.fp.close() + self.fp = None + def _copy(self): self.load() self.im = self.im.copy() self.pyaccess = None self.readonly = 0 - def _dump(self, file=None, format=None): + def _dump(self, file=None, format=None, **options): import tempfile suffix = '' if format: @@ -581,7 +595,7 @@ class Image(object): else: if not file.endswith(format): file = file + "." + format - self.save(file, format) + self.save(file, format, **options) return file def __eq__(self, other): @@ -695,8 +709,8 @@ class Image(object): return b"".join(data) def tostring(self, *args, **kw): - raise NotImplementedError("tostring() has been removed. " + - "Please call tobytes() instead.") + raise NotImplementedError("tostring() has been removed. " + "Please call tobytes() instead.") def tobitmap(self, name="image"): """ @@ -746,8 +760,8 @@ class Image(object): raise ValueError("cannot decode image data") def fromstring(self, *args, **kw): - raise NotImplementedError("fromstring() has been removed. " + - "Please call frombytes() instead.") + raise NotImplementedError("fromstring() has been removed. " + "Please call frombytes() instead.") def load(self): """ @@ -777,7 +791,7 @@ class Image(object): if HAS_CFFI and USE_CFFI_ACCESS: if self.pyaccess: return self.pyaccess - from PIL import PyAccess + from . import PyAccess self.pyaccess = PyAccess.new(self, self.readonly) if self.pyaccess: return self.pyaccess @@ -883,7 +897,7 @@ class Image(object): try: t = trns_im.palette.getcolor(t) except: - raise ValueError("Couldn't allocate a palette " + + raise ValueError("Couldn't allocate a palette " "color for transparency") trns_im.putpixel((0, 0), t) @@ -910,7 +924,7 @@ class Image(object): if mode == "P" and palette == ADAPTIVE: im = self.im.quantize(colors) new = self._new(im) - from PIL import ImagePalette + from . import ImagePalette new.palette = ImagePalette.raw("RGB", new.im.getpalette("RGB")) if delete_trns: # This could possibly happen if we requantize to fewer colors. @@ -997,7 +1011,7 @@ class Image(object): "only RGB or L mode images can be quantized to a palette" ) im = self.im.convert("P", 1, palette.im) - return self._makeself(im) + return self._new(im) return self._new(self.im.quantize(colors, method, kmeans)) @@ -1031,6 +1045,20 @@ class Image(object): if box is None: return self.copy() + return self._new(self._crop(self.im, box)) + + def _crop(self, im, box): + """ + Returns a rectangular region from the core image object im. + + This is equivalent to calling im.crop((x0, y0, x1, y1)), but + includes additional sanity checks. + + :param im: a core image object + :param box: The crop rectangle, as a (left, upper, right, lower)-tuple. + :returns: A core image object. + """ + x0, y0, x1, y1 = map(int, map(round, box)) if x1 < x0: @@ -1038,8 +1066,9 @@ class Image(object): if y1 < y0: y1 = y0 - return self._new(self.im.crop(( x0, y0, x1, y1))) + _decompression_bomb_check((x1, y1)) + return im.crop((x0, y0, x1, y1)) def draft(self, mode, size): """ @@ -1053,6 +1082,9 @@ class Image(object): in place. If the image has already been loaded, this method has no effect. + Note: This method is not implemented for most images. It is + currently implemented only for JPEG and PCD images. + :param mode: The requested mode. :param size: The requested size. """ @@ -1258,8 +1290,8 @@ class Image(object): return self.im.histogram() def offset(self, xoffset, yoffset=None): - raise NotImplementedError("offset() has been removed. " + - "Please call ImageChops.offset() instead.") + raise NotImplementedError("offset() has been removed. " + "Please call ImageChops.offset() instead.") def paste(self, im, box=None, mask=None): """ @@ -1323,7 +1355,7 @@ class Image(object): box += (box[0]+size[0], box[1]+size[1]) if isStringType(im): - from PIL import ImageColor + from . import ImageColor im = ImageColor.getcolor(im, self.mode) elif isImageType(im): @@ -1344,6 +1376,54 @@ class Image(object): else: self.im.paste(im, box) + def alpha_composite(self, im, dest=(0,0), source=(0,0)): + """ 'In-place' analog of Image.alpha_composite. Composites an image + onto this image. + + :param im: image to composite over this one + :param dest: Optional 2 tuple (left, top) specifying the upper + left corner in this (destination) image. + :param source: Optional 2 (left, top) tuple for the upper left + corner in the overlay source image, or 4 tuple (left, top, right, + bottom) for the bounds of the source rectangle + + Performance Note: Not currently implemented in-place in the core layer. + """ + + if not isinstance(source, tuple): + raise ValueError("Source must be a tuple") + if not isinstance(dest, tuple): + raise ValueError("Destination must be a tuple") + if not len(source) in (2, 4): + raise ValueError("Source must be a 2 or 4-tuple") + if not len(dest) == 2: + raise ValueError("Destination must be a 2-tuple") + if min(source) < 0: + raise ValueError("Source must be non-negative") + if min(dest) < 0: + raise ValueError("Destination must be non-negative") + + if len(source) == 2: + source = source + im.size + + # over image, crop if it's not the whole thing. + if source == (0,0) + im.size: + overlay = im + else: + overlay = im.crop(source) + + # target for the paste + box = dest + (dest[0] + overlay.width, dest[1] + overlay.height) + + # destination image. don't copy if we're using the whole image. + if dest == (0,0) + self.size: + background = self + else: + background = self.crop(box) + + result = alpha_composite(background, overlay) + self.paste(result, box) + def point(self, lut, mode=None): """ Maps this image through a lookup table or function. @@ -1470,7 +1550,7 @@ class Image(object): :param data: A palette sequence (either a list or a string). """ - from PIL import ImagePalette + from . import ImagePalette if self.mode not in ("L", "P"): raise ValueError("illegal image mode") @@ -1519,6 +1599,80 @@ class Image(object): return self.pyaccess.putpixel(xy, value) return self.im.putpixel(xy, value) + def remap_palette(self, dest_map, source_palette=None): + """ + Rewrites the image to reorder the palette. + + :param dest_map: A list of indexes into the original palette. + e.g. [1,0] would swap a two item palette, and list(range(255)) + is the identity transform. + :param source_palette: Bytes or None. + :returns: An :py:class:`~PIL.Image.Image` object. + + """ + from . import ImagePalette + + if self.mode not in ("L", "P"): + raise ValueError("illegal image mode") + + if source_palette is None: + if self.mode == "P": + source_palette = self.im.getpalette("RGB")[:768] + else: # L-mode + source_palette = bytearray(i//3 for i in range(768)) + + palette_bytes = b"" + new_positions = [0]*256 + + # pick only the used colors from the palette + for i, oldPosition in enumerate(dest_map): + palette_bytes += source_palette[oldPosition*3:oldPosition*3+3] + new_positions[oldPosition] = i + + # replace the palette color id of all pixel with the new id + + # 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. + + # 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. + + # perf reference, 9500x4000 gif, w/~135 colors + # 14 sec prepatch, 1 sec postpatch with optimization forced. + + mapping_palette = bytearray(new_positions) + + m_im = self.copy() + m_im.mode = 'P' + + 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)) + + return m_im + def resize(self, size, resample=NEAREST, box=None): """ Returns a resized copy of this image. @@ -1567,7 +1721,8 @@ class Image(object): return self._new(self.im.resize(size, resample, box)) - def rotate(self, angle, resample=NEAREST, expand=0): + def rotate(self, angle, resample=NEAREST, expand=0, center=None, + translate=None): """ Returns a rotated copy of this image. This method returns a copy of this image, rotated the given number of degrees counter @@ -1584,48 +1739,86 @@ class Image(object): :param expand: Optional expansion flag. If true, expands the output image to make it large enough to hold the entire rotated image. If false or omitted, make the output image the same size as the - input image. + input image. Note that the expand flag assumes rotation around + the center and no translation. + :param center: Optional center of rotation (a 2-tuple). Origin is + the upper left corner. Default is the center of the image. + :param translate: An optional post-rotate translation (a 2-tuple). :returns: An :py:class:`~PIL.Image.Image` object. """ angle = angle % 360.0 - # Fast paths regardless of filter - if angle == 0: - return self.copy() - if angle == 180: - return self.transpose(ROTATE_180) - if angle == 90 and expand: - return self.transpose(ROTATE_90) - if angle == 270 and expand: - return self.transpose(ROTATE_270) + # Fast paths regardless of filter, as long as we're not + # translating or changing the center. + if not (center or translate): + if angle == 0: + return self.copy() + if angle == 180: + return self.transpose(ROTATE_180) + if angle == 90 and expand: + return self.transpose(ROTATE_90) + if angle == 270 and expand: + return self.transpose(ROTATE_270) + + # Calculate the affine matrix. Note that this is the reverse + # transformation (from destination image to source) because we + # want to interpolate the (discrete) destination pixel from + # the local area around the (floating) source pixel. + + # The matrix we actually want (note that it operates from the right): + # (1, 0, tx) (1, 0, cx) ( cos a, sin a, 0) (1, 0, -cx) + # (0, 1, ty) * (0, 1, cy) * (-sin a, cos a, 0) * (0, 1, -cy) + # (0, 0, 1) (0, 0, 1) ( 0, 0, 1) (0, 0, 1) + + # The reverse matrix is thus: + # (1, 0, cx) ( cos -a, sin -a, 0) (1, 0, -cx) (1, 0, -tx) + # (0, 1, cy) * (-sin -a, cos -a, 0) * (0, 1, -cy) * (0, 1, -ty) + # (0, 0, 1) ( 0, 0, 1) (0, 0, 1) (0, 0, 1) + + # In any case, the final translation may be updated at the end to + # compensate for the expand flag. + + w, h = self.size + + if translate is None: + translate = [0, 0] + if center is None: + center = [w / 2.0, h / 2.0] angle = - math.radians(angle) matrix = [ round(math.cos(angle), 15), round(math.sin(angle), 15), 0.0, round(-math.sin(angle), 15), round(math.cos(angle), 15), 0.0 - ] + ] - def transform(x, y, matrix=matrix): + def transform(x, y, matrix): (a, b, c, d, e, f) = matrix return a*x + b*y + c, d*x + e*y + f - w, h = self.size + matrix[2], matrix[5] = transform(-center[0] - translate[0], + -center[1] - translate[1], matrix) + matrix[2] += center[0] + matrix[5] += center[1] + if expand: # calculate output size xx = [] yy = [] for x, y in ((0, 0), (w, 0), (w, h), (0, h)): - x, y = transform(x, y) + x, y = transform(x, y, matrix) xx.append(x) yy.append(y) - w = int(math.ceil(max(xx)) - math.floor(min(xx))) - h = int(math.ceil(max(yy)) - math.floor(min(yy))) + nw = int(math.ceil(max(xx)) - math.floor(min(xx))) + nh = int(math.ceil(max(yy)) - math.floor(min(yy))) - # adjust center - x, y = transform(w / 2.0, h / 2.0) - matrix[2] = self.size[0] / 2.0 - x - matrix[5] = self.size[1] / 2.0 - y + # We multiply a translation matrix from the right. Because of its + # special form, this is the same as taking the image of the + # translation vector as new translation vector. + matrix[2], matrix[5] = transform(-(nw - w) / 2.0, + -(nh - h) / 2.0, + matrix) + w, h = nw, nh return self.transform((w, h), AFFINE, matrix, resample) @@ -1689,7 +1882,10 @@ class Image(object): if not format: if ext not in EXTENSION: init() - format = EXTENSION[ext] + try: + format = EXTENSION[ext] + except KeyError: + raise ValueError('unknown file extension: {}'.format(ext)) if format.upper() not in SAVE: init() @@ -1740,8 +1936,8 @@ class Image(object): PPM file, and calls either the **xv** utility or the **display** utility, depending on which one can be found. - On macOS, this method saves the image to a temporary BMP file, and opens - it with the native Preview application. + On macOS, this method saves the image to a temporary BMP file, and + opens it with the native Preview application. On Windows, it saves the image to a temporary BMP file, and uses the standard BMP display utility to show it (usually Paint). @@ -1956,20 +2152,19 @@ class Image(object): def toqimage(self): """Returns a QImage copy of this image""" - from PIL import ImageQt + from . import ImageQt if not ImageQt.qt_is_installed: raise ImportError("Qt bindings are not installed") return ImageQt.toqimage(self) def toqpixmap(self): """Returns a QPixmap copy of this image""" - from PIL import ImageQt + from . import ImageQt if not ImageQt.qt_is_installed: raise ImportError("Qt bindings are not installed") return ImageQt.toqpixmap(self) - # -------------------------------------------------------------------- # Abstract handlers. @@ -1994,6 +2189,7 @@ def _wedge(): return Image()._new(core.wedge("L")) + def _check_size(size): """ Common check to enforce type and sanity check on size tuples @@ -2006,11 +2202,12 @@ def _check_size(size): raise ValueError("Size must be a tuple") if len(size) != 2: raise ValueError("Size must be a tuple of length 2") - if size[0] <= 0 or size[1] <= 0: - raise ValueError("Width and Height must be > 0") + if size[0] < 0 or size[1] < 0: + raise ValueError("Width and height must be >= 0") return True + def new(mode, size, color=0): """ Creates a new image with the given mode and size. @@ -2036,7 +2233,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)) @@ -2082,7 +2279,7 @@ def frombytes(mode, size, data, decoder_name="raw", *args): def fromstring(*args, **kw): raise NotImplementedError("fromstring() has been removed. " + - "Please call frombytes() instead.") + "Please call frombytes() instead.") def frombuffer(mode, size, data, decoder_name="raw", *args): @@ -2164,16 +2361,13 @@ def fromarray(obj, mode=None): arr = obj.__array_interface__ shape = arr['shape'] ndim = len(shape) - try: - strides = arr['strides'] - except KeyError: - strides = None + strides = arr.get('strides', None) if mode is None: try: 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 @@ -2198,7 +2392,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) @@ -2206,11 +2400,12 @@ 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) + _fromarray_typemap = { # (shape, typestr) => mode, rawmode # first two members of shape are set to one @@ -2276,6 +2471,7 @@ def open(fp, mode="r"): if mode != "r": raise ValueError("bad mode %r" % mode) + exclusive_fp = False filename = "" if isPath(fp): filename = fp @@ -2289,11 +2485,13 @@ def open(fp, mode="r"): if filename: fp = builtins.open(filename, "rb") + exclusive_fp = True try: fp.seek(0) except (AttributeError, io.UnsupportedOperation): fp = io.BytesIO(fp.read()) + exclusive_fp = True prefix = fp.read(16) @@ -2322,8 +2520,11 @@ def open(fp, mode="r"): im = _open_core(fp, filename, prefix) if im: + im._exclusive_fp = exclusive_fp return im + if exclusive_fp: + fp.close() raise IOError("cannot identify image file %r" % (filename if filename else fp)) @@ -2491,6 +2692,44 @@ 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 + + +def register_decoder(name, decoder): + """ + Registers an image decoder. This function should not be + used in application code. + + :param name: The name of the decoder + :param decoder: A callable(mode, args) that returns an + ImageFile.PyDecoder object + + .. versionadded:: 4.1.0 + """ + DECODERS[name] = decoder + + +def register_encoder(name, encoder): + """ + Registers an image encoder. This function should not be + used in application code. + + :param name: The name of the encoder + :param encoder: A callable(mode, args) that returns an + ImageFile.PyEncoder object + + .. versionadded:: 4.1.0 + """ + ENCODERS[name] = encoder + + # -------------------------------------------------------------------- # Simple display support. User code may override this. @@ -2500,7 +2739,7 @@ def _show(image, **options): def _showxv(image, title=None, **options): - from PIL import ImageShow + from . import ImageShow ImageShow.show(image, title, **options) @@ -2529,3 +2768,21 @@ def effect_noise(size, sigma): :param sigma: Standard deviation of noise. """ return Image()._new(core.effect_noise(size, sigma)) + + +def linear_gradient(mode): + """ + Generate 256x256 linear gradient from black to white, top to bottom. + + :param mode: Input mode. + """ + return Image()._new(core.linear_gradient(mode)) + + +def radial_gradient(mode): + """ + Generate 256x256 radial gradient from black to white, centre to edge. + + :param mode: Input mode. + """ + return Image()._new(core.radial_gradient(mode)) 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/ImageCms.py b/PIL/ImageCms.py index 267afa4c6..40fb1ad0a 100644 --- a/PIL/ImageCms.py +++ b/PIL/ImageCms.py @@ -166,7 +166,6 @@ class ImageCmsProfile(object): self._set(profile) else: raise TypeError("Invalid type for Profile") - def _set(self, profile, filename=None): self.profile = profile 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 720403920..89df27338 100644 --- a/PIL/ImageDraw.py +++ b/PIL/ImageDraw.py @@ -31,10 +31,9 @@ # 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. @@ -87,25 +86,14 @@ class ImageDraw(object): self.fill = 0 self.font = None - def setink(self, ink): - raise NotImplementedError("setink() has been removed. " + - "Please use keyword arguments instead.") - - def setfill(self, onoff): - raise NotImplementedError("setfill() has been removed. " + - "Please use keyword arguments instead.") - - def setfont(self, font): - warnings.warn("setfont() is deprecated. " + - "Please set the attribute directly instead.") - # compatibility - self.font = font - def getfont(self): - """Get the current default font.""" + """ + Get the current default font. + + :returns: An image 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 @@ -222,7 +210,6 @@ class ImageDraw(object): if self._multiline_check(text): return self.multiline_text(xy, text, fill, font, anchor, *args, **kwargs) - ink, fill = self._getink(fill) if font is None: font = self.getfont() @@ -230,17 +217,17 @@ class ImageDraw(object): ink = fill if ink is not None: try: - mask, offset = font.getmask2(text, self.fontmode) + mask, offset = font.getmask2(text, self.fontmode, *args, **kwargs) xy = xy[0] + offset[0], xy[1] + offset[1] except AttributeError: try: - mask = font.getmask(text, self.fontmode) + mask = font.getmask(text, self.fontmode, *args, **kwargs) except TypeError: mask = font.getmask(text) self.draw.draw_bitmap(xy, mask, ink) def multiline_text(self, xy, text, fill=None, font=None, anchor=None, - spacing=4, align="left"): + spacing=4, align="left", direction=None, features=None): widths = [] max_width = 0 lines = self._multiline_split(text) @@ -259,25 +246,30 @@ class ImageDraw(object): left += (max_width - widths[idx]) else: assert False, 'align must be "left", "center" or "right"' - self.text((left, top), line, fill, font, anchor) + self.text((left, top), line, fill, font, anchor, + direction=direction, features=features) top += line_spacing left = xy[0] - def textsize(self, text, font=None, *args, **kwargs): + def textsize(self, text, font=None, spacing=4, direction=None, + features=None): """Get the size of a given string, in pixels.""" if self._multiline_check(text): - return self.multiline_textsize(text, font, *args, **kwargs) + return self.multiline_textsize(text, font, spacing, + direction, features) if font is None: font = self.getfont() - return font.getsize(text) + return font.getsize(text, direction, features) - def multiline_textsize(self, text, font=None, spacing=4): + def multiline_textsize(self, text, font=None, spacing=4, direction=None, + features=None): max_width = 0 lines = self._multiline_split(text) line_spacing = self.textsize('A', font=font)[1] + spacing for line in lines: - line_width, line_height = self.textsize(line, font) + line_width, line_height = self.textsize(line, font, spacing, + direction, features) max_width = max(max_width, line_width) return max_width, len(lines)*line_spacing @@ -298,6 +290,7 @@ def Draw(im, mode=None): except AttributeError: return ImageDraw(im, mode) + # experimental access to the outline API try: Outline = Image.core.outline @@ -319,17 +312,17 @@ 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 -def floodfill(image, xy, value, border=None): +def floodfill(image, xy, value, border=None, thresh=0): """ (experimental) Fills a bounded region with a given color. @@ -340,16 +333,20 @@ def floodfill(image, xy, value, border=None): pixels with a color different from the border color. If not given, the region consists of pixels having the same color as the seed pixel. + :param thresh: Optional threshold value which specifies a maximum + tolerable difference of a pixel value from the 'background' in + order for it to be replaced. Useful for filling regions of non- + homogeneous, but similar, colors. """ # based on an implementation by Eric S. Raymond pixel = image.load() x, y = xy try: background = pixel[x, y] - if background == value: + if _color_diff(value, background) <= thresh: return # seed point already has fill color pixel[x, y] = value - except IndexError: + except (ValueError, IndexError): return # seed point outside image edge = [(x, y)] if border is None: @@ -362,7 +359,7 @@ def floodfill(image, xy, value, border=None): except IndexError: pass else: - if p == background: + if _color_diff(p, background) <= thresh: pixel[s, t] = value newedge.append((s, t)) edge = newedge @@ -380,3 +377,10 @@ def floodfill(image, xy, value, border=None): pixel[s, t] = value newedge.append((s, t)) edge = newedge + + +def _color_diff(rgb1, rgb2): + """ + Uses 1-norm distance to calculate difference between two rgb values. + """ + return abs(rgb1[0]-rgb2[0]) + abs(rgb1[1]-rgb2[1]) + abs(rgb1[2]-rgb2[2]) 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 94f705b3c..5e1719fdc 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 @@ -88,10 +88,13 @@ class ImageFile(Image.Image): # filename self.fp = open(fp, "rb") self.filename = fp + self._exclusive_fp = True else: # stream self.fp = fp self.filename = filename + # can be overridden + self._exclusive_fp = None try: self._open() @@ -100,6 +103,9 @@ class ImageFile(Image.Image): KeyError, # unsupported mode EOFError, # got header but not the first frame struct.error) as v: + # close the file only if we have opened it this constructor + if self._exclusive_fp: + self.fp.close() raise SyntaxError(v) if not self.mode or self.size[0] <= 0: @@ -115,6 +121,8 @@ class ImageFile(Image.Image): # raise exception if something's wrong. must be called # directly after open, and closes file when finished. + if self._exclusive_fp: + self.fp.close() self.fp = None def load(self): @@ -152,7 +160,7 @@ class ImageFile(Image.Image): # try memory mapping decoder_name, extents, offset, args = self.tile[0] if decoder_name == "raw" and len(args) >= 3 and args[0] == self.mode \ - and args[0] in Image._MAPMODES: + and args[0] in Image._MAPMODES: try: if hasattr(Image.core, "map"): # use built-in mapper WIN32 only @@ -178,7 +186,7 @@ class ImageFile(Image.Image): self.map = None self.load_prepare() - + err_code = -3 # initialize to unknown error if not self.map: # sort tiles in file order self.tile.sort(key=_tilesort) @@ -191,12 +199,9 @@ class ImageFile(Image.Image): for decoder_name, extents, offset, args in self.tile: decoder = Image._getdecoder(self.mode, decoder_name, - args, self.decoderconfig) + args, self.decoderconfig) seek(offset) - try: - decoder.setimage(self.im, extents) - except ValueError: - continue + decoder.setimage(self.im, extents) if decoder.pulls_fd: decoder.setfd(self.fp) status, err_code = decoder.decode(b"") @@ -211,7 +216,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 @@ -237,20 +242,16 @@ class ImageFile(Image.Image): self.tile = [] self.readonly = readonly - self.fp = None # might be shared + self.load_end() + + if self._exclusive_fp and self._close_exclusive_fp_after_loading: + self.fp.close() + self.fp = None if not self.map and not LOAD_TRUNCATED_IMAGES and err_code < 0: # 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) def load_prepare(self): @@ -379,11 +380,8 @@ class Parser(object): # attempt to open this file try: - try: - fp = io.BytesIO(self.data) + with io.BytesIO(self.data) as fp: im = Image.open(fp) - finally: - fp.close() # explicitly close the virtual file except IOError: # traceback.print_exc() pass # not enough data @@ -431,12 +429,11 @@ class Parser(object): if self.data: # incremental parsing not possible; reopen the file # not that we have all data - try: - fp = io.BytesIO(self.data) - self.image = Image.open(fp) - finally: - self.image.load() - fp.close() # explicitly close the virtual file + with io.BytesIO(self.data) as fp: + try: + self.image = Image.open(fp) + finally: + self.image.load() return self.image @@ -526,3 +523,128 @@ def _safe_read(fp, size): data.append(block) size -= len(block) return b"".join(data) + + +class PyCodecState(object): + def __init__(self): + self.xsize = 0 + self.ysize = 0 + self.xoff = 0 + self.yoff = 0 + + def extents(self): + return (self.xoff, self.yoff, + self.xoff+self.xsize, self.yoff+self.ysize) + + +class PyDecoder(object): + """ + Python implementation of a format decoder. Override this class and + add the decoding logic in the `decode` method. + + See :ref:`Writing Your Own File Decoder in Python` + """ + + _pulls_fd = False + + def __init__(self, mode, *args): + self.im = None + self.state = PyCodecState() + self.fd = None + self.mode = mode + self.init(args) + + def init(self, args): + """ + Override to perform decoder specific initialization + + :param args: Array of args items from the tile entry + :returns: None + """ + self.args = args + + @property + def pulls_fd(self): + return self._pulls_fd + + def decode(self, buffer): + """ + Override to perform the decoding process. + + :param buffer: A bytes object with the data to be decoded. If `handles_eof` + is set, then `buffer` will be empty and `self.fd` will be set. + :returns: A tuple of (bytes consumed, errcode). If finished with decoding + return <0 for the bytes consumed. Err codes are from `ERRORS` + """ + raise NotImplementedError() + + def cleanup(self): + """ + Override to perform decoder specific cleanup + + :returns: None + """ + pass + + def setfd(self, fd): + """ + Called from ImageFile to set the python file-like object + + :param fd: A python file-like object + :returns: None + """ + self.fd = fd + + def setimage(self, im, extents=None): + """ + Called from ImageFile to set the core output image for the decoder + + :param im: A core image object + :param extents: a 4 tuple of (x0, y0, x1, y1) defining the rectangle + for this tile + :returns: None + """ + + # following c code + self.im = im + + if extents: + (x0, y0, x1, y1) = extents + else: + (x0, y0, x1, y1) = (0, 0, 0, 0) + + if x0 == 0 and x1 == 0: + self.state.xsize, self.state.ysize = self.im.size + else: + self.state.xoff = x0 + self.state.yoff = y0 + self.state.xsize = x1 - x0 + self.state.ysize = y1 - y0 + + if self.state.xsize <= 0 or self.state.ysize <= 0: + raise ValueError("Size cannot be negative") + + if (self.state.xsize + self.state.xoff > self.im.size[0] or + self.state.ysize + self.state.yoff > self.im.size[1]): + raise ValueError("Tile cannot extend outside image") + + def set_as_raw(self, data, rawmode=None): + """ + Convenience method to set the internal image from a stream of raw data + + :param data: Bytes to be set + :param rawmode: The rawmode to be used for the decoder. If not specified, + it will default to the mode of the image + :returns: None + """ + + if not rawmode: + rawmode = self.mode + d = Image._getdecoder(self.mode, 'raw', (rawmode)) + d.setimage(self.im, self.state.extents()) + s = d.decode(data) + + if s[0] >= 0: + raise ValueError("not enough image data") + if s[1] != 0: + raise ValueError("cannot decode image data") diff --git a/PIL/ImageFont.py b/PIL/ImageFont.py index 49494b33f..5b2611717 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,10 +37,13 @@ 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() +LAYOUT_BASIC = 0 +LAYOUT_RAQM = 1 + # FIXME: add support for pilfont2 format (see FontFile.py) # -------------------------------------------------------------------- @@ -103,9 +106,12 @@ class ImageFont(object): self.font = Image.core.font(image.im, data) - # delegate critical operations to internal type - self.getsize = self.font.getsize - self.getmask = self.font.getmask + def getsize(self, text, *args, **kwargs): + return self.font.getsize(text) + + def getmask(self, text, mode="", *args, **kwargs): + return self.font.getmask(text, mode) + ## @@ -115,7 +121,8 @@ class ImageFont(object): class FreeTypeFont(object): "FreeType font wrapper (requires _imagingft service)" - def __init__(self, font=None, size=10, index=0, encoding=""): + def __init__(self, font=None, size=10, index=0, encoding="", + layout_engine=None): # FIXME: use service provider instead self.path = font @@ -123,12 +130,21 @@ class FreeTypeFont(object): self.index = index self.encoding = encoding + if layout_engine not in (LAYOUT_BASIC, LAYOUT_RAQM): + layout_engine = LAYOUT_BASIC + if core.HAVE_RAQM: + layout_engine = LAYOUT_RAQM + if layout_engine == LAYOUT_RAQM and not core.HAVE_RAQM: + layout_engine = LAYOUT_BASIC + + self.layout_engine = layout_engine + if isPath(font): - self.font = core.getfont(font, size, index, encoding) + self.font = core.getfont(font, size, index, encoding, layout_engine=layout_engine) else: self.font_bytes = font.read() self.font = core.getfont( - "", size, index, encoding, self.font_bytes) + "", size, index, encoding, self.font_bytes, layout_engine) def getname(self): return self.font.family, self.font.style @@ -136,23 +152,24 @@ class FreeTypeFont(object): def getmetrics(self): return self.font.ascent, self.font.descent - def getsize(self, text): - size, offset = self.font.getsize(text) + def getsize(self, text, direction=None, features=None): + size, offset = self.font.getsize(text, direction, features) return (size[0] + offset[0], size[1] + offset[1]) def getoffset(self, text): return self.font.getsize(text)[1] - def getmask(self, text, mode=""): - return self.getmask2(text, mode)[0] + def getmask(self, text, mode="", direction=None, features=None): + return self.getmask2(text, mode, direction=direction, features=features)[0] - def getmask2(self, text, mode="", fill=Image.core.fill): - size, offset = self.font.getsize(text) + def getmask2(self, text, mode="", fill=Image.core.fill, direction=None, features=None): + size, offset = self.font.getsize(text, direction, features) im = fill("L", size, 0) - self.font.render(text, im.id, mode == "1") + self.font.render(text, im.id, mode == "1", direction, features) return im, offset - def font_variant(self, font=None, size=None, index=None, encoding=None): + def font_variant(self, font=None, size=None, index=None, encoding=None, + layout_engine=None): """ Create a copy of this FreeTypeFont object, using any specified arguments to override the settings. @@ -165,8 +182,9 @@ class FreeTypeFont(object): return FreeTypeFont(font=self.path if font is None else font, size=self.size if size is None else size, index=self.index if index is None else index, - encoding=self.encoding if encoding is None else - encoding) + encoding=self.encoding if encoding is None else encoding, + layout_engine=self.layout_engine if layout_engine is None else layout_engine + ) class TransposedFont(object): @@ -185,14 +203,14 @@ class TransposedFont(object): self.font = font self.orientation = orientation # any 'transpose' argument, or None - def getsize(self, text): + def getsize(self, text, *args, **kwargs): w, h = self.font.getsize(text) if self.orientation in (Image.ROTATE_90, Image.ROTATE_270): return h, w return w, h - def getmask(self, text, mode=""): - im = self.font.getmask(text, mode) + def getmask(self, text, mode="", *args, **kwargs): + im = self.font.getmask(text, mode, *args, **kwargs) if self.orientation is not None: return im.transpose(self.orientation) return im @@ -212,7 +230,8 @@ def load(filename): return f -def truetype(font=None, size=10, index=0, encoding=""): +def truetype(font=None, size=10, index=0, encoding="", + layout_engine=None): """ Load a TrueType or OpenType font file, and create a font object. This function loads a font object from the given file, and creates @@ -230,12 +249,14 @@ def truetype(font=None, size=10, index=0, encoding=""): Symbol), "ADOB" (Adobe Standard), "ADBE" (Adobe Expert), and "armn" (Apple Roman). See the FreeType documentation for more information. + :param layout_engine: Which layout engine to use, if available: + `ImageFont.LAYOUT_BASIC` or `ImageFont.LAYOUT_RAQM`. :return: A font object. :exception IOError: If the file could not be read. """ try: - return FreeTypeFont(font, size, index, encoding) + return FreeTypeFont(font, size, index, encoding, layout_engine) except IOError: ttf_filename = os.path.basename(font) @@ -266,16 +287,16 @@ def truetype(font=None, size=10, index=0, encoding=""): for walkfilename in walkfilenames: if ext and walkfilename == ttf_filename: fontpath = os.path.join(walkroot, walkfilename) - return FreeTypeFont(fontpath, size, index, encoding) + return FreeTypeFont(fontpath, size, index, encoding, layout_engine) elif not ext and os.path.splitext(walkfilename)[0] == ttf_filename: fontpath = os.path.join(walkroot, walkfilename) if os.path.splitext(fontpath)[1] == '.ttf': - return FreeTypeFont(fontpath, size, index, encoding) + return FreeTypeFont(fontpath, size, index, encoding, layout_engine) if not ext and first_font_with_a_different_extension is None: first_font_with_a_different_extension = fontpath if first_font_with_a_different_extension: return FreeTypeFont(first_font_with_a_different_extension, size, - index, encoding) + index, encoding, layout_engine) raise diff --git a/PIL/ImageGrab.py b/PIL/ImageGrab.py index 03283f3d6..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"]: @@ -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 0bbfbb42b..d2367737f 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 @@ -122,7 +123,7 @@ class LutBuilder(object): .replace('0', 'Z') .replace('1', '0') .replace('Z', '1')) - res = '%d' % (1-int(res)) + res = 1-int(res) patterns.append((pattern, res)) return patterns @@ -151,9 +152,9 @@ class LutBuilder(object): patterns += self._pattern_permute(pattern, options, result) # # Debugging -# for p,r in patterns: -# print p,r -# print '--' +# for p, r in patterns: +# print(p, r) +# print('--') # compile the patterns into regular expressions for speed for i, pattern in enumerate(patterns): @@ -233,7 +234,7 @@ class MorphOp(object): with open(filename, 'rb') as f: self.lut = bytearray(f.read()) - if len(self.lut) != 8192: + if len(self.lut) != LUT_SIZE: self.lut = None raise Exception('Wrong size operator file!') 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 8bf09385c..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): 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 2ce89e7ad..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 diff --git a/PIL/ImageShow.py b/PIL/ImageShow.py index 33f059d74..f59ba9bad 100644 --- a/PIL/ImageShow.py +++ b/PIL/ImageShow.py @@ -69,7 +69,7 @@ class Viewer(object): # FIXME: auto-contrast if max() > 255? else: base = Image.getmodebase(image.mode) - if base != image.mode and image.mode != "1": + if base != image.mode and image.mode != "1" and image.mode != "RGBA": image = image.convert(base) return self.show_image(image, **options) @@ -77,6 +77,7 @@ class Viewer(object): # hook methods format = None + options = {} def get_format(self, image): """Return format name, or None to save as PGM/PPM""" @@ -87,7 +88,7 @@ class Viewer(object): def save_image(self, image): """Save to temporary file, and return filename""" - return image._dump(format=self.get_format(image)) + return image._dump(format=self.get_format(image), **self.options) def show_image(self, image, **options): """Display given image""" @@ -115,7 +116,8 @@ if sys.platform == "win32": elif sys.platform == "darwin": class MacViewer(Viewer): - format = "BMP" + format = "PNG" + options = {'compress_level': 1} def get_command(self, file, **options): # on darwin open returns immediately resulting in the temp @@ -142,6 +144,9 @@ else: return None class UnixViewer(Viewer): + format = "PNG" + options = {'compress_level': 1} + def show_file(self, file, **options): command, executable = self.get_command_ex(file, **options) command = "(%s %s; rm -f %s)&" % (command, quote(file), diff --git a/PIL/ImageTk.py b/PIL/ImageTk.py index d3957b6d1..a19ed1d0d 100644 --- a/PIL/ImageTk.py +++ b/PIL/ImageTk.py @@ -25,14 +25,21 @@ # See the README file for information on usage and redistribution. # -try: - import tkinter -except ImportError: - import Tkinter - tkinter = Tkinter - del Tkinter +import sys -from PIL import Image +if sys.version_info[0] > 2: + import tkinter +else: + import Tkinter as tkinter + +# 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..d8398e92b 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): @@ -182,14 +182,6 @@ class Dib(object): """ return self.image.tobytes() - def fromstring(self, *args, **kw): - raise NotImplementedError("fromstring() has been removed. " + - "Please use frombytes() instead.") - - def tostring(self, *args, **kw): - raise NotImplementedError("tostring() has been removed. " + - "Please use tobytes() instead.") - class Window(object): """Create a Window with the given title size.""" diff --git a/PIL/ImtImagePlugin.py b/PIL/ImtImagePlugin.py index e95f7aeba..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" diff --git a/PIL/IptcImagePlugin.py b/PIL/IptcImagePlugin.py index 1de17cbba..f5a8de17e 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" @@ -99,7 +95,7 @@ class IptcImageFile(ImageFile.ImageFile): tagdata = self.fp.read(size) else: tagdata = None - if tag in list(self.info.keys()): + if tag in self.info: if isinstance(self.info[tag], list): self.info[tag].append(tagdata) else: @@ -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]) @@ -191,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 221bf6495..47fb0a6df 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 +from .JpegPresets import presets +from ._util import isStringType __version__ = "0.6" @@ -120,6 +117,25 @@ def APP(self, marker): # plus constant header size self.info["mpoffset"] = self.fp.tell() - n + 4 + # If DPI isn't in JPEG header, fetch from EXIF + if "dpi" not in self.info and "exif" in self.info: + try: + exif = self._getexif() + resolution_unit = exif[0x0128] + x_resolution = exif[0x011A] + try: + dpi = x_resolution[0] / x_resolution[1] + except TypeError: + dpi = x_resolution + if resolution_unit == 3: # cm + # 1 dpcm = 2.54 dpi + dpi *= 2.54 + self.info["dpi"] = dpi, dpi + except (KeyError, SyntaxError): + # SyntaxError for invalid/unreadable exif + # KeyError for dpi not included + self.info["dpi"] = 72, 72 + def COM(self, marker): # @@ -316,7 +332,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 @@ -409,7 +425,8 @@ def _fixup_dict(src_dict): try: if len(value) == 1 and not isinstance(value, dict): return value[0] - except: pass + except: + pass return value return {k: _fixup(v) for k, v in src_dict.items()} @@ -491,7 +508,7 @@ def _getmp(self): try: rawmpentries = mp[0xB002] for entrynum in range(0, quant): - unpackedentry = unpack_from( + unpackedentry = struct.unpack_from( '{}LLLHH'.format(endianness), rawmpentries, entrynum * 16) labels = ('Attribute', 'Size', 'DataOffset', 'EntryNo1', 'EntryNo2') @@ -540,7 +557,6 @@ RAWMODE = { "1": "L", "L": "L", "RGB": "RGB", - "RGBA": "RGB", "RGBX": "RGB", "CMYK": "CMYK;I", # assume adobe conventions "YCbCr": "YCbCr", @@ -589,14 +605,6 @@ def _save(im, fp, filename): except KeyError: raise IOError("cannot write mode %s as JPEG" % im.mode) - if im.mode == 'RGBA': - warnings.warn( - 'You are saving RGBA image as JPEG. The alpha channel will be ' - 'discarded. This conversion is deprecated and will be disabled ' - 'in Pillow 3.7. Please, convert the image to RGB explicitly.', - DeprecationWarning - ) - info = im.encoderinfo dpi = [int(round(x)) for x in info.get("dpi", (0, 0))] @@ -691,8 +699,8 @@ def _save(im, fp, filename): # "progressive" is the official name, but older documentation # says "progression" # FIXME: issue a warning if the wrong form is used (post-1.1.7) - progressive = info.get("progressive", False) or\ - info.get("progression", False) + progressive = (info.get("progressive", False) or + info.get("progression", False)) optimize = info.get("optimize", False) @@ -716,15 +724,19 @@ 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] # The exif info needs to be written as one block, + APP1, + one spare byte. - # Ensure that our buffer is big enough - bufsize = max(ImageFile.MAXBLOCK, bufsize, len(info.get("exif", b"")) + 5) + # Ensure that our buffer is big enough. Same with the icc_profile block. + bufsize = max(ImageFile.MAXBLOCK, bufsize, len(info.get("exif", b"")) + 5, + len(extra) + 1) ImageFile._save(im, fp, [("jpeg", (0, 0)+im.size, 0, rawmode)], bufsize) diff --git a/PIL/JpegPresets.py b/PIL/JpegPresets.py index ece33bbbe..6fda20aec 100644 --- a/PIL/JpegPresets.py +++ b/PIL/JpegPresets.py @@ -62,7 +62,7 @@ The tables format between im.quantization and quantization in presets differ in You can convert the dict format to the preset format with the `JpegImagePlugin.convert_dict_qtables(dict_qtables)` function. -Libjpeg ref.: http://web.archive.org/web/20120328125543/http://www.jpegcameras.com/libjpeg/libjpeg-3.html +Libjpeg ref.: https://web.archive.org/web/20120328125543/http://www.jpegcameras.com/libjpeg/libjpeg-3.html """ diff --git a/PIL/McIdasImagePlugin.py b/PIL/McIdasImagePlugin.py index b75360353..06da33f77 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" @@ -66,6 +66,7 @@ class McIdasImageFile(ImageFile.ImageFile): self.tile = [("raw", (0, 0) + self.size, offset, (rawmode, stride, 1))] + # -------------------------------------------------------------------- # registry diff --git a/PIL/MicImagePlugin.py b/PIL/MicImagePlugin.py index 3c912442b..1b7972061 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 ## @@ -38,6 +39,7 @@ class MicImageFile(TiffImagePlugin.TiffImageFile): format = "MIC" format_description = "Microsoft Image Composer" + _close_exclusive_fp_after_loading = False def _open(self): @@ -45,7 +47,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") @@ -95,6 +97,7 @@ class MicImageFile(TiffImagePlugin.TiffImageFile): return self.frame + # # -------------------------------------------------------------------- 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..b12307f00 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" @@ -39,7 +39,8 @@ class MpoImageFile(JpegImagePlugin.JpegImageFile): format = "MPO" format_description = "MPO (CIPA DC-007)" - + _close_exclusive_fp_after_loading = False + def _open(self): self.fp.seek(0) # prep the fp in order to pass the JPEG test JpegImagePlugin.JpegImageFile._open(self) diff --git a/PIL/MspImagePlugin.py b/PIL/MspImagePlugin.py index 85f8e764b..5ea3c1c49 100644 --- a/PIL/MspImagePlugin.py +++ b/PIL/MspImagePlugin.py @@ -1,6 +1,5 @@ # # The Python Imaging Library. -# $Id$ # # MSP file handling # @@ -9,15 +8,25 @@ # History: # 95-09-05 fl Created # 97-01-03 fl Read/write MSP images +# 17-02-21 es Fixed RLE interpretation # # Copyright (c) Secret Labs AB 1997. # Copyright (c) Fredrik Lundh 1995-97. +# Copyright (c) Eric Soroos 2017. # # See the README file for information on usage and redistribution. # +# More info on this format: https://archive.org/details/gg243631 +# Page 313: +# Figure 205. Windows Paint Version 1: "DanM" Format +# Figure 206. Windows Paint Version 2: "LinS" Format. Used in Windows V2.03 +# +# See also: http://www.fileformat.info/format/mspaint/egff.htm - -from PIL import Image, ImageFile, _binary +from . import Image, ImageFile +from ._binary import i16le as i16, o16le as o16, i8 +import struct +import io __version__ = "0.1" @@ -25,8 +34,6 @@ __version__ = "0.1" # # read MSP files -i16 = _binary.i16le - def _accept(prefix): return prefix[:4] in [b"DanM", b"LinS"] @@ -61,13 +68,93 @@ class MspImageFile(ImageFile.ImageFile): if s[:4] == b"DanM": self.tile = [("raw", (0, 0)+self.size, 32, ("1", 0, 1))] else: - self.tile = [("msp", (0, 0)+self.size, 32+2*self.size[1], None)] + self.tile = [("MSP", (0, 0)+self.size, 32, None)] + + +class MspDecoder(ImageFile.PyDecoder): + # The algo for the MSP decoder is from + # http://www.fileformat.info/format/mspaint/egff.htm + # cc-by-attribution -- That page references is taken from the + # Encyclopedia of Graphics File Formats and is licensed by + # O'Reilly under the Creative Common/Attribution license + # + # For RLE encoded files, the 32byte header is followed by a scan + # line map, encoded as one 16bit word of encoded byte length per + # line. + # + # NOTE: the encoded length of the line can be 0. This was not + # handled in the previous version of this encoder, and there's no + # mention of how to handle it in the documentation. From the few + # examples I've seen, I've assumed that it is a fill of the + # background color, in this case, white. + # + # + # Pseudocode of the decoder: + # Read a BYTE value as the RunType + # If the RunType value is zero + # Read next byte as the RunCount + # Read the next byte as the RunValue + # Write the RunValue byte RunCount times + # If the RunType value is non-zero + # Use this value as the RunCount + # Read and write the next RunCount bytes literally + # + # e.g.: + # 0x00 03 ff 05 00 01 02 03 04 + # would yield the bytes: + # 0xff ff ff 00 01 02 03 04 + # + # which are then interpreted as a bit packed mode '1' image + + _pulls_fd = True + + def decode(self, buffer): + + img = io.BytesIO() + blank_line = bytearray((0xff,)*((self.state.xsize+7)//8)) + try: + self.fd.seek(32) + rowmap = struct.unpack_from("<%dH" % (self.state.ysize), + self.fd.read(self.state.ysize*2)) + except struct.error: + raise IOError("Truncated MSP file in row map") + + for x, rowlen in enumerate(rowmap): + try: + if rowlen == 0: + img.write(blank_line) + continue + row = self.fd.read(rowlen) + if len(row) != rowlen: + raise IOError("Truncated MSP file, expected %d bytes on row %s", + (rowlen, x)) + idx = 0 + while idx < rowlen: + runtype = i8(row[idx]) + idx += 1 + if runtype == 0: + (runcount, runval) = struct.unpack("Bc", row[idx:idx+2]) + img.write(runval * runcount) + idx += 2 + else: + runcount = runtype + img.write(row[idx:idx+runcount]) + idx += runcount + + except struct.error: + raise IOError("Corrupted MSP file in row %d" % x) + + self.set_as_raw(img.getvalue(), ("1", 0, 1)) + + return 0, 0 + + +Image.register_decoder('MSP', MspDecoder) + # # write MSP files (uncompressed only) -o16 = _binary.o16le - def _save(im, fp, filename): @@ -95,6 +182,7 @@ def _save(im, fp, filename): # image body ImageFile._save(im, fp, [("raw", (0, 0)+im.size, 32, ("1", 0, 1))]) + # # registry 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 d02839bdf..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" @@ -108,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..ef89a1907 100644 --- a/PIL/PcxImagePlugin.py +++ b/PIL/PcxImagePlugin.py @@ -25,17 +25,12 @@ # See the README file for information on usage and redistribution. # -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 +118,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..e41a59411 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, ImageSequence +from ._binary import i8 import io __version__ = "0.4" @@ -37,11 +37,11 @@ __version__ = "0.4" # 4. page # 5. page contents -def _obj(fp, obj, **dict): +def _obj(fp, obj, **dictionary): fp.write("%d 0 obj\n" % obj) - if dict: + if dictionary: fp.write("<<\n") - for k, v in dict.items(): + for k, v in dictionary.items(): if v is not None: fp.write("/%s %s\n" % (k, v)) fp.write(">>\n") @@ -133,13 +133,24 @@ def _save(im, fp, filename, save_all=False): # # pages - numberOfPages = 1 + ims = [im] if save_all: - try: - numberOfPages = im.n_frames - except AttributeError: - # Image format does not have n_frames. It is a single frame image - pass + append_images = im.encoderinfo.get("append_images", []) + for append_im in append_images: + if append_im.mode != im.mode: + append_im = append_im.convert(im.mode) + append_im.encoderinfo = im.encoderinfo.copy() + ims.append(append_im) + numberOfPages = 0 + for im in ims: + im_numberOfPages = 1 + if save_all: + try: + im_numberOfPages = im.n_frames + except AttributeError: + # Image format does not have n_frames. It is a single frame image + pass + numberOfPages += im_numberOfPages pages = [str(pageNumber*3+4)+" 0 R" for pageNumber in range(0, numberOfPages)] @@ -151,90 +162,92 @@ def _save(im, fp, filename, save_all=False): Kids="["+"\n".join(pages)+"]") _endobj(fp) - for pageNumber in range(0, numberOfPages): - im.seek(pageNumber) + pageNumber = 0 + for imSequence in ims: + for im in ImageSequence.Iterator(imSequence): + # + # image - # - # image + op = io.BytesIO() - op = io.BytesIO() + if filter == "/ASCIIHexDecode": + if bits == 1: + # FIXME: the hex encoder doesn't support packed 1-bit + # images; do things the hard way... + data = im.tobytes("raw", "1") + im = Image.new("L", (len(data), 1), None) + im.putdata(data) + ImageFile._save(im, op, [("hex", (0, 0)+im.size, 0, im.mode)]) + elif filter == "/DCTDecode": + Image.SAVE["JPEG"](im, op, filename) + elif filter == "/FlateDecode": + ImageFile._save(im, op, [("zip", (0, 0)+im.size, 0, im.mode)]) + elif filter == "/RunLengthDecode": + ImageFile._save(im, op, [("packbits", (0, 0)+im.size, 0, im.mode)]) + else: + raise ValueError("unsupported PDF filter (%s)" % filter) - if filter == "/ASCIIHexDecode": - if bits == 1: - # FIXME: the hex encoder doesn't support packed 1-bit - # images; do things the hard way... - data = im.tobytes("raw", "1") - im = Image.new("L", (len(data), 1), None) - im.putdata(data) - ImageFile._save(im, op, [("hex", (0, 0)+im.size, 0, im.mode)]) - elif filter == "/DCTDecode": - Image.SAVE["JPEG"](im, op, filename) - elif filter == "/FlateDecode": - ImageFile._save(im, op, [("zip", (0, 0)+im.size, 0, im.mode)]) - elif filter == "/RunLengthDecode": - ImageFile._save(im, op, [("packbits", (0, 0)+im.size, 0, im.mode)]) - else: - raise ValueError("unsupported PDF filter (%s)" % filter) + # + # Get image characteristics - # - # Get image characteristics + width, height = im.size - width, height = im.size + xref.append(fp.tell()) + _obj( + fp, pageNumber*3+3, + Type="/XObject", + Subtype="/Image", + Width=width, # * 72.0 / resolution, + Height=height, # * 72.0 / resolution, + Length=len(op.getvalue()), + Filter=filter, + BitsPerComponent=bits, + DecodeParams=params, + ColorSpace=colorspace) - xref.append(fp.tell()) - _obj( - fp, pageNumber*3+3, - Type="/XObject", - Subtype="/Image", - Width=width, # * 72.0 / resolution, - Height=height, # * 72.0 / resolution, - Length=len(op.getvalue()), - Filter=filter, - BitsPerComponent=bits, - DecodeParams=params, - ColorSpace=colorspace) + fp.write("stream\n") + fp.fp.write(op.getvalue()) + fp.write("\nendstream\n") - fp.write("stream\n") - fp.fp.write(op.getvalue()) - fp.write("\nendstream\n") + _endobj(fp) - _endobj(fp) + # + # page - # - # page + xref.append(fp.tell()) + _obj(fp, pageNumber*3+4) + fp.write( + "<<\n/Type /Page\n/Parent 2 0 R\n" + "/Resources <<\n/ProcSet [ /PDF %s ]\n" + "/XObject << /image %d 0 R >>\n>>\n" + "/MediaBox [ 0 0 %d %d ]\n/Contents %d 0 R\n>>\n" % ( + procset, + pageNumber*3+3, + int(width * 72.0 / resolution), + int(height * 72.0 / resolution), + pageNumber*3+5)) + _endobj(fp) - xref.append(fp.tell()) - _obj(fp, pageNumber*3+4) - fp.write( - "<<\n/Type /Page\n/Parent 2 0 R\n" - "/Resources <<\n/ProcSet [ /PDF %s ]\n" - "/XObject << /image %d 0 R >>\n>>\n" - "/MediaBox [ 0 0 %d %d ]\n/Contents %d 0 R\n>>\n" % ( - procset, - pageNumber*3+3, - int(width * 72.0 / resolution), - int(height * 72.0 / resolution), - pageNumber*3+5)) - _endobj(fp) + # + # page contents - # - # page contents + op = TextWriter(io.BytesIO()) - op = TextWriter(io.BytesIO()) + op.write( + "q %d 0 0 %d 0 0 cm /image Do Q\n" % ( + int(width * 72.0 / resolution), + int(height * 72.0 / resolution))) - op.write( - "q %d 0 0 %d 0 0 cm /image Do Q\n" % ( - int(width * 72.0 / resolution), - int(height * 72.0 / resolution))) + xref.append(fp.tell()) + _obj(fp, pageNumber*3+5, Length=len(op.fp.getvalue())) - xref.append(fp.tell()) - _obj(fp, pageNumber*3+5, Length=len(op.fp.getvalue())) + fp.write("stream\n") + fp.fp.write(op.fp.getvalue()) + fp.write("\nendstream\n") - fp.write("stream\n") - fp.fp.write(op.fp.getvalue()) - fp.write("\nendstream\n") + _endobj(fp) - _endobj(fp) + pageNumber += 1 # # trailer diff --git a/PIL/PixarImagePlugin.py b/PIL/PixarImagePlugin.py index fd002d9cf..220577cce 100644 --- a/PIL/PixarImagePlugin.py +++ b/PIL/PixarImagePlugin.py @@ -19,16 +19,15 @@ # 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" @@ -63,6 +62,7 @@ class PixarImageFile(ImageFile.ImageFile): # create tile descriptor (assuming "dumped") self.tile = [("raw", (0, 0)+self.size, 1024, (self.mode, 0, 1))] + # # -------------------------------------------------------------------- diff --git a/PIL/PngImagePlugin.py b/PIL/PngImagePlugin.py index 4d6f9d7ff..e329a6c0d 100644 --- a/PIL/PngImagePlugin.py +++ b/PIL/PngImagePlugin.py @@ -31,23 +31,18 @@ # See the README file for information on usage and redistribution. # -from __future__ import print_function - import logging 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, 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(br"\w\w\w\w").match @@ -118,7 +113,8 @@ class ChunkStream(object): length = i32(s) if not is_cid(cid): - raise SyntaxError("broken PNG file (chunk %s)" % repr(cid)) + if not ImageFile.LOAD_TRUNCATED_IMAGES: + raise SyntaxError("broken PNG file (chunk %s)" % repr(cid)) return cid, pos, length @@ -621,10 +617,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 +714,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 +786,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 adaf8384c..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" 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/PyAccess.py b/PIL/PyAccess.py index 8b67a8ea2..d5acfd0fe 100644 --- a/PIL/PyAccess.py +++ b/PIL/PyAccess.py @@ -20,8 +20,6 @@ # Access.c implementation. # -from __future__ import print_function - import logging import sys @@ -167,7 +165,7 @@ class _PyAccess8(PyAccess): try: # integer self.pixels[y][x] = min(color, 255) - except: + except TypeError: # tuple self.pixels[y][x] = min(color[0], 255) @@ -184,7 +182,7 @@ class _PyAccessI16_N(PyAccess): try: # integer self.pixels[y][x] = min(color, 65535) - except: + except TypeError: # tuple self.pixels[y][x] = min(color[0], 65535) @@ -272,7 +270,7 @@ class _PyAccessF(PyAccess): try: # not a tuple self.pixels[y][x] = color - except: + except TypeError: # tuple self.pixels[y][x] = color[0] diff --git a/PIL/SgiImagePlugin.py b/PIL/SgiImagePlugin.py index d2efd3e25..bca7eb13f 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 08726f90c..6730bd104 100644 --- a/PIL/SpiderImagePlugin.py +++ b/PIL/SpiderImagePlugin.py @@ -27,10 +27,10 @@ # image data from electron microscopy and tomography. # # Spider home page: -# http://spider.wadsworth.org/spider_doc/spider/docs/spider.html +# https://spider.wadsworth.org/spider_doc/spider/docs/spider.html # # Details about the Spider image format: -# http://spider.wadsworth.org/spider_doc/spider/docs/image_doc.html +# https://spider.wadsworth.org/spider_doc/spider/docs/image_doc.html # from __future__ import print_function @@ -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: @@ -98,6 +97,7 @@ class SpiderImageFile(ImageFile.ImageFile): format = "SPIDER" format_description = "Spider 2D image" + _close_exclusive_fp_after_loading = False def _open(self): # check header diff --git a/PIL/SunImagePlugin.py b/PIL/SunImagePlugin.py index c3e2bc402..536aa134a 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,7 +37,8 @@ class SunImageFile(ImageFile.ImageFile): def _open(self): - # The Sun Raster file header is 32 bytes in length and has the following format: + # The Sun Raster file header is 32 bytes in length + # and has the following format: # typedef struct _SunRaster # { @@ -52,7 +52,6 @@ class SunImageFile(ImageFile.ImageFile): # DWORD ColorMapLength; /* Size of the color map in bytes */ # } SUNRASTER; - # HEAD s = self.fp.read(32) if i32(s) != 0x59a66a95: @@ -63,11 +62,11 @@ class SunImageFile(ImageFile.ImageFile): self.size = i32(s[4:8]), i32(s[8:12]) depth = i32(s[12:16]) - data_length = i32(s[16:20]) # unreliable, ignore. + data_length = i32(s[16:20]) # unreliable, ignore. file_type = i32(s[20:24]) - palette_type = i32(s[24:28]) # 0: None, 1: RGB, 2: Raw/arbitrary + palette_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: @@ -85,23 +84,23 @@ class SunImageFile(ImageFile.ImageFile): else: self.mode, rawmode = 'RGB', 'BGRX' else: - raise SyntaxError("Unsupported Mode/Bit Depth") - + raise SyntaxError("Unsupported Mode/Bit Depth") + if palette_length: if palette_length > 1024: raise SyntaxError("Unsupported Color Palette 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 = "P" rawmode = rawmode.replace('L', 'P') - + # 16 bit boundaries on stride - stride = ((self.size[0] * depth + 15) // 16) * 2 + stride = ((self.size[0] * depth + 15) // 16) * 2 # file type: Type is the version (or flavor) of the bitmap # file. The following values are typically found in the Type @@ -119,7 +118,7 @@ class SunImageFile(ImageFile.ImageFile): # 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) + # (https://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))] @@ -127,7 +126,7 @@ class SunImageFile(ImageFile.ImageFile): 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..0bc52529a 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 __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 f1860e4aa..333104966 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 @@ -247,6 +242,7 @@ def _limit_rational(val, max_val): n_d = IFDRational(1 / val if inv else val).limit_rational(max_val) return n_d[::-1] if inv else n_d + ## # Wrapper for TIFF IFDs. @@ -338,7 +334,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__') @@ -467,15 +463,6 @@ class ImageFileDirectory_v2(collections.MutableMapping): def __str__(self): return str(dict(self)) - def as_dict(self): - """Return a dictionary of the image's tags. - - .. deprecated:: 3.0.0 - """ - warnings.warn("as_dict() is deprecated. " + - "Please use dict(ifd) instead.", DeprecationWarning) - return dict(self) - def named(self): """ :returns: dict of name|key: value @@ -540,7 +527,8 @@ class ImageFileDirectory_v2(collections.MutableMapping): self.tagtype[tag] = 2 if self.tagtype[tag] == 7 and bytes is not str: - values = [value.encode("ascii", 'replace') if isinstance(value, str) else value] + values = [value.encode("ascii", 'replace') if isinstance( + value, str) else value] values = tuple(info.cvt_enum(value) for value in values) @@ -569,7 +557,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 @@ -583,7 +571,7 @@ 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) @@ -593,9 +581,13 @@ class ImageFileDirectory_v2(collections.MutableMapping): b"".join(self._pack(fmt, value) for value in values)) list(map(_register_basic, - [(3, "H", "short"), (4, "L", "long"), - (6, "b", "signed byte"), (8, "h", "signed short"), - (9, "l", "signed long"), (11, "f", "float"), (12, "d", "double")])) + [(3, "H", "short"), + (4, "L", "long"), + (6, "b", "signed byte"), + (8, "h", "signed short"), + (9, "l", "signed long"), + (11, "f", "float"), + (12, "d", "double")])) @_register_loader(1, 1) # Basic type, except for the legacy API. def load_byte(self, data, legacy_api=True): @@ -621,7 +613,8 @@ class ImageFileDirectory_v2(collections.MutableMapping): @_register_loader(5, 8) def load_rational(self, data, legacy_api=True): vals = self._unpack("{}L".format(len(data) // 4), data) - combine = lambda a, b: (a, b) if legacy_api else IFDRational(a, b) + + def combine(a, b): return (a, b) if legacy_api else IFDRational(a, b) return tuple(combine(num, denom) for num, denom in zip(vals[::2], vals[1::2])) @@ -641,7 +634,8 @@ class ImageFileDirectory_v2(collections.MutableMapping): @_register_loader(10, 8) def load_signed_rational(self, data, legacy_api=True): vals = self._unpack("{}l".format(len(data) // 4), data) - combine = lambda a, b: (a, b) if legacy_api else IFDRational(a, b) + + def combine(a, b): return (a, b) if legacy_api else IFDRational(a, b) return tuple(combine(num, denom) for num, denom in zip(vals[::2], vals[1::2])) @@ -665,7 +659,8 @@ class ImageFileDirectory_v2(collections.MutableMapping): try: for i in range(self._unpack("H", self._ensure_read(fp, 2))[0]): - tag, typ, count, data = self._unpack("HHL4s", self._ensure_read(fp, 12)) + tag, typ, count, data = self._unpack("HHL4s", + self._ensure_read(fp, 12)) if DEBUG: tagname = TiffTags.lookup(tag).name typname = TYPES.get(typ, "unknown") @@ -693,8 +688,8 @@ class ImageFileDirectory_v2(collections.MutableMapping): if len(data) != size: warnings.warn("Possibly corrupt EXIF data. " - "Expecting to read %d bytes but only got %d. " - "Skipping tag %s" % (size, len(data), tag)) + "Expecting to read %d bytes but only got %d." + " Skipping tag %s" % (size, len(data), tag)) continue if not data: @@ -753,7 +748,8 @@ class ImageFileDirectory_v2(collections.MutableMapping): if len(data) <= 4: entries.append((tag, typ, count, data.ljust(4, b"\0"), b"")) else: - entries.append((tag, typ, count, self._pack("L", offset), data)) + entries.append((tag, typ, count, self._pack("L", offset), + data)) offset += (len(data) + 1) // 2 * 2 # pad to word # update strip offset data to point beyond auxiliary data @@ -782,6 +778,7 @@ class ImageFileDirectory_v2(collections.MutableMapping): return offset + ImageFileDirectory_v2._load_dispatch = _load_dispatch ImageFileDirectory_v2._write_dispatch = _write_dispatch for idx, name in TYPES.items(): @@ -800,7 +797,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, @@ -889,6 +886,7 @@ class TiffImageFile(ImageFile.ImageFile): format = "TIFF" format_description = "Adobe TIFF" + _close_exclusive_fp_after_loading = False def _open(self): "Open the first image in a TIFF file" @@ -974,6 +972,7 @@ class TiffImageFile(ImageFile.ImageFile): self.__frame += 1 self.fp.seek(self._frame_pos[frame]) self.tag_v2.load(self.fp) + self.__next = self.tag_v2.next # fill the legacy tag/ifd entries self.tag = self.ifd = ImageFileDirectory_v1.from_v2(self.tag_v2) self.__frame = frame @@ -1006,9 +1005,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): @@ -1016,6 +1012,12 @@ class TiffImageFile(ImageFile.ImageFile): return self._load_libtiff() return super(TiffImageFile, self).load() + def load_end(self): + # allow closing if we're on the first frame, there's no next + # This is the ImageFile.load path only, libtiff specific below. + if self.__frame == 0 and not self.__next: + self._close_exclusive_fp_after_loading = True + def _load_libtiff(self): """ Overload method triggered when we detect a compressed tiff Calls out to libtiff """ @@ -1093,16 +1095,14 @@ class TiffImageFile(ImageFile.ImageFile): self.tile = [] self.readonly = 0 # libtiff closed the fp in a, we need to close self.fp, if possible - if hasattr(self.fp, 'close'): - if not self.__next: + if self._exclusive_fp: + if self.__frame == 0 and not self.__next: self.fp.close() - self.fp = None # might be shared + self.fp = None # might be shared if err < 0: raise IOError(err) - self.load_end() - return Image.Image.load(self) def _setup(self): @@ -1138,7 +1138,7 @@ class TiffImageFile(ImageFile.ImageFile): sampleFormat = self.tag_v2.get(SAMPLEFORMAT, (1,)) if (len(sampleFormat) > 1 - and max(sampleFormat) == min(sampleFormat) == 1): + and max(sampleFormat) == min(sampleFormat) == 1): # SAMPLEFORMAT is properly per band, so an RGB image will # be (1,1,1). But, we don't support per band pixel types, # and anything more than one band is a uint8. So, just @@ -1171,11 +1171,16 @@ 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 is 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 @@ -1197,7 +1202,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) @@ -1281,11 +1286,17 @@ 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": palette = [o8(b // 256) for b in self.tag_v2[COLORMAP]] self.palette = ImagePalette.raw("RGB;L", b"".join(palette)) + + # # -------------------------------------------------------------------- # Write TIFF files @@ -1362,10 +1373,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"), @@ -1377,11 +1388,6 @@ def _save(im, fp, filename): (DATE_TIME, "date_time"), (ARTIST, "artist"), (COPYRIGHT, "copyright")]: - name_with_spaces = name.replace("_", " ") - if "_" in name and name_with_spaces in im.encoderinfo: - warnings.warn("%r is deprecated; use %r instead" % - (name_with_spaces, name), DeprecationWarning) - ifd[key] = im.encoderinfo[name.replace("_", " ")] if name in im.encoderinfo: ifd[key] = im.encoderinfo[name] @@ -1491,6 +1497,7 @@ def _save(im, fp, filename): # just to access o32 and o16 (using correct byte order) im._debug_multipage = ifd + class AppendingTiffWriter: fieldSizes = [ 0, # None @@ -1678,13 +1685,10 @@ class AppendingTiffWriter: def fixIFD(self): numTags = self.readShort() - #trace("fixing IFD at %X; number of tags: %u (0x%X)", self.f.tell()-2, - # numTags, numTags) for i in range(numTags): - tag, fieldType, count = struct.unpack(self.tagFormat, self.f.read(8)) - #trace(" at %X: tag %u (0x%X), type %u, count %u", self.f.tell()-8, - # tag, tag, fieldType, count) + tag, fieldType, count = struct.unpack(self.tagFormat, + self.f.read(8)) fieldSize = self.fieldSizes[fieldType] totalSize = fieldSize * count @@ -1736,21 +1740,34 @@ class AppendingTiffWriter: else: self.rewriteLastLong(offset) + def _save_all(im, fp, filename): - if not hasattr(im, "n_frames"): + encoderinfo = im.encoderinfo.copy() + encoderconfig = im.encoderconfig + append_images = encoderinfo.get("append_images", []) + if not hasattr(im, "n_frames") and not len(append_images): return _save(im, fp, filename) cur_idx = im.tell() try: with AppendingTiffWriter(fp) as tf: - for idx in range(im.n_frames): - im.seek(idx) - im.load() - _save(im, tf, filename) - tf.newFrame() + for ims in [im]+append_images: + ims.encoderinfo = encoderinfo + ims.encoderconfig = encoderconfig + if not hasattr(ims, "n_frames"): + nfr = 1 + else: + nfr = ims.n_frames + + for idx in range(nfr): + ims.seek(idx) + ims.load() + _save(ims, tf, filename) + tf.newFrame() finally: im.seek(cur_idx) + # # -------------------------------------------------------------------- # Register diff --git a/PIL/WalImageFile.py b/PIL/WalImageFile.py index b0b1e684a..69964f7be 100644 --- a/PIL/WalImageFile.py +++ b/PIL/WalImageFile.py @@ -18,12 +18,11 @@ # the WalImageFile.open() function instead. # This reader is based on the specification available from: -# http://www.flipcode.com/archives/Quake_2_BSP_File_Format.shtml +# https://www.flipcode.com/archives/Quake_2_BSP_File_Format.shtml # and has been tested with a few sample files found using google. -from __future__ import print_function - -from PIL import Image, _binary +from . import Image +from ._binary import i32le as i32 try: import builtins @@ -31,8 +30,6 @@ except ImportError: import __builtin__ builtins = __builtin__ -i32 = _binary.i32le - def open(filename): """ @@ -47,33 +44,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..584523fc7 100644 --- a/PIL/WmfImagePlugin.py +++ b/PIL/WmfImagePlugin.py @@ -14,8 +14,16 @@ # # 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,24 +61,11 @@ 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 + def _accept(prefix): return ( prefix[:6] == b"\xd7\xcd\xc6\x9a\x00\x00" or @@ -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..a7d39ed89 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 i8, o8 __version__ = "0.1" -o8 = _binary.o8 - _MAGIC = b"P7 332" # standard color palette for thumbnails (RGB332) @@ -48,7 +47,7 @@ class XVThumbImageFile(ImageFile.ImageFile): def _open(self): # check magic - if self.fp.read(6) != _MAGIC: + if not _accept(self.fp.read(6)): raise SyntaxError("not an XV thumbnail file") # Skip to beginning of next line @@ -59,14 +58,14 @@ class XVThumbImageFile(ImageFile.ImageFile): s = self.fp.readline() if not s: raise SyntaxError("Unexpected EOF reading XV thumbnail file") - if s[0] != b'#': + if i8(s[0]) != 35: # ie. when not a comment: '#' break # parse header line (already read) s = s.strip().split() self.mode = "P" - self.size = int(s[0:1]), int(s[1:2]) + self.size = int(s[0]), int(s[1]) self.palette = ImagePalette.raw("RGB", PALETTE) @@ -75,6 +74,7 @@ class XVThumbImageFile(ImageFile.ImageFile): self.fp.tell(), (self.mode, 0, 1) )] + # -------------------------------------------------------------------- Image.register_open(XVThumbImageFile.format, XVThumbImageFile, _accept) diff --git a/PIL/XbmImagePlugin.py b/PIL/XbmImagePlugin.py index d0b0e47ab..b43fbef50 100644 --- a/PIL/XbmImagePlugin.py +++ b/PIL/XbmImagePlugin.py @@ -20,7 +20,7 @@ # import re -from PIL import Image, ImageFile +from . import Image, ImageFile __version__ = "0.6" diff --git a/PIL/XpmImagePlugin.py b/PIL/XpmImagePlugin.py index 556adb8f7..b6e69802d 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" @@ -116,8 +116,6 @@ class XpmImageFile(ImageFile.ImageFile): for i in range(ysize): s[i] = self.fp.readline()[1:xsize+1].ljust(xsize) - self.fp = None - return b"".join(s) # diff --git a/PIL/__init__.py b/PIL/__init__.py index be672e825..5603bb05f 100644 --- a/PIL/__init__.py +++ b/PIL/__init__.py @@ -11,8 +11,10 @@ # ;-) -VERSION = '1.1.7' # PIL version -PILLOW_VERSION = '3.5.0.dev0' # Pillow +from . import version + +VERSION = '1.1.7' # PIL Version +PILLOW_VERSION = version.__version__ __version__ = PILLOW_VERSION diff --git a/PIL/_binary.py b/PIL/_binary.py index 1cbe59dea..b15f796c0 100644 --- a/PIL/_binary.py +++ b/PIL/_binary.py @@ -28,10 +28,9 @@ 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 @@ -39,9 +38,19 @@ def i16le(c, o=0): return unpack("H", c[o:o+2])[0] diff --git a/PIL/features.py b/PIL/features.py index 134d85abf..60f4c10ca 100644 --- a/PIL/features.py +++ b/PIL/features.py @@ -1,46 +1,27 @@ -from PIL import Image +from . import Image modules = { "pil": "PIL._imaging", - "tkinter": "PIL._imagingtk", + "tkinter": "PIL._tkinter_finder", "freetype2": "PIL._imagingft", "littlecms2": "PIL._imagingcms", "webp": "PIL._webp", - "transp_webp": ("WEBP", "WebPDecoderBuggyAlpha") } - def check_module(feature): - if feature not in modules: + if not (feature in modules): raise ValueError("Unknown module %s" % feature) module = modules[feature] - method_to_call = None - if isinstance(module, tuple): - module, method_to_call = module - try: imported_module = __import__(module) - except ImportError: - # If a method is being checked, None means that - # rather than the method failing, the module required for the method - # failed to be imported first - return None if method_to_call else False - - if method_to_call: - method = getattr(imported_module, method_to_call) - return method() is True - else: return True - + except ImportError: + return False def get_supported_modules(): - supported_modules = [] - for feature in modules: - if check_module(feature): - supported_modules.append(feature) - return supported_modules + return [f for f in modules if check_module(f)] codecs = { "jpg": "jpeg", @@ -49,7 +30,6 @@ codecs = { "libtiff": "libtiff" } - def check_codec(feature): if feature not in codecs: raise ValueError("Unknown codec %s" % feature) @@ -60,8 +40,39 @@ def check_codec(feature): def get_supported_codecs(): - supported_codecs = [] - for feature in codecs: - if check_codec(feature): - supported_codecs.append(feature) - return supported_codecs + return [f for f in codecs if check_codec(f)] + +features = { + "webp_mux": ("PIL._webp", 'HAVE_WEBPMUX'), + "transp_webp": ("PIL._webp", "HAVE_TRANSPARENCY"), + "raqm": ("PIL._imagingft", "HAVE_RAQM") +} + +def check_feature(feature): + if feature not in features: + raise ValueError("Unknown feature %s" % feature) + + module, flag = features[feature] + + try: + imported_module = __import__(module, fromlist=['PIL']) + return getattr(imported_module, flag) + except ImportError: + return None + + +def get_supported_features(): + return [f for f in features if check_feature(f)] + + +def check(feature): + return (feature in modules and check_module(feature) or \ + feature in codecs and check_codec(feature) or \ + feature in features and check_feature(feature)) + +def get_supported(): + ret = get_supported_modules() + ret.extend(get_supported_features()) + ret.extend(get_supported_codecs()) + return ret + diff --git a/PIL/version.py b/PIL/version.py new file mode 100644 index 000000000..4d21d062b --- /dev/null +++ b/PIL/version.py @@ -0,0 +1,2 @@ +# Master version for Pillow +__version__ = '4.3.0.dev0' diff --git a/README.rst b/README.rst index 93e872ece..62cb4bb3f 100644 --- a/README.rst +++ b/README.rst @@ -14,9 +14,9 @@ Pillow is the friendly PIL fork by `Alex Clark and Contributors 2.9.0" - $ git push + $ git checkout [[release tag]] + $ cd .. + $ git commit -m "Pillow -> 2.9.0" Pillow + $ git push ``` -* [ ] Download distributions from the [Pillow macOS Wheel Builder container](http://cdf58691c5cf45771290-6a3b6a0f5f6ab91aadc447b2a897dd9a.r50.cf2.rackcdn.com/) and ``twine upload *``. +* [ ] Download distributions from the [Pillow Wheel Builder container](http://a365fff413fe338398b6-1c8a9b3114517dc5fe17b7c3f8c63a43.r19.cf2.rackcdn.com/). -### Linux ## Publicize Release diff --git a/Scripts/createfontdatachunk.py b/Scripts/createfontdatachunk.py old mode 100644 new mode 100755 diff --git a/Scripts/enhancer.py b/Scripts/enhancer.py old mode 100644 new mode 100755 index 4976e4409..ee333974f --- a/Scripts/enhancer.py +++ b/Scripts/enhancer.py @@ -7,21 +7,22 @@ # drag the slider to modify the image. # -try: - from tkinter import Tk, Toplevel, Frame, Label, Scale, HORIZONTAL -except ImportError: - from Tkinter import Tk, Toplevel, Frame, Label, Scale, HORIZONTAL +import sys + +if sys.version_info[0] > 2: + import tkinter +else: + import Tkinter as tkinter from PIL import Image, ImageTk, ImageEnhance -import sys # # enhancer widget -class Enhance(Frame): +class Enhance(tkinter.Frame): def __init__(self, master, image, name, enhancer, lo, hi): - Frame.__init__(self, master) + tkinter.Frame.__init__(self, master) # set up the image self.tkim = ImageTk.PhotoImage(image.mode, image.size) @@ -29,10 +30,10 @@ class Enhance(Frame): self.update("1.0") # normalize # image window - Label(self, image=self.tkim).pack() + tkinter.Label(self, image=self.tkim).pack() # scale - s = Scale(self, label=name, orient=HORIZONTAL, + s = tkinter.Scale(self, label=name, orient=tkinter.HORIZONTAL, from_=lo, to=hi, resolution=0.01, command=self.update) s.set(self.value) @@ -49,15 +50,15 @@ if len(sys.argv) != 2: print("Usage: enhancer file") sys.exit(1) -root = Tk() +root = tkinter.Tk() im = Image.open(sys.argv[1]) im.thumbnail((200, 200)) Enhance(root, im, "Color", ImageEnhance.Color, 0.0, 4.0).pack() -Enhance(Toplevel(), im, "Sharpness", ImageEnhance.Sharpness, -2.0, 2.0).pack() -Enhance(Toplevel(), im, "Brightness", ImageEnhance.Brightness, -1.0, 3.0).pack() -Enhance(Toplevel(), im, "Contrast", ImageEnhance.Contrast, -1.0, 3.0).pack() +Enhance(tkinter.Toplevel(), im, "Sharpness", ImageEnhance.Sharpness, -2.0, 2.0).pack() +Enhance(tkinter.Toplevel(), im, "Brightness", ImageEnhance.Brightness, -1.0, 3.0).pack() +Enhance(tkinter.Toplevel(), im, "Contrast", ImageEnhance.Contrast, -1.0, 3.0).pack() root.mainloop() diff --git a/Scripts/explode.py b/Scripts/explode.py old mode 100644 new mode 100755 diff --git a/Scripts/gifmaker.py b/Scripts/gifmaker.py old mode 100644 new mode 100755 diff --git a/Scripts/painter.py b/Scripts/painter.py old mode 100644 new mode 100755 index 79470e8e5..30d55c2b6 --- a/Scripts/painter.py +++ b/Scripts/painter.py @@ -9,21 +9,23 @@ # the image into a set of tiles. # -try: - from tkinter import Tk, Canvas, NW -except ImportError: - from Tkinter import Tk, Canvas, NW +import sys + +if sys.version_info[0] > 2: + import tkinter +else: + import Tkinter as tkinter from PIL import Image, ImageTk -import sys # # painter widget -class PaintCanvas(Canvas): +class PaintCanvas(tkinter.Canvas): def __init__(self, master, image): - Canvas.__init__(self, master, width=image.size[0], height=image.size[1]) + tkinter.Canvas.__init__(self, master, + width=image.size[0], height=image.size[1]) # fill the canvas self.tile = {} @@ -33,7 +35,7 @@ class PaintCanvas(Canvas): for y in range(0, ysize, tilesize): box = x, y, min(xsize, x+tilesize), min(ysize, y+tilesize) tile = ImageTk.PhotoImage(image.crop(box)) - self.create_image(x, y, image=tile, anchor=NW) + self.create_image(x, y, image=tile, anchor=tkinter.NW) self.tile[(x, y)] = box, tile self.image = image @@ -70,7 +72,7 @@ if len(sys.argv) != 2: print("Usage: painter file") sys.exit(1) -root = Tk() +root = tkinter.Tk() im = Image.open(sys.argv[1]) diff --git a/Scripts/pilconvert.py b/Scripts/pilconvert.py old mode 100644 new mode 100755 diff --git a/Scripts/pildriver.py b/Scripts/pildriver.py old mode 100644 new mode 100755 diff --git a/Scripts/pilfile.py b/Scripts/pilfile.py old mode 100644 new mode 100755 diff --git a/Scripts/pilfont.py b/Scripts/pilfont.py old mode 100644 new mode 100755 diff --git a/Scripts/player.py b/Scripts/player.py old mode 100644 new mode 100755 index ac9eb817f..dcf7d9307 --- a/Scripts/player.py +++ b/Scripts/player.py @@ -6,19 +6,20 @@ from __future__ import print_function -try: - from tkinter import * -except ImportError: - from Tkinter import * +import sys + +if sys.version_info[0] > 2: + import tkinter +else: + import Tkinter as tkinter from PIL import Image, ImageTk -import sys # -------------------------------------------------------------------- # an image animation player -class UI(Label): +class UI(tkinter.Label): def __init__(self, master, im): if isinstance(im, list): @@ -34,14 +35,11 @@ class UI(Label): else: self.image = ImageTk.PhotoImage(im) - Label.__init__(self, master, image=self.image, bg="black", bd=0) + tkinter.Label.__init__(self, master, image=self.image, bg="black", bd=0) self.update() - try: - duration = im.info["duration"] - except KeyError: - duration = 100 + duration = im.info.get("duration", 100) self.after(duration, self.next) def next(self): @@ -64,10 +62,7 @@ class UI(Label): except EOFError: return # end of file - try: - duration = im.info["duration"] - except KeyError: - duration = 100 + duration = im.info.get("duration", 100) self.after(duration, self.next) self.update_idletasks() @@ -84,7 +79,7 @@ if __name__ == "__main__": filename = sys.argv[1] - root = Tk() + root = tkinter.Tk() root.title(filename) if len(sys.argv) > 2: diff --git a/Scripts/thresholder.py b/Scripts/thresholder.py old mode 100644 new mode 100755 index c2e87d56a..c6c62ff19 --- a/Scripts/thresholder.py +++ b/Scripts/thresholder.py @@ -7,32 +7,34 @@ # as a dynamically updated overlay # -try: - from tkinter import * -except ImportError: - from Tkinter import * +import sys + +if sys.version_info[0] > 2: + import tkinter +else: + import Tkinter as tkinter from PIL import Image, ImageTk -import sys # # an image viewer -class UI(Frame): +class UI(tkinter.Frame): def __init__(self, master, im, value=128): - Frame.__init__(self, master) + tkinter.Frame.__init__(self, master) self.image = im self.value = value - self.canvas = Canvas(self, width=im.size[0], height=im.size[1]) + self.canvas = tkinter.Canvas(self, width=im.size[0], height=im.size[1]) self.backdrop = ImageTk.PhotoImage(im) - self.canvas.create_image(0, 0, image=self.backdrop, anchor=NW) + self.canvas.create_image(0, 0, image=self.backdrop, anchor=tkinter.NW) self.canvas.pack() - scale = Scale(self, orient=HORIZONTAL, from_=0, to=255, - resolution=1, command=self.update_scale, length=256) + scale = tkinter.Scale(self, orient=tkinter.HORIZONTAL, from_=0, to=255, + resolution=1, command=self.update_scale, + length=256) scale.set(value) scale.bind("", self.redraw) scale.pack() @@ -54,7 +56,7 @@ class UI(Frame): # update canvas self.canvas.delete("overlay") - self.canvas.create_image(0, 0, image=self.overlay, anchor=NW, + self.canvas.create_image(0, 0, image=self.overlay, anchor=tkinter.NW, tags="overlay") # -------------------------------------------------------------------- @@ -64,7 +66,7 @@ if len(sys.argv) != 2: print("Usage: thresholder file") sys.exit(1) -root = Tk() +root = tkinter.Tk() im = Image.open(sys.argv[1]) diff --git a/Scripts/viewer.py b/Scripts/viewer.py old mode 100644 new mode 100755 index f9bccec4f..c5b0b55ca --- a/Scripts/viewer.py +++ b/Scripts/viewer.py @@ -6,10 +6,12 @@ from __future__ import print_function -try: - from tkinter import Tk, Label -except ImportError: - from Tkinter import Tk, Label +import sys + +if sys.version_info[0] > 2: + import tkinter +else: + import Tkinter as tkinter from PIL import Image, ImageTk @@ -17,34 +19,33 @@ from PIL import Image, ImageTk # an image viewer -class UI(Label): +class UI(tkinter.Label): def __init__(self, master, im): if im.mode == "1": # bitmap image self.image = ImageTk.BitmapImage(im, foreground="white") - Label.__init__(self, master, image=self.image, bg="black", bd=0) + tkinter.Label.__init__(self, master, image=self.image, bd=0, + bg="black") else: # photo image self.image = ImageTk.PhotoImage(im) - Label.__init__(self, master, image=self.image, bd=0) + tkinter.Label.__init__(self, master, image=self.image, bd=0) # # script interface if __name__ == "__main__": - import sys - if not sys.argv[1:]: print("Syntax: python viewer.py imagefile") sys.exit(1) filename = sys.argv[1] - root = Tk() + root = tkinter.Tk() root.title(filename) im = Image.open(filename) diff --git a/Tests/32bit_segfault_check.py b/Tests/32bit_segfault_check.py old mode 100644 new mode 100755 diff --git a/Tests/check_imaging_leaks.py b/Tests/check_imaging_leaks.py old mode 100644 new mode 100755 diff --git a/Tests/fonts/LICENSE.txt b/Tests/fonts/LICENSE.txt new file mode 100644 index 000000000..223c2aecf --- /dev/null +++ b/Tests/fonts/LICENSE.txt @@ -0,0 +1,6 @@ + +NotoNastaliqUrdu-Regular.ttf: + +(from https://github.com/googlei18n/noto-fonts) + +All Noto fonts are published under the SIL Open Font License (OFL) v1.1 (http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=OFL), which allows you to copy, modify, and redistribute them if you need to. diff --git a/Tests/fonts/NotoNastaliqUrdu-Regular.ttf b/Tests/fonts/NotoNastaliqUrdu-Regular.ttf new file mode 100644 index 000000000..891f633d8 Binary files /dev/null and b/Tests/fonts/NotoNastaliqUrdu-Regular.ttf differ diff --git a/Tests/helper.py b/Tests/helper.py index 7b8bdcce4..65b1ca8bd 100644 --- a/Tests/helper.py +++ b/Tests/helper.py @@ -7,6 +7,21 @@ import tempfile import os 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): @@ -80,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, @@ -135,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) @@ -149,7 +167,6 @@ class PillowTestCase(unittest.TestCase): outfile = self.tempfile("temp.png") if command_succeeds([IMCONVERT, f, outfile]): - from PIL import Image return Image.open(outfile) raise IOError() @@ -161,7 +178,6 @@ py3 = (sys.version_info >= (3, 0)) def fromstring(data): from io import BytesIO - from PIL import Image return Image.open(BytesIO(data)) @@ -173,7 +189,6 @@ def tostring(im, string_format, **options): def hopper(mode=None, cache={}): - from PIL import Image if mode is None: # Always return fresh not-yet-loaded version of image. # Operations on not-yet-loaded images is separate class of errors @@ -200,25 +215,25 @@ def command_succeeds(cmd): command succeeds, or False if an OSError was raised by subprocess.Popen. """ import subprocess - with open(os.devnull, 'w') as f: + with open(os.devnull, 'wb') as f: try: - subprocess.Popen(cmd, stdout=f, stderr=subprocess.STDOUT).wait() + subprocess.call(cmd, stdout=f, stderr=subprocess.STDOUT) except OSError: return False return True def djpeg_available(): - return command_succeeds(['djpeg', '--help']) + return command_succeeds(['djpeg', '-version']) def cjpeg_available(): - return command_succeeds(['cjpeg', '--help']) + return command_succeeds(['cjpeg', '-version']) def netpbm_available(): - return (command_succeeds(["ppmquant", "--help"]) and - command_succeeds(["ppmtogif", "--help"])) + return (command_succeeds(["ppmquant", "--version"]) and + command_succeeds(["ppmtogif", "--version"])) def imagemagick_available(): @@ -236,6 +251,14 @@ else: IMCONVERT = 'convert' +def distro(): + if os.path.exists('/etc/os-release'): + with open('/etc/os-release', 'r') as f: + for line in f: + if 'ID=' in line: + return line.strip().split('=')[1] + + class cached_property(object): def __init__(self, func): self.func = func diff --git a/Tests/images/WAlaska.wind.7days.grb b/Tests/images/WAlaska.wind.7days.grb new file mode 100644 index 000000000..37f9fd9b5 Binary files /dev/null and b/Tests/images/WAlaska.wind.7days.grb differ diff --git a/Tests/images/cmx3g8_wv_1998.260_0745_mcidas.ara b/Tests/images/cmx3g8_wv_1998.260_0745_mcidas.ara new file mode 100644 index 000000000..4cdc741d7 Binary files /dev/null and b/Tests/images/cmx3g8_wv_1998.260_0745_mcidas.ara differ diff --git a/Tests/images/cmx3g8_wv_1998.260_0745_mcidas.png b/Tests/images/cmx3g8_wv_1998.260_0745_mcidas.png new file mode 100644 index 000000000..2b84283b7 Binary files /dev/null and b/Tests/images/cmx3g8_wv_1998.260_0745_mcidas.png 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/dummy.container b/Tests/images/dummy.container new file mode 100644 index 000000000..83e7a3560 Binary files /dev/null and b/Tests/images/dummy.container differ diff --git a/Tests/images/exif-200dpcm.jpg b/Tests/images/exif-200dpcm.jpg new file mode 100644 index 000000000..efa55613b Binary files /dev/null and b/Tests/images/exif-200dpcm.jpg differ diff --git a/Tests/images/exif-72dpi-int.jpg b/Tests/images/exif-72dpi-int.jpg new file mode 100644 index 000000000..0b60190d2 Binary files /dev/null and b/Tests/images/exif-72dpi-int.jpg differ diff --git a/Tests/images/gfs.t06z.rassda.tm00.bufr_d b/Tests/images/gfs.t06z.rassda.tm00.bufr_d new file mode 100644 index 000000000..4f895a157 Binary files /dev/null and b/Tests/images/gfs.t06z.rassda.tm00.bufr_d differ diff --git a/Tests/images/gif_header_data.pkl b/Tests/images/gif_header_data.pkl new file mode 100644 index 000000000..50f01aa0e Binary files /dev/null and b/Tests/images/gif_header_data.pkl differ diff --git a/Tests/images/hdf5.h5 b/Tests/images/hdf5.h5 new file mode 100644 index 000000000..977e9659c Binary files /dev/null and b/Tests/images/hdf5.h5 differ diff --git a/Tests/images/hopper-XYZ.png b/Tests/images/hopper-XYZ.png new file mode 100644 index 000000000..194d24540 Binary files /dev/null and b/Tests/images/hopper-XYZ.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.fits b/Tests/images/hopper.fits new file mode 100644 index 000000000..85afa4ac1 Binary files /dev/null and b/Tests/images/hopper.fits differ diff --git a/Tests/images/hopper.mic b/Tests/images/hopper.mic new file mode 100644 index 000000000..fe6792f29 Binary files /dev/null and b/Tests/images/hopper.mic 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.p7 b/Tests/images/hopper.p7 new file mode 100644 index 000000000..474b233d5 Binary files /dev/null and b/Tests/images/hopper.p7 differ diff --git a/Tests/images/hopper.psd b/Tests/images/hopper.psd index e8b184cf5..5ec686ca0 100644 Binary files a/Tests/images/hopper.psd and b/Tests/images/hopper.psd differ diff --git a/Tests/images/hopper.pxr b/Tests/images/hopper.pxr new file mode 100644 index 000000000..a7dee295a Binary files /dev/null and b/Tests/images/hopper.pxr 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/hopper_bad.p7 b/Tests/images/hopper_bad.p7 new file mode 100644 index 000000000..382929688 --- /dev/null +++ b/Tests/images/hopper_bad.p7 @@ -0,0 +1,2 @@ +P7 332 +# Artificially edited file to cause unexpected EOF diff --git a/Tests/images/hopper_bad_checksum.msp b/Tests/images/hopper_bad_checksum.msp new file mode 100644 index 000000000..248074a22 Binary files /dev/null and b/Tests/images/hopper_bad_checksum.msp differ diff --git a/Tests/images/icc_profile_big.jpg b/Tests/images/icc_profile_big.jpg new file mode 100644 index 000000000..adf98beaa Binary files /dev/null and b/Tests/images/icc_profile_big.jpg differ diff --git a/Tests/images/imagedraw_arc_end_le_start.png b/Tests/images/imagedraw_arc_end_le_start.png new file mode 100644 index 000000000..aee48e1c6 Binary files /dev/null and b/Tests/images/imagedraw_arc_end_le_start.png differ diff --git a/Tests/images/imagedraw_arc_no_loops.png b/Tests/images/imagedraw_arc_no_loops.png new file mode 100644 index 000000000..e45ad57a5 Binary files /dev/null and b/Tests/images/imagedraw_arc_no_loops.png differ diff --git a/Tests/images/imagedraw_big_rectangle.png b/Tests/images/imagedraw_big_rectangle.png new file mode 100644 index 000000000..fa2370b28 Binary files /dev/null and b/Tests/images/imagedraw_big_rectangle.png differ diff --git a/Tests/images/imagedraw_chord.png b/Tests/images/imagedraw_chord.png deleted file mode 100644 index db3b35310..000000000 Binary files a/Tests/images/imagedraw_chord.png and /dev/null differ diff --git a/Tests/images/imagedraw_chord_L.png b/Tests/images/imagedraw_chord_L.png new file mode 100644 index 000000000..a5a0078d0 Binary files /dev/null and b/Tests/images/imagedraw_chord_L.png differ diff --git a/Tests/images/imagedraw_chord_RGB.png b/Tests/images/imagedraw_chord_RGB.png new file mode 100644 index 000000000..af6fc7660 Binary files /dev/null and b/Tests/images/imagedraw_chord_RGB.png differ diff --git a/Tests/images/imagedraw_ellipse_L.png b/Tests/images/imagedraw_ellipse_L.png new file mode 100644 index 000000000..e47e6e441 Binary files /dev/null and b/Tests/images/imagedraw_ellipse_L.png differ diff --git a/Tests/images/imagedraw_ellipse.png b/Tests/images/imagedraw_ellipse_RGB.png similarity index 100% rename from Tests/images/imagedraw_ellipse.png rename to Tests/images/imagedraw_ellipse_RGB.png diff --git a/Tests/images/imagedraw_polygon_kite_L.png b/Tests/images/imagedraw_polygon_kite_L.png new file mode 100644 index 000000000..241d86bf4 Binary files /dev/null and b/Tests/images/imagedraw_polygon_kite_L.png differ diff --git a/Tests/images/imagedraw_polygon_kite_RGB.png b/Tests/images/imagedraw_polygon_kite_RGB.png new file mode 100644 index 000000000..e48d6660f Binary files /dev/null and b/Tests/images/imagedraw_polygon_kite_RGB.png differ diff --git a/Tests/images/imagedraw_shape1.png b/Tests/images/imagedraw_shape1.png new file mode 100644 index 000000000..0e9f3b412 Binary files /dev/null and b/Tests/images/imagedraw_shape1.png differ diff --git a/Tests/images/imagedraw_shape2.png b/Tests/images/imagedraw_shape2.png new file mode 100644 index 000000000..daf030313 Binary files /dev/null and b/Tests/images/imagedraw_shape2.png differ diff --git a/Tests/images/imagedraw_wide_line_dot.png b/Tests/images/imagedraw_wide_line_dot.png new file mode 100644 index 000000000..d6f0e789c Binary files /dev/null and b/Tests/images/imagedraw_wide_line_dot.png differ diff --git a/Tests/images/invalid-exif.jpg b/Tests/images/invalid-exif.jpg new file mode 100644 index 000000000..948b8e055 Binary files /dev/null and b/Tests/images/invalid-exif.jpg differ diff --git a/Tests/images/linear_gradient.png b/Tests/images/linear_gradient.png new file mode 100644 index 000000000..7b02227ce Binary files /dev/null and b/Tests/images/linear_gradient.png differ diff --git a/Tests/images/no-dpi-in-exif.jpg b/Tests/images/no-dpi-in-exif.jpg new file mode 100644 index 000000000..9a4731a5b Binary files /dev/null and b/Tests/images/no-dpi-in-exif.jpg differ diff --git a/Tests/images/photoshop-200dpi.jpg b/Tests/images/photoshop-200dpi.jpg new file mode 100644 index 000000000..72455a147 Binary files /dev/null and b/Tests/images/photoshop-200dpi.jpg differ diff --git a/Tests/images/radial_gradient.png b/Tests/images/radial_gradient.png new file mode 100644 index 000000000..95309d7c0 Binary files /dev/null and b/Tests/images/radial_gradient.png differ diff --git a/Tests/images/test_Nastalifont_text.png b/Tests/images/test_Nastalifont_text.png new file mode 100644 index 000000000..51d56a0de Binary files /dev/null and b/Tests/images/test_Nastalifont_text.png differ diff --git a/Tests/images/test_arabictext_features.png b/Tests/images/test_arabictext_features.png new file mode 100644 index 000000000..9bfa5a931 Binary files /dev/null and b/Tests/images/test_arabictext_features.png differ diff --git a/Tests/images/test_complex_unicode_text.png b/Tests/images/test_complex_unicode_text.png new file mode 100644 index 000000000..f1a6f7ec6 Binary files /dev/null and b/Tests/images/test_complex_unicode_text.png differ diff --git a/Tests/images/test_direction_ltr.png b/Tests/images/test_direction_ltr.png new file mode 100644 index 000000000..42239334d Binary files /dev/null and b/Tests/images/test_direction_ltr.png differ diff --git a/Tests/images/test_direction_rtl.png b/Tests/images/test_direction_rtl.png new file mode 100644 index 000000000..966b67d6b Binary files /dev/null and b/Tests/images/test_direction_rtl.png differ diff --git a/Tests/images/test_kerning_features.png b/Tests/images/test_kerning_features.png new file mode 100644 index 000000000..ca895735c Binary files /dev/null and b/Tests/images/test_kerning_features.png differ diff --git a/Tests/images/test_ligature_features.png b/Tests/images/test_ligature_features.png new file mode 100644 index 000000000..664e9929d Binary files /dev/null and b/Tests/images/test_ligature_features.png differ diff --git a/Tests/images/test_text.png b/Tests/images/test_text.png new file mode 100644 index 000000000..c156399cd Binary files /dev/null and b/Tests/images/test_text.png differ diff --git a/Tests/images/test_y_offset.png b/Tests/images/test_y_offset.png new file mode 100644 index 000000000..5a166be8c Binary files /dev/null and b/Tests/images/test_y_offset.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 0f98d8374..8e84cc8f1 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,7 +22,7 @@ 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 @@ -46,7 +47,7 @@ class TestBmpReference(PillowTestCase): except Exception: # as msg: if os.path.basename(f) in supported: raise - # print ("Bad Image %s: %s" %(f,msg)) + # print("Bad Image %s: %s" %(f,msg)) def test_good(self): """ These should all work. There's a set of target files in the @@ -98,8 +99,7 @@ class TestBmpReference(PillowTestCase): os.path.join(base, 'g', 'pal8rle.bmp'), os.path.join(base, 'g', 'pal4rle.bmp')) if f not in unsupported: - self.assertTrue( - False, "Unsupported Image %s: %s" % (f, msg)) + self.fail("Unsupported Image %s: %s" % (f, msg)) if __name__ == '__main__': diff --git a/Tests/test_decompression_bomb.py b/Tests/test_decompression_bomb.py index e952f6586..5598fd9c8 100644 --- a/Tests/test_decompression_bomb.py +++ b/Tests/test_decompression_bomb.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase +from helper import unittest, PillowTestCase, hopper from PIL import Image @@ -21,7 +21,7 @@ class TestDecompressionBomb(PillowTestCase): # Arrange # Turn limit off Image.MAX_IMAGE_PIXELS = None - self.assertEqual(Image.MAX_IMAGE_PIXELS, None) + self.assertIsNone(Image.MAX_IMAGE_PIXELS) # Act / Assert # Implicit assert: no warning. @@ -35,9 +35,24 @@ class TestDecompressionBomb(PillowTestCase): self.assertEqual(Image.MAX_IMAGE_PIXELS, 10) # Act / Assert - self.assert_warning( - Image.DecompressionBombWarning, - lambda: Image.open(TEST_FILE)) + self.assert_warning(Image.DecompressionBombWarning, + lambda: Image.open(TEST_FILE)) + +class TestDecompressionCrop(PillowTestCase): + + def setUp(self): + self.src = hopper() + Image.MAX_IMAGE_PIXELS = self.src.height * self.src.width + + def tearDown(self): + Image.MAX_IMAGE_PIXELS = ORIGINAL_LIMIT + + def testEnlargeCrop(self): + # Crops can extend the extents, therefore we should have the + # same decompression bomb warnings on them. + box = (0, 0, self.src.width * 2, self.src.height * 2) + self.assert_warning(Image.DecompressionBombWarning, + lambda: self.src.crop(box)) if __name__ == '__main__': unittest.main() diff --git a/Tests/test_features.py b/Tests/test_features.py index b9afe9b1d..d14aaca69 100644 --- a/Tests/test_features.py +++ b/Tests/test_features.py @@ -2,19 +2,50 @@ from helper import unittest, PillowTestCase from PIL import features +try: + from PIL import _webp + HAVE_WEBP = True +except: + HAVE_WEBP = False + class TestFeatures(PillowTestCase): - def test_check_features(self): - for feature in features.modules: - self.assertTrue( - features.check_module(feature) in [True, False, None]) - for feature in features.codecs: - self.assertTrue(features.check_codec(feature) in [True, False]) + def test_check(self): + # Check the correctness of the convenience function + for module in features.modules: + self.assertEqual(features.check_module(module), + features.check(module)) + for codec in features.codecs: + self.assertEqual(features.check_codec(codec), + features.check(codec)) + for feature in features.features: + self.assertEqual(features.check_feature(feature), + features.check(feature)) - def test_supported_features(self): + @unittest.skipUnless(HAVE_WEBP, True) + def check_webp_transparency(self): + self.assertEqual(features.check('transp_webp'), + not _webp.WebPDecoderBuggyAlpha()) + self.assertEqual(features.check('transp_webp'), + _webp.HAVE_TRANSPARENCY) + + @unittest.skipUnless(HAVE_WEBP, True) + def check_webp_mux(self): + self.assertEqual(features.check('webp_mux'), + _webp.HAVE_WEBPMUX) + + def test_check_modules(self): + for feature in features.modules: + self.assertIn(features.check_module(feature), [True, False]) + for feature in features.codecs: + self.assertIn(features.check_codec(feature), [True, False]) + + def test_supported_modules(self): self.assertIsInstance(features.get_supported_modules(), list) self.assertIsInstance(features.get_supported_codecs(), list) + self.assertIsInstance(features.get_supported_features(), list) + self.assertIsInstance(features.get_supported(), list) def test_unsupported_codec(self): # Arrange diff --git a/Tests/test_file_bufrstub.py b/Tests/test_file_bufrstub.py index 6872ca339..4932930ff 100644 --- a/Tests/test_file_bufrstub.py +++ b/Tests/test_file_bufrstub.py @@ -1,17 +1,47 @@ -from helper import unittest, PillowTestCase +from helper import unittest, PillowTestCase, hopper -from PIL import BufrStubImagePlugin +from PIL import BufrStubImagePlugin, Image + +TEST_FILE = "Tests/images/gfs.t06z.rassda.tm00.bufr_d" class TestFileBufrStub(PillowTestCase): + def test_open(self): + # Act + im = Image.open(TEST_FILE) + + # Assert + self.assertEqual(im.format, "BUFR") + + # Dummy data from the stub + self.assertEqual(im.mode, "F") + self.assertEqual(im.size, (1, 1)) + def test_invalid_file(self): + # Arrange invalid_file = "Tests/images/flower.jpg" + # Act / Assert self.assertRaises(SyntaxError, lambda: BufrStubImagePlugin.BufrStubImageFile(invalid_file)) + def test_load(self): + # Arrange + im = Image.open(TEST_FILE) + + # Act / Assert: stub cannot load without an implemented handler + self.assertRaises(IOError, im.load) + + def test_save(self): + # Arrange + im = hopper() + tmpfile = self.tempfile("temp.bufr") + + # Act / Assert: stub cannot save without an implemented handler + self.assertRaises(IOError, lambda: im.save(tmpfile)) + if __name__ == '__main__': unittest.main() diff --git a/Tests/test_file_container.py b/Tests/test_file_container.py new file mode 100644 index 000000000..55228be0c --- /dev/null +++ b/Tests/test_file_container.py @@ -0,0 +1,129 @@ +from helper import unittest, PillowTestCase, hopper + +from PIL import Image +from PIL import ContainerIO + +TEST_FILE = "Tests/images/dummy.container" + + +class TestFileContainer(PillowTestCase): + + def test_sanity(self): + dir(Image) + dir(ContainerIO) + + def test_isatty(self): + im = hopper() + container = ContainerIO.ContainerIO(im, 0, 0) + + self.assertEqual(container.isatty(), 0) + + def test_seek_mode_0(self): + # Arrange + mode = 0 + with open(TEST_FILE) as fh: + container = ContainerIO.ContainerIO(fh, 22, 100) + + # Act + container.seek(33, mode) + container.seek(33, mode) + + # Assert + self.assertEqual(container.tell(), 33) + + def test_seek_mode_1(self): + # Arrange + mode = 1 + with open(TEST_FILE) as fh: + container = ContainerIO.ContainerIO(fh, 22, 100) + + # Act + container.seek(33, mode) + container.seek(33, mode) + + # Assert + self.assertEqual(container.tell(), 66) + + def test_seek_mode_2(self): + # Arrange + mode = 2 + with open(TEST_FILE) as fh: + container = ContainerIO.ContainerIO(fh, 22, 100) + + # Act + container.seek(33, mode) + container.seek(33, mode) + + # Assert + self.assertEqual(container.tell(), 100) + + def test_read_n0(self): + # Arrange + with open(TEST_FILE) as fh: + container = ContainerIO.ContainerIO(fh, 22, 100) + + # Act + container.seek(81) + data = container.read() + + # Assert + self.assertEqual(data, "7\nThis is line 8\n") + + def test_read_n(self): + # Arrange + with open(TEST_FILE) as fh: + container = ContainerIO.ContainerIO(fh, 22, 100) + + # Act + container.seek(81) + data = container.read(3) + + # Assert + self.assertEqual(data, "7\nT") + + def test_read_eof(self): + # Arrange + with open(TEST_FILE) as fh: + container = ContainerIO.ContainerIO(fh, 22, 100) + + # Act + container.seek(100) + data = container.read() + + # Assert + self.assertEqual(data, "") + + def test_readline(self): + # Arrange + with open(TEST_FILE) as fh: + container = ContainerIO.ContainerIO(fh, 0, 120) + + # Act + data = container.readline() + + # Assert + self.assertEqual(data, "This is line 1\n") + + def test_readlines(self): + # Arrange + expected = ["This is line 1\n", + "This is line 2\n", + "This is line 3\n", + "This is line 4\n", + "This is line 5\n", + "This is line 6\n", + "This is line 7\n", + "This is line 8\n"] + with open(TEST_FILE) as fh: + container = ContainerIO.ContainerIO(fh, 0, 120) + + # Act + data = container.readlines() + + # Assert + + self.assertEqual(data, expected) + + +if __name__ == '__main__': + unittest.main() diff --git a/Tests/test_file_cur.py b/Tests/test_file_cur.py index 5ae0e7eff..e6d13ef30 100644 --- a/Tests/test_file_cur.py +++ b/Tests/test_file_cur.py @@ -26,8 +26,8 @@ class TestFileCur(PillowTestCase): no_cursors_file = "Tests/images/no_cursors.cur" cur = CurImagePlugin.CurImageFile(TEST_FILE) - cur.fp = open(no_cursors_file, "rb") - self.assertRaises(TypeError, cur._open) + with open(no_cursors_file, "rb") as cur.fp: + self.assertRaises(TypeError, cur._open) if __name__ == '__main__': diff --git a/Tests/test_file_dcx.py b/Tests/test_file_dcx.py index 09da3c439..3da146d01 100644 --- a/Tests/test_file_dcx.py +++ b/Tests/test_file_dcx.py @@ -50,7 +50,7 @@ class TestFileDcx(PillowTestCase): im.seek(n_frames) break except EOFError: - self.assertTrue(im.tell() < n_frames) + self.assertLess(im.tell(), n_frames) def test_seek_too_far(self): # Arrange diff --git a/Tests/test_file_eps.py b/Tests/test_file_eps.py index c16d2669c..eaf4df899 100644 --- a/Tests/test_file_eps.py +++ b/Tests/test_file_eps.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase +from helper import unittest, PillowTestCase, hopper from PIL import Image, EpsImagePlugin import io @@ -94,6 +94,11 @@ class TestFileEps(PillowTestCase): image1_scale1_compare.load() self.assert_image_similar(img, image1_scale1_compare, 5) + def test_image_mode_not_supported(self): + im = hopper("RGBA") + tmpfile = self.tempfile('temp.eps') + self.assertRaises(ValueError, lambda: im.save(tmpfile)) + def test_render_scale1(self): # We need png support for these render test codecs = dir(Image.core) diff --git a/Tests/test_file_fitsstub.py b/Tests/test_file_fitsstub.py index 89e10acf1..f875a2ce3 100644 --- a/Tests/test_file_fitsstub.py +++ b/Tests/test_file_fitsstub.py @@ -1,17 +1,51 @@ from helper import unittest, PillowTestCase -from PIL import FitsStubImagePlugin +from PIL import FitsStubImagePlugin, Image + +TEST_FILE = "Tests/images/hopper.fits" class TestFileFitsStub(PillowTestCase): + def test_open(self): + # Act + im = Image.open(TEST_FILE) + + # Assert + self.assertEqual(im.format, "FITS") + + # Dummy data from the stub + self.assertEqual(im.mode, "F") + self.assertEqual(im.size, (1, 1)) + def test_invalid_file(self): + # Arrange invalid_file = "Tests/images/flower.jpg" + # Act / Assert self.assertRaises(SyntaxError, lambda: FitsStubImagePlugin.FITSStubImageFile(invalid_file)) + def test_load(self): + # Arrange + im = Image.open(TEST_FILE) + + # Act / Assert: stub cannot load without an implemented handler + self.assertRaises(IOError, im.load) + + def test_save(self): + # Arrange + im = Image.open(TEST_FILE) + dummy_fp = None + dummy_filename = "dummy.filename" + + # Act / Assert: stub cannot save without an implemented handler + self.assertRaises(IOError, lambda: im.save(dummy_filename)) + self.assertRaises( + IOError, + lambda: FitsStubImagePlugin._save(im, dummy_fp, dummy_filename)) + if __name__ == '__main__': unittest.main() diff --git a/Tests/test_file_fli.py b/Tests/test_file_fli.py index a49301de1..0530f6146 100644 --- a/Tests/test_file_fli.py +++ b/Tests/test_file_fli.py @@ -2,7 +2,6 @@ from helper import unittest, PillowTestCase from PIL import Image, FliImagePlugin -# sample ppm stream # created as an export of a palette image from Gimp2.6 # save as...-> hopper.fli, default options. test_file = "Tests/images/hopper.fli" @@ -38,7 +37,7 @@ class TestFileFli(PillowTestCase): im.seek(n_frames) break except EOFError: - self.assertTrue(im.tell() < n_frames) + self.assertLess(im.tell(), n_frames) if __name__ == '__main__': diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index 1672a14f0..bb2aa8a0d 100644 --- a/Tests/test_file_gif.py +++ b/Tests/test_file_gif.py @@ -1,7 +1,6 @@ from helper import unittest, PillowTestCase, hopper, netpbm_available -from PIL import Image -from PIL import GifImagePlugin +from PIL import Image, ImagePalette, GifImagePlugin from io import BytesIO @@ -56,23 +55,22 @@ 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(range(256-colors,256))*colors)) - im = im.resize((size,size)) + im = Image.frombytes('P', (colors, colors), + bytes(bytearray(range(256-colors, 256))*colors)) + im = im.resize((size, size)) outfile = BytesIO() im.save(outfile, 'GIF') outfile.seek(0) reloaded = Image.open(outfile) # check palette length - palette_length = max(i+1 for i,v in enumerate(reloaded.histogram()) if v) + 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')) + self.assert_image_equal(im.convert('RGB'), reloaded.convert('RGB')) # These do optimize the palette check(128, 511, 128) @@ -90,8 +88,6 @@ class TestFileGif(PillowTestCase): check(256, 511, 256) def test_optimize_full_l(self): - from io import BytesIO - im = Image.frombytes("L", (16, 16), bytes(bytearray(range(256)))) test_file = BytesIO() im.save(test_file, "GIF", optimize=True) @@ -230,7 +226,7 @@ class TestFileGif(PillowTestCase): im.seek(n_frames) break except EOFError: - self.assertTrue(im.tell() < n_frames) + self.assertLess(im.tell(), n_frames) def test_dispose_none(self): img = Image.open("Tests/images/dispose_none.gif") @@ -271,26 +267,86 @@ 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() + im.save(out, duration=duration) 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_identical_frames(self): + duration_list = [1000, 1500, 2000, 4000] + + out = self.tempfile('temp.gif') + im_list = [ + Image.new('L', (100, 100), '#000'), + Image.new('L', (100, 100), '#000'), + Image.new('L', (100, 100), '#000'), + Image.new('L', (100, 100), '#111') + ] + + # duration as list + im_list[0].save( + out, + save_all=True, + append_images=im_list[1:], + duration=duration_list + ) + reread = Image.open(out) + + # Assert that the first three frames were combined + self.assertEqual(reread.n_frames, 2) + + # Assert that the new duration is the total of the identical frames + self.assertEqual(reread.info['duration'], 4500) + 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() + im.save(out, loop=number_of_loops) reread = Image.open(out) self.assertEqual(reread.info['loop'], number_of_loops) @@ -362,5 +418,118 @@ 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) + + def test_bbox(self): + out = self.tempfile('temp.gif') + + im = Image.new('RGB', (100, 100), '#fff') + ims = [Image.new("RGB", (100, 100), '#000')] + im.save(out, save_all=True, append_images=ims) + + reread = Image.open(out) + self.assertEqual(reread.n_frames, 2) + + def test_palette_save_L(self): + # generate an L mode image with a separate palette + + im = hopper('P') + im_l = Image.frombytes('L', im.size, im.tobytes()) + palette = bytes(bytearray(im.getpalette())) + + out = self.tempfile('temp.gif') + im_l.save(out, palette=palette) + + reloaded = Image.open(out) + + self.assert_image_equal(reloaded.convert('RGB'), im.convert('RGB')) + + def test_palette_save_P(self): + # pass in a different palette, then construct what the image + # would look like. + # Forcing a non-straight grayscale palette. + + im = hopper('P') + palette = bytes(bytearray([255-i//3 for i in range(768)])) + + out = self.tempfile('temp.gif') + im.save(out, palette=palette) + + reloaded = Image.open(out) + im.putpalette(palette) + self.assert_image_equal(reloaded, im) + + def test_palette_save_ImagePalette(self): + # pass in a different palette, as an ImagePalette.ImagePalette + # effectively the same as test_palette_save_P + + im = hopper('P') + palette = ImagePalette.ImagePalette('RGB', list(range(256))[::-1]*3) + + out = self.tempfile('temp.gif') + im.save(out, palette=palette) + + reloaded = Image.open(out) + im.putpalette(palette) + self.assert_image_equal(reloaded, im) + + def test_save_I(self): + # Test saving something that would trigger the auto-convert to 'L' + + im = hopper('I') + + out = self.tempfile('temp.gif') + im.save(out) + + reloaded = Image.open(out) + self.assert_image_equal(reloaded.convert('L'), im.convert('L')) + + def test_getdata(self): + # test getheader/getdata against legacy values + # Create a 'P' image with holes in the palette + im = Image._wedge().resize((16, 16)) + im.putpalette(ImagePalette.ImagePalette('RGB')) + im.info = {'background': 0} + + passed_palette = bytes(bytearray([255-i//3 for i in range(768)])) + + GifImagePlugin._FORCE_OPTIMIZE = True + try: + h = GifImagePlugin.getheader(im, passed_palette) + d = GifImagePlugin.getdata(im) + + import pickle + # Enable to get target values on pre-refactor version + # with open('Tests/images/gif_header_data.pkl', 'wb') as f: + # pickle.dump((h, d), f, 1) + with open('Tests/images/gif_header_data.pkl', 'rb') as f: + (h_target, d_target) = pickle.load(f) + + self.assertEqual(h, h_target) + self.assertEqual(d, d_target) + finally: + GifImagePlugin._FORCE_OPTIMIZE = False + + if __name__ == '__main__': unittest.main() diff --git a/Tests/test_file_gribstub.py b/Tests/test_file_gribstub.py index 786ce42dd..dc0fa22d7 100644 --- a/Tests/test_file_gribstub.py +++ b/Tests/test_file_gribstub.py @@ -1,17 +1,47 @@ -from helper import unittest, PillowTestCase +from helper import unittest, PillowTestCase, hopper -from PIL import GribStubImagePlugin +from PIL import GribStubImagePlugin, Image + +TEST_FILE = "Tests/images/WAlaska.wind.7days.grb" class TestFileGribStub(PillowTestCase): + def test_open(self): + # Act + im = Image.open(TEST_FILE) + + # Assert + self.assertEqual(im.format, "GRIB") + + # Dummy data from the stub + self.assertEqual(im.mode, "F") + self.assertEqual(im.size, (1, 1)) + def test_invalid_file(self): + # Arrange invalid_file = "Tests/images/flower.jpg" + # Act / Assert self.assertRaises(SyntaxError, lambda: GribStubImagePlugin.GribStubImageFile(invalid_file)) + def test_load(self): + # Arrange + im = Image.open(TEST_FILE) + + # Act / Assert: stub cannot load without an implemented handler + self.assertRaises(IOError, im.load) + + def test_save(self): + # Arrange + im = hopper() + tmpfile = self.tempfile("temp.grib") + + # Act / Assert: stub cannot save without an implemented handler + self.assertRaises(IOError, lambda: im.save(tmpfile)) + if __name__ == '__main__': unittest.main() diff --git a/Tests/test_file_hdf5stub.py b/Tests/test_file_hdf5stub.py index 4545c8212..cc2b80aa2 100644 --- a/Tests/test_file_hdf5stub.py +++ b/Tests/test_file_hdf5stub.py @@ -1,16 +1,50 @@ from helper import unittest, PillowTestCase -from PIL import Hdf5StubImagePlugin +from PIL import Hdf5StubImagePlugin, Image + +TEST_FILE = "Tests/images/hdf5.h5" class TestFileHdf5Stub(PillowTestCase): - def test_invalid_file(self): - test_file = "Tests/images/flower.jpg" + def test_open(self): + # Act + im = Image.open(TEST_FILE) + # Assert + self.assertEqual(im.format, "HDF5") + + # Dummy data from the stub + self.assertEqual(im.mode, "F") + self.assertEqual(im.size, (1, 1)) + + def test_invalid_file(self): + # Arrange + invalid_file = "Tests/images/flower.jpg" + + # Act / Assert self.assertRaises(SyntaxError, lambda: - Hdf5StubImagePlugin.HDF5StubImageFile(test_file)) + Hdf5StubImagePlugin.HDF5StubImageFile(invalid_file)) + + def test_load(self): + # Arrange + im = Image.open(TEST_FILE) + + # Act / Assert: stub cannot load without an implemented handler + self.assertRaises(IOError, im.load) + + def test_save(self): + # Arrange + im = Image.open(TEST_FILE) + dummy_fp = None + dummy_filename = "dummy.filename" + + # Act / Assert: stub cannot save without an implemented handler + self.assertRaises(IOError, lambda: im.save(dummy_filename)) + self.assertRaises( + IOError, + lambda: Hdf5StubImagePlugin._save(im, dummy_fp, dummy_filename)) if __name__ == '__main__': diff --git a/Tests/test_file_ico.py b/Tests/test_file_ico.py index 94b8c44e3..4eb9435bd 100644 --- a/Tests/test_file_ico.py +++ b/Tests/test_file_ico.py @@ -3,7 +3,6 @@ from helper import unittest, PillowTestCase, hopper import io from PIL import Image, IcoImagePlugin -# sample ppm stream TEST_ICO_FILE = "Tests/images/hopper.ico" @@ -48,6 +47,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_im.py b/Tests/test_file_im.py index 94d8bcce6..fcf4f52f4 100644 --- a/Tests/test_file_im.py +++ b/Tests/test_file_im.py @@ -30,7 +30,7 @@ class TestFileIm(PillowTestCase): im.seek(n_frames) break except EOFError: - self.assertTrue(im.tell() < n_frames) + self.assertLess(im.tell(), n_frames) def test_roundtrip(self): out = self.tempfile('temp.im') diff --git a/Tests/test_file_iptc.py b/Tests/test_file_iptc.py index a29e1a4a6..e08d994a2 100644 --- a/Tests/test_file_iptc.py +++ b/Tests/test_file_iptc.py @@ -29,6 +29,16 @@ class TestFileIptc(PillowTestCase): self.assertEqual(iptc[(2, 90)], b"Budapest") self.assertEqual(iptc[(2, 101)], b"Hungary") + def test_getiptcinfo_tiff_none(self): + # Arrange + im = Image.open("Tests/images/hopper.tif") + + # Act + iptc = IptcImagePlugin.getiptcinfo(im) + + # Assert + self.assertIsNone(iptc) + def test_i(self): # Arrange c = b"a" diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index 1b34b42c6..7487263d7 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -1,9 +1,9 @@ -from helper import unittest, PillowTestCase, hopper, py3 +from helper import unittest, PillowTestCase, hopper from helper import djpeg_available, cjpeg_available -import random from io import BytesIO import os +import sys from PIL import Image from PIL import ImageFile @@ -29,6 +29,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 @@ -84,7 +93,7 @@ class TestFileJpeg(PillowTestCase): self.assertEqual(test(72), (72, 72)) self.assertEqual(test(300), (300, 300)) self.assertEqual(test(100, 200), (100, 200)) - self.assertEqual(test(0), None) # square pixels + self.assertIsNone(test(0)) # square pixels def test_icc(self): # Test ICC support @@ -124,6 +133,19 @@ class TestFileJpeg(PillowTestCase): test(ImageFile.MAXBLOCK+1) # full buffer block plus one byte test(ImageFile.MAXBLOCK*4+3) # large block + def test_large_icc_meta(self): + # https://github.com/python-pillow/Pillow/issues/148 + # Sometimes the meta data on the icc_profile block is bigger than + # Image.MAXBLOCK or the image size. + im = Image.open('Tests/images/icc_profile_big.jpg') + f = self.tempfile("temp.jpg") + icc_profile = im.info["icc_profile"] + try: + im.save(f, format='JPEG', progressive=True,quality=95, + icc_profile=icc_profile, optimize=True) + except IOError: + self.fail("Failed saving image with icc larger than image size") + def test_optimize(self): im1 = self.roundtrip(hopper()) im2 = self.roundtrip(hopper(), optimize=0) @@ -159,15 +181,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') @@ -429,21 +452,14 @@ class TestFileJpeg(PillowTestCase): def test_no_duplicate_0x1001_tag(self): # Arrange from PIL import ExifTags - tag_ids = dict(zip(ExifTags.TAGS.values(), ExifTags.TAGS.keys())) + tag_ids = {v: k for k, v in ExifTags.TAGS.items()} # Assert self.assertEqual(tag_ids['RelatedImageWidth'], 0x1001) self.assertEqual(tag_ids['RelatedImageLength'], 0x1002) def test_MAXBLOCK_scaling(self): - def gen_random_image(size): - """ Generates a very hard to compress file - :param size: tuple - """ - return Image.frombytes('RGB', - size, os.urandom(size[0]*size[1] * 3)) - - im = gen_random_image((512, 512)) + im = self.gen_random_image((512, 512)) f = self.tempfile("temp.jpeg") im.save(f, quality=100, optimize=True) @@ -473,17 +489,11 @@ class TestFileJpeg(PillowTestCase): img.save(out, "JPEG") def test_save_wrong_modes(self): - out = BytesIO() - for mode in ['LA', 'La', 'RGBa', 'P']: - img = Image.new(mode, (20, 20)) - self.assertRaises(IOError, img.save, out, "JPEG") - - def test_save_modes_with_warnings(self): # ref https://github.com/python-pillow/Pillow/issues/2005 out = BytesIO() - for mode in ['RGBA']: + for mode in ['LA', 'La', 'RGBA', 'RGBa', 'P']: img = Image.new(mode, (20, 20)) - self.assert_warning(DeprecationWarning, img.save, out, "JPEG") + self.assertRaises(IOError, img.save, out, "JPEG") def test_save_tiff_with_dpi(self): # Arrange @@ -498,6 +508,76 @@ class TestFileJpeg(PillowTestCase): reloaded.load() self.assertEqual(im.info['dpi'], reloaded.info['dpi']) + def test_dpi_tuple_from_exif(self): + # Arrange + # This Photoshop CC 2017 image has DPI in EXIF not metadata + # EXIF XResolution is (2000000, 10000) + im = Image.open("Tests/images/photoshop-200dpi.jpg") + + # Act / Assert + self.assertEqual(im.info.get("dpi"), (200, 200)) + + def test_dpi_int_from_exif(self): + # Arrange + # This image has DPI in EXIF not metadata + # EXIF XResolution is 72 + im = Image.open("Tests/images/exif-72dpi-int.jpg") + + # Act / Assert + self.assertEqual(im.info.get("dpi"), (72, 72)) + + def test_dpi_from_dpcm_exif(self): + # Arrange + # This is photoshop-200dpi.jpg with EXIF resolution unit set to cm: + # exiftool -exif:ResolutionUnit=cm photoshop-200dpi.jpg + im = Image.open("Tests/images/exif-200dpcm.jpg") + + # Act / Assert + self.assertEqual(im.info.get("dpi"), (508, 508)) + + def test_no_dpi_in_exif(self): + # Arrange + # This is photoshop-200dpi.jpg with resolution removed from EXIF: + # exiftool "-*resolution*"= photoshop-200dpi.jpg + im = Image.open("Tests/images/no-dpi-in-exif.jpg") + + # Act / Assert + # "When the image resolution is unknown, 72 [dpi] is designated." + # http://www.exiv2.org/tags.html + self.assertEqual(im.info.get("dpi"), (72, 72)) + + def test_invalid_exif(self): + # This is no-dpi-in-exif with the tiff header of the exif block + # hexedited from MM * to FF FF FF FF + im = Image.open("Tests/images/invalid-exif.jpg") + + # This should return the default, and not a SyntaxError or + # OSError for unidentified image. + self.assertEqual(im.info.get("dpi"), (72, 72)) + + +@unittest.skipUnless(sys.platform.startswith('win32'), "Windows only") +class TestFileCloseW32(PillowTestCase): + def setUp(self): + if "jpeg_encoder" not in codecs or "jpeg_decoder" not in codecs: + self.skipTest("jpeg support not available") + + def test_fd_leak(self): + tmpfile = self.tempfile("temp.jpg") + import os + + with Image.open("Tests/images/hopper.jpg") as im: + im.save(tmpfile) + + im = Image.open(tmpfile) + fp = im.fp + self.assertFalse(fp.closed) + self.assertRaises(Exception, lambda: os.remove(tmpfile)) + im.load() + self.assertTrue(fp.closed) + # this should not fail, as load should have closed the file. + os.remove(tmpfile) + if __name__ == '__main__': unittest.main() diff --git a/Tests/test_file_libtiff.py b/Tests/test_file_libtiff.py index 2d1b33154..c4e4e7d0b 100644 --- a/Tests/test_file_libtiff.py +++ b/Tests/test_file_libtiff.py @@ -39,6 +39,9 @@ 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): @@ -170,8 +173,7 @@ class TestFileLibTiff(LibTiffTestCase): 'RowsPerStrip', 'StripOffsets'] for field in requested_fields: - self.assertTrue(field in reloaded, - "%s not in metadata" % field) + self.assertIn(field, reloaded, "%s not in metadata" % field) def test_additional_metadata(self): # these should not crash. Seriously dummy data, most of it doesn't make @@ -187,7 +189,7 @@ class TestFileLibTiff(LibTiffTestCase): # Exclude ones that have special meaning # that we're already testing them im = Image.open('Tests/images/hopper_g4.tif') - for tag in im.tag_v2.keys(): + for tag in im.tag_v2: try: del(core_items[tag]) except: @@ -399,6 +401,19 @@ class TestFileLibTiff(LibTiffTestCase): TiffImagePlugin.READ_LIBTIFF = False + def test_multipage_nframes(self): + # issue #862 + TiffImagePlugin.READ_LIBTIFF = True + im = Image.open('Tests/images/multipage.tiff') + frames = im.n_frames + self.assertEqual(frames, 3) + for idx in range(frames): + im.seek(0) + # Should not raise ValueError: I/O operation on closed file + im.load() + + TiffImagePlugin.READ_LIBTIFF = False + def test__next(self): TiffImagePlugin.READ_LIBTIFF = True im = Image.open('Tests/images/hopper.tif') @@ -517,11 +532,50 @@ class TestFileLibTiff(LibTiffTestCase): count = im.n_frames im.close() try: - os.remove(tmpfile) # Windows PermissionError here! + os.remove(tmpfile) # Windows PermissionError here! 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) if __name__ == '__main__': diff --git a/Tests/test_file_mcidas.py b/Tests/test_file_mcidas.py index cd601cca3..6785ac4d9 100644 --- a/Tests/test_file_mcidas.py +++ b/Tests/test_file_mcidas.py @@ -1,6 +1,6 @@ from helper import unittest, PillowTestCase -from PIL import McIdasImagePlugin +from PIL import Image, McIdasImagePlugin class TestFileMcIdas(PillowTestCase): @@ -12,6 +12,24 @@ class TestFileMcIdas(PillowTestCase): lambda: McIdasImagePlugin.McIdasImageFile(invalid_file)) + def test_valid_file(self): + # Arrange + # https://ghrc.nsstc.nasa.gov/hydro/details/cmx3g8 + # https://ghrc.nsstc.nasa.gov/pub/fieldCampaigns/camex3/cmx3g8/browse/ + test_file = "Tests/images/cmx3g8_wv_1998.260_0745_mcidas.ara" + saved_file = "Tests/images/cmx3g8_wv_1998.260_0745_mcidas.png" + + # Act + im = Image.open(test_file) + im.load() + + # Assert + self.assertEqual(im.format, "MCIDAS") + self.assertEqual(im.mode, "I") + self.assertEqual(im.size, (1800, 400)) + im2 = Image.open(saved_file) + self.assert_image_equal(im, im2) + if __name__ == '__main__': unittest.main() diff --git a/Tests/test_file_mic.py b/Tests/test_file_mic.py index 044248f3a..8724a2983 100644 --- a/Tests/test_file_mic.py +++ b/Tests/test_file_mic.py @@ -1,10 +1,50 @@ -from helper import unittest, PillowTestCase +from helper import unittest, PillowTestCase, hopper -from PIL import MicImagePlugin +from PIL import Image, ImagePalette, MicImagePlugin + +TEST_FILE = "Tests/images/hopper.mic" class TestFileMic(PillowTestCase): + def test_sanity(self): + im = Image.open(TEST_FILE) + im.load() + self.assertEqual(im.mode, "RGBA") + self.assertEqual(im.size, (128, 128)) + self.assertEqual(im.format, "MIC") + + # Adjust for the gamma of 2.2 encoded into the file + lut = ImagePalette.make_gamma_lut(1/2.2) + im = Image.merge('RGBA', [chan.point(lut) for chan in im.split()]) + + im2 = hopper("RGBA") + self.assert_image_similar(im, im2, 10) + + def test_n_frames(self): + im = Image.open(TEST_FILE) + + self.assertEqual(im.n_frames, 1) + + def test_is_animated(self): + im = Image.open(TEST_FILE) + + self.assertFalse(im.is_animated) + + def test_tell(self): + im = Image.open(TEST_FILE) + + self.assertEqual(im.tell(), 0) + + def test_seek(self): + im = Image.open(TEST_FILE) + + im.seek(0) + self.assertEqual(im.tell(), 0) + + self.assertRaises(EOFError, lambda: im.seek(99)) + self.assertEqual(im.tell(), 0) + def test_invalid_file(self): # Test an invalid OLE file invalid_file = "Tests/images/flower.jpg" diff --git a/Tests/test_file_mpo.py b/Tests/test_file_mpo.py index 4c9f31abd..b4d0c696e 100644 --- a/Tests/test_file_mpo.py +++ b/Tests/test_file_mpo.py @@ -110,7 +110,7 @@ class TestFileMpo(PillowTestCase): im.seek(n_frames) break except EOFError: - self.assertTrue(im.tell() < n_frames) + self.assertLess(im.tell(), n_frames) def test_image_grab(self): for test_file in test_files: diff --git a/Tests/test_file_msp.py b/Tests/test_file_msp.py index f7c518379..9c58deeaf 100644 --- a/Tests/test_file_msp.py +++ b/Tests/test_file_msp.py @@ -2,7 +2,11 @@ from helper import unittest, PillowTestCase, hopper from PIL import Image, MspImagePlugin +import os + TEST_FILE = "Tests/images/hopper.msp" +EXTRA_DIR = "Tests/images/picins" +YA_EXTRA_DIR = "Tests/images/msp" class TestFileMsp(PillowTestCase): @@ -24,14 +28,48 @@ class TestFileMsp(PillowTestCase): self.assertRaises(SyntaxError, lambda: MspImagePlugin.MspImageFile(invalid_file)) - def test_open(self): + def test_bad_checksum(self): + # Arrange + # This was created by forcing Pillow to save with checksum=0 + bad_checksum = "Tests/images/hopper_bad_checksum.msp" + + # Act / Assert + self.assertRaises(SyntaxError, + lambda: MspImagePlugin.MspImageFile(bad_checksum)) + + def test_open_windows_v1(self): # Arrange # Act im = Image.open(TEST_FILE) # Assert - self.assertEqual(im.size, (128, 128)) - self.assert_image_similar(im, hopper("1"), 4) + self.assert_image_equal(im, hopper("1")) + self.assertIsInstance(im, MspImagePlugin.MspImageFile) + + def _assert_file_image_equal(self, source_path, target_path): + with Image.open(source_path) as im: + target = Image.open(target_path) + self.assert_image_equal(im, target) + + @unittest.skipIf(not os.path.exists(EXTRA_DIR), + "Extra image files not installed") + def test_open_windows_v2(self): + + files = (os.path.join(EXTRA_DIR, f) for f in os.listdir(EXTRA_DIR) + if os.path.splitext(f)[1] == '.msp') + for path in files: + self._assert_file_image_equal(path, + path.replace('.msp', '.png')) + + @unittest.skipIf(not os.path.exists(YA_EXTRA_DIR), + "Even More Extra image files not installed") + def test_msp_v2(self): + for f in os.listdir(YA_EXTRA_DIR): + if '.MSP' not in f: + continue + path = os.path.join(YA_EXTRA_DIR, f) + self._assert_file_image_equal(path, + path.replace('.MSP', '.png')) def test_cannot_save_wrong_mode(self): # Arrange diff --git a/Tests/test_file_pcx.py b/Tests/test_file_pcx.py index 7621c1cc6..9766318e6 100644 --- a/Tests/test_file_pcx.py +++ b/Tests/test_file_pcx.py @@ -19,6 +19,10 @@ class TestFilePcx(PillowTestCase): for mode in ('1', 'L', 'P', 'RGB'): self._roundtrip(hopper(mode)) + # Test an unsupported mode + f = self.tempfile("temp.pcx") + self.assertRaises(ValueError, lambda: hopper("RGBA").save(f)) + def test_invalid_file(self): invalid_file = "Tests/images/flower.jpg" diff --git a/Tests/test_file_pdf.py b/Tests/test_file_pdf.py index cfefe2f9e..2caa4cdab 100644 --- a/Tests/test_file_pdf.py +++ b/Tests/test_file_pdf.py @@ -74,6 +74,12 @@ class TestFilePdf(PillowTestCase): self.assertTrue(os.path.isfile(outfile)) self.assertGreater(os.path.getsize(outfile), 0) + # Append images + im.save(outfile, save_all=True, append_images=[hopper()]) + + self.assertTrue(os.path.isfile(outfile)) + self.assertGreater(os.path.getsize(outfile), 0) + if __name__ == '__main__': unittest.main() diff --git a/Tests/test_file_pixar.py b/Tests/test_file_pixar.py new file mode 100644 index 000000000..b6b3f934d --- /dev/null +++ b/Tests/test_file_pixar.py @@ -0,0 +1,29 @@ +from helper import hopper, unittest, PillowTestCase + +from PIL import Image, PixarImagePlugin + +TEST_FILE = "Tests/images/hopper.pxr" + + +class TestFilePixar(PillowTestCase): + + def test_sanity(self): + im = Image.open(TEST_FILE) + im.load() + self.assertEqual(im.mode, "RGB") + self.assertEqual(im.size, (128, 128)) + self.assertEqual(im.format, "PIXAR") + + im2 = hopper() + self.assert_image_similar(im, im2, 4.8) + + def test_invalid_file(self): + invalid_file = "Tests/images/flower.jpg" + + self.assertRaises( + SyntaxError, + lambda: PixarImagePlugin.PixarImageFile(invalid_file)) + + +if __name__ == '__main__': + unittest.main() diff --git a/Tests/test_file_png.py b/Tests/test_file_png.py index cab0e01fa..7dab44333 100644 --- a/Tests/test_file_png.py +++ b/Tests/test_file_png.py @@ -1,14 +1,16 @@ from helper import unittest, PillowTestCase, hopper +from PIL import Image, ImageFile, PngImagePlugin from io import BytesIO - -from PIL import Image -from PIL import ImageFile -from PIL import PngImagePlugin import zlib +import sys codecs = dir(Image.core) +# For Truncated phng memory leak +MEM_LIMIT = 2 # max increase in MB +ITERATIONS = 100 # Leak is 56k/iteration, this will leak 5.6megs + # sample png stream TEST_PNG_FILE = "Tests/images/hopper.png" @@ -316,7 +318,7 @@ class TestFilePng(PillowTestCase): test_file = f.read()[:offset] im = Image.open(BytesIO(test_file)) - self.assertTrue(im.fp is not None) + self.assertIsNotNone(im.fp) self.assertRaises((IOError, SyntaxError), im.verify) def test_verify_ignores_crc_error(self): @@ -331,7 +333,7 @@ class TestFilePng(PillowTestCase): ImageFile.LOAD_TRUNCATED_IMAGES = True try: im = load(image_data) - self.assertTrue(im is not None) + self.assertIsNotNone(im) finally: ImageFile.LOAD_TRUNCATED_IMAGES = False @@ -394,8 +396,8 @@ class TestFilePng(PillowTestCase): 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): @@ -462,7 +464,7 @@ class TestFilePng(PillowTestCase): def test_save_icc_profile(self): im = Image.open("Tests/images/icc_profile_none.png") - self.assertEqual(im.info['icc_profile'], None) + self.assertIsNone(im.info['icc_profile']) with_icc = Image.open("Tests/images/icc_profile.png") expected_icc = with_icc.info['icc_profile'] @@ -485,7 +487,7 @@ class TestFilePng(PillowTestCase): def test_roundtrip_no_icc_profile(self): im = Image.open("Tests/images/icc_profile_none.png") - self.assertEqual(im.info['icc_profile'], None) + self.assertIsNone(im.info['icc_profile']) im = roundtrip(im) self.assertNotIn('icc_profile', im.info) @@ -497,6 +499,77 @@ 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")) + + +@unittest.skipIf(sys.platform.startswith('win32'), "requires Unix or MacOS") +class TestTruncatedPngPLeaks(PillowTestCase): + + def setUp(self): + if "zip_encoder" not in codecs or "zip_decoder" not in codecs: + self.skipTest("zip/deflate support not available") + + def _get_mem_usage(self): + from resource import getpagesize, getrusage, RUSAGE_SELF + mem = getrusage(RUSAGE_SELF).ru_maxrss + if sys.platform == 'darwin': + # man 2 getrusage: + # ru_maxrss the maximum resident set size utilized (in bytes). + return mem / 1024 / 1024 # megs + else: + # linux + # man 2 getrusage + # ru_maxrss (since Linux 2.6.32) + # This is the maximum resident set size used (in kilobytes). + return mem / 1024 # megs + + def test_leak_load(self): + with open('Tests/images/hopper.png', 'rb') as f: + DATA = BytesIO(f.read(16 * 1024)) + + ImageFile.LOAD_TRUNCATED_IMAGES = True + with Image.open(DATA) as im: + im.load() + start_mem = self._get_mem_usage() + try: + for _ in range(ITERATIONS): + with Image.open(DATA) as im: + im.load() + mem = (self._get_mem_usage() - start_mem) + self.assertLess(mem, MEM_LIMIT, msg='memory usage limit exceeded') + finally: + ImageFile.LOAD_TRUNCATED_IMAGES = False + if __name__ == '__main__': unittest.main() diff --git a/Tests/test_file_ppm.py b/Tests/test_file_ppm.py index e7428f88d..fcb26258f 100644 --- a/Tests/test_file_ppm.py +++ b/Tests/test_file_ppm.py @@ -36,19 +36,17 @@ 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. - + # sizes. + with self.assertRaises(IOError): Image.open('Tests/images/negative_size.ppm') diff --git a/Tests/test_file_psd.py b/Tests/test_file_psd.py index 2b4825734..e2a9419b8 100644 --- a/Tests/test_file_psd.py +++ b/Tests/test_file_psd.py @@ -1,8 +1,7 @@ -from helper import unittest, PillowTestCase +from helper import hopper, unittest, PillowTestCase from PIL import Image, PsdImagePlugin -# sample ppm stream test_file = "Tests/images/hopper.psd" @@ -15,6 +14,9 @@ class TestImagePsd(PillowTestCase): self.assertEqual(im.size, (128, 128)) self.assertEqual(im.format, "PSD") + im2 = hopper() + self.assert_image_similar(im, im2, 4.8) + def test_invalid_file(self): invalid_file = "Tests/images/flower.jpg" @@ -41,7 +43,42 @@ class TestImagePsd(PillowTestCase): im.seek(n_frames+1) break except EOFError: - self.assertTrue(im.tell() < n_frames) + self.assertLess(im.tell(), n_frames) + + def test_seek_tell(self): + im = Image.open(test_file) + + layer_number = im.tell() + self.assertEqual(layer_number, 0) + + im.seek(0) + layer_number = im.tell() + self.assertEqual(layer_number, 0) + + im.seek(1) + layer_number = im.tell() + self.assertEqual(layer_number, 1) + + im.seek(2) + layer_number = im.tell() + self.assertEqual(layer_number, 2) + + def test_seek_eoferror(self): + im = Image.open(test_file) + + self.assertRaises(EOFError, lambda: im.seek(-1)) + + def test_icc_profile(self): + im = Image.open(test_file) + self.assertIn("icc_profile", im.info) + + icc_profile = im.info["icc_profile"] + self.assertEqual(len(icc_profile), 3144) + + def test_no_icc_profile(self): + im = Image.open("Tests/images/hopper_merged.psd") + + self.assertNotIn("icc_profile", im.info) if __name__ == '__main__': diff --git a/Tests/test_file_sgi.py b/Tests/test_file_sgi.py index 870e57ed8..c9aeea76c 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" - # Act / Assert - self.assertRaises(ValueError, lambda: Image.open(test_file)) + im = Image.open(test_file) + target = Image.open('Tests/images/transparent.png') + self.assert_image_equal(im, target) + + 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,16 @@ 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_spider.py b/Tests/test_file_spider.py index 96f82054e..32f1f55e5 100644 --- a/Tests/test_file_spider.py +++ b/Tests/test_file_spider.py @@ -69,7 +69,7 @@ class TestImageSpider(PillowTestCase): img_list = SpiderImagePlugin.loadImageSeries(file_list) # Assert - self.assertEqual(img_list, None) + self.assertIsNone(img_list) def test_isInt_not_a_number(self): # Arrange diff --git a/Tests/test_file_sun.py b/Tests/test_file_sun.py index 40eb73898..d5e4699fd 100644 --- a/Tests/test_file_sun.py +++ b/Tests/test_file_sun.py @@ -6,6 +6,7 @@ import os EXTRA_DIR = 'Tests/images/sunraster' + class TestFileSun(PillowTestCase): def test_sanity(self): @@ -19,8 +20,8 @@ class TestFileSun(PillowTestCase): # Assert self.assertEqual(im.size, (128, 128)) - self.assert_image_similar(im, hopper(), 5) # visually verified - + self.assert_image_similar(im, hopper(), 5) # visually verified + invalid_file = "Tests/images/flower.jpg" self.assertRaises(SyntaxError, lambda: SunImagePlugin.SunImageFile(invalid_file)) @@ -30,21 +31,20 @@ class TestFileSun(PillowTestCase): 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')) + 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() + im.load() self.assertIsInstance(im, SunImagePlugin.SunImageFile) target_path = "%s.png" % os.path.splitext(path)[0] - #im.save(target_file) + # 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 9913860ad..6652b431e 100644 --- a/Tests/test_file_tiff.py +++ b/Tests/test_file_tiff.py @@ -1,11 +1,12 @@ -from __future__ import print_function import logging from io import BytesIO import struct +import sys from helper import unittest, PillowTestCase, hopper, py3 from PIL import Image, TiffImagePlugin +from PIL.TiffImagePlugin import X_RESOLUTION, Y_RESOLUTION, RESOLUTION_UNIT logger = logging.getLogger(__name__) @@ -77,7 +78,6 @@ class TestFileTiff(PillowTestCase): self.assertEqual(im.mode, 'RGB') def test_xyres_tiff(self): - from PIL.TiffImagePlugin import X_RESOLUTION, Y_RESOLUTION filename = "Tests/images/pil168.tif" im = Image.open(filename) @@ -93,8 +93,24 @@ class TestFileTiff(PillowTestCase): self.assertEqual(im.info['dpi'], (72., 72.)) + def test_xyres_fallback_tiff(self): + 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" im = Image.open(filename) @@ -105,7 +121,6 @@ class TestFileTiff(PillowTestCase): self.assertEqual(im.info['dpi'], (71., 71.)) def test_save_setting_missing_resolution(self): - from PIL.TiffImagePlugin import X_RESOLUTION, Y_RESOLUTION b = BytesIO() Image.open("Tests/images/10ct_32bit_128.tiff").save( b, format="tiff", resolution=123.45) @@ -219,7 +234,7 @@ class TestFileTiff(PillowTestCase): im.seek(n_frames) break except EOFError: - self.assertTrue(im.tell() < n_frames) + self.assertLess(im.tell(), n_frames) def test_multipage(self): # issue #862 @@ -256,16 +271,6 @@ class TestFileTiff(PillowTestCase): # Assert self.assertIsInstance(ret, str) - def test_as_dict_deprecation(self): - # Arrange - filename = "Tests/images/pil136.tiff" - im = Image.open(filename) - - self.assert_warning(DeprecationWarning, im.tag_v2.as_dict) - self.assert_warning(DeprecationWarning, im.tag.as_dict) - self.assertEqual(dict(im.tag_v2), im.tag_v2.as_dict()) - self.assertEqual(dict(im.tag), im.tag.as_dict()) - def test_dict(self): # Arrange filename = "Tests/images/pil136.tiff" @@ -382,20 +387,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, @@ -413,55 +404,6 @@ class TestFileTiff(PillowTestCase): self.assertEqual(im.tag_v2[X_RESOLUTION], 72) self.assertEqual(im.tag_v2[Y_RESOLUTION], 36) - def test_deprecation_warning_with_spaces(self): - kwargs = {'resolution unit': 'inch', - 'x resolution': 36, - 'y resolution': 72} - filename = self.tempfile("temp.tif") - self.assert_warning(DeprecationWarning, - lambda: hopper("RGB").save(filename, **kwargs)) - from PIL.TiffImagePlugin import X_RESOLUTION, Y_RESOLUTION - - im = Image.open(filename) - - # legacy interface - self.assertEqual(im.tag[X_RESOLUTION][0][0], 36) - self.assertEqual(im.tag[Y_RESOLUTION][0][0], 72) - - # v2 interface - self.assertEqual(im.tag_v2[X_RESOLUTION], 36) - self.assertEqual(im.tag_v2[Y_RESOLUTION], 72) - - def test_multipage_compression(self): - im = Image.open('Tests/images/compression.tif') - - im.seek(0) - self.assertEqual(im._compression, 'tiff_ccitt') - self.assertEqual(im.size, (10, 10)) - - im.seek(1) - self.assertEqual(im._compression, 'packbits') - self.assertEqual(im.size, (10, 10)) - im.load() - - im.seek(0) - self.assertEqual(im._compression, 'tiff_ccitt') - self.assertEqual(im.size, (10, 10)) - im.load() - - def test_save_tiff_with_jpegtables(self): - # Arrange - 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 +441,83 @@ class TestFileTiff(PillowTestCase): with Image.open(mp) as im: self.assertEqual(im.n_frames, 3) + # Test appending images + mp = io.BytesIO() + im = Image.new('RGB', (100, 100), '#f00') + ims = [Image.new('RGB', (100, 100), color) for color + in ['#0f0', '#00f']] + im.save(mp, format="TIFF", save_all=True, append_images=ims) + + mp.seek(0, os.SEEK_SET) + reread = Image.open(mp) + self.assertEqual(reread.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']) + + def test_close_on_load_exclusive(self): + # similar to test_fd_leak, but runs on unixlike os + tmpfile = self.tempfile("temp.tif") + + with Image.open("Tests/images/uint16_1_4660.tif") as im: + im.save(tmpfile) + + im = Image.open(tmpfile) + fp = im.fp + self.assertFalse(fp.closed) + im.load() + self.assertTrue(fp.closed) + + def test_close_on_load_nonexclusive(self): + tmpfile = self.tempfile("temp.tif") + + with Image.open("Tests/images/uint16_1_4660.tif") as im: + im.save(tmpfile) + + f = open(tmpfile, 'rb') + im = Image.open(f) + fp = im.fp + self.assertFalse(fp.closed) + im.load() + self.assertFalse(fp.closed) + +@unittest.skipUnless(sys.platform.startswith('win32'), "Windows only") +class TestFileTiffW32(PillowTestCase): + def test_fd_leak(self): + tmpfile = self.tempfile("temp.tif") + import os + + # this is an mmaped file. + with Image.open("Tests/images/uint16_1_4660.tif") as im: + im.save(tmpfile) + + im = Image.open(tmpfile) + fp = im.fp + self.assertFalse(fp.closed) + self.assertRaises(Exception, lambda: os.remove(tmpfile)) + im.load() + self.assertTrue(fp.closed) + + # this closes the mmap + im.close() + + # this should not fail, as load should have closed the file pointer, + # and close should have closed the mmap + os.remove(tmpfile) + + if __name__ == '__main__': unittest.main() diff --git a/Tests/test_file_tiff_metadata.py b/Tests/test_file_tiff_metadata.py index c75487f9b..844703370 100644 --- a/Tests/test_file_tiff_metadata.py +++ b/Tests/test_file_tiff_metadata.py @@ -1,5 +1,3 @@ -from __future__ import division - import io import struct diff --git a/Tests/test_file_webp.py b/Tests/test_file_webp.py index 88f2e5f5b..a15ca97df 100644 --- a/Tests/test_file_webp.py +++ b/Tests/test_file_webp.py @@ -77,6 +77,11 @@ class TestFileWebp(PillowTestCase): self.assertRaises(IOError, lambda: hopper("L").save(temp_file)) + def test_WebPEncode_with_invalid_args(self): + self.assertRaises(TypeError, _webp.WebPEncode) + + def test_WebPDecode_with_invalid_args(self): + self.assertRaises(TypeError, _webp.WebPDecode) if __name__ == '__main__': unittest.main() diff --git a/Tests/test_file_webp_metadata.py b/Tests/test_file_webp_metadata.py index 947f8464a..88e2b3b88 100644 --- a/Tests/test_file_webp_metadata.py +++ b/Tests/test_file_webp_metadata.py @@ -96,7 +96,7 @@ class TestFileWebpMetadata(PillowTestCase): file_path = "Tests/images/flower.jpg" image = Image.open(file_path) - self.assertTrue('exif' in image.info) + self.assertIn('exif', image.info) test_buffer = BytesIO() diff --git a/Tests/test_file_wmf.py b/Tests/test_file_wmf.py new file mode 100644 index 000000000..a4bc80913 --- /dev/null +++ b/Tests/test_file_wmf.py @@ -0,0 +1,38 @@ +from helper import unittest, PillowTestCase, hopper +from PIL import Image + + +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) + + def test_save(self): + im = hopper() + + for ext in [".wmf", ".emf"]: + tmpfile = self.tempfile("temp"+ext) + self.assertRaises(IOError, lambda: im.save(tmpfile)) + + +if __name__ == '__main__': + unittest.main() diff --git a/Tests/test_file_xpm.py b/Tests/test_file_xpm.py index 5940620ba..d8cc4fa3a 100644 --- a/Tests/test_file_xpm.py +++ b/Tests/test_file_xpm.py @@ -2,7 +2,6 @@ from helper import unittest, PillowTestCase, hopper from PIL import Image, XpmImagePlugin -# sample ppm stream TEST_FILE = "Tests/images/hopper.xpm" diff --git a/Tests/test_file_xvthumb.py b/Tests/test_file_xvthumb.py new file mode 100644 index 000000000..5337a8390 --- /dev/null +++ b/Tests/test_file_xvthumb.py @@ -0,0 +1,42 @@ +from helper import hopper, unittest, PillowTestCase + +from PIL import Image, XVThumbImagePlugin + +TEST_FILE = "Tests/images/hopper.p7" + + +class TestFileXVThumb(PillowTestCase): + + def test_open(self): + # Act + im = Image.open(TEST_FILE) + + # Assert + self.assertEqual(im.format, "XVThumb") + + # Create a Hopper image with a similar XV palette + im_hopper = hopper().quantize(palette=im) + self.assert_image_similar(im, im_hopper, 9) + + def test_unexpected_eof(self): + # Test unexpected EOF reading XV thumbnail file + # Arrange + bad_file = "Tests/images/hopper_bad.p7" + + # Act / Assert + self.assertRaises(SyntaxError, + lambda: + XVThumbImagePlugin.XVThumbImageFile(bad_file)) + + def test_invalid_file(self): + # Arrange + invalid_file = "Tests/images/flower.jpg" + + # Act / Assert + self.assertRaises(SyntaxError, + lambda: + XVThumbImagePlugin.XVThumbImageFile(invalid_file)) + + +if __name__ == '__main__': + unittest.main() diff --git a/Tests/test_format_hsv.py b/Tests/test_format_hsv.py index e32f1c047..b965a854f 100644 --- a/Tests/test_format_hsv.py +++ b/Tests/test_format_hsv.py @@ -1,4 +1,3 @@ -from __future__ import print_function from helper import unittest, PillowTestCase, hopper from PIL import Image @@ -47,9 +46,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 +94,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 +110,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 +131,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 +152,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 ef9aa16af..1f9c4d798 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -29,6 +29,7 @@ class TestImage(PillowTestCase): self.assertEqual(im3.getcolors(), [(10000, 0)]) self.assertRaises(ValueError, lambda: Image.new("X", (100, 100))) + self.assertRaises(ValueError, lambda: Image.new("", (100, 100))) # self.assertRaises( # MemoryError, lambda: Image.new("L", (1000000, 1000000))) @@ -80,11 +81,16 @@ class TestImage(PillowTestCase): # 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_unknown_extension(self): + im = hopper() + temp_file = self.tempfile("temp.unknown") + self.assertRaises(ValueError, lambda: im.save(temp_file)) def test_internals(self): @@ -164,10 +170,10 @@ class TestImage(PillowTestCase): im2 = Image.new('RGB', (25, 25), 'white') # Act / Assert - self.assertTrue(im1 != im2) + self.assertNotEqual(im1, im2) def test_alpha_composite(self): - # http://stackoverflow.com/questions/3374878 + # https://stackoverflow.com/questions/3374878 # Arrange from PIL import ImageDraw @@ -196,6 +202,73 @@ class TestImage(PillowTestCase): img_colors = sorted(img.getcolors()) self.assertEqual(img_colors, expected_colors) + def test_alpha_inplace(self): + src = Image.new('RGBA', (128,128), 'blue') + + over = Image.new('RGBA', (128,128), 'red') + mask = hopper('L') + over.putalpha(mask) + + target = Image.alpha_composite(src, over) + + # basic + full = src.copy() + full.alpha_composite(over) + self.assert_image_equal(full, target) + + # with offset down to right + offset = src.copy() + offset.alpha_composite(over, (64, 64)) + self.assert_image_equal(offset.crop((64, 64, 127, 127)), + target.crop((0, 0, 63, 63))) + self.assertEqual(offset.size, (128, 128)) + + # offset and crop + box = src.copy() + box.alpha_composite(over, (64, 64), (0, 0, 32, 32)) + self.assert_image_equal(box.crop((64, 64, 96, 96)), + target.crop((0, 0, 32, 32))) + self.assert_image_equal(box.crop((96, 96, 128, 128)), + src.crop((0, 0, 32, 32))) + self.assertEqual(box.size, (128, 128)) + + # source point + source = src.copy() + source.alpha_composite(over, (32, 32), (32, 32, 96, 96)) + + self.assert_image_equal(source.crop((32, 32, 96, 96)), + target.crop((32, 32, 96, 96))) + self.assertEqual(source.size, (128, 128)) + + 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) @@ -250,17 +323,22 @@ class TestImage(PillowTestCase): 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. + # 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', (0,0)) # w,h <= 0 + Image.new('RGB', (-1, -1)) # w,h < 0 - self.assertTrue(Image.new('RGB', (1,1))) + # 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]) + i = Image.new('RGB', [1, 1]) self.assertIsInstance(i.size, tuple) def test_storage_neg(self): @@ -270,8 +348,98 @@ class TestImage(PillowTestCase): # Storage.c, rather than the size check above with self.assertRaises(ValueError): - Image.core.fill('RGB', (2,-2), (0,0,0)) + 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) + + def test_linear_gradient_wrong_mode(self): + # Arrange + wrong_mode = "RGB" + + # Act / Assert + self.assertRaises(ValueError, + lambda: Image.linear_gradient(wrong_mode)) + return + + def test_linear_gradient(self): + + # Arrange + target_file = "Tests/images/linear_gradient.png" + for mode in ["L", "P"]: + + # Act + im = Image.linear_gradient(mode) + + # Assert + self.assertEqual(im.size, (256, 256)) + self.assertEqual(im.mode, mode) + self.assertEqual(im.getpixel((0, 0)), 0) + self.assertEqual(im.getpixel((255, 255)), 255) + target = Image.open(target_file).convert(mode) + self.assert_image_equal(im, target) + + def test_radial_gradient_wrong_mode(self): + # Arrange + wrong_mode = "RGB" + + # Act / Assert + self.assertRaises(ValueError, + lambda: Image.radial_gradient(wrong_mode)) + return + + def test_radial_gradient(self): + + # Arrange + target_file = "Tests/images/radial_gradient.png" + for mode in ["L", "P"]: + + # Act + im = Image.radial_gradient(mode) + + # Assert + self.assertEqual(im.size, (256, 256)) + self.assertEqual(im.mode, mode) + self.assertEqual(im.getpixel((0, 0)), 255) + self.assertEqual(im.getpixel((128, 128)), 0) + target = Image.open(target_file).convert(mode) + self.assert_image_equal(im, target) + + +class MockEncoder(object): + pass + + +def mock_encode(*args): + encoder = MockEncoder() + encoder.args = args + return encoder + + +class TestRegistry(PillowTestCase): + + def test_encode_registry(self): + + Image.register_encoder('MOCK', mock_encode) + self.assertIn('MOCK', Image.ENCODERS) + + enc = Image._getencoder('RGB', 'MOCK', ('args',), extra=('extra',)) + + self.assertIsInstance(enc, MockEncoder) + self.assertEqual(enc.args, ('RGB', 'args', 'extra')) + + def test_encode_registry_fail(self): + self.assertRaises(IOError, lambda: Image._getencoder('RGB', + 'DoesNotExist', + ('args',), + extra=('extra',))) if __name__ == '__main__': unittest.main() diff --git a/Tests/test_image_access.py b/Tests/test_image_access.py index 0f8d2a654..9cb27774d 100644 --- a/Tests/test_image_access.py +++ b/Tests/test_image_access.py @@ -1,4 +1,4 @@ -from helper import unittest, PillowTestCase, hopper +from helper import unittest, PillowTestCase, hopper, on_appveyor try: from PIL import PyAccess @@ -7,7 +7,8 @@ except ImportError: pass from PIL import Image - +import sys +import os class AccessTest(PillowTestCase): # initial value @@ -78,12 +79,24 @@ 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"): @@ -221,6 +234,9 @@ class TestCffi(AccessTest): # im = Image.new('I;32B', (10, 10), 2**10) # self._test_set_access(im, 2**13-1) + def test_not_implemented(self): + self.assertIsNone(PyAccess.new(hopper("BGR;15"))) + # ref https://github.com/python-pillow/Pillow/pull/2009 def test_reference_counting(self): size = 10 @@ -233,5 +249,66 @@ class TestCffi(AccessTest): self.assertEqual(px[i, 0], 0) +class TestEmbeddable(unittest.TestCase): + @unittest.skipIf(not sys.platform.startswith('win32') or + sys.version_info[:2] in ((3, 3), (3, 4)) or + on_appveyor(), # failing on appveyor when run from + # subprocess, not from shell + "requires Python 2.7 or >=3.5 for Windows") + def test_embeddable(self): + import subprocess + import ctypes + import setuptools + from distutils import ccompiler, sysconfig + + with open('embed_pil.c', 'w') as fh: + fh.write(""" +#include "Python.h" + +int main(int argc, char* argv[]) +{ + char *home = "%s"; +#if PY_MAJOR_VERSION >= 3 + wchar_t *whome = Py_DecodeLocale(home, NULL); + Py_SetPythonHome(whome); +#else + Py_SetPythonHome(home); +#endif + + Py_InitializeEx(0); + Py_DECREF(PyImport_ImportModule("PIL.Image")); + Py_Finalize(); + + Py_InitializeEx(0); + Py_DECREF(PyImport_ImportModule("PIL.Image")); + Py_Finalize(); + +#if PY_MAJOR_VERSION >= 3 + PyMem_RawFree(whome); +#endif + + return 0; +} + """ % sys.prefix.replace('\\', '\\\\')) + + compiler = ccompiler.new_compiler() + compiler.add_include_dir(sysconfig.get_python_inc()) + + libdir = sysconfig.get_config_var('LIBDIR') or sysconfig.get_python_inc().replace('include', 'libs') + print (libdir) + compiler.add_library_dir(libdir) + objects = compiler.compile(['embed_pil.c']) + compiler.link_executable(objects, 'embed_pil') + + env = os.environ.copy() + env["PATH"] = sys.prefix + ';' + env["PATH"] + + # do not display the Windows Error Reporting dialog + ctypes.windll.kernel32.SetErrorMode(0x0002) + + process = subprocess.Popen(['embed_pil.exe'], env=env) + process.communicate() + self.assertEqual(process.returncode, 0) + if __name__ == '__main__': unittest.main() diff --git a/Tests/test_image_convert.py b/Tests/test_image_convert.py index 0c98211e7..5687232f3 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") @@ -132,6 +137,76 @@ class TestImageConvert(PillowTestCase): self.assert_image_similar(alpha, comparable, 5) + def test_matrix_illegal_conversion(self): + # Arrange + im = hopper('CMYK') + matrix = ( + 0.412453, 0.357580, 0.180423, 0, + 0.212671, 0.715160, 0.072169, 0, + 0.019334, 0.119193, 0.950227, 0) + self.assertNotEqual(im.mode, 'RGB') + + # Act / Assert + self.assertRaises(ValueError, + lambda: im.convert(mode='CMYK', matrix=matrix)) + + def test_matrix_wrong_mode(self): + # Arrange + im = hopper('L') + matrix = ( + 0.412453, 0.357580, 0.180423, 0, + 0.212671, 0.715160, 0.072169, 0, + 0.019334, 0.119193, 0.950227, 0) + self.assertEqual(im.mode, 'L') + + # Act / Assert + self.assertRaises(ValueError, + lambda: im.convert(mode='L', matrix=matrix)) + + def test_matrix_xyz(self): + + def matrix_convert(mode): + # Arrange + im = hopper('RGB') + matrix = ( + 0.412453, 0.357580, 0.180423, 0, + 0.212671, 0.715160, 0.072169, 0, + 0.019334, 0.119193, 0.950227, 0) + self.assertEqual(im.mode, 'RGB') + + # Act + # Convert an RGB image to the CIE XYZ colour space + converted_im = im.convert(mode=mode, matrix=matrix) + + # Assert + self.assertEqual(converted_im.mode, mode) + self.assertEqual(converted_im.size, im.size) + target = Image.open('Tests/images/hopper-XYZ.png') + if converted_im.mode == 'RGB': + self.assert_image_similar(converted_im, target, 3) + else: + self.assert_image_similar(converted_im, target.split()[0], 1) + + matrix_convert('RGB') + matrix_convert('L') + + def test_matrix_identity(self): + # Arrange + im = hopper('RGB') + identity_matrix = ( + 1, 0, 0, 0, + 0, 1, 0, 0, + 0, 0, 1, 0) + self.assertEqual(im.mode, 'RGB') + + # Act + # Convert with an identity matrix + converted_im = im.convert(mode='RGB', matrix=identity_matrix) + + # Assert + # No change + self.assert_image_equal(converted_im, im) + if __name__ == '__main__': unittest.main() diff --git a/Tests/test_image_copy.py b/Tests/test_image_copy.py index ba53758d5..bb1246a73 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..fe92dd865 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) @@ -67,22 +70,38 @@ class TestImageCrop(PillowTestCase): self.assertEqual(cropped.size, (3, 5)) def test_crop_crash(self): - #Image.crop crashes prepatch with an access violation - #apparently a use after free on windows, see - #https://github.com/python-pillow/Pillow/issues/1077 - + # 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 + 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_getbbox.py b/Tests/test_image_getbbox.py index 116f26497..f29032143 100644 --- a/Tests/test_image_getbbox.py +++ b/Tests/test_image_getbbox.py @@ -14,7 +14,7 @@ class TestImageGetBbox(PillowTestCase): # 8-bit mode im = Image.new("L", (100, 100), 0) - self.assertEqual(im.getbbox(), None) + self.assertIsNone(im.getbbox()) im.paste(255, (10, 25, 90, 75)) self.assertEqual(im.getbbox(), (10, 25, 90, 75)) @@ -27,7 +27,7 @@ class TestImageGetBbox(PillowTestCase): # 32-bit mode im = Image.new("RGB", (100, 100), 0) - self.assertEqual(im.getbbox(), None) + self.assertIsNone(im.getbbox()) im.paste(255, (10, 25, 90, 75)) self.assertEqual(im.getbbox(), (10, 25, 90, 75)) diff --git a/Tests/test_image_getcolors.py b/Tests/test_image_getcolors.py index a6a20b288..ca7a9d93d 100644 --- a/Tests/test_image_getcolors.py +++ b/Tests/test_image_getcolors.py @@ -20,15 +20,15 @@ class TestImageGetColors(PillowTestCase): self.assertEqual(getcolors("I"), 255) self.assertEqual(getcolors("F"), 255) self.assertEqual(getcolors("P"), 90) # fixed palette - self.assertEqual(getcolors("RGB"), None) - self.assertEqual(getcolors("RGBA"), None) - self.assertEqual(getcolors("CMYK"), None) - self.assertEqual(getcolors("YCbCr"), None) + self.assertIsNone(getcolors("RGB")) + self.assertIsNone(getcolors("RGBA")) + self.assertIsNone(getcolors("CMYK")) + self.assertIsNone(getcolors("YCbCr")) - self.assertEqual(getcolors("L", 128), None) + self.assertIsNone(getcolors("L", 128)) self.assertEqual(getcolors("L", 1024), 255) - self.assertEqual(getcolors("RGB", 8192), None) + self.assertIsNone(getcolors("RGB", 8192)) self.assertEqual(getcolors("RGB", 16384), 10100) self.assertEqual(getcolors("RGB", 100000), 10100) @@ -48,7 +48,7 @@ class TestImageGetColors(PillowTestCase): (7960, (31, 20, 33))] A = im.getcolors(maxcolors=2) - self.assertEqual(A, None) + self.assertIsNone(A) A = im.getcolors(maxcolors=3) A.sort() diff --git a/Tests/test_image_getpalette.py b/Tests/test_image_getpalette.py index 14ecddbbf..01a6ac7ad 100644 --- a/Tests/test_image_getpalette.py +++ b/Tests/test_image_getpalette.py @@ -9,15 +9,15 @@ class TestImageGetPalette(PillowTestCase): if p: return p[:10] return None - self.assertEqual(palette("1"), None) - self.assertEqual(palette("L"), None) - self.assertEqual(palette("I"), None) - self.assertEqual(palette("F"), None) + self.assertIsNone(palette("1")) + self.assertIsNone(palette("L")) + self.assertIsNone(palette("I")) + self.assertIsNone(palette("F")) self.assertEqual(palette("P"), [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]) - self.assertEqual(palette("RGB"), None) - self.assertEqual(palette("RGBA"), None) - self.assertEqual(palette("CMYK"), None) - self.assertEqual(palette("YCbCr"), None) + self.assertIsNone(palette("RGB")) + self.assertIsNone(palette("RGBA")) + self.assertIsNone(palette("CMYK")) + self.assertIsNone(palette("YCbCr")) if __name__ == '__main__': diff --git a/Tests/test_image_paste.py b/Tests/test_image_paste.py index 4450fd028..e782008a7 100644 --- a/Tests/test_image_paste.py +++ b/Tests/test_image_paste.py @@ -9,9 +9,9 @@ class TestImagingPaste(PillowTestCase): def assert_9points_image(self, im, expected): expected = [ - point[0] + point[0] if im.mode == 'L' else - point[:len(im.mode)] + point[:len(im.mode)] for point in expected ] px = im.load() @@ -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), @@ -256,7 +249,7 @@ class TestImagingPaste(PillowTestCase): im2 = Image.new('RGB', (50, 50)) im.copy().paste(im2) - im.copy().paste(im2, (0,0)) + im.copy().paste(im2, (0, 0)) if __name__ == '__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 17461c868..0e8ec347d 100644 --- a/Tests/test_image_resample.py +++ b/Tests/test_image_resample.py @@ -1,9 +1,9 @@ -from __future__ import division +from __future__ import division, print_function from contextlib import contextmanager from helper import unittest, PillowTestCase, hopper -from PIL import Image, ImageDraw, ImageMode +from PIL import Image, ImageDraw class TestImagingResampleVulnerability(PillowTestCase): @@ -19,8 +19,8 @@ class TestImagingResampleVulnerability(PillowTestCase): def test_invalid_size(self): im = hopper() + # Should not crash im.resize((100, 100)) - self.assertTrue(True, "Should not Crash") with self.assertRaises(ValueError): im.resize((-100, 100)) @@ -161,14 +161,12 @@ class TestImagingCoreResampleAccuracy(PillowTestCase): def test_enlarge_hamming(self): for mode in ['RGBX', 'RGB', 'La', 'L']: - case = self.make_case(mode, (4, 4), 0xe1) - case = case.resize((8, 8), Image.HAMMING) - data = ('e1 e1 ea d1' - 'e1 e1 ea d1' - 'ea ea f4 d9' - 'd1 d1 d9 c4') + case = self.make_case(mode, (2, 2), 0xe1) + case = case.resize((4, 4), Image.HAMMING) + data = ('e1 d2' + 'd2 c5') for channel in case.split(): - self.check_case(channel, self.make_sample(data, (8, 8))) + self.check_case(channel, self.make_sample(data, (4, 4))) def test_enlarge_bicubic(self): for mode in ['RGBX', 'RGB', 'La', 'L']: @@ -247,8 +245,8 @@ class CoreResampleAlphaCorrectTest(PillowTestCase): for y in range(i.size[1]): used_colors = {px[x, y][0] for x in range(i.size[0])} self.assertEqual(256, len(used_colors), - 'All colors should present in resized image. ' - 'Only {} on {} line.'.format(len(used_colors), y)) + 'All colors should present in resized image. ' + 'Only {} on {} line.'.format(len(used_colors), y)) @unittest.skip("current implementation isn't precise enough") def test_levels_rgba(self): @@ -349,10 +347,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) @@ -360,7 +358,7 @@ 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 @@ -368,10 +366,10 @@ class CoreResampleCoefficientsTest(PillowTestCase): im = Image.new('RGBA', (1280, 1280), (0x20, 0x40, 0x60, 0xff)) histogram = im.resize((256, 256), Image.BICUBIC).histogram() - self.assertEqual(histogram[0x100 * 0 + 0x20], 0x10000) # first channel - self.assertEqual(histogram[0x100 * 1 + 0x40], 0x10000) # second channel - self.assertEqual(histogram[0x100 * 2 + 0x60], 0x10000) # third channel - self.assertEqual(histogram[0x100 * 3 + 0xff], 0x10000) # fourth channel + 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 class CoreResampleBoxTest(PillowTestCase): diff --git a/Tests/test_image_resize.py b/Tests/test_image_resize.py index 7db409659..c62f494e0 100644 --- a/Tests/test_image_resize.py +++ b/Tests/test_image_resize.py @@ -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..fbcf9008d 100644 --- a/Tests/test_image_rotate.py +++ b/Tests/test_image_rotate.py @@ -4,23 +4,98 @@ 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 4a318c5c4..6d7715c80 100644 --- a/Tests/test_image_toqimage.py +++ b/Tests/test_image_toqimage.py @@ -1,73 +1,91 @@ from helper import unittest, PillowTestCase, hopper from test_imageqt import PillowQtTestCase -from PIL import ImageQt +from PIL import ImageQt, Image if ImageQt.qt_is_installed: - from PIL.ImageQt import QImage, QPixmap + from PIL.ImageQt import QImage try: from PyQt5 import QtGui + from PyQt5.QtWidgets import QWidget, QHBoxLayout, QLabel, QApplication + QT_VERSION = 5 except (ImportError, RuntimeError): try: from PyQt4 import QtGui + from PyQt4.QtGui import QWidget, QHBoxLayout, QLabel, QApplication + QT_VERSION = 4 except (ImportError, RuntimeError): from PySide import QtGui - - + from PySide.QtGui import QWidget, QHBoxLayout, QLabel, QApplication + QT_VERSION = 4 class TestToQImage(PillowQtTestCase, PillowTestCase): def test_sanity(self): PillowQtTestCase.setUp(self) - for mode in ('1', 'RGB', 'RGBA', 'L', 'P'): - data = ImageQt.toqimage(hopper(mode)) + for mode in ('RGB', 'RGBA', 'L', 'P', '1'): + src = hopper(mode) + data = ImageQt.toqimage(src) self.assertIsInstance(data, QImage) self.assertFalse(data.isNull()) + # reload directly from the qimage + rt = ImageQt.fromqimage(data) + if mode in ('L', 'P', '1'): + self.assert_image_equal(rt, src.convert('RGB')) + else: + self.assert_image_equal(rt, src) + + if mode == '1': + # BW appears to not save correctly on QT4 and QT5 + # kicks out errors on console: + # libpng warning: Invalid color type/bit depth combination in IHDR + # libpng error: Invalid IHDR data + continue + # Test saving the file tempfile = self.tempfile('temp_{}.png'.format(mode)) data.save(tempfile) + # Check that it actually worked. + reloaded = Image.open(tempfile) + # Gray images appear to come back in palette mode. + # They're roughly equivalent + if QT_VERSION == 4 and mode == 'L': + src = src.convert('P') + self.assert_image_equal(reloaded, src) def test_segfault(self): PillowQtTestCase.setUp(self) - app = QtGui.QApplication([]) + app = QApplication([]) ex = Example() + assert(app) # Silence warning + assert(ex) # Silence warning if ImageQt.qt_is_installed: - class Example(QtGui.QWidget): + class Example(QWidget): def __init__(self): super(Example, self).__init__() - img = hopper().resize((1000,1000)) + img = hopper().resize((1000, 1000)) qimage = ImageQt.ImageQt(img) pixmap1 = QtGui.QPixmap.fromImage(qimage) - hbox = QtGui.QHBoxLayout(self) + hbox = QHBoxLayout(self) - lbl = QtGui.QLabel(self) + lbl = 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_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..ef30f3e00 100644 --- a/Tests/test_imagecms.py +++ b/Tests/test_imagecms.py @@ -1,7 +1,7 @@ from helper import unittest, PillowTestCase, hopper import datetime -from PIL import Image +from PIL import Image, ImageMode from io import BytesIO import os @@ -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") @@ -222,7 +222,7 @@ class TestImageCms(PillowTestCase): self.assertTrue(img_srgb.info['icc_profile']) profile = ImageCmsProfile(BytesIO(img_srgb.info['icc_profile'])) - self.assertTrue('sRGB' in ImageCms.getProfileDescription(profile)) + self.assertIn('sRGB', ImageCms.getProfileDescription(profile)) def test_lab_roundtrip(self): # check to see if we're at least internally consistent. @@ -275,12 +275,12 @@ class TestImageCms(PillowTestCase): assert_truncated_tuple_equal(p.blue_colorant, ((0.14306640625, 0.06060791015625, 0.7140960693359375), (0.1558847490315394, 0.06603820639433387, 0.06060791015625))) assert_truncated_tuple_equal(p.blue_primary, ((0.14306641366715667, 0.06060790921083026, 0.7140960805782015), (0.15588475410450106, 0.06603820408959558, 0.06060790921083026))) assert_truncated_tuple_equal(p.chromatic_adaptation, (((1.04791259765625, 0.0229339599609375, -0.050201416015625), (0.02960205078125, 0.9904632568359375, -0.0170745849609375), (-0.009246826171875, 0.0150604248046875, 0.7517852783203125)), ((1.0267159024652783, 0.022470062342089134, 0.0229339599609375), (0.02951378324103937, 0.9875098886387147, 0.9904632568359375), (-0.012205438066465256, 0.01987915407854985, 0.0150604248046875)))) - self.assertEqual(p.chromaticity, None) + self.assertIsNone(p.chromaticity) self.assertEqual(p.clut, {0: (False, False, True), 1: (False, False, True), 2: (False, False, True), 3: (False, False, True)}) self.assertEqual(p.color_space, 'RGB') - self.assertEqual(p.colorant_table, None) - self.assertEqual(p.colorant_table_out, None) - self.assertEqual(p.colorimetric_intent, None) + self.assertIsNone(p.colorant_table) + self.assertIsNone(p.colorant_table_out) + self.assertIsNone(p.colorimetric_intent) self.assertEqual(p.connection_space, 'XYZ ') self.assertEqual(p.copyright, 'Copyright International Color Consortium, 2009') self.assertEqual(p.creation_date, datetime.datetime(2009, 2, 27, 21, 36, 31)) @@ -292,17 +292,17 @@ class TestImageCms(PillowTestCase): self.assertEqual(p.header_model, '\x00\x00\x00\x00') self.assertEqual(p.icc_measurement_condition, {'backing': (0.0, 0.0, 0.0), 'flare': 0.0, 'geo': 'unknown', 'observer': 1, 'illuminant_type': 'D65'}) self.assertEqual(p.icc_version, 33554432) - self.assertEqual(p.icc_viewing_condition, None) + self.assertIsNone(p.icc_viewing_condition) self.assertEqual(p.intent_supported, {0: (True, True, True), 1: (True, True, True), 2: (True, True, True), 3: (True, True, True)}) - self.assertEqual(p.is_matrix_shaper, True) + self.assertTrue(p.is_matrix_shaper) self.assertEqual(p.luminance, ((0.0, 80.0, 0.0), (0.0, 1.0, 80.0))) - self.assertEqual(p.manufacturer, None) + self.assertIsNone(p.manufacturer) assert_truncated_tuple_equal(p.media_black_point, ((0.012054443359375, 0.0124969482421875, 0.01031494140625), (0.34573304157549234, 0.35842450765864337, 0.0124969482421875))) assert_truncated_tuple_equal(p.media_white_point, ((0.964202880859375, 1.0, 0.8249053955078125), (0.3457029219802284, 0.3585375327567059, 1.0))) assert_truncated_tuple_equal((p.media_white_point_temperature,), (5000.722328847392,)) self.assertEqual(p.model, 'IEC 61966-2-1 Default RGB Colour Space - sRGB') self.assertEqual(p.pcs, 'XYZ') - self.assertEqual(p.perceptual_rendering_intent_gamut, None) + self.assertIsNone(p.perceptual_rendering_intent_gamut) self.assertEqual(p.product_copyright, 'Copyright International Color Consortium, 2009') self.assertEqual(p.product_desc, 'sRGB IEC61966-2-1 black scaled') self.assertEqual(p.product_description, 'sRGB IEC61966-2-1 black scaled') @@ -313,9 +313,9 @@ class TestImageCms(PillowTestCase): assert_truncated_tuple_equal(p.red_colorant, ((0.436065673828125, 0.2224884033203125, 0.013916015625), (0.6484536316398539, 0.3308524880306778, 0.2224884033203125))) assert_truncated_tuple_equal(p.red_primary, ((0.43606566581047446, 0.22248840582960838, 0.013916015621759925), (0.6484536250319214, 0.3308524944738204, 0.22248840582960838))) self.assertEqual(p.rendering_intent, 0) - self.assertEqual(p.saturation_rendering_intent_gamut, None) - self.assertEqual(p.screening_description, None) - self.assertEqual(p.target, None) + self.assertIsNone(p.saturation_rendering_intent_gamut) + self.assertIsNone(p.screening_description) + self.assertIsNone(p.target) self.assertEqual(p.technology, 'CRT ') self.assertEqual(p.version, 2.0) self.assertEqual(p.viewing_condition, 'Reference Viewing Condition in IEC 61966-2-1') @@ -326,12 +326,103 @@ 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() - + + def assert_aux_channel_preserved(self, mode, transform_in_place, preserved_channel): + def create_test_image(): + # set up test image with something interesting in the tested aux + # channel. + nine_grid_deltas = [ + (-1, -1), (-1, 0), (-1, 1), + ( 0, -1), ( 0, 0), ( 0, 1), + ( 1, -1), ( 1, 0), ( 1, 1), + ] + chans = [] + bands = ImageMode.getmode(mode).bands + for band_ndx in range(len(bands)): + channel_type = 'L' # 8-bit unorm + channel_pattern = hopper(channel_type) + + # paste pattern with varying offsets to avoid correlation + # potentially hiding some bugs (like channels getting mixed). + paste_offset = ( + int(band_ndx / float(len(bands)) * channel_pattern.size[0]), + int(band_ndx / float(len(bands) * 2) * channel_pattern.size[1]) + ) + channel_data = Image.new(channel_type, channel_pattern.size) + for delta in nine_grid_deltas: + channel_data.paste(channel_pattern, tuple(paste_offset[c] + delta[c]*channel_pattern.size[c] for c in range(2))) + chans.append(channel_data) + return Image.merge(mode, chans) + + source_image = create_test_image() + preserved_channel_ndx = source_image.getbands().index(preserved_channel) + source_image_aux = source_image.split()[preserved_channel_ndx] + + # create some transform, it doesn't matter which one + source_profile = ImageCms.createProfile("sRGB") + destination_profile = ImageCms.createProfile("sRGB") + t = ImageCms.buildTransform(source_profile, destination_profile, inMode=mode, outMode=mode) + + # apply transform + if transform_in_place: + ImageCms.applyTransform(source_image, t, inPlace=True) + result_image = source_image + else: + result_image = ImageCms.applyTransform(source_image, t, inPlace=False) + result_image_aux = result_image.split()[preserved_channel_ndx] + + self.assert_image_equal(source_image_aux, result_image_aux) + + def test_preserve_auxiliary_channels_rgba(self): + self.assert_aux_channel_preserved(mode='RGBA', transform_in_place=False, preserved_channel='A') + + def test_preserve_auxiliary_channels_rgba_in_place(self): + self.assert_aux_channel_preserved(mode='RGBA', transform_in_place=True, preserved_channel='A') + + def test_preserve_auxiliary_channels_rgbx(self): + self.assert_aux_channel_preserved(mode='RGBX', transform_in_place=False, preserved_channel='X') + + def test_preserve_auxiliary_channels_rgbx_in_place(self): + self.assert_aux_channel_preserved(mode='RGBX', transform_in_place=True, preserved_channel='X') + + def test_auxiliary_channels_isolated(self): + # test data in aux channels does not affect non-aux channels + aux_channel_formats = [ + # format, profile, color-only format, source test image + ('RGBA', 'sRGB', 'RGB', hopper('RGBA')), + ('RGBX', 'sRGB', 'RGB', hopper('RGBX')), + ('LAB', 'LAB', 'LAB', Image.open('Tests/images/hopper.Lab.tif')), + ] + for src_format in aux_channel_formats: + for dst_format in aux_channel_formats: + for transform_in_place in [True, False]: + # inplace only if format doesn't change + if transform_in_place and src_format[0] != dst_format[0]: + continue + + # convert with and without AUX data, test colors are equal + source_profile = ImageCms.createProfile(src_format[1]) + destination_profile = ImageCms.createProfile(dst_format[1]) + source_image = src_format[3] + test_transform = ImageCms.buildTransform(source_profile, destination_profile, inMode=src_format[0], outMode=dst_format[0]) + + # test conversion from aux-ful source + if transform_in_place: + test_image = source_image.copy() + ImageCms.applyTransform(test_image, test_transform, inPlace=True) + else: + test_image = ImageCms.applyTransform(source_image, test_transform, inPlace=False) + + # reference conversion from aux-less source + reference_transform = ImageCms.buildTransform(source_profile, destination_profile, inMode=src_format[2], outMode=dst_format[2]) + reference_image = ImageCms.applyTransform(source_image.convert(src_format[2]), reference_transform) + + self.assert_image_equal(test_image.convert(dst_format[2]), reference_image) if __name__ == '__main__': unittest.main() diff --git a/Tests/test_imagecolor.py b/Tests/test_imagecolor.py index 996367b30..64e88cf9c 100644 --- a/Tests/test_imagecolor.py +++ b/Tests/test_imagecolor.py @@ -119,7 +119,7 @@ class TestImageColor(PillowTestCase): # look for rounding errors (based on code by Tim Hatch) def test_rounding_errors(self): - for color in list(ImageColor.colormap.keys()): + for color in ImageColor.colormap: expected = Image.new( "RGB", (1, 1), color).convert("L").getpixel((0, 0)) actual = ImageColor.getcolor(color, 'L') diff --git a/Tests/test_imagedraw.py b/Tests/test_imagedraw.py index 6f92ac3a0..9c989d2e5 100644 --- a/Tests/test_imagedraw.py +++ b/Tests/test_imagedraw.py @@ -30,6 +30,8 @@ BBOX2 = [X0, Y0, X1, Y1] POINTS1 = [(10, 10), (20, 40), (30, 30)] POINTS2 = [10, 10, 20, 40, 30, 30] +KITE_POINTS = [(10, 50), (70, 10), (90, 50), (70, 90), (10, 50)] + class TestImageDraw(PillowTestCase): @@ -44,14 +46,6 @@ class TestImageDraw(PillowTestCase): draw.polygon(list(range(100))) draw.rectangle(list(range(4))) - def test_removed_methods(self): - im = hopper() - - draw = ImageDraw.Draw(im) - - self.assertRaises(Exception, lambda: draw.setink(0)) - self.assertRaises(Exception, lambda: draw.setfill(0)) - def test_valueerror(self): im = Image.open("Tests/images/chi.gif") @@ -86,6 +80,37 @@ class TestImageDraw(PillowTestCase): self.helper_arc(BBOX2, 0, 180) self.helper_arc(BBOX2, 0.5, 180.4) + def test_arc_end_le_start(self): + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw.Draw(im) + start = 270.5 + end = 0 + + # Act + draw.arc(BBOX1, start=start, end=end) + del draw + + # Assert + self.assert_image_equal( + im, Image.open("Tests/images/imagedraw_arc_end_le_start.png")) + + def test_arc_no_loops(self): + # No need to go in loops + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw.Draw(im) + start = 5 + end = 370 + + # Act + draw.arc(BBOX1, start=start, end=end) + del draw + + # Assert + self.assert_image_similar( + im, Image.open("Tests/images/imagedraw_arc_no_loops.png"), 1) + def test_bitmap(self): # Arrange small = Image.open("Tests/images/pil123rgba.png").resize((50, 50)) @@ -100,45 +125,49 @@ class TestImageDraw(PillowTestCase): self.assert_image_equal( im, Image.open("Tests/images/imagedraw_bitmap.png")) - def helper_chord(self, bbox, start, end): + def helper_chord(self, mode, bbox, start, end): # Arrange - im = Image.new("RGB", (W, H)) + im = Image.new(mode, (W, H)) draw = ImageDraw.Draw(im) + expected = "Tests/images/imagedraw_chord_{}.png".format(mode) # Act draw.chord(bbox, start, end, fill="red", outline="yellow") del draw # Assert - self.assert_image_similar( - im, Image.open("Tests/images/imagedraw_chord.png"), 1) + self.assert_image_similar(im, Image.open(expected), 1) def test_chord1(self): - self.helper_chord(BBOX1, 0, 180) - self.helper_chord(BBOX1, 0.5, 180.4) + for mode in ["RGB", "L"]: + self.helper_chord(mode, BBOX1, 0, 180) + self.helper_chord(mode, BBOX1, 0.5, 180.4) def test_chord2(self): - self.helper_chord(BBOX2, 0, 180) - self.helper_chord(BBOX2, 0.5, 180.4) + for mode in ["RGB", "L"]: + self.helper_chord(mode, BBOX2, 0, 180) + self.helper_chord(mode, BBOX2, 0.5, 180.4) - def helper_ellipse(self, bbox): + def helper_ellipse(self, mode, bbox): # Arrange - im = Image.new("RGB", (W, H)) + im = Image.new(mode, (W, H)) draw = ImageDraw.Draw(im) + expected = "Tests/images/imagedraw_ellipse_{}.png".format(mode) # Act draw.ellipse(bbox, fill="green", outline="blue") del draw # Assert - self.assert_image_similar( - im, Image.open("Tests/images/imagedraw_ellipse.png"), 1) + self.assert_image_similar(im, Image.open(expected), 1) def test_ellipse1(self): - self.helper_ellipse(BBOX1) + for mode in ["RGB", "L"]: + self.helper_ellipse(mode, BBOX1) def test_ellipse2(self): - self.helper_ellipse(BBOX2) + for mode in ["RGB", "L"]: + self.helper_ellipse(mode, BBOX2) def test_ellipse_edge(self): # Arrange @@ -172,6 +201,50 @@ class TestImageDraw(PillowTestCase): def test_line2(self): self.helper_line(POINTS2) + def test_shape1(self): + # Arrange + im = Image.new("RGB", (100, 100), "white") + draw = ImageDraw.Draw(im) + x0, y0 = 5, 5 + x1, y1 = 5, 50 + x2, y2 = 95, 50 + x3, y3 = 95, 5 + + # Act + s = ImageDraw.Outline() + s.move(x0, y0) + s.curve(x1, y1, x2, y2, x3, y3) + s.line(x0, y0) + + draw.shape(s, fill=1) + del draw + + # Assert + self.assert_image_equal( + im, Image.open("Tests/images/imagedraw_shape1.png")) + + def test_shape2(self): + # Arrange + im = Image.new("RGB", (100, 100), "white") + draw = ImageDraw.Draw(im) + x0, y0 = 95, 95 + x1, y1 = 95, 50 + x2, y2 = 5, 50 + x3, y3 = 5, 95 + + # Act + s = ImageDraw.Outline() + s.move(x0, y0) + s.curve(x1, y1, x2, y2, x3, y3) + s.line(x0, y0) + + draw.shape(s, outline="blue") + del draw + + # Assert + self.assert_image_equal( + im, Image.open("Tests/images/imagedraw_shape2.png")) + def helper_pieslice(self, bbox, start, end): # Arrange im = Image.new("RGB", (W, H)) @@ -231,6 +304,23 @@ class TestImageDraw(PillowTestCase): def test_polygon2(self): self.helper_polygon(POINTS2) + def test_polygon_kite(self): + # Test drawing lines of different gradients (dx>dy, dy>dx) and + # vertical (dx==0) and horizontal (dy==0) lines + for mode in ["RGB", "L"]: + # Arrange + im = Image.new(mode, (W, H)) + draw = ImageDraw.Draw(im) + expected = "Tests/images/imagedraw_polygon_kite_{}.png".format( + mode) + + # Act + draw.polygon(KITE_POINTS, fill="blue", outline="yellow") + del draw + + # Assert + self.assert_image_equal(im, Image.open(expected)) + def helper_rectangle(self, bbox): # Arrange im = Image.new("RGB", (W, H)) @@ -250,20 +340,44 @@ class TestImageDraw(PillowTestCase): def test_rectangle2(self): self.helper_rectangle(BBOX2) + def test_big_rectangle(self): + # Test drawing a rectangle bigger than the image + # Arrange + im = Image.new("RGB", (W, H)) + bbox = [(-1, -1), (W+1, H+1)] + draw = ImageDraw.Draw(im) + expected = "Tests/images/imagedraw_big_rectangle.png" + + # Act + draw.rectangle(bbox, fill="orange") + del draw + + # Assert + self.assert_image_similar(im, Image.open(expected), 1) + def test_floodfill(self): # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) draw.rectangle(BBOX2, outline="yellow", fill="green") centre_point = (int(W/2), int(H/2)) + red = ImageColor.getrgb("red") + im_floodfill = Image.open("Tests/images/imagedraw_floodfill.png") # Act - ImageDraw.floodfill(im, centre_point, ImageColor.getrgb("red")) - del draw + ImageDraw.floodfill(im, centre_point, red) # Assert - self.assert_image_equal( - im, Image.open("Tests/images/imagedraw_floodfill.png")) + self.assert_image_equal(im, im_floodfill) + + # Test that using the same colour does not change the image + ImageDraw.floodfill(im, centre_point, red) + self.assert_image_equal(im, im_floodfill) + + # Test that filling outside the image does not change the image + ImageDraw.floodfill(im, (W, H), red) + self.assert_image_equal(im, im_floodfill) + del draw @unittest.skipIf(hasattr(sys, 'pypy_version_info'), "Causes fatal RPython error on PyPy") @@ -286,6 +400,26 @@ class TestImageDraw(PillowTestCase): self.assert_image_equal( im, Image.open("Tests/images/imagedraw_floodfill2.png")) + + def test_floodfill_thresh(self): + # floodfill() is experimental + + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw.Draw(im) + draw.rectangle(BBOX2, outline="darkgreen", fill="green") + centre_point = (int(W/2), int(H/2)) + + # Act + ImageDraw.floodfill( + im, centre_point, ImageColor.getrgb("red"), + thresh=30) + del draw + + # Assert + self.assert_image_equal( + im, Image.open("Tests/images/imagedraw_floodfill2.png")) + def create_base_image_draw(self, size, mode=DEFAULT_MODE, background1=WHITE, @@ -433,6 +567,21 @@ class TestImageDraw(PillowTestCase): self.assert_image_equal(img, expected, 'line oblique 45 inverted 3px wide B failed') + def test_wide_line_dot(self): + # Test drawing a wide "line" from one point to another just draws + # a single point + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw.Draw(im) + expected = "Tests/images/imagedraw_wide_line_dot.png" + + # Act + draw.line([(50, 50), (50, 50)], width=3) + del draw + + # Assert + self.assert_image_similar(im, Image.open(expected), 1) + if __name__ == '__main__': unittest.main() diff --git a/Tests/test_imagefile.py b/Tests/test_imagefile.py index 65f54eb8c..d150d996d 100644 --- a/Tests/test_imagefile.py +++ b/Tests/test_imagefile.py @@ -95,6 +95,11 @@ class TestImageFile(PillowTestCase): def test_raise_ioerror(self): self.assertRaises(IOError, lambda: ImageFile.raise_ioerror(1)) + def test_raise_typeerror(self): + with self.assertRaises(TypeError): + parser = ImageFile.Parser() + parser.feed(1) + def test_truncated_with_errors(self): if "zip_encoder" not in codecs: self.skipTest("PNG (zlib) encoder not available") @@ -135,5 +140,87 @@ class TestImageFile(PillowTestCase): finally: ImageFile.LOAD_TRUNCATED_IMAGES = False + +class MockPyDecoder(ImageFile.PyDecoder): + def decode(self, buffer): + # eof + return (-1, 0) + +xoff, yoff, xsize, ysize = 10, 20, 100, 100 + + +class MockImageFile(ImageFile.ImageFile): + def _open(self): + self.rawmode = 'RGBA' + self.mode = 'RGBA' + self.size = (200, 200) + self.tile = [("MOCK", (xoff, yoff, xoff+xsize, yoff+ysize), 32, None)] + + +class TestPyDecoder(PillowTestCase): + + def get_decoder(self): + decoder = MockPyDecoder(None) + + def closure(mode, *args): + decoder.__init__(mode, *args) + return decoder + + Image.register_decoder('MOCK', closure) + return decoder + + def test_setimage(self): + buf = BytesIO(b'\x00'*255) + + im = MockImageFile(buf) + d = self.get_decoder() + + im.load() + + self.assertEqual(d.state.xoff, xoff) + self.assertEqual(d.state.yoff, yoff) + self.assertEqual(d.state.xsize, xsize) + self.assertEqual(d.state.ysize, ysize) + + self.assertRaises(ValueError, lambda: d.set_as_raw(b'\x00')) + + def test_extents_none(self): + buf = BytesIO(b'\x00'*255) + + im = MockImageFile(buf) + im.tile = [("MOCK", None, 32, None)] + d = self.get_decoder() + + im.load() + + self.assertEqual(d.state.xoff, 0) + self.assertEqual(d.state.yoff, 0) + self.assertEqual(d.state.xsize, 200) + self.assertEqual(d.state.ysize, 200) + + def test_negsize(self): + buf = BytesIO(b'\x00'*255) + + im = MockImageFile(buf) + im.tile = [("MOCK", (xoff, yoff, -10, yoff+ysize), 32, None)] + d = self.get_decoder() + + self.assertRaises(ValueError, im.load) + + im.tile = [("MOCK", (xoff, yoff, xoff+xsize, -10), 32, None)] + self.assertRaises(ValueError, im.load) + + def test_oversize(self): + buf = BytesIO(b'\x00'*255) + + im = MockImageFile(buf) + im.tile = [("MOCK", (xoff, yoff, xoff+xsize + 100, yoff+ysize), 32, None)] + d = self.get_decoder() + + self.assertRaises(ValueError, im.load) + + im.tile = [("MOCK", (xoff, yoff, xoff+xsize, yoff+ysize + 100), 32, None)] + self.assertRaises(ValueError, im.load) + if __name__ == '__main__': unittest.main() diff --git a/Tests/test_imagefont.py b/Tests/test_imagefont.py index de89ac929..fde312f44 100644 --- a/Tests/test_imagefont.py +++ b/Tests/test_imagefont.py @@ -1,7 +1,6 @@ from helper import unittest, PillowTestCase -from PIL import Image -from PIL import ImageDraw +from PIL import Image, ImageDraw, ImageFont, features from io import BytesIO import os import sys @@ -12,461 +11,488 @@ FONT_SIZE = 20 TEST_TEXT = "hey you\nyou are awesome\nthis looks awkward" +HAS_FREETYPE = features.check('freetype2') +HAS_RAQM = features.check('raqm') -try: - from PIL import ImageFont - ImageFont.core.getfont # check if freetype is available - class SimplePatcher(object): - def __init__(self, parent_obj, attr_name, value): - self._parent_obj = parent_obj - self._attr_name = attr_name - self._saved = None +class SimplePatcher(object): + def __init__(self, parent_obj, attr_name, value): + self._parent_obj = parent_obj + self._attr_name = attr_name + self._saved = None + self._is_saved = False + self._value = value + + def __enter__(self): + # Patch the attr on the object + if hasattr(self._parent_obj, self._attr_name): + self._saved = getattr(self._parent_obj, self._attr_name) + setattr(self._parent_obj, self._attr_name, self._value) + self._is_saved = True + else: + setattr(self._parent_obj, self._attr_name, self._value) self._is_saved = False - self._value = value - def __enter__(self): - # Patch the attr on the object - if hasattr(self._parent_obj, self._attr_name): - self._saved = getattr(self._parent_obj, self._attr_name) - setattr(self._parent_obj, self._attr_name, self._value) - self._is_saved = True - else: - setattr(self._parent_obj, self._attr_name, self._value) - self._is_saved = False + def __exit__(self, type, value, traceback): + # Restore the original value + if self._is_saved: + setattr(self._parent_obj, self._attr_name, self._saved) + else: + delattr(self._parent_obj, self._attr_name) - def __exit__(self, type, value, traceback): - # Restore the original value - if self._is_saved: - setattr(self._parent_obj, self._attr_name, self._saved) - else: - delattr(self._parent_obj, self._attr_name) +@unittest.skipUnless(HAS_FREETYPE, "ImageFont not Available") +class TestImageFont(PillowTestCase): + LAYOUT_ENGINE = ImageFont.LAYOUT_BASIC - class TestImageFont(PillowTestCase): + # Freetype has different metrics depending on the version. + # (and, other things, but first things first) + METRICS = { ('2', '3'): {'multiline': 30, + 'textsize': 12, + 'getters': (13, 16)}, + ('2', '7'): {'multiline': 6.2, + 'textsize': 2.5, + 'getters': (12, 16)}, + ('2', '8'): {'multiline': 6.2, + 'textsize': 2.5, + 'getters': (12, 16)}, + 'Default': {'multiline': 0.5, + 'textsize': 0.5, + 'getters': (12, 16)}, + } - def test_sanity(self): - self.assertRegexpMatches( - ImageFont.core.freetype2_version, r"\d+\.\d+\.\d+$") + def setUp(self): + freetype_version = tuple(ImageFont.core.freetype2_version.split('.'))[:2] + self.metrics = self.METRICS.get(freetype_version, self.METRICS['Default']) - def test_font_properties(self): - ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) - self.assertEqual(ttf.path, FONT_PATH) - self.assertEqual(ttf.size, FONT_SIZE) + def get_font(self): + return ImageFont.truetype(FONT_PATH, FONT_SIZE, + layout_engine=self.LAYOUT_ENGINE) - ttf_copy = ttf.font_variant() - self.assertEqual(ttf_copy.path, FONT_PATH) - self.assertEqual(ttf_copy.size, FONT_SIZE) + def test_sanity(self): + self.assertRegexpMatches( + ImageFont.core.freetype2_version, r"\d+\.\d+\.\d+$") - ttf_copy = ttf.font_variant(size=FONT_SIZE+1) - self.assertEqual(ttf_copy.size, FONT_SIZE+1) + def test_font_properties(self): + ttf = self.get_font() + self.assertEqual(ttf.path, FONT_PATH) + self.assertEqual(ttf.size, FONT_SIZE) - second_font_path = "Tests/fonts/DejaVuSans.ttf" - ttf_copy = ttf.font_variant(font=second_font_path) - self.assertEqual(ttf_copy.path, second_font_path) + ttf_copy = ttf.font_variant() + self.assertEqual(ttf_copy.path, FONT_PATH) + self.assertEqual(ttf_copy.size, FONT_SIZE) - def test_font_with_name(self): - ImageFont.truetype(FONT_PATH, FONT_SIZE) - self._render(FONT_PATH) - self._clean() + ttf_copy = ttf.font_variant(size=FONT_SIZE+1) + self.assertEqual(ttf_copy.size, FONT_SIZE+1) - def _font_as_bytes(self): - with open(FONT_PATH, 'rb') as f: - font_bytes = BytesIO(f.read()) - return font_bytes + second_font_path = "Tests/fonts/DejaVuSans.ttf" + ttf_copy = ttf.font_variant(font=second_font_path) + self.assertEqual(ttf_copy.path, second_font_path) - def test_font_with_filelike(self): - ImageFont.truetype(self._font_as_bytes(), FONT_SIZE) - self._render(self._font_as_bytes()) - # Usage note: making two fonts from the same buffer fails. - # shared_bytes = self._font_as_bytes() - # self._render(shared_bytes) - # self.assertRaises(Exception, lambda: _render(shared_bytes)) - self._clean() + def test_font_with_name(self): + self.get_font() + self._render(FONT_PATH) - def test_font_with_open_file(self): - with open(FONT_PATH, 'rb') as f: - self._render(f) - self._clean() + def _font_as_bytes(self): + with open(FONT_PATH, 'rb') as f: + font_bytes = BytesIO(f.read()) + return font_bytes - def _render(self, font): - txt = "Hello World!" - ttf = ImageFont.truetype(font, FONT_SIZE) - ttf.getsize(txt) + def test_font_with_filelike(self): + ImageFont.truetype(self._font_as_bytes(), FONT_SIZE, + layout_engine=self.LAYOUT_ENGINE) + self._render(self._font_as_bytes()) + # Usage note: making two fonts from the same buffer fails. + # shared_bytes = self._font_as_bytes() + # self._render(shared_bytes) + # self.assertRaises(Exception, lambda: _render(shared_bytes)) - img = Image.new("RGB", (256, 64), "white") - d = ImageDraw.Draw(img) - d.text((10, 10), txt, font=ttf, fill='black') + def test_font_with_open_file(self): + with open(FONT_PATH, 'rb') as f: + self._render(f) - img.save('font.png') - return img + def _render(self, font): + txt = "Hello World!" + ttf = ImageFont.truetype(font, FONT_SIZE, + layout_engine=self.LAYOUT_ENGINE) + ttf.getsize(txt) - def _clean(self): - os.unlink('font.png') + img = Image.new("RGB", (256, 64), "white") + d = ImageDraw.Draw(img) + d.text((10, 10), txt, font=ttf, fill='black') - def test_render_equal(self): - img_path = self._render(FONT_PATH) - with open(FONT_PATH, 'rb') as f: - font_filelike = BytesIO(f.read()) - img_filelike = self._render(font_filelike) + return img - self.assert_image_equal(img_path, img_filelike) - self._clean() + def test_render_equal(self): + img_path = self._render(FONT_PATH) + with open(FONT_PATH, 'rb') as f: + font_filelike = BytesIO(f.read()) + img_filelike = self._render(font_filelike) - def test_textsize_equal(self): + self.assert_image_equal(img_path, img_filelike) + + def test_textsize_equal(self): + im = Image.new(mode='RGB', size=(300, 100)) + draw = ImageDraw.Draw(im) + ttf = self.get_font() + + txt = "Hello World!" + size = draw.textsize(txt, ttf) + draw.text((10, 10), txt, font=ttf) + draw.rectangle((10, 10, 10 + size[0], 10 + size[1])) + del draw + + target = 'Tests/images/rectangle_surrounding_text.png' + target_img = Image.open(target) + + # Epsilon ~.5 fails with FreeType 2.7 + self.assert_image_similar(im, target_img, self.metrics['textsize']) + + def test_render_multiline(self): + im = Image.new(mode='RGB', size=(300, 100)) + draw = ImageDraw.Draw(im) + ttf = self.get_font() + line_spacing = draw.textsize('A', font=ttf)[1] + 4 + lines = TEST_TEXT.split("\n") + y = 0 + for line in lines: + draw.text((0, y), line, font=ttf) + y += line_spacing + + target = 'Tests/images/multiline_text.png' + target_img = Image.open(target) + + # some versions of freetype have different horizontal spacing. + # setting a tight epsilon, I'm showing the original test failure + # at epsilon = ~38. + self.assert_image_similar(im, target_img, self.metrics['multiline']) + + def test_render_multiline_text(self): + ttf = self.get_font() + + # Test that text() correctly connects to multiline_text() + # and that align defaults to left + im = Image.new(mode='RGB', size=(300, 100)) + draw = ImageDraw.Draw(im) + draw.text((0, 0), TEST_TEXT, font=ttf) + + target = 'Tests/images/multiline_text.png' + target_img = Image.open(target) + + # Epsilon ~.5 fails with FreeType 2.7 + self.assert_image_similar(im, target_img, self.metrics['multiline']) + + # Test that text() can pass on additional arguments + # to multiline_text() + draw.text((0, 0), TEST_TEXT, fill=None, font=ttf, anchor=None, + spacing=4, align="left") + draw.text((0, 0), TEST_TEXT, None, ttf, None, 4, "left") + del draw + + # Test align center and right + for align, ext in {"center": "_center", + "right": "_right"}.items(): im = Image.new(mode='RGB', size=(300, 100)) draw = ImageDraw.Draw(im) - ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) - - txt = "Hello World!" - size = draw.textsize(txt, ttf) - draw.text((10, 10), txt, font=ttf) - draw.rectangle((10, 10, 10 + size[0], 10 + size[1])) + draw.multiline_text((0, 0), TEST_TEXT, font=ttf, align=align) del draw - target = 'Tests/images/rectangle_surrounding_text.png' - target_img = Image.open(target) - self.assert_image_similar(im, target_img, .5) - - def test_render_multiline(self): - im = Image.new(mode='RGB', size=(300, 100)) - draw = ImageDraw.Draw(im) - ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) - line_spacing = draw.textsize('A', font=ttf)[1] + 4 - lines = TEST_TEXT.split("\n") - y = 0 - for line in lines: - draw.text((0, y), line, font=ttf) - y += line_spacing - - target = 'Tests/images/multiline_text.png' + target = 'Tests/images/multiline_text'+ext+'.png' target_img = Image.open(target) - # some versions of freetype have different horizontal spacing. - # setting a tight epsilon, I'm showing the original test failure - # at epsilon = ~38. - self.assert_image_similar(im, target_img, .5) + # Epsilon ~.5 fails with FreeType 2.7 + self.assert_image_similar(im, target_img, self.metrics['multiline']) - def test_render_multiline_text(self): - ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) + def test_unknown_align(self): + im = Image.new(mode='RGB', size=(300, 100)) + draw = ImageDraw.Draw(im) + ttf = self.get_font() - # Test that text() correctly connects to multiline_text() - # and that align defaults to left - im = Image.new(mode='RGB', size=(300, 100)) - draw = ImageDraw.Draw(im) - draw.text((0, 0), TEST_TEXT, font=ttf) + # Act/Assert + self.assertRaises(AssertionError, + lambda: draw.multiline_text((0, 0), TEST_TEXT, + font=ttf, + align="unknown")) - target = 'Tests/images/multiline_text.png' - target_img = Image.open(target) + def test_multiline_size(self): + ttf = self.get_font() + im = Image.new(mode='RGB', size=(300, 100)) + draw = ImageDraw.Draw(im) - self.assert_image_similar(im, target_img, .5) + # Test that textsize() correctly connects to multiline_textsize() + self.assertEqual(draw.textsize(TEST_TEXT, font=ttf), + draw.multiline_textsize(TEST_TEXT, font=ttf)) - # Test that text() can pass on additional arguments - # to multiline_text() - draw.text((0, 0), TEST_TEXT, fill=None, font=ttf, anchor=None, - spacing=4, align="left") - draw.text((0, 0), TEST_TEXT, None, ttf, None, 4, "left") - del draw + # Test that textsize() can pass on additional arguments + # to multiline_textsize() + draw.textsize(TEST_TEXT, font=ttf, spacing=4) + draw.textsize(TEST_TEXT, ttf, 4) + del draw - # Test align center and right - for align, ext in {"center": "_center", - "right": "_right"}.items(): - im = Image.new(mode='RGB', size=(300, 100)) - draw = ImageDraw.Draw(im) - draw.multiline_text((0, 0), TEST_TEXT, font=ttf, align=align) - del draw + def test_multiline_width(self): + ttf = self.get_font() + im = Image.new(mode='RGB', size=(300, 100)) + draw = ImageDraw.Draw(im) - target = 'Tests/images/multiline_text'+ext+'.png' - target_img = Image.open(target) + self.assertEqual(draw.textsize("longest line", font=ttf)[0], + draw.multiline_textsize("longest line\nline", + font=ttf)[0]) + del draw - self.assert_image_similar(im, target_img, .5) + def test_multiline_spacing(self): + ttf = self.get_font() - def test_unknown_align(self): - im = Image.new(mode='RGB', size=(300, 100)) - draw = ImageDraw.Draw(im) - ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) + im = Image.new(mode='RGB', size=(300, 100)) + draw = ImageDraw.Draw(im) + draw.multiline_text((0, 0), TEST_TEXT, font=ttf, spacing=10) + del draw - # Act/Assert - self.assertRaises(AssertionError, - lambda: draw.multiline_text((0, 0), TEST_TEXT, - font=ttf, - align="unknown")) + target = 'Tests/images/multiline_text_spacing.png' + target_img = Image.open(target) - def test_multiline_size(self): - ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) - im = Image.new(mode='RGB', size=(300, 100)) - draw = ImageDraw.Draw(im) + # Epsilon ~.5 fails with FreeType 2.7 + self.assert_image_similar(im, target_img, self.metrics['multiline']) - # Test that textsize() correctly connects to multiline_textsize() - self.assertEqual(draw.textsize(TEST_TEXT, font=ttf), - draw.multiline_textsize(TEST_TEXT, font=ttf)) + def test_rotated_transposed_font(self): + img_grey = Image.new("L", (100, 100)) + draw = ImageDraw.Draw(img_grey) + word = "testing" + font = self.get_font() - # Test that textsize() can pass on additional arguments - # to multiline_textsize() - draw.textsize(TEST_TEXT, font=ttf, spacing=4) - draw.textsize(TEST_TEXT, ttf, 4) - del draw + orientation = Image.ROTATE_90 + transposed_font = ImageFont.TransposedFont( + font, orientation=orientation) - def test_multiline_width(self): - ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) - im = Image.new(mode='RGB', size=(300, 100)) - draw = ImageDraw.Draw(im) + # Original font + draw.font = font + box_size_a = draw.textsize(word) - self.assertEqual(draw.textsize("longest line", font=ttf)[0], - draw.multiline_textsize("longest line\nline", - font=ttf)[0]) - del draw + # Rotated font + draw.font = transposed_font + box_size_b = draw.textsize(word) + del draw - def test_multiline_spacing(self): - ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) + # Check (w,h) of box a is (h,w) of box b + self.assertEqual(box_size_a[0], box_size_b[1]) + self.assertEqual(box_size_a[1], box_size_b[0]) - im = Image.new(mode='RGB', size=(300, 100)) - draw = ImageDraw.Draw(im) - draw.multiline_text((0, 0), TEST_TEXT, font=ttf, spacing=10) - del draw + def test_unrotated_transposed_font(self): + img_grey = Image.new("L", (100, 100)) + draw = ImageDraw.Draw(img_grey) + word = "testing" + font = self.get_font() - target = 'Tests/images/multiline_text_spacing.png' - target_img = Image.open(target) + orientation = None + transposed_font = ImageFont.TransposedFont( + font, orientation=orientation) - self.assert_image_similar(im, target_img, .5) + # Original font + draw.font = font + box_size_a = draw.textsize(word) - def test_rotated_transposed_font(self): - img_grey = Image.new("L", (100, 100)) - draw = ImageDraw.Draw(img_grey) - word = "testing" - font = ImageFont.truetype(FONT_PATH, FONT_SIZE) + # Rotated font + draw.font = transposed_font + box_size_b = draw.textsize(word) + del draw - orientation = Image.ROTATE_90 - transposed_font = ImageFont.TransposedFont( - font, orientation=orientation) + # Check boxes a and b are same size + self.assertEqual(box_size_a, box_size_b) - # Original font - draw.font = font - box_size_a = draw.textsize(word) + def test_rotated_transposed_font_get_mask(self): + # Arrange + text = "mask this" + font = self.get_font() + orientation = Image.ROTATE_90 + transposed_font = ImageFont.TransposedFont( + font, orientation=orientation) - # Rotated font - draw.font = transposed_font - box_size_b = draw.textsize(word) - del draw + # Act + mask = transposed_font.getmask(text) - # Check (w,h) of box a is (h,w) of box b - self.assertEqual(box_size_a[0], box_size_b[1]) - self.assertEqual(box_size_a[1], box_size_b[0]) + # Assert + self.assertEqual(mask.size, (13, 108)) - def test_unrotated_transposed_font(self): - img_grey = Image.new("L", (100, 100)) - draw = ImageDraw.Draw(img_grey) - word = "testing" - font = ImageFont.truetype(FONT_PATH, FONT_SIZE) + def test_unrotated_transposed_font_get_mask(self): + # Arrange + text = "mask this" + font = self.get_font() + orientation = None + transposed_font = ImageFont.TransposedFont( + font, orientation=orientation) - orientation = None - transposed_font = ImageFont.TransposedFont( - font, orientation=orientation) + # Act + mask = transposed_font.getmask(text) - # Original font - draw.font = font - box_size_a = draw.textsize(word) + # Assert + self.assertEqual(mask.size, (108, 13)) - # Rotated font - draw.font = transposed_font - box_size_b = draw.textsize(word) - del draw + def test_free_type_font_get_name(self): + # Arrange + font = self.get_font() - # Check boxes a and b are same size - self.assertEqual(box_size_a, box_size_b) + # Act + name = font.getname() - def test_rotated_transposed_font_get_mask(self): - # Arrange - text = "mask this" - font = ImageFont.truetype(FONT_PATH, FONT_SIZE) - orientation = Image.ROTATE_90 - transposed_font = ImageFont.TransposedFont( - font, orientation=orientation) + # Assert + self.assertEqual(('FreeMono', 'Regular'), name) - # Act - mask = transposed_font.getmask(text) + def test_free_type_font_get_metrics(self): + # Arrange + font = self.get_font() - # Assert - self.assertEqual(mask.size, (13, 108)) + # Act + ascent, descent = font.getmetrics() - def test_unrotated_transposed_font_get_mask(self): - # Arrange - text = "mask this" - font = ImageFont.truetype(FONT_PATH, FONT_SIZE) - orientation = None - transposed_font = ImageFont.TransposedFont( - font, orientation=orientation) + # Assert + self.assertIsInstance(ascent, int) + self.assertIsInstance(descent, int) + self.assertEqual((ascent, descent), (16, 4)) # too exact check? - # Act - mask = transposed_font.getmask(text) + def test_free_type_font_get_offset(self): + # Arrange + font = self.get_font() + text = "offset this" - # Assert - self.assertEqual(mask.size, (108, 13)) + # Act + offset = font.getoffset(text) - def test_free_type_font_get_name(self): - # Arrange - font = ImageFont.truetype(FONT_PATH, FONT_SIZE) + # Assert + self.assertEqual(offset, (0, 3)) - # Act - name = font.getname() + def test_free_type_font_get_mask(self): + # Arrange + font = self.get_font() + text = "mask this" - # Assert - self.assertEqual(('FreeMono', 'Regular'), name) + # Act + mask = font.getmask(text) - def test_free_type_font_get_metrics(self): - # Arrange - font = ImageFont.truetype(FONT_PATH, FONT_SIZE) + # Assert + self.assertEqual(mask.size, (108, 13)) - # Act - ascent, descent = font.getmetrics() + def test_load_path_not_found(self): + # Arrange + filename = "somefilenamethatdoesntexist.ttf" - # Assert - self.assertIsInstance(ascent, int) - self.assertIsInstance(descent, int) - self.assertEqual((ascent, descent), (16, 4)) # too exact check? + # Act/Assert + self.assertRaises(IOError, lambda: ImageFont.load_path(filename)) - def test_free_type_font_get_offset(self): - # Arrange - font = ImageFont.truetype(FONT_PATH, FONT_SIZE) - text = "offset this" + def test_default_font(self): + # Arrange + txt = 'This is a "better than nothing" default font.' + im = Image.new(mode='RGB', size=(300, 100)) + draw = ImageDraw.Draw(im) - # Act - offset = font.getoffset(text) + target = 'Tests/images/default_font.png' + target_img = Image.open(target) - # Assert - self.assertEqual(offset, (0, 3)) + # Act + default_font = ImageFont.load_default() + draw.text((10, 10), txt, font=default_font) + del draw - def test_free_type_font_get_mask(self): - # Arrange - font = ImageFont.truetype(FONT_PATH, FONT_SIZE) - text = "mask this" + # Assert + self.assert_image_equal(im, target_img) - # Act - mask = font.getmask(text) + def test_getsize_empty(self): + font = self.get_font() + # should not crash. + self.assertEqual((0, 0), font.getsize('')) - # Assert - self.assertEqual(mask.size, (108, 13)) + def _test_fake_loading_font(self, path_to_fake, fontname): + # Make a copy of FreeTypeFont so we can patch the original + free_type_font = copy.deepcopy(ImageFont.FreeTypeFont) + with SimplePatcher(ImageFont, '_FreeTypeFont', free_type_font): + def loadable_font(filepath, size, index, encoding, *args, **kwargs): + if filepath == path_to_fake: + return ImageFont._FreeTypeFont(FONT_PATH, size, index, + encoding, *args, **kwargs) + return ImageFont._FreeTypeFont(filepath, size, index, + encoding, *args, **kwargs) + with SimplePatcher(ImageFont, 'FreeTypeFont', loadable_font): + font = ImageFont.truetype(fontname) + # Make sure it's loaded + name = font.getname() + self.assertEqual(('FreeMono', 'Regular'), name) - def test_load_path_not_found(self): - # Arrange - filename = "somefilenamethatdoesntexist.ttf" - - # Act/Assert - self.assertRaises(IOError, lambda: ImageFont.load_path(filename)) - - def test_default_font(self): - # Arrange - txt = 'This is a "better than nothing" default font.' - im = Image.new(mode='RGB', size=(300, 100)) - draw = ImageDraw.Draw(im) - - target = 'Tests/images/default_font.png' - target_img = Image.open(target) - - # Act - default_font = ImageFont.load_default() - draw.text((10, 10), txt, font=default_font) - del draw - - # Assert - self.assert_image_equal(im, target_img) - - def _test_fake_loading_font(self, path_to_fake, fontname): - # Make a copy of FreeTypeFont so we can patch the original - free_type_font = copy.deepcopy(ImageFont.FreeTypeFont) - with SimplePatcher(ImageFont, '_FreeTypeFont', free_type_font): - def loadable_font(filepath, size, index, encoding): - if filepath == path_to_fake: - return ImageFont._FreeTypeFont(FONT_PATH, size, index, - encoding) - return ImageFont._FreeTypeFont(filepath, size, index, - encoding) - with SimplePatcher(ImageFont, 'FreeTypeFont', loadable_font): - font = ImageFont.truetype(fontname) - # Make sure it's loaded - name = font.getname() - self.assertEqual(('FreeMono', 'Regular'), name) - - @unittest.skipIf(sys.platform.startswith('win32'), - "requires Unix or MacOS") - def test_find_linux_font(self): - # A lot of mocking here - this is more for hitting code and - # catching syntax like errors - font_directory = '/usr/local/share/fonts' - with SimplePatcher(sys, 'platform', 'linux'): - patched_env = copy.deepcopy(os.environ) - patched_env['XDG_DATA_DIRS'] = '/usr/share/:/usr/local/share/' - with SimplePatcher(os, 'environ', patched_env): - def fake_walker(path): - if path == font_directory: - return [(path, [], [ - 'Arial.ttf', 'Single.otf', 'Duplicate.otf', - 'Duplicate.ttf'], )] - return [(path, [], ['some_random_font.ttf'], )] - with SimplePatcher(os, 'walk', fake_walker): - # Test that the font loads both with and without the - # extension - self._test_fake_loading_font( - font_directory+'/Arial.ttf', 'Arial.ttf') - self._test_fake_loading_font( - font_directory+'/Arial.ttf', 'Arial') - - # Test that non-ttf fonts can be found without the - # extension - self._test_fake_loading_font( - font_directory+'/Single.otf', 'Single') - - # Test that ttf fonts are preferred if the extension is - # not specified - self._test_fake_loading_font( - font_directory+'/Duplicate.ttf', 'Duplicate') - - @unittest.skipIf(sys.platform.startswith('win32'), - "requires Unix or MacOS") - def test_find_macos_font(self): - # Like the linux test, more cover hitting code rather than testing - # correctness. - font_directory = '/System/Library/Fonts' - with SimplePatcher(sys, 'platform', 'darwin'): + @unittest.skipIf(sys.platform.startswith('win32'), + "requires Unix or MacOS") + def test_find_linux_font(self): + # A lot of mocking here - this is more for hitting code and + # catching syntax like errors + font_directory = '/usr/local/share/fonts' + with SimplePatcher(sys, 'platform', 'linux'): + patched_env = copy.deepcopy(os.environ) + patched_env['XDG_DATA_DIRS'] = '/usr/share/:/usr/local/share/' + with SimplePatcher(os, 'environ', patched_env): def fake_walker(path): if path == font_directory: - return [(path, [], - ['Arial.ttf', 'Single.otf', - 'Duplicate.otf', 'Duplicate.ttf'], )] + return [(path, [], [ + 'Arial.ttf', 'Single.otf', 'Duplicate.otf', + 'Duplicate.ttf'], )] return [(path, [], ['some_random_font.ttf'], )] with SimplePatcher(os, 'walk', fake_walker): + # Test that the font loads both with and without the + # extension self._test_fake_loading_font( font_directory+'/Arial.ttf', 'Arial.ttf') self._test_fake_loading_font( font_directory+'/Arial.ttf', 'Arial') + + # Test that non-ttf fonts can be found without the + # extension self._test_fake_loading_font( font_directory+'/Single.otf', 'Single') + + # Test that ttf fonts are preferred if the extension is + # not specified self._test_fake_loading_font( font_directory+'/Duplicate.ttf', 'Duplicate') - def test_imagefont_getters(self): - # Arrange - t = ImageFont.truetype(FONT_PATH, FONT_SIZE) + @unittest.skipIf(sys.platform.startswith('win32'), + "requires Unix or MacOS") + def test_find_macos_font(self): + # Like the linux test, more cover hitting code rather than testing + # correctness. + font_directory = '/System/Library/Fonts' + with SimplePatcher(sys, 'platform', 'darwin'): + def fake_walker(path): + if path == font_directory: + return [(path, [], + ['Arial.ttf', 'Single.otf', + 'Duplicate.otf', 'Duplicate.ttf'], )] + return [(path, [], ['some_random_font.ttf'], )] + with SimplePatcher(os, 'walk', fake_walker): + self._test_fake_loading_font( + font_directory+'/Arial.ttf', 'Arial.ttf') + self._test_fake_loading_font( + font_directory+'/Arial.ttf', 'Arial') + self._test_fake_loading_font( + font_directory+'/Single.otf', 'Single') + self._test_fake_loading_font( + font_directory+'/Duplicate.ttf', 'Duplicate') - # Act / Assert - self.assertEqual(t.getmetrics(), (16, 4)) - self.assertEqual(t.font.ascent, 16) - self.assertEqual(t.font.descent, 4) - self.assertEqual(t.font.height, 20) - self.assertEqual(t.font.x_ppem, 20) - self.assertEqual(t.font.y_ppem, 20) - self.assertEqual(t.font.glyphs, 4177) - self.assertEqual(t.getsize('A'), (12, 16)) - self.assertEqual(t.getsize('AB'), (24, 16)) - self.assertEqual(t.getsize('M'), (12, 16)) - self.assertEqual(t.getsize('y'), (12, 20)) - self.assertEqual(t.getsize('a'), (12, 16)) + def test_imagefont_getters(self): + # Arrange + t = self.get_font() + + # Act / Assert + self.assertEqual(t.getmetrics(), (16, 4)) + self.assertEqual(t.font.ascent, 16) + self.assertEqual(t.font.descent, 4) + self.assertEqual(t.font.height, 20) + self.assertEqual(t.font.x_ppem, 20) + self.assertEqual(t.font.y_ppem, 20) + self.assertEqual(t.font.glyphs, 4177) + self.assertEqual(t.getsize('A'), (12, 16)) + self.assertEqual(t.getsize('AB'), (24, 16)) + self.assertEqual(t.getsize('M'), self.metrics['getters']) + self.assertEqual(t.getsize('y'), (12, 20)) + self.assertEqual(t.getsize('a'), (12, 16)) -except ImportError: - class TestImageFont(PillowTestCase): - def test_skip(self): - self.skipTest("ImportError") - +@unittest.skipUnless(HAS_RAQM, "Raqm not Available") +class TestImageFont_RaqmLayout(TestImageFont): + LAYOUT_ENGINE = ImageFont.LAYOUT_RAQM if __name__ == '__main__': unittest.main() diff --git a/Tests/test_imagefontctl.py b/Tests/test_imagefontctl.py new file mode 100644 index 000000000..79122f6c1 --- /dev/null +++ b/Tests/test_imagefontctl.py @@ -0,0 +1,133 @@ +# -*- coding: utf-8 -*- +from helper import unittest, PillowTestCase +from PIL import Image, ImageDraw, ImageFont, features + + +FONT_SIZE = 20 +FONT_PATH = "Tests/fonts/DejaVuSans.ttf" + +@unittest.skipUnless(features.check('raqm'), "Raqm Library is not installed.") +class TestImagecomplextext(PillowTestCase): + + def test_english(self): + #smoke test, this should not fail + ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) + im = Image.new(mode='RGB', size=(300, 100)) + draw = ImageDraw.Draw(im) + draw.text((0, 0), 'TEST', font=ttf, fill=500, direction='ltr') + + + def test_complex_text(self): + ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) + + im = Image.new(mode='RGB', size=(300, 100)) + draw = ImageDraw.Draw(im) + draw.text((0, 0), 'اهلا عمان', font=ttf, fill=500) + + target = 'Tests/images/test_text.png' + target_img = Image.open(target) + + self.assert_image_similar(im, target_img, .5) + + def test_y_offset(self): + ttf = ImageFont.truetype("Tests/fonts/NotoNastaliqUrdu-Regular.ttf", FONT_SIZE) + + im = Image.new(mode='RGB', size=(300, 100)) + draw = ImageDraw.Draw(im) + draw.text((0, 0), 'العالم العربي', font=ttf, fill=500) + + target = 'Tests/images/test_y_offset.png' + target_img = Image.open(target) + + self.assert_image_similar(im, target_img, 1.7) + + def test_complex_unicode_text(self): + ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) + + im = Image.new(mode='RGB', size=(300, 100)) + draw = ImageDraw.Draw(im) + draw.text((0, 0), 'السلام عليكم', font=ttf, fill=500) + + target = 'Tests/images/test_complex_unicode_text.png' + target_img = Image.open(target) + + self.assert_image_similar(im, target_img, .5) + + def test_text_direction_rtl(self): + ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) + + im = Image.new(mode='RGB', size=(300, 100)) + draw = ImageDraw.Draw(im) + draw.text((0, 0), 'English عربي', font=ttf, fill=500, direction='rtl') + + target = 'Tests/images/test_direction_rtl.png' + target_img = Image.open(target) + + self.assert_image_similar(im, target_img, .5) + + def test_text_direction_ltr(self): + ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) + + im = Image.new(mode='RGB', size=(300, 100)) + draw = ImageDraw.Draw(im) + draw.text((0, 0), 'سلطنة عمان Oman', font=ttf, fill=500, direction='ltr') + + target = 'Tests/images/test_direction_ltr.png' + target_img = Image.open(target) + + self.assert_image_similar(im, target_img, .5) + + def test_text_direction_rtl2(self): + ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) + + im = Image.new(mode='RGB', size=(300, 100)) + draw = ImageDraw.Draw(im) + draw.text((0, 0), 'Oman سلطنة عمان', font=ttf, fill=500, direction='rtl') + + target = 'Tests/images/test_direction_ltr.png' + target_img = Image.open(target) + + self.assert_image_similar(im, target_img, .5) + + def test_ligature_features(self): + ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) + + im = Image.new(mode='RGB', size=(300, 100)) + draw = ImageDraw.Draw(im) + draw.text((0, 0), 'filling', font=ttf, fill=500, features=['-liga']) + target = 'Tests/images/test_ligature_features.png' + target_img = Image.open(target) + + self.assert_image_similar(im, target_img, .5) + + liga_size = ttf.getsize('fi', features=['-liga']) + self.assertEqual(liga_size,(13,19)) + + def test_kerning_features(self): + ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) + + im = Image.new(mode='RGB', size=(300, 100)) + draw = ImageDraw.Draw(im) + draw.text((0, 0), 'TeToAV', font=ttf, fill=500, features=['-kern']) + + target = 'Tests/images/test_kerning_features.png' + target_img = Image.open(target) + + self.assert_image_similar(im, target_img, .5) + + def test_arabictext_features(self): + ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) + + im = Image.new(mode='RGB', size=(300, 100)) + draw = ImageDraw.Draw(im) + draw.text((0, 0), 'اللغة العربية', font=ttf, fill=500, features=['-fina','-init','-medi']) + + target = 'Tests/images/test_arabictext_features.png' + target_img = Image.open(target) + + self.assert_image_similar(im, target_img, .5) + +if __name__ == '__main__': + unittest.main() + +# End of file diff --git a/Tests/test_imagegrab.py b/Tests/test_imagegrab.py index 840df31c2..b2edffa57 100644 --- a/Tests/test_imagegrab.py +++ b/Tests/test_imagegrab.py @@ -38,7 +38,7 @@ class TestImageGrabImport(PillowTestCase): # Assert if sys.platform in ["win32", "darwin"]: - self.assertIsNone(exception, None) + self.assertIsNone(exception) else: self.assertIsInstance(exception, ImportError) self.assertEqual(str(exception), diff --git a/Tests/test_imagemorph.py b/Tests/test_imagemorph.py index ea54417a2..0ce8ac6da 100644 --- a/Tests/test_imagemorph.py +++ b/Tests/test_imagemorph.py @@ -73,10 +73,18 @@ class MorphTests(PillowTestCase): 'corner', 'dilation4', 'dilation8', 'erosion4', 'erosion8', 'edge'): lb = ImageMorph.LutBuilder(op_name=op) + self.assertIsNone(lb.get_lut()) + lut = lb.build_lut() with open('Tests/images/%s.lut' % op, 'rb') as f: self.assertEqual(lut, bytearray(f.read())) + def test_no_operator_loaded(self): + mop = ImageMorph.MorphOp() + self.assertRaises(Exception, lambda: mop.apply(None)) + self.assertRaises(Exception, lambda: mop.match(None)) + self.assertRaises(Exception, lambda: mop.save_lut(None)) + # Test the named patterns def test_erosion8(self): # erosion8 @@ -168,6 +176,40 @@ class MorphTests(PillowTestCase): self.assertEqual(len(coords), 4) self.assertEqual(tuple(coords), ((2, 2), (4, 2), (2, 4), (4, 4))) + def test_mirroring(self): + # Test 'M' for mirroring + mop = ImageMorph.MorphOp(patterns=['1:(... ... ...)->0', + 'M:(00. 01. ...)->1']) + count, Aout = mop.apply(self.A) + self.assertEqual(count, 7) + self.assert_img_equal_img_string(Aout, + """ + ....... + ....... + ..1.1.. + ....... + ....... + ....... + ....... + """) + + def test_negate(self): + # Test 'N' for negate + mop = ImageMorph.MorphOp(patterns=['1:(... ... ...)->0', + 'N:(00. 01. ...)->1']) + count, Aout = mop.apply(self.A) + self.assertEqual(count, 8) + self.assert_img_equal_img_string(Aout, + """ + ....... + ....... + ..1.... + ....... + ....... + ....... + ....... + """) + def test_non_binary_images(self): im = hopper('RGB') mop = ImageMorph.MorphOp(op_name="erosion8") @@ -176,6 +218,74 @@ class MorphTests(PillowTestCase): self.assertRaises(Exception, lambda: mop.match(im)) self.assertRaises(Exception, lambda: mop.get_on_pixels(im)) + def test_add_patterns(self): + # Arrange + lb = ImageMorph.LutBuilder(op_name='corner') + self.assertEqual(lb.patterns, ['1:(... ... ...)->0', + '4:(00. 01. ...)->1']) + new_patterns = ['M:(00. 01. ...)->1', + 'N:(00. 01. ...)->1'] + + # Act + lb.add_patterns(new_patterns) + + # Assert + self.assertEqual( + lb.patterns, + ['1:(... ... ...)->0', + '4:(00. 01. ...)->1', + 'M:(00. 01. ...)->1', + 'N:(00. 01. ...)->1']) + + def test_unknown_pattern(self): + self.assertRaises( + Exception, + lambda: ImageMorph.LutBuilder(op_name='unknown')) + + def test_pattern_syntax_error(self): + # Arrange + lb = ImageMorph.LutBuilder(op_name='corner') + new_patterns = ['a pattern with a syntax error'] + lb.add_patterns(new_patterns) + + # Act / Assert + self.assertRaises( + Exception, + lambda: lb.build_lut()) + + def test_load_invalid_mrl(self): + # Arrange + invalid_mrl = 'Tests/images/hopper.png' + mop = ImageMorph.MorphOp() + + # Act / Assert + self.assertRaises(Exception, lambda: mop.load_lut(invalid_mrl)) + + def test_roundtrip_mrl(self): + # Arrange + tempfile = self.tempfile('temp.mrl') + mop = ImageMorph.MorphOp(op_name='corner') + initial_lut = mop.lut + + # Act + mop.save_lut(tempfile) + mop.load_lut(tempfile) + + # Act / Assert + self.assertEqual(mop.lut, initial_lut) + + def test_set_lut(self): + # Arrange + lb = ImageMorph.LutBuilder(op_name='corner') + lut = lb.build_lut() + mop = ImageMorph.MorphOp() + + # Act + mop.set_lut(lut) + + # Assert + self.assertEqual(mop.lut, lut) + if __name__ == '__main__': unittest.main() diff --git a/Tests/test_imagepalette.py b/Tests/test_imagepalette.py index e26c242b0..248138377 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_imagesequence.py b/Tests/test_imagesequence.py index 0877bfb8a..e50b13924 100644 --- a/Tests/test_imagesequence.py +++ b/Tests/test_imagesequence.py @@ -58,12 +58,10 @@ class TestImageSequence(PillowTestCase): for frame in ImageSequence.Iterator(im): if firstFrame is None: firstFrame = frame.copy() - pass for frame in ImageSequence.Iterator(im): self.assert_image_equal(frame, firstFrame) break - def test_palette_mmap(self): # Using mmap in ImageFile can require to reload the palette. im = Image.open('Tests/images/multipage-mmap.tiff') diff --git a/Tests/test_imageshow.py b/Tests/test_imageshow.py index 4424fa0eb..342c63851 100644 --- a/Tests/test_imageshow.py +++ b/Tests/test_imageshow.py @@ -10,6 +10,13 @@ class TestImageShow(PillowTestCase): dir(Image) dir(ImageShow) + def test_viewer(self): + viewer = ImageShow.Viewer() + + self.assertIsNone(viewer.get_format(None)) + + self.assertRaises(NotImplementedError, lambda: viewer.get_command(None)) + if __name__ == '__main__': unittest.main() diff --git a/Tests/test_imagetk.py b/Tests/test_imagetk.py index f56333a59..fbf48a1b6 100644 --- a/Tests/test_imagetk.py +++ b/Tests/test_imagetk.py @@ -1,22 +1,30 @@ -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" @@ -37,7 +45,45 @@ class TestImageTk(PillowTestCase): # Test no relevant entry im = ImageTk._get_image_from_kw(kw) - self.assertEqual(im, None) + self.assertIsNone(im) + + 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__': diff --git a/Tests/test_imagewin.py b/Tests/test_imagewin.py index d802b1fb2..70bf28247 100644 --- a/Tests/test_imagewin.py +++ b/Tests/test_imagewin.py @@ -107,15 +107,6 @@ class TestImageWinDib(PillowTestCase): # Confirm they're the same self.assertEqual(dib1.tobytes(), dib2.tobytes()) - def test_removed_methods(self): - # Arrange - im = hopper() - dib = ImageWin.Dib(im) - - # Act/Assert - self.assertRaises(Exception, dib.tostring) - self.assertRaises(Exception, dib.fromstring) - if __name__ == '__main__': unittest.main() 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_numpy.py b/Tests/test_numpy.py index 370b8c1f9..8b9208cd6 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 @@ -100,7 +100,7 @@ class TestNumpy(PillowTestCase): self.assert_image(to_image(numpy.uint8, 4), "RGBA", (10, 10)) # based on an erring example at - # http://stackoverflow.com/questions/10854903/what-is-causing-dimension-dependent-attributeerror-in-pil-fromarray-function + # https://stackoverflow.com/questions/10854903/what-is-causing-dimension-dependent-attributeerror-in-pil-fromarray-function def test_3d_array(self): size = (5, TEST_IMAGE_SIZE[0], TEST_IMAGE_SIZE[1]) a = numpy.ones(size, dtype=numpy.uint8) @@ -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. @@ -199,6 +204,14 @@ class TestNumpy(PillowTestCase): self.assertEqual(len(im.getdata()), len(arr)) + def test_zero_size(self): + # Shouldn't cause floating point exception + # See https://github.com/python-pillow/Pillow/issues/2259 + + im = Image.fromarray(numpy.empty((0, 0), dtype=numpy.uint8)) + + self.assertEqual(im.size, (0, 0)) + if __name__ == '__main__': unittest.main() diff --git a/Tests/test_olefileio.py b/Tests/test_olefileio.py deleted file mode 100644 index cb0496db4..000000000 --- a/Tests/test_olefileio.py +++ /dev/null @@ -1,147 +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.assertIsNone(ctime) - self.assertIsInstance(mtime, datetime.datetime) - 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..17fa3662b 100644 --- a/Tests/test_psdraw.py +++ b/Tests/test_psdraw.py @@ -31,16 +31,14 @@ class TestPsDraw(PillowTestCase): def test_draw_postscript(self): # Based on Pillow tutorial, but there is no textsize: - # https://pillow.readthedocs.io/en/latest/handbook/tutorial.html + # https://pillow.readthedocs.io/en/latest/handbook/tutorial.html#drawing-postscript # 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 8be16c518..1f4f016d1 100644 --- a/Tests/test_scipy.py +++ b/Tests/test_scipy.py @@ -1,10 +1,11 @@ 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]]) diff --git a/Tests/test_tiff_ifdrational.py b/Tests/test_tiff_ifdrational.py index 7e2c908b5..0bf4503c4 100644 --- a/Tests/test_tiff_ifdrational.py +++ b/Tests/test_tiff_ifdrational.py @@ -1,5 +1,3 @@ -from __future__ import print_function - from helper import unittest, PillowTestCase, hopper from PIL import TiffImagePlugin, Image @@ -36,10 +34,10 @@ class Test_IFDRational(PillowTestCase): xres = IFDRational(72) yres = IFDRational(72) - self.assertTrue(xres._val is not None) - self.assertTrue(xres.numerator is not None) - self.assertTrue(xres.denominator is not None) - self.assertTrue(yres._val is not None) + self.assertIsNotNone(xres._val) + self.assertIsNotNone(xres.numerator) + self.assertIsNotNone(xres.denominator) + self.assertIsNotNone(yres._val) self.assertTrue(xres and 1) self.assertTrue(xres and yres) @@ -47,7 +45,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 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 6c612cfe9..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) diff --git a/_imaging.c b/_imaging.c index f093f598d..ad083840d 100644 --- a/_imaging.c +++ b/_imaging.c @@ -71,8 +71,6 @@ * See the README file for information on usage and redistribution. */ -#define PILLOW_VERSION "3.5.0.dev0" - #include "Python.h" #ifdef HAVE_LIBZ @@ -96,8 +94,6 @@ #define WITH_THREADING /* "friendly" threading support */ #define WITH_UNSHARPMASK /* Kevin Cazabon's unsharpmask module */ -#define WITH_DEBUG /* extra debugging interfaces */ - #undef VERBOSE #define CLIP(x) ((x) <= 0 ? 0 : (x) < 256 ? (x) : 255) @@ -251,7 +247,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 @@ -631,18 +629,6 @@ _new(PyObject* self, PyObject* args) return PyImagingNew(ImagingNew(mode, xsize, ysize)); } -static PyObject* -_new_array(PyObject* self, PyObject* args) -{ - char* mode; - int xsize, ysize; - - if (!PyArg_ParseTuple(args, "s(ii)", &mode, &xsize, &ysize)) - return NULL; - - return PyImagingNew(ImagingNewArray(mode, xsize, ysize)); -} - static PyObject* _new_block(PyObject* self, PyObject* args) { @@ -2939,8 +2925,6 @@ _getcodecstatus(PyObject* self, PyObject* args) /* -------------------------------------------------------------------- */ -#ifdef WITH_DEBUG - static PyObject* _save_ppm(ImagingObject* self, PyObject* args) { @@ -2956,7 +2940,6 @@ _save_ppm(ImagingObject* self, PyObject* args) return Py_None; } -#endif /* -------------------------------------------------------------------- */ @@ -2996,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}, @@ -3052,12 +3032,9 @@ static struct PyMethodDef methods[] = { #endif /* Misc. */ - {"new_array", (PyCFunction)_new_array, 1}, {"new_block", (PyCFunction)_new_block, 1}, -#ifdef WITH_DEBUG {"save_ppm", (PyCFunction)_save_ppm, 1}, -#endif {NULL, NULL} /* sentinel */ }; @@ -3310,7 +3287,6 @@ extern PyObject* PyImaging_JpegDecoderNew(PyObject* self, PyObject* args); extern PyObject* PyImaging_Jpeg2KDecoderNew(PyObject* self, PyObject* args); extern PyObject* PyImaging_TiffLzwDecoderNew(PyObject* self, PyObject* args); extern PyObject* PyImaging_LibTiffDecoderNew(PyObject* self, PyObject* args); -extern PyObject* PyImaging_MspDecoderNew(PyObject* self, PyObject* args); extern PyObject* PyImaging_PackbitsDecoderNew(PyObject* self, PyObject* args); extern PyObject* PyImaging_PcdDecoderNew(PyObject* self, PyObject* args); extern PyObject* PyImaging_PcxDecoderNew(PyObject* self, PyObject* args); @@ -3388,7 +3364,6 @@ static PyMethodDef functions[] = { {"libtiff_decoder", (PyCFunction)PyImaging_LibTiffDecoderNew, 1}, {"libtiff_encoder", (PyCFunction)PyImaging_LibTiffEncoderNew, 1}, #endif - {"msp_decoder", (PyCFunction)PyImaging_MspDecoderNew, 1}, {"packbits_decoder", (PyCFunction)PyImaging_PackbitsDecoderNew, 1}, {"pcd_decoder", (PyCFunction)PyImaging_PcdDecoderNew, 1}, {"pcx_decoder", (PyCFunction)PyImaging_PcxDecoderNew, 1}, @@ -3459,6 +3434,7 @@ static PyMethodDef functions[] = { static int setup_module(PyObject* m) { PyObject* d = PyModule_GetDict(m); + const char* version = (char*)PILLOW_VERSION; /* Ready object types */ if (PyType_Ready(&Imaging_Type) < 0) @@ -3503,7 +3479,7 @@ setup_module(PyObject* m) { } #endif - PyDict_SetItemString(d, "PILLOW_VERSION", PyUnicode_FromString(PILLOW_VERSION)); + PyDict_SetItemString(d, "PILLOW_VERSION", PyUnicode_FromString(version)); return 0; } diff --git a/_imagingcms.c b/_imagingcms.c index fe905f969..fed455941 100644 --- a/_imagingcms.c +++ b/_imagingcms.c @@ -291,6 +291,97 @@ findLCMStype(char* PILmode) } } +#define Cms_Min(a, b) ((a) < (b) ? (a) : (b)) + +static int +pyCMSgetAuxChannelChannel (cmsUInt32Number format, int auxChannelNdx) +{ + int numColors = T_CHANNELS(format); + int numExtras = T_EXTRA(format); + + if (T_SWAPFIRST(format) && T_DOSWAP(format)) { + // reverse order, before anything but last extra is shifted last + if (auxChannelNdx == numExtras - 1) + return numColors + numExtras - 1; + else + return numExtras - 2 - auxChannelNdx; + } + else if (T_SWAPFIRST(format)) { + // in order, after color channels, but last extra is shifted to first + if (auxChannelNdx == numExtras - 1) + return 0; + else + return numColors + 1 + auxChannelNdx; + } + else if (T_DOSWAP(format)) { + // reverse order, before anything + return numExtras - 1 - auxChannelNdx; + } + else { + // in order, after color channels + return numColors + auxChannelNdx; + } +} + +static void +pyCMScopyAux (cmsHTRANSFORM hTransform, Imaging imDst, const Imaging imSrc) +{ + cmsUInt32Number dstLCMSFormat; + cmsUInt32Number srcLCMSFormat; + int numSrcExtras; + int numDstExtras; + int numExtras; + int ySize; + int xSize; + int channelSize; + int srcChunkSize; + int dstChunkSize; + int e; + + // trivially copied + if (imDst == imSrc) + return; + + dstLCMSFormat = cmsGetTransformOutputFormat(hTransform); + srcLCMSFormat = cmsGetTransformInputFormat(hTransform); + + // currently, all Pillow formats are chunky formats, but check it anyway + if (T_PLANAR(dstLCMSFormat) || T_PLANAR(srcLCMSFormat)) + return; + + // copy only if channel format is identical, except OPTIMIZED is ignored as it + // does not affect the aux channel + if (T_FLOAT(dstLCMSFormat) != T_FLOAT(srcLCMSFormat) + || T_FLAVOR(dstLCMSFormat) != T_FLAVOR(srcLCMSFormat) + || T_ENDIAN16(dstLCMSFormat) != T_ENDIAN16(srcLCMSFormat) + || T_BYTES(dstLCMSFormat) != T_BYTES(srcLCMSFormat)) + return; + + numSrcExtras = T_EXTRA(srcLCMSFormat); + numDstExtras = T_EXTRA(dstLCMSFormat); + numExtras = Cms_Min(numSrcExtras, numDstExtras); + ySize = Cms_Min(imSrc->ysize, imDst->ysize); + xSize = Cms_Min(imSrc->xsize, imDst->xsize); + channelSize = T_BYTES(dstLCMSFormat); + srcChunkSize = (T_CHANNELS(srcLCMSFormat) + T_EXTRA(srcLCMSFormat)) * channelSize; + dstChunkSize = (T_CHANNELS(dstLCMSFormat) + T_EXTRA(dstLCMSFormat)) * channelSize; + + for (e = 0; e < numExtras; ++e) { + int y; + int dstChannel = pyCMSgetAuxChannelChannel(dstLCMSFormat, e); + int srcChannel = pyCMSgetAuxChannelChannel(srcLCMSFormat, e); + + for (y = 0; y < ySize; y++) { + int x; + char* pDstExtras = imDst->image[y] + dstChannel * channelSize; + const char* pSrcExtras = imSrc->image[y] + srcChannel * channelSize; + + for (x = 0; x < xSize; x++) + memcpy(pDstExtras + x * dstChunkSize, pSrcExtras + x * srcChunkSize, channelSize); + } + } +} + static int pyCMSdoTransform(Imaging im, Imaging imOut, cmsHTRANSFORM hTransform) { @@ -301,9 +392,19 @@ pyCMSdoTransform(Imaging im, Imaging imOut, cmsHTRANSFORM hTransform) Py_BEGIN_ALLOW_THREADS + // transform color channels only for (i = 0; i < im->ysize; i++) cmsDoTransform(hTransform, im->image[i], imOut->image[i], im->xsize); + // lcms by default does nothing to the auxiliary channels leaving those + // unchanged. To do "the right thing" here, i.e. maintain identical results + // with and without inPlace, we replicate those channels to the output. + // + // As of lcms 2.8, a new cmsFLAGS_COPY_ALPHA flag is introduced which would + // do the same thing automagically. Unfortunately, lcms2.8 is not yet widely + // enough available on all platforms, so we polyfill it here for now. + pyCMScopyAux(hTransform, imOut, im); + Py_END_ALLOW_THREADS return 0; @@ -727,7 +828,7 @@ static cmsBool _calculate_rgb_primaries(CmsProfileObject* self, cmsCIEXYZTRIPLE* cmsHPROFILE hXYZ; cmsHTRANSFORM hTransform; - /* http://littlecms2.blogspot.com/2009/07/less-is-more.html */ + /* https://littlecms2.blogspot.com/2009/07/less-is-more.html */ // double array of RGB values with max on each identity hXYZ = cmsCreateXYZProfile(); @@ -1505,7 +1606,7 @@ PyInit__imagingcms(void) { if (setup_module(m) < 0) return NULL; - + PyDateTime_IMPORT; return m; diff --git a/_imagingft.c b/_imagingft.c index ae62fc74e..3eb8bc0b7 100644 --- a/_imagingft.c +++ b/_imagingft.c @@ -41,6 +41,18 @@ #define FT_ERRORDEF( e, v, s ) { e, s }, #define FT_ERROR_START_LIST { #define FT_ERROR_END_LIST { 0, 0 } }; +#ifdef HAVE_RAQM +#include +#endif + +#define LAYOUT_FALLBACK 0 +#define LAYOUT_RAQM 1 + +typedef struct +{ + int index, x_offset, x_advance, y_offset; + unsigned int cluster; +} GlyphInfo; struct { int code; @@ -58,6 +70,7 @@ typedef struct { PyObject_HEAD FT_Face face; unsigned char *font_bytes; + int layout_engine; } FontObject; static PyTypeObject Font_Type; @@ -91,11 +104,13 @@ getfont(PyObject* self_, PyObject* args, PyObject* kw) char* filename = NULL; int size; int index = 0; + int layout_engine = 0; unsigned char* encoding; unsigned char* font_bytes; int font_bytes_size = 0; static char* kwlist[] = { - "filename", "size", "index", "encoding", "font_bytes", NULL + "filename", "size", "index", "encoding", "font_bytes", + "layout_engine", NULL }; if (!library) { @@ -106,10 +121,10 @@ getfont(PyObject* self_, PyObject* args, PyObject* kw) return NULL; } - if (!PyArg_ParseTupleAndKeywords(args, kw, "eti|iss#", kwlist, + if (!PyArg_ParseTupleAndKeywords(args, kw, "eti|iss#i", kwlist, Py_FileSystemDefaultEncoding, &filename, &size, &index, &encoding, &font_bytes, - &font_bytes_size)) { + &font_bytes_size, &layout_engine)) { return NULL; } @@ -121,6 +136,7 @@ getfont(PyObject* self_, PyObject* args, PyObject* kw) } self->face = NULL; + self->layout_engine = layout_engine; if (filename && font_bytes_size <= 0) { self->font_bytes = NULL; @@ -188,60 +204,286 @@ font_getchar(PyObject* string, int index, FT_ULong* char_out) return 0; } -static PyObject* -font_getsize(FontObject* self, PyObject* args) +#ifdef HAVE_RAQM +static size_t +text_layout_raqm(PyObject* string, FontObject* self, const char* dir, + PyObject *features ,GlyphInfo **glyph_info, int mask) { - int i, x, y_max, y_min; + int i = 0; + raqm_t *rq; + size_t count = 0; + raqm_glyph_t *glyphs; + raqm_direction_t direction; + + rq = raqm_create(); + if (rq == NULL) { + PyErr_SetString(PyExc_ValueError, "raqm_create() failed."); + goto failed; + } + + if (PyUnicode_Check(string)) { + Py_UNICODE *text = PyUnicode_AS_UNICODE(string); + Py_ssize_t size = PyUnicode_GET_SIZE(string); + if (!raqm_set_text(rq, (const uint32_t *)(text), size)) { + PyErr_SetString(PyExc_ValueError, "raqm_set_text() failed"); + goto failed; + } + } +#if PY_VERSION_HEX < 0x03000000 + else if (PyString_Check(string)) { + char *text = PyString_AS_STRING(string); + int size = PyString_GET_SIZE(string); + if (!raqm_set_text_utf8(rq, text, size)) { + PyErr_SetString(PyExc_ValueError, "raqm_set_text_utf8() failed"); + goto failed; + } + } +#endif + else { + PyErr_SetString(PyExc_TypeError, "expected string"); + goto failed; + } + + direction = RAQM_DIRECTION_DEFAULT; + if (dir) { + if (strcmp(dir, "rtl") == 0) + direction = RAQM_DIRECTION_RTL; + else if (strcmp(dir, "ltr") == 0) + direction = RAQM_DIRECTION_LTR; + else if (strcmp(dir, "ttb") == 0) + direction = RAQM_DIRECTION_TTB; + else { + PyErr_SetString(PyExc_ValueError, "direction must be either 'rtl', 'ltr' or 'ttb'"); + goto failed; + } + } + + if (!raqm_set_par_direction(rq, direction)) { + PyErr_SetString(PyExc_ValueError, "raqm_set_par_direction() failed"); + goto failed; + } + + if (features != Py_None) { + int len; + PyObject *seq = PySequence_Fast(features, "expected a sequence"); + if (!seq) { + goto failed; + } + + len = PySequence_Size(seq); + for (i = 0; i < len; i++) { + PyObject *item = PySequence_Fast_GET_ITEM(seq, i); + char *feature = NULL; + Py_ssize_t size = 0; + PyObject *bytes; + +#if PY_VERSION_HEX >= 0x03000000 + if (!PyUnicode_Check(item)) { +#else + if (!PyUnicode_Check(item) && !PyString_Check(item)) { +#endif + PyErr_SetString(PyExc_TypeError, "expected a string"); + goto failed; + } + + if (PyUnicode_Check(item)) { + bytes = PyUnicode_AsUTF8String(item); + if (bytes == NULL) + goto failed; + feature = PyBytes_AS_STRING(bytes); + size = PyBytes_GET_SIZE(bytes); + } +#if PY_VERSION_HEX < 0x03000000 + else { + feature = PyString_AsString(item); + size = PyString_GET_SIZE(item); + } +#endif + if (!raqm_add_font_feature(rq, feature, size)) { + PyErr_SetString(PyExc_ValueError, "raqm_add_font_feature() failed"); + goto failed; + } + } + } + + if (!raqm_set_freetype_face(rq, self->face)) { + PyErr_SetString(PyExc_RuntimeError, "raqm_set_freetype_face() failed."); + goto failed; + } + + if (!raqm_layout (rq)) { + PyErr_SetString(PyExc_RuntimeError, "raqm_layout() failed."); + goto failed; + } + + glyphs = raqm_get_glyphs(rq, &count); + if (glyphs == NULL) { + PyErr_SetString(PyExc_ValueError, "raqm_get_glyphs() failed."); + count = 0; + goto failed; + } + + (*glyph_info) = PyMem_New(GlyphInfo, count); + if ((*glyph_info) == NULL) { + PyErr_SetString(PyExc_MemoryError, "PyMem_New() failed"); + count = 0; + goto failed; + } + + for (i = 0; i < count; i++) { + (*glyph_info)[i].index = glyphs[i].index; + (*glyph_info)[i].x_offset = glyphs[i].x_offset; + (*glyph_info)[i].x_advance = glyphs[i].x_advance; + (*glyph_info)[i].y_offset = glyphs[i].y_offset; + (*glyph_info)[i].cluster = glyphs[i].cluster; + } + +failed: + raqm_destroy (rq); + return count; +} +#endif + +static size_t +text_layout_fallback(PyObject* string, FontObject* self, const char* dir, + PyObject *features ,GlyphInfo **glyph_info, int mask) +{ + int error, load_flags; FT_ULong ch; - FT_Face face; - int xoffset, yoffset; + Py_ssize_t count; + FT_GlyphSlot glyph; FT_Bool kerning = FT_HAS_KERNING(self->face); FT_UInt last_index = 0; + int i; - /* calculate size and bearing for a given string */ - - PyObject* string; - if (!PyArg_ParseTuple(args, "O:getsize", &string)) - return NULL; - + if (features != Py_None || dir != NULL) { + PyErr_SetString(PyExc_KeyError, "setting text direction or font features is not supported without libraqm"); + } #if PY_VERSION_HEX >= 0x03000000 if (!PyUnicode_Check(string)) { #else if (!PyUnicode_Check(string) && !PyString_Check(string)) { #endif PyErr_SetString(PyExc_TypeError, "expected string"); - return NULL; + return 0; } + count = 0; + while (font_getchar(string, count, &ch)) { + count++; + } + if (count == 0) { + return 0; + } + + (*glyph_info) = PyMem_New(GlyphInfo, count); + if ((*glyph_info) == NULL) { + PyErr_SetString(PyExc_MemoryError, "PyMem_New() failed"); + return 0; + } + + load_flags = FT_LOAD_RENDER|FT_LOAD_NO_BITMAP; + if (mask) { + load_flags |= FT_LOAD_TARGET_MONO; + } + for (i = 0; font_getchar(string, i, &ch); i++) { + (*glyph_info)[i].index = FT_Get_Char_Index(self->face, ch); + error = FT_Load_Glyph(self->face, (*glyph_info)[i].index, load_flags); + if (error) { + geterror(error); + return 0; + } + glyph = self->face->glyph; + (*glyph_info)[i].x_offset=0; + (*glyph_info)[i].y_offset=0; + if (kerning && last_index && (*glyph_info)[i].index) { + FT_Vector delta; + if (FT_Get_Kerning(self->face, last_index, (*glyph_info)[i].index, + ft_kerning_default,&delta) == 0) + (*glyph_info)[i-1].x_advance += PIXEL(delta.x); + } + + (*glyph_info)[i].x_advance = glyph->metrics.horiAdvance; + last_index = (*glyph_info)[i].index; + (*glyph_info)[i].cluster = ch; + } + return count; +} + +static size_t +text_layout(PyObject* string, FontObject* self, const char* dir, + PyObject *features, GlyphInfo **glyph_info, int mask) +{ + size_t count; +#ifdef HAVE_RAQM + if (self->layout_engine == LAYOUT_RAQM) { + count = text_layout_raqm(string, self, dir, features, glyph_info, mask); + } else { + count = text_layout_fallback(string, self, dir, features, glyph_info, mask); + } +#else + count = text_layout_fallback(string, self, dir, features, glyph_info, mask); +#endif + return count; +} + +static PyObject* +font_getsize(FontObject* self, PyObject* args) +{ + int i, x, y_max, y_min; + FT_Face face; + int xoffset, yoffset; + const char *dir = NULL; + size_t count; + GlyphInfo *glyph_info = NULL; + PyObject *features = Py_None; + + /* calculate size and bearing for a given string */ + + PyObject* string; + if (!PyArg_ParseTuple(args, "O|zO:getsize", &string, &dir, &features)) + return NULL; + face = NULL; xoffset = yoffset = 0; y_max = y_min = 0; - for (x = i = 0; font_getchar(string, i, &ch); i++) { + count = text_layout(string, self, dir, features, &glyph_info, 0); + + for (x = i = 0; i < count; i++) { int index, error; FT_BBox bbox; FT_Glyph glyph; face = self->face; - index = FT_Get_Char_Index(face, ch); - if (kerning && last_index && index) { - FT_Vector delta; - FT_Get_Kerning(self->face, last_index, index, ft_kerning_default, - &delta); - x += delta.x; - } - - /* Note: bitmap fonts within ttf fonts do not work, see #891/pr#960 - * Yifu Yu, 2014-10-15 - */ + index = glyph_info[i].index; + /* Note: bitmap fonts within ttf fonts do not work, see #891/pr#960 + * Yifu Yu, 2014-10-15 + */ error = FT_Load_Glyph(face, index, FT_LOAD_DEFAULT|FT_LOAD_NO_BITMAP); if (error) return geterror(error); - if (i == 0) + + if (i == 0 && face->glyph->metrics.horiBearingX < 0) { xoffset = face->glyph->metrics.horiBearingX; - x += face->glyph->metrics.horiAdvance; + x -= xoffset; + } + + x += glyph_info[i].x_advance; + + if (i == count - 1) + { + int offset; + offset = glyph_info[i].x_advance - + face->glyph->metrics.width - + face->glyph->metrics.horiBearingX; + if (offset < 0) + x -= offset; + } FT_Get_Glyph(face->glyph, &glyph); FT_Glyph_Get_CBox(glyph, FT_GLYPH_BBOX_SUBPIXELS, &bbox); + bbox.yMax -= glyph_info[i].y_offset; + bbox.yMin -= glyph_info[i].y_offset; if (bbox.yMax > y_max) y_max = bbox.yMax; if (bbox.yMin < y_min) @@ -251,23 +493,16 @@ font_getsize(FontObject* self, PyObject* args) if (face->glyph->metrics.horiBearingY > yoffset) yoffset = face->glyph->metrics.horiBearingY; - last_index = index; FT_Done_Glyph(glyph); } if (face) { - int offset; + /* left bearing */ if (xoffset < 0) x -= xoffset; else xoffset = 0; - /* right bearing */ - offset = face->glyph->metrics.horiAdvance - - face->glyph->metrics.width - - face->glyph->metrics.horiBearingX; - if (offset < 0) - x -= offset; /* difference between the font ascender and the distance of * the baseline from the top */ yoffset = PIXEL(self->face->size->metrics.ascender - yoffset); @@ -306,7 +541,7 @@ font_getabc(FontObject* self, PyObject* args) int index, error; face = self->face; index = FT_Get_Char_Index(face, ch); - /* Note: bitmap fonts within ttf fonts do not work, see #891/pr#960 */ + /* Note: bitmap fonts within ttf fonts do not work, see #891/pr#960 */ error = FT_Load_Glyph(face, index, FT_LOAD_DEFAULT|FT_LOAD_NO_BITMAP); if (error) return geterror(error); @@ -329,11 +564,7 @@ font_render(FontObject* self, PyObject* args) int index, error, ascender; int load_flags; unsigned char *source; - FT_ULong ch; FT_GlyphSlot glyph; - FT_Bool kerning = FT_HAS_KERNING(self->face); - FT_UInt last_index = 0; - /* render string into given buffer (the buffer *must* have the right size, or this will crash) */ PyObject* string; @@ -341,15 +572,18 @@ font_render(FontObject* self, PyObject* args) int mask = 0; int temp; int xx, x0, x1; - if (!PyArg_ParseTuple(args, "On|i:render", &string, &id, &mask)) - return NULL; + const char *dir = NULL; + size_t count; + GlyphInfo *glyph_info; + PyObject *features = NULL; -#if PY_VERSION_HEX >= 0x03000000 - if (!PyUnicode_Check(string)) { -#else - if (!PyUnicode_Check(string) && !PyString_Check(string)) { -#endif - PyErr_SetString(PyExc_TypeError, "expected string"); + if (!PyArg_ParseTuple(args, "On|izO:render", &string, &id, &mask, &dir, &features)) { + return NULL; + } + + glyph_info = NULL; + count = text_layout(string, self, dir, features, &glyph_info, mask); + if (count == 0) { return NULL; } @@ -360,36 +594,37 @@ font_render(FontObject* self, PyObject* args) load_flags |= FT_LOAD_TARGET_MONO; ascender = 0; - for (i = 0; font_getchar(string, i, &ch); i++) { - index = FT_Get_Char_Index(self->face, ch); + for (i = 0; i < count; i++) { + index = glyph_info[i].index; error = FT_Load_Glyph(self->face, index, load_flags); if (error) return geterror(error); + glyph = self->face->glyph; temp = (glyph->bitmap.rows - glyph->bitmap_top); + temp -= PIXEL(glyph_info[i].y_offset); if (temp > ascender) ascender = temp; } - for (x = i = 0; font_getchar(string, i, &ch); i++) { + for (x = i = 0; i < count; i++) { if (i == 0 && self->face->glyph->metrics.horiBearingX < 0) - x = -PIXEL(self->face->glyph->metrics.horiBearingX); - index = FT_Get_Char_Index(self->face, ch); - if (kerning && last_index && index) { - FT_Vector delta; - FT_Get_Kerning(self->face, last_index, index, ft_kerning_default, - &delta); - x += delta.x >> 6; - } + x = -self->face->glyph->metrics.horiBearingX; + index = glyph_info[i].index; error = FT_Load_Glyph(self->face, index, load_flags); if (error) return geterror(error); + if (i == 0 && self->face->glyph->metrics.horiBearingX < 0) { + x = -self->face->glyph->metrics.horiBearingX; + } + glyph = self->face->glyph; source = (unsigned char*) glyph->bitmap.buffer; - xx = x + glyph->bitmap_left; + xx = PIXEL(x) + glyph->bitmap_left; + xx += PIXEL(glyph_info[i].x_offset); x0 = 0; x1 = glyph->bitmap.width; if (xx < 0) @@ -401,6 +636,7 @@ font_render(FontObject* self, PyObject* args) /* use monochrome mask (on palette images, etc) */ for (y = 0; y < glyph->bitmap.rows; y++) { int yy = y + im->ysize - (PIXEL(glyph->metrics.horiBearingY) + ascender); + yy -= PIXEL(glyph_info[i].y_offset); if (yy >= 0 && yy < im->ysize) { /* blend this glyph into the buffer */ unsigned char *target = im->image8[yy] + xx; @@ -420,8 +656,10 @@ font_render(FontObject* self, PyObject* args) /* use antialiased rendering */ for (y = 0; y < glyph->bitmap.rows; y++) { int yy = y + im->ysize - (PIXEL(glyph->metrics.horiBearingY) + ascender); + yy -= PIXEL(glyph_info[i].y_offset); if (yy >= 0 && yy < im->ysize) { /* blend this glyph into the buffer */ + int i; unsigned char *target = im->image8[yy] + xx; for (i = x0; i < x1; i++) { @@ -432,10 +670,10 @@ font_render(FontObject* self, PyObject* args) source += glyph->bitmap.pitch; } } - x += PIXEL(glyph->metrics.horiAdvance); - last_index = index; + x += glyph_info[i].x_advance; } + PyMem_Del(glyph_info); Py_RETURN_NONE; } @@ -593,6 +831,14 @@ setup_module(PyObject* m) { #endif PyDict_SetItemString(d, "freetype2_version", v); + +#ifdef HAVE_RAQM + v = PyBool_FromLong(1); +#else + v = PyBool_FromLong(0); +#endif + PyDict_SetItemString(d, "HAVE_RAQM", v); + return 0; } 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/_webp.c b/_webp.c index a8c6d40af..421f49957 100644 --- a/_webp.c +++ b/_webp.c @@ -1,5 +1,6 @@ #define PY_SSIZE_T_CLEAN #include +#include "Imaging.h" #include "py3.h" #include #include @@ -28,7 +29,7 @@ PyObject* WebPEncode_wrapper(PyObject* self, PyObject* args) if (!PyArg_ParseTuple(args, "s#iiifss#s#", (char**)&rgb, &size, &width, &height, &lossless, &quality_factor, &mode, &icc_bytes, &icc_size, &exif_bytes, &exif_size)) { - Py_RETURN_NONE; + return NULL; } if (strcmp(mode, "RGBA")==0){ if (size < width * height * 4){ @@ -144,7 +145,7 @@ PyObject* WebPDecode_wrapper(PyObject* self, PyObject* args) char* mode = "RGB"; if (!PyArg_ParseTuple(args, "S", &webp_string)) { - Py_RETURN_NONE; + return NULL; } if (!WebPInitDecoderConfig(&config)) { @@ -246,8 +247,12 @@ PyObject* WebPDecoderVersion_wrapper(PyObject* self, PyObject* args){ * The version of webp that ships with (0.1.3) Ubuntu 12.04 doesn't handle alpha well. * Files that are valid with 0.3 are reported as being invalid. */ +int WebPDecoderBuggyAlpha() { + return WebPGetDecoderVersion()==0x0103; +} + PyObject* WebPDecoderBuggyAlpha_wrapper(PyObject* self, PyObject* args){ - return Py_BuildValue("i", WebPGetDecoderVersion()==0x0103); + return Py_BuildValue("i", WebPDecoderBuggyAlpha()); } static PyMethodDef webpMethods[] = @@ -267,6 +272,11 @@ void addMuxFlagToModule(PyObject* m) { #endif } +void addTransparencyFlagToModule(PyObject* m) { + PyModule_AddObject(m, "HAVE_TRANSPARENCY", + PyBool_FromLong(!WebPDecoderBuggyAlpha())); +} + #if PY_VERSION_HEX >= 0x03000000 PyMODINIT_FUNC @@ -283,6 +293,7 @@ PyInit__webp(void) { m = PyModule_Create(&module_def); addMuxFlagToModule(m); + addTransparencyFlagToModule(m); return m; } #else @@ -291,5 +302,6 @@ init_webp(void) { PyObject* m = Py_InitModule("_webp", webpMethods); addMuxFlagToModule(m); + addTransparencyFlagToModule(m); } #endif diff --git a/appveyor.yml b/appveyor.yml index 9ff661224..18c179983 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,13 +1,20 @@ -version: 3.5.pre.{build} +version: '{build}' clone_folder: c:\pillow init: - ECHO %PYTHON% #- ps: iex ((new-object net.webclient).DownloadString('https://raw.githubusercontent.com/appveyor/ci/master/scripts/enable-rdp.ps1')) -# Uncomment previous line to get RDP access during the build. +# Uncomment previous line to get RDP access during the build. environment: X64_EXT: -x64 + EXECUTABLE: python.exe + PIP_DIR: Scripts + VENV: NO matrix: + - PYTHON: C:/vp/pypy2 + EXECUTABLE: bin/pypy.exe + PIP_DIR: bin + VENV: YES - PYTHON: C:/Python27-x64 - PYTHON: C:/Python34 - PYTHON: C:/Python27 @@ -15,25 +22,33 @@ environment: - PYTHON: C:/Python33 - PYTHON: C:/Python33-x64 + install: -- git clone https://github.com/python-pillow/pillow-depends.git c:\pillow-depends +- curl -fsSL -o pillow-depends.zip https://github.com/python-pillow/pillow-depends/archive/master.zip +- 7z x pillow-depends.zip -oc:\ +- mv c:\pillow-depends-master 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\ +- ps: | + if ($env:PYTHON -eq "c:/vp/pypy2") + { + c:\pillow\winbuild\appveyor_install_pypy.cmd + } - c:\python34\python.exe c:\pillow\winbuild\build_dep.py - c:\pillow\winbuild\build_deps.cmd build_script: -- '%PYTHON%\python.exe c:\pillow\winbuild\build.py' +- '%PYTHON%\%EXECUTABLE% c:\pillow\winbuild\build.py' - cd c:\pillow - dir dist\*.egg -- '%PYTHON%\python.exe selftest.py --installed' +- '%PYTHON%\%EXECUTABLE% selftest.py --installed' test_script: - cd c:\pillow -- '%PYTHON%\Scripts\pip.exe install nose' -- '%PYTHON%\python.exe test-installed.py -v -s' +- '%PYTHON%\%PIP_DIR%\pip.exe install nose' +- '%PYTHON%\%EXECUTABLE% test-installed.py -v -s' matrix: fast_finish: true @@ -45,23 +60,24 @@ artifacts: name: wheel after_test: - - '%PYTHON%\Scripts\pip.exe install wheel' + - '%PYTHON%\%PIP_DIR%\pip.exe install wheel' - cd c:\pillow\winbuild\ - - '%PYTHON%\python.exe c:\pillow\winbuild\build.py --wheel' + - '%PYTHON%\%EXECUTABLE% c:\pillow\winbuild\build.py --wheel' - cd c:\pillow - ps: Get-ChildItem .\dist\*.* | % { Push-AppveyorArtifact $_.FullName -FileName $_.Name } deploy: provider: S3 - access_key_id: AKIAIRAXC62ZNTVQJMOQ + region: us-west-2 + access_key_id: AKIAIRAXC62ZNTVQJMOQ secret_access_key: secure: Hwb6klTqtBeMgxAjRoDltiiqpuH8xbwD4UooDzBSiCWXjuFj1lyl4kHgHwTCCGqi bucket: pillow-nightly folder: win/$(APPVEYOR_BUILD_NUMBER)/ artifact: /.*egg|wheel/ - on: + on: branch: master - + # Uncomment the following line to get RDP access after the build/test and block for # up to the timeout limit (~1hr) # diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 000000000..db2472009 --- /dev/null +++ b/codecov.yml @@ -0,0 +1 @@ +comment: off diff --git a/decode.c b/decode.c index f700747e1..f749a40a7 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}, @@ -570,27 +557,6 @@ PyImaging_LibTiffDecoderNew(PyObject* self, PyObject* args) #endif -/* -------------------------------------------------------------------- */ -/* MSP */ -/* -------------------------------------------------------------------- */ - -PyObject* -PyImaging_MspDecoderNew(PyObject* self, PyObject* args) -{ - ImagingDecoderObject* decoder; - - decoder = PyImaging_DecoderNew(0); - if (decoder == NULL) - return NULL; - - if (get_unpacker(decoder, "1", "1") < 0) - return NULL; - - decoder->decode = ImagingMspDecode; - - return (PyObject*) decoder; -} - /* -------------------------------------------------------------------- */ /* PackBits */ @@ -813,6 +779,7 @@ PyImaging_ZipDecoderNew(PyObject* self, PyObject* args) return NULL; decoder->decode = ImagingZipDecode; + decoder->cleanup = ImagingZipDecodeCleanup; ((ZIPSTATE*)decoder->state.context)->interlaced = interlaced; @@ -918,7 +885,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/debian_8.2.sh b/depends/debian_8.2.sh index 96e6a8e2b..c4f72bf8e 100755 --- a/depends/debian_8.2.sh +++ b/depends/debian_8.2.sh @@ -11,7 +11,8 @@ sudo apt-get -y install python-dev python-setuptools \ python3-dev python-virtualenv cmake sudo apt-get -y install libtiff5-dev libjpeg62-turbo-dev zlib1g-dev \ libfreetype6-dev liblcms2-dev libwebp-dev tcl8.6-dev tk8.6-dev \ - python-tk python3-tk + python-tk python3-tk libharfbuzz-dev libfribidi-dev ./install_openjpeg.sh ./install_imagequant.sh +./install_raqm.sh diff --git a/depends/download-and-extract.sh b/depends/download-and-extract.sh new file mode 100755 index 000000000..7cc905e85 --- /dev/null +++ b/depends/download-and-extract.sh @@ -0,0 +1,12 @@ +#!/bin/sh +# 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/fedora_23.sh b/depends/fedora_23.sh index bad03e764..5bdcf7f17 100755 --- a/depends/fedora_23.sh +++ b/depends/fedora_23.sh @@ -15,4 +15,4 @@ sudo dnf install python-devel python3-devel python-virtualenv make gcc sudo dnf install libtiff-devel libjpeg-devel zlib-devel freetype-devel \ lcms2-devel libwebp-devel openjpeg2-devel tkinter python3-tkinter \ - tcl-devel tk-devel \ No newline at end of file + tcl-devel tk-devel harfbuzz-devel fribidi-devel libraqm-devel \ No newline at end of file diff --git a/depends/freebsd_10.sh b/depends/freebsd_10.sh index 99b4d6d0f..36d9c1069 100755 --- a/depends/freebsd_10.sh +++ b/depends/freebsd_10.sh @@ -4,8 +4,10 @@ # Installs all of the dependencies for Pillow for Freebsd 10.x # for both system Pythons 2.7 and 3.4 # -sudo pkg install python2 python3 py27-pip py27-virtualenv py27-setuptools27 +sudo pkg install python2 python3 py27-pip py27-virtualenv wget cmake # Openjpeg fails badly using the openjpeg package. # I can't find a python3.4 version of tkinter -sudo pkg install jpeg-turbo tiff webp lcms2 freetype2 py27-tkinter +sudo pkg install jpeg-turbo tiff webp lcms2 freetype2 harfbuzz fribidi py27-tkinter + +./install_raqm_cmake.sh diff --git a/depends/install_extra_test_images.sh b/depends/install_extra_test_images.sh index 00ed219d6..667c74e6d 100755 --- a/depends/install_extra_test_images.sh +++ b/depends/install_extra_test_images.sh @@ -1,11 +1,9 @@ #!/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 + +# Use SVN to just fetch a single git subdirectory +svn checkout https://github.com/python-pillow/pillow-depends/trunk/test_images cp -r test_images/* ../Tests/images diff --git a/depends/install_imagequant.sh b/depends/install_imagequant.sh index dd497dc3c..0b19525d2 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.10.1 -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 8d5f5c010..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.2.tar.gz ]; then - wget -O 'openjpeg-2.1.2.tar.gz' 'https://github.com/python-pillow/pillow-depends/blob/master/openjpeg-2.1.2.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.2 -tar -xvzf openjpeg-2.1.2.tar.gz - - -pushd openjpeg-2.1.2 +pushd $archive cmake -DCMAKE_INSTALL_PREFIX=/usr . && make -j4 && sudo make -j4 install popd - diff --git a/depends/install_raqm.sh b/depends/install_raqm.sh new file mode 100755 index 000000000..e1df5f616 --- /dev/null +++ b/depends/install_raqm.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash +# install raqm + + +archive=raqm-0.2.0 + +./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/master/$archive.tar.gz + +pushd $archive + +./configure --prefix=/usr && make -j4 && sudo make -j4 install + +popd + diff --git a/depends/install_raqm_cmake.sh b/depends/install_raqm_cmake.sh new file mode 100755 index 000000000..0c5ed8b69 --- /dev/null +++ b/depends/install_raqm_cmake.sh @@ -0,0 +1,18 @@ +#!/usr/bin/env bash +# install raqm + + +archive=raqm-cmake-b517ba80 + +./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/master/$archive.tar.gz + +pushd $archive + +mkdir build +cd build +cmake .. +make && sudo make install +cd .. + +popd + diff --git a/depends/install_webp.sh b/depends/install_webp.sh index 356c6e552..9993254a6 100755 --- a/depends/install_webp.sh +++ b/depends/install_webp.sh @@ -1,14 +1,11 @@ #!/bin/bash # install webp -if [ ! -f libwebp-0.5.1.tar.gz ]; then - wget -O 'libwebp-0.5.1.tar.gz' 'https://github.com/python-pillow/pillow-depends/blob/master/libwebp-0.5.1.tar.gz?raw=true' -fi +archive=libwebp-0.6.0 -rm -r libwebp-0.5.1 -tar -xvzf libwebp-0.5.1.tar.gz +./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/master/$archive.tar.gz -pushd libwebp-0.5.1 +pushd $archive ./configure --prefix=/usr --enable-libwebpmux --enable-libwebpdemux && make -j4 && sudo make -j4 install diff --git a/depends/ubuntu_14.04.sh b/depends/ubuntu_14.04.sh index a548f74fa..f7d28fba7 100755 --- a/depends/ubuntu_14.04.sh +++ b/depends/ubuntu_14.04.sh @@ -4,12 +4,13 @@ # Installs all of the dependencies for Pillow for Ubuntu 14.04 # for both system Pythons 2.7 and 3.4 # - +sudo apt-get update sudo apt-get -y install python-dev python-setuptools \ python3-dev python-virtualenv cmake sudo apt-get -y install libtiff5-dev libjpeg8-dev zlib1g-dev \ libfreetype6-dev liblcms2-dev libwebp-dev tcl8.6-dev tk8.6-dev \ - python-tk python3-tk + python-tk python3-tk libharfbuzz-dev libfribidi-dev ./install_openjpeg.sh ./install_imagequant.sh +./install_raqm.sh diff --git a/docs/COPYING b/docs/COPYING index 5d10c7364..2d4f92b41 100644 --- a/docs/COPYING +++ b/docs/COPYING @@ -5,9 +5,9 @@ The Python Imaging Library (PIL) is Pillow is the friendly PIL fork. It is - Copyright © 2016 by Alex Clark and contributors + Copyright © 2010-2017 by Alex Clark and contributors -Like PIL, Pillow is licensed under the MIT-like open source PIL +Like PIL, Pillow is licensed under the open source PIL Software License: By obtaining, using, and/or copying this software and/or its diff --git a/docs/_templates/sidebarhelp.html b/docs/_templates/sidebarhelp.html index e07180a99..111fa5dae 100644 --- a/docs/_templates/sidebarhelp.html +++ b/docs/_templates/sidebarhelp.html @@ -1,4 +1,4 @@

Need help?

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

diff --git a/docs/about.rst b/docs/about.rst index 2f97020ca..dd6ca9a98 100644 --- a/docs/about.rst +++ b/docs/about.rst @@ -18,7 +18,7 @@ The fork author's goal is to foster and support active development of PIL throug License ------- -Like PIL, Pillow is `licensed under the MIT-like open source PIL Software License `_ +Like PIL, Pillow is `licensed under the open source PIL Software License `_ Why a fork? ----------- diff --git a/docs/conf.py b/docs/conf.py index f66bea521..f21e2e51a 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -12,10 +12,6 @@ # All configuration values have a default; values that are commented out # serve to show the default. -import sys -import os -import shlex - # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. @@ -48,8 +44,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/example/DdsImagePlugin.py b/docs/example/DdsImagePlugin.py new file mode 100644 index 000000000..29e13b920 --- /dev/null +++ b/docs/example/DdsImagePlugin.py @@ -0,0 +1,283 @@ +""" +A Pillow loader for .dds files (S3TC-compressed aka DXTC) +Jerome Leclanche + +Documentation: + http://oss.sgi.com/projects/ogl-sample/registry/EXT/texture_compression_s3tc.txt + +The contents of this file are hereby released in the public domain (CC0) +Full text of the CC0 license: + https://creativecommons.org/publicdomain/zero/1.0/ +""" + +import struct +from io import BytesIO +from PIL import Image, ImageFile + + +# Magic ("DDS ") +DDS_MAGIC = 0x20534444 + +# DDS flags +DDSD_CAPS = 0x1 +DDSD_HEIGHT = 0x2 +DDSD_WIDTH = 0x4 +DDSD_PITCH = 0x8 +DDSD_PIXELFORMAT = 0x1000 +DDSD_MIPMAPCOUNT = 0x20000 +DDSD_LINEARSIZE = 0x80000 +DDSD_DEPTH = 0x800000 + +# DDS caps +DDSCAPS_COMPLEX = 0x8 +DDSCAPS_TEXTURE = 0x1000 +DDSCAPS_MIPMAP = 0x400000 + +DDSCAPS2_CUBEMAP = 0x200 +DDSCAPS2_CUBEMAP_POSITIVEX = 0x400 +DDSCAPS2_CUBEMAP_NEGATIVEX = 0x800 +DDSCAPS2_CUBEMAP_POSITIVEY = 0x1000 +DDSCAPS2_CUBEMAP_NEGATIVEY = 0x2000 +DDSCAPS2_CUBEMAP_POSITIVEZ = 0x4000 +DDSCAPS2_CUBEMAP_NEGATIVEZ = 0x8000 +DDSCAPS2_VOLUME = 0x200000 + +# Pixel Format +DDPF_ALPHAPIXELS = 0x1 +DDPF_ALPHA = 0x2 +DDPF_FOURCC = 0x4 +DDPF_PALETTEINDEXED8 = 0x20 +DDPF_RGB = 0x40 +DDPF_LUMINANCE = 0x20000 + + +# dds.h + +DDS_FOURCC = DDPF_FOURCC +DDS_RGB = DDPF_RGB +DDS_RGBA = DDPF_RGB | DDPF_ALPHAPIXELS +DDS_LUMINANCE = DDPF_LUMINANCE +DDS_LUMINANCEA = DDPF_LUMINANCE | DDPF_ALPHAPIXELS +DDS_ALPHA = DDPF_ALPHA +DDS_PAL8 = DDPF_PALETTEINDEXED8 + +DDS_HEADER_FLAGS_TEXTURE = (DDSD_CAPS | DDSD_HEIGHT | DDSD_WIDTH | + DDSD_PIXELFORMAT) +DDS_HEADER_FLAGS_MIPMAP = DDSD_MIPMAPCOUNT +DDS_HEADER_FLAGS_VOLUME = DDSD_DEPTH +DDS_HEADER_FLAGS_PITCH = DDSD_PITCH +DDS_HEADER_FLAGS_LINEARSIZE = DDSD_LINEARSIZE + +DDS_HEIGHT = DDSD_HEIGHT +DDS_WIDTH = DDSD_WIDTH + +DDS_SURFACE_FLAGS_TEXTURE = DDSCAPS_TEXTURE +DDS_SURFACE_FLAGS_MIPMAP = DDSCAPS_COMPLEX | DDSCAPS_MIPMAP +DDS_SURFACE_FLAGS_CUBEMAP = DDSCAPS_COMPLEX + +DDS_CUBEMAP_POSITIVEX = DDSCAPS2_CUBEMAP | DDSCAPS2_CUBEMAP_POSITIVEX +DDS_CUBEMAP_NEGATIVEX = DDSCAPS2_CUBEMAP | DDSCAPS2_CUBEMAP_NEGATIVEX +DDS_CUBEMAP_POSITIVEY = DDSCAPS2_CUBEMAP | DDSCAPS2_CUBEMAP_POSITIVEY +DDS_CUBEMAP_NEGATIVEY = DDSCAPS2_CUBEMAP | DDSCAPS2_CUBEMAP_NEGATIVEY +DDS_CUBEMAP_POSITIVEZ = DDSCAPS2_CUBEMAP | DDSCAPS2_CUBEMAP_POSITIVEZ +DDS_CUBEMAP_NEGATIVEZ = DDSCAPS2_CUBEMAP | DDSCAPS2_CUBEMAP_NEGATIVEZ + + +# DXT1 +DXT1_FOURCC = 0x31545844 + +# DXT3 +DXT3_FOURCC = 0x33545844 + +# DXT5 +DXT5_FOURCC = 0x35545844 + + +def _decode565(bits): + a = ((bits >> 11) & 0x1f) << 3 + b = ((bits >> 5) & 0x3f) << 2 + c = (bits & 0x1f) << 3 + return a, b, c + + +def _c2a(a, b): + return (2 * a + b) // 3 + + +def _c2b(a, b): + return (a + b) // 2 + + +def _c3(a, b): + return (2 * b + a) // 3 + + +def _dxt1(data, width, height): + # TODO implement this function as pixel format in decode.c + ret = bytearray(4 * width * height) + + for y in range(0, height, 4): + for x in range(0, width, 4): + color0, color1, bits = struct.unpack("> 2 + if control == 0: + r, g, b = r0, g0, b0 + elif control == 1: + r, g, b = r1, g1, b1 + elif control == 2: + if color0 > color1: + r, g, b = _c2a(r0, r1), _c2a(g0, g1), _c2a(b0, b1) + else: + r, g, b = _c2b(r0, r1), _c2b(g0, g1), _c2b(b0, b1) + elif control == 3: + if color0 > color1: + r, g, b = _c3(r0, r1), _c3(g0, g1), _c3(b0, b1) + else: + r, g, b = 0, 0, 0 + + idx = 4 * ((y + j) * width + x + i) + ret[idx:idx+4] = struct.pack('4B', r, g, b, 255) + + return bytes(ret) + + +def _dxtc_alpha(a0, a1, ac0, ac1, ai): + if ai <= 12: + ac = (ac0 >> ai) & 7 + elif ai == 15: + ac = (ac0 >> 15) | ((ac1 << 1) & 6) + else: + ac = (ac1 >> (ai - 16)) & 7 + + if ac == 0: + alpha = a0 + elif ac == 1: + alpha = a1 + elif a0 > a1: + alpha = ((8 - ac) * a0 + (ac - 1) * a1) // 7 + elif ac == 6: + alpha = 0 + elif ac == 7: + alpha = 0xff + else: + alpha = ((6 - ac) * a0 + (ac - 1) * a1) // 5 + + return alpha + + +def _dxt5(data, width, height): + # TODO implement this function as pixel format in decode.c + ret = bytearray(4 * width * height) + + for y in range(0, height, 4): + for x in range(0, width, 4): + a0, a1, ac0, ac1, c0, c1, code = struct.unpack("<2BHI2HI", + data.read(16)) + + r0, g0, b0 = _decode565(c0) + r1, g1, b1 = _decode565(c1) + + for j in range(4): + for i in range(4): + ai = 3 * (4 * j + i) + alpha = _dxtc_alpha(a0, a1, ac0, ac1, ai) + + cc = (code >> 2 * (4 * j + i)) & 3 + if cc == 0: + r, g, b = r0, g0, b0 + elif cc == 1: + r, g, b = r1, g1, b1 + elif cc == 2: + r, g, b = _c2a(r0, r1), _c2a(g0, g1), _c2a(b0, b1) + elif cc == 3: + r, g, b = _c3(r0, r1), _c3(g0, g1), _c3(b0, b1) + + idx = 4 * ((y + j) * width + x + i) + ret[idx:idx+4] = struct.pack('4B', r, g, b, alpha) + + return bytes(ret) + + +class DdsImageFile(ImageFile.ImageFile): + format = "DDS" + format_description = "DirectDraw Surface" + + def _open(self): + magic, header_size = struct.unpack(">> from PIL import Image - >>> im = Image.open("lena.ppm") + >>> im = Image.open("hopper.ppm") If successful, this function returns an :py:class:`~PIL.Image.Image` object. You can now use instance attributes to examine the file contents:: @@ -276,7 +276,8 @@ Converting between modes :: - im = Image.open("lena.ppm").convert("L") + from PIL import Image + im = Image.open("hopper.ppm").convert("L") The library supports transformations between each supported mode and the “L” and “RGB” modes. To convert between other modes, you may have to use an @@ -435,8 +436,8 @@ Drawing Postscript from PIL import Image from PIL import PSDraw - im = Image.open("lena.ppm") - title = "lena" + im = Image.open("hopper.ppm") + title = "hopper" box = (1*72, 2*72, 7*72, 10*72) # in points ps = PSDraw.PSDraw() # default is sys.stdout @@ -459,7 +460,8 @@ As described earlier, the :py:func:`~PIL.Image.open` function of the :py:mod:`~PIL.Image` module is used to open an image file. In most cases, you simply pass it the filename as an argument:: - im = Image.open("lena.ppm") + from PIL import Image + im = Image.open("hopper.ppm") If everything goes well, the result is an :py:class:`PIL.Image.Image` object. Otherwise, an :exc:`IOError` exception is raised. @@ -473,8 +475,9 @@ Reading from an open file :: - fp = open("lena.ppm", "rb") - im = Image.open(fp) + from PIL import Image + with open("hopper.ppm", "rb") as fp: + im = Image.open(fp) To read an image from string data, use the :py:class:`~StringIO.StringIO` class: @@ -499,9 +502,9 @@ Reading from a tar archive :: - from PIL import TarIO + from PIL import Image, TarIO - fp = TarIO.TarIO("Imaging.tar", "Imaging/test/lena.ppm") + fp = TarIO.TarIO("Tests/images/hopper.tar", "hopper.jpg") im = Image.open(fp) Controlling the decoder diff --git a/docs/handbook/writing-your-own-file-decoder.rst b/docs/handbook/writing-your-own-file-decoder.rst index f72851179..0292a7880 100644 --- a/docs/handbook/writing-your-own-file-decoder.rst +++ b/docs/handbook/writing-your-own-file-decoder.rst @@ -9,7 +9,10 @@ itself. Such plug-ins usually have names like :file:`XxxImagePlugin.py`, where ``Xxx`` is a unique format name (usually an abbreviation). -.. warning:: Pillow >= 2.1.0 no longer automatically imports any file in the Python path with a name ending in :file:`ImagePlugin.py`. You will need to import your image plugin manually. +.. warning:: Pillow >= 2.1.0 no longer automatically imports any file + in the Python path with a name ending in + :file:`ImagePlugin.py`. You will need to import your + image plugin manually. Pillow decodes files in 2 stages: @@ -23,21 +26,19 @@ Pillow decodes files in 2 stages: called, which sets up a decoder for each tile and feeds the data to it. -A decoder plug-in should contain a decoder class, based on the -:py:class:`PIL.ImageFile.ImageFile` base class. This class should provide an -:py:meth:`_open` method, which reads the file header and sets up at least the -:py:attr:`~PIL.Image.Image.mode` and :py:attr:`~PIL.Image.Image.size` -attributes. To be able to load the file, the method must also create a list of -:py:attr:`tile` descriptors. The class must be explicitly registered, via a -call to the :py:mod:`~PIL.Image` module. +An image plug-in should contain a format handler derived from the +:py:class:`PIL.ImageFile.ImageFile` base class. This class should +provide an :py:meth:`_open` method, which reads the file header and +sets up at least the :py:attr:`~PIL.Image.Image.mode` and +:py:attr:`~PIL.Image.Image.size` attributes. To be able to load the +file, the method must also create a list of :py:attr:`tile` +descriptors, which contain a decoder name, extents of the tile, and +any decoder-specific data. The format handler class must be explicitly +registered, via a call to the :py:mod:`~PIL.Image` module. -For performance reasons, it is important that the :py:meth:`_open` method -quickly rejects files that do not have the appropriate contents. - -The ``raw`` decoder is useful for uncompressed image formats, but many -formats require more control of the decoding context, either with a -decoder written in ``C`` or by linking in an external library to do -the decoding. (Examples of this include PNG, Tiff, and Jpeg support) +.. note:: For performance reasons, it is important that the + :py:meth:`_open` method quickly rejects files that do not have the + appropriate contents. Example ------- @@ -94,12 +95,12 @@ true color. The format handler must always set the :py:attr:`~PIL.Image.Image.size` and :py:attr:`~PIL.Image.Image.mode` attributes. If these are not set, the file cannot be opened. To -simplify the decoder, the calling code considers exceptions like +simplify the plugin, the calling code considers exceptions like :py:exc:`SyntaxError`, :py:exc:`KeyError`, :py:exc:`IndexError`, :py:exc:`EOFError` and :py:exc:`struct.error` as a failure to identify the file. -Note that the decoder must be explicitly registered using +Note that the image plugin must be explicitly registered using :py:func:`PIL.Image.register_open`. Although not required, it is also a good idea to register any extensions used by this format. @@ -137,6 +138,9 @@ The fields are used as follows: Note that the :py:attr:`tile` attribute contains a list of tile descriptors, not just a single descriptor. +Decoders +======== + The raw decoder --------------- @@ -304,13 +308,14 @@ The fields are used as follows: .. _file-decoders: -Writing Your Own File Decoder -============================= +Writing Your Own File Decoder in C +================================== There are 3 stages in a file decoder's lifetime: -1. Setup: Pillow looks for a function named ``[decodername]_decoder`` - on the internal core image object. That function is called with the ``args`` tuple +1. Setup: Pillow looks for a function in the decoder registry, falling + back to a function named ``[decodername]_decoder`` on the internal + core image object. That function is called with the ``args`` tuple from the ``tile`` setup in the ``_open`` method. 2. Decoding: The decoder's decode function is repeatedly called with @@ -349,9 +354,6 @@ interest in this object are: member is an opaque struct that can be used by the decoder to store any format specific state or options. -**handles_eof** - UNDONE, set if your code handles EOF errors. - **pulls_fd** **EXPERIMENTAL** -- **WARNING**, interface may change. If set to 1, ``state->fd`` will be a pointer to the Python file like object. The @@ -391,3 +393,25 @@ The cleanup function is called after the decoder returns a negative value, or if there is a read error from the file. This function should free any allocated memory and release any resources from external libraries. + +.. _file-decoders-py: + +Writing Your Own File Decoder in Python +======================================= + +Python file decoders should derive from +:py:class:`PIL.ImageFile.PyDecoder` and should at least override the +decode method. File decoders should be registered using +:py:meth:`PIL.Image.register_decoder`. As in the C implementation of +the file decoders, there are three stages in the lifetime of a +Python-based file decoder: + +1. Setup: Pillow looks for the decoder in the registry, then + instantiates the class. + +2. Decoding: The decoder instance's ``decode`` method is repeatedly + called with a buffer of data to be interpreted. + +3. Cleanup: The decoder instance's ``cleanup`` method is called. + + diff --git a/docs/index.rst b/docs/index.rst index b8455be60..55ba13bb7 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -26,18 +26,10 @@ Pillow is the friendly PIL fork by `Alex Clark and Contributors = 2.0.0 < 3.5.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 >= 3.5.0 supports Python versions 2.7, 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 ------------------ @@ -28,12 +28,6 @@ Basic Installation most common image formats. See :ref:`external-libraries` for a full list of external libraries supported. -.. note:: - - The basic installation works on Windows and macOS using the binaries - from PyPI. Other installations require building from source as - detailed below. - Install Pillow with :command:`pip`:: $ pip install Pillow @@ -72,11 +66,15 @@ except OpenJPEG:: Linux Installation ^^^^^^^^^^^^^^^^^^ -We do not provide binaries for Linux. Most major Linux distributions, -including Fedora, Debian/Ubuntu and ArchLinux include Pillow in -packages that previously contained PIL e.g. ``python-imaging``. Please -consider using native operating system packages first to avoid -installation problems and/or missing library support later. +We provide binaries for Linux for each of the supported Python +versions in the manylinux wheel format. These include support for all +optional libraries except Raqm:: + + $ pip install Pillow + +Most major Linux distributions, including Fedora, Debian/Ubuntu and +ArchLinux also include Pillow in packages that previously contained +PIL e.g. ``python-imaging``. FreeBSD Installation ^^^^^^^^^^^^^^^^^^^^ @@ -120,14 +118,16 @@ External Libraries .. note:: There are scripts to install the dependencies for some operating - systems included in the ``depends`` directory. + systems included in the ``depends`` directory. Also see the + Dockerfiles in our `docker images repo + `_. 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. @@ -159,7 +159,7 @@ Many of Pillow's features require external libraries: * Pillow has been tested with openjpeg **2.0.0** and **2.1.0**. * Pillow does **not** support the earlier **1.5** series which ships - with Ubuntu and Debian. + with Ubuntu <= 14.04 and Debian Jessie. * **libimagequant** provides improved color quantization @@ -170,6 +170,18 @@ Many of Pillow's features require external libraries: * Windows support: Libimagequant requires VS2013/MSVC 18 to compile, so it is unlikely to work with any Python prior to 3.5 on Windows. +* **libraqm** provides complex text layout support. + + * libraqm provides bidirectional text support (using FriBiDi), + shaping (using HarfBuzz), and proper script itemization. As a + result, Raqm can support most writing systems covered by Unicode. + * libraqm depends on the following libraries: FreeType, HarfBuzz, + FriBiDi, make sure that you install them before install libraqm + if not available as package in your system. + * setting text direction or font features is not supported without + libraqm. + * Windows support: Raqm support is currently unsupported on Windows. + Once you have installed the prerequisites, run:: $ pip install Pillow @@ -193,7 +205,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. @@ -201,14 +213,16 @@ Build Options * Build flags: ``--disable-zlib``, ``--disable-jpeg``, ``--disable-tiff``, ``--disable-freetype``, ``--disable-tcl``, ``--disable-tk``, ``--disable-lcms``, ``--disable-webp``, - ``--disable-webpmux``, ``--disable-jpeg2000``, ``--disable-imagequant``. + ``--disable-webpmux``, ``--disable-jpeg2000``, + ``--disable-imagequant``, ``--disable-raqm``. Disable building the corresponding feature even if the development libraries are present on the building machine. * Build flags: ``--enable-zlib``, ``--enable-jpeg``, ``--enable-tiff``, ``--enable-freetype``, ``--enable-tcl``, ``--enable-tk``, ``--enable-lcms``, ``--enable-webp``, - ``--enable-webpmux``, ``--enable-jpeg2000``, ``--enable-imagequant``. + ``--enable-webpmux``, ``--enable-jpeg2000``, + ``--enable-imagequant``, ``--enable-raqm``. Require that the corresponding feature is built. The build will raise an exception if the libraries are not found. Webpmux (WebP metadata) relies on WebP support. Tcl and Tk also must be used together. @@ -223,7 +237,7 @@ Build Options stdout. -Sample Usage:: +Sample usage:: $ MAX_CONCURRENCY=1 python setup.py build_ext --enable-[feature] install @@ -243,11 +257,16 @@ have the full Xcode package installed. It may be necessary to run tools. The easiest way to install external libraries is via `Homebrew -`_. After you install Homebrew, run:: +`_. After you install Homebrew, run:: $ brew install libtiff libjpeg webp little-cms2 -Install Pillow with:: +To install libraqm on macOS use Homebrew to install its dependencies:: + $ brew install freetype harfbuzz fribidi + +Then see ``depends/install_raqm_cmake.sh`` to install libraqm. + +Now install Pillow with:: $ pip install Pillow @@ -265,7 +284,7 @@ Windows build in the ``winbuild`` directory. Building on FreeBSD ^^^^^^^^^^^^^^^^^^^ -.. Note:: Only FreeBSD 10 tested +.. Note:: Only FreeBSD 10 and 11 tested Make sure you have Python's development libraries installed.:: @@ -275,9 +294,11 @@ Or for Python 3:: $ sudo pkg install python3 -Prerequisites are installed on **FreeBSD 10** with:: +Prerequisites are installed on **FreeBSD 10 or 11** with:: - $ sudo pkg install jpeg tiff webp lcms2 freetype2 + $ sudo pkg install jpeg-turbo tiff webp lcms2 freetype2 openjpeg harfbuzz fribidi + +Then see ``depends/install_raqm_cmake.sh`` to install libraqm. Building on Linux @@ -304,83 +325,119 @@ Or for Python 3:: .. Note:: ``redhat-rpm-config`` is required on Fedora 23, but not earlier versions. -Prerequisites are installed on **Ubuntu 12.04 LTS** or **Raspian Wheezy -7.0** with:: - - $ sudo apt-get install libtiff4-dev libjpeg8-dev zlib1g-dev \ - libfreetype6-dev liblcms2-dev libwebp-dev tcl8.5-dev tk8.5-dev python-tk - Prerequisites are installed on **Ubuntu 14.04 LTS** with:: $ sudo apt-get install libtiff5-dev libjpeg8-dev zlib1g-dev \ - libfreetype6-dev liblcms2-dev libwebp-dev tcl8.6-dev tk8.6-dev python-tk + libfreetype6-dev liblcms2-dev libwebp-dev libharfbuzz-dev libfribidi-dev \ + tcl8.6-dev tk8.6-dev python-tk + +Then see ``depends/install_raqm.sh`` to install libraqm. Prerequisites are installed on **Fedora 23** with:: $ sudo dnf install libtiff-devel libjpeg-devel zlib-devel freetype-devel \ - lcms2-devel libwebp-devel tcl-devel tk-devel + lcms2-devel libwebp-devel tcl-devel tk-devel libraqm-devel Platform Support ---------------- -Current platform support for Pillow. Binary distributions are contributed for -each release on a volunteer basis, but the source should compile and run -everywhere platform support is listed. In general, we aim to support all -current versions of Linux, macOS, and Windows. +Current platform support for Pillow. Binary distributions are +contributed for each release on a volunteer basis, but the source +should compile and run everywhere platform support is listed. In +general, we aim to support all current versions of Linux, macOS, and +Windows. Note that Android is not currently supported, but there have +been reports of success. + +Continuous Integration Targets +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +These platforms are built and tested for every change. + ++----------------------------------+-------------------------------+-----------------------+ +|**Operating system** |**Tested Python versions** |**Tested Architecture**| ++----------------------------------+-------------------------------+-----------------------+ +| Alpine | 2.7 |x86-64 | ++----------------------------------+-------------------------------+-----------------------+ +| Arch | 2.7 |x86-64 | ++----------------------------------+-------------------------------+-----------------------+ +| Amazon | 2.7 |x86-64 | ++----------------------------------+-------------------------------+-----------------------+ +| Centos 6 | 2.7 |x86-64 | ++----------------------------------+-------------------------------+-----------------------+ +| Debian Stretch | 2.7 |x86 | ++----------------------------------+-------------------------------+-----------------------+ +| Mac OS X 10.10 Yosemite* | 2.7, 3.3, 3.4, 3.5, 3.6 |x86-64 | ++----------------------------------+-------------------------------+-----------------------+ +| Ubuntu Linux 16.04 LTS | 2.7 |x86-64 | ++----------------------------------+-------------------------------+-----------------------+ +| Ubuntu Linux 14.04 LTS | 2.7, 3.3, 3.4, 3.5, 3.6, |x86-64 | +| | pypy, pypy3 | | +| | | | +| | 2.7 |x86 | ++----------------------------------+-------------------------------+-----------------------+ +| Ubuntu Linux 12.04 LTS | 2.7 |x86-64 | ++----------------------------------+-------------------------------+-----------------------+ +| Windows Server 2012 R2 | 2.7,3.3,3.4,pypy |x86, x86-64 | ++----------------------------------+-------------------------------+-----------------------+ + +\* Mac OS X CI is not run for every commit, but is run for every release. + +Other Platforms +^^^^^^^^^^^^^^^ + +These platforms have been reported to work at the versions mentioned. .. note:: Contributors please test Pillow on your platform then update this document and send a pull request. -+----------------------------------+-------------+------------------------------+--------------------------------+-----------------------+ -|**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.4.1 |x86-64 | -+----------------------------------+-------------+------------------------------+--------------------------------+-----------------------+ -| Mac OS X 10.10 Yosemite |Yes | 2.7,3.3,3.4 | 3.0.0 |x86-64 | -+----------------------------------+-------------+------------------------------+--------------------------------+-----------------------+ -| Mac OS X 10.9 Mavericks |Yes | 2.7,3.2,3.3,3.4 | 3.0.0 |x86-64 | -+----------------------------------+-------------+------------------------------+--------------------------------+-----------------------+ -| Mac OS X 10.8 Mountain Lion |Yes | 2.6,2.7,3.2,3.3 | |x86-64 | -+----------------------------------+-------------+------------------------------+--------------------------------+-----------------------+ -| Redhat Linux 6 |Yes | 2.6 | |x86 | -+----------------------------------+-------------+------------------------------+--------------------------------+-----------------------+ -| CentOS 6.3 |Yes | 2.7,3.3 | |x86 | -+----------------------------------+-------------+------------------------------+--------------------------------+-----------------------+ -| Fedora 23 |Yes | 2.7,3.4 | 3.1.0 |x86-64 | -+----------------------------------+-------------+------------------------------+--------------------------------+-----------------------+ -| Ubuntu Linux 10.04 LTS |Yes | 2.6 | 2.3.0 |x86,x86-64 | -+----------------------------------+-------------+------------------------------+--------------------------------+-----------------------+ -| Ubuntu Linux 12.04 LTS |Yes | 2.6,2.7,3.2,3.3,3.4,3.5 | 3.4.1 (CI target) |x86,x86-64 | -| | | PyPy5.3.1,PyPy3 v2.4.0 | | | -| | | | | | -| | | 2.7,3.2 | 3.4.1 |ppc | -+----------------------------------+-------------+------------------------------+--------------------------------+-----------------------+ -| Ubuntu Linux 14.04 LTS |Yes | 2.7,3.4 | 3.1.0 |x86-64 | -+----------------------------------+-------------+------------------------------+--------------------------------+-----------------------+ -| Debian 8.2 Jessie |Yes | 2.7,3.4 | 3.1.0 |x86-64 | -+----------------------------------+-------------+------------------------------+--------------------------------+-----------------------+ -| Raspian Jessie |Yes | 2.7,3.4 | 3.1.0 |arm | -+----------------------------------+-------------+------------------------------+--------------------------------+-----------------------+ -| Gentoo Linux |Yes | 2.7,3.2 | 2.1.0 |x86-64 | -+----------------------------------+-------------+------------------------------+--------------------------------+-----------------------+ -| Arch Linux |Yes | 2.7,3.5 | 3.4.1 |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 | 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.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 | -+----------------------------------+-------------+------------------------------+--------------------------------+-----------------------+ -| Windows 8.1 Pro |Yes | 2.6,2.7,3.2,3.3,3.4 | 2.4.0 |x86,x86-64 | -+----------------------------------+-------------+------------------------------+--------------------------------+-----------------------+ ++----------------------------------+------------------------------+--------------------------------+-----------------------+ +|**Operating system** |**Tested Python versions** |**Latest tested Pillow version**|**Tested processors** | ++----------------------------------+------------------------------+--------------------------------+-----------------------+ +| macOS 10.12 Sierra | 2.7,3.4,3.5,3.6 | 4.1.1 |x86-64 | ++----------------------------------+------------------------------+--------------------------------+-----------------------+ +| Mac OS X 10.11 El Capitan | 2.7,3.3,3.4,3.5 | 4.1.0 |x86-64 | ++----------------------------------+------------------------------+--------------------------------+-----------------------+ +| Mac OS X 10.9 Mavericks | 2.7,3.2,3.3,3.4 | 3.0.0 |x86-64 | ++----------------------------------+------------------------------+--------------------------------+-----------------------+ +| Mac OS X 10.8 Mountain Lion | 2.6,2.7,3.2,3.3 | |x86-64 | ++----------------------------------+------------------------------+--------------------------------+-----------------------+ +| Redhat Linux 6 | 2.6 | |x86 | ++----------------------------------+------------------------------+--------------------------------+-----------------------+ +| CentOS 6.3 | 2.7,3.3 | |x86 | ++----------------------------------+------------------------------+--------------------------------+-----------------------+ +| Fedora 23 | 2.7,3.4 | 3.1.0 |x86-64 | ++----------------------------------+------------------------------+--------------------------------+-----------------------+ +| Ubuntu Linux 12.04 LTS | 2.6,2.7,3.2,3.3,3.4,3.5 | 3.4.1 |x86,x86-64 | +| | PyPy5.3.1,PyPy3 v2.4.0 | | | +| | | | | +| | 2.7,3.2 | 3.4.1 |ppc | ++----------------------------------+------------------------------+--------------------------------+-----------------------+ +| Ubuntu Linux 10.04 LTS | 2.6 | 2.3.0 |x86,x86-64 | ++----------------------------------+------------------------------+--------------------------------+-----------------------+ +| Debian 8.2 Jessie | 2.7,3.4 | 3.1.0 |x86-64 | ++----------------------------------+------------------------------+--------------------------------+-----------------------+ +| Raspian Jessie | 2.7,3.4 | 3.1.0 |arm | ++----------------------------------+------------------------------+--------------------------------+-----------------------+ +| Gentoo Linux | 2.7,3.2 | 2.1.0 |x86-64 | ++----------------------------------+------------------------------+--------------------------------+-----------------------+ +| FreeBSD 11.0 | 2.7,3.4,3.5,3.6 | 4.2.0 |x86-64 | ++----------------------------------+------------------------------+--------------------------------+-----------------------+ +| FreeBSD 10.3 | 2.7,3.4,3.5 | 4.2.0 |x86-64 | ++----------------------------------+------------------------------+--------------------------------+-----------------------+ +| FreeBSD 10.2 | 2.7,3.4 | 3.1.0 |x86-64 | ++----------------------------------+------------------------------+--------------------------------+-----------------------+ +| Windows 8.1 Pro | 2.6,2.7,3.2,3.3,3.4 | 2.4.0 |x86,x86-64 | ++----------------------------------+------------------------------+--------------------------------+-----------------------+ +| Windows 8 Pro | 2.6,2.7,3.2,3.3,3.4a3 | 2.2.0 |x86,x86-64 | ++----------------------------------+------------------------------+--------------------------------+-----------------------+ +| Windows 7 Pro | 2.7,3.2,3.3 | 3.4.1 |x86-64 | ++----------------------------------+------------------------------+--------------------------------+-----------------------+ +| Windows Server 2008 R2 Enterprise| 3.3 | |x86-64 | ++----------------------------------+------------------------------+--------------------------------+-----------------------+ Old Versions ------------ diff --git a/docs/reference/Image.rst b/docs/reference/Image.rst index ac8b6f506..e867494b2 100644 --- a/docs/reference/Image.rst +++ b/docs/reference/Image.rst @@ -88,10 +88,13 @@ Registering plugins ignore them. .. autofunction:: register_open +.. autofunction:: register_decoder .. autofunction:: register_mime .. autofunction:: register_save +.. autofunction:: register_encoder .. autofunction:: register_extension + The Image Class --------------- @@ -101,6 +104,8 @@ An instance of the :py:class:`~PIL.Image.Image` class has the following methods. Unless otherwise stated, all methods return a new instance of the :py:class:`~PIL.Image.Image` class, holding the resulting image. + +.. automethod:: PIL.Image.Image.alpha_composite .. automethod:: PIL.Image.Image.convert The following example converts an RGB image (linearly calibrated according to @@ -135,6 +140,7 @@ ITU-R 709, using the D65 luminant) to the CIE XYZ color space: .. automethod:: PIL.Image.Image.putpixel .. automethod:: PIL.Image.Image.quantize .. automethod:: PIL.Image.Image.resize +.. automethod:: PIL.Image.Image.remap_palette .. automethod:: PIL.Image.Image.rotate .. automethod:: PIL.Image.Image.save .. automethod:: PIL.Image.Image.seek @@ -159,6 +165,14 @@ Attributes Instances of the :py:class:`Image` class have the following attributes: +.. py:attribute:: filename + + The filename or path of the source file. Only images created with the + factory function `open` have a filename attribute. If the input is a + file like object, the filename attribute is set to an empty string. + + :type: :py:class: `string` + .. py:attribute:: format The file format of the source file. For images created by the library diff --git a/docs/reference/ImageDraw.rst b/docs/reference/ImageDraw.rst index 842407c90..ea55dd75f 100644 --- a/docs/reference/ImageDraw.rst +++ b/docs/reference/ImageDraw.rst @@ -4,7 +4,7 @@ :py:mod:`ImageDraw` Module ========================== -The :py:mod:`ImageDraw` module provide simple 2D graphics for +The :py:mod:`ImageDraw` module provides simple 2D graphics for :py:class:`~PIL.Image.Image` objects. You can use this module to create new images, annotate or retouch existing images, and to generate graphics on the fly for web use. @@ -20,7 +20,7 @@ Example: Draw a gray cross over an image from PIL import Image, ImageDraw - im = Image.open("lena.pgm") + im = Image.open("hopper.jpg") draw = ImageDraw.Draw(im) draw.line((0, 0) + im.size, fill=128) @@ -64,7 +64,7 @@ Fonts PIL can use bitmap fonts or OpenType/TrueType fonts. Bitmap fonts are stored in PIL’s own format, where each font typically consists -of a two files, one named .pil and the other usually named .pbm. The former +of two files, one named .pil and the other usually named .pbm. The former contains font metrics, the latter raster data. To load a bitmap font, use the load functions in the :py:mod:`~PIL.ImageFont` @@ -81,7 +81,7 @@ Example: Draw Partial Opacity Text from PIL import Image, ImageDraw, ImageFont # get an image - base = Image.open('Pillow/Tests/images/lena.png').convert('RGBA') + base = Image.open('Pillow/Tests/images/hopper.png').convert('RGBA') # make a blank image for the text, initialized to transparent text color txt = Image.new('RGBA', base.size, (255,255,255,0)) @@ -121,7 +121,13 @@ Functions Methods ------- -.. py:method:: PIL.ImageDraw.Draw.arc(xy, start, end, fill=None) +.. py:method:: PIL.ImageDraw.ImageDraw.getfont() + + Get the current default font. + + :returns: An image font. + +.. py:method:: PIL.ImageDraw.ImageDraw.arc(xy, start, end, fill=None) Draws an arc (a portion of a circle outline) between the start and end angles, inside the given bounding box. @@ -133,7 +139,7 @@ Methods :param end: Ending angle, in degrees. :param fill: Color to use for the arc. -.. py:method:: PIL.ImageDraw.Draw.bitmap(xy, bitmap, fill=None) +.. py:method:: PIL.ImageDraw.ImageDraw.bitmap(xy, bitmap, fill=None) Draws a bitmap (mask) at the given position, using the current fill color for the non-zero portions. The bitmap should be a valid transparency mask @@ -144,9 +150,9 @@ Methods To paste pixel data into an image, use the :py:meth:`~PIL.Image.Image.paste` method on the image itself. -.. py:method:: PIL.ImageDraw.Draw.chord(xy, start, end, fill=None, outline=None) +.. py:method:: PIL.ImageDraw.ImageDraw.chord(xy, start, end, fill=None, outline=None) - Same as :py:meth:`~PIL.ImageDraw.Draw.arc`, but connects the end points + Same as :py:meth:`~PIL.ImageDraw.ImageDraw.arc`, but connects the end points with a straight line. :param xy: Four points to define the bounding box. Sequence of @@ -154,7 +160,7 @@ Methods :param outline: Color to use for the outline. :param fill: Color to use for the fill. -.. py:method:: PIL.ImageDraw.Draw.ellipse(xy, fill=None, outline=None) +.. py:method:: PIL.ImageDraw.ImageDraw.ellipse(xy, fill=None, outline=None) Draws an ellipse inside the given bounding box. @@ -163,7 +169,7 @@ Methods :param outline: Color to use for the outline. :param fill: Color to use for the fill. -.. py:method:: PIL.ImageDraw.Draw.line(xy, fill=None, width=0) +.. py:method:: PIL.ImageDraw.ImageDraw.line(xy, fill=None, width=0) Draws a line between the coordinates in the **xy** list. @@ -177,7 +183,7 @@ Methods .. note:: This option was broken until version 1.1.6. -.. py:method:: PIL.ImageDraw.Draw.pieslice(xy, start, end, fill=None, outline=None) +.. py:method:: PIL.ImageDraw.ImageDraw.pieslice(xy, start, end, fill=None, outline=None) Same as arc, but also draws straight lines between the end points and the center of the bounding box. @@ -190,7 +196,7 @@ Methods :param fill: Color to use for the fill. :param outline: Color to use for the outline. -.. py:method:: PIL.ImageDraw.Draw.point(xy, fill=None) +.. py:method:: PIL.ImageDraw.ImageDraw.point(xy, fill=None) Draws points (individual pixels) at the given coordinates. @@ -198,7 +204,7 @@ Methods numeric values like ``[x, y, x, y, ...]``. :param fill: Color to use for the point. -.. py:method:: PIL.ImageDraw.Draw.polygon(xy, fill=None, outline=None) +.. py:method:: PIL.ImageDraw.ImageDraw.polygon(xy, fill=None, outline=None) Draws a polygon. @@ -211,7 +217,7 @@ Methods :param outline: Color to use for the outline. :param fill: Color to use for the fill. -.. py:method:: PIL.ImageDraw.Draw.rectangle(xy, fill=None, outline=None) +.. py:method:: PIL.ImageDraw.ImageDraw.rectangle(xy, fill=None, outline=None) Draws a rectangle. @@ -221,13 +227,13 @@ Methods :param outline: Color to use for the outline. :param fill: Color to use for the fill. -.. py:method:: PIL.ImageDraw.Draw.shape(shape, fill=None, outline=None) +.. py:method:: PIL.ImageDraw.ImageDraw.shape(shape, fill=None, outline=None) .. warning:: This method is experimental. Draw a shape. -.. py:method:: PIL.ImageDraw.Draw.text(xy, text, fill=None, font=None, anchor=None, spacing=0, align="left") +.. py:method:: PIL.ImageDraw.ImageDraw.text(xy, text, fill=None, font=None, anchor=None, spacing=0, align="left", direction=None, features=None) Draws the string at the given position. @@ -240,9 +246,28 @@ Methods the number of pixels between lines. :param align: If the text is passed on to multiline_text(), "left", "center" or "right". + :param direction: Direction of the text. It can be 'rtl' (right to + left), 'ltr' (left to right), 'ttb' (top to + bottom) or 'btt' (bottom to top). Requires + libraqm. + .. versionadded:: 4.2.0 -.. py:method:: PIL.ImageDraw.Draw.multiline_text(xy, text, fill=None, font=None, anchor=None, spacing=0, align="left") + :param features: A list of OpenType font features to be used during text + layout. This is usually used to turn on optional + font features that are not enabled by default, + for example 'dlig' or 'ss01', but can be also + used to turn off default font features for + example '-liga' to disable ligatures or '-kern' + to disable kerning. To get all supported + features, see + https://www.microsoft.com/typography/otspec/featurelist.htm + Requires libraqm. + + .. versionadded:: 4.2.0 + +.. py:method:: PIL.ImageDraw.ImageDraw.multiline_text(xy, text, fill=None, font=None, anchor=None, spacing=0, align="left", + direction=None, features=None) Draws the string at the given position. @@ -252,8 +277,28 @@ Methods :param font: An :py:class:`~PIL.ImageFont.ImageFont` instance. :param spacing: The number of pixels between lines. :param align: "left", "center" or "right". + :param direction: Direction of the text. It can be 'rtl' (right to + left), 'ltr' (left to right), 'ttb' (top to + bottom) or 'btt' (bottom to top). Requires + libraqm. -.. py:method:: PIL.ImageDraw.Draw.textsize(text, font=None, spacing=0) + .. versionadded:: 4.2.0 + + :param features: A list of OpenType font features to be used during text + layout. This is usually used to turn on optional + font features that are not enabled by default, + for example 'dlig' or 'ss01', but can be also + used to turn off default font features for + example '-liga' to disable ligatures or '-kern' + to disable kerning. To get all supported + features, see + https://www.microsoft.com/typography/otspec/featurelist.htm + Requires libraqm. + + .. versionadded:: 4.2.0 + +.. py:method:: PIL.ImageDraw.ImageDraw.textsize(text, font=None, spacing=4, direction=None, + features=None) Return the size of the given string, in pixels. @@ -262,32 +307,79 @@ Methods :param font: An :py:class:`~PIL.ImageFont.ImageFont` instance. :param spacing: If the text is passed on to multiline_textsize(), the number of pixels between lines. + :param direction: Direction of the text. It can be 'rtl' (right to + left), 'ltr' (left to right), 'ttb' (top to + bottom) or 'btt' (bottom to top). Requires + libraqm. -.. py:method:: PIL.ImageDraw.Draw.multiline_textsize(text, font=None, spacing=0) + .. versionadded:: 4.2.0 + + :param features: A list of OpenType font features to be used during text + layout. This is usually used to turn on optional + font features that are not enabled by default, + for example 'dlig' or 'ss01', but can be also + used to turn off default font features for + example '-liga' to disable ligatures or '-kern' + to disable kerning. To get all supported + features, see + https://www.microsoft.com/typography/otspec/featurelist.htm + Requires libraqm. + + .. versionadded:: 4.2.0 + +.. py:method:: PIL.ImageDraw.ImageDraw.multiline_textsize(text, font=None, spacing=4, direction=None, + features=None) Return the size of the given string, in pixels. :param text: Text to be measured. :param font: An :py:class:`~PIL.ImageFont.ImageFont` instance. :param spacing: The number of pixels between lines. + :param direction: Direction of the text. It can be 'rtl' (right to + left), 'ltr' (left to right), 'ttb' (top to + bottom) or 'btt' (bottom to top). Requires + libraqm. -Legacy API ----------- + .. versionadded:: 4.2.0 -The :py:class:`~PIL.ImageDraw.Draw` class contains a constructor and a number -of methods which are provided for backwards compatibility only. For this to -work properly, you should either use options on the drawing primitives, or -these methods. Do not mix the old and new calling conventions. + :param features: A list of OpenType font features to be used during text + layout. This is usually used to turn on optional + font features that are not enabled by default, + for example 'dlig' or 'ss01', but can be also + used to turn off default font features for + example '-liga' to disable ligatures or '-kern' + to disable kerning. To get all supported + features, see + https://www.microsoft.com/typography/otspec/featurelist.htm + Requires libraqm. + .. versionadded:: 4.2.0 -.. py:function:: PIL.ImageDraw.ImageDraw(image) +.. py:method:: PIL.ImageDraw.getdraw(im=None, hints=None) - :rtype: :py:class:`~PIL.ImageDraw.Draw` + .. warning:: This method is experimental. -.. py:method:: PIL.ImageDraw.Draw.setfont(font) + A more advanced 2D drawing interface for PIL images, + based on the WCK interface. - .. deprecated:: 1.1.5 + :param im: The image to draw in. + :param hints: An optional list of hints. + :returns: A (drawing context, drawing resource factory) tuple. - Sets the default font to use for the text method. +.. py:method:: PIL.ImageDraw.floodfill(image, xy, value, border=None, thresh=0) - :param font: An :py:class:`~PIL.ImageFont.ImageFont` instance. + .. warning:: This method is experimental. + + Fills a bounded region with a given color. + + :param image: Target image. + :param xy: Seed position (a 2-item coordinate tuple). + :param value: Fill color. + :param border: Optional border value. If given, the region consists of + pixels with a color different from the border color. If not given, + the region consists of pixels having the same color as the seed + pixel. + :param thresh: Optional threshold value which specifies a maximum + tolerable difference of a pixel value from the 'background' in + order for it to be replaced. Useful for filling regions of non- + homogeneous, but similar, colors. diff --git a/docs/reference/ImageFile.rst b/docs/reference/ImageFile.rst index 9612658e9..d93dfb3a3 100644 --- a/docs/reference/ImageFile.rst +++ b/docs/reference/ImageFile.rst @@ -19,7 +19,7 @@ Example: Parse an image from PIL import ImageFile - fp = open("lena.pgm", "rb") + fp = open("hopper.pgm", "rb") p = ImageFile.Parser() @@ -39,3 +39,9 @@ Example: Parse an image .. autoclass:: PIL.ImageFile.Parser() :members: + +:py:class:`~PIL.ImageFile.PyDecoder` +------------------------------------ + +.. autoclass:: PIL.ImageFile.PyDecoder() + :members: diff --git a/docs/reference/ImageFont.rst b/docs/reference/ImageFont.rst index 166d977a6..76fde44ff 100644 --- a/docs/reference/ImageFont.rst +++ b/docs/reference/ImageFont.rst @@ -9,7 +9,7 @@ this class store bitmap fonts, and are used with the :py:meth:`PIL.ImageDraw.Draw.text` method. PIL uses its own font file format to store bitmap fonts. You can use the -:command`pilfont` utility to convert BDF and PCF font descriptors (X window +:command:`pilfont` utility to convert BDF and PCF font descriptors (X window font formats) to this format. Starting with version 1.1.4, PIL can be configured to support TrueType and @@ -51,7 +51,7 @@ Methods :return: (width, height) -.. py:method:: PIL.ImageFont.ImageFont.getmask(text, mode='') +.. py:method:: PIL.ImageFont.ImageFont.getmask(text, mode='', direction=None, features=[]) Create a bitmap for the text. @@ -65,5 +65,26 @@ Methods C-level implementations. .. versionadded:: 1.1.5 + + :param direction: Direction of the text. It can be 'rtl' (right to + left), 'ltr' (left to right), 'ttb' (top to + bottom) or 'btt' (bottom to top). Requires + libraqm. + + .. versionadded:: 4.2.0 + + :param features: A list of OpenType font features to be used during text + layout. This is usually used to turn on optional + font features that are not enabled by default, + for example 'dlig' or 'ss01', but can be also + used to turn off default font features for + example '-liga' to disable ligatures or '-kern' + to disable kerning. To get all supported + features, see + https://www.microsoft.com/typography/otspec/featurelist.htm + Requires libraqm. + + .. versionadded:: 4.2.0 + :return: An internal PIL storage memory instance as defined by the :py:mod:`PIL.Image.core` interface module. diff --git a/docs/reference/ImageQt.rst b/docs/reference/ImageQt.rst index e63fd99fe..7bc426eec 100644 --- a/docs/reference/ImageQt.rst +++ b/docs/reference/ImageQt.rst @@ -4,8 +4,8 @@ :py:mod:`ImageQt` Module ======================== -The :py:mod:`ImageQt` module contains support for creating PyQt4 or PyQt5 QImage objects -from PIL images. +The :py:mod:`ImageQt` module contains support for creating PyQt4, PyQt5 or +PySide QImage objects from PIL images. .. versionadded:: 1.1.6 @@ -14,7 +14,7 @@ from PIL images. Creates an :py:class:`~PIL.ImageQt.ImageQt` object from a PIL :py:class:`~PIL.Image.Image` object. This class is a subclass of QtGui.QImage, which means that you can pass the resulting objects directly - to PyQt4/5 API functions and methods. + to PyQt4/PyQt5/PySide API functions and methods. This operation is currently supported for mode 1, L, P, RGB, and RGBA images. To handle other modes, you need to convert the image first. 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..79dc66e67 --- /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. (Although not in 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/2.7.0.rst b/docs/releasenotes/2.7.0.rst index a51ca81b4..4bb25e371 100644 --- a/docs/releasenotes/2.7.0.rst +++ b/docs/releasenotes/2.7.0.rst @@ -148,7 +148,7 @@ Blur performance Box filter computation time is constant relative to the radius and depends on source image size only. Because the new Gaussian blur implementation -is based on box filter, its computation time also doesn't depends on the blur +is based on box filter, its computation time also doesn't depend on the blur radius. For example, previously, if the execution time for a given test image was 1 @@ -172,4 +172,3 @@ specified as strings with included spaces (e.g. 'x resolution'). This was difficult to use as kwargs without constructing and passing a dictionary. These parameters now use the underscore character instead of space. (e.g. 'x_resolution') - diff --git a/docs/releasenotes/3.1.0.rst b/docs/releasenotes/3.1.0.rst index 7f0a7f052..388af03ac 100644 --- a/docs/releasenotes/3.1.0.rst +++ b/docs/releasenotes/3.1.0.rst @@ -68,7 +68,7 @@ Out of Spec Metadata ++++++++++++++++++++ In Pillow 3.0 and 3.1, images that contain metadata that is internally -consistent but not in agreement with the TIFF spec may cause an +consistent, but not in agreement with the TIFF spec, may cause an exception when reading the metadata. This can happen when a tag that is specified to have a single value is stored with an array of values. diff --git a/docs/releasenotes/3.3.0.rst b/docs/releasenotes/3.3.0.rst index 544c7162e..39ffdbb2e 100644 --- a/docs/releasenotes/3.3.0.rst +++ b/docs/releasenotes/3.3.0.rst @@ -29,9 +29,9 @@ Resizing ======== Image resampling for 8-bit per channel images was rewritten using only integer -computings. This is faster on most of the platforms and doesn't introduce -precision errors on the wide range of scales. With other performance -improvements, this makes resampling 60% faster on average. +computings. This is faster on most platforms and doesn't introduce precision +errors on the wide range of scales. With other performance improvements, this +makes resampling 60% faster on average. Color calculation for images in the ``LA`` mode on semitransparent pixels was fixed. @@ -41,7 +41,7 @@ Rotation ======== Rotation for angles divisible by 90 degrees now always uses transposition. -This greatly improve both quality and performance in this cases. +This greatly improves both quality and performance in this case. Also, the bug with wrong image size calculation when rotating by 90 degrees was fixed. @@ -52,4 +52,3 @@ Image Metadata The return type for binary data in version 2 Exif and Tiff metadata has been changed from a tuple of integers to bytes. This is a change from the behavior since ``3.0.0``. - diff --git a/docs/releasenotes/3.3.2.rst b/docs/releasenotes/3.3.2.rst index 141413093..68a09a3c8 100644 --- a/docs/releasenotes/3.3.2.rst +++ b/docs/releasenotes/3.3.2.rst @@ -11,7 +11,7 @@ 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 +``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 diff --git a/docs/releasenotes/3.4.0.rst b/docs/releasenotes/3.4.0.rst index a6512fc12..dc5e2e295 100644 --- a/docs/releasenotes/3.4.0.rst +++ b/docs/releasenotes/3.4.0.rst @@ -8,7 +8,7 @@ New resizing filters Two new filters available for ``Image.resize()`` and ``Image.thumbnail()`` functions: ``BOX`` and ``HAMMING``. ``BOX`` is the high-performance filter with two times shorter window than ``BILINEAR``. It can be used for image reduction -3 and more times and produces a more sharp result than ``BILINEAR``. +3 and more times and produces a sharper result than ``BILINEAR``. ``HAMMING`` filter has the same performance as ``BILINEAR`` filter while providing the image downscaling quality comparable to ``BICUBIC``. @@ -20,12 +20,12 @@ 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. +image as a JPEG. This will become an error in Pillow 4.2. New DDS Decoders ================ -Pillow can now decode DXT3 images, as well as the previously support +Pillow can now decode DXT3 images, as well as the previously supported DXT1 and DXT5 formats. All three formats are now decoded in C code for better performance. @@ -44,7 +44,7 @@ in effect, e.g.:: Save multiple frame TIFF ======================== -Multiple frames can now be saved in a TIFF file by using the ``save_all`` option. +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) 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/4.1.0.rst b/docs/releasenotes/4.1.0.rst new file mode 100644 index 000000000..a6fb9d2af --- /dev/null +++ b/docs/releasenotes/4.1.0.rst @@ -0,0 +1,86 @@ +4.1.0 +----- + +Removed Deprecated Items +======================== + +Several deprecated items have been removed. + +* Support for spaces in tiff kwargs in the parameters for 'x resolution', 'y + resolution', 'resolution unit', and 'date time' has been + removed. Underscores should be used instead. + +* The methods :py:meth:`PIL.ImageDraw.ImageDraw.setink`, + :py:meth:`PIL.ImageDraw.ImageDraw.setfill`, and + :py:meth:`PIL.ImageDraw.ImageDraw.setfont` have been removed. + + +Closing Files When Opening Images +================================= + +The file handling when opening images has been overhauled. Previously, +Pillow would attempt to close some, but not all image formats +after loading the image data. Now, the following behavior +is specified: + +* For images where an open file is passed in, it is the + responsibility of the calling code to close the file. + +* For images where Pillow opens the file and the file is known to have + only one frame, the file is closed after loading. + +* If the file has more than one frame, or if it can't be determined, + then the file is left open to permit seeking to subsequent + frames. It will be closed, eventually, in the ``close`` or + ``__del__`` methods. + +* If the image is memory mapped, then we can't close the mapping to + the underlying file until we are done with the image. The mapping + will be closed in the ``close`` or ``__del__`` method. + + +Changes to GIF Handling When Saving +=================================== + +The :py:class:`PIL.GifImagePlugin` code has been refactored to fix the flow when +saving images. There are two external changes that arise from this: + +* An :py:class:`PIL.ImagePalette.ImagePalette` object is now accepted + as a specified palette argument in :py:meth:`PIL.Image.Image.save()`. + +* The image to be saved is no longer modified in place by any of the + operations of the save function. Previously it was modified when + optimizing the image palette. + +This refactor fixed some bugs with palette handling when saving +multiple frame GIFs. + +New Method: Image.remap_palette +=============================== + +The method :py:meth:`PIL.Image.Image.remap_palette()` has been +added. This method was hoisted from the GifImagePlugin code used to +optimize the palette. + +Added Decoder Registry and Support for Python Based Decoders +============================================================ + +There is now a decoder registry similar to the image plugin +registries. Image plugins can register a decoder, and it will be +called when the decoding is requested. This allows for the creation of +pure Python decoders. While the Python decoders will not be as fast as +their C based counterparts, they may be easier and quicker to develop +or safer to run. + +Tests +===== + +Many tests have been added, including correctness tests for image +formats that have been previously untested. + +We are now running automated tests in Docker containers against more +Linux versions than are provided on Travis CI, which is currently +Ubuntu 14.04 x64. This Pillow release is tested on 64-bit Alpine, +Arch, Ubuntu 12.04 and 16.04, and 32-bit Debian Stretch and Ubuntu +14.04. This also covers a wider range of dependency versions than are +provided on Travis natively. diff --git a/docs/releasenotes/4.1.1.rst b/docs/releasenotes/4.1.1.rst new file mode 100644 index 000000000..7aa3c1fbf --- /dev/null +++ b/docs/releasenotes/4.1.1.rst @@ -0,0 +1,24 @@ +4.1.1 +----- + +Fix Regression with reading DPI from EXIF data +============================================== + +Some JPEG images don't contain DPI information in the image metadata, +but do contain it in the EXIF data. A patch was added in 4.1.0 to read +from the EXIF data, but it did not accept all possible types that +could be included there. This fix adds the ability to read ints as +well as rational values. + + +Incompatibility between 3.6.0 and 3.6.1 +======================================= + +CPython 3.6.1 added a new symbol, PySlice_GetIndicesEx, which was not +present in 3.6.0. This had the effect of causing binaries compiled on +CPython 3.6.1 to not work on installations of C-Python 3.6.0. This fix +undefines PySlice_GetIndicesEx if it exists to restore compatibility +with both 3.6.0 and 3.6.1. See https://bugs.python.org/issue29943 for +more details. + + diff --git a/docs/releasenotes/4.2.0.rst b/docs/releasenotes/4.2.0.rst new file mode 100644 index 000000000..1b41580a7 --- /dev/null +++ b/docs/releasenotes/4.2.0.rst @@ -0,0 +1,51 @@ +4.2.0 +----- + +Added Complex Text Rendering +============================ + +Pillow now supports complex text rendering for scripts requiring glyph +composition and bidirectional flow. This optional feature adds three +dependencies: harfbuzz, fribidi, and raqm. See the install +documentation for further details. This feature is tested and works on +Unix and Mac, but has not yet been built on Windows platforms. + +New Optional Parameters +======================= + +* :py:meth:`PIL.ImageDraw.floodfill` has a new optional parameter: + threshold. This specifies a tolerance for the color to replace with + the flood fill. + +* The TIFF and PDF image writers now support the ``append_images`` + optional parameter for specifying additional images to create + multipage outputs. + +New DecompressionBomb Warning +============================= + +:py:meth:`PIL.Image.Image.crop` now may raise a DecompressionBomb +warning if the crop region enlarges the image over the threshold +specified by :py:attr:`PIL.Image.MAX_PIXELS`. + +Removed Deprecated Items +======================== + +Several deprecated items have been removed. + +* The methods :py:meth:`PIL.ImageWin.Dib.fromstring`, + :py:meth:`PIL.ImageWin.Dib.tostring` and + :py:meth:`PIL.TiffImagePlugin.ImageFileDirectory_v2.as_dict` have + been removed. + +* Before Pillow 4.2.0, attempting to save an RGBA image as JPEG would + discard the alpha channel. From Pillow 3.4.0, a deprecation warning + was shown. From Pillow 4.2.0, the deprecation warning is removed and + an :py:exc:`IOError` is raised. + +Removed Core Image Function +=========================== + +The unused function ``Image.core.new_array`` was removed. This is an +internal function that should not have been used by user code, but it +was accessible from the python layer. diff --git a/docs/releasenotes/4.2.1.rst b/docs/releasenotes/4.2.1.rst new file mode 100644 index 000000000..c9e953da4 --- /dev/null +++ b/docs/releasenotes/4.2.1.rst @@ -0,0 +1,11 @@ +4.2.1 +----- + +There are no functional changes in this release. + +Fixed Windows PyPy Build +======================== + +A change in the 4.2.0 cycle broke the Windows PyPy build. This has +been fixed, and PyPy is now part of the Windows CI matrix. + diff --git a/docs/releasenotes/index.rst b/docs/releasenotes/index.rst index 8c484af44..27b12face 100644 --- a/docs/releasenotes/index.rst +++ b/docs/releasenotes/index.rst @@ -6,6 +6,11 @@ Release Notes .. toctree:: :maxdepth: 2 + 4.2.1 + 4.2.0 + 4.1.1 + 4.1.0 + 4.0.0 3.4.0 3.3.2 3.3.0 diff --git a/libImaging/Access.c b/libImaging/Access.c index 059f2aaeb..292968f1c 100644 --- a/libImaging/Access.c +++ b/libImaging/Access.c @@ -32,7 +32,7 @@ add_item(const char* mode) { UINT32 i = hash(mode); /* printf("hash %s => %d\n", mode, i); */ - if (access_table[i].mode) { + if (access_table[i].mode && strcmp(access_table[i].mode, mode) != 0) { fprintf(stderr, "AccessInit: hash collision: %d for both %s and %s\n", i, mode, access_table[i].mode); exit(1); diff --git a/libImaging/Bands.c b/libImaging/Bands.c index cc8d634dd..d7424ab25 100644 --- a/libImaging/Bands.c +++ b/libImaging/Bands.c @@ -30,14 +30,14 @@ ImagingGetBand(Imaging imIn, int band) /* Check arguments */ if (!imIn || imIn->type != IMAGING_TYPE_UINT8) - return (Imaging) ImagingError_ModeError(); + return (Imaging) ImagingError_ModeError(); if (band < 0 || band >= imIn->bands) - return (Imaging) ImagingError_ValueError("band index out of range"); + return (Imaging) ImagingError_ValueError("band index out of range"); /* Shortcuts */ if (imIn->bands == 1) - return ImagingCopy(imIn); + return ImagingCopy(imIn); /* Special case for LXXA etc */ if (imIn->bands == 2 && band == 1) @@ -45,16 +45,16 @@ ImagingGetBand(Imaging imIn, int band) imOut = ImagingNew("L", imIn->xsize, imIn->ysize); if (!imOut) - return NULL; + return NULL; /* Extract band from image */ for (y = 0; y < imIn->ysize; y++) { - UINT8* in = (UINT8*) imIn->image[y] + band; - UINT8* out = imOut->image8[y]; - for (x = 0; x < imIn->xsize; x++) { - out[x] = *in; - in += 4; - } + UINT8* in = (UINT8*) imIn->image[y] + band; + UINT8* out = imOut->image8[y]; + for (x = 0; x < imIn->xsize; x++) { + out[x] = *in; + in += 4; + } } return imOut; @@ -67,19 +67,19 @@ ImagingPutBand(Imaging imOut, Imaging imIn, int band) /* Check arguments */ if (!imIn || imIn->bands != 1 || !imOut) - return (Imaging) ImagingError_ModeError(); + return (Imaging) ImagingError_ModeError(); if (band < 0 || band >= imOut->bands) - return (Imaging) ImagingError_ValueError("band index out of range"); + return (Imaging) ImagingError_ValueError("band index out of range"); if (imIn->type != imOut->type || - imIn->xsize != imOut->xsize || - imIn->ysize != imOut->ysize) - return (Imaging) ImagingError_Mismatch(); + imIn->xsize != imOut->xsize || + imIn->ysize != imOut->ysize) + return (Imaging) ImagingError_Mismatch(); /* Shortcuts */ if (imOut->bands == 1) - return ImagingCopy2(imOut, imIn); + return ImagingCopy2(imOut, imIn); /* Special case for LXXA etc */ if (imOut->bands == 2 && band == 1) @@ -87,12 +87,12 @@ ImagingPutBand(Imaging imOut, Imaging imIn, int band) /* Insert band into image */ for (y = 0; y < imIn->ysize; y++) { - UINT8* in = imIn->image8[y]; - UINT8* out = (UINT8*) imOut->image[y] + band; - for (x = 0; x < imIn->xsize; x++) { - *out = in[x]; - out += 4; - } + UINT8* in = imIn->image8[y]; + UINT8* out = (UINT8*) imOut->image[y] + band; + for (x = 0; x < imIn->xsize; x++) { + *out = in[x]; + out += 4; + } } return imOut; @@ -105,10 +105,10 @@ ImagingFillBand(Imaging imOut, int band, int color) /* Check arguments */ if (!imOut || imOut->type != IMAGING_TYPE_UINT8) - return (Imaging) ImagingError_ModeError(); + return (Imaging) ImagingError_ModeError(); if (band < 0 || band >= imOut->bands) - return (Imaging) ImagingError_ValueError("band index out of range"); + return (Imaging) ImagingError_ValueError("band index out of range"); /* Special case for LXXA etc */ if (imOut->bands == 2 && band == 1) @@ -118,11 +118,11 @@ ImagingFillBand(Imaging imOut, int band, int color) /* Insert color into image */ for (y = 0; y < imOut->ysize; y++) { - UINT8* out = (UINT8*) imOut->image[y] + band; - for (x = 0; x < imOut->xsize; x++) { - *out = (UINT8) color; - out += 4; - } + UINT8* out = (UINT8*) imOut->image[y] + band; + for (x = 0; x < imOut->xsize; x++) { + *out = (UINT8) color; + out += 4; + } } return imOut; diff --git a/libImaging/Draw.c b/libImaging/Draw.c index ca26282be..65ab34a66 100644 --- a/libImaging/Draw.c +++ b/libImaging/Draw.c @@ -605,11 +605,6 @@ ImagingDrawWideLine(Imaging im, int x0, int y0, int x1, int y1, DRAWINIT(); - if (width <= 1) { - draw->line(im, x0, y0, x1, y1, ink); - return 0; - } - dx = x1-x0; dy = y1-y0; if (dx == 0 && dy == 0) { @@ -1030,20 +1025,6 @@ ImagingOutlineCurve(ImagingOutline outline, float x1, float y1, return 0; } -int -ImagingOutlineCurve2(ImagingOutline outline, float cx, float cy, - float x3, float y3) -{ - /* add bezier curve based on three control points (as - in the Flash file format) */ - - return ImagingOutlineCurve( - outline, - (outline->x + cx + cx)/3, (outline->y + cy + cy)/3, - (cx + cx + x3)/3, (cy + cy + y3)/3, - x3, y3); -} - int ImagingOutlineClose(ImagingOutline outline) { diff --git a/libImaging/Fill.c b/libImaging/Fill.c index 1e8dbf9d8..15107b6d6 100644 --- a/libImaging/Fill.c +++ b/libImaging/Fill.c @@ -64,15 +64,18 @@ ImagingFillLinearGradient(const char *mode) Imaging im; int y; - if (strlen(mode) != 1) - return (Imaging) ImagingError_ModeError(); + if (strlen(mode) != 1) { + return (Imaging) ImagingError_ModeError(); + } im = ImagingNew(mode, 256, 256); - if (!im) - return NULL; + if (!im) { + return NULL; + } - for (y = 0; y < 256; y++) - memset(im->image8[y], (unsigned char) y, 256); + for (y = 0; y < 256; y++) { + memset(im->image8[y], (unsigned char) y, 256); + } return im; } @@ -84,21 +87,25 @@ ImagingFillRadialGradient(const char *mode) int x, y; int d; - if (strlen(mode) != 1) - return (Imaging) ImagingError_ModeError(); + if (strlen(mode) != 1) { + return (Imaging) ImagingError_ModeError(); + } im = ImagingNew(mode, 256, 256); - if (!im) - return NULL; + if (!im) { + return NULL; + } - for (y = 0; y < 256; y++) - for (x = 0; x < 256; x++) { - d = (int) sqrt((double) ((x-128)*(x-128) + (y-128)*(y-128)) * 2.0); - if (d >= 255) - im->image8[y][x] = 255; - else - im->image8[y][x] = d; - } + for (y = 0; y < 256; y++) { + for (x = 0; x < 256; x++) { + d = (int) sqrt((double) ((x-128)*(x-128) + (y-128)*(y-128)) * 2.0); + if (d >= 255) { + im->image8[y][x] = 255; + } else { + im->image8[y][x] = d; + } + } + } return im; } diff --git a/libImaging/Filter.c b/libImaging/Filter.c index 9079fbf88..4c8b81587 100644 --- a/libImaging/Filter.c +++ b/libImaging/Filter.c @@ -33,13 +33,13 @@ ImagingExpand(Imaging imIn, int xmargin, int ymargin, int mode) int x, y; if (xmargin < 0 && ymargin < 0) - return (Imaging) ImagingError_ValueError("bad kernel size"); + return (Imaging) ImagingError_ValueError("bad kernel size"); imOut = ImagingNew( imIn->mode, imIn->xsize+2*xmargin, imIn->ysize+2*ymargin ); if (!imOut) - return NULL; + return NULL; #define EXPAND_LINE(type, image, yin, yout) {\ for (x = 0; x < xmargin; x++)\ @@ -61,9 +61,9 @@ ImagingExpand(Imaging imIn, int xmargin, int ymargin, int mode) } if (imIn->image8) { - EXPAND(UINT8, image8); + EXPAND(UINT8, image8); } else { - EXPAND(INT32, image32); + EXPAND(INT32, image32); } ImagingCopyInfo(imOut, imIn); @@ -80,20 +80,20 @@ ImagingFilter(Imaging im, int xsize, int ysize, const FLOAT32* kernel, FLOAT32 sum; if (!im || strcmp(im->mode, "L") != 0) - return (Imaging) ImagingError_ModeError(); + return (Imaging) ImagingError_ModeError(); if (im->xsize < xsize || im->ysize < ysize) return ImagingCopy(im); if ((xsize != 3 && xsize != 5) || xsize != ysize) - return (Imaging) ImagingError_ValueError("bad kernel size"); + return (Imaging) ImagingError_ValueError("bad kernel size"); imOut = ImagingNew(im->mode, im->xsize, im->ysize); if (!imOut) - return NULL; + return NULL; /* brute force kernel implementations */ -#define KERNEL3x3(image, kernel, d) ( \ +#define KERNEL3x3(image, kernel, d) ( \ (int) image[y+1][x-d] * kernel[0] + \ (int) image[y+1][x] * kernel[1] + \ (int) image[y+1][x+d] * kernel[2] + \ @@ -104,7 +104,7 @@ ImagingFilter(Imaging im, int xsize, int ysize, const FLOAT32* kernel, (int) image[y-1][x] * kernel[7] + \ (int) image[y-1][x+d] * kernel[8]) -#define KERNEL5x5(image, kernel, d) ( \ +#define KERNEL5x5(image, kernel, d) ( \ (int) image[y+2][x-d-d] * kernel[0] + \ (int) image[y+2][x-d] * kernel[1] + \ (int) image[y+2][x] * kernel[2] + \ @@ -132,47 +132,47 @@ ImagingFilter(Imaging im, int xsize, int ysize, const FLOAT32* kernel, (int) image[y-2][x+d+d] * kernel[24]) if (xsize == 3) { - /* 3x3 kernel. */ - for (x = 0; x < im->xsize; x++) - imOut->image[0][x] = im->image8[0][x]; - for (y = 1; y < im->ysize-1; y++) { - imOut->image[y][0] = im->image8[y][0]; - for (x = 1; x < im->xsize-1; x++) { - sum = KERNEL3x3(im->image8, kernel, 1) / divisor + offset; - if (sum <= 0) - imOut->image8[y][x] = 0; - else if (sum >= 255) - imOut->image8[y][x] = 255; - else - imOut->image8[y][x] = (UINT8) sum; - } - imOut->image8[y][x] = im->image8[y][x]; - } - for (x = 0; x < im->xsize; x++) - imOut->image8[y][x] = im->image8[y][x]; + /* 3x3 kernel. */ + for (x = 0; x < im->xsize; x++) + imOut->image[0][x] = im->image8[0][x]; + for (y = 1; y < im->ysize-1; y++) { + imOut->image[y][0] = im->image8[y][0]; + for (x = 1; x < im->xsize-1; x++) { + sum = KERNEL3x3(im->image8, kernel, 1) / divisor + offset; + if (sum <= 0) + imOut->image8[y][x] = 0; + else if (sum >= 255) + imOut->image8[y][x] = 255; + else + imOut->image8[y][x] = (UINT8) sum; + } + imOut->image8[y][x] = im->image8[y][x]; + } + for (x = 0; x < im->xsize; x++) + imOut->image8[y][x] = im->image8[y][x]; } else { - /* 5x5 kernel. */ - for (y = 0; y < 2; y++) - for (x = 0; x < im->xsize; x++) - imOut->image8[y][x] = im->image8[y][x]; - for (; y < im->ysize-2; y++) { - for (x = 0; x < 2; x++) - imOut->image8[y][x] = im->image8[y][x]; - for (; x < im->xsize-2; x++) { - sum = KERNEL5x5(im->image8, kernel, 1) / divisor + offset; - if (sum <= 0) - imOut->image8[y][x] = 0; - else if (sum >= 255) - imOut->image8[y][x] = 255; - else - imOut->image8[y][x] = (UINT8) sum; - } - for (; x < im->xsize; x++) - imOut->image8[y][x] = im->image8[y][x]; - } - for (; y < im->ysize; y++) - for (x = 0; x < im->xsize; x++) - imOut->image8[y][x] = im->image8[y][x]; + /* 5x5 kernel. */ + for (y = 0; y < 2; y++) + for (x = 0; x < im->xsize; x++) + imOut->image8[y][x] = im->image8[y][x]; + for (; y < im->ysize-2; y++) { + for (x = 0; x < 2; x++) + imOut->image8[y][x] = im->image8[y][x]; + for (; x < im->xsize-2; x++) { + sum = KERNEL5x5(im->image8, kernel, 1) / divisor + offset; + if (sum <= 0) + imOut->image8[y][x] = 0; + else if (sum >= 255) + imOut->image8[y][x] = 255; + else + imOut->image8[y][x] = (UINT8) sum; + } + for (; x < im->xsize; x++) + imOut->image8[y][x] = im->image8[y][x]; + } + for (; y < im->ysize; y++) + for (x = 0; x < im->xsize; x++) + imOut->image8[y][x] = im->image8[y][x]; } return imOut; } diff --git a/libImaging/ImPlatform.h b/libImaging/ImPlatform.h index a04bd9dcd..7b42510d4 100644 --- a/libImaging/ImPlatform.h +++ b/libImaging/ImPlatform.h @@ -9,6 +9,11 @@ #include "Python.h" +/* Workaround issue #2479 */ +#if PY_VERSION_HEX < 0x03070000 && defined(PySlice_GetIndicesEx) && !defined(PYPY_VERSION) +#undef PySlice_GetIndicesEx +#endif + /* Check that we have an ANSI compliant compiler */ #ifndef HAVE_PROTOTYPES #error Sorry, this library requires support for ANSI prototypes. diff --git a/libImaging/Imaging.h b/libImaging/Imaging.h index 686bb82ec..b32347680 100644 --- a/libImaging/Imaging.h +++ b/libImaging/Imaging.h @@ -455,6 +455,7 @@ extern int ImagingXbmEncode(Imaging im, ImagingCodecState state, #ifdef HAVE_LIBZ extern int ImagingZipDecode(Imaging im, ImagingCodecState state, UINT8* buffer, int bytes); +extern int ImagingZipDecodeCleanup(ImagingCodecState state); extern int ImagingZipEncode(Imaging im, ImagingCodecState state, UINT8* buffer, int bytes); extern int ImagingZipEncodeCleanup(ImagingCodecState state); diff --git a/libImaging/JpegDecode.c b/libImaging/JpegDecode.c index 6ebdb8f93..4bb929b6a 100644 --- a/libImaging/JpegDecode.c +++ b/libImaging/JpegDecode.c @@ -268,7 +268,7 @@ ImagingJpegDecode(Imaging im, ImagingCodecState state, UINT8* buf, int bytes) /* -------------------------------------------------------------------- */ int ImagingJpegDecodeCleanup(ImagingCodecState state){ - /* called to fee the decompression engine when the decode terminates + /* called to free the decompression engine when the decode terminates due to a corrupt or truncated image */ JPEGSTATE* context = (JPEGSTATE*) state->context; diff --git a/libImaging/MspDecode.c b/libImaging/MspDecode.c deleted file mode 100644 index b611098d8..000000000 --- a/libImaging/MspDecode.c +++ /dev/null @@ -1,91 +0,0 @@ -/* - * The Python Imaging Library. - * $Id$ - * - * decoder for MSP version 2 data. - * - * history: - * 97-01-03 fl Created - * - * Copyright (c) Fredrik Lundh 1997. - * Copyright (c) Secret Labs AB 1997. - * - * See the README file for information on usage and redistribution. - */ - - -#include "Imaging.h" - - -int -ImagingMspDecode(Imaging im, ImagingCodecState state, UINT8* buf, int bytes) -{ - int n; - UINT8* ptr; - - ptr = buf; - - for (;;) { - - if (bytes < 1) - return ptr - buf; - - if (ptr[0] == 0) { - - /* Run (3 bytes block) */ - if (bytes < 3) - break; - - n = ptr[1]; - - if (state->x + n > state->bytes) { - state->errcode = IMAGING_CODEC_OVERRUN; - return -1; - } - - memset(state->buffer + state->x, ptr[2], n); - - ptr += 3; - bytes -= 3; - - } else { - - /* Literal (1+n bytes block) */ - n = ptr[0]; - - if (bytes < 1 + n) - break; - - if (state->x + n > state->bytes) { - state->errcode = IMAGING_CODEC_OVERRUN; - return -1; - } - - memcpy(state->buffer + state->x, ptr + 1, n); - - ptr += 1 + n; - bytes -= 1 + n; - - } - - 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; - } - } - - } - - return ptr - buf; -} 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 64f780c71..18aed037d 100644 --- a/libImaging/Resample.c +++ b/libImaging/Resample.c @@ -32,6 +32,8 @@ static inline double hamming_filter(double x) x = -x; if (x == 0.0) return 1.0; + if (x >= 1.0) + return 0.0; x = x * M_PI; return sin(x) / x * (0.54f + 0.46f * cos(x)); } @@ -171,10 +173,12 @@ precompute_coeffs(int inSize, float in0, float in1, int outSize, center = in0 + (xx + 0.5) * scale; ww = 0.0; ss = 1.0 / filterscale; - xmin = (int) floor(center - support); + // Round the value + xmin = (int) (center - support + 0.5); if (xmin < 0) xmin = 0; - xmax = (int) ceil(center + support); + // Round the value + xmax = (int) (center + support + 0.5); if (xmax > inSize) xmax = inSize; xmax -= xmin; @@ -183,21 +187,6 @@ precompute_coeffs(int inSize, float in0, float in1, int outSize, 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; - } - } } for (x = 0; x < xmax; x++) { if (ww != 0.0) diff --git a/libImaging/Storage.c b/libImaging/Storage.c index 27661bfdb..fb27572d3 100644 --- a/libImaging/Storage.c +++ b/libImaging/Storage.c @@ -398,6 +398,9 @@ ImagingNew(const char* mode, int xsize, int ysize) int bytes; Imaging im; + if (strcmp(mode, "") == 0) + return (Imaging) ImagingError_ValueError("empty mode"); + if (strlen(mode) == 1) { if (mode[0] == 'F' || mode[0] == 'I') bytes = 4; diff --git a/libImaging/SunRleDecode.c b/libImaging/SunRleDecode.c index 09375cfa5..50d816e38 100644 --- a/libImaging/SunRleDecode.c +++ b/libImaging/SunRleDecode.c @@ -58,7 +58,7 @@ ImagingSunRleDecode(Imaging im, ImagingCodecState state, UINT8* buf, int bytes) if (bytes < 3) break; - /* from (http://www.fileformat.info/format/sunraster/egff.htm) + /* from (https://www.fileformat.info/format/sunraster/egff.htm) For example, a run of 100 pixels with the value of 0Ah would encode as the values 80h 64h 0Ah. A @@ -67,8 +67,8 @@ ImagingSunRleDecode(Imaging im, ImagingCodecState state, UINT8* buf, int bytes) 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) + But Wait! There's More! + (https://www.fileformat.info/format/sunraster/spec/598a59c4fac64c52897585d390d86360/view.htm) If the first byte is 0x80, and the second byte is not zero, the record is three bytes long. The @@ -78,7 +78,7 @@ ImagingSunRleDecode(Imaging im, ImagingCodecState state, UINT8* buf, int bytes) 2 specs, same site, but Imagemagick and GIMP seem to agree on the second one. */ - n += 1; + n += 1; if (state->x + n > state->bytes) { extra_bytes = n; /* full value */ @@ -96,8 +96,8 @@ ImagingSunRleDecode(Imaging im, ImagingCodecState state, UINT8* buf, int bytes) } else { - /* Literal byte */ - n = 1; + /* Literal byte */ + n = 1; state->buffer[state->x] = ptr[0]; @@ -108,7 +108,7 @@ ImagingSunRleDecode(Imaging im, ImagingCodecState state, UINT8* buf, int bytes) for (;;) { state->x += n; - + if (state->x >= state->bytes) { /* Got a full line, unpack it */ @@ -123,7 +123,7 @@ ImagingSunRleDecode(Imaging im, ImagingCodecState state, UINT8* buf, int bytes) return -1; } } - + if (extra_bytes == 0) { break; } 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/libImaging/ZipDecode.c b/libImaging/ZipDecode.c index 37cb2866c..e96e3200c 100644 --- a/libImaging/ZipDecode.c +++ b/libImaging/ZipDecode.c @@ -85,6 +85,8 @@ ImagingZipDecode(Imaging im, ImagingCodecState state, UINT8* buf, int bytes) err = inflateInit(&context->z_stream); if (err < 0) { state->errcode = IMAGING_CODEC_CONFIG; + free(context->previous); + context->previous = NULL; return -1; } @@ -126,6 +128,7 @@ ImagingZipDecode(Imaging im, ImagingCodecState state, UINT8* buf, int bytes) else state->errcode = IMAGING_CODEC_CONFIG; free(context->previous); + context->previous = NULL; inflateEnd(&context->z_stream); return -1; } @@ -191,6 +194,7 @@ ImagingZipDecode(Imaging im, ImagingCodecState state, UINT8* buf, int bytes) default: state->errcode = IMAGING_CODEC_UNKNOWN; free(context->previous); + context->previous = NULL; inflateEnd(&context->z_stream); return -1; } @@ -258,6 +262,7 @@ ImagingZipDecode(Imaging im, ImagingCodecState state, UINT8* buf, int bytes) state->errcode = IMAGING_CODEC_BROKEN; */ free(context->previous); + context->previous = NULL; inflateEnd(&context->z_stream); return -1; /* end of file (errcode=0) */ @@ -274,4 +279,20 @@ ImagingZipDecode(Imaging im, ImagingCodecState state, UINT8* buf, int bytes) } + +int ImagingZipDecodeCleanup(ImagingCodecState state){ + /* called to free the decompression engine when the decode terminates + due to a corrupt or truncated image + */ + ZIPSTATE* context = (ZIPSTATE*) state->context; + + /* Clean up */ + if (context->previous) { + inflateEnd(&context->z_stream); + free(context->previous); + context->previous = NULL; + } + return -1; +} + #endif diff --git a/map.c b/map.c index 75f463440..9d4751e31 100644 --- a/map.c +++ b/map.c @@ -342,7 +342,7 @@ PyImaging_MapBuffer(PyObject* self, PyObject* args) stride = xsize * 4; } - if (ysize > INT_MAX / stride) { + if (stride > 0 && ysize > INT_MAX / stride) { PyErr_SetString(PyExc_MemoryError, "Integer overflow in ysize"); return NULL; } @@ -352,7 +352,7 @@ PyImaging_MapBuffer(PyObject* self, PyObject* args) 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) diff --git a/mp_compile.py b/mp_compile.py index 3139e0bb9..59f14a651 100644 --- a/mp_compile.py +++ b/mp_compile.py @@ -54,7 +54,8 @@ def _mp_compile(self, sources, output_dir=None, macros=None, def install(): - fl_pypy3 = hasattr(sys, 'pypy_version_info') and sys.version_info > (3, 0) + fl_pypy3 = (hasattr(sys, 'pypy_version_info') and + (3, 0) < sys.version_info < (3, 3)) fl_win = sys.platform.startswith('win') fl_cygwin = sys.platform.startswith('cygwin') @@ -72,8 +73,8 @@ def install(): # explicitly don't enable if environment says 1 processor try: # bug, only enable if we can make a Pool. see issue #790 and - # http://stackoverflow.com/questions/6033599/oserror-38-errno-38-with-multiprocessing - pool = Pool(2) + # https://stackoverflow.com/questions/6033599/oserror-38-errno-38-with-multiprocessing + Pool(2) CCompiler.compile = _mp_compile except Exception as msg: print("Exception installing mp_compile, proceeding without:" @@ -82,4 +83,5 @@ def install(): print("Single threaded build, not installing mp_compile:" "%s processes" % MAX_PROCS) + install() diff --git a/path.c b/path.c index 824fbdd89..b56ea838e 100644 --- a/path.c +++ b/path.c @@ -27,6 +27,7 @@ #include "Python.h" +#include "Imaging.h" #include @@ -312,22 +313,6 @@ path_compact(PyPathObject* self, PyObject* args) return Py_BuildValue("i", i); /* number of removed vertices */ } -static PyObject* -path_clip_polygon(PyPathObject* self, PyObject* args) -{ - /* Clip path representing a single polygon */ - PyErr_SetString(PyExc_RuntimeError, "not yet implemented"); - return NULL; -} - -static PyObject* -path_clip_polyline(PyPathObject* self, PyObject* args) -{ - /* Clip path representing a single polyline (outline) */ - PyErr_SetString(PyExc_RuntimeError, "not yet implemented"); - return NULL; -} - static PyObject* path_getbbox(PyPathObject* self, PyObject* args) { @@ -533,8 +518,6 @@ path_transform(PyPathObject* self, PyObject* args) static struct PyMethodDef methods[] = { {"getbbox", (PyCFunction)path_getbbox, 1}, {"tolist", (PyCFunction)path_tolist, 1}, - {"clip_polygon", (PyCFunction)path_clip_polygon, 1}, - {"clip_polyline", (PyCFunction)path_clip_polyline, 1}, {"compact", (PyCFunction)path_compact, 1}, {"map", (PyCFunction)path_map, 1}, {"transform", (PyCFunction)path_transform, 1}, diff --git a/selftest.py b/selftest.py old mode 100644 new mode 100755 index 7829bae5b..108e57fd2 --- a/selftest.py +++ b/selftest.py @@ -1,3 +1,4 @@ +#!/usr/bin/env python # minimal sanity check from __future__ import print_function @@ -177,25 +178,15 @@ if __name__ == "__main__": ("freetype2", "FREETYPE2"), ("littlecms2", "LITTLECMS2"), ("webp", "WEBP"), - ("transp_webp", "Transparent WEBP") - ]: - supported = features.check_module(name) - - if supported is None: - # A method was being tested, but the module required - # for the method could not be correctly imported - pass - elif supported: - print("---", feature, "support ok") - else: - print("***", feature, "support not installed") - for name, feature in [ + ("transp_webp", "Transparent WEBP"), + ("webp_mux", "WEBPMUX"), ("jpg", "JPEG"), ("jpg_2000", "OPENJPEG (JPEG2000)"), ("zlib", "ZLIB (PNG/ZIP)"), - ("libtiff", "LIBTIFF") + ("libtiff", "LIBTIFF"), + ("raqm", "RAQM (Bidirectional Text)") ]: - if features.check_codec(name): + if features.check(name): print("---", feature, "support ok") else: print("***", feature, "support not installed") diff --git a/setup.py b/setup.py old mode 100644 new mode 100755 index 808ec728a..1cbfc259a --- a/setup.py +++ b/setup.py @@ -1,3 +1,4 @@ +#!/usr/bin/env python # > pyroma . # ------------------------------ # Checking . @@ -31,7 +32,7 @@ _LIB_IMAGING = ( "Draw", "Effects", "EpsEncode", "File", "Fill", "Filter", "FliDecode", "Geometry", "GetBBox", "GifDecode", "GifEncode", "HexDecode", "Histo", "JpegDecode", "JpegEncode", "LzwDecode", "Matrix", "ModeFilter", - "MspDecode", "Negative", "Offset", "Pack", "PackDecode", "Palette", "Paste", + "Negative", "Offset", "Pack", "PackDecode", "Palette", "Paste", "Quant", "QuantOctree", "QuantHash", "QuantHeap", "PcdDecode", "PcxDecode", "PcxEncode", "Point", "RankFilter", "RawDecode", "RawEncode", "Storage", "SunRleDecode", "TgaRleDecode", "Unpack", "UnpackYCC", "UnsharpMask", @@ -62,6 +63,9 @@ def _add_directory(path, subdir, where=None): else: _dbg('Inserting path %s', subdir) path.insert(where, subdir) + elif subdir in path and where is not None: + path.remove(subdir) + path.insert(where, subdir) def _find_include_file(self, include): @@ -83,13 +87,9 @@ def _find_library_file(self, library): return ret -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) + os.access(os.path.join(path, cmd), os.X_OK) for path in os.environ["PATH"].split(os.pathsep) ) @@ -98,6 +98,12 @@ def _read(file): return fp.read() +def get_version(): + version_file = 'PIL/version.py' + with open(version_file, 'r') as f: + exec(compile(f.read(), version_file, 'exec')) + return locals()['__version__'] + try: import _tkinter except (ImportError, OSError): @@ -105,7 +111,7 @@ except (ImportError, OSError): _tkinter = None NAME = 'Pillow' -PILLOW_VERSION = '3.5.0.dev0' +PILLOW_VERSION = get_version() JPEG_ROOT = None JPEG2K_ROOT = None ZLIB_ROOT = None @@ -113,7 +119,7 @@ IMAGEQUANT_ROOT = None TIFF_ROOT = None FREETYPE_ROOT = None LCMS_ROOT = None - +RAQM_ROOT = None def _pkg_config(name): try: @@ -131,7 +137,7 @@ def _pkg_config(name): class pil_build_ext(build_ext): class feature: - features = ['zlib', 'jpeg', 'tiff', 'freetype', 'lcms', 'webp', + features = ['zlib', 'jpeg', 'tiff', 'freetype', 'raqm', 'lcms', 'webp', 'webpmux', 'jpeg2000', 'imagequant'] required = {'jpeg', 'zlib'} @@ -465,6 +471,10 @@ class pil_build_ext(build_ext): # Add the directory to the include path so we can include # rather than having to cope with the versioned # include path + # FIXME (melvyn-sopacua): + # At this point it's possible that best_path is already in + # self.compiler.include_dirs. Should investigate how that is + # possible. _add_directory(self.compiler.include_dirs, best_path, 0) feature.jpeg2000 = 'openjp2' feature.openjpeg_version = '.'.join(str(x) for x in best_version) @@ -512,6 +522,14 @@ class pil_build_ext(build_ext): if subdir: _add_directory(self.compiler.include_dirs, subdir, 0) + if feature.want('raqm'): + _dbg('Looking for raqm') + if _find_include_file(self, "raqm.h"): + if _find_library_file(self, "raqm") and \ + _find_library_file(self, "harfbuzz") and \ + _find_library_file(self, "fribidi"): + feature.raqm = ["raqm", "harfbuzz", "fribidi"] + if feature.want('lcms'): _dbg('Looking for lcms') if _find_include_file(self, "lcms2.h"): @@ -547,7 +565,7 @@ class pil_build_ext(build_ext): if f in ('jpeg', 'zlib'): raise RequiredDependencyException(f) raise DependencyException(f) - + # # core library @@ -581,6 +599,11 @@ class pil_build_ext(build_ext): if struct.unpack("h", "\0\1".encode('ascii'))[0] == 1: defs.append(("WORDS_BIGENDIAN", None)) + if sys.platform == "win32" and not hasattr(sys, 'pypy_version_info'): + defs.append(("PILLOW_VERSION", '"\\"%s\\""'%PILLOW_VERSION)) + else: + defs.append(("PILLOW_VERSION", '"%s"'%PILLOW_VERSION)) + exts = [(Extension("PIL._imaging", files, libraries=libs, @@ -590,9 +613,14 @@ class pil_build_ext(build_ext): # additional libraries if feature.freetype: - exts.append(Extension("PIL._imagingft", - ["_imagingft.c"], - libraries=["freetype"])) + libs = ["freetype"] + defs = [] + if feature.raqm: + libs.extend(feature.raqm) + defs.append(('HAVE_RAQM', None)) + exts.append(Extension( + "PIL._imagingft", ["_imagingft.c"], libraries=libs, + define_macros=defs)) if feature.lcms: extra = [] @@ -630,16 +658,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") @@ -659,6 +682,7 @@ class pil_build_ext(build_ext): (feature.imagequant, "LIBIMAGEQUANT"), (feature.tiff, "LIBTIFF"), (feature.freetype, "FREETYPE2"), + (feature.raqm, "RAQM"), (feature.lcms, "LITTLECMS2"), (feature.webp, "WEBP"), (feature.webpmux, "WEBPMUX"), @@ -675,16 +699,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: @@ -696,21 +710,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. @@ -727,9 +726,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, @@ -748,7 +746,7 @@ try: long_description=_read('README.rst').decode('utf-8'), author='Alex Clark (Fork Author)', author_email='aclark@aclark.net', - url='http://python-pillow.org', + url='https://python-pillow.org', classifiers=[ "Development Status :: 6 - Mature", "Topic :: Multimedia :: Graphics", @@ -762,6 +760,7 @@ try: "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', ], @@ -770,6 +769,7 @@ try: 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', @@ -781,7 +781,7 @@ 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 + https://pillow.readthedocs.io/en/latest/installation.html """ % (str(err)) sys.stderr.write(msg) diff --git a/tox.ini b/tox.ini index 4d63febd0..84ac7135e 100644 --- a/tox.ini +++ b/tox.ini @@ -1,10 +1,10 @@ -# Tox (https://testrun.org/tox/latest/) is a tool for running tests +# Tox (https://tox.readthedocs.io/en/latest/) is a tool for running tests # in multiple virtualenvs. This configuration file will run the # test suite on all supported python versions. To use it, "pip install tox" # and then run "tox" from this directory. [tox] -envlist = py27, py33, py34, py35 +envlist = py27, py33, py34, py35, py36 [testenv] commands = diff --git a/winbuild/appveyor_install_pypy.cmd b/winbuild/appveyor_install_pypy.cmd new file mode 100644 index 000000000..39bf66782 --- /dev/null +++ b/winbuild/appveyor_install_pypy.cmd @@ -0,0 +1,3 @@ +curl -fsSL -o pypy2.zip https://bitbucket.org/pypy/pypy/downloads/pypy2-v5.8.0-win32.zip +7z x pypy2.zip -oc:\ +c:\Python34\Scripts\virtualenv.exe -p c:\pypy2-v5.8.0-win32\pypy.exe c:\vp\pypy2 \ No newline at end of file diff --git a/winbuild/build.py b/winbuild/build.py old mode 100644 new mode 100755 index 487977adb..3d6c6e57f --- a/winbuild/build.py +++ b/winbuild/build.py @@ -12,11 +12,11 @@ from config import (compilers, compiler_from_env, pythons, pyversion_from_env, def setup_vms(): ret = [] - for py in pythons.keys(): + for py in pythons: 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)) return "\n".join(ret) @@ -72,6 +72,11 @@ def build_one(py_ver, compiler): args['python_path'] = "%PYTHON%" else: args['python_path'] = "%s%s\\Scripts" % (VIRT_BASE, py_ver) + + args['executable'] = "python.exe" + if 'EXECUTABLE' in os.environ: + args['executable'] = "%EXECUTABLE%" + args['py_ver'] = py_ver if '34' in py_ver: args['tcl_ver'] = '86' @@ -87,7 +92,7 @@ set INCLUDE=%%INCLUDE%%;%%INCLIB%%\%(inc_dir)s;%%INCLIB%%\tcl%(tcl_ver)s\include setlocal set LIB=%%LIB%%;C:\Python%(py_ver)s\tcl -call %(python_path)s\python.exe setup.py %%BLDOPT%% +call %(python_path)s\%(executable)s setup.py %%BLDOPT%% endlocal endlocal diff --git a/winbuild/build_dep.py b/winbuild/build_dep.py index 4c397236b..a059b1ee8 100644 --- a/winbuild/build_dep.py +++ b/winbuild/build_dep.py @@ -1,7 +1,6 @@ from unzip import unzip from untar import untar import os -import hashlib from config import compilers, compiler_from_env, libs @@ -10,26 +9,10 @@ def _relpath(*args): return os.path.join(os.getcwd(), *args) -def _relbuild(*args): - return _relpath('build', *args) - build_dir = _relpath('build') inc_dir = _relpath('depends') -def check_hash(filename, checksum): - if not checksum: - return filename - - (algo, value) = checksum.split(':') - h = hashlib.new(algo) - with open(filename, 'rb') as f: - h.update(f.read()) - if not(h.hexdigest().lower() == value): - raise ValueError('Checksum Mismatch for %s' % filename) - return filename - - def check_sig(filename, signame): # UNDONE -- need gpg return filename @@ -60,8 +43,8 @@ def extract(src, dest): def extract_libs(): for name, lib in libs.items(): + filename = lib['filename'] if name == 'openjpeg': - filename = check_hash(lib['filename'], lib['hash']) for compiler in compilers.values(): if not os.path.exists(os.path.join( build_dir, lib['dir']+compiler['inc_dir'])): @@ -70,7 +53,7 @@ def extract_libs(): os.path.join( build_dir, lib['dir']+compiler['inc_dir'])) else: - extract(check_hash(lib['filename'], lib['hash']), build_dir) + extract(filename, build_dir) def extract_openjpeg(compiler): @@ -108,7 +91,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 344c0be35..c7a6fe675 100644 --- a/winbuild/config.py +++ b/winbuild/config.py @@ -5,6 +5,7 @@ PILLOW_DEPENDS_DIR = 'C:\\pillow-depends\\' pythons = { # '26': 7, '27': 7, + 'pypy2': 7, # '32': 7, '33': 7.1, '34': 7.1} @@ -15,75 +16,63 @@ X64_EXT = os.environ.get('X64_EXT', "x64") libs = { # 'openjpeg': { # 'filename': 'openjpeg-2.0.0-win32-x86.zip', - # 'hash': 'sha1:xxx', # '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', + 'dir': 'zlib-1.2.11', }, 'jpeg': { 'url': 'http://www.ijg.org/files/jpegsr9b.zip', 'filename': PILLOW_DEPENDS_DIR + 'jpegsr9b.zip', - 'hash': 'md5:a21b8024d78ba05857a75272b4fa95ec', # not found - generated by wiredfool 'dir': 'jpeg-9b', }, 'tiff': { - 'url': 'ftp://ftp.remotesensing.org/pub/libtiff/tiff-4.0.6.zip', - 'filename': PILLOW_DEPENDS_DIR + 'tiff-4.0.6.zip', - 'hash': 'md5:f5b485d750b2001255ed64224b98b857', - 'dir': 'tiff-4.0.6', + 'url': 'ftp://download.osgeo.org/libtiff/tiff-4.0.8.zip', + 'filename': PILLOW_DEPENDS_DIR + 'tiff-4.0.8.zip', + 'dir': 'tiff-4.0.8', }, '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': 'https://download.savannah.gnu.org/releases/freetype/freetype-2.8.tar.gz', + 'filename': PILLOW_DEPENDS_DIR + 'freetype-2.8.tar.gz', + 'dir': 'freetype-2.8', }, 'lcms': { 'url': SF_MIRROR+'/project/lcms/lcms/2.7/lcms2-2.7.zip', 'filename': PILLOW_DEPENDS_DIR + 'lcms2-2.7.zip', - 'hash': 'sha1:7ff1a5b721ca719760ba6eb4ec6f38d5e65381cf', 'dir': 'lcms2-2.7', }, 'tcl-8.5': { 'url': SF_MIRROR+'/project/tcl/Tcl/8.5.19/tcl8519-src.zip', 'filename': PILLOW_DEPENDS_DIR + 'tcl8519-src.zip', - 'hash': 'sha1:9de57fd34bd688716c16c978db96fa16a5fde924', 'dir': '', }, 'tk-8.5': { 'url': SF_MIRROR+'/project/tcl/Tcl/8.5.19/tk8519-src.zip', 'filename': PILLOW_DEPENDS_DIR + 'tk8519-src.zip', - 'hash': 'sha1:78d0d2c81e024e0b48bfd7b2cc16718f08f46ed9', 'dir': '', 'version': '8.5.19', }, 'tcl-8.6': { '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.6/tk866-src.zip', 'filename': PILLOW_DEPENDS_DIR + 'tk866-src.zip', - 'hash': 'md5:5004cc0ed2ab820406a36a0c0553b917', 'dir': '', 'version': '8.6.6', }, 'webp': { - 'url': 'http://downloads.webmproject.org/releases/webp/libwebp-0.5.1.tar.gz', - 'filename': PILLOW_DEPENDS_DIR + 'libwebp-0.5.1.tar.gz', - 'hash': 'sha1:66efb2213015ad3460bef64b4fb218fdc10ce83f', - 'dir': 'libwebp-0.5.1', + 'url': 'http://downloads.webmproject.org/releases/webp/libwebp-0.6.0.tar.gz', + 'filename': PILLOW_DEPENDS_DIR + 'libwebp-0.6.0.tar.gz', + 'dir': 'libwebp-0.6.0', }, 'openjpeg': { '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', }, } @@ -128,7 +117,7 @@ def pyversion_from_env(): py = os.environ['PYTHON'] py_version = '27' - for k in pythons.keys(): + for k in pythons: if k in py: py_version = k break diff --git a/winbuild/test.py b/winbuild/test.py old mode 100644 new mode 100755