mirror of
				https://github.com/python-pillow/Pillow.git
				synced 2025-10-30 15:37:55 +03:00 
			
		
		
		
	Merge branch 'main' into main
This commit is contained in:
		
						commit
						4c94fa4d13
					
				|  | @ -20,10 +20,10 @@ fi | ||||||
| set -e | set -e | ||||||
| 
 | 
 | ||||||
| if [[ $(uname) != CYGWIN* ]]; then | if [[ $(uname) != CYGWIN* ]]; then | ||||||
|     sudo apt-get -qq install libfreetype6-dev liblcms2-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.0 | 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 | ||||||
|  |  | ||||||
							
								
								
									
										2
									
								
								.github/renovate.json
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/renovate.json
									
									
									
									
										vendored
									
									
								
							|  | @ -16,6 +16,6 @@ | ||||||
|         } |         } | ||||||
|     ], |     ], | ||||||
|     "schedule": [ |     "schedule": [ | ||||||
|         "on the 3rd day of the month" |         "* * 3 * *" | ||||||
|     ] |     ] | ||||||
| } | } | ||||||
|  |  | ||||||
							
								
								
									
										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 | ||||||
|  |  | ||||||
							
								
								
									
										15
									
								
								.github/workflows/test-docker.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										15
									
								
								.github/workflows/test-docker.yml
									
									
									
									
										vendored
									
									
								
							|  | @ -35,6 +35,10 @@ jobs: | ||||||
|       matrix: |       matrix: | ||||||
|         os: ["ubuntu-latest"] |         os: ["ubuntu-latest"] | ||||||
|         docker: [ |         docker: [ | ||||||
|  |           # Run slower jobs first to give them a headstart and reduce waiting time | ||||||
|  |           ubuntu-24.04-noble-ppc64le, | ||||||
|  |           ubuntu-24.04-noble-s390x, | ||||||
|  |           # Then run the remainder | ||||||
|           alpine, |           alpine, | ||||||
|           amazon-2-amd64, |           amazon-2-amd64, | ||||||
|           amazon-2023-amd64, |           amazon-2023-amd64, | ||||||
|  | @ -43,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, | ||||||
|  | @ -52,13 +56,9 @@ jobs: | ||||||
|         dockerTag: [main] |         dockerTag: [main] | ||||||
|         include: |         include: | ||||||
|           - docker: "ubuntu-24.04-noble-ppc64le" |           - docker: "ubuntu-24.04-noble-ppc64le" | ||||||
|             os: "ubuntu-22.04" |  | ||||||
|             qemu-arch: "ppc64le" |             qemu-arch: "ppc64le" | ||||||
|             dockerTag: main |  | ||||||
|           - docker: "ubuntu-24.04-noble-s390x" |           - docker: "ubuntu-24.04-noble-s390x" | ||||||
|             os: "ubuntu-22.04" |  | ||||||
|             qemu-arch: "s390x" |             qemu-arch: "s390x" | ||||||
|             dockerTag: main |  | ||||||
|           - docker: "ubuntu-24.04-noble-arm64v8" |           - docker: "ubuntu-24.04-noble-arm64v8" | ||||||
|             os: "ubuntu-24.04-arm" |             os: "ubuntu-24.04-arm" | ||||||
|             dockerTag: main |             dockerTag: main | ||||||
|  | @ -75,8 +75,9 @@ jobs: | ||||||
| 
 | 
 | ||||||
|     - name: Set up QEMU |     - name: Set up QEMU | ||||||
|       if: "matrix.qemu-arch" |       if: "matrix.qemu-arch" | ||||||
|       run: | |       uses: docker/setup-qemu-action@v3 | ||||||
|         docker run --rm --privileged aptman/qus -s -- -p ${{ matrix.qemu-arch }} |       with: | ||||||
|  |         platforms: ${{ matrix.qemu-arch }} | ||||||
| 
 | 
 | ||||||
|     - name: Docker pull |     - name: Docker pull | ||||||
|       run: | |       run: | | ||||||
|  |  | ||||||
							
								
								
									
										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 \ | ||||||
|  |  | ||||||
							
								
								
									
										14
									
								
								.github/workflows/test-windows.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										14
									
								
								.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,14 +88,18 @@ 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: | | ||||||
|         choco install nasm --no-progress |         choco install nasm --no-progress | ||||||
|         echo "C:\Program Files\NASM" >> $env:GITHUB_PATH |         echo "C:\Program Files\NASM" >> $env:GITHUB_PATH | ||||||
| 
 | 
 | ||||||
|         choco install ghostscript --version=10.4.0 --no-progress |         choco install ghostscript --version=10.5.0 --no-progress | ||||||
|         echo "C:\Program Files\gs\gs10.04.0\bin" >> $env:GITHUB_PATH |         echo "C:\Program Files\gs\gs10.05.0\bin" >> $env:GITHUB_PATH | ||||||
| 
 | 
 | ||||||
|         # Install extra test images |         # Install extra test images | ||||||
|         xcopy /S /Y Tests\test-images\* Tests\images |         xcopy /S /Y Tests\test-images\* Tests\images | ||||||
|  | @ -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.6.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 | ||||||
|  |     if [[ -n "$IS_MACOS" ]] && [[ "$MACOSX_DEPLOYMENT_TARGET" == "10.10" || "$MACOSX_DEPLOYMENT_TARGET" == "10.13" ]]; then | ||||||
|  |         build_new_zlib | ||||||
|  |     else | ||||||
|         build_zlib_ng |         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,4 +1,4 @@ | ||||||
| Pillow Tests | Pillow tests | ||||||
| ============ | ============ | ||||||
| 
 | 
 | ||||||
| Test scripts are named ``test_xxx.py``. Helper classes and functions can be found in ``helper.py``. | Test scripts are named ``test_xxx.py``. Helper classes and functions can be found in ``helper.py``. | ||||||
|  |  | ||||||
|  | @ -9,6 +9,6 @@ from PIL import Image | ||||||
| 
 | 
 | ||||||
| def test_j2k_overflow(tmp_path: Path) -> None: | def test_j2k_overflow(tmp_path: Path) -> None: | ||||||
|     im = Image.new("RGBA", (1024, 131584)) |     im = Image.new("RGBA", (1024, 131584)) | ||||||
|     target = str(tmp_path / "temp.jpc") |     target = tmp_path / "temp.jpc" | ||||||
|     with pytest.raises(OSError): |     with pytest.raises(OSError): | ||||||
|         im.save(target) |         im.save(target) | ||||||
|  |  | ||||||
|  | @ -32,7 +32,7 @@ pytestmark = pytest.mark.skipif(sys.maxsize <= 2**32, reason="requires 64-bit sy | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def _write_png(tmp_path: Path, xdim: int, ydim: int) -> None: | def _write_png(tmp_path: Path, xdim: int, ydim: int) -> None: | ||||||
|     f = str(tmp_path / "temp.png") |     f = tmp_path / "temp.png" | ||||||
|     im = Image.new("L", (xdim, ydim), 0) |     im = Image.new("L", (xdim, ydim), 0) | ||||||
|     im.save(f) |     im.save(f) | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -28,7 +28,7 @@ pytestmark = pytest.mark.skipif(sys.maxsize <= 2**32, reason="requires 64-bit sy | ||||||
| def _write_png(tmp_path: Path, xdim: int, ydim: int) -> None: | def _write_png(tmp_path: Path, xdim: int, ydim: int) -> None: | ||||||
|     dtype = np.uint8 |     dtype = np.uint8 | ||||||
|     a = np.zeros((xdim, ydim), dtype=dtype) |     a = np.zeros((xdim, ydim), dtype=dtype) | ||||||
|     f = str(tmp_path / "temp.png") |     f = tmp_path / "temp.png" | ||||||
|     im = Image.fromarray(a, "L") |     im = Image.fromarray(a, "L") | ||||||
|     im.save(f) |     im.save(f) | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -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 | ||||||
|  |  | ||||||
|  | @ -13,6 +13,7 @@ import tempfile | ||||||
| from collections.abc import Sequence | from collections.abc import Sequence | ||||||
| from functools import lru_cache | from functools import lru_cache | ||||||
| from io import BytesIO | from io import BytesIO | ||||||
|  | from pathlib import Path | ||||||
| from typing import Any, Callable | from typing import Any, Callable | ||||||
| 
 | 
 | ||||||
| import pytest | import pytest | ||||||
|  | @ -95,7 +96,10 @@ def assert_image_equal(a: Image.Image, b: Image.Image, msg: str | None = None) - | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def assert_image_equal_tofile( | def assert_image_equal_tofile( | ||||||
|     a: Image.Image, filename: str, msg: str | None = None, mode: str | None = None |     a: Image.Image, | ||||||
|  |     filename: str | Path, | ||||||
|  |     msg: str | None = None, | ||||||
|  |     mode: str | None = None, | ||||||
| ) -> None: | ) -> None: | ||||||
|     with Image.open(filename) as img: |     with Image.open(filename) as img: | ||||||
|         if mode: |         if mode: | ||||||
|  | @ -136,7 +140,7 @@ def assert_image_similar( | ||||||
| 
 | 
 | ||||||
| def assert_image_similar_tofile( | def assert_image_similar_tofile( | ||||||
|     a: Image.Image, |     a: Image.Image, | ||||||
|     filename: str, |     filename: str | Path, | ||||||
|     epsilon: float, |     epsilon: float, | ||||||
|     msg: str | None = None, |     msg: str | None = None, | ||||||
| ) -> None: | ) -> None: | ||||||
|  |  | ||||||
							
								
								
									
										
											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) | ||||||
|  | @ -12,6 +12,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" | ||||||
|  | @ -20,6 +21,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" | ||||||
|  | @ -34,8 +36,11 @@ def test_apng_basic() -> None: | ||||||
|         with pytest.raises(EOFError): |         with pytest.raises(EOFError): | ||||||
|             im.seek(2) |             im.seek(2) | ||||||
| 
 | 
 | ||||||
|         # test rewind support |  | ||||||
|         im.seek(0) |         im.seek(0) | ||||||
|  |         with pytest.raises(ValueError, match="cannot seek to frame 2"): | ||||||
|  |             im._seek(2) | ||||||
|  | 
 | ||||||
|  |         # test rewind support | ||||||
|         assert im.getpixel((0, 0)) == (255, 0, 0, 255) |         assert im.getpixel((0, 0)) == (255, 0, 0, 255) | ||||||
|         assert im.getpixel((64, 32)) == (255, 0, 0, 255) |         assert im.getpixel((64, 32)) == (255, 0, 0, 255) | ||||||
|         im.seek(1) |         im.seek(1) | ||||||
|  | @ -49,6 +54,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) | ||||||
|  | @ -56,31 +62,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) | ||||||
|  | @ -88,21 +100,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) | ||||||
|  | @ -129,6 +145,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) | ||||||
| 
 | 
 | ||||||
|  | @ -142,26 +159,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) | ||||||
|  | @ -175,6 +197,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) | ||||||
|  | @ -230,24 +253,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") | ||||||
|  | @ -255,6 +282,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") | ||||||
|  | @ -262,6 +290,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") | ||||||
|  | @ -271,25 +300,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) | ||||||
| 
 | 
 | ||||||
|  | @ -297,26 +332,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() | ||||||
| 
 | 
 | ||||||
|  | @ -336,16 +376,18 @@ 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() | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def test_apng_save(tmp_path: Path) -> None: | def test_apng_save(tmp_path: Path) -> None: | ||||||
|     with Image.open("Tests/images/apng/single_frame.png") as im: |     with Image.open("Tests/images/apng/single_frame.png") as im: | ||||||
|         test_file = str(tmp_path / "temp.png") |         test_file = tmp_path / "temp.png" | ||||||
|         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 | ||||||
|  | @ -361,6 +403,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 | ||||||
|  | @ -372,7 +415,7 @@ def test_apng_save(tmp_path: Path) -> None: | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def test_apng_save_alpha(tmp_path: Path) -> None: | def test_apng_save_alpha(tmp_path: Path) -> None: | ||||||
|     test_file = str(tmp_path / "temp.png") |     test_file = tmp_path / "temp.png" | ||||||
| 
 | 
 | ||||||
