Merge branch 'main' into init

This commit is contained in:
Andrew Murray 2024-01-31 20:17:22 +11:00 committed by GitHub
commit 1e16747159
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
430 changed files with 7443 additions and 4368 deletions

View File

@ -10,11 +10,11 @@ environment:
TEST_OPTIONS: TEST_OPTIONS:
DEPLOY: YES DEPLOY: YES
matrix: matrix:
- PYTHON: C:/Python311 - PYTHON: C:/Python312
ARCHITECTURE: x86 ARCHITECTURE: x86
APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2022 APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2022
- PYTHON: C:/Python38-x64 - PYTHON: C:/Python38-x64
ARCHITECTURE: x64 ARCHITECTURE: AMD64
APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2017 APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2017
@ -43,7 +43,7 @@ build_script:
test_script: test_script:
- cd c:\pillow - cd c:\pillow
- '%PYTHON%\%EXECUTABLE% -m pip install pytest pytest-cov pytest-timeout' - '%PYTHON%\%EXECUTABLE% -m pip install pytest pytest-cov pytest-timeout defusedxml numpy olefile pyroma'
- c:\"Program Files (x86)"\"Windows Kits"\10\Debuggers\x86\gflags.exe /p /enable %PYTHON%\%EXECUTABLE% - c:\"Program Files (x86)"\"Windows Kits"\10\Debuggers\x86\gflags.exe /p /enable %PYTHON%\%EXECUTABLE%
- '%PYTHON%\%EXECUTABLE% -c "from PIL import Image"' - '%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% -m pytest -vx --cov PIL --cov Tests --cov-report term --cov-report xml Tests'

View File

@ -28,7 +28,8 @@ fi
python3 -m pip install --upgrade pip python3 -m pip install --upgrade pip
python3 -m pip install --upgrade wheel python3 -m pip install --upgrade wheel
PYTHONOPTIMIZE=0 python3 -m pip install cffi # 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 coverage
python3 -m pip install defusedxml python3 -m pip install defusedxml
python3 -m pip install olefile python3 -m pip install olefile
@ -38,7 +39,8 @@ python3 -m pip install -U pytest-timeout
python3 -m pip install pyroma python3 -m pip install pyroma
if [[ $(uname) != CYGWIN* ]]; then if [[ $(uname) != CYGWIN* ]]; then
python3 -m pip install numpy # TODO Update condition when NumPy supports 3.13
if ! [[ "$GHA_PYTHON_VERSION" == "3.13" ]]; then python3 -m pip install numpy ; fi
# PyQt6 doesn't support PyPy3 # PyQt6 doesn't support PyPy3
if [[ $GHA_PYTHON_VERSION == 3.* ]]; then if [[ $GHA_PYTHON_VERSION == 3.* ]]; then
@ -46,6 +48,16 @@ if [[ $(uname) != CYGWIN* ]]; then
python3 -m pip install pyqt6 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
# To match pyproject.toml
python3 -m pip install "setuptools>=67.8"
fi
# webp # webp
pushd depends && ./install_webp.sh && popd pushd depends && ./install_webp.sh && popd

View File

@ -0,0 +1 @@
cibuildwheel==2.16.5

View File

@ -2,15 +2,14 @@
[report] [report]
# Regexes for lines to exclude from consideration # Regexes for lines to exclude from consideration
exclude_lines = exclude_also =
# Have to re-enable the standard pragma: # Don't complain if non-runnable code isn't run
pragma: no cover
# Don't complain if non-runnable code isn't run:
if 0: if 0:
if __name__ == .__main__.: if __name__ == .__main__.:
# Don't complain about debug code # Don't complain about debug code
if DEBUG: if DEBUG:
# Don't complain about compatibility code for missing optional dependencies
except ImportError
[run] [run]
omit = omit =

View File

@ -13,7 +13,7 @@ indent_style = space
trim_trailing_whitespace = true trim_trailing_whitespace = true
[*.yml] [*.{toml,yml}]
# Two-space indentation # Two-space indentation
indent_size = 2 indent_size = 2

6
.git-blame-ignore-revs Normal file
View File

@ -0,0 +1,6 @@
# Flake8
8de95676e0fd89f2326b3953488ab66ff29cd2d0
# Format with Black
53a7e3500437a9fd5826bc04758f7116bd7e52dc
# Format the C code with ClangFormat
46b7e86bab79450ec0a2866c6c0c679afb659d17

18
.github/problem-matchers/gcc.json vendored Normal file
View File

@ -0,0 +1,18 @@
{
"__comment": "Based on vscode-cpptools' Extension/package.json gcc rule",
"problemMatcher": [
{
"owner": "gcc-problem-matcher",
"pattern": [
{
"regexp": "^\\s*(.*):(\\d+):(\\d+):\\s+(?:fatal\\s+)?(warning|error):\\s+(.*)$",
"file": 1,
"line": 2,
"column": 3,
"severity": 4,
"message": 5
}
]
}
]
}

View File

@ -13,6 +13,8 @@ categories:
label: "Removal" label: "Removal"
- title: "Testing" - title: "Testing"
label: "Testing" label: "Testing"
- title: "Type hints"
label: "Type hints"
exclude-labels: exclude-labels:
- "changelog: skip" - "changelog: skip"

View File

@ -42,13 +42,13 @@ jobs:
language: python language: python
dry-run: false dry-run: false
- name: Upload New Crash - name: Upload New Crash
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v4
if: failure() && steps.build.outcome == 'success' if: failure() && steps.build.outcome == 'success'
with: with:
name: artifacts name: artifacts
path: ./out/artifacts path: ./out/artifacts
- name: Upload Legacy Crash - name: Upload Legacy Crash
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v4
if: steps.run.outcome == 'success' if: steps.run.outcome == 'success'
with: with:
name: crash name: crash

View File

@ -33,20 +33,30 @@ jobs:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v4 uses: actions/setup-python@v5
with: with:
python-version: "3.x" python-version: "3.x"
cache: pip cache: pip
cache-dependency-path: ".ci/*.sh" cache-dependency-path: |
".ci/*.sh"
"pyproject.toml"
- name: Build system information - name: Build system information
run: python3 .github/workflows/system-info.py run: python3 .github/workflows/system-info.py
- name: Cache libimagequant
uses: actions/cache@v4
id: cache-libimagequant
with:
path: ~/cache-libimagequant
key: ${{ runner.os }}-libimagequant-${{ hashFiles('depends/install_imagequant.sh') }}
- name: Install Linux dependencies - name: Install Linux dependencies
run: | run: |
.ci/install.sh .ci/install.sh
env: env:
GHA_PYTHON_VERSION: "3.x" GHA_PYTHON_VERSION: "3.x"
GHA_LIBIMAGEQUANT_CACHE_HIT: ${{ steps.cache-libimagequant.outputs.cache-hit }}
- name: Build - name: Build
run: | run: |

View File

@ -2,6 +2,9 @@ name: Lint
on: [push, pull_request, workflow_dispatch] on: [push, pull_request, workflow_dispatch]
env:
FORCE_COLOR: 1
permissions: permissions:
contents: read contents: read
@ -20,7 +23,7 @@ jobs:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: pre-commit cache - name: pre-commit cache
uses: actions/cache@v3 uses: actions/cache@v4
with: with:
path: ~/.cache/pre-commit path: ~/.cache/pre-commit
key: lint-pre-commit-${{ hashFiles('**/.pre-commit-config.yaml') }} key: lint-pre-commit-${{ hashFiles('**/.pre-commit-config.yaml') }}
@ -28,7 +31,7 @@ jobs:
lint-pre-commit- lint-pre-commit-
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v4 uses: actions/setup-python@v5
with: with:
python-version: "3.x" python-version: "3.x"
cache: pip cache: pip
@ -46,3 +49,6 @@ jobs:
run: tox -e lint run: tox -e lint
env: env:
PRE_COMMIT_COLOR: always PRE_COMMIT_COLOR: always
- name: Mypy
run: tox -e mypy

View File

@ -2,10 +2,21 @@
set -e set -e
brew install libtiff libjpeg openjpeg libimagequant webp little-cms2 freetype libraqm brew install \
freetype \
ghostscript \
libimagequant \
libjpeg \
libraqm \
libtiff \
little-cms2 \
openjpeg \
webp
export PKG_CONFIG_PATH="/usr/local/opt/openblas/lib/pkgconfig" export PKG_CONFIG_PATH="/usr/local/opt/openblas/lib/pkgconfig"
PYTHONOPTIMIZE=0 python3 -m pip install cffi # 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 coverage
python3 -m pip install defusedxml python3 -m pip install defusedxml
python3 -m pip install olefile python3 -m pip install olefile
@ -14,7 +25,8 @@ python3 -m pip install -U pytest-cov
python3 -m pip install -U pytest-timeout python3 -m pip install -U pytest-timeout
python3 -m pip install pyroma python3 -m pip install pyroma
python3 -m pip install numpy # TODO Update condition when NumPy supports 3.13
if ! [[ "$GHA_PYTHON_VERSION" == "3.13" ]]; then python3 -m pip install numpy ; fi
# extra test images # extra test images
pushd depends && ./install_extra_test_images.sh && popd pushd depends && ./install_extra_test_images.sh && popd

View File

@ -20,7 +20,7 @@ jobs:
steps: steps:
- name: "Check issues" - name: "Check issues"
uses: actions/stale@v8 uses: actions/stale@v9
with: with:
repo-token: ${{ secrets.GITHUB_TOKEN }} repo-token: ${{ secrets.GITHUB_TOKEN }}
only-labels: "Awaiting OP Action" only-labels: "Awaiting OP Action"

View File

@ -6,6 +6,8 @@ This sort of info is missing from GitHub Actions.
Requested here: Requested here:
https://github.com/actions/virtual-environments/issues/79 https://github.com/actions/virtual-environments/issues/79
""" """
from __future__ import annotations
import os import os
import platform import platform
import sys import sys

View File

@ -8,7 +8,6 @@ on:
- ".github/workflows/docs.yml" - ".github/workflows/docs.yml"
- ".github/workflows/wheels*" - ".github/workflows/wheels*"
- ".gitmodules" - ".gitmodules"
- ".travis.yml"
- "docs/**" - "docs/**"
- "wheels/**" - "wheels/**"
pull_request: pull_request:
@ -16,7 +15,6 @@ on:
- ".github/workflows/docs.yml" - ".github/workflows/docs.yml"
- ".github/workflows/wheels*" - ".github/workflows/wheels*"
- ".gitmodules" - ".gitmodules"
- ".travis.yml"
- "docs/**" - "docs/**"
- "wheels/**" - "wheels/**"
workflow_dispatch: workflow_dispatch:
@ -49,7 +47,7 @@ jobs:
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Install Cygwin - name: Install Cygwin
uses: cygwin/cygwin-install-action@v4 uses: egor-tensin/setup-cygwin@v4
with: with:
platform: x86_64 platform: x86_64
packages: > packages: >
@ -71,6 +69,7 @@ jobs:
make make
netpbm netpbm
perl perl
python39=3.9.16-1
python3${{ matrix.python-minor-version }}-cffi python3${{ matrix.python-minor-version }}-cffi
python3${{ matrix.python-minor-version }}-cython python3${{ matrix.python-minor-version }}-cython
python3${{ matrix.python-minor-version }}-devel python3${{ matrix.python-minor-version }}-devel
@ -88,7 +87,7 @@ jobs:
- name: Select Python version - name: Select Python version
run: | run: |
ln -sf c:/cygwin/bin/python3.${{ matrix.python-minor-version }} c:/cygwin/bin/python3 ln -sf c:/tools/cygwin/bin/python3.${{ matrix.python-minor-version }} c:/tools/cygwin/bin/python3
- name: Get latest NumPy version - name: Get latest NumPy version
id: latest-numpy id: latest-numpy
@ -97,7 +96,7 @@ jobs:
python3 -m pip list --outdated | grep numpy | sed -r 's/ +/ /g' | cut -d ' ' -f 3 | sed 's/^/version=/' >> $GITHUB_OUTPUT python3 -m pip list --outdated | grep numpy | sed -r 's/ +/ /g' | cut -d ' ' -f 3 | sed 's/^/version=/' >> $GITHUB_OUTPUT
- name: pip cache - name: pip cache
uses: actions/cache@v3 uses: actions/cache@v4
with: with:
path: 'C:\cygwin\home\runneradmin\.cache\pip' path: 'C:\cygwin\home\runneradmin\.cache\pip'
key: ${{ runner.os }}-cygwin-pip3.${{ matrix.python-minor-version }}-numpy${{ steps.latest-numpy.outputs.version }}-${{ hashFiles('.ci/install.sh') }} key: ${{ runner.os }}-cygwin-pip3.${{ matrix.python-minor-version }}-numpy${{ steps.latest-numpy.outputs.version }}-${{ hashFiles('.ci/install.sh') }}
@ -132,7 +131,7 @@ jobs:
dash.exe -c "mkdir -p Tests/errors" dash.exe -c "mkdir -p Tests/errors"
- name: Upload errors - name: Upload errors
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v4
if: failure() if: failure()
with: with:
name: errors name: errors

View File

@ -8,7 +8,6 @@ on:
- ".github/workflows/docs.yml" - ".github/workflows/docs.yml"
- ".github/workflows/wheels*" - ".github/workflows/wheels*"
- ".gitmodules" - ".gitmodules"
- ".travis.yml"
- "docs/**" - "docs/**"
- "wheels/**" - "wheels/**"
pull_request: pull_request:
@ -16,7 +15,6 @@ on:
- ".github/workflows/docs.yml" - ".github/workflows/docs.yml"
- ".github/workflows/wheels*" - ".github/workflows/wheels*"
- ".gitmodules" - ".gitmodules"
- ".travis.yml"
- "docs/**" - "docs/**"
- "wheels/**" - "wheels/**"
workflow_dispatch: workflow_dispatch:
@ -51,8 +49,8 @@ jobs:
debian-11-bullseye-amd64, debian-11-bullseye-amd64,
debian-12-bookworm-x86, debian-12-bookworm-x86,
debian-12-bookworm-amd64, debian-12-bookworm-amd64,
fedora-37-amd64,
fedora-38-amd64, fedora-38-amd64,
fedora-39-amd64,
gentoo, gentoo,
ubuntu-20.04-focal-amd64, ubuntu-20.04-focal-amd64,
ubuntu-22.04-jammy-amd64, ubuntu-22.04-jammy-amd64,

