mirror of
				https://github.com/python-pillow/Pillow.git
				synced 2025-10-30 07:27:49 +03:00 
			
		
		
		
	Merge branch 'main' into progress
This commit is contained in:
		
						commit
						09e4df10af
					
				|  | @ -23,7 +23,7 @@ if [[ $(uname) != CYGWIN* ]]; then | |||
|     sudo apt-get -qq install libfreetype6-dev liblcms2-dev libtiff-dev python3-tk\ | ||||
|                              ghostscript libjpeg-turbo8-dev libopenjp2-7-dev\ | ||||
|                              cmake meson imagemagick libharfbuzz-dev libfribidi-dev\ | ||||
|                              sway wl-clipboard libopenblas-dev | ||||
|                              sway wl-clipboard libopenblas-dev nasm | ||||
| fi | ||||
| 
 | ||||
| python3 -m pip install --upgrade pip | ||||
|  | @ -36,6 +36,9 @@ python3 -m pip install -U pytest | |||
| python3 -m pip install -U pytest-cov | ||||
| python3 -m pip install -U pytest-timeout | ||||
| python3 -m pip install pyroma | ||||
| # optional test dependency, only install if there's a binary package. | ||||
| # fails on beta 3.14 and PyPy | ||||
| python3 -m pip install --only-binary=:all: pyarrow || true | ||||
| 
 | ||||
| if [[ $(uname) != CYGWIN* ]]; then | ||||
|     python3 -m pip install numpy | ||||
|  | @ -50,7 +53,7 @@ if [[ $(uname) != CYGWIN* ]]; then | |||
|     # Pyroma uses non-isolated build and fails with old setuptools | ||||
|     if [[ $GHA_PYTHON_VERSION == 3.9 ]]; then | ||||
|         # To match pyproject.toml | ||||
|         python3 -m pip install "setuptools>=67.8" | ||||
|         python3 -m pip install "setuptools>=77" | ||||
|     fi | ||||
| 
 | ||||
|     # webp | ||||
|  | @ -62,6 +65,9 @@ if [[ $(uname) != CYGWIN* ]]; then | |||
|     # raqm | ||||
|     pushd depends && ./install_raqm.sh && popd | ||||
| 
 | ||||
|     # libavif | ||||
|     pushd depends && CMAKE_POLICY_VERSION_MINIMUM=3.5 ./install_libavif.sh && popd | ||||
| 
 | ||||
|     # extra test images | ||||
|     pushd depends && ./install_extra_test_images.sh && popd | ||||
| else | ||||
|  |  | |||
|  | @ -1 +1 @@ | |||
| cibuildwheel==2.23.1 | ||||
| cibuildwheel==2.23.2 | ||||
|  |  | |||
|  | @ -1,5 +1,26 @@ | |||
| # A clang-format style that approximates Python's PEP 7 | ||||
| # Useful for IDE integration | ||||
| Language: C | ||||
| BasedOnStyle: Google | ||||
| AlwaysBreakAfterReturnType: All | ||||
| AllowShortIfStatementsOnASingleLine: false | ||||
| AlignAfterOpenBracket: BlockIndent | ||||
| BinPackArguments: false | ||||
| BinPackParameters: false | ||||
| BreakBeforeBraces: Attach | ||||
| ColumnLimit: 88 | ||||
| DerivePointerAlignment: false | ||||
| IndentGotoLabels: false | ||||
| IndentWidth: 4 | ||||
| PointerAlignment: Right | ||||
| ReflowComments: true | ||||
| SortIncludes: false | ||||
| SpaceBeforeParens: ControlStatements | ||||
| SpacesInParentheses: false | ||||
| TabWidth: 4 | ||||
| UseTab: Never | ||||
| --- | ||||
| Language: Cpp | ||||
| BasedOnStyle: Google | ||||
| AlwaysBreakAfterReturnType: All | ||||
| AllowShortIfStatementsOnASingleLine: false | ||||
|  | @ -11,7 +32,6 @@ ColumnLimit: 88 | |||
| DerivePointerAlignment: false | ||||
| IndentGotoLabels: false | ||||
| IndentWidth: 4 | ||||
| Language: Cpp | ||||
| PointerAlignment: Right | ||||
| ReflowComments: true | ||||
| SortIncludes: false | ||||
|  |  | |||
							
								
								
									
										10
									
								
								.github/workflows/macos-install.sh
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										10
									
								
								.github/workflows/macos-install.sh
									
									
									
									
										vendored
									
									
								
							|  | @ -6,6 +6,8 @@ if [[ "$ImageOS" == "macos13" ]]; then | |||
|     brew uninstall gradle maven | ||||
| fi | ||||
| brew install \ | ||||
|     aom \ | ||||
|     dav1d \ | ||||
|     freetype \ | ||||
|     ghostscript \ | ||||
|     jpeg-turbo \ | ||||
|  | @ -14,6 +16,8 @@ brew install \ | |||
|     libtiff \ | ||||
|     little-cms2 \ | ||||
|     openjpeg \ | ||||
|     rav1e \ | ||||
|     svt-av1 \ | ||||
|     webp | ||||
| export PKG_CONFIG_PATH="/usr/local/opt/openblas/lib/pkgconfig" | ||||
| 
 | ||||
|  | @ -26,6 +30,12 @@ python3 -m pip install -U pytest-cov | |||
| python3 -m pip install -U pytest-timeout | ||||
| python3 -m pip install pyroma | ||||
| python3 -m pip install numpy | ||||
| # optional test dependency, only install if there's a binary package. | ||||
| # fails on beta 3.14 and PyPy | ||||
| python3 -m pip install --only-binary=:all: pyarrow || true | ||||
| 
 | ||||
| # libavif | ||||
| pushd depends && ./install_libavif.sh && popd | ||||
| 
 | ||||
| # extra test images | ||||
| pushd depends && ./install_extra_test_images.sh && popd | ||||
|  |  | |||
							
								
								
									
										2
									
								
								.github/workflows/test-docker.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/test-docker.yml
									
									
									
									
										vendored
									
									
								
							|  | @ -47,8 +47,8 @@ jobs: | |||
|           centos-stream-10-amd64, | ||||
|           debian-12-bookworm-x86, | ||||
|           debian-12-bookworm-amd64, | ||||
|           fedora-40-amd64, | ||||
|           fedora-41-amd64, | ||||
|           fedora-42-amd64, | ||||
|           gentoo, | ||||
|           ubuntu-22.04-jammy-amd64, | ||||
|           ubuntu-24.04-noble-amd64, | ||||
|  |  | |||
							
								
								
									
										1
									
								
								.github/workflows/test-mingw.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.github/workflows/test-mingw.yml
									
									
									
									
										vendored
									
									
								
							|  | @ -60,6 +60,7 @@ jobs: | |||
|               mingw-w64-x86_64-gcc \ | ||||
|               mingw-w64-x86_64-ghostscript \ | ||||
|               mingw-w64-x86_64-lcms2 \ | ||||
|               mingw-w64-x86_64-libavif \ | ||||
|               mingw-w64-x86_64-libimagequant \ | ||||
|               mingw-w64-x86_64-libjpeg-turbo \ | ||||
|               mingw-w64-x86_64-libraqm \ | ||||
|  |  | |||
							
								
								
									
										10
									
								
								.github/workflows/test-windows.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										10
									
								
								.github/workflows/test-windows.yml
									
									
									
									
										vendored
									
									
								
							|  | @ -42,7 +42,7 @@ jobs: | |||
|             # Test the oldest Python on 32-bit | ||||
|             - { python-version: "3.9", architecture: "x86", os: "windows-2019" } | ||||
| 
 | ||||
|     timeout-minutes: 30 | ||||
|     timeout-minutes: 45 | ||||
| 
 | ||||
|     name: Python ${{ matrix.python-version }} (${{ matrix.architecture }}) | ||||
| 
 | ||||
|  | @ -88,6 +88,10 @@ jobs: | |||
|       run: | | ||||
|         python3 -m pip install PyQt6 | ||||
| 
 | ||||
|     - name: Install PyArrow dependency | ||||
|       run: | | ||||
|         python3 -m pip install --only-binary=:all: pyarrow || true | ||||
| 
 | ||||
|     - name: Install dependencies | ||||
|       id: install | ||||
|       run: | | ||||
|  | @ -145,6 +149,10 @@ jobs: | |||
|       if: steps.build-cache.outputs.cache-hit != 'true' | ||||
|       run: "& winbuild\\build\\build_dep_libpng.cmd" | ||||
| 
 | ||||
|     - name: Build dependencies / libavif | ||||
|       if: steps.build-cache.outputs.cache-hit != 'true' && matrix.architecture == 'x64' | ||||
|       run: "& winbuild\\build\\build_dep_libavif.cmd" | ||||
| 
 | ||||
|     # for FreeType WOFF2 font support | ||||
|     - name: Build dependencies / brotli | ||||
|       if: steps.build-cache.outputs.cache-hit != 'true' | ||||
|  |  | |||
							
								
								
									
										2
									
								
								.github/workflows/test.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/test.yml
									
									
									
									
										vendored
									
									
								
							|  | @ -70,7 +70,7 @@ jobs: | |||
|         persist-credentials: false | ||||
| 
 | ||||
|     - name: Set up Python ${{ matrix.python-version }} | ||||
|       uses: Quansight-Labs/setup-python@v5 | ||||
|       uses: actions/setup-python@v5 | ||||
|       with: | ||||
|         python-version: ${{ matrix.python-version }} | ||||
|         allow-prereleases: true | ||||
|  |  | |||
							
								
								
									
										19
									
								
								.github/workflows/wheels-dependencies.sh
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										19
									
								
								.github/workflows/wheels-dependencies.sh
									
									
									
									
										vendored
									
									
								
							|  | @ -25,7 +25,7 @@ else | |||
|     MB_ML_LIBC=${AUDITWHEEL_POLICY::9} | ||||
|     MB_ML_VER=${AUDITWHEEL_POLICY:9} | ||||
| fi | ||||
| PLAT=$CIBW_ARCHS | ||||
| PLAT="${CIBW_ARCHS:-$AUDITWHEEL_ARCH}" | ||||
| 
 | ||||
| # Define custom utilities | ||||
| source wheels/multibuild/common_utils.sh | ||||
|  | @ -38,13 +38,14 @@ ARCHIVE_SDIR=pillow-depends-main | |||
| 
 | ||||
