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\ |     sudo apt-get -qq install libfreetype6-dev liblcms2-dev libtiff-dev python3-tk\ | ||||||
|                              ghostscript libjpeg-turbo8-dev libopenjp2-7-dev\ |                              ghostscript libjpeg-turbo8-dev libopenjp2-7-dev\ | ||||||
|                              cmake meson imagemagick libharfbuzz-dev libfribidi-dev\ |                              cmake meson imagemagick libharfbuzz-dev libfribidi-dev\ | ||||||
|                              sway wl-clipboard libopenblas-dev |                              sway wl-clipboard libopenblas-dev nasm | ||||||
| fi | fi | ||||||
| 
 | 
 | ||||||
| python3 -m pip install --upgrade pip | 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-cov | ||||||
| python3 -m pip install -U pytest-timeout | python3 -m pip install -U pytest-timeout | ||||||
| python3 -m pip install pyroma | 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 | if [[ $(uname) != CYGWIN* ]]; then | ||||||
|     python3 -m pip install numpy |     python3 -m pip install numpy | ||||||
|  | @ -50,7 +53,7 @@ if [[ $(uname) != CYGWIN* ]]; then | ||||||
|     # Pyroma uses non-isolated build and fails with old setuptools |     # Pyroma uses non-isolated build and fails with old setuptools | ||||||
|     if [[ $GHA_PYTHON_VERSION == 3.9 ]]; then |     if [[ $GHA_PYTHON_VERSION == 3.9 ]]; then | ||||||
|         # To match pyproject.toml |         # To match pyproject.toml | ||||||
|         python3 -m pip install "setuptools>=67.8" |         python3 -m pip install "setuptools>=77" | ||||||
|     fi |     fi | ||||||
| 
 | 
 | ||||||
|     # webp |     # webp | ||||||
|  | @ -62,6 +65,9 @@ if [[ $(uname) != CYGWIN* ]]; then | ||||||
|     # raqm |     # raqm | ||||||
|     pushd depends && ./install_raqm.sh && popd |     pushd depends && ./install_raqm.sh && popd | ||||||
| 
 | 
 | ||||||
|  |     # libavif | ||||||
|  |     pushd depends && CMAKE_POLICY_VERSION_MINIMUM=3.5 ./install_libavif.sh && popd | ||||||
|  | 
 | ||||||
|     # extra test images |     # extra test images | ||||||
|     pushd depends && ./install_extra_test_images.sh && popd |     pushd depends && ./install_extra_test_images.sh && popd | ||||||
| else | 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 | # A clang-format style that approximates Python's PEP 7 | ||||||
| # Useful for IDE integration | # 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 | BasedOnStyle: Google | ||||||
| AlwaysBreakAfterReturnType: All | AlwaysBreakAfterReturnType: All | ||||||
| AllowShortIfStatementsOnASingleLine: false | AllowShortIfStatementsOnASingleLine: false | ||||||
|  | @ -11,7 +32,6 @@ ColumnLimit: 88 | ||||||
| DerivePointerAlignment: false | DerivePointerAlignment: false | ||||||
| IndentGotoLabels: false | IndentGotoLabels: false | ||||||
| IndentWidth: 4 | IndentWidth: 4 | ||||||
| Language: Cpp |  | ||||||
| PointerAlignment: Right | PointerAlignment: Right | ||||||
| ReflowComments: true | ReflowComments: true | ||||||
| SortIncludes: false | 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 |     brew uninstall gradle maven | ||||||
| fi | fi | ||||||
| brew install \ | brew install \ | ||||||
|  |     aom \ | ||||||
|  |     dav1d \ | ||||||
|     freetype \ |     freetype \ | ||||||
|     ghostscript \ |     ghostscript \ | ||||||
|     jpeg-turbo \ |     jpeg-turbo \ | ||||||
|  | @ -14,6 +16,8 @@ brew install \ | ||||||
|     libtiff \ |     libtiff \ | ||||||
|     little-cms2 \ |     little-cms2 \ | ||||||
|     openjpeg \ |     openjpeg \ | ||||||
|  |     rav1e \ | ||||||
|  |     svt-av1 \ | ||||||
|     webp |     webp | ||||||
| export PKG_CONFIG_PATH="/usr/local/opt/openblas/lib/pkgconfig" | 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 -U pytest-timeout | ||||||
| python3 -m pip install pyroma | python3 -m pip install pyroma | ||||||
| python3 -m pip install numpy | 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 | # extra test images | ||||||
| pushd depends && ./install_extra_test_images.sh && popd | 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, |           centos-stream-10-amd64, | ||||||
|           debian-12-bookworm-x86, |           debian-12-bookworm-x86, | ||||||
|           debian-12-bookworm-amd64, |           debian-12-bookworm-amd64, | ||||||
|           fedora-40-amd64, |  | ||||||
|           fedora-41-amd64, |           fedora-41-amd64, | ||||||
|  |           fedora-42-amd64, | ||||||
|           gentoo, |           gentoo, | ||||||
|           ubuntu-22.04-jammy-amd64, |           ubuntu-22.04-jammy-amd64, | ||||||
|           ubuntu-24.04-noble-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-gcc \ | ||||||
|               mingw-w64-x86_64-ghostscript \ |               mingw-w64-x86_64-ghostscript \ | ||||||
|               mingw-w64-x86_64-lcms2 \ |               mingw-w64-x86_64-lcms2 \ | ||||||
|  |               mingw-w64-x86_64-libavif \ | ||||||
|               mingw-w64-x86_64-libimagequant \ |               mingw-w64-x86_64-libimagequant \ | ||||||
|               mingw-w64-x86_64-libjpeg-turbo \ |               mingw-w64-x86_64-libjpeg-turbo \ | ||||||
|               mingw-w64-x86_64-libraqm \ |               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 |             # Test the oldest Python on 32-bit | ||||||
|             - { python-version: "3.9", architecture: "x86", os: "windows-2019" } |             - { python-version: "3.9", architecture: "x86", os: "windows-2019" } | ||||||
| 
 | 
 | ||||||
|     timeout-minutes: 30 |     timeout-minutes: 45 | ||||||
| 
 | 
 | ||||||
|     name: Python ${{ matrix.python-version }} (${{ matrix.architecture }}) |     name: Python ${{ matrix.python-version }} (${{ matrix.architecture }}) | ||||||
| 
 | 
 | ||||||
|  | @ -88,6 +88,10 @@ jobs: | ||||||
|       run: | |       run: | | ||||||
|         python3 -m pip install PyQt6 |         python3 -m pip install PyQt6 | ||||||
| 
 | 
 | ||||||
|  |     - name: Install PyArrow dependency | ||||||
|  |       run: | | ||||||
|  |         python3 -m pip install --only-binary=:all: pyarrow || true | ||||||
|  | 
 | ||||||
|     - name: Install dependencies |     - name: Install dependencies | ||||||
|       id: install |       id: install | ||||||
|       run: | |       run: | | ||||||
|  | @ -145,6 +149,10 @@ jobs: | ||||||
|       if: steps.build-cache.outputs.cache-hit != 'true' |       if: steps.build-cache.outputs.cache-hit != 'true' | ||||||
|       run: "& winbuild\\build\\build_dep_libpng.cmd" |       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 |     # for FreeType WOFF2 font support | ||||||
|     - name: Build dependencies / brotli |     - name: Build dependencies / brotli | ||||||
|       if: steps.build-cache.outputs.cache-hit != 'true' |       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 |         persist-credentials: false | ||||||
| 
 | 
 | ||||||
|     - name: Set up Python ${{ matrix.python-version }} |     - name: Set up Python ${{ matrix.python-version }} | ||||||
|       uses: Quansight-Labs/setup-python@v5 |       uses: actions/setup-python@v5 | ||||||
|       with: |       with: | ||||||
|         python-version: ${{ matrix.python-version }} |         python-version: ${{ matrix.python-version }} | ||||||
|         allow-prereleases: true |         allow-prereleases: true | ||||||
|  |  | ||||||
							
								
								
									
										21
									
								
								.github/workflows/wheels-dependencies.sh
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										21
									
								
								.github/workflows/wheels-dependencies.sh
									
									
									
									
										vendored
									
									
								
							|  | @ -25,7 +25,7 @@ else | ||||||
|     MB_ML_LIBC=${AUDITWHEEL_POLICY::9} |     MB_ML_LIBC=${AUDITWHEEL_POLICY::9} | ||||||
|     MB_ML_VER=${AUDITWHEEL_POLICY:9} |     MB_ML_VER=${AUDITWHEEL_POLICY:9} | ||||||
| fi | fi | ||||||
| PLAT=$CIBW_ARCHS | PLAT="${CIBW_ARCHS:-$AUDITWHEEL_ARCH}" | ||||||
| 
 | 
 | ||||||
| # Define custom utilities | # Define custom utilities | ||||||
| source wheels/multibuild/common_utils.sh | source wheels/multibuild/common_utils.sh | ||||||
|  | @ -38,13 +38,14 @@ ARCHIVE_SDIR=pillow-depends-main | ||||||
| 
 | 
 | ||||||
| # Package versions for fresh source builds | # Package versions for fresh source builds | ||||||
| FREETYPE_VERSION=2.13.3 | FREETYPE_VERSION=2.13.3 | ||||||
| HARFBUZZ_VERSION=10.4.0 | HARFBUZZ_VERSION=11.1.0 | ||||||
| LIBPNG_VERSION=1.6.47 | LIBPNG_VERSION=1.6.47 | ||||||
| JPEGTURBO_VERSION=3.1.0 | JPEGTURBO_VERSION=3.1.0 | ||||||
| OPENJPEG_VERSION=2.5.3 | OPENJPEG_VERSION=2.5.3 | ||||||
| XZ_VERSION=5.6.4 | XZ_VERSION=5.8.1 | ||||||
| TIFF_VERSION=4.7.0 | TIFF_VERSION=4.7.0 | ||||||
| LCMS2_VERSION=2.17 | LCMS2_VERSION=2.17 | ||||||
|  | ZLIB_VERSION=1.3.1 | ||||||
| ZLIB_NG_VERSION=2.2.4 | ZLIB_NG_VERSION=2.2.4 | ||||||
| LIBWEBP_VERSION=1.5.0 | LIBWEBP_VERSION=1.5.0 | ||||||
| BZIP2_VERSION=1.0.8 | BZIP2_VERSION=1.0.8 | ||||||
|  | @ -64,11 +65,7 @@ function build_pkg_config { | ||||||
| 
 | 
 | ||||||
| function build_zlib_ng { | function build_zlib_ng { | ||||||
|     if [ -e zlib-stamp ]; then return; fi |     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 |     build_github zlib-ng/zlib-ng $ZLIB_NG_VERSION --zlib-compat | ||||||
|     (cd zlib-ng-$ZLIB_NG_VERSION \ |  | ||||||
|         && ./configure --prefix=$BUILD_PREFIX --zlib-compat \ |  | ||||||
|         && make -j4 \ |  | ||||||
|         && make install) |  | ||||||
| 
 | 
 | ||||||
|     if [ -n "$IS_MACOS" ]; then |     if [ -n "$IS_MACOS" ]; then | ||||||
|         # Ensure that on macOS, the library name is an absolute path, not an |         # 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) |     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 \ |     (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 \ |     (cd $out_dir/build \ | ||||||
|         && meson install) |         && meson install) | ||||||
|     touch harfbuzz-stamp |     touch harfbuzz-stamp | ||||||
|  | @ -106,7 +103,11 @@ function build { | ||||||
|     if [ -z "$IS_ALPINE" ] && [ -z "$SANITIZER" ] && [ -z "$IS_MACOS" ]; then |     if [ -z "$IS_ALPINE" ] && [ -z "$SANITIZER" ] && [ -z "$IS_MACOS" ]; then | ||||||
|         yum remove -y zlib-devel |         yum remove -y zlib-devel | ||||||
|     fi |     fi | ||||||
|     build_zlib_ng |     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 |     build_simple xcb-proto 1.17.0 https://xorg.freedesktop.org/archive/individual/proto | ||||||
|     if [ -n "$IS_MACOS" ]; then |     if [ -n "$IS_MACOS" ]; then | ||||||
|  |  | ||||||
							
								
								
									
										9
									
								
								.github/workflows/wheels.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										9
									
								
								.github/workflows/wheels.yml
									
									
									
									
										vendored
									
									
								
							|  | @ -121,14 +121,17 @@ jobs: | ||||||
|   windows: |   windows: | ||||||
|     if: github.event_name != 'schedule' || github.repository_owner == 'python-pillow' |     if: github.event_name != 'schedule' || github.repository_owner == 'python-pillow' | ||||||
|     name: Windows ${{ matrix.cibw_arch }} |     name: Windows ${{ matrix.cibw_arch }} | ||||||
|     runs-on: windows-latest |     runs-on: ${{ matrix.os }} | ||||||
|     strategy: |     strategy: | ||||||
|       fail-fast: false |       fail-fast: false | ||||||
|       matrix: |       matrix: | ||||||
|         include: |         include: | ||||||
|           - cibw_arch: x86 |           - cibw_arch: x86 | ||||||
|  |             os: windows-latest | ||||||
|           - cibw_arch: AMD64 |           - cibw_arch: AMD64 | ||||||
|  |             os: windows-latest | ||||||
|           - cibw_arch: ARM64 |           - cibw_arch: ARM64 | ||||||
|  |             os: windows-11-arm | ||||||
|     steps: |     steps: | ||||||
|       - uses: actions/checkout@v4 |       - uses: actions/checkout@v4 | ||||||
|         with: |         with: | ||||||
|  | @ -157,7 +160,7 @@ jobs: | ||||||
|           # Install extra test images |           # Install extra test images | ||||||
|           xcopy /S /Y Tests\test-images\* Tests\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 |         shell: pwsh | ||||||
| 
 | 
 | ||||||
|       - name: Build wheels |       - name: Build wheels | ||||||
|  | @ -240,7 +243,7 @@ jobs: | ||||||
|           path: dist |           path: dist | ||||||
|           merge-multiple: true |           merge-multiple: true | ||||||
|       - name: Upload wheels to scientific-python-nightly-wheels |       - 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: |         with: | ||||||
|           artifacts_path: dist |           artifacts_path: dist | ||||||
|           anaconda_nightly_upload_token: ${{ secrets.ANACONDA_ORG_UPLOAD_TOKEN }} |           anaconda_nightly_upload_token: ${{ secrets.ANACONDA_ORG_UPLOAD_TOKEN }} | ||||||
|  |  | ||||||
|  | @ -1,6 +1,6 @@ | ||||||
| repos: | repos: | ||||||
|   - repo: https://github.com/astral-sh/ruff-pre-commit |   - repo: https://github.com/astral-sh/ruff-pre-commit | ||||||
|     rev: v0.9.9 |     rev: v0.11.4 | ||||||
|     hooks: |     hooks: | ||||||
|       - id: ruff |       - id: ruff | ||||||
|         args: [--exit-non-zero-on-fix] |         args: [--exit-non-zero-on-fix] | ||||||
|  | @ -24,7 +24,7 @@ repos: | ||||||
|         exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.gd$|\.opt$) |         exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.gd$|\.opt$) | ||||||
| 
 | 
 | ||||||
|   - repo: https://github.com/pre-commit/mirrors-clang-format |   - repo: https://github.com/pre-commit/mirrors-clang-format | ||||||
|     rev: v19.1.7 |     rev: v20.1.0 | ||||||
|     hooks: |     hooks: | ||||||
|       - id: clang-format |       - id: clang-format | ||||||
|         types: [c] |         types: [c] | ||||||
|  | @ -44,20 +44,21 @@ repos: | ||||||
|       - id: check-json |       - id: check-json | ||||||
|       - id: check-toml |       - id: check-toml | ||||||
|       - id: check-yaml |       - id: check-yaml | ||||||
|  |         args: [--allow-multiple-documents] | ||||||
|       - id: end-of-file-fixer |       - id: end-of-file-fixer | ||||||
|         exclude: ^Tests/images/ |         exclude: ^Tests/images/ | ||||||
|       - id: trailing-whitespace |       - id: trailing-whitespace | ||||||
|         exclude: ^.github/.*TEMPLATE|^Tests/(fonts|images)/ |         exclude: ^.github/.*TEMPLATE|^Tests/(fonts|images)/ | ||||||
| 
 | 
 | ||||||
|   - repo: https://github.com/python-jsonschema/check-jsonschema |   - repo: https://github.com/python-jsonschema/check-jsonschema | ||||||
|     rev: 0.31.2 |     rev: 0.32.1 | ||||||
|     hooks: |     hooks: | ||||||
|       - id: check-github-workflows |       - id: check-github-workflows | ||||||
|       - id: check-readthedocs |       - id: check-readthedocs | ||||||
|       - id: check-renovate |       - id: check-renovate | ||||||
| 
 | 
 | ||||||
|   - repo: https://github.com/woodruffw/zizmor-pre-commit |   - repo: https://github.com/woodruffw/zizmor-pre-commit | ||||||
|     rev: v1.4.1 |     rev: v1.5.2 | ||||||
|     hooks: |     hooks: | ||||||
|       - id: zizmor |       - id: zizmor | ||||||
| 
 | 
 | ||||||
|  | @ -72,7 +73,7 @@ repos: | ||||||
|       - id: pyproject-fmt |       - id: pyproject-fmt | ||||||
| 
 | 
 | ||||||
|   - repo: https://github.com/abravalheri/validate-pyproject |   - repo: https://github.com/abravalheri/validate-pyproject | ||||||
|     rev: v0.23 |     rev: v0.24.1 | ||||||
|     hooks: |     hooks: | ||||||
|       - id: validate-pyproject |       - id: validate-pyproject | ||||||
|         additional_dependencies: [trove-classifiers>=2024.10.12] |         additional_dependencies: [trove-classifiers>=2024.10.12] | ||||||
|  |  | ||||||
							
								
								
									
										5
									
								
								Makefile
									
									
									
									
									
								
							
							
						
						
									
										5
									
								
								Makefile
									
									
									
									
									
								
							|  | @ -23,6 +23,10 @@ doc html: | ||||||
| htmlview: | htmlview: | ||||||
| 	$(MAKE) -C docs htmlview | 	$(MAKE) -C docs htmlview | ||||||
| 
 | 
 | ||||||
|  | .PHONY: htmllive | ||||||
|  | htmllive: | ||||||
|  | 	$(MAKE) -C docs htmllive | ||||||
|  | 
 | ||||||
| .PHONY: doccheck | .PHONY: doccheck | ||||||
| doccheck: | doccheck: | ||||||
| 	$(MAKE) doc | 	$(MAKE) doc | ||||||
|  | @ -43,6 +47,7 @@ help: | ||||||
| 	@echo "  docserve           run an HTTP server on the docs directory" | 	@echo "  docserve           run an HTTP server on the docs directory" | ||||||
| 	@echo "  html               make HTML docs" | 	@echo "  html               make HTML docs" | ||||||
| 	@echo "  htmlview           open the index page built by the html target in your browser" | 	@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            make and install" | ||||||
| 	@echo "  install-coverage   make and install with C coverage" | 	@echo "  install-coverage   make and install with C coverage" | ||||||
| 	@echo "  lint               run the lint checks" | 	@echo "  lint               run the lint checks" | ||||||
|  |  | ||||||
|  | @ -1,9 +1,12 @@ | ||||||
| from __future__ import annotations | from __future__ import annotations | ||||||
| 
 | 
 | ||||||
|  | import platform | ||||||
| import sys | import sys | ||||||
| 
 | 
 | ||||||
| from PIL import features | from PIL import features | ||||||
| 
 | 
 | ||||||
|  | from .helper import is_pypy | ||||||
|  | 
 | ||||||
| 
 | 
 | ||||||
| def test_wheel_modules() -> None: | def test_wheel_modules() -> None: | ||||||
|     expected_modules = {"pil", "tkinter", "freetype2", "littlecms2", "webp"} |     expected_modules = {"pil", "tkinter", "freetype2", "littlecms2", "webp"} | ||||||
|  | @ -40,5 +43,7 @@ def test_wheel_features() -> None: | ||||||
| 
 | 
 | ||||||
|     if sys.platform == "win32": |     if sys.platform == "win32": | ||||||
|         expected_features.remove("xcb") |         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 |     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) | # (referenced from https://wiki.mozilla.org/APNG_Specification) | ||||||
| def test_apng_basic() -> None: | def test_apng_basic() -> None: | ||||||
|     with Image.open("Tests/images/apng/single_frame.png") as im: |     with Image.open("Tests/images/apng/single_frame.png") as im: | ||||||
|  |         assert isinstance(im, PngImagePlugin.PngImageFile) | ||||||
|         assert not im.is_animated |         assert not im.is_animated | ||||||
|         assert im.n_frames == 1 |         assert im.n_frames == 1 | ||||||
|         assert im.get_format_mimetype() == "image/apng" |         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) |         assert im.getpixel((64, 32)) == (0, 255, 0, 255) | ||||||
| 
 | 
 | ||||||