View File

@ -8,7 +8,6 @@ on:
- ".github/workflows/docs.yml" - ".github/workflows/docs.yml"
- ".github/workflows/wheels*" - ".github/workflows/wheels*"
- ".gitmodules" - ".gitmodules"
- ".travis.yml"
- "docs/**" - "docs/**"
- "wheels/**" - "wheels/**"
pull_request: pull_request:
@ -16,7 +15,6 @@ on:
- ".github/workflows/docs.yml" - ".github/workflows/docs.yml"
- ".github/workflows/wheels*" - ".github/workflows/wheels*"
- ".gitmodules" - ".gitmodules"
- ".travis.yml"
- "docs/**" - "docs/**"
- "wheels/**" - "wheels/**"
workflow_dispatch: workflow_dispatch:

View File

@ -2,11 +2,12 @@ name: Test Windows
on: on:
push: push:
branches:
- "**"
paths-ignore: paths-ignore:
- ".github/workflows/docs.yml" - ".github/workflows/docs.yml"
- ".github/workflows/wheels*" - ".github/workflows/wheels*"
- ".gitmodules" - ".gitmodules"
- ".travis.yml"
- "docs/**" - "docs/**"
- "wheels/**" - "wheels/**"
pull_request: pull_request:
@ -14,7 +15,6 @@ on:
- ".github/workflows/docs.yml" - ".github/workflows/docs.yml"
- ".github/workflows/wheels*" - ".github/workflows/wheels*"
- ".gitmodules" - ".gitmodules"
- ".travis.yml"
- "docs/**" - "docs/**"
- "wheels/**" - "wheels/**"
workflow_dispatch: workflow_dispatch:
@ -32,7 +32,7 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
python-version: ["pypy3.10", "pypy3.9", "3.8", "3.9", "3.10", "3.11", "3.12"] python-version: ["pypy3.10", "pypy3.9", "3.8", "3.9", "3.10", "3.11", "3.12", "3.13"]
timeout-minutes: 30 timeout-minutes: 30
@ -56,25 +56,26 @@ jobs:
# sets env: pythonLocation # sets env: pythonLocation
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v4 uses: actions/setup-python@v5
with: with:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
allow-prereleases: true
cache: pip cache: pip
cache-dependency-path: ".github/workflows/test-windows.yml" cache-dependency-path: ".github/workflows/test-windows.yml"
- name: Print build system information - name: Print build system information
run: python3 .github/workflows/system-info.py run: python3 .github/workflows/system-info.py
- name: python3 -m pip install pytest pytest-cov pytest-timeout defusedxml - name: python3 -m pip install pytest pytest-cov pytest-timeout defusedxml olefile pyroma
run: python3 -m pip install pytest pytest-cov pytest-timeout defusedxml run: python3 -m pip install pytest pytest-cov pytest-timeout defusedxml olefile pyroma
- name: Install dependencies - name: Install dependencies
id: install id: install
run: | run: |
7z x winbuild\depends\nasm-2.16.01-win64.zip "-o$env:RUNNER_WORKSPACE\" choco install nasm --no-progress
echo "$env:RUNNER_WORKSPACE\nasm-2.16.01" >> $env:GITHUB_PATH echo "C:\Program Files\NASM" >> $env:GITHUB_PATH
choco install ghostscript --version=10.0.0.20230317 choco install ghostscript --version=10.0.0.20230317 --no-progress
echo "C:\Program Files\gs\gs10.00.0\bin" >> $env:GITHUB_PATH echo "C:\Program Files\gs\gs10.00.0\bin" >> $env:GITHUB_PATH
# Install extra test images # Install extra test images
@ -88,7 +89,7 @@ jobs:
- name: Cache build - name: Cache build
id: build-cache id: build-cache
uses: actions/cache@v3 uses: actions/cache@v4
with: with:
path: winbuild\build path: winbuild\build
key: key:
@ -166,7 +167,6 @@ jobs:
- name: Build Pillow - name: Build Pillow
run: | run: |
$FLAGS="-C raqm=vendor -C fribidi=vendor" $FLAGS="-C raqm=vendor -C fribidi=vendor"
if ('${{ github.event_name }}' -ne 'pull_request') { $FLAGS+=" -C imagequant=disable" }
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 ."
& $env:pythonLocation\python.exe selftest.py --installed & $env:pythonLocation\python.exe selftest.py --installed
shell: pwsh shell: pwsh
@ -190,7 +190,7 @@ jobs:
shell: bash shell: bash
- name: Upload errors - name: Upload errors
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v4
if: failure() if: failure()
with: with:
name: errors name: errors
@ -208,47 +208,6 @@ jobs:
flags: GHA_Windows flags: GHA_Windows
name: ${{ runner.os }} Python ${{ matrix.python-version }} name: ${{ runner.os }} Python ${{ matrix.python-version }}
- name: Build wheel
id: wheel
if: "github.event_name != 'pull_request'"
run: |
mkdir fribidi
copy winbuild\build\bin\fribidi* fribidi
setlocal EnableDelayedExpansion
for %%f in (winbuild\build\license\*) do (
set x=%%~nf
rem Skip FriBiDi license, it is not included in the wheel.
set fribidi=!x:~0,7!
if NOT !fribidi!==fribidi (
rem Skip imagequant license, it is not included in the wheel.
set libimagequant=!x:~0,13!
if NOT !libimagequant!==libimagequant (
echo. >> LICENSE
echo ===== %%~nf ===== >> LICENSE
echo. >> LICENSE
type %%f >> LICENSE
)
)
)
for /f "tokens=3 delims=/" %%a in ("${{ github.ref }}") do echo dist=dist-%%a >> %GITHUB_OUTPUT%
call winbuild\\build\\build_env.cmd
%pythonLocation%\python.exe -m pip wheel -v -C raqm=vendor -C fribidi=vendor -C imagequant=disable .
shell: cmd
- name: Upload wheel
uses: actions/upload-artifact@v3
if: "github.event_name != 'pull_request'"
with:
name: ${{ steps.wheel.outputs.dist }}
path: "*.whl"
- name: Upload fribidi.dll
if: "github.event_name != 'pull_request' && matrix.python-version == 3.11"
uses: actions/upload-artifact@v3
with:
name: fribidi
path: fribidi\*
success: success:
permissions: permissions:
contents: none contents: none

View File

@ -8,7 +8,6 @@ on:
- ".github/workflows/docs.yml" - ".github/workflows/docs.yml"
- ".github/workflows/wheels*" - ".github/workflows/wheels*"
- ".gitmodules" - ".gitmodules"
- ".travis.yml"
- "docs/**" - "docs/**"
- "wheels/**" - "wheels/**"
pull_request: pull_request:
@ -16,7 +15,6 @@ on:
- ".github/workflows/docs.yml" - ".github/workflows/docs.yml"
- ".github/workflows/wheels*" - ".github/workflows/wheels*"
- ".gitmodules" - ".gitmodules"
- ".travis.yml"
- "docs/**" - "docs/**"
- "wheels/**" - "wheels/**"
workflow_dispatch: workflow_dispatch:
@ -28,6 +26,9 @@ concurrency:
group: ${{ github.workflow }}-${{ github.ref }} group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true cancel-in-progress: true
env:
FORCE_COLOR: 1
jobs: jobs:
build: build:
@ -35,12 +36,13 @@ jobs:
fail-fast: false fail-fast: false
matrix: matrix:
os: [ os: [
"macos-latest", "macos-14",
"ubuntu-latest", "ubuntu-latest",
] ]
python-version: [ python-version: [
"pypy3.10", "pypy3.10",
"pypy3.9", "pypy3.9",
"3.13",
"3.12", "3.12",
"3.11", "3.11",
"3.10", "3.10",
@ -48,11 +50,21 @@ jobs:
"3.8", "3.8",
] ]
include: include:
- python-version: "3.9" - python-version: "3.11"
PYTHONOPTIMIZE: 1 PYTHONOPTIMIZE: 1
REVERSE: "--reverse" REVERSE: "--reverse"
- python-version: "3.8" - python-version: "3.10"
PYTHONOPTIMIZE: 2 PYTHONOPTIMIZE: 2
# M1 only available for 3.10+
- os: "macos-latest"
python-version: "3.9"
- os: "macos-latest"
python-version: "3.8"
exclude:
- os: "macos-14"
python-version: "3.9"
- os: "macos-14"
python-version: "3.8"
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
name: ${{ matrix.os }} Python ${{ matrix.python-version }} name: ${{ matrix.os }} Python ${{ matrix.python-version }}
@ -61,21 +73,33 @@ jobs:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }} - name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4 uses: actions/setup-python@v5
with: with:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
allow-prereleases: true
cache: pip cache: pip
cache-dependency-path: ".ci/*.sh" cache-dependency-path: |
".ci/*.sh"
"pyproject.toml"
- name: Build system information - name: Build system information
run: python3 .github/workflows/system-info.py run: python3 .github/workflows/system-info.py
- name: Cache libimagequant
if: startsWith(matrix.os, 'ubuntu')
uses: actions/cache@v4
id: cache-libimagequant
with:
path: ~/cache-libimagequant
key: ${{ runner.os }}-libimagequant-${{ hashFiles('depends/install_imagequant.sh') }}
- name: Install Linux dependencies - name: Install Linux dependencies
if: startsWith(matrix.os, 'ubuntu') if: startsWith(matrix.os, 'ubuntu')
run: | run: |
.ci/install.sh .ci/install.sh
env: env:
GHA_PYTHON_VERSION: ${{ matrix.python-version }} GHA_PYTHON_VERSION: ${{ matrix.python-version }}
GHA_LIBIMAGEQUANT_CACHE_HIT: ${{ steps.cache-libimagequant.outputs.cache-hit }}
- name: Install macOS dependencies - name: Install macOS dependencies
if: startsWith(matrix.os, 'macOS') if: startsWith(matrix.os, 'macOS')
@ -84,6 +108,10 @@ jobs:
env: env:
GHA_PYTHON_VERSION: ${{ matrix.python-version }} GHA_PYTHON_VERSION: ${{ matrix.python-version }}
- name: Register gcc problem matcher
if: "matrix.os == 'ubuntu-latest' && matrix.python-version == '3.12'"
run: echo "::add-matcher::.github/problem-matchers/gcc.json"
- name: Build - name: Build
run: | run: |
.ci/build.sh .ci/build.sh
@ -110,7 +138,7 @@ jobs:
mkdir -p Tests/errors mkdir -p Tests/errors
- name: Upload errors - name: Upload errors
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v4
if: failure() if: failure()
with: with:
name: errors name: errors
@ -123,7 +151,7 @@ jobs:
- name: Upload coverage - name: Upload coverage
uses: codecov/codecov-action@v3 uses: codecov/codecov-action@v3
with: with:
flags: ${{ matrix.os == 'macos-latest' && 'GHA_macOS' || 'GHA_Ubuntu' }} flags: ${{ matrix.os == 'ubuntu-latest' && 'GHA_Ubuntu' || 'GHA_macOS' }}
name: ${{ matrix.os }} Python ${{ matrix.python-version }} name: ${{ matrix.os }} Python ${{ matrix.python-version }}
gcov: true gcov: true

View File

@ -1,40 +0,0 @@
#!/bin/bash
if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then
# webp, zstd, xz, libtiff, libxcb cause a conflict with building webp, libtiff, libxcb
# libxdmcp causes an issue on macOS < 11
# curl from brew requires zstd, use system curl
# if php is installed, brew tries to reinstall these after installing openblas
# remove lcms2 and libpng to fix building openjpeg on arm64
brew remove --ignore-dependencies webp zstd xz libpng libtiff libxcb libxdmcp curl php lcms2 ghostscript
brew install pkg-config
if [[ "$PLAT" == "arm64" ]]; then
export MACOSX_DEPLOYMENT_TARGET="11.0"
else
export MACOSX_DEPLOYMENT_TARGET="10.10"
fi
fi
if [[ "$MB_PYTHON_VERSION" == pypy3* ]]; then
MB_PYTHON_OSX_VER="10.9"
fi
echo "::group::Install a virtualenv"
source wheels/multibuild/common_utils.sh
source wheels/multibuild/travis_steps.sh
python3 -m pip install virtualenv
before_install
echo "::endgroup::"
echo "::group::Build wheel"
build_wheel
ls -l "${GITHUB_WORKSPACE}/${WHEEL_SDIR}/"
echo "::endgroup::"
if [[ $MACOSX_DEPLOYMENT_TARGET != "11.0" ]]; then
echo "::group::Test wheel"
install_run
echo "::endgroup::"
fi

151
.github/workflows/wheels-dependencies.sh vendored Executable file
View File