|     im = Image.new("RGBA", (1, 1), (255, 0, 0, 255)) |     im = Image.new("RGBA", (1, 1), (255, 0, 0, 255)) | ||||||
|     im2 = Image.new("RGBA", (1, 1), (255, 0, 0, 127)) |     im2 = Image.new("RGBA", (1, 1), (255, 0, 0, 127)) | ||||||
|  | @ -390,7 +433,7 @@ def test_apng_save_split_fdat(tmp_path: Path) -> None: | ||||||
|     # frames with image data spanning multiple fdAT chunks (in this case |     # frames with image data spanning multiple fdAT chunks (in this case | ||||||
|     # both the default image and first animation frame will span multiple |     # both the default image and first animation frame will span multiple | ||||||
|     # data chunks) |     # data chunks) | ||||||
|     test_file = str(tmp_path / "temp.png") |     test_file = tmp_path / "temp.png" | ||||||
|     with Image.open("Tests/images/old-style-jpeg-compression.png") as im: |     with Image.open("Tests/images/old-style-jpeg-compression.png") as im: | ||||||
|         frames = [im.copy(), Image.new("RGBA", im.size, (255, 0, 0, 255))] |         frames = [im.copy(), Image.new("RGBA", im.size, (255, 0, 0, 255))] | ||||||
|         im.save( |         im.save( | ||||||
|  | @ -400,12 +443,13 @@ 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() | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def test_apng_save_duration_loop(tmp_path: Path) -> None: | def test_apng_save_duration_loop(tmp_path: Path) -> None: | ||||||
|     test_file = str(tmp_path / "temp.png") |     test_file = tmp_path / "temp.png" | ||||||
|     with Image.open("Tests/images/apng/delay.png") as im: |     with Image.open("Tests/images/apng/delay.png") as im: | ||||||
|         frames = [] |         frames = [] | ||||||
|         durations = [] |         durations = [] | ||||||
|  | @ -442,6 +486,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 | ||||||
| 
 | 
 | ||||||
|  | @ -453,6 +498,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 | ||||||
| 
 | 
 | ||||||
|  | @ -463,12 +509,13 @@ 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 | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def test_apng_save_disposal(tmp_path: Path) -> None: | def test_apng_save_disposal(tmp_path: Path) -> None: | ||||||
|     test_file = str(tmp_path / "temp.png") |     test_file = tmp_path / "temp.png" | ||||||
|     size = (128, 64) |     size = (128, 64) | ||||||
|     red = Image.new("RGBA", size, (255, 0, 0, 255)) |     red = Image.new("RGBA", size, (255, 0, 0, 255)) | ||||||
|     green = Image.new("RGBA", size, (0, 255, 0, 255)) |     green = Image.new("RGBA", size, (0, 255, 0, 255)) | ||||||
|  | @ -569,7 +616,7 @@ def test_apng_save_disposal(tmp_path: Path) -> None: | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def test_apng_save_disposal_previous(tmp_path: Path) -> None: | def test_apng_save_disposal_previous(tmp_path: Path) -> None: | ||||||
|     test_file = str(tmp_path / "temp.png") |     test_file = tmp_path / "temp.png" | ||||||
|     size = (128, 64) |     size = (128, 64) | ||||||
|     blue = Image.new("RGBA", size, (0, 0, 255, 255)) |     blue = Image.new("RGBA", size, (0, 0, 255, 255)) | ||||||
|     red = Image.new("RGBA", size, (255, 0, 0, 255)) |     red = Image.new("RGBA", size, (255, 0, 0, 255)) | ||||||
|  | @ -591,7 +638,7 @@ def test_apng_save_disposal_previous(tmp_path: Path) -> None: | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def test_apng_save_blend(tmp_path: Path) -> None: | def test_apng_save_blend(tmp_path: Path) -> None: | ||||||
|     test_file = str(tmp_path / "temp.png") |     test_file = tmp_path / "temp.png" | ||||||
|     size = (128, 64) |     size = (128, 64) | ||||||
|     red = Image.new("RGBA", size, (255, 0, 0, 255)) |     red = Image.new("RGBA", size, (255, 0, 0, 255)) | ||||||
|     green = Image.new("RGBA", size, (0, 255, 0, 255)) |     green = Image.new("RGBA", size, (0, 255, 0, 255)) | ||||||
|  | @ -659,7 +706,7 @@ def test_apng_save_blend(tmp_path: Path) -> None: | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def test_apng_save_size(tmp_path: Path) -> None: | def test_apng_save_size(tmp_path: Path) -> None: | ||||||
|     test_file = str(tmp_path / "temp.png") |     test_file = tmp_path / "temp.png" | ||||||
| 
 | 
 | ||||||
|     im = Image.new("L", (100, 100)) |     im = Image.new("L", (100, 100)) | ||||||
|     im.save(test_file, save_all=True, append_images=[Image.new("L", (200, 200))]) |     im.save(test_file, save_all=True, append_images=[Image.new("L", (200, 200))]) | ||||||
|  | @ -683,7 +730,7 @@ def test_seek_after_close() -> None: | ||||||
| def test_different_modes_in_later_frames( | def test_different_modes_in_later_frames( | ||||||
|     mode: str, default_image: bool, duplicate: bool, tmp_path: Path |     mode: str, default_image: bool, duplicate: bool, tmp_path: Path | ||||||
| ) -> None: | ) -> None: | ||||||
|     test_file = str(tmp_path / "temp.png") |     test_file = tmp_path / "temp.png" | ||||||
| 
 | 
 | ||||||
|     im = Image.new("L", (1, 1)) |     im = Image.new("L", (1, 1)) | ||||||
|     im.save( |     im.save( | ||||||
|  | @ -697,7 +744,7 @@ def test_different_modes_in_later_frames( | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def test_different_durations(tmp_path: Path) -> None: | def test_different_durations(tmp_path: Path) -> None: | ||||||
|     test_file = str(tmp_path / "temp.png") |     test_file = tmp_path / "temp.png" | ||||||
| 
 | 
 | ||||||
|     with Image.open("Tests/images/apng/different_durations.png") as im: |     with Image.open("Tests/images/apng/different_durations.png") as im: | ||||||
|         for _ in range(3): |         for _ in range(3): | ||||||
|  |  | ||||||
							
								
								
									
										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) | ||||||
|  | @ -46,7 +46,7 @@ def test_invalid_file() -> None: | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def test_save(tmp_path: Path) -> None: | def test_save(tmp_path: Path) -> None: | ||||||
|     f = str(tmp_path / "temp.blp") |     f = tmp_path / "temp.blp" | ||||||
| 
 | 
 | ||||||
|     for version in ("BLP1", "BLP2"): |     for version in ("BLP1", "BLP2"): | ||||||
|         im = hopper("P") |         im = hopper("P") | ||||||
|  | @ -56,7 +56,7 @@ def test_save(tmp_path: Path) -> None: | ||||||
|             assert_image_equal(im.convert("RGB"), reloaded) |             assert_image_equal(im.convert("RGB"), reloaded) | ||||||
| 
 | 
 | ||||||
|         with Image.open("Tests/images/transparent.png") as im: |         with Image.open("Tests/images/transparent.png") as im: | ||||||
|             f = str(tmp_path / "temp.blp") |             f = tmp_path / "temp.blp" | ||||||
|             im.convert("P").save(f, blp_version=version) |             im.convert("P").save(f, blp_version=version) | ||||||
| 
 | 
 | ||||||
|             with Image.open(f) as reloaded: |             with Image.open(f) as reloaded: | ||||||
|  |  | ||||||
|  | @ -15,10 +15,11 @@ 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 = str(tmp_path / "temp.bmp") |     outfile = tmp_path / "temp.bmp" | ||||||
| 
 | 
 | ||||||
|  |     im = hopper(mode) | ||||||
|     im.save(outfile, "BMP") |     im.save(outfile, "BMP") | ||||||
| 
 | 
 | ||||||
|     with Image.open(outfile) as reloaded: |     with Image.open(outfile) as reloaded: | ||||||
|  | @ -28,13 +29,6 @@ def test_sanity(tmp_path: Path) -> None: | ||||||
|         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: | ||||||
|     with open("Tests/images/flower.jpg", "rb") as fp: |     with open("Tests/images/flower.jpg", "rb") as fp: | ||||||
|  | @ -66,7 +60,7 @@ def test_small_palette(tmp_path: Path) -> None: | ||||||
|     colors = [0, 0, 0, 125, 125, 125, 255, 255, 255] |     colors = [0, 0, 0, 125, 125, 125, 255, 255, 255] | ||||||
|     im.putpalette(colors) |     im.putpalette(colors) | ||||||
| 
 | 
 | ||||||
|     out = str(tmp_path / "temp.bmp") |     out = tmp_path / "temp.bmp" | ||||||
|     im.save(out) |     im.save(out) | ||||||
| 
 | 
 | ||||||
|     with Image.open(out) as reloaded: |     with Image.open(out) as reloaded: | ||||||
|  | @ -74,7 +68,7 @@ def test_small_palette(tmp_path: Path) -> None: | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def test_save_too_large(tmp_path: Path) -> None: | def test_save_too_large(tmp_path: Path) -> None: | ||||||
|     outfile = str(tmp_path / "temp.bmp") |     outfile = tmp_path / "temp.bmp" | ||||||
|     with Image.new("RGB", (1, 1)) as im: |     with Image.new("RGB", (1, 1)) as im: | ||||||
|         im._size = (37838, 37838) |         im._size = (37838, 37838) | ||||||
|         with pytest.raises(ValueError): |         with pytest.raises(ValueError): | ||||||
|  | @ -96,7 +90,7 @@ def test_dpi() -> None: | ||||||
| def test_save_bmp_with_dpi(tmp_path: Path) -> None: | def test_save_bmp_with_dpi(tmp_path: Path) -> None: | ||||||
|     # Test for #1301 |     # Test for #1301 | ||||||
|     # Arrange |     # Arrange | ||||||
|     outfile = str(tmp_path / "temp.jpg") |     outfile = tmp_path / "temp.jpg" | ||||||
|     with Image.open("Tests/images/hopper.bmp") as im: |     with Image.open("Tests/images/hopper.bmp") as im: | ||||||
|         assert im.info["dpi"] == (95.98654816726399, 95.98654816726399) |         assert im.info["dpi"] == (95.98654816726399, 95.98654816726399) | ||||||
| 
 | 
 | ||||||
|  | @ -112,7 +106,7 @@ def test_save_bmp_with_dpi(tmp_path: Path) -> None: | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def test_save_float_dpi(tmp_path: Path) -> None: | def test_save_float_dpi(tmp_path: Path) -> None: | ||||||
|     outfile = str(tmp_path / "temp.bmp") |     outfile = tmp_path / "temp.bmp" | ||||||
|     with Image.open("Tests/images/hopper.bmp") as im: |     with Image.open("Tests/images/hopper.bmp") as im: | ||||||
|         im.save(outfile, dpi=(72.21216100543306, 72.21216100543306)) |         im.save(outfile, dpi=(72.21216100543306, 72.21216100543306)) | ||||||
|         with Image.open(outfile) as reloaded: |         with Image.open(outfile) as reloaded: | ||||||
|  | @ -152,7 +146,7 @@ def test_dib_header_size(header_size: int, path: str) -> None: | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def test_save_dib(tmp_path: Path) -> None: | def test_save_dib(tmp_path: Path) -> None: | ||||||
|     outfile = str(tmp_path / "temp.dib") |     outfile = tmp_path / "temp.dib" | ||||||
| 
 | 
 | ||||||
|     with Image.open("Tests/images/clipboard.dib") as im: |     with Image.open("Tests/images/clipboard.dib") as im: | ||||||
|         im.save(outfile) |         im.save(outfile) | ||||||
|  | @ -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" | ||||||
|  |  | ||||||
|  | @ -43,7 +43,7 @@ def test_load() -> None: | ||||||
| def test_save(tmp_path: Path) -> None: | def test_save(tmp_path: Path) -> None: | ||||||
|     # Arrange |     # Arrange | ||||||
|     im = hopper() |     im = hopper() | ||||||
|     tmpfile = str(tmp_path / "temp.bufr") |     tmpfile = tmp_path / "temp.bufr" | ||||||
| 
 | 
 | ||||||
|     # 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): | ||||||
|  | @ -79,7 +79,7 @@ def test_handler(tmp_path: Path) -> None: | ||||||
|         im.load() |         im.load() | ||||||
|         assert handler.is_loaded() |         assert handler.is_loaded() | ||||||
| 
 | 
 | ||||||
|         temp_file = str(tmp_path / "temp.bufr") |         temp_file = tmp_path / "temp.bufr" | ||||||
|         im.save(temp_file) |         im.save(temp_file) | ||||||
|         assert handler.saved |         assert handler.saved | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -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 | ||||||
|  |  | ||||||
|  | @ -9,7 +9,13 @@ import pytest | ||||||
| 
 | 
 | ||||||
| from PIL import DdsImagePlugin, Image | from PIL import DdsImagePlugin, Image | ||||||
| 
 | 
 | ||||||
| from .helper import assert_image_equal, assert_image_equal_tofile, hopper | from .helper import ( | ||||||
|  |     assert_image_equal, | ||||||
|  |     assert_image_equal_tofile, | ||||||
|  |     assert_image_similar, | ||||||
|  |     assert_image_similar_tofile, | ||||||
|  |     hopper, | ||||||
|  | ) | ||||||
| 
 | 
 | ||||||
| TEST_FILE_DXT1 = "Tests/images/dxt1-rgb-4bbp-noalpha_MipMaps-1.dds" | TEST_FILE_DXT1 = "Tests/images/dxt1-rgb-4bbp-noalpha_MipMaps-1.dds" | ||||||
| TEST_FILE_DXT3 = "Tests/images/dxt3-argb-8bbp-explicitalpha_MipMaps-1.dds" | TEST_FILE_DXT3 = "Tests/images/dxt3-argb-8bbp-explicitalpha_MipMaps-1.dds" | ||||||
|  | @ -109,6 +115,32 @@ def test_sanity_ati1_bc4u(image_path: str) -> None: | ||||||
|         assert_image_equal_tofile(im, TEST_FILE_ATI1.replace(".dds", ".png")) |         assert_image_equal_tofile(im, TEST_FILE_ATI1.replace(".dds", ".png")) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | def test_dx10_bc2(tmp_path: Path) -> None: | ||||||
|  |     out = tmp_path / "temp.dds" | ||||||
|  |     with Image.open(TEST_FILE_DXT3) as im: | ||||||
|  |         im.save(out, pixel_format="BC2") | ||||||
|  | 
 | ||||||
|  |     with Image.open(out) as reloaded: | ||||||
|  |         assert reloaded.format == "DDS" | ||||||
|  |         assert reloaded.mode == "RGBA" | ||||||
|  |         assert reloaded.size == (256, 256) | ||||||
|  | 
 | ||||||
|  |         assert_image_similar(im, reloaded, 3.81) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def test_dx10_bc3(tmp_path: Path) -> None: | ||||||
|  |     out = tmp_path / "temp.dds" | ||||||
|  |     with Image.open(TEST_FILE_DXT5) as im: | ||||||
|  |         im.save(out, pixel_format="BC3") | ||||||
|  | 
 | ||||||
|  |     with Image.open(out) as reloaded: | ||||||
|  |         assert reloaded.format == "DDS" | ||||||
|  |         assert reloaded.mode == "RGBA" | ||||||
|  |         assert reloaded.size == (256, 256) | ||||||
|  | 
 | ||||||
|  |         assert_image_similar(im, reloaded, 3.69) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
| @pytest.mark.parametrize( | @pytest.mark.parametrize( | ||||||
|     "image_path", |     "image_path", | ||||||
|     ( |     ( | ||||||
|  | @ -368,9 +400,9 @@ def test_not_implemented(test_file: str) -> None: | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def test_save_unsupported_mode(tmp_path: Path) -> None: | def test_save_unsupported_mode(tmp_path: Path) -> None: | ||||||
|     out = str(tmp_path / "temp.dds") |     out = tmp_path / "temp.dds" | ||||||
|     im = hopper("HSV") |     im = hopper("HSV") | ||||||
|     with pytest.raises(OSError): |     with pytest.raises(OSError, match="cannot write mode HSV as DDS"): | ||||||
|         im.save(out) |         im.save(out) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | @ -384,10 +416,98 @@ def test_save_unsupported_mode(tmp_path: Path) -> None: | ||||||
|     ], |     ], | ||||||
| ) | ) | ||||||
| def test_save(mode: str, test_file: str, tmp_path: Path) -> None: | def test_save(mode: str, test_file: str, tmp_path: Path) -> None: | ||||||
|     out = str(tmp_path / "temp.dds") |     out = tmp_path / "temp.dds" | ||||||
|     with Image.open(test_file) as im: |     with Image.open(test_file) as im: | ||||||
|         assert im.mode == mode |         assert im.mode == mode | ||||||
|         im.save(out) |         im.save(out) | ||||||
| 
 | 
 | ||||||
|  |         assert_image_equal_tofile(im, out) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def test_save_unsupported_pixel_format(tmp_path: Path) -> None: | ||||||
|  |     out = tmp_path / "temp.dds" | ||||||
|  |     im = hopper() | ||||||
|  |     with pytest.raises(OSError, match="cannot write pixel format UNKNOWN"): | ||||||
|  |         im.save(out, pixel_format="UNKNOWN") | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def test_save_dxt1(tmp_path: Path) -> None: | ||||||
|  |     # RGB | ||||||
|  |     out = tmp_path / "temp.dds" | ||||||
|  |     with Image.open(TEST_FILE_DXT1) as im: | ||||||
|  |         im.convert("RGB").save(out, pixel_format="DXT1") | ||||||
|  |     assert_image_similar_tofile(im, out, 1.84) | ||||||
|  | 
 | ||||||
|  |     # RGBA | ||||||
|  |     im_alpha = im.copy() | ||||||
|  |     im_alpha.putpixel((0, 0), (0, 0, 0, 0)) | ||||||
|  |     im_alpha.save(out, pixel_format="DXT1") | ||||||
|     with Image.open(out) as reloaded: |     with Image.open(out) as reloaded: | ||||||
|             assert_image_equal(im, reloaded) |         assert reloaded.getpixel((0, 0)) == (0, 0, 0, 0) | ||||||
|  | 
 | ||||||
|  |     # L | ||||||
|  |     im_l = im.convert("L") | ||||||
|  |     im_l.save(out, pixel_format="DXT1") | ||||||
|  |     assert_image_similar_tofile(im_l.convert("RGBA"), out, 6.07) | ||||||
|  | 
 | ||||||
|  |     # LA | ||||||
|  |     im_alpha.convert("LA").save(out, pixel_format="DXT1") | ||||||
|  |     with Image.open(out) as reloaded: | ||||||
|  |         assert reloaded.getpixel((0, 0)) == (0, 0, 0, 0) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def test_save_dxt3(tmp_path: Path) -> None: | ||||||
|  |     # RGB | ||||||
|  |     out = tmp_path / "temp.dds" | ||||||
|  |     with Image.open(TEST_FILE_DXT3) as im: | ||||||
|  |         im_rgb = im.convert("RGB") | ||||||
|  |     im_rgb.save(out, pixel_format="DXT3") | ||||||
|  |     assert_image_similar_tofile(im_rgb.convert("RGBA"), out, 1.26) | ||||||
|  | 
 | ||||||
|  |     # RGBA | ||||||
|  |     im.save(out, pixel_format="DXT3") | ||||||
|  |     assert_image_similar_tofile(im, out, 3.81) | ||||||
|  | 
 | ||||||
|  |     # L | ||||||
|  |     im_l = im.convert("L") | ||||||
|  |     im_l.save(out, pixel_format="DXT3") | ||||||
|  |     assert_image_similar_tofile(im_l.convert("RGBA"), out, 5.89) | ||||||
|  | 
 | ||||||
|  |     # LA | ||||||
|  |     im_la = im.convert("LA") | ||||||
|  |     im_la.save(out, pixel_format="DXT3") | ||||||
|  |     assert_image_similar_tofile(im_la.convert("RGBA"), out, 8.44) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def test_save_dxt5(tmp_path: Path) -> None: | ||||||
|  |     # RGB | ||||||
|  |     out = tmp_path / "temp.dds" | ||||||
|  |     with Image.open(TEST_FILE_DXT1) as im: | ||||||
|  |         im.convert("RGB").save(out, pixel_format="DXT5") | ||||||
|  |     assert_image_similar_tofile(im, out, 1.84) | ||||||
|  | 
 | ||||||
|  |     # RGBA | ||||||
|  |     with Image.open(TEST_FILE_DXT5) as im_rgba: | ||||||
|  |         im_rgba.save(out, pixel_format="DXT5") | ||||||
|  |     assert_image_similar_tofile(im_rgba, out, 3.69) | ||||||
|  | 
 | ||||||
|  |     # L | ||||||
|  |     im_l = im.convert("L") | ||||||
|  |     im_l.save(out, pixel_format="DXT5") | ||||||
|  |     assert_image_similar_tofile(im_l.convert("RGBA"), out, 6.07) | ||||||
|  | 
 | ||||||
|  |     # LA | ||||||
|  |     im_la = im_rgba.convert("LA") | ||||||
|  |     im_la.save(out, pixel_format="DXT5") | ||||||
|  |     assert_image_similar_tofile(im_la.convert("RGBA"), out, 8.32) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def test_save_dx10_bc5(tmp_path: Path) -> None: | ||||||
|  |     out = tmp_path / "temp.dds" | ||||||
|  |     with Image.open(TEST_FILE_DX10_BC5_TYPELESS) as im: | ||||||
|  |         im.save(out, pixel_format="BC5") | ||||||
|  |     assert_image_similar_tofile(im, out, 9.56) | ||||||
|  | 
 | ||||||
|  |     im = hopper("L") | ||||||
|  |     with pytest.raises(OSError, match="only RGB mode can be written as BC5"): | ||||||
|  |         im.save(out, pixel_format="BC5") | ||||||
|  |  | ||||||
|  | @ -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" | ||||||
| 
 | 
 | ||||||
|  | @ -239,7 +243,7 @@ def test_transparency() -> None: | ||||||
| def test_file_object(tmp_path: Path) -> None: | def test_file_object(tmp_path: Path) -> None: | ||||||
|     # issue 479 |     # issue 479 | ||||||
|     with Image.open(FILE1) as image1: |     with Image.open(FILE1) as image1: | ||||||
|         with open(str(tmp_path / "temp.eps"), "wb") as fh: |         with open(tmp_path / "temp.eps", "wb") as fh: | ||||||
|             image1.save(fh, "EPS") |             image1.save(fh, "EPS") | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | @ -274,7 +278,7 @@ def test_1(filename: str) -> None: | ||||||
| 
 | 
 | ||||||
| def test_image_mode_not_supported(tmp_path: Path) -> None: | def test_image_mode_not_supported(tmp_path: Path) -> None: | ||||||
|     im = hopper("RGBA") |     im = hopper("RGBA") | ||||||
|     tmpfile = str(tmp_path / "temp.eps") |     tmpfile = tmp_path / "temp.eps" | ||||||
|     with pytest.raises(ValueError): |     with pytest.raises(ValueError): | ||||||
|         im.save(tmpfile) |         im.save(tmpfile) | ||||||
| 
 | 
 | ||||||
|  | @ -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") | ||||||
|  |  | ||||||
|  | @ -1,5 +1,6 @@ | ||||||
| from __future__ import annotations | from __future__ import annotations | ||||||
| 
 | 
 | ||||||
|  | import io | ||||||
| import warnings | import warnings | ||||||
| 
 | 
 | ||||||
| import pytest | import pytest | ||||||
|  | @ -21,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) | ||||||
|  | @ -28,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" | ||||||
|  | @ -111,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 | ||||||
|  | @ -132,6 +140,15 @@ def test_eoferror() -> None: | ||||||
|         im.seek(n_frames - 1) |         im.seek(n_frames - 1) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | def test_missing_frame_size() -> None: | ||||||
|  |     with open(animated_test_file, "rb") as fp: | ||||||
|  |         data = fp.read() | ||||||
|  |     data = data[:6188] | ||||||
|  |     with Image.open(io.BytesIO(data)) as im: | ||||||
|  |         with pytest.raises(EOFError, match="missing frame size"): | ||||||
|  |             im.seek(1) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
| def test_seek_tell() -> None: | def test_seek_tell() -> None: | ||||||
|     with Image.open(animated_test_file) as im: |     with Image.open(animated_test_file) as im: | ||||||
|         layer_number = im.tell() |         layer_number = im.tell() | ||||||
|  | @ -156,10 +173,14 @@ 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") | ||||||
| 
 | 
 | ||||||
|  |         with pytest.raises(ValueError, match="cannot seek to frame 52"): | ||||||
|  |             im._seek(52) | ||||||
|  | 
 | ||||||
| 
 | 
 | ||||||
| @pytest.mark.parametrize( | @pytest.mark.parametrize( | ||||||
|     "test_file", |     "test_file", | ||||||
|  |  | ||||||
|  | @ -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 | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -228,7 +228,7 @@ def test_optimize_if_palette_can_be_reduced_by_half() -> None: | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def test_full_palette_second_frame(tmp_path: Path) -> None: | def test_full_palette_second_frame(tmp_path: Path) -> None: | ||||||
|     out = str(tmp_path / "temp.gif") |     out = tmp_path / "temp.gif" | ||||||
|     im = Image.new("P", (1, 256)) |     im = Image.new("P", (1, 256)) | ||||||
| 
 | 
 | ||||||
|     full_palette_im = Image.new("P", (1, 256)) |     full_palette_im = Image.new("P", (1, 256)) | ||||||
|  | @ -249,7 +249,7 @@ def test_full_palette_second_frame(tmp_path: Path) -> None: | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def test_roundtrip(tmp_path: Path) -> None: | def test_roundtrip(tmp_path: Path) -> None: | ||||||
|     out = str(tmp_path / "temp.gif") |     out = tmp_path / "temp.gif" | ||||||
|     im = hopper() |     im = hopper() | ||||||
|     im.save(out) |     im.save(out) | ||||||
|     with Image.open(out) as reread: |     with Image.open(out) as reread: | ||||||
|  | @ -258,7 +258,7 @@ def test_roundtrip(tmp_path: Path) -> None: | ||||||
| 
 | 
 | ||||||
| def test_roundtrip2(tmp_path: Path) -> None: | def test_roundtrip2(tmp_path: Path) -> None: | ||||||
|     # see https://github.com/python-pillow/Pillow/issues/403 |     # see https://github.com/python-pillow/Pillow/issues/403 | ||||||
|     out = str(tmp_path / "temp.gif") |     out = tmp_path / "temp.gif" | ||||||
|     with Image.open(TEST_GIF) as im: |     with Image.open(TEST_GIF) as im: | ||||||
|         im2 = im.copy() |         im2 = im.copy() | ||||||
|         im2.save(out) |         im2.save(out) | ||||||
|  | @ -268,7 +268,7 @@ def test_roundtrip2(tmp_path: Path) -> None: | ||||||
| 
 | 
 | ||||||
| def test_roundtrip_save_all(tmp_path: Path) -> None: | def test_roundtrip_save_all(tmp_path: Path) -> None: | ||||||
|     # Single frame image |     # Single frame image | ||||||
|     out = str(tmp_path / "temp.gif") |     out = tmp_path / "temp.gif" | ||||||
|     im = hopper() |     im = hopper() | ||||||
|     im.save(out, save_all=True) |     im.save(out, save_all=True) | ||||||
|     with Image.open(out) as reread: |     with Image.open(out) as reread: | ||||||
|  | @ -276,7 +276,7 @@ def test_roundtrip_save_all(tmp_path: Path) -> None: | ||||||
| 
 | 
 | ||||||
|     # Multiframe image |     # Multiframe image | ||||||
|     with Image.open("Tests/images/dispose_bgnd.gif") as im: |     with Image.open("Tests/images/dispose_bgnd.gif") as im: | ||||||
|         out = str(tmp_path / "temp.gif") |         out = tmp_path / "temp.gif" | ||||||
|         im.save(out, save_all=True) |         im.save(out, save_all=True) | ||||||
| 
 | 
 | ||||||
|     with Image.open(out) as reread: |     with Image.open(out) as reread: | ||||||
|  | @ -284,7 +284,7 @@ def test_roundtrip_save_all(tmp_path: Path) -> None: | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def test_roundtrip_save_all_1(tmp_path: Path) -> None: | def test_roundtrip_save_all_1(tmp_path: Path) -> None: | ||||||
|     out = str(tmp_path / "temp.gif") |     out = tmp_path / "temp.gif" | ||||||
|     im = Image.new("1", (1, 1)) |     im = Image.new("1", (1, 1)) | ||||||
|     im2 = Image.new("1", (1, 1), 1) |     im2 = Image.new("1", (1, 1), 1) | ||||||
|     im.save(out, save_all=True, append_images=[im2]) |     im.save(out, save_all=True, append_images=[im2]) | ||||||
|  | @ -329,7 +329,7 @@ def test_headers_saving_for_animated_gifs(tmp_path: Path) -> None: | ||||||
|     with Image.open("Tests/images/dispose_bgnd.gif") as im: |     with Image.open("Tests/images/dispose_bgnd.gif") as im: | ||||||
|         info = im.info.copy() |         info = im.info.copy() | ||||||
| 
 | 
 | ||||||
|         out = str(tmp_path / "temp.gif") |         out = tmp_path / "temp.gif" | ||||||
|         im.save(out, save_all=True) |         im.save(out, save_all=True) | ||||||
|     with Image.open(out) as reread: |     with Image.open(out) as reread: | ||||||
|         for header in important_headers: |         for header in important_headers: | ||||||
|  | @ -345,7 +345,7 @@ def test_palette_handling(tmp_path: Path) -> None: | ||||||
|         im = im.resize((100, 100), Image.Resampling.LANCZOS) |         im = im.resize((100, 100), Image.Resampling.LANCZOS) | ||||||
|         im2 = im.convert("P", palette=Image.Palette.ADAPTIVE, colors=256) |         im2 = im.convert("P", palette=Image.Palette.ADAPTIVE, colors=256) | ||||||
| 
 | 
 | ||||||
|         f = str(tmp_path / "temp.gif") |         f = tmp_path / "temp.gif" | ||||||
|         im2.save(f, optimize=True) |         im2.save(f, optimize=True) | ||||||
| 
 | 
 | ||||||
|     with Image.open(f) as reloaded: |     with Image.open(f) as reloaded: | ||||||
|  | @ -356,7 +356,7 @@ def test_palette_434(tmp_path: Path) -> None: | ||||||
|     # see https://github.com/python-pillow/Pillow/issues/434 |     # see https://github.com/python-pillow/Pillow/issues/434 | ||||||
| 
 | 
 | ||||||
|     def roundtrip(im: Image.Image, **kwargs: bool) -> Image.Image: |     def roundtrip(im: Image.Image, **kwargs: bool) -> Image.Image: | ||||||
|         out = str(tmp_path / "temp.gif") |         out = tmp_path / "temp.gif" | ||||||
|         im.copy().save(out, "GIF", **kwargs) |         im.copy().save(out, "GIF", **kwargs) | ||||||
|         reloaded = Image.open(out) |         reloaded = Image.open(out) | ||||||
| 
 | 
 | ||||||
|  | @ -402,6 +402,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: | ||||||
|  | @ -410,6 +411,10 @@ def test_seek() -> None: | ||||||
|         except EOFError: |         except EOFError: | ||||||
|             assert frame_count == 5 |             assert frame_count == 5 | ||||||
| 
 | 
 | ||||||
|  |         img.seek(0) | ||||||
|  |         with pytest.raises(ValueError, match="cannot seek to frame 2"): | ||||||
|  |             img._seek(2) | ||||||
|  | 
 | ||||||
| 
 | 
 | ||||||
| def test_seek_info() -> None: | def test_seek_info() -> None: | ||||||
|     with Image.open("Tests/images/iss634.gif") as im: |     with Image.open("Tests/images/iss634.gif") as im: | ||||||
|  | @ -442,10 +447,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) | ||||||
| 
 | 
 | ||||||
|  | @ -455,6 +462,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) | ||||||
| 
 | 
 | ||||||
|  | @ -462,17 +470,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 | ||||||
|  | @ -491,6 +502,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) | ||||||
|  | @ -514,6 +526,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) | ||||||
|  | @ -567,6 +580,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) | ||||||
|  | @ -595,15 +609,16 @@ def test_previous_frame_loaded() -> None: | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def test_save_dispose(tmp_path: Path) -> None: | def test_save_dispose(tmp_path: Path) -> None: | ||||||
|     out = str(tmp_path / "temp.gif") |     out = tmp_path / "temp.gif" | ||||||
|     im_list = [ |     im_list = [ | ||||||
|         Image.new("L", (100, 100), "#000"), |         Image.new("L", (100, 100), "#000"), | ||||||
|         Image.new("L", (100, 100), "#111"), |         Image.new("L", (100, 100), "#111"), | ||||||
|         Image.new("L", (100, 100), "#222"), |         Image.new("L", (100, 100), "#222"), | ||||||
|     ] |     ] | ||||||
|     for method in range(0, 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 | ||||||
|  | @ -617,13 +632,14 @@ 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 | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def test_dispose2_palette(tmp_path: Path) -> None: | def test_dispose2_palette(tmp_path: Path) -> None: | ||||||
|     out = str(tmp_path / "temp.gif") |     out = tmp_path / "temp.gif" | ||||||
| 
 | 
 | ||||||
|     # Four colors: white, gray, black, red |     # Four colors: white, gray, black, red | ||||||
|     circles = [(255, 255, 255), (153, 153, 153), (0, 0, 0), (255, 0, 0)] |     circles = [(255, 255, 255), (153, 153, 153), (0, 0, 0), (255, 0, 0)] | ||||||
|  | @ -657,7 +673,7 @@ def test_dispose2_palette(tmp_path: Path) -> None: | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def test_dispose2_diff(tmp_path: Path) -> None: | def test_dispose2_diff(tmp_path: Path) -> None: | ||||||
|     out = str(tmp_path / "temp.gif") |     out = tmp_path / "temp.gif" | ||||||
| 
 | 
 | ||||||
|     # 4 frames: red/blue, red/red, blue/blue, red/blue |     # 4 frames: red/blue, red/red, blue/blue, red/blue | ||||||
|     circles = [ |     circles = [ | ||||||
|  | @ -699,7 +715,7 @@ def test_dispose2_diff(tmp_path: Path) -> None: | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def test_dispose2_background(tmp_path: Path) -> None: | def test_dispose2_background(tmp_path: Path) -> None: | ||||||
|     out = str(tmp_path / "temp.gif") |     out = tmp_path / "temp.gif" | ||||||
| 
 | 
 | ||||||
|     im_list = [] |     im_list = [] | ||||||
| 
 | 
 | ||||||
|  | @ -725,7 +741,7 @@ def test_dispose2_background(tmp_path: Path) -> None: | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def test_dispose2_background_frame(tmp_path: Path) -> None: | def test_dispose2_background_frame(tmp_path: Path) -> None: | ||||||
|     out = str(tmp_path / "temp.gif") |     out = tmp_path / "temp.gif" | ||||||
| 
 | 
 | ||||||
|     im_list = [Image.new("RGBA", (1, 20))] |     im_list = [Image.new("RGBA", (1, 20))] | ||||||
| 
 | 
 | ||||||
|  | @ -739,11 +755,12 @@ 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 | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def test_dispose2_previous_frame(tmp_path: Path) -> None: | def test_dispose2_previous_frame(tmp_path: Path) -> None: | ||||||
|     out = str(tmp_path / "temp.gif") |     out = tmp_path / "temp.gif" | ||||||
| 
 | 
 | ||||||
|     im = Image.new("P", (100, 100)) |     im = Image.new("P", (100, 100)) | ||||||
|     im.info["transparency"] = 0 |     im.info["transparency"] = 0 | ||||||
|  | @ -762,7 +779,7 @@ def test_dispose2_previous_frame(tmp_path: Path) -> None: | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def test_dispose2_without_transparency(tmp_path: Path) -> None: | def test_dispose2_without_transparency(tmp_path: Path) -> None: | ||||||
|     out = str(tmp_path / "temp.gif") |     out = tmp_path / "temp.gif" | ||||||
| 
 | 
 | ||||||
|     im = Image.new("P", (100, 100)) |     im = Image.new("P", (100, 100)) | ||||||
| 
 | 
 | ||||||
|  | @ -777,7 +794,7 @@ def test_dispose2_without_transparency(tmp_path: Path) -> None: | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def test_transparency_in_second_frame(tmp_path: Path) -> None: | def test_transparency_in_second_frame(tmp_path: Path) -> None: | ||||||
|     out = str(tmp_path / "temp.gif") |     out = tmp_path / "temp.gif" | ||||||
|     with Image.open("Tests/images/different_transparency.gif") as im: |     with Image.open("Tests/images/different_transparency.gif") as im: | ||||||
|         assert im.info["transparency"] == 0 |         assert im.info["transparency"] == 0 | ||||||
| 
 | 
 | ||||||
|  | @ -807,7 +824,7 @@ def test_no_transparency_in_second_frame() -> None: | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def test_remapped_transparency(tmp_path: Path) -> None: | def test_remapped_transparency(tmp_path: Path) -> None: | ||||||
|     out = str(tmp_path / "temp.gif") |     out = tmp_path / "temp.gif" | ||||||
| 
 | 
 | ||||||
|     im = Image.new("P", (1, 2)) |     im = Image.new("P", (1, 2)) | ||||||
|     im2 = im.copy() |     im2 = im.copy() | ||||||
|  | @ -825,7 +842,7 @@ def test_remapped_transparency(tmp_path: Path) -> None: | ||||||
| def test_duration(tmp_path: Path) -> None: | def test_duration(tmp_path: Path) -> None: | ||||||
|     duration = 1000 |     duration = 1000 | ||||||
| 
 | 
 | ||||||
|     out = str(tmp_path / "temp.gif") |     out = tmp_path / "temp.gif" | ||||||
|     im = Image.new("L", (100, 100), "#000") |     im = Image.new("L", (100, 100), "#000") | ||||||
| 
 | 
 | ||||||
|     # Check that the argument has priority over the info settings |     # Check that the argument has priority over the info settings | ||||||
|  | @ -839,7 +856,7 @@ def test_duration(tmp_path: Path) -> None: | ||||||
| def test_multiple_duration(tmp_path: Path) -> None: | def test_multiple_duration(tmp_path: Path) -> None: | ||||||
|     duration_list = [1000, 2000, 3000] |     duration_list = [1000, 2000, 3000] | ||||||
| 
 | 
 | ||||||
|     out = str(tmp_path / "temp.gif") |     out = tmp_path / "temp.gif" | ||||||
|     im_list = [ |     im_list = [ | ||||||
|         Image.new("L", (100, 100), "#000"), |         Image.new("L", (100, 100), "#000"), | ||||||
|         Image.new("L", (100, 100), "#111"), |         Image.new("L", (100, 100), "#111"), | ||||||
|  | @ -874,7 +891,7 @@ def test_multiple_duration(tmp_path: Path) -> None: | ||||||
| def test_roundtrip_info_duration(tmp_path: Path) -> None: | def test_roundtrip_info_duration(tmp_path: Path) -> None: | ||||||
|     duration_list = [100, 500, 500] |     duration_list = [100, 500, 500] | ||||||
| 
 | 
 | ||||||
|     out = str(tmp_path / "temp.gif") |     out = tmp_path / "temp.gif" | ||||||
|     with Image.open("Tests/images/transparent_dispose.gif") as im: |     with Image.open("Tests/images/transparent_dispose.gif") as im: | ||||||
|         assert [ |         assert [ | ||||||
|             frame.info["duration"] for frame in ImageSequence.Iterator(im) |             frame.info["duration"] for frame in ImageSequence.Iterator(im) | ||||||
|  | @ -889,7 +906,7 @@ def test_roundtrip_info_duration(tmp_path: Path) -> None: | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def test_roundtrip_info_duration_combined(tmp_path: Path) -> None: | def test_roundtrip_info_duration_combined(tmp_path: Path) -> None: | ||||||
|     out = str(tmp_path / "temp.gif") |     out = tmp_path / "temp.gif" | ||||||
|     with Image.open("Tests/images/duplicate_frame.gif") as im: |     with Image.open("Tests/images/duplicate_frame.gif") as im: | ||||||
|         assert [frame.info["duration"] for frame in ImageSequence.Iterator(im)] == [ |         assert [frame.info["duration"] for frame in ImageSequence.Iterator(im)] == [ | ||||||
|             1000, |             1000, | ||||||
|  | @ -907,7 +924,7 @@ def test_roundtrip_info_duration_combined(tmp_path: Path) -> None: | ||||||
| def test_identical_frames(tmp_path: Path) -> None: | def test_identical_frames(tmp_path: Path) -> None: | ||||||
|     duration_list = [1000, 1500, 2000, 4000] |     duration_list = [1000, 1500, 2000, 4000] | ||||||
| 
 | 
 | ||||||
|     out = str(tmp_path / "temp.gif") |     out = tmp_path / "temp.gif" | ||||||
|     im_list = [ |     im_list = [ | ||||||
|         Image.new("L", (100, 100), "#000"), |         Image.new("L", (100, 100), "#000"), | ||||||
|         Image.new("L", (100, 100), "#000"), |         Image.new("L", (100, 100), "#000"), | ||||||
|  | @ -920,6 +937,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 | ||||||
| 
 | 
 | ||||||
|  | @ -940,7 +959,7 @@ def test_identical_frames(tmp_path: Path) -> None: | ||||||
| def test_identical_frames_to_single_frame( | def test_identical_frames_to_single_frame( | ||||||
|     duration: int | list[int], tmp_path: Path |     duration: int | list[int], tmp_path: Path | ||||||
| ) -> None: | ) -> None: | ||||||
|     out = str(tmp_path / "temp.gif") |     out = tmp_path / "temp.gif" | ||||||
|     im_list = [ |     im_list = [ | ||||||
|         Image.new("L", (100, 100), "#000"), |         Image.new("L", (100, 100), "#000"), | ||||||
|         Image.new("L", (100, 100), "#000"), |         Image.new("L", (100, 100), "#000"), | ||||||
|  | @ -949,6 +968,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 | ||||||
| 
 | 
 | ||||||
|  | @ -957,7 +978,7 @@ def test_identical_frames_to_single_frame( | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def test_loop_none(tmp_path: Path) -> None: | def test_loop_none(tmp_path: Path) -> None: | ||||||
|     out = str(tmp_path / "temp.gif") |     out = tmp_path / "temp.gif" | ||||||
|     im = Image.new("L", (100, 100), "#000") |     im = Image.new("L", (100, 100), "#000") | ||||||
|     im.save(out, loop=None) |     im.save(out, loop=None) | ||||||
|     with Image.open(out) as reread: |     with Image.open(out) as reread: | ||||||
|  | @ -967,7 +988,7 @@ def test_loop_none(tmp_path: Path) -> None: | ||||||
| def test_number_of_loops(tmp_path: Path) -> None: | def test_number_of_loops(tmp_path: Path) -> None: | ||||||
|     number_of_loops = 2 |     number_of_loops = 2 | ||||||
| 
 | 
 | ||||||
|     out = str(tmp_path / "temp.gif") |     out = tmp_path / "temp.gif" | ||||||
|     im = Image.new("L", (100, 100), "#000") |     im = Image.new("L", (100, 100), "#000") | ||||||
|     im.save(out, loop=number_of_loops) |     im.save(out, loop=number_of_loops) | ||||||
|     with Image.open(out) as reread: |     with Image.open(out) as reread: | ||||||
|  | @ -983,7 +1004,7 @@ def test_number_of_loops(tmp_path: Path) -> None: | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def test_background(tmp_path: Path) -> None: | def test_background(tmp_path: Path) -> None: | ||||||
|     out = str(tmp_path / "temp.gif") |     out = tmp_path / "temp.gif" | ||||||
|     im = Image.new("L", (100, 100), "#000") |     im = Image.new("L", (100, 100), "#000") | ||||||
|     im.info["background"] = 1 |     im.info["background"] = 1 | ||||||
|     im.save(out) |     im.save(out) | ||||||
|  | @ -992,7 +1013,7 @@ def test_background(tmp_path: Path) -> None: | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def test_webp_background(tmp_path: Path) -> None: | def test_webp_background(tmp_path: Path) -> None: | ||||||
|     out = str(tmp_path / "temp.gif") |     out = tmp_path / "temp.gif" | ||||||
| 
 | 
 | ||||||
|     # Test opaque WebP background |     # Test opaque WebP background | ||||||
|     if features.check("webp"): |     if features.check("webp"): | ||||||
|  | @ -1010,7 +1031,7 @@ def test_comment(tmp_path: Path) -> None: | ||||||
|     with Image.open(TEST_GIF) as im: |     with Image.open(TEST_GIF) as im: | ||||||
|         assert im.info["comment"] == b"File written by Adobe Photoshop\xa8 4.0" |         assert im.info["comment"] == b"File written by Adobe Photoshop\xa8 4.0" | ||||||
| 
 | 
 | ||||||
|     out = str(tmp_path / "temp.gif") |     out = tmp_path / "temp.gif" | ||||||
|     im = Image.new("L", (100, 100), "#000") |     im = Image.new("L", (100, 100), "#000") | ||||||
|     im.info["comment"] = b"Test comment text" |     im.info["comment"] = b"Test comment text" | ||||||
|     im.save(out) |     im.save(out) | ||||||
|  | @ -1027,7 +1048,7 @@ def test_comment(tmp_path: Path) -> None: | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def test_comment_over_255(tmp_path: Path) -> None: | def test_comment_over_255(tmp_path: Path) -> None: | ||||||
|     out = str(tmp_path / "temp.gif") |     out = tmp_path / "temp.gif" | ||||||
|     im = Image.new("L", (100, 100), "#000") |     im = Image.new("L", (100, 100), "#000") | ||||||
|     comment = b"Test comment text" |     comment = b"Test comment text" | ||||||
|     while len(comment) < 256: |     while len(comment) < 256: | ||||||
|  | @ -1053,7 +1074,7 @@ def test_read_multiple_comment_blocks() -> None: | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def test_empty_string_comment(tmp_path: Path) -> None: | def test_empty_string_comment(tmp_path: Path) -> None: | ||||||
|     out = str(tmp_path / "temp.gif") |     out = tmp_path / "temp.gif" | ||||||
|     with Image.open("Tests/images/chi.gif") as im: |     with Image.open("Tests/images/chi.gif") as im: | ||||||
|         assert "comment" in im.info |         assert "comment" in im.info | ||||||
| 
 | 
 | ||||||
|  | @ -1087,7 +1108,7 @@ def test_retain_comment_in_subsequent_frames(tmp_path: Path) -> None: | ||||||
|         assert "comment" not in im.info |         assert "comment" not in im.info | ||||||
| 
 | 
 | ||||||
|     # Test that a saved image keeps the comment |     # Test that a saved image keeps the comment | ||||||
|     out = str(tmp_path / "temp.gif") |     out = tmp_path / "temp.gif" | ||||||
|     with Image.open("Tests/images/dispose_prev.gif") as im: |     with Image.open("Tests/images/dispose_prev.gif") as im: | ||||||
|         im.save(out, save_all=True, comment="Test") |         im.save(out, save_all=True, comment="Test") | ||||||
| 
 | 
 | ||||||
|  | @ -1097,7 +1118,7 @@ def test_retain_comment_in_subsequent_frames(tmp_path: Path) -> None: | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def test_version(tmp_path: Path) -> None: | def test_version(tmp_path: Path) -> None: | ||||||
|     out = str(tmp_path / "temp.gif") |     out = tmp_path / "temp.gif" | ||||||
| 
 | 
 | ||||||
|     def assert_version_after_save(im: Image.Image, version: bytes) -> None: |     def assert_version_after_save(im: Image.Image, version: bytes) -> None: | ||||||
|         im.save(out) |         im.save(out) | ||||||
|  | @ -1127,7 +1148,7 @@ def test_version(tmp_path: Path) -> None: | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def test_append_images(tmp_path: Path) -> None: | def test_append_images(tmp_path: Path) -> None: | ||||||
|     out = str(tmp_path / "temp.gif") |     out = tmp_path / "temp.gif" | ||||||
| 
 | 
 | ||||||
|     # Test appending single frame images |     # Test appending single frame images | ||||||
|     im = Image.new("RGB", (100, 100), "#f00") |     im = Image.new("RGB", (100, 100), "#f00") | ||||||
|  | @ -1135,6 +1156,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 | ||||||
|  | @ -1144,6 +1173,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 | ||||||
|  | @ -1152,11 +1182,12 @@ 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 | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def test_append_different_size_image(tmp_path: Path) -> None: | def test_append_different_size_image(tmp_path: Path) -> None: | ||||||
|     out = str(tmp_path / "temp.gif") |     out = tmp_path / "temp.gif" | ||||||
| 
 | 
 | ||||||
|     im = Image.new("RGB", (100, 100)) |     im = Image.new("RGB", (100, 100)) | ||||||
|     bigger_im = Image.new("RGB", (200, 200), "#f00") |     bigger_im = Image.new("RGB", (200, 200), "#f00") | ||||||
|  | @ -1183,7 +1214,7 @@ def test_transparent_optimize(tmp_path: Path) -> None: | ||||||
|     im.frombytes(data) |     im.frombytes(data) | ||||||
|     im.putpalette(palette) |     im.putpalette(palette) | ||||||
| 
 | 
 | ||||||
|     out = str(tmp_path / "temp.gif") |     out = tmp_path / "temp.gif" | ||||||
|     im.save(out, transparency=im.getpixel((252, 0))) |     im.save(out, transparency=im.getpixel((252, 0))) | ||||||
| 
 | 
 | ||||||
|     with Image.open(out) as reloaded: |     with Image.open(out) as reloaded: | ||||||
|  | @ -1191,7 +1222,7 @@ def test_transparent_optimize(tmp_path: Path) -> None: | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def test_removed_transparency(tmp_path: Path) -> None: | def test_removed_transparency(tmp_path: Path) -> None: | ||||||
|     out = str(tmp_path / "temp.gif") |     out = tmp_path / "temp.gif" | ||||||
|     im = Image.new("RGB", (256, 1)) |     im = Image.new("RGB", (256, 1)) | ||||||
| 
 | 
 | ||||||
|     for x in range(256): |     for x in range(256): | ||||||
|  | @ -1206,7 +1237,7 @@ def test_removed_transparency(tmp_path: Path) -> None: | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def test_rgb_transparency(tmp_path: Path) -> None: | def test_rgb_transparency(tmp_path: Path) -> None: | ||||||
|     out = str(tmp_path / "temp.gif") |     out = tmp_path / "temp.gif" | ||||||
| 
 | 
 | ||||||
|     # Single frame |     # Single frame | ||||||
|     im = Image.new("RGB", (1, 1)) |     im = Image.new("RGB", (1, 1)) | ||||||
|  | @ -1228,7 +1259,7 @@ def test_rgb_transparency(tmp_path: Path) -> None: | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def test_rgba_transparency(tmp_path: Path) -> None: | def test_rgba_transparency(tmp_path: Path) -> None: | ||||||
|     out = str(tmp_path / "temp.gif") |     out = tmp_path / "temp.gif" | ||||||
| 
 | 
 | ||||||
|     im = hopper("P") |     im = hopper("P") | ||||||
|     im.save(out, save_all=True, append_images=[Image.new("RGBA", im.size)]) |     im.save(out, save_all=True, append_images=[Image.new("RGBA", im.size)]) | ||||||
|  | @ -1238,25 +1269,26 @@ def test_rgba_transparency(tmp_path: Path) -> None: | ||||||
|         assert_image_equal(hopper("P").convert("RGB"), reloaded) |         assert_image_equal(hopper("P").convert("RGB"), reloaded) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def test_background_outside_palettte(tmp_path: Path) -> None: | def test_background_outside_palettte() -> None: | ||||||
|     with Image.open("Tests/images/background_outside_palette.gif") as im: |     with Image.open("Tests/images/background_outside_palette.gif") as im: | ||||||
|         im.seek(1) |         im.seek(1) | ||||||
|         assert im.info["background"] == 255 |         assert im.info["background"] == 255 | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def test_bbox(tmp_path: Path) -> None: | def test_bbox(tmp_path: Path) -> None: | ||||||
|     out = str(tmp_path / "temp.gif") |     out = tmp_path / "temp.gif" | ||||||
| 
 | 
 | ||||||
|     im = Image.new("RGB", (100, 100), "#fff") |     im = Image.new("RGB", (100, 100), "#fff") | ||||||
|     ims = [Image.new("RGB", (100, 100), "#000")] |     ims = [Image.new("RGB", (100, 100), "#000")] | ||||||
|     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 | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def test_bbox_alpha(tmp_path: Path) -> None: | def test_bbox_alpha(tmp_path: Path) -> None: | ||||||
|     out = str(tmp_path / "temp.gif") |     out = tmp_path / "temp.gif" | ||||||
| 
 | 
 | ||||||
|     im = Image.new("RGBA", (1, 2), (255, 0, 0, 255)) |     im = Image.new("RGBA", (1, 2), (255, 0, 0, 255)) | ||||||
|     im.putpixel((0, 1), (255, 0, 0, 0)) |     im.putpixel((0, 1), (255, 0, 0, 0)) | ||||||
|  | @ -1264,6 +1296,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 | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | @ -1275,7 +1308,7 @@ def test_palette_save_L(tmp_path: Path) -> None: | ||||||
|     palette = im.getpalette() |     palette = im.getpalette() | ||||||
|     assert palette is not None |     assert palette is not None | ||||||
| 
 | 
 | ||||||
|     out = str(tmp_path / "temp.gif") |     out = tmp_path / "temp.gif" | ||||||
|     im_l.save(out, palette=bytes(palette)) |     im_l.save(out, palette=bytes(palette)) | ||||||
| 
 | 
 | ||||||
|     with Image.open(out) as reloaded: |     with Image.open(out) as reloaded: | ||||||
|  | @ -1286,7 +1319,7 @@ def test_palette_save_P(tmp_path: Path) -> None: | ||||||
|     im = Image.new("P", (1, 2)) |     im = Image.new("P", (1, 2)) | ||||||
|     im.putpixel((0, 1), 1) |     im.putpixel((0, 1), 1) | ||||||
| 
 | 
 | ||||||
|     out = str(tmp_path / "temp.gif") |     out = tmp_path / "temp.gif" | ||||||
|     im.save(out, palette=bytes((1, 2, 3, 4, 5, 6))) |     im.save(out, palette=bytes((1, 2, 3, 4, 5, 6))) | ||||||
| 
 | 
 | ||||||
|     with Image.open(out) as reloaded: |     with Image.open(out) as reloaded: | ||||||
|  | @ -1302,7 +1335,7 @@ def test_palette_save_duplicate_entries(tmp_path: Path) -> None: | ||||||
| 
 | 
 | ||||||
|     im.putpalette((0, 0, 0, 0, 0, 0)) |     im.putpalette((0, 0, 0, 0, 0, 0)) | ||||||
| 
 | 
 | ||||||
|     out = str(tmp_path / "temp.gif") |     out = tmp_path / "temp.gif" | ||||||
|     im.save(out, palette=[0, 0, 0, 0, 0, 0, 1, 1, 1]) |     im.save(out, palette=[0, 0, 0, 0, 0, 0, 1, 1, 1]) | ||||||
| 
 | 
 | ||||||
|     with Image.open(out) as reloaded: |     with Image.open(out) as reloaded: | ||||||
|  | @ -1317,7 +1350,7 @@ def test_palette_save_all_P(tmp_path: Path) -> None: | ||||||
|         frame.putpalette(color) |         frame.putpalette(color) | ||||||
|         frames.append(frame) |         frames.append(frame) | ||||||
| 
 | 
 | ||||||
|     out = str(tmp_path / "temp.gif") |     out = tmp_path / "temp.gif" | ||||||
|     frames[0].save( |     frames[0].save( | ||||||
|         out, save_all=True, palette=[255, 0, 0, 0, 255, 0], append_images=frames[1:] |         out, save_all=True, palette=[255, 0, 0, 0, 255, 0], append_images=frames[1:] | ||||||
|     ) |     ) | ||||||
|  | @ -1340,7 +1373,7 @@ def test_palette_save_ImagePalette(tmp_path: Path) -> None: | ||||||
|     im = hopper("P") |     im = hopper("P") | ||||||
|     palette = ImagePalette.ImagePalette("RGB", list(range(256))[::-1] * 3) |     palette = ImagePalette.ImagePalette("RGB", list(range(256))[::-1] * 3) | ||||||
| 
 | 
 | ||||||
|     out = str(tmp_path / "temp.gif") |     out = tmp_path / "temp.gif" | ||||||
|     im.save(out, palette=palette) |     im.save(out, palette=palette) | ||||||
| 
 | 
 | ||||||
|     with Image.open(out) as reloaded: |     with Image.open(out) as reloaded: | ||||||
|  | @ -1353,7 +1386,7 @@ def test_save_I(tmp_path: Path) -> None: | ||||||
| 
 | 
 | ||||||
|     im = hopper("I") |     im = hopper("I") | ||||||
| 
 | 
 | ||||||
|     out = str(tmp_path / "temp.gif") |     out = tmp_path / "temp.gif" | ||||||
|     im.save(out) |     im.save(out) | ||||||
| 
 | 
 | ||||||
|     with Image.open(out) as reloaded: |     with Image.open(out) as reloaded: | ||||||
|  | @ -1415,6 +1448,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 | ||||||
|  | @ -1437,7 +1471,7 @@ def test_missing_background() -> None: | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def test_saving_rgba(tmp_path: Path) -> None: | def test_saving_rgba(tmp_path: Path) -> None: | ||||||
|     out = str(tmp_path / "temp.gif") |     out = tmp_path / "temp.gif" | ||||||
|     with Image.open("Tests/images/transparent.png") as im: |     with Image.open("Tests/images/transparent.png") as im: | ||||||
|         im.save(out) |         im.save(out) | ||||||
| 
 | 
 | ||||||
|  | @ -1448,7 +1482,7 @@ def test_saving_rgba(tmp_path: Path) -> None: | ||||||
| 
 | 
 | ||||||
| @pytest.mark.parametrize("params", ({}, {"disposal": 2, "optimize": False})) | @pytest.mark.parametrize("params", ({}, {"disposal": 2, "optimize": False})) | ||||||
| def test_p_rgba(tmp_path: Path, params: dict[str, Any]) -> None: | def test_p_rgba(tmp_path: Path, params: dict[str, Any]) -> None: | ||||||
|     out = str(tmp_path / "temp.gif") |     out = tmp_path / "temp.gif" | ||||||
| 
 | 
 | ||||||
|     im1 = Image.new("P", (100, 100)) |     im1 = Image.new("P", (100, 100)) | ||||||
|     d = ImageDraw.Draw(im1) |     d = ImageDraw.Draw(im1) | ||||||
|  | @ -1462,4 +1496,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,3 +37,36 @@ def test_get_palette() -> None: | ||||||
| 
 | 
 | ||||||
|     # Assert |     # Assert | ||||||
|     assert mode == "RGB" |     assert mode == "RGB" | ||||||
|  |     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_load() -> None: | ||||||
| def test_save(tmp_path: Path) -> None: | def test_save(tmp_path: Path) -> None: | ||||||
|     # Arrange |     # Arrange | ||||||
|     im = hopper() |     im = hopper() | ||||||
|     tmpfile = str(tmp_path / "temp.grib") |     tmpfile = tmp_path / "temp.grib" | ||||||
| 
 | 
 | ||||||
|     # 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): | ||||||
|  | @ -79,7 +79,7 @@ def test_handler(tmp_path: Path) -> None: | ||||||
|         im.load() |         im.load() | ||||||
|         assert handler.is_loaded() |         assert handler.is_loaded() | ||||||
| 
 | 
 | ||||||
|         temp_file = str(tmp_path / "temp.grib") |         temp_file = tmp_path / "temp.grib" | ||||||
|         im.save(temp_file) |         im.save(temp_file) | ||||||
|         assert handler.saved |         assert handler.saved | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -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): | ||||||
|  | @ -81,7 +81,7 @@ def test_handler(tmp_path: Path) -> None: | ||||||
|         im.load() |         im.load() | ||||||
|         assert handler.is_loaded() |         assert handler.is_loaded() | ||||||
| 
 | 
 | ||||||
|         temp_file = str(tmp_path / "temp.h5") |         temp_file = tmp_path / "temp.h5" | ||||||
|         im.save(temp_file) |         im.save(temp_file) | ||||||
|         assert handler.saved |         assert handler.saved | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -43,7 +43,7 @@ def test_load() -> None: | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def test_save(tmp_path: Path) -> None: | def test_save(tmp_path: Path) -> None: | ||||||
|     temp_file = str(tmp_path / "temp.icns") |     temp_file = tmp_path / "temp.icns" | ||||||
| 
 | 
 | ||||||
|     with Image.open(TEST_FILE) as im: |     with Image.open(TEST_FILE) as im: | ||||||
|         im.save(temp_file) |         im.save(temp_file) | ||||||
|  | @ -60,7 +60,7 @@ def test_save(tmp_path: Path) -> None: | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def test_save_append_images(tmp_path: Path) -> None: | def test_save_append_images(tmp_path: Path) -> None: | ||||||
|     temp_file = str(tmp_path / "temp.icns") |     temp_file = tmp_path / "temp.icns" | ||||||
|     provided_im = Image.new("RGBA", (32, 32), (255, 0, 0, 128)) |     provided_im = Image.new("RGBA", (32, 32), (255, 0, 0, 128)) | ||||||
| 
 | 
 | ||||||
|     with Image.open(TEST_FILE) as im: |     with Image.open(TEST_FILE) as im: | ||||||
|  | @ -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" | ||||||
|  |  | ||||||
|  | @ -41,7 +41,7 @@ def test_black_and_white() -> None: | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def test_palette(tmp_path: Path) -> None: | def test_palette(tmp_path: Path) -> None: | ||||||
|     temp_file = str(tmp_path / "temp.ico") |     temp_file = tmp_path / "temp.ico" | ||||||
| 
 | 
 | ||||||
|     im = Image.new("P", (16, 16)) |     im = Image.new("P", (16, 16)) | ||||||
|     im.save(temp_file) |     im.save(temp_file) | ||||||
|  | @ -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 | ||||||
|  | @ -88,12 +89,13 @@ def test_save_to_bytes() -> None: | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def test_getpixel(tmp_path: Path) -> None: | def test_getpixel(tmp_path: Path) -> None: | ||||||
|     temp_file = str(tmp_path / "temp.ico") |     temp_file = tmp_path / "temp.ico" | ||||||
| 
 | 
 | ||||||
|     im = hopper() |     im = hopper() | ||||||
|     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) | ||||||
| 
 | 
 | ||||||
|  | @ -101,8 +103,8 @@ def test_getpixel(tmp_path: Path) -> None: | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def test_no_duplicates(tmp_path: Path) -> None: | def test_no_duplicates(tmp_path: Path) -> None: | ||||||
|     temp_file = str(tmp_path / "temp.ico") |     temp_file = tmp_path / "temp.ico" | ||||||
|     temp_file2 = str(tmp_path / "temp2.ico") |     temp_file2 = tmp_path / "temp2.ico" | ||||||
| 
 | 
 | ||||||
|     im = hopper() |     im = hopper() | ||||||
|     sizes = [(32, 32), (64, 64)] |     sizes = [(32, 32), (64, 64)] | ||||||
|  | @ -115,8 +117,8 @@ def test_no_duplicates(tmp_path: Path) -> None: | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def test_different_bit_depths(tmp_path: Path) -> None: | def test_different_bit_depths(tmp_path: Path) -> None: | ||||||
|     temp_file = str(tmp_path / "temp.ico") |     temp_file = tmp_path / "temp.ico" | ||||||
|     temp_file2 = str(tmp_path / "temp2.ico") |     temp_file2 = tmp_path / "temp2.ico" | ||||||
| 
 | 
 | ||||||
|     im = hopper() |     im = hopper() | ||||||
|     im.save(temp_file, "ico", bitmap_format="bmp", sizes=[(128, 128)]) |     im.save(temp_file, "ico", bitmap_format="bmp", sizes=[(128, 128)]) | ||||||
|  | @ -132,8 +134,8 @@ def test_different_bit_depths(tmp_path: Path) -> None: | ||||||
|     assert os.path.getsize(temp_file) != os.path.getsize(temp_file2) |     assert os.path.getsize(temp_file) != os.path.getsize(temp_file2) | ||||||
| 
 | 
 | ||||||
|     # Test that only matching sizes of different bit depths are saved |     # Test that only matching sizes of different bit depths are saved | ||||||
|     temp_file3 = str(tmp_path / "temp3.ico") |     temp_file3 = tmp_path / "temp3.ico" | ||||||
|     temp_file4 = str(tmp_path / "temp4.ico") |     temp_file4 = tmp_path / "temp4.ico" | ||||||
| 
 | 
 | ||||||
|     im.save(temp_file3, "ico", bitmap_format="bmp", sizes=[(128, 128)]) |     im.save(temp_file3, "ico", bitmap_format="bmp", sizes=[(128, 128)]) | ||||||
|     im.save( |     im.save( | ||||||
|  | @ -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) | ||||||
| 
 | 
 | ||||||
|  | @ -186,7 +190,7 @@ def test_save_256x256(tmp_path: Path) -> None: | ||||||
|     """Issue #2264 https://github.com/python-pillow/Pillow/issues/2264""" |     """Issue #2264 https://github.com/python-pillow/Pillow/issues/2264""" | ||||||
|     # Arrange |     # Arrange | ||||||
|     with Image.open("Tests/images/hopper_256x256.ico") as im: |     with Image.open("Tests/images/hopper_256x256.ico") as im: | ||||||
|         outfile = str(tmp_path / "temp_saved_hopper_256x256.ico") |         outfile = tmp_path / "temp_saved_hopper_256x256.ico" | ||||||
| 
 | 
 | ||||||
|         # Act |         # Act | ||||||
|         im.save(outfile) |         im.save(outfile) | ||||||
|  | @ -202,7 +206,7 @@ def test_only_save_relevant_sizes(tmp_path: Path) -> None: | ||||||
|     """ |     """ | ||||||
|     # Arrange |     # Arrange | ||||||
|     with Image.open("Tests/images/python.ico") as im:  # 16x16, 32x32, 48x48 |     with Image.open("Tests/images/python.ico") as im:  # 16x16, 32x32, 48x48 | ||||||
|         outfile = str(tmp_path / "temp_saved_python.ico") |         outfile = tmp_path / "temp_saved_python.ico" | ||||||
|         # Act |         # Act | ||||||
|         im.save(outfile) |         im.save(outfile) | ||||||
| 
 | 
 | ||||||
|  | @ -215,10 +219,11 @@ def test_save_append_images(tmp_path: Path) -> None: | ||||||
|     # append_images should be used for scaled down versions of the image |     # append_images should be used for scaled down versions of the image | ||||||
|     im = hopper("RGBA") |     im = hopper("RGBA") | ||||||
|     provided_im = Image.new("RGBA", (32, 32), (255, 0, 0)) |     provided_im = Image.new("RGBA", (32, 32), (255, 0, 0)) | ||||||
|     outfile = str(tmp_path / "temp_saved_multi_icon.ico") |     outfile = tmp_path / "temp_saved_multi_icon.ico" | ||||||
|     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) | ||||||
|  | @ -235,7 +240,7 @@ def test_unexpected_size() -> None: | ||||||
| 
 | 
 | ||||||
| def test_draw_reloaded(tmp_path: Path) -> None: | def test_draw_reloaded(tmp_path: Path) -> None: | ||||||
|     with Image.open(TEST_ICO_FILE) as im: |     with Image.open(TEST_ICO_FILE) as im: | ||||||
|         outfile = str(tmp_path / "temp_saved_hopper_draw.ico") |         outfile = tmp_path / "temp_saved_hopper_draw.ico" | ||||||
| 
 | 
 | ||||||
|         draw = ImageDraw.Draw(im) |         draw = ImageDraw.Draw(im) | ||||||
|         draw.line((0, 0) + im.size, "#f00") |         draw.line((0, 0) + im.size, "#f00") | ||||||
|  |  | ||||||
|  | @ -23,7 +23,7 @@ def test_sanity() -> None: | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def test_name_limit(tmp_path: Path) -> None: | def test_name_limit(tmp_path: Path) -> None: | ||||||
|     out = str(tmp_path / ("name_limit_test" * 7 + ".im")) |     out = tmp_path / ("name_limit_test" * 7 + ".im") | ||||||
|     with Image.open(TEST_IM) as im: |     with Image.open(TEST_IM) as im: | ||||||
|         im.save(out) |         im.save(out) | ||||||
|     assert filecmp.cmp(out, "Tests/images/hopper_long_name.im") |     assert filecmp.cmp(out, "Tests/images/hopper_long_name.im") | ||||||
|  | @ -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 | ||||||
|  | @ -87,7 +89,7 @@ def test_eoferror() -> None: | ||||||
| 
 | 
 | ||||||
| @pytest.mark.parametrize("mode", ("RGB", "P", "PA")) | @pytest.mark.parametrize("mode", ("RGB", "P", "PA")) | ||||||
| def test_roundtrip(mode: str, tmp_path: Path) -> None: | def test_roundtrip(mode: str, tmp_path: Path) -> None: | ||||||
|     out = str(tmp_path / "temp.im") |     out = tmp_path / "temp.im" | ||||||
|     im = hopper(mode) |     im = hopper(mode) | ||||||
|     im.save(out) |     im.save(out) | ||||||
|     assert_image_equal_tofile(im, out) |     assert_image_equal_tofile(im, out) | ||||||
|  | @ -98,7 +100,7 @@ def test_small_palette(tmp_path: Path) -> None: | ||||||
|     colors = [0, 1, 2] |     colors = [0, 1, 2] | ||||||
|     im.putpalette(colors) |     im.putpalette(colors) | ||||||
| 
 | 
 | ||||||
|     out = str(tmp_path / "temp.im") |     out = tmp_path / "temp.im" | ||||||
|     im.save(out) |     im.save(out) | ||||||
| 
 | 
 | ||||||
|     with Image.open(out) as reloaded: |     with Image.open(out) as reloaded: | ||||||
|  | @ -106,7 +108,7 @@ def test_small_palette(tmp_path: Path) -> None: | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def test_save_unsupported_mode(tmp_path: Path) -> None: | def test_save_unsupported_mode(tmp_path: Path) -> None: | ||||||
|     out = str(tmp_path / "temp.im") |     out = tmp_path / "temp.im" | ||||||
|     im = hopper("HSV") |     im = hopper("HSV") | ||||||
|     with pytest.raises(ValueError): |     with pytest.raises(ValueError): | ||||||
|         im.save(out) |         im.save(out) | ||||||
|  |  | ||||||
|  | @ -83,7 +83,7 @@ class TestFileJpeg: | ||||||
| 
 | 
 | ||||||
|     @pytest.mark.parametrize("size", ((1, 0), (0, 1), (0, 0))) |     @pytest.mark.parametrize("size", ((1, 0), (0, 1), (0, 0))) | ||||||
|     def test_zero(self, size: tuple[int, int], tmp_path: Path) -> None: |     def test_zero(self, size: tuple[int, int], tmp_path: Path) -> None: | ||||||
|         f = str(tmp_path / "temp.jpg") |         f = tmp_path / "temp.jpg" | ||||||
|         im = Image.new("RGB", size) |         im = Image.new("RGB", size) | ||||||
|         with pytest.raises(ValueError): |         with pytest.raises(ValueError): | ||||||
|             im.save(f) |             im.save(f) | ||||||
|  | @ -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", | ||||||
|  | @ -194,7 +195,7 @@ class TestFileJpeg: | ||||||
|             icc_profile = im1.info["icc_profile"] |             icc_profile = im1.info["icc_profile"] | ||||||
|             assert len(icc_profile) == 3144 |             assert len(icc_profile) == 3144 | ||||||
|             # Roundtrip via physical file. |             # Roundtrip via physical file. | ||||||
|             f = str(tmp_path / "temp.jpg") |             f = tmp_path / "temp.jpg" | ||||||
|             im1.save(f, icc_profile=icc_profile) |             im1.save(f, icc_profile=icc_profile) | ||||||
|         with Image.open(f) as im2: |         with Image.open(f) as im2: | ||||||
|             assert im2.info.get("icc_profile") == icc_profile |             assert im2.info.get("icc_profile") == icc_profile | ||||||
|  | @ -238,7 +239,7 @@ class TestFileJpeg: | ||||||
|         # Sometimes the meta data on the icc_profile block is bigger than |         # Sometimes the meta data on the icc_profile block is bigger than | ||||||
|         # Image.MAXBLOCK or the image size. |         # Image.MAXBLOCK or the image size. | ||||||
|         with Image.open("Tests/images/icc_profile_big.jpg") as im: |         with Image.open("Tests/images/icc_profile_big.jpg") as im: | ||||||
|             f = str(tmp_path / "temp.jpg") |             f = tmp_path / "temp.jpg" | ||||||
|             icc_profile = im.info["icc_profile"] |             icc_profile = im.info["icc_profile"] | ||||||
|             # Should not raise OSError for image with icc larger than image size. |             # Should not raise OSError for image with icc larger than image size. | ||||||
|             im.save( |             im.save( | ||||||
|  | @ -250,11 +251,11 @@ class TestFileJpeg: | ||||||
|             ) |             ) | ||||||
| 
 | 
 | ||||||
|         with Image.open("Tests/images/flower2.jpg") as im: |         with Image.open("Tests/images/flower2.jpg") as im: | ||||||
|             f = str(tmp_path / "temp2.jpg") |             f = tmp_path / "temp2.jpg" | ||||||
|             im.save(f, progressive=True, quality=94, icc_profile=b" " * 53955) |             im.save(f, progressive=True, quality=94, icc_profile=b" " * 53955) | ||||||
| 
 | 
 | ||||||
|         with Image.open("Tests/images/flower2.jpg") as im: |         with Image.open("Tests/images/flower2.jpg") as im: | ||||||
|             f = str(tmp_path / "temp3.jpg") |             f = tmp_path / "temp3.jpg" | ||||||
|             im.save(f, progressive=True, quality=94, exif=b" " * 43668) |             im.save(f, progressive=True, quality=94, exif=b" " * 43668) | ||||||
| 
 | 
 | ||||||
|     def test_optimize(self) -> None: |     def test_optimize(self) -> None: | ||||||
|  | @ -268,7 +269,7 @@ class TestFileJpeg: | ||||||
| 
 | 
 | ||||||
|     def test_optimize_large_buffer(self, tmp_path: Path) -> None: |     def test_optimize_large_buffer(self, tmp_path: Path) -> None: | ||||||
|         # https://github.com/python-pillow/Pillow/issues/148 |         # https://github.com/python-pillow/Pillow/issues/148 | ||||||
|         f = str(tmp_path / "temp.jpg") |         f = tmp_path / "temp.jpg" | ||||||
|         # this requires ~ 1.5x Image.MAXBLOCK |         # this requires ~ 1.5x Image.MAXBLOCK | ||||||
|         im = Image.new("RGB", (4096, 4096), 0xFF3333) |         im = Image.new("RGB", (4096, 4096), 0xFF3333) | ||||||
|         im.save(f, format="JPEG", optimize=True) |         im.save(f, format="JPEG", optimize=True) | ||||||
|  | @ -288,13 +289,13 @@ class TestFileJpeg: | ||||||
|         assert im1_bytes >= im3_bytes |         assert im1_bytes >= im3_bytes | ||||||
| 
 | 
 | ||||||
|     def test_progressive_large_buffer(self, tmp_path: Path) -> None: |     def test_progressive_large_buffer(self, tmp_path: Path) -> None: | ||||||
|         f = str(tmp_path / "temp.jpg") |         f = tmp_path / "temp.jpg" | ||||||
|         # this requires ~ 1.5x Image.MAXBLOCK |         # this requires ~ 1.5x Image.MAXBLOCK | ||||||
|         im = Image.new("RGB", (4096, 4096), 0xFF3333) |         im = Image.new("RGB", (4096, 4096), 0xFF3333) | ||||||
|         im.save(f, format="JPEG", progressive=True) |         im.save(f, format="JPEG", progressive=True) | ||||||
| 
 | 
 | ||||||
|     def test_progressive_large_buffer_highest_quality(self, tmp_path: Path) -> None: |     def test_progressive_large_buffer_highest_quality(self, tmp_path: Path) -> None: | ||||||
|         f = str(tmp_path / "temp.jpg") |         f = tmp_path / "temp.jpg" | ||||||
|         im = self.gen_random_image((255, 255)) |         im = self.gen_random_image((255, 255)) | ||||||
|         # this requires more bytes than pixels in the image |         # this requires more bytes than pixels in the image | ||||||
|         im.save(f, format="JPEG", progressive=True, quality=100) |         im.save(f, format="JPEG", progressive=True, quality=100) | ||||||
|  | @ -307,7 +308,7 @@ class TestFileJpeg: | ||||||
| 
 | 
 | ||||||
|     def test_large_exif(self, tmp_path: Path) -> None: |     def test_large_exif(self, tmp_path: Path) -> None: | ||||||
|         # https://github.com/python-pillow/Pillow/issues/148 |         # https://github.com/python-pillow/Pillow/issues/148 | ||||||
|         f = str(tmp_path / "temp.jpg") |         f = tmp_path / "temp.jpg" | ||||||
|         im = hopper() |         im = hopper() | ||||||
|         im.save(f, "JPEG", quality=90, exif=b"1" * 65533) |         im.save(f, "JPEG", quality=90, exif=b"1" * 65533) | ||||||
| 
 | 
 | ||||||
|  | @ -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() | ||||||
| 
 | 
 | ||||||
|  | @ -335,7 +338,7 @@ class TestFileJpeg: | ||||||
|             assert exif[gps_index] == expected_exif_gps |             assert exif[gps_index] == expected_exif_gps | ||||||
| 
 | 
 | ||||||
|         # Writing |         # Writing | ||||||
|         f = str(tmp_path / "temp.jpg") |         f = tmp_path / "temp.jpg" | ||||||
|         exif = Image.Exif() |         exif = Image.Exif() | ||||||
|         exif[gps_index] = expected_exif_gps |         exif[gps_index] = expected_exif_gps | ||||||
|         hopper().save(f, exif=exif) |         hopper().save(f, exif=exif) | ||||||
|  | @ -500,20 +503,21 @@ 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: | ||||||
|         # RGB |         # RGB | ||||||
|         with Image.open("Tests/images/hopper.jpg") as im: |         with Image.open("Tests/images/hopper.jpg") as im: | ||||||
|             f = str(tmp_path / "temp.jpg") |             f = tmp_path / "temp.jpg" | ||||||
|             im.save(f, quality="keep") |             im.save(f, quality="keep") | ||||||
|         # Grayscale |         # Grayscale | ||||||
|         with Image.open("Tests/images/hopper_gray.jpg") as im: |         with Image.open("Tests/images/hopper_gray.jpg") as im: | ||||||
|             f = str(tmp_path / "temp.jpg") |             f = tmp_path / "temp.jpg" | ||||||
|             im.save(f, quality="keep") |             im.save(f, quality="keep") | ||||||
|         # CMYK |         # CMYK | ||||||
|         with Image.open("Tests/images/pil_sample_cmyk.jpg") as im: |         with Image.open("Tests/images/pil_sample_cmyk.jpg") as im: | ||||||
|             f = str(tmp_path / "temp.jpg") |             f = tmp_path / "temp.jpg" | ||||||
|             im.save(f, quality="keep") |             im.save(f, quality="keep") | ||||||
| 
 | 
 | ||||||
|     def test_junk_jpeg_header(self) -> None: |     def test_junk_jpeg_header(self) -> 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) | ||||||
| 
 | 
 | ||||||
|  | @ -726,7 +734,7 @@ class TestFileJpeg: | ||||||
| 
 | 
 | ||||||
|     def test_MAXBLOCK_scaling(self, tmp_path: Path) -> None: |     def test_MAXBLOCK_scaling(self, tmp_path: Path) -> None: | ||||||
|         im = self.gen_random_image((512, 512)) |         im = self.gen_random_image((512, 512)) | ||||||
|         f = str(tmp_path / "temp.jpeg") |         f = tmp_path / "temp.jpeg" | ||||||
|         im.save(f, quality=100, optimize=True) |         im.save(f, quality=100, optimize=True) | ||||||
| 
 | 
 | ||||||
|         with Image.open(f) as reloaded: |         with Image.open(f) as reloaded: | ||||||
|  | @ -762,7 +770,7 @@ class TestFileJpeg: | ||||||
| 
 | 
 | ||||||
|     def test_save_tiff_with_dpi(self, tmp_path: Path) -> None: |     def test_save_tiff_with_dpi(self, tmp_path: Path) -> None: | ||||||
|         # Arrange |         # Arrange | ||||||
|         outfile = str(tmp_path / "temp.tif") |         outfile = tmp_path / "temp.tif" | ||||||
|         with Image.open("Tests/images/hopper.tif") as im: |         with Image.open("Tests/images/hopper.tif") as im: | ||||||
|             # Act |             # Act | ||||||
|             im.save(outfile, "JPEG", dpi=im.info["dpi"]) |             im.save(outfile, "JPEG", dpi=im.info["dpi"]) | ||||||
|  | @ -773,7 +781,7 @@ class TestFileJpeg: | ||||||
|                 assert im.info["dpi"] == reloaded.info["dpi"] |                 assert im.info["dpi"] == reloaded.info["dpi"] | ||||||
| 
 | 
 | ||||||
|     def test_save_dpi_rounding(self, tmp_path: Path) -> None: |     def test_save_dpi_rounding(self, tmp_path: Path) -> None: | ||||||
|         outfile = str(tmp_path / "temp.jpg") |         outfile = tmp_path / "temp.jpg" | ||||||
|         with Image.open("Tests/images/hopper.jpg") as im: |         with Image.open("Tests/images/hopper.jpg") as im: | ||||||
|             im.save(outfile, dpi=(72.2, 72.2)) |             im.save(outfile, dpi=(72.2, 72.2)) | ||||||
| 
 | 
 | ||||||
|  | @ -859,7 +867,7 @@ class TestFileJpeg: | ||||||
|             exif = im.getexif() |             exif = im.getexif() | ||||||
|             assert exif[282] == 180 |             assert exif[282] == 180 | ||||||
| 
 | 
 | ||||||
|             out = str(tmp_path / "out.jpg") |             out = tmp_path / "out.jpg" | ||||||
|             with warnings.catch_warnings(): |             with warnings.catch_warnings(): | ||||||
|                 warnings.simplefilter("error") |                 warnings.simplefilter("error") | ||||||
| 
 | 
 | ||||||
|  | @ -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"] | ||||||
|  | @ -1005,7 +1014,7 @@ class TestFileJpeg: | ||||||
|                 assert im.getxmp() == {"xmpmeta": None} |                 assert im.getxmp() == {"xmpmeta": None} | ||||||
| 
 | 
 | ||||||
|     def test_save_xmp(self, tmp_path: Path) -> None: |     def test_save_xmp(self, tmp_path: Path) -> None: | ||||||
|         f = str(tmp_path / "temp.jpg") |         f = tmp_path / "temp.jpg" | ||||||
|         im = hopper() |         im = hopper() | ||||||
|         im.save(f, xmp=b"XMP test") |         im.save(f, xmp=b"XMP test") | ||||||
|         with Image.open(f) as reloaded: |         with Image.open(f) as reloaded: | ||||||
|  | @ -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): | ||||||
|  | @ -1094,7 +1104,7 @@ class TestFileJpeg: | ||||||
| @skip_unless_feature("jpg") | @skip_unless_feature("jpg") | ||||||
| class TestFileCloseW32: | class TestFileCloseW32: | ||||||
|     def test_fd_leak(self, tmp_path: Path) -> None: |     def test_fd_leak(self, tmp_path: Path) -> None: | ||||||
|         tmpfile = str(tmp_path / "temp.jpg") |         tmpfile = tmp_path / "temp.jpg" | ||||||
| 
 | 
 | ||||||
|         with Image.open("Tests/images/hopper.jpg") as im: |         with Image.open("Tests/images/hopper.jpg") as im: | ||||||
|             im.save(tmpfile) |             im.save(tmpfile) | ||||||
|  |  | ||||||
|  | @ -99,7 +99,7 @@ def test_bytesio(card: ImageFile.ImageFile) -> None: | ||||||
| def test_lossless(card: ImageFile.ImageFile, tmp_path: Path) -> None: | def test_lossless(card: ImageFile.ImageFile, tmp_path: Path) -> None: | ||||||
|     with Image.open("Tests/images/test-card-lossless.jp2") as im: |     with Image.open("Tests/images/test-card-lossless.jp2") as im: | ||||||
|         im.load() |         im.load() | ||||||
|         outfile = str(tmp_path / "temp_test-card.png") |         outfile = tmp_path / "temp_test-card.png" | ||||||
|         im.save(outfile) |         im.save(outfile) | ||||||
|     assert_image_similar(im, card, 1.0e-3) |     assert_image_similar(im, card, 1.0e-3) | ||||||
| 
 | 
 | ||||||
|  | @ -213,7 +213,7 @@ def test_header_errors() -> None: | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def test_layers_type(card: ImageFile.ImageFile, tmp_path: Path) -> None: | def test_layers_type(card: ImageFile.ImageFile, tmp_path: Path) -> None: | ||||||
|     outfile = str(tmp_path / "temp_layers.jp2") |     outfile = tmp_path / "temp_layers.jp2" | ||||||
|     for quality_layers in [[100, 50, 10], (100, 50, 10), None]: |     for quality_layers in [[100, 50, 10], (100, 50, 10), None]: | ||||||
|         card.save(outfile, quality_layers=quality_layers) |         card.save(outfile, quality_layers=quality_layers) | ||||||
| 
 | 
 | ||||||
|  | @ -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) | ||||||
|  | @ -289,7 +291,7 @@ def test_mct(card: ImageFile.ImageFile) -> None: | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def test_sgnd(tmp_path: Path) -> None: | def test_sgnd(tmp_path: Path) -> None: | ||||||
|     outfile = str(tmp_path / "temp.jp2") |     outfile = tmp_path / "temp.jp2" | ||||||
| 
 | 
 | ||||||
|     im = Image.new("L", (1, 1)) |     im = Image.new("L", (1, 1)) | ||||||
|     im.save(outfile) |     im.save(outfile) | ||||||
|  | @ -313,6 +315,18 @@ def test_rgba(ext: str) -> None: | ||||||
|         assert im.mode == "RGBA" |         assert im.mode == "RGBA" | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | def test_grayscale_four_channels() -> None: | ||||||
|  |     with open("Tests/images/rgb_trns_ycbc.jp2", "rb") as fp: | ||||||
|  |         data = fp.read() | ||||||
|  | 
 | ||||||
|  |     # Change color space to OPJ_CLRSPC_GRAY | ||||||
|  |     data = data[:76] + b"\x11" + data[77:] | ||||||
|  | 
 | ||||||
|  |     with Image.open(BytesIO(data)) as im: | ||||||
|  |         im.load() | ||||||
|  |         assert im.mode == "RGBA" | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
| @pytest.mark.skipif( | @pytest.mark.skipif( | ||||||
|     not os.path.exists(EXTRA_DIR), reason="Extra image files not installed" |     not os.path.exists(EXTRA_DIR), reason="Extra image files not installed" | ||||||
| ) | ) | ||||||
|  |  | ||||||
|  | @ -36,10 +36,11 @@ 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. | ||||||
|         out = str(tmp_path / "temp.png") |         out = tmp_path / "temp.png" | ||||||
|         im.save(out) |         im.save(out) | ||||||
| 
 | 
 | ||||||
|         out_bytes = io.BytesIO() |         out_bytes = io.BytesIO() | ||||||
|  | @ -123,7 +124,7 @@ class TestFileLibTiff(LibTiffTestCase): | ||||||
|         """Checking to see that the saved image is the same as what we wrote""" |         """Checking to see that the saved image is the same as what we wrote""" | ||||||
|         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: | ||||||
|             out = str(tmp_path / "temp.tif") |             out = tmp_path / "temp.tif" | ||||||
|             rot = orig.transpose(Image.Transpose.ROTATE_90) |             rot = orig.transpose(Image.Transpose.ROTATE_90) | ||||||
|             assert rot.size == (500, 500) |             assert rot.size == (500, 500) | ||||||
|             rot.save(out) |             rot.save(out) | ||||||
|  | @ -151,8 +152,9 @@ class TestFileLibTiff(LibTiffTestCase): | ||||||
|     @pytest.mark.parametrize("legacy_api", (False, True)) |     @pytest.mark.parametrize("legacy_api", (False, True)) | ||||||
|     def test_write_metadata(self, legacy_api: bool, tmp_path: Path) -> None: |     def test_write_metadata(self, legacy_api: bool, tmp_path: Path) -> None: | ||||||
|         """Test metadata writing through libtiff""" |         """Test metadata writing through libtiff""" | ||||||
|         f = str(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] | ||||||
|  | @ -247,7 +251,7 @@ class TestFileLibTiff(LibTiffTestCase): | ||||||
|             # Extra samples really doesn't make sense in this application. |             # Extra samples really doesn't make sense in this application. | ||||||
|             del new_ifd[338] |             del new_ifd[338] | ||||||
| 
 | 
 | ||||||
|             out = str(tmp_path / "temp.tif") |             out = tmp_path / "temp.tif" | ||||||
|             monkeypatch.setattr(TiffImagePlugin, "WRITE_LIBTIFF", True) |             monkeypatch.setattr(TiffImagePlugin, "WRITE_LIBTIFF", True) | ||||||
| 
 | 
 | ||||||
|             im.save(out, tiffinfo=new_ifd) |             im.save(out, tiffinfo=new_ifd) | ||||||
|  | @ -313,10 +317,11 @@ class TestFileLibTiff(LibTiffTestCase): | ||||||
|         ) -> None: |         ) -> None: | ||||||
|             im = hopper() |             im = hopper() | ||||||
| 
 | 
 | ||||||
|             out = str(tmp_path / "temp.tif") |             out = tmp_path / "temp.tif" | ||||||
|             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 ( | ||||||
|  | @ -347,14 +352,16 @@ class TestFileLibTiff(LibTiffTestCase): | ||||||
|         ) |         ) | ||||||
| 
 | 
 | ||||||
|     def test_osubfiletype(self, tmp_path: Path) -> None: |     def test_osubfiletype(self, tmp_path: Path) -> None: | ||||||
|         outfile = str(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 = str(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 | ||||||
|  | @ -365,17 +372,18 @@ class TestFileLibTiff(LibTiffTestCase): | ||||||
|     ) -> None: |     ) -> None: | ||||||
|         monkeypatch.setattr(TiffImagePlugin, "WRITE_LIBTIFF", True) |         monkeypatch.setattr(TiffImagePlugin, "WRITE_LIBTIFF", True) | ||||||
| 
 | 
 | ||||||
|         out = str(tmp_path / "temp.tif") |         out = tmp_path / "temp.tif" | ||||||
|         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" | ||||||
| 
 | 
 | ||||||
|     def test_int_dpi(self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None: |     def test_int_dpi(self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None: | ||||||
|         # issue #1765 |         # issue #1765 | ||||||
|         im = hopper("RGB") |         im = hopper("RGB") | ||||||
|         out = str(tmp_path / "temp.tif") |         out = tmp_path / "temp.tif" | ||||||
|         monkeypatch.setattr(TiffImagePlugin, "WRITE_LIBTIFF", True) |         monkeypatch.setattr(TiffImagePlugin, "WRITE_LIBTIFF", True) | ||||||
|         im.save(out, dpi=(72, 72)) |         im.save(out, dpi=(72, 72)) | ||||||
|         with Image.open(out) as reloaded: |         with Image.open(out) as reloaded: | ||||||
|  | @ -383,7 +391,7 @@ class TestFileLibTiff(LibTiffTestCase): | ||||||
| 
 | 
 | ||||||
|     def test_g3_compression(self, tmp_path: Path) -> None: |     def test_g3_compression(self, tmp_path: Path) -> None: | ||||||
|         with Image.open("Tests/images/hopper_g4_500.tif") as i: |         with Image.open("Tests/images/hopper_g4_500.tif") as i: | ||||||
|             out = str(tmp_path / "temp.tif") |             out = tmp_path / "temp.tif" | ||||||
|             i.save(out, compression="group3") |             i.save(out, compression="group3") | ||||||
| 
 | 
 | ||||||
|             with Image.open(out) as reread: |             with Image.open(out) as reread: | ||||||
|  | @ -400,7 +408,7 @@ class TestFileLibTiff(LibTiffTestCase): | ||||||
|             assert b[0] == ord(b"\xe0") |             assert b[0] == ord(b"\xe0") | ||||||
|             assert b[1] == ord(b"\x01") |             assert b[1] == ord(b"\x01") | ||||||
| 
 | 
 | ||||||
|             out = str(tmp_path / "temp.tif") |             out = tmp_path / "temp.tif" | ||||||
|             # out = "temp.le.tif" |             # out = "temp.le.tif" | ||||||
|             im.save(out) |             im.save(out) | ||||||
|         with Image.open(out) as reread: |         with Image.open(out) as reread: | ||||||
|  | @ -420,7 +428,7 @@ class TestFileLibTiff(LibTiffTestCase): | ||||||
|             assert b[0] == ord(b"\x01") |             assert b[0] == ord(b"\x01") | ||||||
|             assert b[1] == ord(b"\xe0") |             assert b[1] == ord(b"\xe0") | ||||||
| 
 | 
 | ||||||
|             out = str(tmp_path / "temp.tif") |             out = tmp_path / "temp.tif" | ||||||
|             im.save(out) |             im.save(out) | ||||||
|             with Image.open(out) as reread: |             with Image.open(out) as reread: | ||||||
|                 assert reread.info["compression"] == im.info["compression"] |                 assert reread.info["compression"] == im.info["compression"] | ||||||
|  | @ -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: | ||||||
|             out = str(tmp_path / "temp.tif") |             assert isinstance(orig, TiffImagePlugin.TiffImageFile) | ||||||
|  | 
 | ||||||
|  |             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] | ||||||
| 
 | 
 | ||||||
|  | @ -457,7 +468,7 @@ class TestFileLibTiff(LibTiffTestCase): | ||||||
|     def test_blur(self, tmp_path: Path) -> None: |     def test_blur(self, tmp_path: Path) -> None: | ||||||
|         # test case from irc, how to do blur on b/w image |         # test case from irc, how to do blur on b/w image | ||||||
|         # and save to compressed tif. |         # and save to compressed tif. | ||||||
|         out = str(tmp_path / "temp.tif") |         out = tmp_path / "temp.tif" | ||||||
|         with Image.open("Tests/images/pport_g4.tif") as im: |         with Image.open("Tests/images/pport_g4.tif") as im: | ||||||
|             im = im.convert("L") |             im = im.convert("L") | ||||||
| 
 | 
 | ||||||
|  | @ -470,7 +481,7 @@ class TestFileLibTiff(LibTiffTestCase): | ||||||
|         # Test various tiff compressions and assert similar image content but reduced |         # Test various tiff compressions and assert similar image content but reduced | ||||||
|         # file sizes. |         # file sizes. | ||||||
|         im = hopper("RGB") |         im = hopper("RGB") | ||||||
|         out = str(tmp_path / "temp.tif") |         out = tmp_path / "temp.tif" | ||||||
|         im.save(out) |         im.save(out) | ||||||
|         size_raw = os.path.getsize(out) |         size_raw = os.path.getsize(out) | ||||||
| 
 | 
 | ||||||
|  | @ -494,7 +505,7 @@ class TestFileLibTiff(LibTiffTestCase): | ||||||
| 
 | 
 | ||||||
|     def test_tiff_jpeg_compression(self, tmp_path: Path) -> None: |     def test_tiff_jpeg_compression(self, tmp_path: Path) -> None: | ||||||
|         im = hopper("RGB") |         im = hopper("RGB") | ||||||
|         out = str(tmp_path / "temp.tif") |         out = tmp_path / "temp.tif" | ||||||
|         im.save(out, compression="tiff_jpeg") |         im.save(out, compression="tiff_jpeg") | ||||||
| 
 | 
 | ||||||
|         with Image.open(out) as reloaded: |         with Image.open(out) as reloaded: | ||||||
|  | @ -502,7 +513,7 @@ class TestFileLibTiff(LibTiffTestCase): | ||||||
| 
 | 
 | ||||||
|     def test_tiff_deflate_compression(self, tmp_path: Path) -> None: |     def test_tiff_deflate_compression(self, tmp_path: Path) -> None: | ||||||
|         im = hopper("RGB") |         im = hopper("RGB") | ||||||
|         out = str(tmp_path / "temp.tif") |         out = tmp_path / "temp.tif" | ||||||
|         im.save(out, compression="tiff_deflate") |         im.save(out, compression="tiff_deflate") | ||||||
| 
 | 
 | ||||||
|         with Image.open(out) as reloaded: |         with Image.open(out) as reloaded: | ||||||
|  | @ -510,7 +521,7 @@ class TestFileLibTiff(LibTiffTestCase): | ||||||
| 
 | 
 | ||||||
|     def test_quality(self, tmp_path: Path) -> None: |     def test_quality(self, tmp_path: Path) -> None: | ||||||
|         im = hopper("RGB") |         im = hopper("RGB") | ||||||
|         out = str(tmp_path / "temp.tif") |         out = tmp_path / "temp.tif" | ||||||
| 
 | 
 | ||||||
|         with pytest.raises(ValueError): |         with pytest.raises(ValueError): | ||||||
|             im.save(out, compression="tiff_lzw", quality=50) |             im.save(out, compression="tiff_lzw", quality=50) | ||||||
|  | @ -525,7 +536,7 @@ class TestFileLibTiff(LibTiffTestCase): | ||||||
| 
 | 
 | ||||||
|     def test_cmyk_save(self, tmp_path: Path) -> None: |     def test_cmyk_save(self, tmp_path: Path) -> None: | ||||||
|         im = hopper("CMYK") |         im = hopper("CMYK") | ||||||
|         out = str(tmp_path / "temp.tif") |         out = tmp_path / "temp.tif" | ||||||
| 
 | 
 | ||||||
|         im.save(out, compression="tiff_adobe_deflate") |         im.save(out, compression="tiff_adobe_deflate") | ||||||
|         assert_image_equal_tofile(im, out) |         assert_image_equal_tofile(im, out) | ||||||
|  | @ -534,19 +545,20 @@ class TestFileLibTiff(LibTiffTestCase): | ||||||
|     def test_palette_save( |     def test_palette_save( | ||||||
|         self, im: Image.Image, monkeypatch: pytest.MonkeyPatch, tmp_path: Path |         self, im: Image.Image, monkeypatch: pytest.MonkeyPatch, tmp_path: Path | ||||||
|     ) -> None: |     ) -> None: | ||||||
|         out = str(tmp_path / "temp.tif") |         out = tmp_path / "temp.tif" | ||||||
| 
 | 
 | ||||||
|         monkeypatch.setattr(TiffImagePlugin, "WRITE_LIBTIFF", True) |         monkeypatch.setattr(TiffImagePlugin, "WRITE_LIBTIFF", True) | ||||||
|         im.save(out) |         im.save(out) | ||||||
| 
 | 
 | ||||||
|         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")) | ||||||
|     def test_bw_compression_w_rgb(self, compression: str, tmp_path: Path) -> None: |     def test_bw_compression_w_rgb(self, compression: str, tmp_path: Path) -> None: | ||||||
|         im = hopper("RGB") |         im = hopper("RGB") | ||||||
|         out = str(tmp_path / "temp.tif") |         out = tmp_path / "temp.tif" | ||||||
| 
 | 
 | ||||||
|         with pytest.raises(OSError): |         with pytest.raises(OSError): | ||||||
|             im.save(out, compression=compression) |             im.save(out, compression=compression) | ||||||
|  | @ -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 | ||||||
|  | @ -686,25 +701,29 @@ class TestFileLibTiff(LibTiffTestCase): | ||||||
| 
 | 
 | ||||||
|     def test_save_ycbcr(self, tmp_path: Path) -> None: |     def test_save_ycbcr(self, tmp_path: Path) -> None: | ||||||
|         im = hopper("YCbCr") |         im = hopper("YCbCr") | ||||||
|         outfile = str(tmp_path / "temp.tif") |         outfile = tmp_path / "temp.tif" | ||||||
|         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 | ||||||
| 
 | 
 | ||||||
|  | @ -713,7 +732,7 @@ class TestFileLibTiff(LibTiffTestCase): | ||||||
|     ) -> None: |     ) -> None: | ||||||
|         # issue 1597 |         # issue 1597 | ||||||
|         with Image.open("Tests/images/rdf.tif") as im: |         with Image.open("Tests/images/rdf.tif") as im: | ||||||
|             out = str(tmp_path / "temp.tif") |             out = tmp_path / "temp.tif" | ||||||
| 
 | 
 | ||||||
|             monkeypatch.setattr(TiffImagePlugin, "WRITE_LIBTIFF", True) |             monkeypatch.setattr(TiffImagePlugin, "WRITE_LIBTIFF", True) | ||||||
|             # this shouldn't crash |             # this shouldn't crash | ||||||
|  | @ -724,7 +743,7 @@ class TestFileLibTiff(LibTiffTestCase): | ||||||
|         # Test TIFF with tag 297 (Page Number) having value of 0 0. |         # Test TIFF with tag 297 (Page Number) having value of 0 0. | ||||||
|         # The first number is the current page number. |         # The first number is the current page number. | ||||||
|         # The second is the total number of pages, zero means not available. |         # The second is the total number of pages, zero means not available. | ||||||
|         outfile = str(tmp_path / "temp.tif") |         outfile = tmp_path / "temp.tif" | ||||||
|         # Created by printing a page in Chrome to PDF, then: |         # Created by printing a page in Chrome to PDF, then: | ||||||
|         # /usr/bin/gs -q -sDEVICE=tiffg3 -sOutputFile=total-pages-zero.tif |         # /usr/bin/gs -q -sDEVICE=tiffg3 -sOutputFile=total-pages-zero.tif | ||||||
|         # -dNOPAUSE /tmp/test.pdf -c quit |         # -dNOPAUSE /tmp/test.pdf -c quit | ||||||
|  | @ -736,7 +755,7 @@ class TestFileLibTiff(LibTiffTestCase): | ||||||
|     def test_fd_duplication(self, tmp_path: Path) -> None: |     def test_fd_duplication(self, tmp_path: Path) -> None: | ||||||
|         # https://github.com/python-pillow/Pillow/issues/1651 |         # https://github.com/python-pillow/Pillow/issues/1651 | ||||||
| 
 | 
 | ||||||
|         tmpfile = str(tmp_path / "temp.tif") |         tmpfile = tmp_path / "temp.tif" | ||||||
|         with open(tmpfile, "wb") as f: |         with open(tmpfile, "wb") as f: | ||||||
|             with open("Tests/images/g4-multi.tiff", "rb") as src: |             with open("Tests/images/g4-multi.tiff", "rb") as src: | ||||||
|                 f.write(src.read()) |                 f.write(src.read()) | ||||||
|  | @ -779,13 +798,14 @@ class TestFileLibTiff(LibTiffTestCase): | ||||||
|         with Image.open("Tests/images/hopper.iccprofile.tif") as img: |         with Image.open("Tests/images/hopper.iccprofile.tif") as img: | ||||||
|             icc_profile = img.info["icc_profile"] |             icc_profile = img.info["icc_profile"] | ||||||
| 
 | 
 | ||||||
|             out = str(tmp_path / "temp.tif") |             out = tmp_path / "temp.tif" | ||||||
|             img.save(out, icc_profile=icc_profile) |             img.save(out, icc_profile=icc_profile) | ||||||
|         with Image.open(out) as reloaded: |         with Image.open(out) as reloaded: | ||||||
|             assert icc_profile == reloaded.info["icc_profile"] |             assert icc_profile == reloaded.info["icc_profile"] | ||||||
| 
 | 
 | ||||||
|     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) | ||||||
|  | @ -802,7 +822,7 @@ class TestFileLibTiff(LibTiffTestCase): | ||||||
| 
 | 
 | ||||||
|     def test_save_tiff_with_jpegtables(self, tmp_path: Path) -> None: |     def test_save_tiff_with_jpegtables(self, tmp_path: Path) -> None: | ||||||
|         # Arrange |         # Arrange | ||||||
|         outfile = str(tmp_path / "temp.tif") |         outfile = tmp_path / "temp.tif" | ||||||
| 
 | 
 | ||||||
|         # Created with ImageMagick: convert hopper.jpg hopper_jpg.tif |         # Created with ImageMagick: convert hopper.jpg hopper_jpg.tif | ||||||
|         # Contains JPEGTables (347) tag |         # Contains JPEGTables (347) tag | ||||||
|  | @ -864,7 +884,7 @@ class TestFileLibTiff(LibTiffTestCase): | ||||||
|         self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path |         self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path | ||||||
|     ) -> None: |     ) -> None: | ||||||
|         im = Image.new("F", (1, 1)) |         im = Image.new("F", (1, 1)) | ||||||
|         out = str(tmp_path / "temp.tif") |         out = tmp_path / "temp.tif" | ||||||
|         monkeypatch.setattr(TiffImagePlugin, "WRITE_LIBTIFF", True) |         monkeypatch.setattr(TiffImagePlugin, "WRITE_LIBTIFF", True) | ||||||
|         im.save(out) |         im.save(out) | ||||||
| 
 | 
 | ||||||
|  | @ -1008,7 +1028,7 @@ class TestFileLibTiff(LibTiffTestCase): | ||||||
|     @pytest.mark.parametrize("compression", (None, "jpeg")) |     @pytest.mark.parametrize("compression", (None, "jpeg")) | ||||||
|     def test_block_tile_tags(self, compression: str | None, tmp_path: Path) -> None: |     def test_block_tile_tags(self, compression: str | None, tmp_path: Path) -> None: | ||||||
|         im = hopper() |         im = hopper() | ||||||
|         out = str(tmp_path / "temp.tif") |         out = tmp_path / "temp.tif" | ||||||
| 
 | 
 | ||||||
|         tags = { |         tags = { | ||||||
|             TiffImagePlugin.TILEWIDTH: 256, |             TiffImagePlugin.TILEWIDTH: 256, | ||||||
|  | @ -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() | ||||||
|  | @ -1140,16 +1172,14 @@ class TestFileLibTiff(LibTiffTestCase): | ||||||
|     def test_realloc_overflow(self, monkeypatch: pytest.MonkeyPatch) -> None: |     def test_realloc_overflow(self, monkeypatch: pytest.MonkeyPatch) -> None: | ||||||
|         monkeypatch.setattr(TiffImagePlugin, "READ_LIBTIFF", True) |         monkeypatch.setattr(TiffImagePlugin, "READ_LIBTIFF", True) | ||||||
|         with Image.open("Tests/images/tiff_overflow_rows_per_strip.tif") as im: |         with Image.open("Tests/images/tiff_overflow_rows_per_strip.tif") as im: | ||||||
|             with pytest.raises(OSError) as e: |  | ||||||
|                 im.load() |  | ||||||
| 
 |  | ||||||
|             # Assert that the error code is IMAGING_CODEC_MEMORY |             # Assert that the error code is IMAGING_CODEC_MEMORY | ||||||
|             assert str(e.value) == "decoder error -9" |             with pytest.raises(OSError, match="decoder error -9"): | ||||||
|  |                 im.load() | ||||||
| 
 | 
 | ||||||
|     @pytest.mark.parametrize("compression", ("tiff_adobe_deflate", "jpeg")) |     @pytest.mark.parametrize("compression", ("tiff_adobe_deflate", "jpeg")) | ||||||
|     def test_save_multistrip(self, compression: str, tmp_path: Path) -> None: |     def test_save_multistrip(self, compression: str, tmp_path: Path) -> None: | ||||||
|         im = hopper("RGB").resize((256, 256)) |         im = hopper("RGB").resize((256, 256)) | ||||||
|         out = str(tmp_path / "temp.tif") |         out = tmp_path / "temp.tif" | ||||||
|         im.save(out, compression=compression) |         im.save(out, compression=compression) | ||||||
| 
 | 
 | ||||||
|         with Image.open(out) as im: |         with Image.open(out) as im: | ||||||
|  | @ -1162,7 +1192,7 @@ class TestFileLibTiff(LibTiffTestCase): | ||||||
|         self, argument: bool, tmp_path: Path, monkeypatch: pytest.MonkeyPatch |         self, argument: bool, tmp_path: Path, monkeypatch: pytest.MonkeyPatch | ||||||
|     ) -> None: |     ) -> None: | ||||||
|         im = hopper("RGB").resize((256, 256)) |         im = hopper("RGB").resize((256, 256)) | ||||||
|         out = str(tmp_path / "temp.tif") |         out = tmp_path / "temp.tif" | ||||||
| 
 | 
 | ||||||
|         if not argument: |         if not argument: | ||||||
|             monkeypatch.setattr(TiffImagePlugin, "STRIP_SIZE", 2**18) |             monkeypatch.setattr(TiffImagePlugin, "STRIP_SIZE", 2**18) | ||||||
|  | @ -1178,13 +1208,13 @@ class TestFileLibTiff(LibTiffTestCase): | ||||||
|     @pytest.mark.parametrize("compression", ("tiff_adobe_deflate", None)) |     @pytest.mark.parametrize("compression", ("tiff_adobe_deflate", None)) | ||||||
|     def test_save_zero(self, compression: str | None, tmp_path: Path) -> None: |     def test_save_zero(self, compression: str | None, tmp_path: Path) -> None: | ||||||
|         im = Image.new("RGB", (0, 0)) |         im = Image.new("RGB", (0, 0)) | ||||||
|         out = str(tmp_path / "temp.tif") |         out = tmp_path / "temp.tif" | ||||||
|         with pytest.raises(SystemError): |         with pytest.raises(SystemError): | ||||||
|             im.save(out, compression=compression) |             im.save(out, compression=compression) | ||||||
| 
 | 
 | ||||||
|     def test_save_many_compressed(self, tmp_path: Path) -> None: |     def test_save_many_compressed(self, tmp_path: Path) -> None: | ||||||
|         im = hopper() |         im = hopper() | ||||||
|         out = str(tmp_path / "temp.tif") |         out = tmp_path / "temp.tif" | ||||||
|         for _ in range(10000): |         for _ in range(10000): | ||||||
|             im.save(out, compression="jpeg") |             im.save(out, compression="jpeg") | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -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) | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -15,7 +15,7 @@ YA_EXTRA_DIR = "Tests/images/msp" | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def test_sanity(tmp_path: Path) -> None: | def test_sanity(tmp_path: Path) -> None: | ||||||
|     test_file = str(tmp_path / "temp.msp") |     test_file = tmp_path / "temp.msp" | ||||||
| 
 | 
 | ||||||
|     hopper("1").save(test_file) |     hopper("1").save(test_file) | ||||||
| 
 | 
 | ||||||
|  | @ -84,7 +84,7 @@ def test_msp_v2() -> None: | ||||||
| def test_cannot_save_wrong_mode(tmp_path: Path) -> None: | def test_cannot_save_wrong_mode(tmp_path: Path) -> None: | ||||||
|     # Arrange |     # Arrange | ||||||
|     im = hopper() |     im = hopper() | ||||||
|     filename = str(tmp_path / "temp.msp") |     filename = tmp_path / "temp.msp" | ||||||
| 
 | 
 | ||||||
|     # Act/Assert |     # Act/Assert | ||||||
|     with pytest.raises(OSError): |     with pytest.raises(OSError): | ||||||
|  |  | ||||||
|  | @ -14,7 +14,7 @@ from .helper import assert_image_equal, hopper, magick_command | ||||||
| def helper_save_as_palm(tmp_path: Path, mode: str) -> None: | def helper_save_as_palm(tmp_path: Path, mode: str) -> None: | ||||||
|     # Arrange |     # Arrange | ||||||
|     im = hopper(mode) |     im = hopper(mode) | ||||||
|     outfile = str(tmp_path / ("temp_" + mode + ".palm")) |     outfile = tmp_path / ("temp_" + mode + ".palm") | ||||||
| 
 | 
 | ||||||
|     # Act |     # Act | ||||||
|     im.save(outfile) |     im.save(outfile) | ||||||
|  | @ -25,7 +25,7 @@ def helper_save_as_palm(tmp_path: Path, mode: str) -> None: | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def open_with_magick(magick: list[str], tmp_path: Path, f: str) -> Image.Image: | def open_with_magick(magick: list[str], tmp_path: Path, f: str) -> Image.Image: | ||||||
|     outfile = str(tmp_path / "temp.png") |     outfile = tmp_path / "temp.png" | ||||||
|     rc = subprocess.call( |     rc = subprocess.call( | ||||||
|         magick + [f, outfile], stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT |         magick + [f, outfile], stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT | ||||||
|     ) |     ) | ||||||
|  | @ -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" | ||||||
|  |  | ||||||
|  | @ -1,5 +1,6 @@ | ||||||
| from __future__ import annotations | from __future__ import annotations | ||||||
| 
 | 
 | ||||||
|  | import io | ||||||
| from pathlib import Path | from pathlib import Path | ||||||
| 
 | 
 | ||||||
| import pytest | import pytest | ||||||
|  | @ -10,7 +11,7 @@ from .helper import assert_image_equal, hopper | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def _roundtrip(tmp_path: Path, im: Image.Image) -> None: | def _roundtrip(tmp_path: Path, im: Image.Image) -> None: | ||||||
|     f = str(tmp_path / "temp.pcx") |     f = tmp_path / "temp.pcx" | ||||||
|     im.save(f) |     im.save(f) | ||||||
|     with Image.open(f) as im2: |     with Image.open(f) as im2: | ||||||
|         assert im2.mode == im.mode |         assert im2.mode == im.mode | ||||||
|  | @ -30,12 +31,34 @@ def test_sanity(tmp_path: Path) -> None: | ||||||
|     _roundtrip(tmp_path, im) |     _roundtrip(tmp_path, im) | ||||||
| 
 | 
 | ||||||
|     # Test an unsupported mode |     # Test an unsupported mode | ||||||
|     f = str(tmp_path / "temp.pcx") |     f = tmp_path / "temp.pcx" | ||||||
|     im = hopper("RGBA") |     im = hopper("RGBA") | ||||||
|     with pytest.raises(ValueError): |     with pytest.raises(ValueError): | ||||||
|         im.save(f) |         im.save(f) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | def test_bad_image_size() -> None: | ||||||
|  |     with open("Tests/images/pil184.pcx", "rb") as fp: | ||||||
|  |         data = fp.read() | ||||||
|  |     data = data[:4] + b"\xff\xff" + data[6:] | ||||||
|  | 
 | ||||||
|  |     b = io.BytesIO(data) | ||||||
|  |     with pytest.raises(SyntaxError, match="bad PCX image size"): | ||||||
|  |         with PcxImagePlugin.PcxImageFile(b): | ||||||
|  |             pass | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def test_unknown_mode() -> None: | ||||||
|  |     with open("Tests/images/pil184.pcx", "rb") as fp: | ||||||
|  |         data = fp.read() | ||||||
|  |     data = data[:3] + b"\xff" + data[4:] | ||||||
|  | 
 | ||||||
|  |     b = io.BytesIO(data) | ||||||
|  |     with pytest.raises(OSError, match="unknown PCX mode"): | ||||||
|  |         with Image.open(b): | ||||||
|  |             pass | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
| def test_invalid_file() -> None: | def test_invalid_file() -> None: | ||||||
|     invalid_file = "Tests/images/flower.jpg" |     invalid_file = "Tests/images/flower.jpg" | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -55,7 +55,7 @@ def test_save_alpha(tmp_path: Path, mode: str) -> None: | ||||||
| 
 | 
 | ||||||
| def test_p_alpha(tmp_path: Path) -> None: | def test_p_alpha(tmp_path: Path) -> None: | ||||||
|     # Arrange |     # Arrange | ||||||
|     outfile = str(tmp_path / "temp.pdf") |     outfile = tmp_path / "temp.pdf" | ||||||
|     with Image.open("Tests/images/pil123p.png") as im: |     with Image.open("Tests/images/pil123p.png") as im: | ||||||
|         assert im.mode == "P" |         assert im.mode == "P" | ||||||
|         assert isinstance(im.info["transparency"], bytes) |         assert isinstance(im.info["transparency"], bytes) | ||||||
|  | @ -80,7 +80,7 @@ def test_monochrome(tmp_path: Path) -> None: | ||||||
| 
 | 
 | ||||||
| def test_unsupported_mode(tmp_path: Path) -> None: | def test_unsupported_mode(tmp_path: Path) -> None: | ||||||
|     im = hopper("PA") |     im = hopper("PA") | ||||||
|     outfile = str(tmp_path / "temp_PA.pdf") |     outfile = tmp_path / "temp_PA.pdf" | ||||||
| 
 | 
 | ||||||
|     with pytest.raises(ValueError): |     with pytest.raises(ValueError): | ||||||
|         im.save(outfile) |         im.save(outfile) | ||||||
|  | @ -89,7 +89,7 @@ def test_unsupported_mode(tmp_path: Path) -> None: | ||||||
| def test_resolution(tmp_path: Path) -> None: | def test_resolution(tmp_path: Path) -> None: | ||||||
|     im = hopper() |     im = hopper() | ||||||
| 
 | 
 | ||||||
|     outfile = str(tmp_path / "temp.pdf") |     outfile = tmp_path / "temp.pdf" | ||||||
|     im.save(outfile, resolution=150) |     im.save(outfile, resolution=150) | ||||||
| 
 | 
 | ||||||
|     with open(outfile, "rb") as fp: |     with open(outfile, "rb") as fp: | ||||||
|  | @ -117,7 +117,7 @@ def test_resolution(tmp_path: Path) -> None: | ||||||
| def test_dpi(params: dict[str, int | tuple[int, int]], tmp_path: Path) -> None: | def test_dpi(params: dict[str, int | tuple[int, int]], tmp_path: Path) -> None: | ||||||
|     im = hopper() |     im = hopper() | ||||||
| 
 | 
 | ||||||
|     outfile = str(tmp_path / "temp.pdf") |     outfile = tmp_path / "temp.pdf" | ||||||
|     im.save(outfile, "PDF", **params) |     im.save(outfile, "PDF", **params) | ||||||
| 
 | 
 | ||||||
|     with open(outfile, "rb") as fp: |     with open(outfile, "rb") as fp: | ||||||
|  | @ -144,7 +144,7 @@ def test_save_all(tmp_path: Path) -> None: | ||||||
| 
 | 
 | ||||||
|     # Multiframe image |     # Multiframe image | ||||||
|     with Image.open("Tests/images/dispose_bgnd.gif") as im: |     with Image.open("Tests/images/dispose_bgnd.gif") as im: | ||||||
|         outfile = str(tmp_path / "temp.pdf") |         outfile = tmp_path / "temp.pdf" | ||||||
|         im.save(outfile, save_all=True) |         im.save(outfile, save_all=True) | ||||||
| 
 | 
 | ||||||
|         assert os.path.isfile(outfile) |         assert os.path.isfile(outfile) | ||||||
|  | @ -177,7 +177,7 @@ def test_save_all(tmp_path: Path) -> None: | ||||||
| def test_multiframe_normal_save(tmp_path: Path) -> None: | def test_multiframe_normal_save(tmp_path: Path) -> None: | ||||||
|     # Test saving a multiframe image without save_all |     # Test saving a multiframe image without save_all | ||||||
|     with Image.open("Tests/images/dispose_bgnd.gif") as im: |     with Image.open("Tests/images/dispose_bgnd.gif") as im: | ||||||
|         outfile = str(tmp_path / "temp.pdf") |         outfile = tmp_path / "temp.pdf" | ||||||
|         im.save(outfile) |         im.save(outfile) | ||||||
| 
 | 
 | ||||||
|     assert os.path.isfile(outfile) |     assert os.path.isfile(outfile) | ||||||
|  |  | ||||||
|  | @ -68,7 +68,7 @@ def roundtrip(im: Image.Image, **options: Any) -> PngImagePlugin.PngImageFile: | ||||||
| 
 | 
 | ||||||
| @skip_unless_feature("zlib") | @skip_unless_feature("zlib") | ||||||
| class TestFilePng: | class TestFilePng: | ||||||
|     def get_chunks(self, filename: str) -> list[bytes]: |     def get_chunks(self, filename: Path) -> list[bytes]: | ||||||
|         chunks = [] |         chunks = [] | ||||||
|         with open(filename, "rb") as fp: |         with open(filename, "rb") as fp: | ||||||
|             fp.read(8) |             fp.read(8) | ||||||
|  | @ -89,7 +89,7 @@ class TestFilePng: | ||||||
|         assert version is not None |         assert version is not None | ||||||
|         assert re.search(r"\d+(\.\d+){1,3}(\.zlib\-ng)?$", version) |         assert re.search(r"\d+(\.\d+){1,3}(\.zlib\-ng)?$", version) | ||||||
| 
 | 
 | ||||||
|         test_file = str(tmp_path / "temp.png") |         test_file = tmp_path / "temp.png" | ||||||
| 
 | 
 | ||||||
|         hopper("RGB").save(test_file) |         hopper("RGB").save(test_file) | ||||||
| 
 | 
 | ||||||
|  | @ -250,7 +250,7 @@ class TestFilePng: | ||||||
|             # each palette entry |             # each palette entry | ||||||
|             assert len(im.info["transparency"]) == 256 |             assert len(im.info["transparency"]) == 256 | ||||||
| 
 | 
 | ||||||
|             test_file = str(tmp_path / "temp.png") |             test_file = tmp_path / "temp.png" | ||||||
|             im.save(test_file) |             im.save(test_file) | ||||||
| 
 | 
 | ||||||
|         # check if saved image contains same transparency |         # check if saved image contains same transparency | ||||||
|  | @ -271,7 +271,7 @@ class TestFilePng: | ||||||
|             assert im.info["transparency"] == 164 |             assert im.info["transparency"] == 164 | ||||||
|             assert im.getpixel((31, 31)) == 164 |             assert im.getpixel((31, 31)) == 164 | ||||||
| 
 | 
 | ||||||
|             test_file = str(tmp_path / "temp.png") |             test_file = tmp_path / "temp.png" | ||||||
|             im.save(test_file) |             im.save(test_file) | ||||||
| 
 | 
 | ||||||
|         # check if saved image contains same transparency |         # check if saved image contains same transparency | ||||||
|  | @ -294,7 +294,7 @@ class TestFilePng: | ||||||
|         assert im.getcolors() == [(100, (0, 0, 0, 0))] |         assert im.getcolors() == [(100, (0, 0, 0, 0))] | ||||||
| 
 | 
 | ||||||
|         im = im.convert("P") |         im = im.convert("P") | ||||||
|         test_file = str(tmp_path / "temp.png") |         test_file = tmp_path / "temp.png" | ||||||
|         im.save(test_file) |         im.save(test_file) | ||||||
| 
 | 
 | ||||||
|         # check if saved image contains same transparency |         # check if saved image contains same transparency | ||||||
|  | @ -315,7 +315,7 @@ class TestFilePng: | ||||||
|                 im_rgba = im.convert("RGBA") |                 im_rgba = im.convert("RGBA") | ||||||
|             assert im_rgba.getchannel("A").getcolors()[0][0] == num_transparent |             assert im_rgba.getchannel("A").getcolors()[0][0] == num_transparent | ||||||
| 
 | 
 | ||||||
|             test_file = str(tmp_path / "temp.png") |             test_file = tmp_path / "temp.png" | ||||||
|             im.save(test_file) |             im.save(test_file) | ||||||
| 
 | 
 | ||||||
|             with Image.open(test_file) as test_im: |             with Image.open(test_file) as test_im: | ||||||
|  | @ -329,7 +329,7 @@ class TestFilePng: | ||||||
|     def test_save_rgb_single_transparency(self, tmp_path: Path) -> None: |     def test_save_rgb_single_transparency(self, tmp_path: Path) -> None: | ||||||
|         in_file = "Tests/images/caption_6_33_22.png" |         in_file = "Tests/images/caption_6_33_22.png" | ||||||
|         with Image.open(in_file) as im: |         with Image.open(in_file) as im: | ||||||
|             test_file = str(tmp_path / "temp.png") |             test_file = tmp_path / "temp.png" | ||||||
|             im.save(test_file) |             im.save(test_file) | ||||||
| 
 | 
 | ||||||
|     def test_load_verify(self) -> None: |     def test_load_verify(self) -> None: | ||||||
|  | @ -488,7 +488,7 @@ class TestFilePng: | ||||||
|         im = hopper("P") |         im = hopper("P") | ||||||
|         im.info["transparency"] = 0 |         im.info["transparency"] = 0 | ||||||
| 
 | 
 | ||||||
|         f = str(tmp_path / "temp.png") |         f = tmp_path / "temp.png" | ||||||
|         im.save(f) |         im.save(f) | ||||||
| 
 | 
 | ||||||
|         with Image.open(f) as im2: |         with Image.open(f) as im2: | ||||||
|  | @ -549,7 +549,7 @@ class TestFilePng: | ||||||
| 
 | 
 | ||||||
|     def test_chunk_order(self, tmp_path: Path) -> None: |     def test_chunk_order(self, tmp_path: Path) -> None: | ||||||
|         with Image.open("Tests/images/icc_profile.png") as im: |         with Image.open("Tests/images/icc_profile.png") as im: | ||||||
|             test_file = str(tmp_path / "temp.png") |             test_file = tmp_path / "temp.png" | ||||||
|             im.convert("P").save(test_file, dpi=(100, 100)) |             im.convert("P").save(test_file, dpi=(100, 100)) | ||||||
| 
 | 
 | ||||||
|         chunks = self.get_chunks(test_file) |         chunks = self.get_chunks(test_file) | ||||||
|  | @ -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 | ||||||
|  | @ -661,7 +667,7 @@ class TestFilePng: | ||||||
|     def test_specify_bits(self, save_all: bool, tmp_path: Path) -> None: |     def test_specify_bits(self, save_all: bool, tmp_path: Path) -> None: | ||||||
|         im = hopper("P") |         im = hopper("P") | ||||||
| 
 | 
 | ||||||
|         out = str(tmp_path / "temp.png") |         out = tmp_path / "temp.png" | ||||||
|         im.save(out, bits=4, save_all=save_all) |         im.save(out, bits=4, save_all=save_all) | ||||||
| 
 | 
 | ||||||
|         with Image.open(out) as reloaded: |         with Image.open(out) as reloaded: | ||||||
|  | @ -671,8 +677,8 @@ class TestFilePng: | ||||||
|         im = Image.new("P", (1, 1)) |         im = Image.new("P", (1, 1)) | ||||||
|         im.putpalette((1, 1, 1)) |         im.putpalette((1, 1, 1)) | ||||||
| 
 | 
 | ||||||
|         out = str(tmp_path / "temp.png") |         out = tmp_path / "temp.png" | ||||||
|         im.save(str(tmp_path / "temp.png")) |         im.save(out) | ||||||
| 
 | 
 | ||||||
|         with Image.open(out) as reloaded: |         with Image.open(out) as reloaded: | ||||||
|             assert len(reloaded.png.im_palette[1]) == 3 |             assert len(reloaded.png.im_palette[1]) == 3 | ||||||
|  | @ -721,11 +727,12 @@ class TestFilePng: | ||||||
| 
 | 
 | ||||||
|     def test_exif_save(self, tmp_path: Path) -> None: |     def test_exif_save(self, tmp_path: Path) -> None: | ||||||
|         # Test exif is not saved from info |         # Test exif is not saved from info | ||||||
|         test_file = str(tmp_path / "temp.png") |         test_file = tmp_path / "temp.png" | ||||||
|         with Image.open("Tests/images/exif.png") as im: |         with Image.open("Tests/images/exif.png") as im: | ||||||
|             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 | ||||||
|  | @ -741,7 +748,7 @@ class TestFilePng: | ||||||
|     ) |     ) | ||||||
|     def test_exif_from_jpg(self, tmp_path: Path) -> None: |     def test_exif_from_jpg(self, tmp_path: Path) -> None: | ||||||
|         with Image.open("Tests/images/pil_sample_rgb.jpg") as im: |         with Image.open("Tests/images/pil_sample_rgb.jpg") as im: | ||||||
|             test_file = str(tmp_path / "temp.png") |             test_file = tmp_path / "temp.png" | ||||||
|             im.save(test_file, exif=im.getexif()) |             im.save(test_file, exif=im.getexif()) | ||||||
| 
 | 
 | ||||||
