mirror of
https://github.com/python-pillow/Pillow.git
synced 2025-03-12 16:55:47 +03:00
Merge branch 'main' into progress
This commit is contained in:
commit
af7954bc6f
|
@ -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
|
||||
|
|
|
@ -1 +1 @@
|
|||
cibuildwheel==2.22.0
|
||||
cibuildwheel==2.23.0
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
mypy==1.14.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
|
||||
|
|
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++
|
||||
|
|
24
.github/workflows/test-docker.yml
vendored
24
.github/workflows/test-docker.yml
vendored
|
@ -29,21 +29,18 @@ 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
|
||||
alpine,
|
||||
amazon-2-amd64,
|
||||
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 +51,17 @@ jobs:
|
|||
]
|
||||
dockerTag: [main]
|
||||
include:
|
||||
- docker: "ubuntu-22.04-jammy-arm64v8"
|
||||
qemu-arch: "aarch64"
|
||||
- docker: "ubuntu-24.04-noble-ppc64le"
|
||||
os: "ubuntu-22.04"
|
||||
qemu-arch: "ppc64le"
|
||||
dockerTag: main
|
||||
- docker: "ubuntu-24.04-noble-s390x"
|
||||
os: "ubuntu-22.04"
|
||||
qemu-arch: "s390x"
|
||||
dockerTag: main
|
||||
- docker: "ubuntu-24.04-noble-arm64v8"
|
||||
os: "ubuntu-24.04-arm"
|
||||
dockerTag: main
|
||||
|
||||
name: ${{ matrix.docker }}
|
||||
|
||||
|
@ -89,15 +91,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 \
|
||||
|
|
14
.github/workflows/test-windows.yml
vendored
14
.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
|
||||
|
||||
|
|
2
.github/workflows/test.yml
vendored
2
.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",
|
||||
|
|
42
.github/workflows/wheels-dependencies.sh
vendored
42
.github/workflows/wheels-dependencies.sh
vendored
|
@ -37,20 +37,15 @@ 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=10.4.0
|
||||
LIBPNG_VERSION=1.6.47
|
||||
JPEGTURBO_VERSION=3.1.0
|
||||
OPENJPEG_VERSION=2.5.3
|
||||
XZ_VERSION=5.6.3
|
||||
XZ_VERSION=5.6.4
|
||||
TIFF_VERSION=4.6.0
|
||||
LCMS2_VERSION=2.16
|
||||
if [[ -n "$IS_MACOS" ]]; then
|
||||
GIFLIB_VERSION=5.2.2
|
||||
else
|
||||
GIFLIB_VERSION=5.2.1
|
||||
fi
|
||||
ZLIB_NG_VERSION=2.2.2
|
||||
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
|
||||
|
@ -59,13 +54,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 +69,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 +103,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 +135,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.:
|
||||
|
|
|
@ -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,7 +9,6 @@ import os
|
|||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import sysconfig
|
||||
import tempfile
|
||||
from collections.abc import Sequence
|
||||
from functools import lru_cache
|
||||
|
@ -140,18 +139,11 @@ def assert_image_similar_tofile(
|
|||
filename: str,
|
||||
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 +319,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 +341,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/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,
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -308,13 +308,8 @@ def test_apng_syntax_errors() -> None:
|
|||
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
|
||||
im.seek(im.n_frames - 1)
|
||||
|
||||
with pytest.raises(OSError):
|
||||
with Image.open("Tests/images/apng/syntax_num_frames_high.png") as im:
|
||||
|
@ -406,13 +401,8 @@ 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
|
||||
im.seek(im.n_frames - 1)
|
||||
im.load()
|
||||
|
||||
|
||||
def test_apng_save_duration_loop(tmp_path: Path) -> None:
|
||||
|
|
|
@ -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,6 +38,13 @@ 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")
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -331,11 +331,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
|
||||
|
|
|
@ -95,10 +95,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:
|
||||
|
|
|
@ -35,32 +35,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:
|
||||
|
|
|
@ -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:
|
||||
|
@ -357,8 +355,9 @@ def test_save_all_progress():
|
|||
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
|
||||
|
@ -366,10 +365,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:
|
||||
|
@ -535,8 +534,7 @@ 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:
|
||||
|
@ -576,6 +574,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
|
||||
|
||||
|
||||
|
@ -603,17 +602,15 @@ 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:
|
||||
|
@ -652,7 +649,7 @@ def test_save_dispose(tmp_path: Path) -> None:
|
|||
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:
|
||||
for _ in range(2):
|
||||
|
@ -812,6 +809,21 @@ 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 = str(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")
|
||||
with Image.open("Tests/images/different_transparency.gif") as im:
|
||||
|
@ -1361,6 +1373,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)
|
||||
|
@ -1395,32 +1408,30 @@ def test_save_I(tmp_path: Path) -> None:
|
|||
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:
|
||||
|
@ -1446,24 +1457,23 @@ 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 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:
|
||||
|
|
|
@ -32,10 +32,14 @@ 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:
|
||||
|
|
|
@ -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:
|
||||
|
@ -243,27 +245,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"
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -181,6 +181,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"
|
||||
)
|
||||
|
@ -277,7 +281,10 @@ 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:
|
||||
|
@ -349,7 +356,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 +426,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")
|
||||
|
@ -520,12 +530,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:
|
||||
|
@ -923,7 +934,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
|
||||
|
||||
|
@ -1014,7 +1025,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):
|
||||
|
@ -1027,11 +1038,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()
|
||||
|
|
|
@ -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)
|
||||
|
@ -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(
|
||||
|
@ -315,6 +313,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 +335,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 +434,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 +442,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 +504,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,11 +36,7 @@ class LibTiffTestCase:
|
|||
im.load()
|
||||
im.getdata()
|
||||
|
||||
try:
|
||||
assert im._compression == "group4"
|
||||
except AttributeError:
|
||||
print("No _compression")
|
||||
print(dir(im))
|
||||
assert im._compression == "group4"
|
||||
|
||||
# can we write it back out, in a different form.
|
||||
out = str(tmp_path / "temp.png")
|
||||
|
@ -313,7 +309,7 @@ class TestFileLibTiff(LibTiffTestCase):
|
|||
}
|
||||
|
||||
def check_tags(
|
||||
tiffinfo: TiffImagePlugin.ImageFileDirectory_v2 | dict[int, str]
|
||||
tiffinfo: TiffImagePlugin.ImageFileDirectory_v2 | dict[int, str],
|
||||
) -> None:
|
||||
im = hopper()
|
||||
|
||||
|
@ -1107,13 +1103,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,11 +1140,9 @@ 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:
|
||||
|
@ -1160,23 +1156,22 @@ 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")
|
||||
|
||||
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:
|
||||
|
|
|
@ -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:
|
||||
|
@ -77,8 +82,8 @@ def test_app(test_file: str) -> None:
|
|||
with Image.open(test_file) as im:
|
||||
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
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import io
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
@ -36,6 +37,28 @@ def test_sanity(tmp_path: Path) -> None:
|
|||
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"
|
||||
|
||||
|
|
|
@ -306,7 +306,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()
|
||||
|
@ -334,7 +334,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)
|
||||
|
|
|
@ -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
|
||||
|
@ -600,7 +596,7 @@ 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 "comment" in im.text
|
||||
for k, v in {
|
||||
|
@ -614,18 +610,17 @@ class TestFilePng:
|
|||
with pytest.raises(OSError):
|
||||
assert isinstance(im.text, dict)
|
||||
|
||||
# Raises an EOFError in load_end
|
||||
with Image.open("Tests/images/hopper_idat_after_image_end.png") as im:
|
||||
assert im.text == {"TXT": "VALUE", "ZIP": "VALUE"}
|
||||
|
||||
# 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
|
||||
im.text
|
||||
monkeypatch.setattr(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 im.text == {"TXT": "VALUE", "ZIP": "VALUE"}
|
||||
|
||||
def test_unknown_compression_method(self) -> None:
|
||||
with pytest.raises(SyntaxError, match="Unknown compression method"):
|
||||
|
@ -651,15 +646,16 @@ 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:
|
||||
|
@ -772,38 +768,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 +801,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 +813,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
|
||||
|
||||
|
||||
|
@ -292,12 +293,10 @@ def test_header_token_too_long(tmp_path: Path) -> None:
|
|||
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
|
||||
|
@ -305,12 +304,10 @@ def test_truncated_file(tmp_path: Path) -> None:
|
|||
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:
|
||||
|
@ -334,12 +331,12 @@ def test_invalid_maxval(maxval: bytes, tmp_path: Path) -> None:
|
|||
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
|
||||
|
@ -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:
|
||||
|
|
|
@ -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:
|
||||
|
@ -153,8 +153,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):
|
||||
|
|
|
@ -72,6 +72,7 @@ def test_palette_depth_8(tmp_path: Path) -> None:
|
|||
|
||||
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")
|
||||
|
||||
|
@ -213,10 +214,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:
|
||||
|
|
|
@ -63,12 +63,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():
|
||||
|
@ -115,15 +115,27 @@ class TestFileTiff:
|
|||
outfile = str(tmp_path / "temp.tif")
|
||||
im.save(outfile, save_all=True, append_images=[im], tiffinfo=im.tag_v2)
|
||||
|
||||
def test_bigtiff_save(self, tmp_path: Path) -> None:
|
||||
outfile = str(tmp_path / "temp.tif")
|
||||
im = hopper()
|
||||
im.save(outfile, big_tiff=True)
|
||||
|
||||
with Image.open(outfile) as reloaded:
|
||||
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 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"
|
||||
|
@ -648,6 +660,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:
|
||||
|
@ -797,6 +821,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
|
||||
|
@ -946,11 +1003,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",
|
||||
|
|
|
@ -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:
|
||||
|
@ -208,9 +205,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:
|
||||
|
@ -285,7 +281,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:
|
||||
|
|
|
@ -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)"
|
||||
|
|
|
@ -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:
|
||||
|
@ -71,7 +73,7 @@ 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
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
@ -189,8 +188,6 @@ class TestImage:
|
|||
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))
|
||||
|
||||
def test_fp_name(self, tmp_path: Path) -> None:
|
||||
|
@ -580,9 +577,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 +657,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))
|
||||
|
@ -770,7 +766,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 +789,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"
|
||||
)
|
||||
|
@ -987,6 +987,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")
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -176,9 +176,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:
|
||||
|
@ -191,13 +190,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:
|
||||
|
@ -206,13 +202,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):
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -80,15 +80,12 @@ def test_lut(op: str) -> None:
|
|||
def test_no_operator_loaded() -> None:
|
||||
im = Image.new("L", (1, 1))
|
||||
mop = ImageMorph.MorphOp()
|
||||
with pytest.raises(Exception) as e:
|
||||
with pytest.raises(Exception, match="No operator loaded"):
|
||||
mop.apply(im)
|
||||
assert str(e.value) == "No operator loaded"
|
||||
with pytest.raises(Exception) as e:
|
||||
with pytest.raises(Exception, match="No operator loaded"):
|
||||
mop.match(im)
|
||||
assert str(e.value) == "No operator loaded"
|
||||
with pytest.raises(Exception) as e:
|
||||
with pytest.raises(Exception, match="No operator loaded"):
|
||||
mop.save_lut("")
|
||||
assert str(e.value) == "No operator loaded"
|
||||
|
||||
|
||||
# Test the named patterns
|
||||
|
@ -238,15 +235,12 @@ def test_incorrect_mode() -> None:
|
|||
im = hopper("RGB")
|
||||
mop = ImageMorph.MorphOp(op_name="erosion8")
|
||||
|
||||
with pytest.raises(ValueError) as e:
|
||||
with pytest.raises(ValueError, match="Image mode must be L"):
|
||||
mop.apply(im)
|
||||
assert str(e.value) == "Image mode must be L"
|
||||
with pytest.raises(ValueError) as e:
|
||||
with pytest.raises(ValueError, match="Image mode must be L"):
|
||||
mop.match(im)
|
||||
assert str(e.value) == "Image mode must be L"
|
||||
with pytest.raises(ValueError) as e:
|
||||
with pytest.raises(ValueError, match="Image mode must be L"):
|
||||
mop.get_on_pixels(im)
|
||||
assert str(e.value) == "Image mode must be L"
|
||||
|
||||
|
||||
def test_add_patterns() -> None:
|
||||
|
@ -279,9 +273,10 @@ def test_pattern_syntax_error() -> None:
|
|||
lb.add_patterns(new_patterns)
|
||||
|
||||
# Act / Assert
|
||||
with pytest.raises(Exception) as e:
|
||||
with pytest.raises(
|
||||
Exception, match='Syntax error in pattern "a pattern with a syntax error"'
|
||||
):
|
||||
lb.build_lut()
|
||||
assert str(e.value) == 'Syntax error in pattern "a pattern with a syntax error"'
|
||||
|
||||
|
||||
def test_load_invalid_mrl() -> None:
|
||||
|
@ -290,9 +285,8 @@ def test_load_invalid_mrl() -> None:
|
|||
mop = ImageMorph.MorphOp()
|
||||
|
||||
# Act / Assert
|
||||
with pytest.raises(Exception) as e:
|
||||
with pytest.raises(Exception, match="Wrong size operator file!"):
|
||||
mop.load_lut(invalid_mrl)
|
||||
assert str(e.value) == "Wrong size operator file!"
|
||||
|
||||
|
||||
def test_roundtrip_mrl(tmp_path: Path) -> None:
|
||||
|
|
|
@ -165,14 +165,10 @@ def test_pad() -> None:
|
|||
def test_pad_round() -> None:
|
||||
im = Image.new("1", (1, 1), 1)
|
||||
new_im = ImageOps.pad(im, (4, 1))
|
||||
px = new_im.load()
|
||||
assert px is not None
|
||||
assert px[2, 0] == 1
|
||||
assert new_im.getpixel((2, 0)) == 1
|
||||
|
||||
new_im = ImageOps.pad(im, (1, 4))
|
||||
px = new_im.load()
|
||||
assert px is not None
|
||||
assert px[0, 2] == 1
|
||||
assert new_im.getpixel((0, 2)) == 1
|
||||
|
||||
|
||||
@pytest.mark.parametrize("mode", ("P", "PA"))
|
||||
|
@ -405,7 +401,6 @@ def test_exif_transpose() -> None:
|
|||
else:
|
||||
original_exif = im.info["exif"]
|
||||
transposed_im = ImageOps.exif_transpose(im)
|
||||
assert transposed_im is not None
|
||||
assert_image_similar(base_im, transposed_im, 17)
|
||||
if orientation_im is base_im:
|
||||
assert "exif" not in im.info
|
||||
|
@ -417,7 +412,6 @@ def test_exif_transpose() -> None:
|
|||
|
||||
# Repeat the operation to test that it does not keep transposing
|
||||
transposed_im2 = ImageOps.exif_transpose(transposed_im)
|
||||
assert transposed_im2 is not None
|
||||
assert_image_equal(transposed_im2, transposed_im)
|
||||
|
||||
check(base_im)
|
||||
|
@ -433,7 +427,6 @@ def test_exif_transpose() -> None:
|
|||
assert im.getexif()[0x0112] == 3
|
||||
|
||||
transposed_im = ImageOps.exif_transpose(im)
|
||||
assert transposed_im is not None
|
||||
assert 0x0112 not in transposed_im.getexif()
|
||||
|
||||
transposed_im._reload_exif()
|
||||
|
@ -446,17 +439,24 @@ def test_exif_transpose() -> None:
|
|||
assert im.getexif()[0x0112] == 3
|
||||
|
||||
transposed_im = ImageOps.exif_transpose(im)
|
||||
assert transposed_im is not None
|
||||
assert 0x0112 not in transposed_im.getexif()
|
||||
|
||||
# Orientation set directly on Image.Exif
|
||||
im = hopper()
|
||||
im.getexif()[0x0112] = 3
|
||||
transposed_im = ImageOps.exif_transpose(im)
|
||||
assert transposed_im is not None
|
||||
assert 0x0112 not in transposed_im.getexif()
|
||||
|
||||
|
||||
def test_exif_transpose_with_xmp_tuple() -> None:
|
||||
with Image.open("Tests/images/xmp_tags_orientation.png") as im:
|
||||
assert im.getexif()[0x0112] == 3
|
||||
|
||||
im.info["xmp"] = (b"test",)
|
||||
transposed_im = ImageOps.exif_transpose(im)
|
||||
assert 0x0112 not in transposed_im.getexif()
|
||||
|
||||
|
||||
def test_exif_transpose_xml_without_xmp() -> None:
|
||||
with Image.open("Tests/images/xmp_tags_orientation.png") as im:
|
||||
assert im.getexif()[0x0112] == 3
|
||||
|
@ -464,7 +464,6 @@ def test_exif_transpose_xml_without_xmp() -> None:
|
|||
|
||||
del im.info["xmp"]
|
||||
transposed_im = ImageOps.exif_transpose(im)
|
||||
assert transposed_im is not None
|
||||
assert 0x0112 not in transposed_im.getexif()
|
||||
|
||||
|
||||
|
|
|
@ -17,6 +17,7 @@ def test_sanity() -> None:
|
|||
def test_reload() -> None:
|
||||
with Image.open("Tests/images/hopper.gif") as im:
|
||||
original = im.copy()
|
||||
assert im.palette is not None
|
||||
im.palette.dirty = 1
|
||||
assert_image_equal(im.convert("RGB"), original.convert("RGB"))
|
||||
|
||||
|
@ -111,7 +112,7 @@ def test_make_linear_lut() -> None:
|
|||
assert isinstance(lut, list)
|
||||
assert len(lut) == 256
|
||||
# Check values
|
||||
for i in range(0, len(lut)):
|
||||
for i in range(len(lut)):
|
||||
assert lut[i] == i
|
||||
|
||||
|
||||
|
@ -189,7 +190,7 @@ def test_2bit_palette(tmp_path: Path) -> None:
|
|||
|
||||
rgb = b"\x00" * 2 + b"\x01" * 2 + b"\x02" * 2
|
||||
img = Image.frombytes("P", (6, 1), rgb)
|
||||
img.putpalette(b"\xFF\x00\x00\x00\xFF\x00\x00\x00\xFF") # RGB
|
||||
img.putpalette(b"\xff\x00\x00\x00\xff\x00\x00\x00\xff") # RGB
|
||||
img.save(outfile, format="PNG")
|
||||
|
||||
assert_image_equal_tofile(img, outfile)
|
||||
|
|
|
@ -68,25 +68,10 @@ def test_path_constructors(
|
|||
assert list(p) == [(0.0, 1.0)]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"coords",
|
||||
(
|
||||
("a", "b"),
|
||||
([0, 1],),
|
||||
[[0, 1]],
|
||||
([0.0, 1.0],),
|
||||
[[0.0, 1.0]],
|
||||
),
|
||||
)
|
||||
def test_invalid_path_constructors(
|
||||
coords: tuple[str, str] | Sequence[Sequence[int]]
|
||||
) -> None:
|
||||
# Act
|
||||
with pytest.raises(ValueError) as e:
|
||||
ImagePath.Path(coords)
|
||||
|
||||
# Assert
|
||||
assert str(e.value) == "incorrect coordinate type"
|
||||
def test_invalid_path_constructors() -> None:
|
||||
# Arrange / Act
|
||||
with pytest.raises(ValueError, match="incorrect coordinate type"):
|
||||
ImagePath.Path(("a", "b"))
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
|
@ -99,13 +84,9 @@ def test_invalid_path_constructors(
|
|||
),
|
||||
)
|
||||
def test_path_odd_number_of_coordinates(coords: Sequence[int]) -> None:
|
||||
# Act
|
||||
with pytest.raises(ValueError) as e:
|
||||
with pytest.raises(ValueError, match="wrong number of coordinates"):
|
||||
ImagePath.Path(coords)
|
||||
|
||||
# Assert
|
||||
assert str(e.value) == "wrong number of coordinates"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"coords, expected",
|
||||
|
|
|
@ -32,7 +32,7 @@ def test_sanity(tmp_path: Path) -> None:
|
|||
def test_iterator() -> None:
|
||||
with Image.open("Tests/images/multipage.tiff") as im:
|
||||
i = ImageSequence.Iterator(im)
|
||||
for index in range(0, im.n_frames):
|
||||
for index in range(im.n_frames):
|
||||
assert i[index] == next(i)
|
||||
with pytest.raises(IndexError):
|
||||
i[index + 1]
|
||||
|
|
|
@ -7,36 +7,30 @@ import pytest
|
|||
from PIL import Image
|
||||
|
||||
|
||||
def test_overflow() -> None:
|
||||
def test_overflow(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
# There is the potential to overflow comparisons in map.c
|
||||
# if there are > SIZE_MAX bytes in the image or if
|
||||
# the file encodes an offset that makes
|
||||
# (offset + size(bytes)) > SIZE_MAX
|
||||
|
||||
# Note that this image triggers the decompression bomb warning:
|
||||
max_pixels = Image.MAX_IMAGE_PIXELS
|
||||
Image.MAX_IMAGE_PIXELS = None
|
||||
monkeypatch.setattr(Image, "MAX_IMAGE_PIXELS", None)
|
||||
|
||||
# This image hits the offset test.
|
||||
with Image.open("Tests/images/l2rgb_read.bmp") as im:
|
||||
with pytest.raises((ValueError, MemoryError, OSError)):
|
||||
im.load()
|
||||
|
||||
Image.MAX_IMAGE_PIXELS = max_pixels
|
||||
|
||||
|
||||
def test_tobytes() -> None:
|
||||
def test_tobytes(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
# Note that this image triggers the decompression bomb warning:
|
||||
max_pixels = Image.MAX_IMAGE_PIXELS
|
||||
Image.MAX_IMAGE_PIXELS = None
|
||||
monkeypatch.setattr(Image, "MAX_IMAGE_PIXELS", None)
|
||||
|
||||
# Previously raised an access violation on Windows
|
||||
with Image.open("Tests/images/l2rgb_read.bmp") as im:
|
||||
with pytest.raises((ValueError, MemoryError, OSError)):
|
||||
im.tobytes()
|
||||
|
||||
Image.MAX_IMAGE_PIXELS = max_pixels
|
||||
|
||||
|
||||
@pytest.mark.skipif(sys.maxsize <= 2**32, reason="Requires 64-bit system")
|
||||
def test_ysize() -> None:
|
||||
|
|
|
@ -141,9 +141,7 @@ def test_save_tiff_uint16() -> None:
|
|||
a.shape = TEST_IMAGE_SIZE
|
||||
img = Image.fromarray(a)
|
||||
|
||||
img_px = img.load()
|
||||
assert img_px is not None
|
||||
assert img_px[0, 0] == pixel_value
|
||||
assert img.getpixel((0, 0)) == pixel_value
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
|
|
|
@ -20,10 +20,10 @@ from PIL.PdfParser import (
|
|||
|
||||
|
||||
def test_text_encode_decode() -> None:
|
||||
assert encode_text("abc") == b"\xFE\xFF\x00a\x00b\x00c"
|
||||
assert decode_text(b"\xFE\xFF\x00a\x00b\x00c") == "abc"
|
||||
assert encode_text("abc") == b"\xfe\xff\x00a\x00b\x00c"
|
||||
assert decode_text(b"\xfe\xff\x00a\x00b\x00c") == "abc"
|
||||
assert decode_text(b"abc") == "abc"
|
||||
assert decode_text(b"\x1B a \x1C") == "\u02D9 a \u02DD"
|
||||
assert decode_text(b"\x1b a \x1c") == "\u02d9 a \u02dd"
|
||||
|
||||
|
||||
def test_indirect_refs() -> None:
|
||||
|
@ -45,8 +45,8 @@ def test_parsing() -> None:
|
|||
assert PdfParser.get_value(b"false%", 0) == (False, 5)
|
||||
assert PdfParser.get_value(b"null<", 0) == (None, 4)
|
||||
assert PdfParser.get_value(b"%cmt\n %cmt\n 123\n", 0) == (123, 15)
|
||||
assert PdfParser.get_value(b"<901FA3>", 0) == (b"\x90\x1F\xA3", 8)
|
||||
assert PdfParser.get_value(b"asd < 9 0 1 f A > qwe", 3) == (b"\x90\x1F\xA0", 17)
|
||||
assert PdfParser.get_value(b"<901FA3>", 0) == (b"\x90\x1f\xa3", 8)
|
||||
assert PdfParser.get_value(b"asd < 9 0 1 f A > qwe", 3) == (b"\x90\x1f\xa0", 17)
|
||||
assert PdfParser.get_value(b"(asd)", 0) == (b"asd", 5)
|
||||
assert PdfParser.get_value(b"(asd(qwe)zxc)zzz(aaa)", 0) == (b"asd(qwe)zxc", 13)
|
||||
assert PdfParser.get_value(b"(Two \\\nwords.)", 0) == (b"Two words.", 14)
|
||||
|
@ -56,9 +56,9 @@ def test_parsing() -> None:
|
|||
assert PdfParser.get_value(b"(One\\(paren).", 0) == (b"One(paren", 12)
|
||||
assert PdfParser.get_value(b"(One\\)paren).", 0) == (b"One)paren", 12)
|
||||
assert PdfParser.get_value(b"(\\0053)", 0) == (b"\x053", 7)
|
||||
assert PdfParser.get_value(b"(\\053)", 0) == (b"\x2B", 6)
|
||||
assert PdfParser.get_value(b"(\\53)", 0) == (b"\x2B", 5)
|
||||
assert PdfParser.get_value(b"(\\53a)", 0) == (b"\x2Ba", 6)
|
||||
assert PdfParser.get_value(b"(\\053)", 0) == (b"\x2b", 6)
|
||||
assert PdfParser.get_value(b"(\\53)", 0) == (b"\x2b", 5)
|
||||
assert PdfParser.get_value(b"(\\53a)", 0) == (b"\x2ba", 6)
|
||||
assert PdfParser.get_value(b"(\\1111)", 0) == (b"\x491", 7)
|
||||
assert PdfParser.get_value(b" 123 (", 0) == (123, 4)
|
||||
assert round(abs(PdfParser.get_value(b" 123.4 %", 0)[0] - 123.4), 7) == 0
|
||||
|
@ -118,7 +118,7 @@ def test_pdf_repr() -> None:
|
|||
assert pdf_repr(None) == b"null"
|
||||
assert pdf_repr(b"a)/b\\(c") == rb"(a\)/b\\\(c)"
|
||||
assert pdf_repr([123, True, {"a": PdfName(b"b")}]) == b"[ 123 true <<\n/a /b\n>> ]"
|
||||
assert pdf_repr(PdfBinary(b"\x90\x1F\xA0")) == b"<901FA0>"
|
||||
assert pdf_repr(PdfBinary(b"\x90\x1f\xa0")) == b"<901FA0>"
|
||||
|
||||
|
||||
def test_duplicate_xref_entry() -> None:
|
||||
|
|
|
@ -65,7 +65,7 @@ def helper_pickle_string(protocol: int, test_file: str, mode: str | None) -> Non
|
|||
("Tests/images/itxt_chunks.png", None),
|
||||
],
|
||||
)
|
||||
@pytest.mark.parametrize("protocol", range(0, pickle.HIGHEST_PROTOCOL + 1))
|
||||
@pytest.mark.parametrize("protocol", range(pickle.HIGHEST_PROTOCOL + 1))
|
||||
def test_pickle_image(
|
||||
tmp_path: Path, test_file: str, test_mode: str | None, protocol: int
|
||||
) -> None:
|
||||
|
@ -92,7 +92,7 @@ def test_pickle_la_mode_with_palette(tmp_path: Path) -> None:
|
|||
im = im.convert("PA")
|
||||
|
||||
# Act / Assert
|
||||
for protocol in range(0, pickle.HIGHEST_PROTOCOL + 1):
|
||||
for protocol in range(pickle.HIGHEST_PROTOCOL + 1):
|
||||
im._mode = "LA"
|
||||
with open(filename, "wb") as f:
|
||||
pickle.dump(im, f, protocol)
|
||||
|
@ -133,7 +133,7 @@ def helper_assert_pickled_font_images(
|
|||
|
||||
|
||||
@skip_unless_feature("freetype2")
|
||||
@pytest.mark.parametrize("protocol", list(range(0, pickle.HIGHEST_PROTOCOL + 1)))
|
||||
@pytest.mark.parametrize("protocol", list(range(pickle.HIGHEST_PROTOCOL + 1)))
|
||||
def test_pickle_font_string(protocol: int) -> None:
|
||||
# Arrange
|
||||
font = ImageFont.truetype(FONT_PATH, FONT_SIZE)
|
||||
|
@ -147,7 +147,7 @@ def test_pickle_font_string(protocol: int) -> None:
|
|||
|
||||
|
||||
@skip_unless_feature("freetype2")
|
||||
@pytest.mark.parametrize("protocol", list(range(0, pickle.HIGHEST_PROTOCOL + 1)))
|
||||
@pytest.mark.parametrize("protocol", list(range(pickle.HIGHEST_PROTOCOL + 1)))
|
||||
def test_pickle_font_file(tmp_path: Path, protocol: int) -> None:
|
||||
# Arrange
|
||||
font = ImageFont.truetype(FONT_PATH, FONT_SIZE)
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
# install libimagequant
|
||||
|
||||
archive_name=libimagequant
|
||||
archive_version=4.3.3
|
||||
archive_version=4.3.4
|
||||
|
||||
archive=$archive_name-$archive_version
|
||||
|
||||
|
|
|
@ -6,12 +6,11 @@ Goals
|
|||
|
||||
The fork author's goal is to foster and support active development of PIL through:
|
||||
|
||||
- Continuous integration testing via `GitHub Actions`_ and `AppVeyor`_
|
||||
- Continuous integration testing via `GitHub Actions`_
|
||||
- Publicized development activity on `GitHub`_
|
||||
- Regular releases to the `Python Package Index`_
|
||||
|
||||
.. _GitHub Actions: https://github.com/python-pillow/Pillow/actions
|
||||
.. _AppVeyor: https://ci.appveyor.com/project/Python-pillow/pillow
|
||||
.. _GitHub: https://github.com/python-pillow/Pillow
|
||||
.. _Python Package Index: https://pypi.org/project/pillow/
|
||||
|
||||
|
|
|
@ -22,7 +22,7 @@ import PIL
|
|||
# -- General configuration ------------------------------------------------
|
||||
|
||||
# If your documentation needs a minimal Sphinx version, state it here.
|
||||
needs_sphinx = "8.1"
|
||||
needs_sphinx = "8.2"
|
||||
|
||||
# Add any Sphinx extension module names here, as strings. They can be
|
||||
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
|
||||
|
@ -121,7 +121,7 @@ nitpicky = True
|
|||
# generating warnings in “nitpicky mode”. Note that type should include the domain name
|
||||
# if present. Example entries would be ('py:func', 'int') or
|
||||
# ('envvar', 'LD_LIBRARY_PATH').
|
||||
nitpick_ignore = [("py:class", "_io.BytesIO"), ("py:class", "_CmsProfileCompatible")]
|
||||
nitpick_ignore = [("py:class", "_CmsProfileCompatible")]
|
||||
|
||||
|
||||
# -- Options for HTML output ----------------------------------------------
|
||||
|
|
|
@ -175,6 +175,24 @@ deprecated and will be removed in Pillow 12 (2025-10-15). They were used for obt
|
|||
raw pointers to ``ImagingCore`` internals. To interact with C code, you can use
|
||||
``Image.Image.getim()``, which returns a ``Capsule`` object.
|
||||
|
||||
ExifTags.IFD.Makernote
|
||||
^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
.. deprecated:: 11.1.0
|
||||
|
||||
``ExifTags.IFD.Makernote`` has been deprecated. Instead, use
|
||||
``ExifTags.IFD.MakerNote``.
|
||||
|
||||
Image.Image.get_child_images()
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
.. deprecated:: 11.2.0
|
||||
|
||||
``Image.Image.get_child_images()`` has been deprecated. and will be removed in Pillow
|
||||
13 (2026-10-15). It will be moved to ``ImageFile.ImageFile.get_child_images()``. The
|
||||
method uses an image's file pointer, and so child images could only be retrieved from
|
||||
an :py:class:`PIL.ImageFile.ImageFile` instance.
|
||||
|
||||
Removed features
|
||||
----------------
|
||||
|
||||
|
|
|
@ -285,7 +285,7 @@ Image.register_decoder("DXT5", DXT5Decoder)
|
|||
|
||||
|
||||
def _accept(prefix: bytes) -> bool:
|
||||
return prefix[:4] == b"DDS "
|
||||
return prefix.startswith(b"DDS ")
|
||||
|
||||
|
||||
Image.register_open(DdsImageFile.format, DdsImageFile, _accept)
|
||||
|
|
|
@ -454,7 +454,8 @@ The :py:meth:`~PIL.Image.open` method may set the following
|
|||
Raw EXIF data from the image.
|
||||
|
||||
**comment**
|
||||
A comment about the image.
|
||||
A comment about the image, from the COM marker. This is separate from the
|
||||
UserComment tag that may be stored in the EXIF data.
|
||||
|
||||
.. versionadded:: 7.1.0
|
||||
|
||||
|
@ -572,10 +573,19 @@ JPEG 2000
|
|||
Pillow reads and writes JPEG 2000 files containing ``L``, ``LA``, ``RGB``,
|
||||
``RGBA``, or ``YCbCr`` data. When reading, ``YCbCr`` data is converted to
|
||||
``RGB`` or ``RGBA`` depending on whether or not there is an alpha channel.
|
||||
Beginning with version 8.3.0, Pillow can read (but not write) ``RGB``,
|
||||
``RGBA``, and ``YCbCr`` images with subsampled components. Pillow supports
|
||||
JPEG 2000 raw codestreams (``.j2k`` files), as well as boxed JPEG 2000 files
|
||||
(``.jp2`` or ``.jpx`` files).
|
||||
|
||||
.. versionadded:: 8.3.0
|
||||
Pillow can read (but not write) ``RGB``, ``RGBA``, and ``YCbCr`` images with
|
||||
subsampled components.
|
||||
|
||||
.. versionadded:: 10.4.0
|
||||
Pillow can read ``CMYK`` images with OpenJPEG 2.5.1 and later.
|
||||
|
||||
.. versionadded:: 11.1.0
|
||||
Pillow can write ``CMYK`` images with OpenJPEG 2.5.3 and later.
|
||||
|
||||
Pillow supports JPEG 2000 raw codestreams (``.j2k`` files), as well as boxed
|
||||
JPEG 2000 files (``.jp2`` or ``.jpx`` files).
|
||||
|
||||
When loading, if you set the ``mode`` on the image prior to the
|
||||
:py:meth:`~PIL.Image.Image.load` method being invoked, you can ask Pillow to
|
||||
|
@ -1153,9 +1163,7 @@ The :py:meth:`~PIL.Image.Image.save` method can take the following keyword argum
|
|||
|
||||
**append_images**
|
||||
A list of images to append as additional frames. Each of the
|
||||
images in the list can be single or multiframe images. Note however, that for
|
||||
correct results, all the appended images should have the same
|
||||
``encoderinfo`` and ``encoderconfig`` properties.
|
||||
images in the list can be single or multiframe images.
|
||||
|
||||
.. versionadded:: 4.2.0
|
||||
|
||||
|
@ -1199,6 +1207,11 @@ The :py:meth:`~PIL.Image.Image.save` method can take the following keyword argum
|
|||
|
||||
.. versionadded:: 8.4.0
|
||||
|
||||
**big_tiff**
|
||||
If true, the image will be saved as a BigTIFF.
|
||||
|
||||
.. versionadded:: 11.1.0
|
||||
|
||||
**compression**
|
||||
A string containing the desired compression method for the
|
||||
file. (valid only with libtiff installed) Valid compression
|
||||
|
|
|
@ -54,7 +54,7 @@ true color.
|
|||
|
||||
|
||||
def _accept(prefix: bytes) -> bool:
|
||||
return prefix[:4] == b"SPAM"
|
||||
return prefix.startswith(b"SPAM")
|
||||
|
||||
|
||||
class SpamImageFile(ImageFile.ImageFile):
|
||||
|
|
|
@ -33,10 +33,6 @@ Pillow for enterprise is available via the Tidelift Subscription. `Learn more <h
|
|||
:target: https://github.com/python-pillow/Pillow/actions/workflows/test-cygwin.yml
|
||||
:alt: GitHub Actions build status (Test Cygwin)
|
||||
|
||||
.. image:: https://img.shields.io/appveyor/build/python-pillow/Pillow/main.svg?label=Windows%20build
|
||||
:target: https://ci.appveyor.com/project/python-pillow/Pillow
|
||||
:alt: AppVeyor CI build status (Windows)
|
||||
|
||||
.. image:: https://github.com/python-pillow/Pillow/workflows/Wheels/badge.svg
|
||||
:target: https://github.com/python-pillow/Pillow/actions/workflows/wheels.yml
|
||||
:alt: GitHub Actions build status (Wheels)
|
||||
|
|
|
@ -51,7 +51,7 @@ Many of Pillow's features require external libraries:
|
|||
* **littlecms** provides color management
|
||||
|
||||
* Pillow version 2.2.1 and below uses liblcms1, Pillow 2.3.0 and
|
||||
above uses liblcms2. Tested with **1.19** and **2.7-2.16**.
|
||||
above uses liblcms2. Tested with **1.19** and **2.7-2.17**.
|
||||
|
||||
* **libwebp** provides the WebP format.
|
||||
|
||||
|
@ -64,7 +64,7 @@ Many of Pillow's features require external libraries:
|
|||
|
||||
* **libimagequant** provides improved color quantization
|
||||
|
||||
* Pillow has been tested with libimagequant **2.6-4.3.3**
|
||||
* Pillow has been tested with libimagequant **2.6-4.3.4**
|
||||
* Libimagequant is licensed GPLv3, which is more restrictive than
|
||||
the Pillow license, therefore we will not be distributing binaries
|
||||
with libimagequant support enabled.
|
||||
|
|
|
@ -27,6 +27,8 @@ These platforms are built and tested for every change.
|
|||
+----------------------------------+----------------------------+---------------------+
|
||||
| CentOS Stream 9 | 3.9 | x86-64 |
|
||||
+----------------------------------+----------------------------+---------------------+
|
||||
| CentOS Stream 10 | 3.12 | x86-64 |
|
||||
+----------------------------------+----------------------------+---------------------+
|
||||
| Debian 12 Bookworm | 3.11 | x86, x86-64 |
|
||||
+----------------------------------+----------------------------+---------------------+
|
||||
| Fedora 40 | 3.12 | x86-64 |
|
||||
|
@ -42,18 +44,14 @@ These platforms are built and tested for every change.
|
|||
+----------------------------------+----------------------------+---------------------+
|
||||
| Ubuntu Linux 22.04 LTS (Jammy) | 3.9, 3.10, 3.11, | x86-64 |
|
||||
| | 3.12, 3.13, PyPy3 | |
|
||||
| +----------------------------+---------------------+
|
||||
| | 3.10 | arm64v8 |
|
||||
+----------------------------------+----------------------------+---------------------+
|
||||
| Ubuntu Linux 24.04 LTS (Noble) | 3.12 | x86-64, ppc64le, |
|
||||
| | | s390x |
|
||||
| Ubuntu Linux 24.04 LTS (Noble) | 3.12 | x86-64, arm64v8, |
|
||||
| | | ppc64le, s390x |
|
||||
+----------------------------------+----------------------------+---------------------+
|
||||
| Windows Server 2019 | 3.9 | x86-64 |
|
||||
| Windows Server 2019 | 3.9 | x86 |
|
||||
+----------------------------------+----------------------------+---------------------+
|
||||
| Windows Server 2022 | 3.9, 3.10, 3.11, | x86-64 |
|
||||
| | 3.12, 3.13, PyPy3 | |
|
||||
| +----------------------------+---------------------+
|
||||
| | 3.13 | x86 |
|
||||
| Windows Server 2022 | 3.10, 3.11, 3.12, 3.13, | x86-64 |
|
||||
| | PyPy3 | |
|
||||
| +----------------------------+---------------------+
|
||||
| | 3.12 (MinGW) | x86-64 |
|
||||
| +----------------------------+---------------------+
|
||||
|
@ -75,7 +73,7 @@ These platforms have been reported to work at the versions mentioned.
|
|||
| Operating system | | Tested Python | | Latest tested | | Tested |
|
||||
| | | versions | | Pillow version | | processors |
|
||||
+==================================+============================+==================+==============+
|
||||
| macOS 15 Sequoia | 3.9, 3.10, 3.11, 3.12, 3.13| 11.0.0 |arm |
|
||||
| macOS 15 Sequoia | 3.9, 3.10, 3.11, 3.12, 3.13| 11.1.0 |arm |
|
||||
| +----------------------------+------------------+ |
|
||||
| | 3.8 | 10.4.0 | |
|
||||
+----------------------------------+----------------------------+------------------+--------------+
|
||||
|
|
|
@ -387,8 +387,11 @@ Methods
|
|||
the number of pixels between lines.
|
||||
:param align: If the text is passed on to
|
||||
:py:meth:`~PIL.ImageDraw.ImageDraw.multiline_text`,
|
||||
``"left"``, ``"center"`` or ``"right"``. Determines the relative alignment of lines.
|
||||
Use the ``anchor`` parameter to specify the alignment to ``xy``.
|
||||
``"left"``, ``"center"``, ``"right"`` or ``"justify"``. Determines
|
||||
the relative alignment of lines. Use the ``anchor`` parameter to
|
||||
specify the alignment to ``xy``.
|
||||
|
||||
.. versionadded:: 11.2.0 ``"justify"``
|
||||
:param direction: Direction of the text. It can be ``"rtl"`` (right to
|
||||
left), ``"ltr"`` (left to right) or ``"ttb"`` (top to bottom).
|
||||
Requires libraqm.
|
||||
|
@ -455,8 +458,11 @@ Methods
|
|||
of Pillow, but implemented only in version 8.0.0.
|
||||
|
||||
:param spacing: The number of pixels between lines.
|
||||
:param align: ``"left"``, ``"center"`` or ``"right"``. Determines the relative alignment of lines.
|
||||
Use the ``anchor`` parameter to specify the alignment to ``xy``.
|
||||
:param align: ``"left"``, ``"center"``, ``"right"`` or ``"justify"``. Determines
|
||||
the relative alignment of lines. Use the ``anchor`` parameter to
|
||||
specify the alignment to ``xy``.
|
||||
|
||||
.. versionadded:: 11.2.0 ``"justify"``
|
||||
:param direction: Direction of the text. It can be ``"rtl"`` (right to
|
||||
left), ``"ltr"`` (left to right) or ``"ttb"`` (top to bottom).
|
||||
Requires libraqm.
|
||||
|
@ -599,8 +605,11 @@ Methods
|
|||
the number of pixels between lines.
|
||||
:param align: If the text is passed on to
|
||||
:py:meth:`~PIL.ImageDraw.ImageDraw.multiline_textbbox`,
|
||||
``"left"``, ``"center"`` or ``"right"``. Determines the relative alignment of lines.
|
||||
Use the ``anchor`` parameter to specify the alignment to ``xy``.
|
||||
``"left"``, ``"center"``, ``"right"`` or ``"justify"``. Determines
|
||||
the relative alignment of lines. Use the ``anchor`` parameter to
|
||||
specify the alignment to ``xy``.
|
||||
|
||||
.. versionadded:: 11.2.0 ``"justify"``
|
||||
:param direction: Direction of the text. It can be ``"rtl"`` (right to
|
||||
left), ``"ltr"`` (left to right) or ``"ttb"`` (top to bottom).
|
||||
Requires libraqm.
|
||||
|
@ -650,8 +659,11 @@ Methods
|
|||
vertical text. See :ref:`text-anchors` for details.
|
||||
This parameter is ignored for non-TrueType fonts.
|
||||
:param spacing: The number of pixels between lines.
|
||||
:param align: ``"left"``, ``"center"`` or ``"right"``. Determines the relative alignment of lines.
|
||||
Use the ``anchor`` parameter to specify the alignment to ``xy``.
|
||||
:param align: ``"left"``, ``"center"``, ``"right"`` or ``"justify"``. Determines
|
||||
the relative alignment of lines. Use the ``anchor`` parameter to
|
||||
specify the alignment to ``xy``.
|
||||
|
||||
.. versionadded:: 11.2.0 ``"justify"``
|
||||
:param direction: Direction of the text. It can be ``"rtl"`` (right to
|
||||
left), ``"ltr"`` (left to right) or ``"ttb"`` (top to bottom).
|
||||
Requires libraqm.
|
||||
|
|
|
@ -54,6 +54,7 @@ Feature version numbers are available only where stated.
|
|||
Support for the following features can be checked:
|
||||
|
||||
* ``libjpeg_turbo``: (compile time) Whether Pillow was compiled against the libjpeg-turbo version of libjpeg. Compile-time version number is available.
|
||||
* ``mozjpeg``: (compile time) Whether Pillow was compiled against the MozJPEG version of libjpeg. Compile-time version number is available.
|
||||
* ``zlib_ng``: (compile time) Whether Pillow was compiled against the zlib-ng version of zlib. Compile-time version number is available.
|
||||
* ``raqm``: Raqm library, required for ``ImageFont.Layout.RAQM`` in :py:func:`PIL.ImageFont.truetype`. Run-time version number is available for Raqm 0.7.0 or newer.
|
||||
* ``libimagequant``: (compile time) ImageQuant quantization support in :py:func:`PIL.Image.Image.quantize`. Run-time version number is available.
|
||||
|
|
|
@ -1,40 +1,38 @@
|
|||
11.1.0
|
||||
------
|
||||
|
||||
Security
|
||||
========
|
||||
|
||||
TODO
|
||||
^^^^
|
||||
|
||||
TODO
|
||||
|
||||
:cve:`YYYY-XXXXX`: TODO
|
||||
^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
TODO
|
||||
|
||||
Backwards Incompatible Changes
|
||||
==============================
|
||||
|
||||
TODO
|
||||
^^^^
|
||||
|
||||
Deprecations
|
||||
============
|
||||
|
||||
TODO
|
||||
^^^^
|
||||
ExifTags.IFD.Makernote
|
||||
^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
TODO
|
||||
``ExifTags.IFD.Makernote`` has been deprecated. Instead, use
|
||||
``ExifTags.IFD.MakerNote``.
|
||||
|
||||
API Changes
|
||||
===========
|
||||
|
||||
TODO
|
||||
^^^^
|
||||
Writing XMP bytes to JPEG and MPO
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
TODO
|
||||
Pillow 11.0.0 added writing XMP data to JPEG and MPO images::
|
||||
|
||||
im.info["xmp"] = b"test"
|
||||
im.save("out.jpg")
|
||||
|
||||
However, this meant that XMP data was automatically kept from an opened image,
|
||||
which is inconsistent with the rest of Pillow's behaviour. This functionality
|
||||
has been removed. To write XMP data, the ``xmp`` argument can still be used for
|
||||
JPEG files::
|
||||
|
||||
im.save("out.jpg", xmp=b"test")
|
||||
|
||||
To save XMP data to the second frame of an MPO image, ``encoderinfo`` can now
|
||||
be used::
|
||||
|
||||
second_im.encoderinfo = {"xmp": b"test"}
|
||||
im.save("out.mpo", save_all=True, append_images=[second_im])
|
||||
|
||||
API Additions
|
||||
=============
|
||||
|
@ -49,9 +47,32 @@ zlib library, and what version of zlib-ng is being used::
|
|||
features.check_feature("zlib_ng") # True or False
|
||||
features.version_feature("zlib_ng") # "2.2.2" for example, or None
|
||||
|
||||
Saving TIFF as BigTIFF
|
||||
^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
TIFF images can now be saved as BigTIFF using a ``big_tiff`` argument::
|
||||
|
||||
im.save("out.tiff", big_tiff=True)
|
||||
|
||||
Other Changes
|
||||
=============
|
||||
|
||||
Reading JPEG 2000 comments
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
When opening a JPEG 2000 image, the comment may now be read into
|
||||
:py:attr:`~PIL.Image.Image.info` for J2K images, not just JP2 images.
|
||||
|
||||
Saving JPEG 2000 CMYK images
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
With OpenJPEG 2.5.3 or later, Pillow can now save CMYK images as JPEG 2000 files.
|
||||
|
||||
Minimum C version
|
||||
^^^^^^^^^^^^^^^^^
|
||||
|
||||
C99 is now the minimum version of C required to compile Pillow from source.
|
||||
|
||||
zlib-ng in wheels
|
||||
^^^^^^^^^^^^^^^^^
|
||||
|
||||
|
|
75
docs/releasenotes/11.2.0.rst
Normal file
75
docs/releasenotes/11.2.0.rst
Normal file
|
@ -0,0 +1,75 @@
|
|||
11.2.0
|
||||
------
|
||||
|
||||
Security
|
||||
========
|
||||
|
||||
TODO
|
||||
^^^^
|
||||
|
||||
TODO
|
||||
|
||||
:cve:`YYYY-XXXXX`: TODO
|
||||
^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
TODO
|
||||
|
||||
Backwards Incompatible Changes
|
||||
==============================
|
||||
|
||||
TODO
|
||||
^^^^
|
||||
|
||||
Deprecations
|
||||
============
|
||||
|
||||
Image.Image.get_child_images()
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
.. deprecated:: 11.2.0
|
||||
|
||||
``Image.Image.get_child_images()`` has been deprecated. and will be removed in Pillow
|
||||
13 (2026-10-15). It will be moved to ``ImageFile.ImageFile.get_child_images()``. The
|
||||
method uses an image's file pointer, and so child images could only be retrieved from
|
||||
an :py:class:`PIL.ImageFile.ImageFile` instance.
|
||||
|
||||
API Changes
|
||||
===========
|
||||
|
||||
TODO
|
||||
^^^^
|
||||
|
||||
TODO
|
||||
|
||||
API Additions
|
||||
=============
|
||||
|
||||
``"justify"`` multiline text alignment
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
In addition to ``"left"``, ``"center"`` and ``"right"``, multiline text can also be
|
||||
aligned using ``"justify"`` in :py:mod:`~PIL.ImageDraw`::
|
||||
|
||||
from PIL import Image, ImageDraw
|
||||
im = Image.new("RGB", (50, 25))
|
||||
draw = ImageDraw.Draw(im)
|
||||
draw.multiline_text((0, 0), "Multiline\ntext 1", align="justify")
|
||||
draw.multiline_textbbox((0, 0), "Multiline\ntext 2", align="justify")
|
||||
|
||||
Check for MozJPEG
|
||||
^^^^^^^^^^^^^^^^^
|
||||
|
||||
You can check if Pillow has been built against the MozJPEG version of the
|
||||
libjpeg library, and what version of MozJPEG is being used::
|
||||
|
||||
from PIL import features
|
||||
features.check_feature("mozjpeg") # True or False
|
||||
features.version_feature("mozjpeg") # "4.1.1" for example, or None
|
||||
|
||||
Other Changes
|
||||
=============
|
||||
|
||||
TODO
|
||||
^^^^
|
||||
|
||||
TODO
|
|
@ -14,6 +14,7 @@ expected to be backported to earlier versions.
|
|||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
11.2.0
|
||||
11.1.0
|
||||
11.0.0
|
||||
10.4.0
|
||||
|
|
|
@ -43,7 +43,7 @@ dynamic = [
|
|||
optional-dependencies.docs = [
|
||||
"furo",
|
||||
"olefile",
|
||||
"sphinx>=8.1",
|
||||
"sphinx>=8.2",
|
||||
"sphinx-copybutton",
|
||||
"sphinx-inline-tabs",
|
||||
"sphinxext-opengraph",
|
||||
|
@ -104,7 +104,6 @@ test-extras = "tests"
|
|||
|
||||
[tool.cibuildwheel.macos.environment]
|
||||
PATH = "$(pwd)/build/deps/darwin/bin:$(dirname $(which python3)):/usr/bin:/bin:/usr/sbin:/sbin:/Library/Apple/usr/bin"
|
||||
DYLD_LIBRARY_PATH = "$(pwd)/build/deps/darwin/lib"
|
||||
|
||||
[tool.black]
|
||||
exclude = "wheels/multibuild"
|
||||
|
@ -122,6 +121,7 @@ lint.select = [
|
|||
"ISC", # flake8-implicit-str-concat
|
||||
"LOG", # flake8-logging
|
||||
"PGH", # pygrep-hooks
|
||||
"PIE", # flake8-pie
|
||||
"PT", # flake8-pytest-style
|
||||
"PYI", # flake8-pyi
|
||||
"RUF100", # unused noqa (yesqa)
|
||||
|
@ -134,6 +134,7 @@ lint.ignore = [
|
|||
"E221", # Multiple spaces before operator
|
||||
"E226", # Missing whitespace around arithmetic operator
|
||||
"E241", # Multiple spaces after ','
|
||||
"PIE790", # flake8-pie: unnecessary-placeholder
|
||||
"PT001", # pytest-fixture-incorrect-parentheses-style
|
||||
"PT007", # pytest-parametrize-values-wrong-type
|
||||
"PT011", # pytest-raises-too-broad
|
||||
|
|
|
@ -26,17 +26,6 @@ from typing import BinaryIO
|
|||
|
||||
from . import FontFile, Image
|
||||
|
||||
bdf_slant = {
|
||||
"R": "Roman",
|
||||
"I": "Italic",
|
||||
"O": "Oblique",
|
||||
"RI": "Reverse Italic",
|
||||
"RO": "Reverse Oblique",
|
||||
"OT": "Other",
|
||||
}
|
||||
|
||||
bdf_spacing = {"P": "Proportional", "M": "Monospaced", "C": "Cell"}
|
||||
|
||||
|
||||
def bdf_char(
|
||||
f: BinaryIO,
|
||||
|
@ -54,7 +43,7 @@ def bdf_char(
|
|||
s = f.readline()
|
||||
if not s:
|
||||
return None
|
||||
if s[:9] == b"STARTCHAR":
|
||||
if s.startswith(b"STARTCHAR"):
|
||||
break
|
||||
id = s[9:].strip().decode("ascii")
|
||||
|
||||
|
@ -62,7 +51,7 @@ def bdf_char(
|
|||
props = {}
|
||||
while True:
|
||||
s = f.readline()
|
||||
if not s or s[:6] == b"BITMAP":
|
||||
if not s or s.startswith(b"BITMAP"):
|
||||
break
|
||||
i = s.find(b" ")
|
||||
props[s[:i].decode("ascii")] = s[i + 1 : -1].decode("ascii")
|
||||
|
@ -71,7 +60,7 @@ def bdf_char(
|
|||
bitmap = bytearray()
|
||||
while True:
|
||||
s = f.readline()
|
||||
if not s or s[:7] == b"ENDCHAR":
|
||||
if not s or s.startswith(b"ENDCHAR"):
|
||||
break
|
||||
bitmap += s[:-1]
|
||||
|
||||
|
@ -107,7 +96,7 @@ class BdfFontFile(FontFile.FontFile):
|
|||
super().__init__()
|
||||
|
||||
s = fp.readline()
|
||||
if s[:13] != b"STARTFONT 2.1":
|
||||
if not s.startswith(b"STARTFONT 2.1"):
|
||||
msg = "not a valid BDF file"
|
||||
raise SyntaxError(msg)
|
||||
|
||||
|
@ -116,7 +105,7 @@ class BdfFontFile(FontFile.FontFile):
|
|||
|
||||
while True:
|
||||
s = fp.readline()
|
||||
if not s or s[:13] == b"ENDPROPERTIES":
|
||||
if not s or s.startswith(b"ENDPROPERTIES"):
|
||||
break
|
||||
i = s.find(b" ")
|
||||
props[s[:i].decode("ascii")] = s[i + 1 : -1].decode("ascii")
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user