@ -0,0 +1,151 @@
#!/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}
fi
export PLAT=$CIBW_ARCHS
source wheels/multibuild/common_utils.sh
source wheels/multibuild/library_builders.sh
if [ -z "$IS_MACOS" ]; then
source wheels/multibuild/manylinux_utils.sh
fi
ARCHIVE_SDIR=pillow-depends-main
# Package versions for fresh source builds
FREETYPE_VERSION=2.13.2
HARFBUZZ_VERSION=8.3.0
LIBPNG_VERSION=1.6.40
JPEGTURBO_VERSION=3.0.1
OPENJPEG_VERSION=2.5.0
XZ_VERSION=5.4.5
TIFF_VERSION=4.6.0
LCMS2_VERSION=2.16
if [[ -n "$IS_MACOS" ]]; then
GIFLIB_VERSION=5.1.4
else
GIFLIB_VERSION=5.2.1
fi
if [[ -n "$IS_MACOS" ]] || [[ "$MB_ML_VER" != 2014 ]]; then
ZLIB_VERSION=1.3
else
ZLIB_VERSION=1.2.8
fi
LIBWEBP_VERSION=1.3.2
BZIP2_VERSION=1.0.8
LIBXCB_VERSION=1.16
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-2.5.0.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_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)
(cd $out_dir \
&& $cmake -DCMAKE_INSTALL_PREFIX=$BUILD_PREFIX -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
}
function build {
if [[ -n "$IS_MACOS" ]] && [[ "$CIBW_ARCHS" == "arm64" ]]; then
export BUILD_PREFIX="/usr/local"
fi
build_xz
if [ -z "$IS_ALPINE" ] && [ -z "$IS_MACOS" ]; then
yum remove -y zlib-devel
fi
build_new_zlib
build_simple xcb-proto 1.16.0 https://xorg.freedesktop.org/archive/individual/proto
if [ -n "$IS_MACOS" ]; then
if [[ "$CIBW_ARCHS" == "arm64" ]]; then
build_simple xorgproto 2023.2 https://www.x.org/pub/individual/proto
build_simple libXau 1.0.11 https://www.x.org/pub/individual/lib
build_simple libpthread-stubs 0.5 https://xcb.freedesktop.org/dist
if [ -f /Library/Frameworks/Python.framework/Versions/Current/share/pkgconfig/xcb-proto.pc ]; then
cp /Library/Frameworks/Python.framework/Versions/Current/share/pkgconfig/xcb-proto.pc /Library/Frameworks/Python.framework/Versions/Current/lib/pkgconfig/xcb-proto.pc
fi
fi
else
sed s/\${pc_sysrootdir\}// /usr/local/share/pkgconfig/xcb-proto.pc > /usr/local/lib/pkgconfig/xcb-proto.pc
fi
build_simple libxcb $LIBXCB_VERSION https://www.x.org/releases/individual/lib
build_libjpeg_turbo
build_tiff
build_libpng
build_lcms2
if [[ -n "$IS_MACOS" ]] && [[ "$CIBW_ARCHS" == "arm64" ]]; then
for dylib in libjpeg.dylib libtiff.dylib liblcms2.dylib; do
cp $BUILD_PREFIX/lib/$dylib /opt/arm64-builds/lib
done
fi
build_openjpeg
ORIGINAL_CFLAGS=$CFLAGS
CFLAGS="$CFLAGS -O3 -DNDEBUG"
if [[ -n "$IS_MACOS" ]]; then
CFLAGS="$CFLAGS -Wl,-headerpad_max_install_names"
fi
build_libwebp
CFLAGS=$ORIGINAL_CFLAGS
build_brotli
if [ -n "$IS_MACOS" ]; then
# Custom freetype build
build_simple freetype $FREETYPE_VERSION https://download.savannah.gnu.org/releases/freetype tar.gz --with-harfbuzz=no
else
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
}
# 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 [[ -n "$IS_MACOS" ]]; then
# webp, libtiff, libxcb cause a conflict with building webp, libtiff, libxcb
# libxdmcp causes an issue on macOS < 11
# if php is installed, brew tries to reinstall these after installing openblas
# remove cairo to fix building harfbuzz on arm64
# remove lcms2 and libpng to fix building openjpeg on arm64
# remove zstd to avoid inclusion on x86_64
# curl from brew requires zstd, use system curl
brew remove --ignore-dependencies webp libpng libtiff libxcb libxdmcp curl php cairo lcms2 ghostscript zstd
brew install pkg-config
fi
wrap_wheel_builder build
# Append licenses
for filename in wheels/dependency_licenses/*; do
echo -e "\n\n----\n\n$(basename $filename | cut -f 1 -d '.')\n" | cat >> LICENSE
cat $filename >> LICENSE
done

View File

@ -1,69 +0,0 @@
name: Build Linux wheels
on:
workflow_call:
inputs:
artifacts-name:
required: true
type: string
env:
CONFIG_PATH: "wheels/config.sh"
REPO_DIR: "."
TEST_DEPENDS: "pytest pytest-timeout"
jobs:
build:
name: ${{ matrix.python }} ${{ matrix.mb-ml-libc }}${{ matrix.mb-ml-ver }}
runs-on: "ubuntu-latest"
strategy:
fail-fast: false
matrix:
python: [
"pypy3.9-7.3.13",
"pypy3.10-7.3.13",
"3.8",
"3.9",
"3.10",
"3.11",
"3.12",
]
mb-ml-libc: [ "manylinux" ]
mb-ml-ver: [ 2014, "_2_28" ]
include:
- python: "3.8"
mb-ml-libc: "musllinux"
mb-ml-ver: "_1_1"
- python: "3.9"
mb-ml-libc: "musllinux"
mb-ml-ver: "_1_1"
- python: "3.10"
mb-ml-libc: "musllinux"
mb-ml-ver: "_1_1"
- python: "3.11"
mb-ml-libc: "musllinux"
mb-ml-ver: "_1_1"
- python: "3.12"
mb-ml-libc: "musllinux"
mb-ml-ver: "_1_1"
env:
MB_PYTHON_VERSION: ${{ matrix.python }}
MB_ML_LIBC: ${{ matrix.mb-ml-libc }}
MB_ML_VER: ${{ matrix.mb-ml-ver }}
steps:
- uses: actions/checkout@v4
with:
submodules: true
- uses: actions/setup-python@v4
with:
python-version: "3.x"
- name: Build Wheel
run: .github/workflows/wheels-build.sh
- uses: actions/upload-artifact@v3
with:
name: ${{ inputs.artifacts-name }}
path: wheelhouse/*.whl
# Uncomment to get SSH access for testing
# - name: Setup tmate session
# if: failure()
# uses: mxschmitt/action-tmate@v3

View File

@ -1,57 +0,0 @@
name: Build macOS wheels
on:
workflow_call:
inputs:
artifacts-name:
required: true
type: string
env:
CONFIG_PATH: "wheels/config.sh"
REPO_DIR: "."
TEST_DEPENDS: "pytest pytest-timeout"
jobs:
build:
name: ${{ matrix.python }} ${{ matrix.platform }}
runs-on: "macos-latest"
strategy:
fail-fast: false
matrix:
python: [
"pypy3.9-7.3.13",
"pypy3.10-7.3.13",
"3.8",
"3.9",
"3.10",
"3.11",
"3.12",
]
platform: [ "x86_64", "arm64" ]
exclude:
- python: "pypy3.9-7.3.13"
platform: "arm64"
- python: "pypy3.10-7.3.13"
platform: "arm64"
env:
PLAT: ${{ matrix.platform }}
MB_PYTHON_VERSION: ${{ matrix.python }}
TRAVIS_OS_NAME: "osx"
steps:
- uses: actions/checkout@v4
with:
submodules: true
- uses: actions/setup-python@v4
with:
python-version: "3.x"
- name: Build Wheel
run: .github/workflows/wheels-build.sh
- uses: actions/upload-artifact@v3
with:
name: ${{ inputs.artifacts-name }}
path: wheelhouse/*.whl
# Uncomment to get SSH access for testing
# - name: Setup tmate session
# if: failure()
# uses: mxschmitt/action-tmate@v3

22
.github/workflows/wheels-test.ps1 vendored Normal file
View File

@ -0,0 +1,22 @@
param ([string]$venv, [string]$pillow="C:\pillow")
$ErrorActionPreference = 'Stop'
$ProgressPreference = 'SilentlyContinue'
Set-PSDebug -Trace 1
if ("$venv" -like "*\cibw-run-*\pp*-win_amd64\*") {
# unlike CPython, PyPy requires Visual C++ Redistributable to be installed
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
Invoke-WebRequest -Uri 'https://aka.ms/vs/15/release/vc_redist.x64.exe' -OutFile 'vc_redist.x64.exe'
C:\vc_redist.x64.exe /install /quiet /norestart | Out-Null
}
$env:path += ";$pillow\winbuild\build\bin\"
& "$venv\Scripts\activate.ps1"
& reg add "HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Options\python.exe" /v "GlobalFlag" /t REG_SZ /d "0x02000000" /f
cd $pillow
& python -VV
if (!$?) { exit $LASTEXITCODE }
& python selftest.py
if (!$?) { exit $LASTEXITCODE }
& python -m pytest -vx Tests\check_wheel.py
if (!$?) { exit $LASTEXITCODE }
& python -m pytest -vx Tests
if (!$?) { exit $LASTEXITCODE }

25
.github/workflows/wheels-test.sh vendored Executable file
View File

@ -0,0 +1,25 @@
#!/bin/bash
set -e
if [[ "$OSTYPE" == "darwin"* ]]; then
brew install fribidi
export PKG_CONFIG_PATH="/usr/local/opt/openblas/lib/pkgconfig"
elif [ "${AUDITWHEEL_POLICY::9}" == "musllinux" ]; then
apk add curl fribidi
else
yum install -y fribidi
fi
if [ "${AUDITWHEEL_POLICY::9}" != "musllinux" ]; then
python3 -m pip install numpy
fi
if [ ! -d "test-images-main" ]; then
curl -fsSL -o pillow-test-images.zip https://github.com/python-pillow/test-images/archive/main.zip
unzip pillow-test-images.zip
mv test-images-main/* Tests/images
fi
# Runs tests
python3 selftest.py
python3 -m pytest Tests/check_wheel.py
python3 -m pytest

View File

@ -3,14 +3,20 @@ name: Wheels
on: on:
push: push:
paths: paths:
- ".github/workflows/wheels*.yml" - ".ci/requirements-cibw.txt"
- ".github/workflows/wheel*"
- "wheels/*" - "wheels/*"
- "winbuild/build_prepare.py"
- "winbuild/fribidi.cmake"
tags: tags:
- "*" - "*"
pull_request: pull_request:
paths: paths:
- ".github/workflows/wheels*.yml" - ".ci/requirements-cibw.txt"
- ".github/workflows/wheel*"
- "wheels/*" - "wheels/*"
- "winbuild/build_prepare.py"
- "winbuild/fribidi.cmake"
workflow_dispatch: workflow_dispatch:
permissions: permissions:
@ -20,23 +26,239 @@ concurrency:
group: ${{ github.workflow }}-${{ github.ref }} group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true cancel-in-progress: true
env:
FORCE_COLOR: 1
jobs: jobs:
macos: build-1-QEMU-emulated-wheels:
uses: ./.github/workflows/wheels-macos.yml name: aarch64 ${{ matrix.python-version }} ${{ matrix.spec }}
with:
artifacts-name: "wheels"
linux:
uses: ./.github/workflows/wheels-linux.yml
with:
artifacts-name: "wheels"
success:
permissions:
contents: none
needs: [macos, linux]
runs-on: ubuntu-latest runs-on: ubuntu-latest
name: Wheels Successful strategy:
fail-fast: false
matrix:
python-version:
- pp39
- pp310
- cp38
- cp39
- cp310
- cp311
- cp312
spec:
- manylinux2014
- manylinux_2_28
- musllinux
exclude:
- { python-version: pp39, spec: musllinux }
- { python-version: pp310, spec: musllinux }
steps: steps:
- name: Success - uses: actions/checkout@v4
run: echo Wheels Successful with:
submodules: true
- uses: actions/setup-python@v5
with:
python-version: "3.x"
# https://github.com/docker/setup-qemu-action
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Install cibuildwheel
run: |
python3 -m pip install -r .ci/requirements-cibw.txt
- name: Build wheels
run: |
python3 -m cibuildwheel --output-dir wheelhouse
env:
# Build only the currently selected Linux architecture (so we can
# parallelise for speed).
CIBW_ARCHS: "aarch64"
# Likewise, select only one Python version per job to speed this up.
CIBW_BUILD: "${{ matrix.python-version }}-${{ matrix.spec == 'musllinux' && 'musllinux' || 'manylinux' }}*"
# Extra options for manylinux.
CIBW_MANYLINUX_AARCH64_IMAGE: ${{ matrix.spec }}
CIBW_MANYLINUX_PYPY_AARCH64_IMAGE: ${{ matrix.spec }}
- uses: actions/upload-artifact@v4
with:
name: dist-qemu-${{ matrix.python-version }}-${{ matrix.spec }}
path: ./wheelhouse/*.whl
build-2-native-wheels:
name: ${{ matrix.name }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
include:
- name: "macOS x86_64"
os: macos-latest
cibw_arch: x86_64
macosx_deployment_target: "10.10"
- name: "macOS arm64"
os: macos-latest
cibw_arch: arm64
macosx_deployment_target: "11.0"
- name: "manylinux2014 and musllinux x86_64"
os: ubuntu-latest
cibw_arch: x86_64
- name: "manylinux_2_28 x86_64"
os: ubuntu-latest
cibw_arch: x86_64
build: "*manylinux*"
manylinux: "manylinux_2_28"
steps:
- uses: actions/checkout@v4
with:
submodules: true
- uses: actions/setup-python@v5
with:
python-version: "3.x"
- name: Install cibuildwheel
run: |
python3 -m pip install -r .ci/requirements-cibw.txt
- name: Build wheels
run: |
python3 -m cibuildwheel --output-dir wheelhouse
env:
CIBW_ARCHS: ${{ matrix.cibw_arch }}
CIBW_BUILD: ${{ matrix.build }}
CIBW_MANYLINUX_PYPY_X86_64_IMAGE: ${{ matrix.manylinux }}
CIBW_MANYLINUX_X86_64_IMAGE: ${{ matrix.manylinux }}
CIBW_SKIP: pp38-*
CIBW_TEST_SKIP: "*-macosx_arm64"
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) }}
path: ./wheelhouse/*.whl
windows:
name: Windows ${{ matrix.cibw_arch }}
runs-on: windows-latest
strategy:
fail-fast: false
matrix:
include:
- cibw_arch: x86
- cibw_arch: AMD64
- cibw_arch: ARM64
steps:
- uses: actions/checkout@v4
- name: Checkout extra test images
uses: actions/checkout@v4
with:
repository: python-pillow/test-images
path: Tests\test-images
- uses: actions/setup-python@v5
with:
python-version: "3.x"
- name: Install cibuildwheel
run: |
python.exe -m pip install -r .ci/requirements-cibw.txt
- name: Prepare for build
run: |
choco install nasm --no-progress
echo "C:\Program Files\NASM" >> $env:GITHUB_PATH
# Install extra test images
xcopy /S /Y Tests\test-images\* Tests\images
& python.exe winbuild\build_prepare.py -v --no-imagequant --architecture=${{ matrix.cibw_arch }}
shell: pwsh
- name: Build wheels
run: |
setlocal EnableDelayedExpansion
for %%f in (winbuild\build\license\*) do (
set x=%%~nf
rem Skip FriBiDi license, it is not included in the wheel.
set fribidi=!x:~0,7!
if NOT !fribidi!==fribidi (
rem Skip imagequant license, it is not included in the wheel.
set libimagequant=!x:~0,13!
if NOT !libimagequant!==libimagequant (
echo. >> LICENSE
echo ===== %%~nf ===== >> LICENSE
echo. >> LICENSE
type %%f >> LICENSE
)
)
)
call winbuild\\build\\build_env.cmd
%pythonLocation%\python.exe -m cibuildwheel . --output-dir wheelhouse
env:
CIBW_ARCHS: ${{ matrix.cibw_arch }}
CIBW_BEFORE_ALL: "{package}\\winbuild\\build\\build_dep_all.cmd"
CIBW_CACHE_PATH: "C:\\cibw"
CIBW_SKIP: pp38-*
CIBW_TEST_SKIP: "*-win_arm64"
CIBW_TEST_COMMAND: 'docker run --rm
-v {project}:C:\pillow
-v C:\cibw:C:\cibw
-v %CD%\..\venv-test:%CD%\..\venv-test
-e CI -e GITHUB_ACTIONS
mcr.microsoft.com/windows/servercore:ltsc2022
powershell C:\pillow\.github\workflows\wheels-test.ps1 %CD%\..\venv-test'
shell: cmd
- name: Upload wheels
uses: actions/upload-artifact@v4
with:
name: dist-windows-${{ matrix.cibw_arch }}
path: ./wheelhouse/*.whl
- name: Upload fribidi.dll
uses: actions/upload-artifact@v4
with:
name: fribidi-windows-${{ matrix.cibw_arch }}
path: winbuild\build\bin\fribidi*
sdist:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.x"
cache: pip
cache-dependency-path: "Makefile"
- run: make sdist
- uses: actions/upload-artifact@v4
with:
name: dist-sdist
path: dist/*.tar.gz
pypi-publish:
if: 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
environment:
name: release-pypi
url: https://pypi.org/p/Pillow
permissions:
id-token: write
steps:
- uses: actions/download-artifact@v4
with:
pattern: dist-*
path: dist
merge-multiple: true
- name: Publish to PyPI
uses: pypa/gh-action-pypi-publish@release/v1

View File

@ -1,56 +1,38 @@
repos: repos:
- repo: https://github.com/asottile/pyupgrade - repo: https://github.com/astral-sh/ruff-pre-commit
rev: v3.13.0 rev: v0.1.9
hooks: hooks:
- id: pyupgrade - id: ruff
args: [--py38-plus] args: [--fix, --exit-non-zero-on-fix]
- repo: https://github.com/psf/black-pre-commit-mirror - repo: https://github.com/psf/black-pre-commit-mirror
rev: 23.9.1 rev: 23.12.1
hooks: hooks:
- id: black - id: black
args: [--target-version=py38]
- repo: https://github.com/PyCQA/isort
rev: 5.12.0
hooks:
- id: isort
- repo: https://github.com/PyCQA/bandit - repo: https://github.com/PyCQA/bandit
rev: 1.7.5 rev: 1.7.6
hooks: hooks:
- id: bandit - id: bandit
args: [--severity-level=high] args: [--severity-level=high]
files: ^src/ files: ^src/
- repo: https://github.com/asottile/yesqa
rev: v1.5.0
hooks:
- id: yesqa
- repo: https://github.com/Lucas-C/pre-commit-hooks - repo: https://github.com/Lucas-C/pre-commit-hooks
rev: v1.5.4 rev: v1.5.4
hooks: hooks:
- id: remove-tabs - id: remove-tabs
exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.gd$|\.opt$) exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.gd$|\.opt$)
- repo: https://github.com/PyCQA/flake8
rev: 6.1.0
hooks:
- id: flake8
additional_dependencies:
[flake8-2020, flake8-errmsg, flake8-implicit-str-concat, flake8-logging]
- repo: https://github.com/pre-commit/pygrep-hooks - repo: https://github.com/pre-commit/pygrep-hooks
rev: v1.10.0 rev: v1.10.0
hooks: hooks:
- id: python-check-blanket-noqa
- id: rst-backticks - id: rst-backticks
- repo: https://github.com/pre-commit/pre-commit-hooks - repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.4.0 rev: v4.5.0
hooks: hooks:
- id: check-executables-have-shebangs - id: check-executables-have-shebangs
- id: check-shebang-scripts-are-executable
- id: check-merge-conflict - id: check-merge-conflict
- id: check-json - id: check-json
- id: check-toml - id: check-toml
@ -61,17 +43,17 @@ repos:
exclude: ^.github/.*TEMPLATE|^Tests/(fonts|images)/ exclude: ^.github/.*TEMPLATE|^Tests/(fonts|images)/
- repo: https://github.com/sphinx-contrib/sphinx-lint - repo: https://github.com/sphinx-contrib/sphinx-lint
rev: v0.6.8 rev: v0.9.1
hooks: hooks:
- id: sphinx-lint - id: sphinx-lint
- repo: https://github.com/tox-dev/pyproject-fmt - repo: https://github.com/tox-dev/pyproject-fmt
rev: 1.2.0 rev: 1.5.3
hooks: hooks:
- id: pyproject-fmt - id: pyproject-fmt
- repo: https://github.com/abravalheri/validate-pyproject - repo: https://github.com/abravalheri/validate-pyproject
rev: v0.14 rev: v0.15
hooks: hooks:
- id: validate-pyproject - id: validate-pyproject

View File

@ -5,7 +5,7 @@ formats: [pdf]
build: build:
os: ubuntu-22.04 os: ubuntu-22.04
tools: tools:
python: "3.11" python: "3"
python: python:
install: install:

View File

@ -1,135 +0,0 @@
if: tag IS present OR type = api
env:
global:
- CONFIG_PATH=wheels/config.sh
- REPO_DIR=.
- PLAT=aarch64
- TEST_DEPENDS=pytest-timeout
language: python
# Default Python version is usually 3.6
python: "3.11"
dist: focal
services: docker
jobs:
include:
- name: "3.8 Focal manylinux2014 aarch64"
os: linux
arch: arm64
env:
- MB_ML_VER=2014
- MB_PYTHON_VERSION=3.8
- name: "3.8 Focal manylinux_2_28 aarch64"
os: linux
arch: arm64
env:
- MB_ML_VER="_2_28"
- MB_PYTHON_VERSION=3.8
- name: "3.8 musllinux_1_1 aarch64"
os: linux
arch: arm64
env:
- MB_ML_VER="_1_1"
- MB_ML_LIBC="musllinux"
- MB_PYTHON_VERSION=3.8
- name: "3.9 Focal manylinux2014 aarch64"
os: linux
arch: arm64
env:
- MB_ML_VER=2014
- MB_PYTHON_VERSION=3.9
- name: "3.9 Focal manylinux_2_28 aarch64"
os: linux
arch: arm64
env:
- MB_ML_VER="_2_28"
- MB_PYTHON_VERSION=3.9
- name: "3.9 musllinux_1_1 aarch64"
os: linux
arch: arm64
env:
- MB_ML_VER="_1_1"
- MB_ML_LIBC="musllinux"
- MB_PYTHON_VERSION=3.9
- name: "3.10 Focal manylinux2014 aarch64"
os: linux
arch: arm64
env:
- MB_ML_VER=2014
- MB_PYTHON_VERSION=3.10
- name: "3.10 Focal manylinux_2_28 aarch64"
os: linux
arch: arm64
env:
- MB_ML_VER="_2_28"
- MB_PYTHON_VERSION=3.10
- name: "3.10 musllinux_1_1 aarch64"
os: linux
arch: arm64
env:
- MB_ML_VER="_1_1"
- MB_ML_LIBC="musllinux"
- MB_PYTHON_VERSION=3.10
- name: "3.11 Focal manylinux_2_28 aarch64"
os: linux
arch: arm64
env:
- MB_ML_VER=2014
- MB_PYTHON_VERSION=3.11
- name: "3.11 Focal manylinux_2_28 aarch64"
os: linux
arch: arm64
env:
- MB_ML_VER="_2_28"
- MB_PYTHON_VERSION=3.11
- name: "3.11 musllinux_1_1 aarch64"
os: linux
arch: arm64
env:
- MB_ML_VER="_1_1"
- MB_ML_LIBC="musllinux"
- MB_PYTHON_VERSION=3.11
- name: "3.12 Focal manylinux_2_28 aarch64"
os: linux
arch: arm64
env:
- MB_ML_VER=2014
- MB_PYTHON_VERSION=3.12
- name: "3.12 Focal manylinux_2_28 aarch64"
os: linux
arch: arm64
env:
- MB_ML_VER="_2_28"
- MB_PYTHON_VERSION=3.12
- name: "3.12 musllinux_1_1 aarch64"
os: linux
arch: arm64
env:
- MB_ML_VER="_1_1"
- MB_ML_LIBC="musllinux"
- MB_PYTHON_VERSION=3.12
before_install:
- source wheels/multibuild/common_utils.sh
- source wheels/multibuild/travis_steps.sh
- before_install
install:
- build_multilinux aarch64 build_wheel
- ls -l "${TRAVIS_BUILD_DIR}/${WHEEL_SDIR}/"
script:
- install_run
# Upload wheels to GitHub Releases
deploy:
provider: releases
api_key: $GITHUB_RELEASE_TOKEN
file_glob: true
file: "${TRAVIS_BUILD_DIR}/${WHEEL_SDIR}/*.whl"
on:
repo: python-pillow/Pillow
tags: true
skip_cleanup: true

View File

@ -2,6 +2,141 @@
Changelog (Pillow) Changelog (Pillow)
================== ==================
10.3.0 (unreleased)
-------------------
- Do not support using test-image-results to upload images after test failures #7739
[radarhere]
- Changed ImageMath.ops to be static #7721
[radarhere]
- Fix APNG info after seeking backwards more than twice #7701
[esoma, radarhere]
- Deprecate ImageCms constants and versions() function #7702
[nulano, radarhere]
- Added PerspectiveTransform #7699
[radarhere]
- Add support for reading and writing grayscale PFM images #7696
[nulano, hugovk]
- Add LCMS2 flags to ImageCms #7676
[nulano, radarhere, hugovk]
- Rename x64 to AMD64 in winbuild #7693
[nulano]
10.2.0 (2024-01-02)
-------------------
- Add ``keep_rgb`` option when saving JPEG to prevent conversion of RGB colorspace #7553
[bgilbert, radarhere]
- Trim glyph size in ImageFont.getmask() #7669, #7672
[radarhere, nulano]
- Deprecate IptcImagePlugin helpers #7664
[nulano, hugovk, radarhere]
- Allow uncompressed TIFF images to be saved in chunks #7650
[radarhere]
- Concatenate multiple JPEG EXIF markers #7496
[radarhere]
- Changed IPTC tile tuple to match other plugins #7661
[radarhere]
- Do not assign new fp attribute when exiting context manager #7566
[radarhere]
- Support arbitrary masks for uncompressed RGB DDS images #7589
[radarhere, akx]
- Support setting ROWSPERSTRIP tag #7654
[radarhere]
- Apply ImageFont.MAX_STRING_LENGTH to ImageFont.getmask() #7662
[radarhere]
- Optimise ``ImageColor`` using ``functools.lru_cache`` #7657
[hugovk]
- Restricted environment keys for ImageMath.eval() #7655
[wiredfool, radarhere]
- Optimise ``ImageMode.getmode`` using ``functools.lru_cache`` #7641
[hugovk, radarhere]
- Fix incorrect color blending for overlapping glyphs #7497
[ZachNagengast, nulano, radarhere]
- Attempt memory mapping when tile args is a string #7565
[radarhere]
- Fill identical pixels with transparency in subsequent frames when saving GIF #7568
[radarhere]
- Corrected duration when combining multiple GIF frames into single frame #7521
[radarhere]
- Handle disposing GIF background from outside palette #7515
[radarhere]
- Seek past the data when skipping a PSD layer #7483
[radarhere]
- Import plugins relative to the module #7576
[deliangyang, jaxx0n]
- Translate encoder error codes to strings; deprecate ``ImageFile.raise_oserror()`` #7609
[bgilbert, radarhere]
- Support reading BC4U and DX10 BC1 images #6486
[REDxEYE, radarhere, hugovk]
- Optimize ImageStat.Stat.extrema #7593
[florath, radarhere]
- Handle pathlib.Path in FreeTypeFont #7578
[radarhere, hugovk, nulano]
- Added support for reading DX10 BC4 DDS images #7603
[sambvfx, radarhere]
- Optimized ImageStat.Stat.count #7599
[florath]
- Correct PDF palette size when saving #7555
[radarhere]
- Fixed closing file pointer with olefile 0.47 #7594
[radarhere]
- Raise ValueError when TrueType font size is not greater than zero #7584, #7587
[akx, radarhere]
- If absent, do not try to close fp when closing image #7557
[RaphaelVRossi, radarhere]
- Allow configuring JPEG restart marker interval on save #7488
[bgilbert, radarhere]
- Decrement reference count for PyObject #7549
[radarhere]
- Implement ``streamtype=1`` option for tables-only JPEG encoding #7491
[bgilbert, radarhere]
- If save_all PNG only has one frame, do not create animated image #7522
[radarhere]
- Fixed frombytes() for images with a zero dimension #7493
[radarhere]
10.1.0 (2023-10-15) 10.1.0 (2023-10-15)
------------------- -------------------
@ -2191,7 +2326,7 @@ Changelog (Pillow)
- Cache EXIF information #3498 - Cache EXIF information #3498
[Glandos] [Glandos]
- Added transparency for all PNG greyscale modes #3744 - Added transparency for all PNG grayscale modes #3744
[radarhere] [radarhere]
- Fix deprecation warnings in Python 3.8 #3749 - Fix deprecation warnings in Python 3.8 #3749
@ -4693,7 +4828,7 @@ Changelog (Pillow)
- Fix Bicubic interpolation #970 - Fix Bicubic interpolation #970
[homm] [homm]
- Support for 4-bit greyscale TIFF images #980 - Support for 4-bit grayscale TIFF images #980
[hugovk] [hugovk]
- Updated manifest #957 - Updated manifest #957
@ -6768,7 +6903,7 @@ The test suite includes 750 individual tests.
- You can now convert directly between all modes supported by - You can now convert directly between all modes supported by
PIL. When converting colour images to "P", PIL defaults to PIL. When converting colour images to "P", PIL defaults to
a "web" palette and dithering. When converting greyscale a "web" palette and dithering. When converting grayscale
images to "1", PIL uses a thresholding and dithering. images to "1", PIL uses a thresholding and dithering.
- Added a "dither" option to "convert". By default, "convert" - Added a "dither" option to "convert". By default, "convert"
@ -6846,13 +6981,13 @@ The test suite includes 530 individual tests.
- Fixed "paste" to allow a mask also for mode "F" images. - Fixed "paste" to allow a mask also for mode "F" images.
- The BMP driver now saves mode "1" images. When loading images, the mode - The BMP driver now saves mode "1" images. When loading images, the mode
is set to "L" for 8-bit files with greyscale palettes, and to "P" for is set to "L" for 8-bit files with grayscale palettes, and to "P" for
other 8-bit files. other 8-bit files.
- The IM driver now reads and saves "1" images (file modes "0 1" or "L 1"). - The IM driver now reads and saves "1" images (file modes "0 1" or "L 1").
- The JPEG and GIF drivers now saves "1" images. For JPEG, the image - The JPEG and GIF drivers now saves "1" images. For JPEG, the image
is saved as 8-bit greyscale (it will load as mode "L"). For GIF, the is saved as 8-bit grayscale (it will load as mode "L"). For GIF, the
image will be loaded as a "P" image. image will be loaded as a "P" image.
- Fixed a potential buffer overrun in the GIF encoder. - Fixed a potential buffer overrun in the GIF encoder.
@ -7156,7 +7291,7 @@ The test suite includes 400 individual tests.
drawing capabilities can be used to render vector and metafile drawing capabilities can be used to render vector and metafile
formats. formats.
- Added restricted drivers for images from Image Tools (greyscale - Added restricted drivers for images from Image Tools (grayscale
only) and LabEye/IFUNC (common interchange modes only). only) and LabEye/IFUNC (common interchange modes only).
- Some minor improvements to the sample scripts provided in the - Some minor improvements to the sample scripts provided in the

View File

@ -5,7 +5,7 @@ The Python Imaging Library (PIL) is
Pillow is the friendly PIL fork. It is Pillow is the friendly PIL fork. It is
Copyright © 2010-2023 by Jeffrey A. Clark (Alex) and contributors. Copyright © 2010-2024 by Jeffrey A. Clark (Alex) and contributors.
Like PIL, Pillow is licensed under the open source HPND License: Like PIL, Pillow is licensed under the open source HPND License:

View File

@ -5,8 +5,10 @@ include *.md
include *.py include *.py
include *.rst include *.rst
include *.sh include *.sh
include *.toml
include *.txt include *.txt
include *.yaml include *.yaml
include .flake8
include LICENSE include LICENSE
include Makefile include Makefile
include tox.ini include tox.ini

View File

@ -49,7 +49,7 @@ help:
@echo " install make and install" @echo " install make and install"
@echo " install-coverage make and install with C coverage" @echo " install-coverage make and install with C coverage"
@echo " lint run the lint checks" @echo " lint run the lint checks"
@echo " lint-fix run Black and isort to (mostly) fix lint issues" @echo " lint-fix run Ruff to (mostly) fix lint issues"
@echo " release-test run code and package tests before release" @echo " release-test run code and package tests before release"
@echo " test run tests on installed Pillow" @echo " test run tests on installed Pillow"
@ -118,6 +118,6 @@ lint:
.PHONY: lint-fix .PHONY: lint-fix
lint-fix: lint-fix:
python3 -c "import black" > /dev/null 2>&1 || python3 -m pip install black python3 -c "import black" > /dev/null 2>&1 || python3 -m pip install black
python3 -c "import isort" > /dev/null 2>&1 || python3 -m pip install isort python3 -m black .
python3 -m black --target-version py38 . python3 -c "import ruff" > /dev/null 2>&1 || python3 -m pip install ruff
python3 -m isort . python3 -m ruff --fix .

View File

@ -48,9 +48,6 @@ As of 2019, Pillow development is
<a href="https://github.com/python-pillow/Pillow/actions/workflows/wheels.yml"><img <a href="https://github.com/python-pillow/Pillow/actions/workflows/wheels.yml"><img
alt="GitHub Actions build status (Wheels)" alt="GitHub Actions build status (Wheels)"
src="https://github.com/python-pillow/Pillow/workflows/Wheels/badge.svg"></a> src="https://github.com/python-pillow/Pillow/workflows/Wheels/badge.svg"></a>
<a href="https://app.travis-ci.com/github/python-pillow/Pillow"><img
alt="Travis CI wheels build status (aarch64)"
src="https://img.shields.io/travis/com/python-pillow/Pillow/main.svg?label=aarch64%20wheels"></a>
<a href="https://app.codecov.io/gh/python-pillow/Pillow"><img <a href="https://app.codecov.io/gh/python-pillow/Pillow"><img
alt="Code coverage" alt="Code coverage"
src="https://codecov.io/gh/python-pillow/Pillow/branch/main/graph/badge.svg"></a> src="https://codecov.io/gh/python-pillow/Pillow/branch/main/graph/badge.svg"></a>
@ -68,10 +65,10 @@ As of 2019, Pillow development is
<a href="https://tidelift.com/subscription/pkg/pypi-pillow?utm_source=pypi-pillow&utm_medium=badge"><img <a href="https://tidelift.com/subscription/pkg/pypi-pillow?utm_source=pypi-pillow&utm_medium=badge"><img
alt="Tidelift" alt="Tidelift"
src="https://tidelift.com/badges/package/pypi/Pillow?style=flat"></a> src="https://tidelift.com/badges/package/pypi/Pillow?style=flat"></a>
<a href="https://pypi.org/project/Pillow/"><img <a href="https://pypi.org/project/pillow/"><img
alt="Newest PyPI version" alt="Newest PyPI version"
src="https://img.shields.io/pypi/v/pillow.svg"></a> src="https://img.shields.io/pypi/v/pillow.svg"></a>
<a href="https://pypi.org/project/Pillow/"><img <a href="https://pypi.org/project/pillow/"><img
alt="Number of PyPI downloads" alt="Number of PyPI downloads"
src="https://img.shields.io/pypi/dm/pillow.svg"></a> src="https://img.shields.io/pypi/dm/pillow.svg"></a>
<a href="https://www.bestpractices.dev/projects/6331"><img <a href="https://www.bestpractices.dev/projects/6331"><img

View File

@ -10,7 +10,7 @@ Released quarterly on January 2nd, April 1st, July 1st and October 15th.
* [ ] Open a release ticket e.g. https://github.com/python-pillow/Pillow/issues/3154 * [ ] Open a release ticket e.g. https://github.com/python-pillow/Pillow/issues/3154
* [ ] Develop and prepare release in `main` branch. * [ ] Develop and prepare release in `main` branch.
* [ ] Check [GitHub Actions](https://github.com/python-pillow/Pillow/actions) and [AppVeyor](https://ci.appveyor.com/project/python-pillow/Pillow) to confirm passing tests in `main` branch. * [ ] Check [GitHub Actions](https://github.com/python-pillow/Pillow/actions) and [AppVeyor](https://ci.appveyor.com/project/python-pillow/Pillow) to confirm passing tests in `main` branch.
* [ ] Check that all of the wheel builds pass the tests in the [GitHub Actions "Wheels" workflow](https://github.com/python-pillow/Pillow/actions/workflows/wheels.yml) and [Travis CI](https://app.travis-ci.com/github/python-pillow/pillow) jobs by manually triggering them. * [ ] 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` * [ ] In compliance with [PEP 440](https://peps.python.org/pep-0440/), update version identifier in `src/PIL/_version.py`
* [ ] Update `CHANGES.rst`. * [ ] Update `CHANGES.rst`.
* [ ] Run pre-release check via `make release-test` in a freshly cloned repo. * [ ] Run pre-release check via `make release-test` in a freshly cloned repo.
@ -20,16 +20,7 @@ Released quarterly on January 2nd, April 1st, July 1st and October 15th.
git tag 5.2.0 git tag 5.2.0
git push --tags git push --tags
``` ```
* [ ] Create and check source distribution: * [ ] Create and upload all [source and binary distributions](https://github.com/python-pillow/Pillow/blob/main/RELEASING.md#source-and-binary-distributions)
```bash
make sdist
```
* [ ] Create [binary distributions](https://github.com/python-pillow/Pillow/blob/main/RELEASING.md#binary-distributions)
* [ ] Check and upload all binaries and source distributions e.g.:
```bash
python3 -m twine check --strict dist/*
python3 -m twine upload dist/Pillow-5.2.0*
```
* [ ] Publish the [release on GitHub](https://github.com/python-pillow/Pillow/releases) * [ ] Publish the [release on GitHub](https://github.com/python-pillow/Pillow/releases)
* [ ] In compliance with [PEP 440](https://peps.python.org/pep-0440/), * [ ] In compliance with [PEP 440](https://peps.python.org/pep-0440/),
increment and append `.dev0` to version identifier in `src/PIL/_version.py` and then: increment and append `.dev0` to version identifier in `src/PIL/_version.py` and then:
@ -59,12 +50,7 @@ Released as needed for security, installation or critical bug fixes.
```bash ```bash
make sdist make sdist
``` ```
* [ ] Create [binary distributions](https://github.com/python-pillow/Pillow/blob/main/RELEASING.md#binary-distributions) * [ ] Create and upload all [source and binary distributions](https://github.com/python-pillow/Pillow/blob/main/RELEASING.md#source-and-binary-distributions)
* [ ] Check and upload all binaries and source distributions e.g.:
```bash
python3 -m twine check --strict dist/*
python3 -m twine upload dist/Pillow-5.2.1*
```
* [ ] Publish the [release on GitHub](https://github.com/python-pillow/Pillow/releases) and then: * [ ] Publish the [release on GitHub](https://github.com/python-pillow/Pillow/releases) and then:
```bash ```bash
git push git push
@ -86,35 +72,17 @@ Released as needed privately to individual vendors for critical security-related
git tag 2.5.3 git tag 2.5.3
git push origin --tags git push origin --tags
``` ```
* [ ] Create and check source distribution: * [ ] Create and upload all [source and binary distributions](https://github.com/python-pillow/Pillow/blob/main/RELEASING.md#source-and-binary-distributions)
```bash
make sdist
```
* [ ] Create [binary distributions](https://github.com/python-pillow/Pillow/blob/main/RELEASING.md#binary-distributions)
* [ ] Publish the [release on GitHub](https://github.com/python-pillow/Pillow/releases) and then: * [ ] Publish the [release on GitHub](https://github.com/python-pillow/Pillow/releases) and then:
```bash ```bash
git push origin 2.5.x git push origin 2.5.x
``` ```
## Binary Distributions ## Source and Binary Distributions
### macOS and Linux * [ ] Check the [GitHub Actions "Wheels" workflow](https://github.com/python-pillow/Pillow/actions/workflows/wheels.yml)
* [ ] Download wheels from the [GitHub Actions "Wheels" workflow](https://github.com/python-pillow/Pillow/actions/workflows/wheels.yml) has passed, including the "Upload release to PyPI" job. This will have been triggered
and copy into `dist/`. For example using [GitHub CLI](https://github.com/cli/cli): by the new tag.
```bash
gh run download --dir dist
# select wheels
```
* [ ] Download the Linux aarch64 wheels created by Travis CI from [GitHub releases](https://github.com/python-pillow/Pillow/releases)
and copy into `dist`.
### Windows
* [ ] Download the artifacts from the [GitHub Actions "Test Windows" workflow](https://github.com/python-pillow/Pillow/actions/workflows/test-windows.yml)
and copy into `dist/`. For example using [GitHub CLI](https://github.com/cli/cli):
```bash
gh run download --dir dist
# select dist-x.y.z
```
## Publicize Release ## Publicize Release

View File

@ -1,4 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
from __future__ import annotations
import sys import sys

View File

@ -1,3 +1,5 @@
from __future__ import annotations
import time import time
from PIL import PyAccess from PIL import PyAccess
@ -7,21 +9,21 @@ from .helper import hopper
# Not running this test by default. No DOS against CI. # Not running this test by default. No DOS against CI.
def iterate_get(size, access): def iterate_get(size, access) -> None:
(w, h) = size (w, h) = size
for x in range(w): for x in range(w):
for y in range(h): for y in range(h):
access[(x, y)] access[(x, y)]
def iterate_set(size, access): def iterate_set(size, access) -> None:
(w, h) = size (w, h) = size
for x in range(w): for x in range(w):
for y in range(h): for y in range(h):
access[(x, y)] = (x % 256, y % 256, 0) access[(x, y)] = (x % 256, y % 256, 0)
def timer(func, label, *args): def timer(func, label, *args) -> None:
iterations = 5000 iterations = 5000
starttime = time.time() starttime = time.time()
for x in range(iterations): for x in range(iterations):
@ -36,7 +38,7 @@ def timer(func, label, *args):
) )
def test_direct(): def test_direct() -> None:
im = hopper() im = hopper()
im.load() im.load()
# im = Image.new("RGB", (2000, 2000), (1, 3, 2)) # im = Image.new("RGB", (2000, 2000), (1, 3, 2))
@ -45,7 +47,7 @@ def test_direct():
assert caccess[(0, 0)] == access[(0, 0)] assert caccess[(0, 0)] == access[(0, 0)]
print("Size: %sx%s" % im.size) print(f"Size: {im.width}x{im.height}")
timer(iterate_get, "PyAccess - get", im.size, access) timer(iterate_get, "PyAccess - get", im.size, access)
timer(iterate_set, "PyAccess - set", im.size, access) timer(iterate_set, "PyAccess - set", im.size, access)
timer(iterate_get, "C-api - get", im.size, caccess) timer(iterate_get, "C-api - get", im.size, caccess)

View File

@ -1,4 +1,4 @@
#!/usr/bin/env python3 from __future__ import annotations
from PIL import Image from PIL import Image

View File

@ -1,9 +1,11 @@
from __future__ import annotations
from PIL import Image from PIL import Image
TEST_FILE = "Tests/images/fli_overflow.fli" TEST_FILE = "Tests/images/fli_overflow.fli"
def test_fli_overflow(): def test_fli_overflow() -> None:
# this should not crash with a malloc error or access violation # this should not crash with a malloc error or access violation
with Image.open(TEST_FILE) as im: with Image.open(TEST_FILE) as im:
im.load() im.load()

View File

@ -1,5 +1,6 @@
# Tests potential DOS of IcnsImagePlugin with 0 length block. # Tests potential DOS of IcnsImagePlugin with 0 length block.
# Run from anywhere that PIL is importable. # Run from anywhere that PIL is importable.
from __future__ import annotations
from io import BytesIO from io import BytesIO

View File

@ -1,4 +1,8 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
from __future__ import annotations
from typing import Any, Callable
import pytest import pytest
from PIL import Image from PIL import Image
@ -11,31 +15,34 @@ max_iterations = 10000
pytestmark = pytest.mark.skipif(is_win32(), reason="requires Unix or macOS") pytestmark = pytest.mark.skipif(is_win32(), reason="requires Unix or macOS")
def _get_mem_usage(): def _get_mem_usage() -> float:
from resource import RUSAGE_SELF, getpagesize, getrusage from resource import RUSAGE_SELF, getpagesize, getrusage
mem = getrusage(RUSAGE_SELF).ru_maxrss mem = getrusage(RUSAGE_SELF).ru_maxrss
return mem * getpagesize() / 1024 / 1024 return mem * getpagesize() / 1024 / 1024
def _test_leak(min_iterations, max_iterations, fn, *args, **kwargs): def _test_leak(
min_iterations: int, max_iterations: int, fn: Callable[..., None], *args: Any
) -> None:
mem_limit = None mem_limit = None
for i in range(max_iterations): for i in range(max_iterations):
fn(*args, **kwargs) fn(*args)
mem = _get_mem_usage() mem = _get_mem_usage()
if i < min_iterations: if i < min_iterations:
mem_limit = mem + 1 mem_limit = mem + 1
continue continue
msg = f"memory usage limit exceeded after {i + 1} iterations" msg = f"memory usage limit exceeded after {i + 1} iterations"
assert mem_limit is not None
assert mem <= mem_limit, msg assert mem <= mem_limit, msg
def test_leak_putdata(): def test_leak_putdata() -> None:
im = Image.new("RGB", (25, 25)) im = Image.new("RGB", (25, 25))
_test_leak(min_iterations, max_iterations, im.putdata, im.getdata()) _test_leak(min_iterations, max_iterations, im.putdata, im.getdata())
def test_leak_getlist(): def test_leak_getlist() -> None:
im = Image.new("P", (25, 25)) im = Image.new("P", (25, 25))
_test_leak( _test_leak(
min_iterations, min_iterations,

View File

@ -1,5 +1,6 @@
# Tests potential DOS of Jpeg2kImagePlugin with 0 length block. # Tests potential DOS of Jpeg2kImagePlugin with 0 length block.
# Run from anywhere that PIL is importable. # Run from anywhere that PIL is importable.
from __future__ import annotations
from io import BytesIO from io import BytesIO

View File

@ -1,3 +1,5 @@
from __future__ import annotations
from io import BytesIO from io import BytesIO
import pytest import pytest
@ -18,7 +20,7 @@ pytestmark = [
] ]
def test_leak_load(): def test_leak_load() -> None:
from resource import RLIMIT_AS, RLIMIT_STACK, setrlimit from resource import RLIMIT_AS, RLIMIT_STACK, setrlimit
setrlimit(RLIMIT_STACK, (stack_size, stack_size)) setrlimit(RLIMIT_STACK, (stack_size, stack_size))
@ -28,7 +30,7 @@ def test_leak_load():
im.load() im.load()
def test_leak_save(): def test_leak_save() -> None:
from resource import RLIMIT_AS, RLIMIT_STACK, setrlimit from resource import RLIMIT_AS, RLIMIT_STACK, setrlimit
setrlimit(RLIMIT_STACK, (stack_size, stack_size)) setrlimit(RLIMIT_STACK, (stack_size, stack_size))

View File

@ -1,9 +1,13 @@
from __future__ import annotations
from pathlib import Path
import pytest import pytest
from PIL import Image from PIL import Image
def test_j2k_overflow(tmp_path): def test_j2k_overflow(tmp_path: Path) -> None:
im = Image.new("RGBA", (1024, 131584)) im = Image.new("RGBA", (1024, 131584))
target = str(tmp_path / "temp.jpc") target = str(tmp_path / "temp.jpc")
with pytest.raises(OSError): with pytest.raises(OSError):

4
Tests/check_jp2_overflow.py Executable file → Normal file
View File

@ -1,5 +1,3 @@
#!/usr/bin/env python3
# Reproductions/tests for OOB read errors in FliDecode.c # Reproductions/tests for OOB read errors in FliDecode.c
# When run in python, all of these images should fail for # When run in python, all of these images should fail for
@ -12,7 +10,7 @@
# the output should be empty. There may be python issues # the output should be empty. There may be python issues
# in the valgrind especially if run in a debug python # in the valgrind especially if run in a debug python
# version. # version.
from __future__ import annotations
from PIL import Image from PIL import Image

View File

@ -1,3 +1,5 @@
from __future__ import annotations
from io import BytesIO from io import BytesIO
import pytest import pytest
@ -109,14 +111,14 @@ standard_chrominance_qtable = (
[standard_l_qtable, standard_chrominance_qtable], [standard_l_qtable, standard_chrominance_qtable],
), ),
) )
def test_qtables_leak(qtables): def test_qtables_leak(qtables: tuple[tuple[int, ...]] | list[tuple[int, ...]]) -> None:
im = hopper("RGB") im = hopper("RGB")
for _ in range(iterations): for _ in range(iterations):
test_output = BytesIO() test_output = BytesIO()
im.save(test_output, "JPEG", qtables=qtables) im.save(test_output, "JPEG", qtables=qtables)
def test_exif_leak(): def test_exif_leak() -> None:
""" """
pre patch: pre patch:
@ -179,7 +181,7 @@ def test_exif_leak():
im.save(test_output, "JPEG", exif=exif) im.save(test_output, "JPEG", exif=exif)
def test_base_save(): def test_base_save() -> None:
""" """
base case: base case:
MB MB

View File

@ -1,4 +1,8 @@
from __future__ import annotations
import sys import sys
from pathlib import Path
from types import ModuleType
import pytest import pytest
@ -14,6 +18,7 @@ from PIL import Image
# 2.7 and 3.2. # 2.7 and 3.2.
numpy: ModuleType | None
try: try:
import numpy import numpy
except ImportError: except ImportError:
@ -26,23 +31,24 @@ XDIM = 48000
pytestmark = pytest.mark.skipif(sys.maxsize <= 2**32, reason="requires 64-bit system") pytestmark = pytest.mark.skipif(sys.maxsize <= 2**32, reason="requires 64-bit system")
def _write_png(tmp_path, xdim, ydim): def _write_png(tmp_path: Path, xdim: int, ydim: int) -> None:
f = str(tmp_path / "temp.png") f = str(tmp_path / "temp.png")
im = Image.new("L", (xdim, ydim), 0) im = Image.new("L", (xdim, ydim), 0)
im.save(f) im.save(f)
def test_large(tmp_path): def test_large(tmp_path: Path) -> None:
"""succeeded prepatch""" """succeeded prepatch"""
_write_png(tmp_path, XDIM, YDIM) _write_png(tmp_path, XDIM, YDIM)
def test_2gpx(tmp_path): def test_2gpx(tmp_path: Path) -> None:
"""failed prepatch""" """failed prepatch"""
_write_png(tmp_path, XDIM, XDIM) _write_png(tmp_path, XDIM, XDIM)
@pytest.mark.skipif(numpy is None, reason="Numpy is not installed") @pytest.mark.skipif(numpy is None, reason="Numpy is not installed")
def test_size_greater_than_int(): def test_size_greater_than_int() -> None:
assert numpy is not None
arr = numpy.ndarray(shape=(16394, 16394)) arr = numpy.ndarray(shape=(16394, 16394))
Image.fromarray(arr) Image.fromarray(arr)

View File

@ -1,4 +1,7 @@
from __future__ import annotations
import sys import sys
from pathlib import Path
import pytest import pytest
@ -22,7 +25,7 @@ XDIM = 48000
pytestmark = pytest.mark.skipif(sys.maxsize <= 2**32, reason="requires 64-bit system") pytestmark = pytest.mark.skipif(sys.maxsize <= 2**32, reason="requires 64-bit system")
def _write_png(tmp_path, xdim, ydim): def _write_png(tmp_path: Path, xdim: int, ydim: int) -> None:
dtype = np.uint8 dtype = np.uint8
a = np.zeros((xdim, ydim), dtype=dtype) a = np.zeros((xdim, ydim), dtype=dtype)
f = str(tmp_path / "temp.png") f = str(tmp_path / "temp.png")
@ -30,11 +33,11 @@ def _write_png(tmp_path, xdim, ydim):
im.save(f) im.save(f)
def test_large(tmp_path): def test_large(tmp_path: Path) -> None:
"""succeeded prepatch""" """succeeded prepatch"""
_write_png(tmp_path, XDIM, YDIM) _write_png(tmp_path, XDIM, YDIM)
def test_2gpx(tmp_path): def test_2gpx(tmp_path: Path) -> None:
"""failed prepatch""" """failed prepatch"""
_write_png(tmp_path, XDIM, XDIM) _write_png(tmp_path, XDIM, XDIM)

View File

@ -1,3 +1,5 @@
from __future__ import annotations
import pytest import pytest
from PIL import Image from PIL import Image
@ -5,7 +7,7 @@ from PIL import Image
TEST_FILE = "Tests/images/libtiff_segfault.tif" TEST_FILE = "Tests/images/libtiff_segfault.tif"
def test_libtiff_segfault(): def test_libtiff_segfault() -> None:
"""This test should not segfault. It will on Pillow <= 3.1.0 and """This test should not segfault. It will on Pillow <= 3.1.0 and
libtiff >= 4.0.0 libtiff >= 4.0.0
""" """

View File

@ -1,3 +1,5 @@
from __future__ import annotations
import zlib import zlib
from io import BytesIO from io import BytesIO
@ -6,7 +8,7 @@ from PIL import Image, ImageFile, PngImagePlugin
TEST_FILE = "Tests/images/png_decompression_dos.png" TEST_FILE = "Tests/images/png_decompression_dos.png"
def test_ignore_dos_text(): def test_ignore_dos_text() -> None:
ImageFile.LOAD_TRUNCATED_IMAGES = True ImageFile.LOAD_TRUNCATED_IMAGES = True
try: try:
@ -22,7 +24,7 @@ def test_ignore_dos_text():
assert len(s) < 1024 * 1024, "Text chunk larger than 1M" assert len(s) < 1024 * 1024, "Text chunk larger than 1M"
def test_dos_text(): def test_dos_text() -> None:
try: try:
im = Image.open(TEST_FILE) im = Image.open(TEST_FILE)
im.load() im.load()
@ -34,7 +36,7 @@ def test_dos_text():
assert len(s) < 1024 * 1024, "Text chunk larger than 1M" assert len(s) < 1024 * 1024, "Text chunk larger than 1M"
def test_dos_total_memory(): def test_dos_total_memory() -> None:
im = Image.new("L", (1, 1)) im = Image.new("L", (1, 1))
compressed_data = zlib.compress(b"a" * 1024 * 1023) compressed_data = zlib.compress(b"a" * 1024 * 1023)
@ -51,7 +53,7 @@ def test_dos_total_memory():
try: try:
im2 = Image.open(b) im2 = Image.open(b)
except ValueError as msg: except ValueError as msg:
assert "Too much memory" in msg assert "Too much memory" in str(msg)
return return
total_len = 0 total_len = 0

View File

@ -1,3 +1,5 @@
from __future__ import annotations
import sys import sys
from pathlib import Path from pathlib import Path

43
Tests/check_wheel.py Normal file
View File

@ -0,0 +1,43 @@
from __future__ import annotations
import sys
from PIL import features
def test_wheel_modules() -> None:
expected_modules = {"pil", "tkinter", "freetype2", "littlecms2", "webp"}
# tkinter is not available in cibuildwheel installed CPython on Windows
try:
import tkinter
assert tkinter
except ImportError:
expected_modules.remove("tkinter")
assert set(features.get_supported_modules()) == expected_modules
def test_wheel_codecs() -> None:
expected_codecs = {"jpg", "jpg_2000", "zlib", "libtiff"}
assert set(features.get_supported_codecs()) == expected_codecs
def test_wheel_features() -> None:
expected_features = {
"webp_anim",
"webp_mux",
"transp_webp",
"raqm",
"fribidi",
"harfbuzz",
"libjpeg_turbo",
"xcb",
}
if sys.platform == "win32":
expected_features.remove("xcb")
assert set(features.get_supported_features()) == expected_features

View File

@ -1,7 +1,11 @@
from __future__ import annotations
import io import io
import pytest
def pytest_report_header(config):
def pytest_report_header(config: pytest.Config) -> str:
try: try:
from PIL import features from PIL import features
@ -12,7 +16,7 @@ def pytest_report_header(config):
return f"pytest_report_header failed: {e}" return f"pytest_report_header failed: {e}"
def pytest_configure(config): def pytest_configure(config: pytest.Config) -> None:
config.addinivalue_line( config.addinivalue_line(
"markers", "markers",
"pil_noop_mark: A conditional mark where nothing special happens", "pil_noop_mark: A conditional mark where nothing special happens",

View File

@ -1,4 +1,6 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
from __future__ import annotations
import base64 import base64
import os import os

Binary file not shown.

Binary file not shown.

View File

@ -2,7 +2,6 @@
NotoNastaliqUrdu-Regular.ttf and NotoSansSymbols-Regular.ttf, from https://github.com/googlei18n/noto-fonts NotoNastaliqUrdu-Regular.ttf and NotoSansSymbols-Regular.ttf, from https://github.com/googlei18n/noto-fonts
NotoSans-Regular.ttf, from https://www.google.com/get/noto/ NotoSans-Regular.ttf, from https://www.google.com/get/noto/
NotoSansJP-Thin.otf, from https://www.google.com/get/noto/help/cjk/ NotoSansJP-Thin.otf, from https://www.google.com/get/noto/help/cjk/
NotoColorEmoji.ttf, from https://github.com/googlefonts/noto-emoji
AdobeVFPrototype.ttf, from https://github.com/adobe-fonts/adobe-variable-font-prototype AdobeVFPrototype.ttf, from https://github.com/adobe-fonts/adobe-variable-font-prototype
TINY5x3GX.ttf, from http://velvetyne.fr/fonts/tiny TINY5x3GX.ttf, from http://velvetyne.fr/fonts/tiny
ArefRuqaa-Regular.ttf, from https://github.com/google/fonts/tree/master/ofl/arefruqaa ArefRuqaa-Regular.ttf, from https://github.com/google/fonts/tree/master/ofl/arefruqaa
@ -25,3 +24,5 @@ FreeMono.ttf is licensed under GPLv3.
10x20-ISO8859-1.pcf, from https://packages.ubuntu.com/xenial/xfonts-base 10x20-ISO8859-1.pcf, from https://packages.ubuntu.com/xenial/xfonts-base
"Public domain font. Share and enjoy." "Public domain font. Share and enjoy."
CBDTTestFont.ttf and EBDTTestFont.ttf from https://github.com/nulano/font-tests are public domain.

Binary file not shown.

View File

@ -1,14 +1,17 @@
""" """
Helper functions. Helper functions.
""" """
from __future__ import annotations
import logging import logging
import os import os
import shutil import shutil
import subprocess
import sys import sys
import sysconfig import sysconfig
import tempfile import tempfile
from io import BytesIO from io import BytesIO
from typing import Any, Callable, Sequence
import pytest import pytest
from packaging.version import parse as parse_version from packaging.version import parse as parse_version
@ -17,42 +20,31 @@ from PIL import Image, ImageMath, features
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
uploader = None
HAS_UPLOADER = False
if os.environ.get("SHOW_ERRORS"): if os.environ.get("SHOW_ERRORS"):
# local img.show for errors. uploader = "show"
HAS_UPLOADER = True elif "GITHUB_ACTIONS" in os.environ:
uploader = "github_actions"
class test_image_results:
@staticmethod def upload(a: Image.Image, b: Image.Image) -> str | None:
def upload(a, b): if uploader == "show":
# local img.show for errors.
a.show() a.show()
b.show() b.show()
elif uploader == "github_actions":
elif "GITHUB_ACTIONS" in os.environ:
HAS_UPLOADER = True
class test_image_results:
@staticmethod
def upload(a, b):
dir_errors = os.path.join(os.path.dirname(__file__), "errors") dir_errors = os.path.join(os.path.dirname(__file__), "errors")
os.makedirs(dir_errors, exist_ok=True) os.makedirs(dir_errors, exist_ok=True)
tmpdir = tempfile.mkdtemp(dir=dir_errors) tmpdir = tempfile.mkdtemp(dir=dir_errors)
a.save(os.path.join(tmpdir, "a.png")) a.save(os.path.join(tmpdir, "a.png"))
b.save(os.path.join(tmpdir, "b.png")) b.save(os.path.join(tmpdir, "b.png"))
return tmpdir return tmpdir
return None
else:
try:
import test_image_results
HAS_UPLOADER = True
except ImportError:
pass
def convert_to_comparable(a, b): def convert_to_comparable(
a: Image.Image, b: Image.Image
) -> tuple[Image.Image, Image.Image]:
new_a, new_b = a, b new_a, new_b = a, b
if a.mode == "P": if a.mode == "P":
new_a = Image.new("L", a.size) new_a = Image.new("L", a.size)
@ -65,14 +57,18 @@ def convert_to_comparable(a, b):
return new_a, new_b return new_a, new_b
def assert_deep_equal(a, b, msg=None): def assert_deep_equal(
a: Sequence[Any], b: Sequence[Any], msg: str | None = None
) -> None:
try: try:
assert len(a) == len(b), msg or f"got length {len(a)}, expected {len(b)}" assert len(a) == len(b), msg or f"got length {len(a)}, expected {len(b)}"
except Exception: except Exception:
assert a == b, msg assert a == b, msg
def assert_image(im, mode, size, msg=None): def assert_image(
im: Image.Image, mode: str, size: tuple[int, int], msg: str | None = None
) -> None:
if mode is not None: if mode is not None:
assert im.mode == mode, ( assert im.mode == mode, (
msg or f"got mode {repr(im.mode)}, expected {repr(mode)}" msg or f"got mode {repr(im.mode)}, expected {repr(mode)}"
@ -84,28 +80,32 @@ def assert_image(im, mode, size, msg=None):
) )
def assert_image_equal(a, b, msg=None): def assert_image_equal(a: Image.Image, b: Image.Image, msg: str | None = None) -> None:
assert a.mode == b.mode, msg or f"got mode {repr(a.mode)}, expected {repr(b.mode)}" assert a.mode == b.mode, msg or f"got mode {repr(a.mode)}, expected {repr(b.mode)}"
assert a.size == b.size, msg or f"got size {repr(a.size)}, expected {repr(b.size)}" assert a.size == b.size, msg or f"got size {repr(a.size)}, expected {repr(b.size)}"
if a.tobytes() != b.tobytes(): if a.tobytes() != b.tobytes():
if HAS_UPLOADER:
try: try:
url = test_image_results.upload(a, b) url = upload(a, b)
if url:
logger.error("URL for test images: %s", url) logger.error("URL for test images: %s", url)
except Exception: except Exception:
pass pass
assert False, msg or "got different content" pytest.fail(msg or "got different content")
def assert_image_equal_tofile(a, filename, msg=None, mode=None): def assert_image_equal_tofile(
a: Image.Image, filename: str, msg: str | None = None, mode: str | None = None
) -> None:
with Image.open(filename) as img: with Image.open(filename) as img:
if mode: if mode:
img = img.convert(mode) img = img.convert(mode)
assert_image_equal(a, img, msg) assert_image_equal(a, img, msg)
def assert_image_similar(a, b, epsilon, msg=None): def assert_image_similar(
a: Image.Image, b: Image.Image, epsilon: float, msg: str | None = None
) -> None:
assert a.mode == b.mode, msg or f"got mode {repr(a.mode)}, expected {repr(b.mode)}" assert a.mode == b.mode, msg or f"got mode {repr(a.mode)}, expected {repr(b.mode)}"
assert a.size == b.size, msg or f"got size {repr(a.size)}, expected {repr(b.size)}" assert a.size == b.size, msg or f"got size {repr(a.size)}, expected {repr(b.size)}"
@ -123,55 +123,68 @@ def assert_image_similar(a, b, epsilon, msg=None):
+ f" average pixel value difference {ave_diff:.4f} > epsilon {epsilon:.4f}" + f" average pixel value difference {ave_diff:.4f} > epsilon {epsilon:.4f}"
) )
except Exception as e: except Exception as e:
if HAS_UPLOADER:
try: try:
url = test_image_results.upload(a, b) url = upload(a, b)
if url:
logger.exception("URL for test images: %s", url) logger.exception("URL for test images: %s", url)
except Exception: except Exception:
pass pass
raise e raise e
def assert_image_similar_tofile(a, filename, epsilon, msg=None, mode=None): def assert_image_similar_tofile(
a: Image.Image,
filename: str,
epsilon: float,
msg: str | None = None,
mode: str | None = None,
) -> None:
with Image.open(filename) as img: with Image.open(filename) as img:
if mode: if mode:
img = img.convert(mode) img = img.convert(mode)
assert_image_similar(a, img, epsilon, msg) assert_image_similar(a, img, epsilon, msg)
def assert_all_same(items, msg=None): def assert_all_same(items: Sequence[Any], msg: str | None = None) -> None:
assert items.count(items[0]) == len(items), msg assert items.count(items[0]) == len(items), msg
def assert_not_all_same(items, msg=None): def assert_not_all_same(items: Sequence[Any], msg: str | None = None) -> None:
assert items.count(items[0]) != len(items), msg assert items.count(items[0]) != len(items), msg
def assert_tuple_approx_equal(actuals, targets, threshold, msg): def assert_tuple_approx_equal(
actuals: Sequence[int], targets: tuple[int, ...], threshold: int, msg: str
) -> None:
"""Tests if actuals has values within threshold from targets""" """Tests if actuals has values within threshold from targets"""
value = True
for i, target in enumerate(targets): for i, target in enumerate(targets):
value *= target - threshold <= actuals[i] <= target + threshold if not (target - threshold <= actuals[i] <= target + threshold):
pytest.fail(msg + ": " + repr(actuals) + " != " + repr(targets))
assert value, msg + ": " + repr(actuals) + " != " + repr(targets)
def skip_unless_feature(feature): def skip_unless_feature(feature: str) -> pytest.MarkDecorator:
reason = f"{feature} not available" reason = f"{feature} not available"
return pytest.mark.skipif(not features.check(feature), reason=reason) return pytest.mark.skipif(not features.check(feature), reason=reason)
def skip_unless_feature_version(feature, version_required, reason=None): def skip_unless_feature_version(
feature: str, required: str, reason: str | None = None
) -> pytest.MarkDecorator:
if not features.check(feature): if not features.check(feature):
return pytest.mark.skip(f"{feature} not available") return pytest.mark.skip(f"{feature} not available")
if reason is None: if reason is None:
reason = f"{feature} is older than {version_required}" reason = f"{feature} is older than {required}"
version_required = parse_version(version_required) version_required = parse_version(required)
version_available = parse_version(features.version(feature)) version_available = parse_version(features.version(feature))
return pytest.mark.skipif(version_available < version_required, reason=reason) return pytest.mark.skipif(version_available < version_required, reason=reason)
def mark_if_feature_version(mark, feature, version_blacklist, reason=None): def mark_if_feature_version(
mark: pytest.MarkDecorator,
feature: str,
version_blacklist: str,
reason: str | None = None,
) -> pytest.MarkDecorator:
if not features.check(feature): if not features.check(feature):
return pytest.mark.pil_noop_mark() return pytest.mark.pil_noop_mark()
if reason is None: if reason is None:
@ -192,7 +205,7 @@ class PillowLeakTestCase:
iterations = 100 # count iterations = 100 # count
mem_limit = 512 # k mem_limit = 512 # k
def _get_mem_usage(self): def _get_mem_usage(self) -> float:
""" """
Gets the RUSAGE memory usage, returns in K. Encapsulates the difference Gets the RUSAGE memory usage, returns in K. Encapsulates the difference
between macOS and Linux rss reporting between macOS and Linux rss reporting
@ -214,7 +227,7 @@ class PillowLeakTestCase:
# This is the maximum resident set size used (in kilobytes). # This is the maximum resident set size used (in kilobytes).
return mem # Kb return mem # Kb
def _test_leak(self, core): def _test_leak(self, core: Callable[[], None]) -> None:
start_mem = self._get_mem_usage() start_mem = self._get_mem_usage()
for cycle in range(self.iterations): for cycle in range(self.iterations):
core() core()
@ -226,17 +239,17 @@ class PillowLeakTestCase:
# helpers # helpers
def fromstring(data): def fromstring(data: bytes) -> Image.Image:
return Image.open(BytesIO(data)) return Image.open(BytesIO(data))
def tostring(im, string_format, **options): def tostring(im: Image.Image, string_format: str, **options: dict[str, Any]) -> bytes:
out = BytesIO() out = BytesIO()
im.save(out, string_format, **options) im.save(out, string_format, **options)
return out.getvalue() return out.getvalue()
def hopper(mode=None, cache={}): def hopper(mode: str | None = None, cache: dict[str, Image.Image] = {}) -> Image.Image:
if mode is None: if mode is None:
# Always return fresh not-yet-loaded version of image. # Always return fresh not-yet-loaded version of image.
# Operations on not-yet-loaded images is separate class of errors # Operations on not-yet-loaded images is separate class of errors
@ -257,19 +270,31 @@ def hopper(mode=None, cache={}):
return im.copy() return im.copy()
def djpeg_available(): def djpeg_available() -> bool:
return bool(shutil.which("djpeg")) if shutil.which("djpeg"):
try:
subprocess.check_call(["djpeg", "-version"])
return True
except subprocess.CalledProcessError: # pragma: no cover
return False
return False
def cjpeg_available(): def cjpeg_available() -> bool:
return bool(shutil.which("cjpeg")) if shutil.which("cjpeg"):
try:
subprocess.check_call(["cjpeg", "-version"])
return True
except subprocess.CalledProcessError: # pragma: no cover
return False
return False
def netpbm_available(): def netpbm_available() -> bool:
return bool(shutil.which("ppmquant") and shutil.which("ppmtogif")) return bool(shutil.which("ppmquant") and shutil.which("ppmtogif"))
def magick_command(): def magick_command() -> list[str] | None:
if sys.platform == "win32": if sys.platform == "win32":
magickhome = os.environ.get("MAGICK_HOME") magickhome = os.environ.get("MAGICK_HOME")
if magickhome: if magickhome:
@ -286,47 +311,48 @@ def magick_command():
return imagemagick return imagemagick
if graphicsmagick and shutil.which(graphicsmagick[0]): if graphicsmagick and shutil.which(graphicsmagick[0]):
return graphicsmagick return graphicsmagick
return None
def on_appveyor(): def on_appveyor() -> bool:
return "APPVEYOR" in os.environ return "APPVEYOR" in os.environ
def on_github_actions(): def on_github_actions() -> bool:
return "GITHUB_ACTIONS" in os.environ return "GITHUB_ACTIONS" in os.environ
def on_ci(): def on_ci() -> bool:
# GitHub Actions and AppVeyor have "CI" # GitHub Actions and AppVeyor have "CI"
return "CI" in os.environ return "CI" in os.environ
def is_big_endian(): def is_big_endian() -> bool:
return sys.byteorder == "big" return sys.byteorder == "big"
def is_ppc64le(): def is_ppc64le() -> bool:
import platform import platform
return platform.machine() == "ppc64le" return platform.machine() == "ppc64le"
def is_win32(): def is_win32() -> bool:
return sys.platform.startswith("win32") return sys.platform.startswith("win32")
def is_pypy(): def is_pypy() -> bool:
return hasattr(sys, "pypy_translation_info") return hasattr(sys, "pypy_translation_info")
def is_mingw(): def is_mingw() -> bool:
return sysconfig.get_platform() == "mingw" return sysconfig.get_platform() == "mingw"
class CachedProperty: class CachedProperty:
def __init__(self, func): def __init__(self, func: Callable[[Any], None]) -> None:
self.func = func self.func = func
def __get__(self, instance, cls=None): def __get__(self, instance: Any, cls: type[Any] | None = None) -> Any:
result = instance.__dict__[self.func.__name__] = self.func(instance) result = instance.__dict__[self.func.__name__] = self.func(instance)
return result return result

Binary file not shown.

After

Width:  |  Height:  |  Size: 233 B

View File

Before

Width:  |  Height:  |  Size: 331 B

After

Width:  |  Height:  |  Size: 331 B

View File

Before

Width:  |  Height:  |  Size: 668 B

After

Width:  |  Height:  |  Size: 668 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 B

BIN
Tests/images/bc1.dds Executable file

Binary file not shown.

BIN
Tests/images/bc1_typeless.dds Executable file

Binary file not shown.

Binary file not shown.

BIN
Tests/images/bc4_unorm.dds Normal file

Binary file not shown.

BIN
Tests/images/bc4_unorm.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 982 B

BIN
Tests/images/bc4u.dds Normal file

Binary file not shown.

BIN
Tests/images/bgr15.dds Normal file

Binary file not shown.

BIN
Tests/images/bgr15.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 387 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

BIN
Tests/images/cbdt.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 348 B

BIN
Tests/images/cbdt_mask.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 367 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

View File

@ -1,5 +1,3 @@
#!/usr/bin/gnuplot
#This is the script that was used to create our sample EPS files #This is the script that was used to create our sample EPS files
#We used the following version of the gnuplot program #We used the following version of the gnuplot program
#G N U P L O T #G N U P L O T

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

BIN
Tests/images/hopper.pfm Normal file

Binary file not shown.

BIN
Tests/images/hopper_be.pfm Normal file

Binary file not shown.

View File

Before

Width:  |  Height:  |  Size: 6.1 KiB

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 364 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.9 KiB

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.9 KiB

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

View File

@ -15,7 +15,7 @@
# #
################################################################################ ################################################################################
python3 setup.py build --build-base=/tmp/build install python3 -m pip install .
# Build fuzzers in $OUT. # Build fuzzers in $OUT.
for fuzzer in $(find $SRC -name 'fuzz_*.py'); do for fuzzer in $(find $SRC -name 'fuzz_*.py'); do

View File

@ -23,7 +23,7 @@ with atheris.instrument_imports():
import fuzzers import fuzzers
def TestOneInput(data): def TestOneInput(data: bytes) -> None:
try: try:
fuzzers.fuzz_font(data) fuzzers.fuzz_font(data)
except Exception: except Exception:
@ -32,7 +32,7 @@ def TestOneInput(data):
pass pass
def main(): def main() -> None:
fuzzers.enable_decompressionbomb_error() fuzzers.enable_decompressionbomb_error()
atheris.Setup(sys.argv, TestOneInput) atheris.Setup(sys.argv, TestOneInput)
atheris.Fuzz() atheris.Fuzz()

View File

@ -1,5 +1,3 @@
#!/usr/bin/python3
# Copyright 2020 Google LLC # Copyright 2020 Google LLC
# #
# Licensed under the Apache License, Version 2.0 (the "License"); # Licensed under the Apache License, Version 2.0 (the "License");
@ -23,7 +21,7 @@ with atheris.instrument_imports():
import fuzzers import fuzzers
def TestOneInput(data): def TestOneInput(data: bytes) -> None:
try: try:
fuzzers.fuzz_image(data) fuzzers.fuzz_image(data)
except Exception: except Exception:
@ -32,7 +30,7 @@ def TestOneInput(data):
pass pass
def main(): def main() -> None:
fuzzers.enable_decompressionbomb_error() fuzzers.enable_decompressionbomb_error()
atheris.Setup(sys.argv, TestOneInput) atheris.Setup(sys.argv, TestOneInput)
atheris.Fuzz() atheris.Fuzz()

View File

@ -1,21 +1,23 @@
from __future__ import annotations
import io import io
import warnings import warnings
from PIL import Image, ImageDraw, ImageFile, ImageFilter, ImageFont from PIL import Image, ImageDraw, ImageFile, ImageFilter, ImageFont
def enable_decompressionbomb_error(): def enable_decompressionbomb_error() -> None:
ImageFile.LOAD_TRUNCATED_IMAGES = True ImageFile.LOAD_TRUNCATED_IMAGES = True
warnings.filterwarnings("ignore") warnings.filterwarnings("ignore")
warnings.simplefilter("error", Image.DecompressionBombWarning) warnings.simplefilter("error", Image.DecompressionBombWarning)
def disable_decompressionbomb_error(): def disable_decompressionbomb_error() -> None:
ImageFile.LOAD_TRUNCATED_IMAGES = False ImageFile.LOAD_TRUNCATED_IMAGES = False
warnings.resetwarnings() warnings.resetwarnings()
def fuzz_image(data): def fuzz_image(data: bytes) -> None:
# This will fail on some images in the corpus, as we have many # This will fail on some images in the corpus, as we have many
# invalid images in the test suite. # invalid images in the test suite.
with Image.open(io.BytesIO(data)) as im: with Image.open(io.BytesIO(data)) as im:
@ -24,7 +26,7 @@ def fuzz_image(data):
im.save(io.BytesIO(), "BMP") im.save(io.BytesIO(), "BMP")
def fuzz_font(data): def fuzz_font(data: bytes) -> None:
wrapper = io.BytesIO(data) wrapper = io.BytesIO(data)
try: try:
font = ImageFont.truetype(wrapper) font = ImageFont.truetype(wrapper)

View File

@ -1,3 +1,5 @@
from __future__ import annotations
import subprocess import subprocess
import sys import sys
@ -22,7 +24,7 @@ if features.check("libjpeg_turbo"):
"path", "path",
subprocess.check_output("find Tests/images -type f", shell=True).split(b"\n"), subprocess.check_output("find Tests/images -type f", shell=True).split(b"\n"),
) )
def test_fuzz_images(path): def test_fuzz_images(path: str) -> None:
fuzzers.enable_decompressionbomb_error() fuzzers.enable_decompressionbomb_error()
try: try:
with open(path, "rb") as f: with open(path, "rb") as f:
@ -53,7 +55,7 @@ def test_fuzz_images(path):
@pytest.mark.parametrize( @pytest.mark.parametrize(
"path", subprocess.check_output("find Tests/fonts -type f", shell=True).split(b"\n") "path", subprocess.check_output("find Tests/fonts -type f", shell=True).split(b"\n")
) )
def test_fuzz_fonts(path): def test_fuzz_fonts(path: str) -> None:
if not path: if not path:
return return
with open(path, "rb") as f: with open(path, "rb") as f:

View File

@ -1,7 +1,9 @@
from __future__ import annotations
from PIL import Image from PIL import Image
def test_sanity(): def test_sanity() -> None:
# Make sure we have the binary extension # Make sure we have the binary extension
Image.core.new("L", (100, 100)) Image.core.new("L", (100, 100))

View File

@ -1,12 +1,14 @@
from __future__ import annotations
from PIL import _binary from PIL import _binary
def test_standard(): def test_standard() -> None:
assert _binary.i8(b"*") == 42 assert _binary.i8(b"*") == 42
assert _binary.o8(42) == b"*" assert _binary.o8(42) == b"*"
def test_little_endian(): def test_little_endian() -> None:
assert _binary.i16le(b"\xff\xff\x00\x00") == 65535 assert _binary.i16le(b"\xff\xff\x00\x00") == 65535
assert _binary.i32le(b"\xff\xff\x00\x00") == 65535 assert _binary.i32le(b"\xff\xff\x00\x00") == 65535
@ -14,7 +16,7 @@ def test_little_endian():
assert _binary.o32le(65535) == b"\xff\xff\x00\x00" assert _binary.o32le(65535) == b"\xff\xff\x00\x00"
def test_big_endian(): def test_big_endian() -> None:
assert _binary.i16be(b"\x00\x00\xff\xff") == 0 assert _binary.i16be(b"\x00\x00\xff\xff") == 0
assert _binary.i32be(b"\x00\x00\xff\xff") == 65535 assert _binary.i32be(b"\x00\x00\xff\xff") == 65535

View File

@ -1,3 +1,5 @@
from __future__ import annotations
import os import os
import warnings import warnings
@ -8,13 +10,13 @@ from .helper import assert_image_similar
base = os.path.join("Tests", "images", "bmp") base = os.path.join("Tests", "images", "bmp")
def get_files(d, ext=".bmp"): def get_files(d, ext: str = ".bmp"):
return [ return [
os.path.join(base, d, f) for f in os.listdir(os.path.join(base, d)) if ext in f os.path.join(base, d, f) for f in os.listdir(os.path.join(base, d)) if ext in f
] ]
def test_bad(): def test_bad() -> None:
"""These shouldn't crash/dos, but they shouldn't return anything """These shouldn't crash/dos, but they shouldn't return anything
either""" either"""
for f in get_files("b"): for f in get_files("b"):
@ -54,7 +56,7 @@ def test_questionable():
raise raise
def test_good(): def test_good() -> None:
"""These should all work. There's a set of target files in the """These should all work. There's a set of target files in the
html directory that we can compare against.""" html directory that we can compare against."""

View File

@ -1,3 +1,5 @@
from __future__ import annotations
import pytest import pytest
from PIL import Image, ImageFilter from PIL import Image, ImageFilter
@ -14,18 +16,18 @@ sample.putdata(sum([
# fmt: on # fmt: on
def test_imageops_box_blur(): def test_imageops_box_blur() -> None:
i = sample.filter(ImageFilter.BoxBlur(1)) i = sample.filter(ImageFilter.BoxBlur(1))
assert i.mode == sample.mode assert i.mode == sample.mode
assert i.size == sample.size assert i.size == sample.size
assert isinstance(i, Image.Image) assert isinstance(i, Image.Image)
def box_blur(image, radius=1, n=1): def box_blur(image, radius: int = 1, n: int = 1):
return image._new(image.im.box_blur((radius, radius), n)) return image._new(image.im.box_blur((radius, radius), n))
def assert_image(im, data, delta=0): def assert_image(im, data, delta: int = 0) -> None:
it = iter(im.getdata()) it = iter(im.getdata())
for data_row in data: for data_row in data:
im_row = [next(it) for _ in range(im.size[0])] im_row = [next(it) for _ in range(im.size[0])]
@ -35,7 +37,7 @@ def assert_image(im, data, delta=0):
next(it) next(it)
def assert_blur(im, radius, data, passes=1, delta=0): def assert_blur(im, radius, data, passes: int = 1, delta: int = 0) -> None:
# check grayscale image # check grayscale image
assert_image(box_blur(im, radius, passes), data, delta) assert_image(box_blur(im, radius, passes), data, delta)
rgba = Image.merge("RGBA", (im, im, im, im)) rgba = Image.merge("RGBA", (im, im, im, im))
@ -43,7 +45,7 @@ def assert_blur(im, radius, data, passes=1, delta=0):
assert_image(band, data, delta) assert_image(band, data, delta)
def test_color_modes(): def test_color_modes() -> None:
with pytest.raises(ValueError): with pytest.raises(ValueError):
box_blur(sample.convert("1")) box_blur(sample.convert("1"))
with pytest.raises(ValueError): with pytest.raises(ValueError):
@ -63,7 +65,7 @@ def test_color_modes():
box_blur(sample.convert("YCbCr")) box_blur(sample.convert("YCbCr"))
def test_radius_0(): def test_radius_0() -> None:
assert_blur( assert_blur(
sample, sample,
0, 0,
@ -79,7 +81,7 @@ def test_radius_0():
) )
def test_radius_0_02(): def test_radius_0_02() -> None:
assert_blur( assert_blur(
sample, sample,
0.02, 0.02,
@ -96,7 +98,7 @@ def test_radius_0_02():
) )
def test_radius_0_05(): def test_radius_0_05() -> None:
assert_blur( assert_blur(
sample, sample,
0.05, 0.05,
@ -113,7 +115,7 @@ def test_radius_0_05():
) )
def test_radius_0_1(): def test_radius_0_1() -> None:
assert_blur( assert_blur(
sample, sample,
0.1, 0.1,
@ -130,7 +132,7 @@ def test_radius_0_1():
) )
def test_radius_0_5(): def test_radius_0_5() -> None:
assert_blur( assert_blur(
sample, sample,
0.5, 0.5,
@ -147,7 +149,7 @@ def test_radius_0_5():
) )
def test_radius_1(): def test_radius_1() -> None:
assert_blur( assert_blur(
sample, sample,
1, 1,
@ -164,7 +166,7 @@ def test_radius_1():
) )
def test_radius_1_5(): def test_radius_1_5() -> None:
assert_blur( assert_blur(
sample, sample,
1.5, 1.5,
@ -181,7 +183,7 @@ def test_radius_1_5():
) )
def test_radius_bigger_then_half(): def test_radius_bigger_then_half() -> None:
assert_blur( assert_blur(
sample, sample,
3, 3,
@ -198,7 +200,7 @@ def test_radius_bigger_then_half():
) )
def test_radius_bigger_then_width(): def test_radius_bigger_then_width() -> None:
assert_blur( assert_blur(
sample, sample,
10, 10,
@ -213,7 +215,7 @@ def test_radius_bigger_then_width():
) )
def test_extreme_large_radius(): def test_extreme_large_radius() -> None:
assert_blur( assert_blur(
sample, sample,
600, 600,
@ -228,7 +230,7 @@ def test_extreme_large_radius():
) )
def test_two_passes(): def test_two_passes() -> None:
assert_blur( assert_blur(
sample, sample,
1, 1,
@ -246,7 +248,7 @@ def test_two_passes():
) )
def test_three_passes(): def test_three_passes() -> None:
assert_blur( assert_blur(
sample, sample,
1, 1,

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