|         with Image.open(test_file) as reloaded: |         with Image.open(test_file) as reloaded: | ||||||
|  | @ -750,7 +757,7 @@ class TestFilePng: | ||||||
| 
 | 
 | ||||||
|     def test_exif_argument(self, tmp_path: Path) -> None: |     def test_exif_argument(self, tmp_path: Path) -> None: | ||||||
|         with Image.open(TEST_PNG_FILE) as im: |         with Image.open(TEST_PNG_FILE) as im: | ||||||
|             test_file = str(tmp_path / "temp.png") |             test_file = tmp_path / "temp.png" | ||||||
|             im.save(test_file, exif=b"exifstring") |             im.save(test_file, exif=b"exifstring") | ||||||
| 
 | 
 | ||||||
|         with Image.open(test_file) as reloaded: |         with Image.open(test_file) as reloaded: | ||||||
|  |  | ||||||
|  | @ -94,7 +94,7 @@ def test_16bit_pgm() -> None: | ||||||
| 
 | 
 | ||||||
| def test_16bit_pgm_write(tmp_path: Path) -> None: | def test_16bit_pgm_write(tmp_path: Path) -> None: | ||||||
|     with Image.open("Tests/images/16_bit_binary.pgm") as im: |     with Image.open("Tests/images/16_bit_binary.pgm") as im: | ||||||
|         filename = str(tmp_path / "temp.pgm") |         filename = tmp_path / "temp.pgm" | ||||||
|         im.save(filename, "PPM") |         im.save(filename, "PPM") | ||||||
|         assert_image_equal_tofile(im, filename) |         assert_image_equal_tofile(im, filename) | ||||||
| 
 | 
 | ||||||