|     with Image.open("Tests/images/apng/single_frame_default.png") as im: |     with Image.open("Tests/images/apng/single_frame_default.png") as im: | ||||||
|  |         assert isinstance(im, PngImagePlugin.PngImageFile) | ||||||
|         assert im.is_animated |         assert im.is_animated | ||||||
|         assert im.n_frames == 2 |         assert im.n_frames == 2 | ||||||
|         assert im.get_format_mimetype() == "image/apng" |         assert im.get_format_mimetype() == "image/apng" | ||||||
|  | @ -53,6 +55,7 @@ def test_apng_basic() -> None: | ||||||
| ) | ) | ||||||
| def test_apng_fdat(filename: str) -> None: | def test_apng_fdat(filename: str) -> None: | ||||||
|     with Image.open(filename) as im: |     with Image.open(filename) as im: | ||||||
|  |         assert isinstance(im, PngImagePlugin.PngImageFile) | ||||||
|         im.seek(im.n_frames - 1) |         im.seek(im.n_frames - 1) | ||||||
|         assert im.getpixel((0, 0)) == (0, 255, 0, 255) |         assert im.getpixel((0, 0)) == (0, 255, 0, 255) | ||||||
|         assert im.getpixel((64, 32)) == (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: | def test_apng_dispose() -> None: | ||||||
|     with Image.open("Tests/images/apng/dispose_op_none.png") as im: |     with Image.open("Tests/images/apng/dispose_op_none.png") as im: | ||||||
|  |         assert isinstance(im, PngImagePlugin.PngImageFile) | ||||||
|         im.seek(im.n_frames - 1) |         im.seek(im.n_frames - 1) | ||||||
|         assert im.getpixel((0, 0)) == (0, 255, 0, 255) |         assert im.getpixel((0, 0)) == (0, 255, 0, 255) | ||||||
|         assert im.getpixel((64, 32)) == (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: |     with Image.open("Tests/images/apng/dispose_op_background.png") as im: | ||||||
|  |         assert isinstance(im, PngImagePlugin.PngImageFile) | ||||||
|         im.seek(im.n_frames - 1) |         im.seek(im.n_frames - 1) | ||||||
|         assert im.getpixel((0, 0)) == (0, 0, 0, 0) |         assert im.getpixel((0, 0)) == (0, 0, 0, 0) | ||||||
|         assert im.getpixel((64, 32)) == (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: |     with Image.open("Tests/images/apng/dispose_op_background_final.png") as im: | ||||||
|  |         assert isinstance(im, PngImagePlugin.PngImageFile) | ||||||
|         im.seek(im.n_frames - 1) |         im.seek(im.n_frames - 1) | ||||||
|         assert im.getpixel((0, 0)) == (0, 255, 0, 255) |         assert im.getpixel((0, 0)) == (0, 255, 0, 255) | ||||||
|         assert im.getpixel((64, 32)) == (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: |     with Image.open("Tests/images/apng/dispose_op_previous.png") as im: | ||||||
|  |         assert isinstance(im, PngImagePlugin.PngImageFile) | ||||||
|         im.seek(im.n_frames - 1) |         im.seek(im.n_frames - 1) | ||||||
|         assert im.getpixel((0, 0)) == (0, 255, 0, 255) |         assert im.getpixel((0, 0)) == (0, 255, 0, 255) | ||||||
|         assert im.getpixel((64, 32)) == (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: |     with Image.open("Tests/images/apng/dispose_op_previous_final.png") as im: | ||||||
|  |         assert isinstance(im, PngImagePlugin.PngImageFile) | ||||||
|         im.seek(im.n_frames - 1) |         im.seek(im.n_frames - 1) | ||||||
|         assert im.getpixel((0, 0)) == (0, 255, 0, 255) |         assert im.getpixel((0, 0)) == (0, 255, 0, 255) | ||||||
|         assert im.getpixel((64, 32)) == (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: |     with Image.open("Tests/images/apng/dispose_op_previous_first.png") as im: | ||||||
|  |         assert isinstance(im, PngImagePlugin.PngImageFile) | ||||||
|         im.seek(im.n_frames - 1) |         im.seek(im.n_frames - 1) | ||||||
|         assert im.getpixel((0, 0)) == (0, 0, 0, 0) |         assert im.getpixel((0, 0)) == (0, 0, 0, 0) | ||||||
|         assert im.getpixel((64, 32)) == (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: | def test_apng_dispose_region() -> None: | ||||||
|     with Image.open("Tests/images/apng/dispose_op_none_region.png") as im: |     with Image.open("Tests/images/apng/dispose_op_none_region.png") as im: | ||||||
|  |         assert isinstance(im, PngImagePlugin.PngImageFile) | ||||||
|         im.seek(im.n_frames - 1) |         im.seek(im.n_frames - 1) | ||||||
|         assert im.getpixel((0, 0)) == (0, 255, 0, 255) |         assert im.getpixel((0, 0)) == (0, 255, 0, 255) | ||||||
|         assert im.getpixel((64, 32)) == (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: |     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) |         im.seek(im.n_frames - 1) | ||||||
|         assert im.getpixel((0, 0)) == (0, 0, 0, 0) |         assert im.getpixel((0, 0)) == (0, 0, 0, 0) | ||||||
|         assert im.getpixel((64, 32)) == (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: |     with Image.open("Tests/images/apng/dispose_op_background_region.png") as im: | ||||||
|  |         assert isinstance(im, PngImagePlugin.PngImageFile) | ||||||
|         im.seek(im.n_frames - 1) |         im.seek(im.n_frames - 1) | ||||||
|         assert im.getpixel((0, 0)) == (0, 0, 255, 255) |         assert im.getpixel((0, 0)) == (0, 0, 255, 255) | ||||||
|         assert im.getpixel((64, 32)) == (0, 0, 0, 0) |         assert im.getpixel((64, 32)) == (0, 0, 0, 0) | ||||||
| 
 | 
 | ||||||
|     with Image.open("Tests/images/apng/dispose_op_previous_region.png") as im: |     with Image.open("Tests/images/apng/dispose_op_previous_region.png") as im: | ||||||
|  |         assert isinstance(im, PngImagePlugin.PngImageFile) | ||||||
|         im.seek(im.n_frames - 1) |         im.seek(im.n_frames - 1) | ||||||
|         assert im.getpixel((0, 0)) == (0, 255, 0, 255) |         assert im.getpixel((0, 0)) == (0, 255, 0, 255) | ||||||
|         assert im.getpixel((64, 32)) == (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: |     with Image.open("Tests/images/apng/dispose_op_previous_frame.png") as im: | ||||||
|  |         assert isinstance(im, PngImagePlugin.PngImageFile) | ||||||
|         im.seek(im.n_frames - 1) |         im.seek(im.n_frames - 1) | ||||||
|         assert im.getpixel((0, 0)) == (255, 0, 0, 255) |         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: | def test_apng_blend() -> None: | ||||||
|     with Image.open("Tests/images/apng/blend_op_source_solid.png") as im: |     with Image.open("Tests/images/apng/blend_op_source_solid.png") as im: | ||||||
|  |         assert isinstance(im, PngImagePlugin.PngImageFile) | ||||||
|         im.seek(im.n_frames - 1) |         im.seek(im.n_frames - 1) | ||||||
|         assert im.getpixel((0, 0)) == (0, 255, 0, 255) |         assert im.getpixel((0, 0)) == (0, 255, 0, 255) | ||||||
|         assert im.getpixel((64, 32)) == (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: |     with Image.open("Tests/images/apng/blend_op_source_transparent.png") as im: | ||||||
|  |         assert isinstance(im, PngImagePlugin.PngImageFile) | ||||||
|         im.seek(im.n_frames - 1) |         im.seek(im.n_frames - 1) | ||||||
|         assert im.getpixel((0, 0)) == (0, 0, 0, 0) |         assert im.getpixel((0, 0)) == (0, 0, 0, 0) | ||||||
|         assert im.getpixel((64, 32)) == (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: |     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) |         im.seek(im.n_frames - 1) | ||||||
|         assert im.getpixel((0, 0)) == (0, 255, 0, 2) |         assert im.getpixel((0, 0)) == (0, 255, 0, 2) | ||||||
|         assert im.getpixel((64, 32)) == (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: |     with Image.open("Tests/images/apng/blend_op_over.png") as im: | ||||||
|  |         assert isinstance(im, PngImagePlugin.PngImageFile) | ||||||
|         im.seek(im.n_frames - 1) |         im.seek(im.n_frames - 1) | ||||||
|         assert im.getpixel((0, 0)) == (0, 255, 0, 255) |         assert im.getpixel((0, 0)) == (0, 255, 0, 255) | ||||||
|         assert im.getpixel((64, 32)) == (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: |     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) |         im.seek(im.n_frames - 1) | ||||||
|         assert im.getpixel((0, 0)) == (0, 255, 0, 97) |         assert im.getpixel((0, 0)) == (0, 255, 0, 97) | ||||||
|         assert im.getpixel((64, 32)) == (0, 255, 0, 255) |         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: | def test_apng_chunk_order() -> None: | ||||||
|     with Image.open("Tests/images/apng/fctl_actl.png") as im: |     with Image.open("Tests/images/apng/fctl_actl.png") as im: | ||||||
|  |         assert isinstance(im, PngImagePlugin.PngImageFile) | ||||||
|         im.seek(im.n_frames - 1) |         im.seek(im.n_frames - 1) | ||||||
|         assert im.getpixel((0, 0)) == (0, 255, 0, 255) |         assert im.getpixel((0, 0)) == (0, 255, 0, 255) | ||||||
|         assert im.getpixel((64, 32)) == (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: | def test_apng_mode() -> None: | ||||||
|     with Image.open("Tests/images/apng/mode_16bit.png") as im: |     with Image.open("Tests/images/apng/mode_16bit.png") as im: | ||||||
|  |         assert isinstance(im, PngImagePlugin.PngImageFile) | ||||||
|         assert im.mode == "RGBA" |         assert im.mode == "RGBA" | ||||||
|         im.seek(im.n_frames - 1) |         im.seek(im.n_frames - 1) | ||||||
|         assert im.getpixel((0, 0)) == (0, 0, 128, 191) |         assert im.getpixel((0, 0)) == (0, 0, 128, 191) | ||||||
|         assert im.getpixel((64, 32)) == (0, 0, 128, 191) |         assert im.getpixel((64, 32)) == (0, 0, 128, 191) | ||||||
| 
 | 
 | ||||||
|     with Image.open("Tests/images/apng/mode_grayscale.png") as im: |     with Image.open("Tests/images/apng/mode_grayscale.png") as im: | ||||||
|  |         assert isinstance(im, PngImagePlugin.PngImageFile) | ||||||
|         assert im.mode == "L" |         assert im.mode == "L" | ||||||
|         im.seek(im.n_frames - 1) |         im.seek(im.n_frames - 1) | ||||||
|         assert im.getpixel((0, 0)) == 128 |         assert im.getpixel((0, 0)) == 128 | ||||||
|         assert im.getpixel((64, 32)) == 255 |         assert im.getpixel((64, 32)) == 255 | ||||||
| 
 | 
 | ||||||
|     with Image.open("Tests/images/apng/mode_grayscale_alpha.png") as im: |     with Image.open("Tests/images/apng/mode_grayscale_alpha.png") as im: | ||||||
|  |         assert isinstance(im, PngImagePlugin.PngImageFile) | ||||||
|         assert im.mode == "LA" |         assert im.mode == "LA" | ||||||
|         im.seek(im.n_frames - 1) |         im.seek(im.n_frames - 1) | ||||||
|         assert im.getpixel((0, 0)) == (128, 191) |         assert im.getpixel((0, 0)) == (128, 191) | ||||||
|         assert im.getpixel((64, 32)) == (128, 191) |         assert im.getpixel((64, 32)) == (128, 191) | ||||||
| 
 | 
 | ||||||
|     with Image.open("Tests/images/apng/mode_palette.png") as im: |     with Image.open("Tests/images/apng/mode_palette.png") as im: | ||||||
|  |         assert isinstance(im, PngImagePlugin.PngImageFile) | ||||||
|         assert im.mode == "P" |         assert im.mode == "P" | ||||||
|         im.seek(im.n_frames - 1) |         im.seek(im.n_frames - 1) | ||||||
|         im = im.convert("RGB") |         im = im.convert("RGB") | ||||||
|  | @ -259,6 +283,7 @@ def test_apng_mode() -> None: | ||||||
|         assert im.getpixel((64, 32)) == (0, 255, 0) |         assert im.getpixel((64, 32)) == (0, 255, 0) | ||||||
| 
 | 
 | ||||||
|     with Image.open("Tests/images/apng/mode_palette_alpha.png") as im: |     with Image.open("Tests/images/apng/mode_palette_alpha.png") as im: | ||||||
|  |         assert isinstance(im, PngImagePlugin.PngImageFile) | ||||||
|         assert im.mode == "P" |         assert im.mode == "P" | ||||||
|         im.seek(im.n_frames - 1) |         im.seek(im.n_frames - 1) | ||||||
|         im = im.convert("RGBA") |         im = im.convert("RGBA") | ||||||
|  | @ -266,6 +291,7 @@ def test_apng_mode() -> None: | ||||||
|         assert im.getpixel((64, 32)) == (0, 255, 0, 255) |         assert im.getpixel((64, 32)) == (0, 255, 0, 255) | ||||||
| 
 | 
 | ||||||
|     with Image.open("Tests/images/apng/mode_palette_1bit_alpha.png") as im: |     with Image.open("Tests/images/apng/mode_palette_1bit_alpha.png") as im: | ||||||
|  |         assert isinstance(im, PngImagePlugin.PngImageFile) | ||||||
|         assert im.mode == "P" |         assert im.mode == "P" | ||||||
|         im.seek(im.n_frames - 1) |         im.seek(im.n_frames - 1) | ||||||
|         im = im.convert("RGBA") |         im = im.convert("RGBA") | ||||||
|  | @ -275,25 +301,31 @@ def test_apng_mode() -> None: | ||||||
| 
 | 
 | ||||||
| def test_apng_chunk_errors() -> None: | def test_apng_chunk_errors() -> None: | ||||||
|     with Image.open("Tests/images/apng/chunk_no_actl.png") as im: |     with Image.open("Tests/images/apng/chunk_no_actl.png") as im: | ||||||
|  |         assert isinstance(im, PngImagePlugin.PngImageFile) | ||||||
|         assert not im.is_animated |         assert not im.is_animated | ||||||
| 
 | 
 | ||||||
|     with pytest.warns(UserWarning): |     with pytest.warns(UserWarning): | ||||||
|         with Image.open("Tests/images/apng/chunk_multi_actl.png") as im: |         with Image.open("Tests/images/apng/chunk_multi_actl.png") as im: | ||||||
|             im.load() |             im.load() | ||||||
|  |         assert isinstance(im, PngImagePlugin.PngImageFile) | ||||||
|         assert not im.is_animated |         assert not im.is_animated | ||||||
| 
 | 
 | ||||||
|     with Image.open("Tests/images/apng/chunk_actl_after_idat.png") as im: |     with Image.open("Tests/images/apng/chunk_actl_after_idat.png") as im: | ||||||
|  |         assert isinstance(im, PngImagePlugin.PngImageFile) | ||||||
|         assert not im.is_animated |         assert not im.is_animated | ||||||
| 
 | 
 | ||||||
|     with Image.open("Tests/images/apng/chunk_no_fctl.png") as im: |     with Image.open("Tests/images/apng/chunk_no_fctl.png") as im: | ||||||
|  |         assert isinstance(im, PngImagePlugin.PngImageFile) | ||||||
|         with pytest.raises(SyntaxError): |         with pytest.raises(SyntaxError): | ||||||
|             im.seek(im.n_frames - 1) |             im.seek(im.n_frames - 1) | ||||||
| 
 | 
 | ||||||
|     with Image.open("Tests/images/apng/chunk_repeat_fctl.png") as im: |     with Image.open("Tests/images/apng/chunk_repeat_fctl.png") as im: | ||||||
|  |         assert isinstance(im, PngImagePlugin.PngImageFile) | ||||||
|         with pytest.raises(SyntaxError): |         with pytest.raises(SyntaxError): | ||||||
|             im.seek(im.n_frames - 1) |             im.seek(im.n_frames - 1) | ||||||
| 
 | 
 | ||||||
|     with Image.open("Tests/images/apng/chunk_no_fdat.png") as im: |     with Image.open("Tests/images/apng/chunk_no_fdat.png") as im: | ||||||
|  |         assert isinstance(im, PngImagePlugin.PngImageFile) | ||||||
|         with pytest.raises(SyntaxError): |         with pytest.raises(SyntaxError): | ||||||
|             im.seek(im.n_frames - 1) |             im.seek(im.n_frames - 1) | ||||||
| 
 | 
 | ||||||
|  | @ -301,26 +333,31 @@ def test_apng_chunk_errors() -> None: | ||||||
| def test_apng_syntax_errors() -> None: | def test_apng_syntax_errors() -> None: | ||||||
|     with pytest.warns(UserWarning): |     with pytest.warns(UserWarning): | ||||||
|         with Image.open("Tests/images/apng/syntax_num_frames_zero.png") as im: |         with Image.open("Tests/images/apng/syntax_num_frames_zero.png") as im: | ||||||
|  |             assert isinstance(im, PngImagePlugin.PngImageFile) | ||||||
|             assert not im.is_animated |             assert not im.is_animated | ||||||
|             with pytest.raises(OSError): |             with pytest.raises(OSError): | ||||||
|                 im.load() |                 im.load() | ||||||
| 
 | 
 | ||||||
|     with pytest.warns(UserWarning): |     with pytest.warns(UserWarning): | ||||||
|         with Image.open("Tests/images/apng/syntax_num_frames_zero_default.png") as im: |         with Image.open("Tests/images/apng/syntax_num_frames_zero_default.png") as im: | ||||||
|  |             assert isinstance(im, PngImagePlugin.PngImageFile) | ||||||
|             assert not im.is_animated |             assert not im.is_animated | ||||||
|             im.load() |             im.load() | ||||||
| 
 | 
 | ||||||
|     # we can handle this case gracefully |     # we can handle this case gracefully | ||||||
|     with Image.open("Tests/images/apng/syntax_num_frames_low.png") as im: |     with Image.open("Tests/images/apng/syntax_num_frames_low.png") as im: | ||||||
|  |         assert isinstance(im, PngImagePlugin.PngImageFile) | ||||||
|         im.seek(im.n_frames - 1) |         im.seek(im.n_frames - 1) | ||||||
| 
 | 
 | ||||||
|     with pytest.raises(OSError): |     with pytest.raises(OSError): | ||||||
|         with Image.open("Tests/images/apng/syntax_num_frames_high.png") as im: |         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.seek(im.n_frames - 1) | ||||||
|             im.load() |             im.load() | ||||||
| 
 | 
 | ||||||
|     with pytest.warns(UserWarning): |     with pytest.warns(UserWarning): | ||||||
|         with Image.open("Tests/images/apng/syntax_num_frames_invalid.png") as im: |         with Image.open("Tests/images/apng/syntax_num_frames_invalid.png") as im: | ||||||
|  |             assert isinstance(im, PngImagePlugin.PngImageFile) | ||||||
|             assert not im.is_animated |             assert not im.is_animated | ||||||
|             im.load() |             im.load() | ||||||
| 
 | 
 | ||||||
|  | @ -340,6 +377,7 @@ def test_apng_syntax_errors() -> None: | ||||||
| def test_apng_sequence_errors(test_file: str) -> None: | def test_apng_sequence_errors(test_file: str) -> None: | ||||||
|     with pytest.raises(SyntaxError): |     with pytest.raises(SyntaxError): | ||||||
|         with Image.open(f"Tests/images/apng/{test_file}") as im: |         with Image.open(f"Tests/images/apng/{test_file}") as im: | ||||||
|  |             assert isinstance(im, PngImagePlugin.PngImageFile) | ||||||
|             im.seek(im.n_frames - 1) |             im.seek(im.n_frames - 1) | ||||||
|             im.load() |             im.load() | ||||||
| 
 | 
 | ||||||
|  | @ -350,6 +388,7 @@ def test_apng_save(tmp_path: Path) -> None: | ||||||
|         im.save(test_file, save_all=True) |         im.save(test_file, save_all=True) | ||||||
| 
 | 
 | ||||||
|     with Image.open(test_file) as im: |     with Image.open(test_file) as im: | ||||||
|  |         assert isinstance(im, PngImagePlugin.PngImageFile) | ||||||
|         im.load() |         im.load() | ||||||
|         assert not im.is_animated |         assert not im.is_animated | ||||||
|         assert im.n_frames == 1 |         assert im.n_frames == 1 | ||||||
|  | @ -365,6 +404,7 @@ def test_apng_save(tmp_path: Path) -> None: | ||||||
|         ) |         ) | ||||||
| 
 | 
 | ||||||
|     with Image.open(test_file) as im: |     with Image.open(test_file) as im: | ||||||
|  |         assert isinstance(im, PngImagePlugin.PngImageFile) | ||||||
|         im.load() |         im.load() | ||||||
|         assert im.is_animated |         assert im.is_animated | ||||||
|         assert im.n_frames == 2 |         assert im.n_frames == 2 | ||||||
|  | @ -404,6 +444,7 @@ def test_apng_save_split_fdat(tmp_path: Path) -> None: | ||||||
|             append_images=frames, |             append_images=frames, | ||||||
|         ) |         ) | ||||||
|     with Image.open(test_file) as im: |     with Image.open(test_file) as im: | ||||||
|  |         assert isinstance(im, PngImagePlugin.PngImageFile) | ||||||
|         im.seek(im.n_frames - 1) |         im.seek(im.n_frames - 1) | ||||||
|         im.load() |         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] |         test_file, save_all=True, append_images=[frame, frame], duration=[500, 100, 150] | ||||||
|     ) |     ) | ||||||
|     with Image.open(test_file) as im: |     with Image.open(test_file) as im: | ||||||
|  |         assert isinstance(im, PngImagePlugin.PngImageFile) | ||||||
|         assert im.n_frames == 1 |         assert im.n_frames == 1 | ||||||
|         assert "duration" not in im.info |         assert "duration" not in im.info | ||||||
| 
 | 
 | ||||||
|  | @ -457,6 +499,7 @@ def test_apng_save_duration_loop(tmp_path: Path) -> None: | ||||||
|         duration=[500, 100, 150], |         duration=[500, 100, 150], | ||||||
|     ) |     ) | ||||||
|     with Image.open(test_file) as im: |     with Image.open(test_file) as im: | ||||||
|  |         assert isinstance(im, PngImagePlugin.PngImageFile) | ||||||
|         assert im.n_frames == 2 |         assert im.n_frames == 2 | ||||||
|         assert im.info["duration"] == 600 |         assert im.info["duration"] == 600 | ||||||
| 
 | 
 | ||||||
|  | @ -467,6 +510,7 @@ def test_apng_save_duration_loop(tmp_path: Path) -> None: | ||||||
|     frame.info["duration"] = 300 |     frame.info["duration"] = 300 | ||||||
|     frame.save(test_file, save_all=True, append_images=[frame, different_frame]) |     frame.save(test_file, save_all=True, append_images=[frame, different_frame]) | ||||||
|     with Image.open(test_file) as im: |     with Image.open(test_file) as im: | ||||||
|  |         assert isinstance(im, PngImagePlugin.PngImageFile) | ||||||
|         assert im.n_frames == 2 |         assert im.n_frames == 2 | ||||||
|         assert im.info["duration"] == 600 |         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,25 +15,19 @@ from .helper import ( | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def test_sanity(tmp_path: Path) -> None: | @pytest.mark.parametrize("mode", ("1", "L", "P", "RGB")) | ||||||
|     def roundtrip(im: Image.Image) -> None: | def test_sanity(mode: str, tmp_path: Path) -> None: | ||||||
|         outfile = tmp_path / "temp.bmp" |     outfile = tmp_path / "temp.bmp" | ||||||
| 
 | 
 | ||||||
|         im.save(outfile, "BMP") |     im = hopper(mode) | ||||||
|  |     im.save(outfile, "BMP") | ||||||
| 
 | 
 | ||||||
|         with Image.open(outfile) as reloaded: |     with Image.open(outfile) as reloaded: | ||||||
|             reloaded.load() |         reloaded.load() | ||||||
|             assert im.mode == reloaded.mode |         assert im.mode == reloaded.mode | ||||||
|             assert im.size == reloaded.size |         assert im.size == reloaded.size | ||||||
|             assert reloaded.format == "BMP" |         assert reloaded.format == "BMP" | ||||||
|             assert reloaded.get_format_mimetype() == "image/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: | def test_invalid_file() -> None: | ||||||
|  | @ -196,9 +190,9 @@ def test_rle8() -> None: | ||||||
|     # Signal end of bitmap before the image is finished |     # Signal end of bitmap before the image is finished | ||||||
|     with open("Tests/images/bmp/g/pal8rle.bmp", "rb") as fp: |     with open("Tests/images/bmp/g/pal8rle.bmp", "rb") as fp: | ||||||
|         data = fp.read(1063) + b"\x01" |         data = fp.read(1063) + b"\x01" | ||||||
|         with Image.open(io.BytesIO(data)) as im: |     with Image.open(io.BytesIO(data)) as im: | ||||||
|             with pytest.raises(ValueError): |         with pytest.raises(ValueError): | ||||||
|                 im.load() |             im.load() | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def test_rle4() -> None: | def test_rle4() -> None: | ||||||
|  | @ -220,9 +214,9 @@ def test_rle4() -> None: | ||||||
| def test_rle8_eof(file_name: str, length: int) -> None: | def test_rle8_eof(file_name: str, length: int) -> None: | ||||||
|     with open(file_name, "rb") as fp: |     with open(file_name, "rb") as fp: | ||||||
|         data = fp.read(length) |         data = fp.read(length) | ||||||
|         with Image.open(io.BytesIO(data)) as im: |     with Image.open(io.BytesIO(data)) as im: | ||||||
|             with pytest.raises(ValueError): |         with pytest.raises(ValueError): | ||||||
|                 im.load() |             im.load() | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def test_offset() -> None: | def test_offset() -> None: | ||||||
|  | @ -230,3 +224,13 @@ def test_offset() -> None: | ||||||
|     # to exclude the palette size from the pixel data offset |     # to exclude the palette size from the pixel data offset | ||||||
|     with Image.open("Tests/images/pal8_offset.bmp") as im: |     with Image.open("Tests/images/pal8_offset.bmp") as im: | ||||||
|         assert_image_equal_tofile(im, "Tests/images/bmp/g/pal8.bmp") |         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: | def test_n_frames() -> None: | ||||||
|     with Image.open(TEST_FILE) as im: |     with Image.open(TEST_FILE) as im: | ||||||
|  |         assert isinstance(im, DcxImagePlugin.DcxImageFile) | ||||||
|         assert im.n_frames == 1 |         assert im.n_frames == 1 | ||||||
|         assert not im.is_animated |         assert not im.is_animated | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def test_eoferror() -> None: | def test_eoferror() -> None: | ||||||
|     with Image.open(TEST_FILE) as im: |     with Image.open(TEST_FILE) as im: | ||||||
|  |         assert isinstance(im, DcxImagePlugin.DcxImageFile) | ||||||
|         n_frames = im.n_frames |         n_frames = im.n_frames | ||||||
| 
 | 
 | ||||||
|         # Test seeking past the last frame |         # 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: | def test_sanity(filename: str, size: tuple[int, int], scale: int) -> None: | ||||||
|     expected_size = tuple(s * scale for s in size) |     expected_size = tuple(s * scale for s in size) | ||||||
|     with Image.open(filename) as image: |     with Image.open(filename) as image: | ||||||
|  |         assert isinstance(image, EpsImagePlugin.EpsImageFile) | ||||||
|  | 
 | ||||||
|         image.load(scale=scale) |         image.load(scale=scale) | ||||||
|         assert image.mode == "RGB" |         assert image.mode == "RGB" | ||||||
|         assert image.size == expected_size |         assert image.size == expected_size | ||||||
|  | @ -227,6 +229,8 @@ def test_showpage() -> None: | ||||||
| @pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") | @pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") | ||||||
| def test_transparency() -> None: | def test_transparency() -> None: | ||||||
|     with Image.open("Tests/images/eps/reqd_showpage.eps") as plot_image: |     with Image.open("Tests/images/eps/reqd_showpage.eps") as plot_image: | ||||||
|  |         assert isinstance(plot_image, EpsImagePlugin.EpsImageFile) | ||||||
|  | 
 | ||||||
|         plot_image.load(transparency=True) |         plot_image.load(transparency=True) | ||||||
|         assert plot_image.mode == "RGBA" |         assert plot_image.mode == "RGBA" | ||||||
| 
 | 
 | ||||||
|  | @ -308,6 +312,7 @@ def test_render_scale2() -> None: | ||||||
| 
 | 
 | ||||||
|     # Zero bounding box |     # Zero bounding box | ||||||
|     with Image.open(FILE1) as image1_scale2: |     with Image.open(FILE1) as image1_scale2: | ||||||
|  |         assert isinstance(image1_scale2, EpsImagePlugin.EpsImageFile) | ||||||
|         image1_scale2.load(scale=2) |         image1_scale2.load(scale=2) | ||||||
|         with Image.open(FILE1_COMPARE_SCALE2) as image1_scale2_compare: |         with Image.open(FILE1_COMPARE_SCALE2) as image1_scale2_compare: | ||||||
|             image1_scale2_compare = image1_scale2_compare.convert("RGB") |             image1_scale2_compare = image1_scale2_compare.convert("RGB") | ||||||
|  | @ -316,6 +321,7 @@ def test_render_scale2() -> None: | ||||||
| 
 | 
 | ||||||
|     # Non-zero bounding box |     # Non-zero bounding box | ||||||
|     with Image.open(FILE2) as image2_scale2: |     with Image.open(FILE2) as image2_scale2: | ||||||
|  |         assert isinstance(image2_scale2, EpsImagePlugin.EpsImageFile) | ||||||
|         image2_scale2.load(scale=2) |         image2_scale2.load(scale=2) | ||||||
|         with Image.open(FILE2_COMPARE_SCALE2) as image2_scale2_compare: |         with Image.open(FILE2_COMPARE_SCALE2) as image2_scale2_compare: | ||||||
|             image2_scale2_compare = image2_scale2_compare.convert("RGB") |             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: | def test_sanity() -> None: | ||||||
|     with Image.open(static_test_file) as im: |     with Image.open(static_test_file) as im: | ||||||
|  |         assert isinstance(im, FliImagePlugin.FliImageFile) | ||||||
|  | 
 | ||||||
|         im.load() |         im.load() | ||||||
|         assert im.mode == "P" |         assert im.mode == "P" | ||||||
|         assert im.size == (128, 128) |         assert im.size == (128, 128) | ||||||
|  | @ -29,6 +31,8 @@ def test_sanity() -> None: | ||||||
|         assert not im.is_animated |         assert not im.is_animated | ||||||
| 
 | 
 | ||||||
|     with Image.open(animated_test_file) as im: |     with Image.open(animated_test_file) as im: | ||||||
|  |         assert isinstance(im, FliImagePlugin.FliImageFile) | ||||||
|  | 
 | ||||||
|         assert im.mode == "P" |         assert im.mode == "P" | ||||||
|         assert im.size == (320, 200) |         assert im.size == (320, 200) | ||||||
|         assert im.format == "FLI" |         assert im.format == "FLI" | ||||||
|  | @ -112,16 +116,19 @@ def test_palette_chunk_second() -> None: | ||||||
| 
 | 
 | ||||||
| def test_n_frames() -> None: | def test_n_frames() -> None: | ||||||
|     with Image.open(static_test_file) as im: |     with Image.open(static_test_file) as im: | ||||||
|  |         assert isinstance(im, FliImagePlugin.FliImageFile) | ||||||
|         assert im.n_frames == 1 |         assert im.n_frames == 1 | ||||||
|         assert not im.is_animated |         assert not im.is_animated | ||||||
| 
 | 
 | ||||||
|     with Image.open(animated_test_file) as im: |     with Image.open(animated_test_file) as im: | ||||||
|  |         assert isinstance(im, FliImagePlugin.FliImageFile) | ||||||
|         assert im.n_frames == 384 |         assert im.n_frames == 384 | ||||||
|         assert im.is_animated |         assert im.is_animated | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def test_eoferror() -> None: | def test_eoferror() -> None: | ||||||
|     with Image.open(animated_test_file) as im: |     with Image.open(animated_test_file) as im: | ||||||
|  |         assert isinstance(im, FliImagePlugin.FliImageFile) | ||||||
|         n_frames = im.n_frames |         n_frames = im.n_frames | ||||||
| 
 | 
 | ||||||
|         # Test seeking past the last frame |         # Test seeking past the last frame | ||||||
|  | @ -166,6 +173,7 @@ def test_seek_tell() -> None: | ||||||
| 
 | 
 | ||||||
| def test_seek() -> None: | def test_seek() -> None: | ||||||
|     with Image.open(animated_test_file) as im: |     with Image.open(animated_test_file) as im: | ||||||
|  |         assert isinstance(im, FliImagePlugin.FliImageFile) | ||||||
|         im.seek(50) |         im.seek(50) | ||||||
| 
 | 
 | ||||||
|         assert_image_equal_tofile(im, "Tests/images/a_fli.png") |         assert_image_equal_tofile(im, "Tests/images/a_fli.png") | ||||||
|  |  | ||||||
|  | @ -22,10 +22,11 @@ def test_sanity() -> None: | ||||||
| 
 | 
 | ||||||
| def test_close() -> None: | def test_close() -> None: | ||||||
|     with Image.open("Tests/images/input_bw_one_band.fpx") as im: |     with Image.open("Tests/images/input_bw_one_band.fpx") as im: | ||||||
|         pass |         assert isinstance(im, FpxImagePlugin.FpxImageFile) | ||||||
|     assert im.ole.fp.closed |     assert im.ole.fp.closed | ||||||
| 
 | 
 | ||||||
|     im = Image.open("Tests/images/input_bw_one_band.fpx") |     im = Image.open("Tests/images/input_bw_one_band.fpx") | ||||||
|  |     assert isinstance(im, FpxImagePlugin.FpxImageFile) | ||||||
|     im.close() |     im.close() | ||||||
|     assert im.ole.fp.closed |     assert im.ole.fp.closed | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -450,6 +450,7 @@ def test_save_netpbm_l_mode(tmp_path: Path) -> None: | ||||||
| 
 | 
 | ||||||
| def test_seek() -> None: | def test_seek() -> None: | ||||||
|     with Image.open("Tests/images/dispose_none.gif") as img: |     with Image.open("Tests/images/dispose_none.gif") as img: | ||||||
|  |         assert isinstance(img, GifImagePlugin.GifImageFile) | ||||||
|         frame_count = 0 |         frame_count = 0 | ||||||
|         try: |         try: | ||||||
|             while True: |             while True: | ||||||
|  | @ -494,10 +495,12 @@ def test_seek_rewind() -> None: | ||||||
| def test_n_frames(path: str, n_frames: int) -> None: | def test_n_frames(path: str, n_frames: int) -> None: | ||||||
|     # Test is_animated before n_frames |     # Test is_animated before n_frames | ||||||
|     with Image.open(path) as im: |     with Image.open(path) as im: | ||||||
|  |         assert isinstance(im, GifImagePlugin.GifImageFile) | ||||||
|         assert im.is_animated == (n_frames != 1) |         assert im.is_animated == (n_frames != 1) | ||||||
| 
 | 
 | ||||||
|     # Test is_animated after n_frames |     # Test is_animated after n_frames | ||||||
|     with Image.open(path) as im: |     with Image.open(path) as im: | ||||||
|  |         assert isinstance(im, GifImagePlugin.GifImageFile) | ||||||
|         assert im.n_frames == n_frames |         assert im.n_frames == n_frames | ||||||
|         assert im.is_animated == (n_frames != 1) |         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: |     with Image.open("Tests/images/dispose_bgnd.gif") as im: | ||||||
|         im.seek(1) |         im.seek(1) | ||||||
|         expected = im.copy() |         expected = im.copy() | ||||||
|  |         assert isinstance(im, GifImagePlugin.GifImageFile) | ||||||
|         assert im.n_frames == 5 |         assert im.n_frames == 5 | ||||||
|         assert_image_equal(im, expected) |         assert_image_equal(im, expected) | ||||||
| 
 | 
 | ||||||
|  | @ -514,17 +518,20 @@ def test_no_change() -> None: | ||||||
|     with Image.open("Tests/images/dispose_bgnd.gif") as im: |     with Image.open("Tests/images/dispose_bgnd.gif") as im: | ||||||
|         im.seek(3) |         im.seek(3) | ||||||
|         expected = im.copy() |         expected = im.copy() | ||||||
|  |         assert isinstance(im, GifImagePlugin.GifImageFile) | ||||||
|         assert im.is_animated |         assert im.is_animated | ||||||
|         assert_image_equal(im, expected) |         assert_image_equal(im, expected) | ||||||
| 
 | 
 | ||||||
|     with Image.open("Tests/images/comment_after_only_frame.gif") as im: |     with Image.open("Tests/images/comment_after_only_frame.gif") as im: | ||||||
|         expected = Image.new("P", (1, 1)) |         expected = Image.new("P", (1, 1)) | ||||||
|  |         assert isinstance(im, GifImagePlugin.GifImageFile) | ||||||
|         assert not im.is_animated |         assert not im.is_animated | ||||||
|         assert_image_equal(im, expected) |         assert_image_equal(im, expected) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def test_eoferror() -> None: | def test_eoferror() -> None: | ||||||
|     with Image.open(TEST_GIF) as im: |     with Image.open(TEST_GIF) as im: | ||||||
|  |         assert isinstance(im, GifImagePlugin.GifImageFile) | ||||||
|         n_frames = im.n_frames |         n_frames = im.n_frames | ||||||
| 
 | 
 | ||||||
|         # Test seeking past the last frame |         # Test seeking past the last frame | ||||||
|  | @ -543,6 +550,7 @@ def test_first_frame_transparency() -> None: | ||||||
| 
 | 
 | ||||||
| def test_dispose_none() -> None: | def test_dispose_none() -> None: | ||||||
|     with Image.open("Tests/images/dispose_none.gif") as img: |     with Image.open("Tests/images/dispose_none.gif") as img: | ||||||
|  |         assert isinstance(img, GifImagePlugin.GifImageFile) | ||||||
|         try: |         try: | ||||||
|             while True: |             while True: | ||||||
|                 img.seek(img.tell() + 1) |                 img.seek(img.tell() + 1) | ||||||
|  | @ -566,6 +574,7 @@ def test_dispose_none_load_end() -> None: | ||||||
| 
 | 
 | ||||||
| def test_dispose_background() -> None: | def test_dispose_background() -> None: | ||||||
|     with Image.open("Tests/images/dispose_bgnd.gif") as img: |     with Image.open("Tests/images/dispose_bgnd.gif") as img: | ||||||
|  |         assert isinstance(img, GifImagePlugin.GifImageFile) | ||||||
|         try: |         try: | ||||||
|             while True: |             while True: | ||||||
|                 img.seek(img.tell() + 1) |                 img.seek(img.tell() + 1) | ||||||
|  | @ -619,6 +628,7 @@ def test_transparent_dispose( | ||||||
| 
 | 
 | ||||||
| def test_dispose_previous() -> None: | def test_dispose_previous() -> None: | ||||||
|     with Image.open("Tests/images/dispose_prev.gif") as img: |     with Image.open("Tests/images/dispose_prev.gif") as img: | ||||||
|  |         assert isinstance(img, GifImagePlugin.GifImageFile) | ||||||
|         try: |         try: | ||||||
|             while True: |             while True: | ||||||
|                 img.seek(img.tell() + 1) |                 img.seek(img.tell() + 1) | ||||||
|  | @ -656,6 +666,7 @@ def test_save_dispose(tmp_path: Path) -> None: | ||||||
|     for method in range(4): |     for method in range(4): | ||||||
|         im_list[0].save(out, save_all=True, append_images=im_list[1:], disposal=method) |         im_list[0].save(out, save_all=True, append_images=im_list[1:], disposal=method) | ||||||
|         with Image.open(out) as img: |         with Image.open(out) as img: | ||||||
|  |             assert isinstance(img, GifImagePlugin.GifImageFile) | ||||||
|             for _ in range(2): |             for _ in range(2): | ||||||
|                 img.seek(img.tell() + 1) |                 img.seek(img.tell() + 1) | ||||||
|                 assert img.disposal_method == method |                 assert img.disposal_method == method | ||||||
|  | @ -669,6 +680,7 @@ def test_save_dispose(tmp_path: Path) -> None: | ||||||
|     ) |     ) | ||||||
| 
 | 
 | ||||||
|     with Image.open(out) as img: |     with Image.open(out) as img: | ||||||
|  |         assert isinstance(img, GifImagePlugin.GifImageFile) | ||||||
|         for i in range(2): |         for i in range(2): | ||||||
|             img.seek(img.tell() + 1) |             img.seek(img.tell() + 1) | ||||||
|             assert img.disposal_method == i + 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) |     im_list[0].save(out, save_all=True, append_images=im_list[1:], disposal=2) | ||||||
| 
 | 
 | ||||||
|     with Image.open(out) as im: |     with Image.open(out) as im: | ||||||
|  |         assert isinstance(im, GifImagePlugin.GifImageFile) | ||||||
|         assert im.n_frames == 3 |         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 |         out, save_all=True, append_images=im_list[1:], duration=duration_list | ||||||
|     ) |     ) | ||||||
|     with Image.open(out) as reread: |     with Image.open(out) as reread: | ||||||
|  |         assert isinstance(reread, GifImagePlugin.GifImageFile) | ||||||
|  | 
 | ||||||