| # Package versions for fresh source builds | ||||
| FREETYPE_VERSION=2.13.3 | ||||
| HARFBUZZ_VERSION=10.4.0 | ||||
| HARFBUZZ_VERSION=11.1.0 | ||||
| LIBPNG_VERSION=1.6.47 | ||||
| JPEGTURBO_VERSION=3.1.0 | ||||
| OPENJPEG_VERSION=2.5.3 | ||||
| XZ_VERSION=5.6.4 | ||||
| XZ_VERSION=5.8.1 | ||||
| TIFF_VERSION=4.7.0 | ||||
| LCMS2_VERSION=2.17 | ||||
| ZLIB_VERSION=1.3.1 | ||||
| ZLIB_NG_VERSION=2.2.4 | ||||
| LIBWEBP_VERSION=1.5.0 | ||||
| BZIP2_VERSION=1.0.8 | ||||
|  | @ -64,11 +65,7 @@ function build_pkg_config { | |||
| 
 | ||||
| function build_zlib_ng { | ||||
|     if [ -e zlib-stamp ]; then return; fi | ||||
|     fetch_unpack https://github.com/zlib-ng/zlib-ng/archive/$ZLIB_NG_VERSION.tar.gz zlib-ng-$ZLIB_NG_VERSION.tar.gz | ||||
|     (cd zlib-ng-$ZLIB_NG_VERSION \ | ||||
|         && ./configure --prefix=$BUILD_PREFIX --zlib-compat \ | ||||
|         && make -j4 \ | ||||
|         && make install) | ||||
|     build_github zlib-ng/zlib-ng $ZLIB_NG_VERSION --zlib-compat | ||||
| 
 | ||||
|     if [ -n "$IS_MACOS" ]; then | ||||
|         # Ensure that on macOS, the library name is an absolute path, not an | ||||
|  | @ -95,7 +92,7 @@ function build_harfbuzz { | |||
| 
 | ||||
|     local out_dir=$(fetch_unpack https://github.com/harfbuzz/harfbuzz/releases/download/$HARFBUZZ_VERSION/harfbuzz-$HARFBUZZ_VERSION.tar.xz harfbuzz-$HARFBUZZ_VERSION.tar.xz) | ||||
|     (cd $out_dir \ | ||||
|         && meson setup build --prefix=$BUILD_PREFIX --libdir=$BUILD_PREFIX/lib --buildtype=release -Dfreetype=enabled -Dglib=disabled) | ||||
|         && meson setup build --prefix=$BUILD_PREFIX --libdir=$BUILD_PREFIX/lib --buildtype=minsize -Dfreetype=enabled -Dglib=disabled -Dtests=disabled) | ||||
|     (cd $out_dir/build \ | ||||
|         && meson install) | ||||
|     touch harfbuzz-stamp | ||||
|  | @ -106,7 +103,11 @@ function build { | |||
|     if [ -z "$IS_ALPINE" ] && [ -z "$SANITIZER" ] && [ -z "$IS_MACOS" ]; then | ||||
|         yum remove -y zlib-devel | ||||
|     fi | ||||
|     if [[ -n "$IS_MACOS" ]] && [[ "$MACOSX_DEPLOYMENT_TARGET" == "10.10" || "$MACOSX_DEPLOYMENT_TARGET" == "10.13" ]]; then | ||||
|         build_new_zlib | ||||
|     else | ||||
|         build_zlib_ng | ||||
|     fi | ||||
| 
 | ||||
|     build_simple xcb-proto 1.17.0 https://xorg.freedesktop.org/archive/individual/proto | ||||
|     if [ -n "$IS_MACOS" ]; then | ||||
|  |  | |||
							
								
								
									
										9
									
								
								.github/workflows/wheels.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										9
									
								
								.github/workflows/wheels.yml
									
									
									
									
										vendored
									
									
								
							|  | @ -121,14 +121,17 @@ jobs: | |||
|   windows: | ||||
|     if: github.event_name != 'schedule' || github.repository_owner == 'python-pillow' | ||||
|     name: Windows ${{ matrix.cibw_arch }} | ||||
|     runs-on: windows-latest | ||||
|     runs-on: ${{ matrix.os }} | ||||
|     strategy: | ||||
|       fail-fast: false | ||||
|       matrix: | ||||
|         include: | ||||
|           - cibw_arch: x86 | ||||
|             os: windows-latest | ||||
|           - cibw_arch: AMD64 | ||||
|             os: windows-latest | ||||
|           - cibw_arch: ARM64 | ||||
|             os: windows-11-arm | ||||
|     steps: | ||||
|       - uses: actions/checkout@v4 | ||||
|         with: | ||||
|  | @ -157,7 +160,7 @@ jobs: | |||
|           # Install extra test images | ||||
|           xcopy /S /Y Tests\test-images\* Tests\images | ||||
| 
 | ||||
|           & python.exe winbuild\build_prepare.py -v --no-imagequant --architecture=${{ matrix.cibw_arch }} | ||||
|           & python.exe winbuild\build_prepare.py -v --no-imagequant --no-avif --architecture=${{ matrix.cibw_arch }} | ||||
|         shell: pwsh | ||||
| 
 | ||||
|       - name: Build wheels | ||||
|  | @ -240,7 +243,7 @@ jobs: | |||
|           path: dist | ||||
|           merge-multiple: true | ||||
|       - name: Upload wheels to scientific-python-nightly-wheels | ||||
|         uses: scientific-python/upload-nightly-action@82396a2ed4269ba06c6b2988bb4fd568ef3c3d6b # 0.6.1 | ||||
|         uses: scientific-python/upload-nightly-action@b36e8c0c10dbcfd2e05bf95f17ef8c14fd708dbf # 0.6.2 | ||||
|         with: | ||||
|           artifacts_path: dist | ||||
|           anaconda_nightly_upload_token: ${{ secrets.ANACONDA_ORG_UPLOAD_TOKEN }} | ||||
|  |  | |||
|  | @ -1,6 +1,6 @@ | |||
| repos: | ||||
|   - repo: https://github.com/astral-sh/ruff-pre-commit | ||||
|     rev: v0.9.9 | ||||
|     rev: v0.11.4 | ||||
|     hooks: | ||||
|       - id: ruff | ||||
|         args: [--exit-non-zero-on-fix] | ||||
|  | @ -24,7 +24,7 @@ repos: | |||
|         exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.gd$|\.opt$) | ||||
| 
 | ||||
|   - repo: https://github.com/pre-commit/mirrors-clang-format | ||||
|     rev: v19.1.7 | ||||
|     rev: v20.1.0 | ||||
|     hooks: | ||||
|       - id: clang-format | ||||
|         types: [c] | ||||
|  | @ -44,20 +44,21 @@ repos: | |||
|       - id: check-json | ||||
|       - id: check-toml | ||||
|       - id: check-yaml | ||||
|         args: [--allow-multiple-documents] | ||||
|       - id: end-of-file-fixer | ||||
|         exclude: ^Tests/images/ | ||||
|       - id: trailing-whitespace | ||||
|         exclude: ^.github/.*TEMPLATE|^Tests/(fonts|images)/ | ||||
| 
 | ||||
|   - repo: https://github.com/python-jsonschema/check-jsonschema | ||||
|     rev: 0.31.2 | ||||
|     rev: 0.32.1 | ||||
|     hooks: | ||||
|       - id: check-github-workflows | ||||
|       - id: check-readthedocs | ||||
|       - id: check-renovate | ||||
| 
 | ||||
|   - repo: https://github.com/woodruffw/zizmor-pre-commit | ||||
|     rev: v1.4.1 | ||||
|     rev: v1.5.2 | ||||
|     hooks: | ||||
|       - id: zizmor | ||||
| 
 | ||||
|  | @ -72,7 +73,7 @@ repos: | |||
|       - id: pyproject-fmt | ||||
| 
 | ||||
|   - repo: https://github.com/abravalheri/validate-pyproject | ||||
|     rev: v0.23 | ||||
|     rev: v0.24.1 | ||||
|     hooks: | ||||
|       - id: validate-pyproject | ||||
|         additional_dependencies: [trove-classifiers>=2024.10.12] | ||||
|  |  | |||
							
								
								
									
										5
									
								
								Makefile
									
									
									
									
									
								
							
							
						
						
									
										5
									
								
								Makefile
									
									
									
									
									
								
							|  | @ -23,6 +23,10 @@ doc html: | |||
| htmlview: | ||||
| 	$(MAKE) -C docs htmlview | ||||
| 
 | ||||
| .PHONY: htmllive | ||||
| htmllive: | ||||
| 	$(MAKE) -C docs htmllive | ||||
| 
 | ||||
| .PHONY: doccheck | ||||
| doccheck: | ||||
| 	$(MAKE) doc | ||||
|  | @ -43,6 +47,7 @@ help: | |||
| 	@echo "  docserve           run an HTTP server on the docs directory" | ||||
| 	@echo "  html               make HTML docs" | ||||
| 	@echo "  htmlview           open the index page built by the html target in your browser" | ||||
| 	@echo "  htmllive           rebuild and reload HTML files in your browser" | ||||
| 	@echo "  install            make and install" | ||||
| 	@echo "  install-coverage   make and install with C coverage" | ||||
| 	@echo "  lint               run the lint checks" | ||||
|  |  | |||
|  | @ -1,9 +1,12 @@ | |||
| from __future__ import annotations | ||||
| 
 | ||||
| import platform | ||||
| import sys | ||||
| 
 | ||||
| from PIL import features | ||||
| 
 | ||||
| from .helper import is_pypy | ||||
| 
 | ||||
| 
 | ||||
| def test_wheel_modules() -> None: | ||||
|     expected_modules = {"pil", "tkinter", "freetype2", "littlecms2", "webp"} | ||||
|  | @ -40,5 +43,7 @@ def test_wheel_features() -> None: | |||
| 
 | ||||
|     if sys.platform == "win32": | ||||
|         expected_features.remove("xcb") | ||||
|     elif sys.platform == "darwin" and not is_pypy() and platform.processor() != "arm": | ||||
|         expected_features.remove("zlib_ng") | ||||
| 
 | ||||
|     assert set(features.get_supported_features()) == expected_features | ||||
|  |  | |||
							
								
								
									
										
											BIN
										
									
								
								Tests/images/avif/exif.avif
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								Tests/images/avif/exif.avif
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								Tests/images/avif/hopper-missing-pixi.avif
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								Tests/images/avif/hopper-missing-pixi.avif
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								Tests/images/avif/hopper.avif
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								Tests/images/avif/hopper.avif
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								Tests/images/avif/hopper.heif
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								Tests/images/avif/hopper.heif
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								Tests/images/avif/hopper_avif_write.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								Tests/images/avif/hopper_avif_write.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 30 KiB | 
							
								
								
									
										
											BIN
										
									
								
								Tests/images/avif/icc_profile.avif
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								Tests/images/avif/icc_profile.avif
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								Tests/images/avif/icc_profile_none.avif
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								Tests/images/avif/icc_profile_none.avif
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								Tests/images/avif/rot0mir0.avif
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								Tests/images/avif/rot0mir0.avif
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								Tests/images/avif/rot0mir1.avif
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								Tests/images/avif/rot0mir1.avif
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								Tests/images/avif/rot1mir0.avif
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								Tests/images/avif/rot1mir0.avif
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								Tests/images/avif/rot1mir1.avif
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								Tests/images/avif/rot1mir1.avif
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								Tests/images/avif/rot2mir0.avif
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								Tests/images/avif/rot2mir0.avif
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								Tests/images/avif/rot2mir1.avif
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								Tests/images/avif/rot2mir1.avif
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								Tests/images/avif/rot3mir0.avif
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								Tests/images/avif/rot3mir0.avif
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								Tests/images/avif/rot3mir1.avif
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								Tests/images/avif/rot3mir1.avif
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								Tests/images/avif/star.avifs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								Tests/images/avif/star.avifs
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								Tests/images/avif/star.gif
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								Tests/images/avif/star.gif
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 2.8 KiB | 
							
								
								
									
										
											BIN
										
									
								
								Tests/images/avif/star.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								Tests/images/avif/star.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 3.8 KiB | 
							
								
								
									
										
											BIN
										
									
								
								Tests/images/avif/transparency.avif
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								Tests/images/avif/transparency.avif
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								Tests/images/avif/xmp_tags_orientation.avif
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								Tests/images/avif/xmp_tags_orientation.avif
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								Tests/images/drawing_emf_ref_72_144.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								Tests/images/drawing_emf_ref_72_144.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 984 B | 
							
								
								
									
										260
									
								
								Tests/images/full_gimp_palette.gpl
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										260
									
								
								Tests/images/full_gimp_palette.gpl
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,260 @@ | |||
| GIMP Palette | ||||
| Name: fullpalette | ||||
| Columns: 4 | ||||
| # | ||||
|   0   0   0     Index 0 | ||||
|   1   1   1     Index 1 | ||||
|   2   2   2     Index 2 | ||||
|   3   3   3     Index 3 | ||||
|   4   4   4     Index 4 | ||||
|   5   5   5     Index 5 | ||||
|   6   6   6     Index 6 | ||||
|   7   7   7     Index 7 | ||||
|   8   8   8     Index 8 | ||||
|   9   9   9     Index 9 | ||||
|  10  10  10     Index 10 | ||||
|  11  11  11     Index 11 | ||||
|  12  12  12     Index 12 | ||||
|  13  13  13     Index 13 | ||||
|  14  14  14     Index 14 | ||||
|  15  15  15     Index 15 | ||||
|  16  16  16     Index 16 | ||||
|  17  17  17     Index 17 | ||||
|  18  18  18     Index 18 | ||||
|  19  19  19     Index 19 | ||||
|  20  20  20     Index 20 | ||||
|  21  21  21     Index 21 | ||||
|  22  22  22     Index 22 | ||||
|  23  23  23     Index 23 | ||||
|  24  24  24     Index 24 | ||||
|  25  25  25     Index 25 | ||||
|  26  26  26     Index 26 | ||||
|  27  27  27     Index 27 | ||||
|  28  28  28     Index 28 | ||||
|  29  29  29     Index 29 | ||||
|  30  30  30     Index 30 | ||||
|  31  31  31     Index 31 | ||||
|  32  32  32     Index 32 | ||||
|  33  33  33     Index 33 | ||||
|  34  34  34     Index 34 | ||||
|  35  35  35     Index 35 | ||||
|  36  36  36     Index 36 | ||||
|  37  37  37     Index 37 | ||||
|  38  38  38     Index 38 | ||||
|  39  39  39     Index 39 | ||||
|  40  40  40     Index 40 | ||||
|  41  41  41     Index 41 | ||||
|  42  42  42     Index 42 | ||||
|  43  43  43     Index 43 | ||||
|  44  44  44     Index 44 | ||||
|  45  45  45     Index 45 | ||||
|  46  46  46     Index 46 | ||||
|  47  47  47     Index 47 | ||||
|  48  48  48     Index 48 | ||||
|  49  49  49     Index 49 | ||||
|  50  50  50     Index 50 | ||||
|  51  51  51     Index 51 | ||||
|  52  52  52     Index 52 | ||||
|  53  53  53     Index 53 | ||||
|  54  54  54     Index 54 | ||||
|  55  55  55     Index 55 | ||||
|  56  56  56     Index 56 | ||||
|  57  57  57     Index 57 | ||||
|  58  58  58     Index 58 | ||||
|  59  59  59     Index 59 | ||||
|  60  60  60     Index 60 | ||||
|  61  61  61     Index 61 | ||||
|  62  62  62     Index 62 | ||||
|  63  63  63     Index 63 | ||||
|  64  64  64     Index 64 | ||||
|  65  65  65     Index 65 | ||||
|  66  66  66     Index 66 | ||||
|  67  67  67     Index 67 | ||||
|  68  68  68     Index 68 | ||||
|  69  69  69     Index 69 | ||||
|  70  70  70     Index 70 | ||||
|  71  71  71     Index 71 | ||||
|  72  72  72     Index 72 | ||||
|  73  73  73     Index 73 | ||||
|  74  74  74     Index 74 | ||||
|  75  75  75     Index 75 | ||||
|  76  76  76     Index 76 | ||||
|  77  77  77     Index 77 | ||||
|  78  78  78     Index 78 | ||||
|  79  79  79     Index 79 | ||||
|  80  80  80     Index 80 | ||||
|  81  81  81     Index 81 | ||||
|  82  82  82     Index 82 | ||||
|  83  83  83     Index 83 | ||||
|  84  84  84     Index 84 | ||||
|  85  85  85     Index 85 | ||||
|  86  86  86     Index 86 | ||||
|  87  87  87     Index 87 | ||||
|  88  88  88     Index 88 | ||||
|  89  89  89     Index 89 | ||||
|  90  90  90     Index 90 | ||||
|  91  91  91     Index 91 | ||||
|  92  92  92     Index 92 | ||||
|  93  93  93     Index 93 | ||||
|  94  94  94     Index 94 | ||||
|  95  95  95     Index 95 | ||||
|  96  96  96     Index 96 | ||||
|  97  97  97     Index 97 | ||||
|  98  98  98     Index 98 | ||||
|  99  99  99     Index 99 | ||||
| 100 100 100     Index 100 | ||||
| 101 101 101     Index 101 | ||||
| 102 102 102     Index 102 | ||||
| 103 103 103     Index 103 | ||||
| 104 104 104     Index 104 | ||||
| 105 105 105     Index 105 | ||||
| 106 106 106     Index 106 | ||||
| 107 107 107     Index 107 | ||||
| 108 108 108     Index 108 | ||||
| 109 109 109     Index 109 | ||||
| 110 110 110     Index 110 | ||||
| 111 111 111     Index 111 | ||||
| 112 112 112     Index 112 | ||||
| 113 113 113     Index 113 | ||||
| 114 114 114     Index 114 | ||||
| 115 115 115     Index 115 | ||||
| 116 116 116     Index 116 | ||||
| 117 117 117     Index 117 | ||||
| 118 118 118     Index 118 | ||||
| 119 119 119     Index 119 | ||||
| 120 120 120     Index 120 | ||||
| 121 121 121     Index 121 | ||||
| 122 122 122     Index 122 | ||||
| 123 123 123     Index 123 | ||||
| 124 124 124     Index 124 | ||||
| 125 125 125     Index 125 | ||||
| 126 126 126     Index 126 | ||||
| 127 127 127     Index 127 | ||||
| 128 128 128     Index 128 | ||||
| 129 129 129     Index 129 | ||||
| 130 130 130     Index 130 | ||||
| 131 131 131     Index 131 | ||||
| 132 132 132     Index 132 | ||||
| 133 133 133     Index 133 | ||||
| 134 134 134     Index 134 | ||||
| 135 135 135     Index 135 | ||||
| 136 136 136     Index 136 | ||||
| 137 137 137     Index 137 | ||||
| 138 138 138     Index 138 | ||||
| 139 139 139     Index 139 | ||||
| 140 140 140     Index 140 | ||||
| 141 141 141     Index 141 | ||||
| 142 142 142     Index 142 | ||||
| 143 143 143     Index 143 | ||||
| 144 144 144     Index 144 | ||||
| 145 145 145     Index 145 | ||||
| 146 146 146     Index 146 | ||||
| 147 147 147     Index 147 | ||||
| 148 148 148     Index 148 | ||||
| 149 149 149     Index 149 | ||||
| 150 150 150     Index 150 | ||||
| 151 151 151     Index 151 | ||||
| 152 152 152     Index 152 | ||||
| 153 153 153     Index 153 | ||||
| 154 154 154     Index 154 | ||||
| 155 155 155     Index 155 | ||||
| 156 156 156     Index 156 | ||||
| 157 157 157     Index 157 | ||||
| 158 158 158     Index 158 | ||||
| 159 159 159     Index 159 | ||||
| 160 160 160     Index 160 | ||||
| 161 161 161     Index 161 | ||||
| 162 162 162     Index 162 | ||||
| 163 163 163     Index 163 | ||||
| 164 164 164     Index 164 | ||||
| 165 165 165     Index 165 | ||||
| 166 166 166     Index 166 | ||||
| 167 167 167     Index 167 | ||||
| 168 168 168     Index 168 | ||||
| 169 169 169     Index 169 | ||||
| 170 170 170     Index 170 | ||||
| 171 171 171     Index 171 | ||||
| 172 172 172     Index 172 | ||||
| 173 173 173     Index 173 | ||||
| 174 174 174     Index 174 | ||||
| 175 175 175     Index 175 | ||||
| 176 176 176     Index 176 | ||||
| 177 177 177     Index 177 | ||||
| 178 178 178     Index 178 | ||||
| 179 179 179     Index 179 | ||||
| 180 180 180     Index 180 | ||||
| 181 181 181     Index 181 | ||||
| 182 182 182     Index 182 | ||||
| 183 183 183     Index 183 | ||||
| 184 184 184     Index 184 | ||||
| 185 185 185     Index 185 | ||||
| 186 186 186     Index 186 | ||||
| 187 187 187     Index 187 | ||||
| 188 188 188     Index 188 | ||||
| 189 189 189     Index 189 | ||||
| 190 190 190     Index 190 | ||||
| 191 191 191     Index 191 | ||||
| 192 192 192     Index 192 | ||||
| 193 193 193     Index 193 | ||||
| 194 194 194     Index 194 | ||||
| 195 195 195     Index 195 | ||||
| 196 196 196     Index 196 | ||||
| 197 197 197     Index 197 | ||||
| 198 198 198     Index 198 | ||||
| 199 199 199     Index 199 | ||||
| 200 200 200     Index 200 | ||||
| 201 201 201     Index 201 | ||||
| 202 202 202     Index 202 | ||||
| 203 203 203     Index 203 | ||||
| 204 204 204     Index 204 | ||||
| 205 205 205     Index 205 | ||||
| 206 206 206     Index 206 | ||||
| 207 207 207     Index 207 | ||||
| 208 208 208     Index 208 | ||||
| 209 209 209     Index 209 | ||||
| 210 210 210     Index 210 | ||||
| 211 211 211     Index 211 | ||||
| 212 212 212     Index 212 | ||||
| 213 213 213     Index 213 | ||||
| 214 214 214     Index 214 | ||||
| 215 215 215     Index 215 | ||||
| 216 216 216     Index 216 | ||||
| 217 217 217     Index 217 | ||||
| 218 218 218     Index 218 | ||||
| 219 219 219     Index 219 | ||||
| 220 220 220     Index 220 | ||||
| 221 221 221     Index 221 | ||||
| 222 222 222     Index 222 | ||||
| 223 223 223     Index 223 | ||||
| 224 224 224     Index 224 | ||||
| 225 225 225     Index 225 | ||||
| 226 226 226     Index 226 | ||||
| 227 227 227     Index 227 | ||||
| 228 228 228     Index 228 | ||||
| 229 229 229     Index 229 | ||||
| 230 230 230     Index 230 | ||||
| 231 231 231     Index 231 | ||||
| 232 232 232     Index 232 | ||||
| 233 233 233     Index 233 | ||||
| 234 234 234     Index 234 | ||||
| 235 235 235     Index 235 | ||||
| 236 236 236     Index 236 | ||||
| 237 237 237     Index 237 | ||||
| 238 238 238     Index 238 | ||||
| 239 239 239     Index 239 | ||||
| 240 240 240     Index 240 | ||||
| 241 241 241     Index 241 | ||||
| 242 242 242     Index 242 | ||||
| 243 243 243     Index 243 | ||||
| 244 244 244     Index 244 | ||||
| 245 245 245     Index 245 | ||||
| 246 246 246     Index 246 | ||||
| 247 247 247     Index 247 | ||||
| 248 248 248     Index 248 | ||||
| 249 249 249     Index 249 | ||||
| 250 250 250     Index 250 | ||||
| 251 251 251     Index 251 | ||||
| 252 252 252     Index 252 | ||||
| 253 253 253     Index 253 | ||||
| 254 254 254     Index 254 | ||||
| 255 255 255     Index 255 | ||||
										
											Binary file not shown.
										
									
								
							| Before Width: | Height: | Size: 533 B After Width: | Height: | Size: 533 B | 
							
								
								
									
										164
									
								
								Tests/test_arrow.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										164
									
								
								Tests/test_arrow.py
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,164 @@ | |||
| from __future__ import annotations | ||||
| 
 | ||||
| import pytest | ||||
| 
 | ||||
| from PIL import Image | ||||
| 
 | ||||
| from .helper import hopper | ||||
| 
 | ||||
| 
 | ||||
| @pytest.mark.parametrize( | ||||
|     "mode, dest_modes", | ||||
|     ( | ||||
|         ("L", ["I", "F", "LA", "RGB", "RGBA", "RGBX", "CMYK", "YCbCr", "HSV"]), | ||||
|         ("I", ["L", "F"]),  # Technically I;32 can work for any 4x8bit storage. | ||||
|         ("F", ["I", "L", "LA", "RGB", "RGBA", "RGBX", "CMYK", "YCbCr", "HSV"]), | ||||
|         ("LA", ["L", "F"]), | ||||
|         ("RGB", ["L", "F"]), | ||||
|         ("RGBA", ["L", "F"]), | ||||
|         ("RGBX", ["L", "F"]), | ||||
|         ("CMYK", ["L", "F"]), | ||||
|         ("YCbCr", ["L", "F"]), | ||||
|         ("HSV", ["L", "F"]), | ||||
|     ), | ||||
| ) | ||||
| def test_invalid_array_type(mode: str, dest_modes: list[str]) -> None: | ||||
|     img = hopper(mode) | ||||
|     for dest_mode in dest_modes: | ||||
|         with pytest.raises(ValueError): | ||||
|             Image.fromarrow(img, dest_mode, img.size) | ||||
| 
 | ||||
| 
 | ||||
| def test_invalid_array_size() -> None: | ||||
|     img = hopper("RGB") | ||||
| 
 | ||||
|     assert img.size != (10, 10) | ||||
|     with pytest.raises(ValueError): | ||||
|         Image.fromarrow(img, "RGB", (10, 10)) | ||||
| 
 | ||||
| 
 | ||||
| def test_release_schema() -> None: | ||||
|     # these should not error out, valgrind should be clean | ||||
|     img = hopper("L") | ||||
|     schema = img.__arrow_c_schema__() | ||||
|     del schema | ||||
| 
 | ||||
| 
 | ||||
| def test_release_array() -> None: | ||||
|     # these should not error out, valgrind should be clean | ||||
|     img = hopper("L") | ||||
|     array, schema = img.__arrow_c_array__() | ||||
|     del array | ||||
|     del schema | ||||
| 
 | ||||
| 
 | ||||
| def test_readonly() -> None: | ||||
|     img = hopper("L") | ||||
|     reloaded = Image.fromarrow(img, img.mode, img.size) | ||||
|     assert reloaded.readonly == 1 | ||||
|     reloaded._readonly = 0 | ||||
|     assert reloaded.readonly == 1 | ||||
| 
 | ||||
| 
 | ||||
| def test_multiblock_l_image() -> None: | ||||
|     block_size = Image.core.get_block_size() | ||||
| 
 | ||||
|     # check a 2 block image in single channel mode | ||||
|     size = (4096, 2 * block_size // 4096) | ||||
|     img = Image.new("L", size, 128) | ||||
| 
 | ||||
|     with pytest.raises(ValueError): | ||||
|         (schema, arr) = img.__arrow_c_array__() | ||||
| 
 | ||||
| 
 | ||||
| def test_multiblock_rgba_image() -> None: | ||||
|     block_size = Image.core.get_block_size() | ||||
| 
 | ||||
|     # check a 2 block image in 4 channel mode | ||||
|     size = (4096, (block_size // 4096) // 2) | ||||
|     img = Image.new("RGBA", size, (128, 127, 126, 125)) | ||||
| 
 | ||||
|     with pytest.raises(ValueError): | ||||
|         (schema, arr) = img.__arrow_c_array__() | ||||
| 
 | ||||
| 
 | ||||
| def test_multiblock_l_schema() -> None: | ||||
|     block_size = Image.core.get_block_size() | ||||
| 
 | ||||
|     # check a 2 block image in single channel mode | ||||
|     size = (4096, 2 * block_size // 4096) | ||||
|     img = Image.new("L", size, 128) | ||||
| 
 | ||||
|     with pytest.raises(ValueError): | ||||
|         img.__arrow_c_schema__() | ||||
| 
 | ||||
| 
 | ||||
| def test_multiblock_rgba_schema() -> None: | ||||
|     block_size = Image.core.get_block_size() | ||||
| 
 | ||||
|     # check a 2 block image in 4 channel mode | ||||
|     size = (4096, (block_size // 4096) // 2) | ||||
|     img = Image.new("RGBA", size, (128, 127, 126, 125)) | ||||
| 
 | ||||
|     with pytest.raises(ValueError): | ||||
|         img.__arrow_c_schema__() | ||||
| 
 | ||||
| 
 | ||||
| def test_singleblock_l_image() -> None: | ||||
|     Image.core.set_use_block_allocator(1) | ||||
| 
 | ||||
|     block_size = Image.core.get_block_size() | ||||
| 
 | ||||
|     # check a 2 block image in 4 channel mode | ||||
|     size = (4096, 2 * (block_size // 4096)) | ||||
|     img = Image.new("L", size, 128) | ||||
|     assert img.im.isblock() | ||||
| 
 | ||||
|     (schema, arr) = img.__arrow_c_array__() | ||||
|     assert schema | ||||
|     assert arr | ||||
| 
 | ||||
|     Image.core.set_use_block_allocator(0) | ||||
| 
 | ||||
| 
 | ||||
| def test_singleblock_rgba_image() -> None: | ||||
|     Image.core.set_use_block_allocator(1) | ||||
|     block_size = Image.core.get_block_size() | ||||
| 
 | ||||
|     # check a 2 block image in 4 channel mode | ||||
|     size = (4096, (block_size // 4096) // 2) | ||||
|     img = Image.new("RGBA", size, (128, 127, 126, 125)) | ||||
|     assert img.im.isblock() | ||||
| 
 | ||||
|     (schema, arr) = img.__arrow_c_array__() | ||||
|     assert schema | ||||
|     assert arr | ||||
|     Image.core.set_use_block_allocator(0) | ||||
| 
 | ||||
| 
 | ||||
| def test_singleblock_l_schema() -> None: | ||||
|     Image.core.set_use_block_allocator(1) | ||||
|     block_size = Image.core.get_block_size() | ||||
| 
 | ||||
|     # check a 2 block image in single channel mode | ||||
|     size = (4096, 2 * block_size // 4096) | ||||
|     img = Image.new("L", size, 128) | ||||
|     assert img.im.isblock() | ||||
| 
 | ||||
|     schema = img.__arrow_c_schema__() | ||||
|     assert schema | ||||
|     Image.core.set_use_block_allocator(0) | ||||
| 
 | ||||
| 
 | ||||
| def test_singleblock_rgba_schema() -> None: | ||||
|     Image.core.set_use_block_allocator(1) | ||||
|     block_size = Image.core.get_block_size() | ||||
| 
 | ||||
|     # check a 2 block image in 4 channel mode | ||||
|     size = (4096, (block_size // 4096) // 2) | ||||
|     img = Image.new("RGBA", size, (128, 127, 126, 125)) | ||||
|     assert img.im.isblock() | ||||
| 
 | ||||
|     schema = img.__arrow_c_schema__() | ||||
|     assert schema | ||||
|     Image.core.set_use_block_allocator(0) | ||||
|  | @ -13,6 +13,7 @@ from PIL import Image, ImageSequence, PngImagePlugin | |||
| # (referenced from https://wiki.mozilla.org/APNG_Specification) | ||||
| def test_apng_basic() -> None: | ||||
|     with Image.open("Tests/images/apng/single_frame.png") as im: | ||||
|         assert isinstance(im, PngImagePlugin.PngImageFile) | ||||
|         assert not im.is_animated | ||||
|         assert im.n_frames == 1 | ||||
|         assert im.get_format_mimetype() == "image/apng" | ||||
|  | @ -21,6 +22,7 @@ def test_apng_basic() -> None: | |||
|         assert im.getpixel((64, 32)) == (0, 255, 0, 255) | ||||
| 
 | ||||
|     with Image.open("Tests/images/apng/single_frame_default.png") as im: | ||||
|         assert isinstance(im, PngImagePlugin.PngImageFile) | ||||
|         assert im.is_animated | ||||
|         assert im.n_frames == 2 | ||||
|         assert im.get_format_mimetype() == "image/apng" | ||||
|  | @ -53,6 +55,7 @@ def test_apng_basic() -> None: | |||
| ) | ||||
| def test_apng_fdat(filename: str) -> None: | ||||
|     with Image.open(filename) as im: | ||||
|         assert isinstance(im, PngImagePlugin.PngImageFile) | ||||
|         im.seek(im.n_frames - 1) | ||||
|         assert im.getpixel((0, 0)) == (0, 255, 0, 255) | ||||
|         assert im.getpixel((64, 32)) == (0, 255, 0, 255) | ||||
|  | @ -60,31 +63,37 @@ def test_apng_fdat(filename: str) -> None: | |||
| 
 | ||||
| def test_apng_dispose() -> None: | ||||
|     with Image.open("Tests/images/apng/dispose_op_none.png") as im: | ||||
|         assert isinstance(im, PngImagePlugin.PngImageFile) | ||||
|         im.seek(im.n_frames - 1) | ||||
|         assert im.getpixel((0, 0)) == (0, 255, 0, 255) | ||||
|         assert im.getpixel((64, 32)) == (0, 255, 0, 255) | ||||
| 
 | ||||
|     with Image.open("Tests/images/apng/dispose_op_background.png") as im: | ||||
|         assert isinstance(im, PngImagePlugin.PngImageFile) | ||||
|         im.seek(im.n_frames - 1) | ||||
|         assert im.getpixel((0, 0)) == (0, 0, 0, 0) | ||||
|         assert im.getpixel((64, 32)) == (0, 0, 0, 0) | ||||
| 
 | ||||
|     with Image.open("Tests/images/apng/dispose_op_background_final.png") as im: | ||||
|         assert isinstance(im, PngImagePlugin.PngImageFile) | ||||
|         im.seek(im.n_frames - 1) | ||||
|         assert im.getpixel((0, 0)) == (0, 255, 0, 255) | ||||
|         assert im.getpixel((64, 32)) == (0, 255, 0, 255) | ||||
| 
 | ||||
|     with Image.open("Tests/images/apng/dispose_op_previous.png") as im: | ||||
|         assert isinstance(im, PngImagePlugin.PngImageFile) | ||||
|         im.seek(im.n_frames - 1) | ||||
|         assert im.getpixel((0, 0)) == (0, 255, 0, 255) | ||||
|         assert im.getpixel((64, 32)) == (0, 255, 0, 255) | ||||
| 
 | ||||
|     with Image.open("Tests/images/apng/dispose_op_previous_final.png") as im: | ||||
|         assert isinstance(im, PngImagePlugin.PngImageFile) | ||||
|         im.seek(im.n_frames - 1) | ||||
|         assert im.getpixel((0, 0)) == (0, 255, 0, 255) | ||||
|         assert im.getpixel((64, 32)) == (0, 255, 0, 255) | ||||
| 
 | ||||
|     with Image.open("Tests/images/apng/dispose_op_previous_first.png") as im: | ||||
|         assert isinstance(im, PngImagePlugin.PngImageFile) | ||||
|         im.seek(im.n_frames - 1) | ||||
|         assert im.getpixel((0, 0)) == (0, 0, 0, 0) | ||||
|         assert im.getpixel((64, 32)) == (0, 0, 0, 0) | ||||
|  | @ -92,21 +101,25 @@ def test_apng_dispose() -> None: | |||
| 
 | ||||
| def test_apng_dispose_region() -> None: | ||||
|     with Image.open("Tests/images/apng/dispose_op_none_region.png") as im: | ||||
|         assert isinstance(im, PngImagePlugin.PngImageFile) | ||||
|         im.seek(im.n_frames - 1) | ||||
|         assert im.getpixel((0, 0)) == (0, 255, 0, 255) | ||||
|         assert im.getpixel((64, 32)) == (0, 255, 0, 255) | ||||
| 
 | ||||
|     with Image.open("Tests/images/apng/dispose_op_background_before_region.png") as im: | ||||
|         assert isinstance(im, PngImagePlugin.PngImageFile) | ||||
|         im.seek(im.n_frames - 1) | ||||
|         assert im.getpixel((0, 0)) == (0, 0, 0, 0) | ||||
|         assert im.getpixel((64, 32)) == (0, 0, 0, 0) | ||||
| 
 | ||||
|     with Image.open("Tests/images/apng/dispose_op_background_region.png") as im: | ||||
|         assert isinstance(im, PngImagePlugin.PngImageFile) | ||||
|         im.seek(im.n_frames - 1) | ||||
|         assert im.getpixel((0, 0)) == (0, 0, 255, 255) | ||||
|         assert im.getpixel((64, 32)) == (0, 0, 0, 0) | ||||
| 
 | ||||
|     with Image.open("Tests/images/apng/dispose_op_previous_region.png") as im: | ||||
|         assert isinstance(im, PngImagePlugin.PngImageFile) | ||||
|         im.seek(im.n_frames - 1) | ||||
|         assert im.getpixel((0, 0)) == (0, 255, 0, 255) | ||||
|         assert im.getpixel((64, 32)) == (0, 255, 0, 255) | ||||
|  | @ -133,6 +146,7 @@ def test_apng_dispose_op_previous_frame() -> None: | |||
|     #     ], | ||||
|     # ) | ||||
|     with Image.open("Tests/images/apng/dispose_op_previous_frame.png") as im: | ||||
|         assert isinstance(im, PngImagePlugin.PngImageFile) | ||||
|         im.seek(im.n_frames - 1) | ||||
|         assert im.getpixel((0, 0)) == (255, 0, 0, 255) | ||||
| 
 | ||||
|  | @ -146,26 +160,31 @@ def test_apng_dispose_op_background_p_mode() -> None: | |||
| 
 | ||||
| def test_apng_blend() -> None: | ||||
|     with Image.open("Tests/images/apng/blend_op_source_solid.png") as im: | ||||
|         assert isinstance(im, PngImagePlugin.PngImageFile) | ||||
|         im.seek(im.n_frames - 1) | ||||
|         assert im.getpixel((0, 0)) == (0, 255, 0, 255) | ||||
|         assert im.getpixel((64, 32)) == (0, 255, 0, 255) | ||||
| 
 | ||||
|     with Image.open("Tests/images/apng/blend_op_source_transparent.png") as im: | ||||
|         assert isinstance(im, PngImagePlugin.PngImageFile) | ||||
|         im.seek(im.n_frames - 1) | ||||
|         assert im.getpixel((0, 0)) == (0, 0, 0, 0) | ||||
|         assert im.getpixel((64, 32)) == (0, 0, 0, 0) | ||||
| 
 | ||||
|     with Image.open("Tests/images/apng/blend_op_source_near_transparent.png") as im: | ||||
|         assert isinstance(im, PngImagePlugin.PngImageFile) | ||||
|         im.seek(im.n_frames - 1) | ||||
|         assert im.getpixel((0, 0)) == (0, 255, 0, 2) | ||||
|         assert im.getpixel((64, 32)) == (0, 255, 0, 2) | ||||
| 
 | ||||
|     with Image.open("Tests/images/apng/blend_op_over.png") as im: | ||||
|         assert isinstance(im, PngImagePlugin.PngImageFile) | ||||
|         im.seek(im.n_frames - 1) | ||||
|         assert im.getpixel((0, 0)) == (0, 255, 0, 255) | ||||
|         assert im.getpixel((64, 32)) == (0, 255, 0, 255) | ||||
| 
 | ||||
|     with Image.open("Tests/images/apng/blend_op_over_near_transparent.png") as im: | ||||
|         assert isinstance(im, PngImagePlugin.PngImageFile) | ||||
|         im.seek(im.n_frames - 1) | ||||
|         assert im.getpixel((0, 0)) == (0, 255, 0, 97) | ||||
|         assert im.getpixel((64, 32)) == (0, 255, 0, 255) | ||||
|  | @ -179,6 +198,7 @@ def test_apng_blend_transparency() -> None: | |||
| 
 | ||||
| def test_apng_chunk_order() -> None: | ||||
|     with Image.open("Tests/images/apng/fctl_actl.png") as im: | ||||
|         assert isinstance(im, PngImagePlugin.PngImageFile) | ||||
|         im.seek(im.n_frames - 1) | ||||
|         assert im.getpixel((0, 0)) == (0, 255, 0, 255) | ||||
|         assert im.getpixel((64, 32)) == (0, 255, 0, 255) | ||||
|  | @ -234,24 +254,28 @@ def test_apng_num_plays() -> None: | |||
| 
 | ||||
| def test_apng_mode() -> None: | ||||
|     with Image.open("Tests/images/apng/mode_16bit.png") as im: | ||||
|         assert isinstance(im, PngImagePlugin.PngImageFile) | ||||
|         assert im.mode == "RGBA" | ||||
|         im.seek(im.n_frames - 1) | ||||
|         assert im.getpixel((0, 0)) == (0, 0, 128, 191) | ||||
|         assert im.getpixel((64, 32)) == (0, 0, 128, 191) | ||||
| 
 | ||||
|     with Image.open("Tests/images/apng/mode_grayscale.png") as im: | ||||
|         assert isinstance(im, PngImagePlugin.PngImageFile) | ||||
|         assert im.mode == "L" | ||||
|         im.seek(im.n_frames - 1) | ||||
|         assert im.getpixel((0, 0)) == 128 | ||||
|         assert im.getpixel((64, 32)) == 255 | ||||
| 
 | ||||
|     with Image.open("Tests/images/apng/mode_grayscale_alpha.png") as im: | ||||
|         assert isinstance(im, PngImagePlugin.PngImageFile) | ||||
|         assert im.mode == "LA" | ||||
|         im.seek(im.n_frames - 1) | ||||
|         assert im.getpixel((0, 0)) == (128, 191) | ||||
|         assert im.getpixel((64, 32)) == (128, 191) | ||||
| 
 | ||||
|     with Image.open("Tests/images/apng/mode_palette.png") as im: | ||||
|         assert isinstance(im, PngImagePlugin.PngImageFile) | ||||
|         assert im.mode == "P" | ||||
|         im.seek(im.n_frames - 1) | ||||
|         im = im.convert("RGB") | ||||
|  | @ -259,6 +283,7 @@ def test_apng_mode() -> None: | |||
|         assert im.getpixel((64, 32)) == (0, 255, 0) | ||||
| 
 | ||||
|     with Image.open("Tests/images/apng/mode_palette_alpha.png") as im: | ||||
|         assert isinstance(im, PngImagePlugin.PngImageFile) | ||||
|         assert im.mode == "P" | ||||
|         im.seek(im.n_frames - 1) | ||||
|         im = im.convert("RGBA") | ||||
|  | @ -266,6 +291,7 @@ def test_apng_mode() -> None: | |||
|         assert im.getpixel((64, 32)) == (0, 255, 0, 255) | ||||
| 
 | ||||
|     with Image.open("Tests/images/apng/mode_palette_1bit_alpha.png") as im: | ||||
|         assert isinstance(im, PngImagePlugin.PngImageFile) | ||||
|         assert im.mode == "P" | ||||
|         im.seek(im.n_frames - 1) | ||||
|         im = im.convert("RGBA") | ||||
|  | @ -275,25 +301,31 @@ def test_apng_mode() -> None: | |||
| 
 | ||||
| def test_apng_chunk_errors() -> None: | ||||
|     with Image.open("Tests/images/apng/chunk_no_actl.png") as im: | ||||
|         assert isinstance(im, PngImagePlugin.PngImageFile) | ||||
|         assert not im.is_animated | ||||
| 
 | ||||
|     with pytest.warns(UserWarning): | ||||
|         with Image.open("Tests/images/apng/chunk_multi_actl.png") as im: | ||||
|             im.load() | ||||
|         assert isinstance(im, PngImagePlugin.PngImageFile) | ||||
|         assert not im.is_animated | ||||
| 
 | ||||
|     with Image.open("Tests/images/apng/chunk_actl_after_idat.png") as im: | ||||
|         assert isinstance(im, PngImagePlugin.PngImageFile) | ||||
|         assert not im.is_animated | ||||
| 
 | ||||
|     with Image.open("Tests/images/apng/chunk_no_fctl.png") as im: | ||||
|         assert isinstance(im, PngImagePlugin.PngImageFile) | ||||
|         with pytest.raises(SyntaxError): | ||||
|             im.seek(im.n_frames - 1) | ||||
| 
 | ||||
|     with Image.open("Tests/images/apng/chunk_repeat_fctl.png") as im: | ||||
|         assert isinstance(im, PngImagePlugin.PngImageFile) | ||||
|         with pytest.raises(SyntaxError): | ||||
|             im.seek(im.n_frames - 1) | ||||
| 
 | ||||
|     with Image.open("Tests/images/apng/chunk_no_fdat.png") as im: | ||||
|         assert isinstance(im, PngImagePlugin.PngImageFile) | ||||
|         with pytest.raises(SyntaxError): | ||||
|             im.seek(im.n_frames - 1) | ||||
| 
 | ||||
|  | @ -301,26 +333,31 @@ def test_apng_chunk_errors() -> None: | |||
| def test_apng_syntax_errors() -> None: | ||||
|     with pytest.warns(UserWarning): | ||||
|         with Image.open("Tests/images/apng/syntax_num_frames_zero.png") as im: | ||||
|             assert isinstance(im, PngImagePlugin.PngImageFile) | ||||
|             assert not im.is_animated | ||||
|             with pytest.raises(OSError): | ||||
|                 im.load() | ||||
| 
 | ||||
|     with pytest.warns(UserWarning): | ||||
|         with Image.open("Tests/images/apng/syntax_num_frames_zero_default.png") as im: | ||||
|             assert isinstance(im, PngImagePlugin.PngImageFile) | ||||
|             assert not im.is_animated | ||||
|             im.load() | ||||
| 
 | ||||
|     # we can handle this case gracefully | ||||
|     with Image.open("Tests/images/apng/syntax_num_frames_low.png") as im: | ||||
|         assert isinstance(im, PngImagePlugin.PngImageFile) | ||||
|         im.seek(im.n_frames - 1) | ||||
| 
 | ||||
|     with pytest.raises(OSError): | ||||
|         with Image.open("Tests/images/apng/syntax_num_frames_high.png") as im: | ||||
|             assert isinstance(im, PngImagePlugin.PngImageFile) | ||||
|             im.seek(im.n_frames - 1) | ||||
|             im.load() | ||||
| 
 | ||||
|     with pytest.warns(UserWarning): | ||||
|         with Image.open("Tests/images/apng/syntax_num_frames_invalid.png") as im: | ||||
|             assert isinstance(im, PngImagePlugin.PngImageFile) | ||||
|             assert not im.is_animated | ||||
|             im.load() | ||||
| 
 | ||||
|  | @ -340,6 +377,7 @@ def test_apng_syntax_errors() -> None: | |||
| def test_apng_sequence_errors(test_file: str) -> None: | ||||
|     with pytest.raises(SyntaxError): | ||||
|         with Image.open(f"Tests/images/apng/{test_file}") as im: | ||||
|             assert isinstance(im, PngImagePlugin.PngImageFile) | ||||
|             im.seek(im.n_frames - 1) | ||||
|             im.load() | ||||
| 
 | ||||
|  | @ -350,6 +388,7 @@ def test_apng_save(tmp_path: Path) -> None: | |||
|         im.save(test_file, save_all=True) | ||||
| 
 | ||||
|     with Image.open(test_file) as im: | ||||
|         assert isinstance(im, PngImagePlugin.PngImageFile) | ||||
|         im.load() | ||||
|         assert not im.is_animated | ||||
|         assert im.n_frames == 1 | ||||
|  | @ -365,6 +404,7 @@ def test_apng_save(tmp_path: Path) -> None: | |||
|         ) | ||||
| 
 | ||||
|     with Image.open(test_file) as im: | ||||
|         assert isinstance(im, PngImagePlugin.PngImageFile) | ||||
|         im.load() | ||||
|         assert im.is_animated | ||||
|         assert im.n_frames == 2 | ||||
|  | @ -404,6 +444,7 @@ def test_apng_save_split_fdat(tmp_path: Path) -> None: | |||
|             append_images=frames, | ||||
|         ) | ||||
|     with Image.open(test_file) as im: | ||||
|         assert isinstance(im, PngImagePlugin.PngImageFile) | ||||
|         im.seek(im.n_frames - 1) | ||||
|         im.load() | ||||
| 
 | ||||
|  | @ -446,6 +487,7 @@ def test_apng_save_duration_loop(tmp_path: Path) -> None: | |||
|         test_file, save_all=True, append_images=[frame, frame], duration=[500, 100, 150] | ||||
|     ) | ||||
|     with Image.open(test_file) as im: | ||||
|         assert isinstance(im, PngImagePlugin.PngImageFile) | ||||
|         assert im.n_frames == 1 | ||||
|         assert "duration" not in im.info | ||||
| 
 | ||||
|  | @ -457,6 +499,7 @@ def test_apng_save_duration_loop(tmp_path: Path) -> None: | |||
|         duration=[500, 100, 150], | ||||
|     ) | ||||
|     with Image.open(test_file) as im: | ||||
|         assert isinstance(im, PngImagePlugin.PngImageFile) | ||||
|         assert im.n_frames == 2 | ||||
|         assert im.info["duration"] == 600 | ||||
| 
 | ||||
|  | @ -467,6 +510,7 @@ def test_apng_save_duration_loop(tmp_path: Path) -> None: | |||
|     frame.info["duration"] = 300 | ||||
|     frame.save(test_file, save_all=True, append_images=[frame, different_frame]) | ||||
|     with Image.open(test_file) as im: | ||||
|         assert isinstance(im, PngImagePlugin.PngImageFile) | ||||
|         assert im.n_frames == 2 | ||||
|         assert im.info["duration"] == 600 | ||||
| 
 | ||||
|  |  | |||
							
								
								
									
										779
									
								
								Tests/test_file_avif.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										779
									
								
								Tests/test_file_avif.py
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,779 @@ | |||
| from __future__ import annotations | ||||
| 
 | ||||
| import gc | ||||
| import os | ||||
| import re | ||||
| import warnings | ||||
| from collections.abc import Generator, Sequence | ||||
| from contextlib import contextmanager | ||||
| from io import BytesIO | ||||
| from pathlib import Path | ||||
| from typing import Any | ||||
| 
 | ||||
| import pytest | ||||
| 
 | ||||
| from PIL import ( | ||||
|     AvifImagePlugin, | ||||
|     Image, | ||||
|     ImageDraw, | ||||
|     ImageFile, | ||||
|     UnidentifiedImageError, | ||||
|     features, | ||||
| ) | ||||
| 
 | ||||
| from .helper import ( | ||||
|     PillowLeakTestCase, | ||||
|     assert_image, | ||||
|     assert_image_similar, | ||||
|     assert_image_similar_tofile, | ||||
|     hopper, | ||||
|     skip_unless_feature, | ||||
| ) | ||||
| 
 | ||||
| try: | ||||
|     from PIL import _avif | ||||
| 
 | ||||
|     HAVE_AVIF = True | ||||
| except ImportError: | ||||
|     HAVE_AVIF = False | ||||
| 
 | ||||
| 
 | ||||
| TEST_AVIF_FILE = "Tests/images/avif/hopper.avif" | ||||
| 
 | ||||
| 
 | ||||
| def assert_xmp_orientation(xmp: bytes, expected: int) -> None: | ||||
|     assert int(xmp.split(b'tiff:Orientation="')[1].split(b'"')[0]) == expected | ||||
| 
 | ||||
| 
 | ||||
| def roundtrip(im: ImageFile.ImageFile, **options: Any) -> ImageFile.ImageFile: | ||||
|     out = BytesIO() | ||||
|     im.save(out, "AVIF", **options) | ||||
|     return Image.open(out) | ||||
| 
 | ||||
| 
 | ||||
| def skip_unless_avif_decoder(codec_name: str) -> pytest.MarkDecorator: | ||||
|     reason = f"{codec_name} decode not available" | ||||
|     return pytest.mark.skipif( | ||||
|         not HAVE_AVIF or not _avif.decoder_codec_available(codec_name), reason=reason | ||||
|     ) | ||||
| 
 | ||||
| 
 | ||||
| def skip_unless_avif_encoder(codec_name: str) -> pytest.MarkDecorator: | ||||
|     reason = f"{codec_name} encode not available" | ||||
|     return pytest.mark.skipif( | ||||
|         not HAVE_AVIF or not _avif.encoder_codec_available(codec_name), reason=reason | ||||
|     ) | ||||
| 
 | ||||
| 
 | ||||
| def is_docker_qemu() -> bool: | ||||
|     try: | ||||
|         init_proc_exe = os.readlink("/proc/1/exe") | ||||
|     except (FileNotFoundError, PermissionError): | ||||
|         return False | ||||
|     return "qemu" in init_proc_exe | ||||
| 
 | ||||
| 
 | ||||
| class TestUnsupportedAvif: | ||||
|     def test_unsupported(self, monkeypatch: pytest.MonkeyPatch) -> None: | ||||
|         monkeypatch.setattr(AvifImagePlugin, "SUPPORTED", False) | ||||
| 
 | ||||
|         with pytest.warns(UserWarning): | ||||
|             with pytest.raises(UnidentifiedImageError): | ||||
|                 with Image.open(TEST_AVIF_FILE): | ||||
|                     pass | ||||
| 
 | ||||
|     def test_unsupported_open(self, monkeypatch: pytest.MonkeyPatch) -> None: | ||||
|         monkeypatch.setattr(AvifImagePlugin, "SUPPORTED", False) | ||||
| 
 | ||||
|         with pytest.raises(SyntaxError): | ||||
|             AvifImagePlugin.AvifImageFile(TEST_AVIF_FILE) | ||||
| 
 | ||||
| 
 | ||||
| @skip_unless_feature("avif") | ||||
| class TestFileAvif: | ||||
|     def test_version(self) -> None: | ||||
|         version = features.version_module("avif") | ||||
|         assert version is not None | ||||
|         assert re.search(r"^\d+\.\d+\.\d+$", version) | ||||
| 
 | ||||
|     def test_codec_version(self) -> None: | ||||
|         assert AvifImagePlugin.get_codec_version("unknown") is None | ||||
| 
 | ||||
|         for codec_name in ("aom", "dav1d", "rav1e", "svt"): | ||||
|             codec_version = AvifImagePlugin.get_codec_version(codec_name) | ||||
|             if _avif.decoder_codec_available( | ||||
|                 codec_name | ||||
|             ) or _avif.encoder_codec_available(codec_name): | ||||
|                 assert codec_version is not None | ||||
|                 assert re.search(r"^v?\d+\.\d+\.\d+(-([a-z\d])+)*$", codec_version) | ||||
|             else: | ||||
|                 assert codec_version is None | ||||
| 
 | ||||
|     def test_read(self) -> None: | ||||
|         """ | ||||
|         Can we read an AVIF file without error? | ||||
|         Does it have the bits we expect? | ||||
|         """ | ||||
| 
 | ||||
|         with Image.open(TEST_AVIF_FILE) as image: | ||||
|             assert image.mode == "RGB" | ||||
|             assert image.size == (128, 128) | ||||
|             assert image.format == "AVIF" | ||||
|             assert image.get_format_mimetype() == "image/avif" | ||||
|             image.getdata() | ||||
| 
 | ||||
|             # generated with: | ||||
|             # avifdec hopper.avif hopper_avif_write.png | ||||
|             assert_image_similar_tofile( | ||||
|                 image, "Tests/images/avif/hopper_avif_write.png", 11.5 | ||||
|             ) | ||||
| 
 | ||||
|     def test_write_rgb(self, tmp_path: Path) -> None: | ||||
|         """ | ||||
|         Can we write a RGB mode file to avif without error? | ||||
|         Does it have the bits we expect? | ||||
|         """ | ||||
| 
 | ||||
|         temp_file = tmp_path / "temp.avif" | ||||
| 
 | ||||
|         im = hopper() | ||||
|         im.save(temp_file) | ||||
|         with Image.open(temp_file) as reloaded: | ||||
|             assert reloaded.mode == "RGB" | ||||
|             assert reloaded.size == (128, 128) | ||||
|             assert reloaded.format == "AVIF" | ||||
|             reloaded.getdata() | ||||
| 
 | ||||
|             # avifdec hopper.avif avif/hopper_avif_write.png | ||||
|             assert_image_similar_tofile( | ||||
|                 reloaded, "Tests/images/avif/hopper_avif_write.png", 6.02 | ||||
|             ) | ||||
| 
 | ||||
|             # This test asserts that the images are similar. If the average pixel | ||||
|             # difference between the two images is less than the epsilon value, | ||||
|             # then we're going to accept that it's a reasonable lossy version of | ||||
|             # the image. | ||||
|             assert_image_similar(reloaded, im, 8.62) | ||||
| 
 | ||||
|     def test_AvifEncoder_with_invalid_args(self) -> None: | ||||
|         """ | ||||
|         Calling encoder functions with no arguments should result in an error. | ||||
|         """ | ||||
|         with pytest.raises(TypeError): | ||||
|             _avif.AvifEncoder() | ||||
| 
 | ||||
|     def test_AvifDecoder_with_invalid_args(self) -> None: | ||||
|         """ | ||||
|         Calling decoder functions with no arguments should result in an error. | ||||
|         """ | ||||
|         with pytest.raises(TypeError): | ||||
|             _avif.AvifDecoder() | ||||
| 
 | ||||
|     def test_invalid_dimensions(self, tmp_path: Path) -> None: | ||||
|         test_file = tmp_path / "temp.avif" | ||||
|         im = Image.new("RGB", (0, 0)) | ||||
|         with pytest.raises(ValueError): | ||||
|             im.save(test_file) | ||||
| 
 | ||||
|     def test_encoder_finish_none_error( | ||||
|         self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path | ||||
|     ) -> None: | ||||
|         """Save should raise an OSError if AvifEncoder.finish returns None""" | ||||
| 
 | ||||
|         class _mock_avif: | ||||
|             class AvifEncoder: | ||||
|                 def __init__(self, *args: Any) -> None: | ||||
|                     pass | ||||
| 
 | ||||
|                 def add(self, *args: Any) -> None: | ||||
|                     pass | ||||
| 
 | ||||
|                 def finish(self) -> None: | ||||
|                     return None | ||||
| 
 | ||||
|         monkeypatch.setattr(AvifImagePlugin, "_avif", _mock_avif) | ||||
| 
 | ||||
|         im = Image.new("RGB", (150, 150)) | ||||
|         test_file = tmp_path / "temp.avif" | ||||
|         with pytest.raises(OSError): | ||||
|             im.save(test_file) | ||||
| 
 | ||||
|     def test_no_resource_warning(self, tmp_path: Path) -> None: | ||||
|         with Image.open(TEST_AVIF_FILE) as im: | ||||
|             with warnings.catch_warnings(): | ||||
|                 warnings.simplefilter("error") | ||||
| 
 | ||||
|                 im.save(tmp_path / "temp.avif") | ||||
| 
 | ||||
|     @pytest.mark.parametrize("major_brand", [b"avif", b"avis", b"mif1", b"msf1"]) | ||||
|     def test_accept_ftyp_brands(self, major_brand: bytes) -> None: | ||||
|         data = b"\x00\x00\x00\x1cftyp%s\x00\x00\x00\x00" % major_brand | ||||
|         assert AvifImagePlugin._accept(data) is True | ||||
| 
 | ||||
|     def test_file_pointer_could_be_reused(self) -> None: | ||||
|         with open(TEST_AVIF_FILE, "rb") as blob: | ||||
|             with Image.open(blob) as im: | ||||
|                 im.load() | ||||
|             with Image.open(blob) as im: | ||||
|                 im.load() | ||||
| 
 | ||||
|     def test_background_from_gif(self, tmp_path: Path) -> None: | ||||
|         with Image.open("Tests/images/chi.gif") as im: | ||||
|             original_value = im.convert("RGB").getpixel((1, 1)) | ||||
| 
 | ||||
|             # Save as AVIF | ||||
|             out_avif = tmp_path / "temp.avif" | ||||
|             im.save(out_avif, save_all=True) | ||||
| 
 | ||||
|         # Save as GIF | ||||
|         out_gif = tmp_path / "temp.gif" | ||||
|         with Image.open(out_avif) as im: | ||||
|             im.save(out_gif) | ||||
| 
 | ||||
|         with Image.open(out_gif) as reread: | ||||
|             reread_value = reread.convert("RGB").getpixel((1, 1)) | ||||
|         difference = sum([abs(original_value[i] - reread_value[i]) for i in range(3)]) | ||||
|         assert difference <= 3 | ||||
| 
 | ||||
|     def test_save_single_frame(self, tmp_path: Path) -> None: | ||||
|         temp_file = tmp_path / "temp.avif" | ||||
|         with Image.open("Tests/images/chi.gif") as im: | ||||
|             im.save(temp_file) | ||||
|         with Image.open(temp_file) as im: | ||||
|             assert im.n_frames == 1 | ||||
| 
 | ||||
|     def test_invalid_file(self) -> None: | ||||
|         invalid_file = "Tests/images/flower.jpg" | ||||
| 
 | ||||
|         with pytest.raises(SyntaxError): | ||||
|             AvifImagePlugin.AvifImageFile(invalid_file) | ||||
| 
 | ||||
|     def test_load_transparent_rgb(self) -> None: | ||||
|         test_file = "Tests/images/avif/transparency.avif" | ||||
|         with Image.open(test_file) as im: | ||||
|             assert_image(im, "RGBA", (64, 64)) | ||||
| 
 | ||||
|             # image has 876 transparent pixels | ||||
|             assert im.getchannel("A").getcolors()[0] == (876, 0) | ||||
| 
 | ||||
|     def test_save_transparent(self, tmp_path: Path) -> None: | ||||
|         im = Image.new("RGBA", (10, 10), (0, 0, 0, 0)) | ||||
|         assert im.getcolors() == [(100, (0, 0, 0, 0))] | ||||
| 
 | ||||
|         test_file = tmp_path / "temp.avif" | ||||
|         im.save(test_file) | ||||
| 
 | ||||
|         # check if saved image contains the same transparency | ||||
|         with Image.open(test_file) as im: | ||||
|             assert_image(im, "RGBA", (10, 10)) | ||||
|             assert im.getcolors() == [(100, (0, 0, 0, 0))] | ||||
| 
 | ||||
|     def test_save_icc_profile(self) -> None: | ||||
|         with Image.open("Tests/images/avif/icc_profile_none.avif") as im: | ||||
|             assert "icc_profile" not in im.info | ||||
| 
 | ||||
|             with Image.open("Tests/images/avif/icc_profile.avif") as with_icc: | ||||
|                 expected_icc = with_icc.info["icc_profile"] | ||||
|                 assert expected_icc is not None | ||||
| 
 | ||||
|                 im = roundtrip(im, icc_profile=expected_icc) | ||||
|                 assert im.info["icc_profile"] == expected_icc | ||||
| 
 | ||||
|     def test_discard_icc_profile(self) -> None: | ||||
|         with Image.open("Tests/images/avif/icc_profile.avif") as im: | ||||
|             im = roundtrip(im, icc_profile=None) | ||||
|         assert "icc_profile" not in im.info | ||||
| 
 | ||||
|     def test_roundtrip_icc_profile(self) -> None: | ||||
|         with Image.open("Tests/images/avif/icc_profile.avif") as im: | ||||
|             expected_icc = im.info["icc_profile"] | ||||
| 
 | ||||
|             im = roundtrip(im) | ||||
|         assert im.info["icc_profile"] == expected_icc | ||||
| 
 | ||||
|     def test_roundtrip_no_icc_profile(self) -> None: | ||||
|         with Image.open("Tests/images/avif/icc_profile_none.avif") as im: | ||||
|             assert "icc_profile" not in im.info | ||||
| 
 | ||||
|             im = roundtrip(im) | ||||
|         assert "icc_profile" not in im.info | ||||
| 
 | ||||
|     def test_exif(self) -> None: | ||||
|         # With an EXIF chunk | ||||
|         with Image.open("Tests/images/avif/exif.avif") as im: | ||||
|             exif = im.getexif() | ||||
|         assert exif[274] == 1 | ||||
| 
 | ||||
|         with Image.open("Tests/images/avif/xmp_tags_orientation.avif") as im: | ||||
|             exif = im.getexif() | ||||
|         assert exif[274] == 3 | ||||
| 
 | ||||
|     @pytest.mark.parametrize("use_bytes", [True, False]) | ||||
|     @pytest.mark.parametrize("orientation", [1, 2, 3, 4, 5, 6, 7, 8]) | ||||
|     def test_exif_save( | ||||
|         self, | ||||
|         tmp_path: Path, | ||||
|         use_bytes: bool, | ||||
|         orientation: int, | ||||
|     ) -> None: | ||||
|         exif = Image.Exif() | ||||
|         exif[274] = orientation | ||||
|         exif_data = exif.tobytes() | ||||
|         with Image.open(TEST_AVIF_FILE) as im: | ||||
|             test_file = tmp_path / "temp.avif" | ||||
|             im.save(test_file, exif=exif_data if use_bytes else exif) | ||||
| 
 | ||||
|         with Image.open(test_file) as reloaded: | ||||
|             if orientation == 1: | ||||
|                 assert "exif" not in reloaded.info | ||||
|             else: | ||||
|                 assert reloaded.getexif()[274] == orientation | ||||
|                 assert reloaded.info["exif"] == exif_data | ||||
| 
 | ||||
|     def test_exif_without_orientation(self, tmp_path: Path) -> None: | ||||
|         exif = Image.Exif() | ||||
|         exif[272] = b"test" | ||||
|         exif_data = exif.tobytes() | ||||
|         with Image.open(TEST_AVIF_FILE) as im: | ||||
|             test_file = tmp_path / "temp.avif" | ||||
|             im.save(test_file, exif=exif) | ||||
| 
 | ||||
|         with Image.open(test_file) as reloaded: | ||||
|             assert reloaded.info["exif"] == exif_data | ||||
| 
 | ||||
|     def test_exif_invalid(self, tmp_path: Path) -> None: | ||||
|         with Image.open(TEST_AVIF_FILE) as im: | ||||
|             test_file = tmp_path / "temp.avif" | ||||
|             with pytest.raises(SyntaxError): | ||||
|                 im.save(test_file, exif=b"invalid") | ||||
| 
 | ||||
|     @pytest.mark.parametrize( | ||||
|         "rot, mir, exif_orientation", | ||||
|         [ | ||||
|             (0, 0, 4), | ||||
|             (0, 1, 2), | ||||
|             (1, 0, 5), | ||||
|             (1, 1, 7), | ||||
|             (2, 0, 2), | ||||
|             (2, 1, 4), | ||||
|             (3, 0, 7), | ||||
|             (3, 1, 5), | ||||
|         ], | ||||
|     ) | ||||
|     def test_rot_mir_exif( | ||||
|         self, rot: int, mir: int, exif_orientation: int, tmp_path: Path | ||||
|     ) -> None: | ||||
|         with Image.open(f"Tests/images/avif/rot{rot}mir{mir}.avif") as im: | ||||
|             exif = im.getexif() | ||||
|             assert exif[274] == exif_orientation | ||||
| 
 | ||||
|             test_file = tmp_path / "temp.avif" | ||||
|             im.save(test_file, exif=exif) | ||||
|         with Image.open(test_file) as reloaded: | ||||
|             assert reloaded.getexif()[274] == exif_orientation | ||||
| 
 | ||||
|     def test_xmp(self) -> None: | ||||
|         with Image.open("Tests/images/avif/xmp_tags_orientation.avif") as im: | ||||
|             xmp = im.info["xmp"] | ||||
|         assert_xmp_orientation(xmp, 3) | ||||
| 
 | ||||
|     def test_xmp_save(self, tmp_path: Path) -> None: | ||||
|         xmp_arg = "\n".join( | ||||
|             [ | ||||
|                 '<?xpacket begin="" id="W5M0MpCehiHzreSzNTczkc9d"?>', | ||||
|                 '<x:xmpmeta xmlns:x="adobe:ns:meta/">', | ||||
|                 ' <rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">', | ||||
|                 '  <rdf:Description rdf:about=""', | ||||
|                 '    xmlns:tiff="http://ns.adobe.com/tiff/1.0/"', | ||||
|                 '   tiff:Orientation="1"/>', | ||||
|                 " </rdf:RDF>", | ||||
|                 "</x:xmpmeta>", | ||||
|                 '<?xpacket end="r"?>', | ||||
|             ] | ||||
|         ) | ||||
|         with Image.open(TEST_AVIF_FILE) as im: | ||||
|             test_file = tmp_path / "temp.avif" | ||||
|             im.save(test_file, xmp=xmp_arg) | ||||
| 
 | ||||
|         with Image.open(test_file) as reloaded: | ||||
|             xmp = reloaded.info["xmp"] | ||||
|         assert_xmp_orientation(xmp, 1) | ||||
| 
 | ||||
|     def test_tell(self) -> None: | ||||
|         with Image.open(TEST_AVIF_FILE) as im: | ||||
|             assert im.tell() == 0 | ||||
| 
 | ||||
|     def test_seek(self) -> None: | ||||
|         with Image.open(TEST_AVIF_FILE) as im: | ||||
|             im.seek(0) | ||||
| 
 | ||||
|             with pytest.raises(EOFError): | ||||
|                 im.seek(1) | ||||
| 
 | ||||
|     @pytest.mark.parametrize("subsampling", ["4:4:4", "4:2:2", "4:2:0", "4:0:0"]) | ||||
|     def test_encoder_subsampling(self, tmp_path: Path, subsampling: str) -> None: | ||||
|         with Image.open(TEST_AVIF_FILE) as im: | ||||
|             test_file = tmp_path / "temp.avif" | ||||
|             im.save(test_file, subsampling=subsampling) | ||||
| 
 | ||||
|     def test_encoder_subsampling_invalid(self, tmp_path: Path) -> None: | ||||
|         with Image.open(TEST_AVIF_FILE) as im: | ||||
|             test_file = tmp_path / "temp.avif" | ||||
|             with pytest.raises(ValueError): | ||||
|                 im.save(test_file, subsampling="foo") | ||||
| 
 | ||||
|     @pytest.mark.parametrize("value", ["full", "limited"]) | ||||
|     def test_encoder_range(self, tmp_path: Path, value: str) -> None: | ||||
|         with Image.open(TEST_AVIF_FILE) as im: | ||||
|             test_file = tmp_path / "temp.avif" | ||||
|             im.save(test_file, range=value) | ||||
| 
 | ||||
|     def test_encoder_range_invalid(self, tmp_path: Path) -> None: | ||||
|         with Image.open(TEST_AVIF_FILE) as im: | ||||
|             test_file = tmp_path / "temp.avif" | ||||
|             with pytest.raises(ValueError): | ||||
|                 im.save(test_file, range="foo") | ||||
| 
 | ||||
|     @skip_unless_avif_encoder("aom") | ||||
|     def test_encoder_codec_param(self, tmp_path: Path) -> None: | ||||
|         with Image.open(TEST_AVIF_FILE) as im: | ||||
|             test_file = tmp_path / "temp.avif" | ||||
|             im.save(test_file, codec="aom") | ||||
| 
 | ||||
|     def test_encoder_codec_invalid(self, tmp_path: Path) -> None: | ||||
|         with Image.open(TEST_AVIF_FILE) as im: | ||||
|             test_file = tmp_path / "temp.avif" | ||||
|             with pytest.raises(ValueError): | ||||
|                 im.save(test_file, codec="foo") | ||||
| 
 | ||||
|     @skip_unless_avif_decoder("dav1d") | ||||
|     def test_decoder_codec_cannot_encode(self, tmp_path: Path) -> None: | ||||
|         with Image.open(TEST_AVIF_FILE) as im: | ||||
|             test_file = tmp_path / "temp.avif" | ||||
|             with pytest.raises(ValueError): | ||||
|                 im.save(test_file, codec="dav1d") | ||||
| 
 | ||||
|     @skip_unless_avif_encoder("aom") | ||||
|     @pytest.mark.parametrize( | ||||
|         "advanced", | ||||
|         [ | ||||
|             { | ||||
|                 "aq-mode": "1", | ||||
|                 "enable-chroma-deltaq": "1", | ||||
|             }, | ||||
|             (("aq-mode", "1"), ("enable-chroma-deltaq", "1")), | ||||
|             [("aq-mode", "1"), ("enable-chroma-deltaq", "1")], | ||||
|         ], | ||||
|     ) | ||||
|     def test_encoder_advanced_codec_options( | ||||
|         self, advanced: dict[str, str] | Sequence[tuple[str, str]] | ||||
|     ) -> None: | ||||
|         with Image.open(TEST_AVIF_FILE) as im: | ||||
|             ctrl_buf = BytesIO() | ||||
|             im.save(ctrl_buf, "AVIF", codec="aom") | ||||
|             test_buf = BytesIO() | ||||
|             im.save( | ||||
|                 test_buf, | ||||
|                 "AVIF", | ||||
|                 codec="aom", | ||||
|                 advanced=advanced, | ||||
|             ) | ||||
|             assert ctrl_buf.getvalue() != test_buf.getvalue() | ||||
| 
 | ||||
|     @skip_unless_avif_encoder("aom") | ||||
|     @pytest.mark.parametrize("advanced", [{"foo": "bar"}, {"foo": 1234}, 1234]) | ||||
|     def test_encoder_advanced_codec_options_invalid( | ||||
|         self, tmp_path: Path, advanced: dict[str, str] | int | ||||
|     ) -> None: | ||||
|         with Image.open(TEST_AVIF_FILE) as im: | ||||
|             test_file = tmp_path / "temp.avif" | ||||
|             with pytest.raises(ValueError): | ||||
|                 im.save(test_file, codec="aom", advanced=advanced) | ||||
| 
 | ||||
|     @skip_unless_avif_decoder("aom") | ||||
|     def test_decoder_codec_param(self, monkeypatch: pytest.MonkeyPatch) -> None: | ||||
|         monkeypatch.setattr(AvifImagePlugin, "DECODE_CODEC_CHOICE", "aom") | ||||
| 
 | ||||
|         with Image.open(TEST_AVIF_FILE) as im: | ||||
|             assert im.size == (128, 128) | ||||
| 
 | ||||
|     @skip_unless_avif_encoder("rav1e") | ||||
|     def test_encoder_codec_cannot_decode( | ||||
|         self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path | ||||
|     ) -> None: | ||||
|         monkeypatch.setattr(AvifImagePlugin, "DECODE_CODEC_CHOICE", "rav1e") | ||||
| 
 | ||||
|         with pytest.raises(ValueError): | ||||
|             with Image.open(TEST_AVIF_FILE): | ||||
|                 pass | ||||
| 
 | ||||
|     def test_decoder_codec_invalid(self, monkeypatch: pytest.MonkeyPatch) -> None: | ||||
|         monkeypatch.setattr(AvifImagePlugin, "DECODE_CODEC_CHOICE", "foo") | ||||
| 
 | ||||
|         with pytest.raises(ValueError): | ||||
|             with Image.open(TEST_AVIF_FILE): | ||||
|                 pass | ||||
| 
 | ||||
|     @skip_unless_avif_encoder("aom") | ||||
|     def test_encoder_codec_available(self) -> None: | ||||
|         assert _avif.encoder_codec_available("aom") is True | ||||
| 
 | ||||
|     def test_encoder_codec_available_bad_params(self) -> None: | ||||
|         with pytest.raises(TypeError): | ||||
|             _avif.encoder_codec_available() | ||||
| 
 | ||||
|     @skip_unless_avif_decoder("dav1d") | ||||
|     def test_encoder_codec_available_cannot_decode(self) -> None: | ||||
|         assert _avif.encoder_codec_available("dav1d") is False | ||||
| 
 | ||||
|     def test_encoder_codec_available_invalid(self) -> None: | ||||
|         assert _avif.encoder_codec_available("foo") is False | ||||
| 
 | ||||
|     def test_encoder_quality_valueerror(self, tmp_path: Path) -> None: | ||||
|         with Image.open(TEST_AVIF_FILE) as im: | ||||
|             test_file = tmp_path / "temp.avif" | ||||
|             with pytest.raises(ValueError): | ||||
|                 im.save(test_file, quality="invalid") | ||||
| 
 | ||||
|     @skip_unless_avif_decoder("aom") | ||||
|     def test_decoder_codec_available(self) -> None: | ||||
|         assert _avif.decoder_codec_available("aom") is True | ||||
| 
 | ||||
|     def test_decoder_codec_available_bad_params(self) -> None: | ||||
|         with pytest.raises(TypeError): | ||||
|             _avif.decoder_codec_available() | ||||
| 
 | ||||
|     @skip_unless_avif_encoder("rav1e") | ||||
|     def test_decoder_codec_available_cannot_decode(self) -> None: | ||||
|         assert _avif.decoder_codec_available("rav1e") is False | ||||
| 
 | ||||
|     def test_decoder_codec_available_invalid(self) -> None: | ||||
|         assert _avif.decoder_codec_available("foo") is False | ||||
| 
 | ||||
|     def test_p_mode_transparency(self, tmp_path: Path) -> None: | ||||
|         im = Image.new("P", size=(64, 64)) | ||||
|         draw = ImageDraw.Draw(im) | ||||
|         draw.rectangle(xy=[(0, 0), (32, 32)], fill=255) | ||||
|         draw.rectangle(xy=[(32, 32), (64, 64)], fill=255) | ||||
| 
 | ||||
|         out_png = tmp_path / "temp.png" | ||||
|         im.save(out_png, transparency=0) | ||||
|         with Image.open(out_png) as im_png: | ||||
|             out_avif = tmp_path / "temp.avif" | ||||
|             im_png.save(out_avif, quality=100) | ||||
| 
 | ||||
|             with Image.open(out_avif) as expected: | ||||
|                 assert_image_similar(im_png.convert("RGBA"), expected, 0.17) | ||||
| 
 | ||||
|     def test_decoder_strict_flags(self) -> None: | ||||
|         # This would fail if full avif strictFlags were enabled | ||||
|         with Image.open("Tests/images/avif/hopper-missing-pixi.avif") as im: | ||||
|             assert im.size == (128, 128) | ||||
| 
 | ||||
|     @skip_unless_avif_encoder("aom") | ||||
|     @pytest.mark.parametrize("speed", [-1, 1, 11]) | ||||
|     def test_aom_optimizations(self, tmp_path: Path, speed: int) -> None: | ||||
|         test_file = tmp_path / "temp.avif" | ||||
|         hopper().save(test_file, codec="aom", speed=speed) | ||||
| 
 | ||||
|     @skip_unless_avif_encoder("svt") | ||||
|     def test_svt_optimizations(self, tmp_path: Path) -> None: | ||||
|         test_file = tmp_path / "temp.avif" | ||||
|         hopper().save(test_file, codec="svt", speed=1) | ||||
| 
 | ||||
| 
 | ||||
| @skip_unless_feature("avif") | ||||
| class TestAvifAnimation: | ||||
|     @contextmanager | ||||
|     def star_frames(self) -> Generator[list[Image.Image], None, None]: | ||||
|         with Image.open("Tests/images/avif/star.png") as f: | ||||
|             yield [f, f.rotate(90), f.rotate(180), f.rotate(270)] | ||||
| 
 | ||||
|     def test_n_frames(self) -> None: | ||||
|         """ | ||||
|         Ensure that AVIF format sets n_frames and is_animated attributes | ||||
|         correctly. | ||||
|         """ | ||||
| 
 | ||||
|         with Image.open(TEST_AVIF_FILE) as im: | ||||
|             assert im.n_frames == 1 | ||||
|             assert not im.is_animated | ||||
| 
 | ||||
|         with Image.open("Tests/images/avif/star.avifs") as im: | ||||
|             assert im.n_frames == 5 | ||||
|             assert im.is_animated | ||||
| 
 | ||||
|     def test_write_animation_P(self, tmp_path: Path) -> None: | ||||
|         """ | ||||
|         Convert an animated GIF to animated AVIF, then compare the frame | ||||
|         count, and ensure the frames are visually similar to the originals. | ||||
|         """ | ||||
| 
 | ||||
|         with Image.open("Tests/images/avif/star.gif") as original: | ||||
|             assert original.n_frames > 1 | ||||
| 
 | ||||
|             temp_file = tmp_path / "temp.avif" | ||||
|             original.save(temp_file, save_all=True) | ||||
|             with Image.open(temp_file) as im: | ||||
|                 assert im.n_frames == original.n_frames | ||||
| 
 | ||||
|                 # Compare first frame in P mode to frame from original GIF | ||||
|                 assert_image_similar(im, original.convert("RGBA"), 2) | ||||
| 
 | ||||
|                 # Compare later frames in RGBA mode to frames from original GIF | ||||
|                 for frame in range(1, original.n_frames): | ||||
|                     original.seek(frame) | ||||
|                     im.seek(frame) | ||||
|                     assert_image_similar(im, original, 2.54) | ||||
| 
 | ||||
|     def test_write_animation_RGBA(self, tmp_path: Path) -> None: | ||||
|         """ | ||||
|         Write an animated AVIF from RGBA frames, and ensure the frames | ||||
|         are visually similar to the originals. | ||||
|         """ | ||||
| 
 | ||||
|         def check(temp_file: Path) -> None: | ||||
|             with Image.open(temp_file) as im: | ||||
|                 assert im.n_frames == 4 | ||||
| 
 | ||||
|                 # Compare first frame to original | ||||
|                 assert_image_similar(im, frame1, 2.7) | ||||
| 
 | ||||
|                 # Compare second frame to original | ||||
|                 im.seek(1) | ||||
|                 assert_image_similar(im, frame2, 4.1) | ||||
| 
 | ||||
|         with self.star_frames() as frames: | ||||
|             frame1 = frames[0] | ||||
|             frame2 = frames[1] | ||||
|             temp_file1 = tmp_path / "temp.avif" | ||||
|             frames[0].copy().save(temp_file1, save_all=True, append_images=frames[1:]) | ||||
|             check(temp_file1) | ||||
| 
 | ||||
|             # Test appending using a generator | ||||
|             def imGenerator( | ||||
|                 ims: list[Image.Image], | ||||
|             ) -> Generator[Image.Image, None, None]: | ||||
|                 yield from ims | ||||
| 
 | ||||
|             temp_file2 = tmp_path / "temp_generator.avif" | ||||
|             frames[0].copy().save( | ||||
|                 temp_file2, | ||||
|                 save_all=True, | ||||
|                 append_images=imGenerator(frames[1:]), | ||||
|             ) | ||||
|             check(temp_file2) | ||||
| 
 | ||||
|     def test_sequence_dimension_mismatch_check(self, tmp_path: Path) -> None: | ||||
|         temp_file = tmp_path / "temp.avif" | ||||
|         frame1 = Image.new("RGB", (100, 100)) | ||||
|         frame2 = Image.new("RGB", (150, 150)) | ||||
|         with pytest.raises(ValueError): | ||||
|             frame1.save(temp_file, save_all=True, append_images=[frame2]) | ||||
| 
 | ||||
|     def test_heif_raises_unidentified_image_error(self) -> None: | ||||
|         with pytest.raises(UnidentifiedImageError): | ||||
|             with Image.open("Tests/images/avif/hopper.heif"): | ||||
|                 pass | ||||
| 
 | ||||
|     @pytest.mark.parametrize("alpha_premultiplied", [False, True]) | ||||
|     def test_alpha_premultiplied( | ||||
|         self, tmp_path: Path, alpha_premultiplied: bool | ||||
|     ) -> None: | ||||
|         temp_file = tmp_path / "temp.avif" | ||||
|         color = (200, 200, 200, 1) | ||||
|         im = Image.new("RGBA", (1, 1), color) | ||||
|         im.save(temp_file, alpha_premultiplied=alpha_premultiplied) | ||||
| 
 | ||||
|         expected = (255, 255, 255, 1) if alpha_premultiplied else color | ||||
|         with Image.open(temp_file) as reloaded: | ||||
|             assert reloaded.getpixel((0, 0)) == expected | ||||
| 
 | ||||
|     def test_timestamp_and_duration(self, tmp_path: Path) -> None: | ||||
|         """ | ||||
|         Try passing a list of durations, and make sure the encoded | ||||
|         timestamps and durations are correct. | ||||
|         """ | ||||
| 
 | ||||
|         durations = [1, 10, 20, 30, 40] | ||||
|         temp_file = tmp_path / "temp.avif" | ||||
|         with self.star_frames() as frames: | ||||
|             frames[0].save( | ||||
|                 temp_file, | ||||
|                 save_all=True, | ||||
|                 append_images=(frames[1:] + [frames[0]]), | ||||
|                 duration=durations, | ||||
|             ) | ||||
| 
 | ||||
|         with Image.open(temp_file) as im: | ||||
|             assert im.n_frames == 5 | ||||
|             assert im.is_animated | ||||
| 
 | ||||
|             # Check that timestamps and durations match original values specified | ||||
|             timestamp = 0 | ||||
|             for frame in range(im.n_frames): | ||||
|                 im.seek(frame) | ||||
|                 im.load() | ||||
|                 assert im.info["duration"] == durations[frame] | ||||
|                 assert im.info["timestamp"] == timestamp | ||||
|                 timestamp += durations[frame] | ||||
| 
 | ||||
|     def test_seeking(self, tmp_path: Path) -> None: | ||||
|         """ | ||||
|         Create an animated AVIF file, and then try seeking through frames in | ||||
|         reverse-order, verifying the timestamps and durations are correct. | ||||
|         """ | ||||
| 
 | ||||
|         duration = 33 | ||||
|         temp_file = tmp_path / "temp.avif" | ||||
|         with self.star_frames() as frames: | ||||
|             frames[0].save( | ||||
|                 temp_file, | ||||
|                 save_all=True, | ||||
|                 append_images=(frames[1:] + [frames[0]]), | ||||
|                 duration=duration, | ||||
|             ) | ||||
| 
 | ||||
|         with Image.open(temp_file) as im: | ||||
|             assert im.n_frames == 5 | ||||
|             assert im.is_animated | ||||
| 
 | ||||
|             # Traverse frames in reverse, checking timestamps and durations | ||||
|             timestamp = duration * (im.n_frames - 1) | ||||
|             for frame in reversed(range(im.n_frames)): | ||||
|                 im.seek(frame) | ||||
|                 im.load() | ||||
|                 assert im.info["duration"] == duration | ||||
|                 assert im.info["timestamp"] == timestamp | ||||
|                 timestamp -= duration | ||||
| 
 | ||||
|     def test_seek_errors(self) -> None: | ||||
|         with Image.open("Tests/images/avif/star.avifs") as im: | ||||
|             with pytest.raises(EOFError): | ||||
|                 im.seek(-1) | ||||
| 
 | ||||
|             with pytest.raises(EOFError): | ||||
|                 im.seek(42) | ||||
| 
 | ||||
| 
 | ||||
| MAX_THREADS = os.cpu_count() or 1 | ||||
| 
 | ||||
| 
 | ||||
| @skip_unless_feature("avif") | ||||
| class TestAvifLeaks(PillowLeakTestCase): | ||||
|     mem_limit = MAX_THREADS * 3 * 1024 | ||||
|     iterations = 100 | ||||
| 
 | ||||
|     @pytest.mark.skipif( | ||||
|         is_docker_qemu(), reason="Skipping on cross-architecture containers" | ||||
|     ) | ||||
|     def test_leak_load(self) -> None: | ||||
|         with open(TEST_AVIF_FILE, "rb") as f: | ||||
|             im_data = f.read() | ||||
| 
 | ||||
|         def core() -> None: | ||||
|             with Image.open(BytesIO(im_data)) as im: | ||||
|                 im.load() | ||||
|             gc.collect() | ||||
| 
 | ||||
|         self._test_leak(core) | ||||
|  | @ -15,10 +15,11 @@ from .helper import ( | |||
| ) | ||||
| 
 | ||||
| 
 | ||||
| def test_sanity(tmp_path: Path) -> None: | ||||
|     def roundtrip(im: Image.Image) -> None: | ||||
| @pytest.mark.parametrize("mode", ("1", "L", "P", "RGB")) | ||||
| def test_sanity(mode: str, tmp_path: Path) -> None: | ||||
|     outfile = tmp_path / "temp.bmp" | ||||
| 
 | ||||
|     im = hopper(mode) | ||||
|     im.save(outfile, "BMP") | ||||
| 
 | ||||
|     with Image.open(outfile) as reloaded: | ||||
|  | @ -28,13 +29,6 @@ def test_sanity(tmp_path: Path) -> None: | |||
|         assert reloaded.format == "BMP" | ||||
|         assert reloaded.get_format_mimetype() == "image/bmp" | ||||
| 
 | ||||
|     roundtrip(hopper()) | ||||
| 
 | ||||
|     roundtrip(hopper("1")) | ||||
|     roundtrip(hopper("L")) | ||||
|     roundtrip(hopper("P")) | ||||
|     roundtrip(hopper("RGB")) | ||||
| 
 | ||||
| 
 | ||||
| def test_invalid_file() -> None: | ||||
|     with open("Tests/images/flower.jpg", "rb") as fp: | ||||
|  | @ -230,3 +224,13 @@ def test_offset() -> None: | |||
|     # to exclude the palette size from the pixel data offset | ||||
|     with Image.open("Tests/images/pal8_offset.bmp") as im: | ||||
|         assert_image_equal_tofile(im, "Tests/images/bmp/g/pal8.bmp") | ||||
| 
 | ||||
| 
 | ||||
| def test_use_raw_alpha(monkeypatch: pytest.MonkeyPatch) -> None: | ||||
|     with Image.open("Tests/images/bmp/g/rgb32.bmp") as im: | ||||
|         assert im.info["compression"] == BmpImagePlugin.BmpImageFile.COMPRESSIONS["RAW"] | ||||
|         assert im.mode == "RGB" | ||||
| 
 | ||||
|     monkeypatch.setattr(BmpImagePlugin, "USE_RAW_ALPHA", True) | ||||
|     with Image.open("Tests/images/bmp/g/rgb32.bmp") as im: | ||||
|         assert im.mode == "RGBA" | ||||
|  |  | |||
|  | @ -69,12 +69,14 @@ def test_tell() -> None: | |||
| 
 | ||||
| def test_n_frames() -> None: | ||||
|     with Image.open(TEST_FILE) as im: | ||||
|         assert isinstance(im, DcxImagePlugin.DcxImageFile) | ||||
|         assert im.n_frames == 1 | ||||
|         assert not im.is_animated | ||||
| 
 | ||||
| 
 | ||||
| def test_eoferror() -> None: | ||||
|     with Image.open(TEST_FILE) as im: | ||||
|         assert isinstance(im, DcxImagePlugin.DcxImageFile) | ||||
|         n_frames = im.n_frames | ||||
| 
 | ||||
|         # Test seeking past the last frame | ||||
|  |  | |||
|  | @ -86,6 +86,8 @@ simple_eps_file_with_long_binary_data = ( | |||
| def test_sanity(filename: str, size: tuple[int, int], scale: int) -> None: | ||||
|     expected_size = tuple(s * scale for s in size) | ||||
|     with Image.open(filename) as image: | ||||
|         assert isinstance(image, EpsImagePlugin.EpsImageFile) | ||||
| 
 | ||||
|         image.load(scale=scale) | ||||
|         assert image.mode == "RGB" | ||||
|         assert image.size == expected_size | ||||
|  | @ -227,6 +229,8 @@ def test_showpage() -> None: | |||
| @pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") | ||||
| def test_transparency() -> None: | ||||
|     with Image.open("Tests/images/eps/reqd_showpage.eps") as plot_image: | ||||
|         assert isinstance(plot_image, EpsImagePlugin.EpsImageFile) | ||||
| 
 | ||||
|         plot_image.load(transparency=True) | ||||
|         assert plot_image.mode == "RGBA" | ||||
| 
 | ||||
|  | @ -308,6 +312,7 @@ def test_render_scale2() -> None: | |||
| 
 | ||||
|     # Zero bounding box | ||||
|     with Image.open(FILE1) as image1_scale2: | ||||
|         assert isinstance(image1_scale2, EpsImagePlugin.EpsImageFile) | ||||
|         image1_scale2.load(scale=2) | ||||
|         with Image.open(FILE1_COMPARE_SCALE2) as image1_scale2_compare: | ||||
|             image1_scale2_compare = image1_scale2_compare.convert("RGB") | ||||
|  | @ -316,6 +321,7 @@ def test_render_scale2() -> None: | |||
| 
 | ||||
|     # Non-zero bounding box | ||||
|     with Image.open(FILE2) as image2_scale2: | ||||
|         assert isinstance(image2_scale2, EpsImagePlugin.EpsImageFile) | ||||
|         image2_scale2.load(scale=2) | ||||
|         with Image.open(FILE2_COMPARE_SCALE2) as image2_scale2_compare: | ||||
|             image2_scale2_compare = image2_scale2_compare.convert("RGB") | ||||
|  |  | |||
|  | @ -22,6 +22,8 @@ animated_test_file_with_prefix_chunk = "Tests/images/2422.flc" | |||
| 
 | ||||
| def test_sanity() -> None: | ||||
|     with Image.open(static_test_file) as im: | ||||
|         assert isinstance(im, FliImagePlugin.FliImageFile) | ||||
| 
 | ||||
|         im.load() | ||||
|         assert im.mode == "P" | ||||
|         assert im.size == (128, 128) | ||||
|  | @ -29,6 +31,8 @@ def test_sanity() -> None: | |||
|         assert not im.is_animated | ||||
| 
 | ||||
|     with Image.open(animated_test_file) as im: | ||||
|         assert isinstance(im, FliImagePlugin.FliImageFile) | ||||
| 
 | ||||
|         assert im.mode == "P" | ||||
|         assert im.size == (320, 200) | ||||
|         assert im.format == "FLI" | ||||
|  | @ -112,16 +116,19 @@ def test_palette_chunk_second() -> None: | |||
| 
 | ||||
| def test_n_frames() -> None: | ||||
|     with Image.open(static_test_file) as im: | ||||
|         assert isinstance(im, FliImagePlugin.FliImageFile) | ||||
|         assert im.n_frames == 1 | ||||
|         assert not im.is_animated | ||||
| 
 | ||||
|     with Image.open(animated_test_file) as im: | ||||
|         assert isinstance(im, FliImagePlugin.FliImageFile) | ||||
|         assert im.n_frames == 384 | ||||
|         assert im.is_animated | ||||
| 
 | ||||
| 
 | ||||
| def test_eoferror() -> None: | ||||
|     with Image.open(animated_test_file) as im: | ||||
|         assert isinstance(im, FliImagePlugin.FliImageFile) | ||||
|         n_frames = im.n_frames | ||||
| 
 | ||||
|         # Test seeking past the last frame | ||||
|  | @ -166,6 +173,7 @@ def test_seek_tell() -> None: | |||
| 
 | ||||
| def test_seek() -> None: | ||||
|     with Image.open(animated_test_file) as im: | ||||
|         assert isinstance(im, FliImagePlugin.FliImageFile) | ||||
|         im.seek(50) | ||||
| 
 | ||||
|         assert_image_equal_tofile(im, "Tests/images/a_fli.png") | ||||
|  |  | |||
|  | @ -22,10 +22,11 @@ def test_sanity() -> None: | |||
| 
 | ||||
| def test_close() -> None: | ||||
|     with Image.open("Tests/images/input_bw_one_band.fpx") as im: | ||||
|         pass | ||||
|         assert isinstance(im, FpxImagePlugin.FpxImageFile) | ||||
|     assert im.ole.fp.closed | ||||
| 
 | ||||
|     im = Image.open("Tests/images/input_bw_one_band.fpx") | ||||
|     assert isinstance(im, FpxImagePlugin.FpxImageFile) | ||||
|     im.close() | ||||
|     assert im.ole.fp.closed | ||||
| 
 | ||||
|  |  | |||
|  | @ -450,6 +450,7 @@ def test_save_netpbm_l_mode(tmp_path: Path) -> None: | |||
| 
 | ||||
| def test_seek() -> None: | ||||
|     with Image.open("Tests/images/dispose_none.gif") as img: | ||||
|         assert isinstance(img, GifImagePlugin.GifImageFile) | ||||
|         frame_count = 0 | ||||
|         try: | ||||
|             while True: | ||||
|  | @ -494,10 +495,12 @@ def test_seek_rewind() -> None: | |||
| def test_n_frames(path: str, n_frames: int) -> None: | ||||
|     # Test is_animated before n_frames | ||||
|     with Image.open(path) as im: | ||||
|         assert isinstance(im, GifImagePlugin.GifImageFile) | ||||
|         assert im.is_animated == (n_frames != 1) | ||||
| 
 | ||||
|     # Test is_animated after n_frames | ||||
|     with Image.open(path) as im: | ||||
|         assert isinstance(im, GifImagePlugin.GifImageFile) | ||||
|         assert im.n_frames == n_frames | ||||
|         assert im.is_animated == (n_frames != 1) | ||||
| 
 | ||||
|  | @ -507,6 +510,7 @@ def test_no_change() -> None: | |||
|     with Image.open("Tests/images/dispose_bgnd.gif") as im: | ||||
|         im.seek(1) | ||||
|         expected = im.copy() | ||||
|         assert isinstance(im, GifImagePlugin.GifImageFile) | ||||
|         assert im.n_frames == 5 | ||||
|         assert_image_equal(im, expected) | ||||
| 
 | ||||
|  | @ -514,17 +518,20 @@ def test_no_change() -> None: | |||
|     with Image.open("Tests/images/dispose_bgnd.gif") as im: | ||||
|         im.seek(3) | ||||
|         expected = im.copy() | ||||
|         assert isinstance(im, GifImagePlugin.GifImageFile) | ||||
|         assert im.is_animated | ||||
|         assert_image_equal(im, expected) | ||||
| 
 | ||||
|     with Image.open("Tests/images/comment_after_only_frame.gif") as im: | ||||
|         expected = Image.new("P", (1, 1)) | ||||
|         assert isinstance(im, GifImagePlugin.GifImageFile) | ||||
|         assert not im.is_animated | ||||
|         assert_image_equal(im, expected) | ||||
| 
 | ||||
| 
 | ||||
| def test_eoferror() -> None: | ||||
|     with Image.open(TEST_GIF) as im: | ||||
|         assert isinstance(im, GifImagePlugin.GifImageFile) | ||||
|         n_frames = im.n_frames | ||||
| 
 | ||||
|         # Test seeking past the last frame | ||||
|  | @ -543,6 +550,7 @@ def test_first_frame_transparency() -> None: | |||
| 
 | ||||
| def test_dispose_none() -> None: | ||||
|     with Image.open("Tests/images/dispose_none.gif") as img: | ||||
|         assert isinstance(img, GifImagePlugin.GifImageFile) | ||||
|         try: | ||||
|             while True: | ||||
|                 img.seek(img.tell() + 1) | ||||
|  | @ -566,6 +574,7 @@ def test_dispose_none_load_end() -> None: | |||
| 
 | ||||
| def test_dispose_background() -> None: | ||||
|     with Image.open("Tests/images/dispose_bgnd.gif") as img: | ||||
|         assert isinstance(img, GifImagePlugin.GifImageFile) | ||||
|         try: | ||||
|             while True: | ||||
|                 img.seek(img.tell() + 1) | ||||
|  | @ -619,6 +628,7 @@ def test_transparent_dispose( | |||
| 
 | ||||
| def test_dispose_previous() -> None: | ||||
|     with Image.open("Tests/images/dispose_prev.gif") as img: | ||||
|         assert isinstance(img, GifImagePlugin.GifImageFile) | ||||
|         try: | ||||
|             while True: | ||||
|                 img.seek(img.tell() + 1) | ||||
|  | @ -656,6 +666,7 @@ def test_save_dispose(tmp_path: Path) -> None: | |||
|     for method in range(4): | ||||
|         im_list[0].save(out, save_all=True, append_images=im_list[1:], disposal=method) | ||||
|         with Image.open(out) as img: | ||||
|             assert isinstance(img, GifImagePlugin.GifImageFile) | ||||
|             for _ in range(2): | ||||
|                 img.seek(img.tell() + 1) | ||||
|                 assert img.disposal_method == method | ||||
|  | @ -669,6 +680,7 @@ def test_save_dispose(tmp_path: Path) -> None: | |||
|     ) | ||||
| 
 | ||||
|     with Image.open(out) as img: | ||||
|         assert isinstance(img, GifImagePlugin.GifImageFile) | ||||
|         for i in range(2): | ||||
|             img.seek(img.tell() + 1) | ||||
|             assert img.disposal_method == i + 1 | ||||
|  | @ -791,6 +803,7 @@ def test_dispose2_background_frame(tmp_path: Path) -> None: | |||
|     im_list[0].save(out, save_all=True, append_images=im_list[1:], disposal=2) | ||||
| 
 | ||||
|     with Image.open(out) as im: | ||||
|         assert isinstance(im, GifImagePlugin.GifImageFile) | ||||
|         assert im.n_frames == 3 | ||||
| 
 | ||||
| 
 | ||||
|  | @ -972,6 +985,8 @@ def test_identical_frames(tmp_path: Path) -> None: | |||
|         out, save_all=True, append_images=im_list[1:], duration=duration_list | ||||
|     ) | ||||
|     with Image.open(out) as reread: | ||||
|         assert isinstance(reread, GifImagePlugin.GifImageFile) | ||||
| 
 | ||||
|         # Assert that the first three frames were combined | ||||
|         assert reread.n_frames == 2 | ||||
| 
 | ||||
|  | @ -1001,6 +1016,8 @@ def test_identical_frames_to_single_frame( | |||
| 
 | ||||
|     im_list[0].save(out, save_all=True, append_images=im_list[1:], duration=duration) | ||||
|     with Image.open(out) as reread: | ||||
|         assert isinstance(reread, GifImagePlugin.GifImageFile) | ||||
| 
 | ||||
|         # Assert that all frames were combined | ||||
|         assert reread.n_frames == 1 | ||||
| 
 | ||||
|  | @ -1187,6 +1204,14 @@ def test_append_images(tmp_path: Path) -> None: | |||
|     im.copy().save(out, save_all=True, append_images=ims) | ||||
| 
 | ||||
|     with Image.open(out) as reread: | ||||
|         assert isinstance(reread, GifImagePlugin.GifImageFile) | ||||
|         assert reread.n_frames == 3 | ||||
| 
 | ||||
|     # Test append_images without save_all | ||||
|     im.copy().save(out, append_images=ims) | ||||
| 
 | ||||
|     with Image.open(out) as reread: | ||||
|         assert isinstance(reread, GifImagePlugin.GifImageFile) | ||||
|         assert reread.n_frames == 3 | ||||
| 
 | ||||
|     # Tests appending using a generator | ||||
|  | @ -1196,6 +1221,7 @@ def test_append_images(tmp_path: Path) -> None: | |||
|     im.save(out, save_all=True, append_images=im_generator(ims)) | ||||
| 
 | ||||
|     with Image.open(out) as reread: | ||||
|         assert isinstance(reread, GifImagePlugin.GifImageFile) | ||||
|         assert reread.n_frames == 3 | ||||
| 
 | ||||
|     # Tests appending single and multiple frame images | ||||
|  | @ -1204,6 +1230,7 @@ def test_append_images(tmp_path: Path) -> None: | |||
|             im.save(out, save_all=True, append_images=[im2]) | ||||
| 
 | ||||
|     with Image.open(out) as reread: | ||||
|         assert isinstance(reread, GifImagePlugin.GifImageFile) | ||||
|         assert reread.n_frames == 10 | ||||
| 
 | ||||
| 
 | ||||
|  | @ -1304,6 +1331,7 @@ def test_bbox(tmp_path: Path) -> None: | |||
|     im.save(out, save_all=True, append_images=ims) | ||||
| 
 | ||||
|     with Image.open(out) as reread: | ||||
|         assert isinstance(reread, GifImagePlugin.GifImageFile) | ||||
|         assert reread.n_frames == 2 | ||||
| 
 | ||||
| 
 | ||||
|  | @ -1316,6 +1344,7 @@ def test_bbox_alpha(tmp_path: Path) -> None: | |||
|     im.save(out, save_all=True, append_images=[im2]) | ||||
| 
 | ||||
|     with Image.open(out) as reread: | ||||
|         assert isinstance(reread, GifImagePlugin.GifImageFile) | ||||
|         assert reread.n_frames == 2 | ||||
| 
 | ||||
| 
 | ||||
|  | @ -1467,6 +1496,7 @@ def test_extents( | |||
| ) -> None: | ||||
|     monkeypatch.setattr(GifImagePlugin, "LOADING_STRATEGY", loading_strategy) | ||||
|     with Image.open("Tests/images/" + test_file) as im: | ||||
|         assert isinstance(im, GifImagePlugin.GifImageFile) | ||||
|         assert im.size == (100, 100) | ||||
| 
 | ||||
|         # Check that n_frames does not change the size | ||||
|  | @ -1514,4 +1544,5 @@ def test_p_rgba(tmp_path: Path, params: dict[str, Any]) -> None: | |||
|     im1.save(out, save_all=True, append_images=[im2], **params) | ||||
| 
 | ||||
|     with Image.open(out) as reloaded: | ||||
|         assert isinstance(reloaded, GifImagePlugin.GifImageFile) | ||||
|         assert reloaded.n_frames == 2 | ||||
|  |  | |||
|  | @ -1,5 +1,7 @@ | |||
| from __future__ import annotations | ||||
| 
 | ||||
| from io import BytesIO | ||||
| 
 | ||||
| import pytest | ||||
| 
 | ||||
| from PIL.GimpPaletteFile import GimpPaletteFile | ||||
|  | @ -14,17 +16,20 @@ def test_sanity() -> None: | |||
|             GimpPaletteFile(fp) | ||||
| 
 | ||||
|     with open("Tests/images/bad_palette_file.gpl", "rb") as fp: | ||||
|         with pytest.raises(SyntaxError): | ||||
|         with pytest.raises(SyntaxError, match="bad palette file"): | ||||
|             GimpPaletteFile(fp) | ||||
| 
 | ||||
|     with open("Tests/images/bad_palette_entry.gpl", "rb") as fp: | ||||
|         with pytest.raises(ValueError): | ||||
|         with pytest.raises(ValueError, match="bad palette entry"): | ||||
|             GimpPaletteFile(fp) | ||||
| 
 | ||||
| 
 | ||||
| def test_get_palette() -> None: | ||||
| @pytest.mark.parametrize( | ||||
|     "filename, size", (("custom_gimp_palette.gpl", 8), ("full_gimp_palette.gpl", 256)) | ||||
| ) | ||||
| def test_get_palette(filename: str, size: int) -> None: | ||||
|     # Arrange | ||||
|     with open("Tests/images/custom_gimp_palette.gpl", "rb") as fp: | ||||
|     with open("Tests/images/" + filename, "rb") as fp: | ||||
|         palette_file = GimpPaletteFile(fp) | ||||
| 
 | ||||
|     # Act | ||||
|  | @ -32,4 +37,36 @@ def test_get_palette() -> None: | |||
| 
 | ||||
|     # Assert | ||||
|     assert mode == "RGB" | ||||
|     assert len(palette) / 3 == 8 | ||||
|     assert len(palette) / 3 == size | ||||
| 
 | ||||
| 
 | ||||
| def test_frombytes() -> None: | ||||
|     # Test that __init__ stops reading after 260 lines | ||||
|     with open("Tests/images/custom_gimp_palette.gpl", "rb") as fp: | ||||
|         custom_data = fp.read() | ||||
|     custom_data += b"#\n" * 300 + b"  0   0   0     Index 12" | ||||
|     b = BytesIO(custom_data) | ||||
|     palette = GimpPaletteFile(b) | ||||
|     assert len(palette.palette) / 3 == 8 | ||||
| 
 | ||||
|     # Test that __init__ only reads 256 entries | ||||
|     with open("Tests/images/full_gimp_palette.gpl", "rb") as fp: | ||||
|         full_data = fp.read() | ||||
|     data = full_data.replace(b"#\n", b"") + b"  0   0   0     Index 256" | ||||
|     b = BytesIO(data) | ||||
|     palette = GimpPaletteFile(b) | ||||
|     assert len(palette.palette) / 3 == 256 | ||||
| 
 | ||||
|     # Test that frombytes() can read beyond that | ||||
|     palette = GimpPaletteFile.frombytes(data) | ||||
|     assert len(palette.palette) / 3 == 257 | ||||
| 
 | ||||
|     # Test that __init__ raises an error if a comment is too long | ||||
|     data = full_data[:-1] + b"a" * 100 | ||||
|     b = BytesIO(data) | ||||
|     with pytest.raises(SyntaxError, match="bad palette file"): | ||||
|         palette = GimpPaletteFile(b) | ||||
| 
 | ||||
|     # Test that frombytes() can read the data regardless | ||||
|     palette = GimpPaletteFile.frombytes(data) | ||||
|     assert len(palette.palette) / 3 == 256 | ||||
|  |  | |||
|  | @ -43,7 +43,7 @@ def test_save() -> None: | |||
|     # Arrange | ||||
|     with Image.open(TEST_FILE) as im: | ||||
|         dummy_fp = BytesIO() | ||||
|         dummy_filename = "dummy.filename" | ||||
|         dummy_filename = "dummy.h5" | ||||
| 
 | ||||
|         # Act / Assert: stub cannot save without an implemented handler | ||||
|         with pytest.raises(OSError): | ||||
|  |  | |||
|  | @ -69,6 +69,7 @@ def test_save_append_images(tmp_path: Path) -> None: | |||
|         assert_image_similar_tofile(im, temp_file, 1) | ||||
| 
 | ||||
|         with Image.open(temp_file) as reread: | ||||
|             assert isinstance(reread, IcnsImagePlugin.IcnsImageFile) | ||||
|             reread.size = (16, 16) | ||||
|             reread.load(2) | ||||
|             assert_image_equal(reread, provided_im) | ||||
|  | @ -90,6 +91,7 @@ def test_sizes() -> None: | |||
|     # Check that we can load all of the sizes, and that the final pixel | ||||
|     # dimensions are as expected | ||||
|     with Image.open(TEST_FILE) as im: | ||||
|         assert isinstance(im, IcnsImagePlugin.IcnsImageFile) | ||||
|         for w, h, r in im.info["sizes"]: | ||||
|             wr = w * r | ||||
|             hr = h * r | ||||
|  | @ -118,6 +120,7 @@ def test_older_icon() -> None: | |||
|             wr = w * r | ||||
|             hr = h * r | ||||
|             with Image.open("Tests/images/pillow2.icns") as im2: | ||||
|                 assert isinstance(im2, IcnsImagePlugin.IcnsImageFile) | ||||
|                 im2.size = (w, h) | ||||
|                 im2.load(r) | ||||
|                 assert im2.mode == "RGBA" | ||||
|  | @ -135,6 +138,7 @@ def test_jp2_icon() -> None: | |||
|             wr = w * r | ||||
|             hr = h * r | ||||
|             with Image.open("Tests/images/pillow3.icns") as im2: | ||||
|                 assert isinstance(im2, IcnsImagePlugin.IcnsImageFile) | ||||
|                 im2.size = (w, h) | ||||
|                 im2.load(r) | ||||
|                 assert im2.mode == "RGBA" | ||||
|  |  | |||
|  | @ -77,6 +77,7 @@ def test_save_to_bytes() -> None: | |||
|     # The other one | ||||
|     output.seek(0) | ||||
|     with Image.open(output) as reloaded: | ||||
|         assert isinstance(reloaded, IcoImagePlugin.IcoImageFile) | ||||
|         reloaded.size = (32, 32) | ||||
| 
 | ||||
|         assert im.mode == reloaded.mode | ||||
|  | @ -94,6 +95,7 @@ def test_getpixel(tmp_path: Path) -> None: | |||
|     im.save(temp_file, "ico", sizes=[(32, 32), (64, 64)]) | ||||
| 
 | ||||
|     with Image.open(temp_file) as reloaded: | ||||
|         assert isinstance(reloaded, IcoImagePlugin.IcoImageFile) | ||||
|         reloaded.load() | ||||
|         reloaded.size = (32, 32) | ||||
| 
 | ||||
|  | @ -167,6 +169,7 @@ def test_save_to_bytes_bmp(mode: str) -> None: | |||
|     # The other one | ||||
|     output.seek(0) | ||||
|     with Image.open(output) as reloaded: | ||||
|         assert isinstance(reloaded, IcoImagePlugin.IcoImageFile) | ||||
|         reloaded.size = (32, 32) | ||||
| 
 | ||||
|         assert "RGBA" == reloaded.mode | ||||
|  | @ -178,6 +181,7 @@ def test_save_to_bytes_bmp(mode: str) -> None: | |||
| 
 | ||||
| def test_incorrect_size() -> None: | ||||
|     with Image.open(TEST_ICO_FILE) as im: | ||||
|         assert isinstance(im, IcoImagePlugin.IcoImageFile) | ||||
|         with pytest.raises(ValueError): | ||||
|             im.size = (1, 1) | ||||
| 
 | ||||
|  | @ -219,6 +223,7 @@ def test_save_append_images(tmp_path: Path) -> None: | |||
|     im.save(outfile, sizes=[(32, 32), (128, 128)], append_images=[provided_im]) | ||||
| 
 | ||||
|     with Image.open(outfile) as reread: | ||||
|         assert isinstance(reread, IcoImagePlugin.IcoImageFile) | ||||
|         assert_image_equal(reread, hopper("RGBA")) | ||||
| 
 | ||||
|         reread.size = (32, 32) | ||||
|  |  | |||
|  | @ -68,12 +68,14 @@ def test_tell() -> None: | |||
| 
 | ||||
| def test_n_frames() -> None: | ||||
|     with Image.open(TEST_IM) as im: | ||||
|         assert isinstance(im, ImImagePlugin.ImImageFile) | ||||
|         assert im.n_frames == 1 | ||||
|         assert not im.is_animated | ||||
| 
 | ||||
| 
 | ||||
| def test_eoferror() -> None: | ||||
|     with Image.open(TEST_IM) as im: | ||||
|         assert isinstance(im, ImImagePlugin.ImImageFile) | ||||
|         n_frames = im.n_frames | ||||
| 
 | ||||
|         # Test seeking past the last frame | ||||
|  |  | |||
|  | @ -91,6 +91,7 @@ class TestFileJpeg: | |||
|     def test_app(self) -> None: | ||||
|         # Test APP/COM reader (@PIL135) | ||||
|         with Image.open(TEST_FILE) as im: | ||||
|             assert isinstance(im, JpegImagePlugin.JpegImageFile) | ||||
|             assert im.applist[0] == ("APP0", b"JFIF\x00\x01\x01\x01\x00`\x00`\x00\x00") | ||||
|             assert im.applist[1] == ( | ||||
|                 "COM", | ||||
|  | @ -316,6 +317,8 @@ class TestFileJpeg: | |||
| 
 | ||||
|     def test_exif_typeerror(self) -> None: | ||||
|         with Image.open("Tests/images/exif_typeerror.jpg") as im: | ||||
|             assert isinstance(im, JpegImagePlugin.JpegImageFile) | ||||
| 
 | ||||
|             # Should not raise a TypeError | ||||
|             im._getexif() | ||||
| 
 | ||||
|  | @ -500,6 +503,7 @@ class TestFileJpeg: | |||
| 
 | ||||
|     def test_mp(self) -> None: | ||||
|         with Image.open("Tests/images/pil_sample_rgb.jpg") as im: | ||||
|             assert isinstance(im, JpegImagePlugin.JpegImageFile) | ||||
|             assert im._getmp() is None | ||||
| 
 | ||||
|     def test_quality_keep(self, tmp_path: Path) -> None: | ||||
|  | @ -558,12 +562,14 @@ class TestFileJpeg: | |||
|             with Image.open(test_file) as im: | ||||
|                 im.save(b, "JPEG", qtables=[[n] * 64] * n) | ||||
|             with Image.open(b) as im: | ||||
|                 assert isinstance(im, JpegImagePlugin.JpegImageFile) | ||||
|                 assert len(im.quantization) == n | ||||
|                 reloaded = self.roundtrip(im, qtables="keep") | ||||
|                 assert im.quantization == reloaded.quantization | ||||
|                 assert max(reloaded.quantization[0]) <= 255 | ||||
| 
 | ||||
|         with Image.open("Tests/images/hopper.jpg") as im: | ||||
|             assert isinstance(im, JpegImagePlugin.JpegImageFile) | ||||
|             qtables = im.quantization | ||||
|             reloaded = self.roundtrip(im, qtables=qtables, subsampling=0) | ||||
|             assert im.quantization == reloaded.quantization | ||||
|  | @ -663,6 +669,7 @@ class TestFileJpeg: | |||
| 
 | ||||
|     def test_load_16bit_qtables(self) -> None: | ||||
|         with Image.open("Tests/images/hopper_16bit_qtables.jpg") as im: | ||||
|             assert isinstance(im, JpegImagePlugin.JpegImageFile) | ||||
|             assert len(im.quantization) == 2 | ||||
|             assert len(im.quantization[0]) == 64 | ||||
|             assert max(im.quantization[0]) > 255 | ||||
|  | @ -705,6 +712,7 @@ class TestFileJpeg: | |||
|     @pytest.mark.skipif(not djpeg_available(), reason="djpeg not available") | ||||
|     def test_load_djpeg(self) -> None: | ||||
|         with Image.open(TEST_FILE) as img: | ||||
|             assert isinstance(img, JpegImagePlugin.JpegImageFile) | ||||
|             img.load_djpeg() | ||||
|             assert_image_similar_tofile(img, TEST_FILE, 5) | ||||
| 
 | ||||
|  | @ -909,6 +917,7 @@ class TestFileJpeg: | |||
| 
 | ||||
|     def test_photoshop_malformed_and_multiple(self) -> None: | ||||
|         with Image.open("Tests/images/app13-multiple.jpg") as im: | ||||
|             assert isinstance(im, JpegImagePlugin.JpegImageFile) | ||||
|             assert "photoshop" in im.info | ||||
|             assert 24 == len(im.info["photoshop"]) | ||||
|             apps_13_lengths = [len(v) for k, v in im.applist if k == "APP13"] | ||||
|  | @ -1084,6 +1093,7 @@ class TestFileJpeg: | |||
| 
 | ||||
|     def test_deprecation(self) -> None: | ||||
|         with Image.open(TEST_FILE) as im: | ||||
|             assert isinstance(im, JpegImagePlugin.JpegImageFile) | ||||
|             with pytest.warns(DeprecationWarning): | ||||
|                 assert im.huffman_ac == {} | ||||
|             with pytest.warns(DeprecationWarning): | ||||
|  |  | |||
|  | @ -228,12 +228,14 @@ def test_layers(card: ImageFile.ImageFile) -> None: | |||
|     out.seek(0) | ||||
| 
 | ||||
|     with Image.open(out) as im: | ||||
|         assert isinstance(im, Jpeg2KImagePlugin.Jpeg2KImageFile) | ||||
|         im.layers = 1 | ||||
|         im.load() | ||||
|         assert_image_similar(im, card, 13) | ||||
| 
 | ||||
|     out.seek(0) | ||||
|     with Image.open(out) as im: | ||||
|         assert isinstance(im, Jpeg2KImagePlugin.Jpeg2KImageFile) | ||||
|         im.layers = 3 | ||||
|         im.load() | ||||
|         assert_image_similar(im, card, 0.4) | ||||
|  |  | |||
|  | @ -36,6 +36,7 @@ class LibTiffTestCase: | |||
|         im.load() | ||||
|         im.getdata() | ||||
| 
 | ||||
|         assert isinstance(im, TiffImagePlugin.TiffImageFile) | ||||
|         assert im._compression == "group4" | ||||
| 
 | ||||
|         # can we write it back out, in a different form. | ||||
|  | @ -153,6 +154,7 @@ class TestFileLibTiff(LibTiffTestCase): | |||
|         """Test metadata writing through libtiff""" | ||||
|         f = tmp_path / "temp.tiff" | ||||
|         with Image.open("Tests/images/hopper_g4.tif") as img: | ||||
|             assert isinstance(img, TiffImagePlugin.TiffImageFile) | ||||
|             img.save(f, tiffinfo=img.tag) | ||||
| 
 | ||||
|             if legacy_api: | ||||
|  | @ -170,6 +172,7 @@ class TestFileLibTiff(LibTiffTestCase): | |||
|         ] | ||||
| 
 | ||||
|         with Image.open(f) as loaded: | ||||
|             assert isinstance(loaded, TiffImagePlugin.TiffImageFile) | ||||
|             if legacy_api: | ||||
|                 reloaded = loaded.tag.named() | ||||
|             else: | ||||
|  | @ -212,6 +215,7 @@ class TestFileLibTiff(LibTiffTestCase): | |||
|         # Exclude ones that have special meaning | ||||
|         # that we're already testing them | ||||
|         with Image.open("Tests/images/hopper_g4.tif") as im: | ||||
|             assert isinstance(im, TiffImagePlugin.TiffImageFile) | ||||
|             for tag in im.tag_v2: | ||||
|                 try: | ||||
|                     del core_items[tag] | ||||
|  | @ -317,6 +321,7 @@ class TestFileLibTiff(LibTiffTestCase): | |||
|             im.save(out, tiffinfo=tiffinfo) | ||||
| 
 | ||||
|             with Image.open(out) as reloaded: | ||||
|                 assert isinstance(reloaded, TiffImagePlugin.TiffImageFile) | ||||
|                 for tag, value in tiffinfo.items(): | ||||
|                     reloaded_value = reloaded.tag_v2[tag] | ||||
|                     if ( | ||||
|  | @ -349,12 +354,14 @@ class TestFileLibTiff(LibTiffTestCase): | |||
|     def test_osubfiletype(self, tmp_path: Path) -> None: | ||||
|         outfile = tmp_path / "temp.tif" | ||||
|         with Image.open("Tests/images/g4_orientation_6.tif") as im: | ||||
|             assert isinstance(im, TiffImagePlugin.TiffImageFile) | ||||
|             im.tag_v2[OSUBFILETYPE] = 1 | ||||
|             im.save(outfile) | ||||
| 
 | ||||
|     def test_subifd(self, tmp_path: Path) -> None: | ||||
|         outfile = tmp_path / "temp.tif" | ||||
|         with Image.open("Tests/images/g4_orientation_6.tif") as im: | ||||
|             assert isinstance(im, TiffImagePlugin.TiffImageFile) | ||||
|             im.tag_v2[SUBIFD] = 10000 | ||||
| 
 | ||||
|             # Should not segfault | ||||
|  | @ -369,6 +376,7 @@ class TestFileLibTiff(LibTiffTestCase): | |||
|         hopper().save(out, tiffinfo={700: b"xmlpacket tag"}) | ||||
| 
 | ||||
|         with Image.open(out) as reloaded: | ||||
|             assert isinstance(reloaded, TiffImagePlugin.TiffImageFile) | ||||
|             if 700 in reloaded.tag_v2: | ||||
|                 assert reloaded.tag_v2[700] == b"xmlpacket tag" | ||||
| 
 | ||||
|  | @ -430,12 +438,15 @@ class TestFileLibTiff(LibTiffTestCase): | |||
|         """Tests String data in info directory""" | ||||
|         test_file = "Tests/images/hopper_g4_500.tif" | ||||
|         with Image.open(test_file) as orig: | ||||
|             assert isinstance(orig, TiffImagePlugin.TiffImageFile) | ||||
| 
 | ||||
|             out = tmp_path / "temp.tif" | ||||
| 
 | ||||
|             orig.tag[269] = "temp.tif" | ||||
|             orig.save(out) | ||||
| 
 | ||||
|         with Image.open(out) as reread: | ||||
|             assert isinstance(reread, TiffImagePlugin.TiffImageFile) | ||||
|             assert "temp.tif" == reread.tag_v2[269] | ||||
|             assert "temp.tif" == reread.tag[269][0] | ||||
| 
 | ||||
|  | @ -541,6 +552,7 @@ class TestFileLibTiff(LibTiffTestCase): | |||
| 
 | ||||
|         with Image.open(out) as reloaded: | ||||
|             # colormap/palette tag | ||||
|             assert isinstance(reloaded, TiffImagePlugin.TiffImageFile) | ||||
|             assert len(reloaded.tag_v2[320]) == 768 | ||||
| 
 | ||||
|     @pytest.mark.parametrize("compression", ("tiff_ccitt", "group3", "group4")) | ||||
|  | @ -572,6 +584,7 @@ class TestFileLibTiff(LibTiffTestCase): | |||
|         with Image.open("Tests/images/multipage.tiff") as im: | ||||
|             # file is a multipage tiff,  10x10 green, 10x10 red, 20x20 blue | ||||
| 
 | ||||
|             assert isinstance(im, TiffImagePlugin.TiffImageFile) | ||||
|             im.seek(0) | ||||
|             assert im.size == (10, 10) | ||||
|             assert im.convert("RGB").getpixel((0, 0)) == (0, 128, 0) | ||||
|  | @ -591,6 +604,7 @@ class TestFileLibTiff(LibTiffTestCase): | |||
|         # issue #862 | ||||
|         monkeypatch.setattr(TiffImagePlugin, "READ_LIBTIFF", True) | ||||
|         with Image.open("Tests/images/multipage.tiff") as im: | ||||
|             assert isinstance(im, TiffImagePlugin.TiffImageFile) | ||||
|             frames = im.n_frames | ||||
|             assert frames == 3 | ||||
|             for _ in range(frames): | ||||
|  | @ -610,6 +624,7 @@ class TestFileLibTiff(LibTiffTestCase): | |||
|     def test__next(self, monkeypatch: pytest.MonkeyPatch) -> None: | ||||
|         monkeypatch.setattr(TiffImagePlugin, "READ_LIBTIFF", True) | ||||
|         with Image.open("Tests/images/hopper.tif") as im: | ||||
|             assert isinstance(im, TiffImagePlugin.TiffImageFile) | ||||
|             assert not im.tag.next | ||||
|             im.load() | ||||
|             assert not im.tag.next | ||||
|  | @ -690,21 +705,25 @@ class TestFileLibTiff(LibTiffTestCase): | |||
|         im.save(outfile, compression="jpeg") | ||||
| 
 | ||||
|         with Image.open(outfile) as reloaded: | ||||
|             assert isinstance(reloaded, TiffImagePlugin.TiffImageFile) | ||||
|             assert reloaded.tag_v2[530] == (1, 1) | ||||
|             assert reloaded.tag_v2[532] == (0, 255, 128, 255, 128, 255) | ||||
| 
 | ||||
|     def test_exif_ifd(self) -> None: | ||||
|         out = io.BytesIO() | ||||
|         with Image.open("Tests/images/tiff_adobe_deflate.tif") as im: | ||||
|             assert isinstance(im, TiffImagePlugin.TiffImageFile) | ||||
|             assert im.tag_v2[34665] == 125456 | ||||
|             im.save(out, "TIFF") | ||||
| 
 | ||||
|             with Image.open(out) as reloaded: | ||||
|                 assert isinstance(reloaded, TiffImagePlugin.TiffImageFile) | ||||
|                 assert 34665 not in reloaded.tag_v2 | ||||
| 
 | ||||
|             im.save(out, "TIFF", tiffinfo={34665: 125456}) | ||||
| 
 | ||||
|         with Image.open(out) as reloaded: | ||||
|             assert isinstance(reloaded, TiffImagePlugin.TiffImageFile) | ||||
|             if Image.core.libtiff_support_custom_tags: | ||||
|                 assert reloaded.tag_v2[34665] == 125456 | ||||
| 
 | ||||
|  | @ -786,6 +805,7 @@ class TestFileLibTiff(LibTiffTestCase): | |||
| 
 | ||||
|     def test_multipage_compression(self) -> None: | ||||
|         with Image.open("Tests/images/compression.tif") as im: | ||||
|             assert isinstance(im, TiffImagePlugin.TiffImageFile) | ||||
|             im.seek(0) | ||||
|             assert im._compression == "tiff_ccitt" | ||||
|             assert im.size == (10, 10) | ||||
|  | @ -1026,6 +1046,17 @@ class TestFileLibTiff(LibTiffTestCase): | |||
|         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_old_style_jpeg_orientation(self) -> None: | ||||
|         with open("Tests/images/old-style-jpeg-compression.tif", "rb") as fp: | ||||
|             data = fp.read() | ||||
| 
 | ||||
|         # Set EXIF Orientation to 2 | ||||
|         data = data[:102] + b"\x02" + data[103:] | ||||
| 
 | ||||
|         with Image.open(io.BytesIO(data)) as im: | ||||
|             im = im.transpose(Image.Transpose.FLIP_LEFT_RIGHT) | ||||
|         assert_image_equal_tofile(im, "Tests/images/old-style-jpeg-compression.png") | ||||
| 
 | ||||
|     def test_open_missing_samplesperpixel(self) -> None: | ||||
|         with Image.open( | ||||
|             "Tests/images/old-style-jpeg-compression-no-samplesperpixel.tif" | ||||
|  | @ -1079,6 +1110,7 @@ class TestFileLibTiff(LibTiffTestCase): | |||
|         with Image.open("Tests/images/g4_orientation_1.tif") as base_im: | ||||
|             for i in range(2, 9): | ||||
|                 with Image.open("Tests/images/g4_orientation_" + str(i) + ".tif") as im: | ||||
|                     assert isinstance(im, TiffImagePlugin.TiffImageFile) | ||||
|                     assert 274 in im.tag_v2 | ||||
| 
 | ||||
|                     im.load() | ||||
|  |  | |||
|  | @ -30,11 +30,13 @@ def test_sanity() -> None: | |||
| 
 | ||||
| def test_n_frames() -> None: | ||||
|     with Image.open(TEST_FILE) as im: | ||||
|         assert isinstance(im, MicImagePlugin.MicImageFile) | ||||
|         assert im.n_frames == 1 | ||||
| 
 | ||||
| 
 | ||||
| def test_is_animated() -> None: | ||||
|     with Image.open(TEST_FILE) as im: | ||||
|         assert isinstance(im, MicImagePlugin.MicImageFile) | ||||
|         assert not im.is_animated | ||||
| 
 | ||||
| 
 | ||||
|  | @ -55,10 +57,11 @@ def test_seek() -> None: | |||
| 
 | ||||
| def test_close() -> None: | ||||
|     with Image.open(TEST_FILE) as im: | ||||
|         pass | ||||
|         assert isinstance(im, MicImagePlugin.MicImageFile) | ||||
|     assert im.ole.fp.closed | ||||
| 
 | ||||
|     im = Image.open(TEST_FILE) | ||||
|     assert isinstance(im, MicImagePlugin.MicImageFile) | ||||
|     im.close() | ||||
|     assert im.ole.fp.closed | ||||
| 
 | ||||
|  |  | |||
|  | @ -6,7 +6,7 @@ from typing import Any | |||
| 
 | ||||
| import pytest | ||||
| 
 | ||||
| from PIL import Image, ImageFile, MpoImagePlugin | ||||
| from PIL import Image, ImageFile, JpegImagePlugin, MpoImagePlugin | ||||
| 
 | ||||
| from .helper import ( | ||||
|     assert_image_equal, | ||||
|  | @ -80,6 +80,7 @@ def test_context_manager() -> None: | |||
| def test_app(test_file: str) -> None: | ||||
|     # Test APP/COM reader (@PIL135) | ||||
|     with Image.open(test_file) as im: | ||||
|         assert isinstance(im, MpoImagePlugin.MpoImageFile) | ||||
|         assert im.applist[0][0] == "APP1" | ||||
|         assert im.applist[1][0] == "APP2" | ||||
|         assert im.applist[1][1].startswith( | ||||
|  | @ -220,12 +221,14 @@ def test_seek(test_file: str) -> None: | |||
| 
 | ||||
| def test_n_frames() -> None: | ||||
|     with Image.open("Tests/images/sugarshack.mpo") as im: | ||||
|         assert isinstance(im, MpoImagePlugin.MpoImageFile) | ||||
|         assert im.n_frames == 2 | ||||
|         assert im.is_animated | ||||
| 
 | ||||
| 
 | ||||
| def test_eoferror() -> None: | ||||
|     with Image.open("Tests/images/sugarshack.mpo") as im: | ||||
|         assert isinstance(im, MpoImagePlugin.MpoImageFile) | ||||
|         n_frames = im.n_frames | ||||
| 
 | ||||
|         # Test seeking past the last frame | ||||
|  | @ -239,6 +242,8 @@ def test_eoferror() -> None: | |||
| 
 | ||||
| def test_adopt_jpeg() -> None: | ||||
|     with Image.open("Tests/images/hopper.jpg") as im: | ||||
|         assert isinstance(im, JpegImagePlugin.JpegImageFile) | ||||
| 
 | ||||
|         with pytest.raises(ValueError): | ||||
|             MpoImagePlugin.MpoImageFile.adopt(im) | ||||
| 
 | ||||
|  |  | |||
|  | @ -43,6 +43,11 @@ def roundtrip(tmp_path: Path, mode: str) -> None: | |||
| 
 | ||||
|     im.save(outfile) | ||||
|     converted = open_with_magick(magick, tmp_path, outfile) | ||||
|     if mode == "P": | ||||
|         assert converted.mode == "P" | ||||
| 
 | ||||
|         im = im.convert("RGB") | ||||
|         converted = converted.convert("RGB") | ||||
|     assert_image_equal(converted, im) | ||||
| 
 | ||||
| 
 | ||||
|  | @ -55,7 +60,6 @@ def test_monochrome(tmp_path: Path) -> None: | |||
|     roundtrip(tmp_path, mode) | ||||
| 
 | ||||
| 
 | ||||
| @pytest.mark.xfail(reason="Palm P image is wrong") | ||||
| def test_p_mode(tmp_path: Path) -> None: | ||||
|     # Arrange | ||||
|     mode = "P" | ||||
|  |  | |||
|  | @ -576,6 +576,7 @@ class TestFilePng: | |||
| 
 | ||||
|     def test_read_private_chunks(self) -> None: | ||||
|         with Image.open("Tests/images/exif.png") as im: | ||||
|             assert isinstance(im, PngImagePlugin.PngImageFile) | ||||
|             assert im.private_chunks == [(b"orNT", b"\x01")] | ||||
| 
 | ||||
|     def test_roundtrip_private_chunk(self) -> None: | ||||
|  | @ -598,6 +599,7 @@ class TestFilePng: | |||
| 
 | ||||
|     def test_textual_chunks_after_idat(self, monkeypatch: pytest.MonkeyPatch) -> None: | ||||
|         with Image.open("Tests/images/hopper.png") as im: | ||||
|             assert isinstance(im, PngImagePlugin.PngImageFile) | ||||
|             assert "comment" in im.text | ||||
|             for k, v in { | ||||
|                 "date:create": "2014-09-04T09:37:08+03:00", | ||||
|  | @ -607,15 +609,19 @@ class TestFilePng: | |||
| 
 | ||||
|         # Raises a SyntaxError in load_end | ||||
|         with Image.open("Tests/images/broken_data_stream.png") as im: | ||||
|             assert isinstance(im, PngImagePlugin.PngImageFile) | ||||
|             with pytest.raises(OSError): | ||||
|                 assert isinstance(im.text, dict) | ||||
| 
 | ||||
|         # Raises an EOFError in load_end | ||||
|         with Image.open("Tests/images/hopper_idat_after_image_end.png") as im: | ||||
|             assert isinstance(im, PngImagePlugin.PngImageFile) | ||||
|             assert im.text == {"TXT": "VALUE", "ZIP": "VALUE"} | ||||
| 
 | ||||
|         # Raises a UnicodeDecodeError in load_end | ||||
|         with Image.open("Tests/images/truncated_image.png") as im: | ||||
|             assert isinstance(im, PngImagePlugin.PngImageFile) | ||||
| 
 | ||||
|             # The file is truncated | ||||
|             with pytest.raises(OSError): | ||||
|                 im.text | ||||
|  | @ -726,6 +732,7 @@ class TestFilePng: | |||
|             im.save(test_file) | ||||
| 
 | ||||
|         with Image.open(test_file) as reloaded: | ||||
|             assert isinstance(reloaded, PngImagePlugin.PngImageFile) | ||||
|             assert reloaded._getexif() is None | ||||
| 
 | ||||
|         # Test passing in exif | ||||
|  |  | |||
|  | @ -59,17 +59,21 @@ def test_invalid_file() -> None: | |||
| 
 | ||||
| def test_n_frames() -> None: | ||||
|     with Image.open("Tests/images/hopper_merged.psd") as im: | ||||
|         assert isinstance(im, PsdImagePlugin.PsdImageFile) | ||||
|         assert im.n_frames == 1 | ||||
|         assert not im.is_animated | ||||
| 
 | ||||
|     for path in [test_file, "Tests/images/negative_layer_count.psd"]: | ||||
|         with Image.open(path) as im: | ||||
|             assert isinstance(im, PsdImagePlugin.PsdImageFile) | ||||
|             assert im.n_frames == 2 | ||||
|             assert im.is_animated | ||||
| 
 | ||||
| 
 | ||||
| def test_eoferror() -> None: | ||||
|     with Image.open(test_file) as im: | ||||
|         assert isinstance(im, PsdImagePlugin.PsdImageFile) | ||||
| 
 | ||||
|         # PSD seek index starts at 1 rather than 0 | ||||
|         n_frames = im.n_frames + 1 | ||||
| 
 | ||||
|  | @ -119,11 +123,13 @@ def test_rgba() -> None: | |||
| 
 | ||||
| def test_negative_top_left_layer() -> None: | ||||
|     with Image.open("Tests/images/negative_top_left_layer.psd") as im: | ||||
|         assert isinstance(im, PsdImagePlugin.PsdImageFile) | ||||
|         assert im.layers[0][2] == (-50, -50, 50, 50) | ||||
| 
 | ||||
| 
 | ||||
| def test_layer_skip() -> None: | ||||
|     with Image.open("Tests/images/five_channels.psd") as im: | ||||
|         assert isinstance(im, PsdImagePlugin.PsdImageFile) | ||||
|         assert im.n_frames == 1 | ||||
| 
 | ||||
| 
 | ||||
|  | @ -175,5 +181,6 @@ def test_crashes(test_file: str, raises: type[Exception]) -> None: | |||
| def test_layer_crashes(test_file: str) -> None: | ||||
|     with open(test_file, "rb") as f: | ||||
|         with Image.open(f) as im: | ||||
|             assert isinstance(im, PsdImagePlugin.PsdImageFile) | ||||
|             with pytest.raises(SyntaxError): | ||||
|                 im.layers | ||||
|  |  | |||
|  | @ -71,8 +71,7 @@ def test_invalid_file() -> None: | |||
|         SgiImagePlugin.SgiImageFile(invalid_file) | ||||
| 
 | ||||
| 
 | ||||
| def test_write(tmp_path: Path) -> None: | ||||
|     def roundtrip(img: Image.Image) -> None: | ||||
| def roundtrip(img: Image.Image, tmp_path: Path) -> None: | ||||
|     out = tmp_path / "temp.sgi" | ||||
|     img.save(out, format="sgi") | ||||
|     assert_image_equal_tofile(img, out) | ||||
|  | @ -84,11 +83,14 @@ def test_write(tmp_path: Path) -> None: | |||
| 
 | ||||
|         assert not fp.closed | ||||
| 
 | ||||
|     for mode in ("L", "RGB", "RGBA"): | ||||
|         roundtrip(hopper(mode)) | ||||
| 
 | ||||
|     # Test 1 dimension for an L mode image | ||||
|     roundtrip(Image.new("L", (10, 1))) | ||||
| @pytest.mark.parametrize("mode", ("L", "RGB", "RGBA")) | ||||
| def test_write(mode: str, tmp_path: Path) -> None: | ||||
|     roundtrip(hopper(mode), tmp_path) | ||||
| 
 | ||||
| 
 | ||||
| def test_write_L_mode_1_dimension(tmp_path: Path) -> None: | ||||
|     roundtrip(Image.new("L", (10, 1)), tmp_path) | ||||
| 
 | ||||
| 
 | ||||
| def test_write16(tmp_path: Path) -> None: | ||||
|  |  | |||
|  | @ -96,6 +96,7 @@ def test_tell() -> None: | |||
| 
 | ||||
| def test_n_frames() -> None: | ||||
|     with Image.open(TEST_FILE) as im: | ||||
|         assert isinstance(im, SpiderImagePlugin.SpiderImageFile) | ||||
|         assert im.n_frames == 1 | ||||
|         assert not im.is_animated | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,8 +1,6 @@ | |||
| from __future__ import annotations | ||||
| 
 | ||||
| import os | ||||
| from glob import glob | ||||
| from itertools import product | ||||
| from pathlib import Path | ||||
| 
 | ||||
| import pytest | ||||
|  | @ -15,14 +13,27 @@ _TGA_DIR = os.path.join("Tests", "images", "tga") | |||
| _TGA_DIR_COMMON = os.path.join(_TGA_DIR, "common") | ||||
| 
 | ||||
| 
 | ||||
| _MODES = ("L", "LA", "P", "RGB", "RGBA") | ||||
| _ORIGINS = ("tl", "bl") | ||||
| 
 | ||||
| _ORIGIN_TO_ORIENTATION = {"tl": 1, "bl": -1} | ||||
| 
 | ||||
| 
 | ||||
| @pytest.mark.parametrize("mode", _MODES) | ||||
| def test_sanity(mode: str, tmp_path: Path) -> None: | ||||
| @pytest.mark.parametrize( | ||||
|     "size_mode", | ||||
|     ( | ||||
|         ("1x1", "L"), | ||||
|         ("200x32", "L"), | ||||
|         ("200x32", "LA"), | ||||
|         ("200x32", "P"), | ||||
|         ("200x32", "RGB"), | ||||
|         ("200x32", "RGBA"), | ||||
|     ), | ||||
| ) | ||||
| @pytest.mark.parametrize("origin", _ORIGINS) | ||||
| @pytest.mark.parametrize("rle", (True, False)) | ||||
| def test_sanity( | ||||
|     size_mode: tuple[str, str], origin: str, rle: str, tmp_path: Path | ||||
| ) -> None: | ||||
|     def roundtrip(original_im: Image.Image) -> None: | ||||
|         out = tmp_path / "temp.tga" | ||||
| 
 | ||||
|  | @ -36,27 +47,20 @@ def test_sanity(mode: str, tmp_path: Path) -> None: | |||
| 
 | ||||
|             assert_image_equal(saved_im, original_im) | ||||
| 
 | ||||
|     png_paths = glob(os.path.join(_TGA_DIR_COMMON, f"*x*_{mode.lower()}.png")) | ||||
| 
 | ||||
|     for png_path in png_paths: | ||||
|     size, mode = size_mode | ||||
|     png_path = os.path.join(_TGA_DIR_COMMON, size + "_" + mode.lower() + ".png") | ||||
|     with Image.open(png_path) as reference_im: | ||||
|         assert reference_im.mode == mode | ||||
| 
 | ||||
|         path_no_ext = os.path.splitext(png_path)[0] | ||||
|             for origin, rle in product(_ORIGINS, (True, False)): | ||||
|                 tga_path = "{}_{}_{}.tga".format( | ||||
|                     path_no_ext, origin, "rle" if rle else "raw" | ||||
|                 ) | ||||
|         tga_path = "{}_{}_{}.tga".format(path_no_ext, origin, "rle" if rle else "raw") | ||||
| 
 | ||||
|         with Image.open(tga_path) as original_im: | ||||
|             assert original_im.format == "TGA" | ||||
|             assert original_im.get_format_mimetype() == "image/x-tga" | ||||
|             if rle: | ||||
|                 assert original_im.info["compression"] == "tga_rle" | ||||
|                     assert ( | ||||
|                         original_im.info["orientation"] | ||||
|                         == _ORIGIN_TO_ORIENTATION[origin] | ||||
|                     ) | ||||
|             assert original_im.info["orientation"] == _ORIGIN_TO_ORIENTATION[origin] | ||||
|             if mode == "P": | ||||
|                 assert original_im.getpalette() == reference_im.getpalette() | ||||
| 
 | ||||
|  |  | |||
|  | @ -9,7 +9,13 @@ from types import ModuleType | |||
| 
 | ||||
| import pytest | ||||
| 
 | ||||
| from PIL import Image, ImageFile, TiffImagePlugin, UnidentifiedImageError | ||||
| from PIL import ( | ||||
|     Image, | ||||
|     ImageFile, | ||||
|     JpegImagePlugin, | ||||
|     TiffImagePlugin, | ||||
|     UnidentifiedImageError, | ||||
| ) | ||||
| from PIL.TiffImagePlugin import RESOLUTION_UNIT, X_RESOLUTION, Y_RESOLUTION | ||||
| 
 | ||||
| from .helper import ( | ||||
|  | @ -113,6 +119,7 @@ class TestFileTiff: | |||
| 
 | ||||
|         with Image.open("Tests/images/hopper_bigtiff.tif") as im: | ||||
|             outfile = tmp_path / "temp.tif" | ||||
|             assert isinstance(im, TiffImagePlugin.TiffImageFile) | ||||
|             im.save(outfile, save_all=True, append_images=[im], tiffinfo=im.tag_v2) | ||||
| 
 | ||||
|     def test_bigtiff_save(self, tmp_path: Path) -> None: | ||||
|  | @ -121,11 +128,13 @@ class TestFileTiff: | |||
|         im.save(outfile, big_tiff=True) | ||||
| 
 | ||||
|         with Image.open(outfile) as reloaded: | ||||
|             assert isinstance(reloaded, TiffImagePlugin.TiffImageFile) | ||||
|             assert reloaded.tag_v2._bigtiff is True | ||||
| 
 | ||||
|         im.save(outfile, save_all=True, append_images=[im], big_tiff=True) | ||||
| 
 | ||||
|         with Image.open(outfile) as reloaded: | ||||
|             assert isinstance(reloaded, TiffImagePlugin.TiffImageFile) | ||||
|             assert reloaded.tag_v2._bigtiff is True | ||||
| 
 | ||||
|     def test_seek_too_large(self) -> None: | ||||
|  | @ -140,6 +149,8 @@ class TestFileTiff: | |||
|     def test_xyres_tiff(self) -> None: | ||||
|         filename = "Tests/images/pil168.tif" | ||||
|         with Image.open(filename) as im: | ||||
|             assert isinstance(im, TiffImagePlugin.TiffImageFile) | ||||
| 
 | ||||
|             # legacy api | ||||
|             assert isinstance(im.tag[X_RESOLUTION][0], tuple) | ||||
|             assert isinstance(im.tag[Y_RESOLUTION][0], tuple) | ||||
|  | @ -153,6 +164,8 @@ class TestFileTiff: | |||
|     def test_xyres_fallback_tiff(self) -> None: | ||||
|         filename = "Tests/images/compression.tif" | ||||
|         with Image.open(filename) as im: | ||||
|             assert isinstance(im, TiffImagePlugin.TiffImageFile) | ||||
| 
 | ||||
|             # v2 api | ||||
|             assert isinstance(im.tag_v2[X_RESOLUTION], TiffImagePlugin.IFDRational) | ||||
|             assert isinstance(im.tag_v2[Y_RESOLUTION], TiffImagePlugin.IFDRational) | ||||
|  | @ -167,6 +180,8 @@ class TestFileTiff: | |||
|     def test_int_resolution(self) -> None: | ||||
|         filename = "Tests/images/pil168.tif" | ||||
|         with Image.open(filename) as im: | ||||
|             assert isinstance(im, TiffImagePlugin.TiffImageFile) | ||||
| 
 | ||||
|             # Try to read a file where X,Y_RESOLUTION are ints | ||||
|             im.tag_v2[X_RESOLUTION] = 71 | ||||
|             im.tag_v2[Y_RESOLUTION] = 71 | ||||
|  | @ -181,6 +196,7 @@ class TestFileTiff: | |||
|         with Image.open( | ||||
|             "Tests/images/hopper_float_dpi_" + str(resolution_unit) + ".tif" | ||||
|         ) as im: | ||||
|             assert isinstance(im, TiffImagePlugin.TiffImageFile) | ||||
|             assert im.tag_v2.get(RESOLUTION_UNIT) == resolution_unit | ||||
|             assert im.info["dpi"] == (dpi, dpi) | ||||
| 
 | ||||
|  | @ -198,6 +214,7 @@ class TestFileTiff: | |||
|         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 isinstance(im, TiffImagePlugin.TiffImageFile) | ||||
|             assert im.tag_v2[X_RESOLUTION] == 123.45 | ||||
|             assert im.tag_v2[Y_RESOLUTION] == 123.45 | ||||
| 
 | ||||
|  | @ -213,10 +230,12 @@ class TestFileTiff: | |||
|         TiffImagePlugin.PREFIXES.pop() | ||||
| 
 | ||||
|     def test_bad_exif(self) -> None: | ||||
|         with Image.open("Tests/images/hopper_bad_exif.jpg") as i: | ||||
|         with Image.open("Tests/images/hopper_bad_exif.jpg") as im: | ||||
|             assert isinstance(im, JpegImagePlugin.JpegImageFile) | ||||
| 
 | ||||
|             # Should not raise struct.error. | ||||
|             with pytest.warns(UserWarning): | ||||
|                 i._getexif() | ||||
|                 im._getexif() | ||||
| 
 | ||||
|     def test_save_rgba(self, tmp_path: Path) -> None: | ||||
|         im = hopper("RGBA") | ||||
|  | @ -307,11 +326,13 @@ class TestFileTiff: | |||
|     ) | ||||
|     def test_n_frames(self, path: str, n_frames: int) -> None: | ||||
|         with Image.open(path) as im: | ||||
|             assert isinstance(im, TiffImagePlugin.TiffImageFile) | ||||
|             assert im.n_frames == n_frames | ||||
|             assert im.is_animated == (n_frames != 1) | ||||
| 
 | ||||
|     def test_eoferror(self) -> None: | ||||
|         with Image.open("Tests/images/multipage-lastframe.tif") as im: | ||||
|             assert isinstance(im, TiffImagePlugin.TiffImageFile) | ||||
|             n_frames = im.n_frames | ||||
| 
 | ||||
|             # Test seeking past the last frame | ||||
|  | @ -355,19 +376,24 @@ class TestFileTiff: | |||
|     def test_frame_order(self) -> None: | ||||
|         # A frame can't progress to itself after reading | ||||
|         with Image.open("Tests/images/multipage_single_frame_loop.tiff") as im: | ||||
|             assert isinstance(im, TiffImagePlugin.TiffImageFile) | ||||
|             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 isinstance(im, TiffImagePlugin.TiffImageFile) | ||||
|             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 isinstance(im, TiffImagePlugin.TiffImageFile) | ||||
|             assert im.n_frames == 3 | ||||
| 
 | ||||
|     def test___str__(self) -> None: | ||||
|         filename = "Tests/images/pil136.tiff" | ||||
|         with Image.open(filename) as im: | ||||
|             assert isinstance(im, TiffImagePlugin.TiffImageFile) | ||||
| 
 | ||||
|             # Act | ||||
|             ret = str(im.ifd) | ||||
| 
 | ||||
|  | @ -378,6 +404,8 @@ class TestFileTiff: | |||
|         # Arrange | ||||
|         filename = "Tests/images/pil136.tiff" | ||||
|         with Image.open(filename) as im: | ||||
|             assert isinstance(im, TiffImagePlugin.TiffImageFile) | ||||
| 
 | ||||
|             # v2 interface | ||||
|             v2_tags = { | ||||
|                 256: 55, | ||||
|  | @ -417,6 +445,7 @@ class TestFileTiff: | |||
|     def test__delitem__(self) -> None: | ||||
|         filename = "Tests/images/pil136.tiff" | ||||
|         with Image.open(filename) as im: | ||||
|             assert isinstance(im, TiffImagePlugin.TiffImageFile) | ||||
|             len_before = len(dict(im.ifd)) | ||||
|             del im.ifd[256] | ||||
|             len_after = len(dict(im.ifd)) | ||||
|  | @ -449,6 +478,7 @@ class TestFileTiff: | |||
| 
 | ||||
|     def test_ifd_tag_type(self) -> None: | ||||
|         with Image.open("Tests/images/ifd_tag_type.tiff") as im: | ||||
|             assert isinstance(im, TiffImagePlugin.TiffImageFile) | ||||
|             assert 0x8825 in im.tag_v2 | ||||
| 
 | ||||
|     def test_exif(self, tmp_path: Path) -> None: | ||||
|  | @ -537,6 +567,7 @@ class TestFileTiff: | |||
|         im = hopper(mode) | ||||
|         im.save(filename, tiffinfo={262: 0}) | ||||
|         with Image.open(filename) as reloaded: | ||||
|             assert isinstance(reloaded, TiffImagePlugin.TiffImageFile) | ||||
|             assert reloaded.tag_v2[262] == 0 | ||||
|             assert_image_equal(im, reloaded) | ||||
| 
 | ||||
|  | @ -615,6 +646,8 @@ class TestFileTiff: | |||
|         filename = tmp_path / "temp.tif" | ||||
|         hopper("RGB").save(filename, "TIFF", **kwargs) | ||||
|         with Image.open(filename) as im: | ||||
|             assert isinstance(im, TiffImagePlugin.TiffImageFile) | ||||
| 
 | ||||
|             # legacy interface | ||||
|             assert im.tag[X_RESOLUTION][0][0] == 72 | ||||
|             assert im.tag[Y_RESOLUTION][0][0] == 36 | ||||
|  | @ -701,6 +734,7 @@ class TestFileTiff: | |||
|     def test_planar_configuration_save(self, tmp_path: Path) -> None: | ||||
|         infile = "Tests/images/tiff_tiled_planar_raw.tif" | ||||
|         with Image.open(infile) as im: | ||||
|             assert isinstance(im, TiffImagePlugin.TiffImageFile) | ||||
|             assert im._planar_configuration == 2 | ||||
| 
 | ||||
|             outfile = tmp_path / "temp.tif" | ||||
|  | @ -733,6 +767,7 @@ class TestFileTiff: | |||
| 
 | ||||
|         mp.seek(0, os.SEEK_SET) | ||||
|         with Image.open(mp) as im: | ||||
|             assert isinstance(im, TiffImagePlugin.TiffImageFile) | ||||
|             assert im.n_frames == 3 | ||||
| 
 | ||||
|         # Test appending images | ||||
|  | @ -743,6 +778,7 @@ class TestFileTiff: | |||
| 
 | ||||
|         mp.seek(0, os.SEEK_SET) | ||||
|         with Image.open(mp) as reread: | ||||
|             assert isinstance(reread, TiffImagePlugin.TiffImageFile) | ||||
|             assert reread.n_frames == 3 | ||||
| 
 | ||||
|         # Test appending using a generator | ||||
|  | @ -754,6 +790,7 @@ class TestFileTiff: | |||
| 
 | ||||
|         mp.seek(0, os.SEEK_SET) | ||||
|         with Image.open(mp) as reread: | ||||
|             assert isinstance(reread, TiffImagePlugin.TiffImageFile) | ||||
|             assert reread.n_frames == 3 | ||||
| 
 | ||||
|     def test_save_all_progress(self) -> None: | ||||
|  | @ -915,6 +952,7 @@ class TestFileTiff: | |||
| 
 | ||||
|     def test_get_photoshop_blocks(self) -> None: | ||||
|         with Image.open("Tests/images/lab.tif") as im: | ||||
|             assert isinstance(im, TiffImagePlugin.TiffImageFile) | ||||
|             assert list(im.get_photoshop_blocks().keys()) == [ | ||||
|                 1061, | ||||
|                 1002, | ||||
|  |  | |||
|  | @ -61,6 +61,7 @@ def test_rt_metadata(tmp_path: Path) -> None: | |||
|     img.save(f, tiffinfo=info) | ||||
| 
 | ||||
|     with Image.open(f) as loaded: | ||||
|         assert isinstance(loaded, TiffImagePlugin.TiffImageFile) | ||||
|         assert loaded.tag[ImageJMetaDataByteCounts] == (len(bin_data),) | ||||
|         assert loaded.tag_v2[ImageJMetaDataByteCounts] == (len(bin_data),) | ||||
| 
 | ||||
|  | @ -80,12 +81,14 @@ def test_rt_metadata(tmp_path: Path) -> None: | |||
|     info[ImageJMetaDataByteCounts] = (8, len(bin_data) - 8) | ||||
|     img.save(f, tiffinfo=info) | ||||
|     with Image.open(f) as loaded: | ||||
|         assert isinstance(loaded, TiffImagePlugin.TiffImageFile) | ||||
|         assert loaded.tag[ImageJMetaDataByteCounts] == (8, len(bin_data) - 8) | ||||
|         assert loaded.tag_v2[ImageJMetaDataByteCounts] == (8, len(bin_data) - 8) | ||||
| 
 | ||||
| 
 | ||||
| def test_read_metadata() -> None: | ||||
|     with Image.open("Tests/images/hopper_g4.tif") as img: | ||||
|         assert isinstance(img, TiffImagePlugin.TiffImageFile) | ||||
|         assert { | ||||
|             "YResolution": IFDRational(4294967295, 113653537), | ||||
|             "PlanarConfiguration": 1, | ||||
|  | @ -128,6 +131,7 @@ def test_read_metadata() -> None: | |||
| def test_write_metadata(tmp_path: Path) -> None: | ||||
|     """Test metadata writing through the python code""" | ||||
|     with Image.open("Tests/images/hopper.tif") as img: | ||||
|         assert isinstance(img, TiffImagePlugin.TiffImageFile) | ||||
|         f = tmp_path / "temp.tiff" | ||||
|         del img.tag[278] | ||||
|         img.save(f, tiffinfo=img.tag) | ||||
|  | @ -135,6 +139,7 @@ def test_write_metadata(tmp_path: Path) -> None: | |||
|         original = img.tag_v2.named() | ||||
| 
 | ||||
|     with Image.open(f) as loaded: | ||||
|         assert isinstance(loaded, TiffImagePlugin.TiffImageFile) | ||||
|         reloaded = loaded.tag_v2.named() | ||||
| 
 | ||||
|     ignored = ["StripByteCounts", "RowsPerStrip", "PageNumber", "StripOffsets"] | ||||
|  | @ -165,6 +170,7 @@ def test_write_metadata(tmp_path: Path) -> None: | |||
| def test_change_stripbytecounts_tag_type(tmp_path: Path) -> None: | ||||
|     out = tmp_path / "temp.tiff" | ||||
|     with Image.open("Tests/images/hopper.tif") as im: | ||||
|         assert isinstance(im, TiffImagePlugin.TiffImageFile) | ||||
|         info = im.tag_v2 | ||||
|         del info[278] | ||||
| 
 | ||||
|  | @ -178,6 +184,7 @@ def test_change_stripbytecounts_tag_type(tmp_path: Path) -> None: | |||
|         im.save(out, tiffinfo=info) | ||||
| 
 | ||||
|     with Image.open(out) as reloaded: | ||||
|         assert isinstance(reloaded, TiffImagePlugin.TiffImageFile) | ||||
|         assert reloaded.tag_v2.tagtype[TiffImagePlugin.STRIPBYTECOUNTS] == TiffTags.LONG | ||||
| 
 | ||||
| 
 | ||||
|  | @ -231,6 +238,7 @@ def test_writing_other_types_to_ascii( | |||
|     im.save(out, tiffinfo=info) | ||||
| 
 | ||||
|     with Image.open(out) as reloaded: | ||||
|         assert isinstance(reloaded, TiffImagePlugin.TiffImageFile) | ||||
|         assert reloaded.tag_v2[271] == expected | ||||
| 
 | ||||
| 
 | ||||
|  | @ -248,6 +256,7 @@ def test_writing_other_types_to_bytes(value: int | IFDRational, tmp_path: Path) | |||
|     im.save(out, tiffinfo=info) | ||||
| 
 | ||||
|     with Image.open(out) as reloaded: | ||||
|         assert isinstance(reloaded, TiffImagePlugin.TiffImageFile) | ||||
|         assert reloaded.tag_v2[700] == b"\x01" | ||||
| 
 | ||||
| 
 | ||||
|  | @ -267,6 +276,7 @@ def test_writing_other_types_to_undefined( | |||
|     im.save(out, tiffinfo=info) | ||||
| 
 | ||||
|     with Image.open(out) as reloaded: | ||||
|         assert isinstance(reloaded, TiffImagePlugin.TiffImageFile) | ||||
|         assert reloaded.tag_v2[33723] == b"1" | ||||
| 
 | ||||
| 
 | ||||
|  | @ -311,6 +321,7 @@ def test_iccprofile_binary() -> None: | |||
|     # but probably won't be able to save it. | ||||
| 
 | ||||
|     with Image.open("Tests/images/hopper.iccprofile_binary.tif") as im: | ||||
|         assert isinstance(im, TiffImagePlugin.TiffImageFile) | ||||
|         assert im.tag_v2.tagtype[34675] == 1 | ||||
|         assert im.info["icc_profile"] | ||||
| 
 | ||||
|  | @ -336,6 +347,7 @@ def test_exif_div_zero(tmp_path: Path) -> None: | |||
|     im.save(out, tiffinfo=info, compression="raw") | ||||
| 
 | ||||
|     with Image.open(out) as reloaded: | ||||
|         assert isinstance(reloaded, TiffImagePlugin.TiffImageFile) | ||||
|         assert 0 == reloaded.tag_v2[41988].numerator | ||||
|         assert 0 == reloaded.tag_v2[41988].denominator | ||||
| 
 | ||||
|  | @ -355,6 +367,7 @@ def test_ifd_unsigned_rational(tmp_path: Path) -> None: | |||
|     im.save(out, tiffinfo=info, compression="raw") | ||||
| 
 | ||||
|     with Image.open(out) as reloaded: | ||||
|         assert isinstance(reloaded, TiffImagePlugin.TiffImageFile) | ||||
|         assert max_long == reloaded.tag_v2[41493].numerator | ||||
|         assert 1 == reloaded.tag_v2[41493].denominator | ||||
| 
 | ||||
|  | @ -367,6 +380,7 @@ def test_ifd_unsigned_rational(tmp_path: Path) -> None: | |||
|     im.save(out, tiffinfo=info, compression="raw") | ||||
| 
 | ||||
|     with Image.open(out) as reloaded: | ||||
|         assert isinstance(reloaded, TiffImagePlugin.TiffImageFile) | ||||
|         assert max_long == reloaded.tag_v2[41493].numerator | ||||
|         assert 1 == reloaded.tag_v2[41493].denominator | ||||
| 
 | ||||
|  | @ -385,6 +399,7 @@ def test_ifd_signed_rational(tmp_path: Path) -> None: | |||
|     im.save(out, tiffinfo=info, compression="raw") | ||||
| 
 | ||||
|     with Image.open(out) as reloaded: | ||||
|         assert isinstance(reloaded, TiffImagePlugin.TiffImageFile) | ||||
|         assert numerator == reloaded.tag_v2[37380].numerator | ||||
|         assert denominator == reloaded.tag_v2[37380].denominator | ||||
| 
 | ||||
|  | @ -397,6 +412,7 @@ def test_ifd_signed_rational(tmp_path: Path) -> None: | |||
|     im.save(out, tiffinfo=info, compression="raw") | ||||
| 
 | ||||
|     with Image.open(out) as reloaded: | ||||
|         assert isinstance(reloaded, TiffImagePlugin.TiffImageFile) | ||||
|         assert numerator == reloaded.tag_v2[37380].numerator | ||||
|         assert denominator == reloaded.tag_v2[37380].denominator | ||||
| 
 | ||||
|  | @ -410,6 +426,7 @@ def test_ifd_signed_rational(tmp_path: Path) -> None: | |||
|     im.save(out, tiffinfo=info, compression="raw") | ||||
| 
 | ||||
|     with Image.open(out) as reloaded: | ||||
|         assert isinstance(reloaded, TiffImagePlugin.TiffImageFile) | ||||
|         assert 2**31 - 1 == reloaded.tag_v2[37380].numerator | ||||
|         assert -1 == reloaded.tag_v2[37380].denominator | ||||
| 
 | ||||
|  | @ -424,6 +441,7 @@ def test_ifd_signed_long(tmp_path: Path) -> None: | |||
|     im.save(out, tiffinfo=info, compression="raw") | ||||
| 
 | ||||
|     with Image.open(out) as reloaded: | ||||
|         assert isinstance(reloaded, TiffImagePlugin.TiffImageFile) | ||||
|         assert reloaded.tag_v2[37000] == -60000 | ||||
| 
 | ||||
| 
 | ||||
|  | @ -444,11 +462,13 @@ def test_empty_values() -> None: | |||
| 
 | ||||
| def test_photoshop_info(tmp_path: Path) -> None: | ||||
|     with Image.open("Tests/images/issue_2278.tif") as im: | ||||
|         assert isinstance(im, TiffImagePlugin.TiffImageFile) | ||||
|         assert len(im.tag_v2[34377]) == 70 | ||||
|         assert isinstance(im.tag_v2[34377], bytes) | ||||
|         out = tmp_path / "temp.tiff" | ||||
|         im.save(out) | ||||
|     with Image.open(out) as reloaded: | ||||
|         assert isinstance(reloaded, TiffImagePlugin.TiffImageFile) | ||||
|         assert len(reloaded.tag_v2[34377]) == 70 | ||||
|         assert isinstance(reloaded.tag_v2[34377], bytes) | ||||
| 
 | ||||
|  |  | |||
|  | @ -6,7 +6,7 @@ from pathlib import Path | |||
| import pytest | ||||
| from packaging.version import parse as parse_version | ||||
| 
 | ||||
| from PIL import Image, features | ||||
| from PIL import GifImagePlugin, Image, WebPImagePlugin, features | ||||
| 
 | ||||
| from .helper import ( | ||||
|     assert_image_equal, | ||||
|  | @ -22,10 +22,12 @@ def test_n_frames() -> None: | |||
|     """Ensure that WebP format sets n_frames and is_animated attributes correctly.""" | ||||
| 
 | ||||
|     with Image.open("Tests/images/hopper.webp") as im: | ||||
|         assert isinstance(im, WebPImagePlugin.WebPImageFile) | ||||
|         assert im.n_frames == 1 | ||||
|         assert not im.is_animated | ||||
| 
 | ||||
|     with Image.open("Tests/images/iss634.webp") as im: | ||||
|         assert isinstance(im, WebPImagePlugin.WebPImageFile) | ||||
|         assert im.n_frames == 42 | ||||
|         assert im.is_animated | ||||
| 
 | ||||
|  | @ -37,11 +39,13 @@ def test_write_animation_L(tmp_path: Path) -> None: | |||
|     """ | ||||
| 
 | ||||
|     with Image.open("Tests/images/iss634.gif") as orig: | ||||
|         assert isinstance(orig, GifImagePlugin.GifImageFile) | ||||
|         assert orig.n_frames > 1 | ||||
| 
 | ||||
|         temp_file = tmp_path / "temp.webp" | ||||
|         orig.save(temp_file, save_all=True) | ||||
|         with Image.open(temp_file) as im: | ||||
|             assert isinstance(im, WebPImagePlugin.WebPImageFile) | ||||
|             assert im.n_frames == orig.n_frames | ||||
| 
 | ||||
|             # Compare first and last frames to the original animated GIF | ||||
|  | @ -69,6 +73,7 @@ def test_write_animation_RGB(tmp_path: Path) -> None: | |||
| 
 | ||||
|     def check(temp_file: Path) -> None: | ||||
|         with Image.open(temp_file) as im: | ||||
|             assert isinstance(im, WebPImagePlugin.WebPImageFile) | ||||
|             assert im.n_frames == 2 | ||||
| 
 | ||||
|             # Compare first frame to original | ||||
|  | @ -127,6 +132,7 @@ def test_timestamp_and_duration(tmp_path: Path) -> None: | |||
|             ) | ||||
| 
 | ||||
|     with Image.open(temp_file) as im: | ||||
|         assert isinstance(im, WebPImagePlugin.WebPImageFile) | ||||
|         assert im.n_frames == 5 | ||||
|         assert im.is_animated | ||||
| 
 | ||||
|  | @ -170,6 +176,7 @@ def test_seeking(tmp_path: Path) -> None: | |||
|             ) | ||||
| 
 | ||||
|     with Image.open(temp_file) as im: | ||||
|         assert isinstance(im, WebPImagePlugin.WebPImageFile) | ||||
|         assert im.n_frames == 5 | ||||
|         assert im.is_animated | ||||
| 
 | ||||
|  |  | |||
|  | @ -6,7 +6,7 @@ from types import ModuleType | |||
| 
 | ||||
| import pytest | ||||
| 
 | ||||
| from PIL import Image | ||||
| from PIL import Image, WebPImagePlugin | ||||
| 
 | ||||
| from .helper import mark_if_feature_version, skip_unless_feature | ||||
| 
 | ||||
|  | @ -110,6 +110,7 @@ def test_read_no_exif() -> None: | |||
| 
 | ||||
|     test_buffer.seek(0) | ||||
|     with Image.open(test_buffer) as webp_image: | ||||
|         assert isinstance(webp_image, WebPImagePlugin.WebPImageFile) | ||||
|         assert not webp_image._getexif() | ||||
| 
 | ||||
| 
 | ||||
|  |  | |||
|  | @ -8,7 +8,7 @@ import pytest | |||
| 
 | ||||
| from PIL import Image, ImageFile, WmfImagePlugin | ||||
| 
 | ||||
| from .helper import assert_image_similar_tofile, hopper | ||||
| from .helper import assert_image_equal_tofile, assert_image_similar_tofile, hopper | ||||
| 
 | ||||
| 
 | ||||
| def test_load_raw() -> None: | ||||
|  | @ -44,6 +44,15 @@ def test_load_zero_inch() -> None: | |||
|             pass | ||||
| 
 | ||||
| 
 | ||||
| def test_render() -> None: | ||||
|     with open("Tests/images/drawing.emf", "rb") as fp: | ||||
|         data = fp.read() | ||||
|     b = BytesIO(data[:808] + b"\x00" + data[809:]) | ||||
|     with Image.open(b) as im: | ||||
|         if hasattr(Image.core, "drawwmf"): | ||||
|             assert_image_equal_tofile(im, "Tests/images/drawing.emf") | ||||
| 
 | ||||
| 
 | ||||
| def test_register_handler(tmp_path: Path) -> None: | ||||
|     class TestHandler(ImageFile.StubHandler): | ||||
|         methodCalled = False | ||||
|  | @ -80,6 +89,7 @@ def test_load_float_dpi() -> None: | |||
| 
 | ||||
| def test_load_set_dpi() -> None: | ||||
|     with Image.open("Tests/images/drawing.wmf") as im: | ||||
|         assert isinstance(im, WmfImagePlugin.WmfStubImageFile) | ||||
|         assert im.size == (82, 82) | ||||
| 
 | ||||
|         if hasattr(Image.core, "drawwmf"): | ||||
|  | @ -88,6 +98,22 @@ def test_load_set_dpi() -> None: | |||
| 
 | ||||
|             assert_image_similar_tofile(im, "Tests/images/drawing_wmf_ref_144.png", 2.1) | ||||
| 
 | ||||
|     with Image.open("Tests/images/drawing.emf") as im: | ||||
|         assert im.size == (1625, 1625) | ||||
| 
 | ||||
|         if not hasattr(Image.core, "drawwmf"): | ||||
|             return | ||||
|         assert isinstance(im, WmfImagePlugin.WmfStubImageFile) | ||||
|         im.load(im.info["dpi"]) | ||||
|         assert im.size == (1625, 1625) | ||||
| 
 | ||||
|     with Image.open("Tests/images/drawing.emf") as im: | ||||
|         assert isinstance(im, WmfImagePlugin.WmfStubImageFile) | ||||
|         im.load((72, 144)) | ||||
|         assert im.size == (82, 164) | ||||
| 
 | ||||
|         assert_image_equal_tofile(im, "Tests/images/drawing_emf_ref_72_144.png") | ||||
| 
 | ||||
| 
 | ||||
| @pytest.mark.parametrize("ext", (".wmf", ".emf")) | ||||
| def test_save(ext: str, tmp_path: Path) -> None: | ||||
|  |  | |||
|  | @ -30,6 +30,7 @@ def test_invalid_file() -> None: | |||
| def test_load_read() -> None: | ||||
|     # Arrange | ||||
|     with Image.open(TEST_FILE) as im: | ||||
|         assert isinstance(im, XpmImagePlugin.XpmImageFile) | ||||
|         dummy_bytes = 1 | ||||
| 
 | ||||
|         # Act | ||||
|  |  | |||
|  | @ -230,8 +230,8 @@ class TestImage: | |||
|                 assert_image_similar(im, reloaded, 20) | ||||
| 
 | ||||
|     def test_unknown_extension(self, tmp_path: Path) -> None: | ||||
|         im = hopper() | ||||
|         temp_file = tmp_path / "temp.unknown" | ||||
|         with hopper() as im: | ||||
|             with pytest.raises(ValueError): | ||||
|                 im.save(temp_file) | ||||
| 
 | ||||
|  | @ -258,6 +258,15 @@ class TestImage: | |||
|             assert im.readonly | ||||
|             im.save(temp_file) | ||||
| 
 | ||||
|     def test_save_without_changing_readonly(self, tmp_path: Path) -> None: | ||||
|         temp_file = tmp_path / "temp.bmp" | ||||
| 
 | ||||
|         with Image.open("Tests/images/rgb32bf-rgba.bmp") as im: | ||||
|             assert im.readonly | ||||
| 
 | ||||
|             im.save(temp_file) | ||||
|             assert im.readonly | ||||
| 
 | ||||
|     def test_dump(self, tmp_path: Path) -> None: | ||||
|         im = Image.new("L", (10, 10)) | ||||
|         im._dump(str(tmp_path / "temp_L.ppm")) | ||||
|  |  | |||
|  | @ -1704,7 +1704,7 @@ def test_discontiguous_corners_polygon() -> None: | |||
|         BLACK, | ||||
|     ) | ||||
|     expected = os.path.join(IMAGES_PATH, "discontiguous_corners_polygon.png") | ||||
|     assert_image_similar_tofile(img, expected, 1) | ||||
|     assert_image_equal_tofile(img, expected) | ||||
| 
 | ||||
| 
 | ||||
| def test_polygon2() -> None: | ||||
|  |  | |||
|  | @ -131,6 +131,26 @@ class TestImageFile: | |||
| 
 | ||||
|         assert_image_equal(im1, im2) | ||||
| 
 | ||||
|     def test_tile_size(self) -> None: | ||||
|         with open("Tests/images/hopper.tif", "rb") as im_fp: | ||||
|             data = im_fp.read() | ||||
| 
 | ||||
|         reads = [] | ||||
| 
 | ||||
|         class FP(BytesIO): | ||||
|             def read(self, size: int | None = None) -> bytes: | ||||
|                 reads.append(size) | ||||
|                 return super().read(size) | ||||
| 
 | ||||
|         fp = FP(data) | ||||
|         with Image.open(fp) as im: | ||||
|             assert len(im.tile) == 7 | ||||
| 
 | ||||
|             im.load() | ||||
| 
 | ||||
|         # Despite multiple tiles, assert only one tile caused a read of maxblock size | ||||
|         assert reads.count(im.decodermaxblock) == 1 | ||||
| 
 | ||||
|     def test_raise_oserror(self) -> None: | ||||
|         with pytest.warns(DeprecationWarning): | ||||
|             with pytest.raises(OSError): | ||||
|  |  | |||
|  | @ -40,8 +40,11 @@ class TestImageGrab: | |||
| 
 | ||||
|     @pytest.mark.skipif(Image.core.HAVE_XCB, reason="tests missing XCB") | ||||
|     def test_grab_no_xcb(self) -> None: | ||||
|         if sys.platform not in ("win32", "darwin") and not shutil.which( | ||||
|             "gnome-screenshot" | ||||
|         if ( | ||||
|             sys.platform not in ("win32", "darwin") | ||||
|             and not shutil.which("gnome-screenshot") | ||||
|             and not shutil.which("grim") | ||||
|             and not shutil.which("spectacle") | ||||
|         ): | ||||
|             with pytest.raises(OSError) as e: | ||||
|                 ImageGrab.grab() | ||||
|  | @ -57,6 +60,13 @@ class TestImageGrab: | |||
|             ImageGrab.grab(xdisplay="error.test:0.0") | ||||
|         assert str(e.value).startswith("X connection failed") | ||||
| 
 | ||||
|     @pytest.mark.skipif(sys.platform != "win32", reason="Windows only") | ||||
|     def test_grab_invalid_handle(self) -> None: | ||||
|         with pytest.raises(OSError, match="unable to get device context for handle"): | ||||
|             ImageGrab.grab(window=-1) | ||||
|         with pytest.raises(OSError, match="screen grab failed"): | ||||
|             ImageGrab.grab(window=0) | ||||
| 
 | ||||
|     def test_grabclipboard(self) -> None: | ||||
|         if sys.platform == "darwin": | ||||
|             subprocess.call(["screencapture", "-cx"]) | ||||
|  |  | |||
|  | @ -4,7 +4,7 @@ from pathlib import Path | |||
| 
 | ||||
| import pytest | ||||
| 
 | ||||
| from PIL import Image, ImageSequence, TiffImagePlugin | ||||
| from PIL import Image, ImageSequence, PsdImagePlugin, TiffImagePlugin | ||||
| 
 | ||||
| from .helper import assert_image_equal, hopper, skip_unless_feature | ||||
| 
 | ||||
|  | @ -31,6 +31,7 @@ def test_sanity(tmp_path: Path) -> None: | |||
| 
 | ||||
| def test_iterator() -> None: | ||||
|     with Image.open("Tests/images/multipage.tiff") as im: | ||||
|         assert isinstance(im, TiffImagePlugin.TiffImageFile) | ||||
|         i = ImageSequence.Iterator(im) | ||||
|         for index in range(im.n_frames): | ||||
|             assert i[index] == next(i) | ||||
|  | @ -42,6 +43,7 @@ def test_iterator() -> None: | |||
| 
 | ||||
| def test_iterator_min_frame() -> None: | ||||
|     with Image.open("Tests/images/hopper.psd") as im: | ||||
|         assert isinstance(im, PsdImagePlugin.PsdImageFile) | ||||
|         i = ImageSequence.Iterator(im) | ||||
|         for index in range(1, im.n_frames): | ||||
|             assert i[index] == next(i) | ||||
|  |  | |||
|  | @ -81,6 +81,7 @@ def test_pickle_jpeg() -> None: | |||
|         unpickled_image = pickle.loads(pickle.dumps(image)) | ||||
| 
 | ||||
|     # Assert | ||||
|     assert unpickled_image.filename == "Tests/images/hopper.jpg" | ||||
|     assert len(unpickled_image.layer) == 3 | ||||
|     assert unpickled_image.layers == 3 | ||||
| 
 | ||||
|  |  | |||
							
								
								
									
										112
									
								
								Tests/test_pyarrow.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										112
									
								
								Tests/test_pyarrow.py
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,112 @@ | |||
| from __future__ import annotations | ||||
| 
 | ||||
| from typing import Any  # undone | ||||
| 
 | ||||
| import pytest | ||||
| 
 | ||||
| from PIL import Image | ||||
| 
 | ||||
| from .helper import ( | ||||
|     assert_deep_equal, | ||||
|     assert_image_equal, | ||||
|     hopper, | ||||
| ) | ||||
| 
 | ||||
| pyarrow = pytest.importorskip("pyarrow", reason="PyArrow not installed") | ||||
| 
 | ||||
| TEST_IMAGE_SIZE = (10, 10) | ||||
| 
 | ||||
| 
 | ||||
| def _test_img_equals_pyarray( | ||||
|     img: Image.Image, arr: Any, mask: list[int] | None | ||||
| ) -> None: | ||||
|     assert img.height * img.width == len(arr) | ||||
|     px = img.load() | ||||
|     assert px is not None | ||||
|     for x in range(0, img.size[0], int(img.size[0] / 10)): | ||||
|         for y in range(0, img.size[1], int(img.size[1] / 10)): | ||||
|             if mask: | ||||
|                 for ix, elt in enumerate(mask): | ||||
|                     pixel = px[x, y] | ||||
|                     assert isinstance(pixel, tuple) | ||||
|                     assert pixel[ix] == arr[y * img.width + x].as_py()[elt] | ||||
|             else: | ||||
|                 assert_deep_equal(px[x, y], arr[y * img.width + x].as_py()) | ||||
| 
 | ||||
| 
 | ||||
| # really hard to get a non-nullable list type | ||||
| fl_uint8_4_type = pyarrow.field( | ||||
|     "_", pyarrow.list_(pyarrow.field("_", pyarrow.uint8()).with_nullable(False), 4) | ||||
| ).type | ||||
| 
 | ||||
| 
 | ||||
| @pytest.mark.parametrize( | ||||
|     "mode, dtype, mask", | ||||
|     ( | ||||
|         ("L", pyarrow.uint8(), None), | ||||
|         ("I", pyarrow.int32(), None), | ||||
|         ("F", pyarrow.float32(), None), | ||||
|         ("LA", fl_uint8_4_type, [0, 3]), | ||||
|         ("RGB", fl_uint8_4_type, [0, 1, 2]), | ||||
|         ("RGBA", fl_uint8_4_type, None), | ||||
|         ("RGBX", fl_uint8_4_type, None), | ||||
|         ("CMYK", fl_uint8_4_type, None), | ||||
|         ("YCbCr", fl_uint8_4_type, [0, 1, 2]), | ||||
|         ("HSV", fl_uint8_4_type, [0, 1, 2]), | ||||
|     ), | ||||
| ) | ||||
| def test_to_array(mode: str, dtype: Any, mask: list[int] | None) -> None: | ||||
|     img = hopper(mode) | ||||
| 
 | ||||
|     # Resize to non-square | ||||
|     img = img.crop((3, 0, 124, 127)) | ||||
|     assert img.size == (121, 127) | ||||
| 
 | ||||
|     arr = pyarrow.array(img) | ||||
|     _test_img_equals_pyarray(img, arr, mask) | ||||
|     assert arr.type == dtype | ||||
| 
 | ||||
|     reloaded = Image.fromarrow(arr, mode, img.size) | ||||
| 
 | ||||
|     assert reloaded | ||||
| 
 | ||||
|     assert_image_equal(img, reloaded) | ||||
| 
 | ||||
| 
 | ||||
| def test_lifetime() -> None: | ||||
|     # valgrind shouldn't error out here. | ||||
|     # arrays should be accessible after the image is deleted. | ||||
| 
 | ||||
|     img = hopper("L") | ||||
| 
 | ||||
|     arr_1 = pyarrow.array(img) | ||||
|     arr_2 = pyarrow.array(img) | ||||
| 
 | ||||
|     del img | ||||
| 
 | ||||
|     assert arr_1.sum().as_py() > 0 | ||||
|     del arr_1 | ||||
| 
 | ||||
|     assert arr_2.sum().as_py() > 0 | ||||
|     del arr_2 | ||||
| 
 | ||||
| 
 | ||||
| def test_lifetime2() -> None: | ||||
|     # valgrind shouldn't error out here. | ||||
|     # img should remain after the arrays are collected. | ||||
| 
 | ||||
|     img = hopper("L") | ||||
| 
 | ||||
|     arr_1 = pyarrow.array(img) | ||||
|     arr_2 = pyarrow.array(img) | ||||
| 
 | ||||
|     assert arr_1.sum().as_py() > 0 | ||||
|     del arr_1 | ||||
| 
 | ||||
|     assert arr_2.sum().as_py() > 0 | ||||
|     del arr_2 | ||||
| 
 | ||||
|     img2 = img.copy() | ||||
|     px = img2.load() | ||||
|     assert px  # make mypy happy | ||||
|     assert isinstance(px[0, 0], int) | ||||
|  | @ -23,5 +23,11 @@ def test_pyroma() -> None: | |||
|         ) | ||||
| 
 | ||||
|     else: | ||||
|         # Should have a perfect score | ||||
|         assert rating == (10, []) | ||||
|         # Should have a perfect score, but pyroma does not support PEP 639 yet. | ||||
|         assert rating == ( | ||||
|             9, | ||||
|             [ | ||||
|                 "Your package does neither have a license field " | ||||
|                 "nor any license classifiers." | ||||
|             ], | ||||
|         ) | ||||
|  |  | |||
|  | @ -39,6 +39,7 @@ class TestShellInjection: | |||
|             shutil.copy(TEST_JPG, src_file) | ||||
| 
 | ||||
|             with Image.open(src_file) as im: | ||||
|                 assert isinstance(im, JpegImagePlugin.JpegImageFile) | ||||
|                 im.load_djpeg() | ||||
| 
 | ||||
|     @pytest.mark.skipif(not cjpeg_available(), reason="cjpeg not available") | ||||
|  |  | |||
|  | @ -72,4 +72,5 @@ def test_ifd_rational_save( | |||
|     im.save(out, dpi=(res, res), compression="raw") | ||||
| 
 | ||||
|     with Image.open(out) as reloaded: | ||||
|         assert isinstance(reloaded, TiffImagePlugin.TiffImageFile) | ||||
|         assert float(IFDRational(301, 1)) == float(reloaded.tag_v2[282]) | ||||
|  |  | |||
							
								
								
									
										64
									
								
								depends/install_libavif.sh
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										64
									
								
								depends/install_libavif.sh
									
									
									
									
									
										Executable file
									
								
							|  | @ -0,0 +1,64 @@ | |||
| #!/usr/bin/env bash | ||||
| set -eo pipefail | ||||
| 
 | ||||
| version=1.2.1 | ||||
| 
 | ||||
| ./download-and-extract.sh libavif-$version https://github.com/AOMediaCodec/libavif/archive/refs/tags/v$version.tar.gz | ||||
| 
 | ||||
| pushd libavif-$version | ||||
| 
 | ||||
| if [ $(uname) == "Darwin" ] && [ -x "$(command -v brew)" ]; then | ||||
|     PREFIX=$(brew --prefix) | ||||
| else | ||||
|     PREFIX=/usr | ||||
| fi | ||||
| 
 | ||||
| PKGCONFIG=${PKGCONFIG:-pkg-config} | ||||
| 
 | ||||
| LIBAVIF_CMAKE_FLAGS=() | ||||
| HAS_DECODER=0 | ||||
| HAS_ENCODER=0 | ||||
| 
 | ||||
| if $PKGCONFIG --exists aom; then | ||||
|     LIBAVIF_CMAKE_FLAGS+=(-DAVIF_CODEC_AOM=SYSTEM) | ||||
|     HAS_ENCODER=1 | ||||
|     HAS_DECODER=1 | ||||
| fi | ||||
| 
 | ||||
| if $PKGCONFIG --exists dav1d; then | ||||
|     LIBAVIF_CMAKE_FLAGS+=(-DAVIF_CODEC_DAV1D=SYSTEM) | ||||
|     HAS_DECODER=1 | ||||
| fi | ||||
| 
 | ||||
| if $PKGCONFIG --exists libgav1; then | ||||
|     LIBAVIF_CMAKE_FLAGS+=(-DAVIF_CODEC_LIBGAV1=SYSTEM) | ||||
|     HAS_DECODER=1 | ||||
| fi | ||||
| 
 | ||||
| if $PKGCONFIG --exists rav1e; then | ||||
|     LIBAVIF_CMAKE_FLAGS+=(-DAVIF_CODEC_RAV1E=SYSTEM) | ||||
|     HAS_ENCODER=1 | ||||
| fi | ||||
| 
 | ||||
| if $PKGCONFIG --exists SvtAv1Enc; then | ||||
|     LIBAVIF_CMAKE_FLAGS+=(-DAVIF_CODEC_SVT=SYSTEM) | ||||
|     HAS_ENCODER=1 | ||||
| fi | ||||
| 
 | ||||
| if [ "$HAS_ENCODER" != 1 ] || [ "$HAS_DECODER" != 1 ]; then | ||||
|     LIBAVIF_CMAKE_FLAGS+=(-DAVIF_CODEC_AOM=LOCAL) | ||||
| fi | ||||
| 
 | ||||
| cmake \ | ||||
|     -DCMAKE_INSTALL_PREFIX=$PREFIX \ | ||||
|     -DCMAKE_INSTALL_NAME_DIR=$PREFIX/lib \ | ||||
|     -DCMAKE_BUILD_TYPE=Release \ | ||||
|     -DCMAKE_MACOSX_RPATH=OFF \ | ||||
|     -DAVIF_LIBSHARPYUV=LOCAL \ | ||||
|     -DAVIF_LIBYUV=LOCAL \ | ||||
|     "${LIBAVIF_CMAKE_FLAGS[@]}" \ | ||||
|     . | ||||
| 
 | ||||
| sudo make install | ||||
| 
 | ||||
| popd | ||||
|  | @ -1,10 +0,0 @@ | |||
| #!/usr/bin/env python3 | ||||
| from __future__ import annotations | ||||
| 
 | ||||
| from livereload.compiler import shell | ||||
| from livereload.task import Task | ||||
| 
 | ||||
| Task.add("*.rst", shell("make html")) | ||||
| Task.add("*/*.rst", shell("make html")) | ||||
| Task.add("Makefile", shell("make html")) | ||||
| Task.add("conf.py", shell("make html")) | ||||
|  | @ -20,8 +20,8 @@ help: | |||
| 	@echo "Please use \`make <target>' where <target> is one of" | ||||
| 	@echo "  html       to make standalone HTML files" | ||||
| 	@echo "  htmlview   to open the index page built by the html target in your browser" | ||||
| 	@echo "  htmllive   to rebuild and reload HTML files in your browser" | ||||
| 	@echo "  serve      to start a local server for viewing docs" | ||||
| 	@echo "  livehtml   to start a local server for viewing docs and auto-reload on change" | ||||
| 	@echo "  dirhtml    to make HTML files named index.html in directories" | ||||
| 	@echo "  singlehtml to make a single large HTML file" | ||||
| 	@echo "  pickle     to make pickle files" | ||||
|  | @ -201,9 +201,10 @@ doctest: | |||
| htmlview: html | ||||
| 	$(PYTHON) -c "import os, webbrowser; webbrowser.open('file://' + os.path.realpath('$(BUILDDIR)/html/index.html'))" | ||||
| 
 | ||||
| .PHONY: livehtml | ||||
| livehtml: html | ||||
| 	livereload $(BUILDDIR)/html -p 33233 | ||||
| .PHONY: htmllive | ||||
| htmllive: SPHINXBUILD = $(PYTHON) -m sphinx_autobuild | ||||
| htmllive: SPHINXOPTS = --open-browser --delay 0 | ||||
| htmllive: html | ||||
| 
 | ||||
| .PHONY: serve | ||||
| serve: | ||||
|  |  | |||
|  | @ -186,7 +186,7 @@ ExifTags.IFD.Makernote | |||
| Image.Image.get_child_images() | ||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | ||||
| 
 | ||||
| .. deprecated:: 11.2.0 | ||||
| .. deprecated:: 11.2.1 | ||||
| 
 | ||||
| ``Image.Image.get_child_images()`` has been deprecated. and will be removed in Pillow | ||||
| 13 (2026-10-15). It will be moved to ``ImageFile.ImageFile.get_child_images()``. The | ||||
|  |  | |||
|  | @ -30,35 +30,35 @@ image. Each pixel uses the full range of the bit depth. So a 1-bit pixel has a r | |||
| INT32 and a 32-bit floating point pixel has the range of FLOAT32. The current release | ||||
| supports the following standard modes: | ||||
| 
 | ||||
|     * ``1`` (1-bit pixels, black and white, stored with one pixel per byte) | ||||
|     * ``L`` (8-bit pixels, grayscale) | ||||
|     * ``P`` (8-bit pixels, mapped to any other mode using a color palette) | ||||
|     * ``RGB`` (3x8-bit pixels, true color) | ||||
|     * ``RGBA`` (4x8-bit pixels, true color with transparency mask) | ||||
|     * ``CMYK`` (4x8-bit pixels, color separation) | ||||
|     * ``YCbCr`` (3x8-bit pixels, color video format) | ||||
| * ``1`` (1-bit pixels, black and white, stored with one pixel per byte) | ||||
| * ``L`` (8-bit pixels, grayscale) | ||||
| * ``P`` (8-bit pixels, mapped to any other mode using a color palette) | ||||
| * ``RGB`` (3x8-bit pixels, true color) | ||||
| * ``RGBA`` (4x8-bit pixels, true color with transparency mask) | ||||
| * ``CMYK`` (4x8-bit pixels, color separation) | ||||
| * ``YCbCr`` (3x8-bit pixels, color video format) | ||||
| 
 | ||||
|   * Note that this refers to the JPEG, and not the ITU-R BT.2020, standard | ||||
| 
 | ||||
|     * ``LAB`` (3x8-bit pixels, the L*a*b color space) | ||||
|     * ``HSV`` (3x8-bit pixels, Hue, Saturation, Value color space) | ||||
| * ``LAB`` (3x8-bit pixels, the L*a*b color space) | ||||
| * ``HSV`` (3x8-bit pixels, Hue, Saturation, Value color space) | ||||
| 
 | ||||
|   * Hue's range of 0-255 is a scaled version of 0 degrees <= Hue < 360 degrees | ||||
| 
 | ||||
|     * ``I`` (32-bit signed integer pixels) | ||||
|     * ``F`` (32-bit floating point pixels) | ||||
| * ``I`` (32-bit signed integer pixels) | ||||
| * ``F`` (32-bit floating point pixels) | ||||
| 
 | ||||
| Pillow also provides limited support for a few additional modes, including: | ||||
| 
 | ||||
|     * ``LA`` (L with alpha) | ||||
|     * ``PA`` (P with alpha) | ||||
|     * ``RGBX`` (true color with padding) | ||||
|     * ``RGBa`` (true color with premultiplied alpha) | ||||
|     * ``La`` (L with premultiplied alpha) | ||||
|     * ``I;16`` (16-bit unsigned integer pixels) | ||||
|     * ``I;16L`` (16-bit little endian unsigned integer pixels) | ||||
|     * ``I;16B`` (16-bit big endian unsigned integer pixels) | ||||
|     * ``I;16N`` (16-bit native endian unsigned integer pixels) | ||||
| * ``LA`` (L with alpha) | ||||
| * ``PA`` (P with alpha) | ||||
| * ``RGBX`` (true color with padding) | ||||
| * ``RGBa`` (true color with premultiplied alpha) | ||||
| * ``La`` (L with premultiplied alpha) | ||||
| * ``I;16`` (16-bit unsigned integer pixels) | ||||
| * ``I;16L`` (16-bit little endian unsigned integer pixels) | ||||
| * ``I;16B`` (16-bit big endian unsigned integer pixels) | ||||
| * ``I;16N`` (16-bit native endian unsigned integer pixels) | ||||
| 
 | ||||
| Premultiplied alpha is where the values for each other channel have been | ||||
| multiplied by the alpha. For example, an RGBA pixel of ``(10, 20, 30, 127)`` | ||||
|  |  | |||
|  | @ -24,6 +24,83 @@ present, and the :py:attr:`~PIL.Image.Image.format` attribute will be ``None``. | |||
| Fully supported formats | ||||
| ----------------------- | ||||
| 
 | ||||
| AVIF | ||||
| ^^^^ | ||||
| 
 | ||||
| Pillow reads and writes AVIF files, including AVIF sequence images. | ||||
| It is only possible to save 8-bit AVIF images, and all AVIF images are decoded | ||||
| as 8-bit RGB(A). | ||||
| 
 | ||||
| The :py:meth:`~PIL.Image.Image.save` method supports the following options: | ||||
| 
 | ||||
| **quality** | ||||
|     Integer, 0-100, defaults to 75. 0 gives the smallest size and poorest | ||||
|     quality, 100 the largest size and best quality. | ||||
| 
 | ||||
| **subsampling** | ||||
|     If present, sets the subsampling for the encoder. Defaults to ``4:2:0``. | ||||
|     Options include: | ||||
| 
 | ||||
|     * ``4:0:0`` | ||||
|     * ``4:2:0`` | ||||
|     * ``4:2:2`` | ||||
|     * ``4:4:4`` | ||||
| 
 | ||||
| **speed** | ||||
|     Quality/speed trade-off (0=slower/better, 10=fastest). Defaults to 6. | ||||
| 
 | ||||
| **max_threads** | ||||
|     Limit the number of active threads used. By default, there is no limit. If the aom | ||||
|     codec is used, there is a maximum of 64. | ||||
| 
 | ||||
| **range** | ||||
|     YUV range, either "full" or "limited". Defaults to "full". | ||||
| 
 | ||||
| **codec** | ||||
|     AV1 codec to use for encoding. Specific values are "aom", "rav1e", and | ||||
|     "svt", presuming the chosen codec is available. Defaults to "auto", which | ||||
|     will choose the first available codec in the order of the preceding list. | ||||
| 
 | ||||
| **tile_rows** / **tile_cols** | ||||
|     For tile encoding, the (log 2) number of tile rows and columns to use. | ||||
|     Valid values are 0-6, default 0. Ignored if "autotiling" is set to true. | ||||
| 
 | ||||
| **autotiling** | ||||
|     Split the image up to allow parallelization. Enabled automatically if "tile_rows" | ||||
|     and "tile_cols" both have their default values of zero. | ||||
| 
 | ||||
| **alpha_premultiplied** | ||||
|     Encode the image with premultiplied alpha. Defaults to ``False``. | ||||
| 
 | ||||
| **advanced** | ||||
|     Codec specific options. | ||||
| 
 | ||||
| **icc_profile** | ||||
|     The ICC Profile to include in the saved file. | ||||
| 
 | ||||
| **exif** | ||||
|     The exif data to include in the saved file. | ||||
| 
 | ||||
| **xmp** | ||||
|     The XMP data to include in the saved file. | ||||
| 
 | ||||
| Saving sequences | ||||
| ~~~~~~~~~~~~~~~~ | ||||
| 
 | ||||
| When calling :py:meth:`~PIL.Image.Image.save` to write an AVIF file, by default | ||||
| only the first frame of a multiframe image will be saved. If the ``save_all`` | ||||
| argument is present and true, then all frames will be saved, and the following | ||||
| options will also be available. | ||||
| 
 | ||||
| **append_images** | ||||
|     A list of images to append as additional frames. Each of the | ||||
|     images in the list can be single or multiframe images. | ||||
| 
 | ||||
| **duration** | ||||
|     The display duration of each frame, in milliseconds. Pass a single | ||||
|     integer for a constant duration, or a list or tuple to set the | ||||
|     duration for each frame separately. | ||||
| 
 | ||||
| BLP | ||||
| ^^^ | ||||
| 
 | ||||
|  | @ -93,7 +170,7 @@ DXT1 and DXT5 pixel formats can be read, only in ``RGBA`` mode. | |||
|    in ``P`` mode. | ||||
| 
 | ||||
| 
 | ||||
| .. versionadded:: 11.2.0 | ||||
| .. versionadded:: 11.2.1 | ||||
|    DXT1, DXT3, DXT5, BC2, BC3 and BC5 pixel formats can be saved:: | ||||
| 
 | ||||
|        im.save(out, pixel_format="DXT1") | ||||
|  | @ -235,13 +312,14 @@ following options are available:: | |||
|     im.save(out, save_all=True, append_images=[im1, im2, ...]) | ||||
| 
 | ||||
| **save_all** | ||||
|     If present and true, all frames of the image will be saved. If | ||||
|     not, then only the first frame of a multiframe image will be saved. | ||||
|     If present and true, or if ``append_images`` is not empty, all frames of | ||||
|     the image will be saved. Otherwise, only the first frame of a multiframe | ||||
|     image will be saved. | ||||
| 
 | ||||
| **append_images** | ||||
|     A list of images to append as additional frames. Each of the | ||||
|     images in the list can be single or multiframe images. | ||||
|     This is currently supported for GIF, PDF, PNG, TIFF, and WebP. | ||||
|     This is supported for AVIF, GIF, PDF, PNG, TIFF and WebP. | ||||
| 
 | ||||
|     It is also supported for ICO and ICNS. If images are passed in of relevant | ||||
|     sizes, they will be used instead of scaling down the main image. | ||||
|  | @ -723,8 +801,8 @@ Saving | |||
| 
 | ||||
| When calling :py:meth:`~PIL.Image.Image.save` to write an MPO file, by default | ||||
| only the first frame of a multiframe image will be saved. If the ``save_all`` | ||||
| argument is present and true, then all frames will be saved, and the following | ||||
| option will also be available. | ||||
| argument is present and true, or if ``append_images`` is not empty, all frames | ||||
| will be saved. | ||||
| 
 | ||||
| **append_images** | ||||
|     A list of images to append as additional pictures. Each of the | ||||
|  | @ -934,7 +1012,8 @@ Saving | |||
| 
 | ||||
| When calling :py:meth:`~PIL.Image.Image.save`, by default only a single frame PNG file | ||||
| will be saved. To save an APNG file (including a single frame APNG), the ``save_all`` | ||||
| parameter must be set to ``True``. The following parameters can also be set: | ||||
| parameter should be set to ``True`` or ``append_images`` should not be empty. The | ||||
| following parameters can also be set: | ||||
| 
 | ||||
| **default_image** | ||||
|     Boolean value, specifying whether or not the base image is a default image. | ||||
|  | @ -1163,7 +1242,8 @@ Saving | |||
| The :py:meth:`~PIL.Image.Image.save` method can take the following keyword arguments: | ||||
| 
 | ||||
| **save_all** | ||||
|     If true, Pillow will save all frames of the image to a multiframe tiff document. | ||||
|     If true, or if ``append_images`` is not empty, Pillow will save all frames of the | ||||
|     image to a multiframe tiff document. | ||||
| 
 | ||||
|     .. versionadded:: 3.4.0 | ||||
| 
 | ||||
|  | @ -1313,8 +1393,8 @@ Saving sequences | |||
| 
 | ||||
| When calling :py:meth:`~PIL.Image.Image.save` to write a WebP file, by default | ||||
| only the first frame of a multiframe image will be saved. If the ``save_all`` | ||||
| argument is present and true, then all frames will be saved, and the following | ||||
| options will also be available. | ||||
| argument is present and true, or if ``append_images`` is not empty, all frames | ||||
| will be saved, and the following options will also be available. | ||||
| 
 | ||||
| **append_images** | ||||
|     A list of images to append as additional frames. Each of the | ||||
|  | @ -1584,6 +1664,11 @@ The :py:meth:`~PIL.Image.open` method sets the following | |||
|     Transparency color index. This key is omitted if the image is not | ||||
|     transparent. | ||||
| 
 | ||||
| XV Thumbnails | ||||
| ^^^^^^^^^^^^^ | ||||
| 
 | ||||
| Pillow can read XV thumbnail files. | ||||
| 
 | ||||
| Write-only formats | ||||
| ------------------ | ||||
| 
 | ||||
|  | @ -1616,15 +1701,14 @@ The :py:meth:`~PIL.Image.Image.save` method can take the following keyword argum | |||
| **save_all** | ||||
|     If a multiframe image is used, by default, only the first image will be saved. | ||||
|     To save all frames, each frame to a separate page of the PDF, the ``save_all`` | ||||
|     parameter must be present and set to ``True``. | ||||
|     parameter should be present and set to ``True`` or ``append_images`` should not be | ||||
|     empty. | ||||
| 
 | ||||
|     .. versionadded:: 3.0.0 | ||||
| 
 | ||||
| **append_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``. | ||||
|     of the images in the list can be single or multiframe images. | ||||
| 
 | ||||
|     .. versionadded:: 4.2.0 | ||||
| 
 | ||||
|  | @ -1690,11 +1774,6 @@ The :py:meth:`~PIL.Image.Image.save` method can take the following keyword argum | |||
| 
 | ||||
|     .. versionadded:: 5.3.0 | ||||
| 
 | ||||
| XV Thumbnails | ||||
| ^^^^^^^^^^^^^ | ||||
| 
 | ||||
| Pillow can read XV thumbnail files. | ||||
| 
 | ||||
| Identify-only formats | ||||
| --------------------- | ||||
| 
 | ||||
|  |  | |||
|  | @ -534,7 +534,6 @@ You can create animated GIFs with Pillow, e.g. | |||
|     # Save the images as an animated GIF | ||||
|     images[0].save( | ||||
|         "animated_hopper.gif", | ||||
|         save_all=True, | ||||
|         append_images=images[1:], | ||||
|         duration=500,  # duration of each frame in milliseconds | ||||
|         loop=0,  # loop forever | ||||
|  |  | |||
|  | @ -89,6 +89,14 @@ Many of Pillow's features require external libraries: | |||
| 
 | ||||
| * **libxcb** provides X11 screengrab support. | ||||
| 
 | ||||
| * **libavif** provides support for the AVIF format. | ||||
| 
 | ||||
|   * Pillow requires libavif version **1.0.0** or greater. | ||||
|   * libavif is merely an API that wraps AVIF codecs. If you are compiling | ||||
|     libavif from source, you will also need to install both an AVIF encoder | ||||
|     and decoder, such as rav1e and dav1d, or libaom, which both encodes and | ||||
|     decodes AVIF images. | ||||
| 
 | ||||
| .. tab:: Linux | ||||
| 
 | ||||
|     If you didn't build Python from source, make sure you have Python's | ||||
|  | @ -117,6 +125,12 @@ Many of Pillow's features require external libraries: | |||
|     To install libraqm, ``sudo apt-get install meson`` and then see | ||||
|     ``depends/install_raqm.sh``. | ||||
| 
 | ||||
|     Build prerequisites for libavif on Ubuntu are installed with:: | ||||
| 
 | ||||
|         sudo apt-get install cmake ninja-build nasm | ||||
| 
 | ||||
|     Then see ``depends/install_libavif.sh`` to build and install libavif. | ||||
| 
 | ||||
|     Prerequisites are installed on recent **Red Hat**, **CentOS** or **Fedora** with:: | ||||
| 
 | ||||
|         sudo dnf install libtiff-devel libjpeg-devel openjpeg2-devel zlib-devel \ | ||||
|  | @ -148,7 +162,15 @@ Many of Pillow's features require external libraries: | |||
|     The easiest way to install external libraries is via `Homebrew | ||||
|     <https://brew.sh/>`_. After you install Homebrew, run:: | ||||
| 
 | ||||
|         brew install libjpeg libraqm libtiff little-cms2 openjpeg webp | ||||
|         brew install libavif libjpeg libraqm libtiff little-cms2 openjpeg webp | ||||
| 
 | ||||
|     If you would like to use libavif with more codecs than just aom, then | ||||
|     instead of installing libavif through Homebrew directly, you can use | ||||
|     Homebrew to install libavif's build dependencies:: | ||||
| 
 | ||||
|         brew install aom dav1d rav1e svt-av1 | ||||
| 
 | ||||
|     Then see ``depends/install_libavif.sh`` to install libavif. | ||||
| 
 | ||||
| .. tab:: Windows | ||||
| 
 | ||||
|  | @ -187,7 +209,8 @@ Many of Pillow's features require external libraries: | |||
|             mingw-w64-x86_64-libwebp \ | ||||
|             mingw-w64-x86_64-openjpeg2 \ | ||||
|             mingw-w64-x86_64-libimagequant \ | ||||
|             mingw-w64-x86_64-libraqm | ||||
|             mingw-w64-x86_64-libraqm \ | ||||
|             mingw-w64-x86_64-libavif | ||||
| 
 | ||||
| .. tab:: FreeBSD | ||||
| 
 | ||||
|  | @ -199,7 +222,7 @@ Many of Pillow's features require external libraries: | |||
| 
 | ||||
|     Prerequisites are installed on **FreeBSD 10 or 11** with:: | ||||
| 
 | ||||
|         sudo pkg install jpeg-turbo tiff webp lcms2 freetype2 openjpeg harfbuzz fribidi libxcb | ||||
|         sudo pkg install jpeg-turbo tiff webp lcms2 freetype2 openjpeg harfbuzz fribidi libxcb libavif | ||||
| 
 | ||||
|     Then see ``depends/install_raqm_cmake.sh`` to install libraqm. | ||||
| 
 | ||||
|  | @ -261,14 +284,16 @@ Build Options | |||
| * Config settings: ``-C zlib=disable``, ``-C jpeg=disable``, | ||||
|   ``-C tiff=disable``, ``-C freetype=disable``, ``-C raqm=disable``, | ||||
|   ``-C lcms=disable``, ``-C webp=disable``, | ||||
|   ``-C jpeg2000=disable``, ``-C imagequant=disable``, ``-C xcb=disable``. | ||||
|   ``-C jpeg2000=disable``, ``-C imagequant=disable``, ``-C xcb=disable``, | ||||
|   ``-C avif=disable``. | ||||
|   Disable building the corresponding feature even if the development | ||||
|   libraries are present on the building machine. | ||||
| 
 | ||||
| * Config settings: ``-C zlib=enable``, ``-C jpeg=enable``, | ||||
|   ``-C tiff=enable``, ``-C freetype=enable``, ``-C raqm=enable``, | ||||
|   ``-C lcms=enable``, ``-C webp=enable``, | ||||
|   ``-C jpeg2000=enable``, ``-C imagequant=enable``, ``-C xcb=enable``. | ||||
|   ``-C jpeg2000=enable``, ``-C imagequant=enable``, ``-C xcb=enable``, | ||||
|   ``-C avif=enable``. | ||||
|   Require that the corresponding feature is built. The build will raise | ||||
|   an exception if the libraries are not found. Tcl and Tk must be used | ||||
|   together. | ||||
|  |  | |||
|  | @ -23,7 +23,7 @@ These platforms are built and tested for every change. | |||
| +----------------------------------+----------------------------+---------------------+ | ||||
| | Amazon Linux 2023                | 3.9                        | x86-64              | | ||||
| +----------------------------------+----------------------------+---------------------+ | ||||
| | Arch                             | 3.12                       | x86-64              | | ||||
| | Arch                             | 3.13                       | x86-64              | | ||||
| +----------------------------------+----------------------------+---------------------+ | ||||
| | CentOS Stream 9                  | 3.9                        | x86-64              | | ||||
| +----------------------------------+----------------------------+---------------------+ | ||||
|  | @ -31,10 +31,10 @@ These platforms are built and tested for every change. | |||
| +----------------------------------+----------------------------+---------------------+ | ||||
| | Debian 12 Bookworm               | 3.11                       | x86, x86-64         | | ||||
| +----------------------------------+----------------------------+---------------------+ | ||||
| | Fedora 40                        | 3.12                       | x86-64              | | ||||
| +----------------------------------+----------------------------+---------------------+ | ||||
| | Fedora 41                        | 3.13                       | x86-64              | | ||||
| +----------------------------------+----------------------------+---------------------+ | ||||
| | Fedora 42                        | 3.13                       | x86-64              | | ||||
| +----------------------------------+----------------------------+---------------------+ | ||||
| | Gentoo                           | 3.12                       | x86-64              | | ||||
| +----------------------------------+----------------------------+---------------------+ | ||||
| | macOS 13 Ventura                 | 3.9                        | x86-64              | | ||||
|  | @ -73,7 +73,7 @@ These platforms have been reported to work at the versions mentioned. | |||
| | Operating system                 | | Tested Python            | | Latest tested  | | Tested     | | ||||
| |                                  | | versions                 | | Pillow version | | processors | | ||||
| +==================================+============================+==================+==============+ | ||||
| | macOS 15 Sequoia                 | 3.9, 3.10, 3.11, 3.12, 3.13| 11.1.0           |arm           | | ||||
| | macOS 15 Sequoia                 | 3.9, 3.10, 3.11, 3.12, 3.13| 11.2.1           |arm           | | ||||
| |                                  +----------------------------+------------------+              | | ||||
| |                                  | 3.8                        | 10.4.0           |              | | ||||
| +----------------------------------+----------------------------+------------------+--------------+ | ||||
|  |  | |||
|  | @ -79,6 +79,7 @@ Constructing images | |||
| 
 | ||||
| .. autofunction:: new | ||||
| .. autofunction:: fromarray | ||||
| .. autofunction:: fromarrow | ||||
| .. autofunction:: frombytes | ||||
| .. autofunction:: frombuffer | ||||
| 
 | ||||
|  | @ -370,6 +371,8 @@ Protocols | |||
| 
 | ||||
| .. autoclass:: SupportsArrayInterface | ||||
|     :show-inheritance: | ||||
| .. autoclass:: SupportsArrowArrayInterface | ||||
|     :show-inheritance: | ||||
| .. autoclass:: SupportsGetData | ||||
|     :show-inheritance: | ||||
| 
 | ||||
|  |  | |||
|  | @ -286,6 +286,14 @@ can be easily displayed in a chromaticity diagram, for example). | |||
| 
 | ||||
|         The value is in the format ``((X, Y, Z), (x, y, Y))``, if available. | ||||
| 
 | ||||
|     .. py:attribute:: media_white_point | ||||
|         :type: tuple[tuple[float, float, float], tuple[float, float, float]] | None | ||||
| 
 | ||||
|         This tag specifies the media white point and is used for | ||||
|         generating absolute colorimetry. | ||||
| 
 | ||||
|         The value is in the format ``((X, Y, Z), (x, y, Y))``, if available. | ||||
| 
 | ||||
|     .. py:attribute:: media_white_point_temperature | ||||
|         :type: float | None | ||||
| 
 | ||||
|  |  | |||
|  | @ -391,7 +391,7 @@ Methods | |||
|                   the relative alignment of lines. Use the ``anchor`` parameter to | ||||
|                   specify the alignment to ``xy``. | ||||
| 
 | ||||
|                   .. versionadded:: 11.2.0 ``"justify"`` | ||||
|                   .. versionadded:: 11.2.1 ``"justify"`` | ||||
|     :param direction: Direction of the text. It can be ``"rtl"`` (right to | ||||
|                       left), ``"ltr"`` (left to right) or ``"ttb"`` (top to bottom). | ||||
|                       Requires libraqm. | ||||
|  | @ -462,7 +462,7 @@ Methods | |||
|                   the relative alignment of lines. Use the ``anchor`` parameter to | ||||
|                   specify the alignment to ``xy``. | ||||
| 
 | ||||
|                   .. versionadded:: 11.2.0 ``"justify"`` | ||||
|                   .. versionadded:: 11.2.1 ``"justify"`` | ||||
|     :param direction: Direction of the text. It can be ``"rtl"`` (right to | ||||
|                       left), ``"ltr"`` (left to right) or ``"ttb"`` (top to bottom). | ||||
|                       Requires libraqm. | ||||
|  | @ -609,7 +609,7 @@ Methods | |||
|                   the relative alignment of lines. Use the ``anchor`` parameter to | ||||
|                   specify the alignment to ``xy``. | ||||
| 
 | ||||
|                   .. versionadded:: 11.2.0 ``"justify"`` | ||||
|                   .. versionadded:: 11.2.1 ``"justify"`` | ||||
|     :param direction: Direction of the text. It can be ``"rtl"`` (right to | ||||
|                       left), ``"ltr"`` (left to right) or ``"ttb"`` (top to bottom). | ||||
|                       Requires libraqm. | ||||
|  | @ -663,7 +663,7 @@ Methods | |||
|                   the relative alignment of lines. Use the ``anchor`` parameter to | ||||
|                   specify the alignment to ``xy``. | ||||
| 
 | ||||
|                   .. versionadded:: 11.2.0 ``"justify"`` | ||||
|                   .. versionadded:: 11.2.1 ``"justify"`` | ||||
|     :param direction: Direction of the text. It can be ``"rtl"`` (right to | ||||
|                       left), ``"ltr"`` (left to right) or ``"ttb"`` (top to bottom). | ||||
|                       Requires libraqm. | ||||
|  |  | |||
|  | @ -9,15 +9,16 @@ or the clipboard to a PIL image memory. | |||
| 
 | ||||
| .. versionadded:: 1.1.3 | ||||
| 
 | ||||
| .. py:function:: grab(bbox=None, include_layered_windows=False, all_screens=False, xdisplay=None) | ||||
| .. py:function:: grab(bbox=None, include_layered_windows=False, all_screens=False, xdisplay=None, window=None) | ||||
| 
 | ||||
|     Take a snapshot of the screen. The pixels inside the bounding box are returned as | ||||
|     an "RGBA" on macOS, or an "RGB" image otherwise. If the bounding box is omitted, | ||||
|     the entire screen is copied, and on macOS, it will be at 2x if on a Retina screen. | ||||
| 
 | ||||
|     On Linux, if ``xdisplay`` is ``None`` and the default X11 display does not return | ||||
|     a snapshot of the screen, ``gnome-screenshot`` will be used as fallback if it is | ||||
|     installed. To disable this behaviour, pass ``xdisplay=""`` instead. | ||||
|     a snapshot of the screen, ``gnome-screenshot``, ``grim`` or ``spectacle`` will be | ||||
|     used as a fallback if they are installed. To disable this behaviour, pass | ||||
|     ``xdisplay=""`` instead. | ||||
| 
 | ||||
|     .. versionadded:: 1.1.3 (Windows), 3.0.0 (macOS), 7.1.0 (Linux) | ||||
| 
 | ||||
|  | @ -39,6 +40,11 @@ or the clipboard to a PIL image memory. | |||
|         You can check X11 support using :py:func:`PIL.features.check_feature` with ``feature="xcb"``. | ||||
| 
 | ||||
|         .. versionadded:: 7.1.0 | ||||
| 
 | ||||
|     :param window: | ||||
|         HWND, to capture a single window. Windows only. | ||||
| 
 | ||||
|         .. versionadded:: 11.2.1 | ||||
|     :return: An image | ||||
| 
 | ||||
| .. py:function:: grabclipboard() | ||||
|  |  | |||
							
								
								
									
										88
									
								
								docs/reference/arrow_support.rst
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										88
									
								
								docs/reference/arrow_support.rst
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,88 @@ | |||
| .. _arrow-support: | ||||
| 
 | ||||
| ============= | ||||
| Arrow Support | ||||
| ============= | ||||
| 
 | ||||
| `Arrow <https://arrow.apache.org/>`__ | ||||
| is an in-memory data exchange format that is the spiritual | ||||
| successor to the NumPy array interface. It provides for zero-copy | ||||
| access to columnar data, which in our case is ``Image`` data. | ||||
| 
 | ||||
| The goal with Arrow is to provide native zero-copy interoperability | ||||
| with any Arrow provider or consumer in the Python ecosystem. | ||||
| 
 | ||||
| .. warning:: Zero-copy does not mean zero allocation -- the internal | ||||
|   memory layout of Pillow images contains an allocation for row | ||||
|   pointers, so there is a non-zero, but significantly smaller than a | ||||
|   full-copy memory cost to reading an Arrow image. | ||||
| 
 | ||||
| 
 | ||||
| Data Formats | ||||
| ============ | ||||
| 
 | ||||
| Pillow currently supports exporting Arrow images in all modes | ||||
| **except** for ``BGR;15``, ``BGR;16`` and ``BGR;24``. This is due to | ||||
| line-length packing in these modes making for non-continuous memory. | ||||
| 
 | ||||
| For single-band images, the exported array is width*height elements, | ||||
| with each pixel corresponding to the appropriate Arrow type. | ||||
| 
 | ||||
| For multiband images, the exported array is width*height fixed-length | ||||
| four-element arrays of uint8. This is memory compatible with the raw | ||||
| image storage of four bytes per pixel. | ||||
| 
 | ||||
| Mode ``1`` images are exported as one uint8 byte/pixel, as this is | ||||
| consistent with the internal storage. | ||||
| 
 | ||||
| Pillow will accept, but not produce, one other format. For any | ||||
| multichannel image with 32-bit storage per pixel, Pillow will accept | ||||
| an array of width*height int32 elements, which will then be | ||||
| interpreted using the mode-specific interpretation of the bytes. | ||||
| 
 | ||||
| The image mode must match the Arrow band format when reading single | ||||
| channel images. | ||||
| 
 | ||||
| Memory Allocator | ||||
| ================ | ||||
| 
 | ||||
| Pillow's default memory allocator, the :ref:`block_allocator`, | ||||
| allocates up to a 16 MB block for images by default. Larger images | ||||
| overflow into additional blocks. Arrow requires a single continuous | ||||
| memory allocation, so images allocated in multiple blocks cannot be | ||||
| exported in the Arrow format. | ||||
| 
 | ||||
| To enable the single block allocator:: | ||||
| 
 | ||||
|   from PIL import Image | ||||
|   Image.core.set_use_block_allocator(1) | ||||
| 
 | ||||
| Note that this is a global setting, not a per-image setting. | ||||
| 
 | ||||
| Unsupported Features | ||||
| ==================== | ||||
| 
 | ||||
| * Table/dataframe protocol. We support a single array. | ||||
| * Null markers, producing or consuming. Null values are inferred from | ||||
|   the mode, e.g. RGB images are stored in the first three bytes of | ||||
|   each 32-bit pixel, and the last byte is an implied null. | ||||
| * Schema negotiation. There is an optional schema for the requested | ||||
|   datatype in the Arrow source interface. We ignore that | ||||
|   parameter. | ||||
| * Array metadata. | ||||
| 
 | ||||
| Internal Details | ||||
| ================ | ||||
| 
 | ||||
| Python Arrow C interface: | ||||
| https://arrow.apache.org/docs/format/CDataInterface/PyCapsuleInterface.html | ||||
| 
 | ||||
| The memory that is exported from the Arrow interface is shared -- not | ||||
| copied, so the lifetime of the memory allocation is no longer strictly | ||||
| tied to the life of the Python object. | ||||
| 
 | ||||
| The core imaging struct now has a refcount associated with it, and the | ||||
| lifetime of the core image struct is now divorced from the Python | ||||
| image object. Creating an arrow reference to the image increments the | ||||
| refcount, and the imaging struct is only released when the refcount | ||||
| reaches zero. | ||||
|  | @ -1,3 +1,6 @@ | |||
| 
 | ||||
| .. _block_allocator: | ||||
| 
 | ||||
| Block Allocator | ||||
| =============== | ||||
| 
 | ||||
|  | @ -34,14 +37,14 @@ fresh allocation. This caching of free blocks is currently disabled by | |||
| default, but can be enabled and tweaked using three environment | ||||
| variables: | ||||
| 
 | ||||
|   * ``PILLOW_ALIGNMENT``, in bytes. Specifies the alignment of memory | ||||
| * ``PILLOW_ALIGNMENT``, in bytes. Specifies the alignment of memory | ||||
|   allocations. Valid values are powers of 2 between 1 and | ||||
|   128, inclusive. Defaults to 1. | ||||
| 
 | ||||
|   * ``PILLOW_BLOCK_SIZE``, in bytes, K, or M.  Specifies the maximum | ||||
| * ``PILLOW_BLOCK_SIZE``, in bytes, K, or M.  Specifies the maximum | ||||
|   block size for ``ImagingAllocateArray``. Valid values are | ||||
|   integers, with an optional ``k`` or ``m`` suffix. Defaults to 16M. | ||||
| 
 | ||||
|   * ``PILLOW_BLOCKS_MAX`` Specifies the number of freed blocks to | ||||
| * ``PILLOW_BLOCKS_MAX`` Specifies the number of freed blocks to | ||||
|   retain to fill future memory requests. Any freed blocks over this | ||||
|   threshold will be returned to the OS immediately. Defaults to 0. | ||||
|  |  | |||
|  | @ -21,6 +21,7 @@ Support for the following modules can be checked: | |||
| * ``freetype2``: FreeType font support via :py:func:`PIL.ImageFont.truetype`. | ||||
| * ``littlecms2``: LittleCMS 2 support via :py:mod:`PIL.ImageCms`. | ||||
| * ``webp``: WebP image support. | ||||
| * ``avif``: AVIF image support. | ||||
| 
 | ||||
| .. autofunction:: PIL.features.check_module | ||||
| .. autofunction:: PIL.features.version_module | ||||
|  |  | |||
|  | @ -9,3 +9,4 @@ Internal Reference | |||
|   block_allocator | ||||
|   internal_modules | ||||
|   c_extension_debugging | ||||
|   arrow_support | ||||
|  |  | |||
|  | @ -1,6 +1,14 @@ | |||
| Plugin reference | ||||
| ================ | ||||
| 
 | ||||
| :mod:`~PIL.AvifImagePlugin` Module | ||||
| ---------------------------------- | ||||
| 
 | ||||
| .. automodule:: PIL.AvifImagePlugin | ||||
|     :members: | ||||
|     :undoc-members: | ||||
|     :show-inheritance: | ||||
| 
 | ||||
| :mod:`~PIL.BmpImagePlugin` Module | ||||
| --------------------------------- | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,83 +0,0 @@ | |||
| 11.2.0 | ||||
| ------ | ||||
| 
 | ||||
| Security | ||||
| ======== | ||||
| 
 | ||||
| TODO | ||||
| ^^^^ | ||||
| 
 | ||||
| TODO | ||||
| 
 | ||||
| :cve:`YYYY-XXXXX`: TODO | ||||
| ^^^^^^^^^^^^^^^^^^^^^^^ | ||||
| 
 | ||||
| TODO | ||||
| 
 | ||||
| Backwards Incompatible Changes | ||||
| ============================== | ||||
| 
 | ||||
| TODO | ||||
| ^^^^ | ||||
| 
 | ||||
| Deprecations | ||||
| ============ | ||||
| 
 | ||||
| Image.Image.get_child_images() | ||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | ||||
| 
 | ||||
| .. deprecated:: 11.2.0 | ||||
| 
 | ||||
| ``Image.Image.get_child_images()`` has been deprecated. and will be removed in Pillow | ||||
| 13 (2026-10-15). It will be moved to ``ImageFile.ImageFile.get_child_images()``. The | ||||
| method uses an image's file pointer, and so child images could only be retrieved from | ||||
| an :py:class:`PIL.ImageFile.ImageFile` instance. | ||||
| 
 | ||||
| API Changes | ||||
| =========== | ||||
| 
 | ||||
| TODO | ||||
| ^^^^ | ||||
| 
 | ||||
| TODO | ||||
| 
 | ||||
| API Additions | ||||
| ============= | ||||
| 
 | ||||
| ``"justify"`` multiline text alignment | ||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | ||||
| 
 | ||||
| In addition to ``"left"``, ``"center"`` and ``"right"``, multiline text can also be | ||||
| aligned using ``"justify"`` in :py:mod:`~PIL.ImageDraw`:: | ||||
| 
 | ||||
|     from PIL import Image, ImageDraw | ||||
|     im = Image.new("RGB", (50, 25)) | ||||
|     draw = ImageDraw.Draw(im) | ||||
|     draw.multiline_text((0, 0), "Multiline\ntext 1", align="justify") | ||||
|     draw.multiline_textbbox((0, 0), "Multiline\ntext 2", align="justify") | ||||
| 
 | ||||
| Check for MozJPEG | ||||
| ^^^^^^^^^^^^^^^^^ | ||||
| 
 | ||||
| You can check if Pillow has been built against the MozJPEG version of the | ||||
| libjpeg library, and what version of MozJPEG is being used:: | ||||
| 
 | ||||
|     from PIL import features | ||||
|     features.check_feature("mozjpeg")  # True or False | ||||
|     features.version_feature("mozjpeg")  # "4.1.1" for example, or None | ||||
| 
 | ||||
| Saving compressed DDS images | ||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | ||||
| 
 | ||||
| Compressed DDS images can now be saved using a ``pixel_format`` argument. DXT1, DXT3, | ||||
| DXT5, BC2, BC3 and BC5 are supported:: | ||||
| 
 | ||||
|     im.save("out.dds", pixel_format="DXT1") | ||||
| 
 | ||||
| Other Changes | ||||
| ============= | ||||
| 
 | ||||
| TODO | ||||
| ^^^^ | ||||
| 
 | ||||
| TODO | ||||
							
								
								
									
										118
									
								
								docs/releasenotes/11.2.1.rst
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										118
									
								
								docs/releasenotes/11.2.1.rst
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,118 @@ | |||
| 11.2.1 | ||||
| ------ | ||||
| 
 | ||||
| .. warning:: | ||||
| 
 | ||||
|    The release of Pillow *11.2.0* was halted prematurely, due to hitting PyPI's | ||||
|    project size limit and concern over the size of Pillow wheels containing libavif. | ||||
|    The PyPI limit has now been increased and Pillow *11.2.1* has been released | ||||
|    instead, without libavif included in the wheels. | ||||
|    To avoid confusion, the incomplete 11.2.0 release has been removed from PyPI. | ||||
| 
 | ||||
| Security | ||||
| ======== | ||||
| 
 | ||||
| Undefined shift when loading compressed DDS images | ||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | ||||
| 
 | ||||
| When loading some compressed DDS formats, an integer was bitshifted by 24 places to | ||||
| generate the 32 bits of the lookup table. This was undefined behaviour, and has been | ||||
| present since Pillow 3.4.0. | ||||
| 
 | ||||
| Deprecations | ||||
| ============ | ||||
| 
 | ||||
| Image.Image.get_child_images() | ||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | ||||
| 
 | ||||
| .. deprecated:: 11.2.1 | ||||
| 
 | ||||
| ``Image.Image.get_child_images()`` has been deprecated. and will be removed in Pillow | ||||
| 13 (2026-10-15). It will be moved to ``ImageFile.ImageFile.get_child_images()``. The | ||||
| method uses an image's file pointer, and so child images could only be retrieved from | ||||
| an :py:class:`PIL.ImageFile.ImageFile` instance. | ||||
| 
 | ||||
| API Changes | ||||
| =========== | ||||
| 
 | ||||
| ``append_images`` no longer requires ``save_all`` | ||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | ||||
| 
 | ||||
| Previously, ``save_all`` was required to in order to use ``append_images``. Now, | ||||
| ``save_all`` will default to ``True`` if ``append_images`` is not empty and the format | ||||
| supports saving multiple frames:: | ||||
| 
 | ||||
|     im.save("out.gif", append_images=ims) | ||||
| 
 | ||||
| API Additions | ||||
| ============= | ||||
| 
 | ||||
| ``"justify"`` multiline text alignment | ||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | ||||
| 
 | ||||
| In addition to ``"left"``, ``"center"`` and ``"right"``, multiline text can also be | ||||
| aligned using ``"justify"`` in :py:mod:`~PIL.ImageDraw`:: | ||||
| 
 | ||||
|     from PIL import Image, ImageDraw | ||||
|     im = Image.new("RGB", (50, 25)) | ||||
|     draw = ImageDraw.Draw(im) | ||||
|     draw.multiline_text((0, 0), "Multiline\ntext 1", align="justify") | ||||
|     draw.multiline_textbbox((0, 0), "Multiline\ntext 2", align="justify") | ||||
| 
 | ||||
| Specify window in ImageGrab on Windows | ||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | ||||
| 
 | ||||
| When using :py:meth:`~PIL.ImageGrab.grab`, a specific window can be selected using the | ||||
| HWND:: | ||||
| 
 | ||||
|     from PIL import ImageGrab | ||||
|     ImageGrab.grab(window=hwnd) | ||||
| 
 | ||||
| Check for MozJPEG | ||||
| ^^^^^^^^^^^^^^^^^ | ||||
| 
 | ||||
| You can check if Pillow has been built against the MozJPEG version of the | ||||
| libjpeg library, and what version of MozJPEG is being used:: | ||||
| 
 | ||||
|     from PIL import features | ||||
|     features.check_feature("mozjpeg")  # True or False | ||||
|     features.version_feature("mozjpeg")  # "4.1.1" for example, or None | ||||
| 
 | ||||
| Saving compressed DDS images | ||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | ||||
| 
 | ||||
| Compressed DDS images can now be saved using a ``pixel_format`` argument. DXT1, DXT3, | ||||
| DXT5, BC2, BC3 and BC5 are supported:: | ||||
| 
 | ||||
|     im.save("out.dds", pixel_format="DXT1") | ||||
| 
 | ||||
| Other Changes | ||||
| ============= | ||||
| 
 | ||||
| Arrow support | ||||
| ^^^^^^^^^^^^^ | ||||
| 
 | ||||
| `Arrow <https://arrow.apache.org/>`__ is an in-memory data exchange format that is the | ||||
| spiritual successor to the NumPy array interface. It provides for zero-copy access to | ||||
| columnar data, which in our case is ``Image`` data. | ||||
| 
 | ||||
| To create an image with zero-copy shared memory from an object exporting the | ||||
| arrow_c_array interface protocol:: | ||||
| 
 | ||||
|     from PIL import Image | ||||
|     import pyarrow as pa | ||||
|     arr = pa.array([0]*(5*5*4), type=pa.uint8()) | ||||
|     im = Image.fromarrow(arr, 'RGBA', (5, 5)) | ||||
| 
 | ||||
| Pillow images can also be converted to Arrow objects:: | ||||
| 
 | ||||
|     from PIL import Image | ||||
|     import pyarrow as pa | ||||
|     im = Image.open('hopper.jpg') | ||||
|     arr = pa.array(im) | ||||
| 
 | ||||
| Reading and writing AVIF images | ||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | ||||
| 
 | ||||
| Pillow can now read and write AVIF images when built from source with libavif 1.0.0 | ||||
| or later. | ||||
|  | @ -14,7 +14,7 @@ expected to be backported to earlier versions. | |||
| .. toctree:: | ||||
|   :maxdepth: 2 | ||||
| 
 | ||||
|   11.2.0 | ||||
|   11.2.1 | ||||
|   11.1.0 | ||||
|   11.0.0 | ||||
|   10.4.0 | ||||
|  |  | |||
|  | @ -1,7 +1,7 @@ | |||
| [build-system] | ||||
| build-backend = "backend" | ||||
| requires = [ | ||||
|   "setuptools>=67.8", | ||||
|   "setuptools>=77", | ||||
| ] | ||||
| backend-path = [ | ||||
|   "_custom_build", | ||||
|  | @ -14,14 +14,14 @@ readme = "README.md" | |||
| keywords = [ | ||||
|   "Imaging", | ||||
| ] | ||||
| license = { text = "MIT-CMU" } | ||||
| license = "MIT-CMU" | ||||
| license-files = [ "LICENSE" ] | ||||
| authors = [ | ||||
|   { name = "Jeffrey A. Clark", email = "aclark@aclark.net" }, | ||||
| ] | ||||
| requires-python = ">=3.9" | ||||
| classifiers = [ | ||||
|   "Development Status :: 6 - Mature", | ||||
|   "License :: OSI Approved :: CMU License (MIT-CMU)", | ||||
|   "Programming Language :: Python :: 3 :: Only", | ||||
|   "Programming Language :: Python :: 3.9", | ||||
|   "Programming Language :: Python :: 3.10", | ||||
|  | @ -44,6 +44,7 @@ optional-dependencies.docs = [ | |||
|   "furo", | ||||
|   "olefile", | ||||
|   "sphinx>=8.2", | ||||
|   "sphinx-autobuild", | ||||
|   "sphinx-copybutton", | ||||
|   "sphinx-inline-tabs", | ||||
|   "sphinxext-opengraph", | ||||
|  | @ -54,6 +55,10 @@ optional-dependencies.fpx = [ | |||
| optional-dependencies.mic = [ | ||||
|   "olefile", | ||||
| ] | ||||
| optional-dependencies.test-arrow = [ | ||||
|   "pyarrow", | ||||
| ] | ||||
| 
 | ||||
| optional-dependencies.tests = [ | ||||
|   "check-manifest", | ||||
|   "coverage>=7.4.2", | ||||
|  | @ -67,6 +72,7 @@ optional-dependencies.tests = [ | |||
|   "pytest-timeout", | ||||
|   "trove-classifiers>=2024.10.12", | ||||
| ] | ||||
| 
 | ||||
| optional-dependencies.typing = [ | ||||
|   "typing-extensions; python_version<'3.10'", | ||||
| ] | ||||
|  |  | |||
							
								
								
									
										20
									
								
								setup.py
									
									
									
									
									
								
							
							
						
						
									
										20
									
								
								setup.py
									
									
									
									
									
								
							|  | @ -32,6 +32,7 @@ configuration: dict[str, list[str]] = {} | |||
| 
 | ||||
| 
 | ||||
| PILLOW_VERSION = get_version() | ||||
| AVIF_ROOT = None | ||||
| FREETYPE_ROOT = None | ||||
| HARFBUZZ_ROOT = None | ||||
| FRIBIDI_ROOT = None | ||||
|  | @ -64,6 +65,7 @@ _IMAGING = ("decode", "encode", "map", "display", "outline", "path") | |||
| _LIB_IMAGING = ( | ||||
|     "Access", | ||||
|     "AlphaComposite", | ||||
|     "Arrow", | ||||
|     "Resample", | ||||
|     "Reduce", | ||||
|     "Bands", | ||||
|  | @ -306,6 +308,7 @@ class pil_build_ext(build_ext): | |||
|             "jpeg2000", | ||||
|             "imagequant", | ||||
|             "xcb", | ||||
|             "avif", | ||||
|         ] | ||||
| 
 | ||||
|         required = {"jpeg", "zlib"} | ||||
|  | @ -481,6 +484,7 @@ class pil_build_ext(build_ext): | |||
|         # | ||||
|         # add configured kits | ||||
|         for root_name, lib_name in { | ||||
|             "AVIF_ROOT": "avif", | ||||
|             "JPEG_ROOT": "libjpeg", | ||||
|             "JPEG2K_ROOT": "libopenjp2", | ||||
|             "TIFF_ROOT": ("libtiff-5", "libtiff-4"), | ||||
|  | @ -846,6 +850,12 @@ class pil_build_ext(build_ext): | |||
|                 if _find_library_file(self, "xcb"): | ||||
|                     feature.set("xcb", "xcb") | ||||
| 
 | ||||
|         if feature.want("avif"): | ||||
|             _dbg("Looking for avif") | ||||
|             if _find_include_file(self, "avif/avif.h"): | ||||
|                 if _find_library_file(self, "avif"): | ||||
|                     feature.set("avif", "avif") | ||||
| 
 | ||||
|         for f in feature: | ||||
|             if not feature.get(f) and feature.require(f): | ||||
|                 if f in ("jpeg", "zlib"): | ||||
|  | @ -934,6 +944,14 @@ class pil_build_ext(build_ext): | |||
|         else: | ||||
|             self._remove_extension("PIL._webp") | ||||
| 
 | ||||
|         if feature.get("avif"): | ||||
|             libs = [feature.get("avif")] | ||||
|             if sys.platform == "win32": | ||||
|                 libs.extend(["ntdll", "userenv", "ws2_32", "bcrypt"]) | ||||
|             self._update_extension("PIL._avif", libs) | ||||
|         else: | ||||
|             self._remove_extension("PIL._avif") | ||||
| 
 | ||||
|         tk_libs = ["psapi"] if sys.platform in ("win32", "cygwin") else [] | ||||
|         self._update_extension("PIL._imagingtk", tk_libs) | ||||
| 
 | ||||
|  | @ -976,6 +994,7 @@ class pil_build_ext(build_ext): | |||
|             (feature.get("lcms"), "LITTLECMS2"), | ||||
|             (feature.get("webp"), "WEBP"), | ||||
|             (feature.get("xcb"), "XCB (X protocol)"), | ||||
|             (feature.get("avif"), "LIBAVIF"), | ||||
|         ] | ||||
| 
 | ||||
|         all = 1 | ||||
|  | @ -1018,6 +1037,7 @@ ext_modules = [ | |||
|     Extension("PIL._imagingft", ["src/_imagingft.c"]), | ||||
|     Extension("PIL._imagingcms", ["src/_imagingcms.c"]), | ||||
|     Extension("PIL._webp", ["src/_webp.c"]), | ||||
|     Extension("PIL._avif", ["src/_avif.c"]), | ||||
|     Extension("PIL._imagingtk", ["src/_imagingtk.c", "src/Tk/tkImaging.c"]), | ||||
|     Extension("PIL._imagingmath", ["src/_imagingmath.c"]), | ||||
|     Extension("PIL._imagingmorph", ["src/_imagingmorph.c"]), | ||||
|  |  | |||
Some files were not shown because too many files have changed in this diff Show More
		Loading…
	
		Reference in New Issue
	
	Block a user