|  | @ -106,7 +106,7 @@ def test_pnm(tmp_path: Path) -> None: | ||||||
|     with Image.open("Tests/images/hopper.pnm") as im: |     with Image.open("Tests/images/hopper.pnm") as im: | ||||||
|         assert_image_similar(im, hopper(), 0.0001) |         assert_image_similar(im, hopper(), 0.0001) | ||||||
| 
 | 
 | ||||||
|         filename = str(tmp_path / "temp.pnm") |         filename = tmp_path / "temp.pnm" | ||||||
|         im.save(filename) |         im.save(filename) | ||||||
| 
 | 
 | ||||||
|         assert_image_equal_tofile(im, filename) |         assert_image_equal_tofile(im, filename) | ||||||
|  | @ -117,7 +117,7 @@ def test_pfm(tmp_path: Path) -> None: | ||||||
|         assert im.info["scale"] == 1.0 |         assert im.info["scale"] == 1.0 | ||||||
|         assert_image_equal(im, hopper("F")) |         assert_image_equal(im, hopper("F")) | ||||||
| 
 | 
 | ||||||
|         filename = str(tmp_path / "tmp.pfm") |         filename = tmp_path / "tmp.pfm" | ||||||
|         im.save(filename) |         im.save(filename) | ||||||
| 
 | 
 | ||||||
