mirror of
https://github.com/python-pillow/Pillow.git
synced 2025-04-11 12:54:13 +03:00
Merge branch 'main' into register_handler
This commit is contained in:
commit
f405b1e4c3
|
@ -1,99 +0,0 @@
|
|||
skip_commits:
|
||||
files:
|
||||
- ".github/**/*"
|
||||
- ".gitmodules"
|
||||
- "docs/**/*"
|
||||
- "wheels/**/*"
|
||||
|
||||
version: '{build}'
|
||||
clone_folder: c:\pillow
|
||||
init:
|
||||
- ECHO %PYTHON%
|
||||
#- ps: iex ((new-object net.webclient).DownloadString('https://raw.githubusercontent.com/appveyor/ci/master/scripts/enable-rdp.ps1'))
|
||||
# Uncomment previous line to get RDP access during the build.
|
||||
|
||||
environment:
|
||||
COVERAGE_CORE: sysmon
|
||||
EXECUTABLE: python.exe
|
||||
TEST_OPTIONS:
|
||||
DEPLOY: YES
|
||||
matrix:
|
||||
- PYTHON: C:/Python313
|
||||
ARCHITECTURE: x86
|
||||
APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2022
|
||||
- PYTHON: C:/Python39-x64
|
||||
ARCHITECTURE: AMD64
|
||||
APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2019
|
||||
|
||||
|
||||
install:
|
||||
- '%PYTHON%\%EXECUTABLE% --version'
|
||||
- '%PYTHON%\%EXECUTABLE% -m pip install --upgrade pip'
|
||||
- curl -fsSL -o pillow-test-images.zip https://github.com/python-pillow/test-images/archive/main.zip
|
||||
- 7z x pillow-test-images.zip -oc:\
|
||||
- xcopy /S /Y c:\test-images-main\* c:\pillow\tests\images
|
||||
- curl -fsSL -o nasm-win64.zip https://raw.githubusercontent.com/python-pillow/pillow-depends/main/nasm-2.16.03-win64.zip
|
||||
- 7z x nasm-win64.zip -oc:\
|
||||
- choco install ghostscript --version=10.4.0
|
||||
- path c:\nasm-2.16.03;C:\Program Files\gs\gs10.04.0\bin;%PATH%
|
||||
- cd c:\pillow\winbuild\
|
||||
- ps: |
|
||||
c:\python39\python.exe c:\pillow\winbuild\build_prepare.py -v --depends=C:\pillow-depends\
|
||||
c:\pillow\winbuild\build\build_dep_all.cmd
|
||||
$host.SetShouldExit(0)
|
||||
- path C:\pillow\winbuild\build\bin;%PATH%
|
||||
|
||||
build_script:
|
||||
- cd c:\pillow
|
||||
- winbuild\build\build_env.cmd
|
||||
- '%PYTHON%\%EXECUTABLE% -m pip install -v -C raqm=vendor -C fribidi=vendor .'
|
||||
- '%PYTHON%\%EXECUTABLE% selftest.py --installed'
|
||||
|
||||
test_script:
|
||||
- cd c:\pillow
|
||||
- '%PYTHON%\%EXECUTABLE% -m pip install pytest pytest-cov pytest-timeout defusedxml ipython numpy olefile pyroma'
|
||||
- c:\"Program Files (x86)"\"Windows Kits"\10\Debuggers\x86\gflags.exe /p /enable %PYTHON%\%EXECUTABLE%
|
||||
- path %PYTHON%;%PATH%
|
||||
- .ci\test.cmd
|
||||
|
||||
after_test:
|
||||
- curl -Os https://uploader.codecov.io/latest/windows/codecov.exe
|
||||
- .\codecov.exe --file coverage.xml --name %PYTHON% --flags AppVeyor
|
||||
|
||||
matrix:
|
||||
fast_finish: true
|
||||
|
||||
cache:
|
||||
- '%LOCALAPPDATA%\pip\Cache'
|
||||
|
||||
artifacts:
|
||||
- path: pillow\*.egg
|
||||
name: egg
|
||||
- path: pillow\*.whl
|
||||
name: wheel
|
||||
|
||||
before_deploy:
|
||||
- cd c:\pillow
|
||||
- '%PYTHON%\%EXECUTABLE% -m pip wheel -v -C raqm=vendor -C fribidi=vendor .'
|
||||
- ps: Get-ChildItem .\*.whl | % { Push-AppveyorArtifact $_.FullName -FileName $_.Name }
|
||||
|
||||
deploy:
|
||||
provider: S3
|
||||
region: us-west-2
|
||||
access_key_id: AKIAIRAXC62ZNTVQJMOQ
|
||||
secret_access_key:
|
||||
secure: Hwb6klTqtBeMgxAjRoDltiiqpuH8xbwD4UooDzBSiCWXjuFj1lyl4kHgHwTCCGqi
|
||||
bucket: pillow-nightly
|
||||
folder: win/$(APPVEYOR_BUILD_NUMBER)/
|
||||
artifact: /.*egg|wheel/
|
||||
on:
|
||||
APPVEYOR_REPO_NAME: python-pillow/Pillow
|
||||
branch: main
|
||||
deploy: YES
|
||||
|
||||
|
||||
# Uncomment the following lines to get RDP access after the build/test and block for
|
||||
# up to the timeout limit (~1hr)
|
||||
#
|
||||
#on_finish:
|
||||
#- ps: $blockRdp = $true; iex ((new-object net.webclient).DownloadString('https://raw.githubusercontent.com/appveyor/ci/master/scripts/enable-rdp.ps1'))
|
|
@ -2,8 +2,4 @@
|
|||
|
||||
# gather the coverage data
|
||||
python3 -m pip install coverage
|
||||
if [[ $MATRIX_DOCKER ]]; then
|
||||
python3 -m coverage xml --ignore-errors
|
||||
else
|
||||
python3 -m coverage xml
|
||||
fi
|
||||
python3 -m coverage xml
|
||||
|
|
|
@ -3,8 +3,5 @@
|
|||
set -e
|
||||
|
||||
python3 -m coverage erase
|
||||
if [ $(uname) == "Darwin" ]; then
|
||||
export CPPFLAGS="-I/usr/local/miniconda/include";
|
||||
fi
|
||||
make clean
|
||||
make install-coverage
|
||||
|
|
|
@ -2,12 +2,12 @@
|
|||
|
||||
aptget_update()
|
||||
{
|
||||
if [ ! -z $1 ]; then
|
||||
if [ -n "$1" ]; then
|
||||
echo ""
|
||||
echo "Retrying apt-get update..."
|
||||
echo ""
|
||||
fi
|
||||
output=`sudo apt-get update 2>&1`
|
||||
output=$(sudo apt-get update 2>&1)
|
||||
echo "$output"
|
||||
if [[ $output == *[WE]:\ * ]]; then
|
||||
return 1
|
||||
|
@ -20,8 +20,8 @@ fi
|
|||
set -e
|
||||
|
||||
if [[ $(uname) != CYGWIN* ]]; then
|
||||
sudo apt-get -qq install libfreetype6-dev liblcms2-dev python3-tk\
|
||||
ghostscript libjpeg-turbo-progs libopenjp2-7-dev\
|
||||
sudo apt-get -qq install libfreetype6-dev liblcms2-dev libtiff-dev python3-tk\
|
||||
ghostscript libjpeg-turbo8-dev libopenjp2-7-dev\
|
||||
cmake meson imagemagick libharfbuzz-dev libfribidi-dev\
|
||||
sway wl-clipboard libopenblas-dev
|
||||
fi
|
||||
|
|
|
@ -1 +1 @@
|
|||
cibuildwheel==2.22.0
|
||||
cibuildwheel==2.23.2
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
mypy==1.13.0
|
||||
mypy==1.15.0
|
||||
IceSpringPySideStubs-PyQt6
|
||||
IceSpringPySideStubs-PySide6
|
||||
ipython
|
||||
|
|
4
.github/CONTRIBUTING.md
vendored
4
.github/CONTRIBUTING.md
vendored
|
@ -9,7 +9,7 @@ Please send a pull request to the `main` branch. Please include [documentation](
|
|||
- Fork the Pillow repository.
|
||||
- Create a branch from `main`.
|
||||
- Develop bug fixes, features, tests, etc.
|
||||
- Run the test suite. You can enable GitHub Actions (https://github.com/MY-USERNAME/Pillow/actions) and [AppVeyor](https://ci.appveyor.com/projects/new) on your repo to catch test failures prior to the pull request, and [Codecov](https://codecov.io/gh) to see if the changed code is covered by tests.
|
||||
- Run the test suite. You can enable GitHub Actions (https://github.com/MY-USERNAME/Pillow/actions) on your repo to catch test failures prior to the pull request, and [Codecov](https://codecov.io/gh) to see if the changed code is covered by tests.
|
||||
- Create a pull request to pull the changes from your branch to the Pillow `main`.
|
||||
|
||||
### Guidelines
|
||||
|
@ -17,7 +17,7 @@ Please send a pull request to the `main` branch. Please include [documentation](
|
|||
- Separate code commits from reformatting commits.
|
||||
- Provide tests for any newly added code.
|
||||
- Follow PEP 8.
|
||||
- When committing only documentation changes please include `[ci skip]` in the commit message to avoid running tests on AppVeyor.
|
||||
- When committing only documentation changes please include `[ci skip]` in the commit message to avoid running extra tests.
|
||||
- Include [release notes](https://github.com/python-pillow/Pillow/tree/main/docs/releasenotes) as needed or appropriate with your bug fixes, feature additions and tests.
|
||||
|
||||
## Reporting Issues
|
||||
|
|
1
.github/mergify.yml
vendored
1
.github/mergify.yml
vendored
|
@ -9,7 +9,6 @@ pull_request_rules:
|
|||
- status-success=Windows Test Successful
|
||||
- status-success=MinGW
|
||||
- status-success=Cygwin Test Successful
|
||||
- status-success=continuous-integration/appveyor/pr
|
||||
actions:
|
||||
merge:
|
||||
method: merge
|
||||
|
|
2
.github/renovate.json
vendored
2
.github/renovate.json
vendored
|
@ -16,6 +16,6 @@
|
|||
}
|
||||
],
|
||||
"schedule": [
|
||||
"on the 3rd day of the month"
|
||||
"* * 3 * *"
|
||||
]
|
||||
}
|
||||
|
|
6
.github/workflows/macos-install.sh
vendored
6
.github/workflows/macos-install.sh
vendored
|
@ -10,15 +10,11 @@ brew install \
|
|||
ghostscript \
|
||||
jpeg-turbo \
|
||||
libimagequant \
|
||||
libraqm \
|
||||
libtiff \
|
||||
little-cms2 \
|
||||
openjpeg \
|
||||
webp
|
||||
if [[ "$ImageOS" == "macos13" ]]; then
|
||||
brew install --ignore-dependencies libraqm
|
||||
else
|
||||
brew install libraqm
|
||||
fi
|
||||
export PKG_CONFIG_PATH="/usr/local/opt/openblas/lib/pkgconfig"
|
||||
|
||||
python3 -m pip install coverage
|
||||
|
|
2
.github/workflows/test-cygwin.yml
vendored
2
.github/workflows/test-cygwin.yml
vendored
|
@ -52,7 +52,7 @@ jobs:
|
|||
persist-credentials: false
|
||||
|
||||
- name: Install Cygwin
|
||||
uses: cygwin/cygwin-install-action@v4
|
||||
uses: cygwin/cygwin-install-action@v5
|
||||
with:
|
||||
packages: >
|
||||
gcc-g++
|
||||
|
|
21
.github/workflows/test-docker.yml
vendored
21
.github/workflows/test-docker.yml
vendored
|
@ -29,13 +29,13 @@ concurrency:
|
|||
jobs:
|
||||
build:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: ["ubuntu-latest"]
|
||||
docker: [
|
||||
# Run slower jobs first to give them a headstart and reduce waiting time
|
||||
ubuntu-22.04-jammy-arm64v8,
|
||||
ubuntu-24.04-noble-ppc64le,
|
||||
ubuntu-24.04-noble-s390x,
|
||||
# Then run the remainder
|
||||
|
@ -44,6 +44,7 @@ jobs:
|
|||
amazon-2023-amd64,
|
||||
arch,
|
||||
centos-stream-9-amd64,
|
||||
centos-stream-10-amd64,
|
||||
debian-12-bookworm-x86,
|
||||
debian-12-bookworm-amd64,
|
||||
fedora-40-amd64,
|
||||
|
@ -54,12 +55,13 @@ jobs:
|
|||
]
|
||||
dockerTag: [main]
|
||||
include:
|
||||
- docker: "ubuntu-22.04-jammy-arm64v8"
|
||||
qemu-arch: "aarch64"
|
||||
- docker: "ubuntu-24.04-noble-ppc64le"
|
||||
qemu-arch: "ppc64le"
|
||||
- docker: "ubuntu-24.04-noble-s390x"
|
||||
qemu-arch: "s390x"
|
||||
- docker: "ubuntu-24.04-noble-arm64v8"
|
||||
os: "ubuntu-24.04-arm"
|
||||
dockerTag: main
|
||||
|
||||
name: ${{ matrix.docker }}
|
||||
|
||||
|
@ -73,8 +75,9 @@ jobs:
|
|||
|
||||
- name: Set up QEMU
|
||||
if: "matrix.qemu-arch"
|
||||
run: |
|
||||
docker run --rm --privileged aptman/qus -s -- -p ${{ matrix.qemu-arch }}
|
||||
uses: docker/setup-qemu-action@v3
|
||||
with:
|
||||
platforms: ${{ matrix.qemu-arch }}
|
||||
|
||||
- name: Docker pull
|
||||
run: |
|
||||
|
@ -89,15 +92,15 @@ jobs:
|
|||
|
||||
- name: After success
|
||||
run: |
|
||||
PATH="$PATH:~/.local/bin"
|
||||
docker start pillow_container
|
||||
sudo docker cp pillow_container:/Pillow /Pillow
|
||||
sudo chown -R runner /Pillow
|
||||
pil_path=`docker exec pillow_container /vpy3/bin/python -c 'import os, PIL;print(os.path.realpath(os.path.dirname(PIL.__file__)))'`
|
||||
docker stop pillow_container
|
||||
sudo mkdir -p $pil_path
|
||||
sudo cp src/PIL/*.py $pil_path
|
||||
cd /Pillow
|
||||
.ci/after_success.sh
|
||||
env:
|
||||
MATRIX_DOCKER: ${{ matrix.docker }}
|
||||
|
||||
- name: Upload coverage
|
||||
uses: codecov/codecov-action@v5
|
||||
|
|
6
.github/workflows/test-mingw.yml
vendored
6
.github/workflows/test-mingw.yml
vendored
|
@ -66,9 +66,9 @@ jobs:
|
|||
mingw-w64-x86_64-libtiff \
|
||||
mingw-w64-x86_64-libwebp \
|
||||
mingw-w64-x86_64-openjpeg2 \
|
||||
mingw-w64-x86_64-python3-numpy \
|
||||
mingw-w64-x86_64-python3-olefile \
|
||||
mingw-w64-x86_64-python3-pip \
|
||||
mingw-w64-x86_64-python-numpy \
|
||||
mingw-w64-x86_64-python-olefile \
|
||||
mingw-w64-x86_64-python-pip \
|
||||
mingw-w64-x86_64-python-pytest \
|
||||
mingw-w64-x86_64-python-pytest-cov \
|
||||
mingw-w64-x86_64-python-pytest-timeout \
|
||||
|
|
18
.github/workflows/test-windows.yml
vendored
18
.github/workflows/test-windows.yml
vendored
|
@ -31,15 +31,20 @@ env:
|
|||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: windows-latest
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
python-version: ["pypy3.10", "3.9", "3.10", "3.11", "3.12", "3.13"]
|
||||
python-version: ["pypy3.11", "pypy3.10", "3.10", "3.11", "3.12", "3.13", "3.14"]
|
||||
architecture: ["x64"]
|
||||
os: ["windows-latest"]
|
||||
include:
|
||||
# Test the oldest Python on 32-bit
|
||||
- { python-version: "3.9", architecture: "x86", os: "windows-2019" }
|
||||
|
||||
timeout-minutes: 30
|
||||
|
||||
name: Python ${{ matrix.python-version }}
|
||||
name: Python ${{ matrix.python-version }} (${{ matrix.architecture }})
|
||||
|
||||
steps:
|
||||
- name: Checkout Pillow
|
||||
|
@ -67,6 +72,7 @@ jobs:
|
|||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
allow-prereleases: true
|
||||
architecture: ${{ matrix.architecture }}
|
||||
cache: pip
|
||||
cache-dependency-path: ".github/workflows/test-windows.yml"
|
||||
|
||||
|
@ -78,7 +84,7 @@ jobs:
|
|||
python3 -m pip install --upgrade pip
|
||||
|
||||
- name: Install CPython dependencies
|
||||
if: "!contains(matrix.python-version, 'pypy')"
|
||||
if: "!contains(matrix.python-version, 'pypy') && matrix.architecture != 'x86'"
|
||||
run: |
|
||||
python3 -m pip install PyQt6
|
||||
|
||||
|
@ -88,8 +94,8 @@ jobs:
|
|||
choco install nasm --no-progress
|
||||
echo "C:\Program Files\NASM" >> $env:GITHUB_PATH
|
||||
|
||||
choco install ghostscript --version=10.4.0 --no-progress
|
||||
echo "C:\Program Files\gs\gs10.04.0\bin" >> $env:GITHUB_PATH
|
||||
choco install ghostscript --version=10.5.0 --no-progress
|
||||
echo "C:\Program Files\gs\gs10.05.0\bin" >> $env:GITHUB_PATH
|
||||
|
||||
# Install extra test images
|
||||
xcopy /S /Y Tests\test-images\* Tests\images
|
||||
|
|
4
.github/workflows/test.yml
vendored
4
.github/workflows/test.yml
vendored
|
@ -41,7 +41,9 @@ jobs:
|
|||
"ubuntu-latest",
|
||||
]
|
||||
python-version: [
|
||||
"pypy3.11",
|
||||
"pypy3.10",
|
||||
"3.14",
|
||||
"3.13t",
|
||||
"3.13",
|
||||
"3.12",
|
||||
|
@ -68,7 +70,7 @@ jobs:
|
|||
persist-credentials: false
|
||||
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: Quansight-Labs/setup-python@v5
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
allow-prereleases: true
|
||||
|
|
46
.github/workflows/wheels-dependencies.sh
vendored
46
.github/workflows/wheels-dependencies.sh
vendored
|
@ -37,21 +37,20 @@ fi
|
|||
ARCHIVE_SDIR=pillow-depends-main
|
||||
|
||||
# Package versions for fresh source builds
|
||||
FREETYPE_VERSION=2.13.2
|
||||
HARFBUZZ_VERSION=10.1.0
|
||||
LIBPNG_VERSION=1.6.44
|
||||
FREETYPE_VERSION=2.13.3
|
||||
HARFBUZZ_VERSION=11.0.0
|
||||
LIBPNG_VERSION=1.6.47
|
||||
JPEGTURBO_VERSION=3.1.0
|
||||
OPENJPEG_VERSION=2.5.3
|
||||
XZ_VERSION=5.6.3
|
||||
TIFF_VERSION=4.6.0
|
||||
LCMS2_VERSION=2.16
|
||||
if [[ -n "$IS_MACOS" ]]; then
|
||||
GIFLIB_VERSION=5.2.2
|
||||
if [[ $MB_ML_VER == 2014 ]]; then
|
||||
XZ_VERSION=5.6.4
|
||||
else
|
||||
GIFLIB_VERSION=5.2.1
|
||||
XZ_VERSION=5.8.0
|
||||
fi
|
||||
ZLIB_NG_VERSION=2.2.2
|
||||
LIBWEBP_VERSION=1.4.0
|
||||
TIFF_VERSION=4.7.0
|
||||
LCMS2_VERSION=2.17
|
||||
ZLIB_NG_VERSION=2.2.4
|
||||
LIBWEBP_VERSION=1.5.0
|
||||
BZIP2_VERSION=1.0.8
|
||||
LIBXCB_VERSION=1.17.0
|
||||
BROTLI_VERSION=1.1.0
|
||||
|
@ -59,13 +58,10 @@ BROTLI_VERSION=1.1.0
|
|||
function build_pkg_config {
|
||||
if [ -e pkg-config-stamp ]; then return; fi
|
||||
# This essentially duplicates the Homebrew recipe
|
||||
ORIGINAL_CFLAGS=$CFLAGS
|
||||
CFLAGS="$CFLAGS -Wno-int-conversion"
|
||||
build_simple pkg-config 0.29.2 https://pkg-config.freedesktop.org/releases tar.gz \
|
||||
CFLAGS="$CFLAGS -Wno-int-conversion" build_simple pkg-config 0.29.2 https://pkg-config.freedesktop.org/releases tar.gz \
|
||||
--disable-debug --disable-host-tool --with-internal-glib \
|
||||
--with-pc-path=$BUILD_PREFIX/share/pkgconfig:$BUILD_PREFIX/lib/pkgconfig \
|
||||
--with-system-include-path=$(xcrun --show-sdk-path --sdk macosx)/usr/include
|
||||
CFLAGS=$ORIGINAL_CFLAGS
|
||||
export PKG_CONFIG=$BUILD_PREFIX/bin/pkg-config
|
||||
touch pkg-config-stamp
|
||||
}
|
||||
|
@ -77,6 +73,14 @@ function build_zlib_ng {
|
|||
&& ./configure --prefix=$BUILD_PREFIX --zlib-compat \
|
||||
&& make -j4 \
|
||||
&& make install)
|
||||
|
||||
if [ -n "$IS_MACOS" ]; then
|
||||
# Ensure that on macOS, the library name is an absolute path, not an
|
||||
# @rpath, so that delocate picks up the right library (and doesn't need
|
||||
# DYLD_LIBRARY_PATH to be set). The default Makefile doesn't have an
|
||||
# option to control the install_name.
|
||||
install_name_tool -id $BUILD_PREFIX/lib/libz.1.dylib $BUILD_PREFIX/lib/libz.1.dylib
|
||||
fi
|
||||
touch zlib-stamp
|
||||
}
|
||||
|
||||
|
@ -103,7 +107,7 @@ function build_harfbuzz {
|
|||
|
||||
function build {
|
||||
build_xz
|
||||
if [ -z "$IS_ALPINE" ] && [ -z "$IS_MACOS" ]; then
|
||||
if [ -z "$IS_ALPINE" ] && [ -z "$SANITIZER" ] && [ -z "$IS_MACOS" ]; then
|
||||
yum remove -y zlib-devel
|
||||
fi
|
||||
build_zlib_ng
|
||||
|
@ -135,13 +139,13 @@ function build {
|
|||
build_lcms2
|
||||
build_openjpeg
|
||||
|
||||
ORIGINAL_CFLAGS=$CFLAGS
|
||||
CFLAGS="$CFLAGS -O3 -DNDEBUG"
|
||||
webp_cflags="-O3 -DNDEBUG"
|
||||
if [[ -n "$IS_MACOS" ]]; then
|
||||
CFLAGS="$CFLAGS -Wl,-headerpad_max_install_names"
|
||||
webp_cflags="$webp_cflags -Wl,-headerpad_max_install_names"
|
||||
fi
|
||||
build_libwebp
|
||||
CFLAGS=$ORIGINAL_CFLAGS
|
||||
CFLAGS="$CFLAGS $webp_cflags" build_simple libwebp $LIBWEBP_VERSION \
|
||||
https://storage.googleapis.com/downloads.webmproject.org/releases/webp tar.gz \
|
||||
--enable-libwebpmux --enable-libwebpdemux
|
||||
|
||||
build_brotli
|
||||
|
||||
|
|
3
.github/workflows/wheels-test.ps1
vendored
3
.github/workflows/wheels-test.ps1
vendored
|
@ -11,6 +11,9 @@ if ("$venv" -like "*\cibw-run-*\pp*-win_amd64\*") {
|
|||
$env:path += ";$pillow\winbuild\build\bin\"
|
||||
& "$venv\Scripts\activate.ps1"
|
||||
& reg add "HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Options\python.exe" /v "GlobalFlag" /t REG_SZ /d "0x02000000" /f
|
||||
if ("$venv" -like "*\cibw-run-*-win_amd64\*") {
|
||||
& python -m pip install numpy
|
||||
}
|
||||
cd $pillow
|
||||
& python -VV
|
||||
if (!$?) { exit $LASTEXITCODE }
|
||||
|
|
81
.github/workflows/wheels.yml
vendored
81
.github/workflows/wheels.yml
vendored
|
@ -13,6 +13,7 @@ on:
|
|||
paths:
|
||||
- ".ci/requirements-cibw.txt"
|
||||
- ".github/workflows/wheel*"
|
||||
- "pyproject.toml"
|
||||
- "setup.py"
|
||||
- "wheels/*"
|
||||
- "winbuild/build_prepare.py"
|
||||
|
@ -23,6 +24,7 @@ on:
|
|||
paths:
|
||||
- ".ci/requirements-cibw.txt"
|
||||
- ".github/workflows/wheel*"
|
||||
- "pyproject.toml"
|
||||
- "setup.py"
|
||||
- "wheels/*"
|
||||
- "winbuild/build_prepare.py"
|
||||
|
@ -40,62 +42,7 @@ env:
|
|||
FORCE_COLOR: 1
|
||||
|
||||
jobs:
|
||||
build-1-QEMU-emulated-wheels:
|
||||
if: github.event_name != 'schedule'
|
||||
name: aarch64 ${{ matrix.python-version }} ${{ matrix.spec }}
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
python-version:
|
||||
- pp310
|
||||
- cp3{9,10,11}
|
||||
- cp3{12,13}
|
||||
spec:
|
||||
- manylinux2014
|
||||
- manylinux_2_28
|
||||
- musllinux
|
||||
exclude:
|
||||
- { python-version: pp310, spec: musllinux }
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: true
|
||||
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.x"
|
||||
|
||||
# https://github.com/docker/setup-qemu-action
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Install cibuildwheel
|
||||
run: |
|
||||
python3 -m pip install -r .ci/requirements-cibw.txt
|
||||
|
||||
- name: Build wheels
|
||||
run: |
|
||||
python3 -m cibuildwheel --output-dir wheelhouse
|
||||
env:
|
||||
# Build only the currently selected Linux architecture (so we can
|
||||
# parallelise for speed).
|
||||
CIBW_ARCHS: "aarch64"
|
||||
# Likewise, select only one Python version per job to speed this up.
|
||||
CIBW_BUILD: "${{ matrix.python-version }}-${{ matrix.spec == 'musllinux' && 'musllinux' || 'manylinux' }}*"
|
||||
CIBW_ENABLE: cpython-prerelease
|
||||
# Extra options for manylinux.
|
||||
CIBW_MANYLINUX_AARCH64_IMAGE: ${{ matrix.spec }}
|
||||
CIBW_MANYLINUX_PYPY_AARCH64_IMAGE: ${{ matrix.spec }}
|
||||
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: dist-qemu-${{ matrix.python-version }}-${{ matrix.spec }}
|
||||
path: ./wheelhouse/*.whl
|
||||
|
||||
build-2-native-wheels:
|
||||
build-native-wheels:
|
||||
if: github.event_name != 'schedule' || github.repository_owner == 'python-pillow'
|
||||
name: ${{ matrix.name }}
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
@ -116,7 +63,7 @@ jobs:
|
|||
- name: "macOS 10.15 x86_64"
|
||||
os: macos-13
|
||||
cibw_arch: x86_64
|
||||
build: "pp310*"
|
||||
build: "pp3*"
|
||||
macosx_deployment_target: "10.15"
|
||||
- name: "macOS arm64"
|
||||
os: macos-latest
|
||||
|
@ -130,6 +77,14 @@ jobs:
|
|||
cibw_arch: x86_64
|
||||
build: "*manylinux*"
|
||||
manylinux: "manylinux_2_28"
|
||||
- name: "manylinux2014 and musllinux aarch64"
|
||||
os: ubuntu-24.04-arm
|
||||
cibw_arch: aarch64
|
||||
- name: "manylinux_2_28 aarch64"
|
||||
os: ubuntu-24.04-arm
|
||||
cibw_arch: aarch64
|
||||
build: "*manylinux*"
|
||||
manylinux: "manylinux_2_28"
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
|
@ -150,7 +105,9 @@ jobs:
|
|||
env:
|
||||
CIBW_ARCHS: ${{ matrix.cibw_arch }}
|
||||
CIBW_BUILD: ${{ matrix.build }}
|
||||
CIBW_ENABLE: cpython-prerelease cpython-freethreading
|
||||
CIBW_ENABLE: cpython-prerelease cpython-freethreading pypy
|
||||
CIBW_MANYLINUX_AARCH64_IMAGE: ${{ matrix.manylinux }}
|
||||
CIBW_MANYLINUX_PYPY_AARCH64_IMAGE: ${{ matrix.manylinux }}
|
||||
CIBW_MANYLINUX_PYPY_X86_64_IMAGE: ${{ matrix.manylinux }}
|
||||
CIBW_MANYLINUX_X86_64_IMAGE: ${{ matrix.manylinux }}
|
||||
CIBW_SKIP: pp39-*
|
||||
|
@ -227,7 +184,7 @@ jobs:
|
|||
CIBW_ARCHS: ${{ matrix.cibw_arch }}
|
||||
CIBW_BEFORE_ALL: "{package}\\winbuild\\build\\build_dep_all.cmd"
|
||||
CIBW_CACHE_PATH: "C:\\cibw"
|
||||
CIBW_ENABLE: cpython-prerelease cpython-freethreading
|
||||
CIBW_ENABLE: cpython-prerelease cpython-freethreading pypy
|
||||
CIBW_SKIP: pp39-*
|
||||
CIBW_TEST_SKIP: "*-win_arm64"
|
||||
CIBW_TEST_COMMAND: 'docker run --rm
|
||||
|
@ -263,8 +220,6 @@ jobs:
|
|||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.x"
|
||||
cache: pip
|
||||
cache-dependency-path: "Makefile"
|
||||
|
||||
- run: make sdist
|
||||
|
||||
|
@ -275,7 +230,7 @@ jobs:
|
|||
|
||||
scientific-python-nightly-wheels-publish:
|
||||
if: github.repository_owner == 'python-pillow' && (github.event_name == 'schedule' || github.event_name == 'workflow_dispatch')
|
||||
needs: [build-2-native-wheels, windows]
|
||||
needs: [build-native-wheels, windows]
|
||||
runs-on: ubuntu-latest
|
||||
name: Upload wheels to scientific-python-nightly-wheels
|
||||
steps:
|
||||
|
@ -292,7 +247,7 @@ jobs:
|
|||
|
||||
pypi-publish:
|
||||
if: github.repository_owner == 'python-pillow' && github.event_name == 'push' && startsWith(github.ref, 'refs/tags')
|
||||
needs: [build-1-QEMU-emulated-wheels, build-2-native-wheels, windows, sdist]
|
||||
needs: [build-native-wheels, windows, sdist]
|
||||
runs-on: ubuntu-latest
|
||||
name: Upload release to PyPI
|
||||
environment:
|
||||
|
|
|
@ -1,17 +1,17 @@
|
|||
repos:
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.8.1
|
||||
rev: v0.9.9
|
||||
hooks:
|
||||
- id: ruff
|
||||
args: [--exit-non-zero-on-fix]
|
||||
|
||||
- repo: https://github.com/psf/black-pre-commit-mirror
|
||||
rev: 24.10.0
|
||||
rev: 25.1.0
|
||||
hooks:
|
||||
- id: black
|
||||
|
||||
- repo: https://github.com/PyCQA/bandit
|
||||
rev: 1.8.0
|
||||
rev: 1.8.3
|
||||
hooks:
|
||||
- id: bandit
|
||||
args: [--severity-level=high]
|
||||
|
@ -24,7 +24,7 @@ repos:
|
|||
exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.gd$|\.opt$)
|
||||
|
||||
- repo: https://github.com/pre-commit/mirrors-clang-format
|
||||
rev: v19.1.4
|
||||
rev: v19.1.7
|
||||
hooks:
|
||||
- id: clang-format
|
||||
types: [c]
|
||||
|
@ -50,19 +50,24 @@ repos:
|
|||
exclude: ^.github/.*TEMPLATE|^Tests/(fonts|images)/
|
||||
|
||||
- repo: https://github.com/python-jsonschema/check-jsonschema
|
||||
rev: 0.30.0
|
||||
rev: 0.31.2
|
||||
hooks:
|
||||
- id: check-github-workflows
|
||||
- id: check-readthedocs
|
||||
- id: check-renovate
|
||||
|
||||
- repo: https://github.com/woodruffw/zizmor-pre-commit
|
||||
rev: v1.4.1
|
||||
hooks:
|
||||
- id: zizmor
|
||||
|
||||
- repo: https://github.com/sphinx-contrib/sphinx-lint
|
||||
rev: v1.0.0
|
||||
hooks:
|
||||
- id: sphinx-lint
|
||||
|
||||
- repo: https://github.com/tox-dev/pyproject-fmt
|
||||
rev: v2.5.0
|
||||
rev: v2.5.1
|
||||
hooks:
|
||||
- id: pyproject-fmt
|
||||
|
||||
|
@ -73,7 +78,7 @@ repos:
|
|||
additional_dependencies: [trove-classifiers>=2024.10.12]
|
||||
|
||||
- repo: https://github.com/tox-dev/tox-ini-fmt
|
||||
rev: 1.4.1
|
||||
rev: 1.5.0
|
||||
hooks:
|
||||
- id: tox-ini-fmt
|
||||
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
version: 2
|
||||
|
||||
sphinx:
|
||||
configuration: docs/conf.py
|
||||
|
||||
formats: [pdf]
|
||||
|
||||
build:
|
||||
|
|
|
@ -20,7 +20,6 @@ graft docs
|
|||
graft _custom_build
|
||||
|
||||
# build/src control detritus
|
||||
exclude .appveyor.yml
|
||||
exclude .clang-format
|
||||
exclude .coveragerc
|
||||
exclude .editorconfig
|
||||
|
|
|
@ -42,9 +42,6 @@ As of 2019, Pillow development is
|
|||
<a href="https://github.com/python-pillow/Pillow/actions/workflows/test-docker.yml"><img
|
||||
alt="GitHub Actions build status (Test Docker)"
|
||||
src="https://github.com/python-pillow/Pillow/workflows/Test%20Docker/badge.svg"></a>
|
||||
<a href="https://ci.appveyor.com/project/python-pillow/Pillow"><img
|
||||
alt="AppVeyor CI build status (Windows)"
|
||||
src="https://img.shields.io/appveyor/build/python-pillow/Pillow/main.svg?label=Windows%20build"></a>
|
||||
<a href="https://github.com/python-pillow/Pillow/actions/workflows/wheels.yml"><img
|
||||
alt="GitHub Actions build status (Wheels)"
|
||||
src="https://github.com/python-pillow/Pillow/workflows/Wheels/badge.svg"></a>
|
||||
|
|
|
@ -9,7 +9,7 @@ Released quarterly on January 2nd, April 1st, July 1st and October 15th.
|
|||
|
||||
* [ ] Open a release ticket e.g. https://github.com/python-pillow/Pillow/issues/3154
|
||||
* [ ] Develop and prepare release in `main` branch.
|
||||
* [ ] Check [GitHub Actions](https://github.com/python-pillow/Pillow/actions) and [AppVeyor](https://ci.appveyor.com/project/python-pillow/Pillow) to confirm passing tests in `main` branch.
|
||||
* [ ] Check [GitHub Actions](https://github.com/python-pillow/Pillow/actions) to confirm passing tests in `main` branch.
|
||||
* [ ] Check that all the wheel builds pass the tests in the [GitHub Actions "Wheels" workflow](https://github.com/python-pillow/Pillow/actions/workflows/wheels.yml) jobs by manually triggering them.
|
||||
* [ ] In compliance with [PEP 440](https://peps.python.org/pep-0440/), update version identifier in `src/PIL/_version.py`
|
||||
* [ ] Run pre-release check via `make release-test` in a freshly cloned repo.
|
||||
|
@ -38,7 +38,7 @@ Released as needed for security, installation or critical bug fixes.
|
|||
git checkout -t remotes/origin/5.2.x
|
||||
```
|
||||
* [ ] Cherry pick individual commits from `main` branch to release branch e.g. `5.2.x`, then `git push`.
|
||||
* [ ] Check [GitHub Actions](https://github.com/python-pillow/Pillow/actions) and [AppVeyor](https://ci.appveyor.com/project/python-pillow/Pillow) to confirm passing tests in release branch e.g. `5.2.x`.
|
||||
* [ ] Check [GitHub Actions](https://github.com/python-pillow/Pillow/actions) to confirm passing tests in release branch e.g. `5.2.x`.
|
||||
* [ ] In compliance with [PEP 440](https://peps.python.org/pep-0440/), update version identifier in `src/PIL/_version.py`
|
||||
* [ ] Run pre-release check via `make release-test`.
|
||||
* [ ] Create tag for release e.g.:
|
||||
|
|
|
@ -9,6 +9,6 @@ from PIL import Image
|
|||
|
||||
def test_j2k_overflow(tmp_path: Path) -> None:
|
||||
im = Image.new("RGBA", (1024, 131584))
|
||||
target = str(tmp_path / "temp.jpc")
|
||||
target = tmp_path / "temp.jpc"
|
||||
with pytest.raises(OSError):
|
||||
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:
|
||||
f = str(tmp_path / "temp.png")
|
||||
f = tmp_path / "temp.png"
|
||||
im = Image.new("L", (xdim, ydim), 0)
|
||||
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:
|
||||
dtype = np.uint8
|
||||
a = np.zeros((xdim, ydim), dtype=dtype)
|
||||
f = str(tmp_path / "temp.png")
|
||||
f = tmp_path / "temp.png"
|
||||
im = Image.fromarray(a, "L")
|
||||
im.save(f)
|
||||
|
||||
|
|
|
@ -3,26 +3,25 @@ from __future__ import annotations
|
|||
import zlib
|
||||
from io import BytesIO
|
||||
|
||||
import pytest
|
||||
|
||||
from PIL import Image, ImageFile, PngImagePlugin
|
||||
|
||||
TEST_FILE = "Tests/images/png_decompression_dos.png"
|
||||
|
||||
|
||||
def test_ignore_dos_text() -> None:
|
||||
ImageFile.LOAD_TRUNCATED_IMAGES = True
|
||||
def test_ignore_dos_text(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setattr(ImageFile, "LOAD_TRUNCATED_IMAGES", True)
|
||||
|
||||
try:
|
||||
im = Image.open(TEST_FILE)
|
||||
with Image.open(TEST_FILE) as im:
|
||||
im.load()
|
||||
finally:
|
||||
ImageFile.LOAD_TRUNCATED_IMAGES = False
|
||||
|
||||
assert isinstance(im, PngImagePlugin.PngImageFile)
|
||||
for s in im.text.values():
|
||||
assert len(s) < 1024 * 1024, "Text chunk larger than 1M"
|
||||
assert isinstance(im, PngImagePlugin.PngImageFile)
|
||||
for s in im.text.values():
|
||||
assert len(s) < 1024 * 1024, "Text chunk larger than 1M"
|
||||
|
||||
for s in im.info.values():
|
||||
assert len(s) < 1024 * 1024, "Text chunk larger than 1M"
|
||||
for s in im.info.values():
|
||||
assert len(s) < 1024 * 1024, "Text chunk larger than 1M"
|
||||
|
||||
|
||||
def test_dos_text() -> None:
|
||||
|
|
|
@ -9,11 +9,11 @@ import os
|
|||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import sysconfig
|
||||
import tempfile
|
||||
from collections.abc import Sequence
|
||||
from functools import lru_cache
|
||||
from io import BytesIO
|
||||
from pathlib import Path
|
||||
from typing import Any, Callable
|
||||
|
||||
import pytest
|
||||
|
@ -96,7 +96,10 @@ def assert_image_equal(a: Image.Image, b: Image.Image, msg: str | None = None) -
|
|||
|
||||
|
||||
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:
|
||||
with Image.open(filename) as img:
|
||||
if mode:
|
||||
|
@ -137,21 +140,14 @@ def assert_image_similar(
|
|||
|
||||
def assert_image_similar_tofile(
|
||||
a: Image.Image,
|
||||
filename: str,
|
||||
filename: str | Path,
|
||||
epsilon: float,
|
||||
msg: str | None = None,
|
||||
mode: str | None = None,
|
||||
) -> None:
|
||||
with Image.open(filename) as img:
|
||||
if mode:
|
||||
img = img.convert(mode)
|
||||
assert_image_similar(a, img, epsilon, msg)
|
||||
|
||||
|
||||
def assert_all_same(items: Sequence[Any], msg: str | None = None) -> None:
|
||||
assert items.count(items[0]) == len(items), msg
|
||||
|
||||
|
||||
def assert_not_all_same(items: Sequence[Any], msg: str | None = None) -> None:
|
||||
assert items.count(items[0]) != len(items), msg
|
||||
|
||||
|
@ -327,16 +323,7 @@ def magick_command() -> list[str] | None:
|
|||
return None
|
||||
|
||||
|
||||
def on_appveyor() -> bool:
|
||||
return "APPVEYOR" in os.environ
|
||||
|
||||
|
||||
def on_github_actions() -> bool:
|
||||
return "GITHUB_ACTIONS" in os.environ
|
||||
|
||||
|
||||
def on_ci() -> bool:
|
||||
# GitHub Actions and AppVeyor have "CI"
|
||||
return "CI" in os.environ
|
||||
|
||||
|
||||
|
@ -358,10 +345,6 @@ def is_pypy() -> bool:
|
|||
return hasattr(sys, "pypy_translation_info")
|
||||
|
||||
|
||||
def is_mingw() -> bool:
|
||||
return sysconfig.get_platform() == "mingw"
|
||||
|
||||
|
||||
class CachedProperty:
|
||||
def __init__(self, func: Callable[[Any], Any]) -> None:
|
||||
self.func = func
|
||||
|
|
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: 486 B After Width: | Height: | Size: 533 B |
BIN
Tests/images/jfif_unit_cm.jpg
Normal file
BIN
Tests/images/jfif_unit_cm.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 391 B |
BIN
Tests/images/multiline_text_justify.png
Normal file
BIN
Tests/images/multiline_text_justify.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.2 KiB |
|
@ -7,7 +7,7 @@ import fuzzers
|
|||
import packaging
|
||||
import pytest
|
||||
|
||||
from PIL import Image, UnidentifiedImageError, features
|
||||
from PIL import Image, features
|
||||
from Tests.helper import skip_unless_feature
|
||||
|
||||
if sys.platform.startswith("win32"):
|
||||
|
@ -32,21 +32,17 @@ def test_fuzz_images(path: str) -> None:
|
|||
fuzzers.fuzz_image(f.read())
|
||||
assert True
|
||||
except (
|
||||
# Known exceptions from Pillow
|
||||
OSError,
|
||||
SyntaxError,
|
||||
MemoryError,
|
||||
ValueError,
|
||||
NotImplementedError,
|
||||
OverflowError,
|
||||
):
|
||||
# Known exceptions that are through from Pillow
|
||||
assert True
|
||||
except (
|
||||
# Known Image.* exceptions
|
||||
Image.DecompressionBombError,
|
||||
Image.DecompressionBombWarning,
|
||||
UnidentifiedImageError,
|
||||
):
|
||||
# Known Image.* exceptions
|
||||
assert True
|
||||
finally:
|
||||
fuzzers.disable_decompressionbomb_error()
|
||||
|
|
|
@ -19,7 +19,7 @@ except ImportError:
|
|||
class TestColorLut3DCoreAPI:
|
||||
def generate_identity_table(
|
||||
self, channels: int, size: int | tuple[int, int, int]
|
||||
) -> tuple[int, int, int, int, list[float]]:
|
||||
) -> tuple[int, tuple[int, int, int], list[float]]:
|
||||
if isinstance(size, tuple):
|
||||
size_1d, size_2d, size_3d = size
|
||||
else:
|
||||
|
@ -39,9 +39,7 @@ class TestColorLut3DCoreAPI:
|
|||
]
|
||||
return (
|
||||
channels,
|
||||
size_1d,
|
||||
size_2d,
|
||||
size_3d,
|
||||
(size_1d, size_2d, size_3d),
|
||||
[item for sublist in table for item in sublist],
|
||||
)
|
||||
|
||||
|
@ -89,21 +87,21 @@ class TestColorLut3DCoreAPI:
|
|||
|
||||
with pytest.raises(ValueError, match=r"size1D \* size2D \* size3D"):
|
||||
im.im.color_lut_3d(
|
||||
"RGB", Image.Resampling.BILINEAR, 3, 2, 2, 2, [0, 0, 0] * 7
|
||||
"RGB", Image.Resampling.BILINEAR, 3, (2, 2, 2), [0, 0, 0] * 7
|
||||
)
|
||||
|
||||
with pytest.raises(ValueError, match=r"size1D \* size2D \* size3D"):
|
||||
im.im.color_lut_3d(
|
||||
"RGB", Image.Resampling.BILINEAR, 3, 2, 2, 2, [0, 0, 0] * 9
|
||||
"RGB", Image.Resampling.BILINEAR, 3, (2, 2, 2), [0, 0, 0] * 9
|
||||
)
|
||||
|
||||
with pytest.raises(TypeError):
|
||||
im.im.color_lut_3d(
|
||||
"RGB", Image.Resampling.BILINEAR, 3, 2, 2, 2, [0, 0, "0"] * 8
|
||||
"RGB", Image.Resampling.BILINEAR, 3, (2, 2, 2), [0, 0, "0"] * 8
|
||||
)
|
||||
|
||||
with pytest.raises(TypeError):
|
||||
im.im.color_lut_3d("RGB", Image.Resampling.BILINEAR, 3, 2, 2, 2, 16)
|
||||
im.im.color_lut_3d("RGB", Image.Resampling.BILINEAR, 3, (2, 2, 2), 16)
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"lut_mode, table_channels, table_size",
|
||||
|
@ -264,7 +262,7 @@ class TestColorLut3DCoreAPI:
|
|||
assert_image_equal(
|
||||
Image.merge('RGB', im.split()[::-1]),
|
||||
im._new(im.im.color_lut_3d('RGB', Image.Resampling.BILINEAR,
|
||||
3, 2, 2, 2, [
|
||||
3, (2, 2, 2), [
|
||||
0, 0, 0, 0, 0, 1,
|
||||
0, 1, 0, 0, 1, 1,
|
||||
|
||||
|
@ -286,7 +284,7 @@ class TestColorLut3DCoreAPI:
|
|||
|
||||
# fmt: off
|
||||
transformed = im._new(im.im.color_lut_3d('RGB', Image.Resampling.BILINEAR,
|
||||
3, 2, 2, 2,
|
||||
3, (2, 2, 2),
|
||||
[
|
||||
-1, -1, -1, 2, -1, -1,
|
||||
-1, 2, -1, 2, 2, -1,
|
||||
|
@ -307,7 +305,7 @@ class TestColorLut3DCoreAPI:
|
|||
|
||||
# fmt: off
|
||||
transformed = im._new(im.im.color_lut_3d('RGB', Image.Resampling.BILINEAR,
|
||||
3, 2, 2, 2,
|
||||
3, (2, 2, 2),
|
||||
[
|
||||
-3, -3, -3, 5, -3, -3,
|
||||
-3, 5, -3, 5, 5, -3,
|
||||
|
@ -388,10 +386,12 @@ class TestColorLut3DFilter:
|
|||
|
||||
table = numpy.ones((7 * 6 * 5, 3), dtype=numpy.float16)
|
||||
lut = ImageFilter.Color3DLUT((5, 6, 7), table)
|
||||
assert isinstance(lut.table, numpy.ndarray)
|
||||
assert lut.table.shape == (table.size,)
|
||||
|
||||
table = numpy.ones((7 * 6 * 5 * 3), dtype=numpy.float16)
|
||||
lut = ImageFilter.Color3DLUT((5, 6, 7), table)
|
||||
assert isinstance(lut.table, numpy.ndarray)
|
||||
assert lut.table.shape == (table.size,)
|
||||
|
||||
# Check application
|
||||
|
|
|
@ -12,19 +12,16 @@ ORIGINAL_LIMIT = Image.MAX_IMAGE_PIXELS
|
|||
|
||||
|
||||
class TestDecompressionBomb:
|
||||
def teardown_method(self) -> None:
|
||||
Image.MAX_IMAGE_PIXELS = ORIGINAL_LIMIT
|
||||
|
||||
def test_no_warning_small_file(self) -> None:
|
||||
# Implicit assert: no warning.
|
||||
# A warning would cause a failure.
|
||||
with Image.open(TEST_FILE):
|
||||
pass
|
||||
|
||||
def test_no_warning_no_limit(self) -> None:
|
||||
def test_no_warning_no_limit(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
# Arrange
|
||||
# Turn limit off
|
||||
Image.MAX_IMAGE_PIXELS = None
|
||||
monkeypatch.setattr(Image, "MAX_IMAGE_PIXELS", None)
|
||||
assert Image.MAX_IMAGE_PIXELS is None
|
||||
|
||||
# Act / Assert
|
||||
|
@ -33,18 +30,18 @@ class TestDecompressionBomb:
|
|||
with Image.open(TEST_FILE):
|
||||
pass
|
||||
|
||||
def test_warning(self) -> None:
|
||||
def test_warning(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
# Set limit to trigger warning on the test file
|
||||
Image.MAX_IMAGE_PIXELS = 128 * 128 - 1
|
||||
monkeypatch.setattr(Image, "MAX_IMAGE_PIXELS", 128 * 128 - 1)
|
||||
assert Image.MAX_IMAGE_PIXELS == 128 * 128 - 1
|
||||
|
||||
with pytest.warns(Image.DecompressionBombWarning):
|
||||
with Image.open(TEST_FILE):
|
||||
pass
|
||||
|
||||
def test_exception(self) -> None:
|
||||
def test_exception(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
# Set limit to trigger exception on the test file
|
||||
Image.MAX_IMAGE_PIXELS = 64 * 128 - 1
|
||||
monkeypatch.setattr(Image, "MAX_IMAGE_PIXELS", 64 * 128 - 1)
|
||||
assert Image.MAX_IMAGE_PIXELS == 64 * 128 - 1
|
||||
|
||||
with pytest.raises(Image.DecompressionBombError):
|
||||
|
@ -66,9 +63,9 @@ class TestDecompressionBomb:
|
|||
with pytest.raises(Image.DecompressionBombError):
|
||||
im.seek(1)
|
||||
|
||||
def test_exception_gif_zero_width(self) -> None:
|
||||
def test_exception_gif_zero_width(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
# Set limit to trigger exception on the test file
|
||||
Image.MAX_IMAGE_PIXELS = 4 * 64 * 128
|
||||
monkeypatch.setattr(Image, "MAX_IMAGE_PIXELS", 4 * 64 * 128)
|
||||
assert Image.MAX_IMAGE_PIXELS == 4 * 64 * 128
|
||||
|
||||
with pytest.raises(Image.DecompressionBombError):
|
||||
|
|
|
@ -12,6 +12,7 @@ from PIL import Image, ImageSequence, PngImagePlugin
|
|||
# (referenced from https://wiki.mozilla.org/APNG_Specification)
|
||||
def test_apng_basic() -> None:
|
||||
with Image.open("Tests/images/apng/single_frame.png") as im:
|
||||
assert isinstance(im, PngImagePlugin.PngImageFile)
|
||||
assert not im.is_animated
|
||||
assert im.n_frames == 1
|
||||
assert im.get_format_mimetype() == "image/apng"
|
||||
|
@ -20,6 +21,7 @@ def test_apng_basic() -> None:
|
|||
assert im.getpixel((64, 32)) == (0, 255, 0, 255)
|
||||
|
||||
with Image.open("Tests/images/apng/single_frame_default.png") as im:
|
||||
assert isinstance(im, PngImagePlugin.PngImageFile)
|
||||
assert im.is_animated
|
||||
assert im.n_frames == 2
|
||||
assert im.get_format_mimetype() == "image/apng"
|
||||
|
@ -34,8 +36,11 @@ def test_apng_basic() -> None:
|
|||
with pytest.raises(EOFError):
|
||||
im.seek(2)
|
||||
|
||||
# test rewind support
|
||||
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((64, 32)) == (255, 0, 0, 255)
|
||||
im.seek(1)
|
||||
|
@ -49,6 +54,7 @@ def test_apng_basic() -> None:
|
|||
)
|
||||
def test_apng_fdat(filename: str) -> None:
|
||||
with Image.open(filename) as im:
|
||||
assert isinstance(im, PngImagePlugin.PngImageFile)
|
||||
im.seek(im.n_frames - 1)
|
||||
assert im.getpixel((0, 0)) == (0, 255, 0, 255)
|
||||
assert im.getpixel((64, 32)) == (0, 255, 0, 255)
|
||||
|
@ -56,31 +62,37 @@ def test_apng_fdat(filename: str) -> None:
|
|||
|
||||
def test_apng_dispose() -> None:
|
||||
with Image.open("Tests/images/apng/dispose_op_none.png") as im:
|
||||
assert isinstance(im, PngImagePlugin.PngImageFile)
|
||||
im.seek(im.n_frames - 1)
|
||||
assert im.getpixel((0, 0)) == (0, 255, 0, 255)
|
||||
assert im.getpixel((64, 32)) == (0, 255, 0, 255)
|
||||
|
||||
with Image.open("Tests/images/apng/dispose_op_background.png") as im:
|
||||
assert isinstance(im, PngImagePlugin.PngImageFile)
|
||||
im.seek(im.n_frames - 1)
|
||||
assert im.getpixel((0, 0)) == (0, 0, 0, 0)
|
||||
assert im.getpixel((64, 32)) == (0, 0, 0, 0)
|
||||
|
||||
with Image.open("Tests/images/apng/dispose_op_background_final.png") as im:
|
||||
assert isinstance(im, PngImagePlugin.PngImageFile)
|
||||
im.seek(im.n_frames - 1)
|
||||
assert im.getpixel((0, 0)) == (0, 255, 0, 255)
|
||||
assert im.getpixel((64, 32)) == (0, 255, 0, 255)
|
||||
|
||||
with Image.open("Tests/images/apng/dispose_op_previous.png") as im:
|
||||
assert isinstance(im, PngImagePlugin.PngImageFile)
|
||||
im.seek(im.n_frames - 1)
|
||||
assert im.getpixel((0, 0)) == (0, 255, 0, 255)
|
||||
assert im.getpixel((64, 32)) == (0, 255, 0, 255)
|
||||
|
||||
with Image.open("Tests/images/apng/dispose_op_previous_final.png") as im:
|
||||
assert isinstance(im, PngImagePlugin.PngImageFile)
|
||||
im.seek(im.n_frames - 1)
|
||||
assert im.getpixel((0, 0)) == (0, 255, 0, 255)
|
||||
assert im.getpixel((64, 32)) == (0, 255, 0, 255)
|
||||
|
||||
with Image.open("Tests/images/apng/dispose_op_previous_first.png") as im:
|
||||
assert isinstance(im, PngImagePlugin.PngImageFile)
|
||||
im.seek(im.n_frames - 1)
|
||||
assert im.getpixel((0, 0)) == (0, 0, 0, 0)
|
||||
assert im.getpixel((64, 32)) == (0, 0, 0, 0)
|
||||
|
@ -88,21 +100,25 @@ def test_apng_dispose() -> None:
|
|||
|
||||
def test_apng_dispose_region() -> None:
|
||||
with Image.open("Tests/images/apng/dispose_op_none_region.png") as im:
|
||||
assert isinstance(im, PngImagePlugin.PngImageFile)
|
||||
im.seek(im.n_frames - 1)
|
||||
assert im.getpixel((0, 0)) == (0, 255, 0, 255)
|
||||
assert im.getpixel((64, 32)) == (0, 255, 0, 255)
|
||||
|
||||
with Image.open("Tests/images/apng/dispose_op_background_before_region.png") as im:
|
||||
assert isinstance(im, PngImagePlugin.PngImageFile)
|
||||
im.seek(im.n_frames - 1)
|
||||
assert im.getpixel((0, 0)) == (0, 0, 0, 0)
|
||||
assert im.getpixel((64, 32)) == (0, 0, 0, 0)
|
||||
|
||||
with Image.open("Tests/images/apng/dispose_op_background_region.png") as im:
|
||||
assert isinstance(im, PngImagePlugin.PngImageFile)
|
||||
im.seek(im.n_frames - 1)
|
||||
assert im.getpixel((0, 0)) == (0, 0, 255, 255)
|
||||
assert im.getpixel((64, 32)) == (0, 0, 0, 0)
|
||||
|
||||
with Image.open("Tests/images/apng/dispose_op_previous_region.png") as im:
|
||||
assert isinstance(im, PngImagePlugin.PngImageFile)
|
||||
im.seek(im.n_frames - 1)
|
||||
assert im.getpixel((0, 0)) == (0, 255, 0, 255)
|
||||
assert im.getpixel((64, 32)) == (0, 255, 0, 255)
|
||||
|
@ -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:
|
||||
assert isinstance(im, PngImagePlugin.PngImageFile)
|
||||
im.seek(im.n_frames - 1)
|
||||
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:
|
||||
with Image.open("Tests/images/apng/blend_op_source_solid.png") as im:
|
||||
assert isinstance(im, PngImagePlugin.PngImageFile)
|
||||
im.seek(im.n_frames - 1)
|
||||
assert im.getpixel((0, 0)) == (0, 255, 0, 255)
|
||||
assert im.getpixel((64, 32)) == (0, 255, 0, 255)
|
||||
|
||||
with Image.open("Tests/images/apng/blend_op_source_transparent.png") as im:
|
||||
assert isinstance(im, PngImagePlugin.PngImageFile)
|
||||
im.seek(im.n_frames - 1)
|
||||
assert im.getpixel((0, 0)) == (0, 0, 0, 0)
|
||||
assert im.getpixel((64, 32)) == (0, 0, 0, 0)
|
||||
|
||||
with Image.open("Tests/images/apng/blend_op_source_near_transparent.png") as im:
|
||||
assert isinstance(im, PngImagePlugin.PngImageFile)
|
||||
im.seek(im.n_frames - 1)
|
||||
assert im.getpixel((0, 0)) == (0, 255, 0, 2)
|
||||
assert im.getpixel((64, 32)) == (0, 255, 0, 2)
|
||||
|
||||
with Image.open("Tests/images/apng/blend_op_over.png") as im:
|
||||
assert isinstance(im, PngImagePlugin.PngImageFile)
|
||||
im.seek(im.n_frames - 1)
|
||||
assert im.getpixel((0, 0)) == (0, 255, 0, 255)
|
||||
assert im.getpixel((64, 32)) == (0, 255, 0, 255)
|
||||
|
||||
with Image.open("Tests/images/apng/blend_op_over_near_transparent.png") as im:
|
||||
assert isinstance(im, PngImagePlugin.PngImageFile)
|
||||
im.seek(im.n_frames - 1)
|
||||
assert im.getpixel((0, 0)) == (0, 255, 0, 97)
|
||||
assert im.getpixel((64, 32)) == (0, 255, 0, 255)
|
||||
|
@ -175,6 +197,7 @@ def test_apng_blend_transparency() -> None:
|
|||
|
||||
def test_apng_chunk_order() -> None:
|
||||
with Image.open("Tests/images/apng/fctl_actl.png") as im:
|
||||
assert isinstance(im, PngImagePlugin.PngImageFile)
|
||||
im.seek(im.n_frames - 1)
|
||||
assert im.getpixel((0, 0)) == (0, 255, 0, 255)
|
||||
assert im.getpixel((64, 32)) == (0, 255, 0, 255)
|
||||
|
@ -230,24 +253,28 @@ def test_apng_num_plays() -> None:
|
|||
|
||||
def test_apng_mode() -> None:
|
||||
with Image.open("Tests/images/apng/mode_16bit.png") as im:
|
||||
assert isinstance(im, PngImagePlugin.PngImageFile)
|
||||
assert im.mode == "RGBA"
|
||||
im.seek(im.n_frames - 1)
|
||||
assert im.getpixel((0, 0)) == (0, 0, 128, 191)
|
||||
assert im.getpixel((64, 32)) == (0, 0, 128, 191)
|
||||
|
||||
with Image.open("Tests/images/apng/mode_grayscale.png") as im:
|
||||
assert isinstance(im, PngImagePlugin.PngImageFile)
|
||||
assert im.mode == "L"
|
||||
im.seek(im.n_frames - 1)
|
||||
assert im.getpixel((0, 0)) == 128
|
||||
assert im.getpixel((64, 32)) == 255
|
||||
|
||||
with Image.open("Tests/images/apng/mode_grayscale_alpha.png") as im:
|
||||
assert isinstance(im, PngImagePlugin.PngImageFile)
|
||||
assert im.mode == "LA"
|
||||
im.seek(im.n_frames - 1)
|
||||
assert im.getpixel((0, 0)) == (128, 191)
|
||||
assert im.getpixel((64, 32)) == (128, 191)
|
||||
|
||||
with Image.open("Tests/images/apng/mode_palette.png") as im:
|
||||
assert isinstance(im, PngImagePlugin.PngImageFile)
|
||||
assert im.mode == "P"
|
||||
im.seek(im.n_frames - 1)
|
||||
im = im.convert("RGB")
|
||||
|
@ -255,6 +282,7 @@ def test_apng_mode() -> None:
|
|||
assert im.getpixel((64, 32)) == (0, 255, 0)
|
||||
|
||||
with Image.open("Tests/images/apng/mode_palette_alpha.png") as im:
|
||||
assert isinstance(im, PngImagePlugin.PngImageFile)
|
||||
assert im.mode == "P"
|
||||
im.seek(im.n_frames - 1)
|
||||
im = im.convert("RGBA")
|
||||
|
@ -262,6 +290,7 @@ def test_apng_mode() -> None:
|
|||
assert im.getpixel((64, 32)) == (0, 255, 0, 255)
|
||||
|
||||
with Image.open("Tests/images/apng/mode_palette_1bit_alpha.png") as im:
|
||||
assert isinstance(im, PngImagePlugin.PngImageFile)
|
||||
assert im.mode == "P"
|
||||
im.seek(im.n_frames - 1)
|
||||
im = im.convert("RGBA")
|
||||
|
@ -271,25 +300,31 @@ def test_apng_mode() -> None:
|
|||
|
||||
def test_apng_chunk_errors() -> None:
|
||||
with Image.open("Tests/images/apng/chunk_no_actl.png") as im:
|
||||
assert isinstance(im, PngImagePlugin.PngImageFile)
|
||||
assert not im.is_animated
|
||||
|
||||
with pytest.warns(UserWarning):
|
||||
with Image.open("Tests/images/apng/chunk_multi_actl.png") as im:
|
||||
im.load()
|
||||
assert isinstance(im, PngImagePlugin.PngImageFile)
|
||||
assert not im.is_animated
|
||||
|
||||
with Image.open("Tests/images/apng/chunk_actl_after_idat.png") as im:
|
||||
assert isinstance(im, PngImagePlugin.PngImageFile)
|
||||
assert not im.is_animated
|
||||
|
||||
with Image.open("Tests/images/apng/chunk_no_fctl.png") as im:
|
||||
assert isinstance(im, PngImagePlugin.PngImageFile)
|
||||
with pytest.raises(SyntaxError):
|
||||
im.seek(im.n_frames - 1)
|
||||
|
||||
with Image.open("Tests/images/apng/chunk_repeat_fctl.png") as im:
|
||||
assert isinstance(im, PngImagePlugin.PngImageFile)
|
||||
with pytest.raises(SyntaxError):
|
||||
im.seek(im.n_frames - 1)
|
||||
|
||||
with Image.open("Tests/images/apng/chunk_no_fdat.png") as im:
|
||||
assert isinstance(im, PngImagePlugin.PngImageFile)
|
||||
with pytest.raises(SyntaxError):
|
||||
im.seek(im.n_frames - 1)
|
||||
|
||||
|
@ -297,31 +332,31 @@ def test_apng_chunk_errors() -> None:
|
|||
def test_apng_syntax_errors() -> None:
|
||||
with pytest.warns(UserWarning):
|
||||
with Image.open("Tests/images/apng/syntax_num_frames_zero.png") as im:
|
||||
assert isinstance(im, PngImagePlugin.PngImageFile)
|
||||
assert not im.is_animated
|
||||
with pytest.raises(OSError):
|
||||
im.load()
|
||||
|
||||
with pytest.warns(UserWarning):
|
||||
with Image.open("Tests/images/apng/syntax_num_frames_zero_default.png") as im:
|
||||
assert isinstance(im, PngImagePlugin.PngImageFile)
|
||||
assert not im.is_animated
|
||||
im.load()
|
||||
|
||||
# we can handle this case gracefully
|
||||
exception = None
|
||||
with Image.open("Tests/images/apng/syntax_num_frames_low.png") as im:
|
||||
try:
|
||||
im.seek(im.n_frames - 1)
|
||||
except Exception as e:
|
||||
exception = e
|
||||
assert exception is None
|
||||
assert isinstance(im, PngImagePlugin.PngImageFile)
|
||||
im.seek(im.n_frames - 1)
|
||||
|
||||
with pytest.raises(OSError):
|
||||
with Image.open("Tests/images/apng/syntax_num_frames_high.png") as im:
|
||||
assert isinstance(im, PngImagePlugin.PngImageFile)
|
||||
im.seek(im.n_frames - 1)
|
||||
im.load()
|
||||
|
||||
with pytest.warns(UserWarning):
|
||||
with Image.open("Tests/images/apng/syntax_num_frames_invalid.png") as im:
|
||||
assert isinstance(im, PngImagePlugin.PngImageFile)
|
||||
assert not im.is_animated
|
||||
im.load()
|
||||
|
||||
|
@ -341,16 +376,18 @@ def test_apng_syntax_errors() -> None:
|
|||
def test_apng_sequence_errors(test_file: str) -> None:
|
||||
with pytest.raises(SyntaxError):
|
||||
with Image.open(f"Tests/images/apng/{test_file}") as im:
|
||||
assert isinstance(im, PngImagePlugin.PngImageFile)
|
||||
im.seek(im.n_frames - 1)
|
||||
im.load()
|
||||
|
||||
|
||||
def test_apng_save(tmp_path: Path) -> None:
|
||||
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)
|
||||
|
||||
with Image.open(test_file) as im:
|
||||
assert isinstance(im, PngImagePlugin.PngImageFile)
|
||||
im.load()
|
||||
assert not im.is_animated
|
||||
assert im.n_frames == 1
|
||||
|
@ -366,6 +403,7 @@ def test_apng_save(tmp_path: Path) -> None:
|
|||
)
|
||||
|
||||
with Image.open(test_file) as im:
|
||||
assert isinstance(im, PngImagePlugin.PngImageFile)
|
||||
im.load()
|
||||
assert im.is_animated
|
||||
assert im.n_frames == 2
|
||||
|
@ -377,7 +415,7 @@ def test_apng_save(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))
|
||||
im2 = Image.new("RGBA", (1, 1), (255, 0, 0, 127))
|
||||
|
@ -395,7 +433,7 @@ def test_apng_save_split_fdat(tmp_path: Path) -> None:
|
|||
# frames with image data spanning multiple fdAT chunks (in this case
|
||||
# both the default image and first animation frame will span multiple
|
||||
# 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:
|
||||
frames = [im.copy(), Image.new("RGBA", im.size, (255, 0, 0, 255))]
|
||||
im.save(
|
||||
|
@ -405,17 +443,13 @@ def test_apng_save_split_fdat(tmp_path: Path) -> None:
|
|||
append_images=frames,
|
||||
)
|
||||
with Image.open(test_file) as im:
|
||||
exception = None
|
||||
try:
|
||||
im.seek(im.n_frames - 1)
|
||||
im.load()
|
||||
except Exception as e:
|
||||
exception = e
|
||||
assert exception is None
|
||||
assert isinstance(im, PngImagePlugin.PngImageFile)
|
||||
im.seek(im.n_frames - 1)
|
||||
im.load()
|
||||
|
||||
|
||||
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:
|
||||
frames = []
|
||||
durations = []
|
||||
|
@ -452,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]
|
||||
)
|
||||
with Image.open(test_file) as im:
|
||||
assert isinstance(im, PngImagePlugin.PngImageFile)
|
||||
assert im.n_frames == 1
|
||||
assert "duration" not in im.info
|
||||
|
||||
|
@ -463,6 +498,7 @@ def test_apng_save_duration_loop(tmp_path: Path) -> None:
|
|||
duration=[500, 100, 150],
|
||||
)
|
||||
with Image.open(test_file) as im:
|
||||
assert isinstance(im, PngImagePlugin.PngImageFile)
|
||||
assert im.n_frames == 2
|
||||
assert im.info["duration"] == 600
|
||||
|
||||
|
@ -473,12 +509,13 @@ def test_apng_save_duration_loop(tmp_path: Path) -> None:
|
|||
frame.info["duration"] = 300
|
||||
frame.save(test_file, save_all=True, append_images=[frame, different_frame])
|
||||
with Image.open(test_file) as im:
|
||||
assert isinstance(im, PngImagePlugin.PngImageFile)
|
||||
assert im.n_frames == 2
|
||||
assert im.info["duration"] == 600
|
||||
|
||||
|
||||
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)
|
||||
red = Image.new("RGBA", size, (255, 0, 0, 255))
|
||||
green = Image.new("RGBA", size, (0, 255, 0, 255))
|
||||
|
@ -579,7 +616,7 @@ def test_apng_save_disposal(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)
|
||||
blue = Image.new("RGBA", size, (0, 0, 255, 255))
|
||||
red = Image.new("RGBA", size, (255, 0, 0, 255))
|
||||
|
@ -601,7 +638,7 @@ def test_apng_save_disposal_previous(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)
|
||||
red = Image.new("RGBA", size, (255, 0, 0, 255))
|
||||
green = Image.new("RGBA", size, (0, 255, 0, 255))
|
||||
|
@ -669,7 +706,7 @@ def test_apng_save_blend(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.save(test_file, save_all=True, append_images=[Image.new("L", (200, 200))])
|
||||
|
@ -693,7 +730,7 @@ def test_seek_after_close() -> None:
|
|||
def test_different_modes_in_later_frames(
|
||||
mode: str, default_image: bool, duplicate: bool, tmp_path: Path
|
||||
) -> None:
|
||||
test_file = str(tmp_path / "temp.png")
|
||||
test_file = tmp_path / "temp.png"
|
||||
|
||||
im = Image.new("L", (1, 1))
|
||||
im.save(
|
||||
|
@ -707,7 +744,7 @@ def test_different_modes_in_later_frames(
|
|||
|
||||
|
||||
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:
|
||||
for _ in range(3):
|
||||
|
|
|
@ -4,7 +4,7 @@ from pathlib import Path
|
|||
|
||||
import pytest
|
||||
|
||||
from PIL import Image
|
||||
from PIL import BlpImagePlugin, Image
|
||||
|
||||
from .helper import (
|
||||
assert_image_equal,
|
||||
|
@ -19,6 +19,7 @@ def test_load_blp1() -> None:
|
|||
assert_image_equal_tofile(im, "Tests/images/blp/blp1_jpeg.png")
|
||||
|
||||
with Image.open("Tests/images/blp/blp1_jpeg2.blp") as im:
|
||||
assert im.mode == "RGBA"
|
||||
im.load()
|
||||
|
||||
|
||||
|
@ -37,8 +38,15 @@ def test_load_blp2_dxt1a() -> None:
|
|||
assert_image_equal_tofile(im, "Tests/images/blp/blp2_dxt1a.png")
|
||||
|
||||
|
||||
def test_invalid_file() -> None:
|
||||
invalid_file = "Tests/images/flower.jpg"
|
||||
|
||||
with pytest.raises(BlpImagePlugin.BLPFormatError):
|
||||
BlpImagePlugin.BlpImageFile(invalid_file)
|
||||
|
||||
|
||||
def test_save(tmp_path: Path) -> None:
|
||||
f = str(tmp_path / "temp.blp")
|
||||
f = tmp_path / "temp.blp"
|
||||
|
||||
for version in ("BLP1", "BLP2"):
|
||||
im = hopper("P")
|
||||
|
@ -48,7 +56,7 @@ def test_save(tmp_path: Path) -> None:
|
|||
assert_image_equal(im.convert("RGB"), reloaded)
|
||||
|
||||
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)
|
||||
|
||||
with Image.open(f) as reloaded:
|
||||
|
|
|
@ -15,25 +15,19 @@ from .helper import (
|
|||
)
|
||||
|
||||
|
||||
def test_sanity(tmp_path: Path) -> None:
|
||||
def roundtrip(im: Image.Image) -> None:
|
||||
outfile = str(tmp_path / "temp.bmp")
|
||||
@pytest.mark.parametrize("mode", ("1", "L", "P", "RGB"))
|
||||
def test_sanity(mode: str, tmp_path: Path) -> None:
|
||||
outfile = tmp_path / "temp.bmp"
|
||||
|
||||
im.save(outfile, "BMP")
|
||||
im = hopper(mode)
|
||||
im.save(outfile, "BMP")
|
||||
|
||||
with Image.open(outfile) as reloaded:
|
||||
reloaded.load()
|
||||
assert im.mode == reloaded.mode
|
||||
assert im.size == reloaded.size
|
||||
assert reloaded.format == "BMP"
|
||||
assert reloaded.get_format_mimetype() == "image/bmp"
|
||||
|
||||
roundtrip(hopper())
|
||||
|
||||
roundtrip(hopper("1"))
|
||||
roundtrip(hopper("L"))
|
||||
roundtrip(hopper("P"))
|
||||
roundtrip(hopper("RGB"))
|
||||
with Image.open(outfile) as reloaded:
|
||||
reloaded.load()
|
||||
assert im.mode == reloaded.mode
|
||||
assert im.size == reloaded.size
|
||||
assert reloaded.format == "BMP"
|
||||
assert reloaded.get_format_mimetype() == "image/bmp"
|
||||
|
||||
|
||||
def test_invalid_file() -> None:
|
||||
|
@ -66,7 +60,7 @@ def test_small_palette(tmp_path: Path) -> None:
|
|||
colors = [0, 0, 0, 125, 125, 125, 255, 255, 255]
|
||||
im.putpalette(colors)
|
||||
|
||||
out = str(tmp_path / "temp.bmp")
|
||||
out = tmp_path / "temp.bmp"
|
||||
im.save(out)
|
||||
|
||||
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:
|
||||
outfile = str(tmp_path / "temp.bmp")
|
||||
outfile = tmp_path / "temp.bmp"
|
||||
with Image.new("RGB", (1, 1)) as im:
|
||||
im._size = (37838, 37838)
|
||||
with pytest.raises(ValueError):
|
||||
|
@ -96,7 +90,7 @@ def test_dpi() -> None:
|
|||
def test_save_bmp_with_dpi(tmp_path: Path) -> None:
|
||||
# Test for #1301
|
||||
# Arrange
|
||||
outfile = str(tmp_path / "temp.jpg")
|
||||
outfile = tmp_path / "temp.jpg"
|
||||
with Image.open("Tests/images/hopper.bmp") as im:
|
||||
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:
|
||||
outfile = str(tmp_path / "temp.bmp")
|
||||
outfile = tmp_path / "temp.bmp"
|
||||
with Image.open("Tests/images/hopper.bmp") as im:
|
||||
im.save(outfile, dpi=(72.21216100543306, 72.21216100543306))
|
||||
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:
|
||||
outfile = str(tmp_path / "temp.dib")
|
||||
outfile = tmp_path / "temp.dib"
|
||||
|
||||
with Image.open("Tests/images/clipboard.dib") as im:
|
||||
im.save(outfile)
|
||||
|
@ -230,3 +224,13 @@ def test_offset() -> None:
|
|||
# to exclude the palette size from the pixel data offset
|
||||
with Image.open("Tests/images/pal8_offset.bmp") as im:
|
||||
assert_image_equal_tofile(im, "Tests/images/bmp/g/pal8.bmp")
|
||||
|
||||
|
||||
def test_use_raw_alpha(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
with Image.open("Tests/images/bmp/g/rgb32.bmp") as im:
|
||||
assert im.info["compression"] == BmpImagePlugin.BmpImageFile.COMPRESSIONS["RAW"]
|
||||
assert im.mode == "RGB"
|
||||
|
||||
monkeypatch.setattr(BmpImagePlugin, "USE_RAW_ALPHA", True)
|
||||
with Image.open("Tests/images/bmp/g/rgb32.bmp") as im:
|
||||
assert im.mode == "RGBA"
|
||||
|
|
|
@ -43,7 +43,7 @@ def test_load() -> None:
|
|||
def test_save(tmp_path: Path) -> None:
|
||||
# Arrange
|
||||
im = hopper()
|
||||
tmpfile = str(tmp_path / "temp.bufr")
|
||||
tmpfile = tmp_path / "temp.bufr"
|
||||
|
||||
# Act / Assert: stub cannot save without an implemented handler
|
||||
with pytest.raises(OSError):
|
||||
|
@ -82,8 +82,8 @@ def test_handler(tmp_path: Path) -> None:
|
|||
im.load()
|
||||
assert handler.is_loaded()
|
||||
|
||||
temp_file = str(tmp_path / "temp.bufr")
|
||||
temp_file = tmp_path / "temp.bufr"
|
||||
im.save(temp_file)
|
||||
assert handler.saved
|
||||
|
||||
BufrStubImagePlugin._handler = None
|
||||
BufrStubImagePlugin.register_handler(None)
|
||||
|
|
|
@ -4,8 +4,6 @@ import pytest
|
|||
|
||||
from PIL import ContainerIO, Image
|
||||
|
||||
from .helper import hopper
|
||||
|
||||
TEST_FILE = "Tests/images/dummy.container"
|
||||
|
||||
|
||||
|
@ -15,15 +13,15 @@ def test_sanity() -> None:
|
|||
|
||||
|
||||
def test_isatty() -> None:
|
||||
with hopper() as im:
|
||||
container = ContainerIO.ContainerIO(im, 0, 0)
|
||||
with open(TEST_FILE, "rb") as fh:
|
||||
container = ContainerIO.ContainerIO(fh, 0, 0)
|
||||
|
||||
assert container.isatty() is False
|
||||
|
||||
|
||||
def test_seekable() -> None:
|
||||
with hopper() as im:
|
||||
container = ContainerIO.ContainerIO(im, 0, 0)
|
||||
with open(TEST_FILE, "rb") as fh:
|
||||
container = ContainerIO.ContainerIO(fh, 0, 0)
|
||||
|
||||
assert container.seekable() is True
|
||||
|
||||
|
|
|
@ -26,12 +26,12 @@ def test_sanity() -> None:
|
|||
|
||||
@pytest.mark.skipif(is_pypy(), reason="Requires CPython")
|
||||
def test_unclosed_file() -> None:
|
||||
def open() -> None:
|
||||
def open_test_image() -> None:
|
||||
im = Image.open(TEST_FILE)
|
||||
im.load()
|
||||
|
||||
with pytest.warns(ResourceWarning):
|
||||
open()
|
||||
open_test_image()
|
||||
|
||||
|
||||
def test_closed_file() -> None:
|
||||
|
@ -69,12 +69,14 @@ def test_tell() -> None:
|
|||
|
||||
def test_n_frames() -> None:
|
||||
with Image.open(TEST_FILE) as im:
|
||||
assert isinstance(im, DcxImagePlugin.DcxImageFile)
|
||||
assert im.n_frames == 1
|
||||
assert not im.is_animated
|
||||
|
||||
|
||||
def test_eoferror() -> None:
|
||||
with Image.open(TEST_FILE) as im:
|
||||
assert isinstance(im, DcxImagePlugin.DcxImageFile)
|
||||
n_frames = im.n_frames
|
||||
|
||||
# Test seeking past the last frame
|
||||
|
|
|
@ -9,7 +9,13 @@ import pytest
|
|||
|
||||
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_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"))
|
||||
|
||||
|
||||
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(
|
||||
"image_path",
|
||||
(
|
||||
|
@ -331,11 +363,13 @@ def test_dxt5_colorblock_alpha_issue_4142() -> None:
|
|||
|
||||
with Image.open("Tests/images/dxt5-colorblock-alpha-issue-4142.dds") as im:
|
||||
px = im.getpixel((0, 0))
|
||||
assert isinstance(px, tuple)
|
||||
assert px[0] != 0
|
||||
assert px[1] != 0
|
||||
assert px[2] != 0
|
||||
|
||||
px = im.getpixel((1, 0))
|
||||
assert isinstance(px, tuple)
|
||||
assert px[0] != 0
|
||||
assert px[1] != 0
|
||||
assert px[2] != 0
|
||||
|
@ -366,9 +400,9 @@ def test_not_implemented(test_file: str) -> None:
|
|||
|
||||
|
||||
def test_save_unsupported_mode(tmp_path: Path) -> None:
|
||||
out = str(tmp_path / "temp.dds")
|
||||
out = tmp_path / "temp.dds"
|
||||
im = hopper("HSV")
|
||||
with pytest.raises(OSError):
|
||||
with pytest.raises(OSError, match="cannot write mode HSV as DDS"):
|
||||
im.save(out)
|
||||
|
||||
|
||||
|
@ -382,10 +416,98 @@ def test_save_unsupported_mode(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:
|
||||
assert im.mode == mode
|
||||
im.save(out)
|
||||
|
||||
with Image.open(out) as reloaded:
|
||||
assert_image_equal(im, reloaded)
|
||||
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:
|
||||
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:
|
||||
expected_size = tuple(s * scale for s in size)
|
||||
with Image.open(filename) as image:
|
||||
assert isinstance(image, EpsImagePlugin.EpsImageFile)
|
||||
|
||||
image.load(scale=scale)
|
||||
assert image.mode == "RGB"
|
||||
assert image.size == expected_size
|
||||
|
@ -95,10 +97,14 @@ def test_sanity(filename: str, size: tuple[int, int], scale: int) -> None:
|
|||
@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available")
|
||||
def test_load() -> None:
|
||||
with Image.open(FILE1) as im:
|
||||
assert im.load()[0, 0] == (255, 255, 255)
|
||||
px = im.load()
|
||||
assert px is not None
|
||||
assert px[0, 0] == (255, 255, 255)
|
||||
|
||||
# Test again now that it has already been loaded once
|
||||
assert im.load()[0, 0] == (255, 255, 255)
|
||||
px = im.load()
|
||||
assert px is not None
|
||||
assert px[0, 0] == (255, 255, 255)
|
||||
|
||||
|
||||
def test_binary() -> None:
|
||||
|
@ -223,6 +229,8 @@ def test_showpage() -> None:
|
|||
@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available")
|
||||
def test_transparency() -> None:
|
||||
with Image.open("Tests/images/eps/reqd_showpage.eps") as plot_image:
|
||||
assert isinstance(plot_image, EpsImagePlugin.EpsImageFile)
|
||||
|
||||
plot_image.load(transparency=True)
|
||||
assert plot_image.mode == "RGBA"
|
||||
|
||||
|
@ -235,7 +243,7 @@ def test_transparency() -> None:
|
|||
def test_file_object(tmp_path: Path) -> None:
|
||||
# issue 479
|
||||
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")
|
||||
|
||||
|
||||
|
@ -270,7 +278,7 @@ def test_1(filename: str) -> None:
|
|||
|
||||
def test_image_mode_not_supported(tmp_path: Path) -> None:
|
||||
im = hopper("RGBA")
|
||||
tmpfile = str(tmp_path / "temp.eps")
|
||||
tmpfile = tmp_path / "temp.eps"
|
||||
with pytest.raises(ValueError):
|
||||
im.save(tmpfile)
|
||||
|
||||
|
@ -304,6 +312,7 @@ def test_render_scale2() -> None:
|
|||
|
||||
# Zero bounding box
|
||||
with Image.open(FILE1) as image1_scale2:
|
||||
assert isinstance(image1_scale2, EpsImagePlugin.EpsImageFile)
|
||||
image1_scale2.load(scale=2)
|
||||
with Image.open(FILE1_COMPARE_SCALE2) as image1_scale2_compare:
|
||||
image1_scale2_compare = image1_scale2_compare.convert("RGB")
|
||||
|
@ -312,6 +321,7 @@ def test_render_scale2() -> None:
|
|||
|
||||
# Non-zero bounding box
|
||||
with Image.open(FILE2) as image2_scale2:
|
||||
assert isinstance(image2_scale2, EpsImagePlugin.EpsImageFile)
|
||||
image2_scale2.load(scale=2)
|
||||
with Image.open(FILE2_COMPARE_SCALE2) as image2_scale2_compare:
|
||||
image2_scale2_compare = image2_scale2_compare.convert("RGB")
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import io
|
||||
import warnings
|
||||
|
||||
import pytest
|
||||
|
@ -21,6 +22,8 @@ animated_test_file_with_prefix_chunk = "Tests/images/2422.flc"
|
|||
|
||||
def test_sanity() -> None:
|
||||
with Image.open(static_test_file) as im:
|
||||
assert isinstance(im, FliImagePlugin.FliImageFile)
|
||||
|
||||
im.load()
|
||||
assert im.mode == "P"
|
||||
assert im.size == (128, 128)
|
||||
|
@ -28,6 +31,8 @@ def test_sanity() -> None:
|
|||
assert not im.is_animated
|
||||
|
||||
with Image.open(animated_test_file) as im:
|
||||
assert isinstance(im, FliImagePlugin.FliImageFile)
|
||||
|
||||
assert im.mode == "P"
|
||||
assert im.size == (320, 200)
|
||||
assert im.format == "FLI"
|
||||
|
@ -35,32 +40,29 @@ def test_sanity() -> None:
|
|||
assert im.is_animated
|
||||
|
||||
|
||||
def test_prefix_chunk() -> None:
|
||||
ImageFile.LOAD_TRUNCATED_IMAGES = True
|
||||
try:
|
||||
with Image.open(animated_test_file_with_prefix_chunk) as im:
|
||||
assert im.mode == "P"
|
||||
assert im.size == (320, 200)
|
||||
assert im.format == "FLI"
|
||||
assert im.info["duration"] == 171
|
||||
assert im.is_animated
|
||||
def test_prefix_chunk(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setattr(ImageFile, "LOAD_TRUNCATED_IMAGES", True)
|
||||
with Image.open(animated_test_file_with_prefix_chunk) as im:
|
||||
assert im.mode == "P"
|
||||
assert im.size == (320, 200)
|
||||
assert im.format == "FLI"
|
||||
assert im.info["duration"] == 171
|
||||
assert im.is_animated
|
||||
|
||||
palette = im.getpalette()
|
||||
assert palette[3:6] == [255, 255, 255]
|
||||
assert palette[381:384] == [204, 204, 12]
|
||||
assert palette[765:] == [252, 0, 0]
|
||||
finally:
|
||||
ImageFile.LOAD_TRUNCATED_IMAGES = False
|
||||
palette = im.getpalette()
|
||||
assert palette[3:6] == [255, 255, 255]
|
||||
assert palette[381:384] == [204, 204, 12]
|
||||
assert palette[765:] == [252, 0, 0]
|
||||
|
||||
|
||||
@pytest.mark.skipif(is_pypy(), reason="Requires CPython")
|
||||
def test_unclosed_file() -> None:
|
||||
def open() -> None:
|
||||
def open_test_image() -> None:
|
||||
im = Image.open(static_test_file)
|
||||
im.load()
|
||||
|
||||
with pytest.warns(ResourceWarning):
|
||||
open()
|
||||
open_test_image()
|
||||
|
||||
|
||||
def test_closed_file() -> None:
|
||||
|
@ -114,16 +116,19 @@ def test_palette_chunk_second() -> None:
|
|||
|
||||
def test_n_frames() -> None:
|
||||
with Image.open(static_test_file) as im:
|
||||
assert isinstance(im, FliImagePlugin.FliImageFile)
|
||||
assert im.n_frames == 1
|
||||
assert not im.is_animated
|
||||
|
||||
with Image.open(animated_test_file) as im:
|
||||
assert isinstance(im, FliImagePlugin.FliImageFile)
|
||||
assert im.n_frames == 384
|
||||
assert im.is_animated
|
||||
|
||||
|
||||
def test_eoferror() -> None:
|
||||
with Image.open(animated_test_file) as im:
|
||||
assert isinstance(im, FliImagePlugin.FliImageFile)
|
||||
n_frames = im.n_frames
|
||||
|
||||
# Test seeking past the last frame
|
||||
|
@ -135,6 +140,15 @@ def test_eoferror() -> None:
|
|||
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:
|
||||
with Image.open(animated_test_file) as im:
|
||||
layer_number = im.tell()
|
||||
|
@ -159,10 +173,14 @@ def test_seek_tell() -> None:
|
|||
|
||||
def test_seek() -> None:
|
||||
with Image.open(animated_test_file) as im:
|
||||
assert isinstance(im, FliImagePlugin.FliImageFile)
|
||||
im.seek(50)
|
||||
|
||||
assert_image_equal_tofile(im, "Tests/images/a_fli.png")
|
||||
|
||||
with pytest.raises(ValueError, match="cannot seek to frame 52"):
|
||||
im._seek(52)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"test_file",
|
||||
|
|
|
@ -22,10 +22,11 @@ def test_sanity() -> None:
|
|||
|
||||
def test_close() -> None:
|
||||
with Image.open("Tests/images/input_bw_one_band.fpx") as im:
|
||||
pass
|
||||
assert isinstance(im, FpxImagePlugin.FpxImageFile)
|
||||
assert im.ole.fp.closed
|
||||
|
||||
im = Image.open("Tests/images/input_bw_one_band.fpx")
|
||||
assert isinstance(im, FpxImagePlugin.FpxImageFile)
|
||||
im.close()
|
||||
assert im.ole.fp.closed
|
||||
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import io
|
||||
import struct
|
||||
|
||||
import pytest
|
||||
|
||||
from PIL import FtexImagePlugin, Image
|
||||
|
@ -23,3 +26,15 @@ def test_invalid_file() -> None:
|
|||
|
||||
with pytest.raises(SyntaxError):
|
||||
FtexImagePlugin.FtexImageFile(invalid_file)
|
||||
|
||||
|
||||
def test_invalid_texture() -> None:
|
||||
with open("Tests/images/ftex_dxt1.ftc", "rb") as fp:
|
||||
data = fp.read()
|
||||
|
||||
# Change texture compression format
|
||||
data = data[:24] + struct.pack("<i", 2) + data[28:]
|
||||
|
||||
with pytest.raises(ValueError, match="Invalid texture compression format: 2"):
|
||||
with Image.open(io.BytesIO(data)):
|
||||
pass
|
||||
|
|
|
@ -14,10 +14,14 @@ def test_gbr_file() -> None:
|
|||
|
||||
def test_load() -> None:
|
||||
with Image.open("Tests/images/gbr.gbr") as im:
|
||||
assert im.load()[0, 0] == (0, 0, 0, 0)
|
||||
px = im.load()
|
||||
assert px is not None
|
||||
assert px[0, 0] == (0, 0, 0, 0)
|
||||
|
||||
# Test again now that it has already been loaded once
|
||||
assert im.load()[0, 0] == (0, 0, 0, 0)
|
||||
px = im.load()
|
||||
assert px is not None
|
||||
assert px[0, 0] == (0, 0, 0, 0)
|
||||
|
||||
|
||||
def test_multiple_load_operations() -> None:
|
||||
|
|
|
@ -4,6 +4,8 @@ import pytest
|
|||
|
||||
from PIL import GdImageFile, UnidentifiedImageError
|
||||
|
||||
from .helper import assert_image_similar_tofile
|
||||
|
||||
TEST_GD_FILE = "Tests/images/hopper.gd"
|
||||
|
||||
|
||||
|
@ -11,6 +13,7 @@ def test_sanity() -> None:
|
|||
with GdImageFile.open(TEST_GD_FILE) as im:
|
||||
assert im.size == (128, 128)
|
||||
assert im.format == "GD"
|
||||
assert_image_similar_tofile(im.convert("RGB"), "Tests/images/hopper.jpg", 14)
|
||||
|
||||
|
||||
def test_bad_mode() -> None:
|
||||
|
|
|
@ -22,9 +22,6 @@ from .helper import (
|
|||
# sample gif stream
|
||||
TEST_GIF = "Tests/images/hopper.gif"
|
||||
|
||||
with open(TEST_GIF, "rb") as f:
|
||||
data = f.read()
|
||||
|
||||
|
||||
def test_sanity() -> None:
|
||||
with Image.open(TEST_GIF) as im:
|
||||
|
@ -37,12 +34,12 @@ def test_sanity() -> None:
|
|||
|
||||
@pytest.mark.skipif(is_pypy(), reason="Requires CPython")
|
||||
def test_unclosed_file() -> None:
|
||||
def open() -> None:
|
||||
def open_test_image() -> None:
|
||||
im = Image.open(TEST_GIF)
|
||||
im.load()
|
||||
|
||||
with pytest.warns(ResourceWarning):
|
||||
open()
|
||||
open_test_image()
|
||||
|
||||
|
||||
def test_closed_file() -> None:
|
||||
|
@ -86,12 +83,12 @@ def test_invalid_file() -> None:
|
|||
def test_l_mode_transparency() -> None:
|
||||
with Image.open("Tests/images/no_palette_with_transparency.gif") as im:
|
||||
assert im.mode == "L"
|
||||
assert im.load()[0, 0] == 128
|
||||
assert im.getpixel((0, 0)) == 128
|
||||
assert im.info["transparency"] == 255
|
||||
|
||||
im.seek(1)
|
||||
assert im.mode == "L"
|
||||
assert im.load()[0, 0] == 128
|
||||
assert im.getpixel((0, 0)) == 128
|
||||
|
||||
|
||||
def test_l_mode_after_rgb() -> None:
|
||||
|
@ -109,7 +106,7 @@ def test_palette_not_needed_for_second_frame() -> None:
|
|||
assert_image_similar(im, hopper("L").convert("RGB"), 8)
|
||||
|
||||
|
||||
def test_strategy() -> None:
|
||||
def test_strategy(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
with Image.open("Tests/images/iss634.gif") as im:
|
||||
expected_rgb_always = im.convert("RGB")
|
||||
|
||||
|
@ -119,35 +116,36 @@ def test_strategy() -> None:
|
|||
im.seek(1)
|
||||
expected_different = im.convert("RGB")
|
||||
|
||||
try:
|
||||
GifImagePlugin.LOADING_STRATEGY = GifImagePlugin.LoadingStrategy.RGB_ALWAYS
|
||||
with Image.open("Tests/images/iss634.gif") as im:
|
||||
assert im.mode == "RGB"
|
||||
assert_image_equal(im, expected_rgb_always)
|
||||
monkeypatch.setattr(
|
||||
GifImagePlugin, "LOADING_STRATEGY", GifImagePlugin.LoadingStrategy.RGB_ALWAYS
|
||||
)
|
||||
with Image.open("Tests/images/iss634.gif") as im:
|
||||
assert im.mode == "RGB"
|
||||
assert_image_equal(im, expected_rgb_always)
|
||||
|
||||
with Image.open("Tests/images/chi.gif") as im:
|
||||
assert im.mode == "RGBA"
|
||||
assert_image_equal(im, expected_rgb_always_rgba)
|
||||
with Image.open("Tests/images/chi.gif") as im:
|
||||
assert im.mode == "RGBA"
|
||||
assert_image_equal(im, expected_rgb_always_rgba)
|
||||
|
||||
GifImagePlugin.LOADING_STRATEGY = (
|
||||
GifImagePlugin.LoadingStrategy.RGB_AFTER_DIFFERENT_PALETTE_ONLY
|
||||
)
|
||||
# Stay in P mode with only a global palette
|
||||
with Image.open("Tests/images/chi.gif") as im:
|
||||
assert im.mode == "P"
|
||||
monkeypatch.setattr(
|
||||
GifImagePlugin,
|
||||
"LOADING_STRATEGY",
|
||||
GifImagePlugin.LoadingStrategy.RGB_AFTER_DIFFERENT_PALETTE_ONLY,
|
||||
)
|
||||
# Stay in P mode with only a global palette
|
||||
with Image.open("Tests/images/chi.gif") as im:
|
||||
assert im.mode == "P"
|
||||
|
||||
im.seek(1)
|
||||
assert im.mode == "P"
|
||||
assert_image_equal(im.convert("RGB"), expected_different)
|
||||
im.seek(1)
|
||||
assert im.mode == "P"
|
||||
assert_image_equal(im.convert("RGB"), expected_different)
|
||||
|
||||
# Change to RGB mode when a frame has an individual palette
|
||||
with Image.open("Tests/images/iss634.gif") as im:
|
||||
assert im.mode == "P"
|
||||
# Change to RGB mode when a frame has an individual palette
|
||||
with Image.open("Tests/images/iss634.gif") as im:
|
||||
assert im.mode == "P"
|
||||
|
||||
im.seek(1)
|
||||
assert im.mode == "RGB"
|
||||
finally:
|
||||
GifImagePlugin.LOADING_STRATEGY = GifImagePlugin.LoadingStrategy.RGB_AFTER_FIRST
|
||||
im.seek(1)
|
||||
assert im.mode == "RGB"
|
||||
|
||||
|
||||
def test_optimize() -> None:
|
||||
|
@ -230,7 +228,7 @@ def test_optimize_if_palette_can_be_reduced_by_half() -> 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))
|
||||
|
||||
full_palette_im = Image.new("P", (1, 256))
|
||||
|
@ -251,7 +249,7 @@ def test_full_palette_second_frame(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.save(out)
|
||||
with Image.open(out) as reread:
|
||||
|
@ -260,7 +258,7 @@ def test_roundtrip(tmp_path: Path) -> None:
|
|||
|
||||
def test_roundtrip2(tmp_path: Path) -> None:
|
||||
# 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:
|
||||
im2 = im.copy()
|
||||
im2.save(out)
|
||||
|
@ -270,7 +268,7 @@ def test_roundtrip2(tmp_path: Path) -> None:
|
|||
|
||||
def test_roundtrip_save_all(tmp_path: Path) -> None:
|
||||
# Single frame image
|
||||
out = str(tmp_path / "temp.gif")
|
||||
out = tmp_path / "temp.gif"
|
||||
im = hopper()
|
||||
im.save(out, save_all=True)
|
||||
with Image.open(out) as reread:
|
||||
|
@ -278,7 +276,7 @@ def test_roundtrip_save_all(tmp_path: Path) -> None:
|
|||
|
||||
# Multiframe image
|
||||
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)
|
||||
|
||||
with Image.open(out) as reread:
|
||||
|
@ -286,7 +284,7 @@ def test_roundtrip_save_all(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))
|
||||
im2 = Image.new("1", (1, 1), 1)
|
||||
im.save(out, save_all=True, append_images=[im2])
|
||||
|
@ -309,8 +307,9 @@ def test_roundtrip_save_all_1(tmp_path: Path) -> None:
|
|||
def test_loading_multiple_palettes(path: str, mode: str) -> None:
|
||||
with Image.open(path) as im:
|
||||
assert im.mode == "P"
|
||||
assert im.palette is not None
|
||||
first_frame_colors = im.palette.colors.keys()
|
||||
original_color = im.convert("RGB").load()[0, 0]
|
||||
original_color = im.convert("RGB").getpixel((0, 0))
|
||||
|
||||
im.seek(1)
|
||||
assert im.mode == mode
|
||||
|
@ -318,10 +317,10 @@ def test_loading_multiple_palettes(path: str, mode: str) -> None:
|
|||
im = im.convert("RGB")
|
||||
|
||||
# Check a color only from the old palette
|
||||
assert im.load()[0, 0] == original_color
|
||||
assert im.getpixel((0, 0)) == original_color
|
||||
|
||||
# Check a color from the new palette
|
||||
assert im.load()[24, 24] not in first_frame_colors
|
||||
assert im.getpixel((24, 24)) not in first_frame_colors
|
||||
|
||||
|
||||
def test_headers_saving_for_animated_gifs(tmp_path: Path) -> None:
|
||||
|
@ -330,7 +329,7 @@ def test_headers_saving_for_animated_gifs(tmp_path: Path) -> None:
|
|||
with Image.open("Tests/images/dispose_bgnd.gif") as im:
|
||||
info = im.info.copy()
|
||||
|
||||
out = str(tmp_path / "temp.gif")
|
||||
out = tmp_path / "temp.gif"
|
||||
im.save(out, save_all=True)
|
||||
with Image.open(out) as reread:
|
||||
for header in important_headers:
|
||||
|
@ -346,7 +345,7 @@ def test_palette_handling(tmp_path: Path) -> None:
|
|||
im = im.resize((100, 100), Image.Resampling.LANCZOS)
|
||||
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)
|
||||
|
||||
with Image.open(f) as reloaded:
|
||||
|
@ -357,7 +356,7 @@ def test_palette_434(tmp_path: Path) -> None:
|
|||
# see https://github.com/python-pillow/Pillow/issues/434
|
||||
|
||||
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)
|
||||
reloaded = Image.open(out)
|
||||
|
||||
|
@ -403,6 +402,7 @@ def test_save_netpbm_l_mode(tmp_path: Path) -> None:
|
|||
|
||||
def test_seek() -> None:
|
||||
with Image.open("Tests/images/dispose_none.gif") as img:
|
||||
assert isinstance(img, GifImagePlugin.GifImageFile)
|
||||
frame_count = 0
|
||||
try:
|
||||
while True:
|
||||
|
@ -411,6 +411,10 @@ def test_seek() -> None:
|
|||
except EOFError:
|
||||
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:
|
||||
with Image.open("Tests/images/iss634.gif") as im:
|
||||
|
@ -443,10 +447,12 @@ def test_seek_rewind() -> None:
|
|||
def test_n_frames(path: str, n_frames: int) -> None:
|
||||
# Test is_animated before n_frames
|
||||
with Image.open(path) as im:
|
||||
assert isinstance(im, GifImagePlugin.GifImageFile)
|
||||
assert im.is_animated == (n_frames != 1)
|
||||
|
||||
# Test is_animated after n_frames
|
||||
with Image.open(path) as im:
|
||||
assert isinstance(im, GifImagePlugin.GifImageFile)
|
||||
assert im.n_frames == n_frames
|
||||
assert im.is_animated == (n_frames != 1)
|
||||
|
||||
|
@ -456,6 +462,7 @@ def test_no_change() -> None:
|
|||
with Image.open("Tests/images/dispose_bgnd.gif") as im:
|
||||
im.seek(1)
|
||||
expected = im.copy()
|
||||
assert isinstance(im, GifImagePlugin.GifImageFile)
|
||||
assert im.n_frames == 5
|
||||
assert_image_equal(im, expected)
|
||||
|
||||
|
@ -463,17 +470,20 @@ def test_no_change() -> None:
|
|||
with Image.open("Tests/images/dispose_bgnd.gif") as im:
|
||||
im.seek(3)
|
||||
expected = im.copy()
|
||||
assert isinstance(im, GifImagePlugin.GifImageFile)
|
||||
assert im.is_animated
|
||||
assert_image_equal(im, expected)
|
||||
|
||||
with Image.open("Tests/images/comment_after_only_frame.gif") as im:
|
||||
expected = Image.new("P", (1, 1))
|
||||
assert isinstance(im, GifImagePlugin.GifImageFile)
|
||||
assert not im.is_animated
|
||||
assert_image_equal(im, expected)
|
||||
|
||||
|
||||
def test_eoferror() -> None:
|
||||
with Image.open(TEST_GIF) as im:
|
||||
assert isinstance(im, GifImagePlugin.GifImageFile)
|
||||
n_frames = im.n_frames
|
||||
|
||||
# Test seeking past the last frame
|
||||
|
@ -487,12 +497,12 @@ def test_eoferror() -> None:
|
|||
|
||||
def test_first_frame_transparency() -> None:
|
||||
with Image.open("Tests/images/first_frame_transparency.gif") as im:
|
||||
px = im.load()
|
||||
assert px[0, 0] == im.info["transparency"]
|
||||
assert im.getpixel((0, 0)) == im.info["transparency"]
|
||||
|
||||
|
||||
def test_dispose_none() -> None:
|
||||
with Image.open("Tests/images/dispose_none.gif") as img:
|
||||
assert isinstance(img, GifImagePlugin.GifImageFile)
|
||||
try:
|
||||
while True:
|
||||
img.seek(img.tell() + 1)
|
||||
|
@ -516,6 +526,7 @@ def test_dispose_none_load_end() -> None:
|
|||
|
||||
def test_dispose_background() -> None:
|
||||
with Image.open("Tests/images/dispose_bgnd.gif") as img:
|
||||
assert isinstance(img, GifImagePlugin.GifImageFile)
|
||||
try:
|
||||
while True:
|
||||
img.seek(img.tell() + 1)
|
||||
|
@ -528,6 +539,7 @@ def test_dispose_background_transparency() -> None:
|
|||
with Image.open("Tests/images/dispose_bgnd_transparency.gif") as img:
|
||||
img.seek(2)
|
||||
px = img.load()
|
||||
assert px is not None
|
||||
assert px[35, 30][3] == 0
|
||||
|
||||
|
||||
|
@ -555,21 +567,20 @@ def test_dispose_background_transparency() -> None:
|
|||
def test_transparent_dispose(
|
||||
loading_strategy: GifImagePlugin.LoadingStrategy,
|
||||
expected_colors: tuple[tuple[int | tuple[int, int, int, int], ...]],
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
GifImagePlugin.LOADING_STRATEGY = loading_strategy
|
||||
try:
|
||||
with Image.open("Tests/images/transparent_dispose.gif") as img:
|
||||
for frame in range(3):
|
||||
img.seek(frame)
|
||||
for x in range(3):
|
||||
color = img.getpixel((x, 0))
|
||||
assert color == expected_colors[frame][x]
|
||||
finally:
|
||||
GifImagePlugin.LOADING_STRATEGY = GifImagePlugin.LoadingStrategy.RGB_AFTER_FIRST
|
||||
monkeypatch.setattr(GifImagePlugin, "LOADING_STRATEGY", loading_strategy)
|
||||
with Image.open("Tests/images/transparent_dispose.gif") as img:
|
||||
for frame in range(3):
|
||||
img.seek(frame)
|
||||
for x in range(3):
|
||||
color = img.getpixel((x, 0))
|
||||
assert color == expected_colors[frame][x]
|
||||
|
||||
|
||||
def test_dispose_previous() -> None:
|
||||
with Image.open("Tests/images/dispose_prev.gif") as img:
|
||||
assert isinstance(img, GifImagePlugin.GifImageFile)
|
||||
try:
|
||||
while True:
|
||||
img.seek(img.tell() + 1)
|
||||
|
@ -598,15 +609,16 @@ def test_previous_frame_loaded() -> None:
|
|||
|
||||
|
||||
def test_save_dispose(tmp_path: Path) -> None:
|
||||
out = str(tmp_path / "temp.gif")
|
||||
out = tmp_path / "temp.gif"
|
||||
im_list = [
|
||||
Image.new("L", (100, 100), "#000"),
|
||||
Image.new("L", (100, 100), "#111"),
|
||||
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)
|
||||
with Image.open(out) as img:
|
||||
assert isinstance(img, GifImagePlugin.GifImageFile)
|
||||
for _ in range(2):
|
||||
img.seek(img.tell() + 1)
|
||||
assert img.disposal_method == method
|
||||
|
@ -620,13 +632,14 @@ def test_save_dispose(tmp_path: Path) -> None:
|
|||
)
|
||||
|
||||
with Image.open(out) as img:
|
||||
assert isinstance(img, GifImagePlugin.GifImageFile)
|
||||
for i in range(2):
|
||||
img.seek(img.tell() + 1)
|
||||
assert img.disposal_method == i + 1
|
||||
|
||||
|
||||
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
|
||||
circles = [(255, 255, 255), (153, 153, 153), (0, 0, 0), (255, 0, 0)]
|
||||
|
@ -660,7 +673,7 @@ def test_dispose2_palette(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
|
||||
circles = [
|
||||
|
@ -702,7 +715,7 @@ def test_dispose2_diff(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 = []
|
||||
|
||||
|
@ -728,7 +741,7 @@ def test_dispose2_background(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))]
|
||||
|
||||
|
@ -742,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)
|
||||
|
||||
with Image.open(out) as im:
|
||||
assert isinstance(im, GifImagePlugin.GifImageFile)
|
||||
assert im.n_frames == 3
|
||||
|
||||
|
||||
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.info["transparency"] = 0
|
||||
|
@ -764,8 +778,23 @@ def test_dispose2_previous_frame(tmp_path: Path) -> None:
|
|||
assert im.getpixel((0, 0)) == (0, 0, 0, 255)
|
||||
|
||||
|
||||
def test_dispose2_without_transparency(tmp_path: Path) -> None:
|
||||
out = tmp_path / "temp.gif"
|
||||
|
||||
im = Image.new("P", (100, 100))
|
||||
|
||||
im2 = Image.new("P", (100, 100), (0, 0, 0))
|
||||
im2.putpixel((50, 50), (255, 0, 0))
|
||||
|
||||
im.save(out, save_all=True, append_images=[im2], disposal=2)
|
||||
|
||||
with Image.open(out) as reloaded:
|
||||
reloaded.seek(1)
|
||||
assert reloaded.tile[0].extents == (0, 0, 100, 100)
|
||||
|
||||
|
||||
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:
|
||||
assert im.info["transparency"] == 0
|
||||
|
||||
|
@ -795,7 +824,7 @@ def test_no_transparency_in_second_frame() -> 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))
|
||||
im2 = im.copy()
|
||||
|
@ -813,7 +842,7 @@ def test_remapped_transparency(tmp_path: Path) -> None:
|
|||
def test_duration(tmp_path: Path) -> None:
|
||||
duration = 1000
|
||||
|
||||
out = str(tmp_path / "temp.gif")
|
||||
out = tmp_path / "temp.gif"
|
||||
im = Image.new("L", (100, 100), "#000")
|
||||
|
||||
# Check that the argument has priority over the info settings
|
||||
|
@ -827,7 +856,7 @@ def test_duration(tmp_path: Path) -> None:
|
|||
def test_multiple_duration(tmp_path: Path) -> None:
|
||||
duration_list = [1000, 2000, 3000]
|
||||
|
||||
out = str(tmp_path / "temp.gif")
|
||||
out = tmp_path / "temp.gif"
|
||||
im_list = [
|
||||
Image.new("L", (100, 100), "#000"),
|
||||
Image.new("L", (100, 100), "#111"),
|
||||
|
@ -862,7 +891,7 @@ def test_multiple_duration(tmp_path: Path) -> None:
|
|||
def test_roundtrip_info_duration(tmp_path: Path) -> None:
|
||||
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:
|
||||
assert [
|
||||
frame.info["duration"] for frame in ImageSequence.Iterator(im)
|
||||
|
@ -877,7 +906,7 @@ def test_roundtrip_info_duration(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:
|
||||
assert [frame.info["duration"] for frame in ImageSequence.Iterator(im)] == [
|
||||
1000,
|
||||
|
@ -895,7 +924,7 @@ def test_roundtrip_info_duration_combined(tmp_path: Path) -> None:
|
|||
def test_identical_frames(tmp_path: Path) -> None:
|
||||
duration_list = [1000, 1500, 2000, 4000]
|
||||
|
||||
out = str(tmp_path / "temp.gif")
|
||||
out = tmp_path / "temp.gif"
|
||||
im_list = [
|
||||
Image.new("L", (100, 100), "#000"),
|
||||
Image.new("L", (100, 100), "#000"),
|
||||
|
@ -908,6 +937,8 @@ def test_identical_frames(tmp_path: Path) -> None:
|
|||
out, save_all=True, append_images=im_list[1:], duration=duration_list
|
||||
)
|
||||
with Image.open(out) as reread:
|
||||
assert isinstance(reread, GifImagePlugin.GifImageFile)
|
||||
|
||||
# Assert that the first three frames were combined
|
||||
assert reread.n_frames == 2
|
||||
|
||||
|
@ -928,7 +959,7 @@ def test_identical_frames(tmp_path: Path) -> None:
|
|||
def test_identical_frames_to_single_frame(
|
||||
duration: int | list[int], tmp_path: Path
|
||||
) -> None:
|
||||
out = str(tmp_path / "temp.gif")
|
||||
out = tmp_path / "temp.gif"
|
||||
im_list = [
|
||||
Image.new("L", (100, 100), "#000"),
|
||||
Image.new("L", (100, 100), "#000"),
|
||||
|
@ -937,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)
|
||||
with Image.open(out) as reread:
|
||||
assert isinstance(reread, GifImagePlugin.GifImageFile)
|
||||
|
||||
# Assert that all frames were combined
|
||||
assert reread.n_frames == 1
|
||||
|
||||
|
@ -945,7 +978,7 @@ def test_identical_frames_to_single_frame(
|
|||
|
||||
|
||||
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.save(out, loop=None)
|
||||
with Image.open(out) as reread:
|
||||
|
@ -955,7 +988,7 @@ def test_loop_none(tmp_path: Path) -> None:
|
|||
def test_number_of_loops(tmp_path: Path) -> None:
|
||||
number_of_loops = 2
|
||||
|
||||
out = str(tmp_path / "temp.gif")
|
||||
out = tmp_path / "temp.gif"
|
||||
im = Image.new("L", (100, 100), "#000")
|
||||
im.save(out, loop=number_of_loops)
|
||||
with Image.open(out) as reread:
|
||||
|
@ -971,7 +1004,7 @@ def test_number_of_loops(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.info["background"] = 1
|
||||
im.save(out)
|
||||
|
@ -980,7 +1013,7 @@ def test_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
|
||||
if features.check("webp"):
|
||||
|
@ -998,7 +1031,7 @@ def test_comment(tmp_path: Path) -> None:
|
|||
with Image.open(TEST_GIF) as im:
|
||||
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.info["comment"] = b"Test comment text"
|
||||
im.save(out)
|
||||
|
@ -1015,7 +1048,7 @@ def test_comment(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")
|
||||
comment = b"Test comment text"
|
||||
while len(comment) < 256:
|
||||
|
@ -1041,7 +1074,7 @@ def test_read_multiple_comment_blocks() -> 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:
|
||||
assert "comment" in im.info
|
||||
|
||||
|
@ -1075,7 +1108,7 @@ def test_retain_comment_in_subsequent_frames(tmp_path: Path) -> None:
|
|||
assert "comment" not in im.info
|
||||
|
||||
# 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:
|
||||
im.save(out, save_all=True, comment="Test")
|
||||
|
||||
|
@ -1085,7 +1118,7 @@ def test_retain_comment_in_subsequent_frames(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:
|
||||
im.save(out)
|
||||
|
@ -1115,7 +1148,7 @@ def test_version(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
|
||||
im = Image.new("RGB", (100, 100), "#f00")
|
||||
|
@ -1123,6 +1156,14 @@ def test_append_images(tmp_path: Path) -> None:
|
|||
im.copy().save(out, save_all=True, append_images=ims)
|
||||
|
||||
with Image.open(out) as reread:
|
||||
assert isinstance(reread, GifImagePlugin.GifImageFile)
|
||||
assert reread.n_frames == 3
|
||||
|
||||
# Test append_images without save_all
|
||||
im.copy().save(out, append_images=ims)
|
||||
|
||||
with Image.open(out) as reread:
|
||||
assert isinstance(reread, GifImagePlugin.GifImageFile)
|
||||
assert reread.n_frames == 3
|
||||
|
||||
# Tests appending using a generator
|
||||
|
@ -1132,6 +1173,7 @@ def test_append_images(tmp_path: Path) -> None:
|
|||
im.save(out, save_all=True, append_images=im_generator(ims))
|
||||
|
||||
with Image.open(out) as reread:
|
||||
assert isinstance(reread, GifImagePlugin.GifImageFile)
|
||||
assert reread.n_frames == 3
|
||||
|
||||
# Tests appending single and multiple frame images
|
||||
|
@ -1140,11 +1182,12 @@ def test_append_images(tmp_path: Path) -> None:
|
|||
im.save(out, save_all=True, append_images=[im2])
|
||||
|
||||
with Image.open(out) as reread:
|
||||
assert isinstance(reread, GifImagePlugin.GifImageFile)
|
||||
assert reread.n_frames == 10
|
||||
|
||||
|
||||
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))
|
||||
bigger_im = Image.new("RGB", (200, 200), "#f00")
|
||||
|
@ -1171,7 +1214,7 @@ def test_transparent_optimize(tmp_path: Path) -> None:
|
|||
im.frombytes(data)
|
||||
im.putpalette(palette)
|
||||
|
||||
out = str(tmp_path / "temp.gif")
|
||||
out = tmp_path / "temp.gif"
|
||||
im.save(out, transparency=im.getpixel((252, 0)))
|
||||
|
||||
with Image.open(out) as reloaded:
|
||||
|
@ -1179,7 +1222,7 @@ def test_transparent_optimize(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))
|
||||
|
||||
for x in range(256):
|
||||
|
@ -1194,7 +1237,7 @@ def test_removed_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
|
||||
im = Image.new("RGB", (1, 1))
|
||||
|
@ -1216,7 +1259,7 @@ def test_rgb_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.save(out, save_all=True, append_images=[Image.new("RGBA", im.size)])
|
||||
|
@ -1226,25 +1269,26 @@ def test_rgba_transparency(tmp_path: Path) -> None:
|
|||
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:
|
||||
im.seek(1)
|
||||
assert im.info["background"] == 255
|
||||
|
||||
|
||||
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")
|
||||
ims = [Image.new("RGB", (100, 100), "#000")]
|
||||
im.save(out, save_all=True, append_images=ims)
|
||||
|
||||
with Image.open(out) as reread:
|
||||
assert isinstance(reread, GifImagePlugin.GifImageFile)
|
||||
assert reread.n_frames == 2
|
||||
|
||||
|
||||
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.putpixel((0, 1), (255, 0, 0, 0))
|
||||
|
@ -1252,6 +1296,7 @@ def test_bbox_alpha(tmp_path: Path) -> None:
|
|||
im.save(out, save_all=True, append_images=[im2])
|
||||
|
||||
with Image.open(out) as reread:
|
||||
assert isinstance(reread, GifImagePlugin.GifImageFile)
|
||||
assert reread.n_frames == 2
|
||||
|
||||
|
||||
|
@ -1263,7 +1308,7 @@ def test_palette_save_L(tmp_path: Path) -> None:
|
|||
palette = im.getpalette()
|
||||
assert palette is not None
|
||||
|
||||
out = str(tmp_path / "temp.gif")
|
||||
out = tmp_path / "temp.gif"
|
||||
im_l.save(out, palette=bytes(palette))
|
||||
|
||||
with Image.open(out) as reloaded:
|
||||
|
@ -1274,7 +1319,7 @@ def test_palette_save_P(tmp_path: Path) -> None:
|
|||
im = Image.new("P", (1, 2))
|
||||
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)))
|
||||
|
||||
with Image.open(out) as reloaded:
|
||||
|
@ -1290,7 +1335,7 @@ def test_palette_save_duplicate_entries(tmp_path: Path) -> None:
|
|||
|
||||
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])
|
||||
|
||||
with Image.open(out) as reloaded:
|
||||
|
@ -1305,7 +1350,7 @@ def test_palette_save_all_P(tmp_path: Path) -> None:
|
|||
frame.putpalette(color)
|
||||
frames.append(frame)
|
||||
|
||||
out = str(tmp_path / "temp.gif")
|
||||
out = tmp_path / "temp.gif"
|
||||
frames[0].save(
|
||||
out, save_all=True, palette=[255, 0, 0, 0, 255, 0], append_images=frames[1:]
|
||||
)
|
||||
|
@ -1313,6 +1358,7 @@ def test_palette_save_all_P(tmp_path: Path) -> None:
|
|||
with Image.open(out) as im:
|
||||
# Assert that the frames are correct, and each frame has the same palette
|
||||
assert_image_equal(im.convert("RGB"), frames[0].convert("RGB"))
|
||||
assert im.palette is not None
|
||||
assert im.palette.palette == im.global_palette.palette
|
||||
|
||||
im.seek(1)
|
||||
|
@ -1327,7 +1373,7 @@ def test_palette_save_ImagePalette(tmp_path: Path) -> None:
|
|||
im = hopper("P")
|
||||
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)
|
||||
|
||||
with Image.open(out) as reloaded:
|
||||
|
@ -1340,39 +1386,37 @@ def test_save_I(tmp_path: Path) -> None:
|
|||
|
||||
im = hopper("I")
|
||||
|
||||
out = str(tmp_path / "temp.gif")
|
||||
out = tmp_path / "temp.gif"
|
||||
im.save(out)
|
||||
|
||||
with Image.open(out) as reloaded:
|
||||
assert_image_equal(reloaded.convert("L"), im.convert("L"))
|
||||
|
||||
|
||||
def test_getdata() -> None:
|
||||
def test_getdata(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
# Test getheader/getdata against legacy values.
|
||||
# Create a 'P' image with holes in the palette.
|
||||
im = Image._wedge().resize((16, 16), Image.Resampling.NEAREST)
|
||||
im = Image.linear_gradient(mode="L").resize((16, 16), Image.Resampling.NEAREST)
|
||||
im.putpalette(ImagePalette.ImagePalette("RGB"))
|
||||
im.info = {"background": 0}
|
||||
|
||||
passed_palette = bytes(255 - i // 3 for i in range(768))
|
||||
|
||||
GifImagePlugin._FORCE_OPTIMIZE = True
|
||||
try:
|
||||
h = GifImagePlugin.getheader(im, passed_palette)
|
||||
d = GifImagePlugin.getdata(im)
|
||||
monkeypatch.setattr(GifImagePlugin, "_FORCE_OPTIMIZE", True)
|
||||
|
||||
import pickle
|
||||
h = GifImagePlugin.getheader(im, passed_palette)
|
||||
d = GifImagePlugin.getdata(im)
|
||||
|
||||
# Enable to get target values on pre-refactor version
|
||||
# with open('Tests/images/gif_header_data.pkl', 'wb') as f:
|
||||
# pickle.dump((h, d), f, 1)
|
||||
with open("Tests/images/gif_header_data.pkl", "rb") as f:
|
||||
(h_target, d_target) = pickle.load(f)
|
||||
import pickle
|
||||
|
||||
assert h == h_target
|
||||
assert d == d_target
|
||||
finally:
|
||||
GifImagePlugin._FORCE_OPTIMIZE = False
|
||||
# Enable to get target values on pre-refactor version
|
||||
# with open('Tests/images/gif_header_data.pkl', 'wb') as f:
|
||||
# pickle.dump((h, d), f, 1)
|
||||
with open("Tests/images/gif_header_data.pkl", "rb") as f:
|
||||
(h_target, d_target) = pickle.load(f)
|
||||
|
||||
assert h == h_target
|
||||
assert d == d_target
|
||||
|
||||
|
||||
def test_lzw_bits() -> None:
|
||||
|
@ -1398,24 +1442,24 @@ def test_lzw_bits() -> None:
|
|||
),
|
||||
)
|
||||
def test_extents(
|
||||
test_file: str, loading_strategy: GifImagePlugin.LoadingStrategy
|
||||
test_file: str,
|
||||
loading_strategy: GifImagePlugin.LoadingStrategy,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
GifImagePlugin.LOADING_STRATEGY = loading_strategy
|
||||
try:
|
||||
with Image.open("Tests/images/" + test_file) as im:
|
||||
assert im.size == (100, 100)
|
||||
monkeypatch.setattr(GifImagePlugin, "LOADING_STRATEGY", loading_strategy)
|
||||
with Image.open("Tests/images/" + test_file) as im:
|
||||
assert isinstance(im, GifImagePlugin.GifImageFile)
|
||||
assert im.size == (100, 100)
|
||||
|
||||
# Check that n_frames does not change the size
|
||||
assert im.n_frames == 2
|
||||
assert im.size == (100, 100)
|
||||
# Check that n_frames does not change the size
|
||||
assert im.n_frames == 2
|
||||
assert im.size == (100, 100)
|
||||
|
||||
im.seek(1)
|
||||
assert im.size == (150, 150)
|
||||
im.seek(1)
|
||||
assert im.size == (150, 150)
|
||||
|
||||
im.load()
|
||||
assert im.im.size == (150, 150)
|
||||
finally:
|
||||
GifImagePlugin.LOADING_STRATEGY = GifImagePlugin.LoadingStrategy.RGB_AFTER_FIRST
|
||||
im.load()
|
||||
assert im.im.size == (150, 150)
|
||||
|
||||
|
||||
def test_missing_background() -> None:
|
||||
|
@ -1427,7 +1471,7 @@ def test_missing_background() -> 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:
|
||||
im.save(out)
|
||||
|
||||
|
@ -1438,7 +1482,7 @@ def test_saving_rgba(tmp_path: Path) -> None:
|
|||
|
||||
@pytest.mark.parametrize("params", ({}, {"disposal": 2, "optimize": False}))
|
||||
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))
|
||||
d = ImageDraw.Draw(im1)
|
||||
|
@ -1452,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)
|
||||
|
||||
with Image.open(out) as reloaded:
|
||||
assert isinstance(reloaded, GifImagePlugin.GifImageFile)
|
||||
assert reloaded.n_frames == 2
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from io import BytesIO
|
||||
|
||||
import pytest
|
||||
|
||||
from PIL.GimpPaletteFile import GimpPaletteFile
|
||||
|
@ -14,17 +16,20 @@ def test_sanity() -> None:
|
|||
GimpPaletteFile(fp)
|
||||
|
||||
with open("Tests/images/bad_palette_file.gpl", "rb") as fp:
|
||||
with pytest.raises(SyntaxError):
|
||||
with pytest.raises(SyntaxError, match="bad palette file"):
|
||||
GimpPaletteFile(fp)
|
||||
|
||||
with open("Tests/images/bad_palette_entry.gpl", "rb") as fp:
|
||||
with pytest.raises(ValueError):
|
||||
with pytest.raises(ValueError, match="bad palette entry"):
|
||||
GimpPaletteFile(fp)
|
||||
|
||||
|
||||
def test_get_palette() -> None:
|
||||
@pytest.mark.parametrize(
|
||||
"filename, size", (("custom_gimp_palette.gpl", 8), ("full_gimp_palette.gpl", 256))
|
||||
)
|
||||
def test_get_palette(filename: str, size: int) -> None:
|
||||
# Arrange
|
||||
with open("Tests/images/custom_gimp_palette.gpl", "rb") as fp:
|
||||
with open("Tests/images/" + filename, "rb") as fp:
|
||||
palette_file = GimpPaletteFile(fp)
|
||||
|
||||
# Act
|
||||
|
@ -32,3 +37,36 @@ def test_get_palette() -> None:
|
|||
|
||||
# Assert
|
||||
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:
|
||||
# Arrange
|
||||
im = hopper()
|
||||
tmpfile = str(tmp_path / "temp.grib")
|
||||
tmpfile = tmp_path / "temp.grib"
|
||||
|
||||
# Act / Assert: stub cannot save without an implemented handler
|
||||
with pytest.raises(OSError):
|
||||
|
@ -82,8 +82,8 @@ def test_handler(tmp_path: Path) -> None:
|
|||
im.load()
|
||||
assert handler.is_loaded()
|
||||
|
||||
temp_file = str(tmp_path / "temp.grib")
|
||||
temp_file = tmp_path / "temp.grib"
|
||||
im.save(temp_file)
|
||||
assert handler.saved
|
||||
|
||||
GribStubImagePlugin._handler = None
|
||||
GribStubImagePlugin.register_handler(None)
|
||||
|
|
|
@ -43,7 +43,7 @@ def test_save() -> None:
|
|||
# Arrange
|
||||
with Image.open(TEST_FILE) as im:
|
||||
dummy_fp = BytesIO()
|
||||
dummy_filename = "dummy.filename"
|
||||
dummy_filename = "dummy.h5"
|
||||
|
||||
# Act / Assert: stub cannot save without an implemented handler
|
||||
with pytest.raises(OSError):
|
||||
|
@ -84,8 +84,8 @@ def test_handler(tmp_path: Path) -> None:
|
|||
im.load()
|
||||
assert handler.is_loaded()
|
||||
|
||||
temp_file = str(tmp_path / "temp.h5")
|
||||
temp_file = tmp_path / "temp.h5"
|
||||
im.save(temp_file)
|
||||
assert handler.saved
|
||||
|
||||
Hdf5StubImagePlugin._handler = None
|
||||
Hdf5StubImagePlugin.register_handler(None)
|
||||
|
|
|
@ -32,14 +32,18 @@ def test_sanity() -> None:
|
|||
|
||||
def test_load() -> None:
|
||||
with Image.open(TEST_FILE) as im:
|
||||
assert im.load()[0, 0] == (0, 0, 0, 0)
|
||||
px = im.load()
|
||||
assert px is not None
|
||||
assert px[0, 0] == (0, 0, 0, 0)
|
||||
|
||||
# Test again now that it has already been loaded once
|
||||
assert im.load()[0, 0] == (0, 0, 0, 0)
|
||||
px = im.load()
|
||||
assert px is not None
|
||||
assert px[0, 0] == (0, 0, 0, 0)
|
||||
|
||||
|
||||
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:
|
||||
im.save(temp_file)
|
||||
|
@ -56,7 +60,7 @@ def test_save(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))
|
||||
|
||||
with Image.open(TEST_FILE) as im:
|
||||
|
@ -65,6 +69,7 @@ def test_save_append_images(tmp_path: Path) -> None:
|
|||
assert_image_similar_tofile(im, temp_file, 1)
|
||||
|
||||
with Image.open(temp_file) as reread:
|
||||
assert isinstance(reread, IcnsImagePlugin.IcnsImageFile)
|
||||
reread.size = (16, 16)
|
||||
reread.load(2)
|
||||
assert_image_equal(reread, provided_im)
|
||||
|
@ -86,6 +91,7 @@ def test_sizes() -> None:
|
|||
# Check that we can load all of the sizes, and that the final pixel
|
||||
# dimensions are as expected
|
||||
with Image.open(TEST_FILE) as im:
|
||||
assert isinstance(im, IcnsImagePlugin.IcnsImageFile)
|
||||
for w, h, r in im.info["sizes"]:
|
||||
wr = w * r
|
||||
hr = h * r
|
||||
|
@ -114,6 +120,7 @@ def test_older_icon() -> None:
|
|||
wr = w * r
|
||||
hr = h * r
|
||||
with Image.open("Tests/images/pillow2.icns") as im2:
|
||||
assert isinstance(im2, IcnsImagePlugin.IcnsImageFile)
|
||||
im2.size = (w, h)
|
||||
im2.load(r)
|
||||
assert im2.mode == "RGBA"
|
||||
|
@ -131,6 +138,7 @@ def test_jp2_icon() -> None:
|
|||
wr = w * r
|
||||
hr = h * r
|
||||
with Image.open("Tests/images/pillow3.icns") as im2:
|
||||
assert isinstance(im2, IcnsImagePlugin.IcnsImageFile)
|
||||
im2.size = (w, h)
|
||||
im2.load(r)
|
||||
assert im2.mode == "RGBA"
|
||||
|
|
|
@ -24,7 +24,9 @@ def test_sanity() -> None:
|
|||
|
||||
def test_load() -> None:
|
||||
with Image.open(TEST_ICO_FILE) as im:
|
||||
assert im.load()[0, 0] == (1, 1, 9, 255)
|
||||
px = im.load()
|
||||
assert px is not None
|
||||
assert px[0, 0] == (1, 1, 9, 255)
|
||||
|
||||
|
||||
def test_mask() -> None:
|
||||
|
@ -39,7 +41,7 @@ def test_black_and_white() -> 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.save(temp_file)
|
||||
|
@ -75,6 +77,7 @@ def test_save_to_bytes() -> None:
|
|||
# The other one
|
||||
output.seek(0)
|
||||
with Image.open(output) as reloaded:
|
||||
assert isinstance(reloaded, IcoImagePlugin.IcoImageFile)
|
||||
reloaded.size = (32, 32)
|
||||
|
||||
assert im.mode == reloaded.mode
|
||||
|
@ -86,12 +89,13 @@ def test_save_to_bytes() -> None:
|
|||
|
||||
|
||||
def test_getpixel(tmp_path: Path) -> None:
|
||||
temp_file = str(tmp_path / "temp.ico")
|
||||
temp_file = tmp_path / "temp.ico"
|
||||
|
||||
im = hopper()
|
||||
im.save(temp_file, "ico", sizes=[(32, 32), (64, 64)])
|
||||
|
||||
with Image.open(temp_file) as reloaded:
|
||||
assert isinstance(reloaded, IcoImagePlugin.IcoImageFile)
|
||||
reloaded.load()
|
||||
reloaded.size = (32, 32)
|
||||
|
||||
|
@ -99,8 +103,8 @@ def test_getpixel(tmp_path: Path) -> None:
|
|||
|
||||
|
||||
def test_no_duplicates(tmp_path: Path) -> None:
|
||||
temp_file = str(tmp_path / "temp.ico")
|
||||
temp_file2 = str(tmp_path / "temp2.ico")
|
||||
temp_file = tmp_path / "temp.ico"
|
||||
temp_file2 = tmp_path / "temp2.ico"
|
||||
|
||||
im = hopper()
|
||||
sizes = [(32, 32), (64, 64)]
|
||||
|
@ -113,8 +117,8 @@ def test_no_duplicates(tmp_path: Path) -> None:
|
|||
|
||||
|
||||
def test_different_bit_depths(tmp_path: Path) -> None:
|
||||
temp_file = str(tmp_path / "temp.ico")
|
||||
temp_file2 = str(tmp_path / "temp2.ico")
|
||||
temp_file = tmp_path / "temp.ico"
|
||||
temp_file2 = tmp_path / "temp2.ico"
|
||||
|
||||
im = hopper()
|
||||
im.save(temp_file, "ico", bitmap_format="bmp", sizes=[(128, 128)])
|
||||
|
@ -130,8 +134,8 @@ def test_different_bit_depths(tmp_path: Path) -> None:
|
|||
assert os.path.getsize(temp_file) != os.path.getsize(temp_file2)
|
||||
|
||||
# Test that only matching sizes of different bit depths are saved
|
||||
temp_file3 = str(tmp_path / "temp3.ico")
|
||||
temp_file4 = str(tmp_path / "temp4.ico")
|
||||
temp_file3 = tmp_path / "temp3.ico"
|
||||
temp_file4 = tmp_path / "temp4.ico"
|
||||
|
||||
im.save(temp_file3, "ico", bitmap_format="bmp", sizes=[(128, 128)])
|
||||
im.save(
|
||||
|
@ -165,6 +169,7 @@ def test_save_to_bytes_bmp(mode: str) -> None:
|
|||
# The other one
|
||||
output.seek(0)
|
||||
with Image.open(output) as reloaded:
|
||||
assert isinstance(reloaded, IcoImagePlugin.IcoImageFile)
|
||||
reloaded.size = (32, 32)
|
||||
|
||||
assert "RGBA" == reloaded.mode
|
||||
|
@ -176,6 +181,7 @@ def test_save_to_bytes_bmp(mode: str) -> None:
|
|||
|
||||
def test_incorrect_size() -> None:
|
||||
with Image.open(TEST_ICO_FILE) as im:
|
||||
assert isinstance(im, IcoImagePlugin.IcoImageFile)
|
||||
with pytest.raises(ValueError):
|
||||
im.size = (1, 1)
|
||||
|
||||
|
@ -184,7 +190,7 @@ def test_save_256x256(tmp_path: Path) -> None:
|
|||
"""Issue #2264 https://github.com/python-pillow/Pillow/issues/2264"""
|
||||
# Arrange
|
||||
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
|
||||
im.save(outfile)
|
||||
|
@ -200,7 +206,7 @@ def test_only_save_relevant_sizes(tmp_path: Path) -> None:
|
|||
"""
|
||||
# Arrange
|
||||
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
|
||||
im.save(outfile)
|
||||
|
||||
|
@ -213,10 +219,11 @@ def test_save_append_images(tmp_path: Path) -> None:
|
|||
# append_images should be used for scaled down versions of the image
|
||||
im = hopper("RGBA")
|
||||
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])
|
||||
|
||||
with Image.open(outfile) as reread:
|
||||
assert isinstance(reread, IcoImagePlugin.IcoImageFile)
|
||||
assert_image_equal(reread, hopper("RGBA"))
|
||||
|
||||
reread.size = (32, 32)
|
||||
|
@ -233,7 +240,7 @@ def test_unexpected_size() -> None:
|
|||
|
||||
def test_draw_reloaded(tmp_path: Path) -> None:
|
||||
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.line((0, 0) + im.size, "#f00")
|
||||
|
@ -243,27 +250,23 @@ def test_draw_reloaded(tmp_path: Path) -> None:
|
|||
assert_image_equal_tofile(im, "Tests/images/hopper_draw.ico")
|
||||
|
||||
|
||||
def test_truncated_mask() -> None:
|
||||
def test_truncated_mask(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
# 1 bpp
|
||||
with open("Tests/images/hopper_mask.ico", "rb") as fp:
|
||||
data = fp.read()
|
||||
|
||||
ImageFile.LOAD_TRUNCATED_IMAGES = True
|
||||
monkeypatch.setattr(ImageFile, "LOAD_TRUNCATED_IMAGES", True)
|
||||
data = data[:-3]
|
||||
|
||||
try:
|
||||
with Image.open(io.BytesIO(data)) as im:
|
||||
with Image.open("Tests/images/hopper_mask.png") as expected:
|
||||
assert im.mode == "1"
|
||||
with Image.open(io.BytesIO(data)) as im:
|
||||
assert im.mode == "1"
|
||||
|
||||
# 32 bpp
|
||||
output = io.BytesIO()
|
||||
expected = hopper("RGBA")
|
||||
expected.save(output, "ico", bitmap_format="bmp")
|
||||
# 32 bpp
|
||||
output = io.BytesIO()
|
||||
expected = hopper("RGBA")
|
||||
expected.save(output, "ico", bitmap_format="bmp")
|
||||
|
||||
data = output.getvalue()[:-1]
|
||||
data = output.getvalue()[:-1]
|
||||
|
||||
with Image.open(io.BytesIO(data)) as im:
|
||||
assert im.mode == "RGB"
|
||||
finally:
|
||||
ImageFile.LOAD_TRUNCATED_IMAGES = False
|
||||
with Image.open(io.BytesIO(data)) as im:
|
||||
assert im.mode == "RGB"
|
||||
|
|
|
@ -23,7 +23,7 @@ def test_sanity() -> 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:
|
||||
im.save(out)
|
||||
assert filecmp.cmp(out, "Tests/images/hopper_long_name.im")
|
||||
|
@ -31,12 +31,12 @@ def test_name_limit(tmp_path: Path) -> None:
|
|||
|
||||
@pytest.mark.skipif(is_pypy(), reason="Requires CPython")
|
||||
def test_unclosed_file() -> None:
|
||||
def open() -> None:
|
||||
def open_test_image() -> None:
|
||||
im = Image.open(TEST_IM)
|
||||
im.load()
|
||||
|
||||
with pytest.warns(ResourceWarning):
|
||||
open()
|
||||
open_test_image()
|
||||
|
||||
|
||||
def test_closed_file() -> None:
|
||||
|
@ -68,12 +68,14 @@ def test_tell() -> None:
|
|||
|
||||
def test_n_frames() -> None:
|
||||
with Image.open(TEST_IM) as im:
|
||||
assert isinstance(im, ImImagePlugin.ImImageFile)
|
||||
assert im.n_frames == 1
|
||||
assert not im.is_animated
|
||||
|
||||
|
||||
def test_eoferror() -> None:
|
||||
with Image.open(TEST_IM) as im:
|
||||
assert isinstance(im, ImImagePlugin.ImImageFile)
|
||||
n_frames = im.n_frames
|
||||
|
||||
# Test seeking past the last frame
|
||||
|
@ -87,7 +89,7 @@ def test_eoferror() -> None:
|
|||
|
||||
@pytest.mark.parametrize("mode", ("RGB", "P", "PA"))
|
||||
def test_roundtrip(mode: str, tmp_path: Path) -> None:
|
||||
out = str(tmp_path / "temp.im")
|
||||
out = tmp_path / "temp.im"
|
||||
im = hopper(mode)
|
||||
im.save(out)
|
||||
assert_image_equal_tofile(im, out)
|
||||
|
@ -98,7 +100,7 @@ def test_small_palette(tmp_path: Path) -> None:
|
|||
colors = [0, 1, 2]
|
||||
im.putpalette(colors)
|
||||
|
||||
out = str(tmp_path / "temp.im")
|
||||
out = tmp_path / "temp.im"
|
||||
im.save(out)
|
||||
|
||||
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:
|
||||
out = str(tmp_path / "temp.im")
|
||||
out = tmp_path / "temp.im"
|
||||
im = hopper("HSV")
|
||||
with pytest.raises(ValueError):
|
||||
im.save(out)
|
||||
|
|
|
@ -58,10 +58,7 @@ def test_getiptcinfo_fotostation() -> None:
|
|||
|
||||
# Assert
|
||||
assert iptc is not None
|
||||
for tag in iptc.keys():
|
||||
if tag[0] == 240:
|
||||
return
|
||||
pytest.fail("FotoStation tag not found")
|
||||
assert 240 in (tag[0] for tag in iptc.keys()), "FotoStation tag not found"
|
||||
|
||||
|
||||
def test_getiptcinfo_zero_padding() -> None:
|
||||
|
|
|
@ -83,7 +83,7 @@ class TestFileJpeg:
|
|||
|
||||
@pytest.mark.parametrize("size", ((1, 0), (0, 1), (0, 0)))
|
||||
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)
|
||||
with pytest.raises(ValueError):
|
||||
im.save(f)
|
||||
|
@ -91,6 +91,7 @@ class TestFileJpeg:
|
|||
def test_app(self) -> None:
|
||||
# Test APP/COM reader (@PIL135)
|
||||
with Image.open(TEST_FILE) as im:
|
||||
assert isinstance(im, JpegImagePlugin.JpegImageFile)
|
||||
assert im.applist[0] == ("APP0", b"JFIF\x00\x01\x01\x01\x00`\x00`\x00\x00")
|
||||
assert im.applist[1] == (
|
||||
"COM",
|
||||
|
@ -181,6 +182,10 @@ class TestFileJpeg:
|
|||
assert test(100, 200) == (100, 200)
|
||||
assert test(0) is None # square pixels
|
||||
|
||||
def test_dpi_jfif_cm(self) -> None:
|
||||
with Image.open("Tests/images/jfif_unit_cm.jpg") as im:
|
||||
assert im.info["dpi"] == (2.54, 5.08)
|
||||
|
||||
@mark_if_feature_version(
|
||||
pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing"
|
||||
)
|
||||
|
@ -190,7 +195,7 @@ class TestFileJpeg:
|
|||
icc_profile = im1.info["icc_profile"]
|
||||
assert len(icc_profile) == 3144
|
||||
# Roundtrip via physical file.
|
||||
f = str(tmp_path / "temp.jpg")
|
||||
f = tmp_path / "temp.jpg"
|
||||
im1.save(f, icc_profile=icc_profile)
|
||||
with Image.open(f) as im2:
|
||||
assert im2.info.get("icc_profile") == icc_profile
|
||||
|
@ -234,7 +239,7 @@ class TestFileJpeg:
|
|||
# Sometimes the meta data on the icc_profile block is bigger than
|
||||
# Image.MAXBLOCK or the image size.
|
||||
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"]
|
||||
# Should not raise OSError for image with icc larger than image size.
|
||||
im.save(
|
||||
|
@ -246,11 +251,11 @@ class TestFileJpeg:
|
|||
)
|
||||
|
||||
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)
|
||||
|
||||
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)
|
||||
|
||||
def test_optimize(self) -> None:
|
||||
|
@ -264,7 +269,7 @@ class TestFileJpeg:
|
|||
|
||||
def test_optimize_large_buffer(self, tmp_path: Path) -> None:
|
||||
# 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
|
||||
im = Image.new("RGB", (4096, 4096), 0xFF3333)
|
||||
im.save(f, format="JPEG", optimize=True)
|
||||
|
@ -277,17 +282,20 @@ class TestFileJpeg:
|
|||
assert not im2.info.get("progressive")
|
||||
assert im3.info.get("progressive")
|
||||
|
||||
assert_image_equal(im1, im3)
|
||||
if features.check_feature("mozjpeg"):
|
||||
assert_image_similar(im1, im3, 9.39)
|
||||
else:
|
||||
assert_image_equal(im1, im3)
|
||||
assert im1_bytes >= im3_bytes
|
||||
|
||||
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
|
||||
im = Image.new("RGB", (4096, 4096), 0xFF3333)
|
||||
im.save(f, format="JPEG", progressive=True)
|
||||
|
||||
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))
|
||||
# this requires more bytes than pixels in the image
|
||||
im.save(f, format="JPEG", progressive=True, quality=100)
|
||||
|
@ -300,7 +308,7 @@ class TestFileJpeg:
|
|||
|
||||
def test_large_exif(self, tmp_path: Path) -> None:
|
||||
# https://github.com/python-pillow/Pillow/issues/148
|
||||
f = str(tmp_path / "temp.jpg")
|
||||
f = tmp_path / "temp.jpg"
|
||||
im = hopper()
|
||||
im.save(f, "JPEG", quality=90, exif=b"1" * 65533)
|
||||
|
||||
|
@ -309,6 +317,8 @@ class TestFileJpeg:
|
|||
|
||||
def test_exif_typeerror(self) -> None:
|
||||
with Image.open("Tests/images/exif_typeerror.jpg") as im:
|
||||
assert isinstance(im, JpegImagePlugin.JpegImageFile)
|
||||
|
||||
# Should not raise a TypeError
|
||||
im._getexif()
|
||||
|
||||
|
@ -328,7 +338,7 @@ class TestFileJpeg:
|
|||
assert exif[gps_index] == expected_exif_gps
|
||||
|
||||
# Writing
|
||||
f = str(tmp_path / "temp.jpg")
|
||||
f = tmp_path / "temp.jpg"
|
||||
exif = Image.Exif()
|
||||
exif[gps_index] = expected_exif_gps
|
||||
hopper().save(f, exif=exif)
|
||||
|
@ -349,7 +359,6 @@ class TestFileJpeg:
|
|||
assert exif.get_ifd(0x8825) == {}
|
||||
|
||||
transposed = ImageOps.exif_transpose(im)
|
||||
assert transposed is not None
|
||||
exif = transposed.getexif()
|
||||
assert exif.get_ifd(0x8825) == {}
|
||||
|
||||
|
@ -420,8 +429,12 @@ class TestFileJpeg:
|
|||
|
||||
im2 = self.roundtrip(hopper(), progressive=1)
|
||||
im3 = self.roundtrip(hopper(), progression=1) # compatibility
|
||||
assert_image_equal(im1, im2)
|
||||
assert_image_equal(im1, im3)
|
||||
if features.check_feature("mozjpeg"):
|
||||
assert_image_similar(im1, im2, 9.39)
|
||||
assert_image_similar(im1, im3, 9.39)
|
||||
else:
|
||||
assert_image_equal(im1, im2)
|
||||
assert_image_equal(im1, im3)
|
||||
assert im2.info.get("progressive")
|
||||
assert im2.info.get("progression")
|
||||
assert im3.info.get("progressive")
|
||||
|
@ -490,20 +503,21 @@ class TestFileJpeg:
|
|||
|
||||
def test_mp(self) -> None:
|
||||
with Image.open("Tests/images/pil_sample_rgb.jpg") as im:
|
||||
assert isinstance(im, JpegImagePlugin.JpegImageFile)
|
||||
assert im._getmp() is None
|
||||
|
||||
def test_quality_keep(self, tmp_path: Path) -> None:
|
||||
# RGB
|
||||
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")
|
||||
# Grayscale
|
||||
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")
|
||||
# CMYK
|
||||
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")
|
||||
|
||||
def test_junk_jpeg_header(self) -> None:
|
||||
|
@ -520,12 +534,13 @@ class TestFileJpeg:
|
|||
@mark_if_feature_version(
|
||||
pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing"
|
||||
)
|
||||
def test_truncated_jpeg_should_read_all_the_data(self) -> None:
|
||||
def test_truncated_jpeg_should_read_all_the_data(
|
||||
self, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
filename = "Tests/images/truncated_jpeg.jpg"
|
||||
ImageFile.LOAD_TRUNCATED_IMAGES = True
|
||||
monkeypatch.setattr(ImageFile, "LOAD_TRUNCATED_IMAGES", True)
|
||||
with Image.open(filename) as im:
|
||||
im.load()
|
||||
ImageFile.LOAD_TRUNCATED_IMAGES = False
|
||||
assert im.getbbox() is not None
|
||||
|
||||
def test_truncated_jpeg_throws_oserror(self) -> None:
|
||||
|
@ -547,12 +562,14 @@ class TestFileJpeg:
|
|||
with Image.open(test_file) as im:
|
||||
im.save(b, "JPEG", qtables=[[n] * 64] * n)
|
||||
with Image.open(b) as im:
|
||||
assert isinstance(im, JpegImagePlugin.JpegImageFile)
|
||||
assert len(im.quantization) == n
|
||||
reloaded = self.roundtrip(im, qtables="keep")
|
||||
assert im.quantization == reloaded.quantization
|
||||
assert max(reloaded.quantization[0]) <= 255
|
||||
|
||||
with Image.open("Tests/images/hopper.jpg") as im:
|
||||
assert isinstance(im, JpegImagePlugin.JpegImageFile)
|
||||
qtables = im.quantization
|
||||
reloaded = self.roundtrip(im, qtables=qtables, subsampling=0)
|
||||
assert im.quantization == reloaded.quantization
|
||||
|
@ -652,6 +669,7 @@ class TestFileJpeg:
|
|||
|
||||
def test_load_16bit_qtables(self) -> None:
|
||||
with Image.open("Tests/images/hopper_16bit_qtables.jpg") as im:
|
||||
assert isinstance(im, JpegImagePlugin.JpegImageFile)
|
||||
assert len(im.quantization) == 2
|
||||
assert len(im.quantization[0]) == 64
|
||||
assert max(im.quantization[0]) > 255
|
||||
|
@ -694,6 +712,7 @@ class TestFileJpeg:
|
|||
@pytest.mark.skipif(not djpeg_available(), reason="djpeg not available")
|
||||
def test_load_djpeg(self) -> None:
|
||||
with Image.open(TEST_FILE) as img:
|
||||
assert isinstance(img, JpegImagePlugin.JpegImageFile)
|
||||
img.load_djpeg()
|
||||
assert_image_similar_tofile(img, TEST_FILE, 5)
|
||||
|
||||
|
@ -715,7 +734,7 @@ class TestFileJpeg:
|
|||
|
||||
def test_MAXBLOCK_scaling(self, tmp_path: Path) -> None:
|
||||
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)
|
||||
|
||||
with Image.open(f) as reloaded:
|
||||
|
@ -751,7 +770,7 @@ class TestFileJpeg:
|
|||
|
||||
def test_save_tiff_with_dpi(self, tmp_path: Path) -> None:
|
||||
# Arrange
|
||||
outfile = str(tmp_path / "temp.tif")
|
||||
outfile = tmp_path / "temp.tif"
|
||||
with Image.open("Tests/images/hopper.tif") as im:
|
||||
# Act
|
||||
im.save(outfile, "JPEG", dpi=im.info["dpi"])
|
||||
|
@ -762,7 +781,7 @@ class TestFileJpeg:
|
|||
assert im.info["dpi"] == reloaded.info["dpi"]
|
||||
|
||||
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:
|
||||
im.save(outfile, dpi=(72.2, 72.2))
|
||||
|
||||
|
@ -848,7 +867,7 @@ class TestFileJpeg:
|
|||
exif = im.getexif()
|
||||
assert exif[282] == 180
|
||||
|
||||
out = str(tmp_path / "out.jpg")
|
||||
out = tmp_path / "out.jpg"
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter("error")
|
||||
|
||||
|
@ -898,6 +917,7 @@ class TestFileJpeg:
|
|||
|
||||
def test_photoshop_malformed_and_multiple(self) -> None:
|
||||
with Image.open("Tests/images/app13-multiple.jpg") as im:
|
||||
assert isinstance(im, JpegImagePlugin.JpegImageFile)
|
||||
assert "photoshop" in im.info
|
||||
assert 24 == len(im.info["photoshop"])
|
||||
apps_13_lengths = [len(v) for k, v in im.applist if k == "APP13"]
|
||||
|
@ -923,7 +943,7 @@ class TestFileJpeg:
|
|||
|
||||
def test_jpeg_magic_number(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
size = 4097
|
||||
buffer = BytesIO(b"\xFF" * size) # Many xFF bytes
|
||||
buffer = BytesIO(b"\xff" * size) # Many xff bytes
|
||||
max_pos = 0
|
||||
orig_read = buffer.read
|
||||
|
||||
|
@ -994,14 +1014,19 @@ class TestFileJpeg:
|
|||
assert im.getxmp() == {"xmpmeta": None}
|
||||
|
||||
def test_save_xmp(self, tmp_path: Path) -> None:
|
||||
f = str(tmp_path / "temp.jpg")
|
||||
f = tmp_path / "temp.jpg"
|
||||
im = hopper()
|
||||
im.save(f, xmp=b"XMP test")
|
||||
with Image.open(f) as reloaded:
|
||||
assert reloaded.info["xmp"] == b"XMP test"
|
||||
|
||||
im.info["xmp"] = b"1" * 65504
|
||||
im.save(f)
|
||||
# Check that XMP is not saved from image info
|
||||
reloaded.save(f)
|
||||
|
||||
with Image.open(f) as reloaded:
|
||||
assert "xmp" not in reloaded.info
|
||||
|
||||
im.save(f, xmp=b"1" * 65504)
|
||||
with Image.open(f) as reloaded:
|
||||
assert reloaded.info["xmp"] == b"1" * 65504
|
||||
|
||||
|
@ -1009,7 +1034,7 @@ class TestFileJpeg:
|
|||
im.save(f, xmp=b"1" * 65505)
|
||||
|
||||
@pytest.mark.timeout(timeout=1)
|
||||
def test_eof(self) -> None:
|
||||
def test_eof(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
# Even though this decoder never says that it is finished
|
||||
# the image should still end when there is no new data
|
||||
class InfiniteMockPyDecoder(ImageFile.PyDecoder):
|
||||
|
@ -1022,11 +1047,10 @@ class TestFileJpeg:
|
|||
|
||||
with Image.open(TEST_FILE) as im:
|
||||
im.tile = [
|
||||
("INFINITE", (0, 0, 128, 128), 0, ("RGB", 0, 1)),
|
||||
ImageFile._Tile("INFINITE", (0, 0, 128, 128), 0, ("RGB", 0, 1)),
|
||||
]
|
||||
ImageFile.LOAD_TRUNCATED_IMAGES = True
|
||||
monkeypatch.setattr(ImageFile, "LOAD_TRUNCATED_IMAGES", True)
|
||||
im.load()
|
||||
ImageFile.LOAD_TRUNCATED_IMAGES = False
|
||||
|
||||
def test_separate_tables(self) -> None:
|
||||
im = hopper()
|
||||
|
@ -1069,6 +1093,7 @@ class TestFileJpeg:
|
|||
|
||||
def test_deprecation(self) -> None:
|
||||
with Image.open(TEST_FILE) as im:
|
||||
assert isinstance(im, JpegImagePlugin.JpegImageFile)
|
||||
with pytest.warns(DeprecationWarning):
|
||||
assert im.huffman_ac == {}
|
||||
with pytest.warns(DeprecationWarning):
|
||||
|
@ -1079,7 +1104,7 @@ class TestFileJpeg:
|
|||
@skip_unless_feature("jpg")
|
||||
class TestFileCloseW32:
|
||||
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:
|
||||
im.save(tmpfile)
|
||||
|
|
|
@ -63,6 +63,7 @@ def test_sanity() -> None:
|
|||
|
||||
with Image.open("Tests/images/test-card-lossless.jp2") as im:
|
||||
px = im.load()
|
||||
assert px is not None
|
||||
assert px[0, 0] == (0, 0, 0)
|
||||
assert im.mode == "RGB"
|
||||
assert im.size == (640, 480)
|
||||
|
@ -98,7 +99,7 @@ def test_bytesio(card: ImageFile.ImageFile) -> None:
|
|||
def test_lossless(card: ImageFile.ImageFile, tmp_path: Path) -> None:
|
||||
with Image.open("Tests/images/test-card-lossless.jp2") as im:
|
||||
im.load()
|
||||
outfile = str(tmp_path / "temp_test-card.png")
|
||||
outfile = tmp_path / "temp_test-card.png"
|
||||
im.save(outfile)
|
||||
assert_image_similar(im, card, 1.0e-3)
|
||||
|
||||
|
@ -181,14 +182,11 @@ def test_load_dpi() -> None:
|
|||
assert "dpi" not in im.info
|
||||
|
||||
|
||||
def test_restricted_icc_profile() -> None:
|
||||
ImageFile.LOAD_TRUNCATED_IMAGES = True
|
||||
try:
|
||||
# JPEG2000 image with a restricted ICC profile and a known colorspace
|
||||
with Image.open("Tests/images/balloon_eciRGBv2_aware.jp2") as im:
|
||||
assert im.mode == "RGB"
|
||||
finally:
|
||||
ImageFile.LOAD_TRUNCATED_IMAGES = False
|
||||
def test_restricted_icc_profile(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setattr(ImageFile, "LOAD_TRUNCATED_IMAGES", True)
|
||||
# JPEG2000 image with a restricted ICC profile and a known colorspace
|
||||
with Image.open("Tests/images/balloon_eciRGBv2_aware.jp2") as im:
|
||||
assert im.mode == "RGB"
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
|
@ -215,7 +213,7 @@ def test_header_errors() -> 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]:
|
||||
card.save(outfile, quality_layers=quality_layers)
|
||||
|
||||
|
@ -230,12 +228,14 @@ def test_layers(card: ImageFile.ImageFile) -> None:
|
|||
out.seek(0)
|
||||
|
||||
with Image.open(out) as im:
|
||||
assert isinstance(im, Jpeg2KImagePlugin.Jpeg2KImageFile)
|
||||
im.layers = 1
|
||||
im.load()
|
||||
assert_image_similar(im, card, 13)
|
||||
|
||||
out.seek(0)
|
||||
with Image.open(out) as im:
|
||||
assert isinstance(im, Jpeg2KImagePlugin.Jpeg2KImageFile)
|
||||
im.layers = 3
|
||||
im.load()
|
||||
assert_image_similar(im, card, 0.4)
|
||||
|
@ -291,7 +291,7 @@ def test_mct(card: ImageFile.ImageFile) -> 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.save(outfile)
|
||||
|
@ -315,6 +315,18 @@ def test_rgba(ext: str) -> None:
|
|||
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(
|
||||
not os.path.exists(EXTRA_DIR), reason="Extra image files not installed"
|
||||
)
|
||||
|
@ -325,6 +337,18 @@ def test_cmyk() -> None:
|
|||
assert im.getpixel((0, 0)) == (185, 134, 0, 0)
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
not os.path.exists(EXTRA_DIR), reason="Extra image files not installed"
|
||||
)
|
||||
@skip_unless_feature_version("jpg_2000", "2.5.3")
|
||||
def test_cmyk_save() -> None:
|
||||
with Image.open(f"{EXTRA_DIR}/issue205.jp2") as jp2:
|
||||
assert jp2.mode == "CMYK"
|
||||
|
||||
im = roundtrip(jp2)
|
||||
assert_image_equal(im, jp2)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("ext", (".j2k", ".jp2"))
|
||||
def test_16bit_monochrome_has_correct_mode(ext: str) -> None:
|
||||
with Image.open("Tests/images/16bit.cropped" + ext) as im:
|
||||
|
@ -412,6 +436,7 @@ def test_subsampling_decode(name: str) -> None:
|
|||
def test_pclr() -> None:
|
||||
with Image.open(f"{EXTRA_DIR}/issue104_jpxstream.jp2") as im:
|
||||
assert im.mode == "P"
|
||||
assert im.palette is not None
|
||||
assert len(im.palette.colors) == 256
|
||||
assert im.palette.colors[(255, 255, 255)] == 0
|
||||
|
||||
|
@ -419,13 +444,15 @@ def test_pclr() -> None:
|
|||
f"{EXTRA_DIR}/147af3f1083de4393666b7d99b01b58b_signal_sigsegv_130c531_6155_5136.jp2"
|
||||
) as im:
|
||||
assert im.mode == "P"
|
||||
assert im.palette is not None
|
||||
assert len(im.palette.colors) == 139
|
||||
assert im.palette.colors[(0, 0, 0, 0)] == 0
|
||||
|
||||
|
||||
def test_comment() -> None:
|
||||
with Image.open("Tests/images/comment.jp2") as im:
|
||||
assert im.info["comment"] == b"Created by OpenJPEG version 2.5.0"
|
||||
for path in ("Tests/images/9bit.j2k", "Tests/images/comment.jp2"):
|
||||
with Image.open(path) as im:
|
||||
assert im.info["comment"] == b"Created by OpenJPEG version 2.5.0"
|
||||
|
||||
# Test an image that is truncated partway through a codestream
|
||||
with open("Tests/images/comment.jp2", "rb") as fp:
|
||||
|
@ -479,8 +506,7 @@ def test_plt_marker(card: ImageFile.ImageFile) -> None:
|
|||
out.seek(0)
|
||||
while True:
|
||||
marker = out.read(2)
|
||||
if not marker:
|
||||
pytest.fail("End of stream without PLT")
|
||||
assert marker, "End of stream without PLT"
|
||||
|
||||
jp2_boxid = _binary.i16be(marker)
|
||||
if jp2_boxid == 0xFF4F:
|
||||
|
|
|
@ -36,14 +36,11 @@ class LibTiffTestCase:
|
|||
im.load()
|
||||
im.getdata()
|
||||
|
||||
try:
|
||||
assert im._compression == "group4"
|
||||
except AttributeError:
|
||||
print("No _compression")
|
||||
print(dir(im))
|
||||
assert isinstance(im, TiffImagePlugin.TiffImageFile)
|
||||
assert im._compression == "group4"
|
||||
|
||||
# can we write it back out, in a different form.
|
||||
out = str(tmp_path / "temp.png")
|
||||
out = tmp_path / "temp.png"
|
||||
im.save(out)
|
||||
|
||||
out_bytes = io.BytesIO()
|
||||
|
@ -127,7 +124,7 @@ class TestFileLibTiff(LibTiffTestCase):
|
|||
"""Checking to see that the saved image is the same as what we wrote"""
|
||||
test_file = "Tests/images/hopper_g4_500.tif"
|
||||
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)
|
||||
assert rot.size == (500, 500)
|
||||
rot.save(out)
|
||||
|
@ -155,8 +152,9 @@ class TestFileLibTiff(LibTiffTestCase):
|
|||
@pytest.mark.parametrize("legacy_api", (False, True))
|
||||
def test_write_metadata(self, legacy_api: bool, tmp_path: Path) -> None:
|
||||
"""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:
|
||||
assert isinstance(img, TiffImagePlugin.TiffImageFile)
|
||||
img.save(f, tiffinfo=img.tag)
|
||||
|
||||
if legacy_api:
|
||||
|
@ -174,6 +172,7 @@ class TestFileLibTiff(LibTiffTestCase):
|
|||
]
|
||||
|
||||
with Image.open(f) as loaded:
|
||||
assert isinstance(loaded, TiffImagePlugin.TiffImageFile)
|
||||
if legacy_api:
|
||||
reloaded = loaded.tag.named()
|
||||
else:
|
||||
|
@ -216,6 +215,7 @@ class TestFileLibTiff(LibTiffTestCase):
|
|||
# Exclude ones that have special meaning
|
||||
# that we're already testing them
|
||||
with Image.open("Tests/images/hopper_g4.tif") as im:
|
||||
assert isinstance(im, TiffImagePlugin.TiffImageFile)
|
||||
for tag in im.tag_v2:
|
||||
try:
|
||||
del core_items[tag]
|
||||
|
@ -251,7 +251,7 @@ class TestFileLibTiff(LibTiffTestCase):
|
|||
# Extra samples really doesn't make sense in this application.
|
||||
del new_ifd[338]
|
||||
|
||||
out = str(tmp_path / "temp.tif")
|
||||
out = tmp_path / "temp.tif"
|
||||
monkeypatch.setattr(TiffImagePlugin, "WRITE_LIBTIFF", True)
|
||||
|
||||
im.save(out, tiffinfo=new_ifd)
|
||||
|
@ -313,14 +313,15 @@ class TestFileLibTiff(LibTiffTestCase):
|
|||
}
|
||||
|
||||
def check_tags(
|
||||
tiffinfo: TiffImagePlugin.ImageFileDirectory_v2 | dict[int, str]
|
||||
tiffinfo: TiffImagePlugin.ImageFileDirectory_v2 | dict[int, str],
|
||||
) -> None:
|
||||
im = hopper()
|
||||
|
||||
out = str(tmp_path / "temp.tif")
|
||||
out = tmp_path / "temp.tif"
|
||||
im.save(out, tiffinfo=tiffinfo)
|
||||
|
||||
with Image.open(out) as reloaded:
|
||||
assert isinstance(reloaded, TiffImagePlugin.TiffImageFile)
|
||||
for tag, value in tiffinfo.items():
|
||||
reloaded_value = reloaded.tag_v2[tag]
|
||||
if (
|
||||
|
@ -351,14 +352,16 @@ class TestFileLibTiff(LibTiffTestCase):
|
|||
)
|
||||
|
||||
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:
|
||||
assert isinstance(im, TiffImagePlugin.TiffImageFile)
|
||||
im.tag_v2[OSUBFILETYPE] = 1
|
||||
im.save(outfile)
|
||||
|
||||
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:
|
||||
assert isinstance(im, TiffImagePlugin.TiffImageFile)
|
||||
im.tag_v2[SUBIFD] = 10000
|
||||
|
||||
# Should not segfault
|
||||
|
@ -369,17 +372,18 @@ class TestFileLibTiff(LibTiffTestCase):
|
|||
) -> None:
|
||||
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"})
|
||||
|
||||
with Image.open(out) as reloaded:
|
||||
assert isinstance(reloaded, TiffImagePlugin.TiffImageFile)
|
||||
if 700 in reloaded.tag_v2:
|
||||
assert reloaded.tag_v2[700] == b"xmlpacket tag"
|
||||
|
||||
def test_int_dpi(self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
|
||||
# issue #1765
|
||||
im = hopper("RGB")
|
||||
out = str(tmp_path / "temp.tif")
|
||||
out = tmp_path / "temp.tif"
|
||||
monkeypatch.setattr(TiffImagePlugin, "WRITE_LIBTIFF", True)
|
||||
im.save(out, dpi=(72, 72))
|
||||
with Image.open(out) as reloaded:
|
||||
|
@ -387,7 +391,7 @@ class TestFileLibTiff(LibTiffTestCase):
|
|||
|
||||
def test_g3_compression(self, tmp_path: Path) -> None:
|
||||
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")
|
||||
|
||||
with Image.open(out) as reread:
|
||||
|
@ -404,7 +408,7 @@ class TestFileLibTiff(LibTiffTestCase):
|
|||
assert b[0] == ord(b"\xe0")
|
||||
assert b[1] == ord(b"\x01")
|
||||
|
||||
out = str(tmp_path / "temp.tif")
|
||||
out = tmp_path / "temp.tif"
|
||||
# out = "temp.le.tif"
|
||||
im.save(out)
|
||||
with Image.open(out) as reread:
|
||||
|
@ -424,7 +428,7 @@ class TestFileLibTiff(LibTiffTestCase):
|
|||
assert b[0] == ord(b"\x01")
|
||||
assert b[1] == ord(b"\xe0")
|
||||
|
||||
out = str(tmp_path / "temp.tif")
|
||||
out = tmp_path / "temp.tif"
|
||||
im.save(out)
|
||||
with Image.open(out) as reread:
|
||||
assert reread.info["compression"] == im.info["compression"]
|
||||
|
@ -434,12 +438,15 @@ class TestFileLibTiff(LibTiffTestCase):
|
|||
"""Tests String data in info directory"""
|
||||
test_file = "Tests/images/hopper_g4_500.tif"
|
||||
with Image.open(test_file) as orig:
|
||||
out = str(tmp_path / "temp.tif")
|
||||
assert isinstance(orig, TiffImagePlugin.TiffImageFile)
|
||||
|
||||
out = tmp_path / "temp.tif"
|
||||
|
||||
orig.tag[269] = "temp.tif"
|
||||
orig.save(out)
|
||||
|
||||
with Image.open(out) as reread:
|
||||
assert isinstance(reread, TiffImagePlugin.TiffImageFile)
|
||||
assert "temp.tif" == reread.tag_v2[269]
|
||||
assert "temp.tif" == reread.tag[269][0]
|
||||
|
||||
|
@ -461,7 +468,7 @@ class TestFileLibTiff(LibTiffTestCase):
|
|||
def test_blur(self, tmp_path: Path) -> None:
|
||||
# test case from irc, how to do blur on b/w image
|
||||
# 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:
|
||||
im = im.convert("L")
|
||||
|
||||
|
@ -474,7 +481,7 @@ class TestFileLibTiff(LibTiffTestCase):
|
|||
# Test various tiff compressions and assert similar image content but reduced
|
||||
# file sizes.
|
||||
im = hopper("RGB")
|
||||
out = str(tmp_path / "temp.tif")
|
||||
out = tmp_path / "temp.tif"
|
||||
im.save(out)
|
||||
size_raw = os.path.getsize(out)
|
||||
|
||||
|
@ -498,7 +505,7 @@ class TestFileLibTiff(LibTiffTestCase):
|
|||
|
||||
def test_tiff_jpeg_compression(self, tmp_path: Path) -> None:
|
||||
im = hopper("RGB")
|
||||
out = str(tmp_path / "temp.tif")
|
||||
out = tmp_path / "temp.tif"
|
||||
im.save(out, compression="tiff_jpeg")
|
||||
|
||||
with Image.open(out) as reloaded:
|
||||
|
@ -506,7 +513,7 @@ class TestFileLibTiff(LibTiffTestCase):
|
|||
|
||||
def test_tiff_deflate_compression(self, tmp_path: Path) -> None:
|
||||
im = hopper("RGB")
|
||||
out = str(tmp_path / "temp.tif")
|
||||
out = tmp_path / "temp.tif"
|
||||
im.save(out, compression="tiff_deflate")
|
||||
|
||||
with Image.open(out) as reloaded:
|
||||
|
@ -514,7 +521,7 @@ class TestFileLibTiff(LibTiffTestCase):
|
|||
|
||||
def test_quality(self, tmp_path: Path) -> None:
|
||||
im = hopper("RGB")
|
||||
out = str(tmp_path / "temp.tif")
|
||||
out = tmp_path / "temp.tif"
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
im.save(out, compression="tiff_lzw", quality=50)
|
||||
|
@ -529,7 +536,7 @@ class TestFileLibTiff(LibTiffTestCase):
|
|||
|
||||
def test_cmyk_save(self, tmp_path: Path) -> None:
|
||||
im = hopper("CMYK")
|
||||
out = str(tmp_path / "temp.tif")
|
||||
out = tmp_path / "temp.tif"
|
||||
|
||||
im.save(out, compression="tiff_adobe_deflate")
|
||||
assert_image_equal_tofile(im, out)
|
||||
|
@ -538,19 +545,20 @@ class TestFileLibTiff(LibTiffTestCase):
|
|||
def test_palette_save(
|
||||
self, im: Image.Image, monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
||||
) -> None:
|
||||
out = str(tmp_path / "temp.tif")
|
||||
out = tmp_path / "temp.tif"
|
||||
|
||||
monkeypatch.setattr(TiffImagePlugin, "WRITE_LIBTIFF", True)
|
||||
im.save(out)
|
||||
|
||||
with Image.open(out) as reloaded:
|
||||
# colormap/palette tag
|
||||
assert isinstance(reloaded, TiffImagePlugin.TiffImageFile)
|
||||
assert len(reloaded.tag_v2[320]) == 768
|
||||
|
||||
@pytest.mark.parametrize("compression", ("tiff_ccitt", "group3", "group4"))
|
||||
def test_bw_compression_w_rgb(self, compression: str, tmp_path: Path) -> None:
|
||||
im = hopper("RGB")
|
||||
out = str(tmp_path / "temp.tif")
|
||||
out = tmp_path / "temp.tif"
|
||||
|
||||
with pytest.raises(OSError):
|
||||
im.save(out, compression=compression)
|
||||
|
@ -576,6 +584,7 @@ class TestFileLibTiff(LibTiffTestCase):
|
|||
with Image.open("Tests/images/multipage.tiff") as im:
|
||||
# file is a multipage tiff, 10x10 green, 10x10 red, 20x20 blue
|
||||
|
||||
assert isinstance(im, TiffImagePlugin.TiffImageFile)
|
||||
im.seek(0)
|
||||
assert im.size == (10, 10)
|
||||
assert im.convert("RGB").getpixel((0, 0)) == (0, 128, 0)
|
||||
|
@ -595,6 +604,7 @@ class TestFileLibTiff(LibTiffTestCase):
|
|||
# issue #862
|
||||
monkeypatch.setattr(TiffImagePlugin, "READ_LIBTIFF", True)
|
||||
with Image.open("Tests/images/multipage.tiff") as im:
|
||||
assert isinstance(im, TiffImagePlugin.TiffImageFile)
|
||||
frames = im.n_frames
|
||||
assert frames == 3
|
||||
for _ in range(frames):
|
||||
|
@ -614,6 +624,7 @@ class TestFileLibTiff(LibTiffTestCase):
|
|||
def test__next(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setattr(TiffImagePlugin, "READ_LIBTIFF", True)
|
||||
with Image.open("Tests/images/hopper.tif") as im:
|
||||
assert isinstance(im, TiffImagePlugin.TiffImageFile)
|
||||
assert not im.tag.next
|
||||
im.load()
|
||||
assert not im.tag.next
|
||||
|
@ -690,25 +701,29 @@ class TestFileLibTiff(LibTiffTestCase):
|
|||
|
||||
def test_save_ycbcr(self, tmp_path: Path) -> None:
|
||||
im = hopper("YCbCr")
|
||||
outfile = str(tmp_path / "temp.tif")
|
||||
outfile = tmp_path / "temp.tif"
|
||||
im.save(outfile, compression="jpeg")
|
||||
|
||||
with Image.open(outfile) as reloaded:
|
||||
assert isinstance(reloaded, TiffImagePlugin.TiffImageFile)
|
||||
assert reloaded.tag_v2[530] == (1, 1)
|
||||
assert reloaded.tag_v2[532] == (0, 255, 128, 255, 128, 255)
|
||||
|
||||
def test_exif_ifd(self) -> None:
|
||||
out = io.BytesIO()
|
||||
with Image.open("Tests/images/tiff_adobe_deflate.tif") as im:
|
||||
assert isinstance(im, TiffImagePlugin.TiffImageFile)
|
||||
assert im.tag_v2[34665] == 125456
|
||||
im.save(out, "TIFF")
|
||||
|
||||
with Image.open(out) as reloaded:
|
||||
assert isinstance(reloaded, TiffImagePlugin.TiffImageFile)
|
||||
assert 34665 not in reloaded.tag_v2
|
||||
|
||||
im.save(out, "TIFF", tiffinfo={34665: 125456})
|
||||
|
||||
with Image.open(out) as reloaded:
|
||||
assert isinstance(reloaded, TiffImagePlugin.TiffImageFile)
|
||||
if Image.core.libtiff_support_custom_tags:
|
||||
assert reloaded.tag_v2[34665] == 125456
|
||||
|
||||
|
@ -717,7 +732,7 @@ class TestFileLibTiff(LibTiffTestCase):
|
|||
) -> None:
|
||||
# issue 1597
|
||||
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)
|
||||
# this shouldn't crash
|
||||
|
@ -728,7 +743,7 @@ class TestFileLibTiff(LibTiffTestCase):
|
|||
# Test TIFF with tag 297 (Page Number) having value of 0 0.
|
||||
# The first number is the current page number.
|
||||
# The second is the total number of pages, zero means not available.
|
||||
outfile = str(tmp_path / "temp.tif")
|
||||
outfile = tmp_path / "temp.tif"
|
||||
# Created by printing a page in Chrome to PDF, then:
|
||||
# /usr/bin/gs -q -sDEVICE=tiffg3 -sOutputFile=total-pages-zero.tif
|
||||
# -dNOPAUSE /tmp/test.pdf -c quit
|
||||
|
@ -740,7 +755,7 @@ class TestFileLibTiff(LibTiffTestCase):
|
|||
def test_fd_duplication(self, tmp_path: Path) -> None:
|
||||
# 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("Tests/images/g4-multi.tiff", "rb") as src:
|
||||
f.write(src.read())
|
||||
|
@ -783,13 +798,14 @@ class TestFileLibTiff(LibTiffTestCase):
|
|||
with Image.open("Tests/images/hopper.iccprofile.tif") as img:
|
||||
icc_profile = img.info["icc_profile"]
|
||||
|
||||
out = str(tmp_path / "temp.tif")
|
||||
out = tmp_path / "temp.tif"
|
||||
img.save(out, icc_profile=icc_profile)
|
||||
with Image.open(out) as reloaded:
|
||||
assert icc_profile == reloaded.info["icc_profile"]
|
||||
|
||||
def test_multipage_compression(self) -> None:
|
||||
with Image.open("Tests/images/compression.tif") as im:
|
||||
assert isinstance(im, TiffImagePlugin.TiffImageFile)
|
||||
im.seek(0)
|
||||
assert im._compression == "tiff_ccitt"
|
||||
assert im.size == (10, 10)
|
||||
|
@ -806,7 +822,7 @@ class TestFileLibTiff(LibTiffTestCase):
|
|||
|
||||
def test_save_tiff_with_jpegtables(self, tmp_path: Path) -> None:
|
||||
# Arrange
|
||||
outfile = str(tmp_path / "temp.tif")
|
||||
outfile = tmp_path / "temp.tif"
|
||||
|
||||
# Created with ImageMagick: convert hopper.jpg hopper_jpg.tif
|
||||
# Contains JPEGTables (347) tag
|
||||
|
@ -868,7 +884,7 @@ class TestFileLibTiff(LibTiffTestCase):
|
|||
self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
||||
) -> None:
|
||||
im = Image.new("F", (1, 1))
|
||||
out = str(tmp_path / "temp.tif")
|
||||
out = tmp_path / "temp.tif"
|
||||
monkeypatch.setattr(TiffImagePlugin, "WRITE_LIBTIFF", True)
|
||||
im.save(out)
|
||||
|
||||
|
@ -1012,7 +1028,7 @@ class TestFileLibTiff(LibTiffTestCase):
|
|||
@pytest.mark.parametrize("compression", (None, "jpeg"))
|
||||
def test_block_tile_tags(self, compression: str | None, tmp_path: Path) -> None:
|
||||
im = hopper()
|
||||
out = str(tmp_path / "temp.tif")
|
||||
out = tmp_path / "temp.tif"
|
||||
|
||||
tags = {
|
||||
TiffImagePlugin.TILEWIDTH: 256,
|
||||
|
@ -1030,6 +1046,17 @@ class TestFileLibTiff(LibTiffTestCase):
|
|||
with Image.open("Tests/images/old-style-jpeg-compression.tif") as im:
|
||||
assert_image_equal_tofile(im, "Tests/images/old-style-jpeg-compression.png")
|
||||
|
||||
def test_old_style_jpeg_orientation(self) -> None:
|
||||
with open("Tests/images/old-style-jpeg-compression.tif", "rb") as fp:
|
||||
data = fp.read()
|
||||
|
||||
# Set EXIF Orientation to 2
|
||||
data = data[:102] + b"\x02" + data[103:]
|
||||
|
||||
with Image.open(io.BytesIO(data)) as im:
|
||||
im = im.transpose(Image.Transpose.FLIP_LEFT_RIGHT)
|
||||
assert_image_equal_tofile(im, "Tests/images/old-style-jpeg-compression.png")
|
||||
|
||||
def test_open_missing_samplesperpixel(self) -> None:
|
||||
with Image.open(
|
||||
"Tests/images/old-style-jpeg-compression-no-samplesperpixel.tif"
|
||||
|
@ -1083,6 +1110,7 @@ class TestFileLibTiff(LibTiffTestCase):
|
|||
with Image.open("Tests/images/g4_orientation_1.tif") as base_im:
|
||||
for i in range(2, 9):
|
||||
with Image.open("Tests/images/g4_orientation_" + str(i) + ".tif") as im:
|
||||
assert isinstance(im, TiffImagePlugin.TiffImageFile)
|
||||
assert 274 in im.tag_v2
|
||||
|
||||
im.load()
|
||||
|
@ -1107,13 +1135,15 @@ class TestFileLibTiff(LibTiffTestCase):
|
|||
)
|
||||
def test_buffering(self, test_file: str) -> None:
|
||||
# load exif first
|
||||
with Image.open(open(test_file, "rb", buffering=1048576)) as im:
|
||||
exif = dict(im.getexif())
|
||||
with open(test_file, "rb", buffering=1048576) as f:
|
||||
with Image.open(f) as im:
|
||||
exif = dict(im.getexif())
|
||||
|
||||
# load image before exif
|
||||
with Image.open(open(test_file, "rb", buffering=1048576)) as im2:
|
||||
im2.load()
|
||||
exif_after_load = dict(im2.getexif())
|
||||
with open(test_file, "rb", buffering=1048576) as f:
|
||||
with Image.open(f) as im2:
|
||||
im2.load()
|
||||
exif_after_load = dict(im2.getexif())
|
||||
|
||||
assert exif == exif_after_load
|
||||
|
||||
|
@ -1142,16 +1172,14 @@ class TestFileLibTiff(LibTiffTestCase):
|
|||
def test_realloc_overflow(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setattr(TiffImagePlugin, "READ_LIBTIFF", True)
|
||||
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 str(e.value) == "-9"
|
||||
with pytest.raises(OSError, match="decoder error -9"):
|
||||
im.load()
|
||||
|
||||
@pytest.mark.parametrize("compression", ("tiff_adobe_deflate", "jpeg"))
|
||||
def test_save_multistrip(self, compression: str, tmp_path: Path) -> None:
|
||||
im = hopper("RGB").resize((256, 256))
|
||||
out = str(tmp_path / "temp.tif")
|
||||
out = tmp_path / "temp.tif"
|
||||
im.save(out, compression=compression)
|
||||
|
||||
with Image.open(out) as im:
|
||||
|
@ -1160,34 +1188,33 @@ class TestFileLibTiff(LibTiffTestCase):
|
|||
assert len(im.tag_v2[STRIPOFFSETS]) > 1
|
||||
|
||||
@pytest.mark.parametrize("argument", (True, False))
|
||||
def test_save_single_strip(self, argument: bool, tmp_path: Path) -> None:
|
||||
def test_save_single_strip(
|
||||
self, argument: bool, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
im = hopper("RGB").resize((256, 256))
|
||||
out = str(tmp_path / "temp.tif")
|
||||
out = tmp_path / "temp.tif"
|
||||
|
||||
if not argument:
|
||||
TiffImagePlugin.STRIP_SIZE = 2**18
|
||||
try:
|
||||
arguments: dict[str, str | int] = {"compression": "tiff_adobe_deflate"}
|
||||
if argument:
|
||||
arguments["strip_size"] = 2**18
|
||||
im.save(out, "TIFF", **arguments)
|
||||
monkeypatch.setattr(TiffImagePlugin, "STRIP_SIZE", 2**18)
|
||||
arguments: dict[str, str | int] = {"compression": "tiff_adobe_deflate"}
|
||||
if argument:
|
||||
arguments["strip_size"] = 2**18
|
||||
im.save(out, "TIFF", **arguments)
|
||||
|
||||
with Image.open(out) as im:
|
||||
assert isinstance(im, TiffImagePlugin.TiffImageFile)
|
||||
assert len(im.tag_v2[STRIPOFFSETS]) == 1
|
||||
finally:
|
||||
TiffImagePlugin.STRIP_SIZE = 65536
|
||||
with Image.open(out) as im:
|
||||
assert isinstance(im, TiffImagePlugin.TiffImageFile)
|
||||
assert len(im.tag_v2[STRIPOFFSETS]) == 1
|
||||
|
||||
@pytest.mark.parametrize("compression", ("tiff_adobe_deflate", None))
|
||||
def test_save_zero(self, compression: str | None, tmp_path: Path) -> None:
|
||||
im = Image.new("RGB", (0, 0))
|
||||
out = str(tmp_path / "temp.tif")
|
||||
out = tmp_path / "temp.tif"
|
||||
with pytest.raises(SystemError):
|
||||
im.save(out, compression=compression)
|
||||
|
||||
def test_save_many_compressed(self, tmp_path: Path) -> None:
|
||||
im = hopper()
|
||||
out = str(tmp_path / "temp.tif")
|
||||
out = tmp_path / "temp.tif"
|
||||
for _ in range(10000):
|
||||
im.save(out, compression="jpeg")
|
||||
|
||||
|
|
|
@ -30,11 +30,13 @@ def test_sanity() -> None:
|
|||
|
||||
def test_n_frames() -> None:
|
||||
with Image.open(TEST_FILE) as im:
|
||||
assert isinstance(im, MicImagePlugin.MicImageFile)
|
||||
assert im.n_frames == 1
|
||||
|
||||
|
||||
def test_is_animated() -> None:
|
||||
with Image.open(TEST_FILE) as im:
|
||||
assert isinstance(im, MicImagePlugin.MicImageFile)
|
||||
assert not im.is_animated
|
||||
|
||||
|
||||
|
@ -55,10 +57,11 @@ def test_seek() -> None:
|
|||
|
||||
def test_close() -> None:
|
||||
with Image.open(TEST_FILE) as im:
|
||||
pass
|
||||
assert isinstance(im, MicImagePlugin.MicImageFile)
|
||||
assert im.ole.fp.closed
|
||||
|
||||
im = Image.open(TEST_FILE)
|
||||
assert isinstance(im, MicImagePlugin.MicImageFile)
|
||||
im.close()
|
||||
assert im.ole.fp.closed
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@ from typing import Any
|
|||
|
||||
import pytest
|
||||
|
||||
from PIL import Image, ImageFile, MpoImagePlugin
|
||||
from PIL import Image, ImageFile, JpegImagePlugin, MpoImagePlugin
|
||||
|
||||
from .helper import (
|
||||
assert_image_equal,
|
||||
|
@ -29,21 +29,26 @@ def roundtrip(im: Image.Image, **options: Any) -> ImageFile.ImageFile:
|
|||
|
||||
@pytest.mark.parametrize("test_file", test_files)
|
||||
def test_sanity(test_file: str) -> None:
|
||||
with Image.open(test_file) as im:
|
||||
def check(im: ImageFile.ImageFile) -> None:
|
||||
im.load()
|
||||
assert im.mode == "RGB"
|
||||
assert im.size == (640, 480)
|
||||
assert im.format == "MPO"
|
||||
|
||||
with Image.open(test_file) as im:
|
||||
check(im)
|
||||
with MpoImagePlugin.MpoImageFile(test_file) as im:
|
||||
check(im)
|
||||
|
||||
|
||||
@pytest.mark.skipif(is_pypy(), reason="Requires CPython")
|
||||
def test_unclosed_file() -> None:
|
||||
def open() -> None:
|
||||
def open_test_image() -> None:
|
||||
im = Image.open(test_files[0])
|
||||
im.load()
|
||||
|
||||
with pytest.warns(ResourceWarning):
|
||||
open()
|
||||
open_test_image()
|
||||
|
||||
|
||||
def test_closed_file() -> None:
|
||||
|
@ -75,10 +80,11 @@ def test_context_manager() -> None:
|
|||
def test_app(test_file: str) -> None:
|
||||
# Test APP/COM reader (@PIL135)
|
||||
with Image.open(test_file) as im:
|
||||
assert isinstance(im, MpoImagePlugin.MpoImageFile)
|
||||
assert im.applist[0][0] == "APP1"
|
||||
assert im.applist[1][0] == "APP2"
|
||||
assert (
|
||||
im.applist[1][1][:16] == b"MPF\x00MM\x00*\x00\x00\x00\x08\x00\x03\xb0\x00"
|
||||
assert im.applist[1][1].startswith(
|
||||
b"MPF\x00MM\x00*\x00\x00\x00\x08\x00\x03\xb0\x00"
|
||||
)
|
||||
assert len(im.applist) == 2
|
||||
|
||||
|
@ -215,12 +221,14 @@ def test_seek(test_file: str) -> None:
|
|||
|
||||
def test_n_frames() -> None:
|
||||
with Image.open("Tests/images/sugarshack.mpo") as im:
|
||||
assert isinstance(im, MpoImagePlugin.MpoImageFile)
|
||||
assert im.n_frames == 2
|
||||
assert im.is_animated
|
||||
|
||||
|
||||
def test_eoferror() -> None:
|
||||
with Image.open("Tests/images/sugarshack.mpo") as im:
|
||||
assert isinstance(im, MpoImagePlugin.MpoImageFile)
|
||||
n_frames = im.n_frames
|
||||
|
||||
# Test seeking past the last frame
|
||||
|
@ -234,6 +242,8 @@ def test_eoferror() -> None:
|
|||
|
||||
def test_adopt_jpeg() -> None:
|
||||
with Image.open("Tests/images/hopper.jpg") as im:
|
||||
assert isinstance(im, JpegImagePlugin.JpegImageFile)
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
MpoImagePlugin.MpoImageFile.adopt(im)
|
||||
|
||||
|
@ -297,3 +307,15 @@ def test_save_all() -> None:
|
|||
# Test that a single frame image will not be saved as an MPO
|
||||
jpg = roundtrip(im, save_all=True)
|
||||
assert "mp" not in jpg.info
|
||||
|
||||
|
||||
def test_save_xmp() -> None:
|
||||
im = Image.new("RGB", (1, 1))
|
||||
im2 = Image.new("RGB", (1, 1), "#f00")
|
||||
im2.encoderinfo = {"xmp": b"Second frame"}
|
||||
im_reloaded = roundtrip(im, xmp=b"First frame", save_all=True, append_images=[im2])
|
||||
|
||||
assert im_reloaded.info["xmp"] == b"First frame"
|
||||
|
||||
im_reloaded.seek(1)
|
||||
assert im_reloaded.info["xmp"] == b"Second frame"
|
||||
|
|
|
@ -15,7 +15,7 @@ YA_EXTRA_DIR = "Tests/images/msp"
|
|||
|
||||
|
||||
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)
|
||||
|
||||
|
@ -84,7 +84,7 @@ def test_msp_v2() -> None:
|
|||
def test_cannot_save_wrong_mode(tmp_path: Path) -> None:
|
||||
# Arrange
|
||||
im = hopper()
|
||||
filename = str(tmp_path / "temp.msp")
|
||||
filename = tmp_path / "temp.msp"
|
||||
|
||||
# Act/Assert
|
||||
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:
|
||||
# Arrange
|
||||
im = hopper(mode)
|
||||
outfile = str(tmp_path / ("temp_" + mode + ".palm"))
|
||||
outfile = tmp_path / ("temp_" + mode + ".palm")
|
||||
|
||||
# Act
|
||||
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:
|
||||
outfile = str(tmp_path / "temp.png")
|
||||
outfile = tmp_path / "temp.png"
|
||||
rc = subprocess.call(
|
||||
magick + [f, outfile], stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT
|
||||
)
|
||||
|
@ -43,6 +43,11 @@ def roundtrip(tmp_path: Path, mode: str) -> None:
|
|||
|
||||
im.save(outfile)
|
||||
converted = open_with_magick(magick, tmp_path, outfile)
|
||||
if mode == "P":
|
||||
assert converted.mode == "P"
|
||||
|
||||
im = im.convert("RGB")
|
||||
converted = converted.convert("RGB")
|
||||
assert_image_equal(converted, im)
|
||||
|
||||
|
||||
|
@ -55,7 +60,6 @@ def test_monochrome(tmp_path: Path) -> None:
|
|||
roundtrip(tmp_path, mode)
|
||||
|
||||
|
||||
@pytest.mark.xfail(reason="Palm P image is wrong")
|
||||
def test_p_mode(tmp_path: Path) -> None:
|
||||
# Arrange
|
||||
mode = "P"
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import io
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
@ -10,7 +11,7 @@ from .helper import assert_image_equal, hopper
|
|||
|
||||
|
||||
def _roundtrip(tmp_path: Path, im: Image.Image) -> None:
|
||||
f = str(tmp_path / "temp.pcx")
|
||||
f = tmp_path / "temp.pcx"
|
||||
im.save(f)
|
||||
with Image.open(f) as im2:
|
||||
assert im2.mode == im.mode
|
||||
|
@ -30,12 +31,34 @@ def test_sanity(tmp_path: Path) -> None:
|
|||
_roundtrip(tmp_path, im)
|
||||
|
||||
# Test an unsupported mode
|
||||
f = str(tmp_path / "temp.pcx")
|
||||
f = tmp_path / "temp.pcx"
|
||||
im = hopper("RGBA")
|
||||
with pytest.raises(ValueError):
|
||||
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:
|
||||
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:
|
||||
# Arrange
|
||||
outfile = str(tmp_path / "temp.pdf")
|
||||
outfile = tmp_path / "temp.pdf"
|
||||
with Image.open("Tests/images/pil123p.png") as im:
|
||||
assert im.mode == "P"
|
||||
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:
|
||||
im = hopper("PA")
|
||||
outfile = str(tmp_path / "temp_PA.pdf")
|
||||
outfile = tmp_path / "temp_PA.pdf"
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
im.save(outfile)
|
||||
|
@ -89,7 +89,7 @@ def test_unsupported_mode(tmp_path: Path) -> None:
|
|||
def test_resolution(tmp_path: Path) -> None:
|
||||
im = hopper()
|
||||
|
||||
outfile = str(tmp_path / "temp.pdf")
|
||||
outfile = tmp_path / "temp.pdf"
|
||||
im.save(outfile, resolution=150)
|
||||
|
||||
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:
|
||||
im = hopper()
|
||||
|
||||
outfile = str(tmp_path / "temp.pdf")
|
||||
outfile = tmp_path / "temp.pdf"
|
||||
im.save(outfile, "PDF", **params)
|
||||
|
||||
with open(outfile, "rb") as fp:
|
||||
|
@ -144,7 +144,7 @@ def test_save_all(tmp_path: Path) -> None:
|
|||
|
||||
# Multiframe image
|
||||
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)
|
||||
|
||||
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:
|
||||
# Test saving a multiframe image without save_all
|
||||
with Image.open("Tests/images/dispose_bgnd.gif") as im:
|
||||
outfile = str(tmp_path / "temp.pdf")
|
||||
outfile = tmp_path / "temp.pdf"
|
||||
im.save(outfile)
|
||||
|
||||
assert os.path.isfile(outfile)
|
||||
|
@ -264,7 +264,7 @@ def test_pdf_append(tmp_path: Path) -> None:
|
|||
# append some info
|
||||
pdf.info.Title = "abc"
|
||||
pdf.info.Author = "def"
|
||||
pdf.info.Subject = "ghi\uABCD"
|
||||
pdf.info.Subject = "ghi\uabcd"
|
||||
pdf.info.Keywords = "qw)e\\r(ty"
|
||||
pdf.info.Creator = "hopper()"
|
||||
pdf.start_writing()
|
||||
|
@ -292,7 +292,7 @@ def test_pdf_append(tmp_path: Path) -> None:
|
|||
assert pdf.info.Title == "abc"
|
||||
assert pdf.info.Producer == "PdfParser"
|
||||
assert pdf.info.Keywords == "qw)e\\r(ty"
|
||||
assert pdf.info.Subject == "ghi\uABCD"
|
||||
assert pdf.info.Subject == "ghi\uabcd"
|
||||
assert b"CreationDate" in pdf.info
|
||||
assert b"ModDate" in pdf.info
|
||||
check_pdf_pages_consistency(pdf)
|
||||
|
|
|
@ -68,7 +68,7 @@ def roundtrip(im: Image.Image, **options: Any) -> PngImagePlugin.PngImageFile:
|
|||
|
||||
@skip_unless_feature("zlib")
|
||||
class TestFilePng:
|
||||
def get_chunks(self, filename: str) -> list[bytes]:
|
||||
def get_chunks(self, filename: Path) -> list[bytes]:
|
||||
chunks = []
|
||||
with open(filename, "rb") as fp:
|
||||
fp.read(8)
|
||||
|
@ -89,7 +89,7 @@ class TestFilePng:
|
|||
assert version is not None
|
||||
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)
|
||||
|
||||
|
@ -250,7 +250,7 @@ class TestFilePng:
|
|||
# each palette entry
|
||||
assert len(im.info["transparency"]) == 256
|
||||
|
||||
test_file = str(tmp_path / "temp.png")
|
||||
test_file = tmp_path / "temp.png"
|
||||
im.save(test_file)
|
||||
|
||||
# check if saved image contains same transparency
|
||||
|
@ -271,7 +271,7 @@ class TestFilePng:
|
|||
assert im.info["transparency"] == 164
|
||||
assert im.getpixel((31, 31)) == 164
|
||||
|
||||
test_file = str(tmp_path / "temp.png")
|
||||
test_file = tmp_path / "temp.png"
|
||||
im.save(test_file)
|
||||
|
||||
# check if saved image contains same transparency
|
||||
|
@ -294,7 +294,7 @@ class TestFilePng:
|
|||
assert im.getcolors() == [(100, (0, 0, 0, 0))]
|
||||
|
||||
im = im.convert("P")
|
||||
test_file = str(tmp_path / "temp.png")
|
||||
test_file = tmp_path / "temp.png"
|
||||
im.save(test_file)
|
||||
|
||||
# check if saved image contains same transparency
|
||||
|
@ -315,7 +315,7 @@ class TestFilePng:
|
|||
im_rgba = im.convert("RGBA")
|
||||
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)
|
||||
|
||||
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:
|
||||
in_file = "Tests/images/caption_6_33_22.png"
|
||||
with Image.open(in_file) as im:
|
||||
test_file = str(tmp_path / "temp.png")
|
||||
test_file = tmp_path / "temp.png"
|
||||
im.save(test_file)
|
||||
|
||||
def test_load_verify(self) -> None:
|
||||
|
@ -363,7 +363,7 @@ class TestFilePng:
|
|||
with pytest.raises((OSError, SyntaxError)):
|
||||
im.verify()
|
||||
|
||||
def test_verify_ignores_crc_error(self) -> None:
|
||||
def test_verify_ignores_crc_error(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
# check ignores crc errors in ancillary chunks
|
||||
|
||||
chunk_data = chunk(b"tEXt", b"spam")
|
||||
|
@ -373,24 +373,20 @@ class TestFilePng:
|
|||
with pytest.raises(SyntaxError):
|
||||
PngImagePlugin.PngImageFile(BytesIO(image_data))
|
||||
|
||||
ImageFile.LOAD_TRUNCATED_IMAGES = True
|
||||
try:
|
||||
im = load(image_data)
|
||||
assert im is not None
|
||||
finally:
|
||||
ImageFile.LOAD_TRUNCATED_IMAGES = False
|
||||
monkeypatch.setattr(ImageFile, "LOAD_TRUNCATED_IMAGES", True)
|
||||
im = load(image_data)
|
||||
assert im is not None
|
||||
|
||||
def test_verify_not_ignores_crc_error_in_required_chunk(self) -> None:
|
||||
def test_verify_not_ignores_crc_error_in_required_chunk(
|
||||
self, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
# check does not ignore crc errors in required chunks
|
||||
|
||||
image_data = MAGIC + IHDR[:-1] + b"q" + TAIL
|
||||
|
||||
ImageFile.LOAD_TRUNCATED_IMAGES = True
|
||||
try:
|
||||
with pytest.raises(SyntaxError):
|
||||
PngImagePlugin.PngImageFile(BytesIO(image_data))
|
||||
finally:
|
||||
ImageFile.LOAD_TRUNCATED_IMAGES = False
|
||||
monkeypatch.setattr(ImageFile, "LOAD_TRUNCATED_IMAGES", True)
|
||||
with pytest.raises(SyntaxError):
|
||||
PngImagePlugin.PngImageFile(BytesIO(image_data))
|
||||
|
||||
def test_roundtrip_dpi(self) -> None:
|
||||
# Check dpi roundtripping
|
||||
|
@ -492,7 +488,7 @@ class TestFilePng:
|
|||
im = hopper("P")
|
||||
im.info["transparency"] = 0
|
||||
|
||||
f = str(tmp_path / "temp.png")
|
||||
f = tmp_path / "temp.png"
|
||||
im.save(f)
|
||||
|
||||
with Image.open(f) as im2:
|
||||
|
@ -553,7 +549,7 @@ class TestFilePng:
|
|||
|
||||
def test_chunk_order(self, tmp_path: Path) -> None:
|
||||
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))
|
||||
|
||||
chunks = self.get_chunks(test_file)
|
||||
|
@ -580,6 +576,7 @@ class TestFilePng:
|
|||
|
||||
def test_read_private_chunks(self) -> None:
|
||||
with Image.open("Tests/images/exif.png") as im:
|
||||
assert isinstance(im, PngImagePlugin.PngImageFile)
|
||||
assert im.private_chunks == [(b"orNT", b"\x01")]
|
||||
|
||||
def test_roundtrip_private_chunk(self) -> None:
|
||||
|
@ -600,8 +597,9 @@ class TestFilePng:
|
|||
(b"prIV", b"VALUE3", True),
|
||||
]
|
||||
|
||||
def test_textual_chunks_after_idat(self) -> None:
|
||||
def test_textual_chunks_after_idat(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
with Image.open("Tests/images/hopper.png") as im:
|
||||
assert isinstance(im, PngImagePlugin.PngImageFile)
|
||||
assert "comment" in im.text
|
||||
for k, v in {
|
||||
"date:create": "2014-09-04T09:37:08+03:00",
|
||||
|
@ -611,22 +609,25 @@ class TestFilePng:
|
|||
|
||||
# Raises a SyntaxError in load_end
|
||||
with Image.open("Tests/images/broken_data_stream.png") as im:
|
||||
assert isinstance(im, PngImagePlugin.PngImageFile)
|
||||
with pytest.raises(OSError):
|
||||
assert isinstance(im.text, dict)
|
||||
|
||||
# Raises a UnicodeDecodeError in load_end
|
||||
with Image.open("Tests/images/truncated_image.png") as im:
|
||||
# The file is truncated
|
||||
with pytest.raises(OSError):
|
||||
im.text()
|
||||
ImageFile.LOAD_TRUNCATED_IMAGES = True
|
||||
assert isinstance(im.text, dict)
|
||||
ImageFile.LOAD_TRUNCATED_IMAGES = False
|
||||
|
||||
# Raises an EOFError in load_end
|
||||
with Image.open("Tests/images/hopper_idat_after_image_end.png") as im:
|
||||
assert isinstance(im, PngImagePlugin.PngImageFile)
|
||||
assert im.text == {"TXT": "VALUE", "ZIP": "VALUE"}
|
||||
|
||||
# Raises a UnicodeDecodeError in load_end
|
||||
with Image.open("Tests/images/truncated_image.png") as im:
|
||||
assert isinstance(im, PngImagePlugin.PngImageFile)
|
||||
|
||||
# The file is truncated
|
||||
with pytest.raises(OSError):
|
||||
im.text
|
||||
monkeypatch.setattr(ImageFile, "LOAD_TRUNCATED_IMAGES", True)
|
||||
assert isinstance(im.text, dict)
|
||||
|
||||
def test_unknown_compression_method(self) -> None:
|
||||
with pytest.raises(SyntaxError, match="Unknown compression method"):
|
||||
PngImagePlugin.PngImageFile("Tests/images/unknown_compression_method.png")
|
||||
|
@ -651,21 +652,22 @@ class TestFilePng:
|
|||
@pytest.mark.parametrize(
|
||||
"cid", (b"IHDR", b"sRGB", b"pHYs", b"acTL", b"fcTL", b"fdAT")
|
||||
)
|
||||
def test_truncated_chunks(self, cid: bytes) -> None:
|
||||
def test_truncated_chunks(
|
||||
self, cid: bytes, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
fp = BytesIO()
|
||||
with PngImagePlugin.PngStream(fp) as png:
|
||||
with pytest.raises(ValueError):
|
||||
png.call(cid, 0, 0)
|
||||
|
||||
ImageFile.LOAD_TRUNCATED_IMAGES = True
|
||||
monkeypatch.setattr(ImageFile, "LOAD_TRUNCATED_IMAGES", True)
|
||||
png.call(cid, 0, 0)
|
||||
ImageFile.LOAD_TRUNCATED_IMAGES = False
|
||||
|
||||
@pytest.mark.parametrize("save_all", (True, False))
|
||||
def test_specify_bits(self, save_all: bool, tmp_path: Path) -> None:
|
||||
im = hopper("P")
|
||||
|
||||
out = str(tmp_path / "temp.png")
|
||||
out = tmp_path / "temp.png"
|
||||
im.save(out, bits=4, save_all=save_all)
|
||||
|
||||
with Image.open(out) as reloaded:
|
||||
|
@ -675,8 +677,8 @@ class TestFilePng:
|
|||
im = Image.new("P", (1, 1))
|
||||
im.putpalette((1, 1, 1))
|
||||
|
||||
out = str(tmp_path / "temp.png")
|
||||
im.save(str(tmp_path / "temp.png"))
|
||||
out = tmp_path / "temp.png"
|
||||
im.save(out)
|
||||
|
||||
with Image.open(out) as reloaded:
|
||||
assert len(reloaded.png.im_palette[1]) == 3
|
||||
|
@ -725,11 +727,12 @@ class TestFilePng:
|
|||
|
||||
def test_exif_save(self, tmp_path: Path) -> None:
|
||||
# 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:
|
||||
im.save(test_file)
|
||||
|
||||
with Image.open(test_file) as reloaded:
|
||||
assert isinstance(reloaded, PngImagePlugin.PngImageFile)
|
||||
assert reloaded._getexif() is None
|
||||
|
||||
# Test passing in exif
|
||||
|
@ -745,7 +748,7 @@ class TestFilePng:
|
|||
)
|
||||
def test_exif_from_jpg(self, tmp_path: Path) -> None:
|
||||
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())
|
||||
|
||||
with Image.open(test_file) as reloaded:
|
||||
|
@ -754,7 +757,7 @@ class TestFilePng:
|
|||
|
||||
def test_exif_argument(self, tmp_path: Path) -> None:
|
||||
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")
|
||||
|
||||
with Image.open(test_file) as reloaded:
|
||||
|
@ -772,38 +775,31 @@ class TestFilePng:
|
|||
im.seek(1)
|
||||
|
||||
@pytest.mark.parametrize("buffer", (True, False))
|
||||
def test_save_stdout(self, buffer: bool) -> None:
|
||||
old_stdout = sys.stdout
|
||||
def test_save_stdout(self, buffer: bool, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
|
||||
class MyStdOut:
|
||||
buffer = BytesIO()
|
||||
|
||||
mystdout: MyStdOut | BytesIO = MyStdOut() if buffer else BytesIO()
|
||||
|
||||
sys.stdout = mystdout
|
||||
monkeypatch.setattr(sys, "stdout", mystdout)
|
||||
|
||||
with Image.open(TEST_PNG_FILE) as im:
|
||||
im.save(sys.stdout, "PNG")
|
||||
|
||||
# Reset stdout
|
||||
sys.stdout = old_stdout
|
||||
|
||||
if isinstance(mystdout, MyStdOut):
|
||||
mystdout = mystdout.buffer
|
||||
with Image.open(mystdout) as reloaded:
|
||||
assert_image_equal_tofile(reloaded, TEST_PNG_FILE)
|
||||
|
||||
def test_truncated_end_chunk(self) -> None:
|
||||
def test_truncated_end_chunk(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
with Image.open("Tests/images/truncated_end_chunk.png") as im:
|
||||
with pytest.raises(OSError):
|
||||
im.load()
|
||||
|
||||
ImageFile.LOAD_TRUNCATED_IMAGES = True
|
||||
try:
|
||||
with Image.open("Tests/images/truncated_end_chunk.png") as im:
|
||||
assert_image_equal_tofile(im, "Tests/images/hopper.png")
|
||||
finally:
|
||||
ImageFile.LOAD_TRUNCATED_IMAGES = False
|
||||
monkeypatch.setattr(ImageFile, "LOAD_TRUNCATED_IMAGES", True)
|
||||
with Image.open("Tests/images/truncated_end_chunk.png") as im:
|
||||
assert_image_equal_tofile(im, "Tests/images/hopper.png")
|
||||
|
||||
|
||||
@pytest.mark.skipif(is_win32(), reason="Requires Unix or macOS")
|
||||
|
@ -812,11 +808,11 @@ class TestTruncatedPngPLeaks(PillowLeakTestCase):
|
|||
mem_limit = 2 * 1024 # max increase in K
|
||||
iterations = 100 # Leak is 56k/iteration, this will leak 5.6megs
|
||||
|
||||
def test_leak_load(self) -> None:
|
||||
def test_leak_load(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
with open("Tests/images/hopper.png", "rb") as f:
|
||||
DATA = BytesIO(f.read(16 * 1024))
|
||||
|
||||
ImageFile.LOAD_TRUNCATED_IMAGES = True
|
||||
monkeypatch.setattr(ImageFile, "LOAD_TRUNCATED_IMAGES", True)
|
||||
with Image.open(DATA) as im:
|
||||
im.load()
|
||||
|
||||
|
@ -824,7 +820,4 @@ class TestTruncatedPngPLeaks(PillowLeakTestCase):
|
|||
with Image.open(DATA) as im:
|
||||
im.load()
|
||||
|
||||
try:
|
||||
self._test_leak(core)
|
||||
finally:
|
||||
ImageFile.LOAD_TRUNCATED_IMAGES = False
|
||||
self._test_leak(core)
|
||||
|
|
|
@ -49,7 +49,7 @@ def test_sanity() -> None:
|
|||
(b"P5 3 1 257 \x00\x00\x00\x80\x01\x01", "I", (0, 32640, 65535)),
|
||||
# P6 with maxval < 255
|
||||
(
|
||||
b"P6 3 1 17 \x00\x01\x02\x08\x09\x0A\x0F\x10\x11",
|
||||
b"P6 3 1 17 \x00\x01\x02\x08\x09\x0a\x0f\x10\x11",
|
||||
"RGB",
|
||||
(
|
||||
(0, 15, 30),
|
||||
|
@ -60,7 +60,7 @@ def test_sanity() -> None:
|
|||
# P6 with maxval > 255
|
||||
(
|
||||
b"P6 3 1 257 \x00\x00\x00\x01\x00\x02"
|
||||
b"\x00\x80\x00\x81\x00\x82\x01\x00\x01\x01\xFF\xFF",
|
||||
b"\x00\x80\x00\x81\x00\x82\x01\x00\x01\x01\xff\xff",
|
||||
"RGB",
|
||||
(
|
||||
(0, 1, 2),
|
||||
|
@ -79,6 +79,7 @@ def test_arbitrary_maxval(
|
|||
assert im.mode == mode
|
||||
|
||||
px = im.load()
|
||||
assert px is not None
|
||||
assert tuple(px[x, 0] for x in range(3)) == pixels
|
||||
|
||||
|
||||
|
@ -93,7 +94,7 @@ def test_16bit_pgm() -> None:
|
|||
|
||||
def test_16bit_pgm_write(tmp_path: Path) -> None:
|
||||
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")
|
||||
assert_image_equal_tofile(im, filename)
|
||||
|
||||
|
@ -105,7 +106,7 @@ def test_pnm(tmp_path: Path) -> None:
|
|||
with Image.open("Tests/images/hopper.pnm") as im:
|
||||
assert_image_similar(im, hopper(), 0.0001)
|
||||
|
||||
filename = str(tmp_path / "temp.pnm")
|
||||
filename = tmp_path / "temp.pnm"
|
||||
im.save(filename)
|
||||
|
||||
assert_image_equal_tofile(im, filename)
|
||||
|
@ -116,7 +117,7 @@ def test_pfm(tmp_path: Path) -> None:
|
|||
assert im.info["scale"] == 1.0
|
||||
assert_image_equal(im, hopper("F"))
|
||||
|
||||
filename = str(tmp_path / "tmp.pfm")
|
||||
filename = tmp_path / "tmp.pfm"
|
||||
im.save(filename)
|
||||
|
||||
assert_image_equal_tofile(im, filename)
|
||||
|
@ -127,7 +128,7 @@ def test_pfm_big_endian(tmp_path: Path) -> None:
|
|||
assert im.info["scale"] == 2.5
|
||||
assert_image_equal(im, hopper("F"))
|
||||
|
||||
filename = str(tmp_path / "tmp.pfm")
|
||||
filename = tmp_path / "tmp.pfm"
|
||||
im.save(filename)
|
||||
|
||||
assert_image_equal_tofile(im, filename)
|
||||
|
@ -193,8 +194,8 @@ def test_16bit_plain_pgm() -> None:
|
|||
def test_plain_data_with_comment(
|
||||
tmp_path: Path, header: bytes, data: bytes, comment_count: int
|
||||
) -> None:
|
||||
path1 = str(tmp_path / "temp1.ppm")
|
||||
path2 = str(tmp_path / "temp2.ppm")
|
||||
path1 = tmp_path / "temp1.ppm"
|
||||
path2 = tmp_path / "temp2.ppm"
|
||||
comment = b"# comment" * comment_count
|
||||
with open(path1, "wb") as f1, open(path2, "wb") as f2:
|
||||
f1.write(header + b"\n\n" + data)
|
||||
|
@ -206,7 +207,7 @@ def test_plain_data_with_comment(
|
|||
|
||||
@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:
|
||||
path = str(tmp_path / "temp.ppm")
|
||||
path = tmp_path / "temp.ppm"
|
||||
with open(path, "wb") as f:
|
||||
f.write(data)
|
||||
|
||||
|
@ -217,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"))
|
||||
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:
|
||||
f.write(data)
|
||||
|
||||
|
@ -234,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:
|
||||
path = str(tmp_path / "temp.ppm")
|
||||
path = tmp_path / "temp.ppm"
|
||||
with open(path, "wb") as f:
|
||||
f.write(data)
|
||||
|
||||
|
@ -244,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:
|
||||
path = str(tmp_path / "temp.ppm")
|
||||
path = tmp_path / "temp.ppm"
|
||||
with open(path, "wb") as f:
|
||||
f.write(b"P3\n128 128\n255\n-1")
|
||||
|
||||
|
@ -254,7 +255,7 @@ def test_plain_ppm_value_negative(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:
|
||||
f.write(b"P3\n128 128\n255\n256")
|
||||
|
||||
|
@ -269,7 +270,7 @@ def test_magic() -> 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:
|
||||
f.write(b"P6 #comment\n#comment\r12#comment\r8\n128 #comment\n255\n")
|
||||
|
||||
|
@ -278,7 +279,7 @@ def test_header_with_comments(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:
|
||||
f.write(b"P6\nTEST")
|
||||
|
||||
|
@ -288,29 +289,25 @@ def test_non_integer_token(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:
|
||||
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):
|
||||
pass
|
||||
|
||||
assert str(e.value) == "Token too long in file header: 01234567890"
|
||||
|
||||
|
||||
def test_truncated_file(tmp_path: Path) -> None:
|
||||
# Test EOF in header
|
||||
path = str(tmp_path / "temp.pgm")
|
||||
path = tmp_path / "temp.pgm"
|
||||
with open(path, "wb") as f:
|
||||
f.write(b"P6")
|
||||
|
||||
with pytest.raises(ValueError) as e:
|
||||
with pytest.raises(ValueError, match="Reached EOF while reading header"):
|
||||
with Image.open(path):
|
||||
pass
|
||||
|
||||
assert str(e.value) == "Reached EOF while reading header"
|
||||
|
||||
# Test EOF for PyDecoder
|
||||
fp = BytesIO(b"P5 3 1 4")
|
||||
with Image.open(fp) as im:
|
||||
|
@ -319,7 +316,7 @@ def test_truncated_file(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:
|
||||
f.write(b"P2 1 2 255 255")
|
||||
|
||||
|
@ -330,16 +327,16 @@ def test_not_enough_image_data(tmp_path: Path) -> None:
|
|||
|
||||
@pytest.mark.parametrize("maxval", (b"0", b"65536"))
|
||||
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:
|
||||
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):
|
||||
pass
|
||||
|
||||
assert str(e.value) == "maxval must be greater than 0 and less than 65536"
|
||||
|
||||
|
||||
def test_neg_ppm() -> None:
|
||||
# Storage.c accepted negative values for xsize, ysize. the
|
||||
|
@ -353,7 +350,7 @@ def test_neg_ppm() -> 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:
|
||||
f.write(b"P4\n128 128\n255")
|
||||
|
@ -367,22 +364,18 @@ def test_mimetypes(tmp_path: Path) -> None:
|
|||
|
||||
|
||||
@pytest.mark.parametrize("buffer", (True, False))
|
||||
def test_save_stdout(buffer: bool) -> None:
|
||||
old_stdout = sys.stdout
|
||||
def test_save_stdout(buffer: bool, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
|
||||
class MyStdOut:
|
||||
buffer = BytesIO()
|
||||
|
||||
mystdout: MyStdOut | BytesIO = MyStdOut() if buffer else BytesIO()
|
||||
|
||||
sys.stdout = mystdout
|
||||
monkeypatch.setattr(sys, "stdout", mystdout)
|
||||
|
||||
with Image.open(TEST_FILE) as im:
|
||||
im.save(sys.stdout, "PPM")
|
||||
|
||||
# Reset stdout
|
||||
sys.stdout = old_stdout
|
||||
|
||||
if isinstance(mystdout, MyStdOut):
|
||||
mystdout = mystdout.buffer
|
||||
with Image.open(mystdout) as reloaded:
|
||||
|
|
|
@ -25,12 +25,12 @@ def test_sanity() -> None:
|
|||
|
||||
@pytest.mark.skipif(is_pypy(), reason="Requires CPython")
|
||||
def test_unclosed_file() -> None:
|
||||
def open() -> None:
|
||||
def open_test_image() -> None:
|
||||
im = Image.open(test_file)
|
||||
im.load()
|
||||
|
||||
with pytest.warns(ResourceWarning):
|
||||
open()
|
||||
open_test_image()
|
||||
|
||||
|
||||
def test_closed_file() -> None:
|
||||
|
@ -59,17 +59,21 @@ def test_invalid_file() -> None:
|
|||
|
||||
def test_n_frames() -> None:
|
||||
with Image.open("Tests/images/hopper_merged.psd") as im:
|
||||
assert isinstance(im, PsdImagePlugin.PsdImageFile)
|
||||
assert im.n_frames == 1
|
||||
assert not im.is_animated
|
||||
|
||||
for path in [test_file, "Tests/images/negative_layer_count.psd"]:
|
||||
with Image.open(path) as im:
|
||||
assert isinstance(im, PsdImagePlugin.PsdImageFile)
|
||||
assert im.n_frames == 2
|
||||
assert im.is_animated
|
||||
|
||||
|
||||
def test_eoferror() -> None:
|
||||
with Image.open(test_file) as im:
|
||||
assert isinstance(im, PsdImagePlugin.PsdImageFile)
|
||||
|
||||
# PSD seek index starts at 1 rather than 0
|
||||
n_frames = im.n_frames + 1
|
||||
|
||||
|
@ -119,11 +123,13 @@ def test_rgba() -> None:
|
|||
|
||||
def test_negative_top_left_layer() -> None:
|
||||
with Image.open("Tests/images/negative_top_left_layer.psd") as im:
|
||||
assert isinstance(im, PsdImagePlugin.PsdImageFile)
|
||||
assert im.layers[0][2] == (-50, -50, 50, 50)
|
||||
|
||||
|
||||
def test_layer_skip() -> None:
|
||||
with Image.open("Tests/images/five_channels.psd") as im:
|
||||
assert isinstance(im, PsdImagePlugin.PsdImageFile)
|
||||
assert im.n_frames == 1
|
||||
|
||||
|
||||
|
@ -175,5 +181,6 @@ def test_crashes(test_file: str, raises: type[Exception]) -> None:
|
|||
def test_layer_crashes(test_file: str) -> None:
|
||||
with open(test_file, "rb") as f:
|
||||
with Image.open(f) as im:
|
||||
assert isinstance(im, PsdImagePlugin.PsdImageFile)
|
||||
with pytest.raises(SyntaxError):
|
||||
im.layers
|
||||
|
|
|
@ -71,31 +71,33 @@ def test_invalid_file() -> None:
|
|||
SgiImagePlugin.SgiImageFile(invalid_file)
|
||||
|
||||
|
||||
def test_write(tmp_path: Path) -> None:
|
||||
def roundtrip(img: Image.Image) -> None:
|
||||
out = str(tmp_path / "temp.sgi")
|
||||
img.save(out, format="sgi")
|
||||
def roundtrip(img: Image.Image, tmp_path: Path) -> None:
|
||||
out = tmp_path / "temp.sgi"
|
||||
img.save(out, format="sgi")
|
||||
assert_image_equal_tofile(img, out)
|
||||
|
||||
out = tmp_path / "fp.sgi"
|
||||
with open(out, "wb") as fp:
|
||||
img.save(fp)
|
||||
assert_image_equal_tofile(img, out)
|
||||
|
||||
out = str(tmp_path / "fp.sgi")
|
||||
with open(out, "wb") as fp:
|
||||
img.save(fp)
|
||||
assert_image_equal_tofile(img, out)
|
||||
assert not fp.closed
|
||||
|
||||
assert not fp.closed
|
||||
|
||||
for mode in ("L", "RGB", "RGBA"):
|
||||
roundtrip(hopper(mode))
|
||||
@pytest.mark.parametrize("mode", ("L", "RGB", "RGBA"))
|
||||
def test_write(mode: str, tmp_path: Path) -> None:
|
||||
roundtrip(hopper(mode), tmp_path)
|
||||
|
||||
# Test 1 dimension for an L mode image
|
||||
roundtrip(Image.new("L", (10, 1)))
|
||||
|
||||
def test_write_L_mode_1_dimension(tmp_path: Path) -> None:
|
||||
roundtrip(Image.new("L", (10, 1)), tmp_path)
|
||||
|
||||
|
||||
def test_write16(tmp_path: Path) -> None:
|
||||
test_file = "Tests/images/hopper16.rgb"
|
||||
|
||||
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)
|
||||
|
||||
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:
|
||||
im = hopper("LA")
|
||||
out = str(tmp_path / "temp.sgi")
|
||||
out = tmp_path / "temp.sgi"
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
im.save(out, format="sgi")
|
||||
|
|
|
@ -7,7 +7,7 @@ from pathlib import Path
|
|||
|
||||
import pytest
|
||||
|
||||
from PIL import Image, ImageSequence, SpiderImagePlugin
|
||||
from PIL import Image, SpiderImagePlugin
|
||||
|
||||
from .helper import assert_image_equal, hopper, is_pypy
|
||||
|
||||
|
@ -24,12 +24,12 @@ def test_sanity() -> None:
|
|||
|
||||
@pytest.mark.skipif(is_pypy(), reason="Requires CPython")
|
||||
def test_unclosed_file() -> None:
|
||||
def open() -> None:
|
||||
def open_test_image() -> None:
|
||||
im = Image.open(TEST_FILE)
|
||||
im.load()
|
||||
|
||||
with pytest.warns(ResourceWarning):
|
||||
open()
|
||||
open_test_image()
|
||||
|
||||
|
||||
def test_closed_file() -> None:
|
||||
|
@ -51,7 +51,7 @@ def test_context_manager() -> None:
|
|||
|
||||
def test_save(tmp_path: Path) -> None:
|
||||
# Arrange
|
||||
temp = str(tmp_path / "temp.spider")
|
||||
temp = tmp_path / "temp.spider"
|
||||
im = hopper()
|
||||
|
||||
# Act
|
||||
|
@ -96,6 +96,7 @@ def test_tell() -> None:
|
|||
|
||||
def test_n_frames() -> None:
|
||||
with Image.open(TEST_FILE) as im:
|
||||
assert isinstance(im, SpiderImagePlugin.SpiderImageFile)
|
||||
assert im.n_frames == 1
|
||||
assert not im.is_animated
|
||||
|
||||
|
@ -153,8 +154,8 @@ def test_nonstack_file() -> None:
|
|||
|
||||
def test_nonstack_dos() -> None:
|
||||
with Image.open(TEST_FILE) as im:
|
||||
for i, frame in enumerate(ImageSequence.Iterator(im)):
|
||||
assert i <= 1, "Non-stack DOS file test failed"
|
||||
with pytest.raises(EOFError):
|
||||
im.seek(0)
|
||||
|
||||
|
||||
# for issue #4093
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import io
|
||||
import os
|
||||
|
||||
import pytest
|
||||
|
||||
from PIL import Image, SunImagePlugin
|
||||
from PIL import Image, SunImagePlugin, _binary
|
||||
|
||||
from .helper import assert_image_equal_tofile, assert_image_similar, hopper
|
||||
|
||||
|
@ -33,6 +34,60 @@ def test_im1() -> None:
|
|||
assert_image_equal_tofile(im, "Tests/images/sunraster.im1.png")
|
||||
|
||||
|
||||
def _sun_header(
|
||||
depth: int = 0, file_type: int = 0, palette_length: int = 0
|
||||
) -> io.BytesIO:
|
||||
return io.BytesIO(
|
||||
_binary.o32be(0x59A66A95)
|
||||
+ b"\x00" * 8
|
||||
+ _binary.o32be(depth)
|
||||
+ b"\x00" * 4
|
||||
+ _binary.o32be(file_type)
|
||||
+ b"\x00" * 4
|
||||
+ _binary.o32be(palette_length)
|
||||
)
|
||||
|
||||
|
||||
def test_unsupported_mode_bit_depth() -> None:
|
||||
with pytest.raises(SyntaxError, match="Unsupported Mode/Bit Depth"):
|
||||
with SunImagePlugin.SunImageFile(_sun_header()):
|
||||
pass
|
||||
|
||||
|
||||
def test_unsupported_color_palette_length() -> None:
|
||||
with pytest.raises(SyntaxError, match="Unsupported Color Palette Length"):
|
||||
with SunImagePlugin.SunImageFile(_sun_header(depth=1, palette_length=1025)):
|
||||
pass
|
||||
|
||||
|
||||
def test_unsupported_palette_type() -> None:
|
||||
with pytest.raises(SyntaxError, match="Unsupported Palette Type"):
|
||||
with SunImagePlugin.SunImageFile(_sun_header(depth=1, palette_length=1)):
|
||||
pass
|
||||
|
||||
|
||||
def test_unsupported_file_type() -> None:
|
||||
with pytest.raises(SyntaxError, match="Unsupported Sun Raster file type"):
|
||||
with SunImagePlugin.SunImageFile(_sun_header(depth=1, file_type=6)):
|
||||
pass
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
not os.path.exists(EXTRA_DIR), reason="Extra image files not installed"
|
||||
)
|
||||
def test_rgbx() -> None:
|
||||
with open(os.path.join(EXTRA_DIR, "32bpp.ras"), "rb") as fp:
|
||||
data = fp.read()
|
||||
|
||||
# Set file type to 3
|
||||
data = data[:20] + _binary.o32be(3) + data[24:]
|
||||
|
||||
with Image.open(io.BytesIO(data)) as im:
|
||||
r, g, b = im.split()
|
||||
im = Image.merge("RGB", (b, g, r))
|
||||
assert_image_equal_tofile(im, os.path.join(EXTRA_DIR, "32bpp.png"))
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
not os.path.exists(EXTRA_DIR), reason="Extra image files not installed"
|
||||
)
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import warnings
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
|
@ -29,6 +30,22 @@ def test_sanity(codec: str, test_path: str, format: str) -> None:
|
|||
assert im.format == format
|
||||
|
||||
|
||||
def test_unexpected_end(tmp_path: Path) -> None:
|
||||
tmpfile = str(tmp_path / "temp.tar")
|
||||
with open(tmpfile, "w"):
|
||||
pass
|
||||
|
||||
with pytest.raises(OSError, match="unexpected end of tar file"):
|
||||
with TarIO.TarIO(tmpfile, "test"):
|
||||
pass
|
||||
|
||||
|
||||
def test_cannot_find_subfile() -> None:
|
||||
with pytest.raises(OSError, match="cannot find subfile"):
|
||||
with TarIO.TarIO(TEST_TAR_FILE, "test"):
|
||||
pass
|
||||
|
||||
|
||||
@pytest.mark.skipif(is_pypy(), reason="Requires CPython")
|
||||
def test_unclosed_file() -> None:
|
||||
with pytest.warns(ResourceWarning):
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from glob import glob
|
||||
from itertools import product
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
@ -15,16 +13,29 @@ _TGA_DIR = os.path.join("Tests", "images", "tga")
|
|||
_TGA_DIR_COMMON = os.path.join(_TGA_DIR, "common")
|
||||
|
||||
|
||||
_MODES = ("L", "LA", "P", "RGB", "RGBA")
|
||||
_ORIGINS = ("tl", "bl")
|
||||
|
||||
_ORIGIN_TO_ORIENTATION = {"tl": 1, "bl": -1}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("mode", _MODES)
|
||||
def test_sanity(mode: str, tmp_path: Path) -> None:
|
||||
@pytest.mark.parametrize(
|
||||
"size_mode",
|
||||
(
|
||||
("1x1", "L"),
|
||||
("200x32", "L"),
|
||||
("200x32", "LA"),
|
||||
("200x32", "P"),
|
||||
("200x32", "RGB"),
|
||||
("200x32", "RGBA"),
|
||||
),
|
||||
)
|
||||
@pytest.mark.parametrize("origin", _ORIGINS)
|
||||
@pytest.mark.parametrize("rle", (True, False))
|
||||
def test_sanity(
|
||||
size_mode: tuple[str, str], origin: str, rle: str, tmp_path: Path
|
||||
) -> None:
|
||||
def roundtrip(original_im: Image.Image) -> None:
|
||||
out = str(tmp_path / "temp.tga")
|
||||
out = tmp_path / "temp.tga"
|
||||
|
||||
original_im.save(out, rle=rle)
|
||||
with Image.open(out) as saved_im:
|
||||
|
@ -36,46 +47,40 @@ def test_sanity(mode: str, tmp_path: Path) -> None:
|
|||
|
||||
assert_image_equal(saved_im, original_im)
|
||||
|
||||
png_paths = glob(os.path.join(_TGA_DIR_COMMON, f"*x*_{mode.lower()}.png"))
|
||||
size, mode = size_mode
|
||||
png_path = os.path.join(_TGA_DIR_COMMON, size + "_" + mode.lower() + ".png")
|
||||
with Image.open(png_path) as reference_im:
|
||||
assert reference_im.mode == mode
|
||||
|
||||
for png_path in png_paths:
|
||||
with Image.open(png_path) as reference_im:
|
||||
assert reference_im.mode == mode
|
||||
path_no_ext = os.path.splitext(png_path)[0]
|
||||
tga_path = "{}_{}_{}.tga".format(path_no_ext, origin, "rle" if rle else "raw")
|
||||
|
||||
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"
|
||||
)
|
||||
with Image.open(tga_path) as original_im:
|
||||
assert original_im.format == "TGA"
|
||||
assert original_im.get_format_mimetype() == "image/x-tga"
|
||||
if rle:
|
||||
assert original_im.info["compression"] == "tga_rle"
|
||||
assert original_im.info["orientation"] == _ORIGIN_TO_ORIENTATION[origin]
|
||||
if mode == "P":
|
||||
assert original_im.getpalette() == reference_im.getpalette()
|
||||
|
||||
with Image.open(tga_path) as original_im:
|
||||
assert original_im.format == "TGA"
|
||||
assert original_im.get_format_mimetype() == "image/x-tga"
|
||||
if rle:
|
||||
assert original_im.info["compression"] == "tga_rle"
|
||||
assert (
|
||||
original_im.info["orientation"]
|
||||
== _ORIGIN_TO_ORIENTATION[origin]
|
||||
)
|
||||
if mode == "P":
|
||||
assert original_im.getpalette() == reference_im.getpalette()
|
||||
assert_image_equal(original_im, reference_im)
|
||||
|
||||
assert_image_equal(original_im, reference_im)
|
||||
|
||||
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):
|
||||
Image.open("Tests/images/p_8.tga")
|
||||
|
||||
|
||||
def test_palette_depth_16(tmp_path: Path) -> None:
|
||||
with Image.open("Tests/images/p_16.tga") as im:
|
||||
assert im.palette is not None
|
||||
assert im.palette.mode == "RGBA"
|
||||
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)
|
||||
with Image.open(out) as reloaded:
|
||||
assert_image_equal_tofile(reloaded.convert("RGBA"), "Tests/images/p_16.png")
|
||||
|
@ -121,7 +126,7 @@ def test_cross_scan_line() -> None:
|
|||
def test_save(tmp_path: Path) -> None:
|
||||
test_file = "Tests/images/tga_id_field.tga"
|
||||
with Image.open(test_file) as im:
|
||||
out = str(tmp_path / "temp.tga")
|
||||
out = tmp_path / "temp.tga"
|
||||
|
||||
# Save
|
||||
im.save(out)
|
||||
|
@ -140,7 +145,7 @@ def test_small_palette(tmp_path: Path) -> None:
|
|||
colors = [0, 0, 0]
|
||||
im.putpalette(colors)
|
||||
|
||||
out = str(tmp_path / "temp.tga")
|
||||
out = tmp_path / "temp.tga"
|
||||
im.save(out)
|
||||
|
||||
with Image.open(out) as reloaded:
|
||||
|
@ -154,7 +159,7 @@ def test_missing_palette() -> None:
|
|||
|
||||
def test_save_wrong_mode(tmp_path: Path) -> None:
|
||||
im = hopper("PA")
|
||||
out = str(tmp_path / "temp.tga")
|
||||
out = tmp_path / "temp.tga"
|
||||
|
||||
with pytest.raises(OSError):
|
||||
im.save(out)
|
||||
|
@ -171,7 +176,7 @@ def test_save_mapdepth() -> None:
|
|||
def test_save_id_section(tmp_path: Path) -> None:
|
||||
test_file = "Tests/images/rgb32rle.tga"
|
||||
with Image.open(test_file) as im:
|
||||
out = str(tmp_path / "temp.tga")
|
||||
out = tmp_path / "temp.tga"
|
||||
|
||||
# Check there is no id section
|
||||
im.save(out)
|
||||
|
@ -201,7 +206,7 @@ def test_save_id_section(tmp_path: Path) -> None:
|
|||
|
||||
def test_save_orientation(tmp_path: Path) -> None:
|
||||
test_file = "Tests/images/rgb32rle.tga"
|
||||
out = str(tmp_path / "temp.tga")
|
||||
out = tmp_path / "temp.tga"
|
||||
with Image.open(test_file) as im:
|
||||
assert im.info["orientation"] == -1
|
||||
|
||||
|
@ -213,10 +218,14 @@ def test_save_orientation(tmp_path: Path) -> None:
|
|||
def test_horizontal_orientations() -> None:
|
||||
# These images have been manually hexedited to have the relevant orientations
|
||||
with Image.open("Tests/images/rgb32rle_top_right.tga") as im:
|
||||
assert im.load()[90, 90][:3] == (0, 0, 0)
|
||||
px = im.load()
|
||||
assert px is not None
|
||||
assert px[90, 90][:3] == (0, 0, 0)
|
||||
|
||||
with Image.open("Tests/images/rgb32rle_bottom_right.tga") as im:
|
||||
assert im.load()[90, 90][:3] == (0, 255, 0)
|
||||
px = im.load()
|
||||
assert px is not None
|
||||
assert px[90, 90][:3] == (0, 255, 0)
|
||||
|
||||
|
||||
def test_save_rle(tmp_path: Path) -> None:
|
||||
|
@ -224,7 +233,7 @@ def test_save_rle(tmp_path: Path) -> None:
|
|||
with Image.open(test_file) as im:
|
||||
assert im.info["compression"] == "tga_rle"
|
||||
|
||||
out = str(tmp_path / "temp.tga")
|
||||
out = tmp_path / "temp.tga"
|
||||
|
||||
# Save
|
||||
im.save(out)
|
||||
|
@ -261,7 +270,7 @@ def test_save_l_transparency(tmp_path: Path) -> None:
|
|||
assert im.mode == "LA"
|
||||
assert im.getchannel("A").getcolors()[0][0] == num_transparent
|
||||
|
||||
out = str(tmp_path / "temp.tga")
|
||||
out = tmp_path / "temp.tga"
|
||||
im.save(out)
|
||||
|
||||
with Image.open(out) as test_im:
|
||||
|
|
|
@ -9,7 +9,13 @@ from types import ModuleType
|
|||
|
||||
import pytest
|
||||
|
||||
from PIL import Image, ImageFile, TiffImagePlugin, UnidentifiedImageError
|
||||
from PIL import (
|
||||
Image,
|
||||
ImageFile,
|
||||
JpegImagePlugin,
|
||||
TiffImagePlugin,
|
||||
UnidentifiedImageError,
|
||||
)
|
||||
from PIL.TiffImagePlugin import RESOLUTION_UNIT, X_RESOLUTION, Y_RESOLUTION
|
||||
|
||||
from .helper import (
|
||||
|
@ -31,7 +37,7 @@ except ImportError:
|
|||
|
||||
class TestFileTiff:
|
||||
def test_sanity(self, tmp_path: Path) -> None:
|
||||
filename = str(tmp_path / "temp.tif")
|
||||
filename = tmp_path / "temp.tif"
|
||||
|
||||
hopper("RGB").save(filename)
|
||||
|
||||
|
@ -63,12 +69,12 @@ class TestFileTiff:
|
|||
|
||||
@pytest.mark.skipif(is_pypy(), reason="Requires CPython")
|
||||
def test_unclosed_file(self) -> None:
|
||||
def open() -> None:
|
||||
def open_test_image() -> None:
|
||||
im = Image.open("Tests/images/multipage.tiff")
|
||||
im.load()
|
||||
|
||||
with pytest.warns(ResourceWarning):
|
||||
open()
|
||||
open_test_image()
|
||||
|
||||
def test_closed_file(self) -> None:
|
||||
with warnings.catch_warnings():
|
||||
|
@ -112,22 +118,39 @@ class TestFileTiff:
|
|||
assert_image_equal_tofile(im, "Tests/images/hopper.tif")
|
||||
|
||||
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)
|
||||
|
||||
def test_bigtiff_save(self, tmp_path: Path) -> None:
|
||||
outfile = tmp_path / "temp.tif"
|
||||
im = hopper()
|
||||
im.save(outfile, big_tiff=True)
|
||||
|
||||
with Image.open(outfile) as reloaded:
|
||||
assert isinstance(reloaded, TiffImagePlugin.TiffImageFile)
|
||||
assert reloaded.tag_v2._bigtiff is True
|
||||
|
||||
im.save(outfile, save_all=True, append_images=[im], big_tiff=True)
|
||||
|
||||
with Image.open(outfile) as reloaded:
|
||||
assert isinstance(reloaded, TiffImagePlugin.TiffImageFile)
|
||||
assert reloaded.tag_v2._bigtiff is True
|
||||
|
||||
def test_seek_too_large(self) -> None:
|
||||
with pytest.raises(ValueError, match="Unable to seek to frame"):
|
||||
Image.open("Tests/images/seek_too_large.tif")
|
||||
|
||||
def test_set_legacy_api(self) -> None:
|
||||
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
|
||||
assert str(e.value) == "Not allowing setting of legacy api"
|
||||
|
||||
def test_xyres_tiff(self) -> None:
|
||||
filename = "Tests/images/pil168.tif"
|
||||
with Image.open(filename) as im:
|
||||
assert isinstance(im, TiffImagePlugin.TiffImageFile)
|
||||
|
||||
# legacy api
|
||||
assert isinstance(im.tag[X_RESOLUTION][0], tuple)
|
||||
assert isinstance(im.tag[Y_RESOLUTION][0], tuple)
|
||||
|
@ -141,6 +164,8 @@ class TestFileTiff:
|
|||
def test_xyres_fallback_tiff(self) -> None:
|
||||
filename = "Tests/images/compression.tif"
|
||||
with Image.open(filename) as im:
|
||||
assert isinstance(im, TiffImagePlugin.TiffImageFile)
|
||||
|
||||
# v2 api
|
||||
assert isinstance(im.tag_v2[X_RESOLUTION], TiffImagePlugin.IFDRational)
|
||||
assert isinstance(im.tag_v2[Y_RESOLUTION], TiffImagePlugin.IFDRational)
|
||||
|
@ -155,6 +180,8 @@ class TestFileTiff:
|
|||
def test_int_resolution(self) -> None:
|
||||
filename = "Tests/images/pil168.tif"
|
||||
with Image.open(filename) as im:
|
||||
assert isinstance(im, TiffImagePlugin.TiffImageFile)
|
||||
|
||||
# Try to read a file where X,Y_RESOLUTION are ints
|
||||
im.tag_v2[X_RESOLUTION] = 71
|
||||
im.tag_v2[Y_RESOLUTION] = 71
|
||||
|
@ -169,11 +196,12 @@ class TestFileTiff:
|
|||
with Image.open(
|
||||
"Tests/images/hopper_float_dpi_" + str(resolution_unit) + ".tif"
|
||||
) as im:
|
||||
assert isinstance(im, TiffImagePlugin.TiffImageFile)
|
||||
assert im.tag_v2.get(RESOLUTION_UNIT) == resolution_unit
|
||||
assert im.info["dpi"] == (dpi, dpi)
|
||||
|
||||
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:
|
||||
dpi = (72.2, 72.2)
|
||||
im.save(outfile, dpi=dpi)
|
||||
|
@ -186,6 +214,7 @@ class TestFileTiff:
|
|||
with Image.open("Tests/images/10ct_32bit_128.tiff") as im:
|
||||
im.save(b, format="tiff", resolution=123.45)
|
||||
with Image.open(b) as im:
|
||||
assert isinstance(im, TiffImagePlugin.TiffImageFile)
|
||||
assert im.tag_v2[X_RESOLUTION] == 123.45
|
||||
assert im.tag_v2[Y_RESOLUTION] == 123.45
|
||||
|
||||
|
@ -201,19 +230,21 @@ class TestFileTiff:
|
|||
TiffImagePlugin.PREFIXES.pop()
|
||||
|
||||
def test_bad_exif(self) -> None:
|
||||
with Image.open("Tests/images/hopper_bad_exif.jpg") as i:
|
||||
with Image.open("Tests/images/hopper_bad_exif.jpg") as im:
|
||||
assert isinstance(im, JpegImagePlugin.JpegImageFile)
|
||||
|
||||
# Should not raise struct.error.
|
||||
with pytest.warns(UserWarning):
|
||||
i._getexif()
|
||||
im._getexif()
|
||||
|
||||
def test_save_rgba(self, tmp_path: Path) -> None:
|
||||
im = hopper("RGBA")
|
||||
outfile = str(tmp_path / "temp.tif")
|
||||
outfile = tmp_path / "temp.tif"
|
||||
im.save(outfile)
|
||||
|
||||
def test_save_unsupported_mode(self, tmp_path: Path) -> None:
|
||||
im = hopper("HSV")
|
||||
outfile = str(tmp_path / "temp.tif")
|
||||
outfile = tmp_path / "temp.tif"
|
||||
with pytest.raises(OSError):
|
||||
im.save(outfile)
|
||||
|
||||
|
@ -295,11 +326,13 @@ class TestFileTiff:
|
|||
)
|
||||
def test_n_frames(self, path: str, n_frames: int) -> None:
|
||||
with Image.open(path) as im:
|
||||
assert isinstance(im, TiffImagePlugin.TiffImageFile)
|
||||
assert im.n_frames == n_frames
|
||||
assert im.is_animated == (n_frames != 1)
|
||||
|
||||
def test_eoferror(self) -> None:
|
||||
with Image.open("Tests/images/multipage-lastframe.tif") as im:
|
||||
assert isinstance(im, TiffImagePlugin.TiffImageFile)
|
||||
n_frames = im.n_frames
|
||||
|
||||
# Test seeking past the last frame
|
||||
|
@ -343,19 +376,24 @@ class TestFileTiff:
|
|||
def test_frame_order(self) -> None:
|
||||
# A frame can't progress to itself after reading
|
||||
with Image.open("Tests/images/multipage_single_frame_loop.tiff") as im:
|
||||
assert isinstance(im, TiffImagePlugin.TiffImageFile)
|
||||
assert im.n_frames == 1
|
||||
|
||||
# A frame can't progress to a frame that has already been read
|
||||
with Image.open("Tests/images/multipage_multiple_frame_loop.tiff") as im:
|
||||
assert isinstance(im, TiffImagePlugin.TiffImageFile)
|
||||
assert im.n_frames == 2
|
||||
|
||||
# Frames don't have to be in sequence
|
||||
with Image.open("Tests/images/multipage_out_of_order.tiff") as im:
|
||||
assert isinstance(im, TiffImagePlugin.TiffImageFile)
|
||||
assert im.n_frames == 3
|
||||
|
||||
def test___str__(self) -> None:
|
||||
filename = "Tests/images/pil136.tiff"
|
||||
with Image.open(filename) as im:
|
||||
assert isinstance(im, TiffImagePlugin.TiffImageFile)
|
||||
|
||||
# Act
|
||||
ret = str(im.ifd)
|
||||
|
||||
|
@ -366,6 +404,8 @@ class TestFileTiff:
|
|||
# Arrange
|
||||
filename = "Tests/images/pil136.tiff"
|
||||
with Image.open(filename) as im:
|
||||
assert isinstance(im, TiffImagePlugin.TiffImageFile)
|
||||
|
||||
# v2 interface
|
||||
v2_tags = {
|
||||
256: 55,
|
||||
|
@ -405,6 +445,7 @@ class TestFileTiff:
|
|||
def test__delitem__(self) -> None:
|
||||
filename = "Tests/images/pil136.tiff"
|
||||
with Image.open(filename) as im:
|
||||
assert isinstance(im, TiffImagePlugin.TiffImageFile)
|
||||
len_before = len(dict(im.ifd))
|
||||
del im.ifd[256]
|
||||
len_after = len(dict(im.ifd))
|
||||
|
@ -437,6 +478,7 @@ class TestFileTiff:
|
|||
|
||||
def test_ifd_tag_type(self) -> None:
|
||||
with Image.open("Tests/images/ifd_tag_type.tiff") as im:
|
||||
assert isinstance(im, TiffImagePlugin.TiffImageFile)
|
||||
assert 0x8825 in im.tag_v2
|
||||
|
||||
def test_exif(self, tmp_path: Path) -> None:
|
||||
|
@ -473,14 +515,14 @@ class TestFileTiff:
|
|||
assert gps[0] == b"\x03\x02\x00\x00"
|
||||
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:
|
||||
exif = im.getexif()
|
||||
check_exif(exif)
|
||||
|
||||
im.save(outfile, exif=exif)
|
||||
|
||||
outfile2 = str(tmp_path / "temp2.tif")
|
||||
outfile2 = tmp_path / "temp2.tif"
|
||||
with Image.open(outfile) as im:
|
||||
exif = im.getexif()
|
||||
check_exif(exif)
|
||||
|
@ -492,7 +534,7 @@ class TestFileTiff:
|
|||
check_exif(exif)
|
||||
|
||||
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:
|
||||
exif = im.getexif()
|
||||
exif[264] = 100
|
||||
|
@ -521,10 +563,11 @@ class TestFileTiff:
|
|||
|
||||
@pytest.mark.parametrize("mode", ("1", "L"))
|
||||
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.save(filename, tiffinfo={262: 0})
|
||||
with Image.open(filename) as reloaded:
|
||||
assert isinstance(reloaded, TiffImagePlugin.TiffImageFile)
|
||||
assert reloaded.tag_v2[262] == 0
|
||||
assert_image_equal(im, reloaded)
|
||||
|
||||
|
@ -600,9 +643,11 @@ class TestFileTiff:
|
|||
|
||||
def test_with_underscores(self, tmp_path: Path) -> None:
|
||||
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)
|
||||
with Image.open(filename) as im:
|
||||
assert isinstance(im, TiffImagePlugin.TiffImageFile)
|
||||
|
||||
# legacy interface
|
||||
assert im.tag[X_RESOLUTION][0][0] == 72
|
||||
assert im.tag[Y_RESOLUTION][0][0] == 36
|
||||
|
@ -618,14 +663,14 @@ class TestFileTiff:
|
|||
with Image.open(infile) as im:
|
||||
assert im.getpixel((0, 0)) == pixel_value
|
||||
|
||||
tmpfile = str(tmp_path / "temp.tif")
|
||||
tmpfile = tmp_path / "temp.tif"
|
||||
im.save(tmpfile)
|
||||
|
||||
assert_image_equal_tofile(im, tmpfile)
|
||||
|
||||
def test_iptc(self, tmp_path: Path) -> None:
|
||||
# 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:
|
||||
im.load()
|
||||
assert isinstance(im, TiffImagePlugin.TiffImageFile)
|
||||
|
@ -640,7 +685,7 @@ class TestFileTiff:
|
|||
assert 33723 not in im.tag_v2
|
||||
|
||||
def test_rowsperstrip(self, tmp_path: Path) -> None:
|
||||
outfile = str(tmp_path / "temp.tif")
|
||||
outfile = tmp_path / "temp.tif"
|
||||
im = hopper()
|
||||
im.save(outfile, tiffinfo={278: 256})
|
||||
|
||||
|
@ -648,6 +693,18 @@ class TestFileTiff:
|
|||
assert isinstance(im, TiffImagePlugin.TiffImageFile)
|
||||
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:
|
||||
infile = "Tests/images/tiff_strip_raw.tif"
|
||||
with Image.open(infile) as im:
|
||||
|
@ -677,9 +734,10 @@ class TestFileTiff:
|
|||
def test_planar_configuration_save(self, tmp_path: Path) -> None:
|
||||
infile = "Tests/images/tiff_tiled_planar_raw.tif"
|
||||
with Image.open(infile) as im:
|
||||
assert isinstance(im, TiffImagePlugin.TiffImageFile)
|
||||
assert im._planar_configuration == 2
|
||||
|
||||
outfile = str(tmp_path / "temp.tif")
|
||||
outfile = tmp_path / "temp.tif"
|
||||
im.save(outfile)
|
||||
|
||||
with Image.open(outfile) as reloaded:
|
||||
|
@ -694,7 +752,7 @@ class TestFileTiff:
|
|||
|
||||
@pytest.mark.parametrize("mode", ("P", "PA"))
|
||||
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.save(outfile)
|
||||
|
@ -709,6 +767,7 @@ class TestFileTiff:
|
|||
|
||||
mp.seek(0, os.SEEK_SET)
|
||||
with Image.open(mp) as im:
|
||||
assert isinstance(im, TiffImagePlugin.TiffImageFile)
|
||||
assert im.n_frames == 3
|
||||
|
||||
# Test appending images
|
||||
|
@ -719,6 +778,7 @@ class TestFileTiff:
|
|||
|
||||
mp.seek(0, os.SEEK_SET)
|
||||
with Image.open(mp) as reread:
|
||||
assert isinstance(reread, TiffImagePlugin.TiffImageFile)
|
||||
assert reread.n_frames == 3
|
||||
|
||||
# Test appending using a generator
|
||||
|
@ -730,6 +790,7 @@ class TestFileTiff:
|
|||
|
||||
mp.seek(0, os.SEEK_SET)
|
||||
with Image.open(mp) as reread:
|
||||
assert isinstance(reread, TiffImagePlugin.TiffImageFile)
|
||||
assert reread.n_frames == 3
|
||||
|
||||
def test_fixoffsets(self) -> None:
|
||||
|
@ -746,6 +807,39 @@ class TestFileTiff:
|
|||
with pytest.raises(RuntimeError):
|
||||
a.fixOffsets(1)
|
||||
|
||||
b = BytesIO(b"II\x2a\x00\x00\x00\x00\x00")
|
||||
with TiffImagePlugin.AppendingTiffWriter(b) as a:
|
||||
a.offsetOfNewPage = 2**16
|
||||
|
||||
b.seek(0)
|
||||
a.fixOffsets(1, isShort=True)
|
||||
|
||||
b = BytesIO(b"II\x2b\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00")
|
||||
with TiffImagePlugin.AppendingTiffWriter(b) as a:
|
||||
a.offsetOfNewPage = 2**32
|
||||
|
||||
b.seek(0)
|
||||
a.fixOffsets(1, isShort=True)
|
||||
|
||||
b.seek(0)
|
||||
a.fixOffsets(1, isLong=True)
|
||||
|
||||
def test_appending_tiff_writer_writelong(self) -> None:
|
||||
data = b"II\x2a\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
|
||||
b = BytesIO(data)
|
||||
with TiffImagePlugin.AppendingTiffWriter(b) as a:
|
||||
a.seek(-4, os.SEEK_CUR)
|
||||
a.writeLong(2**32 - 1)
|
||||
assert b.getvalue() == data[:-4] + b"\xff\xff\xff\xff"
|
||||
|
||||
def test_appending_tiff_writer_rewritelastshorttolong(self) -> None:
|
||||
data = b"II\x2a\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
|
||||
b = BytesIO(data)
|
||||
with TiffImagePlugin.AppendingTiffWriter(b) as a:
|
||||
a.seek(-2, os.SEEK_CUR)
|
||||
a.rewriteLastShortToLong(2**32 - 1)
|
||||
assert b.getvalue() == data[:-4] + b"\xff\xff\xff\xff"
|
||||
|
||||
def test_saving_icc_profile(self, tmp_path: Path) -> None:
|
||||
# Tests saving TIFF with icc_profile set.
|
||||
# At the time of writing this will only work for non-compressed tiffs
|
||||
|
@ -755,7 +849,7 @@ class TestFileTiff:
|
|||
im.info["icc_profile"] = "Dummy value"
|
||||
|
||||
# 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")
|
||||
with Image.open(tmpfile) as reloaded:
|
||||
assert b"Dummy value" == reloaded.info["icc_profile"]
|
||||
|
@ -764,7 +858,7 @@ class TestFileTiff:
|
|||
im = hopper()
|
||||
assert "icc_profile" not in im.info
|
||||
|
||||
outfile = str(tmp_path / "temp.tif")
|
||||
outfile = tmp_path / "temp.tif"
|
||||
icc_profile = b"Dummy value"
|
||||
im.save(outfile, icc_profile=icc_profile)
|
||||
|
||||
|
@ -775,11 +869,11 @@ class TestFileTiff:
|
|||
with Image.open("Tests/images/hopper.bmp") as im:
|
||||
assert im.info["compression"] == 0
|
||||
|
||||
outfile = str(tmp_path / "temp.tif")
|
||||
outfile = tmp_path / "temp.tif"
|
||||
im.save(outfile)
|
||||
|
||||
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:
|
||||
assert "icc_profile" in im.info
|
||||
|
@ -807,6 +901,7 @@ class TestFileTiff:
|
|||
|
||||
def test_get_photoshop_blocks(self) -> None:
|
||||
with Image.open("Tests/images/lab.tif") as im:
|
||||
assert isinstance(im, TiffImagePlugin.TiffImageFile)
|
||||
assert list(im.get_photoshop_blocks().keys()) == [
|
||||
1061,
|
||||
1002,
|
||||
|
@ -832,7 +927,7 @@ class TestFileTiff:
|
|||
]
|
||||
|
||||
def test_tiff_chunks(self, tmp_path: Path) -> None:
|
||||
tmpfile = str(tmp_path / "temp.tif")
|
||||
tmpfile = tmp_path / "temp.tif"
|
||||
|
||||
im = hopper()
|
||||
with open(tmpfile, "wb") as fp:
|
||||
|
@ -854,7 +949,7 @@ class TestFileTiff:
|
|||
|
||||
def test_close_on_load_exclusive(self, tmp_path: Path) -> None:
|
||||
# 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:
|
||||
im.save(tmpfile)
|
||||
|
@ -866,7 +961,7 @@ class TestFileTiff:
|
|||
assert fp.closed
|
||||
|
||||
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:
|
||||
im.save(tmpfile)
|
||||
|
@ -895,11 +990,10 @@ class TestFileTiff:
|
|||
|
||||
@pytest.mark.timeout(6)
|
||||
@pytest.mark.filterwarnings("ignore:Truncated File Read")
|
||||
def test_timeout(self) -> None:
|
||||
def test_timeout(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
with Image.open("Tests/images/timeout-6646305047838720") as im:
|
||||
ImageFile.LOAD_TRUNCATED_IMAGES = True
|
||||
monkeypatch.setattr(ImageFile, "LOAD_TRUNCATED_IMAGES", True)
|
||||
im.load()
|
||||
ImageFile.LOAD_TRUNCATED_IMAGES = False
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"test_file",
|
||||
|
@ -918,7 +1012,7 @@ class TestFileTiff:
|
|||
@pytest.mark.skipif(not is_win32(), reason="Windows only")
|
||||
class TestFileTiffW32:
|
||||
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.
|
||||
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
|
||||
|
||||
f = str(tmp_path / "temp.tif")
|
||||
f = tmp_path / "temp.tif"
|
||||
|
||||
img.save(f, tiffinfo=info)
|
||||
|
||||
with Image.open(f) as loaded:
|
||||
assert isinstance(loaded, TiffImagePlugin.TiffImageFile)
|
||||
assert loaded.tag[ImageJMetaDataByteCounts] == (len(bin_data),)
|
||||
assert loaded.tag_v2[ImageJMetaDataByteCounts] == (len(bin_data),)
|
||||
|
||||
|
@ -80,12 +81,14 @@ def test_rt_metadata(tmp_path: Path) -> None:
|
|||
info[ImageJMetaDataByteCounts] = (8, len(bin_data) - 8)
|
||||
img.save(f, tiffinfo=info)
|
||||
with Image.open(f) as loaded:
|
||||
assert isinstance(loaded, TiffImagePlugin.TiffImageFile)
|
||||
assert loaded.tag[ImageJMetaDataByteCounts] == (8, len(bin_data) - 8)
|
||||
assert loaded.tag_v2[ImageJMetaDataByteCounts] == (8, len(bin_data) - 8)
|
||||
|
||||
|
||||
def test_read_metadata() -> None:
|
||||
with Image.open("Tests/images/hopper_g4.tif") as img:
|
||||
assert isinstance(img, TiffImagePlugin.TiffImageFile)
|
||||
assert {
|
||||
"YResolution": IFDRational(4294967295, 113653537),
|
||||
"PlanarConfiguration": 1,
|
||||
|
@ -128,13 +131,15 @@ def test_read_metadata() -> None:
|
|||
def test_write_metadata(tmp_path: Path) -> None:
|
||||
"""Test metadata writing through the python code"""
|
||||
with Image.open("Tests/images/hopper.tif") as img:
|
||||
f = str(tmp_path / "temp.tiff")
|
||||
assert isinstance(img, TiffImagePlugin.TiffImageFile)
|
||||
f = tmp_path / "temp.tiff"
|
||||
del img.tag[278]
|
||||
img.save(f, tiffinfo=img.tag)
|
||||
|
||||
original = img.tag_v2.named()
|
||||
|
||||
with Image.open(f) as loaded:
|
||||
assert isinstance(loaded, TiffImagePlugin.TiffImageFile)
|
||||
reloaded = loaded.tag_v2.named()
|
||||
|
||||
ignored = ["StripByteCounts", "RowsPerStrip", "PageNumber", "StripOffsets"]
|
||||
|
@ -163,8 +168,9 @@ def test_write_metadata(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:
|
||||
assert isinstance(im, TiffImagePlugin.TiffImageFile)
|
||||
info = im.tag_v2
|
||||
del info[278]
|
||||
|
||||
|
@ -178,6 +184,7 @@ def test_change_stripbytecounts_tag_type(tmp_path: Path) -> None:
|
|||
im.save(out, tiffinfo=info)
|
||||
|
||||
with Image.open(out) as reloaded:
|
||||
assert isinstance(reloaded, TiffImagePlugin.TiffImageFile)
|
||||
assert reloaded.tag_v2.tagtype[TiffImagePlugin.STRIPBYTECOUNTS] == TiffTags.LONG
|
||||
|
||||
|
||||
|
@ -210,7 +217,7 @@ def test_no_duplicate_50741_tag() -> 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:
|
||||
im.save(out)
|
||||
|
||||
|
@ -227,10 +234,11 @@ def test_writing_other_types_to_ascii(
|
|||
info[271] = value
|
||||
|
||||
im = hopper()
|
||||
out = str(tmp_path / "temp.tiff")
|
||||
out = tmp_path / "temp.tiff"
|
||||
im.save(out, tiffinfo=info)
|
||||
|
||||
with Image.open(out) as reloaded:
|
||||
assert isinstance(reloaded, TiffImagePlugin.TiffImageFile)
|
||||
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
|
||||
|
||||
out = str(tmp_path / "temp.tiff")
|
||||
out = tmp_path / "temp.tiff"
|
||||
im.save(out, tiffinfo=info)
|
||||
|
||||
with Image.open(out) as reloaded:
|
||||
assert isinstance(reloaded, TiffImagePlugin.TiffImageFile)
|
||||
assert reloaded.tag_v2[700] == b"\x01"
|
||||
|
||||
|
||||
|
@ -263,10 +272,11 @@ def test_writing_other_types_to_undefined(
|
|||
|
||||
info[33723] = value
|
||||
|
||||
out = str(tmp_path / "temp.tiff")
|
||||
out = tmp_path / "temp.tiff"
|
||||
im.save(out, tiffinfo=info)
|
||||
|
||||
with Image.open(out) as reloaded:
|
||||
assert isinstance(reloaded, TiffImagePlugin.TiffImageFile)
|
||||
assert reloaded.tag_v2[33723] == b"1"
|
||||
|
||||
|
||||
|
@ -296,7 +306,7 @@ def test_empty_metadata() -> None:
|
|||
|
||||
def test_iccprofile(tmp_path: Path) -> None:
|
||||
# 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:
|
||||
im.save(out)
|
||||
|
||||
|
@ -311,19 +321,20 @@ def test_iccprofile_binary() -> None:
|
|||
# but probably won't be able to save it.
|
||||
|
||||
with Image.open("Tests/images/hopper.iccprofile_binary.tif") as im:
|
||||
assert isinstance(im, TiffImagePlugin.TiffImageFile)
|
||||
assert im.tag_v2.tagtype[34675] == 1
|
||||
assert im.info["icc_profile"]
|
||||
|
||||
|
||||
def test_iccprofile_save_png(tmp_path: Path) -> None:
|
||||
with Image.open("Tests/images/hopper.iccprofile.tif") as im:
|
||||
outfile = str(tmp_path / "temp.png")
|
||||
outfile = tmp_path / "temp.png"
|
||||
im.save(outfile)
|
||||
|
||||
|
||||
def test_iccprofile_binary_save_png(tmp_path: Path) -> None:
|
||||
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)
|
||||
|
||||
|
||||
|
@ -332,10 +343,11 @@ def test_exif_div_zero(tmp_path: Path) -> None:
|
|||
info = TiffImagePlugin.ImageFileDirectory_v2()
|
||||
info[41988] = TiffImagePlugin.IFDRational(0, 0)
|
||||
|
||||
out = str(tmp_path / "temp.tiff")
|
||||
out = tmp_path / "temp.tiff"
|
||||
im.save(out, tiffinfo=info, compression="raw")
|
||||
|
||||
with Image.open(out) as reloaded:
|
||||
assert isinstance(reloaded, TiffImagePlugin.TiffImageFile)
|
||||
assert 0 == reloaded.tag_v2[41988].numerator
|
||||
assert 0 == reloaded.tag_v2[41988].denominator
|
||||
|
||||
|
@ -351,10 +363,11 @@ def test_ifd_unsigned_rational(tmp_path: Path) -> None:
|
|||
|
||||
info[41493] = TiffImagePlugin.IFDRational(numerator, 1)
|
||||
|
||||
out = str(tmp_path / "temp.tiff")
|
||||
out = tmp_path / "temp.tiff"
|
||||
im.save(out, tiffinfo=info, compression="raw")
|
||||
|
||||
with Image.open(out) as reloaded:
|
||||
assert isinstance(reloaded, TiffImagePlugin.TiffImageFile)
|
||||
assert max_long == reloaded.tag_v2[41493].numerator
|
||||
assert 1 == reloaded.tag_v2[41493].denominator
|
||||
|
||||
|
@ -363,10 +376,11 @@ def test_ifd_unsigned_rational(tmp_path: Path) -> None:
|
|||
|
||||
info[41493] = TiffImagePlugin.IFDRational(numerator, 1)
|
||||
|
||||
out = str(tmp_path / "temp.tiff")
|
||||
out = tmp_path / "temp.tiff"
|
||||
im.save(out, tiffinfo=info, compression="raw")
|
||||
|
||||
with Image.open(out) as reloaded:
|
||||
assert isinstance(reloaded, TiffImagePlugin.TiffImageFile)
|
||||
assert max_long == reloaded.tag_v2[41493].numerator
|
||||
assert 1 == reloaded.tag_v2[41493].denominator
|
||||
|
||||
|
@ -381,10 +395,11 @@ def test_ifd_signed_rational(tmp_path: Path) -> None:
|
|||
|
||||
info[37380] = TiffImagePlugin.IFDRational(numerator, denominator)
|
||||
|
||||
out = str(tmp_path / "temp.tiff")
|
||||
out = tmp_path / "temp.tiff"
|
||||
im.save(out, tiffinfo=info, compression="raw")
|
||||
|
||||
with Image.open(out) as reloaded:
|
||||
assert isinstance(reloaded, TiffImagePlugin.TiffImageFile)
|
||||
assert numerator == reloaded.tag_v2[37380].numerator
|
||||
assert denominator == reloaded.tag_v2[37380].denominator
|
||||
|
||||
|
@ -393,10 +408,11 @@ def test_ifd_signed_rational(tmp_path: Path) -> None:
|
|||
|
||||
info[37380] = TiffImagePlugin.IFDRational(numerator, denominator)
|
||||
|
||||
out = str(tmp_path / "temp.tiff")
|
||||
out = tmp_path / "temp.tiff"
|
||||
im.save(out, tiffinfo=info, compression="raw")
|
||||
|
||||
with Image.open(out) as reloaded:
|
||||
assert isinstance(reloaded, TiffImagePlugin.TiffImageFile)
|
||||
assert numerator == reloaded.tag_v2[37380].numerator
|
||||
assert denominator == reloaded.tag_v2[37380].denominator
|
||||
|
||||
|
@ -406,10 +422,11 @@ def test_ifd_signed_rational(tmp_path: Path) -> None:
|
|||
|
||||
info[37380] = TiffImagePlugin.IFDRational(numerator, denominator)
|
||||
|
||||
out = str(tmp_path / "temp.tiff")
|
||||
out = tmp_path / "temp.tiff"
|
||||
im.save(out, tiffinfo=info, compression="raw")
|
||||
|
||||
with Image.open(out) as reloaded:
|
||||
assert isinstance(reloaded, TiffImagePlugin.TiffImageFile)
|
||||
assert 2**31 - 1 == reloaded.tag_v2[37380].numerator
|
||||
assert -1 == reloaded.tag_v2[37380].denominator
|
||||
|
||||
|
@ -420,10 +437,11 @@ def test_ifd_signed_long(tmp_path: Path) -> None:
|
|||
|
||||
info[37000] = -60000
|
||||
|
||||
out = str(tmp_path / "temp.tiff")
|
||||
out = tmp_path / "temp.tiff"
|
||||
im.save(out, tiffinfo=info, compression="raw")
|
||||
|
||||
with Image.open(out) as reloaded:
|
||||
assert isinstance(reloaded, TiffImagePlugin.TiffImageFile)
|
||||
assert reloaded.tag_v2[37000] == -60000
|
||||
|
||||
|
||||
|
@ -444,11 +462,13 @@ def test_empty_values() -> None:
|
|||
|
||||
def test_photoshop_info(tmp_path: Path) -> None:
|
||||
with Image.open("Tests/images/issue_2278.tif") as im:
|
||||
assert isinstance(im, TiffImagePlugin.TiffImageFile)
|
||||
assert len(im.tag_v2[34377]) == 70
|
||||
assert isinstance(im.tag_v2[34377], bytes)
|
||||
out = str(tmp_path / "temp.tiff")
|
||||
out = tmp_path / "temp.tiff"
|
||||
im.save(out)
|
||||
with Image.open(out) as reloaded:
|
||||
assert isinstance(reloaded, TiffImagePlugin.TiffImageFile)
|
||||
assert len(reloaded.tag_v2[34377]) == 70
|
||||
assert isinstance(reloaded.tag_v2[34377], bytes)
|
||||
|
||||
|
@ -480,7 +500,7 @@ def test_tag_group_data() -> None:
|
|||
|
||||
|
||||
def test_empty_subifd(tmp_path: Path) -> None:
|
||||
out = str(tmp_path / "temp.jpg")
|
||||
out = tmp_path / "temp.jpg"
|
||||
|
||||
im = hopper()
|
||||
exif = im.getexif()
|
||||
|
|
|
@ -21,7 +21,11 @@ def test_open() -> None:
|
|||
|
||||
def test_load() -> None:
|
||||
with WalImageFile.open(TEST_FILE) as im:
|
||||
assert im.load()[0, 0] == 122
|
||||
px = im.load()
|
||||
assert px is not None
|
||||
assert px[0, 0] == 122
|
||||
|
||||
# Test again now that it has already been loaded once
|
||||
assert im.load()[0, 0] == 122
|
||||
px = im.load()
|
||||
assert px is not None
|
||||
assert px[0, 0] == 122
|
||||
|
|
|
@ -28,9 +28,9 @@ except ImportError:
|
|||
|
||||
|
||||
class TestUnsupportedWebp:
|
||||
def test_unsupported(self) -> None:
|
||||
def test_unsupported(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
if HAVE_WEBP:
|
||||
WebPImagePlugin.SUPPORTED = False
|
||||
monkeypatch.setattr(WebPImagePlugin, "SUPPORTED", False)
|
||||
|
||||
file_path = "Tests/images/hopper.webp"
|
||||
with pytest.warns(UserWarning):
|
||||
|
@ -38,9 +38,6 @@ class TestUnsupportedWebp:
|
|||
with Image.open(file_path):
|
||||
pass
|
||||
|
||||
if HAVE_WEBP:
|
||||
WebPImagePlugin.SUPPORTED = True
|
||||
|
||||
|
||||
@skip_unless_feature("webp")
|
||||
class TestFileWebp:
|
||||
|
@ -157,9 +154,8 @@ class TestFileWebp:
|
|||
@pytest.mark.skipif(sys.maxsize <= 2**32, reason="Requires 64-bit system")
|
||||
def test_write_encoding_error_message(self, tmp_path: Path) -> None:
|
||||
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)
|
||||
assert str(e.value) == "encoding error 6"
|
||||
|
||||
@pytest.mark.skipif(sys.maxsize <= 2**32, reason="Requires 64-bit system")
|
||||
def test_write_encoding_error_bad_dimension(self, tmp_path: Path) -> None:
|
||||
|
@ -234,7 +230,7 @@ class TestFileWebp:
|
|||
|
||||
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(0, 3))
|
||||
difference = sum(abs(original_value[i] - reread_value[i]) for i in range(3))
|
||||
assert difference < 5
|
||||
|
||||
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?
|
||||
"""
|
||||
|
||||
temp_file = str(tmp_path / "temp.webp")
|
||||
temp_file = tmp_path / "temp.webp"
|
||||
# temp_file = "temp.webp"
|
||||
|
||||
pil_image = hopper("RGBA")
|
||||
|
@ -71,7 +71,7 @@ def test_write_rgba(tmp_path: Path) -> None:
|
|||
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.save(temp_file)
|
||||
|
@ -104,7 +104,7 @@ def test_keep_rgb_values_when_transparent(tmp_path: Path) -> None:
|
|||
half_transparent_image.putalpha(new_alpha)
|
||||
|
||||
# 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)
|
||||
|
||||
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.
|
||||
"""
|
||||
|
||||
temp_file = str(tmp_path / "temp.webp")
|
||||
temp_file = tmp_path / "temp.webp"
|
||||
file_path = "Tests/images/transparent.gif"
|
||||
with Image.open(file_path) as im:
|
||||
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:
|
||||
with Image.open("Tests/images/transparent.png") as im:
|
||||
out = str(tmp_path / "temp.webp")
|
||||
out = tmp_path / "temp.webp"
|
||||
im.save(out)
|
||||
|
||||
out_quality = str(tmp_path / "quality.webp")
|
||||
out_quality = tmp_path / "quality.webp"
|
||||
im.save(out_quality, alpha_quality=50)
|
||||
with Image.open(out) as reloaded:
|
||||
with Image.open(out_quality) as reloaded_quality:
|
||||
|
|
|
@ -6,7 +6,7 @@ from pathlib import Path
|
|||
import pytest
|
||||
from packaging.version import parse as parse_version
|
||||
|
||||
from PIL import Image, features
|
||||
from PIL import GifImagePlugin, Image, WebPImagePlugin, features
|
||||
|
||||
from .helper import (
|
||||
assert_image_equal,
|
||||
|
@ -22,10 +22,12 @@ def test_n_frames() -> None:
|
|||
"""Ensure that WebP format sets n_frames and is_animated attributes correctly."""
|
||||
|
||||
with Image.open("Tests/images/hopper.webp") as im:
|
||||
assert isinstance(im, WebPImagePlugin.WebPImageFile)
|
||||
assert im.n_frames == 1
|
||||
assert not im.is_animated
|
||||
|
||||
with Image.open("Tests/images/iss634.webp") as im:
|
||||
assert isinstance(im, WebPImagePlugin.WebPImageFile)
|
||||
assert im.n_frames == 42
|
||||
assert im.is_animated
|
||||
|
||||
|
@ -37,11 +39,13 @@ def test_write_animation_L(tmp_path: Path) -> None:
|
|||
"""
|
||||
|
||||
with Image.open("Tests/images/iss634.gif") as orig:
|
||||
assert isinstance(orig, GifImagePlugin.GifImageFile)
|
||||
assert orig.n_frames > 1
|
||||
|
||||
temp_file = str(tmp_path / "temp.webp")
|
||||
temp_file = tmp_path / "temp.webp"
|
||||
orig.save(temp_file, save_all=True)
|
||||
with Image.open(temp_file) as im:
|
||||
assert isinstance(im, WebPImagePlugin.WebPImageFile)
|
||||
assert im.n_frames == orig.n_frames
|
||||
|
||||
# Compare first and last frames to the original animated GIF
|
||||
|
@ -67,8 +71,9 @@ def test_write_animation_RGB(tmp_path: Path) -> None:
|
|||
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:
|
||||
assert isinstance(im, WebPImagePlugin.WebPImageFile)
|
||||
assert im.n_frames == 2
|
||||
|
||||
# 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_frame2.webp") as frame2:
|
||||
temp_file1 = str(tmp_path / "temp.webp")
|
||||
temp_file1 = tmp_path / "temp.webp"
|
||||
frame1.copy().save(
|
||||
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]:
|
||||
yield from ims
|
||||
|
||||
temp_file2 = str(tmp_path / "temp_generator.webp")
|
||||
temp_file2 = tmp_path / "temp_generator.webp"
|
||||
frame1.copy().save(
|
||||
temp_file2,
|
||||
save_all=True,
|
||||
|
@ -116,7 +121,7 @@ def test_timestamp_and_duration(tmp_path: Path) -> None:
|
|||
"""
|
||||
|
||||
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_frame2.webp") as frame2:
|
||||
frame1.save(
|
||||
|
@ -127,6 +132,7 @@ def test_timestamp_and_duration(tmp_path: Path) -> None:
|
|||
)
|
||||
|
||||
with Image.open(temp_file) as im:
|
||||
assert isinstance(im, WebPImagePlugin.WebPImageFile)
|
||||
assert im.n_frames == 5
|
||||
assert im.is_animated
|
||||
|
||||
|
@ -141,7 +147,7 @@ def test_timestamp_and_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:
|
||||
assert im.info["duration"] == 70.0
|
||||
|
||||
|
@ -159,7 +165,7 @@ def test_seeking(tmp_path: Path) -> None:
|
|||
"""
|
||||
|
||||
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_frame2.webp") as frame2:
|
||||
frame1.save(
|
||||
|
@ -170,6 +176,7 @@ def test_seeking(tmp_path: Path) -> None:
|
|||
)
|
||||
|
||||
with Image.open(temp_file) as im:
|
||||
assert isinstance(im, WebPImagePlugin.WebPImageFile)
|
||||
assert im.n_frames == 5
|
||||
assert im.is_animated
|
||||
|
||||
|
@ -196,10 +203,10 @@ def test_alpha_quality(tmp_path: Path) -> None:
|
|||
with Image.open("Tests/images/transparent.png") as im:
|
||||
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])
|
||||
|
||||
out_quality = str(tmp_path / "quality.webp")
|
||||
out_quality = tmp_path / "quality.webp"
|
||||
first_frame.save(
|
||||
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:
|
||||
temp_file = str(tmp_path / "temp.webp")
|
||||
temp_file = tmp_path / "temp.webp"
|
||||
|
||||
hopper(RGB_MODE).save(temp_file, lossless=True)
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@ from types import ModuleType
|
|||
|
||||
import pytest
|
||||
|
||||
from PIL import Image
|
||||
from PIL import Image, WebPImagePlugin
|
||||
|
||||
from .helper import mark_if_feature_version, skip_unless_feature
|
||||
|
||||
|
@ -40,7 +40,7 @@ def test_read_exif_metadata() -> None:
|
|||
def test_read_exif_metadata_without_prefix() -> None:
|
||||
with Image.open("Tests/images/flower2.webp") as im:
|
||||
# Assert prefix is not present
|
||||
assert im.info["exif"][:6] != b"Exif\x00\x00"
|
||||
assert not im.info["exif"].startswith(b"Exif\x00\x00")
|
||||
|
||||
exif = im.getexif()
|
||||
assert exif[305] == "Adobe Photoshop CS6 (Macintosh)"
|
||||
|
@ -110,6 +110,7 @@ def test_read_no_exif() -> None:
|
|||
|
||||
test_buffer.seek(0)
|
||||
with Image.open(test_buffer) as webp_image:
|
||||
assert isinstance(webp_image, WebPImagePlugin.WebPImageFile)
|
||||
assert not webp_image._getexif()
|
||||
|
||||
|
||||
|
@ -146,7 +147,7 @@ def test_write_animated_metadata(tmp_path: Path) -> None:
|
|||
exif_data = b"<exif_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_frame2.webp") as frame2:
|
||||
frame1.save(
|
||||
|
|
|
@ -8,7 +8,7 @@ import pytest
|
|||
|
||||
from PIL import Image, ImageFile, WmfImagePlugin
|
||||
|
||||
from .helper import assert_image_similar_tofile, hopper
|
||||
from .helper import assert_image_equal_tofile, assert_image_similar_tofile, hopper
|
||||
|
||||
|
||||
def test_load_raw() -> None:
|
||||
|
@ -32,7 +32,9 @@ def test_load_raw() -> None:
|
|||
def test_load() -> None:
|
||||
with Image.open("Tests/images/drawing.emf") as im:
|
||||
if hasattr(Image.core, "drawwmf"):
|
||||
assert im.load()[0, 0] == (255, 255, 255)
|
||||
px = im.load()
|
||||
assert px is not None
|
||||
assert px[0, 0] == (255, 255, 255)
|
||||
|
||||
|
||||
def test_load_zero_inch() -> None:
|
||||
|
@ -42,6 +44,15 @@ def test_load_zero_inch() -> None:
|
|||
pass
|
||||
|
||||
|
||||
def test_render() -> None:
|
||||
with open("Tests/images/drawing.emf", "rb") as fp:
|
||||
data = fp.read()
|
||||
b = BytesIO(data[:808] + b"\x00" + data[809:])
|
||||
with Image.open(b) as im:
|
||||
if hasattr(Image.core, "drawwmf"):
|
||||
assert_image_equal_tofile(im, "Tests/images/drawing.emf")
|
||||
|
||||
|
||||
class TestHandler(ImageFile.StubHandler):
|
||||
methodCalled = False
|
||||
|
||||
|
@ -61,7 +72,7 @@ def test_register_handler(tmp_path: Path) -> None:
|
|||
WmfImagePlugin.register_handler(handler)
|
||||
|
||||
im = hopper()
|
||||
tmpfile = str(tmp_path / "temp.wmf")
|
||||
tmpfile = tmp_path / "temp.wmf"
|
||||
im.save(tmpfile)
|
||||
assert handler.methodCalled
|
||||
|
||||
|
@ -75,13 +86,14 @@ def test_load_float_dpi() -> None:
|
|||
|
||||
with open("Tests/images/drawing.emf", "rb") as fp:
|
||||
data = fp.read()
|
||||
b = BytesIO(data[:8] + b"\x06\xFA" + data[10:])
|
||||
b = BytesIO(data[:8] + b"\x06\xfa" + data[10:])
|
||||
with Image.open(b) as im:
|
||||
assert im.info["dpi"][0] == 2540
|
||||
|
||||
|
||||
def test_load_set_dpi() -> None:
|
||||
with Image.open("Tests/images/drawing.wmf") as im:
|
||||
assert isinstance(im, WmfImagePlugin.WmfStubImageFile)
|
||||
assert im.size == (82, 82)
|
||||
|
||||
if hasattr(Image.core, "drawwmf"):
|
||||
|
@ -90,11 +102,27 @@ def test_load_set_dpi() -> None:
|
|||
|
||||
assert_image_similar_tofile(im, "Tests/images/drawing_wmf_ref_144.png", 2.1)
|
||||
|
||||
with Image.open("Tests/images/drawing.emf") as im:
|
||||
assert im.size == (1625, 1625)
|
||||
|
||||
if not hasattr(Image.core, "drawwmf"):
|
||||
return
|
||||
assert isinstance(im, WmfImagePlugin.WmfStubImageFile)
|
||||
im.load(im.info["dpi"])
|
||||
assert im.size == (1625, 1625)
|
||||
|
||||
with Image.open("Tests/images/drawing.emf") as im:
|
||||
assert isinstance(im, WmfImagePlugin.WmfStubImageFile)
|
||||
im.load((72, 144))
|
||||
assert im.size == (82, 164)
|
||||
|
||||
assert_image_equal_tofile(im, "Tests/images/drawing_emf_ref_72_144.png")
|
||||
|
||||
|
||||
@pytest.mark.parametrize("ext", (".wmf", ".emf"))
|
||||
def test_save(ext: str, tmp_path: Path) -> None:
|
||||
im = hopper()
|
||||
|
||||
tmpfile = str(tmp_path / ("temp" + ext))
|
||||
tmpfile = tmp_path / ("temp" + ext)
|
||||
with pytest.raises(OSError):
|
||||
im.save(tmpfile)
|
||||
|
|
|
@ -73,7 +73,7 @@ def test_invalid_file() -> None:
|
|||
|
||||
def test_save_wrong_mode(tmp_path: Path) -> None:
|
||||
im = hopper()
|
||||
out = str(tmp_path / "temp.xbm")
|
||||
out = tmp_path / "temp.xbm"
|
||||
|
||||
with pytest.raises(OSError):
|
||||
im.save(out)
|
||||
|
@ -81,7 +81,7 @@ def test_save_wrong_mode(tmp_path: Path) -> None:
|
|||
|
||||
def test_hotspot(tmp_path: Path) -> None:
|
||||
im = hopper("1")
|
||||
out = str(tmp_path / "temp.xbm")
|
||||
out = tmp_path / "temp.xbm"
|
||||
|
||||
hotspot = (0, 7)
|
||||
im.save(out, hotspot=hotspot)
|
||||
|
|
|
@ -30,6 +30,7 @@ def test_invalid_file() -> None:
|
|||
def test_load_read() -> None:
|
||||
# Arrange
|
||||
with Image.open(TEST_FILE) as im:
|
||||
assert isinstance(im, XpmImagePlugin.XpmImageFile)
|
||||
dummy_bytes = 1
|
||||
|
||||
# Act
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import io
|
||||
|
||||
import pytest
|
||||
|
||||
from PIL import BdfFontFile, FontFile
|
||||
|
@ -8,13 +10,20 @@ filename = "Tests/images/courB08.bdf"
|
|||
|
||||
|
||||
def test_sanity() -> None:
|
||||
with open(filename, "rb") as test_file:
|
||||
font = BdfFontFile.BdfFontFile(test_file)
|
||||
with open(filename, "rb") as fp:
|
||||
font = BdfFontFile.BdfFontFile(fp)
|
||||
|
||||
assert isinstance(font, FontFile.FontFile)
|
||||
assert len([_f for _f in font.glyph if _f]) == 190
|
||||
|
||||
|
||||
def test_zero_width_chars() -> None:
|
||||
with open(filename, "rb") as fp:
|
||||
data = fp.read()
|
||||
data = data[:2650] + b"\x00\x00" + data[2652:]
|
||||
BdfFontFile.BdfFontFile(io.BytesIO(data))
|
||||
|
||||
|
||||
def test_invalid_file() -> None:
|
||||
with open("Tests/images/flower.jpg", "rb") as fp:
|
||||
with pytest.raises(SyntaxError):
|
||||
|
|
|
@ -4,7 +4,20 @@ from pathlib import Path
|
|||
|
||||
import pytest
|
||||
|
||||
from PIL import FontFile
|
||||
from PIL import FontFile, Image
|
||||
|
||||
|
||||
def test_compile() -> None:
|
||||
font = FontFile.FontFile()
|
||||
font.glyph[0] = ((0, 0), (0, 0, 0, 0), (0, 0, 0, 1), Image.new("L", (0, 0)))
|
||||
font.compile()
|
||||
assert font.ysize == 1
|
||||
|
||||
font.ysize = 2
|
||||
font.compile()
|
||||
|
||||
# Assert that compiling again did not change anything
|
||||
assert font.ysize == 2
|
||||
|
||||
|
||||
def test_save(tmp_path: Path) -> None:
|
||||
|
|
|
@ -22,28 +22,26 @@ def test_sanity() -> None:
|
|||
Image.new("HSV", (100, 100))
|
||||
|
||||
|
||||
def wedge() -> Image.Image:
|
||||
w = Image._wedge()
|
||||
w90 = w.rotate(90)
|
||||
def linear_gradient() -> Image.Image:
|
||||
im = Image.linear_gradient(mode="L")
|
||||
im90 = im.rotate(90)
|
||||
|
||||
(px, h) = w.size
|
||||
(px, h) = im.size
|
||||
|
||||
r = Image.new("L", (px * 3, h))
|
||||
g = r.copy()
|
||||
b = r.copy()
|
||||
|
||||
r.paste(w, (0, 0))
|
||||
r.paste(w90, (px, 0))
|
||||
r.paste(im, (0, 0))
|
||||
r.paste(im90, (px, 0))
|
||||
|
||||
g.paste(w90, (0, 0))
|
||||
g.paste(w, (2 * px, 0))
|
||||
g.paste(im90, (0, 0))
|
||||
g.paste(im, (2 * px, 0))
|
||||
|
||||
b.paste(w, (px, 0))
|
||||
b.paste(w90, (2 * px, 0))
|
||||
b.paste(im, (px, 0))
|
||||
b.paste(im90, (2 * px, 0))
|
||||
|
||||
img = Image.merge("RGB", (r, g, b))
|
||||
|
||||
return img
|
||||
return Image.merge("RGB", (r, g, b))
|
||||
|
||||
|
||||
def to_xxx_colorsys(
|
||||
|
@ -79,8 +77,8 @@ def to_rgb_colorsys(im: Image.Image) -> Image.Image:
|
|||
return to_xxx_colorsys(im, colorsys.hsv_to_rgb, "RGB")
|
||||
|
||||
|
||||
def test_wedge() -> None:
|
||||
src = wedge().resize((3 * 32, 32), Image.Resampling.BILINEAR)
|
||||
def test_linear_gradient() -> None:
|
||||
src = linear_gradient().resize((3 * 32, 32), Image.Resampling.BILINEAR)
|
||||
im = src.convert("HSV")
|
||||
comparable = to_hsv_colorsys(src)
|
||||
|
||||
|
|
|
@ -65,21 +65,20 @@ class TestImage:
|
|||
|
||||
@pytest.mark.parametrize("mode", ("", "bad", "very very long"))
|
||||
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))
|
||||
assert str(e.value) == "unrecognized image mode"
|
||||
|
||||
def test_exception_inheritance(self) -> None:
|
||||
assert issubclass(UnidentifiedImageError, OSError)
|
||||
|
||||
def test_sanity(self) -> None:
|
||||
im = Image.new("L", (100, 100))
|
||||
assert repr(im)[:45] == "<PIL.Image.Image image mode=L size=100x100 at"
|
||||
assert repr(im).startswith("<PIL.Image.Image image mode=L size=100x100 at")
|
||||
assert im.mode == "L"
|
||||
assert im.size == (100, 100)
|
||||
|
||||
im = Image.new("RGB", (100, 100))
|
||||
assert repr(im)[:45] == "<PIL.Image.Image image mode=RGB size=100x100 "
|
||||
assert repr(im).startswith("<PIL.Image.Image image mode=RGB size=100x100 ")
|
||||
assert im.mode == "RGB"
|
||||
assert im.size == (100, 100)
|
||||
|
||||
|
@ -176,6 +175,13 @@ class TestImage:
|
|||
with Image.open(io.StringIO()): # type: ignore[arg-type]
|
||||
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:
|
||||
with Image.open(Path("Tests/images/multipage-mmap.tiff")) as im:
|
||||
assert im.mode == "P"
|
||||
|
@ -188,16 +194,13 @@ class TestImage:
|
|||
for ext in (".jpg", ".jp2"):
|
||||
if ext == ".jp2" and not features.check_codec("jpg_2000"):
|
||||
pytest.skip("jpg_2000 not available")
|
||||
temp_file = str(tmp_path / ("temp." + ext))
|
||||
if os.path.exists(temp_file):
|
||||
os.remove(temp_file)
|
||||
im.save(Path(temp_file))
|
||||
im.save(tmp_path / ("temp." + ext))
|
||||
|
||||
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):
|
||||
name: str
|
||||
name: Path
|
||||
|
||||
if sys.version_info >= (3, 12):
|
||||
from collections.abc import Buffer
|
||||
|
@ -228,7 +231,7 @@ class TestImage:
|
|||
|
||||
def test_unknown_extension(self, tmp_path: Path) -> None:
|
||||
im = hopper()
|
||||
temp_file = str(tmp_path / "temp.unknown")
|
||||
temp_file = tmp_path / "temp.unknown"
|
||||
with pytest.raises(ValueError):
|
||||
im.save(temp_file)
|
||||
|
||||
|
@ -248,7 +251,7 @@ class TestImage:
|
|||
reason="Test requires opening an mmaped file for writing",
|
||||
)
|
||||
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)
|
||||
|
||||
with Image.open(temp_file) as im:
|
||||
|
@ -580,9 +583,7 @@ class TestImage:
|
|||
def test_one_item_tuple(self) -> None:
|
||||
for mode in ("I", "F", "L"):
|
||||
im = Image.new(mode, (100, 100), (5,))
|
||||
px = im.load()
|
||||
assert px is not None
|
||||
assert px[0, 0] == 5
|
||||
assert im.getpixel((0, 0)) == 5
|
||||
|
||||
def test_linear_gradient_wrong_mode(self) -> None:
|
||||
# Arrange
|
||||
|
@ -662,12 +663,13 @@ class TestImage:
|
|||
im.putpalette(list(range(256)) * 4, "RGBA")
|
||||
im_remapped = im.remap_palette(list(range(256)))
|
||||
assert_image_equal(im, im_remapped)
|
||||
assert im.palette is not None
|
||||
assert im.palette.palette == im_remapped.palette.palette
|
||||
|
||||
# Test illegal image mode
|
||||
with hopper() as im:
|
||||
with pytest.raises(ValueError):
|
||||
im.remap_palette(None)
|
||||
im.remap_palette([])
|
||||
|
||||
def test_remap_palette_transparency(self) -> None:
|
||||
im = Image.new("P", (1, 2), (0, 0, 0))
|
||||
|
@ -732,7 +734,7 @@ class TestImage:
|
|||
# https://github.com/python-pillow/Pillow/issues/835
|
||||
# Arrange
|
||||
test_file = "Tests/images/hopper.png"
|
||||
temp_file = str(tmp_path / "temp.jpg")
|
||||
temp_file = tmp_path / "temp.jpg"
|
||||
|
||||
# Act/Assert
|
||||
with Image.open(test_file) as im:
|
||||
|
@ -742,7 +744,7 @@ class TestImage:
|
|||
im.save(temp_file)
|
||||
|
||||
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))
|
||||
with pytest.raises(ValueError):
|
||||
|
@ -770,7 +772,7 @@ class TestImage:
|
|||
assert dict(exif)
|
||||
|
||||
# Test that exif data is cleared after another load
|
||||
exif.load(None)
|
||||
exif.load(b"")
|
||||
assert not dict(exif)
|
||||
|
||||
# Test loading just the EXIF header
|
||||
|
@ -793,6 +795,10 @@ class TestImage:
|
|||
ifd[36864] = b"0220"
|
||||
assert exif.get_ifd(0x8769) == {36864: b"0220"}
|
||||
|
||||
reloaded_exif = Image.Exif()
|
||||
reloaded_exif.load(exif.tobytes())
|
||||
assert reloaded_exif.get_ifd(0x8769) == {36864: b"0220"}
|
||||
|
||||
@mark_if_feature_version(
|
||||
pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing"
|
||||
)
|
||||
|
@ -805,7 +811,7 @@ class TestImage:
|
|||
assert exif[296] == 2
|
||||
assert exif[11] == "gThumb 3.0.1"
|
||||
|
||||
out = str(tmp_path / "temp.jpg")
|
||||
out = tmp_path / "temp.jpg"
|
||||
exif[258] = 8
|
||||
del exif[274]
|
||||
del exif[282]
|
||||
|
@ -827,7 +833,7 @@ class TestImage:
|
|||
assert exif[274] == 1
|
||||
assert exif[305] == "Adobe Photoshop CC 2017 (Macintosh)"
|
||||
|
||||
out = str(tmp_path / "temp.jpg")
|
||||
out = tmp_path / "temp.jpg"
|
||||
exif[258] = 8
|
||||
del exif[306]
|
||||
exif[274] = 455
|
||||
|
@ -846,7 +852,7 @@ class TestImage:
|
|||
exif = im.getexif()
|
||||
assert exif == {}
|
||||
|
||||
out = str(tmp_path / "temp.webp")
|
||||
out = tmp_path / "temp.webp"
|
||||
exif[258] = 8
|
||||
exif[40963] = 455
|
||||
exif[305] = "Pillow test"
|
||||
|
@ -868,7 +874,7 @@ class TestImage:
|
|||
exif = im.getexif()
|
||||
assert exif == {274: 1}
|
||||
|
||||
out = str(tmp_path / "temp.png")
|
||||
out = tmp_path / "temp.png"
|
||||
exif[258] = 8
|
||||
del exif[274]
|
||||
exif[40963] = 455
|
||||
|
@ -987,6 +993,11 @@ class TestImage:
|
|||
else:
|
||||
assert im.getxmp() == {"xmpmeta": None}
|
||||
|
||||
def test_get_child_images(self) -> None:
|
||||
im = Image.new("RGB", (1, 1))
|
||||
with pytest.warns(DeprecationWarning):
|
||||
assert im.get_child_images() == []
|
||||
|
||||
@pytest.mark.parametrize("size", ((1, 0), (0, 1), (0, 0)))
|
||||
def test_zero_tobytes(self, size: tuple[int, int]) -> None:
|
||||
im = Image.new("RGB", size)
|
||||
|
|
|
@ -271,13 +271,25 @@ class TestImagePutPixelError:
|
|||
|
||||
|
||||
class TestEmbeddable:
|
||||
@pytest.mark.xfail(reason="failing test")
|
||||
@pytest.mark.xfail(not (sys.version_info >= (3, 13)), reason="failing test")
|
||||
@pytest.mark.skipif(not is_win32(), reason="requires Windows")
|
||||
def test_embeddable(self) -> None:
|
||||
import ctypes
|
||||
|
||||
from setuptools.command import build_ext
|
||||
|
||||
compiler = getattr(build_ext, "new_compiler")()
|
||||
compiler.add_include_dir(sysconfig.get_config_var("INCLUDEPY"))
|
||||
|
||||
libdir = sysconfig.get_config_var("LIBDIR") or sysconfig.get_config_var(
|
||||
"INCLUDEPY"
|
||||
).replace("include", "libs")
|
||||
compiler.add_library_dir(libdir)
|
||||
try:
|
||||
compiler.initialize()
|
||||
except Exception:
|
||||
pytest.skip("Compiler could not be initialized")
|
||||
|
||||
with open("embed_pil.c", "w", encoding="utf-8") as fh:
|
||||
home = sys.prefix.replace("\\", "\\\\")
|
||||
fh.write(
|
||||
|
@ -305,13 +317,6 @@ int main(int argc, char* argv[])
|
|||
"""
|
||||
)
|
||||
|
||||
compiler = getattr(build_ext, "new_compiler")()
|
||||
compiler.add_include_dir(sysconfig.get_config_var("INCLUDEPY"))
|
||||
|
||||
libdir = sysconfig.get_config_var("LIBDIR") or sysconfig.get_config_var(
|
||||
"INCLUDEPY"
|
||||
).replace("include", "libs")
|
||||
compiler.add_library_dir(libdir)
|
||||
objects = compiler.compile(["embed_pil.c"])
|
||||
compiler.link_executable(objects, "embed_pil")
|
||||
|
||||
|
|
|
@ -118,7 +118,7 @@ def test_trns_p(tmp_path: Path) -> None:
|
|||
im = hopper("P")
|
||||
im.info["transparency"] = 0
|
||||
|
||||
f = str(tmp_path / "temp.png")
|
||||
f = tmp_path / "temp.png"
|
||||
|
||||
im_l = im.convert("L")
|
||||
assert im_l.info["transparency"] == 0
|
||||
|
@ -154,7 +154,7 @@ def test_trns_l(tmp_path: Path) -> None:
|
|||
im = hopper("L")
|
||||
im.info["transparency"] = 128
|
||||
|
||||
f = str(tmp_path / "temp.png")
|
||||
f = tmp_path / "temp.png"
|
||||
|
||||
im_la = im.convert("LA")
|
||||
assert "transparency" not in im_la.info
|
||||
|
@ -177,7 +177,7 @@ def test_trns_RGB(tmp_path: Path) -> None:
|
|||
im = hopper("RGB")
|
||||
im.info["transparency"] = im.getpixel((0, 0))
|
||||
|
||||
f = str(tmp_path / "temp.png")
|
||||
f = tmp_path / "temp.png"
|
||||
|
||||
im_l = im.convert("L")
|
||||
assert im_l.info["transparency"] == im_l.getpixel((0, 0)) # undone
|
||||
|
@ -222,9 +222,7 @@ def test_l_macro_rounding(convert_mode: str) -> None:
|
|||
im.palette.getcolor((0, 1, 2))
|
||||
|
||||
converted_im = im.convert(convert_mode)
|
||||
px = converted_im.load()
|
||||
assert px is not None
|
||||
converted_color = px[0, 0]
|
||||
converted_color = converted_im.getpixel((0, 0))
|
||||
if convert_mode == "LA":
|
||||
assert isinstance(converted_color, tuple)
|
||||
converted_color = converted_color[0]
|
||||
|
@ -236,6 +234,7 @@ def test_gif_with_rgba_palette_to_p() -> None:
|
|||
with Image.open("Tests/images/hopper.gif") as im:
|
||||
im.info["transparency"] = 255
|
||||
im.load()
|
||||
assert im.palette is not None
|
||||
assert im.palette.mode == "RGB"
|
||||
im_p = im.convert("P")
|
||||
|
||||
|
|
|
@ -148,10 +148,8 @@ def test_palette(method: Image.Quantize, color: tuple[int, ...]) -> None:
|
|||
im = Image.new("RGBA" if len(color) == 4 else "RGB", (1, 1), color)
|
||||
|
||||
converted = im.quantize(method=method)
|
||||
converted_px = converted.load()
|
||||
assert converted_px is not None
|
||||
assert converted.palette is not None
|
||||
assert converted_px[0, 0] == converted.palette.colors[color]
|
||||
assert converted.getpixel((0, 0)) == converted.palette.colors[color]
|
||||
|
||||
|
||||
def test_small_palette() -> None:
|
||||
|
|
|
@ -171,7 +171,7 @@ class TestImagingCoreResize:
|
|||
# platforms. So if a future Pillow change requires that the test file
|
||||
# be updated, that is okay.
|
||||
im = hopper().resize((64, 64))
|
||||
temp_file = str(tmp_path / "temp.gif")
|
||||
temp_file = tmp_path / "temp.gif"
|
||||
im.save(temp_file)
|
||||
|
||||
with Image.open(temp_file) as reloaded:
|
||||
|
@ -309,7 +309,7 @@ class TestImageResize:
|
|||
# Test unknown resampling filter
|
||||
with hopper() as im:
|
||||
with pytest.raises(ValueError):
|
||||
im.resize((10, 10), "unknown")
|
||||
im.resize((10, 10), -1)
|
||||
|
||||
@skip_unless_feature("libtiff")
|
||||
def test_transposed(self) -> None:
|
||||
|
|
|
@ -45,9 +45,9 @@ def test_split_merge(mode: str) -> None:
|
|||
|
||||
def test_split_open(tmp_path: Path) -> None:
|
||||
if features.check("zlib"):
|
||||
test_file = str(tmp_path / "temp.png")
|
||||
test_file = tmp_path / "temp.png"
|
||||
else:
|
||||
test_file = str(tmp_path / "temp.pcx")
|
||||
test_file = tmp_path / "temp.pcx"
|
||||
|
||||
def split_open(mode: str) -> int:
|
||||
hopper(mode).save(test_file)
|
||||
|
|
|
@ -104,20 +104,20 @@ def test_transposed() -> None:
|
|||
assert im.size == (590, 88)
|
||||
|
||||
|
||||
def test_load_first_unless_jpeg() -> None:
|
||||
def test_load_first_unless_jpeg(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
# Test that thumbnail() still uses draft() for JPEG
|
||||
with Image.open("Tests/images/hopper.jpg") as im:
|
||||
draft = im.draft
|
||||
original_draft = im.draft
|
||||
|
||||
def im_draft(
|
||||
mode: str, size: tuple[int, int]
|
||||
mode: str | None, size: tuple[int, int] | None
|
||||
) -> tuple[str, tuple[int, int, float, float]] | None:
|
||||
result = draft(mode, size)
|
||||
result = original_draft(mode, size)
|
||||
assert result is not None
|
||||
|
||||
return result
|
||||
|
||||
im.draft = im_draft
|
||||
monkeypatch.setattr(im, "draft", im_draft)
|
||||
|
||||
im.thumbnail((64, 64))
|
||||
|
||||
|
|
|
@ -47,6 +47,7 @@ class TestImageTransform:
|
|||
transformed = im.transform(
|
||||
im.size, Image.Transform.AFFINE, [1, 0, 0, 0, 1, 0]
|
||||
)
|
||||
assert im.palette is not None
|
||||
assert im.palette.palette == transformed.palette.palette
|
||||
|
||||
def test_extent(self) -> None:
|
||||
|
|
|
@ -39,6 +39,8 @@ BBOX = (((X0, Y0), (X1, Y1)), [(X0, Y0), (X1, Y1)], (X0, Y0, X1, Y1), [X0, Y0, X
|
|||
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],
|
||||
)
|
||||
|
@ -46,6 +48,8 @@ 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]],
|
||||
)
|
||||
|
||||
|
||||
|
@ -448,7 +452,6 @@ def test_shape1() -> None:
|
|||
x3, y3 = 95, 5
|
||||
|
||||
# Act
|
||||
assert ImageDraw.Outline is not None
|
||||
s = ImageDraw.Outline()
|
||||
s.move(x0, y0)
|
||||
s.curve(x1, y1, x2, y2, x3, y3)
|
||||
|
@ -470,7 +473,6 @@ def test_shape2() -> None:
|
|||
x3, y3 = 5, 95
|
||||
|
||||
# Act
|
||||
assert ImageDraw.Outline is not None
|
||||
s = ImageDraw.Outline()
|
||||
s.move(x0, y0)
|
||||
s.curve(x1, y1, x2, y2, x3, y3)
|
||||
|
@ -489,7 +491,6 @@ def test_transform() -> None:
|
|||
draw = ImageDraw.Draw(im)
|
||||
|
||||
# Act
|
||||
assert ImageDraw.Outline is not None
|
||||
s = ImageDraw.Outline()
|
||||
s.line(0, 0)
|
||||
s.transform((0, 0, 0, 0, 0, 0))
|
||||
|
@ -812,7 +813,7 @@ def test_rounded_rectangle(
|
|||
tuple[int, int, int, int]
|
||||
| tuple[list[int]]
|
||||
| tuple[tuple[int, int], tuple[int, int]]
|
||||
)
|
||||
),
|
||||
) -> None:
|
||||
# Arrange
|
||||
im = Image.new("RGB", (200, 200))
|
||||
|
@ -1047,8 +1048,8 @@ def create_base_image_draw(
|
|||
background2: tuple[int, int, int] = GRAY,
|
||||
) -> tuple[Image.Image, ImageDraw.ImageDraw]:
|
||||
img = Image.new(mode, size, background1)
|
||||
for x in range(0, size[0]):
|
||||
for y in range(0, size[1]):
|
||||
for x in range(size[0]):
|
||||
for y in range(size[1]):
|
||||
if (x + y) % 2 == 0:
|
||||
img.putpixel((x, y), background2)
|
||||
return img, ImageDraw.Draw(img)
|
||||
|
@ -1396,6 +1397,28 @@ def test_stroke_descender() -> None:
|
|||
assert_image_similar_tofile(im, "Tests/images/imagedraw_stroke_descender.png", 6.76)
|
||||
|
||||
|
||||
@skip_unless_feature("freetype2")
|
||||
def test_stroke_inside_gap() -> None:
|
||||
# Arrange
|
||||
im = Image.new("RGB", (120, 130))
|
||||
draw = ImageDraw.Draw(im)
|
||||
font = ImageFont.truetype("Tests/fonts/FreeMono.ttf", 120)
|
||||
|
||||
# Act
|
||||
draw.text((12, 12), "i", "#f00", font, stroke_width=20)
|
||||
|
||||
# Assert
|
||||
for y in range(im.height):
|
||||
glyph = ""
|
||||
for x in range(im.width):
|
||||
if im.getpixel((x, y)) == (0, 0, 0):
|
||||
if glyph == "started":
|
||||
glyph = "ended"
|
||||
else:
|
||||
assert glyph != "ended", "Gap inside stroked glyph"
|
||||
glyph = "started"
|
||||
|
||||
|
||||
@skip_unless_feature("freetype2")
|
||||
def test_split_word() -> None:
|
||||
# Arrange
|
||||
|
@ -1504,7 +1527,6 @@ def test_same_color_outline(bbox: Coords) -> None:
|
|||
x2, y2 = 95, 50
|
||||
x3, y3 = 95, 5
|
||||
|
||||
assert ImageDraw.Outline is not None
|
||||
s = ImageDraw.Outline()
|
||||
s.move(x0, y0)
|
||||
s.curve(x1, y1, x2, y2, x3, y3)
|
||||
|
@ -1608,7 +1630,7 @@ def test_compute_regular_polygon_vertices(
|
|||
0,
|
||||
ValueError,
|
||||
"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,
|
||||
|
@ -1622,7 +1644,7 @@ def test_compute_regular_polygon_vertices(
|
|||
((50, 50, 50), 25),
|
||||
0,
|
||||
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,
|
||||
|
@ -1647,9 +1669,8 @@ def test_compute_regular_polygon_vertices_input_error_handling(
|
|||
expected_error: type[Exception],
|
||||
error_message: str,
|
||||
) -> 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]
|
||||
assert str(e.value) == error_message
|
||||
|
||||
|
||||
def test_continuous_horizontal_edges_polygon() -> None:
|
||||
|
@ -1674,13 +1695,16 @@ def test_continuous_horizontal_edges_polygon() -> None:
|
|||
def test_discontiguous_corners_polygon() -> None:
|
||||
img, draw = create_base_image_draw((84, 68))
|
||||
draw.polygon(((1, 21), (34, 4), (71, 1), (38, 18)), BLACK)
|
||||
draw.polygon(
|
||||
((82, 29), (82, 26), (82, 24), (67, 22), (52, 29), (52, 15), (67, 22)), BLACK
|
||||
)
|
||||
draw.polygon(((71, 44), (38, 27), (1, 24)), BLACK)
|
||||
draw.polygon(
|
||||
((38, 66), (5, 49), (77, 49), (47, 66), (82, 63), (82, 47), (1, 47), (1, 63)),
|
||||
BLACK,
|
||||
)
|
||||
expected = os.path.join(IMAGES_PATH, "discontiguous_corners_polygon.png")
|
||||
assert_image_similar_tofile(img, expected, 1)
|
||||
assert_image_equal_tofile(img, expected)
|
||||
|
||||
|
||||
def test_polygon2() -> None:
|
||||
|
|
|
@ -93,6 +93,19 @@ class TestImageFile:
|
|||
assert p.image is not None
|
||||
assert (48, 48) == p.image.size
|
||||
|
||||
@pytest.mark.filterwarnings("ignore:Corrupt EXIF data")
|
||||
def test_incremental_tiff(self) -> None:
|
||||
with ImageFile.Parser() as p:
|
||||
with open("Tests/images/hopper.tif", "rb") as f:
|
||||
p.feed(f.read(1024))
|
||||
|
||||
# Check that insufficient data was given in the first feed
|
||||
assert not p.image
|
||||
|
||||
p.feed(f.read())
|
||||
assert p.image is not None
|
||||
assert (128, 128) == p.image.size
|
||||
|
||||
@skip_unless_feature("webp")
|
||||
def test_incremental_webp(self) -> None:
|
||||
with ImageFile.Parser() as p:
|
||||
|
@ -118,6 +131,26 @@ class TestImageFile:
|
|||
|
||||
assert_image_equal(im1, im2)
|
||||
|
||||
def test_tile_size(self) -> None:
|
||||
with open("Tests/images/hopper.tif", "rb") as im_fp:
|
||||
data = im_fp.read()
|
||||
|
||||
reads = []
|
||||
|
||||
class FP(BytesIO):
|
||||
def read(self, size: int | None = None) -> bytes:
|
||||
reads.append(size)
|
||||
return super().read(size)
|
||||
|
||||
fp = FP(data)
|
||||
with Image.open(fp) as im:
|
||||
assert len(im.tile) == 7
|
||||
|
||||
im.load()
|
||||
|
||||
# Despite multiple tiles, assert only one tile caused a read of maxblock size
|
||||
assert reads.count(im.decodermaxblock) == 1
|
||||
|
||||
def test_raise_oserror(self) -> None:
|
||||
with pytest.warns(DeprecationWarning):
|
||||
with pytest.raises(OSError):
|
||||
|
@ -163,9 +196,8 @@ class TestImageFile:
|
|||
b"0" * ImageFile.SAFEBLOCK
|
||||
) # 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)
|
||||
assert str(e.value) == "Truncated File Read"
|
||||
|
||||
@skip_unless_feature("zlib")
|
||||
def test_truncated_with_errors(self) -> None:
|
||||
|
@ -178,13 +210,10 @@ class TestImageFile:
|
|||
im.load()
|
||||
|
||||
@skip_unless_feature("zlib")
|
||||
def test_truncated_without_errors(self) -> None:
|
||||
def test_truncated_without_errors(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
with Image.open("Tests/images/truncated_image.png") as im:
|
||||
ImageFile.LOAD_TRUNCATED_IMAGES = True
|
||||
try:
|
||||
im.load()
|
||||
finally:
|
||||
ImageFile.LOAD_TRUNCATED_IMAGES = False
|
||||
monkeypatch.setattr(ImageFile, "LOAD_TRUNCATED_IMAGES", True)
|
||||
im.load()
|
||||
|
||||
@skip_unless_feature("zlib")
|
||||
def test_broken_datastream_with_errors(self) -> None:
|
||||
|
@ -193,13 +222,12 @@ class TestImageFile:
|
|||
im.load()
|
||||
|
||||
@skip_unless_feature("zlib")
|
||||
def test_broken_datastream_without_errors(self) -> None:
|
||||
def test_broken_datastream_without_errors(
|
||||
self, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
with Image.open("Tests/images/broken_data_stream.png") as im:
|
||||
ImageFile.LOAD_TRUNCATED_IMAGES = True
|
||||
try:
|
||||
im.load()
|
||||
finally:
|
||||
ImageFile.LOAD_TRUNCATED_IMAGES = False
|
||||
monkeypatch.setattr(ImageFile, "LOAD_TRUNCATED_IMAGES", True)
|
||||
im.load()
|
||||
|
||||
|
||||
class MockPyDecoder(ImageFile.PyDecoder):
|
||||
|
|
|
@ -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:
|
||||
tempfile = str(tmp_path / ("temp_" + chr(128) + ".ttf"))
|
||||
tempfile = tmp_path / ("temp_" + chr(128) + ".ttf")
|
||||
try:
|
||||
shutil.copy(FONT_PATH, tempfile)
|
||||
except UnicodeEncodeError:
|
||||
|
@ -254,7 +254,8 @@ def test_render_multiline_text(font: ImageFont.FreeTypeFont) -> None:
|
|||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"align, ext", (("left", ""), ("center", "_center"), ("right", "_right"))
|
||||
"align, ext",
|
||||
(("left", ""), ("center", "_center"), ("right", "_right"), ("justify", "_justify")),
|
||||
)
|
||||
def test_render_multiline_text_align(
|
||||
font: ImageFont.FreeTypeFont, align: str, ext: str
|
||||
|
@ -461,6 +462,20 @@ def test_free_type_font_get_mask(font: ImageFont.FreeTypeFont) -> None:
|
|||
assert mask.size == (108, 13)
|
||||
|
||||
|
||||
def test_stroke_mask() -> None:
|
||||
# Arrange
|
||||
text = "i"
|
||||
|
||||
# Act
|
||||
font = ImageFont.truetype(FONT_PATH, 128)
|
||||
mask = font.getmask(text, stroke_width=2)
|
||||
|
||||
# Assert
|
||||
assert mask.getpixel((34, 5)) == 255
|
||||
assert mask.getpixel((38, 5)) == 0
|
||||
assert mask.getpixel((42, 5)) == 255
|
||||
|
||||
|
||||
def test_load_when_image_not_found() -> None:
|
||||
with tempfile.NamedTemporaryFile(delete=False) as tmp:
|
||||
pass
|
||||
|
@ -543,7 +558,7 @@ def test_render_empty(font: ImageFont.FreeTypeFont) -> None:
|
|||
|
||||
def test_unicode_extended(layout_engine: ImageFont.Layout) -> None:
|
||||
# issue #3777
|
||||
text = "A\u278A\U0001F12B"
|
||||
text = "A\u278a\U0001f12b"
|
||||
target = "Tests/images/unicode_extended.png"
|
||||
|
||||
ttf = ImageFont.truetype(
|
||||
|
@ -1012,7 +1027,7 @@ def test_sbix(layout_engine: ImageFont.Layout) -> None:
|
|||
im = Image.new("RGB", (400, 400), "white")
|
||||
d = ImageDraw.Draw(im)
|
||||
|
||||
d.text((50, 50), "\uE901", font=font, embedded_color=True)
|
||||
d.text((50, 50), "\ue901", font=font, embedded_color=True)
|
||||
|
||||
assert_image_similar_tofile(im, "Tests/images/chromacheck-sbix.png", 1)
|
||||
except OSError as e: # pragma: no cover
|
||||
|
@ -1029,7 +1044,7 @@ def test_sbix_mask(layout_engine: ImageFont.Layout) -> None:
|
|||
im = Image.new("RGB", (400, 400), "white")
|
||||
d = ImageDraw.Draw(im)
|
||||
|
||||
d.text((50, 50), "\uE901", (100, 0, 0), font=font)
|
||||
d.text((50, 50), "\ue901", (100, 0, 0), font=font)
|
||||
|
||||
assert_image_similar_tofile(im, "Tests/images/chromacheck-sbix_mask.png", 1)
|
||||
except OSError as e: # pragma: no cover
|
||||
|
|
|
@ -229,7 +229,7 @@ def test_getlength(
|
|||
@pytest.mark.parametrize("direction", ("ltr", "ttb"))
|
||||
@pytest.mark.parametrize(
|
||||
"text",
|
||||
("i" + ("\u030C" * 15) + "i", "i" + "\u032C" * 15 + "i", "\u035Cii", "i\u0305i"),
|
||||
("i" + ("\u030c" * 15) + "i", "i" + "\u032c" * 15 + "i", "\u035cii", "i\u0305i"),
|
||||
ids=("caron-above", "caron-below", "double-breve", "overline"),
|
||||
)
|
||||
def test_getlength_combine(mode: str, direction: str, text: str) -> None:
|
||||
|
@ -272,27 +272,27 @@ def test_anchor_ttb(anchor: str) -> None:
|
|||
|
||||
combine_tests = (
|
||||
# extends above (e.g. issue #4553)
|
||||
("caron", "a\u030C\u030C\u030C\u030C\u030Cb", None, None, 0.08),
|
||||
("caron_la", "a\u030C\u030C\u030C\u030C\u030Cb", "la", None, 0.08),
|
||||
("caron_lt", "a\u030C\u030C\u030C\u030C\u030Cb", "lt", None, 0.08),
|
||||
("caron_ls", "a\u030C\u030C\u030C\u030C\u030Cb", "ls", None, 0.08),
|
||||
("caron_ttb", "ca" + ("\u030C" * 15) + "b", None, "ttb", 0.3),
|
||||
("caron_ttb_lt", "ca" + ("\u030C" * 15) + "b", "lt", "ttb", 0.3),
|
||||
("caron", "a\u030c\u030c\u030c\u030c\u030cb", None, None, 0.08),
|
||||
("caron_la", "a\u030c\u030c\u030c\u030c\u030cb", "la", None, 0.08),
|
||||
("caron_lt", "a\u030c\u030c\u030c\u030c\u030cb", "lt", None, 0.08),
|
||||
("caron_ls", "a\u030c\u030c\u030c\u030c\u030cb", "ls", None, 0.08),
|
||||
("caron_ttb", "ca" + ("\u030c" * 15) + "b", None, "ttb", 0.3),
|
||||
("caron_ttb_lt", "ca" + ("\u030c" * 15) + "b", "lt", "ttb", 0.3),
|
||||
# extends below
|
||||
("caron_below", "a\u032C\u032C\u032C\u032C\u032Cb", None, None, 0.02),
|
||||
("caron_below_ld", "a\u032C\u032C\u032C\u032C\u032Cb", "ld", None, 0.02),
|
||||
("caron_below_lb", "a\u032C\u032C\u032C\u032C\u032Cb", "lb", None, 0.02),
|
||||
("caron_below_ls", "a\u032C\u032C\u032C\u032C\u032Cb", "ls", None, 0.02),
|
||||
("caron_below_ttb", "a" + ("\u032C" * 15) + "b", None, "ttb", 0.03),
|
||||
("caron_below_ttb_lb", "a" + ("\u032C" * 15) + "b", "lb", "ttb", 0.03),
|
||||
("caron_below", "a\u032c\u032c\u032c\u032c\u032cb", None, None, 0.02),
|
||||
("caron_below_ld", "a\u032c\u032c\u032c\u032c\u032cb", "ld", None, 0.02),
|
||||
("caron_below_lb", "a\u032c\u032c\u032c\u032c\u032cb", "lb", None, 0.02),
|
||||
("caron_below_ls", "a\u032c\u032c\u032c\u032c\u032cb", "ls", None, 0.02),
|
||||
("caron_below_ttb", "a" + ("\u032c" * 15) + "b", None, "ttb", 0.03),
|
||||
("caron_below_ttb_lb", "a" + ("\u032c" * 15) + "b", "lb", "ttb", 0.03),
|
||||
# extends to the right (e.g. issue #3745)
|
||||
("double_breve_below", "a\u035Ci", None, None, 0.02),
|
||||
("double_breve_below_ma", "a\u035Ci", "ma", None, 0.02),
|
||||
("double_breve_below_ra", "a\u035Ci", "ra", None, 0.02),
|
||||
("double_breve_below_ttb", "a\u035Cb", None, "ttb", 0.02),
|
||||
("double_breve_below_ttb_rt", "a\u035Cb", "rt", "ttb", 0.02),
|
||||
("double_breve_below_ttb_mt", "a\u035Cb", "mt", "ttb", 0.02),
|
||||
("double_breve_below_ttb_st", "a\u035Cb", "st", "ttb", 0.02),
|
||||
("double_breve_below", "a\u035ci", None, None, 0.02),
|
||||
("double_breve_below_ma", "a\u035ci", "ma", None, 0.02),
|
||||
("double_breve_below_ra", "a\u035ci", "ra", None, 0.02),
|
||||
("double_breve_below_ttb", "a\u035cb", None, "ttb", 0.02),
|
||||
("double_breve_below_ttb_rt", "a\u035cb", "rt", "ttb", 0.02),
|
||||
("double_breve_below_ttb_mt", "a\u035cb", "mt", "ttb", 0.02),
|
||||
("double_breve_below_ttb_st", "a\u035cb", "st", "ttb", 0.02),
|
||||
# extends to the left (fail=0.064)
|
||||
("overline", "i\u0305", None, None, 0.02),
|
||||
("overline_la", "i\u0305", "la", None, 0.02),
|
||||
|
@ -346,7 +346,7 @@ def test_combine_multiline(anchor: str, align: str) -> None:
|
|||
|
||||
path = f"Tests/images/test_combine_multiline_{anchor}_{align}.png"
|
||||
f = ImageFont.truetype("Tests/fonts/NotoSans-Regular.ttf", 48)
|
||||
text = "i\u0305\u035C\ntext" # i with overline and double breve, and a word
|
||||
text = "i\u0305\u035c\ntext" # i with overline and double breve, and a word
|
||||
|
||||
im = Image.new("RGB", (400, 400), "white")
|
||||
d = ImageDraw.Draw(im)
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user