Merge branch 'main' into convert_mode

This commit is contained in:
Andrew Murray 2024-12-28 07:02:02 +11:00 committed by GitHub
commit 1187777c5b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
399 changed files with 10972 additions and 6261 deletions

View File

@ -18,12 +18,12 @@ environment:
TEST_OPTIONS:
DEPLOY: YES
matrix:
- PYTHON: C:/Python312
- PYTHON: C:/Python313
ARCHITECTURE: x86
APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2022
- PYTHON: C:/Python38-x64
- PYTHON: C:/Python39-x64
ARCHITECTURE: AMD64
APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2017
APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2019
install:
@ -34,11 +34,11 @@ install:
- 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.3.1
- path c:\nasm-2.16.03;C:\Program Files\gs\gs10.00.0\bin;%PATH%
- 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:\python38\python.exe c:\pillow\winbuild\build_prepare.py -v --depends=C:\pillow-depends\
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%
@ -51,11 +51,10 @@ build_script:
test_script:
- cd c:\pillow
- '%PYTHON%\%EXECUTABLE% -m pip install pytest pytest-cov pytest-timeout defusedxml numpy olefile pyroma'
- '%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%
- '%PYTHON%\%EXECUTABLE% -c "from PIL import Image"'
- '%PYTHON%\%EXECUTABLE% -m pytest -vx --cov PIL --cov Tests --cov-report term --cov-report xml Tests'
#- '%PYTHON%\%EXECUTABLE% test-installed.py -v -s %TEST_OPTIONS%' TODO TEST_OPTIONS with pytest?
- path %PYTHON%;%PATH%
- .ci\test.cmd
after_test:
- curl -Os https://uploader.codecov.io/latest/windows/codecov.exe

View File

@ -21,17 +21,16 @@ set -e
if [[ $(uname) != CYGWIN* ]]; then
sudo apt-get -qq install libfreetype6-dev liblcms2-dev python3-tk\
ghostscript libffi-dev libjpeg-turbo-progs libopenjp2-7-dev\
ghostscript libjpeg-turbo8-dev libopenjp2-7-dev\
cmake meson imagemagick libharfbuzz-dev libfribidi-dev\
sway wl-clipboard libopenblas-dev
fi
python3 -m pip install --upgrade pip
python3 -m pip install --upgrade wheel
# TODO Update condition when cffi supports 3.13
if ! [[ "$GHA_PYTHON_VERSION" == "3.13" ]]; then PYTHONOPTIMIZE=0 python3 -m pip install cffi ; fi
python3 -m pip install coverage
python3 -m pip install defusedxml
python3 -m pip install ipython
python3 -m pip install olefile
python3 -m pip install -U pytest
python3 -m pip install -U pytest-cov
@ -39,21 +38,17 @@ python3 -m pip install -U pytest-timeout
python3 -m pip install pyroma
if [[ $(uname) != CYGWIN* ]]; then
# TODO Update condition when NumPy supports 3.13
if ! [[ "$GHA_PYTHON_VERSION" == "3.13" ]]; then python3 -m pip install numpy ; fi
python3 -m pip install numpy
# PyQt6 doesn't support PyPy3
if [[ $GHA_PYTHON_VERSION == 3.* ]]; then
sudo apt-get -qq install libegl1 libxcb-cursor0 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-randr0 libxcb-render-util0 libxcb-shape0 libxkbcommon-x11-0
python3 -m pip install pyqt6
# TODO Update condition when pyqt6 supports free-threading
if ! [[ "$PYTHON_GIL" == "0" ]]; then python3 -m pip install pyqt6 ; fi
fi
# Pyroma uses non-isolated build and fails with old setuptools
if [[
$GHA_PYTHON_VERSION == pypy3.9
|| $GHA_PYTHON_VERSION == 3.8
|| $GHA_PYTHON_VERSION == 3.9
]]; then
if [[ $GHA_PYTHON_VERSION == 3.9 ]]; then
# To match pyproject.toml
python3 -m pip install "setuptools>=67.8"
fi

View File

@ -1 +1 @@
cibuildwheel==2.18.1
cibuildwheel==2.22.0

View File

@ -1 +1,12 @@
mypy==1.10.0
mypy==1.14.0
IceSpringPySideStubs-PyQt6
IceSpringPySideStubs-PySide6
ipython
numpy
packaging
pytest
sphinx
types-atheris
types-defusedxml
types-olefile
types-setuptools

3
.ci/test.cmd Normal file
View File

@ -0,0 +1,3 @@
python.exe -c "from PIL import Image"
IF ERRORLEVEL 1 EXIT /B
python.exe -bb -m pytest -v -x -W always --cov PIL --cov Tests --cov-report term --cov-report xml Tests

View File

@ -4,4 +4,4 @@ set -e
python3 -c "from PIL import Image"
python3 -bb -m pytest -v -x -W always --cov PIL --cov Tests --cov-report term Tests $REVERSE
python3 -bb -m pytest -v -x -W always --cov PIL --cov Tests --cov-report term --cov-report xml Tests $REVERSE

View File

@ -3,7 +3,7 @@
BasedOnStyle: Google
AlwaysBreakAfterReturnType: All
AllowShortIfStatementsOnASingleLine: false
AlignAfterOpenBracket: AlwaysBreak
AlignAfterOpenBracket: BlockIndent
BinPackArguments: false
BinPackParameters: false
BreakBeforeBraces: Attach

View File

@ -19,6 +19,5 @@ exclude_also =
[run]
omit =
Tests/32bit_segfault_check.py
Tests/bench_cffi_access.py
Tests/check_*.py
Tests/createfontdatachunk.py

View File