|         # Assert that the first three frames were combined |         # Assert that the first three frames were combined | ||||||
|         assert reread.n_frames == 2 |         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) |     im_list[0].save(out, save_all=True, append_images=im_list[1:], duration=duration) | ||||||
|     with Image.open(out) as reread: |     with Image.open(out) as reread: | ||||||
|  |         assert isinstance(reread, GifImagePlugin.GifImageFile) | ||||||
|  | 
 | ||||||
|         # Assert that all frames were combined |         # Assert that all frames were combined | ||||||
|         assert reread.n_frames == 1 |         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) |     im.copy().save(out, save_all=True, append_images=ims) | ||||||
| 
 | 
 | ||||||
|     with Image.open(out) as reread: |     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 |         assert reread.n_frames == 3 | ||||||
| 
 | 
 | ||||||
|     # Tests appending using a generator |     # 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)) |     im.save(out, save_all=True, append_images=im_generator(ims)) | ||||||
| 
 | 
 | ||||||
|     with Image.open(out) as reread: |     with Image.open(out) as reread: | ||||||
|  |         assert isinstance(reread, GifImagePlugin.GifImageFile) | ||||||
|         assert reread.n_frames == 3 |         assert reread.n_frames == 3 | ||||||
| 
 | 
 | ||||||
|     # Tests appending single and multiple frame images |     # 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]) |             im.save(out, save_all=True, append_images=[im2]) | ||||||
| 
 | 
 | ||||||
|     with Image.open(out) as reread: |     with Image.open(out) as reread: | ||||||
|  |         assert isinstance(reread, GifImagePlugin.GifImageFile) | ||||||
|         assert reread.n_frames == 10 |         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) |     im.save(out, save_all=True, append_images=ims) | ||||||
| 
 | 
 | ||||||
|     with Image.open(out) as reread: |     with Image.open(out) as reread: | ||||||
|  |         assert isinstance(reread, GifImagePlugin.GifImageFile) | ||||||
|         assert reread.n_frames == 2 |         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]) |     im.save(out, save_all=True, append_images=[im2]) | ||||||
| 
 | 
 | ||||||
|     with Image.open(out) as reread: |     with Image.open(out) as reread: | ||||||
|  |         assert isinstance(reread, GifImagePlugin.GifImageFile) | ||||||
|         assert reread.n_frames == 2 |         assert reread.n_frames == 2 | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | @ -1467,6 +1496,7 @@ def test_extents( | ||||||
| ) -> None: | ) -> None: | ||||||
|     monkeypatch.setattr(GifImagePlugin, "LOADING_STRATEGY", loading_strategy) |     monkeypatch.setattr(GifImagePlugin, "LOADING_STRATEGY", loading_strategy) | ||||||
|     with Image.open("Tests/images/" + test_file) as im: |     with Image.open("Tests/images/" + test_file) as im: | ||||||
|  |         assert isinstance(im, GifImagePlugin.GifImageFile) | ||||||
|         assert im.size == (100, 100) |         assert im.size == (100, 100) | ||||||
| 
 | 
 | ||||||
|         # Check that n_frames does not change the size |         # 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) |     im1.save(out, save_all=True, append_images=[im2], **params) | ||||||
| 
 | 
 | ||||||
|     with Image.open(out) as reloaded: |     with Image.open(out) as reloaded: | ||||||
|  |         assert isinstance(reloaded, GifImagePlugin.GifImageFile) | ||||||
|         assert reloaded.n_frames == 2 |         assert reloaded.n_frames == 2 | ||||||
|  |  | ||||||
|  | @ -1,5 +1,7 @@ | ||||||
| from __future__ import annotations | from __future__ import annotations | ||||||
| 
 | 
 | ||||||
|  | from io import BytesIO | ||||||
|  | 
 | ||||||
| import pytest | import pytest | ||||||
| 
 | 
 | ||||||
| from PIL.GimpPaletteFile import GimpPaletteFile | from PIL.GimpPaletteFile import GimpPaletteFile | ||||||
|  | @ -14,17 +16,20 @@ def test_sanity() -> None: | ||||||
|             GimpPaletteFile(fp) |             GimpPaletteFile(fp) | ||||||
| 
 | 
 | ||||||
|     with open("Tests/images/bad_palette_file.gpl", "rb") as 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) |             GimpPaletteFile(fp) | ||||||
| 
 | 
 | ||||||
|     with open("Tests/images/bad_palette_entry.gpl", "rb") as 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) |             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 |     # Arrange | ||||||
|     with open("Tests/images/custom_gimp_palette.gpl", "rb") as fp: |     with open("Tests/images/" + filename, "rb") as fp: | ||||||
|         palette_file = GimpPaletteFile(fp) |         palette_file = GimpPaletteFile(fp) | ||||||
| 
 | 
 | ||||||
|     # Act |     # Act | ||||||
|  | @ -32,4 +37,36 @@ def test_get_palette() -> None: | ||||||
| 
 | 
 | ||||||
|     # Assert |     # Assert | ||||||
|     assert mode == "RGB" |     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 |     # Arrange | ||||||
|     with Image.open(TEST_FILE) as im: |     with Image.open(TEST_FILE) as im: | ||||||
|         dummy_fp = BytesIO() |         dummy_fp = BytesIO() | ||||||
|         dummy_filename = "dummy.filename" |         dummy_filename = "dummy.h5" | ||||||
| 
 | 
 | ||||||
|         # Act / Assert: stub cannot save without an implemented handler |         # Act / Assert: stub cannot save without an implemented handler | ||||||
|         with pytest.raises(OSError): |         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) |         assert_image_similar_tofile(im, temp_file, 1) | ||||||
| 
 | 
 | ||||||