|         assert_image_equal_tofile(im, filename) |         assert_image_equal_tofile(im, filename) | ||||||
|  | @ -128,7 +128,7 @@ def test_pfm_big_endian(tmp_path: Path) -> None: | ||||||
|         assert im.info["scale"] == 2.5 |         assert im.info["scale"] == 2.5 | ||||||
|         assert_image_equal(im, hopper("F")) |         assert_image_equal(im, hopper("F")) | ||||||
| 
 | 
 | ||||||
|         filename = str(tmp_path / "tmp.pfm") |         filename = tmp_path / "tmp.pfm" | ||||||
|         im.save(filename) |         im.save(filename) | ||||||
| 
 | 
 | ||||||
|         assert_image_equal_tofile(im, filename) |         assert_image_equal_tofile(im, filename) | ||||||
|  | @ -194,8 +194,8 @@ def test_16bit_plain_pgm() -> None: | ||||||
| def test_plain_data_with_comment( | def test_plain_data_with_comment( | ||||||
|     tmp_path: Path, header: bytes, data: bytes, comment_count: int |     tmp_path: Path, header: bytes, data: bytes, comment_count: int | ||||||
| ) -> None: | ) -> None: | ||||||
|     path1 = str(tmp_path / "temp1.ppm") |     path1 = tmp_path / "temp1.ppm" | ||||||
|     path2 = str(tmp_path / "temp2.ppm") |     path2 = tmp_path / "temp2.ppm" | ||||||
|     comment = b"# comment" * comment_count |     comment = b"# comment" * comment_count | ||||||
|     with open(path1, "wb") as f1, open(path2, "wb") as f2: |     with open(path1, "wb") as f1, open(path2, "wb") as f2: | ||||||
|         f1.write(header + b"\n\n" + data) |         f1.write(header + b"\n\n" + data) | ||||||
|  | @ -207,7 +207,7 @@ def test_plain_data_with_comment( | ||||||
| 
 | 
 | ||||||
| @pytest.mark.parametrize("data", (b"P1\n128 128\n", b"P3\n128 128\n255\n")) | @pytest.mark.parametrize("data", (b"P1\n128 128\n", b"P3\n128 128\n255\n")) | ||||||
| def test_plain_truncated_data(tmp_path: Path, data: bytes) -> None: | def test_plain_truncated_data(tmp_path: Path, data: bytes) -> None: | ||||||
|     path = str(tmp_path / "temp.ppm") |     path = tmp_path / "temp.ppm" | ||||||
|     with open(path, "wb") as f: |     with open(path, "wb") as f: | ||||||
|         f.write(data) |         f.write(data) | ||||||
| 
 | 
 | ||||||
|  | @ -218,7 +218,7 @@ def test_plain_truncated_data(tmp_path: Path, data: bytes) -> None: | ||||||
| 
 | 
 | ||||||
| @pytest.mark.parametrize("data", (b"P1\n128 128\n1009", b"P3\n128 128\n255\n100A")) | @pytest.mark.parametrize("data", (b"P1\n128 128\n1009", b"P3\n128 128\n255\n100A")) | ||||||
| def test_plain_invalid_data(tmp_path: Path, data: bytes) -> None: | def test_plain_invalid_data(tmp_path: Path, data: bytes) -> None: | ||||||
|     path = str(tmp_path / "temp.ppm") |     path = tmp_path / "temp.ppm" | ||||||
|     with open(path, "wb") as f: |     with open(path, "wb") as f: | ||||||
|         f.write(data) |         f.write(data) | ||||||
| 
 | 
 | ||||||
|  | @ -235,7 +235,7 @@ def test_plain_invalid_data(tmp_path: Path, data: bytes) -> None: | ||||||
|     ), |     ), | ||||||
| ) | ) | ||||||
| def test_plain_ppm_token_too_long(tmp_path: Path, data: bytes) -> None: | def test_plain_ppm_token_too_long(tmp_path: Path, data: bytes) -> None: | ||||||
|     path = str(tmp_path / "temp.ppm") |     path = tmp_path / "temp.ppm" | ||||||
|     with open(path, "wb") as f: |     with open(path, "wb") as f: | ||||||
|         f.write(data) |         f.write(data) | ||||||
| 
 | 
 | ||||||
|  | @ -245,7 +245,7 @@ def test_plain_ppm_token_too_long(tmp_path: Path, data: bytes) -> None: | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def test_plain_ppm_value_negative(tmp_path: Path) -> None: | def test_plain_ppm_value_negative(tmp_path: Path) -> None: | ||||||
|     path = str(tmp_path / "temp.ppm") |     path = tmp_path / "temp.ppm" | ||||||
|     with open(path, "wb") as f: |     with open(path, "wb") as f: | ||||||
|         f.write(b"P3\n128 128\n255\n-1") |         f.write(b"P3\n128 128\n255\n-1") | ||||||
| 
 | 
 | ||||||
|  | @ -255,7 +255,7 @@ def test_plain_ppm_value_negative(tmp_path: Path) -> None: | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def test_plain_ppm_value_too_large(tmp_path: Path) -> None: | def test_plain_ppm_value_too_large(tmp_path: Path) -> None: | ||||||
|     path = str(tmp_path / "temp.ppm") |     path = tmp_path / "temp.ppm" | ||||||
|     with open(path, "wb") as f: |     with open(path, "wb") as f: | ||||||
|         f.write(b"P3\n128 128\n255\n256") |         f.write(b"P3\n128 128\n255\n256") | ||||||
| 
 | 
 | ||||||