@ -19,7 +19,6 @@ Please send a pull request to the `main` branch. Please include [documentation](
- Follow PEP 8.
- When committing only documentation changes please include `[ci skip]` in the commit message to avoid running tests on AppVeyor.
- 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.
- Do not add to the [changelog](https://github.com/python-pillow/Pillow/blob/main/CHANGES.rst) for proposed changes, as that is updated after changes are merged.
## Reporting Issues

View File

@ -3,18 +3,19 @@ tag-template: "$NEXT_MINOR_VERSION"
change-template: '- $TITLE #$NUMBER [@$AUTHOR]'
categories:
- title: "Dependencies"
label: "Dependency"
- title: "Removals"
label: "Removal"
- title: "Deprecations"
label: "Deprecation"
- title: "Documentation"
label: "Documentation"
- title: "Removals"
label: "Removal"
- title: "Dependencies"
label: "Dependency"
- title: "Testing"
label: "Testing"
- title: "Type hints"
label: "Type hints"
- title: "Other changes"
exclude-labels:
- "changelog: skip"
@ -23,6 +24,4 @@ template: |
https://pillow.readthedocs.io/en/stable/releasenotes/$NEXT_MINOR_VERSION.html
## Changes
$CHANGES

12
.github/renovate.json vendored
View File

@ -1,7 +1,7 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [
"config:base"
"config:recommended"
],
"labels": [
"Dependency"
@ -9,9 +9,13 @@
"packageRules": [
{
"groupName": "github-actions",
"matchManagers": ["github-actions"],
"separateMajorMinor": "false"
"matchManagers": [
"github-actions"
],
"separateMajorMinor": false
}
],
"schedule": ["on the 3rd day of the month"]
"schedule": [
"on the 3rd day of the month"
]
}

View File

@ -6,11 +6,13 @@ on:
- "**"
paths:
- ".github/workflows/cifuzz.yml"
- ".github/workflows/wheels-dependencies.sh"
- "**.c"
- "**.h"
pull_request:
paths:
- ".github/workflows/cifuzz.yml"
- ".github/workflows/wheels-dependencies.sh"
- "**.c"
- "**.h"
workflow_dispatch:

View File

@ -33,6 +33,8 @@ jobs:
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- name: Set up Python
uses: actions/setup-python@v5

View File

@ -21,6 +21,8 @@ jobs:
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- name: pre-commit cache
uses: actions/cache@v4

View File

@ -2,31 +2,34 @@
set -e
if [[ "$ImageOS" == "macos13" ]]; then
brew uninstall gradle maven
fi
brew install \
freetype \
ghostscript \
jpeg-turbo \
libimagequant \
libjpeg \
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"
# TODO Update condition when cffi supports 3.13
if ! [[ "$GHA_PYTHON_VERSION" == "3.13" ]]; then PYTHONOPTIMIZE=0 python3 -m pip install cffi ; fi
python3 -m pip install coverage
python3 -m pip install defusedxml
python3 -m pip install ipython
python3 -m pip install olefile
python3 -m pip install -U pytest
python3 -m pip install -U pytest-cov
python3 -m pip install -U pytest-timeout
python3 -m pip install pyroma
# TODO Update condition when NumPy supports 3.13
if ! [[ "$GHA_PYTHON_VERSION" == "3.13" ]]; then python3 -m pip install numpy ; fi
python3 -m pip install numpy
# extra test images
pushd depends && ./install_extra_test_images.sh && popd

View File

@ -6,7 +6,7 @@ on:
workflow_dispatch:
permissions:
issues: write
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
@ -15,6 +15,8 @@ concurrency:
jobs:
stale:
if: github.repository_owner == 'python-pillow'
permissions:
issues: write
runs-on: ubuntu-latest

View File

@ -35,7 +35,7 @@ jobs:
strategy:
fail-fast: false
matrix:
python-minor-version: [8, 9]
python-minor-version: [9]
timeout-minutes: 40
@ -48,6 +48,8 @@ jobs:
- name: Checkout Pillow
uses: actions/checkout@v4
with:
persist-credentials: false
- name: Install Cygwin
uses: cygwin/cygwin-install-action@v4
@ -72,9 +74,9 @@ jobs:
make
netpbm
perl
python3${{ matrix.python-minor-version }}-cffi
python3${{ matrix.python-minor-version }}-cython
python3${{ matrix.python-minor-version }}-devel
python3${{ matrix.python-minor-version }}-ipython
python3${{ matrix.python-minor-version }}-numpy
python3${{ matrix.python-minor-version }}-sip
python3${{ matrix.python-minor-version }}-tkinter
@ -131,11 +133,12 @@ jobs:
- name: After success
run: |
bash.exe .ci/after_success.sh
rm C:\cygwin\bin\bash.EXE
- name: Upload coverage
uses: codecov/codecov-action@v4
uses: codecov/codecov-action@v5
with:
file: ./coverage.xml
files: ./coverage.xml
flags: GHA_Cygwin
name: Cygwin Python 3.${{ matrix.python-minor-version }}
token: ${{ secrets.CODECOV_ORG_TOKEN }}

View File

@ -44,13 +44,11 @@ jobs:
amazon-2023-amd64,
arch,
centos-stream-9-amd64,
debian-11-bullseye-amd64,
debian-12-bookworm-x86,
debian-12-bookworm-amd64,
fedora-39-amd64,
fedora-40-amd64,
fedora-41-amd64,
gentoo,
ubuntu-20.04-focal-amd64,
ubuntu-22.04-jammy-amd64,
ubuntu-24.04-noble-amd64,
]
@ -67,6 +65,8 @@ jobs:
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- name: Build system information
run: python3 .github/workflows/system-info.py
@ -100,11 +100,10 @@ jobs:
MATRIX_DOCKER: ${{ matrix.docker }}
- name: Upload coverage
uses: codecov/codecov-action@v4
uses: codecov/codecov-action@v5
with:
flags: GHA_Docker
name: ${{ matrix.docker }}
gcov: true
token: ${{ secrets.CODECOV_ORG_TOKEN }}
success:

View File

@ -46,6 +46,8 @@ jobs:
steps:
- name: Checkout Pillow
uses: actions/checkout@v4
with:
persist-credentials: false
- name: Set up shell
run: echo "C:\msys64\usr\bin\" >> $env:GITHUB_PATH
@ -64,30 +66,28 @@ jobs:
mingw-w64-x86_64-libtiff \
mingw-w64-x86_64-libwebp \
mingw-w64-x86_64-openjpeg2 \
mingw-w64-x86_64-python3-cffi \
mingw-w64-x86_64-python3-numpy \
mingw-w64-x86_64-python3-olefile \
mingw-w64-x86_64-python3-setuptools \
mingw-w64-x86_64-python3-pip \
mingw-w64-x86_64-python-pytest \
mingw-w64-x86_64-python-pytest-cov \
mingw-w64-x86_64-python-pytest-timeout \
mingw-w64-x86_64-python-pyqt6
python3 -m ensurepip
python3 -m pip install pyroma pytest pytest-cov pytest-timeout
pushd depends && ./install_extra_test_images.sh && popd
- name: Build Pillow
run: SETUPTOOLS_USE_DISTUTILS="stdlib" CFLAGS="-coverage" python3 -m pip install .
run: CFLAGS="-coverage" python3 -m pip install .
- name: Test Pillow
run: |
python3 selftest.py --installed
python3 -c "from PIL import Image"
python3 -m pytest -vx --cov PIL --cov Tests --cov-report term --cov-report xml Tests
.ci/test.sh
- name: Upload coverage
uses: codecov/codecov-action@v4
uses: codecov/codecov-action@v5
with:
file: ./coverage.xml
files: ./coverage.xml
flags: GHA_Windows
name: "MSYS2 MinGW"
token: ${{ secrets.CODECOV_ORG_TOKEN }}

View File

@ -40,6 +40,8 @@ jobs:
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- name: Build system information
run: python3 .github/workflows/system-info.py

View File

@ -35,7 +35,7 @@ jobs:
strategy:
fail-fast: false
matrix:
python-version: ["pypy3.10", "pypy3.9", "3.8", "3.9", "3.10", "3.11", "3.12", "3.13"]
python-version: ["pypy3.10", "3.9", "3.10", "3.11", "3.12", "3.13"]
timeout-minutes: 30
@ -44,16 +44,20 @@ jobs:
steps:
- name: Checkout Pillow
uses: actions/checkout@v4
with:
persist-credentials: false
- name: Checkout cached dependencies
uses: actions/checkout@v4
with:
persist-credentials: false
repository: python-pillow/pillow-depends
path: winbuild\depends
- name: Checkout extra test images
uses: actions/checkout@v4
with:
persist-credentials: false
repository: python-pillow/test-images
path: Tests\test-images
@ -69,16 +73,14 @@ jobs:
- name: Print build system information
run: python3 .github/workflows/system-info.py
- name: Install Python dependencies
run: >
python3 -m pip install
coverage>=7.4.2
defusedxml
olefile
pyroma
pytest
pytest-cov
pytest-timeout
- name: Upgrade pip
run: |
python3 -m pip install --upgrade pip
- name: Install CPython dependencies
if: "!contains(matrix.python-version, 'pypy')"
run: |
python3 -m pip install PyQt6
- name: Install dependencies
id: install
@ -86,8 +88,8 @@ jobs:
choco install nasm --no-progress
echo "C:\Program Files\NASM" >> $env:GITHUB_PATH
choco install ghostscript --version=10.3.1 --no-progress
echo "C:\Program Files\gs\gs10.00.0\bin" >> $env:GITHUB_PATH
choco install ghostscript --version=10.4.0 --no-progress
echo "C:\Program Files\gs\gs10.04.0\bin" >> $env:GITHUB_PATH
# Install extra test images
xcopy /S /Y Tests\test-images\* Tests\images
@ -178,7 +180,7 @@ jobs:
- name: Build Pillow
run: |
$FLAGS="-C raqm=vendor -C fribidi=vendor"
cmd /c "winbuild\build\build_env.cmd && $env:pythonLocation\python.exe -m pip install -v $FLAGS ."
cmd /c "winbuild\build\build_env.cmd && $env:pythonLocation\python.exe -m pip install -v $FLAGS .[tests]"
& $env:pythonLocation\python.exe selftest.py --installed
shell: pwsh
@ -190,8 +192,8 @@ jobs:
- name: Test Pillow
run: |
path %GITHUB_WORKSPACE%\\winbuild\\build\\bin;%PATH%
python.exe -m pytest -vx -W always --cov PIL --cov Tests --cov-report term --cov-report xml Tests
path %GITHUB_WORKSPACE%\winbuild\build\bin;%PATH%
.ci\test.cmd
shell: cmd
- name: Prepare to upload errors
@ -213,9 +215,9 @@ jobs:
shell: pwsh
- name: Upload coverage
uses: codecov/codecov-action@v4
uses: codecov/codecov-action@v5
with:
file: ./coverage.xml
files: ./coverage.xml
flags: GHA_Windows
name: ${{ runner.os }} Python ${{ matrix.python-version }}
token: ${{ secrets.CODECOV_ORG_TOKEN }}

View File

@ -37,44 +37,38 @@ jobs:
fail-fast: false
matrix:
os: [
"macos-14",
"macos-latest",
"ubuntu-latest",
]
python-version: [
"pypy3.10",
"pypy3.9",
"3.13t",
"3.13",
"3.12",
"3.11",
"3.10",
"3.9",
"3.8",
]
include:
- python-version: "3.11"
PYTHONOPTIMIZE: 1
REVERSE: "--reverse"
- python-version: "3.10"
PYTHONOPTIMIZE: 2
- { python-version: "3.11", PYTHONOPTIMIZE: 1, REVERSE: "--reverse" }
- { python-version: "3.10", PYTHONOPTIMIZE: 2 }
# Free-threaded
- { python-version: "3.13t", disable-gil: true }
# M1 only available for 3.10+
- os: "macos-13"
python-version: "3.9"
- os: "macos-13"
python-version: "3.8"
- { os: "macos-13", python-version: "3.9" }
exclude:
- os: "macos-14"
python-version: "3.9"
- os: "macos-14"
python-version: "3.8"
- { os: "macos-latest", python-version: "3.9" }
runs-on: ${{ matrix.os }}
name: ${{ matrix.os }} Python ${{ matrix.python-version }}
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
uses: Quansight-Labs/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
allow-prereleases: true
@ -83,6 +77,11 @@ jobs:
".ci/*.sh"
"pyproject.toml"
- name: Set PYTHON_GIL
if: "${{ matrix.disable-gil }}"
run: |
echo "PYTHON_GIL=0" >> $GITHUB_ENV
- name: Build system information
run: python3 .github/workflows/system-info.py
@ -110,7 +109,7 @@ jobs:
GHA_PYTHON_VERSION: ${{ matrix.python-version }}
- name: Register gcc problem matcher
if: "matrix.os == 'ubuntu-latest' && matrix.python-version == '3.12'"
if: "matrix.os == 'ubuntu-latest' && matrix.python-version == '3.13'"
run: echo "::add-matcher::.github/problem-matchers/gcc.json"
- name: Build
@ -150,11 +149,10 @@ jobs:
.ci/after_success.sh
- name: Upload coverage
uses: codecov/codecov-action@v4
uses: codecov/codecov-action@v5
with:
flags: ${{ matrix.os == 'ubuntu-latest' && 'GHA_Ubuntu' || 'GHA_macOS' }}
name: ${{ matrix.os }} Python ${{ matrix.python-version }}
gcov: true
token: ${{ secrets.CODECOV_ORG_TOKEN }}
success:

View File

@ -1,11 +1,33 @@
#!/bin/bash
# Define custom utilities
# Test for macOS with [ -n "$IS_MACOS" ]
if [ -z "$IS_MACOS" ]; then
export MB_ML_LIBC=${AUDITWHEEL_POLICY::9}
export MB_ML_VER=${AUDITWHEEL_POLICY:9}
# Setup that needs to be done before multibuild utils are invoked
PROJECTDIR=$(pwd)
if [[ "$(uname -s)" == "Darwin" ]]; then
# Safety check - macOS builds require that CIBW_ARCHS is set, and that it
# only contains a single value (even though cibuildwheel allows multiple
# values in CIBW_ARCHS).
if [[ -z "$CIBW_ARCHS" ]]; then
echo "ERROR: Pillow macOS builds require CIBW_ARCHS be defined."
exit 1
fi
if [[ "$CIBW_ARCHS" == *" "* ]]; then
echo "ERROR: Pillow macOS builds only support a single architecture in CIBW_ARCHS."
exit 1
fi
# Build macOS dependencies in `build/darwin`
# Install them into `build/deps/darwin`
WORKDIR=$(pwd)/build/darwin
BUILD_PREFIX=$(pwd)/build/deps/darwin
else
# Build prefix will default to /usr/local
WORKDIR=$(pwd)/build
MB_ML_LIBC=${AUDITWHEEL_POLICY::9}
MB_ML_VER=${AUDITWHEEL_POLICY:9}
fi
export PLAT=$CIBW_ARCHS
PLAT=$CIBW_ARCHS
# Define custom utilities
source wheels/multibuild/common_utils.sh
source wheels/multibuild/library_builders.sh
if [ -z "$IS_MACOS" ]; then
@ -16,11 +38,11 @@ ARCHIVE_SDIR=pillow-depends-main
# Package versions for fresh source builds
FREETYPE_VERSION=2.13.2
HARFBUZZ_VERSION=8.5.0
LIBPNG_VERSION=1.6.43
JPEGTURBO_VERSION=3.0.3
OPENJPEG_VERSION=2.5.2
XZ_VERSION=5.4.5
HARFBUZZ_VERSION=10.1.0
LIBPNG_VERSION=1.6.44
JPEGTURBO_VERSION=3.1.0
OPENJPEG_VERSION=2.5.3
XZ_VERSION=5.6.3
TIFF_VERSION=4.6.0
LCMS2_VERSION=2.16
if [[ -n "$IS_MACOS" ]]; then
@ -28,69 +50,90 @@ if [[ -n "$IS_MACOS" ]]; then
else
GIFLIB_VERSION=5.2.1
fi
if [[ -n "$IS_MACOS" ]] || [[ "$MB_ML_VER" != 2014 ]]; then
ZLIB_VERSION=1.3.1
else
ZLIB_VERSION=1.2.8
fi
LIBWEBP_VERSION=1.4.0
ZLIB_NG_VERSION=2.2.2
LIBWEBP_VERSION=1.5.0
BZIP2_VERSION=1.0.8
LIBXCB_VERSION=1.17.0
BROTLI_VERSION=1.1.0
if [[ -n "$IS_MACOS" ]] && [[ "$CIBW_ARCHS" == "x86_64" ]]; then
function build_openjpeg {
local out_dir=$(fetch_unpack https://github.com/uclouvain/openjpeg/archive/v${OPENJPEG_VERSION}.tar.gz openjpeg-${OPENJPEG_VERSION}.tar.gz)
(cd $out_dir \
&& cmake -DCMAKE_INSTALL_PREFIX=$BUILD_PREFIX -DCMAKE_INSTALL_NAME_DIR=$BUILD_PREFIX/lib . \
&& make install)
touch openjpeg-stamp
}
fi
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 \
--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
}
function build_zlib_ng {
if [ -e zlib-stamp ]; then return; fi
fetch_unpack https://github.com/zlib-ng/zlib-ng/archive/$ZLIB_NG_VERSION.tar.gz zlib-ng-$ZLIB_NG_VERSION.tar.gz
(cd zlib-ng-$ZLIB_NG_VERSION \
&& ./configure --prefix=$BUILD_PREFIX --zlib-compat \
&& make -j4 \
&& make install)
touch zlib-stamp
}
function build_brotli {
local cmake=$(get_modern_cmake)
local out_dir=$(fetch_unpack https://github.com/google/brotli/archive/v$BROTLI_VERSION.tar.gz brotli-1.1.0.tar.gz)
if [ -e brotli-stamp ]; then return; fi
local out_dir=$(fetch_unpack https://github.com/google/brotli/archive/v$BROTLI_VERSION.tar.gz brotli-$BROTLI_VERSION.tar.gz)
(cd $out_dir \
&& $cmake -DCMAKE_INSTALL_PREFIX=$BUILD_PREFIX -DCMAKE_INSTALL_NAME_DIR=$BUILD_PREFIX/lib . \
&& cmake -DCMAKE_INSTALL_PREFIX=$BUILD_PREFIX -DCMAKE_INSTALL_LIBDIR=$BUILD_PREFIX/lib -DCMAKE_INSTALL_NAME_DIR=$BUILD_PREFIX/lib . \
&& make install)
if [[ "$MB_ML_LIBC" == "manylinux" ]]; then
cp /usr/local/lib64/libbrotli* /usr/local/lib
cp /usr/local/lib64/pkgconfig/libbrotli* /usr/local/lib/pkgconfig
fi
touch brotli-stamp
}
function build_harfbuzz {
if [ -e harfbuzz-stamp ]; then return; fi
python3 -m pip install meson ninja
local out_dir=$(fetch_unpack https://github.com/harfbuzz/harfbuzz/releases/download/$HARFBUZZ_VERSION/harfbuzz-$HARFBUZZ_VERSION.tar.xz harfbuzz-$HARFBUZZ_VERSION.tar.xz)
(cd $out_dir \
&& meson setup build --prefix=$BUILD_PREFIX --libdir=$BUILD_PREFIX/lib --buildtype=release -Dfreetype=enabled -Dglib=disabled)
(cd $out_dir/build \
&& meson install)
touch harfbuzz-stamp
}
function build {
if [[ -n "$IS_MACOS" ]] && [[ "$CIBW_ARCHS" == "arm64" ]]; then
sudo chown -R runner /usr/local
fi
build_xz
if [ -z "$IS_ALPINE" ] && [ -z "$IS_MACOS" ]; then
yum remove -y zlib-devel
fi
build_new_zlib
build_zlib_ng
build_simple xcb-proto 1.17.0 https://xorg.freedesktop.org/archive/individual/proto
if [ -n "$IS_MACOS" ]; then
build_simple xorgproto 2024.1 https://www.x.org/pub/individual/proto
build_simple libXau 1.0.11 https://www.x.org/pub/individual/lib
build_simple libXau 1.0.12 https://www.x.org/pub/individual/lib
build_simple libpthread-stubs 0.5 https://xcb.freedesktop.org/dist
if [[ "$CIBW_ARCHS" == "arm64" ]]; then
cp /usr/local/share/pkgconfig/xcb-proto.pc /usr/local/lib/pkgconfig
fi
else
sed s/\${pc_sysrootdir\}// /usr/local/share/pkgconfig/xcb-proto.pc > /usr/local/lib/pkgconfig/xcb-proto.pc
sed s/\${pc_sysrootdir\}// $BUILD_PREFIX/share/pkgconfig/xcb-proto.pc > $BUILD_PREFIX/lib/pkgconfig/xcb-proto.pc
fi
build_simple libxcb $LIBXCB_VERSION https://www.x.org/releases/individual/lib
build_libjpeg_turbo
build_tiff
if [ -n "$IS_MACOS" ]; then
# Custom tiff build to include jpeg; by default, configure won't include
# headers/libs in the custom macOS prefix. Explicitly disable webp,
# libdeflate and zstd, because on x86_64 macs, it will pick up the
# Homebrew versions of those libraries from /usr/local.
build_simple tiff $TIFF_VERSION https://download.osgeo.org/libtiff tar.gz \
--with-jpeg-include-dir=$BUILD_PREFIX/include --with-jpeg-lib-dir=$BUILD_PREFIX/lib \
--disable-webp --disable-libdeflate --disable-zstd
else
build_tiff
fi
build_libpng
build_lcms2
build_openjpeg
if [ -f /usr/local/lib64/libopenjp2.so ]; then
cp /usr/local/lib64/libopenjp2.so /usr/local/lib
fi
ORIGINAL_CFLAGS=$CFLAGS
CFLAGS="$CFLAGS -O3 -DNDEBUG"
@ -109,42 +152,50 @@ function build {
build_freetype
fi
if [ -z "$IS_MACOS" ]; then
export FREETYPE_LIBS=-lfreetype
export FREETYPE_CFLAGS=-I/usr/local/include/freetype2/
fi
build_simple harfbuzz $HARFBUZZ_VERSION https://github.com/harfbuzz/harfbuzz/releases/download/$HARFBUZZ_VERSION tar.xz --with-freetype=yes --with-glib=no
if [ -z "$IS_MACOS" ]; then
export FREETYPE_LIBS=""
export FREETYPE_CFLAGS=""
fi
build_harfbuzz
}
# Perform all dependency builds in the build subfolder.
mkdir -p $WORKDIR
pushd $WORKDIR > /dev/null
# Any stuff that you need to do before you start building the wheels
# Runs in the root directory of this repository.
curl -fsSL -o pillow-depends-main.zip https://github.com/python-pillow/pillow-depends/archive/main.zip
untar pillow-depends-main.zip
if [[ ! -d $WORKDIR/pillow-depends-main ]]; then
if [[ ! -f $PROJECTDIR/pillow-depends-main.zip ]]; then
echo "Download pillow dependency sources..."
curl -fSL -o $PROJECTDIR/pillow-depends-main.zip https://github.com/python-pillow/pillow-depends/archive/main.zip
fi
echo "Unpacking pillow dependency sources..."
untar $PROJECTDIR/pillow-depends-main.zip
fi
if [[ -n "$IS_MACOS" ]]; then
# libtiff and libxcb cause a conflict with building libtiff and libxcb
# libxau and libxdmcp cause an issue on macOS < 11
# remove cairo to fix building harfbuzz on arm64
# remove lcms2 and libpng to fix building openjpeg on arm64
# remove jpeg-turbo to avoid inclusion on arm64
# remove webp and zstd to avoid inclusion on x86_64
# curl from brew requires zstd, use system curl
brew remove --ignore-dependencies libpng libtiff libxcb libxau libxdmcp curl cairo lcms2 zstd
if [[ "$CIBW_ARCHS" == "arm64" ]]; then
brew remove --ignore-dependencies jpeg-turbo
else
brew remove --ignore-dependencies webp
fi
# Homebrew (or similar packaging environments) install can contain some of
# the libraries that we're going to build. However, they may be compiled
# with a MACOSX_DEPLOYMENT_TARGET that doesn't match what we want to use,
# and they may bring in other dependencies that we don't want. The same will
# be true of any other locations on the path. To avoid conflicts, strip the
# path down to the bare minimum (which, on macOS, won't include any
# development dependencies).
export PATH="$BUILD_PREFIX/bin:$(dirname $(which python3)):/usr/bin:/bin:/usr/sbin:/sbin:/Library/Apple/usr/bin"
export CMAKE_PREFIX_PATH=$BUILD_PREFIX
brew install pkg-config
# Ensure the basic structure of the build prefix directory exists.
mkdir -p "$BUILD_PREFIX/bin"
mkdir -p "$BUILD_PREFIX/lib"
# Ensure pkg-config is available
build_pkg_config
# Ensure cmake is available
python3 -m pip install cmake
fi
wrap_wheel_builder build
# Return to the project root to finish the build
popd > /dev/null
# Append licenses
for filename in wheels/dependency_licenses/*; do
echo -e "\n\n----\n\n$(basename $filename | cut -f 1 -d '.')\n" | cat >> LICENSE

View File

@ -1,20 +1,31 @@
#!/bin/bash
set -e
# Ensure fribidi is installed by the system.
if [[ "$OSTYPE" == "darwin"* ]]; then
brew install fribidi
export PKG_CONFIG_PATH="/usr/local/opt/openblas/lib/pkgconfig"
if [ -f /opt/homebrew/lib/libfribidi.dylib ]; then
sudo cp /opt/homebrew/lib/libfribidi.dylib /usr/local/lib
# If Homebrew is on the path during the build, it may leak into the wheels.
# However, we *do* need Homebrew to provide a copy of fribidi for
# testing purposes so that we can verify the fribidi shim works as expected.
if [[ "$(uname -m)" == "x86_64" ]]; then
HOMEBREW_PREFIX=/usr/local
else
HOMEBREW_PREFIX=/opt/homebrew
fi
$HOMEBREW_PREFIX/bin/brew install fribidi
# Add the lib folder for fribidi so that the vendored library can be found.
# Don't use $HOMEWBREW_PREFIX/lib directly - use the lib folder where the
# installed copy of fribidi is cellared. This ensures we don't pick up the
# Homebrew version of any other library that we're dependent on (most notably,
# freetype).
export DYLD_LIBRARY_PATH=$(dirname $(realpath $HOMEBREW_PREFIX/lib/libfribidi.dylib))
elif [ "${AUDITWHEEL_POLICY::9}" == "musllinux" ]; then
apk add curl fribidi
else
yum install -y fribidi
fi
if [ "${AUDITWHEEL_POLICY::9}" != "musllinux" ] && !([[ "$OSTYPE" == "darwin"* ]] && [[ $(python3 --version) == *"3.13."* ]]); then
python3 -m pip install numpy
fi
python3 -m pip install numpy
if [ ! -d "test-images-main" ]; then
curl -fsSL -o pillow-test-images.zip https://github.com/python-pillow/test-images/archive/main.zip

View File

@ -1,6 +1,14 @@
name: Wheels
on:
schedule:
# ┌───────────── minute (0 - 59)
# │ ┌───────────── hour (0 - 23)
# │ │ ┌───────────── day of the month (1 - 31)
# │ │ │ ┌───────────── month (1 - 12 or JAN-DEC)
# │ │ │ │ ┌───────────── day of the week (0 - 6 or SUN-SAT)
# │ │ │ │ │
- cron: "42 1 * * 0,3"
push:
paths:
- ".ci/requirements-cibw.txt"
@ -33,31 +41,27 @@ env:
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:
- pp39
- pp310
- cp38
- cp39
- cp310
- cp311
- cp312
- cp313
- cp3{9,10,11}
- cp3{12,13}
spec:
- manylinux2014
- manylinux_2_28
- musllinux
exclude:
- { python-version: pp39, spec: musllinux }
- { python-version: pp310, spec: musllinux }
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
submodules: true
- uses: actions/setup-python@v5
@ -81,7 +85,7 @@ jobs:
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_PRERELEASE_PYTHONS: True
CIBW_ENABLE: cpython-prerelease
# Extra options for manylinux.
CIBW_MANYLINUX_AARCH64_IMAGE: ${{ matrix.spec }}
CIBW_MANYLINUX_PYPY_AARCH64_IMAGE: ${{ matrix.spec }}
@ -92,18 +96,30 @@ jobs:
path: ./wheelhouse/*.whl
build-2-native-wheels:
if: github.event_name != 'schedule' || github.repository_owner == 'python-pillow'
name: ${{ matrix.name }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
include:
- name: "macOS x86_64"
- name: "macOS 10.10 x86_64"
os: macos-13
cibw_arch: x86_64
build: "cp3{9,10,11}*"
macosx_deployment_target: "10.10"
- name: "macOS 10.13 x86_64"
os: macos-13
cibw_arch: x86_64
build: "cp3{12,13}*"
macosx_deployment_target: "10.13"
- name: "macOS 10.15 x86_64"
os: macos-13
cibw_arch: x86_64
build: "pp310*"
macosx_deployment_target: "10.15"
- name: "macOS arm64"
os: macos-14
os: macos-latest
cibw_arch: arm64
macosx_deployment_target: "11.0"
- name: "manylinux2014 and musllinux x86_64"
@ -117,6 +133,7 @@ jobs:
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
submodules: true
- uses: actions/setup-python@v5
@ -133,19 +150,19 @@ jobs:
env:
CIBW_ARCHS: ${{ matrix.cibw_arch }}
CIBW_BUILD: ${{ matrix.build }}
CIBW_ENABLE: cpython-prerelease cpython-freethreading
CIBW_MANYLINUX_PYPY_X86_64_IMAGE: ${{ matrix.manylinux }}
CIBW_MANYLINUX_X86_64_IMAGE: ${{ matrix.manylinux }}
CIBW_PRERELEASE_PYTHONS: True
CIBW_SKIP: pp38-*
CIBW_TEST_SKIP: cp38-macosx_arm64
CIBW_SKIP: pp39-*
MACOSX_DEPLOYMENT_TARGET: ${{ matrix.macosx_deployment_target }}
- uses: actions/upload-artifact@v4
with:
name: dist-${{ matrix.os }}-${{ matrix.cibw_arch }}${{ matrix.manylinux && format('-{0}', matrix.manylinux) }}
name: dist-${{ matrix.os }}${{ matrix.macosx_deployment_target && format('-{0}', matrix.macosx_deployment_target) }}-${{ matrix.cibw_arch }}${{ matrix.manylinux && format('-{0}', matrix.manylinux) }}
path: ./wheelhouse/*.whl
windows:
if: github.event_name != 'schedule' || github.repository_owner == 'python-pillow'
name: Windows ${{ matrix.cibw_arch }}
runs-on: windows-latest
strategy:
@ -157,10 +174,13 @@ jobs:
- cibw_arch: ARM64
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- name: Checkout extra test images
uses: actions/checkout@v4
with:
persist-credentials: false
repository: python-pillow/test-images
path: Tests\test-images
@ -207,8 +227,8 @@ jobs:
CIBW_ARCHS: ${{ matrix.cibw_arch }}
CIBW_BEFORE_ALL: "{package}\\winbuild\\build\\build_dep_all.cmd"
CIBW_CACHE_PATH: "C:\\cibw"
CIBW_PRERELEASE_PYTHONS: True
CIBW_SKIP: pp38-*
CIBW_ENABLE: cpython-prerelease cpython-freethreading
CIBW_SKIP: pp39-*
CIBW_TEST_SKIP: "*-win_arm64"
CIBW_TEST_COMMAND: 'docker run --rm
-v {project}:C:\pillow
@ -232,9 +252,12 @@ jobs:
path: winbuild\build\bin\fribidi*
sdist:
if: github.event_name != 'schedule'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- name: Set up Python
uses: actions/setup-python@v5
@ -250,8 +273,25 @@ jobs:
name: dist-sdist
path: dist/*.tar.gz
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]
runs-on: ubuntu-latest
name: Upload wheels to scientific-python-nightly-wheels
steps:
- uses: actions/download-artifact@v4
with:
pattern: dist-*
path: dist
merge-multiple: true
- name: Upload wheels to scientific-python-nightly-wheels
uses: scientific-python/upload-nightly-action@82396a2ed4269ba06c6b2988bb4fd568ef3c3d6b # 0.6.1
with:
artifacts_path: dist
anaconda_nightly_upload_token: ${{ secrets.ANACONDA_ORG_UPLOAD_TOKEN }}
pypi-publish:
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags')
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]
runs-on: ubuntu-latest
name: Upload release to PyPI
@ -268,3 +308,5 @@ jobs:
merge-multiple: true
- name: Publish to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
with:
attestations: true

5
.gitignore vendored
View File

@ -19,6 +19,7 @@ lib64/
parts/
sdist/
var/
wheelhouse/
*.egg-info/
.installed.cfg
*.egg
@ -90,5 +91,9 @@ Tests/images/msp
Tests/images/picins
Tests/images/sunraster
# Test and dependency downloads
pillow-depends-main.zip
pillow-test-images.zip
# pyinstaller
*.spec

View File

@ -1,17 +1,17 @@
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.4.7
rev: v0.8.1
hooks:
- id: ruff
args: [--exit-non-zero-on-fix]
- repo: https://github.com/psf/black-pre-commit-mirror
rev: 24.4.2
rev: 24.10.0
hooks:
- id: black
- repo: https://github.com/PyCQA/bandit
rev: 1.7.8
rev: 1.8.0
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: v18.1.5
rev: v19.1.4
hooks:
- id: clang-format
types: [c]
@ -36,7 +36,7 @@ repos:
- id: rst-backticks
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.6.0
rev: v5.0.0
hooks:
- id: check-executables-have-shebangs
- id: check-shebang-scripts-are-executable
@ -50,29 +50,30 @@ repos:
exclude: ^.github/.*TEMPLATE|^Tests/(fonts|images)/
- repo: https://github.com/python-jsonschema/check-jsonschema
rev: 0.28.4
rev: 0.30.0
hooks:
- id: check-github-workflows
- id: check-readthedocs
- id: check-renovate
- repo: https://github.com/sphinx-contrib/sphinx-lint
rev: v0.9.1
rev: v1.0.0
hooks:
- id: sphinx-lint
- repo: https://github.com/tox-dev/pyproject-fmt
rev: 1.8.0
rev: v2.5.0
hooks:
- id: pyproject-fmt
- repo: https://github.com/abravalheri/validate-pyproject
rev: v0.18
rev: v0.23
hooks:
- id: validate-pyproject
additional_dependencies: [trove-classifiers>=2024.10.12]
- repo: https://github.com/tox-dev/tox-ini-fmt
rev: 1.3.1
rev: 1.4.1
hooks:
- id: tox-ini-fmt

View File

@ -3,7 +3,7 @@ version: 2
formats: [pdf]
build:
os: ubuntu-22.04
os: ubuntu-lts-latest
tools:
python: "3"
jobs:

View File

@ -2,9 +2,265 @@
Changelog (Pillow)
==================
10.4.0 (unreleased)
11.1.0 and newer
----------------
See GitHub Releases:
- https://github.com/python-pillow/Pillow/releases
11.0.0 (2024-10-15)
-------------------
- Update licence to MIT-CMU #8460
[hugovk]
- Conditionally define ImageCms type hint to avoid requiring core #8197
[radarhere]
- Support writing LONG8 offsets in AppendingTiffWriter #8417
[radarhere]
- Use ImageFile.MAXBLOCK when saving TIFF images #8461
[radarhere]
- Do not close provided file handles with libtiff when saving #8458
[radarhere]
- Support ImageFilter.BuiltinFilter for I;16* images #8438
[radarhere]
- Use ImagingCore.ptr instead of ImagingCore.id #8341
[homm, radarhere, hugovk]
- Updated EPS mode when opening images without transparency #8281
[Yay295, radarhere]
- Use transparency when combining P frames from APNGs #8443
[radarhere]
- Support all resampling filters when resizing I;16* images #8422
[radarhere]
- Free memory on early return #8413
[radarhere]
- Cast int before potentially exceeding INT_MAX #8402
[radarhere]
- Check image value before use #8400
[radarhere]
- Improved copying imagequant libraries #8420
[radarhere]
- Use Capsule for WebP saving #8386
[homm, radarhere]
- Fixed writing multiple StripOffsets to TIFF #8317
[Yay295, radarhere]
- Fix dereference before checking for NULL in ImagingTransformAffine #8398
[PavlNekrasov]
- Use transposed size after opening for TIFF images #8390
[radarhere, homm]
- Improve ImageFont error messages #8338
[yngvem, radarhere, hugovk]
- Mention MAX_TEXT_CHUNK limit in PNG error message #8391
[radarhere]
- Cast Dib handle to int #8385
[radarhere]
- Accept float stroke widths #8369
[radarhere]
- Deprecate ICNS (width, height, scale) sizes in favour of load(scale) #8352
[radarhere]
- Improved handling of RGBA palettes when saving GIF images #8366
[radarhere]
- Deprecate isImageType #8364
[radarhere]
- Support converting more modes to LAB by converting to RGBA first #8358
[radarhere]
- Deprecate support for FreeType 2.9.0 #8356
[hugovk, radarhere]
- Removed unused TiffImagePlugin IFD_LEGACY_API #8355
[radarhere]
- Handle duplicate EXIF header #8350
[zakajd, radarhere]
- Return early from BoxBlur if either width or height is zero #8347
[radarhere]
- Check text is either string or bytes #8308
[radarhere]
- Added writing XMP bytes to JPEG #8286
[radarhere]
- Support JPEG2000 RGBA palettes #8256
[radarhere]
- Expand C image to match GIF frame image size #8237
[radarhere]
- Allow saving I;16 images as PPM #8231
[radarhere]
- When IFD is missing, connect get_ifd() dictionary to Exif #8230
[radarhere]
- Skip truncated ICO mask if LOAD_TRUNCATED_IMAGES is enabled #8180
[radarhere]
- Treat unknown JPEG2000 colorspace as unspecified #8343
[radarhere]
- Updated error message when saving WebP with invalid width or height #8322
[radarhere, hugovk]
- Remove warning if NumPy failed to raise an error during conversion #8326
[radarhere]
- If left and right sides meet in ImageDraw.rounded_rectangle(), do not draw rectangle to fill gap #8304
[radarhere]
- Remove WebP support without anim, mux/demux, and with buggy alpha #8213
[homm, radarhere]
- Add missing TIFF CMYK;16B reader #8298
[homm]
- Remove all WITH_* flags from _imaging.c and other flags #8211
[homm]
- Improve ImageDraw2 shape methods #8265
[radarhere]
- Lock around usages of imaging memory arenas #8238
[lysnikolaou]
- Deprecate JpegImageFile huffman_ac and huffman_dc #8274
[radarhere]
- Deprecate ImageMath lambda_eval and unsafe_eval options argument #8242
[radarhere]
- Changed ContainerIO to subclass IO #8240
[radarhere]
- Move away from APIs that use borrowed references under the free-threaded build #8216
[hugovk, lysnikolaou]
- Allow size argument to resize() to be a NumPy array #8201
[radarhere]
- Drop support for Python 3.8 #8183
[hugovk, radarhere]
- Add support for Python 3.13 #8181
[hugovk, radarhere]
- Fix incompatibility with NumPy 1.20 #8187
[neutrinoceros, radarhere]
- Remove PSFile, PyAccess and USE_CFFI_ACCESS #8182
[hugovk, radarhere]
10.4.0 (2024-07-01)
-------------------
- Raise FileNotFoundError if show_file() path does not exist #8178
[radarhere]
- Improved reading 16-bit TGA images with colour #7965
[Yay295, radarhere]
- Deprecate non-image ImageCms modes #8031
[radarhere]
- Fixed processing multiple JPEG EXIF markers #8127
[radarhere]
- Do not preserve EXIFIFD tag by default when saving TIFF images #8110
[radarhere]
- Added ImageFont.load_default_imagefont() #8086
[radarhere]
- Added Image.WARN_POSSIBLE_FORMATS #8063
[radarhere]
- Remove zero-byte end padding when parsing any XMP data #8171
[radarhere]
- Do not detect Ultra HDR images as MPO #8056
[radarhere]
- Raise SyntaxError specific to JP2 #8146
[Yay295, radarhere]
- Do not use first frame duration for other frames when saving APNG images #8104
[radarhere]
- Consider I;16 pixel size when using a 1 mode mask #8112
[radarhere]
- When saving multiple PNG frames, convert to mode rather than raw mode #8087
[radarhere]
- Added byte support to FreeTypeFont #8141
[radarhere]
- Allow float center for rotate operations #8114
[radarhere]
- Do not read layers immediately when opening PSD images #8039
[radarhere]
- Restore original thread state #8065
[radarhere]
- Read IM and TIFF images as RGB, rather than RGBX #7997
[radarhere]
- Only preserve TIFF IPTC_NAA_CHUNK tag if type is BYTE or UNDEFINED #7948
[radarhere]
- Clarify ImageDraw2 error message when size is missing #8165
[radarhere]
- Support unpacking more rawmodes to RGBA palettes #7966
[radarhere]
- Removed support for Qt 5 #8159
[radarhere]
- Improve ``ImageFont.freetype`` support for XDG directories on Linux #8135
[mamg22, radarhere]
- Improved consistency of XMP handling #8069
[radarhere]
- Use pkg-config to help find libwebp and raqm #8142
[radarhere]
- Accept 't' suffix for libtiff version #8126, #8129
[radarhere]
- Deprecate ImageDraw.getdraw hints parameter #8124
[radarhere, hugovk]
- Added ImageDraw circle() #8085
[void4, hugovk, radarhere]

View File

@ -5,9 +5,9 @@ The Python Imaging Library (PIL) is
Pillow is the friendly PIL fork. It is
Copyright © 2010-2024 by Jeffrey A. Clark and contributors
Copyright © 2010 by Jeffrey A. Clark and contributors
Like PIL, Pillow is licensed under the open source HPND License:
Like PIL, Pillow is licensed under the open source MIT-CMU License:
By obtaining, using, and/or copying this software and/or its associated
documentation, you agree that you have read, understood, and will comply

View File

@ -17,12 +17,10 @@ coverage:
.PHONY: doc
.PHONY: html
doc html:
python3 -c "import PIL" > /dev/null 2>&1 || python3 -m pip install .
$(MAKE) -C docs html
.PHONY: htmlview
htmlview:
python3 -c "import PIL" > /dev/null 2>&1 || python3 -m pip install .
$(MAKE) -C docs htmlview
.PHONY: doccheck
@ -117,7 +115,7 @@ lint-fix:
python3 -c "import black" > /dev/null 2>&1 || python3 -m pip install black
python3 -m black .
python3 -c "import ruff" > /dev/null 2>&1 || python3 -m pip install ruff
python3 -m ruff --fix .
python3 -m ruff check --fix .
.PHONY: mypy
mypy:

View File

@ -51,7 +51,7 @@ As of 2019, Pillow development is
<a href="https://app.codecov.io/gh/python-pillow/Pillow"><img
alt="Code coverage"
src="https://codecov.io/gh/python-pillow/Pillow/branch/main/graph/badge.svg"></a>
<a href="https://bugs.chromium.org/p/oss-fuzz/issues/list?sort=-opened&can=1&q=proj:pillow"><img
<a href="https://issues.oss-fuzz.com/issues?q=title:pillow"><img
alt="Fuzzing Status"
src="https://oss-fuzz-build-logs.storage.googleapis.com/badges/pillow.svg"></a>
</td>
@ -107,7 +107,7 @@ The core image library is designed for fast access to data stored in a few basic
- [Issues](https://github.com/python-pillow/Pillow/issues)
- [Pull requests](https://github.com/python-pillow/Pillow/pulls)
- [Release notes](https://pillow.readthedocs.io/en/stable/releasenotes/index.html)
- [Changelog](https://github.com/python-pillow/Pillow/blob/main/CHANGES.rst)
- [Changelog](https://github.com/python-pillow/Pillow/releases)
- [Pre-fork](https://github.com/python-pillow/Pillow/blob/main/CHANGES.rst#pre-fork)
## Report a Vulnerability

View File

@ -12,7 +12,6 @@ Released quarterly on January 2nd, April 1st, July 1st and October 15th.
* [ ] 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 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`
* [ ] Update `CHANGES.rst`.
* [ ] Run pre-release check via `make release-test` in a freshly cloned repo.
* [ ] Create branch and tag for release e.g.:
```bash
@ -34,7 +33,6 @@ Released quarterly on January 2nd, April 1st, July 1st and October 15th.
Released as needed for security, installation or critical bug fixes.
* [ ] Make necessary changes in `main` branch.
* [ ] Update `CHANGES.rst`.
* [ ] Check out release branch e.g.:
```bash
git checkout -t remotes/origin/5.2.x

View File

@ -1,54 +0,0 @@
from __future__ import annotations
import time
from PIL import PyAccess
from .helper import hopper
# Not running this test by default. No DOS against CI.
def iterate_get(size, access) -> None:
(w, h) = size
for x in range(w):
for y in range(h):
access[(x, y)]
def iterate_set(size, access) -> None:
(w, h) = size
for x in range(w):
for y in range(h):
access[(x, y)] = (x % 256, y % 256, 0)
def timer(func, label, *args) -> None:
iterations = 5000
starttime = time.time()
for x in range(iterations):
func(*args)
if time.time() - starttime > 10:
break
endtime = time.time()
print(
f"{label}: completed {x + 1} iterations in {endtime - starttime:.4f}s, "
f"{(endtime - starttime) / (x + 1.0):.6f}s per iteration"
)
def test_direct() -> None:
im = hopper()
im.load()
# im = Image.new("RGB", (2000, 2000), (1, 3, 2))
caccess = im.im.pixel_access(False)
access = PyAccess.new(im, False)
assert access is not None
assert caccess[(0, 0)] == access[(0, 0)]
print(f"Size: {im.width}x{im.height}")
timer(iterate_get, "PyAccess - get", im.size, access)
timer(iterate_set, "PyAccess - set", im.size, access)
timer(iterate_get, "C-api - get", im.size, caccess)
timer(iterate_set, "C-api - set", im.size, caccess)

View File

@ -34,6 +34,7 @@ def test_wheel_features() -> None:
"fribidi",
"harfbuzz",
"libjpeg_turbo",
"zlib_ng",
"xcb",
}

View File

@ -11,14 +11,15 @@ import subprocess
import sys
import sysconfig
import tempfile
from collections.abc import Sequence
from functools import lru_cache
from io import BytesIO
from typing import Any, Callable, Sequence
from typing import Any, Callable
import pytest
from packaging.version import parse as parse_version
from PIL import Image, ImageMath, features
from PIL import Image, ImageFile, ImageMath, features
logger = logging.getLogger(__name__)
@ -59,9 +60,7 @@ def convert_to_comparable(
return new_a, new_b
def assert_deep_equal(
a: Sequence[Any], b: Sequence[Any], msg: str | None = None
) -> None:
def assert_deep_equal(a: Any, b: Any, msg: str | None = None) -> None:
try:
assert len(a) == len(b), msg or f"got length {len(a)}, expected {len(b)}"
except Exception:
@ -240,7 +239,7 @@ class PillowLeakTestCase:
# helpers
def fromstring(data: bytes) -> Image.Image:
def fromstring(data: bytes) -> ImageFile.ImageFile:
return Image.open(BytesIO(data))

BIN
Tests/images/eps/1.bmp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Binary file not shown.

View File

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

Before

Width:  |  Height:  |  Size: 5.4 KiB

After

Width:  |  Height:  |  Size: 5.4 KiB

View File

Before

Width:  |  Height:  |  Size: 5.9 KiB

After

Width:  |  Height:  |  Size: 5.9 KiB

View File

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 17 KiB

View File

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

Before

Width:  |  Height:  |  Size: 5.8 KiB

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 486 B

After

Width:  |  Height:  |  Size: 533 B

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 411 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 364 B

After

Width:  |  Height:  |  Size: 391 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 378 B

After

Width:  |  Height:  |  Size: 414 B

BIN
Tests/images/rgba16.tga Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 415 B

BIN
Tests/images/ultrahdr.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

View File

@ -16,8 +16,9 @@
import atheris
from atheris.import_hook import instrument_imports
with atheris.instrument_imports():
with instrument_imports():
import sys
import fuzzers

View File

@ -14,8 +14,9 @@
import atheris
from atheris.import_hook import instrument_imports
with atheris.instrument_imports():
with instrument_imports():
import sys
import fuzzers

View File

@ -1,5 +1,5 @@
{
<py3_8_encode_current_locale>
<py3_10_encode_current_locale>
Memcheck:Cond
...
fun:encode_current_locale

View File

@ -22,6 +22,8 @@ def test_bad() -> None:
for f in get_files("b"):
# Assert that there is no unclosed file warning
with warnings.catch_warnings():
warnings.simplefilter("error")
try:
with Image.open(f) as im:
im.load()

View File

@ -71,6 +71,11 @@ def test_color_modes() -> None:
box_blur(sample.convert("YCbCr"))
@pytest.mark.parametrize("size", ((0, 1), (1, 0)))
def test_zero_dimension(size: tuple[int, int]) -> None:
assert box_blur(Image.new("L", size)).size == size
def test_radius_0() -> None:
assert_blur(
sample,

View File

@ -105,91 +105,65 @@ class TestColorLut3DCoreAPI:
with pytest.raises(TypeError):
im.im.color_lut_3d("RGB", Image.Resampling.BILINEAR, 3, 2, 2, 2, 16)
def test_correct_args(self) -> None:
im = Image.new("RGB", (10, 10), 0)
im.im.color_lut_3d(
"RGB", Image.Resampling.BILINEAR, *self.generate_identity_table(3, 3)
)
im.im.color_lut_3d(
"CMYK", Image.Resampling.BILINEAR, *self.generate_identity_table(4, 3)
)
im.im.color_lut_3d(
"RGB",
Image.Resampling.BILINEAR,
*self.generate_identity_table(3, (2, 3, 3)),
)
im.im.color_lut_3d(
"RGB",
Image.Resampling.BILINEAR,
*self.generate_identity_table(3, (65, 3, 3)),
)
im.im.color_lut_3d(
"RGB",
Image.Resampling.BILINEAR,
*self.generate_identity_table(3, (3, 65, 3)),
)
im.im.color_lut_3d(
"RGB",
Image.Resampling.BILINEAR,
*self.generate_identity_table(3, (3, 3, 65)),
)
def test_wrong_mode(self) -> None:
with pytest.raises(ValueError, match="wrong mode"):
im = Image.new("L", (10, 10), 0)
im.im.color_lut_3d(
"RGB", Image.Resampling.BILINEAR, *self.generate_identity_table(3, 3)
)
with pytest.raises(ValueError, match="wrong mode"):
im = Image.new("RGB", (10, 10), 0)
im.im.color_lut_3d(
"L", Image.Resampling.BILINEAR, *self.generate_identity_table(3, 3)
)
with pytest.raises(ValueError, match="wrong mode"):
im = Image.new("L", (10, 10), 0)
im.im.color_lut_3d(
"L", Image.Resampling.BILINEAR, *self.generate_identity_table(3, 3)
)
with pytest.raises(ValueError, match="wrong mode"):
im = Image.new("RGB", (10, 10), 0)
im.im.color_lut_3d(
"RGBA", Image.Resampling.BILINEAR, *self.generate_identity_table(3, 3)
)
with pytest.raises(ValueError, match="wrong mode"):
im = Image.new("RGB", (10, 10), 0)
im.im.color_lut_3d(
"RGB", Image.Resampling.BILINEAR, *self.generate_identity_table(4, 3)
)
def test_correct_mode(self) -> None:
im = Image.new("RGBA", (10, 10), 0)
im.im.color_lut_3d(
"RGBA", Image.Resampling.BILINEAR, *self.generate_identity_table(3, 3)
)
im = Image.new("RGBA", (10, 10), 0)
im.im.color_lut_3d(
"RGBA", Image.Resampling.BILINEAR, *self.generate_identity_table(4, 3)
)
@pytest.mark.parametrize(
"lut_mode, table_channels, table_size",
[
("RGB", 3, 3),
("CMYK", 4, 3),
("RGB", 3, (2, 3, 3)),
("RGB", 3, (65, 3, 3)),
("RGB", 3, (3, 65, 3)),
("RGB", 3, (2, 3, 65)),
],
)
def test_correct_args(
self, lut_mode: str, table_channels: int, table_size: int | tuple[int, int, int]
) -> None:
im = Image.new("RGB", (10, 10), 0)
im.im.color_lut_3d(
"HSV", Image.Resampling.BILINEAR, *self.generate_identity_table(3, 3)
lut_mode,
Image.Resampling.BILINEAR,
*self.generate_identity_table(table_channels, table_size),
)
im = Image.new("RGB", (10, 10), 0)
@pytest.mark.parametrize(
"image_mode, lut_mode, table_channels, table_size",
[
("L", "RGB", 3, 3),
("RGB", "L", 3, 3),
("L", "L", 3, 3),
("RGB", "RGBA", 3, 3),
("RGB", "RGB", 4, 3),
],
)
def test_wrong_mode(
self, image_mode: str, lut_mode: str, table_channels: int, table_size: int
) -> None:
with pytest.raises(ValueError, match="wrong mode"):
im = Image.new(image_mode, (10, 10), 0)
im.im.color_lut_3d(
lut_mode,
Image.Resampling.BILINEAR,
*self.generate_identity_table(table_channels, table_size),
)
@pytest.mark.parametrize(
"image_mode, lut_mode, table_channels, table_size",
[
("RGBA", "RGBA", 3, 3),
("RGBA", "RGBA", 4, 3),
("RGB", "HSV", 3, 3),
("RGB", "RGBA", 4, 3),
],
)
def test_correct_mode(
self, image_mode: str, lut_mode: str, table_channels: int, table_size: int
) -> None:
im = Image.new(image_mode, (10, 10), 0)
im.im.color_lut_3d(
"RGBA", Image.Resampling.BILINEAR, *self.generate_identity_table(4, 3)
lut_mode,
Image.Resampling.BILINEAR,
*self.generate_identity_table(table_channels, table_size),
)
def test_identities(self) -> None:
@ -321,6 +295,7 @@ class TestColorLut3DCoreAPI:
-1, 2, 2, 2, 2, 2,
])).load()
# fmt: on
assert transformed is not None
assert transformed[0, 0] == (0, 0, 255)
assert transformed[50, 50] == (0, 0, 255)
assert transformed[255, 0] == (0, 255, 255)
@ -341,6 +316,7 @@ class TestColorLut3DCoreAPI:
-3, 5, 5, 5, 5, 5,
])).load()
# fmt: on
assert transformed is not None
assert transformed[0, 0] == (0, 0, 255)
assert transformed[50, 50] == (0, 0, 255)
assert transformed[255, 0] == (0, 255, 255)
@ -354,10 +330,10 @@ class TestColorLut3DCoreAPI:
class TestColorLut3DFilter:
def test_wrong_args(self) -> None:
with pytest.raises(ValueError, match="should be either an integer"):
ImageFilter.Color3DLUT("small", [1])
ImageFilter.Color3DLUT("small", [1]) # type: ignore[arg-type]
with pytest.raises(ValueError, match="should be either an integer"):
ImageFilter.Color3DLUT((11, 11), [1])
ImageFilter.Color3DLUT((11, 11), [1]) # type: ignore[arg-type]
with pytest.raises(ValueError, match=r"in \[2, 65\] range"):
ImageFilter.Color3DLUT((11, 11, 1), [1])
@ -412,10 +388,12 @@ class TestColorLut3DFilter:
table = numpy.ones((7 * 6 * 5, 3), dtype=numpy.float16)
lut = ImageFilter.Color3DLUT((5, 6, 7), table)
assert isinstance(lut.table, numpy.ndarray)
assert lut.table.shape == (table.size,)
table = numpy.ones((7 * 6 * 5 * 3), dtype=numpy.float16)
lut = ImageFilter.Color3DLUT((5, 6, 7), table)
assert isinstance(lut.table, numpy.ndarray)
assert lut.table.shape == (table.size,)
# Check application

View File

@ -12,7 +12,7 @@ ORIGINAL_LIMIT = Image.MAX_IMAGE_PIXELS
class TestDecompressionBomb:
def teardown_method(self, method) -> None:
def teardown_method(self) -> None:
Image.MAX_IMAGE_PIXELS = ORIGINAL_LIMIT
def test_no_warning_small_file(self) -> None:

View File

@ -9,9 +9,9 @@ from PIL import _deprecate
"version, expected",
[
(
11,
"Old thing is deprecated and will be removed in Pillow 11 "
r"\(2024-10-15\)\. Use new thing instead\.",
12,
"Old thing is deprecated and will be removed in Pillow 12 "
r"\(2025-10-15\)\. Use new thing instead\.",
),
(
None,
@ -54,18 +54,18 @@ def test_old_version(deprecated: str, plural: bool, expected: str) -> None:
def test_plural() -> None:
expected = (
r"Old things are deprecated and will be removed in Pillow 11 \(2024-10-15\)\. "
r"Old things are deprecated and will be removed in Pillow 12 \(2025-10-15\)\. "
r"Use new thing instead\."
)
with pytest.warns(DeprecationWarning, match=expected):
_deprecate.deprecate("Old things", 11, "new thing", plural=True)
_deprecate.deprecate("Old things", 12, "new thing", plural=True)
def test_replacement_and_action() -> None:
expected = "Use only one of 'replacement' and 'action'"
with pytest.raises(ValueError, match=expected):
_deprecate.deprecate(
"Old thing", 11, replacement="new thing", action="Upgrade to new thing"
"Old thing", 12, replacement="new thing", action="Upgrade to new thing"
)
@ -78,16 +78,16 @@ def test_replacement_and_action() -> None:
)
def test_action(action: str) -> None:
expected = (
r"Old thing is deprecated and will be removed in Pillow 11 \(2024-10-15\)\. "
r"Old thing is deprecated and will be removed in Pillow 12 \(2025-10-15\)\. "
r"Upgrade to new thing\."
)
with pytest.warns(DeprecationWarning, match=expected):
_deprecate.deprecate("Old thing", 11, action=action)
_deprecate.deprecate("Old thing", 12, action=action)
def test_no_replacement_or_action() -> None:
expected = (
r"Old thing is deprecated and will be removed in Pillow 11 \(2024-10-15\)"
r"Old thing is deprecated and will be removed in Pillow 12 \(2025-10-15\)"
)
with pytest.warns(DeprecationWarning, match=expected):
_deprecate.deprecate("Old thing", 11)
_deprecate.deprecate("Old thing", 12)

View File

@ -10,11 +10,6 @@ from PIL import features
from .helper import skip_unless_feature
try:
from PIL import _webp
except ImportError:
pass
def test_check() -> None:
# Check the correctness of the convenience function
@ -23,7 +18,11 @@ def test_check() -> None:
for codec in features.codecs:
assert features.check_codec(codec) == features.check(codec)
for feature in features.features:
assert features.check_feature(feature) == features.check(feature)
if "webp" in feature:
with pytest.warns(DeprecationWarning):
assert features.check_feature(feature) == features.check(feature)
else:
assert features.check_feature(feature) == features.check(feature)
def test_version() -> None:
@ -37,8 +36,11 @@ def test_version() -> None:
else:
assert function(name) == version
if name != "PIL":
if name == "zlib" and version is not None:
version = version.replace(".zlib-ng", "")
if version is not None:
if name == "zlib" and features.check_feature("zlib_ng"):
version = re.sub(".zlib-ng$", "", version)
elif name == "libtiff":
version = re.sub("t$", "", version)
assert version is None or re.search(r"\d+(\.\d+)*$", version)
for module in features.modules:
@ -46,23 +48,26 @@ def test_version() -> None:
for codec in features.codecs:
test(codec, features.version_codec)
for feature in features.features:
test(feature, features.version_feature)
if "webp" in feature:
with pytest.warns(DeprecationWarning):
test(feature, features.version_feature)
else:
test(feature, features.version_feature)
@skip_unless_feature("webp")
def test_webp_transparency() -> None:
assert features.check("transp_webp") != _webp.WebPDecoderBuggyAlpha()
assert features.check("transp_webp") == _webp.HAVE_TRANSPARENCY
with pytest.warns(DeprecationWarning):
assert (features.check("transp_webp") or False) == features.check_module("webp")
@skip_unless_feature("webp")
def test_webp_mux() -> None:
assert features.check("webp_mux") == _webp.HAVE_WEBPMUX
with pytest.warns(DeprecationWarning):
assert (features.check("webp_mux") or False) == features.check_module("webp")
@skip_unless_feature("webp")
def test_webp_anim() -> None:
assert features.check("webp_anim") == _webp.HAVE_WEBPANIM
with pytest.warns(DeprecationWarning):
assert (features.check("webp_anim") or False) == features.check_module("webp")
@skip_unless_feature("libjpeg_turbo")

View File

@ -258,8 +258,8 @@ def test_apng_mode() -> None:
assert im.mode == "P"
im.seek(im.n_frames - 1)
im = im.convert("RGBA")
assert im.getpixel((0, 0)) == (255, 0, 0, 0)
assert im.getpixel((64, 32)) == (255, 0, 0, 0)
assert im.getpixel((0, 0)) == (0, 255, 0, 255)
assert im.getpixel((64, 32)) == (0, 255, 0, 255)
with Image.open("Tests/images/apng/mode_palette_1bit_alpha.png") as im:
assert im.mode == "P"
@ -706,10 +706,21 @@ def test_different_modes_in_later_frames(
assert reloaded.mode == mode
def test_apng_repeated_seeks_give_correct_info() -> None:
def test_different_durations(tmp_path: Path) -> None:
test_file = str(tmp_path / "temp.png")
with Image.open("Tests/images/apng/different_durations.png") as im:
for i in range(3):
for _ in range(3):
im.seek(0)
assert im.info["duration"] == 4000
im.seek(1)
assert im.info["duration"] == 1000
im.save(test_file, save_all=True)
with Image.open(test_file) as reloaded:
assert reloaded.info["duration"] == 4000
reloaded.seek(1)
assert reloaded.info["duration"] == 1000

View File

@ -64,6 +64,9 @@ def test_handler(tmp_path: Path) -> None:
im.fp.close()
return Image.new("RGB", (1, 1))
def is_loaded(self) -> bool:
return self.loaded
def save(self, im: Image.Image, fp: IO[bytes], filename: str) -> None:
self.saved = True
@ -71,13 +74,13 @@ def test_handler(tmp_path: Path) -> None:
BufrStubImagePlugin.register_handler(handler)
with Image.open(TEST_FILE) as im:
assert handler.opened
assert not handler.loaded
assert not handler.is_loaded()
im.load()
assert handler.loaded
assert handler.is_loaded()
temp_file = str(tmp_path / "temp.bufr")
im.save(temp_file)
assert handler.saved
BufrStubImagePlugin._handler = None
BufrStubImagePlugin.register_handler(None)

View File

@ -1,13 +1,9 @@
from __future__ import annotations
from typing import Literal
import pytest
from PIL import ContainerIO, Image
from .helper import hopper
TEST_FILE = "Tests/images/dummy.container"
@ -17,12 +13,19 @@ def test_sanity() -> None:
def test_isatty() -> None:
with hopper() as im:
container = ContainerIO.ContainerIO(im, 0, 0)
with open(TEST_FILE, "rb") as fh:
container = ContainerIO.ContainerIO(fh, 0, 0)
assert container.isatty() is False
def test_seekable() -> None:
with open(TEST_FILE, "rb") as fh:
container = ContainerIO.ContainerIO(fh, 0, 0)
assert container.seekable() is True
@pytest.mark.parametrize(
"mode, expected_position",
(
@ -31,7 +34,7 @@ def test_isatty() -> None:
(2, 100),
),
)
def test_seek_mode(mode: Literal[0, 1, 2], expected_position: int) -> None:
def test_seek_mode(mode: int, expected_position: int) -> None:
# Arrange
with open(TEST_FILE, "rb") as fh:
container = ContainerIO.ContainerIO(fh, 22, 100)
@ -44,6 +47,14 @@ def test_seek_mode(mode: Literal[0, 1, 2], expected_position: int) -> None:
assert container.tell() == expected_position
@pytest.mark.parametrize("bytesmode", (True, False))
def test_readable(bytesmode: bool) -> None:
with open(TEST_FILE, "rb" if bytesmode else "r") as fh:
container = ContainerIO.ContainerIO(fh, 0, 120)
assert container.readable() is True
@pytest.mark.parametrize("bytesmode", (True, False))
def test_read_n0(bytesmode: bool) -> None:
# Arrange
@ -51,7 +62,7 @@ def test_read_n0(bytesmode: bool) -> None:
container = ContainerIO.ContainerIO(fh, 22, 100)
# Act
container.seek(81)
assert container.seek(81) == 81
data = container.read()
# Assert
@ -67,7 +78,7 @@ def test_read_n(bytesmode: bool) -> None:
container = ContainerIO.ContainerIO(fh, 22, 100)
# Act
container.seek(81)
assert container.seek(81) == 81
data = container.read(3)
# Assert
@ -83,7 +94,7 @@ def test_read_eof(bytesmode: bool) -> None:
container = ContainerIO.ContainerIO(fh, 22, 100)
# Act
container.seek(100)
assert container.seek(100) == 100
data = container.read()
# Assert
@ -94,21 +105,65 @@ def test_read_eof(bytesmode: bool) -> None:
@pytest.mark.parametrize("bytesmode", (True, False))
def test_readline(bytesmode: bool) -> None:
# Arrange
with open(TEST_FILE, "rb" if bytesmode else "r") as fh:
container = ContainerIO.ContainerIO(fh, 0, 120)
# Act
data = container.readline()
# Assert
if bytesmode:
data = data.decode()
assert data == "This is line 1\n"
data = container.readline(4)
if bytesmode:
data = data.decode()
assert data == "This"
@pytest.mark.parametrize("bytesmode", (True, False))
def test_readlines(bytesmode: bool) -> None:
expected = [
"This is line 1\n",
"This is line 2\n",
"This is line 3\n",
"This is line 4\n",
"This is line 5\n",
"This is line 6\n",
"This is line 7\n",
"This is line 8\n",
]
with open(TEST_FILE, "rb" if bytesmode else "r") as fh:
container = ContainerIO.ContainerIO(fh, 0, 120)
data = container.readlines()
if bytesmode:
data = [line.decode() for line in data]
assert data == expected
assert container.seek(0) == 0
data = container.readlines(2)
if bytesmode:
data = [line.decode() for line in data]
assert data == expected[:2]
@pytest.mark.parametrize("bytesmode", (True, False))
def test_write(bytesmode: bool) -> None:
with open(TEST_FILE, "rb" if bytesmode else "r") as fh:
container = ContainerIO.ContainerIO(fh, 0, 120)
assert container.writable() is False
with pytest.raises(NotImplementedError):
container.write(b"" if bytesmode else "")
with pytest.raises(NotImplementedError):
container.writelines([])
with pytest.raises(NotImplementedError):
container.truncate()
@pytest.mark.parametrize("bytesmode", (True, False))
def test_iter(bytesmode: bool) -> None:
# Arrange
expected = [
"This is line 1\n",
@ -124,9 +179,21 @@ def test_readlines(bytesmode: bool) -> None:
container = ContainerIO.ContainerIO(fh, 0, 120)
# Act
data = container.readlines()
data = []
for line in container:
data.append(line)
# Assert
if bytesmode:
data = [line.decode() for line in data]
assert data == expected
@pytest.mark.parametrize("bytesmode", (True, False))
def test_file(bytesmode: bool) -> None:
with open(TEST_FILE, "rb" if bytesmode else "r") as fh:
container = ContainerIO.ContainerIO(fh, 0, 120)
assert isinstance(container.fileno(), int)
container.flush()
container.close()

View File

@ -36,6 +36,8 @@ def test_unclosed_file() -> None:
def test_closed_file() -> None:
with warnings.catch_warnings():
warnings.simplefilter("error")
im = Image.open(TEST_FILE)
im.load()
im.close()
@ -43,6 +45,8 @@ def test_closed_file() -> None:
def test_context_manager() -> None:
with warnings.catch_warnings():
warnings.simplefilter("error")
with Image.open(TEST_FILE) as im:
im.load()

View File

@ -152,7 +152,7 @@ def test_sanity_ati2_bc5u(image_path: str) -> None:
@pytest.mark.parametrize(
("image_path", "expected_path"),
"image_path, expected_path",
(
# hexeditted to be typeless
(TEST_FILE_DX10_BC5_TYPELESS, TEST_FILE_DX10_BC5_UNORM),
@ -248,7 +248,7 @@ def test_dx10_r8g8b8a8_unorm_srgb() -> None:
@pytest.mark.parametrize(
("mode", "size", "test_file"),
"mode, size, test_file",
[
("L", (128, 128), TEST_FILE_UNCOMPRESSED_L),
("LA", (128, 128), TEST_FILE_UNCOMPRESSED_L_WITH_ALPHA),
@ -373,7 +373,7 @@ def test_save_unsupported_mode(tmp_path: Path) -> None:
@pytest.mark.parametrize(
("mode", "test_file"),
"mode, test_file",
[
("L", "Tests/images/linear_gradient.png"),
("LA", "Tests/images/uncompressed_la.png"),

View File

@ -8,6 +8,7 @@ import pytest
from PIL import EpsImagePlugin, Image, UnidentifiedImageError, features
from .helper import (
assert_image_equal_tofile,
assert_image_similar,
assert_image_similar_tofile,
hopper,
@ -19,18 +20,18 @@ from .helper import (
HAS_GHOSTSCRIPT = EpsImagePlugin.has_ghostscript()
# Our two EPS test files (they are identical except for their bounding boxes)
FILE1 = "Tests/images/zero_bb.eps"
FILE2 = "Tests/images/non_zero_bb.eps"
FILE1 = "Tests/images/eps/zero_bb.eps"
FILE2 = "Tests/images/eps/non_zero_bb.eps"
# Due to palletization, we'll need to convert these to RGB after load
FILE1_COMPARE = "Tests/images/zero_bb.png"
FILE1_COMPARE_SCALE2 = "Tests/images/zero_bb_scale2.png"
FILE1_COMPARE = "Tests/images/eps/zero_bb.png"
FILE1_COMPARE_SCALE2 = "Tests/images/eps/zero_bb_scale2.png"
FILE2_COMPARE = "Tests/images/non_zero_bb.png"
FILE2_COMPARE_SCALE2 = "Tests/images/non_zero_bb_scale2.png"
FILE2_COMPARE = "Tests/images/eps/non_zero_bb.png"
FILE2_COMPARE_SCALE2 = "Tests/images/eps/non_zero_bb_scale2.png"
# EPS test files with binary preview
FILE3 = "Tests/images/binary_preview_map.eps"
FILE3 = "Tests/images/eps/binary_preview_map.eps"
# Three unsigned 32bit little-endian values:
# 0xC6D3D0C5 magic number
@ -80,9 +81,7 @@ simple_eps_file_with_long_binary_data = (
@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available")
@pytest.mark.parametrize(
("filename", "size"), ((FILE1, (460, 352)), (FILE2, (360, 252)))
)
@pytest.mark.parametrize("filename, size", ((FILE1, (460, 352)), (FILE2, (360, 252))))
@pytest.mark.parametrize("scale", (1, 2))
def test_sanity(filename: str, size: tuple[int, int], scale: int) -> None:
expected_size = tuple(s * scale for s in size)
@ -128,6 +127,15 @@ def test_binary_header_only() -> None:
EpsImagePlugin.EpsImageFile(data)
@pytest.mark.parametrize("prefix", (b"", simple_binary_header))
def test_simple_eps_file(prefix: bytes) -> None:
data = io.BytesIO(prefix + b"\n".join(simple_eps_file))
with Image.open(data) as img:
assert img.mode == "RGB"
assert img.size == (100, 100)
assert img.format == "EPS"
@pytest.mark.parametrize("prefix", (b"", simple_binary_header))
def test_missing_version_comment(prefix: bytes) -> None:
data = io.BytesIO(prefix + b"\n".join(simple_eps_file_without_version))
@ -143,23 +151,21 @@ def test_missing_boundingbox_comment(prefix: bytes) -> None:
@pytest.mark.parametrize("prefix", (b"", simple_binary_header))
def test_invalid_boundingbox_comment(prefix: bytes) -> None:
data = io.BytesIO(prefix + b"\n".join(simple_eps_file_with_invalid_boundingbox))
@pytest.mark.parametrize(
"file_lines",
(
simple_eps_file_with_invalid_boundingbox,
simple_eps_file_with_invalid_boundingbox_valid_imagedata,
),
)
def test_invalid_boundingbox_comment(
prefix: bytes, file_lines: tuple[bytes, ...]
) -> None:
data = io.BytesIO(prefix + b"\n".join(file_lines))
with pytest.raises(OSError, match="cannot determine EPS bounding box"):
EpsImagePlugin.EpsImageFile(data)
@pytest.mark.parametrize("prefix", (b"", simple_binary_header))
def test_invalid_boundingbox_comment_valid_imagedata_comment(prefix: bytes) -> None:
data = io.BytesIO(
prefix + b"\n".join(simple_eps_file_with_invalid_boundingbox_valid_imagedata)
)
with Image.open(data) as img:
assert img.mode == "RGB"
assert img.size == (100, 100)
assert img.format == "EPS"
@pytest.mark.parametrize("prefix", (b"", simple_binary_header))
def test_ascii_comment_too_long(prefix: bytes) -> None:
data = io.BytesIO(prefix + b"\n".join(simple_eps_file_with_long_ascii_comment))
@ -179,7 +185,7 @@ def test_load_long_binary_data(prefix: bytes) -> None:
data = io.BytesIO(prefix + b"\n".join(simple_eps_file_with_long_binary_data))
with Image.open(data) as img:
img.load()
assert img.mode == "RGB"
assert img.mode == "1"
assert img.size == (100, 100)
assert img.format == "EPS"
@ -189,7 +195,7 @@ def test_load_long_binary_data(prefix: bytes) -> None:
)
@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available")
def test_cmyk() -> None:
with Image.open("Tests/images/pil_sample_cmyk.eps") as cmyk_image:
with Image.open("Tests/images/eps/pil_sample_cmyk.eps") as cmyk_image:
assert cmyk_image.mode == "CMYK"
assert cmyk_image.size == (100, 100)
assert cmyk_image.format == "EPS"
@ -206,8 +212,8 @@ def test_cmyk() -> None:
@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available")
def test_showpage() -> None:
# See https://github.com/python-pillow/Pillow/issues/2615
with Image.open("Tests/images/reqd_showpage.eps") as plot_image:
with Image.open("Tests/images/reqd_showpage.png") as target:
with Image.open("Tests/images/eps/reqd_showpage.eps") as plot_image:
with Image.open("Tests/images/eps/reqd_showpage.png") as target:
# should not crash/hang
plot_image.load()
# fonts could be slightly different
@ -216,11 +222,11 @@ def test_showpage() -> None:
@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available")
def test_transparency() -> None:
with Image.open("Tests/images/reqd_showpage.eps") as plot_image:
with Image.open("Tests/images/eps/reqd_showpage.eps") as plot_image:
plot_image.load(transparency=True)
assert plot_image.mode == "RGBA"
with Image.open("Tests/images/reqd_showpage_transparency.png") as target:
with Image.open("Tests/images/eps/reqd_showpage_transparency.png") as target:
# fonts could be slightly different
assert_image_similar(plot_image, target, 6)
@ -247,9 +253,19 @@ def test_bytesio_object() -> None:
assert_image_similar(img, image1_scale1_compare, 5)
def test_1_mode() -> None:
with Image.open("Tests/images/1.eps") as im:
assert im.mode == "1"
@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available")
@pytest.mark.parametrize(
# These images have an "ImageData" descriptor.
"filename",
(
"Tests/images/eps/1.eps",
"Tests/images/eps/1_boundingbox_after_imagedata.eps",
"Tests/images/eps/1_second_imagedata.eps",
),
)
def test_1(filename: str) -> None:
with Image.open(filename) as im:
assert_image_equal_tofile(im, "Tests/images/eps/1.bmp")
def test_image_mode_not_supported(tmp_path: Path) -> None:
@ -304,7 +320,9 @@ def test_render_scale2() -> None:
@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available")
@pytest.mark.parametrize("filename", (FILE1, FILE2, "Tests/images/illu10_preview.eps"))
@pytest.mark.parametrize(
"filename", (FILE1, FILE2, "Tests/images/eps/illu10_preview.eps")
)
def test_resize(filename: str) -> None:
with Image.open(filename) as im:
new_size = (100, 100)
@ -329,46 +347,6 @@ def test_read_binary_preview() -> None:
pass
def test_readline_psfile(tmp_path: Path) -> None:
# check all the freaking line endings possible from the spec
# test_string = u'something\r\nelse\n\rbaz\rbif\n'
line_endings = ["\r\n", "\n", "\n\r", "\r"]
strings = ["something", "else", "baz", "bif"]
def _test_readline(t: EpsImagePlugin.PSFile, ending: str) -> None:
ending = f"Failure with line ending: {''.join(str(ord(s)) for s in ending)}"
assert t.readline().strip("\r\n") == "something", ending
assert t.readline().strip("\r\n") == "else", ending
assert t.readline().strip("\r\n") == "baz", ending
assert t.readline().strip("\r\n") == "bif", ending
def _test_readline_io_psfile(test_string: str, ending: str) -> None:
f = io.BytesIO(test_string.encode("latin-1"))
with pytest.warns(DeprecationWarning):
t = EpsImagePlugin.PSFile(f)
_test_readline(t, ending)
def _test_readline_file_psfile(test_string: str, ending: str) -> None:
f = str(tmp_path / "temp.txt")
with open(f, "wb") as w:
w.write(test_string.encode("latin-1"))
with open(f, "rb") as r:
with pytest.warns(DeprecationWarning):
t = EpsImagePlugin.PSFile(r)
_test_readline(t, ending)
for ending in line_endings:
s = ending.join(strings)
_test_readline_io_psfile(s, ending)
_test_readline_file_psfile(s, ending)
def test_psfile_deprecation() -> None:
with pytest.warns(DeprecationWarning):
EpsImagePlugin.PSFile(None)
@pytest.mark.parametrize("prefix", (b"", simple_binary_header))
@pytest.mark.parametrize(
"line_ending",
@ -386,10 +364,10 @@ def test_readline(prefix: bytes, line_ending: bytes) -> None:
@pytest.mark.parametrize(
"filename",
(
"Tests/images/illu10_no_preview.eps",
"Tests/images/illu10_preview.eps",
"Tests/images/illuCS6_no_preview.eps",
"Tests/images/illuCS6_preview.eps",
"Tests/images/eps/illu10_no_preview.eps",
"Tests/images/eps/illu10_preview.eps",
"Tests/images/eps/illuCS6_no_preview.eps",
"Tests/images/eps/illuCS6_preview.eps",
),
)
def test_open_eps(filename: str) -> None:
@ -401,7 +379,7 @@ def test_open_eps(filename: str) -> None:
@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available")
def test_emptyline() -> None:
# Test file includes an empty line in the header data
emptyline_file = "Tests/images/zero_bb_emptyline.eps"
emptyline_file = "Tests/images/eps/zero_bb_emptyline.eps"
with Image.open(emptyline_file) as image:
image.load()
@ -413,7 +391,7 @@ def test_emptyline() -> None:
@pytest.mark.timeout(timeout=5)
@pytest.mark.parametrize(
"test_file",
["Tests/images/timeout-d675703545fee17acab56e5fec644c19979175de.eps"],
["Tests/images/eps/timeout-d675703545fee17acab56e5fec644c19979175de.eps"],
)
def test_timeout(test_file: str) -> None:
with open(test_file, "rb") as f:
@ -425,20 +403,21 @@ def test_timeout(test_file: str) -> None:
def test_bounding_box_in_trailer() -> None:
# Check bounding boxes are parsed in the same way
# when specified in the header and the trailer
with Image.open("Tests/images/zero_bb_trailer.eps") as trailer_image, Image.open(
FILE1
) as header_image:
with (
Image.open("Tests/images/eps/zero_bb_trailer.eps") as trailer_image,
Image.open(FILE1) as header_image,
):
assert trailer_image.size == header_image.size
def test_eof_before_bounding_box() -> None:
with pytest.raises(OSError):
with Image.open("Tests/images/zero_bb_eof_before_boundingbox.eps"):
with Image.open("Tests/images/eps/zero_bb_eof_before_boundingbox.eps"):
pass
def test_invalid_data_after_eof() -> None:
with open("Tests/images/illuCS6_preview.eps", "rb") as f:
with open("Tests/images/eps/illuCS6_preview.eps", "rb") as f:
img_bytes = io.BytesIO(f.read() + b"\r\n%" + (b" " * 255))
with Image.open(img_bytes) as img:

View File

@ -65,6 +65,8 @@ def test_unclosed_file() -> None:
def test_closed_file() -> None:
with warnings.catch_warnings():
warnings.simplefilter("error")
im = Image.open(static_test_file)
im.load()
im.close()
@ -81,6 +83,8 @@ def test_seek_after_close() -> None:
def test_context_manager() -> None:
with warnings.catch_warnings():
warnings.simplefilter("error")
with Image.open(static_test_file) as im:
im.load()

View File

@ -1,9 +1,10 @@
from __future__ import annotations
import warnings
from collections.abc import Generator
from io import BytesIO
from pathlib import Path
from typing import Generator
from typing import Any
import pytest
@ -46,6 +47,8 @@ def test_unclosed_file() -> None:
def test_closed_file() -> None:
with warnings.catch_warnings():
warnings.simplefilter("error")
im = Image.open(TEST_GIF)
im.load()
im.close()
@ -53,6 +56,7 @@ def test_closed_file() -> None:
def test_seek_after_close() -> None:
im = Image.open("Tests/images/iss634.gif")
assert isinstance(im, GifImagePlugin.GifImageFile)
im.load()
im.close()
@ -66,6 +70,8 @@ def test_seek_after_close() -> None:
def test_context_manager() -> None:
with warnings.catch_warnings():
warnings.simplefilter("error")
with Image.open(TEST_GIF) as im:
im.load()
@ -352,7 +358,7 @@ def test_palette_434(tmp_path: Path) -> None:
def roundtrip(im: Image.Image, **kwargs: bool) -> Image.Image:
out = str(tmp_path / "temp.gif")
im.copy().save(out, **kwargs)
im.copy().save(out, "GIF", **kwargs)
reloaded = Image.open(out)
return reloaded
@ -377,7 +383,8 @@ def test_save_netpbm_bmp_mode(tmp_path: Path) -> None:
img = img.convert("RGB")
tempfile = str(tmp_path / "temp.gif")
GifImagePlugin._save_netpbm(img, 0, tempfile)
b = BytesIO()
GifImagePlugin._save_netpbm(img, b, tempfile)
with Image.open(tempfile) as reloaded:
assert_image_similar(img, reloaded.convert("RGB"), 0)
@ -388,7 +395,8 @@ def test_save_netpbm_l_mode(tmp_path: Path) -> None:
img = img.convert("L")
tempfile = str(tmp_path / "temp.gif")
GifImagePlugin._save_netpbm(img, 0, tempfile)
b = BytesIO()
GifImagePlugin._save_netpbm(img, b, tempfile)
with Image.open(tempfile) as reloaded:
assert_image_similar(img, reloaded.convert("L"), 0)
@ -648,7 +656,7 @@ def test_dispose2_palette(tmp_path: Path) -> None:
assert rgb_img.getpixel((50, 50)) == circle
# Check that frame transparency wasn't added unnecessarily
assert img._frame_transparency is None
assert getattr(img, "_frame_transparency") is None
def test_dispose2_diff(tmp_path: Path) -> None:
@ -975,7 +983,7 @@ def test_webp_background(tmp_path: Path) -> None:
out = str(tmp_path / "temp.gif")
# Test opaque WebP background
if features.check("webp") and features.check("webp_anim"):
if features.check("webp"):
with Image.open("Tests/images/hopper.webp") as im:
assert im.info["background"] == (255, 255, 255, 255)
im.save(out)
@ -1387,16 +1395,39 @@ def test_lzw_bits() -> None:
im.load()
def test_extents() -> None:
with Image.open("Tests/images/test_extents.gif") as im:
assert im.size == (100, 100)
@pytest.mark.parametrize(
"test_file, loading_strategy",
(
("test_extents.gif", GifImagePlugin.LoadingStrategy.RGB_AFTER_FIRST),
(
"test_extents.gif",
GifImagePlugin.LoadingStrategy.RGB_AFTER_DIFFERENT_PALETTE_ONLY,
),
(
"test_extents_transparency.gif",
GifImagePlugin.LoadingStrategy.RGB_AFTER_FIRST,
),
),
)
def test_extents(
test_file: str, loading_strategy: GifImagePlugin.LoadingStrategy
) -> None:
GifImagePlugin.LOADING_STRATEGY = loading_strategy
try:
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
def test_missing_background() -> None:
@ -1415,3 +1446,22 @@ def test_saving_rgba(tmp_path: Path) -> None:
with Image.open(out) as reloaded:
reloaded_rgba = reloaded.convert("RGBA")
assert reloaded_rgba.load()[0, 0][3] == 0
@pytest.mark.parametrize("params", ({}, {"disposal": 2, "optimize": False}))
def test_p_rgba(tmp_path: Path, params: dict[str, Any]) -> None:
out = str(tmp_path / "temp.gif")
im1 = Image.new("P", (100, 100))
d = ImageDraw.Draw(im1)
d.ellipse([(40, 40), (60, 60)], fill=1)
data = [0, 0, 0, 0, 0, 0, 0, 255] + [0, 0, 0, 0] * 254
im1.putpalette(data, "RGBA")
im2 = Image.new("P", (100, 100))
im2.putpalette(data, "RGBA")
im1.save(out, save_all=True, append_images=[im2], **params)
with Image.open(out) as reloaded:
assert reloaded.n_frames == 2

View File

@ -64,6 +64,9 @@ def test_handler(tmp_path: Path) -> None:
im.fp.close()
return Image.new("RGB", (1, 1))
def is_loaded(self) -> bool:
return self.loaded
def save(self, im: Image.Image, fp: IO[bytes], filename: str) -> None:
self.saved = True
@ -71,13 +74,13 @@ def test_handler(tmp_path: Path) -> None:
GribStubImagePlugin.register_handler(handler)
with Image.open(TEST_FILE) as im:
assert handler.opened
assert not handler.loaded
assert not handler.is_loaded()
im.load()
assert handler.loaded
assert handler.is_loaded()
temp_file = str(tmp_path / "temp.grib")
im.save(temp_file)
assert handler.saved
GribStubImagePlugin._handler = None
GribStubImagePlugin.register_handler(None)

View File

@ -66,6 +66,9 @@ def test_handler(tmp_path: Path) -> None:
im.fp.close()
return Image.new("RGB", (1, 1))
def is_loaded(self) -> bool:
return self.loaded
def save(self, im: Image.Image, fp: IO[bytes], filename: str) -> None:
self.saved = True
@ -73,13 +76,13 @@ def test_handler(tmp_path: Path) -> None:
Hdf5StubImagePlugin.register_handler(handler)
with Image.open(TEST_FILE) as im:
assert handler.opened
assert not handler.loaded
assert not handler.is_loaded()
im.load()
assert handler.loaded
assert handler.is_loaded()
temp_file = str(tmp_path / "temp.h5")
im.save(temp_file)
assert handler.saved
Hdf5StubImagePlugin._handler = None
Hdf5StubImagePlugin.register_handler(None)

View File

@ -21,6 +21,8 @@ def test_sanity() -> None:
with Image.open(TEST_FILE) as im:
# Assert that there is no unclosed file warning
with warnings.catch_warnings():
warnings.simplefilter("error")
im.load()
assert im.mode == "RGBA"
@ -63,8 +65,8 @@ def test_save_append_images(tmp_path: Path) -> None:
assert_image_similar_tofile(im, temp_file, 1)
with Image.open(temp_file) as reread:
reread.size = (16, 16, 2)
reread.load()
reread.size = (16, 16)
reread.load(2)
assert_image_equal(reread, provided_im)
@ -87,14 +89,21 @@ def test_sizes() -> None:
for w, h, r in im.info["sizes"]:
wr = w * r
hr = h * r
im.size = (w, h, r)
with pytest.warns(DeprecationWarning):
im.size = (w, h, r)
im.load()
assert im.mode == "RGBA"
assert im.size == (wr, hr)
# Test using load() with scale
im.size = (w, h)
im.load(scale=r)
assert im.mode == "RGBA"
assert im.size == (wr, hr)
# Check that we cannot load an incorrect size
with pytest.raises(ValueError):
im.size = (1, 1)
im.size = (1, 2)
def test_older_icon() -> None:
@ -105,8 +114,8 @@ def test_older_icon() -> None:
wr = w * r
hr = h * r
with Image.open("Tests/images/pillow2.icns") as im2:
im2.size = (w, h, r)
im2.load()
im2.size = (w, h)
im2.load(r)
assert im2.mode == "RGBA"
assert im2.size == (wr, hr)
@ -122,8 +131,8 @@ def test_jp2_icon() -> None:
wr = w * r
hr = h * r
with Image.open("Tests/images/pillow3.icns") as im2:
im2.size = (w, h, r)
im2.load()
im2.size = (w, h)
im2.load(r)
assert im2.mode == "RGBA"
assert im2.size == (wr, hr)

View File

@ -6,7 +6,7 @@ from pathlib import Path
import pytest
from PIL import IcoImagePlugin, Image, ImageDraw
from PIL import IcoImagePlugin, Image, ImageDraw, ImageFile
from .helper import assert_image_equal, assert_image_equal_tofile, hopper
@ -241,3 +241,29 @@ def test_draw_reloaded(tmp_path: Path) -> None:
with Image.open(outfile) as im:
assert_image_equal_tofile(im, "Tests/images/hopper_draw.ico")
def test_truncated_mask() -> None:
# 1 bpp
with open("Tests/images/hopper_mask.ico", "rb") as fp:
data = fp.read()
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"
# 32 bpp
output = io.BytesIO()
expected = hopper("RGBA")
expected.save(output, "ico", bitmap_format="bmp")
data = output.getvalue()[:-1]
with Image.open(io.BytesIO(data)) as im:
assert im.mode == "RGB"
finally:
ImageFile.LOAD_TRUNCATED_IMAGES = False

View File

@ -41,6 +41,8 @@ def test_unclosed_file() -> None:
def test_closed_file() -> None:
with warnings.catch_warnings():
warnings.simplefilter("error")
im = Image.open(TEST_IM)
im.load()
im.close()
@ -48,6 +50,8 @@ def test_closed_file() -> None:
def test_context_manager() -> None:
with warnings.catch_warnings():
warnings.simplefilter("error")
with Image.open(TEST_IM) as im:
im.load()

View File

@ -57,6 +57,7 @@ def test_getiptcinfo_fotostation() -> None:
iptc = IptcImagePlugin.getiptcinfo(im)
# Assert
assert iptc is not None
for tag in iptc.keys():
if tag[0] == 240:
return
@ -76,6 +77,16 @@ def test_getiptcinfo_zero_padding() -> None:
assert len(iptc) == 3
def test_getiptcinfo_tiff() -> None:
# Arrange
with Image.open("Tests/images/hopper.Lab.tif") as im:
# Act
iptc = IptcImagePlugin.getiptcinfo(im)
# Assert
assert iptc == {(1, 90): b"\x1b%G", (2, 0): b"\xcf\xc0"}
def test_getiptcinfo_tiff_none() -> None:
# Arrange
with Image.open("Tests/images/hopper.tif") as im:

View File

@ -154,7 +154,7 @@ class TestFileJpeg:
assert k > 0.9
def test_rgb(self) -> None:
def getchannels(im: JpegImagePlugin.JpegImageFile) -> tuple[int, int, int]:
def getchannels(im: JpegImagePlugin.JpegImageFile) -> tuple[int, ...]:
return tuple(v[0] for v in im.layer)
im = hopper()
@ -443,7 +443,9 @@ class TestFileJpeg:
assert_image(im1, im2.mode, im2.size)
def test_subsampling(self) -> None:
def getsampling(im: JpegImagePlugin.JpegImageFile):
def getsampling(
im: JpegImagePlugin.JpegImageFile,
) -> tuple[int, int, int, int, int, int]:
layer = im.layer
return layer[0][1:3] + layer[1][1:3] + layer[2][1:3]
@ -539,12 +541,12 @@ class TestFileJpeg:
@mark_if_feature_version(
pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing"
)
def test_qtables(self, tmp_path: Path) -> None:
def test_qtables(self) -> None:
def _n_qtables_helper(n: int, test_file: str) -> None:
b = BytesIO()
with Image.open(test_file) as im:
f = str(tmp_path / "temp.jpg")
im.save(f, qtables=[[n] * 64] * n)
with Image.open(f) as im:
im.save(b, "JPEG", qtables=[[n] * 64] * n)
with Image.open(b) as im:
assert len(im.quantization) == n
reloaded = self.roundtrip(im, qtables="keep")
assert im.quantization == reloaded.quantization
@ -699,7 +701,7 @@ class TestFileJpeg:
def test_save_cjpeg(self, tmp_path: Path) -> None:
with Image.open(TEST_FILE) as img:
tempfile = str(tmp_path / "temp.jpg")
JpegImagePlugin._save_cjpeg(img, 0, tempfile)
JpegImagePlugin._save_cjpeg(img, BytesIO(), tempfile)
# Default save quality is 75%, so a tiny bit of difference is alright
assert_image_similar_tofile(img, tempfile, 17)
@ -838,7 +840,7 @@ class TestFileJpeg:
with Image.open("Tests/images/no-dpi-in-exif.jpg") as im:
# Act / Assert
# "When the image resolution is unknown, 72 [dpi] is designated."
# https://web.archive.org/web/20240227115053/https://exiv2.org/tags.html
# https://exiv2.org/tags.html
assert im.info.get("dpi") == (72, 72)
def test_invalid_exif(self) -> None:
@ -859,6 +861,8 @@ class TestFileJpeg:
out = str(tmp_path / "out.jpg")
with warnings.catch_warnings():
warnings.simplefilter("error")
im.save(out, exif=exif)
with Image.open(out) as reloaded:
@ -881,7 +885,7 @@ class TestFileJpeg:
def test_multiple_exif(self) -> None:
with Image.open("Tests/images/multiple_exif.jpg") as im:
assert im.info["exif"] == b"Exif\x00\x00firstsecond"
assert im.getexif()[270] == "firstsecond"
@mark_if_feature_version(
pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing"
@ -928,24 +932,25 @@ class TestFileJpeg:
with Image.open("Tests/images/icc-after-SOF.jpg") as im:
assert im.info["icc_profile"] == b"profile"
def test_jpeg_magic_number(self) -> None:
def test_jpeg_magic_number(self, monkeypatch: pytest.MonkeyPatch) -> None:
size = 4097
buffer = BytesIO(b"\xFF" * size) # Many xFF bytes
buffer.max_pos = 0
max_pos = 0
orig_read = buffer.read
def read(n=-1):
def read(n: int | None = -1) -> bytes:
nonlocal max_pos
res = orig_read(n)
buffer.max_pos = max(buffer.max_pos, buffer.tell())
max_pos = max(max_pos, buffer.tell())
return res
buffer.read = read
monkeypatch.setattr(buffer, "read", read)
with pytest.raises(UnidentifiedImageError):
with Image.open(buffer):
pass
# Assert the entire file has not been read
assert 0 < buffer.max_pos < size
assert 0 < max_pos < size
def test_getxmp(self) -> None:
with Image.open("Tests/images/xmp_test.jpg") as im:
@ -956,6 +961,7 @@ class TestFileJpeg:
):
assert im.getxmp() == {}
else:
assert "xmp" in im.info
xmp = im.getxmp()
description = xmp["xmpmeta"]["RDF"]["Description"]
@ -998,12 +1004,34 @@ class TestFileJpeg:
else:
assert im.getxmp() == {"xmpmeta": None}
def test_save_xmp(self, tmp_path: Path) -> None:
f = str(tmp_path / "temp.jpg")
im = hopper()
im.save(f, xmp=b"XMP test")
with Image.open(f) as reloaded:
assert reloaded.info["xmp"] == b"XMP test"
# Check that XMP is not saved from image info
reloaded.save(f)
with Image.open(f) as reloaded:
assert "xmp" not in reloaded.info
im.save(f, xmp=b"1" * 65504)
with Image.open(f) as reloaded:
assert reloaded.info["xmp"] == b"1" * 65504
with pytest.raises(ValueError):
im.save(f, xmp=b"1" * 65505)
@pytest.mark.timeout(timeout=1)
def test_eof(self) -> 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):
def decode(self, buffer: bytes) -> tuple[int, int]:
def decode(
self, buffer: bytes | Image.SupportsArrayInterface
) -> tuple[int, int]:
return 0, 0
Image.register_decoder("INFINITE", InfiniteMockPyDecoder)
@ -1026,13 +1054,16 @@ class TestFileJpeg:
# SOI, EOI
for marker in b"\xff\xd8", b"\xff\xd9":
assert marker in data[1] and marker in data[2]
assert marker in data[1]
assert marker in data[2]
# DHT, DQT
for marker in b"\xff\xc4", b"\xff\xdb":
assert marker in data[1] and marker not in data[2]
assert marker in data[1]
assert marker not in data[2]
# SOF0, SOS, APP0 (JFIF header)
for marker in b"\xff\xc0", b"\xff\xda", b"\xff\xe0":
assert marker not in data[1] and marker in data[2]
assert marker not in data[1]
assert marker in data[2]
with Image.open(BytesIO(data[0])) as interchange_im:
with Image.open(BytesIO(data[1] + data[2])) as combined_im:
@ -1040,8 +1071,10 @@ class TestFileJpeg:
def test_repr_jpeg(self) -> None:
im = hopper()
b = im._repr_jpeg_()
assert b is not None
with Image.open(BytesIO(im._repr_jpeg_())) as repr_jpeg:
with Image.open(BytesIO(b)) as repr_jpeg:
assert repr_jpeg.format == "JPEG"
assert_image_similar(im, repr_jpeg, 17)
@ -1050,6 +1083,13 @@ class TestFileJpeg:
assert im._repr_jpeg_() is None
def test_deprecation(self) -> None:
with Image.open(TEST_FILE) as im:
with pytest.warns(DeprecationWarning):
assert im.huffman_ac == {}
with pytest.warns(DeprecationWarning):
assert im.huffman_dc == {}
@pytest.mark.skipif(not is_win32(), reason="Windows only")
@skip_unless_feature("jpg")

View File

@ -2,6 +2,7 @@ from __future__ import annotations
import os
import re
from collections.abc import Generator
from io import BytesIO
from pathlib import Path
from typing import Any
@ -29,8 +30,16 @@ EXTRA_DIR = "Tests/images/jpeg2000"
pytestmark = skip_unless_feature("jpg_2000")
test_card = Image.open("Tests/images/test-card.png")
test_card.load()
@pytest.fixture
def card() -> Generator[ImageFile.ImageFile, None, None]:
with Image.open("Tests/images/test-card.png") as im:
im.load()
try:
yield im
finally:
im.close()
# OpenJPEG 2.0.0 outputs this debugging message sometimes; we should
# ignore it---it doesn't represent a test failure.
@ -74,76 +83,76 @@ def test_invalid_file() -> None:
Jpeg2KImagePlugin.Jpeg2KImageFile(invalid_file)
def test_bytesio() -> None:
def test_bytesio(card: ImageFile.ImageFile) -> None:
with open("Tests/images/test-card-lossless.jp2", "rb") as f:
data = BytesIO(f.read())
with Image.open(data) as im:
im.load()
assert_image_similar(im, test_card, 1.0e-3)
assert_image_similar(im, card, 1.0e-3)
# These two test pre-written JPEG 2000 files that were not written with
# PIL (they were made using Adobe Photoshop)
def test_lossless(tmp_path: Path) -> None:
def test_lossless(card: ImageFile.ImageFile, tmp_path: Path) -> None:
with Image.open("Tests/images/test-card-lossless.jp2") as im:
im.load()
outfile = str(tmp_path / "temp_test-card.png")
im.save(outfile)
assert_image_similar(im, test_card, 1.0e-3)
assert_image_similar(im, card, 1.0e-3)
def test_lossy_tiled() -> None:
assert_image_similar_tofile(
test_card, "Tests/images/test-card-lossy-tiled.jp2", 2.0
)
def test_lossy_tiled(card: ImageFile.ImageFile) -> None:
assert_image_similar_tofile(card, "Tests/images/test-card-lossy-tiled.jp2", 2.0)
def test_lossless_rt() -> None:
im = roundtrip(test_card)
assert_image_equal(im, test_card)
def test_lossless_rt(card: ImageFile.ImageFile) -> None:
im = roundtrip(card)
assert_image_equal(im, card)
def test_lossy_rt() -> None:
im = roundtrip(test_card, quality_layers=[20])
assert_image_similar(im, test_card, 2.0)
def test_lossy_rt(card: ImageFile.ImageFile) -> None:
im = roundtrip(card, quality_layers=[20])
assert_image_similar(im, card, 2.0)
def test_tiled_rt() -> None:
im = roundtrip(test_card, tile_size=(128, 128))
assert_image_equal(im, test_card)
def test_tiled_rt(card: ImageFile.ImageFile) -> None:
im = roundtrip(card, tile_size=(128, 128))
assert_image_equal(im, card)
def test_tiled_offset_rt() -> None:
im = roundtrip(test_card, tile_size=(128, 128), tile_offset=(0, 0), offset=(32, 32))
assert_image_equal(im, test_card)
def test_tiled_offset_rt(card: ImageFile.ImageFile) -> None:
im = roundtrip(card, tile_size=(128, 128), tile_offset=(0, 0), offset=(32, 32))
assert_image_equal(im, card)
def test_tiled_offset_too_small() -> None:
def test_tiled_offset_too_small(card: ImageFile.ImageFile) -> None:
with pytest.raises(ValueError):
roundtrip(test_card, tile_size=(128, 128), tile_offset=(0, 0), offset=(128, 32))
roundtrip(card, tile_size=(128, 128), tile_offset=(0, 0), offset=(128, 32))
def test_irreversible_rt() -> None:
im = roundtrip(test_card, irreversible=True, quality_layers=[20])
assert_image_similar(im, test_card, 2.0)
def test_irreversible_rt(card: ImageFile.ImageFile) -> None:
im = roundtrip(card, irreversible=True, quality_layers=[20])
assert_image_similar(im, card, 2.0)
def test_prog_qual_rt() -> None:
im = roundtrip(test_card, quality_layers=[60, 40, 20], progression="LRCP")
assert_image_similar(im, test_card, 2.0)
def test_prog_qual_rt(card: ImageFile.ImageFile) -> None:
im = roundtrip(card, quality_layers=[60, 40, 20], progression="LRCP")
assert_image_similar(im, card, 2.0)
def test_prog_res_rt() -> None:
im = roundtrip(test_card, num_resolutions=8, progression="RLCP")
assert_image_equal(im, test_card)
def test_prog_res_rt(card: ImageFile.ImageFile) -> None:
im = roundtrip(card, num_resolutions=8, progression="RLCP")
assert_image_equal(im, card)
@pytest.mark.parametrize("num_resolutions", range(2, 6))
def test_default_num_resolutions(num_resolutions: int) -> None:
def test_default_num_resolutions(
card: ImageFile.ImageFile, num_resolutions: int
) -> None:
d = 1 << (num_resolutions - 1)
im = test_card.resize((d - 1, d - 1))
im = card.resize((d - 1, d - 1))
with pytest.raises(OSError):
roundtrip(im, num_resolutions=num_resolutions)
reloaded = roundtrip(im)
@ -182,6 +191,15 @@ def test_restricted_icc_profile() -> None:
ImageFile.LOAD_TRUNCATED_IMAGES = False
@pytest.mark.skipif(
not os.path.exists(EXTRA_DIR), reason="Extra image files not installed"
)
def test_unknown_colorspace() -> None:
with Image.open(f"{EXTRA_DIR}/file8.jp2") as im:
im.load()
assert im.mode == "L"
def test_header_errors() -> None:
for path in (
"Tests/images/invalid_header_length.jp2",
@ -196,31 +214,31 @@ def test_header_errors() -> None:
pass
def test_layers_type(tmp_path: Path) -> None:
def test_layers_type(card: ImageFile.ImageFile, tmp_path: Path) -> None:
outfile = str(tmp_path / "temp_layers.jp2")
for quality_layers in [[100, 50, 10], (100, 50, 10), None]:
test_card.save(outfile, quality_layers=quality_layers)
card.save(outfile, quality_layers=quality_layers)
for quality_layers_str in ["quality_layers", ("100", "50", "10")]:
with pytest.raises(ValueError):
test_card.save(outfile, quality_layers=quality_layers_str)
card.save(outfile, quality_layers=quality_layers_str)
def test_layers() -> None:
def test_layers(card: ImageFile.ImageFile) -> None:
out = BytesIO()
test_card.save(out, "JPEG2000", quality_layers=[100, 50, 10], progression="LRCP")
card.save(out, "JPEG2000", quality_layers=[100, 50, 10], progression="LRCP")
out.seek(0)
with Image.open(out) as im:
im.layers = 1
im.load()
assert_image_similar(im, test_card, 13)
assert_image_similar(im, card, 13)
out.seek(0)
with Image.open(out) as im:
im.layers = 3
im.load()
assert_image_similar(im, test_card, 0.4)
assert_image_similar(im, card, 0.4)
@pytest.mark.parametrize(
@ -233,27 +251,33 @@ def test_layers() -> None:
("foo.jp2", {"no_jp2": True}, 0, b"\xff\x4f"),
("foo.j2k", {"no_jp2": False}, 0, b"\xff\x4f"),
("foo.jp2", {"no_jp2": False}, 4, b"jP"),
("foo.jp2", {"no_jp2": False}, 4, b"jP"),
(None, {"no_jp2": False}, 4, b"jP"),
),
)
def test_no_jp2(name: str, args: dict[str, bool], offset: int, data: bytes) -> None:
def test_no_jp2(
card: ImageFile.ImageFile,
name: str,
args: dict[str, bool],
offset: int,
data: bytes,
) -> None:
out = BytesIO()
if name:
out.name = name
test_card.save(out, "JPEG2000", **args)
card.save(out, "JPEG2000", **args)
out.seek(offset)
assert out.read(2) == data
def test_mct() -> None:
def test_mct(card: ImageFile.ImageFile) -> None:
# Three component
for val in (0, 1):
out = BytesIO()
test_card.save(out, "JPEG2000", mct=val, no_jp2=True)
card.save(out, "JPEG2000", mct=val, no_jp2=True)
assert out.getvalue()[59] == val
with Image.open(out) as im:
assert_image_similar(im, test_card, 1.0e-3)
assert_image_similar(im, card, 1.0e-3)
# Single component should have MCT disabled
for val in (0, 1):
@ -335,9 +359,15 @@ def test_issue_6194() -> None:
assert im.getpixel((5, 5)) == 31
def test_unknown_j2k_mode() -> None:
with pytest.raises(UnidentifiedImageError):
with Image.open("Tests/images/unknown_mode.j2k"):
pass
def test_unbound_local() -> None:
# prepatch, a malformed jp2 file could cause an UnboundLocalError exception.
with pytest.raises(OSError):
with pytest.raises(UnidentifiedImageError):
with Image.open("Tests/images/unbound_variable.jp2"):
pass
@ -385,6 +415,13 @@ def test_pclr() -> None:
assert len(im.palette.colors) == 256
assert im.palette.colors[(255, 255, 255)] == 0
with Image.open(
f"{EXTRA_DIR}/147af3f1083de4393666b7d99b01b58b_signal_sigsegv_130c531_6155_5136.jp2"
) as im:
assert im.mode == "P"
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:
@ -397,22 +434,22 @@ def test_comment() -> None:
pass
def test_save_comment() -> None:
def test_save_comment(card: ImageFile.ImageFile) -> None:
for comment in ("Created by Pillow", b"Created by Pillow"):
out = BytesIO()
test_card.save(out, "JPEG2000", comment=comment)
card.save(out, "JPEG2000", comment=comment)
with Image.open(out) as im:
assert im.info["comment"] == b"Created by Pillow"
out = BytesIO()
long_comment = b" " * 65531
test_card.save(out, "JPEG2000", comment=long_comment)
card.save(out, "JPEG2000", comment=long_comment)
with Image.open(out) as im:
assert im.info["comment"] == long_comment
with pytest.raises(ValueError):
test_card.save(out, "JPEG2000", comment=long_comment + b" ")
card.save(out, "JPEG2000", comment=long_comment + b" ")
@pytest.mark.parametrize(
@ -435,10 +472,10 @@ def test_crashes(test_file: str) -> None:
@skip_unless_feature_version("jpg_2000", "2.4.0")
def test_plt_marker() -> None:
def test_plt_marker(card: ImageFile.ImageFile) -> None:
# Search the start of the codesteam for PLT
out = BytesIO()
test_card.save(out, "JPEG2000", no_jp2=True, plt=True)
card.save(out, "JPEG2000", no_jp2=True, plt=True)
out.seek(0)
while True:
marker = out.read(2)
@ -460,7 +497,7 @@ def test_plt_marker() -> None:
out.seek(length - 2, os.SEEK_CUR)
def test_9bit():
def test_9bit() -> None:
with Image.open("Tests/images/9bit.j2k") as im:
assert im.mode == "I;16"
assert im.size == (128, 128)

View File

@ -54,7 +54,7 @@ class TestFileLibTiff(LibTiffTestCase):
def test_version(self) -> None:
version = features.version_codec("libtiff")
assert version is not None
assert re.search(r"\d+\.\d+\.\d+$", version)
assert re.search(r"\d+\.\d+\.\d+t?$", version)
def test_g4_tiff(self, tmp_path: Path) -> None:
"""Test the ordinary file path load path"""
@ -92,11 +92,22 @@ class TestFileLibTiff(LibTiffTestCase):
def test_g4_non_disk_file_object(self, tmp_path: Path) -> None:
"""Testing loading from non-disk non-BytesIO file object"""
test_file = "Tests/images/hopper_g4_500.tif"
s = io.BytesIO()
with open(test_file, "rb") as f:
s.write(f.read())
s.seek(0)
r = io.BufferedReader(s)
data = f.read()
class NonBytesIO(io.RawIOBase):
def read(self, size: int = -1) -> bytes:
nonlocal data
if size == -1:
size = len(data)
result = data[:size]
data = data[size:]
return result
def readable(self) -> bool:
return True
r = io.BufferedReader(NonBytesIO())
with Image.open(r) as im:
assert im.size == (500, 500)
self._assert_noerr(tmp_path, im)
@ -229,9 +240,10 @@ class TestFileLibTiff(LibTiffTestCase):
new_ifd = TiffImagePlugin.ImageFileDirectory_v2()
for tag, info in core_items.items():
assert info.type is not None
if info.length == 1:
new_ifd[tag] = values[info.type]
if info.length == 0:
elif not info.length:
new_ifd[tag] = tuple(values[info.type] for _ in range(3))
else:
new_ifd[tag] = tuple(values[info.type] for _ in range(info.length))
@ -685,13 +697,18 @@ class TestFileLibTiff(LibTiffTestCase):
assert reloaded.tag_v2[530] == (1, 1)
assert reloaded.tag_v2[532] == (0, 255, 128, 255, 128, 255)
def test_exif_ifd(self, tmp_path: Path) -> None:
outfile = str(tmp_path / "temp.tif")
def test_exif_ifd(self) -> None:
out = io.BytesIO()
with Image.open("Tests/images/tiff_adobe_deflate.tif") as im:
assert im.tag_v2[34665] == 125456
im.save(outfile)
im.save(out, "TIFF")
with Image.open(outfile) as reloaded:
with Image.open(out) as reloaded:
assert 34665 not in reloaded.tag_v2
im.save(out, "TIFF", tiffinfo={34665: 125456})
with Image.open(out) as reloaded:
if Image.core.libtiff_support_custom_tags:
assert reloaded.tag_v2[34665] == 125456
@ -1043,7 +1060,11 @@ class TestFileLibTiff(LibTiffTestCase):
],
)
def test_wrong_bits_per_sample(
self, file_name: str, mode: str, size: tuple[int, int], tile
self,
file_name: str,
mode: str,
size: tuple[int, int],
tile: list[tuple[str, tuple[int, int, int, int], int, tuple[Any, ...]]],
) -> None:
with Image.open("Tests/images/" + file_name) as im:
assert im.mode == mode
@ -1077,6 +1098,25 @@ class TestFileLibTiff(LibTiffTestCase):
assert_image_similar(base_im, im, 0.7)
@pytest.mark.parametrize(
"test_file",
[
"Tests/images/old-style-jpeg-compression-no-samplesperpixel.tif",
"Tests/images/old-style-jpeg-compression.tif",
],
)
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())
# load image before exif
with Image.open(open(test_file, "rb", buffering=1048576)) as im2:
im2.load()
exif_after_load = dict(im2.getexif())
assert exif == exif_after_load
@pytest.mark.valgrind_known_error(reason="Backtrace in Python Core")
def test_sampleformat_not_corrupted(self) -> None:
# Assert that a TIFF image with SampleFormat=UINT tag is not corrupted
@ -1130,7 +1170,7 @@ class TestFileLibTiff(LibTiffTestCase):
arguments: dict[str, str | int] = {"compression": "tiff_adobe_deflate"}
if argument:
arguments["strip_size"] = 2**18
im.save(out, **arguments)
im.save(out, "TIFF", **arguments)
with Image.open(out) as im:
assert isinstance(im, TiffImagePlugin.TiffImageFile)

View File

@ -2,11 +2,11 @@ from __future__ import annotations
import warnings
from io import BytesIO
from typing import Any, cast
from typing import Any
import pytest
from PIL import Image, MpoImagePlugin
from PIL import Image, ImageFile, MpoImagePlugin
from .helper import (
assert_image_equal,
@ -20,11 +20,11 @@ test_files = ["Tests/images/sugarshack.mpo", "Tests/images/frozenpond.mpo"]
pytestmark = skip_unless_feature("jpg")
def roundtrip(im: Image.Image, **options: Any) -> MpoImagePlugin.MpoImageFile:
def roundtrip(im: Image.Image, **options: Any) -> ImageFile.ImageFile:
out = BytesIO()
im.save(out, "MPO", **options)
out.seek(0)
return cast(MpoImagePlugin.MpoImageFile, Image.open(out))
return Image.open(out)
@pytest.mark.parametrize("test_file", test_files)
@ -48,6 +48,8 @@ def test_unclosed_file() -> None:
def test_closed_file() -> None:
with warnings.catch_warnings():
warnings.simplefilter("error")
im = Image.open(test_files[0])
im.load()
im.close()
@ -63,6 +65,8 @@ def test_seek_after_close() -> None:
def test_context_manager() -> None:
with warnings.catch_warnings():
warnings.simplefilter("error")
with Image.open(test_files[0]) as im:
im.load()
@ -85,7 +89,9 @@ def test_exif(test_file: str) -> None:
im_reloaded = roundtrip(im_original, save_all=True, exif=im_original.getexif())
for im in (im_original, im_reloaded):
assert isinstance(im, MpoImagePlugin.MpoImageFile)
info = im._getexif()
assert info is not None
assert info[272] == "Nintendo 3DS"
assert info[296] == 2
assert info[34665] == 188
@ -226,6 +232,17 @@ def test_eoferror() -> None:
im.seek(n_frames - 1)
def test_adopt_jpeg() -> None:
with Image.open("Tests/images/hopper.jpg") as im:
with pytest.raises(ValueError):
MpoImagePlugin.MpoImageFile.adopt(im)
def test_ultra_hdr() -> None:
with Image.open("Tests/images/ultrahdr.jpg") as im:
assert im.format == "JPEG"
@pytest.mark.parametrize("test_file", test_files)
def test_image_grab(test_file: str) -> None:
with Image.open(test_file) as im:
@ -270,6 +287,8 @@ def test_save_all() -> None:
im_reloaded = roundtrip(im, save_all=True, append_images=[im2])
assert_image_equal(im, im_reloaded)
assert isinstance(im_reloaded, MpoImagePlugin.MpoImageFile)
assert im_reloaded.mpinfo is not None
assert im_reloaded.mpinfo[45056] == b"0100"
im_reloaded.seek(1)
@ -278,3 +297,15 @@ def test_save_all() -> None:
# Test that a single frame image will not be saved as an MPO
jpg = roundtrip(im, save_all=True)
assert "mp" not in jpg.info
def test_save_xmp() -> None:
im = Image.new("RGB", (1, 1))
im2 = Image.new("RGB", (1, 1), "#f00")
im2.encoderinfo = {"xmp": b"Second frame"}
im_reloaded = roundtrip(im, xmp=b"First frame", save_all=True, append_images=[im2])
assert im_reloaded.info["xmp"] == b"First frame"
im_reloaded.seek(1)
assert im_reloaded.info["xmp"] == b"Second frame"

View File

@ -76,6 +76,7 @@ def test_pil184() -> None:
def test_1px_width(tmp_path: Path) -> None:
im = Image.new("L", (1, 256))
px = im.load()
assert px is not None
for y in range(256):
px[0, y] = y
_roundtrip(tmp_path, im)
@ -84,6 +85,7 @@ def test_1px_width(tmp_path: Path) -> None:
def test_large_count(tmp_path: Path) -> None:
im = Image.new("L", (256, 1))
px = im.load()
assert px is not None
for x in range(256):
px[x, 0] = x // 67 * 67
_roundtrip(tmp_path, im)
@ -101,6 +103,7 @@ def _test_buffer_overflow(tmp_path: Path, im: Image.Image, size: int = 1024) ->
def test_break_in_count_overflow(tmp_path: Path) -> None:
im = Image.new("L", (256, 5))
px = im.load()
assert px is not None
for y in range(4):
for x in range(256):
px[x, y] = x % 128
@ -110,6 +113,7 @@ def test_break_in_count_overflow(tmp_path: Path) -> None:
def test_break_one_in_loop(tmp_path: Path) -> None:
im = Image.new("L", (256, 5))
px = im.load()
assert px is not None
for y in range(5):
for x in range(256):
px[x, y] = x % 128
@ -119,6 +123,7 @@ def test_break_one_in_loop(tmp_path: Path) -> None:
def test_break_many_in_loop(tmp_path: Path) -> None:
im = Image.new("L", (256, 5))
px = im.load()
assert px is not None
for y in range(4):
for x in range(256):
px[x, y] = x % 128
@ -130,6 +135,7 @@ def test_break_many_in_loop(tmp_path: Path) -> None:
def test_break_one_at_end(tmp_path: Path) -> None:
im = Image.new("L", (256, 5))
px = im.load()
assert px is not None
for y in range(5):
for x in range(256):
px[x, y] = x % 128
@ -140,6 +146,7 @@ def test_break_one_at_end(tmp_path: Path) -> None:
def test_break_many_at_end(tmp_path: Path) -> None:
im = Image.new("L", (256, 5))
px = im.load()
assert px is not None
for y in range(5):
for x in range(256):
px[x, y] = x % 128
@ -152,6 +159,7 @@ def test_break_many_at_end(tmp_path: Path) -> None:
def test_break_padding(tmp_path: Path) -> None:
im = Image.new("L", (257, 5))
px = im.load()
assert px is not None
for y in range(5):
for x in range(257):
px[x, y] = x % 128

View File

@ -5,8 +5,9 @@ import os
import os.path
import tempfile
import time
from collections.abc import Generator
from pathlib import Path
from typing import Any, Generator
from typing import Any
import pytest
@ -117,7 +118,7 @@ def test_dpi(params: dict[str, int | tuple[int, int]], tmp_path: Path) -> None:
im = hopper()
outfile = str(tmp_path / "temp.pdf")
im.save(outfile, **params)
im.save(outfile, "PDF", **params)
with open(outfile, "rb") as fp:
contents = fp.read()
@ -228,6 +229,7 @@ def test_pdf_append_fails_on_nonexistent_file() -> None:
def check_pdf_pages_consistency(pdf: PdfParser.PdfParser) -> None:
assert pdf.pages_ref is not None
pages_info = pdf.read_indirect(pdf.pages_ref)
assert b"Parent" not in pages_info
assert b"Kids" in pages_info

View File

@ -41,7 +41,7 @@ MAGIC = PngImagePlugin._MAGIC
def chunk(cid: bytes, *data: bytes) -> bytes:
test_file = BytesIO()
PngImagePlugin.putchunk(*(test_file, cid) + data)
PngImagePlugin.putchunk(test_file, cid, *data)
return test_file.getvalue()
@ -346,6 +346,8 @@ class TestFilePng:
with Image.open(TEST_PNG_FILE) as im:
# Assert that there is no unclosed file warning
with warnings.catch_warnings():
warnings.simplefilter("error")
im.verify()
with Image.open(TEST_PNG_FILE) as im:
@ -432,8 +434,10 @@ class TestFilePng:
im = roundtrip(im, pnginfo=info)
assert im.info == {"spam": "Eggs", "eggs": "Spam"}
assert im.text == {"spam": "Eggs", "eggs": "Spam"}
assert isinstance(im.text["spam"], PngImagePlugin.iTXt)
assert im.text["spam"].lang == "en"
assert im.text["spam"].tkey == "Spam"
assert isinstance(im.text["eggs"], PngImagePlugin.iTXt)
assert im.text["eggs"].lang == "en"
assert im.text["eggs"].tkey == "Eggs"
@ -543,8 +547,10 @@ class TestFilePng:
def test_repr_png(self) -> None:
im = hopper()
b = im._repr_png_()
assert b is not None
with Image.open(BytesIO(im._repr_png_())) as repr_png:
with Image.open(BytesIO(b)) as repr_png:
assert repr_png.format == "PNG"
assert_image_equal(im, repr_png)
@ -663,11 +669,12 @@ class TestFilePng:
png.call(cid, 0, 0)
ImageFile.LOAD_TRUNCATED_IMAGES = False
def test_specify_bits(self, tmp_path: Path) -> None:
@pytest.mark.parametrize("save_all", (True, False))
def test_specify_bits(self, save_all: bool, tmp_path: Path) -> None:
im = hopper("P")
out = str(tmp_path / "temp.png")
im.save(out, bits=4)
im.save(out, bits=4, save_all=save_all)
with Image.open(out) as reloaded:
assert len(reloaded.png.im_palette[1]) == 48
@ -691,6 +698,7 @@ class TestFilePng:
):
assert im.getxmp() == {}
else:
assert "xmp" in im.info
xmp = im.getxmp()
description = xmp["xmpmeta"]["RDF"]["Description"]
@ -775,14 +783,10 @@ class TestFilePng:
def test_save_stdout(self, buffer: bool) -> None:
old_stdout = sys.stdout
if buffer:
class MyStdOut:
buffer = BytesIO()
class MyStdOut:
buffer = BytesIO()
mystdout = MyStdOut()
else:
mystdout = BytesIO()
mystdout: MyStdOut | BytesIO = MyStdOut() if buffer else BytesIO()
sys.stdout = mystdout
@ -792,7 +796,7 @@ class TestFilePng:
# Reset stdout
sys.stdout = old_stdout
if buffer:
if isinstance(mystdout, MyStdOut):
mystdout = mystdout.buffer
with Image.open(mystdout) as reloaded:
assert_image_equal_tofile(reloaded, TEST_PNG_FILE)

Some files were not shown because too many files have changed in this diff Show More