|         with Image.open(temp_file) as reread: |         with Image.open(temp_file) as reread: | ||||||
|  |             assert isinstance(reread, IcnsImagePlugin.IcnsImageFile) | ||||||
|             reread.size = (16, 16) |             reread.size = (16, 16) | ||||||
|             reread.load(2) |             reread.load(2) | ||||||
|             assert_image_equal(reread, provided_im) |             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 |     # Check that we can load all of the sizes, and that the final pixel | ||||||
|     # dimensions are as expected |     # dimensions are as expected | ||||||
|     with Image.open(TEST_FILE) as im: |     with Image.open(TEST_FILE) as im: | ||||||
|  |         assert isinstance(im, IcnsImagePlugin.IcnsImageFile) | ||||||
|         for w, h, r in im.info["sizes"]: |         for w, h, r in im.info["sizes"]: | ||||||
|             wr = w * r |             wr = w * r | ||||||
|             hr = h * r |             hr = h * r | ||||||
|  | @ -118,6 +120,7 @@ def test_older_icon() -> None: | ||||||
|             wr = w * r |             wr = w * r | ||||||
|             hr = h * r |             hr = h * r | ||||||
|             with Image.open("Tests/images/pillow2.icns") as im2: |             with Image.open("Tests/images/pillow2.icns") as im2: | ||||||
|  |                 assert isinstance(im2, IcnsImagePlugin.IcnsImageFile) | ||||||
|                 im2.size = (w, h) |                 im2.size = (w, h) | ||||||
|                 im2.load(r) |                 im2.load(r) | ||||||
|                 assert im2.mode == "RGBA" |                 assert im2.mode == "RGBA" | ||||||
|  | @ -135,6 +138,7 @@ def test_jp2_icon() -> None: | ||||||
|             wr = w * r |             wr = w * r | ||||||
|             hr = h * r |             hr = h * r | ||||||
|             with Image.open("Tests/images/pillow3.icns") as im2: |             with Image.open("Tests/images/pillow3.icns") as im2: | ||||||
|  |                 assert isinstance(im2, IcnsImagePlugin.IcnsImageFile) | ||||||
|                 im2.size = (w, h) |                 im2.size = (w, h) | ||||||
|                 im2.load(r) |                 im2.load(r) | ||||||
|                 assert im2.mode == "RGBA" |                 assert im2.mode == "RGBA" | ||||||
|  |  | ||||||
|  | @ -77,6 +77,7 @@ def test_save_to_bytes() -> None: | ||||||
|     # The other one |     # The other one | ||||||
|     output.seek(0) |     output.seek(0) | ||||||
|     with Image.open(output) as reloaded: |     with Image.open(output) as reloaded: | ||||||
|  |         assert isinstance(reloaded, IcoImagePlugin.IcoImageFile) | ||||||
|         reloaded.size = (32, 32) |         reloaded.size = (32, 32) | ||||||
| 
 | 
 | ||||||
|         assert im.mode == reloaded.mode |         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)]) |     im.save(temp_file, "ico", sizes=[(32, 32), (64, 64)]) | ||||||
| 
 | 
 | ||||||
|     with Image.open(temp_file) as reloaded: |     with Image.open(temp_file) as reloaded: | ||||||
|  |         assert isinstance(reloaded, IcoImagePlugin.IcoImageFile) | ||||||
|         reloaded.load() |         reloaded.load() | ||||||
|         reloaded.size = (32, 32) |         reloaded.size = (32, 32) | ||||||
| 
 | 
 | ||||||
|  | @ -167,6 +169,7 @@ def test_save_to_bytes_bmp(mode: str) -> None: | ||||||
|     # The other one |     # The other one | ||||||
|     output.seek(0) |     output.seek(0) | ||||||
|     with Image.open(output) as reloaded: |     with Image.open(output) as reloaded: | ||||||
|  |         assert isinstance(reloaded, IcoImagePlugin.IcoImageFile) | ||||||
|         reloaded.size = (32, 32) |         reloaded.size = (32, 32) | ||||||
| 
 | 
 | ||||||
|         assert "RGBA" == reloaded.mode |         assert "RGBA" == reloaded.mode | ||||||
|  | @ -178,6 +181,7 @@ def test_save_to_bytes_bmp(mode: str) -> None: | ||||||
| 
 | 
 | ||||||
| def test_incorrect_size() -> None: | def test_incorrect_size() -> None: | ||||||
|     with Image.open(TEST_ICO_FILE) as im: |     with Image.open(TEST_ICO_FILE) as im: | ||||||
|  |         assert isinstance(im, IcoImagePlugin.IcoImageFile) | ||||||
|         with pytest.raises(ValueError): |         with pytest.raises(ValueError): | ||||||
|             im.size = (1, 1) |             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]) |     im.save(outfile, sizes=[(32, 32), (128, 128)], append_images=[provided_im]) | ||||||
| 
 | 
 | ||||||
|     with Image.open(outfile) as reread: |     with Image.open(outfile) as reread: | ||||||
|  |         assert isinstance(reread, IcoImagePlugin.IcoImageFile) | ||||||
|         assert_image_equal(reread, hopper("RGBA")) |         assert_image_equal(reread, hopper("RGBA")) | ||||||
| 
 | 
 | ||||||
|         reread.size = (32, 32) |         reread.size = (32, 32) | ||||||
|  |  | ||||||
|  | @ -68,12 +68,14 @@ def test_tell() -> None: | ||||||
| 
 | 
 | ||||||
| def test_n_frames() -> None: | def test_n_frames() -> None: | ||||||
|     with Image.open(TEST_IM) as im: |     with Image.open(TEST_IM) as im: | ||||||
|  |         assert isinstance(im, ImImagePlugin.ImImageFile) | ||||||
|         assert im.n_frames == 1 |         assert im.n_frames == 1 | ||||||
|         assert not im.is_animated |         assert not im.is_animated | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def test_eoferror() -> None: | def test_eoferror() -> None: | ||||||
|     with Image.open(TEST_IM) as im: |     with Image.open(TEST_IM) as im: | ||||||
|  |         assert isinstance(im, ImImagePlugin.ImImageFile) | ||||||
|         n_frames = im.n_frames |         n_frames = im.n_frames | ||||||
| 
 | 
 | ||||||
|         # Test seeking past the last frame |         # Test seeking past the last frame | ||||||
|  |  | ||||||
|  | @ -91,6 +91,7 @@ class TestFileJpeg: | ||||||
|     def test_app(self) -> None: |     def test_app(self) -> None: | ||||||
|         # Test APP/COM reader (@PIL135) |         # Test APP/COM reader (@PIL135) | ||||||
|         with Image.open(TEST_FILE) as im: |         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[0] == ("APP0", b"JFIF\x00\x01\x01\x01\x00`\x00`\x00\x00") | ||||||
|             assert im.applist[1] == ( |             assert im.applist[1] == ( | ||||||
|                 "COM", |                 "COM", | ||||||
|  | @ -316,6 +317,8 @@ class TestFileJpeg: | ||||||
| 
 | 
 | ||||||
|     def test_exif_typeerror(self) -> None: |     def test_exif_typeerror(self) -> None: | ||||||
|         with Image.open("Tests/images/exif_typeerror.jpg") as im: |         with Image.open("Tests/images/exif_typeerror.jpg") as im: | ||||||
|  |             assert isinstance(im, JpegImagePlugin.JpegImageFile) | ||||||
|  | 
 | ||||||
|             # Should not raise a TypeError |             # Should not raise a TypeError | ||||||
|             im._getexif() |             im._getexif() | ||||||
| 
 | 
 | ||||||
|  | @ -500,6 +503,7 @@ class TestFileJpeg: | ||||||
| 
 | 
 | ||||||
|     def test_mp(self) -> None: |     def test_mp(self) -> None: | ||||||
|         with Image.open("Tests/images/pil_sample_rgb.jpg") as im: |         with Image.open("Tests/images/pil_sample_rgb.jpg") as im: | ||||||
|  |             assert isinstance(im, JpegImagePlugin.JpegImageFile) | ||||||
|             assert im._getmp() is None |             assert im._getmp() is None | ||||||
| 
 | 
 | ||||||
|     def test_quality_keep(self, tmp_path: Path) -> None: |     def test_quality_keep(self, tmp_path: Path) -> None: | ||||||
|  | @ -558,12 +562,14 @@ class TestFileJpeg: | ||||||
|             with Image.open(test_file) as im: |             with Image.open(test_file) as im: | ||||||
|                 im.save(b, "JPEG", qtables=[[n] * 64] * n) |                 im.save(b, "JPEG", qtables=[[n] * 64] * n) | ||||||
|             with Image.open(b) as im: |             with Image.open(b) as im: | ||||||
|  |                 assert isinstance(im, JpegImagePlugin.JpegImageFile) | ||||||
|                 assert len(im.quantization) == n |                 assert len(im.quantization) == n | ||||||
|                 reloaded = self.roundtrip(im, qtables="keep") |                 reloaded = self.roundtrip(im, qtables="keep") | ||||||
|                 assert im.quantization == reloaded.quantization |                 assert im.quantization == reloaded.quantization | ||||||
|                 assert max(reloaded.quantization[0]) <= 255 |                 assert max(reloaded.quantization[0]) <= 255 | ||||||
| 
 | 
 | ||||||
|         with Image.open("Tests/images/hopper.jpg") as im: |         with Image.open("Tests/images/hopper.jpg") as im: | ||||||
|  |             assert isinstance(im, JpegImagePlugin.JpegImageFile) | ||||||
|             qtables = im.quantization |             qtables = im.quantization | ||||||
|             reloaded = self.roundtrip(im, qtables=qtables, subsampling=0) |             reloaded = self.roundtrip(im, qtables=qtables, subsampling=0) | ||||||
|             assert im.quantization == reloaded.quantization |             assert im.quantization == reloaded.quantization | ||||||
|  | @ -663,6 +669,7 @@ class TestFileJpeg: | ||||||
| 
 | 
 | ||||||
|     def test_load_16bit_qtables(self) -> None: |     def test_load_16bit_qtables(self) -> None: | ||||||
|         with Image.open("Tests/images/hopper_16bit_qtables.jpg") as im: |         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) == 2 | ||||||
|             assert len(im.quantization[0]) == 64 |             assert len(im.quantization[0]) == 64 | ||||||
|             assert max(im.quantization[0]) > 255 |             assert max(im.quantization[0]) > 255 | ||||||
|  | @ -705,6 +712,7 @@ class TestFileJpeg: | ||||||
|     @pytest.mark.skipif(not djpeg_available(), reason="djpeg not available") |     @pytest.mark.skipif(not djpeg_available(), reason="djpeg not available") | ||||||
|     def test_load_djpeg(self) -> None: |     def test_load_djpeg(self) -> None: | ||||||
|         with Image.open(TEST_FILE) as img: |         with Image.open(TEST_FILE) as img: | ||||||
|  |             assert isinstance(img, JpegImagePlugin.JpegImageFile) | ||||||
|             img.load_djpeg() |             img.load_djpeg() | ||||||
|             assert_image_similar_tofile(img, TEST_FILE, 5) |             assert_image_similar_tofile(img, TEST_FILE, 5) | ||||||
| 
 | 
 | ||||||
|  | @ -909,6 +917,7 @@ class TestFileJpeg: | ||||||
| 
 | 
 | ||||||
|     def test_photoshop_malformed_and_multiple(self) -> None: |     def test_photoshop_malformed_and_multiple(self) -> None: | ||||||
|         with Image.open("Tests/images/app13-multiple.jpg") as im: |         with Image.open("Tests/images/app13-multiple.jpg") as im: | ||||||
|  |             assert isinstance(im, JpegImagePlugin.JpegImageFile) | ||||||
|             assert "photoshop" in im.info |             assert "photoshop" in im.info | ||||||
|             assert 24 == len(im.info["photoshop"]) |             assert 24 == len(im.info["photoshop"]) | ||||||
|             apps_13_lengths = [len(v) for k, v in im.applist if k == "APP13"] |             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: |     def test_deprecation(self) -> None: | ||||||
|         with Image.open(TEST_FILE) as im: |         with Image.open(TEST_FILE) as im: | ||||||
|  |             assert isinstance(im, JpegImagePlugin.JpegImageFile) | ||||||
|             with pytest.warns(DeprecationWarning): |             with pytest.warns(DeprecationWarning): | ||||||
|                 assert im.huffman_ac == {} |                 assert im.huffman_ac == {} | ||||||
|             with pytest.warns(DeprecationWarning): |             with pytest.warns(DeprecationWarning): | ||||||
|  |  | ||||||
|  | @ -228,12 +228,14 @@ def test_layers(card: ImageFile.ImageFile) -> None: | ||||||
|     out.seek(0) |     out.seek(0) | ||||||
| 
 | 
 | ||||||
|     with Image.open(out) as im: |     with Image.open(out) as im: | ||||||
|  |         assert isinstance(im, Jpeg2KImagePlugin.Jpeg2KImageFile) | ||||||
|         im.layers = 1 |         im.layers = 1 | ||||||
|         im.load() |         im.load() | ||||||
|         assert_image_similar(im, card, 13) |         assert_image_similar(im, card, 13) | ||||||
| 
 | 
 | ||||||
|     out.seek(0) |     out.seek(0) | ||||||
|     with Image.open(out) as im: |     with Image.open(out) as im: | ||||||
|  |         assert isinstance(im, Jpeg2KImagePlugin.Jpeg2KImageFile) | ||||||
|         im.layers = 3 |         im.layers = 3 | ||||||
|         im.load() |         im.load() | ||||||
|         assert_image_similar(im, card, 0.4) |         assert_image_similar(im, card, 0.4) | ||||||
|  | @ -455,8 +457,8 @@ def test_comment() -> None: | ||||||
|     # Test an image that is truncated partway through a codestream |     # Test an image that is truncated partway through a codestream | ||||||
|     with open("Tests/images/comment.jp2", "rb") as fp: |     with open("Tests/images/comment.jp2", "rb") as fp: | ||||||
|         b = BytesIO(fp.read(130)) |         b = BytesIO(fp.read(130)) | ||||||
|         with Image.open(b) as im: |     with Image.open(b) as im: | ||||||
|             pass |         pass | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def test_save_comment(card: ImageFile.ImageFile) -> None: | def test_save_comment(card: ImageFile.ImageFile) -> None: | ||||||
|  |  | ||||||
|  | @ -36,6 +36,7 @@ class LibTiffTestCase: | ||||||
|         im.load() |         im.load() | ||||||
|         im.getdata() |         im.getdata() | ||||||
| 
 | 
 | ||||||
|  |         assert isinstance(im, TiffImagePlugin.TiffImageFile) | ||||||
|         assert im._compression == "group4" |         assert im._compression == "group4" | ||||||
| 
 | 
 | ||||||
|         # can we write it back out, in a different form. |         # can we write it back out, in a different form. | ||||||
|  | @ -80,7 +81,7 @@ class TestFileLibTiff(LibTiffTestCase): | ||||||
|         s = io.BytesIO() |         s = io.BytesIO() | ||||||
|         with open(test_file, "rb") as f: |         with open(test_file, "rb") as f: | ||||||
|             s.write(f.read()) |             s.write(f.read()) | ||||||
|             s.seek(0) |         s.seek(0) | ||||||
|         with Image.open(s) as im: |         with Image.open(s) as im: | ||||||
|             assert im.size == (500, 500) |             assert im.size == (500, 500) | ||||||
|             self._assert_noerr(tmp_path, im) |             self._assert_noerr(tmp_path, im) | ||||||
|  | @ -153,6 +154,7 @@ class TestFileLibTiff(LibTiffTestCase): | ||||||
|         """Test metadata writing through libtiff""" |         """Test metadata writing through libtiff""" | ||||||
|         f = tmp_path / "temp.tiff" |         f = tmp_path / "temp.tiff" | ||||||
|         with Image.open("Tests/images/hopper_g4.tif") as img: |         with Image.open("Tests/images/hopper_g4.tif") as img: | ||||||
|  |             assert isinstance(img, TiffImagePlugin.TiffImageFile) | ||||||
|             img.save(f, tiffinfo=img.tag) |             img.save(f, tiffinfo=img.tag) | ||||||
| 
 | 
 | ||||||
|             if legacy_api: |             if legacy_api: | ||||||
|  | @ -170,6 +172,7 @@ class TestFileLibTiff(LibTiffTestCase): | ||||||
|         ] |         ] | ||||||
| 
 | 
 | ||||||
|         with Image.open(f) as loaded: |         with Image.open(f) as loaded: | ||||||
|  |             assert isinstance(loaded, TiffImagePlugin.TiffImageFile) | ||||||
|             if legacy_api: |             if legacy_api: | ||||||
|                 reloaded = loaded.tag.named() |                 reloaded = loaded.tag.named() | ||||||
|             else: |             else: | ||||||
|  | @ -212,6 +215,7 @@ class TestFileLibTiff(LibTiffTestCase): | ||||||
|         # Exclude ones that have special meaning |         # Exclude ones that have special meaning | ||||||
|         # that we're already testing them |         # that we're already testing them | ||||||
|         with Image.open("Tests/images/hopper_g4.tif") as im: |         with Image.open("Tests/images/hopper_g4.tif") as im: | ||||||
|  |             assert isinstance(im, TiffImagePlugin.TiffImageFile) | ||||||
|             for tag in im.tag_v2: |             for tag in im.tag_v2: | ||||||
|                 try: |                 try: | ||||||
|                     del core_items[tag] |                     del core_items[tag] | ||||||
|  | @ -317,6 +321,7 @@ class TestFileLibTiff(LibTiffTestCase): | ||||||
|             im.save(out, tiffinfo=tiffinfo) |             im.save(out, tiffinfo=tiffinfo) | ||||||
| 
 | 
 | ||||||