|  | @ -270,7 +270,7 @@ def test_magic() -> None: | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def test_header_with_comments(tmp_path: Path) -> None: | def test_header_with_comments(tmp_path: Path) -> None: | ||||||
|     path = str(tmp_path / "temp.ppm") |     path = tmp_path / "temp.ppm" | ||||||
|     with open(path, "wb") as f: |     with open(path, "wb") as f: | ||||||
|         f.write(b"P6 #comment\n#comment\r12#comment\r8\n128 #comment\n255\n") |         f.write(b"P6 #comment\n#comment\r12#comment\r8\n128 #comment\n255\n") | ||||||
| 
 | 
 | ||||||
|  | @ -279,7 +279,7 @@ def test_header_with_comments(tmp_path: Path) -> None: | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def test_non_integer_token(tmp_path: Path) -> None: | def test_non_integer_token(tmp_path: Path) -> None: | ||||||
|     path = str(tmp_path / "temp.ppm") |     path = tmp_path / "temp.ppm" | ||||||
|     with open(path, "wb") as f: |     with open(path, "wb") as f: | ||||||
|         f.write(b"P6\nTEST") |         f.write(b"P6\nTEST") | ||||||
| 
 | 
 | ||||||
|  | @ -289,29 +289,25 @@ def test_non_integer_token(tmp_path: Path) -> None: | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def test_header_token_too_long(tmp_path: Path) -> None: | def test_header_token_too_long(tmp_path: Path) -> None: | ||||||
|     path = str(tmp_path / "temp.ppm") |     path = tmp_path / "temp.ppm" | ||||||
|     with open(path, "wb") as f: |     with open(path, "wb") as f: | ||||||
|         f.write(b"P6\n 01234567890") |         f.write(b"P6\n 01234567890") | ||||||
| 
 | 
 | ||||||
|     with pytest.raises(ValueError) as e: |     with pytest.raises(ValueError, match="Token too long in file header: 01234567890"): | ||||||
|         with Image.open(path): |         with Image.open(path): | ||||||
|             pass |             pass | ||||||
| 
 | 
 | ||||||
|     assert str(e.value) == "Token too long in file header: 01234567890" |  | ||||||
| 
 |  | ||||||
| 
 | 
 | ||||||
| def test_truncated_file(tmp_path: Path) -> None: | def test_truncated_file(tmp_path: Path) -> None: | ||||||
|     # Test EOF in header |     # Test EOF in header | ||||||
|     path = str(tmp_path / "temp.pgm") |     path = tmp_path / "temp.pgm" | ||||||
|     with open(path, "wb") as f: |     with open(path, "wb") as f: | ||||||
|         f.write(b"P6") |         f.write(b"P6") | ||||||
| 
 | 
 | ||||||
|     with pytest.raises(ValueError) as e: |     with pytest.raises(ValueError, match="Reached EOF while reading header"): | ||||||
|         with Image.open(path): |         with Image.open(path): | ||||||
|             pass |             pass | ||||||
| 
 | 
 | ||||||
|     assert str(e.value) == "Reached EOF while reading header" |  | ||||||
| 
 |  | ||||||
|     # Test EOF for PyDecoder |     # Test EOF for PyDecoder | ||||||
|     fp = BytesIO(b"P5 3 1 4") |     fp = BytesIO(b"P5 3 1 4") | ||||||
|     with Image.open(fp) as im: |     with Image.open(fp) as im: | ||||||
|  | @ -320,7 +316,7 @@ def test_truncated_file(tmp_path: Path) -> None: | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def test_not_enough_image_data(tmp_path: Path) -> None: | def test_not_enough_image_data(tmp_path: Path) -> None: | ||||||
|     path = str(tmp_path / "temp.ppm") |     path = tmp_path / "temp.ppm" | ||||||
|     with open(path, "wb") as f: |     with open(path, "wb") as f: | ||||||
|         f.write(b"P2 1 2 255 255") |         f.write(b"P2 1 2 255 255") | ||||||
| 
 | 
 | ||||||
|  | @ -331,16 +327,16 @@ def test_not_enough_image_data(tmp_path: Path) -> None: | ||||||
| 
 | 
 | ||||||
| @pytest.mark.parametrize("maxval", (b"0", b"65536")) | @pytest.mark.parametrize("maxval", (b"0", b"65536")) | ||||||
| def test_invalid_maxval(maxval: bytes, tmp_path: Path) -> None: | def test_invalid_maxval(maxval: bytes, tmp_path: Path) -> None: | ||||||
|     path = str(tmp_path / "temp.ppm") |     path = tmp_path / "temp.ppm" | ||||||
|     with open(path, "wb") as f: |     with open(path, "wb") as f: | ||||||
|         f.write(b"P6\n3 1 " + maxval) |         f.write(b"P6\n3 1 " + maxval) | ||||||
| 
 | 
 | ||||||
|     with pytest.raises(ValueError) as e: |     with pytest.raises( | ||||||
|  |         ValueError, match="maxval must be greater than 0 and less than 65536" | ||||||
|  |     ): | ||||||
|         with Image.open(path): |         with Image.open(path): | ||||||
|             pass |             pass | ||||||
| 
 | 
 | ||||||
|     assert str(e.value) == "maxval must be greater than 0 and less than 65536" |  | ||||||
| 
 |  | ||||||
| 
 | 
 | ||||||
| def test_neg_ppm() -> None: | def test_neg_ppm() -> None: | ||||||
|     # Storage.c accepted negative values for xsize, ysize.  the |     # Storage.c accepted negative values for xsize, ysize.  the | ||||||
|  | @ -354,7 +350,7 @@ def test_neg_ppm() -> None: | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def test_mimetypes(tmp_path: Path) -> None: | def test_mimetypes(tmp_path: Path) -> None: | ||||||
|     path = str(tmp_path / "temp.pgm") |     path = tmp_path / "temp.pgm" | ||||||
| 
 | 
 | ||||||
|     with open(path, "wb") as f: |     with open(path, "wb") as f: | ||||||
|         f.write(b"P4\n128 128\n255") |         f.write(b"P4\n128 128\n255") | ||||||
|  |  | ||||||
|  | @ -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,31 +71,33 @@ 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 = str(tmp_path / "temp.sgi") |  | ||||||
|     img.save(out, format="sgi") |     img.save(out, format="sgi") | ||||||
|     assert_image_equal_tofile(img, out) |     assert_image_equal_tofile(img, out) | ||||||
| 
 | 
 | ||||||
|         out = str(tmp_path / "fp.sgi") |     out = tmp_path / "fp.sgi" | ||||||
|     with open(out, "wb") as fp: |     with open(out, "wb") as fp: | ||||||
|         img.save(fp) |         img.save(fp) | ||||||
|         assert_image_equal_tofile(img, out) |         assert_image_equal_tofile(img, out) | ||||||
| 
 | 
 | ||||||
|         assert not fp.closed |         assert not fp.closed | ||||||
| 
 | 
 | ||||||
|     for mode in ("L", "RGB", "RGBA"): |  | ||||||
|         roundtrip(hopper(mode)) |  | ||||||
| 
 | 
 | ||||||
|     # Test 1 dimension for an L mode image | @pytest.mark.parametrize("mode", ("L", "RGB", "RGBA")) | ||||||
|     roundtrip(Image.new("L", (10, 1))) | def test_write(mode: str, tmp_path: Path) -> None: | ||||||
|  |     roundtrip(hopper(mode), tmp_path) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def test_write_L_mode_1_dimension(tmp_path: Path) -> None: | ||||||
|  |     roundtrip(Image.new("L", (10, 1)), tmp_path) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def test_write16(tmp_path: Path) -> None: | def test_write16(tmp_path: Path) -> None: | ||||||
|     test_file = "Tests/images/hopper16.rgb" |     test_file = "Tests/images/hopper16.rgb" | ||||||
| 
 | 
 | ||||||
|     with Image.open(test_file) as im: |     with Image.open(test_file) as im: | ||||||
|         out = str(tmp_path / "temp.sgi") |         out = tmp_path / "temp.sgi" | ||||||
|         im.save(out, format="sgi", bpc=2) |         im.save(out, format="sgi", bpc=2) | ||||||
| 
 | 
 | ||||||
|         assert_image_equal_tofile(im, out) |         assert_image_equal_tofile(im, out) | ||||||
|  | @ -103,7 +105,7 @@ def test_write16(tmp_path: Path) -> None: | ||||||
| 
 | 
 | ||||||
| def test_unsupported_mode(tmp_path: Path) -> None: | def test_unsupported_mode(tmp_path: Path) -> None: | ||||||
|     im = hopper("LA") |     im = hopper("LA") | ||||||
|     out = str(tmp_path / "temp.sgi") |     out = tmp_path / "temp.sgi" | ||||||
| 
 | 
 | ||||||
|     with pytest.raises(ValueError): |     with pytest.raises(ValueError): | ||||||
|         im.save(out, format="sgi") |         im.save(out, format="sgi") | ||||||
|  |  | ||||||
|  | @ -51,7 +51,7 @@ def test_context_manager() -> None: | ||||||
| 
 | 
 | ||||||
| def test_save(tmp_path: Path) -> None: | def test_save(tmp_path: Path) -> None: | ||||||
|     # Arrange |     # Arrange | ||||||
|     temp = str(tmp_path / "temp.spider") |     temp = tmp_path / "temp.spider" | ||||||
|     im = hopper() |     im = hopper() | ||||||
| 
 | 
 | ||||||
|     # Act |     # Act | ||||||
|  | @ -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,16 +13,29 @@ _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 = str(tmp_path / "temp.tga") |         out = tmp_path / "temp.tga" | ||||||
| 
 | 
 | ||||||
|         original_im.save(out, rle=rle) |         original_im.save(out, rle=rle) | ||||||
|         with Image.open(out) as saved_im: |         with Image.open(out) as saved_im: | ||||||
|  | @ -36,27 +47,20 @@ 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") | ||||||
|     for png_path in png_paths: |  | ||||||
|     with Image.open(png_path) as reference_im: |     with Image.open(png_path) as reference_im: | ||||||
|         assert reference_im.mode == mode |         assert reference_im.mode == mode | ||||||
| 
 | 
 | ||||||
|         path_no_ext = os.path.splitext(png_path)[0] |         path_no_ext = os.path.splitext(png_path)[0] | ||||||
|             for origin, rle in product(_ORIGINS, (True, False)): |         tga_path = "{}_{}_{}.tga".format(path_no_ext, origin, "rle" if rle else "raw") | ||||||
|                 tga_path = "{}_{}_{}.tga".format( |  | ||||||
|                     path_no_ext, origin, "rle" if rle else "raw" |  | ||||||
|                 ) |  | ||||||
| 
 | 
 | ||||||
|         with Image.open(tga_path) as original_im: |         with Image.open(tga_path) as original_im: | ||||||
|             assert original_im.format == "TGA" |             assert original_im.format == "TGA" | ||||||
|             assert original_im.get_format_mimetype() == "image/x-tga" |             assert original_im.get_format_mimetype() == "image/x-tga" | ||||||
|             if rle: |             if rle: | ||||||
|                 assert original_im.info["compression"] == "tga_rle" |                 assert original_im.info["compression"] == "tga_rle" | ||||||
|                     assert ( |             assert original_im.info["orientation"] == _ORIGIN_TO_ORIENTATION[origin] | ||||||
|                         original_im.info["orientation"] |  | ||||||
|                         == _ORIGIN_TO_ORIENTATION[origin] |  | ||||||
|                     ) |  | ||||||
|             if mode == "P": |             if mode == "P": | ||||||
|                 assert original_im.getpalette() == reference_im.getpalette() |                 assert original_im.getpalette() == reference_im.getpalette() | ||||||
| 
 | 
 | ||||||
|  | @ -65,7 +69,7 @@ def test_sanity(mode: str, tmp_path: Path) -> None: | ||||||
|             roundtrip(original_im) |             roundtrip(original_im) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def test_palette_depth_8(tmp_path: Path) -> None: | def test_palette_depth_8() -> None: | ||||||
|     with pytest.raises(UnidentifiedImageError): |     with pytest.raises(UnidentifiedImageError): | ||||||
|         Image.open("Tests/images/p_8.tga") |         Image.open("Tests/images/p_8.tga") | ||||||
| 
 | 
 | ||||||
|  | @ -76,7 +80,7 @@ def test_palette_depth_16(tmp_path: Path) -> None: | ||||||
|         assert im.palette.mode == "RGBA" |         assert im.palette.mode == "RGBA" | ||||||
|         assert_image_equal_tofile(im.convert("RGBA"), "Tests/images/p_16.png") |         assert_image_equal_tofile(im.convert("RGBA"), "Tests/images/p_16.png") | ||||||
| 
 | 
 | ||||||
|         out = str(tmp_path / "temp.png") |         out = tmp_path / "temp.png" | ||||||
|         im.save(out) |         im.save(out) | ||||||
|         with Image.open(out) as reloaded: |         with Image.open(out) as reloaded: | ||||||
|             assert_image_equal_tofile(reloaded.convert("RGBA"), "Tests/images/p_16.png") |             assert_image_equal_tofile(reloaded.convert("RGBA"), "Tests/images/p_16.png") | ||||||
|  | @ -122,7 +126,7 @@ def test_cross_scan_line() -> None: | ||||||
| def test_save(tmp_path: Path) -> None: | def test_save(tmp_path: Path) -> None: | ||||||
|     test_file = "Tests/images/tga_id_field.tga" |     test_file = "Tests/images/tga_id_field.tga" | ||||||
|     with Image.open(test_file) as im: |     with Image.open(test_file) as im: | ||||||
|         out = str(tmp_path / "temp.tga") |         out = tmp_path / "temp.tga" | ||||||
| 
 | 
 | ||||||
|         # Save |         # Save | ||||||
|         im.save(out) |         im.save(out) | ||||||
|  | @ -141,7 +145,7 @@ def test_small_palette(tmp_path: Path) -> None: | ||||||
|     colors = [0, 0, 0] |     colors = [0, 0, 0] | ||||||
|     im.putpalette(colors) |     im.putpalette(colors) | ||||||
| 
 | 
 | ||||||
|     out = str(tmp_path / "temp.tga") |     out = tmp_path / "temp.tga" | ||||||
|     im.save(out) |     im.save(out) | ||||||
| 
 | 
 | ||||||
|     with Image.open(out) as reloaded: |     with Image.open(out) as reloaded: | ||||||
|  | @ -155,7 +159,7 @@ def test_missing_palette() -> None: | ||||||
| 
 | 
 | ||||||
| def test_save_wrong_mode(tmp_path: Path) -> None: | def test_save_wrong_mode(tmp_path: Path) -> None: | ||||||
|     im = hopper("PA") |     im = hopper("PA") | ||||||
|     out = str(tmp_path / "temp.tga") |     out = tmp_path / "temp.tga" | ||||||
| 
 | 
 | ||||||
|     with pytest.raises(OSError): |     with pytest.raises(OSError): | ||||||
|         im.save(out) |         im.save(out) | ||||||
|  | @ -172,7 +176,7 @@ def test_save_mapdepth() -> None: | ||||||
| def test_save_id_section(tmp_path: Path) -> None: | def test_save_id_section(tmp_path: Path) -> None: | ||||||
|     test_file = "Tests/images/rgb32rle.tga" |     test_file = "Tests/images/rgb32rle.tga" | ||||||
|     with Image.open(test_file) as im: |     with Image.open(test_file) as im: | ||||||
|         out = str(tmp_path / "temp.tga") |         out = tmp_path / "temp.tga" | ||||||
| 
 | 
 | ||||||
|         # Check there is no id section |         # Check there is no id section | ||||||
|         im.save(out) |         im.save(out) | ||||||
|  | @ -202,7 +206,7 @@ def test_save_id_section(tmp_path: Path) -> None: | ||||||
| 
 | 
 | ||||||
| def test_save_orientation(tmp_path: Path) -> None: | def test_save_orientation(tmp_path: Path) -> None: | ||||||
|     test_file = "Tests/images/rgb32rle.tga" |     test_file = "Tests/images/rgb32rle.tga" | ||||||
|     out = str(tmp_path / "temp.tga") |     out = tmp_path / "temp.tga" | ||||||
|     with Image.open(test_file) as im: |     with Image.open(test_file) as im: | ||||||
|         assert im.info["orientation"] == -1 |         assert im.info["orientation"] == -1 | ||||||
| 
 | 
 | ||||||
|  | @ -229,7 +233,7 @@ def test_save_rle(tmp_path: Path) -> None: | ||||||
|     with Image.open(test_file) as im: |     with Image.open(test_file) as im: | ||||||
|         assert im.info["compression"] == "tga_rle" |         assert im.info["compression"] == "tga_rle" | ||||||
| 
 | 
 | ||||||
|         out = str(tmp_path / "temp.tga") |         out = tmp_path / "temp.tga" | ||||||
| 
 | 
 | ||||||
|         # Save |         # Save | ||||||
|         im.save(out) |         im.save(out) | ||||||
|  | @ -266,7 +270,7 @@ def test_save_l_transparency(tmp_path: Path) -> None: | ||||||
|         assert im.mode == "LA" |         assert im.mode == "LA" | ||||||
|         assert im.getchannel("A").getcolors()[0][0] == num_transparent |         assert im.getchannel("A").getcolors()[0][0] == num_transparent | ||||||
| 
 | 
 | ||||||
|         out = str(tmp_path / "temp.tga") |         out = tmp_path / "temp.tga" | ||||||
|         im.save(out) |         im.save(out) | ||||||
| 
 | 
 | ||||||
|     with Image.open(out) as test_im: |     with Image.open(out) as test_im: | ||||||
|  |  | ||||||
|  | @ -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 ( | ||||||
|  | @ -31,7 +37,7 @@ except ImportError: | ||||||
| 
 | 
 | ||||||
| class TestFileTiff: | class TestFileTiff: | ||||||
|     def test_sanity(self, tmp_path: Path) -> None: |     def test_sanity(self, tmp_path: Path) -> None: | ||||||
|         filename = str(tmp_path / "temp.tif") |         filename = tmp_path / "temp.tif" | ||||||
| 
 | 
 | ||||||
|         hopper("RGB").save(filename) |         hopper("RGB").save(filename) | ||||||
| 
 | 
 | ||||||
|  | @ -112,20 +118,23 @@ class TestFileTiff: | ||||||
|             assert_image_equal_tofile(im, "Tests/images/hopper.tif") |             assert_image_equal_tofile(im, "Tests/images/hopper.tif") | ||||||
| 
 | 
 | ||||||
|         with Image.open("Tests/images/hopper_bigtiff.tif") as im: |         with Image.open("Tests/images/hopper_bigtiff.tif") as im: | ||||||
|             outfile = str(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: | ||||||
|         outfile = str(tmp_path / "temp.tif") |         outfile = tmp_path / "temp.tif" | ||||||
|         im = hopper() |         im = hopper() | ||||||
|         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: | ||||||
|  | @ -134,13 +143,14 @@ class TestFileTiff: | ||||||
| 
 | 
 | ||||||
|     def test_set_legacy_api(self) -> None: |     def test_set_legacy_api(self) -> None: | ||||||
|         ifd = TiffImagePlugin.ImageFileDirectory_v2() |         ifd = TiffImagePlugin.ImageFileDirectory_v2() | ||||||
|         with pytest.raises(Exception) as e: |         with pytest.raises(Exception, match="Not allowing setting of legacy api"): | ||||||
|             ifd.legacy_api = False |             ifd.legacy_api = False | ||||||
|         assert str(e.value) == "Not allowing setting of legacy api" |  | ||||||
| 
 | 
 | ||||||
|     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) | ||||||
|  | @ -154,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) | ||||||
|  | @ -168,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 | ||||||
|  | @ -182,11 +196,12 @@ 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) | ||||||
| 
 | 
 | ||||||
|     def test_save_float_dpi(self, tmp_path: Path) -> None: |     def test_save_float_dpi(self, tmp_path: Path) -> None: | ||||||
|         outfile = str(tmp_path / "temp.tif") |         outfile = tmp_path / "temp.tif" | ||||||
|         with Image.open("Tests/images/hopper.tif") as im: |         with Image.open("Tests/images/hopper.tif") as im: | ||||||
|             dpi = (72.2, 72.2) |             dpi = (72.2, 72.2) | ||||||
|             im.save(outfile, dpi=dpi) |             im.save(outfile, dpi=dpi) | ||||||
|  | @ -199,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 | ||||||
| 
 | 
 | ||||||
|  | @ -214,19 +230,21 @@ 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") | ||||||
|         outfile = str(tmp_path / "temp.tif") |         outfile = tmp_path / "temp.tif" | ||||||
|         im.save(outfile) |         im.save(outfile) | ||||||
| 
 | 
 | ||||||
|     def test_save_unsupported_mode(self, tmp_path: Path) -> None: |     def test_save_unsupported_mode(self, tmp_path: Path) -> None: | ||||||
|         im = hopper("HSV") |         im = hopper("HSV") | ||||||
|         outfile = str(tmp_path / "temp.tif") |         outfile = tmp_path / "temp.tif" | ||||||
|         with pytest.raises(OSError): |         with pytest.raises(OSError): | ||||||
|             im.save(outfile) |             im.save(outfile) | ||||||
| 
 | 
 | ||||||
|  | @ -308,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 | ||||||
|  | @ -356,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) | ||||||
| 
 | 
 | ||||||
|  | @ -379,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, | ||||||
|  | @ -418,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)) | ||||||
|  | @ -450,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: | ||||||
|  | @ -486,14 +515,14 @@ class TestFileTiff: | ||||||
|             assert gps[0] == b"\x03\x02\x00\x00" |             assert gps[0] == b"\x03\x02\x00\x00" | ||||||
|             assert gps[18] == "WGS-84" |             assert gps[18] == "WGS-84" | ||||||
| 
 | 
 | ||||||
|         outfile = str(tmp_path / "temp.tif") |         outfile = tmp_path / "temp.tif" | ||||||
|         with Image.open("Tests/images/ifd_tag_type.tiff") as im: |         with Image.open("Tests/images/ifd_tag_type.tiff") as im: | ||||||
|             exif = im.getexif() |             exif = im.getexif() | ||||||
|             check_exif(exif) |             check_exif(exif) | ||||||
| 
 | 
 | ||||||
|             im.save(outfile, exif=exif) |             im.save(outfile, exif=exif) | ||||||
| 
 | 
 | ||||||
|         outfile2 = str(tmp_path / "temp2.tif") |         outfile2 = tmp_path / "temp2.tif" | ||||||
|         with Image.open(outfile) as im: |         with Image.open(outfile) as im: | ||||||
|             exif = im.getexif() |             exif = im.getexif() | ||||||
|             check_exif(exif) |             check_exif(exif) | ||||||
|  | @ -505,7 +534,7 @@ class TestFileTiff: | ||||||
|             check_exif(exif) |             check_exif(exif) | ||||||
| 
 | 
 | ||||||
|     def test_modify_exif(self, tmp_path: Path) -> None: |     def test_modify_exif(self, tmp_path: Path) -> None: | ||||||
|         outfile = str(tmp_path / "temp.tif") |         outfile = tmp_path / "temp.tif" | ||||||
|         with Image.open("Tests/images/ifd_tag_type.tiff") as im: |         with Image.open("Tests/images/ifd_tag_type.tiff") as im: | ||||||
|             exif = im.getexif() |             exif = im.getexif() | ||||||
|             exif[264] = 100 |             exif[264] = 100 | ||||||
|  | @ -534,10 +563,11 @@ class TestFileTiff: | ||||||
| 
 | 
 | ||||||
|     @pytest.mark.parametrize("mode", ("1", "L")) |     @pytest.mark.parametrize("mode", ("1", "L")) | ||||||
|     def test_photometric(self, mode: str, tmp_path: Path) -> None: |     def test_photometric(self, mode: str, tmp_path: Path) -> None: | ||||||
|         filename = str(tmp_path / "temp.tif") |         filename = tmp_path / "temp.tif" | ||||||
|         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) | ||||||
| 
 | 
 | ||||||
|  | @ -613,9 +643,11 @@ class TestFileTiff: | ||||||
| 
 | 
 | ||||||
|     def test_with_underscores(self, tmp_path: Path) -> None: |     def test_with_underscores(self, tmp_path: Path) -> None: | ||||||
|         kwargs = {"resolution_unit": "inch", "x_resolution": 72, "y_resolution": 36} |         kwargs = {"resolution_unit": "inch", "x_resolution": 72, "y_resolution": 36} | ||||||
|         filename = str(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 | ||||||
|  | @ -631,14 +663,14 @@ class TestFileTiff: | ||||||
|         with Image.open(infile) as im: |         with Image.open(infile) as im: | ||||||
|             assert im.getpixel((0, 0)) == pixel_value |             assert im.getpixel((0, 0)) == pixel_value | ||||||
| 
 | 
 | ||||||
|             tmpfile = str(tmp_path / "temp.tif") |             tmpfile = tmp_path / "temp.tif" | ||||||
|             im.save(tmpfile) |             im.save(tmpfile) | ||||||
| 
 | 
 | ||||||
|             assert_image_equal_tofile(im, tmpfile) |             assert_image_equal_tofile(im, tmpfile) | ||||||
| 
 | 
 | ||||||
|     def test_iptc(self, tmp_path: Path) -> None: |     def test_iptc(self, tmp_path: Path) -> None: | ||||||
|         # Do not preserve IPTC_NAA_CHUNK by default if type is LONG |         # Do not preserve IPTC_NAA_CHUNK by default if type is LONG | ||||||
|         outfile = str(tmp_path / "temp.tif") |         outfile = tmp_path / "temp.tif" | ||||||
|         with Image.open("Tests/images/hopper.tif") as im: |         with Image.open("Tests/images/hopper.tif") as im: | ||||||
|             im.load() |             im.load() | ||||||
|             assert isinstance(im, TiffImagePlugin.TiffImageFile) |             assert isinstance(im, TiffImagePlugin.TiffImageFile) | ||||||
|  | @ -653,7 +685,7 @@ class TestFileTiff: | ||||||
|             assert 33723 not in im.tag_v2 |             assert 33723 not in im.tag_v2 | ||||||
| 
 | 
 | ||||||
|     def test_rowsperstrip(self, tmp_path: Path) -> None: |     def test_rowsperstrip(self, tmp_path: Path) -> None: | ||||||
|         outfile = str(tmp_path / "temp.tif") |         outfile = tmp_path / "temp.tif" | ||||||
|         im = hopper() |         im = hopper() | ||||||
|         im.save(outfile, tiffinfo={278: 256}) |         im.save(outfile, tiffinfo={278: 256}) | ||||||
| 
 | 
 | ||||||
|  | @ -661,6 +693,18 @@ class TestFileTiff: | ||||||
|             assert isinstance(im, TiffImagePlugin.TiffImageFile) |             assert isinstance(im, TiffImagePlugin.TiffImageFile) | ||||||
|             assert im.tag_v2[278] == 256 |             assert im.tag_v2[278] == 256 | ||||||
| 
 | 
 | ||||||
|  |         im = hopper() | ||||||
|  |         im2 = Image.new("L", (128, 128)) | ||||||
|  |         im2.encoderinfo = {"tiffinfo": {278: 256}} | ||||||
|  |         im.save(outfile, save_all=True, append_images=[im2]) | ||||||
|  | 
 | ||||||
|  |         with Image.open(outfile) as im: | ||||||
|  |             assert isinstance(im, TiffImagePlugin.TiffImageFile) | ||||||
|  |             assert im.tag_v2[278] == 128 | ||||||
|  | 
 | ||||||
|  |             im.seek(1) | ||||||
|  |             assert im.tag_v2[278] == 256 | ||||||
|  | 
 | ||||||
|     def test_strip_raw(self) -> None: |     def test_strip_raw(self) -> None: | ||||||
|         infile = "Tests/images/tiff_strip_raw.tif" |         infile = "Tests/images/tiff_strip_raw.tif" | ||||||
|         with Image.open(infile) as im: |         with Image.open(infile) as im: | ||||||
|  | @ -690,9 +734,10 @@ 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 = str(tmp_path / "temp.tif") |             outfile = tmp_path / "temp.tif" | ||||||
|             im.save(outfile) |             im.save(outfile) | ||||||
| 
 | 
 | ||||||
|             with Image.open(outfile) as reloaded: |             with Image.open(outfile) as reloaded: | ||||||
|  | @ -707,7 +752,7 @@ class TestFileTiff: | ||||||
| 
 | 
 | ||||||
|     @pytest.mark.parametrize("mode", ("P", "PA")) |     @pytest.mark.parametrize("mode", ("P", "PA")) | ||||||
|     def test_palette(self, mode: str, tmp_path: Path) -> None: |     def test_palette(self, mode: str, tmp_path: Path) -> None: | ||||||
|         outfile = str(tmp_path / "temp.tif") |         outfile = tmp_path / "temp.tif" | ||||||
| 
 | 
 | ||||||
|         im = hopper(mode) |         im = hopper(mode) | ||||||
|         im.save(outfile) |         im.save(outfile) | ||||||
|  | @ -722,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 | ||||||
|  | @ -732,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 | ||||||
|  | @ -743,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_fixoffsets(self) -> None: |     def test_fixoffsets(self) -> None: | ||||||
|  | @ -801,7 +849,7 @@ class TestFileTiff: | ||||||
|         im.info["icc_profile"] = "Dummy value" |         im.info["icc_profile"] = "Dummy value" | ||||||
| 
 | 
 | ||||||
|         # Try save-load round trip to make sure both handle icc_profile. |         # Try save-load round trip to make sure both handle icc_profile. | ||||||
|         tmpfile = str(tmp_path / "temp.tif") |         tmpfile = tmp_path / "temp.tif" | ||||||
|         im.save(tmpfile, "TIFF", compression="raw") |         im.save(tmpfile, "TIFF", compression="raw") | ||||||
|         with Image.open(tmpfile) as reloaded: |         with Image.open(tmpfile) as reloaded: | ||||||
|             assert b"Dummy value" == reloaded.info["icc_profile"] |             assert b"Dummy value" == reloaded.info["icc_profile"] | ||||||
|  | @ -810,7 +858,7 @@ class TestFileTiff: | ||||||
|         im = hopper() |         im = hopper() | ||||||
|         assert "icc_profile" not in im.info |         assert "icc_profile" not in im.info | ||||||
| 
 | 
 | ||||||
|         outfile = str(tmp_path / "temp.tif") |         outfile = tmp_path / "temp.tif" | ||||||
|         icc_profile = b"Dummy value" |         icc_profile = b"Dummy value" | ||||||
|         im.save(outfile, icc_profile=icc_profile) |         im.save(outfile, icc_profile=icc_profile) | ||||||
| 
 | 
 | ||||||
|  | @ -821,11 +869,11 @@ class TestFileTiff: | ||||||
|         with Image.open("Tests/images/hopper.bmp") as im: |         with Image.open("Tests/images/hopper.bmp") as im: | ||||||
|             assert im.info["compression"] == 0 |             assert im.info["compression"] == 0 | ||||||
| 
 | 
 | ||||||
|             outfile = str(tmp_path / "temp.tif") |             outfile = tmp_path / "temp.tif" | ||||||
|             im.save(outfile) |             im.save(outfile) | ||||||
| 
 | 
 | ||||||
|     def test_discard_icc_profile(self, tmp_path: Path) -> None: |     def test_discard_icc_profile(self, tmp_path: Path) -> None: | ||||||
|         outfile = str(tmp_path / "temp.tif") |         outfile = tmp_path / "temp.tif" | ||||||
| 
 | 
 | ||||||
|         with Image.open("Tests/images/icc_profile.png") as im: |         with Image.open("Tests/images/icc_profile.png") as im: | ||||||
|             assert "icc_profile" in im.info |             assert "icc_profile" in im.info | ||||||
|  | @ -853,6 +901,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, | ||||||
|  | @ -878,7 +927,7 @@ class TestFileTiff: | ||||||
|             ] |             ] | ||||||
| 
 | 
 | ||||||
