diff --git a/.appveyor.yml b/.appveyor.yml index 4e2ca1071..0cf1d5a9e 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -23,9 +23,9 @@ install: - 7z x pillow-depends.zip -oc:\ - mv c:\pillow-depends-master c:\pillow-depends - xcopy /S /Y c:\pillow-depends\test_images\* c:\pillow\tests\images -- 7z x ..\pillow-depends\nasm-2.14.02-win64.zip -oc:\ -- ..\pillow-depends\gs9533w32.exe /S -- path c:\nasm-2.14.02;C:\Program Files (x86)\gs\gs9.53.3\bin;%PATH% +- 7z x ..\pillow-depends\nasm-2.15.05-win64.zip -oc:\ +- ..\pillow-depends\gs9540w32.exe /S +- path c:\nasm-2.15.05;C:\Program Files (x86)\gs\gs9.54.0\bin;%PATH% - cd c:\pillow\winbuild\ - ps: | c:\python37\python.exe c:\pillow\winbuild\build_prepare.py -v --depends=C:\pillow-depends\ diff --git a/.ci/install.sh b/.ci/install.sh index 4917b3a7c..0dbf2d690 100755 --- a/.ci/install.sh +++ b/.ci/install.sh @@ -24,6 +24,7 @@ sudo apt-get -qq install libfreetype6-dev liblcms2-dev python3-tk\ python3 -m pip install --upgrade pip PYTHONOPTIMIZE=0 python3 -m pip install cffi python3 -m pip install coverage +python3 -m pip install defusedxml python3 -m pip install olefile python3 -m pip install -U pytest python3 -m pip install -U pytest-cov @@ -33,10 +34,6 @@ python3 -m pip install test-image-results # TODO Remove condition when numpy supports 3.10 if ! [ "$GHA_PYTHON_VERSION" == "3.10-dev" ]; then python3 -m pip install numpy ; fi -# TODO Remove when 3.8 / 3.9 includes setuptools 49.3.2+: -if [ "$GHA_PYTHON_VERSION" == "3.8" ]; then python3 -m pip install -U "setuptools>=49.3.2" ; fi -if [ "$GHA_PYTHON_VERSION" == "3.9" ]; then python3 -m pip install -U "setuptools>=49.3.2" ; fi - # PyQt5 doesn't support PyPy3 # Wheel doesn't yet support 3.10 if [[ $GHA_PYTHON_VERSION == 3.* && $GHA_PYTHON_VERSION != "3.10-dev" ]]; then diff --git a/.ci/test.sh b/.ci/test.sh index 9d2c123da..8ff7c5f64 100755 --- a/.ci/test.sh +++ b/.ci/test.sh @@ -4,4 +4,4 @@ set -e python3 -c "from PIL import Image" -python3 -bb -m pytest -v -x -W always --cov PIL --cov Tests --cov-report term Tests +python3 -bb -m pytest -v -x -W always --cov PIL --cov Tests --cov-report term Tests $REVERSE diff --git a/.github/workflows/macos-install.sh b/.github/workflows/macos-install.sh index f45824445..3a70c8047 100755 --- a/.github/workflows/macos-install.sh +++ b/.github/workflows/macos-install.sh @@ -6,6 +6,7 @@ brew install libtiff libjpeg openjpeg libimagequant webp little-cms2 freetype op PYTHONOPTIMIZE=0 python3 -m pip install cffi python3 -m pip install coverage +python3 -m pip install defusedxml python3 -m pip install olefile python3 -m pip install -U pytest python3 -m pip install -U pytest-cov @@ -17,9 +18,5 @@ echo -e "[openblas]\nlibraries = openblas\nlibrary_dirs = /usr/local/opt/openbla # TODO Remove condition when numpy supports 3.10 if ! [ "$GHA_PYTHON_VERSION" == "3.10-dev" ]; then python3 -m pip install numpy ; fi -# TODO Remove when 3.8 / 3.9 includes setuptools 49.3.2+: -if [ "$GHA_PYTHON_VERSION" == "3.8" ]; then python3 -m pip install -U "setuptools>=49.3.2" ; fi -if [ "$GHA_PYTHON_VERSION" == "3.9" ]; then python3 -m pip install -U "setuptools>=49.3.2" ; fi - # extra test images pushd depends && ./install_extra_test_images.sh && popd diff --git a/.github/workflows/test-docker.yml b/.github/workflows/test-docker.yml index 2ecc27460..8274549d4 100644 --- a/.github/workflows/test-docker.yml +++ b/.github/workflows/test-docker.yml @@ -21,8 +21,8 @@ jobs: centos-7-amd64, centos-8-amd64, debian-10-buster-x86, - fedora-32-amd64, fedora-33-amd64, + fedora-34-amd64, ubuntu-18.04-bionic-amd64, ubuntu-20.04-focal-amd64, ] diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index e3b2201a7..ce04ba5ca 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -8,19 +8,13 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["pypy-3.6", "pypy-3.7", "3.6", "3.7", "3.8", "3.9", "3.10-dev"] + python-version: ["3.6", "3.7", "3.8", "3.9", "3.10-dev"] architecture: ["x86", "x64"] include: - - architecture: "x86" - platform-vcvars: "x86" - platform-msbuild: "Win32" - - architecture: "x64" - platform-vcvars: "x86_amd64" - platform-msbuild: "x64" - exclude: - # PyPy does not support 64-bit on Windows + # PyPy3.6 only ships 32-bit binaries for Windows - python-version: "pypy-3.6" - architecture: "x64" + architecture: "x86" + # PyPy 7.3.4+ only ships 64-bit binaries for Windows - python-version: "pypy-3.7" architecture: "x64" timeout-minutes: 30 @@ -57,22 +51,17 @@ jobs: - name: Print build system information run: python .github/workflows/system-info.py - - name: python -m pip install wheel pytest pytest-cov pytest-timeout - run: python -m pip install wheel pytest pytest-cov pytest-timeout - - # TODO Remove when 3.8 / 3.9 includes setuptools 49.3.2+: - - name: Upgrade setuptools - if: "contains(matrix.python-version, '3.8') || contains(matrix.python-version, '3.9')" - run: python -m pip install -U "setuptools>=49.3.2" + - name: python -m pip install wheel pytest pytest-cov pytest-timeout defusedxml + run: python -m pip install wheel pytest pytest-cov pytest-timeout defusedxml - name: Install dependencies id: install run: | - 7z x winbuild\depends\nasm-2.14.02-win64.zip "-o$env:RUNNER_WORKSPACE\" - echo "$env:RUNNER_WORKSPACE\nasm-2.14.02" >> $env:GITHUB_PATH + 7z x winbuild\depends\nasm-2.15.05-win64.zip "-o$env:RUNNER_WORKSPACE\" + echo "$env:RUNNER_WORKSPACE\nasm-2.15.05" >> $env:GITHUB_PATH - winbuild\depends\gs9533w32.exe /S - echo "C:\Program Files (x86)\gs\gs9.53.3\bin" >> $env:GITHUB_PATH + winbuild\depends\gs9540w32.exe /S + echo "C:\Program Files (x86)\gs\gs9.54.0\bin" >> $env:GITHUB_PATH xcopy /S /Y winbuild\depends\test_images\* Tests\images\ diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e52fefc69..042e6d83e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -24,6 +24,7 @@ jobs: include: - python-version: "3.6" PYTHONOPTIMIZE: 1 + REVERSE: "--reverse" - python-version: "3.7" PYTHONOPTIMIZE: 2 # Include new variables for Codecov @@ -80,6 +81,9 @@ jobs: - name: Test run: | + if [ $REVERSE ]; then + python3 -m pip install pytest-reverse + fi if [ "${{ matrix.os }}" = "ubuntu-latest" ]; then xvfb-run -s '-screen 0 1024x768x24' .ci/test.sh else @@ -87,6 +91,7 @@ jobs: fi env: PYTHONOPTIMIZE: ${{ matrix.PYTHONOPTIMIZE }} + REVERSE: ${{ matrix.REVERSE }} - name: Prepare to upload errors if: failure() @@ -103,7 +108,7 @@ jobs: - name: Docs if: startsWith(matrix.os, 'ubuntu') && matrix.python-version == 3.9 run: | - python3 -m pip install sphinx-issues sphinx-removed-in sphinx-rtd-theme + python3 -m pip install sphinx-copybutton sphinx-issues sphinx-removed-in sphinx-rtd-theme sphinxext-opengraph make doccheck - name: After success diff --git a/CHANGES.rst b/CHANGES.rst index a2d40305f..7c820e17a 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -2,9 +2,204 @@ Changelog (Pillow) ================== -8.2.0 (unreleased) +8.3.0 (2021-07-01) ------------------ +- Use snprintf instead of sprintf. CVE-2021-34552 #5567 + [radarhere] + +- Limit TIFF strip size when saving with LibTIFF #5514 + [kmilos] + +- Allow ICNS save on all operating systems #4526 + [baletu, radarhere, newpanjing, hugovk] + +- De-zigzag JPEG's DQT when loading; deprecate convert_dict_qtables #4989 + [gofr, radarhere] + +- Replaced xml.etree.ElementTree #5565 + [radarhere] + +- Moved CVE image to pillow-depends #5561 + [radarhere] + +- Added tag data for IFD groups #5554 + [radarhere] + +- Improved ImagePalette #5552 + [radarhere] + +- Add DDS saving #5402 + [radarhere] + +- Improved getxmp() #5455 + [radarhere] + +- Convert to float for comparison with float in IFDRational __eq__ #5412 + [radarhere] + +- Allow getexif() to access TIFF tag_v2 data #5416 + [radarhere] + +- Read FITS image mode and size #5405 + [radarhere] + +- Merge parallel horizontal edges in ImagingDrawPolygon #5347 + [radarhere, hrdrq] + +- Use transparency behind first GIF frame and when disposing to background #5557 + [radarhere, zewt] + +- Avoid unstable nature of qsort in Quant.c #5367 + [radarhere] + +- Copy palette to new images in ImageOps expand #5551 + [radarhere] + +- Ensure palette string matches RGB mode #5549 + [radarhere] + +- Do not modify EXIF of original image instance in exif_transpose() #5547 + [radarhere] + +- Fixed default numresolution for small JPEG2000 images #5540 + [radarhere] + +- Added DDS BC5 reading #5501 + [radarhere] + +- Raise an error if ImageDraw.textbbox is used without a TrueType font #5510 + [radarhere] + +- Added ICO saving in BMP format #5513 + [radarhere] + +- Ensure PNG seeks to end of previous chunk at start of load_end #5493 + [radarhere] + +- Do not allow TIFF to seek to a past frame #5473 + [radarhere] + +- Avoid race condition when displaying images with eog #5507 + [mconst] + +- Added specific error messages when ink has incorrect number of bands #5504 + [radarhere] + +- Allow converting an image to a numpy array to raise errors #5379 + [radarhere] + +- Removed DPI rounding from BMP, JPEG, PNG and WMF loading #5476, #5470 + [radarhere] + +- Remove spikes when drawing thin pieslices #5460 + [xtsm] + +- Updated default value for SAMPLESPERPIXEL TIFF tag #5452 + [radarhere] + +- Removed TIFF DPI rounding #5446 + [radarhere, hugovk] + +- Include code in WebP error #5471 + [radarhere] + +- Do not alter pixels outside mask when drawing text on an image with transparency #5434 + [radarhere] + +- Reset handle when seeking backwards in TIFF #5443 + [radarhere] + +- Replace sys.stdout with sys.stdout.buffer when saving #5437 + [radarhere] + +- Fixed UNDEFINED TIFF tag of length 0 being changed in roundtrip #5426 + [radarhere] + +- Fixed bug when checking FreeType2 version if it is not installed #5445 + [radarhere] + +- Do not round dimensions when saving PDF #5459 + [radarhere] + +- Added ImageOps contain() #5417 + [radarhere, hugovk] + +- Changed WebP default "method" value to 4 #5450 + [radarhere] + +- Switched to saving 1-bit PDFs with DCTDecode #5430 + [radarhere] + +- Use bpp from ICO header #5429 + [radarhere] + +- Corrected JPEG APP14 transform value #5408 + [radarhere] + +- Changed TIFF tag 33723 length to 1 #5425 + [radarhere] + +- Changed ImageMorph incorrect mode errors to ValueError #5414 + [radarhere] + +- Add EXIF tags specified in EXIF 2.32 #5419 + [gladiusglad] + +- Treat previous contents of first GIF frame as transparent #5391 + [radarhere] + +- For special image modes, revert default resize resampling to NEAREST #5411 + [radarhere] + +- JPEG2000: Support decoding subsampled RGB and YCbCr images #4996 + [nulano, radarhere] + +- Stop decoding BC1 punchthrough alpha in BC2&3 #4144 + [jansol] + +- Use zero if GIF background color index is missing #5390 + [radarhere] + +- Fixed ensuring that GIF previous frame was loaded #5386 + [radarhere] + +- Valgrind fixes #5397 + [wiredfool] + +- Round down the radius in rounded_rectangle #5382 + [radarhere] + +- Fixed reading uncompressed RGB data from DDS #5383 + [radarhere] + +8.2.0 (2021-04-01) +------------------ + +- Added getxmp() method #5144 + [UrielMaD, radarhere] + +- Add ImageShow support for GraphicsMagick #5349 + [latosha-maltba, radarhere] + +- Do not load transparent pixels from subsequent GIF frames #5333 + [zewt, radarhere] + +- Use LZW encoding when saving GIF images #5291 + [raygard] + +- Set all transparent colors to be equal in quantize() #5282 + [radarhere] + +- Allow PixelAccess to use Python __int__ when parsing x and y #5206 + [radarhere] + +- Removed Image._MODEINFO #5316 + [radarhere] + +- Add preserve_tone option to autocontrast #5350 + [elejke, radarhere] + - Fixed linear_gradient and radial_gradient I and F modes #5274 [radarhere] diff --git a/Makefile b/Makefile index 53eaa0566..af3059f34 100644 --- a/Makefile +++ b/Makefile @@ -102,6 +102,13 @@ sdist: test: pytest -qq +.PHONY: valgrind +valgrind: + python3 -c "import pytest_valgrind" || pip3 install pytest-valgrind + PYTHONMALLOC=malloc valgrind --suppressions=Tests/oss-fuzz/python.supp --leak-check=no \ + --log-file=/tmp/valgrind-output \ + python3 -m pytest --no-memcheck -vv --valgrind --valgrind-log=/tmp/valgrind-output + .PHONY: readme readme: python3 setup.py --long-description | markdown2 > .long-description.html && open .long-description.html diff --git a/README.md b/README.md index 0408f4c28..29b5b8a6a 100644 --- a/README.md +++ b/README.md @@ -39,9 +39,12 @@ As of 2019, Pillow development is AppVeyor CI build status (Windows) + GitHub Actions wheels build status (Wheels) Travis CI build status (macOS) + alt="Travis CI wheels build status (aarch64)" + src="https://img.shields.io/travis/com/python-pillow/pillow-wheels/master.svg?label=aarch64%20wheels"> Code coverage diff --git a/Tests/32bit_segfault_check.py b/Tests/32bit_segfault_check.py index 26a91d5cd..e19cdf7a9 100755 --- a/Tests/32bit_segfault_check.py +++ b/Tests/32bit_segfault_check.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 import sys diff --git a/Tests/check_fli_oob.py b/Tests/check_fli_oob.py index 739ad224e..6b63a6826 100644 --- a/Tests/check_fli_oob.py +++ b/Tests/check_fli_oob.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 from PIL import Image diff --git a/Tests/check_imaging_leaks.py b/Tests/check_imaging_leaks.py index 407f3ea80..d07082aba 100755 --- a/Tests/check_imaging_leaks.py +++ b/Tests/check_imaging_leaks.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 import pytest from PIL import Image diff --git a/Tests/check_jp2_overflow.py b/Tests/check_jp2_overflow.py index a7a343c98..f81a360ce 100755 --- a/Tests/check_jp2_overflow.py +++ b/Tests/check_jp2_overflow.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 # Reproductions/tests for OOB read errors in FliDecode.c diff --git a/Tests/conftest.py b/Tests/conftest.py index 6f9945204..66da7593c 100644 --- a/Tests/conftest.py +++ b/Tests/conftest.py @@ -1,7 +1,4 @@ import io -import warnings - -import pytest def pytest_report_header(config): @@ -16,16 +13,19 @@ def pytest_report_header(config): def pytest_configure(config): + config.addinivalue_line( + "markers", + "pil_noop_mark: A conditional mark where nothing special happens", + ) + # We're marking some tests to ignore valgrind errors and XFAIL them. # Ensure that the mark is defined # even in cases where pytest-valgrind isn't installed - - with warnings.catch_warnings(): - warnings.simplefilter("error") - try: - getattr(pytest.mark, "valgrind_known_error") - except Exception: - config.addinivalue_line( - "markers", - "valgrind_known_error: Tests that have known issues with valgrind", - ) + try: + config.addinivalue_line( + "markers", + "valgrind_known_error: Tests that have known issues with valgrind", + ) + except Exception: + # valgrind is already installed + pass diff --git a/Tests/createfontdatachunk.py b/Tests/createfontdatachunk.py index 011bb0bed..e318eb732 100755 --- a/Tests/createfontdatachunk.py +++ b/Tests/createfontdatachunk.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 import base64 import os diff --git a/Tests/fonts/oom-e8e927ba6c0d38274a37c1567560eb33baf74627.ttf b/Tests/fonts/oom-e8e927ba6c0d38274a37c1567560eb33baf74627.ttf new file mode 100644 index 000000000..790132515 Binary files /dev/null and b/Tests/fonts/oom-e8e927ba6c0d38274a37c1567560eb33baf74627.ttf differ diff --git a/Tests/helper.py b/Tests/helper.py index be3bdb76f..8504993fb 100644 --- a/Tests/helper.py +++ b/Tests/helper.py @@ -173,6 +173,21 @@ def skip_unless_feature_version(feature, version_required, reason=None): return pytest.mark.skipif(version_available < version_required, reason=reason) +def mark_if_feature_version(mark, feature, version_blacklist, reason=None): + if not features.check(feature): + return pytest.mark.pil_noop_mark() + if reason is None: + reason = f"{feature} is {version_blacklist}" + version_required = parse_version(version_blacklist) + version_available = parse_version(features.version(feature)) + if ( + version_available.major == version_required.major + and version_available.minor == version_required.minor + ): + return mark(reason=reason) + return pytest.mark.pil_noop_mark() + + @pytest.mark.skipif(sys.platform.startswith("win32"), reason="Requires Unix or macOS") class PillowLeakTestCase: # requires unix/macOS @@ -257,8 +272,23 @@ def netpbm_available(): return bool(shutil.which("ppmquant") and shutil.which("ppmtogif")) -def imagemagick_available(): - return bool(IMCONVERT and shutil.which(IMCONVERT)) +def magick_command(): + if sys.platform == "win32": + magickhome = os.environ.get("MAGICK_HOME", "") + if magickhome: + imagemagick = [os.path.join(magickhome, "convert.exe")] + graphicsmagick = [os.path.join(magickhome, "gm.exe"), "convert"] + else: + imagemagick = None + graphicsmagick = None + else: + imagemagick = ["convert"] + graphicsmagick = ["gm", "convert"] + + if imagemagick and shutil.which(imagemagick[0]): + return imagemagick + elif graphicsmagick and shutil.which(graphicsmagick[0]): + return graphicsmagick def on_appveyor(): @@ -296,14 +326,6 @@ def is_mingw(): return sysconfig.get_platform() == "mingw" -if sys.platform == "win32": - IMCONVERT = os.environ.get("MAGICK_HOME", "") - if IMCONVERT: - IMCONVERT = os.path.join(IMCONVERT, "convert.exe") -else: - IMCONVERT = "convert" - - class cached_property: def __init__(self, func): self.func = func diff --git a/Tests/images/200x32_p_bl_raw_origin.tga b/Tests/images/200x32_p_bl_raw_origin.tga new file mode 100644 index 000000000..329f0ca4d Binary files /dev/null and b/Tests/images/200x32_p_bl_raw_origin.tga differ diff --git a/Tests/images/bc5_snorm.dds b/Tests/images/bc5_snorm.dds new file mode 100644 index 000000000..7458c67c6 Binary files /dev/null and b/Tests/images/bc5_snorm.dds differ diff --git a/Tests/images/bc5_typeless.dds b/Tests/images/bc5_typeless.dds new file mode 100644 index 000000000..b5bae52bb Binary files /dev/null and b/Tests/images/bc5_typeless.dds differ diff --git a/Tests/images/bc5_unorm.dds b/Tests/images/bc5_unorm.dds new file mode 100644 index 000000000..a04a026eb Binary files /dev/null and b/Tests/images/bc5_unorm.dds differ diff --git a/Tests/images/bc5_unorm.png b/Tests/images/bc5_unorm.png new file mode 100644 index 000000000..05279ddfb Binary files /dev/null and b/Tests/images/bc5_unorm.png differ diff --git a/Tests/images/bc5s.dds b/Tests/images/bc5s.dds new file mode 100644 index 000000000..0b999eed3 Binary files /dev/null and b/Tests/images/bc5s.dds differ diff --git a/Tests/images/bc5s.png b/Tests/images/bc5s.png new file mode 100644 index 000000000..39d7811bf Binary files /dev/null and b/Tests/images/bc5s.png differ diff --git a/Tests/images/black_and_white.ico b/Tests/images/black_and_white.ico new file mode 100644 index 000000000..f98d7ac8e Binary files /dev/null and b/Tests/images/black_and_white.ico differ diff --git a/Tests/images/crash-0da013a13571cc8eb457a39fee8db18f8a3c7127.tif b/Tests/images/crash-0da013a13571cc8eb457a39fee8db18f8a3c7127.tif new file mode 100644 index 000000000..6e4e9b9ca Binary files /dev/null and b/Tests/images/crash-0da013a13571cc8eb457a39fee8db18f8a3c7127.tif differ diff --git a/Tests/images/crash-4fb027452e6988530aa5dabee76eecacb3b79f8a.j2k b/Tests/images/crash-4fb027452e6988530aa5dabee76eecacb3b79f8a.j2k new file mode 100644 index 000000000..c9bd7fc0a Binary files /dev/null and b/Tests/images/crash-4fb027452e6988530aa5dabee76eecacb3b79f8a.j2k differ diff --git a/Tests/images/crash-74d2a78403a5a59db1fb0a2b8735ac068a75f6e3.tif b/Tests/images/crash-74d2a78403a5a59db1fb0a2b8735ac068a75f6e3.tif new file mode 100644 index 000000000..053e4e4e9 Binary files /dev/null and b/Tests/images/crash-74d2a78403a5a59db1fb0a2b8735ac068a75f6e3.tif differ diff --git a/Tests/images/crash-7d4c83eb92150fb8f1653a697703ae06ae7c4998.j2k b/Tests/images/crash-7d4c83eb92150fb8f1653a697703ae06ae7c4998.j2k new file mode 100644 index 000000000..fd2f4dd36 Binary files /dev/null and b/Tests/images/crash-7d4c83eb92150fb8f1653a697703ae06ae7c4998.j2k differ diff --git a/Tests/images/crash-ccca68ff40171fdae983d924e127a721cab2bd50.j2k b/Tests/images/crash-ccca68ff40171fdae983d924e127a721cab2bd50.j2k new file mode 100644 index 000000000..c3ad0d633 Binary files /dev/null and b/Tests/images/crash-ccca68ff40171fdae983d924e127a721cab2bd50.j2k differ diff --git a/Tests/images/crash-d2c93af851d3ab9a19e34503626368b2ecde9c03.j2k b/Tests/images/crash-d2c93af851d3ab9a19e34503626368b2ecde9c03.j2k new file mode 100644 index 000000000..3aadfc377 Binary files /dev/null and b/Tests/images/crash-d2c93af851d3ab9a19e34503626368b2ecde9c03.j2k differ diff --git a/Tests/images/different_transparency.gif b/Tests/images/different_transparency.gif new file mode 100644 index 000000000..2d36bef9e Binary files /dev/null and b/Tests/images/different_transparency.gif differ diff --git a/Tests/images/different_transparency_merged.gif b/Tests/images/different_transparency_merged.gif new file mode 100644 index 000000000..94d0f53e0 Binary files /dev/null and b/Tests/images/different_transparency_merged.gif differ diff --git a/Tests/images/dispose_prev_first_frame.gif b/Tests/images/dispose_prev_first_frame.gif new file mode 100644 index 000000000..4c19dd1ed Binary files /dev/null and b/Tests/images/dispose_prev_first_frame.gif differ diff --git a/Tests/images/dispose_prev_first_frame_seeked.gif b/Tests/images/dispose_prev_first_frame_seeked.gif new file mode 100644 index 000000000..bc3eb1393 Binary files /dev/null and b/Tests/images/dispose_prev_first_frame_seeked.gif differ diff --git a/Tests/images/drawing_roundDown.emf b/Tests/images/drawing_roundDown.emf deleted file mode 100644 index 6c3e20248..000000000 Binary files a/Tests/images/drawing_roundDown.emf and /dev/null differ diff --git a/Tests/images/dxt5-colorblock-alpha-issue-4142.dds b/Tests/images/dxt5-colorblock-alpha-issue-4142.dds new file mode 100644 index 000000000..905527ead Binary files /dev/null and b/Tests/images/dxt5-colorblock-alpha-issue-4142.dds differ diff --git a/Tests/images/first_frame_transparency.gif b/Tests/images/first_frame_transparency.gif new file mode 100644 index 000000000..86dc0de64 Binary files /dev/null and b/Tests/images/first_frame_transparency.gif differ diff --git a/Tests/images/hopper.dds b/Tests/images/hopper.dds new file mode 100644 index 000000000..8b9af9ed9 Binary files /dev/null and b/Tests/images/hopper.dds differ diff --git a/Tests/images/hopper_roundUp_2.tif b/Tests/images/hopper_float_dpi_2.tif similarity index 100% rename from Tests/images/hopper_roundUp_2.tif rename to Tests/images/hopper_float_dpi_2.tif diff --git a/Tests/images/hopper_roundUp_3.tif b/Tests/images/hopper_float_dpi_3.tif similarity index 100% rename from Tests/images/hopper_roundUp_3.tif rename to Tests/images/hopper_float_dpi_3.tif diff --git a/Tests/images/hopper_roundUp_None.tif b/Tests/images/hopper_float_dpi_None.tif similarity index 100% rename from Tests/images/hopper_roundUp_None.tif rename to Tests/images/hopper_float_dpi_None.tif diff --git a/Tests/images/hopper_naxis_zero.fits b/Tests/images/hopper_naxis_zero.fits new file mode 100644 index 000000000..580cf3a2c Binary files /dev/null and b/Tests/images/hopper_naxis_zero.fits differ diff --git a/Tests/images/hopper_resized.gif b/Tests/images/hopper_resized.gif new file mode 100644 index 000000000..f7be6c262 Binary files /dev/null and b/Tests/images/hopper_resized.gif differ diff --git a/Tests/images/hopper_roundDown.bmp b/Tests/images/hopper_roundDown.bmp deleted file mode 100644 index 62aada050..000000000 Binary files a/Tests/images/hopper_roundDown.bmp and /dev/null differ diff --git a/Tests/images/hopper_roundDown_2.tif b/Tests/images/hopper_roundDown_2.tif deleted file mode 100644 index ac8cd057d..000000000 Binary files a/Tests/images/hopper_roundDown_2.tif and /dev/null differ diff --git a/Tests/images/hopper_roundDown_3.tif b/Tests/images/hopper_roundDown_3.tif deleted file mode 100644 index 0542fab9a..000000000 Binary files a/Tests/images/hopper_roundDown_3.tif and /dev/null differ diff --git a/Tests/images/hopper_roundDown_None.tif b/Tests/images/hopper_roundDown_None.tif deleted file mode 100644 index 21c40e8fe..000000000 Binary files a/Tests/images/hopper_roundDown_None.tif and /dev/null differ diff --git a/Tests/images/imagedraw/continuous_horizontal_edges_polygon.png b/Tests/images/imagedraw/continuous_horizontal_edges_polygon.png new file mode 100644 index 000000000..beffed5b9 Binary files /dev/null and b/Tests/images/imagedraw/continuous_horizontal_edges_polygon.png differ diff --git a/Tests/images/imagedraw_rounded_rectangle_non_integer_radius_given.png b/Tests/images/imagedraw_rounded_rectangle_non_integer_radius_given.png new file mode 100644 index 000000000..59e55b2a1 Binary files /dev/null and b/Tests/images/imagedraw_rounded_rectangle_non_integer_radius_given.png differ diff --git a/Tests/images/imagedraw_rounded_rectangle_non_integer_radius_height.png b/Tests/images/imagedraw_rounded_rectangle_non_integer_radius_height.png new file mode 100644 index 000000000..c4e54896b Binary files /dev/null and b/Tests/images/imagedraw_rounded_rectangle_non_integer_radius_height.png differ diff --git a/Tests/images/imagedraw_rounded_rectangle_non_integer_radius_width.png b/Tests/images/imagedraw_rounded_rectangle_non_integer_radius_width.png new file mode 100644 index 000000000..6b0f11fa6 Binary files /dev/null and b/Tests/images/imagedraw_rounded_rectangle_non_integer_radius_width.png differ diff --git a/Tests/images/iptc_roundDown.jpg b/Tests/images/iptc_roundDown.jpg deleted file mode 100644 index f98206f18..000000000 Binary files a/Tests/images/iptc_roundDown.jpg and /dev/null differ diff --git a/Tests/images/missing_background.gif b/Tests/images/missing_background.gif new file mode 100644 index 000000000..550d68d81 Binary files /dev/null and b/Tests/images/missing_background.gif differ diff --git a/Tests/images/missing_background_first_frame.gif b/Tests/images/missing_background_first_frame.gif new file mode 100644 index 000000000..be2c95b99 Binary files /dev/null and b/Tests/images/missing_background_first_frame.gif differ diff --git a/Tests/images/multipage_multiple_frame_loop.tiff b/Tests/images/multipage_multiple_frame_loop.tiff new file mode 100644 index 000000000..b6759b080 Binary files /dev/null and b/Tests/images/multipage_multiple_frame_loop.tiff differ diff --git a/Tests/images/multipage_out_of_order.tiff b/Tests/images/multipage_out_of_order.tiff new file mode 100644 index 000000000..1576a549b Binary files /dev/null and b/Tests/images/multipage_out_of_order.tiff differ diff --git a/Tests/images/multipage_single_frame_loop.tiff b/Tests/images/multipage_single_frame_loop.tiff new file mode 100644 index 000000000..26f27c421 Binary files /dev/null and b/Tests/images/multipage_single_frame_loop.tiff differ diff --git a/Tests/images/old-style-jpeg-compression-no-samplesperpixel.tif b/Tests/images/old-style-jpeg-compression-no-samplesperpixel.tif new file mode 100644 index 000000000..d43ba9192 Binary files /dev/null and b/Tests/images/old-style-jpeg-compression-no-samplesperpixel.tif differ diff --git a/Tests/images/p_16.png b/Tests/images/p_16.png new file mode 100644 index 000000000..e35886412 Binary files /dev/null and b/Tests/images/p_16.png differ diff --git a/Tests/images/p_16.tga b/Tests/images/p_16.tga new file mode 100644 index 000000000..2b2ca4c70 Binary files /dev/null and b/Tests/images/p_16.tga differ diff --git a/Tests/images/padded_idat.png b/Tests/images/padded_idat.png new file mode 100644 index 000000000..18c5a4990 Binary files /dev/null and b/Tests/images/padded_idat.png differ diff --git a/Tests/images/timeout-060745d3f534ad6e4128c51d336ea5489182c69d.blp b/Tests/images/timeout-060745d3f534ad6e4128c51d336ea5489182c69d.blp new file mode 100644 index 000000000..97def320f Binary files /dev/null and b/Tests/images/timeout-060745d3f534ad6e4128c51d336ea5489182c69d.blp differ diff --git a/Tests/images/timeout-1ee28a249896e05b83840ae8140622de8e648ba9.psd b/Tests/images/timeout-1ee28a249896e05b83840ae8140622de8e648ba9.psd new file mode 100644 index 000000000..63319e545 Binary files /dev/null and b/Tests/images/timeout-1ee28a249896e05b83840ae8140622de8e648ba9.psd differ diff --git a/Tests/images/timeout-31c8f86233ea728339c6e586be7af661a09b5b98.blp b/Tests/images/timeout-31c8f86233ea728339c6e586be7af661a09b5b98.blp new file mode 100644 index 000000000..73022abfc Binary files /dev/null and b/Tests/images/timeout-31c8f86233ea728339c6e586be7af661a09b5b98.blp differ diff --git a/Tests/images/timeout-598843abc37fc080ec36a2699ebbd44f795d3a6f.psd b/Tests/images/timeout-598843abc37fc080ec36a2699ebbd44f795d3a6f.psd new file mode 100644 index 000000000..c259a15e7 Binary files /dev/null and b/Tests/images/timeout-598843abc37fc080ec36a2699ebbd44f795d3a6f.psd differ diff --git a/Tests/images/timeout-60d8b7c8469d59fc9ffff6b3a3dc0faeae6ea8ee.blp b/Tests/images/timeout-60d8b7c8469d59fc9ffff6b3a3dc0faeae6ea8ee.blp new file mode 100644 index 000000000..79e97dce3 Binary files /dev/null and b/Tests/images/timeout-60d8b7c8469d59fc9ffff6b3a3dc0faeae6ea8ee.blp differ diff --git a/Tests/images/timeout-8073b430977660cdd48d96f6406ddfd4114e69c7.blp b/Tests/images/timeout-8073b430977660cdd48d96f6406ddfd4114e69c7.blp new file mode 100644 index 000000000..9b9ecbcb0 Binary files /dev/null and b/Tests/images/timeout-8073b430977660cdd48d96f6406ddfd4114e69c7.blp differ diff --git a/Tests/images/timeout-9139147ce93e20eb14088fe238e541443ffd64b3.fli b/Tests/images/timeout-9139147ce93e20eb14088fe238e541443ffd64b3.fli new file mode 100644 index 000000000..ce4607d2d Binary files /dev/null and b/Tests/images/timeout-9139147ce93e20eb14088fe238e541443ffd64b3.fli differ diff --git a/Tests/images/timeout-bba4f2e026b5786529370e5dfe9a11b1bf991f07.blp b/Tests/images/timeout-bba4f2e026b5786529370e5dfe9a11b1bf991f07.blp new file mode 100644 index 000000000..cb9a4e8b3 Binary files /dev/null and b/Tests/images/timeout-bba4f2e026b5786529370e5dfe9a11b1bf991f07.blp differ diff --git a/Tests/images/timeout-bff0a9dc7243a8e6ede2408d2ffa6a9964698b87.fli b/Tests/images/timeout-bff0a9dc7243a8e6ede2408d2ffa6a9964698b87.fli new file mode 100644 index 000000000..77a94b87a Binary files /dev/null and b/Tests/images/timeout-bff0a9dc7243a8e6ede2408d2ffa6a9964698b87.fli differ diff --git a/Tests/images/timeout-c8efc3fded6426986ba867a399791bae544f59bc.psd b/Tests/images/timeout-c8efc3fded6426986ba867a399791bae544f59bc.psd new file mode 100644 index 000000000..955fc3325 Binary files /dev/null and b/Tests/images/timeout-c8efc3fded6426986ba867a399791bae544f59bc.psd differ diff --git a/Tests/images/timeout-d675703545fee17acab56e5fec644c19979175de.eps b/Tests/images/timeout-d675703545fee17acab56e5fec644c19979175de.eps new file mode 100644 index 000000000..5000ca9aa Binary files /dev/null and b/Tests/images/timeout-d675703545fee17acab56e5fec644c19979175de.eps differ diff --git a/Tests/images/timeout-d6ec061c4afdef39d3edf6da8927240bb07fe9b7.blp b/Tests/images/timeout-d6ec061c4afdef39d3edf6da8927240bb07fe9b7.blp new file mode 100644 index 000000000..5044fbde1 Binary files /dev/null and b/Tests/images/timeout-d6ec061c4afdef39d3edf6da8927240bb07fe9b7.blp differ diff --git a/Tests/images/timeout-dedc7a4ebd856d79b4359bbcc79e8ef231ce38f6.psd b/Tests/images/timeout-dedc7a4ebd856d79b4359bbcc79e8ef231ce38f6.psd new file mode 100644 index 000000000..c658ea45c Binary files /dev/null and b/Tests/images/timeout-dedc7a4ebd856d79b4359bbcc79e8ef231ce38f6.psd differ diff --git a/Tests/images/timeout-ef9112a065e7183fa7faa2e18929b03e44ee16bf.blp b/Tests/images/timeout-ef9112a065e7183fa7faa2e18929b03e44ee16bf.blp new file mode 100644 index 000000000..7ef78eeec Binary files /dev/null and b/Tests/images/timeout-ef9112a065e7183fa7faa2e18929b03e44ee16bf.blp differ diff --git a/Tests/images/transparent_background_text.png b/Tests/images/transparent_background_text.png index 40acd92b6..8ddd65cc6 100644 Binary files a/Tests/images/transparent_background_text.png and b/Tests/images/transparent_background_text.png differ diff --git a/Tests/images/transparent_background_text_L.png b/Tests/images/transparent_background_text_L.png new file mode 100644 index 000000000..d37de20a7 Binary files /dev/null and b/Tests/images/transparent_background_text_L.png differ diff --git a/Tests/images/transparent_dispose.gif b/Tests/images/transparent_dispose.gif new file mode 100644 index 000000000..92b615543 Binary files /dev/null and b/Tests/images/transparent_dispose.gif differ diff --git a/Tests/images/truncated_app14.jpg b/Tests/images/truncated_app14.jpg new file mode 100644 index 000000000..232a4c35f Binary files /dev/null and b/Tests/images/truncated_app14.jpg differ diff --git a/Tests/images/uncompressed_rgb.png b/Tests/images/uncompressed_rgb.png index 50bca09ee..f02b50f6f 100644 Binary files a/Tests/images/uncompressed_rgb.png and b/Tests/images/uncompressed_rgb.png differ diff --git a/Tests/images/xmp_test.jpg b/Tests/images/xmp_test.jpg new file mode 100644 index 000000000..4b9354f3a Binary files /dev/null and b/Tests/images/xmp_test.jpg differ diff --git a/Tests/oss-fuzz/fuzz_font.py b/Tests/oss-fuzz/fuzz_font.py index bdfda7a13..e11471011 100755 --- a/Tests/oss-fuzz/fuzz_font.py +++ b/Tests/oss-fuzz/fuzz_font.py @@ -34,6 +34,7 @@ def main(): fuzzers.enable_decompressionbomb_error() atheris.Setup(sys.argv, TestOneInput, enable_python_coverage=True) atheris.Fuzz() + fuzzers.disable_decompressionbomb_error() if __name__ == "__main__": diff --git a/Tests/oss-fuzz/fuzz_pillow.py b/Tests/oss-fuzz/fuzz_pillow.py index d816d535f..b3c55fe22 100644 --- a/Tests/oss-fuzz/fuzz_pillow.py +++ b/Tests/oss-fuzz/fuzz_pillow.py @@ -34,6 +34,7 @@ def main(): fuzzers.enable_decompressionbomb_error() atheris.Setup(sys.argv, TestOneInput, enable_python_coverage=True) atheris.Fuzz() + fuzzers.disable_decompressionbomb_error() if __name__ == "__main__": diff --git a/Tests/oss-fuzz/fuzzers.py b/Tests/oss-fuzz/fuzzers.py index 1e7a4e27d..5786764a6 100644 --- a/Tests/oss-fuzz/fuzzers.py +++ b/Tests/oss-fuzz/fuzzers.py @@ -10,6 +10,11 @@ def enable_decompressionbomb_error(): warnings.simplefilter("error", Image.DecompressionBombWarning) +def disable_decompressionbomb_error(): + ImageFile.LOAD_TRUNCATED_IMAGES = False + warnings.resetwarnings() + + def fuzz_image(data): # This will fail on some images in the corpus, as we have many # invalid images in the test suite. diff --git a/Tests/oss-fuzz/python.supp b/Tests/oss-fuzz/python.supp new file mode 100644 index 000000000..94cc87db9 --- /dev/null +++ b/Tests/oss-fuzz/python.supp @@ -0,0 +1,16 @@ +{ + + Memcheck:Cond + ... + fun:encode_current_locale +} + + +{ + + Memcheck:Cond + fun:inflate + fun:ZIPDecode + fun:_TIFFReadEncodedTileAndAllocBuffer + ... +} diff --git a/Tests/oss-fuzz/test_fuzzers.py b/Tests/oss-fuzz/test_fuzzers.py index a243c0260..629e9ac00 100644 --- a/Tests/oss-fuzz/test_fuzzers.py +++ b/Tests/oss-fuzz/test_fuzzers.py @@ -2,12 +2,19 @@ import subprocess import sys import fuzzers +import packaging import pytest -from PIL import Image +from PIL import Image, features if sys.platform.startswith("win32"): pytest.skip("Fuzzer is linux only", allow_module_level=True) +if features.check("libjpeg_turbo"): + version = packaging.version.parse(features.version("libjpeg_turbo")) + if version.major == 2 and version.minor == 0: + pytestmark = pytest.mark.valgrind_known_error( + reason="Known failing with libjpeg_turbo 2.0" + ) @pytest.mark.parametrize( @@ -37,6 +44,8 @@ def test_fuzz_images(path): ): # Known Image.* exceptions assert True + finally: + fuzzers.disable_decompressionbomb_error() @pytest.mark.parametrize( diff --git a/Tests/test_decompression_bomb.py b/Tests/test_decompression_bomb.py index 80ab92666..d918ef941 100644 --- a/Tests/test_decompression_bomb.py +++ b/Tests/test_decompression_bomb.py @@ -10,8 +10,7 @@ ORIGINAL_LIMIT = Image.MAX_IMAGE_PIXELS class TestDecompressionBomb: - @classmethod - def teardown_class(cls): + def teardown_method(self, method): Image.MAX_IMAGE_PIXELS = ORIGINAL_LIMIT def test_no_warning_small_file(self): @@ -52,6 +51,7 @@ class TestDecompressionBomb: with Image.open(TEST_FILE): pass + @pytest.mark.xfail(reason="different exception") def test_exception_ico(self): with pytest.raises(Image.DecompressionBombError): with Image.open("Tests/images/decompression_bomb.ico"): diff --git a/Tests/test_file_apng.py b/Tests/test_file_apng.py index 97e2a150e..7fb6f59d4 100644 --- a/Tests/test_file_apng.py +++ b/Tests/test_file_apng.py @@ -249,8 +249,8 @@ def test_apng_mode(): assert im.mode == "P" im.seek(im.n_frames - 1) im = im.convert("RGBA") - assert im.getpixel((0, 0)) == (0, 255, 0, 255) - assert im.getpixel((64, 32)) == (0, 255, 0, 255) + assert im.getpixel((0, 0)) == (255, 0, 0, 0) + assert im.getpixel((64, 32)) == (255, 0, 0, 0) with Image.open("Tests/images/apng/mode_palette_1bit_alpha.png") as im: assert im.mode == "P" @@ -312,7 +312,7 @@ def test_apng_syntax_errors(): exception = e assert exception is None - with pytest.raises(SyntaxError): + with pytest.raises(OSError): with Image.open("Tests/images/apng/syntax_num_frames_high.png") as im: im.seek(im.n_frames - 1) im.load() diff --git a/Tests/test_file_blp.py b/Tests/test_file_blp.py index 864607301..15bd7e4f8 100644 --- a/Tests/test_file_blp.py +++ b/Tests/test_file_blp.py @@ -1,3 +1,5 @@ +import pytest + from PIL import Image from .helper import assert_image_equal_tofile @@ -16,3 +18,22 @@ def test_load_blp2_dxt1(): def test_load_blp2_dxt1a(): with Image.open("Tests/images/blp/blp2_dxt1a.blp") as im: assert_image_equal_tofile(im, "Tests/images/blp/blp2_dxt1a.png") + + +@pytest.mark.parametrize( + "test_file", + [ + "Tests/images/timeout-060745d3f534ad6e4128c51d336ea5489182c69d.blp", + "Tests/images/timeout-31c8f86233ea728339c6e586be7af661a09b5b98.blp", + "Tests/images/timeout-60d8b7c8469d59fc9ffff6b3a3dc0faeae6ea8ee.blp", + "Tests/images/timeout-8073b430977660cdd48d96f6406ddfd4114e69c7.blp", + "Tests/images/timeout-bba4f2e026b5786529370e5dfe9a11b1bf991f07.blp", + "Tests/images/timeout-d6ec061c4afdef39d3edf6da8927240bb07fe9b7.blp", + "Tests/images/timeout-ef9112a065e7183fa7faa2e18929b03e44ee16bf.blp", + ], +) +def test_crashes(test_file): + with open(test_file, "rb") as f: + with Image.open(f) as im: + with pytest.raises(OSError): + im.load() diff --git a/Tests/test_file_bmp.py b/Tests/test_file_bmp.py index d5fe2a4dd..3374fe54e 100644 --- a/Tests/test_file_bmp.py +++ b/Tests/test_file_bmp.py @@ -63,7 +63,7 @@ def test_dpi(): output.seek(0) with Image.open(output) as reloaded: - assert reloaded.info["dpi"] == dpi + assert reloaded.info["dpi"] == (72.008961115161, 72.008961115161) def test_save_bmp_with_dpi(tmp_path): @@ -71,6 +71,7 @@ def test_save_bmp_with_dpi(tmp_path): # Arrange outfile = str(tmp_path / "temp.jpg") with Image.open("Tests/images/hopper.bmp") as im: + assert im.info["dpi"] == (95.98654816726399, 95.98654816726399) # Act im.save(outfile, "JPEG", dpi=im.info["dpi"]) @@ -78,31 +79,17 @@ def test_save_bmp_with_dpi(tmp_path): # Assert with Image.open(outfile) as reloaded: reloaded.load() - assert im.info["dpi"] == reloaded.info["dpi"] - assert im.size == reloaded.size + assert reloaded.info["dpi"] == (96, 96) + assert reloaded.size == im.size assert reloaded.format == "JPEG" -def test_load_dpi_rounding(): - # Round up - with Image.open("Tests/images/hopper.bmp") as im: - assert im.info["dpi"] == (96, 96) - - # Round down - with Image.open("Tests/images/hopper_roundDown.bmp") as im: - assert im.info["dpi"] == (72, 72) - - -def test_save_dpi_rounding(tmp_path): +def test_save_float_dpi(tmp_path): outfile = str(tmp_path / "temp.bmp") with Image.open("Tests/images/hopper.bmp") as im: - im.save(outfile, dpi=(72.2, 72.2)) + im.save(outfile, dpi=(72.21216100543306, 72.21216100543306)) with Image.open(outfile) as reloaded: - assert reloaded.info["dpi"] == (72, 72) - - im.save(outfile, dpi=(72.8, 72.8)) - with Image.open(outfile) as reloaded: - assert reloaded.info["dpi"] == (73, 73) + assert reloaded.info["dpi"] == (72.21216100543306, 72.21216100543306) def test_load_dib(): diff --git a/Tests/test_file_dds.py b/Tests/test_file_dds.py index 682cd048b..46ebcad0c 100644 --- a/Tests/test_file_dds.py +++ b/Tests/test_file_dds.py @@ -5,16 +5,21 @@ import pytest from PIL import DdsImagePlugin, Image -from .helper import assert_image_equal, assert_image_equal_tofile +from .helper import assert_image_equal, assert_image_equal_tofile, hopper TEST_FILE_DXT1 = "Tests/images/dxt1-rgb-4bbp-noalpha_MipMaps-1.dds" TEST_FILE_DXT3 = "Tests/images/dxt3-argb-8bbp-explicitalpha_MipMaps-1.dds" TEST_FILE_DXT5 = "Tests/images/dxt5-argb-8bbp-interpolatedalpha_MipMaps-1.dds" +TEST_FILE_DX10_BC5_TYPELESS = "Tests/images/bc5_typeless.dds" +TEST_FILE_DX10_BC5_UNORM = "Tests/images/bc5_unorm.dds" +TEST_FILE_DX10_BC5_SNORM = "Tests/images/bc5_snorm.dds" +TEST_FILE_BC5S = "Tests/images/bc5s.dds" TEST_FILE_DX10_BC7 = "Tests/images/bc7-argb-8bpp_MipMaps-1.dds" TEST_FILE_DX10_BC7_UNORM_SRGB = "Tests/images/DXGI_FORMAT_BC7_UNORM_SRGB.dds" TEST_FILE_DX10_R8G8B8A8 = "Tests/images/argb-32bpp_MipMaps-1.dds" TEST_FILE_DX10_R8G8B8A8_UNORM_SRGB = "Tests/images/DXGI_FORMAT_R8G8B8A8_UNORM_SRGB.dds" -TEST_FILE_UNCOMPRESSED_RGB = "Tests/images/uncompressed_rgb.dds" +TEST_FILE_UNCOMPRESSED_RGB = "Tests/images/hopper.dds" +TEST_FILE_UNCOMPRESSED_RGB_WITH_ALPHA = "Tests/images/uncompressed_rgb.dds" def test_sanity_dxt1(): @@ -31,6 +36,19 @@ def test_sanity_dxt1(): assert_image_equal(im, target) +def test_sanity_dxt3(): + """Check DXT3 images can be opened""" + + with Image.open(TEST_FILE_DXT3) as im: + im.load() + + assert im.format == "DDS" + assert im.mode == "RGBA" + assert im.size == (256, 256) + + assert_image_equal_tofile(im, TEST_FILE_DXT3.replace(".dds", ".png")) + + def test_sanity_dxt5(): """Check DXT5 images can be opened""" @@ -44,17 +62,28 @@ def test_sanity_dxt5(): assert_image_equal_tofile(im, TEST_FILE_DXT5.replace(".dds", ".png")) -def test_sanity_dxt3(): - """Check DXT3 images can be opened""" +@pytest.mark.parametrize( + ("image_path", "expected_path"), + ( + # hexeditted to be typeless + (TEST_FILE_DX10_BC5_TYPELESS, TEST_FILE_DX10_BC5_UNORM), + (TEST_FILE_DX10_BC5_UNORM, TEST_FILE_DX10_BC5_UNORM), + # hexeditted to use DX10 FourCC + (TEST_FILE_DX10_BC5_SNORM, TEST_FILE_BC5S), + (TEST_FILE_BC5S, TEST_FILE_BC5S), + ), +) +def test_dx10_bc5(image_path, expected_path): + """Check DX10 BC5 images can be opened""" - with Image.open(TEST_FILE_DXT3) as im: + with Image.open(image_path) as im: im.load() assert im.format == "DDS" - assert im.mode == "RGBA" + assert im.mode == "RGB" assert im.size == (256, 256) - assert_image_equal_tofile(im, TEST_FILE_DXT3.replace(".dds", ".png")) + assert_image_equal_tofile(im, expected_path.replace(".dds", ".png")) def test_dx10_bc7(): @@ -124,37 +153,44 @@ def test_unimplemented_dxgi_format(): def test_uncompressed_rgb(): """Check uncompressed RGB images can be opened""" + # convert -format dds -define dds:compression=none hopper.jpg hopper.dds with Image.open(TEST_FILE_UNCOMPRESSED_RGB) as im: - im.load() + assert im.format == "DDS" + assert im.mode == "RGB" + assert im.size == (128, 128) + assert_image_equal_tofile(im, "Tests/images/hopper.png") + + # Test image with alpha + with Image.open(TEST_FILE_UNCOMPRESSED_RGB_WITH_ALPHA) as im: assert im.format == "DDS" assert im.mode == "RGBA" assert im.size == (800, 600) assert_image_equal_tofile( - im, TEST_FILE_UNCOMPRESSED_RGB.replace(".dds", ".png") + im, TEST_FILE_UNCOMPRESSED_RGB_WITH_ALPHA.replace(".dds", ".png") ) -def test__validate_true(): +def test__accept_true(): """Check valid prefix""" # Arrange prefix = b"DDS etc" # Act - output = DdsImagePlugin._validate(prefix) + output = DdsImagePlugin._accept(prefix) # Assert assert output -def test__validate_false(): +def test__accept_false(): """Check invalid prefix""" # Arrange prefix = b"something invalid" # Act - output = DdsImagePlugin._validate(prefix) + output = DdsImagePlugin._accept(prefix) # Assert assert not output @@ -187,7 +223,46 @@ def test_short_file(): short_file() +def test_dxt5_colorblock_alpha_issue_4142(): + """ Check that colorblocks are decoded correctly in DXT5""" + + with Image.open("Tests/images/dxt5-colorblock-alpha-issue-4142.dds") as im: + px = im.getpixel((0, 0)) + assert px[0] != 0 + assert px[1] != 0 + assert px[2] != 0 + + px = im.getpixel((1, 0)) + assert px[0] != 0 + assert px[1] != 0 + assert px[2] != 0 + + def test_unimplemented_pixel_format(): with pytest.raises(NotImplementedError): with Image.open("Tests/images/unimplemented_pixel_format.dds"): pass + + +def test_save_unsupported_mode(tmp_path): + out = str(tmp_path / "temp.dds") + im = hopper("HSV") + with pytest.raises(OSError): + im.save(out) + + +@pytest.mark.parametrize( + ("mode", "test_file"), + [ + ("RGB", "Tests/images/hopper.png"), + ("RGBA", "Tests/images/pil123rgba.png"), + ], +) +def test_save(mode, test_file, tmp_path): + out = str(tmp_path / "temp.dds") + with Image.open(test_file) as im: + assert im.mode == mode + im.save(out) + + with Image.open(out) as reloaded: + assert_image_equal(im, reloaded) diff --git a/Tests/test_file_eps.py b/Tests/test_file_eps.py index ea91375da..1994a124c 100644 --- a/Tests/test_file_eps.py +++ b/Tests/test_file_eps.py @@ -8,6 +8,7 @@ from .helper import ( assert_image_similar, assert_image_similar_tofile, hopper, + mark_if_feature_version, skip_unless_feature, ) @@ -64,7 +65,9 @@ def test_invalid_file(): EpsImagePlugin.EpsImageFile(invalid_file) -@pytest.mark.valgrind_known_error(reason="Known Failing") +@mark_if_feature_version( + pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" +) @pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") def test_cmyk(): with Image.open("Tests/images/pil_sample_cmyk.eps") as cmyk_image: @@ -264,3 +267,15 @@ def test_emptyline(): assert image.mode == "RGB" assert image.size == (460, 352) assert image.format == "EPS" + + +@pytest.mark.timeout(timeout=5) +@pytest.mark.parametrize( + "test_file", + ["Tests/images/timeout-d675703545fee17acab56e5fec644c19979175de.eps"], +) +def test_timeout(test_file): + with open(test_file, "rb") as f: + with pytest.raises(Image.UnidentifiedImageError): + with Image.open(f): + pass diff --git a/Tests/test_file_fitsstub.py b/Tests/test_file_fitsstub.py index 6dc7c4602..c77457947 100644 --- a/Tests/test_file_fitsstub.py +++ b/Tests/test_file_fitsstub.py @@ -1,3 +1,5 @@ +from io import BytesIO + import pytest from PIL import FitsStubImagePlugin, Image @@ -11,10 +13,8 @@ def test_open(): # Assert assert im.format == "FITS" - - # Dummy data from the stub - assert im.mode == "F" - assert im.size == (1, 1) + assert im.size == (128, 128) + assert im.mode == "L" def test_invalid_file(): @@ -35,6 +35,21 @@ def test_load(): im.load() +def test_truncated_fits(): + # No END to headers + image_data = b"SIMPLE = T" + b" " * 50 + b"TRUNCATE" + with pytest.raises(OSError): + FitsStubImagePlugin.FITSStubImageFile(BytesIO(image_data)) + + +def test_naxis_zero(): + # This test image has been manually hexedited + # to set the number of data axes to zero + with pytest.raises(ValueError): + with Image.open("Tests/images/hopper_naxis_zero.fits"): + pass + + def test_save(): # Arrange with Image.open(TEST_FILE) as im: diff --git a/Tests/test_file_fli.py b/Tests/test_file_fli.py index 0d9748a95..1c1abf2b1 100644 --- a/Tests/test_file_fli.py +++ b/Tests/test_file_fli.py @@ -123,3 +123,18 @@ def test_seek(): im.seek(50) assert_image_equal_tofile(im, "Tests/images/a_fli.png") + + +@pytest.mark.parametrize( + "test_file", + [ + "Tests/images/timeout-9139147ce93e20eb14088fe238e541443ffd64b3.fli", + "Tests/images/timeout-bff0a9dc7243a8e6ede2408d2ffa6a9964698b87.fli", + ], +) +@pytest.mark.timeout(timeout=3) +def test_timeouts(test_file): + with open(test_file, "rb") as f: + with Image.open(f) as im: + with pytest.raises(OSError): + im.load() diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index 1b2314d51..2632ab7c0 100644 --- a/Tests/test_file_gif.py +++ b/Tests/test_file_gif.py @@ -298,6 +298,12 @@ def test_eoferror(): im.seek(n_frames - 1) +def test_first_frame_transparency(): + with Image.open("Tests/images/first_frame_transparency.gif") as im: + px = im.load() + assert px[0, 0] == im.info["transparency"] + + def test_dispose_none(): with Image.open("Tests/images/dispose_none.gif") as img: try: @@ -331,6 +337,16 @@ def test_dispose_background(): pass +def test_transparent_dispose(): + expected_colors = [(2, 1, 2), (0, 1, 0), (2, 1, 2)] + with Image.open("Tests/images/transparent_dispose.gif") as img: + for frame in range(3): + img.seek(frame) + for x in range(3): + color = img.getpixel((x, 0)) + assert color == expected_colors[frame][x] + + def test_dispose_previous(): with Image.open("Tests/images/dispose_prev.gif") as img: try: @@ -341,6 +357,25 @@ def test_dispose_previous(): pass +def test_dispose_previous_first_frame(): + with Image.open("Tests/images/dispose_prev_first_frame.gif") as im: + im.seek(1) + assert_image_equal_tofile( + im, "Tests/images/dispose_prev_first_frame_seeked.gif" + ) + + +def test_previous_frame_loaded(): + with Image.open("Tests/images/dispose_none.gif") as img: + img.load() + img.seek(1) + img.load() + img.seek(2) + with Image.open("Tests/images/dispose_none.gif") as img_skipped: + img_skipped.seek(2) + assert_image_equal(img_skipped, img) + + def test_save_dispose(tmp_path): out = str(tmp_path / "temp.gif") im_list = [ @@ -373,14 +408,15 @@ def test_save_dispose(tmp_path): def test_dispose2_palette(tmp_path): out = str(tmp_path / "temp.gif") - # 4 backgrounds: White, Grey, Black, Red + # Four colors: white, grey, black, red circles = [(255, 255, 255), (153, 153, 153), (0, 0, 0), (255, 0, 0)] im_list = [] for circle in circles: + # Red background img = Image.new("RGB", (100, 100), (255, 0, 0)) - # Red circle in center of each frame + # Circle in center of each frame d = ImageDraw.Draw(img) d.ellipse([(40, 40), (60, 60)], fill=circle) @@ -468,12 +504,25 @@ def test_dispose2_background(tmp_path): assert im.getpixel((0, 0)) == 0 -def test_iss634(): +def test_transparency_in_second_frame(): + with Image.open("Tests/images/different_transparency.gif") as im: + assert im.info["transparency"] == 0 + + # Seek to the second frame + im.seek(im.tell() + 1) + assert im.info["transparency"] == 0 + + assert_image_equal_tofile(im, "Tests/images/different_transparency_merged.gif") + + +def test_no_transparency_in_second_frame(): with Image.open("Tests/images/iss634.gif") as img: # Seek to the second frame img.seek(img.tell() + 1) + assert "transparency" not in img.info + # All transparent pixels should be replaced with the color from the first frame - assert img.histogram()[img.info["transparency"]] == 0 + assert img.histogram()[255] == 0 def test_duration(tmp_path): @@ -717,10 +766,10 @@ def test_rgb_transparency(tmp_path): # Single frame im = Image.new("RGB", (1, 1)) im.info["transparency"] = (255, 0, 0) - pytest.warns(UserWarning, im.save, out) + im.save(out) with Image.open(out) as reloaded: - assert "transparency" not in reloaded.info + assert "transparency" in reloaded.info # Multiple frames im = Image.new("RGB", (1, 1)) @@ -840,3 +889,11 @@ def test_extents(): assert im.size == (100, 100) im.seek(1) assert im.size == (150, 150) + + +def test_missing_background(): + # The Global Color Table Flag isn't set, so there is no background color index, + # but the disposal method is "Restore to background color" + with Image.open("Tests/images/missing_background.gif") as im: + im.seek(1) + assert_image_equal_tofile(im, "Tests/images/missing_background_first_frame.gif") diff --git a/Tests/test_file_icns.py b/Tests/test_file_icns.py index 30ec3dc72..47de38d06 100644 --- a/Tests/test_file_icns.py +++ b/Tests/test_file_icns.py @@ -1,5 +1,4 @@ import io -import sys import pytest @@ -28,7 +27,6 @@ def test_sanity(): assert im.format == "ICNS" -@pytest.mark.skipif(sys.platform != "darwin", reason="Requires macOS") def test_save(tmp_path): temp_file = str(tmp_path / "temp.icns") @@ -41,7 +39,6 @@ def test_save(tmp_path): assert reread.format == "ICNS" -@pytest.mark.skipif(sys.platform != "darwin", reason="Requires macOS") def test_save_append_images(tmp_path): temp_file = str(tmp_path / "temp.icns") provided_im = Image.new("RGBA", (32, 32), (255, 0, 0, 128)) @@ -57,7 +54,6 @@ def test_save_append_images(tmp_path): assert_image_equal(reread, provided_im) -@pytest.mark.skipif(sys.platform != "darwin", reason="Requires macOS") def test_save_fp(): fp = io.BytesIO() diff --git a/Tests/test_file_ico.py b/Tests/test_file_ico.py index 5ace0c55e..8060d1b76 100644 --- a/Tests/test_file_ico.py +++ b/Tests/test_file_ico.py @@ -18,6 +18,12 @@ def test_sanity(): assert im.get_format_mimetype() == "image/x-icon" +def test_black_and_white(): + with Image.open("Tests/images/black_and_white.ico") as im: + assert im.mode == "RGBA" + assert im.size == (16, 16) + + def test_invalid_file(): with open("Tests/images/flower.jpg", "rb") as fp: with pytest.raises(SyntaxError): @@ -50,6 +56,35 @@ def test_save_to_bytes(): assert_image_equal(reloaded, hopper().resize((32, 32), Image.LANCZOS)) +@pytest.mark.parametrize("mode", ("1", "L", "P", "RGB", "RGBA")) +def test_save_to_bytes_bmp(mode): + output = io.BytesIO() + im = hopper(mode) + im.save(output, "ico", bitmap_format="bmp", sizes=[(32, 32), (64, 64)]) + + # The default image + output.seek(0) + with Image.open(output) as reloaded: + assert reloaded.info["sizes"] == {(32, 32), (64, 64)} + + assert "RGBA" == reloaded.mode + assert (64, 64) == reloaded.size + assert reloaded.format == "ICO" + im = hopper(mode).resize((64, 64), Image.LANCZOS).convert("RGBA") + assert_image_equal(reloaded, im) + + # The other one + output.seek(0) + with Image.open(output) as reloaded: + reloaded.size = (32, 32) + + assert "RGBA" == reloaded.mode + assert (32, 32) == reloaded.size + assert reloaded.format == "ICO" + im = hopper(mode).resize((32, 32), Image.LANCZOS).convert("RGBA") + assert_image_equal(reloaded, im) + + def test_incorrect_size(): with Image.open(TEST_ICO_FILE) as im: with pytest.raises(ValueError): @@ -119,5 +154,4 @@ def test_draw_reloaded(tmp_path): im.save(outfile) with Image.open(outfile) as im: - im.save("Tests/images/hopper_draw.ico") assert_image_equal_tofile(im, "Tests/images/hopper_draw.ico") diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index 3ee33d65f..68096e92d 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -24,9 +24,15 @@ from .helper import ( djpeg_available, hopper, is_win32, + mark_if_feature_version, skip_unless_feature, ) +try: + import defusedxml.ElementTree as ElementTree +except ImportError: + ElementTree = None + TEST_FILE = "Tests/images/hopper.jpg" @@ -116,7 +122,9 @@ class TestFileJpeg: assert test(100, 200) == (100, 200) assert test(0) is None # square pixels - @pytest.mark.valgrind_known_error(reason="Known Failing") + @mark_if_feature_version( + pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" + ) def test_icc(self, tmp_path): # Test ICC support with Image.open("Tests/images/rgb.jpg") as im1: @@ -156,7 +164,9 @@ class TestFileJpeg: test(ImageFile.MAXBLOCK + 1) # full buffer block plus one byte test(ImageFile.MAXBLOCK * 4 + 3) # large block - @pytest.mark.valgrind_known_error(reason="Known Failing") + @mark_if_feature_version( + pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" + ) def test_large_icc_meta(self, tmp_path): # https://github.com/python-pillow/Pillow/issues/148 # Sometimes the meta data on the icc_profile block is bigger than @@ -423,7 +433,9 @@ class TestFileJpeg: with Image.open(filename): pass - @pytest.mark.valgrind_known_error(reason="Known Failing") + @mark_if_feature_version( + pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" + ) def test_truncated_jpeg_should_read_all_the_data(self): filename = "Tests/images/truncated_jpeg.jpg" ImageFile.LOAD_TRUNCATED_IMAGES = True @@ -442,7 +454,9 @@ class TestFileJpeg: with pytest.raises(OSError): im.load() - @pytest.mark.valgrind_known_error(reason="Known Failing") + @mark_if_feature_version( + pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" + ) def test_qtables(self, tmp_path): def _n_qtables_helper(n, test_file): with Image.open(test_file) as im: @@ -452,7 +466,7 @@ class TestFileJpeg: assert len(im.quantization) == n reloaded = self.roundtrip(im, qtables="keep") assert im.quantization == reloaded.quantization - assert reloaded.quantization[0].typecode == "B" + assert max(reloaded.quantization[0]) <= 255 with Image.open("Tests/images/hopper.jpg") as im: qtables = im.quantization @@ -464,7 +478,8 @@ class TestFileJpeg: # valid bounds for baseline qtable bounds_qtable = [int(s) for s in ("255 1 " * 32).split(None)] - self.roundtrip(im, qtables=[bounds_qtable]) + im2 = self.roundtrip(im, qtables=[bounds_qtable]) + assert im2.quantization == {0: bounds_qtable} # values from wizard.txt in jpeg9-a src package. standard_l_qtable = [ @@ -575,6 +590,12 @@ class TestFileJpeg: assert max(im2.quantization[0]) <= 255 assert max(im2.quantization[1]) <= 255 + def test_convert_dict_qtables_deprecation(self): + with pytest.warns(DeprecationWarning): + qtable = {0: [1, 2, 3, 4]} + qtable2 = JpegImagePlugin.convert_dict_qtables(qtable) + assert qtable == qtable2 + @pytest.mark.skipif(not djpeg_available(), reason="djpeg not available") def test_load_djpeg(self): with Image.open(TEST_FILE) as img: @@ -647,15 +668,6 @@ class TestFileJpeg: reloaded.load() assert im.info["dpi"] == reloaded.info["dpi"] - def test_load_dpi_rounding(self): - # Round up - with Image.open("Tests/images/iptc_roundUp.jpg") as im: - assert im.info["dpi"] == (44, 44) - - # Round down - with Image.open("Tests/images/iptc_roundDown.jpg") as im: - assert im.info["dpi"] == (2, 2) - def test_save_dpi_rounding(self, tmp_path): outfile = str(tmp_path / "temp.jpg") with Image.open("Tests/images/hopper.jpg") as im: @@ -726,7 +738,9 @@ class TestFileJpeg: # OSError for unidentified image. assert im.info.get("dpi") == (72, 72) - @pytest.mark.valgrind_known_error(reason="Known Failing") + @mark_if_feature_version( + pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" + ) def test_exif_x_resolution(self, tmp_path): with Image.open("Tests/images/flower.jpg") as im: exif = im.getexif() @@ -757,7 +771,9 @@ class TestFileJpeg: # Act / Assert assert im._getexif()[306] == "2017:03:13 23:03:09" - @pytest.mark.valgrind_known_error(reason="Backtrace in Python Core") + @mark_if_feature_version( + pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" + ) def test_photoshop(self): with Image.open("Tests/images/photoshop-200dpi.jpg") as im: assert im.info["photoshop"][0x03ED] == { @@ -782,6 +798,20 @@ class TestFileJpeg: apps_13_lengths = [len(v) for k, v in im.applist if k == "APP13"] assert [65504, 24] == apps_13_lengths + def test_adobe_transform(self): + with Image.open("Tests/images/pil_sample_rgb.jpg") as im: + assert im.info["adobe_transform"] == 1 + + with Image.open("Tests/images/pil_sample_cmyk.jpg") as im: + assert im.info["adobe_transform"] == 2 + + # This image has been manually hexedited + # so that the APP14 reports its length to be 11, + # leaving no room for "adobe_transform" + with Image.open("Tests/images/truncated_app14.jpg") as im: + assert "adobe" in im.info + assert "adobe_transform" not in im.info + def test_icc_after_SOF(self): with Image.open("Tests/images/icc-after-SOF.jpg") as im: assert im.info["icc_profile"] == b"profile" @@ -805,6 +835,32 @@ class TestFileJpeg: # Assert the entire file has not been read assert 0 < buffer.max_pos < size + def test_getxmp(self): + with Image.open("Tests/images/xmp_test.jpg") as im: + if ElementTree is None: + with pytest.warns(UserWarning): + assert im.getxmp() == {} + else: + xmp = im.getxmp() + + description = xmp["xmpmeta"]["RDF"]["Description"] + assert description["DerivedFrom"] == { + "documentID": "8367D410E636EA95B7DE7EBA1C43A412", + "originalDocumentID": "8367D410E636EA95B7DE7EBA1C43A412", + } + assert description["Look"]["Description"]["Group"]["Alt"]["li"] == { + "lang": "x-default", + "text": "Profiles", + } + assert description["ToneCurve"]["Seq"]["li"] == ["0, 0", "255, 255"] + + # Attribute + assert description["Version"] == "10.4" + + if ElementTree is not None: + with Image.open("Tests/images/hopper.jpg") as im: + assert im.getxmp() == {} + @pytest.mark.skipif(not is_win32(), reason="Windows only") @skip_unless_feature("jpg") diff --git a/Tests/test_file_jpeg2k.py b/Tests/test_file_jpeg2k.py index 13ae09af5..20280a579 100644 --- a/Tests/test_file_jpeg2k.py +++ b/Tests/test_file_jpeg2k.py @@ -1,3 +1,4 @@ +import os import re from io import BytesIO @@ -13,6 +14,8 @@ from .helper import ( skip_unless_feature, ) +EXTRA_DIR = "Tests/images/jpeg2000" + pytestmark = skip_unless_feature("jpg_2000") test_card = Image.open("Tests/images/test-card.png") @@ -124,6 +127,16 @@ def test_prog_res_rt(): assert_image_equal(im, test_card) +def test_default_num_resolutions(): + for num_resolutions in range(2, 6): + d = 1 << (num_resolutions - 1) + im = test_card.resize((d - 1, d - 1)) + with pytest.raises(OSError): + roundtrip(im, num_resolutions=num_resolutions) + reloaded = roundtrip(im) + assert_image_equal(im, reloaded) + + def test_reduce(): with Image.open("Tests/images/test-card-lossless.jp2") as im: assert callable(im.reduce) @@ -231,3 +244,42 @@ def test_parser_feed(): # Assert assert p.image.size == (640, 480) + + +@pytest.mark.skipif( + not os.path.exists(EXTRA_DIR), reason="Extra image files not installed" +) +@pytest.mark.parametrize("name", ("subsampling_1", "subsampling_2", "zoo1", "zoo2")) +def test_subsampling_decode(name): + test = f"{EXTRA_DIR}/{name}.jp2" + reference = f"{EXTRA_DIR}/{name}.ppm" + + with Image.open(test) as im: + epsilon = 3 # for YCbCr images + with Image.open(reference) as im2: + width, height = im2.size + if name[-1] == "2": + # RGB reference images are downscaled + epsilon = 3e-3 + width, height = width * 2, height * 2 + expected = im2.resize((width, height), Image.NEAREST) + assert_image_similar(im, expected, epsilon) + + +@pytest.mark.parametrize( + "test_file", + [ + "Tests/images/crash-4fb027452e6988530aa5dabee76eecacb3b79f8a.j2k", + "Tests/images/crash-7d4c83eb92150fb8f1653a697703ae06ae7c4998.j2k", + "Tests/images/crash-ccca68ff40171fdae983d924e127a721cab2bd50.j2k", + "Tests/images/crash-d2c93af851d3ab9a19e34503626368b2ecde9c03.j2k", + ], +) +def test_crashes(test_file): + with open(test_file, "rb") as f: + with Image.open(f) as im: + # Valgrind should not complain here + try: + im.load() + except OSError: + pass diff --git a/Tests/test_file_libtiff.py b/Tests/test_file_libtiff.py index 22b641b5f..e2f0df84a 100644 --- a/Tests/test_file_libtiff.py +++ b/Tests/test_file_libtiff.py @@ -9,7 +9,7 @@ from ctypes import c_float import pytest from PIL import Image, ImageFilter, TiffImagePlugin, TiffTags, features -from PIL.TiffImagePlugin import SUBIFD +from PIL.TiffImagePlugin import STRIPOFFSETS, SUBIFD from .helper import ( assert_image_equal, @@ -17,6 +17,7 @@ from .helper import ( assert_image_similar, assert_image_similar_tofile, hopper, + mark_if_feature_version, skip_unless_feature, ) @@ -577,6 +578,17 @@ class TestFileLibTiff(LibTiffTestCase): TiffImagePlugin.READ_LIBTIFF = False + def test_multipage_seek_backwards(self): + TiffImagePlugin.READ_LIBTIFF = True + with Image.open("Tests/images/multipage.tiff") as im: + im.seek(1) + im.load() + + im.seek(0) + assert im.convert("RGB").getpixel((0, 0)) == (0, 128, 0) + + TiffImagePlugin.READ_LIBTIFF = False + def test__next(self): TiffImagePlugin.READ_LIBTIFF = True with Image.open("Tests/images/hopper.tif") as im: @@ -822,13 +834,17 @@ class TestFileLibTiff(LibTiffTestCase): with Image.open(infile) as im: assert_image_similar_tofile(im, "Tests/images/pil_sample_cmyk.jpg", 0.5) - @pytest.mark.valgrind_known_error(reason="Known Failing") + @mark_if_feature_version( + pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" + ) def test_strip_ycbcr_jpeg_2x2_sampling(self): infile = "Tests/images/tiff_strip_ycbcr_jpeg_2x2_sampling.tif" with Image.open(infile) as im: assert_image_similar_tofile(im, "Tests/images/flower.jpg", 0.5) - @pytest.mark.valgrind_known_error(reason="Known Failing") + @mark_if_feature_version( + pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" + ) def test_strip_ycbcr_jpeg_1x1_sampling(self): infile = "Tests/images/tiff_strip_ycbcr_jpeg_1x1_sampling.tif" with Image.open(infile) as im: @@ -839,13 +855,17 @@ class TestFileLibTiff(LibTiffTestCase): with Image.open(infile) as im: assert_image_similar_tofile(im, "Tests/images/pil_sample_cmyk.jpg", 0.5) - @pytest.mark.valgrind_known_error(reason="Known Failing") + @mark_if_feature_version( + pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" + ) def test_tiled_ycbcr_jpeg_1x1_sampling(self): infile = "Tests/images/tiff_tiled_ycbcr_jpeg_1x1_sampling.tif" with Image.open(infile) as im: assert_image_equal_tofile(im, "Tests/images/flower2.jpg") - @pytest.mark.valgrind_known_error(reason="Known Failing") + @mark_if_feature_version( + pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" + ) def test_tiled_ycbcr_jpeg_2x2_sampling(self): infile = "Tests/images/tiff_tiled_ycbcr_jpeg_2x2_sampling.tif" with Image.open(infile) as im: @@ -892,8 +912,13 @@ class TestFileLibTiff(LibTiffTestCase): assert_image_equal_tofile(im, "Tests/images/tiff_16bit_RGBa_target.png") def test_old_style_jpeg(self): - infile = "Tests/images/old-style-jpeg-compression.tif" - with Image.open(infile) as im: + with Image.open("Tests/images/old-style-jpeg-compression.tif") as im: + assert_image_equal_tofile(im, "Tests/images/old-style-jpeg-compression.png") + + def test_open_missing_samplesperpixel(self): + with Image.open( + "Tests/images/old-style-jpeg-compression-no-samplesperpixel.tif" + ) as im: assert_image_equal_tofile(im, "Tests/images/old-style-jpeg-compression.png") def test_no_rows_per_strip(self): @@ -942,3 +967,12 @@ class TestFileLibTiff(LibTiffTestCase): # Assert that the error code is IMAGING_CODEC_MEMORY assert str(e.value) == "-9" TiffImagePlugin.READ_LIBTIFF = False + + def test_save_multistrip(self, tmp_path): + im = hopper("RGB").resize((256, 256)) + out = str(tmp_path / "temp.tif") + im.save(out, compression="tiff_adobe_deflate") + + with Image.open(out) as im: + # Assert that there are multiple strips + assert len(im.tag_v2[STRIPOFFSETS]) > 1 diff --git a/Tests/test_file_palm.py b/Tests/test_file_palm.py index 25d194b62..e1c1c361b 100644 --- a/Tests/test_file_palm.py +++ b/Tests/test_file_palm.py @@ -5,9 +5,7 @@ import pytest from PIL import Image -from .helper import IMCONVERT, assert_image_equal, hopper, imagemagick_available - -_roundtrip = imagemagick_available() +from .helper import assert_image_equal, hopper, magick_command def helper_save_as_palm(tmp_path, mode): @@ -23,13 +21,10 @@ def helper_save_as_palm(tmp_path, mode): assert os.path.getsize(outfile) > 0 -def open_with_imagemagick(tmp_path, f): - if not imagemagick_available(): - raise OSError() - +def open_with_magick(magick, tmp_path, f): outfile = str(tmp_path / "temp.png") rc = subprocess.call( - [IMCONVERT, f, outfile], stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT + magick + [f, outfile], stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT ) if rc: raise OSError @@ -37,14 +32,15 @@ def open_with_imagemagick(tmp_path, f): def roundtrip(tmp_path, mode): - if not _roundtrip: + magick = magick_command() + if not magick: return im = hopper(mode) outfile = str(tmp_path / "temp.palm") im.save(outfile) - converted = open_with_imagemagick(tmp_path, outfile) + converted = open_with_magick(magick, tmp_path, outfile) assert_image_equal(converted, im) diff --git a/Tests/test_file_pdf.py b/Tests/test_file_pdf.py index e5bba483a..40a027cc5 100644 --- a/Tests/test_file_pdf.py +++ b/Tests/test_file_pdf.py @@ -8,7 +8,7 @@ import pytest from PIL import Image, PdfParser -from .helper import hopper +from .helper import hopper, mark_if_feature_version def helper_save_as_pdf(tmp_path, mode, **kwargs): @@ -30,7 +30,7 @@ def helper_save_as_pdf(tmp_path, mode, **kwargs): with open(outfile, "rb") as fp: contents = fp.read() size = tuple( - int(d) for d in contents.split(b"/MediaBox [ 0 0 ")[1].split(b"]")[0].split() + float(d) for d in contents.split(b"/MediaBox [ 0 0 ")[1].split(b"]")[0].split() ) assert im.size == size @@ -42,7 +42,8 @@ def test_monochrome(tmp_path): mode = "1" # Act / Assert - helper_save_as_pdf(tmp_path, mode) + outfile = helper_save_as_pdf(tmp_path, mode) + assert os.path.getsize(outfile) < 15000 def test_greyscale(tmp_path): @@ -85,7 +86,30 @@ def test_unsupported_mode(tmp_path): im.save(outfile) -@pytest.mark.valgrind_known_error(reason="Known Failing") +def test_resolution(tmp_path): + im = hopper() + + outfile = str(tmp_path / "temp.pdf") + im.save(outfile, resolution=150) + + with open(outfile, "rb") as fp: + contents = fp.read() + + size = tuple( + float(d) + for d in contents.split(b"stream\nq ")[1].split(b" 0 0 cm")[0].split(b" 0 0 ") + ) + assert size == (61.44, 61.44) + + size = tuple( + float(d) for d in contents.split(b"/MediaBox [ 0 0 ")[1].split(b"]")[0].split() + ) + assert size == (61.44, 61.44) + + +@mark_if_feature_version( + pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" +) def test_save_all(tmp_path): # Single frame image helper_save_as_pdf(tmp_path, "RGB", save_all=True) @@ -286,3 +310,13 @@ def test_pdf_append_to_bytesio(): f = io.BytesIO(f.getvalue()) im.save(f, format="PDF", append=True) assert len(f.getvalue()) > initial_size + + +@pytest.mark.timeout(1) +def test_redos(): + malicious = b" trailer<<>>" + b"\n" * 3456 + + # This particular exception isn't relevant here. + # The important thing is it doesn't timeout, cause a ReDoS (CVE-2021-25292). + with pytest.raises(PdfParser.PdfFormatError): + PdfParser.PdfParser(buf=malicious) diff --git a/Tests/test_file_png.py b/Tests/test_file_png.py index bbf5f5772..ffacbbbf4 100644 --- a/Tests/test_file_png.py +++ b/Tests/test_file_png.py @@ -1,4 +1,5 @@ import re +import sys import zlib from io import BytesIO @@ -10,12 +11,19 @@ from .helper import ( PillowLeakTestCase, assert_image, assert_image_equal, + assert_image_equal_tofile, hopper, is_big_endian, is_win32, + mark_if_feature_version, skip_unless_feature, ) +try: + import defusedxml.ElementTree as ElementTree +except ImportError: + ElementTree = None + # sample png stream TEST_PNG_FILE = "Tests/images/hopper.png" @@ -383,25 +391,12 @@ class TestFilePng: # Check dpi roundtripping with Image.open(TEST_PNG_FILE) as im: - im = roundtrip(im, dpi=(100, 100)) - assert im.info["dpi"] == (100, 100) + im = roundtrip(im, dpi=(100.33, 100.33)) + assert im.info["dpi"] == (100.33, 100.33) - def test_load_dpi_rounding(self): - # Round up + def test_load_float_dpi(self): with Image.open(TEST_PNG_FILE) as im: - assert im.info["dpi"] == (96, 96) - - # Round down - with Image.open("Tests/images/icc_profile_none.png") as im: - assert im.info["dpi"] == (72, 72) - - def test_save_dpi_rounding(self): - with Image.open(TEST_PNG_FILE) as im: - im = roundtrip(im, dpi=(72.2, 72.2)) - assert im.info["dpi"] == (72, 72) - - im = roundtrip(im, dpi=(72.8, 72.8)) - assert im.info["dpi"] == (73, 73) + assert im.info["dpi"] == (95.9866, 95.9866) def test_roundtrip_text(self): # Check text roundtripping @@ -625,6 +620,23 @@ class TestFilePng: with Image.open("Tests/images/hopper_idat_after_image_end.png") as im: assert im.text == {"TXT": "VALUE", "ZIP": "VALUE"} + def test_padded_idat(self): + # This image has been manually hexedited + # so that the IDAT chunk has padding at the end + # Set MAXBLOCK to the length of the actual data + # so that the decoder finishes reading before the chunk ends + MAXBLOCK = ImageFile.MAXBLOCK + ImageFile.MAXBLOCK = 45 + ImageFile.LOAD_TRUNCATED_IMAGES = True + + with Image.open("Tests/images/padded_idat.png") as im: + im.load() + + ImageFile.MAXBLOCK = MAXBLOCK + ImageFile.LOAD_TRUNCATED_IMAGES = False + + assert_image_equal_tofile(im, "Tests/images/bw_gradient.png") + def test_specify_bits(self, tmp_path): im = hopper("P") @@ -644,6 +656,18 @@ class TestFilePng: with Image.open(out) as reloaded: assert len(reloaded.png.im_palette[1]) == 3 + def test_getxmp(self): + with Image.open("Tests/images/color_snakes.png") as im: + if ElementTree is None: + with pytest.warns(UserWarning): + assert im.getxmp() == {} + else: + xmp = im.getxmp() + + description = xmp["xmpmeta"]["RDF"]["Description"] + assert description["PixelXDimension"] == "10" + assert description["subject"]["Seq"] is None + def test_exif(self): # With an EXIF chunk with Image.open("Tests/images/exif.png") as im: @@ -679,7 +703,9 @@ class TestFilePng: exif = reloaded._getexif() assert exif[274] == 1 - @pytest.mark.valgrind_known_error(reason="Known Failing") + @mark_if_feature_version( + pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" + ) def test_exif_from_jpg(self, tmp_path): with Image.open("Tests/images/pil_sample_rgb.jpg") as im: test_file = str(tmp_path / "temp.png") @@ -708,6 +734,32 @@ class TestFilePng: with pytest.raises(EOFError): im.seek(1) + @pytest.mark.parametrize("buffer", (True, False)) + def test_save_stdout(self, buffer): + old_stdout = sys.stdout + + if buffer: + + class MyStdOut: + buffer = BytesIO() + + mystdout = MyStdOut() + else: + mystdout = BytesIO() + + sys.stdout = mystdout + + with Image.open(TEST_PNG_FILE) as im: + im.save(sys.stdout, "PNG") + + # Reset stdout + sys.stdout = old_stdout + + if buffer: + mystdout = mystdout.buffer + reloaded = Image.open(mystdout) + assert_image_equal_tofile(reloaded, TEST_PNG_FILE) + @pytest.mark.skipif(is_win32(), reason="Requires Unix or macOS") @skip_unless_feature("zlib") diff --git a/Tests/test_file_psd.py b/Tests/test_file_psd.py index 87373d2c4..bf2a5fea0 100644 --- a/Tests/test_file_psd.py +++ b/Tests/test_file_psd.py @@ -130,3 +130,25 @@ def test_combined_larger_than_size(): with pytest.raises(OSError): with Image.open("Tests/images/combined_larger_than_size.psd"): pass + + +@pytest.mark.parametrize( + "test_file,raises", + [ + ( + "Tests/images/timeout-1ee28a249896e05b83840ae8140622de8e648ba9.psd", + Image.UnidentifiedImageError, + ), + ( + "Tests/images/timeout-598843abc37fc080ec36a2699ebbd44f795d3a6f.psd", + Image.UnidentifiedImageError, + ), + ("Tests/images/timeout-c8efc3fded6426986ba867a399791bae544f59bc.psd", OSError), + ("Tests/images/timeout-dedc7a4ebd856d79b4359bbcc79e8ef231ce38f6.psd", OSError), + ], +) +def test_crashes(test_file, raises): + with open(test_file, "rb") as f: + with pytest.raises(raises): + with Image.open(f): + pass diff --git a/Tests/test_file_tga.py b/Tests/test_file_tga.py index 465e13316..3450c9274 100644 --- a/Tests/test_file_tga.py +++ b/Tests/test_file_tga.py @@ -6,7 +6,7 @@ import pytest from PIL import Image -from .helper import assert_image_equal, hopper +from .helper import assert_image_equal, assert_image_equal_tofile, hopper _TGA_DIR = os.path.join("Tests", "images", "tga") _TGA_DIR_COMMON = os.path.join(_TGA_DIR, "common") @@ -65,6 +65,16 @@ def test_sanity(tmp_path): roundtrip(original_im) +def test_palette_depth_16(tmp_path): + with Image.open("Tests/images/p_16.tga") as im: + assert_image_equal_tofile(im.convert("RGB"), "Tests/images/p_16.png") + + out = str(tmp_path / "temp.png") + im.save(out) + with Image.open(out) as reloaded: + assert_image_equal_tofile(reloaded.convert("RGB"), "Tests/images/p_16.png") + + def test_id_field(): # tga file with id field test_file = "Tests/images/tga_id_field.tga" @@ -112,6 +122,14 @@ def test_save_wrong_mode(tmp_path): im.save(out) +def test_save_mapdepth(): + # This image has been manually hexedited from 200x32_p_bl_raw.tga + # to include an origin + test_file = "Tests/images/200x32_p_bl_raw_origin.tga" + with Image.open(test_file) as im: + assert_image_equal_tofile(im, "Tests/images/tga/common/200x32_p.png") + + def test_save_id_section(tmp_path): test_file = "Tests/images/rgb32rle.tga" with Image.open(test_file) as im: diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py index ba7f9a084..57f45bd09 100644 --- a/Tests/test_file_tiff.py +++ b/Tests/test_file_tiff.py @@ -16,6 +16,11 @@ from .helper import ( is_win32, ) +try: + import defusedxml.ElementTree as ElementTree +except ImportError: + ElementTree = None + class TestFileTiff: def test_sanity(self, tmp_path): @@ -137,37 +142,33 @@ class TestFileTiff: im._setup() assert im.info["dpi"] == (71.0, 71.0) - def test_load_dpi_rounding(self): - for resolutionUnit, dpi in ((None, (72, 73)), (2, (72, 73)), (3, (183, 185))): - with Image.open( - "Tests/images/hopper_roundDown_" + str(resolutionUnit) + ".tif" - ) as im: - assert im.tag_v2.get(RESOLUTION_UNIT) == resolutionUnit - assert im.info["dpi"] == (dpi[0], dpi[0]) + @pytest.mark.parametrize( + "resolutionUnit, dpi", + [(None, 72.8), (2, 72.8), (3, 184.912)], + ) + def test_load_float_dpi(self, resolutionUnit, dpi): + with Image.open( + "Tests/images/hopper_float_dpi_" + str(resolutionUnit) + ".tif" + ) as im: + assert im.tag_v2.get(RESOLUTION_UNIT) == resolutionUnit + assert im.info["dpi"] == (dpi, dpi) - with Image.open( - "Tests/images/hopper_roundUp_" + str(resolutionUnit) + ".tif" - ) as im: - assert im.tag_v2.get(RESOLUTION_UNIT) == resolutionUnit - assert im.info["dpi"] == (dpi[1], dpi[1]) - - def test_save_dpi_rounding(self, tmp_path): + def test_save_float_dpi(self, tmp_path): outfile = str(tmp_path / "temp.tif") with Image.open("Tests/images/hopper.tif") as im: - for dpi in (72.2, 72.8): - im.save(outfile, dpi=(dpi, dpi)) + dpi = (72.2, 72.2) + im.save(outfile, dpi=dpi) - with Image.open(outfile) as reloaded: - reloaded.load() - assert (round(dpi), round(dpi)) == reloaded.info["dpi"] + with Image.open(outfile) as reloaded: + assert reloaded.info["dpi"] == dpi def test_save_setting_missing_resolution(self): b = BytesIO() with Image.open("Tests/images/10ct_32bit_128.tiff") as im: im.save(b, format="tiff", resolution=123.45) with Image.open(b) as im: - assert float(im.tag_v2[X_RESOLUTION]) == 123.45 - assert float(im.tag_v2[Y_RESOLUTION]) == 123.45 + assert im.tag_v2[X_RESOLUTION] == 123.45 + assert im.tag_v2[Y_RESOLUTION] == 123.45 def test_invalid_file(self): invalid_file = "Tests/images/flower.jpg" @@ -301,6 +302,19 @@ class TestFileTiff: assert im.size == (20, 20) assert im.convert("RGB").getpixel((0, 0)) == (0, 0, 255) + def test_frame_order(self): + # A frame can't progress to itself after reading + with Image.open("Tests/images/multipage_single_frame_loop.tiff") as im: + assert im.n_frames == 1 + + # A frame can't progress to a frame that has already been read + with Image.open("Tests/images/multipage_multiple_frame_loop.tiff") as im: + assert im.n_frames == 2 + + # Frames don't have to be in sequence + with Image.open("Tests/images/multipage_out_of_order.tiff") as im: + assert im.n_frames == 3 + def test___str__(self): filename = "Tests/images/pil136.tiff" with Image.open(filename) as im: @@ -389,6 +403,50 @@ class TestFileTiff: with Image.open("Tests/images/ifd_tag_type.tiff") as im: assert 0x8825 in im.tag_v2 + def test_exif(self): + with Image.open("Tests/images/ifd_tag_type.tiff") as im: + exif = im.getexif() + + assert sorted(exif.keys()) == [ + 256, + 257, + 258, + 259, + 262, + 271, + 272, + 273, + 277, + 278, + 279, + 282, + 283, + 284, + 296, + 297, + 305, + 339, + 700, + 34665, + 34853, + 50735, + ] + assert exif[256] == 640 + assert exif[271] == "FLIR" + + gps = exif.get_ifd(0x8825) + assert list(gps.keys()) == [0, 1, 2, 3, 4, 5, 6, 18] + assert gps[0] == b"\x03\x02\x00\x00" + assert gps[18] == "WGS-84" + + def test_exif_frames(self): + # Test that EXIF data can change across frames + with Image.open("Tests/images/g4-multi.tiff") as im: + assert im.getexif()[273] == (328, 815) + + im.seek(1) + assert im.getexif()[273] == (1408, 1907) + def test_seek(self): filename = "Tests/images/pil136.tiff" with Image.open(filename) as im: @@ -590,6 +648,18 @@ class TestFileTiff: with Image.open(outfile) as reloaded: assert "icc_profile" not in reloaded.info + def test_getxmp(self): + with Image.open("Tests/images/lab.tif") as im: + if ElementTree is None: + with pytest.warns(UserWarning): + assert im.getxmp() == {} + else: + xmp = im.getxmp() + + description = xmp["xmpmeta"]["RDF"]["Description"] + assert description[0]["format"] == "image/tiff" + assert description[3]["BitsPerSample"]["Seq"]["li"] == ["8", "8", "8"] + def test_close_on_load_exclusive(self, tmp_path): # similar to test_fd_leak, but runs on unixlike os tmpfile = str(tmp_path / "temp.tif") @@ -625,9 +695,9 @@ class TestFileTiff: ) def test_string_dimension(self): # Assert that an error is raised if one of the dimensions is a string - with pytest.raises(ValueError): - with Image.open("Tests/images/string_dimension.tiff"): - pass + with Image.open("Tests/images/string_dimension.tiff") as im: + with pytest.raises(OSError): + im.load() @pytest.mark.skipif(not is_win32(), reason="Windows only") diff --git a/Tests/test_file_tiff_metadata.py b/Tests/test_file_tiff_metadata.py index 0f7f8adf1..0adbaf016 100644 --- a/Tests/test_file_tiff_metadata.py +++ b/Tests/test_file_tiff_metadata.py @@ -179,6 +179,27 @@ def test_no_duplicate_50741_tag(): assert TAG_IDS["BestQualityScale"] == 50780 +def test_iptc(tmp_path): + out = str(tmp_path / "temp.tiff") + with Image.open("Tests/images/hopper.Lab.tif") as im: + im.save(out) + + +def test_undefined_zero(tmp_path): + # Check that the tag has not been changed since this test was created + tag = TiffTags.TAGS_V2[45059] + assert tag.type == TiffTags.UNDEFINED + assert tag.length == 0 + + info = TiffImagePlugin.ImageFileDirectory(b"II*\x00\x08\x00\x00\x00") + info[45059] = b"test" + + # Assert that the tag value does not change by setting it to itself + original = info[45059] + info[45059] = info[45059] + assert info[45059] == original + + def test_empty_metadata(): f = io.BytesIO(b"II*\x00\x08\x00\x00\x00") head = f.read(8) @@ -355,3 +376,30 @@ def test_too_many_entries(): # Should not raise ValueError. pytest.warns(UserWarning, lambda: ifd[277]) + + +def test_tag_group_data(): + base_ifd = TiffImagePlugin.ImageFileDirectory_v2() + interop_ifd = TiffImagePlugin.ImageFileDirectory_v2(group=40965) + for ifd in (base_ifd, interop_ifd): + ifd[2] = "test" + ifd[256] = 10 + + assert base_ifd.tagtype[256] == 4 + assert interop_ifd.tagtype[256] != base_ifd.tagtype[256] + + assert interop_ifd.tagtype[2] == 7 + assert base_ifd.tagtype[2] != interop_ifd.tagtype[256] + + +def test_empty_subifd(tmp_path): + out = str(tmp_path / "temp.jpg") + + im = hopper() + exif = im.getexif() + exif[TiffImagePlugin.EXIFIFD] = {} + im.save(out, exif=exif) + + with Image.open(out) as reloaded: + exif = reloaded.getexif() + assert exif.get_ifd(TiffImagePlugin.EXIFIFD) == {} diff --git a/Tests/test_file_webp.py b/Tests/test_file_webp.py index cde7020ed..7fdb32ef4 100644 --- a/Tests/test_file_webp.py +++ b/Tests/test_file_webp.py @@ -1,5 +1,6 @@ import io import re +import sys import pytest @@ -119,6 +120,14 @@ class TestFileWebp: self._roundtrip(tmp_path, "P", 50.0) + @pytest.mark.skipif(sys.maxsize <= 2 ** 32, reason="Requires 64-bit system") + def test_write_encoding_error_message(self, tmp_path): + temp_file = str(tmp_path / "temp.webp") + im = Image.new("RGB", (15000, 15000)) + with pytest.raises(ValueError) as e: + im.save(temp_file, method=0) + assert str(e.value) == "encoding error 6" + def test_WebPEncode_with_invalid_args(self): """ Calling encoder functions with no arguments should result in an error. diff --git a/Tests/test_file_webp_animated.py b/Tests/test_file_webp_animated.py index 26e903488..25ebffe02 100644 --- a/Tests/test_file_webp_animated.py +++ b/Tests/test_file_webp_animated.py @@ -45,12 +45,12 @@ def test_write_animation_L(tmp_path): # Compare first and last frames to the original animated GIF orig.load() im.load() - assert_image_similar(im, orig.convert("RGBA"), 25.0) + assert_image_similar(im, orig.convert("RGBA"), 32.9) orig.seek(orig.n_frames - 1) im.seek(im.n_frames - 1) orig.load() im.load() - assert_image_similar(im, orig.convert("RGBA"), 25.0) + assert_image_similar(im, orig.convert("RGBA"), 32.9) @pytest.mark.xfail(is_big_endian(), reason="Fails on big-endian") diff --git a/Tests/test_file_webp_metadata.py b/Tests/test_file_webp_metadata.py index cb133e2c5..e6d6fc63f 100644 --- a/Tests/test_file_webp_metadata.py +++ b/Tests/test_file_webp_metadata.py @@ -4,7 +4,7 @@ import pytest from PIL import Image -from .helper import skip_unless_feature +from .helper import mark_if_feature_version, skip_unless_feature pytestmark = [ skip_unless_feature("webp"), @@ -41,7 +41,9 @@ def test_read_exif_metadata_without_prefix(): assert exif[305] == "Adobe Photoshop CS6 (Macintosh)" -@pytest.mark.valgrind_known_error(reason="Known Failing") +@mark_if_feature_version( + pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" +) def test_write_exif_metadata(): file_path = "Tests/images/flower.jpg" test_buffer = BytesIO() @@ -74,7 +76,9 @@ def test_read_icc_profile(): assert icc == expected_icc -@pytest.mark.valgrind_known_error(reason="Known Failing") +@mark_if_feature_version( + pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" +) def test_write_icc_metadata(): file_path = "Tests/images/flower2.jpg" test_buffer = BytesIO() @@ -92,7 +96,9 @@ def test_write_icc_metadata(): assert webp_icc_profile == expected_icc_profile, "Webp ICC didn't match" -@pytest.mark.valgrind_known_error(reason="Known Failing") +@mark_if_feature_version( + pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" +) def test_read_no_exif(): file_path = "Tests/images/flower.jpg" test_buffer = BytesIO() diff --git a/Tests/test_file_wmf.py b/Tests/test_file_wmf.py index bf9d105e5..3f8bc96cc 100644 --- a/Tests/test_file_wmf.py +++ b/Tests/test_file_wmf.py @@ -44,14 +44,9 @@ def test_register_handler(tmp_path): WmfImagePlugin.register_handler(original_handler) -def test_load_dpi_rounding(): - # Round up +def test_load_float_dpi(): with Image.open("Tests/images/drawing.emf") as im: - assert im.info["dpi"] == 1424 - - # Round down - with Image.open("Tests/images/drawing_roundDown.emf") as im: - assert im.info["dpi"] == 1426 + assert im.info["dpi"] == 1423.7668161434979 def test_load_set_dpi(): diff --git a/Tests/test_image.py b/Tests/test_image.py index 30d093e15..c4e6f8ade 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -16,6 +16,7 @@ from .helper import ( assert_not_all_same, hopper, is_win32, + mark_if_feature_version, skip_unless_feature, ) @@ -581,6 +582,10 @@ class TestImage: assert ext_individual == ext_multiple def test_remap_palette(self): + # Test identity transform + with Image.open("Tests/images/hopper.gif") as im: + assert_image_equal(im, im.remap_palette(list(range(256)))) + # Test illegal image mode with hopper() as im: with pytest.raises(ValueError): @@ -605,7 +610,7 @@ class TestImage: else: assert new_im.palette is None - _make_new(im, im_p, im_p.palette) + _make_new(im, im_p, ImagePalette.ImagePalette(list(range(256)) * 3)) _make_new(im_p, im, None) _make_new(im, blank_p, ImagePalette.ImagePalette()) _make_new(im, blank_pa, ImagePalette.ImagePalette()) @@ -662,7 +667,9 @@ class TestImage: assert not fp.closed - @pytest.mark.valgrind_known_error(reason="Known Failing") + @mark_if_feature_version( + pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" + ) def test_exif_jpeg(self, tmp_path): with Image.open("Tests/images/exif-72dpi-int.jpg") as im: # Little endian exif = im.getexif() @@ -770,6 +777,27 @@ class TestImage: reloaded_exif.load(exif.tobytes()) assert reloaded_exif.get_ifd(0x8769) == exif.get_ifd(0x8769) + def test_exif_load_from_fp(self): + with Image.open("Tests/images/flower.jpg") as im: + data = im.info["exif"] + if data.startswith(b"Exif\x00\x00"): + data = data[6:] + fp = io.BytesIO(data) + + exif = Image.Exif() + exif.load_from_fp(fp) + assert exif == { + 271: "Canon", + 272: "Canon PowerShot S40", + 274: 1, + 282: 180.0, + 283: 180.0, + 296: 2, + 306: "2003:12:14 12:01:44", + 531: 1, + 34665: 196, + } + @pytest.mark.skipif( sys.version_info < (3, 7), reason="Python 3.7 or greater required" ) diff --git a/Tests/test_image_access.py b/Tests/test_image_access.py index e86dc8530..7b3036979 100644 --- a/Tests/test_image_access.py +++ b/Tests/test_image_access.py @@ -23,6 +23,11 @@ else: except ImportError: cffi = None +try: + import numpy +except ImportError: + numpy = None + class AccessTest: # initial value @@ -66,6 +71,10 @@ class TestImagePutPixel(AccessTest): pix1 = im1.load() pix2 = im2.load() + for x, y in ((0, "0"), ("0", 0)): + with pytest.raises(TypeError): + pix1[x, y] + for y in range(im1.size[1]): for x in range(im1.size[0]): pix2[x, y] = pix1[x, y] @@ -109,6 +118,13 @@ class TestImagePutPixel(AccessTest): assert_image_equal(im1, im2) + @pytest.mark.skipif(numpy is None, reason="NumPy not installed") + def test_numpy(self): + im = hopper() + pix = im.load() + + assert pix[numpy.int32(1), numpy.int32(2)] == (18, 20, 59) + class TestImageGetPixel(AccessTest): @staticmethod @@ -339,6 +355,24 @@ class TestImagePutPixelError(AccessTest): with pytest.raises(TypeError, match="color must be int or tuple"): im.putpixel((0, 0), v) + @pytest.mark.parametrize( + ("mode", "band_numbers", "match"), + ( + ("L", (0, 2), "color must be int or single-element tuple"), + ("LA", (0, 3), "color must be int, or tuple of one or two elements"), + ( + "RGB", + (0, 2, 5), + "color must be int, or tuple of one, three or four elements", + ), + ), + ) + def test_putpixel_invalid_number_of_bands(self, mode, band_numbers, match): + im = hopper(mode) + for band_number in band_numbers: + with pytest.raises(TypeError, match=match): + im.putpixel((0, 0), (0,) * band_number) + @pytest.mark.parametrize("mode", IMAGE_MODES2) def test_putpixel_type_error2(self, mode): im = hopper(mode) diff --git a/Tests/test_image_array.py b/Tests/test_image_array.py index 980458407..4dbbdd218 100644 --- a/Tests/test_image_array.py +++ b/Tests/test_image_array.py @@ -4,26 +4,32 @@ from PIL import Image from .helper import hopper +numpy = pytest.importorskip("numpy", reason="NumPy not installed") + im = hopper().resize((128, 100)) def test_toarray(): def test(mode): - ai = im.convert(mode).__array_interface__ - return ai["version"], ai["shape"], ai["typestr"], len(ai["data"]) + ai = numpy.array(im.convert(mode)) + return ai.shape, ai.dtype.str, ai.nbytes - # assert test("1") == (3, (100, 128), '|b1', 1600)) - assert test("L") == (3, (100, 128), "|u1", 12800) + # assert test("1") == ((100, 128), '|b1', 1600)) + assert test("L") == ((100, 128), "|u1", 12800) # FIXME: wrong? - assert test("I") == (3, (100, 128), Image._ENDIAN + "i4", 51200) + assert test("I") == ((100, 128), Image._ENDIAN + "i4", 51200) # FIXME: wrong? - assert test("F") == (3, (100, 128), Image._ENDIAN + "f4", 51200) + assert test("F") == ((100, 128), Image._ENDIAN + "f4", 51200) - assert test("LA") == (3, (100, 128, 2), "|u1", 25600) - assert test("RGB") == (3, (100, 128, 3), "|u1", 38400) - assert test("RGBA") == (3, (100, 128, 4), "|u1", 51200) - assert test("RGBX") == (3, (100, 128, 4), "|u1", 51200) + assert test("LA") == ((100, 128, 2), "|u1", 25600) + assert test("RGB") == ((100, 128, 3), "|u1", 38400) + assert test("RGBA") == ((100, 128, 4), "|u1", 51200) + assert test("RGBX") == ((100, 128, 4), "|u1", 51200) + + with Image.open("Tests/images/truncated_jpeg.jpg") as im_truncated: + with pytest.raises(OSError): + numpy.array(im_truncated) def test_fromarray(): @@ -39,10 +45,18 @@ def test_fromarray(): def test(mode): i = im.convert(mode) - a = i.__array_interface__ - a["strides"] = 1 # pretend it's non-contiguous + a = numpy.array(i) # Make wrapper instance for image, new array interface - wrapped = Wrapper(i, a) + wrapped = Wrapper( + i, + { + "shape": a.shape, + "typestr": a.dtype.str, + "version": 3, + "data": a.data, + "strides": 1, # pretend it's non-contiguous + }, + ) out = Image.fromarray(wrapped) return out.mode, out.size, list(i.getdata()) == list(out.getdata()) diff --git a/Tests/test_image_convert.py b/Tests/test_image_convert.py index 6fe1bd962..5dcdac0e4 100644 --- a/Tests/test_image_convert.py +++ b/Tests/test_image_convert.py @@ -93,7 +93,7 @@ def test_trns_p(tmp_path): im_l.save(f) im_rgb = im.convert("RGB") - assert im_rgb.info["transparency"] == (0, 0, 0) # undone + assert im_rgb.info["transparency"] == (0, 1, 2) # undone im_rgb.save(f) @@ -128,8 +128,8 @@ def test_trns_l(tmp_path): assert "transparency" in im_p.info im_p.save(f) - im_p = pytest.warns(UserWarning, im.convert, "P", palette=Image.ADAPTIVE) - assert "transparency" not in im_p.info + im_p = im.convert("P", palette=Image.ADAPTIVE) + assert "transparency" in im_p.info im_p.save(f) @@ -155,13 +155,19 @@ def test_trns_RGB(tmp_path): assert "transparency" not in im_p.info im_p.save(f) + im = Image.new("RGB", (1, 1)) + im.info["transparency"] = im.getpixel((0, 0)) + im_p = im.convert("P", palette=Image.ADAPTIVE) + assert im_p.info["transparency"] == im_p.getpixel((0, 0)) + im_p.save(f) + def test_gif_with_rgba_palette_to_p(): # See https://github.com/python-pillow/Pillow/issues/2433 with Image.open("Tests/images/hopper.gif") as im: im.info["transparency"] = 255 im.load() - assert im.palette.mode == "RGBA" + assert im.palette.mode == "RGB" im_p = im.convert("P") # Should not raise ValueError: unrecognized raw mode diff --git a/Tests/test_image_paste.py b/Tests/test_image_paste.py index 3740fbcdc..1d3ca8135 100644 --- a/Tests/test_image_paste.py +++ b/Tests/test_image_paste.py @@ -236,7 +236,7 @@ class TestImagingPaste: [ (127, 191, 254, 191), (111, 207, 206, 110), - (255, 255, 255, 0) if mode == "RGBA" else (127, 254, 127, 0), + (127, 254, 127, 0), (207, 207, 239, 239), (191, 191, 190, 191), (207, 206, 111, 112), diff --git a/Tests/test_image_quantize.py b/Tests/test_image_quantize.py index af4172c88..1ceff0842 100644 --- a/Tests/test_image_quantize.py +++ b/Tests/test_image_quantize.py @@ -74,3 +74,13 @@ def test_quantize_dither_diff(): nodither = image.quantize(dither=0, palette=palette) assert dither.tobytes() != nodither.tobytes() + + +def test_transparent_colors_equal(): + im = Image.new("RGBA", (1, 2), (0, 0, 0, 0)) + px = im.load() + px[0, 1] = (255, 255, 255, 0) + + converted = im.quantize() + converted_px = converted.load() + assert converted_px[0, 0] == converted_px[0, 1] diff --git a/Tests/test_image_resample.py b/Tests/test_image_resample.py index 69449198e..8bf2ce916 100644 --- a/Tests/test_image_resample.py +++ b/Tests/test_image_resample.py @@ -4,7 +4,12 @@ import pytest from PIL import Image, ImageDraw -from .helper import assert_image_equal, assert_image_similar, hopper +from .helper import ( + assert_image_equal, + assert_image_similar, + hopper, + mark_if_feature_version, +) class TestImagingResampleVulnerability: @@ -455,7 +460,9 @@ class TestCoreResampleBox: tiled.paste(tile, (x0, y0)) return tiled - @pytest.mark.valgrind_known_error(reason="Known Failing") + @mark_if_feature_version( + pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" + ) def test_tiles(self): with Image.open("Tests/images/flower.jpg") as im: assert im.size == (480, 360) @@ -466,7 +473,9 @@ class TestCoreResampleBox: tiled = self.resize_tiled(im, dst_size, *tiles) assert_image_similar(reference, tiled, 0.01) - @pytest.mark.valgrind_known_error(reason="Known Failing") + @mark_if_feature_version( + pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" + ) def test_subsample(self): # This test shows advantages of the subpixel resizing # after supersampling (e.g. during JPEG decoding). diff --git a/Tests/test_image_resize.py b/Tests/test_image_resize.py index a49abe1b9..17490e1a8 100644 --- a/Tests/test_image_resize.py +++ b/Tests/test_image_resize.py @@ -7,7 +7,12 @@ import pytest from PIL import Image -from .helper import assert_image_equal, assert_image_similar, hopper +from .helper import ( + assert_image_equal, + assert_image_equal_tofile, + assert_image_similar, + hopper, +) class TestImagingCoreResize: @@ -135,6 +140,17 @@ class TestImagingCoreResize: with pytest.raises(ValueError): self.resize(hopper(), (10, 10), 9) + def test_cross_platform(self, tmp_path): + # This test is intended for only check for consistent behaviour across + # platforms. So if a future Pillow change requires that the test file + # be updated, that is okay. + im = hopper().resize((64, 64)) + temp_file = str(tmp_path / "temp.gif") + im.save(temp_file) + + with Image.open(temp_file) as reloaded: + assert_image_equal_tofile(reloaded, "Tests/images/hopper_resized.gif") + @pytest.fixture def gradients_image(): @@ -250,3 +266,7 @@ class TestImageResize: for mode in "1", "P": im = hopper(mode) assert im.resize((20, 20), Image.NEAREST) == im.resize((20, 20)) + + for mode in "I;16", "I;16L", "I;16B", "BGR;15", "BGR;16": + im = hopper(mode) + assert im.resize((20, 20), Image.NEAREST) == im.resize((20, 20)) diff --git a/Tests/test_image_thumbnail.py b/Tests/test_image_thumbnail.py index 6911ce460..dd140955d 100644 --- a/Tests/test_image_thumbnail.py +++ b/Tests/test_image_thumbnail.py @@ -88,6 +88,7 @@ def test_no_resize(): assert im.size == (64, 64) +# valgrind test is failing with memory allocated in libjpeg @pytest.mark.valgrind_known_error(reason="Known Failing") def test_DCT_scaling_edges(): # Make an image with red borders and size (N * 8) + 1 to cross DCT grid diff --git a/Tests/test_imagedraw.py b/Tests/test_imagedraw.py index 06c5b2503..6be8fafa1 100644 --- a/Tests/test_imagedraw.py +++ b/Tests/test_imagedraw.py @@ -538,6 +538,36 @@ def test_pieslice_wide(): assert_image_equal_tofile(im, "Tests/images/imagedraw_pieslice_wide.png") +def test_pieslice_no_spikes(): + im = Image.new("RGB", (161, 161), "white") + draw = ImageDraw.Draw(im) + cxs = ( + [140] * 3 + + list(range(140, 19, -20)) + + [20] * 5 + + list(range(20, 141, 20)) + + [140] * 2 + ) + cys = ( + list(range(80, 141, 20)) + + [140] * 5 + + list(range(140, 19, -20)) + + [20] * 5 + + list(range(20, 80, 20)) + ) + + for cx, cy, angle in zip(cxs, cys, range(0, 360, 15)): + draw.pieslice( + [cx - 100, cy - 100, cx + 100, cy + 100], angle, angle + 1, fill="black" + ) + draw.point([cx, cy], fill="red") + + im_pre_erase = im.copy() + draw.rectangle([21, 21, 139, 139], fill="white") + + assert_image_equal(im, im_pre_erase) + + def helper_point(points): # Arrange im = Image.new("RGB", (W, H)) @@ -722,6 +752,29 @@ def test_rounded_rectangle(xy): assert_image_equal_tofile(im, "Tests/images/imagedraw_rounded_rectangle.png") +@pytest.mark.parametrize( + "xy, radius, type", + [ + ((10, 20, 190, 180), 30.5, "given"), + ((10, 10, 181, 190), 90, "width"), + ((10, 20, 190, 181), 85, "height"), + ], +) +def test_rounded_rectangle_non_integer_radius(xy, radius, type): + # Arrange + im = Image.new("RGB", (200, 200)) + draw = ImageDraw.Draw(im) + + # Act + draw.rounded_rectangle(xy, radius, fill="red", outline="green", width=5) + + # Assert + assert_image_equal_tofile( + im, + "Tests/images/imagedraw_rounded_rectangle_non_integer_radius_" + type + ".png", + ) + + def test_rounded_rectangle_zero_radius(): # Arrange im = Image.new("RGB", (W, H)) @@ -1326,3 +1379,22 @@ def test_compute_regular_polygon_vertices_input_error_handling( with pytest.raises(expected_error) as e: ImageDraw._compute_regular_polygon_vertices(bounding_circle, n_sides, rotation) assert str(e.value) == error_message + + +def test_continuous_horizontal_edges_polygon(): + xy = [ + (2, 6), + (6, 6), + (12, 6), + (12, 12), + (8, 12), + (8, 8), + (4, 8), + (2, 8), + ] + img, draw = create_base_image_draw((16, 16)) + draw.polygon(xy, BLACK) + expected = os.path.join(IMAGES_PATH, "continuous_horizontal_edges_polygon.png") + assert_image_equal_tofile( + img, expected, "continuous horizontal edges polygon failed" + ) diff --git a/Tests/test_imagefile.py b/Tests/test_imagefile.py index b4107e8e3..892087916 100644 --- a/Tests/test_imagefile.py +++ b/Tests/test_imagefile.py @@ -248,4 +248,4 @@ class TestPyDecoder: def test_oserror(self): im = Image.new("RGB", (1, 1)) with pytest.raises(OSError): - im.save(BytesIO(), "JPEG2000") + im.save(BytesIO(), "JPEG2000", num_resolutions=2) diff --git a/Tests/test_imagefont.py b/Tests/test_imagefont.py index dc88cb31d..892bd0ed1 100644 --- a/Tests/test_imagefont.py +++ b/Tests/test_imagefont.py @@ -131,6 +131,9 @@ class TestImageFont: target = "Tests/images/transparent_background_text.png" assert_image_similar_tofile(im, target, 4.09) + target = "Tests/images/transparent_background_text_L.png" + assert_image_similar_tofile(im.convert("L"), target, 0.01) + def test_textsize_equal(self): im = Image.new(mode="RGB", size=(300, 100)) draw = ImageDraw.Draw(im) @@ -716,6 +719,13 @@ class TestImageFont: font.set_variation_by_axes([100]) self._check_text(font, "Tests/images/variation_tiny_axes.png", 32.5) + def test_textbbox_non_freetypefont(self): + im = Image.new("RGB", (200, 200)) + d = ImageDraw.Draw(im) + default_font = ImageFont.load_default() + with pytest.raises(ValueError): + d.textbbox((0, 0), "test", font=default_font) + @pytest.mark.parametrize( "anchor, left, left_old, top", ( @@ -997,3 +1007,16 @@ def test_freetype_deprecation(monkeypatch): # Act / Assert with pytest.warns(DeprecationWarning): ImageFont.truetype(FONT_PATH, FONT_SIZE) + + +@pytest.mark.parametrize( + "test_file", + [ + "Tests/fonts/oom-e8e927ba6c0d38274a37c1567560eb33baf74627.ttf", + ], +) +def test_oom(test_file): + with open(test_file, "rb") as f: + font = ImageFont.truetype(BytesIO(f.read())) + with pytest.raises(Image.DecompressionBombError): + font.getmask("Test Text") diff --git a/Tests/test_imagemorph.py b/Tests/test_imagemorph.py index eb41d2d08..368c2bba1 100644 --- a/Tests/test_imagemorph.py +++ b/Tests/test_imagemorph.py @@ -235,19 +235,19 @@ def test_negate(): ) -def test_non_binary_images(): +def test_incorrect_mode(): im = hopper("RGB") mop = ImageMorph.MorphOp(op_name="erosion8") - with pytest.raises(Exception) as e: + with pytest.raises(ValueError) as e: mop.apply(im) - assert str(e.value) == "Image must be binary, meaning it must use mode L" - with pytest.raises(Exception) as e: + assert str(e.value) == "Image mode must be L" + with pytest.raises(ValueError) as e: mop.match(im) - assert str(e.value) == "Image must be binary, meaning it must use mode L" - with pytest.raises(Exception) as e: + assert str(e.value) == "Image mode must be L" + with pytest.raises(ValueError) as e: mop.get_on_pixels(im) - assert str(e.value) == "Image must be binary, meaning it must use mode L" + assert str(e.value) == "Image mode must be L" def test_add_patterns(): diff --git a/Tests/test_imageops.py b/Tests/test_imageops.py index 33489bd13..dc20d432f 100644 --- a/Tests/test_imageops.py +++ b/Tests/test_imageops.py @@ -29,6 +29,7 @@ def test_sanity(): ImageOps.autocontrast(hopper("L"), cutoff=(2, 10)) ImageOps.autocontrast(hopper("L"), ignore=[0, 255]) ImageOps.autocontrast(hopper("L"), mask=hopper("L")) + ImageOps.autocontrast(hopper("L"), preserve_tone=True) ImageOps.colorize(hopper("L"), (0, 0, 0), (255, 255, 255)) ImageOps.colorize(hopper("L"), "black", "white") @@ -36,6 +37,9 @@ def test_sanity(): ImageOps.pad(hopper("L"), (128, 128)) ImageOps.pad(hopper("RGB"), (128, 128)) + ImageOps.contain(hopper("L"), (128, 128)) + ImageOps.contain(hopper("RGB"), (128, 128)) + ImageOps.crop(hopper("L"), 1) ImageOps.crop(hopper("RGB"), 1) @@ -98,6 +102,13 @@ def test_fit_same_ratio(): assert new_im.size == (1000, 755) +@pytest.mark.parametrize("new_size", ((256, 256), (512, 256), (256, 512))) +def test_contain(new_size): + im = hopper() + new_im = ImageOps.contain(im, new_size) + assert new_im.size == (256, 256) + + def test_pad(): # Same ratio im = hopper() @@ -145,6 +156,25 @@ def test_scale(): assert newimg.size == (25, 25) +def test_expand_palette(): + im = Image.open("Tests/images/p_16.tga") + im_expanded = ImageOps.expand(im, 10, (255, 0, 0)) + + px = im_expanded.convert("RGB").load() + for b in range(10): + for x in range(im_expanded.width): + assert px[x, b] == (255, 0, 0) + assert px[x, im_expanded.height - 1 - b] == (255, 0, 0) + for y in range(im_expanded.height): + assert px[b, x] == (255, 0, 0) + assert px[b, im_expanded.width - 1 - b] == (255, 0, 0) + + im_cropped = im_expanded.crop( + (10, 10, im_expanded.width - 10, im_expanded.height - 10) + ) + assert_image_equal(im_cropped, im) + + def test_colorize_2color(): # Test the colorizing function with 2-color functionality @@ -291,6 +321,7 @@ def test_exif_transpose(): else: assert transposed_im.info["exif"] != original_exif + assert 0x0112 in im.getexif() assert 0x0112 not in transposed_im.getexif() # Repeat the operation to test that it does not keep transposing @@ -336,7 +367,7 @@ def test_autocontrast_mask_toy_input(): assert ImageStat.Stat(result_nomask).median == [128] -def test_auto_contrast_mask_real_input(): +def test_autocontrast_mask_real_input(): # Test the autocontrast with a rectangular mask with Image.open("Tests/images/iptc.jpg") as img: @@ -362,3 +393,52 @@ def test_auto_contrast_mask_real_input(): threshold=2, msg="autocontrast without mask pixel incorrect", ) + + +def test_autocontrast_preserve_tone(): + def autocontrast(mode, preserve_tone): + im = hopper(mode) + return ImageOps.autocontrast(im, preserve_tone=preserve_tone).histogram() + + assert autocontrast("RGB", True) != autocontrast("RGB", False) + assert autocontrast("L", True) == autocontrast("L", False) + + +def test_autocontrast_preserve_gradient(): + gradient = Image.linear_gradient("L") + + # test with a grayscale gradient that extends to 0,255. + # Should be a noop. + out = ImageOps.autocontrast(gradient, cutoff=0, preserve_tone=True) + + assert_image_equal(gradient, out) + + # cutoff the top and bottom + # autocontrast should make the first and last histogram entries equal + # and, with rounding, should be 10% of the image pixels + out = ImageOps.autocontrast(gradient, cutoff=10, preserve_tone=True) + hist = out.histogram() + assert hist[0] == hist[-1] + assert hist[-1] == 256 * round(256 * 0.10) + + # in rgb + img = gradient.convert("RGB") + out = ImageOps.autocontrast(img, cutoff=0, preserve_tone=True) + assert_image_equal(img, out) + + +@pytest.mark.parametrize( + "color", ((255, 255, 255), (127, 255, 0), (127, 127, 127), (0, 0, 0)) +) +def test_autocontrast_preserve_one_color(color): + img = Image.new("RGB", (10, 10), color) + + # single color images shouldn't change + out = ImageOps.autocontrast(img, cutoff=0, preserve_tone=True) + assert_image_equal(img, out) # single color, no cutoff + + # even if there is a cutoff + out = ImageOps.autocontrast( + img, cutoff=10, preserve_tone=True + ) # single color 10 cutoff + assert_image_equal(img, out) diff --git a/Tests/test_imagepalette.py b/Tests/test_imagepalette.py index 0ea2472a9..ecfbda1d8 100644 --- a/Tests/test_imagepalette.py +++ b/Tests/test_imagepalette.py @@ -2,33 +2,76 @@ import pytest from PIL import Image, ImagePalette -from .helper import assert_image_equal_tofile +from .helper import assert_image_equal, assert_image_equal_tofile def test_sanity(): - ImagePalette.ImagePalette("RGB", list(range(256)) * 3) + palette = ImagePalette.ImagePalette("RGB", list(range(256)) * 3) + assert len(palette.colors) == 256 + with pytest.raises(ValueError): - ImagePalette.ImagePalette("RGB", list(range(256)) * 2) + ImagePalette.ImagePalette("RGB", list(range(256)) * 3, 10) + + +def test_reload(): + im = Image.open("Tests/images/hopper.gif") + original = im.copy() + im.palette.dirty = 1 + assert_image_equal(im.convert("RGB"), original.convert("RGB")) def test_getcolor(): palette = ImagePalette.ImagePalette() + assert len(palette.palette) == 0 + assert len(palette.colors) == 0 test_map = {} for i in range(256): test_map[palette.getcolor((i, i, i))] = i - assert len(test_map) == 256 + + # Colors can be converted between RGB and RGBA + rgba_palette = ImagePalette.ImagePalette("RGBA") + assert rgba_palette.getcolor((0, 0, 0)) == rgba_palette.getcolor((0, 0, 0, 255)) + + assert palette.getcolor((0, 0, 0)) == palette.getcolor((0, 0, 0, 255)) + + # An error is raised when the palette is full with pytest.raises(ValueError): palette.getcolor((1, 2, 3)) + # But not if the image is not using one of the palette entries + palette.getcolor((1, 2, 3), image=Image.new("P", (1, 1))) # Test unknown color specifier with pytest.raises(ValueError): palette.getcolor("unknown") +@pytest.mark.parametrize( + "index, palette", + [ + # Test when the palette is not full + (0, ImagePalette.ImagePalette()), + # Test when the palette is full + (255, ImagePalette.ImagePalette("RGB", list(range(256)) * 3)), + ], +) +def test_getcolor_not_special(index, palette): + im = Image.new("P", (1, 1)) + + # Do not use transparency index as a new color + im.info["transparency"] = index + index1 = palette.getcolor((0, 0, 0), im) + assert index1 != index + + # Do not use background index as a new color + im.info["background"] = index1 + index2 = palette.getcolor((0, 0, 1), im) + assert index2 not in (index, index1) + + def test_file(tmp_path): palette = ImagePalette.ImagePalette("RGB", list(range(256)) * 3) @@ -116,7 +159,7 @@ def test_getdata(): mode, data_out = palette.getdata() # Assert - assert mode == "RGB;L" + assert mode == "RGB" def test_rawmode_getdata(): diff --git a/Tests/test_psdraw.py b/Tests/test_psdraw.py index 31f0f493b..e74d79828 100644 --- a/Tests/test_psdraw.py +++ b/Tests/test_psdraw.py @@ -1,6 +1,8 @@ import os import sys -from io import StringIO +from io import BytesIO + +import pytest from PIL import Image, PSDraw @@ -44,10 +46,21 @@ def test_draw_postscript(tmp_path): assert os.path.getsize(tempfile) > 0 -def test_stdout(): +@pytest.mark.parametrize("buffer", (True, False)) +def test_stdout(buffer): # Temporarily redirect stdout old_stdout = sys.stdout - sys.stdout = mystdout = StringIO() + + if buffer: + + class MyStdOut: + buffer = BytesIO() + + mystdout = MyStdOut() + else: + mystdout = BytesIO() + + sys.stdout = mystdout ps = PSDraw.PSDraw() _create_document(ps) @@ -55,4 +68,6 @@ def test_stdout(): # Reset stdout sys.stdout = old_stdout - assert mystdout.getvalue() != "" + if buffer: + mystdout = mystdout.buffer + assert mystdout.getvalue() != b"" diff --git a/Tests/test_sgi_crash.py b/Tests/test_sgi_crash.py index d4ddc12f9..f9eaf9b19 100644 --- a/Tests/test_sgi_crash.py +++ b/Tests/test_sgi_crash.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python import pytest from PIL import Image diff --git a/Tests/test_tiff_crashes.py b/Tests/test_tiff_crashes.py index ae4d0f100..6cdb8e44d 100644 --- a/Tests/test_tiff_crashes.py +++ b/Tests/test_tiff_crashes.py @@ -33,6 +33,9 @@ from .helper import on_ci "Tests/images/crash-86214e58da443d2b80820cff9677a38a33dcbbca.tif", "Tests/images/crash-f46f5b2f43c370fe65706c11449f567ecc345e74.tif", "Tests/images/crash-63b1dffefc8c075ddc606c0a2f5fdc15ece78863.tif", + "Tests/images/crash-74d2a78403a5a59db1fb0a2b8735ac068a75f6e3.tif", + "Tests/images/crash-81154a65438ba5aaeca73fd502fa4850fbde60f8.tif", + "Tests/images/crash-0da013a13571cc8eb457a39fee8db18f8a3c7127.tif", ], ) @pytest.mark.filterwarnings("ignore:Possibly corrupt EXIF data") diff --git a/Tests/test_tiff_ifdrational.py b/Tests/test_tiff_ifdrational.py index 1697a8d49..12f475df0 100644 --- a/Tests/test_tiff_ifdrational.py +++ b/Tests/test_tiff_ifdrational.py @@ -28,6 +28,8 @@ def test_sanity(): _test_equal(1, 2, Fraction(1, 2)) _test_equal(1, 2, IFDRational(1, 2)) + _test_equal(7, 5, 1.4) + def test_ranges(): for num in range(1, 10): diff --git a/depends/install_imagequant.sh b/depends/install_imagequant.sh index 376d8ef9b..c6c7506a3 100755 --- a/depends/install_imagequant.sh +++ b/depends/install_imagequant.sh @@ -1,7 +1,7 @@ #!/bin/bash # install libimagequant -archive=libimagequant-2.14.1 +archive=libimagequant-2.15.1 ./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/master/$archive.tar.gz diff --git a/docs/Guardfile b/docs/Guardfile index 16f731611..b689b079a 100755 --- a/docs/Guardfile +++ b/docs/Guardfile @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 from livereload.compiler import shell from livereload.task import Task diff --git a/docs/conf.py b/docs/conf.py index 123e93c9b..807281965 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -29,11 +29,13 @@ needs_sphinx = "2.4" # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ + "sphinx_copybutton", + "sphinx_issues", + "sphinx_removed_in", "sphinx.ext.autodoc", "sphinx.ext.intersphinx", "sphinx.ext.viewcode", - "sphinx_issues", - "sphinx_removed_in", + "sphinxext.opengraph", ] intersphinx_mapping = {"python": ("https://docs.python.org/3", None)} @@ -310,9 +312,17 @@ texinfo_documents = [ def setup(app): app.add_js_file("js/script.js") + app.add_css_file("css/styles.css") app.add_css_file("css/dark.css") app.add_css_file("css/light.css") # GitHub repo for sphinx-issues issues_github_path = "python-pillow/Pillow" + +# sphinxext.opengraph +ogp_image = ( + "https://raw.githubusercontent.com/python-pillow/pillow-logo/master/" + "pillow-logo-dark-text-1280x640.png" +) +ogp_image_alt = "Pillow" diff --git a/docs/deprecations.rst b/docs/deprecations.rst index ef88afa23..9ce2fe7b3 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -25,26 +25,6 @@ vulnerability introduced in FreeType 2.6 (:cve:`CVE-2020-15999`). .. _2.10.4: https://sourceforge.net/projects/freetype/files/freetype2/2.10.4/ -Tk/Tcl 8.4 -~~~~~~~~~~ - -.. deprecated:: 8.2.0 - -Support for Tk/Tcl 8.4 is deprecated and will be removed in Pillow 10.0.0 (2023-01-02), -when Tk/Tcl 8.5 will be the minimum supported. - -Categories -~~~~~~~~~~ - -.. deprecated:: 8.2.0 - -``im.category`` is deprecated and will be removed in Pillow 10.0.0 (2023-01-02), -along with the related ``Image.NORMAL``, ``Image.SEQUENCE`` and -``Image.CONTAINER`` attributes. - -To determine if an image has multiple frames or not, -``getattr(im, "is_animated", False)`` can be used instead. - Image.show command parameter ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -82,6 +62,36 @@ Use ``__version__`` instead. It was initially removed in Pillow 7.0.0, but brought back in 7.1.0 to give projects more time to upgrade. +Tk/Tcl 8.4 +~~~~~~~~~~ + +.. deprecated:: 8.2.0 + +Support for Tk/Tcl 8.4 is deprecated and will be removed in Pillow 10.0.0 (2023-01-02), +when Tk/Tcl 8.5 will be the minimum supported. + +Categories +~~~~~~~~~~ + +.. deprecated:: 8.2.0 + +``im.category`` is deprecated and will be removed in Pillow 10.0.0 (2023-01-02), +along with the related ``Image.NORMAL``, ``Image.SEQUENCE`` and +``Image.CONTAINER`` attributes. + +To determine if an image has multiple frames or not, +``getattr(im, "is_animated", False)`` can be used instead. + +JpegImagePlugin.convert_dict_qtables +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. deprecated:: 8.3.0 + +JPEG ``quantization`` is now automatically converted, but still returned as a +dictionary. The :py:attr:`~PIL.JpegImagePlugin.convert_dict_qtables` method no longer +performs any operations on the data given to it, has been deprecated and will be +removed in Pillow 10.0.0 (2023-01-02). + Removed features ---------------- @@ -125,7 +135,6 @@ Some attributes in :py:class:`PIL.ImageCms.CmsProfile` have been removed. From 6 they issued a ``DeprecationWarning``: ======================== =================================================== - Removed Use instead ======================== =================================================== ``color_space`` Padded :py:attr:`~.CmsProfile.xcolor_space` @@ -251,7 +260,7 @@ PIL.OleFileIO .. deprecated:: 4.0.0 .. versionremoved:: 6.0.0 -PIL.OleFileIO was removed as a vendored file and in Pillow 4.0.0 (2017-01) in favour of +PIL.OleFileIO was removed as a vendored file in Pillow 4.0.0 (2017-01) in favour of the upstream olefile Python package, and replaced with an ``ImportError`` in 5.0.0 (2018-01). The deprecated file has now been removed from Pillow. If needed, install from PyPI (eg. ``python3 -m pip install olefile``). diff --git a/docs/handbook/concepts.rst b/docs/handbook/concepts.rst index c4cdda78d..66eeaf6f8 100644 --- a/docs/handbook/concepts.rst +++ b/docs/handbook/concepts.rst @@ -44,7 +44,7 @@ supports the following standard modes: * ``I`` (32-bit signed integer pixels) * ``F`` (32-bit floating point pixels) -Pillow also provides limited support for a few special modes, including: +Pillow also provides limited support for a few additional modes, including: * ``LA`` (L with alpha) * ``PA`` (P with alpha) diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index c67f8fb8f..5d4e83494 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -13,6 +13,14 @@ contents, not their names, but the :py:meth:`~PIL.Image.Image.save` method looks at the name to determine which format to use, unless the format is given explicitly. +When an image is opened from a file, only that instance of the image is considered to +have the format. Copies of the image will contain data loaded from the file, but not +the file itself, meaning that it can no longer be considered to be in the original +format. So if :py:meth:`~PIL.Image.Image.copy` is called on an image, or another method +internally creates a copy of the image, the ``fp`` (file pointer), along with any +methods and attributes specific to a format. The :py:attr:`~PIL.Image.Image.format` +attribute will be ``None``. + Fully supported formats ----------------------- @@ -31,6 +39,13 @@ The :py:meth:`~PIL.Image.open` method sets the following **compression** Set to ``bmp_rle`` if the file is run-length encoded. +DDS +^^^ + +DDS is a popular container texture format used in video games and natively supported +by DirectX. Uncompressed RGB and RGBA can be read, and (since 8.3.0) written. DXT1, +DXT3 (since 3.4.0) and DXT5 pixel formats can be read, only in ``RGBA`` mode. + DIB ^^^ @@ -200,12 +215,16 @@ attributes before loading the file:: ICNS ^^^^ -Pillow reads and (macOS only) writes macOS ``.icns`` files. By default, the +Pillow reads and writes macOS ``.icns`` files. By default, the largest available icon is read, though you can override this by setting the :py:attr:`~PIL.Image.Image.size` property before calling :py:meth:`~PIL.Image.Image.load`. The :py:meth:`~PIL.Image.open` method sets the following :py:attr:`~PIL.Image.Image.info` property: +.. note:: + + Prior to version 8.3.0, Pillow could only write ICNS files on macOS. + **sizes** A list of supported sizes found in this icon file; these are a 3-tuple, ``(width, height, scale)``, where ``scale`` is 2 for a retina @@ -247,6 +266,12 @@ The :py:meth:`~PIL.Image.Image.save` method can take the following keyword argum .. versionadded:: 8.1.0 +**bitmap_format** + By default, the image data will be saved in PNG format. With a bitmap format of + "bmp", image data will be saved in BMP format instead. + + .. versionadded:: 8.3.0 + IM ^^ @@ -940,7 +965,7 @@ The :py:meth:`~PIL.Image.Image.save` method supports the following options: files compared to the slowest, but best, 100. **method** - Quality/speed trade-off (0=fast, 6=slower-better). Defaults to 0. + Quality/speed trade-off (0=fast, 6=slower-better). Defaults to 4. **icc_profile** The ICC Profile to include in the saved file. Only supported if @@ -1028,17 +1053,6 @@ is commonly used in fax applications. The DCX decoder can read files containing When the file is opened, only the first image is read. You can use :py:meth:`~PIL.Image.Image.seek` or :py:mod:`~PIL.ImageSequence` to read other images. - -DDS -^^^ - -DDS is a popular container texture format used in video games and natively -supported by DirectX. -Currently, uncompressed RGB data and DXT1, DXT3, and DXT5 pixel formats are -supported, and only in ``RGBA`` mode. - -.. versionadded:: 3.4.0 DXT3 - FLI, FLC ^^^^^^^^ @@ -1242,8 +1256,10 @@ The :py:meth:`~PIL.Image.Image.save` method can take the following keyword argum .. versionadded:: 3.0.0 **append_images** - A list of images to append as additional pages. Each of the - images in the list can be single or multiframe images. + A list of :py:class:`PIL.Image.Image` objects to append as additional pages. Each + of the images in the list can be single or multiframe images. The ``save_all`` + parameter must be present and set to ``True`` in conjunction with + ``append_images``. .. versionadded:: 4.2.0 diff --git a/docs/handbook/tutorial.rst b/docs/handbook/tutorial.rst index 6b68a0562..cdac0ae2d 100644 --- a/docs/handbook/tutorial.rst +++ b/docs/handbook/tutorial.rst @@ -424,7 +424,7 @@ Drawing PostScript title = "hopper" box = (1*72, 2*72, 7*72, 10*72) # in points - ps = PSDraw.PSDraw() # default is sys.stdout + ps = PSDraw.PSDraw() # default is sys.stdout or sys.stdout.buffer ps.begin_document(title) # draw the image (75 dpi) diff --git a/docs/handbook/writing-your-own-file-decoder.rst b/docs/handbook/writing-your-own-file-decoder.rst index 9b670dba8..5f600a667 100644 --- a/docs/handbook/writing-your-own-file-decoder.rst +++ b/docs/handbook/writing-your-own-file-decoder.rst @@ -191,34 +191,34 @@ match PIL’s internal pixel layout. PIL supports a large set of raw modes; for complete list, see the table in the :file:`Unpack.c` module. The following table describes some commonly used **raw modes**: -+-----------+-----------------------------------------------------------------+ -| mode | description | -+===========+=================================================================+ -| ``1`` | 1-bit bilevel, stored with the leftmost pixel in the most | -| | significant bit. 0 means black, 1 means white. | -+-----------+-----------------------------------------------------------------+ -| ``1;I`` | 1-bit inverted bilevel, stored with the leftmost pixel in the | -| | most significant bit. 0 means white, 1 means black. | -+-----------+-----------------------------------------------------------------+ -| ``1;R`` | 1-bit reversed bilevel, stored with the leftmost pixel in the | -| | least significant bit. 0 means black, 1 means white. | -+-----------+-----------------------------------------------------------------+ -| ``L`` | 8-bit greyscale. 0 means black, 255 means white. | -+-----------+-----------------------------------------------------------------+ -| ``L;I`` | 8-bit inverted greyscale. 0 means white, 255 means black. | -+-----------+-----------------------------------------------------------------+ -| ``P`` | 8-bit palette-mapped image. | -+-----------+-----------------------------------------------------------------+ -| ``RGB`` | 24-bit true colour, stored as (red, green, blue). | -+-----------+-----------------------------------------------------------------+ -| ``BGR`` | 24-bit true colour, stored as (blue, green, red). | -+-----------+-----------------------------------------------------------------+ -| ``RGBX`` | 24-bit true colour, stored as (red, green, blue, pad). The pad | -| | pixels may vary. | -+-----------+-----------------------------------------------------------------+ -| ``RGB;L`` | 24-bit true colour, line interleaved (first all red pixels, then| -| | all green pixels, finally all blue pixels). | -+-----------+-----------------------------------------------------------------+ ++-----------+-------------------------------------------------------------------+ +| mode | description | ++===========+===================================================================+ +| ``1`` | | 1-bit bilevel, stored with the leftmost pixel in the most | +| | | significant bit. 0 means black, 1 means white. | ++-----------+-------------------------------------------------------------------+ +| ``1;I`` | | 1-bit inverted bilevel, stored with the leftmost pixel in the | +| | | most significant bit. 0 means white, 1 means black. | ++-----------+-------------------------------------------------------------------+ +| ``1;R`` | | 1-bit reversed bilevel, stored with the leftmost pixel in the | +| | | least significant bit. 0 means black, 1 means white. | ++-----------+-------------------------------------------------------------------+ +| ``L`` | 8-bit greyscale. 0 means black, 255 means white. | ++-----------+-------------------------------------------------------------------+ +| ``L;I`` | 8-bit inverted greyscale. 0 means white, 255 means black. | ++-----------+-------------------------------------------------------------------+ +| ``P`` | 8-bit palette-mapped image. | ++-----------+-------------------------------------------------------------------+ +| ``RGB`` | 24-bit true colour, stored as (red, green, blue). | ++-----------+-------------------------------------------------------------------+ +| ``BGR`` | 24-bit true colour, stored as (blue, green, red). | ++-----------+-------------------------------------------------------------------+ +| ``RGBX`` | | 24-bit true colour, stored as (red, green, blue, pad). The pad | +| | | pixels may vary. | ++-----------+-------------------------------------------------------------------+ +| ``RGB;L`` | | 24-bit true colour, line interleaved (first all red pixels, then| +| | | all green pixels, finally all blue pixels). | ++-----------+-------------------------------------------------------------------+ Note that for the most common cases, the raw mode is simply the same as the mode. diff --git a/docs/index.rst b/docs/index.rst index d2aca4bc4..3348feb89 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -29,9 +29,13 @@ Pillow for enterprise is available via the Tidelift Subscription. `Learn more = 1.0 no longer supports "import Image". Please use "from PIL import Image" instead. +.. warning:: Pillow >= 1.0 no longer supports ``import Image``. Please use ``from PIL import Image`` instead. -.. warning:: Pillow >= 2.1.0 no longer supports "import _imaging". Please use "from PIL.Image import core as _imaging" instead. +.. warning:: Pillow >= 2.1.0 no longer supports ``import _imaging``. Please use ``from PIL.Image import core as _imaging`` instead. Python Support -------------- Pillow supports these Python versions. -+----------------------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+ -| **Python** |**3.9**|**3.8**|**3.7**|**3.6**|**3.5**|**3.4**|**3.3**|**3.2**|**2.7**|**2.6**|**2.5**|**2.4**| -+----------------------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+ -| Pillow >= 8.0 | Yes | Yes | Yes | Yes | | | | | | | | | -+----------------------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+ -| Pillow 7.0 - 7.2 | | Yes | Yes | Yes | Yes | | | | | | | | -+----------------------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+ -| Pillow 6.2.1 - 6.2.2 | | Yes | Yes | Yes | Yes | | | | Yes | | | | -+----------------------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+ -| Pillow 6.0 - 6.2.0 | | | Yes | Yes | Yes | | | | Yes | | | | -+----------------------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+ -| Pillow 5.2 - 5.4 | | | Yes | Yes | Yes | Yes | | | Yes | | | | -+----------------------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+ -| Pillow 5.0 - 5.1 | | | | Yes | Yes | Yes | | | Yes | | | | -+----------------------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+ -| Pillow 4 | | | | Yes | Yes | Yes | Yes | | Yes | | | | -+----------------------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+ -| Pillow 2 - 3 | | | | | Yes | Yes | Yes | Yes | Yes | Yes | | | -+----------------------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+ -| Pillow < 2 | | | | | | | | | Yes | Yes | Yes | Yes | -+----------------------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+-------+ ++----------------------+-----+-----+-----+-----+-----+-----+-----+-----+ +| Python |3.10 | 3.9 | 3.8 | 3.7 | 3.6 | 3.5 | 3.4 | 2.7 | ++======================+=====+=====+=====+=====+=====+=====+=====+=====+ +| Pillow >= 8.3 | Yes | Yes | Yes | Yes | Yes | | | | ++----------------------+-----+-----+-----+-----+-----+-----+-----+-----+ +| Pillow 8.0 - 8.2 | | Yes | Yes | Yes | Yes | | | | ++----------------------+-----+-----+-----+-----+-----+-----+-----+-----+ +| Pillow 7.0 - 7.2 | | | Yes | Yes | Yes | Yes | | | ++----------------------+-----+-----+-----+-----+-----+-----+-----+-----+ +| Pillow 6.2.1 - 6.2.2 | | | Yes | Yes | Yes | Yes | | Yes | ++----------------------+-----+-----+-----+-----+-----+-----+-----+-----+ +| Pillow 6.0 - 6.2.0 | | | | Yes | Yes | Yes | | Yes | ++----------------------+-----+-----+-----+-----+-----+-----+-----+-----+ +| Pillow 5.2 - 5.4 | | | | Yes | Yes | Yes | Yes | Yes | ++----------------------+-----+-----+-----+-----+-----+-----+-----+-----+ + ++------------------+-----+-----+-----+-----+-----+-----+-----+-----+-----+ +| Python | 3.6 | 3.5 | 3.4 | 3.3 | 3.2 | 2.7 | 2.6 | 2.5 | 2.4 | ++==================+=====+=====+=====+=====+=====+=====+=====+=====+=====+ +| Pillow 5.0 - 5.1 | Yes | Yes | Yes | | | Yes | | | | ++------------------+-----+-----+-----+-----+-----+-----+-----+-----+-----+ +| Pillow 4 | Yes | Yes | Yes | Yes | | Yes | | | | ++------------------+-----+-----+-----+-----+-----+-----+-----+-----+-----+ +| Pillow 2 - 3 | | Yes | Yes | Yes | Yes | Yes | Yes | | | ++------------------+-----+-----+-----+-----+-----+-----+-----+-----+-----+ +| Pillow < 2 | | | | | | Yes | Yes | Yes | Yes | ++------------------+-----+-----+-----+-----+-----+-----+-----+-----+-----+ Basic Installation ------------------ @@ -57,8 +63,9 @@ Windows Installation We provide Pillow binaries for Windows compiled for the matrix of supported Pythons in both 32 and 64-bit versions in the wheel format. -These binaries have all of the optional libraries included except -for raqm, libimagequant, and libxcb:: +These binaries include support for all optional libraries except +libimagequant and libxcb. Raqm support requires +FriBiDi to be installed separately:: python3 -m pip install --upgrade pip python3 -m pip install --upgrade Pillow @@ -71,8 +78,8 @@ macOS Installation We provide binaries for macOS for each of the supported Python versions in the wheel format. These include support for all optional -libraries except libimagequant and libxcb. Raqm support requires -libraqm, fribidi, and harfbuzz to be installed separately:: +libraries except libimagequant. Raqm support requires +FriBiDi to be installed separately:: python3 -m pip install --upgrade pip python3 -m pip install --upgrade Pillow @@ -83,7 +90,7 @@ Linux Installation 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 libimagequant. Raqm support requires -libraqm, fribidi, and harfbuzz to be installed separately:: +FriBiDi to be installed separately:: python3 -m pip install --upgrade pip python3 -m pip install --upgrade Pillow @@ -153,7 +160,7 @@ Many of Pillow's features require external libraries: * **libtiff** provides compressed TIFF functionality - * Pillow has been tested with libtiff versions **3.x** and **4.0-4.1** + * Pillow has been tested with libtiff versions **3.x** and **4.0-4.3** * **libfreetype** provides type related services @@ -178,7 +185,7 @@ Many of Pillow's features require external libraries: * **libimagequant** provides improved color quantization - * Pillow has been tested with libimagequant **2.6-2.14.1** + * Pillow has been tested with libimagequant **2.6-2.15.1** * Libimagequant is licensed GPLv3, which is more restrictive than the Pillow license, therefore we will not be distributing binaries with libimagequant support enabled. @@ -191,11 +198,15 @@ Many of Pillow's features require external libraries: * libraqm depends on the following libraries: FreeType, HarfBuzz, FriBiDi, make sure that you install them before installing libraqm if not available as package in your system. - * setting text direction or font features is not supported without - libraqm. - * libraqm is dynamically loaded in Pillow 5.0.0 and above, so support - is available if all the libraries are installed. - * Windows support: Raqm is not included in prebuilt wheels + * Setting text direction or font features is not supported without libraqm. + * Pillow wheels since version 8.2.0 include a modified version of libraqm that + loads libfribidi at runtime if it is installed. + On Windows this requires compiling FriBiDi and installing ``fribidi.dll`` + into a directory listed in the `Dynamic-Link Library Search Order (Microsoft Docs) + `_ + (``fribidi-0.dll`` or ``libfribidi-0.dll`` are also detected). + See `Build Options`_ to see how to build this version. + * Previous versions of Pillow (5.0.0 to 8.1.2) linked libraqm dynamically at runtime. * **libxcb** provides X11 screengrab support. @@ -244,6 +255,12 @@ Build Options an exception if the libraries are not found. Webpmux (WebP metadata) relies on WebP support. Tcl and Tk also must be used together. +* Build flags: ``--vendor-raqm --vendor-fribidi`` + These flags are used to compile a modified version of libraqm and + a shim that dynamically loads libfribidi at runtime. These are + used to compile the standard Pillow wheels. Compiling libraqm requires + a C99-compliant compiler. + * Build flag: ``--disable-platform-guessing``. Skips all of the platform dependent guessing of include and library directories for automated build systems that configure the proper paths in the @@ -426,41 +443,41 @@ Continuous Integration Targets These platforms are built and tested for every change. -+----------------------------------+--------------------------+-----------------------+ -|**Operating system** |**Tested Python versions**|**Tested architecture**| -+----------------------------------+--------------------------+-----------------------+ -| Alpine | 3.8 |x86-64 | -+----------------------------------+--------------------------+-----------------------+ -| Arch | 3.8 |x86-64 | -+----------------------------------+--------------------------+-----------------------+ -| Amazon Linux 2 | 3.7 |x86-64 | -+----------------------------------+--------------------------+-----------------------+ -| CentOS 7 | 3.6 |x86-64 | -+----------------------------------+--------------------------+-----------------------+ -| CentOS 8 | 3.6 |x86-64 | -+----------------------------------+--------------------------+-----------------------+ -| Debian 10 Buster | 3.7 |x86 | -+----------------------------------+--------------------------+-----------------------+ -| Fedora 32 | 3.8 |x86-64 | -+----------------------------------+--------------------------+-----------------------+ -| Fedora 33 | 3.9 |x86-64 | -+----------------------------------+--------------------------+-----------------------+ -| macOS 10.15 Catalina | 3.6, 3.7, 3.8, 3.9, PyPy3|x86-64 | -+----------------------------------+--------------------------+-----------------------+ -| Ubuntu Linux 16.04 LTS (Xenial) | 3.6, 3.7, 3.8, 3.9, PyPy3|x86-64 | -+----------------------------------+--------------------------+-----------------------+ -| Ubuntu Linux 18.04 LTS (Bionic) | 3.6, 3.7, 3.8, 3.9, PyPy3|x86-64 | -+----------------------------------+--------------------------+-----------------------+ -| Ubuntu Linux 20.04 LTS (Focal) | 3.8 |x86-64 | -+----------------------------------+--------------------------+-----------------------+ -| Windows Server 2016 | 3.6 |x86-64 | -+----------------------------------+--------------------------+-----------------------+ -| Windows Server 2019 | 3.6, 3.7, 3.8, 3.9 |x86, x86-64 | -| +--------------------------+-----------------------+ -| | PyPy3 |x86 | -| +--------------------------+-----------------------+ -| | 3.8/MinGW |x86, x86-64 | -+----------------------------------+--------------------------+-----------------------+ ++----------------------------------+---------------------------+---------------------+ +| Operating system | Tested Python versions | Tested architecture | ++==================================+===========================+=====================+ +| Alpine | 3.8 | x86-64 | ++----------------------------------+---------------------------+---------------------+ +| Arch | 3.8 | x86-64 | ++----------------------------------+---------------------------+---------------------+ +| Amazon Linux 2 | 3.7 | x86-64 | ++----------------------------------+---------------------------+---------------------+ +| CentOS 7 | 3.6 | x86-64 | ++----------------------------------+---------------------------+---------------------+ +| CentOS 8 | 3.6 | x86-64 | ++----------------------------------+---------------------------+---------------------+ +| Debian 10 Buster | 3.7 | x86 | ++----------------------------------+---------------------------+---------------------+ +| Fedora 33 | 3.9 | x86-64 | ++----------------------------------+---------------------------+---------------------+ +| Fedora 34 | 3.9 | x86-64 | ++----------------------------------+---------------------------+---------------------+ +| macOS 10.15 Catalina | 3.6, 3.7, 3.8, 3.9, PyPy3 | x86-64 | ++----------------------------------+---------------------------+---------------------+ +| Ubuntu Linux 16.04 LTS (Xenial) | 3.6, 3.7, 3.8, 3.9, PyPy3 | x86-64 | ++----------------------------------+---------------------------+---------------------+ +| Ubuntu Linux 18.04 LTS (Bionic) | 3.6, 3.7, 3.8, 3.9, PyPy3 | x86-64 | ++----------------------------------+---------------------------+---------------------+ +| Ubuntu Linux 20.04 LTS (Focal) | 3.8 | x86-64 | ++----------------------------------+---------------------------+---------------------+ +| Windows Server 2016 | 3.6 | x86-64 | ++----------------------------------+---------------------------+---------------------+ +| Windows Server 2019 | 3.6, 3.7, 3.8, 3.9 | x86, x86-64 | +| +---------------------------+---------------------+ +| | PyPy3 | x86 | +| +---------------------------+---------------------+ +| | 3.8/MinGW | x86, x86-64 | ++----------------------------------+---------------------------+---------------------+ Other Platforms @@ -473,74 +490,79 @@ These platforms have been reported to work at the versions mentioned. Contributors please test Pillow on your platform then update this document and send a pull request. -+----------------------------------+------------------------------+--------------------------------+-----------------------+ -|**Operating system** |**Tested Python versions** |**Latest tested Pillow version**|**Tested processors** | -+----------------------------------+------------------------------+--------------------------------+-----------------------+ -| macOS 11.0 Big Sur | 3.8, 3.9 | 8.1.2 |arm | -| +------------------------------+--------------------------------+-----------------------+ -| | 3.6, 3.7, 3.8, 3.9 | 8.1.2 |x86-64 | -+----------------------------------+------------------------------+--------------------------------+-----------------------+ -| macOS 10.15 Catalina | 3.6, 3.7, 3.8, 3.9 | 8.0.1 |x86-64 | -| +------------------------------+--------------------------------+ + -| | 3.5 | 7.2.0 | | -+----------------------------------+------------------------------+--------------------------------+-----------------------+ -| macOS 10.14 Mojave | 3.5, 3.6, 3.7, 3.8 | 7.2.0 |x86-64 | -| +------------------------------+--------------------------------+ + -| | 2.7 | 6.0.0 | | -| +------------------------------+--------------------------------+ + -| | 3.4 | 5.4.1 | | -+----------------------------------+------------------------------+--------------------------------+-----------------------+ -| macOS 10.13 High Sierra | 2.7, 3.4, 3.5, 3.6 | 4.2.1 |x86-64 | -+----------------------------------+------------------------------+--------------------------------+-----------------------+ -| 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.4, 3.5, 3.6, 3.7 | 5.4.1 |x86-64 | -| +------------------------------+--------------------------------+ + -| | 3.3 | 4.1.0 | | -+----------------------------------+------------------------------+--------------------------------+-----------------------+ -| 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 (Precise) | 2.6, 3.2, 3.3, 3.4, 3.5 | 3.4.1 |x86,x86-64 | -| | PyPy5.3.1, PyPy3 v2.4.0 | | | -| +------------------------------+--------------------------------+-----------------------+ -| | 2.7 | 4.3.0 |x86-64 | -| +------------------------------+--------------------------------+-----------------------+ -| | 2.7, 3.2 | 3.4.1 |ppc | -+----------------------------------+------------------------------+--------------------------------+-----------------------+ -| Ubuntu Linux 10.04 LTS (Lucid) | 2.6 | 2.3.0 |x86,x86-64 | -+----------------------------------+------------------------------+--------------------------------+-----------------------+ -| Debian 8.2 Jessie | 2.7, 3.4 | 3.1.0 |x86-64 | -+----------------------------------+------------------------------+--------------------------------+-----------------------+ -| Raspbian Jessie | 2.7, 3.4 | 3.1.0 |arm | -+----------------------------------+------------------------------+--------------------------------+-----------------------+ -| Raspbian Stretch | 2.7, 3.5 | 4.0.0 |arm | -+----------------------------------+------------------------------+--------------------------------+-----------------------+ -| Gentoo Linux | 2.7, 3.2 | 2.1.0 |x86-64 | -+----------------------------------+------------------------------+--------------------------------+-----------------------+ -| FreeBSD 11.1 | 2.7, 3.4, 3.5, 3.6 | 4.3.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 10 | 3.7 | 7.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 Professional | 3.7 | 7.0.0 |x86,x86-64 | -+----------------------------------+------------------------------+--------------------------------+-----------------------+ -| Windows Server 2008 R2 Enterprise| 3.3 | |x86-64 | -+----------------------------------+------------------------------+--------------------------------+-----------------------+ ++----------------------------------+---------------------------+------------------+--------------+ +| Operating system | | Tested Python | | Latest tested | | Tested | +| | | versions | | Pillow version | | processors | ++==================================+===========================+==================+==============+ +| macOS 11.0 Big Sur | 3.7, 3.8, 3.9 | 8.2.0 |arm | +| +---------------------------+------------------+--------------+ +| | 3.6, 3.7, 3.8, 3.9 | 8.2.0 |x86-64 | ++----------------------------------+---------------------------+------------------+--------------+ +| macOS 10.15 Catalina | 3.6, 3.7, 3.8, 3.9 | 8.0.1 |x86-64 | +| +---------------------------+------------------+ | +| | 3.5 | 7.2.0 | | ++----------------------------------+---------------------------+------------------+--------------+ +| macOS 10.14 Mojave | 3.5, 3.6, 3.7, 3.8 | 7.2.0 |x86-64 | +| +---------------------------+------------------+ | +| | 2.7 | 6.0.0 | | +| +---------------------------+------------------+ | +| | 3.4 | 5.4.1 | | ++----------------------------------+---------------------------+------------------+--------------+ +| macOS 10.13 High Sierra | 2.7, 3.4, 3.5, 3.6 | 4.2.1 |x86-64 | ++----------------------------------+---------------------------+------------------+--------------+ +| 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.4, 3.5, 3.6, 3.7 | 5.4.1 |x86-64 | +| +---------------------------+------------------+ | +| | 3.3 | 4.1.0 | | ++----------------------------------+---------------------------+------------------+--------------+ +| 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 (Precise) | | 2.6, 3.2, 3.3, 3.4, 3.5 | 3.4.1 |x86,x86-64 | +| | | PyPy5.3.1, PyPy3 v2.4.0 | | | +| +---------------------------+------------------+--------------+ +| | 2.7 | 4.3.0 |x86-64 | +| +---------------------------+------------------+--------------+ +| | 2.7, 3.2 | 3.4.1 |ppc | ++----------------------------------+---------------------------+------------------+--------------+ +| Ubuntu Linux 10.04 LTS (Lucid) | 2.6 | 2.3.0 |x86,x86-64 | ++----------------------------------+---------------------------+------------------+--------------+ +| Debian 8.2 Jessie | 2.7, 3.4 | 3.1.0 |x86-64 | ++----------------------------------+---------------------------+------------------+--------------+ +| Raspbian Jessie | 2.7, 3.4 | 3.1.0 |arm | ++----------------------------------+---------------------------+------------------+--------------+ +| Raspbian Stretch | 2.7, 3.5 | 4.0.0 |arm | ++----------------------------------+---------------------------+------------------+--------------+ +| Raspberry Pi OS | 3.6, 3.7, 3.8, 3.9 | 8.2.0 |arm | +| +---------------------------+------------------+ | +| | 2.7 | 6.2.2 | | ++----------------------------------+---------------------------+------------------+--------------+ +| Gentoo Linux | 2.7, 3.2 | 2.1.0 |x86-64 | ++----------------------------------+---------------------------+------------------+--------------+ +| FreeBSD 11.1 | 2.7, 3.4, 3.5, 3.6 | 4.3.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 10 | 3.7 | 7.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 Professional | 3.7 | 7.0.0 |x86,x86-64 | ++----------------------------------+---------------------------+------------------+--------------+ +| Windows Server 2008 R2 Enterprise| 3.3 | |x86-64 | ++----------------------------------+---------------------------+------------------+--------------+ Old Versions ------------ diff --git a/docs/reference/ImageDraw.rst b/docs/reference/ImageDraw.rst index 37fb5f726..46e1595c2 100644 --- a/docs/reference/ImageDraw.rst +++ b/docs/reference/ImageDraw.rst @@ -18,6 +18,7 @@ Example: Draw a gray cross over an image .. code-block:: python + import sys from PIL import Image, ImageDraw with Image.open("hopper.jpg") as im: diff --git a/docs/reference/ImageOps.rst b/docs/reference/ImageOps.rst index 9a16d6625..d1c43cf60 100644 --- a/docs/reference/ImageOps.rst +++ b/docs/reference/ImageOps.rst @@ -12,6 +12,7 @@ only work on L and RGB images. .. autofunction:: autocontrast .. autofunction:: colorize +.. autofunction:: contain .. autofunction:: pad .. autofunction:: crop .. autofunction:: scale diff --git a/docs/reference/ImageShow.rst b/docs/reference/ImageShow.rst index f1fbd90ce..e4d9805ab 100644 --- a/docs/reference/ImageShow.rst +++ b/docs/reference/ImageShow.rst @@ -18,6 +18,7 @@ All default viewers convert the image to be shown to PNG format. The following viewers may be registered on Unix-based systems, if the given command is found: .. autoclass:: PIL.ImageShow.DisplayViewer + .. autoclass:: PIL.ImageShow.GmDisplayViewer .. autoclass:: PIL.ImageShow.EogViewer .. autoclass:: PIL.ImageShow.XVViewer diff --git a/docs/releasenotes/8.0.0.rst b/docs/releasenotes/8.0.0.rst index 28dc8324d..2ff9b3799 100644 --- a/docs/releasenotes/8.0.0.rst +++ b/docs/releasenotes/8.0.0.rst @@ -78,7 +78,7 @@ Added a new ``formats`` parameter to :py:func:`.Image.open`: * A list or tuple of formats to attempt to load the file in. This can be used to restrict the set of formats checked. Pass ``None`` to try all supported formats. You can print the set of - available formats by running ``python -m PIL`` or using + available formats by running ``python3 -m PIL`` or using the :py:func:`PIL.features.pilinfo` function. ImageOps.autocontrast: add mask parameter diff --git a/docs/releasenotes/8.2.0.rst b/docs/releasenotes/8.2.0.rst index 3ef05894d..912af3ad2 100644 --- a/docs/releasenotes/8.2.0.rst +++ b/docs/releasenotes/8.2.0.rst @@ -4,12 +4,6 @@ Deprecations ============ -Tk/Tcl 8.4 -^^^^^^^^^^ - -Support for Tk/Tcl 8.4 is deprecated and will be removed in Pillow 10.0.0 (2023-01-02), -when Tk/Tcl 8.5 will be the minimum supported. - Categories ^^^^^^^^^^ @@ -20,6 +14,12 @@ along with the related ``Image.NORMAL``, ``Image.SEQUENCE`` and To determine if an image has multiple frames or not, ``getattr(im, "is_animated", False)`` can be used instead. +Tk/Tcl 8.4 +^^^^^^^^^^ + +Support for Tk/Tcl 8.4 is deprecated and will be removed in Pillow 10.0.0 (2023-01-02), +when Tk/Tcl 8.5 will be the minimum supported. + API Changes =========== @@ -45,9 +45,31 @@ This is now consistent with other IFDs, and must be accessed through These changes only affect :py:meth:`~PIL.Image.Image.getexif`, introduced in Pillow 6.0. The older ``_getexif()`` methods are unaffected. +Image._MODEINFO +^^^^^^^^^^^^^^^ + +This internal dictionary had been deprecated by a comment since PIL, and is now +removed. Instead, ``Image.getmodebase()``, ``Image.getmodetype()``, +``Image.getmodebandnames()``, ``Image.getmodebands()`` or ``ImageMode.getmode()`` +can be used. + API Additions ============= +getxmp() for JPEG images +^^^^^^^^^^^^^^^^^^^^^^^^ + +A new method has been added to return +`XMP data `_ for JPEG +images. It reads the XML data into a dictionary of names and values. + +For example:: + + >>> from PIL import Image + >>> with Image.open("Tests/images/xmp_test.jpg") as im: + >>> print(im.getxmp()) + {'RDF': {}, 'Description': {'Version': '10.4', 'ProcessVersion': '10.0', ...}, ...} + ImageDraw.rounded_rectangle ^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -63,6 +85,26 @@ create a circle, but not any other ellipse. draw = ImageDraw.Draw(im) draw.rounded_rectangle(xy=(10, 20, 190, 180), radius=30, fill="red") +ImageOps.autocontrast: preserve_tone +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The default behaviour of :py:meth:`~PIL.ImageOps.autocontrast` is to normalize +separate histograms for each color channel, changing the tone of the image. The new +``preserve_tone`` argument keeps the tone unchanged by using one luminance histogram +for all channels. + +ImageShow.GmDisplayViewer +^^^^^^^^^^^^^^^^^^^^^^^^^ + +If GraphicsMagick is present, this new :py:class:`PIL.ImageShow.Viewer` subclass will +be registered. It uses GraphicsMagick_, an ImageMagick_ fork, to display images. + +The GraphicsMagick based viewer has a lower priority than its ImageMagick +counterpart. Thus, if both ImageMagick and GraphicsMagick are installed, +``im.show()`` and :py:func:`.ImageShow.show()` prefer the viewer based on +ImageMagick, i.e the behaviour stays the same for Pillow users having +ImageMagick installed. + ImageShow.IPythonViewer ^^^^^^^^^^^^^^^^^^^^^^^ @@ -83,16 +125,106 @@ be specified through a keyword argument:: im.save("out.tif", icc_profile=...) + Security ======== -TODO +These were all found with `OSS-Fuzz`_. + +:cve:`CVE-2021-25287`, :cve:`CVE-2021-25288`: Fix OOB read in Jpeg2KDecode +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +* For J2k images with multiple bands, it's legal to have different widths for each band, + e.g. 1 byte for ``L``, 4 bytes for ``A``. +* This dates to Pillow 2.4.0. + +:cve:`CVE-2021-28675`: Fix DOS in PsdImagePlugin +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +* :py:class:`.PsdImagePlugin.PsdImageFile` did not sanity check the number of input + layers with regard to the size of the data block, this could lead to a + denial-of-service on :py:meth:`~PIL.Image.open` prior to + :py:meth:`~PIL.Image.Image.load`. +* This dates to the PIL fork. + +:cve:`CVE-2021-28676`: Fix FLI DOS +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +* ``FliDecode.c`` did not properly check that the block advance was non-zero, + potentially leading to an infinite loop on load. +* This dates to the PIL fork. + +:cve:`CVE-2021-28677`: Fix EPS DOS on _open +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +* The readline used in EPS has to deal with any combination of ``\r`` and ``\n`` as line + endings. It accidentally used a quadratic method of accumulating lines while looking + for a line ending. +* A malicious EPS file could use this to perform a denial-of-service of Pillow in the + open phase, before an image was accepted for opening. +* This dates to the PIL fork. + +:cve:`CVE-2021-28678`: Fix BLP DOS +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +* ``BlpImagePlugin`` did not properly check that reads after jumping to file offsets + returned data. This could lead to a denial-of-service where the decoder could be run a + large number of times on empty data. +* This dates to Pillow 5.1.0. + +Fix memory DOS in ImageFont +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +* A corrupt or specially crafted TTF font could have font metrics that lead to + unreasonably large sizes when rendering text in font. ``ImageFont.py`` did not check + the image size before allocating memory for it. +* This dates to the PIL fork. Other Changes ============= +GIF writer uses LZW encoding +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +GIF files are now written using LZW encoding, which will generate smaller files, +typically about 70% of the size generated by the older encoder. + +The pixel data is encoded using the format specified in the `CompuServe GIF standard +`_. + +The older encoder used a variant of run-length encoding that was compatible but less +efficient. + +GraphicsMagick +^^^^^^^^^^^^^^ + +The test suite can now be run on systems which have GraphicsMagick_ but not +ImageMagick_ installed. If both are installed, the tests prefer ImageMagick. + +Libraqm and FriBiDi linking +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The way the libraqm dependency for complex text scripts is linked has been changed: + +Source builds will now link against the system version of libraqm at build time +rather than at runtime by default. + +Binary wheels now include a statically linked modified version of libraqm that +links against FriBiDi at runtime instead. This change is intended to address +issues with the previous implementation on some platforms. These are created +by building Pillow with the new build flags ``--vendor-raqm --vendor-fribidi``. + +Windows users will now need to install ``fribidi.dll`` (or ``fribidi-0.dll``) only, +``libraqm.dll`` is no longer used. + +See :doc:`installation documentation<../installation>` for more information. + PyQt6 ^^^^^ Support has been added for PyQt6. If it is installed, it will be used instead of PySide6, PyQt5 or PySide2. + +.. _GraphicsMagick: http://www.graphicsmagick.org/ +.. _ImageMagick: https://imagemagick.org/ +.. _OSS-Fuzz: https://github.com/google/oss-fuzz diff --git a/docs/releasenotes/8.3.0.rst b/docs/releasenotes/8.3.0.rst new file mode 100644 index 000000000..eb4883deb --- /dev/null +++ b/docs/releasenotes/8.3.0.rst @@ -0,0 +1,113 @@ +8.3.0 +----- + +Deprecations +============ + +JpegImagePlugin.convert_dict_qtables +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +JPEG ``quantization`` is now automatically converted, but still returned as a +dictionary. The :py:attr:`~PIL.JpegImagePlugin.convert_dict_qtables` method no longer +performs any operations on the data given to it, has been deprecated and will be +removed in Pillow 10.0.0 (2023-01-02). + +API Changes +=========== + +Changed WebP default "method" value when saving +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Previously, it was 0, for the best speed. The default has now been changed to 4, to +match WebP's default, for higher quality with still some speed optimisation. + +Default resampling filter for special image modes +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Pillow 7.0 changed the default resampling filter to ``Image.BICUBIC``. However, as this +is not supported yet for images with a custom number of bits, the default filter for +those modes has been reverted to ``Image.NEAREST``. + +ImageMorph incorrect mode errors +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +For ``apply()``, ``match()`` and ``get_on_pixels()``, if the image mode is not L, an +:py:exc:`Exception` was thrown. This has now been changed to a :py:exc:`ValueError`. + +getxmp() +^^^^^^^^ + +`XMP data `_ can now be +returned for PNG and TIFF images, through ``getxmp()`` for each format. + +The returned dictionary will start from the base of the XML, meaning that the top level +should contain an "xmpmeta" key. JPEG's ``getxmp()`` method has also been updated to +this structure. + +TIFF getexif() +^^^^^^^^^^^^^^ + +TIFF :py:attr:`~PIL.TiffImagePlugin.TiffImageFile.tag_v2` data can now be accessed +through :py:meth:`~PIL.Image.Image.getexif`. This also provides access to the GPS and +EXIF IFDs, through ``im.getexif().get_ifd(0x8825)`` and +``im.getexif().get_ifd(0x8769)`` respectively. + +API Additions +============= + +ImageOps.contain +^^^^^^^^^^^^^^^^ + +Returns a resized version of the image, set to the maximum width and height within +``size``, while maintaining the original aspect ratio. + +To compare it to other ImageOps methods: + +- :py:meth:`~PIL.ImageOps.fit` expands an image until is fills ``size``, cropping the + parts of the image that do not fit. +- :py:meth:`~PIL.ImageOps.pad` expands an image to fill ``size``, without cropping, but + instead filling the extra space with ``color``. +- :py:meth:`~PIL.ImageOps.contain` is similar to :py:meth:`~PIL.ImageOps.pad`, but it + does not fill the extra space. Instead, the original aspect ratio is maintained. So + unlike the other two methods, it is not guaranteed to return an image of ``size``. + +ICO saving: bitmap_format argument +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +By default, Pillow saves ICO files in the PNG format. They can now also be saved in BMP +format, through the new ``bitmap_format`` argument:: + + im.save("out.ico", bitmap_format="bmp") + +Security +======== + +Buffer overflow +^^^^^^^^^^^^^^^ + +This release addresses :cve:`CVE-2021-34552`. PIL since 1.1.4 and Pillow since 1.0 +allowed parameters passed into a convert function to trigger buffer overflow in +Convert.c. + +Parsing XML +^^^^^^^^^^^ + +Pillow previously parsed XMP data using Python's ``xml`` module. However, this module +is not secure. + +- :py:meth:`~PIL.Image.Image.getexif` has used ``xml`` to potentially retrieve + orientation data since Pillow 7.2.0. It has been refactored to use ``re`` instead. +- :py:meth:`~PIL.JpegImagePlugin.JpegImageFile.getxmp` was added in Pillow 8.2.0. It + will now use ``defusedxml`` instead. If the dependency is not present, an empty + dictionary will be returned and a warning raised. + +Other Changes +============= + +Added DDS BC5 reading and uncompressed saving +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Support has been added to read the BC5 format of DDS images, whether UNORM, SNORM or +TYPELESS. + +Support has also been added to write the uncompressed format of DDS images. diff --git a/docs/releasenotes/index.rst b/docs/releasenotes/index.rst index 117738675..3e23e43d3 100644 --- a/docs/releasenotes/index.rst +++ b/docs/releasenotes/index.rst @@ -14,6 +14,7 @@ expected to be backported to earlier versions. .. toctree:: :maxdepth: 2 + 8.3.0 8.2.0 8.1.2 8.1.1 diff --git a/docs/resources/css/dark.css b/docs/resources/css/dark.css index cc213d674..8866c07ea 100644 --- a/docs/resources/css/dark.css +++ b/docs/resources/css/dark.css @@ -1275,7 +1275,7 @@ .wy-body-for-nav { background-image: initial; - background-color: rgb(26, 28, 29); + background-color: rgb(24, 26, 27); } .wy-nav-side { @@ -1333,11 +1333,6 @@ color: rgb(152, 143, 129); } - .wy-body-for-nav { - background-image: initial; - background-color: rgb(26, 28, 29); - } - @media screen and (min-width: 1100px) { .wy-nav-content-wrap { background-image: initial; diff --git a/docs/resources/css/styles.css b/docs/resources/css/styles.css new file mode 100644 index 000000000..111f84085 --- /dev/null +++ b/docs/resources/css/styles.css @@ -0,0 +1,8 @@ +th p { + margin-bottom: 0; +} + +.rst-content tr .line-block { + font-size: 1rem; + margin-bottom: 0; +} diff --git a/requirements.txt b/requirements.txt index 4b534ae53..38011fd39 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,6 +2,7 @@ black check-manifest coverage +defusedxml markdown2 olefile packaging @@ -10,6 +11,8 @@ pytest pytest-cov pytest-timeout sphinx>=2.4 +sphinx-copybutton sphinx-issues sphinx-removed-in sphinx-rtd-theme +sphinxext-opengraph diff --git a/selftest.py b/selftest.py index 7e08d183b..8d77cc5a9 100755 --- a/selftest.py +++ b/selftest.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 # minimal sanity check import sys diff --git a/setup.py b/setup.py index 6c4840c75..6dc4e1b77 100755 --- a/setup.py +++ b/setup.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 # > pyroma . # ------------------------------ # Checking . @@ -39,7 +39,7 @@ TIFF_ROOT = None ZLIB_ROOT = None FUZZING_BUILD = "LIB_FUZZING_ENGINE" in os.environ -if sys.platform == "win32" and sys.version_info >= (3, 10): +if sys.platform == "win32" and sys.version_info >= (3, 11): import atexit atexit.register( @@ -810,9 +810,11 @@ class pil_build_ext(build_ext): if feature.tiff: libs.append(feature.tiff) defs.append(("HAVE_LIBTIFF", None)) - # FIXME the following define should be detected automatically - # based on system libtiff, see #4237 - if PLATFORM_MINGW: + if sys.platform == "win32": + # This define needs to be defined if-and-only-if it was defined + # when compiling LibTIFF. LibTIFF doesn't expose it in `tiffconf.h`, + # so we have to guess; by default it is defined in all Windows builds. + # See #4237, #5243, #5359 for more information. defs.append(("USE_WIN32_FILEIO", None)) if feature.xcb: libs.append(feature.xcb) @@ -990,6 +992,7 @@ try: "index.html", "Changelog": "https://github.com/python-pillow/Pillow/blob/master/" "CHANGES.rst", + "Twitter": "https://twitter.com/PythonPillow", }, classifiers=[ "Development Status :: 6 - Mature", @@ -999,6 +1002,7 @@ try: "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", diff --git a/src/PIL/BlpImagePlugin.py b/src/PIL/BlpImagePlugin.py index 3b9c1baea..f62b1bebe 100644 --- a/src/PIL/BlpImagePlugin.py +++ b/src/PIL/BlpImagePlugin.py @@ -286,33 +286,36 @@ class _BLPBaseDecoder(ImageFile.PyDecoder): raise OSError("Truncated Blp file") from e return 0, 0 + def _safe_read(self, length): + return ImageFile._safe_read(self.fd, length) + def _read_palette(self): ret = [] for i in range(256): try: - b, g, r, a = struct.unpack("<4B", self.fd.read(4)) + b, g, r, a = struct.unpack("<4B", self._safe_read(4)) except struct.error: break ret.append((b, g, r, a)) return ret def _read_blp_header(self): - (self._blp_compression,) = struct.unpack("= 52: for idx, mask in enumerate( diff --git a/src/PIL/DdsImagePlugin.py b/src/PIL/DdsImagePlugin.py index df2d0060c..260924fca 100644 --- a/src/PIL/DdsImagePlugin.py +++ b/src/PIL/DdsImagePlugin.py @@ -14,6 +14,7 @@ import struct from io import BytesIO from . import Image, ImageFile +from ._binary import o32le as o32 # Magic ("DDS ") DDS_MAGIC = 0x20534444 @@ -97,6 +98,9 @@ DXT5_FOURCC = 0x35545844 DXGI_FORMAT_R8G8B8A8_TYPELESS = 27 DXGI_FORMAT_R8G8B8A8_UNORM = 28 DXGI_FORMAT_R8G8B8A8_UNORM_SRGB = 29 +DXGI_FORMAT_BC5_TYPELESS = 82 +DXGI_FORMAT_BC5_UNORM = 83 +DXGI_FORMAT_BC5_SNORM = 84 DXGI_FORMAT_BC7_TYPELESS = 97 DXGI_FORMAT_BC7_UNORM = 98 DXGI_FORMAT_BC7_UNORM_SRGB = 99 @@ -127,15 +131,17 @@ class DdsImageFile(ImageFile.ImageFile): fourcc = header.read(4) (bitcount,) = struct.unpack("i", sum(entry["size"] for entry in entries))) - if fp_only: - with open(filename, "rb") as f: - fp.write(f.read()) + # TOC + fp.write(b"TOC ") + fp.write(struct.pack(">i", HEADERSIZE + len(entries) * HEADERSIZE)) + for entry in entries: + fp.write(entry["type"]) + fp.write(struct.pack(">i", HEADERSIZE + entry["size"])) + + # Data + for entry in entries: + fp.write(entry["type"]) + fp.write(struct.pack(">i", HEADERSIZE + entry["size"])) + fp.write(entry["stream"]) + + if hasattr(fp, "flush"): + fp.flush() -Image.register_open(IcnsImageFile.format, IcnsImageFile, lambda x: x[:4] == b"icns") +def _accept(prefix): + return prefix[:4] == MAGIC + + +Image.register_open(IcnsImageFile.format, IcnsImageFile, _accept) Image.register_extension(IcnsImageFile.format, ".icns") -if sys.platform == "darwin": - Image.register_save(IcnsImageFile.format, _save) - - Image.register_mime(IcnsImageFile.format, "image/icns") - +Image.register_save(IcnsImageFile.format, _save) +Image.register_mime(IcnsImageFile.format, "image/icns") if __name__ == "__main__": - if len(sys.argv) < 2: - print("Syntax: python IcnsImagePlugin.py [file]") + print("Syntax: python3 IcnsImagePlugin.py [file]") sys.exit() with open(sys.argv[1], "rb") as fp: diff --git a/src/PIL/IcoImagePlugin.py b/src/PIL/IcoImagePlugin.py index 5634bf8e9..ffb1e873d 100644 --- a/src/PIL/IcoImagePlugin.py +++ b/src/PIL/IcoImagePlugin.py @@ -30,6 +30,7 @@ from math import ceil, log from . import BmpImagePlugin, Image, ImageFile, PngImagePlugin from ._binary import i16le as i16 from ._binary import i32le as i32 +from ._binary import o32le as o32 # # -------------------------------------------------------------------- @@ -53,6 +54,7 @@ def _save(im, fp, filename): sizes = list(sizes) fp.write(struct.pack(". diff --git a/src/PIL/Image.py b/src/PIL/Image.py index be5efb953..9debddeec 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -31,14 +31,19 @@ import logging import math import numbers import os +import re import struct import sys import tempfile import warnings -import xml.etree.ElementTree from collections.abc import Callable, MutableMapping from pathlib import Path +try: + import defusedxml.ElementTree as ElementTree +except ImportError: + ElementTree = None + # VERSION was removed in Pillow 6.0.0. # PILLOW_VERSION is deprecated and will be removed in a future release. # Use __version__ instead. @@ -223,28 +228,7 @@ DECODERS = {} ENCODERS = {} # -------------------------------------------------------------------- -# Modes supported by this version - -_MODEINFO = { - # NOTE: this table will be removed in future versions. use - # getmode* functions or ImageMode descriptors instead. - # official modes - "1": ("L", "L", ("1",)), - "L": ("L", "L", ("L",)), - "I": ("L", "I", ("I",)), - "F": ("L", "F", ("F",)), - "P": ("P", "L", ("P",)), - "RGB": ("RGB", "L", ("R", "G", "B")), - "RGBX": ("RGB", "L", ("R", "G", "B", "X")), - "RGBA": ("RGB", "L", ("R", "G", "B", "A")), - "CMYK": ("RGB", "L", ("C", "M", "Y", "K")), - "YCbCr": ("RGB", "L", ("Y", "Cb", "Cr")), - "LAB": ("RGB", "L", ("L", "A", "B")), - "HSV": ("RGB", "L", ("H", "S", "V")), - # Experimental modes include I;16, I;16L, I;16B, RGBa, BGR;15, and - # BGR;24. Use these modes only if you know exactly what you're - # doing... -} +# Modes if sys.byteorder == "little": _ENDIAN = "<" @@ -290,7 +274,7 @@ def _conv_type_shape(im): return (im.size[1], im.size[0], extra), typ -MODES = sorted(_MODEINFO) +MODES = ["1", "CMYK", "F", "HSV", "I", "L", "LAB", "P", "RGB", "RGBA", "RGBX", "YCbCr"] # raw modes that may be memory mapped. NOTE: if you change this, you # may have to modify the stride calculation in map.c too! @@ -697,9 +681,10 @@ class Image: raise ValueError("Could not save to PNG for display") from e return b.getvalue() - @property - def __array_interface__(self): + def __array__(self): # numpy array interface support + import numpy as np + new = {} shape, typestr = _conv_type_shape(self) new["shape"] = shape @@ -711,7 +696,11 @@ class Image: new["data"] = self.tobytes("raw", "L") else: new["data"] = self.tobytes() - return new + + class ArrayData: + __array_interface__ = new + + return np.array(ArrayData()) def __getstate__(self): return [self.info, self.mode, self.size, self.getpalette(), self.tobytes()] @@ -846,10 +835,10 @@ class Image: arr = bytes( value for (index, value) in enumerate(arr) if index % 4 != 3 ) - self.im.putpalette(mode, arr) + palette_length = self.im.putpalette(mode, arr) self.palette.dirty = 0 self.palette.rawmode = None - if "transparency" in self.info: + if "transparency" in self.info and mode in ("RGBA", "LA", "PA"): if isinstance(self.info["transparency"], int): self.im.putpalettealpha(self.info["transparency"], 0) else: @@ -857,6 +846,7 @@ class Image: self.palette.mode = "RGBA" else: self.palette.mode = "RGB" + self.palette.palette = self.im.getpalette()[: palette_length * 3] if self.im: if cffi and USE_CFFI_ACCESS: @@ -992,21 +982,28 @@ class Image: if self.mode == "P": trns_im.putpalette(self.palette) if isinstance(t, tuple): + err = "Couldn't allocate a palette color for transparency" try: - t = trns_im.palette.getcolor(t) - except Exception as e: - raise ValueError( - "Couldn't allocate a palette color for transparency" - ) from e - trns_im.putpixel((0, 0), t) - - if mode in ("L", "RGB"): - trns_im = trns_im.convert(mode) + t = trns_im.palette.getcolor(t, self) + except ValueError as e: + if str(e) == "cannot allocate more than 256 colors": + # If all 256 colors are in use, + # then there is no need for transparency + t = None + else: + raise ValueError(err) from e + if t is None: + trns = None else: - # can't just retrieve the palette number, got to do it - # after quantization. - trns_im = trns_im.convert("RGB") - trns = trns_im.getpixel((0, 0)) + trns_im.putpixel((0, 0), t) + + if mode in ("L", "RGB"): + trns_im = trns_im.convert(mode) + else: + # can't just retrieve the palette number, got to do it + # after quantization. + trns_im = trns_im.convert("RGB") + trns = trns_im.getpixel((0, 0)) elif self.mode == "P" and mode == "RGBA": t = self.info["transparency"] @@ -1024,14 +1021,14 @@ class Image: new = self._new(im) from . import ImagePalette - new.palette = ImagePalette.raw("RGB", new.im.getpalette("RGB")) + new.palette = ImagePalette.ImagePalette("RGB", new.im.getpalette("RGB")) if delete_trns: # This could possibly happen if we requantize to fewer colors. # The transparency would be totally off in that case. del new.info["transparency"] if trns is not None: try: - new.info["transparency"] = new.palette.getcolor(trns) + new.info["transparency"] = new.palette.getcolor(trns, new) except Exception: # if we can't make a transparent color, don't leave the old # transparency hanging around to mess us up. @@ -1054,16 +1051,25 @@ class Image: raise ValueError("illegal conversion") from e new_im = self._new(im) + if mode == "P" and palette != ADAPTIVE: + from . import ImagePalette + + new_im.palette = ImagePalette.ImagePalette("RGB", list(range(256)) * 3) if delete_trns: # crash fail if we leave a bytes transparency in an rgb/l mode. del new_im.info["transparency"] if trns is not None: if new_im.mode == "P": try: - new_im.info["transparency"] = new_im.palette.getcolor(trns) - except Exception: + new_im.info["transparency"] = new_im.palette.getcolor(trns, new_im) + except ValueError as e: del new_im.info["transparency"] - warnings.warn("Couldn't allocate palette entry for transparency") + if str(e) != "cannot allocate more than 256 colors": + # If all 256 colors are in use, + # then there is no need for transparency + warnings.warn( + "Couldn't allocate palette entry for transparency" + ) else: new_im.info["transparency"] = trns return new_im @@ -1333,30 +1339,60 @@ class Image: return tuple(extrema) return self.im.getextrema() + def _getxmp(self, xmp_tags): + def get_name(tag): + return tag.split("}")[1] + + def get_value(element): + value = {get_name(k): v for k, v in element.attrib.items()} + children = list(element) + if children: + for child in children: + name = get_name(child.tag) + child_value = get_value(child) + if name in value: + if not isinstance(value[name], list): + value[name] = [value[name]] + value[name].append(child_value) + else: + value[name] = child_value + elif value: + if element.text: + value["text"] = element.text + else: + return element.text + return value + + if ElementTree is None: + warnings.warn("XMP data cannot be read without defusedxml dependency") + return {} + else: + root = ElementTree.fromstring(xmp_tags) + return {get_name(root.tag): get_value(root)} + def getexif(self): if self._exif is None: self._exif = Exif() exif_info = self.info.get("exif") - if exif_info is None and "Raw profile type exif" in self.info: - exif_info = bytes.fromhex( - "".join(self.info["Raw profile type exif"].split("\n")[3:]) - ) - self._exif.load(exif_info) + if exif_info is None: + if "Raw profile type exif" in self.info: + exif_info = bytes.fromhex( + "".join(self.info["Raw profile type exif"].split("\n")[3:]) + ) + elif hasattr(self, "tag_v2"): + self._exif.endian = self.tag_v2._endian + self._exif.load_from_fp(self.fp, self.tag_v2._offset) + if exif_info is not None: + self._exif.load(exif_info) # XMP tags if 0x0112 not in self._exif: xmp_tags = self.info.get("XML:com.adobe.xmp") if xmp_tags: - root = xml.etree.ElementTree.fromstring(xmp_tags) - for elem in root.iter(): - if elem.tag.endswith("}Description"): - orientation = elem.attrib.get( - "{http://ns.adobe.com/tiff/1.0/}Orientation" - ) - if orientation: - self._exif[0x0112] = int(orientation) - break + match = re.search(r'tiff:Orientation="([0-9])"', xmp_tags) + if match: + self._exif[0x0112] = int(match[1]) return self._exif @@ -1715,7 +1751,7 @@ class Image: Attaches a palette to this image. The image must be a "P", "PA", "L" or "LA" image. - The palette sequence must contain either 768 integer values, or 1024 + The palette sequence must contain at most 768 integer values, or 1024 integer values if alpha is included. Each group of values represents the red, green, blue (and alpha if included) values for the corresponding pixel index. Instead of an integer sequence, you can use @@ -1728,7 +1764,6 @@ class Image: if self.mode not in ("L", "LA", "P", "PA"): raise ValueError("illegal image mode") - self.load() if isinstance(data, ImagePalette.ImagePalette): palette = ImagePalette.raw(data.rawmode, data.palette) else: @@ -1775,7 +1810,7 @@ class Image: and len(value) in [3, 4] ): # RGB or RGBA value for a P image - value = self.palette.getcolor(value) + value = self.palette.getcolor(value, self) return self.im.putpixel(xy, value) def remap_palette(self, dest_map, source_palette=None): @@ -1796,6 +1831,7 @@ class Image: if source_palette is None: if self.mode == "P": + self.load() real_source_palette = self.im.getpalette("RGB")[:768] else: # L-mode real_source_palette = bytearray(i // 3 for i in range(768)) @@ -1833,23 +1869,19 @@ class Image: m_im = self.copy() m_im.mode = "P" - m_im.palette = ImagePalette.ImagePalette( - "RGB", palette=mapping_palette * 3, size=768 - ) + m_im.palette = ImagePalette.ImagePalette("RGB", palette=mapping_palette * 3) # 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.im.putpalette("RGB;L", m_im.palette.tobytes()) 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) - ) + m_im.palette = ImagePalette.ImagePalette("RGB", palette=palette_bytes) return m_im @@ -1870,7 +1902,7 @@ class Image: min(self.size[1], math.ceil(box[3] + support_y)), ) - def resize(self, size, resample=BICUBIC, box=None, reducing_gap=None): + def resize(self, size, resample=None, box=None, reducing_gap=None): """ Returns a resized copy of this image. @@ -1880,9 +1912,11 @@ class Image: one of :py:data:`PIL.Image.NEAREST`, :py:data:`PIL.Image.BOX`, :py:data:`PIL.Image.BILINEAR`, :py:data:`PIL.Image.HAMMING`, :py:data:`PIL.Image.BICUBIC` or :py:data:`PIL.Image.LANCZOS`. - Default filter is :py:data:`PIL.Image.BICUBIC`. - If the image has mode "1" or "P", it is - always set to :py:data:`PIL.Image.NEAREST`. + If the image has mode "1" or "P", it is always set to + :py:data:`PIL.Image.NEAREST`. + If the image mode specifies a number of bits, such as "I;16", then the + default filter is :py:data:`PIL.Image.NEAREST`. + Otherwise, the default filter is :py:data:`PIL.Image.BICUBIC`. See: :ref:`concept-filters`. :param box: An optional 4-tuple of floats providing the source image region to be scaled. @@ -1903,7 +1937,10 @@ class Image: :returns: An :py:class:`~PIL.Image.Image` object. """ - if resample not in (NEAREST, BILINEAR, BICUBIC, LANCZOS, BOX, HAMMING): + if resample is None: + type_special = ";" in self.mode + resample = NEAREST if type_special else BICUBIC + elif resample not in (NEAREST, BILINEAR, BICUBIC, LANCZOS, BOX, HAMMING): message = f"Unknown resampling filter ({resample})." filters = [ @@ -1938,7 +1975,7 @@ class Image: resample = NEAREST if self.mode in ["LA", "RGBA"] and resample != NEAREST: - im = self.convert(self.mode[:-1] + "a") + im = self.convert({"LA": "La", "RGBA": "RGBa"}[self.mode]) im = im.resize(size, resample, box) return im.convert(self.mode) @@ -1988,7 +2025,7 @@ class Image: return self.copy() if self.mode in ["LA", "RGBA"]: - im = self.convert(self.mode[:-1] + "a") + im = self.convert({"LA": "La", "RGBA": "RGBa"}[self.mode]) im = im.reduce(factor, box) return im.convert(self.mode) @@ -2151,6 +2188,11 @@ class Image: elif isinstance(fp, Path): filename = str(fp) open_fp = True + elif fp == sys.stdout: + try: + fp = sys.stdout.buffer + except AttributeError: + pass if not filename and hasattr(fp, "name") and isPath(fp.name): # only set the name for metadata purposes filename = fp.name @@ -2421,18 +2463,11 @@ class Image: :returns: An :py:class:`~PIL.Image.Image` object. """ - if self.mode == "LA" and resample != NEAREST: + if self.mode in ("LA", "RGBA") and resample != NEAREST: return ( - self.convert("La") + self.convert({"LA": "La", "RGBA": "RGBa"}[self.mode]) .transform(size, method, data, resample, fill, fillcolor) - .convert("LA") - ) - - if self.mode == "RGBA" and resample != NEAREST: - return ( - self.convert("RGBa") - .transform(size, method, data, resample, fill, fillcolor) - .convert("RGBA") + .convert(self.mode) ) if isinstance(method, ImageTransformHandler): @@ -2898,7 +2933,7 @@ def open(fp, mode="r", formats=None): :param formats: A list or tuple of formats to attempt to load the file in. This can be used to restrict the set of formats checked. Pass ``None`` to try all supported formats. You can print the set of - available formats by running ``python -m PIL`` or using + available formats by running ``python3 -m PIL`` or using the :py:func:`PIL.features.pilinfo` function. :returns: An :py:class:`~PIL.Image.Image` object. :exception FileNotFoundError: If the file cannot be found. @@ -3313,7 +3348,7 @@ atexit.register(core.clear_cache) class Exif(MutableMapping): - endian = "<" + endian = None def __init__(self): self._data = {} @@ -3348,6 +3383,12 @@ class Exif(MutableMapping): info.load(self.fp) return self._fixup_dict(info) + def _get_head(self): + if self.endian == "<": + return b"II\x2A\x00\x08\x00\x00\x00" + else: + return b"MM\x00\x2A\x00\x00\x00\x08" + def load(self, data): # Extract EXIF information. This is highly experimental, # and is likely to be replaced with something better in a future @@ -3360,8 +3401,8 @@ class Exif(MutableMapping): self._loaded_exif = data self._data.clear() self._ifds.clear() - self._info = None if not data: + self._info = None return if data.startswith(b"Exif\x00\x00"): @@ -3376,6 +3417,27 @@ class Exif(MutableMapping): self.fp.seek(self._info.next) self._info.load(self.fp) + def load_from_fp(self, fp, offset=None): + self._loaded_exif = None + self._data.clear() + self._ifds.clear() + + # process dictionary + from . import TiffImagePlugin + + self.fp = fp + if offset is not None: + self.head = self._get_head() + else: + self.head = self.fp.read(8) + self._info = TiffImagePlugin.ImageFileDirectory_v2(self.head) + if self.endian is None: + self.endian = self._info._endian + if offset is None: + offset = self._info.next + self.fp.seek(offset) + self._info.load(self.fp) + def _get_merged_dict(self): merged_dict = dict(self) @@ -3394,10 +3456,7 @@ class Exif(MutableMapping): def tobytes(self, offset=8): from . import TiffImagePlugin - if self.endian == "<": - head = b"II\x2A\x00\x08\x00\x00\x00" - else: - head = b"MM\x00\x2A\x00\x00\x00\x08" + head = self._get_head() ifd = TiffImagePlugin.ImageFileDirectory_v2(ifh=head) for tag, value in self.items(): if tag in [0x8769, 0x8225, 0x8825] and not isinstance(value, dict): diff --git a/src/PIL/ImageDraw.py b/src/PIL/ImageDraw.py index 8988e4233..aea0cc680 100644 --- a/src/PIL/ImageDraw.py +++ b/src/PIL/ImageDraw.py @@ -33,7 +33,7 @@ import math import numbers -from . import Image, ImageColor +from . import Image, ImageColor, ImageFont """ A simple 2D drawing interface for PIL images. @@ -70,6 +70,7 @@ class ImageDraw: self.palette = im.palette else: self.palette = None + self._image = im self.im = im.im self.draw = Image.core.draw(self.im, blend) self.mode = mode @@ -108,13 +109,13 @@ class ImageDraw: if isinstance(ink, str): ink = ImageColor.getcolor(ink, self.mode) if self.palette and not isinstance(ink, numbers.Number): - ink = self.palette.getcolor(ink) + ink = self.palette.getcolor(ink, self._image) ink = self.draw.draw_ink(ink) if fill is not None: if isinstance(fill, str): fill = ImageColor.getcolor(fill, self.mode) if self.palette and not isinstance(fill, numbers.Number): - fill = self.palette.getcolor(fill) + fill = self.palette.getcolor(fill, self._image) fill = self.draw.draw_ink(fill) return ink, fill @@ -282,6 +283,7 @@ class ImageDraw: # If the corners have no curve, that is a rectangle return self.rectangle(xy, fill, outline, width) + r = d // 2 ink, fill = self._getink(outline, fill) def draw_corners(pieslice): @@ -315,36 +317,28 @@ class ImageDraw: draw_corners(True) if full_x: - self.draw.draw_rectangle( - (x0, y0 + d / 2 + 1, x1, y1 - d / 2 - 1), fill, 1 - ) + self.draw.draw_rectangle((x0, y0 + r + 1, x1, y1 - r - 1), fill, 1) else: - self.draw.draw_rectangle( - (x0 + d / 2 + 1, y0, x1 - d / 2 - 1, y1), fill, 1 - ) + self.draw.draw_rectangle((x0 + r + 1, y0, x1 - r - 1, y1), fill, 1) if not full_x and not full_y: - self.draw.draw_rectangle( - (x0, y0 + d / 2 + 1, x0 + d / 2, y1 - d / 2 - 1), fill, 1 - ) - self.draw.draw_rectangle( - (x1 - d / 2, y0 + d / 2 + 1, x1, y1 - d / 2 - 1), fill, 1 - ) + self.draw.draw_rectangle((x0, y0 + r + 1, x0 + r, y1 - r - 1), fill, 1) + self.draw.draw_rectangle((x1 - r, y0 + r + 1, x1, y1 - r - 1), fill, 1) if ink is not None and ink != fill and width != 0: draw_corners(False) if not full_x: self.draw.draw_rectangle( - (x0 + d / 2 + 1, y0, x1 - d / 2 - 1, y0 + width - 1), ink, 1 + (x0 + r + 1, y0, x1 - r - 1, y0 + width - 1), ink, 1 ) self.draw.draw_rectangle( - (x0 + d / 2 + 1, y1 - width + 1, x1 - d / 2 - 1, y1), ink, 1 + (x0 + r + 1, y1 - width + 1, x1 - r - 1, y1), ink, 1 ) if not full_y: self.draw.draw_rectangle( - (x0, y0 + d / 2 + 1, x0 + width - 1, y1 - d / 2 - 1), ink, 1 + (x0, y0 + r + 1, x0 + width - 1, y1 - r - 1), ink, 1 ) self.draw.draw_rectangle( - (x1 - width + 1, y0 + d / 2 + 1, x1, y1 - d / 2 - 1), ink, 1 + (x1 - width + 1, y0 + r + 1, x1, y1 - r - 1), ink, 1 ) def _multiline_check(self, text): @@ -653,6 +647,8 @@ class ImageDraw: if font is None: font = self.getfont() + if not isinstance(font, ImageFont.FreeTypeFont): + raise ValueError("Only supported for TrueType fonts") mode = "RGBA" if embedded_color else self.fontmode bbox = font.getbbox( text, mode, direction, features, language, stroke_width, anchor diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py index f58de95bd..daf732de1 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -493,7 +493,7 @@ def _save(im, fp, tile, bufsize=0): # But, it would need at least the image size in most cases. RawEncode is # a tricky case. bufsize = max(MAXBLOCK, bufsize, im.size[0] * 4) # see RawEncode.c - if fp == sys.stdout: + if fp == sys.stdout or (hasattr(sys.stdout, "buffer") and fp == sys.stdout.buffer): fp.flush() return try: @@ -545,12 +545,18 @@ def _safe_read(fp, size): :param fp: File handle. Must implement a read method. :param size: Number of bytes to read. - :returns: A string containing up to size bytes of data. + :returns: A string containing size bytes of data. + + Raises an OSError if the file is truncated and the read cannot be completed + """ if size <= 0: return b"" if size <= SAFEBLOCK: - return fp.read(size) + data = fp.read(size) + if len(data) < size: + raise OSError("Truncated File Read") + return data data = [] while size > 0: block = fp.read(min(size, SAFEBLOCK)) @@ -558,6 +564,8 @@ def _safe_read(fp, size): break data.append(block) size -= len(block) + if sum(len(d) for d in data) < size: + raise OSError("Truncated File Read") return b"".join(data) diff --git a/src/PIL/ImageFilter.py b/src/PIL/ImageFilter.py index 6800bc3a0..d2ece3752 100644 --- a/src/PIL/ImageFilter.py +++ b/src/PIL/ImageFilter.py @@ -149,9 +149,11 @@ class ModeFilter(Filter): class GaussianBlur(MultibandFilter): - """Gaussian blur filter. + """Blurs the image with a sequence of extended box filters, which + approximates a Gaussian kernel. For details on accuracy see + - :param radius: Blur radius. + :param radius: Standard deviation of the Gaussian kernel. """ name = "GaussianBlur" diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index c48d89835..e99ca21b2 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -170,8 +170,10 @@ class FreeTypeFont: except ImportError: pass else: - freetype_version = parse_version(features.version_module("freetype2")) - if freetype_version < parse_version("2.8"): + freetype_version = features.version_module("freetype2") + if freetype_version is not None and parse_version( + freetype_version + ) < parse_version("2.8"): warnings.warn( "Support for FreeType 2.7 is deprecated and will be removed" " in Pillow 9 (2022-01-02). Please upgrade to FreeType 2.8 " @@ -669,6 +671,7 @@ class FreeTypeFont: ) size = size[0] + stroke_width * 2, size[1] + stroke_width * 2 offset = offset[0] - stroke_width, offset[1] - stroke_width + Image._decompression_bomb_check(size) im = fill("RGBA" if mode == "RGBA" else "L", size, 0) self.font.render( text, im.id, mode, direction, features, language, stroke_width, ink diff --git a/src/PIL/ImageMode.py b/src/PIL/ImageMode.py index 988288329..0afcf9fe1 100644 --- a/src/PIL/ImageMode.py +++ b/src/PIL/ImageMode.py @@ -35,18 +35,28 @@ def getmode(mode): global _modes if not _modes: # initialize mode cache - - from . import Image - modes = {} - # core modes - for m, (basemode, basetype, bands) in Image._MODEINFO.items(): + for m, (basemode, basetype, bands) in { + # core modes + "1": ("L", "L", ("1",)), + "L": ("L", "L", ("L",)), + "I": ("L", "I", ("I",)), + "F": ("L", "F", ("F",)), + "P": ("P", "L", ("P",)), + "RGB": ("RGB", "L", ("R", "G", "B")), + "RGBX": ("RGB", "L", ("R", "G", "B", "X")), + "RGBA": ("RGB", "L", ("R", "G", "B", "A")), + "CMYK": ("RGB", "L", ("C", "M", "Y", "K")), + "YCbCr": ("RGB", "L", ("Y", "Cb", "Cr")), + "LAB": ("RGB", "L", ("L", "A", "B")), + "HSV": ("RGB", "L", ("H", "S", "V")), + # extra experimental modes + "RGBa": ("RGB", "L", ("R", "G", "B", "a")), + "LA": ("L", "L", ("L", "A")), + "La": ("L", "L", ("L", "a")), + "PA": ("RGB", "L", ("P", "A")), + }.items(): 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") # mapping modes for i16mode in ( "I;16", diff --git a/src/PIL/ImageMorph.py b/src/PIL/ImageMorph.py index b76dfa01f..fe0083754 100644 --- a/src/PIL/ImageMorph.py +++ b/src/PIL/ImageMorph.py @@ -196,7 +196,7 @@ class MorphOp: raise Exception("No operator loaded") if image.mode != "L": - raise Exception("Image must be binary, meaning it must use mode L") + raise ValueError("Image mode must be L") outimage = Image.new(image.mode, image.size, None) count = _imagingmorph.apply(bytes(self.lut), image.im.id, outimage.im.id) return count, outimage @@ -211,7 +211,7 @@ class MorphOp: raise Exception("No operator loaded") if image.mode != "L": - raise Exception("Image must be binary, meaning it must use mode L") + raise ValueError("Image mode must be L") return _imagingmorph.match(bytes(self.lut), image.im.id) def get_on_pixels(self, image): @@ -221,7 +221,7 @@ class MorphOp: of all matching pixels. See :ref:`coordinate-system`.""" if image.mode != "L": - raise Exception("Image must be binary, meaning it must use mode L") + raise ValueError("Image mode must be L") return _imagingmorph.get_on_pixels(image.im.id) def load_lut(self, filename): diff --git a/src/PIL/ImageOps.py b/src/PIL/ImageOps.py index 14602a5c8..711a519fc 100644 --- a/src/PIL/ImageOps.py +++ b/src/PIL/ImageOps.py @@ -20,7 +20,7 @@ import functools import operator -from . import Image +from . import Image, ImageDraw # # helpers @@ -61,7 +61,7 @@ def _lut(image, lut): # actions -def autocontrast(image, cutoff=0, ignore=None, mask=None): +def autocontrast(image, cutoff=0, ignore=None, mask=None, preserve_tone=False): """ Maximize (normalize) image contrast. This function calculates a histogram of the input image (or mask region), removes ``cutoff`` percent of the @@ -77,9 +77,17 @@ def autocontrast(image, cutoff=0, ignore=None, mask=None): :param mask: Histogram used in contrast operation is computed using pixels within the mask. If no mask is given the entire image is used for histogram computation. + :param preserve_tone: Preserve image tone in Photoshop-like style autocontrast. + + .. versionadded:: 8.2.0 + :return: An image. """ - histogram = image.histogram(mask) + if preserve_tone: + histogram = image.convert("L").histogram(mask) + else: + histogram = image.histogram(mask) + lut = [] for layer in range(0, len(histogram), 256): h = histogram[layer : layer + 256] @@ -228,15 +236,43 @@ def colorize(image, black, white, mid=None, blackpoint=0, whitepoint=255, midpoi return _lut(image, red + green + blue) -def pad(image, size, method=Image.BICUBIC, color=None, centering=(0.5, 0.5)): +def contain(image, size, method=Image.BICUBIC): """ - Returns a sized and padded version of the image, expanded to fill the - requested aspect ratio and size. + Returns a resized version of the image, set to the maximum width and height + within the requested size, while maintaining the original aspect ratio. - :param image: The image to size and crop. + :param image: The image to resize and crop. :param size: The requested output size in pixels, given as a (width, height) tuple. - :param method: What resampling method to use. Default is + :param method: Resampling method to use. Default is + :py:attr:`PIL.Image.BICUBIC`. See :ref:`concept-filters`. + :return: An image. + """ + + im_ratio = image.width / image.height + dest_ratio = size[0] / size[1] + + if im_ratio != dest_ratio: + if im_ratio > dest_ratio: + new_height = int(image.height / image.width * size[0]) + if new_height != size[1]: + size = (size[0], new_height) + else: + new_width = int(image.width / image.height * size[1]) + if new_width != size[0]: + size = (new_width, size[1]) + return image.resize(size, resample=method) + + +def pad(image, size, method=Image.BICUBIC, color=None, centering=(0.5, 0.5)): + """ + Returns a resized and padded version of the image, expanded to fill the + requested aspect ratio and size. + + :param image: The image to resize and crop. + :param size: The requested output size in pixels, given as a + (width, height) tuple. + :param method: Resampling method to use. Default is :py:attr:`PIL.Image.BICUBIC`. See :ref:`concept-filters`. :param color: The background color of the padded image. :param centering: Control the position of the original image within the @@ -249,27 +285,17 @@ def pad(image, size, method=Image.BICUBIC, color=None, centering=(0.5, 0.5)): :return: An image. """ - im_ratio = image.width / image.height - dest_ratio = size[0] / size[1] - - if im_ratio == dest_ratio: - out = image.resize(size, resample=method) + resized = contain(image, size, method) + if resized.size == size: + out = resized else: out = Image.new(image.mode, size, color) - if im_ratio > dest_ratio: - new_height = int(image.height / image.width * size[0]) - if new_height != size[1]: - image = image.resize((size[0], new_height), resample=method) - - y = int((size[1] - new_height) * max(0, min(centering[1], 1))) - out.paste(image, (0, y)) + if resized.width != size[0]: + x = int((size[0] - resized.width) * max(0, min(centering[0], 1))) + out.paste(resized, (x, 0)) else: - new_width = int(image.width / image.height * size[1]) - if new_width != size[0]: - image = image.resize((new_width, size[1]), resample=method) - - x = int((size[0] - new_width) * max(0, min(centering[0], 1))) - out.paste(image, (x, 0)) + y = int((size[1] - resized.height) * max(0, min(centering[1], 1))) + out.paste(resized, (0, y)) return out @@ -296,7 +322,7 @@ def scale(image, factor, resample=Image.BICUBIC): :param image: The image to rescale. :param factor: The expansion factor, as a float. - :param resample: What resampling method to use. Default is + :param resample: Resampling method to use. Default is :py:attr:`PIL.Image.BICUBIC`. See :ref:`concept-filters`. :returns: An :py:class:`~PIL.Image.Image` object. """ @@ -366,22 +392,31 @@ def expand(image, border=0, fill=0): left, top, right, bottom = _border(border) width = left + image.size[0] + right height = top + image.size[1] + bottom - out = Image.new(image.mode, (width, height), _color(fill, image.mode)) - out.paste(image, (left, top)) + color = _color(fill, image.mode) + if image.mode == "P" and image.palette: + out = Image.new(image.mode, (width, height)) + out.putpalette(image.palette) + out.paste(image, (left, top)) + + draw = ImageDraw.Draw(out) + draw.rectangle((0, 0, width - 1, height - 1), outline=color, width=border) + else: + out = Image.new(image.mode, (width, height), color) + out.paste(image, (left, top)) return out def fit(image, size, method=Image.BICUBIC, bleed=0.0, centering=(0.5, 0.5)): """ - Returns a sized and cropped version of the image, cropped to the + Returns a resized and cropped version of the image, cropped to the requested aspect ratio and size. This function was contributed by Kevin Cazabon. - :param image: The image to size and crop. + :param image: The image to resize and crop. :param size: The requested output size in pixels, given as a (width, height) tuple. - :param method: What resampling method to use. Default is + :param method: Resampling method to use. Default is :py:attr:`PIL.Image.BICUBIC`. See :ref:`concept-filters`. :param bleed: Remove a border around the outside of the image from all four edges. The value is a decimal percentage (use 0.01 for @@ -552,7 +587,8 @@ def exif_transpose(image): }.get(orientation) if method is not None: transposed_image = image.transpose(method) - del exif[0x0112] - transposed_image.info["exif"] = exif.tobytes() + transposed_exif = transposed_image.getexif() + del transposed_exif[0x0112] + transposed_image.info["exif"] = transposed_exif.tobytes() return transposed_image return image.copy() diff --git a/src/PIL/ImagePalette.py b/src/PIL/ImagePalette.py index d0604112f..b0c722b29 100644 --- a/src/PIL/ImagePalette.py +++ b/src/PIL/ImagePalette.py @@ -39,14 +39,27 @@ class ImagePalette: def __init__(self, mode="RGB", palette=None, size=0): self.mode = mode self.rawmode = None # if set, palette contains raw data - self.palette = palette or bytearray(range(256)) * len(self.mode) - self.colors = {} + self.palette = palette or bytearray() self.dirty = None - if (size == 0 and len(self.mode) * 256 != len(self.palette)) or ( - size != 0 and size != len(self.palette) - ): + if size != 0 and size != len(self.palette): raise ValueError("wrong palette size") + @property + def palette(self): + return self._palette + + @palette.setter + def palette(self, palette): + self._palette = palette + + mode_len = len(self.mode) + self.colors = {} + for i in range(0, len(self.palette), mode_len): + color = tuple(self.palette[i : i + mode_len]) + if color in self.colors: + continue + self.colors[color] = i // mode_len + def copy(self): new = ImagePalette() @@ -54,7 +67,6 @@ class ImagePalette: new.rawmode = self.rawmode if self.palette is not None: new.palette = self.palette[:] - new.colors = self.colors.copy() new.dirty = self.dirty return new @@ -68,7 +80,7 @@ class ImagePalette: """ if self.rawmode: return self.rawmode, self.palette - return self.mode + ";L", self.tobytes() + return self.mode, self.tobytes() def tobytes(self): """Convert palette to bytes. @@ -80,14 +92,12 @@ class ImagePalette: if isinstance(self.palette, bytes): return self.palette arr = array.array("B", self.palette) - if hasattr(arr, "tobytes"): - return arr.tobytes() - return arr.tostring() + return arr.tobytes() # Declare tostring as an alias for tobytes tostring = tobytes - def getcolor(self, color): + def getcolor(self, color, image=None): """Given an rgb tuple, allocate palette entry. .. warning:: This method is experimental. @@ -95,19 +105,45 @@ class ImagePalette: if self.rawmode: raise ValueError("palette contains raw palette data") if isinstance(color, tuple): + if self.mode == "RGB": + if len(color) == 4 and color[3] == 255: + color = color[:3] + elif self.mode == "RGBA": + if len(color) == 3: + color += (255,) try: return self.colors[color] except KeyError as e: # allocate new color slot - if isinstance(self.palette, bytes): - self.palette = bytearray(self.palette) - index = len(self.colors) + if not isinstance(self.palette, bytearray): + self._palette = bytearray(self.palette) + index = len(self.palette) // 3 + special_colors = () + if image: + special_colors = ( + image.info.get("background"), + image.info.get("transparency"), + ) + while index in special_colors: + index += 1 if index >= 256: - raise ValueError("cannot allocate more than 256 colors") from e + if image: + # Search for an unused index + for i, count in reversed(list(enumerate(image.histogram()))): + if count == 0 and i not in special_colors: + index = i + break + if index >= 256: + raise ValueError("cannot allocate more than 256 colors") from e self.colors[color] = index - self.palette[index] = color[0] - self.palette[index + 256] = color[1] - self.palette[index + 512] = color[2] + if index * 3 < len(self.palette): + self._palette = ( + self.palette[: index * 3] + + bytes(color) + + self.palette[index * 3 + 3 :] + ) + else: + self._palette += bytes(color) self.dirty = 1 return index else: diff --git a/src/PIL/ImageShow.py b/src/PIL/ImageShow.py index 3368865a4..c3693eb61 100644 --- a/src/PIL/ImageShow.py +++ b/src/PIL/ImageShow.py @@ -194,11 +194,21 @@ class DisplayViewer(UnixViewer): return command, executable +class GmDisplayViewer(UnixViewer): + """The GraphicsMagick ``gm display`` command.""" + + def get_command_ex(self, file, **options): + executable = "gm" + command = "gm display" + return command, executable + + class EogViewer(UnixViewer): """The GNOME Image Viewer ``eog`` command.""" def get_command_ex(self, file, **options): - command = executable = "eog" + executable = "eog" + command = "eog -n" return command, executable @@ -220,6 +230,8 @@ class XVViewer(UnixViewer): if sys.platform not in ("win32", "darwin"): # unixoids if shutil.which("display"): register(DisplayViewer) + if shutil.which("gm"): + register(GmDisplayViewer) if shutil.which("eog"): register(EogViewer) if shutil.which("xv"): @@ -245,7 +257,7 @@ else: if __name__ == "__main__": if len(sys.argv) < 2: - print("Syntax: python ImageShow.py imagefile [title]") + print("Syntax: python3 ImageShow.py imagefile [title]") sys.exit() with Image.open(sys.argv[1]) as im: diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py index ad260acbd..b18e8126f 100644 --- a/src/PIL/JpegImagePlugin.py +++ b/src/PIL/JpegImagePlugin.py @@ -33,6 +33,7 @@ # import array import io +import math import os import struct import subprocess @@ -139,8 +140,8 @@ def APP(self, marker): self.info["adobe"] = i16(s, 5) # extract Adobe custom properties try: - adobe_transform = s[1] - except Exception: + adobe_transform = s[11] + except IndexError: pass else: self.info["adobe_transform"] = adobe_transform @@ -161,15 +162,17 @@ def APP(self, marker): dpi = float(x_resolution[0]) / x_resolution[1] except TypeError: dpi = x_resolution + if math.isnan(dpi): + raise ValueError if resolution_unit == 3: # cm # 1 dpcm = 2.54 dpi dpi *= 2.54 - self.info["dpi"] = int(dpi + 0.5), int(dpi + 0.5) + self.info["dpi"] = dpi, dpi except (KeyError, SyntaxError, ValueError, ZeroDivisionError): # SyntaxError for invalid/unreadable EXIF # KeyError for dpi not included # ZeroDivisionError for invalid dpi rational value - # ValueError for x_resolution[0] being an invalid float + # ValueError for dpi being an invalid float self.info["dpi"] = 72, 72 @@ -251,7 +254,7 @@ def DQT(self, marker): data = array.array("B" if precision == 1 else "H", s[1:qt_length]) if sys.byteorder == "little" and precision > 1: data.byteswap() # the values are always big-endian - self.quantization[v & 15] = data + self.quantization[v & 15] = [data[i] for i in zigzag_index] s = s[qt_length:] @@ -474,6 +477,20 @@ class JpegImageFile(ImageFile.ImageFile): def _getmp(self): return _getmp(self) + def getxmp(self): + """ + Returns a dictionary containing the XMP tags. + Requires defusedxml to be installed. + :returns: XMP tags in a dictionary. + """ + + for segment, content in self.applist: + if segment == "APP1": + marker, xmp_tags = content.rsplit(b"\x00", 1) + if marker == b"http://ns.adobe.com/xap/1.0/": + return self._getxmp(xmp_tags) + return {} + def _getexif(self): if "exif" not in self.info: @@ -584,9 +601,11 @@ samplings = { def convert_dict_qtables(qtables): - qtables = [qtables[key] for key in range(len(qtables)) if key in qtables] - for idx, table in enumerate(qtables): - qtables[idx] = [table[i] for i in zigzag_index] + warnings.warn( + "convert_dict_qtables is deprecated and will be removed in Pillow 10" + "(2023-01-02). Conversion is no longer needed.", + DeprecationWarning, + ) return qtables @@ -667,7 +686,9 @@ def _save(im, fp, filename): qtables = [lines[s : s + 64] for s in range(0, len(lines), 64)] if isinstance(qtables, (tuple, list, dict)): if isinstance(qtables, dict): - qtables = convert_dict_qtables(qtables) + qtables = [ + qtables[key] for key in range(len(qtables)) if key in qtables + ] elif isinstance(qtables, tuple): qtables = list(qtables) if not (0 < len(qtables) < 5): diff --git a/src/PIL/JpegPresets.py b/src/PIL/JpegPresets.py index 79d10ebb2..e5a5d178a 100644 --- a/src/PIL/JpegPresets.py +++ b/src/PIL/JpegPresets.py @@ -52,19 +52,11 @@ You can get the quantization tables of a JPEG with:: im.quantization -This will return a dict with a number of arrays. You can pass this dict +This will return a dict with a number of lists. You can pass this dict directly as the qtables argument when saving a JPEG. -The tables format between im.quantization and quantization in presets differ in -3 ways: - -1. The base container of the preset is a list with sublists instead of dict. - dict[0] -> list[0], dict[1] -> list[1], ... -2. Each table in a preset is a list instead of an array. -3. The zigzag order is remove in the preset (needed by libjpeg >= 6a). - -You can convert the dict format to the preset format with the -:func:`.JpegImagePlugin.convert_dict_qtables()` function. +The quantization table format in presets is a list with sublists. These formats +are interchangeable. Libjpeg ref.: https://web.archive.org/web/20120328125543/http://www.jpegcameras.com/libjpeg/libjpeg-3.html diff --git a/src/PIL/PSDraw.py b/src/PIL/PSDraw.py index c1bd933d3..743c35f01 100644 --- a/src/PIL/PSDraw.py +++ b/src/PIL/PSDraw.py @@ -26,39 +26,36 @@ from . import EpsImagePlugin class PSDraw: """ Sets up printing to the given file. If ``fp`` is omitted, - :py:data:`sys.stdout` is assumed. + ``sys.stdout.buffer`` or ``sys.stdout`` is assumed. """ def __init__(self, fp=None): if not fp: - fp = sys.stdout + try: + fp = sys.stdout.buffer + except AttributeError: + fp = sys.stdout self.fp = fp - def _fp_write(self, to_write): - if self.fp == sys.stdout: - self.fp.write(to_write) - else: - self.fp.write(bytes(to_write, "UTF-8")) - def begin_document(self, id=None): """Set up printing of a document. (Write PostScript DSC header.)""" # FIXME: incomplete - self._fp_write( - "%!PS-Adobe-3.0\n" - "save\n" - "/showpage { } def\n" - "%%EndComments\n" - "%%BeginDocument\n" + self.fp.write( + b"%!PS-Adobe-3.0\n" + b"save\n" + b"/showpage { } def\n" + b"%%EndComments\n" + b"%%BeginDocument\n" ) - # self._fp_write(ERROR_PS) # debugging! - self._fp_write(EDROFF_PS) - self._fp_write(VDI_PS) - self._fp_write("%%EndProlog\n") + # self.fp.write(ERROR_PS) # debugging! + self.fp.write(EDROFF_PS) + self.fp.write(VDI_PS) + self.fp.write(b"%%EndProlog\n") self.isofont = {} def end_document(self): """Ends printing. (Write PostScript DSC footer.)""" - self._fp_write("%%EndDocument\nrestore showpage\n%%End\n") + self.fp.write(b"%%EndDocument\nrestore showpage\n%%End\n") if hasattr(self.fp, "flush"): self.fp.flush() @@ -69,12 +66,13 @@ class PSDraw: :param font: A PostScript font name :param size: Size in points. """ + font = bytes(font, "UTF-8") if font not in self.isofont: # reencode font - self._fp_write(f"/PSDraw-{font} ISOLatin1Encoding /{font} E\n") + self.fp.write(b"/PSDraw-%s ISOLatin1Encoding /%s E\n" % (font, font)) self.isofont[font] = 1 # rough - self._fp_write(f"/F0 {size} /PSDraw-{font} F\n") + self.fp.write(b"/F0 %d /PSDraw-%s F\n" % (size, font)) def line(self, xy0, xy1): """ @@ -82,7 +80,7 @@ class PSDraw: PostScript point coordinates (72 points per inch, (0, 0) is the lower left corner of the page). """ - self._fp_write("%d %d %d %d Vl\n" % (*xy0, *xy1)) + self.fp.write(b"%d %d %d %d Vl\n" % (*xy0, *xy1)) def rectangle(self, box): """ @@ -97,16 +95,18 @@ class PSDraw: %d %d M %d %d 0 Vr\n """ - self._fp_write("%d %d M %d %d 0 Vr\n" % box) + self.fp.write(b"%d %d M %d %d 0 Vr\n" % box) def text(self, xy, text): """ Draws text at the given position. You must use :py:meth:`~PIL.PSDraw.PSDraw.setfont` before calling this method. """ - text = "\\(".join(text.split("(")) - text = "\\)".join(text.split(")")) - self._fp_write(f"{xy[0]} {xy[1]} M ({text}) S\n") + text = bytes(text, "UTF-8") + text = b"\\(".join(text.split(b"(")) + text = b"\\)".join(text.split(b")")) + xy += (text,) + self.fp.write(b"%d %d M (%s) S\n" % xy) def image(self, box, im, dpi=None): """Draw a PIL image, centered in the given box.""" @@ -130,14 +130,14 @@ class PSDraw: y = ymax dx = (xmax - x) / 2 + box[0] dy = (ymax - y) / 2 + box[1] - self._fp_write(f"gsave\n{dx:f} {dy:f} translate\n") + self.fp.write(b"gsave\n%f %f translate\n" % (dx, dy)) if (x, y) != im.size: # EpsImagePlugin._save prints the image at (0,0,xsize,ysize) sx = x / im.size[0] sy = y / im.size[1] - self._fp_write(f"{sx:f} {sy:f} scale\n") + self.fp.write(b"%f %f scale\n" % (sx, sy)) EpsImagePlugin._save(im, self.fp, None, 0) - self._fp_write("\ngrestore\n") + self.fp.write(b"\ngrestore\n") # -------------------------------------------------------------------- @@ -153,7 +153,7 @@ class PSDraw: # -EDROFF_PS = """\ +EDROFF_PS = b"""\ /S { show } bind def /P { moveto show } bind def /M { moveto } bind def @@ -182,7 +182,7 @@ EDROFF_PS = """\ # Copyright (c) Fredrik Lundh 1994. # -VDI_PS = """\ +VDI_PS = b"""\ /Vm { moveto } bind def /Va { newpath arcn stroke } bind def /Vl { moveto lineto stroke } bind def @@ -207,7 +207,7 @@ VDI_PS = """\ # 89-11-21 fl: created (pslist 1.10) # -ERROR_PS = """\ +ERROR_PS = b"""\ /landscape false def /errorBUF 200 string def /errorNL { currentpoint 10 sub exch pop 72 exch moveto } def diff --git a/src/PIL/PdfImagePlugin.py b/src/PIL/PdfImagePlugin.py index 36c8fb849..49ba077e6 100644 --- a/src/PIL/PdfImagePlugin.py +++ b/src/PIL/PdfImagePlugin.py @@ -124,7 +124,7 @@ def _save(im, fp, filename, save_all=False): decode = None if im.mode == "1": - filter = "ASCIIHexDecode" + filter = "DCTDecode" colorspace = PdfParser.PdfName("DeviceGray") procset = "ImageB" # grayscale bits = 1 @@ -161,12 +161,6 @@ def _save(im, fp, filename, save_all=False): 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", im.size) - im.putdata(data) ImageFile._save(im, op, [("hex", (0, 0) + im.size, 0, im.mode)]) elif filter == "DCTDecode": Image.SAVE["JPEG"](im, op, filename) @@ -208,8 +202,8 @@ def _save(im, fp, filename, save_all=False): MediaBox=[ 0, 0, - int(width * 72.0 / resolution), - int(height * 72.0 / resolution), + width * 72.0 / resolution, + height * 72.0 / resolution, ], Contents=contents_refs[pageNumber], ) @@ -217,9 +211,9 @@ def _save(im, fp, filename, save_all=False): # # page contents - page_contents = b"q %d 0 0 %d 0 0 cm /image Do Q\n" % ( - int(width * 72.0 / resolution), - int(height * 72.0 / resolution), + page_contents = b"q %f 0 0 %f 0 0 cm /image Do Q\n" % ( + width * 72.0 / resolution, + height * 72.0 / resolution, ) existing_pdf.write_obj(contents_refs[pageNumber], stream=page_contents) diff --git a/src/PIL/PdfParser.py b/src/PIL/PdfParser.py index 86d78a95c..b5279e0d7 100644 --- a/src/PIL/PdfParser.py +++ b/src/PIL/PdfParser.py @@ -330,6 +330,8 @@ def pdf_repr(x): return bytes(x) elif isinstance(x, int): return str(x).encode("us-ascii") + elif isinstance(x, float): + return str(x).encode("us-ascii") elif isinstance(x, time.struct_time): return b"(D:" + time.strftime("%Y%m%d%H%M%SZ", x).encode("us-ascii") + b")" elif isinstance(x, dict): diff --git a/src/PIL/PngImagePlugin.py b/src/PIL/PngImagePlugin.py index 07bbc5228..bd886e218 100644 --- a/src/PIL/PngImagePlugin.py +++ b/src/PIL/PngImagePlugin.py @@ -500,7 +500,7 @@ class PngStream(ChunkStream): px, py = i32(s, 0), i32(s, 4) unit = s[8] if unit == 1: # meter - dpi = int(px * 0.0254 + 0.5), int(py * 0.0254 + 0.5) + dpi = px * 0.0254, py * 0.0254 self.im_info["dpi"] = dpi elif unit == 0: self.im_info["aspect"] = px, py @@ -920,6 +920,8 @@ class PngImageFile(ImageFile.ImageFile): def load_end(self): """internal: finished reading image data""" + if self.__idat != 0: + self.fp.read(self.__idat) while True: self.fp.read(4) # CRC @@ -976,6 +978,18 @@ class PngImageFile(ImageFile.ImageFile): return super().getexif() + def getxmp(self): + """ + Returns a dictionary containing the XMP tags. + Requires defusedxml to be installed. + :returns: XMP tags in a dictionary. + """ + return ( + self._getxmp(self.info["XML:com.adobe.xmp"]) + if "XML:com.adobe.xmp" in self.info + else {} + ) + def _close__fp(self): try: if self.__fp != self.fp: diff --git a/src/PIL/PsdImagePlugin.py b/src/PIL/PsdImagePlugin.py index d3799edc3..e7b884674 100644 --- a/src/PIL/PsdImagePlugin.py +++ b/src/PIL/PsdImagePlugin.py @@ -119,7 +119,8 @@ class PsdImageFile(ImageFile.ImageFile): end = self.fp.tell() + size size = i32(read(4)) if size: - self.layers = _layerinfo(self.fp) + _layer_data = io.BytesIO(ImageFile._safe_read(self.fp, size)) + self.layers = _layerinfo(_layer_data, size) self.fp.seek(end) self.n_frames = len(self.layers) self.is_animated = self.n_frames > 1 @@ -171,11 +172,20 @@ class PsdImageFile(ImageFile.ImageFile): self.__fp = None -def _layerinfo(file): +def _layerinfo(fp, ct_bytes): # read layerinfo block layers = [] - read = file.read - for i in range(abs(i16(read(2)))): + + def read(size): + return ImageFile._safe_read(fp, size) + + ct = i16(read(2)) + + # sanity check + if ct_bytes < (abs(ct) * 20): + raise SyntaxError("Layer block too short for number of layers requested") + + for i in range(abs(ct)): # bounding box y0 = i32(read(4)) @@ -186,7 +196,8 @@ def _layerinfo(file): # image info info = [] mode = [] - types = list(range(i16(read(2)))) + ct_types = i16(read(2)) + types = list(range(ct_types)) if len(types) > 4: continue @@ -219,16 +230,16 @@ def _layerinfo(file): size = i32(read(4)) # length of the extra data field combined = 0 if size: - data_end = file.tell() + size + data_end = fp.tell() + size length = i32(read(4)) if length: - file.seek(length - 16, io.SEEK_CUR) + fp.seek(length - 16, io.SEEK_CUR) combined += length + 4 length = i32(read(4)) if length: - file.seek(length, io.SEEK_CUR) + fp.seek(length, io.SEEK_CUR) combined += length + 4 length = i8(read(1)) @@ -238,7 +249,7 @@ def _layerinfo(file): name = read(length).decode("latin-1", "replace") combined += length + 1 - file.seek(data_end) + fp.seek(data_end) layers.append((name, mode, (x0, y0, x1, y1))) # get tiles @@ -246,7 +257,7 @@ def _layerinfo(file): for name, mode, bbox in layers: tile = [] for m in mode: - t = _maketile(file, m, bbox, 1) + t = _maketile(fp, m, bbox, 1) if t: tile.extend(t) layers[i] = name, mode, bbox, tile diff --git a/src/PIL/PyAccess.py b/src/PIL/PyAccess.py index 494f5f9f4..5ceaa238a 100644 --- a/src/PIL/PyAccess.py +++ b/src/PIL/PyAccess.py @@ -54,6 +54,7 @@ class PyAccess: self.image32 = ffi.cast("int **", vals["image32"]) self.image = ffi.cast("unsigned char **", vals["image"]) self.xsize, self.ysize = img.im.size + self._img = img # Keep pointer to im object to prevent dereferencing. self._im = img.im @@ -93,7 +94,7 @@ class PyAccess: and len(color) in [3, 4] ): # RGB or RGBA value for a P image - color = self._palette.getcolor(color) + color = self._palette.getcolor(color, self._img) return self.set_pixel(x, y, color) diff --git a/src/PIL/SpiderImagePlugin.py b/src/PIL/SpiderImagePlugin.py index 819f2ed0a..062af9f98 100644 --- a/src/PIL/SpiderImagePlugin.py +++ b/src/PIL/SpiderImagePlugin.py @@ -296,7 +296,7 @@ Image.register_save(SpiderImageFile.format, _save_spider) if __name__ == "__main__": if len(sys.argv) < 2: - print("Syntax: python SpiderImagePlugin.py [infile] [outfile]") + print("Syntax: python3 SpiderImagePlugin.py [infile] [outfile]") sys.exit() filename = sys.argv[1] diff --git a/src/PIL/TgaImagePlugin.py b/src/PIL/TgaImagePlugin.py index 2b936d687..5e5d52d1a 100644 --- a/src/PIL/TgaImagePlugin.py +++ b/src/PIL/TgaImagePlugin.py @@ -110,10 +110,10 @@ class TgaImageFile(ImageFile.ImageFile): if colormaptype: # read palette - start, size, mapdepth = i16(s, 3), i16(s, 5), i16(s, 7) + start, size, mapdepth = i16(s, 3), i16(s, 5), s[7] if mapdepth == 16: self.palette = ImagePalette.raw( - "BGR;16", b"\0" * 2 * start + self.fp.read(2 * size) + "BGR;15", b"\0" * 2 * start + self.fp.read(2 * size) ) elif mapdepth == 24: self.palette = ImagePalette.raw( diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index 24821d130..a5e2bb53d 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -354,9 +354,22 @@ class IFDRational(Rational): return self._val.__hash__() def __eq__(self, other): + val = self._val if isinstance(other, IFDRational): other = other._val - return self._val == other + if isinstance(other, float): + val = float(val) + return val == other + + def __getstate__(self): + return [self._val, self._numerator, self._denominator] + + def __setstate__(self, state): + IFDRational.__init__(self, 0) + _val, _numerator, _denominator = state + self._val = _val + self._numerator = _numerator + self._denominator = _denominator def _delegate(op): def delegate(self, *args): @@ -446,16 +459,16 @@ class ImageFileDirectory_v2(MutableMapping): Tags will be found in the private attributes self._tagdata, and in self._tags_v2 once decoded. - Self.legacy_api is a value for internal use, and shouldn't be + self.legacy_api is a value for internal use, and shouldn't be changed from outside code. In cooperation with the ImageFileDirectory_v1 class, if legacy_api is true, then decoded - tags will be populated into both _tags_v1 and _tags_v2. _Tags_v2 + tags will be populated into both _tags_v1 and _tags_v2. _tags_v2 will be used if this IFD is used in the TIFF save routine. Tags - should be read from tags_v1 if legacy_api == true. + should be read from _tags_v1 if legacy_api == true. """ - def __init__(self, ifh=b"II\052\0\0\0\0\0", prefix=None): + def __init__(self, ifh=b"II\052\0\0\0\0\0", prefix=None, group=None): """Initialize an ImageFileDirectory. To construct an ImageFileDirectory from a real file, pass the 8-byte @@ -475,6 +488,7 @@ class ImageFileDirectory_v2(MutableMapping): self._endian = "<" else: raise SyntaxError("not a TIFF IFD") + self.group = group self.tagtype = {} """ Dictionary of tag types """ self.reset() @@ -506,7 +520,10 @@ class ImageFileDirectory_v2(MutableMapping): Returns the complete tag dictionary, with named tags where possible. """ - return {TiffTags.lookup(code).name: value for code, value in self.items()} + return { + TiffTags.lookup(code, self.group).name: value + for code, value in self.items() + } def __len__(self): return len(set(self._tagdata) | set(self._tags_v2)) @@ -531,7 +548,7 @@ class ImageFileDirectory_v2(MutableMapping): def _setitem(self, tag, value, legacy_api): basetypes = (Number, bytes, str) - info = TiffTags.lookup(tag) + info = TiffTags.lookup(tag, self.group) values = [value] if isinstance(value, basetypes) else value if tag not in self.tagtype: @@ -565,7 +582,8 @@ class ImageFileDirectory_v2(MutableMapping): if self.tagtype[tag] == TiffTags.UNDEFINED: values = [ - value.encode("ascii", "replace") if isinstance(value, str) else value + v.encode("ascii", "replace") if isinstance(v, str) else v + for v in values ] elif self.tagtype[tag] == TiffTags.RATIONAL: values = [float(v) if isinstance(v, int) else v for v in values] @@ -747,7 +765,7 @@ class ImageFileDirectory_v2(MutableMapping): 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)) - tagname = TiffTags.lookup(tag).name + tagname = TiffTags.lookup(tag, self.group).name typname = TYPES.get(typ, "unknown") msg = f"tag: {tagname} ({tag}) - type: {typname} ({typ})" @@ -814,15 +832,16 @@ class ImageFileDirectory_v2(MutableMapping): ifh = b"II\x2A\x00\x08\x00\x00\x00" else: ifh = b"MM\x00\x2A\x00\x00\x00\x08" - ifd = ImageFileDirectory_v2(ifh) - for ifd_tag, ifd_value in self._tags_v2[tag].items(): + ifd = ImageFileDirectory_v2(ifh, group=tag) + values = self._tags_v2[tag] + for ifd_tag, ifd_value in values.items(): ifd[ifd_tag] = ifd_value data = ifd.tobytes(offset) else: values = value if isinstance(value, tuple) else (value,) data = self._write_dispatch[typ](self, *values) - tagname = TiffTags.lookup(tag).name + tagname = TiffTags.lookup(tag, self.group).name typname = "ifd" if is_ifd else TYPES.get(typ, "unknown") msg = f"save: {tagname} ({tag}) - type: {typname} ({typ})" msg += " - value: " + ( @@ -1052,6 +1071,11 @@ class TiffImageFile(ImageFile.ImageFile): def _seek(self, frame): self.fp = self.__fp + + # reset buffered io handle in case fp + # was passed to libtiff, invalidating the buffer + self.fp.tell() + while len(self._frame_pos) <= frame: if not self.__next: raise EOFError("no more images in TIFF file") @@ -1059,14 +1083,16 @@ class TiffImageFile(ImageFile.ImageFile): f"Seeking to frame {frame}, on frame {self.__frame}, " f"__next {self.__next}, location: {self.fp.tell()}" ) - # reset buffered io handle in case fp - # was passed to libtiff, invalidating the buffer - self.fp.tell() self.fp.seek(self.__next) self._frame_pos.append(self.__next) logger.debug("Loading tags, location: %s" % self.fp.tell()) self.tag_v2.load(self.fp) - self.__next = self.tag_v2.next + if self.tag_v2.next in self._frame_pos: + # This IFD has already been processed + # Declare this to be the end of the image + self.__next = 0 + else: + self.__next = self.tag_v2.next if self.__next == 0: self._n_frames = frame + 1 if len(self._frame_pos) == 1: @@ -1083,6 +1109,14 @@ class TiffImageFile(ImageFile.ImageFile): """Return the current frame number""" return self.__frame + def getxmp(self): + """ + Returns a dictionary containing the XMP tags. + Requires defusedxml to be installed. + :returns: XMP tags in a dictionary. + """ + return self._getxmp(self.tag_v2[700]) if 700 in self.tag_v2 else {} + def load(self): if self.tile and self.use_load_libtiff: return self._load_libtiff() @@ -1250,6 +1284,13 @@ class TiffImageFile(ImageFile.ImageFile): if bps_count > len(bps_tuple) and len(bps_tuple) == 1: bps_tuple = bps_tuple * bps_count + samplesPerPixel = self.tag_v2.get( + SAMPLESPERPIXEL, + 3 if self._compression == "tiff_jpeg" and photo in (2, 6) else 1, + ) + if len(bps_tuple) != samplesPerPixel: + raise SyntaxError("unknown data organization") + # mode: check photometric interpretation and bits per pixel key = ( self.tag_v2.prefix, @@ -1277,11 +1318,11 @@ class TiffImageFile(ImageFile.ImageFile): if xres and yres: resunit = self.tag_v2.get(RESOLUTION_UNIT) if resunit == 2: # dots per inch - self.info["dpi"] = int(xres + 0.5), int(yres + 0.5) + self.info["dpi"] = (xres, yres) elif resunit == 3: # dots per centimeter. convert to dpi - self.info["dpi"] = int(xres * 2.54 + 0.5), int(yres * 2.54 + 0.5) + self.info["dpi"] = (xres * 2.54, yres * 2.54) elif resunit is None: # used to default to 1, but now 2) - self.info["dpi"] = int(xres + 0.5), int(yres + 0.5) + self.info["dpi"] = (xres, yres) # For backward compatibility, # we also preserve the old behavior self.info["resolution"] = xres, yres @@ -1514,8 +1555,8 @@ def _save(im, fp, filename): dpi = im.encoderinfo.get("dpi") if dpi: ifd[RESOLUTION_UNIT] = 2 - ifd[X_RESOLUTION] = int(dpi[0] + 0.5) - ifd[Y_RESOLUTION] = int(dpi[1] + 0.5) + ifd[X_RESOLUTION] = dpi[0] + ifd[Y_RESOLUTION] = dpi[1] if bits != (1,): ifd[BITSPERSAMPLE] = bits @@ -1533,12 +1574,22 @@ def _save(im, fp, filename): ifd[COLORMAP] = tuple(v * 256 for v in lut) # data orientation stride = len(bits) * ((im.size[0] * bits[0] + 7) // 8) - ifd[ROWSPERSTRIP] = im.size[1] - strip_byte_counts = stride * im.size[1] + # aim for 64 KB strips when using libtiff writer + if libtiff: + rows_per_strip = min((2 ** 16 + stride - 1) // stride, im.size[1]) + else: + rows_per_strip = im.size[1] + strip_byte_counts = stride * rows_per_strip + strips_per_image = (im.size[1] + rows_per_strip - 1) // rows_per_strip + ifd[ROWSPERSTRIP] = rows_per_strip if strip_byte_counts >= 2 ** 16: ifd.tagtype[STRIPBYTECOUNTS] = TiffTags.LONG - ifd[STRIPBYTECOUNTS] = strip_byte_counts - ifd[STRIPOFFSETS] = 0 # this is adjusted by IFD writer + ifd[STRIPBYTECOUNTS] = (strip_byte_counts,) * (strips_per_image - 1) + ( + stride * im.size[1] - strip_byte_counts * (strips_per_image - 1), + ) + ifd[STRIPOFFSETS] = tuple( + range(0, strip_byte_counts * strips_per_image, strip_byte_counts) + ) # this is adjusted by IFD writer # no compression by default: ifd[COMPRESSION] = COMPRESSION_INFO_REV.get(compression, 1) diff --git a/src/PIL/TiffTags.py b/src/PIL/TiffTags.py index 9e9e117a4..88856aa92 100644 --- a/src/PIL/TiffTags.py +++ b/src/PIL/TiffTags.py @@ -33,7 +33,7 @@ class TagInfo(namedtuple("_TagInfo", "value name type length enum")): return self.enum.get(value, value) if self.enum else value -def lookup(tag): +def lookup(tag, group=None): """ :param tag: Integer tag number :returns: Taginfo namedtuple, From the TAGS_V2 info if possible, @@ -42,7 +42,11 @@ def lookup(tag): """ - return TAGS_V2.get(tag, TagInfo(tag, TAGS.get(tag, "unknown"))) + if group is not None: + info = TAGS_V2_GROUPS[group].get(tag) if group in TAGS_V2_GROUPS else None + else: + info = TAGS_V2.get(tag) + return info or TagInfo(tag, TAGS.get(tag, "unknown")) ## @@ -178,13 +182,15 @@ TAGS_V2 = { 532: ("ReferenceBlackWhite", RATIONAL, 6), 700: ("XMP", BYTE, 0), 33432: ("Copyright", ASCII, 1), - 33723: ("IptcNaaInfo", UNDEFINED, 0), + 33723: ("IptcNaaInfo", UNDEFINED, 1), 34377: ("PhotoshopInfo", BYTE, 0), # FIXME add more tags here 34665: ("ExifIFD", LONG, 1), 34675: ("ICCProfile", UNDEFINED, 1), 34853: ("GPSInfoIFD", LONG, 1), + 36864: ("ExifVersion", UNDEFINED, 1), 40965: ("InteroperabilityIFD", LONG, 1), + 41730: ("CFAPattern", UNDEFINED, 1), # MPInfo 45056: ("MPFVersion", UNDEFINED, 1), 45057: ("NumberOfImages", LONG, 1), @@ -205,11 +211,25 @@ TAGS_V2 = { 45579: ("YawAngle", SIGNED_RATIONAL, 1), 45580: ("PitchAngle", SIGNED_RATIONAL, 1), 45581: ("RollAngle", SIGNED_RATIONAL, 1), + 40960: ("FlashPixVersion", UNDEFINED, 1), 50741: ("MakerNoteSafety", SHORT, 1, {"Unsafe": 0, "Safe": 1}), 50780: ("BestQualityScale", RATIONAL, 1), 50838: ("ImageJMetaDataByteCounts", LONG, 0), # Can be more than one 50839: ("ImageJMetaData", UNDEFINED, 1), # see Issue #2006 } +TAGS_V2_GROUPS = { + # ExifIFD + 34665: { + 36864: ("ExifVersion", UNDEFINED, 1), + 40960: ("FlashPixVersion", UNDEFINED, 1), + 40965: ("InteroperabilityIFD", LONG, 1), + 41730: ("CFAPattern", UNDEFINED, 1), + }, + # GPSInfoIFD + 34853: {}, + # InteroperabilityIFD + 40965: {1: ("InteropIndex", ASCII, 1), 2: ("InteropVersion", UNDEFINED, 1)}, +} # Legacy Tags structure # these tags aren't included above, but were in the previous versions @@ -368,6 +388,10 @@ def _populate(): TAGS_V2[k] = TagInfo(k, *v) + for group, tags in TAGS_V2_GROUPS.items(): + for k, v in tags.items(): + tags[k] = TagInfo(k, *v) + _populate() ## @@ -485,9 +509,6 @@ LIBTIFF_CORE = { 65537, } -LIBTIFF_CORE.remove(301) # Array of short, crashes -LIBTIFF_CORE.remove(532) # Array of long, crashes - LIBTIFF_CORE.remove(255) # We don't have support for subfiletypes LIBTIFF_CORE.remove(322) # We don't have support for writing tiled images with libtiff LIBTIFF_CORE.remove(323) # Tiled images diff --git a/src/PIL/WebPImagePlugin.py b/src/PIL/WebPImagePlugin.py index dfc8351ef..b63a07ca8 100644 --- a/src/PIL/WebPImagePlugin.py +++ b/src/PIL/WebPImagePlugin.py @@ -314,13 +314,13 @@ def _save(im, fp, filename): if isinstance(exif, Image.Exif): exif = exif.tobytes() xmp = im.encoderinfo.get("xmp", "") - method = im.encoderinfo.get("method", 0) + method = im.encoderinfo.get("method", 4) if im.mode not in _VALID_WEBP_LEGACY_MODES: alpha = ( "A" in im.mode or "a" in im.mode - or (im.mode == "P" and "A" in im.im.getpalettemode()) + or (im.mode == "P" and "transparency" in im.info) ) im = im.convert("RGBA" if alpha else "RGB") diff --git a/src/PIL/WmfImagePlugin.py b/src/PIL/WmfImagePlugin.py index 87847a107..27f5d2f87 100644 --- a/src/PIL/WmfImagePlugin.py +++ b/src/PIL/WmfImagePlugin.py @@ -127,8 +127,8 @@ class WmfStubImageFile(ImageFile.StubImageFile): size = x1 - x0, y1 - y0 # calculate dots per inch from bbox and frame - xdpi = int(2540.0 * (x1 - y0) / (frame[2] - frame[0]) + 0.5) - ydpi = int(2540.0 * (y1 - y0) / (frame[3] - frame[1]) + 0.5) + xdpi = 2540.0 * (x1 - y0) / (frame[2] - frame[0]) + ydpi = 2540.0 * (y1 - y0) / (frame[3] - frame[1]) self.info["wmf_bbox"] = x0, y0, x1, y1 @@ -152,7 +152,7 @@ class WmfStubImageFile(ImageFile.StubImageFile): def load(self, dpi=None): if dpi is not None and self._inch is not None: - self.info["dpi"] = int(dpi + 0.5) + self.info["dpi"] = dpi x0, y0, x1, y1 = self.info["wmf_bbox"] self._size = ( (x1 - x0) * self.info["dpi"] // self._inch, diff --git a/src/PIL/_version.py b/src/PIL/_version.py index 20e8754a4..31f5daa47 100644 --- a/src/PIL/_version.py +++ b/src/PIL/_version.py @@ -1,2 +1,2 @@ # Master version for Pillow -__version__ = "8.2.0.dev0" +__version__ = "8.4.0.dev0" diff --git a/src/PIL/features.py b/src/PIL/features.py index 66d0ba10a..3838568f3 100644 --- a/src/PIL/features.py +++ b/src/PIL/features.py @@ -218,7 +218,7 @@ def get_supported(): def pilinfo(out=None, supported_formats=True): """ Prints information about this installation of Pillow. - This function can be called with ``python -m PIL``. + This function can be called with ``python3 -m PIL``. :param out: The output stream to print to. Defaults to ``sys.stdout`` if ``None``. diff --git a/src/_imaging.c b/src/_imaging.c index a5b12d325..e2193fec3 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -498,7 +498,7 @@ getink(PyObject *color, Imaging im, char *ink) { be cast to either UINT8 or INT32 */ int rIsInt = 0; - if (PyTuple_Check(color) && PyTuple_Size(color) == 1) { + if (PyTuple_Check(color) && PyTuple_GET_SIZE(color) == 1) { color = PyTuple_GetItem(color, 0); } if (im->type == IMAGING_TYPE_UINT8 || im->type == IMAGING_TYPE_INT32 || @@ -527,7 +527,10 @@ getink(PyObject *color, Imaging im, char *ink) { if (im->bands == 1) { /* unsigned integer, single layer */ if (rIsInt != 1) { - if (!PyArg_ParseTuple(color, "L", &r)) { + if (PyTuple_GET_SIZE(color) != 1) { + PyErr_SetString(PyExc_TypeError, "color must be int or single-element tuple"); + return NULL; + } else if (!PyArg_ParseTuple(color, "L", &r)) { return NULL; } } @@ -542,13 +545,20 @@ getink(PyObject *color, Imaging im, char *ink) { g = (UINT8)(r >> 8); r = (UINT8)r; } else { + int tupleSize = PyTuple_GET_SIZE(color); if (im->bands == 2) { - if (!PyArg_ParseTuple(color, "L|i", &r, &a)) { + if (tupleSize != 1 && tupleSize != 2) { + PyErr_SetString(PyExc_TypeError, "color must be int, or tuple of one or two elements"); + return NULL; + } else if (!PyArg_ParseTuple(color, "L|i", &r, &a)) { return NULL; } g = b = r; } else { - if (!PyArg_ParseTuple(color, "Lii|i", &r, &g, &b, &a)) { + if (tupleSize != 3 && tupleSize != 4) { + PyErr_SetString(PyExc_TypeError, "color must be int, or tuple of one, three or four elements"); + return NULL; + } else if (!PyArg_ParseTuple(color, "Lii|i", &r, &g, &b, &a)) { return NULL; } } @@ -1086,7 +1096,7 @@ _getpalette(ImagingObject *self, PyObject *args) { } static PyObject * -_getpalettemode(ImagingObject *self, PyObject *args) { +_getpalettemode(ImagingObject *self) { if (!self->image->palette) { PyErr_SetString(PyExc_ValueError, no_palette); return NULL; @@ -1109,7 +1119,12 @@ _getxy(PyObject *xy, int *x, int *y) { } else if (PyFloat_Check(value)) { *x = (int)PyFloat_AS_DOUBLE(value); } else { - goto badval; + PyObject *int_value = PyObject_CallMethod(value, "__int__", NULL); + if (int_value != NULL && PyLong_Check(int_value)) { + *x = PyLong_AS_LONG(int_value); + } else { + goto badval; + } } value = PyTuple_GET_ITEM(xy, 1); @@ -1118,7 +1133,12 @@ _getxy(PyObject *xy, int *x, int *y) { } else if (PyFloat_Check(value)) { *y = (int)PyFloat_AS_DOUBLE(value); } else { - goto badval; + PyObject *int_value = PyObject_CallMethod(value, "__int__", NULL); + if (int_value != NULL && PyLong_Check(int_value)) { + *y = PyLong_AS_LONG(int_value); + } else { + goto badval; + } } return 0; @@ -1643,8 +1663,7 @@ _putpalette(ImagingObject *self, PyObject *args) { unpack(self->image->palette->palette, palette, palettesize * 8 / bits); - Py_INCREF(Py_None); - return Py_None; + return PyLong_FromLong(palettesize * 8 / bits); } static PyObject * @@ -2085,12 +2104,12 @@ _box_blur(ImagingObject *self, PyObject *args) { /* -------------------------------------------------------------------- */ static PyObject * -_isblock(ImagingObject *self, PyObject *args) { +_isblock(ImagingObject *self) { return PyBool_FromLong(self->image->block != NULL); } static PyObject * -_getbbox(ImagingObject *self, PyObject *args) { +_getbbox(ImagingObject *self) { int bbox[4]; if (!ImagingGetBBox(self->image, bbox)) { Py_INCREF(Py_None); @@ -2135,7 +2154,7 @@ _getcolors(ImagingObject *self, PyObject *args) { } static PyObject * -_getextrema(ImagingObject *self, PyObject *args) { +_getextrema(ImagingObject *self) { union { UINT8 u[2]; INT32 i[2]; @@ -2169,7 +2188,7 @@ _getextrema(ImagingObject *self, PyObject *args) { } static PyObject * -_getprojection(ImagingObject *self, PyObject *args) { +_getprojection(ImagingObject *self) { unsigned char *xprofile; unsigned char *yprofile; PyObject *result; @@ -2287,7 +2306,7 @@ _merge(PyObject *self, PyObject *args) { } static PyObject * -_split(ImagingObject *self, PyObject *args) { +_split(ImagingObject *self) { int fails = 0; Py_ssize_t i; PyObject *list; @@ -2318,7 +2337,7 @@ _split(ImagingObject *self, PyObject *args) { #ifdef WITH_IMAGECHOPS static PyObject * -_chop_invert(ImagingObject *self, PyObject *args) { +_chop_invert(ImagingObject *self) { return PyImagingNew(ImagingNegative(self->image)); } @@ -2697,8 +2716,8 @@ _font_getsize(ImagingFontObject *self, PyObject *args) { } static struct PyMethodDef _font_methods[] = { - {"getmask", (PyCFunction)_font_getmask, 1}, - {"getsize", (PyCFunction)_font_getsize, 1}, + {"getmask", (PyCFunction)_font_getmask, METH_VARARGS}, + {"getsize", (PyCFunction)_font_getsize, METH_VARARGS}, {NULL, NULL} /* sentinel */ }; @@ -3192,19 +3211,19 @@ _draw_rectangle(ImagingDrawObject *self, PyObject *args) { static struct PyMethodDef _draw_methods[] = { #ifdef WITH_IMAGEDRAW /* Graphics (ImageDraw) */ - {"draw_lines", (PyCFunction)_draw_lines, 1}, + {"draw_lines", (PyCFunction)_draw_lines, METH_VARARGS}, #ifdef WITH_ARROW - {"draw_outline", (PyCFunction)_draw_outline, 1}, + {"draw_outline", (PyCFunction)_draw_outline, METH_VARARGS}, #endif - {"draw_polygon", (PyCFunction)_draw_polygon, 1}, - {"draw_rectangle", (PyCFunction)_draw_rectangle, 1}, - {"draw_points", (PyCFunction)_draw_points, 1}, - {"draw_arc", (PyCFunction)_draw_arc, 1}, - {"draw_bitmap", (PyCFunction)_draw_bitmap, 1}, - {"draw_chord", (PyCFunction)_draw_chord, 1}, - {"draw_ellipse", (PyCFunction)_draw_ellipse, 1}, - {"draw_pieslice", (PyCFunction)_draw_pieslice, 1}, - {"draw_ink", (PyCFunction)_draw_ink, 1}, + {"draw_polygon", (PyCFunction)_draw_polygon, METH_VARARGS}, + {"draw_rectangle", (PyCFunction)_draw_rectangle, METH_VARARGS}, + {"draw_points", (PyCFunction)_draw_points, METH_VARARGS}, + {"draw_arc", (PyCFunction)_draw_arc, METH_VARARGS}, + {"draw_bitmap", (PyCFunction)_draw_bitmap, METH_VARARGS}, + {"draw_chord", (PyCFunction)_draw_chord, METH_VARARGS}, + {"draw_ellipse", (PyCFunction)_draw_ellipse, METH_VARARGS}, + {"draw_pieslice", (PyCFunction)_draw_pieslice, METH_VARARGS}, + {"draw_ink", (PyCFunction)_draw_ink, METH_VARARGS}, #endif {NULL, NULL} /* sentinel */ }; @@ -3411,100 +3430,100 @@ _save_ppm(ImagingObject *self, PyObject *args) { static struct PyMethodDef methods[] = { /* Put commonly used methods first */ - {"getpixel", (PyCFunction)_getpixel, 1}, - {"putpixel", (PyCFunction)_putpixel, 1}, + {"getpixel", (PyCFunction)_getpixel, METH_VARARGS}, + {"putpixel", (PyCFunction)_putpixel, METH_VARARGS}, - {"pixel_access", (PyCFunction)pixel_access_new, 1}, + {"pixel_access", (PyCFunction)pixel_access_new, METH_VARARGS}, /* Standard processing methods (Image) */ - {"color_lut_3d", (PyCFunction)_color_lut_3d, 1}, - {"convert", (PyCFunction)_convert, 1}, - {"convert2", (PyCFunction)_convert2, 1}, - {"convert_matrix", (PyCFunction)_convert_matrix, 1}, - {"convert_transparent", (PyCFunction)_convert_transparent, 1}, - {"copy", (PyCFunction)_copy, 1}, - {"crop", (PyCFunction)_crop, 1}, - {"expand", (PyCFunction)_expand_image, 1}, - {"filter", (PyCFunction)_filter, 1}, - {"histogram", (PyCFunction)_histogram, 1}, - {"entropy", (PyCFunction)_entropy, 1}, + {"color_lut_3d", (PyCFunction)_color_lut_3d, METH_VARARGS}, + {"convert", (PyCFunction)_convert, METH_VARARGS}, + {"convert2", (PyCFunction)_convert2, METH_VARARGS}, + {"convert_matrix", (PyCFunction)_convert_matrix, METH_VARARGS}, + {"convert_transparent", (PyCFunction)_convert_transparent, METH_VARARGS}, + {"copy", (PyCFunction)_copy, METH_VARARGS}, + {"crop", (PyCFunction)_crop, METH_VARARGS}, + {"expand", (PyCFunction)_expand_image, METH_VARARGS}, + {"filter", (PyCFunction)_filter, METH_VARARGS}, + {"histogram", (PyCFunction)_histogram, METH_VARARGS}, + {"entropy", (PyCFunction)_entropy, METH_VARARGS}, #ifdef WITH_MODEFILTER - {"modefilter", (PyCFunction)_modefilter, 1}, + {"modefilter", (PyCFunction)_modefilter, METH_VARARGS}, #endif - {"offset", (PyCFunction)_offset, 1}, - {"paste", (PyCFunction)_paste, 1}, - {"point", (PyCFunction)_point, 1}, - {"point_transform", (PyCFunction)_point_transform, 1}, - {"putdata", (PyCFunction)_putdata, 1}, + {"offset", (PyCFunction)_offset, METH_VARARGS}, + {"paste", (PyCFunction)_paste, METH_VARARGS}, + {"point", (PyCFunction)_point, METH_VARARGS}, + {"point_transform", (PyCFunction)_point_transform, METH_VARARGS}, + {"putdata", (PyCFunction)_putdata, METH_VARARGS}, #ifdef WITH_QUANTIZE - {"quantize", (PyCFunction)_quantize, 1}, + {"quantize", (PyCFunction)_quantize, METH_VARARGS}, #endif #ifdef WITH_RANKFILTER - {"rankfilter", (PyCFunction)_rankfilter, 1}, + {"rankfilter", (PyCFunction)_rankfilter, METH_VARARGS}, #endif - {"resize", (PyCFunction)_resize, 1}, - {"reduce", (PyCFunction)_reduce, 1}, - {"transpose", (PyCFunction)_transpose, 1}, - {"transform2", (PyCFunction)_transform2, 1}, + {"resize", (PyCFunction)_resize, METH_VARARGS}, + {"reduce", (PyCFunction)_reduce, METH_VARARGS}, + {"transpose", (PyCFunction)_transpose, METH_VARARGS}, + {"transform2", (PyCFunction)_transform2, METH_VARARGS}, - {"isblock", (PyCFunction)_isblock, 1}, + {"isblock", (PyCFunction)_isblock, METH_NOARGS}, - {"getbbox", (PyCFunction)_getbbox, 1}, - {"getcolors", (PyCFunction)_getcolors, 1}, - {"getextrema", (PyCFunction)_getextrema, 1}, - {"getprojection", (PyCFunction)_getprojection, 1}, + {"getbbox", (PyCFunction)_getbbox, METH_NOARGS}, + {"getcolors", (PyCFunction)_getcolors, METH_VARARGS}, + {"getextrema", (PyCFunction)_getextrema, METH_NOARGS}, + {"getprojection", (PyCFunction)_getprojection, METH_NOARGS}, - {"getband", (PyCFunction)_getband, 1}, - {"putband", (PyCFunction)_putband, 1}, - {"split", (PyCFunction)_split, 1}, - {"fillband", (PyCFunction)_fillband, 1}, + {"getband", (PyCFunction)_getband, METH_VARARGS}, + {"putband", (PyCFunction)_putband, METH_VARARGS}, + {"split", (PyCFunction)_split, METH_NOARGS}, + {"fillband", (PyCFunction)_fillband, METH_VARARGS}, - {"setmode", (PyCFunction)im_setmode, 1}, + {"setmode", (PyCFunction)im_setmode, METH_VARARGS}, - {"getpalette", (PyCFunction)_getpalette, 1}, - {"getpalettemode", (PyCFunction)_getpalettemode, 1}, - {"putpalette", (PyCFunction)_putpalette, 1}, - {"putpalettealpha", (PyCFunction)_putpalettealpha, 1}, - {"putpalettealphas", (PyCFunction)_putpalettealphas, 1}, + {"getpalette", (PyCFunction)_getpalette, METH_VARARGS}, + {"getpalettemode", (PyCFunction)_getpalettemode, METH_NOARGS}, + {"putpalette", (PyCFunction)_putpalette, METH_VARARGS}, + {"putpalettealpha", (PyCFunction)_putpalettealpha, METH_VARARGS}, + {"putpalettealphas", (PyCFunction)_putpalettealphas, METH_VARARGS}, #ifdef WITH_IMAGECHOPS /* Channel operations (ImageChops) */ - {"chop_invert", (PyCFunction)_chop_invert, 1}, - {"chop_lighter", (PyCFunction)_chop_lighter, 1}, - {"chop_darker", (PyCFunction)_chop_darker, 1}, - {"chop_difference", (PyCFunction)_chop_difference, 1}, - {"chop_multiply", (PyCFunction)_chop_multiply, 1}, - {"chop_screen", (PyCFunction)_chop_screen, 1}, - {"chop_add", (PyCFunction)_chop_add, 1}, - {"chop_subtract", (PyCFunction)_chop_subtract, 1}, - {"chop_add_modulo", (PyCFunction)_chop_add_modulo, 1}, - {"chop_subtract_modulo", (PyCFunction)_chop_subtract_modulo, 1}, - {"chop_and", (PyCFunction)_chop_and, 1}, - {"chop_or", (PyCFunction)_chop_or, 1}, - {"chop_xor", (PyCFunction)_chop_xor, 1}, - {"chop_soft_light", (PyCFunction)_chop_soft_light, 1}, - {"chop_hard_light", (PyCFunction)_chop_hard_light, 1}, - {"chop_overlay", (PyCFunction)_chop_overlay, 1}, + {"chop_invert", (PyCFunction)_chop_invert, METH_NOARGS}, + {"chop_lighter", (PyCFunction)_chop_lighter, METH_VARARGS}, + {"chop_darker", (PyCFunction)_chop_darker, METH_VARARGS}, + {"chop_difference", (PyCFunction)_chop_difference, METH_VARARGS}, + {"chop_multiply", (PyCFunction)_chop_multiply, METH_VARARGS}, + {"chop_screen", (PyCFunction)_chop_screen, METH_VARARGS}, + {"chop_add", (PyCFunction)_chop_add, METH_VARARGS}, + {"chop_subtract", (PyCFunction)_chop_subtract, METH_VARARGS}, + {"chop_add_modulo", (PyCFunction)_chop_add_modulo, METH_VARARGS}, + {"chop_subtract_modulo", (PyCFunction)_chop_subtract_modulo, METH_VARARGS}, + {"chop_and", (PyCFunction)_chop_and, METH_VARARGS}, + {"chop_or", (PyCFunction)_chop_or, METH_VARARGS}, + {"chop_xor", (PyCFunction)_chop_xor, METH_VARARGS}, + {"chop_soft_light", (PyCFunction)_chop_soft_light, METH_VARARGS}, + {"chop_hard_light", (PyCFunction)_chop_hard_light, METH_VARARGS}, + {"chop_overlay", (PyCFunction)_chop_overlay, METH_VARARGS}, #endif #ifdef WITH_UNSHARPMASK /* Kevin Cazabon's unsharpmask extension */ - {"gaussian_blur", (PyCFunction)_gaussian_blur, 1}, - {"unsharp_mask", (PyCFunction)_unsharp_mask, 1}, + {"gaussian_blur", (PyCFunction)_gaussian_blur, METH_VARARGS}, + {"unsharp_mask", (PyCFunction)_unsharp_mask, METH_VARARGS}, #endif - {"box_blur", (PyCFunction)_box_blur, 1}, + {"box_blur", (PyCFunction)_box_blur, METH_VARARGS}, #ifdef WITH_EFFECTS /* Special effects */ - {"effect_spread", (PyCFunction)_effect_spread, 1}, + {"effect_spread", (PyCFunction)_effect_spread, METH_VARARGS}, #endif /* Misc. */ - {"new_block", (PyCFunction)_new_block, 1}, + {"new_block", (PyCFunction)_new_block, METH_VARARGS}, - {"save_ppm", (PyCFunction)_save_ppm, 1}, + {"save_ppm", (PyCFunction)_save_ppm, METH_VARARGS}, {NULL, NULL} /* sentinel */ }; @@ -3979,111 +3998,111 @@ PyImaging_MapBuffer(PyObject *self, PyObject *args); static PyMethodDef functions[] = { /* Object factories */ - {"alpha_composite", (PyCFunction)_alpha_composite, 1}, - {"blend", (PyCFunction)_blend, 1}, - {"fill", (PyCFunction)_fill, 1}, - {"new", (PyCFunction)_new, 1}, - {"merge", (PyCFunction)_merge, 1}, + {"alpha_composite", (PyCFunction)_alpha_composite, METH_VARARGS}, + {"blend", (PyCFunction)_blend, METH_VARARGS}, + {"fill", (PyCFunction)_fill, METH_VARARGS}, + {"new", (PyCFunction)_new, METH_VARARGS}, + {"merge", (PyCFunction)_merge, METH_VARARGS}, /* Functions */ - {"convert", (PyCFunction)_convert2, 1}, + {"convert", (PyCFunction)_convert2, METH_VARARGS}, /* Codecs */ - {"bcn_decoder", (PyCFunction)PyImaging_BcnDecoderNew, 1}, - {"bit_decoder", (PyCFunction)PyImaging_BitDecoderNew, 1}, - {"eps_encoder", (PyCFunction)PyImaging_EpsEncoderNew, 1}, - {"fli_decoder", (PyCFunction)PyImaging_FliDecoderNew, 1}, - {"gif_decoder", (PyCFunction)PyImaging_GifDecoderNew, 1}, - {"gif_encoder", (PyCFunction)PyImaging_GifEncoderNew, 1}, - {"hex_decoder", (PyCFunction)PyImaging_HexDecoderNew, 1}, - {"hex_encoder", (PyCFunction)PyImaging_EpsEncoderNew, 1}, /* EPS=HEX! */ + {"bcn_decoder", (PyCFunction)PyImaging_BcnDecoderNew, METH_VARARGS}, + {"bit_decoder", (PyCFunction)PyImaging_BitDecoderNew, METH_VARARGS}, + {"eps_encoder", (PyCFunction)PyImaging_EpsEncoderNew, METH_VARARGS}, + {"fli_decoder", (PyCFunction)PyImaging_FliDecoderNew, METH_VARARGS}, + {"gif_decoder", (PyCFunction)PyImaging_GifDecoderNew, METH_VARARGS}, + {"gif_encoder", (PyCFunction)PyImaging_GifEncoderNew, METH_VARARGS}, + {"hex_decoder", (PyCFunction)PyImaging_HexDecoderNew, METH_VARARGS}, + {"hex_encoder", (PyCFunction)PyImaging_EpsEncoderNew, METH_VARARGS}, /* EPS=HEX! */ #ifdef HAVE_LIBJPEG - {"jpeg_decoder", (PyCFunction)PyImaging_JpegDecoderNew, 1}, - {"jpeg_encoder", (PyCFunction)PyImaging_JpegEncoderNew, 1}, + {"jpeg_decoder", (PyCFunction)PyImaging_JpegDecoderNew, METH_VARARGS}, + {"jpeg_encoder", (PyCFunction)PyImaging_JpegEncoderNew, METH_VARARGS}, #endif #ifdef HAVE_OPENJPEG - {"jpeg2k_decoder", (PyCFunction)PyImaging_Jpeg2KDecoderNew, 1}, - {"jpeg2k_encoder", (PyCFunction)PyImaging_Jpeg2KEncoderNew, 1}, + {"jpeg2k_decoder", (PyCFunction)PyImaging_Jpeg2KDecoderNew, METH_VARARGS}, + {"jpeg2k_encoder", (PyCFunction)PyImaging_Jpeg2KEncoderNew, METH_VARARGS}, #endif #ifdef HAVE_LIBTIFF - {"libtiff_decoder", (PyCFunction)PyImaging_LibTiffDecoderNew, 1}, - {"libtiff_encoder", (PyCFunction)PyImaging_LibTiffEncoderNew, 1}, + {"libtiff_decoder", (PyCFunction)PyImaging_LibTiffDecoderNew, METH_VARARGS}, + {"libtiff_encoder", (PyCFunction)PyImaging_LibTiffEncoderNew, METH_VARARGS}, #endif - {"packbits_decoder", (PyCFunction)PyImaging_PackbitsDecoderNew, 1}, - {"pcd_decoder", (PyCFunction)PyImaging_PcdDecoderNew, 1}, - {"pcx_decoder", (PyCFunction)PyImaging_PcxDecoderNew, 1}, - {"pcx_encoder", (PyCFunction)PyImaging_PcxEncoderNew, 1}, - {"raw_decoder", (PyCFunction)PyImaging_RawDecoderNew, 1}, - {"raw_encoder", (PyCFunction)PyImaging_RawEncoderNew, 1}, - {"sgi_rle_decoder", (PyCFunction)PyImaging_SgiRleDecoderNew, 1}, - {"sun_rle_decoder", (PyCFunction)PyImaging_SunRleDecoderNew, 1}, - {"tga_rle_decoder", (PyCFunction)PyImaging_TgaRleDecoderNew, 1}, - {"tga_rle_encoder", (PyCFunction)PyImaging_TgaRleEncoderNew, 1}, - {"xbm_decoder", (PyCFunction)PyImaging_XbmDecoderNew, 1}, - {"xbm_encoder", (PyCFunction)PyImaging_XbmEncoderNew, 1}, + {"packbits_decoder", (PyCFunction)PyImaging_PackbitsDecoderNew, METH_VARARGS}, + {"pcd_decoder", (PyCFunction)PyImaging_PcdDecoderNew, METH_VARARGS}, + {"pcx_decoder", (PyCFunction)PyImaging_PcxDecoderNew, METH_VARARGS}, + {"pcx_encoder", (PyCFunction)PyImaging_PcxEncoderNew, METH_VARARGS}, + {"raw_decoder", (PyCFunction)PyImaging_RawDecoderNew, METH_VARARGS}, + {"raw_encoder", (PyCFunction)PyImaging_RawEncoderNew, METH_VARARGS}, + {"sgi_rle_decoder", (PyCFunction)PyImaging_SgiRleDecoderNew, METH_VARARGS}, + {"sun_rle_decoder", (PyCFunction)PyImaging_SunRleDecoderNew, METH_VARARGS}, + {"tga_rle_decoder", (PyCFunction)PyImaging_TgaRleDecoderNew, METH_VARARGS}, + {"tga_rle_encoder", (PyCFunction)PyImaging_TgaRleEncoderNew, METH_VARARGS}, + {"xbm_decoder", (PyCFunction)PyImaging_XbmDecoderNew, METH_VARARGS}, + {"xbm_encoder", (PyCFunction)PyImaging_XbmEncoderNew, METH_VARARGS}, #ifdef HAVE_LIBZ - {"zip_decoder", (PyCFunction)PyImaging_ZipDecoderNew, 1}, - {"zip_encoder", (PyCFunction)PyImaging_ZipEncoderNew, 1}, + {"zip_decoder", (PyCFunction)PyImaging_ZipDecoderNew, METH_VARARGS}, + {"zip_encoder", (PyCFunction)PyImaging_ZipEncoderNew, METH_VARARGS}, #endif /* Memory mapping */ #ifdef WITH_MAPPING - {"map_buffer", (PyCFunction)PyImaging_MapBuffer, 1}, + {"map_buffer", (PyCFunction)PyImaging_MapBuffer, METH_VARARGS}, #endif /* Display support */ #ifdef _WIN32 - {"display", (PyCFunction)PyImaging_DisplayWin32, 1}, - {"display_mode", (PyCFunction)PyImaging_DisplayModeWin32, 1}, - {"grabscreen_win32", (PyCFunction)PyImaging_GrabScreenWin32, 1}, - {"grabclipboard_win32", (PyCFunction)PyImaging_GrabClipboardWin32, 1}, - {"createwindow", (PyCFunction)PyImaging_CreateWindowWin32, 1}, - {"eventloop", (PyCFunction)PyImaging_EventLoopWin32, 1}, - {"listwindows", (PyCFunction)PyImaging_ListWindowsWin32, 1}, - {"drawwmf", (PyCFunction)PyImaging_DrawWmf, 1}, + {"display", (PyCFunction)PyImaging_DisplayWin32, METH_VARARGS}, + {"display_mode", (PyCFunction)PyImaging_DisplayModeWin32, METH_VARARGS}, + {"grabscreen_win32", (PyCFunction)PyImaging_GrabScreenWin32, METH_VARARGS}, + {"grabclipboard_win32", (PyCFunction)PyImaging_GrabClipboardWin32, METH_VARARGS}, + {"createwindow", (PyCFunction)PyImaging_CreateWindowWin32, METH_VARARGS}, + {"eventloop", (PyCFunction)PyImaging_EventLoopWin32, METH_VARARGS}, + {"listwindows", (PyCFunction)PyImaging_ListWindowsWin32, METH_VARARGS}, + {"drawwmf", (PyCFunction)PyImaging_DrawWmf, METH_VARARGS}, #endif #ifdef HAVE_XCB - {"grabscreen_x11", (PyCFunction)PyImaging_GrabScreenX11, 1}, + {"grabscreen_x11", (PyCFunction)PyImaging_GrabScreenX11, METH_VARARGS}, #endif /* Utilities */ - {"getcodecstatus", (PyCFunction)_getcodecstatus, 1}, + {"getcodecstatus", (PyCFunction)_getcodecstatus, METH_VARARGS}, /* Special effects (experimental) */ #ifdef WITH_EFFECTS - {"effect_mandelbrot", (PyCFunction)_effect_mandelbrot, 1}, - {"effect_noise", (PyCFunction)_effect_noise, 1}, - {"linear_gradient", (PyCFunction)_linear_gradient, 1}, - {"radial_gradient", (PyCFunction)_radial_gradient, 1}, - {"wedge", (PyCFunction)_linear_gradient, 1}, /* Compatibility */ + {"effect_mandelbrot", (PyCFunction)_effect_mandelbrot, METH_VARARGS}, + {"effect_noise", (PyCFunction)_effect_noise, METH_VARARGS}, + {"linear_gradient", (PyCFunction)_linear_gradient, METH_VARARGS}, + {"radial_gradient", (PyCFunction)_radial_gradient, METH_VARARGS}, + {"wedge", (PyCFunction)_linear_gradient, METH_VARARGS}, /* Compatibility */ #endif /* Drawing support stuff */ #ifdef WITH_IMAGEDRAW - {"font", (PyCFunction)_font_new, 1}, - {"draw", (PyCFunction)_draw_new, 1}, + {"font", (PyCFunction)_font_new, METH_VARARGS}, + {"draw", (PyCFunction)_draw_new, METH_VARARGS}, #endif /* Experimental path stuff */ #ifdef WITH_IMAGEPATH - {"path", (PyCFunction)PyPath_Create, 1}, + {"path", (PyCFunction)PyPath_Create, METH_VARARGS}, #endif /* Experimental arrow graphics stuff */ #ifdef WITH_ARROW - {"outline", (PyCFunction)PyOutline_Create, 1}, + {"outline", (PyCFunction)PyOutline_Create, METH_VARARGS}, #endif /* Resource management */ - {"get_stats", (PyCFunction)_get_stats, 1}, - {"reset_stats", (PyCFunction)_reset_stats, 1}, - {"get_alignment", (PyCFunction)_get_alignment, 1}, - {"get_block_size", (PyCFunction)_get_block_size, 1}, - {"get_blocks_max", (PyCFunction)_get_blocks_max, 1}, - {"set_alignment", (PyCFunction)_set_alignment, 1}, - {"set_block_size", (PyCFunction)_set_block_size, 1}, - {"set_blocks_max", (PyCFunction)_set_blocks_max, 1}, - {"clear_cache", (PyCFunction)_clear_cache, 1}, + {"get_stats", (PyCFunction)_get_stats, METH_VARARGS}, + {"reset_stats", (PyCFunction)_reset_stats, METH_VARARGS}, + {"get_alignment", (PyCFunction)_get_alignment, METH_VARARGS}, + {"get_block_size", (PyCFunction)_get_block_size, METH_VARARGS}, + {"get_blocks_max", (PyCFunction)_get_blocks_max, METH_VARARGS}, + {"set_alignment", (PyCFunction)_set_alignment, METH_VARARGS}, + {"set_block_size", (PyCFunction)_set_block_size, METH_VARARGS}, + {"set_blocks_max", (PyCFunction)_set_blocks_max, METH_VARARGS}, + {"clear_cache", (PyCFunction)_clear_cache, METH_VARARGS}, {NULL, NULL} /* sentinel */ }; diff --git a/src/_imagingcms.c b/src/_imagingcms.c index 314150420..1446bd02b 100644 --- a/src/_imagingcms.c +++ b/src/_imagingcms.c @@ -959,25 +959,25 @@ _is_intent_supported(CmsProfileObject *self, int clut) { static PyMethodDef pyCMSdll_methods[] = { - {"profile_open", cms_profile_open, 1}, - {"profile_frombytes", cms_profile_fromstring, 1}, - {"profile_fromstring", cms_profile_fromstring, 1}, - {"profile_tobytes", cms_profile_tobytes, 1}, + {"profile_open", cms_profile_open, METH_VARARGS}, + {"profile_frombytes", cms_profile_fromstring, METH_VARARGS}, + {"profile_fromstring", cms_profile_fromstring, METH_VARARGS}, + {"profile_tobytes", cms_profile_tobytes, METH_VARARGS}, /* profile and transform functions */ - {"buildTransform", buildTransform, 1}, - {"buildProofTransform", buildProofTransform, 1}, - {"createProfile", createProfile, 1}, + {"buildTransform", buildTransform, METH_VARARGS}, + {"buildProofTransform", buildProofTransform, METH_VARARGS}, + {"createProfile", createProfile, METH_VARARGS}, /* platform specific tools */ #ifdef _WIN32 - {"get_display_profile_win32", cms_get_display_profile_win32, 1}, + {"get_display_profile_win32", cms_get_display_profile_win32, METH_VARARGS}, #endif {NULL, NULL}}; static struct PyMethodDef cms_profile_methods[] = { - {"is_intent_supported", (PyCFunction)cms_profile_is_intent_supported, 1}, + {"is_intent_supported", (PyCFunction)cms_profile_is_intent_supported, METH_VARARGS}, {NULL, NULL} /* sentinel */ }; diff --git a/src/_imagingft.c b/src/_imagingft.c index 73f0f6362..2427bc685 100644 --- a/src/_imagingft.c +++ b/src/_imagingft.c @@ -277,7 +277,8 @@ text_layout_raqm( direction = RAQM_DIRECTION_LTR; } else if (strcmp(dir, "ttb") == 0) { direction = RAQM_DIRECTION_TTB; -#if !defined(RAQM_VERSION_ATLEAST) || !RAQM_VERSION_ATLEAST(0, 7, 0) +#if !defined(RAQM_VERSION_ATLEAST) + /* RAQM_VERSION_ATLEAST was added in Raqm 0.7.0 */ PyErr_SetString( PyExc_ValueError, "libraqm 0.7 or greater required for 'ttb' direction"); diff --git a/src/_webp.c b/src/_webp.c index 4d51d99df..6c357dbb0 100644 --- a/src/_webp.c +++ b/src/_webp.c @@ -663,7 +663,7 @@ WebPEncode_wrapper(PyObject *self, PyObject *args) { WebPPictureFree(&pic); if (!ok) { - PyErr_SetString(PyExc_ValueError, "encoding error"); + PyErr_Format(PyExc_ValueError, "encoding error %d", (&pic)->error_code); return NULL; } output = writer.mem; diff --git a/src/decode.c b/src/decode.c index 5d64bd0b9..91bfabf34 100644 --- a/src/decode.c +++ b/src/decode.c @@ -34,9 +34,10 @@ #include "libImaging/Imaging.h" +#include "libImaging/Bit.h" +#include "libImaging/Bcn.h" #include "libImaging/Gif.h" #include "libImaging/Raw.h" -#include "libImaging/Bit.h" #include "libImaging/Sgi.h" /* -------------------------------------------------------------------- */ @@ -199,7 +200,7 @@ _setimage(ImagingDecoderObject *decoder, PyObject *args) { state->bytes = (state->bits * state->xsize + 7) / 8; } /* malloc check ok, overflow checked above */ - state->buffer = (UINT8 *)malloc(state->bytes); + state->buffer = (UINT8 *)calloc(1, state->bytes); if (!state->buffer) { return ImagingError_MemoryError(); } @@ -239,10 +240,10 @@ _get_pulls_fd(ImagingDecoderObject *decoder) { } static struct PyMethodDef methods[] = { - {"decode", (PyCFunction)_decode, 1}, - {"cleanup", (PyCFunction)_decode_cleanup, 1}, - {"setimage", (PyCFunction)_setimage, 1}, - {"setfd", (PyCFunction)_setfd, 1}, + {"decode", (PyCFunction)_decode, METH_VARARGS}, + {"cleanup", (PyCFunction)_decode_cleanup, METH_VARARGS}, + {"setimage", (PyCFunction)_setimage, METH_VARARGS}, + {"setfd", (PyCFunction)_setfd, METH_VARARGS}, {NULL, NULL} /* sentinel */ }; @@ -298,7 +299,7 @@ get_unpacker(ImagingDecoderObject *decoder, const char *mode, const char *rawmod unpack = ImagingFindUnpacker(mode, rawmode, &bits); if (!unpack) { Py_DECREF(decoder); - PyErr_SetString(PyExc_ValueError, "unknown raw mode"); + PyErr_SetString(PyExc_ValueError, "unknown raw mode for given image mode"); return -1; } @@ -359,8 +360,8 @@ PyImaging_BcnDecoderNew(PyObject *self, PyObject *args) { char *mode; char *actual; int n = 0; - int ystep = 1; - if (!PyArg_ParseTuple(args, "s|ii", &mode, &n, &ystep)) { + char *pixel_format = ""; + if (!PyArg_ParseTuple(args, "si|s", &mode, &n, &pixel_format)) { return NULL; } @@ -368,13 +369,15 @@ PyImaging_BcnDecoderNew(PyObject *self, PyObject *args) { case 1: /* BC1: 565 color, 1-bit alpha */ case 2: /* BC2: 565 color, 4-bit alpha */ case 3: /* BC3: 565 color, 2-endpoint 8-bit interpolated alpha */ - case 5: /* BC5: 2-channel 8-bit via 2 BC3 alpha blocks */ case 7: /* BC7: 4-channel 8-bit via everything */ actual = "RGBA"; break; case 4: /* BC4: 1-channel 8-bit via 1 BC3 alpha block */ actual = "L"; break; + case 5: /* BC5: 2-channel 8-bit via 2 BC3 alpha blocks */ + actual = "RGB"; + break; case 6: /* BC6: 3-channel 16-bit float */ /* TODO: support 4-channel floating point images */ actual = "RGBAF"; @@ -389,14 +392,14 @@ PyImaging_BcnDecoderNew(PyObject *self, PyObject *args) { return NULL; } - decoder = PyImaging_DecoderNew(0); + decoder = PyImaging_DecoderNew(sizeof(char *)); if (decoder == NULL) { return NULL; } decoder->decode = ImagingBcnDecode; decoder->state.state = n; - decoder->state.ystep = ystep; + ((BCNSTATE *)decoder->state.context)->pixel_format = pixel_format; return (PyObject *)decoder; } @@ -430,7 +433,8 @@ PyImaging_GifDecoderNew(PyObject *self, PyObject *args) { char *mode; int bits = 8; int interlace = 0; - if (!PyArg_ParseTuple(args, "s|ii", &mode, &bits, &interlace)) { + int transparency = -1; + if (!PyArg_ParseTuple(args, "s|iii", &mode, &bits, &interlace, &transparency)) { return NULL; } @@ -448,6 +452,7 @@ PyImaging_GifDecoderNew(PyObject *self, PyObject *args) { ((GIFDECODERSTATE *)decoder->state.context)->bits = bits; ((GIFDECODERSTATE *)decoder->state.context)->interlace = interlace; + ((GIFDECODERSTATE *)decoder->state.context)->transparency = transparency; return (PyObject *)decoder; } @@ -497,7 +502,7 @@ PyImaging_LibTiffDecoderNew(PyObject *self, PyObject *args) { char *rawmode; char *compname; int fp; - uint32 ifdoffset; + uint32_t ifdoffset; if (!PyArg_ParseTuple(args, "sssiI", &mode, &rawmode, &compname, &fp, &ifdoffset)) { return NULL; diff --git a/src/display.c b/src/display.c index 3541655cf..0ce10e249 100644 --- a/src/display.c +++ b/src/display.c @@ -224,14 +224,14 @@ _tobytes(ImagingDisplayObject *display, PyObject *args) { } static struct PyMethodDef methods[] = { - {"draw", (PyCFunction)_draw, 1}, - {"expose", (PyCFunction)_expose, 1}, - {"paste", (PyCFunction)_paste, 1}, - {"query_palette", (PyCFunction)_query_palette, 1}, - {"getdc", (PyCFunction)_getdc, 1}, - {"releasedc", (PyCFunction)_releasedc, 1}, - {"frombytes", (PyCFunction)_frombytes, 1}, - {"tobytes", (PyCFunction)_tobytes, 1}, + {"draw", (PyCFunction)_draw, METH_VARARGS}, + {"expose", (PyCFunction)_expose, METH_VARARGS}, + {"paste", (PyCFunction)_paste, METH_VARARGS}, + {"query_palette", (PyCFunction)_query_palette, METH_VARARGS}, + {"getdc", (PyCFunction)_getdc, METH_VARARGS}, + {"releasedc", (PyCFunction)_releasedc, METH_VARARGS}, + {"frombytes", (PyCFunction)_frombytes, METH_VARARGS}, + {"tobytes", (PyCFunction)_tobytes, METH_VARARGS}, {NULL, NULL} /* sentinel */ }; diff --git a/src/encode.c b/src/encode.c index f92ba62c2..daa806ff4 100644 --- a/src/encode.c +++ b/src/encode.c @@ -264,7 +264,7 @@ _setimage(ImagingEncoderObject *encoder, PyObject *args) { } state->bytes = (state->bits * state->xsize + 7) / 8; /* malloc check ok, overflow checked above */ - state->buffer = (UINT8 *)malloc(state->bytes); + state->buffer = (UINT8 *)calloc(1, state->bytes); if (!state->buffer) { return ImagingError_MemoryError(); } @@ -304,12 +304,12 @@ _get_pushes_fd(ImagingEncoderObject *encoder) { } static struct PyMethodDef methods[] = { - {"encode", (PyCFunction)_encode, 1}, - {"cleanup", (PyCFunction)_encode_cleanup, 1}, - {"encode_to_file", (PyCFunction)_encode_to_file, 1}, - {"encode_to_pyfd", (PyCFunction)_encode_to_pyfd, 1}, - {"setimage", (PyCFunction)_setimage, 1}, - {"setfd", (PyCFunction)_setfd, 1}, + {"encode", (PyCFunction)_encode, METH_VARARGS}, + {"cleanup", (PyCFunction)_encode_cleanup, METH_VARARGS}, + {"encode_to_file", (PyCFunction)_encode_to_file, METH_VARARGS}, + {"encode_to_pyfd", (PyCFunction)_encode_to_pyfd, METH_VARARGS}, + {"setimage", (PyCFunction)_setimage, METH_VARARGS}, + {"setfd", (PyCFunction)_setfd, METH_VARARGS}, {NULL, NULL} /* sentinel */ }; @@ -644,10 +644,10 @@ PyImaging_LibTiffEncoderNew(PyObject *self, PyObject *args) { int key_int, status, is_core_tag, is_var_length, num_core_tags, i; TIFFDataType type = TIFF_NOTYPE; // This list also exists in TiffTags.py - const int core_tags[] = {256, 257, 258, 259, 262, 263, 266, 269, 274, - 277, 278, 280, 281, 340, 341, 282, 283, 284, - 286, 287, 296, 297, 320, 321, 338, 32995, 32998, - 32996, 339, 32997, 330, 531, 530, 65537}; + const int core_tags[] = {256, 257, 258, 259, 262, 263, 266, 269, 274, + 277, 278, 280, 281, 340, 341, 282, 283, 284, + 286, 287, 296, 297, 320, 321, 338, 32995, 32998, + 32996, 339, 32997, 330, 531, 530, 65537, 301, 532}; Py_ssize_t tags_size; PyObject *item; @@ -790,7 +790,7 @@ PyImaging_LibTiffEncoderNew(PyObject *self, PyObject *args) { int stride = 256; if (len != 768) { PyErr_SetString( - PyExc_ValueError, "Requiring 768 items for for Colormap"); + PyExc_ValueError, "Requiring 768 items for Colormap"); return NULL; } UINT16 *av; diff --git a/src/libImaging/Bcn.h b/src/libImaging/Bcn.h new file mode 100644 index 000000000..1a6fbee45 --- /dev/null +++ b/src/libImaging/Bcn.h @@ -0,0 +1,3 @@ +typedef struct { + char *pixel_format; +} BCNSTATE; diff --git a/src/libImaging/BcnDecode.c b/src/libImaging/BcnDecode.c index b6a4cbadc..22b36eb7a 100644 --- a/src/libImaging/BcnDecode.c +++ b/src/libImaging/BcnDecode.c @@ -13,6 +13,8 @@ #include "Imaging.h" +#include "Bcn.h" + typedef struct { UINT8 r, g, b, a; } rgba; @@ -35,6 +37,11 @@ typedef struct { UINT8 lut[6]; } bc3_alpha; +typedef struct { + INT8 a0, a1; + UINT8 lut[6]; +} bc5s_alpha; + #define LOAD16(p) (p)[0] | ((p)[1] << 8) #define LOAD32(p) (p)[0] | ((p)[1] << 8) | ((p)[2] << 16) | ((p)[3] << 24) @@ -46,11 +53,6 @@ bc1_color_load(bc1_color *dst, const UINT8 *src) { dst->lut = LOAD32(src + 4); } -static void -bc3_alpha_load(bc3_alpha *dst, const UINT8 *src) { - memcpy(dst, src, sizeof(bc3_alpha)); -} - static rgba decode_565(UINT16 x) { rgba c; @@ -69,7 +71,7 @@ decode_565(UINT16 x) { } static void -decode_bc1_color(rgba *dst, const UINT8 *src) { +decode_bc1_color(rgba *dst, const UINT8 *src, int separate_alpha) { bc1_color col; rgba p[4]; int n, cw; @@ -84,7 +86,10 @@ decode_bc1_color(rgba *dst, const UINT8 *src) { r1 = p[1].r; g1 = p[1].g; b1 = p[1].b; - if (col.c0 > col.c1) { + + + /* NOTE: BC2 and BC3 reuse BC1 color blocks but always act like c0 > c1 */ + if (col.c0 > col.c1 || separate_alpha) { p[2].r = (2 * r0 + 1 * r1) / 3; p[2].g = (2 * g0 + 1 * g1) / 3; p[2].b = (2 * b0 + 1 * b1) / 3; @@ -110,15 +115,26 @@ decode_bc1_color(rgba *dst, const UINT8 *src) { } static void -decode_bc3_alpha(char *dst, const UINT8 *src, int stride, int o) { - bc3_alpha b; +decode_bc3_alpha(char *dst, const UINT8 *src, int stride, int o, int sign) { UINT16 a0, a1; UINT8 a[8]; - int n, lut, aw; - bc3_alpha_load(&b, src); + int n, lut1, lut2, aw; + if (sign == 1) { + bc5s_alpha b; + memcpy(&b, src, sizeof(bc5s_alpha)); + a0 = (b.a0 + 255) / 2; + a1 = (b.a1 + 255) / 2; + lut1 = b.lut[0] | (b.lut[1] << 8) | (b.lut[2] << 16); + lut2 = b.lut[3] | (b.lut[4] << 8) | (b.lut[5] << 16); + } else { + bc3_alpha b; + memcpy(&b, src, sizeof(bc3_alpha)); + a0 = b.a0; + a1 = b.a1; + lut1 = b.lut[0] | (b.lut[1] << 8) | (b.lut[2] << 16); + lut2 = b.lut[3] | (b.lut[4] << 8) | (b.lut[5] << 16); + } - a0 = b.a0; - a1 = b.a1; a[0] = (UINT8)a0; a[1] = (UINT8)a1; if (a0 > a1) { @@ -136,27 +152,25 @@ decode_bc3_alpha(char *dst, const UINT8 *src, int stride, int o) { a[6] = 0; a[7] = 0xff; } - lut = b.lut[0] | (b.lut[1] << 8) | (b.lut[2] << 16); for (n = 0; n < 8; n++) { - aw = 7 & (lut >> (3 * n)); + aw = 7 & (lut1 >> (3 * n)); dst[stride * n + o] = a[aw]; } - lut = b.lut[3] | (b.lut[4] << 8) | (b.lut[5] << 16); for (n = 0; n < 8; n++) { - aw = 7 & (lut >> (3 * n)); + aw = 7 & (lut2 >> (3 * n)); dst[stride * (8 + n) + o] = a[aw]; } } static void decode_bc1_block(rgba *col, const UINT8 *src) { - decode_bc1_color(col, src); + decode_bc1_color(col, src, 0); } static void decode_bc2_block(rgba *col, const UINT8 *src) { int n, bitI, byI, av; - decode_bc1_color(col, src + 8); + decode_bc1_color(col, src + 8, 1); for (n = 0; n < 16; n++) { bitI = n * 4; byI = bitI >> 3; @@ -168,19 +182,19 @@ decode_bc2_block(rgba *col, const UINT8 *src) { static void decode_bc3_block(rgba *col, const UINT8 *src) { - decode_bc1_color(col, src + 8); - decode_bc3_alpha((char *)col, src, sizeof(col[0]), 3); + decode_bc1_color(col, src + 8, 1); + decode_bc3_alpha((char *)col, src, sizeof(col[0]), 3, 0); } static void decode_bc4_block(lum *col, const UINT8 *src) { - decode_bc3_alpha((char *)col, src, sizeof(col[0]), 0); + decode_bc3_alpha((char *)col, src, sizeof(col[0]), 0, 0); } static void -decode_bc5_block(rgba *col, const UINT8 *src) { - decode_bc3_alpha((char *)col, src, sizeof(col[0]), 0); - decode_bc3_alpha((char *)col, src + 8, sizeof(col[0]), 1); +decode_bc5_block(rgba *col, const UINT8 *src, int sign) { + decode_bc3_alpha((char *)col, src, sizeof(col[0]), 0, sign); + decode_bc3_alpha((char *)col, src + 8, sizeof(col[0]), 1, sign); } /* BC6 and BC7 are described here: @@ -810,7 +824,7 @@ put_block(Imaging im, ImagingCodecState state, const char *col, int sz, int C) { static int decode_bcn( - Imaging im, ImagingCodecState state, const UINT8 *src, int bytes, int N, int C) { + Imaging im, ImagingCodecState state, const UINT8 *src, int bytes, int N, int C, char *pixel_format) { int ymax = state->ysize + state->yoff; const UINT8 *ptr = src; switch (N) { @@ -833,7 +847,19 @@ decode_bcn( DECODE_LOOP(2, 16, rgba); DECODE_LOOP(3, 16, rgba); DECODE_LOOP(4, 8, lum); - DECODE_LOOP(5, 16, rgba); + case 5: + while (bytes >= 16) { + rgba col[16]; + memset(col, 0, 16 * sizeof(col[0])); + decode_bc5_block(col, ptr, strcmp(pixel_format, "BC5S") == 0 ? 1 : 0); + put_block(im, state, (const char *)col, sizeof(col[0]), C); + ptr += 16; + bytes -= 16; + if (state->y >= ymax) { + return -1; + } + } + break; case 6: while (bytes >= 16) { rgb32f col[16]; @@ -846,7 +872,7 @@ decode_bcn( } } break; - DECODE_LOOP(7, 16, rgba); + DECODE_LOOP(7, 16, rgba); #undef DECODE_LOOP } return (int)(ptr - src); @@ -857,9 +883,7 @@ ImagingBcnDecode(Imaging im, ImagingCodecState state, UINT8 *buf, Py_ssize_t byt int N = state->state & 0xf; int width = state->xsize; int height = state->ysize; - if ((width & 3) | (height & 3)) { - return decode_bcn(im, state, buf, bytes, N, 1); - } else { - return decode_bcn(im, state, buf, bytes, N, 0); - } + int C = (width & 3) | (height & 3) ? 1 : 0; + char *pixel_format = ((BCNSTATE *)state->context)->pixel_format; + return decode_bcn(im, state, buf, bytes, N, C, pixel_format); } diff --git a/src/libImaging/BoxBlur.c b/src/libImaging/BoxBlur.c index 88862eb73..2e45a3358 100644 --- a/src/libImaging/BoxBlur.c +++ b/src/libImaging/BoxBlur.c @@ -287,7 +287,7 @@ ImagingGaussianBlur(Imaging imOut, Imaging imIn, float radius, int passes) { float sigma2, L, l, a; sigma2 = radius * radius / passes; - // from http://www.mia.uni-saarland.de/Publications/gwosdek-ssvm11.pdf + // from https://www.mia.uni-saarland.de/Publications/gwosdek-ssvm11.pdf // [7] Box length. L = sqrt(12.0 * sigma2 + 1.0); // [11] Integer part of box radius. diff --git a/src/libImaging/Convert.c b/src/libImaging/Convert.c index 8c7be36a2..9012cfcd7 100644 --- a/src/libImaging/Convert.c +++ b/src/libImaging/Convert.c @@ -1594,9 +1594,8 @@ convert( #ifdef notdef return (Imaging)ImagingError_ValueError("conversion not supported"); #else - static char buf[256]; - /* FIXME: may overflow if mode is too large */ - sprintf(buf, "conversion from %s to %s not supported", imIn->mode, mode); + static char buf[100]; + snprintf(buf, 100, "conversion from %.10s to %.10s not supported", imIn->mode, mode); return (Imaging)ImagingError_ValueError(buf); #endif } @@ -1645,11 +1644,11 @@ ImagingConvertTransparent(Imaging imIn, const char *mode, int r, int g, int b) { } #else { - static char buf[256]; - /* FIXME: may overflow if mode is too large */ - sprintf( + static char buf[100]; + snprintf( buf, - "conversion from %s to %s not supported in convert_transparent", + 100, + "conversion from %.10s to %.10s not supported in convert_transparent", imIn->mode, mode); return (Imaging)ImagingError_ValueError(buf); diff --git a/src/libImaging/Draw.c b/src/libImaging/Draw.c index b6f63b7e8..161895dc6 100644 --- a/src/libImaging/Draw.c +++ b/src/libImaging/Draw.c @@ -734,7 +734,7 @@ ImagingDrawRectangle( int ImagingDrawPolygon(Imaging im, int count, int *xy, const void *ink_, int fill, int op) { - int i, n; + int i, n, x0, y0, x1, y1; DRAW *draw; INT32 ink; @@ -753,10 +753,28 @@ ImagingDrawPolygon(Imaging im, int count, int *xy, const void *ink_, int fill, i return -1; } for (i = n = 0; i < count - 1; i++) { - add_edge(&e[n++], xy[i + i], xy[i + i + 1], xy[i + i + 2], xy[i + i + 3]); + x0 = xy[i * 2]; + y0 = xy[i * 2 + 1]; + x1 = xy[i * 2 + 2]; + y1 = xy[i * 2 + 3]; + if (y0 == y1 && i != 0 && y0 == xy[i * 2 - 1]) { + // This is a horizontal line, + // that immediately follows another horizontal line + Edge *last_e = &e[n-1]; + if (x1 > x0 && x0 > xy[i * 2 - 2]) { + // They are both increasing in x + last_e->xmax = x1; + continue; + } else if (x1 < x0 && x0 < xy[i * 2 - 2]) { + // They are both decreasing in x + last_e->xmin = x1; + continue; + } + } + add_edge(&e[n++], x0, y0, x1, y1); } - if (xy[i + i] != xy[0] || xy[i + i + 1] != xy[1]) { - add_edge(&e[n++], xy[i + i], xy[i + i + 1], xy[0], xy[1]); + if (xy[i * 2] != xy[0] || xy[i * 2 + 1] != xy[1]) { + add_edge(&e[n++], xy[i * 2], xy[i * 2 + 1], xy[0], xy[1]); } draw->polygon(im, n, e, ink, 0); free(e); @@ -764,9 +782,9 @@ ImagingDrawPolygon(Imaging im, int count, int *xy, const void *ink_, int fill, i } else { /* Outline */ for (i = 0; i < count - 1; i++) { - draw->line(im, xy[i + i], xy[i + i + 1], xy[i + i + 2], xy[i + i + 3], ink); + draw->line(im, xy[i * 2], xy[i * 2 + 1], xy[i * 2 + 2], xy[i * 2 + 3], ink); } - draw->line(im, xy[i + i], xy[i + i + 1], xy[0], xy[1], ink); + draw->line(im, xy[i * 2], xy[i * 2 + 1], xy[0], xy[1], ink); } return 0; @@ -1347,6 +1365,22 @@ pie_init(clip_ellipse_state *s, int32_t a, int32_t b, int32_t w, float al, float s->root->l = lc; s->root->r = rc; s->root->type = ar - al < 180 ? CT_AND : CT_OR; + + // add one more semiplane to avoid spikes + if (ar - al < 90) { + clip_node *old_root = s->root; + clip_node *spike_clipper = s->nodes + s->node_count++; + s->root = s->nodes + s->node_count++; + s->root->l = old_root; + s->root->r = spike_clipper; + s->root->type = CT_AND; + + spike_clipper->l = spike_clipper->r = NULL; + spike_clipper->type = CT_CLIP; + spike_clipper->a = (xl + xr) / 2.0; + spike_clipper->b = (yl + yr) / 2.0; + spike_clipper->c = 0; + } } void diff --git a/src/libImaging/FliDecode.c b/src/libImaging/FliDecode.c index e9000fc99..3a6030703 100644 --- a/src/libImaging/FliDecode.c +++ b/src/libImaging/FliDecode.c @@ -243,6 +243,11 @@ ImagingFliDecode(Imaging im, ImagingCodecState state, UINT8 *buf, Py_ssize_t byt return -1; } advance = I32(ptr); + if (advance == 0 ) { + // If there's no advance, we're in an infinite loop + state->errcode = IMAGING_CODEC_BROKEN; + return -1; + } if (advance < 0 || advance > bytes) { state->errcode = IMAGING_CODEC_OVERRUN; return -1; diff --git a/src/libImaging/Gif.h b/src/libImaging/Gif.h index a85ce2b6e..91132e2e6 100644 --- a/src/libImaging/Gif.h +++ b/src/libImaging/Gif.h @@ -9,10 +9,10 @@ /* Max size for a LZW code word. */ -#define GIFBITS 12 +#define GIFBITS 12 -#define GIFTABLE (1 << GIFBITS) -#define GIFBUFFER (1 << GIFBITS) +#define GIFTABLE (1<next < GIFTABLE) { /* We'll only add this symbol if we have room - for it (take advise, Netscape!) */ + for it (take the advice, Netscape!) */ context->data[context->next] = c; context->link[context->next] = context->lastcode; @@ -248,29 +248,33 @@ ImagingGifDecode(Imaging im, ImagingCodecState state, UINT8 *buffer, Py_ssize_t /* To squeeze some extra pixels out of this loop, we test for some common cases and handle them separately. */ - /* FIXME: should we handle the transparency index in here??? */ - - if (i == 1) { - if (state->x < state->xsize - 1) { - /* Single pixel, not at the end of the line. */ - *out++ = p[0]; - state->x++; + /* If we have transparency, we need to use the regular loop. */ + if (context->transparency == -1) { + if (i == 1) { + if (state->x < state->xsize - 1) { + /* Single pixel, not at the end of the line. */ + *out++ = p[0]; + state->x++; + continue; + } + } else if (state->x + i <= state->xsize) { + /* This string fits into current line. */ + memcpy(out, p, i); + out += i; + state->x += i; + if (state->x == state->xsize) { + NEWLINE(state, context); + } continue; } - } else if (state->x + i <= state->xsize) { - /* This string fits into current line. */ - memcpy(out, p, i); - out += i; - state->x += i; - if (state->x == state->xsize) { - NEWLINE(state, context); - } - continue; } /* No shortcut, copy pixel by pixel */ for (c = 0; c < i; c++) { - *out++ = p[c]; + if (p[c] != context->transparency) { + *out = p[c]; + } + out++; if (++state->x >= state->xsize) { NEWLINE(state, context); } diff --git a/src/libImaging/GifEncode.c b/src/libImaging/GifEncode.c index e9064b3f5..f23245405 100644 --- a/src/libImaging/GifEncode.c +++ b/src/libImaging/GifEncode.c @@ -10,6 +10,7 @@ * 98-07-09 fl added interlace write support * 99-02-07 fl rewritten, now uses a run-length encoding strategy * 99-02-08 fl improved run-length encoding for long runs + * 2020-12-12 rdg Reworked for LZW compression. * * Copyright (c) Secret Labs AB 1997-99. * Copyright (c) Fredrik Lundh 1997. @@ -21,136 +22,206 @@ #include "Gif.h" -/* codes from 0 to 255 are literals */ -#define CLEAR_CODE 256 -#define EOF_CODE 257 -#define FIRST_CODE 258 -#define LAST_CODE 511 +enum { INIT, ENCODE, FINISH }; -enum { INIT, ENCODE, ENCODE_EOF, FLUSH, EXIT }; +/* GIF LZW encoder by Raymond Gardner. */ +/* Released here under PIL license. */ -/* to make things a little less complicated, we use a simple output - queue to hold completed blocks. the following inlined function - adds a byte to the current block. it allocates a new block if - necessary. */ +/* This LZW encoder conforms to the GIF LZW format specified in the original + * Compuserve GIF 87a and GIF 89a specifications (see e.g. + * https://www.w3.org/Graphics/GIF/spec-gif87.txt Appendix C and + * https://www.w3.org/Graphics/GIF/spec-gif89a.txt Appendix F). + */ -static inline int -emit(GIFENCODERSTATE *context, int byte) { - /* write a byte to the output buffer */ +/* Return values */ +#define GLZW_OK 0 +#define GLZW_NO_INPUT_AVAIL 1 +#define GLZW_NO_OUTPUT_AVAIL 2 +#define GLZW_INTERNAL_ERROR 3 - if (!context->block || context->block->size == 255) { - GIFENCODERBLOCK *block; +#define CODE_LIMIT 4096 - /* no room in the current block (or no current block); - allocate a new one */ +/* Values of entry_state */ +enum { LZW_INITIAL, LZW_TRY_IN1, LZW_TRY_IN2, LZW_TRY_OUT1, LZW_TRY_OUT2, + LZW_FINISHED }; - /* add current block to end of flush queue */ - if (context->block) { - block = context->flush; - while (block && block->next) { - block = block->next; - } - if (block) { - block->next = context->block; - } else { - context->flush = context->block; - } - } +/* Values of control_state */ +enum { PUT_HEAD, PUT_INIT_CLEAR, PUT_CLEAR, PUT_LAST_HEAD, PUT_END }; - /* get a new block */ - if (context->free) { - block = context->free; - context->free = NULL; - } else { - /* malloc check ok, small constant allocation */ - block = malloc(sizeof(GIFENCODERBLOCK)); - if (!block) { - return 0; - } - } - - block->size = 0; - block->next = NULL; - - context->block = block; - } - - /* write new byte to block */ - context->block->data[context->block->size++] = byte; - - return 1; +static void glzwe_reset(GIFENCODERSTATE *st) { + st->next_code = st->end_code + 1; + st->max_code = 2 * st->clear_code - 1; + st->code_width = st->bits + 1; + memset(st->codes, 0, sizeof(st->codes)); } -/* write a code word to the current block. this is a macro to make - sure it's inlined on all platforms */ +static void glzwe_init(GIFENCODERSTATE *st) { + st->clear_code = 1 << st->bits; + st->end_code = st->clear_code + 1; + glzwe_reset(st); + st->entry_state = LZW_INITIAL; + st->buf_bits_left = 8; + st->code_buffer = 0; +} -#define EMIT(code) \ - { \ - context->bitbuffer |= ((INT32)(code)) << context->bitcount; \ - context->bitcount += 9; \ - while (context->bitcount >= 8) { \ - if (!emit(context, (UINT8)context->bitbuffer)) { \ - state->errcode = IMAGING_CODEC_MEMORY; \ - return 0; \ - } \ - context->bitbuffer >>= 8; \ - context->bitcount -= 8; \ - } \ - } - -/* write a run. we use a combination of literals and combinations of - literals. this can give quite decent compression for images with - long stretches of identical pixels. but remember: if you want - really good compression, use another file format. */ - -#define EMIT_RUN(label) \ - { \ - label: \ - while (context->count > 0) { \ - int run = 2; \ - EMIT(context->last); \ - context->count--; \ - if (state->count++ == LAST_CODE) { \ - EMIT(CLEAR_CODE); \ - state->count = FIRST_CODE; \ - goto label; \ - } \ - while (context->count >= run) { \ - EMIT(state->count - 1); \ - context->count -= run; \ - run++; \ - if (state->count++ == LAST_CODE) { \ - EMIT(CLEAR_CODE); \ - state->count = FIRST_CODE; \ - goto label; \ - } \ - } \ - if (context->count > 1) { \ - EMIT(state->count - 1 - (run - context->count)); \ - context->count = 0; \ - if (state->count++ == LAST_CODE) { \ - EMIT(CLEAR_CODE); \ - state->count = FIRST_CODE; \ - } \ - break; \ - } \ - } \ +static int glzwe(GIFENCODERSTATE *st, const UINT8 *in_ptr, UINT8 *out_ptr, + UINT32 *in_avail, UINT32 *out_avail, + UINT32 end_of_data) { + switch (st->entry_state) { + + case LZW_TRY_IN1: +get_first_byte: + if (!*in_avail) { + if (end_of_data) { + goto end_of_data; + } + st->entry_state = LZW_TRY_IN1; + return GLZW_NO_INPUT_AVAIL; + } + st->head = *in_ptr++; + (*in_avail)--; + + case LZW_TRY_IN2: +encode_loop: + if (!*in_avail) { + if (end_of_data) { + st->code = st->head; + st->put_state = PUT_LAST_HEAD; + goto put_code; + } + st->entry_state = LZW_TRY_IN2; + return GLZW_NO_INPUT_AVAIL; + } + st->tail = *in_ptr++; + (*in_avail)--; + + /* Knuth TAOCP vol 3 sec. 6.4 algorithm D. */ + /* Hash found experimentally to be pretty good. */ + /* This works ONLY with TABLE_SIZE a power of 2. */ + st->probe = ((st->head ^ (st->tail << 6)) * 31) & (TABLE_SIZE - 1); + while (st->codes[st->probe]) { + if ((st->codes[st->probe] & 0xFFFFF) == + ((st->head << 8) | st->tail)) { + st->head = st->codes[st->probe] >> 20; + goto encode_loop; + } else { + /* Reprobe decrement must be nonzero and relatively prime to table + * size. So, any odd positive number for power-of-2 size. */ + if ((st->probe -= ((st->tail << 2) | 1)) < 0) { + st->probe += TABLE_SIZE; + } + } + } + /* Key not found, probe is at empty slot. */ + st->code = st->head; + st->put_state = PUT_HEAD; + goto put_code; +insert_code_or_clear: /* jump here after put_code */ + if (st->next_code < CODE_LIMIT) { + st->codes[st->probe] = (st->next_code << 20) | + (st->head << 8) | st->tail; + if (st->next_code > st->max_code) { + st->max_code = st->max_code * 2 + 1; + st->code_width++; + } + st->next_code++; + } else { + st->code = st->clear_code; + st->put_state = PUT_CLEAR; + goto put_code; +reset_after_clear: /* jump here after put_code */ + glzwe_reset(st); + } + st->head = st->tail; + goto encode_loop; + + case LZW_INITIAL: + glzwe_reset(st); + st->code = st->clear_code; + st->put_state = PUT_INIT_CLEAR; +put_code: + st->code_bits_left = st->code_width; +check_buf_bits: + if (!st->buf_bits_left) { /* out buffer full */ + + case LZW_TRY_OUT1: + if (!*out_avail) { + st->entry_state = LZW_TRY_OUT1; + return GLZW_NO_OUTPUT_AVAIL; + } + *out_ptr++ = st->code_buffer; + (*out_avail)--; + st->code_buffer = 0; + st->buf_bits_left = 8; + } + /* code bits to pack */ + UINT32 n = st->buf_bits_left < st->code_bits_left + ? st->buf_bits_left : st->code_bits_left; + st->code_buffer |= + (st->code & ((1 << n) - 1)) << (8 - st->buf_bits_left); + st->code >>= n; + st->buf_bits_left -= n; + st->code_bits_left -= n; + if (st->code_bits_left) { + goto check_buf_bits; + } + switch (st->put_state) { + case PUT_INIT_CLEAR: + goto get_first_byte; + case PUT_HEAD: + goto insert_code_or_clear; + case PUT_CLEAR: + goto reset_after_clear; + case PUT_LAST_HEAD: + goto end_of_data; + case PUT_END: + goto flush_code_buffer; + default: + return GLZW_INTERNAL_ERROR; + } + +end_of_data: + st->code = st->end_code; + st->put_state = PUT_END; + goto put_code; +flush_code_buffer: /* jump here after put_code */ + if (st->buf_bits_left < 8) { + + case LZW_TRY_OUT2: + if (!*out_avail) { + st->entry_state = LZW_TRY_OUT2; + return GLZW_NO_OUTPUT_AVAIL; + } + *out_ptr++ = st->code_buffer; + (*out_avail)--; + } + st->entry_state = LZW_FINISHED; + return GLZW_OK; + + case LZW_FINISHED: + return GLZW_OK; + + default: + return GLZW_INTERNAL_ERROR; } +} +/* -END- GIF LZW encoder. */ int -ImagingGifEncode(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) { - UINT8 *ptr; - int this; +ImagingGifEncode(Imaging im, ImagingCodecState state, UINT8* buf, int bytes) { + UINT8* ptr; + UINT8* sub_block_ptr; + UINT8* sub_block_limit; + UINT8* buf_limit; + GIFENCODERSTATE *context = (GIFENCODERSTATE*) state->context; + int r; - GIFENCODERBLOCK *block; - GIFENCODERSTATE *context = (GIFENCODERSTATE *)state->context; + UINT32 in_avail, in_used; + UINT32 out_avail, out_used; - if (!state->state) { - /* place a clear code in the output buffer */ - context->bitbuffer = CLEAR_CODE; - context->bitcount = 9; - - state->count = FIRST_CODE; + if (state->state == INIT) { + state->state = ENCODE; + glzwe_init(context); if (context->interlace) { context->interlace = 1; @@ -159,166 +230,132 @@ ImagingGifEncode(Imaging im, ImagingCodecState state, UINT8 *buf, int bytes) { context->step = 1; } - context->last = -1; - + /* Need at least 2 bytes for data sub-block; 5 for empty image */ + if (bytes < 5) { + state->errcode = IMAGING_CODEC_CONFIG; + return 0; + } /* sanity check */ if (state->xsize <= 0 || state->ysize <= 0) { - state->state = ENCODE_EOF; + /* Is this better than an error return? */ + /* This will handle any legal "LZW Minimum Code Size" */ + memset(buf, 0, 5); + in_avail = 0; + out_avail = 5; + r = glzwe(context, (const UINT8 *)"", buf + 1, &in_avail, &out_avail, 1); + if (r == GLZW_OK) { + r = 5 - out_avail; + if (r < 1 || r > 3) { + state->errcode = IMAGING_CODEC_BROKEN; + return 0; + } + buf[0] = r; + state->errcode = IMAGING_CODEC_END; + return r + 2; + } else { + /* Should not be possible unless something external to this + * routine messes with our state data */ + state->errcode = IMAGING_CODEC_BROKEN; + return 0; + } } + /* Init state->x to make if() below true the first time through. */ + state->x = state->xsize; } - ptr = buf; + buf_limit = buf + bytes; + sub_block_limit = sub_block_ptr = ptr = buf; + /* On entry, buf is output buffer, bytes is space available in buf. + * Loop here getting input until buf is full or image is all encoded. */ for (;;) { - switch (state->state) { - case INIT: - case ENCODE: + /* Set up sub-block ptr and limit. sub_block_ptr stays at beginning + * of sub-block until it is full. ptr will advance when any data is + * placed in buf. + */ + if (ptr >= sub_block_limit) { + if (buf_limit - ptr < 2) { /* Need at least 2 for data sub-block */ + return ptr - buf; + } + sub_block_ptr = ptr; + sub_block_limit = sub_block_ptr + + (256 < buf_limit - sub_block_ptr ? + 256 : buf_limit - sub_block_ptr); + *ptr++ = 0; + } - /* identify and store a run of pixels */ + /* Get next row of pixels. */ + /* This if() originally tested state->x==0 for the first time through. + * This no longer works, as the loop will not advance state->x if + * glzwe() does not consume any input; this would advance the row + * spuriously. Now pre-init state->x above for first time, and avoid + * entering if() when state->state is FINISH, or it will loop + * infinitely. + */ + if (state->x >= state->xsize && state->state == ENCODE) { + if (!context->interlace && state->y >= state->ysize) { + state->state = FINISH; + continue; + } - if (state->x == 0 || state->x >= state->xsize) { - if (!context->interlace && state->y >= state->ysize) { - state->state = ENCODE_EOF; + /* get another line of data */ + state->shuffle( + state->buffer, + (UINT8*) im->image[state->y + state->yoff] + + state->xoff * im->pixelsize, state->xsize + ); + state->x = 0; + + /* step forward, according to the interlace settings */ + state->y += context->step; + while (context->interlace && state->y >= state->ysize) { + switch (context->interlace) { + case 1: + state->y = 4; + context->interlace = 2; break; - } - - if (context->flush) { - state->state = FLUSH; + case 2: + context->step = 4; + state->y = 2; + context->interlace = 3; break; - } - - /* get another line of data */ - state->shuffle( - state->buffer, - (UINT8 *)im->image[state->y + state->yoff] + - state->xoff * im->pixelsize, - state->xsize); - - state->x = 0; - - if (state->state == INIT) { - /* preload the run-length buffer and get going */ - context->last = state->buffer[0]; - context->count = state->x = 1; - state->state = ENCODE; - } - - /* step forward, according to the interlace settings */ - state->y += context->step; - while (context->interlace && state->y >= state->ysize) - switch (context->interlace) { - case 1: - state->y = 4; - context->interlace = 2; - break; - case 2: - context->step = 4; - state->y = 2; - context->interlace = 3; - break; - case 3: - context->step = 2; - state->y = 1; - context->interlace = 0; - break; - default: - /* just make sure we don't loop forever */ - context->interlace = 0; - } - } - /* Potential special case for xsize==1 */ - if (state->x < state->xsize) { - this = state->buffer[state->x++]; - } else { - EMIT_RUN(label0); - break; + case 3: + context->step = 2; + state->y = 1; + context->interlace = 0; + break; + default: + /* just make sure we don't loop forever */ + context->interlace = 0; } + } + } - if (this == context->last) { - context->count++; - } else { - EMIT_RUN(label1); - context->last = this; - context->count = 1; - } - break; + in_avail = state->xsize - state->x; /* bytes left in line */ + out_avail = sub_block_limit - ptr; /* bytes left in sub-block */ + r = glzwe(context, &state->buffer[state->x], ptr, &in_avail, + &out_avail, state->state == FINISH); + out_used = sub_block_limit - ptr - out_avail; + *sub_block_ptr += out_used; + ptr += out_used; + in_used = state->xsize - state->x - in_avail; + state->x += in_used; - case ENCODE_EOF: - - /* write the final run */ - EMIT_RUN(label2); - - /* write an end of image marker */ - EMIT(EOF_CODE); - - /* empty the bit buffer */ - while (context->bitcount > 0) { - if (!emit(context, (UINT8)context->bitbuffer)) { - state->errcode = IMAGING_CODEC_MEMORY; - return 0; - } - context->bitbuffer >>= 8; - context->bitcount -= 8; - } - - /* flush the last block, and exit */ - if (context->block) { - GIFENCODERBLOCK *block; - block = context->flush; - while (block && block->next) { - block = block->next; - } - if (block) { - block->next = context->block; - } else { - context->flush = context->block; - } - context->block = NULL; - } - - state->state = EXIT; - - /* fall through... */ - - case EXIT: - case FLUSH: - - while (context->flush) { - /* get a block from the flush queue */ - block = context->flush; - - if (block->size > 0) { - /* make sure it fits into the output buffer */ - if (bytes < block->size + 1) { - return ptr - buf; - } - - ptr[0] = block->size; - memcpy(ptr + 1, block->data, block->size); - - ptr += block->size + 1; - bytes -= block->size + 1; - } - - context->flush = block->next; - - if (context->free) { - free(context->free); - } - context->free = block; - } - - if (state->state == EXIT) { - /* this was the last block! */ - if (context->free) { - free(context->free); - } - state->errcode = IMAGING_CODEC_END; - return ptr - buf; - } - - state->state = ENCODE; - break; + if (r == GLZW_OK) { + /* Should not be possible when end-of-data flag is false. */ + state->errcode = IMAGING_CODEC_END; + return ptr - buf; + } else if (r == GLZW_NO_INPUT_AVAIL) { + /* Used all the input line; get another line */ + continue; + } else if (r == GLZW_NO_OUTPUT_AVAIL) { + /* subblock is full */ + continue; + } else { + /* Should not be possible unless something external to this + * routine messes with our state data */ + state->errcode = IMAGING_CODEC_BROKEN; + return 0; } } } diff --git a/src/libImaging/Imaging.h b/src/libImaging/Imaging.h index ae323f390..6d18dee4e 100644 --- a/src/libImaging/Imaging.h +++ b/src/libImaging/Imaging.h @@ -370,7 +370,7 @@ ImagingTransform( int y0, int x1, int y1, - double *a, + double a[8], int filter, int fill); extern Imaging diff --git a/src/libImaging/Jpeg2KDecode.c b/src/libImaging/Jpeg2KDecode.c index a2a7354db..601bd4b62 100644 --- a/src/libImaging/Jpeg2KDecode.c +++ b/src/libImaging/Jpeg2KDecode.c @@ -73,6 +73,8 @@ struct j2k_decode_unpacker { const char *mode; OPJ_COLOR_SPACE color_space; unsigned components; + /* bool indicating if unpacker supports subsampling */ + int subsampling; j2k_unpacker_t unpacker; }; @@ -350,6 +352,7 @@ j2ku_srgb_rgb( unsigned h = tileinfo->y1 - tileinfo->y0; int shifts[3], offsets[3], csiz[3]; + unsigned dx[3], dy[3]; const UINT8 *cdata[3]; const UINT8 *cptr = tiledata; unsigned n, x, y; @@ -359,6 +362,8 @@ j2ku_srgb_rgb( shifts[n] = 8 - in->comps[n].prec; offsets[n] = in->comps[n].sgnd ? 1 << (in->comps[n].prec - 1) : 0; csiz[n] = (in->comps[n].prec + 7) >> 3; + dx[n] = (in->comps[n].dx); + dy[n] = (in->comps[n].dy); if (csiz[n] == 3) { csiz[n] = 4; @@ -368,14 +373,14 @@ j2ku_srgb_rgb( offsets[n] += 1 << (-shifts[n] - 1); } - cptr += csiz[n] * w * h; + cptr += csiz[n] * (w / dx[n]) * (h / dy[n]); } for (y = 0; y < h; ++y) { const UINT8 *data[3]; UINT8 *row = (UINT8 *)im->image[y0 + y] + x0 * 4; for (n = 0; n < 3; ++n) { - data[n] = &cdata[n][csiz[n] * y * w]; + data[n] = &cdata[n][csiz[n] * (y / dy[n]) * (w / dx[n])]; } for (x = 0; x < w; ++x) { @@ -384,15 +389,13 @@ j2ku_srgb_rgb( switch (csiz[n]) { case 1: - word = *data[n]++; + word = data[n][x / dx[n]]; break; case 2: - word = *(const UINT16 *)data[n]; - data[n] += 2; + word = ((const UINT16 *)data[n])[x / dx[n]]; break; case 4: - word = *(const UINT32 *)data[n]; - data[n] += 4; + word = ((const UINT32 *)data[n])[x / dx[n]]; break; } @@ -415,6 +418,7 @@ j2ku_sycc_rgb( unsigned h = tileinfo->y1 - tileinfo->y0; int shifts[3], offsets[3], csiz[3]; + unsigned dx[3], dy[3]; const UINT8 *cdata[3]; const UINT8 *cptr = tiledata; unsigned n, x, y; @@ -424,6 +428,8 @@ j2ku_sycc_rgb( shifts[n] = 8 - in->comps[n].prec; offsets[n] = in->comps[n].sgnd ? 1 << (in->comps[n].prec - 1) : 0; csiz[n] = (in->comps[n].prec + 7) >> 3; + dx[n] = (in->comps[n].dx); + dy[n] = (in->comps[n].dy); if (csiz[n] == 3) { csiz[n] = 4; @@ -433,7 +439,7 @@ j2ku_sycc_rgb( offsets[n] += 1 << (-shifts[n] - 1); } - cptr += csiz[n] * w * h; + cptr += csiz[n] * (w / dx[n]) * (h / dy[n]); } for (y = 0; y < h; ++y) { @@ -441,7 +447,7 @@ j2ku_sycc_rgb( UINT8 *row = (UINT8 *)im->image[y0 + y] + x0 * 4; UINT8 *row_start = row; for (n = 0; n < 3; ++n) { - data[n] = &cdata[n][csiz[n] * y * w]; + data[n] = &cdata[n][csiz[n] * (y / dy[n]) * (w / dx[n])]; } for (x = 0; x < w; ++x) { @@ -450,15 +456,13 @@ j2ku_sycc_rgb( switch (csiz[n]) { case 1: - word = *data[n]++; + word = data[n][x / dx[n]]; break; case 2: - word = *(const UINT16 *)data[n]; - data[n] += 2; + word = ((const UINT16 *)data[n])[x / dx[n]]; break; case 4: - word = *(const UINT32 *)data[n]; - data[n] += 4; + word = ((const UINT32 *)data[n])[x / dx[n]]; break; } @@ -483,6 +487,7 @@ j2ku_srgba_rgba( unsigned h = tileinfo->y1 - tileinfo->y0; int shifts[4], offsets[4], csiz[4]; + unsigned dx[4], dy[4]; const UINT8 *cdata[4]; const UINT8 *cptr = tiledata; unsigned n, x, y; @@ -492,6 +497,8 @@ j2ku_srgba_rgba( shifts[n] = 8 - in->comps[n].prec; offsets[n] = in->comps[n].sgnd ? 1 << (in->comps[n].prec - 1) : 0; csiz[n] = (in->comps[n].prec + 7) >> 3; + dx[n] = (in->comps[n].dx); + dy[n] = (in->comps[n].dy); if (csiz[n] == 3) { csiz[n] = 4; @@ -501,14 +508,14 @@ j2ku_srgba_rgba( offsets[n] += 1 << (-shifts[n] - 1); } - cptr += csiz[n] * w * h; + cptr += csiz[n] * (w / dx[n]) * (h / dy[n]); } for (y = 0; y < h; ++y) { const UINT8 *data[4]; UINT8 *row = (UINT8 *)im->image[y0 + y] + x0 * 4; for (n = 0; n < 4; ++n) { - data[n] = &cdata[n][csiz[n] * y * w]; + data[n] = &cdata[n][csiz[n] * (y / dy[n]) * (w / dx[n])]; } for (x = 0; x < w; ++x) { @@ -517,15 +524,13 @@ j2ku_srgba_rgba( switch (csiz[n]) { case 1: - word = *data[n]++; + word = data[n][x / dx[n]]; break; case 2: - word = *(const UINT16 *)data[n]; - data[n] += 2; + word = ((const UINT16 *)data[n])[x / dx[n]]; break; case 4: - word = *(const UINT32 *)data[n]; - data[n] += 4; + word = ((const UINT32 *)data[n])[x / dx[n]]; break; } @@ -547,6 +552,7 @@ j2ku_sycca_rgba( unsigned h = tileinfo->y1 - tileinfo->y0; int shifts[4], offsets[4], csiz[4]; + unsigned dx[4], dy[4]; const UINT8 *cdata[4]; const UINT8 *cptr = tiledata; unsigned n, x, y; @@ -556,6 +562,8 @@ j2ku_sycca_rgba( shifts[n] = 8 - in->comps[n].prec; offsets[n] = in->comps[n].sgnd ? 1 << (in->comps[n].prec - 1) : 0; csiz[n] = (in->comps[n].prec + 7) >> 3; + dx[n] = (in->comps[n].dx); + dy[n] = (in->comps[n].dy); if (csiz[n] == 3) { csiz[n] = 4; @@ -565,7 +573,7 @@ j2ku_sycca_rgba( offsets[n] += 1 << (-shifts[n] - 1); } - cptr += csiz[n] * w * h; + cptr += csiz[n] * (w / dx[n]) * (h / dy[n]); } for (y = 0; y < h; ++y) { @@ -573,7 +581,7 @@ j2ku_sycca_rgba( UINT8 *row = (UINT8 *)im->image[y0 + y] + x0 * 4; UINT8 *row_start = row; for (n = 0; n < 4; ++n) { - data[n] = &cdata[n][csiz[n] * y * w]; + data[n] = &cdata[n][csiz[n] * (y / dy[n]) * (w / dx[n])]; } for (x = 0; x < w; ++x) { @@ -582,15 +590,13 @@ j2ku_sycca_rgba( switch (csiz[n]) { case 1: - word = *data[n]++; + word = data[n][x / dx[n]]; break; case 2: - word = *(const UINT16 *)data[n]; - data[n] += 2; + word = ((const UINT16 *)data[n])[x / dx[n]]; break; case 4: - word = *(const UINT32 *)data[n]; - data[n] += 4; + word = ((const UINT32 *)data[n])[x / dx[n]]; break; } @@ -604,22 +610,22 @@ j2ku_sycca_rgba( } static const struct j2k_decode_unpacker j2k_unpackers[] = { - {"L", OPJ_CLRSPC_GRAY, 1, j2ku_gray_l}, - {"I;16", OPJ_CLRSPC_GRAY, 1, j2ku_gray_i}, - {"I;16B", OPJ_CLRSPC_GRAY, 1, j2ku_gray_i}, - {"LA", OPJ_CLRSPC_GRAY, 2, j2ku_graya_la}, - {"RGB", OPJ_CLRSPC_GRAY, 1, j2ku_gray_rgb}, - {"RGB", OPJ_CLRSPC_GRAY, 2, j2ku_gray_rgb}, - {"RGB", OPJ_CLRSPC_SRGB, 3, j2ku_srgb_rgb}, - {"RGB", OPJ_CLRSPC_SYCC, 3, j2ku_sycc_rgb}, - {"RGB", OPJ_CLRSPC_SRGB, 4, j2ku_srgb_rgb}, - {"RGB", OPJ_CLRSPC_SYCC, 4, j2ku_sycc_rgb}, - {"RGBA", OPJ_CLRSPC_GRAY, 1, j2ku_gray_rgb}, - {"RGBA", OPJ_CLRSPC_GRAY, 2, j2ku_graya_la}, - {"RGBA", OPJ_CLRSPC_SRGB, 3, j2ku_srgb_rgb}, - {"RGBA", OPJ_CLRSPC_SYCC, 3, j2ku_sycc_rgb}, - {"RGBA", OPJ_CLRSPC_SRGB, 4, j2ku_srgba_rgba}, - {"RGBA", OPJ_CLRSPC_SYCC, 4, j2ku_sycca_rgba}, + {"L", OPJ_CLRSPC_GRAY, 1, 0, j2ku_gray_l}, + {"I;16", OPJ_CLRSPC_GRAY, 1, 0, j2ku_gray_i}, + {"I;16B", OPJ_CLRSPC_GRAY, 1, 0, j2ku_gray_i}, + {"LA", OPJ_CLRSPC_GRAY, 2, 0, j2ku_graya_la}, + {"RGB", OPJ_CLRSPC_GRAY, 1, 0, j2ku_gray_rgb}, + {"RGB", OPJ_CLRSPC_GRAY, 2, 0, j2ku_gray_rgb}, + {"RGB", OPJ_CLRSPC_SRGB, 3, 1, j2ku_srgb_rgb}, + {"RGB", OPJ_CLRSPC_SYCC, 3, 1, j2ku_sycc_rgb}, + {"RGB", OPJ_CLRSPC_SRGB, 4, 1, j2ku_srgb_rgb}, + {"RGB", OPJ_CLRSPC_SYCC, 4, 1, j2ku_sycc_rgb}, + {"RGBA", OPJ_CLRSPC_GRAY, 1, 0, j2ku_gray_rgb}, + {"RGBA", OPJ_CLRSPC_GRAY, 2, 0, j2ku_graya_la}, + {"RGBA", OPJ_CLRSPC_SRGB, 3, 1, j2ku_srgb_rgb}, + {"RGBA", OPJ_CLRSPC_SYCC, 3, 1, j2ku_sycc_rgb}, + {"RGBA", OPJ_CLRSPC_SRGB, 4, 1, j2ku_srgba_rgba}, + {"RGBA", OPJ_CLRSPC_SYCC, 4, 1, j2ku_sycca_rgba}, }; /* -------------------------------------------------------------------- */ @@ -644,7 +650,8 @@ j2k_decode_entry(Imaging im, ImagingCodecState state) { j2k_unpacker_t unpack = NULL; size_t buffer_size = 0, tile_bytes = 0; unsigned n, tile_height, tile_width; - int components; + int subsampling; + int total_component_width = 0; stream = opj_stream_create(BUFFER_SIZE, OPJ_TRUE); @@ -706,11 +713,16 @@ j2k_decode_entry(Imaging im, ImagingCodecState state) { goto quick_exit; } - for (n = 1; n < image->numcomps; ++n) { + /* + * Find first component with subsampling. + * + * This is a heuristic to determine the colorspace if unspecified. + */ + subsampling = -1; + for (n = 0; n < image->numcomps; ++n) { if (image->comps[n].dx != 1 || image->comps[n].dy != 1) { - state->errcode = IMAGING_CODEC_BROKEN; - state->state = J2K_STATE_FAILED; - goto quick_exit; + subsampling = n; + break; } } @@ -726,12 +738,14 @@ j2k_decode_entry(Imaging im, ImagingCodecState state) { If colorspace is unspecified, we assume: - Number of components Colorspace - ----------------------------------------- - 1 gray - 2 gray (+ alpha) - 3 sRGB - 4 sRGB (+ alpha) + Number of components Subsampling Colorspace + ------------------------------------------------------- + 1 Any gray + 2 Any gray (+ alpha) + 3 -1, 0 sRGB + 3 1, 2 YCbCr + 4 -1, 0, 3 sRGB (+ alpha) + 4 1, 2 YCbCr (+ alpha) */ @@ -746,14 +760,25 @@ j2k_decode_entry(Imaging im, ImagingCodecState state) { break; case 3: case 4: - color_space = OPJ_CLRSPC_SRGB; - break; + switch (subsampling) { + case -1: + case 0: + case 3: + color_space = OPJ_CLRSPC_SRGB; + break; + case 1: + case 2: + color_space = OPJ_CLRSPC_SYCC; + break; + } + break; } } for (n = 0; n < sizeof(j2k_unpackers) / sizeof(j2k_unpackers[0]); ++n) { if (color_space == j2k_unpackers[n].color_space && image->numcomps == j2k_unpackers[n].components && + (j2k_unpackers[n].subsampling || (subsampling == -1)) && strcmp(im->mode, j2k_unpackers[n].mode) == 0) { unpack = j2k_unpackers[n].unpacker; break; @@ -814,23 +839,40 @@ j2k_decode_entry(Imaging im, ImagingCodecState state) { goto quick_exit; } - /* Sometimes the tile_info.datasize we get back from openjpeg - is less than numcomps*w*h, and we overflow in the - shuffle stage */ - - tile_width = tile_info.x1 - tile_info.x0; - tile_height = tile_info.y1 - tile_info.y0; - components = tile_info.nb_comps == 3 ? 4 : tile_info.nb_comps; - if ((tile_width > UINT_MAX / components) || - (tile_height > UINT_MAX / components) || - (tile_width > UINT_MAX / (tile_height * components)) || - (tile_height > UINT_MAX / (tile_width * components))) { + if (tile_info.nb_comps != image->numcomps) { state->errcode = IMAGING_CODEC_BROKEN; state->state = J2K_STATE_FAILED; goto quick_exit; } - tile_bytes = tile_width * tile_height * components; + /* Sometimes the tile_info.datasize we get back from openjpeg + is less than sum(comp_bytes)*w*h, and we overflow in the + shuffle stage */ + + tile_width = tile_info.x1 - tile_info.x0; + tile_height = tile_info.y1 - tile_info.y0; + + /* Total component width = sum (component_width) e.g, it's + legal for an la file to have a 1 byte width for l, and 4 for + a, and then a malicious file could have a smaller tile_bytes + */ + + for (n=0; n < tile_info.nb_comps; n++) { + // see csize /acsize calcs + int csize = (image->comps[n].prec + 7) >> 3; + csize = (csize == 3) ? 4 : csize; + total_component_width += csize; + } + if ((tile_width > UINT_MAX / total_component_width) || + (tile_height > UINT_MAX / total_component_width) || + (tile_width > UINT_MAX / (tile_height * total_component_width)) || + (tile_height > UINT_MAX / (tile_width * total_component_width))) { + state->errcode = IMAGING_CODEC_BROKEN; + state->state = J2K_STATE_FAILED; + goto quick_exit; + } + + tile_bytes = tile_width * tile_height * total_component_width; if (tile_bytes > tile_info.data_size) { tile_info.data_size = tile_bytes; @@ -844,6 +886,10 @@ j2k_decode_entry(Imaging im, ImagingCodecState state) { state->state = J2K_STATE_FAILED; goto quick_exit; } + /* Undefined behavior, sometimes decode_tile_data doesn't + fill the buffer and we do things with it later, leading + to valgrind errors. */ + memset(new, 0, tile_info.data_size); state->buffer = new; buffer_size = tile_info.data_size; } diff --git a/src/libImaging/Jpeg2KEncode.c b/src/libImaging/Jpeg2KEncode.c index 2e6b5daf0..701853159 100644 --- a/src/libImaging/Jpeg2KEncode.c +++ b/src/libImaging/Jpeg2KEncode.c @@ -458,6 +458,12 @@ j2k_encode_entry(Imaging im, ImagingCodecState state) { break; } + if (!context->num_resolutions) { + while (tile_width < (1 << (params.numresolution - 1U)) || tile_height < (1 << (params.numresolution - 1U))) { + params.numresolution -= 1; + } + } + if (context->cinema_mode != OPJ_OFF) { j2k_set_cinema_params(im, components, ¶ms); } diff --git a/src/libImaging/Paste.c b/src/libImaging/Paste.c index 03b17f571..a1bf18a92 100644 --- a/src/libImaging/Paste.c +++ b/src/libImaging/Paste.c @@ -436,7 +436,7 @@ fill_mask_L( strcmp(imOut->mode, "La") == 0 || strcmp(imOut->mode, "LA") == 0 || strcmp(imOut->mode, "PA") == 0) && - i != 3) { + i != 3 && channel_mask != 0) { channel_mask = 255 - (255 - channel_mask) * (1 - (255 - out[3]) / 255); } diff --git a/src/libImaging/Quant.c b/src/libImaging/Quant.c index f5a5d567c..1c6b9d6a2 100644 --- a/src/libImaging/Quant.c +++ b/src/libImaging/Quant.c @@ -753,11 +753,19 @@ annotate_hash_table(BoxNode *n, HashTable *h, uint32_t *box) { return 1; } +typedef struct { + uint32_t *distance; + uint32_t index; +} DistanceWithIndex; + static int -_sort_ulong_ptr_keys(const void *a, const void *b) { - uint32_t A = **(uint32_t **)a; - uint32_t B = **(uint32_t **)b; - return (A == B) ? 0 : ((A < B) ? -1 : +1); +_distance_index_cmp(const void *a, const void *b) { + DistanceWithIndex *A = (DistanceWithIndex *)a; + DistanceWithIndex *B = (DistanceWithIndex *)b; + if (*A->distance == *B->distance) { + return A->index < B->index ? -1 : +1; + } + return *A->distance < *B->distance ? -1 : +1; } static int @@ -789,10 +797,11 @@ resort_distance_tables( return 1; } -static void +static int build_distance_tables( uint32_t *avgDist, uint32_t **avgDistSortKey, Pixel *p, uint32_t nEntries) { uint32_t i, j; + DistanceWithIndex *dwi; for (i = 0; i < nEntries; i++) { avgDist[i * nEntries + i] = 0; @@ -804,13 +813,29 @@ build_distance_tables( avgDistSortKey[i * nEntries + j] = &(avgDist[i * nEntries + j]); } } - for (i = 0; i < nEntries; i++) { - qsort( - avgDistSortKey + i * nEntries, - nEntries, - sizeof(uint32_t *), - _sort_ulong_ptr_keys); + + dwi = calloc(nEntries, sizeof(DistanceWithIndex)); + if (!dwi) { + return 0; } + for (i = 0; i < nEntries; i++) { + for (j = 0; j < nEntries; j++) { + dwi[j] = (DistanceWithIndex){ + &(avgDist[i * nEntries + j]), + j + }; + } + qsort( + dwi, + nEntries, + sizeof(DistanceWithIndex), + _distance_index_cmp); + for (j = 0; j < nEntries; j++) { + avgDistSortKey[i * nEntries + j] = dwi[j].distance; + } + } + free(dwi); + return 1; } static int @@ -1175,8 +1200,10 @@ k_means( if (!built) { compute_palette_from_quantized_pixels( pixelData, nPixels, paletteData, nPaletteEntries, avg, count, qp); - build_distance_tables( - avgDist, avgDistSortKey, paletteData, nPaletteEntries); + if (!build_distance_tables( + avgDist, avgDistSortKey, paletteData, nPaletteEntries)) { + goto error_3; + } built = 1; } else { recompute_palette_from_averages(paletteData, nPaletteEntries, avg, count); @@ -1243,7 +1270,7 @@ error_1: return 0; } -int +static int quantize( Pixel *pixelData, uint32_t nPixels, @@ -1372,7 +1399,9 @@ quantize( goto error_6; } - build_distance_tables(avgDist, avgDistSortKey, p, nPaletteEntries); + if (!build_distance_tables(avgDist, avgDistSortKey, p, nPaletteEntries)) { + goto error_7; + } if (!map_image_pixels_from_median_box( pixelData, nPixels, p, nPaletteEntries, h, avgDist, avgDistSortKey, qp)) { @@ -1511,7 +1540,7 @@ compute_distances(const HashTable *h, const Pixel pixel, uint32_t *dist, void *u } } -int +static int quantize2( Pixel *pixelData, uint32_t nPixels, @@ -1577,7 +1606,9 @@ quantize2( goto error_3; } - build_distance_tables(avgDist, avgDistSortKey, p, nQuantPixels); + if (!build_distance_tables(avgDist, avgDistSortKey, p, nQuantPixels)) { + goto error_4; + } if (!map_image_pixels( pixelData, nPixels, p, nQuantPixels, avgDist, avgDistSortKey, qp)) { @@ -1683,9 +1714,26 @@ ImagingQuantize(Imaging im, int colors, int mode, int kmeans) { } else if (!strcmp(im->mode, "RGB") || !strcmp(im->mode, "RGBA")) { /* true colour */ + withAlpha = !strcmp(im->mode, "RGBA"); + int transparency = 0; + unsigned char r, g, b; for (i = y = 0; y < im->ysize; y++) { for (x = 0; x < im->xsize; x++, i++) { p[i].v = im->image32[y][x]; + if (withAlpha && p[i].c.a == 0) { + if (transparency == 0) { + transparency = 1; + r = p[i].c.r; + g = p[i].c.g; + b = p[i].c.b; + } else { + /* Set all subsequent transparent pixels + to the same colour as the first */ + p[i].c.r = r; + p[i].c.g = g; + p[i].c.b = b; + } + } } } @@ -1720,9 +1768,6 @@ ImagingQuantize(Imaging im, int colors, int mode, int kmeans) { kmeans); break; case 2: - if (!strcmp(im->mode, "RGBA")) { - withAlpha = 1; - } result = quantize_octree( p, im->xsize * im->ysize, @@ -1734,9 +1779,6 @@ ImagingQuantize(Imaging im, int colors, int mode, int kmeans) { break; case 3: #ifdef HAVE_LIBIMAGEQUANT - if (!strcmp(im->mode, "RGBA")) { - withAlpha = 1; - } result = quantize_pngquant( p, im->xsize, diff --git a/src/libImaging/TiffDecode.c b/src/libImaging/TiffDecode.c index 021c2898c..46bd101b5 100644 --- a/src/libImaging/TiffDecode.c +++ b/src/libImaging/TiffDecode.c @@ -56,7 +56,7 @@ _tiffReadProc(thandle_t hdata, tdata_t buf, tsize_t size) { dump_state(state); if (state->loc > state->eof) { - TIFFError("_tiffReadProc", "Invalid Read at loc %llu, eof: %llu", state->loc, state->eof); + TIFFError("_tiffReadProc", "Invalid Read at loc %" PRIu64 ", eof: %" PRIu64, state->loc, state->eof); return 0; } to_read = min(size, min(state->size, (tsize_t)state->eof) - (tsize_t)state->loc); @@ -181,7 +181,7 @@ _tiffUnmapProc(thandle_t hdata, tdata_t base, toff_t size) { } int -ImagingLibTiffInit(ImagingCodecState state, int fp, uint32 offset) { +ImagingLibTiffInit(ImagingCodecState state, int fp, uint32_t offset) { TIFFSTATE *clientstate = (TIFFSTATE *)state->context; TRACE(("initing libtiff\n")); @@ -213,10 +213,10 @@ ImagingLibTiffInit(ImagingCodecState state, int fp, uint32 offset) { } int -_pickUnpackers(Imaging im, ImagingCodecState state, TIFF *tiff, uint16 planarconfig, ImagingShuffler *unpackers) { +_pickUnpackers(Imaging im, ImagingCodecState state, TIFF *tiff, uint16_t planarconfig, ImagingShuffler *unpackers) { // if number of bands is 1, there is no difference with contig case if (planarconfig == PLANARCONFIG_SEPARATE && im->bands > 1) { - uint16 bits_per_sample = 8; + uint16_t bits_per_sample = 8; TIFFGetFieldDefaulted(tiff, TIFFTAG_BITSPERSAMPLE, &bits_per_sample); if (bits_per_sample != 8 && bits_per_sample != 16) { @@ -265,7 +265,7 @@ _decodeAsRGBA(Imaging im, ImagingCodecState state, TIFF *tiff) { ret = TIFFGetFieldDefaulted(tiff, TIFFTAG_ROWSPERSTRIP, &rows_per_block); } - if (ret != 1) { + if (ret != 1 || rows_per_block==(UINT32)(-1)) { rows_per_block = state->ysize; } @@ -281,17 +281,6 @@ _decodeAsRGBA(Imaging im, ImagingCodecState state, TIFF *tiff) { img.req_orientation = ORIENTATION_TOPLEFT; img.col_offset = 0; - if (state->xsize != img.width || state->ysize != img.height) { - TRACE( - ("Inconsistent Image Error: %d =? %d, %d =? %d", - state->xsize, - img.width, - state->ysize, - img.height)); - state->errcode = IMAGING_CODEC_BROKEN; - goto decodergba_err; - } - /* overflow check for row byte size */ if (INT_MAX / 4 < img.width) { state->errcode = IMAGING_CODEC_MEMORY; @@ -427,15 +416,6 @@ _decodeTile(Imaging im, ImagingCodecState state, TIFF *tiff, int planes, Imaging for (plane = 0; plane < planes; plane++) { ImagingShuffler shuffler = unpackers[plane]; for (x = state->xoff; x < state->xsize; x += tile_width) { - /* Sanity Check. Apparently in some cases, the TiffReadRGBA* functions - have a different view of the size of the tiff than we're getting from - other functions. So, we need to check here. - */ - if (!TIFFCheckTile(tiff, x, y, 0, plane)) { - TRACE(("Check Tile Error, Tile at %dx%d\n", x, y)); - state->errcode = IMAGING_CODEC_BROKEN; - return -1; - } if (TIFFReadTile(tiff, (tdata_t)state->buffer, x, y, 0, plane) == -1) { TRACE(("Decode Error, Tile at %dx%d\n", x, y)); state->errcode = IMAGING_CODEC_BROKEN; @@ -471,7 +451,7 @@ _decodeStrip(Imaging im, ImagingCodecState state, TIFF *tiff, int planes, Imagin UINT8 *new_data; UINT32 rows_per_strip; int ret; - tsize_t strip_size, row_byte_size; + tsize_t strip_size, row_byte_size, unpacker_row_byte_size; ret = TIFFGetField(tiff, TIFFTAG_ROWSPERSTRIP, &rows_per_strip); if (ret != 1 || rows_per_strip==(UINT32)(-1)) { @@ -491,7 +471,8 @@ _decodeStrip(Imaging im, ImagingCodecState state, TIFF *tiff, int planes, Imagin return -1; } - if (strip_size > ((state->xsize * state->bits / planes + 7) / 8) * rows_per_strip) { + unpacker_row_byte_size = (state->xsize * state->bits / planes + 7) / 8; + if (strip_size > (unpacker_row_byte_size * rows_per_strip)) { // If the strip size as expected by LibTiff isn't what we're expecting, abort. // man: TIFFStripSize returns the equivalent size for a strip of data as it would be returned in a // call to TIFFReadEncodedStrip ... @@ -505,7 +486,9 @@ _decodeStrip(Imaging im, ImagingCodecState state, TIFF *tiff, int planes, Imagin row_byte_size = TIFFScanlineSize(tiff); - if (row_byte_size == 0 || row_byte_size > strip_size) { + // if the unpacker calculated row size is > row byte size, (at least) the last + // row of the strip will have a read buffer overflow. + if (row_byte_size == 0 || unpacker_row_byte_size > row_byte_size) { state->errcode = IMAGING_CODEC_BROKEN; return -1; } @@ -562,12 +545,13 @@ ImagingLibTiffDecode( char *filename = "tempfile.tif"; char *mode = "r"; TIFF *tiff; - uint16 photometric = 0; // init to not PHOTOMETRIC_YCBCR - uint16 compression; + uint16_t photometric = 0; // init to not PHOTOMETRIC_YCBCR + uint16_t compression; int readAsRGBA = 0; - uint16 planarconfig = 0; + uint16_t planarconfig = 0; int planes = 1; ImagingShuffler unpackers[4]; + UINT32 img_width, img_height; memset(unpackers, 0, sizeof(ImagingShuffler) * 4); @@ -655,7 +639,7 @@ ImagingLibTiffDecode( if (clientstate->ifd) { int rv; - uint32 ifdoffset = clientstate->ifd; + uint32_t ifdoffset = clientstate->ifd; TRACE(("reading tiff ifd %u\n", ifdoffset)); rv = TIFFSetSubDirectory(tiff, ifdoffset); if (!rv) { @@ -664,6 +648,20 @@ ImagingLibTiffDecode( } } + TIFFGetField(tiff, TIFFTAG_IMAGEWIDTH, &img_width); + TIFFGetField(tiff, TIFFTAG_IMAGELENGTH, &img_height); + + if (state->xsize != img_width || state->ysize != img_height) { + TRACE( + ("Inconsistent Image Error: %d =? %d, %d =? %d", + state->xsize, + img_width, + state->ysize, + img_height)); + state->errcode = IMAGING_CODEC_BROKEN; + goto decode_err; + } + TIFFGetField(tiff, TIFFTAG_PHOTOMETRIC, &photometric); TIFFGetField(tiff, TIFFTAG_COMPRESSION, &compression); @@ -674,7 +672,7 @@ ImagingLibTiffDecode( readAsRGBA = photometric == PHOTOMETRIC_YCBCR; if (readAsRGBA && compression == COMPRESSION_JPEG && planarconfig == PLANARCONFIG_CONTIG) { - // If using new JPEG compression, let libjpeg do RGB convertion for performance reasons + // If using new JPEG compression, let libjpeg do RGB conversion for performance reasons TIFFSetField(tiff, TIFFTAG_JPEGCOLORMODE, JPEGCOLORMODE_RGB); readAsRGBA = 0; } @@ -699,8 +697,8 @@ ImagingLibTiffDecode( // Check if raw mode was RGBa and it was stored on separate planes // so we have to convert it to RGBA if (planes > 3 && strcmp(im->mode, "RGBA") == 0) { - uint16 extrasamples; - uint16* sampleinfo; + uint16_t extrasamples; + uint16_t* sampleinfo; ImagingShuffler shuffle; INT32 y; @@ -812,7 +810,7 @@ ImagingLibTiffMergeFieldInfo( ImagingCodecState state, TIFFDataType field_type, int key, int is_var_length) { // Refer to libtiff docs (http://www.simplesystems.org/libtiff/addingtags.html) TIFFSTATE *clientstate = (TIFFSTATE *)state->context; - uint32 n; + uint32_t n; int status = 0; // custom fields added with ImagingLibTiffMergeFieldInfo are only used for @@ -935,7 +933,7 @@ ImagingLibTiffEncode(Imaging im, ImagingCodecState state, UINT8 *buffer, int byt state->xsize); if (TIFFWriteScanline( - tiff, (tdata_t)(state->buffer), (uint32)state->y, 0) == -1) { + tiff, (tdata_t)(state->buffer), (uint32_t)state->y, 0) == -1) { TRACE(("Encode Error, row %d\n", state->y)); state->errcode = IMAGING_CODEC_BROKEN; TIFFClose(tiff); diff --git a/src/libImaging/TiffDecode.h b/src/libImaging/TiffDecode.h index 2c3d88caa..c7c7d48ed 100644 --- a/src/libImaging/TiffDecode.h +++ b/src/libImaging/TiffDecode.h @@ -32,17 +32,17 @@ typedef struct { toff_t loc; /* toff_t == uint32 */ tsize_t size; /* tsize_t == int32 */ int fp; - uint32 ifd; /* offset of the ifd, used for multipage - * Should be uint32 for libtiff 3.9.x - * uint64 for libtiff 4.0.x - */ + uint32_t ifd; /* offset of the ifd, used for multipage + * Should be uint32 for libtiff 3.9.x + * uint64 for libtiff 4.0.x + */ TIFF *tiff; /* Used in write */ toff_t eof; int flrealloc; /* may we realloc */ } TIFFSTATE; extern int -ImagingLibTiffInit(ImagingCodecState state, int fp, uint32 offset); +ImagingLibTiffInit(ImagingCodecState state, int fp, uint32_t offset); extern int ImagingLibTiffEncodeInit(ImagingCodecState state, char *filename, int fp); extern int diff --git a/src/outline.c b/src/outline.c index ba3e056cc..0a9a3646e 100644 --- a/src/outline.c +++ b/src/outline.c @@ -145,11 +145,11 @@ _outline_transform(OutlineObject *self, PyObject *args) { } static struct PyMethodDef _outline_methods[] = { - {"line", (PyCFunction)_outline_line, 1}, - {"curve", (PyCFunction)_outline_curve, 1}, - {"move", (PyCFunction)_outline_move, 1}, - {"close", (PyCFunction)_outline_close, 1}, - {"transform", (PyCFunction)_outline_transform, 1}, + {"line", (PyCFunction)_outline_line, METH_VARARGS}, + {"curve", (PyCFunction)_outline_curve, METH_VARARGS}, + {"move", (PyCFunction)_outline_move, METH_VARARGS}, + {"close", (PyCFunction)_outline_close, METH_VARARGS}, + {"transform", (PyCFunction)_outline_transform, METH_VARARGS}, {NULL, NULL} /* sentinel */ }; diff --git a/src/path.c b/src/path.c index 8d1f68e84..4764c58aa 100644 --- a/src/path.c +++ b/src/path.c @@ -524,11 +524,11 @@ path_transform(PyPathObject *self, PyObject *args) { } static struct PyMethodDef methods[] = { - {"getbbox", (PyCFunction)path_getbbox, 1}, - {"tolist", (PyCFunction)path_tolist, 1}, - {"compact", (PyCFunction)path_compact, 1}, - {"map", (PyCFunction)path_map, 1}, - {"transform", (PyCFunction)path_transform, 1}, + {"getbbox", (PyCFunction)path_getbbox, METH_VARARGS}, + {"tolist", (PyCFunction)path_tolist, METH_VARARGS}, + {"compact", (PyCFunction)path_compact, METH_VARARGS}, + {"map", (PyCFunction)path_map, METH_VARARGS}, + {"transform", (PyCFunction)path_transform, METH_VARARGS}, {NULL, NULL} /* sentinel */ }; diff --git a/src/thirdparty/fribidi-shim/fribidi.c b/src/thirdparty/fribidi-shim/fribidi.c index 55e2a6ab3..abbab07b0 100644 --- a/src/thirdparty/fribidi-shim/fribidi.c +++ b/src/thirdparty/fribidi-shim/fribidi.c @@ -56,6 +56,9 @@ int load_fribidi(void) { error = error || (func == 0); p_fribidi = LoadLibrary("fribidi"); + if (!p_fribidi) { + p_fribidi = LoadLibrary("fribidi-0"); + } /* MSYS2 */ if (!p_fribidi) { p_fribidi = LoadLibrary("libfribidi-0"); diff --git a/tox.ini b/tox.ini index 2557d5067..cdc2ab881 100644 --- a/tox.ini +++ b/tox.ini @@ -6,7 +6,7 @@ [tox] envlist = lint - py{36,37,38,39,py3} + py{36,37,38,39,310,py3} minversion = 1.9 [testenv] diff --git a/winbuild/build.rst b/winbuild/build.rst index cd4a45e87..7493c30e5 100644 --- a/winbuild/build.rst +++ b/winbuild/build.rst @@ -87,7 +87,7 @@ and install Pillow in develop mode (instead of ``python3 -m pip install --editab Testing Pillow -------------- -Some binary dependencies (e.g. ``libraqm.dll``) will be stored in the +Some binary dependencies (e.g. ``fribidi.dll``) will be stored in the ``winbuild\build\bin`` directory; this directory should be added to ``PATH`` before running tests. diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index a91c86a73..63270d753 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -105,9 +105,9 @@ header = [ # dependencies, listed in order of compilation deps = { "libjpeg": { - "url": SF_MIRROR + "/project/libjpeg-turbo/2.0.6/libjpeg-turbo-2.0.6.tar.gz", - "filename": "libjpeg-turbo-2.0.6.tar.gz", - "dir": "libjpeg-turbo-2.0.6", + "url": SF_MIRROR + "/project/libjpeg-turbo/2.1.0/libjpeg-turbo-2.1.0.tar.gz", + "filename": "libjpeg-turbo-2.1.0.tar.gz", + "dir": "libjpeg-turbo-2.1.0", "build": [ cmd_cmake( [ @@ -141,13 +141,13 @@ deps = { "libs": [r"*.lib"], }, "libtiff": { - "url": "https://download.osgeo.org/libtiff/tiff-4.2.0.tar.gz", - "filename": "tiff-4.2.0.tar.gz", - "dir": "tiff-4.2.0", + "url": "https://download.osgeo.org/libtiff/tiff-4.3.0.tar.gz", + "filename": "tiff-4.3.0.tar.gz", + "dir": "tiff-4.3.0", "build": [ - cmd_copy(r"{winbuild_dir}\tiff.opt", "nmake.opt"), - cmd_nmake("makefile.vc", "clean"), - cmd_nmake("makefile.vc", "lib"), + cmd_cmake("-DBUILD_SHARED_LIBS:BOOL=OFF"), + cmd_nmake(target="clean"), + cmd_nmake(target="tiff"), ], "headers": [r"libtiff\tiff*.h"], "libs": [r"libtiff\*.lib"], @@ -236,7 +236,9 @@ deps = { cmd_rmdir("Lib"), cmd_rmdir(r"Projects\VC2017\Release"), cmd_msbuild(r"Projects\VC2017\lcms2.sln", "Release", "Clean"), - cmd_msbuild(r"Projects\VC2017\lcms2.sln", "Release", "lcms2_static"), + cmd_msbuild( + r"Projects\VC2017\lcms2.sln", "Release", "lcms2_static:Rebuild" + ), cmd_xcopy("include", "{inc_dir}"), ], "libs": [r"Lib\MS\*.lib"], @@ -275,9 +277,9 @@ deps = { "libs": [r"*.lib"], }, "harfbuzz": { - "url": "https://github.com/harfbuzz/harfbuzz/archive/2.8.0.zip", - "filename": "harfbuzz-2.8.0.zip", - "dir": "harfbuzz-2.8.0", + "url": "https://github.com/harfbuzz/harfbuzz/archive/2.8.1.zip", + "filename": "harfbuzz-2.8.1.zip", + "dir": "harfbuzz-2.8.1", "build": [ cmd_cmake("-DHB_HAVE_FREETYPE:BOOL=TRUE"), cmd_nmake(target="clean"), diff --git a/winbuild/tiff.opt b/winbuild/tiff.opt deleted file mode 100644 index d82c51678..000000000 --- a/winbuild/tiff.opt +++ /dev/null @@ -1,220 +0,0 @@ -# $Id: nmake.opt,v 1.18 2006/06/07 16:33:45 dron Exp $ -# -# Copyright (C) 2004, Andrey Kiselev -# -# Permission to use, copy, modify, distribute, and sell this software and -# its documentation for any purpose is hereby granted without fee, provided -# that (i) the above copyright notices and this permission notice appear in -# all copies of the software and related documentation, and (ii) the names of -# Sam Leffler and Silicon Graphics may not be used in any advertising or -# publicity relating to the software without the specific, prior written -# permission of Sam Leffler and Silicon Graphics. -# -# THE SOFTWARE IS PROVIDED "AS-IS" AND WITHOUT WARRANTY OF ANY KIND, -# EXPRESS, IMPLIED OR OTHERWISE, INCLUDING WITHOUT LIMITATION, ANY -# WARRANTY OF MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. -# -# IN NO EVENT SHALL SAM LEFFLER OR SILICON GRAPHICS BE LIABLE FOR -# ANY SPECIAL, INCIDENTAL, INDIRECT OR CONSEQUENTIAL DAMAGES OF ANY KIND, -# OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, -# WHETHER OR NOT ADVISED OF THE POSSIBILITY OF DAMAGE, AND ON ANY THEORY OF -# LIABILITY, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE -# OF THIS SOFTWARE. - -# Compile time parameters for MS Visual C++ compiler. -# You may edit this file to specify building options. - -# -###### Edit the following lines to choose a feature set you need. ####### -# - -# -# Select WINMODE_CONSOLE to build a library which reports errors to stderr, or -# WINMODE_WINDOWED to build such that errors are reported via MessageBox(). -# -WINMODE_CONSOLE = 1 -#WINMODE_WINDOWED = 1 - -# -# Comment out the following lines to disable internal codecs. -# -# Support for CCITT Group 3 & 4 algorithms -CCITT_SUPPORT = 1 -# Support for Macintosh PackBits algorithm -PACKBITS_SUPPORT = 1 -# Support for LZW algorithm -LZW_SUPPORT = 1 -# Support for ThunderScan 4-bit RLE algorithm -THUNDER_SUPPORT = 1 -# Support for NeXT 2-bit RLE algorithm -NEXT_SUPPORT = 1 -# Support for LogLuv high dynamic range encoding -LOGLUV_SUPPORT = 1 - -# -# Uncomment and edit following lines to enable JPEG support. -# -JPEG_SUPPORT = 1 -JPEG_INCLUDE = -I$(INCLIB) -JPEG_LIB = $(INCLIB)/libjpeg.lib - -# -# Uncomment and edit following lines to enable ZIP support -# (required for Deflate compression and Pixar log-format) -# -ZIP_SUPPORT = 1 -ZLIB_INCLUDE = -I$(INCLIB) -ZLIB_LIB = $(INCLIB)/zlib.lib - -# Indicate if the compiler provides strtoll/strtoull (default 1) -# Users of MSVC++ 14.0 ("Visual Studio 2015") and later should set this to 1 -HAVE_STRTOLL = 1 - -# -# Uncomment and edit following lines to enable ISO JBIG support -# -#JBIG_SUPPORT = 1 -#JBIGDIR = d:/projects/jbigkit -#JBIG_INCLUDE = -I$(JBIGDIR)/libjbig -#JBIG_LIB = $(JBIGDIR)/libjbig/jbig.lib - -# -# Uncomment following line to enable Pixar log-format algorithm -# (Zlib required). -# -#PIXARLOG_SUPPORT = 1 - -# -# Comment out the following lines to disable strip chopping -# (whether or not to convert single-strip uncompressed images to multiple -# strips of specified size to reduce memory usage). Default strip size -# is 8192 bytes, it can be configured via the STRIP_SIZE_DEFAULT parameter -# -STRIPCHOP_SUPPORT = 1 -STRIP_SIZE_DEFAULT = 8192 - -# -# Comment out the following lines to disable treating the fourth sample with -# no EXTRASAMPLE_ value as being ASSOCALPHA. Many packages produce RGBA -# files but don't mark the alpha properly. -# -EXTRASAMPLE_AS_ALPHA_SUPPORT = 1 - -# -# Comment out the following lines to disable picking up YCbCr subsampling -# info from the JPEG data stream to support files lacking the tag. -# See Bug 168 in Bugzilla, and JPEGFixupTestSubsampling() for details. -# -CHECK_JPEG_YCBCR_SUBSAMPLING = 1 - -# -####################### Compiler related options. ####################### -# - -# -# Pick debug or optimized build flags. We default to an optimized build -# with no debugging information. -# NOTE: /EHsc option required if you want to build the C++ stream API -# -OPTFLAGS = /Ox /MD /EHsc /W3 /D_CRT_SECURE_NO_DEPRECATE -#OPTFLAGS = /Zi - -# -# Uncomment following line to enable using Windows Common RunTime Library -# instead of Windows specific system calls. See notes on top of tif_unix.c -# module for details. -# -USE_WIN_CRT_LIB = 1 - -# Compiler specific options. You may probably want to adjust compilation -# parameters in CFLAGS variable. Refer to your compiler documentation -# for the option reference. -# -MAKE = nmake /nologo -CC = cl /nologo -CXX = cl /nologo -AR = lib /nologo -LD = link /nologo - -CFLAGS = $(OPTFLAGS) $(INCL) $(EXTRAFLAGS) -CXXFLAGS = $(OPTFLAGS) $(INCL) $(EXTRAFLAGS) -EXTRAFLAGS = -LIBS = - -# Name of the output shared library -DLLNAME = libtiff.dll - -# -########### There is nothing to edit below this line normally. ########### -# - -# Set the native cpu bit order -EXTRAFLAGS = -DFILLODER_LSB2MSB $(EXTRAFLAGS) - -!IFDEF WINMODE_WINDOWED -EXTRAFLAGS = -DTIF_PLATFORM_WINDOWED $(EXTRAFLAGS) -LIBS = user32.lib $(LIBS) -!ELSE -EXTRAFLAGS = -DTIF_PLATFORM_CONSOLE $(EXTRAFLAGS) -!ENDIF - -# Codec stuff -!IFDEF CCITT_SUPPORT -EXTRAFLAGS = -DCCITT_SUPPORT $(EXTRAFLAGS) -!ENDIF - -!IFDEF PACKBITS_SUPPORT -EXTRAFLAGS = -DPACKBITS_SUPPORT $(EXTRAFLAGS) -!ENDIF - -!IFDEF LZW_SUPPORT -EXTRAFLAGS = -DLZW_SUPPORT $(EXTRAFLAGS) -!ENDIF - -!IFDEF THUNDER_SUPPORT -EXTRAFLAGS = -DTHUNDER_SUPPORT $(EXTRAFLAGS) -!ENDIF - -!IFDEF NEXT_SUPPORT -EXTRAFLAGS = -DNEXT_SUPPORT $(EXTRAFLAGS) -!ENDIF - -!IFDEF LOGLUV_SUPPORT -EXTRAFLAGS = -DLOGLUV_SUPPORT $(EXTRAFLAGS) -!ENDIF - -!IFDEF JPEG_SUPPORT -LIBS = $(LIBS) $(JPEG_LIB) -EXTRAFLAGS = -DJPEG_SUPPORT -DOJPEG_SUPPORT $(EXTRAFLAGS) -!ENDIF - -!IFDEF ZIP_SUPPORT -LIBS = $(LIBS) $(ZLIB_LIB) -EXTRAFLAGS = -DZIP_SUPPORT $(EXTRAFLAGS) -!IFDEF PIXARLOG_SUPPORT -EXTRAFLAGS = -DPIXARLOG_SUPPORT $(EXTRAFLAGS) -!ENDIF -!ENDIF - -!IFDEF JBIG_SUPPORT -LIBS = $(LIBS) $(JBIG_LIB) -EXTRAFLAGS = -DJBIG_SUPPORT $(EXTRAFLAGS) -!ENDIF - -!IFDEF STRIPCHOP_SUPPORT -EXTRAFLAGS = -DSTRIPCHOP_DEFAULT=TIFF_STRIPCHOP -DSTRIP_SIZE_DEFAULT=$(STRIP_SIZE_DEFAULT) $(EXTRAFLAGS) -!ENDIF - -!IFDEF EXTRASAMPLE_AS_ALPHA_SUPPORT -EXTRAFLAGS = -DDEFAULT_EXTRASAMPLE_AS_ALPHA $(EXTRAFLAGS) -!ENDIF - -!IFDEF CHECK_JPEG_YCBCR_SUBSAMPLING -EXTRAFLAGS = -DCHECK_JPEG_YCBCR_SUBSAMPLING $(EXTRAFLAGS) -!ENDIF - -!IFDEF USE_WIN_CRT_LIB -EXTRAFLAGS = -DAVOID_WIN32_FILEIO $(EXTRAFLAGS) -!ELSE -EXTRAFLAGS = -DUSE_WIN32_FILEIO $(EXTRAFLAGS) -!ENDIF