|             with Image.open(out) as reloaded: |             with Image.open(out) as reloaded: | ||||||
|  |                 assert isinstance(reloaded, TiffImagePlugin.TiffImageFile) | ||||||
|                 for tag, value in tiffinfo.items(): |                 for tag, value in tiffinfo.items(): | ||||||
|                     reloaded_value = reloaded.tag_v2[tag] |                     reloaded_value = reloaded.tag_v2[tag] | ||||||
|                     if ( |                     if ( | ||||||
|  | @ -349,12 +354,14 @@ class TestFileLibTiff(LibTiffTestCase): | ||||||
|     def test_osubfiletype(self, tmp_path: Path) -> None: |     def test_osubfiletype(self, tmp_path: Path) -> None: | ||||||
|         outfile = tmp_path / "temp.tif" |         outfile = tmp_path / "temp.tif" | ||||||
|         with Image.open("Tests/images/g4_orientation_6.tif") as im: |         with Image.open("Tests/images/g4_orientation_6.tif") as im: | ||||||
|  |             assert isinstance(im, TiffImagePlugin.TiffImageFile) | ||||||
|             im.tag_v2[OSUBFILETYPE] = 1 |             im.tag_v2[OSUBFILETYPE] = 1 | ||||||
|             im.save(outfile) |             im.save(outfile) | ||||||
| 
 | 
 | ||||||
|     def test_subifd(self, tmp_path: Path) -> None: |     def test_subifd(self, tmp_path: Path) -> None: | ||||||
|         outfile = tmp_path / "temp.tif" |         outfile = tmp_path / "temp.tif" | ||||||
|         with Image.open("Tests/images/g4_orientation_6.tif") as im: |         with Image.open("Tests/images/g4_orientation_6.tif") as im: | ||||||
|  |             assert isinstance(im, TiffImagePlugin.TiffImageFile) | ||||||
|             im.tag_v2[SUBIFD] = 10000 |             im.tag_v2[SUBIFD] = 10000 | ||||||
| 
 | 
 | ||||||
|             # Should not segfault |             # Should not segfault | ||||||
|  | @ -369,6 +376,7 @@ class TestFileLibTiff(LibTiffTestCase): | ||||||
|         hopper().save(out, tiffinfo={700: b"xmlpacket tag"}) |         hopper().save(out, tiffinfo={700: b"xmlpacket tag"}) | ||||||
| 
 | 
 | ||||||
|         with Image.open(out) as reloaded: |         with Image.open(out) as reloaded: | ||||||
|  |             assert isinstance(reloaded, TiffImagePlugin.TiffImageFile) | ||||||
|             if 700 in reloaded.tag_v2: |             if 700 in reloaded.tag_v2: | ||||||
|                 assert reloaded.tag_v2[700] == b"xmlpacket tag" |                 assert reloaded.tag_v2[700] == b"xmlpacket tag" | ||||||
| 
 | 
 | ||||||
|  | @ -430,12 +438,15 @@ class TestFileLibTiff(LibTiffTestCase): | ||||||
|         """Tests String data in info directory""" |         """Tests String data in info directory""" | ||||||
|         test_file = "Tests/images/hopper_g4_500.tif" |         test_file = "Tests/images/hopper_g4_500.tif" | ||||||
|         with Image.open(test_file) as orig: |         with Image.open(test_file) as orig: | ||||||
|  |             assert isinstance(orig, TiffImagePlugin.TiffImageFile) | ||||||
|  | 
 | ||||||
|             out = tmp_path / "temp.tif" |             out = tmp_path / "temp.tif" | ||||||
| 
 | 
 | ||||||
|             orig.tag[269] = "temp.tif" |             orig.tag[269] = "temp.tif" | ||||||
|             orig.save(out) |             orig.save(out) | ||||||
| 
 | 
 | ||||||
|         with Image.open(out) as reread: |         with Image.open(out) as reread: | ||||||
|  |             assert isinstance(reread, TiffImagePlugin.TiffImageFile) | ||||||
|             assert "temp.tif" == reread.tag_v2[269] |             assert "temp.tif" == reread.tag_v2[269] | ||||||
|             assert "temp.tif" == reread.tag[269][0] |             assert "temp.tif" == reread.tag[269][0] | ||||||
| 
 | 
 | ||||||
|  | @ -541,6 +552,7 @@ class TestFileLibTiff(LibTiffTestCase): | ||||||
| 
 | 
 | ||||||
|         with Image.open(out) as reloaded: |         with Image.open(out) as reloaded: | ||||||
|             # colormap/palette tag |             # colormap/palette tag | ||||||
|  |             assert isinstance(reloaded, TiffImagePlugin.TiffImageFile) | ||||||
|             assert len(reloaded.tag_v2[320]) == 768 |             assert len(reloaded.tag_v2[320]) == 768 | ||||||
| 
 | 
 | ||||||
|     @pytest.mark.parametrize("compression", ("tiff_ccitt", "group3", "group4")) |     @pytest.mark.parametrize("compression", ("tiff_ccitt", "group3", "group4")) | ||||||
|  | @ -572,6 +584,7 @@ class TestFileLibTiff(LibTiffTestCase): | ||||||
|         with Image.open("Tests/images/multipage.tiff") as im: |         with Image.open("Tests/images/multipage.tiff") as im: | ||||||
|             # file is a multipage tiff,  10x10 green, 10x10 red, 20x20 blue |             # file is a multipage tiff,  10x10 green, 10x10 red, 20x20 blue | ||||||
| 
 | 
 | ||||||
|  |             assert isinstance(im, TiffImagePlugin.TiffImageFile) | ||||||
|             im.seek(0) |             im.seek(0) | ||||||
|             assert im.size == (10, 10) |             assert im.size == (10, 10) | ||||||
|             assert im.convert("RGB").getpixel((0, 0)) == (0, 128, 0) |             assert im.convert("RGB").getpixel((0, 0)) == (0, 128, 0) | ||||||
|  | @ -591,6 +604,7 @@ class TestFileLibTiff(LibTiffTestCase): | ||||||
|         # issue #862 |         # issue #862 | ||||||
|         monkeypatch.setattr(TiffImagePlugin, "READ_LIBTIFF", True) |         monkeypatch.setattr(TiffImagePlugin, "READ_LIBTIFF", True) | ||||||
|         with Image.open("Tests/images/multipage.tiff") as im: |         with Image.open("Tests/images/multipage.tiff") as im: | ||||||
|  |             assert isinstance(im, TiffImagePlugin.TiffImageFile) | ||||||
|             frames = im.n_frames |             frames = im.n_frames | ||||||
|             assert frames == 3 |             assert frames == 3 | ||||||
|             for _ in range(frames): |             for _ in range(frames): | ||||||
|  | @ -610,6 +624,7 @@ class TestFileLibTiff(LibTiffTestCase): | ||||||
|     def test__next(self, monkeypatch: pytest.MonkeyPatch) -> None: |     def test__next(self, monkeypatch: pytest.MonkeyPatch) -> None: | ||||||
|         monkeypatch.setattr(TiffImagePlugin, "READ_LIBTIFF", True) |         monkeypatch.setattr(TiffImagePlugin, "READ_LIBTIFF", True) | ||||||
|         with Image.open("Tests/images/hopper.tif") as im: |         with Image.open("Tests/images/hopper.tif") as im: | ||||||
|  |             assert isinstance(im, TiffImagePlugin.TiffImageFile) | ||||||
|             assert not im.tag.next |             assert not im.tag.next | ||||||
|             im.load() |             im.load() | ||||||
|             assert not im.tag.next |             assert not im.tag.next | ||||||
|  | @ -690,21 +705,25 @@ class TestFileLibTiff(LibTiffTestCase): | ||||||
|         im.save(outfile, compression="jpeg") |         im.save(outfile, compression="jpeg") | ||||||
| 
 | 
 | ||||||
|         with Image.open(outfile) as reloaded: |         with Image.open(outfile) as reloaded: | ||||||
|  |             assert isinstance(reloaded, TiffImagePlugin.TiffImageFile) | ||||||
|             assert reloaded.tag_v2[530] == (1, 1) |             assert reloaded.tag_v2[530] == (1, 1) | ||||||
|             assert reloaded.tag_v2[532] == (0, 255, 128, 255, 128, 255) |             assert reloaded.tag_v2[532] == (0, 255, 128, 255, 128, 255) | ||||||
| 
 | 
 | ||||||
|     def test_exif_ifd(self) -> None: |     def test_exif_ifd(self) -> None: | ||||||
|         out = io.BytesIO() |         out = io.BytesIO() | ||||||
|         with Image.open("Tests/images/tiff_adobe_deflate.tif") as im: |         with Image.open("Tests/images/tiff_adobe_deflate.tif") as im: | ||||||
|  |             assert isinstance(im, TiffImagePlugin.TiffImageFile) | ||||||
|             assert im.tag_v2[34665] == 125456 |             assert im.tag_v2[34665] == 125456 | ||||||
|             im.save(out, "TIFF") |             im.save(out, "TIFF") | ||||||
| 
 | 
 | ||||||
|             with Image.open(out) as reloaded: |             with Image.open(out) as reloaded: | ||||||
|  |                 assert isinstance(reloaded, TiffImagePlugin.TiffImageFile) | ||||||
|                 assert 34665 not in reloaded.tag_v2 |                 assert 34665 not in reloaded.tag_v2 | ||||||
| 
 | 
 | ||||||
|             im.save(out, "TIFF", tiffinfo={34665: 125456}) |             im.save(out, "TIFF", tiffinfo={34665: 125456}) | ||||||
| 
 | 
 | ||||||
|         with Image.open(out) as reloaded: |         with Image.open(out) as reloaded: | ||||||
|  |             assert isinstance(reloaded, TiffImagePlugin.TiffImageFile) | ||||||
|             if Image.core.libtiff_support_custom_tags: |             if Image.core.libtiff_support_custom_tags: | ||||||
|                 assert reloaded.tag_v2[34665] == 125456 |                 assert reloaded.tag_v2[34665] == 125456 | ||||||
| 
 | 
 | ||||||
|  | @ -786,6 +805,7 @@ class TestFileLibTiff(LibTiffTestCase): | ||||||
| 
 | 
 | ||||||
|     def test_multipage_compression(self) -> None: |     def test_multipage_compression(self) -> None: | ||||||
|         with Image.open("Tests/images/compression.tif") as im: |         with Image.open("Tests/images/compression.tif") as im: | ||||||
|  |             assert isinstance(im, TiffImagePlugin.TiffImageFile) | ||||||
|             im.seek(0) |             im.seek(0) | ||||||
|             assert im._compression == "tiff_ccitt" |             assert im._compression == "tiff_ccitt" | ||||||
|             assert im.size == (10, 10) |             assert im.size == (10, 10) | ||||||
|  | @ -1026,6 +1046,17 @@ class TestFileLibTiff(LibTiffTestCase): | ||||||
|         with Image.open("Tests/images/old-style-jpeg-compression.tif") as im: |         with Image.open("Tests/images/old-style-jpeg-compression.tif") as im: | ||||||
|             assert_image_equal_tofile(im, "Tests/images/old-style-jpeg-compression.png") |             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: |     def test_open_missing_samplesperpixel(self) -> None: | ||||||
|         with Image.open( |         with Image.open( | ||||||
|             "Tests/images/old-style-jpeg-compression-no-samplesperpixel.tif" |             "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: |         with Image.open("Tests/images/g4_orientation_1.tif") as base_im: | ||||||
|             for i in range(2, 9): |             for i in range(2, 9): | ||||||
|                 with Image.open("Tests/images/g4_orientation_" + str(i) + ".tif") as im: |                 with Image.open("Tests/images/g4_orientation_" + str(i) + ".tif") as im: | ||||||
|  |                     assert isinstance(im, TiffImagePlugin.TiffImageFile) | ||||||
|                     assert 274 in im.tag_v2 |                     assert 274 in im.tag_v2 | ||||||
| 
 | 
 | ||||||
|                     im.load() |                     im.load() | ||||||
|  |  | ||||||
|  | @ -32,7 +32,7 @@ class TestFileLibTiffSmall(LibTiffTestCase): | ||||||
|         s = BytesIO() |         s = BytesIO() | ||||||
|         with open(test_file, "rb") as f: |         with open(test_file, "rb") as f: | ||||||
|             s.write(f.read()) |             s.write(f.read()) | ||||||
|             s.seek(0) |         s.seek(0) | ||||||
|         with Image.open(s) as im: |         with Image.open(s) as im: | ||||||
|             assert im.size == (128, 128) |             assert im.size == (128, 128) | ||||||
|             self._assert_noerr(tmp_path, im) |             self._assert_noerr(tmp_path, im) | ||||||
|  |  | ||||||
|  | @ -30,11 +30,13 @@ def test_sanity() -> None: | ||||||
| 
 | 
 | ||||||
| def test_n_frames() -> None: | def test_n_frames() -> None: | ||||||
|     with Image.open(TEST_FILE) as im: |     with Image.open(TEST_FILE) as im: | ||||||
|  |         assert isinstance(im, MicImagePlugin.MicImageFile) | ||||||
|         assert im.n_frames == 1 |         assert im.n_frames == 1 | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def test_is_animated() -> None: | def test_is_animated() -> None: | ||||||
|     with Image.open(TEST_FILE) as im: |     with Image.open(TEST_FILE) as im: | ||||||
|  |         assert isinstance(im, MicImagePlugin.MicImageFile) | ||||||
|         assert not im.is_animated |         assert not im.is_animated | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | @ -55,10 +57,11 @@ def test_seek() -> None: | ||||||
| 
 | 
 | ||||||
| def test_close() -> None: | def test_close() -> None: | ||||||
|     with Image.open(TEST_FILE) as im: |     with Image.open(TEST_FILE) as im: | ||||||
|         pass |         assert isinstance(im, MicImagePlugin.MicImageFile) | ||||||
|     assert im.ole.fp.closed |     assert im.ole.fp.closed | ||||||
| 
 | 
 | ||||||
|     im = Image.open(TEST_FILE) |     im = Image.open(TEST_FILE) | ||||||
|  |     assert isinstance(im, MicImagePlugin.MicImageFile) | ||||||
|     im.close() |     im.close() | ||||||
|     assert im.ole.fp.closed |     assert im.ole.fp.closed | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -6,7 +6,7 @@ from typing import Any | ||||||
| 
 | 
 | ||||||
| import pytest | import pytest | ||||||
| 
 | 
 | ||||||
| from PIL import Image, ImageFile, MpoImagePlugin | from PIL import Image, ImageFile, JpegImagePlugin, MpoImagePlugin | ||||||
| 
 | 
 | ||||||
| from .helper import ( | from .helper import ( | ||||||
|     assert_image_equal, |     assert_image_equal, | ||||||
|  | @ -80,6 +80,7 @@ def test_context_manager() -> None: | ||||||
| def test_app(test_file: str) -> None: | def test_app(test_file: str) -> None: | ||||||
|     # Test APP/COM reader (@PIL135) |     # Test APP/COM reader (@PIL135) | ||||||
|     with Image.open(test_file) as im: |     with Image.open(test_file) as im: | ||||||
|  |         assert isinstance(im, MpoImagePlugin.MpoImageFile) | ||||||
|         assert im.applist[0][0] == "APP1" |         assert im.applist[0][0] == "APP1" | ||||||
|         assert im.applist[1][0] == "APP2" |         assert im.applist[1][0] == "APP2" | ||||||
|         assert im.applist[1][1].startswith( |         assert im.applist[1][1].startswith( | ||||||
|  | @ -220,12 +221,14 @@ def test_seek(test_file: str) -> None: | ||||||
| 
 | 
 | ||||||
| def test_n_frames() -> None: | def test_n_frames() -> None: | ||||||
|     with Image.open("Tests/images/sugarshack.mpo") as im: |     with Image.open("Tests/images/sugarshack.mpo") as im: | ||||||
|  |         assert isinstance(im, MpoImagePlugin.MpoImageFile) | ||||||
|         assert im.n_frames == 2 |         assert im.n_frames == 2 | ||||||
|         assert im.is_animated |         assert im.is_animated | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def test_eoferror() -> None: | def test_eoferror() -> None: | ||||||
|     with Image.open("Tests/images/sugarshack.mpo") as im: |     with Image.open("Tests/images/sugarshack.mpo") as im: | ||||||
|  |         assert isinstance(im, MpoImagePlugin.MpoImageFile) | ||||||
|         n_frames = im.n_frames |         n_frames = im.n_frames | ||||||
| 
 | 
 | ||||||
|         # Test seeking past the last frame |         # Test seeking past the last frame | ||||||
|  | @ -239,6 +242,8 @@ def test_eoferror() -> None: | ||||||
| 
 | 
 | ||||||
| def test_adopt_jpeg() -> None: | def test_adopt_jpeg() -> None: | ||||||
|     with Image.open("Tests/images/hopper.jpg") as im: |     with Image.open("Tests/images/hopper.jpg") as im: | ||||||
|  |         assert isinstance(im, JpegImagePlugin.JpegImageFile) | ||||||
|  | 
 | ||||||
|         with pytest.raises(ValueError): |         with pytest.raises(ValueError): | ||||||
|             MpoImagePlugin.MpoImageFile.adopt(im) |             MpoImagePlugin.MpoImageFile.adopt(im) | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -43,6 +43,11 @@ def roundtrip(tmp_path: Path, mode: str) -> None: | ||||||
| 
 | 
 | ||||||
|     im.save(outfile) |     im.save(outfile) | ||||||
|     converted = open_with_magick(magick, tmp_path, 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) |     assert_image_equal(converted, im) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | @ -55,7 +60,6 @@ def test_monochrome(tmp_path: Path) -> None: | ||||||
|     roundtrip(tmp_path, mode) |     roundtrip(tmp_path, mode) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @pytest.mark.xfail(reason="Palm P image is wrong") |  | ||||||
| def test_p_mode(tmp_path: Path) -> None: | def test_p_mode(tmp_path: Path) -> None: | ||||||
|     # Arrange |     # Arrange | ||||||
|     mode = "P" |     mode = "P" | ||||||
|  |  | ||||||
|  | @ -576,6 +576,7 @@ class TestFilePng: | ||||||
| 
 | 
 | ||||||
|     def test_read_private_chunks(self) -> None: |     def test_read_private_chunks(self) -> None: | ||||||
|         with Image.open("Tests/images/exif.png") as im: |         with Image.open("Tests/images/exif.png") as im: | ||||||
|  |             assert isinstance(im, PngImagePlugin.PngImageFile) | ||||||
|             assert im.private_chunks == [(b"orNT", b"\x01")] |             assert im.private_chunks == [(b"orNT", b"\x01")] | ||||||
| 
 | 
 | ||||||
|     def test_roundtrip_private_chunk(self) -> None: |     def test_roundtrip_private_chunk(self) -> None: | ||||||
|  | @ -598,6 +599,7 @@ class TestFilePng: | ||||||
| 
 | 
 | ||||||
|     def test_textual_chunks_after_idat(self, monkeypatch: pytest.MonkeyPatch) -> None: |     def test_textual_chunks_after_idat(self, monkeypatch: pytest.MonkeyPatch) -> None: | ||||||
|         with Image.open("Tests/images/hopper.png") as im: |         with Image.open("Tests/images/hopper.png") as im: | ||||||
|  |             assert isinstance(im, PngImagePlugin.PngImageFile) | ||||||
|             assert "comment" in im.text |             assert "comment" in im.text | ||||||
|             for k, v in { |             for k, v in { | ||||||
|                 "date:create": "2014-09-04T09:37:08+03:00", |                 "date:create": "2014-09-04T09:37:08+03:00", | ||||||
|  | @ -607,15 +609,19 @@ class TestFilePng: | ||||||
| 
 | 
 | ||||||
|         # Raises a SyntaxError in load_end |         # Raises a SyntaxError in load_end | ||||||
|         with Image.open("Tests/images/broken_data_stream.png") as im: |         with Image.open("Tests/images/broken_data_stream.png") as im: | ||||||
|  |             assert isinstance(im, PngImagePlugin.PngImageFile) | ||||||
|             with pytest.raises(OSError): |             with pytest.raises(OSError): | ||||||
|                 assert isinstance(im.text, dict) |                 assert isinstance(im.text, dict) | ||||||
| 
 | 
 | ||||||
|         # Raises an EOFError in load_end |         # Raises an EOFError in load_end | ||||||
|         with Image.open("Tests/images/hopper_idat_after_image_end.png") as im: |         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"} |             assert im.text == {"TXT": "VALUE", "ZIP": "VALUE"} | ||||||
| 
 | 
 | ||||||
|         # Raises a UnicodeDecodeError in load_end |         # Raises a UnicodeDecodeError in load_end | ||||||
|         with Image.open("Tests/images/truncated_image.png") as im: |         with Image.open("Tests/images/truncated_image.png") as im: | ||||||
|  |             assert isinstance(im, PngImagePlugin.PngImageFile) | ||||||
|  | 
 | ||||||
|             # The file is truncated |             # The file is truncated | ||||||
|             with pytest.raises(OSError): |             with pytest.raises(OSError): | ||||||
|                 im.text |                 im.text | ||||||
|  | @ -726,6 +732,7 @@ class TestFilePng: | ||||||
|             im.save(test_file) |             im.save(test_file) | ||||||
| 
 | 
 | ||||||
|         with Image.open(test_file) as reloaded: |         with Image.open(test_file) as reloaded: | ||||||
|  |             assert isinstance(reloaded, PngImagePlugin.PngImageFile) | ||||||
|             assert reloaded._getexif() is None |             assert reloaded._getexif() is None | ||||||
| 
 | 
 | ||||||
|         # Test passing in exif |         # Test passing in exif | ||||||
|  |  | ||||||
|  | @ -59,17 +59,21 @@ def test_invalid_file() -> None: | ||||||
| 
 | 
 | ||||||
| def test_n_frames() -> None: | def test_n_frames() -> None: | ||||||
|     with Image.open("Tests/images/hopper_merged.psd") as im: |     with Image.open("Tests/images/hopper_merged.psd") as im: | ||||||
|  |         assert isinstance(im, PsdImagePlugin.PsdImageFile) | ||||||
|         assert im.n_frames == 1 |         assert im.n_frames == 1 | ||||||
|         assert not im.is_animated |         assert not im.is_animated | ||||||
| 
 | 
 | ||||||
|     for path in [test_file, "Tests/images/negative_layer_count.psd"]: |     for path in [test_file, "Tests/images/negative_layer_count.psd"]: | ||||||
|         with Image.open(path) as im: |         with Image.open(path) as im: | ||||||
|  |             assert isinstance(im, PsdImagePlugin.PsdImageFile) | ||||||
|             assert im.n_frames == 2 |             assert im.n_frames == 2 | ||||||
|             assert im.is_animated |             assert im.is_animated | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def test_eoferror() -> None: | def test_eoferror() -> None: | ||||||
|     with Image.open(test_file) as im: |     with Image.open(test_file) as im: | ||||||
|  |         assert isinstance(im, PsdImagePlugin.PsdImageFile) | ||||||
|  | 
 | ||||||
|         # PSD seek index starts at 1 rather than 0 |         # PSD seek index starts at 1 rather than 0 | ||||||
|         n_frames = im.n_frames + 1 |         n_frames = im.n_frames + 1 | ||||||
| 
 | 
 | ||||||
|  | @ -119,11 +123,13 @@ def test_rgba() -> None: | ||||||
| 
 | 
 | ||||||
| def test_negative_top_left_layer() -> None: | def test_negative_top_left_layer() -> None: | ||||||
|     with Image.open("Tests/images/negative_top_left_layer.psd") as im: |     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) |         assert im.layers[0][2] == (-50, -50, 50, 50) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def test_layer_skip() -> None: | def test_layer_skip() -> None: | ||||||
|     with Image.open("Tests/images/five_channels.psd") as im: |     with Image.open("Tests/images/five_channels.psd") as im: | ||||||
|  |         assert isinstance(im, PsdImagePlugin.PsdImageFile) | ||||||
|         assert im.n_frames == 1 |         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: | def test_layer_crashes(test_file: str) -> None: | ||||||
|     with open(test_file, "rb") as f: |     with open(test_file, "rb") as f: | ||||||
|         with Image.open(f) as im: |         with Image.open(f) as im: | ||||||
|  |             assert isinstance(im, PsdImagePlugin.PsdImageFile) | ||||||
|             with pytest.raises(SyntaxError): |             with pytest.raises(SyntaxError): | ||||||
|                 im.layers |                 im.layers | ||||||
|  |  | ||||||
|  | @ -71,24 +71,26 @@ def test_invalid_file() -> None: | ||||||
|         SgiImagePlugin.SgiImageFile(invalid_file) |         SgiImagePlugin.SgiImageFile(invalid_file) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def test_write(tmp_path: Path) -> None: | def roundtrip(img: Image.Image, tmp_path: Path) -> None: | ||||||
|     def roundtrip(img: Image.Image) -> None: |     out = tmp_path / "temp.sgi" | ||||||
|         out = tmp_path / "temp.sgi" |     img.save(out, format="sgi") | ||||||
|         img.save(out, format="sgi") |     assert_image_equal_tofile(img, out) | ||||||
|  | 
 | ||||||
|  |     out = tmp_path / "fp.sgi" | ||||||
|  |     with open(out, "wb") as fp: | ||||||
|  |         img.save(fp) | ||||||
|         assert_image_equal_tofile(img, out) |         assert_image_equal_tofile(img, out) | ||||||
| 
 | 
 | ||||||
|         out = tmp_path / "fp.sgi" |         assert not fp.closed | ||||||
|         with open(out, "wb") as fp: |  | ||||||
|             img.save(fp) |  | ||||||
|             assert_image_equal_tofile(img, out) |  | ||||||
| 
 | 
 | ||||||
|             assert not fp.closed |  | ||||||
| 
 | 
 | ||||||
|     for mode in ("L", "RGB", "RGBA"): | @pytest.mark.parametrize("mode", ("L", "RGB", "RGBA")) | ||||||
|         roundtrip(hopper(mode)) | def test_write(mode: str, tmp_path: Path) -> None: | ||||||
|  |     roundtrip(hopper(mode), tmp_path) | ||||||
| 
 | 
 | ||||||
|     # Test 1 dimension for an L mode image | 
 | ||||||
|     roundtrip(Image.new("L", (10, 1))) | 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: | def test_write16(tmp_path: Path) -> None: | ||||||
|  |  | ||||||
|  | @ -96,6 +96,7 @@ def test_tell() -> None: | ||||||
| 
 | 
 | ||||||
| def test_n_frames() -> None: | def test_n_frames() -> None: | ||||||
|     with Image.open(TEST_FILE) as im: |     with Image.open(TEST_FILE) as im: | ||||||
|  |         assert isinstance(im, SpiderImagePlugin.SpiderImageFile) | ||||||
|         assert im.n_frames == 1 |         assert im.n_frames == 1 | ||||||
|         assert not im.is_animated |         assert not im.is_animated | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1,8 +1,6 @@ | ||||||
| from __future__ import annotations | from __future__ import annotations | ||||||
| 
 | 
 | ||||||
| import os | import os | ||||||
| from glob import glob |  | ||||||
| from itertools import product |  | ||||||
| from pathlib import Path | from pathlib import Path | ||||||
| 
 | 
 | ||||||
| import pytest | import pytest | ||||||
|  | @ -15,14 +13,27 @@ _TGA_DIR = os.path.join("Tests", "images", "tga") | ||||||
| _TGA_DIR_COMMON = os.path.join(_TGA_DIR, "common") | _TGA_DIR_COMMON = os.path.join(_TGA_DIR, "common") | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| _MODES = ("L", "LA", "P", "RGB", "RGBA") |  | ||||||
| _ORIGINS = ("tl", "bl") | _ORIGINS = ("tl", "bl") | ||||||
| 
 | 
 | ||||||
| _ORIGIN_TO_ORIENTATION = {"tl": 1, "bl": -1} | _ORIGIN_TO_ORIENTATION = {"tl": 1, "bl": -1} | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @pytest.mark.parametrize("mode", _MODES) | @pytest.mark.parametrize( | ||||||
| def test_sanity(mode: str, tmp_path: Path) -> None: |     "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: |     def roundtrip(original_im: Image.Image) -> None: | ||||||
|         out = tmp_path / "temp.tga" |         out = tmp_path / "temp.tga" | ||||||
| 
 | 
 | ||||||
|  | @ -36,33 +47,26 @@ def test_sanity(mode: str, tmp_path: Path) -> None: | ||||||
| 
 | 
 | ||||||
|             assert_image_equal(saved_im, original_im) |             assert_image_equal(saved_im, original_im) | ||||||
| 
 | 
 | ||||||
|     png_paths = glob(os.path.join(_TGA_DIR_COMMON, f"*x*_{mode.lower()}.png")) |     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 | ||||||
| 
 | 
 | ||||||
|     for png_path in png_paths: |         path_no_ext = os.path.splitext(png_path)[0] | ||||||
|         with Image.open(png_path) as reference_im: |         tga_path = "{}_{}_{}.tga".format(path_no_ext, origin, "rle" if rle else "raw") | ||||||
|             assert reference_im.mode == mode |  | ||||||
| 
 | 
 | ||||||
|             path_no_ext = os.path.splitext(png_path)[0] |         with Image.open(tga_path) as original_im: | ||||||
|             for origin, rle in product(_ORIGINS, (True, False)): |             assert original_im.format == "TGA" | ||||||
|                 tga_path = "{}_{}_{}.tga".format( |             assert original_im.get_format_mimetype() == "image/x-tga" | ||||||
|                     path_no_ext, origin, "rle" if rle else "raw" |             if rle: | ||||||
|                 ) |                 assert original_im.info["compression"] == "tga_rle" | ||||||
|  |             assert original_im.info["orientation"] == _ORIGIN_TO_ORIENTATION[origin] | ||||||
|  |             if mode == "P": | ||||||
|  |                 assert original_im.getpalette() == reference_im.getpalette() | ||||||
| 
 | 
 | ||||||
|                 with Image.open(tga_path) as original_im: |             assert_image_equal(original_im, reference_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] |  | ||||||
|                     ) |  | ||||||
|                     if mode == "P": |  | ||||||
|                         assert original_im.getpalette() == reference_im.getpalette() |  | ||||||
| 
 | 
 | ||||||
|                     assert_image_equal(original_im, reference_im) |             roundtrip(original_im) | ||||||
| 
 |  | ||||||
|                     roundtrip(original_im) |  | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def test_palette_depth_8() -> None: | def test_palette_depth_8() -> None: | ||||||
|  |  | ||||||
|  | @ -9,7 +9,13 @@ from types import ModuleType | ||||||
| 
 | 
 | ||||||
| import pytest | 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 PIL.TiffImagePlugin import RESOLUTION_UNIT, X_RESOLUTION, Y_RESOLUTION | ||||||
| 
 | 
 | ||||||
| from .helper import ( | from .helper import ( | ||||||
|  | @ -113,6 +119,7 @@ class TestFileTiff: | ||||||
| 
 | 
 | ||||||
|         with Image.open("Tests/images/hopper_bigtiff.tif") as im: |         with Image.open("Tests/images/hopper_bigtiff.tif") as im: | ||||||
|             outfile = tmp_path / "temp.tif" |             outfile = tmp_path / "temp.tif" | ||||||
|  |             assert isinstance(im, TiffImagePlugin.TiffImageFile) | ||||||
|             im.save(outfile, save_all=True, append_images=[im], tiffinfo=im.tag_v2) |             im.save(outfile, save_all=True, append_images=[im], tiffinfo=im.tag_v2) | ||||||
| 
 | 
 | ||||||
|     def test_bigtiff_save(self, tmp_path: Path) -> None: |     def test_bigtiff_save(self, tmp_path: Path) -> None: | ||||||
|  | @ -121,11 +128,13 @@ class TestFileTiff: | ||||||
|         im.save(outfile, big_tiff=True) |         im.save(outfile, big_tiff=True) | ||||||
| 
 | 
 | ||||||
|         with Image.open(outfile) as reloaded: |         with Image.open(outfile) as reloaded: | ||||||
|  |             assert isinstance(reloaded, TiffImagePlugin.TiffImageFile) | ||||||
|             assert reloaded.tag_v2._bigtiff is True |             assert reloaded.tag_v2._bigtiff is True | ||||||
| 
 | 
 | ||||||
|         im.save(outfile, save_all=True, append_images=[im], big_tiff=True) |         im.save(outfile, save_all=True, append_images=[im], big_tiff=True) | ||||||
| 
 | 
 | ||||||
|         with Image.open(outfile) as reloaded: |         with Image.open(outfile) as reloaded: | ||||||
|  |             assert isinstance(reloaded, TiffImagePlugin.TiffImageFile) | ||||||
|             assert reloaded.tag_v2._bigtiff is True |             assert reloaded.tag_v2._bigtiff is True | ||||||
| 
 | 
 | ||||||
|     def test_seek_too_large(self) -> None: |     def test_seek_too_large(self) -> None: | ||||||
|  | @ -140,6 +149,8 @@ class TestFileTiff: | ||||||
|     def test_xyres_tiff(self) -> None: |     def test_xyres_tiff(self) -> None: | ||||||
|         filename = "Tests/images/pil168.tif" |         filename = "Tests/images/pil168.tif" | ||||||
|         with Image.open(filename) as im: |         with Image.open(filename) as im: | ||||||
|  |             assert isinstance(im, TiffImagePlugin.TiffImageFile) | ||||||
|  | 
 | ||||||
|             # legacy api |             # legacy api | ||||||
|             assert isinstance(im.tag[X_RESOLUTION][0], tuple) |             assert isinstance(im.tag[X_RESOLUTION][0], tuple) | ||||||
|             assert isinstance(im.tag[Y_RESOLUTION][0], tuple) |             assert isinstance(im.tag[Y_RESOLUTION][0], tuple) | ||||||
|  | @ -153,6 +164,8 @@ class TestFileTiff: | ||||||
|     def test_xyres_fallback_tiff(self) -> None: |     def test_xyres_fallback_tiff(self) -> None: | ||||||
|         filename = "Tests/images/compression.tif" |         filename = "Tests/images/compression.tif" | ||||||
|         with Image.open(filename) as im: |         with Image.open(filename) as im: | ||||||
|  |             assert isinstance(im, TiffImagePlugin.TiffImageFile) | ||||||
|  | 
 | ||||||
|             # v2 api |             # v2 api | ||||||
|             assert isinstance(im.tag_v2[X_RESOLUTION], TiffImagePlugin.IFDRational) |             assert isinstance(im.tag_v2[X_RESOLUTION], TiffImagePlugin.IFDRational) | ||||||
|             assert isinstance(im.tag_v2[Y_RESOLUTION], TiffImagePlugin.IFDRational) |             assert isinstance(im.tag_v2[Y_RESOLUTION], TiffImagePlugin.IFDRational) | ||||||
|  | @ -167,6 +180,8 @@ class TestFileTiff: | ||||||
|     def test_int_resolution(self) -> None: |     def test_int_resolution(self) -> None: | ||||||
|         filename = "Tests/images/pil168.tif" |         filename = "Tests/images/pil168.tif" | ||||||
|         with Image.open(filename) as im: |         with Image.open(filename) as im: | ||||||
|  |             assert isinstance(im, TiffImagePlugin.TiffImageFile) | ||||||
|  | 
 | ||||||
|             # Try to read a file where X,Y_RESOLUTION are ints |             # Try to read a file where X,Y_RESOLUTION are ints | ||||||
|             im.tag_v2[X_RESOLUTION] = 71 |             im.tag_v2[X_RESOLUTION] = 71 | ||||||
|             im.tag_v2[Y_RESOLUTION] = 71 |             im.tag_v2[Y_RESOLUTION] = 71 | ||||||
|  | @ -181,6 +196,7 @@ class TestFileTiff: | ||||||
|         with Image.open( |         with Image.open( | ||||||
|             "Tests/images/hopper_float_dpi_" + str(resolution_unit) + ".tif" |             "Tests/images/hopper_float_dpi_" + str(resolution_unit) + ".tif" | ||||||
|         ) as im: |         ) as im: | ||||||
|  |             assert isinstance(im, TiffImagePlugin.TiffImageFile) | ||||||
|             assert im.tag_v2.get(RESOLUTION_UNIT) == resolution_unit |             assert im.tag_v2.get(RESOLUTION_UNIT) == resolution_unit | ||||||
|             assert im.info["dpi"] == (dpi, dpi) |             assert im.info["dpi"] == (dpi, dpi) | ||||||
| 
 | 
 | ||||||
|  | @ -198,6 +214,7 @@ class TestFileTiff: | ||||||
|         with Image.open("Tests/images/10ct_32bit_128.tiff") as im: |         with Image.open("Tests/images/10ct_32bit_128.tiff") as im: | ||||||
|             im.save(b, format="tiff", resolution=123.45) |             im.save(b, format="tiff", resolution=123.45) | ||||||
|         with Image.open(b) as im: |         with Image.open(b) as im: | ||||||
|  |             assert isinstance(im, TiffImagePlugin.TiffImageFile) | ||||||
|             assert im.tag_v2[X_RESOLUTION] == 123.45 |             assert im.tag_v2[X_RESOLUTION] == 123.45 | ||||||
|             assert im.tag_v2[Y_RESOLUTION] == 123.45 |             assert im.tag_v2[Y_RESOLUTION] == 123.45 | ||||||
| 
 | 
 | ||||||
|  | @ -213,10 +230,12 @@ class TestFileTiff: | ||||||
|         TiffImagePlugin.PREFIXES.pop() |         TiffImagePlugin.PREFIXES.pop() | ||||||
| 
 | 
 | ||||||
|     def test_bad_exif(self) -> None: |     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. |             # Should not raise struct.error. | ||||||
|             with pytest.warns(UserWarning): |             with pytest.warns(UserWarning): | ||||||
|                 i._getexif() |                 im._getexif() | ||||||
| 
 | 
 | ||||||
|     def test_save_rgba(self, tmp_path: Path) -> None: |     def test_save_rgba(self, tmp_path: Path) -> None: | ||||||
|         im = hopper("RGBA") |         im = hopper("RGBA") | ||||||
|  | @ -307,11 +326,13 @@ class TestFileTiff: | ||||||
|     ) |     ) | ||||||
|     def test_n_frames(self, path: str, n_frames: int) -> None: |     def test_n_frames(self, path: str, n_frames: int) -> None: | ||||||
|         with Image.open(path) as im: |         with Image.open(path) as im: | ||||||
|  |             assert isinstance(im, TiffImagePlugin.TiffImageFile) | ||||||
|             assert im.n_frames == n_frames |             assert im.n_frames == n_frames | ||||||
|             assert im.is_animated == (n_frames != 1) |             assert im.is_animated == (n_frames != 1) | ||||||
| 
 | 
 | ||||||
|     def test_eoferror(self) -> None: |     def test_eoferror(self) -> None: | ||||||
|         with Image.open("Tests/images/multipage-lastframe.tif") as im: |         with Image.open("Tests/images/multipage-lastframe.tif") as im: | ||||||
|  |             assert isinstance(im, TiffImagePlugin.TiffImageFile) | ||||||
|             n_frames = im.n_frames |             n_frames = im.n_frames | ||||||
| 
 | 
 | ||||||
|             # Test seeking past the last frame |             # Test seeking past the last frame | ||||||
|  | @ -355,19 +376,24 @@ class TestFileTiff: | ||||||
|     def test_frame_order(self) -> None: |     def test_frame_order(self) -> None: | ||||||
|         # A frame can't progress to itself after reading |         # A frame can't progress to itself after reading | ||||||
|         with Image.open("Tests/images/multipage_single_frame_loop.tiff") as im: |         with Image.open("Tests/images/multipage_single_frame_loop.tiff") as im: | ||||||
|  |             assert isinstance(im, TiffImagePlugin.TiffImageFile) | ||||||
|             assert im.n_frames == 1 |             assert im.n_frames == 1 | ||||||
| 
 | 
 | ||||||
|         # A frame can't progress to a frame that has already been read |         # 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: |         with Image.open("Tests/images/multipage_multiple_frame_loop.tiff") as im: | ||||||
|  |             assert isinstance(im, TiffImagePlugin.TiffImageFile) | ||||||
|             assert im.n_frames == 2 |             assert im.n_frames == 2 | ||||||
| 
 | 
 | ||||||
|         # Frames don't have to be in sequence |         # Frames don't have to be in sequence | ||||||
|         with Image.open("Tests/images/multipage_out_of_order.tiff") as im: |         with Image.open("Tests/images/multipage_out_of_order.tiff") as im: | ||||||
|  |             assert isinstance(im, TiffImagePlugin.TiffImageFile) | ||||||
|             assert im.n_frames == 3 |             assert im.n_frames == 3 | ||||||
| 
 | 
 | ||||||
|     def test___str__(self) -> None: |     def test___str__(self) -> None: | ||||||
|         filename = "Tests/images/pil136.tiff" |         filename = "Tests/images/pil136.tiff" | ||||||
|         with Image.open(filename) as im: |         with Image.open(filename) as im: | ||||||
|  |             assert isinstance(im, TiffImagePlugin.TiffImageFile) | ||||||
|  | 
 | ||||||
|             # Act |             # Act | ||||||
|             ret = str(im.ifd) |             ret = str(im.ifd) | ||||||
| 
 | 
 | ||||||
|  | @ -378,6 +404,8 @@ class TestFileTiff: | ||||||
|         # Arrange |         # Arrange | ||||||
|         filename = "Tests/images/pil136.tiff" |         filename = "Tests/images/pil136.tiff" | ||||||
|         with Image.open(filename) as im: |         with Image.open(filename) as im: | ||||||
|  |             assert isinstance(im, TiffImagePlugin.TiffImageFile) | ||||||
|  | 
 | ||||||
|             # v2 interface |             # v2 interface | ||||||
|             v2_tags = { |             v2_tags = { | ||||||
|                 256: 55, |                 256: 55, | ||||||
|  | @ -417,6 +445,7 @@ class TestFileTiff: | ||||||
|     def test__delitem__(self) -> None: |     def test__delitem__(self) -> None: | ||||||
|         filename = "Tests/images/pil136.tiff" |         filename = "Tests/images/pil136.tiff" | ||||||
|         with Image.open(filename) as im: |         with Image.open(filename) as im: | ||||||
|  |             assert isinstance(im, TiffImagePlugin.TiffImageFile) | ||||||
|             len_before = len(dict(im.ifd)) |             len_before = len(dict(im.ifd)) | ||||||
|             del im.ifd[256] |             del im.ifd[256] | ||||||
|             len_after = len(dict(im.ifd)) |             len_after = len(dict(im.ifd)) | ||||||
|  | @ -449,6 +478,7 @@ class TestFileTiff: | ||||||
| 
 | 
 | ||||||
|     def test_ifd_tag_type(self) -> None: |     def test_ifd_tag_type(self) -> None: | ||||||
|         with Image.open("Tests/images/ifd_tag_type.tiff") as im: |         with Image.open("Tests/images/ifd_tag_type.tiff") as im: | ||||||
|  |             assert isinstance(im, TiffImagePlugin.TiffImageFile) | ||||||
|             assert 0x8825 in im.tag_v2 |             assert 0x8825 in im.tag_v2 | ||||||
| 
 | 
 | ||||||
|     def test_exif(self, tmp_path: Path) -> None: |     def test_exif(self, tmp_path: Path) -> None: | ||||||
|  | @ -537,6 +567,7 @@ class TestFileTiff: | ||||||
|         im = hopper(mode) |         im = hopper(mode) | ||||||
|         im.save(filename, tiffinfo={262: 0}) |         im.save(filename, tiffinfo={262: 0}) | ||||||
|         with Image.open(filename) as reloaded: |         with Image.open(filename) as reloaded: | ||||||
|  |             assert isinstance(reloaded, TiffImagePlugin.TiffImageFile) | ||||||
|             assert reloaded.tag_v2[262] == 0 |             assert reloaded.tag_v2[262] == 0 | ||||||
|             assert_image_equal(im, reloaded) |             assert_image_equal(im, reloaded) | ||||||
| 
 | 
 | ||||||
|  | @ -615,6 +646,8 @@ class TestFileTiff: | ||||||
|         filename = tmp_path / "temp.tif" |         filename = tmp_path / "temp.tif" | ||||||
|         hopper("RGB").save(filename, "TIFF", **kwargs) |         hopper("RGB").save(filename, "TIFF", **kwargs) | ||||||
|         with Image.open(filename) as im: |         with Image.open(filename) as im: | ||||||
|  |             assert isinstance(im, TiffImagePlugin.TiffImageFile) | ||||||
|  | 
 | ||||||
|             # legacy interface |             # legacy interface | ||||||
|             assert im.tag[X_RESOLUTION][0][0] == 72 |             assert im.tag[X_RESOLUTION][0][0] == 72 | ||||||
|             assert im.tag[Y_RESOLUTION][0][0] == 36 |             assert im.tag[Y_RESOLUTION][0][0] == 36 | ||||||
|  | @ -701,6 +734,7 @@ class TestFileTiff: | ||||||
|     def test_planar_configuration_save(self, tmp_path: Path) -> None: |     def test_planar_configuration_save(self, tmp_path: Path) -> None: | ||||||
|         infile = "Tests/images/tiff_tiled_planar_raw.tif" |         infile = "Tests/images/tiff_tiled_planar_raw.tif" | ||||||
|         with Image.open(infile) as im: |         with Image.open(infile) as im: | ||||||
|  |             assert isinstance(im, TiffImagePlugin.TiffImageFile) | ||||||
|             assert im._planar_configuration == 2 |             assert im._planar_configuration == 2 | ||||||
| 
 | 
 | ||||||
|             outfile = tmp_path / "temp.tif" |             outfile = tmp_path / "temp.tif" | ||||||
|  | @ -733,6 +767,7 @@ class TestFileTiff: | ||||||
| 
 | 
 | ||||||
|         mp.seek(0, os.SEEK_SET) |         mp.seek(0, os.SEEK_SET) | ||||||
|         with Image.open(mp) as im: |         with Image.open(mp) as im: | ||||||
|  |             assert isinstance(im, TiffImagePlugin.TiffImageFile) | ||||||
|             assert im.n_frames == 3 |             assert im.n_frames == 3 | ||||||
| 
 | 
 | ||||||
|         # Test appending images |         # Test appending images | ||||||
|  | @ -743,6 +778,7 @@ class TestFileTiff: | ||||||
| 
 | 
 | ||||||
|         mp.seek(0, os.SEEK_SET) |         mp.seek(0, os.SEEK_SET) | ||||||
|         with Image.open(mp) as reread: |         with Image.open(mp) as reread: | ||||||
|  |             assert isinstance(reread, TiffImagePlugin.TiffImageFile) | ||||||
|             assert reread.n_frames == 3 |             assert reread.n_frames == 3 | ||||||
| 
 | 
 | ||||||
|         # Test appending using a generator |         # Test appending using a generator | ||||||
|  | @ -754,6 +790,7 @@ class TestFileTiff: | ||||||
| 
 | 
 | ||||||
|         mp.seek(0, os.SEEK_SET) |         mp.seek(0, os.SEEK_SET) | ||||||
|         with Image.open(mp) as reread: |         with Image.open(mp) as reread: | ||||||
|  |             assert isinstance(reread, TiffImagePlugin.TiffImageFile) | ||||||
|             assert reread.n_frames == 3 |             assert reread.n_frames == 3 | ||||||
| 
 | 
 | ||||||
|     def test_save_all_progress(self) -> None: |     def test_save_all_progress(self) -> None: | ||||||
|  | @ -915,6 +952,7 @@ class TestFileTiff: | ||||||
| 
 | 
 | ||||||
|     def test_get_photoshop_blocks(self) -> None: |     def test_get_photoshop_blocks(self) -> None: | ||||||
|         with Image.open("Tests/images/lab.tif") as im: |         with Image.open("Tests/images/lab.tif") as im: | ||||||
|  |             assert isinstance(im, TiffImagePlugin.TiffImageFile) | ||||||
|             assert list(im.get_photoshop_blocks().keys()) == [ |             assert list(im.get_photoshop_blocks().keys()) == [ | ||||||
|                 1061, |                 1061, | ||||||
|                 1002, |                 1002, | ||||||
|  |  | ||||||
|  | @ -61,6 +61,7 @@ def test_rt_metadata(tmp_path: Path) -> None: | ||||||
|     img.save(f, tiffinfo=info) |     img.save(f, tiffinfo=info) | ||||||
| 
 | 
 | ||||||
|     with Image.open(f) as loaded: |     with Image.open(f) as loaded: | ||||||
|  |         assert isinstance(loaded, TiffImagePlugin.TiffImageFile) | ||||||
|         assert loaded.tag[ImageJMetaDataByteCounts] == (len(bin_data),) |         assert loaded.tag[ImageJMetaDataByteCounts] == (len(bin_data),) | ||||||
|         assert loaded.tag_v2[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) |     info[ImageJMetaDataByteCounts] = (8, len(bin_data) - 8) | ||||||
|     img.save(f, tiffinfo=info) |     img.save(f, tiffinfo=info) | ||||||
|     with Image.open(f) as loaded: |     with Image.open(f) as loaded: | ||||||
|  |         assert isinstance(loaded, TiffImagePlugin.TiffImageFile) | ||||||
|         assert loaded.tag[ImageJMetaDataByteCounts] == (8, len(bin_data) - 8) |         assert loaded.tag[ImageJMetaDataByteCounts] == (8, len(bin_data) - 8) | ||||||
|         assert loaded.tag_v2[ImageJMetaDataByteCounts] == (8, len(bin_data) - 8) |         assert loaded.tag_v2[ImageJMetaDataByteCounts] == (8, len(bin_data) - 8) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def test_read_metadata() -> None: | def test_read_metadata() -> None: | ||||||
|     with Image.open("Tests/images/hopper_g4.tif") as img: |     with Image.open("Tests/images/hopper_g4.tif") as img: | ||||||
|  |         assert isinstance(img, TiffImagePlugin.TiffImageFile) | ||||||
|         assert { |         assert { | ||||||
|             "YResolution": IFDRational(4294967295, 113653537), |             "YResolution": IFDRational(4294967295, 113653537), | ||||||
|             "PlanarConfiguration": 1, |             "PlanarConfiguration": 1, | ||||||
|  | @ -128,6 +131,7 @@ def test_read_metadata() -> None: | ||||||
| def test_write_metadata(tmp_path: Path) -> None: | def test_write_metadata(tmp_path: Path) -> None: | ||||||
|     """Test metadata writing through the python code""" |     """Test metadata writing through the python code""" | ||||||
|     with Image.open("Tests/images/hopper.tif") as img: |     with Image.open("Tests/images/hopper.tif") as img: | ||||||
|  |         assert isinstance(img, TiffImagePlugin.TiffImageFile) | ||||||
|         f = tmp_path / "temp.tiff" |         f = tmp_path / "temp.tiff" | ||||||
|         del img.tag[278] |         del img.tag[278] | ||||||
|         img.save(f, tiffinfo=img.tag) |         img.save(f, tiffinfo=img.tag) | ||||||
|  | @ -135,6 +139,7 @@ def test_write_metadata(tmp_path: Path) -> None: | ||||||
|         original = img.tag_v2.named() |         original = img.tag_v2.named() | ||||||
| 
 | 
 | ||||||
|     with Image.open(f) as loaded: |     with Image.open(f) as loaded: | ||||||
|  |         assert isinstance(loaded, TiffImagePlugin.TiffImageFile) | ||||||
|         reloaded = loaded.tag_v2.named() |         reloaded = loaded.tag_v2.named() | ||||||
| 
 | 
 | ||||||
|     ignored = ["StripByteCounts", "RowsPerStrip", "PageNumber", "StripOffsets"] |     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: | def test_change_stripbytecounts_tag_type(tmp_path: Path) -> None: | ||||||
|     out = tmp_path / "temp.tiff" |     out = tmp_path / "temp.tiff" | ||||||
|     with Image.open("Tests/images/hopper.tif") as im: |     with Image.open("Tests/images/hopper.tif") as im: | ||||||
|  |         assert isinstance(im, TiffImagePlugin.TiffImageFile) | ||||||
|         info = im.tag_v2 |         info = im.tag_v2 | ||||||
|         del info[278] |         del info[278] | ||||||
| 
 | 
 | ||||||
|  | @ -178,6 +184,7 @@ def test_change_stripbytecounts_tag_type(tmp_path: Path) -> None: | ||||||
|         im.save(out, tiffinfo=info) |         im.save(out, tiffinfo=info) | ||||||
| 
 | 
 | ||||||
|     with Image.open(out) as reloaded: |     with Image.open(out) as reloaded: | ||||||
|  |         assert isinstance(reloaded, TiffImagePlugin.TiffImageFile) | ||||||
|         assert reloaded.tag_v2.tagtype[TiffImagePlugin.STRIPBYTECOUNTS] == TiffTags.LONG |         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) |     im.save(out, tiffinfo=info) | ||||||
| 
 | 
 | ||||||
|     with Image.open(out) as reloaded: |     with Image.open(out) as reloaded: | ||||||
|  |         assert isinstance(reloaded, TiffImagePlugin.TiffImageFile) | ||||||
|         assert reloaded.tag_v2[271] == expected |         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) |     im.save(out, tiffinfo=info) | ||||||
| 
 | 
 | ||||||
|     with Image.open(out) as reloaded: |     with Image.open(out) as reloaded: | ||||||
|  |         assert isinstance(reloaded, TiffImagePlugin.TiffImageFile) | ||||||
|         assert reloaded.tag_v2[700] == b"\x01" |         assert reloaded.tag_v2[700] == b"\x01" | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | @ -267,6 +276,7 @@ def test_writing_other_types_to_undefined( | ||||||
|     im.save(out, tiffinfo=info) |     im.save(out, tiffinfo=info) | ||||||
| 
 | 
 | ||||||
|     with Image.open(out) as reloaded: |     with Image.open(out) as reloaded: | ||||||
|  |         assert isinstance(reloaded, TiffImagePlugin.TiffImageFile) | ||||||
|         assert reloaded.tag_v2[33723] == b"1" |         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. |     # but probably won't be able to save it. | ||||||
| 
 | 
 | ||||||
|     with Image.open("Tests/images/hopper.iccprofile_binary.tif") as im: |     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.tag_v2.tagtype[34675] == 1 | ||||||
|         assert im.info["icc_profile"] |         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") |     im.save(out, tiffinfo=info, compression="raw") | ||||||
| 
 | 
 | ||||||
|     with Image.open(out) as reloaded: |     with Image.open(out) as reloaded: | ||||||
|  |         assert isinstance(reloaded, TiffImagePlugin.TiffImageFile) | ||||||
|         assert 0 == reloaded.tag_v2[41988].numerator |         assert 0 == reloaded.tag_v2[41988].numerator | ||||||
|         assert 0 == reloaded.tag_v2[41988].denominator |         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") |     im.save(out, tiffinfo=info, compression="raw") | ||||||
| 
 | 
 | ||||||
|     with Image.open(out) as reloaded: |     with Image.open(out) as reloaded: | ||||||
|  |         assert isinstance(reloaded, TiffImagePlugin.TiffImageFile) | ||||||
|         assert max_long == reloaded.tag_v2[41493].numerator |         assert max_long == reloaded.tag_v2[41493].numerator | ||||||
|         assert 1 == reloaded.tag_v2[41493].denominator |         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") |     im.save(out, tiffinfo=info, compression="raw") | ||||||
| 
 | 
 | ||||||
|     with Image.open(out) as reloaded: |     with Image.open(out) as reloaded: | ||||||
|  |         assert isinstance(reloaded, TiffImagePlugin.TiffImageFile) | ||||||
|         assert max_long == reloaded.tag_v2[41493].numerator |         assert max_long == reloaded.tag_v2[41493].numerator | ||||||
|         assert 1 == reloaded.tag_v2[41493].denominator |         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") |     im.save(out, tiffinfo=info, compression="raw") | ||||||
| 
 | 
 | ||||||
|     with Image.open(out) as reloaded: |     with Image.open(out) as reloaded: | ||||||
|  |         assert isinstance(reloaded, TiffImagePlugin.TiffImageFile) | ||||||
|         assert numerator == reloaded.tag_v2[37380].numerator |         assert numerator == reloaded.tag_v2[37380].numerator | ||||||
|         assert denominator == reloaded.tag_v2[37380].denominator |         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") |     im.save(out, tiffinfo=info, compression="raw") | ||||||
| 
 | 
 | ||||||
|     with Image.open(out) as reloaded: |     with Image.open(out) as reloaded: | ||||||
|  |         assert isinstance(reloaded, TiffImagePlugin.TiffImageFile) | ||||||
|         assert numerator == reloaded.tag_v2[37380].numerator |         assert numerator == reloaded.tag_v2[37380].numerator | ||||||
|         assert denominator == reloaded.tag_v2[37380].denominator |         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") |     im.save(out, tiffinfo=info, compression="raw") | ||||||
| 
 | 
 | ||||||
|     with Image.open(out) as reloaded: |     with Image.open(out) as reloaded: | ||||||
|  |         assert isinstance(reloaded, TiffImagePlugin.TiffImageFile) | ||||||
|         assert 2**31 - 1 == reloaded.tag_v2[37380].numerator |         assert 2**31 - 1 == reloaded.tag_v2[37380].numerator | ||||||
|         assert -1 == reloaded.tag_v2[37380].denominator |         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") |     im.save(out, tiffinfo=info, compression="raw") | ||||||
| 
 | 
 | ||||||
|     with Image.open(out) as reloaded: |     with Image.open(out) as reloaded: | ||||||
|  |         assert isinstance(reloaded, TiffImagePlugin.TiffImageFile) | ||||||
|         assert reloaded.tag_v2[37000] == -60000 |         assert reloaded.tag_v2[37000] == -60000 | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | @ -444,11 +462,13 @@ def test_empty_values() -> None: | ||||||
| 
 | 
 | ||||||
| def test_photoshop_info(tmp_path: Path) -> None: | def test_photoshop_info(tmp_path: Path) -> None: | ||||||
|     with Image.open("Tests/images/issue_2278.tif") as im: |     with Image.open("Tests/images/issue_2278.tif") as im: | ||||||
|  |         assert isinstance(im, TiffImagePlugin.TiffImageFile) | ||||||
|         assert len(im.tag_v2[34377]) == 70 |         assert len(im.tag_v2[34377]) == 70 | ||||||
|         assert isinstance(im.tag_v2[34377], bytes) |         assert isinstance(im.tag_v2[34377], bytes) | ||||||
|         out = tmp_path / "temp.tiff" |         out = tmp_path / "temp.tiff" | ||||||
|         im.save(out) |         im.save(out) | ||||||
|     with Image.open(out) as reloaded: |     with Image.open(out) as reloaded: | ||||||
|  |         assert isinstance(reloaded, TiffImagePlugin.TiffImageFile) | ||||||
|         assert len(reloaded.tag_v2[34377]) == 70 |         assert len(reloaded.tag_v2[34377]) == 70 | ||||||
|         assert isinstance(reloaded.tag_v2[34377], bytes) |         assert isinstance(reloaded.tag_v2[34377], bytes) | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -6,7 +6,7 @@ from pathlib import Path | ||||||
| import pytest | import pytest | ||||||
| from packaging.version import parse as parse_version | from packaging.version import parse as parse_version | ||||||
| 
 | 
 | ||||||
| from PIL import Image, features | from PIL import GifImagePlugin, Image, WebPImagePlugin, features | ||||||
| 
 | 
 | ||||||
| from .helper import ( | from .helper import ( | ||||||
|     assert_image_equal, |     assert_image_equal, | ||||||
|  | @ -22,10 +22,12 @@ def test_n_frames() -> None: | ||||||
|     """Ensure that WebP format sets n_frames and is_animated attributes correctly.""" |     """Ensure that WebP format sets n_frames and is_animated attributes correctly.""" | ||||||
| 
 | 
 | ||||||
|     with Image.open("Tests/images/hopper.webp") as im: |     with Image.open("Tests/images/hopper.webp") as im: | ||||||
|  |         assert isinstance(im, WebPImagePlugin.WebPImageFile) | ||||||
|         assert im.n_frames == 1 |         assert im.n_frames == 1 | ||||||
|         assert not im.is_animated |         assert not im.is_animated | ||||||
| 
 | 
 | ||||||
|     with Image.open("Tests/images/iss634.webp") as im: |     with Image.open("Tests/images/iss634.webp") as im: | ||||||
|  |         assert isinstance(im, WebPImagePlugin.WebPImageFile) | ||||||
|         assert im.n_frames == 42 |         assert im.n_frames == 42 | ||||||
|         assert im.is_animated |         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: |     with Image.open("Tests/images/iss634.gif") as orig: | ||||||
|  |         assert isinstance(orig, GifImagePlugin.GifImageFile) | ||||||
|         assert orig.n_frames > 1 |         assert orig.n_frames > 1 | ||||||
| 
 | 
 | ||||||
|         temp_file = tmp_path / "temp.webp" |         temp_file = tmp_path / "temp.webp" | ||||||
|         orig.save(temp_file, save_all=True) |         orig.save(temp_file, save_all=True) | ||||||
|         with Image.open(temp_file) as im: |         with Image.open(temp_file) as im: | ||||||
|  |             assert isinstance(im, WebPImagePlugin.WebPImageFile) | ||||||
|             assert im.n_frames == orig.n_frames |             assert im.n_frames == orig.n_frames | ||||||
| 
 | 
 | ||||||
|             # Compare first and last frames to the original animated GIF |             # 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: |     def check(temp_file: Path) -> None: | ||||||
|         with Image.open(temp_file) as im: |         with Image.open(temp_file) as im: | ||||||
|  |             assert isinstance(im, WebPImagePlugin.WebPImageFile) | ||||||
|             assert im.n_frames == 2 |             assert im.n_frames == 2 | ||||||
| 
 | 
 | ||||||
|             # Compare first frame to original |             # 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: |     with Image.open(temp_file) as im: | ||||||
|  |         assert isinstance(im, WebPImagePlugin.WebPImageFile) | ||||||
|         assert im.n_frames == 5 |         assert im.n_frames == 5 | ||||||
|         assert im.is_animated |         assert im.is_animated | ||||||
| 
 | 
 | ||||||
|  | @ -170,6 +176,7 @@ def test_seeking(tmp_path: Path) -> None: | ||||||
|             ) |             ) | ||||||
| 
 | 
 | ||||||
|     with Image.open(temp_file) as im: |     with Image.open(temp_file) as im: | ||||||
|  |         assert isinstance(im, WebPImagePlugin.WebPImageFile) | ||||||
|         assert im.n_frames == 5 |         assert im.n_frames == 5 | ||||||
|         assert im.is_animated |         assert im.is_animated | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -6,7 +6,7 @@ from types import ModuleType | ||||||
| 
 | 
 | ||||||
| import pytest | import pytest | ||||||
| 
 | 
 | ||||||
| from PIL import Image | from PIL import Image, WebPImagePlugin | ||||||
| 
 | 
 | ||||||
| from .helper import mark_if_feature_version, skip_unless_feature | from .helper import mark_if_feature_version, skip_unless_feature | ||||||
| 
 | 
 | ||||||
|  | @ -110,6 +110,7 @@ def test_read_no_exif() -> None: | ||||||
| 
 | 
 | ||||||
|     test_buffer.seek(0) |     test_buffer.seek(0) | ||||||
|     with Image.open(test_buffer) as webp_image: |     with Image.open(test_buffer) as webp_image: | ||||||
|  |         assert isinstance(webp_image, WebPImagePlugin.WebPImageFile) | ||||||
|         assert not webp_image._getexif() |         assert not webp_image._getexif() | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -8,7 +8,7 @@ import pytest | ||||||
| 
 | 
 | ||||||
| from PIL import Image, ImageFile, WmfImagePlugin | 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: | def test_load_raw() -> None: | ||||||
|  | @ -44,6 +44,15 @@ def test_load_zero_inch() -> None: | ||||||
|             pass |             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: | def test_register_handler(tmp_path: Path) -> None: | ||||||
|     class TestHandler(ImageFile.StubHandler): |     class TestHandler(ImageFile.StubHandler): | ||||||
|         methodCalled = False |         methodCalled = False | ||||||
|  | @ -80,6 +89,7 @@ def test_load_float_dpi() -> None: | ||||||
| 
 | 
 | ||||||
| def test_load_set_dpi() -> None: | def test_load_set_dpi() -> None: | ||||||
|     with Image.open("Tests/images/drawing.wmf") as im: |     with Image.open("Tests/images/drawing.wmf") as im: | ||||||
|  |         assert isinstance(im, WmfImagePlugin.WmfStubImageFile) | ||||||
|         assert im.size == (82, 82) |         assert im.size == (82, 82) | ||||||
| 
 | 
 | ||||||
|         if hasattr(Image.core, "drawwmf"): |         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) |             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")) | @pytest.mark.parametrize("ext", (".wmf", ".emf")) | ||||||
| def test_save(ext: str, tmp_path: Path) -> None: | def test_save(ext: str, tmp_path: Path) -> None: | ||||||
|  |  | ||||||
|  | @ -30,6 +30,7 @@ def test_invalid_file() -> None: | ||||||
| def test_load_read() -> None: | def test_load_read() -> None: | ||||||
|     # Arrange |     # Arrange | ||||||
|     with Image.open(TEST_FILE) as im: |     with Image.open(TEST_FILE) as im: | ||||||
|  |         assert isinstance(im, XpmImagePlugin.XpmImageFile) | ||||||
|         dummy_bytes = 1 |         dummy_bytes = 1 | ||||||
| 
 | 
 | ||||||
|         # Act |         # Act | ||||||
|  |  | ||||||
|  | @ -230,10 +230,10 @@ class TestImage: | ||||||
|                 assert_image_similar(im, reloaded, 20) |                 assert_image_similar(im, reloaded, 20) | ||||||
| 
 | 
 | ||||||
|     def test_unknown_extension(self, tmp_path: Path) -> None: |     def test_unknown_extension(self, tmp_path: Path) -> None: | ||||||
|         im = hopper() |  | ||||||
|         temp_file = tmp_path / "temp.unknown" |         temp_file = tmp_path / "temp.unknown" | ||||||
|         with pytest.raises(ValueError): |         with hopper() as im: | ||||||
|             im.save(temp_file) |             with pytest.raises(ValueError): | ||||||
|  |                 im.save(temp_file) | ||||||
| 
 | 
 | ||||||
|     def test_internals(self) -> None: |     def test_internals(self) -> None: | ||||||
|         im = Image.new("L", (100, 100)) |         im = Image.new("L", (100, 100)) | ||||||
|  | @ -258,6 +258,15 @@ class TestImage: | ||||||
|             assert im.readonly |             assert im.readonly | ||||||
|             im.save(temp_file) |             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: |     def test_dump(self, tmp_path: Path) -> None: | ||||||
|         im = Image.new("L", (10, 10)) |         im = Image.new("L", (10, 10)) | ||||||
|         im._dump(str(tmp_path / "temp_L.ppm")) |         im._dump(str(tmp_path / "temp_L.ppm")) | ||||||
|  |  | ||||||
|  | @ -1704,7 +1704,7 @@ def test_discontiguous_corners_polygon() -> None: | ||||||
|         BLACK, |         BLACK, | ||||||
|     ) |     ) | ||||||
|     expected = os.path.join(IMAGES_PATH, "discontiguous_corners_polygon.png") |     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: | def test_polygon2() -> None: | ||||||
|  |  | ||||||
|  | @ -131,6 +131,26 @@ class TestImageFile: | ||||||
| 
 | 
 | ||||||
|         assert_image_equal(im1, im2) |         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: |     def test_raise_oserror(self) -> None: | ||||||
|         with pytest.warns(DeprecationWarning): |         with pytest.warns(DeprecationWarning): | ||||||
|             with pytest.raises(OSError): |             with pytest.raises(OSError): | ||||||
|  |  | ||||||
|  | @ -40,8 +40,11 @@ class TestImageGrab: | ||||||
| 
 | 
 | ||||||
|     @pytest.mark.skipif(Image.core.HAVE_XCB, reason="tests missing XCB") |     @pytest.mark.skipif(Image.core.HAVE_XCB, reason="tests missing XCB") | ||||||
|     def test_grab_no_xcb(self) -> None: |     def test_grab_no_xcb(self) -> None: | ||||||
|         if sys.platform not in ("win32", "darwin") and not shutil.which( |         if ( | ||||||
|             "gnome-screenshot" |             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: |             with pytest.raises(OSError) as e: | ||||||
|                 ImageGrab.grab() |                 ImageGrab.grab() | ||||||
|  | @ -57,6 +60,13 @@ class TestImageGrab: | ||||||
|             ImageGrab.grab(xdisplay="error.test:0.0") |             ImageGrab.grab(xdisplay="error.test:0.0") | ||||||
|         assert str(e.value).startswith("X connection failed") |         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: |     def test_grabclipboard(self) -> None: | ||||||
|         if sys.platform == "darwin": |         if sys.platform == "darwin": | ||||||
|             subprocess.call(["screencapture", "-cx"]) |             subprocess.call(["screencapture", "-cx"]) | ||||||
|  |  | ||||||
|  | @ -4,7 +4,7 @@ from pathlib import Path | ||||||
| 
 | 
 | ||||||
| import pytest | 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 | 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: | def test_iterator() -> None: | ||||||
|     with Image.open("Tests/images/multipage.tiff") as im: |     with Image.open("Tests/images/multipage.tiff") as im: | ||||||
|  |         assert isinstance(im, TiffImagePlugin.TiffImageFile) | ||||||
|         i = ImageSequence.Iterator(im) |         i = ImageSequence.Iterator(im) | ||||||
|         for index in range(im.n_frames): |         for index in range(im.n_frames): | ||||||
|             assert i[index] == next(i) |             assert i[index] == next(i) | ||||||
|  | @ -42,6 +43,7 @@ def test_iterator() -> None: | ||||||
| 
 | 
 | ||||||
| def test_iterator_min_frame() -> None: | def test_iterator_min_frame() -> None: | ||||||
|     with Image.open("Tests/images/hopper.psd") as im: |     with Image.open("Tests/images/hopper.psd") as im: | ||||||
|  |         assert isinstance(im, PsdImagePlugin.PsdImageFile) | ||||||
|         i = ImageSequence.Iterator(im) |         i = ImageSequence.Iterator(im) | ||||||
|         for index in range(1, im.n_frames): |         for index in range(1, im.n_frames): | ||||||
|             assert i[index] == next(i) |             assert i[index] == next(i) | ||||||
|  |  | ||||||
|  | @ -81,6 +81,7 @@ def test_pickle_jpeg() -> None: | ||||||
|         unpickled_image = pickle.loads(pickle.dumps(image)) |         unpickled_image = pickle.loads(pickle.dumps(image)) | ||||||
| 
 | 
 | ||||||
|     # Assert |     # Assert | ||||||
|  |     assert unpickled_image.filename == "Tests/images/hopper.jpg" | ||||||
|     assert len(unpickled_image.layer) == 3 |     assert len(unpickled_image.layer) == 3 | ||||||
|     assert unpickled_image.layers == 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: |     else: | ||||||
|         # Should have a perfect score |         # Should have a perfect score, but pyroma does not support PEP 639 yet. | ||||||
|         assert rating == (10, []) |         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) |             shutil.copy(TEST_JPG, src_file) | ||||||
| 
 | 
 | ||||||
|             with Image.open(src_file) as im: |             with Image.open(src_file) as im: | ||||||
|  |                 assert isinstance(im, JpegImagePlugin.JpegImageFile) | ||||||
|                 im.load_djpeg() |                 im.load_djpeg() | ||||||
| 
 | 
 | ||||||
|     @pytest.mark.skipif(not cjpeg_available(), reason="cjpeg not available") |     @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") |     im.save(out, dpi=(res, res), compression="raw") | ||||||
| 
 | 
 | ||||||
|     with Image.open(out) as reloaded: |     with Image.open(out) as reloaded: | ||||||
|  |         assert isinstance(reloaded, TiffImagePlugin.TiffImageFile) | ||||||
|         assert float(IFDRational(301, 1)) == float(reloaded.tag_v2[282]) |         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 "Please use \`make <target>' where <target> is one of" | ||||||
| 	@echo "  html       to make standalone HTML files" | 	@echo "  html       to make standalone HTML files" | ||||||
| 	@echo "  htmlview   to open the index page built by the html target in your browser" | 	@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 "  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 "  dirhtml    to make HTML files named index.html in directories" | ||||||
| 	@echo "  singlehtml to make a single large HTML file" | 	@echo "  singlehtml to make a single large HTML file" | ||||||
| 	@echo "  pickle     to make pickle files" | 	@echo "  pickle     to make pickle files" | ||||||
|  | @ -201,9 +201,10 @@ doctest: | ||||||
| htmlview: html | htmlview: html | ||||||
| 	$(PYTHON) -c "import os, webbrowser; webbrowser.open('file://' + os.path.realpath('$(BUILDDIR)/html/index.html'))" | 	$(PYTHON) -c "import os, webbrowser; webbrowser.open('file://' + os.path.realpath('$(BUILDDIR)/html/index.html'))" | ||||||
| 
 | 
 | ||||||
| .PHONY: livehtml | .PHONY: htmllive | ||||||
| livehtml: html | htmllive: SPHINXBUILD = $(PYTHON) -m sphinx_autobuild | ||||||
| 	livereload $(BUILDDIR)/html -p 33233 | htmllive: SPHINXOPTS = --open-browser --delay 0 | ||||||
|  | htmllive: html | ||||||
| 
 | 
 | ||||||
| .PHONY: serve | .PHONY: serve | ||||||
| serve: | serve: | ||||||
|  |  | ||||||
|  | @ -186,7 +186,7 @@ ExifTags.IFD.Makernote | ||||||
| Image.Image.get_child_images() | 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 | ``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 | 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 | INT32 and a 32-bit floating point pixel has the range of FLOAT32. The current release | ||||||
| supports the following standard modes: | supports the following standard modes: | ||||||
| 
 | 
 | ||||||
|     * ``1`` (1-bit pixels, black and white, stored with one pixel per byte) | * ``1`` (1-bit pixels, black and white, stored with one pixel per byte) | ||||||
|     * ``L`` (8-bit pixels, grayscale) | * ``L`` (8-bit pixels, grayscale) | ||||||
|     * ``P`` (8-bit pixels, mapped to any other mode using a color palette) | * ``P`` (8-bit pixels, mapped to any other mode using a color palette) | ||||||
|     * ``RGB`` (3x8-bit pixels, true color) | * ``RGB`` (3x8-bit pixels, true color) | ||||||
|     * ``RGBA`` (4x8-bit pixels, true color with transparency mask) | * ``RGBA`` (4x8-bit pixels, true color with transparency mask) | ||||||
|     * ``CMYK`` (4x8-bit pixels, color separation) | * ``CMYK`` (4x8-bit pixels, color separation) | ||||||
|     * ``YCbCr`` (3x8-bit pixels, color video format) | * ``YCbCr`` (3x8-bit pixels, color video format) | ||||||
| 
 | 
 | ||||||
|       * Note that this refers to the JPEG, and not the ITU-R BT.2020, standard |   * 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) | * ``LAB`` (3x8-bit pixels, the L*a*b color space) | ||||||
|     * ``HSV`` (3x8-bit pixels, Hue, Saturation, Value 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 |   * Hue's range of 0-255 is a scaled version of 0 degrees <= Hue < 360 degrees | ||||||
| 
 | 
 | ||||||
|     * ``I`` (32-bit signed integer pixels) | * ``I`` (32-bit signed integer pixels) | ||||||
|     * ``F`` (32-bit floating point pixels) | * ``F`` (32-bit floating point pixels) | ||||||
| 
 | 
 | ||||||
| Pillow also provides limited support for a few additional modes, including: | Pillow also provides limited support for a few additional modes, including: | ||||||
| 
 | 
 | ||||||
|     * ``LA`` (L with alpha) | * ``LA`` (L with alpha) | ||||||
|     * ``PA`` (P with alpha) | * ``PA`` (P with alpha) | ||||||
|     * ``RGBX`` (true color with padding) | * ``RGBX`` (true color with padding) | ||||||
|     * ``RGBa`` (true color with premultiplied alpha) | * ``RGBa`` (true color with premultiplied alpha) | ||||||
|     * ``La`` (L with premultiplied alpha) | * ``La`` (L with premultiplied alpha) | ||||||
|     * ``I;16`` (16-bit unsigned integer pixels) | * ``I;16`` (16-bit unsigned integer pixels) | ||||||
|     * ``I;16L`` (16-bit little endian unsigned integer pixels) | * ``I;16L`` (16-bit little endian unsigned integer pixels) | ||||||
|     * ``I;16B`` (16-bit big endian unsigned integer pixels) | * ``I;16B`` (16-bit big endian unsigned integer pixels) | ||||||
|     * ``I;16N`` (16-bit native 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 | 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)`` | 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 | 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 | BLP | ||||||
| ^^^ | ^^^ | ||||||
| 
 | 
 | ||||||
|  | @ -93,7 +170,7 @@ DXT1 and DXT5 pixel formats can be read, only in ``RGBA`` mode. | ||||||
|    in ``P`` mode. |    in ``P`` mode. | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| .. versionadded:: 11.2.0 | .. versionadded:: 11.2.1 | ||||||
|    DXT1, DXT3, DXT5, BC2, BC3 and BC5 pixel formats can be saved:: |    DXT1, DXT3, DXT5, BC2, BC3 and BC5 pixel formats can be saved:: | ||||||
| 
 | 
 | ||||||
|        im.save(out, pixel_format="DXT1") |        im.save(out, pixel_format="DXT1") | ||||||
|  | @ -235,13 +312,14 @@ following options are available:: | ||||||
|     im.save(out, save_all=True, append_images=[im1, im2, ...]) |     im.save(out, save_all=True, append_images=[im1, im2, ...]) | ||||||
| 
 | 
 | ||||||
| **save_all** | **save_all** | ||||||
|     If present and true, all frames of the image will be saved. If |     If present and true, or if ``append_images`` is not empty, all frames of | ||||||
|     not, then only the first frame of a multiframe image will be saved. |     the image will be saved. Otherwise, only the first frame of a multiframe | ||||||
|  |     image will be saved. | ||||||
| 
 | 
 | ||||||
| **append_images** | **append_images** | ||||||
|     A list of images to append as additional frames. Each of the |     A list of images to append as additional frames. Each of the | ||||||
|     images in the list can be single or multiframe images. |     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 |     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. |     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 | 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`` | 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 | argument is present and true, or if ``append_images`` is not empty, all frames | ||||||
| option will also be available. | will be saved. | ||||||
| 
 | 
 | ||||||
| **append_images** | **append_images** | ||||||
|     A list of images to append as additional pictures. Each of the |     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 | 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`` | 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** | **default_image** | ||||||
|     Boolean value, specifying whether or not the base image is a 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: | The :py:meth:`~PIL.Image.Image.save` method can take the following keyword arguments: | ||||||
| 
 | 
 | ||||||
| **save_all** | **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 |     .. 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 | 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`` | 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 | argument is present and true, or if ``append_images`` is not empty, all frames | ||||||
| options will also be available. | will be saved, and the following options will also be available. | ||||||
| 
 | 
 | ||||||
| **append_images** | **append_images** | ||||||
|     A list of images to append as additional frames. Each of the |     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 |     Transparency color index. This key is omitted if the image is not | ||||||
|     transparent. |     transparent. | ||||||
| 
 | 
 | ||||||
|  | XV Thumbnails | ||||||
|  | ^^^^^^^^^^^^^ | ||||||
|  | 
 | ||||||
|  | Pillow can read XV thumbnail files. | ||||||
|  | 
 | ||||||
| Write-only formats | Write-only formats | ||||||
| ------------------ | ------------------ | ||||||
| 
 | 
 | ||||||
|  | @ -1616,15 +1701,14 @@ The :py:meth:`~PIL.Image.Image.save` method can take the following keyword argum | ||||||
| **save_all** | **save_all** | ||||||
|     If a multiframe image is used, by default, only the first image will be saved. |     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`` |     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 |     .. versionadded:: 3.0.0 | ||||||
| 
 | 
 | ||||||
| **append_images** | **append_images** | ||||||
|     A list of :py:class:`PIL.Image.Image` objects to append as additional pages. Each |     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`` |     of the images in the list can be single or multiframe images. | ||||||
|     parameter must be present and set to ``True`` in conjunction with |  | ||||||
|     ``append_images``. |  | ||||||
| 
 | 
 | ||||||
|     .. versionadded:: 4.2.0 |     .. 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 |     .. versionadded:: 5.3.0 | ||||||
| 
 | 
 | ||||||
| XV Thumbnails |  | ||||||
| ^^^^^^^^^^^^^ |  | ||||||
| 
 |  | ||||||
| Pillow can read XV thumbnail files. |  | ||||||
| 
 |  | ||||||
| Identify-only formats | Identify-only formats | ||||||
| --------------------- | --------------------- | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -534,7 +534,6 @@ You can create animated GIFs with Pillow, e.g. | ||||||
|     # Save the images as an animated GIF |     # Save the images as an animated GIF | ||||||
|     images[0].save( |     images[0].save( | ||||||
|         "animated_hopper.gif", |         "animated_hopper.gif", | ||||||
|         save_all=True, |  | ||||||
|         append_images=images[1:], |         append_images=images[1:], | ||||||
|         duration=500,  # duration of each frame in milliseconds |         duration=500,  # duration of each frame in milliseconds | ||||||
|         loop=0,  # loop forever |         loop=0,  # loop forever | ||||||
|  |  | ||||||
|  | @ -89,6 +89,14 @@ Many of Pillow's features require external libraries: | ||||||
| 
 | 
 | ||||||
| * **libxcb** provides X11 screengrab support. | * **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 | .. tab:: Linux | ||||||
| 
 | 
 | ||||||
|     If you didn't build Python from source, make sure you have Python's |     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 |     To install libraqm, ``sudo apt-get install meson`` and then see | ||||||
|     ``depends/install_raqm.sh``. |     ``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:: |     Prerequisites are installed on recent **Red Hat**, **CentOS** or **Fedora** with:: | ||||||
| 
 | 
 | ||||||
|         sudo dnf install libtiff-devel libjpeg-devel openjpeg2-devel zlib-devel \ |         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 |     The easiest way to install external libraries is via `Homebrew | ||||||
|     <https://brew.sh/>`_. After you install Homebrew, run:: |     <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 | .. tab:: Windows | ||||||
| 
 | 
 | ||||||
|  | @ -187,7 +209,8 @@ Many of Pillow's features require external libraries: | ||||||
|             mingw-w64-x86_64-libwebp \ |             mingw-w64-x86_64-libwebp \ | ||||||
|             mingw-w64-x86_64-openjpeg2 \ |             mingw-w64-x86_64-openjpeg2 \ | ||||||
|             mingw-w64-x86_64-libimagequant \ |             mingw-w64-x86_64-libimagequant \ | ||||||
|             mingw-w64-x86_64-libraqm |             mingw-w64-x86_64-libraqm \ | ||||||
|  |             mingw-w64-x86_64-libavif | ||||||
| 
 | 
 | ||||||
| .. tab:: FreeBSD | .. tab:: FreeBSD | ||||||
| 
 | 
 | ||||||
|  | @ -199,7 +222,7 @@ Many of Pillow's features require external libraries: | ||||||
| 
 | 
 | ||||||
|     Prerequisites are installed on **FreeBSD 10 or 11** with:: |     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. |     Then see ``depends/install_raqm_cmake.sh`` to install libraqm. | ||||||
| 
 | 
 | ||||||
|  | @ -261,14 +284,16 @@ Build Options | ||||||
| * Config settings: ``-C zlib=disable``, ``-C jpeg=disable``, | * Config settings: ``-C zlib=disable``, ``-C jpeg=disable``, | ||||||
|   ``-C tiff=disable``, ``-C freetype=disable``, ``-C raqm=disable``, |   ``-C tiff=disable``, ``-C freetype=disable``, ``-C raqm=disable``, | ||||||
|   ``-C lcms=disable``, ``-C webp=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 |   Disable building the corresponding feature even if the development | ||||||
|   libraries are present on the building machine. |   libraries are present on the building machine. | ||||||
| 
 | 
 | ||||||
| * Config settings: ``-C zlib=enable``, ``-C jpeg=enable``, | * Config settings: ``-C zlib=enable``, ``-C jpeg=enable``, | ||||||
|   ``-C tiff=enable``, ``-C freetype=enable``, ``-C raqm=enable``, |   ``-C tiff=enable``, ``-C freetype=enable``, ``-C raqm=enable``, | ||||||
|   ``-C lcms=enable``, ``-C webp=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 |   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 |   an exception if the libraries are not found. Tcl and Tk must be used | ||||||
|   together. |   together. | ||||||
|  |  | ||||||
|  | @ -23,7 +23,7 @@ These platforms are built and tested for every change. | ||||||
| +----------------------------------+----------------------------+---------------------+ | +----------------------------------+----------------------------+---------------------+ | ||||||
| | Amazon Linux 2023                | 3.9                        | x86-64              | | | Amazon Linux 2023                | 3.9                        | x86-64              | | ||||||
| +----------------------------------+----------------------------+---------------------+ | +----------------------------------+----------------------------+---------------------+ | ||||||
| | Arch                             | 3.12                       | x86-64              | | | Arch                             | 3.13                       | x86-64              | | ||||||
| +----------------------------------+----------------------------+---------------------+ | +----------------------------------+----------------------------+---------------------+ | ||||||
| | CentOS Stream 9                  | 3.9                        | 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         | | | Debian 12 Bookworm               | 3.11                       | x86, x86-64         | | ||||||
| +----------------------------------+----------------------------+---------------------+ | +----------------------------------+----------------------------+---------------------+ | ||||||
| | Fedora 40                        | 3.12                       | x86-64              | |  | ||||||
| +----------------------------------+----------------------------+---------------------+ |  | ||||||
| | Fedora 41                        | 3.13                       | x86-64              | | | Fedora 41                        | 3.13                       | x86-64              | | ||||||
| +----------------------------------+----------------------------+---------------------+ | +----------------------------------+----------------------------+---------------------+ | ||||||
|  | | Fedora 42                        | 3.13                       | x86-64              | | ||||||
|  | +----------------------------------+----------------------------+---------------------+ | ||||||
| | Gentoo                           | 3.12                       | x86-64              | | | Gentoo                           | 3.12                       | x86-64              | | ||||||
| +----------------------------------+----------------------------+---------------------+ | +----------------------------------+----------------------------+---------------------+ | ||||||
| | macOS 13 Ventura                 | 3.9                        | 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     | | | Operating system                 | | Tested Python            | | Latest tested  | | Tested     | | ||||||
| |                                  | | versions                 | | Pillow version | | processors | | |                                  | | 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           |              | | |                                  | 3.8                        | 10.4.0           |              | | ||||||
| +----------------------------------+----------------------------+------------------+--------------+ | +----------------------------------+----------------------------+------------------+--------------+ | ||||||
|  |  | ||||||
|  | @ -79,6 +79,7 @@ Constructing images | ||||||
| 
 | 
 | ||||||
| .. autofunction:: new | .. autofunction:: new | ||||||
| .. autofunction:: fromarray | .. autofunction:: fromarray | ||||||
|  | .. autofunction:: fromarrow | ||||||
| .. autofunction:: frombytes | .. autofunction:: frombytes | ||||||
| .. autofunction:: frombuffer | .. autofunction:: frombuffer | ||||||
| 
 | 
 | ||||||
|  | @ -370,6 +371,8 @@ Protocols | ||||||
| 
 | 
 | ||||||
| .. autoclass:: SupportsArrayInterface | .. autoclass:: SupportsArrayInterface | ||||||
|     :show-inheritance: |     :show-inheritance: | ||||||
|  | .. autoclass:: SupportsArrowArrayInterface | ||||||
|  |     :show-inheritance: | ||||||
| .. autoclass:: SupportsGetData | .. autoclass:: SupportsGetData | ||||||
|     :show-inheritance: |     :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. |         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 |     .. py:attribute:: media_white_point_temperature | ||||||
|         :type: float | None |         :type: float | None | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -391,7 +391,7 @@ Methods | ||||||
|                   the relative alignment of lines. Use the ``anchor`` parameter to |                   the relative alignment of lines. Use the ``anchor`` parameter to | ||||||
|                   specify the alignment to ``xy``. |                   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 |     :param direction: Direction of the text. It can be ``"rtl"`` (right to | ||||||
|                       left), ``"ltr"`` (left to right) or ``"ttb"`` (top to bottom). |                       left), ``"ltr"`` (left to right) or ``"ttb"`` (top to bottom). | ||||||
|                       Requires libraqm. |                       Requires libraqm. | ||||||
|  | @ -462,7 +462,7 @@ Methods | ||||||
|                   the relative alignment of lines. Use the ``anchor`` parameter to |                   the relative alignment of lines. Use the ``anchor`` parameter to | ||||||
|                   specify the alignment to ``xy``. |                   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 |     :param direction: Direction of the text. It can be ``"rtl"`` (right to | ||||||
|                       left), ``"ltr"`` (left to right) or ``"ttb"`` (top to bottom). |                       left), ``"ltr"`` (left to right) or ``"ttb"`` (top to bottom). | ||||||
|                       Requires libraqm. |                       Requires libraqm. | ||||||
|  | @ -609,7 +609,7 @@ Methods | ||||||
|                   the relative alignment of lines. Use the ``anchor`` parameter to |                   the relative alignment of lines. Use the ``anchor`` parameter to | ||||||
|                   specify the alignment to ``xy``. |                   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 |     :param direction: Direction of the text. It can be ``"rtl"`` (right to | ||||||
|                       left), ``"ltr"`` (left to right) or ``"ttb"`` (top to bottom). |                       left), ``"ltr"`` (left to right) or ``"ttb"`` (top to bottom). | ||||||
|                       Requires libraqm. |                       Requires libraqm. | ||||||
|  | @ -663,7 +663,7 @@ Methods | ||||||
|                   the relative alignment of lines. Use the ``anchor`` parameter to |                   the relative alignment of lines. Use the ``anchor`` parameter to | ||||||
|                   specify the alignment to ``xy``. |                   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 |     :param direction: Direction of the text. It can be ``"rtl"`` (right to | ||||||
|                       left), ``"ltr"`` (left to right) or ``"ttb"`` (top to bottom). |                       left), ``"ltr"`` (left to right) or ``"ttb"`` (top to bottom). | ||||||
|                       Requires libraqm. |                       Requires libraqm. | ||||||
|  |  | ||||||
|  | @ -9,15 +9,16 @@ or the clipboard to a PIL image memory. | ||||||
| 
 | 
 | ||||||
| .. versionadded:: 1.1.3 | .. 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 |     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, |     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. |     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 |     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 |     a snapshot of the screen, ``gnome-screenshot``, ``grim`` or ``spectacle`` will be | ||||||
|     installed. To disable this behaviour, pass ``xdisplay=""`` instead. |     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) |     .. 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"``. |         You can check X11 support using :py:func:`PIL.features.check_feature` with ``feature="xcb"``. | ||||||
| 
 | 
 | ||||||
|         .. versionadded:: 7.1.0 |         .. versionadded:: 7.1.0 | ||||||
|  | 
 | ||||||
|  |     :param window: | ||||||
|  |         HWND, to capture a single window. Windows only. | ||||||
|  | 
 | ||||||
|  |         .. versionadded:: 11.2.1 | ||||||
|     :return: An image |     :return: An image | ||||||
| 
 | 
 | ||||||
| .. py:function:: grabclipboard() | .. 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 | 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 | default, but can be enabled and tweaked using three environment | ||||||
| variables: | 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 |   allocations. Valid values are powers of 2 between 1 and | ||||||
|     128, inclusive. Defaults to 1. |   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 |   block size for ``ImagingAllocateArray``. Valid values are | ||||||
|     integers, with an optional ``k`` or ``m`` suffix. Defaults to 16M. |   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 |   retain to fill future memory requests. Any freed blocks over this | ||||||
|     threshold will be returned to the OS immediately. Defaults to 0. |   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`. | * ``freetype2``: FreeType font support via :py:func:`PIL.ImageFont.truetype`. | ||||||
| * ``littlecms2``: LittleCMS 2 support via :py:mod:`PIL.ImageCms`. | * ``littlecms2``: LittleCMS 2 support via :py:mod:`PIL.ImageCms`. | ||||||
| * ``webp``: WebP image support. | * ``webp``: WebP image support. | ||||||
|  | * ``avif``: AVIF image support. | ||||||
| 
 | 
 | ||||||
| .. autofunction:: PIL.features.check_module | .. autofunction:: PIL.features.check_module | ||||||
| .. autofunction:: PIL.features.version_module | .. autofunction:: PIL.features.version_module | ||||||
|  |  | ||||||
|  | @ -9,3 +9,4 @@ Internal Reference | ||||||
|   block_allocator |   block_allocator | ||||||
|   internal_modules |   internal_modules | ||||||
|   c_extension_debugging |   c_extension_debugging | ||||||
|  |   arrow_support | ||||||
|  |  | ||||||
|  | @ -1,6 +1,14 @@ | ||||||
| Plugin reference | Plugin reference | ||||||
| ================ | ================ | ||||||
| 
 | 
 | ||||||
|  | :mod:`~PIL.AvifImagePlugin` Module | ||||||
|  | ---------------------------------- | ||||||
|  | 
 | ||||||
|  | .. automodule:: PIL.AvifImagePlugin | ||||||
|  |     :members: | ||||||
|  |     :undoc-members: | ||||||
|  |     :show-inheritance: | ||||||
|  | 
 | ||||||
| :mod:`~PIL.BmpImagePlugin` Module | :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:: | .. toctree:: | ||||||
|   :maxdepth: 2 |   :maxdepth: 2 | ||||||
| 
 | 
 | ||||||
|   11.2.0 |   11.2.1 | ||||||
|   11.1.0 |   11.1.0 | ||||||
|   11.0.0 |   11.0.0 | ||||||
|   10.4.0 |   10.4.0 | ||||||
|  |  | ||||||
|  | @ -1,7 +1,7 @@ | ||||||
| [build-system] | [build-system] | ||||||
| build-backend = "backend" | build-backend = "backend" | ||||||
| requires = [ | requires = [ | ||||||
|   "setuptools>=67.8", |   "setuptools>=77", | ||||||
| ] | ] | ||||||
| backend-path = [ | backend-path = [ | ||||||
|   "_custom_build", |   "_custom_build", | ||||||
|  | @ -14,14 +14,14 @@ readme = "README.md" | ||||||
| keywords = [ | keywords = [ | ||||||
|   "Imaging", |   "Imaging", | ||||||
| ] | ] | ||||||
| license = { text = "MIT-CMU" } | license = "MIT-CMU" | ||||||
|  | license-files = [ "LICENSE" ] | ||||||
| authors = [ | authors = [ | ||||||
|   { name = "Jeffrey A. Clark", email = "aclark@aclark.net" }, |   { name = "Jeffrey A. Clark", email = "aclark@aclark.net" }, | ||||||
| ] | ] | ||||||
| requires-python = ">=3.9" | requires-python = ">=3.9" | ||||||
| classifiers = [ | classifiers = [ | ||||||
|   "Development Status :: 6 - Mature", |   "Development Status :: 6 - Mature", | ||||||
|   "License :: OSI Approved :: CMU License (MIT-CMU)", |  | ||||||
|   "Programming Language :: Python :: 3 :: Only", |   "Programming Language :: Python :: 3 :: Only", | ||||||
|   "Programming Language :: Python :: 3.9", |   "Programming Language :: Python :: 3.9", | ||||||
|   "Programming Language :: Python :: 3.10", |   "Programming Language :: Python :: 3.10", | ||||||
|  | @ -44,6 +44,7 @@ optional-dependencies.docs = [ | ||||||
|   "furo", |   "furo", | ||||||
|   "olefile", |   "olefile", | ||||||
|   "sphinx>=8.2", |   "sphinx>=8.2", | ||||||
|  |   "sphinx-autobuild", | ||||||
|   "sphinx-copybutton", |   "sphinx-copybutton", | ||||||
|   "sphinx-inline-tabs", |   "sphinx-inline-tabs", | ||||||
|   "sphinxext-opengraph", |   "sphinxext-opengraph", | ||||||
|  | @ -54,6 +55,10 @@ optional-dependencies.fpx = [ | ||||||
| optional-dependencies.mic = [ | optional-dependencies.mic = [ | ||||||
|   "olefile", |   "olefile", | ||||||
| ] | ] | ||||||
|  | optional-dependencies.test-arrow = [ | ||||||
|  |   "pyarrow", | ||||||
|  | ] | ||||||
|  | 
 | ||||||
| optional-dependencies.tests = [ | optional-dependencies.tests = [ | ||||||
|   "check-manifest", |   "check-manifest", | ||||||
|   "coverage>=7.4.2", |   "coverage>=7.4.2", | ||||||
|  | @ -67,6 +72,7 @@ optional-dependencies.tests = [ | ||||||
|   "pytest-timeout", |   "pytest-timeout", | ||||||
|   "trove-classifiers>=2024.10.12", |   "trove-classifiers>=2024.10.12", | ||||||
| ] | ] | ||||||
|  | 
 | ||||||
| optional-dependencies.typing = [ | optional-dependencies.typing = [ | ||||||
|   "typing-extensions; python_version<'3.10'", |   "typing-extensions; python_version<'3.10'", | ||||||
| ] | ] | ||||||
|  |  | ||||||
Some files were not shown because too many files have changed in this diff Show More
		Loading…
	
		Reference in New Issue
	
	Block a user