|     def test_tiff_chunks(self, tmp_path: Path) -> None: |     def test_tiff_chunks(self, tmp_path: Path) -> None: | ||||||
|         tmpfile = str(tmp_path / "temp.tif") |         tmpfile = tmp_path / "temp.tif" | ||||||
| 
 | 
 | ||||||
|         im = hopper() |         im = hopper() | ||||||
|         with open(tmpfile, "wb") as fp: |         with open(tmpfile, "wb") as fp: | ||||||
|  | @ -900,7 +949,7 @@ class TestFileTiff: | ||||||
| 
 | 
 | ||||||
|     def test_close_on_load_exclusive(self, tmp_path: Path) -> None: |     def test_close_on_load_exclusive(self, tmp_path: Path) -> None: | ||||||
|         # similar to test_fd_leak, but runs on unixlike os |         # similar to test_fd_leak, but runs on unixlike os | ||||||
|         tmpfile = str(tmp_path / "temp.tif") |         tmpfile = tmp_path / "temp.tif" | ||||||
| 
 | 
 | ||||||
|         with Image.open("Tests/images/uint16_1_4660.tif") as im: |         with Image.open("Tests/images/uint16_1_4660.tif") as im: | ||||||
|             im.save(tmpfile) |             im.save(tmpfile) | ||||||
|  | @ -912,7 +961,7 @@ class TestFileTiff: | ||||||
|         assert fp.closed |         assert fp.closed | ||||||
| 
 | 
 | ||||||
|     def test_close_on_load_nonexclusive(self, tmp_path: Path) -> None: |     def test_close_on_load_nonexclusive(self, tmp_path: Path) -> None: | ||||||
|         tmpfile = str(tmp_path / "temp.tif") |         tmpfile = tmp_path / "temp.tif" | ||||||
| 
 | 
 | ||||||
|         with Image.open("Tests/images/uint16_1_4660.tif") as im: |         with Image.open("Tests/images/uint16_1_4660.tif") as im: | ||||||
|             im.save(tmpfile) |             im.save(tmpfile) | ||||||
|  | @ -963,7 +1012,7 @@ class TestFileTiff: | ||||||
| @pytest.mark.skipif(not is_win32(), reason="Windows only") | @pytest.mark.skipif(not is_win32(), reason="Windows only") | ||||||
| class TestFileTiffW32: | class TestFileTiffW32: | ||||||
|     def test_fd_leak(self, tmp_path: Path) -> None: |     def test_fd_leak(self, tmp_path: Path) -> None: | ||||||
|         tmpfile = str(tmp_path / "temp.tif") |         tmpfile = tmp_path / "temp.tif" | ||||||
| 
 | 
 | ||||||
|         # this is an mmaped file. |         # this is an mmaped file. | ||||||
|         with Image.open("Tests/images/uint16_1_4660.tif") as im: |         with Image.open("Tests/images/uint16_1_4660.tif") as im: | ||||||
|  |  | ||||||
|  | @ -56,11 +56,12 @@ def test_rt_metadata(tmp_path: Path) -> None: | ||||||
| 
 | 
 | ||||||
|     info[ImageDescription] = text_data |     info[ImageDescription] = text_data | ||||||
| 
 | 
 | ||||||
|     f = str(tmp_path / "temp.tif") |     f = tmp_path / "temp.tif" | ||||||
| 
 | 
 | ||||||
|     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,13 +131,15 @@ 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: | ||||||
|         f = str(tmp_path / "temp.tiff") |         assert isinstance(img, TiffImagePlugin.TiffImageFile) | ||||||
|  |         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) | ||||||
| 
 | 
 | ||||||
|         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"] | ||||||
|  | @ -163,8 +168,9 @@ 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 = str(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 | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | @ -210,7 +217,7 @@ def test_no_duplicate_50741_tag() -> None: | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def test_iptc(tmp_path: Path) -> None: | def test_iptc(tmp_path: Path) -> None: | ||||||
|     out = str(tmp_path / "temp.tiff") |     out = tmp_path / "temp.tiff" | ||||||
|     with Image.open("Tests/images/hopper.Lab.tif") as im: |     with Image.open("Tests/images/hopper.Lab.tif") as im: | ||||||
|         im.save(out) |         im.save(out) | ||||||
| 
 | 
 | ||||||
|  | @ -227,10 +234,11 @@ def test_writing_other_types_to_ascii( | ||||||
|     info[271] = value |     info[271] = value | ||||||
| 
 | 
 | ||||||
|     im = hopper() |     im = hopper() | ||||||
|     out = str(tmp_path / "temp.tiff") |     out = tmp_path / "temp.tiff" | ||||||
|     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 | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | @ -244,10 +252,11 @@ def test_writing_other_types_to_bytes(value: int | IFDRational, tmp_path: Path) | ||||||
| 
 | 
 | ||||||
|     info[700] = value |     info[700] = value | ||||||
| 
 | 
 | ||||||
|     out = str(tmp_path / "temp.tiff") |     out = tmp_path / "temp.tiff" | ||||||
|     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" | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | @ -263,10 +272,11 @@ def test_writing_other_types_to_undefined( | ||||||
| 
 | 
 | ||||||
|     info[33723] = value |     info[33723] = value | ||||||
| 
 | 
 | ||||||
|     out = str(tmp_path / "temp.tiff") |     out = tmp_path / "temp.tiff" | ||||||
|     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" | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | @ -296,7 +306,7 @@ def test_empty_metadata() -> None: | ||||||
| 
 | 
 | ||||||
| def test_iccprofile(tmp_path: Path) -> None: | def test_iccprofile(tmp_path: Path) -> None: | ||||||
|     # https://github.com/python-pillow/Pillow/issues/1462 |     # https://github.com/python-pillow/Pillow/issues/1462 | ||||||
|     out = str(tmp_path / "temp.tiff") |     out = tmp_path / "temp.tiff" | ||||||
|     with Image.open("Tests/images/hopper.iccprofile.tif") as im: |     with Image.open("Tests/images/hopper.iccprofile.tif") as im: | ||||||
|         im.save(out) |         im.save(out) | ||||||
| 
 | 
 | ||||||
|  | @ -311,19 +321,20 @@ 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"] | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def test_iccprofile_save_png(tmp_path: Path) -> None: | def test_iccprofile_save_png(tmp_path: Path) -> None: | ||||||
|     with Image.open("Tests/images/hopper.iccprofile.tif") as im: |     with Image.open("Tests/images/hopper.iccprofile.tif") as im: | ||||||
|         outfile = str(tmp_path / "temp.png") |         outfile = tmp_path / "temp.png" | ||||||
|         im.save(outfile) |         im.save(outfile) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def test_iccprofile_binary_save_png(tmp_path: Path) -> None: | def test_iccprofile_binary_save_png(tmp_path: Path) -> None: | ||||||
|     with Image.open("Tests/images/hopper.iccprofile_binary.tif") as im: |     with Image.open("Tests/images/hopper.iccprofile_binary.tif") as im: | ||||||
|         outfile = str(tmp_path / "temp.png") |         outfile = tmp_path / "temp.png" | ||||||
|         im.save(outfile) |         im.save(outfile) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | @ -332,10 +343,11 @@ def test_exif_div_zero(tmp_path: Path) -> None: | ||||||
|     info = TiffImagePlugin.ImageFileDirectory_v2() |     info = TiffImagePlugin.ImageFileDirectory_v2() | ||||||
|     info[41988] = TiffImagePlugin.IFDRational(0, 0) |     info[41988] = TiffImagePlugin.IFDRational(0, 0) | ||||||
| 
 | 
 | ||||||
|     out = str(tmp_path / "temp.tiff") |     out = tmp_path / "temp.tiff" | ||||||
|     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 | ||||||
| 
 | 
 | ||||||
|  | @ -351,10 +363,11 @@ def test_ifd_unsigned_rational(tmp_path: Path) -> None: | ||||||
| 
 | 
 | ||||||
|     info[41493] = TiffImagePlugin.IFDRational(numerator, 1) |     info[41493] = TiffImagePlugin.IFDRational(numerator, 1) | ||||||
| 
 | 
 | ||||||
|     out = str(tmp_path / "temp.tiff") |     out = tmp_path / "temp.tiff" | ||||||
|     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 | ||||||
| 
 | 
 | ||||||
|  | @ -363,10 +376,11 @@ def test_ifd_unsigned_rational(tmp_path: Path) -> None: | ||||||
| 
 | 
 | ||||||
|     info[41493] = TiffImagePlugin.IFDRational(numerator, 1) |     info[41493] = TiffImagePlugin.IFDRational(numerator, 1) | ||||||
| 
 | 
 | ||||||
|     out = str(tmp_path / "temp.tiff") |     out = tmp_path / "temp.tiff" | ||||||
|     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 | ||||||
| 
 | 
 | ||||||
|  | @ -381,10 +395,11 @@ def test_ifd_signed_rational(tmp_path: Path) -> None: | ||||||
| 
 | 
 | ||||||
|     info[37380] = TiffImagePlugin.IFDRational(numerator, denominator) |     info[37380] = TiffImagePlugin.IFDRational(numerator, denominator) | ||||||
| 
 | 
 | ||||||
|     out = str(tmp_path / "temp.tiff") |     out = tmp_path / "temp.tiff" | ||||||
|     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 | ||||||
| 
 | 
 | ||||||
|  | @ -393,10 +408,11 @@ def test_ifd_signed_rational(tmp_path: Path) -> None: | ||||||
| 
 | 
 | ||||||
|     info[37380] = TiffImagePlugin.IFDRational(numerator, denominator) |     info[37380] = TiffImagePlugin.IFDRational(numerator, denominator) | ||||||
| 
 | 
 | ||||||
|     out = str(tmp_path / "temp.tiff") |     out = tmp_path / "temp.tiff" | ||||||
|     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 | ||||||
| 
 | 
 | ||||||
|  | @ -406,10 +422,11 @@ def test_ifd_signed_rational(tmp_path: Path) -> None: | ||||||
| 
 | 
 | ||||||
|     info[37380] = TiffImagePlugin.IFDRational(numerator, denominator) |     info[37380] = TiffImagePlugin.IFDRational(numerator, denominator) | ||||||
| 
 | 
 | ||||||
|     out = str(tmp_path / "temp.tiff") |     out = tmp_path / "temp.tiff" | ||||||
|     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 | ||||||
| 
 | 
 | ||||||
|  | @ -420,10 +437,11 @@ def test_ifd_signed_long(tmp_path: Path) -> None: | ||||||
| 
 | 
 | ||||||
|     info[37000] = -60000 |     info[37000] = -60000 | ||||||
| 
 | 
 | ||||||
|     out = str(tmp_path / "temp.tiff") |     out = tmp_path / "temp.tiff" | ||||||
|     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 = str(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) | ||||||
| 
 | 
 | ||||||
|  | @ -480,7 +500,7 @@ def test_tag_group_data() -> None: | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def test_empty_subifd(tmp_path: Path) -> None: | def test_empty_subifd(tmp_path: Path) -> None: | ||||||
|     out = str(tmp_path / "temp.jpg") |     out = tmp_path / "temp.jpg" | ||||||
| 
 | 
 | ||||||
|     im = hopper() |     im = hopper() | ||||||
|     exif = im.getexif() |     exif = im.getexif() | ||||||
|  |  | ||||||
|  | @ -154,9 +154,8 @@ class TestFileWebp: | ||||||
|     @pytest.mark.skipif(sys.maxsize <= 2**32, reason="Requires 64-bit system") |     @pytest.mark.skipif(sys.maxsize <= 2**32, reason="Requires 64-bit system") | ||||||
|     def test_write_encoding_error_message(self, tmp_path: Path) -> None: |     def test_write_encoding_error_message(self, tmp_path: Path) -> None: | ||||||
|         im = Image.new("RGB", (15000, 15000)) |         im = Image.new("RGB", (15000, 15000)) | ||||||
|         with pytest.raises(ValueError) as e: |         with pytest.raises(ValueError, match="encoding error 6"): | ||||||
|             im.save(tmp_path / "temp.webp", method=0) |             im.save(tmp_path / "temp.webp", method=0) | ||||||
|         assert str(e.value) == "encoding error 6" |  | ||||||
| 
 | 
 | ||||||
|     @pytest.mark.skipif(sys.maxsize <= 2**32, reason="Requires 64-bit system") |     @pytest.mark.skipif(sys.maxsize <= 2**32, reason="Requires 64-bit system") | ||||||
|     def test_write_encoding_error_bad_dimension(self, tmp_path: Path) -> None: |     def test_write_encoding_error_bad_dimension(self, tmp_path: Path) -> None: | ||||||
|  | @ -231,7 +230,7 @@ class TestFileWebp: | ||||||
| 
 | 
 | ||||||
|         with Image.open(out_gif) as reread: |         with Image.open(out_gif) as reread: | ||||||
|             reread_value = reread.convert("RGB").getpixel((1, 1)) |             reread_value = reread.convert("RGB").getpixel((1, 1)) | ||||||
|         difference = sum(abs(original_value[i] - reread_value[i]) for i in range(0, 3)) |         difference = sum(abs(original_value[i] - reread_value[i]) for i in range(3)) | ||||||
|         assert difference < 5 |         assert difference < 5 | ||||||
| 
 | 
 | ||||||
|     def test_duration(self, tmp_path: Path) -> None: |     def test_duration(self, tmp_path: Path) -> None: | ||||||
|  |  | ||||||
|  | @ -42,7 +42,7 @@ def test_write_lossless_rgb(tmp_path: Path) -> None: | ||||||
|     Does it have the bits we expect? |     Does it have the bits we expect? | ||||||
|     """ |     """ | ||||||
| 
 | 
 | ||||||
|     temp_file = str(tmp_path / "temp.webp") |     temp_file = tmp_path / "temp.webp" | ||||||
|     # temp_file = "temp.webp" |     # temp_file = "temp.webp" | ||||||
| 
 | 
 | ||||||
|     pil_image = hopper("RGBA") |     pil_image = hopper("RGBA") | ||||||
|  | @ -71,7 +71,7 @@ def test_write_rgba(tmp_path: Path) -> None: | ||||||
|     Does it have the bits we expect? |     Does it have the bits we expect? | ||||||
|     """ |     """ | ||||||
| 
 | 
 | ||||||
|     temp_file = str(tmp_path / "temp.webp") |     temp_file = tmp_path / "temp.webp" | ||||||
| 
 | 
 | ||||||
|     pil_image = Image.new("RGBA", (10, 10), (255, 0, 0, 20)) |     pil_image = Image.new("RGBA", (10, 10), (255, 0, 0, 20)) | ||||||
|     pil_image.save(temp_file) |     pil_image.save(temp_file) | ||||||
|  | @ -104,7 +104,7 @@ def test_keep_rgb_values_when_transparent(tmp_path: Path) -> None: | ||||||
|     half_transparent_image.putalpha(new_alpha) |     half_transparent_image.putalpha(new_alpha) | ||||||
| 
 | 
 | ||||||
|     # save with transparent area preserved |     # save with transparent area preserved | ||||||
|     temp_file = str(tmp_path / "temp.webp") |     temp_file = tmp_path / "temp.webp" | ||||||
|     half_transparent_image.save(temp_file, exact=True, lossless=True) |     half_transparent_image.save(temp_file, exact=True, lossless=True) | ||||||
| 
 | 
 | ||||||
|     with Image.open(temp_file) as reloaded: |     with Image.open(temp_file) as reloaded: | ||||||
|  | @ -123,7 +123,7 @@ def test_write_unsupported_mode_PA(tmp_path: Path) -> None: | ||||||
|     should work, and be similar to the original file. |     should work, and be similar to the original file. | ||||||
|     """ |     """ | ||||||
| 
 | 
 | ||||||
|     temp_file = str(tmp_path / "temp.webp") |     temp_file = tmp_path / "temp.webp" | ||||||
|     file_path = "Tests/images/transparent.gif" |     file_path = "Tests/images/transparent.gif" | ||||||
|     with Image.open(file_path) as im: |     with Image.open(file_path) as im: | ||||||
|         im.save(temp_file) |         im.save(temp_file) | ||||||
|  | @ -142,10 +142,10 @@ def test_write_unsupported_mode_PA(tmp_path: Path) -> None: | ||||||
| 
 | 
 | ||||||
| def test_alpha_quality(tmp_path: Path) -> None: | def test_alpha_quality(tmp_path: Path) -> None: | ||||||
|     with Image.open("Tests/images/transparent.png") as im: |     with Image.open("Tests/images/transparent.png") as im: | ||||||
|         out = str(tmp_path / "temp.webp") |         out = tmp_path / "temp.webp" | ||||||
|         im.save(out) |         im.save(out) | ||||||
| 
 | 
 | ||||||
|         out_quality = str(tmp_path / "quality.webp") |         out_quality = tmp_path / "quality.webp" | ||||||
|         im.save(out_quality, alpha_quality=50) |         im.save(out_quality, alpha_quality=50) | ||||||
|         with Image.open(out) as reloaded: |         with Image.open(out) as reloaded: | ||||||
|             with Image.open(out_quality) as reloaded_quality: |             with Image.open(out_quality) as reloaded_quality: | ||||||
|  |  | ||||||
|  | @ -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 = str(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 | ||||||
|  | @ -67,8 +71,9 @@ def test_write_animation_RGB(tmp_path: Path) -> None: | ||||||
|     are visually similar to the originals. |     are visually similar to the originals. | ||||||
|     """ |     """ | ||||||
| 
 | 
 | ||||||
|     def check(temp_file: str) -> 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 | ||||||
|  | @ -87,7 +92,7 @@ def test_write_animation_RGB(tmp_path: Path) -> None: | ||||||
| 
 | 
 | ||||||
|     with Image.open("Tests/images/anim_frame1.webp") as frame1: |     with Image.open("Tests/images/anim_frame1.webp") as frame1: | ||||||
|         with Image.open("Tests/images/anim_frame2.webp") as frame2: |         with Image.open("Tests/images/anim_frame2.webp") as frame2: | ||||||
|             temp_file1 = str(tmp_path / "temp.webp") |             temp_file1 = tmp_path / "temp.webp" | ||||||
|             frame1.copy().save( |             frame1.copy().save( | ||||||
|                 temp_file1, save_all=True, append_images=[frame2], lossless=True |                 temp_file1, save_all=True, append_images=[frame2], lossless=True | ||||||
|             ) |             ) | ||||||
|  | @ -99,7 +104,7 @@ def test_write_animation_RGB(tmp_path: Path) -> None: | ||||||
|             ) -> Generator[Image.Image, None, None]: |             ) -> Generator[Image.Image, None, None]: | ||||||
|                 yield from ims |                 yield from ims | ||||||
| 
 | 
 | ||||||
|             temp_file2 = str(tmp_path / "temp_generator.webp") |             temp_file2 = tmp_path / "temp_generator.webp" | ||||||
|             frame1.copy().save( |             frame1.copy().save( | ||||||
|                 temp_file2, |                 temp_file2, | ||||||
|                 save_all=True, |                 save_all=True, | ||||||
|  | @ -116,7 +121,7 @@ def test_timestamp_and_duration(tmp_path: Path) -> None: | ||||||
|     """ |     """ | ||||||
| 
 | 
 | ||||||
|     durations = [0, 10, 20, 30, 40] |     durations = [0, 10, 20, 30, 40] | ||||||
|     temp_file = str(tmp_path / "temp.webp") |     temp_file = tmp_path / "temp.webp" | ||||||
|     with Image.open("Tests/images/anim_frame1.webp") as frame1: |     with Image.open("Tests/images/anim_frame1.webp") as frame1: | ||||||
|         with Image.open("Tests/images/anim_frame2.webp") as frame2: |         with Image.open("Tests/images/anim_frame2.webp") as frame2: | ||||||
|             frame1.save( |             frame1.save( | ||||||
|  | @ -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 | ||||||
| 
 | 
 | ||||||
|  | @ -141,7 +147,7 @@ def test_timestamp_and_duration(tmp_path: Path) -> None: | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def test_float_duration(tmp_path: Path) -> None: | def test_float_duration(tmp_path: Path) -> None: | ||||||
|     temp_file = str(tmp_path / "temp.webp") |     temp_file = tmp_path / "temp.webp" | ||||||
|     with Image.open("Tests/images/iss634.apng") as im: |     with Image.open("Tests/images/iss634.apng") as im: | ||||||
|         assert im.info["duration"] == 70.0 |         assert im.info["duration"] == 70.0 | ||||||
| 
 | 
 | ||||||
|  | @ -159,7 +165,7 @@ def test_seeking(tmp_path: Path) -> None: | ||||||
|     """ |     """ | ||||||
| 
 | 
 | ||||||
|     dur = 33 |     dur = 33 | ||||||
|     temp_file = str(tmp_path / "temp.webp") |     temp_file = tmp_path / "temp.webp" | ||||||
|     with Image.open("Tests/images/anim_frame1.webp") as frame1: |     with Image.open("Tests/images/anim_frame1.webp") as frame1: | ||||||
|         with Image.open("Tests/images/anim_frame2.webp") as frame2: |         with Image.open("Tests/images/anim_frame2.webp") as frame2: | ||||||
|             frame1.save( |             frame1.save( | ||||||
|  | @ -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 | ||||||
| 
 | 
 | ||||||
|  | @ -196,10 +203,10 @@ def test_alpha_quality(tmp_path: Path) -> None: | ||||||
|     with Image.open("Tests/images/transparent.png") as im: |     with Image.open("Tests/images/transparent.png") as im: | ||||||
|         first_frame = Image.new("L", im.size) |         first_frame = Image.new("L", im.size) | ||||||
| 
 | 
 | ||||||
|         out = str(tmp_path / "temp.webp") |         out = tmp_path / "temp.webp" | ||||||
|         first_frame.save(out, save_all=True, append_images=[im]) |         first_frame.save(out, save_all=True, append_images=[im]) | ||||||
| 
 | 
 | ||||||
|         out_quality = str(tmp_path / "quality.webp") |         out_quality = tmp_path / "quality.webp" | ||||||
|         first_frame.save( |         first_frame.save( | ||||||
|             out_quality, save_all=True, append_images=[im], alpha_quality=50 |             out_quality, save_all=True, append_images=[im], alpha_quality=50 | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|  | @ -13,7 +13,7 @@ RGB_MODE = "RGB" | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def test_write_lossless_rgb(tmp_path: Path) -> None: | def test_write_lossless_rgb(tmp_path: Path) -> None: | ||||||
|     temp_file = str(tmp_path / "temp.webp") |     temp_file = tmp_path / "temp.webp" | ||||||
| 
 | 
 | ||||||
|     hopper(RGB_MODE).save(temp_file, lossless=True) |     hopper(RGB_MODE).save(temp_file, lossless=True) | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -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() | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | @ -146,7 +147,7 @@ def test_write_animated_metadata(tmp_path: Path) -> None: | ||||||
|     exif_data = b"<exif_data>" |     exif_data = b"<exif_data>" | ||||||
|     xmp_data = b"<xmp_data>" |     xmp_data = b"<xmp_data>" | ||||||
| 
 | 
 | ||||||
|     temp_file = str(tmp_path / "temp.webp") |     temp_file = tmp_path / "temp.webp" | ||||||
|     with Image.open("Tests/images/anim_frame1.webp") as frame1: |     with Image.open("Tests/images/anim_frame1.webp") as frame1: | ||||||
|         with Image.open("Tests/images/anim_frame2.webp") as frame2: |         with Image.open("Tests/images/anim_frame2.webp") as frame2: | ||||||
|             frame1.save( |             frame1.save( | ||||||
|  |  | ||||||
|  | @ -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 | ||||||
|  | @ -59,7 +68,7 @@ def test_register_handler(tmp_path: Path) -> None: | ||||||
|     WmfImagePlugin.register_handler(handler) |     WmfImagePlugin.register_handler(handler) | ||||||
| 
 | 
 | ||||||
|     im = hopper() |     im = hopper() | ||||||
|     tmpfile = str(tmp_path / "temp.wmf") |     tmpfile = tmp_path / "temp.wmf" | ||||||
|     im.save(tmpfile) |     im.save(tmpfile) | ||||||
|     assert handler.methodCalled |     assert handler.methodCalled | ||||||
| 
 | 
 | ||||||
|  | @ -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,11 +98,27 @@ 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: | ||||||
|     im = hopper() |     im = hopper() | ||||||
| 
 | 
 | ||||||
|     tmpfile = str(tmp_path / ("temp" + ext)) |     tmpfile = tmp_path / ("temp" + ext) | ||||||
|     with pytest.raises(OSError): |     with pytest.raises(OSError): | ||||||
|         im.save(tmpfile) |         im.save(tmpfile) | ||||||
|  |  | ||||||
|  | @ -73,7 +73,7 @@ def test_invalid_file() -> None: | ||||||
| 
 | 
 | ||||||
| def test_save_wrong_mode(tmp_path: Path) -> None: | def test_save_wrong_mode(tmp_path: Path) -> None: | ||||||
|     im = hopper() |     im = hopper() | ||||||
|     out = str(tmp_path / "temp.xbm") |     out = tmp_path / "temp.xbm" | ||||||
| 
 | 
 | ||||||
|     with pytest.raises(OSError): |     with pytest.raises(OSError): | ||||||
|         im.save(out) |         im.save(out) | ||||||
|  | @ -81,7 +81,7 @@ def test_save_wrong_mode(tmp_path: Path) -> None: | ||||||
| 
 | 
 | ||||||
| def test_hotspot(tmp_path: Path) -> None: | def test_hotspot(tmp_path: Path) -> None: | ||||||
|     im = hopper("1") |     im = hopper("1") | ||||||
|     out = str(tmp_path / "temp.xbm") |     out = tmp_path / "temp.xbm" | ||||||
| 
 | 
 | ||||||
|     hotspot = (0, 7) |     hotspot = (0, 7) | ||||||
|     im.save(out, hotspot=hotspot) |     im.save(out, hotspot=hotspot) | ||||||
|  |  | ||||||
|  | @ -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 | ||||||
|  |  | ||||||
|  | @ -65,9 +65,8 @@ class TestImage: | ||||||
| 
 | 
 | ||||||
|     @pytest.mark.parametrize("mode", ("", "bad", "very very long")) |     @pytest.mark.parametrize("mode", ("", "bad", "very very long")) | ||||||
|     def test_image_modes_fail(self, mode: str) -> None: |     def test_image_modes_fail(self, mode: str) -> None: | ||||||
|         with pytest.raises(ValueError) as e: |         with pytest.raises(ValueError, match="unrecognized image mode"): | ||||||
|             Image.new(mode, (1, 1)) |             Image.new(mode, (1, 1)) | ||||||
|         assert str(e.value) == "unrecognized image mode" |  | ||||||
| 
 | 
 | ||||||
|     def test_exception_inheritance(self) -> None: |     def test_exception_inheritance(self) -> None: | ||||||
|         assert issubclass(UnidentifiedImageError, OSError) |         assert issubclass(UnidentifiedImageError, OSError) | ||||||
|  | @ -176,6 +175,13 @@ class TestImage: | ||||||
|             with Image.open(io.StringIO()):  # type: ignore[arg-type] |             with Image.open(io.StringIO()):  # type: ignore[arg-type] | ||||||
|                 pass |                 pass | ||||||
| 
 | 
 | ||||||
|  |     def test_string(self, tmp_path: Path) -> None: | ||||||
|  |         out = str(tmp_path / "temp.png") | ||||||
|  |         im = hopper() | ||||||
|  |         im.save(out) | ||||||
|  |         with Image.open(out) as reloaded: | ||||||
|  |             assert_image_equal(im, reloaded) | ||||||
|  | 
 | ||||||
|     def test_pathlib(self, tmp_path: Path) -> None: |     def test_pathlib(self, tmp_path: Path) -> None: | ||||||
|         with Image.open(Path("Tests/images/multipage-mmap.tiff")) as im: |         with Image.open(Path("Tests/images/multipage-mmap.tiff")) as im: | ||||||
|             assert im.mode == "P" |             assert im.mode == "P" | ||||||
|  | @ -188,14 +194,13 @@ class TestImage: | ||||||
|             for ext in (".jpg", ".jp2"): |             for ext in (".jpg", ".jp2"): | ||||||
|                 if ext == ".jp2" and not features.check_codec("jpg_2000"): |                 if ext == ".jp2" and not features.check_codec("jpg_2000"): | ||||||
|                     pytest.skip("jpg_2000 not available") |                     pytest.skip("jpg_2000 not available") | ||||||
|                 temp_file = str(tmp_path / ("temp." + ext)) |                 im.save(tmp_path / ("temp." + ext)) | ||||||
|                 im.save(Path(temp_file)) |  | ||||||
| 
 | 
 | ||||||
|     def test_fp_name(self, tmp_path: Path) -> None: |     def test_fp_name(self, tmp_path: Path) -> None: | ||||||
|         temp_file = str(tmp_path / "temp.jpg") |         temp_file = tmp_path / "temp.jpg" | ||||||
| 
 | 
 | ||||||
|         class FP(io.BytesIO): |         class FP(io.BytesIO): | ||||||
|             name: str |             name: Path | ||||||
| 
 | 
 | ||||||
|             if sys.version_info >= (3, 12): |             if sys.version_info >= (3, 12): | ||||||
|                 from collections.abc import Buffer |                 from collections.abc import Buffer | ||||||
|  | @ -225,8 +230,8 @@ 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 = str(tmp_path / "temp.unknown") |         with hopper() as im: | ||||||
|             with pytest.raises(ValueError): |             with pytest.raises(ValueError): | ||||||
|                 im.save(temp_file) |                 im.save(temp_file) | ||||||
| 
 | 
 | ||||||
|  | @ -246,13 +251,22 @@ class TestImage: | ||||||
|         reason="Test requires opening an mmaped file for writing", |         reason="Test requires opening an mmaped file for writing", | ||||||
|     ) |     ) | ||||||
|     def test_readonly_save(self, tmp_path: Path) -> None: |     def test_readonly_save(self, tmp_path: Path) -> None: | ||||||
|         temp_file = str(tmp_path / "temp.bmp") |         temp_file = tmp_path / "temp.bmp" | ||||||
|         shutil.copy("Tests/images/rgb32bf-rgba.bmp", temp_file) |         shutil.copy("Tests/images/rgb32bf-rgba.bmp", temp_file) | ||||||
| 
 | 
 | ||||||
|         with Image.open(temp_file) as im: |         with Image.open(temp_file) as im: | ||||||
|             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")) | ||||||
|  | @ -729,7 +743,7 @@ class TestImage: | ||||||
|         # https://github.com/python-pillow/Pillow/issues/835 |         # https://github.com/python-pillow/Pillow/issues/835 | ||||||
|         # Arrange |         # Arrange | ||||||
|         test_file = "Tests/images/hopper.png" |         test_file = "Tests/images/hopper.png" | ||||||
|         temp_file = str(tmp_path / "temp.jpg") |         temp_file = tmp_path / "temp.jpg" | ||||||
| 
 | 
 | ||||||
|         # Act/Assert |         # Act/Assert | ||||||
|         with Image.open(test_file) as im: |         with Image.open(test_file) as im: | ||||||
|  | @ -739,7 +753,7 @@ class TestImage: | ||||||
|                 im.save(temp_file) |                 im.save(temp_file) | ||||||
| 
 | 
 | ||||||
|     def test_no_new_file_on_error(self, tmp_path: Path) -> None: |     def test_no_new_file_on_error(self, tmp_path: Path) -> None: | ||||||
|         temp_file = str(tmp_path / "temp.jpg") |         temp_file = tmp_path / "temp.jpg" | ||||||
| 
 | 
 | ||||||
|         im = Image.new("RGB", (0, 0)) |         im = Image.new("RGB", (0, 0)) | ||||||
|         with pytest.raises(ValueError): |         with pytest.raises(ValueError): | ||||||
|  | @ -806,7 +820,7 @@ class TestImage: | ||||||
|             assert exif[296] == 2 |             assert exif[296] == 2 | ||||||
|             assert exif[11] == "gThumb 3.0.1" |             assert exif[11] == "gThumb 3.0.1" | ||||||
| 
 | 
 | ||||||
|             out = str(tmp_path / "temp.jpg") |             out = tmp_path / "temp.jpg" | ||||||
|             exif[258] = 8 |             exif[258] = 8 | ||||||
|             del exif[274] |             del exif[274] | ||||||
|             del exif[282] |             del exif[282] | ||||||
|  | @ -828,7 +842,7 @@ class TestImage: | ||||||
|             assert exif[274] == 1 |             assert exif[274] == 1 | ||||||
|             assert exif[305] == "Adobe Photoshop CC 2017 (Macintosh)" |             assert exif[305] == "Adobe Photoshop CC 2017 (Macintosh)" | ||||||
| 
 | 
 | ||||||
|             out = str(tmp_path / "temp.jpg") |             out = tmp_path / "temp.jpg" | ||||||
|             exif[258] = 8 |             exif[258] = 8 | ||||||
|             del exif[306] |             del exif[306] | ||||||
|             exif[274] = 455 |             exif[274] = 455 | ||||||
|  | @ -847,7 +861,7 @@ class TestImage: | ||||||
|             exif = im.getexif() |             exif = im.getexif() | ||||||
|             assert exif == {} |             assert exif == {} | ||||||
| 
 | 
 | ||||||
|             out = str(tmp_path / "temp.webp") |             out = tmp_path / "temp.webp" | ||||||
|             exif[258] = 8 |             exif[258] = 8 | ||||||
|             exif[40963] = 455 |             exif[40963] = 455 | ||||||
|             exif[305] = "Pillow test" |             exif[305] = "Pillow test" | ||||||
|  | @ -869,7 +883,7 @@ class TestImage: | ||||||
|             exif = im.getexif() |             exif = im.getexif() | ||||||
|             assert exif == {274: 1} |             assert exif == {274: 1} | ||||||
| 
 | 
 | ||||||
|             out = str(tmp_path / "temp.png") |             out = tmp_path / "temp.png" | ||||||
|             exif[258] = 8 |             exif[258] = 8 | ||||||
|             del exif[274] |             del exif[274] | ||||||
|             exif[40963] = 455 |             exif[40963] = 455 | ||||||
|  |  | ||||||
|  | @ -118,7 +118,7 @@ def test_trns_p(tmp_path: Path) -> None: | ||||||
|     im = hopper("P") |     im = hopper("P") | ||||||
|     im.info["transparency"] = 0 |     im.info["transparency"] = 0 | ||||||
| 
 | 
 | ||||||
|     f = str(tmp_path / "temp.png") |     f = tmp_path / "temp.png" | ||||||
| 
 | 
 | ||||||
|     im_l = im.convert("L") |     im_l = im.convert("L") | ||||||
|     assert im_l.info["transparency"] == 0 |     assert im_l.info["transparency"] == 0 | ||||||
|  | @ -154,7 +154,7 @@ def test_trns_l(tmp_path: Path) -> None: | ||||||
|     im = hopper("L") |     im = hopper("L") | ||||||
|     im.info["transparency"] = 128 |     im.info["transparency"] = 128 | ||||||
| 
 | 
 | ||||||
|     f = str(tmp_path / "temp.png") |     f = tmp_path / "temp.png" | ||||||
| 
 | 
 | ||||||
|     im_la = im.convert("LA") |     im_la = im.convert("LA") | ||||||
|     assert "transparency" not in im_la.info |     assert "transparency" not in im_la.info | ||||||
|  | @ -177,7 +177,7 @@ def test_trns_RGB(tmp_path: Path) -> None: | ||||||
|     im = hopper("RGB") |     im = hopper("RGB") | ||||||
|     im.info["transparency"] = im.getpixel((0, 0)) |     im.info["transparency"] = im.getpixel((0, 0)) | ||||||
| 
 | 
 | ||||||
|     f = str(tmp_path / "temp.png") |     f = tmp_path / "temp.png" | ||||||
| 
 | 
 | ||||||
|     im_l = im.convert("L") |     im_l = im.convert("L") | ||||||
|     assert im_l.info["transparency"] == im_l.getpixel((0, 0))  # undone |     assert im_l.info["transparency"] == im_l.getpixel((0, 0))  # undone | ||||||
|  |  | ||||||
|  | @ -171,7 +171,7 @@ class TestImagingCoreResize: | ||||||
|         # platforms. So if a future Pillow change requires that the test file |         # platforms. So if a future Pillow change requires that the test file | ||||||
|         # be updated, that is okay. |         # be updated, that is okay. | ||||||
|         im = hopper().resize((64, 64)) |         im = hopper().resize((64, 64)) | ||||||
|         temp_file = str(tmp_path / "temp.gif") |         temp_file = tmp_path / "temp.gif" | ||||||
|         im.save(temp_file) |         im.save(temp_file) | ||||||
| 
 | 
 | ||||||
|         with Image.open(temp_file) as reloaded: |         with Image.open(temp_file) as reloaded: | ||||||
|  |  | ||||||
|  | @ -45,9 +45,9 @@ def test_split_merge(mode: str) -> None: | ||||||
| 
 | 
 | ||||||
| def test_split_open(tmp_path: Path) -> None: | def test_split_open(tmp_path: Path) -> None: | ||||||
|     if features.check("zlib"): |     if features.check("zlib"): | ||||||
|         test_file = str(tmp_path / "temp.png") |         test_file = tmp_path / "temp.png" | ||||||
|     else: |     else: | ||||||
|         test_file = str(tmp_path / "temp.pcx") |         test_file = tmp_path / "temp.pcx" | ||||||
| 
 | 
 | ||||||
|     def split_open(mode: str) -> int: |     def split_open(mode: str) -> int: | ||||||
|         hopper(mode).save(test_file) |         hopper(mode).save(test_file) | ||||||
|  |  | ||||||
|  | @ -39,6 +39,8 @@ BBOX = (((X0, Y0), (X1, Y1)), [(X0, Y0), (X1, Y1)], (X0, Y0, X1, Y1), [X0, Y0, X | ||||||
| POINTS = ( | POINTS = ( | ||||||
|     ((10, 10), (20, 40), (30, 30)), |     ((10, 10), (20, 40), (30, 30)), | ||||||
|     [(10, 10), (20, 40), (30, 30)], |     [(10, 10), (20, 40), (30, 30)], | ||||||
|  |     ([10, 10], [20, 40], [30, 30]), | ||||||
|  |     [[10, 10], [20, 40], [30, 30]], | ||||||
|     (10, 10, 20, 40, 30, 30), |     (10, 10, 20, 40, 30, 30), | ||||||
|     [10, 10, 20, 40, 30, 30], |     [10, 10, 20, 40, 30, 30], | ||||||
| ) | ) | ||||||
|  | @ -46,6 +48,8 @@ POINTS = ( | ||||||
| KITE_POINTS = ( | KITE_POINTS = ( | ||||||
|     ((10, 50), (70, 10), (90, 50), (70, 90), (10, 50)), |     ((10, 50), (70, 10), (90, 50), (70, 90), (10, 50)), | ||||||
|     [(10, 50), (70, 10), (90, 50), (70, 90), (10, 50)], |     [(10, 50), (70, 10), (90, 50), (70, 90), (10, 50)], | ||||||
|  |     ([10, 50], [70, 10], [90, 50], [70, 90], [10, 50]), | ||||||
|  |     [[10, 50], [70, 10], [90, 50], [70, 90], [10, 50]], | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | @ -1044,8 +1048,8 @@ def create_base_image_draw( | ||||||
|     background2: tuple[int, int, int] = GRAY, |     background2: tuple[int, int, int] = GRAY, | ||||||
| ) -> tuple[Image.Image, ImageDraw.ImageDraw]: | ) -> tuple[Image.Image, ImageDraw.ImageDraw]: | ||||||
|     img = Image.new(mode, size, background1) |     img = Image.new(mode, size, background1) | ||||||
|     for x in range(0, size[0]): |     for x in range(size[0]): | ||||||
|         for y in range(0, size[1]): |         for y in range(size[1]): | ||||||
|             if (x + y) % 2 == 0: |             if (x + y) % 2 == 0: | ||||||
|                 img.putpixel((x, y), background2) |                 img.putpixel((x, y), background2) | ||||||
|     return img, ImageDraw.Draw(img) |     return img, ImageDraw.Draw(img) | ||||||
|  | @ -1626,7 +1630,7 @@ def test_compute_regular_polygon_vertices( | ||||||
|             0, |             0, | ||||||
|             ValueError, |             ValueError, | ||||||
|             "bounding_circle should contain 2D coordinates " |             "bounding_circle should contain 2D coordinates " | ||||||
|             "and a radius (e.g. (x, y, r) or ((x, y), r) )", |             r"and a radius \(e.g. \(x, y, r\) or \(\(x, y\), r\) \)", | ||||||
|         ), |         ), | ||||||
|         ( |         ( | ||||||
|             3, |             3, | ||||||
|  | @ -1640,7 +1644,7 @@ def test_compute_regular_polygon_vertices( | ||||||
|             ((50, 50, 50), 25), |             ((50, 50, 50), 25), | ||||||
|             0, |             0, | ||||||
|             ValueError, |             ValueError, | ||||||
|             "bounding_circle centre should contain 2D coordinates (e.g. (x, y))", |             r"bounding_circle centre should contain 2D coordinates \(e.g. \(x, y\)\)", | ||||||
|         ), |         ), | ||||||
|         ( |         ( | ||||||
|             3, |             3, | ||||||
|  | @ -1665,9 +1669,8 @@ def test_compute_regular_polygon_vertices_input_error_handling( | ||||||
|     expected_error: type[Exception], |     expected_error: type[Exception], | ||||||
|     error_message: str, |     error_message: str, | ||||||
| ) -> None: | ) -> None: | ||||||
|     with pytest.raises(expected_error) as e: |     with pytest.raises(expected_error, match=error_message): | ||||||
|         ImageDraw._compute_regular_polygon_vertices(bounding_circle, n_sides, rotation)  # type: ignore[arg-type] |         ImageDraw._compute_regular_polygon_vertices(bounding_circle, n_sides, rotation)  # type: ignore[arg-type] | ||||||
|     assert str(e.value) == error_message |  | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def test_continuous_horizontal_edges_polygon() -> None: | def test_continuous_horizontal_edges_polygon() -> None: | ||||||
|  | @ -1701,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): | ||||||
|  | @ -176,9 +196,8 @@ class TestImageFile: | ||||||
|                 b"0" * ImageFile.SAFEBLOCK |                 b"0" * ImageFile.SAFEBLOCK | ||||||
|             )  # only SAFEBLOCK bytes, so that the header is truncated |             )  # only SAFEBLOCK bytes, so that the header is truncated | ||||||
|         ) |         ) | ||||||
|         with pytest.raises(OSError) as e: |         with pytest.raises(OSError, match="Truncated File Read"): | ||||||
|             BmpImagePlugin.BmpImageFile(b) |             BmpImagePlugin.BmpImageFile(b) | ||||||
|         assert str(e.value) == "Truncated File Read" |  | ||||||
| 
 | 
 | ||||||
|     @skip_unless_feature("zlib") |     @skip_unless_feature("zlib") | ||||||
|     def test_truncated_with_errors(self) -> None: |     def test_truncated_with_errors(self) -> None: | ||||||
|  |  | ||||||
|  | @ -124,7 +124,7 @@ def test_render_equal(layout_engine: ImageFont.Layout) -> None: | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def test_non_ascii_path(tmp_path: Path, layout_engine: ImageFont.Layout) -> None: | def test_non_ascii_path(tmp_path: Path, layout_engine: ImageFont.Layout) -> None: | ||||||
|     tempfile = str(tmp_path / ("temp_" + chr(128) + ".ttf")) |     tempfile = tmp_path / ("temp_" + chr(128) + ".ttf") | ||||||
|     try: |     try: | ||||||
|         shutil.copy(FONT_PATH, tempfile) |         shutil.copy(FONT_PATH, tempfile) | ||||||
|     except UnicodeEncodeError: |     except UnicodeEncodeError: | ||||||
|  |  | ||||||
|  | @ -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"]) | ||||||
|  |  | ||||||
|  | @ -80,15 +80,12 @@ def test_lut(op: str) -> None: | ||||||
| def test_no_operator_loaded() -> None: | def test_no_operator_loaded() -> None: | ||||||
|     im = Image.new("L", (1, 1)) |     im = Image.new("L", (1, 1)) | ||||||
|     mop = ImageMorph.MorphOp() |     mop = ImageMorph.MorphOp() | ||||||
|     with pytest.raises(Exception) as e: |     with pytest.raises(Exception, match="No operator loaded"): | ||||||
|         mop.apply(im) |         mop.apply(im) | ||||||
|     assert str(e.value) == "No operator loaded" |     with pytest.raises(Exception, match="No operator loaded"): | ||||||
|     with pytest.raises(Exception) as e: |  | ||||||
|         mop.match(im) |         mop.match(im) | ||||||
|     assert str(e.value) == "No operator loaded" |     with pytest.raises(Exception, match="No operator loaded"): | ||||||
|     with pytest.raises(Exception) as e: |  | ||||||
|         mop.save_lut("") |         mop.save_lut("") | ||||||
|     assert str(e.value) == "No operator loaded" |  | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| # Test the named patterns | # Test the named patterns | ||||||
|  | @ -238,15 +235,12 @@ def test_incorrect_mode() -> None: | ||||||
|     im = hopper("RGB") |     im = hopper("RGB") | ||||||
|     mop = ImageMorph.MorphOp(op_name="erosion8") |     mop = ImageMorph.MorphOp(op_name="erosion8") | ||||||
| 
 | 
 | ||||||
|     with pytest.raises(ValueError) as e: |     with pytest.raises(ValueError, match="Image mode must be L"): | ||||||
|         mop.apply(im) |         mop.apply(im) | ||||||
|     assert str(e.value) == "Image mode must be L" |     with pytest.raises(ValueError, match="Image mode must be L"): | ||||||
|     with pytest.raises(ValueError) as e: |  | ||||||
|         mop.match(im) |         mop.match(im) | ||||||
|     assert str(e.value) == "Image mode must be L" |     with pytest.raises(ValueError, match="Image mode must be L"): | ||||||
|     with pytest.raises(ValueError) as e: |  | ||||||
|         mop.get_on_pixels(im) |         mop.get_on_pixels(im) | ||||||
|     assert str(e.value) == "Image mode must be L" |  | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def test_add_patterns() -> None: | def test_add_patterns() -> None: | ||||||
|  | @ -279,9 +273,10 @@ def test_pattern_syntax_error() -> None: | ||||||
|     lb.add_patterns(new_patterns) |     lb.add_patterns(new_patterns) | ||||||
| 
 | 
 | ||||||
|     # Act / Assert |     # Act / Assert | ||||||
|     with pytest.raises(Exception) as e: |     with pytest.raises( | ||||||
|  |         Exception, match='Syntax error in pattern "a pattern with a syntax error"' | ||||||
|  |     ): | ||||||
|         lb.build_lut() |         lb.build_lut() | ||||||
|     assert str(e.value) == 'Syntax error in pattern "a pattern with a syntax error"' |  | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def test_load_invalid_mrl() -> None: | def test_load_invalid_mrl() -> None: | ||||||
|  | @ -290,9 +285,8 @@ def test_load_invalid_mrl() -> None: | ||||||
|     mop = ImageMorph.MorphOp() |     mop = ImageMorph.MorphOp() | ||||||
| 
 | 
 | ||||||
|     # Act / Assert |     # Act / Assert | ||||||
|     with pytest.raises(Exception) as e: |     with pytest.raises(Exception, match="Wrong size operator file!"): | ||||||
|         mop.load_lut(invalid_mrl) |         mop.load_lut(invalid_mrl) | ||||||
|     assert str(e.value) == "Wrong size operator file!" |  | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def test_roundtrip_mrl(tmp_path: Path) -> None: | def test_roundtrip_mrl(tmp_path: Path) -> None: | ||||||
|  |  | ||||||
|  | @ -112,7 +112,7 @@ def test_make_linear_lut() -> None: | ||||||
|     assert isinstance(lut, list) |     assert isinstance(lut, list) | ||||||
|     assert len(lut) == 256 |     assert len(lut) == 256 | ||||||
|     # Check values |     # Check values | ||||||
|     for i in range(0, len(lut)): |     for i in range(len(lut)): | ||||||
|         assert lut[i] == i |         assert lut[i] == i | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -68,25 +68,10 @@ def test_path_constructors( | ||||||
|     assert list(p) == [(0.0, 1.0)] |     assert list(p) == [(0.0, 1.0)] | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @pytest.mark.parametrize( | def test_invalid_path_constructors() -> None: | ||||||
|     "coords", |     # Arrange / Act | ||||||
|     ( |     with pytest.raises(ValueError, match="incorrect coordinate type"): | ||||||
|         ("a", "b"), |         ImagePath.Path(("a", "b")) | ||||||
|         ([0, 1],), |  | ||||||
|         [[0, 1]], |  | ||||||
|         ([0.0, 1.0],), |  | ||||||
|         [[0.0, 1.0]], |  | ||||||
|     ), |  | ||||||
| ) |  | ||||||
| def test_invalid_path_constructors( |  | ||||||
|     coords: tuple[str, str] | Sequence[Sequence[int]], |  | ||||||
| ) -> None: |  | ||||||
|     # Act |  | ||||||
|     with pytest.raises(ValueError) as e: |  | ||||||
|         ImagePath.Path(coords) |  | ||||||
| 
 |  | ||||||
|     # Assert |  | ||||||
|     assert str(e.value) == "incorrect coordinate type" |  | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @pytest.mark.parametrize( | @pytest.mark.parametrize( | ||||||
|  | @ -99,13 +84,9 @@ def test_invalid_path_constructors( | ||||||
|     ), |     ), | ||||||
| ) | ) | ||||||
| def test_path_odd_number_of_coordinates(coords: Sequence[int]) -> None: | def test_path_odd_number_of_coordinates(coords: Sequence[int]) -> None: | ||||||
|     # Act |     with pytest.raises(ValueError, match="wrong number of coordinates"): | ||||||
|     with pytest.raises(ValueError) as e: |  | ||||||
|         ImagePath.Path(coords) |         ImagePath.Path(coords) | ||||||
| 
 | 
 | ||||||
|     # Assert |  | ||||||
|     assert str(e.value) == "wrong number of coordinates" |  | ||||||
| 
 |  | ||||||
| 
 | 
 | ||||||
| @pytest.mark.parametrize( | @pytest.mark.parametrize( | ||||||
|     "coords, expected", |     "coords, expected", | ||||||
|  |  | ||||||
|  | @ -4,13 +4,13 @@ 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 | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def test_sanity(tmp_path: Path) -> None: | def test_sanity(tmp_path: Path) -> None: | ||||||
|     test_file = str(tmp_path / "temp.im") |     test_file = tmp_path / "temp.im" | ||||||
| 
 | 
 | ||||||
|     im = hopper("RGB") |     im = hopper("RGB") | ||||||
|     im.save(test_file) |     im.save(test_file) | ||||||
|  | @ -31,8 +31,9 @@ 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(0, im.n_frames): |         for index in range(im.n_frames): | ||||||
|             assert i[index] == next(i) |             assert i[index] == next(i) | ||||||
|         with pytest.raises(IndexError): |         with pytest.raises(IndexError): | ||||||
|             i[index + 1] |             i[index + 1] | ||||||
|  | @ -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) | ||||||
|  |  | ||||||
|  | @ -88,7 +88,7 @@ if is_win32(): | ||||||
|     def test_pointer(tmp_path: Path) -> None: |     def test_pointer(tmp_path: Path) -> None: | ||||||
|         im = hopper() |         im = hopper() | ||||||
|         (width, height) = im.size |         (width, height) = im.size | ||||||
|         opath = str(tmp_path / "temp.png") |         opath = tmp_path / "temp.png" | ||||||
|         imdib = ImageWin.Dib(im) |         imdib = ImageWin.Dib(im) | ||||||
| 
 | 
 | ||||||
|         hdr = BITMAPINFOHEADER() |         hdr = BITMAPINFOHEADER() | ||||||
|  |  | ||||||
|  | @ -44,7 +44,7 @@ def test_basic(tmp_path: Path, mode: str) -> None: | ||||||
|     im_out = im_in.transform((w, h), Image.Transform.EXTENT, (0, 0, w, h)) |     im_out = im_in.transform((w, h), Image.Transform.EXTENT, (0, 0, w, h)) | ||||||
|     verify(im_out)  # transform |     verify(im_out)  # transform | ||||||
| 
 | 
 | ||||||
|     filename = str(tmp_path / "temp.im") |     filename = tmp_path / "temp.im" | ||||||
|     im_in.save(filename) |     im_in.save(filename) | ||||||
| 
 | 
 | ||||||
|     with Image.open(filename) as im_out: |     with Image.open(filename) as im_out: | ||||||
|  |  | ||||||
|  | @ -18,7 +18,7 @@ def helper_pickle_file( | ||||||
| ) -> None: | ) -> None: | ||||||
|     # Arrange |     # Arrange | ||||||
|     with Image.open(test_file) as im: |     with Image.open(test_file) as im: | ||||||
|         filename = str(tmp_path / "temp.pkl") |         filename = tmp_path / "temp.pkl" | ||||||
|         if mode: |         if mode: | ||||||
|             im = im.convert(mode) |             im = im.convert(mode) | ||||||
| 
 | 
 | ||||||
|  | @ -65,7 +65,7 @@ def helper_pickle_string(protocol: int, test_file: str, mode: str | None) -> Non | ||||||
|         ("Tests/images/itxt_chunks.png", None), |         ("Tests/images/itxt_chunks.png", None), | ||||||
|     ], |     ], | ||||||
| ) | ) | ||||||
| @pytest.mark.parametrize("protocol", range(0, pickle.HIGHEST_PROTOCOL + 1)) | @pytest.mark.parametrize("protocol", range(pickle.HIGHEST_PROTOCOL + 1)) | ||||||
| def test_pickle_image( | def test_pickle_image( | ||||||
|     tmp_path: Path, test_file: str, test_mode: str | None, protocol: int |     tmp_path: Path, test_file: str, test_mode: str | None, protocol: int | ||||||
| ) -> None: | ) -> None: | ||||||
|  | @ -81,18 +81,19 @@ 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 | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def test_pickle_la_mode_with_palette(tmp_path: Path) -> None: | def test_pickle_la_mode_with_palette(tmp_path: Path) -> None: | ||||||
|     # Arrange |     # Arrange | ||||||
|     filename = str(tmp_path / "temp.pkl") |     filename = tmp_path / "temp.pkl" | ||||||
|     with Image.open("Tests/images/hopper.jpg") as im: |     with Image.open("Tests/images/hopper.jpg") as im: | ||||||
|         im = im.convert("PA") |         im = im.convert("PA") | ||||||
| 
 | 
 | ||||||
|     # Act / Assert |     # Act / Assert | ||||||
|     for protocol in range(0, pickle.HIGHEST_PROTOCOL + 1): |     for protocol in range(pickle.HIGHEST_PROTOCOL + 1): | ||||||
|         im._mode = "LA" |         im._mode = "LA" | ||||||
|         with open(filename, "wb") as f: |         with open(filename, "wb") as f: | ||||||
|             pickle.dump(im, f, protocol) |             pickle.dump(im, f, protocol) | ||||||
|  | @ -133,7 +134,7 @@ def helper_assert_pickled_font_images( | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @skip_unless_feature("freetype2") | @skip_unless_feature("freetype2") | ||||||
| @pytest.mark.parametrize("protocol", list(range(0, pickle.HIGHEST_PROTOCOL + 1))) | @pytest.mark.parametrize("protocol", list(range(pickle.HIGHEST_PROTOCOL + 1))) | ||||||
| def test_pickle_font_string(protocol: int) -> None: | def test_pickle_font_string(protocol: int) -> None: | ||||||
|     # Arrange |     # Arrange | ||||||
|     font = ImageFont.truetype(FONT_PATH, FONT_SIZE) |     font = ImageFont.truetype(FONT_PATH, FONT_SIZE) | ||||||
|  | @ -147,11 +148,11 @@ def test_pickle_font_string(protocol: int) -> None: | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @skip_unless_feature("freetype2") | @skip_unless_feature("freetype2") | ||||||
| @pytest.mark.parametrize("protocol", list(range(0, pickle.HIGHEST_PROTOCOL + 1))) | @pytest.mark.parametrize("protocol", list(range(pickle.HIGHEST_PROTOCOL + 1))) | ||||||
| def test_pickle_font_file(tmp_path: Path, protocol: int) -> None: | def test_pickle_font_file(tmp_path: Path, protocol: int) -> None: | ||||||
|     # Arrange |     # Arrange | ||||||
|     font = ImageFont.truetype(FONT_PATH, FONT_SIZE) |     font = ImageFont.truetype(FONT_PATH, FONT_SIZE) | ||||||
|     filename = str(tmp_path / "temp.pkl") |     filename = tmp_path / "temp.pkl" | ||||||
| 
 | 
 | ||||||
|     # Act: roundtrip |     # Act: roundtrip | ||||||
|     with open(filename, "wb") as f: |     with open(filename, "wb") as f: | ||||||
|  |  | ||||||
Some files were not shown because too many files have changed in this diff Show More
		Loading…
	
		Reference in New Issue
	
	Block a user